import { LspDiagnosticsRegistry } from "@sema-agent/core"; import type { ExecutionEnv } from "@sema-agent/core"; import type { LspServerManager, LspSession, LspTransport } from "./types.js"; /** The E2B surface the LSP needs beyond base ExecutionEnv (present on RemoteContainerExecutionEnv). * `exec`'s `stdout` is optional/structural (§DESIGN-V2 F7) — additive to the existing `stderr`-only shape so * the postmortem/install-failure tail reads (`tail -n … `, whose output arrives on stdout) can use the * SAME duck-typed surface without widening what a plain command result MUST carry. */ export interface LspCapableEnv { exec(command: string): Promise<{ ok: boolean; value?: { exitCode: number; stderr: string; stdout?: string; }; }>; startBackground(command: string): Promise; writeFile(path: string, content: string): Promise<{ ok: boolean; }>; getHost(port: number): Promise; readTextFile(path: string): Promise<{ ok: boolean; value?: string; }>; workspaceHandle(): { mountPath: string; }; } /** * §DESIGN-V2 B/F5/F7: the fixed, model-facing failure vocabulary for `unavailableReason`. Three are STATIC * (re-derivable synchronously from `filePath`+`env` alone, without ever having attempted an open — * `env-not-lsp-capable` / `language-not-mapped` / `no-server-command`); the other five are DYNAMIC (only * knowable after an actual open attempt, so they're recorded in `lastFailure` per env+language and read back). * Short + stable by design — a model reads these directly (core lsp.js renders `why` straight into the tool's * degrade text), so adding/renaming a word is a behavior-facing change, not a refactor. */ export type LspUnavailableReason = "env-not-lsp-capable" | "language-not-mapped" | "no-server-command" | "install-failed" | "bridge-start-failed" | "bridge-connect-exhausted" | "bridge-restart-budget-exhausted" | "session-open-failed"; /** §DESIGN-V2 F5: the transport factory's discriminated result — replaces the old plain `LspTransport|undefined` * so the bridge-resource owner (e2b-manager's `transportFactory`) can report WHY an attempt failed directly * through its return value, instead of a side channel that could drift from what actually happened. */ export type LspFactoryResult = { ok: true; transport: LspTransport; } | { ok: false; reason: LspUnavailableReason; }; /** Duck-type check: is this ExecutionEnv an E2B adapter with the LSP surface? */ export declare function isLspCapable(env: ExecutionEnv): env is ExecutionEnv & LspCapableEnv; export interface E2bLspManagerOptions { /** Opens (+ `initialize`s) a WS transport to the language server inside THIS env's sandbox. */ transportFactory: (language: string, env: LspCapableEnv) => Promise; /** Override the file-extension → languageId map. */ extToLang?: Readonly>; /** Observability hook (open/heal/degrade events) — prod wires the service logger; silent when unset. */ log?: (event: string, fields: Record) => void; /** §DESIGN-V2 F5: synchronous re-derivation of the "no-server-command" static reason — owned by the factory * (e2b-manager's `SERVER_CMD` table), which this generic manager doesn't import. `undefined` (option unset) * or a `true` return ⇒ a server command IS configured, so a miss (if any) is dynamic (read from * `lastFailure`) rather than this static one. */ hasServerCommand?: (language: string) => boolean; /** §DESIGN-V2 E: `lsp_session_open_total{language,outcome}` / `lsp_session_heal_total{outcome}` — fired at * the SAME point as the matching `log()` call (event and metric share one source, never drift apart). */ metrics?: { inc(name: string, labels?: Record): void; }; } /** * §DESIGN-V2 F1/F8/F9 —— 本文件曾**手抄**一份 core 的 `settleOnAbort`(注释自陈 core「un-exported」,并写下 * 「一旦 core 导出即 find-and-delete」的计划)。[ref]([ref])执行了那句话:core 现已从包根导出它 * (`index.d.ts`,来自 `core/lsp.js`),手抄件遂删除,本文件的两处调用直接消费 core 的实现。 * * 它的语义(读点在 `open` 的两处调用):给**这一个**调用方一个自己的 settle —— 调用方的 `signal` 让它拿到 * `onAbort()` 的返回,而**共享作业本身不受打扰**(别的调用方可能还等着它)。铁律仍在:绝不能把 signal 穿进 * 作业里(F9:共享的 open/heal 链不接受 per-caller signal,见 `open` 的文档)。 */ export declare class E2bLspManager implements LspServerManager { /** Per-sandbox session cache: env (weak) → language → the (self-contained, see `startJob`) shared job. */ private readonly perEnv; /** §DESIGN-V2 B/F10: env (weak) → language → the reason the MOST RECENT open/heal attempt failed. Cleared * on a successful open (F10) so a stale reason never outlives the failure that produced it. */ private readonly lastFailure; private readonly factory; private readonly extToLang; private readonly log; private readonly hasServerCommand; private readonly metrics; /** [ref] (core 1.220, board [S] ①): the workspace diagnostics registry — WORKSPACE-scoped (survives * heal/evict; diagnostics belong to files, not servers), drained by the Runner at turn boundaries. Every * session feeds it from `textDocument/publishDiagnostics` (the WS transport now dispatches notifications). * ⚠️ ONE registry across sandboxes on this manager instance (it is per-deployment) — same posture as * NodeLspManager's workspace scoping; a task only sees diagnostics for files IT touched (delivered-set + * fileEdited keying is per-uri, and sandbox workspace paths are per-task-unique). */ readonly diagnostics: LspDiagnosticsRegistry; constructor(opts: E2bLspManagerOptions); /** * §DESIGN-V2 B: the core 1.86.2+ seam's optional method — WHY the most recent `sessionFor` miss for this * file happened. Three reasons are STATIC (re-derived here fresh, every call, from `filePath`+`env` alone — * never stored, since they don't need an open attempt to be knowable); the rest are DYNAMIC, read back from * `lastFailure`. `undefined` = this exact (env, language) was never attempted (NOT "available" — a * best-effort postmortem surface, per the seam's own contract). */ unavailableReason(filePath: string, env?: ExecutionEnv): LspUnavailableReason | undefined; sessionFor(filePath: string, signal?: AbortSignal, env?: ExecutionEnv): Promise; /** * §DESIGN-V2 F1 (aligned with core's `NodeLspManager.open`, node-lsp-manager.js `open` — same self- * contained-job shape, not the same code: that manager also tracks a TTL/LRU cache and per-job * `SharedAbortScope`, which this one has no equivalent of per F9). The returned job is SELF-completing: * * - self-caches: registers itself into `sessions` synchronously BEFORE returning (so a concurrent caller * in the SAME microtask sees it — no dedupe race); * - self-cleans: on failure, removes ITSELF from `sessions` (never a newer entry a racer already installed * — the `sessions.get(language) === job` check) so the next request retries instead of caching a miss; * - self-warms: a heal job (`heal` set) `warmOpen`s the carried files as part of settling, before anyone * can observe it as "done" — no caller-side extra step, no window where a racer sees a warm-less session; * - self-closes an orphan: if a CONCURRENT heal already replaced this job in `sessions` before it settled * (this job "lost" the race), the just-opened transport is closed instead of leaked with no owner. * * Callers must NEVER wrap `settleOnAbort` around a naive `await` of a SHARED promise and then branch the * logging/cache-eviction on THAT wrapped value — a caller whose OWN signal aborted would then look * indistinguishable from the job itself failing, misfiring `lsp_session_*_failed` and evicting a still-live * in-flight entry out from under a concurrent caller (§DESIGN-V2 F1). `sessionFor` only ever wraps the * RETURN boundary; every cache/log decision above happens INSIDE the job, driven by the job's own outcome. */ private startJob; private open; } //# sourceMappingURL=manager.d.ts.map