Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ that index**, so they inherit it: treat `dashboard.html`, `handoff-*.md` and
Start with one agent rather than all of them:

```sh
voyager scan --platform codex # codex | claude | zcode | dsh, comma-separated
voyager scan --platform codex # codex | claude | zcode | dsh | opencode, comma-separated
```

Once at least one of your agents has session history on this machine:
Expand Down Expand Up @@ -614,6 +614,7 @@ For verification of actual zero-touch startup behavior, see [docs/DOGFOOD.md](ht
| Cursor | `state.vscdb` (SQLite) | ✅ | ✅ | ❌ | ⚠️ in raw | ⚠️ | ❌ IDE only |
| Kiro IDE | workspace-session JSON | ✅ | ❌ not persisted | ❌ | ❌ | ❌ | ❌ IDE only |
| Antigravity | conversation SQLite (protobuf) | ⚠️ heuristic | ⚠️ heuristic | ⚠️ text | ⚠️ snapshots on disk | ❌ | ❌ IDE only |
| OpenCode | read-only SQLite (`~/.local/share/opencode`) | ✅ | ✅ | ⚠️ shell only | ❌ | ✅ session totals | ❌ read-only index |

The **Resume** column names the command Voyager will run for that provider — it
says the command exists and is wired up, **not** that a resume has been observed
Expand All @@ -622,6 +623,14 @@ treat every `✅` there as `NOT VERIFIED` until the verification table above
carries a row saying otherwise. (The `*_continuity_verdict.md` files cover hook
injection, which is a different capability.)

OpenCode is indexed **read-only** from its SQLite database (both the current
`session_v2` schema and the pre-v2 `session`/`message`/`part` shape). That
gives you messages, tool calls and results in `search`/`show`/`repo` and lets
another agent continue from the context — it is **not** native resume, and no
resume command, startup hook or transcript writer exists for it. Verified only
against synthetic fixtures (`tests/test_opencode.py`); a real install has not
been observed yet. Set `VOYAGER_OPENCODE_DB` to point at a specific database.

Cursor and Antigravity adapters are marked experimental: Cursor reads its
key-value store read-only and Antigravity decodes protobuf blobs
heuristically (no public schema). Full per-field availability matrix and
Expand Down
10 changes: 9 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ voyager --db ~/.voyager/demo.db show codex:demo-auth-01 # 编译好的会话
先只扫一个 Agent,而不是全部:

```sh
voyager scan --platform codex # codex | claude | zcode | dsh,逗号分隔
voyager scan --platform codex # codex | claude | zcode | dsh | opencode,逗号分隔
```

只要本机至少有一个 Agent 留有会话历史:
Expand Down Expand Up @@ -422,12 +422,20 @@ voyager continue --thread thr_xxxx # 或 voyager switch claude --thread ...
| Cursor | `state.vscdb`(SQLite) | ✅ | ✅ | ❌ | ⚠️ 在 raw 里 | ⚠️ | ❌ 仅 IDE |
| Kiro IDE | workspace-session JSON | ✅ | ❌ 未持久化 | ❌ | ❌ | ❌ | ❌ 仅 IDE |
| Antigravity | 会话 SQLite(protobuf) | ⚠️ 启发式 | ⚠️ 启发式 | ⚠️ 文本 | ⚠️ 快照在磁盘 | ❌ | ❌ 仅 IDE |
| OpenCode | 只读 SQLite(`~/.local/share/opencode`) | ✅ | ✅ | ⚠️ 仅 shell | ❌ | ✅ 会话汇总 | ❌ 只读索引 |

**Resume** 一列写的是 Voyager 会为该 provider 运行的命令 —— 它说明命令存在且已接好,
**不**说明有人观察到 resume 真的成功过。目前**没有任何 provider 的 native resume 被端到端跑过**,
所以在那张验证表出现对应行之前,请把这一列的每个 `✅` 都当作 `NOT VERIFIED`。
(`*_continuity_verdict.md` 覆盖的是 hook 注入,那是另一种能力。)

OpenCode 从它的 SQLite 数据库**只读**索引(同时支持当前 `session_v2` 和旧版
`session`/`message`/`part` 两套表结构)。消息、工具调用与结果会进入
`search`/`show`/`repo`,也能给另一个 Agent 提供接续上下文——但这**不是**
native resume,也没有为其实现 resume 命令、启动 hook 或 transcript writer。
目前只用合成 fixture 验证过(`tests/test_opencode.py`),尚未在真实安装上观察。
可用 `VOYAGER_OPENCODE_DB` 指向指定数据库。

Cursor 与 Antigravity 适配器标记为实验性:Cursor 只读解析它的 KV 存储,
Antigravity 用启发式方式解码 protobuf blob(无公开 schema)。
每个工具的逐字段可得性矩阵和数据源路径见 [docs/RECON.md](docs/RECON.md)。
Expand Down
2 changes: 2 additions & 0 deletions docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ true on a date; it is not a description of the product and is not updated.
| [audits/2026-11-opencode-adapter-design.md](audits/2026-11-opencode-adapter-design.md) | DESIGN | maintainer | frozen — Phase 3 input: verified schema, options, risks, acceptance criteria |
| [audits/2026-11-phase-2-report.md](audits/2026-11-phase-2-report.md) | AUDIT | maintainer | frozen — Phase 2 delivery: docs, consistency checker, contract evidence, issue triage |
| [audits/2026-11-v1.3.1-final-release-report.md](audits/2026-11-v1.3.1-final-release-report.md) | AUDIT | maintainer | frozen — v1.3.1 release readiness: gates, artifact hashes, rollback, publish steps |
| [audits/2026-11-opencode-phase3-report.md](audits/2026-11-opencode-phase3-report.md) | DESIGN | maintainer | frozen — Phase 3 implementation: schema corrections, UNIT_VERIFIED scope, NOT_VERIFIED gaps |
| [audits/2026-11-opencode-v20x-real-gate.md](audits/2026-11-opencode-v20x-real-gate.md) | AUDIT | maintainer | frozen — v2.0.x inline-schema compatibility: real-install Gate A/B/C, no user data, no LIVE_VERIFIED claim |

## Performance evidence

Expand Down
162 changes: 162 additions & 0 deletions docs/audits/2026-11-opencode-phase3-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# OpenCode adapter — Phase 3 implementation report

> **Status: synthetic-verified only.** This round built and tested the adapter
> against **generated SQLite fixtures**. No real OpenCode install was read, no
> provider process was launched, and no native resume was attempted. Those
> claims stay `NOT_VERIFIED`; nothing here should be read as `LIVE_VERIFIED`.

## A. What was verified upstream, and from where

Checked on 2026-11-10 against `anomalyco/opencode` release **v2.2.6**
(2026-11-06). `packages/core/src/session/sql.ts` at that tag is
**byte-identical** to v1.19.5 (the version the Phase 2 design note used), so
the schema did not move between them.
Upstream sources were read from a scratch checkout of that tag, outside the
repository; they are not committed as an artifact.

| Fact | Source |
|---|---|
| Tables `session_v2`, `session_message`, `session_part`, `project` | `packages/core/src/session/sql.ts` |
| `session_part` has `session_id`, `ordinal`, `type`, `call_id` and `data` | same |
| Timestamps are `Date.now()` milliseconds | `packages/core/src/database/schema.sql.ts` (`Timestamps`) |
| Tool call + result live in ONE part's `state` (`status`, `input`, `content`, `error`) | `packages/schema/src/session-message.ts`, `packages/schema/src/tool.ts` |
| Message `type` values include `user`, `assistant`, `synthetic`, `system`, `skill`, `shell`, `compaction`, `idle`, `*-switched` | `packages/schema/src/session-message.ts` |
| Data root is `XDG_DATA_HOME/opencode` or `$HOME/.local/share/opencode` on all platforms | `packages/util/src/global-roots.ts` |
| `OPENCODE_DB` overrides the filename; channel builds use `opencode-<channel>.db` | `packages/cli/src/database-path.ts` |
| Pre-v2 schema uses `session` / `message` / `part` (different columns) | `packages/core/test/v1-migration.test.ts` (lines 834–885), `src/database/migration.ts` |

### Corrections to the Phase 2 design note

The design note was right about the recommended approach (read-only SQLite) but
wrong or incomplete on three points, all found by reading upstream code:

1. It listed `session_part` as `(message_id, ordinal) PRIMARY KEY, type,
call_id, data`. The real table **also has `session_id`**, `time_created` and
`time_updated`. Discovery of parts can be scoped by session.
2. It assumed `call_id` joins a tool *call* row to a separate *result* row.
**There is no separate result row.** One part carries both: `state.input` is
the call, `state.content` (array of `{type:"text",text}`) or `state.error`
is the result. `call_id` is recorded as `tool_call_id` but is not the join
key. The adapter emits `tool_call` always, and `tool_result` only when
upstream actually recorded a completed or errored state.
3. It listed the message JSON shapes as "to confirm". They are now read from
`packages/schema/src/session-message.ts`, including the distinction between
a real `user` message and provider-generated `synthetic`/`system` messages.
That distinction is what stops an agent's own claim from being indexed as a
human instruction.

The design note also said to check `PRAGMA user_version`; upstream bootstrap
keys off the **presence of `session` / `session_v2` tables** instead
(`src/database/migration.ts`), so the adapter probes table/column shape and
treats `user_version` as non-authoritative.

## B. What was implemented

`voyager/adapters/opencode.py`, registered in `adapters/__init__.py`.

- **Read-only.** `file:...?mode=ro` plus `PRAGMA query_only=ON`. Never
`immutable=1`: that would hide committed WAL pages and silently drop live
turns. No migration, no writes, no deletion of provider files.
- **Schema probe.** `absent` / `ok` / `unreadable` / `unknown-schema`, with a
real table read so a truncated file is not accepted as an empty index.
Both `v2` and `legacy` shapes are detected by table + required columns.
- **WAL-aware incremental scanning.** `-wal` and `-shm` are returned by
`discover()` deliberately: the scanner's `source_changed()` only stats
discovered paths, so without this a running agent's new turns stay invisible
until a checkpoint rewrites the main `.db`. Verified end to end through the
real `Store` + `run_scan` (6 → 7 events, new turn searchable).
- **Mapping.** Session (title/cwd/repo/model/tokens/timestamps), messages,
parts, tool calls and results, Unicode, reasoning, file parts, shell
messages, inline assistant content fallback. `seq` is a stable per-session
counter; `ts` is ms → s.
- **Provenance.** `origin` comes from the message `type` column, never from
text. `synthetic`/`system`/`skill` messages become `meta` with
`provider_system`, not `user`; a session with `parent_id` marks its user
turns `provider_synthetic`.
- **Failure behaviour.** Unknown/corrupt/locked databases are named on stderr
and reported via `unusable_sources()` / `unrecognised_sources()`, so a scan
never quietly reports "0 sessions". A vanished database follows the existing
O2 rule: history is retained as `SOURCE_MISSING`.
- **Streaming.** `emit=` produces the same bundles as the list form.
- **No native resume.** `can_resume = can_fork = False`, `resume_cmd = None`,
and the capability matrix says `NOT_FOUND`.

### A real limitation, stated honestly

Opening a WAL database even with `mode=ro` can make **SQLite itself create a
`-shm` shared-memory index file** if one does not exist. That is a SQLite side
effect, not a write to session history; the adapter does not create it
deliberately and never touches session rows. The alternative (`immutable=1`)
would hide committed WAL pages, which is worse. The test asserts the main DB
bytes, WAL bytes and all row counts are unchanged, and documents the `-shm`
behaviour rather than claiming "no files are ever created".

## C. Tests (all synthetic)

`tests/opencode_fixtures.py` builds databases from reviewable DDL (no committed
binaries). `tests/test_opencode.py` — **27 passed**:

- both schemas, including the legacy `tool`/`callID`/`state.output` shape;
- tool call/result pairing, running tool with no invented result, error state
without a fabricated exit code;
- synthetic/system messages not promoted to human turns; subagent sessions;
- Unicode (CJK/Japanese/Korean/emoji), malformed JSON rows, empty sessions;
- corrupt / unknown-schema / absent databases and the named warnings;
- read-only behaviour (DB bytes, WAL bytes and row counts unchanged);
- WAL discovery, WAL-driven rescan through the real scanner, idempotence,
streaming equivalence, 2-character CJK search via `run_scan`, `SOURCE_MISSING`
retention, and a 5000-message session.

Isolation: `tests/iso_guard.py` now covers `OPENCODE_DB`,
`VOYAGER_OPENCODE_DB` (explicit paths that bypass directory redirection) and
the `opencode/` directory derived from `XDG_DATA_HOME`.
`tests/test_isolation_guard.py` gained 4 regressions; a **mutation check**
confirmed the derived-path test actually fails when `_DERIVED_DATA_DIRS` loses
its `XDG_DATA_HOME` entry — the guard is load-bearing, not decorative.
Related suites: 62 passed / 2 skipped (isolation + adapter), 50 passed
(capability/doctor/adapters), 66 passed / 2 skipped (streaming/docs).

### Verification-level ledger

| Capability | Level | Evidence |
|---|---|---|
| Session discovery / indexing (synthetic DB) | `UNIT_VERIFIED` | `tests/test_opencode.py` |
| Message / part / tool mapping | `UNIT_VERIFIED` | same |
| WAL incremental rescan, idempotence, SOURCE_MISSING | `UNIT_VERIFIED` | real `Store` + `run_scan` in-test |
| Search / show / repo / merge / continue consume the index | `INTEGRATION_TESTED` via shared Store path; **not separately exercised for OpenCode** | existing CLI tests + `run_scan` search test |
| Real OpenCode install read | `NOT_VERIFIED` | no install observed |
| Native resume / startup hook / transcript writer | `NOT_FOUND` | not implemented; adapter denies resume |
| Cross-agent continuation from an OpenCode session | `NOT_VERIFIED` | no live agent run |

## D. Known gaps / next round

1. **Real-install verification is the top gap.** The message/part JSON shapes
were read from upstream schema code, not from a row produced by a running
OpenCode. A real install (any channel) should be pointed at with
`VOYAGER_OPENCODE_DB`, scanned, and its rows compared with the fixtures.
Only then may any cell move to `LIVE_VERIFIED`.
2. `session_v2` has more columns (`fork_*`, `summary_*`, `revert`, `permission`,
`time_archived`, …) that are intentionally not indexed. Archived sessions
are not filtered — a policy decision, not a bug.
3. `session_part.call_id` may be NULL on some builds. The adapter falls back to
the part's `id`/legacy `callID`; where none exists, `tool_call_id` stays NULL
rather than being invented.
4. Channel databases (`opencode-<channel>.db`) are all candidates and all get
probed; a machine with several channels will index each. This is deliberate
but untested against a real multi-channel install.
5. The static diagram (`scripts/make_diagram.py`) still shows eight platforms;
it is a generated illustration, not the capability source of truth. Updating
it is a separate asset change.
6. `OPENCODE_DB` can be a filename relative to the data dir or `:memory:`.
Relative names are resolved against the data dir; `:memory:` is skipped
because there is no file to index. Both are synthetic-tested, not real-tested.

## E. Compatibility impact

- Additive: one new provider in `enabled_adapters` and `PROVIDERS`. No change
to `Store`, the continuity engine, the bridge schema, or any existing CLI
contract. `voyager doctor` / `verify --matrix` gain one row whose states are
`UNIT_VERIFIED` at best.
- No database migration. Existing indexes are untouched.
- No new runtime dependency (stdlib `sqlite3` only).
- Not released: this work lives on the `phase3-opencode` branch only.
Loading
Loading