Skip to content

fix(api): make bound agent calls idempotent#338

Merged
Yevanchen merged 2 commits into
langgenius:mainfrom
AsperforMias:fix/bound-agent-idempotency
Jul 20, 2026
Merged

fix(api): make bound agent calls idempotent#338
Yevanchen merged 2 commits into
langgenius:mainfrom
AsperforMias:fix/bound-agent-idempotency

Conversation

@AsperforMias

Copy link
Copy Markdown
Collaborator

Summary

  • Add standard Idempotency-Key handling to POST /api/v1/bound/:token.
  • Reserve a stable Session identity in D1 before Run admission, bind the original Run durably, and recover that exact Session/Run after timeout, lost response, or an interrupted binding write.
  • Scope keys to the verified bound capability/deployment identity and reject same-key/different-body reuse with 409.
  • Preserve the existing behavior for callers that do not send an idempotency key.
  • Add an additive D1 migration plus HTTP/service-level concurrency and fault-injection coverage.

Fixes #327.

Why

  • Before this change, an accepted bound call could create and queue a Run, return an ambiguous timeout to the caller, and create a second owner-billed Session/Run when the caller retried the same logical request.
  • Rate limiting limits volume but cannot distinguish a retry from a new logical request. The durable reservation gives the request a stable identity before any billable Run is admitted.
  • The original reproduction observed 2 Session / 2 Run / 2 api_command rows after two identical timeout attempts. The same timeout trigger now leaves exactly 1 / 1 / 1 and one Queue delivery.

Verification

  • Commands:
    • just test-file apps/api/tests/bound-agent-idempotency.e2e.test.ts - 12 passed, 0 failed, 55 assertions.
    • just tc-package @mosoo/api
    • just tc-package @mosoo/db
    • just db-reset-local - applied migrations 0000, 0001, and the new additive 0002 to local Wrangler D1 only.
    • just check - exit 0; formatting, docs, lint, all typechecks, all package tests, API 1013 passed/0 failed, and GraphQL codegen check passed.
  • Manual steps:
    • Replayed the issue repro by forcing the first and second service calls to reach deployment_agent_call_timeout after durable admission, then queried D1 and confirmed one reservation, Session, Run, and dispatch command.
    • Sent concurrent identical HTTP requests and confirmed both resolve from one Session/Run and only one Queue message is sent.
    • Discarded a successful HTTP result and retried the same key/body to model an ambiguous response; the response and original Run were recovered.
    • Injected failures at Session creation, Run admission, and reservation-to-Run binding, then confirmed retries recover the reserved identity without duplicate admission.
    • Verified same-key/different-body rejection, capability subject isolation, revocation recheck, and legacy no-key behavior.
  • Not run:
    • No remote Cloudflare account, deployed Worker, remote D1, Queue, Durable Object, provider credential, model request, or billable resource was used.
    • Source-level invariants, the issue repro, and the local injected HTTP/service/D1/outbox chain demonstrate that the business-consistency defect is fixed. The remaining unverified boundary is remote Cloudflare platform integration behavior; it is not an absence of validation of the issue's business-consistency fix.

Impact

  • User/API/contract changes:
    • Bound Agent callers may provide Idempotency-Key. Identical retries under the same verified capability/deployment identity recover the original work; conflicting reuse returns 409.
    • Requests without the header retain the previous non-idempotent behavior for backward compatibility.
  • Generated files / GraphQL / DB / lockfile:
    • Adds bound_agent_call_idempotency_key, its unique subject/key index, and an updated-at index through additive migration 0002_bound-agent-call-idempotency.sql.
    • Includes the generated Drizzle snapshot/journal and local HTTP fixture schema update.
    • No destructive SQL, GraphQL output, or lockfile changes.
  • Env or config changes:
    • None.
  • Risk and rollback:
    • Main risks are reservation recovery and subject scoping; concurrency, timeout, ambiguous-response, persistence-failure, body-conflict, and revocation cases are covered directly.
    • Reverting the application commit restores the previous route behavior. The additive table can remain unused safely; production rollback does not require destructive D1 SQL.

Review

  • Closest review areas:
    • Public bound-call authentication/rate-limit/idempotency ordering.
    • Stable Session/Run recovery after interrupted writes.
    • D1 schema, migration, and capability/deployment subject scope.
  • Known trade-offs:

@AsperforMias

Copy link
Copy Markdown
Collaborator Author

那个七千多lines diff的是drizzle为每次迁移生成的完整db schema snapshot

@Yevanchen
Yevanchen force-pushed the fix/bound-agent-idempotency branch 4 times, most recently from 6eaedb2 to 262891d Compare July 18, 2026 16:05
@Yevanchen
Yevanchen force-pushed the fix/bound-agent-idempotency branch from 262891d to 8fd460c Compare July 19, 2026 16:06
@Yevanchen
Yevanchen merged commit 7712b25 into langgenius:main Jul 20, 2026
6 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.

fix(api): make bound Agent calls idempotent across ambiguous retries

2 participants