Skip to content

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

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.

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions