Summary
POST /tasks on the classic path is idempotent by client-supplied id: when a task with the requested id already exists, createTask silently returns the existing record (broker.ts "Idempotent create" block, unchanged through 674fc75b). The HTTP response for an idempotent return is byte-indistinguishable from a fresh create — no flag, no status distinction, no audit event.
This caused a real operational incident during the skills-intake review gate (context: #2007): a fleet dispatcher reused a fixed task id across 6 dispatch rounds. Rounds 2–6 were silent no-ops (idempotent returns of the round-1 record), but the dispatcher counted each response as created=1 and archived the same round-1 result 6 times as if they were per-round results. A false "broker rewrites assignedWorkerId at create" bug hypothesis was investigated for hours before the audit log showed only one task.created ever happened.
Problems
- Caller cannot distinguish "created" from "returned existing". The idempotent return is a deliberate design (good for retries), but it is unobservable.
- No audit trail for idempotent hits. A create attempt against an existing id leaves zero broker-side evidence (no
task.created, no deny), which made incident reconstruction rely on client-side logs.
- Inconsistent semantics with the live-task path.
handleCreateTaskRequest rejects duplicate ids for payload.mode === live tasks with invalid_transition ("task X already exists"), while the classic path silently returns the old record.
Suggested fix (any subset)
- Include an explicit marker on the response for idempotent returns (e.g.,
idempotentReturn: true, or created: false), and/or
- Emit an audit event (e.g.,
task.create_idempotent_hit) with the requested vs existing id, and/or
- Document the idempotent contract in the tasks API spec so dispatchers know to treat an existing-id response as "already exists" rather than "created".
Happy to follow up with a PR if the maintainer picks a direction.
Summary
POST /taskson the classic path is idempotent by client-supplied id: when a task with the requested id already exists,createTasksilently returns the existing record (broker.ts"Idempotent create" block, unchanged through674fc75b). The HTTP response for an idempotent return is byte-indistinguishable from a fresh create — no flag, no status distinction, no audit event.This caused a real operational incident during the skills-intake review gate (context: #2007): a fleet dispatcher reused a fixed task id across 6 dispatch rounds. Rounds 2–6 were silent no-ops (idempotent returns of the round-1 record), but the dispatcher counted each response as
created=1and archived the same round-1 result 6 times as if they were per-round results. A false "broker rewrites assignedWorkerId at create" bug hypothesis was investigated for hours before the audit log showed only onetask.createdever happened.Problems
task.created, no deny), which made incident reconstruction rely on client-side logs.handleCreateTaskRequestrejects duplicate ids forpayload.mode === livetasks withinvalid_transition("task X already exists"), while the classic path silently returns the old record.Suggested fix (any subset)
idempotentReturn: true, orcreated: false), and/ortask.create_idempotent_hit) with the requested vs existing id, and/orHappy to follow up with a PR if the maintainer picks a direction.