Skip to content

Tool loading hangs indefinitely when a toolset's Tools() blocks — sidebar "Loading tools…" never resolves and /quit shutdown delays #3137

@uzaidabin

Description

@uzaidabin

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:

  1. The sidebar permanently shows "Loading tools…" because the final ToolsetInfo event with Loading=false is never emitted.
  2. 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

  1. 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"]
  2. Run docker agent run ./agent.yaml
  3. Observe the sidebar: "Loading tools…" stays forever.
  4. Type /quit to exit.
  5. 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

  1. 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.
  2. The sidebar should show the list of tools that loaded successfully, rather than an indefinite spinner.
  3. /quit should exit promptly without waiting for hung toolset cleanup.

Suggested Fix

  1. 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))
    }
  2. Always send a terminal ToolsetInfo even when some toolsets fail, so the sidebar resolves its loading state.

  3. 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

Metadata

Metadata

Assignees

Labels

area/agentFor work that has to do with the general agent loop/agentic features of the apparea/mcpMCP protocol, MCP tool servers, integrationarea/toolsFor features/issues/fixes related to the usage of built-in and MCP toolsarea/tuiFor features/issues/fixes related to the TUI

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions