Bound the first transcript read in bytes and make it cancellable
Opening a chat session on a very large transcript could pin cmux at high CPU and push its memory into many gigabytes. The reported case was a 6.8 GB Codex transcript whose last 2,000 lines held far more data than that limit suggests, and the read couldn't be stopped once it began. Fixes https://github.com/manaflow-ai/cmux/issues/31300.
Summary
The first read now decodes at most 32 MiB of a transcript's tail, one record at a time, off the tailer's actor. A record over 8 MiB isn't decoded: it keeps its seq and is published as an oversized_record unsupported message. stop() cancels the read during the newline count and between records.
- Tailer:
seqmust stay the absolute line index, so the first read still counts every newline once. The count uses a fixed 64 KiB buffer and a ring of offsets rather than mapping the file and storing every offset. Older lines past the window reporthasMore, the same way the line limit already did. - Appends: these now read in 1 MiB chunks under the same record cap, publishing every 32 MiB, so a long backlog or a huge live record can't sit in memory whole.
- Late lines: the tailer drains once when its watcher attaches, so a line written during the first read isn't left unseen until the next write.
- Rotation: the old cache keeps serving history until the replacement read finishes, then the state swaps in one step.
- Artifact index:
AgentChatArtifactIndexcopied the whole transcript into oneStringand decoded every line. It now uses the same reader, off its actor, limited to the last 256 MiB or 200,000 lines. A path referenced only before that window leaves the artifact scope, which fails closed. Concurrent requests for the same session, path and working directory share a single build, so a burst of thumbnail requests can't start a scan each; the last waiter to cancel cancels the build. - JSON:
TranscriptJSONValue(jsonLine:)usesJSONSerializationinstead of probing each value's shape throughDecoder, which is the recursive work the report's samples showed. Parity tests compare it against theDecodablepath. - History service: if a session ends while its tailer is still reading,
historyretries with a new tailer instead of returning an empty page.
Limits that remain: nothing in this repo renders the oversized_record message yet, so today a skipped record is simply absent from what people see, and older history reports hasMore. The first read is still linear in file size, though its memory no longer is. A record under 8 MiB still decodes in full. If an oversized record is a tool result, its tool row stays in its running state. The Vault transcript preview (SessionTranscriptLoader) is unchanged: it already streams with its own oversized-line handling and a turn limit, but view cancellation still doesn't reach it.
Testing
Focused command, run on the fleet: cmux-ci run --class light --script scripts/ci/run-swift-testing-suites.sh --ref <sha> --arg Packages/macOS/CmuxMobileHost (and --arg Packages/Shared/CmuxAgentChat for the parity tests).
- Failing tests on the base code: 304681ba36269afdf7aa330e4a5bfa18851cf7dd, fleet step 84e83887e5afb26fa384aa23, exit 1. The 2 MiB-per-line transcript loaded all 40 lines (
seqs.count <= 16failed,hasMorewas false), and the oversized record produced no row. - Passing on the head: 12a7f8e2873e8f28de536be2eede4ab7324fd2c3,
CmuxMobileHoststep 03988ed2cbc70705591fb609, exit 0, every suite in the package.CmuxAgentChatstep 7f00dd09af858f8db97f2b76 passed at 9e97cc33eac41cd84295b5060f07a594266c73ea; no later commit touches that package. - App build: fleet job 6ad71df09a55e0fe7882ff0e at 0c864dc14df7cbd2ae73a3fb0c45a45fcc643708, tag
31300-transcript-backfill-bounds, exit 0. It establishes that the app compiles, not how it behaves. Later commits change onlyCmuxMobileHost(scan budget,maxLinesand access levels), which the step above compiles and tests.
Observed / Not run
Observed: the fleet steps above. They cover the window and record limits, a 3 GiB sparse file, cancellation, trailing partial records, batched backlogs, rotation, shared artifact builds, a cancelled waiter, the last waiter cancelling a build, the trailing fragment charged to the byte budget, a zero line limit, and 38+ JSON parity cases against the Decodable path. scripts/verify-local.py passed its selected static checks.
Not run: the tagged build has not been launched, so there is no run against the original 6.8 GB transcript and no measurement of CPU or memory in the app. The AgentChatTranscriptService.history retry has no test and is only read. stopAbandonsInitialLoad can pass with stop() landing before the read starts, so cancelling an in-progress read is covered by the reader's own cancellation tests rather than end to end. No test covers the artifact index's trailing line or its 256 MiB window, or a line written between the first read and the watcher attaching.
CI: CI fast guards fails on test_every_owned_workflow_is_watched for the origin/feat-cmux-next tree. It fails the same way on main (https://github.com/manaflow-ai/cmux/actions/runs/41500671477) and is fixed by https://github.com/manaflow-ai/cmux/pull/32585 (with https://github.com/manaflow-ai/cmux/pull/32649 for feat-cmux-next). linux-preflight, macOS admission gate and macOS compile admission decline as a result.
Changelog
Fixed: Opening a chat session on a very large agent transcript no longer drives cmux to high CPU and many gigabytes of memory
Proof
No screenshots: the change is in transcript reading. A record over 8 MiB is skipped, with no rendered row today.
Checklist
- Behavior changes have added or updated tests, or Testing says why not
- UI, settings, menu, schema, help-text or user-facing docs change: localization audited (no new user-facing strings)
- New or changed v2 socket method allowlisted for
cmux ssh: not applicable - iOS connectivity, auth, lifecycle, workspace action, terminal I/O or mobile RPC contract change: not applicable beyond the existing fail-open unsupported row
🤖 Generated with Claude Code
Summary by CodeRabbit
-
Performance
- Transcript history and artifact indexing now read only a bounded portion of large transcripts, helping keep loading responsive.
- Concurrent artifact-index requests can share in-progress work, and completed results can be reused.
-
Reliability
- Oversized transcript records are shown as unsupported entries while preserving their sequence numbers.
- Transcript loading can be cancelled; if an initial load is interrupted, history requests retry.
- Boolean and numeric values in transcript data are now distinguished more consistently.
-
Bug Fixes
- Transcript updates after file replacement and completion of partial records are handled more reliably.