@sema-agent/core 1.451.0 → 2.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 (191) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +15 -0
  2. package/dist/agents/agent-transcript-tool.js +122 -0
  3. package/dist/agents/cascade.js +5 -8
  4. package/dist/agents/observer.js +3 -0
  5. package/dist/agents/repair-loop.d.ts +1 -1
  6. package/dist/agents/repair-loop.js +1 -1
  7. package/dist/agents/retain-ledger.d.ts +60 -0
  8. package/dist/agents/retain-ledger.js +225 -0
  9. package/dist/agents/roster-store.d.ts +1 -0
  10. package/dist/agents/roster-store.js +9 -0
  11. package/dist/agents/send-message-tool.d.ts +41 -0
  12. package/dist/agents/send-message-tool.js +469 -0
  13. package/dist/agents/subagent.d.ts +33 -108
  14. package/dist/agents/subagent.js +40 -813
  15. package/dist/agents/teacher.d.ts +0 -1
  16. package/dist/agents/teacher.js +0 -9
  17. package/dist/bin/sema-tb.js +3 -3
  18. package/dist/brain/anthropic.js +15 -40
  19. package/dist/brain/openai.js +15 -40
  20. package/dist/brain/reasoning.d.ts +1 -1
  21. package/dist/brain/stream-engine.js +2 -2
  22. package/dist/brain/stream-shared.d.ts +17 -0
  23. package/dist/brain/stream-shared.js +50 -0
  24. package/dist/brain/timeout.d.ts +1 -1
  25. package/dist/brain/timeout.js +1 -1
  26. package/dist/config/catalog.js +1 -3
  27. package/dist/config/defaults.d.ts +6 -0
  28. package/dist/config/defaults.js +6 -0
  29. package/dist/core/ask-question.d.ts +1 -1
  30. package/dist/core/ask-question.js +5 -5
  31. package/dist/core/background-agent-store.d.ts +2 -0
  32. package/dist/core/background-agent-store.js +5 -1
  33. package/dist/core/background-shell.d.ts +1 -1
  34. package/dist/core/checkpoint-store.d.ts +0 -1
  35. package/dist/core/exec-gate.d.ts +1 -1
  36. package/dist/core/exec-output-tail.d.ts +19 -1
  37. package/dist/core/exec-output-tail.js +44 -5
  38. package/dist/core/file-snapshot-store.d.ts +1 -1
  39. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  40. package/dist/core/git-worktree-env.d.ts +1 -1
  41. package/dist/core/hooks.d.ts +1 -1
  42. package/dist/core/image-downsample.d.ts +44 -0
  43. package/dist/core/image-downsample.js +97 -0
  44. package/dist/core/lsp-session.d.ts +1 -0
  45. package/dist/core/lsp-session.js +24 -6
  46. package/dist/core/lsp.d.ts +11 -1
  47. package/dist/core/lsp.js +63 -6
  48. package/dist/core/mailbox-store.d.ts +3 -2
  49. package/dist/core/mailbox-store.js +19 -4
  50. package/dist/core/mcp.d.ts +3 -45
  51. package/dist/core/mcp.js +2 -97
  52. package/dist/core/media-byte-cap.d.ts +1 -1
  53. package/dist/core/memory-recall.d.ts +0 -12
  54. package/dist/core/memory-recall.js +0 -222
  55. package/dist/core/memory.d.ts +0 -6
  56. package/dist/core/memory.js +0 -88
  57. package/dist/core/message-utils.d.ts +1 -1
  58. package/dist/core/oracle-isolation.d.ts +1 -1
  59. package/dist/core/remote-env.d.ts +1 -1
  60. package/dist/core/roles.d.ts +1 -1
  61. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  62. package/dist/core/runner/assemble-result.d.ts +0 -2
  63. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  64. package/dist/core/runner/prepare-memory.d.ts +16 -0
  65. package/dist/core/runner/prepare-memory.js +191 -0
  66. package/dist/core/runner/prepare-task.d.ts +4 -28
  67. package/dist/core/runner/prepare-task.js +29 -208
  68. package/dist/core/runner/prompt-suggestions.d.ts +1 -1
  69. package/dist/core/runner/runtask.d.ts +2 -3
  70. package/dist/core/runner/runtask.js +337 -529
  71. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  72. package/dist/core/runner/synthetic-tools.d.ts +5 -5
  73. package/dist/core/runner/synthetic-tools.js +4 -4
  74. package/dist/core/runner/tool-disclosure.d.ts +3 -3
  75. package/dist/core/runner/tool-disclosure.js +2 -2
  76. package/dist/core/runner/tool-output-projection.d.ts +6 -0
  77. package/dist/core/runner/tool-output-projection.js +81 -0
  78. package/dist/core/runtime.d.ts +1 -1
  79. package/dist/core/scheduler.d.ts +1 -1
  80. package/dist/core/sensitive-path-policy.d.ts +1 -1
  81. package/dist/core/session-reconcile.d.ts +1 -1
  82. package/dist/core/session-reconcile.js +5 -2
  83. package/dist/core/session-store.d.ts +2 -1
  84. package/dist/core/session-store.js +2 -1
  85. package/dist/core/session.d.ts +0 -1
  86. package/dist/core/task-notification.d.ts +1 -0
  87. package/dist/core/task-registry-agent.d.ts +115 -0
  88. package/dist/core/task-registry-agent.js +1163 -0
  89. package/dist/core/task-registry-monitor.d.ts +13 -0
  90. package/dist/core/task-registry-monitor.js +347 -0
  91. package/dist/core/task-registry-shared.d.ts +294 -0
  92. package/dist/core/task-registry-shared.js +271 -0
  93. package/dist/core/task-registry-workflow.d.ts +5 -0
  94. package/dist/core/task-registry-workflow.js +143 -0
  95. package/dist/core/task-registry.d.ts +54 -213
  96. package/dist/core/task-registry.js +190 -1766
  97. package/dist/core/task-tool-shape.d.ts +32 -0
  98. package/dist/core/task-tool-shape.js +83 -0
  99. package/dist/core/tool-policy.d.ts +1 -2
  100. package/dist/core/tool-result-budget.d.ts +1 -1
  101. package/dist/core/tool-result-store.d.ts +2 -2
  102. package/dist/core/tool-result-store.js +3 -3
  103. package/dist/core/tools.d.ts +7 -3
  104. package/dist/core/tools.js +3 -8
  105. package/dist/core/trace.d.ts +2 -2
  106. package/dist/core/types.d.ts +1 -29
  107. package/dist/core/workflow-journal-store.d.ts +2 -0
  108. package/dist/core/workflow-journal-store.js +14 -0
  109. package/dist/core/workflow-run-store-contract.d.ts +1 -1
  110. package/dist/core/workflow-run-store-contract.js +19 -19
  111. package/dist/engine/execution-env/node-execution-env.d.ts +14 -0
  112. package/dist/engine/execution-env/node-execution-env.js +184 -44
  113. package/dist/engine/lsp/frame-decoder.d.ts +1 -1
  114. package/dist/engine/lsp/frame-decoder.js +1 -1
  115. package/dist/engine/lsp/node-lsp-manager.d.ts +4 -2
  116. package/dist/engine/lsp/node-lsp-manager.js +22 -5
  117. package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
  118. package/dist/engine/lsp/stdio-lsp-transport.js +19 -8
  119. package/dist/index.d.ts +18 -16
  120. package/dist/index.js +15 -13
  121. package/dist/internal/harness-types.d.ts +7 -0
  122. package/dist/internal/harness-types.js +1 -0
  123. package/dist/internal/harness.d.ts +1 -7
  124. package/dist/internal/harness.js +1 -0
  125. package/dist/orchestration/goal.js +2 -2
  126. package/dist/orchestration/run-workflow-tool.d.ts +2 -1
  127. package/dist/orchestration/run-workflow-tool.js +29 -6
  128. package/dist/orchestration/workflow-types.d.ts +193 -0
  129. package/dist/orchestration/workflow-types.js +54 -0
  130. package/dist/orchestration/workflow.d.ts +3 -184
  131. package/dist/orchestration/workflow.js +87 -64
  132. package/dist/prompt-assembly/assemble.d.ts +3 -3
  133. package/dist/prompt-assembly/assemble.js +0 -16
  134. package/dist/prompt-assembly/explain.d.ts +1 -1
  135. package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
  136. package/dist/prompts/default.d.ts +1 -6
  137. package/dist/prompts/default.js +3 -11
  138. package/dist/scenarios/env.d.ts +1 -1
  139. package/dist/scenarios/full-body.d.ts +0 -3
  140. package/dist/scenarios/full-body.js +0 -2
  141. package/dist/stores/cc/mailbox-store.js +6 -1
  142. package/dist/stores/file/background-agent-store.d.ts +2 -15
  143. package/dist/stores/file/background-agent-store.js +33 -102
  144. package/dist/stores/file/checkpoint-store.d.ts +1 -14
  145. package/dist/stores/file/checkpoint-store.js +47 -119
  146. package/dist/stores/file/file-snapshot-store.d.ts +1 -1
  147. package/dist/stores/file/mailbox-store.d.ts +1 -1
  148. package/dist/stores/file/mailbox-store.js +9 -7
  149. package/dist/stores/file/shared-ledger.d.ts +35 -0
  150. package/dist/stores/file/shared-ledger.js +94 -0
  151. package/dist/stores/file/workflow-run-store.d.ts +1 -14
  152. package/dist/stores/file/workflow-run-store.js +24 -93
  153. package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
  154. package/dist/tools/fs/bash-readonly-classifier.js +166 -0
  155. package/dist/tools/fs/encoding.d.ts +5 -0
  156. package/dist/tools/fs/encoding.js +6 -0
  157. package/dist/tools/fs/fs-bash.d.ts +31 -0
  158. package/dist/tools/fs/fs-bash.js +672 -0
  159. package/dist/tools/fs/fs-pdf.d.ts +22 -0
  160. package/dist/tools/fs/fs-pdf.js +236 -0
  161. package/dist/tools/fs/fs-read.d.ts +8 -0
  162. package/dist/tools/fs/fs-read.js +286 -0
  163. package/dist/tools/fs/fs-search-tools.d.ts +5 -0
  164. package/dist/tools/fs/fs-search-tools.js +173 -0
  165. package/dist/tools/fs/fs-shared.d.ts +55 -0
  166. package/dist/tools/fs/fs-shared.js +144 -0
  167. package/dist/tools/fs/fs-write.d.ts +8 -0
  168. package/dist/tools/fs/fs-write.js +423 -0
  169. package/dist/tools/fs/index.d.ts +12 -65
  170. package/dist/tools/fs/index.js +26 -2026
  171. package/dist/tools/fs/notebook.d.ts +43 -0
  172. package/dist/tools/fs/notebook.js +141 -0
  173. package/dist/tools/fs/pdf.d.ts +1 -1
  174. package/dist/tools/fs/repo-map.d.ts +3 -3
  175. package/dist/tools/fs/repo-map.js +3 -3
  176. package/dist/tools/fs/safety.d.ts +1 -1
  177. package/dist/tools/fs/search.d.ts +1 -1
  178. package/dist/tools/fs/search.js +141 -12
  179. package/dist/tools/gitea-issue.js +4 -2
  180. package/dist/tools/monitor.d.ts +1 -1
  181. package/dist/tools/monitor.js +12 -8
  182. package/dist/tools/scheduler-tools.d.ts +1 -1
  183. package/dist/tools/scheduler-tools.js +16 -16
  184. package/dist/tools/task-list.js +34 -12
  185. package/dist/tools/web.d.ts +4 -2
  186. package/dist/tools/web.js +106 -20
  187. package/dist/tools/worktree.d.ts +1 -1
  188. package/dist/tools/worktree.js +14 -14
  189. package/package.json +1 -1
  190. package/dist/core/dynamic-recall.d.ts +0 -32
  191. package/dist/core/dynamic-recall.js +0 -76
@@ -1,14 +1,14 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { closeSync, constants, createReadStream, mkdtempSync, openSync, readSync, statSync, truncateSync, unlinkSync } from "node:fs";
4
- import { access, appendFile, lstat, mkdir, mkdtemp, readdir, readFile, readlink, realpath, rm, writeFile, } from "node:fs/promises";
4
+ import { access, appendFile, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rename, rm, unlink, writeFile, } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { isAbsolute, join, resolve } from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
9
9
  import { killProcessTree, shutdownDebug } from "./kill-tree.js";
10
10
  import { scrubSecretEnv } from "../../core/secret-env.js";
11
- import { RollingTailBuffer, markTruncated } from "../../core/exec-output-tail.js";
11
+ import { RollingTailBuffer, markLiveSpoolRotations, markTruncated, newStreamCursorState, sliceStreamIncrement, } from "../../core/exec-output-tail.js";
12
12
  import { BackgroundShellError } from "../../core/background-shell.js";
13
13
  import { SchedulerError } from "../../core/scheduler.js";
14
14
  const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
@@ -88,6 +88,11 @@ function toFileError(error, path) {
88
88
  function abortResult(signal, path) {
89
89
  return signal?.aborted ? err(new FileError("aborted", "aborted", path)) : undefined;
90
90
  }
91
+ function isFsSupportError(error) {
92
+ const code = isNodeError(error) ? error.code : undefined;
93
+ return code === "EINVAL" || code === "ENOTSUP" || code === "EPERM" || code === "ENOSYS";
94
+ }
95
+ const RENAME_FALLBACK_CODES = new Set(["EXDEV", "EPERM", "EEXIST", "EBUSY"]);
91
96
  async function pathExists(path) {
92
97
  try {
93
98
  await access(path, constants.F_OK);
@@ -208,6 +213,23 @@ const EXEC_SPOOL_PUMP_MS = 200;
208
213
  const EXEC_SPOOL_ROTATE_BYTES = 8 * 1024 * 1024;
209
214
  const EXEC_SPOOL_FINAL_TAIL_BYTES = 8 * 1024 * 1024;
210
215
  const EXEC_SPOOL_HARD_CAP_BYTES = 16 * 1024 * 1024;
216
+ export function decideSpoolReclaim(args) {
217
+ const rotate = args.rotateBytes ?? EXEC_SPOOL_ROTATE_BYTES;
218
+ const hardCap = args.hardCapBytes ?? EXEC_SPOOL_HARD_CAP_BYTES;
219
+ if (args.cursor < rotate)
220
+ return { reclaim: false };
221
+ if (args.size === args.cursor) {
222
+ if (!args.writerLive)
223
+ return { reclaim: true, live: false, skippedBytes: 0 };
224
+ if (args.size >= hardCap)
225
+ return { reclaim: true, live: true, skippedBytes: 0 };
226
+ return { reclaim: false };
227
+ }
228
+ if (args.size > args.cursor && args.size >= hardCap) {
229
+ return { reclaim: true, live: args.writerLive, skippedBytes: args.size - args.cursor };
230
+ }
231
+ return { reclaim: false };
232
+ }
211
233
  const SIGNUM = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6, SIGKILL: 9, SIGSEGV: 11, SIGPIPE: 13, SIGTERM: 15 };
212
234
  export function openSpoolPair(base) {
213
235
  const outPath = `${base}.out.log`;
@@ -308,6 +330,9 @@ export class NodeExecutionEnv {
308
330
  const adoptOnTimeoutRef = {};
309
331
  const pumpSpoolRef = {};
310
332
  const drainSpoolRef = {};
333
+ const liveRotations = { out: 0, err: 0 };
334
+ const liveRotationsTotal = () => liveRotations.out + liveRotations.err;
335
+ const writerLive = () => !settled && child !== undefined && child.exitCode === null && child.signalCode === null;
311
336
  const armForceSettle = (result) => {
312
337
  if (forceSettleRef.current || settled)
313
338
  return;
@@ -326,7 +351,7 @@ export class NodeExecutionEnv {
326
351
  drainSpoolRef.current?.();
327
352
  const so = stdoutTail.result();
328
353
  const se = stderrTail.result();
329
- return { stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes) };
354
+ return { stdout: markTruncated(so.text, so.droppedBytes), stderr: markLiveSpoolRotations(markTruncated(se.text, se.droppedBytes), liveRotationsTotal()) };
330
355
  };
331
356
  const onAbort = () => {
332
357
  abortKillInFlight = true;
@@ -515,16 +540,15 @@ export class NodeExecutionEnv {
515
540
  if (spoolCursor[lane.cursor] < EXEC_SPOOL_ROTATE_BYTES)
516
541
  return;
517
542
  try {
518
- const size = statSync(lane.path).size;
519
- if (size === spoolCursor[lane.cursor]) {
520
- truncateSync(lane.path, 0);
521
- spoolCursor[lane.cursor] = 0;
522
- }
523
- else if (size >= EXEC_SPOOL_HARD_CAP_BYTES) {
524
- lane.tail.recordSkippedBytes(size - spoolCursor[lane.cursor]);
525
- truncateSync(lane.path, 0);
526
- spoolCursor[lane.cursor] = 0;
527
- }
543
+ const d = decideSpoolReclaim({ size: statSync(lane.path).size, cursor: spoolCursor[lane.cursor], writerLive: writerLive() });
544
+ if (!d.reclaim)
545
+ return;
546
+ if (d.skippedBytes > 0)
547
+ lane.tail.recordSkippedBytes(d.skippedBytes);
548
+ if (d.live)
549
+ liveRotations[lane.cursor]++;
550
+ truncateSync(lane.path, 0);
551
+ spoolCursor[lane.cursor] = 0;
528
552
  }
529
553
  catch {
530
554
  }
@@ -559,7 +583,9 @@ export class NodeExecutionEnv {
559
583
  if (settled || !child)
560
584
  return false;
561
585
  pumpSpoolRef.current?.();
562
- const shellId = this.adoptRunningChild(child, stdoutTail.result(), stderrTail.result(), execSpoolRef.current !== undefined ? { spool: execSpoolRef.current, cursors: { out: spoolCursor.out, err: spoolCursor.err } } : undefined);
586
+ const shellId = this.adoptRunningChild(child, stdoutTail.result(), stderrTail.result(), execSpoolRef.current !== undefined
587
+ ? { spool: execSpoolRef.current, cursors: { out: spoolCursor.out, err: spoolCursor.err }, liveRotations: liveRotationsTotal() }
588
+ : undefined);
563
589
  if (shellId === undefined)
564
590
  return false;
565
591
  if (spoolPumpRef.current)
@@ -652,12 +678,12 @@ export class NodeExecutionEnv {
652
678
  const signum = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6, SIGKILL: 9, SIGSEGV: 11, SIGPIPE: 13, SIGTERM: 15 };
653
679
  settle(ok({
654
680
  stdout: markTruncated(so.text, so.droppedBytes),
655
- stderr: `${markTruncated(se.text, se.droppedBytes)}${se.text.endsWith("\n") || se.text === "" ? "" : "\n"}[process terminated by ${signal}]`,
681
+ stderr: markLiveSpoolRotations(`${markTruncated(se.text, se.droppedBytes)}${se.text.endsWith("\n") || se.text === "" ? "" : "\n"}[process terminated by ${signal}]`, liveRotationsTotal()),
656
682
  exitCode: 128 + (signum[signal] ?? 9),
657
683
  }));
658
684
  return;
659
685
  }
660
- settle(ok({ stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes), exitCode: code ?? 0 }));
686
+ settle(ok({ stdout: markTruncated(so.text, so.droppedBytes), stderr: markLiveSpoolRotations(markTruncated(se.text, se.droppedBytes), liveRotationsTotal()), exitCode: code ?? 0 }));
661
687
  };
662
688
  child.on("close", (code, signal) => settleFromTermination(code, signal));
663
689
  child.on("exit", (code, signal) => {
@@ -744,10 +770,116 @@ export class NodeExecutionEnv {
744
770
  }
745
771
  try {
746
772
  await mkdir(resolve(resolved, ".."), { recursive: true });
747
- const afterMkdirAbort = abortResult(abortSignal, resolved);
748
- if (afterMkdirAbort) {
749
- return afterMkdirAbort;
773
+ }
774
+ catch (error) {
775
+ return err(toFileError(error, resolved));
776
+ }
777
+ const afterMkdirAbort = abortResult(abortSignal, resolved);
778
+ if (afterMkdirAbort) {
779
+ return afterMkdirAbort;
780
+ }
781
+ let target = resolved;
782
+ let existingMode;
783
+ let st;
784
+ try {
785
+ st = await lstat(resolved);
786
+ }
787
+ catch {
788
+ st = undefined;
789
+ }
790
+ if (st !== undefined) {
791
+ if (st.isSymbolicLink()) {
792
+ let hop;
793
+ try {
794
+ const link = await readlink(resolved);
795
+ const parentReal = await realpath(resolve(resolved, "..")).catch(() => resolve(resolved, ".."));
796
+ hop = isAbsolute(link) ? link : resolve(parentReal, link);
797
+ }
798
+ catch {
799
+ hop = undefined;
800
+ }
801
+ if (hop === undefined)
802
+ return await this.writeFileInPlace(resolved, content, abortSignal);
803
+ let hopStat;
804
+ try {
805
+ hopStat = await lstat(hop);
806
+ }
807
+ catch {
808
+ hopStat = undefined;
809
+ }
810
+ if (hopStat !== undefined && !hopStat.isFile()) {
811
+ return await this.writeFileInPlace(resolved, content, abortSignal);
812
+ }
813
+ target = hop;
814
+ existingMode = hopStat !== undefined ? hopStat.mode & 0o7777 : undefined;
815
+ }
816
+ else if (!st.isFile()) {
817
+ return await this.writeFileInPlace(resolved, content, abortSignal);
750
818
  }
819
+ else {
820
+ existingMode = st.mode & 0o7777;
821
+ }
822
+ }
823
+ const tmp = `${target}.tmp.${process.pid}.${randomUUID().replace(/-/g, "").slice(0, 12)}`;
824
+ let tmpDurable = false;
825
+ try {
826
+ const handle = await open(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0));
827
+ try {
828
+ await handle.writeFile(content, { signal: abortSignal });
829
+ if (existingMode !== undefined) {
830
+ try {
831
+ await handle.chmod(existingMode);
832
+ }
833
+ catch (error) {
834
+ if (!isFsSupportError(error))
835
+ throw error;
836
+ }
837
+ }
838
+ try {
839
+ await handle.sync();
840
+ }
841
+ catch (error) {
842
+ if (!isFsSupportError(error))
843
+ throw error;
844
+ }
845
+ tmpDurable = true;
846
+ }
847
+ finally {
848
+ await handle.close().catch(() => { });
849
+ }
850
+ }
851
+ catch (error) {
852
+ await unlink(tmp).catch(() => { });
853
+ if (isNodeError(error) && error.code === "EACCES" && existingMode !== undefined) {
854
+ return await this.writeFileInPlace(resolved, content, abortSignal);
855
+ }
856
+ return err(toFileError(error, resolved));
857
+ }
858
+ const preRenameAbort = abortResult(abortSignal, resolved);
859
+ if (preRenameAbort) {
860
+ await unlink(tmp).catch(() => { });
861
+ return preRenameAbort;
862
+ }
863
+ try {
864
+ await rename(tmp, target);
865
+ return ok(undefined);
866
+ }
867
+ catch (error) {
868
+ const code = isNodeError(error) ? error.code : undefined;
869
+ if (tmpDurable && code !== undefined && RENAME_FALLBACK_CODES.has(code)) {
870
+ const inPlace = await this.writeFileInPlace(resolved, content, abortSignal);
871
+ if (inPlace.ok) {
872
+ await unlink(tmp).catch(() => { });
873
+ return inPlace;
874
+ }
875
+ return err(new FileError("unknown", `write to ${resolved} failed after the atomic rename was refused (${code}): ${inPlace.error.message}. The new content was preserved at ${tmp}`, resolved, inPlace.error));
876
+ }
877
+ await unlink(tmp).catch(() => { });
878
+ return err(toFileError(error, resolved));
879
+ }
880
+ }
881
+ async writeFileInPlace(resolved, content, abortSignal) {
882
+ try {
751
883
  await writeFile(resolved, content, { signal: abortSignal });
752
884
  return ok(undefined);
753
885
  }
@@ -913,12 +1045,13 @@ export class NodeExecutionEnv {
913
1045
  const shellId = `bg_${++this.bgCounter}_${randomUUID()}`;
914
1046
  const entry = {
915
1047
  child,
916
- stdout: { tail: new RollingTailBuffer(), totalBytes: 0, cursorBytes: 0 },
917
- stderr: { tail: new RollingTailBuffer(), totalBytes: 0, cursorBytes: 0 },
1048
+ stdout: { ...newStreamCursorState(), totalBytes: 0 },
1049
+ stderr: { ...newStreamCursorState(), totalBytes: 0 },
918
1050
  status: "running",
919
1051
  ...(handover !== undefined
920
1052
  ? { spool: { outPath: handover.spool.outPath, errPath: handover.spool.errPath, fileCursor: { ...handover.cursors }, ephemeral: true } }
921
1053
  : {}),
1054
+ ...(handover !== undefined && (handover.liveRotations ?? 0) > 0 ? { liveRotations: handover.liveRotations } : {}),
922
1055
  };
923
1056
  const seedOut = Buffer.from(stdoutSoFar.text, "utf8");
924
1057
  const seedErr = Buffer.from(stderrSoFar.text, "utf8");
@@ -928,15 +1061,19 @@ export class NodeExecutionEnv {
928
1061
  entry.stderr.tail.push(seedErr);
929
1062
  entry.stdout.totalBytes += stdoutSoFar.droppedBytes + seedOut.length;
930
1063
  entry.stderr.totalBytes += stderrSoFar.droppedBytes + seedErr.length;
1064
+ entry.stdout.acceptedBytes += seedOut.length;
1065
+ entry.stderr.acceptedBytes += seedErr.length;
931
1066
  child.stdout?.on("data", (chunk) => {
932
1067
  const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
933
1068
  entry.stdout.tail.push(buf);
934
1069
  entry.stdout.totalBytes += buf.length;
1070
+ entry.stdout.acceptedBytes += buf.length;
935
1071
  });
936
1072
  child.stderr?.on("data", (chunk) => {
937
1073
  const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
938
1074
  entry.stderr.tail.push(buf);
939
1075
  entry.stderr.totalBytes += buf.length;
1076
+ entry.stderr.acceptedBytes += buf.length;
940
1077
  });
941
1078
  child.on("error", () => {
942
1079
  if (entry.timer)
@@ -1040,8 +1177,8 @@ export class NodeExecutionEnv {
1040
1177
  const shellId = `bg_${++this.bgCounter}_${randomUUID()}`;
1041
1178
  const entry = {
1042
1179
  child,
1043
- stdout: { tail: new RollingTailBuffer(), totalBytes: 0, cursorBytes: 0 },
1044
- stderr: { tail: new RollingTailBuffer(), totalBytes: 0, cursorBytes: 0 },
1180
+ stdout: { ...newStreamCursorState(), totalBytes: 0 },
1181
+ stderr: { ...newStreamCursorState(), totalBytes: 0 },
1045
1182
  status: "running",
1046
1183
  ...(spool ? { spool } : {}),
1047
1184
  };
@@ -1051,11 +1188,13 @@ export class NodeExecutionEnv {
1051
1188
  const buf = Buffer.from(chunk, "utf8");
1052
1189
  entry.stdout.tail.push(buf);
1053
1190
  entry.stdout.totalBytes += buf.length;
1191
+ entry.stdout.acceptedBytes += buf.length;
1054
1192
  });
1055
1193
  child.stderr?.on("data", (chunk) => {
1056
1194
  const buf = Buffer.from(chunk, "utf8");
1057
1195
  entry.stderr.tail.push(buf);
1058
1196
  entry.stderr.totalBytes += buf.length;
1197
+ entry.stderr.acceptedBytes += buf.length;
1059
1198
  });
1060
1199
  child.stdout?.on("error", () => { });
1061
1200
  child.stderr?.on("error", () => { });
@@ -1098,21 +1237,18 @@ export class NodeExecutionEnv {
1098
1237
  return err(new BackgroundShellError("not_found", `Unknown background shell: ${shellId}`));
1099
1238
  }
1100
1239
  this.syncSpool(entry);
1101
- const slice = (s) => {
1102
- const { text, droppedBytes } = s.tail.result();
1103
- const droppedBeforeCursor = Math.max(0, droppedBytes - s.cursorBytes);
1104
- const startByte = Math.max(s.cursorBytes, droppedBytes);
1105
- const startInTail = startByte - droppedBytes;
1106
- const inc = startInTail <= 0 ? text : Buffer.from(text, "utf8").subarray(startInTail).toString("utf8");
1107
- s.cursorBytes = s.totalBytes;
1108
- return { inc, droppedBeforeCursor };
1109
- };
1110
- const out = slice(entry.stdout);
1111
- const er = slice(entry.stderr);
1240
+ const terminal = entry.status !== "running";
1241
+ const out = sliceStreamIncrement(entry.stdout, terminal);
1242
+ const er = sliceStreamIncrement(entry.stderr, terminal);
1112
1243
  const droppedBeforeCursor = out.droppedBeforeCursor + er.droppedBeforeCursor;
1244
+ let stderrInc = er.inc;
1245
+ if (terminal && (entry.liveRotations ?? 0) > 0 && entry.liveRotationsDisclosed !== true) {
1246
+ entry.liveRotationsDisclosed = true;
1247
+ stderrInc = markLiveSpoolRotations(er.inc, entry.liveRotations ?? 0);
1248
+ }
1113
1249
  return ok({
1114
1250
  stdout: out.inc,
1115
- stderr: er.inc,
1251
+ stderr: stderrInc,
1116
1252
  status: entry.status,
1117
1253
  ...(entry.exitCode !== undefined ? { exitCode: entry.exitCode } : {}),
1118
1254
  ...(droppedBeforeCursor > 0 ? { truncated: true, bytesDroppedBeforeCursor: droppedBeforeCursor } : {}),
@@ -1165,6 +1301,7 @@ export class NodeExecutionEnv {
1165
1301
  if (got > 0) {
1166
1302
  stream.tail.push(buf.subarray(0, got));
1167
1303
  stream.totalBytes += got;
1304
+ stream.acceptedBytes += got;
1168
1305
  spool.fileCursor[lane] += got;
1169
1306
  }
1170
1307
  }
@@ -1173,15 +1310,18 @@ export class NodeExecutionEnv {
1173
1310
  }
1174
1311
  }
1175
1312
  if (spool.ephemeral && spool.fileCursor[lane] >= EXEC_SPOOL_ROTATE_BYTES) {
1176
- const size2 = statSync(path).size;
1177
- if (size2 === spool.fileCursor[lane]) {
1178
- truncateSync(path, 0);
1179
- spool.fileCursor[lane] = 0;
1180
- }
1181
- else if (size2 >= EXEC_SPOOL_HARD_CAP_BYTES) {
1182
- const skipped = size2 - spool.fileCursor[lane];
1183
- stream.tail.recordSkippedBytes(skipped);
1184
- stream.totalBytes += skipped;
1313
+ const d = decideSpoolReclaim({
1314
+ size: statSync(path).size,
1315
+ cursor: spool.fileCursor[lane],
1316
+ writerLive: entry.status === "running" || (entry.child.exitCode === null && entry.child.signalCode === null),
1317
+ });
1318
+ if (d.reclaim) {
1319
+ if (d.skippedBytes > 0) {
1320
+ stream.tail.recordSkippedBytes(d.skippedBytes);
1321
+ stream.totalBytes += d.skippedBytes;
1322
+ }
1323
+ if (d.live)
1324
+ entry.liveRotations = (entry.liveRotations ?? 0) + 1;
1185
1325
  truncateSync(path, 0);
1186
1326
  spool.fileCursor[lane] = 0;
1187
1327
  }
@@ -1,2 +1,2 @@
1
1
  export declare function encodeFrame(json: string): Buffer;
2
- export declare function makeFrameDecoder(maxBytes?: number): (chunk: Buffer) => string[];
2
+ export declare function createFrameDecoder(maxBytes?: number): (chunk: Buffer) => string[];
@@ -2,7 +2,7 @@ export function encodeFrame(json) {
2
2
  const payload = Buffer.from(json, "utf8");
3
3
  return Buffer.concat([Buffer.from(`Content-Length: ${payload.length}\r\n\r\n`, "ascii"), payload]);
4
4
  }
5
- export function makeFrameDecoder(maxBytes = 50 * 1024 * 1024) {
5
+ export function createFrameDecoder(maxBytes = 50 * 1024 * 1024) {
6
6
  const HEADER_SLACK = 128;
7
7
  let buf = Buffer.alloc(0);
8
8
  return function decode(chunk) {
@@ -1,7 +1,7 @@
1
1
  import { type SessionWarmup } from "../../core/lsp-session.js";
2
2
  import { type LspChildProcess } from "./stdio-lsp-transport.js";
3
- import type { LspServerManager, LspSession, LspReadText } from "../../core/lsp.js";
4
- import type { ExecutionEnv } from "../../internal/harness.js";
3
+ import { type LspServerManager, type LspSession, type LspReadText } from "../../core/lsp.js";
4
+ import type { ExecutionEnv } from "../../internal/harness-types.js";
5
5
  export declare const DEFAULT_LSP_SERVERS: Readonly<Record<string, string>>;
6
6
  export type LspSpawn = (command: string, args: string[], cwd: string, signal?: AbortSignal) => Promise<LspChildProcess | undefined>;
7
7
  export interface NodeLspManagerOptions {
@@ -40,6 +40,8 @@ export declare class NodeLspManager implements LspServerManager {
40
40
  dispose(): Promise<void>;
41
41
  }
42
42
  export declare function languageFor(filePath: string): string | undefined;
43
+ export { MAX_LSP_FILE_BYTES } from "../../core/lsp.js";
44
+ export declare const defaultLspReadText: LspReadText;
43
45
  export declare function lspSpawnOptions(cwd: string): {
44
46
  cwd: string;
45
47
  stdio: ["pipe", "pipe", "ignore"];
@@ -1,12 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { killProcessTree } from "../execution-env/kill-tree.js";
3
- import { readFile } from "node:fs/promises";
4
- import { dirname, extname, resolve as resolvePath } from "node:path";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { basename, dirname, extname, resolve as resolvePath } from "node:path";
5
5
  import { 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
11
  export const DEFAULT_LSP_SERVERS = {
11
12
  typescript: "typescript-language-server --stdio",
12
13
  javascript: "typescript-language-server --stdio",
@@ -43,7 +44,7 @@ const CLIENT_CAPABILITIES = {
43
44
  definition: {}, references: {}, hover: {}, documentSymbol: {}, implementation: {}, callHierarchy: {},
44
45
  publishDiagnostics: { versionSupport: true },
45
46
  },
46
- workspace: { symbol: {} },
47
+ workspace: { symbol: {}, workspaceFolders: false },
47
48
  };
48
49
  export class NodeLspManager {
49
50
  servers;
@@ -62,7 +63,7 @@ export class NodeLspManager {
62
63
  constructor(opts = {}) {
63
64
  this.servers = { ...DEFAULT_LSP_SERVERS, ...opts.servers };
64
65
  this.spawnFn = opts.spawn ?? defaultLspSpawn;
65
- this.readTextFn = opts.readText ?? ((fp) => readFile(fp, "utf8"));
66
+ this.readTextFn = opts.readText ?? defaultLspReadText;
66
67
  this.resolveRootFn = opts.resolveRoot ?? defaultResolveRoot;
67
68
  this.log = opts.log ?? (() => { });
68
69
  this.warmup = opts.warmup;
@@ -175,7 +176,14 @@ export class NodeLspManager {
175
176
  }
176
177
  const transport = new StdioLspTransport(child);
177
178
  try {
178
- await transport.request("initialize", { processId: null, rootUri: pathToUri(root), capabilities: CLIENT_CAPABILITIES }, signal);
179
+ const rootUri = pathToUri(root);
180
+ await transport.request("initialize", {
181
+ processId: null,
182
+ rootPath: root,
183
+ rootUri,
184
+ workspaceFolders: [{ uri: rootUri, name: basename(root) || root }],
185
+ capabilities: CLIENT_CAPABILITIES,
186
+ }, signal);
179
187
  transport.notify("initialized", {});
180
188
  }
181
189
  catch (e) {
@@ -218,6 +226,15 @@ function defaultResolveRoot(filePath, env) {
218
226
  }
219
227
  return dirname(resolvePath(filePath));
220
228
  }
229
+ export { MAX_LSP_FILE_BYTES } from "../../core/lsp.js";
230
+ export const defaultLspReadText = async (fp, signal) => {
231
+ const st = await stat(fp);
232
+ if (!st.isFile())
233
+ throw new Error(`not a regular file: ${fp}`);
234
+ if (st.size > MAX_LSP_FILE_BYTES)
235
+ throw new Error(`file too large for LSP analysis (${st.size} bytes exceeds ${MAX_LSP_FILE_BYTES})`);
236
+ return readFile(fp, { encoding: "utf8", signal });
237
+ };
221
238
  export function lspSpawnOptions(cwd) {
222
239
  return process.platform === "win32"
223
240
  ? { cwd, stdio: ["pipe", "pipe", "ignore"], shell: true }
@@ -1,4 +1,4 @@
1
- import type { LspTransport } from "../../core/lsp.js";
1
+ import { type LspTransport } from "../../core/lsp.js";
2
2
  export interface LspChildProcess {
3
3
  stdin: {
4
4
  write(data: Buffer): void;
@@ -1,12 +1,13 @@
1
- import { encodeFrame, makeFrameDecoder } from "./frame-decoder.js";
1
+ import { encodeFrame, createFrameDecoder } from "./frame-decoder.js";
2
2
  import { killProcessTree } from "../execution-env/kill-tree.js";
3
+ import { brandLspFailure } from "../../core/lsp.js";
3
4
  export class StdioLspTransport {
4
5
  child;
5
6
  requestTimeoutMs;
6
7
  nextId = 0;
7
8
  pending = new Map();
8
9
  isClosed = false;
9
- decode = makeFrameDecoder();
10
+ decode = createFrameDecoder();
10
11
  notificationHandlers = new Set();
11
12
  constructor(child, requestTimeoutMs = 30_000) {
12
13
  this.child = child;
@@ -44,8 +45,10 @@ export class StdioLspTransport {
44
45
  const p = this.pending.get(msg.id);
45
46
  if (p) {
46
47
  this.pending.delete(msg.id);
47
- if (msg.error)
48
- p.reject(new Error(msg.error.message ?? "LSP error"));
48
+ if (msg.error) {
49
+ const message = msg.error.message ?? "LSP error";
50
+ p.reject(brandLspFailure(new Error(message), jsonRpcReason(msg.error.code), message));
51
+ }
49
52
  else
50
53
  p.resolve(msg.result);
51
54
  }
@@ -68,6 +71,7 @@ export class StdioLspTransport {
68
71
  }
69
72
  failAll(err) {
70
73
  this.isClosed = true;
74
+ brandLspFailure(err, "server_terminated");
71
75
  for (const p of this.pending.values())
72
76
  p.reject(err);
73
77
  this.pending.clear();
@@ -77,13 +81,13 @@ export class StdioLspTransport {
77
81
  }
78
82
  request(method, params, signal) {
79
83
  if (this.isClosed)
80
- return Promise.reject(new Error("LSP transport closed"));
84
+ return Promise.reject(brandLspFailure(new Error("LSP transport closed"), "server_terminated"));
81
85
  const id = ++this.nextId;
82
86
  return new Promise((resolve, reject) => {
83
87
  const timer = setTimeout(() => {
84
88
  if (this.pending.delete(id)) {
85
89
  signal?.removeEventListener("abort", onAbort);
86
- reject(new Error(`LSP ${method} timed out after ${this.requestTimeoutMs}ms`));
90
+ reject(brandLspFailure(new Error(`LSP ${method} timed out after ${this.requestTimeoutMs}ms`), "timeout"));
87
91
  }
88
92
  }, this.requestTimeoutMs);
89
93
  timer.unref?.();
@@ -95,7 +99,7 @@ export class StdioLspTransport {
95
99
  if (this.pending.delete(id)) {
96
100
  cleanup();
97
101
  this.notify("$/cancelRequest", { id });
98
- reject(new Error(`LSP ${method} aborted`));
102
+ reject(brandLspFailure(new Error(`LSP ${method} aborted`), "cancelled"));
99
103
  }
100
104
  };
101
105
  this.pending.set(id, {
@@ -119,7 +123,7 @@ export class StdioLspTransport {
119
123
  catch (e) {
120
124
  if (this.pending.delete(id)) {
121
125
  cleanup();
122
- reject(e instanceof Error ? e : new Error(String(e)));
126
+ reject(brandLspFailure(e instanceof Error ? e : new Error(String(e)), "server_terminated"));
123
127
  }
124
128
  }
125
129
  });
@@ -145,6 +149,13 @@ export class StdioLspTransport {
145
149
  }
146
150
  }
147
151
  }
152
+ function jsonRpcReason(code) {
153
+ if (code === -32601)
154
+ return "unsupported_operation";
155
+ if (code === -32800 || code === -32802)
156
+ return "cancelled";
157
+ return "server_error";
158
+ }
148
159
  function toError(e) {
149
160
  if (e instanceof Error)
150
161
  return e;