Summary
Follow-up from Codex on #4058 (round 23, P2), filed rather than fixed there under Mike's stopping rule for that PR.
AgentSession.sendMessage (src/node/services/agentSession.ts, the pre-persist gate around line 3823 on b795316c1e) checks the shutdown latch (shuttingDown, set by beginShutdown()) once, before it awaits materializeAgentSkillSnapshots() / materializeMcpPromptSnapshots() and appends the turn's snapshot and user rows. Codex:
If shutdown begins while materializeAgentSkillSnapshots() or materializeMcpPromptSnapshots() is awaiting, this one-time check has already passed, so the subsequent snapshot/user appends still become durable before streamWithHistory() observes the latch and declines to run. [...] leaving a recovery turn recorded as dispatched during shutdown; fold the latch into the pre-acceptance rollback checkpoints or otherwise serialize durable admission with beginShutdown().
The window is the duration of those two awaits during process shutdown. Consequence: a recovery-dispatched turn (compaction follow-up) could leave its user row in chat.jsonl with no response, so the next startup treats it as an interrupted turn and auto-retries it (a duplicate dispatch of the follow-up rather than a lost one).
Suggested direction
Re-check the latch at the pre-acceptance rollback checkpoints that already exist for the context-mutation epoch (isAdmissionStale()), so the same rollback path covers shutdown.
Generated with xum • Model: anthropic:claude-fable-5-1 • Thinking: xhigh • Cost: $202.20
Summary
Follow-up from Codex on #4058 (round 23, P2), filed rather than fixed there under Mike's stopping rule for that PR.
AgentSession.sendMessage(src/node/services/agentSession.ts, the pre-persist gate around line 3823 onb795316c1e) checks the shutdown latch (shuttingDown, set bybeginShutdown()) once, before it awaitsmaterializeAgentSkillSnapshots()/materializeMcpPromptSnapshots()and appends the turn's snapshot and user rows. Codex:The window is the duration of those two awaits during process shutdown. Consequence: a recovery-dispatched turn (compaction follow-up) could leave its user row in chat.jsonl with no response, so the next startup treats it as an interrupted turn and auto-retries it (a duplicate dispatch of the follow-up rather than a lost one).
Suggested direction
Re-check the latch at the pre-acceptance rollback checkpoints that already exist for the context-mutation epoch (
isAdmissionStale()), so the same rollback path covers shutdown.Generated with
xum• Model:anthropic:claude-fable-5-1• Thinking:xhigh• Cost:$202.20