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
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).
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`.fnis_opencode_db_filename(name:&str) -> bool{let stem = match name.strip_suffix(".db"){Some(s) => s,None => returnfalse};if stem == "opencode"{returntrue;}let channel = match stem.strip_prefix("opencode-"){Some(c) => c,None => returnfalse};
!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).
crates/ai-hist/src/discover.rs:298DiscoveryEnv.opencode_db: PathBuf, :332with_roots, :429/:476ScanEnv.opencode_db: &Path, :2109open_opencode_snapshot (one lazily opened snapshot keyed on the file's generation).
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
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).
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.
ingest.rs: sync_opencode_at / sync_opencode_exclusive iterate the list; the opencode-backup feature path (store.rs:4160) applies per file.
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.
Summary
OpenCode writes a per-channel database. Users on the
stableornightlychannel keep their sessions inopencode-stable.db/opencode-nightly.dbnext toopencode.db, and relayhistory never sees them. Found while comparing with tokscale (commitd8fd670).Upstream behavior
tokscale's
ScanResult::opencode_dbs(crates/tokscale-core/src/scanner.rs:96-104):README data-source row: "
~/.local/share/opencode/opencode.db(1.2+, all channels includingopencode-stable.db) or/and~/.local/share/opencode/storage/message/(legacy/unmigrated)".The rule (
scanner.rs:1190-1249) is small enough to copy: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-29opencode_db_path,:44ProviderRoots.opencode_db,:62from_home(home, opencode_db).crates/ai-hist/src/discover.rs:298DiscoveryEnv.opencode_db: PathBuf,:332with_roots,:429/:476ScanEnv.opencode_db: &Path,:2109open_opencode_snapshot(one lazily opened snapshot keyed on the file's generation).crates/ai-hist/src/ingest.rs:344sync_opencode_at(db_path, source_path, ...),:1049sync_watch_roots(home, opencode_db),:1382,:1702sync_opencode_exclusive.crates/ai-hist/src/ingest/opencode.rsheader documents "$OPENCODE_DB, default~/.local/share/opencode/opencode.db" as the SQLite layout.crates/ai-hist/src/ingest/hydrate.rs:1868-1880root validation has noopencodearm (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
paths.rs: addopencode_db_paths(home) -> Vec<PathBuf>: ifOPENCODE_DBis set, exactly that file (today's behavior); otherwise every entry of the data dir matchingis_opencode_db_filename, sorted.ProviderRoots.opencode_dbbecomes aVec(keep a single-path accessor for callers that genuinely mean "the default file", e.g.sync_watch_roots).discover.rs:OpencodeProviderkeeps oneOpencodeReadSnapshotper DB (theMutex<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.ingest.rs:sync_opencode_at/sync_opencode_exclusiveiterate the list; theopencode-backupfeature path (store.rs:4160) applies per file.--hook claude) #174): register each discovered DB (and its-wal) rather than only the default file.docs/session-catalog.mdOpenCode table row ("all channel databases under the data dir;OPENCODE_DBpins one file") and theingest/opencode.rsheader.Acceptance
opencode.db(session A),opencode-nightly.db(session B) and anopencode-nightly.db-walsidecar:sessions discover --locallists A and B;sessions hydrate opencode Bworks; the-walfile is never opened as a database.OPENCODE_DB=/x/opencode-stable.dbrestricts discovery to that file.opencode_parityand the existingsync_opencode_*tests pass unchanged.Related
--hook claude) #174 (watch roots), [G1] One-entry harness registry and out-of-tree local source plugins (absorb burn's collector backlog) #177 (registry: multi-root descriptors).