Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,17 @@ must read that live cache before disk so unsaved editor buffers can identify the
requested token, but provider results remain conservative and index-backed:
return empty arrays or null when the database cannot answer safely instead of
inventing language-server analysis.
`LspServer` uses one lock-protected lifecycle state machine across ordinary
dispatch, the cancellation fast path, and queue-overload responses. Its phases
are before-initialize, initializing, running, shutdown, and exited. Only the
first `initialize` request can enter initialization. The transport reserves each
frame's lifecycle action in receive order. Under the output gate, the serialized
initialize response and the transition to running share one publication boundary:
the state changes immediately before the frame write starts. Shutdown changes
phase before waiting for active dispatches, then disposes an owned query context
and reader exactly once.
After shutdown, requests receive `-32600`, notifications are ignored, and only
the `exit` notification completes the normal lifecycle.
Disk-backed position-line caching must enforce its 4 MiB input limit while
streaming, not only through a pre-read `Length` check. Bytes beyond the limit
must never reach text decoding, including when a shared file grows concurrently,
Expand Down Expand Up @@ -3627,6 +3638,15 @@ editor integration は標準的な location 形状を直接要求できる。`de
request token を特定できるよう disk より先に live cache を読む必要があるが、provider result は
保守的かつ index-backed のままにする。database が安全に答えられない場合は、language-server
analysis を作り上げず、空配列または null を返す。
`LspServer` は通常 dispatch、cancellation fast path、queue-overload response の全経路で、
1 つの lock 保護された lifecycle state machine を使う。phase は before-initialize、
initializing、running、shutdown、exited である。最初の `initialize` request だけが初期化へ
遷移できる。transport は各 frame の lifecycle action を受信順で予約する。output gate の下で、
serialize 済み initialize response と running への遷移は 1 つの公開境界を共有し、frame の
書き込み開始直前に state を変更する。shutdown は active dispatch の完了待ちより先に phase を
変更し、その後に所有する query context と reader を正確に 1 回だけ破棄する。shutdown 後の
request は `-32600` を返し、notification は無視し、`exit` notification だけが正常な lifecycle を
完了させる。
disk 上の position-line cache は、事前の `Length` check だけでなく streaming 中も 4 MiB の
input 上限を強制する必要がある。共有 file が同時に増大する場合も上限超過 byte を text decode に
渡してはならず、bounded な failure reason は `position_file_too_large` のままとする。
Expand Down
14 changes: 14 additions & 0 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2702,6 +2702,14 @@ but do not speak MCP. It also advertises full `textDocument` sync and
conservative `hover`, `completion`, `documentHighlight`, `semanticTokens/full`,
`codeLens`, and `inlayHint` providers backed by indexed symbols and references
where available.
Clients must follow the standard LSP lifecycle: send one `initialize` request
first, optionally send the `initialized` notification after its response, then
send ordinary requests, finish with one `shutdown` request, and finally send the
`exit` notification. Requests received before initialization completes return
JSON-RPC `-32002` (`Server not initialized`). A duplicate `initialize`, or any
request received after `shutdown`, returns `-32600` (`Invalid Request`);
out-of-phase notifications are ignored. Sending `exit` before a successful
`shutdown` terminates the server with a usage error.
`textDocument/inlayHint` honors the requested LSP range (including its exclusive
end position) and omits type labels when the indexed return type is already
written immediately before the symbol name, so explicit field, property, and
Expand Down Expand Up @@ -5813,6 +5821,12 @@ MCP stdio は line protocol です。LF 区切りの各行に compact な UTF-8
indexed symbols / references で答えられる範囲に限定した `hover`、`completion`、
`documentHighlight`、`semanticTokens/full`、`codeLens`、`inlayHint` provider を
advertise します。
client は標準の LSP lifecycle に従う必要があります。最初に `initialize` request を 1 回だけ
送り、その response 後に必要なら `initialized` notification を送り、通常 request を処理した後、
`shutdown` request、最後に `exit` notification の順で終了してください。初期化完了前に受信した
request は JSON-RPC `-32002`(`Server not initialized`)を返します。重複した `initialize`、
または `shutdown` 後の request は `-32600`(`Invalid Request`)を返し、順序外の notification は
無視します。成功した `shutdown` より前に `exit` を送ると、server は usage error で終了します。
`textDocument/inlayHint` は end position を含まない requested LSP range を尊重し、
indexed return type が symbol name の直前にすでに明記されている場合は type label を
省略するため、field / property / method の明示型を hint として重複表示しません。
Expand Down
20 changes: 20 additions & 0 deletions changelog.d/unreleased/4849.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
category: fixed
issues:
- 4849
affected:
- src/CodeIndex/Lsp/LspServer.cs
- src/CodeIndex/Lsp/LspServer.Protocol.cs
- tests/CodeIndex.Tests/LspServerTests.cs
- tests/CodeIndex.Tests/LspServerBudgetTests.cs
- USER_GUIDE.md
- DEVELOPER_GUIDE.md
---

## English

- **LSP sessions now enforce the initialize-to-exit lifecycle (#4849)** — Requests before initialization return `ServerNotInitialized`, duplicate initialization and post-shutdown requests return `InvalidRequest`, and out-of-phase notifications are ignored. Shutdown waits for active dispatches and disposes owned query resources exactly once, preventing the #2830 regression across direct, cancellation, and overload dispatch paths.

## 日本語

- **LSP session で initialize から exit までの lifecycle を強制するようにしました (#4849)** — 初期化前の request は `ServerNotInitialized`、重複初期化と shutdown 後の request は `InvalidRequest` を返し、順序外の notification は無視します。shutdown は active dispatch の完了を待ち、所有する query resource を正確に 1 回だけ破棄するため、direct、cancellation、overload の各 dispatch 経路で #2830 の regression を防ぎます。
29 changes: 23 additions & 6 deletions src/CodeIndex/Lsp/LspServer.Protocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,14 @@ private async Task WriteResponseMessageAsync(
Stream output,
SemaphoreSlim outputGate,
JsonObject response,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
Action? responsePublicationStarting = null)
{
await outputGate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var payload = response.ToJsonString(_jsonOptions);
responsePublicationStarting?.Invoke();
if (await LspProtocol.TryWriteMessageAsync(output, payload, cancellationToken).ConfigureAwait(false))
return;

Expand Down Expand Up @@ -360,12 +362,27 @@ internal static bool TryWriteMessage(Stream output, string payload, out int body

public void Dispose()
{
_ = _shutdownRequested;
if (_ownedQueryDb != null)
lock (_sessionStateGate)
{
_reader.Dispose();
_ownedQueryDb.Dispose();
_ownedQueryDb = null;
_sessionState = LspSessionState.Exited;
while (_activeSessionDispatches != 0)
Monitor.Wait(_sessionStateGate);
}

DisposeOwnedResourcesOnce();
}

private void DisposeOwnedResourcesOnce()
{
if (Interlocked.Exchange(ref _ownedResourcesDisposed, 1) != 0)
return;

var ownedQueryDb = Interlocked.Exchange(ref _ownedQueryDb, null);
if (ownedQueryDb == null)
return;

_reader.Dispose();
ownedQueryDb.Dispose();
Interlocked.Increment(ref _ownedResourceDisposeCount);
}
}
Loading
Loading