fix(server): stop per-session toolsets on session delete - #3884
Open
EronWright wants to merge 1 commit into
Open
fix(server): stop per-session toolsets on session delete#3884EronWright wants to merge 1 commit into
EronWright wants to merge 1 commit into
Conversation
Collaborator
|
The idea is interesting @EronWright Could you rebase your PR and validate that linters and tests are passing. |
docker-agent
reviewed
Aug 4, 2026
docker-agent
left a comment
Contributor
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
One medium-confidence finding was identified in the new code added by this PR.
Collaborator
|
There are linter + tests failures @EronWright |
aheritier
marked this pull request as draft
August 10, 2026 11:53
EronWright
force-pushed
the
contrib/stop-toolsets-on-delete
branch
from
August 14, 2026 18:52
68e6493 to
b652962
Compare
A session materialised via teamloader owns a team whose toolsets may hold external resources — notably stdio MCP subprocesses. DeleteSession cancelled the runtime context but never called team.StopToolSets, so those subprocesses leaked until the server process exited; BatchDeleteSessions had the same gap. Track the per-session team on activeRuntimes and route both delete paths through one scheduleTeardown helper: it keeps the entry in deletedSessions, waits for the session's stream to drain, stops the toolsets, then drops the entry. The drain and StopToolSets carry separate budgets, so a session that exhausts the drain still gets a live context to stop its toolsets with. BatchDeleteSessions now registers the same teardown, so a batch-deleted session is observable through WaitStopped just like a singly-deleted one — it previously never stored into deletedSessions, making WaitStopped return nil while teardown was still running. WaitStopped in turn waits on a completion channel closed after StopToolSets rather than polling the streaming mutex, so it means "fully torn down" and can no longer return before teardown has run. Attached runtimes (AttachRuntime) leave team nil, so stopping is a no-op there — their toolset lifecycle belongs to the embedder. Claude-Session: https://claude.ai/code/session_01BiXE2cM4DxDQNYhKfsPn5K
EronWright
force-pushed
the
contrib/stop-toolsets-on-delete
branch
from
August 14, 2026 18:56
b652962 to
e0785f3
Compare
EronWright
marked this pull request as ready for review
August 14, 2026 19:09
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.
fix(server): stop per-session toolsets on session delete
Problem
A session materialised via
teamloaderowns ateamwhose toolsets may holdexternal resources — notably stdio MCP subprocesses.
DeleteSessioncancels theruntime context but never calls
team.StopToolSets, andBatchDeleteSessionshasthe same gap (
LocalRuntime.Closeonly stops background agents;StopToolSetswasotherwise reached only via the transient
GetAgentToolCount). So a deleted session'sMCP subprocesses leak until the server process exits.
Fix
Track the per-session
teamonactiveRuntimesand route both delete paths throughone
scheduleTeardownhelper. It keeps the entry indeletedSessions, waits for thesession's stream to drain, stops the toolsets, then drops the entry. Teardown runs in
a background goroutine that outlives the originating request, on a context detached
with
WithoutCancel(so it still carries trace/logging values). The drain andStopToolSetscarry separate budgets — a session that exhausts the 5 min drainmust still get a live context to stop its toolsets with.
teamis nil for attached runtimes (AttachRuntime), whose toolset lifecycle belongsto the embedder — a no-op there.
Two things beyond the original revision, from review feedback:
BatchDeleteSessionsis nowWaitStopped-able. It previously never stored intodeletedSessions, soWaitStoppedreturnednilimmediately while teardown wasstill running. It now registers the same teardown as
DeleteSession.WaitStoppedmeans "fully torn down". It waits on a completion channel closedafter
StopToolSetsinstead of polling the streaming mutex. This also closes anexisting race where
WaitStoppedcould win theTryLock, delete the entry andreturn
nilbefore teardown had run.Why this is safe
Teams are never shared between sessions:
loadTeamWithConfig→teamloader.LoadWithConfigbuilds a fresh team and fresh toolset instances per call(
loaderdefaults.Opts()hands in a newtoolsets.NewDefaultToolsetRegistry()eachtime). The only process-wide singletons —
plan, andtodowithshared: true— donot implement
Stop, soStopToolSetscannot reach across sessions.There is already precedent in-tree:
GetAgentToolCountloads a throwaway team, startsevery toolset and unconditionally stops them while arbitrary sessions are live. If
teams or toolsets were shared, that endpoint would already be killing live sessions'
MCP servers on every call.
StopToolSetsis also safe to call twice —Agent.StopToolSetsskips!toolSet.IsStarted(), andmcp.Toolset.Stopdocuments itself idempotent.Test
Four new tests in
pkg/server/session_manager_test.go, all of which fail without thefix:
TestDeleteSession_StopsSessionToolSets— the leak itself.TestBatchDeleteSessions_StopsSessionToolSets— the batch path, incl.WaitStopped.TestWaitStopped_WaitsForToolSetTeardown— teardown parks on an in-flight stream andcompletes once it drains;
WaitStoppedreturns only after the toolsets are stopped.TestDeleteSession_StopsSessionToolSetsOnlyOnce— no double-stop.task testandtask lintgreen locally, plusgo test ./pkg/server/ -race.Verified against a multi-tenant host: the per-session subprocess count returns to zero
on delete and after server shutdown (previously it stayed pinned until exit).
Noted, not fixed here
LocalRuntime.Closenever callsStopToolSets, soruntimeForSession'sfailed-construction
defer run.Close()does not deliver on its "an unclosed runtimewould leak its tool sets" comment. Real, but a
pkg/runtimebug.SessionManagerhas noClose/Shutdown, so toolsets of sessions that are neverdeleted still leak at process exit.
Team.StopToolSetsis fail-fast: the first agent error aborts the loop, leavinglater agents' toolsets running.