Two distinct loss shapes in paired session files (<id>.jsonl + <id>.trajectory.jsonl), observed on 2026.7.1-beta.2. Each file loses history the other retains, so any consumer that treats one file as "the transcript" (backup, archive, history reassembly, memory extraction, incident forensics) works from an incomplete record without knowing it.
Shape 1 — .jsonl head-drop
A session's .jsonl was missing its first 7 user-message entries while the paired .trajectory.jsonl recorded all 14 turns (session.started events) correctly. The .jsonl retained session-startup events (model_change, thinking_level_change) but the early user messages themselves never landed. Nothing in either file marks the gap.
Detection signature: jsonl.first_user_message_ts > trajectory.first_session_started_ts. In our case the gap was +3313s (~55 min of conversation missing from the runtime transcript).
From reading the dist source: appendJsonlEntrySync (transcript-write-context) appends per-message with no retry; a failure between the trajectory recorder's event write and the jsonl flush leaves the trajectory with session.started + prompt.submitted while the jsonl permanently lacks the user message. Side note: the message content survived in the context-engine DB (lossless-claw's messages table), which is how we confirmed the content was real — but the runtime transcript is what downstream consumers read.
Shape 2 — trajectory FIFO window (expected, but undocumented as a pairing hazard)
trimJsonlWindow drops oldest trajectory events once the file approaches TRAJECTORY_RUNTIME_CAPTURE_MAX_BYTES (10 MiB). A 3-week-old session had its trajectory at 99.4% of cap holding only the last ~25 turns, while its .jsonl held the full history. This is by design — but combined with shape 1 it means neither file is a superset of the other, and there is no marker in either file saying "the other file has what I lost."
Why it matters
Repro
Synthetic fixtures for both shapes (head-drop and rotation-with-covered-head) are exercised by a standalone checker we wrote while diagnosing this: it classifies a session pair as no-loss / rotation-truncated / anomalous / indeterminate from timestamps, counts, and coverage spans. Happy to share the script or fold it into a PR if useful.
Suggested fixes (any one of these)
- Atomic paired flush on the prompt-submitted path (trajectory event + jsonl user-message entry under one mutex; replay from the trajectory
seq cursor on crash).
- Embed the jsonl
message.id into prompt.submitted.data.messageId so downstream consumers can do record-level alignment and detect gaps (today the id schemes are disjoint: per-message uuid vs per-turn seq/sourceSeq).
- On compaction/rotation, snapshot BOTH files as a pair (
rotateTranscriptAfterCompaction currently handles only the jsonl).
Version: [email protected] · single-host LXC install · no client data in this report; timestamps and counts only.
Two distinct loss shapes in paired session files (
<id>.jsonl+<id>.trajectory.jsonl), observed on2026.7.1-beta.2. Each file loses history the other retains, so any consumer that treats one file as "the transcript" (backup, archive, history reassembly, memory extraction, incident forensics) works from an incomplete record without knowing it.Shape 1 —
.jsonlhead-dropA session's
.jsonlwas missing its first 7 user-message entries while the paired.trajectory.jsonlrecorded all 14 turns (session.startedevents) correctly. The.jsonlretained session-startup events (model_change,thinking_level_change) but the early user messages themselves never landed. Nothing in either file marks the gap.Detection signature:
jsonl.first_user_message_ts > trajectory.first_session_started_ts. In our case the gap was +3313s (~55 min of conversation missing from the runtime transcript).From reading the dist source:
appendJsonlEntrySync(transcript-write-context) appends per-message with no retry; a failure between the trajectory recorder's event write and the jsonl flush leaves the trajectory withsession.started+prompt.submittedwhile the jsonl permanently lacks the user message. Side note: the message content survived in the context-engine DB (lossless-claw'smessagestable), which is how we confirmed the content was real — but the runtime transcript is what downstream consumers read.Shape 2 — trajectory FIFO window (expected, but undocumented as a pairing hazard)
trimJsonlWindowdrops oldest trajectory events once the file approachesTRAJECTORY_RUNTIME_CAPTURE_MAX_BYTES(10 MiB). A 3-week-old session had its trajectory at 99.4% of cap holding only the last ~25 turns, while its.jsonlheld the full history. This is by design — but combined with shape 1 it means neither file is a superset of the other, and there is no marker in either file saying "the other file has what I lost."Why it matters
openclaw backup createalready skips both files ([Backup] Preserve or explicitly support session transcript JSONL for chat history restore #86382); tooling that reads them directly is the only record, and that record is silently lossy in both directions..jsonlalone inherits shape-1 gaps invisibly.Repro
Synthetic fixtures for both shapes (head-drop and rotation-with-covered-head) are exercised by a standalone checker we wrote while diagnosing this: it classifies a session pair as no-loss / rotation-truncated / anomalous / indeterminate from timestamps, counts, and coverage spans. Happy to share the script or fold it into a PR if useful.
Suggested fixes (any one of these)
seqcursor on crash).message.idintoprompt.submitted.data.messageIdso downstream consumers can do record-level alignment and detect gaps (today the id schemes are disjoint: per-message uuid vs per-turnseq/sourceSeq).rotateTranscriptAfterCompactioncurrently handles only the jsonl).Version:
[email protected]· single-host LXC install · no client data in this report; timestamps and counts only.