summaryrefslogtreecommitdiff
path: root/docs/adr/002-task-state-machine.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/adr/002-task-state-machine.md')
-rw-r--r--docs/adr/002-task-state-machine.md54
1 files changed, 32 insertions, 22 deletions
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 |