Skip to content

experimental/serverStatus: tell clients when answers to workspace-wide requests are complete (a readiness field, or a successor notification) #23331

Description

@tagawa0525

Summary

experimental/serverStatus is documented as a status line for the end user, and quiescent answers "is there pending background work?". A client that needs to know whether an answer to references, workspace/symbol, rename, … is complete has to read quiescent as "ready", and that reading is wrong at the moment it matters most: before the first workspace has been loaded, the server is trivially quiescent. #10888 asked for a readiness notification and was closed as "already exists" pointing at serverStatus; this issue is about the part that does not exist yet, with two implementations to choose from. I will open a PR for whichever you prefer.

What a client sees today

Measured with rust-analyzer 2026-08-03 (nixpkgs) over stdio, the client declaring experimental.serverStatusNotification (script: https://github.com/tagawa0525/lsp-det/blob/main/scripts/rust-analyzer/status-probe.py):

  • A directory without Cargo.toml: the first notification, 5 ms after initialized, is {health: "warning", quiescent: true, message: "Failed to discover workspace. …"}. Nothing has been loaded and the fetch has not started (GlobalState::run reports the status before fetch_workspaces_queue.request_op("startup")). quiescent: true here means "nothing in flight", not "ready". 1 ms later: {health: "error", quiescent: true, …}.
  • A one-crate project: {quiescent: false} at 5 ms, {quiescent: true} at 1.7 s. The initial last_reported_status is quiescent: true, so a client that missed a notification (there is no request form) has to assume the server is ready.
  • The doc says "this functionality is intended primarily to inform the end user … Clients are discouraged from but are allowed to use the health status to decide if it's worth sending a request." There is no field a client may rely on for completeness.

Coding agents are the client that needs this: they send references right after starting the server and take an empty answer as a fact (anthropics/claude-code#76870). Zed already reads serverStatus (crates/project/src/lsp_store/rust_analyzer_ext.rs), so there is a consumer for whichever shape is chosen.

Option A: a readiness field in ServerStatusParams

Branch: https://github.com/tagawa0525/rust-analyzer/tree/server-status-readiness

interface ServerStatusParams {
    health: "ok" | "warning" | "error",
    quiescent: boolean,
    /// initializing: no workspace loaded yet.
    /// indexing: workspaces are being (re)loaded or caches primed; answers may be incomplete.
    /// ready: fully loaded; answers are complete.
    readiness: "initializing" | "indexing" | "ready",
    message?: string,
}

Computed from the same facts as quiescent: initializing while workspaces is empty and no load has failed, ready when is_fully_ready(), indexing otherwise. The initial last_reported_status starts at initializing, so the notification traffic does not change. The doc paragraph is amended: this field, unlike the others, is meant for clients that decide whether to trust an answer. quiescent, the VS Code extension and every other client keep working as they are.

Measured on the branch: initializing (4 ms) → indexing (0.2 s) → ready (0.54 s) on the one-crate project; initializing{health: "error", readiness: "ready"} on the directory without Cargo.toml (a failed load is reported on the health axis, and readiness says the failure is settled).

Smallest change. What it does not give: a request form (a client that attaches late waits for the next notification), and a declaration of what ready covers.

Option B: a successor, experimental/serverState

Branch: https://github.com/tagawa0525/rust-analyzer/tree/server-state

A request experimental/serverState answering {health, readiness, message} at any time after initialize; a notification experimental/serverStateChanged sent when health or readiness changes, if the client declares experimental.serverState; and a server capability serverStateProvider that declares the guarantees by naming what is missing (coverage: {scope: "workspace", incomplete: {"workspace/symbol": <workspace.symbol.search.limit>}}, freshness: {fileChanges: ["Created", "Changed", "Deleted"]}). serverStatus is untouched. An undiscovered workspace is health: "error" there (nothing workspace-wide can be answered), which A leaves at warning for compatibility.

The vocabulary follows the server state protocol specification (https://github.com/tagawa0525/lsp-det/blob/main/docs/spec/server-state.md), written as a candidate for LSP itself; serverStatus is the closest existing vocabulary and the one it was modelled on.

Either way

lsp-det, a transparent proxy that derives the same three values from quiescent today, reads the field when present (A) or passes the notification through untouched (B), so agents that already sit behind it are covered by both. Both branches pass cargo xtask tidy and the rust-analyzer lib tests, with lsp-extensions.md and its hash updated.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions