Skip to content

OpenCode: discover channel-suffixed databases (opencode-stable.db, opencode-nightly.db) alongside opencode.db #209

Description

@willwashburn

Status against main @ 338ce5f (2026-09-21): still open. #168 merged as PR #199 (SQLite + legacy JSON parity, OPENCODE_STORAGE_DIR, crates/ai-hist/src/ingest/opencode.rs), but the SQLite side still opens exactly one file: opencode_db_path (crates/ai-hist/src/paths.rs:26-29) is $OPENCODE_DB else ~/.local/share/opencode/opencode.db, and git grep 'opencode-' crates/ai-hist/src finds no channel-suffix handling. This is now a follow-up to #168, not a prerequisite.

Summary

OpenCode writes a per-channel database. Users on the stable or nightly channel keep their sessions in opencode-stable.db / opencode-nightly.db next to opencode.db, and relayhistory never sees them. Found while comparing with tokscale (commit d8fd670).

Upstream behavior

tokscale's ScanResult::opencode_dbs (crates/tokscale-core/src/scanner.rs:96-104):

All OpenCode SQLite databases discovered under the data dir. Includes the default opencode.db (used by latest/beta channels and anyone with OPENCODE_DISABLE_CHANNEL_DB=1) as well as any channel-suffixed variants such as opencode-stable.db, opencode-nightly.db, etc. See upstream logic in opencode's packages/opencode/src/storage/db.ts (getChannelPath).

README data-source row: "~/.local/share/opencode/opencode.db (1.2+, all channels including opencode-stable.db) or/and ~/.local/share/opencode/storage/message/ (legacy/unmigrated)".

The rule (scanner.rs:1190-1249) is small enough to copy:

/// `opencode.db` or `opencode-<channel>.db` with `<channel>` drawn from the
/// same `[a-zA-Z0-9._-]` character class that opencode's `getChannelPath`
/// normalizes to. Sidecar files (`.db-wal`, `.db-shm`, `.db-journal`) are
/// rejected because they do not end in `.db`.
fn is_opencode_db_filename(name: &str) -> bool {
    let stem = match name.strip_suffix(".db") { Some(s) => s, None => return false };
    if stem == "opencode" { return true; }
    let channel = match stem.strip_prefix("opencode-") { Some(c) => c, None => return false };
    !channel.is_empty()
        && channel.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
}

discover_opencode_dbs(data_dir) lists the directory (accepting symlinks that resolve to files), filters, sorts, and returns all matches; user-supplied extra paths are merged and deduplicated by canonical path (scanner.rs:1719).

Where relayhistory holds the single path (main)

  • crates/ai-hist/src/paths.rs:26-29 opencode_db_path, :44 ProviderRoots.opencode_db, :62 from_home(home, opencode_db).
  • crates/ai-hist/src/discover.rs:298 DiscoveryEnv.opencode_db: PathBuf, :332 with_roots, :429/:476 ScanEnv.opencode_db: &Path, :2109 open_opencode_snapshot (one lazily opened snapshot keyed on the file's generation).
  • crates/ai-hist/src/ingest.rs:344 sync_opencode_at(db_path, source_path, ...), :1049 sync_watch_roots(home, opencode_db), :1382, :1702 sync_opencode_exclusive.
  • crates/ai-hist/src/ingest/opencode.rs header documents "$OPENCODE_DB, default ~/.local/share/opencode/opencode.db" as the SQLite layout.
  • crates/ai-hist/src/ingest/hydrate.rs:1868-1880 root validation has no opencode arm (paths are validated elsewhere for that source); whatever it does must accept every discovered DB.

The observation model already supports several observations per source with independent locators, stamps, revisions and checkpoints (docs/architecture.md, "Session ledger and location scope"), so multiple DBs need no schema change: one connector instance per file.

Proposed change

  1. paths.rs: add opencode_db_paths(home) -> Vec<PathBuf>: if OPENCODE_DB is set, exactly that file (today's behavior); otherwise every entry of the data dir matching is_opencode_db_filename, sorted. ProviderRoots.opencode_db becomes a Vec (keep a single-path accessor for callers that genuinely mean "the default file", e.g. sync_watch_roots).
  2. discover.rs: OpencodeProvider keeps one OpencodeReadSnapshot per DB (the Mutex<Option<_>> becomes a map keyed by path); connector_instance() is the file name so revisions and hydration checkpoints stay independent. A session present in two channel DBs (possible after a channel switch) is deduplicated by canonical identity, not double-listed.
  3. ingest.rs: sync_opencode_at / sync_opencode_exclusive iterate the list; the opencode-backup feature path (store.rs:4160) applies per file.
  4. Watch roots ([G1] Live capture: fs-event watch with debounce + polling fallback, stat-only change fingerprint, and a Claude hook fast path (--hook claude) #174): register each discovered DB (and its -wal) rather than only the default file.
  5. Docs: docs/session-catalog.md OpenCode table row ("all channel databases under the data dir; OPENCODE_DB pins one file") and the ingest/opencode.rs header.

Acceptance

  • Fixture data dir with opencode.db (session A), opencode-nightly.db (session B) and an opencode-nightly.db-wal sidecar: sessions discover --local lists A and B; sessions hydrate opencode B works; the -wal file is never opened as a database.
  • OPENCODE_DB=/x/opencode-stable.db restricts discovery to that file.
  • opencode_parity and the existing sync_opencode_* tests pass unchanged.

Related

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 request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions