Skip to content

Commit 5a31c64

Browse files
docs(vrs): stage the immutable-id half of decision 0015 behind named triggers
0015 keeps its target. Amendment 1 records that its two halves ship separately, because only one of them answers the Context: the mutable address does, with no migration, no record version, and no activation gate. The ID half buys host-move invariance and live/archive collision attribution — both real, both with no observed instance — so it re-enters on an observation rather than on a schedule: a completed cross-host seat move, a live/archive identity collision, or a UUIDv7 creation call site. The amendment also corrects two premises the implementation was built against. Version-1 readers are additively tolerant by documented policy (`crates/st2-wire/src/lib.rs`), so an additive field is not a version bump; the one genuine cross-build hazard is single-field and about routing. And an ownership key is not an address: an exact selector answers to either immutable key, which is what keeps a running seat, its resync stream, and an interrupted send bound to their own subject across a cutover. DELTA-003 narrows to the ID half and records the two defects that block it: `supervisor_chain::resolve_spec` must accept `effective_id`, and `migrate-ids` must exempt `agent-id-missing` from its own pre-admission gate or the prescribed rollout order deadlocks on the only command that can clear it. 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
1 parent bc92ec6 commit 5a31c64

5 files changed

Lines changed: 142 additions & 37 deletions

File tree

INVARIANTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ materialization, messaging, DING, or presence must preserve them.
2020
| **Prompt catalog convergence** | A resident catalog supervisor observes cooperative transaction commits through a constant-cost catalog-generation watcher and authorized direct Agent Spec publication through an independent declaration watcher. Both channels coalesce into the serialized loop without waiting for the periodic audit interval; failure of one leaves the other and the timer fallback available. Runtime/control noise remains excluded. | `src/watch.rs::catalog_generation_commit_wakes_catalog_watch`; `src/watch.rs::atomic_agent_bundle_publication_wakes_production_shaped_catalog`; `src/run.rs::supervisor_wakes_and_launches_a_new_direct_declaration`; `src/run.rs::failed_watch_installation_keeps_supervisor_on_timer_cadence` |
2121
| **Bounded DING PTY probe churn** | An unsafe or active composer retains its FIFO notice but deferred delivery retries use a bounded backoff, so each inbox poll cannot spawn another short-lived PTY probe. | `src/ding/mod.rs::deferred_delivery_backoff_bounds_short_lived_pty_attempts` |
2222
| **Agent-declared presence discipline** | The shipped bus contract requires agents to declare `busy` before executing work, use `available` only while yielding or ready, and reserve `dnd` for an explicit hold. Both native harnesses materialize that contract. Busy remains observable but does not suppress DING; fresh `dnd` is the only delivery gate. | `tests/native_only.rs::clean_path_executes_the_maintained_native_authoring_guide`; `src/ding/mod.rs::pending_delivery_ignores_busy_but_respects_fresh_dnd_archive_and_retry` |
23-
| **Stable roster JSON** | `st2 agents --json [--enrich]` preserves field names, order, null handling, presence, typed desired state and rationale, the retirement compatibility projection, opaque declared Resource descriptors, origin-timed activity, inbox counts, and the appended `observedState`, `driverDiagnostic`, and `context` objects. Declared presence, desired lifecycle, observed harness state, native-driver degradation, and harness context are independent axes: none is derived from another; a missing observed or context record is `null`, while missing diagnostic evidence is explicitly `absent`, never healthy. | `src/agents.rs::agents_json_has_stable_wire_shape`; `src/agents.rs::agents_json_preserves_opaque_declared_resource_descriptors`; `src/agents.rs::observed_state_joins_declared_presence_without_touching_either`; `src/agents.rs::driver_diagnostic_wire_exposes_failure_and_evidence_age_without_identity_payloads`; `src/agents.rs::context_is_a_fourth_axis_that_survives_an_indeterminate_observed_state`; `tests/status_agents.rs::roster_json_and_human_output_distinguish_retirement_from_presence`; `tests/status_agents.rs::roster_keeps_presence_separate_from_suspended_desired_state`; `tests/status_agents.rs::roster_uses_version_1_origin_time_for_last_activity`; `tests/status_agents.rs::roster_joins_a_real_context_record_independently_of_observed_state` |
23+
| **Stable roster JSON** | `st2 agents --json [--enrich]` preserves field names, order, null handling, presence, typed desired state and rationale, the retirement compatibility projection, opaque declared Resource descriptors, origin-timed activity, inbox counts, and the appended immutable `id`, mutable `address`, nullable `busAddress`, `observedState`, `driverDiagnostic`, and `context` objects. Declared presence, desired lifecycle, observed harness state, native-driver degradation, and harness context are independent axes: none is derived from another; a missing observed or context record is `null`, while missing diagnostic evidence is explicitly `absent`, never healthy. | `src/agents.rs::agents_json_has_stable_wire_shape`; `src/agents.rs::agents_json_preserves_opaque_declared_resource_descriptors`; `src/agents.rs::observed_state_joins_declared_presence_without_touching_either`; `src/agents.rs::driver_diagnostic_wire_exposes_failure_and_evidence_age_without_identity_payloads`; `src/agents.rs::context_is_a_fourth_axis_that_survives_an_indeterminate_observed_state`; `tests/status_agents.rs::roster_json_and_human_output_distinguish_retirement_from_presence`; `tests/status_agents.rs::roster_keeps_presence_separate_from_suspended_desired_state`; `tests/status_agents.rs::roster_uses_version_1_origin_time_for_last_activity`; `tests/status_agents.rs::roster_joins_a_real_context_record_independently_of_observed_state` |
2424
| **Agent-declared presence** | Refresh preserves non-DND declared status and advances the version 1 heartbeat. A missing status starts as `available`. Legacy DND migrates without renewing its hold. Version 1 DND is not refreshed. Stale, malformed, or implausibly future heartbeats read as `unknown`. The outer Codex, Claude, and pi session wrappers own a five-minute heartbeat while their provider remains alive. | `src/status.rs::refresh_preserves_value_and_changes_heartbeat_bytes`; `src/status.rs::refresh_upgrades_legacy_dnd_without_renewing_the_hold`; `src/status.rs::refresh_missing_writes_available_default`; `src/status.rs::version_1_staleness_and_future_skew_are_bounded`; `src/status.rs::malformed_versioned_record_is_unknown_without_mtime_fallback`; `src/claude_session.rs::idle_provider_refreshes_presence_without_mcp_input`; `src/pi_session.rs::idle_pi_provider_refreshes_presence_without_channel_input`; `src/codex_app_server.rs::inbox_fallback_does_not_write_a_fifteen_second_presence_heartbeat` |
2525
| **Scoped delivery-input wakeups** | Native delivery pumps watch only their inputs: the agent's `resources/inbox` subtree and its `status` file. Runtime records written beside them by the pump's own process group — presence temp siblings, the `harness-state` and `harness-context` records with their locks and staged siblings, stream state — never wake delivery, so a producer that writes on every turn boundary cannot pump its own delivery loop. | `src/watch.rs::delivery_watcher_ignores_runtime_records_but_wakes_on_inbox_and_status` |
2626
| **Observed harness state discipline** | The `harness-state` record is written only by the owning session's driver processes — wrapper, channel, or hooks — serialized by a cross-process lock and coalesced against the on-disk record, atomically and byte-distinct on every write that lands, with freshness from its embedded timestamp and never file mtime; restating an unchanged state touches the record only when the refresh cadence is due. `unknown` is derived, never written: staleness, future skew, malformation, an unsupported schema, and a provably dead pty session each read as `unknown` with a distinct reason; an indeterminate liveness probe downgrades nothing; a missing record is no observation rather than `unknown`; no absence derives a definite state. A writer that loses sight of its harness stops heartbeating instead of refreshing a state it cannot see, and a predecessor session's record is never re-stamped. A reaped provider yields a terminal `ended` record carrying its real exit — written before the stop path's SIGKILL escalation and rewritten from the escalation cover when a grace-window reap observes the real status, proven against the real wrapper binaries of both stop implementations — and never a live state. | `src/harness_state.rs::unknown_state_is_derived_and_cannot_be_written`; `src/harness_state.rs::malformed_record_is_unknown_without_mtime_fallback`; `src/harness_state.rs::staleness_and_future_skew_derive_unknown_with_distinct_reasons`; `src/harness_state.rs::a_dead_session_reads_unknown_even_while_fresh_but_ended_survives`; `src/harness_state.rs::every_landed_write_is_byte_distinct_and_fresh_restatements_do_not_write`; `src/harness_state.rs::a_chatty_producer_restating_its_state_causes_zero_writes`; `src/harness_state.rs::concurrent_writers_defer_to_the_on_disk_record_not_their_cache`; `src/harness_state.rs::a_predecessor_sessions_record_is_never_heartbeat_eligible`; `src/harness_state.rs::missing_record_reads_as_none_not_unknown`; `src/codex_app_server.rs::pump_publishes_observations_and_stops_heartbeating_on_evidence_loss`; `src/claude_session.rs::a_provider_killed_mid_turn_reads_ended_rather_than_active`; `src/claude_session.rs::a_clean_provider_exit_writes_the_terminal_record`; `tests/harness_state_teardown.rs::stop_escalation_writes_the_terminal_record_before_sigkill`; `tests/harness_state_teardown.rs::opencode_stop_escalation_writes_the_cover_record_before_sigkill`; `tests/harness_state_teardown.rs::opencode_graceful_stop_records_the_real_reaped_exit` |

docs/vrs/.decisions/0015-immutable-agent-id-and-mutable-address.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,74 @@ host-qualified bus identity are the sole stable routing keys.
101101
the open implementation delta prevents the target VRS from being mistaken for
102102
shipped behavior.
103103

104+
## Amendment 1 — 2026-09-05: the address ships first, the ID half is staged
105+
106+
The decision above stands as the target. Its two halves ship separately, because
107+
only one of them answers the Context: the mutable address does, with no
108+
migration, no record version, and no activation gate. Splitting a provisional
109+
route from a durable key needs one new field; freezing an explicit ID buys
110+
host-move invariance and live/archive collision attribution, both real and both
111+
with no observed instance on any admitted host.
112+
113+
Shipped now: the optional `id` and `address` grammar with catalog-global `dup-id`
114+
and host-local `dup-address` admission, `effective_id`/`effective_address`/
115+
`bus_address`, roster and graph projection, `st2 agent address`, and the
116+
fail-closed bare-or-qualified reference resolution — which every reference plane
117+
now shares, including stream ingress. Positional `identity` remains the durable
118+
key, so `ST_AGENT`, task IDs, session socket paths, declaration-parent state,
119+
harness records, PTY tags, and supervisor edges are untouched by a cutover.
120+
121+
Deferred: UUIDv7 creation, the `st2 catalog migrate-ids` freeze transaction,
122+
ID-keyed durable records (message version 2, harness-state and harness-context
123+
version 2, PTY schema 2), collision metadata and collision-aware attribution of
124+
reassigned legacy endpoints, and the activation gate that would sequence them.
125+
No part of it is contradicted by shipping the address first: `id` and `address`
126+
are independent fields, and the freeze migration is unchanged by their order.
127+
128+
The ID half re-enters on any one of these observations, each of which turns a
129+
latent argument into a live obligation:
130+
131+
- a completed cross-host seat move, or a concrete plan for one — the frozen ID is
132+
the only thing that preserves `ST_AGENT`, task IDs, and socket paths across it;
133+
- a live/archive identity collision on any admitted host, which is what the
134+
reassignment record and legacy-endpoint attribution exist for; or
135+
- a UUIDv7 creation call site — a generator that authors new subjects — because a
136+
subject whose ID is in no address namespace cannot be reached without ID-keyed
137+
resolution.
138+
139+
Two corrections to the premises this decision was implemented against:
140+
141+
- **Version-1 readers are additively tolerant, so a new field is not a version
142+
bump.** `crates/st2-wire/src/lib.rs` states the opposite of the assumption as
143+
policy: no type in the reader crate uses `deny_unknown_fields`, precisely so a
144+
reader older than the binary it shells out to ignores unknown fields. The one
145+
genuine cross-build hazard is single-field and about routing, not records: a
146+
build that does not read `address` routes the positional identity and refuses
147+
the new address, the exact inverse of a build that does. So the reader-first
148+
obligation is "deploy an `address`-reading build on every admitted host before
149+
authoring any address", and nothing more. A durable record that *does* reject
150+
unknown fields — `SentRecord` — keeps its version-1 shape here, so its version-2
151+
reader belongs with the writer that emits one.
152+
- **Ownership keys are not addresses.** An exact selector answers to either
153+
immutable key: the explicit `id`, or the positional `<host>.<identity>` bus
154+
identity that migration would freeze into it. Both are unique by admission and
155+
neither moves when an address does, which is what keeps a running seat, its
156+
resync stream, and an interrupted send bound to their own subject across a
157+
cutover.
158+
159+
Before the ID half can land, two defects found while auditing its
160+
implementation must be fixed:
161+
162+
- `supervisor_chain::resolve_spec` resolves parents by `bus_id(host)` or bare
163+
`identity` only, so it must accept `effective_id`; otherwise a subject born with
164+
UUIDv7 loses its org-chart edge, which is the clause the migration transaction
165+
exists to satisfy;
166+
- `st2 catalog migrate-ids` must exempt `agent-id-missing` from its own
167+
pre-admission gate. A partially migrated catalog is inadmissible by that
168+
diagnostic, and the migration verb refuses to write into a non-admitting
169+
catalog, so the rollout order the decision prescribes — teach the projection to
170+
emit `id`, then migrate — deadlocks on the only command that can clear it.
171+
104172
## Options
105173

106174
| Option | Result | Reason |

docs/vrs/.delta/DELTA-003-agent-address-not-implemented.md

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
# DELTA-003: immutable subject ID and mutable address are not implemented
1+
# DELTA-003: the immutable subject ID is not implemented
22

33
Status: open
44

5+
Narrowed by [0015 Amendment 1](../.decisions/0015-immutable-agent-id-and-mutable-address.md)
6+
on 2026-09-05: the mutable address shipped, the immutable ID is staged behind the
7+
triggers that amendment names.
8+
59
## Divergence
610

711
[Decision 0015](../.decisions/0015-immutable-agent-id-and-mutable-address.md)
812
and root requirements R19 and R24-R26 define the accepted target identity
9-
model. The implementation still uses positional `identity` plus current host as
10-
logical subject ID, human route, ownership key, task prefix, state selector, and
11-
`ST_AGENT`. `AgentSpec` has no explicit `id` or `address`; ordinary resolution,
12-
roster output, graph output, supervisor edges, messages, authoring, and PTY
13-
metadata all retain the pre-decision behavior.
13+
model. `AgentSpec` now admits an optional `id` and an optional `address`;
14+
`st2 agent address` authors the route; ordinary references, inbox and status
15+
selection, recipients, and stream ingress all resolve through the fail-closed
16+
bare-or-qualified address algorithm; roster and graph publish `id`, `address`,
17+
and `busAddress`.
18+
19+
What remains divergent is the ID half. No writer emits `id`, so the effective ID
20+
of every subject is still its positional `<host>.<identity>` bus identity, and
21+
that value — not an explicit ID — is what ownership keys, task prefixes, durable
22+
record endpoints, supervisor edges, PTY tags, and `ST_AGENT` carry. A subject
23+
created after this delta closes would need UUIDv7 and ID-keyed resolution to be
24+
reachable at all.
1425

1526
## VRS
1627

@@ -37,48 +48,63 @@ the fail-closed bare-or-qualified address algorithm.
3748
Agent endpoints persist an immutable ID plus a publication-time address
3849
snapshot. Principal and external endpoints persist an explicit endpoint kind
3950
and canonical typed address instead of pretending that address is an agent ID.
40-
New durable message records use version 2 because strict version-1 readers
41-
reject the new endpoint and snapshot fields.
51+
That is a new durable record version for `SentRecord`, which rejects unknown
52+
fields (`src/message.rs`). It is not one for harness-state or harness-context,
53+
whose readers ignore unknown fields by policy
54+
(`crates/st2-wire/src/lib.rs`) — the premise that strict version-1 readers reject
55+
additive fields was wrong for every record but the sender ledger, and each
56+
version-2 reader belongs in the pull request that adds the writer emitting it.
4257

4358
## Implementation
4459

45-
No runtime code changes are part of the VRS pull request that opens this delta.
46-
Implementation must begin with tests at the Agent Spec and address-book
47-
boundaries. It must then propagate one typed ID/address distinction through:
60+
The address half is implemented. What it leaves is the ID half, and it must
61+
propagate one typed ID distinction through:
4862

4963
- live and archived catalog validation, explicit-ID migration, unarchive, and
50-
ID-keyed supervisor references;
51-
- every agent-selecting CLI, generated hook, channel adapter, driver argument,
52-
ambient `ST_AGENT` consumer, authoring command, graph, roster, and Doctor
53-
projection;
64+
ID-keyed supervisor references — `supervisor_chain::resolve_spec` resolves a
65+
parent by `bus_id(host)` or bare `identity` only, so it must accept
66+
`effective_id` before a UUIDv7-born subject can hold an org-chart edge, and
67+
`st2 catalog migrate-ids` must exempt `agent-id-missing` from its own
68+
pre-admission gate or the prescribed rollout order deadlocks;
69+
- every ambient `ST_AGENT` consumer, generated hook, channel adapter, and driver
70+
argument, which today carry the positional bus identity;
5471
- runtime ownership, default task IDs, task inventory, socket admission, PTY
5572
schema-2 metadata, and launch metadata while keeping declaration-parent state
5673
and Resource paths stable;
57-
- ordinary messages, replies, version-2 Sent records, typed non-Agent endpoints,
58-
DING sender projection, stream ingress and ownership, resync subscriptions,
59-
harness-state, and harness-context records; and
74+
- version-2 Sent records, typed non-Agent endpoints, DING sender projection,
75+
stream and resync ownership keys, harness-state, and harness-context records;
76+
and
6077
- all supported downstream evals and generators.
6178

62-
Activation is a reader-first transition, not a one-version flag day:
79+
The remaining reader-first obligation of the shipped half is single-field and
80+
about routing: a build that does not read `address` routes the positional
81+
identity and refuses an authored address, so an `address`-reading build must be
82+
deployed on every admitted host **before** any address is authored. That is
83+
satisfied by the release carrying the grammar; no record version, downstream
84+
reader survey, or catalog transaction is implied by it.
85+
86+
Activating the ID half is still a reader-first transition, not a one-version flag
87+
day:
6388

6489
1. Deploy readers that accept legacy and target Agent Specs, message versions 1
65-
and 2, PTY schemas 1 and 2, harness-state and harness-context schemas 1 and
66-
2, and old and new projections. Keep every writer on legacy output.
90+
and 2, and PTY schemas 1 and 2. Keep every writer on legacy output. The
91+
harness-state and harness-context readers are additively tolerant already, so
92+
their version-2 arms ship with their writers rather than ahead of them.
6793
2. Prove reader readiness on every admitted host and supported downstream
6894
consumer. An unreadable or unknown reader is not ready.
6995
3. In one catalog transaction, add migrated unique IDs to live and structurally
7096
archived declarations, update archived tombstones, and rewrite every
7197
supervisor reference to its already-resolved migrated ID.
7298
4. Re-prove reader readiness immediately before enabling target writers.
73-
5. Activate UUIDv7 creation, mutable-address routing, raw-ID `ST_AGENT`, ID-keyed
74-
runtime ownership, message version 2, harness-state and harness-context
75-
version 2, and PTY schema 2 together.
76-
77-
No timeout substitutes for readiness. Until step 5 completes, existing identity
78-
resolution and every current invariant remain normative implementation behavior.
79-
After activation, an unmigrated archived declaration cannot re-enter the
80-
catalog; unarchive validates ID uniqueness, and a transition from retired to
81-
routable validates full-catalog address uniqueness.
99+
5. Activate UUIDv7 creation, raw-ID `ST_AGENT`, ID-keyed runtime ownership,
100+
message version 2, harness-state and harness-context version 2, and PTY
101+
schema 2 together.
102+
103+
No timeout substitutes for readiness. Until step 5 completes, the positional bus
104+
identity remains the normative durable key and every current invariant remains
105+
normative implementation behavior. After activation, an unmigrated archived
106+
declaration cannot re-enter the catalog; unarchive validates ID uniqueness, and a
107+
transition from retired to routable validates full-catalog address uniqueness.
82108

83109
## Direction
84110

0 commit comments

Comments
 (0)