@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.
Files changed (115) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +1 -0
  2. package/dist/agents/agent-transcript-tool.js +1 -1
  3. package/dist/agents/cascade.d.ts +4 -0
  4. package/dist/agents/cascade.js +54 -9
  5. package/dist/agents/repair-loop.d.ts +2 -0
  6. package/dist/agents/repair-loop.js +101 -2
  7. package/dist/agents/retain-ledger.d.ts +18 -2
  8. package/dist/agents/retain-ledger.js +37 -8
  9. package/dist/agents/roster-store.d.ts +4 -0
  10. package/dist/agents/roster-store.js +22 -1
  11. package/dist/agents/teacher.js +4 -2
  12. package/dist/agents/team.js +7 -4
  13. package/dist/agents/verify.d.ts +15 -2
  14. package/dist/agents/verify.js +36 -1
  15. package/dist/bin/sema-tb.js +2 -2
  16. package/dist/brain/circuit-breaker.d.ts +1 -0
  17. package/dist/brain/circuit-breaker.js +26 -9
  18. package/dist/brain/degrading.js +13 -1
  19. package/dist/brain/failover.js +2 -0
  20. package/dist/brain/repetition.js +10 -1
  21. package/dist/brain/routing.js +5 -1
  22. package/dist/brain/status-sink.js +5 -1
  23. package/dist/brain/stream-engine.js +4 -1
  24. package/dist/brain/tool-call-repair.js +32 -14
  25. package/dist/core/auto-compaction.js +17 -7
  26. package/dist/core/auto-promote.js +2 -1
  27. package/dist/core/checkpoint-store.d.ts +1 -0
  28. package/dist/core/checkpoint-store.js +28 -10
  29. package/dist/core/consolidate-scope.js +4 -3
  30. package/dist/core/context-edit.js +3 -0
  31. package/dist/core/context-guard.js +3 -0
  32. package/dist/core/git-worktree-env.d.ts +6 -1
  33. package/dist/core/git-worktree-env.js +17 -1
  34. package/dist/core/hooks.d.ts +1 -0
  35. package/dist/core/hooks.js +40 -2
  36. package/dist/core/lsp-diagnostics.js +13 -7
  37. package/dist/core/lsp-protocol.d.ts +1 -1
  38. package/dist/core/lsp-protocol.js +20 -8
  39. package/dist/core/lsp-session.d.ts +12 -2
  40. package/dist/core/lsp-session.js +114 -49
  41. package/dist/core/lsp.d.ts +13 -1
  42. package/dist/core/lsp.js +64 -9
  43. package/dist/core/mcp.d.ts +1 -0
  44. package/dist/core/mcp.js +4 -3
  45. package/dist/core/memory-engine/data-plane.js +4 -0
  46. package/dist/core/memory-engine/engine.d.ts +2 -1
  47. package/dist/core/memory-engine/engine.js +82 -7
  48. package/dist/core/memory-engine/file-backend.d.ts +1 -0
  49. package/dist/core/memory-engine/file-backend.js +13 -3
  50. package/dist/core/memory-engine/layout.js +12 -1
  51. package/dist/core/memory.js +3 -0
  52. package/dist/core/permission-rules.d.ts +2 -1
  53. package/dist/core/permission-rules.js +24 -3
  54. package/dist/core/runner/active-skill-scope.js +34 -6
  55. package/dist/core/runner/assemble-result.d.ts +1 -1
  56. package/dist/core/runner/assemble-result.js +0 -1
  57. package/dist/core/runner/prepare-task.js +23 -3
  58. package/dist/core/runner/runtask.js +48 -30
  59. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  60. package/dist/core/runner/tool-disclosure.js +26 -7
  61. package/dist/core/runner/usage-accounting.d.ts +6 -0
  62. package/dist/core/runner/usage-accounting.js +9 -8
  63. package/dist/core/secret-env.js +5 -1
  64. package/dist/core/sensitive-path-policy.js +9 -3
  65. package/dist/core/session-store.js +15 -4
  66. package/dist/core/skill-tool-specifier.d.ts +8 -0
  67. package/dist/core/skill-tool-specifier.js +58 -0
  68. package/dist/core/skills-directory.d.ts +1 -1
  69. package/dist/core/skills-directory.js +16 -4
  70. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  71. package/dist/core/task-registry-monitor.js +7 -1
  72. package/dist/core/tool-policy.js +65 -17
  73. package/dist/core/trace.d.ts +1 -0
  74. package/dist/core/types.d.ts +3 -5
  75. package/dist/core/with-retry.js +0 -1
  76. package/dist/engine/compaction/compaction.d.ts +1 -0
  77. package/dist/engine/compaction/compaction.js +14 -5
  78. package/dist/engine/lsp/frame-decoder.js +6 -3
  79. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  80. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  81. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  82. package/dist/index.d.ts +1 -1
  83. package/dist/internal/harness.d.ts +1 -1
  84. package/dist/internal/harness.js +1 -1
  85. package/dist/internal/llm.d.ts +1 -1
  86. package/dist/orchestration/workflow-governance.js +43 -9
  87. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  88. package/dist/orchestration/workflow-script-store.js +34 -4
  89. package/dist/orchestration/workflow.js +10 -6
  90. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  91. package/dist/prompt-assembly/artifact-store.js +39 -24
  92. package/dist/prompt-assembly/assemble.js +38 -7
  93. package/dist/prompt-assembly/epoch.js +19 -2
  94. package/dist/prompt-assembly/event-registry.js +3 -3
  95. package/dist/prompt-assembly/explain.js +1 -1
  96. package/dist/prompt-assembly/tool-catalog.js +16 -1
  97. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  98. package/dist/stores/file/fs-atomic.d.ts +3 -0
  99. package/dist/stores/file/fs-atomic.js +53 -8
  100. package/dist/stores/file/index.d.ts +1 -0
  101. package/dist/stores/file/index.js +7 -0
  102. package/dist/stores/file/session-store.d.ts +2 -0
  103. package/dist/stores/file/session-store.js +28 -1
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  105. package/dist/tools/fs/bash-readonly-classifier.js +57 -8
  106. package/dist/tools/fs/fs-bash.d.ts +1 -1
  107. package/dist/tools/fs/fs-bash.js +93 -5
  108. package/dist/tools/loop-tick.js +1 -1
  109. package/dist/tools/monitor.js +55 -9
  110. package/dist/tools/scheduler-tools.js +9 -3
  111. package/dist/tools/web.d.ts +8 -2
  112. package/dist/tools/web.js +46 -17
  113. package/dist/tools/worktree.d.ts +2 -0
  114. package/dist/tools/worktree.js +233 -143
  115. package/package.json +5 -2
@@ -1,6 +1,7 @@
1
1
  import { homedir } from "node:os";
2
- import { isAbsolute, join, sep } from "node:path";
2
+ import { isAbsolute, join, normalize as normalizePath, sep } from "node:path";
3
3
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
4
+ import { writeTargetPath } from "../tools/fs/safety.js";
4
5
  import { canonicalToolName } from "./tool-name-aliases.js";
5
6
  export function decisionText(d) {
6
7
  return d.message ?? d.reason;
@@ -106,7 +107,7 @@ export function combinePolicies(...policies) {
106
107
  current = { ...current, args: d.updatedInput };
107
108
  rewrite = d;
108
109
  }
109
- if (d.action === "ask" && !asked) {
110
+ if (d.action === "ask" && (asked === undefined || (d.requiresRealApproval === true && asked.requiresRealApproval !== true))) {
110
111
  asked = d;
111
112
  }
112
113
  }
@@ -119,10 +120,13 @@ export function combinePolicies(...policies) {
119
120
  };
120
121
  }
121
122
  export const COARSE_SHELL_TOOLS = ["Bash", "Monitor"];
123
+ function canonicalToolNameSet(tools) {
124
+ return new Set((tools ?? COARSE_SHELL_TOOLS).map(canonicalToolName));
125
+ }
122
126
  export function createCoarseCommandNamePolicy(opts) {
123
127
  const allow = opts.allow ? new Set(opts.allow) : undefined;
124
128
  const deny = new Set(opts.deny ?? []);
125
- const shellTools = new Set(opts.tools ?? COARSE_SHELL_TOOLS);
129
+ const shellTools = canonicalToolNameSet(opts.tools);
126
130
  const defaultAction = opts.defaultAction ?? "ask";
127
131
  const fallback = (reason) => defaultAction === "deny"
128
132
  ? { action: "deny", reason, decisionReason: "rule" }
@@ -154,6 +158,7 @@ const DELETE_HEADS = new Set(["rm", "xargs", "Remove-Item"]);
154
158
  const DELETE_WRAPPERS = new Set([
155
159
  "sudo", "command", "nohup", "time", "do", "then", "else",
156
160
  "env", "nice", "ionice", "exec", "doas", "setsid", "stdbuf", "chroot", "eval",
161
+ "sh", "bash", "zsh", "dash", "ksh", "ash", "timeout", "case",
157
162
  ]);
158
163
  const STATEMENT_SPLIT = /[;&|\n]+/;
159
164
  const stripTokenEdges = (t) => t.replace(/^[("'{]+|[)"'}]+$/g, "");
@@ -209,7 +214,7 @@ export function findUnverifiableRecursiveDelete(command, safeVars = new Set(UNVE
209
214
  }
210
215
  return false;
211
216
  };
212
- const statements = command.split(STATEMENT_SPLIT);
217
+ const statements = command.replace(/\\\r?\n/g, " ").split(STATEMENT_SPLIT);
213
218
  for (const stmt of statements) {
214
219
  const tokens = stmt.trim().split(/\s+/).filter(Boolean);
215
220
  let i = 0;
@@ -230,6 +235,10 @@ export function findUnverifiableRecursiveDelete(command, safeVars = new Set(UNVE
230
235
  let sawWrapper = false;
231
236
  while (i < tokens.length) {
232
237
  const t = stripTokenEdges(tokens[i]).replace(/^\\/, "");
238
+ if (t === "") {
239
+ i++;
240
+ continue;
241
+ }
233
242
  if (DELETE_WRAPPERS.has(t)) {
234
243
  sawWrapper = true;
235
244
  i++;
@@ -237,8 +246,9 @@ export function findUnverifiableRecursiveDelete(command, safeVars = new Set(UNVE
237
246
  i++;
238
247
  continue;
239
248
  }
240
- if (t === "timeout" && i + 1 < tokens.length) {
241
- i += 2;
249
+ if (/\)$/.test(tokens[i]) && !DELETE_HEADS.has(t)) {
250
+ sawWrapper = true;
251
+ i++;
242
252
  continue;
243
253
  }
244
254
  break;
@@ -368,7 +378,7 @@ function hasRecursiveForce(argvTail) {
368
378
  }
369
379
  export function createUnverifiableDeletePolicy(opts) {
370
380
  const safeVars = new Set([...UNVERIFIABLE_DELETE_SAFE_VARS, ...(opts?.safeVars ?? [])]);
371
- const shellTools = new Set(opts?.tools ?? COARSE_SHELL_TOOLS);
381
+ const shellTools = canonicalToolNameSet(opts?.tools);
372
382
  return {
373
383
  check(req) {
374
384
  if (!shellTools.has(canonicalToolName(req.toolName)))
@@ -390,10 +400,28 @@ export function createUnverifiableDeletePolicy(opts) {
390
400
  },
391
401
  };
392
402
  }
403
+ function pathSegments(p) {
404
+ return p.split(/[\\/]/).filter(Boolean);
405
+ }
406
+ function lexicalPath(p, home) {
407
+ const norm = p.normalize("NFC");
408
+ const expanded = norm === "~" ? home : /^~[\\/]/.test(norm) ? join(home, norm.slice(2)) : norm;
409
+ return normalizePath(expanded);
410
+ }
411
+ const RELATIVE_TAIL_MIN_SEGMENTS = 2;
412
+ function relativeContinuesDirTail(segs, dirSegs) {
413
+ const from = Math.min(RELATIVE_TAIL_MIN_SEGMENTS, dirSegs.length);
414
+ for (let k = from; k <= Math.min(segs.length, dirSegs.length); k++) {
415
+ const tail = dirSegs.slice(dirSegs.length - k);
416
+ if (tail.every((s, i) => s === segs[i]))
417
+ return true;
418
+ }
419
+ return false;
420
+ }
393
421
  export function createTranscriptIntegrityPolicy(opts) {
394
422
  const home = homedir();
395
423
  const defaultRoot = (process.env.AGENT_DATA_DIR ?? join(home, ".ai-agent")).normalize("NFC");
396
- const dirs = (opts?.sessionsDirs ?? [join(defaultRoot, "sessions")]).map((d) => d.normalize("NFC").replace(/[\\/]+$/, ""));
424
+ const dirs = (opts?.sessionsDirs ?? [join(defaultRoot, "sessions")]).map((d) => lexicalPath(d, home).replace(/(?<=.)[\\/]+$/, ""));
397
425
  const textualForms = dirs.flatMap((d) => {
398
426
  const forms = [d];
399
427
  if (d.startsWith(home + sep)) {
@@ -403,13 +431,34 @@ export function createTranscriptIntegrityPolicy(opts) {
403
431
  return forms;
404
432
  });
405
433
  const readAllow = new Set(opts?.readAllow ?? BASH_READONLY_DEFAULT_ALLOW);
406
- const shellTools = new Set(opts?.tools ?? COARSE_SHELL_TOOLS);
434
+ const shellTools = canonicalToolNameSet(opts?.tools);
435
+ const dirSegs = dirs.map(pathSegments);
407
436
  const inProtectedDir = (p) => {
408
- const norm = p.normalize("NFC");
409
- const abs = norm.startsWith("~" + sep) || norm === "~" ? join(home, norm.slice(1)) : norm;
410
- if (!isAbsolute(abs))
411
- return false;
412
- return dirs.some((d) => abs === d || abs.startsWith(d + sep));
437
+ const abs = lexicalPath(p, home);
438
+ const segs = pathSegments(abs);
439
+ if (isAbsolute(abs)) {
440
+ return dirSegs.some((d) => d.length <= segs.length && d.every((s, i) => s === segs[i]));
441
+ }
442
+ return dirSegs.some((d) => relativeContinuesDirTail(segs, d));
443
+ };
444
+ const referencesProtectedDir = (cmd) => {
445
+ if (textualForms.some((f) => cmd.includes(f)))
446
+ return true;
447
+ for (const raw of cmd.split(/[\s;&|<>()]+/)) {
448
+ if (raw === "")
449
+ continue;
450
+ const unquoted = raw.replace(/["'`]/g, "");
451
+ if (unquoted === "")
452
+ continue;
453
+ const expanded = unquoted.startsWith("${HOME}")
454
+ ? home + unquoted.slice("${HOME}".length)
455
+ : unquoted.startsWith("$HOME")
456
+ ? home + unquoted.slice("$HOME".length)
457
+ : unquoted;
458
+ if (inProtectedDir(expanded))
459
+ return true;
460
+ }
461
+ return false;
413
462
  };
414
463
  const askReason = (what) => ({
415
464
  action: "ask",
@@ -428,7 +477,7 @@ export function createTranscriptIntegrityPolicy(opts) {
428
477
  if (typeof command !== "string")
429
478
  return ALLOW;
430
479
  const cmd = command.normalize("NFC");
431
- if (!textualForms.some((f) => cmd.includes(f)))
480
+ if (!referencesProtectedDir(cmd))
432
481
  return ALLOW;
433
482
  const parsed = parseLeadingCommandName(command);
434
483
  if ("name" in parsed && readAllow.has(parsed.name))
@@ -438,8 +487,7 @@ export function createTranscriptIntegrityPolicy(opts) {
438
487
  : "the command references the transcript directory and is not a single read-only command (fail-closed)");
439
488
  }
440
489
  if (toolName === "Write" || toolName === "Edit" || toolName === "NotebookEdit") {
441
- const a = req.args;
442
- const p = a?.file_path ?? a?.notebook_path;
490
+ const p = writeTargetPath(toolName, req.args);
443
491
  if (typeof p === "string" && inProtectedDir(p)) {
444
492
  return askReason(`"${p}" is inside the session-transcript directory`);
445
493
  }
@@ -140,6 +140,7 @@ export type TraceEvent = {
140
140
  provider?: string;
141
141
  turn?: number;
142
142
  promptTokens: number;
143
+ totalInputTokens: number;
143
144
  completionTokens: number;
144
145
  cacheRead: number;
145
146
  cacheWrite: number;
@@ -1,6 +1,6 @@
1
1
  import type { TSchema } from "typebox";
2
2
  import type { AgentTool, ThinkingLevel } from "../internal/harness.js";
3
- import type { DocumentContent, ImageContent, Model, ResilienceOptions, StreamFn, TextContent } from "../internal/llm.js";
3
+ import type { CompleteSimpleFn, DocumentContent, ImageContent, Model, ResilienceOptions, StreamFn, TextContent } from "../internal/llm.js";
4
4
  import type { TaskNotificationPayload } from "./task-notification.js";
5
5
  export type ModelRef = string | Model;
6
6
  export type ModelRole = "default" | "summarize" | "subagent" | "team" | "synthesize" | "advisor" | "verifier" | "classifier";
@@ -13,10 +13,7 @@ export type RoleSpec = ModelRef | {
13
13
  export type ModelRoles = Partial<Record<ModelRole, RoleSpec>>;
14
14
  export interface Brain {
15
15
  stream: StreamFn;
16
- complete?: (model: Model, context: {
17
- systemPrompt?: string;
18
- messages: unknown[];
19
- }, options?: unknown) => Promise<unknown>;
16
+ complete?: CompleteSimpleFn;
20
17
  }
21
18
  export type ToolEffect = "read" | "write" | "idempotent";
22
19
  export interface ToolSpec<TParams extends TSchema = TSchema> {
@@ -527,6 +524,7 @@ export type TaskEvent = ({
527
524
  type: "turn_end";
528
525
  usage?: {
529
526
  inputTokens: number;
527
+ totalInputTokens: number;
530
528
  outputTokens: number;
531
529
  cacheReadTokens: number;
532
530
  cacheWriteTokens: number;
@@ -13,7 +13,6 @@ export async function withRetry(op, policy, opts = {}) {
13
13
  opts.signal?.removeEventListener("abort", onAbort);
14
14
  resolve();
15
15
  }, ms);
16
- timer.unref?.();
17
16
  opts.signal?.addEventListener("abort", onAbort, { once: true });
18
17
  }));
19
18
  let last = await op(1);
@@ -78,3 +78,4 @@ export interface CompactionPreparation {
78
78
  export declare function prepareCompaction(pathEntries: SessionTreeEntry[], settings: CompactionSettings, charsPerToken?: number, windowTokens?: number): Result<CompactionPreparation | undefined, CompactionError>;
79
79
  export { computeFileLists, serializeConversation } from "./utils.js";
80
80
  export declare function compact(preparation: CompactionPreparation, model: Model, apiKey: string | undefined, headers?: Record<string, string>, customInstructions?: string, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, runtime?: AgentCoreCompletionRuntimeDeps, charsPerToken?: number, onInputTruncated?: (info: SummarizationInputTruncation) => void, onPtlRetry?: () => void): Promise<Result<CompactionResult, CompactionError>>;
81
+ export declare function turnPrefixSummarizationPrompt(customInstructions?: string): string;
@@ -132,6 +132,9 @@ export function shouldCompact(contextTokens, contextWindow, settings) {
132
132
  if (!settings.enabled) {
133
133
  return false;
134
134
  }
135
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0) {
136
+ return false;
137
+ }
135
138
  return contextTokens > contextWindow - settings.reserveTokens;
136
139
  }
137
140
  export function estimateTokens(message, charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
@@ -539,7 +542,7 @@ export function dryRunSummarizationClamp(preparation, model, customInstructions,
539
542
  if (isSplitTurn && turnPrefixMessages.length > 0) {
540
543
  const text = serializeConversation(convertToLlm(turnPrefixMessages));
541
544
  conversationChars += text.length;
542
- const clamped = clampSummarizationConversation(text, model, cpt, SUMMARIZATION_SYSTEM_PROMPT.length + TURN_PREFIX_SUMMARIZATION_PROMPT.length + 128, Math.floor(0.5 * summaryBudget));
545
+ const clamped = clampSummarizationConversation(text, model, cpt, SUMMARIZATION_SYSTEM_PROMPT.length + turnPrefixSummarizationPrompt(customInstructions).length + 128, Math.floor(0.5 * summaryBudget));
543
546
  dropped += Math.max(0, clamped.droppedChars);
544
547
  kept += clamped.keptChars;
545
548
  }
@@ -879,7 +882,7 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
879
882
  ? generateSummary(messagesToSummarize, model, summaryBudget, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry)
880
883
  :
881
884
  Promise.resolve(ok(previousSummary ? stripFileOperationsFooter(previousSummary) : "No prior history.")),
882
- generateTurnPrefixSummary(turnPrefixMessages, model, summaryBudget, apiKey, headers, signal, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry),
885
+ generateTurnPrefixSummary(turnPrefixMessages, model, summaryBudget, apiKey, headers, signal, customInstructions, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry),
883
886
  ]);
884
887
  if (!historyResult.ok) {
885
888
  return err(historyResult.error);
@@ -913,16 +916,22 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
913
916
  },
914
917
  });
915
918
  }
916
- async function generateTurnPrefixSummary(messages, model, summaryBudgetTokens, apiKey, headers, signal, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry) {
919
+ export function turnPrefixSummarizationPrompt(customInstructions) {
920
+ return customInstructions
921
+ ? `${TURN_PREFIX_SUMMARIZATION_PROMPT}\n\nAdditional Instructions:\n${customInstructions}`
922
+ : TURN_PREFIX_SUMMARIZATION_PROMPT;
923
+ }
924
+ async function generateTurnPrefixSummary(messages, model, summaryBudgetTokens, apiKey, headers, signal, customInstructions, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry) {
917
925
  const llmMessages = convertToLlm(messages);
926
+ const basePrompt = turnPrefixSummarizationPrompt(customInstructions);
918
927
  return summarizeWithPtlRetry({
919
928
  label: "Turn prefix summarization",
920
929
  truncationLabel: "turn_prefix",
921
930
  llmMessages,
922
- buildPromptText: (conversationText) => `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`,
931
+ buildPromptText: (conversationText) => `<conversation>\n${conversationText}\n</conversation>\n\n${basePrompt}`,
923
932
  model,
924
933
  cpt: charsPerToken ?? model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN,
925
- fixedPromptChars: SUMMARIZATION_SYSTEM_PROMPT.length + TURN_PREFIX_SUMMARIZATION_PROMPT.length + 128,
934
+ fixedPromptChars: SUMMARIZATION_SYSTEM_PROMPT.length + basePrompt.length + 128,
926
935
  baseMaxTokens: Math.floor(0.5 * summaryBudgetTokens),
927
936
  apiKey,
928
937
  headers,
@@ -8,8 +8,7 @@ export function createFrameDecoder(maxBytes = 50 * 1024 * 1024) {
8
8
  return function decode(chunk) {
9
9
  const frames = [];
10
10
  buf = Buffer.concat([buf, chunk]);
11
- if (buf.length > maxBytes + HEADER_SLACK)
12
- buf = Buffer.alloc(0);
11
+ let awaiting;
13
12
  for (;;) {
14
13
  const he = buf.indexOf("\r\n\r\n");
15
14
  if (he === -1)
@@ -25,11 +24,15 @@ export function createFrameDecoder(maxBytes = 50 * 1024 * 1024) {
25
24
  continue;
26
25
  }
27
26
  const start = he + 4;
28
- if (buf.length < start + len)
27
+ if (buf.length < start + len) {
28
+ awaiting = start + len;
29
29
  break;
30
+ }
30
31
  frames.push(buf.subarray(start, start + len).toString("utf8"));
31
32
  buf = buf.subarray(start + len);
32
33
  }
34
+ if (buf.length > (awaiting ?? maxBytes + HEADER_SLACK))
35
+ buf = Buffer.alloc(0);
33
36
  return frames;
34
37
  };
35
38
  }
@@ -29,6 +29,7 @@ export declare class NodeLspManager implements LspServerManager {
29
29
  private readonly cache;
30
30
  private readonly opening;
31
31
  private readonly failed;
32
+ private readonly initFailures;
32
33
  private disposed;
33
34
  constructor(opts?: NodeLspManagerOptions);
34
35
  private sweepIdle;
@@ -37,6 +38,7 @@ export declare class NodeLspManager implements LspServerManager {
37
38
  private evictOverCap;
38
39
  evict(root: string): Promise<void>;
39
40
  clearFailed(root?: string): void;
41
+ unavailableReason(filePath: string, env?: ExecutionEnv): string | undefined;
40
42
  private openSession;
41
43
  private serverCmd;
42
44
  dispose(): Promise<void>;
@@ -1,13 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { killProcessTree } from "../execution-env/kill-tree.js";
3
- import { readFile, stat } from "node:fs/promises";
3
+ import { open, readFile, stat } from "node:fs/promises";
4
4
  import { basename, dirname, extname, resolve as resolvePath } from "node:path";
5
- import { existsSync } from "node:fs";
5
+ import { constants as fsConstants, existsSync } from "node:fs";
6
6
  import { pathToUri } from "../../core/lsp-protocol.js";
7
7
  import { LspDiagnosticsRegistry } from "../../core/lsp-diagnostics.js";
8
8
  import { TransportLspSession } from "../../core/lsp-session.js";
9
9
  import { StdioLspTransport } from "./stdio-lsp-transport.js";
10
- import { MAX_LSP_FILE_BYTES } from "../../core/lsp.js";
10
+ import { MAX_LSP_FILE_BYTES, SharedAbortScope, settleOnAbort } from "../../core/lsp.js";
11
11
  export const DEFAULT_LSP_SERVERS = {
12
12
  typescript: "typescript-language-server --stdio",
13
13
  javascript: "typescript-language-server --stdio",
@@ -46,6 +46,7 @@ const CLIENT_CAPABILITIES = {
46
46
  },
47
47
  workspace: { symbol: {}, workspaceFolders: false },
48
48
  };
49
+ const INIT_FAILURE_LATCH = 2;
49
50
  export class NodeLspManager {
50
51
  servers;
51
52
  spawnFn;
@@ -60,6 +61,7 @@ export class NodeLspManager {
60
61
  cache = new Map();
61
62
  opening = new Map();
62
63
  failed = new Map();
64
+ initFailures = new Map();
63
65
  disposed = false;
64
66
  constructor(opts = {}) {
65
67
  this.servers = { ...DEFAULT_LSP_SERVERS, ...opts.servers };
@@ -79,6 +81,10 @@ export class NodeLspManager {
79
81
  sweepIdle() {
80
82
  const cutoff = Date.now() - this.idleTtlMs;
81
83
  for (const [key, e] of [...this.cache]) {
84
+ if (e.session.inFlight > 0) {
85
+ e.lastUsed = Date.now();
86
+ continue;
87
+ }
82
88
  if (e.lastUsed < cutoff) {
83
89
  this.cache.delete(key);
84
90
  this.log("lsp_evict_idle", { key });
@@ -106,19 +112,20 @@ export class NodeLspManager {
106
112
  this.cache.delete(key);
107
113
  const carry = cached.session.openedFiles();
108
114
  this.log("lsp_heal", { language, root, carried: carry.length });
109
- const healed = await this.open(key, language, root, signal, env);
110
- if (healed && carry.length)
111
- await healed.warmOpen(carry, signal).catch(() => undefined);
112
- return healed;
115
+ return this.open(key, language, root, signal, env, carry);
113
116
  }
114
117
  return this.open(key, language, root, signal, env);
115
118
  }
116
- open(key, language, root, signal, env) {
119
+ open(key, language, root, signal, env, carry = []) {
117
120
  const inflight = this.opening.get(key);
118
- if (inflight)
119
- return inflight.job;
120
- const entry = { root, cancelled: false, job: undefined };
121
- entry.job = this.openSession(language, root, signal, env)
121
+ if (inflight && !inflight.scope.signal.aborted) {
122
+ inflight.scope.join(signal);
123
+ return settleOnAbort(inflight.job, signal, () => undefined);
124
+ }
125
+ const scope = new SharedAbortScope();
126
+ scope.join(signal);
127
+ const entry = { root, cancelled: false, scope, job: undefined };
128
+ entry.job = this.openSession(language, root, scope.signal, env)
122
129
  .then(async (session) => {
123
130
  if (!session)
124
131
  return undefined;
@@ -126,26 +133,41 @@ export class NodeLspManager {
126
133
  await session.close().catch(() => undefined);
127
134
  return undefined;
128
135
  }
136
+ if (carry.length) {
137
+ const carried = await session.warmOpen(carry, scope.signal).catch(() => ({ opened: 0, failed: carry.length }));
138
+ this.log("lsp_heal_carried", { language, root, ...carried });
139
+ if (this.disposed || entry.cancelled) {
140
+ await session.close().catch(() => undefined);
141
+ return undefined;
142
+ }
143
+ }
129
144
  this.evictOverCap();
130
145
  this.cache.set(key, { session, root, lastUsed: Date.now() });
131
146
  return session;
132
147
  })
133
- .finally(() => this.opening.delete(key));
148
+ .finally(() => {
149
+ if (this.opening.get(key) === entry)
150
+ this.opening.delete(key);
151
+ });
134
152
  this.opening.set(key, entry);
135
- return entry.job;
153
+ return settleOnAbort(entry.job, signal, () => undefined);
136
154
  }
137
155
  evictOverCap() {
138
156
  while (this.cache.size >= this.maxSessions) {
139
157
  let lruKey;
140
158
  let lruTime = Infinity;
141
159
  for (const [k, e] of this.cache) {
160
+ if (e.session.inFlight > 0)
161
+ continue;
142
162
  if (e.lastUsed < lruTime) {
143
163
  lruTime = e.lastUsed;
144
164
  lruKey = k;
145
165
  }
146
166
  }
147
- if (lruKey === undefined)
167
+ if (lruKey === undefined) {
168
+ this.log("lsp_evict_deferred", { size: this.cache.size, maxSessions: this.maxSessions });
148
169
  break;
170
+ }
149
171
  const evicted = this.cache.get(lruKey);
150
172
  this.cache.delete(lruKey);
151
173
  this.log("lsp_evict_lru", { key: lruKey });
@@ -154,6 +176,7 @@ export class NodeLspManager {
154
176
  }
155
177
  async evict(root) {
156
178
  const closing = [];
179
+ this.clearFailed(root);
157
180
  for (const [key, e] of [...this.cache]) {
158
181
  if (e.root === root) {
159
182
  this.cache.delete(key);
@@ -174,8 +197,27 @@ export class NodeLspManager {
174
197
  if (root !== undefined && e.root !== root)
175
198
  continue;
176
199
  this.failed.delete(key);
177
- this.log("lsp_negative_cache_cleared", { key, attempts: e.attempts });
200
+ this.initFailures.delete(key);
201
+ this.log("lsp_negative_cache_cleared", { key, attempts: e.attempts, kind: e.kind });
178
202
  }
203
+ for (const [key, _n] of [...this.initFailures]) {
204
+ if (root === undefined || key.endsWith(` ${root}`))
205
+ this.initFailures.delete(key);
206
+ }
207
+ }
208
+ unavailableReason(filePath, env) {
209
+ const language = languageFor(filePath);
210
+ if (!language)
211
+ return undefined;
212
+ const cmd = this.serverCmd(language);
213
+ if (!cmd)
214
+ return `no ${language} language server is configured in this environment`;
215
+ const e = this.failed.get(`${language} ${this.resolveRootFn(filePath, env)}`);
216
+ if (e?.kind === "missing_binary")
217
+ return `the ${language} language server ("${e.command}") is not installed on this host`;
218
+ if (e?.kind === "initialize")
219
+ return `the ${language} language server ("${e.command}") is installed but failed to initialize`;
220
+ return undefined;
179
221
  }
180
222
  async openSession(language, root, signal, _env) {
181
223
  const cmd = this.serverCmd(language);
@@ -183,10 +225,18 @@ export class NodeLspManager {
183
225
  return undefined;
184
226
  const tokens = (cmd.match(/"[^"]*"|\S+/g) ?? []).map((t) => (t.startsWith('"') && t.endsWith('"') && t.length >= 2 ? t.slice(1, -1) : t));
185
227
  const [command, ...args] = tokens.length > 0 ? tokens : [""];
186
- const child = await this.spawnFn(command, args, root, signal).catch(() => undefined);
228
+ const key = `${language} ${root}`;
229
+ let child;
230
+ try {
231
+ child = await this.spawnFn(command, args, root, signal);
232
+ }
233
+ catch (e) {
234
+ this.log("lsp_spawn_error", { language, root, command, err: e instanceof Error ? e.message : String(e) });
235
+ return undefined;
236
+ }
187
237
  if (!child) {
188
238
  if (signal?.aborted !== true)
189
- this.failed.set(`${language} ${root}`, { root, attempts: 1 });
239
+ this.failed.set(key, { root, attempts: 1, kind: "missing_binary", command, language });
190
240
  this.log("lsp_spawn_degraded", { language, root, command });
191
241
  return undefined;
192
242
  }
@@ -204,9 +254,18 @@ export class NodeLspManager {
204
254
  }
205
255
  catch (e) {
206
256
  await transport.close();
207
- this.log("lsp_init_degraded", { language, root, err: e instanceof Error ? e.message : String(e) });
257
+ const attempt = (this.initFailures.get(key) ?? 0) + 1;
258
+ const latched = attempt >= INIT_FAILURE_LATCH && signal?.aborted !== true;
259
+ if (latched) {
260
+ this.failed.set(key, { root, attempts: 1, kind: "initialize", command, language });
261
+ this.initFailures.delete(key);
262
+ }
263
+ else
264
+ this.initFailures.set(key, attempt);
265
+ this.log("lsp_init_degraded", { language, root, err: e instanceof Error ? e.message : String(e), attempt, latched });
208
266
  return undefined;
209
267
  }
268
+ this.initFailures.delete(key);
210
269
  this.log("lsp_open", { language, root });
211
270
  return new TransportLspSession(transport, language, this.readTextFn, this.log, this.warmup, this.diagnostics);
212
271
  }
@@ -249,33 +308,51 @@ export const defaultLspReadText = async (fp, signal) => {
249
308
  throw new Error(`not a regular file: ${fp}`);
250
309
  if (st.size > MAX_LSP_FILE_BYTES)
251
310
  throw new Error(`file too large for LSP analysis (${st.size} bytes exceeds ${MAX_LSP_FILE_BYTES})`);
252
- return readFile(fp, { encoding: "utf8", signal });
311
+ const flags = fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK ?? 0);
312
+ const fh = await open(fp, flags);
313
+ try {
314
+ const fst = await fh.stat();
315
+ if (!fst.isFile())
316
+ throw new Error(`not a regular file: ${fp}`);
317
+ if (fst.size > MAX_LSP_FILE_BYTES)
318
+ throw new Error(`file too large for LSP analysis (${fst.size} bytes exceeds ${MAX_LSP_FILE_BYTES})`);
319
+ return await readFile(fh, { encoding: "utf8", signal });
320
+ }
321
+ finally {
322
+ await fh.close().catch(() => undefined);
323
+ }
253
324
  };
254
325
  export function lspSpawnOptions(cwd) {
255
326
  return process.platform === "win32"
256
327
  ? { cwd, stdio: ["pipe", "pipe", "ignore"], shell: true }
257
328
  : { cwd, stdio: ["pipe", "pipe", "ignore"] };
258
329
  }
259
- export const defaultLspSpawn = (command, args, cwd, signal) => new Promise((resolve) => {
330
+ export const defaultLspSpawn = (command, args, cwd, signal) => new Promise((resolve, reject) => {
260
331
  let child;
261
332
  try {
262
333
  child = spawn(command, args, lspSpawnOptions(cwd));
263
334
  }
264
- catch {
265
- return resolve(undefined);
335
+ catch (e) {
336
+ return reject(e instanceof Error ? e : new Error(String(e)));
266
337
  }
338
+ child.on("error", () => { });
267
339
  const onSpawn = () => {
268
340
  cleanup();
269
341
  resolve(child);
270
342
  };
271
- const onError = () => {
343
+ const onError = (err) => {
272
344
  cleanup();
273
- resolve(undefined);
345
+ if (err?.code === "ENOENT")
346
+ resolve(undefined);
347
+ else
348
+ reject(err instanceof Error ? err : new Error(String(err)));
274
349
  };
275
350
  const onAbort = () => {
276
351
  cleanup();
277
352
  try {
278
- if (process.platform === "win32" && child.pid)
353
+ if (child.pid === undefined)
354
+ return resolve(undefined);
355
+ if (process.platform === "win32")
279
356
  killProcessTree(child.pid, { force: true });
280
357
  else
281
358
  child.kill();
@@ -16,7 +16,17 @@ export class StdioLspTransport {
16
16
  for (const frame of this.decode(chunk))
17
17
  this.onMessage(frame);
18
18
  });
19
- child.stdin.on("error", () => { });
19
+ child.stdin.on("error", (e) => this.failAll(toError(e)));
20
+ const stdout = child.stdout;
21
+ if (typeof stdout.removeListener === "function" && typeof stdout.once === "function") {
22
+ for (const event of ["end", "close", "error"]) {
23
+ stdout.on(event, (arg) => {
24
+ if (Buffer.isBuffer(arg))
25
+ return;
26
+ this.failAll(new Error(`LSP server stdout ${event}`));
27
+ });
28
+ }
29
+ }
20
30
  child.on("exit", () => this.failAll(new Error("LSP server exited")));
21
31
  child.on("error", (e) => this.failAll(toError(e)));
22
32
  }
@@ -36,7 +46,8 @@ export class StdioLspTransport {
36
46
  try {
37
47
  this.send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `Method not found: ${String(msg.method)}` } });
38
48
  }
39
- catch {
49
+ catch (e) {
50
+ this.failAll(toError(e));
40
51
  }
41
52
  }
42
53
  return;
@@ -121,10 +132,7 @@ export class StdioLspTransport {
121
132
  this.send({ jsonrpc: "2.0", id, method, params });
122
133
  }
123
134
  catch (e) {
124
- if (this.pending.delete(id)) {
125
- cleanup();
126
- reject(brandLspFailure(e instanceof Error ? e : new Error(String(e)), "server_terminated"));
127
- }
135
+ this.failAll(brandLspFailure(e instanceof Error ? e : new Error(String(e)), "server_terminated"));
128
136
  }
129
137
  });
130
138
  }
@@ -134,7 +142,8 @@ export class StdioLspTransport {
134
142
  try {
135
143
  this.send({ jsonrpc: "2.0", method, params });
136
144
  }
137
- catch {
145
+ catch (e) {
146
+ this.failAll(toError(e));
138
147
  }
139
148
  }
140
149
  async close() {
package/dist/index.d.ts CHANGED
@@ -188,7 +188,7 @@ export { createDegradingBrain, readDegradation, DEGRADED_DIAGNOSTIC_TYPE, type D
188
188
  export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
189
189
  export { type BrainTimeoutConfig } from "./brain/timeout.js";
190
190
  export { createAssistantMessageEventStream } from "./internal/llm.js";
191
- export type { AssistantMessage, AssistantMessageEvent, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
191
+ export type { AssistantMessage, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
192
192
  export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, RuntimeCaps, BackgroundChildEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskResult, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolEffect, WorkflowGovernanceBaseline, } from "./core/types.js";
193
193
  export { Type } from "typebox";
194
194
  export type { TSchema, Static } from "typebox";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatFileOperations, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, formatFileOperations, formatPersistedOutputRefs, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";