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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
- muse
- omp
- opencode
- opencode-v2
- openhands
- pi
- qwen
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ here. Native format compatibility is documented separately in

## Unreleased

- Add version-aware OpenCode 2.0 public session transfer support, retaining the
legacy 1.17.20 adapter and native no-overwrite/dry-run controls. Preserve
portable tools, inline media, and completed compaction summaries, and account
for incomplete tools and provider-private state.

## 0.11.0 - 2026-09-11

- Read root Codex rollouts written with `history_mode: "paginated"` by using
Expand Down
22 changes: 14 additions & 8 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ silently changed. The source is never overwritten.
- Copilot reserves the complete session directory and writes events, workspace
sidecar, and manifest.
- OpenCode and Kilo reserve a private external manifest, invoke only the
official pinned importer, confirm the ID through official read/export
supported OpenCode or pinned Kilo importer, confirm the ID through official read/export
operations, then finalize the manifest.
- Antigravity and Cursor reserve the manifest, verify the exact pinned binary,
invoke their clean-room atomic database installers, validate the installed
Expand All @@ -178,15 +178,20 @@ that the session may already exist. Blind retry is intentionally avoided.
## Version boundaries

Claude/Codex writers are pinned to the local integration image; Pi, OMP,
OpenCode, Copilot, Antigravity, Cursor, Vibe, Muse, Qwen, Kimi, Grok, Kilo,
Copilot, Antigravity, Cursor, Vibe, Muse, Qwen, Kimi, Grok, Kilo,
OpenHands, Hermes, MastraCode, and Devin to exact host
builds/releases. A source declaring a
different version produces `unvalidated_source_version`. A
`--target-cli-version` override changes metadata only and produces
`unvalidated_target_version`; it never changes writer architecture.

Automatic OpenCode, Kilo, Antigravity, Cursor, and Hermes installation is stricter: metadata
overrides cannot bypass exact runtime version checks. Antigravity verifies its
`--target-cli-version` override changes metadata only for most writers and
produces `unvalidated_target_version`. OpenCode selects between its legacy
and 2.0 public transfer schemas; native import detects the installed CLI,
while convert-only defaults to legacy. Exact release warnings are scoped to
the selected schema. See [OpenCode v2](opencode-v2.md).

Automatic Kilo, Antigravity, Cursor, and Hermes installation is stricter: metadata
overrides cannot bypass exact runtime version checks. OpenCode requires legacy
1.17.20 or its supported 2.0 schema family and rejects artifact/CLI schema
mismatches. Antigravity verifies its
binary digest. Cursor verifies launcher, main bundle, protobuf-bearing chunk,
bundled Node, sizes, SHA-256 values, and reported version.

Expand All @@ -205,7 +210,8 @@ Enumeration covers:
- Claude main sessions and nested sidechains;
- Codex active and archived rollouts;
- Pi and OMP workspace buckets, classified by their native heads;
- every OpenCode and Kilo `session` row, including parents/archives;
- every OpenCode `session_v2`/legacy `session` and Kilo `session` row,
including parents/archives;
- Copilot session directories, including missing event logs;
- Antigravity conversation DBs;
- Cursor workspace/chat DBs, including missing stores;
Expand Down
21 changes: 13 additions & 8 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ validation.
`--format` bypasses automatic format selection. It does not make an unsupported
schema or version safe.

Kilo and OpenCode official bundles intentionally share one schema and retain
Kilo and legacy OpenCode official bundles intentionally share one schema and retain
stored metadata across cross-imports. A standalone bundle cannot be attributed
reliably, so automatic inspection fails closed; pass `--format kilo` or
`--format opencode` for a trusted bundle.
`--format opencode` for a trusted legacy bundle. Flat OpenCode 2.0 bundles are
recognized distinctly; Kilo rejects that schema.

## `convert`

Expand Down Expand Up @@ -83,7 +84,7 @@ performs conversion, native validation, and collision checks but does not
install a session or manifest. OpenCode's official read/list preflight can
initialize its ordinary XDG cache/database metadata during a dry run.

OpenCode and Kilo import always use their official pinned CLIs and do not accept
OpenCode and Kilo import always use their official CLIs and do not accept
`--home`; isolate or select them with normal `HOME`/XDG variables. Antigravity
and Cursor installs verify the exact pinned executable and its published
hashes. Muse and Qwen install one native JSONL. Kimi, Grok, and OpenHands
Expand Down Expand Up @@ -113,7 +114,7 @@ smigrate transfer fix-timeline-merging --from devin --to claude
```

Claude, Pi, OMP, Cursor, Vibe, Qwen, Kimi, and Grok can use `--source-cwd` to
select a workspace-specific store. OpenCode and Kilo are virtual: the pinned
select a workspace-specific store. OpenCode and Kilo are virtual: the supported
official CLI exports the requested ID. Hermes, MastraCode, and Devin are also
virtual catalog sources, but their selected native ID is parsed directly from
the agent's shared database. All other sources are read from their native files.
Expand Down Expand Up @@ -144,17 +145,21 @@ default. Every other source requires an explicit target.
| `--format FORMAT` | Override source detection for file-based `inspect`, `convert`, or `import` |
| `--session-id UUID` | Assign a new target UUID; generated by default |
| `--cwd PATH` | Target working directory; precedence is option, source CWD, process CWD |
| `--target-cli-version VERSION` | Change emitted metadata only; the writer architecture remains pinned |
| `--target-cli PATH` | Pinned OpenCode, Kilo, Antigravity, Cursor, or Hermes executable/source for native import |
| `--target-cli-version VERSION` | Select legacy or 2.0 transfer schema for OpenCode; change metadata only for other targets |
| `--target-cli PATH` | Supported OpenCode or pinned Kilo, Antigravity, Cursor, or Hermes executable/source for native import |
| `--model-provider ID` | Codex, Pi, OMP, OpenCode, or Muse target provider |
| `--model ID` | Target model label for formats that persist one, including Grok, Kilo, and OpenHands |
| `--home PATH` | Target native home, except OpenCode and Kilo |
| `--dry-run` | Validate and collision-check without installing migrator artifacts |

An irrelevant target-specific option may be accepted but has no effect. The
manifest records the target metadata version and warns when it differs from the
validated writer pin. Automatic OpenCode, Kilo, Antigravity, Cursor, and Hermes
installation still requires the exact pinned version.
validated writer release. Native OpenCode import selects its schema from the
installed CLI unless explicitly overridden: legacy `1.17.20` or the `2.0.x`
family. Conversion without native import defaults to legacy; use
`--target-cli-version 2.0.22` for a v2 bundle. Other native schema series fail
closed. See [OpenCode v2](opencode-v2.md) for exact validation scope. Kilo,
Antigravity, Cursor, and Hermes installation requires the exact pinned version.

## Home resolution

Expand Down
11 changes: 11 additions & 0 deletions docs/credential-free-native-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ No production credential is read, translated, or written by this workflow.
| Muse 0.2.1 | Exact Muse with the pinned OpenRouter adapter pointed locally; replay and append |
| OMP 18.0.5 | Local provider; replay, append, and rename |
| OpenCode 1.17.20 | Official import/export plus local provider replay and append |
| OpenCode v2 | Stock 2.0.22 locally; stock 2.0.23 CI variant; public import/export, catalog, cold reopen and local continuation |
| OpenHands 1.16.0 | OpenAI-compatible override; native creation, reload, TUI replay, and append |
| Pi 0.80.6 | Offline RPC with local provider; tools, images, compaction, replay, and append |
| Qwen 0.22.1 | OpenAI-compatible override; imported history replays and the reply persists |
Expand All @@ -82,3 +83,13 @@ harness and installations do not compete for disk space.
The normal Python job separately runs the deterministic 18 by 18 route oracle.
The native matrix is the independent acceptance layer: generated files must be
accepted by code owned and shipped by each harness.

## OpenCode v2 variant

The additional `opencode-v2` job pins `@opencode/cli@2.0.23` and sets
`SESSION_MIGRATE_TEST_OPENCODE_V2`. Its runner requires every assigned test to
run without skips, exercising synthetic import/export, collisions, cold reopen
and localhost-only continuation. The same native tests accept an explicitly supplied stock 2.0.22 binary and
select its observed release in the artifact. Legacy `opencode` remains pinned
to 1.17.20.
See [the v2 transfer contract](opencode-v2.md).
3 changes: 2 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ pytest and Ruff are development dependencies locked by `uv.lock`.
| `src/session_migrate/formats/codex.py` | Codex rollout reader and legacy writer |
| `src/session_migrate/formats/pi.py` | Pi 0.80.6 v3 writer/parser/validator |
| `src/session_migrate/formats/omp.py` | Oh My Pi 18.0.5 title-slot v3 adapter |
| `src/session_migrate/formats/opencode.py` | OpenCode 1.17.20 public-bundle writer/parser/validator |
| `src/session_migrate/formats/opencode.py` | OpenCode legacy/v2 public-bundle dispatch, writer/parser/validator |
| `src/session_migrate/formats/opencode_v2.py` | OpenCode 2.0 flat transfer projection and validation |
| `src/session_migrate/formats/copilot.py` | Copilot CLI 1.0.70 event writer/parser/validator |
| `src/session_migrate/formats/antigravity.py` | Antigravity 1.1.16 clean-room DB adapter |
| `src/session_migrate/formats/cursor.py` | Experimental pinned Cursor text DB adapter |
Expand Down
14 changes: 11 additions & 3 deletions docs/format-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ to separately installed host binaries:
| Codex CLI | `0.144.4` |
| Pi source and target | `0.80.6` |
| Oh My Pi source and target | `18.0.5` |
| OpenCode source and target | `1.17.20` |
| OpenCode legacy source and target | `1.17.20` |
| OpenCode v2 transfer adapter | Stock Linux `2.0.22` local gate; `2.0.23` CI pin |
| GitHub Copilot CLI source and target | `1.0.70` |
| Antigravity CLI source and target | `1.1.16` |
| Cursor Agent experimental text adapter | `2026.03.20-44cb435` |
Expand Down Expand Up @@ -54,6 +55,13 @@ rendering without a model append; Devin proves ACP loading and the vendor-login
boundary. See
[Credential-free native client testing](credential-free-native-testing.md).

OpenCode 2.0.x has a separate public-transfer adapter. Its local synthetic tests
cover native import/export, tool linkage, completed compaction, dry-run and
identity conflicts; this does not extend the historical eighteen-client corpus
validation claim. See [OpenCode v2 transfers](opencode-v2.md) for exact scope and
limitations. No real user transcript migration or native Windows validation has
been performed for this adapter.

All eighteen formats are sources and targets. Their mappings, native probes, and
loss keys are specified in [Additional native formats](additional-target-formats.md),
[OpenCode source research](opencode-source-exploration.md),
Expand Down Expand Up @@ -150,7 +158,7 @@ the current fixed-title-slot form. See [the exact OMP contract](omp-format.md).

### Additional native stores

- OpenCode sessions are inventoried from its read-only SQLite `session` table
- OpenCode sessions are inventoried from its read-only SQLite `session` / `session_v2` tables
and exported/imported only through the pinned official CLI.
- Copilot sessions use `~/.copilot/session-state/<uuid>/events.jsonl` plus the
workspace sidecar and content-addressed assets.
Expand Down Expand Up @@ -387,7 +395,7 @@ Claude, Codex, Pi, OMP, Copilot, Antigravity, Cursor, Vibe, Muse, Qwen, Kimi,
Grok, and OpenHands native files plus
content-free manifests use no-clobber private publication; if manifest creation
fails after a new filesystem target is created, the error reports whether that
native session may remain. OpenCode and Kilo instead use their exact pinned
native session may remain. OpenCode and Kilo instead use their version-gated
public importers and publish only a private migrator manifest after official
export-based verification; the migrator never writes either SQLite database.
Explicit UUID resume is the
Expand Down
92 changes: 92 additions & 0 deletions docs/opencode-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# OpenCode 2.0 public session transfers

OpenCode 1.17.20 uses `import/export --pure` and nested message/part bundles.
OpenCode 2.0 uses `session import/export --standalone` and flat typed messages.
Native import detects the installed client and selects its transfer schema.
Convert-only output remains legacy unless `--target-cli-version 2.0.22` or another accepted 2.0.x release is set.
Unsupported schema series fail closed. The `opencode v` version prefix and
custom suffixes are accepted; unvalidated exact releases receive schema-aware
warnings. Local native gates cover stock 2.0.22; the CI variant pins stock
2.0.23. Accepting the 2.0 schema family does not establish native compatibility
with every future patch or custom build. Unknown series such as 2.1 and 3.x
fail closed during native import/export.

The v2 adapter preserves ordered user/assistant text, linked tool inputs and
results, inline user/tool images, and readable completed compaction summaries.
Completed tool results retain the original ordered `content_blocks`, including
text segmentation and captions interleaved with multiple images, in portable
events and v2 output. Capable targets such as Claude, Codex and Pi consume these
ordered blocks. Legacy OpenCode 1.17.20 and Kilo 7.5.0 have a single output string
plus separate attachments: flattening the ordered blocks is counted once per
result as `tool_result:content_block_order` when it changes their representation.
Structured tool errors retain their message as a failed result. Omitted native
error type, HTTP status and response are counted separately as
`opaque:opencode_v2_tool_error_type`, `opaque:opencode_v2_tool_error_status` and
`opaque:opencode_v2_tool_error_response`; type is counted when it differs from
the output's `ToolError`. Diagnostic response bodies never enter opaque-event
payloads, manifest metadata or privileged messages.
User text blocks in one native message become their exact newline join.
Private-only or whitespace-only checkpoints do not retire readable history.
Encrypted provider checkpoints, private traces, source reasoning variants and
unsupported control/metadata fields are explicitly counted as omissions.
The reader validates original session fields and all accepted typed messages
before projection, including system, synthetic, skill, switch, shell, idle and
every compaction status. Optional fields may be absent; explicit `null` is
rejected when the pinned schemas require a typed value. Message and content
limits apply before discarded control records or joined tool text can hide them.

A streamed tool input can be an incomplete JSON string. The source projection
keeps it unchanged inside `{ "input": ORIGINAL_STRING }`; it does not replace
it with `{}` or attempt to parse/complete it. On conversion, an unfinished call
is archived as an `ImportedIncompleteTool` error with the same ID and input,
because native transfer discards unsettled assistant records. This is historical
input preservation, not an executable resumed partial tool invocation. The
source→portable→target regression includes an unterminated string input.

OpenCode owns its database import. No private SQLite writes, credentials or
machine-specific launchers are part of the adapter. Dry run performs public
native preflight without importing or writing a migration manifest; native
preflight may initialize the client's ordinary store. Imports enforce global
identity collisions and explicit success confirmation, then verify the imported
ID using public export. A conflict can have native exit status zero and still
fails import confirmation.

## Reproducible native and route checks

```sh
./scripts/install-native-test-clis.sh /tmp/session-migrate-native opencode-v2
./scripts/run-native-test-client.sh \
opencode-v2 /tmp/session-migrate-native/session-migrate-native.env
PYTHONPATH=src python3 -m pytest -q tests/test_opencode_v2.py tests/test_opencode_v2_review.py tests/test_route_matrix.py
```

The `opencode-v2` CI matrix entry installs **@opencode/cli@2.0.23** (the legacy
package is opencode-ai), supplies `SESSION_MIGRATE_TEST_OPENCODE_V2`, and rejects
skipped assigned tests. Native tests use synthetic transcripts, isolated HOME
and XDG directories, an explicit credential-free config, and a deterministic
localhost provider. They verify import/export, dry run, collisions, cold process
reopening, compaction/tool replay and a new persisted reply under the same ID.
They do not use real accounts or supplier inference credits. The default suite
can skip native tests when neither tested executable is supplied; the selected
CI job cannot silently pass those skips.

Route tests cover legacy and v2 OpenCode source and target variants against the
other supported formats. The 19 variants remain eighteen harnesses. Existing
legacy wire behavior and native tests remain separate.

The metadata catalog reads `session_v2` and legacy `session` within one
read-only transaction. A duplicate ID uses the v2 metadata; legacy-only rows
remain visible. A missing v2 title is represented as an untitled session.
Message bodies are never queried or indexed. Kilo 7.5.0 continues to emit and
accept only the legacy nested schema, even with a metadata version override.

For a preinstalled stock 2.0.22 binary, no dependency installation is needed:

```sh
SESSION_MIGRATE_TEST_OPENCODE_V2=/absolute/path/to/opencode \
PYTHONPATH=src python3 -m pytest -q \
tests/test_opencode_v2.py tests/test_opencode_v2_review.py tests/test_opencode_v2_native.py
```

Native Windows acceptance and all historical client versions are not
established by these Linux gates.
13 changes: 9 additions & 4 deletions docs/session-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ disposable SQLite index and never changes an agent session store.
An exhaustive refresh means **every recognized native session below every
enabled catalog root**: every expected JSONL or per-session database, including
declared Copilot/Cursor directories with missing native state, plus every
OpenCode/Kilo `session` row and every Hermes/MastraCode/Devin shared-database
OpenCode `session_v2`/legacy `session` row, Kilo `session` row and every
Hermes/MastraCode/Devin shared-database
identity. It does not mean an implicit whole-disk crawl. Agent
homes can have arbitrary names and locations, so discovering all of them still
requires either a known root or an explicit search boundary.
Expand Down Expand Up @@ -164,14 +165,17 @@ timestamps/version, and record count. A selected transfer then parses exactly
that identity. The catalog never stores the conversation rows.

OpenCode enumeration opens `HOME/opencode.db` with SQLite `mode=ro` and
`query_only`, then projects only these `session` columns:
`query_only`, then projects only these columns from `session_v2` and legacy
`session` in one read-only transaction:

```text
id, title, directory, version, time_created, time_updated,
parent_id, time_archived
```

Kilo uses the same bounded read-only inventory strategy against `HOME/kilo.db`.
Duplicate IDs use v2 metadata, and a null v2 title appears as an untitled
session. Kilo uses the same bounded read-only inventory strategy against the
legacy `session` table in `HOME/kilo.db`.
Neither scan runs a per-row export or inspects `message`/`part` tables.
This keeps refresh work proportional to the small inventory table, not the
total transcript corpus.
Expand Down Expand Up @@ -286,7 +290,8 @@ match at least one indexed field for the same session. Search covers:
- Claude sidechain `agentId` and `agent-<id>` filename keys;
- Pi `session_info.name` values and native session IDs;
- OMP fixed-slot/header/`title_change` values and native session IDs;
- OpenCode and Kilo native session IDs and bounded `session.title` values;
- OpenCode native IDs and bounded `session_v2.title`/`session.title` values;
- Kilo native IDs and bounded `session.title` values;
- Copilot session IDs, `session.title_changed` values, and bounded picker names
from `workspace.yaml`;
- Antigravity UUIDs and bounded native summary titles;
Expand Down
Loading
Loading