mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(lsp): address codex review round 1
Lifecycle and safety fixes from the external review: - Observe abort while awaiting the initialize handshake, so a server that never replies can't defeat the tool-timeout signal. - On an aborted request the server won't cancel, tear the instance down after a bounded grace instead of releasing the serialized queue with work still live (prevents overlapping document lifecycles). - Re-check provider disposal after the canonicalize/read awaits so a query can't spawn an unowned server after disposeAll(). - Read the source through one open handle (stat + read on the same fd) to close the realpath-vs-read TOCTOU; decode with a fatal UTF-8 decoder so a legitimate U+FFFD is not misclassified as invalid. - Validate and read the source BEFORE spawning a server (pre-start rejection). - Require an explicit openClose for option-form textDocumentSync. - Reject nonpositive teardown budgets and non-executable absolute commands at load; surface unsupported operations as structured LSP_UNSUPPORTED_OPERATION. - Retain the stderr tail (fatal diagnostics land at exit), not the prefix. - Catalog the seam vocabulary in docs/core-data-structures/lsp.md.
This commit is contained in:
@@ -28,6 +28,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
|
||||
| [sandbox.md](sandbox.md) | the process-confinement seam: file-effect modes, `SandboxPolicy`, `ConfinedArgv`, enforcement and fail-closed errors |
|
||||
| [code-runtime.md](code-runtime.md) | the code-execution seam: `CodeRunRequest`/`Result`, binding namespaces, captured logs, the `CodeRunFailure` taxonomy |
|
||||
| [filesystem.md](filesystem.md) | the filesystem seam: `FsTarget`, read/write/edit outcomes, observed-file state, `FsErrorCode` |
|
||||
| [lsp.md](lsp.md) | the LSP navigation seam: `LspQueryRequest`/`Result`, `LspProvider`/`Service`, four operations, `LspError` |
|
||||
| [skills.md](skills.md) | the skill service: discovery priority, `SkillSummary`/`SkillDefinition`, session-prefix catalog, model-facing `skill` loading |
|
||||
| [compaction.md](compaction.md) | the compaction seam: the `compact/*` session events, `CompactionResult`, the `CompactService` interface |
|
||||
| [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split |
|
||||
|
||||
124
docs/core-data-structures/lsp.md
Normal file
124
docs/core-data-structures/lsp.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# LSP navigation
|
||||
|
||||
The LSP seam — a [capability seam](../rfc/implemented/architecture/2026-07-15-lsp-capability-seam.md) exposing semantic code navigation on one `ctx.lsp` service, split across packages: interface ([dsh-lsp](../../packages/lsp/lsp), `ctx.lsp` + the provider registry), a generic implementation ([dsh-lsp-local](../../packages/lsp/lsp-local), a configured stdio language-server host), and consumer ([dsh-tool-lsp](../../packages/lsp/tool-lsp), the `lsp` tool schema). LSP is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A provider swap does not change how the model asks for navigation.
|
||||
|
||||
Source: [`packages/lsp/lsp/src/types.ts`](../../packages/lsp/lsp/src/types.ts)
|
||||
|
||||
## Operations and coordinates
|
||||
|
||||
The seam and model expose exactly four semantic queries; the union is closed, so adding one is a compile-enforced change across the seam, providers, and the tool. Positions and ranges are zero-based UTF-16, matching the protocol; the model-facing tool owns the one-based cursor convention and converts on the way in and out.
|
||||
|
||||
```ts type-equiv
|
||||
type LspOperation = 'definition' | 'references' | 'implementation' | 'hover'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
interface LspPosition {
|
||||
/** Zero-based line. */
|
||||
readonly line: number
|
||||
/** Zero-based UTF-16 code-unit offset within the line. */
|
||||
readonly character: number
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
interface LspRange {
|
||||
readonly start: LspPosition
|
||||
readonly end: LspPosition
|
||||
}
|
||||
```
|
||||
|
||||
## Request
|
||||
|
||||
Every field is required: `workspaceRoot` is caller-supplied, `languageId` comes from the provider's registration (not the request), and consumers own timeouts and result limits — so no field needs implementation defaulting and there is no `resolve()` step. The provider receives the caller's request plus the derived `languageId`, which only synchronizes the transient document and never participates in selection.
|
||||
|
||||
```ts type-equiv
|
||||
interface LspQueryRequest {
|
||||
/** Which semantic query to run. */
|
||||
readonly operation: LspOperation
|
||||
/** The source file to query (relative to `workspaceRoot` or absolute; the provider canonicalizes). */
|
||||
readonly filePath: string
|
||||
/** The zero-based UTF-16 cursor position to query at. */
|
||||
readonly position: LspPosition
|
||||
/** The workspace root the provider resolves against and indexes; required, never defaulted. */
|
||||
readonly workspaceRoot: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
interface LspProviderQuery extends LspQueryRequest {
|
||||
/** The LSP language id for `filePath`, from this provider's extension mapping. */
|
||||
readonly languageId: string
|
||||
}
|
||||
```
|
||||
|
||||
## Result
|
||||
|
||||
A CLOSED discriminated union: navigation operations normalize to `locations`, `hover` to content or `null`. Consumers `switch` on `kind` to exhaustiveness so a new arm breaks compilation until handled. `references` always includes declarations — the provider enforces this internally, so callers get no flag.
|
||||
|
||||
```ts type-equiv
|
||||
interface LspLocation {
|
||||
/** The target document URI (`file:` or otherwise), verbatim from the server. */
|
||||
readonly uri: string
|
||||
/** The range within the target document. */
|
||||
readonly range: LspRange
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
interface LspHover {
|
||||
/** The normalized hover text (markdown or plaintext, provider-joined). */
|
||||
readonly contents: string
|
||||
/** The range the hover applies to, when the server supplied one. */
|
||||
readonly range?: LspRange
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
type LspQueryResult =
|
||||
| { readonly kind: 'locations'; readonly locations: readonly LspLocation[] }
|
||||
| { readonly kind: 'hover'; readonly hover: LspHover | null }
|
||||
```
|
||||
|
||||
## Provider and service
|
||||
|
||||
A provider owns a stable branded `id` and an exclusive lowercase leading-dot extension map. `registerProvider` reserves the id and every extension atomically — an invalid or conflicting registration publishes nothing — and its disposer releases all reservations. Selection is per query and order-independent; no match throws `LspError` `LSP_UNAVAILABLE`. The seam exposes no protocol types, process/document controls, or generic JSON-RPC escape hatch.
|
||||
|
||||
```ts type-equiv
|
||||
interface LspProvider {
|
||||
/** Stable provider identity, reserved atomically with the extension mappings. */
|
||||
readonly id: LspProviderId
|
||||
/** Lowercase leading-dot extension → LSP language id (e.g. `{ '.ts': 'typescript' }`). */
|
||||
readonly extensionToLanguage: Readonly<Record<string, string>>
|
||||
/**
|
||||
* Run one query. The seam has already selected this provider and derived `languageId`.
|
||||
* @param request - the resolved provider query (caller request + derived language id).
|
||||
* @param signal - optional cancellation; the provider stops its own work when it aborts.
|
||||
* @returns the normalized, closed-union result.
|
||||
*/
|
||||
query(request: LspProviderQuery, signal?: AbortSignal): Promise<LspQueryResult>
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
interface LspService {
|
||||
/**
|
||||
* Register a provider, atomically reserving its id and every normalized extension. Any conflict
|
||||
* or invalid input publishes nothing and throws `LspError`; the returned disposer releases all
|
||||
* reservations. Disposed with the calling fiber.
|
||||
* @param provider - the backend to register.
|
||||
* @returns a synchronous disposer releasing the id and all extension reservations.
|
||||
*/
|
||||
registerProvider(provider: LspProvider): () => void
|
||||
/**
|
||||
* Select a provider by the file's extension and run one query. Selection is per-query and
|
||||
* order-independent; no match throws `LspError` `LSP_UNAVAILABLE`.
|
||||
* @param request - the normalized query.
|
||||
* @param signal - optional cancellation forwarded to the selected provider.
|
||||
* @returns the normalized, closed-union result.
|
||||
*/
|
||||
query(request: LspQueryRequest, signal?: AbortSignal): Promise<LspQueryResult>
|
||||
}
|
||||
```
|
||||
|
||||
`LspProviderId` is the seam's branded id (`Branded<'LspProviderId'>` from [dsh-brand](../../packages/util/brand)); `LspError` extends `HarnessError` with a stable `code` (`LSP_INVALID_PROVIDER`, `LSP_CONFLICT`, `LSP_UNAVAILABLE`, `LSP_UNSUPPORTED_OPERATION`) callers route on instead of parsing `message`.
|
||||
Reference in New Issue
Block a user