Skip to content

Unbounded growth of the event table: opencode.db reaches 13GB+, mostly message.updated.1 snapshots (no retention/compaction) #33356

Open

Description

@rustyaos

Summary

The local SQLite store (~/.local/share/opencode/opencode.db) grows unbounded on long-lived instances because the event-sourcing event table is never pruned, capped, or compacted. On two of our long-running instances it reached ~13 GB each, filling a 22 GB volume to 97–99% and eventually producing sqlite3.OperationalError: database or disk is full on ordinary queries — the instance becomes effectively unusable.

The actual conversation data is tiny by comparison; >90% of the DB is the event log, and within it a single event type (message.updated.1) dominates.

Measured breakdown (real instance, 13.7 GB DB)

page_size=4096  page_count=3342166  total=13.69 GB
freelist_count=18  dead=0.00 GB (0.0%)        <- live data, VACUUM does not help

Per-table (sum(length(data))):

table rows size avg/row
event 141,951 12.20 GB 83.9 KB
message 6,955 1.15 GB 161 KB
part 27,775 0.07 GB 2.4 KB
others — ~0 —

Within event, by type:

event type rows size avg
message.updated.1 26,463 11.94 GB 440 KB
session.next.tool.success.1 7,843 0.12 GB 14.8 KB
message.part.updated.1 52,644 0.10 GB 1.8 KB
everything else ~55k < 0.3 GB —

So ~87% of the entire database is message.updated.1 events. With ~6,955 messages there are ~3.8 message.updated events per message, each persisting what looks like a full message snapshot (avg 440 KB), and all historical snapshots are retained forever.

The same pattern was observed independently on a second instance (also ~13 GB, same event-table dominance), so this is reproducible, not a one-off.

Root cause (confirmed in source, dev branch)

packages/core/src/event/sql.ts — the event table has no TTL, no row cap, no size limit, and no compaction column; it just stores every event's full data JSON:

export const EventTable = sqliteTable("event", {
  id: text().$type<EventV2.ID>().primaryKey(),
  aggregate_id: text().notNull().references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }),
  seq: integer().notNull(),
  type: text().notNull(),
  data: text({ mode: "json" }).$type<Record<string, unknown>>().notNull(),
}, ...)

packages/core/src/event.ts — the only delete against event is a full-aggregate cascade, used when an entire session/aggregate is removed:

function remove(aggregateID: string) {
  return db.transaction(() => Effect.gen(function* () {
    yield* db.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).run()
    yield* db.delete(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).run()
  })).pipe(Effect.orDie)
}

There is no partial pruning — nothing collapses superseded message.updated.* snapshots, keeps only the latest per message, caps per-aggregate history, or stores deltas. packages/opencode/src/sync/README.md confirms this is an intentional append-only log "for session replayability." That's a reasonable design goal, but with full-snapshot message.updated events and no retention it means every session's event history accumulates for the lifetime of the session, with no upper bound.

SessionCompaction.prune does not address this — it trims context tokens sent to the model, not persisted event rows.

Impact

  • Disk exhaustion on long-lived instances (containers/sandboxes/servers with persistent home dirs).
  • Once the volume is full, even read queries that need temp space fail with database or disk is full.
  • VACUUM cannot reclaim it (freelist ≈ 0%; it is all live data). The only current remediation is deleting whole sessions (cascade) or manually DELETE FROM event ....

Suggested fixes (any one would help)

  1. Compact superseded snapshots: for snapshot-style events like message.updated.*, retain only the latest event per (aggregate_id, message_id) (or per message seq), deleting older ones — newer fully supersedes older.
  2. Store deltas instead of full snapshots for message.updated (the README already notes session.updated was moved to fields-only; message.updated appears to still carry a full snapshot).
  3. Per-aggregate retention cap / periodic compaction of the event log for inactive/completed sessions.

Environment

  • Observed on builds in the 1.15.x–1.17.x range; root cause confirmed present on dev (current latest, 1.17.9) via the source above.
  • SQLite store at ~/.local/share/opencode/opencode.db.

Happy to provide the full per-session breakdown or a repro script if useful.

Activity

  1. github-actions commented on Jun 22, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your issue is primarily about the message.updated.1 snapshot accumulation, #32005 is the closest match and combining discussion there may be useful.

  2. joaomj commented on Jul 6, 2026

    @joaomj

    Same issue here. After a few months of regular use, ~/.local/share/opencode was consuming over 50 GB split roughly 45 GB in tool output artifacts and 5.7 GB in the SQLite database (opencode.db). There is no built-in mechanism to cap or prune either, and neither compaction.prune nor tool_output.max_lines/bytes addresses persisted storage, only in-context token limits.

    As noted in the original issue, the event table grows unbounded with full-snapshot message.updated events that are never compacted. Combined with tool outputs saved to disk for every turn, this makes long-term usage on storage-constrained machines (laptops, containers, VPS) untenable without manual intervention or third-party tools like ocgc.

    A configurable retention policy (max DB age, max size cap, or auto-compaction of superseded events) would be extremely valuable.

  3. pspranger-throw commented on Jul 11, 2026

    @pspranger-throw

    Adding to that:

    Reproduction (one command → ~2.3 GB of events)

    A single bash tool call downloading a model file:
    wget -q --show-progress "https://huggingface.co/<...>/model.Q4_K_M.gguf" -O model.gguf 2>&1

    • Downloaded file: 1.8 GB
    • --show-progress redraws the progress bar in place via \r many times per second
    • OpenCode captures the stream and emits a message.part.updated.1 event containing a full snapshot of the accumulated output on every redraw

    Result for that one part: 79,083 events, 2,325 MB — 97.7% of the session's event cost and ~75% of an 3.1 GB opencode.db. The part's actual final content was 81 KB, so the overhead here was ~29,000×.

    Environment: OpenCode 1.17.18, Linux terminal, SQLite WAL mode, auto_vacuum=0.

    Where the bytes actually are

    For the affected session, the event table was 84% of the DB. Within it:

    event type rows total size avg
    message.part.updated.1 178,062 2,470 MB 14.5 KB
    message.updated.1 36,939 16 MB 456 B
    session.updated.1 6,874 3.8 MB 573 B

    So in our case the dominant type was message.part.updated.1 (part-level streaming snapshots), ~154× more costly by bytes than message.updated.1. One single part held 2,325 MB of the 2,380 MB total — the bloat is extremely concentrated, not spread across many sessions.

    The waste is byte-identical consecutive snapshots

    Growth of that one part across its 79,083 updates:
    update # part size
    1st 322 B
    1,000th 31,017 B
    10,000th 31,017 B
    40,000th 31,017 B
    79,083rd (last) 83,352 B

    From update ~1,000 to 40,000 the part content was byte-for-byte identical (the progress bar at full size, unchanged).

    OpenCode re-snapshotted the same 31 KB 39,000 times — ~1.2 GB of pure duplicates that carry zero information.

    Any command that emits thousands of output lines triggers this: wget --show-progress, tail -f, verbose build logs, yes/loops, etc.

  4. EgilSandfeld commented on Jul 12, 2026

    @EgilSandfeld

    Confirmed on Windows x64 with OpenCode v1.17.18.

    opencode.db reached 10.69 GB:

    • event table: 8.13 GB allocated / 7.15 GiB JSON
    • message.part.updated.1: 280,318 rows / 4.55 GiB
    • message.updated.1: 62,397 rows / 2.60 GiB
    • Largest active session: 38,592 events / 2.13 GiB
    • Largest individual event JSON: 248.7 MiB

    Tool breakdown exposed the write amplification clearly:

    • bash: 125,685 snapshots / 3.67 GiB
    • A single bash tool part: 2,001 snapshots / 189.6 MiB
    • A completed apply_patch event: 248.7 MiB

    PRAGMA freelist_count is 0, so this is live append-only data—not reclaimable SQLite/VACUUM bloat.

    Active sessions already account for 2.96 GiB and sessions archived less than one day for another 2.67 GiB. Session retention therefore cannot prevent this; intermediate tool/message updates need deltas or terminal checkpoints instead of repeated full durable snapshots.

  5. chubes4 commented on Jul 13, 2026

    @chubes4
    Contributor

    Revised safety model in #36710: compaction is explicit and dry-run by default, limited to 1,000 rows per transaction, and validates the current message/part projection against the retained latest snapshot. The sync protocol has no checkpoint-marker version negotiation, so apply is allowed only for aggregates with no workspace or sync owner; synced/owned aggregates remain report-only pending a protocol compatibility decision.

    The supplied production measurement of message.part.updated.1 is 2,614,320 rows / 17.73 GB (within a 20.07 GB event table), which is the primary target for bounded remediation.

  6. kitlangton commented on Jul 19, 2026

    @kitlangton
    Contributor

    The same unbounded-retention shape is present in the current V2 next-15834 channel database, although V2's dominant event types differ from the V1 snapshot data in this issue. Read-only aggregate measurements show 2,125 Sessions, 1,302,717 event rows, 3.04 GiB of event JSON, and a 6.09 GiB SQLite file. session.tool.success.1 alone occupies 1,833.6 MiB across 168,146 rows, followed by session.tool.progress.1 at 483.8 MiB and session.reasoning.ended.1 at 245.0 MiB. The largest stored event is 27,266,659 bytes.

    This volume was measured without reading or publishing payload contents, Session IDs, titles, or paths. It is not the direct cause of the continuous multi-TUI CPU incident tracked in #36441, which is driven by live global fan-out, but it independently increases disk use and the cost of cold database reads. V2 retention/compaction should therefore cover large tool/progress events and attachments, not only superseded message.updated snapshots.

  7. self-assigned this
    on Jul 19, 2026
  8. ZhiLin-Sam commented on Jul 22, 2026

    @ZhiLin-Sam

    Lol same 🙃 Script to keep it under control while we wait: https://gist.github.com/ZhiLin-Sam/552a6454704e31be0164cf9c6cbe70fd

  9. rafaelbn commented on Jul 23, 2026

    @rafaelbn

    I can confirm the same unbounded event-log growth on OpenCode Desktop/CLI 1.18.4 for Windows.

    Database diagnostics:

    • opencode.db: approximately 2.49 GB
    • PRAGMA page_size: 4096
    • PRAGMA page_count: 608158
    • PRAGMA freelist_count: 0
    • PRAGMA auto_vacuum: 0
    • Journal mode: WAL
    • PRAGMA quick_check: ok

    Event table:

    • 93,514 rows
    • 2,216,598,425 JSON bytes

    Breakdown:

    • message.updated.1: 19,588 rows, 2,030,847,015 bytes, maximum row 19,157,539 bytes
    • message.part.updated.1: 67,575 rows, 181,888,345 bytes
    • session.updated.1: 5,959 rows, 3,709,489 bytes

    A single session aggregate contains 28,363 events and 1,891,336,715 bytes, approximately 76% of the complete database. session.revert and session.summary_diffs are empty, so the dominant cause here is specifically the retained message.updated.1 snapshots.

    Because freelist_count is zero, VACUUM alone cannot help. The database is healthy but almost entirely occupied by live event history. This is also creating a significant performance and memory risk for Desktop.

    I can run additional anonymized SQL diagnostics if useful. Official safe compaction or migration guidance for existing databases would be especially helpful.

  10. chubes4 commented on Jul 23, 2026

    @chubes4
    Contributor

    Mine got over 50gb and regrows pretty fast when I clean it up. I think this should be prioritized. As we get into deeper orchestration we need to handle storage carefully. Disk space fills up quick. 1TB does not go very far!

  11. joaomj commented on Jul 23, 2026

    @joaomj

    Mine got over 50gb and regrows pretty fast when I clean it up. I think this should be prioritized. As we get into deeper orchestration we need to handle storage carefully. Disk space fills up quick. 1TB does not go very far!

    does this happen even if you set "snapshot":false in opencode.json?

  12. rayborg commented on Jul 25, 2026

    @rayborg

    I have a similar project-switch/bootstrap failure with a long-lived but structurally healthy database:

    • Database size: approximately 31.1 GiB
    • Events: 1.83 million
    • Event JSON: approximately 19.8 GiB
    • message.part.updated.1 and message.updated.1 snapshots: approximately 99.7% of event JSON
    • Largest aggregate: approximately 3.19 GiB of event JSON
    • PRAGMA quick_check: ok; freelist pages: 0

    I captured repeated sidecar V8 OOMs on Desktop 1.18.3, 1.18.4, and 1.18.5. The visible trigger is switching projects or bootstrapping the selected project: the UI hangs, V8 reports allocation-failure scavenges followed by Zone Allocation failed - process out of memory, the sidecar exits with code 5, and renderer requests then fail with TypeError: Failed to fetch.

    This looks consistent with the unbounded event-snapshot growth reported here and with the OOM behavior in #38362. PR #36710 also appears relevant as proposed bounded compaction work, although it is not currently a supported recovery path for this database.

    All diagnostics were read-only and aggregate-only. I did not inspect or include local paths, project/session identifiers, titles, or payload content. No event deletion, VACUUM, third-party cleanup, or other unsupported cleanup was applied, so the database remains available as evidence.

    Could maintainers provide supported recovery guidance for an existing database of this size, ideally including a safe dry run and handling for synced/owned aggregates? Guidance on what diagnostic would best confirm the project-switch/bootstrap loading path would also help.

  13. 23 remaining items

  14. arul28 commented on Sep 26, 2026

    @arul28

    Independent reproduction with the same shape, plus the exact payload that makes it unbounded — from ADE's use of the pinned v1.18.32 server.

    Store: ~/.local/share/opencode/opencode.db = 12.5 GB, grew ~160 MB/hour in active use. freelist_count=0, so this is live data, not free pages.

    dbstat attribution:

    table bytes share
    event 12,311,945,216 (11.47 GiB) 98%
    message 200,884,224 1.6%
    part 43,364,352 0.3%

    By event type (SUM(LENGTH(data))):

    event type rows bytes max row
    message.updated.1 12,939 12,209,167,117 79,355,461
    message.part.updated.1 33,917 62,077,425 —
    session.updated.1 3,417 2,336,731 —

    Earlier sample from the same machine: 621 event rows over 1 MB, together 3.68 GB, largest row 88.76 MB.

    Payload. Every message.updated re-publishes the entire message, and for an assistant message that includes the complete file-diff summary:

    {"sessionID":"ses_...","info":{"id":"msg_...","role":"user","summary":{"diffs":[{"file":"...","patch":"<entire git patch>"}]}}}

    So one patch is copied into every subsequent snapshot of that message, with no retention, cap, or offload. Only 55 sessions / 5 projects produced 12.5 GB.

    Why a workspace hits this: a full-auto chat that touches a few large files accumulates multi-hundred-KB patches per message; a long-running loop (tool → message.updated → tool → …) multiplies that per turn. packages/core/src/session/message-updater.ts and packages/core/src/event/sql.ts are the same on dev, so this is not fixed by a version bump.

    What we're asking for: retention/compaction for the event log and/or moving summary.diffs[].patch out of message.updated payloads (reference it, or bound it). ADE now isolates its own OpenCode data home and ships an opt-in whole-session prune, but that is a workaround for every consumer of this store, not a fix.

  15. iceteaSA commented on Sep 26, 2026

    @iceteaSA

    @arul28 #42771 does the second thing you ask for. summary.diffs[].patch moves to a message_diff table, message.updated keeps only each file's name, status and line counts, and clients fetch a patch when the diff is expanded. On a 51 GB database, diff-carrying message.updated events dropped from about 433 KB to 4.4 KB on average (largest 60 MB → 2.3 MB), and it has been running on that machine since Aug 15.

    @peterradzisz it also adds an opt-in retention.event_idle_days. Sessions idle longer than that lose their event history through the same EventV2.remove that deleting a session uses, which removes the session's events and its sequence row together, so it doesn't leave the sequence holes described above. It does nothing for sessions that stay active. With the patches gone, their events are small (about 1.2 KB per row on that machine now), but the table still grows with use.

  16. chubes4 commented on Sep 26, 2026

    @chubes4
    Contributor

    Can we please do something about this already? Maintaining the OpenCode database is such a massive pain in the ass! I cannot believe nobody from their team has gotten fed up with this unbounded growth yet. Surely they are all using this product heavily???

  17. kidvanguard commented on Sep 29, 2026

    @kidvanguard

    One more data point, plus a policy that actually bounds the table — which is different from what #43456 landed on.

    Store: opencode.db = 5.7 GB on 1.18.25. freelist_count = 0 and auto_vacuum = 0, so it is live rows; VACUUM alone reclaims nothing (agrees with the measurements above).

    Redundancy, not just size. The log stores a full snapshot per streamed delta, so the same object is rewritten repeatedly:

    event type rows bytes distinct targets churn
    message.part.updated.1 981,423 2.07 GB 393,198 parts ($.part.id) 2.5x
    message.updated.1 354,787 1.61 GB 90,805 messages ($.info.id) 3.9x

    event.aggregate_id is the session id for 100% of rows (1,434,078/1,434,078), so the log ages with its session — and 59% of the 5.7 GB file (82% of the event table) is events for sessions idle more than a day (2.93 GB for idle >7d, 0.41 GB for 1-7d, 0.32 GB for <1d).

    What bounds it. Not retiring sessions — that is what #43456 concluded too ("orphan-only pruning reclaims almost nothing... 85 GB of events remained for live sessions"). Deleting the events of sessions that are merely idle does bound it, and the sessions stay readable because the durable state is in message/part:

    DELETE FROM event
    WHERE aggregate_id IN (
      SELECT id FROM session
      WHERE time_updated < :cutoff AND time_updated IS NOT NULL AND id <> :newest
    );

    On the 5.7 GB database that removed 1,374,504 rows / 3.35 GB. After it, the table holds about one day of use (~0.3 GB/day) instead of growing without limit. session / message / part / todo row counts were identical before and after, and PRAGMA integrity_check was ok; wal_checkpoint(TRUNCATE) + VACUUM then took the file from 5.8 GB to 1.8 GB.

    So a --max-age-hours-style event policy (drop events for idle sessions, always exempt the newest) would be the property worth having in db prune. Note that #43456 is closed unmerged, and I could not find a merged PR that bounds this table, so it still looks open on 1.18.33.

  18. AndyS77 commented on Sep 29, 2026

    @AndyS77

    Your measurements confirm exactly what's needed — deleting events for idle sessions (not the sessions themselves) is what bounds the table, and the durable state in message/part keeps everything readable.

    I've updated #43456 to implement precisely the policy you described. pruneIdleEvents() does the same DELETE FROM event WHERE aggregate_id IN (SELECT id FROM session WHERE time_updated < :cutoff AND time_updated IS NOT NULL AND id <> :newest) — always exempting the newest session.

    What changed since the version you saw:

    • --max-age is in hours (e.g. --max-age 168 for 7 days), matching the --max-age-hours-style policy you proposed.
    • --purge-sessions flag for the aggressive mode (also deletes sessions, not just events).
    • VACUUM uses VACUUM INTO + file swap instead of plain VACUUM, to avoid WAL blow-up on large databases. On your 5.7 GB database, plain VACUUM worked because auto_vacuum=0 and you checkpointed first, but VACUUM INTO is safer in general — it writes a compacted copy to a separate file, then swaps it in after closing the connection, so the WAL never grows by the full database size.

    Usage:

    opencode db prune --max-age 168                    # delete events for idle sessions (safe, 7 days)
    opencode db prune --max-age 168 --purge-sessions   # also delete sessions themselves
    opencode db prune                                    # orphan-only (no session reference)
    

    #43456 was closed by automated PR cleanup (>1 month, <2 reactions). I've requested reopen — the PR now implements exactly the event-pruning policy your data validates. If a maintainer reopens it, this is ready for review.

  19. lfgranja commented on Oct 3, 2026

    @lfgranja

    Adding measured evidence from a third instance that reproduced this, plus a validated remediation path. Apologies for the length — the detail is what makes it actionable.

    Measured instance

    metric value
    opencode.db before 9.0 GB (page_count=2345774, freelist_count=0)
    event payload 7 950 MiB — 91% of the DB
    message.updated.1 138 466 rows / 7 103 MiB = 89% of event
    growth rate ~1.2 GiB/day over 7 days of normal interactive use
    messages 36 308 → ≈3.9 snapshots per message
    version 1.18.34, Linux x86_64, local single-device (no multi-device sync)

    Confirmed on 1.18.34, so the "confirmed in source on dev" analysis in the original report still holds for a released build.

    New finding: the payload is dominated by role=user, and by git diffs

    This is specific to my instance and I don't see it called out above. Breaking message.updated.1 down by info.role:

    role       events   payload
    assistant  12520      6 MiB
    user        4496    399 MiB     <-- 98% of the payload
    

    User messages are ~66× larger than assistant messages because User.summary.diffs[] (FileDiff.Info[], see packages/schema/src/v1/session.ts) embeds full unified git diffs inside every snapshot. The largest two rows in my database were 27 185 810 bytes each — byte-identical content in two different sessions (same msg_…, same .gitignore patch), differing only in sessionID. A single msg_0fa55a79… was stored 196 times (38 MB).

    Mechanism: any git add that touches .gitignore (or similar) inflates a user message, and the streaming loop re-emits a complete snapshot of it per token. So the multiplier is diffs size × token count.

    Why VACUUM cannot help (confirms the original report)

    pragma freelist_count;  -> 0
    pragma page_count;      -> 2345774   (× 4096 = 8.89 GiB)
    

    All live data. VACUUM rewrites the file at the same size.

    A remediation that worked (7.1 GB reclaimed, data intact)

    Since the projections are what the UI reads, pruning superseded message.updated.1 snapshots is safe. Three properties from packages/core/src/event.ts and packages/core/src/session/projector.ts make it so:

    1. Projections are committed atomically with the event. commitDurableEvent() runs the projectors and the insert(EventTable) in the same db.transaction(..., { behavior: "immediate" }). There is no window where message/part lag event.
    2. latestSequence() reads event_sequence, not event. Verified: for every aggregate, event_sequence.seq == max(seq in event) + 1. Preserving event_sequence means the next event gets seq = 10403, no collision on the unique event_aggregate_seq_idx.
    3. MessageUpdated projects via upsert — insert(MessageTable).onConflictDoUpdate({ target: MessageTable.id, set: { data } }). Only the latest snapshot of each message survives in message, so the other ~3.9 per message are redundant by construction.

    Result:

    page_count : 2345774 -> 489703
    freelist   : 1828607 -> 0
    file size  : 9.0 GB  -> 1.9 GB
    event      : 7950 MiB -> 870 MiB
    free /home : 29 GB   -> 41 GB
    
    pragma integrity_check;   -> ok
    pragma foreign_key_check; -> (clean)
    orphaned aggregates (event without event_sequence) -> 0
    message/part/event_sequence row+byte counts -> unchanged
    

    Post-prune spot-check: old sessions open and render normally in the TUI.

    Caveats, stated plainly:

    • This breaks durable replay for pre-prune sessions on a multi-device setup (replay() dies on sequence divergence). Irrelevant for single-device, which is why I checked before doing it.
    • It does not stop the growth. I re-measured ~10 minutes after the prune and message.updated.1 had already reappeared with rowid above the prune point. This is a mitigation, not a fix.
    • message.part.updated.1 (843 MiB here, the largest remaining block) I deliberately did not prune — parts are append-only and an older snapshot can be the final state, so removing it needs the projection verification that fix(core): bound event log compaction #36710 was attempting.
    • Batching by rowid rather than LIMIT, since the index is (aggregate_id, type, seq) and filtering on type alone is a full scan.

    Two operational gotchas worth adding to any maintenance path

    VACUUM uses $SQLITE_TMPDIR (default /tmp), not the DB's directory. On my box /tmp is a 2 GB tmpfs with ~950 MB free. VACUUM failed with database or disk is full after the backup and prune had already completed, even with 29 GB free on /home. It needs space for the live bytes, not the file size ((page_count - freelist_count) * page_size ≈ 2 GB in my case, vs. 8.9 GB if you dimension by file size). Preflight should check the effective SQLITE_TMPDIR and abort before touching anything.

    Cascading deletes need PRAGMA foreign_keys=ON in the maintenance session (as @ljieyao noted in #47729) — the app itself appears to run with FKs off.

    Suggestion for the fix

    Since MessageUpdated already projects by upsert, a compaction pass that retains only the latest message.updated.1 per (aggregate_id, message_id) would be safe by the same argument. That is suggestion #1 in the original report and what #36710 attempted. It would also be worth making event emit deltas rather than full snapshots for message.updated — the diff payload in summary.diffs[] is what makes each snapshot expensive, and re-sending an unchanged diff on every token is pure amplification.

    Happy to provide the pruning script if useful.

  20. one-pyy commented on Oct 5, 2026

    @one-pyy

    Adding one data point for the diagnostics slice of this. A meaningful part of the bloat in my own database came from metadata.diagnostics on edit-tool parts/events storing the whole workspace's diagnostics on every edit.

    I wrote a small cleanup tool, opencode-edit-diagnostics-pruner — https://github.com/one-pyy/opencode-edit-diagnostics-pruner — that removes only those diagnostics entries from historical part rows and completed message.part.updated.1 snapshots of a session, after a full SQLite online backup and within a single transaction.

    On my database it removed ~540 MB of JSON across 327 parts + 327 event copies.

    To be clear about scope: this only covers the diagnostics portion. It does not touch message.updated.1 growth and provides no general retention or compaction, so it is not a fix for this issue — just a targeted cleanup for one contributor.

  21. mihainadas commented on Oct 17, 2026

    @mihainadas

    Independent reproduction on OpenCode v1.18.35 (2026-10-17), from an automated builder workload:

    • opencode.db: 20 GB before cleanup
    • event: 92,639 rows before cleanup
    • message.updated: 11,381 rows / 16.8 GB (about 1.5 MB per row)
    • message.part.updated: about 28,000 rows / 90 MB
    • observed growth: about 6 GB/day

    This has the same full-snapshot amplification described here: each update while a long assistant message streams persists the whole message again, so storage grows roughly with the square of message length. The part-update events were comparatively small in this sample.

    I also confirmed that the current configuration schema has no retention, prune, or TTL setting; compaction.prune trims tool output in live context rather than persisted storage; snapshots are for filesystem undo; and opencode run has no ephemeral/no-persist option. The only supported cleanup available in the CLI was whole-session deletion (opencode session delete <id>).

    As a local stopgap, 213 sessions older than three days were deleted through the CLI (12 failed and remain retryable), reducing events from 92,639 to 42,348 before an idle-time vacuum. A built-in retention bound and/or delta/supersession behavior for message.updated would remove the need for consumers to implement whole-session cleanup themselves.

    Only aggregate counts and sizes are included here; no session identifiers, prompts, paths, or payload contents were inspected or published.

  22. heymaaz commented on Oct 23, 2026

    @heymaaz

    We’re seeing this in production with @opencode/sdk/workerd v2.1.4, using OpenCodeWorkerd.create in Cloudflare Durable Objects.

    Each DO runs short sessions and retains them for debugging. After 7 days / 641 sessions, one DO’s SQLite database reached 76.8 MB:

    Storage Size
    event 38.1 MB
    event indexes 3.2 MB
    session_message 32.7 MB
    Application-owned tables 0.6 MB

    Prompts and tool outputs appear in both the durable events and projected messages. Observed growth is approximately 12.7 MB/day.

    We also reproduced retention locally twice on v2.1.4 / Bun 1.4.4, using the recorded OpenAI cassette and Bus.configured({ persist: true }), matching workerd’s forced persistence setting. After one prompt completed and the session became idle:

    {
      "event": { "rows": 8, "bytes": 983 },
      "session_message": { "rows": 2, "bytes": 345 }
    }

    The assertion expecting zero retained events fails with Received: 8. This demonstrates the current retention behavior; automatic cleanup is the requested behavior, not an existing guarantee.

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

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions