Skip to content

fix(lifecycle): retired agents keep their Resources; admission requires runtime-record absence only - #439

Merged
schickling merged 1 commit into
mainfrom
schickling-assistant/2026-09-04-b2-retired-keeps-resources
Sep 4, 2026
Merged

fix(lifecycle): retired agents keep their Resources; admission requires runtime-record absence only#439
schickling merged 1 commit into
mainfrom
schickling-assistant/2026-09-04-b2-retired-keeps-resources

Conversation

@schickling-assistant

@schickling-assistant schickling-assistant commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Problem

st2 agent desired-state <id> retired is meant to be runtime teardown only: a retired agent keeps its Agent Spec and its resources/ byte-identical so it can be un-retired later with its decisions/tracing intact. But retirement did not actually strip its resource work, while admission only ever required runtime-record absence — the split behind dotfiles#1535 (retired left "resource work" running; supervisor crash-loop, NRestarts up to 781).

Root cause: live_resync_specs — which drives both resync installs and resource-Profile subscriptions — proved liveness from the live-session snapshot alone and explicitly ignored desired state. A retired (or suspended) agent whose canonical seat is still alive mid-teardown was therefore still treated as a live subscription owner, so its resync/resource bindings were re-installed every reconcile pass and lingered after retirement.

desired_bindings (resource-Profile) fully trusts this set and never re-checks desired state, so this was the single gate that let retired "resource work" survive.

Change

Gate live_resync_specs on desired_state.is_running():

  • A non-running (suspended/retired) desired state is a negative gate — no live subscription work even while the seat is alive, so resync installs and resource-Profile bindings are stripped this pass rather than lingering until the seat dies. This is the derived-companion lifecycle contract ("stop while suspended/retired") extended to the two supervisor-managed works that are not tasks.
  • Positive liveness is unchanged: a running agent still needs a proven live/launched/restarted canonical seat; desired state and whole-spec adoption are never positive evidence.
  • The declaration and its resources/ are untouched — resources are declaration metadata, not runtime records. Un-retiring restores the exact resources; st2 doctor still requires only runtime-record (task/PTY) absence and adds no resource-specific check.

This aligns with the already-recorded VRS design (decision 0003; docs/vrs/02-agent-spec/spec.md F13/F13 note "inbox, archive, context, resources … remain addressable"; DOCTOR-R06 "does not require … active-declaration checks") — the code now closes the one runtime gap.

Gates / evidence

  • src/run.rs::subscription_eligibility_excludes_non_running_agents_even_with_a_live_seat — retired and suspended agents with a live seat are excluded from subscription/resync eligibility; only the running peer remains.
  • tests/reconcile.rs::retired_agent_keeps_resources_and_still_reconciles_to_zero_live_tasks — a spec with resource bindings (incl. a work:// URI) set to retired admits (no TaskIdentityAdmissionError), tears down the live seat, settles retirement, launches nothing; un-retiring relaunches with resources intact.
  • tests/doctor.rs::retired_declaration_with_resources_is_healthy_when_tasks_are_absent — doctor stays healthy for a retired agent that keeps resource bindings, adds no resource line, and mutates nothing.
  • tests/agent_desired_state.rs::legacy_retirement_reads_and_authoring_preserves_resources — legacy retired #true reads as retired with its resources; authoring collapses to the canonical desired-state "retired" reason=… form on the write path while leaving every resource byte-identical (read compatibility preserved).

Gate as CI runs it: nix build .#st2 (the hermetic doCheck suite that the aarch64-darwin job runs and the Linux nix flake check builds) is green — every test target ok, 0 failed. In the dev shell, cargo test --lib is 677 passed and the reconcile/doctor/agent_desired_state suites are green.

cargo fmt/clippy are intentionally not gated on this tree (flake.nix: a repo-wide formatting gate "would fight the maintainer's own commits on every rebase"), so no reformatting is included.

Two real-PTY / empty-PATH dev-shell tests fail identically on the clean tree (targeted_reconcile::targeted_once_real_pty_preserves_sibling_generation_across_selected_lifecycle, stream_authoring_cli::external_stream_removal_performs_no_runtime_operation) — pre-existing and environment-specific, not reproducible under the hermetic gate above, and untouched by this change (neither path calls live_resync_specs).

Intentionally out of scope

st2 catalog archive (B3). No INVARIANTS/VRS text change: the existing "Retirement health", "Suspension health", and "Derived companion lifecycle" invariants and the VRS spec already state this behavior; this only makes the runtime match.

Refs: dotfiles#1535, dotfiles#2408.

Posted on behalf of @schickling
field value
agent_identity dev3.direct.omp.v6c4mkm2
session dev3.v6c4mkm2
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.1.2
agent_runtime OMP 18.1.2
tooling_profile dotfiles@7534055

…es runtime-record absence only

A retired (or suspended) agent whose canonical seat is still alive
mid-teardown was still credited as a live subscription owner:
live_resync_specs proved liveness from the session snapshot alone,
ignoring desired state. So its resync installs and resource-Profile
bindings ('resource work') were re-installed each pass and lingered
after retirement, while admission only ever required runtime-record
absence — the exact split behind dotfiles#1535 (retire did not strip
resource work; NRestarts crash-loop).

Gate live_resync_specs on desired-state running: non-running agents own
no live subscription work even with a live seat, so the work is stripped
this pass. Positive liveness still requires a proven seat. The
declaration and its resources/ are untouched — un-retiring restores the
exact resources, and doctor still requires only runtime-record absence.

Tests: retire-with-resources admits and reconciles to zero live tasks,
un-retire relaunches, legacy 'retired #true' reads and canonicalizes
while preserving resources, and doctor stays healthy for a retired agent
that keeps resource bindings.

Refs: dotfiles#1535, dotfiles#2408
agent-identity: dev3.direct.omp.v6c4mkm2
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: OMP
agent-tool-version: 18.1.2
agent-runtime: OMP 18.1.2
tooling-profile: dotfiles@7534055
@schickling-assistant
schickling-assistant marked this pull request as ready for review September 4, 2026 10:31
@schickling
schickling merged commit 02d2aa4 into main Sep 4, 2026
2 checks passed
schickling pushed a commit that referenced this pull request Sep 4, 2026
PR #439 added `retired_catalog_with_resources` to tests/doctor.rs — a
20-line copy of `retired_catalog` differing only in the retirement clause
and two `resource` lines — and duplicated the same two-`resource` KDL
literal inline in tests/agent_desired_state.rs.

Hoist the literal to `support::RETIRED_RESOURCES`, parameterize
`retired_catalog(root, retirement, extra)`, and delete the clone. Net -4
lines with the 20-line clone gone; the fixture literal now exists once.

agent-identity: dev3.direct.omp.v6c4mkm2
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: OMP
agent-tool-version: 18.1.2
agent-runtime: OMP 18.1.2
tooling-profile: dotfiles@7534055
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants