How the central queue works
hird is a local queue: one process per harness session, stdio,
no network. hird-server is the other thing — a process on a
host that two people reach from two machines, speaking MCP over HTTP, with
the queue's SQLite file underneath it. This page is that server, simulated
in your browser: every panel below runs against one shared board, the way
every connection runs against one shared file.
The shape of it
Ana's laptop Ben's cloud container
┌──────────────┐ ┌──────────────┐
│ Claude Code │ │ Codex CLI │
└──────┬───────┘ └──────┬───────┘
│ MCP / HTTP │ MCP / HTTP
│ Authorization: Bearer 3f9c… │ Authorization: Bearer a70b…
└───────────────┬─────────────────┘
│
┌──────────────▼───────────────────────────────┐
│ hird-server │
│ │
│ bearer() → roster lookup → endpoint │
│ (one per │
│ identity) │
│ │ │
│ Shared { Mutex<Db> } │
└──────────────────────┬───────────────────────┘
│
┌──────▼──────┐
│ SQLite │ one file, one
│ (WAL) │ compare-and-set
└─────────────┘
Four facts do all the work on this page, and the panels below are each one of them made visible:
- Nothing a connection says about who it is gets believed. A request carries a bearer token, the roster maps that token to a person, and the session is built from the roster's entry.
- Identity is fixed when the endpoint is built, not per call. The server assembles one MCP service per roster entry up front; dispatch is a map lookup on the token.
- There is still one SQLite file. Two people on two machines race through the same compare-and-set, and exactly one wins.
- Nothing routes. The server hands nobody anything. Work crosses machines because a human says file this on one side and pick up 42 on the other.
Watch a request land
Pick who is calling, which token they present, and what they ask for. The packet walks the same five stages the real server walks it through — and the board and trail at the bottom are the queue's own state, shared by every panel on this page.
1 · the wire
waiting…
2 · bearer()
("Bearer ")
3 · roster lookup
4 · the endpoint
5 · the queue
one SQLite file
What came back
—
Press Send.
What the queue ran
—
The board — project /srv/acme
| # | task | status | holder | lease |
|---|
The queue's clock runs at ×60 here, so a fifteen-minute lease lapses in fifteen seconds. Nothing else is sped up.
The trail — every event, append-only
Same rows hird events streams and the board's replay slider
reads. Assertions filed by mem_store are memory, not task
events, so they do not appear here.
The roster is the whole map
Authorization is a TOML file the operator edits, deliberately outside the queue's own database — the queue records what happened; who is permitted to make things happen is a different kind of fact, and keeping it outside means a compromised queue cannot grant access to itself. The last request you sent highlights the row it resolved to:
| token | identity | harness | project | capabilities |
|---|
# The project every worker files into unless it names its own.
project = "/srv/acme"
[[worker]]
# Generate with `openssl rand -hex 32`. Refused under 24 characters:
# this is the only thing between a stranger and the queue.
token = "3f9c1d0e…7b21"
identity = "ana"
capabilities = ["browser", "linux"]
[[worker]]
token = "a70b55c8…44de"
identity = "ben"
harness = "codex"
capabilities = ["linux"]
Parsing refuses the mistakes that would quietly mis-attribute work rather than accepting them: a duplicated token — two people the queue could not tell apart — a missing project, a token under 24 characters, an empty roster, a capability label the session would refuse. Each entry becomes its own MCP service at start-up, so by the time a request arrives there is nothing left to decide but which service to hand it to.
Capabilities are per token, and a worker that pins a harness may inherit a
set from a [harness.<name>] table — [harness.opencode]
says what every pinned OpenCode worker starts with, and the worker's own
labels add to it. Only the harness the operator pinned reaches that
table; the name a client gives itself never does, because a label is
something a human grants and the client's name for itself is the one thing
it is allowed to be wrong about.
A request with no token, and a request with a token the roster does not
know, get the same answer:
401, WWW-Authenticate: Bearer realm="hird", and
this queue is not open to unnamed callers. The same answer
either way, so the response does not say which tokens are real.
A client may not name the person
A local hird reads its identity from the environment it was
started in, because that environment belongs to the human running the
harness. A server has no such luxury: the environment is the server's, and
the far end of a socket can claim anything. So the rule is split. A client
may say what it is — being wrong there costs a badge — and may never
say who it is. Change what the client claims and watch the session
the server actually builds:
What the client sent
The session the server built
The actor string — ana/claude-code:z55d — is what lands on every
claim, event and assertion the session makes: the person from the roster, the
harness from wherever it was allowed to come from, and a fresh session suffix
per connection. There is no path from a connection to a name it was not
given.
The race, and who wins it
This is the load-bearing fact of the whole design, and going over HTTP does not bend it. Claiming is a single compare-and-set. Two agents on two machines reach for the same task in the same instant; both statements run against one file; one updates a row and the other updates none.
ana/claude-code — laptop
rows changed: —
ben/codex — cloud container
rows changed: —
Both requests are already inside the server when this runs — different tokens, different endpoints, different sessions. The only thing they share is the file.
Zero rows updated means somebody else won. An append-only trail merges
without coordination, but a claim is not an append — it is a consensus. Two
machines with async-replicated copies would both see open and
both win. One server in front of one file is why the swarm model still
holds across machines, and it is the reason the transport was built before
any sync was.
The lease, and what happens when a session goes quiet
A claim is a lease, not a lock. It lasts fifteen minutes by default, every
task_update renews it, and if the agent holding it crashes, is
killed, or simply wanders off, the task returns to open on its
own. Nothing sweeps in the background: expiry is enforced lazily, by whoever
calls next.
Nothing yet.
A lapsed lease is not swept by a timer — there is no timer. It sits there,
overdue, until the next call runs sweep_announcing() on its
way in, which is exactly what the second button does. Self-healing within
the TTL, and no background process to keep alive.
What it refuses to do
No TLS
It speaks plain HTTP and says so. Terminate TLS in a reverse proxy in front of it — a bearer token on an unencrypted connection is a token you have given away. Rolling certificates into the binary is how a server grows a configuration language.
No accounts, no token issuance
The roster is a file you edit. OAuth 2.1 is the right answer for a queue open to more than a few known people; a file is the right answer for two colleagues, and shipping the file keeps the question open honestly.
No witness
The witness reads a working tree, and the server's tree is nobody's
checkout. It prints a warning when the witness is on, because reporting
about the host's own directory would be the design's one genuinely false
report. Set witness = false.
Nothing routes
No scheduler, no placement, no assignment. task_next hands
out the next task worth doing to a caller who asked for one. The
queue chooses nobody.
It also carries no new surface: the same twelve MCP tools the local binary
exposes — task_list, task_get,
task_claim, task_next, task_scope,
task_split, task_release, task_update,
task_complete, task_fail, mem_store,
mem_search — and the same six statuses. A transport may not grow
the surface.
Running one yourself
hird-server --roster roster.toml --bind 127.0.0.1 --port 7474
hird-server on http://127.0.0.1:7474 (2 in the roster)
database: /var/lib/hird/hird.db
It is a second binary rather than a flag on the first, and that is load
bearing: hird-server carries axum, hyper and tower on its own
side of the line, while cargo tree -p hird names no HTTP
dependency at all. CI asserts exactly that, so the separation fails loudly
rather than eroding.
A harness then points at the URL with its token, and the server builds the session from the roster:
{ "mcpServers": { "hird": {
"type": "http",
"url": "https://queue.internal/",
"headers": { "Authorization": "Bearer 3f9c1d0e…7b21" }
} } }
The longer written account is the usage guide, DESIGN.md §31 for why it is shaped this way, and REMOTE.md for what was assessed and rejected on the way here.