Skip to content

Excessive storage: event table stores full message snapshots per streaming update — community tool available #41175

Closed

Description

@ANGELBERRIOS23

Problem

opencode.db grows to multiple gigabytes because the event table stores a complete copy of every message on each streaming update — not a delta. On a typical installation with heavy agent use, the event table accounts for ~90% of the database size.

My installation (macOS, ~weeks of daily use):

Metric Value
opencode.db size 3.2 GB
Actual message content (message + part tables) ~10% of total
Redundant event snapshots ~90%

The message.updated.1 events are on average 4.4× larger than the final messages themselves, because each event carries the full accumulated text at that point in the stream.

Root cause

While a reply streams in, the event log records the full state on every token batch. For a 35 KB final message, the event table may store 20+ intermediate snapshots, each increasingly large. This is the expected behavior of an event-sourced architecture, but without compaction or pruning, it becomes a storage problem.

Proposed fix (upstream)

  1. Don't persist full snapshots per streaming update — store deltas or only the final state.
  2. Prune the event feed when a session completes — once streaming is done, intermediate snapshots serve no purpose.
  3. Add a built-in opencode db prune command — similar to git gc, let users reclaim space safely.

Community tool available

I built a single-file Python script (stdlib only, no dependencies) that safely reclaims this space without deleting any sessions, messages, or file parts:

https://github.com/ANGELBERRIOS23/opencode-db-prune

Features:

  • Report-only mode by default (changes nothing without --apply)
  • Backs up before pruning
  • Runs PRAGMA integrity_check before and after
  • Refuses to run if the DB is open or already corrupt
  • Verifies message content is readable outside event before deleting
  • Enables auto_vacuum = INCREMENTAL for future reclaimability
  • Cross-platform (macOS tested, Linux/Windows implemented)

On a real 39 GB installation it reclaimed 36 GB (down to 3.15 GB) with all 2,106 sessions, 59,294 messages, and 293,777 file parts intact.

I'd be happy to collaborate if the team wants to integrate this as a built-in command or adapt the approach upstream.

Related issues

Activity

  1. github-actions commented on Aug 8, 2026

    @github-actions
    Contributor

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

  2. Zaczero commented on Aug 18, 2026

    @Zaczero

    This is fixed for the local CLI and still open for the workerd / Desktop-sidecar path. A heavily used V2 instance (0.0.0-beta-17498, 3,246 sessions, 114,870 messages) has:

    event            0 rows
    event_sequence   3,246 rows
    page_size 4096 × page_count 462232 → 1.89 GB, almost all in session_message
    

    Event persistence became opt-in, default off, in 120e4e7388af61c1b9274b58a16b6e3f9878699c (“fix(core): make event persistence opt-in”, 2026-08-05). The flag defaults here:

    const persist = options?.persist ?? false

    One production caller still turns it on — the workerd server:

    events: { persist: true },

    On that path the 3.2 GB / 90%-events shape in this report is unchanged. SELECT count(*) FROM event vs event_sequence tells the two installs apart without the version: a fixed CLI is 0 / N, a workerd box is huge / N.

    Two things a prune script cannot paper over. There is still no retention on the workerd path (delete is per-aggregate, no TTL). And PRAGMA auto_vacuum is NONE, so even a real delete leaves the bytes in the freelist until a VACUUM — #31526, measured: deleting half the sessions stranded 855 MB. A community opencode db prune that does not VACUUM will look like a no-op on disk.

  3. ANGELBERRIOS23 commented on Sep 9, 2026

    @ANGELBERRIOS23
    Author

    @Zaczero Thanks, and sorry for the slow reply. Both of your points hold up, and the second one is the more important of the two.

    On VACUUM: you are right, and it is the failure mode most likely to make a community tool look broken. auto_vacuum is NONE on every install reported here and freelist_count is consistently 0, so a delete without a follow-up VACUUM returns pages to the freelist and the file on disk does not move at all. Your 855 MB measurement in #31526 is the concrete version of that. Anything I point people at does run VACUUM and an integrity_check afterwards, and I have written up the manual procedure in #33356 with the pre-flight check that the live projection lives in message/part rather than in event — without that check a prune is not safe to run at all.

    On retention: also correct, and worth restating plainly. Per-aggregate deletion with no TTL is not a retention policy, and on the workerd path there is nothing bounding growth. That matters more than it sounds because the growth is not spread evenly — @opgrsankkar measured six aggregates holding 92.6% of all event payload bytes. Age-based session cleanup cannot reach that; a single output-heavy session recreates the problem in an afternoon.

    One correction to "fixed for the local CLI". The flag default you linked is real, but it does not appear to have reached the stable CLI. @opgrsankkar measured 11.15 GB on Homebrew 1.18.25, macOS arm64, on 2026-09-01, with OPENCODE_EXPERIMENTAL and OPENCODE_EXPERIMENTAL_EVENT_SYSTEM both unset in shell and launchd, and 401,369 message.part.updated.1 rows accounting for 99.8% of payload bytes. They also checked the installed source and found Session.updatePart still calling events.publish(SessionV1.Event.PartUpdated, ...). So the shape is: opt-in landed in dev, but the stable release path still persists. Your event vs event_sequence count is the right way to tell them apart in the field and I have been pointing people at it, but the conclusion should be "check your install" rather than "CLI users are fine".

    Which leaves the 0 rows / 3,246 aggregates you measured as evidence that the fix works where it is actually active, not that it has shipped everywhere. That distinction seems worth keeping straight before this issue gets closed against the commit.

  4. lovasoa commented on Sep 10, 2026

    @lovasoa

    It looks like it's just bot talking to bots about issues generated by other bots, here.

    However, the issue highlighted here is real. It looks like 90% of the database contents is just partial repeated updates that are actually completely useless ? Does opencode save each progressive updates as the models stream their response ?

    $ sqlite3 ~/.local/share/opencode/opencode.db  "SELECT type, COUNT(*) AS rows, SUM(length(data))/1000000000 AS gib FROM event GROUP BY type ORDER BY gib DESC"
    
    message.updated.1|86508|10
    message.part.updated.1|253578|1
    session.updated.1|22906|0
    session.created.1|350|0
    message.removed.1|107|0
    

    I can see it storing incremental updates of cli tool outputs, that's crazy 🤯

    Details
    $ sqlite3 ~/.local/share/opencode/opencode.db "SELECT seq,json_extract(data,'$.part.state.metadata.output') FROM event WHERE aggregate_id='ses_f73d73895ffeByEeBpuZFVePH5' AND seq BETWEEN 2892 AND 2901;"
    
    2892|    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
    
    2893|    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
         Running `/home/ophir/dezoomify-react-wxt-target/debug/xtask test all`
    
    2894|    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
         Running `/home/ophir/dezoomify-react-wxt-target/debug/xtask test all`
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
    
    2895|    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
         Running `/home/ophir/dezoomify-react-wxt-target/debug/xtask test all`
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
    
    > dezoomify@ typecheck /tmp/opencode/dezoomify-react-wxt-migration
    > tsc --noEmit && pnpm -r typecheck
    
    
    2896|    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
         Running `/home/ophir/dezoomify-react-wxt-target/debug/xtask test all`
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
    
    > dezoomify@ typecheck /tmp/opencode/dezoomify-react-wxt-migration
    > tsc --noEmit && pnpm -r typecheck
    
    Scope: 9 of 10 workspace projects
    
    2897|    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
         Running `/home/ophir/dezoomify-react-wxt-target/debug/xtask test all`
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
    
    > dezoomify@ typecheck /tmp/opencode/dezoomify-react-wxt-migration
    > tsc --noEmit && pnpm -r typecheck
    
    Scope: 9 of 10 workspace projects
    apps/extension typecheck$ tsc --noEmit
    packages/browser-runtime typecheck$ tsc --noEmit
    packages/shared-ui typecheck$ tsc --noEmit
    
    2898|    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
         Running `/home/ophir/dezoomify-react-wxt-target/debug/xtask test all`
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
    
    > dezoomify@ typecheck /tmp/opencode/dezoomify-react-wxt-migration
    > tsc --noEmit && pnpm -r typecheck
    
    Scope: 9 of 10 workspace projects
    apps/extension typecheck$ tsc --noEmit
    packages/browser-runtime typecheck$ tsc --noEmit
    packages/shared-ui typecheck$ tsc --noEmit
    packages/browser-runtime typecheck: Done
    
    2899|    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
         Running `/home/ophir/dezoomify-react-wxt-target/debug/xtask test all`
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
    
    > dezoomify@ typecheck /tmp/opencode/dezoomify-react-wxt-migration
    > tsc --noEmit && pnpm -r typecheck
    
    Scope: 9 of 10 workspace projects
    apps/extension typecheck$ tsc --noEmit
    packages/browser-runtime typecheck$ tsc --noEmit
    packages/shared-ui typecheck$ tsc --noEmit
    packages/browser-runtime typecheck: Done
    packages/shared-ui typecheck: Done
    
    2900|    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
         Running `/home/ophir/dezoomify-react-wxt-target/debug/xtask test all`
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
    
    > dezoomify@ typecheck /tmp/opencode/dezoomify-react-wxt-migration
    > tsc --noEmit && pnpm -r typecheck
    
    Scope: 9 of 10 workspace projects
    apps/extension typecheck$ tsc --noEmit
    packages/browser-runtime typecheck$ tsc --noEmit
    packages/shared-ui typecheck$ tsc --noEmit
    packages/browser-runtime typecheck: Done
    packages/shared-ui typecheck: Done
    apps/extension typecheck: Done
    
    2901|    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
         Running `/home/ophir/dezoomify-react-wxt-target/debug/xtask test all`
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
    
    > dezoomify@ typecheck /tmp/opencode/dezoomify-react-wxt-migration
    > tsc --noEmit && pnpm -r typecheck
    
    Scope: 9 of 10 workspace projects
    apps/extension typecheck$ tsc --noEmit
    packages/browser-runtime typecheck$ tsc --noEmit
    packages/shared-ui typecheck$ tsc --noEmit
    packages/browser-runtime typecheck: Done
    packages/shared-ui typecheck: Done
    apps/extension typecheck: Done
    apps/desktop typecheck$ tsc --noEmit
    
    
  5. lovasoa commented on Sep 10, 2026

    @lovasoa

    Looking at the code, it looks like that huge multi-Gb events table is never even actually read ? It looks like the only places that read that table are just copying it to another instance where it will be equally ignored ? Could you maybe just remove it ? @thdxr

  6. jtoronto commented on Sep 17, 2026

    @jtoronto

    This script worked great. Shrunk my 17gb db to 3gb with no issues or data-loss afterwards.

  7. andrebrov commented on Oct 2, 2026

    @andrebrov

    Another data point from a multi-agent setup (opencode 1.18.34, macOS, 5 concurrent sessions running unattended, --auto):

    • opencode.db reached 122 GB in ~9 days (285 sessions, oldest created 2026-09-23).
    • event table: 683,865 rows, average payload ~77 KB (recent 2,000 rows; max ~5 MB).
      • message.part.updated.1: 449,534 rows
      • message.updated.1: 185,381 rows
      • session.updated.1: 48,696 rows
    • By contrast the materialised state is small: part 186,630 rows, message 47,124 rows; the largest single session's parts total ~0.1 GB.
    • pragma auto_vacuum = 0 and freelist_count = 0, so neither deleting sessions nor VACUUM would reclaim anything — all sessions were active in the last 7 days, and there is no CLI to prune or compact the event log (opencode db only offers a shell / path; opencode session delete removes the session rows, not the journal).

    Our only option was to stop all sessions and rotate the whole database file, which drops every live session.

    What would fix it for us, in order of preference: (1) store part deltas (or coalesce streaming updates) instead of a full snapshot per update; (2) retention/compaction for the event journal once an aggregate's state is materialised (e.g. opencode db compact --keep-days N); (3) auto_vacuum=incremental so deletions can be reclaimed without an exclusive full VACUUM.

    Related: #48641 (turn diff payloads) and #52586 (the journal also keeps redacted secrets in cleartext, which makes an unbounded journal a retention problem as well as a disk one).

  8. neriousy commented on Oct 19, 2026

    @neriousy
    Member

    The repeated streaming-snapshot storage is fixed in v2: text, reasoning, and tool-progress updates are live-only, while completed content is persisted at its final boundary rather than on every update. This addresses new event-log growth from streaming; it does not promise to reclaim an existing v1 database. To upgrade, run npm i -g @opencode/cli@latest. If you still see this on v2, please open a new issue with the details.

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

Metadata

Metadata

Assignees

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