Tasks
Each Acolyte chat request becomes a tracked task with explicit states, validated transitions, queue policy, cancellation, and observable progress.
Lifecycle
accepted → queued → running → completed|failed|cancelled
Six states total:
| State | Terminal | Description |
|---|---|---|
accepted | no | request validated, task_id assigned |
queued | no | waiting under queue policy |
running | no | actively executing lifecycle |
completed | yes | finished successfully |
failed | yes | encountered an error |
cancelled | yes | aborted by client or connection close |
Transitions
Valid transitions (anything else is rejected):
accepted → queued, running, cancelled
queued → running, cancelled
running → completed, failed, cancelled
Terminal states allow no further transitions. Idempotent transitions (same state → same state) are always valid.
Transition reasons
| Reason | Trigger | Transition |
|---|---|---|
chat_accepted | RPC chat.start received | → accepted, then queued or running |
chat_started | worker begins execution | → running |
chat_completed | chat handler finished | → completed |
chat_failed | chat handler threw | → failed |
abort_requested | client sent chat.abort | → cancelled |
connection_closed | WebSocket disconnected | all active/queued → cancelled |
Shutdown
Any unfinished task — accepted, queued, or running — blocks shutdown.
POST /v1/admin/shutdownanswers{ ok: false, live: [{ taskId, sessionId }] }with 409 while work is unfinished{ "force": true }in the body stops the daemon regardless, abandoning that workacolyte stopasks first and escalates to SIGTERM when the daemon does not answer, rejects the request as unauthorized, or stays up after accepting it- a refusal exits non-zero; a refused
restartstarts no replacement, and a self-update never forces - a turn on
/v1/chat/streamregisters no task, so it does not block a stop
Queue model
Per-connection serial execution — one active task at a time, others queue up.
- max 25 queued tasks per connection (
RPC_MAX_QUEUED_TASKS_PER_CONNECTION) - FIFO ordering, 1-indexed positions reported to client
- aborted items marked and skipped on dequeue
- position updates broadcast to remaining clients after abort/dequeue
- different connections execute in parallel
Concurrency
- one
runningChatIdper connection runControl.shouldYield()checked during lifecycle — allows preemption for queued tasksrunControl.signalcarries the connection’sAbortController; aborting it stops the run in flight, not just its delivery- global
rpcQueuedTaskCountfor monitoring across all connections
Storage
- in-memory
TaskStore(Map-based, not persisted across restarts) - max 1000 tasks by default, oldest terminal tasks evicted first
TaskRecord—{ id, state, createdAt, updatedAt, sessionId }— immutable, replaced on transition
Task vs session
TaskId(task_*): server-generated, one per chat request executionSessionId(sess_*): client-provided, persists across requests for conversation continuity- one session can have many tasks over its lifetime
Key files
- src/task-contract.ts — task states, transitions, validation
- src/task-registry.ts — task creation, state machine, pruning
- src/task-store.ts — in-memory task persistence
- src/rpc-queue.ts — per-connection queue with abort and position tracking
- src/server-rpc.ts — RPC layer wiring tasks to lifecycle
Further reading
- Follow the Thread — how the trace tool went from a debugging script to a first-class CLI command