diff --git a/src/servers/sidecar/claude/state.ts b/src/servers/sidecar/claude/state.ts index b55e63ec..59dfd5fc 100644 --- a/src/servers/sidecar/claude/state.ts +++ b/src/servers/sidecar/claude/state.ts @@ -71,14 +71,34 @@ export function updateState(patch: Partial): void { scheduleSave(); } +/** + * Both of these write through instead of debouncing. + * + * This map is the only thing that lets a conversation be reattached with `--resume` after the agent + * sidecar dies, so it is precisely the state that must not be in a 30s window when the process is + * killed rather than asked to stop — `flushAndSave` on SIGTERM covers a `pm2 restart`, but not a + * crash or a SIGKILL, which is the case the resume path exists for. Losing it silently starts the + * next turn as a fresh Claude session and orphans the transcript on disk. + * + * There is nothing to debounce: `onSessionId` fires on every message but with the same id, so the + * equality guard collapses it to one write per session, and clearing happens once. + */ export function setClaudeSession(sessionKey: string, sessionId: string): void { + if (currentState.claudeSessions[sessionKey] === sessionId) return; currentState.claudeSessions[sessionKey] = sessionId; - scheduleSave(); + writeThrough(); } export function clearClaudeSession(sessionKey: string): void { + if (!(sessionKey in currentState.claudeSessions)) return; delete currentState.claudeSessions[sessionKey]; - scheduleSave(); + writeThrough(); +} + +function writeThrough(): void { + void flushAndSave().catch((err) => { + console.error('[claude-state] save failed:', err instanceof Error ? err.message : err); + }); } export function getClaudeSession(sessionKey: string): string | undefined {