You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Session load parses summary.diffs patches from message rows; hundreds of MB of JSON per session, multi-GB heap spike on resume #50089
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).
packages/opencode/src/session/summary.tssummarize(): 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.tsdiffFull: 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.tsupdateMessage (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.
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):
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: "");
bound the per-file patch with the existing 2 MiB budget as well, so one huge file cannot consume the whole allowance;
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.
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.
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.
Activity
argszero commentedon Sep 20, 2026
I'd like to work on this.
I confirmed the shape of the problem against
dev:session/summary.ts:124-126storescomputeDiff()output into the user message viaupdateMessage, and each entry comes fromsnapshot/index.ts:736-737, which builds the patch withstructuredPatch(..., { 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-filelimit = 2 * 1024 * 1024applied 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):
{ 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 MiBbudget as well, so one huge file cannot consume the whole allowance;summary.diffsstays 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.
argszero commentedon Sep 21, 2026
Opened #50360 for this.
It takes the first suggestion from the report (cap the stored patches, keep where diffs live unchanged):
Snapshot.diffFullnow bounds the patch text one turn keeps with the same2 MiBlimit 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 keepsfile/status/additions/deletions, so the changes list, the counts andopencode exportstay 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 (unboundedmessage.updated/message.part.updatedpayloads) is untouched — I did not want to mix two storage changes in one PR.thdxr commentedon Oct 11, 2026
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 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊