463 lines
24 KiB
Markdown
463 lines
24 KiB
Markdown
# Subagent daemon
|
|
|
|
`hive-subagent-daemon` (crate `hive-subagent-mcp`) spawns nested headless
|
|
`claude` sessions on request. Own process, own systemd unit, own MCP
|
|
server (`subagent`, not `hyperhive`) — independent of `hive-bash-daemon`:
|
|
a subagent is a full nested claude process, a materially heavier
|
|
capability than a background shell command, so it gets its own
|
|
deployable/restartable unit rather than living inside the bash daemon.
|
|
|
|
Shipped default-on for every agent — `nix/agent-modules/mcp.nix` injects
|
|
`subagent` into `services.hyperhive.agent.extraMcpServers` via `lib.mkDefault`
|
|
(`allowedTools = ["*"]`), same as `bash`. Default-on rather than
|
|
unconditional: an `agent.nix` can override or drop the entry, which is
|
|
what `mkDefault` is there for.
|
|
|
|
For what the tools do and when an agent should reach for them, see the
|
|
`subagent` MCP server's own tool descriptions and the
|
|
`base:claude-subagents` skill — this page covers the daemon as deployed
|
|
infrastructure, not the agent-facing API.
|
|
|
|
## Tools
|
|
|
|
Served under the `subagent` MCP server (`mcp__subagent__<tool>`): `start`,
|
|
`continue`, `status`, `interrupt`.
|
|
|
|
A second route on the same port serves the two tools a **subagent** calls
|
|
about its own run — `goal_reached` and `need_help`. It isn't part of the
|
|
`subagent` server an agent's own config points at; the daemon writes it
|
|
into each subagent's `--mcp-config` itself, under `subagent_control`. Two
|
|
routes rather than six tools on one, so that being able to report on a run
|
|
never carries the ability to start one: there's no route a subagent holds
|
|
that `start` is reachable from.
|
|
|
|
## A subagent can't name a session, not even its own
|
|
|
|
Neither signal tool takes a session name. **The URL is the identity.** At
|
|
each spawn the daemon mints that run an unguessable token, serves it at
|
|
`/signal/mcp/<token>`, and writes that one URL into that one subagent's own
|
|
`--mcp-config` — a file per session, not a shared one. The daemon resolves
|
|
a request to a session before dispatching it, and the tools read the
|
|
session off the resolution.
|
|
|
|
A subagent therefore has no field in which to name a sibling, and knowing
|
|
a sibling's name buys nothing: a name isn't a token. Two subagents running
|
|
concurrently can't signal each other — which the earlier shape, a shared
|
|
route plus a `name` argument guarded by a liveness check, allowed.
|
|
|
|
A token that resolves to nothing — never minted, or revoked when its run
|
|
ended — gets a bare **404**, the same answer for every token, so nothing
|
|
about the refusal says whether some other session exists.
|
|
|
|
## State
|
|
|
|
In-memory only: what's running now, where each name's session lives, how
|
|
each name's last turn ended, when each running turn last produced output,
|
|
what each session is working toward, how far through its turn budget it
|
|
is, why its run stopped, where it writes its report, and which signal
|
|
token belongs to it. All of it lives only as long as the daemon process
|
|
does — so a daemon restart invalidates every signal URL it had issued,
|
|
which is the same thing as it having stopped the runs those URLs belonged
|
|
to. A daemon
|
|
restart stops whatever was running rather than adopting it. The durable
|
|
record of a subagent's existence is claude's own on-disk session
|
|
(`hive_claude::SessionStore`), which `continue` reattaches to independent
|
|
of the daemon's own lifetime — a restart loses the _in-flight turn_, not
|
|
the subagent's history.
|
|
|
|
`continue`, and each later turn of a goal run, hands claude a session id,
|
|
not the name: the id of the live session carrying that name in `dir`'s
|
|
project directory, read off disk at every resume. claude looks a bare name
|
|
up across every git worktree of the repo and refuses when two sessions
|
|
match, and `start` archives a prior session only in its own directory, so
|
|
a name reused under a sibling worktree would otherwise stop resuming. When
|
|
`dir` holds no session under the name, the daemon refuses and never hands
|
|
claude the bare name — see below.
|
|
|
|
Because the remembered directory goes with the rest of it, a `continue`
|
|
after a restart has to re-supply `dir` when the session lives anywhere
|
|
other than the daemon's own working directory — and a restart is the
|
|
situation you reach for `continue` in most often.
|
|
|
|
## Roles
|
|
|
|
A `start` may name a **role**: `role: "reviewer"` runs that subagent under
|
|
the spawning agent's own `reviewer.md`. Role files live in one directory
|
|
per agent, `subagent_roles/` inside that agent's state directory, one
|
|
markdown file per role, and the `role` argument is the filename without
|
|
its extension.
|
|
|
|
One convention directory of named files, because a named set enumerates.
|
|
Anything that watches roles — a check for a safety clause that has gone
|
|
stale, an operator asking what this agent can dispatch — lists the
|
|
directory and has the answer. Per-agent rather than hive-wide: the clauses
|
|
a role carries come out of that one agent's own prompt files, and the
|
|
daemon only ever spawns on that agent's behalf.
|
|
|
|
**The task is the turn, never a standing system prompt — with or without a
|
|
role.** A task baked into the system prompt re-asserts itself as an
|
|
instruction on every later turn of the session (`continue`, `goal`), not
|
|
just the one the caller wrote it for — the system prompt is the
|
|
subagent's standing identity, not a one-shot channel, so the daemon always
|
|
reads the task instructions (`prompt_file`) and folds them ahead of the
|
|
turn's own prompt instead, whether or not the session has a role. The
|
|
caller's own task file stays untouched either way.
|
|
|
|
A role additionally names a standing identity: its text, alone, becomes a
|
|
per-session file under the harness directory, and `--append-system-prompt-file`
|
|
points at that — nothing about the task ever reaches it. Without a role
|
|
there is no such identity to hold a file open for, so the daemon omits
|
|
`--append-system-prompt-file` altogether rather than pointing it at
|
|
anything task-shaped.
|
|
|
|
⚠️ This applies to every dispatch, not just role-bearing ones.
|
|
|
|
### A role with no file refuses the call
|
|
|
|
A named role with no file **fails the `start`** and names the roles the
|
|
directory does hold. No fallback, no spawn, no reservation — the refusal
|
|
lands before the daemon claims the session name, so a caller that fixes
|
|
the name may retry it immediately.
|
|
|
|
The reason is the state every agent starts in. The directory starts
|
|
empty: hyperhive ships no role files, and nothing creates one until an
|
|
agent writes its own, so a name with no file behind it counts as the
|
|
ordinary first-run answer rather than a rare corruption. Falling back to a spawn
|
|
without the role would hand that subagent a prompt missing every clause
|
|
the role existed to carry, and would do it quietly. An empty role file
|
|
draws the same refusal, for the same reason.
|
|
|
|
## Goals, and turns toward them
|
|
|
|
`start` takes an optional `goal`. Without one a session is a single turn,
|
|
exactly as it always was. With one, the daemon keeps the session going:
|
|
when a turn ends and nothing has said to stop, it starts another turn
|
|
re-prompting the subagent toward that goal, quoting it verbatim and saying
|
|
which turn of the budget this is. `max_turns` caps that, defaulting to
|
|
**5**.
|
|
|
|
`status` reports `Turn N of M` for such a session in every state it
|
|
reaches. Read alongside the last-event age below, it's what separates a
|
|
subagent that's working from one that's wedged from one that's out of
|
|
turns — without `ps` and without opening a file.
|
|
|
|
Five things stop a run; the daemon records each distinctly, reports it via
|
|
`status`, and appends it to the one todo it pushes when the run ends:
|
|
|
|
- **the turn ended and there was no goal** — the single-turn case;
|
|
- **`goal_reached`**, which the subagent calls itself;
|
|
- **`need_help`**, likewise;
|
|
- **the turn cap**, which says so rather than stopping quietly: the todo
|
|
states that the run hit the harness limit and the goal was never
|
|
reported reached, so the work stopped where it had got to;
|
|
- **`interrupt`**, which cancels the whole run and not merely the turn
|
|
that was in flight.
|
|
|
|
A killed or failed turn ends the run too, and keeps the records it already
|
|
had — see [A killed turn](#a-killed-turn).
|
|
|
|
`interrupt` being on that list is load-bearing, not bookkeeping. Killing
|
|
the process isn't by itself a cancel: `interrupt` sends SIGINT by
|
|
default, and `claude --print` handles SIGINT by writing its terminal
|
|
result event and exiting **zero**, which is indistinguishable from a turn
|
|
that finished. `interrupt` therefore records the cancel before it
|
|
signals, and the continuation loop reads that record rather than guessing
|
|
from how the child exited. `continue` clears it and hands the session a
|
|
fresh turn allowance, which is what makes an interrupt a pause you can
|
|
undo rather than a session you have to abandon.
|
|
|
|
On `claude`, the signal is always SIGINT, and it goes to claude's whole
|
|
process group. The signal doesn't reach a command that moved itself out of
|
|
the group (`setsid`), and claude's Bash tool starts every command that way:
|
|
claude stops those commands itself when it shuts down on SIGINT. A SIGKILL
|
|
would leave them running, so nothing escalates to one. `interrupt` returns
|
|
at once, and the name stays claimed until claude has exited: `status` says
|
|
**stopping**, and the daemon refuses `start` and `continue` on that name
|
|
until then. A claude that ignores the SIGINT stays stopping until someone kills
|
|
it by hand or the unit stops.
|
|
|
|
When the session knows where its report goes — `start`'s `report_file`,
|
|
or the path the subagent names when it signals — the daemon appends the
|
|
stop reason to that file as well, so the artifact you were going to read
|
|
anyway also says how the run ended. The daemon infers nothing: with no
|
|
path given, it touches no file.
|
|
|
|
## `goal_reached` is a label, not a gate
|
|
|
|
Both signals stop the continuation and **extend** the done message. Extend,
|
|
not replace: the turn's observed end and the reason the run stopped are
|
|
different facts, and the second never stands in for the first.
|
|
|
|
`goal_reached` is **self-reported**, by a subagent that has just been
|
|
re-prompted with "you haven't reached the goal" — which is precisely the
|
|
incentive to claim it. It's the same failure class as a build report
|
|
asserting "done, tests pass": a claim about an artifact, not the artifact.
|
|
Nothing in this daemon treats it as verification, and every surface that
|
|
renders it says so. Read the diff and the gate output regardless.
|
|
|
|
`need_help` is the blocking signal. It stops the run and shows up in
|
|
`status` as its own state — blocked, with the subagent's reason — so a
|
|
parent that polls `status` sees the block without reading anything else.
|
|
`continue` is how you answer it.
|
|
|
|
## Is it working, or is it wedged?
|
|
|
|
`status` reporting **running** says the daemon tracks a process, which a
|
|
wedged subagent satisfies as fully as a busy one. A running answer therefore
|
|
carries the age of that turn's last event too: seconds means it's working,
|
|
an age climbing into the minutes means it's stuck. That one number
|
|
replaces inferring the same thing from `ps` output and CPU-time deltas.
|
|
It resets at each turn's spawn, so on a goal run it describes the turn in
|
|
flight rather than the run — which is what you want, since a run that's
|
|
making progress spends several perfectly healthy minutes.
|
|
|
|
Every line the subagent's `claude` process writes bumps the timestamp —
|
|
stream-json events, plain stdout chatter and stderr alike — and what the
|
|
subagent actually said is never read. The record says the child is alive,
|
|
not what it's doing. The clock starts at the spawn, so a subagent that wedged
|
|
before it ever emitted anything still reports a climbing age rather than
|
|
no age at all. It's dropped when the turn ends, since a finished turn has
|
|
no progress left to describe.
|
|
|
|
## A `continue` that finds no session
|
|
|
|
`continue` looks for the session before it spawns anything. The lookup is
|
|
the live session titled the name in the project directory of the `dir` in
|
|
effect: the one passed, or else the one the daemon remembers for the name.
|
|
No such session refuses the call — nothing spawns, the name stays free
|
|
for a retry, and the session keeps its `status` (stop reason and turn
|
|
budget) and its remembered `dir` — and the error names the directory
|
|
searched:
|
|
|
|
```
|
|
continue error: no session titled `n` under /home/agent/.claude/projects/-home-agent-work; pass the `dir` the session was started in
|
|
```
|
|
|
|
A name is never searched for in any other directory, because claude would
|
|
resolve a bare title across every worktree of the repo and could pick, or
|
|
refuse, the wrong session. A session started in another directory
|
|
needs that directory passed as `dir` — and after a daemon restart, which
|
|
forgets the remembered one, a session outside the daemon's own working
|
|
directory needs it again. A store location the daemon can't resolve
|
|
refuses the same way, with the io error.
|
|
|
|
A goal run's next turn does the same lookup; a miss there ends the run as
|
|
a failed turn, with the same message in its end-of-turn todo.
|
|
|
|
The refusal covers a session missing from the directory searched.
|
|
It can't cover one that vanishes between the lookup and the spawn, so
|
|
`continue` still waits for claude's answer instead of returning the
|
|
instant the process exists. Where `start` returns at the spawn — it
|
|
creates its session, so the spawn succeeding is the whole story — a
|
|
resumed turn can fail a moment _after_ a pid exists. `continue` holds the
|
|
tool call open until the turn is underway or the resume has come back
|
|
missed, and reports a miss as the call's own error, carrying claude's
|
|
message plus the location this daemon searched:
|
|
|
|
```
|
|
continue error: claude error: no session matched the requested id or title (searched /home/agent/.claude for cwd /home/agent/work; if it was started elsewhere, pass the `dir` it was started in)
|
|
```
|
|
|
|
The directory is the part claude's own message never names, and the part
|
|
that resolves the confusion: pass `dir` to point `continue` at the
|
|
directory where the session began.
|
|
|
|
Nothing here is a fixed delay on the way to a successful turn. The wait
|
|
ends on whichever comes first — the turn's first stream event or its
|
|
early exit — and on this box both land inside a second, so a `continue`
|
|
that works answers about as fast as it did before. A five-second cap
|
|
bounds the one case neither covers: a child that neither speaks nor
|
|
exits, reported as started, with the end-of-turn todo left to say how it
|
|
goes. That todo carries every failure that happens after the one
|
|
`continue` already returned to the caller.
|
|
|
|
## A killed turn
|
|
|
|
A subagent whose `claude` process dies on a signal — the kernel's OOM
|
|
killer under memory pressure, a stopped unit, an `interrupt` from the
|
|
owning agent — didn't finish its turn, and the daemon says so rather
|
|
than letting it settle back into `idle`:
|
|
|
|
- `status` reports the session **killed**, naming the signal, instead of
|
|
the `idle` it reports for a turn that ended on its own;
|
|
- the end-of-turn todo the daemon pushes unprompted says something killed
|
|
the subagent mid-turn, not that it finished;
|
|
- `continue` still resumes such a session — often what you want — but
|
|
its reply says something killed the previous turn, so nobody carries on
|
|
from cut-off work believing it was complete.
|
|
|
|
The record is per-name, in memory with the rest of this daemon's state,
|
|
and the next confirmed spawn under that name clears it. A daemon restart
|
|
loses it along with everything else — the daemon has to outlive the kill
|
|
to report it, which is what `OOMPolicy=continue` on the unit is for.
|
|
|
|
## Compaction trade-off
|
|
|
|
<!-- vale write-good.Passive = NO -->
|
|
|
|
Built on `hive_claude::Claude::spawn` + `RunningClaude::wait` directly
|
|
rather than `InfiniteSession::run`, since only the low-level driver
|
|
exposes a cancel handle to stop a turn mid-flight — that's what makes
|
|
`interrupt` genuinely stop a running turn rather than only cancelling a
|
|
still-pending one. The cost: a turn that overflows the context window
|
|
surfaces as an error rather than self-healing via reactive compaction.
|
|
Subagents are meant to be bounded, single-batch work, not sessions
|
|
long-lived enough to need in-place compaction — a real follow-up if that
|
|
assumption stops holding.
|
|
|
|
<!-- vale write-good.Passive = YES -->
|
|
|
|
## Configuration
|
|
|
|
`services.hyperhive.agent.mcp.subagentHttpPort` — the daemon's streamable-http listen
|
|
port. Same pattern as `bashHttpPort`/`matrixHttpPort`: a per-agent default
|
|
assigned by `nix/agent-modules/mcp.nix`, only worth overriding for an
|
|
agent that needs a stable or non-default port.
|
|
|
|
Own systemd unit, defined alongside the other per-agent MCP daemons in
|
|
`nix/agent-modules/mcp.nix`.
|
|
|
|
## Which models a subagent may run on
|
|
|
|
The daemon refuses a `start` or `continue` that names a `model` outside the
|
|
agent's list before anything spawns, and the error names the list and where
|
|
it came from. One without a `model` runs on claude's own default, which the
|
|
daemon doesn't check. The list is:
|
|
|
|
- the swarm's `subagent_models` for this agent, when it sets one
|
|
([`inference.md`](../swarm/inference.md)). It replaces the agent's own
|
|
list, so it can name models that list leaves out.
|
|
- otherwise `services.hyperhive.agent.availableModels`, which reaches the
|
|
daemon as `HIVE_AVAILABLE_MODELS`. An empty list restricts nothing.
|
|
|
|
The daemon reads the swarm's value on each such call, so a change applies
|
|
to the next one. When the read fails, or doesn't answer within 2 seconds,
|
|
the daemon logs a warning and checks against `availableModels`. The same
|
|
happens until the daemon's first connection to the swarm queue succeeds.
|
|
The match ignores case. On an ACP agent the names are the agent's own, such
|
|
as `<provider>/<model>` on `opencode`.
|
|
|
|
## How many subagents may run at once
|
|
|
|
When the swarm sets `max_subagents` for the agent
|
|
([`inference.md`](../swarm/inference.md)), the daemon refuses a `start`, or a
|
|
`continue` of a session that isn't running, once that many subagents are
|
|
running, before anything spawns:
|
|
|
|
```text
|
|
3 subagents running, cap is 3 (set in swarm-ui → this agent → inference) — wait for one to finish, or `interrupt` one
|
|
```
|
|
|
|
Every subagent the daemon tracks counts, whatever its role: one running a
|
|
turn, one starting, a goal run between turns, and an interrupted one whose
|
|
process hasn't exited yet. A goal run's next turn isn't a new start, and the
|
|
cap never stops it. Lowering the cap stops nothing already running.
|
|
|
|
The daemon reads the cap with the model list, on every `start` and
|
|
`continue`, so a change applies to the next call. A cap of 0 refuses every
|
|
`start` and `continue`, and the message says the cap is 0. Without a swarm value, or when the read fails, no cap applies. A failed read logs a warning naming the agent and the error, and the start proceeds: an unreachable swarm must not block subagent work.
|
|
|
|
## Runtime
|
|
|
|
A subagent runs on its parent agent's runtime
|
|
(`services.hyperhive.agent.runtime`). The unit gets the harness's
|
|
`HIVE_RUNTIME`/`HIVE_ACP_*` variables, and the daemon reads them once at
|
|
startup (`hive_runtime::RuntimeSpec`). On `claude`, everything else on this
|
|
page applies as written.
|
|
|
|
On `acp`:
|
|
|
|
- each run spawns its own copy of the parent's ACP agent. The end of the
|
|
run sends SIGINT to it and every command in its process group. The unit loads
|
|
`backendEnvironmentFile` for it. On the `opencode` preset, the daemon
|
|
reads a provider key that file leaves unset from the swarm secret store
|
|
at `swarm/agents/<agent>/acp-provider`, as the harness does.
|
|
- the session id lives in `subagent-acp-session-<name>` under the harness
|
|
dir. `start` moves an old one aside, `continue` loads it into a fresh
|
|
agent process, and a `continue` with none recorded fails straight away.
|
|
- a role goes in front of the session's first prompt, since ACP has no
|
|
system prompt.
|
|
- `interrupt` sends `session/cancel`. The runtime sends SIGINT to the
|
|
process group of an agent that ignores the cancel for 10s, and when the
|
|
run ends. Nothing escalates past that SIGINT. The name is free as soon as
|
|
`interrupt` sends the cancel.
|
|
- the runtime sets `model` and `effort` on the session when the agent
|
|
offers that value for its `model` or `thought_level` config option. Any
|
|
other value does nothing (`hive_runtime::AcpRuntime`).
|
|
- the agent's permission requests get the same answers a claude subagent's
|
|
tool list gives: MCP tools and file tools, `fetch` with `web_tools`,
|
|
`execute` with `execution` (`session::acp_permits`).
|
|
|
|
## The tool surface a subagent gets
|
|
|
|
Two flags, each covering one half, and neither covering the other:
|
|
`--tools` governs claude's built-in tools, `--strict-mcp-config` governs
|
|
the MCP ones. Dropping either brings that half back in full; in
|
|
particular `--tools` does **not** filter `mcp__*` tools.
|
|
|
|
### Built-in tools (`--tools`)
|
|
|
|
**A subagent gets exactly the built-ins its parent agent has, plus one
|
|
deliberate exception** — the base resolution
|
|
(`hive_sh4re::permissions::subagent_builtin_tools_for`, wrapping the
|
|
harness's own `builtin_tools_for`) over the agent's permissions, which the
|
|
daemon reads from the swarm on each `start`/`continue`:
|
|
`Edit`, `Glob`, `Grep`, `Read`, `Skill`, `Write`, plus `WebFetch`/
|
|
`WebSearch` for an agent granted `web_tools` and not otherwise. A running
|
|
subagent keeps the tools it started with. See
|
|
[the harness's own allowlist](../turn-loop/mcp.md#tool-allowlist-hive_sh4repermissionsallowed_builtin_tools)
|
|
for what that list contains and why.
|
|
|
|
The exception: **a subagent gets Claude's built-in `Bash` iff its parent
|
|
holds the `execution` permission** — the permission that already grants the
|
|
parent shell execution, via the out-of-process `bash` MCP server
|
|
(`mcp__bash__run`/`status`/`kill`). A subagent has no MCP server of its
|
|
own by default (see [MCP servers](#mcp-servers---strict-mcp-config)
|
|
below), so there is no `mcp__bash__*` for it to inherit that capability
|
|
through; it gets Claude's own tool instead. The rule is the capability
|
|
transfers, not the mechanism — the parent itself never gains built-in
|
|
`Bash` no matter which permissions it holds, only a subagent spawned by an
|
|
`execution`-holding parent does.
|
|
|
|
Inheriting rather than listing is the point: a hardcoded subagent list
|
|
would hand web egress (or `Bash`) to the subagent of an agent that isn't
|
|
allowed it, and would diverge from the parent's on the first tool anyone
|
|
adds to either.
|
|
|
|
Everything else in claude's built-in set is absent, in particular the
|
|
tools that let a session act outside the run for which it began: peer and
|
|
operator messaging, nested agents (including the stop verb, which takes
|
|
an _agent_ id rather than a session), schedule and webhook creation, and
|
|
worktree switching.
|
|
|
|
One rule worth knowing before editing any of this: **the daemon never
|
|
emits an empty `--tools` value**, and asserts rather than doing so. Not
|
|
because of what an empty value does — that's exactly the point. Our own
|
|
measurement and the installed `claude --help` disagree about whether
|
|
`--tools ""` means "no tools" or reads as the flag being absent, and the
|
|
answer belongs to whichever claude release the system has installed
|
|
rather than to this repo. Refusing the case is correct under either reading, so the code
|
|
never has to know which holds. Nothing here wants a subagent with no
|
|
built-in tools anyway.
|
|
|
|
### MCP servers (`--strict-mcp-config`)
|
|
|
|
A subagent runs with `--strict-mcp-config` and, by default, exactly one
|
|
MCP server: the two-tool `subagent_control` route above — not the
|
|
parent's `mcp__bash__*` / `mcp__hyperhive__*` surface. Those two signal
|
|
tools are deliberately unnamed in `--tools`, which doesn't govern them;
|
|
they survive on this flag alone. Nothing implicit reaches it: the built-in
|
|
hyperhive surface (todos/messaging) isn't an `extraMcpServers` entry at
|
|
all, and the automatically injected `bash`/`subagent` entries default to excluded
|
|
too (a subagent can't spawn hive-bash tasks or its own nested subagents
|
|
unless an operator opts them in explicitly, same as anything else).
|
|
|
|
Set `services.hyperhive.agent.extraMcpServers.<name>.availableToSubagents = true` on a
|
|
specific entry to hand that one server to subagents as well — useful for,
|
|
say, a read-only lookup or scraper MCP a subagent's bounded, single-batch
|
|
task might need. `hive-subagent-mcp`'s `mcp_config` module renders the
|
|
opted-in subset into a `--mcp-config` file per session, rewritten each
|
|
turn; an entry left at the default `false` never appears there. Per
|
|
session rather than one shared file, because the `subagent_control` entry
|
|
in it carries that session's own signal URL — one file for everyone would
|
|
be a race over whose identity each subagent reads at startup.
|