You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Replace sessions.json with a redb-backed index for projects and thread metadata. Keep event bodies in files.
Why
Every change to any thread re-reads and rewrites the entire sessions.json (Store::upsert_meta / persist_index, crates/services/src/store.rs), including once per turn (persist_meta in crates/runtime/src/app/lifecycle.rs). There are ~18 call sites.
The maintainer does not want SQLite. The goal is a small embedded store, not a query engine: metadata is held in memory and filtered there (crates/runtime/src/app/sessions.rs), so the store needs point updates, ordered scans and crash-safe transactions.
Encode values as our own bytes (serde), not redb's typed Option encoding, which changes in redb 4.4.
One writer (the existing store writer task). Batch per drained queue; Durability::None for index-only updates, Immediate at turn end and on metadata changes.
Compaction: call compact() at startup/shutdown when the file has grown past ~2x its last compacted size (redb has no cheap "needs compaction" query). The DB holds only small rows, so this is expected to take milliseconds (124 ms measured for a 270 MB test file).
Locking: redb takes an exclusive lock. The desktop app and tcode-headless can open the same data dir, and password/pair subcommands open the store for its root path only. Open the DB lazily, in the host only, and fail with an explicit error instead of today's silent last-writer-wins.
Migration: on first start import sessions.json in one transaction, rename it to sessions.json.migrated, keep reading it as a fallback for one release.
Principle 9: redb 5.0 (unreleased) adds experimental-multiprocess; adopt early when it lands so headless/CLI can read while the host runs.
Out of scope
Event bodies in redb. Deleting a thread would not shrink the file until compact(), and redb has no compression (200 MB of values took 269 MB on disk in the test). This is the failure mode seen in Codex's logs_2.sqlite (1.18 GB, ~80% free pages, never vacuumed).
Related work
guivieiras/tcode's unmerged T3 Code importer adds rusqlite (bundled) behind an optional t3-import feature to read T3's state.sqlite. That is import-only and feature-gated, so it does not conflict with this issue, but it is the one place SQLite would enter the tree.
guivieiras/tcode@45e74fc1 (unmerged) adds SessionMeta.last_user_message_at, recovered from transcripts when missing. Any new meta field should be migrated once into the index rather than recovered by parsing logs at load.
Summary
Replace
sessions.jsonwith a redb-backed index for projects and thread metadata. Keep event bodies in files.Why
sessions.json(Store::upsert_meta/persist_index,crates/services/src/store.rs), including once per turn (persist_metaincrates/runtime/src/app/lifecycle.rs). There are ~18 call sites.crates/runtime/src/app/sessions.rs), so the store needs point updates, ordered scans and crash-safe transactions.Engine choice (measured Sep 2026, 15-core Mac, fat-LTO release)
Cargo.lockviatcode-traverse-server)compact()Rejected: sled (format still unstable), native_db (pins redb ^2), canopydb (self-described not production-ready), polodb (RocksDB).
Proposal
tcode.redbtables:projects,sessions(one row per id), optional(project_id, updated_at desc, id)ordering index, per-session log state (committed length, record/turn counts). Per-turn and per-item indexes are in Storage: turn index so long threads open without parsing the whole log #524.Optionencoding, which changes in redb 4.4.Durability::Nonefor index-only updates,Immediateat turn end and on metadata changes.compact()at startup/shutdown when the file has grown past ~2x its last compacted size (redb has no cheap "needs compaction" query). The DB holds only small rows, so this is expected to take milliseconds (124 ms measured for a 270 MB test file).tcode-headlesscan open the same data dir, andpassword/pairsubcommands open the store for its root path only. Open the DB lazily, in the host only, and fail with an explicit error instead of today's silent last-writer-wins.sessions.jsonin one transaction, rename it tosessions.json.migrated, keep reading it as a fallback for one release.experimental-multiprocess; adopt early when it lands so headless/CLI can read while the host runs.Out of scope
Event bodies in redb. Deleting a thread would not shrink the file until
compact(), and redb has no compression (200 MB of values took 269 MB on disk in the test). This is the failure mode seen in Codex'slogs_2.sqlite(1.18 GB, ~80% free pages, never vacuumed).Related work
rusqlite(bundled) behind an optionalt3-importfeature to read T3'sstate.sqlite. That is import-only and feature-gated, so it does not conflict with this issue, but it is the one place SQLite would enter the tree.SessionMeta.last_user_message_at, recovered from transcripts when missing. Any new meta field should be migrated once into the index rather than recovered by parsing logs at load.