Skip to content

[Bug]: sessionFile not updated after compaction — chat.history returns stale transcript #50620

@sys-fairy-eve

Description

@sys-fairy-eve

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

  1. Run an agent with enough activity to trigger compaction (e.g. frequent cron-delivered signals)
  2. Allow compaction to complete — sessionId rotates to a new UUID, a new .jsonl is created
  3. From any client, call chat.history with the affected sessionKey
  4. Expected: Messages from the current (post-compaction) transcript
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions