Skip to content

feat(tasks): autonomous task loop — assign → dispatch → drive → reconcile (#138)#204

Merged
Fullstop000 merged 15 commits into
mainfrom
feat/autonomous-task-loop
May 23, 2026
Merged

feat(tasks): autonomous task loop — assign → dispatch → drive → reconcile (#138)#204
Fullstop000 merged 15 commits into
mainfrom
feat/autonomous-task-loop

Conversation

@Fullstop000
Copy link
Copy Markdown
Owner

What

A human assigns a task to an agent and the agent autonomously wakes, works it, and drives its status — Linear-style — with the status staying truthful if the agent dies or stalls. Closes the upstream half of #138 (a fired task previously woke/assigned nobody).

The loop

  • Assignassign_task (one tx): sets assignee_agent_id + authoritative claim + makes the assignee the sole agent member of the task sub-channel + writes a directive (a normal message authored by the human assigner — the agent inbox excludes its own messages, so the author must differ from the assignee).
  • Dispatch — the directive wakes the agent via the existing chat-delivery path (no new bridge frame).
  • Drive — the agent advances status with the existing update_task_status MCP tool. New blocked status lets an agent self-report a blocker (cooperative resign); reconcile also sets it.
  • Reconcile — a 60s, store-only lease loop (tasks.last_progress_at, renewed when the assignee posts in its sub-channel): stale → re-dispatch (≤2 attempts) → else blocked (claim cleared, assignee kept). Lease, not process-liveness (process-existence ≠ "working this task").

New tasks columns assignee_agent_id, assigned_by_id, attempts, last_progress_at + idx_tasks_sub_channel; TaskStatus::Blocked; POST /api/conversations/{id}/tasks/{n}/assign (+ assigneeId on create); UI assignee picker + assignee/attempt/blocked display.

Design + review

Brainstormed → spec → plan, then two codex review passes (RETHINK → SHIP-WITH-FIXES) folded in (lease over process-liveness; atomic dispatch; sole-agent-member targeting; reuse existing restart-replay; own-message author rule). Spec/plan are local under docs/superpowers/.

Verification

  • Deterministic gates green: cargo clippy --all-targets -- -D warnings; Rust tests (server 86, e2e 10, store::tasks 16); UI tsc clean; vitest 135.
  • Live dogfood (the real bar): a real kimi agent, assigned a task via the API, autonomously woke → did the work → drove the card to in_review by its own update_task_status call, zero human pokes. The first run exposed a real gap (the directive didn't name which channel held the board) — fixed so the directive names the board channel + the exact tool call.

Deferred (P2)

Inbox decision on escalation; agent-to-agent delegation; process-liveness fast-path; hung-agent cancel/restart. A substantial code-task dogfood additionally needs the agent to have a real repo working directory.

🤖 Generated with Claude Code

Fullstop000 and others added 15 commits May 22, 2026 22:56
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the assignee agent posts into its task sub-channel, touch
last_progress_at. Non-assignee posts are silent no-ops (WHERE clause
self-restricts via assignee_agent_id match). Adds task_lease_is_stale
helper for the reconcile loop to detect stalled tasks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds `src/agent/reconcile_tasks.rs` with `spawn_task_reconciler` that
ticks every 60 s, calls `list_stale_assigned_tasks(600)`, and for each
stale task either re-dispatches (attempts < 2) or escalates, publishing
the returned StreamEvents through the EventBus so the agent is woken.
Wired into `serve.rs` after `spawn_trace_writer`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `blocked` to TaskStatus union, NEXT_STATUS, ADVANCE_LABEL, STATUS_LABEL
  in TaskDetail and TaskEventMessage; seed groupTasksByStatus with a blocked
  bucket so blocked tasks are never silently dropped
- Add `assigneeName: string | null` and `attempts: number` to TaskInfo DTO
- Add `blocked` column to TasksPanel board (styled with --color-destructive)
- Add assignTask() client: POST /api/conversations/{id}/tasks/{n}/assign
- TaskDetailView: render assignee picker (select populated from listAgents()),
  show "assigned to <name>" when set, show "attempt N" chip when attempts > 1,
  render blocked status chip via data-status="blocked" + CSS warning token
- Fix UpdateTaskStatusRequest to reference TaskStatus (was a hard-coded union)
- Vitest: 19 tests in TaskDetail covering new paths; all 132 tests green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…us change; assign-on-create test; unify blocked token

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tus call

Dogfood found the assigned agent did the work but couldn't close the task:
the board lives on the parent channel while the agent is sole member of the
sub-channel, so its list_tasks/update_task_status hit the empty sub-channel
board. The internal task routes are claimer-gated (no parent-membership
needed), so naming the board channel + the explicit update_task_status call
in the directive lets the agent drive status to completion.
CI Format check failed on the merge commit; rustfmt-only, no behavior change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- CHANGELOG: autonomous task loop (#138)
- VERSION: 0.16.1 -> 0.17.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Fullstop000 Fullstop000 merged commit 36eb5dd into main May 23, 2026
3 checks passed
@Fullstop000 Fullstop000 deleted the feat/autonomous-task-loop branch May 23, 2026 05:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant