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.
Summary
experimental/serverStatusis documented as a status line for the end user, andquiescentanswers "is there pending background work?". A client that needs to know whether an answer toreferences,workspace/symbol,rename, … is complete has to readquiescentas "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 atserverStatus; 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):Cargo.toml: the first notification, 5 ms afterinitialized, is{health: "warning", quiescent: true, message: "Failed to discover workspace. …"}. Nothing has been loaded and the fetch has not started (GlobalState::runreports the status beforefetch_workspaces_queue.request_op("startup")).quiescent: truehere means "nothing in flight", not "ready". 1 ms later:{health: "error", quiescent: true, …}.{quiescent: false}at 5 ms,{quiescent: true}at 1.7 s. The initiallast_reported_statusisquiescent: true, so a client that missed a notification (there is no request form) has to assume the server is ready.healthstatus 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
referencesright after starting the server and take an empty answer as a fact (anthropics/claude-code#76870). Zed already readsserverStatus(crates/project/src/lsp_store/rust_analyzer_ext.rs), so there is a consumer for whichever shape is chosen.Option A: a
readinessfield inServerStatusParamsBranch: https://github.com/tagawa0525/rust-analyzer/tree/server-status-readiness
Computed from the same facts as
quiescent:initializingwhileworkspacesis empty and no load has failed,readywhenis_fully_ready(),indexingotherwise. The initiallast_reported_statusstarts atinitializing, 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 withoutCargo.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
readycovers.Option B: a successor,
experimental/serverStateBranch: https://github.com/tagawa0525/rust-analyzer/tree/server-state
A request
experimental/serverStateanswering{health, readiness, message}at any time afterinitialize; a notificationexperimental/serverStateChangedsent whenhealthorreadinesschanges, if the client declaresexperimental.serverState; and a server capabilityserverStateProviderthat declares the guarantees by naming what is missing (coverage: {scope: "workspace", incomplete: {"workspace/symbol": <workspace.symbol.search.limit>}},freshness: {fileChanges: ["Created", "Changed", "Deleted"]}).serverStatusis untouched. An undiscovered workspace ishealth: "error"there (nothing workspace-wide can be answered), which A leaves atwarningfor 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;
serverStatusis 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
quiescenttoday, 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 passcargo xtask tidyand therust-analyzerlib tests, withlsp-extensions.mdand its hash updated.