Skip to content

[lsp] Tool calls start a server per call and never tell /diagnostics about it #114

Closed

Description

@yusiwen

Context

The LSP tool path started its own server for every call and killed it at the end, and it never
told the package about it: lsp/tools.go ran Start(...) + defer srv.Close() and never wrote
client/lspAvailable. Two consequences, both measured with LSP_TEST=1 and gopls 0.23.0:

  • A cold call failed outright. The per-call server was initialized with the file as its
    workspace root (srv.Client.Initialize(fileURI)), which gopls answers with
    LSP error 0: no views. main.go registers the four LSP tools whether or not lsp.enabled is
    set and only calls lsp.Init when it is, so the shipped default reached exactly this path:
    a document_symbols call on main.go returned "" and LSP error 0: no views.
  • /diagnostics reported a configuration problem that is not one. IsAvailable() is
    lspAvailable && client != nil, and the per-call server set neither, so after a call the TUI
    said LSP not available (set lsp.enabled=true in config.json) — wrong advice with LSP on and
    gopls working — and every call paid another server start.

What to build

  • An exported Ensure(filePath) error in lsp/touch.go that starts the session's server through
    the existing lazyStart when no client exists, under the same lock contract SyncFile uses.
  • lsp/tools.go promotes that server when a workspace is configured, and keeps a per-call server
    only for a session with no workspace — rooted at the project directory, not at the file.

Acceptance

  • With {"lsp":{"enabled":true}} and gopls on PATH, a cold session whose first LSP activity
    is an lsp_symbols tool call (no file read) answers with symbols and leaves IsAvailable()
    true; a second call reuses the client (LSP: gopls started once in the log across two calls).
  • With no workspace configured, the same tool call still answers with symbols.
  • A gated scenario (LSP_TEST=1, like lsp-diagnostics) asserts the first property on screen, so
    CI's lsp job keeps it true.

Correction (2026-10-05)

The evidence this issue was filed with is retracted, and the two fix attempts it justified
changed nothing for a reason worth recording.

"Measured in #111: a tool call returned LSP client not available (30 bytes in the log)" was
measured from a stub that asked for a tool named document_symbols. No code registers that name —
the real one is lsp_symbols (lsp.ToolDocumentSymbols) — so the 30-byte result was
unknown tool: document_symbols (exactly 30 characters) from agent/agent.go, and tlog's
tool exec line prints before the agent loop looks the tool up, which is what made the call
look like it had run. The lsp/tools.go:NNN references in the original body were read the same
way, from a flow that was never entered.

Every measurement in Context above was redone with the registered tool name.

Activity

  1. yusiwen commented on Oct 4, 2026

    @yusiwen
    OwnerAuthor

    Landed in #116, rebase-merged onto master as 9a910aa..6e1a438 (6 commits). The evidence this
    issue was filed with is retracted in the body above; every number below was re-measured with the
    tool name the code actually registers.

    What the measurement showed

    Cold session, workspace configured, gopls 0.23.0, no file read before the call:

    before after
    tool result 0 bytes, err=LSP error 0: no views 223 bytes of symbols
    IsAvailable() after the call false true
    LSP: gopls started for … across two calls 0 1

    With no workspace configured — the shipped default, since main.go registers the four LSP
    tools unconditionally and only calls lsp.Init when lsp.enabled is set — the same call also
    failed with no views; it now returns symbols. So this was not a stale status line: all four LSP
    tools were non-functional by default.

    What landed

    • lsp/touch.go: Ensure(filePath) starts the session's server through the lazyStart SyncFile
      already uses, under the same lock contract.
    • lsp/tools.go: promote the session's server when a workspace is configured; the per-call server
      stays the second attempt, now rooted at the project directory instead of the file. The change
      is additive to the flow it replaces (promote → IsAvailable() → persistent, else per-call).
    • tui/testdata/scenarios/lsp-tools.scenario + an lsp stub mode + make test-tui-lsp running
      every lsp-*.scenario, with the suite skipping them by that pattern.

    Gates, all mutation-checked

    • lsp/tools_test.go: TestToolCallPromotesTheSessionServer,
      TestToolCallWithoutWorkspaceServesTheRequest (LSP_TEST=1).
      if Initialised() → if false fails the first with "a tool call left the session without a
      client" and "server started 0 times across two calls"; Initialize(dir) → Initialize(fileURI)
      fails the second with LSP error 0: no views.
    • lsp-tools.scenario: with the pre-fix lsp/ code and a rebuilt binary, it sees lsp_symbols and
      then times out waiting for No LSP diagnostics. (exit 1).

    Counts on the merged revision

    LSP_TEST=1 go test ./lsp/ ok · go test ./... -count=1 13 packages ok · go vet ./... clean ·
    make staticcheck exit 0 · make test-tui-scenarios 26 ok / 3 skipped (live-answer, both
    lsp-*) · make test-tui-lsp twice in a row → 4× ok: 14 steps, exit 0 both times · CI: 8 jobs
    success, annotations at the documented baseline (1 per job, 2 on browser and tui-visual).

    Handed off

    Nothing open from this issue. Two limits are recorded in docs/tui-verification.md rather than
    claimed: /diagnostics's listing outcome still needs real diagnostics present, and the
    "persistent client asked about another project's file" case was not measured.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions