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:
- Explicit identity→cwd routing table (
parseIdentityCwdMap) — parser-convention shortcuts are refused by construction; duplicate bindings and relative paths rejected at parse time.
- 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.
- Atomic spool (
enqueueCourierEntry) via the owned writeFileAtomicSync primitive — presence in the outbox is the commit.
- 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.
- 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
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
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.mjsdispatches byroute.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>.jsoncarriescwd,messagingSocketPath,name,kind,peerProtocolfor every live interactive session; derivednames collide by construction fleet-wide (every clone folder isneo), whilecwdis 1:1 with a seat.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 nameclaude-courier:parseIdentityCwdMap) — parser-convention shortcuts are refused by construction; duplicate bindings and relative paths rejected at parse time.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.enqueueCourierEntry) via the ownedwriteFileAtomicSyncprimitive — presence in the outbox is the commit.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.deliveredwith reasoncourier-spool-accepted, explicitly not claiming rendered-in-session confirmation until receipts reconcile.Acceptance Criteria
adapter: 'claude-courier'delivers by spool write; no osascript spawn occurs on that pathcourier-unmapped-identity:<id>; a mapped seat with no live registry session failscourier-no-live-session:<id>; both are recorded failures, never silent sendscourier-ambiguous-sessionlisting both pidsContract Ledger
route.harnessTargetMetadata.adapter: 'claude-courier'WakeSubscriptionService.validAdapters+receiver.mjsPRODUCTION_ADAPTERS(all three admit it; single enum vocabulary)route.adapterConfig.courierIdentityCwdMapparseIdentityCwdMapstrictness (absolute cwds, @ identities, no duplicate ids, no clone bound twice)courier-map-missingfailure; invalid →courier-map-invalid:<reason>— never a convention parseoutbox/<ts>-<token>-<eventId>.json)schemaVersion: '1.0'; fields eventId/subscriptionId/targetIdentity/targetPid/targetSocket/subject/digest/enqueuedAtreceipts/<safeEventId>.json)schemaVersion: '1.0'; replaceable latest-outcome semantics (held → delivered overwrites); outcome enumdelivered/held/expired/refused/error, validated at write; eventId reduced to path-safe segment, traversal-shaped ids rejecteddeliveredcarries reasoncourier-spool-accepted(channel acceptance ≠ rendered confirmation); resolution failures typedcourier-*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
opencode-serverAvoided Traps
/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.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: commitbd8fd9ca3c..ecae787572onfeat/17227-courier-wake-delivery