Files
deepseek-harness/packages/lsp/lsp-local/src/host.ts
Tianyi Cui e385c11e8e fix(pty,subprocess,lsp): address review round on cancellation, lookup, and contracts
- pty-local: a pre-write inspection rejection no longer releases a canceled
  send while its foreground SIGINT is in flight; the interrupt path retains
  the slot and its post-signal tail resumes polling. Regression pins the
  failure-shaped cancellation and a close-during-write release.
- pty-local: SEND_ACTIVE names which provider operation is draining; README
  states the never-settling-provider outcome (slot retained, close recovers).
- subprocess-local: resolveExecutable rejects relative paths containing
  separators instead of expanding them per PATH entry with a misleading
  not-found error; seam JSDoc pins the rule for every provider.
- subprocess-local: LocalTerminalHandle documents why the seam's
  in-flight-join promise holds without operation tracking.
- lsp-local: the oversized-source diagnostic reports the observed byte lower
  bound; README documents that processId: null trades away server-side
  orphan cleanup after a hard-killed harness.
- pty/subprocess seams: cross-reference the twin five-member signal unions.
- pty-local: TODO markers for send-state consolidation and initialize-race
  relocation.
2026-08-08 21:27:59 +08:00

125 lines
4.3 KiB
TypeScript

/** Filesystem-seam source access for the generic stdio LSP provider. */
import { Buffer } from 'node:buffer'
import type { FileSystem, FsTarget } from '@deepseek-ai/dsh-fs'
import { throwIfAborted } from './abort.ts'
/** A canonical workspace in the filesystem/subprocess execution world. */
export interface HostWorkspace {
/** Stable filesystem identity used for provider pooling. */
readonly target: FsTarget
/** Canonical absolute path accepted as a subprocess cwd. */
readonly canonicalPath: string
/** Canonical file URI sent during LSP initialization. */
readonly fileUrl: string
}
/** A validated source and the exact URI sent to the language server. */
export interface HostSource {
/** Canonical file URI in the execution world's platform syntax. */
readonly fileUrl: string
/** Current complete UTF-8 text. */
readonly text: string
}
/**
* Resolve and validate one workspace through `ctx.fs`.
* @param fs - filesystem provider sharing the language server's execution world.
* @param workspaceRoot - caller-supplied workspace path.
* @param signal - optional cancellation around provider operations.
* @returns stable identity plus process path and file URI.
*/
export async function canonicalizeWorkspace(
fs: FileSystem,
workspaceRoot: string,
signal?: AbortSignal,
): Promise<HostWorkspace> {
throwIfAborted(signal)
let target: FsTarget
try {
target = await fs.resolve(workspaceRoot, signal === undefined ? {} : { signal })
} catch (error: unknown) {
throwIfAborted(signal)
throw new Error(`workspace root "${workspaceRoot}" cannot be resolved: ${messageOf(error)}`, { cause: error })
}
throwIfAborted(signal)
const info = await fs.stat(target, signal).catch((error: unknown) => {
throwIfAborted(signal)
throw error
})
throwIfAborted(signal)
if (info?.type !== 'directory') {
throw new Error(`workspace root "${workspaceRoot}" is not a directory`)
}
return {
target,
canonicalPath: fs.processPath(target),
fileUrl: fs.fileUrl(target),
}
}
/**
* Resolve, contain, and read one byte-bounded query source through `ctx.fs`.
* This layer owns the LSP-specific complete-document cap while the filesystem
* provider owns streaming, regular-file checks, and UTF-8 validation.
* @param fs - filesystem provider sharing the server's execution world.
* @param filePath - absolute source path or path relative to `workspace`.
* @param workspace - already-canonical workspace.
* @param maxDocumentBytes - largest complete source accepted by this host.
* @param signal - optional cancellation.
* @returns canonical file URI and current text.
*/
export async function readHostSource(
fs: FileSystem,
filePath: string,
workspace: HostWorkspace,
maxDocumentBytes: number,
signal?: AbortSignal,
): Promise<HostSource> {
throwIfAborted(signal)
let target: FsTarget
try {
target = await fs.resolve(filePath, {
cwd: workspace.canonicalPath,
...signal === undefined ? {} : { signal },
})
} catch (error: unknown) {
throwIfAborted(signal)
throw new Error(`source "${filePath}" cannot be resolved: ${messageOf(error)}`, { cause: error })
}
throwIfAborted(signal)
if (!fs.contains(workspace.target, target)) {
throw new Error(`source "${filePath}" resolves outside the workspace`)
}
const chunks: string[] = []
let bytes = 0
try {
// XXX(lsp-source-replacement): Revisit stable-handle identity only if a real query observes
// replacement between canonical containment and the provider opening this stream.
const stream = await fs.streamText(target, signal)
for await (const chunk of stream) {
throwIfAborted(signal)
bytes += Buffer.byteLength(chunk)
if (bytes > maxDocumentBytes) break
chunks.push(chunk)
}
} catch (error: unknown) {
throwIfAborted(signal)
throw new Error(`source "${filePath}" could not be read: ${messageOf(error)}`, { cause: error })
}
if (bytes > maxDocumentBytes) {
throw new Error(
`source "${filePath}" exceeds the ${maxDocumentBytes}-byte limit; reading stopped after ${bytes} bytes`,
)
}
throwIfAborted(signal)
return {
fileUrl: fs.fileUrl(target),
text: chunks.join(''),
}
}
function messageOf(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}