Repository navigation
VC-809: End a completed subagent's shells and stop re-arming its delegation on host-notice turns - #935
Merged
hussainph merged 8 commits intoOct 19, 2026
Conversation
…very fixture (VC-778) The fixture set followUpAdmissionTimeoutMs: 50 for the whole runtime, but that bound covers every adapter.attach (workspace + executor stages) and every cold binding rebuild, not only queued messages. A slow but healthy real Pi attach under CI load then came back rejected with queue_admission_timeout. Genuine work now keeps the production 30 s bound; the 50 ms bound applies only once the fixture enters an attach/admission gate. Adds a regression test with a real slow attach, and receipts in the accepted-status assertion messages.
…mptions A completed delegate turn now ends the background shells the child owns, through the same port door the attachment's end uses, so no exit notice follows. Re-arming an answered delegation skips a turn a host notice opened (shell, browser, watch, worktree, subagent notice); a person's or the parent's resumption still reports once.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…li-code into volli/VC-809-a-finished-subagent-s-own-background-shell-reope
…ice turn A host-notice turn of an answered child arms nothing at its start, so a message the person or the parent steers into that running turn was never reported. The wake of that message.submit now re-arms on the running turn (rearmJoined), under the turn's resume ids so a racing rearm spends the same ones. Also: shell tool descriptions say a subagent's shells end with its turn; the integration test takes pids from shell_start's result; the host-notice origin table is exhaustive; docs say what a turn-end dispose forgets.
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.
Problem
A delegated subagent that leaves a
shell_startprocess running (e.g. a dev server) keeps it after its turn completes: the child's attachment stays open, and shells only die with the attachment. When the process later exits, crashes, or a person kills it from the Island, the shell notice is steered into the answered child and opens a new turn.agents.tsre-arms the delegation on anyturn.startedof an unwatched subagent, so that reactive turn's end delivers a second, indistinguishable "completed its task" notice to the parent. Meanwhile the child may edit the shared tree.Fix
PiBinding#observe, a subagent turn thatcompletednow calls the shell port'sdispose()next to the headless-tab cleanup. That isBackgroundShellHost.disposeSession, the door the attachment's end already uses: it kills every shell the child owns (SIGTERM to the group, then SIGKILL), drops pending exit notices, and forgets the entries, so no exit or match notice follows (VC-495's "a Session that has ended is told nothing"). The port stays usable, so a turn a person resumes the child with can start shells, and those are retired at that turn's end too. As with the tab cleanup, a failure is logged and never costs the delegate its answer, and the attachment's end still disposes.Delegations.rearmon an answered delegation reads the child's projection. If the wake's turn is still the latest and its recorded origin is a host notice (watch-,subagent-,browser-,shell-,worktree-notice), nothing is armed and no:resume:…:answercommand is sent.Decisions
user/parentsession. This is the conservative reading. Every origin that is not a host notice keeps the VC-457 re-arm: unknown/null(e.g. two pending commands at turn start),automation,relaunch-recovery,scheduled-resumeandsupervision. So a real resumption is never silenced by an origin we failed to attribute.answered: falseunder a parentsessionorigin.latestTurnId≠ the wake's turn) also re-arms.message.submitthat joins a running turn re-arms on that turn, so a lost report is never the failure mode; at worst there is a duplicate.Review (supervisor review at 1ad6eef, landed in 9669aa9 and a test-placement follow-up)
turn.started. So a person's message, or the parent'ssession_send, that joins a settled child's host-notice turn kept that notice's origin, and the parent was never told. That is worse than the old duplicate. Nowagents.tsalso wakes on a child'scommand.recordedmessage.submit.Delegations.rearmJoinedreads the command's origin withcommandOriginOf, from the slim field or the provenance attribution:resumedNoticeIds(op, turnId). A racingrearmfor the same turn spends the same ids, so the parent hears once.message.submitcommands until they are released, so every mid-turnmessage.submitis a steer that joins the turn.rearmadd nothing); host-notice and idle cases arm nothing; unanswered is left alone; unreadable projection;commandOriginOf.shell_startnow says: "A Subagent Session's shells end silently when its turn completes, so a subagent reads what it needs or waits inside the turn."shell_kill's "readable until the attachment ends" now adds the subagent turn-end case. These descriptions are role-neutral, so the provider tool array changes once for every Session.shell_start's own result ((pid N), known at the spawn) instead of a pid file the shell writes, which could race the turn's end. It also asserts that the project parent's group is still alive.AttachmentShellPortdoc ("forgets only the shells it killed" was wrong),background-shell-host.ts's WHAT IT REMEMBERS / WHAT ENDS IT and thedisposeSessiondoc, CONTEXT.md's Background shell (exited shells' output is forgotten at turn end for subagents), and CONTEXT.md's Subagent entry (a steered message still reports).Record<reason, boolean>: done.IS_HOST_NOTICEcovers everyvollireason, so a new reason fails to compile until it is classified.executecall can be running at a completed turn's boundary, sodisposeSession's "attachment ended" cancel message cannot reach a model from this path.Delegations(agent-tool-door.test.ts,facade.test.ts) gainedrearmJoined.facade.test.tsalso routes a steeredmessage.submitwake torearmJoinedwith its origin, and filters out other commands. That keepsagents.tsat the protected 100% branch coverage, which run 39256414893 flagged. Its./delegate-sessionmock now spreads the real module, forcommandOriginOf.delegate-session.test.ts59/59,delegate-browser-cleanup.integration.test.ts4/4, agent-runtimeshell-tools.test.ts24/24,facade.test.ts19/19, each run once on one worker through/tmp/volli-serial.sh;vp checkis clean on all touched files.Overlap
Stacked on #929 (VC-778 crash-recovery fixture fix, test-only): its head 637c7d1 is merged into this branch with an additive merge commit, on the supervising session's direction, instead of a further rerun. Merge #929 first; this PR's own diff is otherwise unchanged.
git merge-treeis clean against origin/main and the heads of #909, #919, #920, #921, #922, #926 (VC-799, which also editsdelegate-session.tsand its test) and #930. The first cut collided textually with #926 because both inserted a helper at the same line, so the helper was moved belowanswerOf. Likewise, the review round's new test blocks sit before the VC-457rearmblock rather than where #926 adds its own. No stacking on #926 needed. #651 and #873 also touch CONTEXT.md, but both already conflict with main on their own.Tests
Each new test was run on one worker through the machine's serial runner.
delegate-session.test.ts(53 tests; 5 fail on main, all pass with the fix):shell,browser,watch,worktree) in a settled child arm nothing and send no:resume:command.delegate-browser-cleanup.integration.test.ts: new VC-809 case, a realBackgroundShellHostthrough the real runtime and Pi adapter (fails on main at "no live shells after the first turn"):shell_startprocess group is gone after its turn completes and the attachment stays open.vp checkon the touched files.CI notes
First run, on 2d3ce02, failed for two reasons:
tsc --noEmit -p packages/host-core.crash-recovery.integration.test.ts › reconciles an accepted command with a different persisted timestamp…readrejected. That is the known flake VC-778; the test involves no subagent or shell. The fix push re-ran CI; no manual rerun was used.Second run, on a9382cb: everything green except
Test (packages), which failed on the same VC-778 file withexpected { closed: 1, errors: [ … ] } to deeply equal { closed: 0, errors: [] }. Main's own latest CI (run 39138198050, 7170501) fails this file with the same assertion, and the root cause is a test-only 50 ms bound (fix pending in VC-778: Bound only deliberately stalled stages in the crash-recovery fixture #929). Rerun disclosed:gh run rerun 39245944393 --failed, once, for the known flake VC-778.That rerun cleared VC-778, and every check except
Test (packages)is green on a9382cb.Test (packages)then failed only onworktree/watch.test.ts › pollRetention — terminal PRs are not re-polled (VC-836) › 500 Done tickets … cost 10 gh calls a cycle, which timed out (12.3 s against a 5 s limit). That is the filed flake VC-856; the worktree watcher has nothing to do with this change. VC-856 is not on this session's pre-approved rerun list, so no further rerun was made without approval.Then, on the supervising session's direction, VC-778: Bound only deliberately stalled stages in the crash-recovery fixture #929 (head 637c7d1) was merged into this branch instead of rerunning again; the new run is on the merge commit.
Review round: run 39256414893 (60c3e4a) failed only the protected branch-coverage gate on
agents.ts. After the coverage commit, run 39258089500 (4edbfed) is all green on the first run, with no rerun.