feat(orchestration): stop confirming closes of an agent's own children - #626
Merged
Conversation
An orchestrating parent closing the children it spawned was raising a confirmation dialog — once per close_agent, and once per run on close_run. Those agents are disposable by construction: the parent created them for a task, and closing them when the task is done is the normal end of their lifecycle, not a destructive act on something the user built. A dialog that fires on routine housekeeping is how "are you sure?" stops meaning anything by the time it guards something that matters. The safety property was never the dialog. It is the ownership gate: isVisibleToOrchestrationParent restricts every orchestration read and close to children the caller itself created, or descendants of its own root run. That gate is untouched, and the WHY on OrchestrationCloseSession now says that if it is ever loosened this decision has to be revisited with it. Agent Management MCP is deliberately NOT changed. Its blast radius is every agent in the caller's project rather than only its own children, so a human still signs off there — which is also what the README's "destructive close is restricted to an explicit current user request" describes. This also fixes the reporting bug PR #625's review surfaced. The old run loop pushed every id into closedSessionIds and relied on a decline THROWING to reach its catch — but the confirmation gate resolves false, it never rejected, so skippedSessionIds could not populate from a decline and the parent was told agents closed that had not. closeOrchestrationAgent was worse: it returned the id unconditionally with no branch at all. Both now branch on closeSession's boolean. With no dialog a decline is impossible, but an already-gone session still returns false and is correctly reported as skipped. The two wrapper adapters added in #625 to drop that boolean for a Promise<void> contract are gone with the contract; OrchestrationCloseSession returns Promise<boolean> and no longer carries requireConfirmation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…agent
Both reviewers returned BLOCK on the same finding, and they were right.
The safety argument was wrong. The ownership gate scopes WHICH SESSION MAY
BE NAMED, not WHICH SESSIONS DIE — closeSession kills a set the gate never
sees. Two shapes reach further than advertised:
- closeLinkedChildren takes every session linked to the target. A user can
run "Linked Agent…" on an orchestration child, and that linked agent
carries no orchestration fields at all, so the gate cannot see it.
Asserting preConfirmed destroyed a hand-built session silently, and it
did not even appear in closedSessionIds.
- Closing a tab's sole grid leaf takes every detached session in that tab.
Orchestration children live detached in the root's tab, so a close could
take the caller itself, its siblings, and Dispatch agents the user parked
there — while reporting one id.
The decisive evidence: Agent Management already refuses both shapes outright
via additionalCloseImpact, *even though it has a dialog available*.
Orchestration's only equivalent protection was the dialog this PR removed.
So the fix is not to restore the dialog but to scope it. A new
silentIfSoleTarget option resolves inside closeSession, where paneCloseTargets
is in scope — the only code that computes the full set a close destroys. The
routine case (a detached child expanding to exactly itself) stays silent,
which is the entire point of the PR; the two reaching shapes fall back to a
dialog naming the requester. CloseSessionOptions' doc, which calls
preConfirmed an ASSERTION and "deliberately NOT a convenience for this close
feels safe", now says why orchestration does not qualify.
Also fixed: Agent Management reported a DECLINED close as a success. Its own
comment said "declining rejects the tool call" — it did not; the gate resolves
false. That is the one surface where a decline is still possible, so it is
where the lie mattered most. close_agent gained the catch close_run already
had, since a rejected backend kill runs after closeLinkedChildren and could
report neither closed nor skipped with children already gone.
Test gaps the reviewers found: the runId filter had zero coverage and is now
close_run's sole blast-radius control; non-agent kinds and the empty-run case
were unpinned; toBeUndefined cannot prove omission; and stateWith's cast
through unknown violated docs/testing/standard.md without the harness comment
that standard requires. All closed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Orchestration MCP closes (
close_agent,close_run) raised a confirmation dialog on every call. Those agents are disposable — the parent created them for a task, and closing them when it's done is the normal end of their lifecycle. A dialog that fires on routine housekeeping is how "are you sure?" stops meaning anything by the time it guards something that matters.Result: routine fleet cleanup is now silent. The dialog survives only for the closes that genuinely reach further than the agent being named.
Why it isn't simply
preConfirmed: trueThat was the first attempt, and both reviewers blocked it. The safety argument was wrong.
The ownership gate (
isVisibleToOrchestrationParent) scopes which session may be named, not which sessions die.closeSessionkills a set, and the gate never sees that set. Two shapes reach past the target:closeLinkedChildrenwould have killed a hand-built session silently, and it wouldn't even appear inclosedSessionIds.The decisive evidence: Agent Management already refuses both shapes outright (
additionalCloseImpact→close_would_affect_additional_sessions) even though it has a dialog available. Orchestration's only equivalent protection was the dialog.The fix: scope the silence, don't remove it
A new
silentIfSoleTargetoption resolves insidecloseSession, wherepaneCloseTargetsis in scope — the only code that computes the full set a close destroys, which a caller cannot know from outside.CloseSessionOptions' doc callspreConfirmedan ASSERTION and "deliberately NOT a convenience for 'this close feels safe'" — it now records why orchestration doesn't qualify and what it uses instead.Two reporting bugs fixed on the way
close_runtold the parent every agent closed. It pushed toclosedSessionIdsunconditionally and relied on a decline throwing to reach its catch — but the gate resolvesfalse, it never rejected.close_agentwas worse: no branch at all. Both now branch oncloseSession's boolean.close_agentalso gained the catchclose_runalready had — a rejected backend kill runs aftercloseLinkedChildren, so a bare throw could report neither closed nor skipped with children already dead.Tests
orchestrationClose.test.ts— 14 cases. The contract is "does it tell the truth", since a parent that believes a child closed stops waiting on it:silentIfSoleTarget, neverpreConfirmed— a regression to the latter would destroy user sessions silently, and dropping it entirely would put dialogs back on routine cleanupskippedSessionIdsomitted rather than emptyrunIdfilter, which is nowclose_run's sole blast-radius control and previously had zero coverageVerification
npx tsc -b→ exit 0NODE_ENV=test npx vitest run→ 256 files, 1775 tests, all passingnpm run check:keybindings→ OKnpm run test:contract→ satisfiedEarlier runs showed 1–4 failures around
store.test.ts; all were 5s-timeout flakes under concurrent load from the review agents. Both reviewers independently confirmed they're pre-existing and outside this PR's import graph, and a clean run with no contention is fully green.Not verified: the live MCP flow — the app was not launched.
🤖 Generated with Claude Code