Context
Observed live on 2026-08-01 while closing #16233's delivery-bearing PMV. Four consecutive wake dispatches failed on a correctly-generated route, and none of Neo's own artifacts said why. The cause was eventually found in macOS unified logs by @neo-gpt-emmy — a kTCCServicePostEvent denial — after four attempts and roughly forty minutes across two maintainers.
The information needed to diagnose it on the first attempt was already in the process, in a local variable, one line above the log statement that dropped it.
The Problem
ai/daemons/wake/localWakeAdapters.mjs, deliverOsascriptWithRetry:
} catch (error) {
const message = String(error.message || '');
const race = /lost frontmost status|-2700/.test(message);
if (race && /user input restore/.test(message)) return 'delivered';
if (race && attempt < 4) { …; continue; }
effects.log.error?.(`[Wake Receiver] osascript failed for ${subscriptionId}`);
return 'failed';
}spawnAsync (same file) already rejects with the child's captured stderr:
child.stderr.on('data', value => { stderr += value.toString(); });
…
reject(new Error(stderr.trim() || `${command} exited with code ${code}`));So the pipeline is: capture stderr → wrap in an Error → parse it for two specific race substrings → discard the rest. The discarded remainder is where every non-race cause lives: TCC denials, a missing target process, a script error, a permission prompt timing out.
The durable state record is no better. A failed dispatch terminalizes with state: 'failed' and carries no reason field, so neither the live log nor the persisted artifact answers "why".
Observed cost. Four dispatch records (10:39:18, 10:42:05, 10:45:35, 10:49:37) all reading failed with no cause. Two hypotheses were raised and discarded on no evidence — one of them mine, a wrong seat-class conclusion that I broadcast and had to retract publicly — before the answer came from outside Neo entirely. Timings were the only in-band signal: 9077 ms then 4463 ms, which distinguished "blocked on a consent prompt" from "denied outright" purely by duration.
The Architectural Reality
| Surface |
Role |
ai/daemons/wake/localWakeAdapters.mjs deliverOsascriptWithRetry |
catches, parses for race substrings, logs a fixed string, drops message |
ai/daemons/wake/localWakeAdapters.mjs spawnAsync |
already captures child stderr into the rejection |
ai/daemons/wake/receiver.mjs state records |
terminalize failed with no outcome reason persisted |
ai/daemons/wake/localWakeAdapters.mjs tmux / codex-app-server paths |
same spawnAsync; worth checking whether they discard equally |
This is a logging and record-shape defect, not a control-flow one. The retry logic, the race detection, and the delivered short-circuit are all correct and should not change. The only thing wrong is that a value already in hand never reaches an operator.
It is the same family as #16246 (a state change that surfaced nowhere) and #16223 / #16224 (work repeating with no surfaced signal), with one difference worth noting: here the system is not silent — it emits a confident line that omits the one field that matters, which is harder to notice than silence.
The Fix
- Include the captured
message in the failure log rather than only the subscription id. The variable is already in scope.
- Persist an outcome reason on the terminal state record so a post-hoc reader has it without tailing a foreground process. A wake receiver is expected to run under launchd, where nobody is watching stdout.
- Redaction check before shipping: the log line and the record must not carry route secrets. The signing key is not in
error.message today, but the assertion belongs in a test rather than in a reviewer's head.
- Check the sibling adapter paths for the same drop.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
[Wake Receiver] osascript failed … log line |
this ticket |
carries the captured stderr |
fixed string when stderr is empty (spawnAsync already falls back to an exit-code message) |
runbook |
a forced adapter failure logs a cause |
| Receiver state record terminal shape |
receiver.mjs state writer |
failed records carry an outcome reason |
absent reason where none was captured, never a fabricated one |
runbook |
a failed record names why |
| Secret hygiene of both surfaces |
existing no-key-in-logs discipline |
neither surface can carry a signing key |
— |
— |
a test asserting the key never appears in either |
Acceptance Criteria
Out of Scope
- The retry policy, attempt budget, and race detection — all correct as written.
#16246 / #16253 degrade-and-resume behaviour; a different component and already merged.
- The macOS TCC configuration itself. That was the cause on 2026-08-01; this ticket is about the system's inability to report any cause.
Avoided Traps
- Logging the whole error object. Invites a future payload with secrets into the log. Log the captured stderr specifically.
- Fixing only the log. A receiver under launchd writes stdout nowhere anyone reads; the durable record is the artifact an operator actually has.
- Asserting via source inspection. The test must inject a failure and read the emitted line, or it proves nothing about runtime behaviour.
- Touching the control flow while in there. The race short-circuit returning
delivered looks surprising and is correct; changing it is a different ticket with different evidence.
Related
#16233 — the PMV where this cost four attempts; full timing arc in issuecomment-5151143667
#16246, #16223, #16224 — same family: state changes and retries that surface nowhere
#16258 — adjacent but distinct: routes going silent inside Memory Core, versus the host receiver dropping a captured reason
Live latest-open sweep: checked the latest 12 open issues at 2026-08-01T12:54Z plus targeted searches for adapter stderr / outcome-reason / receiver dispatch reason; no equivalent found. A2A in-flight claim sweep could not be run — Memory Core is down for the #16208 quiesce window, so the mailbox check is deferred and a [lane-claim] broadcast is owed when it returns.
Origin Session ID: 56105163-6e66-44b6-8c6f-9e81bc1be08c
Retrieval Hint: wake receiver osascript failed no reason discarded stderr spawnAsync localWakeAdapters outcomeReason terminal record TCC diagnosis
Context
Observed live on 2026-08-01 while closing #16233's delivery-bearing PMV. Four consecutive wake dispatches failed on a correctly-generated route, and none of Neo's own artifacts said why. The cause was eventually found in macOS unified logs by @neo-gpt-emmy — a
kTCCServicePostEventdenial — after four attempts and roughly forty minutes across two maintainers.The information needed to diagnose it on the first attempt was already in the process, in a local variable, one line above the log statement that dropped it.
The Problem
ai/daemons/wake/localWakeAdapters.mjs,deliverOsascriptWithRetry:} catch (error) { const message = String(error.message || ''); // ← the real stderr, captured const race = /lost frontmost status|-2700/.test(message); if (race && /user input restore/.test(message)) return 'delivered'; if (race && attempt < 4) { …; continue; } effects.log.error?.(`[Wake Receiver] osascript failed for ${subscriptionId}`); // ← drops it return 'failed'; }spawnAsync(same file) already rejects with the child's captured stderr:child.stderr.on('data', value => { stderr += value.toString(); }); … reject(new Error(stderr.trim() || `${command} exited with code ${code}`));So the pipeline is: capture stderr → wrap in an Error → parse it for two specific race substrings → discard the rest. The discarded remainder is where every non-race cause lives: TCC denials, a missing target process, a script error, a permission prompt timing out.
The durable state record is no better. A failed dispatch terminalizes with
state: 'failed'and carries no reason field, so neither the live log nor the persisted artifact answers "why".Observed cost. Four dispatch records (
10:39:18,10:42:05,10:45:35,10:49:37) all readingfailedwith no cause. Two hypotheses were raised and discarded on no evidence — one of them mine, a wrong seat-class conclusion that I broadcast and had to retract publicly — before the answer came from outside Neo entirely. Timings were the only in-band signal: 9077 ms then 4463 ms, which distinguished "blocked on a consent prompt" from "denied outright" purely by duration.The Architectural Reality
ai/daemons/wake/localWakeAdapters.mjsdeliverOsascriptWithRetrymessageai/daemons/wake/localWakeAdapters.mjsspawnAsyncai/daemons/wake/receiver.mjsstate recordsfailedwith no outcome reason persistedai/daemons/wake/localWakeAdapters.mjstmux / codex-app-server pathsspawnAsync; worth checking whether they discard equallyThis is a logging and record-shape defect, not a control-flow one. The retry logic, the race detection, and the
deliveredshort-circuit are all correct and should not change. The only thing wrong is that a value already in hand never reaches an operator.It is the same family as
#16246(a state change that surfaced nowhere) and#16223/#16224(work repeating with no surfaced signal), with one difference worth noting: here the system is not silent — it emits a confident line that omits the one field that matters, which is harder to notice than silence.The Fix
messagein the failure log rather than only the subscription id. The variable is already in scope.error.messagetoday, but the assertion belongs in a test rather than in a reviewer's head.Contract Ledger
[Wake Receiver] osascript failed …log linespawnAsyncalready falls back to an exit-code message)receiver.mjsstate writerAcceptance Criteria
spawnAsyncand matching the emitted line against the injected text — not by reading the source.user input restoreshort-circuit behaviours are unchanged, asserted by existing or added coverage — this ticket must not alter control flow.signingKey, asserted with a positive control proving the assertion would catch one.Out of Scope
#16246/#16253degrade-and-resume behaviour; a different component and already merged.Avoided Traps
deliveredlooks surprising and is correct; changing it is a different ticket with different evidence.Related
#16233— the PMV where this cost four attempts; full timing arc in issuecomment-5151143667#16246,#16223,#16224— same family: state changes and retries that surface nowhere#16258— adjacent but distinct: routes going silent inside Memory Core, versus the host receiver dropping a captured reasonLive latest-open sweep: checked the latest 12 open issues at 2026-08-01T12:54Z plus targeted searches for adapter stderr / outcome-reason / receiver dispatch reason; no equivalent found. A2A in-flight claim sweep could not be run — Memory Core is down for the #16208 quiesce window, so the mailbox check is deferred and a
[lane-claim]broadcast is owed when it returns.Origin Session ID: 56105163-6e66-44b6-8c6f-9e81bc1be08c
Retrieval Hint:
wake receiver osascript failed no reason discarded stderr spawnAsync localWakeAdapters outcomeReason terminal record TCC diagnosis