Skip to content

Idempotent submit returns DUPLICATE_TASK error instead of the original task_id #58

@scoropeza

Description

@scoropeza

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions