-
-
Notifications
You must be signed in to change notification settings - Fork 69.2k
Bug: initSessionState uses raw session key while all read paths canonicalize — causes silent session loss on reconnect #29683
Description
Summary
initSessionState writes session entries to sessions.json using the raw key from resolveSessionKey() (e.g. "settings2"), but every read path (chat.history, sessions.list, sessions.patch) uses resolveSessionStoreKey() which canonicalizes the key (e.g. "agent:main:settings2").
After any WebSocket reconnect (gateway restart, config reload, network blip), the UI calls chat.history with the canonicalized key, finds no entry, and the next chat.send creates a fresh session UUID — silently orphaning the old transcript.
Root Cause
Write path (initSessionState in src/auto-reply/reply/session.ts):
sessionKey = resolveSessionKey(sessionScope, sessionCtxForState, mainKey);
// → returns raw key as-is (e.g. "settings2")
const entry = sessionStore[sessionKey]; // lookup under "settings2"
// ...
store[sessionKey] = sessionEntry; // saves under "settings2"Read path (loadSessionEntry in src/gateway/session-utils.ts):
const canonicalKey = resolveSessionStoreKey({ cfg, sessionKey });
// → canonicalizes to "agent:main:settings2"
const entry = store[canonicalKey]; // lookup under "agent:main:settings2" — NOT FOUNDThe two functions resolve the same input to different keys. The store write uses the short form; every subsequent read uses the canonical form. They never match.
Reproduction
- Open the Control UI webchat
- Type a custom session name (e.g.
settings2) in the session key input - Send a few messages (session is created and works)
- Trigger a gateway restart (
config.patch,config.apply, SIGUSR1, or just wait for a reconnect) - The webchat reconnects and calls
chat.history— returns empty (key mismatch) - Send a new message — a brand new session UUID is generated
- The old transcript file is now orphaned on disk
Fix
Canonicalize the key in initSessionState before any store read/write:
// Before:
sessionKey = resolveSessionKey(sessionScope, sessionCtxForState, mainKey);
// After:
const rawSessionKey = resolveSessionKey(sessionScope, sessionCtxForState, mainKey);
sessionKey = resolveSessionStoreKey({ cfg, sessionKey: rawSessionKey });This ensures all paths (write during chat.send, read during chat.history, read during sessions.list) use the identical canonical key.
Consolidates These Issues
This is the shared root cause behind multiple open issues:
- Telegram forum topic session continuity breaks after gateway restart (OOM-kill): new sessionFile created, prior history not loaded #21850 — Telegram forum topic session continuity breaks after restart (new sessionFile created)
- Bug: main session silently rolls over after restart/lock recovery; heartbeat lands in fresh main (compactionCount=0) #23615 — Main session silently rolls over after restart/lock recovery
- sessions.json rotation (rename) + unlocked store read can cause random new sessionId ("memory wipe") without /new #18572 —
sessions.jsonrotation + unlocked store read causes random new sessionIds - Sessions: orphan transcript .jsonl files accumulate in agents/main/sessions/ without cleanup #25373 — Orphan transcript
.jsonlfiles accumulate without cleanup - TUI doesn't reload history/session state after gateway restart #1663 — TUI does not reload history/session state after gateway restart
All of these describe the same symptom: after a restart or reconnect, the gateway cannot find the existing session entry and creates a new one. The orphaned files pile up.
Environment
- OpenClaw: 2026.2.22-2
- OS: macOS (arm64)
- Channel: webchat (Control UI)
- Restart method: in-process SIGUSR1 (config.patch)
Evidence
Traced from source at dist/auto-reply/reply/session.js and dist/gateway/session-utils.js. Confirmed via gateway logs showing session file creation timestamps matching the exact moment of the first post-reconnect chat.send, not the restart itself.