reasonix 0.43.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +49 -11
  2. package/README.zh-CN.md +35 -7
  3. package/dashboard/app.css +225 -4
  4. package/dashboard/dist/app.js +6441 -6080
  5. package/dashboard/dist/app.js.map +1 -1
  6. package/data/deepseek-tokenizer.json.gz +0 -0
  7. package/dist/cli/{acp-DAGPCVFZ.js → acp-TYZ2CTDL.js} +28 -30
  8. package/dist/cli/acp-TYZ2CTDL.js.map +1 -0
  9. package/dist/cli/chat-TH7VNNCJ.js +51 -0
  10. package/dist/cli/chunk-2425HK6U.js +0 -0
  11. package/dist/cli/chunk-25T6CVUP.js +0 -0
  12. package/dist/cli/chunk-2UQP6H6T.js +0 -0
  13. package/dist/cli/{chunk-3Z6IBU3D.js → chunk-2V6EAEUW.js} +95 -31
  14. package/dist/cli/chunk-2V6EAEUW.js.map +1 -0
  15. package/dist/cli/{chunk-XCGGEJTI.js → chunk-4CTDEJUF.js} +2 -2
  16. package/dist/cli/chunk-4QUNBQQ2.js +0 -0
  17. package/dist/cli/{chunk-74EX7SUH.js → chunk-5QCB62C4.js} +33 -7
  18. package/dist/cli/{chunk-74EX7SUH.js.map → chunk-5QCB62C4.js.map} +1 -1
  19. package/dist/cli/chunk-6OWJV3YW.js +390 -0
  20. package/dist/cli/chunk-6OWJV3YW.js.map +1 -0
  21. package/dist/cli/chunk-6PBZN4VI.js +0 -0
  22. package/dist/cli/{chunk-7O5ALB4C.js → chunk-7CIGMZT3.js} +2 -2
  23. package/dist/cli/{chunk-H6PS7IUE.js → chunk-7UCMM425.js} +7 -3
  24. package/dist/cli/chunk-7UCMM425.js.map +1 -0
  25. package/dist/cli/{chunk-TJX6BFZZ.js → chunk-AB2RED3C.js} +3 -3
  26. package/dist/cli/{chunk-XPDVG52A.js → chunk-AVFXO2EZ.js} +361 -13
  27. package/dist/cli/chunk-AVFXO2EZ.js.map +1 -0
  28. package/dist/cli/{chunk-FHOGSSCH.js → chunk-C53JQES5.js} +3 -3
  29. package/dist/cli/{chunk-RE4RAVFF.js → chunk-CGDR2ELH.js} +92 -30
  30. package/dist/cli/chunk-CGDR2ELH.js.map +1 -0
  31. package/dist/cli/{chunk-OSZC7C6F.js → chunk-CWZKQ5FE.js} +7 -4
  32. package/dist/cli/chunk-CWZKQ5FE.js.map +1 -0
  33. package/dist/cli/{devtools-YECO25QO.js → chunk-FEZK652I.js} +10 -85
  34. package/dist/cli/chunk-FEZK652I.js.map +1 -0
  35. package/dist/cli/{chunk-45U62RI3.js → chunk-HNXDZGC6.js} +104 -2
  36. package/dist/cli/chunk-HNXDZGC6.js.map +1 -0
  37. package/dist/cli/chunk-J5XJHLWM.js +0 -0
  38. package/dist/cli/chunk-JMBMLOBP.js +0 -0
  39. package/dist/cli/{chunk-5JJRUIPA.js → chunk-JNAQYELD.js} +16 -8
  40. package/dist/cli/{chunk-5JJRUIPA.js.map → chunk-JNAQYELD.js.map} +1 -1
  41. package/dist/cli/{chunk-YFGF5NKA.js → chunk-KGBG6M2X.js} +19 -15
  42. package/dist/cli/chunk-KGBG6M2X.js.map +1 -0
  43. package/dist/cli/{chunk-3BXRZFWS.js → chunk-KLQTAZIY.js} +12 -4
  44. package/dist/cli/chunk-KLQTAZIY.js.map +1 -0
  45. package/dist/cli/{chunk-VK5HG73G.js → chunk-KM465GST.js} +9 -9
  46. package/dist/cli/{chunk-DOYHN4KB.js → chunk-LIR2HBQH.js} +2 -2
  47. package/dist/cli/{chunk-YYQAUTTN.js → chunk-MJ6W5UN3.js} +2 -2
  48. package/dist/cli/{chunk-6PZ3CXBP.js → chunk-MRHHQJAQ.js} +5 -4
  49. package/dist/cli/chunk-MRHHQJAQ.js.map +1 -0
  50. package/dist/cli/{chunk-PQXPXJBJ.js → chunk-NVURFF27.js} +16 -5
  51. package/dist/cli/chunk-NVURFF27.js.map +1 -0
  52. package/dist/cli/{chunk-2R4QCDOZ.js → chunk-OPFUUYHL.js} +540 -287
  53. package/dist/cli/chunk-OPFUUYHL.js.map +1 -0
  54. package/dist/cli/chunk-PLHAZOLZ.js +0 -0
  55. package/dist/cli/{chunk-HFEAY5DT.js → chunk-R3CTO2HM.js} +2 -2
  56. package/dist/cli/{chunk-O52OLQL3.js → chunk-RDRC3XDT.js} +136 -38
  57. package/dist/cli/chunk-RDRC3XDT.js.map +1 -0
  58. package/dist/cli/chunk-S4XVGLRW.js +0 -0
  59. package/dist/cli/chunk-SZ5XES2N.js +0 -0
  60. package/dist/cli/{chunk-2K65GZBT.js → chunk-TEUDEGX2.js} +64 -19
  61. package/dist/cli/chunk-TEUDEGX2.js.map +1 -0
  62. package/dist/cli/{chunk-2Z35JOA4.js → chunk-TKVXTQ3T.js} +4 -4
  63. package/dist/cli/{chunk-2Z35JOA4.js.map → chunk-TKVXTQ3T.js.map} +1 -1
  64. package/dist/cli/chunk-TUK7OWJA.js +0 -0
  65. package/dist/cli/{chunk-32TIKD5U.js → chunk-TXJMRPIL.js} +3 -3
  66. package/dist/cli/{chunk-2KDUS647.js → chunk-V26WPN3J.js} +7 -4
  67. package/dist/cli/chunk-V26WPN3J.js.map +1 -0
  68. package/dist/cli/{chunk-F3PXYSNN.js → chunk-WK3UFQY3.js} +2 -2
  69. package/dist/cli/{chunk-6G3CUUFG.js → chunk-X53B3JIX.js} +3 -3
  70. package/dist/cli/{chunk-6G3CUUFG.js.map → chunk-X53B3JIX.js.map} +1 -1
  71. package/dist/cli/chunk-XJXDHAES.js +0 -0
  72. package/dist/cli/{chunk-6AK4EY3D.js → chunk-XSU4QVFW.js} +1 -81
  73. package/dist/cli/chunk-XSU4QVFW.js.map +1 -0
  74. package/dist/cli/chunk-XXC2BYTV.js +0 -0
  75. package/dist/cli/{chunk-P7EKE5ZQ.js → chunk-Z4S7EYXG.js} +4482 -1310
  76. package/dist/cli/chunk-Z4S7EYXG.js.map +1 -0
  77. package/dist/cli/chunk-ZZM6QJ4W.js +0 -0
  78. package/dist/cli/{chunk-YQ6NTIIE.js → chunk-ZZYBBX5N.js} +13 -5
  79. package/dist/cli/chunk-ZZYBBX5N.js.map +1 -0
  80. package/dist/cli/{code-SMKEW6CD.js → code-PSVJ3KEN.js} +48 -36
  81. package/dist/cli/code-PSVJ3KEN.js.map +1 -0
  82. package/dist/cli/{commands-FVVB5FZF.js → commands-OCU42XG4.js} +4 -4
  83. package/dist/cli/{commit-HE4VSPZ7.js → commit-XCQIQCYG.js} +3 -3
  84. package/dist/cli/{desktop-Q7NDXCON.js → desktop-KWGR4BNE.js} +210 -69
  85. package/dist/cli/desktop-KWGR4BNE.js.map +1 -0
  86. package/dist/cli/devtools-HW3WDT3Q.js +91 -0
  87. package/dist/cli/devtools-HW3WDT3Q.js.map +1 -0
  88. package/dist/cli/{diff-435UTPC5.js → diff-NHANTNC3.js} +9 -9
  89. package/dist/cli/{doctor-OT7KH75K.js → doctor-CC5CLOGG.js} +10 -10
  90. package/dist/cli/events-XEFAD5VX.js +0 -0
  91. package/dist/cli/index.js +132 -94
  92. package/dist/cli/index.js.map +1 -1
  93. package/dist/cli/{mcp-WUL2WO75.js → mcp-MPVGBBJF.js} +2 -2
  94. package/dist/cli/{mcp-browse-RR7R4XET.js → mcp-browse-4XOTC3FJ.js} +3 -3
  95. package/dist/cli/{mcp-inspect-REGLYBWT.js → mcp-inspect-CEMGKKAH.js} +14 -9
  96. package/dist/cli/mcp-inspect-CEMGKKAH.js.map +1 -0
  97. package/dist/cli/{prompt-UW6EFLVR.js → prompt-2D7ID24X.js} +4 -4
  98. package/dist/cli/prune-sessions-3RWUBYRS.js +0 -0
  99. package/dist/cli/{replay-YOURXV4C.js → replay-SR44E6RS.js} +10 -10
  100. package/dist/cli/{run-Q6BUXV66.js → run-MDGL27WL.js} +35 -36
  101. package/dist/cli/run-MDGL27WL.js.map +1 -0
  102. package/dist/cli/{server-XGDBRWMB.js → server-27ARQXIZ.js} +67 -24
  103. package/dist/cli/server-27ARQXIZ.js.map +1 -0
  104. package/dist/cli/{sessions-FH7QVYSY.js → sessions-CKQXCYGP.js} +18 -18
  105. package/dist/cli/sessions-CKQXCYGP.js.map +1 -0
  106. package/dist/cli/{setup-VDS6SVEP.js → setup-TPAGSVXO.js} +6 -6
  107. package/dist/cli/{stats-MQVI2XQH.js → stats-DPUBZNVX.js} +6 -4
  108. package/dist/cli/update-6ITLPRDV.js +0 -0
  109. package/dist/cli/{version-DAHGZY5N.js → version-2X3BHVVK.js} +15 -15
  110. package/dist/index.d.ts +181 -53
  111. package/dist/index.js +1322 -533
  112. package/dist/index.js.map +1 -1
  113. package/package.json +21 -8
  114. package/dist/cli/.-3G6VX5S7.js +0 -327
  115. package/dist/cli/.-6YRPB2C7.js +0 -329
  116. package/dist/cli/.-EYSVINK3.js +0 -317
  117. package/dist/cli/acp-DAGPCVFZ.js.map +0 -1
  118. package/dist/cli/chat-7ES4IBNH.js +0 -50
  119. package/dist/cli/chunk-2K65GZBT.js.map +0 -1
  120. package/dist/cli/chunk-2KDUS647.js.map +0 -1
  121. package/dist/cli/chunk-2R4QCDOZ.js.map +0 -1
  122. package/dist/cli/chunk-3BXRZFWS.js.map +0 -1
  123. package/dist/cli/chunk-3Z6IBU3D.js.map +0 -1
  124. package/dist/cli/chunk-45U62RI3.js.map +0 -1
  125. package/dist/cli/chunk-6AK4EY3D.js.map +0 -1
  126. package/dist/cli/chunk-6PZ3CXBP.js.map +0 -1
  127. package/dist/cli/chunk-H6PS7IUE.js.map +0 -1
  128. package/dist/cli/chunk-O52OLQL3.js.map +0 -1
  129. package/dist/cli/chunk-OSZC7C6F.js.map +0 -1
  130. package/dist/cli/chunk-P7EKE5ZQ.js.map +0 -1
  131. package/dist/cli/chunk-PQXPXJBJ.js.map +0 -1
  132. package/dist/cli/chunk-PV55UMTO.js +0 -200
  133. package/dist/cli/chunk-PV55UMTO.js.map +0 -1
  134. package/dist/cli/chunk-RE4RAVFF.js.map +0 -1
  135. package/dist/cli/chunk-XPDVG52A.js.map +0 -1
  136. package/dist/cli/chunk-YFGF5NKA.js.map +0 -1
  137. package/dist/cli/chunk-YQ6NTIIE.js.map +0 -1
  138. package/dist/cli/code-SMKEW6CD.js.map +0 -1
  139. package/dist/cli/desktop-Q7NDXCON.js.map +0 -1
  140. package/dist/cli/devtools-YECO25QO.js.map +0 -1
  141. package/dist/cli/doctor-OT7KH75K.js.map +0 -1
  142. package/dist/cli/mcp-inspect-REGLYBWT.js.map +0 -1
  143. package/dist/cli/prompt-UW6EFLVR.js.map +0 -1
  144. package/dist/cli/run-Q6BUXV66.js.map +0 -1
  145. package/dist/cli/server-XGDBRWMB.js.map +0 -1
  146. package/dist/cli/sessions-FH7QVYSY.js.map +0 -1
  147. package/dist/cli/stats-MQVI2XQH.js.map +0 -1
  148. /package/dist/cli/{.-3G6VX5S7.js.map → chat-TH7VNNCJ.js.map} +0 -0
  149. /package/dist/cli/{chunk-XCGGEJTI.js.map → chunk-4CTDEJUF.js.map} +0 -0
  150. /package/dist/cli/{chunk-7O5ALB4C.js.map → chunk-7CIGMZT3.js.map} +0 -0
  151. /package/dist/cli/{chunk-TJX6BFZZ.js.map → chunk-AB2RED3C.js.map} +0 -0
  152. /package/dist/cli/{chunk-FHOGSSCH.js.map → chunk-C53JQES5.js.map} +0 -0
  153. /package/dist/cli/{chunk-VK5HG73G.js.map → chunk-KM465GST.js.map} +0 -0
  154. /package/dist/cli/{chunk-DOYHN4KB.js.map → chunk-LIR2HBQH.js.map} +0 -0
  155. /package/dist/cli/{chunk-YYQAUTTN.js.map → chunk-MJ6W5UN3.js.map} +0 -0
  156. /package/dist/cli/{chunk-HFEAY5DT.js.map → chunk-R3CTO2HM.js.map} +0 -0
  157. /package/dist/cli/{chunk-32TIKD5U.js.map → chunk-TXJMRPIL.js.map} +0 -0
  158. /package/dist/cli/{chunk-F3PXYSNN.js.map → chunk-WK3UFQY3.js.map} +0 -0
  159. /package/dist/cli/{commands-FVVB5FZF.js.map → commands-OCU42XG4.js.map} +0 -0
  160. /package/dist/cli/{commit-HE4VSPZ7.js.map → commit-XCQIQCYG.js.map} +0 -0
  161. /package/dist/cli/{diff-435UTPC5.js.map → diff-NHANTNC3.js.map} +0 -0
  162. /package/dist/cli/{.-6YRPB2C7.js.map → doctor-CC5CLOGG.js.map} +0 -0
  163. /package/dist/cli/{mcp-WUL2WO75.js.map → mcp-MPVGBBJF.js.map} +0 -0
  164. /package/dist/cli/{mcp-browse-RR7R4XET.js.map → mcp-browse-4XOTC3FJ.js.map} +0 -0
  165. /package/dist/cli/{.-EYSVINK3.js.map → prompt-2D7ID24X.js.map} +0 -0
  166. /package/dist/cli/{replay-YOURXV4C.js.map → replay-SR44E6RS.js.map} +0 -0
  167. /package/dist/cli/{setup-VDS6SVEP.js.map → setup-TPAGSVXO.js.map} +0 -0
  168. /package/dist/cli/{chat-7ES4IBNH.js.map → stats-DPUBZNVX.js.map} +0 -0
  169. /package/dist/cli/{version-DAHGZY5N.js.map → version-2X3BHVVK.js.map} +0 -0
@@ -3,28 +3,32 @@ import { createRequire as __cr } from 'node:module'; if (typeof globalThis.requi
3
3
  import {
4
4
  MemoryStore,
5
5
  sanitizeMemoryName
6
- } from "./chunk-5JJRUIPA.js";
6
+ } from "./chunk-JNAQYELD.js";
7
7
  import {
8
8
  countTokens,
9
+ countTokensBounded,
9
10
  estimateConversationTokens,
10
11
  estimateRequestTokens
11
- } from "./chunk-PV55UMTO.js";
12
+ } from "./chunk-6OWJV3YW.js";
12
13
  import {
13
14
  Usage
14
- } from "./chunk-2KDUS647.js";
15
+ } from "./chunk-V26WPN3J.js";
15
16
  import {
16
17
  applyEdit,
17
18
  applyMultiEdit,
18
19
  pauseGate
19
- } from "./chunk-O52OLQL3.js";
20
+ } from "./chunk-RDRC3XDT.js";
20
21
  import {
21
22
  NEGATIVE_CLAIM_RULE,
22
- TUI_FORMATTING_RULES
23
- } from "./chunk-2K65GZBT.js";
23
+ PROJECT_MEMORY_FILES,
24
+ PROJECT_MEMORY_MAX_CHARS,
25
+ TUI_FORMATTING_RULES,
26
+ memoryEnabled
27
+ } from "./chunk-TEUDEGX2.js";
24
28
  import {
25
29
  formatHookOutcomeMessage,
26
30
  runHooks
27
- } from "./chunk-7O5ALB4C.js";
31
+ } from "./chunk-7CIGMZT3.js";
28
32
  import {
29
33
  ignoredByLayers,
30
34
  loadGitignoreAt,
@@ -38,23 +42,24 @@ import {
38
42
  rewriteSession,
39
43
  timestampSuffix
40
44
  } from "./chunk-6PBZN4VI.js";
45
+ import {
46
+ DEEPSEEK_CONTEXT_TOKENS,
47
+ DEFAULT_CONTEXT_TOKENS,
48
+ SessionStats
49
+ } from "./chunk-ZZYBBX5N.js";
41
50
  import {
42
51
  t
43
- } from "./chunk-RE4RAVFF.js";
52
+ } from "./chunk-CGDR2ELH.js";
44
53
  import {
45
54
  DEFAULT_INDEX_EXCLUDES,
46
55
  addProjectPathAllowed,
47
56
  loadMemoryTypeRegistry,
57
+ loadMetasoApiKey,
48
58
  loadProjectPathAllowed,
49
59
  require_picomatch,
50
60
  webSearchEndpoint,
51
61
  webSearchEngine
52
- } from "./chunk-XPDVG52A.js";
53
- import {
54
- DEEPSEEK_CONTEXT_TOKENS,
55
- DEFAULT_CONTEXT_TOKENS,
56
- SessionStats
57
- } from "./chunk-YQ6NTIIE.js";
62
+ } from "./chunk-AVFXO2EZ.js";
58
63
  import {
59
64
  __commonJS,
60
65
  __esm,
@@ -2591,10 +2596,10 @@ var require_helpers = __commonJS({
2591
2596
  return !arr.includes(node, i + 1);
2592
2597
  });
2593
2598
  nodes.sort(function(a, b) {
2594
- var relative5 = compareDocumentPosition(a, b);
2595
- if (relative5 & DocumentPosition.PRECEDING) {
2599
+ var relative6 = compareDocumentPosition(a, b);
2600
+ if (relative6 & DocumentPosition.PRECEDING) {
2596
2601
  return -1;
2597
- } else if (relative5 & DocumentPosition.FOLLOWING) {
2602
+ } else if (relative6 & DocumentPosition.FOLLOWING) {
2598
2603
  return 1;
2599
2604
  }
2600
2605
  return 0;
@@ -6044,12 +6049,12 @@ async function waitForReady(ready, timeoutMs, serverName, signal) {
6044
6049
  let timer;
6045
6050
  let onAbort;
6046
6051
  try {
6047
- await new Promise((resolve4, reject) => {
6052
+ await new Promise((resolve5, reject) => {
6048
6053
  ready.then(
6049
6054
  () => {
6050
6055
  if (settled) return;
6051
6056
  settled = true;
6052
- resolve4();
6057
+ resolve5();
6053
6058
  },
6054
6059
  (err) => {
6055
6060
  if (settled) return;
@@ -6584,6 +6589,40 @@ var VolatileScratch = class {
6584
6589
  }
6585
6590
  };
6586
6591
 
6592
+ // src/loop/thinking.ts
6593
+ function isThinkingModeModel(model) {
6594
+ if (model.includes("reasoner")) return true;
6595
+ if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return true;
6596
+ return false;
6597
+ }
6598
+ function thinkingModeForModel(model) {
6599
+ if (model === "deepseek-chat") return "disabled";
6600
+ if (model.includes("reasoner")) return "enabled";
6601
+ if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return "enabled";
6602
+ return void 0;
6603
+ }
6604
+ function stripHallucinatedToolMarkup(s) {
6605
+ let out = s;
6606
+ out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
6607
+ out = out.replace(/<\|DSML\|function_calls>[\s\S]*?<\/?\|DSML\|function_calls>/g, "");
6608
+ out = out.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "");
6609
+ out = out.replace(/<|DSML|[\s\S]*$/g, "");
6610
+ return out.trim();
6611
+ }
6612
+
6613
+ // src/loop/messages.ts
6614
+ function buildAssistantMessage(content, toolCalls, producingModel, reasoningContent) {
6615
+ const msg = { role: "assistant", content };
6616
+ if (toolCalls.length > 0) msg.tool_calls = toolCalls;
6617
+ if (isThinkingModeModel(producingModel) || reasoningContent && reasoningContent.length > 0) {
6618
+ msg.reasoning_content = reasoningContent ?? "";
6619
+ }
6620
+ return msg;
6621
+ }
6622
+ function buildSyntheticAssistantMessage(content, fallbackModel) {
6623
+ return buildAssistantMessage(content, [], fallbackModel, "");
6624
+ }
6625
+
6587
6626
  // src/context-manager.ts
6588
6627
  var HISTORY_FOLD_THRESHOLD = 0.5;
6589
6628
  var HISTORY_FOLD_TAIL_FRACTION = 0.2;
@@ -6592,6 +6631,8 @@ var HISTORY_FOLD_AGGRESSIVE_TAIL_FRACTION = 0.1;
6592
6631
  var HISTORY_FOLD_MIN_SAVINGS_FRACTION = 0.3;
6593
6632
  var FORCE_SUMMARY_THRESHOLD = 0.8;
6594
6633
  var PREFLIGHT_EMERGENCY_THRESHOLD = 0.95;
6634
+ var PREFLIGHT_MECHANICAL_TARGET_FRACTION = 0.7;
6635
+ var HISTORY_FOLD_SUMMARY_TIMEOUT_MS = 15e3;
6595
6636
  var HISTORY_FOLD_MARKER = "[CONVERSATION HISTORY SUMMARY \u2014 earlier turns folded for context efficiency]\n\n";
6596
6637
  var SKILL_PIN_MEMO_HEADER = "[Active skill memos \u2014 preserved verbatim across the fold:]";
6597
6638
  var SKILL_PIN_REGEX = /<skill-pin name="([^"]+)">\n[\s\S]*?\n<\/skill-pin>/g;
@@ -6646,7 +6687,7 @@ var ContextManager = class {
6646
6687
  /** Local-side preflight before sending a request — catches oversized payloads early. */
6647
6688
  decidePreflight(messages, toolSpecs, model) {
6648
6689
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
6649
- const estimate = estimateRequestTokens(messages, toolSpecs ?? null);
6690
+ const estimate = estimateRequestTokens(messages, toolSpecs ?? null, true);
6650
6691
  return {
6651
6692
  needsAction: estimate / ctxMax > PREFLIGHT_EMERGENCY_THRESHOLD,
6652
6693
  estimateTokens: estimate,
@@ -6665,7 +6706,7 @@ var ContextManager = class {
6665
6706
  summaryChars: 0
6666
6707
  };
6667
6708
  if (all.length === 0) return noop;
6668
- const tokenCounts = all.map((m) => estimateConversationTokens([m]));
6709
+ const tokenCounts = all.map((m) => countTokensBounded(m.content ?? ""));
6669
6710
  const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);
6670
6711
  let cumTokens = 0;
6671
6712
  let boundary = all.length;
@@ -6681,16 +6722,18 @@ var ContextManager = class {
6681
6722
  if (headTokens < totalTokens * HISTORY_FOLD_MIN_SAVINGS_FRACTION) return noop;
6682
6723
  const { stubbedHead, pinnedBodies } = extractPinnedSkills(head);
6683
6724
  const summary = await this.summarizeForFold(stubbedHead);
6684
- if (!summary) return noop;
6725
+ if (!summary.content) return noop;
6685
6726
  const memoTail = pinnedBodies.length > 0 ? `
6686
6727
 
6687
6728
  ${SKILL_PIN_MEMO_HEADER}
6688
6729
 
6689
6730
  ${pinnedBodies.join("\n\n")}` : "";
6690
- const summaryMsg = {
6691
- role: "assistant",
6692
- content: HISTORY_FOLD_MARKER + summary + memoTail
6693
- };
6731
+ const summaryMsg = buildAssistantMessage(
6732
+ HISTORY_FOLD_MARKER + summary.content + memoTail,
6733
+ [],
6734
+ model,
6735
+ summary.reasoningContent
6736
+ );
6694
6737
  const replacement = [summaryMsg, ...tail];
6695
6738
  this.deps.log.compactInPlace(replacement);
6696
6739
  this.persistRewrite(replacement);
@@ -6698,7 +6741,51 @@ ${pinnedBodies.join("\n\n")}` : "";
6698
6741
  folded: true,
6699
6742
  beforeMessages: all.length,
6700
6743
  afterMessages: replacement.length,
6701
- summaryChars: summary.length
6744
+ summaryChars: summary.content.length
6745
+ };
6746
+ }
6747
+ /** Pure local emergency compaction for preflight: drop oldest log entries and keep a valid tail. */
6748
+ mechanicalTruncate(model, opts) {
6749
+ const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
6750
+ const targetTokens = opts?.targetTokens ?? Math.floor(ctxMax * PREFLIGHT_MECHANICAL_TARGET_FRACTION);
6751
+ const all = this.deps.log.toMessages();
6752
+ const noop = {
6753
+ folded: false,
6754
+ beforeMessages: all.length,
6755
+ afterMessages: all.length,
6756
+ summaryChars: 0
6757
+ };
6758
+ if (all.length === 0) return noop;
6759
+ const tokenCounts = all.map((m) => estimateConversationTokens([m], true));
6760
+ let latestUserBoundary = -1;
6761
+ for (let i = all.length - 1; i >= 0; i--) {
6762
+ if (all[i].role === "user") {
6763
+ latestUserBoundary = i;
6764
+ break;
6765
+ }
6766
+ }
6767
+ let cumTokens = 0;
6768
+ let boundary = all.length;
6769
+ let foundSafeBoundary = false;
6770
+ for (let i = all.length - 1; i >= 0; i--) {
6771
+ const next = cumTokens + tokenCounts[i];
6772
+ if (next > targetTokens) break;
6773
+ cumTokens = next;
6774
+ if (all[i].role === "user") {
6775
+ boundary = i;
6776
+ foundSafeBoundary = true;
6777
+ }
6778
+ }
6779
+ if (boundary <= 0) return noop;
6780
+ const replacement = foundSafeBoundary ? all.slice(boundary) : opts?.allowEmpty ? [] : latestUserBoundary >= 0 ? all.slice(latestUserBoundary) : all;
6781
+ if (replacement.length === all.length) return noop;
6782
+ this.deps.log.compactInPlace(replacement);
6783
+ this.persistRewrite(replacement);
6784
+ return {
6785
+ folded: true,
6786
+ beforeMessages: all.length,
6787
+ afterMessages: replacement.length,
6788
+ summaryChars: 0
6702
6789
  };
6703
6790
  }
6704
6791
  /** Drop a trailing in-flight assistant-with-tool_calls before a forced summary. Tail-only mutation; prefix cache safe. */
@@ -6724,18 +6811,51 @@ ${pinnedBodies.join("\n\n")}` : "";
6724
6811
  content: "Summarize the conversation above as plain prose. This summary replaces the original turns to free context \u2014 make it self-contained."
6725
6812
  }
6726
6813
  ];
6814
+ const turnSignal = this.deps.getAbortSignal();
6815
+ const foldCtrl = new AbortController();
6816
+ let cleanupAbort = () => {
6817
+ };
6818
+ let timeout;
6727
6819
  try {
6728
- const resp = await this.deps.client.chat({
6729
- model: summaryModel,
6730
- messages,
6731
- signal: this.deps.getAbortSignal(),
6732
- thinking: thinkingModeForModel(summaryModel),
6733
- reasoningEffort: "high"
6820
+ const abortPromise = new Promise((_, reject) => {
6821
+ const abort = () => {
6822
+ foldCtrl.abort();
6823
+ reject(new Error("fold-aborted"));
6824
+ };
6825
+ if (turnSignal.aborted) {
6826
+ abort();
6827
+ } else {
6828
+ turnSignal.addEventListener("abort", abort, { once: true });
6829
+ cleanupAbort = () => turnSignal.removeEventListener("abort", abort);
6830
+ }
6734
6831
  });
6832
+ const timeoutPromise = new Promise((_, reject) => {
6833
+ timeout = setTimeout(() => {
6834
+ foldCtrl.abort();
6835
+ reject(new Error("fold-timeout"));
6836
+ }, HISTORY_FOLD_SUMMARY_TIMEOUT_MS);
6837
+ });
6838
+ const resp = await Promise.race([
6839
+ this.deps.client.chat({
6840
+ model: summaryModel,
6841
+ messages,
6842
+ signal: foldCtrl.signal,
6843
+ thinking: thinkingModeForModel(summaryModel),
6844
+ reasoningEffort: "high"
6845
+ }),
6846
+ abortPromise,
6847
+ timeoutPromise
6848
+ ]);
6735
6849
  this.deps.stats.record(this.deps.getCurrentTurn(), summaryModel, resp.usage ?? new Usage());
6736
- return stripHallucinatedToolMarkup((resp.content ?? "").trim());
6850
+ return {
6851
+ content: stripHallucinatedToolMarkup((resp.content ?? "").trim()),
6852
+ reasoningContent: resp.reasoningContent ?? ""
6853
+ };
6737
6854
  } catch {
6738
- return "";
6855
+ return { content: "", reasoningContent: "" };
6856
+ } finally {
6857
+ if (timeout) clearTimeout(timeout);
6858
+ cleanupAbort();
6739
6859
  }
6740
6860
  }
6741
6861
  persistRewrite(messages) {
@@ -6827,17 +6947,15 @@ function formatDeepSeek5xx(status, probe) {
6827
6947
  const action = probe?.reachable === false ? t("errors.deepseek5xxActionNetwork") : t("errors.deepseek5xxActionRetry");
6828
6948
  return `${head}${probeNote}${action}`;
6829
6949
  }
6830
- function reasonPrefixFor(reason, iterCap) {
6950
+ function reasonPrefixFor(reason) {
6831
6951
  if (reason === "aborted") return t("errors.reasonAborted");
6832
6952
  if (reason === "context-guard") return t("errors.reasonContextGuard");
6833
- if (reason === "stuck") return t("errors.reasonStuck");
6834
- return t("errors.reasonBudget", { iterCap });
6953
+ return t("errors.reasonStuck");
6835
6954
  }
6836
- function errorLabelFor(reason, iterCap) {
6955
+ function errorLabelFor(reason) {
6837
6956
  if (reason === "aborted") return t("errors.labelAborted");
6838
6957
  if (reason === "context-guard") return t("errors.labelContextGuard");
6839
- if (reason === "stuck") return t("errors.labelStuck");
6840
- return t("errors.labelBudget", { iterCap });
6958
+ return t("errors.labelStuck");
6841
6959
  }
6842
6960
  function extractDeepSeekErrorMessage(body) {
6843
6961
  const trimmed = body.trim();
@@ -6879,50 +6997,14 @@ function looksLikePartialEscalationMarker(buf) {
6879
6997
  return true;
6880
6998
  }
6881
6999
 
6882
- // src/loop/thinking.ts
6883
- function isThinkingModeModel(model) {
6884
- if (model.includes("reasoner")) return true;
6885
- if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return true;
6886
- return false;
6887
- }
6888
- function thinkingModeForModel(model) {
6889
- if (model === "deepseek-chat") return "disabled";
6890
- if (model.includes("reasoner")) return "enabled";
6891
- if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return "enabled";
6892
- return void 0;
6893
- }
6894
- function stripHallucinatedToolMarkup(s) {
6895
- let out = s;
6896
- out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
6897
- out = out.replace(/<\|DSML\|function_calls>[\s\S]*?<\/?\|DSML\|function_calls>/g, "");
6898
- out = out.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "");
6899
- out = out.replace(/<|DSML|[\s\S]*$/g, "");
6900
- return out.trim();
6901
- }
6902
-
6903
- // src/loop/messages.ts
6904
- function buildAssistantMessage(content, toolCalls, producingModel, reasoningContent) {
6905
- const msg = { role: "assistant", content };
6906
- if (toolCalls.length > 0) msg.tool_calls = toolCalls;
6907
- if (isThinkingModeModel(producingModel) || reasoningContent && reasoningContent.length > 0) {
6908
- msg.reasoning_content = reasoningContent ?? "";
6909
- }
6910
- return msg;
6911
- }
6912
- function buildSyntheticAssistantMessage(content, fallbackModel) {
6913
- return buildAssistantMessage(content, [], fallbackModel, "");
6914
- }
6915
-
6916
7000
  // src/loop/force-summary.ts
6917
- var PAUSE_SUMMARY_MODEL = "deepseek-v4-flash";
6918
- var PAUSE_SUMMARY_EFFORT = "high";
6919
- async function* forceSummaryAfterIterLimit(ctx, opts = { reason: "budget" }) {
7001
+ async function* forceSummaryAfterIterLimit(ctx, opts) {
6920
7002
  try {
6921
7003
  yield { turn: ctx.turn, role: "status", content: t("summary.status") };
6922
7004
  const messages = ctx.buildMessages();
6923
7005
  messages.push({
6924
7006
  role: "user",
6925
- 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."
7007
+ 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."
6926
7008
  });
6927
7009
  const summaryModel = "deepseek-v4-flash";
6928
7010
  const summaryEffort = "high";
@@ -6936,7 +7018,7 @@ async function* forceSummaryAfterIterLimit(ctx, opts = { reason: "budget" }) {
6936
7018
  const rawContent = resp.content?.trim() ?? "";
6937
7019
  const cleaned = stripHallucinatedToolMarkup(rawContent);
6938
7020
  const summary = cleaned || t("summary.hallucinatedFallback");
6939
- const reasonPrefix = reasonPrefixFor(opts.reason, ctx.maxToolIters);
7021
+ const reasonPrefix = reasonPrefixFor(opts.reason);
6940
7022
  const annotated = `${reasonPrefix}
6941
7023
 
6942
7024
  ${summary}`;
@@ -6951,7 +7033,7 @@ ${summary}`;
6951
7033
  };
6952
7034
  yield { turn: ctx.turn, role: "done", content: summary };
6953
7035
  } catch (err) {
6954
- const label = errorLabelFor(opts.reason, ctx.maxToolIters);
7036
+ const label = errorLabelFor(opts.reason);
6955
7037
  yield {
6956
7038
  turn: ctx.turn,
6957
7039
  role: "error",
@@ -6961,28 +7043,6 @@ ${summary}`;
6961
7043
  yield { turn: ctx.turn, role: "done", content: "" };
6962
7044
  }
6963
7045
  }
6964
- async function summarizePartialProgress(ctx) {
6965
- try {
6966
- const messages = ctx.buildMessages();
6967
- messages.push({
6968
- role: "user",
6969
- content: "You're being paused at a checkpoint, not stopped. In 3-6 sentences of plain prose, tell the parent agent: (1) what you accomplished so far, (2) what's still left, (3) any blockers or open questions. Be concrete \u2014 mention specific files / functions / tool results \u2014 so the parent can decide whether to resume you or take over. Do NOT emit any tool calls, function-call markup, DSML invocations, or SEARCH/REPLACE edit blocks \u2014 they will be silently discarded. Just plain text."
6970
- });
6971
- const resp = await ctx.client.chat({
6972
- model: PAUSE_SUMMARY_MODEL,
6973
- messages,
6974
- signal: ctx.signal,
6975
- thinking: thinkingModeForModel(PAUSE_SUMMARY_MODEL),
6976
- reasoningEffort: PAUSE_SUMMARY_EFFORT
6977
- });
6978
- const cleaned = stripHallucinatedToolMarkup(resp.content?.trim() ?? "");
6979
- if (!cleaned) return null;
6980
- const stats = ctx.recordStats(PAUSE_SUMMARY_MODEL, resp.usage ?? new Usage());
6981
- return { summary: cleaned, stats };
6982
- } catch {
6983
- return null;
6984
- }
6985
- }
6986
7046
 
6987
7047
  // src/loop/shrink.ts
6988
7048
  function looksLikeCompleteJson(s) {
@@ -7015,7 +7075,7 @@ function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
7015
7075
  if (msg.role !== "tool") return msg;
7016
7076
  const content = typeof msg.content === "string" ? msg.content : "";
7017
7077
  if (content.length <= maxTokens) return msg;
7018
- const beforeTokens = countTokens(content);
7078
+ const beforeTokens = countTokensBounded(content);
7019
7079
  if (beforeTokens <= maxTokens) return msg;
7020
7080
  const truncated = truncateForModelByTokens(content, maxTokens);
7021
7081
  const afterTokens = countTokens(truncated);
@@ -7341,11 +7401,16 @@ var StormBreaker = class {
7341
7401
  function repairTruncatedJson(input) {
7342
7402
  const notes = [];
7343
7403
  if (!input || !input.trim()) {
7344
- return { repaired: "{}", changed: input !== "{}", notes: ["empty input \u2192 {}"] };
7404
+ return {
7405
+ repaired: "{}",
7406
+ changed: input !== "{}",
7407
+ notes: ["empty input \u2192 {}"],
7408
+ fallback: false
7409
+ };
7345
7410
  }
7346
7411
  try {
7347
7412
  JSON.parse(input);
7348
- return { repaired: input, changed: false, notes: [] };
7413
+ return { repaired: input, changed: false, notes: [], fallback: false };
7349
7414
  } catch {
7350
7415
  }
7351
7416
  const stack = [];
@@ -7400,10 +7465,12 @@ function repairTruncatedJson(input) {
7400
7465
  }
7401
7466
  try {
7402
7467
  JSON.parse(s);
7403
- return { repaired: s, changed: true, notes };
7468
+ return { repaired: s, changed: s !== input, notes, fallback: false };
7404
7469
  } catch (err) {
7470
+ const preview = input.length <= 500 ? input : `${input.slice(0, 500)} \u2026[+${input.length - 500} chars]`;
7405
7471
  notes.push(`fallback to {}: ${err.message}`);
7406
- return { repaired: "{}", changed: true, notes };
7472
+ notes.push(`unrecoverable truncation \u2014 original args preview: ${preview}`);
7473
+ return { repaired: "{}", changed: true, notes, fallback: true };
7407
7474
  }
7408
7475
  }
7409
7476
 
@@ -7450,9 +7517,16 @@ var ToolCallRepair = class {
7450
7517
  const args = call.function?.arguments ?? "";
7451
7518
  const r = repairTruncatedJson(args);
7452
7519
  if (r.changed) {
7453
- call.function.arguments = r.repaired;
7454
- report.truncationsFixed++;
7455
- report.notes.push(...r.notes.map((n) => `[${call.function.name}] ${n}`));
7520
+ if (r.fallback) {
7521
+ report.truncationsFixed++;
7522
+ report.notes.push(
7523
+ ...r.notes.map((n) => `[${call.function?.name}] \u26A0\uFE0F TRUNCATION UNRECOVERABLE: ${n}`)
7524
+ );
7525
+ } else {
7526
+ call.function.arguments = r.repaired;
7527
+ report.truncationsFixed++;
7528
+ report.notes.push(...r.notes.map((n) => `[${call.function.name}] ${n}`));
7529
+ }
7456
7530
  }
7457
7531
  }
7458
7532
  const filtered = [];
@@ -7474,12 +7548,10 @@ function signature(call) {
7474
7548
 
7475
7549
  // src/loop.ts
7476
7550
  var ESCALATION_MODEL = "deepseek-v4-pro";
7477
- var PARENT_BUDGET_WARN_THRESHOLD = 5;
7478
7551
  var CacheFirstLoop = class {
7479
7552
  client;
7480
7553
  prefix;
7481
7554
  tools;
7482
- maxToolIters;
7483
7555
  log = new AppendOnlyLog();
7484
7556
  scratch = new VolatileScratch();
7485
7557
  stats = new SessionStats();
@@ -7494,7 +7566,6 @@ var CacheFirstLoop = class {
7494
7566
  /** One-shot 80% warning latch — cleared by setBudget so a bump re-arms at the new boundary. */
7495
7567
  _budgetWarned = false;
7496
7568
  sessionName;
7497
- onIterBudgetExhausted;
7498
7569
  hooks;
7499
7570
  hookCwd;
7500
7571
  /** PauseGate bridge — defaults to singleton, injectable for tests. */
@@ -7508,12 +7579,25 @@ var CacheFirstLoop = class {
7508
7579
  _turnAbort = new AbortController();
7509
7580
  /** Authoritative running-id set — UI cards consult this instead of trusting end-event delivery. Insert at dispatch entry, delete in finally. */
7510
7581
  _inflight = new InflightSet();
7582
+ /** Typeahead steer message set by the UI; step() consumes it at the next iter boundary. */
7583
+ _steer = null;
7584
+ /** Set true when a steer was consumed this turn; cleared on next step() entry. */
7585
+ _steerConsumed = false;
7586
+ /** UI calls this to inject a mid-turn steer message without aborting the current turn.
7587
+ * New text resets steerConsumed — a fresh steer hasn't been consumed yet. */
7588
+ steer(text) {
7589
+ this._steer = text;
7590
+ if (text !== null) this._steerConsumed = false;
7591
+ }
7592
+ /** True when a steer was consumed this turn (UI gate to avoid double-submit). */
7593
+ get steerConsumed() {
7594
+ return this._steerConsumed;
7595
+ }
7511
7596
  _proArmedForNextTurn = false;
7512
7597
  _escalateThisTurn = false;
7513
7598
  _turnFailures;
7514
7599
  _turnSelfCorrected = false;
7515
7600
  _foldedThisTurn = false;
7516
- _toolDispatchesThisStep = 0;
7517
7601
  context;
7518
7602
  /** Subscribe API so UI hooks can derive `running` from finally-guaranteed insertions. */
7519
7603
  get inflight() {
@@ -7533,8 +7617,6 @@ var CacheFirstLoop = class {
7533
7617
  this._turnFailures = new TurnFailureTracker(
7534
7618
  resolveFailureThreshold(opts.failureThreshold, FAILURE_ESCALATION_THRESHOLD)
7535
7619
  );
7536
- this.maxToolIters = opts.maxToolIters ?? 64;
7537
- this.onIterBudgetExhausted = opts.onIterBudgetExhausted ?? "summarize";
7538
7620
  this.hooks = opts.hooks ?? [];
7539
7621
  this.hookCwd = opts.hookCwd ?? process.cwd();
7540
7622
  this.confirmationGate = opts.confirmationGate ?? pauseGate;
@@ -7555,23 +7637,6 @@ var CacheFirstLoop = class {
7555
7637
  stormThreshold: parsePositiveIntEnv(process.env.REASONIX_STORM_THRESHOLD),
7556
7638
  stormWindow: parsePositiveIntEnv(process.env.REASONIX_STORM_WINDOW)
7557
7639
  });
7558
- if (!this.tools.hasResultAugmenter) {
7559
- this.tools.setResultAugmenter((_name, _args, result) => {
7560
- this._toolDispatchesThisStep++;
7561
- const remaining = this.maxToolIters - this._toolDispatchesThisStep;
7562
- if (remaining <= 0) {
7563
- return `${result}
7564
-
7565
- [budget: 0 of ${this.maxToolIters} tool calls left this turn \u2014 finalize NOW; the next iter forces a summary]`;
7566
- }
7567
- if (remaining <= PARENT_BUDGET_WARN_THRESHOLD) {
7568
- return `${result}
7569
-
7570
- [budget: ${remaining} of ${this.maxToolIters} tool calls left this turn \u2014 wrap up soon]`;
7571
- }
7572
- return result;
7573
- });
7574
- }
7575
7640
  this.sessionName = opts.session ?? null;
7576
7641
  if (this.sessionName) {
7577
7642
  const prior = loadSessionMessages(this.sessionName);
@@ -7657,6 +7722,10 @@ var CacheFirstLoop = class {
7657
7722
  }
7658
7723
  this.scratch.reset();
7659
7724
  this._inflight.clear();
7725
+ this.stats.reset();
7726
+ this._turn = 0;
7727
+ this._turnFailures.reset();
7728
+ this._budgetWarned = false;
7660
7729
  let systemRebuilt = false;
7661
7730
  if (this._rebuildSystem) {
7662
7731
  try {
@@ -7666,6 +7735,29 @@ var CacheFirstLoop = class {
7666
7735
  }
7667
7736
  return { dropped, archived, systemRebuilt };
7668
7737
  }
7738
+ /** `/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). */
7739
+ switchWorkspace(opts) {
7740
+ const dropped = this.log.length;
7741
+ let archived = null;
7742
+ if (this.sessionName) {
7743
+ try {
7744
+ archived = archiveSession(this.sessionName);
7745
+ if (archived === null) rewriteSession(this.sessionName, []);
7746
+ } catch {
7747
+ }
7748
+ }
7749
+ this.log.compactInPlace([]);
7750
+ this.scratch.reset();
7751
+ this._inflight.clear();
7752
+ this.sessionName = opts.sessionName;
7753
+ if (this._rebuildSystem) {
7754
+ try {
7755
+ this.prefix.replaceSystem(this._rebuildSystem());
7756
+ } catch {
7757
+ }
7758
+ }
7759
+ return { dropped, archived };
7760
+ }
7669
7761
  configure(opts) {
7670
7762
  if (opts.model !== void 0) this.model = opts.model;
7671
7763
  if (opts.stream !== void 0) {
@@ -7821,6 +7913,7 @@ ${reason}`
7821
7913
  return userText;
7822
7914
  }
7823
7915
  async *step(userInput) {
7916
+ this._steerConsumed = false;
7824
7917
  if (this.budgetUsd !== null) {
7825
7918
  const spent = this.stats.totalCost;
7826
7919
  if (spent >= this.budgetUsd) {
@@ -7854,7 +7947,6 @@ ${reason}`
7854
7947
  this._turnSelfCorrected = false;
7855
7948
  this._escalateThisTurn = false;
7856
7949
  this._foldedThisTurn = false;
7857
- this._toolDispatchesThisStep = 0;
7858
7950
  let armedConsumed = false;
7859
7951
  if (this._proArmedForNextTurn) {
7860
7952
  this._escalateThisTurn = true;
@@ -7872,16 +7964,15 @@ ${reason}`
7872
7964
  content: t("loop.proArmed")
7873
7965
  };
7874
7966
  }
7875
- let pendingUser = userInput;
7967
+ this.appendAndPersist({ role: "user", content: userInput });
7968
+ let pendingUser = null;
7876
7969
  const toolSpecs = this.prefix.tools();
7877
- const warnAt = Math.max(1, Math.floor(this.maxToolIters * 0.7));
7878
- let warnedForIterBudget = false;
7879
- for (let iter = 0; iter < this.maxToolIters; iter++) {
7970
+ for (let iter = 0; ; iter++) {
7880
7971
  if (signal.aborted) {
7881
7972
  yield {
7882
7973
  turn: this._turn,
7883
7974
  role: "warning",
7884
- content: t("loop.abortedAtIter", { iter, cap: this.maxToolIters })
7975
+ content: t("loop.abortedAtIter", { iter })
7885
7976
  };
7886
7977
  const stoppedMsg = "[aborted by user (Esc) \u2014 no summary produced. Ask again or /retry when ready; prior tool output is still in the log.]";
7887
7978
  this.appendAndPersist(buildSyntheticAssistantMessage(stoppedMsg, this.model));
@@ -7902,15 +7993,20 @@ ${reason}`
7902
7993
  content: t("loop.toolUploadStatus")
7903
7994
  };
7904
7995
  }
7905
- if (!warnedForIterBudget && iter >= warnAt) {
7906
- warnedForIterBudget = true;
7996
+ let messages = this.buildMessages(pendingUser);
7997
+ if (this._steer !== null) {
7998
+ const steer = this._steer;
7999
+ this._steer = null;
8000
+ this._steerConsumed = true;
8001
+ this.appendAndPersist({ role: "user", content: steer });
8002
+ messages = this.buildMessages(pendingUser);
8003
+ pendingUser = null;
7907
8004
  yield {
7908
8005
  turn: this._turn,
7909
- role: "warning",
7910
- content: t("loop.toolBudgetWarning", { iter, cap: this.maxToolIters })
8006
+ role: "steer",
8007
+ content: steer
7911
8008
  };
7912
8009
  }
7913
- let messages = this.buildMessages(pendingUser);
7914
8010
  {
7915
8011
  const decision2 = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
7916
8012
  if (decision2.needsAction) {
@@ -7918,23 +8014,29 @@ ${reason}`
7918
8014
  yield {
7919
8015
  turn: this._turn,
7920
8016
  role: "status",
7921
- content: t("loop.preflightFoldStatus")
8017
+ content: t("loop.preflightTruncateStatus")
7922
8018
  };
7923
- const result = await this.context.fold(this.model);
8019
+ const result = this.context.mechanicalTruncate(this.model, {
8020
+ allowEmpty: pendingUser !== null
8021
+ });
7924
8022
  if (result.folded) {
8023
+ messages = this.buildMessages(pendingUser);
8024
+ const after = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
8025
+ const stillFull = after.needsAction;
7925
8026
  yield {
7926
8027
  turn: this._turn,
7927
8028
  role: "warning",
7928
- content: t("loop.preflightFolded", {
7929
- estimate: estimate.toLocaleString(),
7930
- ctxMax: ctxMax.toLocaleString(),
7931
- pct: Math.round(estimate / ctxMax * 100),
7932
- beforeMessages: result.beforeMessages,
7933
- afterMessages: result.afterMessages,
7934
- summaryChars: result.summaryChars
7935
- })
8029
+ content: t(
8030
+ stillFull ? "loop.preflightTruncatedStillFull" : "loop.preflightTruncated",
8031
+ {
8032
+ estimate: after.estimateTokens.toLocaleString(),
8033
+ ctxMax: after.ctxMax.toLocaleString(),
8034
+ pct: Math.round(after.estimateTokens / after.ctxMax * 100),
8035
+ beforeMessages: result.beforeMessages,
8036
+ afterMessages: result.afterMessages
8037
+ }
8038
+ )
7936
8039
  };
7937
- messages = this.buildMessages(pendingUser);
7938
8040
  } else {
7939
8041
  yield {
7940
8042
  turn: this._turn,
@@ -8091,10 +8193,6 @@ ${reason}`
8091
8193
  this.modelForCurrentCall(),
8092
8194
  usage ?? new Usage()
8093
8195
  );
8094
- if (pendingUser !== null) {
8095
- this.appendAndPersist({ role: "user", content: pendingUser });
8096
- pendingUser = null;
8097
- }
8098
8196
  this.scratch.reasoning = reasoningContent || null;
8099
8197
  const { calls: repairedCalls, report } = this.repair.process(
8100
8198
  toolCalls,
@@ -8288,20 +8386,6 @@ ${reason}`
8288
8386
  }
8289
8387
  }
8290
8388
  }
8291
- if (this.onIterBudgetExhausted === "pause") {
8292
- const partial = await summarizePartialProgress(this.summaryContext());
8293
- yield {
8294
- turn: this._turn,
8295
- role: "paused",
8296
- content: "",
8297
- sessionName: this.sessionName ?? void 0,
8298
- pausedAtIter: this.maxToolIters,
8299
- partialSummary: partial?.summary
8300
- };
8301
- yield { turn: this._turn, role: "done", content: "" };
8302
- return;
8303
- }
8304
- yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "budget" });
8305
8389
  }
8306
8390
  summaryContext() {
8307
8391
  return {
@@ -8310,8 +8394,7 @@ ${reason}`
8310
8394
  buildMessages: () => this.buildMessages(null),
8311
8395
  appendAndPersist: (m) => this.appendAndPersist(m),
8312
8396
  recordStats: (model, usage) => this.stats.record(this._turn, model, usage),
8313
- turn: this._turn,
8314
- maxToolIters: this.maxToolIters
8397
+ turn: this._turn
8315
8398
  };
8316
8399
  }
8317
8400
  async run(userInput, onEvent) {
@@ -9144,6 +9227,7 @@ var DEFAULT_TOPK = 5;
9144
9227
  var FETCH_MAX_BYTES = 10 * 1024 * 1024;
9145
9228
  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";
9146
9229
  var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
9230
+ var METASO_ENDPOINT = "https://metaso.cn/api/v1";
9147
9231
  function searchStatusError(status) {
9148
9232
  if (status === 429) return t("webErrors.rateLimit429");
9149
9233
  if (status === 403) return t("webErrors.forbidden403");
@@ -9157,6 +9241,9 @@ function fetchStatusError(status, url) {
9157
9241
  return t("webErrors.fetchStatus", { status, url });
9158
9242
  }
9159
9243
  async function webSearch(query, opts = {}) {
9244
+ if (opts.engine === "metaso") {
9245
+ return searchMetaso(query, opts);
9246
+ }
9160
9247
  if (opts.engine === "searxng") {
9161
9248
  return searchSearxng(query, opts);
9162
9249
  }
@@ -9232,6 +9319,68 @@ async function searchSearxng(query, opts = {}) {
9232
9319
  }
9233
9320
  return results;
9234
9321
  }
9322
+ async function searchMetaso(query, opts = {}) {
9323
+ const topK = Math.max(1, Math.min(100, opts.topK ?? DEFAULT_TOPK));
9324
+ const apiKey = loadMetasoApiKey();
9325
+ let resp;
9326
+ try {
9327
+ resp = await fetch(`${METASO_ENDPOINT}/search`, {
9328
+ method: "POST",
9329
+ headers: {
9330
+ "Content-Type": "application/json",
9331
+ Accept: "application/json",
9332
+ Authorization: `Bearer ${apiKey}`
9333
+ },
9334
+ body: JSON.stringify({
9335
+ q: query,
9336
+ scope: "webpage",
9337
+ size: topK
9338
+ }),
9339
+ signal: opts.signal
9340
+ });
9341
+ } catch (err) {
9342
+ if (err instanceof TypeError && err.message.includes("fetch")) {
9343
+ throw new Error(t("webErrors.cannotReach", { endpoint: METASO_ENDPOINT }));
9344
+ }
9345
+ throw err;
9346
+ }
9347
+ const raw = await resp.text();
9348
+ let data;
9349
+ try {
9350
+ data = JSON.parse(raw);
9351
+ } catch {
9352
+ throw new Error(t("webErrors.metasoParseError", { status: resp.status }));
9353
+ }
9354
+ if (!resp.ok) {
9355
+ if (resp.status === 401 || resp.status === 403) {
9356
+ throw new Error(t("webErrors.metasoUnauthorized"));
9357
+ }
9358
+ if (resp.status === 429) {
9359
+ throw new Error(t("webErrors.metasoRateLimit"));
9360
+ }
9361
+ throw new Error(t("webErrors.metasoServerError", { status: resp.status }));
9362
+ }
9363
+ if (data.code === 3003) {
9364
+ throw new Error(t("webErrors.metasoDailyLimit"));
9365
+ }
9366
+ if (data.code === 2005) {
9367
+ throw new Error(t("webErrors.metasoUnauthorized"));
9368
+ }
9369
+ if (data.code && data.code !== 0) {
9370
+ throw new Error(
9371
+ t("webErrors.metasoApiError", { code: data.code, message: data.message ?? "" })
9372
+ );
9373
+ }
9374
+ const webpages = data.webpages ?? [];
9375
+ if (webpages.length === 0) {
9376
+ return [];
9377
+ }
9378
+ return webpages.slice(0, topK).map((wp) => ({
9379
+ title: wp.title,
9380
+ url: wp.link,
9381
+ snippet: wp.snippet ?? wp.summary ?? ""
9382
+ }));
9383
+ }
9235
9384
  function parseSearxngHtmlResults(html) {
9236
9385
  const root = (0, import_node_html_parser.parse)(html);
9237
9386
  const results = [];
@@ -9450,7 +9599,7 @@ function registerWebTools(registry, opts = {}) {
9450
9599
  const maxFetchChars = opts.maxFetchChars ?? DEFAULT_FETCH_MAX_CHARS;
9451
9600
  registry.register({
9452
9601
  name: "web_search",
9453
- 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.",
9602
+ 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.",
9454
9603
  readOnly: true,
9455
9604
  parallelSafe: true,
9456
9605
  parameters: {
@@ -9519,6 +9668,51 @@ var import_picomatch2 = __toESM(require_picomatch(), 1);
9519
9668
  import { promises as fs3 } from "fs";
9520
9669
  import * as pathMod4 from "path";
9521
9670
 
9671
+ // src/memory/subdir.ts
9672
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
9673
+ import { dirname, join as join2, relative as relative2, resolve as resolve2 } from "path";
9674
+ function findSubdirMemoryAncestors(absPath, rootDir) {
9675
+ const root = resolve2(rootDir);
9676
+ const target = resolve2(absPath);
9677
+ const rel = relative2(root, target);
9678
+ if (!rel || rel.startsWith("..")) return [];
9679
+ const found = [];
9680
+ let cur = dirname(target);
9681
+ while (cur !== root) {
9682
+ const r = relative2(root, cur);
9683
+ if (!r || r.startsWith("..")) break;
9684
+ for (const name of PROJECT_MEMORY_FILES) {
9685
+ const path = join2(cur, name);
9686
+ if (existsSync2(path)) {
9687
+ found.push(path);
9688
+ break;
9689
+ }
9690
+ }
9691
+ const parent = dirname(cur);
9692
+ if (parent === cur) break;
9693
+ cur = parent;
9694
+ }
9695
+ return found;
9696
+ }
9697
+ function readSubdirMemoryContent(path) {
9698
+ let raw;
9699
+ try {
9700
+ raw = readFileSync2(path, "utf8");
9701
+ } catch {
9702
+ return null;
9703
+ }
9704
+ const trimmed = raw.trim();
9705
+ if (!trimmed) return null;
9706
+ if (trimmed.length <= PROJECT_MEMORY_MAX_CHARS) return trimmed;
9707
+ return `${trimmed.slice(0, PROJECT_MEMORY_MAX_CHARS)}
9708
+ \u2026 (truncated ${trimmed.length - PROJECT_MEMORY_MAX_CHARS} chars)`;
9709
+ }
9710
+ function formatSubdirMemorySection(displayPath, content) {
9711
+ return `[module memory: ${displayPath}]
9712
+
9713
+ ${content}`;
9714
+ }
9715
+
9522
9716
  // src/tools/fs/glob.ts
9523
9717
  var import_picomatch = __toESM(require_picomatch(), 1);
9524
9718
  import { promises as fs } from "fs";
@@ -9593,6 +9787,18 @@ var RUST_DECL_RE = /^(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|str
9593
9787
  var RUST_IMPL_RE = /^(?:unsafe\s+)?impl(?:\s*<[^>]+>)?\s+(?:[^{]+\s+for\s+)?(\w+)/;
9594
9788
  var MD_HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
9595
9789
  var MD_FENCE_RE = /^```/;
9790
+ var PROTO_TOP_RE = /^(message|service|enum|extend)\s+(\w+)/;
9791
+ var PROTO_RPC_RE = /^\s+rpc\s+(\w+)/;
9792
+ var CN_NUM = "[\\d\u96F6\u4E00\u4E8C\u4E09\u56DB\u4E94\u516D\u4E03\u516B\u4E5D\u5341\u767E\u5343\u4E07\uFF10-\uFF19]+";
9793
+ var TXT_CHAPTER_PATTERNS = [
9794
+ new RegExp(`^\u7B2C${CN_NUM}[\u7AE0\u8282\u56DE].{0,80}$`),
9795
+ new RegExp(`^\u5377${CN_NUM}.{0,80}$`),
9796
+ /^(?:序章|楔子|番外篇?|前言|后记|尾声|引子)(?:[\s\u3000::、—\-.].{0,80})?$/,
9797
+ /^Chapter\s+(?:\d+|[IVXLCDMivxlcdm]+|[A-Za-z]+)\b.{0,80}$/,
9798
+ /^CHAPTER\s+.{1,80}$/,
9799
+ /^Part\s+(?:\d+|[IVXLCDMivxlcdm]+)\b.{0,80}$/,
9800
+ /^PART\s+.{1,80}$/
9801
+ ];
9596
9802
  var EXT_TO_LANG = {
9597
9803
  ".ts": "ts",
9598
9804
  ".tsx": "ts",
@@ -9608,7 +9814,10 @@ var EXT_TO_LANG = {
9608
9814
  ".rs": "rust",
9609
9815
  ".md": "md",
9610
9816
  ".markdown": "md",
9611
- ".mdx": "md"
9817
+ ".mdx": "md",
9818
+ ".proto": "proto",
9819
+ ".txt": "txt",
9820
+ ".text": "txt"
9612
9821
  };
9613
9822
  function extractOutline(filename, lines) {
9614
9823
  const ext = pathMod2.extname(filename).toLowerCase();
@@ -9625,6 +9834,10 @@ function extractOutline(filename, lines) {
9625
9834
  return extractRust(lines);
9626
9835
  case "md":
9627
9836
  return extractMarkdown(lines);
9837
+ case "proto":
9838
+ return extractProto(lines);
9839
+ case "txt":
9840
+ return extractText(lines);
9628
9841
  }
9629
9842
  }
9630
9843
  function extractTs(lines) {
@@ -9676,6 +9889,36 @@ function extractRust(lines) {
9676
9889
  }
9677
9890
  return out;
9678
9891
  }
9892
+ function extractProto(lines) {
9893
+ const out = [];
9894
+ for (let i = 0; i < lines.length; i++) {
9895
+ const line = lines[i];
9896
+ if (!line.startsWith(" ") && !line.startsWith(" ")) {
9897
+ const m = PROTO_TOP_RE.exec(line);
9898
+ if (m) {
9899
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
9900
+ continue;
9901
+ }
9902
+ }
9903
+ const rpc = PROTO_RPC_RE.exec(line);
9904
+ if (rpc) out.push({ line: i + 1, text: `rpc ${rpc[1]}` });
9905
+ }
9906
+ return out;
9907
+ }
9908
+ function extractText(lines) {
9909
+ const out = [];
9910
+ for (let i = 0; i < lines.length; i++) {
9911
+ const line = lines[i].trim();
9912
+ if (line.length === 0 || line.length > 100) continue;
9913
+ for (const re of TXT_CHAPTER_PATTERNS) {
9914
+ if (re.test(line)) {
9915
+ out.push({ line: i + 1, text: line });
9916
+ break;
9917
+ }
9918
+ }
9919
+ }
9920
+ return out;
9921
+ }
9679
9922
  function extractMarkdown(lines) {
9680
9923
  const out = [];
9681
9924
  let inFence = false;
@@ -9923,17 +10166,25 @@ async function searchContent(ctx, startAbs, args) {
9923
10166
  }
9924
10167
 
9925
10168
  // src/tools/filesystem.ts
9926
- var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
10169
+ var DEFAULT_OUTLINE_THRESHOLD_BYTES = 512 * 1024;
9927
10170
  var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
9928
- var DEFAULT_AUTO_PREVIEW_LINES = 200;
9929
- var AUTO_PREVIEW_HEAD_LINES = 80;
9930
- var AUTO_PREVIEW_TAIL_LINES = 40;
9931
- var OUTLINE_MAX_ENTRIES2 = 30;
10171
+ var HARD_MAX_FILE_BYTES = 32 * 1024 * 1024;
10172
+ var OUTLINE_HEAD_LINES = 80;
9932
10173
  var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
9933
10174
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
9934
10175
  function displayRel3(rootDir, full) {
9935
10176
  return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
9936
10177
  }
10178
+ function looksLikeAbsoluteSystemPath(raw) {
10179
+ if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
10180
+ return /^\/(?:home|Users|etc|var|opt|tmp|usr|mnt|Library|Volumes|proc|sys|dev|run|srv|media|Applications|System|root|boot|private)(?:[/\\]|$)/.test(
10181
+ raw
10182
+ );
10183
+ }
10184
+ function pathIsUnder(child, parent) {
10185
+ const rel = pathMod4.relative(parent, child);
10186
+ return rel === "" || !rel.startsWith("..") && !pathMod4.isAbsolute(rel);
10187
+ }
9937
10188
  var GLOB_METACHARS = /[*?{[]/;
9938
10189
  function compileNameFilter(filter) {
9939
10190
  if (!filter) return null;
@@ -9950,24 +10201,45 @@ function isLikelyBinaryByName(name) {
9950
10201
  if (dot < 0) return false;
9951
10202
  return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
9952
10203
  }
10204
+ function looksBinary(buf) {
10205
+ const end = Math.min(buf.length, 8192);
10206
+ for (let i = 0; i < end; i++) {
10207
+ if (buf[i] === 0) return true;
10208
+ }
10209
+ return false;
10210
+ }
10211
+ function formatBytes(n) {
10212
+ if (n < 1024) return `${n} B`;
10213
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
10214
+ if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MiB`;
10215
+ return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
10216
+ }
9953
10217
  function registerFilesystemTools(registry, opts) {
9954
10218
  const rootDir = pathMod4.resolve(opts.rootDir);
9955
10219
  const allowWriting = opts.allowWriting !== false;
9956
- const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
10220
+ const outlineThresholdBytes = opts.outlineThresholdBytes ?? DEFAULT_OUTLINE_THRESHOLD_BYTES;
9957
10221
  const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
9958
10222
  const normRoot = pathMod4.resolve(rootDir);
9959
10223
  const sessionApproved = /* @__PURE__ */ new Set();
9960
- const inflightGate = /* @__PURE__ */ new Map();
9961
- function pathIsUnder(child, parent) {
9962
- const rel = pathMod4.relative(parent, child);
9963
- return rel === "" || !rel.startsWith("..") && !pathMod4.isAbsolute(rel);
9964
- }
9965
- function looksLikeAbsoluteSystemPath(raw) {
9966
- if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
9967
- return /^\/(?:home|Users|etc|var|opt|tmp|usr|mnt|Library|Volumes|proc|sys|dev|run|srv|media|Applications|System|root|boot|private)(?:[/\\]|$)/.test(
9968
- raw
9969
- );
10224
+ const shownSubdirMemory = /* @__PURE__ */ new Set();
10225
+ function withSubdirMemory(absPath, body) {
10226
+ if (!memoryEnabled()) return body;
10227
+ const ancestors = findSubdirMemoryAncestors(absPath, rootDir);
10228
+ if (ancestors.length === 0) return body;
10229
+ const sections = [];
10230
+ for (const memPath of [...ancestors].reverse()) {
10231
+ if (shownSubdirMemory.has(memPath)) continue;
10232
+ const content = readSubdirMemoryContent(memPath);
10233
+ if (!content) continue;
10234
+ shownSubdirMemory.add(memPath);
10235
+ sections.push(formatSubdirMemorySection(displayRel3(rootDir, memPath), content));
10236
+ }
10237
+ if (sections.length === 0) return body;
10238
+ return `${sections.join("\n\n")}
10239
+
10240
+ ${body}`;
9970
10241
  }
10242
+ const inflightGate = /* @__PURE__ */ new Map();
9971
10243
  async function ensureOutsideSandboxAllowed(abs, intent, toolName, ctx) {
9972
10244
  for (const dir of loadProjectPathAllowed(rootDir)) {
9973
10245
  if (pathIsUnder(abs, dir)) return;
@@ -10032,11 +10304,11 @@ function registerFilesystemTools(registry, opts) {
10032
10304
  registry.register({
10033
10305
  name: "read_file",
10034
10306
  parallelSafe: true,
10035
- description: `Read a file under the sandbox root. To save context, PREFER to scope the read instead of pulling the whole file:
10036
- - head: N \u2192 first N lines (imports, public API, small configs)
10037
- - tail: N \u2192 last N lines (recently-added code, log tails)
10038
- - range: "A-B" \u2192 inclusive line range A..B, 1-indexed (e.g. "120-180" around an edit site)
10039
- 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.`,
10307
+ 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:
10308
+ - head: N \u2192 first N lines (cheap probe of imports / config head)
10309
+ - tail: N \u2192 last N lines (recent-tail of a log)
10310
+ - range: "A-B" \u2192 inclusive 1-indexed range (e.g. "120-180" around an edit site)
10311
+ 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.`,
10040
10312
  readOnly: true,
10041
10313
  stormExempt: true,
10042
10314
  parameters: {
@@ -10054,22 +10326,31 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
10054
10326
  },
10055
10327
  fn: async (args, ctx) => {
10056
10328
  const abs = await safePath(args.path, "read_file", ctx);
10329
+ const rel = displayRel3(rootDir, abs);
10057
10330
  const fh = await fs3.open(abs, "r");
10058
10331
  let raw;
10332
+ let sizeBytes;
10059
10333
  try {
10060
10334
  const stat2 = await fh.stat();
10061
10335
  if (stat2.isDirectory()) {
10062
10336
  throw new Error(`not a file: ${args.path} (it's a directory)`);
10063
10337
  }
10338
+ sizeBytes = stat2.size;
10339
+ if (sizeBytes > HARD_MAX_FILE_BYTES) {
10340
+ return [
10341
+ `[refused: ${rel} is ${formatBytes(sizeBytes)} (> ${formatBytes(HARD_MAX_FILE_BYTES)} hard ceiling) \u2014 too large to load]`,
10342
+ "Use one of:",
10343
+ ` - search_content path:"${rel}" pattern:"<your regex>" \u2014 grep within the file`,
10344
+ ` - read_file path:"${rel}" range:"A-B" \u2014 read a specific 1-indexed line range`,
10345
+ ` - read_file path:"${rel}" head:N / tail:N \u2014 read N lines at the start or end`
10346
+ ].join("\n");
10347
+ }
10064
10348
  raw = await fh.readFile();
10065
10349
  } finally {
10066
10350
  await fh.close();
10067
10351
  }
10068
- if (raw.length > maxReadBytes) {
10069
- const headBytes = raw.slice(0, maxReadBytes).toString("utf8");
10070
- return `${headBytes}
10071
-
10072
- [\u2026truncated ${raw.length - maxReadBytes} bytes \u2014 file is ${raw.length} B, cap ${maxReadBytes} B. Retry with head/tail/range for targeted view.]`;
10352
+ if (looksBinary(raw)) {
10353
+ return `[refused: ${rel} appears to be binary (${formatBytes(sizeBytes)}) \u2014 read_file returns text only. Use get_file_info for stat.]`;
10073
10354
  }
10074
10355
  const text = raw.toString("utf8");
10075
10356
  let lines = text.split(/\r?\n/);
@@ -10081,8 +10362,8 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
10081
10362
  const end = Math.min(totalLines, Math.max(start, rawEnd ?? totalLines));
10082
10363
  const slice = lines.slice(start - 1, end);
10083
10364
  const label = `[range ${start}-${end} of ${totalLines} lines]`;
10084
- return `${label}
10085
- ${slice.join("\n")}`;
10365
+ return withSubdirMemory(abs, `${label}
10366
+ ${slice.join("\n")}`);
10086
10367
  }
10087
10368
  if (typeof args.head === "number" && args.head > 0) {
10088
10369
  const count = Math.min(args.head, totalLines);
@@ -10090,7 +10371,7 @@ ${slice.join("\n")}`;
10090
10371
  const marker = count < totalLines ? `
10091
10372
 
10092
10373
  [\u2026head ${count} of ${totalLines} lines \u2014 call again with range / tail for more]` : "";
10093
- return slice.join("\n") + marker;
10374
+ return withSubdirMemory(abs, slice.join("\n") + marker);
10094
10375
  }
10095
10376
  if (typeof args.tail === "number" && args.tail > 0) {
10096
10377
  const count = Math.min(args.tail, totalLines);
@@ -10098,25 +10379,26 @@ ${slice.join("\n")}`;
10098
10379
  const marker = count < totalLines ? `[\u2026tail ${count} of ${totalLines} lines \u2014 call again with range / head for more]
10099
10380
 
10100
10381
  ` : "";
10101
- return marker + slice.join("\n");
10382
+ return withSubdirMemory(abs, marker + slice.join("\n"));
10102
10383
  }
10103
- if (totalLines <= DEFAULT_AUTO_PREVIEW_LINES) return lines.join("\n");
10104
- const head = lines.slice(0, AUTO_PREVIEW_HEAD_LINES).join("\n");
10105
- const tail = lines.slice(totalLines - AUTO_PREVIEW_TAIL_LINES).join("\n");
10106
- const omitted = totalLines - AUTO_PREVIEW_HEAD_LINES - AUTO_PREVIEW_TAIL_LINES;
10384
+ if (sizeBytes <= outlineThresholdBytes) return withSubdirMemory(abs, lines.join("\n"));
10385
+ const head = lines.slice(0, Math.min(OUTLINE_HEAD_LINES, totalLines)).join("\n");
10107
10386
  const outline = formatOutline(extractOutline(abs, lines));
10108
10387
  const parts = [
10109
- `[auto-preview: head ${AUTO_PREVIEW_HEAD_LINES} + tail ${AUTO_PREVIEW_TAIL_LINES} of ${totalLines} lines]`,
10388
+ `[large file: ${formatBytes(sizeBytes)}, ${totalLines} lines \u2014 outline mode (threshold ${formatBytes(outlineThresholdBytes)})]`,
10389
+ "",
10390
+ `[head ${Math.min(OUTLINE_HEAD_LINES, totalLines)} lines for orientation]`,
10110
10391
  head
10111
10392
  ];
10112
10393
  if (outline) parts.push("", outline);
10113
10394
  parts.push(
10114
- `
10115
- [\u2026 ${omitted} lines omitted \u2014 call read_file again with range:"A-B" (1-indexed) or head / tail to get the middle]
10116
- `,
10117
- tail
10395
+ "",
10396
+ "[to read more, call one of:",
10397
+ ` - read_file path:"${rel}" range:"A-B" \u2014 1-indexed line range`,
10398
+ ` - read_file path:"${rel}" head:N / tail:N \u2014 first/last N lines`,
10399
+ ` - search_content path:"${rel}" pattern:"..." \u2014 grep within this file]`
10118
10400
  );
10119
- return parts.join("\n");
10401
+ return withSubdirMemory(abs, parts.join("\n"));
10120
10402
  }
10121
10403
  });
10122
10404
  registry.register({
@@ -10857,7 +11139,7 @@ var VERIFY_SYSTEM = `You are a verify subagent. Narrow check \u2014 return YES /
10857
11139
  How to operate:
10858
11140
  - Read only what's needed to verify the specific claim. No exploration past the claim.
10859
11141
  - Use search_content / read_file to confirm the exact behavior, type, or call site in question.
10860
- - Cap at 6-8 tool calls. If you can't verify in that, return INCONCLUSIVE plus what's missing.
11142
+ - If a focused round of reads can't verify it, return INCONCLUSIVE plus what's missing \u2014 don't keep digging.
10861
11143
 
10862
11144
  Final answer:
10863
11145
  - Lead with VERIFIED / NOT VERIFIED / INCONCLUSIVE.
@@ -10868,8 +11150,8 @@ ${NEGATIVE_CLAIM_RULE}
10868
11150
 
10869
11151
  ${TUI_FORMATTING_RULES}`;
10870
11152
  var TYPES = {
10871
- explore: { system: EXPLORE_SYSTEM, maxToolIters: 20 },
10872
- verify: { system: VERIFY_SYSTEM, maxToolIters: 8 }
11153
+ explore: { system: EXPLORE_SYSTEM },
11154
+ verify: { system: VERIFY_SYSTEM }
10873
11155
  };
10874
11156
  var SUBAGENT_TYPE_NAMES = Object.freeze(
10875
11157
  Object.keys(TYPES)
@@ -10893,18 +11175,12 @@ ${NEGATIVE_CLAIM_RULE}
10893
11175
 
10894
11176
  ${TUI_FORMATTING_RULES}`;
10895
11177
  var DEFAULT_MAX_RESULT_CHARS2 = 8e3;
10896
- var DEFAULT_PAUSE_EVERY = 16;
10897
- var BUDGET_WARN_THRESHOLD = 3;
10898
- function budgetParagraph(maxToolIters) {
10899
- 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.`;
10900
- }
10901
11178
  var DEFAULT_SUBAGENT_MODEL = "deepseek-v4-flash";
10902
11179
  var DEFAULT_SUBAGENT_EFFORT = "high";
10903
11180
  var SUBAGENT_TOOL_NAME = "spawn_subagent";
10904
11181
  var NEVER_INHERITED_TOOLS = /* @__PURE__ */ new Set([SUBAGENT_TOOL_NAME, "submit_plan"]);
10905
11182
  async function spawnSubagent(opts) {
10906
11183
  const model = opts.model ?? DEFAULT_SUBAGENT_MODEL;
10907
- const maxToolIters = opts.maxToolIters ?? DEFAULT_PAUSE_EVERY;
10908
11184
  const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
10909
11185
  const sink = opts.sink;
10910
11186
  const skillName = opts.skillName;
@@ -10957,26 +11233,8 @@ async function spawnSubagent(opts) {
10957
11233
  new Set(opts.allowedTools),
10958
11234
  NEVER_INHERITED_TOOLS
10959
11235
  ) : forkRegistryExcluding(opts.parentRegistry, NEVER_INHERITED_TOOLS);
10960
- let dispatchCount = 0;
10961
- childTools.setResultAugmenter((_name, _args, result) => {
10962
- dispatchCount++;
10963
- const remaining = maxToolIters - dispatchCount;
10964
- if (remaining <= 0) {
10965
- return `${result}
10966
-
10967
- [budget: 0 of ${maxToolIters} tool calls left \u2014 finalize NOW; the next tool call will be refused]`;
10968
- }
10969
- if (remaining <= BUDGET_WARN_THRESHOLD) {
10970
- return `${result}
10971
-
10972
- [budget: ${remaining} of ${maxToolIters} tool call${remaining === 1 ? "" : "s"} left \u2014 wrap up soon]`;
10973
- }
10974
- return result;
10975
- });
10976
11236
  const childPrefix = new ImmutablePrefix({
10977
- system: `${opts.system}
10978
-
10979
- ${budgetParagraph(maxToolIters)}`,
11237
+ system: opts.system,
10980
11238
  toolSpecs: childTools.specs()
10981
11239
  });
10982
11240
  const childLoop = new CacheFirstLoop({
@@ -10988,11 +11246,9 @@ ${budgetParagraph(maxToolIters)}`,
10988
11246
  // task is already narrow by construction, and `high` cuts output
10989
11247
  // tokens substantially vs `max`.
10990
11248
  reasoningEffort: DEFAULT_SUBAGENT_EFFORT,
10991
- maxToolIters,
10992
11249
  hooks: [],
10993
11250
  stream: true,
10994
- session: sessionName,
10995
- onIterBudgetExhausted: "pause"
11251
+ session: sessionName
10996
11252
  });
10997
11253
  const onParentAbort = () => childLoop.abort();
10998
11254
  if (opts.parentSignal?.aborted) {
@@ -11004,13 +11260,9 @@ ${budgetParagraph(maxToolIters)}`,
11004
11260
  let errorMessage;
11005
11261
  let toolIter = 0;
11006
11262
  let summarisingEmitted = false;
11007
- let paused = false;
11008
- let partialSummary;
11009
- const taskForLoop = opts.resumeSession ? `[Resume: your tool-call budget has been refreshed with ${maxToolIters} new calls. Earlier "wrap up" / "finalize NOW" budget hints in this conversation referred to the previous window \u2014 they no longer apply. Continue the work you were given.]
11010
-
11011
- ${opts.task}` : opts.task;
11263
+ let forcedSummaryFired = false;
11012
11264
  try {
11013
- for await (const ev of childLoop.step(taskForLoop)) {
11265
+ for await (const ev of childLoop.step(opts.task)) {
11014
11266
  sink?.current?.({ kind: "inner", runId, task: taskPreview, skillName, model, inner: ev });
11015
11267
  if (ev.role === "tool") {
11016
11268
  toolIter++;
@@ -11040,7 +11292,12 @@ ${opts.task}` : opts.task;
11040
11292
  }
11041
11293
  if (ev.role === "assistant_final") {
11042
11294
  if (ev.forcedSummary) {
11043
- errorMessage = ev.content?.trim() || "subagent ended without producing an answer";
11295
+ if (opts.parentSignal?.aborted) {
11296
+ errorMessage = ev.content?.trim() || "subagent aborted before producing an answer";
11297
+ } else {
11298
+ final = ev.content ?? "";
11299
+ forcedSummaryFired = true;
11300
+ }
11044
11301
  } else {
11045
11302
  final = ev.content ?? "";
11046
11303
  }
@@ -11048,17 +11305,13 @@ ${opts.task}` : opts.task;
11048
11305
  if (ev.role === "error") {
11049
11306
  errorMessage = ev.error ?? "subagent error";
11050
11307
  }
11051
- if (ev.role === "paused") {
11052
- paused = true;
11053
- if (ev.partialSummary) partialSummary = ev.partialSummary;
11054
- }
11055
11308
  }
11056
11309
  } catch (err) {
11057
11310
  errorMessage = err.message;
11058
11311
  } finally {
11059
11312
  opts.parentSignal?.removeEventListener("abort", onParentAbort);
11060
11313
  }
11061
- if (!errorMessage && !final && !paused) {
11314
+ if (!errorMessage && !final) {
11062
11315
  errorMessage = opts.parentSignal?.aborted ? "subagent aborted before producing an answer" : "subagent ended without producing an answer";
11063
11316
  }
11064
11317
  const elapsedMs = Date.now() - startedAt;
@@ -11083,7 +11336,7 @@ ${opts.task}` : opts.task;
11083
11336
  usage
11084
11337
  });
11085
11338
  return {
11086
- success: !errorMessage,
11339
+ success: !errorMessage && !forcedSummaryFired,
11087
11340
  output: errorMessage ? "" : truncated,
11088
11341
  error: errorMessage,
11089
11342
  turns,
@@ -11093,9 +11346,7 @@ ${opts.task}` : opts.task;
11093
11346
  model,
11094
11347
  skillName,
11095
11348
  usage,
11096
- paused: paused || void 0,
11097
- pausedSession: paused ? sessionName : void 0,
11098
- partialSummary: paused ? partialSummary : void 0
11349
+ forcedSummary: forcedSummaryFired || void 0
11099
11350
  };
11100
11351
  }
11101
11352
  function aggregateChildUsage(loop) {
@@ -11110,16 +11361,16 @@ function aggregateChildUsage(loop) {
11110
11361
  return agg;
11111
11362
  }
11112
11363
  function formatSubagentResult(r) {
11113
- if (r.paused) {
11364
+ if (r.forcedSummary) {
11114
11365
  return JSON.stringify({
11115
11366
  success: false,
11116
- paused: true,
11117
- resume_session: r.pausedSession,
11367
+ partial: true,
11368
+ output: r.output,
11369
+ turns: r.turns,
11118
11370
  tool_iters: r.toolIters,
11119
11371
  elapsed_ms: r.elapsedMs,
11120
11372
  cost_usd: r.costUsd,
11121
- partial_summary: r.partialSummary,
11122
- note: `Subagent reached its pause-every interval (${r.toolIters} tool calls) without producing a final answer. Read partial_summary above to see what was done / left / blocked, then decide: resume by calling spawn_subagent again with resume_session="${r.pausedSession}" (the task arg becomes a continuation nudge \u2014 e.g. "finish what you started"), or accept the partial work and proceed with what you already know.`
11373
+ 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."
11123
11374
  });
11124
11375
  }
11125
11376
  if (!r.success) {
@@ -11169,18 +11420,18 @@ function forkRegistryWithAllowList(parent, allow, alsoExclude) {
11169
11420
  // src/code/edit-blocks.ts
11170
11421
  import {
11171
11422
  closeSync,
11172
- existsSync as existsSync2,
11423
+ existsSync as existsSync3,
11173
11424
  fstatSync,
11174
11425
  ftruncateSync,
11175
11426
  mkdirSync,
11176
11427
  openSync,
11177
- readFileSync as readFileSync2,
11428
+ readFileSync as readFileSync3,
11178
11429
  readSync,
11179
11430
  unlinkSync,
11180
11431
  writeFileSync,
11181
11432
  writeSync
11182
11433
  } from "fs";
11183
- import { dirname as dirname2, resolve as resolve3 } from "path";
11434
+ import { dirname as dirname3, resolve as resolve4 } from "path";
11184
11435
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
11185
11436
  function parseEditBlocks(text) {
11186
11437
  const out = [];
@@ -11198,8 +11449,8 @@ function parseEditBlocks(text) {
11198
11449
  return out;
11199
11450
  }
11200
11451
  function applyEditBlock(block, rootDir) {
11201
- const absRoot = resolve3(rootDir);
11202
- const absTarget = resolve3(absRoot, block.path);
11452
+ const absRoot = resolve4(rootDir);
11453
+ const absTarget = resolve4(absRoot, block.path);
11203
11454
  if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
11204
11455
  return {
11205
11456
  path: block.path,
@@ -11210,7 +11461,7 @@ function applyEditBlock(block, rootDir) {
11210
11461
  const searchEmpty = block.search.length === 0;
11211
11462
  if (searchEmpty) {
11212
11463
  try {
11213
- mkdirSync(dirname2(absTarget), { recursive: true });
11464
+ mkdirSync(dirname3(absTarget), { recursive: true });
11214
11465
  const fd = openSync(absTarget, "wx");
11215
11466
  try {
11216
11467
  writeSync(fd, block.replace);
@@ -11286,11 +11537,11 @@ function applyEditBlocks(blocks, rootDir) {
11286
11537
  return blocks.map((b) => applyEditBlock(b, rootDir));
11287
11538
  }
11288
11539
  function toWholeFileEditBlock(path, content, rootDir) {
11289
- const abs = resolve3(rootDir, path);
11540
+ const abs = resolve4(rootDir, path);
11290
11541
  let search = "";
11291
- if (existsSync2(abs)) {
11542
+ if (existsSync3(abs)) {
11292
11543
  try {
11293
- search = readFileSync2(abs, "utf8");
11544
+ search = readFileSync3(abs, "utf8");
11294
11545
  } catch {
11295
11546
  search = "";
11296
11547
  }
@@ -11298,19 +11549,19 @@ function toWholeFileEditBlock(path, content, rootDir) {
11298
11549
  return { path, search, replace: content, offset: 0 };
11299
11550
  }
11300
11551
  function snapshotBeforeEdits(blocks, rootDir) {
11301
- const absRoot = resolve3(rootDir);
11552
+ const absRoot = resolve4(rootDir);
11302
11553
  const seen = /* @__PURE__ */ new Set();
11303
11554
  const snapshots = [];
11304
11555
  for (const b of blocks) {
11305
11556
  if (seen.has(b.path)) continue;
11306
11557
  seen.add(b.path);
11307
- const abs = resolve3(absRoot, b.path);
11308
- if (!existsSync2(abs)) {
11558
+ const abs = resolve4(absRoot, b.path);
11559
+ if (!existsSync3(abs)) {
11309
11560
  snapshots.push({ path: b.path, prevContent: null });
11310
11561
  continue;
11311
11562
  }
11312
11563
  try {
11313
- snapshots.push({ path: b.path, prevContent: readFileSync2(abs, "utf8") });
11564
+ snapshots.push({ path: b.path, prevContent: readFileSync3(abs, "utf8") });
11314
11565
  } catch {
11315
11566
  snapshots.push({ path: b.path, prevContent: null });
11316
11567
  }
@@ -11318,9 +11569,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
11318
11569
  return snapshots;
11319
11570
  }
11320
11571
  function restoreSnapshots(snapshots, rootDir) {
11321
- const absRoot = resolve3(rootDir);
11572
+ const absRoot = resolve4(rootDir);
11322
11573
  return snapshots.map((snap) => {
11323
- const abs = resolve3(absRoot, snap.path);
11574
+ const abs = resolve4(absRoot, snap.path);
11324
11575
  if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
11325
11576
  return {
11326
11577
  path: snap.path,
@@ -11330,7 +11581,7 @@ function restoreSnapshots(snapshots, rootDir) {
11330
11581
  }
11331
11582
  try {
11332
11583
  if (snap.prevContent === null) {
11333
- if (existsSync2(abs)) unlinkSync(abs);
11584
+ if (existsSync3(abs)) unlinkSync(abs);
11334
11585
  return {
11335
11586
  path: snap.path,
11336
11587
  status: "applied",
@@ -11369,6 +11620,8 @@ export {
11369
11620
  detectAtPicker,
11370
11621
  rankPickerCandidates,
11371
11622
  expandAtMentions,
11623
+ looksLikeAbsoluteSystemPath,
11624
+ pathIsUnder,
11372
11625
  registerFilesystemTools,
11373
11626
  registerMemoryTools,
11374
11627
  registerChoiceTool,
@@ -11389,4 +11642,4 @@ export {
11389
11642
  he/he.js:
11390
11643
  (*! https://mths.be/he v1.2.0 by @mathias | MIT license *)
11391
11644
  */
11392
- //# sourceMappingURL=chunk-2R4QCDOZ.js.map
11645
+ //# sourceMappingURL=chunk-OPFUUYHL.js.map