Skip to content

Orphaned subagents

flowchart TD
    c["Every 15 s, each session checks<br/>the children it launched"] --> s{"A child is stranded?"}
    s -->|"yes"| m["One intercom message to the child:<br/>send your final report, then stop"]
    m --> r["The report reaches the launcher"]
    s -->|"no"| n["Nothing is sent"]

A subagent’s result reaches the session that launched it only while that session’s own process keeps pi-herdr-agents’ watchers and its registry of launched children (runningSubagents, in the process-wide slot globalThis[Symbol.for("pi-subagents/runtime")]). /reload, /new, /resume and /fork keep them, quitting pi suppresses the delivery, and a crash or restart drops them for good: pi-herdr-agents does not recover watchers after a full restart. A subagent that stays open after it finished, an interactive one for instance, is then stranded: it sits done in its pane and nobody is told.

Details

Every session in Herdr that is not itself a subagent checks for this every 15 seconds. While no subagent of the session is stranded, a check reads the registry and one directory; only a subagent that is not tracked and whose own activity says it waits brings a pi-intercom session listing, and after that one herdr pane list per check (and one herdr pane process-info when it is about to be asked). A subagent is asked to report when all of these hold:

  • Its launcher no longer tracks it. Its id is not in that registry. When the registry cannot be read (pi-herdr-agents is not loaded, or no longer keeps it there) nothing is known to be lost, so nothing is sent.
  • Its result is not in the launcher’s transcript. No subagent_result or subagent_ping message names the child’s session file.
  • It has finished. Its activity file says waiting (it ended its turn), its pane runs pi and Herdr says done (or idle, which a pane becomes once someone has looked at it), and pi-intercom says the session in that pane is idle. A child that works, waits on a question, or has not started is left alone.
  • It was found so twice in a row, 15 seconds apart, so that a delivery just going out is not taken for a lost one.
  • Its pane runs the child itself. The foreground processes of the pane (herdr pane process-info) include the launch script this session wrote for that run, artifacts/<session id>/subagent-scripts/<name>-<run id>.sh. A pane that only reuses the run’s session file, such as a run another session resumed or a pi someone started on that file, is not the child, and neither is a pane whose processes Herdr does not list.

Straw Boss then sends that session one intercom message: the launcher’s session id and name, and the request to send its final report to that session through intercom and stop. The launcher shows a notification. It sends at most one message per run, where a run is a child id: its activity file is named after it, and the child’s own /reload starts that file again, so its creation time says nothing about a run. The run is written to the launcher’s session (a straw-boss entry with a nudged key) before the message goes out, so a restart does not repeat it, and a send that fails is shown as a warning and not tried again.

A child whose pane cannot be found or does not run its launch script, or that has no pi-intercom session of its own, is given up on after four looks, until the next session start (a reload counts). A subagent whose process has exited cannot be asked; its result stays in its session file. A resumed run (subagent_resume) has an id of its own that nothing in its pane’s session file names, so it is never found and never asked.

The subagent’s own launcher does this, not a tower: only the launcher’s process knows what it tracks, so a tower never nudges the subagents of other sessions, and a subagent that launches subagents of its own does not (it runs no Straw Boss).

Subagents a Herdr restore brought back plain

Section titled “Subagents a Herdr restore brought back plain”

The restore case in one picture:

sequenceDiagram
    participant H as Herdr
    participant P as Restored pane
    participant S as Straw Boss
    participant L as Launcher session
    H->>P: a restart starts a plain pi in every agent pane
    S->>P: finds it (a check every 15 s, twice in a row)
    S->>L: writes a closing key, closes the pane
    S->>L: one message with the follow-up
    L->>L: subagent_resume (pi-herdr-agents)

The nudge above needs the child to still be what pi-herdr-agents started. A Herdr restart does not leave that: with resume_agents_on_restore (on by default) Herdr starts a plain pi --session <file> in every pane that had reported an agent session, subagent panes included. That pi has none of the child’s setup (no subagent-done.ts extension, no PI_SUBAGENT_* environment, no caller_ping, no auto-exit, no <session>.exit sidecar), so it sits idle at its prompt with the task unfinished and will never report, and the restored launcher’s registry is empty and is never filled again. Nothing can ask it to report by itself, so the launcher resumes the run instead, through pi-herdr-agents’ public subagent_resume.

Details

The same check every 15 seconds (restored.ts) finds such a run when all of these hold. It lists Herdr’s panes only while some run could still be noted or found: a run that can never qualify (a persistent specialist, or a launch script that is missing or does not name the run’s own session) is dropped before that, so it costs no list and no process look.

  • The launcher does not track it, and its result is not in the transcript. As for the nudge: its id is not in the registry, and no subagent_result or subagent_ping names the child’s session file. It is not a persistent specialist (PI_SUBAGENT_PERSISTENT=1 in its launch script), which pi-herdr-agents cannot resume.
  • It never ended. There is no <session>.exit beside its session file, and Straw Boss has not acted on it before (no resumed or nudged key for it in the straw-boss session entries).
  • Its pane is the restored one. The launch script’s header (# Session: and # Surface:) names the child’s session file and the pane it ran in, and a restore keeps pane ids. Herdr lists that same pane with agent pi on that session file, idle or done.
  • A restore happened. While the child runs as pi-herdr-agents started it (its pane’s foreground runs the launch script), the launcher notes the pane’s terminal_id in its session (a straw-boss entry with seen and terminal). Herdr gives a restored pane a new terminal while the pane id stays, so the pane counts only when its terminal differs from the noted one. A pi someone started by hand in the same pane (same terminal), a run never seen running (the restart came before the first look), and a Herdr that reports no terminal_id are all unknown, and unknown is never closed.
  • Nothing else runs there. herdr pane process-info lists exactly one foreground process whose argv0 is pi, and every other process has a readable argv with no launch or resume script (a path with / or \). A pane that still runs the launch script is the nudge’s case; a run another session resumed sits in a pane of its own and runs a resume script; none of them is touched. On Windows the list cannot prove the absence of the script, so a pane there is never taken for restored.
  • The launcher is no subagent itself, and the run is an ordinary one. A session with a .pi-herdr-subagent-policy.json beside its file (a restored nested subagent) does nothing. The child’s own policy sidecar decides: persistent runs are left alone, and an owner other than public is handled as a worktree run.
  • It was found so twice in a row, 15 seconds apart, because a pane may still be coming up after the restart. A run with no such pane is given up on after eight looks, until the next session start.

Then, in this order: a closing key for the run is written to the launcher’s session, the restored pane, and only that pane, is closed with herdr pane close (the old pi must not hold the session file beside the resumed one) and Straw Boss waits up to five seconds for it to be gone, and the launcher gets one custom message; once the session has taken it, a resumed key is written. A reload or quit between the close and the message leaves closing without resumed: the next instance of the launcher finds the run, sees that its pane is gone, and sends the message then.

If the pane is still open, the close never took effect (the process died first): the closing key is taken back (an unclosed key in the session, so a reload does not read it again) and the run goes through the proof of a restore from the start. A run that then can no longer qualify (no restored pane, the same terminal, not a plain pi) is counted as a miss like any other, and when the eighth miss gives up on it one warning names its session path. The message names each run with its sessionPath and name, quotes the follow-up to pass (finish the task you were given, and report your result), and tells the model to call subagent_resume once per run (with autoExit false for a run launched with PI_SUBAGENT_AUTO_EXIT=0); the resumed run then reports as a normal subagent_result.

It is a steering message that triggers a turn, the way pi-herdr-agents delivers subagent_result itself and the way a tower run is driven: an idle launcher wakes at once and a busy one gets it at its next step. Straw Boss does not call the tool, import pi-herdr-agents, or write a result for the child: whether and how the run is resumed stays pi-herdr-agents’ and the model’s. A launcher that is a tower does none of this (a tower is driven by the server’s queue, not by messages of this kind).

Each step that fails is shown as a warning, recorded as gaveUp, and not tried again: a pane that cannot be closed (or stays open) is not resumed, and a message that cannot be put into the session is shown with the sessions it would have named, so they can be resumed by hand.

A managed-worktree run (it has a manifest under artifacts/<session id>/worktree-runs/) is the exception. Its pane is the root pane of the retained worktree workspace, so Straw Boss does not close it, and pi-herdr-agents’ subagent_resume neither reattaches the worktree’s lifecycle nor accepts such a session. The message says so, names the pane, worktree and branch, tells the model not to call subagent_resume for it, and leaves the restored pi waiting at its prompt for the user to continue (a run cut off mid-turn is told to go on, below).

A restart or a reboot can land in the middle of a child’s turn, and the restored pi then waits at its prompt with nothing to do: its interrupted tool call has no result and nobody continues the turn. Before a run is handed to the launcher, cut.ts reads the end of the child’s session file (the last chunk of it, which grows until it holds the last assistant message; a large file is not read whole). Only message entries and the custom messages that start a turn (an intercom message, a nested launcher’s result, a monitor’s wake, a second-thought or document-review notice, a robot-hand follow-up) count, so a custom entry, a model change, or any other notice says nothing.

Details

The session is cut when the last tool calls of the last assistant message have no result, or the last step is a tool result the model never read, or the last step is a message nobody answered, or the last assistant message is an error a restart leaves: claude-bridge’s Claude Code process terminated by signal SIGHUP or exited with code 129 (a SIGTERM or code 143 may be a kill by hand, and nothing shows a restart came after it, so it is no cut) (the reply was cut while it was being written, with or without a tool call that had begun to stream).

It is not cut when the last assistant message ended its turn (a stop; an abort, which is the boss’s own Esc; any other error, such as a quota or a replaced conversation), when the turn stands at a question (ask_user_question, caller_ping, an intercom ask: the child is waiting for an answer, as before the restart), when the file cannot be read, or when the file has no step. Only plain tool names (letters, digits, _, ., :, -, up to 64) are quoted into the text, others are called “a tool”, and the time must be an ISO time.

The child is told once, in one message: its last turn was cut by a Herdr restart or reboot (with the time of its last recorded step), the tool call (by name) got no result, it should check the current state before re-running anything that changes state, then continue the task and report. How it gets there depends on the run:

  • A run that is resumed (the pane was closed) gets it as its follow-up. The launcher’s message lists the run as restored mid-turn, with the text, and tells the model to pass that one to subagent_resume instead of the generic follow-up. The run is resumed once, so it is told once.

  • A managed-worktree run keeps its restored pane, so Straw Boss sends the text itself over intercom (the same send as the report request above), to the broker session whose Herdr location is current and in the run’s pane (an unavailable location keeps the pane id of its registration, so it does not count). A restored worktree pi reports to nobody by itself, so the text also names the launcher (its name and intercom session id) and asks for the final report there, as the report request above does. It is never typed into the pane, so whatever the boss has in the prompt stays.

    It is sent only while that session’s own intercom status is idle (and the pane was idle or done at the look), and the transcript is read again just before the send: a child someone went on with is not told. A session that is not reachable yet (pi-intercom connects a moment after pi starts) is looked at again at the next looks, four at most; then the launcher is told it could not be reached. continued is written to the launcher’s session before the send, so a child is told at most once per restore, even across a reload or a failing send.

  • In both cases the one message the launcher gets says which runs were restored mid-turn, and for a worktree run whether the child was told (“not idle, wait for its report over intercom”; then the message does not send the launcher to the pane for that run) or not (why, and to continue it in its pane).

Straw Boss does not see the prompt editor of another pi, and the intercom message starts the child’s turn as soon as its status is idle. If the boss has a draft in the restored pane in the first minute after the restore, the draft is kept (nothing is typed into the editor) but the child may act before the boss sends it. The text tells the child to check the state first. A guard on the child’s side (its own Straw Boss extension holding the message until the editor is empty) would need a channel of its own between the two, an acknowledgement, and a fallback for a child whose extension is older, so it is not part of this release.

Details

Not covered: a run that was itself resumed (subagent_resume) is not recovered by a second restart. Its resume script is named <name>-resume-<time>.sh and its header (# Session:, # Surface:) does give the session file and pane, but not the run’s id (that is only in the script’s command, as PI_SUBAGENT_ID), and the id is what every other proof here hangs on. The resumed run reuses the original session file, whose name carries the original id, so the pane and the transcript cannot be matched to the new id (ownFile), and the original run’s subagent_result names the same session file, so the transcript cannot say whether the resumed run finished.

Covering it would mean closing a pane on a guess, so a restart after a resume stays unknown and nothing is closed; and a .exit sidecar left unconsumed means the child finished, so the run is left alone and its result has to be read from the session file.