Skip to content

feat(agent): mutable address, immutable positional identity - #467

Merged
schickling-assistant merged 9 commits into
mainfrom
schickling-assistant/2026-09-05-agent-address-writer
Sep 6, 2026
Merged

feat(agent): mutable address, immutable positional identity#467
schickling-assistant merged 9 commits into
mainfrom
schickling-assistant/2026-09-05-agent-address-writer

Conversation

@schickling-assistant

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

Copy link
Copy Markdown
Contributor

Problem

Stacked on #468 (which continues #456 after its trim; #456 cannot be reopened through the API). That
PR adds the address field — this one makes it do something.

Coordination: #456 and #457 were closed at 15:18 UTC in favour of #464, a single 12.5k-line
implementation of the whole ID/address program. That is option B of decision Q23, and the answer of
record chose option A: land the trimmed reader PR, lift the address writer out of #463, defer the ID
half. This PR and #468 execute that answer; #464 is untouched and needs a human call, because the two
lanes overlap the same contract completely.

#453 accepted decision 0015:
a provisional semantic route becomes permanent because the route and the durable key are the same
string. #456 splits the two in the grammar, but nothing writes an address and nothing resolves one,
so dev3.dotfiles.fractal.help-key.verifier still cannot become
dev3.dotfiles.fractal.keymap.verifier without stranding the subject.

The remaining work is one writer plus one resolution algorithm. The immutable-id half of 0015 —
UUIDv7 creation, the freeze migration, ID-keyed durable records, the activation gate — is deferred
behind named triggers (0015 Amendment 1, in this PR). #457 and the remainder of #463 are closed as
deferred with their branches kept.

Change

st2 agent address <agent> <address> / --clear (src/agent_author.rs, src/main.rs) — one
atomic address-book cutover on the publication path st2 rename and st2 describe already use: same
authoring lock, same exact-declaration resolution, same ST_AGENT self/descendant guardrail, same
Nix and non-KDL refusals. It rewrites exactly the address child node. What it adds:

  • host-local effective-address uniqueness decided by building the complete prospective catalog and
    re-running validate's own dup-address rule, so explicit/explicit, explicit/fallback, and
    --clear collisions are one rule, not three;
  • the R24 grammar check before any write;
  • a read-back gate that refuses if the candidate's immutable id moved — id is the one declared
    value no authoring command may rewrite, so the gate compares it instead of trusting the edit.

One typed selector, one algorithm (src/identity.rs) — AgentSelector is either an ordinary
address reference or an exact subject. resolve_address is R24's fail-closed candidate set rather
than a precedence rule: a dotted semantic address and a host-qualified bus address are
indistinguishable by shape, so both readings are collected, deduplicated by agent ID, and exactly one
survivor decides. resolve_id answers on either immutable key — the explicit id, or the positional
<host>.<identity> bus identity a later migration freezes into it — and never falls through to
address lookup. Both keys are unique by admission (dup-id); a key naming two subjects refuses
rather than answering with a first match.

The message plane is cut over, not wrapped (src/message.rs) — recipients, inboxes, state
directories, message boxes, and archival take &AgentSelector. No &str arm survives, so a route
cannot be silently retyped as an exact subject or the reverse. Recipient matching moves from
bus_id == r || identity == r to the effective address, with retirement handled by two books
instead of a tie-break: a retired subject cannot make a live claimant ambiguous, and still answers to
its own address when nothing routable does, so its retained state stays reachable by name.

Exact forms where a route is not enough — every agent-selecting command gains --id (--to-id
for message send), which performs ID lookup only and never retries its input as an address.

Drivers, channels, and hooks name their own subject by exact key through
message::resolve_declared_dir / declared_selector, so a cutover cannot disconnect a running seat
from its own directories, workspace, or message boxes — and so a relaunch still works, since
reconciliation hands a driver the positional bus identity. Because st2 driver … --identity has
always accepted the bare positional identity too, and an identity may itself contain dots, both
readings are tried in today's order (whole key first, then qualified by this host) rather than
guessing which dot is a separator. Both are immutable keys; neither is the address namespace.

A declared supervisor is a declaration key on both of its planes — the org-chart walk
(supervisor_chain::resolve_spec) and the notices that walk carries — so it resolves through the
same two exact readings, never through the address book. An address is a routing alias for human and
message references only.

event::resolve_stream routes through identity::resolve (src/event.rs) — also a correctness
fix, not just deduplication: stream ingress hand-rolled a three-arm filter with no dedupe-by-ID and
no host pinning, so it did not implement the algorithm 0015 mandates. The local host is tried first
(today's bare-name behavior), then the whole catalog, so a foreign subject still resolves and meets
its own "owned by host X" refusal instead of an unhelpful absence. Built-in resync names its
recipient by the agent key reconciliation holds, so publication survives a cutover; st2 event emit
names an ordinary address, so a released spelling refuses at once. An interrupted send recovers by
its record's canonical endpoint — an immutable key — because a cutover between the pending write and
the retry is a route change, not a changed recipient.

Contract (0015 Amendment 1, DELTA-003, docs/vrs/spec.md, docs/vrs/02-agent-spec/spec.md,
INVARIANTS.md) — records that the address ships first; that the ID half re-enters on an observed
trigger (a completed cross-host seat move, a live/archive identity collision, or a UUIDv7 creation
call site); that the "strict version-1 readers reject additive fields" premise is false by documented
policy (crates/st2-wire/src/lib.rs) for every record but the sender ledger; and that two defects
block the ID half — 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.

Size: +2,243/−253 over 19 files — ≈1,265 production lines, ≈410 inline test lines, 428 in tests/,
142 docs.

Review round 2 — the independent review's findings

Four commits on top of the reviewed head, plus three on the base PR (#468). Every fix removes or
narrows surface; none adds a mechanism.

Blocking — supervisor was two namespaces at once (src/run.rs, src/codex_app_server.rs).
surface_crash_loop read the declared supervisor value as a mutable address while
supervisor_chain::resolve_spec — untouched by this PR — reads it positionally, so once a parent
declared an address neither spelling satisfied both planes: the org chart kept validating while every
child's crash-loop notice was dropped with a tracing::warn! only, silently, in exactly the
situation M2.4 exists for. codex_app_server's protocol-rejection report had the same shape. Both
sites now resolve through the two exact declaration-key readings this stack already introduced for
runtimes, so one field has one namespace. Those helpers are renamed actor_*resolve_declared_dir
/ declared_selector / declaration_readings, because an org-chart edge is a declaration key too.
0015 Amendment 1 now states the rule. The alternative — making supervisor an address — is a
supervisor_chain contract change and stays deferred with the ID half.

Blocking — the authoring positional says what it selects (src/main.rs, src/identity.rs). The
new help text promised address resolution on four authoring commands, but
agent_author::resolve_target matches bus_id then the positional identity and never
effective_address, so after a cutover the subject's own new address could not select it while the
released spelling still could. Two candidate fixes: teach resolve_target the address book, or
restore the accurate wording origin/main carried. Chosen: the wording — four doc lines against
a second address-resolving path, and it narrows the authoring plane instead of widening it (authoring
must select exactly one declaration to edit; --id is the exact form). A test pins the contract in
both directions.

Non-blocking, fail-open edges (src/message.rs, src/event.rs, src/identity.rs):

  • select_index resolved candidates deduplicated by agent ID and then took the first position for
    the winning ID, so on a catalog with a duplicate ID the resolved subject and the delivered
    directory could be different declarations; event.rs had the same shape on the declaration key.
    Both refuse now.
  • Ambiguous deduplicated its names after deciding, so two declarations sharing one ID rendered as
    "names 1 subjects". One name per surviving subject, and the noun agrees.
  • message/state resolution passed no pinned host while stream ingress pinned the local host first, so
    st2 message send chat and st2 event emit chat disagreed on a catalog where two hosts declare
    one address. The local-first step moved into identity::resolve_local_first; both planes call it
    and event.rs lost its copy.
  • the address-conflict refusal now names the incumbent claimant instead of forwarding validate's
    dup-address message, which pointed at the first declaration in path order — usually the
    candidate's own file.
  • --as's flag help names the namespace it reads (an ordinary address reference), since this stack is
    what makes it diverge from $ST_AGENT's exact ID.

Retired subjects and new mail. The review is right that the doc overclaimed; the behaviour is
origin/main's (which had no retirement filter at all in recipient resolution), so this PR did not
introduce it. Restricting the retired book to read paths means threading a read/write distinction
through four resolution entry points and changing behaviour relative to main, which is a separate
change. Fixed here instead: AddressBookEntry's doc and ResolveError::Unknown's text now state the
rule the code implements — retirement releases the address for claiming, and a retired subject
answers on its own address only when nothing routable does, which is what keeps its retained state
reachable by name (Q3).

Evidence for the fixes

nix develop -c cargo build --workspace --tests                     # clean
nix develop -c cargo test --lib                                    # 724 passed, 1 ignored
nix develop -c cargo test --test agent_address                      # 12 passed (4 new)
  a_parents_address_cutover_keeps_the_org_chart_and_its_notifications
  authoring_selects_the_declaration_key_and_not_the_current_address
  two_declarations_sharing_one_effective_id_refuse_a_reference
  a_bare_address_declared_on_two_hosts_resolves_to_the_local_subject
nix develop -c cargo test --test message --test message_cli --test validate \
      --test catalog_graph --test status_agents --test reconcile --test run \
      --test event_e2e --test codex_app_server --test resync --test agent_desired_state \
      --test agent_presentation                                     # all green

Each new test was run against the unfixed source (source hunks reverted, test kept) and fails there.

Live proof of the supervisor fix, scratch catalog, isolated PTY_ROOT, dev3.root supervising
dev3.verifier and a dev3.flapper whose command "false" with restart { attempts 1; mode "fail" }
crash-loops:

before   tasks  dev3.root pid 981556 gen sha256:d65629577385   dev3.verifier pid 981834 gen sha256:0faa0ca7b111
$ st2 agent address verifier keymap.verifier   -> changed   (leaf)
$ st2 agent address root      ops.root         -> changed   (PARENT — the case the review flagged)
after    tasks  identical pids, runtimeIds and generationIds
$ st2 validate --host dev3   -> 0 errors, 0 warnings across 3 agents
         child declaration still reads   supervisor "dev3.root"    (unedited)
routing  ops.root            -> delivered
         root / dev3.root    -> Error: no agent … found in catalog   (released spellings refuse)
         --to-id dev3.root   -> delivered
$ st2 up --interval 2   (loop, ~25s)
         dev3.root inbox gains   subject: crash-loop: dev3.flapper parked   tags: crash-loop
$ st2 up --once      -> adopted (2): root, verifier          (relaunched nothing)
$ st2 down           -> torn down (2): dev3.root, dev3.verifier

So the crash-loop notice reaches the renamed parent, which is exactly what the reviewed head
dropped. No live catalog or supervisor was touched.

Gates and evidence

cargo build --workspace --tests                              # clean
cargo test --lib                                             # 716 passed, 1 ignored
cargo test --test agent_address                              # 8 passed
cargo test --test message --test message_cli                 # 6 + 19 passed
cargo test --test harness_state_teardown                     # 3 passed (real-wrapper stop path)
cargo clippy --lib --bin st2                                 # no finding in the changed surface

Whole-suite comparison against the stack base. cargo test --workspace --no-fail-fast (clean env,
PTY_ROOT/ST_AGENT unset) run twice — here, and at b30bc54 in a detached worktree — then the
failing test names diffed:

this branch : 48 failing tests
b30bc54     : 50 failing tests
only here   : (none)
only at base: manual_pty_restart_preserves_every_st2_managed_environment_and_config_value,
              roster_joins_a_real_context_record_independently_of_observed_state

So the 48 are pre-existing on this machine: agent_publish (9-10, candidate fails full-catalog validation plus checkpoint timeouts), catalog_apply (14), catalog_diff (2, native-driver-missing
and root-count fixtures), eval_up (4), eval_run_e2e (2), native_only (3), invariants (1),
materialize (1), nomad_survival (1), status_agents (1), targeted_reconcile (1) and the rest.
One regression this comparison did catch and this PR fixes: the three harness_state_teardown tests
launch the real wrapper with a bare --identity worker, which is what forced the two-reading
actor resolution above rather than a single spelling.

cargo fmt --check disagrees with committed code in 40+ untouched files (resync.rs 42 hunks,
run.rs 29), so formatting is left alone.

Live cutover proof on a scratch catalog (h.root supervising h.verifier, both launched by
st2 up --once, isolated PTY_ROOT):

before   tasks   [{"runtimeId":"h.root","pid":1030334},{"runtimeId":"h.verifier","pid":1030426}]
         pty     [{"name":"h.verifier","agent.actor.path":"h.verifier","schema":"1","pid":1030426}]
         graph   verifier: address=verifier busAddress=h.verifier parentId=h.root

$ st2 agent address verifier keymap.verifier --host h --json      # ST_AGENT=h.root
{"result":"changed","id":"h.verifier","identity":"h.verifier",
 "address":"keymap.verifier","busAddress":"h.keymap.verifier","retired":false}
declaration gains exactly one node:  address "keymap.verifier"

after    tasks   [{"runtimeId":"h.root","pid":1030334},{"runtimeId":"h.verifier","pid":1030426}]
         pty     [{"name":"h.verifier","agent.actor.path":"h.verifier","schema":"1","pid":1030426}]
         graph   verifier: address=keymap.verifier busAddress=h.keymap.verifier parentId=h.root

routing  verifier              -> Error: no agent 'verifier' found in catalog       (refuses)
         h.verifier            -> Error: no agent 'h.verifier' found in catalog     (refuses)
         keymap.verifier       -> delivered
         h.keymap.verifier     -> delivered
         --to-id h.verifier    -> delivered
         message ls --id h.verifier --count  -> 4
         h.root's sent ledger `to` for all 4 rows -> "h.verifier"   (durable endpoint unmoved)

a second `st2 up --once` relaunches nothing: same pids, same runtimeIds, same PTY tags

So the route moved and nothing else did: task IDs, ST_AGENT-derived keys, PTY agent.actor.path
(still schema 1), the supervisor edge, and every durable record endpoint are byte-identical across
the cutover, while the old route and the old bus identity both fail loudly as routes and the exact
selector keeps working.

New tests: tests/agent_address.rs (8 — receipt classification, byte-preservation, collision
refusal on the same host and admission on another, --clear fallback collision, actor guardrail,
grammar/Nix/non-KDL/ambiguity refusals, exact-ID form for declaration and route);
src/identity.rs (10 — candidate-set resolution, host pinning, fail-closed ambiguity, exact
selection after an address moves); src/event.rs (2 — a declared address routes an event while
ownership stays on the bus identity; resync still reaches a subject whose address moved).

Intentionally left

  • The immutable-id half. No writer emits id; effective_id still answers with the bus
    identity, so ownership keys, task IDs, socket paths, record endpoints, and PTY tags are unchanged.
    st2 catalog migrate-ids ([deferred] Freeze every legacy subject's immutable agent id in one catalog transaction #457), UUIDv7 creation, version-2 records, and the activation gate are
    deferred with their branches kept.
  • Rename history. No alias, redirect, or expiry: a stale route fails loudly, which is what the
    decision requires and what the proof above shows.
  • Restricting the retired address book to read paths. Delivering into a retired subject's inbox
    by its own address is origin/main's behaviour, not something this PR introduces; changing it
    means threading a read/write distinction through find_agent_handle, require_agent_handle,
    with_resolved_state_dir, and select_index, and is its own change. The doc and error text now
    describe what the code does.
  • Collision-aware attribution of a reassigned legacy endpoint and archived-subject ID
    uniqueness
    keep their // DELTA-003: notes — both need the deferred migration's durable
    metadata.
  • GraphArchived address fields. A tombstone has no address field and an archived subject is
    non-routable.
  • The main.rs and message.rs shapes the audit flagged as boilerplate (one AgentRef clap
    flatten, one selected_or_acting_* helper for the 10-copy idiom) are left for a follow-up: they
    are pure refactors of code this PR is introducing, and doing both at once would hide the semantic
    change under a restructure.

Refs #453, schickling/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

@schickling-assistant

Copy link
Copy Markdown
Contributor Author

@codex review

Stacked on #456 (review that first — its branch was rewritten to the identity/address model only). This PR makes address do something: st2 agent address, AgentSelector, the fail-closed bare-or-qualified resolution, and the message/stream cutover to effective_address(). The immutable-id half of decision 0015 is deferred behind named triggers, recorded as 0015 Amendment 1 here.

Places worth the most scrutiny:

  1. src/identity.rs resolve_id — an exact selector answers on two immutable keys (explicit id, positional bus identity) and fails closed when a key names two subjects. Is the "never falls through to address lookup" property actually preserved by that second key, given the positional identity is also the legacy address fallback when no address is declared?
  2. src/message.rs select_index — the two-book rule for retired subjects (routable book first, whole book only when nothing routable answers). Does it hold for a retired subject whose released address a live subject has since claimed?
  3. src/message.rs actor_readings — a runtime naming itself accepts the whole reference and the host-qualified reading, in that order. tests/harness_state_teardown.rs passes a bare --identity worker, and reconciliation passes <host>.<identity>; both must work, and neither may consult the address.
  4. src/event.rs resolve_stream — now routes through identity::resolve (pinned host first, then the whole catalog). Compare the refusal classification against src/resync.rs's expectations: RecipientNotRunning must stay retryable and ambiguity/foreign-host must stay permanent.
  5. recover_active in src/message.rs — an interrupted send now resolves its recipient by the record's canonical endpoint as an exact key rather than as an address. Is that the right reading of a version-1 row?

Evidence, including the live cutover proof and a whole-suite diff against the stack base, is in the description.

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

@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.

Comment thread src/run.rs Outdated
Comment thread src/main.rs Outdated
Comment thread src/message.rs Outdated
Comment thread src/identity.rs Outdated
Comment thread src/identity.rs
Comment thread src/agent_author.rs Outdated
Comment thread src/message.rs Outdated
Comment thread src/main.rs
Decision 0015 splits one overloaded string into two namespaces, so selection
needs a type: an ordinary human reference resolves against the mutable address,
and an exact selector names a subject by a key no cutover moves.

- `resolve_address` is R24's fail-closed candidate set rather than a precedence
  rule: a dotted semantic address and a host-qualified bus address are
  indistinguishable by shape, so both readings are collected and exactly one
  surviving subject decides, deduplicated by agent ID and optionally host-pinned.
- `resolve_id` answers on either immutable key — the explicit `id`, or the
  positional `<host>.<identity>` bus identity a later ID migration freezes into
  it — and never falls through to address lookup, which is what stops a released
  semantic address from staying alive as an exact selector. Both keys are unique
  by admission (`dup-id`), and a key that names two subjects refuses rather than
  answering with a first match.
- `address_book` projects a discovered catalog: retired subjects are absent,
  because retirement releases the address and makes the subject non-routable.

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
One atomic address-book cutover (R25) on the existing source-preserving,
authority-scoped, transactional publication path `st2 rename` and `st2 describe`
already use: the same catalog-authoring lock, the same exact-declaration
resolution, the same `ST_AGENT` self/descendant guardrail, the same Nix and
non-KDL refusals. The edit rewrites exactly the `address` child node, so every
byte of the declaration around it survives.

What address adds beyond presentation:

- host-local effective-address uniqueness, decided by building the complete
  prospective catalog and re-running `validate`'s own `dup-address` rule rather
  than re-deriving it, which covers explicit/explicit and
  explicit/identity-fallback collisions and the `--clear` case alike;
- the R24 grammar check before any write;
- a read-back gate that refuses if the candidate's immutable `id` moved. `id` is
  the one declared value no authoring command may rewrite, so the gate compares
  it rather than trusting the edit that produced the candidate.

The receipt names the unchanged `id` and `identity` beside the new `address` and
`busAddress`; a retired subject projects a null bus address, exactly as the
roster does, because it released its address.

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
`st2 agent address <agent> <address>` becomes reachable, and every reference
plane resolves through the one algorithm decision 0015 mandates instead of each
site's own precedence filter.

- `message`'s resolution surface takes `&AgentSelector`, not `&str`: recipients,
  inboxes, state directories, message boxes, and archival all state which
  namespace they were named in. There is no `&str` arm left to fall back to, so a
  route can never be silently retyped as an exact subject or the reverse.
  Recipient matching moves from `bus_id == r || identity == r` to the effective
  address, with retirement handled by two books rather than a tie-break: a
  retired subject cannot make a live claimant ambiguous, and still answers to its
  own address when nothing routable does, keeping its retained state reachable.
- Every command that names an agent gains the exact `--id` / `--to-id` form,
  which resolves without consulting the address namespace at all.
- Drivers, channels, and hooks select their own subject by exact key, so an
  address cutover cannot disconnect a running seat from its own directories. A
  supervisor reference stays an ordinary address reference.
- `event::resolve_stream` routes through `identity::resolve` instead of a
  hand-rolled three-arm filter, which is also a fix: stream ingress had no
  dedupe-by-ID and no host pinning, so it did not implement the mandated
  algorithm. Built-in resync names its recipient by the agent key reconciliation
  holds, so publication survives a cutover; `st2 event emit` names an ordinary
  address, so the released spelling refuses at once.
- An interrupted send recovers by its record's canonical endpoint, which is an
  immutable key: a cutover between the pending write and the retry is a route
  change, not a changed recipient.

Durable state is untouched by design. `ST_AGENT`, default task IDs, session
socket paths, declaration-parent state, harness records, PTY tags, message
provenance, and supervisor edges are all keyed on values this cutover never
writes.

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
…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
… address

`supervisor_chain::resolve_spec` reads a declared `supervisor` positionally — an
exact bus id, or a bare identity on the local host — so reading the same value as
a mutable address gave one string two namespaces. After a parent declared an
address, neither spelling satisfied both planes: the org chart kept validating
while every child crash-loop notice was dropped with a `tracing::warn!` only,
silently, in exactly the situation M2.4 exists for. `codex_app_server` reported
protocol rejections the same way.

Both sites now resolve through the two exact readings the org chart walks, which
this stack already introduced for runtimes naming themselves. Those helpers are
renamed from `actor_*` to declaration-key names, since a supervisor edge is a
declaration key too: an address is a routing alias for humans and messages, and
neither a runtime nor an org-chart edge is one.

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
The new help text on `agent address`, `agent desired-state`, `rename`, and
`describe` promised address resolution: `agent_author::resolve_target` matches
`bus_id` then the positional `identity`, never `effective_address`, so after a
cutover the subject own new address could not select it while the released
spelling still could.

Two candidate fixes: teach `resolve_target` the address book, or restore the
accurate wording origin/main carried. The second is smaller (four doc lines
against a second address-resolving path) and it narrows rather than widens the
authoring plane, which must select exactly one declaration to edit. Chosen, plus
a test pinning the contract in both directions.

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
…cate id

Three fail-open edges on the routing planes:

- `select_index` resolved candidates deduplicated by agent ID and then took the
  *first* position for the winning ID, so on a catalog with a duplicate ID the
  resolved subject and the delivered directory could be different declarations.
  `event.rs` had the same shape on the declaration key. Both now refuse.
- `Ambiguous` deduplicated its names after deciding, so two declarations sharing
  one ID rendered as "names 1 subjects". The names are now one per surviving
  subject, and the noun agrees with the count.
- Message and state resolution passed no pinned host while stream ingress pinned
  the local host first, so `st2 message send chat` and `st2 event emit chat`
  disagreed on a catalog where two hosts declare one address. The local-first
  step now lives in `identity` and both planes call it; `event.rs` loses its copy.

Also states the retired-address rule as the code implements it: retirement
releases the address for claiming, and a retired subject answers on its own
address only when nothing routable does - which is what keeps its retained state
reachable by name (Q3).
The refusal forwarded `validate`'s `dup-address` message, which names the first
declaration in path order - usually the candidate's own file, the one subject the
operator knows is not the conflict. This function already holds the prospective
specs, so it names the other claimant: the subject reading the same effective
address on this host whose declaration key differs from the target.
`--as` is an ordinary address reference while `$ST_AGENT` carries the exact agent
ID, and this stack is what makes the two strings diverge - so a script passing
`--as "$ST_AGENT"` breaks the moment its subject declares an address. The flag
help now says which namespace it reads instead of reading as "the same value,
with a default".
@schickling-assistant
schickling-assistant force-pushed the schickling-assistant/2026-09-05-agent-address-writer branch from e8c148e to 591ca3c Compare September 6, 2026 12:09
@schickling-assistant
schickling-assistant merged commit 1c858ee into main Sep 6, 2026
1 of 2 checks passed
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