diff --git a/docs/2-handbook/agents/persistence.md b/docs/2-handbook/agents/persistence.md index 6c0985214..46a9c9dbc 100644 --- a/docs/2-handbook/agents/persistence.md +++ b/docs/2-handbook/agents/persistence.md @@ -225,7 +225,7 @@ destroy/recreate, gone on `--purge`. ### `/harness/paused` (per agent) -Empty marker file. Its presence parks the agent's turn loop: the +Marker file, empty when an operator paused the agent. Its presence parks the agent's turn loop: the harness keeps serving its web UI and MCP daemons but drives no turns, and inbox messages queue unacked until it's removed (see [turn loop](../../4-implementation/turn-loop/README.md#the-loop)). @@ -243,6 +243,12 @@ pause keeps working when the harness is wedged or the container is stopped. +When the harness pauses itself, after three todo wakes in a row that skipped +`get_loose_ends`, it writes the reason into the marker. The agent's turn-state +header reports a non-empty marker as `paused_by_hive` +([swarm](../swarm/README.md)), and the swarm then declares the +agent `paused` so the hive's wanted-state convergence doesn't resume it. + It lives in `/harness/` rather than `/state/` deliberately: `/state/` is the agent's own space to fill, and this is harness control state. Survives destroy/recreate, gone on `--purge` — so a paused agent comes diff --git a/docs/2-handbook/swarm/README.md b/docs/2-handbook/swarm/README.md index 674bda720..195514f96 100644 --- a/docs/2-handbook/swarm/README.md +++ b/docs/2-handbook/swarm/README.md @@ -273,6 +273,17 @@ context budget and the last turn's context and cost token blocks, session and its turns wait for a login. The harness sets it from its own login state, so it reaches the swarm without going through the hive. +A header also carries `paused_by_hive` while the harness's own pause is in force: +the reason it wrote into the pause marker when it paused its turn loop. A pause +by an operator (`hivectl agent pause`, the dashboard) leaves the marker +empty and reports nothing here, and so does a harness that predates the field. +The controller reacts by declaring the agent `paused`, but only when the swarm +declares it `up` at that moment. Without that, the hive resumes any paused agent the +swarm declares `up` on its next convergence. It acts once per pause: the entry +stays until a header without the field arrives. Setting the agent `up` again +resumes it, and the hive clears the marker. If the swarm is on a controller that +predates the field, the hive's pause lasts until its next convergence, as before. + `agent_state` reuses the swarm's own wanted-state vocabulary (`up`/`offline`/`paused`/`destroyed`) so a reader can compare what an agent _is_ against what the swarm declared it should be without translating between two @@ -301,7 +312,8 @@ agent's latest `model`, `effort` and their sources for agent's latest `needs_login` for the `needs_login` field of `GET /api/agents/status`. That field is `null` until a header carrying the flag arrives, so for up to a minute after the controller starts, and for an agent -whose harness doesn't send it. +whose harness doesn't send it. The row's `paused_by_hive` comes from the same +headers and is `null` in the same cases. The third thing an agent publishes is its **icon**, the same SVG its own `GET /icon` serves. It goes into the `agent-icons` KV bucket under the key diff --git a/docs/2-handbook/swarm/ui.md b/docs/2-handbook/swarm/ui.md index a6c744c21..9456d2f85 100644 --- a/docs/2-handbook/swarm/ui.md +++ b/docs/2-handbook/swarm/ui.md @@ -80,8 +80,9 @@ action that asks first. card per agent, none of them inside a bigger card. A card's header shows its icon, name, freshness, the status message, a **terminal** button that opens the agent's terminal tab, the **wanted** menu (up, offline, -paused, destroy), and a **login needed** badge with a -**log in** button when its claude has no session. Select the header to open the +paused, destroy), a **paused by hive** badge when the agent's own harness paused +its turn loop (hover for the reason; set the agent **up** to resume it), and a +**login needed** badge with a **log in** button when its claude has no session. Select the header to open the card in place; select it again to close it. Several cards can be open at once, and a card stays open across the page's refresh, but not across a reload. The filter card, which also holds the **+ agent** button and the refresh picker, diff --git a/frontend/packages/swarm-ui/src/agent/AgentTypes.ts b/frontend/packages/swarm-ui/src/agent/AgentTypes.ts index 1969c63b8..fbc093711 100644 --- a/frontend/packages/swarm-ui/src/agent/AgentTypes.ts +++ b/frontend/packages/swarm-ui/src/agent/AgentTypes.ts @@ -43,6 +43,10 @@ export interface AgentRow { // no header since swarm-controller started, or an agent that does not // send the flag — not known, so not "needs login". needs_login: boolean | null; + // Why the agent's own harness paused its turn loop. `null`: it did not, or + // the harness does not send the field. The controller declares such an agent + // paused, and only setting it up again resumes it. + paused_by_hive: string | null; // The archetype the agent is built from; `null` for a custom agent. archetype: { source: string; name: string } | null; } @@ -61,6 +65,13 @@ export function showsLoginNeeded(row: Pick): boolean { return row.needs_login === true; } +// The reason to show on the "paused by hive" badge; `null` shows no badge. +export function hivePauseReason( + row: Pick, +): string | null { + return row.paused_by_hive ? row.paused_by_hive : null; +} + // Same tone pairing as HivesCard — one freshness enum shared by both // endpoints, so the same tone rule applies to both pages — but // `unknown`'s label diverges deliberately: an agent outside the diff --git a/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx b/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx index efa1e4a22..d14411e09 100644 --- a/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx +++ b/frontend/packages/swarm-ui/src/agent/roster/AgentCard.tsx @@ -23,7 +23,12 @@ import { AgentIcon } from "../AgentIcon.js"; import { ClaudeLoginDialog } from "../claude-login/ClaudeLoginDialog.js"; import { AgentDetail } from "../detail/AgentDetail.js"; import { termPaths } from "../terminal/term-paths.js"; -import { FRESHNESS, showsLoginNeeded, type AgentRow } from "../AgentTypes.js"; +import { + FRESHNESS, + hivePauseReason, + showsLoginNeeded, + type AgentRow, +} from "../AgentTypes.js"; import type { AgentActions } from "./AgentActions.js"; import { WantedMenu } from "./WantedMenu.js"; import { AgentCardLayout } from "./AgentCardLayout.js"; @@ -82,6 +87,13 @@ export function AgentCard({ } /> + {hivePauseReason(row) !== null ? ( + + ) : null} {showsLoginNeeded(row) ? (