All docs

Task state machine

Task state machine

Module: packages/core/src/task/state.ts Repository: packages/core/src/task/repo.ts (the only thing that writes to tasks) Schema enum: task_state in packages/db/migrations/0005_tasks.sql

States

Allowed transitions

                       deps met
            ┌────────┐──────────▶┌───────┐──atomic──▶┌─────────┐──heartbeat──▶┌─────────────┐
            │ blocked│           │ ready │           │ claimed │              │ in_progress │
            └───┬────┘◀─new dep──└──┬────┘◀lease─exp─└────┬────┘◀lease─exp────└──────┬──────┘
                │                   │                     │                          │ complete
                │                   │                     │                          ▼
                │                   │                     │                  ┌──────────────┐
                │                   │                     │     reopen ◀─────│ needs_review │
                │                   │◀────────────────────────────────       └──────┬───────┘
                │                   │                                               │ approve /
                │                   │                                               │ pass/skip
                │                   │                                               ▼
                │                   │                                       ┌──────────────┐
                │                   │                                       │  done        │ (terminal)
                │                   │                                       └──────────────┘
                ▼                   ▼
            ┌─────────┐ ◀───── (cancel from any non-terminal)
            │cancelled│ (terminal)
            └─────────┘

Full transition table — fromto is legal iff to ∈ TASK_TRANSITIONS[from]:

Illegal transitions throw InvalidTransitionError (code task.invariant_violated).

Invariants (DB-level CHECKs, schema 0005)

The repo layer is built to honour both — every UPDATE that flips state also touches the dependent columns in the same statement.

completeTask outcomes by verification kind

markNeedsReview is idempotent when the task is already in needs_review — it just flips the verification status (e.g. pending → failed after the verifier reports back).

Side-effect ordering

Inside db.transaction():

  1. The state UPDATE.
  2. agent_audit row for the action (task.created, task.claim, task.completed, …).
  3. Any dependent rows (task_dependencies, context.output).

After commit (out-of-band):

  1. redis.publish('pubsub:org:{orgId}:tasks', JSON.stringify(TaskEvent)) — task 33 SSE bridge consumes this.
  2. BullMQ verification enqueue (for automated kinds only).

A failed redis.publish or queue add does not roll back the state change — it only logs. The verification worker (task 32) sweeps verification_status='pending' tasks on startup to recover from missed enqueues.

What's NOT in this module

  • Lease / heartbeat hot path lives next door in lease.ts + reaper.ts (task 17).

The repo writes the durable columns; lease.ts mirrors them into Redis (claim:{org}:{task}) for fast verifyClaim; the reaper sweeps expired rows back to ready every 30 s.

  • Cycle detection in task_dependencies (task 18 — a generic DAG validator).
  • Knowledge edge linking when an agent completes a task with learningsRef (task 28).
  • Verification orchestration — the engine consumes the BullMQ jobs we enqueue (task 32).
  • `isBlockedByReview` is a stub returning false until task 29a (typed comments + amendments).