Skip to content

Session load parses summary.diffs patches from message rows; hundreds of MB of JSON per session, multi-GB heap spike on resume #50089

Closed as not planned

Description

@jrkaisersr

opencode 1.18.30, macOS (Apple Silicon, 36 GB). Bun runtime.

With snapshot tracking on, each user message stores summary.diffs[].patch: the full
unified diff of every working-tree change since the previous turn. In a large working
tree the patches get enormous. One session of 68 messages holds 390 MB of message JSON;
a single message is 330 MB (24,444 file diffs: other agents sharing the same working
directory were unpacking archives during that turn). Across my store, 48 messages hold
2.2 GB (33 sessions); the part table for the same sessions is ~7 MB.

Effect: resuming a session (opencode --session <id>) parses all of it. Measured with
vmmap: 53 MB at start, 5.7 GB resident at 61 s, peak footprint 5.0 GB, settling to ~0.7 GB
after GC while idle. Sending one prompt in that session took the process to 8.2 GB.
With 13 restored sessions resuming together the machine jetsammed (44 GB across opencode
processes).

Related: the event table grows without bound (30.6 GB, 83k rows; message.updated and
message.part.updated copy the full message/part JSON per update), and a 6 h session
wrote 550 GB via WAL checkpoints.

Workarounds that hold: "snapshot": false in config stops new patches; deleting
summary.diffs from the oversized rows (json_remove) and VACUUM took the store from
2.4 GB to 220 MB with the session still exporting intact; the same session then peaks
at 750 MB instead of 5.0 GB.

Suggestions: cap or truncate stored patches (or store diffs out of the message row and
load them lazily), and bound the event table (compact or drop replayed events).

Code paths (v1.18.30 tag; same at HEAD ebb7b76)

  • packages/opencode/src/session/summary.ts summarize(): loads every message of the
    session (sessions.messages, line 116) on each turn, computes snapshot.diffFull(from, to)
    (line 98) and writes the result into the user message (target.info.summary = {..., diffs},
    line 125) via sessions.updateMessage. Nothing bounds the number of files or the total size.
  • packages/opencode/src/snapshot/index.ts diffFull: git show of the before and after
    content for every added/deleted/modified file between the two snapshot hashes. The only
    guard is the per-file limit = 2 * 1024 * 1024 (line 24) at tracking time, so a turn that
    touches thousands of files stores thousands of full-content patches.
  • packages/opencode/src/session/session.ts updateMessage (line 631): publishes
    SessionV1.Event.MessageUpdated with the whole info object, diffs included, so the event
    store persists a full copy of the payload on every update.
  • packages/opencode/src/cli/cmd/export.ts (lines 173, 207) is the only reader of
    summary.diffs besides the UI "changes" chip; the model request never uses it, which is
    why lazy-loading it out of the message row is safe.

Activity

  1. argszero commented on Sep 20, 2026

    @argszero
    Contributor

    I'd like to work on this.

    I confirmed the shape of the problem against dev: session/summary.ts:124-126 stores computeDiff() output into the user message via updateMessage, and each entry comes from snapshot/index.ts:736-737, which builds the patch with structuredPatch(..., { context: Number.MAX_SAFE_INTEGER }) — the patch text is effectively the whole file for every changed file. The only bound in that path is the per-file limit = 2 * 1024 * 1024 applied at tracking time; nothing bounds the number of files or the total bytes written into one message row.

    Plan (smaller of your two suggestions, so the stored record stays useful):

    1. keep the { file, status, additions, deletions } entries complete so the changes list, opencode export, and the diff chip stay correct, but stop attaching patch text for a message once a total byte budget for that message is exhausted — the same shape binary files already use (patch: "");
    2. bound the per-file patch with the existing 2 MiB budget as well, so one huge file cannot consume the whole allowance;
    3. test it with a snapshot that touches many files, asserting the stored summary.diffs stays under the budget while the file list remains complete.

    If you would rather have the patches moved out of the message row and loaded lazily, tell me and I'll implement that instead — the budget cap is meant to be the reviewable first step, not a redesign of where diffs live.

  2. argszero commented on Sep 21, 2026

    @argszero
    Contributor

    Opened #50360 for this.

    It takes the first suggestion from the report (cap the stored patches, keep where diffs live unchanged): Snapshot.diffFull now bounds the patch text one turn keeps with the same 2 MiB limit that already decides what is staged at track time, dropping a patch longer than the limit and stopping patch text once the accumulated size reaches it — the same empty-patch shape binary entries already use. Each entry keeps file/status/additions/deletions, so the changes list, the counts and opencode export stay complete, and the content of files past the budget is never read.

    This covers the message row and the revert path, which share diffFull. The event-table side of the report (unbounded message.updated / message.part.updated payloads) is untouched — I did not want to mix two storage changes in one PR.

  3. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions