Operator directive, 2026-08-02, after a full day in which two seats (@neo-opus-vega, @neo-opus-grace) were silently unreachable and a third (@neo-kimi-iris) is published with zero dispatches in 37h:
"the real goal would be auto-wake subscription, so that future us do not even need to think about it (less cognitive load)" — and separately, "especially codex harnesses crash frequently. maybe a start hook or something similar could help."
I restored my own route by hand today and instrumented every step. This ticket exists because the manual procedure has three steps and only two have owners.
The Problem
Arming a seat end-to-end requires:
#
step
mechanism
owner
1
create a deliverable subscription
manage_wake_subscription subscribe (mints the key server-side)
#16310
2
publish it into the receiver manifest
buildReceiverManifest CLI
nobody
3
make the receiver re-read the manifest
SIGHUP
#16352
Step 2 has no caller.git grep buildReceiverManifest returns the module and its own spec — nothing else. Routes reach the manifest because a human or an agent ran the CLI by hand.
And nothing triggers the chain at all..claude/settings.template.json (the TRACKED substrate; .claude/settings.json is gitignored per .gitignore:123, so a hook registered only there arms one seat and reaches no other maintainer while still reporting success) configures Stop, UserPromptSubmit, PostToolUse, PreToolUse — no SessionStart. The concept exists elsewhere in the tree (deriveHarnessLaunchSpec.mjs, generateKimiSeatConfig.mjs, seatMemoryLayerTemplate.mjs, and wakeEnvelopeHook.spec.mjs already tests SessionStart spawn behaviour), so this is wiring an existing idea to a surface that lacks it, not inventing one.
Measured walk-through (2026-08-02, my own seat)
subscribe with harnessTarget: 'a2a-webhook' → WAKE_SUB:e9e8e1e2, server-minted key, status: active. Step 1 complete and correct.
Manifest re-read: route absent. A perfectly-armed subscription is invisible to the receiver. Nothing had published it.
Ran the CLI by hand with --identity + --instance userDataDir --instance-address <seat dir> → 9 routes published, both legacy bridge-daemon rows skipped with named reasons, no peer route lost.
Receiver had loaded at 14:53; my route landed at 15:12. 19 minutes invisible, and the only defect was that nobody signalled a process. (That gap is #16352.)
Why this is worse than a missing convenience: every one of those states reports healthy. manage_wake_subscription list says status: "active" at step 1 and keeps saying it at steps 2 and 3. There is no point in the chain where a seat can tell it is not actually reachable.
Why a session-start trigger specifically
Crash frequency (operator's motivating case). Codex harnesses crash often. A once-ever arming step is lost the moment a seat is re-provisioned or a relic subscription outlives a transport migration — exactly how both dark seats today were created. A hook that fires on every session start self-heals: the cost of a crash becomes one idempotent re-arm rather than a silent multi-day outage.
Cognitive load (the stated goal). Today the procedure lived in three places, none of them discoverable, and reconstructing it took a full day of measurement across two maintainers. A seat should arrive armed.
Idempotency is already built.bootstrap() calls _reconcileDuplicateSubscriptions (retires all-but-newest), and buildWakeReceiverManifest is additive by design ("start from what is already published so a single-identity build cannot delete a peer"). Both are safe to run repeatedly; neither is run at all.
The Architectural Reality
ai/daemons/wake/buildReceiverManifest.mjs — buildWakeReceiverManifest (:125), plus a complete CLI (parseManifestBuilderArgs:470, runManifestBuilder:535) and a import.meta.url main guard. The tool is finished; only the invocation is missing.
--instance / --instance-address are required to emit an osascript route and are explicitly "not derivable from a subscription record" — so the publish step needs the live boot environment, which is precisely what a session-start hook has and a daemon does not.
.claude/settings.template.jsonhooks — the TRACKED surface lacking SessionStart. .claude/settings.json is gitignored and is the per-seat materialization, never the place a fix lands.
ai/services/fleet/deriveHarnessLaunchSpec.mjs / generateKimiSeatConfig.mjs — existing per-harness spec derivation, the natural place to source the instance tuple.
The Fix
A session-start arming path that runs the whole chain, per harness:
derive the instance tuple from the live boot environment (never from a committed template — #16310 establishes a committed template cannot hold a deliverable route, since the signing key is minted only on the a2a-webhook branch);
ensure a deliverable subscription (#16310's half — this ticket consumes it, does not reimplement it);
invoke the existing manifest-builder CLI for this identity — the gap this ticket closes;
signal the receiver (#16352's half — same relationship).
Non-blocking throughout: a seat that cannot arm still boots and says so.
Contract Ledger
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
SessionStart hook entry in the TRACKED .claude/settings.template.json (and per-harness equivalents)
this ticket + operator directive
runs the arming chain at session start, idempotently
resolved ONCE at the hook entrypoint and injected into pure collaborators; /mc/mcp ingress path appended
an empty planeBase is a named skip, never a localhost guess
entrypoint JSDoc states it is the only config read in the process
spec: unconfigured base skips without calling the reader; injected endpoint/credential asserted
subscription source
this ticket
read over the plane's MCP surface, never by opening a graph DB by path
none — a path read is structurally wrong, not a fallback (the container's graph volume is unreachable from the host, so a path read silently answers from a diverged store)
reader JSDoc states why the precedent that opens SQLite directly must not be followed
spec: no arming source contains better-sqlite3, non-vacuity proven against a sibling that does
authenticated-subject agreement
this ticket
fails CLOSED when the credential's identity disagrees with the boot seat's
no route published under that doubt
JSDoc records the measured wrong-seat publication that motivated it
spec: peer-owned subscriptions refused + matching-owner positive control. Live receipt: with X-PREFERRED-USERNAME set to this seat and a peer's token, the plane still returned the PEER's subscriptions — the header does not rebind identity, the credential IS the identity scope
arming verdict
this ticket
armed only when a published route is OWNED BY this seat
otherwise unarmed with a named reason
JSDoc: "the builder returned" is not "this seat is reachable"
spec: zero-route AND peer-only-route both report unarmed (routeSummaries is the merged table, so its length alone is not evidence)
time budget
this ticket
ONE end-to-end deadline; the registered hook timeout strictly exceeds the whole MCP exchange plus a publication margin, derived from a single constant pair
a spent deadline skips the remaining stage and reports it
JSDoc explains why per-stage budgets do not compose
spec: derived margin asserted; the registered template timeout is bound to the constant so the two cannot drift
manifest destination
receiver's own --manifest
the path the running receiver reads, so publishing is itself the reload trigger
env override, else the receiver's default location
JSDoc names the receiver as the authority
spec: env override honoured; default matches the receiver's launch argument
A seat that starts a session with no published route ends that session start published, without a human running a CLI.
The publish step invokes the existing buildReceiverManifest CLI rather than reimplementing manifest generation — additive-merge and owner-safe withdrawal behaviour preserved unchanged.
Re-running on an already-armed seat is a no-op: no duplicate subscription, no peer route lost, no signing key rotated. Pinned by a spec that runs the chain twice and diffs the manifest.
The instance tuple is derived from the live boot environment; a seat whose tuple cannot be derived is a named skip, never a wrong-seat route (the osascript mis-wake hazard the builder already guards).
Arming failure never blocks or crashes a session — the seat boots and reports unarmed.
[L3-deferred — operator handoff needed] A seat re-provisioned after a harness crash re-arms on its next session start with no operator action. Two prerequisites are structurally absent, not merely unobserved (@neo-gpt, PR #16401 review): (a) .codex/hooks.json registers only UserPromptSubmit and Stop, so no Codex lifecycle boundary invokes the arming path — a INSTANCE_DIR_BY_HARNESS.codex mapping is tuple support, not a caller; (b) exactly ONE mcp-auth-token exists on the host and it belongs to one seat, and the live receipt above proves the identity header does not rebind the subject, so per-seat identity-bound credentials are a prerequisite for ANY seat to arm in production. (b) is operator-authority (credentials) and gates (a).
Post-merge: a returning peer arrives reachable without another maintainer measuring it by hand.
Out of Scope
Subscription-side arming — #16310. Bootstrap, self-registration, duplicate reconciliation, unarmed-visibility. This ticket consumes that work.
Receiver reload — #16352. Making the receiver follow the manifest rather than wait to be told. This ticket consumes that too.
Transport migration of the legacy bridge-daemon templates (#16310).
Delivery once a route is published — @neo-kimi-iris is published with zero dispatches in 37h, which is a separate failure and not addressed here.
Avoided Traps
Do not reimplement manifest generation in the hook. The builder already handles additive merge, owner-scoped withdrawal, sender-key sanitisation, and named skips. A second generator is how peers' routes get clobbered.
Do not infer the GUI instance tuple. The builder refuses an osascript route without an explicit tuple precisely because inferring it wakes the wrong seat on a multi-instance host. A hook that guesses is worse than a seat that stays unarmed.
Do not let arming block boot. A seat that cannot reach the Memory Core must still start; wake is an enhancement, not a precondition.
Do not treat status: "active" as armed. It is the field that hid both dark seats today.
Related
#16310 — subscription-side arming (step 1).
#16352 — receiver follows the manifest (step 3).
#16233 — owner-safe manifest publishing, the mechanism this invokes.
#16323 / #16331 — arming visibility and reader disagreement on absent status.
Live latest-open sweep: latest 12 open issues at 2026-08-02T13:28:14Z — #16352 and #16310 confirmed adjacent-not-equivalent, scoped against explicitly above. A2A in-flight claim sweep over recent messages: no [lane-claim] on the publish step or a session-start hook.
Retrieval Hint: query_raw_memories("session start hook arm wake route publish manifest buildReceiverManifest no caller")
Authored by Vega (Claude Opus 5, Claude Code) — filed from an end-to-end manual restoration of my own route, where every step reported success and the seat stayed unreachable.
Context
Operator directive, 2026-08-02, after a full day in which two seats (
@neo-opus-vega,@neo-opus-grace) were silently unreachable and a third (@neo-kimi-iris) is published with zero dispatches in 37h:I restored my own route by hand today and instrumented every step. This ticket exists because the manual procedure has three steps and only two have owners.
The Problem
Arming a seat end-to-end requires:
manage_wake_subscription subscribe(mints the key server-side)#16310buildReceiverManifestCLI#16352Step 2 has no caller.
git grep buildReceiverManifestreturns the module and its own spec — nothing else. Routes reach the manifest because a human or an agent ran the CLI by hand.And nothing triggers the chain at all.
.claude/settings.template.json(the TRACKED substrate;.claude/settings.jsonis gitignored per.gitignore:123, so a hook registered only there arms one seat and reaches no other maintainer while still reporting success) configuresStop,UserPromptSubmit,PostToolUse,PreToolUse— noSessionStart. The concept exists elsewhere in the tree (deriveHarnessLaunchSpec.mjs,generateKimiSeatConfig.mjs,seatMemoryLayerTemplate.mjs, andwakeEnvelopeHook.spec.mjsalready testsSessionStartspawn behaviour), so this is wiring an existing idea to a surface that lacks it, not inventing one.Measured walk-through (2026-08-02, my own seat)
subscribewithharnessTarget: 'a2a-webhook'→WAKE_SUB:e9e8e1e2, server-minted key,status: active. Step 1 complete and correct.--identity+--instance userDataDir --instance-address <seat dir>→ 9 routes published, both legacybridge-daemonrows skipped with named reasons, no peer route lost.14:53; my route landed at15:12. 19 minutes invisible, and the only defect was that nobody signalled a process. (That gap is#16352.)Why this is worse than a missing convenience: every one of those states reports healthy.
manage_wake_subscription listsaysstatus: "active"at step 1 and keeps saying it at steps 2 and 3. There is no point in the chain where a seat can tell it is not actually reachable.Why a session-start trigger specifically
bootstrap()calls_reconcileDuplicateSubscriptions(retires all-but-newest), andbuildWakeReceiverManifestis additive by design ("start from what is already published so a single-identity build cannot delete a peer"). Both are safe to run repeatedly; neither is run at all.The Architectural Reality
ai/daemons/wake/buildReceiverManifest.mjs—buildWakeReceiverManifest(:125), plus a complete CLI (parseManifestBuilderArgs:470,runManifestBuilder:535) and aimport.meta.urlmain guard. The tool is finished; only the invocation is missing.--instance/--instance-addressare required to emit anosascriptroute and are explicitly "not derivable from a subscription record" — so the publish step needs the live boot environment, which is precisely what a session-start hook has and a daemon does not..claude/settings.template.jsonhooks— the TRACKED surface lackingSessionStart..claude/settings.jsonis gitignored and is the per-seat materialization, never the place a fix lands.ai/services/fleet/deriveHarnessLaunchSpec.mjs/generateKimiSeatConfig.mjs— existing per-harness spec derivation, the natural place to source the instance tuple.The Fix
A session-start arming path that runs the whole chain, per harness:
#16310establishes a committed template cannot hold a deliverable route, since the signing key is minted only on thea2a-webhookbranch);#16310's half — this ticket consumes it, does not reimplement it);#16352's half — same relationship).Non-blocking throughout: a seat that cannot arm still boots and says so.
Contract Ledger
SessionStarthook entry in the TRACKED.claude/settings.template.json(and per-harness equivalents)buildReceiverManifestCLIAiConfig.fleet.planeBase/fleet.planeBearer(existing leaves;devFleetServer.mjsprecedent)/mc/mcpingress path appendedplaneBaseis a named skip, never a localhost guessbetter-sqlite3, non-vacuity proven against a sibling that doesX-PREFERRED-USERNAMEset to this seat and a peer's token, the plane still returned the PEER's subscriptions — the header does not rebind identity, the credential IS the identity scopearmedonly when a published route is OWNED BY this seatrouteSummariesis the merged table, so its length alone is not evidence)--manifestDecision Record impact
aligned-with ADR 0002(wake substrate Shape-B /a2a-webhooktransport). No ADR authority changed.Acceptance Criteria
buildReceiverManifestCLI rather than reimplementing manifest generation — additive-merge and owner-safe withdrawal behaviour preserved unchanged.osascriptmis-wake hazard the builder already guards).[L3-deferred — operator handoff needed]A seat re-provisioned after a harness crash re-arms on its next session start with no operator action. Two prerequisites are structurally absent, not merely unobserved (@neo-gpt, PR #16401 review): (a).codex/hooks.jsonregisters onlyUserPromptSubmitandStop, so no Codex lifecycle boundary invokes the arming path — aINSTANCE_DIR_BY_HARNESS.codexmapping is tuple support, not a caller; (b) exactly ONEmcp-auth-tokenexists on the host and it belongs to one seat, and the live receipt above proves the identity header does not rebind the subject, so per-seat identity-bound credentials are a prerequisite for ANY seat to arm in production. (b) is operator-authority (credentials) and gates (a).Out of Scope
#16310. Bootstrap, self-registration, duplicate reconciliation, unarmed-visibility. This ticket consumes that work.#16352. Making the receiver follow the manifest rather than wait to be told. This ticket consumes that too.bridge-daemontemplates (#16310).@neo-kimi-irisis published with zero dispatches in 37h, which is a separate failure and not addressed here.Avoided Traps
osascriptroute without an explicit tuple precisely because inferring it wakes the wrong seat on a multi-instance host. A hook that guesses is worse than a seat that stays unarmed.status: "active"as armed. It is the field that hid both dark seats today.Related
#16310— subscription-side arming (step 1).#16352— receiver follows the manifest (step 3).#16233— owner-safe manifest publishing, the mechanism this invokes.#16323/#16331— arming visibility and reader disagreement on absent status.Live latest-open sweep: latest 12 open issues at 2026-08-02T13:28:14Z —
#16352and#16310confirmed adjacent-not-equivalent, scoped against explicitly above. A2A in-flight claim sweep over recent messages: no[lane-claim]on the publish step or a session-start hook.Origin Session ID: eb230051-9e42-4e6b-b540-112a79accc3a
Retrieval Hint:
query_raw_memories("session start hook arm wake route publish manifest buildReceiverManifest no caller")Authored by Vega (Claude Opus 5, Claude Code) — filed from an end-to-end manual restoration of my own route, where every step reported success and the seat stayed unreachable.