fix(lifecycle): retired agents keep their Resources; admission requires runtime-record absence only - #439
Merged
schickling merged 1 commit intoSep 4, 2026
Conversation
…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
marked this pull request as ready for review
September 4, 2026 10:31
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
st2 agent desired-state <id> retiredis meant to be runtime teardown only: a retired agent keeps its Agent Spec and itsresources/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 (retiredleft "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_specsondesired_state.is_running():resources/are untouched — resources are declaration metadata, not runtime records. Un-retiring restores the exact resources;st2 doctorstill 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.mdF13/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 withresourcebindings (incl. awork://URI) set to retired admits (noTaskIdentityAdmissionError), 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 keepsresourcebindings, adds no resource line, and mutates nothing.tests/agent_desired_state.rs::legacy_retirement_reads_and_authoring_preserves_resources— legacyretired #truereads as retired with its resources; authoring collapses to the canonicaldesired-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 hermeticdoChecksuite that the aarch64-darwin job runs and the Linuxnix flake checkbuilds) is green — every test targetok, 0 failed. In the dev shell,cargo test --libis 677 passed and the reconcile/doctor/agent_desired_state suites are green.cargo fmt/clippyare 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-
PATHdev-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 callslive_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
agent_identitysessionagent_personaagent_supervisoragent_toolagent_tool_versionagent_runtimetooling_profile