Repository navigation
feat(opencode): read-only SQLite adapter with v2.0.x inline schema support - #25
Conversation
…pport Adds the OpenCode adapter (read-only SQLite: session_v2 / session_message / session_part / project), a provider capability matrix entry, isolation coverage, and an anonymised fixture suite. Three shapes are detected structurally, never by a product version string: - v2 (v2.2.x): parts in session_part; - v2-inline (v2.0.x): content inline in session_message.data, no session_part; - legacy: session / message / part. The inline shape maps user.text, assistant.content[] (text / reasoning / tool), reads a tool exit code from state.metadata.exit, keeps provider-generated rows as meta, never invents a tool result, and orders events by seq with a deterministic tiebreak. An explicit VOYAGER_OPENCODE_DB / OPENCODE_DB is authoritative, so an operator-named database cannot trigger an unrequested read of the default HOME database. Real-install gate (one v2.0.29 install, isolated copy): probe -> ok/v2-inline, 8/8 sessions indexed, per-kind counts exactly match an independent read-only sqlite3 oracle, and the real .db / -wal / -shm hashes and all row counts are unchanged. Synthetic results stay UNIT_VERIFIED; native resume and cross-agent continuation remain NOT_FOUND / NOT_VERIFIED.
The opencode block still said 'no real install was observed' and described only v2/legacy shapes, which contradicts this PR's own gate report (one real v2.0.29 install, three shapes). States stay UNIT_VERIFIED: one install/version/platform does not justify promoting a cell, and nothing is LIVE_VERIFIED.
HarryHeYu
left a comment
There was a problem hiding this comment.
Final technical review — APPROVE (no blocking issues)
Reviewed at 9603164 (was 9471608; one fix added, see "Found and fixed").
Independent evidence, not just "CI is green": I re-derived each claim on
synthetic fixtures and re-checked the real-install gate.
1. Schema identification and priority — verified
| case | result |
|---|---|
v2.2.x (has session_part) |
('ok', 'v2') |
v2.0.x (inline, no session_part) |
('ok', 'v2-inline') |
| pre-v2 | ('ok', 'legacy') |
| readable non-OpenCode DB | ('unknown-schema', None) |
| truncated file | ('unreadable', None) — not absent |
DB with both a parts table and inline content |
('ok', 'v2') |
Priority is structural: SCHEMA_FORBID = {"v2-inline": ("session_part",)} means
a database that carries session_part can never be misread as inline, even if
SCHEMAS order changes later. No product version string is consulted.
2. session_message.data parsing — verified
Inline shape: user.text → user event; assistant.content[] → text /
reasoning / tool events. Tool exit code is read from
state.metadata.exit and 0 is preserved as a real code (not treated as
missing). 27/27 checks passed, including CJK text, one-tool-call/one-result
pairing, and a running tool producing no fabricated result.
3. Event order and counts — verified
seq is a strictly increasing 1..N counter per session; a second scan yields an
identical (seq, kind, content) stream (idempotent). Counts match the fixture
oracle exactly: user 1, assistant 2, reasoning 1, tool_call 1, tool_result 1,
meta 2 (system + compaction), with provider rows staying meta
(origin=provider_system).
4. Tool call / result pairing — verified
tool_call and tool_result share tool_call_id. A result is emitted only
when upstream recorded completed + output, or an error; running produces the
call alone. No exit code is invented for an error state.
5. WAL consistency, incremental indexing, idempotence — verified
A live writer appending one turn through -wal is seen on the next scan
(1 → 2 user events); repeated scans are byte-stable in event terms. -wal and
-shm are in the discovered source set, which is what makes a running agent's
new turns visible before a checkpoint.
6. Read-only behaviour and the -shm side effect — verified
Connections are mode=ro with PRAGMA query_only=1; an INSERT on that
connection is refused; the .db bytes are unchanged after a scan. The one real
side effect — SQLite may rewrite the -shm shared-memory index when opening a
WAL database read-only — is documented in the audit report rather than hidden.
.db and -wal bytes and all row counts are unchanged.
7. Explicit override isolation — verified
With VOYAGER_OPENCODE_DB set, _candidate_dbs() returns only the named
database, and discover() does not include the default data-dir database. This
closes the earlier finding that an operator-named file could cause an
unrequested read of the real HOME database.
8. Error handling — verified
Unknown message types are skipped (no invented content, no crash); a malformed
JSON row does not abort the scan; unknown schema / corrupt file are named
(unusable_sources / unrecognised_sources) rather than reported as "0
sessions" or retired as SOURCE_MISSING.
9. Capability matrix — no overclaim
No cell is LIVE_VERIFIED; native_resume and every startup/injection
dimension are NOT_FOUND_IN_CURRENT_AUDIT; discovery/indexing stay
UNIT_VERIFIED.
Found and fixed in review (P1, now in 9603164)
The opencode block still said "no real install was observed" and described
only v2/legacy, contradicting this PR's own gate report. Those strings are
user-visible in voyager verify --matrix, so I corrected them to state: three
shapes exercised synthetically, one real v2.0.29 install observed, states kept
at UNIT_VERIFIED because one install/version/platform does not justify
promotion. Re-ran the capability/doctor/verification/dashboard suites: 148
passed; test_opencode.py + test_docs.py + test_capability_consistency.py:
61 passed / 1 skipped.
Real-install gate — scope is narrow, and stated as such
Gate A compared the adapter against an independent read-only sqlite3 oracle on
a byte-identical copy: 8/8 sessions, user 68 / assistant 206 / reasoning 2053 /
tool_call 2617 / tool_result 2617, zero mismatches. Gate B: .db / -wal /
-shm SHA-256 and all 20 table row counts unchanged. Gate C: nothing promoted.
This is one install, one version (v2.0.29), one platform (Windows). It does
not generalise to other v2.0.x builds, to macOS/Linux, or to multi-channel
installs. Synthetic results are not a substitute for it, and native resume /
cross-agent continuation are not supported — the adapter denies resume.
Residual risks (non-blocking, documented)
- A future shape change fails loudly as
unknown-schema(good) but will need
another adapter round — detection is by table/column shape, not version. message_countcounts user+assistant content events, not raw
session_messagerows (existing contract).user.filesattachments on the inline shape were empty in the observed data:
NOT_VERIFIED.- Multi-channel and non-Windows installs:
NOT_VERIFIED.
Merge readiness
No blocking issues. After CI is green on 9603164 this is mergeable as an
additive provider (no Store/continuity/bridge-schema change, no migration, no
new dependency, no version or tag change). I am not merging it in this round
— the merge is a separate, explicit step, and no release is made here.
The previous commit was created through the Git Data API from the Windows working tree, so it uploaded CRLF and rewrote every line. Normalise to LF to match the repository; the content is unchanged.
What this adds
The OpenCode adapter: read-only SQLite indexing for OpenCode sessions, plus a
provider capability-matrix entry, isolation coverage, and an anonymised fixture
suite.
It supports three shapes, detected structurally (never by a product version
string):
v2(v2.2.x)session_v2session_messagesession_partv2-inline(v2.0.x)session_v2session_message.datalegacysessionmessagepartv2-inlineis discriminated by requiringsession_v2+session_messageandforbidding
session_part, so a v2.2.x database (which also hassession_message) still resolves tov2.Why the inline shape matters (this was a real failure, not a hypothetical)
The first Phase 3 cut only knew the v2.2.x parts schema. On a real
OpenCode v2.0.29 install the adapter returned
unknown-schema, discoveredno databases, and indexed 0 of 8 real sessions — while the 27 synthetic
tests were green, because their fixtures model v2.2.x. That gap is the reason
synthetic results are reported as
UNIT_VERIFIEDand never asLIVE_VERIFIED.Real structure observed (no user text):
session_message.dataholds contentinline —
user.textis a string;assistant.contentis an array oftext/reasoning/toolitems; a tool's exit code is instate.metadata.exit, notstate.exit.Behaviour details
file:...?mode=ro+PRAGMA query_only=ON; neverimmutable=1(that would hide committed WAL pages).-wal/-shmstay inthe discovered source set so a running agent's new turns trigger a rescan.
assistant content; reasoning; tool calls and results from the same record's
state;seqis a stable per-session counter; ms → s.origincomes from the messagetypecolumn.
system/synthetic/compaction/idle/model-switchedrows become
metawithprovider_system, never a human turn. A subagentsession (
parent_id) marks its user turnsprovider_synthetic.code stays
None(0is preserved as a real code); empty user text emits nouser event; an unknown message type is skipped rather than guessed.
VOYAGER_OPENCODE_DB/OPENCODE_DBisnow authoritative — the default data-directory database and channel builds
are not probed when an override is set. This prevents an operator-named
(or test-named) database from silently causing a read of a real HOME
database.
can_resume = can_fork = False,resume_cmd = None;the capability matrix says
NOT_FOUND.Tests
Directed (
tests/test_opencode.py, 36 tests): both v2 shapes, legacy, unknownschema, corrupt / absent database, tool call/result pairing, running tool with
no invented result, error without a fake exit code, synthetic/system not
promoted to a human turn, subagent sessions, Unicode (CJK/Japanese/Korean/emoji),
malformed JSON, empty sessions, read-only (DB/WAL bytes and row counts
unchanged), WAL discovery and WAL-driven incremental rescan through the real
Store+run_scan, idempotence, streaming equivalence, 2-char CJK search,SOURCE_MISSINGretention, a 5000-message session, and the override-priorityregression.
Real-install gate (one v2.0.29 install, isolated copy)
The real
.db/-wal/-shmwere copied byte-for-byte into scratch; everySQLite connection targeted the copy;
HOME,USERPROFILEandXDG_DATA_HOMEwere all redirected into scratch and the run asserts no candidate lies outside
it. The real directory was only hashed.
Gate A — schema + query results: PASS
The oracle is plain stdlib
sqlite3on the same copy — it does not import theadapter, so agreement is independent.
Gate B — no modification of user data: PASS
.dbSHA-256 before/after-walSHA-256 before/after-shmSHA-256 before/afterGate C — capability ledger: no cell promoted.
provider_statestaysNOT_FOUND_IN_CURRENT_AUDITin the isolated run; the adapter does not claimLIVE_VERIFIED.Report:
docs/audits/2026-11-opencode-v20x-real-gate.md(no user text, nopaths, no hashes).
Known limits (explicitly not claimed)
loudly as
unknown-schemarather than being misparsed.NOT_FOUND.NOT_VERIFIED(no live agent run).user.fileson the inline shape:NOT_VERIFIED(empty inthe observed data).
NOT_VERIFIED.message_countcounts user + assistant content events, not rawsession_messagerows (existing contract, unchanged).Scope
Additive: one provider in
enabled_adapters/PROVIDERS. No change toStore, the continuity engine, the bridge schema, any existing CLI contract,or any other adapter. No database migration, no new runtime dependency (stdlib
sqlite3only), no version bump, no tag or release change.Tracking: sessionFlow#23 (
fix(opencode): support v2.0.x inline session_message schema). Not merged by the author — left for maintainer review.