feat(service-ai): turn idempotency (turnId) — safe Retry without re-planning (ADR-0013 D1)#1900
Merged
Conversation
…lanning (ADR-0013 D1) A user turn becomes an idempotent unit. The client supplies a stable `turnId` per turn (constant across Retry); the server dedups the inbound user message by (conversationId, turnId) and short-circuits the stored reply when the turn already completed, instead of writing a duplicate user row and re-running the tool loop (which re-plans → renamed objects, orphan drafts). - ai_messages: + turn_id column + (conversation_id, turn_id) index (auto-reconciled by the SQL driver; no hand-written migration). - contract: turnId on ToolExecutionContext; addMessage(…, turnId) + new getTurnState(conversationId, turnId) on IAIConversationService. - conversation services (objectql + in-memory): persist turn_id on every message of a turn; getTurnState reports userExists + the final reply (an assistant message with no pending tool calls). - ai-service: dedup + short-circuit in BOTH chatWithToolsImpl and streamChatWithTools; every turn message tagged with turnId. - agent-routes: read body.turnId into toolExecutionContext. Tests: +6 dedup/short-circuit (chat + stream) and +4 getTurnState cases; full service-ai suite green (385). Cross-repo: pairs with objectui chat client (turnId in request body) and a cloud SHA bump, per the ADR-0012 cross-repo recipe. Refs cloud#334. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ADR-0013 D1 — turn idempotency
Part of the cross-repo durable-turn work for cloud#334. Pairs with the objectui chat client (turnId in the request body) and a cloud SHA bump, coordinated via the ADR-0012 cross-repo recipe.
Problem
The agent chat endpoint has no turn identity. On
Retry, the client re-POSTsmessageswith the sameconversationId; the server unconditionally writes a duplicate user row and re-runs the whole tool loop from scratch → the LLM re-plans, can rename objects (contact→contacts), and leaves orphan drafts. The final assistant text is also persisted before it's yielded to SSE, so a turn that only failed on delivery already succeeded server-side — yet Retry re-executes everything.Change
A user turn becomes an idempotent unit. The client supplies a stable
turnId(constant across Retry); the server:(conversationId, turnId);Specifics:
ai_messages: newturn_idcolumn +(conversation_id, turn_id)index. Auto-reconciled by the SQL driver (alterTableadds missing columns + materializes declared indexes) — no hand-written migration.turnIdonToolExecutionContext;addMessage(…, turnId)+ newgetTurnState(conversationId, turnId)onIAIConversationService.turn_idon every message of a turn;getTurnStatereportsuserExists+ the final reply (an assistant message with no pending tool calls — intermediate tool-call/tool turns carry the sameturn_idbut are never mistaken for the reply).ai-service: dedup + short-circuit in bothchatWithToolsImplandstreamChatWithTools.agent-routes: readbody.turnIdintotoolExecutionContext.Tests
turnId⇒ one user row, tools run once, stored reply returned; incomplete turn re-runs without a duplicate user row; noturnId⇒ legacy behaviour.getTurnStatecases (objectql conversation service).service-aisuite green (385).Acceptance (cloud#334)
turnIddoes NOT create a second user message and does NOT re-execute tools.🤖 Generated with Claude Code