Summary
The shared execute_action endpoint (PATCH /api/chat/messages/{message_id}/actions/{action_id}/execute) does not guard against re-execution of an already-executed action before invoking side-effectful handlers. This means a retry, double-click, or concurrent request can trigger the same side effect (e.g., creating a duplicate project issue, calendar event, or sending a duplicate email) from the same staged action.
Root cause
In core-api/api/routers/chat.py, the execute_action endpoint:
- Loads the action part from
content_parts by action_id.
- Calls
_execute_action (which calls the write service, e.g., create_issue).
- Only after the handler returns sets
action_part["data"]["status"] = "executed" and persists it.
There is no check that action.status == "staged" before step 2, and no atomic compare-and-set (staged → executing) to claim the action before the write.
Impact
Any side-effectful handler is replayable:
create_project_issue → duplicate project cards
create_calendar_event → duplicate calendar events
send_email → duplicate emails sent
Suggested fix
Before calling _execute_action, add a status pre-check (or an atomic compare-and-set update) in the execute_action endpoint:
For stronger protection against concurrent requests, perform an atomic DB update that sets status = 'executing' only where status = 'staged' and check the row count before proceeding with the write.
References
Summary
The shared
execute_actionendpoint (PATCH /api/chat/messages/{message_id}/actions/{action_id}/execute) does not guard against re-execution of an already-executed action before invoking side-effectful handlers. This means a retry, double-click, or concurrent request can trigger the same side effect (e.g., creating a duplicate project issue, calendar event, or sending a duplicate email) from the same staged action.Root cause
In
core-api/api/routers/chat.py, theexecute_actionendpoint:content_partsbyaction_id._execute_action(which calls the write service, e.g.,create_issue).action_part["data"]["status"] = "executed"and persists it.There is no check that
action.status == "staged"before step 2, and no atomic compare-and-set (staged → executing) to claim the action before the write.Impact
Any side-effectful handler is replayable:
create_project_issue→ duplicate project cardscreate_calendar_event→ duplicate calendar eventssend_email→ duplicate emails sentSuggested fix
Before calling
_execute_action, add a status pre-check (or an atomic compare-and-set update) in theexecute_actionendpoint:For stronger protection against concurrent requests, perform an atomic DB update that sets
status = 'executing'only wherestatus = 'staged'and check the row count before proceeding with the write.References