Skip to content

feat(opencode): support v2 transfers and catalog discovery - #17

Merged
xhluca merged 2 commits into
xhluca:mainfrom
TheViniAlmeida:fix/opencode-v2-transfer
Oct 19, 2026
Merged

xhluca merged 2 commits into
xhluca:mainfrom
TheViniAlmeida:fix/opencode-v2-transfer

Conversation

@TheViniAlmeida

@TheViniAlmeida TheViniAlmeida commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

OpenCode 2.0 sessions cannot be transferred by the existing adapter: the CLI moved to session import/export --standalone, messages are flat, and catalog metadata lives in session_v2. This change selects the public transfer schema and makes v2 sessions discoverable by the catalog.

Builds on the focused implementation from PR #15, head 0f32c8f377f9667fac8be9325ca3065314b9f0ef, by @tudstudent. The adaptation adds catalog discovery and separates Kilo's legacy contract.

  • Select legacy 1.17.20 or the 2.0 schema family from the installed CLI. Other native schema series fail closed; unvalidated exact 2.0 releases/custom builds receive warnings.
  • Preserve ordered text, linked tool inputs/results, inline images and readable compaction. Completed tool results retain caption/image ordering and text segmentation; legacy OpenCode/Kilo flattening is explicitly counted.
  • Preserve structured tool-error messages and count omitted type, status and response separately, without copying diagnostic bodies into opaque metadata.
  • Validate session fields, tool states and all accepted control variants before projection or defaults, including nested fields and absent-versus-null optionals.
  • Import through the public CLI, reject identity collisions and confirm the imported ID by public export. No direct database writes.
  • Read session_v2 and legacy session in one read-only catalog transaction; prefer v2 metadata for duplicate IDs and support nullable v2 titles. Kilo retains its legacy schema.
  • Add an exact stock 2.0.23 native CI variant with no-skip enforcement and source/target route variants. The review regression module is included in this gate.

Validation on Linux with stock OpenCode 2.0.22 and Python 3.14.4: 1,744 passed, 55 skipped in an isolated HOME/XDG environment. The four minimized review reproductions failed on the original head and now pass. The new regression module contains 125 cases, including five cold native import/export or rejection checks. The complete v2 unit/native modules pass 185 tests with zero skips. Native coverage includes dry run, collisions, catalog discovery, cold reopening and continuation under the same session ID with a deterministic localhost provider.

Independent review approved the final diff after 542 tests passed with zero skips, explicit ordered-payload checks in eight capable targets and additional positive/negative loss-oracle probes. Ruff lint/formatting, shell syntax, whitespace checks and wheel/sdist build pass. A smoke test importing the built wheel directly passes all five new stock-2.0.22 native cases, without installing dependencies or changing the shared installation.

The 55 skips are unavailable historical exact clients and explicit opt-in private-store gates.

Native stock 2.0.23, Windows and unavailable historical exact clients have not been executed locally. The added CI target for stock 2.0.23 still needs maintainer approval to run; contributor eligibility is not a conversion test. All local native checks use synthetic fixtures and isolated HOME/XDG profiles with deterministic localhost providers, without real sessions or account credentials.

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

🟢 Contributor eligibility requirements met for @TheViniAlmeida.

  • 🟢 Account created: 2016-08-27; age: 3696 days (required: at least six calendar months).
  • 🟢 Contributions dated on or before 2026-09-10: 5930 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.

@xhluca xhluca left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested for the OpenCode v2 adapter. I reviewed only #17, leaving the existing reviews on #6/#7/#8 unchanged.

Reviewed head 662976e9e98e8b44040945b6afb9222a48cf2606 against current main a3b0fae4a38bbe0c0c89876f2f2ee6ee0ad154c4; the clean prospective merge tree is af869174829054694802991d2309313c543cf742.

Three P2 findings are detailed inline:

  1. A supported tool result [text A, image A, text B, image B] becomes [text A + "\\n" + text B, image A, image B], with no omission reported. Preserve ordered portable blocks for targets that support them; explicitly account for degradation on targets that do not.
  2. Structured tool errors lose their original type, HTTP status and response body, again with no omission reported. Preserve portable diagnostic details where possible, or count/document the omitted fields.
  3. Validation happens against a lossy projection: a system record missing its required text, and an array-valued session title, pass validation although stock 2.0.23 rejects both. Validate native required fields before projection/defaulting, including records intentionally omitted from portable history.

These are newly introduced v2 representation/validation gaps, not accusations of an observed production overwrite or security exploit. I independently imported/exported the valid tool cases with stock 2.0.23 before testing migration; the invalid cases fail the stock importer with SchemaError: Missing key at ["messages"][6]["text"] and SchemaError: Expected string at ["info"]["title"].

Evidence so far (Linux x86_64, Python 3.11.14, isolated HOME/environment, no credentials or external provider traffic):

  • Current-main full suite: 1,519 passed / 55 skipped.
  • Prospective-merge full suite without supplied native binaries: 1,616 passed / 58 skipped; skips are not counted as passes.
  • Prospective-merge full suite with supplied stock OpenCode v2/legacy/Kilo binaries: 1,624 passed / 50 skipped. An initial Kilo failure was caused by the review sandbox mounting the fresh package read-only while an existing test sets HOME inside that package; it also reproduced on main and passed after correcting the isolated mount, then the entire suite passed.
  • All OpenCode v2 tests with stock 2.0.23: 60 passed / 0 skipped, including public import/export, catalog, collisions and cold continuation.
  • Legacy OpenCode/Kilo corpus gates: 6 passed / 0 skipped; four additional strict baseline/merge logical comparisons match, with independently checked tool/media/loss invariants.
  • Independent native-owned source creation, migration into other adapters, v2 cold continuation/prefix preservation and failure cleanup passed.
  • Public CLI native import auto-selects v2 without a version override, then virtual-source transfer exports into Pi; this and four bounded safety-limit checks: 5 passed / 0 skipped.
  • Ruff lint/format and 12 contributor-policy unit tests passed.
  • The four minimized tests below all fail on this tree, representing the three root causes above. Add them as permanent regression coverage with the fixes.

At inspection, GitHub reported only the contributor-eligibility check, not the Python/native CI matrix. That eligibility check passes, but is not conversion-test evidence. The affected CI gates should run on the corrected head before merging. Windows native acceptance and Python 3.12/3.13 are not established by these local runs.

Runnable minimized regression tests

Save this as /tmp/test_pr17_repro.py, then run from the repository root:

PYTHONPATH=src python -m pytest -q /tmp/test_pr17_repro.py
"""Four minimized regressions representing three PR17 root causes."""
import json
from pathlib import Path

import pytest
from session_migrate.conversion import ConversionOptions, convert_session, load_session
from session_migrate.errors import SessionMigrateError
from session_migrate.formats import opencode
from session_migrate.model import AgentFormat, TargetFormat


def source_bundle(tmp_path):
    source = load_session(Path("tests/fixtures/codex-0.144.4/basic.jsonl"), AgentFormat.CODEX)
    return json.loads(convert_session(source, ConversionOptions(TargetFormat.OPENCODE, target_cli_version="2.0.23", cwd=tmp_path, model_provider="fixture", model="test")).native_bytes)


def tool(bundle):
    return next(c for m in bundle["messages"] if m["type"] == "assistant" for c in m["content"] if c["type"] == "tool")


def rewrite(bundle, tmp_path):
    path = tmp_path / "source-v2.json"
    path.write_text(json.dumps(bundle))
    source = opencode.parse_session(path)
    artifact = convert_session(source, ConversionOptions(TargetFormat.OPENCODE, target_cli_version="2.0.23", cwd=tmp_path, model_provider="fixture", model="test"))
    return json.loads(artifact.native_bytes), artifact.dropped


def test_preserve_tool_block_order(tmp_path):
    bundle = source_bundle(tmp_path)
    image = tool(bundle)["state"]["content"][1]
    blocks = [{"type": "text", "text": "First image: copper"}, image, {"type": "text", "text": "Second image: blue"}, image]
    tool(bundle)["state"]["content"] = blocks
    rewritten, losses = rewrite(bundle, tmp_path)
    assert tool(rewritten)["state"]["content"] == blocks, {"actual": tool(rewritten)["state"]["content"], "losses": losses}


def test_structured_error_preserved_or_counted(tmp_path):
    bundle = source_bundle(tmp_path)
    error = {"type": "ConflictError", "message": "same error text", "status": 409, "response": {"body": "specific diagnostic"}}
    tool(bundle)["state"] = {"status": "error", "input": {"command": "true"}, "error": error}
    rewritten, losses = rewrite(bundle, tmp_path)
    assert tool(rewritten)["state"]["error"] == error or any("error" in key for key in losses), {"actual": tool(rewritten)["state"]["error"], "losses": losses}


@pytest.mark.parametrize("mutation", ["missing-system-text", "array-title"])
def test_malformed_native_fields_rejected_before_projection(tmp_path, mutation):
    bundle = source_bundle(tmp_path)
    if mutation == "array-title":
        bundle["info"]["title"] = []
    else:
        bundle["messages"].append({"id": "msg_invalid_control", "type": "system", "time": {"created": bundle["info"]["time"]["updated"]}})
    with pytest.raises(SessionMigrateError):
        opencode.validate_native_bytes(json.dumps(bundle).encode(), bundle["info"]["id"])

— Codex (OpenAI coding agent, GPT-6.1-sol)

raise SessionMigrateError("OpenCode v2 completed tool needs content")
state["output"] = "\n".join(
b["text"] for b in blocks if b.get("type") == "text"
)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve ordered tool-result content instead of splitting text from images

For a stock-2.0.23-accepted completed tool result with content [text A, image A, text B, image B], this join plus the separate attachment collection rewrites the history to [text A + "\n" + text B, image A, image B]. A v2 → portable → v2 roundtrip reproduces that change with dropped == {}. It loses which caption/context preceded which image even on a target that supports ordered content. Preserve the original ordered content_blocks in the portable representation and on capable targets; account for any unavoidable ordering degradation on legacy targets. Please add a mixed text/image sequence test, not only assertions over image URIs.

— Codex (OpenAI coding agent, GPT-6.1-sol)

error = state.get("error")
if not isinstance(error, dict) or not isinstance(error.get("message"), str):
raise SessionMigrateError("OpenCode v2 tool error is invalid")
state["error"] = error["message"]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Account for structured tool-error fields erased by projection

Stock 2.0.23 accepts and cold-exports an error such as {type: "ConflictError", message: "same error text", status: 409, response: {body: "specific diagnostic"}}. Reducing it here to its message, then rebuilding it in from_legacy, produces {type: "ToolError", message: "same error text"} with no error-related omission in loss_events/the manifest. This silently removes diagnostic information from migrated failed tool calls. Preserve those fields where the destination supports them, or explicitly count and document their loss; add a native-accepted source → portable → target regression.

— Codex (OpenAI coding agent, GPT-6.1-sol)

kind == "compaction" and m.get("model") is not None
):
ref(m.get("model"))
if kind == "user":

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Validate native fields on every accepted message type before discarding them

A v2 system record containing only id, type and time passes validate_native_bytes, because this function has no type-specific validation for it and _to_legacy drops it before the legacy validator runs. Stock 2.0.23 rejects the same record for missing text. The same projection-before-validation gap makes info.title = [] pass after the fallback replaces it with "Imported session", while stock OpenCode rejects the array. Please validate the original session fields and required fields of accepted control variants before projection/defaulting, with parser and dry-run/validation regressions. Intentionally unsupported portable content may be counted as an omission, but malformed native records should not be silently repaired into accepted input.

— Codex (OpenAI coding agent, GPT-6.1-sol)

tudstudent added a commit to tudstudent/session-migrate that referenced this pull request Oct 12, 2026
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>
gabszap pushed a commit to gabszap/session-migrate that referenced this pull request Oct 16, 2026
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>
gabszap pushed a commit to gabszap/session-migrate that referenced this pull request Oct 16, 2026
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>
gabszap pushed a commit to gabszap/session-migrate that referenced this pull request Oct 16, 2026
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>
gabszap pushed a commit to gabszap/session-migrate that referenced this pull request Oct 17, 2026
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>
gabszap pushed a commit to gabszap/session-migrate that referenced this pull request Oct 17, 2026
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>

@xhluca xhluca left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved for the tested scope on head 2e3671dcf402159bcb456380409a103cfaaf19cf. All three findings from my previous review are addressed.

I re-read the updated implementation/tests/docs and tested the clean merge with current main a3b0fae4a38bbe0c0c89876f2f2ee6ee0ad154c4, tree e7ac9c7e709cf4913a0c83e875d4774f1a145cfc. GitHub's prospective merge has that same tree; head/main were rechecked before this approval.

Fix verification:

  • The same four minimized reproductions, with unchanged assertions, still fail on the original reviewed tree and now all pass.
  • V2 source parsing and output preserve the ordered tool content, including captions/interleaved images and text segmentation. Legacy OpenCode/Kilo degradation is explicitly counted.
  • Structured tool-error type/status/response omissions have separate loss counters and safe provenance; diagnostic response bodies do not leak into the portable events or manifest.
  • Required/native optional/control fields are checked before projection/defaults, including the previously accepted malformed system/title cases.

Fresh local evidence (Linux x86_64, Python 3.11.14, credential-free isolated HOME/environment, loopback providers only):

  • Full prospective-merge suite with exact OpenCode v2/legacy/Kilo binaries: 1,749 passed, 50 skipped.
  • V2 unit/review/native modules with stock 2.0.23: 185 passed, zero skipped.
  • Retained independent reproductions, public CLI auto-selection/virtual transfer, lowered resource caps, native-owned source creation, cold continuation/prefix preservation, collisions and failure cleanup: 23 passed.
  • New targeted payload/error/native-positive checks: 53 passing cases, including four nonempty block-order patterns across eight capable targets using real committed PNG bytes, twelve error-field combinations, native acceptance of valid workspace/revert metadata and separate empty-block contracts.
  • Four strict main/merge legacy OpenCode/Kilo comparisons are healthy and identical; lint, formatting and 12 contributor-policy unit tests pass.
  • All 23 CI jobs pass on this head, including Python 3.11/3.12/3.13 and all 19 native-client variants with their no-skip gates.

I separately investigated exploratory failures rather than treating them as successes: Kimi/Vibe artifacts require their native multi-file installers, not parsing the transfer envelope as a session file. Existing older writers normalize empty text blocks; a matched Codex-source probe confirms identical payloads/loss counters on main and this merge for seven targets. V2/Vibe retain empty blocks exactly. Those inherited per-target rules are not a newly introduced ordering regression; the original failing outputs and baseline comparison remain recorded.

No new blocking finding. Skipped unrelated local native gates are not counted as passes; their CI results are separate evidence. No Windows native execution, future/custom 2.0 patch compatibility, real-account migration, or blanket regression-free guarantee is claimed. No merge performed.

— Codex (OpenAI coding agent, GPT-6.1-sol)

xhluca added a commit that referenced this pull request Oct 19, 2026
Merge the exact adversarially approved head after combined integration validation with PR #17. Scope and platform limits remain as documented in the approval.

— Codex (OpenAI coding agent, GPT-6.1-sol)
@xhluca
xhluca merged commit 3c5a0ad into xhluca:main Oct 19, 2026
24 checks passed
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.

2 participants