Skip to content

session: importing a session whose message IDs already exist returns HTTP 500 instead of a conflict #54336

Open

Description

@reynold-hu

Description

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)

  1. opencode session export <id> > session.json
  2. Change info.id in the file to a new ID (importing a copy)
  3. opencode session import session.json → Error: Failed to import session: Internal Server Error

The server log for that request:

cause="... SQLiteError: UNIQUE constraint failed: session_message.id ..." http.method=POST http.url=/api/experimental/session/import http.status=500

Expected

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).

Activity

  1. opencode-agent commented on Oct 10, 2026

    @opencode-agent
    Contributor

    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"
    
    const call = (e: unknown) => e as Effect.Effect<any, any, never>
    
    export default OpenCodeDriver.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")
    
          const list: any = yield* call((opencode as any).session.list({}))
          const sessions = Array.isArray(list) ? list : (list.items ?? list.data ?? list)
          const sessionID = sessions[0].id
          yield* Effect.log(`session: ${sessionID}`)
    
          // 2. Export it.
          const data: any = yield* call((opencode as any).session.export({ sessionID }))
          yield* Effect.log(`exported ${data.messages.length} messages`)
    
          // 3. Import a copy under a new session ID (message IDs unchanged).
          const copy = { ...data, info: { ...data.info, id: "ses_" + "0".repeat(10) + Date.now().toString(36) } }
          const result = yield* Effect.exit(call((opencode as any).session.import(copy)))
          const text = JSON.stringify(result)
          const status = 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[^"\\]*|Internal Server Error|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.

  2. eatmoreduck commented on Oct 10, 2026

    @eatmoreduck

    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.

  3. reynold-hu commented on Oct 11, 2026

    @reynold-hu
    ContributorAuthor

    Opened #54543 with the pre-check fix and a regression test.

  4. FahadBinHussain commented on Oct 14, 2026

    @FahadBinHussain

    +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.

  5. reynold-hu commented on Oct 14, 2026

    @reynold-hu
    ContributorAuthor

    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.

  6. added a commit that references this issue on Oct 22, 2026
    fb0b06f
  7. added a commit that references this issue on Nov 4, 2026
    e04f624
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions