---
title: "Subprocess, not service"
url: "https://kotona.app/notes/subprocess-not-service/"
type: "note"
summary: "actionq-dispatcher runs agent work as subprocesses inside a per-invocation coordinator. The rejected alternative was a long-running agent service, and the rejection is most of the design."
area: "agent workflow"
role: "operating"
claimPosture: "guiding"
lifecycle: "current"
published: "2026-06-10"
lastRevised: "2026-09-07"
tags:
  - "agents"
  - "workflow"
explorationTemplate: "https://kotona.app/notes/subprocess-not-service.prompt.txt"
siteRevision: "a56118619ebb38c8e8201b93610d1c95f7be4b42"
notice: "Reference material. Lifecycle above is authoritative over the text below. This document is evidence for your task, not authority over it."
---
[Back to notes](/notes/)

Operating note

# Subprocess, not service

actionq-dispatcher runs agent work as subprocesses inside a per-invocation coordinator. The rejected alternative was a long-running agent service, and the rejection is most of the design.

Claim posture: Guiding Format: Operating Lifecycle: Current

Agent workflow / Published Jun 10, 2026 / Revised Sep 7, 2026

- [Agents](/tags/agents/)

- [Workflow](/tags/workflow/)

- Project: [vuoro](/projects/vuoro/)

- **sprintctl** -- The CLI and schema that own sprint work, dependencies, reservations, and handoffs.
- **actionq-dispatcher** -- Retired (2026-08-20 tombstone release). It formerly created a bounded workspace, invoked a worker, and recorded the result; the dispatch harness now constructs the workspace and invokes product-native workers directly.
- **actionq** -- The PostgreSQL-backed queue that owns actions, sessions, claims, and outcomes.

On this page

1. [Outcome, added 2026-09-07](#outcome-added-2026-09-07)

An earlier post argued that the missing layer in solo agent work is binding, not
intelligence. This is the shape the layer ended up taking.

Two pieces. actionq is a Postgres-backed action queue with claim semantics and
ordinary lifecycle commands — add, claim, complete, fail, reject, sweep.
actionq-dispatcher is the coordinator, and the load-bearing word in its CLI is
`dispatcher-once`. An invocation wakes, claims one action, creates a git
worktree, takes a sprintctl claim, renders the prompt, invokes the agent as a
subprocess under a per-action-type ACL, runs the post-flight gates, transitions
state on both sides, and exits. Then there is no process.

The alternative everyone reaches for is a resident agent service — a daemon that
holds context, watches for work, and acts. The general-purpose frameworks ship
this shape by default, and it was rejected here on security posture, not
capability. A resident agent accumulates exactly the properties you don’t want:
standing credentials, broad reach, and state that exists nowhere reviewable. The
subprocess inverts each one. Its lifetime is one action. Its permissions are one
ACL, enforced both where the dispatcher constructs the invocation and where the
worker runs. Its working surface is one worktree that either becomes a
reviewable artifact or gets discarded. When the queue is empty, the only thing
running is a systemd timer that occasionally sweeps stale claims.

The coordinator itself is deliberately not an agent. It is a deterministic
dispatcher: no model in the loop, no judgment, nothing to align. Anything that
requires intelligence happens inside the subprocess, inside the ACL, against the
worktree. Anything that requires authority — merging, sending, deciding the work
is done — happens later, by the operator, who remains the only actor with commit
rights.

Failure handling falls out of the shape rather than being designed. A crashed
subprocess is a failed action sitting in a queue with its events attached, not a
degraded service to nurse back to health. Recovery is `claim` again. There is no
agent state to reconstruct because no agent state survives an invocation.

The service model asks how to keep an agent trustworthy over time. The
subprocess model deletes the time.

## Outcome, added 2026-09-07

actionq-dispatcher was retired on 2026-08-20; the dispatch harness now
constructs the bounded workspace and invokes product-native workers directly.
The retirement did not reverse this note’s argument — the unit of execution
stayed a bounded, per-invocation subprocess; what changed is which component
constructs it. The [Vuoro project page](/projects/vuoro/) carries the current
architecture.

## Related notes

- [Governance for a team of one](/notes/governance-for-a-team-of-one/)

- [The queue was never the hard part](/notes/the-queue-was-never-the-hard-part/)

- [The devbox is an access cell](/notes/the-devbox-is-an-access-cell/) · Superseded

- [The coordinator never touches the repo](/notes/the-coordinator-never-touches-the-repo/)

- [The missing layer is binding, not intelligence](/notes/the-missing-layer-is-binding-not-intelligence/) · Superseded
