Description
When a toolset's Tools(ctx) method blocks indefinitely (e.g., an LSP-based MCP server that fails to start or a stdio subprocess that hangs), the startup tool loading never completes. This causes two related problems:
- The sidebar permanently shows "Loading tools…" because the final
ToolsetInfo event with Loading=false is never emitted.
- The
/quit exit path is delayed by up to 30 seconds because StopToolSets has a timeout, and the process may not exit cleanly due to leaked goroutines.
Root Cause
In pkg/runtime/runtime.go, the emitToolsProgressively function iterates over all toolsets and calls toolset.Tools(ctx) without any per-toolset timeout:
func (r *LocalRuntime) emitToolsProgressively(...) {
send(ToolsetInfo(0, true, agentName)) // ← tells sidebar "loading..."
for _, toolset := range toolsets {
ts, err := toolset.Tools(ctx) // ← NO TIMEOUT — blocks forever if toolset hangs
totalTools += len(ts)
// ...
send(ToolsetInfo(totalTools, !isLast, agentName))
}
send(ToolsetInfo(totalTools, false, agentName)) // ← never reached if blocked above
}
If a single toolset's Tools() call hangs:
emitToolsProgressively never returns
ToolsetInfo(_, false, _) is never sent, so the sidebar never updates
- The
startupEvents channel in app.New() (via EmitStartupInfo) is never closed, leaking the reading goroutine
- On
/quit, stopToolSets(t) is called in the deferred cleanup, which has a 30-second context.WithTimeout (cmd/root/run.go ~line 1019). The process appears unresponsive during this window.
Steps to Reproduce
- Configure an agent YAML with a broken or unreachable MCP toolset (e.g., an LSP server that doesn't respond):
tools:
my-lsp:
mcp:
command: npx
args: ["-y", "@some/lsp-server", "--stdio"]
- Run
docker agent run ./agent.yaml
- Observe the sidebar: "Loading tools…" stays forever.
- Type
/quit to exit.
- Observe: the process hangs for ~30 seconds before exiting (or may appear to hang indefinitely if the LSP process doesn't respond to cancellation).
Expected Behavior
- Each toolset's
Tools() call should have a reasonable timeout. If a toolset fails to enumerate its tools, it should be skipped with a warning log, not block the entire startup.
- The sidebar should show the list of tools that loaded successfully, rather than an indefinite spinner.
/quit should exit promptly without waiting for hung toolset cleanup.
Suggested Fix
-
Add a per-toolset timeout in emitToolsProgressively:
for i, toolset := range toolsets {
toolCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
ts, err := toolset.Tools(toolCtx)
cancel()
if err != nil {
slog.WarnContext(ctx, "Toolset failed to load tools, skipping",
"toolset", toolset.Name(), "error", err)
continue // skip this toolset, proceed to the next
}
totalTools += len(ts)
send(ToolsetInfo(totalTools, !isLast, agentName))
}
-
Always send a terminal ToolsetInfo even when some toolsets fail, so the sidebar resolves its loading state.
-
Consider reducing the stopToolSets timeout or making it respect context cancellation from the TUI shutdown signal.
Environment
- docker-agent version: v1.79.0
- OS: Windows via WSL2 (Debian)
- Go: 1.26
Description
When a toolset's
Tools(ctx)method blocks indefinitely (e.g., an LSP-based MCP server that fails to start or a stdio subprocess that hangs), the startup tool loading never completes. This causes two related problems:ToolsetInfoevent withLoading=falseis never emitted./quitexit path is delayed by up to 30 seconds becauseStopToolSetshas a timeout, and the process may not exit cleanly due to leaked goroutines.Root Cause
In
pkg/runtime/runtime.go, theemitToolsProgressivelyfunction iterates over all toolsets and callstoolset.Tools(ctx)without any per-toolset timeout:If a single toolset's
Tools()call hangs:emitToolsProgressivelynever returnsToolsetInfo(_, false, _)is never sent, so the sidebar never updatesstartupEventschannel inapp.New()(viaEmitStartupInfo) is never closed, leaking the reading goroutine/quit,stopToolSets(t)is called in the deferred cleanup, which has a 30-secondcontext.WithTimeout(cmd/root/run.go ~line 1019). The process appears unresponsive during this window.Steps to Reproduce
docker agent run ./agent.yaml/quitto exit.Expected Behavior
Tools()call should have a reasonable timeout. If a toolset fails to enumerate its tools, it should be skipped with a warning log, not block the entire startup./quitshould exit promptly without waiting for hung toolset cleanup.Suggested Fix
Add a per-toolset timeout in
emitToolsProgressively:Always send a terminal
ToolsetInfoeven when some toolsets fail, so the sidebar resolves its loading state.Consider reducing the
stopToolSetstimeout or making it respect context cancellation from the TUI shutdown signal.Environment