Skip to content

[deferred] Activate the immutable-id identity model behind a catalog migration gate - #463

Closed
schickling-assistant wants to merge 7 commits into
schickling-assistant/2026-09-05-delta-003-migrate-idsfrom
schickling-assistant/2026-09-05-delta-003-writers
Closed

[deferred] Activate the immutable-id identity model behind a catalog migration gate#463
schickling-assistant wants to merge 7 commits into
schickling-assistant/2026-09-05-delta-003-migrate-idsfrom
schickling-assistant/2026-09-05-delta-003-writers

Conversation

@schickling-assistant

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

Copy link
Copy Markdown
Contributor

Third and last of the DELTA-003 stack. Stacked on #457, which is stacked on #456. Review those first;
this branch's base is #457's head.

Problem

#456 made an explicit immutable id and an optional mutable address expressible and taught every
reader the target shapes. #457 assigns them: st2 catalog migrate-ids freezes every live and
structurally archived subject's ID in one transaction and rewrites supervisor references to IDs.

Nothing yet uses them. ST_AGENT, default task IDs, PTY presentation tags, message provenance,
harness records, stream and resync ownership, and every ordinary agent reference still key on the
positional identity plus the current host — the exact overloading
decision 0015 exists to undo.
This PR is step 5 of DELTA-003's rollout: "activate UUIDv7 creation, mutable-address routing,
raw-ID ST_AGENT, ID-keyed runtime ownership, message version 2, harness-state and harness-context
version 2, and PTY schema 2 together."

The gate

DELTA-003 also says, in the same section: "until step 5 completes, existing identity resolution and
every current invariant remain normative implementation behavior."
So activation is not a flag —
it is a property of the catalog, computed in src/identity.rs:

IdentityActivation::Activated   every live and structurally archived subject carries an explicit
                                id, and no migration transaction is outstanding
IdentityActivation::Legacy(_)   anything else — byte-for-byte today's behavior, today's record
                                versions, today's tag schema

It is all-or-nothing rather than per-subject because a mixed catalog has no coherent ID namespace:
effective_id would answer with a frozen legacy bus identity for one subject and an authored ID for
its neighbour, keying ownership and provenance two ways in one pass. The last commit makes that state
inadmissible outright (agent-id-missing), so the only holder of a mixed catalog is the migration
transaction itself, inside its own fence.

Every writer here consults activation once per pass or command — from the discovery the caller
already performed, via activation_from, never rediscovering and never per subject — and threads it
as an explicit parameter rather than a global.

Change

Selection (src/identity.rs)AgentSelector has two forms. An exact-ID selector performs
only catalog-global ID lookup and never falls through to address lookup; that is what stops an
existing semantic ID from staying alive as a route after a rename. Ordinary references run R24's
fail-closed candidate set rather than a precedence rule, because a dotted semantic address and a
host-qualified bus address are the same shape — dev4.notes is both a legal bare address and a legal
<host>.<address> split. Both readings are collected, deduplicated by agent ID, and exactly one
must survive; absence and ambiguity refuse with address-specific diagnostics. A pinned host admits
only its own qualified split, so a reference cannot reach another host by spelling it. The address
book excludes retired subjects: retirement releases the address, so it neither resolves nor occupies
the namespace, while suspension keeps both.

Runtime identityST_AGENT becomes the agent key. Default task IDs follow R26 (<agent-id>
for the canonical compact agent task, <agent-id>.<task-name> otherwise, authored ids
authoritative). Managed PTYs receive the schema-2 owned snapshot: agent.actor.id and
agent.actor.address replace agent.actor.path, which is removed as an owned tag; role=agent and
native displayName narrow to the canonical agent task and are cleared on every other PTY.
ST_SUPERVISOR stays a verbatim pass-through — migration already rewrote every supervisor reference
to the parent's migrated ID, so re-resolving it would reintroduce address parsing on an exact
selector. Because migration freezes each live subject's ID to its former bus identity, every legacy
task ID and session socket path is preserved by construction
, not by a compatibility path; the
tests assert that equality with literals rather than deriving it through the helper under test.

Messages — ordinary recipient resolution moves to the address algorithm plus an exact-ID path,
keeping the existing catalog-generation and transition fence and its retry: that fence is precisely
what holds one lookup on one coherent before-or-after address book, which an atomic address cutover
requires. Version-2 Sent records carry immutable IDs in from/to, publication-time bus addresses
as display-only snapshots, and explicit endpoint kinds. Collision-aware attribution is wired in from
#457's durable record: an untouched legacy endpoint is its own migrated ID; a reassigned one resolves
only for the row's own state owner; any other colliding endpoint stays unattributed — rendered as
a historical address, with reply and automation authority refused rather than addressed to the live
replacement (MESSAGE-R04). Stream and resync publication selects its recipient by exact ID once
activated, so a subject created with a UUIDv7 is reachable at all.

Harness records — both agent fields mean the immutable ID in version 2 and the bus identity in
version 1. Producers decide once per driver start from the catalog root each driver already holds,
and a driver that cannot decide falls back to legacy rather than guessing activated. Write-side
ownership becomes exact equality against the writer's own version, in both directions: a v1
writer must not replace a v2 record it cannot read (its seq decodes as serde-default zero, which
every claim would exceed), and a v2 writer must not coalesce or restamp a v1 record, because agent
means different things under each version and restating one namespace's bytes under the other's
promise is not a merge. Read admission stays strictly wider than both.

Authoringst2 agent address <value>|--clear is the third sibling of st2 rename and
st2 describe and reuses their whole path: exclusive lock before discovery, exact selection by
immutable ID, the ST_AGENT self/descendant guardrail over ID-keyed supervisor edges, Nix-owned and
non-KDL refusals, one span-bounded edit, source recheck then atomic rename. Host-local uniqueness is
proved by validating the complete prospective catalog through the existing dup-address rule rather
than a second implementation, and --clear is admitted only when the restored identity fallback is
itself unique. Direct id mutation is refused for address and presentation edits alike — there is no
id writer anywhere in the tree. Every agent-selecting command gains the mutually exclusive exact-ID
form. unarchive refuses an unmigrated archive once the live catalog is migrated and validates ID
uniqueness against the prospective live-and-archived set.

Gates and evidence

nix develop -c cargo test --no-fail-fast, and the decisive comparison:

tree failing test names
this branch 38
#457's head (this PR's base), clean worktree 38
comm -13 (regressions) none
comm -23 (silently fixed) none

The two sets are byte-identical, so this PR introduces no failure and hides none. Green counts on
this branch: st2 lib 762 passed, 1 ignored; agent-spec 17 + 75 + 6; and every other
integration suite green except the standing red lane below.

Pre-existing red, each proved on a clean worktree at an ancestor commit and none of it touched here:

suite failures proved at
tests/agent_publish.rs 9 main 0d22352 and stack base b30bc54
tests/catalog_apply.rs 14 main 0d22352
tests/catalog_diff.rs 2 main 0d22352 — introduced by #399 tightening admission under fixtures authored before it
tests/eval_up.rs 4 origin/main b30bc54tests/eval_up.rs still expects bare t.a while #180 made spec-team task IDs host-qualified in 2026-08
tests/eval_run_e2e.rs 2 origin/main b30bc54 — a strict-validation root-count refusal and a live judge timeout
invariants, materialize, native_only, nomad_survival, stream_authoring_cli, targeted_reconcile, task_inventory_cli, transport_isolation 11 #457's head, same names

One of those deserves a call-out because it touches this PR's own bookkeeping:
invariants::qualified_proof_references_resolve fails on the first unresolvable reference it finds,
tests/resource_profile_supervisor_e2e.rs::demand_observation_settlement_matrix_is_atomic_and_preserves_facts,
which predates this stack. That means it cannot vouch for the rows this PR moved, so every proof
reference added to INVARIANTS.md here was checked directly against a declared fn — all 36 resolve.

New proof

Roughly 40 new tests. The load-bearing ones:

  • Selection — bare, dotted, and host-qualified references; two readings of one reference failing
    closed; one address on two hosts needing a host; a pinned host refusing another host's split; one
    subject reached twice not being ambiguous with itself; exact-ID selection resolving a subject whose
    address bytes belong to a different subject, and refusing an address as an ID.
  • Continuity — a migrated subject's task IDs and socket paths byte-identical to the legacy rule;
    resync watch-set and seat-id continuity across migration; an in-memory spec team with no ids
    gating Legacy and lowering to literally its pre-PR task IDs.
  • Nondisruption — an address change patching PTY metadata while entering no launch, teardown, or
    replacement; a healthy task keeping its PID, creation identity, and generation.
  • Records — activated writers emitting v2 with the agent ID and legacy writers v1 with the bus
    identity; the version gate refusing ownership in both directions with everything else equal
    (same seat, same session token, same claimed sequence) so the version is the only deciding factor;
    a version-1 send still emitting version 1's exact field set and digest.
  • Attribution — a reassigned legacy endpoint attributed for the row's state owner, unattributed
    at the other endpoint, and reply authority refused for the unattributed one.
  • Authoring — set/change/clear with receipts; a colliding address on one host refused and the
    same address on another host admitted; invalid grammar, Nix-owned, TOML, ambiguous target, and
    id mutation all refused; the self/descendant guardrail admitting a descendant and refusing a
    stranger; --clear refused when the fallback would collide; unarchive refusing an unmigrated
    archive once the catalog is migrated.
  • The gate — a partially migrated catalog refused with agent-id-missing; both uniform states
    admitted; activation requiring both planes and a completed transaction.

Intentionally left

  • Activation is implemented but active nowhere, because no catalog is migrated. On every
    existing host identity::activation answers Legacy, and every writer's legacy branch is
    byte-for-byte today's behavior. That is the property that makes this PR safe to land ahead of the
    live migration rather than after it.
  • UUIDv7 creation has no call site. No path in this tree synthesizes a new agent declaration:
    every new subject enters as operator- or generator-authored canonical KDL bound byte-exactly by a
    digest (st2 agent publish --input-sha256, st2 catalog bootstrap|apply), and injecting an id
    would break that binding. The obligation belongs to the grammar gate — the canonical Agent Spec
    requiring id — and to whichever generator authors the declaration. The generator exists and is
    proved; DELTA-003 records the gap and the reasoning rather than leaving a dead branch here.
  • id is still optional in the grammar. Making it required is a canonical-Agent-Spec change in
    compoundingtech/evals plus its proof corpus, not an st2 decision.
  • The live dev3 migration, whose operator sequence is in [deferred] Freeze every legacy subject's immutable agent id in one catalog transaction #457's body, and the dotfiles projection
    change that must precede it.
  • DELTA-003 stays open. Its Resolution Signal needs a migrated fleet, downstream generators
    emitting the model, and the model-free proof corpus passing against an immutable st2 artifact.
    This PR records progress against each of the five rollout steps instead of claiming closure.

Stack created with GitHub Stacks CLIGive Feedback 💬

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

…gate

Decision 0015 splits one overloaded string into two typed namespaces, so
selection needs a type rather than a precedence rule.

- `AgentSelector` has exactly two forms. An exact-ID selector performs only
  catalog-global ID lookup and never falls through to address lookup, which is
  what stops an existing semantic ID from staying alive as a route after a
  rename. Equal bytes across the two namespaces do not collide.
- Ordinary references resolve through R24's fail-closed candidate set instead of
  a precedence rule, because a dotted semantic address and a host-qualified bus
  address are the same shape: collecting both readings and requiring exactly one
  surviving subject makes the reference decidable without guessing which dot is
  the separator. Absence and ambiguity both refuse, and ambiguity names the
  surviving subjects.
- The address book excludes retired subjects: retirement releases the address and
  makes the subject non-routable, so it neither resolves nor occupies the
  namespace. Suspended subjects stay.
- `activation` is DELTA-003 step 5's gate. It answers `Activated` only when every
  live and structurally archived subject carries an explicit ID and no migration
  transaction is outstanding. A partially migrated catalog has no coherent ID
  namespace to key ownership, provenance, or task identity on, so the gate is
  all-or-nothing and legacy behavior stays normative until it opens.
…ble id

R26 derives runtime ownership, default task IDs, and `ST_AGENT` from the
immutable agent ID rather than the mutable address. Activation is decided once
per reconcile pass and threaded as an explicit parameter, never per subject and
never through a global.

- `ST_AGENT` becomes the agent key: the raw immutable ID once the catalog is
  migrated, the legacy bus identity while it is not. `ST_SUPERVISOR` stays a
  verbatim pass-through, because catalog ID migration already rewrote every
  supervisor reference to the parent's migrated ID — re-resolving it would
  reintroduce address parsing on an exact selector.
- Default task IDs follow R26: `<agent-id>` for the canonical compact agent task,
  `<agent-id>.<task-name>` for a long-form named task without an explicit id, and
  an authored id stays authoritative. Because migration freezes each live
  subject's ID to its former bus identity, every legacy task ID and session
  socket path is preserved by construction rather than by a compatibility path.
- Managed PTYs receive the schema-2 owned snapshot: `agent.actor.id` plus
  `agent.actor.address` replace `agent.actor.path`, which is removed as an owned
  tag. `role=agent` and native `displayName` narrow to the canonical compact
  agent task and are cleared on every other PTY. Unrelated tags are preserved and
  clearing a value removes only its own tag.
- An address change is a metadata projection: it patches tags and enters no
  launch, teardown, replacement, or flapping accounting.

The legacy branch is byte-for-byte today's behavior, schema 1 included.
Ordinary message resolution was a precedence rule over bus identity and bare
identity. It becomes the R24 address algorithm over an address book built from
the same fenced snapshot, plus an explicit exact-ID path. The catalog-generation
and transition fence and its retry are unchanged: that fence is exactly what
keeps one lookup on one coherent before-or-after address book, which an atomic
address cutover requires.

- Version-2 Sent records carry immutable IDs in `from`/`to`, publication-time bus
  addresses as display-only snapshots, and explicit endpoint kinds; a principal or
  external endpoint keeps its canonical address with the matching kind. The writer
  emits version 2 only for a provably migrated catalog and otherwise keeps
  version 1's exact bytes, which the digest-named pending record requires.
- Collision-aware attribution is wired into the version-1 read path from the
  migration's durable record: an untouched legacy endpoint is its own migrated ID,
  a reassigned one resolves only for the row's own state owner, and any other
  colliding endpoint stays unattributed — rendered as a historical address, with
  reply and automation authority refused rather than addressed to the live
  replacement.
- Rendered frontmatter and the DING sender projection read address-for-humans
  with the ID as authority, and fall back cosmetically when a subject no longer
  resolves.
- Stream and resync publication selects its recipient by exact ID once activated,
  so a subject created with a UUIDv7 is reachable; the legacy path and the
  persisted stream-record bytes are unchanged.

The transition branch of the address book stays keyed on the legacy pair: mid-apply
the declaration bytes are unreadable, and it locates a retained state directory
rather than resolving a route.
…ership

The `agent` field of both harness records means the immutable agent ID in
version 2 and the bus identity in version 1; the shape is otherwise identical, so
the version suffix is the whole read contract.

- A producer emits version 2 only once its catalog is migrated. The decision is
  taken once per driver start from the catalog root each driver already holds —
  never per record write — and a driver that cannot decide falls back to legacy
  rather than guessing activated.
- Write-side ownership becomes exact equality against the writer's OWN version,
  in both directions. A v1 writer must not replace a v2 record it cannot read
  (its `seq` decodes as serde-default zero, which every claim would exceed), and
  a v2 writer must not coalesce or restamp a v1 record: `agent` means different
  things under each version, and restating one namespace's bytes under the other's
  promise is not a merge. Read admission stays strictly wider than both.
- Resync watch sets and seat IDs, and task-inventory agent and runtime-id
  columns, derive from the shared reconcile key and task-ID rules instead of
  local copies. For a migrated legacy subject every derived value is
  byte-identical, which the tests assert rather than assume.
- Task inventory rows gain a nullable address without weakening completeness: no
  declared task is dropped, the duplicate-runtime-id refusal still fires, and a
  missing address never reads as a missing task.
`st2 agent address` is the third sibling of `st2 rename` and `st2 describe` and
reuses their whole path: the exclusive authoring lock before discovery, exact
selection by immutable agent ID, the caller-supplied `ST_AGENT` self/descendant
guardrail over ID-keyed supervisor edges, the Nix-owned and non-KDL refusals, one
span-bounded edit, and the source-recheck-then-atomic-rename publication.

- The value is validated against R24's grammar, and host-local effective-address
  uniqueness is proved by validating the complete prospective catalog through the
  existing `dup-address` rule rather than a second implementation. `--clear`
  restores the positional identity fallback and is admitted only when that
  fallback is itself unique.
- Direct `id` mutation is refused for address and presentation edits alike: there
  is no `id` writer anywhere in the tree.
- Every agent-selecting command gains the mutually exclusive exact-ID form, and a
  command defaulting from `ST_AGENT` consumes it as an exact selector rather than
  re-resolving it as a route.
- `unarchive` refuses an unmigrated archive once the live catalog is migrated, and
  validates ID uniqueness against the prospective live-and-archived set rather
  than the live catalog alone.

No path in this tree synthesizes a new declaration, so UUIDv7 creation has no
call site here: every new subject enters as authored KDL bound byte-exactly by a
digest, and injecting an id would break that binding. The obligation belongs to
the grammar gate and to the authoring generator, and is recorded in DELTA-003.
A catalog is either fully migrated to explicit agent IDs or not migrated at all.
A mixture has no coherent ID namespace — `effective_id` answers with a frozen
legacy bus identity for one subject and an authored ID for its neighbour — so
ownership, provenance, and task identity would be keyed two ways in one pass
while the activation gate kept every writer on legacy behavior.

`agent-id-missing` is therefore what stops a new declaration from entering a
migrated catalog without an `id`, including one published through a digest-bound
path whose exact bytes cannot carry an injected one. Only whole-catalog ID
migration holds a mixed state, inside its own transaction: it proves an
all-legacy plane before its writes and an all-migrated plane after them, and a
resume skips the pre-check precisely because what it observes is the half-migrated
state this rule refuses.
…TA-003

The target contract is implemented behind one catalog-scoped activation gate, so
the VRS stops describing it as unimplemented and starts describing the gate.

- Root spec, agent-spec F02, harness-state, and harness-context notes now say
  what ships: readers accept both record versions, producers emit the new version
  once their catalog is migrated, and write-side ownership stays exact
  own-version equality.
- DELTA-003 stays open and records progress against its own five rollout steps.
  Two things keep it open: the canonical Agent Spec grammar still makes `id`
  optional, and no admitted host is migrated — the model is proved on fixtures and
  on a scratch copy of a real 722-declaration plane, not on the live fleet, and no
  downstream generator emits `id`.
- The invariant rows DELTA-003 names move with the behavior they pin: runner-owned
  task identity to the raw ID and schema-2 actor tags, R23's inventory to ID-keyed
  ownership with a nullable address, archival to frozen archived IDs and safe
  unarchive, and unbindable session sockets to the new default task-ID shape with
  legacy socket continuity preserved.
@schickling-assistant schickling-assistant changed the title schickling assistant/2026 09 05 delta 003 writers Activate the immutable-id identity model behind a catalog migration gate Sep 5, 2026
@schickling-assistant
schickling-assistant marked this pull request as ready for review September 5, 2026 15:10
@schickling-assistant

Copy link
Copy Markdown
Contributor Author

@codex review

Posted on behalf of @schickling
field value
agent_identity dev3.direct.omp.aetn947t
session dev3.aetn947t
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

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@schickling-assistant

Copy link
Copy Markdown
Contributor Author

Superseded in part, deferred in part. The branch stays.

What shipped out of this PR, as #467 stacked on #456: agent_author::set_address + refuse_address_collision, main::address_cmd, AgentSelector + identity::resolve_address/resolve_id, the exact --id/--to-id selector surface, and the recipient-resolution cutover to effective_address(). Two things were finished rather than ported as-is:

  • the &str arms are gone, not kept beside the selector ones (with_resolved_*, send_to_resolved_inbox, archive_resolved_message now take &AgentSelector only) — the dual API in this branch left the old path reachable, which doubles the resolution surface under test;
  • event.rs::resolve_stream now routes through identity::resolve instead of its own three-arm filter. That was a latent correctness gap here too: stream ingress had no dedupe-by-ID and no host pinning, so it did not implement the algorithm 0015 mandates.

What is deferred (docs/vrs/.decisions/0015-… Amendment 1): UUIDv7 creation, ID-keyed runtime ownership and task identity, the four record/tag version pairs, collision metadata and LegacyAttribution, and the activation gate that sequences them. Re-entry is an observation, not a schedule: a completed cross-host seat move, a live/archive identity collision, or a UUIDv7 creation call site.

Why the version bumps did not ship. The premise at DELTA-003:42-46 — "strict version-1 readers reject the new fields" — is false for every record but the sender ledger. crates/st2-wire/src/lib.rs:18-23 states the opposite as policy ("No type here uses deny_unknown_fields"), the new sent-row fields are skip_serializing_if, and st2.harness-state.v2's shape is byte-identical to v1 — this branch's own the_reserved_next_version_decodes_its_axes_exactly_like_v1 asserts that, and record.agent is never read for any decision (read_raw_at does not look at it). SentRecord is deny_unknown_fields, which is precisely why its version-2 reader belongs in the PR that adds the version-2 writer. The one genuine cross-build hazard is single-field and about routing, and #456 alone satisfies it.

Before the deferred half lands, two defects recorded in the amendment and in the narrowed DELTA-003: supervisor_chain::resolve_spec must accept effective_id (it is byte-untouched by all three PRs, so a UUIDv7-born parent loses its org-chart edge — the clause the migration exists to satisfy), and st2 catalog migrate-ids must exempt agent-id-missing from its own pre-admission gate (the agent-id-missing diagnostic this branch added deadlocks with #457's gate in the prescribed rollout order).

Worth keeping from this branch when it returns, beyond the identity model: the activation-gate consolidation (it is currently re-derived in 8 places with four separately-tested fail-closed policies) and the AddressBookEntry projection, which is written four times.

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

@schickling-assistant schickling-assistant changed the title Activate the immutable-id identity model behind a catalog migration gate [deferred] Activate the immutable-id identity model behind a catalog migration gate Sep 5, 2026
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.

1 participant