Bound the first transcript read in bytes and make it cancellable

AU
3d ago
+1283-96

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: seq must 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 report hasMore, 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: AgentChatArtifactIndex copied the whole transcript into one String and 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:) uses JSONSerialization instead of probing each value's shape through Decoder, which is the recursive work the report's samples showed. Parity tests compare it against the Decodable path.
  • History service: if a session ends while its tailer is still reading, history retries 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).

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.