reasonix 0.40.0 → 0.41.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 +21 -13
  2. package/README.zh-CN.md +19 -13
  3. package/dashboard/app.css +8 -4
  4. package/dashboard/dist/app.js +279 -224
  5. package/dashboard/dist/app.js.map +1 -1
  6. package/dist/cli/acp-64VQZLDJ.js +708 -0
  7. package/dist/cli/acp-64VQZLDJ.js.map +1 -0
  8. package/dist/cli/chat-ZAGX52RV.js +46 -0
  9. package/dist/cli/{chunk-UCMTWZKU.js → chunk-2CXPDAWX.js} +2 -2
  10. package/dist/cli/{chunk-CLAN6PVH.js → chunk-4H3ZRJ2U.js} +19 -7
  11. package/dist/cli/chunk-4H3ZRJ2U.js.map +1 -0
  12. package/dist/cli/{chunk-A5LSGEEK.js → chunk-4W2CICFQ.js} +21 -10
  13. package/dist/cli/{chunk-A5LSGEEK.js.map → chunk-4W2CICFQ.js.map} +1 -1
  14. package/dist/cli/{chunk-CZSJILQP.js → chunk-65Q5HQ26.js} +39 -1
  15. package/dist/cli/chunk-65Q5HQ26.js.map +1 -0
  16. package/dist/cli/{chunk-XHQIK7B6.js → chunk-7SPOFTMT.js} +2 -2
  17. package/dist/cli/{chunk-5GKJLNP2.js → chunk-7VFNPMKG.js} +2 -2
  18. package/dist/cli/{chunk-UVRXTSK3.js → chunk-A3LL4XDV.js} +8 -2
  19. package/dist/cli/chunk-A3LL4XDV.js.map +1 -0
  20. package/dist/cli/{chunk-VLNRQMCI.js → chunk-A7VHMMDE.js} +2 -2
  21. package/dist/cli/{chunk-R4YTW7PR.js → chunk-ARF3N2SY.js} +56 -12
  22. package/dist/cli/chunk-ARF3N2SY.js.map +1 -0
  23. package/dist/cli/{chunk-AVB3WZWU.js → chunk-AT6GGIBV.js} +10 -10
  24. package/dist/cli/{chunk-RFX7TYVV.js → chunk-BOFL3T45.js} +14 -1
  25. package/dist/cli/chunk-BOFL3T45.js.map +1 -0
  26. package/dist/cli/{chunk-SZH34P45.js → chunk-BYZGO3BX.js} +43 -17
  27. package/dist/cli/chunk-BYZGO3BX.js.map +1 -0
  28. package/dist/cli/{chunk-7DLHHBGN.js → chunk-CD4SCQL4.js} +6 -4
  29. package/dist/cli/chunk-CD4SCQL4.js.map +1 -0
  30. package/dist/cli/{chunk-HCC42PEI.js → chunk-CFY2XLY6.js} +6 -2
  31. package/dist/cli/chunk-CFY2XLY6.js.map +1 -0
  32. package/dist/cli/{chunk-26UDIXLD.js → chunk-F2AV2QDK.js} +493 -460
  33. package/dist/cli/chunk-F2AV2QDK.js.map +1 -0
  34. package/dist/cli/{chunk-KMWKGPFZ.js → chunk-H4OLWRSX.js} +10 -1
  35. package/dist/cli/chunk-H4OLWRSX.js.map +1 -0
  36. package/dist/cli/{chunk-4YV2GBYG.js → chunk-IEA6JOIP.js} +291 -98
  37. package/dist/cli/chunk-IEA6JOIP.js.map +1 -0
  38. package/dist/cli/{chunk-WKOMCPXP.js → chunk-KZYLMMU5.js} +21 -13
  39. package/dist/cli/chunk-KZYLMMU5.js.map +1 -0
  40. package/dist/cli/{chunk-JWCTX5S4.js → chunk-L7W3HJZQ.js} +2 -2
  41. package/dist/cli/{chunk-MRLXEMZ7.js → chunk-LN27AKV3.js} +1 -1
  42. package/dist/cli/chunk-LN27AKV3.js.map +1 -0
  43. package/dist/cli/{chunk-IYF36OCJ.js → chunk-LTXADNCO.js} +2 -2
  44. package/dist/cli/{chunk-H7PHYVPM.js → chunk-MHGPBJ2T.js} +44 -8
  45. package/dist/cli/chunk-MHGPBJ2T.js.map +1 -0
  46. package/dist/cli/{chunk-ULBW7DYL.js → chunk-RAUPWSYA.js} +2 -2
  47. package/dist/cli/chunk-SXLJBFIV.js +245 -0
  48. package/dist/cli/chunk-SXLJBFIV.js.map +1 -0
  49. package/dist/cli/{chunk-4X3NY5ZM.js → chunk-UV7XJUJH.js} +2 -2
  50. package/dist/cli/{chunk-XJLZ4HKU.js → chunk-VFG4GIT3.js} +2 -2
  51. package/dist/cli/{chunk-FFNOMR32.js → chunk-WE3YZULK.js} +2 -2
  52. package/dist/cli/chunk-Y5XNV3NX.js +25 -0
  53. package/dist/cli/chunk-Y5XNV3NX.js.map +1 -0
  54. package/dist/cli/{chunk-XST7BSZJ.js → chunk-YJFKFTAL.js} +7 -1
  55. package/dist/cli/chunk-YJFKFTAL.js.map +1 -0
  56. package/dist/cli/{code-YQGVLIT2.js → code-X3M6ENTQ.js} +38 -35
  57. package/dist/cli/{code-YQGVLIT2.js.map → code-X3M6ENTQ.js.map} +1 -1
  58. package/dist/cli/{commands-FQZOBLLZ.js → commands-QY7MSQG7.js} +4 -4
  59. package/dist/cli/{commit-ZS24SHPG.js → commit-BRCQ3OQO.js} +3 -3
  60. package/dist/cli/{desktop-6OLENOOO.js → desktop-ZTMHQR2Y.js} +247 -28
  61. package/dist/cli/desktop-ZTMHQR2Y.js.map +1 -0
  62. package/dist/cli/{diff-2VUKNGEI.js → diff-YASCB7PU.js} +7 -7
  63. package/dist/cli/{doctor-JO2WNN6C.js → doctor-XCN5ETVP.js} +9 -9
  64. package/dist/cli/{events-APSVNROZ.js → events-2AJTXR7I.js} +3 -3
  65. package/dist/cli/index.js +69 -35
  66. package/dist/cli/index.js.map +1 -1
  67. package/dist/cli/{mcp-DCKOE5RF.js → mcp-YMWBLRR7.js} +2 -2
  68. package/dist/cli/{mcp-browse-D6GBP5RQ.js → mcp-browse-XLDUE6SB.js} +7 -3
  69. package/dist/cli/mcp-browse-XLDUE6SB.js.map +1 -0
  70. package/dist/cli/{mcp-inspect-KFGFPJ3E.js → mcp-inspect-H4D2HSJP.js} +5 -7
  71. package/dist/cli/{mcp-inspect-KFGFPJ3E.js.map → mcp-inspect-H4D2HSJP.js.map} +1 -1
  72. package/dist/cli/{prompt-PKCCLLAD.js → prompt-RSIHN62V.js} +4 -3
  73. package/dist/cli/{prune-sessions-LV33R47N.js → prune-sessions-4N3BVST2.js} +2 -2
  74. package/dist/cli/{replay-WFCYX7XF.js → replay-3GTWM75X.js} +8 -8
  75. package/dist/cli/{run-IUJYEPMT.js → run-BLZPTRDX.js} +19 -21
  76. package/dist/cli/{run-IUJYEPMT.js.map → run-BLZPTRDX.js.map} +1 -1
  77. package/dist/cli/{server-CN4QPPVJ.js → server-DRFPXXSH.js} +16 -12
  78. package/dist/cli/{server-CN4QPPVJ.js.map → server-DRFPXXSH.js.map} +1 -1
  79. package/dist/cli/{sessions-F5GPGTJN.js → sessions-BOWFPTXT.js} +13 -13
  80. package/dist/cli/{setup-WWMDBPSB.js → setup-FQL2JJC2.js} +5 -5
  81. package/dist/cli/version-XQXYSJ5L.js +30 -0
  82. package/dist/index.d.ts +148 -103
  83. package/dist/index.js +468 -134
  84. package/dist/index.js.map +1 -1
  85. package/package.json +2 -1
  86. package/dist/cli/chat-G7CUW4ZI.js +0 -45
  87. package/dist/cli/chunk-26UDIXLD.js.map +0 -1
  88. package/dist/cli/chunk-4YV2GBYG.js.map +0 -1
  89. package/dist/cli/chunk-7DLHHBGN.js.map +0 -1
  90. package/dist/cli/chunk-CLAN6PVH.js.map +0 -1
  91. package/dist/cli/chunk-CPTZ5OHX.js +0 -18
  92. package/dist/cli/chunk-CPTZ5OHX.js.map +0 -1
  93. package/dist/cli/chunk-CZSJILQP.js.map +0 -1
  94. package/dist/cli/chunk-H7PHYVPM.js.map +0 -1
  95. package/dist/cli/chunk-HCC42PEI.js.map +0 -1
  96. package/dist/cli/chunk-KMWKGPFZ.js.map +0 -1
  97. package/dist/cli/chunk-MRLXEMZ7.js.map +0 -1
  98. package/dist/cli/chunk-R4YTW7PR.js.map +0 -1
  99. package/dist/cli/chunk-RFX7TYVV.js.map +0 -1
  100. package/dist/cli/chunk-SZH34P45.js.map +0 -1
  101. package/dist/cli/chunk-UVRXTSK3.js.map +0 -1
  102. package/dist/cli/chunk-WKOMCPXP.js.map +0 -1
  103. package/dist/cli/chunk-XST7BSZJ.js.map +0 -1
  104. package/dist/cli/desktop-6OLENOOO.js.map +0 -1
  105. package/dist/cli/mcp-browse-D6GBP5RQ.js.map +0 -1
  106. package/dist/cli/version-KQUPV6T5.js +0 -30
  107. /package/dist/cli/{chat-G7CUW4ZI.js.map → chat-ZAGX52RV.js.map} +0 -0
  108. /package/dist/cli/{chunk-UCMTWZKU.js.map → chunk-2CXPDAWX.js.map} +0 -0
  109. /package/dist/cli/{chunk-XHQIK7B6.js.map → chunk-7SPOFTMT.js.map} +0 -0
  110. /package/dist/cli/{chunk-5GKJLNP2.js.map → chunk-7VFNPMKG.js.map} +0 -0
  111. /package/dist/cli/{chunk-VLNRQMCI.js.map → chunk-A7VHMMDE.js.map} +0 -0
  112. /package/dist/cli/{chunk-AVB3WZWU.js.map → chunk-AT6GGIBV.js.map} +0 -0
  113. /package/dist/cli/{chunk-JWCTX5S4.js.map → chunk-L7W3HJZQ.js.map} +0 -0
  114. /package/dist/cli/{chunk-IYF36OCJ.js.map → chunk-LTXADNCO.js.map} +0 -0
  115. /package/dist/cli/{chunk-ULBW7DYL.js.map → chunk-RAUPWSYA.js.map} +0 -0
  116. /package/dist/cli/{chunk-4X3NY5ZM.js.map → chunk-UV7XJUJH.js.map} +0 -0
  117. /package/dist/cli/{chunk-XJLZ4HKU.js.map → chunk-VFG4GIT3.js.map} +0 -0
  118. /package/dist/cli/{chunk-FFNOMR32.js.map → chunk-WE3YZULK.js.map} +0 -0
  119. /package/dist/cli/{commands-FQZOBLLZ.js.map → commands-QY7MSQG7.js.map} +0 -0
  120. /package/dist/cli/{commit-ZS24SHPG.js.map → commit-BRCQ3OQO.js.map} +0 -0
  121. /package/dist/cli/{diff-2VUKNGEI.js.map → diff-YASCB7PU.js.map} +0 -0
  122. /package/dist/cli/{doctor-JO2WNN6C.js.map → doctor-XCN5ETVP.js.map} +0 -0
  123. /package/dist/cli/{events-APSVNROZ.js.map → events-2AJTXR7I.js.map} +0 -0
  124. /package/dist/cli/{mcp-DCKOE5RF.js.map → mcp-YMWBLRR7.js.map} +0 -0
  125. /package/dist/cli/{prompt-PKCCLLAD.js.map → prompt-RSIHN62V.js.map} +0 -0
  126. /package/dist/cli/{prune-sessions-LV33R47N.js.map → prune-sessions-4N3BVST2.js.map} +0 -0
  127. /package/dist/cli/{replay-WFCYX7XF.js.map → replay-3GTWM75X.js.map} +0 -0
  128. /package/dist/cli/{sessions-F5GPGTJN.js.map → sessions-BOWFPTXT.js.map} +0 -0
  129. /package/dist/cli/{setup-WWMDBPSB.js.map → setup-FQL2JJC2.js.map} +0 -0
  130. /package/dist/cli/{version-KQUPV6T5.js.map → version-XQXYSJ5L.js.map} +0 -0
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  MemoryStore,
4
4
  sanitizeMemoryName
5
- } from "./chunk-R4YTW7PR.js";
5
+ } from "./chunk-ARF3N2SY.js";
6
6
  import {
7
7
  countTokens,
8
8
  estimateConversationTokens,
@@ -10,20 +10,20 @@ import {
10
10
  } from "./chunk-DAEAAVDF.js";
11
11
  import {
12
12
  Usage
13
- } from "./chunk-KMWKGPFZ.js";
13
+ } from "./chunk-H4OLWRSX.js";
14
14
  import {
15
15
  applyEdit,
16
16
  applyMultiEdit,
17
17
  pauseGate
18
- } from "./chunk-SZH34P45.js";
18
+ } from "./chunk-BYZGO3BX.js";
19
19
  import {
20
20
  NEGATIVE_CLAIM_RULE,
21
21
  TUI_FORMATTING_RULES
22
- } from "./chunk-7DLHHBGN.js";
22
+ } from "./chunk-CD4SCQL4.js";
23
23
  import {
24
24
  formatHookOutcomeMessage,
25
25
  runHooks
26
- } from "./chunk-FFNOMR32.js";
26
+ } from "./chunk-WE3YZULK.js";
27
27
  import {
28
28
  ignoredByLayers,
29
29
  loadGitignoreAt,
@@ -35,17 +35,18 @@ import {
35
35
  loadSessionMessages,
36
36
  loadSessionMeta,
37
37
  rewriteSession
38
- } from "./chunk-XST7BSZJ.js";
38
+ } from "./chunk-YJFKFTAL.js";
39
39
  import {
40
40
  t
41
- } from "./chunk-H7PHYVPM.js";
41
+ } from "./chunk-MHGPBJ2T.js";
42
42
  import {
43
43
  DEFAULT_INDEX_EXCLUDES,
44
44
  addProjectPathAllowed,
45
+ loadMemoryTypeRegistry,
45
46
  loadProjectPathAllowed,
46
47
  webSearchEndpoint,
47
48
  webSearchEngine
48
- } from "./chunk-CZSJILQP.js";
49
+ } from "./chunk-65Q5HQ26.js";
49
50
  import {
50
51
  DEEPSEEK_CONTEXT_TOKENS,
51
52
  DEFAULT_CONTEXT_TOKENS,
@@ -557,17 +558,25 @@ function missingRequiredParam(schema, args) {
557
558
  // src/memory/runtime.ts
558
559
  import { createHash } from "crypto";
559
560
  var ImmutablePrefix = class {
561
+ /** Stable across turns; rebuilt only on /new when REASONIX.md changed on disk. */
560
562
  system;
561
563
  /** Each `addTool` costs one cache-miss turn — DeepSeek's prefix cache is keyed by full tool list. */
562
564
  _toolSpecs;
563
565
  fewShots;
564
- /** Invalidated only via `addTool`; bypassing it leaves cache stale → fingerprint diverges from sent prefix. */
566
+ /** Invalidated by addTool / removeTool / replaceSystem; bypassing any of those leaves cache stale → fingerprint diverges from sent prefix. */
565
567
  _fingerprintCache = null;
566
568
  constructor(opts) {
567
569
  this.system = opts.system;
568
570
  this._toolSpecs = [...opts.toolSpecs ?? []];
569
571
  this.fewShots = Object.freeze([...opts.fewShots ?? []]);
570
572
  }
573
+ /** Replaces the system prompt; returns true iff the string actually changed. Caller must accept a cache miss on the next turn. */
574
+ replaceSystem(s) {
575
+ if (this.system === s) return false;
576
+ this.system = s;
577
+ this._fingerprintCache = null;
578
+ return true;
579
+ }
571
580
  get toolSpecs() {
572
581
  return this._toolSpecs;
573
582
  }
@@ -1051,6 +1060,10 @@ function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
1051
1060
  }
1052
1061
 
1053
1062
  // src/loop/healing.ts
1063
+ var _stampSeq = 0;
1064
+ function stampMissingIds(calls) {
1065
+ return calls.map((c) => c.id ? c : { ...c, id: `z-ext-${Date.now()}-${_stampSeq++}` });
1066
+ }
1054
1067
  function fixToolCallPairing(messages) {
1055
1068
  const out = [];
1056
1069
  let droppedAssistantCalls = 0;
@@ -1058,9 +1071,10 @@ function fixToolCallPairing(messages) {
1058
1071
  for (let i = 0; i < messages.length; i++) {
1059
1072
  const msg = messages[i];
1060
1073
  if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
1074
+ const calls = stampMissingIds(msg.tool_calls);
1061
1075
  const needed = /* @__PURE__ */ new Set();
1062
- for (const call of msg.tool_calls) {
1063
- if (call?.id) needed.add(call.id);
1076
+ for (const call of calls) {
1077
+ if (call.id) needed.add(call.id);
1064
1078
  }
1065
1079
  const candidates = [];
1066
1080
  let j = i + 1;
@@ -1074,7 +1088,7 @@ function fixToolCallPairing(messages) {
1074
1088
  j++;
1075
1089
  }
1076
1090
  if (needed.size === 0) {
1077
- out.push(msg);
1091
+ out.push({ ...msg, tool_calls: calls });
1078
1092
  for (const r of candidates) out.push(r);
1079
1093
  i = j - 1;
1080
1094
  } else {
@@ -1138,6 +1152,32 @@ function* hookWarnings(outcomes, turn) {
1138
1152
  }
1139
1153
  }
1140
1154
 
1155
+ // src/loop/read-only-loop-tracker.ts
1156
+ var READONLY_LOOP_ESCALATION_THRESHOLD = 8;
1157
+ var ReadOnlyLoopTracker = class {
1158
+ streak = 0;
1159
+ threshold;
1160
+ constructor(threshold = READONLY_LOOP_ESCALATION_THRESHOLD) {
1161
+ this.threshold = Math.max(1, threshold);
1162
+ }
1163
+ reset() {
1164
+ this.streak = 0;
1165
+ }
1166
+ /** True ONLY on the call where the streak crosses the configured threshold. */
1167
+ noteAndCrossedThreshold(isReadOnly) {
1168
+ if (!isReadOnly) {
1169
+ this.streak = 0;
1170
+ return false;
1171
+ }
1172
+ const before = this.streak;
1173
+ this.streak += 1;
1174
+ return before < this.threshold && this.streak >= this.threshold;
1175
+ }
1176
+ get currentStreak() {
1177
+ return this.streak;
1178
+ }
1179
+ };
1180
+
1141
1181
  // src/loop/turn-failure-tracker.ts
1142
1182
  var FAILURE_ESCALATION_THRESHOLD = 3;
1143
1183
  var TurnFailureTracker = class {
@@ -1518,6 +1558,7 @@ var CacheFirstLoop = class {
1518
1558
  confirmationGate;
1519
1559
  /** Number of messages that were pre-loaded from the session file. */
1520
1560
  resumedMessageCount;
1561
+ _rebuildSystem;
1521
1562
  _turn = 0;
1522
1563
  _streamPreference;
1523
1564
  /** Threaded through HTTP + every tool dispatch so Esc cancels in-flight work, not after. */
@@ -1527,6 +1568,7 @@ var CacheFirstLoop = class {
1527
1568
  _proArmedForNextTurn = false;
1528
1569
  _escalateThisTurn = false;
1529
1570
  _turnFailures;
1571
+ _readOnlyLoop;
1530
1572
  _turnSelfCorrected = false;
1531
1573
  _foldedThisTurn = false;
1532
1574
  _toolDispatchesThisStep = 0;
@@ -1549,34 +1591,18 @@ var CacheFirstLoop = class {
1549
1591
  this._turnFailures = new TurnFailureTracker(
1550
1592
  resolveFailureThreshold(opts.failureThreshold, FAILURE_ESCALATION_THRESHOLD)
1551
1593
  );
1594
+ this._readOnlyLoop = new ReadOnlyLoopTracker(
1595
+ parsePositiveIntEnv(process.env.REASONIX_READONLY_LOOP_THRESHOLD) ?? READONLY_LOOP_ESCALATION_THRESHOLD
1596
+ );
1552
1597
  this.maxToolIters = opts.maxToolIters ?? 64;
1553
1598
  this.hooks = opts.hooks ?? [];
1554
1599
  this.hookCwd = opts.hookCwd ?? process.cwd();
1555
1600
  this.confirmationGate = opts.confirmationGate ?? pauseGate;
1601
+ this._rebuildSystem = opts.rebuildSystem ?? null;
1556
1602
  this._streamPreference = opts.stream ?? true;
1557
1603
  this.stream = this._streamPreference;
1558
1604
  const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
1559
1605
  const registry = this.tools;
1560
- const isMutating = (call) => {
1561
- const name = call.function?.name;
1562
- if (!name) return false;
1563
- const def = registry.get(name);
1564
- if (!def) return false;
1565
- if (def.readOnlyCheck) {
1566
- let args = {};
1567
- try {
1568
- args = JSON.parse(call.function?.arguments ?? "{}") ?? {};
1569
- } catch {
1570
- }
1571
- try {
1572
- if (def.readOnlyCheck(args)) return false;
1573
- } catch (err) {
1574
- process.stderr.write(`readOnlyCheck for ${name} threw: ${err.message}
1575
- `);
1576
- }
1577
- }
1578
- return def.readOnly !== true;
1579
- };
1580
1606
  const isStormExempt = (call) => {
1581
1607
  const name = call.function?.name;
1582
1608
  if (!name) return false;
@@ -1584,7 +1610,7 @@ var CacheFirstLoop = class {
1584
1610
  };
1585
1611
  this.repair = new ToolCallRepair({
1586
1612
  allowedToolNames: allowedNames,
1587
- isMutating,
1613
+ isMutating: (call) => this.isMutating(call),
1588
1614
  isStormExempt,
1589
1615
  stormThreshold: parsePositiveIntEnv(process.env.REASONIX_STORM_THRESHOLD),
1590
1616
  stormWindow: parsePositiveIntEnv(process.env.REASONIX_STORM_WINDOW)
@@ -1677,7 +1703,7 @@ var CacheFirstLoop = class {
1677
1703
  }
1678
1704
  }
1679
1705
  }
1680
- /** "New chat" — drops in-memory messages, archives the on-disk transcript so it survives in Sessions, keeps sessionName so the prefix cache stays warm. */
1706
+ /** "New chat" — drops in-memory messages, archives the on-disk transcript so it survives in Sessions, keeps sessionName so the prefix cache stays warm. Re-runs the system-prompt builder if one was wired (issue #778: REASONIX.md edits otherwise need a restart). */
1681
1707
  clearLog() {
1682
1708
  const dropped = this.log.length;
1683
1709
  this.log.compactInPlace([]);
@@ -1691,7 +1717,14 @@ var CacheFirstLoop = class {
1691
1717
  }
1692
1718
  this.scratch.reset();
1693
1719
  this._inflight.clear();
1694
- return { dropped, archived };
1720
+ let systemRebuilt = false;
1721
+ if (this._rebuildSystem) {
1722
+ try {
1723
+ systemRebuilt = this.prefix.replaceSystem(this._rebuildSystem());
1724
+ } catch {
1725
+ }
1726
+ }
1727
+ return { dropped, archived, systemRebuilt };
1695
1728
  }
1696
1729
  configure(opts) {
1697
1730
  if (opts.model !== void 0) this.model = opts.model;
@@ -1737,6 +1770,35 @@ var CacheFirstLoop = class {
1737
1770
  this._escalateThisTurn = true;
1738
1771
  return true;
1739
1772
  }
1773
+ /** Returns true ONLY on the call where the read-only streak crosses the threshold (#681). */
1774
+ noteReadOnlyToolCall(call) {
1775
+ const isReadOnly = !this.isMutating(call);
1776
+ if (!this._readOnlyLoop.noteAndCrossedThreshold(isReadOnly)) return false;
1777
+ if (this._escalateThisTurn || !this.autoEscalate) return false;
1778
+ this._escalateThisTurn = true;
1779
+ return true;
1780
+ }
1781
+ /** A call counts as mutating when its definition reports `readOnly !== true` and any dynamic `readOnlyCheck` doesn't override that for these args. */
1782
+ isMutating(call) {
1783
+ const name = call.function?.name;
1784
+ if (!name) return false;
1785
+ const def = this.tools.get(name);
1786
+ if (!def) return false;
1787
+ if (def.readOnlyCheck) {
1788
+ let args = {};
1789
+ try {
1790
+ args = JSON.parse(call.function?.arguments ?? "{}") ?? {};
1791
+ } catch {
1792
+ }
1793
+ try {
1794
+ if (def.readOnlyCheck(args)) return false;
1795
+ } catch (err) {
1796
+ process.stderr.write(`readOnlyCheck for ${name} threw: ${err.message}
1797
+ `);
1798
+ }
1799
+ }
1800
+ return def.readOnly !== true;
1801
+ }
1740
1802
  async runOneToolCall(call, signal) {
1741
1803
  const name = call.function?.name ?? "";
1742
1804
  const args = call.function?.arguments ?? "{}";
@@ -1857,6 +1919,7 @@ ${reason}`
1857
1919
  this.scratch.reset();
1858
1920
  this.repair.resetStorm();
1859
1921
  this._turnFailures.reset();
1922
+ this._readOnlyLoop.reset();
1860
1923
  this._turnSelfCorrected = false;
1861
1924
  this._escalateThisTurn = false;
1862
1925
  this._foldedThisTurn = false;
@@ -2283,6 +2346,17 @@ ${reason}`
2283
2346
  })
2284
2347
  };
2285
2348
  }
2349
+ if (this.noteReadOnlyToolCall(call)) {
2350
+ yield {
2351
+ turn: this._turn,
2352
+ role: "warning",
2353
+ content: t("loop.readOnlyLoopEscalation", {
2354
+ model: ESCALATION_MODEL,
2355
+ n: this._readOnlyLoop.currentStreak,
2356
+ fallback: this.model
2357
+ })
2358
+ };
2359
+ }
2286
2360
  yield {
2287
2361
  turn: this._turn,
2288
2362
  role: "tool",
@@ -2687,7 +2761,7 @@ function parseAtQuery(query) {
2687
2761
  trailingSlash: false
2688
2762
  };
2689
2763
  }
2690
- var AT_PICKER_PREFIX = /(?:^|\s)@([a-zA-Z0-9_./\\-]*)$/;
2764
+ var AT_PICKER_PREFIX = /(?:^|\s)@([\p{L}\p{N}_./\\-]*)$/u;
2691
2765
  function detectAtPicker(input) {
2692
2766
  const m = AT_PICKER_PREFIX.exec(input);
2693
2767
  if (!m) return null;
@@ -2773,7 +2847,7 @@ function fuzzySubseqScore(needle, target) {
2773
2847
  const lengthPenalty = Math.floor(target.length / 4);
2774
2848
  return quality + lengthPenalty;
2775
2849
  }
2776
- var AT_MENTION_PATTERN = /(?<=^|\s)@([a-zA-Z0-9_./\\-]+)/g;
2850
+ var AT_MENTION_PATTERN = /(?<=^|\s)@([\p{L}\p{N}_./\\-]+)/gu;
2777
2851
  function expandAtMentions(text, rootDir, opts = {}) {
2778
2852
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
2779
2853
  const maxDirEntries = Math.max(1, opts.maxDirEntries ?? DEFAULT_AT_DIR_MAX_ENTRIES);
@@ -2903,6 +2977,16 @@ var defaultFs = {
2903
2977
  function registerMemoryTools(registry, opts = {}) {
2904
2978
  const store = new MemoryStore({ homeDir: opts.homeDir, projectRoot: opts.projectRoot });
2905
2979
  const hasProject = store.hasProjectScope();
2980
+ const registry_types = loadMemoryTypeRegistry();
2981
+ const customTypeNames = registry_types.filter((r) => !r.builtin).map((r) => r.name);
2982
+ const typeDescParts = [
2983
+ "'user' = role/skills/prefs; 'feedback' = corrections or confirmed approaches; 'project' = facts/decisions about the current work; 'reference' = pointers to external systems the user uses."
2984
+ ];
2985
+ if (customTypeNames.length > 0) {
2986
+ typeDescParts.push(
2987
+ `Custom types declared in config: ${customTypeNames.join(", ")}. Any string is accepted; unknown types are stored verbatim and treated as 'reference' priority.`
2988
+ );
2989
+ }
2906
2990
  registry.register({
2907
2991
  name: "remember",
2908
2992
  description: "Save a memory for future sessions. Use when the user states a preference, corrects your approach, shares a non-obvious fact about this project, or explicitly asks you to remember something. Don't remember transient task state \u2014 only things worth recalling next session. The memory is written now but won't re-load into the system prompt until the next `/new` or launch.",
@@ -2911,8 +2995,7 @@ function registerMemoryTools(registry, opts = {}) {
2911
2995
  properties: {
2912
2996
  type: {
2913
2997
  type: "string",
2914
- enum: ["user", "feedback", "project", "reference"],
2915
- description: "'user' = role/skills/prefs; 'feedback' = corrections or confirmed approaches; 'project' = facts/decisions about the current work; 'reference' = pointers to external systems the user uses."
2998
+ description: typeDescParts.join(" ")
2916
2999
  },
2917
3000
  scope: {
2918
3001
  type: "string",
@@ -2930,6 +3013,16 @@ function registerMemoryTools(registry, opts = {}) {
2930
3013
  content: {
2931
3014
  type: "string",
2932
3015
  description: "Full memory body in markdown. For feedback/project types, structure as: rule/fact, then **Why:** line, then **How to apply:** line."
3016
+ },
3017
+ priority: {
3018
+ type: "string",
3019
+ enum: ["low", "medium", "high"],
3020
+ description: "Optional per-memory priority. `high` injects the entry into a `# HIGH PRIORITY constraints` block at the top of the system prompt \u2014 use sparingly, only for hard rules the model must never violate."
3021
+ },
3022
+ expires: {
3023
+ type: "string",
3024
+ enum: ["project_end"],
3025
+ description: "Optional lifecycle hint. `project_end` causes `/memory clear project` to also remove this entry even when it's stored at global scope."
2933
3026
  }
2934
3027
  },
2935
3028
  required: ["type", "scope", "name", "description", "content"]
@@ -2946,7 +3039,9 @@ function registerMemoryTools(registry, opts = {}) {
2946
3039
  type: args.type,
2947
3040
  scope: args.scope,
2948
3041
  description: args.description,
2949
- body: args.content
3042
+ body: args.content,
3043
+ ...args.priority ? { priority: args.priority } : {},
3044
+ ...args.expires ? { expires: args.expires } : {}
2950
3045
  });
2951
3046
  const key = sanitizeMemoryName(args.name);
2952
3047
  return [
@@ -3488,7 +3583,7 @@ ${i + 1}. ${r.title}`);
3488
3583
 
3489
3584
  // src/tools/filesystem.ts
3490
3585
  import { promises as fs3 } from "fs";
3491
- import * as pathMod3 from "path";
3586
+ import * as pathMod4 from "path";
3492
3587
  import picomatch2 from "picomatch";
3493
3588
 
3494
3589
  // src/tools/fs/glob.ts
@@ -3554,15 +3649,149 @@ async function globFiles(ctx, startAbs, args) {
3554
3649
  return lines.join("\n");
3555
3650
  }
3556
3651
 
3652
+ // src/tools/fs/outline.ts
3653
+ import * as pathMod2 from "path";
3654
+ var OUTLINE_MAX_ENTRIES = 30;
3655
+ var OUTLINE_TAIL_KEEP = 5;
3656
+ var TS_EXPORT_RE = /^export\s+(?:default\s+)?(?:async\s+)?(function|class|const|let|var|interface|type|enum)\s+\*?\s*(\w+)/;
3657
+ var PY_DECL_RE = /^(?:async\s+)?(def|class)\s+(\w+)/;
3658
+ var GO_DECL_RE = /^(func|type|var|const)\s+(?:\([^)]+\)\s+)?(\w+)/;
3659
+ var RUST_DECL_RE = /^(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|mod|type|const|static|union)\s+(\w+)/;
3660
+ var RUST_IMPL_RE = /^(?:unsafe\s+)?impl(?:\s*<[^>]+>)?\s+(?:[^{]+\s+for\s+)?(\w+)/;
3661
+ var MD_HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
3662
+ var MD_FENCE_RE = /^```/;
3663
+ var EXT_TO_LANG = {
3664
+ ".ts": "ts",
3665
+ ".tsx": "ts",
3666
+ ".mts": "ts",
3667
+ ".cts": "ts",
3668
+ ".js": "ts",
3669
+ ".jsx": "ts",
3670
+ ".mjs": "ts",
3671
+ ".cjs": "ts",
3672
+ ".py": "py",
3673
+ ".pyi": "py",
3674
+ ".go": "go",
3675
+ ".rs": "rust",
3676
+ ".md": "md",
3677
+ ".markdown": "md",
3678
+ ".mdx": "md"
3679
+ };
3680
+ function extractOutline(filename, lines) {
3681
+ const ext = pathMod2.extname(filename).toLowerCase();
3682
+ const lang = EXT_TO_LANG[ext];
3683
+ if (!lang) return [];
3684
+ switch (lang) {
3685
+ case "ts":
3686
+ return extractTs(lines);
3687
+ case "py":
3688
+ return extractPython(lines);
3689
+ case "go":
3690
+ return extractGo(lines);
3691
+ case "rust":
3692
+ return extractRust(lines);
3693
+ case "md":
3694
+ return extractMarkdown(lines);
3695
+ }
3696
+ }
3697
+ function extractTs(lines) {
3698
+ const out = [];
3699
+ for (let i = 0; i < lines.length; i++) {
3700
+ const line = lines[i];
3701
+ if (!line.startsWith("export ")) continue;
3702
+ const m = TS_EXPORT_RE.exec(line);
3703
+ if (!m) continue;
3704
+ out.push({ line: i + 1, text: `export ${m[1]} ${m[2]}` });
3705
+ }
3706
+ return out;
3707
+ }
3708
+ function extractPython(lines) {
3709
+ const out = [];
3710
+ for (let i = 0; i < lines.length; i++) {
3711
+ const line = lines[i];
3712
+ if (line.startsWith(" ") || line.startsWith(" ")) continue;
3713
+ const m = PY_DECL_RE.exec(line);
3714
+ if (!m) continue;
3715
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
3716
+ }
3717
+ return out;
3718
+ }
3719
+ function extractGo(lines) {
3720
+ const out = [];
3721
+ for (let i = 0; i < lines.length; i++) {
3722
+ const line = lines[i];
3723
+ if (line.startsWith(" ") || line.startsWith(" ")) continue;
3724
+ const m = GO_DECL_RE.exec(line);
3725
+ if (!m) continue;
3726
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
3727
+ }
3728
+ return out;
3729
+ }
3730
+ function extractRust(lines) {
3731
+ const out = [];
3732
+ for (let i = 0; i < lines.length; i++) {
3733
+ const line = lines[i];
3734
+ if (line.startsWith(" ") || line.startsWith(" ")) continue;
3735
+ const implMatch = RUST_IMPL_RE.exec(line);
3736
+ if (implMatch) {
3737
+ out.push({ line: i + 1, text: `impl ${implMatch[1]}` });
3738
+ continue;
3739
+ }
3740
+ const m = RUST_DECL_RE.exec(line);
3741
+ if (!m) continue;
3742
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
3743
+ }
3744
+ return out;
3745
+ }
3746
+ function extractMarkdown(lines) {
3747
+ const out = [];
3748
+ let inFence = false;
3749
+ for (let i = 0; i < lines.length; i++) {
3750
+ const line = lines[i];
3751
+ if (MD_FENCE_RE.test(line)) {
3752
+ inFence = !inFence;
3753
+ continue;
3754
+ }
3755
+ if (inFence) continue;
3756
+ const m = MD_HEADING_RE.exec(line);
3757
+ if (!m) continue;
3758
+ out.push({ line: i + 1, text: `${m[1]} ${m[2]}` });
3759
+ }
3760
+ return out;
3761
+ }
3762
+ function formatOutline(entries) {
3763
+ const total = entries.length;
3764
+ if (total === 0) return "";
3765
+ const lastEntry = entries[total - 1];
3766
+ const width = String(lastEntry.line).length;
3767
+ const fmt = (e) => ` L${String(e.line).padStart(width, " ")} ${e.text}`;
3768
+ const header = `[outline: ${total} symbol${total === 1 ? "" : "s"}]`;
3769
+ if (total <= OUTLINE_MAX_ENTRIES) {
3770
+ return [header, ...entries.map(fmt)].join("\n");
3771
+ }
3772
+ const headCount = OUTLINE_MAX_ENTRIES - OUTLINE_TAIL_KEEP;
3773
+ const headEntries = entries.slice(0, headCount);
3774
+ const tailEntries = entries.slice(-OUTLINE_TAIL_KEEP);
3775
+ const omitted = total - OUTLINE_MAX_ENTRIES;
3776
+ const gapStart = headEntries[headEntries.length - 1].line;
3777
+ const gapEnd = tailEntries[0].line;
3778
+ return [
3779
+ header,
3780
+ ...headEntries.map(fmt),
3781
+ ` [\u2026 ${omitted} more symbol${omitted === 1 ? "" : "s"} between L${gapStart} and L${gapEnd} \u2026]`,
3782
+ ...tailEntries.map(fmt)
3783
+ ].join("\n");
3784
+ }
3785
+
3557
3786
  // src/tools/fs/search.ts
3558
3787
  import { promises as fs2 } from "fs";
3559
- import * as pathMod2 from "path";
3788
+ import * as pathMod3 from "path";
3560
3789
  function throwIfAborted(signal) {
3561
3790
  if (!signal?.aborted) return;
3562
3791
  throw new DOMException("search aborted by user", "AbortError");
3563
3792
  }
3564
3793
  function displayRel2(rootDir, full) {
3565
- return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
3794
+ return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
3566
3795
  }
3567
3796
  async function searchFiles(ctx, startAbs, args) {
3568
3797
  throwIfAborted(args.signal);
@@ -3586,7 +3815,7 @@ async function searchFiles(ctx, startAbs, args) {
3586
3815
  }
3587
3816
  for (const e of entries) {
3588
3817
  throwIfAborted(args.signal);
3589
- const full = pathMod2.join(dir, e.name);
3818
+ const full = pathMod3.join(dir, e.name);
3590
3819
  const lower = e.name.toLowerCase();
3591
3820
  const hit = re ? re.test(e.name) : lower.includes(needle);
3592
3821
  if (hit) {
@@ -3665,11 +3894,11 @@ async function searchContent(ctx, startAbs, args) {
3665
3894
  throwIfAborted(args.signal);
3666
3895
  if (e.isDirectory()) {
3667
3896
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
3668
- await walk2(pathMod2.join(dir, e.name));
3897
+ await walk2(pathMod3.join(dir, e.name));
3669
3898
  continue;
3670
3899
  }
3671
3900
  if (!e.isFile()) continue;
3672
- const full = pathMod2.join(dir, e.name);
3901
+ const full = pathMod3.join(dir, e.name);
3673
3902
  if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel2(ctx.rootDir, full))) continue;
3674
3903
  if (ctx.isBinaryByName(e.name)) continue;
3675
3904
  let fh;
@@ -3766,47 +3995,11 @@ var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
3766
3995
  var DEFAULT_AUTO_PREVIEW_LINES = 200;
3767
3996
  var AUTO_PREVIEW_HEAD_LINES = 80;
3768
3997
  var AUTO_PREVIEW_TAIL_LINES = 40;
3769
- var OUTLINE_MAX_ENTRIES = 30;
3770
- var OUTLINE_TAIL_KEEP = 5;
3771
- var TS_EXPORT_RE = /^export\s+(?:default\s+)?(?:async\s+)?(function|class|const|let|var|interface|type|enum)\s+\*?\s*(\w+)/;
3772
- function extractTsExportOutline(lines) {
3773
- const out = [];
3774
- for (let i = 0; i < lines.length; i++) {
3775
- const line = lines[i];
3776
- if (!line.startsWith("export ")) continue;
3777
- const m = TS_EXPORT_RE.exec(line);
3778
- if (!m) continue;
3779
- out.push({ line: i + 1, kind: m[1], name: m[2] });
3780
- }
3781
- return out;
3782
- }
3783
- function formatOutline(entries) {
3784
- const total = entries.length;
3785
- if (total === 0) return "";
3786
- const lastEntry = entries[total - 1];
3787
- const width = String(lastEntry.line).length;
3788
- const fmt = (e) => ` L${String(e.line).padStart(width, " ")} export ${e.kind} ${e.name}`;
3789
- const header = `[outline: ${total} top-level export${total === 1 ? "" : "s"}]`;
3790
- if (total <= OUTLINE_MAX_ENTRIES) {
3791
- return [header, ...entries.map(fmt)].join("\n");
3792
- }
3793
- const headCount = OUTLINE_MAX_ENTRIES - OUTLINE_TAIL_KEEP;
3794
- const headEntries = entries.slice(0, headCount);
3795
- const tailEntries = entries.slice(-OUTLINE_TAIL_KEEP);
3796
- const omitted = total - OUTLINE_MAX_ENTRIES;
3797
- const gapStart = headEntries[headEntries.length - 1].line;
3798
- const gapEnd = tailEntries[0].line;
3799
- return [
3800
- header,
3801
- ...headEntries.map(fmt),
3802
- ` [\u2026 ${omitted} more export${omitted === 1 ? "" : "s"} between L${gapStart} and L${gapEnd} \u2026]`,
3803
- ...tailEntries.map(fmt)
3804
- ].join("\n");
3805
- }
3998
+ var OUTLINE_MAX_ENTRIES2 = 30;
3806
3999
  var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
3807
4000
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
3808
4001
  function displayRel3(rootDir, full) {
3809
- return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
4002
+ return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
3810
4003
  }
3811
4004
  var GLOB_METACHARS = /[*?{[]/;
3812
4005
  function compileNameFilter(filter) {
@@ -3825,16 +4018,16 @@ function isLikelyBinaryByName(name) {
3825
4018
  return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
3826
4019
  }
3827
4020
  function registerFilesystemTools(registry, opts) {
3828
- const rootDir = pathMod3.resolve(opts.rootDir);
4021
+ const rootDir = pathMod4.resolve(opts.rootDir);
3829
4022
  const allowWriting = opts.allowWriting !== false;
3830
4023
  const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
3831
4024
  const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
3832
- const normRoot = pathMod3.resolve(rootDir);
4025
+ const normRoot = pathMod4.resolve(rootDir);
3833
4026
  const sessionApproved = /* @__PURE__ */ new Set();
3834
4027
  const inflightGate = /* @__PURE__ */ new Map();
3835
4028
  function pathIsUnder(child, parent) {
3836
- const rel = pathMod3.relative(parent, child);
3837
- return rel === "" || !rel.startsWith("..") && !pathMod3.isAbsolute(rel);
4029
+ const rel = pathMod4.relative(parent, child);
4030
+ return rel === "" || !rel.startsWith("..") && !pathMod4.isAbsolute(rel);
3838
4031
  }
3839
4032
  function looksLikeAbsoluteSystemPath(raw) {
3840
4033
  if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
@@ -3850,7 +4043,7 @@ function registerFilesystemTools(registry, opts) {
3850
4043
  if (pathIsUnder(abs, dir)) return;
3851
4044
  }
3852
4045
  const stat2 = await safeLstat(abs);
3853
- const allowPrefix = stat2?.isDirectory() ? abs : pathMod3.dirname(abs);
4046
+ const allowPrefix = stat2?.isDirectory() ? abs : pathMod4.dirname(abs);
3854
4047
  let pending = inflightGate.get(allowPrefix);
3855
4048
  if (!pending) {
3856
4049
  const gate = ctx?.confirmationGate ?? pauseGate;
@@ -3878,7 +4071,7 @@ function registerFilesystemTools(registry, opts) {
3878
4071
  throw new Error("path must be a non-empty string");
3879
4072
  }
3880
4073
  if (looksLikeAbsoluteSystemPath(raw)) {
3881
- const abs = pathMod3.resolve(raw);
4074
+ const abs = pathMod4.resolve(raw);
3882
4075
  if (pathIsUnder(abs, normRoot)) return abs;
3883
4076
  await ensureOutsideSandboxAllowed(abs, intent, toolName, ctx);
3884
4077
  return abs;
@@ -3888,7 +4081,7 @@ function registerFilesystemTools(registry, opts) {
3888
4081
  normalized = normalized.slice(1);
3889
4082
  }
3890
4083
  if (normalized.length === 0) normalized = ".";
3891
- const resolved = pathMod3.resolve(rootDir, normalized);
4084
+ const resolved = pathMod4.resolve(rootDir, normalized);
3892
4085
  if (!pathIsUnder(resolved, normRoot)) {
3893
4086
  throw new Error(
3894
4087
  `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`
@@ -3910,7 +4103,7 @@ function registerFilesystemTools(registry, opts) {
3910
4103
  - head: N \u2192 first N lines (imports, public API, small configs)
3911
4104
  - tail: N \u2192 last N lines (recently-added code, log tails)
3912
4105
  - range: "A-B" \u2192 inclusive line range A..B, 1-indexed (e.g. "120-180" around an edit site)
3913
- 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 export outline (function / class / const / interface / type / enum names with line numbers, capped at ${OUTLINE_MAX_ENTRIES}) 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.`,
4106
+ 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.`,
3914
4107
  readOnly: true,
3915
4108
  stormExempt: true,
3916
4109
  parameters: {
@@ -3978,7 +4171,7 @@ ${slice.join("\n")}`;
3978
4171
  const head = lines.slice(0, AUTO_PREVIEW_HEAD_LINES).join("\n");
3979
4172
  const tail = lines.slice(totalLines - AUTO_PREVIEW_TAIL_LINES).join("\n");
3980
4173
  const omitted = totalLines - AUTO_PREVIEW_HEAD_LINES - AUTO_PREVIEW_TAIL_LINES;
3981
- const outline = formatOutline(extractTsExportOutline(lines));
4174
+ const outline = formatOutline(extractOutline(abs, lines));
3982
4175
  const parts = [
3983
4176
  `[auto-preview: head ${AUTO_PREVIEW_HEAD_LINES} + tail ${AUTO_PREVIEW_TAIL_LINES} of ${totalLines} lines]`,
3984
4177
  head
@@ -4086,7 +4279,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4086
4279
  lines.push(line);
4087
4280
  emitted++;
4088
4281
  if (e.isDirectory() && !skip) {
4089
- await walk2(pathMod3.join(dir, e.name), depth + 1);
4282
+ await walk2(pathMod4.join(dir, e.name), depth + 1);
4090
4283
  }
4091
4284
  }
4092
4285
  };
@@ -4246,7 +4439,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4246
4439
  },
4247
4440
  fn: async (args, ctx) => {
4248
4441
  const abs = await safePath(args.path, "write_file", ctx, "write");
4249
- await fs3.mkdir(pathMod3.dirname(abs), { recursive: true });
4442
+ await fs3.mkdir(pathMod4.dirname(abs), { recursive: true });
4250
4443
  await fs3.writeFile(abs, args.content, "utf8");
4251
4444
  return `wrote ${args.content.length} chars to ${displayRel3(rootDir, abs)}`;
4252
4445
  }
@@ -4332,7 +4525,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4332
4525
  fn: async (args, ctx) => {
4333
4526
  const src = await safePath(args.source, "move_file", ctx, "write");
4334
4527
  const dst = await safePath(args.destination, "move_file", ctx, "write");
4335
- await fs3.mkdir(pathMod3.dirname(dst), { recursive: true });
4528
+ await fs3.mkdir(pathMod4.dirname(dst), { recursive: true });
4336
4529
  await fs3.rename(src, dst);
4337
4530
  return `moved ${displayRel3(rootDir, src)} \u2192 ${displayRel3(rootDir, dst)}`;
4338
4531
  }
@@ -4400,7 +4593,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4400
4593
  fn: async (args, ctx) => {
4401
4594
  const src = await safePath(args.source, "copy_file", ctx);
4402
4595
  const dst = await safePath(args.destination, "copy_file", ctx, "write");
4403
- await fs3.mkdir(pathMod3.dirname(dst), { recursive: true });
4596
+ await fs3.mkdir(pathMod4.dirname(dst), { recursive: true });
4404
4597
  await fs3.cp(src, dst, { recursive: true, force: false, errorOnExist: true });
4405
4598
  return `copied ${displayRel3(rootDir, src)} \u2192 ${displayRel3(rootDir, dst)}`;
4406
4599
  }
@@ -5234,4 +5427,4 @@ export {
5234
5427
  snapshotBeforeEdits,
5235
5428
  restoreSnapshots
5236
5429
  };
5237
- //# sourceMappingURL=chunk-4YV2GBYG.js.map
5430
+ //# sourceMappingURL=chunk-IEA6JOIP.js.map