reasonix 0.48.0 → 0.49.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 (130) hide show
  1. package/README.md +9 -0
  2. package/dashboard/dist/app.js +123 -16
  3. package/dashboard/dist/app.js.map +1 -1
  4. package/dist/cli/{acp-4ROCGYNH.js → acp-WFQIC6SO.js} +52 -135
  5. package/dist/cli/acp-WFQIC6SO.js.map +1 -0
  6. package/dist/cli/chat-D32JGNVH.js +51 -0
  7. package/dist/cli/{chunk-S2RMQULY.js → chunk-23ZPCIPR.js} +12 -9
  8. package/dist/cli/chunk-23ZPCIPR.js.map +1 -0
  9. package/dist/cli/{chunk-TKVXTQ3T.js → chunk-3ZZXQ3CZ.js} +27 -27
  10. package/dist/cli/chunk-3ZZXQ3CZ.js.map +1 -0
  11. package/dist/cli/{chunk-5OHHAQ4W.js → chunk-7AST3QQ3.js} +2 -2
  12. package/dist/cli/{chunk-MRZG4GBF.js → chunk-7JTKBJ2G.js} +3 -3
  13. package/dist/cli/{chunk-X53B3JIX.js → chunk-7X4JJOO7.js} +2 -61
  14. package/dist/cli/{chunk-X53B3JIX.js.map → chunk-7X4JJOO7.js.map} +1 -1
  15. package/dist/cli/{chunk-MOJYKO2A.js → chunk-ASOLXV67.js} +3 -3
  16. package/dist/cli/{chunk-7M4YYMKW.js → chunk-AWEULQG6.js} +49 -56
  17. package/dist/cli/{chunk-7M4YYMKW.js.map → chunk-AWEULQG6.js.map} +1 -1
  18. package/dist/cli/{chunk-HR5NBKEM.js → chunk-DFX5ZH5L.js} +2 -2
  19. package/dist/cli/{chunk-3WGTGXO4.js → chunk-GNS7BAT2.js} +4 -4
  20. package/dist/cli/chunk-GNS7BAT2.js.map +1 -0
  21. package/dist/cli/{chunk-TE5UIIFL.js → chunk-J2IHQGPQ.js} +12 -6
  22. package/dist/cli/chunk-J2IHQGPQ.js.map +1 -0
  23. package/dist/cli/{chunk-I4M5QJNL.js → chunk-JGTX4RRQ.js} +3 -3
  24. package/dist/cli/{chunk-FY4S7TJZ.js → chunk-JNTMOX7G.js} +10 -2
  25. package/dist/cli/chunk-JNTMOX7G.js.map +1 -0
  26. package/dist/cli/{chunk-OB4BUJBL.js → chunk-MGTBP7GG.js} +5 -2
  27. package/dist/cli/chunk-MGTBP7GG.js.map +1 -0
  28. package/dist/cli/{chunk-OPYALNTT.js → chunk-MQWO32ZD.js} +387 -184
  29. package/dist/cli/chunk-MQWO32ZD.js.map +1 -0
  30. package/dist/cli/{chunk-2QSTA2QV.js → chunk-O5LIHAMP.js} +8 -4
  31. package/dist/cli/chunk-O5LIHAMP.js.map +1 -0
  32. package/dist/cli/{chunk-NMQSUNLB.js → chunk-PB3MAFEI.js} +6 -3
  33. package/dist/cli/chunk-PB3MAFEI.js.map +1 -0
  34. package/dist/cli/{chunk-H4CCXMDD.js → chunk-PEMG6CUB.js} +2 -2
  35. package/dist/cli/{chunk-RUDBUHO4.js → chunk-PXBQ6IZ7.js} +3 -3
  36. package/dist/cli/{chunk-J2TQAWOM.js → chunk-Q46B3Z7H.js} +25 -10
  37. package/dist/cli/{chunk-J2TQAWOM.js.map → chunk-Q46B3Z7H.js.map} +1 -1
  38. package/dist/cli/{chunk-6MZTZO7A.js → chunk-QF32ROX2.js} +2152 -2613
  39. package/dist/cli/chunk-QF32ROX2.js.map +1 -0
  40. package/dist/cli/{chunk-OG5JANQ4.js → chunk-QX5TWXRZ.js} +2 -2
  41. package/dist/cli/{chunk-V4Y732RQ.js → chunk-TAIKVL35.js} +2 -2
  42. package/dist/cli/{chunk-B5CZL2SE.js → chunk-TEDWJKEI.js} +4 -9
  43. package/dist/cli/chunk-TEDWJKEI.js.map +1 -0
  44. package/dist/cli/{chunk-EMMENC4O.js → chunk-U5XQDCK7.js} +5 -5
  45. package/dist/cli/{chunk-DOWEOA6E.js → chunk-W46ZMNKO.js} +3 -3
  46. package/dist/cli/{chunk-CDVSFSAK.js → chunk-WMTMMSXU.js} +184 -8
  47. package/dist/cli/chunk-WMTMMSXU.js.map +1 -0
  48. package/dist/cli/{chunk-YW63N3ZR.js → chunk-YEF7C4XI.js} +270 -96
  49. package/dist/cli/chunk-YEF7C4XI.js.map +1 -0
  50. package/dist/cli/{chunk-JMDE6IO3.js → chunk-ZAEJWKXB.js} +2 -2
  51. package/dist/cli/chunk-ZWHSHFDP.js +6173 -0
  52. package/dist/cli/chunk-ZWHSHFDP.js.map +1 -0
  53. package/dist/cli/{code-PMPJWXEO.js → code-R4IHI7SR.js} +30 -30
  54. package/dist/cli/{commands-QS6TG4G3.js → commands-DRHFCYMO.js} +4 -4
  55. package/dist/cli/{commit-XPRSKUBF.js → commit-AG5KB4YP.js} +3 -3
  56. package/dist/cli/{desktop-562OPWIU.js → desktop-JGL6GORA.js} +60 -23
  57. package/dist/cli/desktop-JGL6GORA.js.map +1 -0
  58. package/dist/cli/{diff-I6W4AUWJ.js → diff-4Z7ETWZO.js} +9 -9
  59. package/dist/cli/{doctor-6XVZKT4U.js → doctor-VA3RHQLB.js} +9 -9
  60. package/dist/cli/index.js +37 -36
  61. package/dist/cli/index.js.map +1 -1
  62. package/dist/cli/{mcp-7W7ANO2Y.js → mcp-LZO4HXFA.js} +34 -23
  63. package/dist/cli/mcp-LZO4HXFA.js.map +1 -0
  64. package/dist/cli/{mcp-browse-LA4I4YIZ.js → mcp-browse-C3GXVMYZ.js} +3 -3
  65. package/dist/cli/{mcp-inspect-LWXXU7BY.js → mcp-inspect-ZMYUNFDS.js} +2 -2
  66. package/dist/cli/{prompt-RKZD4X6Y.js → prompt-MC3U5KRP.js} +5 -4
  67. package/dist/cli/{prune-sessions-SEWX7GP6.js → prune-sessions-OEPFH4N6.js} +11 -7
  68. package/dist/cli/prune-sessions-OEPFH4N6.js.map +1 -0
  69. package/dist/cli/{replay-2X7MVXOI.js → replay-4TP7ZUMZ.js} +10 -10
  70. package/dist/cli/{run-TPKXIJ27.js → run-6MXQYBOE.js} +16 -15
  71. package/dist/cli/run-6MXQYBOE.js.map +1 -0
  72. package/dist/cli/{server-NHQ3QXOZ.js → server-Z3IMJNNI.js} +65 -12
  73. package/dist/cli/server-Z3IMJNNI.js.map +1 -0
  74. package/dist/cli/{sessions-2A4DGSHA.js → sessions-NXQ5SAV7.js} +18 -18
  75. package/dist/cli/sessions-NXQ5SAV7.js.map +1 -0
  76. package/dist/cli/{setup-GOLP7J4C.js → setup-LHZELI6I.js} +6 -6
  77. package/dist/cli/{stats-CGDAFDKI.js → stats-SUIJ3QWY.js} +6 -6
  78. package/dist/cli/{version-FIL4ZFOS.js → version-BIFONEUB.js} +13 -13
  79. package/dist/index.d.ts +71 -17
  80. package/dist/index.js +1040 -391
  81. package/dist/index.js.map +1 -1
  82. package/package.json +6 -2
  83. package/dist/cli/acp-4ROCGYNH.js.map +0 -1
  84. package/dist/cli/chat-GZNB5625.js +0 -51
  85. package/dist/cli/chunk-2QSTA2QV.js.map +0 -1
  86. package/dist/cli/chunk-3WGTGXO4.js.map +0 -1
  87. package/dist/cli/chunk-6MZTZO7A.js.map +0 -1
  88. package/dist/cli/chunk-B5CZL2SE.js.map +0 -1
  89. package/dist/cli/chunk-CDVSFSAK.js.map +0 -1
  90. package/dist/cli/chunk-FY4S7TJZ.js.map +0 -1
  91. package/dist/cli/chunk-NMQSUNLB.js.map +0 -1
  92. package/dist/cli/chunk-OB4BUJBL.js.map +0 -1
  93. package/dist/cli/chunk-OPYALNTT.js.map +0 -1
  94. package/dist/cli/chunk-S2RMQULY.js.map +0 -1
  95. package/dist/cli/chunk-TE5UIIFL.js.map +0 -1
  96. package/dist/cli/chunk-TKVXTQ3T.js.map +0 -1
  97. package/dist/cli/chunk-WZGNXR6E.js +0 -2020
  98. package/dist/cli/chunk-WZGNXR6E.js.map +0 -1
  99. package/dist/cli/chunk-YW63N3ZR.js.map +0 -1
  100. package/dist/cli/desktop-562OPWIU.js.map +0 -1
  101. package/dist/cli/mcp-7W7ANO2Y.js.map +0 -1
  102. package/dist/cli/prune-sessions-SEWX7GP6.js.map +0 -1
  103. package/dist/cli/run-TPKXIJ27.js.map +0 -1
  104. package/dist/cli/server-NHQ3QXOZ.js.map +0 -1
  105. package/dist/cli/sessions-2A4DGSHA.js.map +0 -1
  106. /package/dist/cli/{chat-GZNB5625.js.map → chat-D32JGNVH.js.map} +0 -0
  107. /package/dist/cli/{chunk-5OHHAQ4W.js.map → chunk-7AST3QQ3.js.map} +0 -0
  108. /package/dist/cli/{chunk-MRZG4GBF.js.map → chunk-7JTKBJ2G.js.map} +0 -0
  109. /package/dist/cli/{chunk-MOJYKO2A.js.map → chunk-ASOLXV67.js.map} +0 -0
  110. /package/dist/cli/{chunk-HR5NBKEM.js.map → chunk-DFX5ZH5L.js.map} +0 -0
  111. /package/dist/cli/{chunk-I4M5QJNL.js.map → chunk-JGTX4RRQ.js.map} +0 -0
  112. /package/dist/cli/{chunk-H4CCXMDD.js.map → chunk-PEMG6CUB.js.map} +0 -0
  113. /package/dist/cli/{chunk-RUDBUHO4.js.map → chunk-PXBQ6IZ7.js.map} +0 -0
  114. /package/dist/cli/{chunk-OG5JANQ4.js.map → chunk-QX5TWXRZ.js.map} +0 -0
  115. /package/dist/cli/{chunk-V4Y732RQ.js.map → chunk-TAIKVL35.js.map} +0 -0
  116. /package/dist/cli/{chunk-EMMENC4O.js.map → chunk-U5XQDCK7.js.map} +0 -0
  117. /package/dist/cli/{chunk-DOWEOA6E.js.map → chunk-W46ZMNKO.js.map} +0 -0
  118. /package/dist/cli/{chunk-JMDE6IO3.js.map → chunk-ZAEJWKXB.js.map} +0 -0
  119. /package/dist/cli/{code-PMPJWXEO.js.map → code-R4IHI7SR.js.map} +0 -0
  120. /package/dist/cli/{commands-QS6TG4G3.js.map → commands-DRHFCYMO.js.map} +0 -0
  121. /package/dist/cli/{commit-XPRSKUBF.js.map → commit-AG5KB4YP.js.map} +0 -0
  122. /package/dist/cli/{diff-I6W4AUWJ.js.map → diff-4Z7ETWZO.js.map} +0 -0
  123. /package/dist/cli/{doctor-6XVZKT4U.js.map → doctor-VA3RHQLB.js.map} +0 -0
  124. /package/dist/cli/{mcp-browse-LA4I4YIZ.js.map → mcp-browse-C3GXVMYZ.js.map} +0 -0
  125. /package/dist/cli/{mcp-inspect-LWXXU7BY.js.map → mcp-inspect-ZMYUNFDS.js.map} +0 -0
  126. /package/dist/cli/{prompt-RKZD4X6Y.js.map → prompt-MC3U5KRP.js.map} +0 -0
  127. /package/dist/cli/{replay-2X7MVXOI.js.map → replay-4TP7ZUMZ.js.map} +0 -0
  128. /package/dist/cli/{setup-GOLP7J4C.js.map → setup-LHZELI6I.js.map} +0 -0
  129. /package/dist/cli/{stats-CGDAFDKI.js.map → stats-SUIJ3QWY.js.map} +0 -0
  130. /package/dist/cli/{version-FIL4ZFOS.js.map → version-BIFONEUB.js.map} +0 -0
@@ -3,7 +3,7 @@ import { createRequire as __cr } from 'node:module'; if (typeof globalThis.requi
3
3
  import {
4
4
  MemoryStore,
5
5
  sanitizeMemoryName
6
- } from "./chunk-2QSTA2QV.js";
6
+ } from "./chunk-O5LIHAMP.js";
7
7
  import {
8
8
  countTokens,
9
9
  countTokensBounded,
@@ -12,23 +12,25 @@ import {
12
12
  } from "./chunk-6OWJV3YW.js";
13
13
  import {
14
14
  Usage
15
- } from "./chunk-TE5UIIFL.js";
15
+ } from "./chunk-J2IHQGPQ.js";
16
16
  import {
17
17
  applyEdit,
18
18
  applyMultiEdit,
19
+ decodeFileBuffer,
20
+ encodeFile,
19
21
  pauseGate
20
- } from "./chunk-WZGNXR6E.js";
22
+ } from "./chunk-ZWHSHFDP.js";
21
23
  import {
22
24
  NEGATIVE_CLAIM_RULE,
23
25
  PROJECT_MEMORY_FILES,
24
26
  PROJECT_MEMORY_MAX_CHARS,
25
27
  TUI_FORMATTING_RULES,
26
28
  memoryEnabled
27
- } from "./chunk-FY4S7TJZ.js";
29
+ } from "./chunk-JNTMOX7G.js";
28
30
  import {
29
31
  formatHookOutcomeMessage,
30
32
  runHooks
31
- } from "./chunk-NMQSUNLB.js";
33
+ } from "./chunk-PB3MAFEI.js";
32
34
  import {
33
35
  ignoredByLayers,
34
36
  loadGitignoreAt,
@@ -46,12 +48,13 @@ import {
46
48
  DEEPSEEK_CONTEXT_TOKENS,
47
49
  DEFAULT_CONTEXT_TOKENS,
48
50
  SessionStats
49
- } from "./chunk-OG5JANQ4.js";
51
+ } from "./chunk-QX5TWXRZ.js";
50
52
  import {
51
53
  t
52
- } from "./chunk-YW63N3ZR.js";
54
+ } from "./chunk-YEF7C4XI.js";
53
55
  import {
54
56
  DEFAULT_INDEX_EXCLUDES,
57
+ ToolRateLimiter,
55
58
  addProjectPathAllowed,
56
59
  loadExaApiKey,
57
60
  loadMemoryTypeRegistry,
@@ -59,10 +62,11 @@ import {
59
62
  loadPerplexityApiKey,
60
63
  loadProjectPathAllowed,
61
64
  loadTavilyApiKey,
65
+ parseRateLimitedToolResult,
62
66
  require_picomatch,
63
67
  webSearchEndpoint,
64
68
  webSearchEngine
65
- } from "./chunk-CDVSFSAK.js";
69
+ } from "./chunk-WMTMMSXU.js";
66
70
  import {
67
71
  __commonJS,
68
72
  __esm,
@@ -2599,10 +2603,10 @@ var require_helpers = __commonJS({
2599
2603
  return !arr.includes(node, i + 1);
2600
2604
  });
2601
2605
  nodes.sort(function(a, b) {
2602
- var relative6 = compareDocumentPosition(a, b);
2603
- if (relative6 & DocumentPosition.PRECEDING) {
2606
+ var relative7 = compareDocumentPosition(a, b);
2607
+ if (relative7 & DocumentPosition.PRECEDING) {
2604
2608
  return -1;
2605
- } else if (relative6 & DocumentPosition.FOLLOWING) {
2609
+ } else if (relative7 & DocumentPosition.FOLLOWING) {
2606
2610
  return 1;
2607
2611
  }
2608
2612
  return 0;
@@ -6052,12 +6056,12 @@ async function waitForReady(ready, timeoutMs, serverName, signal) {
6052
6056
  let timer;
6053
6057
  let onAbort;
6054
6058
  try {
6055
- await new Promise((resolve5, reject) => {
6059
+ await new Promise((resolve6, reject) => {
6056
6060
  ready.then(
6057
6061
  () => {
6058
6062
  if (settled) return;
6059
6063
  settled = true;
6060
- resolve5();
6064
+ resolve6();
6061
6065
  },
6062
6066
  (err) => {
6063
6067
  if (settled) return;
@@ -6314,12 +6318,14 @@ var ToolRegistry = class {
6314
6318
  _interceptors = [];
6315
6319
  _auditListener = null;
6316
6320
  _resultAugmenter = null;
6321
+ _rateLimiter;
6317
6322
  /** Per-tool fingerprint of the last call that failed schema validation. Cleared by any successful validation for that tool. */
6318
6323
  _lastMalformed = /* @__PURE__ */ new Map();
6319
6324
  /** Per-tool fingerprint of the last host-side gate rejection. */
6320
6325
  _lastGateRejection = /* @__PURE__ */ new Map();
6321
6326
  constructor(opts = {}) {
6322
6327
  this._autoFlatten = opts.autoFlatten !== false;
6328
+ this._rateLimiter = new ToolRateLimiter(opts.rateLimit);
6323
6329
  }
6324
6330
  /** Enable / disable plan-mode enforcement at dispatch. */
6325
6331
  setPlanMode(on) {
@@ -6356,6 +6362,9 @@ var ToolRegistry = class {
6356
6362
  get hasResultAugmenter() {
6357
6363
  return this._resultAugmenter !== null;
6358
6364
  }
6365
+ get rateLimitPolicy() {
6366
+ return this._rateLimiter.policy;
6367
+ }
6359
6368
  register(def) {
6360
6369
  if (!def.name) throw new Error("tool requires a name");
6361
6370
  const internal = { ...def };
@@ -6454,6 +6463,10 @@ var ToolRegistry = class {
6454
6463
  rejectedReason: "aborted"
6455
6464
  });
6456
6465
  }
6466
+ const rateLimit = this._rateLimiter.consume(name);
6467
+ if (!rateLimit.allowed) {
6468
+ return JSON.stringify(rateLimit.result);
6469
+ }
6457
6470
  let finalResult;
6458
6471
  try {
6459
6472
  try {
@@ -6462,7 +6475,8 @@ var ToolRegistry = class {
6462
6475
  }
6463
6476
  const result = await tool.fn(args, {
6464
6477
  signal: opts.signal,
6465
- confirmationGate: opts.confirmationGate
6478
+ confirmationGate: opts.confirmationGate,
6479
+ readTracker: opts.readTracker
6466
6480
  });
6467
6481
  const str = typeof result === "string" ? result : JSON.stringify(result);
6468
6482
  let clipped = str;
@@ -6547,6 +6561,9 @@ function plainTextRejectedReason(name, result) {
6547
6561
  if ((name === "edit_file" || name === "write_file") && /rejected this edit/i.test(result)) {
6548
6562
  return "edit-gate";
6549
6563
  }
6564
+ if ((name === "edit_file" || name === "multi_edit") && /read_file first/i.test(result)) {
6565
+ return "read-before-edit";
6566
+ }
6550
6567
  if ((name === "run_command" || name === "run_background") && /\buser denied:/i.test(result)) {
6551
6568
  return "shell-gate";
6552
6569
  }
@@ -6556,6 +6573,8 @@ function rejectionRecoveryHint(reason) {
6556
6573
  switch (reason) {
6557
6574
  case "edit-gate":
6558
6575
  return "Do not re-emit the same edit. Try a genuinely different edit or ask the user how to proceed.";
6576
+ case "read-before-edit":
6577
+ return "Call read_file on the target path first, then re-issue the edit.";
6559
6578
  case "shell-gate":
6560
6579
  return "Do not retry the same command. Use an allowlisted/read-only command, wait for approval, or ask the user how to proceed.";
6561
6580
  case "engineering-lifecycle":
@@ -6757,32 +6776,43 @@ function buildSyntheticAssistantMessage(content, fallbackModel) {
6757
6776
  }
6758
6777
 
6759
6778
  // src/context-manager.ts
6760
- var HISTORY_FOLD_THRESHOLD = 0.5;
6779
+ function extractPinnedConstraints(systemPrompt) {
6780
+ const pattern = /# (?:HIGH PRIORITY constraints|User memory|Project memory)[\s\S]*?(?=\n# |\n---|$)/g;
6781
+ return Array.from(systemPrompt.matchAll(pattern), (m) => m[0]).join("\n\n");
6782
+ }
6783
+ var HISTORY_FOLD_THRESHOLD = 0.75;
6761
6784
  var HISTORY_FOLD_TAIL_FRACTION = 0.2;
6762
- var HISTORY_FOLD_AGGRESSIVE_THRESHOLD = 0.7;
6785
+ var HISTORY_FOLD_AGGRESSIVE_THRESHOLD = 0.78;
6763
6786
  var HISTORY_FOLD_AGGRESSIVE_TAIL_FRACTION = 0.1;
6764
6787
  var HISTORY_FOLD_MIN_SAVINGS_FRACTION = 0.3;
6765
6788
  var FORCE_SUMMARY_THRESHOLD = 0.8;
6766
6789
  var PREFLIGHT_EMERGENCY_THRESHOLD = 0.95;
6767
6790
  var PREFLIGHT_MECHANICAL_TARGET_FRACTION = 0.7;
6791
+ var MAX_BODY_BYTES = 7e5;
6792
+ var MAX_BODY_BYTES_TARGET = 5e5;
6768
6793
  var HISTORY_FOLD_SUMMARY_TIMEOUT_MS = 15e3;
6769
6794
  var HISTORY_FOLD_MARKER = "[CONVERSATION HISTORY SUMMARY \u2014 earlier turns folded for context efficiency]\n\n";
6770
6795
  var SKILL_PIN_MEMO_HEADER = "[Active skill memos \u2014 preserved verbatim across the fold:]";
6771
6796
  var SKILL_PIN_REGEX = /<skill-pin name="([^"]+)">\n[\s\S]*?\n<\/skill-pin>/g;
6772
- function extractPinnedSkills(head) {
6797
+ function buildFoldSummaryInstruction(pinnedSkillNames) {
6798
+ const base = "Summarize the conversation above as one self-contained prose recap. Preserve the user's ORIGINAL OBJECTIVE (never paraphrase away negative constraints like 'do NOT do X'), all 'do not' / 'never' / 'avoid' instructions, decisions reached, files inspected or modified, tool results still relevant, and any open todos. Skip turn-by-turn play-by-play. Output plain prose only \u2014 no tool calls, no markdown headings, no SEARCH/REPLACE blocks.";
6799
+ if (pinnedSkillNames.length === 0) return base;
6800
+ const list = pinnedSkillNames.map((n) => `"${n}"`).join(", ");
6801
+ return `${base} The following skill memos are pinned verbatim and appended after your summary \u2014 do NOT quote or paraphrase their bodies: ${list}.`;
6802
+ }
6803
+ function collectPinnedSkills(head) {
6773
6804
  const pinned = /* @__PURE__ */ new Map();
6774
- const stubbedHead = head.map((msg) => {
6775
- if (typeof msg.content !== "string") return msg;
6776
- let hit = false;
6777
- const next = msg.content.replace(SKILL_PIN_REGEX, (full, name) => {
6805
+ for (const msg of head) {
6806
+ if (typeof msg.content !== "string") continue;
6807
+ SKILL_PIN_REGEX.lastIndex = 0;
6808
+ for (const match of msg.content.matchAll(SKILL_PIN_REGEX)) {
6809
+ const name = match[1];
6810
+ const full = match[0];
6778
6811
  pinned.delete(name);
6779
6812
  pinned.set(name, full);
6780
- hit = true;
6781
- return `[skill ${JSON.stringify(name)} memo \u2014 preserved separately, do not summarize.]`;
6782
- });
6783
- return hit ? { ...msg, content: next } : msg;
6784
- });
6785
- return { stubbedHead, pinnedBodies: [...pinned.values()] };
6813
+ }
6814
+ }
6815
+ return { names: [...pinned.keys()], bodies: [...pinned.values()] };
6786
6816
  }
6787
6817
  var ContextManager = class {
6788
6818
  constructor(deps) {
@@ -6831,14 +6861,25 @@ var ContextManager = class {
6831
6861
  }
6832
6862
  return { kind: "none", ...base };
6833
6863
  }
6834
- /** Local-side preflight before sending a request — catches oversized payloads early. */
6864
+ /** Local-side preflight before sending a request — catches oversized payloads early.
6865
+ * Two independent signals trip mechanical truncate: token estimate above the context-window
6866
+ * fraction, OR JSON body bytes above the gateway limit (see `MAX_BODY_BYTES`). */
6835
6867
  decidePreflight(messages, toolSpecs, model) {
6836
6868
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
6837
6869
  const estimate = estimateRequestTokens(messages, toolSpecs ?? null, true);
6870
+ const estimateBytes = Buffer.byteLength(JSON.stringify(messages), "utf8");
6871
+ const tokensOver = estimate / ctxMax > PREFLIGHT_EMERGENCY_THRESHOLD;
6872
+ const bytesOver = estimateBytes > MAX_BODY_BYTES;
6873
+ let trigger = "none";
6874
+ if (tokensOver && bytesOver) trigger = "both";
6875
+ else if (tokensOver) trigger = "tokens";
6876
+ else if (bytesOver) trigger = "bytes";
6838
6877
  return {
6839
- needsAction: estimate / ctxMax > PREFLIGHT_EMERGENCY_THRESHOLD,
6878
+ needsAction: tokensOver || bytesOver,
6840
6879
  estimateTokens: estimate,
6841
- ctxMax
6880
+ estimateBytes,
6881
+ ctxMax,
6882
+ trigger
6842
6883
  };
6843
6884
  }
6844
6885
  /** Replace older turns with one summary message; keep tail within keepRecentTokens budget. */
@@ -6867,16 +6908,22 @@ var ContextManager = class {
6867
6908
  const tail = all.slice(boundary);
6868
6909
  const headTokens = totalTokens - cumTokens;
6869
6910
  if (headTokens < totalTokens * HISTORY_FOLD_MIN_SAVINGS_FRACTION) return noop;
6870
- const { stubbedHead, pinnedBodies } = extractPinnedSkills(head);
6871
- const summary = await this.summarizeForFold(stubbedHead);
6911
+ const { names: pinnedNames, bodies: pinnedBodies } = collectPinnedSkills(head);
6912
+ const summary = await this.summarizeForFold(head, pinnedNames);
6872
6913
  if (!summary.content) return noop;
6873
6914
  const memoTail = pinnedBodies.length > 0 ? `
6874
6915
 
6875
6916
  ${SKILL_PIN_MEMO_HEADER}
6876
6917
 
6877
6918
  ${pinnedBodies.join("\n\n")}` : "";
6919
+ const constraints = extractPinnedConstraints(this.deps.getSystemPrompt());
6920
+ const constraintTail = constraints ? `
6921
+
6922
+ [PINNED CONSTRAINTS \u2014 preserved verbatim]
6923
+
6924
+ ${constraints}` : "";
6878
6925
  const summaryMsg = buildAssistantMessage(
6879
- HISTORY_FOLD_MARKER + summary.content + memoTail,
6926
+ HISTORY_FOLD_MARKER + summary.content + memoTail + constraintTail,
6880
6927
  [],
6881
6928
  model,
6882
6929
  summary.reasoningContent
@@ -6884,6 +6931,7 @@ ${pinnedBodies.join("\n\n")}` : "";
6884
6931
  const replacement = [summaryMsg, ...tail];
6885
6932
  this.deps.log.compactInPlace(replacement);
6886
6933
  this.persistRewrite(replacement);
6934
+ this.deps.onLogRewrite?.();
6887
6935
  return {
6888
6936
  folded: true,
6889
6937
  beforeMessages: all.length,
@@ -6891,10 +6939,13 @@ ${pinnedBodies.join("\n\n")}` : "";
6891
6939
  summaryChars: summary.content.length
6892
6940
  };
6893
6941
  }
6894
- /** Pure local emergency compaction for preflight: drop oldest log entries and keep a valid tail. */
6942
+ /** Pure local emergency compaction for preflight: drop oldest log entries and keep a valid tail.
6943
+ * Bounded by tokens AND bytes — bytes matter because DeepSeek's gateway 400s on bodies past
6944
+ * `MAX_BODY_BYTES` even when the token budget is far from exhausted. */
6895
6945
  mechanicalTruncate(model, opts) {
6896
6946
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
6897
6947
  const targetTokens = opts?.targetTokens ?? Math.floor(ctxMax * PREFLIGHT_MECHANICAL_TARGET_FRACTION);
6948
+ const targetBytes = opts?.targetBytes ?? MAX_BODY_BYTES_TARGET;
6898
6949
  const all = this.deps.log.toMessages();
6899
6950
  const noop = {
6900
6951
  folded: false,
@@ -6904,6 +6955,7 @@ ${pinnedBodies.join("\n\n")}` : "";
6904
6955
  };
6905
6956
  if (all.length === 0) return noop;
6906
6957
  const tokenCounts = all.map((m) => estimateConversationTokens([m], true));
6958
+ const byteCounts = all.map((m) => Buffer.byteLength(JSON.stringify(m), "utf8"));
6907
6959
  let latestUserBoundary = -1;
6908
6960
  for (let i = all.length - 1; i >= 0; i--) {
6909
6961
  if (all[i].role === "user") {
@@ -6912,12 +6964,15 @@ ${pinnedBodies.join("\n\n")}` : "";
6912
6964
  }
6913
6965
  }
6914
6966
  let cumTokens = 0;
6967
+ let cumBytes = 0;
6915
6968
  let boundary = all.length;
6916
6969
  let foundSafeBoundary = false;
6917
6970
  for (let i = all.length - 1; i >= 0; i--) {
6918
- const next = cumTokens + tokenCounts[i];
6919
- if (next > targetTokens) break;
6920
- cumTokens = next;
6971
+ const nextTokens = cumTokens + tokenCounts[i];
6972
+ const nextBytes = cumBytes + byteCounts[i];
6973
+ if (nextTokens > targetTokens || nextBytes > targetBytes) break;
6974
+ cumTokens = nextTokens;
6975
+ cumBytes = nextBytes;
6921
6976
  if (all[i].role === "user") {
6922
6977
  boundary = i;
6923
6978
  foundSafeBoundary = true;
@@ -6928,6 +6983,7 @@ ${pinnedBodies.join("\n\n")}` : "";
6928
6983
  if (replacement.length === all.length) return noop;
6929
6984
  this.deps.log.compactInPlace(replacement);
6930
6985
  this.persistRewrite(replacement);
6986
+ this.deps.onLogRewrite?.();
6931
6987
  return {
6932
6988
  folded: true,
6933
6989
  beforeMessages: all.length,
@@ -6946,17 +7002,18 @@ ${pinnedBodies.join("\n\n")}` : "";
6946
7002
  this.persistRewrite([...kept]);
6947
7003
  return true;
6948
7004
  }
6949
- async summarizeForFold(messagesToSummarize) {
7005
+ async summarizeForFold(messagesToSummarize, pinnedSkillNames) {
6950
7006
  const summaryModel = "deepseek-v4-flash";
6951
- const systemPrompt = "You compress conversation history for a coding agent. Output one prose recap that preserves: the user's overall goal, decisions and conclusions reached, files inspected or modified, important tool results still relevant to ongoing work, and any open todos. Skip turn-by-turn play-by-play. No tool calls, no markdown headings, no SEARCH/REPLACE blocks \u2014 plain prose only.";
6952
7007
  const healed = healLoadedMessages(messagesToSummarize, DEFAULT_MAX_RESULT_CHARS).messages;
7008
+ const agentSystem = this.deps.getSystemPrompt();
7009
+ const fewShots = this.deps.getFewShots?.() ?? [];
7010
+ const tools = this.deps.getToolSpecs?.() ?? [];
7011
+ const instruction = buildFoldSummaryInstruction(pinnedSkillNames);
6953
7012
  const messages = [
6954
- { role: "system", content: systemPrompt },
7013
+ { role: "system", content: agentSystem },
7014
+ ...fewShots.map((m) => ({ ...m })),
6955
7015
  ...healed,
6956
- {
6957
- role: "user",
6958
- content: "Summarize the conversation above as plain prose. This summary replaces the original turns to free context \u2014 make it self-contained."
6959
- }
7016
+ { role: "user", content: instruction }
6960
7017
  ];
6961
7018
  const turnSignal = this.deps.getAbortSignal();
6962
7019
  const foldCtrl = new AbortController();
@@ -6986,9 +7043,9 @@ ${pinnedBodies.join("\n\n")}` : "";
6986
7043
  this.deps.client.chat({
6987
7044
  model: summaryModel,
6988
7045
  messages,
7046
+ tools: tools.length ? tools : void 0,
6989
7047
  signal: foldCtrl.signal,
6990
- thinking: thinkingModeForModel(summaryModel),
6991
- reasoningEffort: "high"
7048
+ thinking: "disabled"
6992
7049
  }),
6993
7050
  abortPromise,
6994
7051
  timeoutPromise
@@ -7073,6 +7130,7 @@ function formatLoopError(err, probe) {
7073
7130
  if (status === "402") return t("errors.balance402", { inner });
7074
7131
  if (status === "422") return t("errors.badparam422", { inner });
7075
7132
  if (status === "400") return t("errors.badrequest400", { inner });
7133
+ if (status === "429") return t("errors.concurrency429", { inner });
7076
7134
  if (is5xxStatus(status)) return formatDeepSeek5xx(status, probe);
7077
7135
  return msg;
7078
7136
  }
@@ -7655,8 +7713,34 @@ function signature(call) {
7655
7713
  return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
7656
7714
  }
7657
7715
 
7716
+ // src/tools/read-tracker.ts
7717
+ import * as pathMod from "path";
7718
+ var ReadTracker = class _ReadTracker {
7719
+ _seen = /* @__PURE__ */ new Set();
7720
+ static norm(abs) {
7721
+ const resolved = pathMod.resolve(abs);
7722
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
7723
+ }
7724
+ markRead(abs) {
7725
+ this._seen.add(_ReadTracker.norm(abs));
7726
+ }
7727
+ hasRead(abs) {
7728
+ return this._seen.has(_ReadTracker.norm(abs));
7729
+ }
7730
+ reset() {
7731
+ this._seen.clear();
7732
+ }
7733
+ get size() {
7734
+ return this._seen.size;
7735
+ }
7736
+ };
7737
+
7658
7738
  // src/loop.ts
7659
7739
  var ESCALATION_MODEL = "deepseek-v4-pro";
7740
+ var MID_TURN_STEER_WRAPPER = "[Mid-turn steer queued by the user. Do not treat this as a new task; use it only as additional guidance for the current task after completing the current step.]";
7741
+ function formatSteerUserMessage(content) {
7742
+ return [MID_TURN_STEER_WRAPPER, content].join("\n");
7743
+ }
7660
7744
  var CacheFirstLoop = class {
7661
7745
  client;
7662
7746
  prefix;
@@ -7665,6 +7749,8 @@ var CacheFirstLoop = class {
7665
7749
  scratch = new VolatileScratch();
7666
7750
  stats = new SessionStats();
7667
7751
  repair;
7752
+ /** Files the model has read this session; gates edit_file / multi_edit so SEARCH text matches on-disk bytes. Cleared on fold / mechanical truncate (the model's byte-level view of the elided history is gone). In-memory only — naturally empty on resume. */
7753
+ readTracker = new ReadTracker();
7668
7754
  // Mutable via configure() — slash commands in the TUI / library callers tweak
7669
7755
  // these mid-session so users don't have to restart.
7670
7756
  model;
@@ -7688,15 +7774,19 @@ var CacheFirstLoop = class {
7688
7774
  _turnAbort = new AbortController();
7689
7775
  /** Authoritative running-id set — UI cards consult this instead of trusting end-event delivery. Insert at dispatch entry, delete in finally. */
7690
7776
  _inflight = new InflightSet();
7691
- /** Typeahead steer message set by the UI; step() consumes it at the next iter boundary. */
7692
- _steer = null;
7777
+ /** Typeahead steer messages set by the UI; step() consumes one at each iter boundary. */
7778
+ _steerQueue = [];
7693
7779
  /** Set true when a steer was consumed this turn; cleared on next step() entry. */
7694
7780
  _steerConsumed = false;
7695
7781
  /** UI calls this to inject a mid-turn steer message without aborting the current turn.
7696
- * New text resets steerConsumed a fresh steer hasn't been consumed yet. */
7782
+ * New text resets steerConsumed because a fresh steer is queued. */
7697
7783
  steer(text) {
7698
- this._steer = text;
7699
- if (text !== null) this._steerConsumed = false;
7784
+ if (text === null) {
7785
+ this._steerQueue.length = 0;
7786
+ return;
7787
+ }
7788
+ this._steerQueue.push(text);
7789
+ this._steerConsumed = false;
7700
7790
  }
7701
7791
  /** True when a steer was consumed this turn (UI gate to avoid double-submit). */
7702
7792
  get steerConsumed() {
@@ -7782,7 +7872,11 @@ var CacheFirstLoop = class {
7782
7872
  stats: this.stats,
7783
7873
  sessionName: this.sessionName,
7784
7874
  getAbortSignal: () => this._turnAbort.signal,
7785
- getCurrentTurn: () => this._turn
7875
+ getCurrentTurn: () => this._turn,
7876
+ getSystemPrompt: () => this.prefix.system,
7877
+ getToolSpecs: () => this.prefix.toolSpecs,
7878
+ getFewShots: () => this.prefix.fewShots,
7879
+ onLogRewrite: () => this.readTracker.reset()
7786
7880
  });
7787
7881
  }
7788
7882
  /** Replace older turns with one summary message; keep tail within keepRecentTokens budget. */
@@ -7953,7 +8047,8 @@ ${reason}`
7953
8047
  const result = await this.tools.dispatch(name, args, {
7954
8048
  signal,
7955
8049
  maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
7956
- confirmationGate: this.confirmationGate
8050
+ confirmationGate: this.confirmationGate,
8051
+ readTracker: this.readTracker
7957
8052
  });
7958
8053
  const postReport = await runHooks({
7959
8054
  hooks: this.hooks,
@@ -7981,11 +8076,9 @@ ${reason}`
7981
8076
  return generated;
7982
8077
  }
7983
8078
  _inflightCounter = 0;
7984
- buildMessages(pendingUser) {
8079
+ buildMessages() {
7985
8080
  const healedMessages = this.healActiveLogBeforeSend();
7986
- const msgs = [...this.prefix.toMessages(), ...healedMessages];
7987
- if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
7988
- return msgs;
8081
+ return [...this.prefix.toMessages(), ...healedMessages];
7989
8082
  }
7990
8083
  healActiveLogBeforeSend() {
7991
8084
  const current = this.log.toMessages();
@@ -8066,6 +8159,7 @@ ${reason}`
8066
8159
  cap: this.budgetUsd.toFixed(2)
8067
8160
  })
8068
8161
  };
8162
+ this._steerQueue.length = 0;
8069
8163
  return;
8070
8164
  }
8071
8165
  if (!this._budgetWarned && spent >= this.budgetUsd * 0.8) {
@@ -8104,8 +8198,8 @@ ${reason}`
8104
8198
  };
8105
8199
  }
8106
8200
  this.appendAndPersist({ role: "user", content: userInput });
8107
- let pendingUser = null;
8108
8201
  const toolSpecs = this.prefix.tools();
8202
+ let rateLimitWarningShown = false;
8109
8203
  for (let iter = 0; ; iter++) {
8110
8204
  if (signal.aborted) {
8111
8205
  try {
@@ -8126,6 +8220,7 @@ ${reason}`
8126
8220
  } finally {
8127
8221
  this._turnAbort = new AbortController();
8128
8222
  }
8223
+ this._steerQueue.length = 0;
8129
8224
  return;
8130
8225
  }
8131
8226
  if (iter > 0) {
@@ -8135,14 +8230,15 @@ ${reason}`
8135
8230
  content: t("loop.toolUploadStatus")
8136
8231
  };
8137
8232
  }
8138
- let messages = this.buildMessages(pendingUser);
8139
- if (this._steer !== null) {
8140
- const steer = this._steer;
8141
- this._steer = null;
8142
- this._steerConsumed = true;
8143
- this.appendAndPersist({ role: "user", content: steer });
8144
- messages = this.buildMessages(pendingUser);
8145
- pendingUser = null;
8233
+ let messages = this.buildMessages();
8234
+ if (this._steerQueue.length > 0) {
8235
+ const steer = this._steerQueue.shift();
8236
+ this._steerConsumed = this._steerQueue.length === 0;
8237
+ this.appendAndPersist({
8238
+ role: "user",
8239
+ content: formatSteerUserMessage(steer)
8240
+ });
8241
+ messages = this.buildMessages();
8146
8242
  yield {
8147
8243
  turn: this._turn,
8148
8244
  role: "steer",
@@ -8152,17 +8248,17 @@ ${reason}`
8152
8248
  {
8153
8249
  const decision2 = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
8154
8250
  if (decision2.needsAction) {
8155
- const { estimateTokens: estimate, ctxMax } = decision2;
8251
+ const { estimateTokens: estimate, estimateBytes, ctxMax } = decision2;
8156
8252
  yield {
8157
8253
  turn: this._turn,
8158
8254
  role: "status",
8159
8255
  content: t("loop.preflightTruncateStatus")
8160
8256
  };
8161
8257
  const result = this.context.mechanicalTruncate(this.model, {
8162
- allowEmpty: pendingUser !== null
8258
+ allowEmpty: false
8163
8259
  });
8164
8260
  if (result.folded) {
8165
- messages = this.buildMessages(pendingUser);
8261
+ messages = this.buildMessages();
8166
8262
  const after = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
8167
8263
  const stillFull = after.needsAction;
8168
8264
  yield {
@@ -8174,6 +8270,7 @@ ${reason}`
8174
8270
  estimate: after.estimateTokens.toLocaleString(),
8175
8271
  ctxMax: after.ctxMax.toLocaleString(),
8176
8272
  pct: Math.round(after.estimateTokens / after.ctxMax * 100),
8273
+ bodyKB: Math.round(after.estimateBytes / 1024).toLocaleString(),
8177
8274
  beforeMessages: result.beforeMessages,
8178
8275
  afterMessages: result.afterMessages
8179
8276
  }
@@ -8186,7 +8283,8 @@ ${reason}`
8186
8283
  content: t("loop.preflightNoFold", {
8187
8284
  estimate: estimate.toLocaleString(),
8188
8285
  ctxMax: ctxMax.toLocaleString(),
8189
- pct: Math.round(estimate / ctxMax * 100)
8286
+ pct: Math.round(estimate / ctxMax * 100),
8287
+ bodyKB: Math.round(estimateBytes / 1024).toLocaleString()
8190
8288
  })
8191
8289
  };
8192
8290
  }
@@ -8306,6 +8404,7 @@ ${reason}`
8306
8404
  } finally {
8307
8405
  this._turnAbort = new AbortController();
8308
8406
  }
8407
+ this._steerQueue.length = 0;
8309
8408
  return;
8310
8409
  }
8311
8410
  const probe = is5xxError(err) ? await probeDeepSeekReachable(this.client) : void 0;
@@ -8315,6 +8414,7 @@ ${reason}`
8315
8414
  content: "",
8316
8415
  error: formatLoopError(err, probe)
8317
8416
  };
8417
+ this._steerQueue.length = 0;
8318
8418
  return;
8319
8419
  }
8320
8420
  if (this.autoEscalate && this.modelForCurrentCall() !== ESCALATION_MODEL && isEscalationRequest(assistantContent)) {
@@ -8395,11 +8495,16 @@ ${reason}`
8395
8495
  };
8396
8496
  }
8397
8497
  if (repairedCalls.length === 0) {
8498
+ if (this._steerQueue.length > 0) {
8499
+ continue;
8500
+ }
8398
8501
  if (allSuppressed) {
8399
8502
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "stuck" });
8503
+ this._steerQueue.length = 0;
8400
8504
  return;
8401
8505
  }
8402
8506
  yield { turn: this._turn, role: "done", content: assistantContent };
8507
+ this._steerQueue.length = 0;
8403
8508
  return;
8404
8509
  }
8405
8510
  const decision = this.context.decideAfterUsage(usage, this.model, this._foldedThisTurn);
@@ -8445,6 +8550,7 @@ ${reason}`
8445
8550
  };
8446
8551
  this.context.trimTrailingToolCalls();
8447
8552
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "context-guard" });
8553
+ this._steerQueue.length = 0;
8448
8554
  return;
8449
8555
  }
8450
8556
  const dispatchSerial = (process.env.REASONIX_TOOL_DISPATCH ?? "auto").toLowerCase() === "serial";
@@ -8492,6 +8598,15 @@ ${reason}`
8492
8598
  }
8493
8599
  for (const w of preWarnings) yield w;
8494
8600
  for (const w of postWarnings) yield w;
8601
+ const rateLimited = parseRateLimitedToolResult(result);
8602
+ if (rateLimited && !rateLimitWarningShown) {
8603
+ rateLimitWarningShown = true;
8604
+ yield {
8605
+ turn: this._turn,
8606
+ role: "warning",
8607
+ content: rateLimited.message
8608
+ };
8609
+ }
8495
8610
  this.appendAndPersist({
8496
8611
  role: "tool",
8497
8612
  tool_call_id: call.id ?? "",
@@ -8514,7 +8629,7 @@ ${reason}`
8514
8629
  return {
8515
8630
  client: this.client,
8516
8631
  signal: this._turnAbort.signal,
8517
- buildMessages: () => this.buildMessages(null),
8632
+ buildMessages: () => this.buildMessages(),
8518
8633
  appendAndPersist: (m) => this.appendAndPersist(m),
8519
8634
  recordStats: (model, usage) => this.stats.record(this._turn, model, usage),
8520
8635
  turn: this._turn
@@ -8539,7 +8654,7 @@ function parsePositiveIntEnv(raw) {
8539
8654
  // src/at-mentions.ts
8540
8655
  import { existsSync, readFileSync, readdirSync, statSync } from "fs";
8541
8656
  import { readdir, stat } from "fs/promises";
8542
- import { isAbsolute, join, relative, resolve } from "path";
8657
+ import { isAbsolute, join, relative, resolve as resolve2 } from "path";
8543
8658
 
8544
8659
  // src/at-mentions-url.ts
8545
8660
  var AT_URL_PATTERN = /(?<=^|\s)@(https?:\/\/\S+)/g;
@@ -8670,7 +8785,7 @@ function listFilesSync(root, opts = {}) {
8670
8785
  function listFilesWithStatsSync(root, opts = {}) {
8671
8786
  const maxResults = Math.max(1, opts.maxResults ?? 2e3);
8672
8787
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
8673
- const rootAbs = resolve(root);
8788
+ const rootAbs = resolve2(root);
8674
8789
  const respectGi = opts.respectGitignore !== false;
8675
8790
  const out = [];
8676
8791
  const walk2 = (dirAbs, dirRel, layers) => {
@@ -8734,7 +8849,7 @@ async function listFilesWithStatsAsync(root, opts = {}) {
8734
8849
  async function walkFilesStream(root, opts) {
8735
8850
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
8736
8851
  const respectGi = opts.respectGitignore !== false;
8737
- const rootAbs = resolve(root);
8852
+ const rootAbs = resolve2(root);
8738
8853
  const progressGap = Math.max(0, opts.progressIntervalMs ?? 100);
8739
8854
  let scanned = 0;
8740
8855
  let halted = false;
@@ -8812,8 +8927,8 @@ async function flushFiles(ents, dirAbs, dirRel, layers, emit) {
8812
8927
  async function listDirectory(root, relDir, opts = {}) {
8813
8928
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
8814
8929
  const respectGi = opts.respectGitignore !== false;
8815
- const rootAbs = resolve(root);
8816
- const dirAbs = resolve(rootAbs, relDir);
8930
+ const rootAbs = resolve2(root);
8931
+ const dirAbs = resolve2(rootAbs, relDir);
8817
8932
  const rel = relative(rootAbs, dirAbs);
8818
8933
  if (rel.startsWith("..") || isAbsolute(rel)) return [];
8819
8934
  const layers = [];
@@ -8978,7 +9093,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
8978
9093
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
8979
9094
  const maxDirEntries = Math.max(1, opts.maxDirEntries ?? DEFAULT_AT_DIR_MAX_ENTRIES);
8980
9095
  const fs4 = opts.fs ?? defaultFs;
8981
- const root = resolve(rootDir);
9096
+ const root = resolve2(rootDir);
8982
9097
  const seen = /* @__PURE__ */ new Map();
8983
9098
  const expansions = [];
8984
9099
  const dirListings = /* @__PURE__ */ new Map();
@@ -9025,7 +9140,7 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs4, dirListings
9025
9140
  if (isAbsolute(rawPath)) {
9026
9141
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
9027
9142
  }
9028
- const resolved = resolve(root, rawPath);
9143
+ const resolved = resolve2(root, rawPath);
9029
9144
  const rel = relative(root, resolved);
9030
9145
  if (rel.startsWith("..") || isAbsolute(rel)) {
9031
9146
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
@@ -9055,7 +9170,7 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs4, dirListings
9055
9170
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
9056
9171
  }
9057
9172
  function readSafe(root, rawPath, fs4) {
9058
- const resolved = resolve(root, rawPath);
9173
+ const resolved = resolve2(root, rawPath);
9059
9174
  try {
9060
9175
  return fs4.read(resolved);
9061
9176
  } catch {
@@ -9331,16 +9446,19 @@ function registerChoiceTool(registry, opts = {}) {
9331
9446
 
9332
9447
  // src/tools/web.ts
9333
9448
  var import_node_html_parser = __toESM(require_dist(), 1);
9449
+ import { lookup } from "dns/promises";
9450
+ import { isIP } from "net";
9334
9451
  var DEFAULT_FETCH_MAX_CHARS = 32e3;
9335
9452
  var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
9336
9453
  var DEFAULT_TOPK = 5;
9337
9454
  var FETCH_MAX_BYTES = 10 * 1024 * 1024;
9338
9455
  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";
9339
- var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
9456
+ var BING_ENDPOINT = "https://cn.bing.com/search";
9340
9457
  var METASO_ENDPOINT = "https://metaso.cn/api/v1";
9341
9458
  var TAVILY_ENDPOINT = "https://api.tavily.com/search";
9342
9459
  var PERPLEXITY_ENDPOINT = "https://api.perplexity.ai/chat/completions";
9343
9460
  var EXA_ENDPOINT = "https://api.exa.ai/answer";
9461
+ var FETCH_MAX_REDIRECTS = 5;
9344
9462
  function searchStatusError(status) {
9345
9463
  if (status === 429) return t("webErrors.rateLimit429");
9346
9464
  if (status === 403) return t("webErrors.forbidden403");
@@ -9353,6 +9471,63 @@ function fetchStatusError(status, url) {
9353
9471
  if (status >= 500 && status <= 599) return t("webErrors.fetchServerError5xx", { status, url });
9354
9472
  return t("webErrors.fetchStatus", { status, url });
9355
9473
  }
9474
+ function parseIpv4(address) {
9475
+ const parts = address.split(".");
9476
+ if (parts.length !== 4) return null;
9477
+ let out = 0;
9478
+ for (const part of parts) {
9479
+ if (!/^\d+$/.test(part)) return null;
9480
+ const n = Number(part);
9481
+ if (!Number.isInteger(n) || n < 0 || n > 255) return null;
9482
+ out = (out << 8) + n;
9483
+ }
9484
+ return out >>> 0;
9485
+ }
9486
+ function ipv4InRange(value, base, bits) {
9487
+ const parsed = parseIpv4(base);
9488
+ if (parsed === null) return false;
9489
+ const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
9490
+ return (value & mask) === (parsed & mask);
9491
+ }
9492
+ function isPrivateIpv4(address) {
9493
+ const value = parseIpv4(address);
9494
+ if (value === null) return false;
9495
+ return ipv4InRange(value, "0.0.0.0", 8) || ipv4InRange(value, "10.0.0.0", 8) || ipv4InRange(value, "100.64.0.0", 10) || ipv4InRange(value, "127.0.0.0", 8) || ipv4InRange(value, "169.254.0.0", 16) || ipv4InRange(value, "172.16.0.0", 12) || ipv4InRange(value, "192.0.0.0", 24) || ipv4InRange(value, "192.0.2.0", 24) || ipv4InRange(value, "192.168.0.0", 16) || ipv4InRange(value, "198.18.0.0", 15) || ipv4InRange(value, "198.51.100.0", 24) || ipv4InRange(value, "203.0.113.0", 24) || ipv4InRange(value, "224.0.0.0", 4) || ipv4InRange(value, "240.0.0.0", 4);
9496
+ }
9497
+ function normalizeIpv6(address) {
9498
+ return address.toLowerCase().replace(/(^|:)0+([0-9a-f])/g, "$1$2");
9499
+ }
9500
+ function isPrivateIpv6(address) {
9501
+ const normalized = normalizeIpv6(address);
9502
+ const mapped = /^::ffff:(?:0+:)?(\d+\.\d+\.\d+\.\d+)$/i.exec(normalized);
9503
+ if (mapped) return isPrivateIpv4(mapped[1]);
9504
+ return normalized === "::" || normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb") || normalized.startsWith("ff");
9505
+ }
9506
+ function isInternalAddress(address) {
9507
+ const family = isIP(address);
9508
+ if (family === 4) return isPrivateIpv4(address);
9509
+ if (family === 6) return isPrivateIpv6(address);
9510
+ return false;
9511
+ }
9512
+ async function assertPublicHttpUrl(rawUrl) {
9513
+ const url = new URL(rawUrl);
9514
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
9515
+ throw new Error(`web_fetch refuses non-HTTP URL: ${url.protocol}`);
9516
+ }
9517
+ const host = url.hostname;
9518
+ const literal = isIP(host);
9519
+ const addresses = literal ? [host] : (await lookup(host, { all: true, verbatim: true })).map((entry) => entry.address);
9520
+ if (addresses.length === 0 || addresses.some(isInternalAddress)) {
9521
+ throw new Error(`web_fetch refuses internal or reserved host: ${host}`);
9522
+ }
9523
+ return url;
9524
+ }
9525
+ function redirectLocation(resp, currentUrl) {
9526
+ if (resp.status < 300 || resp.status > 399) return null;
9527
+ const location = resp.headers.get("location");
9528
+ if (!location) return null;
9529
+ return new URL(location, currentUrl).toString();
9530
+ }
9356
9531
  async function webSearch(query, opts = {}) {
9357
9532
  if (opts.engine === "metaso") {
9358
9533
  return searchMetaso(query, opts);
@@ -9369,29 +9544,29 @@ async function webSearch(query, opts = {}) {
9369
9544
  if (opts.engine === "exa") {
9370
9545
  return searchExa(query, opts);
9371
9546
  }
9372
- return searchMojeek(query, opts);
9547
+ return searchBing(query, opts);
9373
9548
  }
9374
- async function searchMojeek(query, opts = {}) {
9549
+ async function searchBing(query, opts = {}) {
9375
9550
  const topK = Math.max(1, Math.min(10, opts.topK ?? DEFAULT_TOPK));
9376
- const resp = await fetch(`${MOJEEK_ENDPOINT}?q=${encodeURIComponent(query)}`, {
9551
+ const resp = await fetch(`${BING_ENDPOINT}?q=${encodeURIComponent(query)}`, {
9377
9552
  headers: {
9378
9553
  "User-Agent": USER_AGENT,
9379
9554
  Accept: "text/html,application/xhtml+xml,application/xml;q=0.9",
9380
- "Accept-Language": "en-US,en;q=0.9"
9555
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
9381
9556
  },
9382
9557
  signal: opts.signal,
9383
9558
  redirect: "follow"
9384
9559
  });
9385
9560
  if (!resp.ok) throw new Error(searchStatusError(resp.status));
9386
9561
  const html = await resp.text();
9387
- const results = parseMojeekResults(html).slice(0, topK);
9562
+ const results = parseBingResults(html).slice(0, topK);
9388
9563
  if (results.length === 0) {
9389
9564
  if (/no results found|did not match any documents/i.test(html)) return [];
9390
9565
  if (/captcha|verify you are human|access denied|forbidden/i.test(html)) {
9391
- throw new Error(t("webErrors.mojeekBlocked"));
9566
+ throw new Error(t("webErrors.bingBlocked"));
9392
9567
  }
9393
9568
  throw new Error(
9394
- t("webErrors.mojeekNoResults", {
9569
+ t("webErrors.bingNoResults", {
9395
9570
  chars: html.length,
9396
9571
  preview: html.slice(0, 120).replace(/\s+/g, " ")
9397
9572
  })
@@ -9444,6 +9619,7 @@ async function searchSearxng(query, opts = {}) {
9444
9619
  async function searchMetaso(query, opts = {}) {
9445
9620
  const topK = Math.max(1, Math.min(100, opts.topK ?? DEFAULT_TOPK));
9446
9621
  const apiKey = loadMetasoApiKey();
9622
+ if (!apiKey) throw new Error(t("webErrors.metasoMissingKey"));
9447
9623
  let resp;
9448
9624
  try {
9449
9625
  resp = await fetch(`${METASO_ENDPOINT}/search`, {
@@ -9707,35 +9883,19 @@ function parseSearxngHtmlResults(html) {
9707
9883
  }
9708
9884
  return results;
9709
9885
  }
9710
- function parseMojeekResults(html) {
9711
- const titles = [];
9712
- const titleAnchorRe = /<a\b[^>]*\bclass="title"[^>]*>[\s\S]*?<\/a>/g;
9713
- let m;
9714
- while (true) {
9715
- m = titleAnchorRe.exec(html);
9716
- if (m === null) break;
9717
- titles.push(m[0]);
9718
- }
9719
- const snippets = [];
9720
- const snippetRe = /<p\b[^>]*\bclass="s"[^>]*>([\s\S]*?)<\/p>/g;
9721
- while (true) {
9722
- m = snippetRe.exec(html);
9723
- if (m === null) break;
9724
- snippets.push(m[1] ?? "");
9725
- }
9726
- const hrefRe = /href="([^"]+)"/;
9727
- const innerRe = /<a\b[^>]*>([\s\S]*?)<\/a>/;
9886
+ function parseBingResults(html) {
9887
+ const root = (0, import_node_html_parser.parse)(html);
9728
9888
  const results = [];
9729
- for (let i = 0; i < titles.length; i++) {
9730
- const anchor = titles[i];
9731
- const hrefMatch = anchor.match(hrefRe);
9732
- const innerMatch = anchor.match(innerRe);
9733
- if (!hrefMatch?.[1]) continue;
9734
- results.push({
9735
- title: decodeHtmlEntities(stripHtml(innerMatch?.[1] ?? "")).trim(),
9736
- url: hrefMatch[1],
9737
- snippet: decodeHtmlEntities(stripHtml(snippets[i] ?? "")).replace(/\s+/g, " ").trim()
9738
- });
9889
+ for (const li of root.querySelectorAll("li.b_algo")) {
9890
+ const anchor = li.querySelector("h2 a[href]");
9891
+ if (!anchor) continue;
9892
+ const href = anchor.getAttribute("href");
9893
+ if (!href) continue;
9894
+ const title = anchor.textContent.trim();
9895
+ if (!title) continue;
9896
+ const cap = li.querySelector("div.b_caption p");
9897
+ const snippet = cap ? cap.textContent.trim().replace(/\s+/g, " ") : "";
9898
+ results.push({ title, url: href, snippet });
9739
9899
  }
9740
9900
  return results;
9741
9901
  }
@@ -9751,12 +9911,23 @@ async function webFetch(url, opts = {}) {
9751
9911
  const cancel = () => ctl.abort();
9752
9912
  opts.signal?.addEventListener("abort", cancel, { once: true });
9753
9913
  let resp;
9914
+ let currentUrl = url;
9754
9915
  try {
9755
- resp = await fetch(url, {
9756
- headers: { "User-Agent": USER_AGENT, Accept: "text/html,text/plain,*/*" },
9757
- signal: ctl.signal,
9758
- redirect: "follow"
9759
- });
9916
+ for (let redirects = 0; ; redirects++) {
9917
+ const parsed = await assertPublicHttpUrl(currentUrl);
9918
+ if (ctl.signal.aborted) throw new DOMException("aborted", "AbortError");
9919
+ resp = await fetch(parsed, {
9920
+ headers: { "User-Agent": USER_AGENT, Accept: "text/html,text/plain,*/*" },
9921
+ signal: ctl.signal,
9922
+ redirect: "manual"
9923
+ });
9924
+ const nextUrl = redirectLocation(resp, parsed.toString());
9925
+ if (!nextUrl) break;
9926
+ if (redirects >= FETCH_MAX_REDIRECTS) {
9927
+ throw new Error(`web_fetch redirect limit exceeded for ${url}`);
9928
+ }
9929
+ currentUrl = nextUrl;
9930
+ }
9760
9931
  } catch (err) {
9761
9932
  if (timedOut) {
9762
9933
  throw new Error(t("webErrors.fetchTimeout", { ms: timeoutMs, url }));
@@ -9779,7 +9950,7 @@ async function webFetch(url, opts = {}) {
9779
9950
  const finalText = truncated ? `${text.slice(0, maxChars)}
9780
9951
 
9781
9952
  [\u2026 truncated ${text.length - maxChars} chars \u2026]` : text;
9782
- return { url, title, text: finalText, truncated };
9953
+ return { url: currentUrl, title, text: finalText, truncated };
9783
9954
  }
9784
9955
  async function readBodyCapped(resp, maxBytes) {
9785
9956
  if (!resp.body) return await resp.text();
@@ -9851,9 +10022,6 @@ function walkExtract(node, out) {
9851
10022
  for (const child of node.childNodes) walkExtract(child, out);
9852
10023
  if (isBreak) out.push("\n");
9853
10024
  }
9854
- function stripHtml(s) {
9855
- return (0, import_node_html_parser.parse)(s).text;
9856
- }
9857
10025
  var HTML_ENTITIES = {
9858
10026
  amp: "&",
9859
10027
  lt: "<",
@@ -9968,14 +10136,14 @@ ${i + 1}. ${r.title}`);
9968
10136
  // src/tools/filesystem.ts
9969
10137
  var import_picomatch2 = __toESM(require_picomatch(), 1);
9970
10138
  import { promises as fs3 } from "fs";
9971
- import * as pathMod4 from "path";
10139
+ import * as pathMod5 from "path";
9972
10140
 
9973
10141
  // src/memory/subdir.ts
9974
10142
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
9975
- import { dirname, join as join2, relative as relative2, resolve as resolve2 } from "path";
10143
+ import { dirname, join as join2, relative as relative2, resolve as resolve3 } from "path";
9976
10144
  function findDirMemory(absDir, rootDir) {
9977
- const root = resolve2(rootDir);
9978
- const target = resolve2(absDir);
10145
+ const root = resolve3(rootDir);
10146
+ const target = resolve3(absDir);
9979
10147
  const rel = relative2(root, target);
9980
10148
  if (rel.startsWith("..")) return [];
9981
10149
  const found = [];
@@ -9997,7 +10165,7 @@ function findDirMemory(absDir, rootDir) {
9997
10165
  return found;
9998
10166
  }
9999
10167
  function findSubdirMemoryAncestors(absPath, rootDir) {
10000
- return findDirMemory(dirname(resolve2(absPath)), rootDir);
10168
+ return findDirMemory(dirname(resolve3(absPath)), rootDir);
10001
10169
  }
10002
10170
  function readSubdirMemoryContent(path) {
10003
10171
  let raw;
@@ -10021,9 +10189,9 @@ ${content}`;
10021
10189
  // src/tools/fs/glob.ts
10022
10190
  var import_picomatch = __toESM(require_picomatch(), 1);
10023
10191
  import { promises as fs } from "fs";
10024
- import * as pathMod from "path";
10192
+ import * as pathMod2 from "path";
10025
10193
  function displayRel(rootDir, full) {
10026
- return pathMod.relative(rootDir, full).replaceAll("\\", "/");
10194
+ return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
10027
10195
  }
10028
10196
  async function globFiles(ctx, startAbs, args) {
10029
10197
  if (args.signal?.aborted) {
@@ -10045,7 +10213,7 @@ async function globFiles(ctx, startAbs, args) {
10045
10213
  return;
10046
10214
  }
10047
10215
  for (const e of entries) {
10048
- const full = pathMod.join(dir, e.name);
10216
+ const full = pathMod2.join(dir, e.name);
10049
10217
  if (e.isDirectory()) {
10050
10218
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
10051
10219
  await walk2(full);
@@ -10082,7 +10250,7 @@ async function globFiles(ctx, startAbs, args) {
10082
10250
  }
10083
10251
 
10084
10252
  // src/tools/fs/outline.ts
10085
- import * as pathMod2 from "path";
10253
+ import * as pathMod3 from "path";
10086
10254
  var OUTLINE_MAX_ENTRIES = 30;
10087
10255
  var OUTLINE_TAIL_KEEP = 5;
10088
10256
  var TS_EXPORT_RE = /^export\s+(?:default\s+)?(?:async\s+)?(function|class|const|let|var|interface|type|enum)\s+\*?\s*(\w+)/;
@@ -10125,7 +10293,7 @@ var EXT_TO_LANG = {
10125
10293
  ".text": "txt"
10126
10294
  };
10127
10295
  function extractOutline(filename, lines) {
10128
- const ext = pathMod2.extname(filename).toLowerCase();
10296
+ const ext = pathMod3.extname(filename).toLowerCase();
10129
10297
  const lang = EXT_TO_LANG[ext];
10130
10298
  if (!lang) return [];
10131
10299
  switch (lang) {
@@ -10266,7 +10434,7 @@ function formatOutline(entries) {
10266
10434
 
10267
10435
  // src/tools/fs/search.ts
10268
10436
  import { promises as fs2 } from "fs";
10269
- import * as pathMod3 from "path";
10437
+ import * as pathMod4 from "path";
10270
10438
 
10271
10439
  // src/tools/fs/regex-runner.ts
10272
10440
  import { Worker } from "worker_threads";
@@ -10299,7 +10467,7 @@ var RegexRunner = class {
10299
10467
  this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
10300
10468
  }
10301
10469
  testLines(text, source, flags, opts = {}) {
10302
- return new Promise((resolve5, reject) => {
10470
+ return new Promise((resolve6, reject) => {
10303
10471
  if (opts.signal?.aborted) {
10304
10472
  reject(new Error("regex evaluation aborted"));
10305
10473
  return;
@@ -10312,7 +10480,7 @@ var RegexRunner = class {
10312
10480
  this.killWorker();
10313
10481
  reject(new Error(`regex evaluation exceeded ${timeoutMs}ms`));
10314
10482
  }, timeoutMs);
10315
- const entry = { resolve: resolve5, reject, timer };
10483
+ const entry = { resolve: resolve6, reject, timer };
10316
10484
  if (opts.signal) {
10317
10485
  entry.signal = opts.signal;
10318
10486
  entry.onAbort = () => {
@@ -10395,7 +10563,7 @@ function throwIfAborted(signal) {
10395
10563
  throw new DOMException("search aborted by user", "AbortError");
10396
10564
  }
10397
10565
  function displayRel2(rootDir, full) {
10398
- return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
10566
+ return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
10399
10567
  }
10400
10568
  async function searchFiles(ctx, startAbs, args) {
10401
10569
  throwIfAborted(args.signal);
@@ -10419,7 +10587,7 @@ async function searchFiles(ctx, startAbs, args) {
10419
10587
  }
10420
10588
  for (const e of entries) {
10421
10589
  throwIfAborted(args.signal);
10422
- const full = pathMod3.join(dir, e.name);
10590
+ const full = pathMod4.join(dir, e.name);
10423
10591
  const lower = e.name.toLowerCase();
10424
10592
  const hit = re ? re.test(e.name) : lower.includes(needle);
10425
10593
  if (hit) {
@@ -10511,11 +10679,11 @@ async function searchContent(ctx, startAbs, args) {
10511
10679
  throwIfTimedOut();
10512
10680
  if (e.isDirectory()) {
10513
10681
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
10514
- await walk2(pathMod3.join(dir, e.name));
10682
+ await walk2(pathMod4.join(dir, e.name));
10515
10683
  continue;
10516
10684
  }
10517
10685
  if (!e.isFile()) continue;
10518
- const full = pathMod3.join(dir, e.name);
10686
+ const full = pathMod4.join(dir, e.name);
10519
10687
  if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel2(ctx.rootDir, full))) continue;
10520
10688
  if (ctx.isBinaryByName(e.name)) continue;
10521
10689
  let fh;
@@ -10596,8 +10764,8 @@ async function searchContent(ctx, startAbs, args) {
10596
10764
  for (let i = realStart; i <= winEnd; i++) {
10597
10765
  const line = lines[i];
10598
10766
  const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
10599
- const sep2 = hitSet.has(i) ? ":" : "-";
10600
- if (!pushLine(`${rel}:${i + 1}${sep2} ${display}`)) return;
10767
+ const sep = hitSet.has(i) ? ":" : "-";
10768
+ if (!pushLine(`${rel}:${i + 1}${sep} ${display}`)) return;
10601
10769
  }
10602
10770
  prevWindowEnd = winEnd;
10603
10771
  }
@@ -10633,7 +10801,7 @@ var SKIP_DIR_NAMES = new Set(
10633
10801
  );
10634
10802
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
10635
10803
  function displayRel3(rootDir, full) {
10636
- return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
10804
+ return pathMod5.relative(rootDir, full).replaceAll("\\", "/");
10637
10805
  }
10638
10806
  function looksLikeAbsoluteSystemPath(raw) {
10639
10807
  if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
@@ -10642,8 +10810,8 @@ function looksLikeAbsoluteSystemPath(raw) {
10642
10810
  );
10643
10811
  }
10644
10812
  function pathIsUnder(child, parent) {
10645
- const rel = pathMod4.relative(parent, child);
10646
- return rel === "" || !rel.startsWith("..") && !pathMod4.isAbsolute(rel);
10813
+ const rel = pathMod5.relative(parent, child);
10814
+ return rel === "" || !rel.startsWith("..") && !pathMod5.isAbsolute(rel);
10647
10815
  }
10648
10816
  var GLOB_METACHARS = /[*?{[]/;
10649
10817
  function compileNameFilter(filter) {
@@ -10675,11 +10843,11 @@ function formatBytes(n) {
10675
10843
  return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
10676
10844
  }
10677
10845
  function registerFilesystemTools(registry, opts) {
10678
- const rootDir = pathMod4.resolve(opts.rootDir);
10846
+ const rootDir = pathMod5.resolve(opts.rootDir);
10679
10847
  const allowWriting = opts.allowWriting !== false;
10680
10848
  const outlineThresholdBytes = opts.outlineThresholdBytes ?? DEFAULT_OUTLINE_THRESHOLD_BYTES;
10681
10849
  const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
10682
- const normRoot = pathMod4.resolve(rootDir);
10850
+ const normRoot = pathMod5.resolve(rootDir);
10683
10851
  const sessionApproved = /* @__PURE__ */ new Set();
10684
10852
  const shownSubdirMemory = /* @__PURE__ */ new Set();
10685
10853
  function withSubdirMemory(absPath, body) {
@@ -10712,7 +10880,7 @@ ${body}`;
10712
10880
  if (pathIsUnder(abs, dir)) return;
10713
10881
  }
10714
10882
  const stat2 = await safeLstat(abs);
10715
- const allowPrefix = stat2?.isDirectory() ? abs : pathMod4.dirname(abs);
10883
+ const allowPrefix = stat2?.isDirectory() ? abs : pathMod5.dirname(abs);
10716
10884
  let pending = inflightGate.get(allowPrefix);
10717
10885
  if (!pending) {
10718
10886
  const gate = ctx?.confirmationGate ?? pauseGate;
@@ -10740,7 +10908,7 @@ ${body}`;
10740
10908
  throw new Error("path must be a non-empty string");
10741
10909
  }
10742
10910
  if (looksLikeAbsoluteSystemPath(raw)) {
10743
- const abs = pathMod4.resolve(raw);
10911
+ const abs = pathMod5.resolve(raw);
10744
10912
  if (pathIsUnder(abs, normRoot)) return abs;
10745
10913
  await ensureOutsideSandboxAllowed(abs, intent, toolName, ctx);
10746
10914
  return abs;
@@ -10750,7 +10918,7 @@ ${body}`;
10750
10918
  normalized = normalized.slice(1);
10751
10919
  }
10752
10920
  if (normalized.length === 0) normalized = ".";
10753
- const resolved = pathMod4.resolve(rootDir, normalized);
10921
+ const resolved = pathMod5.resolve(rootDir, normalized);
10754
10922
  if (!pathIsUnder(resolved, normRoot)) {
10755
10923
  throw new Error(
10756
10924
  `path escapes sandbox root (${normRoot}): ${raw} \u2014 use an absolute system path like /Users/foo or C:\\Users\\foo to request approved outside-sandbox access`
@@ -10812,7 +10980,8 @@ ${body}`;
10812
10980
  if (looksBinary(raw)) {
10813
10981
  return `[refused: ${rel} appears to be binary (${formatBytes(sizeBytes)}) \u2014 read_file returns text only. Use get_file_info for stat.]`;
10814
10982
  }
10815
- const text = raw.toString("utf8");
10983
+ const { text } = decodeFileBuffer(raw);
10984
+ ctx?.readTracker?.markRead(abs);
10816
10985
  let lines = text.split(/\r?\n/);
10817
10986
  if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
10818
10987
  const totalLines = lines.length;
@@ -10950,7 +11119,7 @@ ${slice.join("\n")}`);
10950
11119
  lines.push(line);
10951
11120
  emitted++;
10952
11121
  if (e.isDirectory() && !skip) {
10953
- await walk2(pathMod4.join(dir, e.name), depth + 1);
11122
+ await walk2(pathMod5.join(dir, e.name), depth + 1);
10954
11123
  }
10955
11124
  }
10956
11125
  };
@@ -11110,14 +11279,20 @@ ${slice.join("\n")}`);
11110
11279
  },
11111
11280
  fn: async (args, ctx) => {
11112
11281
  const abs = await safePath(args.path, "write_file", ctx, "write");
11113
- await fs3.mkdir(pathMod4.dirname(abs), { recursive: true });
11114
- await fs3.writeFile(abs, args.content, "utf8");
11282
+ await fs3.mkdir(pathMod5.dirname(abs), { recursive: true });
11283
+ let encoding = "utf8";
11284
+ try {
11285
+ encoding = decodeFileBuffer(await fs3.readFile(abs)).encoding;
11286
+ } catch {
11287
+ }
11288
+ await fs3.writeFile(abs, encodeFile(args.content, encoding));
11289
+ ctx?.readTracker?.markRead(abs);
11115
11290
  return `wrote ${args.content.length} chars to ${displayRel3(rootDir, abs)}`;
11116
11291
  }
11117
11292
  });
11118
11293
  registry.register({
11119
11294
  name: "edit_file",
11120
- description: "Apply a SEARCH/REPLACE edit to an existing file. `search` must match exactly (whitespace sensitive) \u2014 no regex. The match must be unique in the file; otherwise the edit is refused to avoid surprise rewrites.",
11295
+ description: "Apply a SEARCH/REPLACE edit to an existing file. Call `read_file` on this path first this session \u2014 the tool refuses otherwise, since SEARCH must match on-disk bytes exactly. `search` is whitespace-sensitive plain text (no regex) and must be unique in the file; otherwise the edit is refused to avoid surprise rewrites.",
11121
11296
  parameters: {
11122
11297
  type: "object",
11123
11298
  properties: {
@@ -11127,11 +11302,16 @@ ${slice.join("\n")}`);
11127
11302
  },
11128
11303
  required: ["path", "search", "replace"]
11129
11304
  },
11130
- fn: async (args, ctx) => applyEdit(rootDir, await safePath(args.path, "edit_file", ctx, "write"), args)
11305
+ fn: async (args, ctx) => applyEdit(
11306
+ rootDir,
11307
+ await safePath(args.path, "edit_file", ctx, "write"),
11308
+ args,
11309
+ ctx?.readTracker ? (abs) => ctx.readTracker.hasRead(abs) : void 0
11310
+ )
11131
11311
  });
11132
11312
  registry.register({
11133
11313
  name: "multi_edit",
11134
- description: "Apply N SEARCH/REPLACE edits across ONE OR MORE files in one call. Edits validate across the full batch before writing. Validation failures leave all files untouched; disk write failures trigger best-effort rollback of files that may have been modified. Per-file edits run in array order, so a later edit can match text inserted by an earlier one. Same per-edit rules as edit_file: `search` is exact text (whitespace sensitive, no regex) and must be unique in its target file at the moment that edit applies. Use this for renames spanning multiple files, cross-file refactors, or any batch where you'd otherwise loop edit_file.",
11314
+ description: "Apply N SEARCH/REPLACE edits across ONE OR MORE files in one call. Every target file must have been `read_file`'d this session \u2014 the tool refuses the whole batch otherwise. Edits validate across the full batch before writing. Validation failures leave all files untouched; disk write failures trigger best-effort rollback of files that may have been modified. Per-file edits run in array order, so a later edit can match text inserted by an earlier one. Same per-edit rules as edit_file: `search` is exact text (whitespace sensitive, no regex) and must be unique in its target file at the moment that edit applies. Use this for renames spanning multiple files, cross-file refactors, or any batch where you'd otherwise loop edit_file.",
11135
11315
  parameters: {
11136
11316
  type: "object",
11137
11317
  properties: {
@@ -11165,7 +11345,11 @@ ${slice.join("\n")}`);
11165
11345
  replace: e?.replace
11166
11346
  }))
11167
11347
  );
11168
- return applyMultiEdit(rootDir, resolved);
11348
+ return applyMultiEdit(
11349
+ rootDir,
11350
+ resolved,
11351
+ ctx?.readTracker ? (abs) => ctx.readTracker.hasRead(abs) : void 0
11352
+ );
11169
11353
  }
11170
11354
  });
11171
11355
  registry.register({
@@ -11196,7 +11380,7 @@ ${slice.join("\n")}`);
11196
11380
  fn: async (args, ctx) => {
11197
11381
  const src = await safePath(args.source, "move_file", ctx, "write");
11198
11382
  const dst = await safePath(args.destination, "move_file", ctx, "write");
11199
- await fs3.mkdir(pathMod4.dirname(dst), { recursive: true });
11383
+ await fs3.mkdir(pathMod5.dirname(dst), { recursive: true });
11200
11384
  await fs3.rename(src, dst);
11201
11385
  return `moved ${displayRel3(rootDir, src)} \u2192 ${displayRel3(rootDir, dst)}`;
11202
11386
  }
@@ -11264,7 +11448,7 @@ ${slice.join("\n")}`);
11264
11448
  fn: async (args, ctx) => {
11265
11449
  const src = await safePath(args.source, "copy_file", ctx);
11266
11450
  const dst = await safePath(args.destination, "copy_file", ctx, "write");
11267
- await fs3.mkdir(pathMod4.dirname(dst), { recursive: true });
11451
+ await fs3.mkdir(pathMod5.dirname(dst), { recursive: true });
11268
11452
  await fs3.cp(src, dst, { recursive: true, force: false, errorOnExist: true });
11269
11453
  return `copied ${displayRel3(rootDir, src)} \u2192 ${displayRel3(rootDir, dst)}`;
11270
11454
  }
@@ -11969,7 +12153,7 @@ function formatSubagentResult(r) {
11969
12153
  });
11970
12154
  }
11971
12155
  function forkRegistryExcluding(parent, exclude) {
11972
- const child = new ToolRegistry();
12156
+ const child = new ToolRegistry({ rateLimit: parent.rateLimitPolicy });
11973
12157
  for (const spec of parent.specs()) {
11974
12158
  const name = spec.function.name;
11975
12159
  if (exclude.has(name)) continue;
@@ -11981,7 +12165,7 @@ function forkRegistryExcluding(parent, exclude) {
11981
12165
  return child;
11982
12166
  }
11983
12167
  function forkRegistryWithAllowList(parent, allow, alsoExclude) {
11984
- const child = new ToolRegistry();
12168
+ const child = new ToolRegistry({ rateLimit: parent.rateLimitPolicy });
11985
12169
  for (const spec of parent.specs()) {
11986
12170
  const name = spec.function.name;
11987
12171
  if (!allow.has(name)) continue;
@@ -12008,7 +12192,7 @@ import {
12008
12192
  writeFileSync,
12009
12193
  writeSync
12010
12194
  } from "fs";
12011
- import { dirname as dirname3, resolve as resolve4 } from "path";
12195
+ import { dirname as dirname3, isAbsolute as isAbsolute3, relative as relative6, resolve as resolve5 } from "path";
12012
12196
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
12013
12197
  function parseEditBlocks(text) {
12014
12198
  const out = [];
@@ -12025,10 +12209,30 @@ function parseEditBlocks(text) {
12025
12209
  }
12026
12210
  return out;
12027
12211
  }
12212
+ function resolveEditPath(rootDir, rawPath) {
12213
+ const absRoot = resolve5(rootDir);
12214
+ if (/^[A-Za-z]:[\\/]/.test(rawPath) || looksLikeAbsoluteSystemPath2(rawPath)) {
12215
+ return resolve5(rawPath);
12216
+ }
12217
+ let rooted = rawPath;
12218
+ while (rooted.startsWith("/") || rooted.startsWith("\\")) {
12219
+ rooted = rooted.slice(1);
12220
+ }
12221
+ return resolve5(absRoot, rooted || ".");
12222
+ }
12223
+ function looksLikeAbsoluteSystemPath2(rawPath) {
12224
+ return /^\/(?:home|Users|etc|var|opt|tmp|usr|mnt|Library|Volumes|proc|sys|dev|run|srv|media|Applications|System|root|boot|private)(?:[/\\]|$)/.test(
12225
+ rawPath
12226
+ );
12227
+ }
12228
+ function pathIsUnder2(child, parent) {
12229
+ const rel = relative6(parent, child);
12230
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
12231
+ }
12028
12232
  function applyEditBlock(block, rootDir) {
12029
- const absRoot = resolve4(rootDir);
12030
- const absTarget = resolve4(absRoot, block.path);
12031
- if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
12233
+ const absRoot = resolve5(rootDir);
12234
+ const absTarget = resolveEditPath(rootDir, block.path);
12235
+ if (!pathIsUnder2(absTarget, absRoot)) {
12032
12236
  return {
12033
12237
  path: block.path,
12034
12238
  status: "path-escape",
@@ -12081,7 +12285,7 @@ function applyEditBlock(block, rootDir) {
12081
12285
  if (n <= 0) break;
12082
12286
  readBytes += n;
12083
12287
  }
12084
- const content = inBuf.toString("utf8", 0, readBytes);
12288
+ const { text: content, encoding } = decodeFileBuffer(inBuf.subarray(0, readBytes));
12085
12289
  const le = lineEndingOf(content);
12086
12290
  const adaptedSearch = block.search.replace(/\r?\n/g, le);
12087
12291
  const adaptedReplace = block.replace.replace(/\r?\n/g, le);
@@ -12102,7 +12306,7 @@ function applyEditBlock(block, rootDir) {
12102
12306
  };
12103
12307
  }
12104
12308
  const replaced = `${content.slice(0, idx)}${adaptedReplace}${content.slice(idx + adaptedSearch.length)}`;
12105
- const outBuf = Buffer.from(replaced, "utf8");
12309
+ const outBuf = encodeFile(replaced, encoding);
12106
12310
  ftruncateSync(fd, outBuf.length);
12107
12311
  let written = 0;
12108
12312
  while (written < outBuf.length) {
@@ -12122,11 +12326,11 @@ function applyEditBlocks(blocks, rootDir) {
12122
12326
  return blocks.map((b) => applyEditBlock(b, rootDir));
12123
12327
  }
12124
12328
  function toWholeFileEditBlock(path, content, rootDir) {
12125
- const abs = resolve4(rootDir, path);
12329
+ const abs = resolveEditPath(rootDir, path);
12126
12330
  let search = "";
12127
12331
  if (existsSync3(abs)) {
12128
12332
  try {
12129
- search = readFileSync3(abs, "utf8");
12333
+ search = decodeFileBuffer(readFileSync3(abs)).text;
12130
12334
  } catch {
12131
12335
  search = "";
12132
12336
  }
@@ -12134,19 +12338,21 @@ function toWholeFileEditBlock(path, content, rootDir) {
12134
12338
  return { path, search, replace: content, offset: 0 };
12135
12339
  }
12136
12340
  function snapshotBeforeEdits(blocks, rootDir) {
12137
- const absRoot = resolve4(rootDir);
12341
+ const absRoot = resolve5(rootDir);
12138
12342
  const seen = /* @__PURE__ */ new Set();
12139
12343
  const snapshots = [];
12140
12344
  for (const b of blocks) {
12141
- if (seen.has(b.path)) continue;
12142
- seen.add(b.path);
12143
- const abs = resolve4(absRoot, b.path);
12345
+ const abs = resolveEditPath(rootDir, b.path);
12346
+ if (!pathIsUnder2(abs, absRoot)) continue;
12347
+ if (seen.has(abs)) continue;
12348
+ seen.add(abs);
12144
12349
  if (!existsSync3(abs)) {
12145
12350
  snapshots.push({ path: b.path, prevContent: null });
12146
12351
  continue;
12147
12352
  }
12148
12353
  try {
12149
- snapshots.push({ path: b.path, prevContent: readFileSync3(abs, "utf8") });
12354
+ const { text, encoding } = decodeFileBuffer(readFileSync3(abs));
12355
+ snapshots.push({ path: b.path, prevContent: text, prevEncoding: encoding });
12150
12356
  } catch {
12151
12357
  snapshots.push({ path: b.path, prevContent: null });
12152
12358
  }
@@ -12154,10 +12360,10 @@ function snapshotBeforeEdits(blocks, rootDir) {
12154
12360
  return snapshots;
12155
12361
  }
12156
12362
  function restoreSnapshots(snapshots, rootDir) {
12157
- const absRoot = resolve4(rootDir);
12363
+ const absRoot = resolve5(rootDir);
12158
12364
  return snapshots.map((snap) => {
12159
- const abs = resolve4(absRoot, snap.path);
12160
- if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
12365
+ const abs = resolveEditPath(rootDir, snap.path);
12366
+ if (!pathIsUnder2(abs, absRoot)) {
12161
12367
  return {
12162
12368
  path: snap.path,
12163
12369
  status: "path-escape",
@@ -12173,7 +12379,7 @@ function restoreSnapshots(snapshots, rootDir) {
12173
12379
  message: "removed (the edit had created it)"
12174
12380
  };
12175
12381
  }
12176
- writeFileSync(abs, snap.prevContent, "utf8");
12382
+ writeFileSync(abs, encodeFile(snap.prevContent, snap.prevEncoding ?? "utf8"));
12177
12383
  return {
12178
12384
  path: snap.path,
12179
12385
  status: "applied",
@@ -12184,9 +12390,6 @@ function restoreSnapshots(snapshots, rootDir) {
12184
12390
  }
12185
12391
  });
12186
12392
  }
12187
- function sep() {
12188
- return process.platform === "win32" ? "\\" : "/";
12189
- }
12190
12393
  function lineEndingOf(text) {
12191
12394
  return text.includes("\r\n") ? "\r\n" : "\n";
12192
12395
  }
@@ -12228,4 +12431,4 @@ export {
12228
12431
  he/he.js:
12229
12432
  (*! https://mths.be/he v1.2.0 by @mathias | MIT license *)
12230
12433
  */
12231
- //# sourceMappingURL=chunk-OPYALNTT.js.map
12434
+ //# sourceMappingURL=chunk-MQWO32ZD.js.map