Repository navigation
Closed as not planned
Labels
Description
Summary
Migrating OpenCode V1 chat history into the V2 desktop beta destroys or hides it: the built-in migrator 500s on a few corrupt legacy rows, sessions written by third-party tools into the V2 tables are silently undecodable, very large sessions can't be imported at all, and there is no discoverable way to open the old sessions (project picker exists only as a hidden keybind).
Environment
- opencode version: 0.0.0-next-17403 (embedded
opencode-cli serve --servicevia OpenCode Beta desktop app), alsoopencode20.0.0-next-17428 - OS: Linux 6.8.0-136-generic (Ubuntu), x86_64
- Terminal: xterm-256color (desktop app is the client used)
- Shell: /bin/bash
- Install/channel: next / beta desktop (Electron,
/opt/OpenCode Beta/ai.opencode.desktop.beta) - Active plugins: none found in config
Reproduction
- Start from a V1 data dir (
~/.local/share/opencode/opencode.dbcontaining legacysession/message/parttables alongside V2session_v2/session_message), with 26 corrupt legacypartrows. - Open the V2 desktop beta; let it run its built-in migration.
GET /api/experimental/migration/v1→500 database disk image is malformed(queryingmessageby a session whosepartrows are corrupt). No partial credit, no user-facing warning — history just looks empty.- Any tool or script that "helps" by writing V1-shaped rows directly into
session_v2/session_messageproduces rows thatSELECTcleanly but fail the runtime decoder:
GET /api/session/<id>/message→500 "failed to decode session message"(log only). POST /api/session/importwith a session of ~13k messages → 500 with empty body; log shows
SQLite query expected 27270 values, received 92806atSessionTransfer.import (../server/src/handlers/session.ts:112). Last safe observed size: 9000 messages.- Even after messages are present and decoding, the beta desktop home/history shows nothing for the project — the project picker (
project.select) exists only as a hidden keybindmod+shift+o(not in Settings), and in the current desktop build that shortcut does nothing when pressed.lastProjectstays pinned to whatever was open on restart, so there is no working way to reach another folder's history from the UI.
Expected Behavior
- Migration should survive a few bad legacy rows (warn + skip that session) and report what failed.
- Records written in a compatible format should either decode or fail with the specific field (the import validator already does this well).
- History, once migrated, should be reachable from the UI without a hidden shortcut.
Actual Behavior
- One corrupt part row aborts the whole migration; the app shows empty history with no error.
- Hand-migrated rows are rejected with only
failed to decode session messageand no field detail (root causes we found: missing top-levelfinish, missingmodel.variant,step-start/step-finishcontent items that don't exist in V2, toolstateusingoutputinstead ofcontent, tool error states needingstate.erroras aSession.StructuredErrorobject). - Sessions >~9k messages cannot be imported (prepared-statement parameter mismatch → silent empty-body 500).
- Migrated history is effectively invisible:
GET /api/sessiondefaults to newest-50 and the app has no discoverable session/folder picker — the only candidate (project.select/mod+shift+o) is declared in the command catalog but non-functional in the desktop build.
Additional Context
- Expected import payload shape that works:
{ "info": Session.Info, "messages": Session.Message.Info[] }— verified end-to-end (1000+ sessions imported, all decode, served via the message endpoint). - Frequency: consistently reproducible across restarts.
- Workaround used: delete the offending V2 rows and re-import each session through
POST /api/session/import, converting V1 part data to the strict V2 shapes; for the two ~13k/18k-message sessions, only the most recent 9000 messages could be imported. - Idempotency note: an import whose
POSTreturns empty-body 500 looks like success to clients;?.dataand error tags are not consistently enforced. - No opencode git checkout was available on the machine, so no exact source refs were inspected; stack path in step 5 came from the server log.
Activity
github-actions commentedon Aug 15, 2026
This issue might be a duplicate of (or closely related to) existing issues. Please check:
fork_boundary— the migrator 500ing on corrupt/incompatible legacy rows is the same subsystem and failure mode described here.mod+shift+wandmod+odo nothing in the new layout — the project/directory picker keybind being non-functional in the new layout is covered there; this report'smod+shift+o(project.select) issue is likely the same root cause.For keybind-related issues, please also check our pinned keybinds documentation: #4997
Qiiks commentedon Aug 20, 2026
Independent reproduction with a corrected decode harness (2026-08-20, fork 1.18.18 / v1.18.18 serve)
Reproduced the same decode failure class from step 4 on a fork binary (
1.18.18,opencode serve --port 4096) against a long-lived dual-layer DB. Evidence below is from the serve's own logs plus a bun:sqlite enumeration that mirrors the server-side decoder exactly.1. The decode failure is DB-wide on
session_message, not one corrupt sessionHarness calibrated to the server's decode path (
packages/core/src/session/history.ts→SessionHistory.load), withidandsessionIDinjected from SQL columns exactly as the server does:session_messagerows fail decode under the currentSessionMessage.Messageschema{"type":"aborted"}error objects (pre-tighten error union), rows missing required fields added after they were written (parentID,path,mode,agent,cost,tokens, reasoning-parttime.start), and rows carryingrole:"synthetic"that the current schema no longer accepts as a standalone roleGET /api/session/:id/message→500 "failed to decode session message"for every affected session; serve log shows the rejection at[N]["info"]["error"]2. Stronger finding: the projector stopped writing
session_messageentirely (Aug 13)Fresh sessions created after the V1/V2 split merge have zero
session_messagerows —SessionProjector(packages/core/src/session/projector.ts) no longer projects that table. So this isn't just "old rows are undecodable": the V2 message table is write-dead for new sessions and read-dead for old ones. The table is currently a zombie: 58,002 rows, ~90% undecodable, no new writes.3. Why it's invisible today — and the trigger that makes it user-visible
The app's
detectServerProtocol(packages/app/src/utils/server-protocol.ts:24-34) returns"v1"for any healthy server (/global/health→healthy:trueshort-circuits before the v2 probe). All current clients (web UI, mobile SDK, paseo SDK 1.14.46) therefore ride the legacy/session/:id/messagesurface and never touch the V2 read path — which is why nothing crashes until a V2-protocol client connects. The moment protocol detection or a client selects v2, every old session with pre-merge rows 500s, exactly as step 4 predicts.4. The legacy surface has the same row class — fixed at the data layer, already verified
The same V1-shaped rows also break the legacy unlimited fetch (validates every row against
SessionV1.WithParts), which is the surface mobile/paseo/web actually use. That side was repaired in-place on 2026-08-20 (no fork changes, no migration):{"type":"aborted"}→ current writer's{"name":"MessageAbortedError","data":{"message":"Aborted"}}(shape verified against 3 post-merge reference rows the 1.18.18 binary itself emits)parentID(most-recent preceding user message) andpath {cwd,root}from per-session healthy valuesrole:"synthetic"rows → reparented assynthetic:truetext parts onto the preceding user message (matches current writer via reminders.ts; 0 post-merge standalone rows exist)time:{created,completed}→time:{start,end}Result: 13,053 messages + 41,648 parts decode clean (0 failures), both affected sessions return HTTP 200 on unlimited fetch, and the serve reads the DB live per request (no restart required). The repair surface was precisely bounded — 1,506 failing rows across exactly the 2 user-visible sessions, matching the serve log's rejection indexes.
Suggested fix direction for the V2 side
Data-level repair of
session_messageis impractical at this scale (52.5K rows) and pointless while the projector is write-dead. The two fork-side options, per this issue's framing:{"type":"aborted"}→MessageAbortedError, tolerate missing optional fields with schema defaults). This kills the whole breakage class without a data migration, but is a carve-out from the deliberate schema tighten.session_message(upstream may have disabled/renamed the projection); without writes, V2 message history is empty for all new sessions regardless of decode.Both are latent today (no client selects v2), but become user-visible the moment the V1 cutover (#35054) lands. Happy to provide the harness script, per-session row dumps, or the exact failing-row enumeration if useful.
github-actions commentedon Oct 20, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.