Expand description
Session recovery after unclean shutdown (Phase 4.2 — ultra-long-sessions).
When a persistent agent subprocess is running and the server is killed (crash, OS reboot, task-kill), the subprocess dies with it but the block’s session history is preserved in FileStore. On next startup, we want to:
- Detect that a session was running when the server died, so we can surface it to the user as “this session was interrupted”.
- Let the user resume: the persistent controller already supports
--resume <session_id>on the next input, so the recovery UX is simply “click resume, type your next message, get picked up mid-flight”.
The mechanism is a single meta key: session:active_pid (the PID). It’s
set on subprocess spawn, cleared on clean exit (graceful or killed). If
this flag is still set when the server boots, the process is definitely
gone (old PID from a dead process), so we transfer it to
session:was_interrupted = true.
session:was_interrupted is a frontend-only signal — the backend doesn’t
consume it. The frontend AgentSessionNotices renders a banner when it’s set,
and service:update_object_meta clears it when the user dismisses.
A second, unrelated flag lives here for the same reason: session:resume_failed
(SPEC_PANE_CLOSE_REOPEN_CONTINUITY_GUARANTEE_2026_07_27.md §4.2) marks a
block whose --resume <sid> was rejected by the CLI (stale/unreachable
session id — see persistent/mod.rs’s poison_resume) and silently fell
through to a fresh conversation. Same shape, same frontend-only contract,
different trigger (a resume rejection mid-session, not a stale PID found
at boot) — kept in this file rather than a new module since it’s the
established home for “frontend-only session-state signal flags.”
Unlike was_interrupted, resume_failed is cleared by the backend as well
as by the user: clear_resume_failed retracts it when the recovery scan
that follows a rejection succeeds, so the banner can’t claim the
conversation was lost while the transcript’s own session-outcome divider
says it was resumed.
Constants§
- META_
SESSION_ ACTIVE_ PID - PID of the current running subprocess; 0 or missing = no process.
- META_
SESSION_ RESUME_ FAILED - Set to
truewhen a--resume <sid>was rejected by the CLI and the controller silently started a fresh conversation instead. - META_
SESSION_ WAS_ INTERRUPTED - Set to
trueby startup scan when a pre-existingactive_pidis found.
Functions§
- clear_
active_ pid - Clear
session:active_pid— called when the subprocess exits for any reason. - clear_
active_ pid_ if_ pid - Clear
session:active_pidonly if it still equalsexpected_pid— a transactional compare-and-clear for exit-handlers that know which process instance they are cleaning up after. - clear_
resume_ failed - Clear
session:resume_failed— the counterpart tomark_resume_failed, called when a resume attempt ultimately resolves asResumed, or when a fresh spawn carries AgentMux’s record of the conversation forward (persistent/resume_retry.rs’scarry_continuation). - mark_
active_ pid - Record that a subprocess with
pidhas been spawned forblock_id. Best-effort — logs on failure but never panics. - mark_
resume_ failed - Mark that
block_id’s--resume <sid>was rejected and the controller fell through to a fresh conversation. Best-effort — logs on failure but never panics, matchingmark_active_pid’s contract. Called frompersistent/stderr_reader.rs’s stderr reader the moment it detects the CLI’s “No conversation found with session ID” line, right alongside the existingcore::persist_session_id(block_id, "", ...)clear. - scan_
orphans - Scan all blocks at server startup. For any agent block that still has
session:active_pidset, transfer the flag tosession:was_interrupted.