Skip to content

fix(opencode2): accept v1 stores migrated to OpenCode 2 - #477

Merged
ualtinok merged 1 commit into
cortexkit:masterfrom
qoole:fix/opencode2-migrated-store-generation
Sep 20, 2026
Merged

ualtinok merged 1 commit into
cortexkit:masterfrom
qoole:fix/opencode2-migrated-store-generation

Conversation

@qoole

@qoole qoole commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Problem

On an OpenCode 2 host whose store was migrated from 1.18.x, 0.42.6 refuses the store:

[magic-context] v2 refuseIfUnsafe warn: OpenCode store generation mismatch at …/opencode.db: expected v2, found v1; refusing generation-specific database access

After that, every turn in every session is interrupted.

When OpenCode 2 migrates a 1.18.x store, it keeps the v1 message and part tables beside its own schema. detectOpenCodeStoreGeneration() calls any store with message + part v1. That's correct for real 1.18.x stores, which also ship session_message, as the existing fixture pins. So V2StoreReader's assertOpenCodeStoreGeneration(db, "v2") throws on every migrated install.

doctor's dangling-boundary check has the same blind spot. On a migrated store it reads message, which is frozen at the migration point, and reports every boundary written since the upgrade as dangling.

Evidence for the marker

session_v2 is written only by an OpenCode 2 host:

  • 1.18.31: the binary contains no reference to session_v2, session_inbox or session_pending.
  • 2.0.7: it creates all three, in both a fresh data directory and a migrated store.

Table lists as observed:

  • fresh 2.0.7: session_message, session_v2, session_inbox, session_pending, … and no message/part;
  • migrated 2.0.7: the fresh set plus the kept message, part, session, todo, …;
  • 1.18.30: the existing fixture, which has no session_v2.

On a real migrated store, 6,529 of its 6,885 v1 message ids are also in session_message under the same ids. So session_message is the right table on a migrated host.

Fix

  • The v2 assertion: it now also passes when the store has session_message and session_v2. detectOpenCodeStoreGeneration() is unchanged, so a 1.18.x store is still v1 and still refused for v2 readers, and v1 assertions behave exactly as before.

  • The doctor boundary check: it now takes the host generation that doctor already resolves from the active install. It falls back to detection when there is none, including for Desktop installs, which report no version.

    Table presence can't settle this alone, because a store can hold both generations either way: migrated by 2.x, or used by 1.x again after an upgrade (session_v2 stays behind while 1.x writes to message).

Tests

  • opencode-db-path.test.ts:
    • a migrated store is accepted for v2 and for v1;
    • a fresh 2.0.7 store is v2-only;
    • the existing 1.18.30 fixture still refuses v2.
  • doctor-compartment-boundaries.test.ts adds two stores:
    • a migrated store read as v2, whose legacy message table holds only one id;
    • a downgraded store read as v1, whose session_message holds only one id.

What each test guards, checked by mutation:

  • The migrated cases fail without the fix.
  • Choosing the doctor table from session_v2 instead of from the host fails the downgraded case.
  • Dropping the session_v2 requirement fails the 1.18.30 fixture.

Results:

  • bun test src/commands/ (cli): 164 pass.
  • Lint and typecheck pass in both packages.
  • Full plugin bun test --parallel: 4947 pass, 1 fail. The failure is createCtxSearchTools > explains why commit search is unavailable for a non-repository project, which fails the same way on master in my environment.

Out of scope, but worth knowing

doctor migrate (packages/cli/src/commands/migrate.ts) asserts v1 and reads message/part. On a migrated OpenCode 2 store that passes, and it exports only the history from before the upgrade. This PR doesn't change it (the v1 assertion is untouched). It probably wants to either read session_message for OpenCode 2 sources or refuse them explicitly.

Related: #476 (dropped tool parts leak into OpenCode 2 requests) and #475 (migration 85 relabels real OpenCode 2 rows).


Summary by cubic

Fixes OpenCode 2 rejecting stores migrated from 1.18.x, which previously interrupted every turn on those installs. The v2 reader now accepts migrated stores, and doctor's dangling-boundary check reads the live host's tables instead of the frozen v1 ones.

  • The v2 assertion now accepts any store with session_message and session_v2, even if it also keeps the v1 message/part tables; session_v2 is created only by OpenCode 2 hosts.
  • Generation detection is unchanged, so 1.18.x stores are still classified as v1 and v1 assertions still refuse v2 stores.
  • Doctor's boundary check now prefers the host generation resolved from the active install and falls back to schema detection only when no version is reported, as on Desktop.
  • doctor migrate still reads the v1 message/part tables on migrated stores, so it exports only pre-migration history; changing that is left for a follow-up.

Written for commit 16da14c. Summary will update on new commits.

Review in cubic

RetriggerConfidence Score: 4/5

The PR is not yet safe to merge because doctor can emit materially incorrect dangling-boundary diagnostics for freshly upgraded v1 stores and migrated Desktop stores.

Findings

  1. P1 Host generation selects stale tables
Summary

This PR broadens OpenCode v2 store acceptance for databases migrated from v1 and makes doctor select the boundary table using the detected host generation.

  • Recognizes session_message plus session_v2 as evidence that a mixed-generation store is readable by v2 readers.
  • Allows the doctor boundary check to receive an explicit host generation.
  • Adds fixtures for migrated-v2, downgraded-v1, and fresh-v2 stores.
  • The doctor routing still misreads stores during the pre-migration upgrade window and on migrated Desktop installations.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Active OpenCode installation] --> B{Version contains a digit?}
    B -->|Yes| C[Use generation parsed from installed binary]
    B -->|No / Desktop| D[Detect generation from store tables]
    C --> E{Store state}
    E -->|Already migrated or downgraded mixed store| F[Use host generation to choose live table]
    E -->|Old v1 store before first v2 launch| G[Incorrectly query session_message]
    D --> H{Mixed migrated v2 store?}
    H -->|Yes| I[Detected as v1; incorrectly query frozen message]
    H -->|No| J[Query detected generation table]
Loading

Reviews (1) · Last reviewed commit: "fix(opencode2): accept v1 stores migrate..."

When OpenCode 2 migrates an existing 1.18.x store, it keeps the v1 `message` and
`part` tables beside its own schema. detectOpenCodeStoreGeneration() (rightly,
for 1.18.x stores that also ship session_message) calls any store with
message+part v1, which breaks OpenCode 2 hosts on every migrated install:

- assertOpenCodeStoreGeneration(db, "v2") in V2StoreReader throws "OpenCode
  store generation mismatch ... expected v2, found v1", the v2 hook refuses,
  and every turn in every session is interrupted.
- doctor's dangling-boundary check reads the `message` table, which is frozen
  at the migration point, so boundaries written since the upgrade are reported
  as dangling.

`session_v2` is written only by an OpenCode 2 host: the 1.18.31 binary never
references it, while 2.0.7 creates it in both fresh and migrated stores. The v2
assertion now also passes when the store has session_message and session_v2.
Detection itself is unchanged, so a 1.18.x store is still v1 and still refused
for v2 readers, and v1 assertions behave exactly as before.

A store can carry both generations in either direction (migrated by 2.x, or
used by 1.x again after an upgrade), so table presence cannot say which one is
live. The doctor boundary check now takes the host generation doctor already
resolved from the active install, and falls back to detection without it,
including for Desktop installs, which report no version.
(On a real migrated store, 6,529 of its 6,885 v1 message ids are present in
session_message under the same ids.)

Tests pin the table lists observed on a migrated 2.0.7 store and on a fresh
2.0.7 data directory, and add migrated and downgraded stores to the doctor
boundary check. The migrated cases fail without the fix; the downgraded case
fails if the table is chosen from session_v2 instead of the host; and dropping
the session_v2 requirement fails the existing 1.18.30 fixture.
const dangling = listDanglingCompartmentBoundaries(
contextDb,
sessionDb,
/\d/.test(activeInstallation.version) ? hostGeneration : undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Host generation selects stale tables

When the v2 CLI is installed but has not migrated an existing v1 store yet, this override selects session_message instead of the live v1 message table, so doctor can report valid compartment boundaries as dangling. The reverse happens for Desktop installations: their version is "unknown", so a migrated mixed-generation v2 store falls back to schema detection, is classified as v1, and reads the frozen message table. Table selection needs to account for both the host generation and the store's migration state.

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