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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions