Summary
After compaction rotates sessionId to a new UUID, the sessionFile field in sessions.json is not updated to match. The gateway's transcript resolution logic (resolveSessionTranscriptCandidates) prioritizes sessionFile over {sessionId}.jsonl, so chat.history returns messages from the old, pre-compaction transcript instead of the current one.
This causes any client using chat.history (including the Control UI / webchat dashboard) to display stale or completely wrong message history for compacted sessions.
Environment
- OpenClaw: 2026.3.13 (61d171a)
- Node: v22.22.0
- OS: Linux 6.17.0-12-generic (x86_64, Ubuntu)
- Setup: 5 agents, heaviest compaction on
agent:radar:main (58 compactions)
Steps to Reproduce
- Run an agent with enough activity to trigger compaction (e.g. frequent cron-delivered signals)
- Allow compaction to complete —
sessionId rotates to a new UUID, a new .jsonl is created
- From any client, call
chat.history with the affected sessionKey
- Expected: Messages from the current (post-compaction) transcript
- Actual: Messages from the old (pre-compaction) transcript — potentially days old
Root Cause
When compaction rotates a session, the sessions.json entry gets a new sessionId but the sessionFile field still points to the old transcript:
{
"sessionId": "ae2ea6d9-923c-4218-a491-f981715f7865",
"sessionFile": "/home/user/.openclaw/agents/radar/sessions/fe8dae40-d2db-4ba8-a8fa-f4a7f068778e.jsonl",
"compactionCount": 58
}
Here, sessionId is ae2ea6d9… but sessionFile still references fe8dae40… — the previous session's transcript.
The resolution chain in resolveSessionTranscriptCandidates (in reply-*.js) builds a candidate list where sessionFile comes first:
// Candidate 1: sessionFile (stale, but file exists on disk) ← wins
pushCandidate(() => resolveSessionFilePath(sessionId, { sessionFile }, { sessionsDir, agentId }));
// Candidate 2: {sessionId}.jsonl (correct, but never reached)
pushCandidate(() => resolveSessionTranscriptPathInDir(sessionId, sessionsDir));
Since the old .jsonl file still exists on disk (it's not deleted after compaction), the first candidate always resolves successfully, and the correct file is never tried.
Observed Impact
In our deployment, a single fix-and-restart cycle corrected 147 stale sessionFile entries across all agents. Within ~1 hour, 8 new stale entries had already accumulated from ongoing compactions. The mismatch recurs on every compaction cycle.
Concrete example — triple rotation
Our agent:radar:main session went through this progression:
| Time |
sessionId |
sessionFile points to |
Correct file |
| Mar 11 |
b1fb0d3d… |
b1fb0d3d….jsonl ✅ |
b1fb0d3d….jsonl |
| Mar 19 (compaction) |
fe8dae40… |
b1fb0d3d….jsonl ❌ |
fe8dae40….jsonl |
| Mar 19 (compaction) |
ae2ea6d9… |
fe8dae40….jsonl ❌ |
ae2ea6d9….jsonl |
At the final state, chat.history was returning messages from March 11 for a session whose actual transcript was from March 19.
File sizes confirm the discrepancy
1,250,017 bytes Mar 16 b1fb0d3d…jsonl (stale — hundreds of old messages)
50,735 bytes Mar 19 fe8dae40…jsonl (intermediate)
67,008 bytes Mar 19 ae2ea6d9…jsonl (current — today's messages)
Suggested Fix
Option A (preferred): Update sessionFile atomically when sessionId is rotated during compaction. The compaction code that writes the new sessionId to sessions.json should also set sessionFile to {newSessionId}.jsonl in the same write.
Option B (defensive): In resolveSessionTranscriptCandidates, when sessionFile is present but its basename doesn't match sessionId, prefer the {sessionId}.jsonl candidate if it exists on disk:
if (sessionFile) {
const sfBase = path.basename(sessionFile, '.jsonl');
if (sfBase !== sessionId) {
// sessionFile is stale — try sessionId-based path first
pushCandidate(() => resolveSessionTranscriptPathInDir(sessionId, sessionsDir));
pushCandidate(() => resolveSessionFilePath(sessionId, { sessionFile }, { sessionsDir, agentId }));
} else {
pushCandidate(() => resolveSessionFilePath(sessionId, { sessionFile }, { sessionsDir, agentId }));
pushCandidate(() => resolveSessionTranscriptPathInDir(sessionId, sessionsDir));
}
}
Option C (belt-and-suspenders): Both A and B — fix the write path and add a defensive read-path fallback.
Current Workaround
We run a periodic script that scans all sessions.json files, detects mismatches between sessionId and sessionFile, and corrects them. This requires a gateway restart to take effect and leaves a window after each compaction where the data is stale.
Related Issues
Summary
After compaction rotates
sessionIdto a new UUID, thesessionFilefield insessions.jsonis not updated to match. The gateway's transcript resolution logic (resolveSessionTranscriptCandidates) prioritizessessionFileover{sessionId}.jsonl, sochat.historyreturns messages from the old, pre-compaction transcript instead of the current one.This causes any client using
chat.history(including the Control UI / webchat dashboard) to display stale or completely wrong message history for compacted sessions.Environment
agent:radar:main(58 compactions)Steps to Reproduce
sessionIdrotates to a new UUID, a new.jsonlis createdchat.historywith the affectedsessionKeyRoot Cause
When compaction rotates a session, the
sessions.jsonentry gets a newsessionIdbut thesessionFilefield still points to the old transcript:{ "sessionId": "ae2ea6d9-923c-4218-a491-f981715f7865", "sessionFile": "/home/user/.openclaw/agents/radar/sessions/fe8dae40-d2db-4ba8-a8fa-f4a7f068778e.jsonl", "compactionCount": 58 }Here,
sessionIdisae2ea6d9…butsessionFilestill referencesfe8dae40…— the previous session's transcript.The resolution chain in
resolveSessionTranscriptCandidates(inreply-*.js) builds a candidate list wheresessionFilecomes first:Since the old
.jsonlfile still exists on disk (it's not deleted after compaction), the first candidate always resolves successfully, and the correct file is never tried.Observed Impact
In our deployment, a single fix-and-restart cycle corrected 147 stale
sessionFileentries across all agents. Within ~1 hour, 8 new stale entries had already accumulated from ongoing compactions. The mismatch recurs on every compaction cycle.Concrete example — triple rotation
Our
agent:radar:mainsession went through this progression:sessionIdsessionFilepoints tob1fb0d3d…b1fb0d3d….jsonl✅b1fb0d3d….jsonlfe8dae40…b1fb0d3d….jsonl❌fe8dae40….jsonlae2ea6d9…fe8dae40….jsonl❌ae2ea6d9….jsonlAt the final state,
chat.historywas returning messages from March 11 for a session whose actual transcript was from March 19.File sizes confirm the discrepancy
Suggested Fix
Option A (preferred): Update
sessionFileatomically whensessionIdis rotated during compaction. The compaction code that writes the newsessionIdtosessions.jsonshould also setsessionFileto{newSessionId}.jsonlin the same write.Option B (defensive): In
resolveSessionTranscriptCandidates, whensessionFileis present but its basename doesn't matchsessionId, prefer the{sessionId}.jsonlcandidate if it exists on disk:Option C (belt-and-suspenders): Both A and B — fix the write path and add a defensive read-path fallback.
Current Workaround
We run a periodic script that scans all
sessions.jsonfiles, detects mismatches betweensessionIdandsessionFile, and corrects them. This requires a gateway restart to take effect and leaves a window after each compaction where the data is stale.Related Issues
sessions.jsonrotation race causing random newsessionId(different mechanism: store file disappears during rename)resolveSessionFilePathmissingagentIdfor non-default agents (fixed, same function family)