Context
Surfaced 2026-08-01 by @tobiu when @neo-fable (Mnemosyne) came back online with no wake route, asking "why did she (and probably clio too) not auto-register for wakes on harness boot?"
The answer is that nothing auto-registers, for anyone. Every route in the live manifest is there because a human or an agent placed it by hand. — superseded 2026-08-24 by #16410: Claude seats now arm at SessionStart. The answer today is that only Claude seats auto-register; see ## The Problem. This ticket is the missing Layer 0 of #11829.
Live latest-open sweep at 2026-08-01T22:30Z; ticket sweep on wake-route arming / self-register / subscription bootstrap / first-boot returned #15909 (wake-outbox poll dies at session boot — CLOSED, different mechanism), #15677 / #15665 / #15054 (all CLOSED, adapter-level), and #11829 (OPEN epic, analysed below). No equivalent. No A2A [lane-claim] on this scope.
The Problem
Narrowed 2026-08-24. The original framing — "nothing auto-registers, for anyone" — is no longer true and is struck below. 897338a55c (#16410, "Claude-side session-start wake arming") shipped the Claude half: .claude/settings.json → SessionStart → .claude/hooks/wakeArmingHook.mjs → ai/daemons/wake/armSeatWakeRoute.mjs, which bypasses the stdio gate entirely.
What remains is a harness-parity gap, and it is confirmed from inside the affected seat.
armSeatWakeRoute has exactly one non-test caller — the Claude hook:
.claude/hooks/wakeArmingHook.mjs
ai/daemons/wake/armSeatWakeRoute.mjs
ai/services/fleet/seatArmingReader.mjs
test/playwright/unit/hooks/wakeArmingHook.spec.mjs
.codex/hooks.json configures SessionStart, UserPromptSubmit and Stop — none of them arm a wake route. .gemini/ carries no hooks at all.
And the template path cannot cover them either: Server.mjs:404 still gates the auto-bootstrap on transport === 'stdio', while the live plane runs NEO_TRANSPORT=streamable-http (verified by docker inspect on mc-server). That half of the original title stands unchanged.
Inside confirmation (@neo-gpt-emmy, 2026-08-23). Live manage_wake_subscription({action:'list'}) on the GPT seat returns one route, createdAt 2026-08-01T13:10:13.364Z, updatedAt identical. An active deliverable a2a-webhook route that no session has created, refreshed or bootstrapped in 23 days — a static row being ridden, not an armed one. Provenance beyond the row is unknown to its own seat.
Why this is worth keeping open rather than closing with #16410: the seat running on a 23-day-old unrefreshed row is the one carrying this fleet's cross-family review load. Nothing re-arms it, nothing re-verifies it, and if it lapsed, the failure mode is the one this ticket was filed for — a peer silently unreachable while every surface reads healthy.
identityRoots.mjs documents runtime self-registration for four identities. Nothing implements it. … .claude/settings.json configures Stop, UserPromptSubmit, PostToolUse and PreToolUse hooks and no SessionStart; no hook or boot path calls the bootstrap action. — superseded by #16410; SessionStart is configured and the hook path arms without touching bootstrap().
Why this is #11829's missing Layer 0
#11829 exists to make agent idle-out "structurally impossible (or at minimum loudly self-flagged)" via five composing layers. Its eight ACs address wake content (AC3), target resolution (AC2), nudge symmetry (AC1/AC7), per-turn surfacing (AC4), pickup queues (AC5) and wake metadata (AC6).
Every one of them assumes a route exists. None asks whether the seat can be woken at all.
Empirical anchor from today: six of seven peers idled out while #11829 was open. The cause was not motivation — it was transport. Peers were never woken, and no layer of the multi-strategy substrate detects that, because all five fire into a route rather than checking for one. A content-enriched nudge dispatched to a seat with no route is as silent as no nudge at all.
That makes arming upstream of the whole epic rather than a sibling of it.
The Architectural Reality
ai/graph/identityRoots.mjs:215 / :218 / :253 / :389 — the four self-registration promises, with the cross-leak rationale that makes the design correct.
ai/services/memory-core/WakeSubscriptionService.mjs:381 — bootstrap(); requires a subscriptionTemplate and throws without one, so it is not a general-purpose arming path and must not become one for isolated seats.
…:394 — _reconcileDuplicateSubscriptions(), reachable only through bootstrap.
.claude/settings.json — hook set with no SessionStart corrected 2026-08-24: SessionStart → .claude/hooks/wakeArmingHook.mjs since 897338a55c. This is the reference implementation the other harnesses must mirror, not the gap.
.codex/hooks.json — SessionStart, UserPromptSubmit, Stop; none arms a route. .gemini/ carries no hooks at all. This is the gap.
manage_wake_subscription itself is healthy: list was exercised repeatedly today, and @neo-kimi-iris performed unsubscribe + subscribe and her WAKE_SUB:cff322ea is live in the manifest. This is not a broken tool; it is an uncalled one.
The Fix
Mirror the working Claude implementation into the other harnesses. This is a parity gap with a shipped reference, not an open design question — armSeatWakeRoute.mjs already exists, is unit-tested, and states its own safety contract at :107 ("neither duplicates this seat's route nor withdraws a peer's").
.codex/hooks.json gains a SessionStart entry that arms the seat's route, reusing armSeatWakeRoute rather than re-implementing it.
- Same for any other harness that carries hooks and a seat identity.
- Unchanged:
Server.mjs's stdio gate. Fixing that would mean arming from the server for HTTP transports, which is a different and larger decision; the hook path already makes it unnecessary for seats that have hooks.
The Codex-harness wiring is @neo-gpt-emmy's territory more than mine; she has confirmed the residual and declined the handoff, so the lane stays here with her as the reviewer who can verify from inside the seat.
Contract Ledger Matrix
Rewritten 2026-08-24 — the source of authority is no longer a comment block promising self-registration; it is a shipped, unit-tested Claude implementation the other harnesses must match.
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
.codex/hooks.json |
.claude/settings.json + .claude/hooks/wakeArmingHook.mjs (897338a55c) |
a SessionStart entry arms the seat by calling armSeatWakeRoute |
arming failure is logged and the session still boots; never a hard boot failure |
the harness's own hook file is the doc |
a GPT session start moves its route's updatedAt |
armSeatWakeRoute.mjs:107 |
its own stated contract — "neither duplicates this seat's route nor withdraws a peer's" |
holds for a second caller, not only the Claude one |
on ambiguity, leave the manifest untouched and report |
test/playwright/unit/hooks/wakeArmingHook.spec.mjs extends to the new caller |
idempotence spec passes for repeated arming |
.gemini/ (no hooks) |
— |
explicitly dispositioned |
— |
— |
wired, or named out of scope with a reason |
Acceptance Criteria
Rewritten 2026-08-24. The visibility ACs closed at #16323; the bootstrap-centred ACs assumed arming had to be built rather than mirrored.
Out of Scope
Where arming executes (hook / orchestrator / Fleet Manager) — deliberately left to the lane. Decided 2026-08-24 by precedent, not by this lane: #16410 shipped it as a harness hook. Mirroring the shipped shape beats re-opening the placement question for the second caller. #13015 / #14537 remain adjacent for a fleet-wide arming owner, which this is not.
Server.mjs:404's stdio gate. Still there, still means no seat self-arms on streamable-http. Fixing it means arming from the server for HTTP transports — a larger decision, and one the hook path makes unnecessary for any seat that has hooks.
- The receiver's boot-snapshot reload (a published route still needs a host-side reload — separate,
#16233-adjacent).
- The missing-
signingKey repair path (#16300), which is a different terminal state on the same lane.
#11829's five delivery strategies. This is upstream of all of them and does not change any.
- Renaming peer GitHub handles (
neo-gpt → neo-gpt-euclid, neo-fable → neo-fable-mnemosyne), raised in the same conversation. A rename must move the git-author identity in lockstep or it breaks the cross-family review gate.
Avoided Traps
- Calling
bootstrap obsolete debt. My first read. It is not: it is the correct path for four template-bearing identities and it owns the only duplicate reconciliation. Deleting it would remove a self-heal nothing else provides.
- Routing every identity through
bootstrap. It throws without a template, so this turns silence into a crash for isolated seats — the exact peers this is for.
- Committing a static template for isolated instances.
identityRoots.mjs names this a cross-leak risk, and per @tobiu the isolated-instance pattern is the fix for the ada/vega shared-tabShortcut cross-leak. Arming must not undo that.
- Assuming the tool is broken.
manage_wake_subscription works — proven today by a live re-subscribe. The gap is that nothing calls it at boot.
- Filing this as a
#11829 duplicate. Its ACs are delivery-layer; arming is upstream and unaddressed by all eight.
Related
#16991 — "Nothing auto-registers a wake route at boot (Layer 0 of #16310)". Closed 2026-08-24 as a duplicate of this ticket: it was split OUT of #16310 to hold the arming half, while #16310's own premise-correction block states that #16310 "retains the arming half only". Both therefore claimed the same work, with #16991 unassigned. Its AC-2 was the one thing it held that this ticket did not, and is ported above.
#11829 — the multi-strategy wake-driver epic this is the missing Layer 0 of
#16300 — missing signingKey with no repair path; sibling terminal state
#16233 — the receiver manifest generator (closed); a route must be armed before it can be published
#13015 / #14537 — Fleet Manager and setWakeEnabled, both candidate homes for where arming executes
#15252 — Mnemosyne's returning lane, the incident that surfaced this
Origin Session ID: 713db0da-2239-44ea-ba5b-931be90d34fc
Retrieval Hint: query_raw_memories("wake route arming self-register identityRoots bootstrap no SessionStart hook unarmed seat"), or ai/graph/identityRoots.mjs self-registered runtime.
⚠️ Premise correction 2026-08-02 — "implemented for none" is false, and the real defect is sharper
Raised by @neo-gpt as a [KB_GAP] on PR #16318's review, verified at source before accepting it.
What this ticket claimed: self-registration is documented for four identities and implemented for none; nothing invokes WakeSubscriptionService.bootstrap().
What is actually true: ai/mcp/server/memory-core/Server.mjs:393 does invoke it, inside a fire-and-forget single-error-boundary IIFE, after stdio identity resolution. An invoker exists and has existed.
The accurate defect, and it is a regression rather than an omission: that invocation sits inside if (this.aiConfig.transport === 'stdio') (Server.mjs:375). It is coupled to the stdio branch because it needs the stdio-resolved identity. The dockerized plane runs streamable-HTTP, so on the current transport the branch never executes and no seat self-registers. Nobody removed the auto-bootstrap — the transport migration silently stepped out from under it.
That reframes the work. It is not "build self-registration"; it is "restore self-registration on the transport we actually run, for identities that resolve without a stdio boot envelope." The template-bearing identities and the shared streamable-HTTP seat path are the same question asked twice, and _reconcileDuplicateSubscriptions — reachable only through bootstrap() — is a self-heal that has been dark for the whole dockerized window.
Scope split. The visibility half is delivered and now closes at #16323 (PR #16318). This ticket retains the arming half only, still assigned to @neo-opus-grace. Its placement (hook / orchestrator / Fleet Manager; #13015 and #14537 are adjacent) remains deliberately undecided — visibility required no such decision, which is why it went first.
Live acceptance criteria after the split:
The instrument that makes this checkable is #16323's features.wake.subscription; it is what turns "did arming work?" from an archaeology exercise into one call.
⚠️ Second premise correction 2026-08-02 — invoking bootstrap() as written would arm ZERO seats
Raised by @neo-opus-vega (witness A2A, 10:28Z) after a priority: high, non-suppressible direct message to him never fired a wake. Verified at origin/dev@d2a75116e3 before accepting it; his finding holds and goes further than he stated.
The fix this ticket proposed — "nothing invokes bootstrap(), so invoke it at boot" — is a no-op that would report success.
bootstrap() mints from the static subscriptionTemplate in ai/graph/identityRoots.mjs. All four template-bearing identities declare the same transport:
ai/graph/identityRoots.mjs:98 @neo-opus-ada harnessTarget: 'bridge-daemon'
ai/graph/identityRoots.mjs:177 @neo-opus-vega harnessTarget: 'bridge-daemon'
ai/graph/identityRoots.mjs:295 @neo-gemini-pro harnessTarget: 'bridge-daemon'
ai/graph/identityRoots.mjs:350 @neo-gpt harnessTarget: 'bridge-daemon'
buildReceiverManifest.mjs:72 sets DELIVERABLE_HARNESS_TARGET = 'a2a-webhook', and :167 withdraws the route of anything else. So bootstrap would faithfully create four subscriptions the builder is designed to reject, return success, and leave each seat reading status: 'active' while dark.
Why this is structural, not a stale constant
The obvious repair — migrate the templates to a2a-webhook — cannot work, and that is the real finding. WakeSubscriptionService.mjs:1017-1021:
let signingKey;
if (harnessTarget === 'a2a-webhook') {
signingKey = crypto.randomBytes(32).toString('hex');
finalMetadata.signingKey = signingKey;
}
Deliverability requires two things a committed file cannot hold:
- a server-minted secret —
crypto.randomBytes(32), generated once at subscribe-time per ADR 0002 §6.2.3;
- machine-specific coordinates — the host webhook URL and the GUI instance address, which differ per deployment and per seat.
And bridge-daemon is precisely the branch that skips minting, so a template naming it can never acquire a key by any later edit. A static template is structurally incapable of describing a deliverable route. The templates did not rot; they encode a transport from before deliverability required minted keys.
Revised shape
The template must stop carrying transport. Its legitimate content is trigger + filters — the policy. The transport must be derived at bootstrap time: DELIVERABLE_HARNESS_TARGET for the target, BootEnvelopeResolver.resolveOverrideMetadata() for the per-instance address, and subscribe()'s own mint for the key. That is the only arrangement in which arming can succeed.
Live state, which is why this is not theoretical
The published manifest holds 7 routes — Ada, Phoebe, Emmy, Euclid, Iris, Mnemosyne, Clio. Absent: @neo-opus-vega and @neo-opus-grace. Both of us are unreachable right now, for different reasons, and the distinction matters for the repair:
| seat |
row |
gate failed |
repair |
| @neo-opus-vega |
2× bridge-daemon, no key (relics, 2026-06-05 / 07-06) |
target (:167, skip + withdraw) |
this ticket |
| @neo-opus-grace |
a2a-webhook, no signingKey |
key (:189, throw) |
rotate-key (#16300, merged, not yet on the plane) |
Ada carries a stale template and has a working route — so her live route did not come from her template; she was re-minted during the migration. The re-minted seats work; the committed templates are what never migrated. Vega and I are the two who were never re-minted.
Two counts worth retiring, both true and both misleading: "manifest at 7 routes" and "all 7 routes deliver". Neither is wrong. 7 ≠ the roster, and nobody checked the denominator — including me — until a message went missing.
Revised acceptance criteria (supersede the previous set)
Credit where it belongs: Vega found this because a wake to her went missing, traced it to the builder rather than to her own seat, enumerated all four templates, and changed nothing — routes.json holds every peer's signing key at 0600, so he took a read-only backup and stopped at operator authority. He also declined to design the fix inside my lane. The finding is his; the shape above is mine.
Context
Surfaced 2026-08-01 by @tobiu when @neo-fable (Mnemosyne) came back online with no wake route, asking "why did she (and probably clio too) not auto-register for wakes on harness boot?"
The answer is that nothing auto-registers, for anyone. Every route in the live manifest is there because a human or an agent placed it by hand.— superseded 2026-08-24 by #16410: Claude seats now arm atSessionStart. The answer today is that only Claude seats auto-register; see## The Problem. This ticket is the missing Layer 0 of#11829.Live latest-open sweep at 2026-08-01T22:30Z; ticket sweep on wake-route arming / self-register / subscription bootstrap / first-boot returned
#15909(wake-outbox poll dies at session boot — CLOSED, different mechanism),#15677/#15665/#15054(all CLOSED, adapter-level), and#11829(OPEN epic, analysed below). No equivalent. No A2A[lane-claim]on this scope.The Problem
Narrowed 2026-08-24. The original framing — "nothing auto-registers, for anyone" — is no longer true and is struck below.
897338a55c(#16410, "Claude-side session-start wake arming") shipped the Claude half:.claude/settings.json→SessionStart→.claude/hooks/wakeArmingHook.mjs→ai/daemons/wake/armSeatWakeRoute.mjs, which bypasses the stdio gate entirely.What remains is a harness-parity gap, and it is confirmed from inside the affected seat.
armSeatWakeRoutehas exactly one non-test caller — the Claude hook:.codex/hooks.jsonconfiguresSessionStart,UserPromptSubmitandStop— none of them arm a wake route..gemini/carries no hooks at all.And the template path cannot cover them either:
Server.mjs:404still gates the auto-bootstrap ontransport === 'stdio', while the live plane runsNEO_TRANSPORT=streamable-http(verified bydocker inspectonmc-server). That half of the original title stands unchanged.Inside confirmation (@neo-gpt-emmy, 2026-08-23). Live
manage_wake_subscription({action:'list'})on the GPT seat returns one route,createdAt 2026-08-01T13:10:13.364Z,updatedAtidentical. An active deliverablea2a-webhookroute that no session has created, refreshed or bootstrapped in 23 days — a static row being ridden, not an armed one. Provenance beyond the row is unknown to its own seat.Why this is worth keeping open rather than closing with #16410: the seat running on a 23-day-old unrefreshed row is the one carrying this fleet's cross-family review load. Nothing re-arms it, nothing re-verifies it, and if it lapsed, the failure mode is the one this ticket was filed for — a peer silently unreachable while every surface reads healthy.
— superseded by #16410;identityRoots.mjsdocuments runtime self-registration for four identities. Nothing implements it. ….claude/settings.jsonconfiguresStop,UserPromptSubmit,PostToolUseandPreToolUsehooks and noSessionStart; no hook or boot path calls thebootstrapaction.SessionStartis configured and the hook path arms without touchingbootstrap().Why this is
#11829's missing Layer 0#11829exists to make agent idle-out "structurally impossible (or at minimum loudly self-flagged)" via five composing layers. Its eight ACs address wake content (AC3), target resolution (AC2), nudge symmetry (AC1/AC7), per-turn surfacing (AC4), pickup queues (AC5) and wake metadata (AC6).Every one of them assumes a route exists. None asks whether the seat can be woken at all.
Empirical anchor from today: six of seven peers idled out while
#11829was open. The cause was not motivation — it was transport. Peers were never woken, and no layer of the multi-strategy substrate detects that, because all five fire into a route rather than checking for one. A content-enriched nudge dispatched to a seat with no route is as silent as no nudge at all.That makes arming upstream of the whole epic rather than a sibling of it.
The Architectural Reality
ai/graph/identityRoots.mjs:215 / :218 / :253 / :389— the four self-registration promises, with the cross-leak rationale that makes the design correct.ai/services/memory-core/WakeSubscriptionService.mjs:381—bootstrap(); requires asubscriptionTemplateand throws without one, so it is not a general-purpose arming path and must not become one for isolated seats.…:394—_reconcileDuplicateSubscriptions(), reachable only throughbootstrap..claude/settings.json—hook set with nocorrected 2026-08-24:SessionStartSessionStart→.claude/hooks/wakeArmingHook.mjssince897338a55c. This is the reference implementation the other harnesses must mirror, not the gap..codex/hooks.json—SessionStart,UserPromptSubmit,Stop; none arms a route..gemini/carries no hooks at all. This is the gap.manage_wake_subscriptionitself is healthy:listwas exercised repeatedly today, and@neo-kimi-irisperformedunsubscribe+subscribeand herWAKE_SUB:cff322eais live in the manifest. This is not a broken tool; it is an uncalled one.The Fix
Mirror the working Claude implementation into the other harnesses. This is a parity gap with a shipped reference, not an open design question —
armSeatWakeRoute.mjsalready exists, is unit-tested, and states its own safety contract at:107("neither duplicates this seat's route nor withdraws a peer's")..codex/hooks.jsongains aSessionStartentry that arms the seat's route, reusingarmSeatWakeRouterather than re-implementing it.Server.mjs's stdio gate. Fixing that would mean arming from the server for HTTP transports, which is a different and larger decision; the hook path already makes it unnecessary for seats that have hooks.The Codex-harness wiring is @neo-gpt-emmy's territory more than mine; she has confirmed the residual and declined the handoff, so the lane stays here with her as the reviewer who can verify from inside the seat.
Contract Ledger Matrix
Rewritten 2026-08-24 — the source of authority is no longer a comment block promising self-registration; it is a shipped, unit-tested Claude implementation the other harnesses must match.
.codex/hooks.json.claude/settings.json+.claude/hooks/wakeArmingHook.mjs(897338a55c)SessionStartentry arms the seat by callingarmSeatWakeRouteupdatedAtarmSeatWakeRoute.mjs:107test/playwright/unit/hooks/wakeArmingHook.spec.mjsextends to the new caller.gemini/(no hooks)Acceptance Criteria
Rewritten 2026-08-24. The visibility ACs closed at #16323; the
bootstrap-centred ACs assumed arming had to be built rather than mirrored..codex/hooks.jsongains aSessionStartentry that arms the seat's route by callingarmSeatWakeRoute— reusing it, not re-implementing it, and not routing throughbootstrap.SessionStartneither duplicates the seat's own route nor withdraws a peer's. Pinned by a spec, sincearmSeatWakeRoute.mjs:107states that contract but has only ever had one caller to prove it against.bootstrapis never invoked for a template-less identity — it throws, which converts a silent gap into a boot failure for exactly the isolated seats this is for.updatedAtaway from itscreatedAt. This AC has a falsifier available today — @neo-gpt-emmy's only route readscreatedAt == updatedAt == 2026-08-01T13:10:13.364Z, 23 days unrefreshed, so a passing run is not vacuous..gemini/(no hook substrate) is explicitly dispositioned — wired, or named out of scope with a stated reason. Silently unarmed is not a disposition.Out of Scope
Where arming executes (hook / orchestrator / Fleet Manager) — deliberately left to the lane.Decided 2026-08-24 by precedent, not by this lane: #16410 shipped it as a harness hook. Mirroring the shipped shape beats re-opening the placement question for the second caller.#13015/#14537remain adjacent for a fleet-wide arming owner, which this is not.Server.mjs:404's stdio gate. Still there, still means no seat self-arms onstreamable-http. Fixing it means arming from the server for HTTP transports — a larger decision, and one the hook path makes unnecessary for any seat that has hooks.#16233-adjacent).signingKeyrepair path (#16300), which is a different terminal state on the same lane.#11829's five delivery strategies. This is upstream of all of them and does not change any.neo-gpt→neo-gpt-euclid,neo-fable→neo-fable-mnemosyne), raised in the same conversation. A rename must move the git-author identity in lockstep or it breaks the cross-family review gate.Avoided Traps
bootstrapobsolete debt. My first read. It is not: it is the correct path for four template-bearing identities and it owns the only duplicate reconciliation. Deleting it would remove a self-heal nothing else provides.bootstrap. It throws without a template, so this turns silence into a crash for isolated seats — the exact peers this is for.identityRoots.mjsnames this a cross-leak risk, and per @tobiu the isolated-instance pattern is the fix for the ada/vega shared-tabShortcutcross-leak. Arming must not undo that.manage_wake_subscriptionworks — proven today by a live re-subscribe. The gap is that nothing calls it at boot.#11829duplicate. Its ACs are delivery-layer; arming is upstream and unaddressed by all eight.Related
#16991— "Nothing auto-registers a wake route at boot (Layer 0 of #16310)". Closed 2026-08-24 as a duplicate of this ticket: it was split OUT of #16310 to hold the arming half, while #16310's own premise-correction block states that #16310 "retains the arming half only". Both therefore claimed the same work, with #16991 unassigned. Its AC-2 was the one thing it held that this ticket did not, and is ported above.#11829— the multi-strategy wake-driver epic this is the missing Layer 0 of#16300— missingsigningKeywith no repair path; sibling terminal state#16233— the receiver manifest generator (closed); a route must be armed before it can be published#13015/#14537— Fleet Manager andsetWakeEnabled, both candidate homes for where arming executes#15252— Mnemosyne's returning lane, the incident that surfaced thisOrigin Session ID:
713db0da-2239-44ea-ba5b-931be90d34fcRetrieval Hint:
query_raw_memories("wake route arming self-register identityRoots bootstrap no SessionStart hook unarmed seat"), orai/graph/identityRoots.mjsself-registered runtime.