subagent: give each run its own signal URL, and drop the name argument
`goal_reached`/`need_help` took the session name as a tool argument, so identity was an assertion by the caller and the only guard on it was `occupancy()` — "does that name have a turn in flight", which two concurrently running siblings both satisfy for each other. A subagent could stop its sibling's run by naming it. Identity moves into the URL. Each spawned run is minted an unguessable token (`Uuid::new_v4`, the OS CSPRNG), the URL carrying it goes into that one subagent's own `--mcp-config`, and the route resolves it back to a session before dispatching to a handler bound to that session. Neither tool takes a `name` any more: a subagent has no field in which to name a sibling, and a sibling's name — which a brief may well mention — is not a token. One route with a path parameter, not a route per session: the `Router` is built once at startup and subagents come and go for the daemon's whole life. An unminted or revoked token gets a bare 404, the same answer either way, so nothing enumerates. A run's token is revoked when the run ends (`finish_turn`) or when a call never reached a spawn. Two things fall out of that: - the config file becomes one per session. A single shared path was already a race between two `start`s; with a per-session URL in it, the loser would read the winner's identity. - `occupancy()` stops being the identity guard and is gone from the signal path entirely rather than kept "just in case" — a revoked token can't reach it, and it never answered the question it was standing in for. It still backs `status`, which is what it was always actually for. Refs #4403 Refs #4413
This commit is contained in:
parent
b5fc17aa59
commit
34129d776c
11 changed files with 575 additions and 180 deletions
|
|
@ -32,13 +32,34 @@ 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. A request is
|
||||
resolved to a session before it's dispatched, 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, and where it writes its report. All of it lives
|
||||
only as long as the daemon process does. A daemon
|
||||
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
|
||||
|
|
@ -213,5 +234,8 @@ Set `hyperhive.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 its own `--mcp-config` file per turn; an entry left
|
||||
at the default `false` never appears there.
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue