Summary
opencode web becomes completely unresponsive when the project working directory contains an untracked runtime-generated directory (e.g. a Docker bind-mount for a database). Two structural bugs in the Snapshot.diffFull → SessionSummary.summarize pipeline cause unbounded SQLite row growth.
Reproduction
- Start a Docker stack that bind-mounts a database data directory inside the repo working tree (e.g. PostgreSQL/QuestDB data — 4 300+ binary files, ~12 GB). Directory is not in
.gitignore.
- Run any opencode session with a few agent turns.
- Open
opencode web. UI hangs indefinitely.
Root Cause (v1.18.15)
SessionSummary.summarize()
→ SessionSummary.computeDiff({ messages })
→ Snapshot.diffFull(startHash, endHash)
→ git diff --numstat <h1> <h2>
→ git diff --name-status <h1> <h2>
→ git cat-file --batch (bulk content for every changed file)
→ unified diff generated per file
← returns [{ file, patch, additions, deletions, status }]
← stored into message.info.summary.diffs
→ Session.updateMessage() ← written to SQLite, no size guard
Bug 1 — track() commits ignored files into snapshot trees
track() runs git add --all without honouring .gitignore. Every file in the bind-mount directory is staged and committed into the snapshot bare repo, then appears in every subsequent diffFull call.
Bug 2 — Full diff text serialised into message.data with no size guard
SessionSummary.summarize writes the complete diffFull result (raw unified diff for every file) into message.info.summary.diffs and persists it to SQLite. No byte-length check exists before the write.
Single user-turn message: 4 325 file diff entries, ~400 MB. With parallel subagent sessions: opencode.db reached 66.8 GB in hours.
Bug 3 — Binary files produce { patch: "" } entries that are still serialised
diffFull correctly detects binary files via --numstat but still pushes { file, patch: "", additions: 0, deletions: 0 } into the result, which is meaningless and persisted.
Observed Impact
| Metric |
Observed |
opencode.db size |
66.8 GB |
Largest message row |
413 MB |
GET /session/:id/message payload |
1.2 GB |
opencode web CPU |
371% sustained (GC loops) |
| Web UI |
Completely unresponsive |
Proposed Fixes
Fix 1 — Honour .gitignore in track()
Replace unrestricted stage-all with one that respects ignore rules:
# instead of: git add --all
git add $(git ls-files --cached --others --exclude-standard)
Or libgit2 equivalent: exclude ignored paths when building the index for snapshot commits. This is the only fix that prevents the bloat from occurring.
Fix 2 — Store snapshot hashes in message.data; compute diffs on demand
{ "summary": { "startSnapshot": "<40-char hash>", "endSnapshot": "<40-char hash>" } }
Add: GET /session/:sessionID/message/:messageID/diff — computes the diff live from snapshot hashes. This is how Git itself works: commits store tree pointers, not serialised diff text. The two hashes are already stored on the step-start/step-finish part rows.
Fix 3 — Omit binary files from the diff result
- L.push({ file, patch: c.binary ? "" : Q0(...), additions, deletions, status });
+ if (!c.binary) L.push({ file, patch: Q0(...), additions, deletions, status });
Data Migration
A startup data_migration should trim bloated existing rows: find message rows where length(data) > 50 000 and role = 'user', strip summary.diffs entries with empty patches or from ignored paths, store only { startSnapshot, endSnapshot } hashes. Snapshot commits themselves remain intact in the bare repo under ~/.local/share/opencode/snapshot/.
Environment
- opencode v1.18.15, macOS arm64
- Trigger: Docker bind-mount of PostgreSQL + QuestDB data inside the repo working directory
Summary
opencode webbecomes completely unresponsive when the project working directory contains an untracked runtime-generated directory (e.g. a Docker bind-mount for a database). Two structural bugs in theSnapshot.diffFull→SessionSummary.summarizepipeline cause unbounded SQLite row growth.Reproduction
.gitignore.opencode web. UI hangs indefinitely.Root Cause (v1.18.15)
Bug 1 —
track()commits ignored files into snapshot treestrack()runsgit add --allwithout honouring.gitignore. Every file in the bind-mount directory is staged and committed into the snapshot bare repo, then appears in every subsequentdiffFullcall.Bug 2 — Full diff text serialised into
message.datawith no size guardSessionSummary.summarizewrites the completediffFullresult (raw unified diff for every file) intomessage.info.summary.diffsand persists it to SQLite. No byte-length check exists before the write.Single user-turn message: 4 325 file diff entries, ~400 MB. With parallel subagent sessions:
opencode.dbreached 66.8 GB in hours.Bug 3 — Binary files produce
{ patch: "" }entries that are still serialiseddiffFullcorrectly detects binary files via--numstatbut still pushes{ file, patch: "", additions: 0, deletions: 0 }into the result, which is meaningless and persisted.Observed Impact
opencode.dbsizemessagerowGET /session/:id/messagepayloadopencode webCPUProposed Fixes
Fix 1 — Honour
.gitignoreintrack()Replace unrestricted stage-all with one that respects ignore rules:
Or libgit2 equivalent: exclude ignored paths when building the index for snapshot commits. This is the only fix that prevents the bloat from occurring.
Fix 2 — Store snapshot hashes in
message.data; compute diffs on demand{ "summary": { "startSnapshot": "<40-char hash>", "endSnapshot": "<40-char hash>" } }Add:
GET /session/:sessionID/message/:messageID/diff— computes the diff live from snapshot hashes. This is how Git itself works: commits store tree pointers, not serialised diff text. The two hashes are already stored on thestep-start/step-finishpart rows.Fix 3 — Omit binary files from the diff result
Data Migration
A startup
data_migrationshould trim bloated existing rows: findmessagerows wherelength(data) > 50 000androle = 'user', stripsummary.diffsentries with empty patches or from ignored paths, store only{ startSnapshot, endSnapshot }hashes. Snapshot commits themselves remain intact in the bare repo under~/.local/share/opencode/snapshot/.Environment