Skip to content

feat(service-ai): turn idempotency (turnId) — safe Retry without re-planning (ADR-0013 D1)#1900

Merged
os-zhuang merged 1 commit into
mainfrom
adr-0013-d1
Jun 15, 2026
Merged

feat(service-ai): turn idempotency (turnId) — safe Retry without re-planning (ADR-0013 D1)#1900
os-zhuang merged 1 commit into
mainfrom
adr-0013-d1

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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-POSTs messages with the same conversationId; the server unconditionally writes a duplicate user row and re-runs the whole tool loop from scratch → the LLM re-plans, can rename objects (contactcontacts), 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:

  • dedups the inbound user message by (conversationId, turnId);
  • short-circuits the stored reply when the turn already produced a completed assistant reply (no tool re-run, no re-plan).

Specifics:

  • ai_messages: new turn_id column + (conversation_id, turn_id) index. Auto-reconciled by the SQL driver (alterTable adds missing columns + materializes declared indexes) — 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 — intermediate tool-call/tool turns carry the same turn_id but are never mistaken for the reply).
  • ai-service: dedup + short-circuit in both chatWithToolsImpl and streamChatWithTools.
  • agent-routes: read body.turnId into toolExecutionContext.

Tests

  • +6 dedup/short-circuit cases (chat + stream): same turnId ⇒ one user row, tools run once, stored reply returned; incomplete turn re-runs without a duplicate user row; no turnId ⇒ legacy behaviour.
  • +4 getTurnState cases (objectql conversation service).
  • Full service-ai suite green (385).

Acceptance (cloud#334)

  • ✅ Re-sending the same turnId does NOT create a second user message and does NOT re-execute tools.
  • ✅ A completed turn re-requested returns the stored reply.
  • ⏳ Browser/EE-rig retry (no duplicate/orphan drafts) — verified at the cloud SHA-bump PR.

🤖 Generated with Claude Code

…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>
@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 15, 2026 8:31am

Request Review

@github-actions github-actions Bot added the tests label Jun 15, 2026
@os-zhuang
os-zhuang merged commit ce1aba9 into main Jun 15, 2026
13 checks passed
@os-zhuang
os-zhuang deleted the adr-0013-d1 branch June 15, 2026 09:10
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.

1 participant