Follow-up from PR #52 — surfaced during hands-on deploy-validation (Scenario 14). API contract change.
Functional description
bgagent submit --idempotency-key <key> correctly prevents duplicate task creation when the same key is submitted twice, but the second call returns an error instead of the already-created task:
$ KEY="sam-$(date +%s)"
$ bgagent submit --repo scoropeza/agent-plugins --task "…" \
--idempotency-key "$KEY" --output json | jq -r .task_id
01KQTMCH2MQW4FGZDA2K033VP2
$ bgagent submit --repo scoropeza/agent-plugins --task "…" \
--idempotency-key "$KEY" --output json
Error: A task with this idempotency key already exists. (DUPLICATE_TASK)
Observed on backgroundagent-dev (AWS account 169728770098).
User-visible impact: idempotency semantics exist so callers can safely retry on network failure — the first submit succeeded server-side but the response never reached the client. Under the current behaviour the caller cannot distinguish:
- "The server already has this task; here's its id" (benign; caller can resume)
- "Something unrelated went wrong with my submit" (caller must abort)
The caller is forced to parse the error code and recover the task_id via a separate round-trip (bgagent list --output json | jq …) to make idempotent submits actually idempotent from the caller's perspective.
The underlying storage IS idempotent — no duplicate task is created, that part works correctly. Only the response contract is wrong.
Technical fix
Change the HTTP contract on POST /v1/tasks when the submitted idempotency_key matches an existing task:
|
Today |
Proposed |
| HTTP status |
400 / 409 |
200 (or 201 with Idempotent-Replay: true header) |
| Body |
{error: {code: "DUPLICATE_TASK"}} |
Existing task's full TaskDetail — identical schema to a fresh create |
Standard idempotency behaviour across mature APIs (Stripe's Idempotency-Key, AWS Lambda's X-Amz-Client-Context): the second request returns the original response, not an error.
Implementation notes:
cdk/src/handlers/shared/create-task-core.ts currently detects the DDB ConditionalCheckFailed on the create and errors out. Change to GetItem-by-idempotency-key on failure, and return the found record as if it were a fresh create.
- The idempotency window / TTL (if any) should be documented — a caller replaying a 30-day-old key should get either "task still exists; replaying" or a clean 404 explaining the key expired, not an opaque
DUPLICATE_TASK.
Acceptance criteria
POST /v1/tasks with a previously-used idempotency_key returns 2xx + the original TaskDetail. Schema identical to a fresh create.
bgagent submit --idempotency-key X --output json returns the same JSON shape on first and subsequent calls with the same key.
bgagent submit --idempotency-key X --wait blocks until the original task reaches terminal state (not "error on 2nd call") on subsequent calls.
- Regression test: submit twice with same key, assert identical
task_id on both responses.
- Backwards-compatibility window: document the behaviour change in the release notes (any caller relying on the error shape has to adapt, but the replay semantics are strictly more useful).
Out of scope
- Adding idempotency support to other endpoints (nudge, cancel, webhook CRUD).
- Durable idempotency record retention (weeks / months) — keep the current TTL / window.
- CLI-side changes beyond "surface the returned task_id" — the server contract change makes the CLI transparent here.
References
Functional description
bgagent submit --idempotency-key <key>correctly prevents duplicate task creation when the same key is submitted twice, but the second call returns an error instead of the already-created task:Observed on
backgroundagent-dev(AWS account169728770098).User-visible impact: idempotency semantics exist so callers can safely retry on network failure — the first submit succeeded server-side but the response never reached the client. Under the current behaviour the caller cannot distinguish:
The caller is forced to parse the error code and recover the task_id via a separate round-trip (
bgagent list --output json | jq …) to make idempotent submits actually idempotent from the caller's perspective.The underlying storage IS idempotent — no duplicate task is created, that part works correctly. Only the response contract is wrong.
Technical fix
Change the HTTP contract on
POST /v1/taskswhen the submittedidempotency_keymatches an existing task:Idempotent-Replay: trueheader){error: {code: "DUPLICATE_TASK"}}TaskDetail— identical schema to a fresh createStandard idempotency behaviour across mature APIs (Stripe's
Idempotency-Key, AWS Lambda'sX-Amz-Client-Context): the second request returns the original response, not an error.Implementation notes:
cdk/src/handlers/shared/create-task-core.tscurrently detects the DDBConditionalCheckFailedon the create and errors out. Change toGetItem-by-idempotency-key on failure, and return the found record as if it were a fresh create.DUPLICATE_TASK.Acceptance criteria
POST /v1/taskswith a previously-usedidempotency_keyreturns 2xx + the originalTaskDetail. Schema identical to a fresh create.bgagent submit --idempotency-key X --output jsonreturns the same JSON shape on first and subsequent calls with the same key.bgagent submit --idempotency-key X --waitblocks until the original task reaches terminal state (not "error on 2nd call") on subsequent calls.task_idon both responses.Out of scope
References
cdk/src/handlers/shared/create-task-core.ts— idempotency handling on create