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)
- 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.
- 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).
- 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.
Summary
The local SQLite store (
~/.local/share/opencode/opencode.db) grows unbounded on long-lived instances because the event-sourcingeventtable 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 producingsqlite3.OperationalError: database or disk is fullon 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)
Per-table (
sum(length(data))):Within
event, bytype:message.updated.1session.next.tool.success.1message.part.updated.1So ~87% of the entire database is
message.updated.1events. With ~6,955 messages there are ~3.8message.updatedevents 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,
devbranch)packages/core/src/event/sql.ts— theeventtable has no TTL, no row cap, no size limit, and no compaction column; it just stores every event's fulldataJSON:packages/core/src/event.ts— the only delete againsteventis a full-aggregate cascade, used when an entire session/aggregate is removed: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.mdconfirms this is an intentional append-only log "for session replayability." That's a reasonable design goal, but with full-snapshotmessage.updatedevents and no retention it means every session's event history accumulates for the lifetime of the session, with no upper bound.SessionCompaction.prunedoes not address this — it trims context tokens sent to the model, not persistedeventrows.Impact
database or disk is full.VACUUMcannot reclaim it (freelist ≈ 0%; it is all live data). The only current remediation is deleting whole sessions (cascade) or manuallyDELETE FROM event ....Suggested fixes (any one would help)
message.updated.*, retain only the latest event per(aggregate_id, message_id)(or per messageseq), deleting older ones — newer fully supersedes older.message.updated(the README already notessession.updatedwas moved to fields-only;message.updatedappears to still carry a full snapshot).Environment
dev(currentlatest, 1.17.9) via the source above.~/.local/share/opencode/opencode.db.Happy to provide the full per-session breakdown or a repro script if useful.