MCPRuntimeWiring.build constructs ReplayingToolResultObserver with a
`{ nil }` placeholder resolver because the AgentOrchestrator that owns
the in-flight TurnID doesn't yet exist (the orchestrator consumes the
dispatcher chain we return). Before this fix, the resolver was a `let`
set once at init, so installAgent had no production write site to flip
it. Every observer.record(...) call returned at the guard
`guard let turnId = await turnIDResolver() else { return }`, and the
four-seam AGENT-10 contract (SEC-07 dual-write + ME-04 channel +
drain Task + ReplayLog writer) stayed dead for the process lifetime.
Fix:
- ReplayingToolResultObserver.turnIDResolver becomes a lock-protected
slot (OSAllocatedUnfairLock<ResolverBox> — same pattern as
InProcessConfirmationCache in JarvisMCP, async-safe in Swift 6
contexts unlike NSLock). attach(turnIDResolver:) replaces the slot;
record(...) reads via withLock + invokes the resolver.
- AgentOrchestrator gains a public actor-isolated `currentTurnID()`
accessor over `currentTurn?.id`. One actor hop per tool dispatch
is acceptable (tools fire 1–3× per turn).
- AppDelegate.installAgent calls observer.attach(...) right after
constructing the orchestrator, with `{ orchestrator?.currentTurnID() }`
(weak capture, mirroring the broker/presenter lifetime invariant).
A `systemLogger?.warning` fires if the observer isn't the expected
concrete type (defense for future refactors).
Tests added to MCPRuntimeWiringTests:
- test_attach_turnIDResolver_flipsObserverFromDeadToLive — pre-attach
record(...) drops; post-attach record(...) emits two envelopes
correlated to the attached TurnID.
- test_attach_replayLogReceivesSECDualWriteCorrelatedToTurnID —
end-to-end through the production observer wiring: observer built
with { nil }, attach(...) flips, record(...) drives the channel,
drain Task writes to a real ephemeral ReplayLog. Reads the SQLite
`events` table directly to assert exactly two `tool_result_full`
rows landed for the post-attach toolUseId, both correlated to the
attached TurnID, with zero rows for the pre-attach call.
Refs audit-2026-05-12 CRIT-1 (.planning/audit-2026-05-12/mcp-agent-audit.md).
Related: #21 (the dual-write was redundant in part because the
dispatcher already 8-KB-capped what would have been the "full" raw
side; that's fixed separately, but the SEC-07 audit trail is the
non-redundant value of dual-write — pre-sanitize vs post-sanitize
content for "did the model ever see content X?" forensics).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ToolResultPacker.pack was capping bytes that had already been capped
at the MCP dispatcher boundary by SanitizeForModel.prepareForBoundary,
making the doc-comment lie ("full blob still flows to ReplayLog ...
observability is not lossy") provably wrong. The orchestrator's
.toolResultFull replay row carried the already-capped 8 KB bytes, not
the original helper output, and the second cap was redundant.
Fix: turn ToolResultPacker.pack into a pass-through decoder. The MCP
dispatcher's prepareForBoundary call (sanitize + 8 KB head-truncate
with the …[tool-result-truncated at 8192 bytes] marker) is the sole
cap site. ReplayLog rows now carry exactly the dispatcher-prepared
bytes — never less, never re-marked. The packer's API stability is
preserved (Packed.{modelFacing, fullBytes, wasCapped, omittedByteCount}
all still present); wasCapped reports informationally from the size
signal and omittedByteCount is always zero post-fix.
Doc-comment updated to describe the new contract truthfully. Tests
updated:
- ToolResultPackerTests: TP2 / TP5 renamed to assert the packer
no longer adds the [TRUNCATED: ...] marker (the dispatcher's
…[tool-result-truncated at 8192 bytes] marker is the only one).
- OrchestratorSubmitTests:
- OS6 renamed to test_OS6_replayAndModelSeeDispatcherPreparedBytes,
asserting both replay and the model see the dispatcher's prepared
bytes (no double-cap, no double-marker).
- OS6b is the new #21 regression: a dispatcher that returns exactly
8 KB drives a persisted ReplayLog row of exactly 8 KB (verified
by opening the SQLite file directly) — not 4 KB or less.
Refs audit-2026-05-12 CRIT-2 (.planning/audit-2026-05-12/mcp-agent-audit.md).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>