Repository navigation
feat(pi-subagent): keep idle agents across parent reload and restart - #121
Merged
Merged
Conversation
Roadmap docs/roadmaps/subagent-work-lifecycle.md, step 6. subagent_start always runs in the background, and a resume does too. subagent_get has no wait. Remove claims, the superseded-outcome slot, spawnAndWait and foreground-runner.ts. The completion message is the only carrier of an outcome. Tool descriptions, docs, skills and child prompts say to stash_put large results and name the handle. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mbvfwx8mE8ErNuTBrBH9Q5
Roadmap docs/roadmaps/subagent-work-lifecycle.md, step 7a. Agent statuses become queued, running and idle. How the last run ended (completed, turn limit, error, stopped or interrupted) and its result are stored beside the status. Stop leaves the agent idle. Every run, including a resume, goes through the concurrency limiter, and a current-run guard stops a stopped run from overwriting the run that replaced it. Persisted records get a v2 shape; the reader maps v1 statuses, and a saved queued or running record reads as idle and interrupted. Tool, widget and event output keep today's status words. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mbvfwx8mE8ErNuTBrBH9Q5
Roadmap docs/roadmaps/subagent-work-lifecycle.md, step 7b. Add subagent_send(agent_id, message). It steers a running agent, joins a waiting run's prompt when the agent is queued, and queues a run for an idle agent of any outcome, reopening its transcript if the session was released. An idle agent that never had a session gets a first run with its original task and the message. Remove subagent_steer and subagent_start's resume. A run stopped while its session is being created now disposes the late session, and a send in that window is refused until it unwinds. Roster lines show idle with the last outcome, and subagent:queued is a new event. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mbvfwx8mE8ErNuTBrBH9Q5
Fixes found in review of the work contract (roadmap steps 4 and 5), needed before subagent results flow through the host. - complete() writes the close record only once the message has landed in the transcript. Pi holds a message sent mid-run until the turn ends, so a crash in that window no longer leaves a closed item with no message. The root host checks the branch after each send and at run boundaries, matching only entries after a mark taken at send time, and gives up on a wait once the run has settled or the leaf has moved off the branch. - A disposed Delivery refuses the message, and complete() leaves the item open so the next start reports it lost. - pi-job: adopt completes a job that ended in the reload gap instead of cancelling it silently. Tests added for /job kill cancelling its item and for the before_agent_start fallback release. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mbvfwx8mE8ErNuTBrBH9Q5
Roadmap docs/roadmaps/subagent-work-lifecycle.md, step 8. Spawning an agent, or sending it a message that starts a run, opens an item in the spawning session's work host. The run ending completes the item with the same completion message as before. parent_notify goes through notify, stopping an agent sends its stopped message and cancels the item, and shutdown leaves the item open so the next start reports it lost. NotificationManager's queueing is removed; the host holds a message while the parent runs and the parent answers it in that run. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mbvfwx8mE8ErNuTBrBH9Q5
Roadmap docs/roadmaps/subagent-work-lifecycle.md, step 9a. Each child session gets a work:v1 host, offered before its extensions bind. When a run ends with open work (a job, a watch, an agent of its own), the agent goes idle and waiting instead of done, and its spawner hears nothing until the work is finished. The work's completion wakes the child with a run that goes through the concurrency limiter, started from a short fixed prompt so the transcript stays the inbox. Stopping an agent cancels its open work; an error or turn limit leaves it open, so a reopened agent is told it was lost. Waiting is shown in the widget, status bar, roster, watch overlay, subagent_get and the dashboard, with the items' labels and no clock. The retention sweep and clearCompleted skip agents with open work. pi-work gains an additive onChange and a runtime-only cancelOpen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mbvfwx8mE8ErNuTBrBH9Q5
Factor the check that decides whether a work message has landed in the transcript out of ensureWorkHost into createLandingTracker, so a managed child host can use the same polling points, marks and settle expiry as the root host. The root's behaviour is unchanged, and only the instance that provides a host holds a tracker. The tracker can also tell whether the transcript ends with a delivered work message nobody answered. That check, and the matching one in continuesRun, now skip other extensions' custom messages and branch summaries instead of looking only at the last message. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mbvfwx8mE8ErNuTBrBH9Q5
Roadmap docs/roadmaps/subagent-work-lifecycle.md, step 9b. A message that arrives while a managed child is running is held by Pi until the turn ends and answered in the same run. The child host now confirms landing, so a message Pi still holds does not close its item, and a crash before the turn ends reports the item lost. At agent_settled a delivered message nobody answered wakes the agent through the limiter instead of leaving it done. A wake that meets a compaction waits for it to finish. Tests cover mid-run delivery in real sessions and fail if any run in a managed child starts outside the manager. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mbvfwx8mE8ErNuTBrBH9Q5
Roadmap docs/roadmaps/subagent-work-lifecycle.md, step 10. In a child, ask opens a question item in the child's work host, tells the parent once through the agent's item, and ends the turn. The child is then idle and waiting on the answer and keeps its session. The parent's next subagent_send closes the child's question items and becomes the run's prompt. pi-ask adopts its open question items on session start. This removes the ask:route record path, pendingQuestion as an outcome field, the roster's "waiting for your answer" case and the question carve-out in parking. A new work:parent service lets pi-ask in a child reach its parent. The root session's ask still asks the user. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mbvfwx8mE8ErNuTBrBH9Q5
Roadmap docs/roadmaps/subagent-work-lifecycle.md, step 11. One predicate, Subagent.isDone (idle with no open work), now gates every retention path: the sweep, clearCompleted and releaseSession. A waiting agent keeps its session whatever it waits on (a job, a watch, a subagent of its own or a question), and a done agent is released a window after it became done. subagent_send reopens a released agent from its transcript. A failed, turn-limited or stopped run that still held work now cancels it before the session is released, and tells the agent what was called off with a work:called-off message in its own transcript, so a reopened agent is not left waiting for work that died. The retention settings keep their keys and defaults; their labels now say whether the result was read. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mbvfwx8mE8ErNuTBrBH9Q5
Roadmap docs/roadmaps/subagent-work-lifecycle.md, step 12. A parent reload no longer stops waiting agents. Shutdown suspends instead of aborting: a run still in progress ends as interrupted, and an agent waiting on a job or a question keeps its open items. At the parent's session_start, agents that held open work have their sessions reopened without a model run and without taking a concurrency slot, so pi-job can take back its jobs and pi-ask its questions, and the parent's own items for them are adopted. A reload tells such children reload so pi-job keeps their jobs; quit, new, resume and fork tell them quit. After a restart, jobs and subagents cannot be taken back and are reported lost, and questions stay open. Lost work and interrupted runs are reported to the parent in one subagent:lost message, and written into each agent's own transcript. Reopening an agent is bounded at 20 seconds so one hung child cannot block the parent's startup, and a late session from a timed-out reopen is discarded without writing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mbvfwx8mE8ErNuTBrBH9Q5
zacheryph
changed the base branch from
claude/work-11-retention-open-work
to
main
November 10, 2026 17:04
This was referenced Nov 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 12 of the subagent work roadmap. Stacked on #120 (step 11), then #119, #118, #117, #116, #115, #114 and #113. This is the last of steps 6 to 12.
What
A parent reload no longer stops waiting agents, and a restart reports what it lost.
session_shutdownused to runabortAll, which cancelled every waiting child's items. It now runsSubagentWork.dispose, thenSubagentManager.suspend(), thendispose(reason).suspendcuts a queued or running run short, so the agent reads idle with outcomeinterrupted.abandonWork,workChangedandfinishdo nothing while suspended, so nothing is cancelled or reported. The parent's saved record keeps reading queued or running, which is how the next start recognizes the cut-short run. ESC andsubagent_stopstill go throughabortAlland still cancel.reload, children that hold open work are toldreload, so pi-job keeps their jobs in its reload handoff. Onquit,new,resumeandfork, children are toldquitand their jobs die. The next start finds the items open, reports jobs and subagents lost, and still adopts questions. A done agent, and every child of a delegated session, getsquit.waitingOn(labels of the idle agent's open items). Older records read as before. A running or queued agent is checked from its transcript instead.StartupRestoreis the last of pi-subagent'ssession_starthandlers. It runs for root sessions onreload,startupandresumeonly. It adopts the parent's own items for candidate agents, reopens the sessions, settles, and writes one report.createSubagentSessionwith itstranscriptPath, with noexecution.admitand no turn loop, so it takes no concurrency slot and starts no model run. The child'ssession_startreason isreloadafter a reload, which pi-job needs to take back its jobs, andresumeafter a restart. pi-ask adopts its questions either way. At most four are reopened at once. Whatever nobody adopts is closed as lost by the child's host, which writeswork:lostinto the child's own transcript.subagent:lostmessage, sent withtriggerTurn: false, so it wakes nothing. Interrupted records are corrected and saved so a second start doesn't report them again, and the parent's items for reported agents are cancelled silently.Owner.parentis recorded at reload suspend, and the parent'ssession_startno longer warns "awaiting reload handoff; ownership is unknown" about its own children. pi-job'ssession_startruns before pi-subagent reopens them, so the manual TUI check showed that warning on every/reload.Added in review
Two review rounds each found something worth fixing before this went up:
RESTORE_TIMEOUT_MS, injectable for tests, not a setting). On timeout the agent is reported as one that "could not be reopened at startup, so its work was not checked", with its record untouched and its adopted item cancelled. It occupies a lane for the bound at most.subagent_sendhad reopened the same transcript, it ran its host startup against the shared file. In the reviewer's probe it wrote a secondwork:lostand forked the entry tree, and the wholesubagent_sendexchange was missing from the reopened branch.createSubagentSessionnow takes anisStalecheck, with a second layer inChildWork. A stale creation writes nothing, skips adoption andsettleStartup, and publishes nodisposedevent. A restore that completes after shutdown disposes its session and registers nothing.subagent:lostmessage'sdetailswas unbounded (13.7 MB for 500 agents with 50 items each). It is now capped at 30 agents and 8 items per list with clipped text, keeping totals. Quotes and backslashes in labels are escaped in the report text.Tests
restore-sdk.test.ts): reload with a child waiting on a realjob_watch(still waiting, no model request, the job wakes it through the limiter, the parent hears once), a job that ends in the reload gap, a question across a reload, a nested subagent (the grandchild's item is lost, reported, and the child is told), a run interrupted by a reload, a double reload, a missing transcript, six agents withmaxConcurrent: 2(no run and no slot at startup, then all wake), a restart by copying session files (the lost job is reported in one message, the question is adopted, no model run, both agents takesubagent_send), andquit,new,resumeandforkshutdowns each followed by a restart. A child that hangs insession_startcovers the bound and the late session in both arrival orders.restore.test.ts(including four fake-timer tests for the bound),lost-report.test.ts,transcript-work.test.ts, the restore handler, and the spawner-side adoption.lifecycle.test.ts(it pinnedabortAll; it now pinssuspendand the child shutdown reason per session-end reason), the restart test inwork-sdk.test.ts(it expected a hostwork:lostfor an agent still running at the crash; it now expects an interrupted run insubagent:lost), andrecords-sdk(a question across a reload now readswaiting: answer). No.skipor.onlywas added.tui-screenshotdriver I ran a scripted scenario where a child runsjob_watch, ends its turn, and the parent showswaiting: CI. After/reloadthe Subagents widget still showedwaiting: CIand1 waiting agent, with one "Reloaded" line and no handoff warning. With the watch set to match about 40 seconds after load, the capture afterwards showed the child woken (1 running agent,thinking…). The reviewer re-ran the reload half independently.Verification
npm testexits 0, run twice by the reviewer.npm run checkandnpm run verify:bundlepass on the final commit.waitingOnnot persisted, the bound and late-session disposal removed, and each stale guard.npm testfailed intermittently on pi-job'sjob_list, job_read and job_kill. It is a pre-existing timing race in that test, unrelated to this stack: the test is byte-identical onmain, and the reviewer reproduced the failure on a cleanmaincheckout under CPU load. It passes in the runs recorded above. I'm opening a separate PR againstmainto fix the test.Known gaps
None blocking:
provide(sessionId, …)for their own services (pi-stash'sstash:service, pi-git's), which overwrites the live session's entry, and the late session's withdraw then removes it. Lookups for those services by the child's id fail until the child restarts. The child's own tools keep working. It needs a hang of more than 20 seconds, asubagent_sendin that window, and the hung session finishing later.work:v1andwork:parentare unaffected.session_start. Extension writes made directly during bind, such aspi.appendEntry, aren't covered by the stale check. None of the bundled extensions do this.waitingOn. They are treated as done, so their open items are reported lost the next time the agent is reopened bysubagent_send.subagent:losthas no custom renderer. It shows as Pi's default custom message, likework:lost.fork's copied agents are reported lost by the root host's generic message, because a fork doesn't adopt them.Run.answeringandabandonWorkpaths that steps 10 and 11 added,rebuildFromSaved(still the lazy path for non-candidates), andabortAllwording in older CHANGELOG entries.Stack: #113 ← #114 ← #115 ← #116 ← #117 ← #118 ← #119 ← #120 ← this
Generated by Claude Code