Skip to content

feat(server): Idempotency-Key on POST /chat - #667

Merged
LinuxDevil merged 3 commits into
mainfrom
fix/http-idempotency-key
Oct 16, 2026
Merged

LinuxDevil merged 3 commits into
mainfrom
fix/http-idempotency-key

Conversation

@LinuxDevil

Copy link
Copy Markdown
Owner

Problem

A client that resends POST /chat (retry after a crash or timeout, a double submit) appended a new user turn, so tools with side effects could run twice. An empty input is 400, so there was no HTTP way to "just resume" an interrupted turn.

Fix

POST /chat accepts an Idempotency-Key header (1-255 visible ASCII). The key is stored on the turn's user message (metadata.idempotencyKey), so it lives in the transcript, or in the turn's checkpoint until the turn finishes, and survives restarts. On a resend with the same key in the same session:

  • the turn finished: 200 SSE with a single run.done carrying its final reply, plus Idempotent-Replayed: true. Nothing runs.
  • the turn is still running in this process: 409 LOUSHO_SESSION_BUSY.
  • the turn was interrupted (checkpointed, in-progress): resumed via session.resume() (finished steps are not re-run) and its events streamed through session.on(), instead of appending the message again.
  • the session is paused on an approval: 409 LOUSHO_SESSION_AWAITING_APPROVAL, the same as without a key.
  • otherwise: a new turn whose user message is marked with the key.

PendingTurn gains idempotencyKey? (from the pending checkpoint), so the route can tell its own interrupted turn from someone else's. The approvals route's in-flight release now uses the same releasing() wrapper, so it also fires when the stream is returned before it starts.

Docs: deployment.md "Resending a turn", sessions.md pending(), errors.md LOUSHO_SESSION_BUSY, CHANGELOG.

Verification

  • New src/server/idempotency.test.ts covers replay, a new key vs. no key, 409 while running, replay after a restart over the same store, crash-resume without a duplicate message or re-run tool, and a malformed key. 5 of its 6 tests fail against main.
  • npx vitest run src/server src/session ... (route, deploy and remote tests): 332 passed.
  • npx tsc --noEmit and npm run lint are clean. npm run typecheck:tests shows only errors that already exist on main (loadMcpTools.test.ts, checkpoints.test.ts).

🤖 Generated with Claude Code

@LinuxDevil
LinuxDevil force-pushed the fix/http-idempotency-key branch 2 times, most recently from eb86164 to a663383 Compare October 16, 2026 21:58
LinuxDevil and others added 3 commits October 17, 2026 01:26
A resend of a turn with the same Idempotency-Key no longer appends the
user message again: a finished turn's reply is replayed as run.done
(Idempotent-Replayed: true), a turn a crash interrupted is resumed from
its checkpoint, and a turn still running in this process is a 409
(LOUSHO_SESSION_BUSY). The key is stored on the turn's user message
(metadata.idempotencyKey), so it survives restarts; session.pending()
reports an interrupted turn's key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LinuxDevil
LinuxDevil force-pushed the fix/http-idempotency-key branch from ed28f7d to de7fc44 Compare October 16, 2026 22:32
@LinuxDevil
LinuxDevil merged commit 0a18c87 into main Oct 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant