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
session: importing a session whose message IDs already exist returns HTTP 500 instead of a conflict #54336
Importing a session payload whose message IDs already exist in the local database fails with HTTP 500 (Internal Server Error) instead of a conflict.
This is reachable whenever an export is re-imported under a different session ID (e.g. to import a copy) or when message rows survive without their session — #50260 documents session delete leaving pre-V2 rows orphaned for legacy sessions.
Reproduction (2.0.26, Windows 10)
opencode session export <id> > session.json
Change info.id in the file to a new ID (importing a copy)
opencode session import session.json → Error: Failed to import session: Internal Server Error
A conflict (409): the destination session row does not exist, but its message rows do.
Notes
SessionTransfer.import (packages/core/src/session/transfer.ts) only checks the session row for duplicates, then inserts the messages with their original IDs using Effect.orDie, so the UNIQUE constraint on session_message.id becomes a defect that surfaces as a 500. The import handler already maps ImportConflictError to a 409, so a message-ID conflict check before publishing the import would turn this into a clean conflict.
Related: #50260 (orphaned legacy rows after delete), #53146 (session_message.seq collisions).
Thanks for the detailed report. I reproduced this on 2.0.26: I created a session with a few messages, exported it, changed info.id to a new ID and POSTed the copy to /api/experimental/session/import. The server returned HTTP 500 instead of a 409 conflict. The same reproduction still returns 500 on the latest dev (7b3d4ce).
The code matches your analysis: SessionTransfer.import only checks whether the session row already exists. The messages are inserted with their original IDs through Effect.orDie, so a duplicate message ID comes back as a 500 instead of an ImportConflictError. I couldn't read the server log in my run, so I haven't seen the UNIQUE constraint message myself.
The script below reproduces it with opencode-drive and the simulated model. Related: #50260 (orphaned rows after delete), #53146 (session_message.seq collisions).
Reproduction script (drive.ts)
// Reproduces #54336: re-importing an exported session under a new session ID// fails with HTTP 500 (UNIQUE constraint failed: session_message.id) instead of 409.import{Effect}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"constcall=(e: unknown)=>easEffect.Effect<any,any,never>exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),project: {git: true,files: {"a.txt": "hello\n"}},},({ ui, llm, opencode })=>Effect.gen(function*(){// 1. Create a session with settled messages using the simulated model.yield*llm.queue(Llm.text("Hello from the simulated model."))yield*ui.submit("say hello")yield*ui.waitFor("Hello from the simulated model.")yield*Effect.sleep("2 seconds")constlist: any=yield*call((opencodeasany).session.list({}))constsessions=Array.isArray(list) ? list : (list.items??list.data??list)constsessionID=sessions[0].idyield*Effect.log(`session: ${sessionID}`)// 2. Export it.constdata: any=yield*call((opencodeasany).session.export({ sessionID }))yield*Effect.log(`exported ${data.messages.length} messages`)// 3. Import a copy under a new session ID (message IDs unchanged).constcopy={ ...data,info: { ...data.info,id: "ses_"+"0".repeat(10)+Date.now().toString(36)}}constresult=yield*Effect.exit(call((opencodeasany).session.import(copy)))consttext=JSON.stringify(result)conststatus=text.match(/"status":(\d+)/)?.[1]yield*Effect.log(`import of copy: ${result._tag}, HTTP status ${status??"n/a"}`)yield*Effect.log(`expected 409 (conflict), body/cause excerpt: ${text.match(/UNIQUE[^"\\]*|InternalServerError|ImportConflict[^"\\]*/g)?.join(" | ")??"-"}`)}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
I'd like to take this one. The fix looks contained: pre-check the imported message IDs against session_message before publishing the import and surface the existing ImportConflictError so the handler maps it to 409, instead of the raw UNIQUE-constraint defect. I'll add a regression test for the re-import-under-new-session-id path.
+1, hit this while repairing a wedged session. every import of an exported session (even a single message) returned Failed to import session: Internal Server Error. the server log had the EffectDrizzleQueryError with the query and params but never the driver error, which made it look like the payload shape was wrong.
workaround that works: regenerate every msg_ id (and the ses_ id) before import. cloned ids collide with existing rows and 500; fully synthetic sessions with fresh ids import fine. an explicit conflict error or an auto re-key on import would have saved a lot of digging.
Thanks for the extra data — it matches what we found. The 500 is the session_message UNIQUE constraint firing during import publication; the colliding msg_ IDs are the trigger, not the payload shape.
The fix (pre-check the message IDs and return a 409 Session messages already exist: <id> instead of the raw constraint) is in #54353 and is waiting on review.
On the log: the underlying SQLite error is kept in the error's cause, but EffectDrizzleQueryError's rendered message only contains the query and params, so the actual driver error never appears in the log — that is independent of the import fix and worth its own look.
Activity
opencode-agent commentedon Oct 10, 2026
Thanks for the detailed report. I reproduced this on 2.0.26: I created a session with a few messages, exported it, changed
info.idto a new ID and POSTed the copy to/api/experimental/session/import. The server returned HTTP 500 instead of a 409 conflict. The same reproduction still returns 500 on the latestdev(7b3d4ce).The code matches your analysis:
SessionTransfer.importonly checks whether the session row already exists. The messages are inserted with their original IDs throughEffect.orDie, so a duplicate message ID comes back as a 500 instead of anImportConflictError. I couldn't read the server log in my run, so I haven't seen the UNIQUE constraint message myself.The script below reproduces it with opencode-drive and the simulated model. Related: #50260 (orphaned rows after delete), #53146 (
session_message.seqcollisions).Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.eatmoreduck commentedon Oct 10, 2026
I'd like to take this one. The fix looks contained: pre-check the imported message IDs against
session_messagebefore publishing the import and surface the existingImportConflictErrorso the handler maps it to 409, instead of the raw UNIQUE-constraint defect. I'll add a regression test for the re-import-under-new-session-id path.reynold-hu commentedon Oct 11, 2026
Opened #54543 with the pre-check fix and a regression test.
FahadBinHussain commentedon Oct 14, 2026
+1, hit this while repairing a wedged session. every import of an exported session (even a single message) returned
Failed to import session: Internal Server Error. the server log had theEffectDrizzleQueryErrorwith the query and params but never the driver error, which made it look like the payload shape was wrong.workaround that works: regenerate every
msg_id (and theses_id) before import. cloned ids collide with existing rows and 500; fully synthetic sessions with fresh ids import fine. an explicit conflict error or an auto re-key on import would have saved a lot of digging.reynold-hu commentedon Oct 14, 2026
Thanks for the extra data — it matches what we found. The 500 is the
session_messageUNIQUE constraint firing during import publication; the collidingmsg_IDs are the trigger, not the payload shape.The fix (pre-check the message IDs and return a 409
Session messages already exist: <id>instead of the raw constraint) is in #54353 and is waiting on review.On the log: the underlying SQLite error is kept in the error's
cause, butEffectDrizzleQueryError's rendered message only contains the query and params, so the actual driver error never appears in the log — that is independent of the import fix and worth its own look.fix(core): map imported message-ID collisions to a 409 conflict
fix(core): map imported message-ID collisions to a 409 conflict