From 96d1c439ce27be80b751ea0085f53602606473d1 Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Mon, 9 Mar 2026 07:39:34 +0000 Subject: docs: update ADR-002 for parent-subtask BLOCKED→READY state behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Transition table: add BLOCKED→READY (trigger: all subtasks COMPLETED) - Transition table: clarify RUNNING→READY only when no subtasks exist - Transition table: add RUNNING→BLOCKED for parent-with-subtasks path - Execution outcome mapping: reflect subtask check - State diagram: show BLOCKED→READY arc - Key Invariants: add #7 parent-with-subtasks goes BLOCKED on runner exit Co-Authored-By: Claude Sonnet 4.6 --- docs/adr/002-task-state-machine.md | 54 ++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 22 deletions(-) (limited to 'docs') diff --git a/docs/adr/002-task-state-machine.md b/docs/adr/002-task-state-machine.md index debf7b0..1d41619 100644 --- a/docs/adr/002-task-state-machine.md +++ b/docs/adr/002-task-state-machine.md @@ -34,21 +34,21 @@ Terminal states with no outgoing transitions: `COMPLETED`, `CANCELLED`, `BUDGET_ POST /run │ POST /reject │ POST /cancel │ │ ┌────▼────┐ ┌──────┴─────┐ - ┌────────┤ QUEUED ├─────────────┐ │ READY │ - │ └────┬────┘ │ └──────┬─────┘ - POST /cancel │ │ POST /accept │ - │ pool picks up │ ▼ - ▼ ▼ │ ┌─────────────┐ - ┌──────────┐ ┌─────────┐ │ │ COMPLETED │ - │CANCELLED │◄──┤ RUNNING ├──────────────┘ └─────────────┘ - └──────────┘ └────┬────┘ - │ - ┌───────────────┼───────────────────┬───────────────┐ - │ │ │ │ - ▼ ▼ ▼ ▼ - ┌──────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────┐ - │ FAILED │ │ TIMED_OUT │ │ BUDGET │ │ BLOCKED │ - └────┬─────┘ └──────┬───────┘ │ _EXCEEDED │ └────┬────┘ + ┌────────┤ QUEUED ├─────────────┐ │ READY │◄─────────┐ + │ └────┬────┘ │ └──────┬─────┘ │ + POST /cancel │ │ POST /accept │ │ + │ pool picks up │ ▼ │ + ▼ ▼ │ ┌─────────────┐ │ + ┌──────────┐ ┌─────────┐ │ │ COMPLETED │ │ + │CANCELLED │◄──┤ RUNNING ├──────────────┘ └─────────────┘ │ + └──────────┘ └────┬────┘ │ + │ │ + ┌───────────────┼───────────────────┬───────────────┐ │ + │ │ │ │ │ + ▼ ▼ ▼ ▼ │ + ┌──────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────┐ │ + │ FAILED │ │ TIMED_OUT │ │ BUDGET │ │ BLOCKED │──all subtasks─┘ + └────┬─────┘ └──────┬───────┘ │ _EXCEEDED │ └────┬────┘ COMPLETED │ │ └─────────────┘ │ retry │ resume/ │ POST /answer │ retry │ │ @@ -65,7 +65,8 @@ Terminal states with no outgoing transitions: `COMPLETED`, `CANCELLED`, `BUDGET_ | `PENDING` | `CANCELLED` | `POST /api/tasks/{id}/cancel` | | `QUEUED` | `RUNNING` | Pool goroutine starts execution | | `QUEUED` | `CANCELLED` | `POST /api/tasks/{id}/cancel` | -| `RUNNING` | `READY` | Runner exits 0, no question file, top-level task (`parent_task_id == ""`) | +| `RUNNING` | `READY` | Runner exits 0, no question file, top-level task (`parent_task_id == ""`), and task has no subtasks | +| `RUNNING` | `BLOCKED` | Runner exits 0, no question file, top-level task (`parent_task_id == ""`), and task has subtasks | | `RUNNING` | `COMPLETED` | Runner exits 0, no question file, subtask (`parent_task_id != ""`) | | `RUNNING` | `FAILED` | Runner exits non-zero or stream signals `is_error: true` | | `RUNNING` | `TIMED_OUT` | Context deadline exceeded (`context.DeadlineExceeded`) | @@ -77,6 +78,7 @@ Terminal states with no outgoing transitions: `COMPLETED`, `CANCELLED`, `BUDGET_ | `FAILED` | `QUEUED` | Retry (manual re-run via `POST /api/tasks/{id}/run`) | | `TIMED_OUT` | `QUEUED` | `POST /api/tasks/{id}/resume` (resumes with session ID) | | `BLOCKED` | `QUEUED` | `POST /api/tasks/{id}/answer` (resumes with user answer) | +| `BLOCKED` | `READY` | All subtasks reached `COMPLETED` (parent task unblocked by subtask completion watcher) | ## Implementation @@ -89,12 +91,14 @@ write; called by both API handlers and the executor pool. **Execution outcome → state mapping** (in `executor.Pool.execute` and `executeResume`): ``` -runner.Run() returns nil AND parent_task_id == "" → READY -runner.Run() returns nil AND parent_task_id != "" → COMPLETED -runner.Run() returns *BlockedError → BLOCKED (question stored) -ctx.Err() == DeadlineExceeded → TIMED_OUT -ctx.Err() == Canceled → CANCELLED -any other error → FAILED +runner.Run() returns nil AND parent_task_id == "" AND no subtasks → READY +runner.Run() returns nil AND parent_task_id == "" AND has subtasks → BLOCKED (awaiting subtask completion) +runner.Run() returns nil AND parent_task_id != "" → COMPLETED +runner.Run() returns *BlockedError → BLOCKED (question stored) +ctx.Err() == DeadlineExceeded → TIMED_OUT +ctx.Err() == Canceled → CANCELLED +any other error → FAILED +all subtasks reach COMPLETED (from BLOCKED parent) → READY ``` ## Key Invariants @@ -123,6 +127,12 @@ any other error → FAILED are the only back-edges. Retry is manual (caller must call `/run` again); the `RetryConfig.MaxAttempts` field exists but enforcement is left to callers. +7. **Parent task with subtasks goes `BLOCKED` on runner exit.** A top-level task + that has subtasks transitions to `BLOCKED` (not `READY`) when its runner exits + successfully. It unblocks to `READY` only when all its subtasks reach + `COMPLETED`. This allows the parent to dispatch subtasks and wait for them + before presenting the result for user review. + ## Side Effects on Transition | Transition | Side effects | -- cgit v1.2.3