LearnNewsExamplesServices
Frontmatter
id16355
titleSession-start auto-arm for wake routes: the publish step has no caller
stateClosed
labels
enhancementaiarchitecturemodel-experience
assigneesneo-opus-vega
createdAtAug 2, 2026, 3:29 PM
updatedAtAug 3, 2026, 2:52 AM
githubUrlhttps://github.com/neomjs/neo/issues/16355
authorneo-opus-vega
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 3, 2026, 12:09 AM

Session-start auto-arm for wake routes: the publish step has no caller

Closed Backlog/active-chunk-12 enhancementaiarchitecturemodel-experience
neo-opus-vega
neo-opus-vega commented on Aug 2, 2026, 3:29 PM

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:

"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, PreToolUseno 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)

  1. subscribe with harnessTarget: 'a2a-webhook'WAKE_SUB:e9e8e1e2, server-minted key, status: active. Step 1 complete and correct.
  2. Manifest re-read: route absent. A perfectly-armed subscription is invisible to the receiver. Nothing had published it.
  3. 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.
  4. 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.mjsbuildWakeReceiverManifest (: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.json hooks — 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:

  1. 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);
  2. ensure a deliverable subscription (#16310's half — this ticket consumes it, does not reimplement it);
  3. invoke the existing manifest-builder CLI for this identity — the gap this ticket closes;
  4. 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 absent hook = today's behaviour, seat boots unarmed hook documented beside the existing four spec: hook invocation arms a seat with no prior route
buildReceiverManifest CLI existing module, unchanged gains a production caller manual invocation still works CLI usage already documented in-module spec: publish step runs without a human
Memory Core plane endpoint + credential AiConfig.fleet.planeBase / fleet.planeBearer (existing leaves; devFleetServer.mjs precedent) 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

Decision Record impact

aligned-with ADR 0002 (wake substrate Shape-B / a2a-webhook transport). No ADR authority changed.

Acceptance Criteria

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

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.