Skip to content

Storage: replace sessions.json with a redb index #523

Description

@Tryanks

Summary

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.

Engine choice (measured Sep 2026, 15-core Mac, fat-LTO release)

Engine Clean build Binary delta New crates Space after deletes
redb 4.3.0 22.6 s +679 KB 0 (already in Cargo.lock via tcode-traverse-server) Freed pages reused; file shrinks only on compact()
fjall 3.1.10 31.4 s +929 KB 13 Automatic via compaction/blob GC; fixed 64 MiB journal; background threads
heed / LMDB 14.1 s +101 KB 5 (C) Never shrinks in place; map size preset; Windows preallocation history
rusqlite (reference) 34.7 s +1.75 MB — —
turso 0.8 pre 254 s +15.8 MB 312 total Beta

Rejected: sled (format still unstable), native_db (pins redb ^2), canopydb (self-described not production-ready), polodb (RocksDB).

Proposal

  • tcode.redb tables: 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.
  • 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestperformanceRuntime performance, latency, resource footprint

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions