Skip to content

fix(core): map imported message-ID collisions to a 409 conflict - #54353

Closed
eatmoreduck wants to merge 4 commits into
anomalyco:devfrom
eatmoreduck:fix/session-import-message-id-conflict
Closed

eatmoreduck wants to merge 4 commits into
anomalyco:devfrom
eatmoreduck:fix/session-import-message-id-conflict

Conversation

@eatmoreduck

@eatmoreduck eatmoreduck commented Oct 10, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #54336

Type of change

  • Bug fix

What does this PR do?

session import currently dies with HTTP 500 (empty body) when the payload's message IDs already exist in session_message — reachable by re-importing an export under a new session ID, or when orphaned message rows survive their session (#50260).

SessionTransfer.import only checked the session row for duplicates, then inserted messages with their original IDs behind Effect.orDie, so the UNIQUE constraint surfaced as a defect. This pre-checks the settled message IDs (batched at 500, same pattern as stats.ts) before any write and fails with the existing SessionTransfer.ImportConflictError, which the HTTP handler already maps to 409.

How did you verify your code works?

  • New regression test in packages/server/test/session-import.test.ts: importing a payload whose message IDs exist under another session returns 409 ConflictError and does not create the target session — red on dev, green with this change
  • Existing session-import tests pass; full server/core suites show no new failures vs. the dev baseline (remaining failures are pre-existing environment-dependent tests: shell/bash-3.2, MCP network, OAuth ports)
  • bun run typecheck and oxlint clean on changed files

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

This change was made with AI assistance (GLM via ZCode agent, under my direction); I reviewed and verified the full diff, reproduction, and test results locally.


Added by the opencode bot (AI-generated).

Related: #50260

@github-actions github-actions Bot added the needs:compliance This means the issue will auto-close after 2 hours. label Oct 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix looks good. I ran the reproduction from #54336 against both checkouts with the simulated model: export a session, change info.id, import the copy. On the base the import returns HTTP 500. With this change it fails with ConflictError (409), and no session is created for the new ID. The new server test fails on the base and passes here. The other session-import tests, the core session-create tests, and type checks for core and server all pass.

The public API is unchanged: the import endpoint now returns its existing 409 conflict instead of a 500.

No blocking concerns. The check runs before the write transaction, so a concurrent insert, or a payload that repeats a message ID within itself, could still give a 500. Both are rare and don't need to hold this up.

Related: #50260

@github-actions github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Oct 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@reynold-hu

Copy link
Copy Markdown
Contributor

One note from the original reproduction (matching the triage suggestion on #54336): with ImportConflictError the 409 body reads Session already exists: <sessionID>, but in this path the session row does not exist — only its message IDs do. If you want, a distinct message such as Session messages already exist would keep the response accurate for this case.

An assertion that the rejected import did not create the destination session is also worth adding — that is the failure mode when the constraint fires.

@eatmoreduck

Copy link
Copy Markdown
Author

Both done in 1783849: ImportConflictError now carries a reason, and a message-ID conflict answers Session messages already exist: <id> while a real duplicate session keeps the old Session already exists body. The tests assert both bodies, and the rejected-import case already checks the destination session was not created (404).

@reynold-hu

Copy link
Copy Markdown
Contributor

Thanks for picking this up! I had a PR (#54543) fixing the same issue (#54336) that was closed as a duplicate of this one. One suggestion: consider a dedicated MessageConflictError (a tagged error carrying the sessionID) instead of reusing ImportConflictError — in this scenario the destination session doesn't exist yet, only its messages do, so a 409 like "Session messages already exist: " is more accurate than an import-conflict message. Happy to help test this.

@eatmoreduck

Copy link
Copy Markdown
Author

Done in e2da1ce — MessageConflictError is now its own tagged error carrying the sessionID, ImportConflictError is back to meaning 'the session row exists', and the handler maps each to its own 409 body. Tests assert both bodies plus the no-destination-session guarantee. Typecheck and the import suites are green.

@eatmoreduck
eatmoreduck force-pushed the fix/session-import-message-id-conflict branch from e2da1ce to d29594d Compare October 22, 2026 08:20

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix works, but the latest commit breaks the test file.

I ran the #54336 reproduction against both checkouts with the simulated model: export a session, change info.id, import the copy. On the base it returns a 500. With this change it returns a 409 ConflictError ("Session messages already exist"), and no session is created for the new ID.

The last commit adds two stray lines, ).pipe(Effect.scoped), and ), at lines 53–54 of packages/server/test/session-import.test.ts. Because of them, bun test test/session-import.test.ts stops with a parse error and runs no tests, and bun run typecheck in packages/server fails (TS1128/TS1109). With those two lines removed, all 6 import tests pass and type checks for server and core are clean.

Public API: the response shape is unchanged. A message-ID collision now returns the existing 409 ConflictError, with the new message Session messages already exist: <id>, instead of a 500.

  • packages/server/test/session-import.test.ts:53: These two lines are left over from an edit: the test above is already closed on lines 51–52. They are a syntax error, so bun test stops before running anything in this file and bun run typecheck fails with TS1128/TS1109. Removing them makes all 6 tests pass.

@eatmoreduck

Copy link
Copy Markdown
Author

Good catch on the stray lines — the rebase fix lived only in my working tree and never made it into d29594d. Pushed bfe9f1d with them removed: the 6 import tests run again and server/core typechecks are clean (verified against the committed tree this time).

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stray lines in session-import.test.ts are gone. The import tests (6/6) and the server type check pass again. I re-ran the #54336 reproduction with the simulated model: the base still returns a 500, and this change returns a 409 ConflictError without creating the new session.

Public API: the response shape is unchanged. A message-ID collision now returns the existing 409 ConflictError instead of a 500.

No remaining concerns.

Importing a session whose message IDs already exist (e.g. re-importing
an export under a new session ID) hit the session_message UNIQUE
constraint and died as a 500 with an empty body. Pre-check the settled
message IDs (batched at 500, same as stats.ts) before any write and
fail with the existing SessionTransfer.ImportConflictError, which the
HTTP handler already maps to 409.

Fixes anomalyco#54336
…ssions

ImportConflictError now carries a reason: "session" keeps the existing
'Session already exists' body, while "messages" (only message IDs taken,
the session row was never created) answers 'Session messages already
exist: <id>'. The tests now assert both 409 bodies and that a rejected
message-ID import leaves no destination session behind.
…t conflicts

Per review: a message-ID conflict never created the destination session,
so reusing ImportConflictError (with a reason discriminant) made every
consumer unwrap a union that only this path produced. MessageConflictError
now carries the sessionID directly and maps to the 409 'Session messages
already exist: <id>' body; ImportConflictError is back to meaning 'the
session row exists'. Both 409 bodies and the no-destination-session
assertion are covered by the import tests.
The conflict resolution left a duplicate ').pipe(Effect.scoped),' and
')' at the top of the file; bun test parsed nothing and typecheck
failed (TS1128/TS1109). With them removed the 6 import tests pass.
@eatmoreduck
eatmoreduck force-pushed the fix/session-import-message-id-conflict branch from bfe9f1d to 205627c Compare November 4, 2026 09:28
@eatmoreduck

Copy link
Copy Markdown
Author

Rebased onto current dev — the conflict was upstream's new import subpath feature crossing our settled-messages refactor; resolution keeps both (subpath computation retained, messages built from the existing settled variable). Core suites (session-create / session-title / session-model-request + hooks / compaction) pass locally on the rebased branch.

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants