Memory: MemoryReplaySink.record fire-and-forget Task swallows errors silently #18

Open
opened 2026-05-12 19:04:19 +00:00 by KofTwentyTwo · 0 comments
KofTwentyTwo commented 2026-05-12 19:04:19 +00:00 (Migrated from github.com)

Summary

MemoryReplaySink.record is a fire-and-forget detached Task — failures are unobservable from MemoryStore's perspective. recordMemoryMutation calls sink.record(...) synchronously per protocol; the sink dispatches into a detached Task; any throw inside the task body (including the FK violation from M-2) is swallowed silently. MemoryStore logs success of applyOp while the replay event vanishes downstream.

Evidence

  • File: App/AppDelegate.swift:2786-2797
  • Behavior: MemoryStore reports applyOp success → ReplayLog records into pending buffer → flush fails repeatedly (M-2) → no error path back to MemoryStore. ReplayLog's record(_:for:) is non-throwing — events go into pending buffer; throw is at flush time.

Root cause / hypothesis

The actor surface is sync (MemoryReplaySinking.record(_:forTriggerTurnId:)), so a true terminal log is hard at the call site. Detached Task wraps async ReplayLog work. Without a try/catch in the Task body, throws disappear. ReplayLog itself is structurally observability-gapped: writes succeed at the call site even when flush has been failing for 30 batches.

Recommended fix

  1. In AppDelegateMemoryReplaySink.record, wrap the async body in do { await replayLog.record(...) } catch { logger.error("replay sink record failed: \(error)") }.

  2. ReplayLog should expose consecutiveFlushFailures to MemoryBootHealthProbe (or its own probe). When ≥ N, surface as degraded.

  3. Add MemoryStore comment acknowledging "best-effort with no terminal observability — failures appear only in replay.<date>.log."

Test

  • Inject failing ReplayLog → assert detached Task error is logged once per failed record
  • Probe unit: ReplayLog with consecutiveFlushFailures >= 3 → boot health probe state is degraded

Source

  • Audit: .planning/audit-2026-05-12/memory-audit.md (2026-05-12) — finding M-8 LOW

Cross-references

  • Related: #13 (M-2) — same observability gap; fixing M-2 dissolves much of the impact, but the structural gap remains
## Summary `MemoryReplaySink.record` is a fire-and-forget detached Task — failures are unobservable from MemoryStore's perspective. `recordMemoryMutation` calls `sink.record(...)` synchronously per protocol; the sink dispatches into a detached Task; any throw inside the task body (including the FK violation from M-2) is swallowed silently. MemoryStore logs success of `applyOp` while the replay event vanishes downstream. ## Evidence - File: `App/AppDelegate.swift:2786-2797` - Behavior: MemoryStore reports `applyOp` success → ReplayLog records into pending buffer → flush fails repeatedly (M-2) → no error path back to MemoryStore. ReplayLog's `record(_:for:)` is non-throwing — events go into pending buffer; throw is at flush time. ## Root cause / hypothesis The actor surface is sync (`MemoryReplaySinking.record(_:forTriggerTurnId:)`), so a true terminal log is hard at the call site. Detached Task wraps async ReplayLog work. Without a try/catch in the Task body, throws disappear. ReplayLog itself is structurally observability-gapped: writes succeed at the call site even when flush has been failing for 30 batches. ## Recommended fix 1. In `AppDelegateMemoryReplaySink.record`, wrap the async body in `do { await replayLog.record(...) } catch { logger.error("replay sink record failed: \(error)") }`. 2. ReplayLog should expose `consecutiveFlushFailures` to `MemoryBootHealthProbe` (or its own probe). When ≥ N, surface as `degraded`. 3. Add MemoryStore comment acknowledging "best-effort with no terminal observability — failures appear only in `replay.<date>.log`." ## Test - Inject failing ReplayLog → assert detached Task error is logged once per failed record - Probe unit: ReplayLog with `consecutiveFlushFailures >= 3` → boot health probe state is `degraded` ## Source - Audit: `.planning/audit-2026-05-12/memory-audit.md` (2026-05-12) — finding M-8 LOW ## Cross-references - Related: #13 (M-2) — same observability gap; fixing M-2 dissolves much of the impact, but the structural gap remains
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: KofTwentyTwo/Jarvis#18