fix(sessions): stream JSONL transcript scans instead of buffering whole files#80615
Conversation
cd2ad07 to
42662bc
Compare
|
Codex review: needs real behavior proof before merge. Summary Reproducibility: yes. at source level. Current main still reaches fs.promises.readFile or fileHandle.readFile on the affected transcript scan paths before splitting and scanning JSONL content. Real behavior proof Next step before merge Security Review detailsBest possible solution: Land the shared streaming helper approach after updated real behavior proof for the current head and normal maintainer/CI handling. Do we have a high-confidence way to reproduce the issue? Yes, at source level. Current main still reaches fs.promises.readFile or fileHandle.readFile on the affected transcript scan paths before splitting and scanning JSONL content. Is this the best way to solve the issue? Yes for the code direction. Forward streaming plus a chunked newest-first reverse scanner is the narrow maintainable fix, but merge should wait for current-head real behavior proof of the reverse scan behavior. What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against c53d7aee0f90. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
…le files (openclaw#54296) The remaining whole-file transcript scans flagged by ClawSweeper triage on splitting on newlines. On long-running sessions where transcripts grow into the multi-MB / 100s-of-MB range that scales peak RSS with file size and is the practical OOM risk in the report. Add a shared streaming helper module `src/config/sessions/transcript-stream.ts` exposing: - `streamSessionTranscriptLines(filePath, { signal? })`: forward async-iterator over trimmed non-empty lines using `fs.createReadStream` + `readline` with `crlfDelay: Infinity`. Bounded to one line of memory at a time and honours an abort signal between lines. - `readSessionTranscriptTailLines(filePath, { maxBytes? })`: tail-only read of the last `maxBytes` of a file (default 4 MiB, clamped to [1 KiB, 64 MiB]), returning trimmed non-empty lines in reverse order. Drops the leading line of the slice when the window does not start at byte zero so callers never see a partial-line suffix. Migrate every flagged whole-file scan to these helpers while preserving the malformed-line tolerance and idempotency-key return semantics callers depend on (see `Remaining risk / open question` on the issue): - `src/config/sessions/transcript.ts`: `readLatestAssistantTextFromSessionTranscript`, `readTailAssistantTextFromSessionTranscript`, and the delivery-mirror dedupe helper `findLatestEquivalentAssistantMessageId` now use the tail helper; `transcriptHasIdempotencyKey` uses the forward stream helper. - `src/gateway/server-methods/chat.ts`: the inline `transcriptHasIdempotencyKey` used by chat-method append idempotency now uses the forward stream helper and tolerates malformed lines mid-scan (matching the sibling helper in `config/sessions/transcript.ts`). - `src/gateway/session-compaction-checkpoints.ts`: `readTranscriptEntriesForForkAsync` builds the fork entry array from the forward stream helper instead of one big `fileHandle.readFile("utf-8")` call. Fixes openclaw#54296.
552470c to
a3f4379
Compare
|
Landed via rebase onto main.
Thanks @jack-stormentswe! |
Fixes #54296.
The remaining whole-file transcript scans called out by ClawSweeper triage on #54296 still materialized the entire session JSONL file in memory before splitting on newlines. On long-running sessions where transcripts grow into the multi-MB / 100s-of-MB range, that scales peak RSS with file size and is the practical OOM risk in the report.
This PR adds shared streaming helpers in
src/config/sessions/transcript-stream.tsand migrates every flagged whole-file scan to them. Forward scans usefs.createReadStream+readline(one line of memory at a time). Reverse scans now walk the transcript newest-first in bounded chunks from the file end, decoding complete JSONL lines without a fixed tail cap, so/tts latestand delivery-mirror dedupe keep exact latest-assistant semantics even when a large user/tool entry follows the assistant line.Migrated scans:
src/config/sessions/transcript.ts-- latest/tail assistant reads, delivery-mirror equivalent-message lookup, and idempotency-key lookup.src/gateway/server-methods/chat.ts-- chat-method append idempotency lookup, now also tolerant of malformed lines mid-scan.src/gateway/session-compaction-checkpoints.ts-- compaction fork entry loading from the forward stream helper instead of one bigfileHandle.readFile("utf-8").Preserved invariants from ClawSweeper's remaining-risk note: parentId DAG, malformed-line tolerance, idempotency-key return semantics, newest-assistant lookup semantics, and delivery-mirror dedupe behavior.
Verification
pnpm test src/config/sessions/transcript-stream.test.ts src/config/sessions/transcript.test.ts-- 29/29 pass.node scripts/run-tsgo.mjs -p tsconfig.core.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core.tsbuildinfo-- pass.pnpm check:changedon Testboxtbx_01krbb7zw876zaqv9hfvg90k01-- core and core-test typechecks pass; still fails in pre-existing unrelatedextensions/qqbot/src/engine/tools/remind-logic.test.ts(238,23)ondeleteAfterRun.git diff --check-- clean.Real behavior proof
Behavior addressed: Peak resident memory growing with transcript size during idempotency lookups, latest-assistant scans, delivery-mirror dedupe, and compaction fork loading on long-running sessions. The fix must keep latest-assistant and dedupe behavior exact even when a large user/tool entry follows the assistant entry.
Real environment tested: Blacksmith Testbox via Crabbox on PR head
552470c29a4d29afe1015222420638b657444802, leasetbx_01krbcw4swx29ecnqnpewj5j50, slugamber-barnacle, Node 24 Testbox workflow environment. The command created a real OpenClaw session store and JSONL transcript on disk, used production session append/read helpers, appended an 8 MiB trailing user entry, verified latest-assistant reverse scan and delivery-mirror dedupe, verified idempotency forward scan, and forked a compaction checkpoint transcript.Exact steps or command run after this patch:
pnpm crabbox:run -- --provider blacksmith-testbox --blacksmith-org openclaw --blacksmith-workflow .github/workflows/ci-check-testbox.yml --blacksmith-job check --blacksmith-ref main --idle-timeout 90m --ttl 240m --timing-json --shell -- <script that runs pnpm exec tsx .crabbox/pr80615-transcript-e2e.mts>. The script importssrc/config/sessions/transcript.ts,src/config/sessions/transcript-append.ts, andsrc/gateway/session-compaction-checkpoints.tsfrom the synced PR checkout and exercises real filesystem state under/tmp.Evidence after fix: Copied Crabbox terminal output from the passing Testbox run:
Observed result after fix: The real transcript file was 8,389,986 bytes with the newest entry being a large user message.
readLatestAssistantTextFromSessionTranscriptstill found the earlier assistant id.appendAssistantMessageToSessionTranscriptreturned the same assistant id for delivery-mirror dedupe instead of appending a duplicate. Repeating an append with the same idempotency key returned the same idempotent message id.forkCompactionCheckpointTranscriptAsyncstreamed and forked the transcript with matching line count.What was not tested: No live model/provider turn or channel delivery was exercised; this bug is in local session transcript scanning and compaction file handling, so the E2E uses real OpenClaw session files and production session helpers without provider credentials.