reasonix 0.43.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +49 -11
  2. package/README.zh-CN.md +35 -7
  3. package/dashboard/app.css +225 -4
  4. package/dashboard/dist/app.js +6441 -6080
  5. package/dashboard/dist/app.js.map +1 -1
  6. package/data/deepseek-tokenizer.json.gz +0 -0
  7. package/dist/cli/{acp-DAGPCVFZ.js → acp-TYZ2CTDL.js} +28 -30
  8. package/dist/cli/acp-TYZ2CTDL.js.map +1 -0
  9. package/dist/cli/chat-TH7VNNCJ.js +51 -0
  10. package/dist/cli/chunk-2425HK6U.js +0 -0
  11. package/dist/cli/chunk-25T6CVUP.js +0 -0
  12. package/dist/cli/chunk-2UQP6H6T.js +0 -0
  13. package/dist/cli/{chunk-3Z6IBU3D.js → chunk-2V6EAEUW.js} +95 -31
  14. package/dist/cli/chunk-2V6EAEUW.js.map +1 -0
  15. package/dist/cli/{chunk-XCGGEJTI.js → chunk-4CTDEJUF.js} +2 -2
  16. package/dist/cli/chunk-4QUNBQQ2.js +0 -0
  17. package/dist/cli/{chunk-74EX7SUH.js → chunk-5QCB62C4.js} +33 -7
  18. package/dist/cli/{chunk-74EX7SUH.js.map → chunk-5QCB62C4.js.map} +1 -1
  19. package/dist/cli/chunk-6OWJV3YW.js +390 -0
  20. package/dist/cli/chunk-6OWJV3YW.js.map +1 -0
  21. package/dist/cli/chunk-6PBZN4VI.js +0 -0
  22. package/dist/cli/{chunk-7O5ALB4C.js → chunk-7CIGMZT3.js} +2 -2
  23. package/dist/cli/{chunk-H6PS7IUE.js → chunk-7UCMM425.js} +7 -3
  24. package/dist/cli/chunk-7UCMM425.js.map +1 -0
  25. package/dist/cli/{chunk-TJX6BFZZ.js → chunk-AB2RED3C.js} +3 -3
  26. package/dist/cli/{chunk-XPDVG52A.js → chunk-AVFXO2EZ.js} +361 -13
  27. package/dist/cli/chunk-AVFXO2EZ.js.map +1 -0
  28. package/dist/cli/{chunk-FHOGSSCH.js → chunk-C53JQES5.js} +3 -3
  29. package/dist/cli/{chunk-RE4RAVFF.js → chunk-CGDR2ELH.js} +92 -30
  30. package/dist/cli/chunk-CGDR2ELH.js.map +1 -0
  31. package/dist/cli/{chunk-OSZC7C6F.js → chunk-CWZKQ5FE.js} +7 -4
  32. package/dist/cli/chunk-CWZKQ5FE.js.map +1 -0
  33. package/dist/cli/{devtools-YECO25QO.js → chunk-FEZK652I.js} +10 -85
  34. package/dist/cli/chunk-FEZK652I.js.map +1 -0
  35. package/dist/cli/{chunk-45U62RI3.js → chunk-HNXDZGC6.js} +104 -2
  36. package/dist/cli/chunk-HNXDZGC6.js.map +1 -0
  37. package/dist/cli/chunk-J5XJHLWM.js +0 -0
  38. package/dist/cli/chunk-JMBMLOBP.js +0 -0
  39. package/dist/cli/{chunk-5JJRUIPA.js → chunk-JNAQYELD.js} +16 -8
  40. package/dist/cli/{chunk-5JJRUIPA.js.map → chunk-JNAQYELD.js.map} +1 -1
  41. package/dist/cli/{chunk-YFGF5NKA.js → chunk-KGBG6M2X.js} +19 -15
  42. package/dist/cli/chunk-KGBG6M2X.js.map +1 -0
  43. package/dist/cli/{chunk-3BXRZFWS.js → chunk-KLQTAZIY.js} +12 -4
  44. package/dist/cli/chunk-KLQTAZIY.js.map +1 -0
  45. package/dist/cli/{chunk-VK5HG73G.js → chunk-KM465GST.js} +9 -9
  46. package/dist/cli/{chunk-DOYHN4KB.js → chunk-LIR2HBQH.js} +2 -2
  47. package/dist/cli/{chunk-YYQAUTTN.js → chunk-MJ6W5UN3.js} +2 -2
  48. package/dist/cli/{chunk-6PZ3CXBP.js → chunk-MRHHQJAQ.js} +5 -4
  49. package/dist/cli/chunk-MRHHQJAQ.js.map +1 -0
  50. package/dist/cli/{chunk-PQXPXJBJ.js → chunk-NVURFF27.js} +16 -5
  51. package/dist/cli/chunk-NVURFF27.js.map +1 -0
  52. package/dist/cli/{chunk-2R4QCDOZ.js → chunk-OPFUUYHL.js} +540 -287
  53. package/dist/cli/chunk-OPFUUYHL.js.map +1 -0
  54. package/dist/cli/chunk-PLHAZOLZ.js +0 -0
  55. package/dist/cli/{chunk-HFEAY5DT.js → chunk-R3CTO2HM.js} +2 -2
  56. package/dist/cli/{chunk-O52OLQL3.js → chunk-RDRC3XDT.js} +136 -38
  57. package/dist/cli/chunk-RDRC3XDT.js.map +1 -0
  58. package/dist/cli/chunk-S4XVGLRW.js +0 -0
  59. package/dist/cli/chunk-SZ5XES2N.js +0 -0
  60. package/dist/cli/{chunk-2K65GZBT.js → chunk-TEUDEGX2.js} +64 -19
  61. package/dist/cli/chunk-TEUDEGX2.js.map +1 -0
  62. package/dist/cli/{chunk-2Z35JOA4.js → chunk-TKVXTQ3T.js} +4 -4
  63. package/dist/cli/{chunk-2Z35JOA4.js.map → chunk-TKVXTQ3T.js.map} +1 -1
  64. package/dist/cli/chunk-TUK7OWJA.js +0 -0
  65. package/dist/cli/{chunk-32TIKD5U.js → chunk-TXJMRPIL.js} +3 -3
  66. package/dist/cli/{chunk-2KDUS647.js → chunk-V26WPN3J.js} +7 -4
  67. package/dist/cli/chunk-V26WPN3J.js.map +1 -0
  68. package/dist/cli/{chunk-F3PXYSNN.js → chunk-WK3UFQY3.js} +2 -2
  69. package/dist/cli/{chunk-6G3CUUFG.js → chunk-X53B3JIX.js} +3 -3
  70. package/dist/cli/{chunk-6G3CUUFG.js.map → chunk-X53B3JIX.js.map} +1 -1
  71. package/dist/cli/chunk-XJXDHAES.js +0 -0
  72. package/dist/cli/{chunk-6AK4EY3D.js → chunk-XSU4QVFW.js} +1 -81
  73. package/dist/cli/chunk-XSU4QVFW.js.map +1 -0
  74. package/dist/cli/chunk-XXC2BYTV.js +0 -0
  75. package/dist/cli/{chunk-P7EKE5ZQ.js → chunk-Z4S7EYXG.js} +4482 -1310
  76. package/dist/cli/chunk-Z4S7EYXG.js.map +1 -0
  77. package/dist/cli/chunk-ZZM6QJ4W.js +0 -0
  78. package/dist/cli/{chunk-YQ6NTIIE.js → chunk-ZZYBBX5N.js} +13 -5
  79. package/dist/cli/chunk-ZZYBBX5N.js.map +1 -0
  80. package/dist/cli/{code-SMKEW6CD.js → code-PSVJ3KEN.js} +48 -36
  81. package/dist/cli/code-PSVJ3KEN.js.map +1 -0
  82. package/dist/cli/{commands-FVVB5FZF.js → commands-OCU42XG4.js} +4 -4
  83. package/dist/cli/{commit-HE4VSPZ7.js → commit-XCQIQCYG.js} +3 -3
  84. package/dist/cli/{desktop-Q7NDXCON.js → desktop-KWGR4BNE.js} +210 -69
  85. package/dist/cli/desktop-KWGR4BNE.js.map +1 -0
  86. package/dist/cli/devtools-HW3WDT3Q.js +91 -0
  87. package/dist/cli/devtools-HW3WDT3Q.js.map +1 -0
  88. package/dist/cli/{diff-435UTPC5.js → diff-NHANTNC3.js} +9 -9
  89. package/dist/cli/{doctor-OT7KH75K.js → doctor-CC5CLOGG.js} +10 -10
  90. package/dist/cli/events-XEFAD5VX.js +0 -0
  91. package/dist/cli/index.js +132 -94
  92. package/dist/cli/index.js.map +1 -1
  93. package/dist/cli/{mcp-WUL2WO75.js → mcp-MPVGBBJF.js} +2 -2
  94. package/dist/cli/{mcp-browse-RR7R4XET.js → mcp-browse-4XOTC3FJ.js} +3 -3
  95. package/dist/cli/{mcp-inspect-REGLYBWT.js → mcp-inspect-CEMGKKAH.js} +14 -9
  96. package/dist/cli/mcp-inspect-CEMGKKAH.js.map +1 -0
  97. package/dist/cli/{prompt-UW6EFLVR.js → prompt-2D7ID24X.js} +4 -4
  98. package/dist/cli/prune-sessions-3RWUBYRS.js +0 -0
  99. package/dist/cli/{replay-YOURXV4C.js → replay-SR44E6RS.js} +10 -10
  100. package/dist/cli/{run-Q6BUXV66.js → run-MDGL27WL.js} +35 -36
  101. package/dist/cli/run-MDGL27WL.js.map +1 -0
  102. package/dist/cli/{server-XGDBRWMB.js → server-27ARQXIZ.js} +67 -24
  103. package/dist/cli/server-27ARQXIZ.js.map +1 -0
  104. package/dist/cli/{sessions-FH7QVYSY.js → sessions-CKQXCYGP.js} +18 -18
  105. package/dist/cli/sessions-CKQXCYGP.js.map +1 -0
  106. package/dist/cli/{setup-VDS6SVEP.js → setup-TPAGSVXO.js} +6 -6
  107. package/dist/cli/{stats-MQVI2XQH.js → stats-DPUBZNVX.js} +6 -4
  108. package/dist/cli/update-6ITLPRDV.js +0 -0
  109. package/dist/cli/{version-DAHGZY5N.js → version-2X3BHVVK.js} +15 -15
  110. package/dist/index.d.ts +181 -53
  111. package/dist/index.js +1322 -533
  112. package/dist/index.js.map +1 -1
  113. package/package.json +21 -8
  114. package/dist/cli/.-3G6VX5S7.js +0 -327
  115. package/dist/cli/.-6YRPB2C7.js +0 -329
  116. package/dist/cli/.-EYSVINK3.js +0 -317
  117. package/dist/cli/acp-DAGPCVFZ.js.map +0 -1
  118. package/dist/cli/chat-7ES4IBNH.js +0 -50
  119. package/dist/cli/chunk-2K65GZBT.js.map +0 -1
  120. package/dist/cli/chunk-2KDUS647.js.map +0 -1
  121. package/dist/cli/chunk-2R4QCDOZ.js.map +0 -1
  122. package/dist/cli/chunk-3BXRZFWS.js.map +0 -1
  123. package/dist/cli/chunk-3Z6IBU3D.js.map +0 -1
  124. package/dist/cli/chunk-45U62RI3.js.map +0 -1
  125. package/dist/cli/chunk-6AK4EY3D.js.map +0 -1
  126. package/dist/cli/chunk-6PZ3CXBP.js.map +0 -1
  127. package/dist/cli/chunk-H6PS7IUE.js.map +0 -1
  128. package/dist/cli/chunk-O52OLQL3.js.map +0 -1
  129. package/dist/cli/chunk-OSZC7C6F.js.map +0 -1
  130. package/dist/cli/chunk-P7EKE5ZQ.js.map +0 -1
  131. package/dist/cli/chunk-PQXPXJBJ.js.map +0 -1
  132. package/dist/cli/chunk-PV55UMTO.js +0 -200
  133. package/dist/cli/chunk-PV55UMTO.js.map +0 -1
  134. package/dist/cli/chunk-RE4RAVFF.js.map +0 -1
  135. package/dist/cli/chunk-XPDVG52A.js.map +0 -1
  136. package/dist/cli/chunk-YFGF5NKA.js.map +0 -1
  137. package/dist/cli/chunk-YQ6NTIIE.js.map +0 -1
  138. package/dist/cli/code-SMKEW6CD.js.map +0 -1
  139. package/dist/cli/desktop-Q7NDXCON.js.map +0 -1
  140. package/dist/cli/devtools-YECO25QO.js.map +0 -1
  141. package/dist/cli/doctor-OT7KH75K.js.map +0 -1
  142. package/dist/cli/mcp-inspect-REGLYBWT.js.map +0 -1
  143. package/dist/cli/prompt-UW6EFLVR.js.map +0 -1
  144. package/dist/cli/run-Q6BUXV66.js.map +0 -1
  145. package/dist/cli/server-XGDBRWMB.js.map +0 -1
  146. package/dist/cli/sessions-FH7QVYSY.js.map +0 -1
  147. package/dist/cli/stats-MQVI2XQH.js.map +0 -1
  148. /package/dist/cli/{.-3G6VX5S7.js.map → chat-TH7VNNCJ.js.map} +0 -0
  149. /package/dist/cli/{chunk-XCGGEJTI.js.map → chunk-4CTDEJUF.js.map} +0 -0
  150. /package/dist/cli/{chunk-7O5ALB4C.js.map → chunk-7CIGMZT3.js.map} +0 -0
  151. /package/dist/cli/{chunk-TJX6BFZZ.js.map → chunk-AB2RED3C.js.map} +0 -0
  152. /package/dist/cli/{chunk-FHOGSSCH.js.map → chunk-C53JQES5.js.map} +0 -0
  153. /package/dist/cli/{chunk-VK5HG73G.js.map → chunk-KM465GST.js.map} +0 -0
  154. /package/dist/cli/{chunk-DOYHN4KB.js.map → chunk-LIR2HBQH.js.map} +0 -0
  155. /package/dist/cli/{chunk-YYQAUTTN.js.map → chunk-MJ6W5UN3.js.map} +0 -0
  156. /package/dist/cli/{chunk-HFEAY5DT.js.map → chunk-R3CTO2HM.js.map} +0 -0
  157. /package/dist/cli/{chunk-32TIKD5U.js.map → chunk-TXJMRPIL.js.map} +0 -0
  158. /package/dist/cli/{chunk-F3PXYSNN.js.map → chunk-WK3UFQY3.js.map} +0 -0
  159. /package/dist/cli/{commands-FVVB5FZF.js.map → commands-OCU42XG4.js.map} +0 -0
  160. /package/dist/cli/{commit-HE4VSPZ7.js.map → commit-XCQIQCYG.js.map} +0 -0
  161. /package/dist/cli/{diff-435UTPC5.js.map → diff-NHANTNC3.js.map} +0 -0
  162. /package/dist/cli/{.-6YRPB2C7.js.map → doctor-CC5CLOGG.js.map} +0 -0
  163. /package/dist/cli/{mcp-WUL2WO75.js.map → mcp-MPVGBBJF.js.map} +0 -0
  164. /package/dist/cli/{mcp-browse-RR7R4XET.js.map → mcp-browse-4XOTC3FJ.js.map} +0 -0
  165. /package/dist/cli/{.-EYSVINK3.js.map → prompt-2D7ID24X.js.map} +0 -0
  166. /package/dist/cli/{replay-YOURXV4C.js.map → replay-SR44E6RS.js.map} +0 -0
  167. /package/dist/cli/{setup-VDS6SVEP.js.map → setup-TPAGSVXO.js.map} +0 -0
  168. /package/dist/cli/{chat-7ES4IBNH.js.map → stats-DPUBZNVX.js.map} +0 -0
  169. /package/dist/cli/{version-DAHGZY5N.js.map → version-2X3BHVVK.js.map} +0 -0
package/dist/index.js CHANGED
@@ -47,8 +47,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
47
47
  }
48
48
  function sleep(ms, signal) {
49
49
  if (ms <= 0) return Promise.resolve();
50
- return new Promise((resolve10, reject) => {
51
- const timer = setTimeout(resolve10, ms);
50
+ return new Promise((resolve13, reject) => {
51
+ const timer = setTimeout(resolve13, ms);
52
52
  if (signal) {
53
53
  const onAbort = () => {
54
54
  clearTimeout(timer);
@@ -93,12 +93,15 @@ var Usage = class _Usage {
93
93
  }
94
94
  static fromApi(raw) {
95
95
  const u = raw ?? {};
96
+ const promptTokens = u.prompt_tokens ?? 0;
97
+ const cacheHitTokens = u.prompt_cache_hit_tokens ?? 0;
98
+ const cacheMissTokens = u.prompt_cache_miss_tokens ?? Math.max(0, promptTokens - cacheHitTokens);
96
99
  return new _Usage(
97
- u.prompt_tokens ?? 0,
100
+ promptTokens,
98
101
  u.completion_tokens ?? 0,
99
102
  u.total_tokens ?? 0,
100
- u.prompt_cache_hit_tokens ?? 0,
101
- u.prompt_cache_miss_tokens ?? 0
103
+ cacheHitTokens,
104
+ cacheMissTokens
102
105
  );
103
106
  }
104
107
  };
@@ -309,10 +312,10 @@ var PauseGate = class {
309
312
  `${kind}: no confirmation listener registered \u2014 cannot prompt the user. This tool can only be used inside an interactive Reasonix session.`
310
313
  );
311
314
  }
312
- return new Promise((resolve10) => {
315
+ return new Promise((resolve13) => {
313
316
  const id = this._nextId++;
314
317
  const request = { id, kind, payload };
315
- this._pending.set(id, { resolve: resolve10, request });
318
+ this._pending.set(id, { resolve: resolve13, request });
316
319
  for (const fn of this._listeners) {
317
320
  try {
318
321
  fn(request);
@@ -427,12 +430,12 @@ import { join as join2 } from "path";
427
430
  // src/config.ts
428
431
  import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
429
432
  import { homedir } from "os";
430
- import { dirname, join } from "path";
433
+ import { dirname, isAbsolute, join, resolve } from "path";
431
434
 
432
435
  // src/cli/ui/theme/tokens.ts
433
436
  function card(fg, tone) {
434
437
  return {
435
- user: { color: fg.meta, glyph: "\u25C7" },
438
+ user: { color: tone.brand, glyph: "\u25C7" },
436
439
  reasoning: { color: tone.accent, glyph: "\u25C6" },
437
440
  streaming: { color: tone.brand, glyph: "\u25C8" },
438
441
  task: { color: tone.warn, glyph: "\u25B6" },
@@ -772,6 +775,85 @@ var DEFAULT_INDEX_EXCLUDES = {
772
775
  };
773
776
  var DEFAULT_MAX_FILE_BYTES = 256 * 1024;
774
777
 
778
+ // src/mcp/shell-split.ts
779
+ function shellSplit(input) {
780
+ const tokens = [];
781
+ let cur = "";
782
+ let quote = null;
783
+ let i = 0;
784
+ const s = input;
785
+ while (i < s.length) {
786
+ const ch = s[i];
787
+ if (quote) {
788
+ if (ch === quote) {
789
+ quote = null;
790
+ i++;
791
+ continue;
792
+ }
793
+ if (ch === "\\" && quote === '"' && i + 1 < s.length) {
794
+ cur += s[i + 1];
795
+ i += 2;
796
+ continue;
797
+ }
798
+ cur += ch;
799
+ i++;
800
+ continue;
801
+ }
802
+ if (ch === '"' || ch === "'") {
803
+ quote = ch;
804
+ i++;
805
+ continue;
806
+ }
807
+ if (ch === " " || ch === " ") {
808
+ if (cur.length > 0) {
809
+ tokens.push(cur);
810
+ cur = "";
811
+ }
812
+ i++;
813
+ continue;
814
+ }
815
+ cur += ch;
816
+ i++;
817
+ }
818
+ if (quote) {
819
+ throw new Error(
820
+ `shellSplit: unterminated ${quote === '"' ? "double" : "single"} quote in input`
821
+ );
822
+ }
823
+ if (cur.length > 0) tokens.push(cur);
824
+ return tokens;
825
+ }
826
+
827
+ // src/mcp/spec.ts
828
+ var NAME_PREFIX = /^([a-zA-Z_][a-zA-Z0-9_-]*)=(.*)$/;
829
+ var HTTP_URL = /^https?:\/\//i;
830
+ var STREAMABLE_PREFIX = /^streamable\+(https?:\/\/.+)$/i;
831
+ function parseMcpSpec(input) {
832
+ const trimmed = input.trim();
833
+ if (!trimmed) {
834
+ throw new Error("empty MCP spec");
835
+ }
836
+ const nameMatch = NAME_PREFIX.exec(trimmed);
837
+ const name = nameMatch ? nameMatch[1] : null;
838
+ const body = (nameMatch ? nameMatch[2] : trimmed).trim();
839
+ if (!body) {
840
+ throw new Error(`MCP spec has name but no command: ${input}`);
841
+ }
842
+ const streamMatch = STREAMABLE_PREFIX.exec(body);
843
+ if (streamMatch) {
844
+ return { transport: "streamable-http", name, url: streamMatch[1] };
845
+ }
846
+ if (HTTP_URL.test(body)) {
847
+ return { transport: "sse", name, url: body };
848
+ }
849
+ const argv = shellSplit(body);
850
+ if (argv.length === 0) {
851
+ throw new Error(`MCP spec has name but no command: ${input}`);
852
+ }
853
+ const [command, ...args] = argv;
854
+ return { transport: "stdio", name, command, args };
855
+ }
856
+
775
857
  // src/config.ts
776
858
  var BUILTIN_TYPE_DOCS = {
777
859
  user: "role / skills / preferences",
@@ -809,6 +891,13 @@ function memoryTypeDefaults(typeName, cfg = readConfig()) {
809
891
  if (found.expires) out.expires = found.expires;
810
892
  return out;
811
893
  }
894
+ var DEFAULT_METASO_API_KEY = "mk-E384C1DD5E8501BB7EFE27C949AFDE5B";
895
+ function loadMetasoApiKey(path2 = defaultConfigPath()) {
896
+ if (process.env.METASO_API_KEY) return process.env.METASO_API_KEY;
897
+ const cfg = readConfig(path2).metasoApiKey;
898
+ if (cfg && typeof cfg === "string" && cfg.trim()) return cfg.trim();
899
+ return DEFAULT_METASO_API_KEY;
900
+ }
812
901
  function defaultConfigPath() {
813
902
  return join(homedir(), ".reasonix", "config.json");
814
903
  }
@@ -850,9 +939,44 @@ function saveBaseUrl(url, path2 = defaultConfigPath()) {
850
939
  }
851
940
  writeConfig(cfg, path2);
852
941
  }
942
+ function resolveSkillPath(raw, baseDir) {
943
+ const homeExpanded = expandCurrentUserHome(raw.trim());
944
+ return resolve(isAbsolute(homeExpanded) ? homeExpanded : join(baseDir, homeExpanded));
945
+ }
946
+ function normalizeSkillPathEntries(paths, baseDir) {
947
+ const out = [];
948
+ const seen = /* @__PURE__ */ new Set();
949
+ for (const value of paths) {
950
+ if (typeof value !== "string") continue;
951
+ const raw = value.trim();
952
+ if (!raw) continue;
953
+ const resolved = resolveSkillPath(raw, baseDir);
954
+ const key = skillPathKey(resolved);
955
+ if (seen.has(key)) continue;
956
+ seen.add(key);
957
+ out.push({ raw, resolved });
958
+ }
959
+ return out;
960
+ }
961
+ function resolveSkillPaths(paths, baseDir) {
962
+ return normalizeSkillPathEntries(paths, baseDir).map((entry) => entry.resolved);
963
+ }
964
+ function skillPathKey(path2) {
965
+ return process.platform === "win32" ? path2.toLowerCase() : path2;
966
+ }
967
+ function expandCurrentUserHome(path2) {
968
+ if (path2 === "~") return homedir();
969
+ if (path2.startsWith("~/") || path2.startsWith("~\\")) return join(homedir(), path2.slice(2));
970
+ return path2;
971
+ }
972
+ function loadResolvedSkillPaths(baseDir = process.cwd(), path2 = defaultConfigPath()) {
973
+ const raw = readConfig(path2).skills?.paths;
974
+ return Array.isArray(raw) ? resolveSkillPaths(raw, baseDir) : [];
975
+ }
853
976
  function webSearchEngine(path2 = defaultConfigPath()) {
854
977
  const cfg = readConfig(path2).webSearchEngine;
855
978
  if (cfg === "searxng") return "searxng";
979
+ if (cfg === "metaso") return "metaso";
856
980
  return "mojeek";
857
981
  }
858
982
  function webSearchEndpoint(path2 = defaultConfigPath()) {
@@ -952,6 +1076,16 @@ var EN = {
952
1076
  update: "Check for a newer Reasonix and install it.",
953
1077
  index: "Build (or incrementally refresh) a local semantic search index."
954
1078
  },
1079
+ stats: {
1080
+ usageHint: "run `reasonix chat`, `reasonix code`, or `reasonix run <task>` \u2014 every turn",
1081
+ usageDetail: "appends one line to the log and `reasonix stats` will roll it up."
1082
+ },
1083
+ run: {
1084
+ missingApiKey: "DEEPSEEK_API_KEY is not set and stdin is not a TTY (cannot prompt).\nSet the env var, or run `reasonix chat` once interactively to save a key.\n"
1085
+ },
1086
+ sessions: {
1087
+ emptyHint: "no saved sessions yet \u2014 run `reasonix chat` (sessions are auto-saved unless --no-session)."
1088
+ },
955
1089
  ui: {
956
1090
  welcome: "Run `reasonix` any time to start chatting \u2014 your settings are remembered.",
957
1091
  taglineChat: "DeepSeek-native agent",
@@ -1183,8 +1317,8 @@ var EN = {
1183
1317
  argsHint: "[list|show <name>|forget <name>|clear <scope> confirm]"
1184
1318
  },
1185
1319
  skill: {
1186
- description: "list / run user skills (<project>/.reasonix/skills + ~/.reasonix/skills)",
1187
- argsHint: "[list|show <name>|<name> [args]]"
1320
+ description: "list / run user skills (project + custom + global + builtin)",
1321
+ argsHint: "[list|paths|show <name>|<name> [args]]"
1188
1322
  },
1189
1323
  hooks: {
1190
1324
  description: "list active hooks (settings.json under .reasonix/) \xB7 reload re-reads from disk",
@@ -1226,6 +1360,10 @@ var EN = {
1226
1360
  argsHint: "[N]"
1227
1361
  },
1228
1362
  sessions: { description: "list saved sessions (current marked with \u25B8)" },
1363
+ qq: {
1364
+ description: "connect, inspect, or disconnect the QQ channel for this session",
1365
+ argsHint: "[connect [appId appSecret [sandbox]]|status|disconnect]"
1366
+ },
1229
1367
  setup: { description: "reminds you to exit and run `reasonix setup`" },
1230
1368
  semantic: {
1231
1369
  description: "show semantic_search status \u2014 built? Ollama installed? how to enable"
@@ -1289,8 +1427,8 @@ var EN = {
1289
1427
  argsHint: "<question>"
1290
1428
  },
1291
1429
  "search-engine": {
1292
- description: "switch web search backend \u2014 mojeek (default, no deps) or searxng (self-hosted)",
1293
- argsHint: "<mojeek|searxng> [<endpoint>]"
1430
+ description: "switch web search backend \u2014 mojeek (default, no deps), searxng (self-hosted), or metaso (free quota 100/d)",
1431
+ argsHint: "<mojeek|searxng|metaso> [<endpoint>]"
1294
1432
  }
1295
1433
  },
1296
1434
  wizard: {
@@ -1484,12 +1622,12 @@ var EN = {
1484
1622
  budgetExhausted: "session budget exhausted \u2014 spent ${spent} \u2265 cap ${cap}. Bump the cap with /budget <usd>, clear it with /budget off, or end the session.",
1485
1623
  budget80Pct: "\u25B2 budget 80% used \u2014 ${spent} of ${cap}. Next turn or two likely trips the cap.",
1486
1624
  proArmed: "\u21E7 /pro armed \u2014 this turn runs on deepseek-v4-pro (one-shot \xB7 disarms after turn)",
1487
- abortedAtIter: "aborted at iter {iter}/{cap} \u2014 stopped without producing a summary (press \u2191 + Enter or /retry to resume)",
1625
+ abortedAtIter: "aborted at iter {iter} \u2014 stopped without producing a summary (press \u2191 + Enter or /retry to resume)",
1488
1626
  toolUploadStatus: "tool result uploaded \xB7 model thinking before next response\u2026",
1489
- toolBudgetWarning: "{iter}/{cap} tool calls used \u2014 approaching budget. Press Esc to force a summary now.",
1490
- preflightFoldStatus: "preflight: context near full, attempting fold\u2026",
1491
- preflightFolded: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) \u2014 folded {beforeMessages} messages \u2192 {afterMessages} (summary {summaryChars} chars). Sending.",
1492
- preflightNoFold: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) and nothing left to fold \u2014 DeepSeek will likely 400. Run /clear or /new to start fresh.",
1627
+ preflightTruncateStatus: "preflight: context near full, truncating oldest history\u2026",
1628
+ preflightTruncated: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) \u2014 truncated {beforeMessages} messages \u2192 {afterMessages}. Sending.",
1629
+ preflightTruncatedStillFull: "preflight: request still ~{estimate}/{ctxMax} tokens ({pct}%) after truncating {beforeMessages} messages \u2192 {afterMessages}. DeepSeek will likely 400. Run /clear or /new to start fresh.",
1630
+ preflightNoFold: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) and nothing left to truncate \u2014 DeepSeek will likely 400. Run /clear or /new to start fresh.",
1493
1631
  flashEscalation: "\u21E7 flash requested escalation \u2014 retrying this turn on {model}{reasonSuffix}",
1494
1632
  harvestStatus: "extracting plan state from reasoning\u2026",
1495
1633
  autoEscalation: "\u21E7 auto-escalating to {model} for the rest of this turn \u2014 flash hit {breakdown}. Next turn falls back to {fallback} unless /pro is armed.",
@@ -1518,11 +1656,9 @@ var EN = {
1518
1656
  reasonAborted: "[aborted by user (Esc) \u2014 summarizing what I found so far]",
1519
1657
  reasonContextGuard: "[context budget running low \u2014 summarizing before the next call would overflow]",
1520
1658
  reasonStuck: "[stuck on a repeated tool call \u2014 explaining what was tried and what's blocking progress]",
1521
- reasonBudget: "[tool-call budget ({iterCap}) reached \u2014 forcing summary from what I found]",
1522
1659
  labelAborted: "aborted by user",
1523
1660
  labelContextGuard: "context-guard triggered (prompt > 80% of window)",
1524
- labelStuck: "stuck (repeated tool call suppressed by storm-breaker)",
1525
- labelBudget: "tool-call budget ({iterCap}) reached"
1661
+ labelStuck: "stuck (repeated tool call suppressed by storm-breaker)"
1526
1662
  },
1527
1663
  handlers: {
1528
1664
  basic: {
@@ -1832,11 +1968,13 @@ var EN = {
1832
1968
  usageMojeek: " /search-engine mojeek use Mojeek (default, no external deps)",
1833
1969
  usageSearxng: " /search-engine searxng use SearXNG at default endpoint",
1834
1970
  usageSearxngUrl: " /search-engine searxng <url> use SearXNG at custom endpoint",
1971
+ usageMetaso: " /search-engine metaso use Metaso API (100/d free, configure your own API key for more)",
1835
1972
  alias: "Alias: /se",
1836
1973
  searxngInfo: "SearXNG is a self-hosted metasearch engine (https://github.com/searxng/searxng).",
1837
1974
  searxngInstall: "Install it with: docker run -d -p 8080:8080 searxng/searxng",
1838
1975
  switched: 'Switched web search engine to "{engine}".{note}',
1839
1976
  switchedSearxngNote: " Make sure SearXNG is running at {endpoint}.",
1977
+ switchedMetasoNote: " There is a daily quota of 100 (configure your own API key for higher limits).",
1840
1978
  confirmed: '\u2713 Web search engine set to "{engine}"{detail}. Next assistant turn will pick up the change.',
1841
1979
  confirmedDetail: " ({endpoint})"
1842
1980
  },
@@ -1856,7 +1994,17 @@ var EN = {
1856
1994
  runInfo: "\u25B8 running skill: {name}{args}",
1857
1995
  newUsage: "usage: /skill new <name> [--global]",
1858
1996
  newCreated: "\u25B8 created skill: {name}\n {path}\n edit it, then `/skill {name}` to invoke",
1859
- newError: "\u25B2 /skill new failed: {reason}"
1997
+ newError: "\u25B2 /skill new failed: {reason}",
1998
+ pathsHeader: "Skill paths (priority order):",
1999
+ pathsPriority: "Priority: project > custom paths in config order > global > builtin. Changes affect the system prompt on next /new or new session.",
2000
+ pathsUsage: "usage: /skill paths [list]\n /skill paths add <path>\n /skill paths remove <path|N>",
2001
+ pathsAddUsage: "usage: /skill paths add <path>",
2002
+ pathsRemoveUsage: "usage: /skill paths remove <path|N>",
2003
+ pathsAdded: "\u25B8 added custom skills path: {path}",
2004
+ pathsAlready: "\u25B8 custom skills path already configured: {path}",
2005
+ pathsRemoved: "\u25B8 removed custom skills path: {path}",
2006
+ pathsRemoveNotFound: "\u25B8 no custom skills path matches: {target}",
2007
+ pathsRestartHint: "The current session's system prompt is unchanged; run /new or start a new session to refresh the skills index."
1860
2008
  }
1861
2009
  },
1862
2010
  statusBar: {
@@ -1903,7 +2051,8 @@ var EN = {
1903
2051
  editorNoRawMode: "external editor unavailable \u2014 stdin doesn't support raw-mode toggling on this terminal",
1904
2052
  editorFailed: "external editor:",
1905
2053
  editorMissing: "no $EDITOR / $VISUAL / $GIT_EDITOR set \u2014 export one (e.g. `export EDITOR=nano`) and retry",
1906
- editorExited: "editor exited with code {code}"
2054
+ editorExited: "editor exited with code {code}",
2055
+ typeaheadStaged: "\u25B8 {count} line(s) staged \xB7 esc recall"
1907
2056
  },
1908
2057
  pathConfirm: {
1909
2058
  title: "Outside-sandbox path",
@@ -2073,6 +2222,12 @@ var EN = {
2073
2222
  endpointMustBeHttp: "web_search: SearXNG endpoint must be http(s), got {protocol} \u2014 try: set a valid URL with /search-endpoint http://host:port",
2074
2223
  cannotReach: "web_search: Cannot reach SearXNG server at {endpoint} \u2014 try: install and start SearXNG (https://github.com/searxng/searxng, e.g. `docker run -d -p 8080:8080 searxng/searxng`), or switch to the default engine with /search-engine mojeek",
2075
2224
  searxngNoResults: "web_search: 0 results but SearXNG response doesn't look like an empty results page ({chars} chars) \u2014 try: rephrase the query with simpler terms, or switch engine with /search-engine mojeek",
2225
+ metasoDailyLimit: "web_search: daily search limit reached for the default API key \u2014 set your own METASO_API_KEY env var or get one at https://metaso.cn/search-api/playground",
2226
+ metasoUnauthorized: "web_search: Metaso API key rejected \u2014 check METASO_API_KEY or get one at https://metaso.cn/search-api/playground",
2227
+ metasoRateLimit: "web_search: Metaso rate-limited \u2014 wait and retry, or get your own API key at https://metaso.cn/search-api/playground",
2228
+ metasoServerError: "web_search: Metaso server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek",
2229
+ metasoParseError: "web_search: Metaso returned unparseable response (HTTP {status}) \u2014 try again later",
2230
+ metasoApiError: "web_search: Metaso API error (code {code}: {message}) \u2014 try again later",
2076
2231
  fetchStatus: "web_fetch {status} for {url} \u2014 try: confirm the URL resolves in a browser; status suggests the host returned an error page",
2077
2232
  fetchRateLimit429: "web_fetch 429 for {url} \u2014 try: wait 10s before retrying; the host is rate-limiting this client",
2078
2233
  fetchForbidden403: "web_fetch 403 for {url} \u2014 try: the host is blocking this client; the page may require login or block bots \u2014 use web_search snippets instead",
@@ -2333,6 +2488,16 @@ var zhCN = {
2333
2488
  update: "\u68C0\u67E5\u8F83\u65B0\u7248\u672C\u7684 Reasonix \u5E76\u5B89\u88C5\u3002",
2334
2489
  index: "\u6784\u5EFA\uFF08\u6216\u589E\u91CF\u5237\u65B0\uFF09\u672C\u5730\u8BED\u4E49\u641C\u7D22\u7D22\u5F15\u3002"
2335
2490
  },
2491
+ stats: {
2492
+ usageHint: "\u8FD0\u884C `reasonix chat`\u3001`reasonix code` \u6216 `reasonix run <task>` \u2014 \u6BCF\u6B21\u5BF9\u8BDD\u90FD\u4F1A\u8BB0\u5F55",
2493
+ usageDetail: "\u6BCF\u6B21\u5BF9\u8BDD\u5728\u65E5\u5FD7\u4E2D\u8FFD\u52A0\u4E00\u884C\uFF0C`reasonix stats` \u4F1A\u5C06\u5176\u6C47\u603B\u7EDF\u8BA1\u3002"
2494
+ },
2495
+ run: {
2496
+ missingApiKey: "\u672A\u8BBE\u7F6E DEEPSEEK_API_KEY \u4E14\u6807\u51C6\u8F93\u5165\u4E0D\u662F TTY\uFF08\u65E0\u6CD5\u4EA4\u4E92\u5F0F\u8F93\u5165\uFF09\u3002\n\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF\uFF0C\u6216\u5148\u8FD0\u884C `reasonix chat` \u4EA4\u4E92\u4E00\u6B21\u4EE5\u4FDD\u5B58\u5BC6\u94A5\u3002\n"
2497
+ },
2498
+ sessions: {
2499
+ emptyHint: "\u6682\u65E0\u5DF2\u4FDD\u5B58\u7684\u4F1A\u8BDD \u2014 \u8FD0\u884C `reasonix chat`\uFF08\u4F1A\u8BDD\u4F1A\u81EA\u52A8\u4FDD\u5B58\uFF0C\u9664\u975E\u4F7F\u7528\u4E86 --no-session\uFF09\u3002"
2500
+ },
2336
2501
  ui: {
2337
2502
  welcome: "\u968F\u65F6\u8FD0\u884C `reasonix` \u5F00\u59CB\u804A\u5929 \u2014 \u60A8\u7684\u8BBE\u7F6E\u5C06\u88AB\u8BB0\u4F4F\u3002",
2338
2503
  taglineChat: "DeepSeek \u539F\u751F\u667A\u80FD\u4F53",
@@ -2561,8 +2726,8 @@ var zhCN = {
2561
2726
  argsHint: "[list|show <name>|forget <name>|clear <scope> confirm]"
2562
2727
  },
2563
2728
  skill: {
2564
- description: "\u5217\u51FA / \u8FD0\u884C\u7528\u6237\u6280\u80FD\uFF08<project>/.reasonix/skills + ~/.reasonix/skills\uFF09",
2565
- argsHint: "[list|show <name>|<name> [args]]"
2729
+ description: "\u5217\u51FA / \u8FD0\u884C\u7528\u6237\u6280\u80FD\uFF08\u9879\u76EE + \u81EA\u5B9A\u4E49 + \u5168\u5C40 + \u5185\u7F6E\uFF09",
2730
+ argsHint: "[list|paths|show <name>|<name> [args]]"
2566
2731
  },
2567
2732
  hooks: {
2568
2733
  description: "\u5217\u51FA\u6D3B\u8DC3\u7684 hooks\uFF08.reasonix/ \u4E0B\u7684 settings.json\uFF09\xB7 reload \u4ECE\u78C1\u76D8\u91CD\u65B0\u8BFB\u53D6",
@@ -2606,6 +2771,10 @@ var zhCN = {
2606
2771
  argsHint: "[N]"
2607
2772
  },
2608
2773
  sessions: { description: "\u5217\u51FA\u5DF2\u4FDD\u5B58\u7684\u4F1A\u8BDD\uFF08\u5F53\u524D\u6807\u8BB0\u4E3A \u25B8\uFF09" },
2774
+ qq: {
2775
+ description: "\u8FDE\u63A5\u3001\u67E5\u770B\u6216\u65AD\u5F00\u5F53\u524D\u4F1A\u8BDD\u7684 QQ \u901A\u9053",
2776
+ argsHint: "[connect [appId appSecret [sandbox]]|status|disconnect]"
2777
+ },
2609
2778
  setup: { description: "\u63D0\u9192\u60A8\u9000\u51FA\u5E76\u8FD0\u884C `reasonix setup`" },
2610
2779
  semantic: {
2611
2780
  description: "\u663E\u793A semantic_search \u72B6\u6001 \u2014 \u5DF2\u6784\u5EFA\uFF1FOllama \u5DF2\u5B89\u88C5\uFF1F\u5982\u4F55\u542F\u7528"
@@ -2671,8 +2840,8 @@ var zhCN = {
2671
2840
  argsHint: "<question>"
2672
2841
  },
2673
2842
  "search-engine": {
2674
- description: "\u5207\u6362\u7F51\u7EDC\u641C\u7D22\u540E\u7AEF \u2014 mojeek\uFF08\u9ED8\u8BA4\uFF0C\u65E0\u4F9D\u8D56\uFF09\u6216 searxng\uFF08\u81EA\u6258\u7BA1\uFF09",
2675
- argsHint: "<mojeek|searxng> [<endpoint>]"
2843
+ description: "\u5207\u6362\u7F51\u7EDC\u641C\u7D22\u540E\u7AEF \u2014 mojeek\uFF08\u9ED8\u8BA4\uFF0C\u65E0\u4F9D\u8D56\uFF09\u3001searxng\uFF08\u81EA\u6258\u7BA1\uFF09\u6216 metaso\uFF08\u6BCF\u65E5 100 \u6B21\u514D\u8D39\u989D\u5EA6\uFF09",
2844
+ argsHint: "<mojeek|searxng|metaso> [<endpoint>]"
2676
2845
  }
2677
2846
  },
2678
2847
  wizard: {
@@ -2866,12 +3035,12 @@ var zhCN = {
2866
3035
  budgetExhausted: "\u4F1A\u8BDD\u9884\u7B97\u5DF2\u7528\u5B8C \u2014 \u5DF2\u82B1\u8D39 ${spent} \u2265 \u4E0A\u9650 ${cap}\u3002\u7528 /budget <usd> \u63D0\u9AD8\u4E0A\u9650\uFF0C/budget off \u6E05\u9664\u4E0A\u9650\uFF0C\u6216\u7ED3\u675F\u4F1A\u8BDD\u3002",
2867
3036
  budget80Pct: "\u25B2 \u9884\u7B97\u5DF2\u7528 80% \u2014 ${spent} / ${cap}\u3002\u4E0B\u4E00\u4E24\u8F6E\u53EF\u80FD\u5C31\u89E6\u9876\u3002",
2868
3037
  proArmed: "\u21E7 /pro \u5DF2\u88C5\u5907 \u2014 \u672C\u8F6E\u4F7F\u7528 deepseek-v4-pro\uFF08\u4E00\u6B21\u6027 \xB7 \u672C\u8F6E\u540E\u81EA\u52A8\u89E3\u9664\uFF09",
2869
- abortedAtIter: "\u5728\u7B2C {iter}/{cap} \u6B21\u5DE5\u5177\u8C03\u7528\u5904\u4E2D\u65AD \u2014 \u672A\u751F\u6210\u603B\u7ED3\u5373\u505C\u6B62\uFF08\u6309 \u2191 + Enter \u6216 /retry \u6062\u590D\uFF09",
3038
+ abortedAtIter: "\u5728\u7B2C {iter} \u6B21\u5DE5\u5177\u8C03\u7528\u5904\u4E2D\u65AD \u2014 \u672A\u751F\u6210\u603B\u7ED3\u5373\u505C\u6B62\uFF08\u6309 \u2191 + Enter \u6216 /retry \u6062\u590D\uFF09",
2870
3039
  toolUploadStatus: "\u5DE5\u5177\u7ED3\u679C\u5DF2\u4E0A\u4F20 \xB7 \u6A21\u578B\u5728\u751F\u6210\u4E0B\u4E00\u6761\u54CD\u5E94\u524D\u601D\u8003\u4E2D\u2026",
2871
- toolBudgetWarning: "\u5DF2\u7528 {iter}/{cap} \u6B21\u5DE5\u5177\u8C03\u7528 \u2014 \u63A5\u8FD1\u4E0A\u9650\u3002\u6309 Esc \u7ACB\u5373\u5F3A\u5236\u603B\u7ED3\u3002",
2872
- preflightFoldStatus: "\u9884\u68C0\uFF1A\u4E0A\u4E0B\u6587\u63A5\u8FD1\u4E0A\u9650\uFF0C\u5C1D\u8BD5\u6298\u53E0\u2026",
2873
- preflightFolded: "\u9884\u68C0\uFF1A\u8BF7\u6C42\u7EA6 {estimate}/{ctxMax} tokens\uFF08{pct}%\uFF09\u2014 \u5DF2\u6298\u53E0 {beforeMessages} \u6761\u6D88\u606F \u2192 {afterMessages}\uFF08\u603B\u7ED3 {summaryChars} \u5B57\uFF09\u3002\u53D1\u9001\u4E2D\u3002",
2874
- preflightNoFold: "\u9884\u68C0\uFF1A\u8BF7\u6C42\u7EA6 {estimate}/{ctxMax} tokens\uFF08{pct}%\uFF09\u4E14\u6CA1\u6709\u53EF\u6298\u53E0\u7684\u5185\u5BB9 \u2014 DeepSeek \u5927\u6982\u7387\u4F1A\u8FD4\u56DE 400\u3002\u8BF7\u8FD0\u884C /clear \u6216 /new \u91CD\u65B0\u5F00\u59CB\u3002",
3040
+ preflightTruncateStatus: "\u9884\u68C0\uFF1A\u4E0A\u4E0B\u6587\u63A5\u8FD1\u4E0A\u9650\uFF0C\u6B63\u5728\u88C1\u526A\u6700\u65E9\u5386\u53F2\u2026",
3041
+ preflightTruncated: "\u9884\u68C0\uFF1A\u8BF7\u6C42\u7EA6 {estimate}/{ctxMax} tokens\uFF08{pct}%\uFF09\u2014 \u5DF2\u88C1\u526A {beforeMessages} \u6761\u6D88\u606F \u2192 {afterMessages}\u3002\u53D1\u9001\u4E2D\u3002",
3042
+ preflightTruncatedStillFull: "\u9884\u68C0\uFF1A\u88C1\u526A {beforeMessages} \u6761\u6D88\u606F \u2192 {afterMessages} \u540E\uFF0C\u8BF7\u6C42\u4ECD\u7EA6 {estimate}/{ctxMax} tokens\uFF08{pct}%\uFF09\u2014 DeepSeek \u5927\u6982\u7387\u4F1A\u8FD4\u56DE 400\u3002\u8BF7\u8FD0\u884C /clear \u6216 /new \u91CD\u65B0\u5F00\u59CB\u3002",
3043
+ preflightNoFold: "\u9884\u68C0\uFF1A\u8BF7\u6C42\u7EA6 {estimate}/{ctxMax} tokens\uFF08{pct}%\uFF09\u4E14\u6CA1\u6709\u53EF\u88C1\u526A\u7684\u5185\u5BB9 \u2014 DeepSeek \u5927\u6982\u7387\u4F1A\u8FD4\u56DE 400\u3002\u8BF7\u8FD0\u884C /clear \u6216 /new \u91CD\u65B0\u5F00\u59CB\u3002",
2875
3044
  flashEscalation: "\u21E7 flash \u8BF7\u6C42\u5347\u7EA7 \u2014 \u672C\u8F6E\u6539\u7528 {model}{reasonSuffix}",
2876
3045
  harvestStatus: "\u6B63\u5728\u4ECE\u63A8\u7406\u8FC7\u7A0B\u63D0\u53D6\u8BA1\u5212\u72B6\u6001\u2026",
2877
3046
  autoEscalation: "\u21E7 \u672C\u8F6E\u5269\u4F59\u8C03\u7528\u81EA\u52A8\u5347\u7EA7\u5230 {model} \u2014 flash \u547D\u4E2D {breakdown}\u3002\u4E0B\u4E00\u8F6E\u56DE\u9000\u5230 {fallback}\uFF0C\u9664\u975E\u5DF2\u88C5\u5907 /pro\u3002",
@@ -2900,11 +3069,9 @@ var zhCN = {
2900
3069
  reasonAborted: "[\u7528\u6237\u5DF2\u4E2D\u65AD\uFF08Esc\uFF09 \u2014 \u6B63\u5728\u603B\u7ED3\u5230\u76EE\u524D\u4E3A\u6B62\u7684\u53D1\u73B0]",
2901
3070
  reasonContextGuard: "[\u4E0A\u4E0B\u6587\u989D\u5EA6\u5373\u5C06\u8017\u5C3D \u2014 \u5728\u4E0B\u4E00\u6B21\u8C03\u7528\u6EA2\u51FA\u4E4B\u524D\u5148\u603B\u7ED3]",
2902
3071
  reasonStuck: "[\u5361\u5728\u91CD\u590D\u7684\u5DE5\u5177\u8C03\u7528\u4E0A \u2014 \u8BF4\u660E\u5DF2\u5C1D\u8BD5\u7684\u65B9\u6CD5\u4EE5\u53CA\u963B\u585E\u70B9]",
2903
- reasonBudget: "[\u5DE5\u5177\u8C03\u7528\u914D\u989D\uFF08{iterCap}\uFF09\u5DF2\u7528\u5C3D \u2014 \u57FA\u4E8E\u5DF2\u53D1\u73B0\u7684\u5185\u5BB9\u5F3A\u5236\u603B\u7ED3]",
2904
3072
  labelAborted: "\u7528\u6237\u4E2D\u65AD",
2905
3073
  labelContextGuard: "\u89E6\u53D1\u4E0A\u4E0B\u6587\u4FDD\u62A4\uFF08prompt > 80% \u7A97\u53E3\uFF09",
2906
- labelStuck: "\u5361\u6B7B\uFF08\u91CD\u590D\u5DE5\u5177\u8C03\u7528\u88AB\u53CD\u98CE\u66B4\u673A\u5236\u6291\u5236\uFF09",
2907
- labelBudget: "\u5DE5\u5177\u8C03\u7528\u914D\u989D\uFF08{iterCap}\uFF09\u5DF2\u7528\u5C3D"
3074
+ labelStuck: "\u5361\u6B7B\uFF08\u91CD\u590D\u5DE5\u5177\u8C03\u7528\u88AB\u53CD\u98CE\u66B4\u673A\u5236\u6291\u5236\uFF09"
2908
3075
  },
2909
3076
  handlers: {
2910
3077
  basic: {
@@ -3214,11 +3381,13 @@ var zhCN = {
3214
3381
  usageMojeek: " /search-engine mojeek \u4F7F\u7528 Mojeek\uFF08\u9ED8\u8BA4\uFF0C\u65E0\u5916\u90E8\u4F9D\u8D56\uFF09",
3215
3382
  usageSearxng: " /search-engine searxng \u4F7F\u7528 SearXNG \u9ED8\u8BA4\u7AEF\u70B9",
3216
3383
  usageSearxngUrl: " /search-engine searxng <url> \u4F7F\u7528 SearXNG \u81EA\u5B9A\u4E49\u7AEF\u70B9",
3384
+ usageMetaso: " /search-engine metaso \u4F7F\u7528 Metaso API\uFF08\u6BCF\u5929 100 \u6B21\u514D\u8D39\uFF0C\u914D\u7F6E\u4F60\u81EA\u5DF1\u7684 API \u5BC6\u94A5\u53EF\u63D0\u5347\u9650\u989D\uFF09",
3217
3385
  alias: "\u522B\u540D\uFF1A/se",
3218
3386
  searxngInfo: "SearXNG \u662F\u4E00\u4E2A\u81EA\u6258\u7BA1\u7684\u5143\u641C\u7D22\u5F15\u64CE\uFF08https://github.com/searxng/searxng\uFF09\u3002",
3219
3387
  searxngInstall: "\u5B89\u88C5\u547D\u4EE4\uFF1A docker run -d -p 8080:8080 searxng/searxng",
3220
3388
  switched: '\u5DF2\u5207\u6362\u7F51\u9875\u641C\u7D22\u5F15\u64CE\u4E3A "{engine}"\u3002{note}',
3221
3389
  switchedSearxngNote: " \u8BF7\u786E\u4FDD SearXNG \u5728 {endpoint} \u8FD0\u884C\u3002",
3390
+ switchedMetasoNote: " \u6BCF\u65E5\u9650\u989D 100 \u6B21\uFF08\u914D\u7F6E\u4F60\u81EA\u5DF1\u7684 API \u5BC6\u94A5\u53EF\u63D0\u5347\u9650\u989D\uFF09\u3002",
3222
3391
  confirmed: '\u2713 \u7F51\u9875\u641C\u7D22\u5F15\u64CE\u5DF2\u8BBE\u4E3A "{engine}"{detail}\u3002\u4E0B\u4E00\u8F6E\u6A21\u578B\u8C03\u7528\u5C06\u751F\u6548\u3002',
3223
3392
  confirmedDetail: "\uFF08{endpoint}\uFF09"
3224
3393
  },
@@ -3238,7 +3407,17 @@ var zhCN = {
3238
3407
  runInfo: "\u25B8 \u6B63\u5728\u8FD0\u884C\u6280\u80FD\uFF1A{name}{args}",
3239
3408
  newUsage: "\u7528\u6CD5\uFF1A/skill new <name> [--global]",
3240
3409
  newCreated: "\u25B8 \u5DF2\u521B\u5EFA\u6280\u80FD\uFF1A{name}\n {path}\n \u7F16\u8F91\u540E\u7528 `/skill {name}` \u8C03\u7528",
3241
- newError: "\u25B2 /skill new \u5931\u8D25\uFF1A{reason}"
3410
+ newError: "\u25B2 /skill new \u5931\u8D25\uFF1A{reason}",
3411
+ pathsHeader: "\u6280\u80FD\u8DEF\u5F84\uFF08\u6309\u4F18\u5148\u7EA7\uFF09\uFF1A",
3412
+ pathsPriority: "\u4F18\u5148\u7EA7\uFF1A\u9879\u76EE > \u914D\u7F6E\u987A\u5E8F\u4E2D\u7684\u81EA\u5B9A\u4E49\u8DEF\u5F84 > \u5168\u5C40 > \u5185\u7F6E\u3002\u66F4\u6539\u4F1A\u5728\u4E0B\u6B21 /new \u6216\u65B0\u4F1A\u8BDD\u5237\u65B0\u7CFB\u7EDF\u63D0\u793A\u8BCD\u65F6\u751F\u6548\u3002",
3413
+ pathsUsage: "\u7528\u6CD5\uFF1A/skill paths [list]\n /skill paths add <path>\n /skill paths remove <path|N>",
3414
+ pathsAddUsage: "\u7528\u6CD5\uFF1A/skill paths add <path>",
3415
+ pathsRemoveUsage: "\u7528\u6CD5\uFF1A/skill paths remove <path|N>",
3416
+ pathsAdded: "\u25B8 \u5DF2\u6DFB\u52A0\u81EA\u5B9A\u4E49\u6280\u80FD\u8DEF\u5F84\uFF1A{path}",
3417
+ pathsAlready: "\u25B8 \u81EA\u5B9A\u4E49\u6280\u80FD\u8DEF\u5F84\u5DF2\u5B58\u5728\uFF1A{path}",
3418
+ pathsRemoved: "\u25B8 \u5DF2\u79FB\u9664\u81EA\u5B9A\u4E49\u6280\u80FD\u8DEF\u5F84\uFF1A{path}",
3419
+ pathsRemoveNotFound: "\u25B8 \u6CA1\u6709\u5339\u914D\u7684\u81EA\u5B9A\u4E49\u6280\u80FD\u8DEF\u5F84\uFF1A{target}",
3420
+ pathsRestartHint: "\u5F53\u524D\u4F1A\u8BDD\u7684\u7CFB\u7EDF\u63D0\u793A\u8BCD\u4E0D\u4F1A\u70ED\u66F4\u65B0\uFF1B\u8FD0\u884C /new \u6216\u542F\u52A8\u65B0\u4F1A\u8BDD\u4EE5\u5237\u65B0\u6280\u80FD\u7D22\u5F15\u3002"
3242
3421
  }
3243
3422
  },
3244
3423
  statusBar: {
@@ -3285,7 +3464,8 @@ var zhCN = {
3285
3464
  editorNoRawMode: "\u5916\u90E8\u7F16\u8F91\u5668\u4E0D\u53EF\u7528 \u2014 \u5F53\u524D\u7EC8\u7AEF\u4E0D\u652F\u6301 raw-mode \u5207\u6362",
3286
3465
  editorFailed: "\u5916\u90E8\u7F16\u8F91\u5668\uFF1A",
3287
3466
  editorMissing: "\u672A\u8BBE\u7F6E $EDITOR / $VISUAL / $GIT_EDITOR \u2014 \u8BF7\u5BFC\u51FA\u73AF\u5883\u53D8\u91CF\uFF08\u4F8B\u5982 `export EDITOR=nano`\uFF09\u540E\u91CD\u8BD5",
3288
- editorExited: "\u7F16\u8F91\u5668\u5F02\u5E38\u9000\u51FA\uFF0C\u8FD4\u56DE\u7801 {code}"
3467
+ editorExited: "\u7F16\u8F91\u5668\u5F02\u5E38\u9000\u51FA\uFF0C\u8FD4\u56DE\u7801 {code}",
3468
+ typeaheadStaged: "\u25B8 {count} \u884C\u5DF2\u6682\u5B58 \xB7 esc \u53EC\u56DE"
3289
3469
  },
3290
3470
  pathConfirm: {
3291
3471
  title: "\u6C99\u7BB1\u5916\u8DEF\u5F84",
@@ -3455,6 +3635,12 @@ var zhCN = {
3455
3635
  endpointMustBeHttp: "web_search: SearXNG \u7AEF\u70B9\u5FC5\u987B\u662F http(s) \u534F\u8BAE\uFF0C\u5F53\u524D\u4E3A {protocol} \u2014 try: \u4F7F\u7528 /search-endpoint http://host:port \u8BBE\u7F6E\u6709\u6548\u7684 URL",
3456
3636
  cannotReach: "web_search: \u65E0\u6CD5\u8BBF\u95EE SearXNG \u670D\u52A1\u5668 {endpoint} \u2014 try: \u5B89\u88C5\u5E76\u542F\u52A8 SearXNG\uFF08https://github.com/searxng/searxng\uFF0C\u4F8B\u5982 `docker run -d -p 8080:8080 searxng/searxng`\uFF09\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek \u5207\u6362\u5230\u9ED8\u8BA4\u5F15\u64CE",
3457
3637
  searxngNoResults: "web_search: \u8FD4\u56DE 0 \u6761\u7ED3\u679C\u4F46 SearXNG \u54CD\u5E94\u770B\u8D77\u6765\u4E0D\u662F\u6B63\u5E38\u7A7A\u7ED3\u679C\u9875\uFF08{chars} \u5B57\u7B26\uFF09\u2014 try: \u4F7F\u7528\u66F4\u7B80\u5355\u7684\u5173\u952E\u8BCD\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek \u5207\u6362\u5F15\u64CE",
3638
+ metasoDailyLimit: "web_search: \u9ED8\u8BA4 API \u5BC6\u94A5\u7684\u6BCF\u65E5\u641C\u7D22\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 \u2014 \u8BBE\u7F6E METASO_API_KEY \u73AF\u5883\u53D8\u91CF\uFF0C\u6216\u5728 https://metaso.cn/search-api/playground \u83B7\u53D6\u81EA\u5DF1\u7684\u5BC6\u94A5",
3639
+ metasoUnauthorized: "web_search: Metaso API \u5BC6\u94A5\u88AB\u62D2\u7EDD \u2014 \u68C0\u67E5 METASO_API_KEY\uFF0C\u6216\u5728 https://metaso.cn/search-api/playground \u83B7\u53D6\u5BC6\u94A5",
3640
+ metasoRateLimit: "web_search: Metaso \u8BF7\u6C42\u9891\u7387\u9650\u5236 \u2014 \u7B49\u5F85\u540E\u91CD\u8BD5\uFF0C\u6216\u5728 https://metaso.cn/search-api/playground \u83B7\u53D6\u81EA\u5DF1\u7684\u5BC6\u94A5",
3641
+ metasoServerError: "web_search: Metaso \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek \u5207\u6362\u5F15\u64CE",
3642
+ metasoParseError: "web_search: Metaso \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
3643
+ metasoApiError: "web_search: Metaso API \u9519\u8BEF\uFF08code {code}: {message}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
3458
3644
  fetchStatus: "web_fetch {status} for {url} \u2014 try: \u5728\u6D4F\u89C8\u5668\u4E2D\u786E\u8BA4\u8BE5 URL \u80FD\u5426\u8BBF\u95EE\uFF1B\u8BE5\u72B6\u6001\u7801\u8868\u660E\u76EE\u6807\u4E3B\u673A\u8FD4\u56DE\u4E86\u9519\u8BEF\u9875\u9762",
3459
3645
  fetchRateLimit429: "web_fetch 429 for {url} \u2014 try: \u7B49\u5F85 10 \u79D2\u540E\u91CD\u8BD5\uFF1B\u76EE\u6807\u4E3B\u673A\u6B63\u5728\u5BF9\u8BE5\u5BA2\u6237\u7AEF\u8FDB\u884C\u9650\u6D41",
3460
3646
  fetchForbidden403: "web_fetch 403 for {url} \u2014 try: \u76EE\u6807\u4E3B\u673A\u62D2\u7EDD\u8BE5\u5BA2\u6237\u7AEF\u8BBF\u95EE\uFF1B\u8BE5\u9875\u9762\u53EF\u80FD\u9700\u8981\u767B\u5F55\u6216\u5C4F\u853D\u722C\u866B \u2014 \u6539\u7528 web_search \u6458\u8981",
@@ -3786,7 +3972,7 @@ function matchesTool(hook, toolName) {
3786
3972
  }
3787
3973
  var HOOK_OUTPUT_CAP_BYTES = 256 * 1024;
3788
3974
  function defaultSpawner(input) {
3789
- return new Promise((resolve10) => {
3975
+ return new Promise((resolve13) => {
3790
3976
  const child = spawn(input.command, {
3791
3977
  cwd: input.cwd,
3792
3978
  shell: true,
@@ -3831,7 +4017,7 @@ function defaultSpawner(input) {
3831
4017
  child.stderr.on("data", (chunk) => onChunk("stderr", chunk));
3832
4018
  child.once("error", (err) => {
3833
4019
  clearTimeout(timer);
3834
- resolve10({
4020
+ resolve13({
3835
4021
  exitCode: null,
3836
4022
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
3837
4023
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
@@ -3842,7 +4028,7 @@ function defaultSpawner(input) {
3842
4028
  });
3843
4029
  child.once("close", (code) => {
3844
4030
  clearTimeout(timer);
3845
- resolve10({
4031
+ resolve13({
3846
4032
  exitCode: code,
3847
4033
  stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(),
3848
4034
  stderr: Buffer.concat(stderrChunks).toString("utf8").trim(),
@@ -4078,22 +4264,211 @@ function encode(text) {
4078
4264
  function countTokens(text) {
4079
4265
  return encode(text).length;
4080
4266
  }
4081
- function estimateConversationTokens(messages) {
4082
- let total = 0;
4083
- for (const m of messages) {
4084
- if (typeof m.content === "string" && m.content) {
4085
- total += countTokens(m.content);
4267
+ var DEFAULT_BOUNDED_TOKENIZE_CHARS = 2 * 1024;
4268
+ function countTokensBounded(text, maxChars = DEFAULT_BOUNDED_TOKENIZE_CHARS) {
4269
+ if (text.length === 0) return 0;
4270
+ const cap = Math.floor(maxChars);
4271
+ if (cap > 0 && text.length <= cap) return countTokens(text);
4272
+ if (cap <= 0) return Math.max(1, Math.ceil(text.length * 0.3));
4273
+ const headChars = Math.ceil(cap / 2);
4274
+ const tailChars = Math.floor(cap / 2);
4275
+ const head = text.slice(0, headChars);
4276
+ const tail = tailChars > 0 ? text.slice(-tailChars) : "";
4277
+ const sampleChars = head.length + tail.length;
4278
+ const sampleTokens = countTokens(head) + countTokens(tail);
4279
+ const ratio = sampleChars > 0 ? sampleTokens / sampleChars : 0.3;
4280
+ return Math.max(1, Math.ceil(text.length * ratio));
4281
+ }
4282
+ var BOS = "<\uFF5Cbegin\u2581of\u2581sentence\uFF5C>";
4283
+ var EOS = "<\uFF5Cend\u2581of\u2581sentence\uFF5C>";
4284
+ var USER_SP = "<\uFF5CUser\uFF5C>";
4285
+ var ASSISTANT_SP = "<\uFF5CAssistant\uFF5C>";
4286
+ var THINK_START = "<think>";
4287
+ var THINK_END = "</think>";
4288
+ var DSML = "\uFF5CDSML\uFF5C";
4289
+ var TC_BEGIN = `<${DSML}tool_calls>`;
4290
+ var TC_END = `</${DSML}tool_calls>`;
4291
+ var INVOKE_BEGIN = `<${DSML}invoke name="`;
4292
+ var INVOKE_END = `</${DSML}invoke>`;
4293
+ var PARAM_TEMPLATE = `<${DSML}parameter name="{key}" string="{is_str}">{value}</${DSML}parameter>`;
4294
+ var TOOL_RESULT_TEMPLATE = "<tool_result>{content}</tool_result>";
4295
+ var toolsTemplateCache = /* @__PURE__ */ new WeakMap();
4296
+ function renderTools(tools) {
4297
+ const cached2 = toolsTemplateCache.get(tools);
4298
+ if (cached2 !== void 0) return cached2;
4299
+ const schemas = tools.map((t2) => {
4300
+ const fn = t2.function ?? t2;
4301
+ return JSON.stringify(fn);
4302
+ }).join("\n");
4303
+ const rendered = `## Tools
4304
+
4305
+ You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<${DSML}tool_calls>" block like the following:
4306
+
4307
+ <${DSML}tool_calls>
4308
+ <${DSML}invoke name="$TOOL_NAME">
4309
+ <${DSML}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</${DSML}parameter>
4310
+ ...
4311
+ </${DSML}invoke>
4312
+ <${DSML}invoke name="$TOOL_NAME2">
4313
+ ...
4314
+ </${DSML}invoke>
4315
+ </${DSML}tool_calls>
4316
+
4317
+ String parameters should be specified as is and set \`string="true"\`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set \`string="false"\`.
4318
+
4319
+ If thinking_mode is enabled (triggered by ${THINK_START}), you MUST output your complete reasoning inside ${THINK_START}...${THINK_END} BEFORE any tool calls or final response.
4320
+
4321
+ Otherwise, output directly after ${THINK_END} with tool calls or final response.
4322
+
4323
+ ### Available Tool Schemas
4324
+
4325
+ ${schemas}
4326
+
4327
+ You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.`;
4328
+ toolsTemplateCache.set(tools, rendered);
4329
+ return rendered;
4330
+ }
4331
+ function encodeArgumentsToDsml(argsJson) {
4332
+ let args;
4333
+ try {
4334
+ args = JSON.parse(argsJson);
4335
+ } catch {
4336
+ args = { arguments: argsJson };
4337
+ }
4338
+ return Object.entries(args).map(
4339
+ ([k, v]) => PARAM_TEMPLATE.replace("{key}", k).replace("{is_str}", typeof v === "string" ? "true" : "false").replace("{value}", typeof v === "string" ? v : JSON.stringify(v))
4340
+ ).join("\n");
4341
+ }
4342
+ function renderToolCallsDsml(toolCalls) {
4343
+ const invokes = toolCalls.map((tc) => {
4344
+ const name = tc.function?.name ?? "";
4345
+ const argsJson = tc.function?.arguments ?? "{}";
4346
+ return `${INVOKE_BEGIN + name}">
4347
+ ${encodeArgumentsToDsml(argsJson)}
4348
+ ${INVOKE_END}`;
4349
+ }).join("\n");
4350
+ return `
4351
+
4352
+ ${TC_BEGIN}
4353
+ ${invokes}
4354
+ ${TC_END}`;
4355
+ }
4356
+ function mergeToolMessages(messages) {
4357
+ const merged = [];
4358
+ for (const msg of messages) {
4359
+ const role = msg.role ?? "user";
4360
+ if (role === "tool") {
4361
+ const toolBlock = TOOL_RESULT_TEMPLATE.replace("{content}", msg.content ?? "");
4362
+ const last = merged[merged.length - 1];
4363
+ if (last && last.role === "user" && Array.isArray(last._toolBlocks) && Array.isArray(last._textParts)) {
4364
+ last._toolBlocks.push(toolBlock);
4365
+ last.content = `${last._textParts.join("\n\n")}
4366
+
4367
+ ${last._toolBlocks.join("\n")}`.replace(
4368
+ /^\n\n/,
4369
+ ""
4370
+ );
4371
+ } else {
4372
+ merged.push({
4373
+ role: "user",
4374
+ content: toolBlock,
4375
+ _textParts: [],
4376
+ _toolBlocks: [toolBlock]
4377
+ });
4378
+ }
4379
+ } else if (role === "user") {
4380
+ const text = msg.content ?? "";
4381
+ const last = merged[merged.length - 1];
4382
+ if (last && last.role === "user" && Array.isArray(last._toolBlocks) && Array.isArray(last._textParts)) {
4383
+ last._textParts.push(text);
4384
+ last.content = `${last._textParts.join("\n\n")}
4385
+
4386
+ ${last._toolBlocks.join("\n\n")}`.replace(
4387
+ /^\n\n/,
4388
+ ""
4389
+ );
4390
+ } else {
4391
+ merged.push({
4392
+ ...msg,
4393
+ role: "user",
4394
+ content: text,
4395
+ _textParts: [text],
4396
+ _toolBlocks: []
4397
+ });
4398
+ }
4399
+ } else {
4400
+ merged.push({ ...msg });
4086
4401
  }
4087
- if (m.tool_calls && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
4088
- total += countTokens(JSON.stringify(m.tool_calls));
4402
+ }
4403
+ for (const m of merged) {
4404
+ m._textParts = void 0;
4405
+ m._toolBlocks = void 0;
4406
+ }
4407
+ return merged;
4408
+ }
4409
+ function dropThinkingMessages(messages) {
4410
+ let lastUserIdx = -1;
4411
+ for (let i = messages.length - 1; i >= 0; i--) {
4412
+ const role = messages[i].role;
4413
+ if (role === "user" || role === "developer") {
4414
+ lastUserIdx = i;
4415
+ break;
4089
4416
  }
4090
4417
  }
4091
- return total;
4418
+ if (lastUserIdx < 0) return messages;
4419
+ const result = [];
4420
+ for (let i = 0; i < messages.length; i++) {
4421
+ const msg = messages[i];
4422
+ if (i < lastUserIdx && msg.role === "developer") continue;
4423
+ if (i < lastUserIdx && msg.role === "assistant") {
4424
+ result.push({ ...msg, reasoning_content: null });
4425
+ } else {
4426
+ result.push(msg);
4427
+ }
4428
+ }
4429
+ return result;
4092
4430
  }
4093
- function estimateRequestTokens(messages, toolSpecs) {
4094
- let total = estimateConversationTokens(messages);
4431
+ function formatDeepSeekPrompt(messages, drop_thinking = false) {
4432
+ if (messages.length === 0) return ASSISTANT_SP + THINK_END;
4433
+ let msgs = messages;
4434
+ if (drop_thinking) {
4435
+ msgs = dropThinkingMessages(msgs);
4436
+ }
4437
+ const merged = mergeToolMessages(msgs);
4438
+ let prompt = BOS;
4439
+ for (let i = 0; i < merged.length; i++) {
4440
+ const msg = merged[i];
4441
+ const role = msg.role ?? "user";
4442
+ const nextRole = i + 1 < merged.length ? merged[i + 1].role ?? "user" : null;
4443
+ if (role === "system") {
4444
+ prompt += msg.content ?? "";
4445
+ } else if (role === "user") {
4446
+ prompt += USER_SP + (msg.content ?? "");
4447
+ if (nextRole === "assistant" || nextRole === null) {
4448
+ prompt += ASSISTANT_SP + THINK_END;
4449
+ }
4450
+ } else if (role === "assistant") {
4451
+ if (msg.reasoning_content) {
4452
+ prompt += THINK_START + msg.reasoning_content + THINK_END;
4453
+ }
4454
+ if (msg.content) prompt += msg.content;
4455
+ const tcs = msg.tool_calls;
4456
+ if (Array.isArray(tcs) && tcs.length > 0) {
4457
+ prompt += renderToolCallsDsml(tcs);
4458
+ }
4459
+ prompt += EOS;
4460
+ }
4461
+ }
4462
+ return prompt;
4463
+ }
4464
+ function estimateConversationTokens(messages, drop_thinking = false) {
4465
+ if (messages.length === 0) return 0;
4466
+ return countTokensBounded(formatDeepSeekPrompt(messages, drop_thinking));
4467
+ }
4468
+ function estimateRequestTokens(messages, toolSpecs, drop_thinking = false) {
4469
+ let total = estimateConversationTokens(messages, drop_thinking);
4095
4470
  if (toolSpecs && toolSpecs.length > 0) {
4096
- total += countTokens(JSON.stringify(toolSpecs));
4471
+ total += countTokensBounded(renderTools(toolSpecs));
4097
4472
  }
4098
4473
  return total;
4099
4474
  }
@@ -4446,12 +4821,12 @@ async function waitForReady(ready, timeoutMs, serverName, signal) {
4446
4821
  let timer;
4447
4822
  let onAbort;
4448
4823
  try {
4449
- await new Promise((resolve10, reject) => {
4824
+ await new Promise((resolve13, reject) => {
4450
4825
  ready.then(
4451
4826
  () => {
4452
4827
  if (settled) return;
4453
4828
  settled = true;
4454
- resolve10();
4829
+ resolve13();
4455
4830
  },
4456
4831
  (err) => {
4457
4832
  if (settled) return;
@@ -4600,6 +4975,40 @@ function blockToString(block) {
4600
4975
  return `[unknown block: ${JSON.stringify(block)}]`;
4601
4976
  }
4602
4977
 
4978
+ // src/loop/thinking.ts
4979
+ function isThinkingModeModel(model) {
4980
+ if (model.includes("reasoner")) return true;
4981
+ if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return true;
4982
+ return false;
4983
+ }
4984
+ function thinkingModeForModel(model) {
4985
+ if (model === "deepseek-chat") return "disabled";
4986
+ if (model.includes("reasoner")) return "enabled";
4987
+ if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return "enabled";
4988
+ return void 0;
4989
+ }
4990
+ function stripHallucinatedToolMarkup(s) {
4991
+ let out = s;
4992
+ out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
4993
+ out = out.replace(/<\|DSML\|function_calls>[\s\S]*?<\/?\|DSML\|function_calls>/g, "");
4994
+ out = out.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "");
4995
+ out = out.replace(/<|DSML|[\s\S]*$/g, "");
4996
+ return out.trim();
4997
+ }
4998
+
4999
+ // src/loop/messages.ts
5000
+ function buildAssistantMessage(content, toolCalls, producingModel, reasoningContent) {
5001
+ const msg = { role: "assistant", content };
5002
+ if (toolCalls.length > 0) msg.tool_calls = toolCalls;
5003
+ if (isThinkingModeModel(producingModel) || reasoningContent && reasoningContent.length > 0) {
5004
+ msg.reasoning_content = reasoningContent ?? "";
5005
+ }
5006
+ return msg;
5007
+ }
5008
+ function buildSyntheticAssistantMessage(content, fallbackModel) {
5009
+ return buildAssistantMessage(content, [], fallbackModel, "");
5010
+ }
5011
+
4603
5012
  // src/memory/session.ts
4604
5013
  import { execFileSync } from "child_process";
4605
5014
  import {
@@ -4769,11 +5178,11 @@ function countLines(path2) {
4769
5178
 
4770
5179
  // src/telemetry/stats.ts
4771
5180
  var DEEPSEEK_PRICING = {
4772
- "deepseek-v4-flash": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
4773
- "deepseek-v4-pro": { inputCacheHit: 0.139, inputCacheMiss: 1.667, output: 3.333 },
5181
+ "deepseek-v4-flash": { inputCacheHit: 28e-4, inputCacheMiss: 0.14, output: 0.28 },
5182
+ "deepseek-v4-pro": { inputCacheHit: 3625e-6, inputCacheMiss: 0.435, output: 0.87 },
4774
5183
  // Compat aliases — priced as v4-flash per the deprecation notice.
4775
- "deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
4776
- "deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 }
5184
+ "deepseek-chat": { inputCacheHit: 28e-4, inputCacheMiss: 0.14, output: 0.28 },
5185
+ "deepseek-reasoner": { inputCacheHit: 28e-4, inputCacheMiss: 0.14, output: 0.28 }
4777
5186
  };
4778
5187
  var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
4779
5188
  var DEEPSEEK_CONTEXT_TOKENS = {
@@ -4835,6 +5244,14 @@ var SessionStats = class {
4835
5244
  this._carryoverLastPromptTokens = opts.lastPromptTokens;
4836
5245
  }
4837
5246
  }
5247
+ reset() {
5248
+ this.turns.length = 0;
5249
+ this._carryoverCost = 0;
5250
+ this._carryoverTurns = 0;
5251
+ this._carryoverCacheHit = 0;
5252
+ this._carryoverCacheMiss = 0;
5253
+ this._carryoverLastPromptTokens = 0;
5254
+ }
4838
5255
  record(turn, model, usage) {
4839
5256
  const cost = costUsd(model, usage);
4840
5257
  const stats = {
@@ -4901,6 +5318,8 @@ var HISTORY_FOLD_AGGRESSIVE_TAIL_FRACTION = 0.1;
4901
5318
  var HISTORY_FOLD_MIN_SAVINGS_FRACTION = 0.3;
4902
5319
  var FORCE_SUMMARY_THRESHOLD = 0.8;
4903
5320
  var PREFLIGHT_EMERGENCY_THRESHOLD = 0.95;
5321
+ var PREFLIGHT_MECHANICAL_TARGET_FRACTION = 0.7;
5322
+ var HISTORY_FOLD_SUMMARY_TIMEOUT_MS = 15e3;
4904
5323
  var HISTORY_FOLD_MARKER = "[CONVERSATION HISTORY SUMMARY \u2014 earlier turns folded for context efficiency]\n\n";
4905
5324
  var SKILL_PIN_MEMO_HEADER = "[Active skill memos \u2014 preserved verbatim across the fold:]";
4906
5325
  var SKILL_PIN_REGEX = /<skill-pin name="([^"]+)">\n[\s\S]*?\n<\/skill-pin>/g;
@@ -4955,7 +5374,7 @@ var ContextManager = class {
4955
5374
  /** Local-side preflight before sending a request — catches oversized payloads early. */
4956
5375
  decidePreflight(messages, toolSpecs, model) {
4957
5376
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
4958
- const estimate = estimateRequestTokens(messages, toolSpecs ?? null);
5377
+ const estimate = estimateRequestTokens(messages, toolSpecs ?? null, true);
4959
5378
  return {
4960
5379
  needsAction: estimate / ctxMax > PREFLIGHT_EMERGENCY_THRESHOLD,
4961
5380
  estimateTokens: estimate,
@@ -4974,7 +5393,7 @@ var ContextManager = class {
4974
5393
  summaryChars: 0
4975
5394
  };
4976
5395
  if (all.length === 0) return noop;
4977
- const tokenCounts = all.map((m) => estimateConversationTokens([m]));
5396
+ const tokenCounts = all.map((m) => countTokensBounded(m.content ?? ""));
4978
5397
  const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);
4979
5398
  let cumTokens = 0;
4980
5399
  let boundary = all.length;
@@ -4990,16 +5409,18 @@ var ContextManager = class {
4990
5409
  if (headTokens < totalTokens * HISTORY_FOLD_MIN_SAVINGS_FRACTION) return noop;
4991
5410
  const { stubbedHead, pinnedBodies } = extractPinnedSkills(head);
4992
5411
  const summary = await this.summarizeForFold(stubbedHead);
4993
- if (!summary) return noop;
5412
+ if (!summary.content) return noop;
4994
5413
  const memoTail = pinnedBodies.length > 0 ? `
4995
5414
 
4996
5415
  ${SKILL_PIN_MEMO_HEADER}
4997
5416
 
4998
5417
  ${pinnedBodies.join("\n\n")}` : "";
4999
- const summaryMsg = {
5000
- role: "assistant",
5001
- content: HISTORY_FOLD_MARKER + summary + memoTail
5002
- };
5418
+ const summaryMsg = buildAssistantMessage(
5419
+ HISTORY_FOLD_MARKER + summary.content + memoTail,
5420
+ [],
5421
+ model,
5422
+ summary.reasoningContent
5423
+ );
5003
5424
  const replacement = [summaryMsg, ...tail];
5004
5425
  this.deps.log.compactInPlace(replacement);
5005
5426
  this.persistRewrite(replacement);
@@ -5007,7 +5428,51 @@ ${pinnedBodies.join("\n\n")}` : "";
5007
5428
  folded: true,
5008
5429
  beforeMessages: all.length,
5009
5430
  afterMessages: replacement.length,
5010
- summaryChars: summary.length
5431
+ summaryChars: summary.content.length
5432
+ };
5433
+ }
5434
+ /** Pure local emergency compaction for preflight: drop oldest log entries and keep a valid tail. */
5435
+ mechanicalTruncate(model, opts) {
5436
+ const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
5437
+ const targetTokens = opts?.targetTokens ?? Math.floor(ctxMax * PREFLIGHT_MECHANICAL_TARGET_FRACTION);
5438
+ const all = this.deps.log.toMessages();
5439
+ const noop = {
5440
+ folded: false,
5441
+ beforeMessages: all.length,
5442
+ afterMessages: all.length,
5443
+ summaryChars: 0
5444
+ };
5445
+ if (all.length === 0) return noop;
5446
+ const tokenCounts = all.map((m) => estimateConversationTokens([m], true));
5447
+ let latestUserBoundary = -1;
5448
+ for (let i = all.length - 1; i >= 0; i--) {
5449
+ if (all[i].role === "user") {
5450
+ latestUserBoundary = i;
5451
+ break;
5452
+ }
5453
+ }
5454
+ let cumTokens = 0;
5455
+ let boundary = all.length;
5456
+ let foundSafeBoundary = false;
5457
+ for (let i = all.length - 1; i >= 0; i--) {
5458
+ const next = cumTokens + tokenCounts[i];
5459
+ if (next > targetTokens) break;
5460
+ cumTokens = next;
5461
+ if (all[i].role === "user") {
5462
+ boundary = i;
5463
+ foundSafeBoundary = true;
5464
+ }
5465
+ }
5466
+ if (boundary <= 0) return noop;
5467
+ const replacement = foundSafeBoundary ? all.slice(boundary) : opts?.allowEmpty ? [] : latestUserBoundary >= 0 ? all.slice(latestUserBoundary) : all;
5468
+ if (replacement.length === all.length) return noop;
5469
+ this.deps.log.compactInPlace(replacement);
5470
+ this.persistRewrite(replacement);
5471
+ return {
5472
+ folded: true,
5473
+ beforeMessages: all.length,
5474
+ afterMessages: replacement.length,
5475
+ summaryChars: 0
5011
5476
  };
5012
5477
  }
5013
5478
  /** Drop a trailing in-flight assistant-with-tool_calls before a forced summary. Tail-only mutation; prefix cache safe. */
@@ -5033,18 +5498,51 @@ ${pinnedBodies.join("\n\n")}` : "";
5033
5498
  content: "Summarize the conversation above as plain prose. This summary replaces the original turns to free context \u2014 make it self-contained."
5034
5499
  }
5035
5500
  ];
5501
+ const turnSignal = this.deps.getAbortSignal();
5502
+ const foldCtrl = new AbortController();
5503
+ let cleanupAbort = () => {
5504
+ };
5505
+ let timeout;
5036
5506
  try {
5037
- const resp = await this.deps.client.chat({
5038
- model: summaryModel,
5039
- messages,
5040
- signal: this.deps.getAbortSignal(),
5041
- thinking: thinkingModeForModel(summaryModel),
5042
- reasoningEffort: "high"
5507
+ const abortPromise = new Promise((_, reject) => {
5508
+ const abort = () => {
5509
+ foldCtrl.abort();
5510
+ reject(new Error("fold-aborted"));
5511
+ };
5512
+ if (turnSignal.aborted) {
5513
+ abort();
5514
+ } else {
5515
+ turnSignal.addEventListener("abort", abort, { once: true });
5516
+ cleanupAbort = () => turnSignal.removeEventListener("abort", abort);
5517
+ }
5518
+ });
5519
+ const timeoutPromise = new Promise((_, reject) => {
5520
+ timeout = setTimeout(() => {
5521
+ foldCtrl.abort();
5522
+ reject(new Error("fold-timeout"));
5523
+ }, HISTORY_FOLD_SUMMARY_TIMEOUT_MS);
5043
5524
  });
5525
+ const resp = await Promise.race([
5526
+ this.deps.client.chat({
5527
+ model: summaryModel,
5528
+ messages,
5529
+ signal: foldCtrl.signal,
5530
+ thinking: thinkingModeForModel(summaryModel),
5531
+ reasoningEffort: "high"
5532
+ }),
5533
+ abortPromise,
5534
+ timeoutPromise
5535
+ ]);
5044
5536
  this.deps.stats.record(this.deps.getCurrentTurn(), summaryModel, resp.usage ?? new Usage());
5045
- return stripHallucinatedToolMarkup((resp.content ?? "").trim());
5537
+ return {
5538
+ content: stripHallucinatedToolMarkup((resp.content ?? "").trim()),
5539
+ reasoningContent: resp.reasoningContent ?? ""
5540
+ };
5046
5541
  } catch {
5047
- return "";
5542
+ return { content: "", reasoningContent: "" };
5543
+ } finally {
5544
+ if (timeout) clearTimeout(timeout);
5545
+ cleanupAbort();
5048
5546
  }
5049
5547
  }
5050
5548
  persistRewrite(messages) {
@@ -5136,17 +5634,15 @@ function formatDeepSeek5xx(status, probe) {
5136
5634
  const action = probe?.reachable === false ? t("errors.deepseek5xxActionNetwork") : t("errors.deepseek5xxActionRetry");
5137
5635
  return `${head}${probeNote}${action}`;
5138
5636
  }
5139
- function reasonPrefixFor(reason, iterCap) {
5637
+ function reasonPrefixFor(reason) {
5140
5638
  if (reason === "aborted") return t("errors.reasonAborted");
5141
5639
  if (reason === "context-guard") return t("errors.reasonContextGuard");
5142
- if (reason === "stuck") return t("errors.reasonStuck");
5143
- return t("errors.reasonBudget", { iterCap });
5640
+ return t("errors.reasonStuck");
5144
5641
  }
5145
- function errorLabelFor(reason, iterCap) {
5642
+ function errorLabelFor(reason) {
5146
5643
  if (reason === "aborted") return t("errors.labelAborted");
5147
5644
  if (reason === "context-guard") return t("errors.labelContextGuard");
5148
- if (reason === "stuck") return t("errors.labelStuck");
5149
- return t("errors.labelBudget", { iterCap });
5645
+ return t("errors.labelStuck");
5150
5646
  }
5151
5647
  function extractDeepSeekErrorMessage(body) {
5152
5648
  const trimmed = body.trim();
@@ -5188,50 +5684,14 @@ function looksLikePartialEscalationMarker(buf) {
5188
5684
  return true;
5189
5685
  }
5190
5686
 
5191
- // src/loop/thinking.ts
5192
- function isThinkingModeModel(model) {
5193
- if (model.includes("reasoner")) return true;
5194
- if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return true;
5195
- return false;
5196
- }
5197
- function thinkingModeForModel(model) {
5198
- if (model === "deepseek-chat") return "disabled";
5199
- if (model.includes("reasoner")) return "enabled";
5200
- if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return "enabled";
5201
- return void 0;
5202
- }
5203
- function stripHallucinatedToolMarkup(s) {
5204
- let out = s;
5205
- out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
5206
- out = out.replace(/<\|DSML\|function_calls>[\s\S]*?<\/?\|DSML\|function_calls>/g, "");
5207
- out = out.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "");
5208
- out = out.replace(/<|DSML|[\s\S]*$/g, "");
5209
- return out.trim();
5210
- }
5211
-
5212
- // src/loop/messages.ts
5213
- function buildAssistantMessage(content, toolCalls, producingModel, reasoningContent) {
5214
- const msg = { role: "assistant", content };
5215
- if (toolCalls.length > 0) msg.tool_calls = toolCalls;
5216
- if (isThinkingModeModel(producingModel) || reasoningContent && reasoningContent.length > 0) {
5217
- msg.reasoning_content = reasoningContent ?? "";
5218
- }
5219
- return msg;
5220
- }
5221
- function buildSyntheticAssistantMessage(content, fallbackModel) {
5222
- return buildAssistantMessage(content, [], fallbackModel, "");
5223
- }
5224
-
5225
5687
  // src/loop/force-summary.ts
5226
- var PAUSE_SUMMARY_MODEL = "deepseek-v4-flash";
5227
- var PAUSE_SUMMARY_EFFORT = "high";
5228
- async function* forceSummaryAfterIterLimit(ctx, opts = { reason: "budget" }) {
5688
+ async function* forceSummaryAfterIterLimit(ctx, opts) {
5229
5689
  try {
5230
5690
  yield { turn: ctx.turn, role: "status", content: t("summary.status") };
5231
5691
  const messages = ctx.buildMessages();
5232
5692
  messages.push({
5233
5693
  role: "user",
5234
- content: "I'm out of tool-call budget for this turn. Summarize in plain prose what you learned from the tool results above. Do NOT emit any tool calls, function-call markup, DSML invocations, or SEARCH/REPLACE edit blocks \u2014 they will be silently discarded. Just plain text."
5694
+ content: "The turn is being force-summarized (context guard or stuck-state). Summarize in plain prose what you learned from the tool results above. Do NOT emit any tool calls, function-call markup, DSML invocations, or SEARCH/REPLACE edit blocks \u2014 they will be silently discarded. Just plain text."
5235
5695
  });
5236
5696
  const summaryModel = "deepseek-v4-flash";
5237
5697
  const summaryEffort = "high";
@@ -5245,7 +5705,7 @@ async function* forceSummaryAfterIterLimit(ctx, opts = { reason: "budget" }) {
5245
5705
  const rawContent = resp.content?.trim() ?? "";
5246
5706
  const cleaned = stripHallucinatedToolMarkup(rawContent);
5247
5707
  const summary = cleaned || t("summary.hallucinatedFallback");
5248
- const reasonPrefix = reasonPrefixFor(opts.reason, ctx.maxToolIters);
5708
+ const reasonPrefix = reasonPrefixFor(opts.reason);
5249
5709
  const annotated = `${reasonPrefix}
5250
5710
 
5251
5711
  ${summary}`;
@@ -5260,7 +5720,7 @@ ${summary}`;
5260
5720
  };
5261
5721
  yield { turn: ctx.turn, role: "done", content: summary };
5262
5722
  } catch (err) {
5263
- const label = errorLabelFor(opts.reason, ctx.maxToolIters);
5723
+ const label = errorLabelFor(opts.reason);
5264
5724
  yield {
5265
5725
  turn: ctx.turn,
5266
5726
  role: "error",
@@ -5270,28 +5730,6 @@ ${summary}`;
5270
5730
  yield { turn: ctx.turn, role: "done", content: "" };
5271
5731
  }
5272
5732
  }
5273
- async function summarizePartialProgress(ctx) {
5274
- try {
5275
- const messages = ctx.buildMessages();
5276
- messages.push({
5277
- role: "user",
5278
- content: "You're being paused at a checkpoint, not stopped. In 3-6 sentences of plain prose, tell the parent agent: (1) what you accomplished so far, (2) what's still left, (3) any blockers or open questions. Be concrete \u2014 mention specific files / functions / tool results \u2014 so the parent can decide whether to resume you or take over. Do NOT emit any tool calls, function-call markup, DSML invocations, or SEARCH/REPLACE edit blocks \u2014 they will be silently discarded. Just plain text."
5279
- });
5280
- const resp = await ctx.client.chat({
5281
- model: PAUSE_SUMMARY_MODEL,
5282
- messages,
5283
- signal: ctx.signal,
5284
- thinking: thinkingModeForModel(PAUSE_SUMMARY_MODEL),
5285
- reasoningEffort: PAUSE_SUMMARY_EFFORT
5286
- });
5287
- const cleaned = stripHallucinatedToolMarkup(resp.content?.trim() ?? "");
5288
- if (!cleaned) return null;
5289
- const stats = ctx.recordStats(PAUSE_SUMMARY_MODEL, resp.usage ?? new Usage());
5290
- return { summary: cleaned, stats };
5291
- } catch {
5292
- return null;
5293
- }
5294
- }
5295
5733
 
5296
5734
  // src/loop/shrink.ts
5297
5735
  function looksLikeCompleteJson(s) {
@@ -5324,7 +5762,7 @@ function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
5324
5762
  if (msg.role !== "tool") return msg;
5325
5763
  const content = typeof msg.content === "string" ? msg.content : "";
5326
5764
  if (content.length <= maxTokens) return msg;
5327
- const beforeTokens = countTokens(content);
5765
+ const beforeTokens = countTokensBounded(content);
5328
5766
  if (beforeTokens <= maxTokens) return msg;
5329
5767
  const truncated = truncateForModelByTokens(content, maxTokens);
5330
5768
  const afterTokens = countTokens(truncated);
@@ -5758,11 +6196,16 @@ var StormBreaker = class {
5758
6196
  function repairTruncatedJson(input) {
5759
6197
  const notes = [];
5760
6198
  if (!input || !input.trim()) {
5761
- return { repaired: "{}", changed: input !== "{}", notes: ["empty input \u2192 {}"] };
6199
+ return {
6200
+ repaired: "{}",
6201
+ changed: input !== "{}",
6202
+ notes: ["empty input \u2192 {}"],
6203
+ fallback: false
6204
+ };
5762
6205
  }
5763
6206
  try {
5764
6207
  JSON.parse(input);
5765
- return { repaired: input, changed: false, notes: [] };
6208
+ return { repaired: input, changed: false, notes: [], fallback: false };
5766
6209
  } catch {
5767
6210
  }
5768
6211
  const stack = [];
@@ -5817,10 +6260,12 @@ function repairTruncatedJson(input) {
5817
6260
  }
5818
6261
  try {
5819
6262
  JSON.parse(s);
5820
- return { repaired: s, changed: true, notes };
6263
+ return { repaired: s, changed: s !== input, notes, fallback: false };
5821
6264
  } catch (err) {
6265
+ const preview = input.length <= 500 ? input : `${input.slice(0, 500)} \u2026[+${input.length - 500} chars]`;
5822
6266
  notes.push(`fallback to {}: ${err.message}`);
5823
- return { repaired: "{}", changed: true, notes };
6267
+ notes.push(`unrecoverable truncation \u2014 original args preview: ${preview}`);
6268
+ return { repaired: "{}", changed: true, notes, fallback: true };
5824
6269
  }
5825
6270
  }
5826
6271
 
@@ -5867,9 +6312,16 @@ var ToolCallRepair = class {
5867
6312
  const args = call.function?.arguments ?? "";
5868
6313
  const r = repairTruncatedJson(args);
5869
6314
  if (r.changed) {
5870
- call.function.arguments = r.repaired;
5871
- report.truncationsFixed++;
5872
- report.notes.push(...r.notes.map((n) => `[${call.function.name}] ${n}`));
6315
+ if (r.fallback) {
6316
+ report.truncationsFixed++;
6317
+ report.notes.push(
6318
+ ...r.notes.map((n) => `[${call.function?.name}] \u26A0\uFE0F TRUNCATION UNRECOVERABLE: ${n}`)
6319
+ );
6320
+ } else {
6321
+ call.function.arguments = r.repaired;
6322
+ report.truncationsFixed++;
6323
+ report.notes.push(...r.notes.map((n) => `[${call.function.name}] ${n}`));
6324
+ }
5873
6325
  }
5874
6326
  }
5875
6327
  const filtered = [];
@@ -5891,12 +6343,10 @@ function signature(call) {
5891
6343
 
5892
6344
  // src/loop.ts
5893
6345
  var ESCALATION_MODEL = "deepseek-v4-pro";
5894
- var PARENT_BUDGET_WARN_THRESHOLD = 5;
5895
6346
  var CacheFirstLoop = class {
5896
6347
  client;
5897
6348
  prefix;
5898
6349
  tools;
5899
- maxToolIters;
5900
6350
  log = new AppendOnlyLog();
5901
6351
  scratch = new VolatileScratch();
5902
6352
  stats = new SessionStats();
@@ -5911,7 +6361,6 @@ var CacheFirstLoop = class {
5911
6361
  /** One-shot 80% warning latch — cleared by setBudget so a bump re-arms at the new boundary. */
5912
6362
  _budgetWarned = false;
5913
6363
  sessionName;
5914
- onIterBudgetExhausted;
5915
6364
  hooks;
5916
6365
  hookCwd;
5917
6366
  /** PauseGate bridge — defaults to singleton, injectable for tests. */
@@ -5925,12 +6374,25 @@ var CacheFirstLoop = class {
5925
6374
  _turnAbort = new AbortController();
5926
6375
  /** Authoritative running-id set — UI cards consult this instead of trusting end-event delivery. Insert at dispatch entry, delete in finally. */
5927
6376
  _inflight = new InflightSet();
6377
+ /** Typeahead steer message set by the UI; step() consumes it at the next iter boundary. */
6378
+ _steer = null;
6379
+ /** Set true when a steer was consumed this turn; cleared on next step() entry. */
6380
+ _steerConsumed = false;
6381
+ /** UI calls this to inject a mid-turn steer message without aborting the current turn.
6382
+ * New text resets steerConsumed — a fresh steer hasn't been consumed yet. */
6383
+ steer(text) {
6384
+ this._steer = text;
6385
+ if (text !== null) this._steerConsumed = false;
6386
+ }
6387
+ /** True when a steer was consumed this turn (UI gate to avoid double-submit). */
6388
+ get steerConsumed() {
6389
+ return this._steerConsumed;
6390
+ }
5928
6391
  _proArmedForNextTurn = false;
5929
6392
  _escalateThisTurn = false;
5930
6393
  _turnFailures;
5931
6394
  _turnSelfCorrected = false;
5932
6395
  _foldedThisTurn = false;
5933
- _toolDispatchesThisStep = 0;
5934
6396
  context;
5935
6397
  /** Subscribe API so UI hooks can derive `running` from finally-guaranteed insertions. */
5936
6398
  get inflight() {
@@ -5950,8 +6412,6 @@ var CacheFirstLoop = class {
5950
6412
  this._turnFailures = new TurnFailureTracker(
5951
6413
  resolveFailureThreshold(opts.failureThreshold, FAILURE_ESCALATION_THRESHOLD)
5952
6414
  );
5953
- this.maxToolIters = opts.maxToolIters ?? 64;
5954
- this.onIterBudgetExhausted = opts.onIterBudgetExhausted ?? "summarize";
5955
6415
  this.hooks = opts.hooks ?? [];
5956
6416
  this.hookCwd = opts.hookCwd ?? process.cwd();
5957
6417
  this.confirmationGate = opts.confirmationGate ?? pauseGate;
@@ -5972,23 +6432,6 @@ var CacheFirstLoop = class {
5972
6432
  stormThreshold: parsePositiveIntEnv(process.env.REASONIX_STORM_THRESHOLD),
5973
6433
  stormWindow: parsePositiveIntEnv(process.env.REASONIX_STORM_WINDOW)
5974
6434
  });
5975
- if (!this.tools.hasResultAugmenter) {
5976
- this.tools.setResultAugmenter((_name, _args, result) => {
5977
- this._toolDispatchesThisStep++;
5978
- const remaining = this.maxToolIters - this._toolDispatchesThisStep;
5979
- if (remaining <= 0) {
5980
- return `${result}
5981
-
5982
- [budget: 0 of ${this.maxToolIters} tool calls left this turn \u2014 finalize NOW; the next iter forces a summary]`;
5983
- }
5984
- if (remaining <= PARENT_BUDGET_WARN_THRESHOLD) {
5985
- return `${result}
5986
-
5987
- [budget: ${remaining} of ${this.maxToolIters} tool calls left this turn \u2014 wrap up soon]`;
5988
- }
5989
- return result;
5990
- });
5991
- }
5992
6435
  this.sessionName = opts.session ?? null;
5993
6436
  if (this.sessionName) {
5994
6437
  const prior = loadSessionMessages(this.sessionName);
@@ -6074,6 +6517,10 @@ var CacheFirstLoop = class {
6074
6517
  }
6075
6518
  this.scratch.reset();
6076
6519
  this._inflight.clear();
6520
+ this.stats.reset();
6521
+ this._turn = 0;
6522
+ this._turnFailures.reset();
6523
+ this._budgetWarned = false;
6077
6524
  let systemRebuilt = false;
6078
6525
  if (this._rebuildSystem) {
6079
6526
  try {
@@ -6083,6 +6530,29 @@ var CacheFirstLoop = class {
6083
6530
  }
6084
6531
  return { dropped, archived, systemRebuilt };
6085
6532
  }
6533
+ /** `/cwd` follow-through — archives the previous session, drops in-memory state, repoints sessionName, and rebuilds the system prompt against whatever the rebuilder closure now resolves (the caller is expected to have already updated the root the closure reads). */
6534
+ switchWorkspace(opts) {
6535
+ const dropped = this.log.length;
6536
+ let archived = null;
6537
+ if (this.sessionName) {
6538
+ try {
6539
+ archived = archiveSession(this.sessionName);
6540
+ if (archived === null) rewriteSession(this.sessionName, []);
6541
+ } catch {
6542
+ }
6543
+ }
6544
+ this.log.compactInPlace([]);
6545
+ this.scratch.reset();
6546
+ this._inflight.clear();
6547
+ this.sessionName = opts.sessionName;
6548
+ if (this._rebuildSystem) {
6549
+ try {
6550
+ this.prefix.replaceSystem(this._rebuildSystem());
6551
+ } catch {
6552
+ }
6553
+ }
6554
+ return { dropped, archived };
6555
+ }
6086
6556
  configure(opts) {
6087
6557
  if (opts.model !== void 0) this.model = opts.model;
6088
6558
  if (opts.stream !== void 0) {
@@ -6238,6 +6708,7 @@ ${reason}`
6238
6708
  return userText;
6239
6709
  }
6240
6710
  async *step(userInput) {
6711
+ this._steerConsumed = false;
6241
6712
  if (this.budgetUsd !== null) {
6242
6713
  const spent = this.stats.totalCost;
6243
6714
  if (spent >= this.budgetUsd) {
@@ -6271,7 +6742,6 @@ ${reason}`
6271
6742
  this._turnSelfCorrected = false;
6272
6743
  this._escalateThisTurn = false;
6273
6744
  this._foldedThisTurn = false;
6274
- this._toolDispatchesThisStep = 0;
6275
6745
  let armedConsumed = false;
6276
6746
  if (this._proArmedForNextTurn) {
6277
6747
  this._escalateThisTurn = true;
@@ -6289,16 +6759,15 @@ ${reason}`
6289
6759
  content: t("loop.proArmed")
6290
6760
  };
6291
6761
  }
6292
- let pendingUser = userInput;
6762
+ this.appendAndPersist({ role: "user", content: userInput });
6763
+ let pendingUser = null;
6293
6764
  const toolSpecs = this.prefix.tools();
6294
- const warnAt = Math.max(1, Math.floor(this.maxToolIters * 0.7));
6295
- let warnedForIterBudget = false;
6296
- for (let iter = 0; iter < this.maxToolIters; iter++) {
6765
+ for (let iter = 0; ; iter++) {
6297
6766
  if (signal.aborted) {
6298
6767
  yield {
6299
6768
  turn: this._turn,
6300
6769
  role: "warning",
6301
- content: t("loop.abortedAtIter", { iter, cap: this.maxToolIters })
6770
+ content: t("loop.abortedAtIter", { iter })
6302
6771
  };
6303
6772
  const stoppedMsg = "[aborted by user (Esc) \u2014 no summary produced. Ask again or /retry when ready; prior tool output is still in the log.]";
6304
6773
  this.appendAndPersist(buildSyntheticAssistantMessage(stoppedMsg, this.model));
@@ -6319,15 +6788,20 @@ ${reason}`
6319
6788
  content: t("loop.toolUploadStatus")
6320
6789
  };
6321
6790
  }
6322
- if (!warnedForIterBudget && iter >= warnAt) {
6323
- warnedForIterBudget = true;
6791
+ let messages = this.buildMessages(pendingUser);
6792
+ if (this._steer !== null) {
6793
+ const steer = this._steer;
6794
+ this._steer = null;
6795
+ this._steerConsumed = true;
6796
+ this.appendAndPersist({ role: "user", content: steer });
6797
+ messages = this.buildMessages(pendingUser);
6798
+ pendingUser = null;
6324
6799
  yield {
6325
6800
  turn: this._turn,
6326
- role: "warning",
6327
- content: t("loop.toolBudgetWarning", { iter, cap: this.maxToolIters })
6801
+ role: "steer",
6802
+ content: steer
6328
6803
  };
6329
6804
  }
6330
- let messages = this.buildMessages(pendingUser);
6331
6805
  {
6332
6806
  const decision2 = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
6333
6807
  if (decision2.needsAction) {
@@ -6335,23 +6809,29 @@ ${reason}`
6335
6809
  yield {
6336
6810
  turn: this._turn,
6337
6811
  role: "status",
6338
- content: t("loop.preflightFoldStatus")
6812
+ content: t("loop.preflightTruncateStatus")
6339
6813
  };
6340
- const result = await this.context.fold(this.model);
6814
+ const result = this.context.mechanicalTruncate(this.model, {
6815
+ allowEmpty: pendingUser !== null
6816
+ });
6341
6817
  if (result.folded) {
6818
+ messages = this.buildMessages(pendingUser);
6819
+ const after = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
6820
+ const stillFull = after.needsAction;
6342
6821
  yield {
6343
- turn: this._turn,
6344
- role: "warning",
6345
- content: t("loop.preflightFolded", {
6346
- estimate: estimate.toLocaleString(),
6347
- ctxMax: ctxMax.toLocaleString(),
6348
- pct: Math.round(estimate / ctxMax * 100),
6349
- beforeMessages: result.beforeMessages,
6350
- afterMessages: result.afterMessages,
6351
- summaryChars: result.summaryChars
6352
- })
6822
+ turn: this._turn,
6823
+ role: "warning",
6824
+ content: t(
6825
+ stillFull ? "loop.preflightTruncatedStillFull" : "loop.preflightTruncated",
6826
+ {
6827
+ estimate: after.estimateTokens.toLocaleString(),
6828
+ ctxMax: after.ctxMax.toLocaleString(),
6829
+ pct: Math.round(after.estimateTokens / after.ctxMax * 100),
6830
+ beforeMessages: result.beforeMessages,
6831
+ afterMessages: result.afterMessages
6832
+ }
6833
+ )
6353
6834
  };
6354
- messages = this.buildMessages(pendingUser);
6355
6835
  } else {
6356
6836
  yield {
6357
6837
  turn: this._turn,
@@ -6508,10 +6988,6 @@ ${reason}`
6508
6988
  this.modelForCurrentCall(),
6509
6989
  usage ?? new Usage()
6510
6990
  );
6511
- if (pendingUser !== null) {
6512
- this.appendAndPersist({ role: "user", content: pendingUser });
6513
- pendingUser = null;
6514
- }
6515
6991
  this.scratch.reasoning = reasoningContent || null;
6516
6992
  const { calls: repairedCalls, report } = this.repair.process(
6517
6993
  toolCalls,
@@ -6705,20 +7181,6 @@ ${reason}`
6705
7181
  }
6706
7182
  }
6707
7183
  }
6708
- if (this.onIterBudgetExhausted === "pause") {
6709
- const partial = await summarizePartialProgress(this.summaryContext());
6710
- yield {
6711
- turn: this._turn,
6712
- role: "paused",
6713
- content: "",
6714
- sessionName: this.sessionName ?? void 0,
6715
- pausedAtIter: this.maxToolIters,
6716
- partialSummary: partial?.summary
6717
- };
6718
- yield { turn: this._turn, role: "done", content: "" };
6719
- return;
6720
- }
6721
- yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "budget" });
6722
7184
  }
6723
7185
  summaryContext() {
6724
7186
  return {
@@ -6727,8 +7189,7 @@ ${reason}`
6727
7189
  buildMessages: () => this.buildMessages(null),
6728
7190
  appendAndPersist: (m) => this.appendAndPersist(m),
6729
7191
  recordStats: (model, usage) => this.stats.record(this._turn, model, usage),
6730
- turn: this._turn,
6731
- maxToolIters: this.maxToolIters
7192
+ turn: this._turn
6732
7193
  };
6733
7194
  }
6734
7195
  async run(userInput, onEvent) {
@@ -6763,7 +7224,7 @@ function resolveFailureThreshold(raw, fallback) {
6763
7224
  // src/at-mentions.ts
6764
7225
  import { existsSync as existsSync4, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
6765
7226
  import { readdir, stat } from "fs/promises";
6766
- import { isAbsolute, join as join5, relative, resolve } from "path";
7227
+ import { isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
6767
7228
 
6768
7229
  // src/gitignore.ts
6769
7230
  import { readFileSync as readFileSync5 } from "fs";
@@ -6818,7 +7279,7 @@ function listFilesSync(root, opts = {}) {
6818
7279
  function listFilesWithStatsSync(root, opts = {}) {
6819
7280
  const maxResults = Math.max(1, opts.maxResults ?? 2e3);
6820
7281
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
6821
- const rootAbs = resolve(root);
7282
+ const rootAbs = resolve2(root);
6822
7283
  const respectGi = opts.respectGitignore !== false;
6823
7284
  const out = [];
6824
7285
  const walk2 = (dirAbs, dirRel, layers) => {
@@ -6882,7 +7343,7 @@ async function listFilesWithStatsAsync(root, opts = {}) {
6882
7343
  async function walkFilesStream(root, opts) {
6883
7344
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
6884
7345
  const respectGi = opts.respectGitignore !== false;
6885
- const rootAbs = resolve(root);
7346
+ const rootAbs = resolve2(root);
6886
7347
  const progressGap = Math.max(0, opts.progressIntervalMs ?? 100);
6887
7348
  let scanned = 0;
6888
7349
  let halted = false;
@@ -6960,10 +7421,10 @@ async function flushFiles(ents, dirAbs, dirRel, layers, emit) {
6960
7421
  async function listDirectory(root, relDir, opts = {}) {
6961
7422
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
6962
7423
  const respectGi = opts.respectGitignore !== false;
6963
- const rootAbs = resolve(root);
6964
- const dirAbs = resolve(rootAbs, relDir);
7424
+ const rootAbs = resolve2(root);
7425
+ const dirAbs = resolve2(rootAbs, relDir);
6965
7426
  const rel = relative(rootAbs, dirAbs);
6966
- if (rel.startsWith("..") || isAbsolute(rel)) return [];
7427
+ if (rel.startsWith("..") || isAbsolute2(rel)) return [];
6967
7428
  const layers = [];
6968
7429
  if (respectGi) {
6969
7430
  const segs = rel ? rel.split(/[\\/]/) : [];
@@ -7126,7 +7587,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
7126
7587
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
7127
7588
  const maxDirEntries = Math.max(1, opts.maxDirEntries ?? DEFAULT_AT_DIR_MAX_ENTRIES);
7128
7589
  const fs5 = opts.fs ?? defaultFs;
7129
- const root = resolve(rootDir);
7590
+ const root = resolve2(rootDir);
7130
7591
  const seen = /* @__PURE__ */ new Map();
7131
7592
  const expansions = [];
7132
7593
  const dirListings = /* @__PURE__ */ new Map();
@@ -7170,12 +7631,12 @@ ${blocks.join("\n\n")}`;
7170
7631
  return { text: augmented, expansions };
7171
7632
  }
7172
7633
  function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs5, dirListings) {
7173
- if (isAbsolute(rawPath)) {
7634
+ if (isAbsolute2(rawPath)) {
7174
7635
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
7175
7636
  }
7176
- const resolved = resolve(root, rawPath);
7637
+ const resolved = resolve2(root, rawPath);
7177
7638
  const rel = relative(root, resolved);
7178
- if (rel.startsWith("..") || isAbsolute(rel)) {
7639
+ if (rel.startsWith("..") || isAbsolute2(rel)) {
7179
7640
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
7180
7641
  }
7181
7642
  if (!fs5.exists(resolved)) {
@@ -7203,7 +7664,7 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs5, dirListings
7203
7664
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
7204
7665
  }
7205
7666
  function readSafe(root, rawPath, fs5) {
7206
- const resolved = resolve(root, rawPath);
7667
+ const resolved = resolve2(root, rawPath);
7207
7668
  try {
7208
7669
  return fs5.read(resolved);
7209
7670
  } catch {
@@ -7313,7 +7774,7 @@ import {
7313
7774
  writeFileSync as writeFileSync4
7314
7775
  } from "fs";
7315
7776
  import { homedir as homedir5 } from "os";
7316
- import { join as join8, resolve as resolve3 } from "path";
7777
+ import { join as join8, resolve as resolve4 } from "path";
7317
7778
 
7318
7779
  // src/frontmatter.ts
7319
7780
  var KEY_RE = /^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/;
@@ -7363,9 +7824,18 @@ function parseFrontmatter(raw) {
7363
7824
  }
7364
7825
 
7365
7826
  // src/skills.ts
7366
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
7827
+ import {
7828
+ constants,
7829
+ existsSync as existsSync6,
7830
+ mkdirSync as mkdirSync3,
7831
+ readFileSync as readFileSync8,
7832
+ readdirSync as readdirSync3,
7833
+ statSync as statSync4,
7834
+ writeFileSync as writeFileSync3
7835
+ } from "fs";
7836
+ import { accessSync } from "fs";
7367
7837
  import { homedir as homedir4 } from "os";
7368
- import { dirname as dirname4, join as join7, resolve as resolve2 } from "path";
7838
+ import { dirname as dirname4, isAbsolute as isAbsolute3, join as join7, resolve as resolve3 } from "path";
7369
7839
 
7370
7840
  // src/prompt-fragments.ts
7371
7841
  var TUI_FORMATTING_RULES = `Formatting (rendered in a TUI with a real markdown renderer):
@@ -7410,26 +7880,25 @@ function parseAllowedTools(raw) {
7410
7880
  const names = raw.split(",").map((s) => s.trim()).filter(Boolean);
7411
7881
  return names.length > 0 ? Object.freeze(names) : void 0;
7412
7882
  }
7413
- function parseMaxToolIters(raw) {
7414
- if (raw === void 0) return void 0;
7415
- const n = Number.parseInt(raw.trim(), 10);
7416
- if (!Number.isFinite(n) || n < 1) return void 0;
7417
- return n;
7418
- }
7419
7883
  var SkillStore = class {
7420
7884
  homeDir;
7421
7885
  projectRoot;
7886
+ customSkillPaths;
7422
7887
  disableBuiltins;
7423
7888
  constructor(opts = {}) {
7424
7889
  this.homeDir = opts.homeDir ?? homedir4();
7425
- this.projectRoot = opts.projectRoot ? resolve2(opts.projectRoot) : void 0;
7890
+ this.projectRoot = opts.projectRoot ? resolve3(opts.projectRoot) : void 0;
7891
+ const baseDir = this.projectRoot ?? process.cwd();
7892
+ this.customSkillPaths = dedupePaths(
7893
+ opts.customSkillPaths?.map((p) => resolveCustomSkillPath(p, baseDir, this.homeDir)) ?? []
7894
+ );
7426
7895
  this.disableBuiltins = opts.disableBuiltins === true;
7427
7896
  }
7428
7897
  /** True iff this store was configured with a project root. */
7429
7898
  hasProjectScope() {
7430
7899
  return this.projectRoot !== void 0;
7431
7900
  }
7432
- /** Project scope first so per-repo skill overrides a global with the same name. */
7901
+ /** Project scope first so per-repo skill overrides custom/global entries with the same name. */
7433
7902
  roots() {
7434
7903
  const out = [];
7435
7904
  if (this.projectRoot) {
@@ -7437,15 +7906,24 @@ var SkillStore = class {
7437
7906
  dir: join7(this.projectRoot, ".reasonix", SKILLS_DIRNAME),
7438
7907
  scope: "project"
7439
7908
  });
7909
+ out.push({
7910
+ dir: join7(this.projectRoot, ".agents", SKILLS_DIRNAME),
7911
+ scope: "project"
7912
+ });
7440
7913
  }
7914
+ for (const dir of this.customSkillPaths) out.push({ dir, scope: "custom" });
7441
7915
  out.push({ dir: join7(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
7442
- return out;
7916
+ out.push({ dir: join7(this.homeDir, ".agents", SKILLS_DIRNAME), scope: "global" });
7917
+ return out.map((root, priority) => ({ ...root, priority, status: skillPathStatus(root.dir) }));
7918
+ }
7919
+ customRoots() {
7920
+ return this.roots().filter((root) => root.scope === "custom");
7443
7921
  }
7444
- /** Higher-priority root wins on collision (project > global > builtin); sorted for stable prefix hash. */
7922
+ /** Higher-priority root wins on collision (project > custom > global > builtin); sorted for stable prefix hash. */
7445
7923
  list() {
7446
7924
  const byName = /* @__PURE__ */ new Map();
7447
- for (const { dir, scope } of this.roots()) {
7448
- if (!existsSync6(dir)) continue;
7925
+ for (const { dir, scope, status } of this.roots()) {
7926
+ if (status !== "ok") continue;
7449
7927
  let entries;
7450
7928
  try {
7451
7929
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -7497,8 +7975,8 @@ var SkillStore = class {
7497
7975
  /** Resolve one skill by name. Returns `null` if not found or malformed. */
7498
7976
  read(name) {
7499
7977
  if (!isValidSkillName(name)) return null;
7500
- for (const { dir, scope } of this.roots()) {
7501
- if (!existsSync6(dir)) continue;
7978
+ for (const { dir, scope, status } of this.roots()) {
7979
+ if (status !== "ok") continue;
7502
7980
  const dirCandidate = join7(dir, name, SKILL_FILE);
7503
7981
  if (existsSync6(dirCandidate) && statSync4(dirCandidate).isFile()) {
7504
7982
  return this.parse(dirCandidate, name, scope);
@@ -7546,11 +8024,38 @@ var SkillStore = class {
7546
8024
  path: path2,
7547
8025
  allowedTools: parseAllowedTools(data["allowed-tools"]),
7548
8026
  runAs: parseRunAs(data.runAs),
7549
- model: data.model?.startsWith("deepseek-") ? data.model : void 0,
7550
- maxToolIters: parseMaxToolIters(data["max-iters"])
8027
+ model: data.model?.startsWith("deepseek-") ? data.model : void 0
7551
8028
  };
7552
8029
  }
7553
8030
  };
8031
+ function dedupePaths(paths) {
8032
+ const out = [];
8033
+ const seen = /* @__PURE__ */ new Set();
8034
+ for (const path2 of paths) {
8035
+ const key = process.platform === "win32" ? path2.toLowerCase() : path2;
8036
+ if (seen.has(key)) continue;
8037
+ seen.add(key);
8038
+ out.push(path2);
8039
+ }
8040
+ return out;
8041
+ }
8042
+ function resolveCustomSkillPath(path2, baseDir, homeDir) {
8043
+ const trimmed = path2.trim();
8044
+ const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") || trimmed.startsWith("~\\") ? join7(homeDir, trimmed.slice(2)) : trimmed;
8045
+ return resolve3(isAbsolute3(expanded) ? expanded : join7(baseDir, expanded));
8046
+ }
8047
+ function skillPathStatus(dir) {
8048
+ try {
8049
+ const stat2 = statSync4(dir);
8050
+ if (!stat2.isDirectory()) return "not-directory";
8051
+ accessSync(dir, constants.R_OK);
8052
+ return "ok";
8053
+ } catch (err) {
8054
+ const code = err.code;
8055
+ if (code === "ENOENT") return "missing";
8056
+ return "unreadable";
8057
+ }
8058
+ }
7554
8059
  function parseRunAs(raw) {
7555
8060
  return raw?.trim() === "subagent" ? "subagent" : "inline";
7556
8061
  }
@@ -7568,7 +8073,6 @@ Tips:
7568
8073
  - Reference tools by name (run_command, edit_file, search_content, ...)
7569
8074
  - Add \`runAs: subagent\` to frontmatter to spawn an isolated subagent loop
7570
8075
  - Add \`allowed-tools: read_file, search_content\` to scope a subagent's tools
7571
- - Add \`max-iters: N\` to change the subagent's pause cadence (default 16). This isn't a budget \u2014 the parent resumes on pause, so N is how often the parent gets a checkpoint, not how much total work the subagent gets.
7572
8076
  `;
7573
8077
  }
7574
8078
  function skillIndexLine(s) {
@@ -7803,7 +8307,7 @@ function sanitizeMemoryName(raw) {
7803
8307
  return trimmed;
7804
8308
  }
7805
8309
  function projectHash(rootDir) {
7806
- const abs = resolve3(rootDir);
8310
+ const abs = resolve4(rootDir);
7807
8311
  return createHash2("sha1").update(abs).digest("hex").slice(0, 16);
7808
8312
  }
7809
8313
  function scopeDir(opts) {
@@ -7853,7 +8357,7 @@ var MemoryStore = class {
7853
8357
  projectRoot;
7854
8358
  constructor(opts = {}) {
7855
8359
  this.homeDir = opts.homeDir ?? join8(homedir5(), ".reasonix");
7856
- this.projectRoot = opts.projectRoot ? resolve3(opts.projectRoot) : void 0;
8360
+ this.projectRoot = opts.projectRoot ? resolve4(opts.projectRoot) : void 0;
7857
8361
  }
7858
8362
  /** Directory this store writes `scope` files into, creating it if needed. */
7859
8363
  dir(scope) {
@@ -8094,11 +8598,17 @@ function applyUserMemory(basePrompt, opts = {}) {
8094
8598
  }
8095
8599
  return parts.join("\n");
8096
8600
  }
8097
- function applyMemoryStack(basePrompt, rootDir) {
8601
+ function applyMemoryStack(basePrompt, rootDir, opts = {}) {
8602
+ const homeDir = opts.homeDir;
8603
+ const cfg = opts.cfg;
8098
8604
  const withProject = applyProjectMemory(basePrompt, rootDir);
8099
- const withGlobal = applyGlobalReasonixMemory(withProject);
8100
- const withMemory = applyUserMemory(withGlobal, { projectRoot: rootDir });
8101
- return applySkillsIndex(withMemory, { projectRoot: rootDir });
8605
+ const withGlobal = applyGlobalReasonixMemory(
8606
+ withProject,
8607
+ homeDir ? join8(homeDir, ".reasonix") : void 0
8608
+ );
8609
+ const withMemory = applyUserMemory(withGlobal, { projectRoot: rootDir, homeDir, cfg });
8610
+ const customSkillPaths = cfg?.skills?.paths ? resolveSkillPaths(cfg.skills.paths, rootDir) : loadResolvedSkillPaths(rootDir);
8611
+ return applySkillsIndex(withMemory, { projectRoot: rootDir, homeDir, customSkillPaths });
8102
8612
  }
8103
8613
 
8104
8614
  // src/tools/filesystem.ts
@@ -8106,6 +8616,51 @@ import { promises as fs4 } from "fs";
8106
8616
  import * as pathMod5 from "path";
8107
8617
  import picomatch3 from "picomatch";
8108
8618
 
8619
+ // src/memory/subdir.ts
8620
+ import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
8621
+ import { dirname as dirname5, join as join9, relative as relative2, resolve as resolve5 } from "path";
8622
+ function findSubdirMemoryAncestors(absPath, rootDir) {
8623
+ const root = resolve5(rootDir);
8624
+ const target = resolve5(absPath);
8625
+ const rel = relative2(root, target);
8626
+ if (!rel || rel.startsWith("..")) return [];
8627
+ const found = [];
8628
+ let cur = dirname5(target);
8629
+ while (cur !== root) {
8630
+ const r = relative2(root, cur);
8631
+ if (!r || r.startsWith("..")) break;
8632
+ for (const name of PROJECT_MEMORY_FILES) {
8633
+ const path2 = join9(cur, name);
8634
+ if (existsSync8(path2)) {
8635
+ found.push(path2);
8636
+ break;
8637
+ }
8638
+ }
8639
+ const parent = dirname5(cur);
8640
+ if (parent === cur) break;
8641
+ cur = parent;
8642
+ }
8643
+ return found;
8644
+ }
8645
+ function readSubdirMemoryContent(path2) {
8646
+ let raw;
8647
+ try {
8648
+ raw = readFileSync10(path2, "utf8");
8649
+ } catch {
8650
+ return null;
8651
+ }
8652
+ const trimmed = raw.trim();
8653
+ if (!trimmed) return null;
8654
+ if (trimmed.length <= PROJECT_MEMORY_MAX_CHARS) return trimmed;
8655
+ return `${trimmed.slice(0, PROJECT_MEMORY_MAX_CHARS)}
8656
+ \u2026 (truncated ${trimmed.length - PROJECT_MEMORY_MAX_CHARS} chars)`;
8657
+ }
8658
+ function formatSubdirMemorySection(displayPath, content) {
8659
+ return `[module memory: ${displayPath}]
8660
+
8661
+ ${content}`;
8662
+ }
8663
+
8109
8664
  // src/tools/fs/edit.ts
8110
8665
  import { promises as fs } from "fs";
8111
8666
  import * as pathMod from "path";
@@ -8336,6 +8891,18 @@ var RUST_DECL_RE = /^(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|str
8336
8891
  var RUST_IMPL_RE = /^(?:unsafe\s+)?impl(?:\s*<[^>]+>)?\s+(?:[^{]+\s+for\s+)?(\w+)/;
8337
8892
  var MD_HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
8338
8893
  var MD_FENCE_RE = /^```/;
8894
+ var PROTO_TOP_RE = /^(message|service|enum|extend)\s+(\w+)/;
8895
+ var PROTO_RPC_RE = /^\s+rpc\s+(\w+)/;
8896
+ var CN_NUM = "[\\d\u96F6\u4E00\u4E8C\u4E09\u56DB\u4E94\u516D\u4E03\u516B\u4E5D\u5341\u767E\u5343\u4E07\uFF10-\uFF19]+";
8897
+ var TXT_CHAPTER_PATTERNS = [
8898
+ new RegExp(`^\u7B2C${CN_NUM}[\u7AE0\u8282\u56DE].{0,80}$`),
8899
+ new RegExp(`^\u5377${CN_NUM}.{0,80}$`),
8900
+ /^(?:序章|楔子|番外篇?|前言|后记|尾声|引子)(?:[\s\u3000::、—\-.].{0,80})?$/,
8901
+ /^Chapter\s+(?:\d+|[IVXLCDMivxlcdm]+|[A-Za-z]+)\b.{0,80}$/,
8902
+ /^CHAPTER\s+.{1,80}$/,
8903
+ /^Part\s+(?:\d+|[IVXLCDMivxlcdm]+)\b.{0,80}$/,
8904
+ /^PART\s+.{1,80}$/
8905
+ ];
8339
8906
  var EXT_TO_LANG = {
8340
8907
  ".ts": "ts",
8341
8908
  ".tsx": "ts",
@@ -8351,7 +8918,10 @@ var EXT_TO_LANG = {
8351
8918
  ".rs": "rust",
8352
8919
  ".md": "md",
8353
8920
  ".markdown": "md",
8354
- ".mdx": "md"
8921
+ ".mdx": "md",
8922
+ ".proto": "proto",
8923
+ ".txt": "txt",
8924
+ ".text": "txt"
8355
8925
  };
8356
8926
  function extractOutline(filename, lines) {
8357
8927
  const ext = pathMod3.extname(filename).toLowerCase();
@@ -8368,6 +8938,10 @@ function extractOutline(filename, lines) {
8368
8938
  return extractRust(lines);
8369
8939
  case "md":
8370
8940
  return extractMarkdown(lines);
8941
+ case "proto":
8942
+ return extractProto(lines);
8943
+ case "txt":
8944
+ return extractText(lines);
8371
8945
  }
8372
8946
  }
8373
8947
  function extractTs(lines) {
@@ -8419,6 +8993,36 @@ function extractRust(lines) {
8419
8993
  }
8420
8994
  return out;
8421
8995
  }
8996
+ function extractProto(lines) {
8997
+ const out = [];
8998
+ for (let i = 0; i < lines.length; i++) {
8999
+ const line = lines[i];
9000
+ if (!line.startsWith(" ") && !line.startsWith(" ")) {
9001
+ const m = PROTO_TOP_RE.exec(line);
9002
+ if (m) {
9003
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
9004
+ continue;
9005
+ }
9006
+ }
9007
+ const rpc = PROTO_RPC_RE.exec(line);
9008
+ if (rpc) out.push({ line: i + 1, text: `rpc ${rpc[1]}` });
9009
+ }
9010
+ return out;
9011
+ }
9012
+ function extractText(lines) {
9013
+ const out = [];
9014
+ for (let i = 0; i < lines.length; i++) {
9015
+ const line = lines[i].trim();
9016
+ if (line.length === 0 || line.length > 100) continue;
9017
+ for (const re of TXT_CHAPTER_PATTERNS) {
9018
+ if (re.test(line)) {
9019
+ out.push({ line: i + 1, text: line });
9020
+ break;
9021
+ }
9022
+ }
9023
+ }
9024
+ return out;
9025
+ }
8422
9026
  function extractMarkdown(lines) {
8423
9027
  const out = [];
8424
9028
  let inFence = false;
@@ -8643,8 +9247,8 @@ async function searchContent(ctx, startAbs, args) {
8643
9247
  for (let i = realStart; i <= winEnd; i++) {
8644
9248
  const line = lines[i];
8645
9249
  const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
8646
- const sep2 = hitSet.has(i) ? ":" : "-";
8647
- if (!pushLine(`${rel}:${i + 1}${sep2} ${display}`)) return;
9250
+ const sep3 = hitSet.has(i) ? ":" : "-";
9251
+ if (!pushLine(`${rel}:${i + 1}${sep3} ${display}`)) return;
8648
9252
  }
8649
9253
  prevWindowEnd = winEnd;
8650
9254
  }
@@ -8666,17 +9270,25 @@ async function searchContent(ctx, startAbs, args) {
8666
9270
  }
8667
9271
 
8668
9272
  // src/tools/filesystem.ts
8669
- var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
9273
+ var DEFAULT_OUTLINE_THRESHOLD_BYTES = 512 * 1024;
8670
9274
  var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
8671
- var DEFAULT_AUTO_PREVIEW_LINES = 200;
8672
- var AUTO_PREVIEW_HEAD_LINES = 80;
8673
- var AUTO_PREVIEW_TAIL_LINES = 40;
8674
- var OUTLINE_MAX_ENTRIES2 = 30;
9275
+ var HARD_MAX_FILE_BYTES = 32 * 1024 * 1024;
9276
+ var OUTLINE_HEAD_LINES = 80;
8675
9277
  var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
8676
9278
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
8677
9279
  function displayRel4(rootDir, full) {
8678
9280
  return pathMod5.relative(rootDir, full).replaceAll("\\", "/");
8679
9281
  }
9282
+ function looksLikeAbsoluteSystemPath(raw) {
9283
+ if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
9284
+ return /^\/(?:home|Users|etc|var|opt|tmp|usr|mnt|Library|Volumes|proc|sys|dev|run|srv|media|Applications|System|root|boot|private)(?:[/\\]|$)/.test(
9285
+ raw
9286
+ );
9287
+ }
9288
+ function pathIsUnder(child, parent) {
9289
+ const rel = pathMod5.relative(parent, child);
9290
+ return rel === "" || !rel.startsWith("..") && !pathMod5.isAbsolute(rel);
9291
+ }
8680
9292
  var GLOB_METACHARS = /[*?{[]/;
8681
9293
  function compileNameFilter(filter) {
8682
9294
  if (!filter) return null;
@@ -8693,24 +9305,45 @@ function isLikelyBinaryByName(name) {
8693
9305
  if (dot < 0) return false;
8694
9306
  return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
8695
9307
  }
9308
+ function looksBinary(buf) {
9309
+ const end = Math.min(buf.length, 8192);
9310
+ for (let i = 0; i < end; i++) {
9311
+ if (buf[i] === 0) return true;
9312
+ }
9313
+ return false;
9314
+ }
9315
+ function formatBytes(n) {
9316
+ if (n < 1024) return `${n} B`;
9317
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
9318
+ if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MiB`;
9319
+ return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
9320
+ }
8696
9321
  function registerFilesystemTools(registry, opts) {
8697
9322
  const rootDir = pathMod5.resolve(opts.rootDir);
8698
9323
  const allowWriting = opts.allowWriting !== false;
8699
- const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
9324
+ const outlineThresholdBytes = opts.outlineThresholdBytes ?? DEFAULT_OUTLINE_THRESHOLD_BYTES;
8700
9325
  const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
8701
9326
  const normRoot = pathMod5.resolve(rootDir);
8702
9327
  const sessionApproved = /* @__PURE__ */ new Set();
8703
- const inflightGate = /* @__PURE__ */ new Map();
8704
- function pathIsUnder(child, parent) {
8705
- const rel = pathMod5.relative(parent, child);
8706
- return rel === "" || !rel.startsWith("..") && !pathMod5.isAbsolute(rel);
8707
- }
8708
- function looksLikeAbsoluteSystemPath(raw) {
8709
- if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
8710
- return /^\/(?:home|Users|etc|var|opt|tmp|usr|mnt|Library|Volumes|proc|sys|dev|run|srv|media|Applications|System|root|boot|private)(?:[/\\]|$)/.test(
8711
- raw
8712
- );
9328
+ const shownSubdirMemory = /* @__PURE__ */ new Set();
9329
+ function withSubdirMemory(absPath, body) {
9330
+ if (!memoryEnabled()) return body;
9331
+ const ancestors = findSubdirMemoryAncestors(absPath, rootDir);
9332
+ if (ancestors.length === 0) return body;
9333
+ const sections = [];
9334
+ for (const memPath of [...ancestors].reverse()) {
9335
+ if (shownSubdirMemory.has(memPath)) continue;
9336
+ const content = readSubdirMemoryContent(memPath);
9337
+ if (!content) continue;
9338
+ shownSubdirMemory.add(memPath);
9339
+ sections.push(formatSubdirMemorySection(displayRel4(rootDir, memPath), content));
9340
+ }
9341
+ if (sections.length === 0) return body;
9342
+ return `${sections.join("\n\n")}
9343
+
9344
+ ${body}`;
8713
9345
  }
9346
+ const inflightGate = /* @__PURE__ */ new Map();
8714
9347
  async function ensureOutsideSandboxAllowed(abs, intent, toolName, ctx) {
8715
9348
  for (const dir of loadProjectPathAllowed(rootDir)) {
8716
9349
  if (pathIsUnder(abs, dir)) return;
@@ -8775,11 +9408,11 @@ function registerFilesystemTools(registry, opts) {
8775
9408
  registry.register({
8776
9409
  name: "read_file",
8777
9410
  parallelSafe: true,
8778
- description: `Read a file under the sandbox root. To save context, PREFER to scope the read instead of pulling the whole file:
8779
- - head: N \u2192 first N lines (imports, public API, small configs)
8780
- - tail: N \u2192 last N lines (recently-added code, log tails)
8781
- - range: "A-B" \u2192 inclusive line range A..B, 1-indexed (e.g. "120-180" around an edit site)
8782
- When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_LINES} lines, the tool auto-returns a head+tail preview with an "N lines omitted" marker, plus a top-level symbol outline (TS/JS exports, Python def/class, Go func/type, Rust fn/struct/impl/trait, Markdown headings, with line numbers, capped at ${OUTLINE_MAX_ENTRIES2}) so you can pick a smart range without a follow-up grep. If you need the middle, re-call with a range. Prefer search_content to locate a symbol first only when the outline doesn't have what you want \u2014 one scoped read beats three full-file reads.`,
9411
+ description: `Read a file under the sandbox root. Default behaviour returns FULL CONTENT for files at or under ${Math.round(DEFAULT_OUTLINE_THRESHOLD_BYTES / 1024)} KiB \u2014 trust the prompt cache, don't pre-truncate. Optional scoping:
9412
+ - head: N \u2192 first N lines (cheap probe of imports / config head)
9413
+ - tail: N \u2192 last N lines (recent-tail of a log)
9414
+ - range: "A-B" \u2192 inclusive 1-indexed range (e.g. "120-180" around an edit site)
9415
+ Files OVER the threshold auto-switch to outline mode: file metadata + first ${OUTLINE_HEAD_LINES} lines + a top-level symbol outline (TS/JS exports, Python def/class, Go func/type, Rust fn/struct/impl/trait, Markdown headings, Protobuf message/service/rpc, plain-text chapter markers) + concrete next-step commands. No middle bytes \u2014 drill in with range / search_content. Files over ${Math.round(HARD_MAX_FILE_BYTES / (1024 * 1024))} MiB are refused entirely (use grep / range). Binary files are refused \u2014 use get_file_info if you only need stat.`,
8783
9416
  readOnly: true,
8784
9417
  stormExempt: true,
8785
9418
  parameters: {
@@ -8797,22 +9430,31 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
8797
9430
  },
8798
9431
  fn: async (args, ctx) => {
8799
9432
  const abs = await safePath(args.path, "read_file", ctx);
9433
+ const rel = displayRel4(rootDir, abs);
8800
9434
  const fh = await fs4.open(abs, "r");
8801
9435
  let raw;
9436
+ let sizeBytes;
8802
9437
  try {
8803
9438
  const stat2 = await fh.stat();
8804
9439
  if (stat2.isDirectory()) {
8805
9440
  throw new Error(`not a file: ${args.path} (it's a directory)`);
8806
9441
  }
9442
+ sizeBytes = stat2.size;
9443
+ if (sizeBytes > HARD_MAX_FILE_BYTES) {
9444
+ return [
9445
+ `[refused: ${rel} is ${formatBytes(sizeBytes)} (> ${formatBytes(HARD_MAX_FILE_BYTES)} hard ceiling) \u2014 too large to load]`,
9446
+ "Use one of:",
9447
+ ` - search_content path:"${rel}" pattern:"<your regex>" \u2014 grep within the file`,
9448
+ ` - read_file path:"${rel}" range:"A-B" \u2014 read a specific 1-indexed line range`,
9449
+ ` - read_file path:"${rel}" head:N / tail:N \u2014 read N lines at the start or end`
9450
+ ].join("\n");
9451
+ }
8807
9452
  raw = await fh.readFile();
8808
9453
  } finally {
8809
9454
  await fh.close();
8810
9455
  }
8811
- if (raw.length > maxReadBytes) {
8812
- const headBytes = raw.slice(0, maxReadBytes).toString("utf8");
8813
- return `${headBytes}
8814
-
8815
- [\u2026truncated ${raw.length - maxReadBytes} bytes \u2014 file is ${raw.length} B, cap ${maxReadBytes} B. Retry with head/tail/range for targeted view.]`;
9456
+ if (looksBinary(raw)) {
9457
+ return `[refused: ${rel} appears to be binary (${formatBytes(sizeBytes)}) \u2014 read_file returns text only. Use get_file_info for stat.]`;
8816
9458
  }
8817
9459
  const text = raw.toString("utf8");
8818
9460
  let lines = text.split(/\r?\n/);
@@ -8824,8 +9466,8 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
8824
9466
  const end = Math.min(totalLines, Math.max(start, rawEnd ?? totalLines));
8825
9467
  const slice = lines.slice(start - 1, end);
8826
9468
  const label = `[range ${start}-${end} of ${totalLines} lines]`;
8827
- return `${label}
8828
- ${slice.join("\n")}`;
9469
+ return withSubdirMemory(abs, `${label}
9470
+ ${slice.join("\n")}`);
8829
9471
  }
8830
9472
  if (typeof args.head === "number" && args.head > 0) {
8831
9473
  const count = Math.min(args.head, totalLines);
@@ -8833,7 +9475,7 @@ ${slice.join("\n")}`;
8833
9475
  const marker = count < totalLines ? `
8834
9476
 
8835
9477
  [\u2026head ${count} of ${totalLines} lines \u2014 call again with range / tail for more]` : "";
8836
- return slice.join("\n") + marker;
9478
+ return withSubdirMemory(abs, slice.join("\n") + marker);
8837
9479
  }
8838
9480
  if (typeof args.tail === "number" && args.tail > 0) {
8839
9481
  const count = Math.min(args.tail, totalLines);
@@ -8841,25 +9483,26 @@ ${slice.join("\n")}`;
8841
9483
  const marker = count < totalLines ? `[\u2026tail ${count} of ${totalLines} lines \u2014 call again with range / head for more]
8842
9484
 
8843
9485
  ` : "";
8844
- return marker + slice.join("\n");
9486
+ return withSubdirMemory(abs, marker + slice.join("\n"));
8845
9487
  }
8846
- if (totalLines <= DEFAULT_AUTO_PREVIEW_LINES) return lines.join("\n");
8847
- const head = lines.slice(0, AUTO_PREVIEW_HEAD_LINES).join("\n");
8848
- const tail = lines.slice(totalLines - AUTO_PREVIEW_TAIL_LINES).join("\n");
8849
- const omitted = totalLines - AUTO_PREVIEW_HEAD_LINES - AUTO_PREVIEW_TAIL_LINES;
9488
+ if (sizeBytes <= outlineThresholdBytes) return withSubdirMemory(abs, lines.join("\n"));
9489
+ const head = lines.slice(0, Math.min(OUTLINE_HEAD_LINES, totalLines)).join("\n");
8850
9490
  const outline = formatOutline(extractOutline(abs, lines));
8851
9491
  const parts = [
8852
- `[auto-preview: head ${AUTO_PREVIEW_HEAD_LINES} + tail ${AUTO_PREVIEW_TAIL_LINES} of ${totalLines} lines]`,
9492
+ `[large file: ${formatBytes(sizeBytes)}, ${totalLines} lines \u2014 outline mode (threshold ${formatBytes(outlineThresholdBytes)})]`,
9493
+ "",
9494
+ `[head ${Math.min(OUTLINE_HEAD_LINES, totalLines)} lines for orientation]`,
8853
9495
  head
8854
9496
  ];
8855
9497
  if (outline) parts.push("", outline);
8856
9498
  parts.push(
8857
- `
8858
- [\u2026 ${omitted} lines omitted \u2014 call read_file again with range:"A-B" (1-indexed) or head / tail to get the middle]
8859
- `,
8860
- tail
9499
+ "",
9500
+ "[to read more, call one of:",
9501
+ ` - read_file path:"${rel}" range:"A-B" \u2014 1-indexed line range`,
9502
+ ` - read_file path:"${rel}" head:N / tail:N \u2014 first/last N lines`,
9503
+ ` - search_content path:"${rel}" pattern:"..." \u2014 grep within this file]`
8861
9504
  );
8862
- return parts.join("\n");
9505
+ return withSubdirMemory(abs, parts.join("\n"));
8863
9506
  }
8864
9507
  });
8865
9508
  registry.register({
@@ -9900,7 +10543,7 @@ var VERIFY_SYSTEM = `You are a verify subagent. Narrow check \u2014 return YES /
9900
10543
  How to operate:
9901
10544
  - Read only what's needed to verify the specific claim. No exploration past the claim.
9902
10545
  - Use search_content / read_file to confirm the exact behavior, type, or call site in question.
9903
- - Cap at 6-8 tool calls. If you can't verify in that, return INCONCLUSIVE plus what's missing.
10546
+ - If a focused round of reads can't verify it, return INCONCLUSIVE plus what's missing \u2014 don't keep digging.
9904
10547
 
9905
10548
  Final answer:
9906
10549
  - Lead with VERIFIED / NOT VERIFIED / INCONCLUSIVE.
@@ -9911,8 +10554,8 @@ ${NEGATIVE_CLAIM_RULE}
9911
10554
 
9912
10555
  ${TUI_FORMATTING_RULES}`;
9913
10556
  var TYPES = {
9914
- explore: { system: EXPLORE_SYSTEM, maxToolIters: 20 },
9915
- verify: { system: VERIFY_SYSTEM, maxToolIters: 8 }
10557
+ explore: { system: EXPLORE_SYSTEM },
10558
+ verify: { system: VERIFY_SYSTEM }
9916
10559
  };
9917
10560
  var SUBAGENT_TYPE_NAMES = Object.freeze(
9918
10561
  Object.keys(TYPES)
@@ -9940,11 +10583,6 @@ ${NEGATIVE_CLAIM_RULE}
9940
10583
 
9941
10584
  ${TUI_FORMATTING_RULES}`;
9942
10585
  var DEFAULT_MAX_RESULT_CHARS2 = 8e3;
9943
- var DEFAULT_PAUSE_EVERY = 16;
9944
- var BUDGET_WARN_THRESHOLD = 3;
9945
- function budgetParagraph(maxToolIters) {
9946
- return `Tool budget: you have ${maxToolIters} tool call${maxToolIters === 1 ? "" : "s"} for this task. The cap is enforced from outside \u2014 the call after #${maxToolIters} is refused. Pace yourself: if you can't fully resolve the task within the budget, stop early and return what you have plus what's missing, rather than burning the budget on one branch.`;
9947
- }
9948
10586
  var DEFAULT_SUBAGENT_MODEL = "deepseek-v4-flash";
9949
10587
  var DEFAULT_SUBAGENT_EFFORT = "high";
9950
10588
  var SUBAGENT_TOOL_NAME = "spawn_subagent";
@@ -9963,7 +10601,6 @@ function subagentBudgetHint(spawnCount, totalTokens) {
9963
10601
  }
9964
10602
  async function spawnSubagent(opts) {
9965
10603
  const model = opts.model ?? DEFAULT_SUBAGENT_MODEL;
9966
- const maxToolIters = opts.maxToolIters ?? DEFAULT_PAUSE_EVERY;
9967
10604
  const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
9968
10605
  const sink = opts.sink;
9969
10606
  const skillName = opts.skillName;
@@ -10016,26 +10653,8 @@ async function spawnSubagent(opts) {
10016
10653
  new Set(opts.allowedTools),
10017
10654
  NEVER_INHERITED_TOOLS
10018
10655
  ) : forkRegistryExcluding(opts.parentRegistry, NEVER_INHERITED_TOOLS);
10019
- let dispatchCount = 0;
10020
- childTools.setResultAugmenter((_name, _args, result) => {
10021
- dispatchCount++;
10022
- const remaining = maxToolIters - dispatchCount;
10023
- if (remaining <= 0) {
10024
- return `${result}
10025
-
10026
- [budget: 0 of ${maxToolIters} tool calls left \u2014 finalize NOW; the next tool call will be refused]`;
10027
- }
10028
- if (remaining <= BUDGET_WARN_THRESHOLD) {
10029
- return `${result}
10030
-
10031
- [budget: ${remaining} of ${maxToolIters} tool call${remaining === 1 ? "" : "s"} left \u2014 wrap up soon]`;
10032
- }
10033
- return result;
10034
- });
10035
10656
  const childPrefix = new ImmutablePrefix({
10036
- system: `${opts.system}
10037
-
10038
- ${budgetParagraph(maxToolIters)}`,
10657
+ system: opts.system,
10039
10658
  toolSpecs: childTools.specs()
10040
10659
  });
10041
10660
  const childLoop = new CacheFirstLoop({
@@ -10047,11 +10666,9 @@ ${budgetParagraph(maxToolIters)}`,
10047
10666
  // task is already narrow by construction, and `high` cuts output
10048
10667
  // tokens substantially vs `max`.
10049
10668
  reasoningEffort: DEFAULT_SUBAGENT_EFFORT,
10050
- maxToolIters,
10051
10669
  hooks: [],
10052
10670
  stream: true,
10053
- session: sessionName,
10054
- onIterBudgetExhausted: "pause"
10671
+ session: sessionName
10055
10672
  });
10056
10673
  const onParentAbort = () => childLoop.abort();
10057
10674
  if (opts.parentSignal?.aborted) {
@@ -10063,13 +10680,9 @@ ${budgetParagraph(maxToolIters)}`,
10063
10680
  let errorMessage;
10064
10681
  let toolIter = 0;
10065
10682
  let summarisingEmitted = false;
10066
- let paused = false;
10067
- let partialSummary;
10068
- const taskForLoop = opts.resumeSession ? `[Resume: your tool-call budget has been refreshed with ${maxToolIters} new calls. Earlier "wrap up" / "finalize NOW" budget hints in this conversation referred to the previous window \u2014 they no longer apply. Continue the work you were given.]
10069
-
10070
- ${opts.task}` : opts.task;
10683
+ let forcedSummaryFired = false;
10071
10684
  try {
10072
- for await (const ev of childLoop.step(taskForLoop)) {
10685
+ for await (const ev of childLoop.step(opts.task)) {
10073
10686
  sink?.current?.({ kind: "inner", runId, task: taskPreview, skillName, model, inner: ev });
10074
10687
  if (ev.role === "tool") {
10075
10688
  toolIter++;
@@ -10099,7 +10712,12 @@ ${opts.task}` : opts.task;
10099
10712
  }
10100
10713
  if (ev.role === "assistant_final") {
10101
10714
  if (ev.forcedSummary) {
10102
- errorMessage = ev.content?.trim() || "subagent ended without producing an answer";
10715
+ if (opts.parentSignal?.aborted) {
10716
+ errorMessage = ev.content?.trim() || "subagent aborted before producing an answer";
10717
+ } else {
10718
+ final = ev.content ?? "";
10719
+ forcedSummaryFired = true;
10720
+ }
10103
10721
  } else {
10104
10722
  final = ev.content ?? "";
10105
10723
  }
@@ -10107,17 +10725,13 @@ ${opts.task}` : opts.task;
10107
10725
  if (ev.role === "error") {
10108
10726
  errorMessage = ev.error ?? "subagent error";
10109
10727
  }
10110
- if (ev.role === "paused") {
10111
- paused = true;
10112
- if (ev.partialSummary) partialSummary = ev.partialSummary;
10113
- }
10114
10728
  }
10115
10729
  } catch (err) {
10116
10730
  errorMessage = err.message;
10117
10731
  } finally {
10118
10732
  opts.parentSignal?.removeEventListener("abort", onParentAbort);
10119
10733
  }
10120
- if (!errorMessage && !final && !paused) {
10734
+ if (!errorMessage && !final) {
10121
10735
  errorMessage = opts.parentSignal?.aborted ? "subagent aborted before producing an answer" : "subagent ended without producing an answer";
10122
10736
  }
10123
10737
  const elapsedMs = Date.now() - startedAt;
@@ -10142,7 +10756,7 @@ ${opts.task}` : opts.task;
10142
10756
  usage
10143
10757
  });
10144
10758
  return {
10145
- success: !errorMessage,
10759
+ success: !errorMessage && !forcedSummaryFired,
10146
10760
  output: errorMessage ? "" : truncated,
10147
10761
  error: errorMessage,
10148
10762
  turns,
@@ -10152,9 +10766,7 @@ ${opts.task}` : opts.task;
10152
10766
  model,
10153
10767
  skillName,
10154
10768
  usage,
10155
- paused: paused || void 0,
10156
- pausedSession: paused ? sessionName : void 0,
10157
- partialSummary: paused ? partialSummary : void 0
10769
+ forcedSummary: forcedSummaryFired || void 0
10158
10770
  };
10159
10771
  }
10160
10772
  function aggregateChildUsage(loop) {
@@ -10169,16 +10781,16 @@ function aggregateChildUsage(loop) {
10169
10781
  return agg;
10170
10782
  }
10171
10783
  function formatSubagentResult(r) {
10172
- if (r.paused) {
10784
+ if (r.forcedSummary) {
10173
10785
  return JSON.stringify({
10174
10786
  success: false,
10175
- paused: true,
10176
- resume_session: r.pausedSession,
10787
+ partial: true,
10788
+ output: r.output,
10789
+ turns: r.turns,
10177
10790
  tool_iters: r.toolIters,
10178
10791
  elapsed_ms: r.elapsedMs,
10179
10792
  cost_usd: r.costUsd,
10180
- partial_summary: r.partialSummary,
10181
- note: `Subagent reached its pause-every interval (${r.toolIters} tool calls) without producing a final answer. Read partial_summary above to see what was done / left / blocked, then decide: resume by calling spawn_subagent again with resume_session="${r.pausedSession}" (the task arg becomes a continuation nudge \u2014 e.g. "finish what you started"), or accept the partial work and proceed with what you already know.`
10793
+ note: "Subagent was force-summarized (storm-breaker or context-guard fired). `output` carries the partial synthesis the model produced before being stopped \u2014 useful but not a complete answer. Decide whether to accept the partial, narrow the task and re-spawn, or fall back to direct tools."
10182
10794
  });
10183
10795
  }
10184
10796
  if (!r.success) {
@@ -10203,7 +10815,6 @@ function registerSubagentTool(parentRegistry, opts) {
10203
10815
  const baseSystem = opts.defaultSystem ?? SUBAGENT_BASE_SYSTEM;
10204
10816
  const defaultSystemBase = opts.projectRoot ? applyProjectMemory(baseSystem, opts.projectRoot) : baseSystem;
10205
10817
  const defaultModel = opts.defaultModel ?? DEFAULT_SUBAGENT_MODEL;
10206
- const maxToolIters = opts.maxToolIters ?? DEFAULT_PAUSE_EVERY;
10207
10818
  const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
10208
10819
  const sink = opts.sink;
10209
10820
  let sessionSpawnCount = 0;
@@ -10211,7 +10822,7 @@ function registerSubagentTool(parentRegistry, opts) {
10211
10822
  parentRegistry.register({
10212
10823
  name: SUBAGENT_TOOL_NAME,
10213
10824
  parallelSafe: true,
10214
- description: "Spawn an isolated subagent to handle a self-contained subtask in a fresh context, returning only its final answer. **Prefer direct tools.** Spawn primarily for parallel fan-out (2+ independent investigations issued in one tool batch) or when the work would otherwise need >10 file reads/searches whose trail you don't need to keep. Single greps, 1-3 file cross-references, and 'keep my context clean for one question' are NOT good reasons to spawn \u2014 direct tools are cheaper and let you reference the evidence later. Each fresh spawn pays a prefix-cache miss plus a full child loop. The subagent inherits your tools but runs in its own isolated message log; only the final assistant message comes back. **Pause/resume**: subagents yield back to you every `max_iters` tool calls. A paused result returns `{ paused: true, resume_session: \"...\" }` \u2014 you can either accept the partial state, or call spawn_subagent again with `resume_session` set to continue where it left off (cheap: the prior prefix is cached). Tasks expected to run much longer than 16 tool calls don't need a huge `max_iters` \u2014 just resume.",
10825
+ description: "Spawn an isolated subagent to handle a self-contained subtask in a fresh context, returning only its final answer. **Prefer direct tools.** Spawn primarily for parallel fan-out (2+ independent investigations issued in one tool batch) or when the work would otherwise need >10 file reads/searches whose trail you don't need to keep. Single greps, 1-3 file cross-references, and 'keep my context clean for one question' are NOT good reasons to spawn \u2014 direct tools are cheaper and let you reference the evidence later. Each fresh spawn pays a prefix-cache miss plus a full child loop. The subagent inherits your tools but runs in its own isolated message log; only the final assistant message comes back. The subagent runs to completion \u2014 same stops as top-level chat (token-context guard, storm breaker, parent Esc cascade).",
10215
10826
  parameters: {
10216
10827
  type: "object",
10217
10828
  properties: {
@@ -10228,18 +10839,14 @@ function registerSubagentTool(parentRegistry, opts) {
10228
10839
  enum: ["deepseek-v4-flash", "deepseek-v4-pro"],
10229
10840
  description: "Which DeepSeek model the subagent runs on. Default is 'deepseek-v4-flash' \u2014 cheap and fast, fine for explore/research-style subtasks. Override to 'deepseek-v4-pro' (~12\xD7 more expensive) when the subtask genuinely needs the stronger model: cross-file architecture, subtle bug hunts, anything where flash has empirically underperformed."
10230
10841
  },
10231
- max_iters: {
10232
- type: "integer",
10233
- description: "How many tool calls the subagent runs before pausing back to you for a checkpoint. Default 16. This is checkpoint cadence, not a budget \u2014 work continues across pauses via `resume_session`. Pick what feels natural for the granularity of decisions you want to make: small (4-8) when you want frequent control, larger (32-64) when the subagent should mostly run autonomously."
10234
- },
10235
10842
  resume_session: {
10236
10843
  type: "string",
10237
- description: "Provide the `resume_session` value returned by a previous paused spawn to continue that subagent. When set, prior messages are loaded from disk and the original system prompt is reused (cache-friendly). `task` becomes a continuation nudge."
10844
+ description: "Provide a previous subagent's session name to continue it. When set, prior messages are loaded from disk and the original system prompt is reused (cache-friendly). `task` becomes a continuation nudge."
10238
10845
  },
10239
10846
  type: {
10240
10847
  type: "string",
10241
10848
  enum: [...SUBAGENT_TYPE_NAMES],
10242
- description: "Optional persona shaping the system prompt and default iter budget. 'explore' = wide-net read-only investigation (20-iter budget, returns a distilled answer). 'verify' = narrow yes/no check with evidence (8-iter budget). Omit when supplying your own 'system' prompt or when the default generic persona fits. Caller-supplied 'system' / 'max_iters' override the type's defaults."
10849
+ description: "Optional persona shaping the system prompt. 'explore' = wide-net read-only investigation, returns a distilled answer. 'verify' = narrow yes/no check with evidence. Omit when supplying your own 'system' or when the default generic persona fits."
10243
10850
  }
10244
10851
  },
10245
10852
  required: ["task"]
@@ -10256,7 +10863,6 @@ function registerSubagentTool(parentRegistry, opts) {
10256
10863
  const system = typeof args.system === "string" && args.system.trim().length > 0 ? args.system.trim() : typeSpec?.system ?? `${defaultSystemBase}
10257
10864
 
10258
10865
  ${escalationContract(model)}`;
10259
- const callerIters = parseMaxIters(args.max_iters);
10260
10866
  const resumeSession = typeof args.resume_session === "string" && args.resume_session.trim().length > 0 ? args.resume_session.trim() : void 0;
10261
10867
  const result = await spawnSubagent({
10262
10868
  client: opts.client,
@@ -10264,7 +10870,6 @@ ${escalationContract(model)}`;
10264
10870
  system,
10265
10871
  task,
10266
10872
  model,
10267
- maxToolIters: callerIters ?? typeSpec?.maxToolIters ?? maxToolIters,
10268
10873
  maxResultChars,
10269
10874
  sink,
10270
10875
  parentSignal: ctx?.signal,
@@ -10272,6 +10877,12 @@ ${escalationContract(model)}`;
10272
10877
  });
10273
10878
  sessionSpawnCount++;
10274
10879
  sessionSpawnTokens += result.usage.totalTokens;
10880
+ if (opts.onSpawnComplete) {
10881
+ try {
10882
+ opts.onSpawnComplete(result);
10883
+ } catch {
10884
+ }
10885
+ }
10275
10886
  const formatted = formatSubagentResult(result);
10276
10887
  const hint = subagentBudgetHint(sessionSpawnCount, sessionSpawnTokens);
10277
10888
  return hint ? `${formatted}
@@ -10280,11 +10891,6 @@ ${hint}` : formatted;
10280
10891
  });
10281
10892
  return parentRegistry;
10282
10893
  }
10283
- function parseMaxIters(raw) {
10284
- if (typeof raw !== "number" || !Number.isFinite(raw)) return void 0;
10285
- const n = Math.floor(raw);
10286
- return n >= 1 ? n : void 0;
10287
- }
10288
10894
  function forkRegistryExcluding(parent, exclude) {
10289
10895
  const child = new ToolRegistry();
10290
10896
  for (const spec of parent.specs()) {
@@ -10311,8 +10917,100 @@ function forkRegistryWithAllowList(parent, allow, alsoExclude) {
10311
10917
  return child;
10312
10918
  }
10313
10919
 
10920
+ // src/telemetry/subagent-distillation.ts
10921
+ function computeSpawnDistillation(result) {
10922
+ const outputTokens = countTokensBounded(result.output);
10923
+ const completionTokens = result.usage.completionTokens;
10924
+ const savingsTokens = Math.max(0, completionTokens - outputTokens);
10925
+ const compressionRatio = completionTokens > 0 ? outputTokens / completionTokens : 1;
10926
+ return {
10927
+ completionTokens,
10928
+ outputTokens,
10929
+ savingsTokens,
10930
+ compressionRatio,
10931
+ hasOutput: result.output.trim().length > 0,
10932
+ costUsd: result.costUsd
10933
+ };
10934
+ }
10935
+ function summarizeSubagentSession(spawns) {
10936
+ const spawnCount = spawns.length;
10937
+ if (spawnCount === 0) {
10938
+ return {
10939
+ spawnCount: 0,
10940
+ usefulSpawnCount: 0,
10941
+ successRate: 0,
10942
+ totalCompletionTokens: 0,
10943
+ totalOutputTokens: 0,
10944
+ totalSavingsTokens: 0,
10945
+ aggregateCompressionRatio: 1,
10946
+ totalCostUsd: 0
10947
+ };
10948
+ }
10949
+ let usefulSpawnCount = 0;
10950
+ let totalCompletionTokens = 0;
10951
+ let totalOutputTokens = 0;
10952
+ let totalSavingsTokens = 0;
10953
+ let totalCostUsd = 0;
10954
+ for (const s of spawns) {
10955
+ if (s.hasOutput) usefulSpawnCount++;
10956
+ totalCompletionTokens += s.completionTokens;
10957
+ totalOutputTokens += s.outputTokens;
10958
+ totalSavingsTokens += s.savingsTokens;
10959
+ totalCostUsd += s.costUsd;
10960
+ }
10961
+ const aggregateCompressionRatio = totalCompletionTokens > 0 ? totalOutputTokens / totalCompletionTokens : 1;
10962
+ return {
10963
+ spawnCount,
10964
+ usefulSpawnCount,
10965
+ successRate: usefulSpawnCount / spawnCount,
10966
+ totalCompletionTokens,
10967
+ totalOutputTokens,
10968
+ totalSavingsTokens,
10969
+ aggregateCompressionRatio,
10970
+ totalCostUsd
10971
+ };
10972
+ }
10973
+ var DEFAULT_SPAWN_STORM_THRESHOLD = 3;
10974
+ function countSpawnStorms(spawnsByTurn, threshold = DEFAULT_SPAWN_STORM_THRESHOLD) {
10975
+ let storms = 0;
10976
+ for (const turn of spawnsByTurn) {
10977
+ if (turn.length >= threshold) storms++;
10978
+ }
10979
+ return storms;
10980
+ }
10981
+ var SubagentTelemetry = class {
10982
+ _spawns = [];
10983
+ _byTurn = [];
10984
+ _currentTurn = 0;
10985
+ /** Bound for ergonomic use as a callback. */
10986
+ record = (result) => {
10987
+ const d = computeSpawnDistillation(result);
10988
+ this._spawns.push(d);
10989
+ while (this._byTurn.length <= this._currentTurn) this._byTurn.push([]);
10990
+ this._byTurn[this._currentTurn].push(d);
10991
+ return d;
10992
+ };
10993
+ /** Mark the start of a new parent turn so subsequent records group into a new bucket — call from the parent loop when its turn counter advances. */
10994
+ startTurn(turn) {
10995
+ if (turn < 0) return;
10996
+ this._currentTurn = turn;
10997
+ }
10998
+ get spawns() {
10999
+ return this._spawns;
11000
+ }
11001
+ get spawnsByTurn() {
11002
+ return this._byTurn;
11003
+ }
11004
+ get summary() {
11005
+ return summarizeSubagentSession(this._spawns);
11006
+ }
11007
+ stormCount(threshold = DEFAULT_SPAWN_STORM_THRESHOLD) {
11008
+ return countSpawnStorms(this._byTurn, threshold);
11009
+ }
11010
+ };
11011
+
10314
11012
  // src/tools/shell.ts
10315
- import * as pathMod9 from "path";
11013
+ import * as pathMod10 from "path";
10316
11014
 
10317
11015
  // src/tools/jobs.ts
10318
11016
  import { spawn as spawn2 } from "child_process";
@@ -10561,16 +11259,16 @@ ${job.output.slice(start)}`;
10561
11259
  let wakeOutput = null;
10562
11260
  if (waitFor === "output-or-exit") {
10563
11261
  racers.push(
10564
- new Promise((resolve10) => {
10565
- wakeOutput = resolve10;
10566
- job.outputWaiters.add(resolve10);
11262
+ new Promise((resolve13) => {
11263
+ wakeOutput = resolve13;
11264
+ job.outputWaiters.add(resolve13);
10567
11265
  })
10568
11266
  );
10569
11267
  }
10570
11268
  let timer = null;
10571
11269
  racers.push(
10572
- new Promise((resolve10) => {
10573
- timer = setTimeout(resolve10, timeoutMs);
11270
+ new Promise((resolve13) => {
11271
+ timer = setTimeout(resolve13, timeoutMs);
10574
11272
  })
10575
11273
  );
10576
11274
  await Promise.race(racers);
@@ -10679,8 +11377,8 @@ function latestOutputSince(before, after) {
10679
11377
 
10680
11378
  // src/tools/shell/exec.ts
10681
11379
  import { spawn as spawn4, spawnSync } from "child_process";
10682
- import { existsSync as existsSync8, statSync as statSync5 } from "fs";
10683
- import * as pathMod8 from "path";
11380
+ import { existsSync as existsSync9, statSync as statSync5 } from "fs";
11381
+ import * as pathMod9 from "path";
10684
11382
 
10685
11383
  // src/tools/shell-chain.ts
10686
11384
  import { spawn as spawn3 } from "child_process";
@@ -11013,6 +11711,10 @@ async function runPipeGroup(segments, opts) {
11013
11711
  cwd: opts.cwd,
11014
11712
  shell: false,
11015
11713
  windowsHide: true,
11714
+ // POSIX: detach so the child becomes its own process-group leader,
11715
+ // allowing killProcessTree's neg-pid kill to terminate the whole
11716
+ // pipe chain subtree instead of just the direct child.
11717
+ detached: process.platform !== "win32",
11016
11718
  env,
11017
11719
  stdio: [stdinSpec, stdoutSpec, stderrSpec],
11018
11720
  ...spawnOverrides
@@ -11063,9 +11765,9 @@ async function runPipeGroup(segments, opts) {
11063
11765
  }
11064
11766
  const exits = await Promise.all(
11065
11767
  children.map(
11066
- (c) => new Promise((resolve10) => {
11067
- c.once("error", () => resolve10(null));
11068
- c.once("close", (code) => resolve10(code));
11768
+ (c) => new Promise((resolve13) => {
11769
+ c.once("error", () => resolve13(null));
11770
+ c.once("close", (code) => resolve13(code));
11069
11771
  })
11070
11772
  )
11071
11773
  );
@@ -11109,6 +11811,8 @@ var OutputBuffer = class {
11109
11811
  };
11110
11812
 
11111
11813
  // src/tools/shell/parse.ts
11814
+ import { homedir as homedir6 } from "os";
11815
+ import * as pathMod8 from "path";
11112
11816
  var BUILTIN_ALLOWLIST = [
11113
11817
  // Repo inspection
11114
11818
  "git status",
@@ -11285,7 +11989,65 @@ function tailHasRisky(tail, risky) {
11285
11989
  }
11286
11990
  return false;
11287
11991
  }
11288
- function isAllowed(cmd, extra = []) {
11992
+ var DEFAULT_SENSITIVE_PREFIXES = [
11993
+ "~/.ssh",
11994
+ "~/.aws",
11995
+ "~/.gnupg",
11996
+ "~/.kube",
11997
+ "/etc/shadow",
11998
+ "/etc/sudoers"
11999
+ ];
12000
+ var DEFAULT_SENSITIVE_PATTERNS = [
12001
+ "*.env",
12002
+ "*.env.*",
12003
+ "*.key",
12004
+ "*.pem",
12005
+ "id_rsa*",
12006
+ "id_ed25519*",
12007
+ "*credentials*",
12008
+ "*secret*"
12009
+ ];
12010
+ function resolveSensitivePath(token, projectRoot) {
12011
+ if (!token || token.startsWith("-") || token.includes("://") || token.startsWith("$"))
12012
+ return null;
12013
+ let expanded = token;
12014
+ if (expanded.startsWith("~")) {
12015
+ expanded = pathMod8.join(homedir6(), expanded.slice(1));
12016
+ }
12017
+ return pathMod8.resolve(projectRoot, expanded);
12018
+ }
12019
+ function expandPrefix(prefix) {
12020
+ if (prefix.startsWith("~")) return pathMod8.join(homedir6(), prefix.slice(1));
12021
+ return pathMod8.resolve(prefix);
12022
+ }
12023
+ function pathStartsWithPrefix(normalized, prefix) {
12024
+ return normalized === prefix || normalized.startsWith(`${prefix}${pathMod8.sep}`);
12025
+ }
12026
+ function matchesGlob(name, pattern) {
12027
+ const regex = new RegExp(
12028
+ `^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`,
12029
+ "i"
12030
+ );
12031
+ return regex.test(name);
12032
+ }
12033
+ function hasSensitivePathArgs(argv, projectRoot, extraPrefixes = [], extraPatterns = []) {
12034
+ const prefixes = [...DEFAULT_SENSITIVE_PREFIXES, ...extraPrefixes].map(expandPrefix);
12035
+ const patterns = [...DEFAULT_SENSITIVE_PATTERNS, ...extraPatterns];
12036
+ for (const token of argv) {
12037
+ const resolved = resolveSensitivePath(token, projectRoot);
12038
+ if (!resolved) continue;
12039
+ const normalized = pathMod8.normalize(resolved);
12040
+ for (const pfx of prefixes) {
12041
+ if (pathStartsWithPrefix(normalized, pfx)) return true;
12042
+ }
12043
+ const base = pathMod8.basename(normalized);
12044
+ for (const pat of patterns) {
12045
+ if (matchesGlob(base, pat)) return true;
12046
+ }
12047
+ }
12048
+ return false;
12049
+ }
12050
+ function isAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
11289
12051
  let argv;
11290
12052
  try {
11291
12053
  argv = tokenizeCommand(cmd);
@@ -11307,19 +12069,26 @@ function isAllowed(cmd, extra = []) {
11307
12069
  if (!match) continue;
11308
12070
  const risky = RISKY_ARGS[prefix];
11309
12071
  if (risky && tailHasRisky(argv.slice(prefixTokens.length), risky)) return false;
12072
+ if (projectRoot && hasSensitivePathArgs(
12073
+ argv,
12074
+ projectRoot,
12075
+ sensitivePathConfig?.prefixes,
12076
+ sensitivePathConfig?.patterns
12077
+ ))
12078
+ return false;
11310
12079
  return true;
11311
12080
  }
11312
12081
  return false;
11313
12082
  }
11314
- function isCommandAllowed(cmd, extra = []) {
12083
+ function isCommandAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
11315
12084
  let chain;
11316
12085
  try {
11317
12086
  chain = parseCommandChain(cmd);
11318
12087
  } catch {
11319
12088
  return false;
11320
12089
  }
11321
- if (chain === null) return isAllowed(cmd, extra);
11322
- return chainAllowed(chain, (seg) => isAllowed(seg, extra));
12090
+ if (chain === null) return isAllowed(cmd, extra, projectRoot, sensitivePathConfig);
12091
+ return chainAllowed(chain, (seg) => isAllowed(seg, extra, projectRoot, sensitivePathConfig));
11323
12092
  }
11324
12093
 
11325
12094
  // src/tools/shell/exec.ts
@@ -11366,8 +12135,15 @@ async function runCommand(cmd, opts) {
11366
12135
  const spawnOpts = {
11367
12136
  cwd: opts.cwd,
11368
12137
  shell: false,
11369
- // no shell-expansion — see header comment
11370
12138
  windowsHide: true,
12139
+ // POSIX: detach so the child becomes its own process-group leader.
12140
+ // Required for `process.kill(-pid, …)` in killProcessTree to
12141
+ // terminate the whole subtree (child + grandchildren) instead of
12142
+ // only the leader — without this grandchildren like npm→node→esbuild
12143
+ // become orphaned.
12144
+ // Windows: detached would spawn a new console window; leave the
12145
+ // default and use taskkill /T for tree termination (see killProcessTree).
12146
+ detached: process.platform !== "win32",
11371
12147
  // PYTHONIOENCODING + PYTHONUTF8 force any spawned Python child
11372
12148
  // (run_command running `python script.py`, etc.) to emit UTF-8
11373
12149
  // on stdout/stderr. Without this, Chinese-Windows defaults
@@ -11380,7 +12156,7 @@ async function runCommand(cmd, opts) {
11380
12156
  };
11381
12157
  const { bin, args, spawnOverrides } = prepareSpawn(argv, { env: normalizedEnv });
11382
12158
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
11383
- return await new Promise((resolve10, reject) => {
12159
+ return await new Promise((resolve13, reject) => {
11384
12160
  let child;
11385
12161
  try {
11386
12162
  child = spawn4(bin, args, effectiveSpawnOpts);
@@ -11434,7 +12210,7 @@ async function runCommand(cmd, opts) {
11434
12210
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
11435
12211
 
11436
12212
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
11437
- resolve10({ exitCode: code, output, timedOut });
12213
+ resolve13({ exitCode: code, output, timedOut });
11438
12214
  });
11439
12215
  });
11440
12216
  }
@@ -11456,16 +12232,16 @@ function resolveExecutable(cmd, opts = {}) {
11456
12232
  const platform = opts.platform ?? process.platform;
11457
12233
  if (platform !== "win32") return cmd;
11458
12234
  if (!cmd) return cmd;
11459
- if (cmd.includes("/") || cmd.includes("\\") || pathMod8.isAbsolute(cmd)) return cmd;
11460
- if (pathMod8.extname(cmd)) return cmd;
12235
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod9.isAbsolute(cmd)) return cmd;
12236
+ if (pathMod9.extname(cmd)) return cmd;
11461
12237
  const env = opts.env ?? process.env;
11462
12238
  const pathExt = (getEnvCaseInsensitive(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
11463
- const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod8.delimiter);
12239
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod9.delimiter);
11464
12240
  const pathDirs = (getEnvCaseInsensitive(env, "PATH") ?? "").split(delimiter2).filter(Boolean);
11465
12241
  const isFile = opts.isFile ?? defaultIsFile;
11466
12242
  for (const dir of pathDirs) {
11467
12243
  for (const ext of pathExt) {
11468
- const full = pathMod8.win32.join(dir, cmd + ext);
12244
+ const full = pathMod9.win32.join(dir, cmd + ext);
11469
12245
  if (isFile(full)) return full;
11470
12246
  }
11471
12247
  }
@@ -11519,7 +12295,7 @@ function mergeWindowsPathLike(values, delimiter2) {
11519
12295
  }
11520
12296
  function defaultIsFile(full) {
11521
12297
  try {
11522
- return existsSync8(full) && statSync5(full).isFile();
12298
+ return existsSync9(full) && statSync5(full).isFile();
11523
12299
  } catch {
11524
12300
  return false;
11525
12301
  }
@@ -11581,8 +12357,8 @@ function withUtf8Codepage(cmdline) {
11581
12357
  function isBareWindowsName(s) {
11582
12358
  if (!s) return false;
11583
12359
  if (s.includes("/") || s.includes("\\")) return false;
11584
- if (pathMod8.isAbsolute(s)) return false;
11585
- if (pathMod8.extname(s)) return false;
12360
+ if (pathMod9.isAbsolute(s)) return false;
12361
+ if (pathMod9.extname(s)) return false;
11586
12362
  return true;
11587
12363
  }
11588
12364
  function quoteForCmdExe(arg) {
@@ -11603,7 +12379,7 @@ var NeedsConfirmationError = class extends Error {
11603
12379
  }
11604
12380
  };
11605
12381
  function registerShellTools(registry, opts) {
11606
- const rootDir = pathMod9.resolve(opts.rootDir);
12382
+ const rootDir = pathMod10.resolve(opts.rootDir);
11607
12383
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
11608
12384
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
11609
12385
  const jobs = opts.jobs ?? new JobRegistry();
@@ -11623,7 +12399,7 @@ function registerShellTools(registry, opts) {
11623
12399
  if (isAllowAll()) return true;
11624
12400
  const cmd = typeof args?.command === "string" ? args.command.trim() : "";
11625
12401
  if (!cmd) return false;
11626
- return isCommandAllowed(cmd, getExtraAllowed());
12402
+ return isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths);
11627
12403
  },
11628
12404
  parameters: {
11629
12405
  type: "object",
@@ -11643,7 +12419,7 @@ function registerShellTools(registry, opts) {
11643
12419
  const cmd = args.command.trim();
11644
12420
  if (!cmd) throw new Error("run_command: empty command");
11645
12421
  const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
11646
- if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed())) {
12422
+ if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths)) {
11647
12423
  const gate = ctx?.confirmationGate ?? pauseGate;
11648
12424
  const choice = await gate.ask({
11649
12425
  kind: "run_command",
@@ -11669,7 +12445,7 @@ function registerShellTools(registry, opts) {
11669
12445
  });
11670
12446
  registry.register({
11671
12447
  name: "run_background",
11672
- description: "Spawn a long-running process and detach. Waits up to `waitSec` for startup or a readiness signal ('Local:', 'listening on', 'compiled successfully'), then returns the job id + startup preview. Tail logs with `job_output`, block on completion with `wait_for_job`, kill with `stop_job`, list with `list_jobs`.\n\nSingle process only \u2014 chains / redirects / `cd` work as in run_command, but a typical invocation is one binary. Use the binary's own --cwd / --prefix flag for subdirectories. Vite gotcha: npm's `--prefix` only finds package.json; vite's server root still uses process cwd \u2014 pass `vite <project-dir>` instead.\n\nUSE THIS \u2014 not run_command \u2014 for:\n- Dev servers / watchers: npm/yarn/pnpm dev, uvicorn / flask run, cargo watch, tsc --watch, webpack serve, anything with dev/serve/watch in the name.\n- One-shot long jobs: curl / wget large downloads, `huggingface-cli download`, multi-GB `pip install` / `npm install`, big `cargo build` / `docker build`. Start with `run_background`, then call `wait_for_job` once (default `waitFor: 'exit'`, timeoutMs up to 300_000) \u2014 the harness blocks server-side so a 5-minute download costs ONE tool call, not 30 polls.",
12448
+ description: "Spawn a long-running process and detach. Waits up to `waitSec` for startup or a readiness signal ('Local:', 'listening on', 'compiled successfully'), then returns the job id + startup preview. Tail logs with `job_output`, block on completion with `wait_for_job`, kill with `stop_job`, list with `list_jobs`.\n\nSingle process only \u2014 no chains / redirects. For subdirectories use the `cwd` parameter (workspace-relative or absolute, must stay inside the workspace root); do NOT write `cd X && cmd`, that gets rejected.\n\nUSE THIS \u2014 not run_command \u2014 for:\n- Dev servers / watchers: npm/yarn/pnpm dev, uvicorn / flask run, cargo watch, tsc --watch, webpack serve, anything with dev/serve/watch in the name.\n- One-shot long jobs: curl / wget large downloads, `huggingface-cli download`, multi-GB `pip install` / `npm install`, big `cargo build` / `docker build`. Start with `run_background`, then call `wait_for_job` once (default `waitFor: 'exit'`, timeoutMs up to 300_000) \u2014 the harness blocks server-side so a 5-minute download costs ONE tool call, not 30 polls.",
11673
12449
  parameters: {
11674
12450
  type: "object",
11675
12451
  properties: {
@@ -11677,6 +12453,10 @@ function registerShellTools(registry, opts) {
11677
12453
  type: "string",
11678
12454
  description: "Full command line. Same quoting rules as run_command (no pipes / redirects / chaining)."
11679
12455
  },
12456
+ cwd: {
12457
+ type: "string",
12458
+ description: "Working directory for the spawn. Workspace-relative or absolute. Defaults to the workspace root. Must resolve inside the workspace \u2014 paths escaping the root are rejected."
12459
+ },
11680
12460
  waitSec: {
11681
12461
  type: "integer",
11682
12462
  description: "Max seconds to wait for startup before returning. 0..30, default 3. A ready-signal match short-circuits this."
@@ -11687,11 +12467,12 @@ function registerShellTools(registry, opts) {
11687
12467
  fn: async (args, ctx) => {
11688
12468
  const cmd = args.command.trim();
11689
12469
  if (!cmd) throw new Error("run_background: empty command");
11690
- if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed())) {
12470
+ const cwd = resolveCwdInsideRoot(rootDir, args.cwd);
12471
+ if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths)) {
11691
12472
  const gate = ctx?.confirmationGate ?? pauseGate;
11692
12473
  const choice = await gate.ask({
11693
12474
  kind: "run_background",
11694
- payload: { command: cmd, cwd: rootDir, waitSec: args.waitSec }
12475
+ payload: { command: cmd, cwd, waitSec: args.waitSec }
11695
12476
  });
11696
12477
  if (choice.type === "deny") {
11697
12478
  throw new Error(
@@ -11703,10 +12484,11 @@ function registerShellTools(registry, opts) {
11703
12484
  }
11704
12485
  }
11705
12486
  const result = await jobs.start(cmd, {
11706
- cwd: rootDir,
12487
+ cwd,
11707
12488
  waitSec: args.waitSec,
11708
12489
  signal: ctx?.signal
11709
12490
  });
12491
+ opts.onJobsChanged?.();
11710
12492
  return formatJobStart(result);
11711
12493
  }
11712
12494
  });
@@ -11768,6 +12550,7 @@ function registerShellTools(registry, opts) {
11768
12550
  waitFor: args.waitFor
11769
12551
  });
11770
12552
  if (!out) return `job ${args.jobId}: not found (use list_jobs)`;
12553
+ if (out.exited) opts.onJobsChanged?.();
11771
12554
  return {
11772
12555
  jobId: args.jobId,
11773
12556
  exited: out.exited,
@@ -11788,6 +12571,7 @@ function registerShellTools(registry, opts) {
11788
12571
  },
11789
12572
  fn: async (args) => {
11790
12573
  const rec = await jobs.stop(args.jobId);
12574
+ opts.onJobsChanged?.();
11791
12575
  if (!rec) return `job ${args.jobId}: not found`;
11792
12576
  return formatJobStop(rec);
11793
12577
  }
@@ -11807,6 +12591,18 @@ function registerShellTools(registry, opts) {
11807
12591
  });
11808
12592
  return registry;
11809
12593
  }
12594
+ function resolveCwdInsideRoot(rootDir, raw) {
12595
+ const root = pathMod10.resolve(rootDir);
12596
+ if (!raw || !raw.trim()) return root;
12597
+ const resolved = pathMod10.resolve(root, raw);
12598
+ const rel = pathMod10.relative(root, resolved);
12599
+ if (rel.startsWith("..") || pathMod10.isAbsolute(rel)) {
12600
+ throw new Error(
12601
+ `run_background: cwd "${raw}" resolves outside the workspace root (${root}). Pass a workspace-relative path.`
12602
+ );
12603
+ }
12604
+ return resolved;
12605
+ }
11810
12606
  function formatJobStart(r) {
11811
12607
  const header = r.stillRunning ? `[job ${r.jobId} started \xB7 pid ${r.pid ?? "?"} \xB7 ${r.readyMatched ? "READY signal matched" : "running (no ready signal yet)"}]` : r.exitCode !== null ? `[job ${r.jobId} exited during startup \xB7 exit ${r.exitCode}]` : `[job ${r.jobId} failed to start]`;
11812
12608
  return r.preview ? `${header}
@@ -11855,6 +12651,7 @@ var DEFAULT_TOPK = 5;
11855
12651
  var FETCH_MAX_BYTES = 10 * 1024 * 1024;
11856
12652
  var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
11857
12653
  var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
12654
+ var METASO_ENDPOINT = "https://metaso.cn/api/v1";
11858
12655
  function searchStatusError(status) {
11859
12656
  if (status === 429) return t("webErrors.rateLimit429");
11860
12657
  if (status === 403) return t("webErrors.forbidden403");
@@ -11868,6 +12665,9 @@ function fetchStatusError(status, url) {
11868
12665
  return t("webErrors.fetchStatus", { status, url });
11869
12666
  }
11870
12667
  async function webSearch(query, opts = {}) {
12668
+ if (opts.engine === "metaso") {
12669
+ return searchMetaso(query, opts);
12670
+ }
11871
12671
  if (opts.engine === "searxng") {
11872
12672
  return searchSearxng(query, opts);
11873
12673
  }
@@ -11943,6 +12743,68 @@ async function searchSearxng(query, opts = {}) {
11943
12743
  }
11944
12744
  return results;
11945
12745
  }
12746
+ async function searchMetaso(query, opts = {}) {
12747
+ const topK = Math.max(1, Math.min(100, opts.topK ?? DEFAULT_TOPK));
12748
+ const apiKey = loadMetasoApiKey();
12749
+ let resp;
12750
+ try {
12751
+ resp = await fetch(`${METASO_ENDPOINT}/search`, {
12752
+ method: "POST",
12753
+ headers: {
12754
+ "Content-Type": "application/json",
12755
+ Accept: "application/json",
12756
+ Authorization: `Bearer ${apiKey}`
12757
+ },
12758
+ body: JSON.stringify({
12759
+ q: query,
12760
+ scope: "webpage",
12761
+ size: topK
12762
+ }),
12763
+ signal: opts.signal
12764
+ });
12765
+ } catch (err) {
12766
+ if (err instanceof TypeError && err.message.includes("fetch")) {
12767
+ throw new Error(t("webErrors.cannotReach", { endpoint: METASO_ENDPOINT }));
12768
+ }
12769
+ throw err;
12770
+ }
12771
+ const raw = await resp.text();
12772
+ let data;
12773
+ try {
12774
+ data = JSON.parse(raw);
12775
+ } catch {
12776
+ throw new Error(t("webErrors.metasoParseError", { status: resp.status }));
12777
+ }
12778
+ if (!resp.ok) {
12779
+ if (resp.status === 401 || resp.status === 403) {
12780
+ throw new Error(t("webErrors.metasoUnauthorized"));
12781
+ }
12782
+ if (resp.status === 429) {
12783
+ throw new Error(t("webErrors.metasoRateLimit"));
12784
+ }
12785
+ throw new Error(t("webErrors.metasoServerError", { status: resp.status }));
12786
+ }
12787
+ if (data.code === 3003) {
12788
+ throw new Error(t("webErrors.metasoDailyLimit"));
12789
+ }
12790
+ if (data.code === 2005) {
12791
+ throw new Error(t("webErrors.metasoUnauthorized"));
12792
+ }
12793
+ if (data.code && data.code !== 0) {
12794
+ throw new Error(
12795
+ t("webErrors.metasoApiError", { code: data.code, message: data.message ?? "" })
12796
+ );
12797
+ }
12798
+ const webpages = data.webpages ?? [];
12799
+ if (webpages.length === 0) {
12800
+ return [];
12801
+ }
12802
+ return webpages.slice(0, topK).map((wp) => ({
12803
+ title: wp.title,
12804
+ url: wp.link,
12805
+ snippet: wp.snippet ?? wp.summary ?? ""
12806
+ }));
12807
+ }
11946
12808
  function parseSearxngHtmlResults(html) {
11947
12809
  const root = parseHtml(html);
11948
12810
  const results = [];
@@ -12161,7 +13023,7 @@ function registerWebTools(registry, opts = {}) {
12161
13023
  const maxFetchChars = opts.maxFetchChars ?? DEFAULT_FETCH_MAX_CHARS;
12162
13024
  registry.register({
12163
13025
  name: "web_search",
12164
- description: "Search the public web. Returns ranked results with title, url, and snippet. Call this when the answer's correctness depends on current state \u2014 anything that changes over time (events, prices, releases, status of a thing in the real world). Composing such answers from training memory invents stale numbers; search first, then ground the answer in the results. For evergreen / definitional questions you don't need this. To change the backend, use /web-search-engine mojeek|searxng.",
13026
+ description: "Search the public web. Returns ranked results with title, url, and snippet. Call this when the answer's correctness depends on current state \u2014 anything that changes over time (events, prices, releases, status of a thing in the real world). Composing such answers from training memory invents stale numbers; search first, then ground the answer in the results. For evergreen / definitional questions you don't need this. To change the backend, use /search-engine mojeek|searxng|metaso.",
12165
13027
  readOnly: true,
12166
13028
  parallelSafe: true,
12167
13029
  parameters: {
@@ -12226,12 +13088,12 @@ ${i + 1}. ${r.title}`);
12226
13088
  }
12227
13089
 
12228
13090
  // src/env.ts
12229
- import { readFileSync as readFileSync10 } from "fs";
12230
- import { resolve as resolve8 } from "path";
13091
+ import { readFileSync as readFileSync11 } from "fs";
13092
+ import { resolve as resolve11 } from "path";
12231
13093
  function loadDotenv(path2 = ".env") {
12232
13094
  let raw;
12233
13095
  try {
12234
- raw = readFileSync10(resolve8(process.cwd(), path2), "utf8");
13096
+ raw = readFileSync11(resolve11(process.cwd(), path2), "utf8");
12235
13097
  } catch {
12236
13098
  return;
12237
13099
  }
@@ -12250,7 +13112,7 @@ function loadDotenv(path2 = ".env") {
12250
13112
  }
12251
13113
 
12252
13114
  // src/transcript/log.ts
12253
- import { createWriteStream, readFileSync as readFileSync11 } from "fs";
13115
+ import { createWriteStream, readFileSync as readFileSync12 } from "fs";
12254
13116
  function recordFromLoopEvent(ev, extra) {
12255
13117
  const rec = {
12256
13118
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -12293,7 +13155,7 @@ function openTranscriptFile(path2, meta) {
12293
13155
  return stream;
12294
13156
  }
12295
13157
  function readTranscript(path2) {
12296
- const raw = readFileSync11(path2, "utf8");
13158
+ const raw = readFileSync12(path2, "utf8");
12297
13159
  return parseTranscript(raw);
12298
13160
  }
12299
13161
  function parseTranscript(raw) {
@@ -12680,25 +13542,25 @@ function truncate(s, n) {
12680
13542
  }
12681
13543
 
12682
13544
  // src/version.ts
12683
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
12684
- import { homedir as homedir6 } from "os";
12685
- import { dirname as dirname6, join as join12 } from "path";
13545
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "fs";
13546
+ import { homedir as homedir7 } from "os";
13547
+ import { dirname as dirname7, join as join14 } from "path";
12686
13548
  import { fileURLToPath as fileURLToPath2 } from "url";
12687
13549
  var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
12688
13550
  var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
12689
13551
  var LATEST_FETCH_TIMEOUT_MS = 2e3;
12690
13552
  function readPackageVersion() {
12691
13553
  try {
12692
- let dir = dirname6(fileURLToPath2(import.meta.url));
13554
+ let dir = dirname7(fileURLToPath2(import.meta.url));
12693
13555
  for (let i = 0; i < 6; i++) {
12694
- const p = join12(dir, "package.json");
12695
- if (existsSync9(p)) {
12696
- const pkg = JSON.parse(readFileSync12(p, "utf8"));
13556
+ const p = join14(dir, "package.json");
13557
+ if (existsSync10(p)) {
13558
+ const pkg = JSON.parse(readFileSync13(p, "utf8"));
12697
13559
  if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
12698
13560
  return pkg.version;
12699
13561
  }
12700
13562
  }
12701
- const parent = dirname6(dir);
13563
+ const parent = dirname7(dir);
12702
13564
  if (parent === dir) break;
12703
13565
  dir = parent;
12704
13566
  }
@@ -12708,11 +13570,11 @@ function readPackageVersion() {
12708
13570
  }
12709
13571
  var VERSION = readPackageVersion();
12710
13572
  function cachePath(homeDirOverride) {
12711
- return join12(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
13573
+ return join14(homeDirOverride ?? homedir7(), ".reasonix", "version-cache.json");
12712
13574
  }
12713
13575
  function readCache(homeDirOverride) {
12714
13576
  try {
12715
- const raw = readFileSync12(cachePath(homeDirOverride), "utf8");
13577
+ const raw = readFileSync13(cachePath(homeDirOverride), "utf8");
12716
13578
  const parsed = JSON.parse(raw);
12717
13579
  if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
12718
13580
  return parsed;
@@ -12724,7 +13586,7 @@ function readCache(homeDirOverride) {
12724
13586
  function writeCache(entry, homeDirOverride) {
12725
13587
  try {
12726
13588
  const p = cachePath(homeDirOverride);
12727
- mkdirSync5(dirname6(p), { recursive: true });
13589
+ mkdirSync5(dirname7(p), { recursive: true });
12728
13590
  writeFileSync5(p, JSON.stringify(entry), "utf8");
12729
13591
  } catch {
12730
13592
  }
@@ -12936,7 +13798,7 @@ var McpClient = class {
12936
13798
  const id = this.nextId++;
12937
13799
  const frame = { jsonrpc: "2.0", id, method, params };
12938
13800
  let abortHandler = null;
12939
- const promise = new Promise((resolve10, reject) => {
13801
+ const promise = new Promise((resolve13, reject) => {
12940
13802
  const timeout = setTimeout(() => {
12941
13803
  this.pending.delete(id);
12942
13804
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -12945,7 +13807,7 @@ var McpClient = class {
12945
13807
  );
12946
13808
  }, this.requestTimeoutMs);
12947
13809
  this.pending.set(id, {
12948
- resolve: resolve10,
13810
+ resolve: resolve13,
12949
13811
  reject,
12950
13812
  timeout
12951
13813
  });
@@ -13075,12 +13937,12 @@ var StdioTransport = class {
13075
13937
  }
13076
13938
  async send(message) {
13077
13939
  if (this.closed) throw new Error("MCP transport is closed");
13078
- return new Promise((resolve10, reject) => {
13940
+ return new Promise((resolve13, reject) => {
13079
13941
  const line = `${JSON.stringify(message)}
13080
13942
  `;
13081
13943
  this.child.stdin.write(line, "utf8", (err) => {
13082
13944
  if (err) reject(err);
13083
- else resolve10();
13945
+ else resolve13();
13084
13946
  });
13085
13947
  });
13086
13948
  }
@@ -13091,8 +13953,8 @@ var StdioTransport = class {
13091
13953
  continue;
13092
13954
  }
13093
13955
  if (this.closed) return;
13094
- const next = await new Promise((resolve10) => {
13095
- this.waiters.push(resolve10);
13956
+ const next = await new Promise((resolve13) => {
13957
+ this.waiters.push(resolve13);
13096
13958
  });
13097
13959
  if (next === null) return;
13098
13960
  yield next;
@@ -13165,8 +14027,8 @@ var SseTransport = class {
13165
14027
  constructor(opts) {
13166
14028
  this.url = opts.url;
13167
14029
  this.headers = opts.headers ?? {};
13168
- this.endpointReady = new Promise((resolve10, reject) => {
13169
- this.resolveEndpoint = resolve10;
14030
+ this.endpointReady = new Promise((resolve13, reject) => {
14031
+ this.resolveEndpoint = resolve13;
13170
14032
  this.rejectEndpoint = reject;
13171
14033
  });
13172
14034
  this.endpointReady.catch(() => void 0);
@@ -13193,8 +14055,8 @@ var SseTransport = class {
13193
14055
  continue;
13194
14056
  }
13195
14057
  if (this.closed) return;
13196
- const next = await new Promise((resolve10) => {
13197
- this.waiters.push(resolve10);
14058
+ const next = await new Promise((resolve13) => {
14059
+ this.waiters.push(resolve13);
13198
14060
  });
13199
14061
  if (next === null) return;
13200
14062
  yield next;
@@ -13380,8 +14242,8 @@ var StreamableHttpTransport = class {
13380
14242
  continue;
13381
14243
  }
13382
14244
  if (this.closed) return;
13383
- const next = await new Promise((resolve10) => {
13384
- this.waiters.push(resolve10);
14245
+ const next = await new Promise((resolve13) => {
14246
+ this.waiters.push(resolve13);
13385
14247
  });
13386
14248
  if (next === null) return;
13387
14249
  yield next;
@@ -13439,85 +14301,6 @@ var StreamableHttpTransport = class {
13439
14301
  }
13440
14302
  };
13441
14303
 
13442
- // src/mcp/shell-split.ts
13443
- function shellSplit(input) {
13444
- const tokens = [];
13445
- let cur = "";
13446
- let quote = null;
13447
- let i = 0;
13448
- const s = input;
13449
- while (i < s.length) {
13450
- const ch = s[i];
13451
- if (quote) {
13452
- if (ch === quote) {
13453
- quote = null;
13454
- i++;
13455
- continue;
13456
- }
13457
- if (ch === "\\" && quote === '"' && i + 1 < s.length) {
13458
- cur += s[i + 1];
13459
- i += 2;
13460
- continue;
13461
- }
13462
- cur += ch;
13463
- i++;
13464
- continue;
13465
- }
13466
- if (ch === '"' || ch === "'") {
13467
- quote = ch;
13468
- i++;
13469
- continue;
13470
- }
13471
- if (ch === " " || ch === " ") {
13472
- if (cur.length > 0) {
13473
- tokens.push(cur);
13474
- cur = "";
13475
- }
13476
- i++;
13477
- continue;
13478
- }
13479
- cur += ch;
13480
- i++;
13481
- }
13482
- if (quote) {
13483
- throw new Error(
13484
- `shellSplit: unterminated ${quote === '"' ? "double" : "single"} quote in input`
13485
- );
13486
- }
13487
- if (cur.length > 0) tokens.push(cur);
13488
- return tokens;
13489
- }
13490
-
13491
- // src/mcp/spec.ts
13492
- var NAME_PREFIX = /^([a-zA-Z_][a-zA-Z0-9_-]*)=(.*)$/;
13493
- var HTTP_URL = /^https?:\/\//i;
13494
- var STREAMABLE_PREFIX = /^streamable\+(https?:\/\/.+)$/i;
13495
- function parseMcpSpec(input) {
13496
- const trimmed = input.trim();
13497
- if (!trimmed) {
13498
- throw new Error("empty MCP spec");
13499
- }
13500
- const nameMatch = NAME_PREFIX.exec(trimmed);
13501
- const name = nameMatch ? nameMatch[1] : null;
13502
- const body = (nameMatch ? nameMatch[2] : trimmed).trim();
13503
- if (!body) {
13504
- throw new Error(`MCP spec has name but no command: ${input}`);
13505
- }
13506
- const streamMatch = STREAMABLE_PREFIX.exec(body);
13507
- if (streamMatch) {
13508
- return { transport: "streamable-http", name, url: streamMatch[1] };
13509
- }
13510
- if (HTTP_URL.test(body)) {
13511
- return { transport: "sse", name, url: body };
13512
- }
13513
- const argv = shellSplit(body);
13514
- if (argv.length === 0) {
13515
- throw new Error(`MCP spec has name but no command: ${input}`);
13516
- }
13517
- const [command, ...args] = argv;
13518
- return { transport: "stdio", name, command, args };
13519
- }
13520
-
13521
14304
  // src/mcp/inspect.ts
13522
14305
  async function inspectMcpServer(client) {
13523
14306
  const t0 = Date.now();
@@ -13553,18 +14336,18 @@ async function trySection(load) {
13553
14336
  // src/code/edit-blocks.ts
13554
14337
  import {
13555
14338
  closeSync as closeSync2,
13556
- existsSync as existsSync10,
14339
+ existsSync as existsSync11,
13557
14340
  fstatSync,
13558
14341
  ftruncateSync,
13559
14342
  mkdirSync as mkdirSync6,
13560
14343
  openSync as openSync2,
13561
- readFileSync as readFileSync13,
14344
+ readFileSync as readFileSync14,
13562
14345
  readSync,
13563
14346
  unlinkSync as unlinkSync3,
13564
14347
  writeFileSync as writeFileSync6,
13565
14348
  writeSync
13566
14349
  } from "fs";
13567
- import { dirname as dirname7, resolve as resolve9 } from "path";
14350
+ import { dirname as dirname8, resolve as resolve12 } from "path";
13568
14351
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
13569
14352
  function parseEditBlocks(text) {
13570
14353
  const out = [];
@@ -13582,9 +14365,9 @@ function parseEditBlocks(text) {
13582
14365
  return out;
13583
14366
  }
13584
14367
  function applyEditBlock(block, rootDir) {
13585
- const absRoot = resolve9(rootDir);
13586
- const absTarget = resolve9(absRoot, block.path);
13587
- if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
14368
+ const absRoot = resolve12(rootDir);
14369
+ const absTarget = resolve12(absRoot, block.path);
14370
+ if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep2()}`)) {
13588
14371
  return {
13589
14372
  path: block.path,
13590
14373
  status: "path-escape",
@@ -13594,7 +14377,7 @@ function applyEditBlock(block, rootDir) {
13594
14377
  const searchEmpty = block.search.length === 0;
13595
14378
  if (searchEmpty) {
13596
14379
  try {
13597
- mkdirSync6(dirname7(absTarget), { recursive: true });
14380
+ mkdirSync6(dirname8(absTarget), { recursive: true });
13598
14381
  const fd = openSync2(absTarget, "wx");
13599
14382
  try {
13600
14383
  writeSync(fd, block.replace);
@@ -13670,19 +14453,19 @@ function applyEditBlocks(blocks, rootDir) {
13670
14453
  return blocks.map((b) => applyEditBlock(b, rootDir));
13671
14454
  }
13672
14455
  function snapshotBeforeEdits(blocks, rootDir) {
13673
- const absRoot = resolve9(rootDir);
14456
+ const absRoot = resolve12(rootDir);
13674
14457
  const seen = /* @__PURE__ */ new Set();
13675
14458
  const snapshots = [];
13676
14459
  for (const b of blocks) {
13677
14460
  if (seen.has(b.path)) continue;
13678
14461
  seen.add(b.path);
13679
- const abs = resolve9(absRoot, b.path);
13680
- if (!existsSync10(abs)) {
14462
+ const abs = resolve12(absRoot, b.path);
14463
+ if (!existsSync11(abs)) {
13681
14464
  snapshots.push({ path: b.path, prevContent: null });
13682
14465
  continue;
13683
14466
  }
13684
14467
  try {
13685
- snapshots.push({ path: b.path, prevContent: readFileSync13(abs, "utf8") });
14468
+ snapshots.push({ path: b.path, prevContent: readFileSync14(abs, "utf8") });
13686
14469
  } catch {
13687
14470
  snapshots.push({ path: b.path, prevContent: null });
13688
14471
  }
@@ -13690,10 +14473,10 @@ function snapshotBeforeEdits(blocks, rootDir) {
13690
14473
  return snapshots;
13691
14474
  }
13692
14475
  function restoreSnapshots(snapshots, rootDir) {
13693
- const absRoot = resolve9(rootDir);
14476
+ const absRoot = resolve12(rootDir);
13694
14477
  return snapshots.map((snap) => {
13695
- const abs = resolve9(absRoot, snap.path);
13696
- if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
14478
+ const abs = resolve12(absRoot, snap.path);
14479
+ if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep2()}`)) {
13697
14480
  return {
13698
14481
  path: snap.path,
13699
14482
  status: "path-escape",
@@ -13702,7 +14485,7 @@ function restoreSnapshots(snapshots, rootDir) {
13702
14485
  }
13703
14486
  try {
13704
14487
  if (snap.prevContent === null) {
13705
- if (existsSync10(abs)) unlinkSync3(abs);
14488
+ if (existsSync11(abs)) unlinkSync3(abs);
13706
14489
  return {
13707
14490
  path: snap.path,
13708
14491
  status: "applied",
@@ -13720,7 +14503,7 @@ function restoreSnapshots(snapshots, rootDir) {
13720
14503
  }
13721
14504
  });
13722
14505
  }
13723
- function sep() {
14506
+ function sep2() {
13724
14507
  return process.platform === "win32" ? "\\" : "/";
13725
14508
  }
13726
14509
  function lineEndingOf(text) {
@@ -13728,8 +14511,8 @@ function lineEndingOf(text) {
13728
14511
  }
13729
14512
 
13730
14513
  // src/code/prompt.ts
13731
- import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
13732
- import { join as join13 } from "path";
14514
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
14515
+ import { join as join15 } from "path";
13733
14516
  var DEFAULT_CODE_MODEL = "deepseek-v4-flash";
13734
14517
  function codeSystemBase(modelId) {
13735
14518
  return CODE_SYSTEM_TEMPLATE.replace("__ESCALATION_CONTRACT__", escalationContract(modelId));
@@ -13976,12 +14759,12 @@ function codeSystemPrompt(rootDir, opts = {}) {
13976
14759
  const codeBase = codeSystemBase(opts.modelId ?? DEFAULT_CODE_MODEL);
13977
14760
  const base = opts.hasSemanticSearch ? `${codeBase}${SEMANTIC_SEARCH_ROUTING}` : codeBase;
13978
14761
  const withMemory = applyMemoryStack(base, rootDir);
13979
- const gitignorePath = join13(rootDir, ".gitignore");
14762
+ const gitignorePath = join15(rootDir, ".gitignore");
13980
14763
  let result = withMemory;
13981
- if (existsSync11(gitignorePath)) {
14764
+ if (existsSync12(gitignorePath)) {
13982
14765
  let content;
13983
14766
  try {
13984
- content = readFileSync14(gitignorePath, "utf8");
14767
+ content = readFileSync15(gitignorePath, "utf8");
13985
14768
  } catch {
13986
14769
  }
13987
14770
  if (content !== void 0) {
@@ -14015,21 +14798,21 @@ ${appendParts.join("\n\n")}`;
14015
14798
  import {
14016
14799
  appendFileSync as appendFileSync2,
14017
14800
  closeSync as closeSync3,
14018
- existsSync as existsSync12,
14801
+ existsSync as existsSync13,
14019
14802
  fstatSync as fstatSync2,
14020
14803
  mkdirSync as mkdirSync7,
14021
14804
  openSync as openSync3,
14022
- readFileSync as readFileSync15,
14805
+ readFileSync as readFileSync16,
14023
14806
  readSync as readSync2,
14024
14807
  renameSync as renameSync2,
14025
14808
  statSync as statSync6,
14026
14809
  unlinkSync as unlinkSync4,
14027
14810
  writeFileSync as writeFileSync7
14028
14811
  } from "fs";
14029
- import { homedir as homedir7 } from "os";
14030
- import { dirname as dirname8, join as join14 } from "path";
14812
+ import { homedir as homedir8 } from "os";
14813
+ import { dirname as dirname9, join as join16 } from "path";
14031
14814
  function defaultUsageLogPath(homeDirOverride) {
14032
- return join14(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
14815
+ return join16(homeDirOverride ?? homedir8(), ".reasonix", "usage.jsonl");
14033
14816
  }
14034
14817
  var USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;
14035
14818
  var USAGE_RETENTION_DAYS = 365;
@@ -14094,7 +14877,7 @@ function appendUsage(input) {
14094
14877
  if (input.subagent) record.subagent = input.subagent;
14095
14878
  const path2 = input.path ?? defaultUsageLogPath();
14096
14879
  try {
14097
- mkdirSync7(dirname8(path2), { recursive: true });
14880
+ mkdirSync7(dirname9(path2), { recursive: true });
14098
14881
  appendFileSync2(path2, `${JSON.stringify(record)}
14099
14882
  `, "utf8");
14100
14883
  compactUsageLogIfLarge(path2, record.ts);
@@ -14103,10 +14886,10 @@ function appendUsage(input) {
14103
14886
  return record;
14104
14887
  }
14105
14888
  function readUsageLog(path2 = defaultUsageLogPath()) {
14106
- if (!existsSync12(path2)) return [];
14889
+ if (!existsSync13(path2)) return [];
14107
14890
  let raw;
14108
14891
  try {
14109
- raw = readFileSync15(path2, "utf8");
14892
+ raw = readFileSync16(path2, "utf8");
14110
14893
  } catch {
14111
14894
  return [];
14112
14895
  }
@@ -14213,7 +14996,7 @@ function aggregateUsage(records, opts = {}) {
14213
14996
  };
14214
14997
  }
14215
14998
  function formatLogSize(path2 = defaultUsageLogPath()) {
14216
- if (!existsSync12(path2)) return "";
14999
+ if (!existsSync13(path2)) return "";
14217
15000
  try {
14218
15001
  const s = statSync6(path2);
14219
15002
  const bytes = s.size;
@@ -14236,6 +15019,7 @@ export {
14236
15019
  DEFAULT_MAX_RESULT_CHARS,
14237
15020
  DEFAULT_MAX_RESULT_TOKENS,
14238
15021
  DEFAULT_PICKER_IGNORE_DIRS,
15022
+ DEFAULT_SPAWN_STORM_THRESHOLD,
14239
15023
  DeepSeekClient,
14240
15024
  HOOK_EVENTS,
14241
15025
  HOOK_SETTINGS_DIRNAME,
@@ -14259,6 +15043,7 @@ export {
14259
15043
  StdioTransport,
14260
15044
  StormBreaker,
14261
15045
  StreamableHttpTransport,
15046
+ SubagentTelemetry,
14262
15047
  ToolCallRepair,
14263
15048
  ToolRegistry,
14264
15049
  USER_MEMORY_DIR,
@@ -14281,7 +15066,9 @@ export {
14281
15066
  codeSystemPrompt,
14282
15067
  compareVersions,
14283
15068
  computeReplayStats,
15069
+ computeSpawnDistillation,
14284
15070
  costUsd,
15071
+ countSpawnStorms,
14285
15072
  decideOutcome,
14286
15073
  defaultConfigPath,
14287
15074
  defaultUsageLogPath,
@@ -14324,6 +15111,7 @@ export {
14324
15111
  loadBaseUrl,
14325
15112
  loadDotenv,
14326
15113
  loadHooks,
15114
+ loadMetasoApiKey,
14327
15115
  loadSessionMessages,
14328
15116
  matchesTool,
14329
15117
  memoryEnabled,
@@ -14374,6 +15162,7 @@ export {
14374
15162
  similarity,
14375
15163
  snapshotBeforeEdits,
14376
15164
  stripHallucinatedToolMarkup,
15165
+ summarizeSubagentSession,
14377
15166
  tokenizeCommand,
14378
15167
  truncateForModel,
14379
15168
  truncateForModelByTokens,