Repository navigation
[Bug] Replaying a completed prepared action can regress a task to waiting_approval #36
Description
What happened?
The action layer correctly treats an idempotency replay as the same logical action, including after it has already succeeded:
ActionService.propose(..., idempotencyKey)returns the existing proposal immediately.tests/actions.test.tsexplicitly pins that replaying a completed idempotent proposal returnsstatus: "succeeded"without preparing or dispatching again.
The task/model layer does not preserve that state.
AgentService.prepare() currently treats whatever actions.propose() returns as a newly prepared review:
- checkpoint
actionId: proposal.id; - emit an
approvalevent; - return the proposal.
Then prepare_email / prepare_event in executeModelTask() unconditionally do:
outcome = { status: "waiting_approval", actionId: action.id };
return { status: "waiting_approval", actionId: action.id };So if a resumed model ignores state.approvalResult and repeats the exact same prepare call, the idempotency layer safely returns the already-succeeded action, but the task state regresses to waiting_approval anyway.
No external write is duplicated, which is good, but the durable task/UI state can now claim that a review is required for an action that is already complete. On the following worker tick the succeeded action may be consumed again, but a model that repeats the same tool can cycle through the same state.
The invariant seems to be:
an idempotent replay may reuse a completed operation receipt, but it must not regress the caller back to a pre-commit state.
Steps to reproduce
A focused model-worker regression should be deterministic:
- Create a model-backed task.
- Have the model call
prepare_event. - Approve it and let the action reach
succeeded. - Resume the task;
execute()consumes the succeeded action, checkpointsstate.approvalResult, and clearsactionId. - Have the next model response call the exact same
prepare_eventagain. - Assert that:
- no second provider write occurs;
- no second review is created;
- the task does not return to
waiting_approval; - the completed receipt is returned/consumed as already completed.
The existing action-level replay test already establishes the lower-level behavior; the missing regression is at the task/model boundary.
Commit or release
82ff35b912e4a08c435d29278631c95445adf658
Platform
API or worker
Environment and safe logs
Code-path audit against current main; no local executable run claimed.
A minimal fix could make prepare() or the model prepare tools branch on the returned proposal status:
awaiting_review/executing: preserve the existing wait path;succeeded: surface the existing receipt without creating a new wait state;failed/denied/expired/outcome_unknown: preserve the terminal/uncertain result rather than presenting a fresh review.
AI-use note: I used an AI assistant to trace the action idempotency and task-state paths and draft this report; I verified the behavior against current actions.ts, service.ts, model.ts, and the existing idempotent replay regression before filing.
Activity