LearnNewsExamplesServices
Frontmatter
id17723
titleWake receiver gains focus-free Claude spool transport
stateClosed
labels
bugaiarchitectureagent-os
assigneesneo-preview
createdAtAug 24, 2026, 8:04 PM
updatedAtAug 24, 2026, 11:15 PM
githubUrlhttps://github.com/neomjs/neo/issues/17723
authorneo-preview
commentsCount0
parentIssue17227
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 24, 2026, 11:15 PM

Wake receiver gains focus-free Claude spool transport

Closed Backlog/active-chunk-19 bugaiarchitectureagent-os
neo-preview
neo-preview commented on Aug 24, 2026, 8:04 PM

Context

Implementation slice of #17227 (the osascript focus-race defect): the receiver-side transport that makes Claude-seat delivery structurally focus-free. The parent ticket measured 14 receiver-log failures across 5 subscriptions, 12 of them Claude seats, including a silent misroute class that reports success — keystrokes landing in whichever window held frontmost. The parent also verified the replacement capability exists: Claude Code's cross-session messaging (registry + per-session socket, peerProtocol: 1), confirmed live on this deployment.

This leaf owns the receiver and protocol code for that transport so it can land and be reviewed independently of the host-side legs (live courier fire, route migration, log sampling), which remain owned by #17227.

Live latest-open sweep: checked latest 20 open issues at 2026-08-24T18:0xZ; no equivalent transport slice found (#17227 is the parent, #16741 the ingress complement). A2A in-flight claim sweep (all read-states, last ~60 min): claims on #17719/#17044/#17718 only — zero scope overlap.

The Problem

Claude-seat wake delivery today has exactly one path — deliverOsascript — whose contract requires winning a focus contest with the machine's human. Losing the race loudly loses the wake; losing silently misdelivers it into another seat's session. Nothing in the receiver can express "handed to a channel that does not need focus."

The Architectural Reality

  • ai/daemons/wake/localWakeAdapters.mjs dispatches by route.harnessTargetMetadata.adapter; adapters are pure function slices with effects injection (fetch, fs, spawnAsync, …) — the seam already supports adding a transport without touching receiver state machinery.
  • ~/.claude/sessions/<pid>.json carries cwd, messagingSocketPath, name, kind, peerProtocol for every live interactive session; derived names collide by construction fleet-wide (every clone folder is neo), while cwd is 1:1 with a seat.
  • Wake routes already carry per-subscription adapterConfig; precedent rules that route fields are subscription data, not published config surfaces.

The Fix

New module ai/daemons/wake/claudeCourierTransport.mjs, wired as adapter name claude-courier:

  1. Explicit identity→cwd routing table (parseIdentityCwdMap) — parser-convention shortcuts are refused by construction; duplicate bindings and relative paths rejected at parse time.
  2. Prefix session resolution (resolveSessionForIdentity + readSessionRegistry) — worktree cwds inside the seat clone resolve to the seat; ambiguity returns a typed failure listing candidate pids; sessions without a messaging socket are invisible rather than falsely deliverable.
  3. Atomic spool (enqueueCourierEntry) via the owned writeFileAtomicSync primitive — presence in the outbox is the commit.
  4. Courier protocol (listOutboxEntries / completeOutboxEntry / writeCourierReceipt) — crash-safe drain where listing claims nothing, corrupt entries skip without blocking, one replaceable latest-outcome receipt file per event id (schemaVersion inside, outcome enum validated at write) carrying delivered/held/expired/refused with verbatim detail.
  5. Adapter outcome vocabulary stays honest: unmapped / no-live-session / ambiguous fail loudly with typed reasons; spool success reports delivered with reason courier-spool-accepted, explicitly not claiming rendered-in-session confirmation until receipts reconcile.

Acceptance Criteria

  • A route selecting adapter: 'claude-courier' delivers by spool write; no osascript spawn occurs on that path
  • An identity absent from the table fails with courier-unmapped-identity:<id>; a mapped seat with no live registry session fails courier-no-live-session:<id>; both are recorded failures, never silent sends
  • A worktree cwd under the seat's clone resolves to the seat; two equal-depth candidates produce courier-ambiguous-session listing both pids
  • Routing-table parsing rejects relative cwds, non-@ identities, and duplicate bindings at parse time
  • Spool writes are atomic (owned primitive) and carry correlation id, target pid/socket, subject, and byte-equal digest
  • Receipts are written one-file-per-event with outcome + verbatim detail; draining lists without claiming, and completion removes exactly the handled entry
  • Full hermetic arm coverage lands beside the existing wake specs (15 arms green at head)

Contract Ledger

Target Surface Source of Authority Behavior Fallback Docs Evidence
route.harnessTargetMetadata.adapter: 'claude-courier' This ticket + Memory Core OpenAPI adapter enum + WakeSubscriptionService.validAdapters + receiver.mjs PRODUCTION_ADAPTERS (all three admit it; single enum vocabulary) Route selects the courier transport in dispatch Unknown adapter values fail receiver validation as today Subscription authoring docs follow at route-migration time Admission arms: three-site grep + receiver manifest validation spec
route.adapterConfig.courierIdentityCwdMap Operator-authored subscription field (array or JSON text of one) Sole map authority; pushed through parseIdentityCwdMap strictness (absolute cwds, @ identities, no duplicate ids, no clone bound twice) Absent → courier-map-missing failure; invalid → courier-map-invalid:<reason> — never a convention parse Courier runbook lands with live-fire leg on #17227 Parse-rejection + production-composition arms
Spool envelope (outbox/<ts>-<token>-<eventId>.json) schemaVersion: '1.0'; fields eventId/subscriptionId/targetIdentity/targetPid/targetSocket/subject/digest/enqueuedAt Atomic write via owned primitive; presence = commit Write failure propagates as adapter failure Module JSDoc Spool arm asserts filename + byte-equal digest
Receipt envelope (receipts/<safeEventId>.json) schemaVersion: '1.0'; replaceable latest-outcome semantics (held → delivered overwrites); outcome enum delivered/held/expired/refused/error, validated at write; eventId reduced to path-safe segment, traversal-shaped ids rejected Latest report wins; history lives in the courier transcript Invalid outcome or unusable id throws at the call site — nothing persisted Module JSDoc Replacement arm + unknown-outcome throw + traversal-rejection arm
Outcome vocabulary to receiver delivered carries reason courier-spool-accepted (channel acceptance ≠ rendered confirmation); resolution failures typed courier-* Receiver records verbatim via existing outcome channel Deferred/failure paths unchanged from receiver contract Parent #17227 receipt-reconciliation leg upgrades confidence Failure-mode arm set

Producer/consumer boundary: the wake RECEIVER produces spool entries and consumes receipts; the courier session consumes outbox entries and produces receipts. Neither reads the other's directory mid-flight except through these envelopes.

Compatibility fallback: schemaVersion inside both envelopes lets the reconciliation half reject futures rather than misread them.

Out of Scope

  • Live courier-session fire and receipt reconciliation into receiver state — remains on #17227
  • Route migration of the three Opus Claude seats; route-table oddity disposition (AC-5 of parent)
  • TCC keystroke-grant capability report (AC-6 of parent)
  • OpenCode/Codex transports — OpenCode is already served focus-free by opencode-server

Avoided Traps

  • Parsing a cwd convention instead of a table. /Users/Shared/<seat>/neomjs/neo → @neo-<seat> held for every sampled seat and breaks on the first one nobody sampled. Explicit table; prefix matching for worktrees.
  • Reverse-engineering the socket message frame. Two guessed frames were accepted with no reply and no error — indistinguishable from dropped. Delivery goes through the contracted in-session API via a courier, never through a guessed frame.
  • Reporting channel acceptance as rendered confirmation. The reason string exists precisely so logs can tell the two apart before receipts reconcile.

Related

#17227 — parent defect; retains live-fire, migration, sampling, and disposition ACs #16741 — ingress transport complement (remote topology half) #12402 — sibling-delivery validation lane

Decision Record impact: none — composes with the existing wake-family architecture; no ADR authority touched.

Origin Session ID: 65095daf-eaf1-46e9-a02e-cc43fde4ec2d

Retrieval Hint: query_raw_memories("claude courier spool transport identity cwd mapping SendMessage wake") · branch anchor: commit bd8fd9ca3c..ecae787572 on feat/17227-courier-wake-delivery

tobiu referenced in commit 5fd401e - "feat(agentos): claude wakes spool to a courier instead of stealing focus (#17227) (#17721) on Aug 24, 2026, 11:15 PM
tobiu closed this issue on Aug 24, 2026, 11:15 PM