Repository navigation
migration: V1 migration aborts on legacy message ID failing Session.Message.ID schema #54736
Description
Summary
Upgrading from V1 to v2.0.27 aborts the V1 data migration with Data migration failed / Schema validation failed. The cause is that the migration validates legacy message IDs against Session.Message.ID, which requires a string starting with msg_. At least one V1 message in my database does not match, and the whole migration dies rather than skipping or repairing the offending record. The result is that no V1 session history is imported.
Environment
- opencode version: 2.0.27
- OS: Darwin 27.0.0 (darwin arm64)
- Terminal: TERM_PROGRAM=herdr, TERM=xterm-256color, COLORTERM=truecolor
- Shell: /bin/zsh
- Install/channel: latest
- Active plugins:
oh-my-opencode-slim,superpowers@git+https://github.com/obra/superpowers.git(both load successfully under V2)
Reproduction
- Use OpenCode V1 for an extended period, accumulating session history (this database dates back to roughly May 2026).
- Upgrade to v2.0.27.
- Start OpenCode.
- A
Data migration failed — Schema validation failed. Caused by: [object Object]toast appears, and~/.local/share/opencode/log/opencode.logrecordsV1 migration failed.
Reproducible consistently: the migration is retried and fails identically on every service start.
Expected Behavior
The V1 migration either imports the history, or skips/repairs individual records it cannot validate and reports how many were dropped. A single malformed message ID should not abort the entire migration.
Actual Behavior
The migration dies. No V1 session history is available in V2. From opencode.log:
level=ERROR message="V1 migration failed" cause="Cause([Die(Error: Schema validation failed (cause: _c({
"_tag":"Composite","ast":<String>,"issues":[up({
"_tag":"Filter",
"filter":xB({"_tag":"Filter","annotations":{
"expected":"a string starting with \"msg_\"",
"representation":{"id":"effect/schema/isStartingWith","payload":{"startsWith":"msg_"}},
"arbitraryConstraint":{"patterns":[{"source":"^msg_","flags":""}]},
"identifier":"Session.Message.ID"
},"aborted":false}),
"issue":nr({"_tag":"InvalidValue","annotations":undefined})
})]})))])
Two usability problems on top of the underlying bug:
- The user-facing toast renders the cause as
[object Object], so it conveys nothing actionable. - The logged cause is a dump of minified Effect schema internals. It names the failing schema (
Session.Message.ID) and the constraint, but it does not identify which message, session, or row failed, so there is no way to locate or repair the offending record. Including the offending value and its session ID would make this self-serviceable.
Additional Context
- Frequency: consistent, on every start.
- Workarounds tried: none. The troubleshooting guide says not to edit the database while diagnosing, so the V1 data has been left untouched.
- Happy to provide more detail from the V1 database if you can tell me what to query, or to test a patch.
Activity
opencode-agent commentedon Oct 12, 2026
Thanks for the detailed report. I was able to reproduce this on v2.0.27, and it still happens on the latest
dev(b9ca4d21b).Steps:
msg_…ID and one with an ID that starts withmsgbut has no underscore (e.g.msg000000000002…).V1 migration failedwith the sameSession.Message.ID/a string starting with "msg_"error as yours, nothing is imported, and it fails the same way on every start.The script below reproduces it.
What seems to happen: the migration's V1 schema only requires message IDs to start with
msg, so that row is accepted as valid. It then fails when inserted into V2, which requiresmsg_. That error aborts the whole migration instead of skipping the one row.If you'd like to confirm that this matches your data, this read-only query lists such IDs in your V1 database:
The migration does not change your V1 tables, so your history is still in the database. Related: #53971 and #52226 (V1 messages that fail validation get silently dropped).
Reproduction script (repro.sh)
Run it:
bash repro.shmaciejmalysz commentedon Oct 12, 2026
Confirmed — your diagnosis matches my data exactly.
The read-only query returns 9 rows in my V1 database (~5.1 GB):
All 9 are
msg+ a bare UUID (no underscore), and each sits in a different session. So 9 malformed rows are blocking the import of an entire multi-gigabyte history — which makes the abort-on-first-invalid-row behaviour especially costly here.Two notes on the fix, given the overlap with #53971 / #52226:
msg<uuid>tomsg_<uuid>, or minting a freshmsg_ID and rewriting the matchingpart.message_id) would preserve it. These IDs look like they came from a much older ID scheme rather than corruption, so a deterministic rewrite seems safe.migrated N messages, repaired/skipped M) would make it obvious that something was dropped instead of leaving it silent.Dante-dan commentedon Oct 12, 2026
I traced this to the V1/V2 ID prefix mismatch. I’m working on a focused migration fix that retains the V1 IDs while resolving parts and compaction relationships, then assigns deterministic, collision-safe V2 IDs to messages without the
msg_prefix. The source V1 tables remain unchanged.jorge-trujillo commentedon Oct 13, 2026
Confirming on 2.0.20 (Linux x86_64,
latestchannel, Homebrew install, no plugins configured).My data
The read-only query returns 21 malformed IDs spread across 17 sessions:
Examples:
msg1234,msg100,msg1000000,msg1000001,msg1773607006292,msg1775951960183.Unlike the
msg<uuid>IDs above, these aremsg+ bare digits with no underscore. They sit in sessions whose titles look like API/client smoke tests ("Model inquiry", "Greeting and model inquiry"), so these IDs were most likely supplied by the caller through the V1 HTTP API rather than produced by an older internal ID scheme. So at least two distinct sources of malformed IDs exist in real databases.Migration state at the time of failure:
kvrowmigration.v1-v2={"phase":"sessions","cursor":"ses_30cc…"}session_v2; the remaining 155 never importedV1 migration failedlog line each timeTwo additional findings
1. The malformed IDs are referenced structurally, so a fix has to repair more than
message.id.In my database the 21 bad IDs are referenced by:
part.message_idforeign keysmessage.data.parentIDvalues inside the JSON payloadRenaming only
message.idwould leave danglingparentIDreferences. A repair needs to rewrite all three in one transaction. (part.datahas nomessageIDfield in V1 rows, and nocompaction.tail_start_idpointed at a bad ID, but those are worth checking too.)2. Restarting the TUI does not retry the migration — this makes the bug look unfixable from the client side.
The failure status is cached in the long-lived
opencode serve --serviceprocess, and a relaunched TUI just reattaches to that same service. So the "Data migration failed" toast keeps reappearing on every launch with no new log entry, which reads as "still broken" even after the underlying data is fixed.opencode service restartis required to re-run the migration. Worth documenting, because the obvious first troubleshooting step (relaunch opencode) cannot clear it.Workaround that worked
After a consistent snapshot via SQLite's online backup API, I renamed
msg<rest>→msg_<rest>, repointed the 21part.message_idvalues, and rewrote the 31message.data.parentIDreferences in a single transaction. No collisions resulted.After
opencode service restartthe migration completed: 342 sessions and 15,901 messages insession_v2, 0 malformed IDs,integrity_checkandforeign_key_checkclean, and all previously blocked sessions present.This supports the deterministic rewrite option over skipping — a skip would have silently dropped those messages and their replies. I also agree a
migrated N, repaired M, skipped Ksummary line in the log is the most important part of the fix.marcello-russo commentedon Oct 15, 2026
Real-world confirmation on 2.0.29 (Linux, background service, 12 GB DB with 3681 V1 sessions / 248k messages / 943k parts).
Exactly one offending row, found with
SELECT id, session_id FROM message WHERE id NOT GLOB 'msg_*';:The migration stopped after ~52 sessions with the same
Session.Message.ID ... starting with "msg_"error, andGET /api/experimental/migration/v1never reachedcompleted. Renaming it tomsg_init1(plus itspart.message_idreference) and restarting the service let the migration complete: all 3681 sessions landed insession_v2, V1 tables untouched.Full step-by-step recovery guide (diagnosis queries, backup/rollback notes): https://gist.github.com/marcello-russo/0b3bd4872a9eac4b0c31ebf7dc332c01
Dante-dan commentedon Oct 15, 2026
The
msg-init1example is covered by #54937: the patch handles every V1 message ID without themsg_prefix, including hyphenated IDs, rather than only the UUID examples above. It resolves parts, parent links and compaction relationships using the original IDs, then assigns deterministic, collision-safe IDs to the V2 projection; it does not rename rows in the V1 source tables.Your manual repair confirms the same prefix mismatch on another real database. It is separate from testing the PR itself; the PR remains open for review.