You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
migration: sessions created via 1.19 /api/session lose all messages on v1->v2 migration (2.1.3) #58543
Upgrading a 1.19.x database to 2.1.3 keeps every session created through 1.19's /api/session endpoint (the "next" session store, rows in session_message) but drops all of its messages. The session row survives; GET /api/session/{id}/message then returns 200 with an empty page — no error, no log line. Sessions created through the legacy routes (rows in message/part) migrate correctly.
Related but different from #58041 (that one covers v1 sessions created after the migration, which become malformed stubs and 500). Here the sessions existed before the migration, were already in the 1.19 session_message format, and come back empty.
Measured (copy of a real 1.19.3 database, 832 sessions)
before (1.19.3)
after starting 2.1.3 on the copy
session_message rows / distinct sessions
12,717 / 308
12,184 / 503
legacy message rows / sessions
11,737 / 502
11,741 / 503
Per session, via GET /api/session/{id}/message on 2.1.3:
GET /api/experimental/migration/v1 reports completed; the server log shows only the schema migrations and no errors.
So the bulk migration appears to rebuild session_message from the legacy tables and discard the rows 1.19 had already written there for its own /api/session sessions.
Steps to reproduce
With [email protected], opencode serve, then create a session with POST /api/session and send a couple of prompts with POST /api/session/{id}/prompt. Confirm GET /api/session/{id}/message returns them.
Stop the server. Copy the data dir.
Start @opencode/[email protected] (opencode serve) on that data dir and wait for /api/experimental/migration/v1 → completed.
GET /api/session/{id}/message for the session from step 1 → empty.
Expected
Sessions already stored in the 1.19 session_message format keep their messages through the migration (they need no translation).
Environment
From: opencode-ai 1.19.3 (linux-x64), server mode
To: @opencode/cli 2.1.3 (linux-x64, npm), opencode serve
Thanks for the very precise report. I reproduced this and it still happens on the latest dev (50989519c).
Steps, with a mock OpenAI-compatible provider:
On [email protected], create a session with POST /api/session and send two prompts via POST /api/session/{id}/prompt. The messages are returned and stored in session_message, and the legacy message table stays empty.
/api/experimental/migration/v1 reports completed and the session itself is still there, but GET /api/session/{id}/message returns {"data":[]}. All of that session's session_message rows have been deleted, with nothing in the log.
The cause seems to be in the V1 migration's per-session copy. It deletes the session's existing session_message rows and rebuilds them only from the legacy message/part tables, which are empty for sessions created through /api/session. It is not the reset_v2_session_state schema migration: 1.19.3 had already applied that one.
The script below reproduces this end to end against 2.1.3.
Related: #58041 (v1 sessions created after the migration become stubs and return 500), and #50130 / #57080 (v1 sessions written after the migration are not imported).
Reproduction script (repro.sh)
#!/usr/bin/env bash# Repro for #58543: 1.19 /api/session sessions lose their session_message rows in the v1->v2 migration.# Needs bun, curl, python3. Uses a mock OpenAI-compatible provider on :4999.# Step 2 uses `opencode` from PATH (bun add -g @opencode/[email protected]). Set OC_DEV=/path/to/opencode-checkout to run step 2 from source instead of @opencode/[email protected].set -euo pipefail
W=${W:-/tmp/oc-58543}; rm -rf "$W"; mkdir -p "$W/proj";cd"$W"
cat > mock.ts <<'TS'Bun.serve({port:4999, async fetch(req){ const body:any = await req.json().catch(()=>({})) if(!body.stream) return Response.json({id:"x",object:"chat.completion",created:0,model:"mock",choices:[{index:0,message:{role:"assistant",content:"Title"},finish_reason:"stop"}],usage:{prompt_tokens:1,completion_tokens:1,total_tokens:2}}) const c=[{choices:[{index:0,delta:{role:"assistant",content:"Hello from mock"}}]},{choices:[{index:0,delta:{},finish_reason:"stop"}],usage:{prompt_tokens:5,completion_tokens:3,total_tokens:8}}] return new Response(c.map(x=>`data: ${JSON.stringify({id:"x",object:"chat.completion.chunk",created:0,model:"mock",...x})}\n\n`).join("")+"data: [DONE]\n\n",{headers:{"content-type":"text/event-stream"}})}})TS
bun mock.ts & MOCK=$!
cat > proj/opencode.json <<'J'{"model":"mock/mock","small_model":"mock/mock","provider":{"mock":{"npm":"@ai-sdk/openai-compatible","name":"Mock","options":{"baseURL":"http://127.0.0.1:4999/v1","apiKey":"x"},"models":{"mock":{"name":"mock"}}}}}Jenv_for(){ export XDG_DATA_HOME=$W/$1/data XDG_CONFIG_HOME=$W/$1/config XDG_STATE_HOME=$W/$1/state XDG_CACHE_HOME=$W/$1/cache; }
count(){ python3 -c "import sqlite3,sys;print(sqlite3.connect(sys.argv[1]).execute('select count(*) from session_message').fetchone()[0])""$1"; }
# --- Step 1: [email protected] ---
(cd proj; env_for v1; bunx --bun [email protected] serve --port 4100 >$W/v1.log 2>&1) & V1=$!until curl -sf localhost:4100/doc >/dev/null;do sleep 1;done
B=http://127.0.0.1:4100
SID=$(curl -s -XPOST $B/api/session -H 'content-type: application/json' -d '{"model":{"id":"mock","providerID":"mock"}}'| python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["id"])')fortin"first prompt""second prompt";do
curl -s -XPOST $B/api/session/$SID/prompt -H 'content-type: application/json' -d "{\"prompt\":{\"text\":\"$t\"}}">/dev/null
curl -s -XPOST $B/api/session/$SID/wait >/dev/null
done
sleep 2
echo"1.19.3 messages for $SID:"; curl -s "$B/api/session/$SID/message?limit=50"| python3 -c 'import json,sys;print([m["type"] for m in json.load(sys.stdin)["data"]])'
pkill -P $V1||true;kill$V1||true; sleep 2
echo"session_message rows before: $(count $W/v1/data/opencode/opencode.db)"# --- Step 2: v2 on a copy of the data dir ---
cp -r v1 v2; DB=$W/v2/data/opencode/opencode.db
if [ -n"${OC_DEV:-}" ];then
(cd proj; env_for v2; OPENCODE_DB=$DB bun --conditions=browser --preload=$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js $OC_DEV/packages/cli/src/index.ts serve --port 4200 >$W/v2.log 2>&1) & V2=$!else
(cd proj; env_for v2; opencode serve --port 4200 >$W/v2.log 2>&1) & V2=$!fiuntil grep -q 'password'$W/v2.log 2>/dev/null;do sleep 1;done
A="opencode:$(grep -o 'password .*'$W/v2.log | cut -d'' -f2)"until curl -s -u "$A" localhost:4200/api/experimental/migration/v1 | grep -q completed;do sleep 1;doneecho"v2 session: $(curl -s -u "$A" localhost:4200/api/session/$SID| head -c 80)"echo"v2 messages: $(curl -s -u "$A""localhost:4200/api/session/$SID/message?limit=50")"echo"session_message rows after: $(count $DB)"
pkill -P $V2||true;kill$V2$MOCK||true
Only a copy. The live machine is still on 1.19.3 and was never upgraded; I ran 2.1.3 against a backup of its database in an isolated sandbox, and deleted the copy afterwards. Agreed on the silent completed. Thanks for the offer, but I'll wait for the upstream fix rather than adopt a separate design.
Activity
opencode-agent commentedon Oct 23, 2026
Thanks for the very precise report. I reproduced this and it still happens on the latest
dev(50989519c).Steps, with a mock OpenAI-compatible provider:
[email protected], create a session withPOST /api/sessionand send two prompts viaPOST /api/session/{id}/prompt. The messages are returned and stored insession_message, and the legacymessagetable stays empty.@opencode/[email protected]on it./api/experimental/migration/v1reportscompletedand the session itself is still there, butGET /api/session/{id}/messagereturns{"data":[]}. All of that session'ssession_messagerows have been deleted, with nothing in the log.The cause seems to be in the V1 migration's per-session copy. It deletes the session's existing
session_messagerows and rebuilds them only from the legacymessage/parttables, which are empty for sessions created through/api/session. It is not thereset_v2_session_stateschema migration: 1.19.3 had already applied that one.The script below reproduces this end to end against 2.1.3.
Related: #58041 (v1 sessions created after the migration become stubs and return 500), and #50130 / #57080 (v1 sessions written after the migration are not imported).
Reproduction script (repro.sh)
Run it:
bash repro.shsamvallad33 commentedon Oct 24, 2026
Hi justinhornberger-ctrl,
Did any machine you actually work on already upgrade, or only the copy?
A migration that empties 308 sessions should never report completed without a single log line.
Want a look at a stripped down design pairing memory, security and authority so old sessions stay findable?
Sam
justinhornberger-ctrl commentedon Oct 24, 2026
Only a copy. The live machine is still on 1.19.3 and was never upgraded; I ran 2.1.3 against a backup of its database in an isolated sandbox, and deleted the copy afterwards. Agreed on the silent
completed. Thanks for the offer, but I'll wait for the upstream fix rather than adopt a separate design.