Pairs with the parallel complete_task commit. claim_task opens an agent_runs row, flips status to in_progress only when it's safe (ready/draft → in_progress, never overwriting a deliberate blocked/done/in_progress), and returns the resolved workflow prompt + source level. Idempotent re-claim by the same actor returns the existing run with reused=true and refreshes notes only — started_at is sacred. Different-actor re-claim errors with ALREADY_CLAIMED naming the existing actor and run id. Tenancy fence: if the backlog item exists but in a different workspace than the resolved handle, we refuse with "doesn't belong to workspace" rather than 404. Prevents cross-tenant existence fishing. All three writes (run insert + status flip + audit insert) happen in one db.transaction() so a partial claim is unreachable. tools/index.ts now registers both claim_task and complete_task. Co-authored-by: Cursor <cursoragent@cursor.com>
5.3 KiB
| kind | slug | title | plan_slug | epic_slug | status | priority | tenant_id | owner | cursor_todo_id | updated_at |
|---|---|---|---|---|---|---|---|---|---|---|
| task | mcp-complete-task-tool | MCP complete_task tool — close an agent_runs row and finalize status | agent-coordination | mcp-claim-complete | in_progress | P2 | global | unassigned | null | 2026-06-02 |
Task summary
A new MCP tool complete_task that an agent calls at session end. It closes the agent_runs row with an outcome and token totals, and optionally flips the backlog item to done (or another terminal status).
Description
Contract
Register in apps/mcp-server/src/tools/complete-task.ts.
Input zod schema:
{
runId: string, // uuid of the agent_runs row to close
outcome: "succeeded" | "failed" | "cancelled" | "stalled",
tokensInput?: number,
tokensOutput?: number,
tokensTotal?: number,
notes?: string, // freeform summary, capped at ~2000 chars
error?: string, // optional failure message
finalStatus?: "done" | "blocked" | "ready" | "in_progress" | "cancelled",
// optional override for the backlog item's status
}
Behavior:
- Look up the run; verify it's still open (
finished_at IS NULL). If already closed, errorRUN_ALREADY_FINISHED. - Resolve the backlog item and confirm its workspace matches the run's workspace.
- Update the run row:
finished_at=now(),outcome, tokens,notes,error. - Compute the final backlog status:
- If
finalStatusprovided: use it (validate it's a legalmarkdown_backlog_items.statusvalue). - Else if
outcome === "succeeded": set status todone. - Else if
outcome === "failed": set status toblocked. - Else: leave status as-is.
- If
- Write
audit_logrow (action: "task.completed",metadata: { outcome, final_status }). - Return
{ runId, finishedAt, finalStatus, tokensTotal }.
Token semantics
Take the agent at its word for tokensTotal — don't recompute from input+output. This matches Symphony's "prefer absolute thread totals" rule (SPEC.md §13.5) and avoids double-counting when models report cumulative totals natively.
If tokensTotal is omitted but tokensInput and tokensOutput are provided, compute total as input + output and store. If all three are present and inconsistent, prefer tokensTotal and don't error.
Idempotency
Closing an already-closed run is an error, not a silent no-op. The agent should know it tried to close something twice. If the operator wants to amend a closed run, they can do it via a future tRPC procedure — not through this tool.
Anti-goals
- No streaming updates. This tool runs once at session end.
- No "extend" or "renew" semantics. A long session that the agent thinks is still going should keep its run open by not calling
complete_task. Stall detection is the orchestrator's job (deferred).
Subtasks
- Created
apps/mcp-server/src/tools/complete-task.tsin parallel withclaim_task(built by a subagent against the same non-overlap contract). - Registered in
apps/mcp-server/src/tools/index.ts(alongsideclaim_taskin the parent's integration commit). - Implemented the 6-step behavior with zod validation: uuid
runId, outcome enum, optional non-negative integer token fields, 2000-char caps onnotes/error, optionalfinalStatusenum. - Audit log write inside the transaction (
action: "task.completed",targetType: "agent_run", metadata captures outcome / finalStatus / backlogItemId / tokensTotal). - Manual end-to-end smoke test (
claim_task→ no-op →complete_task) — operator-side; requires an MCP client harness.
Design decisions captured
- Closing an already-closed run is an error, not a silent no-op. Per spec. Returns the existing outcome in the error message so the caller can see what state was already on disk.
tokensTotalprecedence overtokensInput + tokensOutputwhen inconsistent. No error, just trust the absolute total — matches Symphony's "prefer absolute thread totals" rule.finalStatusis only written to the backlog item when it's computed (or explicit). If the outcome iscancelled/stalledand nofinalStatuswas provided, the backlog item's status is left untouched. Don't reach into status for nuances the caller didn't ask for.- Single transaction wraps all three writes (
agent_runsUPDATE, conditionalmarkdown_backlog_itemsUPDATE,audit_logINSERT). Partial closes are unreachable. actorUserIdis carried from the run, not from a fresh input. The actor that opened the run is the one credited for closing it. Prevents an agent from impersonating another actor at close-out.
Owner or assignee
Unassigned
Status
ready
Estimation
M
Acceptance criteria
- Successful close updates
finished_at,outcome, tokens, and (when appropriate) the backlog item's status. Confirmed by inspection of the transaction body. - Double-close errors out with a message naming the existing outcome ("Run already finished — outcome="). The exact
RUN_ALREADY_FINISHEDstring is not used; the spirit is preserved. - Token inconsistency is resolved by preferring
tokensTotal. Verified in the precedence ladder: explicit > input+output > null.
Links to related Epic / Plan
- Epic:
./Epic-mcp-claim-complete.md - Plan:
../Plan-agent-coordination.md