@sema-agent/core 2.13.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.
Files changed (95) hide show
  1. package/dist/agents/cascade.d.ts +4 -0
  2. package/dist/agents/cascade.js +54 -9
  3. package/dist/agents/repair-loop.d.ts +2 -0
  4. package/dist/agents/repair-loop.js +101 -2
  5. package/dist/agents/retain-ledger.d.ts +18 -2
  6. package/dist/agents/retain-ledger.js +37 -8
  7. package/dist/agents/roster-store.d.ts +4 -0
  8. package/dist/agents/roster-store.js +22 -1
  9. package/dist/agents/teacher.js +4 -2
  10. package/dist/agents/team.js +7 -4
  11. package/dist/agents/verify.d.ts +15 -2
  12. package/dist/agents/verify.js +36 -1
  13. package/dist/bin/sema-tb.js +2 -2
  14. package/dist/brain/circuit-breaker.d.ts +1 -0
  15. package/dist/brain/circuit-breaker.js +26 -9
  16. package/dist/brain/degrading.js +13 -1
  17. package/dist/brain/failover.js +2 -0
  18. package/dist/brain/repetition.js +10 -1
  19. package/dist/brain/routing.js +5 -1
  20. package/dist/brain/status-sink.js +5 -1
  21. package/dist/brain/tool-call-repair.js +32 -14
  22. package/dist/core/auto-compaction.js +17 -7
  23. package/dist/core/checkpoint-store.d.ts +1 -0
  24. package/dist/core/checkpoint-store.js +28 -10
  25. package/dist/core/consolidate-scope.js +4 -3
  26. package/dist/core/context-edit.js +3 -0
  27. package/dist/core/context-guard.js +3 -0
  28. package/dist/core/git-worktree-env.d.ts +6 -1
  29. package/dist/core/git-worktree-env.js +12 -1
  30. package/dist/core/hooks.d.ts +1 -0
  31. package/dist/core/hooks.js +40 -2
  32. package/dist/core/lsp-diagnostics.js +13 -7
  33. package/dist/core/lsp-protocol.d.ts +1 -1
  34. package/dist/core/lsp-protocol.js +20 -8
  35. package/dist/core/lsp-session.d.ts +12 -2
  36. package/dist/core/lsp-session.js +114 -49
  37. package/dist/core/lsp.d.ts +13 -1
  38. package/dist/core/lsp.js +64 -9
  39. package/dist/core/memory-engine/data-plane.js +4 -0
  40. package/dist/core/memory-engine/engine.d.ts +2 -1
  41. package/dist/core/memory-engine/engine.js +82 -7
  42. package/dist/core/memory-engine/file-backend.d.ts +1 -0
  43. package/dist/core/memory-engine/file-backend.js +13 -3
  44. package/dist/core/memory-engine/layout.js +12 -1
  45. package/dist/core/memory.js +3 -0
  46. package/dist/core/permission-rules.d.ts +2 -1
  47. package/dist/core/permission-rules.js +24 -3
  48. package/dist/core/runner/assemble-result.d.ts +1 -1
  49. package/dist/core/runner/assemble-result.js +0 -1
  50. package/dist/core/runner/prepare-task.js +11 -0
  51. package/dist/core/runner/runtask.js +21 -22
  52. package/dist/core/runner/usage-accounting.d.ts +6 -0
  53. package/dist/core/runner/usage-accounting.js +9 -8
  54. package/dist/core/secret-env.js +5 -1
  55. package/dist/core/sensitive-path-policy.js +9 -3
  56. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  57. package/dist/core/task-registry-monitor.js +7 -1
  58. package/dist/core/tool-policy.js +65 -17
  59. package/dist/core/trace.d.ts +1 -0
  60. package/dist/core/types.d.ts +1 -0
  61. package/dist/engine/compaction/compaction.d.ts +1 -0
  62. package/dist/engine/compaction/compaction.js +14 -5
  63. package/dist/engine/lsp/frame-decoder.js +6 -3
  64. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  65. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  66. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  67. package/dist/internal/harness.d.ts +1 -1
  68. package/dist/internal/harness.js +1 -1
  69. package/dist/orchestration/workflow-governance.js +43 -9
  70. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  71. package/dist/orchestration/workflow-script-store.js +34 -4
  72. package/dist/orchestration/workflow.js +2 -2
  73. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  74. package/dist/prompt-assembly/artifact-store.js +39 -24
  75. package/dist/prompt-assembly/assemble.js +38 -7
  76. package/dist/prompt-assembly/epoch.js +19 -2
  77. package/dist/prompt-assembly/event-registry.js +3 -3
  78. package/dist/prompt-assembly/explain.js +1 -1
  79. package/dist/prompt-assembly/tool-catalog.js +16 -1
  80. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  81. package/dist/stores/file/fs-atomic.d.ts +3 -0
  82. package/dist/stores/file/fs-atomic.js +53 -8
  83. package/dist/stores/file/index.d.ts +1 -0
  84. package/dist/stores/file/index.js +7 -0
  85. package/dist/stores/file/session-store.d.ts +2 -0
  86. package/dist/stores/file/session-store.js +28 -1
  87. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  88. package/dist/tools/fs/bash-readonly-classifier.js +19 -2
  89. package/dist/tools/fs/fs-bash.d.ts +1 -1
  90. package/dist/tools/fs/fs-bash.js +93 -5
  91. package/dist/tools/loop-tick.js +1 -1
  92. package/dist/tools/monitor.js +55 -9
  93. package/dist/tools/worktree.d.ts +2 -0
  94. package/dist/tools/worktree.js +233 -143
  95. package/package.json +5 -2
@@ -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
- for (const d of fresh)
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 += fresh.length;
51
- out.push({ uri, diagnostics: fresh });
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, fallbackUri) => {
102
+ const walk = (x, inheritedUri) => {
98
103
  const loc = x.location;
99
- const uri = loc?.uri ?? (typeof x.uri === "string" ? x.uri : fallbackUri);
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 ?? { line: 0, character: 0 };
103
- out.push({ name: x.name, ...(symbolKindName(x.kind) ? { kind: symbolKindName(x.kind) } : {}), uri, line: s.line + 1, character: s.character });
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 warmupBarrierDone;
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<void>;
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
  }
@@ -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
- warmupBarrierDone = false;
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 (!this.warmupBarrierDone && this.warmup.windowMs > 0) {
44
- this.warmupBarrierDone = true;
45
- if (op !== "goToDefinition") {
46
- await this.transport
47
- .request("textDocument/definition", { textDocument: { uri: pathToUri(params.filePath) }, position: { line: 0, character: 0 } }, signal)
48
- .catch(() => undefined);
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
- return inflight;
96
- const job = (async () => {
97
- let text;
98
- try {
99
- text = await this.readText(filePath, signal);
100
- }
101
- catch (e) {
102
- this.log("lsp_sync_skipped", {
103
- file: filePath,
104
- error: e instanceof Error ? e.message : String(e),
105
- opened: this.opened.has(filePath),
106
- });
107
- return;
108
- }
109
- const cur = this.opened.get(filePath);
110
- if (!cur) {
111
- this.opened.set(filePath, { version: 1, text });
112
- this.transport.notify("textDocument/didOpen", {
113
- textDocument: { uri: pathToUri(filePath), languageId: this.languageId, version: 1, text },
114
- });
115
- }
116
- else if (cur.text !== text) {
117
- const version = cur.version + 1;
118
- this.opened.set(filePath, { version, text });
119
- this.transport.notify("textDocument/didChange", {
120
- textDocument: { uri: pathToUri(filePath), version },
121
- contentChanges: [{ text }],
122
- });
123
- this.log("lsp_file_resync", { file: filePath, version });
124
- }
125
- })();
126
- this.syncing.set(filePath, job);
127
- return job.finally(() => this.syncing.delete(filePath));
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();
@@ -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.map((s) => `- ${s.name}${s.kind ? ` (${s.kind})` : ""} — ${s.uri}:${s.line}:${s.character}`).join("\n");
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(["server_error", "timeout", "cancelled", "server_terminated"]);
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 isAbsForm = filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath);
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(filePath, ctx.signal, opts.env);
201
+ const session = await manager.sessionFor(target, ctx.signal, opts.env);
148
202
  if (!session) {
149
- return `LSP is unavailable for "${filePath}" (no language server for its language in this environment). Fall back to Grep/Read for this lookup.`;
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;
@@ -2,6 +2,7 @@ import { uuidv7 } from "../../internal/harness.js";
2
2
  import { firstSentence } from "../memory.js";
3
3
  import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
4
4
  import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
5
+ import { MEMORY_INDEX_FILENAME } from "./file-backend.js";
5
6
  import { scopeDirName } from "./layout.js";
6
7
  export const REMOTE_HARVEST_PER_FILE_BYTES = 100 * 1024;
7
8
  export const REMOTE_MASS_DELETION_FUSE_RATIO = 0.5;
@@ -97,6 +98,9 @@ export function harvestFilesToPatches(baseline, files, opts = {}) {
97
98
  out.rejections.push({ path: f.relPath, code: "outside_root", reason: "file path escapes the memory root (containment gate, fail-closed)" });
98
99
  continue;
99
100
  }
101
+ const baseName = f.relPath.split("/").pop() ?? f.relPath;
102
+ if (baseName === MEMORY_INDEX_FILENAME || !baseName.endsWith(".md") || baseName.startsWith("."))
103
+ continue;
100
104
  if (inReadonlyDir(f.relPath)) {
101
105
  if (base === undefined) {
102
106
  out.rejections.push({ path: f.relPath, code: "readonly_layer", reason: "a file inside a read-only inherited layer's directory is not writable from this session" });
@@ -56,9 +56,10 @@ export declare class MemoryEngine {
56
56
  };
57
57
  harvest(handle: MemorySessionHandle): Promise<HarvestReport>;
58
58
  private harvestCore;
59
- rebaseline(handle: MemorySessionHandle): Promise<void>;
59
+ rebaseline(handle: MemorySessionHandle, keepBaseline?: ReadonlySet<string>): Promise<void>;
60
60
  private rebuildIndex;
61
61
  private committedContentFor;
62
+ private gateDerivedIndex;
62
63
  private quarantineFile;
63
64
  private readonlyDirNamesUnderRoot;
64
65
  private writeIfChanged;