Skip to content

feat(opencode): support OpenCode v2 transfers + Windows rollback guard fix - #3

Merged
gabszap merged 39 commits into
mainfrom
feat/opencode-v2
Oct 17, 2026
Merged

gabszap merged 39 commits into
mainfrom
feat/opencode-v2

Conversation

@gabszap

@gabszap gabszap commented Oct 16, 2026

Copy link
Copy Markdown
Owner

PR 3 de 3 — empilhado: merge por último (depois do PR 1 e do PR 2).

O que entra

  • OpenCode v2 (2.0.x) como fonte e alvo. O adapter estava pinado em 1.17.20 e falhava fechado no OpenCode atual: expected 1.17.20, observed opencode v2.0.30. Os 5 commits base são do @tudstudent (a branch opencode-v2-transfer, base do PR feat(opencode): support v2 native transfer and pinned source-target validation xhluca/session-migrate#15 e do PR feat(opencode): support v2 transfers and catalog discovery xhluca/session-migrate#17 no upstream), preservando autoria: seleção da família de schema pela CLI instalada, import/export pelo CLI público (opencode session import/export --standalone, sem escrever tabelas privadas), descoberta de catálogo via session_v2, preservação de texto ordenado, tools vinculadas, mídia inline e resumos de compaction, com conta explícita do que é omitido.

    Meu ajuste de merge: a entrada de changelog deles caiu dentro de uma seção já lançada (a base era antiga) — movida para ## Unreleased.

  • fix(windows): release the opencode manifest guard before the rollback unlink (nosso). Nos caminhos de install do OpenCode e do Kilo a guarda de identidade do manifesto era fechada só no finally, mas o rollback deslinkava o manifesto dentro do except: no Windows o unlink falha com WinError 32 e essa mensagem de limpeza substitui a falha que causou o rollback. É a mesma classe que o PR fix(windows): close native transfer guards before rollback cleanup xhluca/session-migrate#14 (fechado) do @tudstudent tentava resolver, aplicada aos dois caminhos que faltavam. Sem isso, uma migração real de 2.017 registros para um store de 2 GB terminou com erro de "arquivo em uso" enquanto a sessão já estava importada.

Validação real (2.0.30 stock, Windows)

rota resultado
opencode v2 → hermes dry-run 711 records; import real: 710 msgs, 238 tool calls
opencode v2 → claude 609 records; unvalidated_source_version observed 2.0.30 / validated 2.0.23 (aviso, não bloqueia)
claude → opencode v2 import real num store isolado: session_v2 com título acentuado, 1065 session_message, 31 linhas de mídia inline
hermes → opencode v2 import real: 2017 records, 1091 session_message
testes test_opencode_v2*.py → 177 passed, 8 skipped (os skips são os gates nativos que aceitam só 2.0.22/2.0.23 e exigem Linux)

Nenhuma escrita no store real durante os testes: tudo com XDG_*/HOME isolados e --standalone.

elijahr and others added 23 commits October 16, 2026 18:44
- add bidirectional format adapter for Antigravity Desktop (antigravity-desktop)
- support reading and writing SQLite conversation databases, conversation_summaries.db, and agyhub_summaries_proto.pb
- handle both 19-column and 21-column schemas (with raw_summary, group_id) in conversation_summaries.db
- support up to 512 MB native sessions in Antigravity CLI and Desktop
- auto-register projects in app_storage.json and ~/.gemini/config/projects/ when installing desktop sessions
- document the desktop storage architecture in docs/antigravity-desktop-interoperability.md
- add comprehensive unit tests and native language server verification
…tform guards

- add default_language_server_path() supporting darwin, linux, and win32
- add electron_user_data_dir() resolving app_storage.json on macOS, Linux (XDG), and Windows (APPDATA)
- update verify_pinned_desktop() with clear platform diagnostics and unvalidated bypass for Linux/Windows
- document platform support status in docs/antigravity-desktop-interoperability.md
- add platform resolution tests across macOS, Linux, and Windows
…, and Windows

- add verified binary sizes and SHA-256 digests for Linux x64 and Windows x64
- implement PINNED_DESKTOP_SPECS table validating binaries against platform-specific digests
- enhance Linux search order (PATH launcher, /opt, ~/.local/share) and Windows case-insensitivity
- update documentation with complete multi-platform installer matrix
…tion arguments

- rename docs/antigravity-desktop-interoperability.md to docs/antigravity-desktop-format.md to match repository naming convention
- update invocation flags and arguments to reflect verified package launcher details
- update reference in CHANGELOG.md
…file URI paths

- Close SQLite connection handles explicitly before unlinking or cleaning up temporary directories (prevents WinError 32 on Windows).

- Bypass directory fd fsync on Windows NT in conversion.py, jsonl.py, and devin.py to avoid PermissionError [Errno 13].

- Normalize Windows drive letter parsing in file:/// URIs (strip leading slash before drive letter).

- Adjust stat permission test assertion for Windows NT (0o666 vs 0o600).

- Restore standard PyPI endpoints in uv.lock.
…a 31

- Move the Hermes pin from 0.20.6 (v2026.8.27, schema 26) to 0.22.2
  (v2026.9.24, schema 31). Schema 31 keeps every column the adapter requires
  from schema 26, so the source projection and the required-column gates are
  unchanged.
- Match the version line on its pinned prefix instead of the whole line: 0.22.2
  appends build provenance (" Hermes Agent v0.22.2 (2026.9.24) · stable ·
  local <sha>"), which made the exact-equality gate fail on a correct install.
- Keep the full --version report, because it publishes the install directory
  the runtime resolution needs (resolved in the next commit, which completes
  the write path for this release).
- Track the new pin in the native corpus fixture script and parser docs.
- Ask the launcher for its runtime command (hermes --print-runtime-command) and
  reproduce the prelude the published launcher applies to itself: clear
  PYTHONHOME, PYTHONPATH and VIRTUAL_ENV, insert the install root and import
  hermes_bootstrap, which selects and activates the store's dependency
  generation. Store Python ships without third-party packages, so importing
  hermes_state failed on ruamel before this.
- Recover the install root from the "Install directory:" line, or from the
  launcher's own sys.path.insert bootstrap when that line is absent. A Windows
  launcher is a compiled zipapp, so the file head carries no usable shebang;
  the shebang stays as the fallback for builds predating the flag.
- Keep the Windows essentials (USERPROFILE, HOMEDRIVE/HOMEPATH, APPDATA,
  LOCALAPPDATA, SYSTEMROOT, SYSTEMDRIVE, WINDIR, PATHEXT, COMSPEC, TEMP, TMP)
  in the sanitized child environment and mirror HOME into USERPROFILE. Without
  them the CLI cannot resolve a home directory, returns non-zero and prints
  nothing, which surfaced as "observed <unavailable>".
- Initialize a missing state.db through the native SessionDB constructor:
  hermes sessions list no longer creates the store on a read of an absent one.
- Report the launcher's stderr tail when initialization fails instead of a bare
  "failed to initialize" message.
…nlink

Windows refuses to delete a file that still has an open handle, so every failed
shared-database install died with WinError 32 while unlinking its reserved
manifest and masked the original install error. Close the guard first in each of
the six install rollback paths and re-close it as a no-op in the surrounding
finally, so the guard is still released exactly once on every path.
…d range

Windows reports st_dev as an unsigned 64-bit volume serial (15332589137577146414
for an NTFS system volume), which SQLite cannot bind at all: the session INSERT
raised OverflowError, the root scan failed closed, and every SQLite-backed root
(OpenCode, Kilo, Hermes, MastraCode, Devin) ended as scan_failed. Store the
folded value and compare through the same helper so refresh reuse keeps
matching. Adds a regression test with a simulated unsigned identity, covering
storage and the second-refresh unchanged path.
…s limits

- Pin table moves to 0.22.2 (v2026.9.24, schema 31); drop the source-archive
  hash pin, since a git install reports its own build provenance on the version
  line and rebuilds would invalidate a recorded hash.
- Document how the installer reaches the Hermes runtime: the published
  --print-runtime-command, the store Python plus hermes_bootstrap prelude, the
  install-directory recovery, and the Windows essentials the sanitized child
  environment must keep.
- Record the native re-validation performed for this promotion (read-only store
  snapshot, isolated-bundle install, and a real Claude Code to Hermes transfer
  with linked tool calls) and state plainly what was not verified: a live
  resume against a provider, native compaction after import, and WAL snapshots
  under a concurrent writer.
- Update the compatibility, specification, development, troubleshooting, and
  credential-free testing tables to the new pin, and move the native corpus
  fixture script to the new release, with an explicit note that its
  entry-point wrapper bytes are per-install and must be re-recorded.
…title

Hermes enforces idx_sessions_title_unique, a partial unique index over non-null
session titles, and its own importer treats the field as cosmetic: it wraps its
title write in a suppressed exception. The migrator inherited the raw refusal,
so re-migrating a conversation into a store that already held its title failed
the whole bundle with "UNIQUE constraint failed: sessions.title".

- Add hermes.title_taken(), hermes.without_title() and hermes.bundle_title():
  an advisory read-only lookup, a bundle whose only change is the cleared
  cosmetic field, and the title a bundle currently carries.
- Sample the target store before the import and drop the title when it is
  already taken, counting the omission as title:duplicate_in_target and
  reporting a target_title_omitted warning in the manifest instead of failing
  the migration over a cosmetic field.
- The import transaction still enforces the constraint itself: a title created
  by a concurrent writer in between fails closed, with no partial session.
- Covered by a credential-free round-trip test and a conversion-level test;
  verified end to end against a live 0.22.2 store, where the untitled session
  lists in the native picker with all 22 records intact.
The migration manifest carries the title:duplicate_in_target counter and the
target_title_omitted warning; the CLI's content-free stdout summary is built
before any installer runs for every harness, and a dry run writes no manifest.
The bundle crossed the process boundary as text: the caller encoded it with its
own locale codec while the store interpreter always decodes UTF-8, so on a
Windows console whose code page is not UTF-8 (no PYTHONUTF8/PYTHONIOENCODING set)
the install either died with UnicodeEncodeError or replaced every non-ASCII
character of the imported conversation — title and message bodies alike — with
U+FFFD inside the target store.

- Move the payload as bytes (`input=data`, no text mode) and have the helper read
  `sys.stdin.buffer`; decode the helper's stdout/stderr explicitly as UTF-8.
- Start the child interpreter with `-X utf8`, and read the launcher probes with
  an explicit UTF-8 encoding, so neither direction depends on the console code
  page.
- Extract `_run_helper()` so the transport has a single seam, covered by a
  credential-free regression test asserting bytes on stdin and the
  `sys.stdin.buffer` helper.
- Give the opt-in native trajectory accented title/body text (which would have
  caught this) and the Windows essentials in its child environment.
HERMES_HOME still wins, but the fallback is now the CLI's own platform default:
%LOCALAPPDATA%\hermes on Windows (or ~/AppData/Local/hermes without that
variable) and ~/.hermes elsewhere. A bare ~/.hermes on Windows named a directory
the CLI never writes to, so `--to hermes` and automatic root discovery missed
the real store unless the caller exported HERMES_HOME by hand — the CLI finds
its store with no environment at all, and now so does the migrator.

- Thread a `platform` override through hermes_home()/state_database_path() so the
  resolution is testable off-platform, matching the Devin adapter's pattern.
- Extend the resolution test and add one covering both platform defaults, the
  unset-LOCALAPPDATA fallback, blank HERMES_HOME, and the explicit root.
- Update the CLI reference, format doc, and changelog.
The adapter work in this branch (Hermes 0.22.2 / schema 31, the Windows runtime
and home resolution, the UTF-8 import bundle, the duplicate-title rule, and the
signed stat identity fix) plus the Antigravity Desktop Windows fixes are the
0.12.0 release, so stop reporting 0.11.0 from a checkout that behaves
differently.

- pyproject.toml, __init__.__version__, uv.lock, and the CLI assertion move to
  0.12.0.
- Fold the changelog's Unreleased section into "0.12.0 - 2026-10-16".
- Point the CLI reference and the specification at 0.12.0.

The website/ npm package keeps its own 0.11.0 version field; nothing reads it.
A real 73 MB transcript with two compactions failed with "Claude active graph
contains an ancestry cycle" even though its active graph is intact.  The
declaration is the same as the one the validator was written for, but the layout
under it moved: the boundary is now followed by attachment records, then the
compact summary, then the preserved tail — so the summary is no longer the
boundary's direct child — and preservedMessages also names ids from the
summarized branch that are no longer on the active path.

Validate the loop the way the declaration describes it rather than by the shape
one build happened to use: the tail must walk up to the summary and the summary
up to the boundary, with every hop already on the walked path; head/tail must
still appear in preservedMessages, and a boundary whose declaration does not
close over the walk still raises.  Requiring every declared id to be on the path
rejected readable transcripts, because the declared set spans branches the walk
never enters.

- Test the 2.1.300 layout (boundary -> attachment -> summary -> tail) and the
  still-fatal unclosed declaration.
The two boundaries in one real session carry different shapes under the same
declaration — one is the summary's direct parent, the other sits four hops below
it, with the attachments the CLI interleaves in between.  Attribute the variance
to the interleaved records rather than to a build change, and say what the
validator actually requires.
The store's CLI import refuses any one session above 5 MiB while its own web
import accepts 25 MiB, so a portable session carrying a pasted screenshot
(inline base64 image) was rejected as "session exceeds the import size limit"
although nothing about the bundle is invalid — a real 73 MB Claude transcript
produced a 6.88 MB bundle whose rows are 4.5 MB of user-pasted PNGs.

- Size the bundle exactly the way the store does (`measured_session_bytes`,
  ignoring the derived `timings`) and request `max(5 MiB, min(measured, 25 MiB))`
  from the import helper, which applies it to `SessionDB._IMPORT_MAX_SESSION_BYTES`
  before opening the database.  The vendor ceiling is the same one its web import
  uses, so the migrator never exceeds what Hermes accepts elsewhere.
- Report `target_import_budget_raised` (bundle bytes, default, applied budget) in
  the manifest whenever the budget moves, so a raised cap is never silent.
- Carry the session payload on `ParsedHermesBundle` instead of re-parsing it.
Both changes after 0.12.0 are backwards-compatible bug fixes: a readable Claude
transcript refused as an ancestry cycle, and a valid bundle refused by the
store's 5 MiB import cap. No CLI surface changed and the manifest schema stays
at 2; the only additions are a keyword-only `platform` override and the
`target_import_budget_raised` warning.

- pyproject.toml, __init__.__version__, uv.lock, and the CLI assertion move to
  0.12.1.
- Add the 0.12.1 changelog section covering both fixes.
- Point the CLI reference and the specification at 0.12.1.
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

🟢 Contributor eligibility requirements met for @gabszap.

  • 🟢 Account created: 2020-07-15; age: 2285 days (required: at least six calendar months).
  • 🟢 Contributions dated on or before 2026-09-17: 336 or more (required: at least 100).

Pull request authors must have a GitHub account at least six calendar months old and at least 100 GitHub contributions dated on or before the date one calendar month before the check.

See the contributor eligibility policy.

The new Hermes code carried five over-length lines and three files the pinned
ruff 0.16.10 would reformat, so the python matrix jobs failed at the first gate
before any test ran.
The adapter pin moved to 0.22.2 (v2026.9.24, d923a5eb) but the credential-free
gate still installed 0.20.6 (5fc308a7), so the exact-client check compared the
installed revision against the new pin and failed.

- install-native-test-clis.sh: tarball url, recorded revision and the expected
  archive hash for the 0.22.2 source tree.
- rename the native trajectory test and the corpus skip messages to the new
  release.
0.22.2 adds auto_archived, compression_overload_streak,
compression_recovery_deadline, created_source, timings, tool_names and
transport_profile to the official session export, plus absorbed_message_uids,
message_uid, tool_call_uid and tool_call_uids on each message. tool_names
carries the runtime tool catalog, so join system_prompt in the private-field
scrub.

Also scrub the JSON-escaped variant of the private cwd: on Windows the raw
cwd contains backslash separators, which json.dumps escapes in
tool_calls arguments, defeating the plain-text replace.
Recaptured portable-rich against the pinned v0.22.2 build (d923a5eb) on
linux-x86_64 through the loopback provider: schema 31 state.db, 8 messages,
2 tool calls/results, user image accepted (textual fallback), document,
audio and video rejected by the CLI image surface. The sanitizer run lists
schema-driven session and message scrubs plus runtime-ownership removal.

Route and source consumers now load the hermes fixture from
sources/hermes/0.22.2/portable-rich; the loopback context in
capture-hermes.py and test_hermes_native.py rises to 131072 tokens so the
0.22.2 client (which requires a 64k minimum) accepts the scenario.
gabszap and others added 12 commits October 16, 2026 23:03
…0.22.2

install-native-test-clis.sh synced the pinned Hermes tree with Python 3.13,
but the d923a5eb lockfile's supported environments require at least 3.14, so
the exact-client gate aborted before running any Hermes tests. And the
central catalog test fixture hand-wrote a schema_version 26 store, which the
0.22.2 adapter now refuses, so its shared-database assertions saw a root
error. Raise both: sync with 3.14 and stamp the fixture store as 31.
The exact-client gate pins Hermes Agent 0.22.2, but the installed tree is a
gitless tarball extraction, so the client resolves its own version report as
"Hermes Agent vunknown (2026.9.24)" and verify_client refuses it before any
Hermes probe runs. Recreate a minimal checkout in the install directory with
the pinned release tag so the runtime reports Hermes Agent v0.22.2 (the
emulated CI flow was validated on WSL: reported version, wrapper shape, and
uv sync with Python 3.14 all match the pinned environment).
Hermes 0.22.2 names a session that closes without a title, so the request
logged last after a resumed CLI run is the title-generation call; the test
read the wrong provider request and failed against the literal chat call.
Select the last chat request (skipping the "You name chat sessions" system
prompt) as the replay target. Validated on WSL against the emulated CI
install: the exact 0.22.2 client runs the resumed flow and the markers land
in the chat request as before.
Address xhluca's PR xhluca#17 findings with ordered portable tool-result blocks, explicit structured-error omissions and native validation before projection. Incorporate read-only v2 catalog discovery and Kilo legacy-schema separation, while retaining upstream contributor-policy checks.

Adapted from TheViniAlmeida's PR xhluca#17 commits 662976e and 2e3671d, which build on our focused PR xhluca#15. Credit and reuse history are documented in docs/opencode-v2.md.

Validation: Linux Python 3.12.3; focused v2/native/catalog/CLI/conversion/inspection/target gate 335 passed, 1 intentional private-store skip with stock OpenCode 2.0.23. Ruff, formatting, contributor-policy tests and whitespace checks pass. No full-suite or Windows-native claim.

Co-authored-by: TheViniAlmeida <eu@thevinialmeida.com>
…ection

The cherry-picked OpenCode v2 work landed its changelog bullet inside the
released 0.12.0 section, because its branch was cut from the v0.11.0-era file.
The work is not released yet, so it belongs at the top.
… unlink

The v2 OpenCode and Kilo install paths close the identity guard only in `finally`
while the rollback unlinks the manifest inside `except`: on Windows the unlink then
fails with WinError 32, and that cleanup error replaces the failure that triggered
the rollback — a real import into a live 2 GB store reported "o arquivo já está
sendo usado por outro processo" instead of naming what stopped finalization, while
the session was already inside the store.

Same ordering the other six install paths already use.
Routers such as 9router serve OpenCode's own catalogue under an `oc/` prefix while
keeping their own provider id, so a session imported from a Hermes install that ran
through the router recorded `9router/oc/deepseek-v4.1-flash-free` on its messages.
OpenCode resolves a turn from the reference the last message carries, so the imported
session kept routing through the router, and OpenCode's free tier refuses mirrored
traffic with "free tier can only be used from within OpenCode" - the session could not
be continued at all, while a fresh session using the same model worked.

A reference that names the target's own namespace now resolves to the target provider
(`opencode/deepseek-v4.1-flash-free`) for the session header and for every message.
References that do not claim the namespace are written unchanged.

- map `oc/<model>` references to the `opencode` provider when building the portable v2
  bundle, for the session header, assistant messages and compaction entries;
- cover both directions: the mirrored reference is rewritten, an unrelated provider
  reference is left alone.
@gabszap
gabszap merged commit 7ce7d27 into main Oct 17, 2026
24 checks passed
@gabszap gabszap mentioned this pull request Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants