Repository navigation
Open
Labels
Description
Description
Summary
After upgrading from OpenCode 1.19.0 → 2.0.29 → 2.0.32, a migrated long-running session shows only part of its history in the TUI. About 812 messages written between the V2 migration and the exit of the pre-upgrade client process exist in the legacy message/part tables (and in the event log) but were never projected into session_message. opencode session export is truncated for the same reason, so the missing history cannot be exported.
Environment
- opencode version: 2.0.32 (session created on 1.19.0; upgraded to 2.0.29, then 2.0.32)
- OS: Linux 5.15.0-52-generic Panic when tool fails #58~20.04.1-Ubuntu SMP x86_64
- Terminal: TERM_PROGRAM=vscode, TERM=xterm-256color, COLORTERM=truecolor
- Shell: /bin/sh
- Install/channel: latest (curl installer,
~/.opencode/bin/opencode) - Active plugins: none found (global config has no plugin entries)
Reproduction
- On OpenCode 1.19.0, start a long-running session and keep its client/server process open.
- Upgrade to 2.0.29 without stopping that process; keep appending messages through the old process for several days.
- Upgrade to 2.0.32 and open the same session in V2.
- The TUI shows history only up to the migration snapshot; messages written between the snapshot and the old process exit are absent.
- Verify with SQLite (
~/.local/share/opencode/opencode.db):SELECT COUNT(*) FROM message WHERE session_id=?→ 1973SELECT COUNT(*) FROM session_message WHERE session_id=?→ 1185- Missing range (3 days, 662 assistant + 150 user messages).
- The event-sourced log still contains the missing events:
SELECT COUNT(*) FROM event WHERE aggregate_id=? AND seq BETWEEN 1162 AND 12163→ 10999 (message.updated,message.part.updated,session.updated).- However
event_sequence.seqfor the session is 12344, so V2 treats them as already processed and never projects them.
opencode session export "<session-id>"returns 1184 messages (export readssession_message), so the missing history is truncated there too.
Expected Behavior
Migrated sessions should include all messages present in the legacy message/part tables, including those appended after the initial migration (e.g., by re-projecting late legacy messages at startup or when the session is opened). At minimum, session export should not silently omit them.
Actual Behavior
- The TUI and
session exportomit roughly 812 messages written by the pre-upgrade process after migration. - Manually reconstructing
session_messagerows from the retainedeventlog causesfailed to decode session message(HTTP 500 from/api/experimental/session/<session-id>/export) for some reconstructed rows, so the projection schema contains constraints that are not publicly documented.
Additional Context
- DB:
~/.local/share/opencode/opencode.db. - Version timeline from logs: 1.19.0 until 10-15 21:45 (local); 2.0.29 from 10-15 21:45; 2.0.32 from 10-19 14:43.
- The pre-upgrade process kept running with a deleted binary (
/proc/<pid>/exe -> ~/.opencode/bin/opencode (deleted)) and wrote only the legacy tables until it exited on 10-19. - Workarounds tried:
opencode session export+session import: import refuses with "Session already exists"; export is truncated anyway.- Rewinding
event_sequence.seq: do not do this — it is the event append cursor, not a projection cursor. It causes new events to reuse sequence numbers and overwrite the log (observed on a DB copy). - Manual projection from
eventintosession_message: partial success, but the decoder rejects some reconstructed rows (toolstate.content, error-state shape, etc.), so it is not safe as an end-user workaround.
- Suggestion: provide a repair/re-projection command (or automatically project late legacy messages) and let
session exportfall back to legacy tables. - Data safety: raw
message/part/eventrows are intact; only thesession_messageprojection is missing.
Reacted by Kacper Kaśków
Activity
opencode-agent commentedon Oct 19, 2026
Thanks for the detailed report. I can reproduce this on 2.0.32, and it still happens on the latest
dev(ba06ff9e).What I did, in an isolated data directory:
session_messagehas 10 rows.Result: the legacy
messagetable has 20 rows for the session, butsession_messagestill has 10, andopencode session exportreturns only 10 messages. Once the import has run, it never looks at an already-imported session again.One caveat:
opencode importin 1.19.0 doesn't writeeventrows, so I couldn't check your point aboutevent_sequencealready being past the logged events. The missing messages themselves reproduce. The script below runs the whole thing.This is close to #50130 and #49641, where whole V1 sessions created after the import are never imported. Your case is new messages inside a session that was already imported. And thanks for flagging that rewinding
event_sequence.seqcorrupts the log; until there is a proper repair path, please keep using the legacy tables or your DB backup.Reproduction script (repro.sh)
Run it:
bash repro.shhexin commentedon Oct 21, 2026
I can observe the same problem.
desaiamogh commentedon Oct 25, 2026
Cross-posting from #58840 (closing as a duplicate of this + #50130), since the constraints that caused
failed to decode session messagein your manual projection are exactly the ones we had to reverse-engineer.We hit the same truncation pattern (one migrated session that gained ~380 legacy-only messages after the import, on top of 5 fully-stranded sessions / 916 messages) and recovered by inserting
session_messagerows directly. To get the shapes right, I rebuilt the official importer's transformation and validated it against all 650 messages it had already produced on our DB — exact match (parsed-JSON equality) on 650/650, then applied it to the stranded data. The rules:id;type= legacy metarole;seq= legacy order (ORDER BY time_created, rowid);time_created/time_updatedcopied from the legacymessagerow. Message-leveltime={created: row.time_created, completed: row.time_updated}— note the importer uses the row'stime_updated, notmeta.time.completed.{"text": <first text part>, "time": {"created": ...}}. V1 "attachment echo" text parts after the first (Called the Read tool with the following input: ...) are dropped.fileparts →files: [{data: <raw base64 payload after "base64,">, mime, name: filename, source: {"type": "inline"}}](data stays base64, not decoded).{agent, model: {providerID, id: modelID, variant: meta.variant ?? "default"}, content, snapshot?, finish?, cost, tokens: {input, output, reasoning, cache} (droptotal), error?, time}.content: dropstep-start/step-finish/patch;snapshot.start/snapshot.endcome from theirsnapshotfield only when present (older V1 rows have nosnapshotfield at all → nosnapshotkey in the message);patch.filesmerges intosnapshot.files.reasoningpart: keeptime, renamedstart→created,end→completed.textpart: droptime(only{type, text}).toolpart:callID→id,tool→name, part-levelmetadata(thoughtSignature) →providerState.state: keepstatus/input/metadata, droptitleandtime;state.output(a plain string) →state.content = [{"type": "text", "text": ...}];state.errorplain string →{"type": "tool.execution", "message": ...}(this and thecontentshape are the most likely causes of the decode failures you saw). Errored tools normally have nocontent— the only exception is aborted tools, where the partial output hides instate.metadata.output(can be""), notstate.output. Tooltimemoved between V1 builds: older rows carry it on the part, newer rows insidestate.time— handle both, emit as part-level{created, completed}.error:{name: "MessageAbortedError"|"APIError", data: {message}}→{type: "aborted"|"provider.error", message}.Caveats: this is end-user surgery, not a supported path — take an online
sqlite3 .backupfirst, leave thekv['migration.v1-v2']marker alone (per the warning in #49641), and keep the legacy tables intact so it stays reversible. We verified the recovered sessions afterwards throughGET /api/session/{id}/message(paginates vialimit+cursor.next). Happy to share the Python transform if useful.