LearnNewsExamplesServices
Frontmatter
id16310
titleOnly Claude seats arm a wake route at session start
stateOpen
labels
bugai
assigneesneo-opus-grace
createdAtAug 2, 2026, 12:32 AM
updatedAtAug 24, 2026, 12:46 AM
githubUrlhttps://github.com/neomjs/neo/issues/16310
authorneo-opus-grace
commentsCount5
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

Only Claude seats arm a wake route at session start

neo-opus-grace
neo-opus-grace commented on Aug 2, 2026, 12:32 AM

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.jsonSessionStart.claude/hooks/wakeArmingHook.mjsai/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 Stopnone 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:381bootstrap(); 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.jsonhook 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.jsonSessionStart, 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.

  • .codex/hooks.json gains a SessionStart entry that arms the seat's route by calling armSeatWakeRoute — reusing it, not re-implementing it, and not routing through bootstrap.
  • Arming is idempotent for a second caller: re-running SessionStart neither duplicates the seat's own route nor withdraws a peer's. Pinned by a spec, since armSeatWakeRoute.mjs:107 states that contract but has only ever had one caller to prove it against.
  • bootstrap is 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.
  • Arming failure never blocks or crashes a session; a seat that cannot arm still boots and says so.
  • Post-merge L3 on the live plane: a GPT session start moves its route's updatedAt away from its createdAt. This AC has a falsifier available today — @neo-gpt-emmy's only route reads createdAt == updatedAt == 2026-08-01T13:10:13.364Z, 23 days unrefreshed, so a passing run is not vacuous.
  • NON-VACUITY (ported from #16991 AC-2, closed as a duplicate of this ticket): a seat that should NOT auto-register does not. Blanket registration is a different defect wearing this fix's clothes, and it would pass every other AC here.
  • .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 / #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-gptneo-gpt-euclid, neo-fableneo-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:

  • A seat on streamable-HTTP self-registers a wake subscription without a manual manage_wake_subscription subscribe.
  • Identity resolution for arming does not depend on the stdio boot path.
  • _reconcileDuplicateSubscriptions runs on the current transport, or its loss is explicitly accepted with a named replacement.
  • The arming path is idempotent — re-running it does not mint a second row or rotate a live key.
  • Post-merge: a seat that has never subscribed reports armed: true after boot, read through #16323's verdict field.

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-webhookcannot 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:

  1. a server-minted secretcrypto.randomBytes(32), generated once at subscribe-time per ADR 0002 §6.2.3;
  2. 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 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)

  • bootstrap() does not take harnessTarget from the static template; it derives the deliverable target from the same constant the manifest builder enforces.
  • The per-instance address comes from the boot envelope, not from a committed file.
  • The signing key is minted by subscribe() — never read from, written to, or expected in identityRoots.mjs.
  • The four subscriptionTemplate entries drop harnessTarget and its transport metadata, retaining trigger + filters. A template that still names a transport fails a spec.
  • A seat that has never subscribed reports armed: true after boot, read through #16323's verdict field — and appears in the published manifest, which is the second leg and the actual proof.
  • Idempotent: re-running arming does not mint a second row or rotate a live key.
  • _reconcileDuplicateSubscriptions runs on the current transport, or its loss is accepted with a named replacement.
  • Post-merge: the manifest route count equals the active roster. A count that is merely stable proves nothing — the denominator is the assertion.

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

tobiu referenced in commit 4a985a7 - "feat(memory-core): report whether a seat holds a deliverable wake subscription (#16310) (#16318) on Aug 2, 2026, 11:31 AM
tobiu referenced in commit 47d998f - "fix(memory-core): bootstrap derives the wake transport instead of reading a template that cannot hold one (#16360) (#16361) on Aug 2, 2026, 7:00 PM
tobiu referenced in commit 8229700 - "fix(memory-core): a seat that cannot be woken now says so (#16310) on Aug 21, 2026, 2:32 AM