Skip to content

server: V1 sessions created after migration.v1-v2 completes are never imported #50130

Open

Description

@Tr4cck

Description

OpenCode 2 performs a one-time V1 to V2 session import whose state lives in kv['migration.v1-v2']. Once that key is {"phase":"completed"}, any V1 session that appears in the same database afterwards is never imported: it stays in the legacy session / message / part tables, is absent from session_v2, GET /api/session/{id} returns 404, and desktop tabs that reference it show "the session this tab points to no longer exists on this server".

I hit this because I kept using a V1 client (1.18.31) alongside the V2 desktop app. The migration completed on 2026-09-17 (first V2 startup, 2.0.3); 11 sessions created with V1 after that were invisible in V2. The same first run also skipped 29 V1 sessions that already existed in the database. The data was never lost, but nothing surfaced the problem.

Plugins

oh-my-openagent@latest

OpenCode version

2.0.10 (desktop bundled CLI; migration.v1-v2 state was created by 2.0.3, 2.0.5 and 2.0.8 were also installed in between)

Steps to reproduce

  1. Create sessions with a V1 client (1.18.x) in ~/.local/share/opencode/opencode.db.
  2. Let a V2 build finish a startup so kv['migration.v1-v2'] becomes {"phase":"completed"} (here: 2.0.3, even though 29 V1 sessions were already present).
  3. Create new sessions with the V1 client against the same database.
  4. Open the V2 desktop app: the new sessions are missing. opencode api get /api/session/<id> returns 404, opencode api get /api/experimental/migration/v1 keeps returning {"status":"completed"}.

Expected behavior

The one-time import should only be marked completed after it has actually drained existing V1 history, or it should notice new V1 rows and run/resume instead of reporting completed forever. At minimum, unimported V1 sessions should be surfaced instead of making session tabs 404.

Actual behavior

  • kv row migration.v1-v2 = {"phase":"completed"}, created 2026-09-17 10:19:31.
  • 40 V1 sessions existed in session (versions 1.18.31 x11, 1.4.7 x20, 1.2.24 x9), 0 of them were copied to session_v2.
  • All 19901 rows in the event table still carry the default created = 0, suggesting the phase === undefined branch (clear events) did not do what it looks like it should on this V1 database.
  • Log: the client kept polling GET /api/session/ses_... and got 404 while the row existed in session.

Additional context

Recovery/workaround that brought everything back without data loss:

  1. Back up opencode.db.
  2. UPDATE kv SET value='{"phase":"sessions"}' WHERE key='migration.v1-v2';
  3. Boot any V2 server (e.g. opencode api --standalone get /api/experimental/migration/v1) so the importer resumes.
  4. It copied all 40 sessions and converted 2211 V1 messages into session_message correctly, then wrote {"phase":"completed"} again.

So the import/conversion logic itself works; the completion/"already completed" gate is the problem. In the shipped binary the state is read with an early if (state.phase === "completed") return, and loose_psylocke already knows how to detect V1-only history (Previous V2 database contains V1-only session history), but there is no equivalent check or re-arm for V1 sessions newer than the last completed import.

OS

Arch Linux, kernel 7.2.6-arch2-1 (x86_64)

Terminal

OpenCode desktop app 2.0.10 (Electron); same behavior visible from the CLI (opencode api).

Activity

  1. remorses commented on Oct 6, 2026

    @remorses
    Contributor

    Description
    Confirmed on 2.0.19 while moving Kimaki from V1 to V2. The V1 history still exists, but V2 returns SessionNotFoundError.
    My migration completed on September 12. I found 1,406 sessions created afterwards missing from session_v2. One affected session, created October 6, still had 73 messages and 273 parts in the V1 tables.
    Plugins
    Kimaki and kimaki-anthropic, plus the built-in plugins.
    OpenCode version
    2.0.19.
    Steps to reproduce

    1. Finish the first V1-to-V2 migration in a shared OpenCode data directory.
    2. Create another session with a V1 client.
    3. Call V2 session.get for that ID. It returns SessionNotFoundError although the legacy session row exists.
    4. The migration state remains {"phase":"completed"}. In packages/core/src/database/v1-migration.bun.ts, run() returns immediately for that state.
      Screenshot and/or share link
      I recovered the 1,406 late sessions after a verified SQLite backup, using transformSession from the published @opencode/core@2.0.19. Each import kept its session ID and used a separate transaction. I removed only the obsolete V1 events for the imported aggregates and set their converted sequence watermarks.
      The reported session now returns 81 converted history messages through the running V2 server. Backup comparisons found no deleted existing V2 sessions, messages, or events. I did not reset the global migration marker.
      Operating System
      macOS 26.3.1, Apple Silicon.
      Terminal
      Kimaki CLI and Discord.
  2. janeklb commented on Oct 12, 2026

    @janeklb

    Confirmed on macOS with OpenCode CLI 2.0.27 after previously using OpenCode Desktop alongside a V1 terminal installation.

    Likely sequence

    I installed Desktop around September 25 and believe it was a V2 build. I continued using V1 in the terminal afterward. On October 12, I removed the Desktop application and upgraded the terminal installation from 1.19.0 to 2.0.27.

    A verified pre-upgrade database backup already contains:

    • kv['migration.v1-v2'] = {"phase":"completed"}
    • Marker created: September 25, 2026, 22:02:07 local time
    • Marker updated: September 25, 2026, 22:02:24 local time

    I have not independently verified which Desktop build wrote the marker.

    Read-only audit results

    Comparing legacy session records with session_v2 by session ID found:

    • 69 V1 sessions absent from V2 across 10 project directories
    • 42 top-level sessions and 27 child sessions
    • 4,069 message rows retained in the V1 message table
    • All 69 sessions were created September 28–October 12, after the existing completion marker
    • None of those sessions is archived in V1

    One representative top-level session has 300 V1 message rows and 1,292 V1 part rows, but no session_v2 record and no session_message rows. Its dates and V1 row counts match the pre-upgrade backup.

    A separate check found no sessions created before the completion marker and already present in V2 with V1 message rows created after that marker. This does not establish complete content equality or rule out edits to older message rows.

    Existing-backlog recovery question

    PR #54272 appears to address future V1 additions while preserving existing V2 sessions. However, its current handling of an existing completed marker without a watermark records the current minimum legacy session ID and returns. As I understand it, that leaves an already-stranded backlog like these 69 sessions unconverted.

    Could maintainers provide a supported recovery command or migration for this existing-backlog case?

    The recovery needs to preserve existing V2 sessions and messages, handle parent/child sessions, and avoid restoring sessions intentionally deleted in V2. A preview or explicit session-ID selection would help resolve that ambiguity.

    I have kept the backups and have not reset or changed the migration marker, rerun full conversion, or restored an old database over the active database. I would prefer not to reset the marker because the 2.0.27 converter deletes existing V2 message rows before rebuilding them from V1.

    I can provide additional anonymized metadata from read-only queries.

  3. 4sDriven commented on Oct 13, 2026

    @4sDriven
    Contributor

    Thanks for the audit — you read it right, I wrote #54272.

    The conservative path was because a migrated-then-deleted session and a never-migrated one look identical in the DB: V2 deletes hard and never touches the V1 tables, so importing everything missing would resurrect sessions people deliberately deleted.

    Your backlog is still recoverable. The migration marker in kv is timestamped, and it gets rewritten as the migration walks sessions, so it lands on "finished scanning". Compare that with each legacy session's created time: before it and missing from V2 → migrated then deleted, leave alone; after it and missing → genuinely stranded, safe to import. Using the migration start instead is safer still, since re-importing is a no-op and anything V2 deleted predates the migration, so nothing resurrects. Only catch: read that timestamp before a version that stores a watermark rewrites it.

    If the maintainers are fine with it, I'll derive the boundary that way for existing completed states and add a dry-run listing what would be imported vs skipped.

    What does kv show for migration.v1-v2 on your DB? I'd like to check the split against your data.

  4. visuarte commented on Oct 14, 2026

    @visuarte

    Confirmed duplicate — with a fully timestamped reproduction and a non-destructive recovery.

    Environment: macOS, opencode 2.0.28 (desktop app; the homebrew CLI on the same machine was 2.0.25).

    Timeline recovered from the DB

    • kv['migration.v1-v2'] = {"phase":"completed"}; time_created 2026-10-11 19:18:18, time_updated 19:18:22 (local). The marker has no watermark on 2.0.28, i.e. it predates fix(core): import V1 sessions created after migration completes #54272.
    • Last message that reached session_message for a session that did migrate: 19:18:20 — the marker lands on "finished scanning", as described in this thread.
    • First stranded session created 20:06:05, last one 2026-10-13 20:09. 9 sessions / 2,381 messages, all intact in the legacy tables.
    • All 9 sort below the lowest already-migrated session id (9/9) — exactly the condition the watermark check in fix(core): import V1 sessions created after migration completes #54272 is meant to catch.
    • "V1 session ids descend with time" holds on this database: 0 violations across 89 sessions.

    The "missing messages" case here is the same latch, not #53971

    For a session created before the marker (15:37) that did migrate, the legacy tables hold 81 messages written after 19:18:20 (up to 19:56:03) that never reached session_message. They are a contiguous tail — 0 of the 81 predate the last imported message — so in this instance "missing messages" is fully explained by the latch, not by rows dropped in schema validation. Legacy is a strict superset of V2 for that session (293 vs 213 messages). Worth checking whether #53971's path is a separate mechanism.

    Non-destructive recovery (the kv marker was never touched)

    Everything was recovered through the official path, so no marker reset (re: the warning in #49641):

    1. sqlite3 .backup of opencode.db first (pragma integrity_check → ok).
    2. Rebuild each stranded session in the official export format from the legacy tables (session/message/part) and run opencode session import <file> --directory <dir>. It preserves info.id and info.time.created, so the sessions come back as themselves.

    Traps in the import schema, for anyone else attempting this:

    • info.projectID is required — take it from session.project_id.
    • content parts need time.created for reasoning/tool (legacy stores time.start/time.end).
    • a tool state.error must be structured ({"type":"tool.execution","message":"…"}); legacy stores a plain string.
    • message ids are global primary keys, so re-importing a session that already exists fails with UNIQUE constraint failed: session_message.id (surfaced as HTTP 500).
    • the import stamps time_updated = now, so every recovered session shows up as "today" in the list until you restore it from the legacy row.
    1. Fidelity measured against the official export of a dual-present session: 159/212 messages byte-identical; the remainder differ only by ±1 ms on time.completed and cosmetic metadata (snapshot.files, state.attachments). Our reconstruction comes out a superset because of the 81-message tail.

    Selection criterion used

    Matches the safe rule described above: all 9 were created after the marker and missing from session_v2; none predates the marker, so no migrated-then-deleted session was resurrected.

  5. desaiamogh commented on Oct 25, 2026

    @desaiamogh

    Confirming the same latch on Windows, with the V1 desktop app (bundled server 1.19.4) running alongside the V2 CLI (2.1.4): our kv marker completed Oct 14, and 5 sessions created in the V1 desktop afterwards stayed in the legacy tables only — GET /api/session/{id} 404, stale desktop tabs — plus a ~380-message legacy-only tail on one already-migrated session. Details in #58840 (closing as a duplicate of this issue).

    Two notes that may help #54272:

    1. The importer's conversion semantics appear stable across the V1 versions we saw (1.16–1.19.4): I rebuilt its full legacy→session_message transformation from our DB and it reproduces the official importer's output exactly (parsed-JSON equality) on 650/650 already-imported messages. So the PR's import output can be diffed against existing session_message rows as a regression check.
    2. The full verified field mapping (including the undocumented state.error/state.content shapes that make hand-rebuilt rows fail the decoder) is posted on session: migrated session missing messages written after V1→V2 migration; export truncated #57000, which tracks the truncated-transcript half of this bug.
  6. daurge-fff commented on Oct 28, 2026

    @daurge-fff

    OpenCode V1 → V2: Missing sessions after migration - diagnosis and successful recovery

    I am sharing a detailed report of a V1 → V2 migration problem I encountered on macOS, including database diagnostics, failed recovery attempts, the final outcome, and recommendations for making recovery safer and easier for other users.

    The goal is to document a practical troubleshooting workflow for users whose conversation history appears to be missing after upgrading to V2.

    Important distinction: I successfully recovered two sessions, but I have not isolated the exact operation that ultimately caused the recovery to succeed. The commands below distinguish verified diagnostic steps from recovery experiments that require additional safeguards.


    1. Environment

    • Operating system: macOS

    • Architecture: Apple Silicon (ARM64)

    • Application: OpenCode Desktop 2.2.0

    • Legacy session versions: 1.19.3 and 1.19.0

    • SQLite database: ~/.local/share/opencode/opencode.db

    • Project directory: <project>

    The Desktop application's bundled CLI was located at:

    "$HOME/Library/Application Support/ai.opencode.desktop/cli/2.2.0-arm64/opencode-cli"

    The exact CLI location may differ between versions and installations.

    2. Summary of the problem

    After migration from V1 to V2, two important conversations were initially missing from the V2 interface.

    However, the original session records and their associated messages were still present in the legacy SQLite tables.

    The affected sessions were:

    Session ID | Title | Legacy messages -- | -- | -- ses_e81b4289effevUUK53fcEktIu4 | Title 1 | 431 ses_ec563a93bffeAK3KWn03G6ZFYq | Title 2 | 200

    The original session IDs and titles were preserved.

    The V2 message counts were one higher than the legacy counts for both sessions. I have not established the reason for this difference, so I am not claiming that the additional records are necessarily errors or that the counts should always match exactly.

    The important outcome is that both conversations became available again and their histories were recovered.

    I cannot yet identify the single operation that definitively caused the recovery to succeed. The successful outcome followed database inspection, failed JSON imports, migration investigation, and further recovery attempts.

    I am documenting the entire sequence to help others investigate the same failure without assuming that one workaround is appropriate for every database.

    13. What would make recovery much easier for users?

    I believe OpenCode could improve this workflow in several ways.

    A. Detect incomplete migrations

    Before marking a migration as complete, verify that legacy sessions have either been imported or explicitly reported as skipped.

    B. Provide a migration audit command

    A diagnostic command could report:

    • Legacy sessions detected.

    • Sessions already present in V2.

    • Sessions missing from V2.

    • Messages that failed conversion.

    • Sessions skipped because of conflicts.

    • Any inconsistencies that need manual investigation.

    Ideally, it would not expose conversation content or secrets.

    C. Provide a dry-run recovery mode

    Users should be able to see which sessions would be imported, skipped, or potentially duplicated before modifying the database.

    D. Make migration resumable and idempotent

    Rerunning migration should safely process missing records without duplicating or damaging existing V2 sessions.

    E. Provide a supported session recovery command

    A targeted recovery command would be safer than manually changing migration markers or inserting rows into SQLite.

    F. Validate imports

    If session import rejects an export, the CLI should provide more specific information about the invalid field or structure rather than returning only Bad Request.

    G. Document a safe recovery workflow

    The official documentation should explain how to:

    1. Back up the database.

    2. Detect missing sessions.

    3. Inspect the migration state.

    4. Attempt supported recovery.

    5. Validate the resulting database.

    6. Preserve existing V2-native conversations.

    14. Related reports and further investigation

    These issues are relevant to the broader V1 → V2 migration problem:

    The precise behavior may differ by OpenCode version, so recovery instructions should be verified against the version being used.

    Conclusion

    The original session data was still present in the legacy database, and I was able to recover both missing conversations.

    This experience suggests that a migration marked as completed should not automatically be treated as proof that all conversation history is accessible in V2.

    I hope this report helps other users diagnose missing sessions, avoid unnecessary data loss, and recover their history safely. I would also appreciate guidance from the maintainers on the officially supported recovery procedure for sessions that remain in the legacy tables after migration.

  7. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed report. I reproduced this with a V1 1.18.31 server and V2 sharing one opencode.db:

    1. Create a session with V1.
    2. Start V2. The migration reports completed and that session is imported.
    3. Create another session with V1.
    4. Start V2 again. It still reports {"status":"completed"}, the new session is in the legacy session table but not in session_v2, and GET /api/session/<id> returns 404 SessionNotFoundError.

    This happens on 2.0.10 and on the latest release, 2.2.0. I couldn't run the latest dev (62ae125) from source, but its migration code still stops early once the state is completed. Open PR #54272 is aimed at exactly this case. The script below reproduces it.

    I didn't reproduce the other part, where the first run skipped the 29 V1 sessions that already existed: in my test the existing session was imported. #59517 may be related to that part.

    Related: #57080 (PR for V1 messages written after the migration completed), #57000, #58846.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #50130: V1 sessions created after migration.v1-v2 is "completed" are never imported.
    # Needs: V1 binary (opencode-ai@1.18.31) as $V1, V2 command as $V2 (default: `opencode` from @opencode/cli).
    set -u
    V1=${V1:-$HOME/.bun/install/global/node_modules/opencode-linux-x64/bin/opencode}
    V2=${V2:-opencode}
    ROOT=$(mktemp -d /tmp/opencode/repro50130.XXXX)
    export XDG_DATA_HOME=$ROOT/data XDG_CONFIG_HOME=$ROOT/config XDG_STATE_HOME=$ROOT/state XDG_CACHE_HOME=$ROOT/cache HOME=$ROOT/home
    mkdir -p $ROOT/home $ROOT/proj && cd $ROOT/proj && git init -q && git -c user.email=a@b -c user.name=a commit -q --allow-empty -m init
    DB=$XDG_DATA_HOME/opencode/opencode.db
    
    v1_session() { # create one V1 session through a V1 server
      local log=$ROOT/v1.log; : > $log
      $V1 serve --port 0 > $log 2>&1 & local pid=$!
      for i in $(seq 1 120); do grep -q listening $log && break; sleep 0.5; done
      local url=$(grep -o 'http://[0-9.:]*' $log | head -1)
      curl -s -X POST $url/session -H 'content-type: application/json' -d "{\"title\":\"$1\"}" | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])'
      kill $pid; wait $pid 2>/dev/null
    }
    q() { python3 -c "import sqlite3,sys;[print(*r,sep=\"|\") for r in sqlite3.connect(sys.argv[1]).execute(sys.argv[2])]" "$DB" "$1"; }
    
    A=$(v1_session "before-migration"); echo "V1 session A (before V2 ran): $A"
    echo "--- first V2 start"; $V2 api --standalone get /api/experimental/migration/v1; echo
    echo "kv: $(q "select value from kv where key='migration.v1-v2'")"
    echo "session_v2 contains A? $(q "select count(*) from session_v2 where id='$A'")"
    
    B=$(v1_session "after-migration"); echo "V1 session B (created after migration completed): $B"
    echo "--- second V2 start"; $V2 api --standalone get /api/experimental/migration/v1; echo
    echo "kv: $(q "select value from kv where key='migration.v1-v2'")"
    echo "legacy session has B? $(q "select count(*) from session where id='$B'")"
    echo "session_v2 contains B? $(q "select count(*) from session_v2 where id='$B'")"
    echo "--- GET /api/session/$B"; $V2 api --standalone get /api/session/$B; echo

    Run it: bash repro.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions