reasonix 0.41.0 → 0.44.0-rc.5

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 (207) hide show
  1. package/README.md +73 -12
  2. package/README.zh-CN.md +35 -7
  3. package/dashboard/app.css +225 -4
  4. package/dashboard/dist/app.js +6538 -6082
  5. package/dashboard/dist/app.js.map +1 -1
  6. package/data/deepseek-tokenizer.json.gz +0 -0
  7. package/dist/cli/{acp-64VQZLDJ.js → acp-TYZ2CTDL.js} +40 -37
  8. package/dist/cli/acp-TYZ2CTDL.js.map +1 -0
  9. package/dist/cli/chat-TH7VNNCJ.js +51 -0
  10. package/dist/cli/{chunk-E46ECXJD.js → chunk-2425HK6U.js} +2 -1
  11. package/dist/cli/{chunk-E46ECXJD.js.map → chunk-2425HK6U.js.map} +1 -1
  12. package/dist/cli/chunk-25T6CVUP.js +172 -0
  13. package/dist/cli/chunk-25T6CVUP.js.map +1 -0
  14. package/dist/cli/{chunk-3Q3C4W66.js → chunk-2UQP6H6T.js} +2 -1
  15. package/dist/cli/{chunk-3Q3C4W66.js.map → chunk-2UQP6H6T.js.map} +1 -1
  16. package/dist/cli/{chunk-SXLJBFIV.js → chunk-2V6EAEUW.js} +99 -31
  17. package/dist/cli/chunk-2V6EAEUW.js.map +1 -0
  18. package/dist/cli/{chunk-4H3ZRJ2U.js → chunk-4CTDEJUF.js} +4 -3
  19. package/dist/cli/{chunk-4H3ZRJ2U.js.map → chunk-4CTDEJUF.js.map} +1 -1
  20. package/dist/cli/{chunk-7VFNPMKG.js → chunk-4QUNBQQ2.js} +3 -2
  21. package/dist/cli/{chunk-7VFNPMKG.js.map → chunk-4QUNBQQ2.js.map} +1 -1
  22. package/dist/cli/chunk-5QCB62C4.js +25319 -0
  23. package/dist/cli/chunk-5QCB62C4.js.map +1 -0
  24. package/dist/cli/chunk-6OWJV3YW.js +390 -0
  25. package/dist/cli/chunk-6OWJV3YW.js.map +1 -0
  26. package/dist/cli/{chunk-YJFKFTAL.js → chunk-6PBZN4VI.js} +15 -3
  27. package/dist/cli/chunk-6PBZN4VI.js.map +1 -0
  28. package/dist/cli/{chunk-WE3YZULK.js → chunk-7CIGMZT3.js} +3 -2
  29. package/dist/cli/{chunk-WE3YZULK.js.map → chunk-7CIGMZT3.js.map} +1 -1
  30. package/dist/cli/{chunk-LTXADNCO.js → chunk-7UCMM425.js} +8 -3
  31. package/dist/cli/chunk-7UCMM425.js.map +1 -0
  32. package/dist/cli/{chunk-UV7XJUJH.js → chunk-AB2RED3C.js} +16 -9
  33. package/dist/cli/{chunk-UV7XJUJH.js.map → chunk-AB2RED3C.js.map} +1 -1
  34. package/dist/cli/chunk-AVFXO2EZ.js +2996 -0
  35. package/dist/cli/chunk-AVFXO2EZ.js.map +1 -0
  36. package/dist/cli/{chunk-7SPOFTMT.js → chunk-C53JQES5.js} +4 -3
  37. package/dist/cli/{chunk-7SPOFTMT.js.map → chunk-C53JQES5.js.map} +1 -1
  38. package/dist/cli/{chunk-MHGPBJ2T.js → chunk-CGDR2ELH.js} +131 -34
  39. package/dist/cli/chunk-CGDR2ELH.js.map +1 -0
  40. package/dist/cli/{chunk-4DCHFFEY.js → chunk-CWZKQ5FE.js} +8 -4
  41. package/dist/cli/chunk-CWZKQ5FE.js.map +1 -0
  42. package/dist/cli/chunk-FEZK652I.js +3644 -0
  43. package/dist/cli/chunk-FEZK652I.js.map +1 -0
  44. package/dist/cli/{chunk-A7VHMMDE.js → chunk-HNXDZGC6.js} +114 -5
  45. package/dist/cli/chunk-HNXDZGC6.js.map +1 -0
  46. package/dist/cli/{chunk-FWGEHRB7.js → chunk-J5XJHLWM.js} +2 -1
  47. package/dist/cli/{chunk-FWGEHRB7.js.map → chunk-J5XJHLWM.js.map} +1 -1
  48. package/dist/cli/{chunk-Y5XNV3NX.js → chunk-JMBMLOBP.js} +2 -1
  49. package/dist/cli/{chunk-Y5XNV3NX.js.map → chunk-JMBMLOBP.js.map} +1 -1
  50. package/dist/cli/{chunk-ARF3N2SY.js → chunk-JNAQYELD.js} +17 -8
  51. package/dist/cli/{chunk-ARF3N2SY.js.map → chunk-JNAQYELD.js.map} +1 -1
  52. package/dist/cli/{chunk-AT6GGIBV.js → chunk-KGBG6M2X.js} +25 -18
  53. package/dist/cli/chunk-KGBG6M2X.js.map +1 -0
  54. package/dist/cli/{chunk-A3LL4XDV.js → chunk-KLQTAZIY.js} +69 -5
  55. package/dist/cli/chunk-KLQTAZIY.js.map +1 -0
  56. package/dist/cli/{chunk-KZYLMMU5.js → chunk-KM465GST.js} +13 -12
  57. package/dist/cli/{chunk-KZYLMMU5.js.map → chunk-KM465GST.js.map} +1 -1
  58. package/dist/cli/{chunk-2CXPDAWX.js → chunk-LIR2HBQH.js} +3 -2
  59. package/dist/cli/{chunk-2CXPDAWX.js.map → chunk-LIR2HBQH.js.map} +1 -1
  60. package/dist/cli/{chunk-RAUPWSYA.js → chunk-MJ6W5UN3.js} +3 -2
  61. package/dist/cli/{chunk-RAUPWSYA.js.map → chunk-MJ6W5UN3.js.map} +1 -1
  62. package/dist/cli/{chunk-4W2CICFQ.js → chunk-MRHHQJAQ.js} +72 -60
  63. package/dist/cli/chunk-MRHHQJAQ.js.map +1 -0
  64. package/dist/cli/{chunk-BOFL3T45.js → chunk-NVURFF27.js} +23 -11
  65. package/dist/cli/chunk-NVURFF27.js.map +1 -0
  66. package/dist/cli/chunk-OPFUUYHL.js +11645 -0
  67. package/dist/cli/chunk-OPFUUYHL.js.map +1 -0
  68. package/dist/cli/{chunk-FM57FNPJ.js → chunk-PLHAZOLZ.js} +2 -1
  69. package/dist/cli/{chunk-FM57FNPJ.js.map → chunk-PLHAZOLZ.js.map} +1 -1
  70. package/dist/cli/{chunk-ZTLZO42A.js → chunk-R3CTO2HM.js} +3 -2
  71. package/dist/cli/{chunk-ZTLZO42A.js.map → chunk-R3CTO2HM.js.map} +1 -1
  72. package/dist/cli/{chunk-BYZGO3BX.js → chunk-RDRC3XDT.js} +145 -39
  73. package/dist/cli/chunk-RDRC3XDT.js.map +1 -0
  74. package/dist/cli/chunk-S4XVGLRW.js +499 -0
  75. package/dist/cli/chunk-S4XVGLRW.js.map +1 -0
  76. package/dist/cli/{chunk-WJ3YX4PZ.js → chunk-SZ5XES2N.js} +3 -2
  77. package/dist/cli/{chunk-WJ3YX4PZ.js.map → chunk-SZ5XES2N.js.map} +1 -1
  78. package/dist/cli/{chunk-CD4SCQL4.js → chunk-TEUDEGX2.js} +64 -10
  79. package/dist/cli/chunk-TEUDEGX2.js.map +1 -0
  80. package/dist/cli/chunk-TKVXTQ3T.js +96 -0
  81. package/dist/cli/chunk-TKVXTQ3T.js.map +1 -0
  82. package/dist/cli/chunk-TUK7OWJA.js +51 -0
  83. package/dist/cli/chunk-TXJMRPIL.js +54 -0
  84. package/dist/cli/{chunk-L7W3HJZQ.js.map → chunk-TXJMRPIL.js.map} +1 -1
  85. package/dist/cli/{chunk-H4OLWRSX.js → chunk-V26WPN3J.js} +11 -7
  86. package/dist/cli/chunk-V26WPN3J.js.map +1 -0
  87. package/dist/cli/{chunk-VFG4GIT3.js → chunk-WK3UFQY3.js} +3 -2
  88. package/dist/cli/{chunk-VFG4GIT3.js.map → chunk-WK3UFQY3.js.map} +1 -1
  89. package/dist/cli/chunk-X53B3JIX.js +34320 -0
  90. package/dist/cli/chunk-X53B3JIX.js.map +1 -0
  91. package/dist/cli/{chunk-SOZE7V7V.js → chunk-XJXDHAES.js} +3 -2
  92. package/dist/cli/{chunk-SOZE7V7V.js.map → chunk-XJXDHAES.js.map} +1 -1
  93. package/dist/cli/{chunk-CFY2XLY6.js → chunk-XSU4QVFW.js} +7 -85
  94. package/dist/cli/chunk-XSU4QVFW.js.map +1 -0
  95. package/dist/cli/{chunk-CRPQUBP6.js → chunk-XXC2BYTV.js} +2 -1
  96. package/dist/cli/{chunk-CRPQUBP6.js.map → chunk-XXC2BYTV.js.map} +1 -1
  97. package/dist/cli/chunk-Z4S7EYXG.js +63813 -0
  98. package/dist/cli/chunk-Z4S7EYXG.js.map +1 -0
  99. package/dist/cli/chunk-ZZM6QJ4W.js +109 -0
  100. package/dist/cli/chunk-ZZM6QJ4W.js.map +1 -0
  101. package/dist/cli/{chunk-ORM6PK57.js → chunk-ZZYBBX5N.js} +14 -5
  102. package/dist/cli/chunk-ZZYBBX5N.js.map +1 -0
  103. package/dist/cli/code-PSVJ3KEN.js +166 -0
  104. package/dist/cli/code-PSVJ3KEN.js.map +1 -0
  105. package/dist/cli/{commands-QY7MSQG7.js → commands-OCU42XG4.js} +7 -5
  106. package/dist/cli/{commands-QY7MSQG7.js.map → commands-OCU42XG4.js.map} +1 -1
  107. package/dist/cli/{commit-BRCQ3OQO.js → commit-XCQIQCYG.js} +7 -4
  108. package/dist/cli/{commit-BRCQ3OQO.js.map → commit-XCQIQCYG.js.map} +1 -1
  109. package/dist/cli/{desktop-ZTMHQR2Y.js → desktop-KWGR4BNE.js} +327 -98
  110. package/dist/cli/desktop-KWGR4BNE.js.map +1 -0
  111. package/dist/cli/devtools-HW3WDT3Q.js +91 -0
  112. package/dist/cli/devtools-HW3WDT3Q.js.map +1 -0
  113. package/dist/cli/diff-NHANTNC3.js +165 -0
  114. package/dist/cli/{diff-YASCB7PU.js.map → diff-NHANTNC3.js.map} +1 -1
  115. package/dist/cli/doctor-CC5CLOGG.js +27 -0
  116. package/dist/cli/{events-2AJTXR7I.js → events-XEFAD5VX.js} +6 -4
  117. package/dist/cli/{events-2AJTXR7I.js.map → events-XEFAD5VX.js.map} +1 -1
  118. package/dist/cli/index.js +3266 -152
  119. package/dist/cli/index.js.map +1 -1
  120. package/dist/cli/{mcp-YMWBLRR7.js → mcp-MPVGBBJF.js} +6 -4
  121. package/dist/cli/{mcp-YMWBLRR7.js.map → mcp-MPVGBBJF.js.map} +1 -1
  122. package/dist/cli/{mcp-browse-XLDUE6SB.js → mcp-browse-4XOTC3FJ.js} +32 -21
  123. package/dist/cli/{mcp-browse-XLDUE6SB.js.map → mcp-browse-4XOTC3FJ.js.map} +1 -1
  124. package/dist/cli/{mcp-inspect-H4D2HSJP.js → mcp-inspect-CEMGKKAH.js} +18 -10
  125. package/dist/cli/mcp-inspect-CEMGKKAH.js.map +1 -0
  126. package/dist/cli/package.json +3 -0
  127. package/dist/cli/prompt-2D7ID24X.js +16 -0
  128. package/dist/cli/{prune-sessions-4N3BVST2.js → prune-sessions-3RWUBYRS.js} +4 -2
  129. package/dist/cli/{prune-sessions-4N3BVST2.js.map → prune-sessions-3RWUBYRS.js.map} +1 -1
  130. package/dist/cli/{replay-3GTWM75X.js → replay-SR44E6RS.js} +42 -30
  131. package/dist/cli/{replay-3GTWM75X.js.map → replay-SR44E6RS.js.map} +1 -1
  132. package/dist/cli/{run-BLZPTRDX.js → run-MDGL27WL.js} +42 -40
  133. package/dist/cli/run-MDGL27WL.js.map +1 -0
  134. package/dist/cli/{server-DRFPXXSH.js → server-27ARQXIZ.js} +95 -55
  135. package/dist/cli/server-27ARQXIZ.js.map +1 -0
  136. package/dist/cli/{sessions-BOWFPTXT.js → sessions-CKQXCYGP.js} +25 -22
  137. package/dist/cli/sessions-CKQXCYGP.js.map +1 -0
  138. package/dist/cli/setup-TPAGSVXO.js +618 -0
  139. package/dist/cli/setup-TPAGSVXO.js.map +1 -0
  140. package/dist/cli/stats-DPUBZNVX.js +16 -0
  141. package/dist/cli/update-6ITLPRDV.js +15 -0
  142. package/dist/cli/version-2X3BHVVK.js +33 -0
  143. package/dist/cli/{version-XQXYSJ5L.js.map → version-2X3BHVVK.js.map} +1 -1
  144. package/dist/index.d.ts +182 -45
  145. package/dist/index.js +1420 -546
  146. package/dist/index.js.map +1 -1
  147. package/package.json +21 -8
  148. package/dist/cli/.-3G6VX5S7.js +0 -327
  149. package/dist/cli/.-6YRPB2C7.js +0 -329
  150. package/dist/cli/.-EYSVINK3.js +0 -317
  151. package/dist/cli/acp-64VQZLDJ.js.map +0 -1
  152. package/dist/cli/chat-ZAGX52RV.js +0 -46
  153. package/dist/cli/chunk-4DCHFFEY.js.map +0 -1
  154. package/dist/cli/chunk-4W2CICFQ.js.map +0 -1
  155. package/dist/cli/chunk-5X7LZJDE.js +0 -36
  156. package/dist/cli/chunk-5X7LZJDE.js.map +0 -1
  157. package/dist/cli/chunk-65Q5HQ26.js +0 -892
  158. package/dist/cli/chunk-65Q5HQ26.js.map +0 -1
  159. package/dist/cli/chunk-A3LL4XDV.js.map +0 -1
  160. package/dist/cli/chunk-A7VHMMDE.js.map +0 -1
  161. package/dist/cli/chunk-AFFZF3MW.js +0 -36
  162. package/dist/cli/chunk-AFFZF3MW.js.map +0 -1
  163. package/dist/cli/chunk-AT6GGIBV.js.map +0 -1
  164. package/dist/cli/chunk-BOFL3T45.js.map +0 -1
  165. package/dist/cli/chunk-BYZGO3BX.js.map +0 -1
  166. package/dist/cli/chunk-CD4SCQL4.js.map +0 -1
  167. package/dist/cli/chunk-CFY2XLY6.js.map +0 -1
  168. package/dist/cli/chunk-CPOV2O73.js +0 -39
  169. package/dist/cli/chunk-CPOV2O73.js.map +0 -1
  170. package/dist/cli/chunk-DAEAAVDF.js +0 -199
  171. package/dist/cli/chunk-DAEAAVDF.js.map +0 -1
  172. package/dist/cli/chunk-F2AV2QDK.js +0 -16514
  173. package/dist/cli/chunk-F2AV2QDK.js.map +0 -1
  174. package/dist/cli/chunk-H4OLWRSX.js.map +0 -1
  175. package/dist/cli/chunk-IEA6JOIP.js +0 -5430
  176. package/dist/cli/chunk-IEA6JOIP.js.map +0 -1
  177. package/dist/cli/chunk-L7W3HJZQ.js +0 -46
  178. package/dist/cli/chunk-LN27AKV3.js +0 -26
  179. package/dist/cli/chunk-LN27AKV3.js.map +0 -1
  180. package/dist/cli/chunk-LTXADNCO.js.map +0 -1
  181. package/dist/cli/chunk-MHGPBJ2T.js.map +0 -1
  182. package/dist/cli/chunk-ORM6PK57.js.map +0 -1
  183. package/dist/cli/chunk-SXLJBFIV.js.map +0 -1
  184. package/dist/cli/chunk-YJFKFTAL.js.map +0 -1
  185. package/dist/cli/code-X3M6ENTQ.js +0 -150
  186. package/dist/cli/code-X3M6ENTQ.js.map +0 -1
  187. package/dist/cli/desktop-ZTMHQR2Y.js.map +0 -1
  188. package/dist/cli/diff-YASCB7PU.js +0 -153
  189. package/dist/cli/doctor-XCN5ETVP.js +0 -24
  190. package/dist/cli/mcp-inspect-H4D2HSJP.js.map +0 -1
  191. package/dist/cli/prompt-RSIHN62V.js +0 -14
  192. package/dist/cli/run-BLZPTRDX.js.map +0 -1
  193. package/dist/cli/server-DRFPXXSH.js.map +0 -1
  194. package/dist/cli/sessions-BOWFPTXT.js.map +0 -1
  195. package/dist/cli/setup-FQL2JJC2.js +0 -516
  196. package/dist/cli/setup-FQL2JJC2.js.map +0 -1
  197. package/dist/cli/stats-5RJCATCE.js +0 -12
  198. package/dist/cli/stats-5RJCATCE.js.map +0 -1
  199. package/dist/cli/update-GUCWB4UN.js +0 -13
  200. package/dist/cli/update-GUCWB4UN.js.map +0 -1
  201. package/dist/cli/version-XQXYSJ5L.js +0 -30
  202. /package/dist/cli/{.-3G6VX5S7.js.map → chat-TH7VNNCJ.js.map} +0 -0
  203. /package/dist/cli/{.-6YRPB2C7.js.map → chunk-TUK7OWJA.js.map} +0 -0
  204. /package/dist/cli/{.-EYSVINK3.js.map → doctor-CC5CLOGG.js.map} +0 -0
  205. /package/dist/cli/{chat-ZAGX52RV.js.map → prompt-2D7ID24X.js.map} +0 -0
  206. /package/dist/cli/{doctor-XCN5ETVP.js.map → stats-DPUBZNVX.js.map} +0 -0
  207. /package/dist/cli/{prompt-RSIHN62V.js.map → update-6ITLPRDV.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",
@@ -966,6 +1100,7 @@ var EN = {
966
1100
  applied: "applied",
967
1101
  rejected: "rejected",
968
1102
  noDashboard: "Suppress the auto-launched embedded web dashboard.",
1103
+ openDashboardHint: "Open the dashboard URL in your default browser as soon as the server is ready. No-op when --no-dashboard is set.",
969
1104
  dashboardPortHint: "Pin the dashboard to a fixed port (1\u201365535). Stable across restarts \u2014 required for SSH tunnels. Default: ephemeral.",
970
1105
  dashboardPortInvalid: "\u25B2 ignoring --dashboard-port={value} (must be an integer 1\u201365535) \u2014 falling back to ephemeral",
971
1106
  dashboardAutoStartFailed: "\u25B2 dashboard auto-start failed ({reason}) \u2014 try /dashboard, or pass --no-dashboard to silence",
@@ -1130,7 +1265,13 @@ var EN = {
1130
1265
  jsonHintCatalog: "output as JSON",
1131
1266
  jsonHintReport: "output the inspection report as JSON",
1132
1267
  modelOverrideFlash: "override the model (default: deepseek-v4-flash)",
1133
- skipConfirmHint: "skip the confirmation prompt"
1268
+ skipConfirmHint: "skip the confirmation prompt",
1269
+ yoloHint: "auto-approve plan checkpoints for this invocation (equivalent to editMode=yolo without mutating config)"
1270
+ },
1271
+ code: {
1272
+ workspaceConflict: "\u26A0 workspace contains another agent platform's files ({platforms}). Reasonix Code may read them as project content; relaunch with --dir <your-project> if that's not what you want.\n",
1273
+ systemAppendEmpty: "--system-append is empty \u2014 no prompt text will be appended\n",
1274
+ systemAppendFileReadError: 'Error: cannot read --system-append-file "{filePath}": {errorDetails}\n'
1134
1275
  },
1135
1276
  slash: {
1136
1277
  help: { description: "show the full command reference" },
@@ -1176,8 +1317,8 @@ var EN = {
1176
1317
  argsHint: "[list|show <name>|forget <name>|clear <scope> confirm]"
1177
1318
  },
1178
1319
  skill: {
1179
- description: "list / run user skills (<project>/.reasonix/skills + ~/.reasonix/skills)",
1180
- argsHint: "[list|show <name>|<name> [args]]"
1320
+ description: "list / run user skills (project + custom + global + builtin)",
1321
+ argsHint: "[list|paths|show <name>|<name> [args]]"
1181
1322
  },
1182
1323
  hooks: {
1183
1324
  description: "list active hooks (settings.json under .reasonix/) \xB7 reload re-reads from disk",
@@ -1219,6 +1360,10 @@ var EN = {
1219
1360
  argsHint: "[N]"
1220
1361
  },
1221
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
+ },
1222
1367
  setup: { description: "reminds you to exit and run `reasonix setup`" },
1223
1368
  semantic: {
1224
1369
  description: "show semantic_search status \u2014 built? Ollama installed? how to enable"
@@ -1276,6 +1421,14 @@ var EN = {
1276
1421
  logs: {
1277
1422
  description: "tail a background job's output (default last 80 lines)",
1278
1423
  argsHint: "<id> [lines]"
1424
+ },
1425
+ btw: {
1426
+ description: "ask a quick side question \u2014 answered from a blank slate, never added to the conversation context",
1427
+ argsHint: "<question>"
1428
+ },
1429
+ "search-engine": {
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>]"
1279
1432
  }
1280
1433
  },
1281
1434
  wizard: {
@@ -1469,16 +1622,15 @@ var EN = {
1469
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.",
1470
1623
  budget80Pct: "\u25B2 budget 80% used \u2014 ${spent} of ${cap}. Next turn or two likely trips the cap.",
1471
1624
  proArmed: "\u21E7 /pro armed \u2014 this turn runs on deepseek-v4-pro (one-shot \xB7 disarms after turn)",
1472
- 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)",
1473
1626
  toolUploadStatus: "tool result uploaded \xB7 model thinking before next response\u2026",
1474
- toolBudgetWarning: "{iter}/{cap} tool calls used \u2014 approaching budget. Press Esc to force a summary now.",
1475
- preflightFoldStatus: "preflight: context near full, attempting fold\u2026",
1476
- preflightFolded: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) \u2014 folded {beforeMessages} messages \u2192 {afterMessages} (summary {summaryChars} chars). Sending.",
1477
- 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.",
1478
1631
  flashEscalation: "\u21E7 flash requested escalation \u2014 retrying this turn on {model}{reasonSuffix}",
1479
1632
  harvestStatus: "extracting plan state from reasoning\u2026",
1480
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.",
1481
- readOnlyLoopEscalation: "\u21E7 auto-escalating to {model} \u2014 flash made {n} consecutive read-only calls without producing an edit or final answer. Next turn falls back to {fallback} unless /pro is armed.",
1482
1634
  repeatToolCallWarning: "Caught a repeated tool call \u2014 let the model see the issue and retry with a different approach.",
1483
1635
  stormStuck: "Stopped a stuck retry loop \u2014 the model kept calling the same tool with identical args after a self-correction nudge. Try /retry, rephrase, or rule out the underlying blocker.",
1484
1636
  stormSuppressed: "Suppressed {count} repeated tool call(s) \u2014 same name + args fired 3+ times.",
@@ -1504,11 +1656,9 @@ var EN = {
1504
1656
  reasonAborted: "[aborted by user (Esc) \u2014 summarizing what I found so far]",
1505
1657
  reasonContextGuard: "[context budget running low \u2014 summarizing before the next call would overflow]",
1506
1658
  reasonStuck: "[stuck on a repeated tool call \u2014 explaining what was tried and what's blocking progress]",
1507
- reasonBudget: "[tool-call budget ({iterCap}) reached \u2014 forcing summary from what I found]",
1508
1659
  labelAborted: "aborted by user",
1509
1660
  labelContextGuard: "context-guard triggered (prompt > 80% of window)",
1510
- labelStuck: "stuck (repeated tool call suppressed by storm-breaker)",
1511
- labelBudget: "tool-call budget ({iterCap}) reached"
1661
+ labelStuck: "stuck (repeated tool call suppressed by storm-breaker)"
1512
1662
  },
1513
1663
  handlers: {
1514
1664
  basic: {
@@ -1818,11 +1968,13 @@ var EN = {
1818
1968
  usageMojeek: " /search-engine mojeek use Mojeek (default, no external deps)",
1819
1969
  usageSearxng: " /search-engine searxng use SearXNG at default endpoint",
1820
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)",
1821
1972
  alias: "Alias: /se",
1822
1973
  searxngInfo: "SearXNG is a self-hosted metasearch engine (https://github.com/searxng/searxng).",
1823
1974
  searxngInstall: "Install it with: docker run -d -p 8080:8080 searxng/searxng",
1824
1975
  switched: 'Switched web search engine to "{engine}".{note}',
1825
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).",
1826
1978
  confirmed: '\u2713 Web search engine set to "{engine}"{detail}. Next assistant turn will pick up the change.',
1827
1979
  confirmedDetail: " ({endpoint})"
1828
1980
  },
@@ -1842,7 +1994,17 @@ var EN = {
1842
1994
  runInfo: "\u25B8 running skill: {name}{args}",
1843
1995
  newUsage: "usage: /skill new <name> [--global]",
1844
1996
  newCreated: "\u25B8 created skill: {name}\n {path}\n edit it, then `/skill {name}` to invoke",
1845
- 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."
1846
2008
  }
1847
2009
  },
1848
2010
  statusBar: {
@@ -1859,7 +2021,8 @@ var EN = {
1859
2021
  mb: " MB",
1860
2022
  evt: " evt",
1861
2023
  editsLabel: "edits:",
1862
- mcpLoading: "MCP"
2024
+ mcpLoading: "MCP",
2025
+ ctx: "ctx"
1863
2026
  },
1864
2027
  editMode: {
1865
2028
  plan: "PLAN MODE",
@@ -1888,7 +2051,8 @@ var EN = {
1888
2051
  editorNoRawMode: "external editor unavailable \u2014 stdin doesn't support raw-mode toggling on this terminal",
1889
2052
  editorFailed: "external editor:",
1890
2053
  editorMissing: "no $EDITOR / $VISUAL / $GIT_EDITOR set \u2014 export one (e.g. `export EDITOR=nano`) and retry",
1891
- editorExited: "editor exited with code {code}"
2054
+ editorExited: "editor exited with code {code}",
2055
+ typeaheadStaged: "\u25B8 {count} line(s) staged \xB7 esc recall"
1892
2056
  },
1893
2057
  pathConfirm: {
1894
2058
  title: "Outside-sandbox path",
@@ -2058,6 +2222,12 @@ var EN = {
2058
2222
  endpointMustBeHttp: "web_search: SearXNG endpoint must be http(s), got {protocol} \u2014 try: set a valid URL with /search-endpoint http://host:port",
2059
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",
2060
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",
2061
2231
  fetchStatus: "web_fetch {status} for {url} \u2014 try: confirm the URL resolves in a browser; status suggests the host returned an error page",
2062
2232
  fetchRateLimit429: "web_fetch 429 for {url} \u2014 try: wait 10s before retrying; the host is rate-limiting this client",
2063
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",
@@ -2232,7 +2402,9 @@ var EN = {
2232
2402
  reconnect: "reconnect\u2026",
2233
2403
  initDetail: "initialise \u2192 tools/list \u2192 resources/list",
2234
2404
  reconnectDetail: "tearing down \xB7 re-handshake \xB7 listing tools",
2235
- disabledDetail: "via /mcp disable {name}"
2405
+ disabledDetail: "via /mcp disable {name}",
2406
+ failedSetupHint: "\u2192 run `reasonix setup` to remove this entry, or fix the underlying issue (missing npm package, network, etc.).",
2407
+ failedSetupConfigHint: "\u2192 run `reasonix setup` to remove broken entries from your saved config."
2236
2408
  },
2237
2409
  checkpointPicker: {
2238
2410
  title: "restore a checkpoint \u2014 {workspace}",
@@ -2316,6 +2488,16 @@ var zhCN = {
2316
2488
  update: "\u68C0\u67E5\u8F83\u65B0\u7248\u672C\u7684 Reasonix \u5E76\u5B89\u88C5\u3002",
2317
2489
  index: "\u6784\u5EFA\uFF08\u6216\u589E\u91CF\u5237\u65B0\uFF09\u672C\u5730\u8BED\u4E49\u641C\u7D22\u7D22\u5F15\u3002"
2318
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
+ },
2319
2501
  ui: {
2320
2502
  welcome: "\u968F\u65F6\u8FD0\u884C `reasonix` \u5F00\u59CB\u804A\u5929 \u2014 \u60A8\u7684\u8BBE\u7F6E\u5C06\u88AB\u8BB0\u4F4F\u3002",
2321
2503
  taglineChat: "DeepSeek \u539F\u751F\u667A\u80FD\u4F53",
@@ -2330,6 +2512,7 @@ var zhCN = {
2330
2512
  applied: "\u5DF2\u5E94\u7528",
2331
2513
  rejected: "\u5DF2\u62D2\u7EDD",
2332
2514
  noDashboard: "\u7981\u6B62\u81EA\u52A8\u542F\u52A8\u5D4C\u5165\u5F0F Web \u4EEA\u8868\u677F\u3002",
2515
+ openDashboardHint: "\u670D\u52A1\u5C31\u7EEA\u540E\u7ACB\u5373\u5728\u9ED8\u8BA4\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u4EEA\u8868\u677F\u5730\u5740\u3002\u8BBE\u7F6E\u4E86 --no-dashboard \u65F6\u4E0D\u751F\u6548\u3002",
2333
2516
  dashboardPortHint: "\u5C06\u4EEA\u8868\u677F\u7ED1\u5B9A\u5230\u56FA\u5B9A\u7AEF\u53E3 (1\u201365535)\u3002\u91CD\u542F\u540E\u4FDD\u6301\u7A33\u5B9A \u2014 SSH \u96A7\u9053\u8BBF\u95EE\u5FC5\u9700\u3002\u9ED8\u8BA4\u4E3A\u4E34\u65F6\u7AEF\u53E3\u3002",
2334
2517
  dashboardPortInvalid: "\u25B2 \u5FFD\u7565 --dashboard-port={value} (\u5FC5\u987B\u4E3A 1\u201365535 \u4E4B\u95F4\u7684\u6574\u6570) \u2014 \u56DE\u9000\u5230\u4E34\u65F6\u7AEF\u53E3",
2335
2518
  dashboardAutoStartFailed: "\u25B2 \u4EEA\u8868\u677F\u81EA\u52A8\u542F\u52A8\u5931\u8D25 ({reason}) \u2014 \u5C1D\u8BD5 /dashboard\uFF0C\u6216\u4F20\u9012 --no-dashboard \u4EE5\u9759\u9ED8",
@@ -2491,7 +2674,13 @@ var zhCN = {
2491
2674
  jsonHintCatalog: "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA",
2492
2675
  jsonHintReport: "\u4EE5 JSON \u683C\u5F0F\u8F93\u51FA\u68C0\u67E5\u62A5\u544A",
2493
2676
  modelOverrideFlash: "\u8986\u76D6\u6A21\u578B\uFF08\u9ED8\u8BA4\uFF1Adeepseek-v4-flash\uFF09",
2494
- skipConfirmHint: "\u8DF3\u8FC7\u786E\u8BA4\u63D0\u793A"
2677
+ skipConfirmHint: "\u8DF3\u8FC7\u786E\u8BA4\u63D0\u793A",
2678
+ yoloHint: "\u81EA\u52A8\u6279\u51C6\u672C\u6B21\u8C03\u7528\u7684\u8BA1\u5212\u68C0\u67E5\u70B9\uFF08\u7B49\u540C\u4E8E editMode=yolo\uFF0C\u4F46\u4E0D\u4FEE\u6539\u914D\u7F6E\u6587\u4EF6\uFF09"
2679
+ },
2680
+ code: {
2681
+ workspaceConflict: "\u26A0 \u5DE5\u4F5C\u533A\u5305\u542B\u53E6\u4E00\u4E2A\u667A\u80FD\u4F53\u5E73\u53F0\u7684\u6587\u4EF6 ({platforms})\u3002Reasonix Code \u53EF\u80FD\u4F1A\u5C06\u5176\u4F5C\u4E3A\u9879\u76EE\u5185\u5BB9\u8BFB\u53D6\uFF1B\u5982\u679C\u4E0D\u662F\u60A8\u60F3\u8981\u7684\uFF0C\u8BF7\u4F7F\u7528 --dir <your-project> \u91CD\u65B0\u542F\u52A8\u3002\n",
2682
+ systemAppendEmpty: "--system-append \u4E3A\u7A7A \u2014 \u4E0D\u4F1A\u8FFD\u52A0\u4EFB\u4F55\u63D0\u793A\u6587\u672C\n",
2683
+ systemAppendFileReadError: '\u9519\u8BEF\uFF1A\u65E0\u6CD5\u8BFB\u53D6 --system-append-file "{filePath}"\uFF1A{errorDetails}\n'
2495
2684
  },
2496
2685
  slash: {
2497
2686
  help: { description: "\u663E\u793A\u5B8C\u6574\u547D\u4EE4\u53C2\u8003" },
@@ -2537,8 +2726,8 @@ var zhCN = {
2537
2726
  argsHint: "[list|show <name>|forget <name>|clear <scope> confirm]"
2538
2727
  },
2539
2728
  skill: {
2540
- description: "\u5217\u51FA / \u8FD0\u884C\u7528\u6237\u6280\u80FD\uFF08<project>/.reasonix/skills + ~/.reasonix/skills\uFF09",
2541
- 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]]"
2542
2731
  },
2543
2732
  hooks: {
2544
2733
  description: "\u5217\u51FA\u6D3B\u8DC3\u7684 hooks\uFF08.reasonix/ \u4E0B\u7684 settings.json\uFF09\xB7 reload \u4ECE\u78C1\u76D8\u91CD\u65B0\u8BFB\u53D6",
@@ -2582,6 +2771,10 @@ var zhCN = {
2582
2771
  argsHint: "[N]"
2583
2772
  },
2584
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
+ },
2585
2778
  setup: { description: "\u63D0\u9192\u60A8\u9000\u51FA\u5E76\u8FD0\u884C `reasonix setup`" },
2586
2779
  semantic: {
2587
2780
  description: "\u663E\u793A semantic_search \u72B6\u6001 \u2014 \u5DF2\u6784\u5EFA\uFF1FOllama \u5DF2\u5B89\u88C5\uFF1F\u5982\u4F55\u542F\u7528"
@@ -2641,6 +2834,14 @@ var zhCN = {
2641
2834
  logs: {
2642
2835
  description: "\u8DDF\u8E2A\u540E\u53F0\u4F5C\u4E1A\u7684\u8F93\u51FA\uFF08\u9ED8\u8BA4\u6700\u540E 80 \u884C\uFF09",
2643
2836
  argsHint: "<id> [lines]"
2837
+ },
2838
+ btw: {
2839
+ description: "\u987A\u4FBF\u95EE\u4E00\u4E0B \u2014 \u4ECE\u7A7A\u767D\u4E0A\u4E0B\u6587\u56DE\u7B54\uFF0C\u4E0D\u5199\u5165\u4F1A\u8BDD\u5386\u53F2",
2840
+ argsHint: "<question>"
2841
+ },
2842
+ "search-engine": {
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>]"
2644
2845
  }
2645
2846
  },
2646
2847
  wizard: {
@@ -2834,16 +3035,15 @@ var zhCN = {
2834
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",
2835
3036
  budget80Pct: "\u25B2 \u9884\u7B97\u5DF2\u7528 80% \u2014 ${spent} / ${cap}\u3002\u4E0B\u4E00\u4E24\u8F6E\u53EF\u80FD\u5C31\u89E6\u9876\u3002",
2836
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",
2837
- 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",
2838
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",
2839
- 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",
2840
- preflightFoldStatus: "\u9884\u68C0\uFF1A\u4E0A\u4E0B\u6587\u63A5\u8FD1\u4E0A\u9650\uFF0C\u5C1D\u8BD5\u6298\u53E0\u2026",
2841
- 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",
2842
- 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",
2843
3044
  flashEscalation: "\u21E7 flash \u8BF7\u6C42\u5347\u7EA7 \u2014 \u672C\u8F6E\u6539\u7528 {model}{reasonSuffix}",
2844
3045
  harvestStatus: "\u6B63\u5728\u4ECE\u63A8\u7406\u8FC7\u7A0B\u63D0\u53D6\u8BA1\u5212\u72B6\u6001\u2026",
2845
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",
2846
- readOnlyLoopEscalation: "\u21E7 \u81EA\u52A8\u5347\u7EA7\u5230 {model} \u2014 flash \u8FDE\u7EED {n} \u6B21\u53EA\u8BFB\u8C03\u7528\uFF0C\u672A\u4EA7\u51FA\u4FEE\u6539\u6216\u6700\u7EC8\u7B54\u6848\u3002\u4E0B\u4E00\u8F6E\u56DE\u9000\u5230 {fallback}\uFF0C\u9664\u975E\u5DF2\u88C5\u5907 /pro\u3002",
2847
3047
  repeatToolCallWarning: "\u62E6\u622A\u5230\u91CD\u590D\u5DE5\u5177\u8C03\u7528 \u2014 \u8BA9\u6A21\u578B\u5BDF\u89C9\u95EE\u9898\u5E76\u6362\u79CD\u65B9\u5F0F\u91CD\u8BD5\u3002",
2848
3048
  stormStuck: "\u5DF2\u505C\u6B62\u5361\u6B7B\u7684\u91CD\u8BD5\u5FAA\u73AF \u2014 \u6A21\u578B\u5728\u81EA\u7EA0\u63D0\u793A\u540E\u4ECD\u4EE5\u76F8\u540C\u53C2\u6570\u91CD\u590D\u8C03\u7528\u540C\u4E00\u5DE5\u5177\u3002\u8BF7\u5C1D\u8BD5 /retry\u3001\u6362\u79CD\u8BF4\u6CD5\uFF0C\u6216\u6392\u67E5\u5E95\u5C42\u963B\u585E\u3002",
2849
3049
  stormSuppressed: "\u5DF2\u6291\u5236 {count} \u6B21\u91CD\u590D\u5DE5\u5177\u8C03\u7528 \u2014 \u540C\u4E00\u540D\u79F0 + \u53C2\u6570\u89E6\u53D1 3 \u6B21\u4EE5\u4E0A\u3002",
@@ -2869,11 +3069,9 @@ var zhCN = {
2869
3069
  reasonAborted: "[\u7528\u6237\u5DF2\u4E2D\u65AD\uFF08Esc\uFF09 \u2014 \u6B63\u5728\u603B\u7ED3\u5230\u76EE\u524D\u4E3A\u6B62\u7684\u53D1\u73B0]",
2870
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]",
2871
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]",
2872
- 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]",
2873
3072
  labelAborted: "\u7528\u6237\u4E2D\u65AD",
2874
3073
  labelContextGuard: "\u89E6\u53D1\u4E0A\u4E0B\u6587\u4FDD\u62A4\uFF08prompt > 80% \u7A97\u53E3\uFF09",
2875
- labelStuck: "\u5361\u6B7B\uFF08\u91CD\u590D\u5DE5\u5177\u8C03\u7528\u88AB\u53CD\u98CE\u66B4\u673A\u5236\u6291\u5236\uFF09",
2876
- 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"
2877
3075
  },
2878
3076
  handlers: {
2879
3077
  basic: {
@@ -3183,11 +3381,13 @@ var zhCN = {
3183
3381
  usageMojeek: " /search-engine mojeek \u4F7F\u7528 Mojeek\uFF08\u9ED8\u8BA4\uFF0C\u65E0\u5916\u90E8\u4F9D\u8D56\uFF09",
3184
3382
  usageSearxng: " /search-engine searxng \u4F7F\u7528 SearXNG \u9ED8\u8BA4\u7AEF\u70B9",
3185
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",
3186
3385
  alias: "\u522B\u540D\uFF1A/se",
3187
3386
  searxngInfo: "SearXNG \u662F\u4E00\u4E2A\u81EA\u6258\u7BA1\u7684\u5143\u641C\u7D22\u5F15\u64CE\uFF08https://github.com/searxng/searxng\uFF09\u3002",
3188
3387
  searxngInstall: "\u5B89\u88C5\u547D\u4EE4\uFF1A docker run -d -p 8080:8080 searxng/searxng",
3189
3388
  switched: '\u5DF2\u5207\u6362\u7F51\u9875\u641C\u7D22\u5F15\u64CE\u4E3A "{engine}"\u3002{note}',
3190
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",
3191
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',
3192
3392
  confirmedDetail: "\uFF08{endpoint}\uFF09"
3193
3393
  },
@@ -3207,7 +3407,17 @@ var zhCN = {
3207
3407
  runInfo: "\u25B8 \u6B63\u5728\u8FD0\u884C\u6280\u80FD\uFF1A{name}{args}",
3208
3408
  newUsage: "\u7528\u6CD5\uFF1A/skill new <name> [--global]",
3209
3409
  newCreated: "\u25B8 \u5DF2\u521B\u5EFA\u6280\u80FD\uFF1A{name}\n {path}\n \u7F16\u8F91\u540E\u7528 `/skill {name}` \u8C03\u7528",
3210
- 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"
3211
3421
  }
3212
3422
  },
3213
3423
  statusBar: {
@@ -3224,7 +3434,8 @@ var zhCN = {
3224
3434
  mb: " MB",
3225
3435
  evt: " \u4E8B\u4EF6",
3226
3436
  editsLabel: "\u7F16\u8F91:",
3227
- mcpLoading: "MCP"
3437
+ mcpLoading: "MCP",
3438
+ ctx: "\u4E0A\u4E0B\u6587"
3228
3439
  },
3229
3440
  editMode: {
3230
3441
  plan: "\u8BA1\u5212",
@@ -3253,7 +3464,8 @@ var zhCN = {
3253
3464
  editorNoRawMode: "\u5916\u90E8\u7F16\u8F91\u5668\u4E0D\u53EF\u7528 \u2014 \u5F53\u524D\u7EC8\u7AEF\u4E0D\u652F\u6301 raw-mode \u5207\u6362",
3254
3465
  editorFailed: "\u5916\u90E8\u7F16\u8F91\u5668\uFF1A",
3255
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",
3256
- 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"
3257
3469
  },
3258
3470
  pathConfirm: {
3259
3471
  title: "\u6C99\u7BB1\u5916\u8DEF\u5F84",
@@ -3423,6 +3635,12 @@ var zhCN = {
3423
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",
3424
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",
3425
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",
3426
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",
3427
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",
3428
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",
@@ -3597,7 +3815,9 @@ var zhCN = {
3597
3815
  reconnect: "\u91CD\u8FDE\u4E2D\u2026",
3598
3816
  initDetail: "\u521D\u59CB\u5316 \u2192 tools/list \u2192 resources/list",
3599
3817
  reconnectDetail: "\u65AD\u5F00\u65E7\u8FDE\u63A5 \xB7 \u91CD\u65B0\u63E1\u624B \xB7 \u5217\u51FA\u5DE5\u5177",
3600
- disabledDetail: "\u901A\u8FC7 /mcp disable {name}"
3818
+ disabledDetail: "\u901A\u8FC7 /mcp disable {name}",
3819
+ failedSetupHint: "\u2192 \u8FD0\u884C `reasonix setup` \u79FB\u9664\u6B64\u6761\u76EE\uFF0C\u6216\u4FEE\u590D\u5E95\u5C42\u95EE\u9898\uFF08\u7F3A\u5C11 npm \u5305\u3001\u7F51\u7EDC\u7B49\uFF09\u3002",
3820
+ failedSetupConfigHint: "\u2192 \u8FD0\u884C `reasonix setup` \u4ECE\u5DF2\u4FDD\u5B58\u914D\u7F6E\u4E2D\u79FB\u9664\u635F\u574F\u7684\u6761\u76EE\u3002"
3601
3821
  },
3602
3822
  checkpointPicker: {
3603
3823
  title: "\u6062\u590D\u68C0\u67E5\u70B9 \u2014 {workspace}",
@@ -3752,7 +3972,7 @@ function matchesTool(hook, toolName) {
3752
3972
  }
3753
3973
  var HOOK_OUTPUT_CAP_BYTES = 256 * 1024;
3754
3974
  function defaultSpawner(input) {
3755
- return new Promise((resolve10) => {
3975
+ return new Promise((resolve13) => {
3756
3976
  const child = spawn(input.command, {
3757
3977
  cwd: input.cwd,
3758
3978
  shell: true,
@@ -3797,7 +4017,7 @@ function defaultSpawner(input) {
3797
4017
  child.stderr.on("data", (chunk) => onChunk("stderr", chunk));
3798
4018
  child.once("error", (err) => {
3799
4019
  clearTimeout(timer);
3800
- resolve10({
4020
+ resolve13({
3801
4021
  exitCode: null,
3802
4022
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
3803
4023
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
@@ -3808,7 +4028,7 @@ function defaultSpawner(input) {
3808
4028
  });
3809
4029
  child.once("close", (code) => {
3810
4030
  clearTimeout(timer);
3811
- resolve10({
4031
+ resolve13({
3812
4032
  exitCode: code,
3813
4033
  stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(),
3814
4034
  stderr: Buffer.concat(stderrChunks).toString("utf8").trim(),
@@ -4044,22 +4264,211 @@ function encode(text) {
4044
4264
  function countTokens(text) {
4045
4265
  return encode(text).length;
4046
4266
  }
4047
- function estimateConversationTokens(messages) {
4048
- let total = 0;
4049
- for (const m of messages) {
4050
- if (typeof m.content === "string" && m.content) {
4051
- 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 });
4052
4401
  }
4053
- if (m.tool_calls && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
4054
- 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;
4055
4416
  }
4056
4417
  }
4057
- 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;
4058
4430
  }
4059
- function estimateRequestTokens(messages, toolSpecs) {
4060
- 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);
4061
4470
  if (toolSpecs && toolSpecs.length > 0) {
4062
- total += countTokens(JSON.stringify(toolSpecs));
4471
+ total += countTokensBounded(renderTools(toolSpecs));
4063
4472
  }
4064
4473
  return total;
4065
4474
  }
@@ -4412,12 +4821,12 @@ async function waitForReady(ready, timeoutMs, serverName, signal) {
4412
4821
  let timer;
4413
4822
  let onAbort;
4414
4823
  try {
4415
- await new Promise((resolve10, reject) => {
4824
+ await new Promise((resolve13, reject) => {
4416
4825
  ready.then(
4417
4826
  () => {
4418
4827
  if (settled) return;
4419
4828
  settled = true;
4420
- resolve10();
4829
+ resolve13();
4421
4830
  },
4422
4831
  (err) => {
4423
4832
  if (settled) return;
@@ -4566,6 +4975,40 @@ function blockToString(block) {
4566
4975
  return `[unknown block: ${JSON.stringify(block)}]`;
4567
4976
  }
4568
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
+
4569
5012
  // src/memory/session.ts
4570
5013
  import { execFileSync } from "child_process";
4571
5014
  import {
@@ -4581,7 +5024,7 @@ import {
4581
5024
  writeFileSync as writeFileSync2
4582
5025
  } from "fs";
4583
5026
  import { homedir as homedir3 } from "os";
4584
- import { dirname as dirname3, join as join4 } from "path";
5027
+ import { dirname as dirname3, join as join4, posix as posixPath, win32 as win32Path } from "path";
4585
5028
  function sessionsDir() {
4586
5029
  return join4(homedir3(), ".reasonix", "sessions");
4587
5030
  }
@@ -4735,11 +5178,11 @@ function countLines(path2) {
4735
5178
 
4736
5179
  // src/telemetry/stats.ts
4737
5180
  var DEEPSEEK_PRICING = {
4738
- "deepseek-v4-flash": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
4739
- "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 },
4740
5183
  // Compat aliases — priced as v4-flash per the deprecation notice.
4741
- "deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
4742
- "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 }
4743
5186
  };
4744
5187
  var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
4745
5188
  var DEEPSEEK_CONTEXT_TOKENS = {
@@ -4801,6 +5244,14 @@ var SessionStats = class {
4801
5244
  this._carryoverLastPromptTokens = opts.lastPromptTokens;
4802
5245
  }
4803
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
+ }
4804
5255
  record(turn, model, usage) {
4805
5256
  const cost = costUsd(model, usage);
4806
5257
  const stats = {
@@ -4867,7 +5318,26 @@ var HISTORY_FOLD_AGGRESSIVE_TAIL_FRACTION = 0.1;
4867
5318
  var HISTORY_FOLD_MIN_SAVINGS_FRACTION = 0.3;
4868
5319
  var FORCE_SUMMARY_THRESHOLD = 0.8;
4869
5320
  var PREFLIGHT_EMERGENCY_THRESHOLD = 0.95;
5321
+ var PREFLIGHT_MECHANICAL_TARGET_FRACTION = 0.7;
5322
+ var HISTORY_FOLD_SUMMARY_TIMEOUT_MS = 15e3;
4870
5323
  var HISTORY_FOLD_MARKER = "[CONVERSATION HISTORY SUMMARY \u2014 earlier turns folded for context efficiency]\n\n";
5324
+ var SKILL_PIN_MEMO_HEADER = "[Active skill memos \u2014 preserved verbatim across the fold:]";
5325
+ var SKILL_PIN_REGEX = /<skill-pin name="([^"]+)">\n[\s\S]*?\n<\/skill-pin>/g;
5326
+ function extractPinnedSkills(head) {
5327
+ const pinned = /* @__PURE__ */ new Map();
5328
+ const stubbedHead = head.map((msg) => {
5329
+ if (typeof msg.content !== "string") return msg;
5330
+ let hit = false;
5331
+ const next = msg.content.replace(SKILL_PIN_REGEX, (full, name) => {
5332
+ pinned.delete(name);
5333
+ pinned.set(name, full);
5334
+ hit = true;
5335
+ return `[skill ${JSON.stringify(name)} memo \u2014 preserved separately, do not summarize.]`;
5336
+ });
5337
+ return hit ? { ...msg, content: next } : msg;
5338
+ });
5339
+ return { stubbedHead, pinnedBodies: [...pinned.values()] };
5340
+ }
4871
5341
  var ContextManager = class {
4872
5342
  constructor(deps) {
4873
5343
  this.deps = deps;
@@ -4904,7 +5374,7 @@ var ContextManager = class {
4904
5374
  /** Local-side preflight before sending a request — catches oversized payloads early. */
4905
5375
  decidePreflight(messages, toolSpecs, model) {
4906
5376
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
4907
- const estimate = estimateRequestTokens(messages, toolSpecs ?? null);
5377
+ const estimate = estimateRequestTokens(messages, toolSpecs ?? null, true);
4908
5378
  return {
4909
5379
  needsAction: estimate / ctxMax > PREFLIGHT_EMERGENCY_THRESHOLD,
4910
5380
  estimateTokens: estimate,
@@ -4923,7 +5393,7 @@ var ContextManager = class {
4923
5393
  summaryChars: 0
4924
5394
  };
4925
5395
  if (all.length === 0) return noop;
4926
- const tokenCounts = all.map((m) => estimateConversationTokens([m]));
5396
+ const tokenCounts = all.map((m) => countTokensBounded(m.content ?? ""));
4927
5397
  const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);
4928
5398
  let cumTokens = 0;
4929
5399
  let boundary = all.length;
@@ -4937,12 +5407,20 @@ var ContextManager = class {
4937
5407
  const tail = all.slice(boundary);
4938
5408
  const headTokens = totalTokens - cumTokens;
4939
5409
  if (headTokens < totalTokens * HISTORY_FOLD_MIN_SAVINGS_FRACTION) return noop;
4940
- const summary = await this.summarizeForFold(head);
4941
- if (!summary) return noop;
4942
- const summaryMsg = {
4943
- role: "assistant",
4944
- content: HISTORY_FOLD_MARKER + summary
4945
- };
5410
+ const { stubbedHead, pinnedBodies } = extractPinnedSkills(head);
5411
+ const summary = await this.summarizeForFold(stubbedHead);
5412
+ if (!summary.content) return noop;
5413
+ const memoTail = pinnedBodies.length > 0 ? `
5414
+
5415
+ ${SKILL_PIN_MEMO_HEADER}
5416
+
5417
+ ${pinnedBodies.join("\n\n")}` : "";
5418
+ const summaryMsg = buildAssistantMessage(
5419
+ HISTORY_FOLD_MARKER + summary.content + memoTail,
5420
+ [],
5421
+ model,
5422
+ summary.reasoningContent
5423
+ );
4946
5424
  const replacement = [summaryMsg, ...tail];
4947
5425
  this.deps.log.compactInPlace(replacement);
4948
5426
  this.persistRewrite(replacement);
@@ -4950,7 +5428,51 @@ var ContextManager = class {
4950
5428
  folded: true,
4951
5429
  beforeMessages: all.length,
4952
5430
  afterMessages: replacement.length,
4953
- 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
4954
5476
  };
4955
5477
  }
4956
5478
  /** Drop a trailing in-flight assistant-with-tool_calls before a forced summary. Tail-only mutation; prefix cache safe. */
@@ -4976,18 +5498,51 @@ var ContextManager = class {
4976
5498
  content: "Summarize the conversation above as plain prose. This summary replaces the original turns to free context \u2014 make it self-contained."
4977
5499
  }
4978
5500
  ];
5501
+ const turnSignal = this.deps.getAbortSignal();
5502
+ const foldCtrl = new AbortController();
5503
+ let cleanupAbort = () => {
5504
+ };
5505
+ let timeout;
4979
5506
  try {
4980
- const resp = await this.deps.client.chat({
4981
- model: summaryModel,
4982
- messages,
4983
- signal: this.deps.getAbortSignal(),
4984
- thinking: thinkingModeForModel(summaryModel),
4985
- 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
+ }
4986
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);
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
+ ]);
4987
5536
  this.deps.stats.record(this.deps.getCurrentTurn(), summaryModel, resp.usage ?? new Usage());
4988
- return stripHallucinatedToolMarkup((resp.content ?? "").trim());
5537
+ return {
5538
+ content: stripHallucinatedToolMarkup((resp.content ?? "").trim()),
5539
+ reasoningContent: resp.reasoningContent ?? ""
5540
+ };
4989
5541
  } catch {
4990
- return "";
5542
+ return { content: "", reasoningContent: "" };
5543
+ } finally {
5544
+ if (timeout) clearTimeout(timeout);
5545
+ cleanupAbort();
4991
5546
  }
4992
5547
  }
4993
5548
  persistRewrite(messages) {
@@ -5079,17 +5634,15 @@ function formatDeepSeek5xx(status, probe) {
5079
5634
  const action = probe?.reachable === false ? t("errors.deepseek5xxActionNetwork") : t("errors.deepseek5xxActionRetry");
5080
5635
  return `${head}${probeNote}${action}`;
5081
5636
  }
5082
- function reasonPrefixFor(reason, iterCap) {
5637
+ function reasonPrefixFor(reason) {
5083
5638
  if (reason === "aborted") return t("errors.reasonAborted");
5084
5639
  if (reason === "context-guard") return t("errors.reasonContextGuard");
5085
- if (reason === "stuck") return t("errors.reasonStuck");
5086
- return t("errors.reasonBudget", { iterCap });
5640
+ return t("errors.reasonStuck");
5087
5641
  }
5088
- function errorLabelFor(reason, iterCap) {
5642
+ function errorLabelFor(reason) {
5089
5643
  if (reason === "aborted") return t("errors.labelAborted");
5090
5644
  if (reason === "context-guard") return t("errors.labelContextGuard");
5091
- if (reason === "stuck") return t("errors.labelStuck");
5092
- return t("errors.labelBudget", { iterCap });
5645
+ return t("errors.labelStuck");
5093
5646
  }
5094
5647
  function extractDeepSeekErrorMessage(body) {
5095
5648
  const trimmed = body.trim();
@@ -5131,48 +5684,14 @@ function looksLikePartialEscalationMarker(buf) {
5131
5684
  return true;
5132
5685
  }
5133
5686
 
5134
- // src/loop/thinking.ts
5135
- function isThinkingModeModel(model) {
5136
- if (model.includes("reasoner")) return true;
5137
- if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return true;
5138
- return false;
5139
- }
5140
- function thinkingModeForModel(model) {
5141
- if (model === "deepseek-chat") return "disabled";
5142
- if (model.includes("reasoner")) return "enabled";
5143
- if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return "enabled";
5144
- return void 0;
5145
- }
5146
- function stripHallucinatedToolMarkup(s) {
5147
- let out = s;
5148
- out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
5149
- out = out.replace(/<\|DSML\|function_calls>[\s\S]*?<\/?\|DSML\|function_calls>/g, "");
5150
- out = out.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "");
5151
- out = out.replace(/<|DSML|[\s\S]*$/g, "");
5152
- return out.trim();
5153
- }
5154
-
5155
- // src/loop/messages.ts
5156
- function buildAssistantMessage(content, toolCalls, producingModel, reasoningContent) {
5157
- const msg = { role: "assistant", content };
5158
- if (toolCalls.length > 0) msg.tool_calls = toolCalls;
5159
- if (isThinkingModeModel(producingModel) || reasoningContent && reasoningContent.length > 0) {
5160
- msg.reasoning_content = reasoningContent ?? "";
5161
- }
5162
- return msg;
5163
- }
5164
- function buildSyntheticAssistantMessage(content, fallbackModel) {
5165
- return buildAssistantMessage(content, [], fallbackModel, "");
5166
- }
5167
-
5168
5687
  // src/loop/force-summary.ts
5169
- async function* forceSummaryAfterIterLimit(ctx, opts = { reason: "budget" }) {
5688
+ async function* forceSummaryAfterIterLimit(ctx, opts) {
5170
5689
  try {
5171
5690
  yield { turn: ctx.turn, role: "status", content: t("summary.status") };
5172
5691
  const messages = ctx.buildMessages();
5173
5692
  messages.push({
5174
5693
  role: "user",
5175
- 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."
5176
5695
  });
5177
5696
  const summaryModel = "deepseek-v4-flash";
5178
5697
  const summaryEffort = "high";
@@ -5186,7 +5705,7 @@ async function* forceSummaryAfterIterLimit(ctx, opts = { reason: "budget" }) {
5186
5705
  const rawContent = resp.content?.trim() ?? "";
5187
5706
  const cleaned = stripHallucinatedToolMarkup(rawContent);
5188
5707
  const summary = cleaned || t("summary.hallucinatedFallback");
5189
- const reasonPrefix = reasonPrefixFor(opts.reason, ctx.maxToolIters);
5708
+ const reasonPrefix = reasonPrefixFor(opts.reason);
5190
5709
  const annotated = `${reasonPrefix}
5191
5710
 
5192
5711
  ${summary}`;
@@ -5201,7 +5720,7 @@ ${summary}`;
5201
5720
  };
5202
5721
  yield { turn: ctx.turn, role: "done", content: summary };
5203
5722
  } catch (err) {
5204
- const label = errorLabelFor(opts.reason, ctx.maxToolIters);
5723
+ const label = errorLabelFor(opts.reason);
5205
5724
  yield {
5206
5725
  turn: ctx.turn,
5207
5726
  role: "error",
@@ -5243,7 +5762,7 @@ function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
5243
5762
  if (msg.role !== "tool") return msg;
5244
5763
  const content = typeof msg.content === "string" ? msg.content : "";
5245
5764
  if (content.length <= maxTokens) return msg;
5246
- const beforeTokens = countTokens(content);
5765
+ const beforeTokens = countTokensBounded(content);
5247
5766
  if (beforeTokens <= maxTokens) return msg;
5248
5767
  const truncated = truncateForModelByTokens(content, maxTokens);
5249
5768
  const afterTokens = countTokens(truncated);
@@ -5348,32 +5867,6 @@ function* hookWarnings(outcomes, turn) {
5348
5867
  }
5349
5868
  }
5350
5869
 
5351
- // src/loop/read-only-loop-tracker.ts
5352
- var READONLY_LOOP_ESCALATION_THRESHOLD = 8;
5353
- var ReadOnlyLoopTracker = class {
5354
- streak = 0;
5355
- threshold;
5356
- constructor(threshold = READONLY_LOOP_ESCALATION_THRESHOLD) {
5357
- this.threshold = Math.max(1, threshold);
5358
- }
5359
- reset() {
5360
- this.streak = 0;
5361
- }
5362
- /** True ONLY on the call where the streak crosses the configured threshold. */
5363
- noteAndCrossedThreshold(isReadOnly) {
5364
- if (!isReadOnly) {
5365
- this.streak = 0;
5366
- return false;
5367
- }
5368
- const before = this.streak;
5369
- this.streak += 1;
5370
- return before < this.threshold && this.streak >= this.threshold;
5371
- }
5372
- get currentStreak() {
5373
- return this.streak;
5374
- }
5375
- };
5376
-
5377
5870
  // src/loop/turn-failure-tracker.ts
5378
5871
  var FAILURE_ESCALATION_THRESHOLD = 3;
5379
5872
  var TurnFailureTracker = class {
@@ -5703,11 +6196,16 @@ var StormBreaker = class {
5703
6196
  function repairTruncatedJson(input) {
5704
6197
  const notes = [];
5705
6198
  if (!input || !input.trim()) {
5706
- return { repaired: "{}", changed: input !== "{}", notes: ["empty input \u2192 {}"] };
6199
+ return {
6200
+ repaired: "{}",
6201
+ changed: input !== "{}",
6202
+ notes: ["empty input \u2192 {}"],
6203
+ fallback: false
6204
+ };
5707
6205
  }
5708
6206
  try {
5709
6207
  JSON.parse(input);
5710
- return { repaired: input, changed: false, notes: [] };
6208
+ return { repaired: input, changed: false, notes: [], fallback: false };
5711
6209
  } catch {
5712
6210
  }
5713
6211
  const stack = [];
@@ -5762,10 +6260,12 @@ function repairTruncatedJson(input) {
5762
6260
  }
5763
6261
  try {
5764
6262
  JSON.parse(s);
5765
- return { repaired: s, changed: true, notes };
6263
+ return { repaired: s, changed: s !== input, notes, fallback: false };
5766
6264
  } catch (err) {
6265
+ const preview = input.length <= 500 ? input : `${input.slice(0, 500)} \u2026[+${input.length - 500} chars]`;
5767
6266
  notes.push(`fallback to {}: ${err.message}`);
5768
- return { repaired: "{}", changed: true, notes };
6267
+ notes.push(`unrecoverable truncation \u2014 original args preview: ${preview}`);
6268
+ return { repaired: "{}", changed: true, notes, fallback: true };
5769
6269
  }
5770
6270
  }
5771
6271
 
@@ -5812,9 +6312,16 @@ var ToolCallRepair = class {
5812
6312
  const args = call.function?.arguments ?? "";
5813
6313
  const r = repairTruncatedJson(args);
5814
6314
  if (r.changed) {
5815
- call.function.arguments = r.repaired;
5816
- report.truncationsFixed++;
5817
- 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
+ }
5818
6325
  }
5819
6326
  }
5820
6327
  const filtered = [];
@@ -5836,12 +6343,10 @@ function signature(call) {
5836
6343
 
5837
6344
  // src/loop.ts
5838
6345
  var ESCALATION_MODEL = "deepseek-v4-pro";
5839
- var PARENT_BUDGET_WARN_THRESHOLD = 5;
5840
6346
  var CacheFirstLoop = class {
5841
6347
  client;
5842
6348
  prefix;
5843
6349
  tools;
5844
- maxToolIters;
5845
6350
  log = new AppendOnlyLog();
5846
6351
  scratch = new VolatileScratch();
5847
6352
  stats = new SessionStats();
@@ -5869,13 +6374,25 @@ var CacheFirstLoop = class {
5869
6374
  _turnAbort = new AbortController();
5870
6375
  /** Authoritative running-id set — UI cards consult this instead of trusting end-event delivery. Insert at dispatch entry, delete in finally. */
5871
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
+ }
5872
6391
  _proArmedForNextTurn = false;
5873
6392
  _escalateThisTurn = false;
5874
6393
  _turnFailures;
5875
- _readOnlyLoop;
5876
6394
  _turnSelfCorrected = false;
5877
6395
  _foldedThisTurn = false;
5878
- _toolDispatchesThisStep = 0;
5879
6396
  context;
5880
6397
  /** Subscribe API so UI hooks can derive `running` from finally-guaranteed insertions. */
5881
6398
  get inflight() {
@@ -5895,10 +6412,6 @@ var CacheFirstLoop = class {
5895
6412
  this._turnFailures = new TurnFailureTracker(
5896
6413
  resolveFailureThreshold(opts.failureThreshold, FAILURE_ESCALATION_THRESHOLD)
5897
6414
  );
5898
- this._readOnlyLoop = new ReadOnlyLoopTracker(
5899
- parsePositiveIntEnv(process.env.REASONIX_READONLY_LOOP_THRESHOLD) ?? READONLY_LOOP_ESCALATION_THRESHOLD
5900
- );
5901
- this.maxToolIters = opts.maxToolIters ?? 64;
5902
6415
  this.hooks = opts.hooks ?? [];
5903
6416
  this.hookCwd = opts.hookCwd ?? process.cwd();
5904
6417
  this.confirmationGate = opts.confirmationGate ?? pauseGate;
@@ -5919,23 +6432,6 @@ var CacheFirstLoop = class {
5919
6432
  stormThreshold: parsePositiveIntEnv(process.env.REASONIX_STORM_THRESHOLD),
5920
6433
  stormWindow: parsePositiveIntEnv(process.env.REASONIX_STORM_WINDOW)
5921
6434
  });
5922
- if (!this.tools.hasResultAugmenter) {
5923
- this.tools.setResultAugmenter((_name, _args, result) => {
5924
- this._toolDispatchesThisStep++;
5925
- const remaining = this.maxToolIters - this._toolDispatchesThisStep;
5926
- if (remaining <= 0) {
5927
- return `${result}
5928
-
5929
- [budget: 0 of ${this.maxToolIters} tool calls left this turn \u2014 finalize NOW; the next iter forces a summary]`;
5930
- }
5931
- if (remaining <= PARENT_BUDGET_WARN_THRESHOLD) {
5932
- return `${result}
5933
-
5934
- [budget: ${remaining} of ${this.maxToolIters} tool calls left this turn \u2014 wrap up soon]`;
5935
- }
5936
- return result;
5937
- });
5938
- }
5939
6435
  this.sessionName = opts.session ?? null;
5940
6436
  if (this.sessionName) {
5941
6437
  const prior = loadSessionMessages(this.sessionName);
@@ -6021,6 +6517,10 @@ var CacheFirstLoop = class {
6021
6517
  }
6022
6518
  this.scratch.reset();
6023
6519
  this._inflight.clear();
6520
+ this.stats.reset();
6521
+ this._turn = 0;
6522
+ this._turnFailures.reset();
6523
+ this._budgetWarned = false;
6024
6524
  let systemRebuilt = false;
6025
6525
  if (this._rebuildSystem) {
6026
6526
  try {
@@ -6030,6 +6530,29 @@ var CacheFirstLoop = class {
6030
6530
  }
6031
6531
  return { dropped, archived, systemRebuilt };
6032
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
+ }
6033
6556
  configure(opts) {
6034
6557
  if (opts.model !== void 0) this.model = opts.model;
6035
6558
  if (opts.stream !== void 0) {
@@ -6074,14 +6597,6 @@ var CacheFirstLoop = class {
6074
6597
  this._escalateThisTurn = true;
6075
6598
  return true;
6076
6599
  }
6077
- /** Returns true ONLY on the call where the read-only streak crosses the threshold (#681). */
6078
- noteReadOnlyToolCall(call) {
6079
- const isReadOnly = !this.isMutating(call);
6080
- if (!this._readOnlyLoop.noteAndCrossedThreshold(isReadOnly)) return false;
6081
- if (this._escalateThisTurn || !this.autoEscalate) return false;
6082
- this._escalateThisTurn = true;
6083
- return true;
6084
- }
6085
6600
  /** A call counts as mutating when its definition reports `readOnly !== true` and any dynamic `readOnlyCheck` doesn't override that for these args. */
6086
6601
  isMutating(call) {
6087
6602
  const name = call.function?.name;
@@ -6193,6 +6708,7 @@ ${reason}`
6193
6708
  return userText;
6194
6709
  }
6195
6710
  async *step(userInput) {
6711
+ this._steerConsumed = false;
6196
6712
  if (this.budgetUsd !== null) {
6197
6713
  const spent = this.stats.totalCost;
6198
6714
  if (spent >= this.budgetUsd) {
@@ -6223,11 +6739,9 @@ ${reason}`
6223
6739
  this.scratch.reset();
6224
6740
  this.repair.resetStorm();
6225
6741
  this._turnFailures.reset();
6226
- this._readOnlyLoop.reset();
6227
6742
  this._turnSelfCorrected = false;
6228
6743
  this._escalateThisTurn = false;
6229
6744
  this._foldedThisTurn = false;
6230
- this._toolDispatchesThisStep = 0;
6231
6745
  let armedConsumed = false;
6232
6746
  if (this._proArmedForNextTurn) {
6233
6747
  this._escalateThisTurn = true;
@@ -6245,16 +6759,15 @@ ${reason}`
6245
6759
  content: t("loop.proArmed")
6246
6760
  };
6247
6761
  }
6248
- let pendingUser = userInput;
6762
+ this.appendAndPersist({ role: "user", content: userInput });
6763
+ let pendingUser = null;
6249
6764
  const toolSpecs = this.prefix.tools();
6250
- const warnAt = Math.max(1, Math.floor(this.maxToolIters * 0.7));
6251
- let warnedForIterBudget = false;
6252
- for (let iter = 0; iter < this.maxToolIters; iter++) {
6765
+ for (let iter = 0; ; iter++) {
6253
6766
  if (signal.aborted) {
6254
6767
  yield {
6255
6768
  turn: this._turn,
6256
6769
  role: "warning",
6257
- content: t("loop.abortedAtIter", { iter, cap: this.maxToolIters })
6770
+ content: t("loop.abortedAtIter", { iter })
6258
6771
  };
6259
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.]";
6260
6773
  this.appendAndPersist(buildSyntheticAssistantMessage(stoppedMsg, this.model));
@@ -6275,15 +6788,20 @@ ${reason}`
6275
6788
  content: t("loop.toolUploadStatus")
6276
6789
  };
6277
6790
  }
6278
- if (!warnedForIterBudget && iter >= warnAt) {
6279
- 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;
6280
6799
  yield {
6281
6800
  turn: this._turn,
6282
- role: "warning",
6283
- content: t("loop.toolBudgetWarning", { iter, cap: this.maxToolIters })
6801
+ role: "steer",
6802
+ content: steer
6284
6803
  };
6285
6804
  }
6286
- let messages = this.buildMessages(pendingUser);
6287
6805
  {
6288
6806
  const decision2 = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
6289
6807
  if (decision2.needsAction) {
@@ -6291,23 +6809,29 @@ ${reason}`
6291
6809
  yield {
6292
6810
  turn: this._turn,
6293
6811
  role: "status",
6294
- content: t("loop.preflightFoldStatus")
6812
+ content: t("loop.preflightTruncateStatus")
6295
6813
  };
6296
- const result = await this.context.fold(this.model);
6814
+ const result = this.context.mechanicalTruncate(this.model, {
6815
+ allowEmpty: pendingUser !== null
6816
+ });
6297
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;
6298
6821
  yield {
6299
6822
  turn: this._turn,
6300
6823
  role: "warning",
6301
- content: t("loop.preflightFolded", {
6302
- estimate: estimate.toLocaleString(),
6303
- ctxMax: ctxMax.toLocaleString(),
6304
- pct: Math.round(estimate / ctxMax * 100),
6305
- beforeMessages: result.beforeMessages,
6306
- afterMessages: result.afterMessages,
6307
- summaryChars: result.summaryChars
6308
- })
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
+ )
6309
6834
  };
6310
- messages = this.buildMessages(pendingUser);
6311
6835
  } else {
6312
6836
  yield {
6313
6837
  turn: this._turn,
@@ -6341,6 +6865,15 @@ ${reason}`
6341
6865
  thinking: thinkingModeForModel(callModel),
6342
6866
  reasoningEffort: this.reasoningEffort
6343
6867
  })) {
6868
+ if (chunk.reasoningDelta) {
6869
+ reasoningContent += chunk.reasoningDelta;
6870
+ yield {
6871
+ turn: this._turn,
6872
+ role: "assistant_delta",
6873
+ content: "",
6874
+ reasoningDelta: chunk.reasoningDelta
6875
+ };
6876
+ }
6344
6877
  if (chunk.contentDelta) {
6345
6878
  assistantContent += chunk.contentDelta;
6346
6879
  if (bufferForEscalation && !escalationBufFlushed) {
@@ -6365,15 +6898,6 @@ ${reason}`
6365
6898
  };
6366
6899
  }
6367
6900
  }
6368
- if (chunk.reasoningDelta) {
6369
- reasoningContent += chunk.reasoningDelta;
6370
- yield {
6371
- turn: this._turn,
6372
- role: "assistant_delta",
6373
- content: "",
6374
- reasoningDelta: chunk.reasoningDelta
6375
- };
6376
- }
6377
6901
  if (chunk.toolCallDelta) {
6378
6902
  const d = chunk.toolCallDelta;
6379
6903
  const cur = callBuf.get(d.index) ?? {
@@ -6464,10 +6988,6 @@ ${reason}`
6464
6988
  this.modelForCurrentCall(),
6465
6989
  usage ?? new Usage()
6466
6990
  );
6467
- if (pendingUser !== null) {
6468
- this.appendAndPersist({ role: "user", content: pendingUser });
6469
- pendingUser = null;
6470
- }
6471
6991
  this.scratch.reasoning = reasoningContent || null;
6472
6992
  const { calls: repairedCalls, report } = this.repair.process(
6473
6993
  toolCalls,
@@ -6637,26 +7157,15 @@ ${reason}`
6637
7157
  role: "tool",
6638
7158
  tool_call_id: call.id ?? "",
6639
7159
  name,
6640
- content: result
6641
- });
6642
- if (this.noteToolFailureSignal(result)) {
6643
- yield {
6644
- turn: this._turn,
6645
- role: "warning",
6646
- content: t("loop.autoEscalation", {
6647
- model: ESCALATION_MODEL,
6648
- breakdown: this._turnFailures.formatBreakdown(),
6649
- fallback: this.model
6650
- })
6651
- };
6652
- }
6653
- if (this.noteReadOnlyToolCall(call)) {
7160
+ content: result
7161
+ });
7162
+ if (this.noteToolFailureSignal(result)) {
6654
7163
  yield {
6655
7164
  turn: this._turn,
6656
7165
  role: "warning",
6657
- content: t("loop.readOnlyLoopEscalation", {
7166
+ content: t("loop.autoEscalation", {
6658
7167
  model: ESCALATION_MODEL,
6659
- n: this._readOnlyLoop.currentStreak,
7168
+ breakdown: this._turnFailures.formatBreakdown(),
6660
7169
  fallback: this.model
6661
7170
  })
6662
7171
  };
@@ -6672,7 +7181,6 @@ ${reason}`
6672
7181
  }
6673
7182
  }
6674
7183
  }
6675
- yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "budget" });
6676
7184
  }
6677
7185
  summaryContext() {
6678
7186
  return {
@@ -6681,8 +7189,7 @@ ${reason}`
6681
7189
  buildMessages: () => this.buildMessages(null),
6682
7190
  appendAndPersist: (m) => this.appendAndPersist(m),
6683
7191
  recordStats: (model, usage) => this.stats.record(this._turn, model, usage),
6684
- turn: this._turn,
6685
- maxToolIters: this.maxToolIters
7192
+ turn: this._turn
6686
7193
  };
6687
7194
  }
6688
7195
  async run(userInput, onEvent) {
@@ -6717,7 +7224,7 @@ function resolveFailureThreshold(raw, fallback) {
6717
7224
  // src/at-mentions.ts
6718
7225
  import { existsSync as existsSync4, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
6719
7226
  import { readdir, stat } from "fs/promises";
6720
- import { isAbsolute, join as join5, relative, resolve } from "path";
7227
+ import { isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
6721
7228
 
6722
7229
  // src/gitignore.ts
6723
7230
  import { readFileSync as readFileSync5 } from "fs";
@@ -6772,7 +7279,7 @@ function listFilesSync(root, opts = {}) {
6772
7279
  function listFilesWithStatsSync(root, opts = {}) {
6773
7280
  const maxResults = Math.max(1, opts.maxResults ?? 2e3);
6774
7281
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
6775
- const rootAbs = resolve(root);
7282
+ const rootAbs = resolve2(root);
6776
7283
  const respectGi = opts.respectGitignore !== false;
6777
7284
  const out = [];
6778
7285
  const walk2 = (dirAbs, dirRel, layers) => {
@@ -6836,7 +7343,7 @@ async function listFilesWithStatsAsync(root, opts = {}) {
6836
7343
  async function walkFilesStream(root, opts) {
6837
7344
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
6838
7345
  const respectGi = opts.respectGitignore !== false;
6839
- const rootAbs = resolve(root);
7346
+ const rootAbs = resolve2(root);
6840
7347
  const progressGap = Math.max(0, opts.progressIntervalMs ?? 100);
6841
7348
  let scanned = 0;
6842
7349
  let halted = false;
@@ -6914,10 +7421,10 @@ async function flushFiles(ents, dirAbs, dirRel, layers, emit) {
6914
7421
  async function listDirectory(root, relDir, opts = {}) {
6915
7422
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
6916
7423
  const respectGi = opts.respectGitignore !== false;
6917
- const rootAbs = resolve(root);
6918
- const dirAbs = resolve(rootAbs, relDir);
7424
+ const rootAbs = resolve2(root);
7425
+ const dirAbs = resolve2(rootAbs, relDir);
6919
7426
  const rel = relative(rootAbs, dirAbs);
6920
- if (rel.startsWith("..") || isAbsolute(rel)) return [];
7427
+ if (rel.startsWith("..") || isAbsolute2(rel)) return [];
6921
7428
  const layers = [];
6922
7429
  if (respectGi) {
6923
7430
  const segs = rel ? rel.split(/[\\/]/) : [];
@@ -7080,7 +7587,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
7080
7587
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
7081
7588
  const maxDirEntries = Math.max(1, opts.maxDirEntries ?? DEFAULT_AT_DIR_MAX_ENTRIES);
7082
7589
  const fs5 = opts.fs ?? defaultFs;
7083
- const root = resolve(rootDir);
7590
+ const root = resolve2(rootDir);
7084
7591
  const seen = /* @__PURE__ */ new Map();
7085
7592
  const expansions = [];
7086
7593
  const dirListings = /* @__PURE__ */ new Map();
@@ -7124,12 +7631,12 @@ ${blocks.join("\n\n")}`;
7124
7631
  return { text: augmented, expansions };
7125
7632
  }
7126
7633
  function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs5, dirListings) {
7127
- if (isAbsolute(rawPath)) {
7634
+ if (isAbsolute2(rawPath)) {
7128
7635
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
7129
7636
  }
7130
- const resolved = resolve(root, rawPath);
7637
+ const resolved = resolve2(root, rawPath);
7131
7638
  const rel = relative(root, resolved);
7132
- if (rel.startsWith("..") || isAbsolute(rel)) {
7639
+ if (rel.startsWith("..") || isAbsolute2(rel)) {
7133
7640
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
7134
7641
  }
7135
7642
  if (!fs5.exists(resolved)) {
@@ -7157,7 +7664,7 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs5, dirListings
7157
7664
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
7158
7665
  }
7159
7666
  function readSafe(root, rawPath, fs5) {
7160
- const resolved = resolve(root, rawPath);
7667
+ const resolved = resolve2(root, rawPath);
7161
7668
  try {
7162
7669
  return fs5.read(resolved);
7163
7670
  } catch {
@@ -7267,7 +7774,7 @@ import {
7267
7774
  writeFileSync as writeFileSync4
7268
7775
  } from "fs";
7269
7776
  import { homedir as homedir5 } from "os";
7270
- import { join as join8, resolve as resolve3 } from "path";
7777
+ import { join as join8, resolve as resolve4 } from "path";
7271
7778
 
7272
7779
  // src/frontmatter.ts
7273
7780
  var KEY_RE = /^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/;
@@ -7317,9 +7824,18 @@ function parseFrontmatter(raw) {
7317
7824
  }
7318
7825
 
7319
7826
  // src/skills.ts
7320
- 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";
7321
7837
  import { homedir as homedir4 } from "os";
7322
- 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";
7323
7839
 
7324
7840
  // src/prompt-fragments.ts
7325
7841
  var TUI_FORMATTING_RULES = `Formatting (rendered in a TUI with a real markdown renderer):
@@ -7367,17 +7883,22 @@ function parseAllowedTools(raw) {
7367
7883
  var SkillStore = class {
7368
7884
  homeDir;
7369
7885
  projectRoot;
7886
+ customSkillPaths;
7370
7887
  disableBuiltins;
7371
7888
  constructor(opts = {}) {
7372
7889
  this.homeDir = opts.homeDir ?? homedir4();
7373
- 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
+ );
7374
7895
  this.disableBuiltins = opts.disableBuiltins === true;
7375
7896
  }
7376
7897
  /** True iff this store was configured with a project root. */
7377
7898
  hasProjectScope() {
7378
7899
  return this.projectRoot !== void 0;
7379
7900
  }
7380
- /** 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. */
7381
7902
  roots() {
7382
7903
  const out = [];
7383
7904
  if (this.projectRoot) {
@@ -7385,15 +7906,24 @@ var SkillStore = class {
7385
7906
  dir: join7(this.projectRoot, ".reasonix", SKILLS_DIRNAME),
7386
7907
  scope: "project"
7387
7908
  });
7909
+ out.push({
7910
+ dir: join7(this.projectRoot, ".agents", SKILLS_DIRNAME),
7911
+ scope: "project"
7912
+ });
7388
7913
  }
7914
+ for (const dir of this.customSkillPaths) out.push({ dir, scope: "custom" });
7389
7915
  out.push({ dir: join7(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
7390
- 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");
7391
7921
  }
7392
- /** 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. */
7393
7923
  list() {
7394
7924
  const byName = /* @__PURE__ */ new Map();
7395
- for (const { dir, scope } of this.roots()) {
7396
- if (!existsSync6(dir)) continue;
7925
+ for (const { dir, scope, status } of this.roots()) {
7926
+ if (status !== "ok") continue;
7397
7927
  let entries;
7398
7928
  try {
7399
7929
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -7445,8 +7975,8 @@ var SkillStore = class {
7445
7975
  /** Resolve one skill by name. Returns `null` if not found or malformed. */
7446
7976
  read(name) {
7447
7977
  if (!isValidSkillName(name)) return null;
7448
- for (const { dir, scope } of this.roots()) {
7449
- if (!existsSync6(dir)) continue;
7978
+ for (const { dir, scope, status } of this.roots()) {
7979
+ if (status !== "ok") continue;
7450
7980
  const dirCandidate = join7(dir, name, SKILL_FILE);
7451
7981
  if (existsSync6(dirCandidate) && statSync4(dirCandidate).isFile()) {
7452
7982
  return this.parse(dirCandidate, name, scope);
@@ -7498,6 +8028,34 @@ var SkillStore = class {
7498
8028
  };
7499
8029
  }
7500
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
+ }
7501
8059
  function parseRunAs(raw) {
7502
8060
  return raw?.trim() === "subagent" ? "subagent" : "inline";
7503
8061
  }
@@ -7749,7 +8307,7 @@ function sanitizeMemoryName(raw) {
7749
8307
  return trimmed;
7750
8308
  }
7751
8309
  function projectHash(rootDir) {
7752
- const abs = resolve3(rootDir);
8310
+ const abs = resolve4(rootDir);
7753
8311
  return createHash2("sha1").update(abs).digest("hex").slice(0, 16);
7754
8312
  }
7755
8313
  function scopeDir(opts) {
@@ -7799,7 +8357,7 @@ var MemoryStore = class {
7799
8357
  projectRoot;
7800
8358
  constructor(opts = {}) {
7801
8359
  this.homeDir = opts.homeDir ?? join8(homedir5(), ".reasonix");
7802
- this.projectRoot = opts.projectRoot ? resolve3(opts.projectRoot) : void 0;
8360
+ this.projectRoot = opts.projectRoot ? resolve4(opts.projectRoot) : void 0;
7803
8361
  }
7804
8362
  /** Directory this store writes `scope` files into, creating it if needed. */
7805
8363
  dir(scope) {
@@ -8040,11 +8598,17 @@ function applyUserMemory(basePrompt, opts = {}) {
8040
8598
  }
8041
8599
  return parts.join("\n");
8042
8600
  }
8043
- function applyMemoryStack(basePrompt, rootDir) {
8601
+ function applyMemoryStack(basePrompt, rootDir, opts = {}) {
8602
+ const homeDir = opts.homeDir;
8603
+ const cfg = opts.cfg;
8044
8604
  const withProject = applyProjectMemory(basePrompt, rootDir);
8045
- const withGlobal = applyGlobalReasonixMemory(withProject);
8046
- const withMemory = applyUserMemory(withGlobal, { projectRoot: rootDir });
8047
- 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 });
8048
8612
  }
8049
8613
 
8050
8614
  // src/tools/filesystem.ts
@@ -8052,6 +8616,51 @@ import { promises as fs4 } from "fs";
8052
8616
  import * as pathMod5 from "path";
8053
8617
  import picomatch3 from "picomatch";
8054
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
+
8055
8664
  // src/tools/fs/edit.ts
8056
8665
  import { promises as fs } from "fs";
8057
8666
  import * as pathMod from "path";
@@ -8282,6 +8891,18 @@ var RUST_DECL_RE = /^(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|str
8282
8891
  var RUST_IMPL_RE = /^(?:unsafe\s+)?impl(?:\s*<[^>]+>)?\s+(?:[^{]+\s+for\s+)?(\w+)/;
8283
8892
  var MD_HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
8284
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
+ ];
8285
8906
  var EXT_TO_LANG = {
8286
8907
  ".ts": "ts",
8287
8908
  ".tsx": "ts",
@@ -8297,7 +8918,10 @@ var EXT_TO_LANG = {
8297
8918
  ".rs": "rust",
8298
8919
  ".md": "md",
8299
8920
  ".markdown": "md",
8300
- ".mdx": "md"
8921
+ ".mdx": "md",
8922
+ ".proto": "proto",
8923
+ ".txt": "txt",
8924
+ ".text": "txt"
8301
8925
  };
8302
8926
  function extractOutline(filename, lines) {
8303
8927
  const ext = pathMod3.extname(filename).toLowerCase();
@@ -8314,6 +8938,10 @@ function extractOutline(filename, lines) {
8314
8938
  return extractRust(lines);
8315
8939
  case "md":
8316
8940
  return extractMarkdown(lines);
8941
+ case "proto":
8942
+ return extractProto(lines);
8943
+ case "txt":
8944
+ return extractText(lines);
8317
8945
  }
8318
8946
  }
8319
8947
  function extractTs(lines) {
@@ -8365,6 +8993,36 @@ function extractRust(lines) {
8365
8993
  }
8366
8994
  return out;
8367
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
+ }
8368
9026
  function extractMarkdown(lines) {
8369
9027
  const out = [];
8370
9028
  let inFence = false;
@@ -8589,8 +9247,8 @@ async function searchContent(ctx, startAbs, args) {
8589
9247
  for (let i = realStart; i <= winEnd; i++) {
8590
9248
  const line = lines[i];
8591
9249
  const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
8592
- const sep2 = hitSet.has(i) ? ":" : "-";
8593
- if (!pushLine(`${rel}:${i + 1}${sep2} ${display}`)) return;
9250
+ const sep3 = hitSet.has(i) ? ":" : "-";
9251
+ if (!pushLine(`${rel}:${i + 1}${sep3} ${display}`)) return;
8594
9252
  }
8595
9253
  prevWindowEnd = winEnd;
8596
9254
  }
@@ -8612,17 +9270,25 @@ async function searchContent(ctx, startAbs, args) {
8612
9270
  }
8613
9271
 
8614
9272
  // src/tools/filesystem.ts
8615
- var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
9273
+ var DEFAULT_OUTLINE_THRESHOLD_BYTES = 512 * 1024;
8616
9274
  var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
8617
- var DEFAULT_AUTO_PREVIEW_LINES = 200;
8618
- var AUTO_PREVIEW_HEAD_LINES = 80;
8619
- var AUTO_PREVIEW_TAIL_LINES = 40;
8620
- var OUTLINE_MAX_ENTRIES2 = 30;
9275
+ var HARD_MAX_FILE_BYTES = 32 * 1024 * 1024;
9276
+ var OUTLINE_HEAD_LINES = 80;
8621
9277
  var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
8622
9278
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
8623
9279
  function displayRel4(rootDir, full) {
8624
9280
  return pathMod5.relative(rootDir, full).replaceAll("\\", "/");
8625
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
+ }
8626
9292
  var GLOB_METACHARS = /[*?{[]/;
8627
9293
  function compileNameFilter(filter) {
8628
9294
  if (!filter) return null;
@@ -8639,24 +9305,45 @@ function isLikelyBinaryByName(name) {
8639
9305
  if (dot < 0) return false;
8640
9306
  return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
8641
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
+ }
8642
9321
  function registerFilesystemTools(registry, opts) {
8643
9322
  const rootDir = pathMod5.resolve(opts.rootDir);
8644
9323
  const allowWriting = opts.allowWriting !== false;
8645
- const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
9324
+ const outlineThresholdBytes = opts.outlineThresholdBytes ?? DEFAULT_OUTLINE_THRESHOLD_BYTES;
8646
9325
  const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
8647
9326
  const normRoot = pathMod5.resolve(rootDir);
8648
9327
  const sessionApproved = /* @__PURE__ */ new Set();
8649
- const inflightGate = /* @__PURE__ */ new Map();
8650
- function pathIsUnder(child, parent) {
8651
- const rel = pathMod5.relative(parent, child);
8652
- return rel === "" || !rel.startsWith("..") && !pathMod5.isAbsolute(rel);
8653
- }
8654
- function looksLikeAbsoluteSystemPath(raw) {
8655
- if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
8656
- return /^\/(?:home|Users|etc|var|opt|tmp|usr|mnt|Library|Volumes|proc|sys|dev|run|srv|media|Applications|System|root|boot|private)(?:[/\\]|$)/.test(
8657
- raw
8658
- );
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}`;
8659
9345
  }
9346
+ const inflightGate = /* @__PURE__ */ new Map();
8660
9347
  async function ensureOutsideSandboxAllowed(abs, intent, toolName, ctx) {
8661
9348
  for (const dir of loadProjectPathAllowed(rootDir)) {
8662
9349
  if (pathIsUnder(abs, dir)) return;
@@ -8721,11 +9408,11 @@ function registerFilesystemTools(registry, opts) {
8721
9408
  registry.register({
8722
9409
  name: "read_file",
8723
9410
  parallelSafe: true,
8724
- description: `Read a file under the sandbox root. To save context, PREFER to scope the read instead of pulling the whole file:
8725
- - head: N \u2192 first N lines (imports, public API, small configs)
8726
- - tail: N \u2192 last N lines (recently-added code, log tails)
8727
- - range: "A-B" \u2192 inclusive line range A..B, 1-indexed (e.g. "120-180" around an edit site)
8728
- 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.`,
8729
9416
  readOnly: true,
8730
9417
  stormExempt: true,
8731
9418
  parameters: {
@@ -8743,22 +9430,31 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
8743
9430
  },
8744
9431
  fn: async (args, ctx) => {
8745
9432
  const abs = await safePath(args.path, "read_file", ctx);
9433
+ const rel = displayRel4(rootDir, abs);
8746
9434
  const fh = await fs4.open(abs, "r");
8747
9435
  let raw;
9436
+ let sizeBytes;
8748
9437
  try {
8749
9438
  const stat2 = await fh.stat();
8750
9439
  if (stat2.isDirectory()) {
8751
9440
  throw new Error(`not a file: ${args.path} (it's a directory)`);
8752
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
+ }
8753
9452
  raw = await fh.readFile();
8754
9453
  } finally {
8755
9454
  await fh.close();
8756
9455
  }
8757
- if (raw.length > maxReadBytes) {
8758
- const headBytes = raw.slice(0, maxReadBytes).toString("utf8");
8759
- return `${headBytes}
8760
-
8761
- [\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.]`;
8762
9458
  }
8763
9459
  const text = raw.toString("utf8");
8764
9460
  let lines = text.split(/\r?\n/);
@@ -8770,8 +9466,8 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
8770
9466
  const end = Math.min(totalLines, Math.max(start, rawEnd ?? totalLines));
8771
9467
  const slice = lines.slice(start - 1, end);
8772
9468
  const label = `[range ${start}-${end} of ${totalLines} lines]`;
8773
- return `${label}
8774
- ${slice.join("\n")}`;
9469
+ return withSubdirMemory(abs, `${label}
9470
+ ${slice.join("\n")}`);
8775
9471
  }
8776
9472
  if (typeof args.head === "number" && args.head > 0) {
8777
9473
  const count = Math.min(args.head, totalLines);
@@ -8779,7 +9475,7 @@ ${slice.join("\n")}`;
8779
9475
  const marker = count < totalLines ? `
8780
9476
 
8781
9477
  [\u2026head ${count} of ${totalLines} lines \u2014 call again with range / tail for more]` : "";
8782
- return slice.join("\n") + marker;
9478
+ return withSubdirMemory(abs, slice.join("\n") + marker);
8783
9479
  }
8784
9480
  if (typeof args.tail === "number" && args.tail > 0) {
8785
9481
  const count = Math.min(args.tail, totalLines);
@@ -8787,25 +9483,26 @@ ${slice.join("\n")}`;
8787
9483
  const marker = count < totalLines ? `[\u2026tail ${count} of ${totalLines} lines \u2014 call again with range / head for more]
8788
9484
 
8789
9485
  ` : "";
8790
- return marker + slice.join("\n");
9486
+ return withSubdirMemory(abs, marker + slice.join("\n"));
8791
9487
  }
8792
- if (totalLines <= DEFAULT_AUTO_PREVIEW_LINES) return lines.join("\n");
8793
- const head = lines.slice(0, AUTO_PREVIEW_HEAD_LINES).join("\n");
8794
- const tail = lines.slice(totalLines - AUTO_PREVIEW_TAIL_LINES).join("\n");
8795
- 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");
8796
9490
  const outline = formatOutline(extractOutline(abs, lines));
8797
9491
  const parts = [
8798
- `[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]`,
8799
9495
  head
8800
9496
  ];
8801
9497
  if (outline) parts.push("", outline);
8802
9498
  parts.push(
8803
- `
8804
- [\u2026 ${omitted} lines omitted \u2014 call read_file again with range:"A-B" (1-indexed) or head / tail to get the middle]
8805
- `,
8806
- 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]`
8807
9504
  );
8808
- return parts.join("\n");
9505
+ return withSubdirMemory(abs, parts.join("\n"));
8809
9506
  }
8810
9507
  });
8811
9508
  registry.register({
@@ -9846,7 +10543,7 @@ var VERIFY_SYSTEM = `You are a verify subagent. Narrow check \u2014 return YES /
9846
10543
  How to operate:
9847
10544
  - Read only what's needed to verify the specific claim. No exploration past the claim.
9848
10545
  - Use search_content / read_file to confirm the exact behavior, type, or call site in question.
9849
- - 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.
9850
10547
 
9851
10548
  Final answer:
9852
10549
  - Lead with VERIFIED / NOT VERIFIED / INCONCLUSIVE.
@@ -9857,8 +10554,8 @@ ${NEGATIVE_CLAIM_RULE}
9857
10554
 
9858
10555
  ${TUI_FORMATTING_RULES}`;
9859
10556
  var TYPES = {
9860
- explore: { system: EXPLORE_SYSTEM, maxToolIters: 20 },
9861
- verify: { system: VERIFY_SYSTEM, maxToolIters: 8 }
10557
+ explore: { system: EXPLORE_SYSTEM },
10558
+ verify: { system: VERIFY_SYSTEM }
9862
10559
  };
9863
10560
  var SUBAGENT_TYPE_NAMES = Object.freeze(
9864
10561
  Object.keys(TYPES)
@@ -9886,13 +10583,6 @@ ${NEGATIVE_CLAIM_RULE}
9886
10583
 
9887
10584
  ${TUI_FORMATTING_RULES}`;
9888
10585
  var DEFAULT_MAX_RESULT_CHARS2 = 8e3;
9889
- var DEFAULT_MAX_ITERS = 16;
9890
- var MIN_MAX_ITERS = 1;
9891
- var MAX_MAX_ITERS = 32;
9892
- var BUDGET_WARN_THRESHOLD = 3;
9893
- function budgetParagraph(maxToolIters) {
9894
- 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.`;
9895
- }
9896
10586
  var DEFAULT_SUBAGENT_MODEL = "deepseek-v4-flash";
9897
10587
  var DEFAULT_SUBAGENT_EFFORT = "high";
9898
10588
  var SUBAGENT_TOOL_NAME = "spawn_subagent";
@@ -9911,12 +10601,12 @@ function subagentBudgetHint(spawnCount, totalTokens) {
9911
10601
  }
9912
10602
  async function spawnSubagent(opts) {
9913
10603
  const model = opts.model ?? DEFAULT_SUBAGENT_MODEL;
9914
- const maxToolIters = opts.maxToolIters ?? DEFAULT_MAX_ITERS;
9915
10604
  const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
9916
10605
  const sink = opts.sink;
9917
10606
  const skillName = opts.skillName;
9918
- const startedAt = Date.now();
9919
10607
  const runId = nextRunId();
10608
+ const sessionName = opts.resumeSession ?? `subagent-${runId}-${timestampSuffix()}`;
10609
+ const startedAt = Date.now();
9920
10610
  const taskPreview = opts.task.length > 30 ? `${opts.task.slice(0, 30)}\u2026` : opts.task;
9921
10611
  sink?.current?.({
9922
10612
  kind: "start",
@@ -9963,26 +10653,8 @@ async function spawnSubagent(opts) {
9963
10653
  new Set(opts.allowedTools),
9964
10654
  NEVER_INHERITED_TOOLS
9965
10655
  ) : forkRegistryExcluding(opts.parentRegistry, NEVER_INHERITED_TOOLS);
9966
- let dispatchCount = 0;
9967
- childTools.setResultAugmenter((_name, _args, result) => {
9968
- dispatchCount++;
9969
- const remaining = maxToolIters - dispatchCount;
9970
- if (remaining <= 0) {
9971
- return `${result}
9972
-
9973
- [budget: 0 of ${maxToolIters} tool calls left \u2014 finalize NOW; the next tool call will be refused]`;
9974
- }
9975
- if (remaining <= BUDGET_WARN_THRESHOLD) {
9976
- return `${result}
9977
-
9978
- [budget: ${remaining} of ${maxToolIters} tool call${remaining === 1 ? "" : "s"} left \u2014 wrap up soon]`;
9979
- }
9980
- return result;
9981
- });
9982
10656
  const childPrefix = new ImmutablePrefix({
9983
- system: `${opts.system}
9984
-
9985
- ${budgetParagraph(maxToolIters)}`,
10657
+ system: opts.system,
9986
10658
  toolSpecs: childTools.specs()
9987
10659
  });
9988
10660
  const childLoop = new CacheFirstLoop({
@@ -9994,12 +10666,9 @@ ${budgetParagraph(maxToolIters)}`,
9994
10666
  // task is already narrow by construction, and `high` cuts output
9995
10667
  // tokens substantially vs `max`.
9996
10668
  reasoningEffort: DEFAULT_SUBAGENT_EFFORT,
9997
- maxToolIters,
9998
10669
  hooks: [],
9999
- // Streaming on so the parent UI can flip the "summarising" phase the
10000
- // moment the model starts emitting the final answer (first assistant_delta
10001
- // after the last tool result, before assistant_final lands).
10002
- stream: true
10670
+ stream: true,
10671
+ session: sessionName
10003
10672
  });
10004
10673
  const onParentAbort = () => childLoop.abort();
10005
10674
  if (opts.parentSignal?.aborted) {
@@ -10011,6 +10680,7 @@ ${budgetParagraph(maxToolIters)}`,
10011
10680
  let errorMessage;
10012
10681
  let toolIter = 0;
10013
10682
  let summarisingEmitted = false;
10683
+ let forcedSummaryFired = false;
10014
10684
  try {
10015
10685
  for await (const ev of childLoop.step(opts.task)) {
10016
10686
  sink?.current?.({ kind: "inner", runId, task: taskPreview, skillName, model, inner: ev });
@@ -10042,7 +10712,12 @@ ${budgetParagraph(maxToolIters)}`,
10042
10712
  }
10043
10713
  if (ev.role === "assistant_final") {
10044
10714
  if (ev.forcedSummary) {
10045
- 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
+ }
10046
10721
  } else {
10047
10722
  final = ev.content ?? "";
10048
10723
  }
@@ -10081,7 +10756,7 @@ ${budgetParagraph(maxToolIters)}`,
10081
10756
  usage
10082
10757
  });
10083
10758
  return {
10084
- success: !errorMessage,
10759
+ success: !errorMessage && !forcedSummaryFired,
10085
10760
  output: errorMessage ? "" : truncated,
10086
10761
  error: errorMessage,
10087
10762
  turns,
@@ -10090,7 +10765,8 @@ ${budgetParagraph(maxToolIters)}`,
10090
10765
  costUsd: costUsd2,
10091
10766
  model,
10092
10767
  skillName,
10093
- usage
10768
+ usage,
10769
+ forcedSummary: forcedSummaryFired || void 0
10094
10770
  };
10095
10771
  }
10096
10772
  function aggregateChildUsage(loop) {
@@ -10105,6 +10781,18 @@ function aggregateChildUsage(loop) {
10105
10781
  return agg;
10106
10782
  }
10107
10783
  function formatSubagentResult(r) {
10784
+ if (r.forcedSummary) {
10785
+ return JSON.stringify({
10786
+ success: false,
10787
+ partial: true,
10788
+ output: r.output,
10789
+ turns: r.turns,
10790
+ tool_iters: r.toolIters,
10791
+ elapsed_ms: r.elapsedMs,
10792
+ cost_usd: r.costUsd,
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."
10794
+ });
10795
+ }
10108
10796
  if (!r.success) {
10109
10797
  return JSON.stringify({
10110
10798
  success: false,
@@ -10127,7 +10815,6 @@ function registerSubagentTool(parentRegistry, opts) {
10127
10815
  const baseSystem = opts.defaultSystem ?? SUBAGENT_BASE_SYSTEM;
10128
10816
  const defaultSystemBase = opts.projectRoot ? applyProjectMemory(baseSystem, opts.projectRoot) : baseSystem;
10129
10817
  const defaultModel = opts.defaultModel ?? DEFAULT_SUBAGENT_MODEL;
10130
- const maxToolIters = opts.maxToolIters ?? DEFAULT_MAX_ITERS;
10131
10818
  const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
10132
10819
  const sink = opts.sink;
10133
10820
  let sessionSpawnCount = 0;
@@ -10135,33 +10822,31 @@ function registerSubagentTool(parentRegistry, opts) {
10135
10822
  parentRegistry.register({
10136
10823
  name: SUBAGENT_TOOL_NAME,
10137
10824
  parallelSafe: true,
10138
- 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 spawn pays a fresh 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. Keep tasks focused; the subagent has a stricter iter budget than you do.",
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).",
10139
10826
  parameters: {
10140
10827
  type: "object",
10141
10828
  properties: {
10142
10829
  task: {
10143
10830
  type: "string",
10144
- description: "The subtask the subagent should perform. Be specific and self-contained \u2014 the subagent has none of your conversation context, only what you write here."
10831
+ description: 'The subtask the subagent should perform. Be specific and self-contained \u2014 the subagent has none of your conversation context, only what you write here. When resuming via `resume_session`, this becomes a continuation nudge (e.g. "finish what you started" or a delta instruction).'
10145
10832
  },
10146
10833
  system: {
10147
10834
  type: "string",
10148
- description: "Optional override for the subagent's system prompt. The default tells it to stay focused and return a concise answer; override only when the subtask needs a specialized persona."
10835
+ description: "Optional override for the subagent's system prompt. The default tells it to stay focused and return a concise answer; override only when the subtask needs a specialized persona. Ignored on resume \u2014 the prior session keeps its original system prompt for cache stability."
10149
10836
  },
10150
10837
  model: {
10151
10838
  type: "string",
10152
10839
  enum: ["deepseek-v4-flash", "deepseek-v4-pro"],
10153
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."
10154
10841
  },
10155
- max_iters: {
10156
- type: "integer",
10157
- minimum: MIN_MAX_ITERS,
10158
- maximum: MAX_MAX_ITERS,
10159
- description: `Cap on the subagent's tool-call iterations. Default 16 (or the type's default when 'type' is set). Hard range: ${MIN_MAX_ITERS}-${MAX_MAX_ITERS}; out-of-range values are clamped to the nearest end.`
10842
+ resume_session: {
10843
+ type: "string",
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."
10160
10845
  },
10161
10846
  type: {
10162
10847
  type: "string",
10163
10848
  enum: [...SUBAGENT_TYPE_NAMES],
10164
- 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."
10165
10850
  }
10166
10851
  },
10167
10852
  required: ["task"]
@@ -10178,20 +10863,26 @@ function registerSubagentTool(parentRegistry, opts) {
10178
10863
  const system = typeof args.system === "string" && args.system.trim().length > 0 ? args.system.trim() : typeSpec?.system ?? `${defaultSystemBase}
10179
10864
 
10180
10865
  ${escalationContract(model)}`;
10181
- const callerIters = clampMaxIters(args.max_iters);
10866
+ const resumeSession = typeof args.resume_session === "string" && args.resume_session.trim().length > 0 ? args.resume_session.trim() : void 0;
10182
10867
  const result = await spawnSubagent({
10183
10868
  client: opts.client,
10184
10869
  parentRegistry,
10185
10870
  system,
10186
10871
  task,
10187
10872
  model,
10188
- maxToolIters: callerIters ?? typeSpec?.maxToolIters ?? maxToolIters,
10189
10873
  maxResultChars,
10190
10874
  sink,
10191
- parentSignal: ctx?.signal
10875
+ parentSignal: ctx?.signal,
10876
+ resumeSession
10192
10877
  });
10193
10878
  sessionSpawnCount++;
10194
10879
  sessionSpawnTokens += result.usage.totalTokens;
10880
+ if (opts.onSpawnComplete) {
10881
+ try {
10882
+ opts.onSpawnComplete(result);
10883
+ } catch {
10884
+ }
10885
+ }
10195
10886
  const formatted = formatSubagentResult(result);
10196
10887
  const hint = subagentBudgetHint(sessionSpawnCount, sessionSpawnTokens);
10197
10888
  return hint ? `${formatted}
@@ -10200,13 +10891,6 @@ ${hint}` : formatted;
10200
10891
  });
10201
10892
  return parentRegistry;
10202
10893
  }
10203
- function clampMaxIters(raw) {
10204
- if (typeof raw !== "number" || !Number.isFinite(raw)) return void 0;
10205
- const n = Math.floor(raw);
10206
- if (n < MIN_MAX_ITERS) return MIN_MAX_ITERS;
10207
- if (n > MAX_MAX_ITERS) return MAX_MAX_ITERS;
10208
- return n;
10209
- }
10210
10894
  function forkRegistryExcluding(parent, exclude) {
10211
10895
  const child = new ToolRegistry();
10212
10896
  for (const spec of parent.specs()) {
@@ -10233,8 +10917,100 @@ function forkRegistryWithAllowList(parent, allow, alsoExclude) {
10233
10917
  return child;
10234
10918
  }
10235
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
+
10236
11012
  // src/tools/shell.ts
10237
- import * as pathMod9 from "path";
11013
+ import * as pathMod10 from "path";
10238
11014
 
10239
11015
  // src/tools/jobs.ts
10240
11016
  import { spawn as spawn2 } from "child_process";
@@ -10483,16 +11259,16 @@ ${job.output.slice(start)}`;
10483
11259
  let wakeOutput = null;
10484
11260
  if (waitFor === "output-or-exit") {
10485
11261
  racers.push(
10486
- new Promise((resolve10) => {
10487
- wakeOutput = resolve10;
10488
- job.outputWaiters.add(resolve10);
11262
+ new Promise((resolve13) => {
11263
+ wakeOutput = resolve13;
11264
+ job.outputWaiters.add(resolve13);
10489
11265
  })
10490
11266
  );
10491
11267
  }
10492
11268
  let timer = null;
10493
11269
  racers.push(
10494
- new Promise((resolve10) => {
10495
- timer = setTimeout(resolve10, timeoutMs);
11270
+ new Promise((resolve13) => {
11271
+ timer = setTimeout(resolve13, timeoutMs);
10496
11272
  })
10497
11273
  );
10498
11274
  await Promise.race(racers);
@@ -10601,12 +11377,13 @@ function latestOutputSince(before, after) {
10601
11377
 
10602
11378
  // src/tools/shell/exec.ts
10603
11379
  import { spawn as spawn4, spawnSync } from "child_process";
10604
- import { existsSync as existsSync8, statSync as statSync5 } from "fs";
10605
- import * as pathMod8 from "path";
11380
+ import { existsSync as existsSync9, statSync as statSync5 } from "fs";
11381
+ import * as pathMod9 from "path";
10606
11382
 
10607
11383
  // src/tools/shell-chain.ts
10608
11384
  import { spawn as spawn3 } from "child_process";
10609
11385
  import { closeSync, openSync } from "fs";
11386
+ import { devNull } from "os";
10610
11387
  import * as pathMod7 from "path";
10611
11388
  var UnsupportedSyntaxError = class extends Error {
10612
11389
  constructor(detail) {
@@ -10866,6 +11643,12 @@ async function runChain(chain, opts) {
10866
11643
  [\u2026 truncated ${output.length - opts.maxOutputChars} chars \u2026]` : output;
10867
11644
  return { exitCode: lastExit, output: truncated, timedOut };
10868
11645
  }
11646
+ function isNullDeviceAlias(target) {
11647
+ const lower = target.toLowerCase();
11648
+ if (lower === "/dev/null") return true;
11649
+ if (process.platform === "win32" && lower === "nul") return true;
11650
+ return false;
11651
+ }
10869
11652
  function openRedirects(redirects, cwd) {
10870
11653
  let stdinFd = null;
10871
11654
  let stdoutFd = null;
@@ -10874,7 +11657,7 @@ function openRedirects(redirects, cwd) {
10874
11657
  let bothFd = null;
10875
11658
  const toClose = [];
10876
11659
  const open = (target, flags) => {
10877
- const resolved = pathMod7.resolve(cwd, target);
11660
+ const resolved = isNullDeviceAlias(target) ? devNull : pathMod7.resolve(cwd, target);
10878
11661
  const fd = openSync(resolved, flags);
10879
11662
  toClose.push(fd);
10880
11663
  return fd;
@@ -10928,6 +11711,10 @@ async function runPipeGroup(segments, opts) {
10928
11711
  cwd: opts.cwd,
10929
11712
  shell: false,
10930
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",
10931
11718
  env,
10932
11719
  stdio: [stdinSpec, stdoutSpec, stderrSpec],
10933
11720
  ...spawnOverrides
@@ -10978,9 +11765,9 @@ async function runPipeGroup(segments, opts) {
10978
11765
  }
10979
11766
  const exits = await Promise.all(
10980
11767
  children.map(
10981
- (c) => new Promise((resolve10) => {
10982
- c.once("error", () => resolve10(null));
10983
- c.once("close", (code) => resolve10(code));
11768
+ (c) => new Promise((resolve13) => {
11769
+ c.once("error", () => resolve13(null));
11770
+ c.once("close", (code) => resolve13(code));
10984
11771
  })
10985
11772
  )
10986
11773
  );
@@ -11024,6 +11811,8 @@ var OutputBuffer = class {
11024
11811
  };
11025
11812
 
11026
11813
  // src/tools/shell/parse.ts
11814
+ import { homedir as homedir6 } from "os";
11815
+ import * as pathMod8 from "path";
11027
11816
  var BUILTIN_ALLOWLIST = [
11028
11817
  // Repo inspection
11029
11818
  "git status",
@@ -11200,7 +11989,65 @@ function tailHasRisky(tail, risky) {
11200
11989
  }
11201
11990
  return false;
11202
11991
  }
11203
- 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) {
11204
12051
  let argv;
11205
12052
  try {
11206
12053
  argv = tokenizeCommand(cmd);
@@ -11222,19 +12069,26 @@ function isAllowed(cmd, extra = []) {
11222
12069
  if (!match) continue;
11223
12070
  const risky = RISKY_ARGS[prefix];
11224
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;
11225
12079
  return true;
11226
12080
  }
11227
12081
  return false;
11228
12082
  }
11229
- function isCommandAllowed(cmd, extra = []) {
12083
+ function isCommandAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
11230
12084
  let chain;
11231
12085
  try {
11232
12086
  chain = parseCommandChain(cmd);
11233
12087
  } catch {
11234
12088
  return false;
11235
12089
  }
11236
- if (chain === null) return isAllowed(cmd, extra);
11237
- 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));
11238
12092
  }
11239
12093
 
11240
12094
  // src/tools/shell/exec.ts
@@ -11281,8 +12135,15 @@ async function runCommand(cmd, opts) {
11281
12135
  const spawnOpts = {
11282
12136
  cwd: opts.cwd,
11283
12137
  shell: false,
11284
- // no shell-expansion — see header comment
11285
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",
11286
12147
  // PYTHONIOENCODING + PYTHONUTF8 force any spawned Python child
11287
12148
  // (run_command running `python script.py`, etc.) to emit UTF-8
11288
12149
  // on stdout/stderr. Without this, Chinese-Windows defaults
@@ -11295,7 +12156,7 @@ async function runCommand(cmd, opts) {
11295
12156
  };
11296
12157
  const { bin, args, spawnOverrides } = prepareSpawn(argv, { env: normalizedEnv });
11297
12158
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
11298
- return await new Promise((resolve10, reject) => {
12159
+ return await new Promise((resolve13, reject) => {
11299
12160
  let child;
11300
12161
  try {
11301
12162
  child = spawn4(bin, args, effectiveSpawnOpts);
@@ -11349,7 +12210,7 @@ async function runCommand(cmd, opts) {
11349
12210
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
11350
12211
 
11351
12212
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
11352
- resolve10({ exitCode: code, output, timedOut });
12213
+ resolve13({ exitCode: code, output, timedOut });
11353
12214
  });
11354
12215
  });
11355
12216
  }
@@ -11371,16 +12232,16 @@ function resolveExecutable(cmd, opts = {}) {
11371
12232
  const platform = opts.platform ?? process.platform;
11372
12233
  if (platform !== "win32") return cmd;
11373
12234
  if (!cmd) return cmd;
11374
- if (cmd.includes("/") || cmd.includes("\\") || pathMod8.isAbsolute(cmd)) return cmd;
11375
- if (pathMod8.extname(cmd)) return cmd;
12235
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod9.isAbsolute(cmd)) return cmd;
12236
+ if (pathMod9.extname(cmd)) return cmd;
11376
12237
  const env = opts.env ?? process.env;
11377
12238
  const pathExt = (getEnvCaseInsensitive(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
11378
- const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod8.delimiter);
12239
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod9.delimiter);
11379
12240
  const pathDirs = (getEnvCaseInsensitive(env, "PATH") ?? "").split(delimiter2).filter(Boolean);
11380
12241
  const isFile = opts.isFile ?? defaultIsFile;
11381
12242
  for (const dir of pathDirs) {
11382
12243
  for (const ext of pathExt) {
11383
- const full = pathMod8.win32.join(dir, cmd + ext);
12244
+ const full = pathMod9.win32.join(dir, cmd + ext);
11384
12245
  if (isFile(full)) return full;
11385
12246
  }
11386
12247
  }
@@ -11434,7 +12295,7 @@ function mergeWindowsPathLike(values, delimiter2) {
11434
12295
  }
11435
12296
  function defaultIsFile(full) {
11436
12297
  try {
11437
- return existsSync8(full) && statSync5(full).isFile();
12298
+ return existsSync9(full) && statSync5(full).isFile();
11438
12299
  } catch {
11439
12300
  return false;
11440
12301
  }
@@ -11496,8 +12357,8 @@ function withUtf8Codepage(cmdline) {
11496
12357
  function isBareWindowsName(s) {
11497
12358
  if (!s) return false;
11498
12359
  if (s.includes("/") || s.includes("\\")) return false;
11499
- if (pathMod8.isAbsolute(s)) return false;
11500
- if (pathMod8.extname(s)) return false;
12360
+ if (pathMod9.isAbsolute(s)) return false;
12361
+ if (pathMod9.extname(s)) return false;
11501
12362
  return true;
11502
12363
  }
11503
12364
  function quoteForCmdExe(arg) {
@@ -11518,7 +12379,7 @@ var NeedsConfirmationError = class extends Error {
11518
12379
  }
11519
12380
  };
11520
12381
  function registerShellTools(registry, opts) {
11521
- const rootDir = pathMod9.resolve(opts.rootDir);
12382
+ const rootDir = pathMod10.resolve(opts.rootDir);
11522
12383
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
11523
12384
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
11524
12385
  const jobs = opts.jobs ?? new JobRegistry();
@@ -11538,7 +12399,7 @@ function registerShellTools(registry, opts) {
11538
12399
  if (isAllowAll()) return true;
11539
12400
  const cmd = typeof args?.command === "string" ? args.command.trim() : "";
11540
12401
  if (!cmd) return false;
11541
- return isCommandAllowed(cmd, getExtraAllowed());
12402
+ return isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths);
11542
12403
  },
11543
12404
  parameters: {
11544
12405
  type: "object",
@@ -11558,7 +12419,7 @@ function registerShellTools(registry, opts) {
11558
12419
  const cmd = args.command.trim();
11559
12420
  if (!cmd) throw new Error("run_command: empty command");
11560
12421
  const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
11561
- if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed())) {
12422
+ if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths)) {
11562
12423
  const gate = ctx?.confirmationGate ?? pauseGate;
11563
12424
  const choice = await gate.ask({
11564
12425
  kind: "run_command",
@@ -11584,7 +12445,7 @@ function registerShellTools(registry, opts) {
11584
12445
  });
11585
12446
  registry.register({
11586
12447
  name: "run_background",
11587
- 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.",
11588
12449
  parameters: {
11589
12450
  type: "object",
11590
12451
  properties: {
@@ -11592,6 +12453,10 @@ function registerShellTools(registry, opts) {
11592
12453
  type: "string",
11593
12454
  description: "Full command line. Same quoting rules as run_command (no pipes / redirects / chaining)."
11594
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
+ },
11595
12460
  waitSec: {
11596
12461
  type: "integer",
11597
12462
  description: "Max seconds to wait for startup before returning. 0..30, default 3. A ready-signal match short-circuits this."
@@ -11602,11 +12467,12 @@ function registerShellTools(registry, opts) {
11602
12467
  fn: async (args, ctx) => {
11603
12468
  const cmd = args.command.trim();
11604
12469
  if (!cmd) throw new Error("run_background: empty command");
11605
- if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed())) {
12470
+ const cwd = resolveCwdInsideRoot(rootDir, args.cwd);
12471
+ if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths)) {
11606
12472
  const gate = ctx?.confirmationGate ?? pauseGate;
11607
12473
  const choice = await gate.ask({
11608
12474
  kind: "run_background",
11609
- payload: { command: cmd, cwd: rootDir, waitSec: args.waitSec }
12475
+ payload: { command: cmd, cwd, waitSec: args.waitSec }
11610
12476
  });
11611
12477
  if (choice.type === "deny") {
11612
12478
  throw new Error(
@@ -11618,10 +12484,11 @@ function registerShellTools(registry, opts) {
11618
12484
  }
11619
12485
  }
11620
12486
  const result = await jobs.start(cmd, {
11621
- cwd: rootDir,
12487
+ cwd,
11622
12488
  waitSec: args.waitSec,
11623
12489
  signal: ctx?.signal
11624
12490
  });
12491
+ opts.onJobsChanged?.();
11625
12492
  return formatJobStart(result);
11626
12493
  }
11627
12494
  });
@@ -11683,6 +12550,7 @@ function registerShellTools(registry, opts) {
11683
12550
  waitFor: args.waitFor
11684
12551
  });
11685
12552
  if (!out) return `job ${args.jobId}: not found (use list_jobs)`;
12553
+ if (out.exited) opts.onJobsChanged?.();
11686
12554
  return {
11687
12555
  jobId: args.jobId,
11688
12556
  exited: out.exited,
@@ -11703,6 +12571,7 @@ function registerShellTools(registry, opts) {
11703
12571
  },
11704
12572
  fn: async (args) => {
11705
12573
  const rec = await jobs.stop(args.jobId);
12574
+ opts.onJobsChanged?.();
11706
12575
  if (!rec) return `job ${args.jobId}: not found`;
11707
12576
  return formatJobStop(rec);
11708
12577
  }
@@ -11722,6 +12591,18 @@ function registerShellTools(registry, opts) {
11722
12591
  });
11723
12592
  return registry;
11724
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
+ }
11725
12606
  function formatJobStart(r) {
11726
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]`;
11727
12608
  return r.preview ? `${header}
@@ -11770,6 +12651,7 @@ var DEFAULT_TOPK = 5;
11770
12651
  var FETCH_MAX_BYTES = 10 * 1024 * 1024;
11771
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";
11772
12653
  var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
12654
+ var METASO_ENDPOINT = "https://metaso.cn/api/v1";
11773
12655
  function searchStatusError(status) {
11774
12656
  if (status === 429) return t("webErrors.rateLimit429");
11775
12657
  if (status === 403) return t("webErrors.forbidden403");
@@ -11783,6 +12665,9 @@ function fetchStatusError(status, url) {
11783
12665
  return t("webErrors.fetchStatus", { status, url });
11784
12666
  }
11785
12667
  async function webSearch(query, opts = {}) {
12668
+ if (opts.engine === "metaso") {
12669
+ return searchMetaso(query, opts);
12670
+ }
11786
12671
  if (opts.engine === "searxng") {
11787
12672
  return searchSearxng(query, opts);
11788
12673
  }
@@ -11858,6 +12743,68 @@ async function searchSearxng(query, opts = {}) {
11858
12743
  }
11859
12744
  return results;
11860
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
+ }
11861
12808
  function parseSearxngHtmlResults(html) {
11862
12809
  const root = parseHtml(html);
11863
12810
  const results = [];
@@ -12076,7 +13023,7 @@ function registerWebTools(registry, opts = {}) {
12076
13023
  const maxFetchChars = opts.maxFetchChars ?? DEFAULT_FETCH_MAX_CHARS;
12077
13024
  registry.register({
12078
13025
  name: "web_search",
12079
- 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.",
12080
13027
  readOnly: true,
12081
13028
  parallelSafe: true,
12082
13029
  parameters: {
@@ -12141,12 +13088,12 @@ ${i + 1}. ${r.title}`);
12141
13088
  }
12142
13089
 
12143
13090
  // src/env.ts
12144
- import { readFileSync as readFileSync10 } from "fs";
12145
- import { resolve as resolve8 } from "path";
13091
+ import { readFileSync as readFileSync11 } from "fs";
13092
+ import { resolve as resolve11 } from "path";
12146
13093
  function loadDotenv(path2 = ".env") {
12147
13094
  let raw;
12148
13095
  try {
12149
- raw = readFileSync10(resolve8(process.cwd(), path2), "utf8");
13096
+ raw = readFileSync11(resolve11(process.cwd(), path2), "utf8");
12150
13097
  } catch {
12151
13098
  return;
12152
13099
  }
@@ -12165,7 +13112,7 @@ function loadDotenv(path2 = ".env") {
12165
13112
  }
12166
13113
 
12167
13114
  // src/transcript/log.ts
12168
- import { createWriteStream, readFileSync as readFileSync11 } from "fs";
13115
+ import { createWriteStream, readFileSync as readFileSync12 } from "fs";
12169
13116
  function recordFromLoopEvent(ev, extra) {
12170
13117
  const rec = {
12171
13118
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -12208,7 +13155,7 @@ function openTranscriptFile(path2, meta) {
12208
13155
  return stream;
12209
13156
  }
12210
13157
  function readTranscript(path2) {
12211
- const raw = readFileSync11(path2, "utf8");
13158
+ const raw = readFileSync12(path2, "utf8");
12212
13159
  return parseTranscript(raw);
12213
13160
  }
12214
13161
  function parseTranscript(raw) {
@@ -12595,25 +13542,25 @@ function truncate(s, n) {
12595
13542
  }
12596
13543
 
12597
13544
  // src/version.ts
12598
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
12599
- import { homedir as homedir6 } from "os";
12600
- 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";
12601
13548
  import { fileURLToPath as fileURLToPath2 } from "url";
12602
13549
  var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
12603
13550
  var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
12604
13551
  var LATEST_FETCH_TIMEOUT_MS = 2e3;
12605
13552
  function readPackageVersion() {
12606
13553
  try {
12607
- let dir = dirname6(fileURLToPath2(import.meta.url));
13554
+ let dir = dirname7(fileURLToPath2(import.meta.url));
12608
13555
  for (let i = 0; i < 6; i++) {
12609
- const p = join12(dir, "package.json");
12610
- if (existsSync9(p)) {
12611
- 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"));
12612
13559
  if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
12613
13560
  return pkg.version;
12614
13561
  }
12615
13562
  }
12616
- const parent = dirname6(dir);
13563
+ const parent = dirname7(dir);
12617
13564
  if (parent === dir) break;
12618
13565
  dir = parent;
12619
13566
  }
@@ -12623,11 +13570,11 @@ function readPackageVersion() {
12623
13570
  }
12624
13571
  var VERSION = readPackageVersion();
12625
13572
  function cachePath(homeDirOverride) {
12626
- return join12(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
13573
+ return join14(homeDirOverride ?? homedir7(), ".reasonix", "version-cache.json");
12627
13574
  }
12628
13575
  function readCache(homeDirOverride) {
12629
13576
  try {
12630
- const raw = readFileSync12(cachePath(homeDirOverride), "utf8");
13577
+ const raw = readFileSync13(cachePath(homeDirOverride), "utf8");
12631
13578
  const parsed = JSON.parse(raw);
12632
13579
  if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
12633
13580
  return parsed;
@@ -12639,7 +13586,7 @@ function readCache(homeDirOverride) {
12639
13586
  function writeCache(entry, homeDirOverride) {
12640
13587
  try {
12641
13588
  const p = cachePath(homeDirOverride);
12642
- mkdirSync5(dirname6(p), { recursive: true });
13589
+ mkdirSync5(dirname7(p), { recursive: true });
12643
13590
  writeFileSync5(p, JSON.stringify(entry), "utf8");
12644
13591
  } catch {
12645
13592
  }
@@ -12851,7 +13798,7 @@ var McpClient = class {
12851
13798
  const id = this.nextId++;
12852
13799
  const frame = { jsonrpc: "2.0", id, method, params };
12853
13800
  let abortHandler = null;
12854
- const promise = new Promise((resolve10, reject) => {
13801
+ const promise = new Promise((resolve13, reject) => {
12855
13802
  const timeout = setTimeout(() => {
12856
13803
  this.pending.delete(id);
12857
13804
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -12860,7 +13807,7 @@ var McpClient = class {
12860
13807
  );
12861
13808
  }, this.requestTimeoutMs);
12862
13809
  this.pending.set(id, {
12863
- resolve: resolve10,
13810
+ resolve: resolve13,
12864
13811
  reject,
12865
13812
  timeout
12866
13813
  });
@@ -12990,12 +13937,12 @@ var StdioTransport = class {
12990
13937
  }
12991
13938
  async send(message) {
12992
13939
  if (this.closed) throw new Error("MCP transport is closed");
12993
- return new Promise((resolve10, reject) => {
13940
+ return new Promise((resolve13, reject) => {
12994
13941
  const line = `${JSON.stringify(message)}
12995
13942
  `;
12996
13943
  this.child.stdin.write(line, "utf8", (err) => {
12997
13944
  if (err) reject(err);
12998
- else resolve10();
13945
+ else resolve13();
12999
13946
  });
13000
13947
  });
13001
13948
  }
@@ -13006,8 +13953,8 @@ var StdioTransport = class {
13006
13953
  continue;
13007
13954
  }
13008
13955
  if (this.closed) return;
13009
- const next = await new Promise((resolve10) => {
13010
- this.waiters.push(resolve10);
13956
+ const next = await new Promise((resolve13) => {
13957
+ this.waiters.push(resolve13);
13011
13958
  });
13012
13959
  if (next === null) return;
13013
13960
  yield next;
@@ -13080,8 +14027,8 @@ var SseTransport = class {
13080
14027
  constructor(opts) {
13081
14028
  this.url = opts.url;
13082
14029
  this.headers = opts.headers ?? {};
13083
- this.endpointReady = new Promise((resolve10, reject) => {
13084
- this.resolveEndpoint = resolve10;
14030
+ this.endpointReady = new Promise((resolve13, reject) => {
14031
+ this.resolveEndpoint = resolve13;
13085
14032
  this.rejectEndpoint = reject;
13086
14033
  });
13087
14034
  this.endpointReady.catch(() => void 0);
@@ -13108,8 +14055,8 @@ var SseTransport = class {
13108
14055
  continue;
13109
14056
  }
13110
14057
  if (this.closed) return;
13111
- const next = await new Promise((resolve10) => {
13112
- this.waiters.push(resolve10);
14058
+ const next = await new Promise((resolve13) => {
14059
+ this.waiters.push(resolve13);
13113
14060
  });
13114
14061
  if (next === null) return;
13115
14062
  yield next;
@@ -13295,8 +14242,8 @@ var StreamableHttpTransport = class {
13295
14242
  continue;
13296
14243
  }
13297
14244
  if (this.closed) return;
13298
- const next = await new Promise((resolve10) => {
13299
- this.waiters.push(resolve10);
14245
+ const next = await new Promise((resolve13) => {
14246
+ this.waiters.push(resolve13);
13300
14247
  });
13301
14248
  if (next === null) return;
13302
14249
  yield next;
@@ -13354,85 +14301,6 @@ var StreamableHttpTransport = class {
13354
14301
  }
13355
14302
  };
13356
14303
 
13357
- // src/mcp/shell-split.ts
13358
- function shellSplit(input) {
13359
- const tokens = [];
13360
- let cur = "";
13361
- let quote = null;
13362
- let i = 0;
13363
- const s = input;
13364
- while (i < s.length) {
13365
- const ch = s[i];
13366
- if (quote) {
13367
- if (ch === quote) {
13368
- quote = null;
13369
- i++;
13370
- continue;
13371
- }
13372
- if (ch === "\\" && quote === '"' && i + 1 < s.length) {
13373
- cur += s[i + 1];
13374
- i += 2;
13375
- continue;
13376
- }
13377
- cur += ch;
13378
- i++;
13379
- continue;
13380
- }
13381
- if (ch === '"' || ch === "'") {
13382
- quote = ch;
13383
- i++;
13384
- continue;
13385
- }
13386
- if (ch === " " || ch === " ") {
13387
- if (cur.length > 0) {
13388
- tokens.push(cur);
13389
- cur = "";
13390
- }
13391
- i++;
13392
- continue;
13393
- }
13394
- cur += ch;
13395
- i++;
13396
- }
13397
- if (quote) {
13398
- throw new Error(
13399
- `shellSplit: unterminated ${quote === '"' ? "double" : "single"} quote in input`
13400
- );
13401
- }
13402
- if (cur.length > 0) tokens.push(cur);
13403
- return tokens;
13404
- }
13405
-
13406
- // src/mcp/spec.ts
13407
- var NAME_PREFIX = /^([a-zA-Z_][a-zA-Z0-9_-]*)=(.*)$/;
13408
- var HTTP_URL = /^https?:\/\//i;
13409
- var STREAMABLE_PREFIX = /^streamable\+(https?:\/\/.+)$/i;
13410
- function parseMcpSpec(input) {
13411
- const trimmed = input.trim();
13412
- if (!trimmed) {
13413
- throw new Error("empty MCP spec");
13414
- }
13415
- const nameMatch = NAME_PREFIX.exec(trimmed);
13416
- const name = nameMatch ? nameMatch[1] : null;
13417
- const body = (nameMatch ? nameMatch[2] : trimmed).trim();
13418
- if (!body) {
13419
- throw new Error(`MCP spec has name but no command: ${input}`);
13420
- }
13421
- const streamMatch = STREAMABLE_PREFIX.exec(body);
13422
- if (streamMatch) {
13423
- return { transport: "streamable-http", name, url: streamMatch[1] };
13424
- }
13425
- if (HTTP_URL.test(body)) {
13426
- return { transport: "sse", name, url: body };
13427
- }
13428
- const argv = shellSplit(body);
13429
- if (argv.length === 0) {
13430
- throw new Error(`MCP spec has name but no command: ${input}`);
13431
- }
13432
- const [command, ...args] = argv;
13433
- return { transport: "stdio", name, command, args };
13434
- }
13435
-
13436
14304
  // src/mcp/inspect.ts
13437
14305
  async function inspectMcpServer(client) {
13438
14306
  const t0 = Date.now();
@@ -13468,18 +14336,18 @@ async function trySection(load) {
13468
14336
  // src/code/edit-blocks.ts
13469
14337
  import {
13470
14338
  closeSync as closeSync2,
13471
- existsSync as existsSync10,
14339
+ existsSync as existsSync11,
13472
14340
  fstatSync,
13473
14341
  ftruncateSync,
13474
14342
  mkdirSync as mkdirSync6,
13475
14343
  openSync as openSync2,
13476
- readFileSync as readFileSync13,
14344
+ readFileSync as readFileSync14,
13477
14345
  readSync,
13478
14346
  unlinkSync as unlinkSync3,
13479
14347
  writeFileSync as writeFileSync6,
13480
14348
  writeSync
13481
14349
  } from "fs";
13482
- import { dirname as dirname7, resolve as resolve9 } from "path";
14350
+ import { dirname as dirname8, resolve as resolve12 } from "path";
13483
14351
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
13484
14352
  function parseEditBlocks(text) {
13485
14353
  const out = [];
@@ -13497,9 +14365,9 @@ function parseEditBlocks(text) {
13497
14365
  return out;
13498
14366
  }
13499
14367
  function applyEditBlock(block, rootDir) {
13500
- const absRoot = resolve9(rootDir);
13501
- const absTarget = resolve9(absRoot, block.path);
13502
- 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()}`)) {
13503
14371
  return {
13504
14372
  path: block.path,
13505
14373
  status: "path-escape",
@@ -13509,7 +14377,7 @@ function applyEditBlock(block, rootDir) {
13509
14377
  const searchEmpty = block.search.length === 0;
13510
14378
  if (searchEmpty) {
13511
14379
  try {
13512
- mkdirSync6(dirname7(absTarget), { recursive: true });
14380
+ mkdirSync6(dirname8(absTarget), { recursive: true });
13513
14381
  const fd = openSync2(absTarget, "wx");
13514
14382
  try {
13515
14383
  writeSync(fd, block.replace);
@@ -13585,19 +14453,19 @@ function applyEditBlocks(blocks, rootDir) {
13585
14453
  return blocks.map((b) => applyEditBlock(b, rootDir));
13586
14454
  }
13587
14455
  function snapshotBeforeEdits(blocks, rootDir) {
13588
- const absRoot = resolve9(rootDir);
14456
+ const absRoot = resolve12(rootDir);
13589
14457
  const seen = /* @__PURE__ */ new Set();
13590
14458
  const snapshots = [];
13591
14459
  for (const b of blocks) {
13592
14460
  if (seen.has(b.path)) continue;
13593
14461
  seen.add(b.path);
13594
- const abs = resolve9(absRoot, b.path);
13595
- if (!existsSync10(abs)) {
14462
+ const abs = resolve12(absRoot, b.path);
14463
+ if (!existsSync11(abs)) {
13596
14464
  snapshots.push({ path: b.path, prevContent: null });
13597
14465
  continue;
13598
14466
  }
13599
14467
  try {
13600
- snapshots.push({ path: b.path, prevContent: readFileSync13(abs, "utf8") });
14468
+ snapshots.push({ path: b.path, prevContent: readFileSync14(abs, "utf8") });
13601
14469
  } catch {
13602
14470
  snapshots.push({ path: b.path, prevContent: null });
13603
14471
  }
@@ -13605,10 +14473,10 @@ function snapshotBeforeEdits(blocks, rootDir) {
13605
14473
  return snapshots;
13606
14474
  }
13607
14475
  function restoreSnapshots(snapshots, rootDir) {
13608
- const absRoot = resolve9(rootDir);
14476
+ const absRoot = resolve12(rootDir);
13609
14477
  return snapshots.map((snap) => {
13610
- const abs = resolve9(absRoot, snap.path);
13611
- if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
14478
+ const abs = resolve12(absRoot, snap.path);
14479
+ if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep2()}`)) {
13612
14480
  return {
13613
14481
  path: snap.path,
13614
14482
  status: "path-escape",
@@ -13617,7 +14485,7 @@ function restoreSnapshots(snapshots, rootDir) {
13617
14485
  }
13618
14486
  try {
13619
14487
  if (snap.prevContent === null) {
13620
- if (existsSync10(abs)) unlinkSync3(abs);
14488
+ if (existsSync11(abs)) unlinkSync3(abs);
13621
14489
  return {
13622
14490
  path: snap.path,
13623
14491
  status: "applied",
@@ -13635,7 +14503,7 @@ function restoreSnapshots(snapshots, rootDir) {
13635
14503
  }
13636
14504
  });
13637
14505
  }
13638
- function sep() {
14506
+ function sep2() {
13639
14507
  return process.platform === "win32" ? "\\" : "/";
13640
14508
  }
13641
14509
  function lineEndingOf(text) {
@@ -13643,8 +14511,8 @@ function lineEndingOf(text) {
13643
14511
  }
13644
14512
 
13645
14513
  // src/code/prompt.ts
13646
- import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
13647
- import { join as join13 } from "path";
14514
+ import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
14515
+ import { join as join15 } from "path";
13648
14516
  var DEFAULT_CODE_MODEL = "deepseek-v4-flash";
13649
14517
  function codeSystemBase(modelId) {
13650
14518
  return CODE_SYSTEM_TEMPLATE.replace("__ESCALATION_CONTRACT__", escalationContract(modelId));
@@ -13891,12 +14759,12 @@ function codeSystemPrompt(rootDir, opts = {}) {
13891
14759
  const codeBase = codeSystemBase(opts.modelId ?? DEFAULT_CODE_MODEL);
13892
14760
  const base = opts.hasSemanticSearch ? `${codeBase}${SEMANTIC_SEARCH_ROUTING}` : codeBase;
13893
14761
  const withMemory = applyMemoryStack(base, rootDir);
13894
- const gitignorePath = join13(rootDir, ".gitignore");
14762
+ const gitignorePath = join15(rootDir, ".gitignore");
13895
14763
  let result = withMemory;
13896
- if (existsSync11(gitignorePath)) {
14764
+ if (existsSync12(gitignorePath)) {
13897
14765
  let content;
13898
14766
  try {
13899
- content = readFileSync14(gitignorePath, "utf8");
14767
+ content = readFileSync15(gitignorePath, "utf8");
13900
14768
  } catch {
13901
14769
  }
13902
14770
  if (content !== void 0) {
@@ -13930,21 +14798,21 @@ ${appendParts.join("\n\n")}`;
13930
14798
  import {
13931
14799
  appendFileSync as appendFileSync2,
13932
14800
  closeSync as closeSync3,
13933
- existsSync as existsSync12,
14801
+ existsSync as existsSync13,
13934
14802
  fstatSync as fstatSync2,
13935
14803
  mkdirSync as mkdirSync7,
13936
14804
  openSync as openSync3,
13937
- readFileSync as readFileSync15,
14805
+ readFileSync as readFileSync16,
13938
14806
  readSync as readSync2,
13939
14807
  renameSync as renameSync2,
13940
14808
  statSync as statSync6,
13941
14809
  unlinkSync as unlinkSync4,
13942
14810
  writeFileSync as writeFileSync7
13943
14811
  } from "fs";
13944
- import { homedir as homedir7 } from "os";
13945
- 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";
13946
14814
  function defaultUsageLogPath(homeDirOverride) {
13947
- return join14(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
14815
+ return join16(homeDirOverride ?? homedir8(), ".reasonix", "usage.jsonl");
13948
14816
  }
13949
14817
  var USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;
13950
14818
  var USAGE_RETENTION_DAYS = 365;
@@ -14009,7 +14877,7 @@ function appendUsage(input) {
14009
14877
  if (input.subagent) record.subagent = input.subagent;
14010
14878
  const path2 = input.path ?? defaultUsageLogPath();
14011
14879
  try {
14012
- mkdirSync7(dirname8(path2), { recursive: true });
14880
+ mkdirSync7(dirname9(path2), { recursive: true });
14013
14881
  appendFileSync2(path2, `${JSON.stringify(record)}
14014
14882
  `, "utf8");
14015
14883
  compactUsageLogIfLarge(path2, record.ts);
@@ -14018,10 +14886,10 @@ function appendUsage(input) {
14018
14886
  return record;
14019
14887
  }
14020
14888
  function readUsageLog(path2 = defaultUsageLogPath()) {
14021
- if (!existsSync12(path2)) return [];
14889
+ if (!existsSync13(path2)) return [];
14022
14890
  let raw;
14023
14891
  try {
14024
- raw = readFileSync15(path2, "utf8");
14892
+ raw = readFileSync16(path2, "utf8");
14025
14893
  } catch {
14026
14894
  return [];
14027
14895
  }
@@ -14128,7 +14996,7 @@ function aggregateUsage(records, opts = {}) {
14128
14996
  };
14129
14997
  }
14130
14998
  function formatLogSize(path2 = defaultUsageLogPath()) {
14131
- if (!existsSync12(path2)) return "";
14999
+ if (!existsSync13(path2)) return "";
14132
15000
  try {
14133
15001
  const s = statSync6(path2);
14134
15002
  const bytes = s.size;
@@ -14151,6 +15019,7 @@ export {
14151
15019
  DEFAULT_MAX_RESULT_CHARS,
14152
15020
  DEFAULT_MAX_RESULT_TOKENS,
14153
15021
  DEFAULT_PICKER_IGNORE_DIRS,
15022
+ DEFAULT_SPAWN_STORM_THRESHOLD,
14154
15023
  DeepSeekClient,
14155
15024
  HOOK_EVENTS,
14156
15025
  HOOK_SETTINGS_DIRNAME,
@@ -14174,6 +15043,7 @@ export {
14174
15043
  StdioTransport,
14175
15044
  StormBreaker,
14176
15045
  StreamableHttpTransport,
15046
+ SubagentTelemetry,
14177
15047
  ToolCallRepair,
14178
15048
  ToolRegistry,
14179
15049
  USER_MEMORY_DIR,
@@ -14196,7 +15066,9 @@ export {
14196
15066
  codeSystemPrompt,
14197
15067
  compareVersions,
14198
15068
  computeReplayStats,
15069
+ computeSpawnDistillation,
14199
15070
  costUsd,
15071
+ countSpawnStorms,
14200
15072
  decideOutcome,
14201
15073
  defaultConfigPath,
14202
15074
  defaultUsageLogPath,
@@ -14239,6 +15111,7 @@ export {
14239
15111
  loadBaseUrl,
14240
15112
  loadDotenv,
14241
15113
  loadHooks,
15114
+ loadMetasoApiKey,
14242
15115
  loadSessionMessages,
14243
15116
  matchesTool,
14244
15117
  memoryEnabled,
@@ -14289,6 +15162,7 @@ export {
14289
15162
  similarity,
14290
15163
  snapshotBeforeEdits,
14291
15164
  stripHallucinatedToolMarkup,
15165
+ summarizeSubagentSession,
14292
15166
  tokenizeCommand,
14293
15167
  truncateForModel,
14294
15168
  truncateForModelByTokens,