flavor-code 1.4.0-beta.1 → 1.4.0-beta.4

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 (159) hide show
  1. package/dist/agent/loop.d.ts +7 -2
  2. package/dist/{app-C5ZQUOKJ.js → app-UWIEIWBQ.js} +7 -7
  3. package/dist/{chunk-FOTMP4ZS.js → chunk-4RR5VVX4.js} +8 -2
  4. package/dist/{chunk-6MNOZ2TX.js → chunk-DCNUBQCH.js} +1376 -641
  5. package/dist/{chunk-ODRWR4KK.js → chunk-IQAZ42TM.js} +1 -1
  6. package/dist/{chunk-URUVFECH.js → chunk-KQMKY7C5.js} +8 -4
  7. package/dist/chunk-LKQTBL3R.js +107 -0
  8. package/dist/{chunk-3WCDZ6ZL.js → chunk-ZJCAQXA7.js} +2 -1
  9. package/dist/{claude-ink-LP32NSX5.js → claude-ink-ECZ6QF3P.js} +2 -2
  10. package/dist/cli-main.js +22 -15
  11. package/dist/cli.js +24 -5
  12. package/dist/config/schema.d.ts +4 -0
  13. package/dist/context/manager.d.ts +9 -0
  14. package/dist/desktop/main.js +1770 -949
  15. package/dist/desktop-renderer/assets/{index-CqRIr-hz.js → index-C0t1nGih.js} +2 -2
  16. package/dist/desktop-renderer/assets/{interactive-terminal-C4o7MNPO.js → interactive-terminal-BJqe96mZ.js} +1 -1
  17. package/dist/desktop-renderer/index.html +1 -1
  18. package/dist/git/service.d.ts +3 -3
  19. package/dist/git/tools.d.ts +1 -1
  20. package/dist/goal/classifier.d.ts +2 -0
  21. package/dist/goal/orchestrator.d.ts +18 -0
  22. package/dist/goal/planner.d.ts +2 -0
  23. package/dist/goal/store.d.ts +2 -0
  24. package/dist/goal/types.d.ts +8 -0
  25. package/dist/harness/journal.d.ts +2 -0
  26. package/dist/harness/local.d.ts +2 -0
  27. package/dist/init/project.d.ts +0 -6
  28. package/dist/jobs/registry.d.ts +7 -1
  29. package/dist/{load-V2UIYYQI.js → load-F4AYLCF6.js} +1 -1
  30. package/dist/loop/orchestrator.d.ts +18 -0
  31. package/dist/loop/types.d.ts +7 -0
  32. package/dist/production.d.ts +17 -0
  33. package/dist/sdk/index.js +5 -5
  34. package/dist/session/tree.d.ts +2 -0
  35. package/dist/tools/lsp.d.ts +13 -10
  36. package/dist/tools/runtime.d.ts +2 -1
  37. package/dist/tools/search.d.ts +1 -0
  38. package/dist/tools/types.d.ts +1 -1
  39. package/dist/ui/session.d.ts +9 -0
  40. package/dist/utils/abort.d.ts +4 -0
  41. package/dist/utils/execFileNoThrow.d.ts +1 -0
  42. package/dist/utils/heap.d.ts +58 -0
  43. package/dist/utils/memory-restart.d.ts +38 -3
  44. package/dist/utils/process-tree.d.ts +2 -0
  45. package/dist/utils/rotation-census.d.ts +36 -0
  46. package/package.json +2 -2
  47. package//346/212/200/346/234/257/346/226/271/346/241/210/346/212/245/345/221/212.md +7 -7
  48. package/dist/astgraph/db.mjs +0 -237
  49. package/dist/astgraph/extract.mjs +0 -239
  50. package/dist/astgraph/flavor-plugin.json +0 -25
  51. package/dist/astgraph/grammars.mjs +0 -68
  52. package/dist/astgraph/index.js +0 -301
  53. package/dist/astgraph/indexer.mjs +0 -175
  54. package/dist/astgraph/query.mjs +0 -165
  55. package/dist/astgraph/resolve.mjs +0 -93
  56. package/dist/astgraph/vendor/tree-sitter-javascript.wasm +0 -0
  57. package/dist/astgraph/vendor/tree-sitter-tsx.wasm +0 -0
  58. package/dist/astgraph/vendor/tree-sitter-typescript.wasm +0 -0
  59. package/dist/astgraph/vendor/tree-sitter.js +0 -3980
  60. package/dist/astgraph/vendor/tree-sitter.wasm +0 -0
  61. package/dist/astgraph/vendor/zod/LICENSE +0 -21
  62. package/dist/astgraph/vendor/zod/index.js +0 -4
  63. package/dist/astgraph/vendor/zod/locales/index.js +0 -1
  64. package/dist/astgraph/vendor/zod/locales/package.json +0 -7
  65. package/dist/astgraph/vendor/zod/package.json +0 -135
  66. package/dist/astgraph/vendor/zod/v4/classic/checks.js +0 -1
  67. package/dist/astgraph/vendor/zod/v4/classic/coerce.js +0 -17
  68. package/dist/astgraph/vendor/zod/v4/classic/compat.js +0 -31
  69. package/dist/astgraph/vendor/zod/v4/classic/errors.js +0 -48
  70. package/dist/astgraph/vendor/zod/v4/classic/external.js +0 -20
  71. package/dist/astgraph/vendor/zod/v4/classic/from-json-schema.js +0 -599
  72. package/dist/astgraph/vendor/zod/v4/classic/index.js +0 -4
  73. package/dist/astgraph/vendor/zod/v4/classic/iso.js +0 -30
  74. package/dist/astgraph/vendor/zod/v4/classic/package.json +0 -7
  75. package/dist/astgraph/vendor/zod/v4/classic/parse.js +0 -15
  76. package/dist/astgraph/vendor/zod/v4/classic/schemas.js +0 -1395
  77. package/dist/astgraph/vendor/zod/v4/core/api.js +0 -1087
  78. package/dist/astgraph/vendor/zod/v4/core/checks.js +0 -575
  79. package/dist/astgraph/vendor/zod/v4/core/core.js +0 -78
  80. package/dist/astgraph/vendor/zod/v4/core/doc.js +0 -35
  81. package/dist/astgraph/vendor/zod/v4/core/errors.js +0 -185
  82. package/dist/astgraph/vendor/zod/v4/core/index.js +0 -16
  83. package/dist/astgraph/vendor/zod/v4/core/json-schema-generator.js +0 -95
  84. package/dist/astgraph/vendor/zod/v4/core/json-schema-processors.js +0 -601
  85. package/dist/astgraph/vendor/zod/v4/core/json-schema.js +0 -1
  86. package/dist/astgraph/vendor/zod/v4/core/package.json +0 -7
  87. package/dist/astgraph/vendor/zod/v4/core/parse.js +0 -93
  88. package/dist/astgraph/vendor/zod/v4/core/regexes.js +0 -139
  89. package/dist/astgraph/vendor/zod/v4/core/registries.js +0 -51
  90. package/dist/astgraph/vendor/zod/v4/core/schemas.js +0 -2239
  91. package/dist/astgraph/vendor/zod/v4/core/standard-schema.js +0 -1
  92. package/dist/astgraph/vendor/zod/v4/core/to-json-schema.js +0 -448
  93. package/dist/astgraph/vendor/zod/v4/core/util.js +0 -674
  94. package/dist/astgraph/vendor/zod/v4/core/versions.js +0 -5
  95. package/dist/astgraph/vendor/zod/v4/index.js +0 -3
  96. package/dist/astgraph/vendor/zod/v4/locales/ar.js +0 -106
  97. package/dist/astgraph/vendor/zod/v4/locales/az.js +0 -105
  98. package/dist/astgraph/vendor/zod/v4/locales/be.js +0 -156
  99. package/dist/astgraph/vendor/zod/v4/locales/bg.js +0 -120
  100. package/dist/astgraph/vendor/zod/v4/locales/ca.js +0 -107
  101. package/dist/astgraph/vendor/zod/v4/locales/cs.js +0 -111
  102. package/dist/astgraph/vendor/zod/v4/locales/da.js +0 -115
  103. package/dist/astgraph/vendor/zod/v4/locales/de.js +0 -108
  104. package/dist/astgraph/vendor/zod/v4/locales/el.js +0 -109
  105. package/dist/astgraph/vendor/zod/v4/locales/en.js +0 -113
  106. package/dist/astgraph/vendor/zod/v4/locales/eo.js +0 -109
  107. package/dist/astgraph/vendor/zod/v4/locales/es.js +0 -132
  108. package/dist/astgraph/vendor/zod/v4/locales/fa.js +0 -114
  109. package/dist/astgraph/vendor/zod/v4/locales/fi.js +0 -112
  110. package/dist/astgraph/vendor/zod/v4/locales/fr-CA.js +0 -107
  111. package/dist/astgraph/vendor/zod/v4/locales/fr.js +0 -125
  112. package/dist/astgraph/vendor/zod/v4/locales/he.js +0 -214
  113. package/dist/astgraph/vendor/zod/v4/locales/hr.js +0 -122
  114. package/dist/astgraph/vendor/zod/v4/locales/hu.js +0 -108
  115. package/dist/astgraph/vendor/zod/v4/locales/hy.js +0 -147
  116. package/dist/astgraph/vendor/zod/v4/locales/id.js +0 -106
  117. package/dist/astgraph/vendor/zod/v4/locales/index.js +0 -52
  118. package/dist/astgraph/vendor/zod/v4/locales/is.js +0 -109
  119. package/dist/astgraph/vendor/zod/v4/locales/it.js +0 -108
  120. package/dist/astgraph/vendor/zod/v4/locales/ja.js +0 -107
  121. package/dist/astgraph/vendor/zod/v4/locales/ka.js +0 -112
  122. package/dist/astgraph/vendor/zod/v4/locales/kh.js +0 -5
  123. package/dist/astgraph/vendor/zod/v4/locales/km.js +0 -110
  124. package/dist/astgraph/vendor/zod/v4/locales/ko.js +0 -111
  125. package/dist/astgraph/vendor/zod/v4/locales/lt.js +0 -203
  126. package/dist/astgraph/vendor/zod/v4/locales/mk.js +0 -109
  127. package/dist/astgraph/vendor/zod/v4/locales/ms.js +0 -107
  128. package/dist/astgraph/vendor/zod/v4/locales/nl.js +0 -110
  129. package/dist/astgraph/vendor/zod/v4/locales/no.js +0 -108
  130. package/dist/astgraph/vendor/zod/v4/locales/ota.js +0 -109
  131. package/dist/astgraph/vendor/zod/v4/locales/package.json +0 -7
  132. package/dist/astgraph/vendor/zod/v4/locales/pl.js +0 -109
  133. package/dist/astgraph/vendor/zod/v4/locales/ps.js +0 -114
  134. package/dist/astgraph/vendor/zod/v4/locales/pt.js +0 -108
  135. package/dist/astgraph/vendor/zod/v4/locales/ro.js +0 -119
  136. package/dist/astgraph/vendor/zod/v4/locales/ru.js +0 -156
  137. package/dist/astgraph/vendor/zod/v4/locales/sl.js +0 -109
  138. package/dist/astgraph/vendor/zod/v4/locales/sv.js +0 -110
  139. package/dist/astgraph/vendor/zod/v4/locales/ta.js +0 -110
  140. package/dist/astgraph/vendor/zod/v4/locales/th.js +0 -110
  141. package/dist/astgraph/vendor/zod/v4/locales/tr.js +0 -105
  142. package/dist/astgraph/vendor/zod/v4/locales/ua.js +0 -5
  143. package/dist/astgraph/vendor/zod/v4/locales/uk.js +0 -108
  144. package/dist/astgraph/vendor/zod/v4/locales/ur.js +0 -110
  145. package/dist/astgraph/vendor/zod/v4/locales/uz.js +0 -110
  146. package/dist/astgraph/vendor/zod/v4/locales/vi.js +0 -108
  147. package/dist/astgraph/vendor/zod/v4/locales/yo.js +0 -107
  148. package/dist/astgraph/vendor/zod/v4/locales/zh-CN.js +0 -109
  149. package/dist/astgraph/vendor/zod/v4/locales/zh-TW.js +0 -107
  150. package/dist/astgraph/vendor/zod/v4/mini/checks.js +0 -1
  151. package/dist/astgraph/vendor/zod/v4/mini/coerce.js +0 -22
  152. package/dist/astgraph/vendor/zod/v4/mini/external.js +0 -14
  153. package/dist/astgraph/vendor/zod/v4/mini/index.js +0 -3
  154. package/dist/astgraph/vendor/zod/v4/mini/iso.js +0 -34
  155. package/dist/astgraph/vendor/zod/v4/mini/package.json +0 -7
  156. package/dist/astgraph/vendor/zod/v4/mini/parse.js +0 -1
  157. package/dist/astgraph/vendor/zod/v4/mini/schemas.js +0 -961
  158. package/dist/astgraph/vendor/zod/v4/package.json +0 -7
  159. package/dist/chunk-CIVPKHF4.js +0 -53
@@ -2,10 +2,15 @@
2
2
  import { createRequire } from "node:module"; const require = createRequire(import.meta.url);
3
3
  import {
4
4
  MEMORY_RESTART_EXIT_CODE,
5
+ consumedMemoryRestartMarker,
6
+ markMemoryRotation,
5
7
  memoryRestartMarkerPath,
8
+ memoryRotationActive,
6
9
  nextMemoryRestartMarker,
7
- parseMemoryRestartMarker
8
- } from "./chunk-CIVPKHF4.js";
10
+ parseMemoryRestartMarker,
11
+ pendingContinuation,
12
+ rotationCooldownActive
13
+ } from "./chunk-LKQTBL3R.js";
9
14
  import {
10
15
  PERMISSION_MODES,
11
16
  PermissionModeSchema,
@@ -18,7 +23,7 @@ import {
18
23
  redactConfig,
19
24
  setProjectMcpServerDisabled,
20
25
  updateProtectedFile
21
- } from "./chunk-URUVFECH.js";
26
+ } from "./chunk-KQMKY7C5.js";
22
27
  import {
23
28
  AuditLogger,
24
29
  appendUsageLog,
@@ -32,7 +37,7 @@ import {
32
37
  resolveLanguage,
33
38
  setUsageSession,
34
39
  usageLogPath
35
- } from "./chunk-3WCDZ6ZL.js";
40
+ } from "./chunk-ZJCAQXA7.js";
36
41
 
37
42
  // src/models/types.ts
38
43
  function modelContentText(content) {
@@ -75,12 +80,12 @@ function normalizeProviderError(error) {
75
80
 
76
81
  // src/utils/async.ts
77
82
  function withTimeout(promise, ms) {
78
- return new Promise((resolve31, reject) => {
83
+ return new Promise((resolve32, reject) => {
79
84
  let settled = false;
80
85
  const timer = setTimeout(() => {
81
86
  if (settled) return;
82
87
  settled = true;
83
- resolve31({ timedOut: true, value: void 0 });
88
+ resolve32({ timedOut: true, value: void 0 });
84
89
  }, ms);
85
90
  timer.unref?.();
86
91
  promise.then(
@@ -88,7 +93,7 @@ function withTimeout(promise, ms) {
88
93
  if (!settled) {
89
94
  settled = true;
90
95
  clearTimeout(timer);
91
- resolve31({ timedOut: false, value });
96
+ resolve32({ timedOut: false, value });
92
97
  }
93
98
  },
94
99
  (error) => {
@@ -103,13 +108,13 @@ function withTimeout(promise, ms) {
103
108
  }
104
109
  function awaitWithSignal(promise, signal) {
105
110
  signal.throwIfAborted();
106
- return new Promise((resolve31, reject) => {
111
+ return new Promise((resolve32, reject) => {
107
112
  const onAbort = () => reject(signal.reason);
108
113
  signal.addEventListener("abort", onAbort, { once: true });
109
114
  promise.then(
110
115
  (value) => {
111
116
  signal.removeEventListener("abort", onAbort);
112
- resolve31(value);
117
+ resolve32(value);
113
118
  },
114
119
  (error) => {
115
120
  signal.removeEventListener("abort", onAbort);
@@ -572,9 +577,7 @@ function redactSecrets(input, secrets) {
572
577
 
573
578
  // src/init/project.ts
574
579
  import { lstat, mkdir, readdir, readFile, realpath, writeFile } from "fs/promises";
575
- import { copyFileSync, mkdirSync, readdirSync } from "fs";
576
- import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join, relative as relative2, sep as sep2 } from "path";
577
- import { fileURLToPath } from "url";
580
+ import { basename, isAbsolute as isAbsolute2, join, relative as relative2, sep as sep2 } from "path";
578
581
  var GENERATED_START = "<!-- flavor-code:start -->";
579
582
  var GENERATED_END = "<!-- flavor-code:end -->";
580
583
  var MAX_MANIFEST_BYTES = 1e6;
@@ -1025,29 +1028,8 @@ async function createExampleFlavorConfig(cwd) {
1025
1028
  };
1026
1029
  await writeFile(configPath, JSON.stringify(example, null, 2) + "\n");
1027
1030
  }
1028
- var ASTGRAPH_SRC_DIR = join(dirname2(fileURLToPath(import.meta.url)), "astgraph");
1029
- async function copyAstgraphPlugin(cwd) {
1030
- const pluginDir = join(cwd, ".flavor", "plugins", "astgraph");
1031
- try {
1032
- await lstat(pluginDir);
1033
- return;
1034
- } catch (error) {
1035
- if (!isMissingPathError(error)) throw error;
1036
- }
1037
- copyDirectory(ASTGRAPH_SRC_DIR, pluginDir);
1038
- }
1039
- function copyDirectory(from, to) {
1040
- mkdirSync(to, { recursive: true, mode: 448 });
1041
- for (const entry2 of readdirSync(from, { withFileTypes: true })) {
1042
- const source = join(from, entry2.name);
1043
- const target = join(to, entry2.name);
1044
- if (entry2.isDirectory()) copyDirectory(source, target);
1045
- else if (entry2.isFile()) copyFileSync(source, target);
1046
- }
1047
- }
1048
1031
  async function initializeFlavor(cwd) {
1049
1032
  await ensureFlavorDirectories(cwd);
1050
- await copyAstgraphPlugin(cwd);
1051
1033
  await createExampleFlavorConfig(cwd);
1052
1034
  const facts = await inspectProject(cwd);
1053
1035
  const path = join(cwd, "FLAVOR.md");
@@ -1069,12 +1051,12 @@ async function initializeFlavor(cwd) {
1069
1051
 
1070
1052
  // src/utils/version.ts
1071
1053
  import { readFileSync } from "fs";
1072
- import { dirname as dirname3, join as join2 } from "path";
1073
- import { fileURLToPath as fileURLToPath2 } from "url";
1054
+ import { dirname as dirname2, join as join2 } from "path";
1055
+ import { fileURLToPath } from "url";
1074
1056
  var cached;
1075
1057
  function packageVersion() {
1076
1058
  if (cached !== void 0) return cached;
1077
- let directory = dirname3(fileURLToPath2(import.meta.url));
1059
+ let directory = dirname2(fileURLToPath(import.meta.url));
1078
1060
  for (; ; ) {
1079
1061
  try {
1080
1062
  const manifest = JSON.parse(readFileSync(join2(directory, "package.json"), "utf8"));
@@ -1084,7 +1066,7 @@ function packageVersion() {
1084
1066
  }
1085
1067
  } catch {
1086
1068
  }
1087
- const parent = dirname3(directory);
1069
+ const parent = dirname2(directory);
1088
1070
  if (parent === directory) break;
1089
1071
  directory = parent;
1090
1072
  }
@@ -1879,7 +1861,7 @@ function addText(turn, text, onNewLine = false) {
1879
1861
  import { createHash, randomUUID } from "crypto";
1880
1862
  import { execFile } from "child_process";
1881
1863
  import { lstat as lstat2, mkdir as mkdir2, open, readFile as readFile2, readdir as readdir2, rename, rm, writeFile as writeFile2 } from "fs/promises";
1882
- import { dirname as dirname4, join as join3, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
1864
+ import { dirname as dirname3, join as join3, relative as relative3, resolve as resolve2, sep as sep3 } from "path";
1883
1865
  var EXCLUDED = /* @__PURE__ */ new Set([".git", ".flavor", "node_modules", "dist", "build", "release", ".worktrees"]);
1884
1866
  var MAX_FILE_BYTES = 16 * 1024 * 1024;
1885
1867
  var MAX_TOTAL_BYTES = 128 * 1024 * 1024;
@@ -1965,7 +1947,7 @@ var WorkspaceCheckpointStore = class {
1965
1947
  for (const file of checkpoint.files) {
1966
1948
  const target = contained(this.#workspace, file.path);
1967
1949
  const object = objects.get(file.digest);
1968
- await mkdir2(dirname4(target), { recursive: true });
1950
+ await mkdir2(dirname3(target), { recursive: true });
1969
1951
  const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
1970
1952
  await writeFile2(temporary, object, { mode: file.mode });
1971
1953
  await rename(temporary, target);
@@ -2037,7 +2019,7 @@ function contained(workspace, path) {
2037
2019
  return absolute;
2038
2020
  }
2039
2021
  async function atomicJson(path, value) {
2040
- await mkdir2(dirname4(path), { recursive: true });
2022
+ await mkdir2(dirname3(path), { recursive: true });
2041
2023
  const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
2042
2024
  await writeFile2(temporary, `${JSON.stringify(value)}
2043
2025
  `, { encoding: "utf8", mode: 384 });
@@ -2050,7 +2032,7 @@ function isCode(error, code) {
2050
2032
  // src/session/tree.ts
2051
2033
  import { randomUUID as randomUUID2 } from "crypto";
2052
2034
  import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
2053
- import { dirname as dirname5, join as join4, resolve as resolve3 } from "path";
2035
+ import { dirname as dirname4, join as join4, resolve as resolve3 } from "path";
2054
2036
  var MAX_SESSION_HISTORY_NODES = 100;
2055
2037
  var MAX_SESSION_HISTORY_CONTEXT_CHARS = 2e6;
2056
2038
  var SessionHistory = class _SessionHistory {
@@ -2084,6 +2066,10 @@ var SessionHistory = class _SessionHistory {
2084
2066
  get leafId() {
2085
2067
  return this.#leafId;
2086
2068
  }
2069
+ /** Cheap retained-node count for the rotation census (no cloning). */
2070
+ get nodeCount() {
2071
+ return this.#nodes.length;
2072
+ }
2087
2073
  tree() {
2088
2074
  return this.#nodes.map((node) => structuredClone(node));
2089
2075
  }
@@ -2155,7 +2141,7 @@ var SessionHistory = class _SessionHistory {
2155
2141
  leafId: this.#leafId,
2156
2142
  nodes: this.#nodes
2157
2143
  };
2158
- await mkdir3(dirname5(this.#path), { recursive: true });
2144
+ await mkdir3(dirname4(this.#path), { recursive: true });
2159
2145
  const temporary = `${this.#path}.${process.pid}.${randomUUID2()}.tmp`;
2160
2146
  await writeFile3(temporary, `${JSON.stringify(document)}
2161
2147
  `, { encoding: "utf8", mode: 384 });
@@ -2201,7 +2187,7 @@ function isCode2(error, code) {
2201
2187
  // src/tools/search.ts
2202
2188
  import { spawn } from "child_process";
2203
2189
  import { open as open2, opendir, stat } from "fs/promises";
2204
- import { dirname as dirname6, isAbsolute as isAbsolute3, relative as relative4, resolve as resolve4, sep as sep4 } from "path";
2190
+ import { isAbsolute as isAbsolute3, relative as relative4, resolve as resolve4, sep as sep4 } from "path";
2205
2191
  import { rgPath as bundledRgPath } from "@vscode/ripgrep";
2206
2192
  import createIgnore from "ignore";
2207
2193
  import { z } from "zod";
@@ -2292,8 +2278,9 @@ var GlobInput = z.object({
2292
2278
  limit: z.coerce.number().int().positive().max(1e5).nullable().optional()
2293
2279
  });
2294
2280
  var GrepInput = z.object({
2295
- pattern: z.string().min(1),
2296
- path: z.string().min(1).describe('Workspace-relative search path. Use JSON null, not the string "null", for the workspace root.').nullable().optional(),
2281
+ pattern: z.string().min(1).describe("Regular expression without / delimiters, or literal text when fixedStrings=true."),
2282
+ fixedStrings: BooleanInput.describe("Match literal text, useful for Markdown links and paths containing regex punctuation.").nullable().optional(),
2283
+ path: z.string().min(1).describe("Workspace-relative file or directory. A file restricts the search to that file. Use glob for multiple filenames and JSON null for the workspace root.").nullable().optional(),
2297
2284
  glob: z.string().min(1).nullable().optional(),
2298
2285
  fileType: z.enum(FILE_TYPES).nullable().optional(),
2299
2286
  includeIgnored: BooleanInput.describe("Include files excluded by .gitignore or .ignore. Git metadata remains excluded.").nullable().optional(),
@@ -2342,7 +2329,7 @@ function createGrepTool(workspace, options = {}) {
2342
2329
  const resources = searchResources(options);
2343
2330
  return {
2344
2331
  name: "Grep",
2345
- description: `Search workspace text with a regular expression. fileType must be one of: ${FILE_TYPES.join(", ")}.`,
2332
+ description: `Search workspace text with a regular expression or fixedStrings=true for literal text. Zero matches is a successful search, not a path error. fileType must be one of: ${FILE_TYPES.join(", ")}.`,
2346
2333
  inputSchema: GrepInput,
2347
2334
  paths: (input) => [scope(root, searchPath(input.path))],
2348
2335
  summarize: (input) => {
@@ -2350,10 +2337,10 @@ function createGrepTool(workspace, options = {}) {
2350
2337
  const where = path ? ` in ${path}` : "";
2351
2338
  const kind = input.fileType ? ` [${input.fileType}]` : "";
2352
2339
  const ignored2 = input.includeIgnored === true ? " (including ignored files)" : "";
2353
- return `pattern: /${input.pattern}/${kind}${where}${ignored2}`;
2340
+ return `pattern: ${input.fixedStrings === true ? JSON.stringify(input.pattern) : `/${input.pattern}/`}${kind}${where}${ignored2}`;
2354
2341
  },
2355
2342
  execute: async (input, signal) => {
2356
- const expression = new RegExp(input.pattern);
2343
+ const expression = new RegExp(input.fixedStrings === true ? input.pattern.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&") : input.pattern);
2357
2344
  const limit = input.limit ?? options.defaultLimit ?? DEFAULT_RESULT_LIMIT;
2358
2345
  const context = input.context ?? 0;
2359
2346
  const start = await resolveGrepPath(root, searchPath(input.path));
@@ -2404,10 +2391,12 @@ async function rgGrep(root, start, input, context, signal, executable, argsPrefi
2404
2391
  }
2405
2392
  const args = ["--json", "--line-number", "--column", "--sort", "path", "--hidden", "--glob", "!.git", "--glob", "!.git/**", "--max-filesize", String(maxFileBytes), "--context", String(context)];
2406
2393
  if (input.includeIgnored === true) args.push("--no-ignore");
2394
+ if (input.fixedStrings === true) args.push("--fixed-strings");
2407
2395
  if (input.glob !== void 0 && input.glob !== null) args.push("--glob", input.glob);
2408
2396
  else for (const extension of typeExtensions ?? []) args.push("--glob", `*${extension}`);
2409
2397
  args.push("--regexp", input.pattern, "--", relative4(root, start) || ".");
2410
2398
  const matches = [];
2399
+ const matcher = input.glob == null ? void 0 : globRegex(input.glob);
2411
2400
  const ignoreLayers = input.includeIgnored === true ? [] : await collectIgnoreLayers(root, start, signal, resources, ignoreBudget);
2412
2401
  const parser = delimitedParser(10, (record) => {
2413
2402
  if (record.length === 0) return true;
@@ -2420,6 +2409,7 @@ async function rgGrep(root, start, input, context, signal, executable, argsPrefi
2420
2409
  const lineWithEnding = tryDecodeUtf8(lineValue);
2421
2410
  if (decodedPath === void 0 || lineWithEnding === void 0) return true;
2422
2411
  const path = normalizePath(decodedPath).replace(/^\.\//, "");
2412
+ if (matcher !== void 0 && !matcher.test(path)) return true;
2423
2413
  if (typeExtensions !== void 0 && !typeExtensions.some((extension) => path.endsWith(extension))) return true;
2424
2414
  if (ignored(path, false, ignoreLayers)) return true;
2425
2415
  const text = lineWithEnding.replace(/\r?\n$/, "");
@@ -2544,7 +2534,12 @@ async function nodeFiles(root, start, signal, accept, limit, resources, ignoreBu
2544
2534
  }
2545
2535
  ignores.length = ruleCount;
2546
2536
  }
2547
- await walk(start);
2537
+ if ((await stat(start)).isFile()) {
2538
+ const path = normalizedRelative(root, start);
2539
+ if (!isGitMetadataPath(root, start) && (includeIgnored || !ignored(path, false, ignores)) && accept(path)) output.push(path);
2540
+ } else {
2541
+ await walk(start);
2542
+ }
2548
2543
  return output;
2549
2544
  }
2550
2545
  async function collectIgnoreLayers(root, start, signal, resources, ignoreBudget) {
@@ -2574,7 +2569,7 @@ async function collectIgnoreLayers(root, start, signal, resources, ignoreBudget)
2574
2569
  if (!ignored(path, true, layers)) await walk(absolute);
2575
2570
  }
2576
2571
  }
2577
- await walk(start);
2572
+ if (!(await stat(start)).isFile()) await walk(start);
2578
2573
  return layers;
2579
2574
  }
2580
2575
  async function loadIgnoreFiles(directory, base, rules, budget, signal) {
@@ -2694,8 +2689,12 @@ async function resolveGrepPath(root, path = ".") {
2694
2689
  const candidate = scope(root, path);
2695
2690
  try {
2696
2691
  const info = await stat(candidate);
2697
- if (info.isFile()) return dirname6(candidate);
2698
- } catch {
2692
+ if (!info.isFile() && !info.isDirectory()) throw new Error("Search path must be a file or directory");
2693
+ } catch (error) {
2694
+ if (error.code === "ENOENT") {
2695
+ throw new Error(`Search path does not exist: ${path}. Use Glob to find the file; put filename patterns in glob, not path.`);
2696
+ }
2697
+ throw error;
2699
2698
  }
2700
2699
  return candidate;
2701
2700
  }
@@ -3645,6 +3644,7 @@ var FlavorSession = class {
3645
3644
  #pendingCoWorkStarts = [];
3646
3645
  #activeSteeringDurableIds = /* @__PURE__ */ new Set();
3647
3646
  #durableResumeStarted = false;
3647
+ #continuationResumeStarted = false;
3648
3648
  #permissionBaseline;
3649
3649
  #activeSubmission;
3650
3650
  constructor(services) {
@@ -3665,6 +3665,10 @@ var FlavorSession = class {
3665
3665
  queueSnapshot() {
3666
3666
  return this.#queue.snapshot();
3667
3667
  }
3668
+ /** True when start() admitted a persisted /loop or /goal continuation. */
3669
+ get rotationContinuationResumed() {
3670
+ return this.#continuationResumeStarted;
3671
+ }
3668
3672
  clearQueue() {
3669
3673
  const snapshot4 = this.#queue.clear();
3670
3674
  for (const kind of ["steer", "followUp"]) {
@@ -3814,6 +3818,7 @@ var FlavorSession = class {
3814
3818
  if (decision.additionalContext !== void 0) await this.#services.addContext?.(decision.additionalContext);
3815
3819
  this.#started = true;
3816
3820
  this.#resumeDurableQueue();
3821
+ this.#resumeRotationContinuation();
3817
3822
  });
3818
3823
  return this.#startPromise;
3819
3824
  }
@@ -3896,7 +3901,8 @@ var FlavorSession = class {
3896
3901
  skillNames,
3897
3902
  this.#services.managedToolCommands().map(({ name }) => name)
3898
3903
  );
3899
- if (command !== null) await this.#dispatch(command, controller.signal);
3904
+ if (submission.continuation !== void 0) await this.#runContinuation(submission.continuation, controller.signal);
3905
+ else if (command !== null) await this.#dispatch(command, controller.signal);
3900
3906
  else for await (const event of this.#services.run(prompt, controller.signal, {
3901
3907
  getSteeringMessages: () => this.#drainMessages("steer", true).map((item) => item.text),
3902
3908
  ...decision.additionalContext === void 0 ? {} : { additionalContext: decision.additionalContext },
@@ -4154,6 +4160,33 @@ Address this now (for example run the required verification or fix), then finish
4154
4160
  });
4155
4161
  }
4156
4162
  }
4163
+ /**
4164
+ * After a heap rotation the relaunched process inherits the same session and
4165
+ * a marker continuation naming the /loop or /goal that was in flight. Resume
4166
+ * it once here so a multi-day autonomous run survives the rotation instead of
4167
+ * silently stopping at the boundary.
4168
+ */
4169
+ #resumeRotationContinuation() {
4170
+ if (this.#continuationResumeStarted || this.#closed) return;
4171
+ const continuation = this.#services.continuation?.();
4172
+ if (continuation === void 0) return;
4173
+ const supported = continuation.kind === "loop" ? this.#services.resumeLoop !== void 0 : this.#services.resumeGoal !== void 0;
4174
+ if (!supported) return;
4175
+ this.#continuationResumeStarted = true;
4176
+ const displayText = `Resuming the interrupted ${continuation.kind} ${continuation.id} after a heap rotation.`;
4177
+ this.#notice(displayText);
4178
+ this.#enqueueNormalizedSubmission({ text: displayText, displayText, continuation }).catch((error) => {
4179
+ this.#services.output({ type: "error", error: { code: "unknown", message: message(error) } });
4180
+ });
4181
+ }
4182
+ async #runContinuation(continuation, signal) {
4183
+ const stream = continuation.kind === "loop" ? this.#services.resumeLoop?.(continuation.id, signal) : this.#services.resumeGoal?.(continuation.id, signal);
4184
+ if (stream === void 0) {
4185
+ this.#notice(`The runtime cannot resume the interrupted ${continuation.kind} ${continuation.id}; start it again with /${continuation.kind}.`);
4186
+ return;
4187
+ }
4188
+ for await (const event of stream) this.#services.output(event);
4189
+ }
4157
4190
  #ackDurable(submission) {
4158
4191
  if (submission.durableId === void 0) return;
4159
4192
  this.#services.durableQueue?.ack(submission.durableId);
@@ -4200,9 +4233,9 @@ Address this now (for example run the required verification or fix), then finish
4200
4233
  }
4201
4234
  #waitForPlanningIdle(key) {
4202
4235
  if (!this.#activePlanning.has(key)) return Promise.resolve();
4203
- return new Promise((resolve31) => {
4236
+ return new Promise((resolve32) => {
4204
4237
  const check = () => {
4205
- if (!this.#activePlanning.has(key)) resolve31();
4238
+ if (!this.#activePlanning.has(key)) resolve32();
4206
4239
  else this.#planningIdleWaiters.add(check);
4207
4240
  };
4208
4241
  this.#planningIdleWaiters.add(check);
@@ -4321,8 +4354,8 @@ async function fetchLatestVersion(options = {}) {
4321
4354
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4322
4355
  const fetchImpl = options.fetchImpl ?? ((url, init) => fetch(url, init));
4323
4356
  let timer;
4324
- const timeout = new Promise((resolve31) => {
4325
- timer = setTimeout(() => resolve31(void 0), timeoutMs);
4357
+ const timeout = new Promise((resolve32) => {
4358
+ timer = setTimeout(() => resolve32(void 0), timeoutMs);
4326
4359
  timer.unref?.();
4327
4360
  });
4328
4361
  try {
@@ -4447,7 +4480,7 @@ var PluginManifestSchema = z4.object({
4447
4480
  }).strict();
4448
4481
 
4449
4482
  // src/tools/shell.ts
4450
- import { spawn as spawn2 } from "child_process";
4483
+ import { spawn as spawn3 } from "child_process";
4451
4484
  import { existsSync as existsSync3 } from "fs";
4452
4485
  import { basename as basename2, delimiter as delimiter2, isAbsolute as isAbsolute5, join as join5, relative as relative5, resolve as resolve6, sep as sep5 } from "path";
4453
4486
  import { z as z6 } from "zod";
@@ -4490,7 +4523,7 @@ function resolveWindowsCommand(command, cwd, env) {
4490
4523
  ...pathValue.split(";").filter(Boolean).map((entry2) => resolve5(unquote(entry2), command))
4491
4524
  ];
4492
4525
  for (const base of bases) {
4493
- const candidates = extname(base).length > 0 ? [base] : [base, ...pathExt.map((extension) => `${base}${extension}`)];
4526
+ const candidates = extname(base).length > 0 ? [base] : [...pathExt.map((extension) => `${base}${extension}`), base];
4494
4527
  for (const candidate of candidates) {
4495
4528
  if (isRegularFile(candidate)) return resolve5(candidate);
4496
4529
  }
@@ -4568,7 +4601,7 @@ function createJobTools(registry) {
4568
4601
  inputSchema: IdInput,
4569
4602
  paths: () => [],
4570
4603
  execute: async (input, _signal, context) => {
4571
- registry.kill(input.id, owner(context));
4604
+ await registry.kill(input.id, owner(context));
4572
4605
  return registry.read(input.id, owner(context));
4573
4606
  },
4574
4607
  presentResult: (output) => jobPresentation("kill", output)
@@ -4608,11 +4641,51 @@ function owner(context) {
4608
4641
  return context?.ownerId ?? context?.agent ?? "main";
4609
4642
  }
4610
4643
 
4644
+ // src/utils/process-tree.ts
4645
+ import { spawn as spawn2 } from "child_process";
4646
+ async function terminateProcessTree(pid) {
4647
+ if (pid === void 0) return;
4648
+ if (process.platform === "win32") {
4649
+ await new Promise((resolve32) => {
4650
+ const child = spawn2("taskkill", ["/pid", String(pid), "/T", "/F"], { shell: false, windowsHide: true });
4651
+ const finish = () => {
4652
+ clearTimeout(timer);
4653
+ resolve32();
4654
+ };
4655
+ const timer = setTimeout(finish, 5e3);
4656
+ timer.unref();
4657
+ child.once("error", finish);
4658
+ child.once("close", finish);
4659
+ });
4660
+ try {
4661
+ process.kill(pid);
4662
+ } catch {
4663
+ }
4664
+ } else {
4665
+ try {
4666
+ process.kill(-pid, "SIGTERM");
4667
+ } catch {
4668
+ try {
4669
+ process.kill(pid, "SIGTERM");
4670
+ } catch {
4671
+ }
4672
+ }
4673
+ await new Promise((resolve32) => setTimeout(resolve32, 250));
4674
+ try {
4675
+ process.kill(-pid, "SIGKILL");
4676
+ } catch {
4677
+ try {
4678
+ process.kill(pid, "SIGKILL");
4679
+ } catch {
4680
+ }
4681
+ }
4682
+ }
4683
+ }
4684
+
4611
4685
  // src/tools/shell.ts
4612
4686
  var DEFAULT_MAX_OUTPUT_BYTES = 1048576;
4613
4687
  var DEFAULT_SHELL_TIMEOUT_MS = 10 * 6e4;
4614
4688
  var ELLIPSIS = "\u2026";
4615
- var TERMINATION_GRACE_MS = 250;
4616
4689
  var TERMINATION_FAILURE_MS = 5e3;
4617
4690
  var STREAM_CLOSE_GRACE_MS = 500;
4618
4691
  var ShellInput = z6.object({
@@ -4631,12 +4704,6 @@ function normalizeShellCommand(input) {
4631
4704
  if ((executable === "cmd" || executable === "cmd.exe") && /^\/[ck]$/iu.test(args[0] ?? "")) {
4632
4705
  return { command, args: [args[0], ...args.slice(1).map(unquoteToken)] };
4633
4706
  }
4634
- if ((executable === "pwsh" || executable === "pwsh.exe" || executable === "powershell" || executable === "powershell.exe") && /^(?:-c|-command)$/iu.test(args[0] ?? "") && args.length > 1) {
4635
- return { command, args: [args[0], ...args.slice(1).map(unquoteToken)] };
4636
- }
4637
- if (/^(?:sh|bash|zsh|fish)$/iu.test(executable) && args[0] === "-c" && args[1] !== void 0) {
4638
- return { command, args: [args[0], unquoteToken(args[1]), ...args.slice(2)] };
4639
- }
4640
4707
  if (!/\s/u.test(command) || containsShellSyntax(command)) return { command, args };
4641
4708
  const tokens = tokenizeCommandLine(command);
4642
4709
  if (tokens.length <= 1) return { command, args };
@@ -4754,7 +4821,8 @@ function shellInvocation(input, cwd, environment) {
4754
4821
  const payload = serializeCmdPayload(input.args.slice(commandFlag + 1));
4755
4822
  return {
4756
4823
  command: resolved,
4757
- args: [...input.args.slice(0, commandFlag + 1), `chcp 65001>nul & ${payload}`]
4824
+ args: [...input.args.slice(0, commandFlag + 1), `chcp 65001>nul & ${payload}`],
4825
+ windowsVerbatimArguments: true
4758
4826
  };
4759
4827
  }
4760
4828
  }
@@ -4770,7 +4838,8 @@ function shellInvocation(input, cwd, environment) {
4770
4838
  if (shell.kind === "posix") return { command: shell.command, args: ["-c", script] };
4771
4839
  if (shell.kind === "cmd") return {
4772
4840
  command: shell.command,
4773
- args: ["/d", "/s", "/c", `chcp 65001>nul & ${script}`]
4841
+ args: ["/d", "/s", "/c", `chcp 65001>nul & ${script}`],
4842
+ windowsVerbatimArguments: true
4774
4843
  };
4775
4844
  const encoded = Buffer.from(script, "utf16le").toString("base64");
4776
4845
  return {
@@ -4793,8 +4862,8 @@ function executionEnvironmentCommand(environment, command, args) {
4793
4862
  function structuredShellScript(shell, command, args) {
4794
4863
  if (shell.kind === "posix") return [command, ...args].map(posixQuote).join(" ");
4795
4864
  if (shell.kind === "cmd") return serializeCmdCommand([command, ...args]);
4796
- const invocation = `& ${psQuote(command)}${args.length > 0 ? ` ${args.map(psQuote).join(" ")}` : ""}`;
4797
- return `$ErrorActionPreference='Stop'; try { ${invocation}; if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 0 } } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 127 }`;
4865
+ const invocation = `& ${psQuote(command)}${args.length > 0 ? ` ${args.map((arg) => /^-[A-Za-z][A-Za-z0-9-]*$/u.test(arg) ? arg : psQuote(arg)).join(" ")}` : ""}`;
4866
+ return `$ErrorActionPreference='Stop'; try { ${invocation}; if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 0 } } catch { [Console]::Error.WriteLine($_.Exception.Message); if ($_.Exception -is [System.Management.Automation.CommandNotFoundException]) { exit 127 } else { exit 1 } }`;
4798
4867
  }
4799
4868
  function posixQuote(value) {
4800
4869
  return `'${value.replace(/'/gu, `'"'"'`)}'`;
@@ -4827,7 +4896,7 @@ ${result.stderr}`;
4827
4896
  return {
4828
4897
  kind: "command-not-found",
4829
4898
  message: `Executable "${command}" was not found by the selected runtime shell.`,
4830
- hint: "Use a command available on PATH, or pass an existing executable path without surrounding quotes."
4899
+ hint: "Use a command available on PATH, or pass an existing executable path without surrounding quotes. For an unavailable PowerShell cmdlet, check Get-Command and its module in the selected shell; do not keep retrying the same command."
4831
4900
  };
4832
4901
  }
4833
4902
  if (code === "EACCES" || /(?:permission denied|access is denied|拒绝访问)/iu.test(detail)) {
@@ -4836,11 +4905,11 @@ ${result.stderr}`;
4836
4905
  if (/(?:no such file or directory|cannot find (?:the )?path|system cannot find|系统找不到指定的路径)/iu.test(detail)) {
4837
4906
  return { kind: "path-not-found", message: "The command referenced a path that does not exist." };
4838
4907
  }
4839
- if (/(?:syntax error|unexpected token|parsererror|语法错误)/iu.test(detail)) {
4908
+ if (/(?:syntax\s*error|unexpected token|parsererror|unterminated (?:regexp|string)|语法错误)/iu.test(detail)) {
4840
4909
  return {
4841
4910
  kind: "shell-syntax",
4842
- message: "The selected runtime shell rejected the command syntax.",
4843
- hint: "Pass normal arguments through args; reserve shell syntax for pipes, redirects, and operators."
4911
+ message: "The command or interpreter rejected the syntax.",
4912
+ hint: 'Pass normal arguments through args. For JavaScript use command="node", args=["-e", "<complete script>"] without an extra shell or surrounding quotes. Legacy Windows PowerShell can strip quotes when forwarding native arguments.'
4844
4913
  };
4845
4914
  }
4846
4915
  return {
@@ -4862,7 +4931,7 @@ function createShellTool(workspace, options = {}) {
4862
4931
  }
4863
4932
  return {
4864
4933
  name: "Shell",
4865
- description: "Run a bounded command reliably inside the workspace. Pass a bare executable in command and one unquoted logical value per args entry; shell syntax is supported only when command contains pipes, redirects, or operators. stdout/stderr are returned verbatim and failures include a structured diagnostic. background=true returns a managed job id.",
4934
+ description: 'Run a bounded command reliably inside the workspace. Pass a bare executable in command and one logical value per args entry, without adding surrounding shell quotes. For Node scripts use command="node", args=["-e", "<complete JavaScript>"]; do not wrap native commands in cmd /c or PowerShell -Command unless shell syntax is needed, since legacy PowerShell can strip embedded quotes. Use Grep for text searches and Read/Edit for file changes. stdout/stderr are returned verbatim and failures include a structured diagnostic. background=true returns a managed job id.',
4866
4935
  inputSchema: ShellInput,
4867
4936
  paths: (input) => [workingDirectory(root, input.cwd ?? void 0)],
4868
4937
  summarize: (input) => [input.command, ...input.args].join(" "),
@@ -4962,7 +5031,7 @@ async function executeShell(root, input, cancellation, maxBytes, environment = p
4962
5031
  return;
4963
5032
  }
4964
5033
  const invocation = shellInvocation(input, cwd, environment);
4965
- const child = spawn2(invocation.command, invocation.args, {
5034
+ const child = spawn3(invocation.command, invocation.args, {
4966
5035
  cwd,
4967
5036
  shell: false,
4968
5037
  windowsHide: true,
@@ -4989,7 +5058,7 @@ async function executeShell(root, input, cancellation, maxBytes, environment = p
4989
5058
  void finishResolve(observedExitCode ?? child.exitCode, observedSignal ?? child.signalCode);
4990
5059
  return;
4991
5060
  }
4992
- termination = terminateTree(child.pid);
5061
+ termination = terminateProcessTree(child.pid);
4993
5062
  terminalTimer = setTimeout(() => finishReject(new Error(`Process did not close after ${reason} termination`)), TERMINATION_FAILURE_MS);
4994
5063
  terminalTimer.unref();
4995
5064
  };
@@ -5149,52 +5218,6 @@ function workingDirectory(root, cwd = ".") {
5149
5218
  if (delta === ".." || delta.startsWith(`..${sep5}`) || isAbsolute5(delta)) throw new Error("Working directory is outside the workspace");
5150
5219
  return candidate;
5151
5220
  }
5152
- async function terminateTree(pid) {
5153
- if (pid === void 0) return;
5154
- if (process.platform === "win32") {
5155
- await waitForProcess(spawn2("taskkill", ["/pid", String(pid), "/T", "/F"], { shell: false, windowsHide: true }));
5156
- try {
5157
- process.kill(pid);
5158
- } catch {
5159
- }
5160
- } else {
5161
- try {
5162
- process.kill(-pid, "SIGTERM");
5163
- } catch {
5164
- try {
5165
- process.kill(pid, "SIGTERM");
5166
- } catch {
5167
- }
5168
- }
5169
- await delay(TERMINATION_GRACE_MS);
5170
- try {
5171
- process.kill(-pid, "SIGKILL");
5172
- } catch {
5173
- try {
5174
- process.kill(pid, "SIGKILL");
5175
- } catch {
5176
- }
5177
- }
5178
- }
5179
- }
5180
- function waitForProcess(child) {
5181
- return new Promise((resolvePromise) => {
5182
- let settled = false;
5183
- const finish = () => {
5184
- if (settled) return;
5185
- settled = true;
5186
- clearTimeout(timer);
5187
- resolvePromise();
5188
- };
5189
- const timer = setTimeout(finish, TERMINATION_FAILURE_MS);
5190
- timer.unref();
5191
- child.once("error", finish);
5192
- child.once("close", finish);
5193
- });
5194
- }
5195
- function delay(milliseconds) {
5196
- return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
5197
- }
5198
5221
  function decodeOutput(buffer) {
5199
5222
  const encoding = outputEncoding(buffer);
5200
5223
  return new TextDecoder(encoding).decode(buffer);
@@ -5478,7 +5501,7 @@ function errorMessage(error) {
5478
5501
  }
5479
5502
 
5480
5503
  // src/utils/crash-guard.ts
5481
- import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
5504
+ import { mkdirSync, writeFileSync } from "fs";
5482
5505
  import { join as join7 } from "path";
5483
5506
  var installed = false;
5484
5507
  var crashing = false;
@@ -5518,7 +5541,7 @@ function installCrashGuard(options = {}) {
5518
5541
  const writeCrashLog = (kind, detail) => {
5519
5542
  try {
5520
5543
  const dir = crashLogDir();
5521
- mkdirSync2(dir, { recursive: true });
5544
+ mkdirSync(dir, { recursive: true });
5522
5545
  const path = join7(dir, `crash-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "")}-${process.pid}.log`);
5523
5546
  writeFileSync(path, [
5524
5547
  `kind: ${kind}`,
@@ -5573,7 +5596,7 @@ ${reason.stack ?? ""}`;
5573
5596
  }
5574
5597
 
5575
5598
  // src/execution/docker.ts
5576
- import { spawn as spawn3 } from "child_process";
5599
+ import { spawn as spawn4 } from "child_process";
5577
5600
  import { relative as relative7, resolve as resolve8, sep as sep7, win32 } from "path";
5578
5601
  function buildDockerInvocation(options) {
5579
5602
  const workspace = options.workspace;
@@ -5632,7 +5655,7 @@ function runDockerInvocation(invocation, signal, timeoutMs, maxOutputBytes = DEF
5632
5655
  resolvePromise({ exitCode: null, signal: null, stdout: "", stderr: "", terminationReason: "cancelled" });
5633
5656
  return;
5634
5657
  }
5635
- const child = spawn3(invocation.command, invocation.args, { windowsHide: true, shell: false });
5658
+ const child = spawn4(invocation.command, invocation.args, { windowsHide: true, shell: false });
5636
5659
  const stdout = new BoundedUtf8Output(maxOutputBytes);
5637
5660
  const stderr = new BoundedUtf8Output(maxOutputBytes);
5638
5661
  let reason = null;
@@ -5741,13 +5764,13 @@ import { createHash as createHash3 } from "crypto";
5741
5764
  import { Socket as Socket2 } from "net";
5742
5765
  import { tmpdir } from "os";
5743
5766
  import { join as join9 } from "path";
5744
- import { spawn as spawn4 } from "child_process";
5767
+ import { spawn as spawn5 } from "child_process";
5745
5768
 
5746
5769
  // src/pals/broker.ts
5747
5770
  import { createHash as createHash2, randomUUID as randomUUID5, timingSafeEqual } from "crypto";
5748
5771
  import { chmod as chmod2, lstat as lstat4, mkdir as mkdir6, unlink as unlink3 } from "fs/promises";
5749
5772
  import { createServer, Socket } from "net";
5750
- import { dirname as dirname8 } from "path";
5773
+ import { dirname as dirname6 } from "path";
5751
5774
 
5752
5775
  // src/pals/auth.ts
5753
5776
  import { randomBytes, randomUUID as randomUUID3 } from "crypto";
@@ -5798,7 +5821,7 @@ function validatePalAuthToken(token) {
5798
5821
  // src/pals/lifecycle.ts
5799
5822
  import { randomUUID as randomUUID4 } from "crypto";
5800
5823
  import { mkdir as mkdir5, open as open4, readFile as readFile6, rename as rename3, stat as stat3, unlink as unlink2 } from "fs/promises";
5801
- import { dirname as dirname7 } from "path";
5824
+ import { dirname as dirname5 } from "path";
5802
5825
  function defaultProcessAlive(pid) {
5803
5826
  if (!Number.isSafeInteger(pid) || pid <= 0) return true;
5804
5827
  try {
@@ -5819,7 +5842,7 @@ function parseMetadata(value) {
5819
5842
  }
5820
5843
  async function acquirePalFileLock(options) {
5821
5844
  if (await options.endpointLive()) return void 0;
5822
- await mkdir5(dirname7(options.path), { recursive: true, mode: 448 });
5845
+ await mkdir5(dirname5(options.path), { recursive: true, mode: 448 });
5823
5846
  const pid = options.pid ?? process.pid;
5824
5847
  const now = options.now ?? Date.now;
5825
5848
  const nonce = randomUUID4();
@@ -5883,12 +5906,12 @@ var BrokerStateError = class extends Error {
5883
5906
  };
5884
5907
  var WINDOWS_PIPE_PREFIX = "\\\\.\\pipe\\";
5885
5908
  function canConnect(address) {
5886
- return new Promise((resolve31) => {
5909
+ return new Promise((resolve32) => {
5887
5910
  const socket = new Socket();
5888
5911
  const finish = (connected) => {
5889
5912
  socket.removeAllListeners();
5890
5913
  socket.destroy();
5891
- resolve31(connected);
5914
+ resolve32(connected);
5892
5915
  };
5893
5916
  socket.once("connect", () => finish(true));
5894
5917
  socket.once("error", () => finish(false));
@@ -5937,7 +5960,7 @@ var PalBrokerServer = class {
5937
5960
  this.authToken ??= await loadOrCreatePalAuthToken(this.authHome === void 0 ? {} : { home: this.authHome });
5938
5961
  try {
5939
5962
  await this.prepareAddress();
5940
- await new Promise((resolve31, reject) => {
5963
+ await new Promise((resolve32, reject) => {
5941
5964
  const onError = (error) => {
5942
5965
  this.server.off("listening", onListening);
5943
5966
  reject(error);
@@ -5946,7 +5969,7 @@ var PalBrokerServer = class {
5946
5969
  this.server.off("error", onError);
5947
5970
  this.started = true;
5948
5971
  this.ownsAddress = true;
5949
- resolve31();
5972
+ resolve32();
5950
5973
  };
5951
5974
  this.server.once("error", onError);
5952
5975
  this.server.once("listening", onListening);
@@ -5974,7 +5997,7 @@ var PalBrokerServer = class {
5974
5997
  const ownsAddress = this.ownsAddress;
5975
5998
  for (const connection of this.connections) connection.socket.destroy();
5976
5999
  if (this.started) {
5977
- await new Promise((resolve31) => this.server.close(() => resolve31()));
6000
+ await new Promise((resolve32) => this.server.close(() => resolve32()));
5978
6001
  this.started = false;
5979
6002
  }
5980
6003
  this.ownsAddress = false;
@@ -5983,7 +6006,7 @@ var PalBrokerServer = class {
5983
6006
  }
5984
6007
  async prepareAddress() {
5985
6008
  if (isWindowsPipeAddress(this.address, this.platform)) return;
5986
- const parent = dirname8(this.address);
6009
+ const parent = dirname6(this.address);
5987
6010
  await mkdir6(parent, { recursive: true, mode: 448 });
5988
6011
  await chmod2(parent, 448);
5989
6012
  this.endpointLock = await acquirePalFileLock({
@@ -5993,8 +6016,8 @@ var PalBrokerServer = class {
5993
6016
  });
5994
6017
  if (this.endpointLock === void 0) throw new Error(`Pals endpoint '${this.address}' is active or owned`);
5995
6018
  try {
5996
- const stat11 = await lstat4(this.address);
5997
- if (!stat11.isSocket()) throw new Error(`Refusing to unlink '${this.address}' because it is not a socket`);
6019
+ const stat12 = await lstat4(this.address);
6020
+ if (!stat12.isSocket()) throw new Error(`Refusing to unlink '${this.address}' because it is not a socket`);
5998
6021
  await unlink3(this.address);
5999
6022
  } catch (error) {
6000
6023
  if (error.code !== "ENOENT") throw error;
@@ -6007,8 +6030,8 @@ var PalBrokerServer = class {
6007
6030
  }
6008
6031
  async unlinkKnownSocket() {
6009
6032
  try {
6010
- const stat11 = await lstat4(this.address);
6011
- if (stat11.isSocket()) await unlink3(this.address);
6033
+ const stat12 = await lstat4(this.address);
6034
+ if (stat12.isSocket()) await unlink3(this.address);
6012
6035
  } catch (error) {
6013
6036
  if (error.code !== "ENOENT") throw error;
6014
6037
  }
@@ -6642,16 +6665,16 @@ var PalBrokerState = class {
6642
6665
 
6643
6666
  // src/pals/broker-cli.ts
6644
6667
  var startupAttempts = /* @__PURE__ */ new Map();
6645
- function delay2(milliseconds) {
6646
- return new Promise((resolve31) => setTimeout(resolve31, milliseconds));
6668
+ function delay(milliseconds) {
6669
+ return new Promise((resolve32) => setTimeout(resolve32, milliseconds));
6647
6670
  }
6648
6671
  function canConnect2(address) {
6649
- return new Promise((resolve31) => {
6672
+ return new Promise((resolve32) => {
6650
6673
  const socket = new Socket2();
6651
6674
  const finish = (connected) => {
6652
6675
  socket.removeAllListeners();
6653
6676
  socket.destroy();
6654
- resolve31(connected);
6677
+ resolve32(connected);
6655
6678
  };
6656
6679
  socket.once("connect", () => finish(true));
6657
6680
  socket.once("error", () => finish(false));
@@ -6674,7 +6697,7 @@ async function spawnPalBrokerProcess(options) {
6674
6697
  const lock = await acquirePalFileLock({ path: lockPath, endpointLive: () => canConnect2(address) });
6675
6698
  if (lock === void 0) return;
6676
6699
  try {
6677
- const child = spawn4(options.executable, [...options.args], {
6700
+ const child = spawn5(options.executable, [...options.args], {
6678
6701
  detached: true,
6679
6702
  stdio: "ignore",
6680
6703
  windowsHide: true,
@@ -6682,7 +6705,7 @@ async function spawnPalBrokerProcess(options) {
6682
6705
  });
6683
6706
  child.unref();
6684
6707
  const deadline = Date.now() + 5e3;
6685
- while (Date.now() < deadline && !await canConnect2(address)) await delay2(25);
6708
+ while (Date.now() < deadline && !await canConnect2(address)) await delay(25);
6686
6709
  if (!await canConnect2(address)) throw new Error(`Pals broker process did not become ready at '${address}'`);
6687
6710
  } finally {
6688
6711
  await lock.release();
@@ -6694,8 +6717,8 @@ async function runPalBroker(options) {
6694
6717
  const sweepIntervalMs = options.sweepIntervalMs ?? 5e3;
6695
6718
  const idleShutdownMs = options.idleShutdownMs ?? 3e4;
6696
6719
  let resolveClosed;
6697
- const closed = new Promise((resolve31) => {
6698
- resolveClosed = resolve31;
6720
+ const closed = new Promise((resolve32) => {
6721
+ resolveClosed = resolve32;
6699
6722
  });
6700
6723
  let closing;
6701
6724
  const timer = setInterval(() => {
@@ -6729,17 +6752,17 @@ async function ensureOnce(options) {
6729
6752
  const deadline = Date.now() + timeoutMs;
6730
6753
  do {
6731
6754
  if (await canConnect2(options.address)) return;
6732
- await delay2(retryDelayMs);
6755
+ await delay(retryDelayMs);
6733
6756
  } while (Date.now() < deadline);
6734
6757
  throw new Error(`Timed out waiting for pals broker at '${options.address}'`);
6735
6758
  }
6736
6759
 
6737
6760
  // src/production.ts
6738
- import { readFile as readFile23 } from "fs/promises";
6739
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
6761
+ import { readFile as readFile24 } from "fs/promises";
6762
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
6740
6763
  import { createHash as createHash18, randomUUID as randomUUID20 } from "crypto";
6741
6764
  import { homedir as homedir3, release as osRelease, tmpdir as tmpdir4, version as osVersion } from "os";
6742
- import { basename as basename7, dirname as dirname19, join as join29, relative as relative21, resolve as resolve30, sep as sep21 } from "path";
6765
+ import { basename as basename7, dirname as dirname17, join as join30, relative as relative22, resolve as resolve31, sep as sep22 } from "path";
6743
6766
 
6744
6767
  // src/agent/subagents.ts
6745
6768
  import { z as z9 } from "zod";
@@ -7159,7 +7182,7 @@ var TaskPlanSchema = z10.object({
7159
7182
  }
7160
7183
  dependencies.add(dependency);
7161
7184
  }
7162
- if (task.status === "completed") {
7185
+ if (task.status === "completed" || task.status === "in_progress") {
7163
7186
  const incomplete = task.dependencies.find((dependency) => plan.tasks.find((candidate) => candidate.id === dependency)?.status !== "completed");
7164
7187
  if (incomplete !== void 0) {
7165
7188
  context.addIssue({
@@ -7188,7 +7211,7 @@ function updatePlanTask(plan, input) {
7188
7211
  if (!isValidTransition(previousStatus, update.status)) {
7189
7212
  throw new Error(`Invalid task transition for ${update.taskId}: ${previousStatus} -> ${update.status}`);
7190
7213
  }
7191
- if (update.status === "completed") {
7214
+ if (update.status === "completed" || update.status === "in_progress") {
7192
7215
  const incomplete = current.tasks[index].dependencies.find((dependency) => current.tasks.find((task) => task.id === dependency)?.status !== "completed");
7193
7216
  if (incomplete !== void 0) throw new Error(`Task ${update.taskId} has an incomplete dependency: ${incomplete}`);
7194
7217
  }
@@ -7213,6 +7236,7 @@ function isValidTransition(from, to) {
7213
7236
  if (from === "in_progress") {
7214
7237
  return to === "completed" || to === "failed" || to === "blocked" || to === "cancelled";
7215
7238
  }
7239
+ if (from === "blocked" || from === "failed") return to === "pending" || to === "in_progress" || to === "cancelled";
7216
7240
  return false;
7217
7241
  }
7218
7242
  function normalizeAbandonedPlan(plan) {
@@ -7696,6 +7720,18 @@ var ContextManager = class _ContextManager {
7696
7720
  estimatedTokens() {
7697
7721
  return estimateMessageTokens(this.messagesForModel());
7698
7722
  }
7723
+ /** Retained sizes for the rotation census: what this context holds right now. */
7724
+ census() {
7725
+ const chars = (messages) => messages.reduce((sum, entry2) => sum + modelContentText(entry2.content).length, 0);
7726
+ return {
7727
+ messages: this.#messages.length,
7728
+ messageChars: chars(this.#messages),
7729
+ visibilityEntries: this.#visibilityLog.length,
7730
+ visibilityChars: this.#visibilityLog.reduce((sum, record) => sum + record.content.length, 0),
7731
+ transientEntries: this.#activeTransientSystem.size,
7732
+ transientChars: chars([...this.#activeTransientSystem.values()])
7733
+ };
7734
+ }
7699
7735
  needsCompaction() {
7700
7736
  if (modelVisibleText(this.messagesForModel()).length >= this.#compactAtChars) return true;
7701
7737
  return calculateContextPressure(this.#currentTokenUsage(), this.#compaction).shouldAutoCompact;
@@ -8085,7 +8121,7 @@ function providerValidMessages(input) {
8085
8121
  // src/context/workspace-instructions.ts
8086
8122
  import { createHash as createHash5 } from "crypto";
8087
8123
  import { lstat as lstat5, readFile as readFile7, realpath as realpath2 } from "fs/promises";
8088
- import { dirname as dirname9, isAbsolute as isAbsolute7, relative as relative8, resolve as resolve9, sep as sep8 } from "path";
8124
+ import { dirname as dirname7, isAbsolute as isAbsolute7, relative as relative8, resolve as resolve9, sep as sep8 } from "path";
8089
8125
  var NAMES = ["AGENTS.md", "CLAUDE.md", "AGENTS.local.md", "CLAUDE.local.md"];
8090
8126
  var ROOT_NAMES = ["AGENTS.md", "CLAUDE.md", "AGENTS.local.md", "CLAUDE.local.md"];
8091
8127
  var WorkspaceInstructions = class {
@@ -8113,7 +8149,7 @@ var WorkspaceInstructions = class {
8113
8149
  const depth = depthOf(this.#root, current);
8114
8150
  directories.set(current, depth);
8115
8151
  if (current === this.#root) break;
8116
- current = dirname9(current);
8152
+ current = dirname7(current);
8117
8153
  }
8118
8154
  }
8119
8155
  const ordered = [...directories].map(([path, depth]) => ({ path, depth })).sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path));
@@ -8187,9 +8223,9 @@ ${document.content}
8187
8223
  };
8188
8224
  async function directoryForPath(path) {
8189
8225
  try {
8190
- return (await lstat5(path)).isDirectory() ? path : dirname9(path);
8226
+ return (await lstat5(path)).isDirectory() ? path : dirname7(path);
8191
8227
  } catch {
8192
- return dirname9(path);
8228
+ return dirname7(path);
8193
8229
  }
8194
8230
  }
8195
8231
  function depthOf(root, path) {
@@ -8264,8 +8300,75 @@ function reportProgress(callback, percentage) {
8264
8300
  }
8265
8301
  }
8266
8302
 
8267
- // src/agent/loop.ts
8303
+ // src/utils/heap.ts
8268
8304
  import { getHeapStatistics } from "v8";
8305
+ import { totalmem } from "os";
8306
+ var MEMORY_PRESSURE_HEAP_RATIO = 0.8;
8307
+ var MEMORY_ROTATION_HEAP_RATIO = 0.6;
8308
+ var MEMORY_PRESSURE_RSS_RATIO = 0.75;
8309
+ var MEMORY_ROTATION_RSS_RATIO = 0.6;
8310
+ var ROTATION_PRECHECK_RATIO = 0.45;
8311
+ function readHeap() {
8312
+ const heapLimit = getHeapStatistics().heap_size_limit;
8313
+ const heapUsed = process.memoryUsage().heapUsed;
8314
+ return { heapUsed, heapLimit, ratio: heapLimit > 0 ? heapUsed / heapLimit : 0 };
8315
+ }
8316
+ function forceFullGc() {
8317
+ const gc = globalThis.gc;
8318
+ if (typeof gc !== "function") return false;
8319
+ gc();
8320
+ gc();
8321
+ return true;
8322
+ }
8323
+ function ratioOf(heapLimit, heapUsed) {
8324
+ return heapLimit > 0 ? heapUsed / heapLimit : 0;
8325
+ }
8326
+ function effectiveMemoryLimit() {
8327
+ const physical = totalmem();
8328
+ const constrained = process.constrainedMemory?.() ?? 0;
8329
+ return constrained > 0 ? Math.min(physical, constrained) : physical;
8330
+ }
8331
+ function withRss(reading, gcVerified) {
8332
+ const usage = process.memoryUsage();
8333
+ const memoryLimit = effectiveMemoryLimit();
8334
+ return {
8335
+ ...reading,
8336
+ gcVerified,
8337
+ rss: usage.rss,
8338
+ memoryLimit,
8339
+ rssRatio: ratioOf(memoryLimit, usage.rss),
8340
+ external: usage.external,
8341
+ arrayBuffers: usage.arrayBuffers
8342
+ };
8343
+ }
8344
+ function verifiedHeapPressure(threshold) {
8345
+ const raw = readHeap();
8346
+ const before = withRss(raw, false);
8347
+ const rssThreshold = threshold >= MEMORY_PRESSURE_HEAP_RATIO ? MEMORY_PRESSURE_RSS_RATIO : threshold;
8348
+ if ((raw.heapLimit <= 0 || raw.ratio < threshold) && (before.rssRatio ?? 0) < rssThreshold) return void 0;
8349
+ if (!forceFullGc()) return before;
8350
+ const heapUsed = process.memoryUsage().heapUsed;
8351
+ const ratio = ratioOf(raw.heapLimit, heapUsed);
8352
+ const verified = withRss({ heapUsed, heapLimit: raw.heapLimit, ratio, verifiedRatio: ratio }, true);
8353
+ if (ratio < threshold && (verified.rssRatio ?? 0) < rssThreshold) return void 0;
8354
+ return verified;
8355
+ }
8356
+ function heapRotationNeeded() {
8357
+ const raw = readHeap();
8358
+ const before = withRss(raw, false);
8359
+ if ((raw.heapLimit <= 0 || raw.ratio < ROTATION_PRECHECK_RATIO) && (before.rssRatio ?? 0) < ROTATION_PRECHECK_RATIO) return void 0;
8360
+ if (!forceFullGc()) {
8361
+ return raw.ratio >= MEMORY_PRESSURE_HEAP_RATIO || (before.rssRatio ?? 0) >= MEMORY_PRESSURE_RSS_RATIO ? before : void 0;
8362
+ }
8363
+ const heapUsed = process.memoryUsage().heapUsed;
8364
+ const ratio = ratioOf(raw.heapLimit, heapUsed);
8365
+ const verified = withRss({ heapUsed, heapLimit: raw.heapLimit, ratio, verifiedRatio: ratio }, true);
8366
+ if (ratio < MEMORY_ROTATION_HEAP_RATIO && (verified.rssRatio ?? 0) < MEMORY_ROTATION_RSS_RATIO) return void 0;
8367
+ return verified;
8368
+ }
8369
+ function heapSnapshotWarranted(_reading) {
8370
+ return process.env.FLAVOR_HEAP_SNAPSHOT_ON_ROTATE === "1";
8371
+ }
8269
8372
 
8270
8373
  // src/models/structured.ts
8271
8374
  import { z as z11 } from "zod";
@@ -8288,8 +8391,8 @@ function withStructuredOutput(options) {
8288
8391
  if (retry.backoffMs.length < retry.maxRetries) {
8289
8392
  throw new Error("Structured output backoffMs must provide one delay per retry");
8290
8393
  }
8291
- for (const delay3 of retry.backoffMs.slice(0, retry.maxRetries)) {
8292
- if (!Number.isSafeInteger(delay3) || delay3 < 0) {
8394
+ for (const delay2 of retry.backoffMs.slice(0, retry.maxRetries)) {
8395
+ if (!Number.isSafeInteger(delay2) || delay2 < 0) {
8293
8396
  throw new Error("Structured output retry delays must be non-negative integers");
8294
8397
  }
8295
8398
  }
@@ -8650,10 +8753,10 @@ function sanitize(message2, secrets) {
8650
8753
  }
8651
8754
  function waitForRetry(delayMs, signal) {
8652
8755
  if (signal?.aborted) return Promise.reject(signal.reason);
8653
- return new Promise((resolve31, reject) => {
8756
+ return new Promise((resolve32, reject) => {
8654
8757
  const timer = setTimeout(() => {
8655
8758
  signal?.removeEventListener("abort", onAbort);
8656
- resolve31();
8759
+ resolve32();
8657
8760
  }, delayMs);
8658
8761
  const onAbort = () => {
8659
8762
  clearTimeout(timer);
@@ -8684,7 +8787,6 @@ function envMaxIterations() {
8684
8787
  if (!Number.isInteger(parsed) || parsed <= 0) return void 0;
8685
8788
  return parsed;
8686
8789
  }
8687
- var MEMORY_PRESSURE_HEAP_RATIO = 0.8;
8688
8790
  function memoryPressureError(heapUsed, heapLimit) {
8689
8791
  if (heapLimit <= 0 || heapUsed < heapLimit * MEMORY_PRESSURE_HEAP_RATIO) return void 0;
8690
8792
  const mb = (bytes) => Math.round(bytes / 1048576);
@@ -8694,7 +8796,12 @@ function memoryPressureError(heapUsed, heapLimit) {
8694
8796
  };
8695
8797
  }
8696
8798
  function currentMemoryPressure() {
8697
- return memoryPressureError(process.memoryUsage().heapUsed, getHeapStatistics().heap_size_limit);
8799
+ const reading = verifiedHeapPressure(MEMORY_PRESSURE_HEAP_RATIO);
8800
+ if (reading === void 0) return void 0;
8801
+ return memoryPressureError(reading.heapUsed, reading.heapLimit) ?? {
8802
+ code: "memory_pressure",
8803
+ message: `Process RSS reached ${Math.round((reading.rssRatio ?? 0) * 100)}% of the available memory limit; stopping before system OOM`
8804
+ };
8698
8805
  }
8699
8806
  var AgentLoop = class {
8700
8807
  #options;
@@ -8724,6 +8831,15 @@ var AgentLoop = class {
8724
8831
  get iterationLimitMode() {
8725
8832
  return this.#iterationLimitMode;
8726
8833
  }
8834
+ get maxIterations() {
8835
+ return this.#options.maxIterations;
8836
+ }
8837
+ get softLimitFactor() {
8838
+ return this.#options.softLimitFactor;
8839
+ }
8840
+ get extendIterations() {
8841
+ return this.#options.extendIterations;
8842
+ }
8727
8843
  setModel(modelId) {
8728
8844
  this.#options.registry.get(modelId);
8729
8845
  this.#options.modelId = modelId;
@@ -9291,10 +9407,10 @@ function serializeToolInput(input) {
9291
9407
  }
9292
9408
  function waitForRetry2(delayMs, signal) {
9293
9409
  if (signal?.aborted) return Promise.reject(signal.reason);
9294
- return new Promise((resolve31, reject) => {
9410
+ return new Promise((resolve32, reject) => {
9295
9411
  const timer = setTimeout(() => {
9296
9412
  signal?.removeEventListener("abort", onAbort);
9297
- resolve31();
9413
+ resolve32();
9298
9414
  }, delayMs);
9299
9415
  const onAbort = () => {
9300
9416
  clearTimeout(timer);
@@ -9504,8 +9620,9 @@ var ToolRuntime = class {
9504
9620
  }
9505
9621
  }
9506
9622
  async execute(call, context) {
9623
+ if (this.#disposed) return { ok: false, error: { code: "runtime_disposed", message: "ToolRuntime is disposed" } };
9507
9624
  const definition = this.#tools.get(call.name);
9508
- const journalId = definition === void 0 || this.#journal === void 0 ? void 0 : this.#journal.start(call.name, call.input, definition.retrySafe === true || definition.readOnly === true);
9625
+ const journalId = definition === void 0 || this.#journal === void 0 ? void 0 : this.#journal.start(call.name, call.input, definition.retrySafe === true || definition.readOnly === true || getToolCategory(call.name) === "read");
9509
9626
  try {
9510
9627
  const result = await this.#execute(call, context);
9511
9628
  if (journalId !== void 0) this.#journal?.complete(journalId, result);
@@ -9532,6 +9649,10 @@ var ToolRuntime = class {
9532
9649
  if (tool === void 0) return { ok: false, error: { code: "unknown_tool", message: `Unknown tool: ${call.name}` } };
9533
9650
  const signal = context.signal ?? new AbortController().signal;
9534
9651
  const toolCallId = call.id ?? randomUUID7();
9652
+ if (tool.agents !== void 0 && !tool.agents.includes(context.agent)) {
9653
+ return this.#fail(toolCallId, tool.name, call.input, context.agent, "permission_denied", `Tool ${tool.name} is not available to ${context.agent} agents`);
9654
+ }
9655
+ if (signal.aborted) return this.#fail(toolCallId, tool.name, call.input, context.agent, "cancelled", message(signal.reason));
9535
9656
  const validation = this.normalize(call);
9536
9657
  if (!validation.ok) {
9537
9658
  return this.#fail(toolCallId, call.name, call.input, context.agent, validation.error.code, validation.error.message);
@@ -9639,17 +9760,37 @@ var ToolRuntime = class {
9639
9760
  } catch (error) {
9640
9761
  return this.#fail(toolCallId, tool.name, input, context.agent, "invalid_output", message(error));
9641
9762
  }
9642
- const declaredPresentation = tool.presentResult?.(rawOutput, input);
9643
- const presentation = declaredPresentation ?? getToolPresentation(rawOutput);
9644
- const rendered = tool.renderForModel?.(rawOutput, input);
9763
+ const warnings = [];
9764
+ let presentation = getToolPresentation(rawOutput);
9765
+ try {
9766
+ presentation = tool.presentResult?.(rawOutput, input) ?? presentation;
9767
+ } catch (error) {
9768
+ warnings.push(`Tool succeeded; result presentation failed: ${message(error)}`);
9769
+ }
9770
+ let rendered;
9771
+ try {
9772
+ rendered = tool.renderForModel?.(rawOutput, input);
9773
+ } catch (error) {
9774
+ warnings.push(`Tool succeeded; result rendering failed: ${message(error)}`);
9775
+ }
9645
9776
  const output = rendered === void 0 ? await this.#limitOutput(tool.name, rawOutput) : rawOutput;
9646
9777
  const content = rendered === void 0 ? void 0 : await this.#limitText(tool.name, rendered);
9647
- const additionalContext = await this.#afterSuccess?.(tool.name, tool.paths(input), input, rawOutput, context);
9648
- await this.#hooks.emit({
9649
- version: 1,
9650
- type: "PostToolUse",
9651
- payload: { toolCallId, tool: tool.name, input, agent: context.agent, output }
9652
- });
9778
+ let additionalContext = [];
9779
+ try {
9780
+ additionalContext = await this.#afterSuccess?.(tool.name, tool.paths(input), input, rawOutput, context) ?? [];
9781
+ } catch (error) {
9782
+ warnings.push(`Tool succeeded; context update failed: ${message(error)}`);
9783
+ }
9784
+ try {
9785
+ await this.#hooks.emit({
9786
+ version: 1,
9787
+ type: "PostToolUse",
9788
+ payload: { toolCallId, tool: tool.name, input, agent: context.agent, output }
9789
+ });
9790
+ } catch (error) {
9791
+ warnings.push(`Tool succeeded; PostToolUse hook failed: ${message(error)}`);
9792
+ }
9793
+ additionalContext = [...additionalContext, ...warnings];
9653
9794
  return {
9654
9795
  ok: true,
9655
9796
  output,
@@ -9680,17 +9821,23 @@ var ToolRuntime = class {
9680
9821
  const preview = headTailPreview(serialized, previewChars);
9681
9822
  this.#turnOutputChars += preview.length;
9682
9823
  const directory = join10(this.#workspace, ".flavor", "tool-results");
9683
- await mkdir7(directory, { recursive: true });
9684
9824
  const filename2 = `${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}-${randomUUID7()}-${safeFilename(tool)}.txt`;
9685
9825
  const savedTo = join10(directory, filename2);
9686
- await writeFile4(savedTo, serialized, "utf8");
9826
+ let storage;
9827
+ try {
9828
+ await mkdir7(directory, { recursive: true });
9829
+ await writeFile4(savedTo, serialized, "utf8");
9830
+ storage = { savedTo };
9831
+ } catch (error) {
9832
+ storage = { storageError: `Tool succeeded, but the full output could not be saved: ${message(error)}` };
9833
+ }
9687
9834
  return {
9688
9835
  truncated: true,
9689
9836
  reason,
9690
9837
  preview,
9691
9838
  originalChars: serialized.length,
9692
9839
  previewChars: preview.length,
9693
- savedTo
9840
+ ...storage
9694
9841
  };
9695
9842
  }
9696
9843
  callPresentation(call) {
@@ -9891,6 +10038,8 @@ var LocalHarness = class {
9891
10038
  agent,
9892
10039
  ownerId,
9893
10040
  ...maxIterations === void 0 ? {} : { maxIterations },
10041
+ ...this.#options.softLimitFactor === void 0 ? {} : { softLimitFactor: this.#options.softLimitFactor },
10042
+ ...this.#options.extendIterations === void 0 ? {} : { extendIterations: this.#options.extendIterations },
9894
10043
  ...isMain && this.#options.hasActiveProgress !== void 0 ? { hasActiveProgress: this.#options.hasActiveProgress } : {},
9895
10044
  ...isMain && this.#options.hallucinationGuard !== void 0 ? { hallucinationGuard: this.#options.hallucinationGuard } : {},
9896
10045
  ...this.#options.modelJournal === void 0 ? {} : { modelJournal: this.#options.modelJournal }
@@ -9920,7 +10069,7 @@ function toModelTool(tool) {
9920
10069
 
9921
10070
  // src/harness/journal.ts
9922
10071
  import { createHash as createHash6, randomUUID as randomUUID8 } from "crypto";
9923
- import { closeSync, constants as constants3, existsSync as existsSync4, fsyncSync, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync2, realpathSync as realpathSync2, renameSync, statSync as statSync2, truncateSync, unlinkSync, writeSync } from "fs";
10072
+ import { closeSync, constants as constants3, existsSync as existsSync4, fsyncSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, realpathSync as realpathSync2, renameSync, statSync as statSync2, truncateSync, unlinkSync, writeSync } from "fs";
9924
10073
  import { join as join11, relative as relative9, resolve as resolve11, sep as sep9 } from "path";
9925
10074
  import { z as z14 } from "zod";
9926
10075
 
@@ -10013,7 +10162,7 @@ var HarnessJournal = class {
10013
10162
  const workspace = resolve11(options.workspace);
10014
10163
  const directory = join11(workspace, ".flavor", "sessions");
10015
10164
  assertSessionId(options.sessionId);
10016
- mkdirSync3(directory, { recursive: true, mode: 448 });
10165
+ mkdirSync2(directory, { recursive: true, mode: 448 });
10017
10166
  assertWithin(workspace, directory);
10018
10167
  const canonicalWorkspace = realpathSync2.native(workspace);
10019
10168
  const canonicalDirectory = realpathSync2.native(directory);
@@ -10030,6 +10179,10 @@ var HarnessJournal = class {
10030
10179
  get records() {
10031
10180
  return this.#records.map((record) => structuredClone(record));
10032
10181
  }
10182
+ /** Retained record count for the rotation census. */
10183
+ get recordCount() {
10184
+ return this.#records.length;
10185
+ }
10033
10186
  admitQueue(kind, payload) {
10034
10187
  QueueKindSchema.parse(kind);
10035
10188
  const id = randomUUID8();
@@ -10306,19 +10459,38 @@ function jsonSafeRecord(value) {
10306
10459
  return JSON.parse(json);
10307
10460
  }
10308
10461
  function hashJson(value) {
10309
- return createHash6("sha256").update(stableJson(value)).digest("hex");
10310
- }
10311
- function recordHash(record) {
10312
- const { hash: _hash, ...unsigned } = record;
10313
- return hashJson(unsigned);
10462
+ const hash = createHash6("sha256");
10463
+ stableHashInto(hash, value);
10464
+ return hash.digest("hex");
10314
10465
  }
10315
- function stableJson(value) {
10316
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
10466
+ function stableHashInto(hash, value) {
10467
+ if (Array.isArray(value)) {
10468
+ hash.update("[");
10469
+ for (let index = 0; index < value.length; index += 1) {
10470
+ if (index > 0) hash.update(",");
10471
+ stableHashInto(hash, value[index]);
10472
+ }
10473
+ hash.update("]");
10474
+ return;
10475
+ }
10317
10476
  if (typeof value === "object" && value !== null) {
10318
10477
  const object = value;
10319
- return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`;
10478
+ hash.update("{");
10479
+ const keys = Object.keys(object).sort();
10480
+ for (let index = 0; index < keys.length; index += 1) {
10481
+ const key = keys[index];
10482
+ if (index > 0) hash.update(",");
10483
+ hash.update(`${JSON.stringify(key)}:`);
10484
+ stableHashInto(hash, object[key]);
10485
+ }
10486
+ hash.update("}");
10487
+ return;
10320
10488
  }
10321
- return JSON.stringify(value) ?? String(value);
10489
+ hash.update(JSON.stringify(value) ?? String(value));
10490
+ }
10491
+ function recordHash(record) {
10492
+ const { hash: _hash, ...unsigned } = record;
10493
+ return hashJson(unsigned);
10322
10494
  }
10323
10495
  function assertSessionId(value) {
10324
10496
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) throw new Error("Invalid session id");
@@ -10332,7 +10504,7 @@ function isWithin2(root, target) {
10332
10504
  }
10333
10505
 
10334
10506
  // src/hooks/bus.ts
10335
- import { spawn as spawn5 } from "child_process";
10507
+ import { spawn as spawn6 } from "child_process";
10336
10508
  import { randomUUID as randomUUID9 } from "crypto";
10337
10509
  var DEFAULT_TIMEOUT_MS2 = 1e4;
10338
10510
  var DEFAULT_MAX_OUTPUT_BYTES2 = 1048576;
@@ -10439,10 +10611,10 @@ function decoratePayload(payload, envelope) {
10439
10611
  return envelope === void 0 ? payload : { ...payload, ...envelope };
10440
10612
  }
10441
10613
  function runShellHandler(descriptor, event, signal) {
10442
- return new Promise((resolve31, reject) => {
10614
+ return new Promise((resolve32, reject) => {
10443
10615
  const env = { ...process.env, ...descriptor.env };
10444
10616
  const invocation = prepareSpawnInvocation(descriptor.command, descriptor.args ?? [], { env });
10445
- const child = spawn5(invocation.command, invocation.args, {
10617
+ const child = spawn6(invocation.command, invocation.args, {
10446
10618
  env,
10447
10619
  shell: false,
10448
10620
  detached: process.platform !== "win32",
@@ -10459,7 +10631,7 @@ function runShellHandler(descriptor, event, signal) {
10459
10631
  if (settled) return;
10460
10632
  settled = true;
10461
10633
  signal.removeEventListener("abort", onAbort);
10462
- terminateProcessTree(child);
10634
+ terminateProcessTree2(child);
10463
10635
  reject(error);
10464
10636
  };
10465
10637
  const append = (stream, chunk) => {
@@ -10485,7 +10657,7 @@ function runShellHandler(descriptor, event, signal) {
10485
10657
  return;
10486
10658
  }
10487
10659
  try {
10488
- resolve31(HookDecisionSchema.parse(JSON.parse(stdout)));
10660
+ resolve32(HookDecisionSchema.parse(JSON.parse(stdout)));
10489
10661
  } catch (error) {
10490
10662
  reject(error);
10491
10663
  }
@@ -10493,10 +10665,10 @@ function runShellHandler(descriptor, event, signal) {
10493
10665
  child.stdin.end(JSON.stringify(event));
10494
10666
  });
10495
10667
  }
10496
- function terminateProcessTree(child) {
10668
+ function terminateProcessTree2(child) {
10497
10669
  if (child.pid === void 0) return;
10498
10670
  if (process.platform === "win32") {
10499
- const killer = spawn5("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
10671
+ const killer = spawn6("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
10500
10672
  stdio: "ignore",
10501
10673
  windowsHide: true
10502
10674
  });
@@ -10682,14 +10854,14 @@ async function createIslandControlServer(options) {
10682
10854
  void dispatch(line, socket, options, token);
10683
10855
  });
10684
10856
  });
10685
- await new Promise((resolve31, reject) => {
10857
+ await new Promise((resolve32, reject) => {
10686
10858
  const onError = (error) => {
10687
10859
  server.removeListener("listening", onListening);
10688
10860
  reject(error);
10689
10861
  };
10690
10862
  const onListening = () => {
10691
10863
  server.removeListener("error", onError);
10692
- resolve31();
10864
+ resolve32();
10693
10865
  };
10694
10866
  server.once("error", onError);
10695
10867
  server.once("listening", onListening);
@@ -10760,17 +10932,17 @@ function write(socket, payload) {
10760
10932
  }
10761
10933
  }
10762
10934
  function closeServer(server) {
10763
- return new Promise((resolve31) => {
10935
+ return new Promise((resolve32) => {
10764
10936
  try {
10765
- server.close(() => resolve31());
10937
+ server.close(() => resolve32());
10766
10938
  } catch {
10767
- resolve31();
10939
+ resolve32();
10768
10940
  }
10769
10941
  });
10770
10942
  }
10771
10943
 
10772
10944
  // src/evolve/service.ts
10773
- import { spawn as spawn6 } from "child_process";
10945
+ import { spawn as spawn7 } from "child_process";
10774
10946
  import { writeFile as writeFile8 } from "fs/promises";
10775
10947
  import { join as join15 } from "path";
10776
10948
  import { z as z16 } from "zod";
@@ -10790,7 +10962,7 @@ import { z as z15 } from "zod";
10790
10962
 
10791
10963
  // src/plugins/worker-host.ts
10792
10964
  import { realpath as realpath3 } from "fs/promises";
10793
- import { dirname as dirname10 } from "path";
10965
+ import { dirname as dirname8 } from "path";
10794
10966
  import { Worker } from "worker_threads";
10795
10967
  var DEFAULT_ACTIVATION_TIMEOUT_MS = 1e4;
10796
10968
  var DEFAULT_INVOCATION_TIMEOUT_MS = 3e4;
@@ -10798,7 +10970,7 @@ var DEFAULT_MAX_OLD_SPACE_MB = 128;
10798
10970
  var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
10799
10971
  async function startPluginSandbox(options) {
10800
10972
  const entryPath = await realpath3(options.entryPath);
10801
- const pluginRoot = await realpath3(options.pluginRoot ?? dirname10(entryPath));
10973
+ const pluginRoot = await realpath3(options.pluginRoot ?? dirname8(entryPath));
10802
10974
  const activationTimeoutMs = options.activationTimeoutMs ?? DEFAULT_ACTIVATION_TIMEOUT_MS;
10803
10975
  const invocationTimeoutMs = options.invocationTimeoutMs ?? DEFAULT_INVOCATION_TIMEOUT_MS;
10804
10976
  let configJson;
@@ -11618,7 +11790,7 @@ async function revertFixPlugin(workspace, name) {
11618
11790
  // src/evolve/store.ts
11619
11791
  import { createHash as createHash9 } from "crypto";
11620
11792
  import { appendFile, mkdir as mkdir9, readFile as readFile9, rm as rm3, writeFile as writeFile7 } from "fs/promises";
11621
- import { dirname as dirname11, join as join14 } from "path";
11793
+ import { dirname as dirname9, join as join14 } from "path";
11622
11794
  function normalizeError(message2) {
11623
11795
  return String(message2 ?? "").replace(/\s+/g, " ").replace(/"[^"]*"/g, '"\u2026"').replace(/`[^`]*`/g, '"\u2026"').trim().slice(0, 160);
11624
11796
  }
@@ -11647,7 +11819,7 @@ async function readJsonLines(file) {
11647
11819
  }
11648
11820
  }
11649
11821
  async function writeJsonLines(file, records) {
11650
- await mkdir9(dirname11(file), { recursive: true, mode: 448 });
11822
+ await mkdir9(dirname9(file), { recursive: true, mode: 448 });
11651
11823
  const body = records.map((record) => JSON.stringify(record)).join("\n");
11652
11824
  await writeFile7(file, body.length > 0 ? `${body}
11653
11825
  ` : "", { encoding: "utf8", mode: 384 });
@@ -11675,7 +11847,7 @@ async function readRules(file) {
11675
11847
  }
11676
11848
  }
11677
11849
  async function writeRules(file, rules) {
11678
- await mkdir9(dirname11(file), { recursive: true, mode: 448 });
11850
+ await mkdir9(dirname9(file), { recursive: true, mode: 448 });
11679
11851
  await writeFile7(file, JSON.stringify(rules, null, 2), { encoding: "utf8", mode: 384 });
11680
11852
  }
11681
11853
  var EvolveStore = class {
@@ -11915,9 +12087,9 @@ var USAGE = [
11915
12087
  " clear reset signals, done markers, and verified markers"
11916
12088
  ].join("\n");
11917
12089
  function runCommand(command, cwd, timeoutMs) {
11918
- return new Promise((resolve31) => {
12090
+ return new Promise((resolve32) => {
11919
12091
  const isWindows = process.platform === "win32";
11920
- const child = spawn6(isWindows ? "cmd.exe" : "/bin/sh", isWindows ? ["/d", "/s", "/c", command] : ["-c", command], {
12092
+ const child = spawn7(isWindows ? "cmd.exe" : "/bin/sh", isWindows ? ["/d", "/s", "/c", command] : ["-c", command], {
11921
12093
  cwd,
11922
12094
  windowsHide: true
11923
12095
  });
@@ -11925,7 +12097,7 @@ function runCommand(command, cwd, timeoutMs) {
11925
12097
  let stderr = "";
11926
12098
  const timer = setTimeout(() => {
11927
12099
  child.kill();
11928
- resolve31({ ok: false, stdout, stderr: `${stderr}
12100
+ resolve32({ ok: false, stdout, stderr: `${stderr}
11929
12101
  [killed: exceeded ${timeoutMs}ms]`, code: "timeout" });
11930
12102
  }, timeoutMs);
11931
12103
  child.stdout.on("data", (chunk) => {
@@ -11936,12 +12108,12 @@ function runCommand(command, cwd, timeoutMs) {
11936
12108
  });
11937
12109
  child.on("error", (error) => {
11938
12110
  clearTimeout(timer);
11939
- resolve31({ ok: false, stdout, stderr: `${stderr}
12111
+ resolve32({ ok: false, stdout, stderr: `${stderr}
11940
12112
  ${error.message}`, code: "spawn" });
11941
12113
  });
11942
12114
  child.on("close", (code) => {
11943
12115
  clearTimeout(timer);
11944
- resolve31({ ok: code === 0, stdout, stderr, code: code ?? "unknown" });
12116
+ resolve32({ ok: code === 0, stdout, stderr, code: code ?? "unknown" });
11945
12117
  });
11946
12118
  });
11947
12119
  }
@@ -12312,10 +12484,12 @@ ${result.stderr.slice(-4e3)}` : ""}`;
12312
12484
  name: "evolve_improve",
12313
12485
  description: "Implement a fix for one repeated tool failure. Default kind=plugin scaffolds the fix-<tool>/ plugin dir and writes PLAN.md with instructions for implementing, verifying, reloading, and testing it. kind=prompt_rule instead stores a concise guardrail rule (from `implementation`) that is injected into future system prompts and closes the suggestion. Use when the model proposes a concrete fix for a suggestion in the system prompt.",
12314
12486
  inputSchema,
12315
- paths: () => [],
12316
- execute: async (input) => {
12487
+ paths: (input) => [join15(workspace, ".flavor", "evolve"), ...input.kind === "prompt_rule" ? [] : [join15(workspace, ".flavor", "plugins")]],
12488
+ execute: async (input, signal) => {
12489
+ signal.throwIfAborted();
12317
12490
  const { suggestionId, implementation, kind } = input;
12318
12491
  const suggestions = await openWithTrends(100);
12492
+ signal.throwIfAborted();
12319
12493
  const suggestion = suggestions.find((item) => item.id === suggestionId);
12320
12494
  if (suggestion === void 0) throw new Error(`No open suggestion with id "${suggestionId}".`);
12321
12495
  if (kind === "prompt_rule") {
@@ -12432,6 +12606,14 @@ var LoopStateSchema = z17.object({
12432
12606
  approvals: z17.array(LoopApprovalSchema)
12433
12607
  }).strict(),
12434
12608
  cycles: z17.array(LoopCycleEvidenceSchema),
12609
+ /** Completed worker awaiting host verification; avoids replaying tool work after rotation. */
12610
+ pendingCycle: z17.object({
12611
+ cycle: z17.number().int().positive(),
12612
+ startedAt: z17.string().datetime({ offset: true }),
12613
+ inputTokens: z17.number().int().nonnegative(),
12614
+ outputTokens: z17.number().int().nonnegative(),
12615
+ workerText: z17.string().max(16e3)
12616
+ }).strict().nullable().optional(),
12435
12617
  terminalReason: z17.string().optional()
12436
12618
  }).strict();
12437
12619
  var LoopEventSchema = z17.object({
@@ -12520,15 +12702,45 @@ var LoopOrchestrator = class {
12520
12702
  tokenCheckpoint: this.#options.config.maxTokens,
12521
12703
  approvals: []
12522
12704
  },
12523
- cycles: []
12705
+ cycles: [],
12706
+ pendingCycle: null
12524
12707
  });
12525
12708
  await this.#options.persistence.create(state);
12709
+ yield* this.#execute(state, request.goal, request.signal);
12710
+ }
12711
+ /**
12712
+ * Continue a loop whose process rotated onto a fresh heap. The persisted
12713
+ * state decides where to pick up; an interrupted cycle simply runs again.
12714
+ */
12715
+ async *resume(request) {
12716
+ const persistence = this.#options.persistence;
12717
+ if (persistence.load === void 0) throw new Error("Loop persistence does not support resume");
12718
+ let state;
12526
12719
  try {
12720
+ state = await persistence.load(request.loopId);
12721
+ } catch (error) {
12722
+ yield { type: "loop-terminal", loopId: request.loopId, status: "failed", reason: message(error) };
12723
+ return;
12724
+ }
12725
+ if (state.status !== "running") {
12726
+ const status = state.status;
12727
+ yield { type: "loop-terminal", loopId: state.loopId, status, reason: state.terminalReason ?? "The loop is not running." };
12728
+ return;
12729
+ }
12730
+ yield* this.#execute(state, state.goal, request.signal);
12731
+ }
12732
+ async *#execute(initialState, goal, signal) {
12733
+ const now = this.#options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
12734
+ const loopId = initialState.loopId;
12735
+ let state = initialState;
12736
+ try {
12737
+ await this.#options.onMemoryCheckpoint?.(loopId);
12738
+ if (memoryRotationActive()) return;
12527
12739
  const workspaceResolution = await this.#options.prepareWorkspace({
12528
12740
  root: this.#options.workspace,
12529
12741
  loopId,
12530
- goal: request.goal,
12531
- signal: request.signal
12742
+ goal,
12743
+ signal
12532
12744
  });
12533
12745
  if (workspaceResolution.kind === "needs_human") {
12534
12746
  state = await this.#terminal(state, "needs_human", workspaceResolution.reason, now());
@@ -12546,67 +12758,87 @@ var LoopOrchestrator = class {
12546
12758
  cycleCheckpoint: state.budget.cycleCheckpoint,
12547
12759
  tokenCheckpoint: state.budget.tokenCheckpoint
12548
12760
  };
12549
- let previousVerification = plan.commands.length === 0 ? {
12761
+ const lastCompletedCycle = state.cycles.at(-1);
12762
+ let previousVerification = lastCompletedCycle?.verification ?? (plan.commands.length === 0 ? {
12550
12763
  passed: false,
12551
12764
  commands: [],
12552
12765
  summary: `${plan.needsHumanReason ?? "No deterministic verification command was found."} Inspect the project and establish a meaningful project-native deterministic check; never add a trivial pass-through verifier.`
12553
- } : void 0;
12554
- let previousFailureSignature;
12555
- let repeatedFailures = 0;
12766
+ } : void 0);
12767
+ let previousFailureSignature = lastCompletedCycle === void 0 ? void 0 : `${lastCompletedCycle.workspaceFingerprint}
12768
+ ${lastCompletedCycle.verification.summary}`;
12769
+ let repeatedFailures = previousFailureSignature === void 0 ? 0 : countTrailingFailureSignature(state.cycles, previousFailureSignature);
12556
12770
  while (state.status === "running") {
12557
- request.signal.throwIfAborted();
12558
- const cycle = state.budget.cyclesUsed + 1;
12559
- const startedAt = now();
12560
- await this.#options.persistence.append({ version: 1, type: "cycle_started", timestamp: startedAt, loopId, payload: { cycle } });
12561
- yield { type: "loop-cycle-start", loopId, cycle };
12562
- const prompt = buildLoopCyclePrompt({
12563
- goal: request.goal,
12564
- cycle,
12565
- ...previousVerification === void 0 ? {} : {
12566
- memory: `Previous host verification: ${previousVerification.summary}`,
12567
- verification: previousVerification
12568
- }
12569
- });
12570
- let cycleInputTokens = 0;
12571
- let cycleOutputTokens = 0;
12572
- let workerError;
12573
- let workerText = "";
12574
- for await (const event of this.#options.runWorker({
12575
- goal: request.goal,
12576
- cycle,
12577
- workspace: executionWorkspace.root,
12578
- prompt,
12579
- signal: request.signal
12580
- })) {
12581
- if (event.type === "usage") {
12582
- cycleInputTokens += event.inputTokens;
12583
- cycleOutputTokens += event.outputTokens;
12584
- }
12585
- if (event.type === "error") workerError = event.error.message;
12586
- if (event.type === "text") workerText += event.text;
12587
- if (this.#options.hallucinationGuard !== void 0) {
12588
- if (event.type === "tool-start") {
12589
- this.#options.hallucinationGuard.recordToolCall(event.name, event.input, event.id);
12590
- } else if (event.type === "tool-end") {
12591
- this.#options.hallucinationGuard.recordToolResult(event.name, event.result, event.id);
12771
+ signal.throwIfAborted();
12772
+ await this.#options.onCycleBoundary?.(loopId);
12773
+ if (memoryRotationActive()) return;
12774
+ const pendingCycle = state.pendingCycle;
12775
+ const cycle = pendingCycle?.cycle ?? state.budget.cyclesUsed + 1;
12776
+ const startedAt = pendingCycle?.startedAt ?? now();
12777
+ let cycleInputTokens = pendingCycle?.inputTokens ?? 0;
12778
+ let cycleOutputTokens = pendingCycle?.outputTokens ?? 0;
12779
+ let workerText = pendingCycle?.workerText ?? "";
12780
+ if (pendingCycle === void 0 || pendingCycle === null) {
12781
+ await this.#options.persistence.append({ version: 1, type: "cycle_started", timestamp: startedAt, loopId, payload: { cycle } });
12782
+ yield { type: "loop-cycle-start", loopId, cycle };
12783
+ const prompt = buildLoopCyclePrompt({
12784
+ goal,
12785
+ cycle,
12786
+ ...previousVerification === void 0 ? {} : {
12787
+ memory: `Previous host verification: ${previousVerification.summary}`,
12788
+ verification: previousVerification
12592
12789
  }
12790
+ });
12791
+ let workerError;
12792
+ let workerMemoryPressure = false;
12793
+ for await (const event of this.#options.runWorker({
12794
+ goal,
12795
+ cycle,
12796
+ workspace: executionWorkspace.root,
12797
+ prompt,
12798
+ signal
12799
+ })) {
12800
+ if (event.type === "usage") {
12801
+ cycleInputTokens += event.inputTokens;
12802
+ cycleOutputTokens += event.outputTokens;
12803
+ }
12804
+ if (event.type === "error") {
12805
+ workerError = event.error.message;
12806
+ if (event.error.code === "memory_pressure") workerMemoryPressure = true;
12807
+ }
12808
+ if (event.type === "text") workerText = `${workerText}${event.text}`.slice(-16e3);
12809
+ if (this.#options.hallucinationGuard !== void 0) {
12810
+ if (event.type === "tool-start") {
12811
+ this.#options.hallucinationGuard.recordToolCall(event.name, event.input, event.id);
12812
+ } else if (event.type === "tool-end") {
12813
+ this.#options.hallucinationGuard.recordToolResult(event.name, event.result, event.id);
12814
+ }
12815
+ }
12816
+ yield { type: "worker-event", event };
12817
+ }
12818
+ if (workerMemoryPressure) return;
12819
+ if (workerError !== void 0) {
12820
+ state = LoopStateSchema.parse({
12821
+ ...state,
12822
+ budget: {
12823
+ ...state.budget,
12824
+ cyclesUsed: cycle,
12825
+ inputTokens: state.budget.inputTokens + cycleInputTokens,
12826
+ outputTokens: state.budget.outputTokens + cycleOutputTokens
12827
+ }
12828
+ });
12829
+ state = await this.#terminal(state, "failed", workerError, now());
12830
+ yield { type: "loop-terminal", loopId, status: "failed", reason: workerError };
12831
+ return;
12593
12832
  }
12594
- yield { type: "worker-event", event };
12595
- }
12596
- if (workerError !== void 0) {
12597
12833
  state = LoopStateSchema.parse({
12598
12834
  ...state,
12599
- budget: {
12600
- ...state.budget,
12601
- cyclesUsed: cycle,
12602
- inputTokens: state.budget.inputTokens + cycleInputTokens,
12603
- outputTokens: state.budget.outputTokens + cycleOutputTokens
12604
- }
12835
+ pendingCycle: { cycle, startedAt, inputTokens: cycleInputTokens, outputTokens: cycleOutputTokens, workerText },
12836
+ updatedAt: now()
12605
12837
  });
12606
- state = await this.#terminal(state, "failed", workerError, now());
12607
- yield { type: "loop-terminal", loopId, status: "failed", reason: workerError };
12608
- return;
12838
+ await this.#options.persistence.save(state);
12609
12839
  }
12840
+ await this.#options.onMemoryCheckpoint?.(loopId);
12841
+ if (memoryRotationActive()) return;
12610
12842
  if (plan.commands.length === 0) {
12611
12843
  plan = await this.#options.inferVerification(executionWorkspace.root);
12612
12844
  if (plan.commands.length > 0) {
@@ -12626,8 +12858,28 @@ var LoopOrchestrator = class {
12626
12858
  passed: false,
12627
12859
  commands: [],
12628
12860
  summary: plan.needsHumanReason ?? "No deterministic verification command was found after the discovery cycle."
12629
- } : await this.#options.runVerifier(plan, executionWorkspace.root, request.signal);
12861
+ } : await this.#options.runVerifier(plan, executionWorkspace.root, signal);
12862
+ await this.#options.onMemoryCheckpoint?.(loopId);
12863
+ if (memoryRotationActive()) return;
12630
12864
  const fingerprint2 = await this.#options.fingerprint(executionWorkspace.root);
12865
+ let guardWarnings = [];
12866
+ if (evidence.passed && this.#options.hallucinationGuard !== void 0) {
12867
+ try {
12868
+ await this.#options.onMemoryCheckpoint?.(loopId);
12869
+ if (memoryRotationActive()) return;
12870
+ const report = await this.#options.hallucinationGuard.evaluate(goal, workerText);
12871
+ await this.#options.onMemoryCheckpoint?.(loopId);
12872
+ if (memoryRotationActive()) return;
12873
+ if (!report.passed) {
12874
+ const guardReason = report.blockingReasons.join("; ") || "Hallucination guard blocked completion.";
12875
+ state = await this.#terminal(state, "failed", guardReason, now());
12876
+ yield { type: "loop-terminal", loopId, status: "failed", reason: guardReason };
12877
+ return;
12878
+ }
12879
+ guardWarnings = report.warnings;
12880
+ } catch {
12881
+ }
12882
+ }
12631
12883
  const completedAt = now();
12632
12884
  state = LoopStateSchema.parse({
12633
12885
  ...state,
@@ -12646,7 +12898,8 @@ var LoopOrchestrator = class {
12646
12898
  outputTokens: cycleOutputTokens,
12647
12899
  workspaceFingerprint: fingerprint2,
12648
12900
  verification: evidence
12649
- }]
12901
+ }],
12902
+ pendingCycle: null
12650
12903
  });
12651
12904
  await this.#options.persistence.save(state);
12652
12905
  await this.#options.persistence.append({
@@ -12664,20 +12917,6 @@ var LoopOrchestrator = class {
12664
12917
  return;
12665
12918
  }
12666
12919
  if (evidence.passed) {
12667
- let guardWarnings = [];
12668
- if (this.#options.hallucinationGuard !== void 0) {
12669
- try {
12670
- const report = await this.#options.hallucinationGuard.evaluate(request.goal, workerText);
12671
- if (!report.passed) {
12672
- const guardReason = report.blockingReasons.join("; ") || "Hallucination guard blocked completion.";
12673
- state = await this.#terminal(state, "failed", guardReason, now());
12674
- yield { type: "loop-terminal", loopId, status: "failed", reason: guardReason };
12675
- return;
12676
- }
12677
- guardWarnings = report.warnings;
12678
- } catch {
12679
- }
12680
- }
12681
12920
  const reason = [evidence.summary, ...guardWarnings].filter(Boolean).join("; ");
12682
12921
  state = await this.#terminal(state, "succeeded", reason, now());
12683
12922
  yield { type: "loop-terminal", loopId, status: "succeeded", reason };
@@ -12703,7 +12942,7 @@ ${evidence.summary}`;
12703
12942
  cycleCheckpoint: state.budget.cycleCheckpoint,
12704
12943
  tokenCheckpoint: state.budget.tokenCheckpoint
12705
12944
  };
12706
- const confirmation = await this.#options.confirmBudget(state, decision.dimensions, request.signal);
12945
+ const confirmation = await this.#options.confirmBudget(state, decision.dimensions, signal);
12707
12946
  if (confirmation === "unavailable") {
12708
12947
  const reason = "Loop budget extension requires user confirmation.";
12709
12948
  state = await this.#terminal(state, "needs_human", reason, now());
@@ -12730,8 +12969,9 @@ ${evidence.summary}`;
12730
12969
  }
12731
12970
  }
12732
12971
  } catch (error) {
12733
- const status = request.signal.aborted ? "cancelled" : "failed";
12734
- const reason = request.signal.aborted ? "Loop cancelled by user." : message(error);
12972
+ if (memoryRotationActive()) return;
12973
+ const status = signal.aborted ? "cancelled" : "failed";
12974
+ const reason = signal.aborted ? "Loop cancelled by user." : message(error);
12735
12975
  state = await this.#terminal(state, status, reason, now());
12736
12976
  yield { type: "loop-terminal", loopId, status, reason };
12737
12977
  }
@@ -12749,6 +12989,16 @@ ${evidence.summary}`;
12749
12989
  return state;
12750
12990
  }
12751
12991
  };
12992
+ function countTrailingFailureSignature(cycles, signature) {
12993
+ let count = 0;
12994
+ for (let index = cycles.length - 1; index >= 0; index -= 1) {
12995
+ const cycle = cycles[index];
12996
+ if (`${cycle.workspaceFingerprint}
12997
+ ${cycle.verification.summary}` !== signature) break;
12998
+ count += 1;
12999
+ }
13000
+ return count;
13001
+ }
12752
13002
 
12753
13003
  // src/goal/orchestrator.ts
12754
13004
  import { createHash as createHash11 } from "crypto";
@@ -12816,6 +13066,11 @@ var GoalStateSchema = z18.object({
12816
13066
  planPath: z18.string().nullable(),
12817
13067
  verifyRounds: z18.number().int().min(0),
12818
13068
  workerRounds: z18.number().int().min(0),
13069
+ /** Worker output checkpoint used to resume verification without rerunning tools. */
13070
+ pendingVerification: z18.object({
13071
+ round: z18.number().int().positive(),
13072
+ finalResponse: z18.string().max(16e3)
13073
+ }).strict().nullable().optional(),
12819
13074
  lastGaps: z18.array(GapSchema),
12820
13075
  gapFingerprint: z18.string(),
12821
13076
  stallStreak: z18.number().int().min(0),
@@ -12861,6 +13116,7 @@ ${objective}`
12861
13116
  ].join("\n");
12862
13117
  }
12863
13118
  async function runPlanner(options) {
13119
+ await options.onProgress?.();
12864
13120
  const { adapter, model } = options.registry.get(options.modelId);
12865
13121
  const prompt = buildPlannerPrompt(options.objective);
12866
13122
  const messages = [{ role: "user", content: prompt }];
@@ -12872,6 +13128,7 @@ async function runPlanner(options) {
12872
13128
  tools: [],
12873
13129
  ...options.signal === void 0 ? {} : { signal: options.signal }
12874
13130
  })) {
13131
+ await options.onProgress?.();
12875
13132
  if (event.type === "text") text += event.text;
12876
13133
  else if (event.type === "error") throw event.error;
12877
13134
  else if (event.type === "done") {
@@ -12978,7 +13235,7 @@ async function runClassifier(evidence, plan, options) {
12978
13235
  const skepticResults = await Promise.all(
12979
13236
  Array.from(
12980
13237
  { length: n },
12981
- (_, i) => runOneSkeptic(evidence, plan, i + 1, adapter, model, options.signal)
13238
+ (_, i) => runOneSkeptic(evidence, plan, i + 1, adapter, model, options.signal, options.onProgress)
12982
13239
  )
12983
13240
  );
12984
13241
  const verdicts = [];
@@ -12995,7 +13252,8 @@ async function runClassifier(evidence, plan, options) {
12995
13252
  }
12996
13253
  return aggregate(verdicts);
12997
13254
  }
12998
- async function runOneSkeptic(evidence, plan, index, adapter, model, signal) {
13255
+ async function runOneSkeptic(evidence, plan, index, adapter, model, signal, onProgress) {
13256
+ await onProgress?.();
12999
13257
  const prompt = buildSkepticPrompt(evidence, plan, index);
13000
13258
  const messages = [{ role: "user", content: prompt }];
13001
13259
  let text = "";
@@ -13005,6 +13263,7 @@ async function runOneSkeptic(evidence, plan, index, adapter, model, signal) {
13005
13263
  tools: [],
13006
13264
  ...signal === void 0 ? {} : { signal }
13007
13265
  })) {
13266
+ await onProgress?.();
13008
13267
  if (event.type === "text") text += event.text;
13009
13268
  else if (event.type === "error") throw event.error;
13010
13269
  else if (event.type === "done") break;
@@ -13159,7 +13418,7 @@ var GoalOrchestrator = class {
13159
13418
  async *run(request) {
13160
13419
  const timestamp2 = () => this.#options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
13161
13420
  const createdAt = timestamp2();
13162
- let state = {
13421
+ const state = {
13163
13422
  id: this.#options.idFactory?.() ?? `goal-${Date.now().toString(36)}`,
13164
13423
  objective: request.goal,
13165
13424
  phase: "planning",
@@ -13168,6 +13427,7 @@ var GoalOrchestrator = class {
13168
13427
  planPath: null,
13169
13428
  verifyRounds: 0,
13170
13429
  workerRounds: 0,
13430
+ pendingVerification: null,
13171
13431
  lastGaps: [],
13172
13432
  gapFingerprint: "",
13173
13433
  stallStreak: 0,
@@ -13176,87 +13436,147 @@ var GoalOrchestrator = class {
13176
13436
  createdAt,
13177
13437
  updatedAt: createdAt
13178
13438
  };
13439
+ yield* this.#execute(state, request.signal);
13440
+ }
13441
+ /**
13442
+ * Continue a goal whose process rotated onto a fresh heap. The persisted
13443
+ * state decides where to pick up; an interrupted round simply runs again.
13444
+ */
13445
+ async *resume(request) {
13446
+ const persistence = this.#options.persistence;
13447
+ if (persistence === void 0 || persistence.load === void 0) {
13448
+ throw new Error("Goal persistence does not support resume");
13449
+ }
13450
+ let state;
13451
+ try {
13452
+ state = await persistence.load(request.goalId);
13453
+ } catch (error) {
13454
+ yield { type: "goal-failed", reason: message(error) };
13455
+ return;
13456
+ }
13457
+ if (state.status !== "active" && state.status !== "not_achieved") {
13458
+ yield { type: "goal-failed", reason: `Goal "${state.id}" is ${state.status}; it cannot be resumed.` };
13459
+ return;
13460
+ }
13461
+ yield { type: "goal-resumed", goalId: state.id, round: resumeStartRound(state) };
13462
+ yield* this.#execute(state, request.signal);
13463
+ }
13464
+ async *#execute(initialState, signal) {
13465
+ const timestamp2 = () => this.#options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString();
13466
+ let state = initialState;
13179
13467
  const persistState = async (patch) => {
13180
13468
  state = { ...state, ...patch, updatedAt: timestamp2() };
13181
13469
  await this.#options.persistence?.save(state);
13182
13470
  };
13471
+ const memoryCheckpoint = async () => {
13472
+ await this.#options.onMemoryCheckpoint?.(state.id);
13473
+ if (memoryRotationActive()) throw new Error("Heap rotation requested");
13474
+ };
13183
13475
  await persistState();
13184
13476
  const workspace = this.#options.workspace;
13185
13477
  let plan;
13186
13478
  let planPath;
13187
- try {
13188
- request.signal.throwIfAborted();
13189
- plan = await runPlanner({
13190
- registry: this.#options.registry,
13191
- modelId: this.#options.plannerModelId,
13192
- objective: request.goal,
13193
- signal: request.signal
13194
- });
13195
- planPath = await writePlanFile(workspace, plan);
13196
- await persistState({ plan, planPath, contractHash: contractHash(request.goal, plan) });
13197
- yield { type: "goal-plan-created", plan, planPath };
13198
- } catch (error) {
13199
- const reason = `Goal planning failed: ${message(error)}`;
13200
- await persistState({ phase: "complete", status: "failed" });
13201
- yield { type: "goal-plan-failed", reason };
13202
- yield { type: "goal-failed", reason };
13203
- return;
13204
- }
13205
- let priorGaps = [];
13206
- let priorFingerprint = "";
13207
- let stallStreak = 0;
13208
- for (let round = 1; round <= this.#options.maxRounds; round++) {
13209
- request.signal.throwIfAborted();
13210
- const workerPrompt = buildWorkerPrompt(request.goal, plan, priorGaps, round);
13211
- await persistState({
13212
- phase: "executing",
13213
- status: "active",
13214
- workerRounds: round,
13215
- lastGaps: priorGaps,
13216
- gapFingerprint: priorFingerprint,
13217
- stallStreak
13218
- });
13219
- yield { type: "goal-worker-start", round };
13220
- let finalResponse = "";
13221
- let workerError;
13479
+ if (state.plan === null) {
13222
13480
  try {
13223
- for await (const event of this.#options.runWorker({
13224
- goal: request.goal,
13225
- round,
13226
- workspace,
13227
- prompt: workerPrompt,
13228
- priorGaps: formatPriorGaps(priorGaps),
13229
- signal: request.signal
13230
- })) {
13231
- yield { type: "goal-worker-event", round, event };
13232
- if (event.type === "text") finalResponse += event.text;
13233
- if (event.type === "error") {
13234
- workerError = event.error.message;
13235
- break;
13236
- }
13237
- }
13481
+ signal.throwIfAborted();
13482
+ await memoryCheckpoint();
13483
+ plan = await runPlanner({
13484
+ registry: this.#options.registry,
13485
+ modelId: this.#options.plannerModelId,
13486
+ objective: state.objective,
13487
+ signal,
13488
+ onProgress: memoryCheckpoint
13489
+ });
13490
+ planPath = await writePlanFile(workspace, plan);
13491
+ await persistState({ phase: "executing", plan, planPath, contractHash: contractHash(state.objective, plan) });
13492
+ yield { type: "goal-plan-created", plan, planPath };
13238
13493
  } catch (error) {
13239
- workerError = message(error);
13240
- }
13241
- if (workerError !== void 0) {
13242
- const reason = `Worker error in round ${round}: ${workerError}`;
13494
+ if (memoryRotationActive()) return;
13495
+ const reason = `Goal planning failed: ${message(error)}`;
13243
13496
  await persistState({ phase: "complete", status: "failed" });
13497
+ yield { type: "goal-plan-failed", reason };
13244
13498
  yield { type: "goal-failed", reason };
13245
13499
  return;
13246
13500
  }
13247
- await persistState({ phase: "verifying", status: "active", verifyRounds: round });
13501
+ } else {
13502
+ plan = state.plan;
13503
+ planPath = state.planPath ?? await writePlanFile(workspace, plan);
13504
+ await persistState({ phase: "executing", status: "active", ...state.planPath === null ? { planPath } : {} });
13505
+ }
13506
+ let priorGaps = state.lastGaps;
13507
+ let priorFingerprint = state.gapFingerprint;
13508
+ let stallStreak = state.stallStreak;
13509
+ const startRound = resumeStartRound(state);
13510
+ for (let round = startRound; round <= this.#options.maxRounds; round++) {
13511
+ signal.throwIfAborted();
13512
+ await this.#options.onRoundBoundary?.(state.id);
13513
+ if (memoryRotationActive()) return;
13514
+ const pendingVerification = state.pendingVerification?.round === round ? state.pendingVerification : void 0;
13515
+ let finalResponse = pendingVerification?.finalResponse ?? "";
13516
+ if (pendingVerification === void 0) {
13517
+ const workerPrompt = buildWorkerPrompt(state.objective, plan, priorGaps, round);
13518
+ await persistState({
13519
+ phase: "executing",
13520
+ status: "active",
13521
+ workerRounds: round,
13522
+ pendingVerification: null,
13523
+ lastGaps: priorGaps,
13524
+ gapFingerprint: priorFingerprint,
13525
+ stallStreak
13526
+ });
13527
+ yield { type: "goal-worker-start", round };
13528
+ let workerError;
13529
+ let workerMemoryPressure = false;
13530
+ try {
13531
+ for await (const event of this.#options.runWorker({
13532
+ goal: state.objective,
13533
+ round,
13534
+ workspace,
13535
+ prompt: workerPrompt,
13536
+ priorGaps: formatPriorGaps(priorGaps),
13537
+ signal
13538
+ })) {
13539
+ yield { type: "goal-worker-event", round, event };
13540
+ if (event.type === "text") finalResponse = `${finalResponse}${event.text}`.slice(-16e3);
13541
+ if (event.type === "error") {
13542
+ workerError = event.error.message;
13543
+ if (event.error.code === "memory_pressure") workerMemoryPressure = true;
13544
+ break;
13545
+ }
13546
+ }
13547
+ } catch (error) {
13548
+ if (memoryRotationActive()) return;
13549
+ workerError = message(error);
13550
+ }
13551
+ if (workerMemoryPressure) return;
13552
+ if (workerError !== void 0) {
13553
+ const reason = `Worker error in round ${round}: ${workerError}`;
13554
+ await persistState({ phase: "complete", status: "failed" });
13555
+ yield { type: "goal-failed", reason };
13556
+ return;
13557
+ }
13558
+ await persistState({
13559
+ phase: "verifying",
13560
+ status: "active",
13561
+ pendingVerification: { round, finalResponse }
13562
+ });
13563
+ }
13248
13564
  yield { type: "goal-verification-start", round };
13249
- const hostVerification = await this.#options.verifyHost?.(request.signal);
13565
+ await this.#options.onMemoryCheckpoint?.(state.id);
13566
+ if (memoryRotationActive()) return;
13567
+ const hostVerification = await this.#options.verifyHost?.(signal);
13568
+ await this.#options.onMemoryCheckpoint?.(state.id);
13569
+ if (memoryRotationActive()) return;
13250
13570
  const evidence = await collectEvidence(
13251
13571
  workspace,
13252
- request.goal,
13572
+ state.objective,
13253
13573
  finalResponse.slice(-4e3),
13254
13574
  // Last 4000 chars of response
13255
13575
  formatPriorGaps(priorGaps),
13256
13576
  state.contractHash,
13257
13577
  hostVerification
13258
13578
  );
13259
- await persistState({ evidenceRounds: [...state.evidenceRounds, {
13579
+ await persistState({ evidenceRounds: [...state.evidenceRounds.filter((item) => item.round !== round), {
13260
13580
  round,
13261
13581
  workspaceDiffHash: evidence.workspaceDiffHash,
13262
13582
  ...hostVerification === void 0 ? {} : { hostVerification }
@@ -13280,14 +13600,17 @@ var GoalOrchestrator = class {
13280
13600
  modelId: this.#options.classifierModelId,
13281
13601
  skepticCount: this.#options.skepticCount,
13282
13602
  workspace,
13283
- signal: request.signal
13603
+ signal,
13604
+ onProgress: memoryCheckpoint
13284
13605
  });
13285
13606
  } catch (error) {
13607
+ if (memoryRotationActive()) return;
13286
13608
  outcome = {
13287
13609
  type: "blocked",
13288
13610
  reason: `Classifier infrastructure error: ${message(error)}`
13289
13611
  };
13290
13612
  }
13613
+ await persistState({ verifyRounds: round, pendingVerification: null });
13291
13614
  yield {
13292
13615
  type: "goal-verdict",
13293
13616
  round,
@@ -13347,6 +13670,12 @@ var GoalOrchestrator = class {
13347
13670
  function contractHash(objective, plan) {
13348
13671
  return createHash11("sha256").update(JSON.stringify({ objective, plan })).digest("hex");
13349
13672
  }
13673
+ function resumeStartRound(state) {
13674
+ if (state.pendingVerification !== void 0 && state.pendingVerification !== null) {
13675
+ return state.pendingVerification.round;
13676
+ }
13677
+ return state.verifyRounds >= state.workerRounds ? state.workerRounds + 1 : Math.max(state.workerRounds, 1);
13678
+ }
13350
13679
  async function writePlanFile(workspace, plan) {
13351
13680
  const dir = join17(workspace, ".flavor");
13352
13681
  const planPath = join17(dir, "goal-plan.md");
@@ -13426,8 +13755,8 @@ function formatPriorGaps(gaps) {
13426
13755
  // src/goal/store.ts
13427
13756
  import { constants as constants4 } from "fs";
13428
13757
  import { randomUUID as randomUUID11 } from "crypto";
13429
- import { lstat as lstat7, mkdir as mkdir11, open as open6, rename as rename4, rm as rm4 } from "fs/promises";
13430
- import { dirname as dirname12, join as join18, relative as relative11, resolve as resolve14, sep as sep11 } from "path";
13758
+ import { lstat as lstat7, mkdir as mkdir11, open as open6, readFile as readFile11, rename as rename4, rm as rm4, stat as stat6 } from "fs/promises";
13759
+ import { dirname as dirname10, join as join18, relative as relative11, resolve as resolve14, sep as sep11 } from "path";
13431
13760
  var DEFAULT_MAX_BYTES = 1024 * 1024;
13432
13761
  var GoalStore = class {
13433
13762
  #workspace;
@@ -13440,7 +13769,7 @@ var GoalStore = class {
13440
13769
  }
13441
13770
  async save(input) {
13442
13771
  const state = GoalStateSchema.parse(input);
13443
- await assertNoSymlink(this.#workspace, dirname12(this.#root));
13772
+ await assertNoSymlink(this.#workspace, dirname10(this.#root));
13444
13773
  await mkdir11(this.#root, { recursive: true, mode: 448 });
13445
13774
  await assertNoSymlink(this.#workspace, this.#root);
13446
13775
  const target = join18(this.#root, `${state.id}.json`);
@@ -13463,6 +13792,28 @@ var GoalStore = class {
13463
13792
  await rm4(temporary, { force: true }).catch(() => void 0);
13464
13793
  }
13465
13794
  }
13795
+ /** Reload a persisted goal so a rotated process can resume it. */
13796
+ async load(goalId) {
13797
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(goalId)) {
13798
+ throw new Error(`Goal "${goalId}" was not found`);
13799
+ }
13800
+ await assertNoSymlink(this.#workspace, dirname10(this.#root));
13801
+ await assertNoSymlink(this.#workspace, this.#root);
13802
+ const target = join18(this.#root, `${goalId}.json`);
13803
+ try {
13804
+ const metadata = await stat6(target);
13805
+ if (!metadata.isFile() || metadata.size > this.#maxBytes) {
13806
+ throw new Error("Goal state is not a bounded regular file");
13807
+ }
13808
+ const raw = await readFile11(target, "utf8");
13809
+ return GoalStateSchema.parse(JSON.parse(raw));
13810
+ } catch (error) {
13811
+ if (isCode3(error, "ENOENT")) throw new Error(`Goal "${goalId}" was not found`);
13812
+ const quarantine = `${target}.corrupt-${Date.now()}-${randomUUID11().slice(0, 8)}`;
13813
+ await rename4(target, quarantine).catch(() => void 0);
13814
+ throw new Error(`Goal "${goalId}" is corrupt and was quarantined`);
13815
+ }
13816
+ }
13466
13817
  };
13467
13818
  async function assertNoSymlink(root, target) {
13468
13819
  let cursor = resolve14(target);
@@ -13474,7 +13825,7 @@ async function assertNoSymlink(root, target) {
13474
13825
  const chain = [];
13475
13826
  while (cursor !== stop) {
13476
13827
  chain.push(cursor);
13477
- cursor = dirname12(cursor);
13828
+ cursor = dirname10(cursor);
13478
13829
  }
13479
13830
  for (const path of chain.reverse()) {
13480
13831
  try {
@@ -13489,7 +13840,7 @@ function isCode3(error, code) {
13489
13840
  }
13490
13841
 
13491
13842
  // src/loop/isolation.ts
13492
- import { appendFile as appendFile2, mkdir as mkdir12, readFile as readFile11 } from "fs/promises";
13843
+ import { appendFile as appendFile2, mkdir as mkdir12, readFile as readFile12 } from "fs/promises";
13493
13844
  import { join as join19, resolve as resolve15 } from "path";
13494
13845
  var CODE_CHANGE = /\b(?:fix|implement|add|update|change|modify|refactor|build|create|remove|delete|write|migrate)\b|修复|实现|添加|更新|修改|重构|创建|删除|迁移/i;
13495
13846
  var READ_ONLY = /\b(?:review|analy[sz]e|inspect|explain|research|investigate|audit|summari[sz]e|report)\b|调研|分析|审查|检查|解释|研究|总结|报告/i;
@@ -13549,7 +13900,7 @@ async function ensureWorktreeDirectoryIgnored(root) {
13549
13900
  await mkdir12(infoDirectory, { recursive: true });
13550
13901
  let existing = "";
13551
13902
  try {
13552
- existing = await readFile11(excludePath, "utf8");
13903
+ existing = await readFile12(excludePath, "utf8");
13553
13904
  } catch (error) {
13554
13905
  if (!(typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")) throw error;
13555
13906
  }
@@ -13560,8 +13911,8 @@ async function ensureWorktreeDirectoryIgnored(root) {
13560
13911
 
13561
13912
  // src/loop/store.ts
13562
13913
  import { constants as constants5 } from "fs";
13563
- import { lstat as lstat8, mkdir as mkdir13, open as open7, readFile as readFile12, realpath as realpath5, rename as rename5, rm as rm5, stat as stat6 } from "fs/promises";
13564
- import { dirname as dirname13, join as join20, relative as relative12, resolve as resolve16, sep as sep12 } from "path";
13914
+ import { lstat as lstat8, mkdir as mkdir13, open as open7, readFile as readFile13, realpath as realpath5, rename as rename5, rm as rm5, stat as stat7 } from "fs/promises";
13915
+ import { dirname as dirname11, join as join20, relative as relative12, resolve as resolve16, sep as sep12 } from "path";
13565
13916
  import { randomUUID as randomUUID12 } from "crypto";
13566
13917
  var LOOP_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
13567
13918
  var DEFAULT_MAX_BYTES2 = 5 * 1024 * 1024;
@@ -13611,9 +13962,9 @@ var LoopStore = class {
13611
13962
  const directory = await this.#safeExisting(loopId);
13612
13963
  const target = join20(directory, "state.json");
13613
13964
  try {
13614
- const metadata = await stat6(target);
13965
+ const metadata = await stat7(target);
13615
13966
  if (!metadata.isFile() || metadata.size > this.#maxBytes) throw new Error("Loop state is not a bounded regular file");
13616
- const raw = await readFile12(target, "utf8");
13967
+ const raw = await readFile13(target, "utf8");
13617
13968
  const state = LoopStateSchema.parse(JSON.parse(raw));
13618
13969
  await this.#assertWorkspace(state.workspace);
13619
13970
  return state;
@@ -13643,7 +13994,7 @@ var LoopStore = class {
13643
13994
  }
13644
13995
  async #prepare(loopId) {
13645
13996
  validateId(loopId);
13646
- await assertNoSymlink2(this.#workspace, dirname13(this.#root));
13997
+ await assertNoSymlink2(this.#workspace, dirname11(this.#root));
13647
13998
  await mkdir13(this.#root, { recursive: true, mode: 448 });
13648
13999
  await assertNoSymlink2(this.#workspace, this.#root);
13649
14000
  const directory = join20(this.#root, loopId);
@@ -13676,7 +14027,7 @@ async function assertNoSymlink2(root, target) {
13676
14027
  const chain = [];
13677
14028
  while (cursor !== stop) {
13678
14029
  chain.push(cursor);
13679
- cursor = dirname13(cursor);
14030
+ cursor = dirname11(cursor);
13680
14031
  }
13681
14032
  for (const path of chain.reverse()) {
13682
14033
  try {
@@ -13702,7 +14053,7 @@ function isCode4(error, code) {
13702
14053
  }
13703
14054
 
13704
14055
  // src/loop/verifier.ts
13705
- import { readFile as readFile13 } from "fs/promises";
14056
+ import { readFile as readFile14 } from "fs/promises";
13706
14057
  import { join as join21 } from "path";
13707
14058
  var SCRIPT_PRIORITY = ["test", "typecheck", "lint", "build", "smoke:install"];
13708
14059
  async function inferVerificationPlan(workspace) {
@@ -13781,7 +14132,7 @@ function isUnconditionalPassThrough(script) {
13781
14132
  }
13782
14133
  async function optionalText(path) {
13783
14134
  try {
13784
- return await readFile13(path, "utf8");
14135
+ return await readFile14(path, "utf8");
13785
14136
  } catch (error) {
13786
14137
  if (isCode5(error, "ENOENT")) return void 0;
13787
14138
  throw error;
@@ -13802,7 +14153,7 @@ function isCode5(error, code) {
13802
14153
 
13803
14154
  // src/models/anthropic.ts
13804
14155
  import Anthropic from "@anthropic-ai/sdk";
13805
- import { readFile as readFile14 } from "fs/promises";
14156
+ import { readFile as readFile15 } from "fs/promises";
13806
14157
 
13807
14158
  // src/utils/json.ts
13808
14159
  function safeParseJson(input) {
@@ -14039,7 +14390,7 @@ var AnthropicModelAdapter = class {
14039
14390
  source: {
14040
14391
  type: "base64",
14041
14392
  media_type: block.mediaType,
14042
- data: (await readFile14(block.source.path)).toString("base64")
14393
+ data: (await readFile15(block.source.path)).toString("base64")
14043
14394
  }
14044
14395
  }));
14045
14396
  if (message2.cacheBreakpoint && Array.isArray(content) && content.length > 0) {
@@ -14259,7 +14610,7 @@ function resolveCacheProfile(input) {
14259
14610
 
14260
14611
  // src/models/openai.ts
14261
14612
  import OpenAI from "openai";
14262
- import { readFile as readFile15 } from "fs/promises";
14613
+ import { readFile as readFile16 } from "fs/promises";
14263
14614
  async function toInput(message2) {
14264
14615
  if (message2.role === "tool") {
14265
14616
  if (!message2.toolCallId) throw new Error("Tool messages require toolCallId");
@@ -14271,7 +14622,7 @@ async function toInput(message2) {
14271
14622
  }
14272
14623
  const content = typeof message2.content === "string" ? message2.content : await Promise.all(message2.content.map(async (block) => block.type === "text" ? { type: "input_text", text: block.text } : {
14273
14624
  type: "input_image",
14274
- image_url: `data:${block.mediaType};base64,${(await readFile15(block.source.path)).toString("base64")}`,
14625
+ image_url: `data:${block.mediaType};base64,${(await readFile16(block.source.path)).toString("base64")}`,
14275
14626
  detail: "auto"
14276
14627
  }));
14277
14628
  return [
@@ -14908,8 +15259,8 @@ var OAuthCallbackAuthProvider = class {
14908
15259
  async #startCallbackServer(expectedState, signal) {
14909
15260
  let serverResolve;
14910
15261
  let serverReject;
14911
- const codePromise = new Promise((resolve31, reject) => {
14912
- serverResolve = resolve31;
15262
+ const codePromise = new Promise((resolve32, reject) => {
15263
+ serverResolve = resolve32;
14913
15264
  serverReject = reject;
14914
15265
  });
14915
15266
  const server = createServer3((req, res) => {
@@ -14943,7 +15294,7 @@ var OAuthCallbackAuthProvider = class {
14943
15294
  );
14944
15295
  serverResolve(code);
14945
15296
  });
14946
- return new Promise((resolve31, reject) => {
15297
+ return new Promise((resolve32, reject) => {
14947
15298
  server.once("error", (error) => {
14948
15299
  reject(new Error(`OAuth callback server failed: ${error.message}`));
14949
15300
  });
@@ -14960,7 +15311,7 @@ var OAuthCallbackAuthProvider = class {
14960
15311
  signal.addEventListener("abort", () => {
14961
15312
  serverReject(new Error("OAuth flow aborted"));
14962
15313
  }, { once: true });
14963
- resolve31({ redirectUri, promise: codePromise });
15314
+ resolve32({ redirectUri, promise: codePromise });
14964
15315
  });
14965
15316
  });
14966
15317
  }
@@ -15042,7 +15393,7 @@ function escapeHtml(text) {
15042
15393
  }
15043
15394
 
15044
15395
  // src/auth/store.ts
15045
- import { dirname as dirname14 } from "path";
15396
+ import { dirname as dirname12 } from "path";
15046
15397
  async function retainOnlyCredentials(store, keepCredentialId) {
15047
15398
  const tokens = await store.load();
15048
15399
  const retained = Object.fromEntries(Object.entries(tokens).filter(([id]) => id === keepCredentialId));
@@ -15053,7 +15404,7 @@ function createFileTokenStore(filePath) {
15053
15404
  const store = {
15054
15405
  tokens: {},
15055
15406
  async load() {
15056
- const key = await loadOrCreateConfigKey(dirname14(filePath));
15407
+ const key = await loadOrCreateConfigKey(dirname12(filePath));
15057
15408
  const result = await readRecoverableFile(filePath, (raw) => parseTokens(decryptDocument(raw, key)));
15058
15409
  const tokens = result?.value ?? {};
15059
15410
  baseline = { ...tokens };
@@ -15061,7 +15412,7 @@ function createFileTokenStore(filePath) {
15061
15412
  return tokens;
15062
15413
  },
15063
15414
  async save(tokens) {
15064
- const key = await loadOrCreateConfigKey(dirname14(filePath));
15415
+ const key = await loadOrCreateConfigKey(dirname12(filePath));
15065
15416
  const removals = Object.keys(baseline).filter((providerId) => !(providerId in tokens));
15066
15417
  const upserts = Object.fromEntries(Object.entries(tokens).filter(
15067
15418
  ([providerId, token]) => !sameToken(baseline[providerId], token)
@@ -15102,7 +15453,7 @@ function isStoredToken(value) {
15102
15453
  }
15103
15454
 
15104
15455
  // src/permissions/policy.ts
15105
- import { readFile as readFile16 } from "fs/promises";
15456
+ import { readFile as readFile17 } from "fs/promises";
15106
15457
  import { join as join22, resolve as resolve18 } from "path";
15107
15458
  import { z as z20 } from "zod";
15108
15459
  var DecisionSchema2 = z20.enum(["allow", "ask", "deny"]);
@@ -15148,7 +15499,7 @@ async function loadPermissionPolicy(options) {
15148
15499
  for (const source of sources) {
15149
15500
  let raw;
15150
15501
  try {
15151
- raw = await readFile16(source.path, "utf8");
15502
+ raw = await readFile17(source.path, "utf8");
15152
15503
  } catch (error) {
15153
15504
  if (isCode6(error, "ENOENT")) continue;
15154
15505
  throw error;
@@ -15890,10 +16241,14 @@ function createSkillResourceTool(registry) {
15890
16241
  name: "SkillResource",
15891
16242
  description: "Read a bounded resource explicitly referenced by a discovered skill; scripts are returned as data and never executed",
15892
16243
  inputSchema: SkillResourceInput,
16244
+ readOnly: true,
15893
16245
  paths: () => [],
15894
16246
  execute: async (input, signal) => {
15895
16247
  signal.throwIfAborted();
15896
- const skill = (await registry.discover()).find((candidate) => candidate.name === input.skill);
16248
+ const requested = input.skill.trim();
16249
+ const alias = requested.includes(":") ? requested.slice(requested.lastIndexOf(":") + 1) : requested;
16250
+ const discovered = await registry.discover();
16251
+ const skill = discovered.find((candidate) => candidate.name === requested) ?? discovered.find((candidate) => candidate.name === alias);
15897
16252
  if (skill === void 0) throw new Error(`Unknown skill: ${input.skill}`);
15898
16253
  const capability = await registry.resolveResource(skill, input.reference);
15899
16254
  const content = await registry.readResource(capability);
@@ -15910,8 +16265,8 @@ function createSkillResourceTool(registry) {
15910
16265
 
15911
16266
  // src/session/store.ts
15912
16267
  import { constants as constants7 } from "fs";
15913
- import { lstat as lstat10, mkdir as mkdir14, open as open8, readFile as readFile17, readdir as readdir7, realpath as realpath7, rename as rename6, rm as rm6, stat as stat7 } from "fs/promises";
15914
- import { basename as basename4, dirname as dirname15, join as join23, relative as relative14, resolve as resolve20, sep as sep14 } from "path";
16268
+ import { lstat as lstat10, mkdir as mkdir14, open as open8, readFile as readFile18, readdir as readdir7, realpath as realpath7, rename as rename6, rm as rm6, stat as stat8 } from "fs/promises";
16269
+ import { basename as basename4, dirname as dirname13, join as join23, relative as relative14, resolve as resolve20, sep as sep14 } from "path";
15915
16270
  import { randomUUID as randomUUID13 } from "crypto";
15916
16271
  import { z as z22 } from "zod";
15917
16272
  var SESSION_VERSION = 4;
@@ -16108,7 +16463,7 @@ var SessionStore = class {
16108
16463
  release: async () => {
16109
16464
  if (released) return;
16110
16465
  try {
16111
- if (await readFile17(path, "utf8") === token) await rm6(path, { force: true });
16466
+ if (await readFile18(path, "utf8") === token) await rm6(path, { force: true });
16112
16467
  } catch (error) {
16113
16468
  if (!isCode7(error, "ENOENT")) throw error;
16114
16469
  }
@@ -16265,7 +16620,7 @@ var SessionStore = class {
16265
16620
  }
16266
16621
  }
16267
16622
  async #prepareDirectory() {
16268
- await assertNoSymlink3(this.#workspace, dirname15(this.#sessions));
16623
+ await assertNoSymlink3(this.#workspace, dirname13(this.#sessions));
16269
16624
  await mkdir14(this.#sessions, { recursive: true, mode: 448 });
16270
16625
  await this.#assertSafeExistingDirectory();
16271
16626
  }
@@ -16302,14 +16657,14 @@ function storedSessionVersion(raw) {
16302
16657
  }
16303
16658
  async function reclaimStaleSessionLock(path) {
16304
16659
  try {
16305
- const token = await readFile17(path, "utf8");
16306
- const metadata = await stat7(path);
16660
+ const token = await readFile18(path, "utf8");
16661
+ const metadata = await stat8(path);
16307
16662
  const owner2 = parseSessionLockOwner(token);
16308
16663
  if (owner2 !== void 0 && isProcessAlive(owner2)) return owner2;
16309
16664
  if (owner2 === void 0 && Date.now() - metadata.mtimeMs <= INVALID_SESSION_LOCK_STALE_MS) {
16310
16665
  throw new Error(`Session writer lock ${path} is incomplete or invalid; retry after the owning process exits`);
16311
16666
  }
16312
- if (await readFile17(path, "utf8") !== token) return void 0;
16667
+ if (await readFile18(path, "utf8") !== token) return void 0;
16313
16668
  const stale = `${path}.stale-${process.pid}-${randomUUID13()}`;
16314
16669
  try {
16315
16670
  await rename6(path, stale);
@@ -16342,10 +16697,10 @@ function isProcessAlive(pid) {
16342
16697
  }
16343
16698
  }
16344
16699
  async function boundedRead(path, maxBytes) {
16345
- const metadata = await stat7(path);
16700
+ const metadata = await stat8(path);
16346
16701
  if (!metadata.isFile()) throw new Error("Session path is not a regular file");
16347
16702
  if (metadata.size > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
16348
- const value = await readFile17(path);
16703
+ const value = await readFile18(path);
16349
16704
  if (value.byteLength > maxBytes) throw new Error(`Session exceeds maximum read size of ${maxBytes} bytes`);
16350
16705
  return value.toString("utf8");
16351
16706
  }
@@ -16493,7 +16848,7 @@ async function assertNoSymlink3(root, target) {
16493
16848
  const chain = [];
16494
16849
  while (cursor !== stop) {
16495
16850
  chain.push(cursor);
16496
- cursor = dirname15(cursor);
16851
+ cursor = dirname13(cursor);
16497
16852
  }
16498
16853
  for (const path of chain.reverse()) {
16499
16854
  try {
@@ -16535,11 +16890,70 @@ function sanitize2(input, seen = /* @__PURE__ */ new WeakSet()) {
16535
16890
  return output;
16536
16891
  }
16537
16892
 
16893
+ // src/utils/rotation-census.ts
16894
+ import { mkdirSync as mkdirSync3, readdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
16895
+ import { join as join24 } from "path";
16896
+ import { writeHeapSnapshot } from "v8";
16897
+ var CENSUS_PREFIX = "rotation-census-";
16898
+ var SNAPSHOT_PREFIX = "heap-";
16899
+ var KEEP_CENSUS_FILES = 5;
16900
+ var KEEP_SNAPSHOT_FILES = 2;
16901
+ function censusHeapBlock(reading) {
16902
+ const usage = process.memoryUsage();
16903
+ const mb = (bytes) => Math.round(bytes / 1048576);
16904
+ return {
16905
+ heapUsedMb: mb(reading.heapUsed),
16906
+ heapLimitMb: mb(reading.heapLimit),
16907
+ rawRatio: Number(reading.ratio.toFixed(3)),
16908
+ ...reading.verifiedRatio === void 0 ? {} : { verifiedRatio: Number(reading.verifiedRatio.toFixed(3)) },
16909
+ gcVerified: reading.gcVerified,
16910
+ rssMb: mb(usage.rss),
16911
+ externalMb: mb(usage.external),
16912
+ arrayBuffersMb: mb(usage.arrayBuffers),
16913
+ ...reading.memoryLimit === void 0 ? {} : { memoryLimitMb: mb(reading.memoryLimit) },
16914
+ ...reading.rssRatio === void 0 ? {} : { rssRatio: Number(reading.rssRatio.toFixed(3)) }
16915
+ };
16916
+ }
16917
+ function writeRotationCensus(workspace, census) {
16918
+ try {
16919
+ const directory = join24(workspace, ".flavor", "tmp");
16920
+ mkdirSync3(directory, { recursive: true });
16921
+ const path = join24(directory, `${CENSUS_PREFIX}${Date.now()}.json`);
16922
+ writeFileSync2(path, `${JSON.stringify(census, null, 2)}
16923
+ `, { encoding: "utf8", mode: 384 });
16924
+ prune(directory, CENSUS_PREFIX, ".json", KEEP_CENSUS_FILES);
16925
+ return path;
16926
+ } catch {
16927
+ return void 0;
16928
+ }
16929
+ }
16930
+ function writeRotationHeapSnapshot(workspace) {
16931
+ try {
16932
+ const directory = join24(workspace, ".flavor", "tmp");
16933
+ mkdirSync3(directory, { recursive: true });
16934
+ const path = join24(directory, `${SNAPSHOT_PREFIX}${Date.now()}.heapsnapshot`);
16935
+ writeHeapSnapshot(path);
16936
+ prune(directory, SNAPSHOT_PREFIX, ".heapsnapshot", KEEP_SNAPSHOT_FILES);
16937
+ return path;
16938
+ } catch {
16939
+ return void 0;
16940
+ }
16941
+ }
16942
+ function prune(directory, prefix, suffix, keep) {
16943
+ const stale = readdirSync(directory).filter((name) => name.startsWith(prefix) && name.endsWith(suffix)).sort().slice(0, -keep);
16944
+ for (const name of stale) {
16945
+ try {
16946
+ unlinkSync2(join24(directory, name));
16947
+ } catch {
16948
+ }
16949
+ }
16950
+ }
16951
+
16538
16952
  // src/sleep/organizer.ts
16539
16953
  import { constants as constants8 } from "fs";
16540
16954
  import { randomUUID as randomUUID14 } from "crypto";
16541
16955
  import { lstat as lstat11, mkdir as mkdir15, open as open9, readdir as readdir8, realpath as realpath8, rename as rename7, rm as rm7 } from "fs/promises";
16542
- import { isAbsolute as isAbsolute10, join as join24, relative as relative15, resolve as resolve21, sep as sep15 } from "path";
16956
+ import { isAbsolute as isAbsolute10, join as join25, relative as relative15, resolve as resolve21, sep as sep15 } from "path";
16543
16957
  import { z as z23 } from "zod";
16544
16958
  var DateKeySchema = z23.string().regex(/^\d{4}-\d{2}-\d{2}$/);
16545
16959
  var ReviewSchema = z23.object({
@@ -16594,7 +17008,7 @@ var ProjectSleepOrganizer = class {
16594
17008
  #generate;
16595
17009
  constructor(options) {
16596
17010
  this.#workspace = resolve21(options.workspace);
16597
- this.#sleepDirectory = join24(this.#workspace, ".flavor", "sleep");
17011
+ this.#sleepDirectory = join25(this.#workspace, ".flavor", "sleep");
16598
17012
  this.#sessions = options.sessions ?? new SessionStore({ workspace: this.#workspace });
16599
17013
  this.#generate = options.generate;
16600
17014
  }
@@ -16603,7 +17017,7 @@ var ProjectSleepOrganizer = class {
16603
17017
  signal.throwIfAborted();
16604
17018
  await this.#prepareDirectory();
16605
17019
  if (await this.#hasReport(date)) return { status: "exists", date };
16606
- const lockPath = join24(this.#sleepDirectory, `.${date}.lock`);
17020
+ const lockPath = join25(this.#sleepDirectory, `.${date}.lock`);
16607
17021
  let lock;
16608
17022
  try {
16609
17023
  lock = await open9(lockPath, constants8.O_CREAT | constants8.O_EXCL | constants8.O_WRONLY, 384);
@@ -16638,7 +17052,7 @@ Previous output was invalid. Repair it and return strict JSON only.`;
16638
17052
  }
16639
17053
  if (review === void 0) throw lastError;
16640
17054
  const filename2 = `${date}-${filenameSummary(review.title)}.md`;
16641
- const path = join24(this.#sleepDirectory, filename2);
17055
+ const path = join25(this.#sleepDirectory, filename2);
16642
17056
  const markdown = renderSleepReport(date, review, documents);
16643
17057
  await this.#atomicWrite(path, markdown);
16644
17058
  return { status: "written", date, path, sessionCount: documents.length };
@@ -16680,7 +17094,7 @@ Previous output was invalid. Repair it and return strict JSON only.`;
16680
17094
  });
16681
17095
  }
16682
17096
  async #prepareDirectory() {
16683
- const flavorDirectory = join24(this.#workspace, ".flavor");
17097
+ const flavorDirectory = join25(this.#workspace, ".flavor");
16684
17098
  await assertNotSymlink(flavorDirectory);
16685
17099
  await mkdir15(flavorDirectory, { recursive: true, mode: 448 });
16686
17100
  await assertNotSymlink(this.#sleepDirectory);
@@ -16694,7 +17108,7 @@ Previous output was invalid. Repair it and return strict JSON only.`;
16694
17108
  return (await readdir8(this.#sleepDirectory, { withFileTypes: true })).some((entry2) => entry2.isFile() && entry2.name.startsWith(prefix) && entry2.name.endsWith(".md"));
16695
17109
  }
16696
17110
  async #atomicWrite(path, content) {
16697
- const temporary = join24(this.#sleepDirectory, `.${randomUUID14()}.tmp`);
17111
+ const temporary = join25(this.#sleepDirectory, `.${randomUUID14()}.tmp`);
16698
17112
  let handle;
16699
17113
  try {
16700
17114
  handle = await open9(temporary, constants8.O_CREAT | constants8.O_EXCL | constants8.O_WRONLY, 384);
@@ -16767,14 +17181,14 @@ var ProjectSleepScheduler = class {
16767
17181
  const next = new Date(now);
16768
17182
  next.setDate(next.getDate() + 1);
16769
17183
  next.setHours(0, 0, 0, 0);
16770
- const delay3 = Math.max(1, next.getTime() - now.getTime());
17184
+ const delay2 = Math.max(1, next.getTime() - now.getTime());
16771
17185
  const targetDate = localDateKey(now);
16772
17186
  this.#dailyTimer = setTimeout(() => {
16773
17187
  this.#dailyTimer = void 0;
16774
17188
  if (this.#disposed) return;
16775
17189
  this.#enqueue(targetDate);
16776
17190
  this.#scheduleNextMidnight();
16777
- }, delay3);
17191
+ }, delay2);
16778
17192
  this.#dailyTimer.unref?.();
16779
17193
  }
16780
17194
  #enqueue(date) {
@@ -17068,8 +17482,8 @@ function computeSessionStats(documents) {
17068
17482
 
17069
17483
  // src/tools/files.ts
17070
17484
  import { constants as constants9 } from "fs";
17071
- import { mkdir as mkdir16, open as open10, readFile as readFile18, realpath as realpath9, rename as rename8, rm as rm8, stat as stat8 } from "fs/promises";
17072
- import { dirname as dirname16, isAbsolute as isAbsolute11, relative as relative16, resolve as resolve22, sep as sep16 } from "path";
17485
+ import { mkdir as mkdir16, open as open10, readFile as readFile19, realpath as realpath9, rename as rename8, rm as rm8, stat as stat9 } from "fs/promises";
17486
+ import { dirname as dirname14, isAbsolute as isAbsolute11, relative as relative16, resolve as resolve22, sep as sep16 } from "path";
17073
17487
  import { randomUUID as randomUUID15 } from "crypto";
17074
17488
  import { z as z24 } from "zod";
17075
17489
 
@@ -17187,8 +17601,12 @@ var ReadInput2 = z24.object({
17187
17601
  force: z24.union([z24.boolean(), z24.string().refine((value) => value === "true" || value === "false")]).nullable().optional()
17188
17602
  });
17189
17603
  var WriteInput = z24.object({ path: z24.string().min(1), content: z24.string() });
17190
- var EditInput = z24.object({ path: z24.string().min(1), oldText: z24.string().min(1), newText: z24.string() });
17191
- var ApplyPatchInput = z24.object({ patch: z24.string().min(1) });
17604
+ var EditInput = z24.object({
17605
+ path: z24.string().min(1),
17606
+ oldText: z24.string().min(1).describe("Copy exact current file text from Read, without displayed line numbers or diff prefixes. Include enough context for one unique match."),
17607
+ newText: z24.string()
17608
+ });
17609
+ var ApplyPatchInput = z24.object({ patch: z24.string().min(1).describe("One-file unified diff with --- a/path, +++ b/path and @@ hunk headers. Use /dev/null as the old path to create a file.") });
17192
17610
  var FileObservationStore = class {
17193
17611
  #versions = /* @__PURE__ */ new Map();
17194
17612
  get(path) {
@@ -17217,7 +17635,7 @@ function createReadTool(workspace, options = {}) {
17217
17635
  execute: async (input, signal) => {
17218
17636
  abortIfNeeded(signal);
17219
17637
  const path = await guard.existing(input.path);
17220
- const info = await stat8(path);
17638
+ const info = await stat9(path);
17221
17639
  const maxBytes = input.maxBytes ?? MAX_READ_BYTES;
17222
17640
  if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || maxBytes > MAX_READ_BYTES) {
17223
17641
  throw new Error(`maxBytes must be a positive integer no greater than ${MAX_READ_BYTES}`);
@@ -17231,9 +17649,9 @@ function createReadTool(workspace, options = {}) {
17231
17649
  const windowBytes = rangeRequested ? Math.max(maxBytes, Math.min(info.size, MAX_READ_BYTES)) : maxBytes;
17232
17650
  const contents = await readBounded2(path, windowBytes, signal, openFile2);
17233
17651
  if (isBinary(contents)) throw new Error("Cannot read binary file as text");
17234
- const textEnd = utf8SafeEnd(contents, windowBytes);
17652
+ const textEnd = contents.length > windowBytes ? utf8SafeEnd(contents, windowBytes) : contents.length;
17235
17653
  const truncated = contents.length > windowBytes;
17236
- const text = contents.subarray(0, textEnd).toString("utf8").replaceAll("\r\n", "\n");
17654
+ const text = decodeFileText(contents.subarray(0, textEnd)).replaceAll("\r\n", "\n");
17237
17655
  options.observations?.set(path, versionOfStat(info));
17238
17656
  const lines = text.split("\n");
17239
17657
  if (lines.at(-1) === "") lines.pop();
@@ -17309,7 +17727,7 @@ function createEditTool(workspace, options = {}) {
17309
17727
  const guard = createPathGuard(workspace);
17310
17728
  return {
17311
17729
  name: "Edit",
17312
- description: "Replace one unique exact text match",
17730
+ description: "Replace one unique exact text match, accepting LF or CRLF input. Read the file before editing; after a mismatch, re-read with force=true and copy the current text instead of retrying the same oldText.",
17313
17731
  inputSchema: EditInput,
17314
17732
  paths: (input) => [guard.lexical(input.path)],
17315
17733
  execute: async (input, signal) => {
@@ -17319,14 +17737,14 @@ function createEditTool(workspace, options = {}) {
17319
17737
  const expected = await expectedMutationVersion(path, options.observations);
17320
17738
  const contents = await readText(path);
17321
17739
  const hasCRLF = contents.includes("\r\n");
17322
- const norm = (s) => hasCRLF ? s.replace(/\r\n/g, "\n") : s;
17740
+ const norm = (s) => s.replace(/\r\n/g, "\n");
17323
17741
  const contentsLF = norm(contents);
17324
17742
  const oldTextLF = norm(input.oldText);
17325
17743
  const newTextLF = norm(input.newText);
17326
17744
  const first = contentsLF.indexOf(oldTextLF);
17327
- const second = first < 0 ? -1 : contentsLF.indexOf(oldTextLF, first + oldTextLF.length);
17745
+ const second = first < 0 ? -1 : contentsLF.indexOf(oldTextLF, first + 1);
17328
17746
  if (first < 0 || second >= 0) {
17329
- const diagnosis = buildEditDiagnosis(contentsLF, oldTextLF, first, second);
17747
+ const diagnosis = buildEditDiagnosis(contentsLF, oldTextLF, second);
17330
17748
  throw new Error(diagnosis);
17331
17749
  }
17332
17750
  const updatedLF = contentsLF.slice(0, first) + newTextLF + contentsLF.slice(first + oldTextLF.length);
@@ -17346,7 +17764,7 @@ function createApplyPatchTool(workspace, options = {}) {
17346
17764
  const guard = createPathGuard(workspace);
17347
17765
  return {
17348
17766
  name: "ApplyPatch",
17349
- description: "Apply a workspace-limited unified diff, relocating hunks only by unique exact context",
17767
+ description: "Apply a single-file workspace-limited unified diff, relocating hunks only by unique exact context. Submit separate calls for multiple files.",
17350
17768
  inputSchema: ApplyPatchInput,
17351
17769
  paths: (input) => parsePatch(input.patch).map((file) => guard.lexical(file.path)),
17352
17770
  execute: async (input, signal) => {
@@ -17411,7 +17829,7 @@ function createPathGuard(workspace) {
17411
17829
  break;
17412
17830
  } catch (error) {
17413
17831
  if (!isMissing3(error)) throw error;
17414
- const parent = dirname16(ancestor);
17832
+ const parent = dirname14(ancestor);
17415
17833
  if (parent === ancestor) throw error;
17416
17834
  ancestor = parent;
17417
17835
  }
@@ -17422,12 +17840,19 @@ function createPathGuard(workspace) {
17422
17840
  }
17423
17841
  async function atomicWrite(path, content, signal, expectedVersion) {
17424
17842
  abortIfNeeded(signal);
17425
- await mkdir16(dirname16(path), { recursive: true });
17843
+ await mkdir16(dirname14(path), { recursive: true });
17426
17844
  const temporary = `${path}.${randomUUID15()}.tmp`;
17845
+ let mode;
17846
+ try {
17847
+ mode = (await stat9(path)).mode & 511;
17848
+ } catch (error) {
17849
+ if (!isMissing3(error)) throw error;
17850
+ }
17427
17851
  try {
17428
17852
  const handle = await open10(temporary, constants9.O_CREAT | constants9.O_EXCL | constants9.O_WRONLY, 384);
17429
17853
  try {
17430
17854
  await handle.writeFile(content, "utf8");
17855
+ if (mode !== void 0) await handle.chmod(mode);
17431
17856
  await handle.sync();
17432
17857
  } finally {
17433
17858
  await handle.close();
@@ -17442,9 +17867,16 @@ async function atomicWrite(path, content, signal, expectedVersion) {
17442
17867
  }
17443
17868
  }
17444
17869
  async function readText(path) {
17445
- const contents = await readFile18(path);
17870
+ const contents = await readFile19(path);
17446
17871
  if (isBinary(contents)) throw new Error("Cannot edit binary file as text");
17447
- return contents.toString("utf8");
17872
+ return decodeFileText(contents);
17873
+ }
17874
+ function decodeFileText(contents) {
17875
+ try {
17876
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(contents);
17877
+ } catch {
17878
+ throw new Error("File is not valid UTF-8 text. Convert its encoding before using Read/Edit/ApplyPatch to avoid corrupting its contents.");
17879
+ }
17448
17880
  }
17449
17881
  var ABSENT_VERSION = "absent";
17450
17882
  async function expectedMutationVersion(path, observations) {
@@ -17461,7 +17893,7 @@ async function assertMutationVersion(path, expected, observations) {
17461
17893
  }
17462
17894
  async function fileVersion(path) {
17463
17895
  try {
17464
- return versionOfStat(await stat8(path));
17896
+ return versionOfStat(await stat9(path));
17465
17897
  } catch (error) {
17466
17898
  if (isMissing3(error)) return ABSENT_VERSION;
17467
17899
  throw error;
@@ -17472,7 +17904,7 @@ function versionOfStat(info) {
17472
17904
  }
17473
17905
  async function readOptionalPresentationText(path) {
17474
17906
  try {
17475
- const contents = await readFile18(path);
17907
+ const contents = await readFile19(path);
17476
17908
  return isBinary(contents) ? { exists: true } : { exists: true, text: contents.toString("utf8") };
17477
17909
  } catch (error) {
17478
17910
  return isMissing3(error) ? { exists: false } : { exists: true };
@@ -17654,11 +18086,12 @@ function patchPath(header) {
17654
18086
  const raw = header.split(" ", 1)[0].trim();
17655
18087
  return raw.startsWith("a/") || raw.startsWith("b/") ? raw.slice(2) : raw;
17656
18088
  }
17657
- function buildEditDiagnosis(contentsLF, oldTextLF, first, second) {
18089
+ function buildEditDiagnosis(contentsLF, oldTextLF, second) {
17658
18090
  let diagnosis = "oldText must match exactly once";
17659
18091
  if (second >= 0) {
17660
- const count = contentsLF.split(oldTextLF).length - 1;
17661
- diagnosis += ` \u2014 matched ${count} times in the file`;
18092
+ let count = 0;
18093
+ for (let index = contentsLF.indexOf(oldTextLF); index >= 0; index = contentsLF.indexOf(oldTextLF, index + 1)) count += 1;
18094
+ diagnosis += ` \u2014 matched ${count} times in the file. No changes made. Include more surrounding text to select one occurrence.`;
17662
18095
  return diagnosis;
17663
18096
  }
17664
18097
  const oldLines = oldTextLF.split("\n");
@@ -17694,7 +18127,8 @@ ${snippet}
17694
18127
  First lines of file:
17695
18128
  ${fileSnippet}`;
17696
18129
  }
17697
- return diagnosis;
18130
+ return `${diagnosis}
18131
+ No changes made. Use Read with force=true to retrieve current text, or Grep with fixedStrings=true to locate it. Copy oldText exactly without line-number prefixes; do not repeat the same failed edit.`;
17698
18132
  }
17699
18133
  function applyHunks(original, hunks) {
17700
18134
  const hasCRLF = original.includes("\r\n");
@@ -17785,6 +18219,19 @@ import { isIP } from "net";
17785
18219
  import { request as httpRequest } from "http";
17786
18220
  import { request as httpsRequest } from "https";
17787
18221
  import { z as z25 } from "zod";
18222
+
18223
+ // src/utils/abort.ts
18224
+ function waitWithSignal(operation, signal) {
18225
+ if (signal === void 0) return operation;
18226
+ return new Promise((resolve32, reject) => {
18227
+ const abort2 = () => reject(signal.reason);
18228
+ if (signal.aborted) abort2();
18229
+ else signal.addEventListener("abort", abort2, { once: true });
18230
+ operation.then(resolve32, reject).finally(() => signal.removeEventListener("abort", abort2));
18231
+ });
18232
+ }
18233
+
18234
+ // src/tools/web.ts
17788
18235
  var WebFetchInput = z25.object({
17789
18236
  url: z25.string().url().max(8192),
17790
18237
  timeoutMs: z25.coerce.number().int().positive().max(12e4).optional(),
@@ -17830,7 +18277,12 @@ function createWebSearchTool(provider = createDefaultWebSearchProvider()) {
17830
18277
  summarize: (input) => input.query,
17831
18278
  presentCall: (input) => ({ kind: "web", title: `Searching: ${input.query}` }),
17832
18279
  permissions: () => ({ paths: [] }),
17833
- execute: async (input, signal) => ({ query: input.query, results: [...await provider.search(input.query, input.maxResults ?? 8, signal)] }),
18280
+ execute: async (input, signal) => {
18281
+ signal.throwIfAborted();
18282
+ const maxResults = input.maxResults ?? 8;
18283
+ const results = await waitWithSignal(provider.search(input.query, maxResults, signal), signal);
18284
+ return { query: input.query, results: [...results].slice(0, maxResults) };
18285
+ },
17834
18286
  renderForModel: (output) => output.results.length === 0 ? `No web results found for: ${output.query}` : output.results.map((item, index) => `${index + 1}. ${item.title}
17835
18287
  ${item.url}
17836
18288
  ${item.snippet}`).join("\n\n"),
@@ -17875,10 +18327,14 @@ var MultiSourceWebSearchProvider = class {
17875
18327
  }
17876
18328
  };
17877
18329
  async function fetchPublic(initialUrl, options) {
18330
+ options.signal.throwIfAborted();
18331
+ const deadline = AbortSignal.timeout(options.timeoutMs);
18332
+ options = { ...options, signal: AbortSignal.any([options.signal, deadline]) };
17878
18333
  let current = new URL(initialUrl);
17879
18334
  for (let redirects = 0; redirects <= 5; redirects += 1) {
18335
+ options.signal.throwIfAborted();
17880
18336
  validateUrl(current);
17881
- const addresses = await lookup(normalizedHostname(current.hostname), { all: true, verbatim: true });
18337
+ const addresses = await waitWithSignal(lookup(normalizedHostname(current.hostname), { all: true, verbatim: true }), options.signal);
17882
18338
  if (addresses.length === 0) throw new Error("Host did not resolve");
17883
18339
  if (addresses.some((entry2) => isBlockedResolvedAddress(entry2.address, current.hostname))) {
17884
18340
  throw new Error("Blocked private or special network destination");
@@ -17890,7 +18346,13 @@ async function fetchPublic(initialUrl, options) {
17890
18346
  continue;
17891
18347
  }
17892
18348
  const contentType = response.contentType;
17893
- const decoded = new TextDecoder(charset(contentType), { fatal: false }).decode(response.body);
18349
+ let decoder;
18350
+ try {
18351
+ decoder = new TextDecoder(charset(contentType), { fatal: false });
18352
+ } catch {
18353
+ decoder = new TextDecoder("utf-8");
18354
+ }
18355
+ const decoded = decoder.decode(response.body);
17894
18356
  const content = !options.rawHtml && /text\/html|application\/xhtml\+xml/i.test(contentType) ? htmlToText(decoded) : decoded;
17895
18357
  return { url: current.href, status: response.status, contentType, content, truncated: response.truncated };
17896
18358
  }
@@ -17925,8 +18387,8 @@ async function requestAddress(url, address, options) {
17925
18387
  const status = response.statusCode ?? 0;
17926
18388
  const location = response.headers.location;
17927
18389
  if (location !== void 0 && [301, 302, 303, 307, 308].includes(status)) {
17928
- response.resume();
17929
18390
  finish({ status, contentType: "", body: new Uint8Array(), truncated: false, redirect: location });
18391
+ response.destroy();
17930
18392
  return;
17931
18393
  }
17932
18394
  const chunks = [];
@@ -17942,7 +18404,11 @@ async function requestAddress(url, address, options) {
17942
18404
  const accepted = chunk.subarray(0, options.maxBytes - bytes);
17943
18405
  chunks.push(accepted);
17944
18406
  bytes += accepted.length;
17945
- if (accepted.length < chunk.length) truncated = true;
18407
+ if (accepted.length < chunk.length) {
18408
+ truncated = true;
18409
+ finish({ status, contentType: String(response.headers["content-type"] ?? "application/octet-stream"), body: Buffer.concat(chunks), truncated });
18410
+ response.destroy();
18411
+ }
17946
18412
  });
17947
18413
  response.once("end", () => finish({
17948
18414
  status,
@@ -18088,11 +18554,11 @@ function charset(contentType) {
18088
18554
  }
18089
18555
 
18090
18556
  // src/tools/lsp.ts
18091
- import { spawn as spawn7 } from "child_process";
18557
+ import { spawn as spawn8 } from "child_process";
18092
18558
  import { existsSync as existsSync5 } from "fs";
18093
- import { access as access2, constants as constants10, readFile as readFile19 } from "fs/promises";
18094
- import { basename as basename5, dirname as dirname17, extname as extname2, isAbsolute as isAbsolute12, relative as relative17, resolve as resolve23, sep as sep17 } from "path";
18095
- import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL2 } from "url";
18559
+ import { access as access2, constants as constants10, readFile as readFile20 } from "fs/promises";
18560
+ import { basename as basename5, dirname as dirname15, extname as extname2, isAbsolute as isAbsolute12, relative as relative17, resolve as resolve23, sep as sep17 } from "path";
18561
+ import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "url";
18096
18562
  import { z as z26 } from "zod";
18097
18563
  var LSP_REQUEST_TIMEOUT_MS = 15e3;
18098
18564
  var LspConnection = class {
@@ -18104,12 +18570,14 @@ var LspConnection = class {
18104
18570
  #nextId = 1;
18105
18571
  #disposed = false;
18106
18572
  #initError;
18573
+ #shutdownPromise;
18107
18574
  constructor(command, args, cwd) {
18108
18575
  const invocation = prepareSpawnInvocation(command, args, { cwd });
18109
- this.#process = spawn7(invocation.command, invocation.args, {
18576
+ this.#process = spawn8(invocation.command, invocation.args, {
18110
18577
  cwd,
18111
18578
  stdio: ["pipe", "pipe", "pipe"],
18112
18579
  windowsHide: true,
18580
+ detached: process.platform !== "win32",
18113
18581
  ...invocation.windowsVerbatimArguments === void 0 ? {} : { windowsVerbatimArguments: invocation.windowsVerbatimArguments }
18114
18582
  });
18115
18583
  this.#process.on("error", (err) => {
@@ -18118,9 +18586,15 @@ var LspConnection = class {
18118
18586
  });
18119
18587
  this.#process.once("exit", (code, signal) => {
18120
18588
  const reason = signal !== null ? `LSP server "${command}" exited with signal ${signal}` : `LSP server "${command}" exited with code ${code}`;
18121
- this.#rejectAll(new Error(reason));
18589
+ this.#initError = new Error(reason);
18590
+ this.#rejectAll(this.#initError);
18122
18591
  });
18123
18592
  this.#process.stdout.on("data", (chunk) => this.#buffer.feed(chunk, this.#onMessage));
18593
+ this.#process.stderr.resume();
18594
+ this.#process.stdin.on("error", (error) => {
18595
+ this.#initError = error;
18596
+ this.#rejectAll(error);
18597
+ });
18124
18598
  }
18125
18599
  async initialize(rootUri) {
18126
18600
  if (this.#initError !== void 0) throw this.#initError;
@@ -18142,67 +18616,84 @@ var LspConnection = class {
18142
18616
  if (initResult === void 0) throw new Error("LSP initialize returned no capabilities");
18143
18617
  this.#notify("initialized", {});
18144
18618
  }
18145
- async findReferences(uri, line, character) {
18619
+ async findReferences(uri, line, character, signal) {
18620
+ signal?.throwIfAborted();
18146
18621
  await this.#syncDocument(uri);
18147
18622
  const result = await this.#request("textDocument/references", {
18148
18623
  textDocument: { uri },
18149
18624
  position: { line, character },
18150
18625
  context: { includeDeclaration: false }
18151
- });
18626
+ }, signal);
18152
18627
  if (result === null || result === void 0) return [];
18153
18628
  if (!Array.isArray(result)) return [];
18154
18629
  return result.filter(isLocation);
18155
18630
  }
18156
- async hover(uri, line, character) {
18631
+ async hover(uri, line, character, signal) {
18632
+ signal?.throwIfAborted();
18157
18633
  await this.#syncDocument(uri);
18158
18634
  const result = await this.#request("textDocument/hover", {
18159
18635
  textDocument: { uri },
18160
18636
  position: { line, character }
18161
- });
18637
+ }, signal);
18162
18638
  if (result === null || result === void 0) return null;
18163
18639
  if (typeof result === "object" && "contents" in result) {
18164
18640
  return result;
18165
18641
  }
18166
18642
  return null;
18167
18643
  }
18168
- async diagnostics(uri) {
18644
+ async diagnostics(uri, signal) {
18645
+ signal?.throwIfAborted();
18169
18646
  await this.#syncDocument(uri);
18170
18647
  const result = await this.#request("textDocument/diagnostic", {
18171
18648
  textDocument: { uri }
18172
- });
18649
+ }, signal);
18173
18650
  if (result === null || result === void 0) return [];
18174
18651
  const items = result?.items ?? result;
18175
18652
  if (!Array.isArray(items)) return [];
18176
18653
  return items.filter(isDiagnostic);
18177
18654
  }
18178
- async shutdown() {
18179
- if (this.#disposed) return;
18655
+ shutdown() {
18656
+ if (this.#shutdownPromise !== void 0) return this.#shutdownPromise;
18180
18657
  this.#disposed = true;
18181
- try {
18182
- await this.#request("shutdown", null);
18183
- } catch {
18184
- }
18185
- this.#notify("exit", null);
18186
- this.#process.kill();
18187
18658
  this.#rejectAll(new Error("LSP connection disposed"));
18659
+ this.#shutdownPromise = (async () => {
18660
+ await terminateProcessTree(this.#process.pid);
18661
+ this.#process.stdin.destroy();
18662
+ this.#process.stdout.destroy();
18663
+ this.#process.stderr.destroy();
18664
+ })();
18665
+ return this.#shutdownPromise;
18188
18666
  }
18189
18667
  // -- JSON-RPC core -------------------------------------------------------
18190
- async #request(method, params) {
18668
+ async #request(method, params, signal) {
18191
18669
  if (this.#disposed) throw new Error("LSP connection disposed");
18670
+ if (this.#initError !== void 0) throw this.#initError;
18671
+ signal?.throwIfAborted();
18192
18672
  const id = this.#nextId++;
18193
18673
  const request = { jsonrpc: "2.0", id, method, params };
18194
- const timeout = AbortSignal.timeout(LSP_REQUEST_TIMEOUT_MS);
18195
- const promise = new Promise((resolve31, reject) => {
18196
- this.#pending.set(id, { resolve: resolve31, reject });
18197
- timeout.addEventListener("abort", () => {
18198
- if (this.#pending.has(id)) {
18199
- this.#pending.delete(id);
18200
- reject(new Error(`LSP request "${method}" timed out after ${LSP_REQUEST_TIMEOUT_MS}ms`));
18674
+ let timer;
18675
+ let onAbort;
18676
+ const promise = new Promise((resolve32, reject) => {
18677
+ this.#pending.set(id, { resolve: resolve32, reject });
18678
+ timer = setTimeout(() => reject(new Error(`LSP request "${method}" timed out after ${LSP_REQUEST_TIMEOUT_MS}ms`)), LSP_REQUEST_TIMEOUT_MS);
18679
+ timer.unref();
18680
+ onAbort = () => {
18681
+ try {
18682
+ this.#notify("$/cancelRequest", { id });
18683
+ } catch {
18201
18684
  }
18202
- }, { once: true });
18685
+ reject(signal?.reason);
18686
+ };
18687
+ signal?.addEventListener("abort", onAbort, { once: true });
18203
18688
  });
18204
- this.#send(request);
18205
- return promise;
18689
+ try {
18690
+ this.#send(request);
18691
+ return await promise;
18692
+ } finally {
18693
+ if (timer !== void 0) clearTimeout(timer);
18694
+ if (onAbort !== void 0) signal?.removeEventListener("abort", onAbort);
18695
+ this.#pending.delete(id);
18696
+ }
18206
18697
  }
18207
18698
  #notify(method, params) {
18208
18699
  const notification = { jsonrpc: "2.0", method, params };
@@ -18222,6 +18713,7 @@ var LspConnection = class {
18222
18713
  } catch {
18223
18714
  return;
18224
18715
  }
18716
+ if (typeof parsed !== "object" || parsed === null) return;
18225
18717
  if ("method" in parsed) {
18226
18718
  if ("id" in parsed) this.#handleServerRequest(parsed);
18227
18719
  return;
@@ -18257,8 +18749,8 @@ var LspConnection = class {
18257
18749
  async #syncDocumentNow(uri) {
18258
18750
  const url = new URL(uri);
18259
18751
  if (url.protocol !== "file:") return;
18260
- const filePath = fileURLToPath3(url);
18261
- const text = await readFile19(filePath, "utf8");
18752
+ const filePath = fileURLToPath2(url);
18753
+ const text = await readFile20(filePath, "utf8");
18262
18754
  const current = this.#documents.get(uri);
18263
18755
  if (current === void 0) {
18264
18756
  this.#documents.set(uri, { text, version: 1 });
@@ -18310,7 +18802,7 @@ var LspBuffer = class {
18310
18802
  }
18311
18803
  };
18312
18804
  var TYPESCRIPT_CLI = resolve23(
18313
- dirname17(fileURLToPath3(import.meta.resolve("typescript/package.json"))),
18805
+ dirname15(fileURLToPath2(import.meta.resolve("typescript/package.json"))),
18314
18806
  "lib",
18315
18807
  "tsc.js"
18316
18808
  );
@@ -18345,6 +18837,8 @@ var KNOWN_SERVERS = [
18345
18837
  }
18346
18838
  ];
18347
18839
  var RealLspManager = class {
18840
+ #disposed = false;
18841
+ #disposal;
18348
18842
  #workspace;
18349
18843
  #connections = /* @__PURE__ */ new Map();
18350
18844
  #pendingStarts = /* @__PURE__ */ new Map();
@@ -18356,27 +18850,32 @@ var RealLspManager = class {
18356
18850
  this.#onStatus = options.onStatus;
18357
18851
  }
18358
18852
  dispose() {
18359
- for (const [, connection] of this.#connections) {
18360
- connection.shutdown().catch(() => {
18361
- });
18362
- }
18853
+ if (this.#disposal !== void 0) return this.#disposal;
18854
+ this.#disposed = true;
18855
+ const shutdowns = [...this.#connections.values()].map((connection) => connection.shutdown());
18856
+ shutdowns.push(...[...this.#pendingStarts.values()].map(async (start) => {
18857
+ const connection = await start;
18858
+ await connection.shutdown();
18859
+ }));
18363
18860
  this.#connections.clear();
18364
18861
  this.#pendingStarts.clear();
18862
+ this.#disposal = Promise.allSettled(shutdowns).then(() => void 0);
18863
+ return this.#disposal;
18365
18864
  }
18366
- async findReferences(uri, line, character) {
18367
- const connection = await this.#getConnection(uri);
18368
- if (connection === void 0) return [];
18369
- return connection.findReferences(uri, line, character);
18865
+ async findReferences(uri, line, character, signal) {
18866
+ signal?.throwIfAborted();
18867
+ const connection = await waitWithSignal(this.#getConnection(uri), signal);
18868
+ return connection.findReferences(uri, line, character, signal);
18370
18869
  }
18371
- async hover(uri, line, character) {
18372
- const connection = await this.#getConnection(uri);
18373
- if (connection === void 0) return null;
18374
- return connection.hover(uri, line, character);
18870
+ async hover(uri, line, character, signal) {
18871
+ signal?.throwIfAborted();
18872
+ const connection = await waitWithSignal(this.#getConnection(uri), signal);
18873
+ return connection.hover(uri, line, character, signal);
18375
18874
  }
18376
- async diagnostics(uri) {
18377
- const connection = await this.#getConnection(uri);
18378
- if (connection === void 0) return [];
18379
- return connection.diagnostics(uri);
18875
+ async diagnostics(uri, signal) {
18876
+ signal?.throwIfAborted();
18877
+ const connection = await waitWithSignal(this.#getConnection(uri), signal);
18878
+ return connection.diagnostics(uri, signal);
18380
18879
  }
18381
18880
  #languageForUri(uri) {
18382
18881
  try {
@@ -18392,20 +18891,25 @@ var RealLspManager = class {
18392
18891
  return void 0;
18393
18892
  }
18394
18893
  async #getConnection(uri) {
18894
+ if (this.#disposed) throw new Error("LSP manager is disposed");
18395
18895
  const language = this.#languageForUri(uri);
18396
- if (language === void 0) return void 0;
18896
+ if (language === void 0) throw new Error(`No language server configured for ${uri}; use Read/Grep or the project compiler`);
18397
18897
  const existing = this.#connections.get(language);
18398
18898
  if (existing !== void 0) return existing;
18399
18899
  const pending = this.#pendingStarts.get(language);
18400
18900
  if (pending !== void 0) return pending;
18401
18901
  const config = this.#serverConfigs.find((c) => c.language === language);
18402
- if (config === void 0) return void 0;
18902
+ if (config === void 0) throw new Error(`No language server configured for ${language}`);
18403
18903
  const hasRoot = config.rootFiles.some((file) => existsSync5(resolve23(this.#workspace, file)));
18404
- if (!hasRoot) return void 0;
18904
+ if (!hasRoot) throw new Error(`Cannot start ${language} language server: workspace needs ${config.rootFiles.join(" or ")}`);
18405
18905
  const startPromise = this.#startServer(config);
18406
18906
  this.#pendingStarts.set(language, startPromise);
18407
18907
  try {
18408
18908
  const connection = await startPromise;
18909
+ if (this.#disposed) {
18910
+ await connection.shutdown();
18911
+ throw new Error("LSP manager is disposed");
18912
+ }
18409
18913
  this.#connections.set(language, connection);
18410
18914
  return connection;
18411
18915
  } finally {
@@ -18423,6 +18927,7 @@ var RealLspManager = class {
18423
18927
  await connection.initialize(rootUri);
18424
18928
  this.#onStatus?.(`${config.language} Language Server ready (${config.command})`);
18425
18929
  } catch (error) {
18930
+ await connection.shutdown();
18426
18931
  this.#onStatus?.(`${config.language} Language Server failed: ${error instanceof Error ? error.message : String(error)}`);
18427
18932
  throw error;
18428
18933
  }
@@ -18431,13 +18936,13 @@ var RealLspManager = class {
18431
18936
  };
18432
18937
  var LspFindRefsInput = z26.object({
18433
18938
  file: z26.string().min(1),
18434
- line: z26.coerce.number().int().nonnegative(),
18435
- character: z26.coerce.number().int().nonnegative()
18939
+ line: z26.coerce.number().int().nonnegative().describe("Zero-based line number (first line is 0)."),
18940
+ character: z26.coerce.number().int().nonnegative().describe("Zero-based UTF-16 character offset.")
18436
18941
  });
18437
18942
  var LspHoverInput = z26.object({
18438
18943
  file: z26.string().min(1),
18439
- line: z26.coerce.number().int().nonnegative(),
18440
- character: z26.coerce.number().int().nonnegative()
18944
+ line: z26.coerce.number().int().nonnegative().describe("Zero-based line number (first line is 0)."),
18945
+ character: z26.coerce.number().int().nonnegative().describe("Zero-based UTF-16 character offset.")
18441
18946
  });
18442
18947
  var LspDiagnosticsInput = z26.object({
18443
18948
  file: z26.string().min(1)
@@ -18463,10 +18968,11 @@ function createLspTools(workspace, options = {}) {
18463
18968
  inputSchema: LspFindRefsInput,
18464
18969
  paths: (input) => [guard(input.file)],
18465
18970
  summarize: (input) => `${basename5(input.file)}:${input.line}:${input.character}${langTag(input.file)}`,
18466
- execute: async (input, _signal) => {
18971
+ execute: async (input, signal) => {
18972
+ signal.throwIfAborted();
18467
18973
  const path = guard(input.file);
18468
18974
  const uri = toUri(path);
18469
- const refs = await manager.findReferences(uri, input.line, input.character);
18975
+ const refs = await waitWithSignal(manager.findReferences(uri, input.line, input.character, signal), signal);
18470
18976
  if (refs.length === 0) return `No references found at ${input.file}:${input.line}:${input.character}`;
18471
18977
  return refs.map((loc) => {
18472
18978
  const filePath = uriToPath(loc.uri);
@@ -18480,12 +18986,14 @@ function createLspTools(workspace, options = {}) {
18480
18986
  inputSchema: LspHoverInput,
18481
18987
  paths: (input) => [guard(input.file)],
18482
18988
  summarize: (input) => `${basename5(input.file)}:${input.line}:${input.character}${langTag(input.file)}`,
18483
- execute: async (input, _signal) => {
18989
+ execute: async (input, signal) => {
18990
+ signal.throwIfAborted();
18484
18991
  const path = guard(input.file);
18485
18992
  const uri = toUri(path);
18486
- const info = await manager.hover(uri, input.line, input.character);
18993
+ const info = await waitWithSignal(manager.hover(uri, input.line, input.character, signal), signal);
18487
18994
  if (info === null) return `No hover information at ${input.file}:${input.line}:${input.character}`;
18488
- const contents = typeof info.contents === "string" ? info.contents : info.contents.value;
18995
+ const parts = Array.isArray(info.contents) ? info.contents : [info.contents];
18996
+ const contents = parts.map((part) => typeof part === "string" ? part : part.value).join("\n\n");
18489
18997
  return contents;
18490
18998
  }
18491
18999
  };
@@ -18495,10 +19003,11 @@ function createLspTools(workspace, options = {}) {
18495
19003
  inputSchema: LspDiagnosticsInput,
18496
19004
  paths: (input) => [guard(input.file)],
18497
19005
  summarize: (input) => `${basename5(input.file)}${langTag(input.file)}`,
18498
- execute: async (input, _signal) => {
19006
+ execute: async (input, signal) => {
19007
+ signal.throwIfAborted();
18499
19008
  const path = guard(input.file);
18500
19009
  const uri = toUri(path);
18501
- const diags = await manager.diagnostics(uri);
19010
+ const diags = await waitWithSignal(manager.diagnostics(uri, signal), signal);
18502
19011
  if (diags.length === 0) return `No diagnostics for ${input.file}`;
18503
19012
  return diags.map((d) => {
18504
19013
  const severity = severityLabel(d.severity);
@@ -18517,6 +19026,14 @@ function languageIdForPath(filePath) {
18517
19026
  return "typescript";
18518
19027
  case ".cts":
18519
19028
  return "typescript";
19029
+ case ".py":
19030
+ case ".pyi":
19031
+ case ".pyx":
19032
+ return "python";
19033
+ case ".rs":
19034
+ return "rust";
19035
+ case ".go":
19036
+ return "go";
18520
19037
  default:
18521
19038
  return "typescript";
18522
19039
  }
@@ -18528,7 +19045,7 @@ function isWithin7(root, candidate) {
18528
19045
  function uriToPath(uri) {
18529
19046
  try {
18530
19047
  const url = new URL(uri);
18531
- if (url.protocol === "file:") return decodeURIComponent(url.pathname);
19048
+ if (url.protocol === "file:") return fileURLToPath2(url);
18532
19049
  } catch {
18533
19050
  }
18534
19051
  return uri;
@@ -18555,12 +19072,14 @@ function severityLabel(severity) {
18555
19072
  }
18556
19073
 
18557
19074
  // src/git/service.ts
18558
- async function git2(workspace, args, timeoutMs = 3e4) {
18559
- const result = await execFileNoThrow("git", ["-C", workspace, ...args], { timeout: timeoutMs, useCwd: false });
19075
+ async function git2(workspace, args, timeoutMs = 3e4, signal) {
19076
+ signal?.throwIfAborted();
19077
+ const result = await execFileNoThrow("git", ["-C", workspace, ...args], { timeout: timeoutMs, useCwd: false, ...signal === void 0 ? {} : { signal } });
19078
+ signal?.throwIfAborted();
18560
19079
  return { ok: result.code === 0, stdout: result.stdout, stderr: result.stderr, code: result.code };
18561
19080
  }
18562
- async function isGitRepository(workspace) {
18563
- const result = await git2(workspace, ["rev-parse", "--is-inside-work-tree"], 2e3);
19081
+ async function isGitRepository(workspace, signal) {
19082
+ const result = await git2(workspace, ["rev-parse", "--is-inside-work-tree"], 2e3, signal);
18564
19083
  return result.ok && result.stdout.trim() === "true";
18565
19084
  }
18566
19085
  function gitFailure(result, action) {
@@ -18597,19 +19116,19 @@ async function changeSummary(workspace) {
18597
19116
  };
18598
19117
  }
18599
19118
  async function stagedDiff(workspace, maxChars = 96e3) {
18600
- const stat11 = await git2(workspace, ["diff", "--staged", "--stat"]);
19119
+ const stat12 = await git2(workspace, ["diff", "--staged", "--stat"]);
18601
19120
  const diff = await git2(workspace, ["diff", "--staged", "--no-ext-diff", "--no-color"]);
18602
19121
  if (!diff.ok) throw gitFailure(diff, "diff --staged");
18603
19122
  const text = diff.stdout;
18604
19123
  return {
18605
19124
  diff: text.length > maxChars ? `${text.slice(0, maxChars)}
18606
19125
  \u2026[diff truncated]` : text,
18607
- stat: stat11.stdout.trim(),
19126
+ stat: stat12.stdout.trim(),
18608
19127
  truncated: text.length > maxChars
18609
19128
  };
18610
19129
  }
18611
19130
  async function uncommittedDiff(workspace, maxChars = 96e3) {
18612
- const stat11 = await git2(workspace, ["diff", "HEAD", "--stat"]);
19131
+ const stat12 = await git2(workspace, ["diff", "HEAD", "--stat"]);
18613
19132
  const diff = await git2(workspace, ["diff", "HEAD", "--no-ext-diff", "--no-color"]);
18614
19133
  if (!diff.ok && !diff.stderr.includes("unknown revision")) throw gitFailure(diff, "diff HEAD");
18615
19134
  const summary2 = await changeSummary(workspace);
@@ -18617,7 +19136,7 @@ async function uncommittedDiff(workspace, maxChars = 96e3) {
18617
19136
  return {
18618
19137
  diff: text.length > maxChars ? `${text.slice(0, maxChars)}
18619
19138
  \u2026[diff truncated]` : text,
18620
- stat: stat11.stdout.trim(),
19139
+ stat: stat12.stdout.trim(),
18621
19140
  untracked: summary2.untrackedFiles
18622
19141
  };
18623
19142
  }
@@ -18638,16 +19157,21 @@ async function commit(workspace, commitMessage) {
18638
19157
  const head = await git2(workspace, ["log", "-1", "--pretty=%h %s"]);
18639
19158
  return head.stdout.trim() || "committed";
18640
19159
  }
18641
- async function fileHistory(workspace, path, limit) {
19160
+ async function fileHistory(workspace, path, limit, signal) {
18642
19161
  const args = [
19162
+ "--literal-pathspecs",
18643
19163
  "log",
18644
19164
  `--max-count=${limit}`,
18645
19165
  "--date=short",
18646
19166
  "--pretty=format:%h%x1f%ad%x1f%an%x1f%s",
18647
19167
  ...path === void 0 ? [] : ["--follow", "--", path]
18648
19168
  ];
18649
- const result = await git2(workspace, args, 15e3);
18650
- if (!result.ok) throw gitFailure(result, "log");
19169
+ const result = await git2(workspace, args, 15e3, signal);
19170
+ if (!result.ok) {
19171
+ const head = await git2(workspace, ["rev-parse", "--verify", "HEAD"], 2e3, signal);
19172
+ if (!head.ok && await isGitRepository(workspace, signal)) return [];
19173
+ throw gitFailure(result, "log");
19174
+ }
18651
19175
  return result.stdout.split("\n").filter((line) => line.trim().length > 0).map((line) => {
18652
19176
  const [hash = "", date = "", author = "", ...subject] = line.split("");
18653
19177
  return { hash, date, author, subject: subject.join("") };
@@ -18909,17 +19433,17 @@ ${explanation}` : "";
18909
19433
  }
18910
19434
 
18911
19435
  // src/desktop/astgraph-service.ts
18912
- import { stat as stat9 } from "fs/promises";
19436
+ import { stat as stat10 } from "fs/promises";
18913
19437
  import { createRequire as nodeCreateRequire } from "module";
18914
- import { join as join25, resolve as resolve24 } from "path";
19438
+ import { join as join26, resolve as resolve24 } from "path";
18915
19439
  var FIELDS = "id, kind, name, qualified_name, file_path, language, start_line, end_line, signature";
18916
19440
  var DesktopAstGraphService = class {
18917
19441
  #path;
18918
19442
  constructor(workspace) {
18919
- this.#path = join25(resolve24(workspace), ".flavor", "astgraph", "index.db");
19443
+ this.#path = join26(resolve24(workspace), ".flavor", "astgraph", "index.db");
18920
19444
  }
18921
19445
  async status() {
18922
- if ((await stat9(this.#path).catch(() => void 0))?.isFile() !== true) {
19446
+ if ((await stat10(this.#path).catch(() => void 0))?.isFile() !== true) {
18923
19447
  return { available: false, path: this.#path, files: 0, nodes: 0, edges: 0 };
18924
19448
  }
18925
19449
  return this.#read((db) => {
@@ -18986,24 +19510,33 @@ function mapNode(value) {
18986
19510
 
18987
19511
  // src/git/tools.ts
18988
19512
  import { z as z28 } from "zod";
19513
+ import { isAbsolute as isAbsolute13, relative as relative18, resolve as resolve25, sep as sep18 } from "path";
18989
19514
  var GitHistoryInput = z28.object({
18990
- path: z28.string().describe("File to inspect; omit for the whole repository history").optional(),
18991
- limit: z28.number().int().min(1).max(50).describe("Maximum number of commits to return (default 20)").optional()
19515
+ path: z28.string().min(1).describe("Workspace-relative or absolute file path; omit for the whole repository history").optional(),
19516
+ limit: z28.coerce.number().int().min(1).max(50).describe("Maximum number of commits to return (default 20)").optional()
18992
19517
  });
18993
19518
  function createGitHistoryTool(workspace) {
19519
+ const root = resolve25(workspace);
19520
+ const file = (path) => {
19521
+ const absolute = resolve25(root, path);
19522
+ const delta = relative18(root, absolute);
19523
+ if (delta === ".." || delta.startsWith(`..${sep18}`) || isAbsolute13(delta)) throw new Error("GitHistory path is outside the workspace");
19524
+ return absolute;
19525
+ };
18994
19526
  return {
18995
19527
  name: "GitHistory",
18996
19528
  description: "Show git commit history for the repository or for one file (follows renames). Use to understand when and why code changed.",
18997
19529
  inputSchema: GitHistoryInput,
18998
19530
  readOnly: true,
18999
- paths: (input) => input.path === void 0 ? [] : [input.path],
19531
+ paths: (input) => input.path === void 0 ? [root] : [file(input.path)],
19000
19532
  summarize: (input) => input.path ?? "repository",
19001
19533
  renderForModel: (output) => output,
19002
19534
  async execute(input, signal) {
19003
19535
  signal.throwIfAborted();
19004
- if (!await isGitRepository(workspace)) throw new Error("Not a git repository");
19536
+ const path = input.path === void 0 ? void 0 : relative18(root, file(input.path));
19537
+ if (!await isGitRepository(root, signal)) throw new Error("Not a git repository");
19005
19538
  const limit = input.limit ?? 20;
19006
- const entries = await fileHistory(workspace, input.path, limit);
19539
+ const entries = await fileHistory(root, path, limit, signal);
19007
19540
  if (entries.length === 0) {
19008
19541
  return input.path === void 0 ? "No commits found." : `No commits found for ${input.path} (the file may be untracked).`;
19009
19542
  }
@@ -19066,7 +19599,8 @@ var QuestionBridge = class {
19066
19599
  }
19067
19600
  async #askRelayed(questions, signal) {
19068
19601
  try {
19069
- const answers = await this.#relay(questions, signal);
19602
+ const answers = await waitWithSignal(this.#relay(questions, signal), signal);
19603
+ signal.throwIfAborted();
19070
19604
  if (answers !== void 0) {
19071
19605
  this.#active = false;
19072
19606
  return answers;
@@ -19078,10 +19612,15 @@ var QuestionBridge = class {
19078
19612
  }
19079
19613
  }
19080
19614
  #askLocally(questions, signal) {
19081
- return new Promise((resolve31, reject) => {
19615
+ if (signal.aborted) {
19616
+ this.#active = false;
19617
+ return Promise.reject(signal.reason);
19618
+ }
19619
+ return new Promise((resolve32, reject) => {
19082
19620
  const onAbort = () => {
19083
19621
  this.#pending = void 0;
19084
19622
  this.#active = false;
19623
+ this.#onChange?.();
19085
19624
  reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
19086
19625
  };
19087
19626
  signal.addEventListener("abort", onAbort, { once: true });
@@ -19092,7 +19631,7 @@ var QuestionBridge = class {
19092
19631
  this.#pending = void 0;
19093
19632
  this.#active = false;
19094
19633
  this.#onChange?.();
19095
- resolve31(answers);
19634
+ resolve32(answers);
19096
19635
  },
19097
19636
  reject: (error) => {
19098
19637
  signal.removeEventListener("abort", onAbort);
@@ -19184,8 +19723,8 @@ function createTodoWriteTool() {
19184
19723
 
19185
19724
  // src/tools/managed.ts
19186
19725
  import { randomUUID as randomUUID16 } from "crypto";
19187
- import { link as link2, lstat as lstat12, mkdir as mkdir17, readFile as readFile20, readdir as readdir9, unlink as unlink5, writeFile as writeFile10 } from "fs/promises";
19188
- import { dirname as dirname18, join as join26, resolve as resolve25 } from "path";
19726
+ import { link as link2, lstat as lstat12, mkdir as mkdir17, readFile as readFile21, readdir as readdir9, unlink as unlink5, writeFile as writeFile10 } from "fs/promises";
19727
+ import { dirname as dirname16, join as join27, resolve as resolve26 } from "path";
19189
19728
  import { z as z31 } from "zod";
19190
19729
  var MANAGED_TOOL_VERSION = 1;
19191
19730
  var MAX_RECORD_BYTES = 256 * 1024;
@@ -19227,9 +19766,9 @@ var ManagedToolStore = class {
19227
19766
  #entries = /* @__PURE__ */ new Map();
19228
19767
  #diagnostics = [];
19229
19768
  constructor(options) {
19230
- this.#workspace = resolve25(options.workspace);
19231
- this.#projectRoot = join26(this.#workspace, ".flavor", "tools");
19232
- this.#globalRoot = join26(resolve25(options.home), ".flavor-code", "tools");
19769
+ this.#workspace = resolve26(options.workspace);
19770
+ this.#projectRoot = join27(this.#workspace, ".flavor", "tools");
19771
+ this.#globalRoot = join27(resolve26(options.home), ".flavor-code", "tools");
19233
19772
  this.#now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
19234
19773
  }
19235
19774
  get diagnostics() {
@@ -19276,7 +19815,7 @@ var ManagedToolStore = class {
19276
19815
  version: MANAGED_TOOL_VERSION,
19277
19816
  createdAt: this.#now()
19278
19817
  });
19279
- const path = join26(this.directory(input.scope), filename(input.name));
19818
+ const path = join27(this.directory(input.scope), filename(input.name));
19280
19819
  const definition = compileDefinition(record, input.scope, this.#workspace);
19281
19820
  await writeExclusive(path, `${JSON.stringify(record, void 0, 2)}
19282
19821
  `);
@@ -19301,7 +19840,7 @@ var ManagedToolStore = class {
19301
19840
  if (!parsed.success) return [];
19302
19841
  const matches = this.#matching(parsed.data.name).filter((entry2) => parsed.data.scope === void 0 || entry2.scope === parsed.data.scope);
19303
19842
  if (matches.length > 0) return matches.map((entry2) => entry2.path);
19304
- return [join26(this.directory(parsed.data.scope ?? "project"), filename(parsed.data.name))];
19843
+ return [join27(this.directory(parsed.data.scope ?? "project"), filename(parsed.data.name))];
19305
19844
  }
19306
19845
  async #loadDirectory(scope2) {
19307
19846
  const root = this.directory(scope2);
@@ -19314,12 +19853,12 @@ var ManagedToolStore = class {
19314
19853
  return;
19315
19854
  }
19316
19855
  for (const name of names) {
19317
- const path = join26(root, name);
19856
+ const path = join27(root, name);
19318
19857
  try {
19319
19858
  const info = await lstat12(path);
19320
19859
  if (!info.isFile() || info.isSymbolicLink()) throw new Error("Managed tool record must be a regular file");
19321
19860
  if (info.size > MAX_RECORD_BYTES) throw new Error(`Managed tool record exceeds ${MAX_RECORD_BYTES} bytes`);
19322
- const record = ManagedToolRecordSchema.parse(JSON.parse(await readFile20(path, "utf8")));
19861
+ const record = ManagedToolRecordSchema.parse(JSON.parse(await readFile21(path, "utf8")));
19323
19862
  if (record.scope !== scope2) throw new Error(`Managed tool record scope must match its ${scope2} directory`);
19324
19863
  const key = scopedKey(scope2, record.name);
19325
19864
  if (this.#entries.has(key)) throw new Error(`Duplicate managed tool name in ${scope2} scope: ${record.name}`);
@@ -19469,7 +20008,7 @@ ${sourceUrl}`
19469
20008
  }
19470
20009
  }
19471
20010
  async function writeExclusive(path, content) {
19472
- await mkdir17(dirname18(path), { recursive: true });
20011
+ await mkdir17(dirname16(path), { recursive: true });
19473
20012
  const temporary = `${path}.${randomUUID16()}.tmp`;
19474
20013
  try {
19475
20014
  await writeFile10(temporary, content, { encoding: "utf8", flag: "wx", mode: 384 });
@@ -20232,8 +20771,8 @@ ${total}`;
20232
20771
 
20233
20772
  // src/memory/store.ts
20234
20773
  import { createHash as createHash16 } from "crypto";
20235
- import { readFile as readFile21, rm as rm9 } from "fs/promises";
20236
- import { resolve as resolve26, join as join27, relative as relative18, sep as sep18 } from "path";
20774
+ import { readFile as readFile22, rm as rm9 } from "fs/promises";
20775
+ import { resolve as resolve27, join as join28, relative as relative19, sep as sep19 } from "path";
20237
20776
 
20238
20777
  // src/memory/similarity.ts
20239
20778
  function normalizeForSimilarity(value) {
@@ -20335,10 +20874,10 @@ var MemoryStore = class {
20335
20874
  constructor(options) {
20336
20875
  if (!Number.isSafeInteger(options.maxEntries) || options.maxEntries < 1) throw new Error("maxEntries must be an integer of at least 1");
20337
20876
  if (!Number.isSafeInteger(options.maxEntryChars) || options.maxEntryChars < 1) throw new Error("maxEntryChars must be an integer of at least 1");
20338
- this.workspace = resolve26(options.workspace);
20339
- this.#memoryRoot = join27(this.workspace, ".flavor", "memory");
20340
- this.path = join27(this.#memoryRoot, "MEMORY.md");
20341
- this.#behaviorPath = join27(this.#memoryRoot, "behavior.json");
20877
+ this.workspace = resolve27(options.workspace);
20878
+ this.#memoryRoot = join28(this.workspace, ".flavor", "memory");
20879
+ this.path = join28(this.#memoryRoot, "MEMORY.md");
20880
+ this.#behaviorPath = join28(this.#memoryRoot, "behavior.json");
20342
20881
  this.#maxEntries = options.maxEntries;
20343
20882
  this.#maxEntryChars = options.maxEntryChars;
20344
20883
  }
@@ -20556,14 +21095,14 @@ var MemoryStore = class {
20556
21095
  });
20557
21096
  }
20558
21097
  async #readTaskItem(reference) {
20559
- const document = decodeTask(await readFile21(this.#resolveContentPath(reference.contentPath), "utf8"));
21098
+ const document = decodeTask(await readFile22(this.#resolveContentPath(reference.contentPath), "utf8"));
20560
21099
  return document.items.find((item) => item.id === reference.id)?.content ?? reference.summary;
20561
21100
  }
20562
21101
  #resolveContentPath(contentPath) {
20563
21102
  if (!/^tasks\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.md$/.test(contentPath)) throw new Error("Invalid memory content path");
20564
- const path = resolve26(this.#memoryRoot, ...contentPath.split("/"));
20565
- const rel = relative18(this.#memoryRoot, path);
20566
- if (rel === ".." || rel.startsWith(`..${sep18}`)) throw new Error("Memory content path escapes the memory directory");
21103
+ const path = resolve27(this.#memoryRoot, ...contentPath.split("/"));
21104
+ const rel = relative19(this.#memoryRoot, path);
21105
+ if (rel === ".." || rel.startsWith(`..${sep19}`)) throw new Error("Memory content path escapes the memory directory");
20567
21106
  return path;
20568
21107
  }
20569
21108
  async #updateIndex(update) {
@@ -21032,8 +21571,8 @@ function createExecutionEnvironment(workspace, config) {
21032
21571
  }
21033
21572
 
21034
21573
  // src/ide/client.ts
21035
- import { readFile as readFile22, readdir as readdir10, stat as stat10 } from "fs/promises";
21036
- import { basename as basename6, join as join28, relative as relative19, resolve as resolve27, sep as sep19 } from "path";
21574
+ import { readFile as readFile23, readdir as readdir10, stat as stat11 } from "fs/promises";
21575
+ import { basename as basename6, join as join29, relative as relative20, resolve as resolve28, sep as sep20 } from "path";
21037
21576
  var FlavorIdeClient = class {
21038
21577
  #workspace;
21039
21578
  #home;
@@ -21044,8 +21583,8 @@ var FlavorIdeClient = class {
21044
21583
  #sessionId;
21045
21584
  #eventTail = Promise.resolve();
21046
21585
  constructor(options) {
21047
- this.#workspace = resolve27(options.workspace);
21048
- this.#home = resolve27(options.home);
21586
+ this.#workspace = resolve28(options.workspace);
21587
+ this.#home = resolve28(options.home);
21049
21588
  this.#environment = options.environment ?? process.env;
21050
21589
  this.#fetch = options.fetch ?? fetch;
21051
21590
  }
@@ -21129,7 +21668,7 @@ var FlavorIdeClient = class {
21129
21668
  };
21130
21669
  if (await this.#healthy(configured)) return configured;
21131
21670
  }
21132
- const directory = join28(this.#home, ".flavor-code", "ide");
21671
+ const directory = join29(this.#home, ".flavor-code", "ide");
21133
21672
  let entries;
21134
21673
  try {
21135
21674
  entries = (await readdir10(directory)).filter((name) => name.endsWith(".lock"));
@@ -21137,9 +21676,9 @@ var FlavorIdeClient = class {
21137
21676
  return void 0;
21138
21677
  }
21139
21678
  const candidates = (await Promise.all(entries.map(async (name) => {
21140
- const path = join28(directory, name);
21679
+ const path = join29(directory, name);
21141
21680
  try {
21142
- const [contents, info] = await Promise.all([readFile22(path, "utf8"), stat10(path)]);
21681
+ const [contents, info] = await Promise.all([readFile23(path, "utf8"), stat11(path)]);
21143
21682
  const lock = parseLockfile(contents);
21144
21683
  return lock === void 0 ? void 0 : { lock, modified: info.mtimeMs };
21145
21684
  } catch {
@@ -21243,21 +21782,22 @@ function parsePort(value) {
21243
21782
  return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
21244
21783
  }
21245
21784
  function isWithin8(parent, child) {
21246
- const base = normalize(resolve27(parent));
21247
- const target = normalize(resolve27(child));
21248
- return target === base || target.startsWith(`${base}${sep19}`);
21785
+ const base = normalize(resolve28(parent));
21786
+ const target = normalize(resolve28(child));
21787
+ return target === base || target.startsWith(`${base}${sep20}`);
21249
21788
  }
21250
21789
  function normalize(path) {
21251
21790
  const normalized = path.normalize("NFC");
21252
21791
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
21253
21792
  }
21254
21793
  function relativeDisplay(workspace, filePath) {
21255
- const path = relative19(workspace, filePath).replaceAll("\\", "/");
21794
+ const path = relative20(workspace, filePath).replaceAll("\\", "/");
21256
21795
  return path && !path.startsWith("../") ? path : basename6(filePath);
21257
21796
  }
21258
21797
 
21259
21798
  // src/jobs/registry.ts
21260
21799
  import { randomUUID as randomUUID17 } from "crypto";
21800
+ var OUTPUT_SLICE_SLACK_CHARS = 64 * 1024;
21261
21801
  var JobRegistry = class {
21262
21802
  #jobs = /* @__PURE__ */ new Map();
21263
21803
  #maxOutputChars;
@@ -21271,11 +21811,12 @@ var JobRegistry = class {
21271
21811
  }
21272
21812
  create(input) {
21273
21813
  this.#pruneCompleted();
21814
+ if (this.#jobs.size >= this.#maxJobs) throw new Error(`Background job limit (${this.#maxJobs}) reached; wait for or cancel an existing job first`);
21274
21815
  const id = `job-${randomUUID17().slice(0, 12)}`;
21275
21816
  const now = (/* @__PURE__ */ new Date()).toISOString();
21276
21817
  let settle;
21277
- const done = new Promise((resolve31) => {
21278
- settle = resolve31;
21818
+ const done = new Promise((resolve32) => {
21819
+ settle = resolve32;
21279
21820
  });
21280
21821
  const record = {
21281
21822
  id,
@@ -21285,7 +21826,8 @@ var JobRegistry = class {
21285
21826
  updatedAt: now,
21286
21827
  outputChars: 0,
21287
21828
  truncated: false,
21288
- output: "",
21829
+ chunks: [],
21830
+ windowChars: 0,
21289
21831
  outputStart: 0,
21290
21832
  settle,
21291
21833
  done
@@ -21296,11 +21838,16 @@ var JobRegistry = class {
21296
21838
  id,
21297
21839
  append: (text) => {
21298
21840
  if (record.state !== "running" || text === "") return;
21299
- record.output += text;
21841
+ record.chunks.push(text);
21842
+ record.windowChars += text.length;
21300
21843
  record.outputChars += text.length;
21301
- if (record.output.length > this.#maxOutputChars) {
21302
- const removed = record.output.length - this.#maxOutputChars;
21303
- record.output = record.output.slice(removed);
21844
+ if (record.outputChars > this.#maxOutputChars) record.truncated = true;
21845
+ if (record.windowChars > this.#maxOutputChars + OUTPUT_SLICE_SLACK_CHARS) {
21846
+ const joined = record.chunks.join("");
21847
+ const kept = joined.slice(joined.length - this.#maxOutputChars);
21848
+ const removed = joined.length - kept.length;
21849
+ record.chunks = [kept];
21850
+ record.windowChars = kept.length;
21304
21851
  record.outputStart += removed;
21305
21852
  record.truncated = true;
21306
21853
  }
@@ -21309,7 +21856,7 @@ var JobRegistry = class {
21309
21856
  },
21310
21857
  complete: (result = {}) => this.#finish(
21311
21858
  record,
21312
- result.exitCode !== void 0 && result.exitCode !== null && result.exitCode !== 0 ? "failed" : "completed",
21859
+ result.error !== void 0 || result.exitCode !== void 0 && result.exitCode !== null && result.exitCode !== 0 ? "failed" : "completed",
21313
21860
  result
21314
21861
  ),
21315
21862
  fail: (error) => this.#finish(record, "failed", { error: error instanceof Error ? error.message : String(error) }),
@@ -21321,25 +21868,40 @@ var JobRegistry = class {
21321
21868
  }
21322
21869
  read(id, owner2, cursor = 0) {
21323
21870
  const job = this.#owned(id, owner2);
21324
- const effective = Math.max(cursor, job.outputStart);
21325
- return { ...snapshot2(job), output: job.output.slice(effective - job.outputStart), cursor: job.outputStart + job.output.length };
21871
+ const window = job.chunks.join("");
21872
+ const dropped = Math.max(0, window.length - this.#maxOutputChars);
21873
+ const effective = Math.max(cursor, job.outputStart + dropped);
21874
+ return { ...snapshot2(job), output: window.slice(effective - job.outputStart), cursor: job.outputStart + window.length };
21875
+ }
21876
+ /** Retained sizes for the rotation census. */
21877
+ census() {
21878
+ let windowChars = 0;
21879
+ let running = 0;
21880
+ for (const job of this.#jobs.values()) {
21881
+ windowChars += job.windowChars;
21882
+ if (job.state === "running") running += 1;
21883
+ }
21884
+ return { jobs: this.#jobs.size, running, windowChars };
21326
21885
  }
21327
21886
  async wait(id, owner2, signal) {
21887
+ signal?.throwIfAborted();
21328
21888
  const job = this.#owned(id, owner2);
21329
21889
  if (job.state !== "running") return snapshot2(job);
21330
- if (signal === void 0) return job.done;
21331
- return Promise.race([job.done, new Promise((_resolve, reject) => {
21332
- if (signal.aborted) {
21333
- reject(signal.reason);
21334
- return;
21335
- }
21336
- signal.addEventListener("abort", () => reject(signal.reason), { once: true });
21337
- })]);
21890
+ return waitWithSignal(job.done, signal);
21338
21891
  }
21339
21892
  kill(id, owner2) {
21340
21893
  const job = this.#owned(id, owner2);
21341
21894
  if (job.state !== "running") return;
21342
- void job.cancel?.();
21895
+ try {
21896
+ const cancellation = job.cancel?.();
21897
+ if (cancellation !== void 0) return cancellation.then(
21898
+ () => this.#finish(job, "cancelled", {}),
21899
+ (error) => this.#finish(job, "failed", { error: `Cancellation failed: ${error instanceof Error ? error.message : String(error)}` })
21900
+ );
21901
+ } catch (error) {
21902
+ this.#finish(job, "failed", { error: `Cancellation failed: ${error instanceof Error ? error.message : String(error)}` });
21903
+ return;
21904
+ }
21343
21905
  this.#finish(job, "cancelled", {});
21344
21906
  }
21345
21907
  subscribe(listener) {
@@ -21399,7 +21961,7 @@ function snapshot2(job) {
21399
21961
  // src/terminal/service.ts
21400
21962
  import { randomUUID as randomUUID18 } from "crypto";
21401
21963
  import { createRequire as nodeCreateRequire2 } from "module";
21402
- import { isAbsolute as isAbsolute13, relative as relative20, resolve as resolve28, sep as sep20 } from "path";
21964
+ import { isAbsolute as isAbsolute14, relative as relative21, resolve as resolve29, sep as sep21 } from "path";
21403
21965
  var TerminalService = class {
21404
21966
  #root;
21405
21967
  #factory;
@@ -21407,10 +21969,11 @@ var TerminalService = class {
21407
21969
  #maxOutputChars;
21408
21970
  #sessions = /* @__PURE__ */ new Map();
21409
21971
  constructor(workspace, options = {}) {
21410
- this.#root = resolve28(workspace);
21972
+ this.#root = resolve29(workspace);
21411
21973
  this.#factory = options.factory ?? defaultFactory;
21412
21974
  this.#jobs = options.jobs;
21413
21975
  this.#maxOutputChars = options.maxOutputChars ?? 2e5;
21976
+ if (!Number.isSafeInteger(this.#maxOutputChars) || this.#maxOutputChars <= 0) throw new Error("maxOutputChars must be a positive integer");
21414
21977
  }
21415
21978
  open(input) {
21416
21979
  const cwd = workspacePath(this.#root, input.cwd ?? ".");
@@ -21437,7 +22000,13 @@ var TerminalService = class {
21437
22000
  truncated: false,
21438
22001
  disposables: []
21439
22002
  };
21440
- const job = this.#jobs?.create({ kind: "terminal", owner: input.owner, label: shell, cancel: () => this.close(id, input.owner) });
22003
+ let job;
22004
+ try {
22005
+ job = this.#jobs?.create({ kind: "terminal", owner: input.owner, label: shell, cancel: () => this.close(id, input.owner) });
22006
+ } catch (error) {
22007
+ pty.kill();
22008
+ throw error;
22009
+ }
21441
22010
  if (job !== void 0) {
21442
22011
  jobId = job.id;
21443
22012
  record.jobId = job.id;
@@ -21463,10 +22032,10 @@ var TerminalService = class {
21463
22032
  return { ...snapshot3(record), ...jobId === void 0 ? {} : { jobId } };
21464
22033
  }
21465
22034
  write(id, owner2, data2) {
21466
- this.#owned(id, owner2).pty.write(data2);
22035
+ this.#running(id, owner2).pty.write(data2);
21467
22036
  }
21468
22037
  resize(id, owner2, columns, rows) {
21469
- this.#owned(id, owner2).pty.resize(columns, rows);
22038
+ this.#running(id, owner2).pty.resize(columns, rows);
21470
22039
  }
21471
22040
  read(id, owner2, cursor = 0) {
21472
22041
  const record = this.#owned(id, owner2);
@@ -21497,6 +22066,11 @@ var TerminalService = class {
21497
22066
  if (record.owner !== owner2) throw new Error("Terminal owner mismatch");
21498
22067
  return record;
21499
22068
  }
22069
+ #running(id, owner2) {
22070
+ const record = this.#owned(id, owner2);
22071
+ if (record.state !== "running") throw new Error(`Terminal ${id} is ${record.state}; use TerminalOpen to start a new session`);
22072
+ return record;
22073
+ }
21500
22074
  };
21501
22075
  function snapshot3(record) {
21502
22076
  return {
@@ -21525,15 +22099,15 @@ function cleanEnvironment(environment) {
21525
22099
  return Object.fromEntries(Object.entries(environment).filter((entry2) => entry2[1] !== void 0));
21526
22100
  }
21527
22101
  function workspacePath(root, input) {
21528
- const path = resolve28(root, input);
21529
- const delta = relative20(root, path);
21530
- if (delta === ".." || delta.startsWith(`..${sep20}`) || isAbsolute13(delta)) throw new Error("Terminal cwd is outside the workspace");
22102
+ const path = resolve29(root, input);
22103
+ const delta = relative21(root, path);
22104
+ if (delta === ".." || delta.startsWith(`..${sep21}`) || isAbsolute14(delta)) throw new Error("Terminal cwd is outside the workspace");
21531
22105
  return path;
21532
22106
  }
21533
22107
 
21534
22108
  // src/tools/terminal.ts
21535
22109
  import { z as z33 } from "zod";
21536
- import { resolve as resolve29 } from "path";
22110
+ import { resolve as resolve30 } from "path";
21537
22111
  var Id = z33.string().regex(/^term-[a-f0-9-]+$/);
21538
22112
  var OpenInput = z33.object({ cwd: z33.string().optional(), shell: z33.string().optional(), args: z33.array(z33.string()).optional(), columns: z33.coerce.number().int().min(10).max(500).optional(), rows: z33.coerce.number().int().min(2).max(300).optional() });
21539
22113
  var OptionalBoolean = z33.union([z33.boolean(), z33.string().refine((value) => value === "true" || value === "false")]);
@@ -21547,8 +22121,8 @@ function createTerminalTools(terminals, workspace = process.cwd()) {
21547
22121
  name: "TerminalOpen",
21548
22122
  description: "Open a persistent interactive pseudo-terminal in the workspace",
21549
22123
  inputSchema: OpenInput,
21550
- paths: (input) => [resolve29(workspace, input.cwd ?? ".")],
21551
- permissions: (input) => ({ paths: [resolve29(workspace, input.cwd ?? ".")], command: input.shell ?? "system-shell", args: input.args ?? [], cwd: resolve29(workspace, input.cwd ?? ".") }),
22124
+ paths: (input) => [resolve30(workspace, input.cwd ?? ".")],
22125
+ permissions: (input) => ({ paths: [resolve30(workspace, input.cwd ?? ".")], command: input.shell ?? "system-shell", args: input.args ?? [], cwd: resolve30(workspace, input.cwd ?? ".") }),
21552
22126
  execute: async (input, _signal, context) => terminals.open({
21553
22127
  owner: owner(context),
21554
22128
  ...input.cwd === void 0 ? {} : { cwd: input.cwd },
@@ -21577,7 +22151,7 @@ function createTerminalTools(terminals, workspace = process.cwd()) {
21577
22151
  inputSchema: ReadInput3,
21578
22152
  paths: () => [],
21579
22153
  execute: async (input, _signal, context) => terminals.read(input.id, owner(context), input.cursor ?? 0),
21580
- renderForModel: (output) => output.output,
22154
+ renderForModel: (output) => JSON.stringify(output),
21581
22155
  presentResult: (output) => {
21582
22156
  const result = output;
21583
22157
  return {
@@ -21774,8 +22348,8 @@ var PalClient = class {
21774
22348
  let disconnectTimer;
21775
22349
  await Promise.race([
21776
22350
  this.request({ version: 1, type: "disconnect", requestId: randomUUID19() }).catch(() => void 0),
21777
- new Promise((resolve31) => {
21778
- disconnectTimer = setTimeout(resolve31, Math.min(this.requestTimeoutMs, 250));
22351
+ new Promise((resolve32) => {
22352
+ disconnectTimer = setTimeout(resolve32, Math.min(this.requestTimeoutMs, 250));
21779
22353
  })
21780
22354
  ]);
21781
22355
  if (disconnectTimer !== void 0) clearTimeout(disconnectTimer);
@@ -21821,7 +22395,7 @@ var PalClient = class {
21821
22395
  return presence;
21822
22396
  }
21823
22397
  openSocket() {
21824
- return new Promise((resolve31, reject) => {
22398
+ return new Promise((resolve32, reject) => {
21825
22399
  let settled = false;
21826
22400
  const rejectConnection = (error) => {
21827
22401
  if (settled) return;
@@ -21844,7 +22418,7 @@ var PalClient = class {
21844
22418
  settled = true;
21845
22419
  if (this.connectingReject === rejectConnection) this.connectingReject = void 0;
21846
22420
  socket.off("error", onInitialError);
21847
- resolve31();
22421
+ resolve32();
21848
22422
  };
21849
22423
  const onInitialError = (error) => {
21850
22424
  socket.off("connect", onConnect);
@@ -21875,12 +22449,12 @@ var PalClient = class {
21875
22449
  if (this.closed || socket === void 0 || socket.destroyed || !socket.writable) {
21876
22450
  return Promise.reject(new Error(this.closed ? "Pal client is closed" : "Pal client is not connected"));
21877
22451
  }
21878
- return new Promise((resolve31, reject) => {
22452
+ return new Promise((resolve32, reject) => {
21879
22453
  const timer = setTimeout(() => {
21880
22454
  this.pending.delete(request.requestId);
21881
22455
  reject(new Error(`Pal request '${request.type}' timed out`));
21882
22456
  }, this.requestTimeoutMs);
21883
- this.pending.set(request.requestId, { resolve: resolve31, reject, timer });
22457
+ this.pending.set(request.requestId, { resolve: resolve32, reject, timer });
21884
22458
  let frame;
21885
22459
  try {
21886
22460
  frame = encodeControlFrame(BrokerRequestSchema.parse(request));
@@ -21984,12 +22558,12 @@ var PalClient = class {
21984
22558
  }
21985
22559
  scheduleReconnect() {
21986
22560
  if (this.reconnectTimer !== void 0 || this.closed) return;
21987
- const delay3 = this.reconnectDelayMs;
22561
+ const delay2 = this.reconnectDelayMs;
21988
22562
  this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, this.reconnectMaxDelayMs);
21989
22563
  this.reconnectTimer = setTimeout(() => {
21990
22564
  this.reconnectTimer = void 0;
21991
22565
  void this.start().catch(() => this.scheduleReconnect());
21992
- }, delay3);
22566
+ }, delay2);
21993
22567
  this.reconnectTimer.unref?.();
21994
22568
  }
21995
22569
  };
@@ -22091,6 +22665,7 @@ function createPalsTools(client, options) {
22091
22665
  execute: async (input, signal) => {
22092
22666
  signal.throwIfAborted();
22093
22667
  const recipientId = await assertSendTarget(client, input.target, input.coWorkId);
22668
+ signal.throwIfAborted();
22094
22669
  return client.sendChat(recipientId, shareGuard.protect(input.message));
22095
22670
  }
22096
22671
  },
@@ -22114,6 +22689,7 @@ function createPalsTools(client, options) {
22114
22689
  execute: async (input, signal) => {
22115
22690
  signal.throwIfAborted();
22116
22691
  assertRequiredParticipant(await client.coWorkStatus(input.plan.coWorkId), options.selfId);
22692
+ signal.throwIfAborted();
22117
22693
  const plan = CoWorkPlanSchema.parse({
22118
22694
  ...input.plan,
22119
22695
  goal: shareGuard.protect(input.plan.goal),
@@ -22130,11 +22706,13 @@ function createPalsTools(client, options) {
22130
22706
  execute: async (input, signal) => {
22131
22707
  signal.throwIfAborted();
22132
22708
  let state = await assertCurrentToken(client, input);
22709
+ signal.throwIfAborted();
22133
22710
  assertRequiredParticipant(state, options.selfId);
22134
22711
  if (!state.planAcceptedParticipantIds.includes(options.selfId)) {
22135
22712
  state = await client.coWorkAction({ type: "cowork-plan-accept", ...input });
22136
22713
  }
22137
22714
  if (state.readyParticipantIds.includes(options.selfId) || state.phase === "running") return state;
22715
+ signal.throwIfAborted();
22138
22716
  return client.coWorkAction({ type: "cowork-ready", ...input });
22139
22717
  }
22140
22718
  },
@@ -22148,6 +22726,7 @@ function createPalsTools(client, options) {
22148
22726
  execute: async (input, signal) => {
22149
22727
  signal.throwIfAborted();
22150
22728
  const recipientId = await assertSendTarget(client, input.target, input.coWorkId);
22729
+ signal.throwIfAborted();
22151
22730
  return client.sendChat(recipientId, `[co-work ${input.coWorkId} progress] ${shareGuard.protect(input.detail)}`);
22152
22731
  }
22153
22732
  },
@@ -22161,6 +22740,7 @@ function createPalsTools(client, options) {
22161
22740
  execute: async (input, signal) => {
22162
22741
  signal.throwIfAborted();
22163
22742
  const state = await assertCurrentToken(client, input);
22743
+ signal.throwIfAborted();
22164
22744
  assertRequiredParticipant(state, options.selfId);
22165
22745
  if (state.phase !== "running" || !state.acceptedParticipantIds.includes(options.selfId) || !state.readyParticipantIds.includes(options.selfId)) {
22166
22746
  throw new Error("A required participant must be accepted, ready, and running before reporting completion");
@@ -22178,6 +22758,7 @@ function createPalsTools(client, options) {
22178
22758
  execute: async (input, signal) => {
22179
22759
  signal.throwIfAborted();
22180
22760
  const state = await assertCurrentToken(client, input);
22761
+ signal.throwIfAborted();
22181
22762
  assertRequiredParticipant(state, options.selfId);
22182
22763
  if (state.integrationOwnerId !== options.selfId) throw new Error("Only the broker-designated integration owner can finalize integration");
22183
22764
  if (state.phase !== "verifying") throw new Error("Co-work is not ready for integration verification");
@@ -22284,8 +22865,8 @@ var ApprovalBridge = class {
22284
22865
  }
22285
22866
  };
22286
22867
  async function createProductionRuntime(options) {
22287
- const workspace = resolve30(options.workspace ?? process.cwd());
22288
- const home = resolve30(options.home ?? homedir3());
22868
+ const workspace = resolve31(options.workspace ?? process.cwd());
22869
+ const home = resolve31(options.home ?? homedir3());
22289
22870
  const environment = options.environment ?? process.env;
22290
22871
  const ide = new FlavorIdeClient({ workspace, home, environment });
22291
22872
  const ideReady = ide.initialize();
@@ -22459,6 +23040,7 @@ async function createProductionRuntime(options) {
22459
23040
  const executionEnvironment = createExecutionEnvironment(workspace, config.execution);
22460
23041
  const jobs = new JobRegistry();
22461
23042
  const terminals = new TerminalService(workspace, { jobs });
23043
+ const lspManager = new RealLspManager({ workspace, onStatus: (message2) => emitOutput({ type: "notice", message: message2 }) });
22462
23044
  const observations = new FileObservationStore();
22463
23045
  const workspaceInstructions = new WorkspaceInstructions(workspace);
22464
23046
  const fileMutationOptions = {
@@ -22482,9 +23064,7 @@ async function createProductionRuntime(options) {
22482
23064
  createWebSearchTool(),
22483
23065
  ...createJobTools(jobs),
22484
23066
  ...createTerminalTools(terminals, workspace),
22485
- ...createLspTools(workspace, {
22486
- onStatus: (message2) => emitOutput({ type: "notice", message: message2 })
22487
- }),
23067
+ ...createLspTools(workspace, { manager: lspManager }),
22488
23068
  ...options.approvalPolicy === "deny" ? [] : [createAskUserQuestionTool(askUserQuestionHandler)],
22489
23069
  createTaskOutputTool(),
22490
23070
  createTodoWriteTool()
@@ -22529,8 +23109,8 @@ async function createProductionRuntime(options) {
22529
23109
  const registeredProviders = registration.registered;
22530
23110
  let effectiveLlm = registration.effectiveLlm;
22531
23111
  const pluginHost = new PluginHost({
22532
- globalPluginDirs: [join29(home, ".flavor-code", "plugins")],
22533
- projectPluginDirs: [join29(workspace, ".flavor", "plugins")],
23112
+ globalPluginDirs: [join30(home, ".flavor-code", "plugins")],
23113
+ projectPluginDirs: [join30(workspace, ".flavor", "plugins")],
22534
23114
  config,
22535
23115
  // Existing project plugins (including astgraph and superharness) use Node.js
22536
23116
  // built-ins. The Worker/vm sandbox deliberately blocks those imports, so it
@@ -22639,7 +23219,7 @@ async function createProductionRuntime(options) {
22639
23219
  const address = palSocketAddress({
22640
23220
  platform: process.platform,
22641
23221
  userScope: home,
22642
- runtimeDir: join29(tmpdir4(), `flavor-code-pals-${createHash18("sha256").update(home).digest("hex").slice(0, 16)}`)
23222
+ runtimeDir: join30(tmpdir4(), `flavor-code-pals-${createHash18("sha256").update(home).digest("hex").slice(0, 16)}`)
22643
23223
  });
22644
23224
  const baseCollaborationClient = collaboration.client ?? collaboration.createClient?.({
22645
23225
  instanceId: collaboration.instanceId,
@@ -22750,8 +23330,8 @@ async function createProductionRuntime(options) {
22750
23330
  diagnostics.push(`MCP servers could not start: ${message(error)}`);
22751
23331
  });
22752
23332
  const skills = new SkillRegistry({
22753
- globalRoots: [join29(home, ".flavor-code", "skills")],
22754
- projectRoots: [join29(workspace, ".flavor", "skills"), ...pluginSkillRoots],
23333
+ globalRoots: [join30(home, ".flavor-code", "skills")],
23334
+ projectRoots: [join30(workspace, ".flavor", "skills"), ...pluginSkillRoots],
22755
23335
  authorizeResource: async () => true,
22756
23336
  disabledNames: config.skills.disabled
22757
23337
  });
@@ -22759,7 +23339,7 @@ async function createProductionRuntime(options) {
22759
23339
  skillsReady.catch(() => void 0);
22760
23340
  tools.push(createSkillTool(skills), createSkillResourceTool(skills));
22761
23341
  if (options.extraTools !== void 0) tools.push(...options.extraTools);
22762
- const flavor = await optionalText2(join29(workspace, "FLAVOR.md"));
23342
+ const flavor = await optionalText2(join30(workspace, "FLAVOR.md"));
22763
23343
  const instructionBaseline = await workspaceInstructions.baseline();
22764
23344
  let memoryContext;
22765
23345
  if (memoryStore !== void 0) {
@@ -23021,6 +23601,8 @@ async function createProductionRuntime(options) {
23021
23601
  permissionPolicy,
23022
23602
  maxIterationsMain: config.maxIterations.main,
23023
23603
  maxIterationsSubagent: config.maxIterations.subagent,
23604
+ softLimitFactor: config.maxIterations.softLimitFactor,
23605
+ extendIterations: config.maxIterations.extendBy,
23024
23606
  hasActiveProgress,
23025
23607
  afterToolSuccess: async (_tool, paths, _input, _output, context) => workspaceInstructions.discover(paths, context.ownerId ?? context.agent),
23026
23608
  toolJournal: {
@@ -23342,7 +23924,7 @@ async function createProductionRuntime(options) {
23342
23924
  evolveService.toolDefinition(),
23343
23925
  ...mcpTools
23344
23926
  ];
23345
- const loopFlavor = await optionalText2(join29(input.workspace, "FLAVOR.md"));
23927
+ const loopFlavor = await optionalText2(join30(input.workspace, "FLAVOR.md"));
23346
23928
  const loopInstructionBaseline = await loopInstructions.baseline();
23347
23929
  const loopEnvironment = createPromptEnvironment({
23348
23930
  now: /* @__PURE__ */ new Date(),
@@ -23412,6 +23994,8 @@ async function createProductionRuntime(options) {
23412
23994
  permissionPolicy,
23413
23995
  maxIterationsMain: config.maxIterations.main,
23414
23996
  maxIterationsSubagent: config.maxIterations.subagent,
23997
+ softLimitFactor: config.maxIterations.softLimitFactor,
23998
+ extendIterations: config.maxIterations.extendBy,
23415
23999
  loopMode: true,
23416
24000
  afterToolSuccess: async (_tool, paths, _input, _output, context) => loopInstructions.discover(paths, context.ownerId ?? context.agent),
23417
24001
  toolJournal: {
@@ -23448,12 +24032,83 @@ async function createProductionRuntime(options) {
23448
24032
  await loopExecutionEnvironment?.dispose();
23449
24033
  }
23450
24034
  };
24035
+ let activeLongTask;
24036
+ const readMemoryRestartMarker = () => {
24037
+ try {
24038
+ return parseMemoryRestartMarker(readFileSync3(memoryRestartMarkerPath(workspace), "utf8"));
24039
+ } catch {
24040
+ return void 0;
24041
+ }
24042
+ };
24043
+ const currentPressureReading = () => verifiedHeapPressure(MEMORY_PRESSURE_HEAP_RATIO) ?? { ...readHeap(), gcVerified: false };
24044
+ const collectRotationHolders = () => {
24045
+ const holders = {};
24046
+ try {
24047
+ const context = harness.main.context.census();
24048
+ holders.context = { entries: context.messages, chars: context.messageChars };
24049
+ holders.contextVisibility = { entries: context.visibilityEntries, chars: context.visibilityChars };
24050
+ holders.contextTransient = { entries: context.transientEntries, chars: context.transientChars };
24051
+ } catch {
24052
+ }
24053
+ try {
24054
+ holders.journal = { entries: harnessJournal.recordCount };
24055
+ } catch {
24056
+ }
24057
+ try {
24058
+ const jobCensus = jobs.census();
24059
+ holders.jobs = { entries: jobCensus.jobs, chars: jobCensus.windowChars };
24060
+ } catch {
24061
+ }
24062
+ try {
24063
+ holders.sessionTree = { entries: sessionHistory.nodeCount };
24064
+ } catch {
24065
+ }
24066
+ return holders;
24067
+ };
24068
+ const rotateForHeap = (continuation, reading) => {
24069
+ requestMemoryRestart(workspace, sessionId, emitOutput, {
24070
+ ...continuation === void 0 ? {} : { continuation },
24071
+ ...reading === void 0 ? {} : { reading },
24072
+ holders: collectRotationHolders()
24073
+ }, options.onMemoryRestartRequested);
24074
+ };
24075
+ const maybeRotateAtBoundary = (continuation) => {
24076
+ activeLongTask = continuation;
24077
+ const pressure = verifiedHeapPressure(MEMORY_PRESSURE_HEAP_RATIO);
24078
+ if (pressure !== void 0) {
24079
+ rotateForHeap(continuation, pressure);
24080
+ return;
24081
+ }
24082
+ const reading = heapRotationNeeded();
24083
+ if (reading === void 0) return;
24084
+ if (rotationCooldownActive(readMemoryRestartMarker(), sessionId, /* @__PURE__ */ new Date())) return;
24085
+ rotateForHeap(continuation, reading);
24086
+ };
24087
+ const maybeRotateAtTurnBoundary = () => {
24088
+ const reading = heapRotationNeeded();
24089
+ if (reading === void 0) return;
24090
+ if (rotationCooldownActive(readMemoryRestartMarker(), sessionId, /* @__PURE__ */ new Date())) return;
24091
+ rotateForHeap(void 0, reading);
24092
+ };
24093
+ const consumePendingContinuation = () => {
24094
+ const marker = readMemoryRestartMarker();
24095
+ const continuation = pendingContinuation(marker, sessionId, /* @__PURE__ */ new Date());
24096
+ if (continuation === void 0 || marker === void 0) return void 0;
24097
+ try {
24098
+ const markerPath = memoryRestartMarkerPath(workspace);
24099
+ writeMarkerAtomically(markerPath, consumedMemoryRestartMarker(marker));
24100
+ } catch {
24101
+ }
24102
+ return continuation;
24103
+ };
23451
24104
  const loopStore = new LoopStore({ workspace });
23452
24105
  const loopOrchestrator = new LoopOrchestrator({
23453
24106
  workspace,
23454
24107
  config: config.loop,
23455
24108
  persistence: loopStore,
23456
24109
  hallucinationGuard,
24110
+ onCycleBoundary: (loopId) => maybeRotateAtBoundary({ kind: "loop", id: loopId }),
24111
+ onMemoryCheckpoint: (loopId) => maybeRotateAtBoundary({ kind: "loop", id: loopId }),
23457
24112
  prepareWorkspace: (input) => prepareLoopWorkspace(input),
23458
24113
  inferVerification: inferVerificationPlan,
23459
24114
  runWorker: ({ workspace: executionWorkspace, prompt, signal }) => runLoopWorker({ workspace: executionWorkspace, prompt, signal }),
@@ -23490,9 +24145,11 @@ async function createProductionRuntime(options) {
23490
24145
  plannerModelId: mainModel,
23491
24146
  classifierModelId: mainModel,
23492
24147
  skepticCount: 3,
23493
- maxRounds: 5,
23494
- maxStallStreak: 2,
24148
+ maxRounds: config.goal.maxRounds,
24149
+ maxStallStreak: config.goal.maxStallStreak,
23495
24150
  persistence: goalStore,
24151
+ onRoundBoundary: (goalId) => maybeRotateAtBoundary({ kind: "goal", id: goalId }),
24152
+ onMemoryCheckpoint: (goalId) => maybeRotateAtBoundary({ kind: "goal", id: goalId }),
23496
24153
  verifyHost: async (signal) => runVerificationPlan(
23497
24154
  await inferVerificationPlan(workspace),
23498
24155
  workspace,
@@ -23587,30 +24244,49 @@ async function createProductionRuntime(options) {
23587
24244
  contextEpoch: harness.main.context.snapshot().epoch?.id ?? "legacy"
23588
24245
  });
23589
24246
  const turnId = harnessJournal.startTurn(turnConfig, { prompt, initialUserMessage: runOptions?.initialUserMessage });
23590
- return monitorMemoryRestart(durableTurn(persistAfter(runMain(
23591
- harness,
23592
- skills,
23593
- prompt,
23594
- signal,
23595
- selectedModels.mainError,
23596
- memoryStore === void 0 || !memoryHasRoutableEntries && userMemoryContext === void 0 ? void 0 : {
23597
- store: memoryStore,
23598
- taskId: memoryLifecycle.taskId ?? sessionId,
23599
- topK: config.memory.retrievalTopK,
23600
- maxChars: config.memory.maxPromptChars
23601
- },
23602
- runOptions?.getSteeringMessages,
23603
- runOptions?.initialUserMessage,
23604
- runOptions?.additionalContext,
23605
- ide
23606
- ), persist), harnessJournal, turnId, turnConfig), () => requestMemoryRestart(workspace, sessionId, emitOutput));
24247
+ return monitorTurnMemory(
24248
+ durableTurn(persistAfter(runMain(
24249
+ harness,
24250
+ skills,
24251
+ prompt,
24252
+ signal,
24253
+ selectedModels.mainError,
24254
+ memoryStore === void 0 || !memoryHasRoutableEntries && userMemoryContext === void 0 ? void 0 : {
24255
+ store: memoryStore,
24256
+ taskId: memoryLifecycle.taskId ?? sessionId,
24257
+ topK: config.memory.retrievalTopK,
24258
+ maxChars: config.memory.maxPromptChars
24259
+ },
24260
+ runOptions?.getSteeringMessages,
24261
+ runOptions?.initialUserMessage,
24262
+ runOptions?.additionalContext,
24263
+ ide
24264
+ ), persist), harnessJournal, turnId, turnConfig),
24265
+ () => rotateForHeap(void 0, currentPressureReading()),
24266
+ () => maybeRotateAtTurnBoundary()
24267
+ );
23607
24268
  },
23608
24269
  runSkill: (skill, prompt, signal) => persistAfter(
23609
24270
  runExplicitSkill(harness, skills, skill, prompt, signal, selectedModels.mainError),
23610
24271
  persist
23611
24272
  ),
23612
- runLoop: (goal, signal) => runLoopSession(loopOrchestrator, hooks, goal, signal),
23613
- runGoal: (goal, signal) => persistEach(runGoalSession(goalOrchestrator, goal, signal), persist),
24273
+ runLoop: (goal, signal) => monitorLongTaskRestart(
24274
+ runLoopSession(loopOrchestrator, hooks, goal, signal),
24275
+ () => rotateForHeap(activeLongTask, currentPressureReading())
24276
+ ),
24277
+ runGoal: (goal, signal) => monitorLongTaskRestart(
24278
+ persistEach(runGoalSession(goalOrchestrator, goal, signal), persist),
24279
+ () => rotateForHeap(activeLongTask, currentPressureReading())
24280
+ ),
24281
+ resumeLoop: (loopId, signal) => monitorLongTaskRestart(
24282
+ runResumeLoopSession(loopOrchestrator, hooks, loopId, signal),
24283
+ () => rotateForHeap(activeLongTask ?? { kind: "loop", id: loopId }, currentPressureReading())
24284
+ ),
24285
+ resumeGoal: (goalId, signal) => monitorLongTaskRestart(
24286
+ persistEach(runResumeGoalSession(goalOrchestrator, goalId, signal), persist),
24287
+ () => rotateForHeap(activeLongTask ?? { kind: "goal", id: goalId }, currentPressureReading())
24288
+ ),
24289
+ continuation: () => consumePendingContinuation(),
23614
24290
  mcp: async (command, signal) => {
23615
24291
  signal.throwIfAborted();
23616
24292
  const manager = await mcpReady;
@@ -23701,10 +24377,10 @@ async function createProductionRuntime(options) {
23701
24377
  explain: (query, focus, signal, onText) => runExplain({
23702
24378
  graph: new DesktopAstGraphService(workspace),
23703
24379
  readFile: async (path) => {
23704
- const root = resolve30(workspace);
23705
- const abs = resolve30(root, path);
23706
- if (abs !== root && !abs.startsWith(root + sep21)) throw new Error(`path outside workspace: ${path}`);
23707
- return readFile23(abs, "utf8");
24380
+ const root = resolve31(workspace);
24381
+ const abs = resolve31(root, path);
24382
+ if (abs !== root && !abs.startsWith(root + sep22)) throw new Error(`path outside workspace: ${path}`);
24383
+ return readFile24(abs, "utf8");
23708
24384
  },
23709
24385
  history: async (path, limit) => fileHistory(workspace, path, limit),
23710
24386
  questions,
@@ -23716,7 +24392,7 @@ async function createProductionRuntime(options) {
23716
24392
  }, query, focus, signal),
23717
24393
  audit: async (toolFilter) => {
23718
24394
  try {
23719
- const raw = await readFile23(auditLogger.path, "utf8");
24395
+ const raw = await readFile24(auditLogger.path, "utf8");
23720
24396
  const lines = raw.trim().split("\n").filter((line) => line.length > 0);
23721
24397
  const entries = lines.map((line) => {
23722
24398
  try {
@@ -23756,7 +24432,7 @@ ${summary2}`;
23756
24432
  doctor: async () => formatDoctorReport(await runDoctor({ workspace, home, environment })),
23757
24433
  usage: async () => {
23758
24434
  try {
23759
- const raw = await readFile23(usageLogPath(), "utf8");
24435
+ const raw = await readFile24(usageLogPath(), "utf8");
23760
24436
  return formatUsageSummary(summarizeUsage(parseUsageEntries(raw)));
23761
24437
  } catch (error) {
23762
24438
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
@@ -23909,7 +24585,7 @@ ${renderMemoryDocument(entries)}`;
23909
24585
  return `Provider "${providerName}" is missing authorizationUrl, tokenUrl, or clientId.`;
23910
24586
  }
23911
24587
  try {
23912
- const tokenStore = createFileTokenStore(join29(home, ".flavor-code", "auth.json"));
24588
+ const tokenStore = createFileTokenStore(join30(home, ".flavor-code", "auth.json"));
23913
24589
  const credentialId = oauthCredentialId(oauthConfig.tokenUrl, oauthConfig.clientId);
23914
24590
  await migrateLegacyOAuthToken(tokenStore, providerName, credentialId);
23915
24591
  const oauth = new OAuthCallbackAuthProvider({
@@ -23956,7 +24632,7 @@ ${renderMemoryDocument(entries)}`;
23956
24632
  }
23957
24633
  },
23958
24634
  async logout() {
23959
- const tokenStore = createFileTokenStore(join29(home, ".flavor-code", "auth.json"));
24635
+ const tokenStore = createFileTokenStore(join30(home, ".flavor-code", "auth.json"));
23960
24636
  const tokens = await tokenStore.load();
23961
24637
  const hadCredentials = Object.keys(tokens).length > 0 || effectiveLlm !== void 0;
23962
24638
  if (!hadCredentials) {
@@ -24055,6 +24731,7 @@ ${renderMemoryDocument(entries)}`;
24055
24731
  if (executionEnvironment !== void 0) await boundedStep(stepTimeoutMs, diagnostics, "execution-environment", executionEnvironment.dispose());
24056
24732
  if (islandControlServer !== void 0) await boundedStep(stepTimeoutMs, diagnostics, "island-control", islandControlServer.close());
24057
24733
  terminals.dispose();
24734
+ await boundedStep(stepTimeoutMs, diagnostics, "language-servers", lspManager.dispose());
24058
24735
  await boundedStep(stepTimeoutMs, diagnostics, "jobs", jobs.dispose());
24059
24736
  auditLogger.close();
24060
24737
  evolveService.dispose();
@@ -24095,6 +24772,7 @@ ${renderMemoryDocument(entries)}`;
24095
24772
  if (sleepScheduler !== void 0) await boundedStep(stepTimeoutMs, diagnostics, "sleep-scheduler", sleepScheduler.dispose());
24096
24773
  if (executionEnvironment !== void 0) await boundedStep(stepTimeoutMs, diagnostics, "execution-environment", executionEnvironment.dispose());
24097
24774
  terminals.dispose();
24775
+ await boundedStep(stepTimeoutMs, diagnostics, "language-servers", lspManager.dispose());
24098
24776
  await boundedStep(stepTimeoutMs, diagnostics, "jobs", jobs.dispose());
24099
24777
  unsubscribeCollaboration?.();
24100
24778
  pendingCollaborationEvents.length = 0;
@@ -24147,10 +24825,16 @@ function formatMcpReconnect(server) {
24147
24825
  return `Failed to reconnect MCP server "${server.name}": ${server.error ?? "unknown error"}`;
24148
24826
  }
24149
24827
  async function* runLoopSession(orchestrator, hooks, goal, signal) {
24828
+ yield* loopSessionEvents(orchestrator.run({ goal, signal }), hooks);
24829
+ }
24830
+ async function* runResumeLoopSession(orchestrator, hooks, loopId, signal) {
24831
+ yield* loopSessionEvents(orchestrator.resume({ loopId, signal }), hooks);
24832
+ }
24833
+ async function* loopSessionEvents(events, hooks) {
24150
24834
  let totalInputTokens = 0;
24151
24835
  let totalOutputTokens = 0;
24152
24836
  let latestVerification;
24153
- for await (const event of orchestrator.run({ goal, signal })) {
24837
+ for await (const event of events) {
24154
24838
  if (event.type === "worker-event") {
24155
24839
  if (event.event.type === "usage") {
24156
24840
  totalInputTokens += event.event.inputTokens;
@@ -24248,13 +24932,13 @@ async function workspaceFingerprint(workspace) {
24248
24932
  let remaining = 5 * 1024 * 1024;
24249
24933
  for (const name of untracked.stdout.split("\0").filter(Boolean).sort()) {
24250
24934
  if (name === ".flavor/loops" || name.startsWith(".flavor/loops/")) continue;
24251
- const path = resolve30(workspace, name);
24252
- const relativePath = relative21(resolve30(workspace), path);
24253
- if (relativePath === ".." || relativePath.startsWith(`..${sep21}`)) continue;
24935
+ const path = resolve31(workspace, name);
24936
+ const relativePath = relative22(resolve31(workspace), path);
24937
+ if (relativePath === ".." || relativePath.startsWith(`..${sep22}`)) continue;
24254
24938
  hash.update(name);
24255
24939
  if (remaining <= 0) continue;
24256
24940
  try {
24257
- const content = await readFile23(path);
24941
+ const content = await readFile24(path);
24258
24942
  const slice = content.subarray(0, remaining);
24259
24943
  hash.update(slice);
24260
24944
  remaining -= slice.length;
@@ -24366,7 +25050,16 @@ async function* durableTurn(source, journal, turnId, config) {
24366
25050
  }
24367
25051
  }
24368
25052
  }
24369
- async function* monitorMemoryRestart(source, onRestartNeeded) {
25053
+ async function* monitorTurnMemory(source, onPressure, onBoundary) {
25054
+ let pressure = false;
25055
+ for await (const event of source) {
25056
+ if (event.type === "error" && event.error.code === "memory_pressure") pressure = true;
25057
+ yield event;
25058
+ }
25059
+ if (pressure) onPressure();
25060
+ else onBoundary();
25061
+ }
25062
+ async function* monitorLongTaskRestart(source, onRestartNeeded) {
24370
25063
  let pressure = false;
24371
25064
  for await (const event of source) {
24372
25065
  if (event.type === "error" && event.error.code === "memory_pressure") pressure = true;
@@ -24374,7 +25067,10 @@ async function* monitorMemoryRestart(source, onRestartNeeded) {
24374
25067
  }
24375
25068
  if (pressure) onRestartNeeded();
24376
25069
  }
24377
- function requestMemoryRestart(workspace, sessionId, emitOutput) {
25070
+ function requestMemoryRestart(workspace, sessionId, emitOutput, rotation, onRestartRequested) {
25071
+ if (memoryRotationActive()) return;
25072
+ markMemoryRotation();
25073
+ const continuation = rotation?.continuation;
24378
25074
  try {
24379
25075
  const markerPath = memoryRestartMarkerPath(workspace);
24380
25076
  let previous;
@@ -24382,14 +25078,43 @@ function requestMemoryRestart(workspace, sessionId, emitOutput) {
24382
25078
  previous = parseMemoryRestartMarker(readFileSync3(markerPath, "utf8"));
24383
25079
  } catch {
24384
25080
  }
24385
- mkdirSync4(dirname19(markerPath), { recursive: true });
24386
- writeFileSync2(markerPath, `${JSON.stringify(nextMemoryRestartMarker(previous, sessionId, /* @__PURE__ */ new Date()))}
24387
- `, "utf8");
25081
+ writeMarkerAtomically(markerPath, nextMemoryRestartMarker(previous, sessionId, /* @__PURE__ */ new Date(), continuation));
24388
25082
  } catch {
24389
25083
  }
24390
- process.exitCode = MEMORY_RESTART_EXIT_CODE;
24391
- emitOutput({ type: "notice", message: "Memory watermark reached; the session is saved and Flavor is restarting with a fresh heap." });
24392
- emitOutput({ type: "exit" });
25084
+ if (rotation?.reading !== void 0) {
25085
+ const census = {
25086
+ at: (/* @__PURE__ */ new Date()).toISOString(),
25087
+ reason: continuation === void 0 ? "memory-pressure" : `heap-rotation:${continuation.kind}`,
25088
+ sessionId,
25089
+ heap: censusHeapBlock(rotation.reading),
25090
+ holders: rotation.holders ?? {}
25091
+ };
25092
+ writeRotationCensus(workspace, census);
25093
+ if (heapSnapshotWarranted(rotation.reading)) writeRotationHeapSnapshot(workspace);
25094
+ }
25095
+ emitOutput({
25096
+ type: "notice",
25097
+ message: continuation === void 0 ? "Memory watermark reached; the session is saved and Flavor is restarting with a fresh heap." : `Memory watermark reached; the session and the running ${continuation.kind} are saved, and Flavor is restarting with a fresh heap to continue it.`
25098
+ });
25099
+ if (onRestartRequested !== void 0) onRestartRequested(sessionId);
25100
+ else {
25101
+ process.exitCode = MEMORY_RESTART_EXIT_CODE;
25102
+ emitOutput({ type: "exit" });
25103
+ }
25104
+ }
25105
+ function writeMarkerAtomically(path, marker) {
25106
+ mkdirSync4(dirname17(path), { recursive: true });
25107
+ const temporary = `${path}.${process.pid}.${randomUUID20()}.tmp`;
25108
+ try {
25109
+ writeFileSync3(temporary, `${JSON.stringify(marker)}
25110
+ `, { encoding: "utf8", mode: 384 });
25111
+ renameSync2(temporary, path);
25112
+ } finally {
25113
+ try {
25114
+ unlinkSync3(temporary);
25115
+ } catch {
25116
+ }
25117
+ }
24393
25118
  }
24394
25119
  async function runGitCommit(deps, hint, signal) {
24395
25120
  if (!await isGitRepository(deps.workspace)) return "Not a git repository \u2014 /commit needs git.";
@@ -24484,7 +25209,7 @@ async function registerConfiguredAdapters(providers, registry, environment, diag
24484
25209
  const configured = { ...providers };
24485
25210
  if (configured.openai === void 0 && environment.OPENAI_API_KEY) configured.openai = { type: "openai", apiKey: environment.OPENAI_API_KEY };
24486
25211
  if (configured.anthropic === void 0 && environment.ANTHROPIC_API_KEY) configured.anthropic = { type: "anthropic", apiKey: environment.ANTHROPIC_API_KEY };
24487
- const oauthTokenStore = createFileTokenStore(join29(home, ".flavor-code", "auth.json"));
25212
+ const oauthTokenStore = createFileTokenStore(join30(home, ".flavor-code", "auth.json"));
24488
25213
  const registered = [];
24489
25214
  let effectiveLlm;
24490
25215
  for (const [name, provider] of Object.entries(configured)) {
@@ -24753,7 +25478,7 @@ async function generateSleepReview(registry, modelId, prompt, signal) {
24753
25478
  }
24754
25479
  async function optionalText2(path) {
24755
25480
  try {
24756
- return await readFile23(path, "utf8");
25481
+ return await readFile24(path, "utf8");
24757
25482
  } catch (error) {
24758
25483
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return void 0;
24759
25484
  throw error;
@@ -24838,7 +25563,17 @@ function attachCleanupError(primary, cleanup) {
24838
25563
  }
24839
25564
  }
24840
25565
  async function* runGoalSession(orchestrator, goal, signal) {
24841
- for await (const event of orchestrator.run({ goal, signal })) {
25566
+ yield* goalSessionEvents(orchestrator.run({ goal, signal }));
25567
+ }
25568
+ async function* runResumeGoalSession(orchestrator, goalId, signal) {
25569
+ yield* goalSessionEvents(orchestrator.resume({ goalId, signal }));
25570
+ }
25571
+ async function* goalSessionEvents(events) {
25572
+ for await (const event of events) {
25573
+ if (event.type === "goal-resumed") {
25574
+ yield { type: "notice", message: `Resuming goal ${event.goalId} at round ${event.round} after a heap rotation.` };
25575
+ continue;
25576
+ }
24842
25577
  if (event.type === "goal-plan-created") {
24843
25578
  yield { type: "notice", message: `Goal plan created (${event.plan.kind}) with ${event.plan.criteria.length} acceptance criteria.` };
24844
25579
  yield { type: "notice", message: `Plan file: ${event.planPath}` };