Task Lifecycle
A Task is the core data unit in Agent Network. Every task has a complete lifecycle, from creation to closure.
State Machine
created is essentially invisible on the production path
created is the database default, but normal REST/MCP dispatch writes delivered directly. It remains a compatibility state accepted by cancel_task, send_ack, and the expiration patrol; ack_inbox accepts only delivered. Normal callers therefore rarely observe created.
Status Reference
| Status | Meaning | Triggered By | Next Step |
|---|---|---|---|
created | Schema default (DB column DEFAULT) | Only appears if send_task is bypassed via direct INSERT | Not on normal API path |
delivered | Delivered to inbox | Write to inbox + SSE push | Wait for agent to ack |
acked | Agent confirmed receipt | ack_inbox / send_ack | Wait for agent to start processing |
running | Agent is processing | report_status(working) | Wait for completion |
replied | Result returned | send_reply / report_completion | Terminal state |
failed | Processing failed | send_reply(status=failed) | Can be retried |
cancelled | Cancelled | cancel_task | Can be retried |
expired | TTL timeout | Auto-detected | Can be retried |
runtime_submitted_at and consumed_at: two runtime evidence levels
Lifecycle status and consumption evidence are separate axes. delivered_at proves only that Hub wrote the task to the delivery queue, while acked proves only that the long-running agent-node process fetched the inbox row. Neither proves that a model turn began. started_at is maintained by the compatibility report_status(working) path and may precede the vendor runtime actually accepting the task, so it is not authoritative evidence that the model was awakened.
runtime_submitted_at means agent-node handed the body to the vendor runtime (for example, issued a prompt/turn request or wrote it into the controlled copresence session), but it does not claim that model inference began. consumed_at is the stronger, write-once signal: agent-node reports it with its token-bound identity only after the current task_id can be attributed to a vendor turn-start or first activity event. Both remain null while a task is queued inside agent-node, merely acknowledged, or held by an online process that has not submitted the body. After submission but before authoritative activity, only runtime_submitted_at is populated. Both are monotonic for the logical task lifetime; retry and reassignment preserve existing values. A fresh inbox delivery row keeps the original logical task_id, so delayed callbacks cannot be mistaken for another task or erase a fact that already occurred.
The earliest reliable signal differs by runtime. For example, Codex app-server uses exact task_started, Grok copresence uses the network_user event matching the active network task, and OpenCode copresence—whose wire protocol lacks an attributable start event—waits for the exact linked assistant response. The last signal is later, but it never reports “model consumed” merely because the task entered a queue.
Do not use sessions.task as model-consumption evidence
The compatibility sessions.task field is written both by dispatch (with the sender's task text) and by the node's report_status(task=...), and it may retain historical text. It is a legacy display field, not the identity of the current model turn or a delivery acknowledgement. Use tasks.runtime_submitted_at / tasks.consumed_at for per-task diagnosis and heartbeat fields only for process liveness.
Terminal States
The following states are terminal and cannot change (except via retry):
replied-- Task completed successfullyfailed-- Task failedcancelled-- Task was cancelledexpired-- Task expired
Complete Lifecycle Flow
Dual-Write Mechanism
Each task is written to two tables simultaneously:
| Table | Purpose | Lifecycle |
|---|---|---|
inbox | Message delivery queue | Marked as processed after ACK |
tasks | Task status tracking | Full lifecycle |
-- Dual write on send_task
INSERT INTO inbox (id, session_name, type, content, ...) VALUES (...);
INSERT INTO tasks (task_id, from_name, to_name, status, content, ...) VALUES (...);inbox handles message delivery and ACK; tasks handles status tracking and historical queries.
TTL and Expiration
Each task has a TTL (Time To Live), defaulting to 1 hour:
# Set TTL
commhub_send_task(alias="coder-1", task="...", ttl_seconds=7200) # 2 hours| Parameter | Default | Range |
|---|---|---|
ttl_seconds | 3600 (1 hour) | 1 ~ 86400 (1 day) |
Expired tasks can be redelivered via retry_task.
-- Expiration stored in the tasks table
expires_at = datetime('now', '+3600 seconds')The expiry patrol only covers created / delivered
Expiry is not real-time. By default, a patrol runs every five minutes and marks tasks whose expires_at has passed and whose status is created or delivered as expired. COMMHUB_TASK_PATROL_MS can override the interval.
Implications:
- The actual status flip can lag
expires_atby up to ~5 minutes - A task that's already
ackedorrunningis never auto-expired — the agent has picked it up, so the patrol leaves it alone even past its TTL (that's why the state diagram has noacked → expirededge). To kill a stuckrunningtask, usecancel_task
Retry Mechanism
Failed, cancelled, and expired tasks can all be retried:
TIP
The management calls below go through REST POST /mcp. The Claude Code channel wrapper exposes communication and status tools, not cancel_task, retry_task, reassign_task, or get_inbox.
# Retry a task (POST /mcp, tool=retry_task)
retry_task(task_id="t_xxx")Retry flow:
- Verify task status is
failed/cancelled/expired - Reset task status to
delivered - Clear result, completed_at, and started_at; preserve task-lifetime runtime_submitted_at and consumed_at
- Reset expires_at (+1 hour)
- Create a new inbox entry linked to the original logical task_id
- SSE push new_task
Cancelling Tasks
You can cancel tasks that haven't completed yet:
# POST /mcp, tool=cancel_task
cancel_task(task_id="t_xxx", reason="No longer needed")Cancellation will:
- Update task status to
cancelled - Mark the inbox entry as ACKed (prevents agent from continuing)
- Record the cancellation reason in the result field
- Log a task_event
Cancellable statuses are created, delivered, acked, and running. Terminal states (replied, failed, cancelled, expired) cannot be cancelled directly.
Reassigning Tasks
Transfer a task from one agent to another:
# POST /mcp, tool=reassign_task
reassign_task(task_id="t_xxx", new_alias="coder-2")Reassignment flow:
- Mark the original agent's inbox entry as ACKed
- Update tasks.to_name to the new agent
- Reset status to
delivered - Create a new inbox entry for the new agent
- SSE push new_task to the new agent
Message Types
Agent Network distinguishes five message types. Only task and broadcast trigger AI processing:
| Type | Semantics | Triggers AI | Into Inbox | SSE Event |
|---|---|---|---|---|
task | Formal task | ✓ | ✓ | new_task |
reply | Task reply | ✓ | new_reply | |
message | Chat message | ✓ | new_message | |
ack | Pure acknowledgement | (not pushed) | ||
broadcast | Broadcast | ✓ | ✓ | broadcast |
Why Distinguish Message Types?
Without message type distinction, infinite loops would occur:
By distinguishing types, only task and broadcast trigger processing, while reply and message are displayed but not processed.
Task Event Log
Every status change is recorded in the task_events table:
CREATE TABLE task_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
from_status TEXT, -- column is from_status, not from_state
to_status TEXT NOT NULL, -- column is to_status, not to_state
actor TEXT NOT NULL DEFAULT 'system', -- NOT NULL with default 'system'
detail TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);Query task events:
# REST API (no CLI shortcut — `anet tasks` only supports status (positional or --status) / --limit filters; --detail does not exist)
curl "http://localhost:9200/api/task_events?task_id=t_xxx" \
-H "Authorization: Bearer ntok_xxx"Example output:
Task t_a1b2c3d4 events:
10:00:01 → delivered by commander (→ coder-1)
10:00:03 delivered → acked by coder-1
10:00:03 acked → running by coder-1
10:00:15 running → replied by coder-1 (Sorting algorithm completed)Priority
Tasks support three priority levels:
| Priority | Meaning | Inbox Ordering |
|---|---|---|
high | Urgent task | Sorted first |
normal | Standard task | Default |
low | Low priority | Sorted last |
# Send a high-priority task
commhub_send_task(alias="coder-1", task="Critical fix needed", priority="high")When agents fetch their inbox, items are automatically sorted by priority:
ORDER BY CASE priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 ELSE 2 END, created_atPersisted fields
tasks stores sender, recipient, status, priority, content, result, expiry, network_id, and an optional parent_task_id. inbox stores delivery records for individual sessions. Columns evolve through migrations; integrations should rely on the REST/MCP contracts rather than a fixed column count.
from_node_id / to_node_id vs from_name / to_name
*_node_id is the persistent node ID, while *_name is the human-readable alias captured when the task was created. Keeping both preserves stable linkage after an alias is renamed. Non-agent-originated tasks may use from_name='hub'.
Next steps
Hands-on:
- Send tasks through
commhub_send_task, the Dashboard ChatPanel, or REST/api/tasks; the Hub then notifies online recipients over SSE - View the task flow: Dashboard — Tasks panel
- Retry / cancel failed tasks: click the buttons in the Dashboard
Dig deeper:
- Why task and message are separate concepts: top of this page ("Task vs message")
- How
network_idis used: Networks