@sema-agent/core 2.12.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/agent-transcript-tool.d.ts +1 -0
- package/dist/agents/agent-transcript-tool.js +1 -1
- package/dist/agents/cascade.d.ts +4 -0
- package/dist/agents/cascade.js +54 -9
- package/dist/agents/repair-loop.d.ts +2 -0
- package/dist/agents/repair-loop.js +101 -2
- package/dist/agents/retain-ledger.d.ts +18 -2
- package/dist/agents/retain-ledger.js +37 -8
- package/dist/agents/roster-store.d.ts +4 -0
- package/dist/agents/roster-store.js +22 -1
- package/dist/agents/teacher.js +4 -2
- package/dist/agents/team.js +7 -4
- package/dist/agents/verify.d.ts +15 -2
- package/dist/agents/verify.js +36 -1
- package/dist/bin/sema-tb.js +2 -2
- package/dist/brain/circuit-breaker.d.ts +1 -0
- package/dist/brain/circuit-breaker.js +26 -9
- package/dist/brain/degrading.js +13 -1
- package/dist/brain/failover.js +2 -0
- package/dist/brain/repetition.js +10 -1
- package/dist/brain/routing.js +5 -1
- package/dist/brain/status-sink.js +5 -1
- package/dist/brain/stream-engine.js +4 -1
- package/dist/brain/tool-call-repair.js +32 -14
- package/dist/core/auto-compaction.js +17 -7
- package/dist/core/auto-promote.js +2 -1
- package/dist/core/checkpoint-store.d.ts +1 -0
- package/dist/core/checkpoint-store.js +28 -10
- package/dist/core/consolidate-scope.js +4 -3
- package/dist/core/context-edit.js +3 -0
- package/dist/core/context-guard.js +3 -0
- package/dist/core/git-worktree-env.d.ts +6 -1
- package/dist/core/git-worktree-env.js +17 -1
- package/dist/core/hooks.d.ts +1 -0
- package/dist/core/hooks.js +40 -2
- package/dist/core/lsp-diagnostics.js +13 -7
- package/dist/core/lsp-protocol.d.ts +1 -1
- package/dist/core/lsp-protocol.js +20 -8
- package/dist/core/lsp-session.d.ts +12 -2
- package/dist/core/lsp-session.js +114 -49
- package/dist/core/lsp.d.ts +13 -1
- package/dist/core/lsp.js +64 -9
- package/dist/core/mcp.d.ts +1 -0
- package/dist/core/mcp.js +4 -3
- package/dist/core/memory-engine/data-plane.js +4 -0
- package/dist/core/memory-engine/engine.d.ts +2 -1
- package/dist/core/memory-engine/engine.js +82 -7
- package/dist/core/memory-engine/file-backend.d.ts +1 -0
- package/dist/core/memory-engine/file-backend.js +13 -3
- package/dist/core/memory-engine/layout.js +12 -1
- package/dist/core/memory.js +3 -0
- package/dist/core/permission-rules.d.ts +2 -1
- package/dist/core/permission-rules.js +24 -3
- package/dist/core/runner/active-skill-scope.js +34 -6
- package/dist/core/runner/assemble-result.d.ts +1 -1
- package/dist/core/runner/assemble-result.js +0 -1
- package/dist/core/runner/prepare-task.js +23 -3
- package/dist/core/runner/runtask.js +48 -30
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +26 -7
- package/dist/core/runner/usage-accounting.d.ts +6 -0
- package/dist/core/runner/usage-accounting.js +9 -8
- package/dist/core/secret-env.js +5 -1
- package/dist/core/sensitive-path-policy.js +9 -3
- package/dist/core/session-store.js +15 -4
- package/dist/core/skill-tool-specifier.d.ts +8 -0
- package/dist/core/skill-tool-specifier.js +58 -0
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +16 -4
- package/dist/core/store-contracts/session-repo-contract.js +40 -0
- package/dist/core/task-registry-monitor.js +7 -1
- package/dist/core/tool-policy.js +65 -17
- package/dist/core/trace.d.ts +1 -0
- package/dist/core/types.d.ts +3 -5
- package/dist/core/with-retry.js +0 -1
- package/dist/engine/compaction/compaction.d.ts +1 -0
- package/dist/engine/compaction/compaction.js +14 -5
- package/dist/engine/lsp/frame-decoder.js +6 -3
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +103 -26
- package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
- package/dist/index.d.ts +1 -1
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/workflow-governance.js +43 -9
- package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
- package/dist/orchestration/workflow-script-store.js +34 -4
- package/dist/orchestration/workflow.js +10 -6
- package/dist/prompt-assembly/artifact-store.d.ts +2 -0
- package/dist/prompt-assembly/artifact-store.js +39 -24
- package/dist/prompt-assembly/assemble.js +38 -7
- package/dist/prompt-assembly/epoch.js +19 -2
- package/dist/prompt-assembly/event-registry.js +3 -3
- package/dist/prompt-assembly/explain.js +1 -1
- package/dist/prompt-assembly/tool-catalog.js +16 -1
- package/dist/prompt-assembly/turn-snapshot.js +4 -1
- package/dist/stores/file/fs-atomic.d.ts +3 -0
- package/dist/stores/file/fs-atomic.js +53 -8
- package/dist/stores/file/index.d.ts +1 -0
- package/dist/stores/file/index.js +7 -0
- package/dist/stores/file/session-store.d.ts +2 -0
- package/dist/stores/file/session-store.js +28 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
- package/dist/tools/fs/bash-readonly-classifier.js +57 -8
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +93 -5
- package/dist/tools/loop-tick.js +1 -1
- package/dist/tools/monitor.js +55 -9
- package/dist/tools/scheduler-tools.js +9 -3
- package/dist/tools/web.d.ts +8 -2
- package/dist/tools/web.js +46 -17
- package/dist/tools/worktree.d.ts +2 -0
- package/dist/tools/worktree.js +233 -143
- package/package.json +5 -2
|
@@ -28,6 +28,11 @@ export async function addWorktree(baseEnv, opts) {
|
|
|
28
28
|
const detail = add.ok ? add.value.stderr || add.value.stdout : String(add.error);
|
|
29
29
|
throw new Error(`git worktree add failed: ${detail}`);
|
|
30
30
|
}
|
|
31
|
+
const excludeLine = shq(`/${WORKTREE_PARENT}/`);
|
|
32
|
+
await baseEnv
|
|
33
|
+
.exec(`ex="$(git rev-parse --git-path info/exclude)" && mkdir -p "$(dirname "$ex")" && ` +
|
|
34
|
+
`{ grep -qxF ${excludeLine} "$ex" 2>/dev/null || printf '%s\\n' ${excludeLine} >> "$ex"; }`, { cwd: opts.repoRoot })
|
|
35
|
+
.catch(() => undefined);
|
|
31
36
|
let inner;
|
|
32
37
|
try {
|
|
33
38
|
inner = await opts.rootEnvAt(worktreeDir);
|
|
@@ -54,5 +59,16 @@ export async function addWorktree(baseEnv, opts) {
|
|
|
54
59
|
return { env, worktreeDir };
|
|
55
60
|
}
|
|
56
61
|
export async function pruneWorktrees(baseEnv, repoRoot) {
|
|
57
|
-
|
|
62
|
+
try {
|
|
63
|
+
const r = await baseEnv.exec("git worktree prune", { cwd: repoRoot });
|
|
64
|
+
if (r.ok && r.value.exitCode === 0)
|
|
65
|
+
return { ok: true };
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
detail: r.ok ? (r.value.stderr || r.value.stdout || `exit ${r.value.exitCode}`).trim() : String(r.error),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return { ok: false, detail: String(err) };
|
|
73
|
+
}
|
|
58
74
|
}
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -121,6 +121,7 @@ export interface ToolGateInput {
|
|
|
121
121
|
approvalTimeoutMs?: number;
|
|
122
122
|
abortSignal?: AbortSignal;
|
|
123
123
|
permissionDenied?: (payload: PermissionDeniedPayload) => void | Promise<void>;
|
|
124
|
+
onHookError?: (err: unknown) => void;
|
|
124
125
|
shellGated?: boolean;
|
|
125
126
|
autoMode?: {
|
|
126
127
|
decider: import("./auto-mode.js").AutoModeDecider;
|
package/dist/core/hooks.js
CHANGED
|
@@ -16,6 +16,20 @@ export function cloneObserverInput(input) {
|
|
|
16
16
|
export function formatHookFeedback(text) {
|
|
17
17
|
return `<system-reminder>\n${text}\n</system-reminder>`;
|
|
18
18
|
}
|
|
19
|
+
function preToolUseCrashReason(subject, err) {
|
|
20
|
+
const raw = err instanceof Error ? err.message.trim() || err.name : String(err);
|
|
21
|
+
const cause = inlineUntrusted(raw, 200);
|
|
22
|
+
return (`a PreToolUse hook crashed while screening ${subject}; the call was NOT executed (fail-closed). ` +
|
|
23
|
+
`This is a failure of the deployment's hook, NOT of the tool itself — an identical retry reaches the ` +
|
|
24
|
+
`same hook and fails the same way. Hook error: ${cause || "(no message)"}`);
|
|
25
|
+
}
|
|
26
|
+
function traceHookCrash(input, err) {
|
|
27
|
+
try {
|
|
28
|
+
input.onHookError?.(err);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
19
33
|
function withProbeTimeout(p, ms, signal) {
|
|
20
34
|
if (ms === undefined && signal === undefined)
|
|
21
35
|
return p;
|
|
@@ -49,7 +63,22 @@ export async function runToolGate(input) {
|
|
|
49
63
|
const preToolContext = [];
|
|
50
64
|
let hookAsk;
|
|
51
65
|
if (preToolUse) {
|
|
52
|
-
|
|
66
|
+
let r;
|
|
67
|
+
try {
|
|
68
|
+
r = await preToolUse(toolName, currentInput, { toolCallId, toolName });
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const reason = preToolUseCrashReason(`this call to "${toolName}"`, err);
|
|
72
|
+
traceHookCrash(input, err);
|
|
73
|
+
if (input.permissionDenied) {
|
|
74
|
+
try {
|
|
75
|
+
await input.permissionDenied({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook" });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { block: true, reason: formatHookFeedback(reason), preToolContext };
|
|
81
|
+
}
|
|
53
82
|
if (r) {
|
|
54
83
|
if (r.additionalContext) {
|
|
55
84
|
preToolContext.push(r.additionalContext);
|
|
@@ -182,7 +211,16 @@ export async function runToolGate(input) {
|
|
|
182
211
|
break;
|
|
183
212
|
}
|
|
184
213
|
if (preToolUse) {
|
|
185
|
-
|
|
214
|
+
let hr;
|
|
215
|
+
try {
|
|
216
|
+
hr = await preToolUse(toolName, editArgs, { toolCallId, toolName });
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
traceHookCrash(input, err);
|
|
220
|
+
editDenied = { action: "deny", reason: preToolUseCrashReason(`the approved edit for "${toolName}"`, err) };
|
|
221
|
+
denySource = "hook";
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
186
224
|
if (hr) {
|
|
187
225
|
if (hr.additionalContext)
|
|
188
226
|
preToolContext.push(hr.additionalContext);
|
|
@@ -35,22 +35,28 @@ export class LspDiagnosticsRegistry {
|
|
|
35
35
|
}
|
|
36
36
|
drain() {
|
|
37
37
|
const out = [];
|
|
38
|
+
const requeued = new Map();
|
|
38
39
|
let total = 0;
|
|
39
40
|
for (const [uri, all] of this.pending) {
|
|
40
|
-
if (total >= MAX_TOTAL)
|
|
41
|
-
break;
|
|
42
41
|
const fresh = all
|
|
43
42
|
.filter((d) => !this.delivered.has(diagnosticKey(uri, d)))
|
|
44
|
-
.sort((a, b) => (a.severity ?? 99) - (b.severity ?? 99))
|
|
45
|
-
.slice(0, Math.min(MAX_PER_FILE, MAX_TOTAL - total));
|
|
43
|
+
.sort((a, b) => (a.severity ?? 99) - (b.severity ?? 99));
|
|
46
44
|
if (fresh.length === 0)
|
|
47
45
|
continue;
|
|
48
|
-
|
|
46
|
+
const room = Math.max(0, Math.min(MAX_PER_FILE, MAX_TOTAL - total));
|
|
47
|
+
const take = fresh.slice(0, room);
|
|
48
|
+
if (fresh.length > take.length)
|
|
49
|
+
requeued.set(uri, fresh.slice(take.length));
|
|
50
|
+
if (take.length === 0)
|
|
51
|
+
continue;
|
|
52
|
+
for (const d of take)
|
|
49
53
|
this.delivered.add(diagnosticKey(uri, d));
|
|
50
|
-
total +=
|
|
51
|
-
out.push({ uri, diagnostics:
|
|
54
|
+
total += take.length;
|
|
55
|
+
out.push({ uri, diagnostics: take });
|
|
52
56
|
}
|
|
53
57
|
this.pending.clear();
|
|
58
|
+
for (const [uri, rest] of requeued)
|
|
59
|
+
this.pending.set(uri, rest);
|
|
54
60
|
return out;
|
|
55
61
|
}
|
|
56
62
|
}
|
|
@@ -6,4 +6,4 @@ export declare function buildRequest(op: LspOperation, params: LspRequestParams)
|
|
|
6
6
|
method: string;
|
|
7
7
|
params: unknown;
|
|
8
8
|
} | undefined;
|
|
9
|
-
export declare function parseResult(op: LspOperation, raw: unknown): LspResult;
|
|
9
|
+
export declare function parseResult(op: LspOperation, raw: unknown, fallbackUri?: string): LspResult;
|
|
@@ -3,6 +3,11 @@ export function pathToUri(p) {
|
|
|
3
3
|
if (p.startsWith("/")) {
|
|
4
4
|
return `file://${p.split("/").map(encodeURIComponent).join("/")}`;
|
|
5
5
|
}
|
|
6
|
+
const drive = /^([A-Za-z]):[\\/](.*)$/.exec(p);
|
|
7
|
+
if (drive)
|
|
8
|
+
return `file:///${drive[1]}:/${drive[2].split(/[\\/]/).map(encodeURIComponent).join("/")}`;
|
|
9
|
+
if (p.startsWith("\\\\"))
|
|
10
|
+
return `file://${p.slice(2).split(/[\\/]/).map(encodeURIComponent).join("/")}`;
|
|
6
11
|
return pathToFileURL(p).href;
|
|
7
12
|
}
|
|
8
13
|
export function uriToPath(uri) {
|
|
@@ -47,7 +52,7 @@ export function buildRequest(op, params) {
|
|
|
47
52
|
return { method: "textDocument/prepareCallHierarchy", params: { ...td, position } };
|
|
48
53
|
}
|
|
49
54
|
}
|
|
50
|
-
export function parseResult(op, raw) {
|
|
55
|
+
export function parseResult(op, raw, fallbackUri) {
|
|
51
56
|
switch (op) {
|
|
52
57
|
case "goToDefinition":
|
|
53
58
|
case "findReferences":
|
|
@@ -62,7 +67,7 @@ export function parseResult(op, raw) {
|
|
|
62
67
|
case "documentSymbol":
|
|
63
68
|
case "workspaceSymbol":
|
|
64
69
|
case "prepareCallHierarchy": {
|
|
65
|
-
const symbols = toSymbols(raw);
|
|
70
|
+
const symbols = toSymbols(raw, op === "documentSymbol" ? fallbackUri : undefined);
|
|
66
71
|
return symbols.length ? { kind: "symbols", symbols } : { kind: "none" };
|
|
67
72
|
}
|
|
68
73
|
case "incomingCalls":
|
|
@@ -92,21 +97,28 @@ function toLocations(raw) {
|
|
|
92
97
|
})
|
|
93
98
|
.filter((l) => l !== undefined);
|
|
94
99
|
}
|
|
95
|
-
function toSymbols(raw) {
|
|
100
|
+
function toSymbols(raw, fallbackUri) {
|
|
96
101
|
const out = [];
|
|
97
|
-
const walk = (x,
|
|
102
|
+
const walk = (x, inheritedUri) => {
|
|
98
103
|
const loc = x.location;
|
|
99
|
-
const uri = loc?.uri ?? (typeof x.uri === "string" ? x.uri :
|
|
104
|
+
const uri = loc?.uri ?? (typeof x.uri === "string" ? x.uri : inheritedUri);
|
|
100
105
|
const range = (loc?.range ?? x.selectionRange ?? x.range);
|
|
101
106
|
if (typeof x.name === "string" && uri) {
|
|
102
|
-
const s = range?.start
|
|
103
|
-
out.push({
|
|
107
|
+
const s = range?.start;
|
|
108
|
+
out.push({
|
|
109
|
+
name: x.name,
|
|
110
|
+
...(symbolKindName(x.kind) ? { kind: symbolKindName(x.kind) } : {}),
|
|
111
|
+
uri,
|
|
112
|
+
line: (s?.line ?? 0) + 1,
|
|
113
|
+
character: s?.character ?? 0,
|
|
114
|
+
...(s === undefined ? { positionUnknown: true } : {}),
|
|
115
|
+
});
|
|
104
116
|
}
|
|
105
117
|
for (const child of asArray(x.children))
|
|
106
118
|
walk(child, uri);
|
|
107
119
|
};
|
|
108
120
|
for (const x of asArray(raw))
|
|
109
|
-
walk(x);
|
|
121
|
+
walk(x, fallbackUri);
|
|
110
122
|
return out;
|
|
111
123
|
}
|
|
112
124
|
function toCallSymbols(raw) {
|
|
@@ -11,17 +11,27 @@ export declare class TransportLspSession implements LspSession {
|
|
|
11
11
|
private readonly log;
|
|
12
12
|
private readonly warmup;
|
|
13
13
|
private readonly opened;
|
|
14
|
+
private readonly openedUris;
|
|
14
15
|
private readonly syncing;
|
|
15
16
|
private readonly createdAt;
|
|
16
|
-
private
|
|
17
|
+
private warmupBarrier?;
|
|
18
|
+
private warmupDone;
|
|
19
|
+
private warmupAttempts;
|
|
17
20
|
constructor(transport: LspTransport, languageId: string, readText: LspReadText, log?: (event: string, fields: Record<string, unknown>) => void, warmup?: SessionWarmup, diagnostics?: LspDiagnosticsRegistry);
|
|
21
|
+
get inFlight(): number;
|
|
22
|
+
private activeRequests;
|
|
18
23
|
request(op: LspOperation, params: LspRequestParams, signal?: AbortSignal): Promise<LspResult>;
|
|
24
|
+
private requestOnce;
|
|
25
|
+
private warmupBarrierOnce;
|
|
19
26
|
private syncOpenedFiles;
|
|
20
27
|
private syncOne;
|
|
21
28
|
private static classifyUnbranded;
|
|
22
29
|
get closed(): boolean;
|
|
23
30
|
openedFiles(): string[];
|
|
24
|
-
warmOpen(filePaths: string[], signal?: AbortSignal): Promise<
|
|
31
|
+
warmOpen(filePaths: string[], signal?: AbortSignal): Promise<{
|
|
32
|
+
opened: number;
|
|
33
|
+
failed: number;
|
|
34
|
+
}>;
|
|
25
35
|
notifyFileChanged(filePath: string, signal?: AbortSignal): Promise<void>;
|
|
26
36
|
close(): Promise<void>;
|
|
27
37
|
}
|
package/dist/core/lsp-session.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { uriToPath, buildRequest, callHierarchyMethod, parseResult, pathToUri } from "./lsp-protocol.js";
|
|
2
|
-
import { lspFailureOf } from "./lsp.js";
|
|
2
|
+
import { lspFailureOf, SharedAbortScope, settleOnAbort } from "./lsp.js";
|
|
3
|
+
const WARMUP_MAX_ATTEMPTS = 3;
|
|
3
4
|
export class TransportLspSession {
|
|
4
5
|
transport;
|
|
5
6
|
languageId;
|
|
@@ -7,9 +8,12 @@ export class TransportLspSession {
|
|
|
7
8
|
log;
|
|
8
9
|
warmup;
|
|
9
10
|
opened = new Map();
|
|
11
|
+
openedUris = new Map();
|
|
10
12
|
syncing = new Map();
|
|
11
13
|
createdAt = Date.now();
|
|
12
|
-
|
|
14
|
+
warmupBarrier;
|
|
15
|
+
warmupDone = false;
|
|
16
|
+
warmupAttempts = 0;
|
|
13
17
|
constructor(transport, languageId, readText, log = () => { }, warmup = { windowMs: 10_000, retryMs: 1_000 }, diagnostics) {
|
|
14
18
|
this.transport = transport;
|
|
15
19
|
this.languageId = languageId;
|
|
@@ -24,7 +28,7 @@ export class TransportLspSession {
|
|
|
24
28
|
if (!p || typeof p.uri !== "string" || !Array.isArray(p.diagnostics))
|
|
25
29
|
return;
|
|
26
30
|
if (typeof p.version === "number") {
|
|
27
|
-
const filePath = uriToPath(p.uri);
|
|
31
|
+
const filePath = this.openedUris.get(p.uri) ?? uriToPath(p.uri);
|
|
28
32
|
const known = this.opened.get(filePath);
|
|
29
33
|
if (known && p.version < known.version) {
|
|
30
34
|
this.log("lsp_diagnostics_stale_dropped", { uri: p.uri, version: p.version, current: known.version });
|
|
@@ -35,18 +39,34 @@ export class TransportLspSession {
|
|
|
35
39
|
});
|
|
36
40
|
}
|
|
37
41
|
}
|
|
42
|
+
get inFlight() {
|
|
43
|
+
return this.activeRequests;
|
|
44
|
+
}
|
|
45
|
+
activeRequests = 0;
|
|
38
46
|
async request(op, params, signal) {
|
|
47
|
+
this.activeRequests += 1;
|
|
48
|
+
try {
|
|
49
|
+
return await this.requestOnce(op, params, signal);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
this.activeRequests -= 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async requestOnce(op, params, signal) {
|
|
39
56
|
const built = buildRequest(op, params);
|
|
40
57
|
if (!built)
|
|
41
58
|
return { kind: "none", reason: "unsupported_operation" };
|
|
42
|
-
await this.syncOpenedFiles(params.filePath, signal);
|
|
43
|
-
if (!
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
const sync = await this.syncOpenedFiles(params.filePath, signal);
|
|
60
|
+
if (!sync.ok) {
|
|
61
|
+
const reason = signal?.aborted ? "cancelled" : (sync.reason ?? "file_sync_failed");
|
|
62
|
+
this.log("lsp_op_degraded", { op, file: params.filePath, reason, err: sync.error });
|
|
63
|
+
return { kind: "none", reason, ...(reason === "file_sync_failed" ? { detail: sync.error } : {}) };
|
|
64
|
+
}
|
|
65
|
+
if (this.warmup.windowMs > 0 && !this.warmupDone) {
|
|
66
|
+
if (op === "goToDefinition")
|
|
67
|
+
this.warmupDone = true;
|
|
68
|
+
else
|
|
69
|
+
await this.warmupBarrierOnce(params.filePath, signal);
|
|
50
70
|
}
|
|
51
71
|
for (let attempt = 0;; attempt++) {
|
|
52
72
|
let raw;
|
|
@@ -66,7 +86,7 @@ export class TransportLspSession {
|
|
|
66
86
|
}
|
|
67
87
|
const result = noHierarchyItem
|
|
68
88
|
? { kind: "none", reason: "no_call_hierarchy_item" }
|
|
69
|
-
: parseResult(op, raw);
|
|
89
|
+
: parseResult(op, raw, pathToUri(params.filePath));
|
|
70
90
|
if (result.kind === "none" && this.warmup.windowMs > 0 && Date.now() - this.createdAt < this.warmup.windowMs && !signal?.aborted) {
|
|
71
91
|
await new Promise((r) => setTimeout(r, this.warmup.retryMs));
|
|
72
92
|
continue;
|
|
@@ -84,47 +104,90 @@ export class TransportLspSession {
|
|
|
84
104
|
return result;
|
|
85
105
|
}
|
|
86
106
|
}
|
|
107
|
+
async warmupBarrierOnce(filePath, signal) {
|
|
108
|
+
let entry = this.warmupBarrier;
|
|
109
|
+
if (entry && entry.scope.signal.aborted)
|
|
110
|
+
entry = undefined;
|
|
111
|
+
if (!entry) {
|
|
112
|
+
if (this.warmupAttempts >= WARMUP_MAX_ATTEMPTS)
|
|
113
|
+
return;
|
|
114
|
+
this.warmupAttempts += 1;
|
|
115
|
+
const scope = new SharedAbortScope();
|
|
116
|
+
const started = {};
|
|
117
|
+
const job = this.transport
|
|
118
|
+
.request("textDocument/definition", { textDocument: { uri: pathToUri(filePath) }, position: { line: 0, character: 0 } }, scope.signal)
|
|
119
|
+
.then(() => {
|
|
120
|
+
this.warmupDone = true;
|
|
121
|
+
})
|
|
122
|
+
.catch(() => {
|
|
123
|
+
if (this.warmupBarrier === started.self)
|
|
124
|
+
this.warmupBarrier = undefined;
|
|
125
|
+
});
|
|
126
|
+
entry = { job, scope };
|
|
127
|
+
started.self = entry;
|
|
128
|
+
this.warmupBarrier = entry;
|
|
129
|
+
}
|
|
130
|
+
entry.scope.join(signal);
|
|
131
|
+
await settleOnAbort(entry.job, signal, () => undefined);
|
|
132
|
+
}
|
|
87
133
|
async syncOpenedFiles(queried, signal) {
|
|
88
134
|
const files = new Set(this.opened.keys());
|
|
89
135
|
files.add(queried);
|
|
90
|
-
await Promise.all([...files].map((fp) => this.syncOne(fp, signal)));
|
|
136
|
+
const outcomes = await Promise.all([...files].map(async (fp) => [fp, await this.syncOne(fp, signal)]));
|
|
137
|
+
return outcomes.find(([fp]) => fp === queried)?.[1] ?? { ok: true };
|
|
91
138
|
}
|
|
92
139
|
syncOne(filePath, signal) {
|
|
93
140
|
const inflight = this.syncing.get(filePath);
|
|
94
|
-
if (inflight)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
141
|
+
if (inflight && !inflight.scope.signal.aborted) {
|
|
142
|
+
inflight.scope.join(signal);
|
|
143
|
+
return settleOnAbort(inflight.job, signal, () => ({ ok: false, error: "aborted by the caller" }));
|
|
144
|
+
}
|
|
145
|
+
const scope = new SharedAbortScope();
|
|
146
|
+
scope.join(signal);
|
|
147
|
+
const entry = {
|
|
148
|
+
scope,
|
|
149
|
+
job: (async () => {
|
|
150
|
+
let text;
|
|
151
|
+
try {
|
|
152
|
+
text = await this.readText(filePath, scope.signal);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
156
|
+
this.log("lsp_sync_skipped", { file: filePath, error, opened: this.opened.has(filePath) });
|
|
157
|
+
return { ok: false, error };
|
|
158
|
+
}
|
|
159
|
+
const cur = this.opened.get(filePath);
|
|
160
|
+
const uri = pathToUri(filePath);
|
|
161
|
+
if (!cur) {
|
|
162
|
+
this.transport.notify("textDocument/didOpen", {
|
|
163
|
+
textDocument: { uri, languageId: this.languageId, version: 1, text },
|
|
164
|
+
});
|
|
165
|
+
if (this.transport.closed === true) {
|
|
166
|
+
this.log("lsp_sync_notify_dropped", { file: filePath, kind: "didOpen" });
|
|
167
|
+
return { ok: false, error: "didOpen was not delivered (transport closed)", reason: "server_terminated" };
|
|
168
|
+
}
|
|
169
|
+
this.opened.set(filePath, { version: 1, text });
|
|
170
|
+
this.openedUris.set(uri, filePath);
|
|
171
|
+
}
|
|
172
|
+
else if (cur.text !== text) {
|
|
173
|
+
const version = cur.version + 1;
|
|
174
|
+
this.transport.notify("textDocument/didChange", { textDocument: { uri, version }, contentChanges: [{ text }] });
|
|
175
|
+
if (this.transport.closed === true) {
|
|
176
|
+
this.log("lsp_sync_notify_dropped", { file: filePath, kind: "didChange", version });
|
|
177
|
+
return { ok: false, error: "didChange was not delivered (transport closed)", reason: "server_terminated" };
|
|
178
|
+
}
|
|
179
|
+
this.opened.set(filePath, { version, text });
|
|
180
|
+
this.log("lsp_file_resync", { file: filePath, version });
|
|
181
|
+
}
|
|
182
|
+
return { ok: true };
|
|
183
|
+
})(),
|
|
184
|
+
};
|
|
185
|
+
this.syncing.set(filePath, entry);
|
|
186
|
+
const settled = entry.job.finally(() => {
|
|
187
|
+
if (this.syncing.get(filePath) === entry)
|
|
188
|
+
this.syncing.delete(filePath);
|
|
189
|
+
});
|
|
190
|
+
return settleOnAbort(settled, signal, () => ({ ok: false, error: "aborted by the caller" }));
|
|
128
191
|
}
|
|
129
192
|
static classifyUnbranded(signal, transport) {
|
|
130
193
|
if (signal?.aborted)
|
|
@@ -140,10 +203,12 @@ export class TransportLspSession {
|
|
|
140
203
|
return [...this.opened.keys()];
|
|
141
204
|
}
|
|
142
205
|
async warmOpen(filePaths, signal) {
|
|
143
|
-
await Promise.all(filePaths.map((fp) => this.syncOne(fp, signal)));
|
|
206
|
+
const outcomes = await Promise.all(filePaths.map((fp) => this.syncOne(fp, signal)));
|
|
207
|
+
const failed = outcomes.filter((o) => !o.ok).length;
|
|
208
|
+
return { opened: outcomes.length - failed, failed };
|
|
144
209
|
}
|
|
145
210
|
notifyFileChanged(filePath, signal) {
|
|
146
|
-
return this.syncOne(filePath, signal);
|
|
211
|
+
return this.syncOne(filePath, signal).then(() => undefined);
|
|
147
212
|
}
|
|
148
213
|
close() {
|
|
149
214
|
return this.transport.close();
|
package/dist/core/lsp.d.ts
CHANGED
|
@@ -13,8 +13,9 @@ export interface LspSymbolInfo {
|
|
|
13
13
|
uri: string;
|
|
14
14
|
line: number;
|
|
15
15
|
character: number;
|
|
16
|
+
positionUnknown?: boolean;
|
|
16
17
|
}
|
|
17
|
-
export type LspNoneReason = "unsupported_operation" | "server_error" | "timeout" | "cancelled" | "server_terminated" | "no_call_hierarchy_item";
|
|
18
|
+
export type LspNoneReason = "unsupported_operation" | "server_error" | "timeout" | "cancelled" | "server_terminated" | "no_call_hierarchy_item" | "file_sync_failed";
|
|
18
19
|
export type LspResult = {
|
|
19
20
|
kind: "locations";
|
|
20
21
|
locations: LspLocation[];
|
|
@@ -35,6 +36,16 @@ export declare function lspFailureOf(e: unknown): {
|
|
|
35
36
|
reason: LspNoneReason;
|
|
36
37
|
detail?: string;
|
|
37
38
|
} | undefined;
|
|
39
|
+
export declare class SharedAbortScope {
|
|
40
|
+
private readonly controller;
|
|
41
|
+
private joiners;
|
|
42
|
+
private abandoned;
|
|
43
|
+
get signal(): AbortSignal;
|
|
44
|
+
join(signal?: AbortSignal): void;
|
|
45
|
+
private abandon;
|
|
46
|
+
}
|
|
47
|
+
export declare function settleOnAbort<T>(job: Promise<T>, signal: AbortSignal | undefined, onAbort: () => T): Promise<T>;
|
|
48
|
+
export declare function resolveLspPath(filePath: string, cwd?: string): string;
|
|
38
49
|
export interface LspRequestParams {
|
|
39
50
|
filePath: string;
|
|
40
51
|
line?: number;
|
|
@@ -55,6 +66,7 @@ export type LspReadText = (filePath: string, signal?: AbortSignal) => Promise<st
|
|
|
55
66
|
export declare const MAX_LSP_FILE_BYTES: number;
|
|
56
67
|
export interface LspServerManager {
|
|
57
68
|
sessionFor(filePath: string, signal?: AbortSignal, env?: ExecutionEnv): Promise<LspSession | undefined>;
|
|
69
|
+
unavailableReason?(filePath: string, env?: ExecutionEnv): string | undefined;
|
|
58
70
|
readonly diagnostics?: import("./lsp-diagnostics.js").LspDiagnosticsRegistry;
|
|
59
71
|
}
|
|
60
72
|
export interface LspToolOptions {
|
package/dist/core/lsp.js
CHANGED
|
@@ -8,6 +8,7 @@ export const LSP_OPERATIONS = [
|
|
|
8
8
|
export const LSP_FAILURE_BRAND = "lspFailureReason";
|
|
9
9
|
const LSP_NONE_REASONS = new Set([
|
|
10
10
|
"unsupported_operation", "server_error", "timeout", "cancelled", "server_terminated", "no_call_hierarchy_item",
|
|
11
|
+
"file_sync_failed",
|
|
11
12
|
]);
|
|
12
13
|
export function brandLspFailure(e, reason, detail) {
|
|
13
14
|
const carrier = e;
|
|
@@ -23,6 +24,54 @@ export function lspFailureOf(e) {
|
|
|
23
24
|
const d = e.lspFailureDetail;
|
|
24
25
|
return { reason: r, ...(typeof d === "string" ? { detail: d } : {}) };
|
|
25
26
|
}
|
|
27
|
+
export class SharedAbortScope {
|
|
28
|
+
controller = new AbortController();
|
|
29
|
+
joiners = 0;
|
|
30
|
+
abandoned = 0;
|
|
31
|
+
get signal() {
|
|
32
|
+
return this.controller.signal;
|
|
33
|
+
}
|
|
34
|
+
join(signal) {
|
|
35
|
+
this.joiners += 1;
|
|
36
|
+
if (!signal)
|
|
37
|
+
return;
|
|
38
|
+
if (signal.aborted) {
|
|
39
|
+
this.abandon();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
signal.addEventListener("abort", () => this.abandon(), { once: true });
|
|
43
|
+
}
|
|
44
|
+
abandon() {
|
|
45
|
+
this.abandoned += 1;
|
|
46
|
+
if (this.abandoned >= this.joiners)
|
|
47
|
+
this.controller.abort();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function settleOnAbort(job, signal, onAbort) {
|
|
51
|
+
if (!signal)
|
|
52
|
+
return job;
|
|
53
|
+
if (signal.aborted) {
|
|
54
|
+
void job.catch(() => undefined);
|
|
55
|
+
return Promise.resolve(onAbort());
|
|
56
|
+
}
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const onAbortEvent = () => resolve(onAbort());
|
|
59
|
+
signal.addEventListener("abort", onAbortEvent, { once: true });
|
|
60
|
+
job.then((v) => {
|
|
61
|
+
signal.removeEventListener("abort", onAbortEvent);
|
|
62
|
+
resolve(v);
|
|
63
|
+
}, (e) => {
|
|
64
|
+
signal.removeEventListener("abort", onAbortEvent);
|
|
65
|
+
reject(e);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export function resolveLspPath(filePath, cwd) {
|
|
70
|
+
const isAbsForm = filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath) || filePath.startsWith("\\\\");
|
|
71
|
+
if (isAbsForm || !cwd)
|
|
72
|
+
return filePath;
|
|
73
|
+
return cwd.replace(/[\\/]+$/, "") + (cwd.includes("\\") ? "\\" : "/") + filePath;
|
|
74
|
+
}
|
|
26
75
|
export const MAX_LSP_FILE_BYTES = 10 * 1000 * 1000;
|
|
27
76
|
function fmt(result, ctx) {
|
|
28
77
|
switch (result.kind) {
|
|
@@ -37,7 +86,9 @@ function fmt(result, ctx) {
|
|
|
37
86
|
return result.symbols.length === 0
|
|
38
87
|
? "No symbols found."
|
|
39
88
|
: `Found ${result.symbols.length} symbol(s):\n` +
|
|
40
|
-
result.symbols
|
|
89
|
+
result.symbols
|
|
90
|
+
.map((s) => `- ${s.name}${s.kind ? ` (${s.kind})` : ""} — ${s.uri}${s.positionUnknown ? " (no position reported by the language server)" : `:${s.line}:${s.character}`}`)
|
|
91
|
+
.join("\n");
|
|
41
92
|
case "none":
|
|
42
93
|
return fmtNone(result, ctx);
|
|
43
94
|
}
|
|
@@ -60,9 +111,14 @@ function fmtNone(result, ctx) {
|
|
|
60
111
|
return `Error performing ${operation}: the request was cancelled before the language server answered. This is NOT an empty result.`;
|
|
61
112
|
case "server_error":
|
|
62
113
|
return `Error performing ${operation}: ${detail ?? "the language server returned an error"}. This is NOT an empty result — fall back to Grep/Read.`;
|
|
114
|
+
case "file_sync_failed":
|
|
115
|
+
return `Error performing ${operation}: "${filePath}" could not be read to synchronize it with the language server${detail ? ` (${detail})` : ""}. This is NOT an empty result — the server was never given the file's current text; re-check the path or fall back to Grep/Read.`;
|
|
63
116
|
}
|
|
64
117
|
}
|
|
65
|
-
const LSP_ERROR_REASONS = new Set([
|
|
118
|
+
const LSP_ERROR_REASONS = new Set([
|
|
119
|
+
"server_error", "timeout", "cancelled", "server_terminated",
|
|
120
|
+
"file_sync_failed",
|
|
121
|
+
]);
|
|
66
122
|
async function filterIgnored(result, isIgnored) {
|
|
67
123
|
if (!isIgnored)
|
|
68
124
|
return result;
|
|
@@ -129,11 +185,9 @@ export function createLspTool(manager, opts = {}) {
|
|
|
129
185
|
if (operation === "workspaceSymbol" && wsQuery === undefined) {
|
|
130
186
|
return errorResult(`Error (LSP): the "workspaceSymbol" operation needs a query.`);
|
|
131
187
|
}
|
|
188
|
+
const target = resolveLspPath(filePath, opts.env?.cwd);
|
|
132
189
|
if (opts.env) {
|
|
133
|
-
const
|
|
134
|
-
const cwd = opts.env.cwd;
|
|
135
|
-
const probe = isAbsForm || !cwd ? filePath : cwd.replace(/[\\/]+$/, "") + (cwd.includes("\\") ? "\\" : "/") + filePath;
|
|
136
|
-
const info = await opts.env.fileInfo(probe, ctx.signal);
|
|
190
|
+
const info = await opts.env.fileInfo(target, ctx.signal);
|
|
137
191
|
if (!info.ok && info.error.code === "not_found")
|
|
138
192
|
return errorResult(`Error (LSP): File does not exist: ${filePath}`);
|
|
139
193
|
if (!info.ok && info.error.code === "invalid")
|
|
@@ -144,12 +198,13 @@ export function createLspTool(manager, opts = {}) {
|
|
|
144
198
|
return `File too large for LSP analysis (${Math.ceil(info.value.size / 1e6)}MB exceeds 10MB limit)`;
|
|
145
199
|
}
|
|
146
200
|
}
|
|
147
|
-
const session = await manager.sessionFor(
|
|
201
|
+
const session = await manager.sessionFor(target, ctx.signal, opts.env);
|
|
148
202
|
if (!session) {
|
|
149
|
-
|
|
203
|
+
const why = manager.unavailableReason?.(target, opts.env) ?? "no language server for its language in this environment";
|
|
204
|
+
return `LSP is unavailable for "${filePath}" (${why}). Fall back to Grep/Read for this lookup.`;
|
|
150
205
|
}
|
|
151
206
|
const seamCharacter = character !== undefined ? character - 1 : undefined;
|
|
152
|
-
const result = await session.request(operation, { filePath, line, character: seamCharacter, symbol: wsQuery }, ctx.signal);
|
|
207
|
+
const result = await session.request(operation, { filePath: target, line, character: seamCharacter, symbol: wsQuery }, ctx.signal);
|
|
153
208
|
const final = toModelBase(await filterIgnored(result, opts.isPathIgnored));
|
|
154
209
|
const text = fmt(final, { operation, filePath });
|
|
155
210
|
return final.kind === "none" && final.reason !== undefined && LSP_ERROR_REASONS.has(final.reason) ? errorResult(text) : text;
|
package/dist/core/mcp.d.ts
CHANGED
|
@@ -71,6 +71,7 @@ export declare const MCP_IDLE_TIMEOUT_HTTP_DEFAULT_MS: number;
|
|
|
71
71
|
export declare function mcpIdleTimeoutMs(kind: "stdio" | "http"): number;
|
|
72
72
|
export declare function describeMcpSpecErrorCode(code: unknown): string | undefined;
|
|
73
73
|
export declare function collapseMcpErrorPrefix(message: string): string;
|
|
74
|
+
export declare function networkErrorCode(err: unknown, depth?: number): string | undefined;
|
|
74
75
|
export declare function normalizeMcpName(name: string): string;
|
|
75
76
|
export declare function clampNameSegment(seg: string, max?: number): string;
|
|
76
77
|
export * from "./image-downsample.js";
|
package/dist/core/mcp.js
CHANGED
|
@@ -179,11 +179,11 @@ const NETWORK_CODES_NEVER_DELIVERED = new Set([
|
|
|
179
179
|
"ENETUNREACH",
|
|
180
180
|
"UND_ERR_CONNECT_TIMEOUT",
|
|
181
181
|
]);
|
|
182
|
-
function networkErrorCode(err, depth = 0) {
|
|
182
|
+
export function networkErrorCode(err, depth = 0) {
|
|
183
183
|
if (depth > 5 || !(err instanceof Error))
|
|
184
184
|
return undefined;
|
|
185
185
|
const code = err.code;
|
|
186
|
-
if (typeof code === "string" && /^(?:E[A-
|
|
186
|
+
if (typeof code === "string" && /^(?:E[A-Z_]+|UND_ERR_[A-Z_]+)$/.test(code))
|
|
187
187
|
return code;
|
|
188
188
|
if (err instanceof AggregateError) {
|
|
189
189
|
for (const inner of err.errors) {
|
|
@@ -786,7 +786,8 @@ async function readDirViaExtension(rs, uri, signal, timeoutMs, watchdog) {
|
|
|
786
786
|
if (err instanceof McpError && err.code === ErrorCode.InvalidParams) {
|
|
787
787
|
if (pages > 0)
|
|
788
788
|
return { kind: "ok", resources, cursorInvalid: true };
|
|
789
|
-
|
|
789
|
+
const detail = collapseMcpErrorPrefix(err.message);
|
|
790
|
+
return { kind: classifyDirReadInvalidParams(detail), detail };
|
|
790
791
|
}
|
|
791
792
|
if (err instanceof McpError && err.code === ErrorCode.MethodNotFound)
|
|
792
793
|
return { kind: "unsupported" };
|