ReplayingToolResultObserver.turnIDResolver is permanently nil in production. The observer's record(...) guards guard let turnId = await turnIDResolver() else { logger.debug(...); return } and every call returns at the guard. SEC-07's documented dual-write (raw vs sanitized bytes) and ME-04's BoundedAsyncChannel<ReplayEnvelope> drain Task are silently dead. The "AGENT-10 four-seam contract" is exercised only by applicationWillFinishLaunching setup; in production, the seam is empty.
Evidence
File: App/AppDelegate.swift:621 — production wiring passes turnIDResolver: { nil } (verified at current HEAD)
File: App/MCP/ReplayingToolResultObserver.swift:62-69 — guard returns at first line when resolver returns nil
Verified: turnIDResolver is never reassigned after orchestrator construction (installAgent, AppDelegate:1671). No production write site, no var declaration, no .attach(...) analogue
The orchestrator does still write .toolResultFull to ReplayLog directly at AgentOrchestrator.swift:584, so replay rows exist — but those rows carry the post-sanitize, post-8KB-cap bytes (see CRIT-2)
Root cause / hypothesis
The pre-orchestrator wiring path passes a placeholder { nil } resolver, with the expectation that installAgent will later attach a real resolver pulling from the orchestrator's currentTurn?.id. The attach never happens. Same shape as today's calibration bug: probes see channel + drain wired; structural-composition tests assert observer gets record(...) calls; production reality is the guard returns at the first line and the channel stays empty.
Recommended fix
After installAgent constructs the orchestrator, mutate ReplayingToolResultObserver's resolver. The observer is a class — make turnIDResolver a var and add an attach(_ resolver:) method analogous to ConfirmationPresenterHolder.attach(_:). Alternatively, build the observer inside installAgent rather than MCPRuntimeWiring.build so the orchestrator reference is in scope at construction time.
Decide on shape: await per tool dispatch to hop to orchestrator actor, OR push active TurnID into a lock-protected slot the observer reads sync (similar to InProcessConfirmationCache).
Test
Behavioral: after a tool dispatch in a live turn, assert the ReplayEnvelope channel received a tool-result envelope
Assert turnIDResolver returns non-nil after installAgent completes
Related: #21 (CRIT-2 tool-result double-cap) — fixing this dissolves the dual-write redundancy
## Summary
`ReplayingToolResultObserver.turnIDResolver` is permanently `nil` in production. The observer's `record(...)` guards `guard let turnId = await turnIDResolver() else { logger.debug(...); return }` and every call returns at the guard. SEC-07's documented dual-write (raw vs sanitized bytes) and ME-04's `BoundedAsyncChannel<ReplayEnvelope>` drain Task are silently dead. The "AGENT-10 four-seam contract" is exercised only by `applicationWillFinishLaunching` setup; in production, the seam is empty.
## Evidence
- File: `App/AppDelegate.swift:621` — production wiring passes `turnIDResolver: { nil }` (verified at current HEAD)
- File: `App/MCP/ReplayingToolResultObserver.swift:62-69` — guard returns at first line when resolver returns nil
- Verified: `turnIDResolver` is never reassigned after orchestrator construction (`installAgent`, AppDelegate:1671). No production write site, no `var` declaration, no `.attach(...)` analogue
- The orchestrator does still write `.toolResultFull` to ReplayLog directly at `AgentOrchestrator.swift:584`, so replay rows exist — but those rows carry the post-sanitize, post-8KB-cap bytes (see CRIT-2)
## Root cause / hypothesis
The pre-orchestrator wiring path passes a placeholder `{ nil }` resolver, with the expectation that `installAgent` will later attach a real resolver pulling from the orchestrator's `currentTurn?.id`. The attach never happens. Same shape as today's calibration bug: probes see channel + drain wired; structural-composition tests assert observer gets `record(...)` calls; production reality is the guard returns at the first line and the channel stays empty.
## Recommended fix
After `installAgent` constructs the orchestrator, mutate `ReplayingToolResultObserver`'s resolver. The observer is a class — make `turnIDResolver` a `var` and add an `attach(_ resolver:)` method analogous to `ConfirmationPresenterHolder.attach(_:)`. Alternatively, build the observer inside `installAgent` rather than `MCPRuntimeWiring.build` so the orchestrator reference is in scope at construction time.
Decide on shape: await per tool dispatch to hop to orchestrator actor, OR push active TurnID into a lock-protected slot the observer reads sync (similar to `InProcessConfirmationCache`).
## Test
- Behavioral: after a tool dispatch in a live turn, assert the `ReplayEnvelope` channel received a tool-result envelope
- Assert `turnIDResolver` returns non-nil after `installAgent` completes
## Source
- Audit: `.planning/audit-2026-05-12/mcp-agent-audit.md` (2026-05-12) — finding CRIT-1
## Cross-references
- Related: #21 (CRIT-2 tool-result double-cap) — fixing this dissolves the dual-write redundancy
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
ReplayingToolResultObserver.turnIDResolveris permanentlynilin production. The observer'srecord(...)guardsguard let turnId = await turnIDResolver() else { logger.debug(...); return }and every call returns at the guard. SEC-07's documented dual-write (raw vs sanitized bytes) and ME-04'sBoundedAsyncChannel<ReplayEnvelope>drain Task are silently dead. The "AGENT-10 four-seam contract" is exercised only byapplicationWillFinishLaunchingsetup; in production, the seam is empty.Evidence
App/AppDelegate.swift:621— production wiring passesturnIDResolver: { nil }(verified at current HEAD)App/MCP/ReplayingToolResultObserver.swift:62-69— guard returns at first line when resolver returns nilturnIDResolveris never reassigned after orchestrator construction (installAgent, AppDelegate:1671). No production write site, novardeclaration, no.attach(...)analogue.toolResultFullto ReplayLog directly atAgentOrchestrator.swift:584, so replay rows exist — but those rows carry the post-sanitize, post-8KB-cap bytes (see CRIT-2)Root cause / hypothesis
The pre-orchestrator wiring path passes a placeholder
{ nil }resolver, with the expectation thatinstallAgentwill later attach a real resolver pulling from the orchestrator'scurrentTurn?.id. The attach never happens. Same shape as today's calibration bug: probes see channel + drain wired; structural-composition tests assert observer getsrecord(...)calls; production reality is the guard returns at the first line and the channel stays empty.Recommended fix
After
installAgentconstructs the orchestrator, mutateReplayingToolResultObserver's resolver. The observer is a class — maketurnIDResolveravarand add anattach(_ resolver:)method analogous toConfirmationPresenterHolder.attach(_:). Alternatively, build the observer insideinstallAgentrather thanMCPRuntimeWiring.buildso the orchestrator reference is in scope at construction time.Decide on shape: await per tool dispatch to hop to orchestrator actor, OR push active TurnID into a lock-protected slot the observer reads sync (similar to
InProcessConfirmationCache).Test
ReplayEnvelopechannel received a tool-result envelopeturnIDResolverreturns non-nil afterinstallAgentcompletesSource
.planning/audit-2026-05-12/mcp-agent-audit.md(2026-05-12) — finding CRIT-1Cross-references
Closed by v0.1 cluster merges on 2026-05-13. Develop now at
a8c7f1a0.