@cjhyy/code-shell 0.1.0-alpha.3 → 0.1.6

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 (157) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/dist/anthropic-IBTI4WL2.cjs +10 -0
  3. package/dist/anthropic-PRZZKHEZ.js +10 -0
  4. package/dist/arena/index.cjs +7 -6
  5. package/dist/arena/index.js +6 -5
  6. package/dist/arena/model-presets.d.ts +10 -1
  7. package/dist/{arena-ENHBHB2W.js → arena-2LTLIXOL.js} +9 -7
  8. package/dist/{arena-HHDYYVHJ.cjs → arena-ADB7DRFI.cjs} +25 -23
  9. package/dist/{chunk-EL2RL5DG.cjs → chunk-2YW4FPFG.cjs} +2 -2
  10. package/dist/chunk-3OA5G4TM.js +47 -0
  11. package/dist/{chunk-FPFTIKEJ.js → chunk-3QO25N6E.js} +131 -173
  12. package/dist/chunk-3SBSUOWZ.js +447 -0
  13. package/dist/chunk-52LGZKT3.cjs +47 -0
  14. package/dist/{chunk-KOGV6PDQ.cjs → chunk-55XGPMAP.cjs} +44 -9
  15. package/dist/{chunk-YD7XTI7T.cjs → chunk-6QVAYW7C.cjs} +116 -28
  16. package/dist/{chunk-SCDKLTN3.js → chunk-6S4BJ65N.js} +5 -4
  17. package/dist/{chunk-ZLPJJZUW.js → chunk-76HYEPDM.js} +2 -2
  18. package/dist/chunk-7BI3V74Y.cjs +447 -0
  19. package/dist/{chunk-GZBD6QRS.cjs → chunk-7EG3V4JY.cjs} +13 -12
  20. package/dist/{chunk-LMEKCQX4.js → chunk-7JVTYEEC.js} +6 -6
  21. package/dist/{chunk-SW3L7HIM.js → chunk-7QIM3BKD.js} +2 -2
  22. package/dist/{chunk-5C5HIOML.js → chunk-7Y6S3OCX.js} +109 -21
  23. package/dist/{chunk-REFCCVDM.js → chunk-ABRRONJ7.js} +2 -2
  24. package/dist/{chunk-F5BS3GUC.js → chunk-BSW22MPA.js} +39 -4
  25. package/dist/{chunk-ZRVPCXI7.js → chunk-BXCY2FSE.js} +2 -1
  26. package/dist/{chunk-Y7TSCDF6.js → chunk-BZ5TNE7L.js} +347 -6
  27. package/dist/{chunk-X2DNIMPC.cjs → chunk-DDB6QOKJ.cjs} +32 -32
  28. package/dist/{chunk-C7C7ZZY4.cjs → chunk-HEKQCI7B.cjs} +10 -10
  29. package/dist/{chunk-JHNCK7JP.cjs → chunk-IK4EWJBP.cjs} +4 -3
  30. package/dist/{chunk-3CAZIWUA.cjs → chunk-ILHE7XLT.cjs} +9 -9
  31. package/dist/{chunk-M2QI5X3X.cjs → chunk-JMR33QVF.cjs} +2 -2
  32. package/dist/{chunk-TDXBHD24.js → chunk-KAQHULYN.js} +2 -2
  33. package/dist/chunk-KE4EQOWA.js +294 -0
  34. package/dist/{chunk-YAFCN7ZW.cjs → chunk-KOVYSDHZ.cjs} +15 -15
  35. package/dist/{chunk-IJSHQGMP.cjs → chunk-KS3RYAYL.cjs} +4 -4
  36. package/dist/{chunk-DI7RDLOS.js → chunk-MD32R22I.js} +7 -4
  37. package/dist/chunk-MV7YI4WO.cjs +293 -0
  38. package/dist/chunk-MY5XHADU.cjs +216 -0
  39. package/dist/{chunk-OP57WUWQ.cjs → chunk-NUNC3VFB.cjs} +6 -6
  40. package/dist/{chunk-ID6EJUHP.js → chunk-PKGRTCCY.js} +1307 -212
  41. package/dist/{chunk-OUC2DFA7.js → chunk-QARL3IDQ.js} +3 -3
  42. package/dist/{chunk-VIPLU4RY.cjs → chunk-S754JHLR.cjs} +4 -4
  43. package/dist/{chunk-FUOKLKR2.js → chunk-SFXHZSRF.js} +1 -1
  44. package/dist/{chunk-2EUAE7AY.js → chunk-SIHHIQLO.js} +2 -2
  45. package/dist/{chunk-SK62UVS6.js → chunk-T6VYG46N.js} +1 -1
  46. package/dist/{chunk-JHXNOB5G.cjs → chunk-TNLGBJYS.cjs} +6 -6
  47. package/dist/{chunk-3QR52LL5.cjs → chunk-TUY2T5KP.cjs} +2 -2
  48. package/dist/{chunk-VWFET3MG.cjs → chunk-UXOCVJJ6.cjs} +1519 -424
  49. package/dist/{chunk-I57ZD5IQ.js → chunk-VIAGTYYW.js} +1 -1
  50. package/dist/chunk-WASS5YWB.js +215 -0
  51. package/dist/{chunk-HOXQLX5R.cjs → chunk-WZJV6DJC.cjs} +444 -103
  52. package/dist/{chunk-BUJ5KYCI.cjs → chunk-YIQBXAYU.cjs} +13 -13
  53. package/dist/{chunk-BF4LXLQV.cjs → chunk-YKIAELDZ.cjs} +132 -174
  54. package/dist/{chunk-M2SVOKC6.js → chunk-ZBPSQCPU.js} +6 -6
  55. package/dist/cli/commands/builtin/export-md.d.ts +19 -0
  56. package/dist/cli/commands/builtin/init/detect.d.ts +25 -0
  57. package/dist/cli/commands/builtin/init/index.d.ts +19 -0
  58. package/dist/cli/main.cjs +2372 -1453
  59. package/dist/cli/main.js +2502 -1583
  60. package/dist/cli/migrate-models.d.ts +45 -0
  61. package/dist/cli/onboarding.d.ts +110 -5
  62. package/dist/cli/updater.d.ts +59 -4
  63. package/dist/client-base-HTF236ZD.cjs +9 -0
  64. package/dist/client-base-QKRKCOWB.js +9 -0
  65. package/dist/{compaction-YNWCEF7B.js → compaction-6VU2T643.js} +4 -2
  66. package/dist/compaction-OKUNTCA7.cjs +33 -0
  67. package/dist/context/compaction.d.ts +35 -3
  68. package/dist/context/manager.d.ts +40 -3
  69. package/dist/context/tool-result-storage.d.ts +96 -0
  70. package/dist/devtools-T2NIZBJK.cjs +6 -0
  71. package/dist/{devtools-IRQQPIRY.js → devtools-YXWHOVKR.js} +1 -1
  72. package/dist/engine/engine.d.ts +47 -0
  73. package/dist/engine/turn-loop.d.ts +6 -0
  74. package/dist/export-md-RXDW6F5W.cjs +135 -0
  75. package/dist/export-md-XXKZANW5.js +135 -0
  76. package/dist/file-history-KYPE3ECT.cjs +7 -0
  77. package/dist/file-history-LF4F7X26.js +7 -0
  78. package/dist/index.cjs +42 -22
  79. package/dist/index.d.ts +5 -2
  80. package/dist/index.js +38 -18
  81. package/dist/llm/client-base.d.ts +1 -1
  82. package/dist/llm/model-cache.d.ts +21 -0
  83. package/dist/llm/model-fetcher.d.ts +29 -0
  84. package/dist/llm/model-pool.d.ts +13 -0
  85. package/dist/llm/provider-catalog.d.ts +30 -0
  86. package/dist/llm/provider-kinds.d.ts +21 -0
  87. package/dist/llm/types.d.ts +8 -0
  88. package/dist/logging/session-recorder.d.ts +77 -0
  89. package/dist/manager-D65VFAPX.js +7 -0
  90. package/dist/manager-TTF5VHJC.cjs +7 -0
  91. package/dist/mcp-manager-QMKWPBZ4.js +8 -0
  92. package/dist/mcp-manager-Z3OKI74O.cjs +8 -0
  93. package/dist/memory-5LWZ6FI2.cjs +9 -0
  94. package/dist/{memory-EPFDBPII.js → memory-VIJLBAMQ.js} +2 -2
  95. package/dist/model-cache-5KIYL5SL.js +13 -0
  96. package/dist/model-cache-OED3IM4R.cjs +13 -0
  97. package/dist/model-fetcher-HKGICACL.js +8 -0
  98. package/dist/model-fetcher-UR6WJOQP.cjs +8 -0
  99. package/dist/openai-JR2WEKOF.js +10 -0
  100. package/dist/openai-YBTOET4O.cjs +10 -0
  101. package/dist/{openrouter-models-2FM3H4SQ.js → openrouter-models-H56GHBHN.js} +2 -2
  102. package/dist/openrouter-models-TWUFWZ5T.cjs +15 -0
  103. package/dist/openrouter-sync-5VMGN6C2.cjs +11 -0
  104. package/dist/{openrouter-sync-SO3AWEAM.js → openrouter-sync-ALEMF5IU.js} +3 -3
  105. package/dist/product/index.cjs +17 -15
  106. package/dist/product/index.js +16 -14
  107. package/dist/protocol/server.d.ts +3 -1
  108. package/dist/protocol/types.d.ts +36 -2
  109. package/dist/run/index.cjs +17 -15
  110. package/dist/run/index.js +16 -14
  111. package/dist/services/index.d.ts +0 -1
  112. package/dist/session-manager-Q22BBMD3.js +8 -0
  113. package/dist/session-manager-XODQVL3C.cjs +8 -0
  114. package/dist/settings/schema.d.ts +267 -42
  115. package/dist/{tool-summary-RYNR4DDB.cjs → tool-summary-63DPIS7G.cjs} +4 -4
  116. package/dist/{tool-summary-2RUFL5QK.js → tool-summary-NP5CDBKG.js} +2 -2
  117. package/dist/tool-system/builtin/ask-user.d.ts +9 -1
  118. package/dist/tool-system/context.d.ts +31 -2
  119. package/dist/tool-system/executor.d.ts +4 -0
  120. package/dist/tool-system/investigation-guard.d.ts +46 -0
  121. package/dist/types.d.ts +13 -2
  122. package/dist/ui/components/AskUserPrompt.d.ts +8 -1
  123. package/dist/ui/components/DiffLine.d.ts +33 -0
  124. package/dist/ui/components/ModelManager.d.ts +34 -1
  125. package/dist/ui/components/OnboardingPrompt.d.ts +10 -1
  126. package/dist/ui/components/ProviderModelFlow.d.ts +58 -0
  127. package/dist/ui/components/UpdateBanner.d.ts +1 -0
  128. package/dist/updater-I7QBW4DP.cjs +17 -0
  129. package/dist/updater-NVME5S3Z.js +17 -0
  130. package/dist/utils/semver.d.ts +1 -0
  131. package/dist/utils/task-sanitizer.d.ts +6 -0
  132. package/package.json +2 -1
  133. package/skills-builtin/codeshell-help.md +135 -0
  134. package/dist/anthropic-P5VETAFI.cjs +0 -10
  135. package/dist/anthropic-W2GU3D76.js +0 -10
  136. package/dist/chunk-75E4BBFC.js +0 -224
  137. package/dist/chunk-FV2ARONZ.cjs +0 -224
  138. package/dist/client-base-TOVE25FA.js +0 -9
  139. package/dist/client-base-VDDIADAC.cjs +0 -9
  140. package/dist/compaction-D3KV2NWY.cjs +0 -31
  141. package/dist/devtools-WTC3NFNE.cjs +0 -6
  142. package/dist/file-history-HZWO2WQ5.js +0 -7
  143. package/dist/file-history-UQQILIY7.cjs +0 -7
  144. package/dist/manager-FWLOVEEK.cjs +0 -7
  145. package/dist/manager-GEXJLOIS.js +0 -7
  146. package/dist/mcp-manager-37GQLFGX.js +0 -8
  147. package/dist/mcp-manager-U77NEXBY.cjs +0 -8
  148. package/dist/memory-ADGOWADP.cjs +0 -9
  149. package/dist/openai-RXU5URYI.js +0 -10
  150. package/dist/openai-SQW4H2K4.cjs +0 -10
  151. package/dist/openrouter-models-PLCWZZYE.cjs +0 -15
  152. package/dist/openrouter-sync-K2WK3A2A.cjs +0 -11
  153. package/dist/services/compact.d.ts +0 -56
  154. package/dist/session-manager-HRSKENKB.cjs +0 -8
  155. package/dist/session-manager-NEKFEHF3.js +0 -8
  156. package/dist/updater-J7L7R6BY.cjs +0 -185
  157. package/dist/updater-LDBNYCAO.js +0 -185
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  ModelPool
3
- } from "./chunk-F5BS3GUC.js";
3
+ } from "./chunk-BSW22MPA.js";
4
+ import {
5
+ defaultCacheDir
6
+ } from "./chunk-3OA5G4TM.js";
4
7
  import {
5
8
  MCPManager
6
- } from "./chunk-2EUAE7AY.js";
9
+ } from "./chunk-SIHHIQLO.js";
7
10
  import {
8
11
  applySummaryCompaction,
9
12
  applyToolResultBudget,
@@ -15,48 +18,52 @@ import {
15
18
  snipCompact,
16
19
  truncateToolResult,
17
20
  windowCompact
18
- } from "./chunk-5C5HIOML.js";
21
+ } from "./chunk-7Y6S3OCX.js";
19
22
  import {
20
23
  MemoryManager
21
- } from "./chunk-I57ZD5IQ.js";
24
+ } from "./chunk-VIAGTYYW.js";
22
25
  import {
23
26
  SessionManager
24
- } from "./chunk-ZLPJJZUW.js";
27
+ } from "./chunk-76HYEPDM.js";
25
28
  import {
26
29
  SettingsManager
27
- } from "./chunk-75E4BBFC.js";
30
+ } from "./chunk-3SBSUOWZ.js";
28
31
  import {
29
32
  FileHistory
30
- } from "./chunk-SK62UVS6.js";
33
+ } from "./chunk-T6VYG46N.js";
31
34
  import {
32
35
  webFetchTool,
33
36
  webFetchToolDef,
34
37
  webSearchTool,
35
38
  webSearchToolDef
36
- } from "./chunk-OUC2DFA7.js";
39
+ } from "./chunk-QARL3IDQ.js";
37
40
  import {
38
41
  Arena,
39
42
  MODEL_PRESETS,
43
+ buildModelPool,
40
44
  createLLMClient,
41
45
  createProgressRenderer,
46
+ detectProviderFromApiKey,
42
47
  formatArenaResultForSession,
43
48
  getMaxOutputTokens
44
- } from "./chunk-Y7TSCDF6.js";
49
+ } from "./chunk-BZ5TNE7L.js";
45
50
  import {
46
51
  ConfigError,
47
52
  ContextLimitError,
48
53
  ToolExecutionError,
49
54
  ToolNotFoundError,
50
55
  ToolTimeoutError
51
- } from "./chunk-FUOKLKR2.js";
56
+ } from "./chunk-SFXHZSRF.js";
52
57
  import {
58
+ getCurrentSid,
53
59
  logger
54
- } from "./chunk-ZRVPCXI7.js";
60
+ } from "./chunk-BXCY2FSE.js";
55
61
  import {
62
+ __dirname,
56
63
  __require,
57
64
  __toCommonJS,
58
65
  init_esm_shims
59
- } from "./chunk-DI7RDLOS.js";
66
+ } from "./chunk-MD32R22I.js";
60
67
 
61
68
  // src/tool-system/builtin/task.ts
62
69
  init_esm_shims();
@@ -380,7 +387,7 @@ var fileCache = new FileStateCache();
380
387
  // src/tool-system/builtin/read.ts
381
388
  var readToolDef = {
382
389
  name: "Read",
383
- description: "Read a file from the local filesystem. Returns the file content with line numbers. By default reads up to 2000 lines from the beginning. Use offset and limit to read specific portions of large files. For large files, consider using Grep first to find the relevant lines.",
390
+ description: "Read a file from the local filesystem. Returns the file content with line numbers. By default reads up to 2000 lines from the beginning. Use offset and limit to read specific portions of large files. For large files, consider using Grep first to find the relevant lines.\n\nDo NOT re-read a file you just edited to verify \u2014 Edit/Write would have errored if the change failed.\nDo NOT re-read a file (or the same range of a file) you've already read earlier in this conversation. The content is already in your context. If you need a different part of the file, read a different offset; otherwise decide with what you have.",
384
391
  inputSchema: {
385
392
  type: "object",
386
393
  properties: {
@@ -1364,11 +1371,40 @@ ${output}`;
1364
1371
  init_esm_shims();
1365
1372
  var askUserToolDef = {
1366
1373
  name: "AskUserQuestion",
1367
- description: "Ask the user a question and wait for their response. Use this when you need clarification, confirmation, or additional input from the user. In headless mode this tool will return an error.",
1374
+ description: "Ask the user a question and wait for their response. Use this when you need clarification, confirmation, or additional input from the user. Pass `options` to present a multiple-choice list (recommended when the answer is one of a small known set \u2014 much better UX than free text). Each option needs a short `label` (the choice the user sees) and a `description` (what it means). Lead with the recommended option and append a recommendation marker to its label in the same language as the question (e.g. '(Recommended)' for an English question, '(\u63A8\u8350)' for a Chinese question). Set `multiSelect: true` when more than one choice can apply. ALWAYS write the `question`, every option `label`, every option `description`, and the `header` in the SAME LANGUAGE the user has been writing in. If the user wrote to you in Chinese, write all of these fields in Chinese; if Japanese, in Japanese; etc. The framework's UI chrome (separator hints, the implicit 'Other...' entry) is fixed \u2014 only your fields need to match the user's language. In headless mode this tool will return an error.",
1368
1375
  inputSchema: {
1369
1376
  type: "object",
1370
1377
  properties: {
1371
- question: { type: "string", description: "The question to ask the user" }
1378
+ question: {
1379
+ type: "string",
1380
+ description: "The question to ask the user \u2014 in the user's language"
1381
+ },
1382
+ header: {
1383
+ type: "string",
1384
+ description: "Optional short label (\u226412 chars) shown as a chip above the question (e.g. 'Language', 'Approach', '\u8BED\u8A00', '\u65B9\u6848'). Use the user's language."
1385
+ },
1386
+ options: {
1387
+ type: "array",
1388
+ description: "Optional 2-4 multiple-choice options. Omit for free-text input. An implicit 'Other...' entry is appended automatically by the framework.",
1389
+ items: {
1390
+ type: "object",
1391
+ properties: {
1392
+ label: {
1393
+ type: "string",
1394
+ description: "Short display text (1-5 words) \u2014 in the user's language"
1395
+ },
1396
+ description: {
1397
+ type: "string",
1398
+ description: "What this option means or implies \u2014 shown next to the label, in the user's language"
1399
+ }
1400
+ },
1401
+ required: ["label", "description"]
1402
+ }
1403
+ },
1404
+ multiSelect: {
1405
+ type: "boolean",
1406
+ description: "If true, the user can select multiple options. Defaults to false (single choice)."
1407
+ }
1372
1408
  },
1373
1409
  required: ["question"]
1374
1410
  }
@@ -1379,13 +1415,28 @@ async function askUserTool(args, ctx) {
1379
1415
  if (!ctx?.askUser) {
1380
1416
  return "Error: AskUserQuestion is not available in headless mode. Make a reasonable assumption and proceed.";
1381
1417
  }
1418
+ const opts = parseOptions(args);
1382
1419
  try {
1383
- const answer = await ctx.askUser(question);
1420
+ const answer = await ctx.askUser(question, opts);
1384
1421
  return answer || "(user provided empty response)";
1385
1422
  } catch (err) {
1386
1423
  return `Error asking user: ${err.message}`;
1387
1424
  }
1388
1425
  }
1426
+ function parseOptions(args) {
1427
+ const rawOptions = args.options;
1428
+ const header = typeof args.header === "string" ? args.header : void 0;
1429
+ const multiSelect = args.multiSelect === true;
1430
+ if (!Array.isArray(rawOptions) || rawOptions.length === 0) {
1431
+ if (header === void 0 && !multiSelect) return void 0;
1432
+ return { header, multiSelect, options: void 0 };
1433
+ }
1434
+ const options = rawOptions.filter((o) => {
1435
+ return typeof o === "object" && o !== null && typeof o.label === "string" && typeof o.description === "string";
1436
+ }).map((o) => ({ label: o.label, description: o.description }));
1437
+ if (options.length === 0) return { header, multiSelect, options: void 0 };
1438
+ return { header, multiSelect, options };
1439
+ }
1389
1440
 
1390
1441
  // src/tool-system/builtin/agent.ts
1391
1442
  init_esm_shims();
@@ -2379,8 +2430,8 @@ async function lspTool(args) {
2379
2430
  ${content}`;
2380
2431
  }
2381
2432
  case "getDiagnostics": {
2382
- const { readFileSync: readFileSync8 } = await import("fs");
2383
- const text = readFileSync8(filePath, "utf-8");
2433
+ const { readFileSync: readFileSync9 } = await import("fs");
2434
+ const text = readFileSync9(filePath, "utf-8");
2384
2435
  await client.notify("textDocument/didOpen", {
2385
2436
  textDocument: { uri, languageId: serverConfig.language, version: 1, text }
2386
2437
  });
@@ -2695,7 +2746,7 @@ async function mcpToolExecute(args) {
2695
2746
  const tool = args.tool;
2696
2747
  const toolArgs = args.arguments ?? {};
2697
2748
  try {
2698
- const { MCPManager: MCPManager2 } = await import("./mcp-manager-37GQLFGX.js");
2749
+ const { MCPManager: MCPManager2 } = await import("./mcp-manager-QMKWPBZ4.js");
2699
2750
  const manager = MCPManager2.getInstance();
2700
2751
  const result = await manager.callTool(server, tool, toolArgs);
2701
2752
  return typeof result === "string" ? result : JSON.stringify(result, null, 2);
@@ -2719,7 +2770,7 @@ var listMcpResourcesToolDef = {
2719
2770
  async function listMcpResourcesTool(args) {
2720
2771
  const server = args.server ?? "";
2721
2772
  try {
2722
- const { MCPManager: MCPManager2 } = await import("./mcp-manager-37GQLFGX.js");
2773
+ const { MCPManager: MCPManager2 } = await import("./mcp-manager-QMKWPBZ4.js");
2723
2774
  const manager = MCPManager2.getInstance();
2724
2775
  const resources = await manager.listResources(server || void 0);
2725
2776
  if (!resources || resources.length === 0) {
@@ -2754,7 +2805,7 @@ async function readMcpResourceTool(args) {
2754
2805
  const server = args.server;
2755
2806
  const uri = args.uri;
2756
2807
  try {
2757
- const { MCPManager: MCPManager2 } = await import("./mcp-manager-37GQLFGX.js");
2808
+ const { MCPManager: MCPManager2 } = await import("./mcp-manager-QMKWPBZ4.js");
2758
2809
  const manager = MCPManager2.getInstance();
2759
2810
  const content = await manager.readResource(server, uri);
2760
2811
  return typeof content === "string" ? content : JSON.stringify(content, null, 2);
@@ -2792,11 +2843,11 @@ async function remoteTriggerTool(args) {
2792
2843
  const prompt = args.prompt;
2793
2844
  const config = args.config;
2794
2845
  try {
2795
- const { existsSync: existsSync13, readFileSync: readFileSync8, writeFileSync: writeFileSync5, mkdirSync: mkdirSync3 } = await import("fs");
2796
- const { join: join10 } = await import("path");
2846
+ const { existsSync: existsSync15, readFileSync: readFileSync9, writeFileSync: writeFileSync7, mkdirSync: mkdirSync6 } = await import("fs");
2847
+ const { join: join12 } = await import("path");
2797
2848
  const { homedir: homedir8 } = await import("os");
2798
- const triggerDir = join10(homedir8(), ".code-shell", "triggers");
2799
- mkdirSync3(triggerDir, { recursive: true });
2849
+ const triggerDir = join12(homedir8(), ".code-shell", "triggers");
2850
+ mkdirSync6(triggerDir, { recursive: true });
2800
2851
  const trigger = {
2801
2852
  id: `trigger_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
2802
2853
  name,
@@ -2805,8 +2856,8 @@ async function remoteTriggerTool(args) {
2805
2856
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2806
2857
  status: "pending"
2807
2858
  };
2808
- const triggerFile = join10(triggerDir, `${trigger.id}.json`);
2809
- writeFileSync5(triggerFile, JSON.stringify(trigger, null, 2), "utf-8");
2859
+ const triggerFile = join12(triggerDir, `${trigger.id}.json`);
2860
+ writeFileSync7(triggerFile, JSON.stringify(trigger, null, 2), "utf-8");
2810
2861
  return `Remote trigger "${name}" dispatched (ID: ${trigger.id}).
2811
2862
  Stored at: ${triggerFile}`;
2812
2863
  } catch (err) {
@@ -3306,7 +3357,9 @@ var BUILTIN_TOOLS = [
3306
3357
  source: "builtin",
3307
3358
  permissionDefault: "allow",
3308
3359
  isReadOnly: true,
3309
- isConcurrencySafe: false
3360
+ isConcurrencySafe: false,
3361
+ timeoutMs: 0
3362
+ // 纯等待用户操作 — 不设超时,由用户 Esc/取消
3310
3363
  },
3311
3364
  execute: askUserTool
3312
3365
  },
@@ -3745,6 +3798,209 @@ var ToolRegistry = class {
3745
3798
  // src/tool-system/executor.ts
3746
3799
  init_esm_shims();
3747
3800
 
3801
+ // src/logging/session-recorder.ts
3802
+ init_esm_shims();
3803
+ import {
3804
+ appendFileSync,
3805
+ existsSync as existsSync8,
3806
+ mkdirSync as mkdirSync2,
3807
+ readdirSync as readdirSync3,
3808
+ rmSync,
3809
+ statSync
3810
+ } from "fs";
3811
+ import { join as join4 } from "path";
3812
+ var RETENTION_DAYS = 7;
3813
+ var MAX_RECORD_BYTES = 256 * 1024;
3814
+ function clip(s) {
3815
+ if (s === void 0) return void 0;
3816
+ if (s.length <= MAX_RECORD_BYTES) return s;
3817
+ return s.slice(0, MAX_RECORD_BYTES) + `
3818
+ \u2026(truncated, ${s.length - MAX_RECORD_BYTES} more bytes)`;
3819
+ }
3820
+ function isLocalDev() {
3821
+ if (process.env.CODE_SHELL_VERBOSE_LOG === "0") return false;
3822
+ if (process.env.CODE_SHELL_DEV === "1") return true;
3823
+ if (process.argv.includes("--debug") || process.argv.includes("-d")) return true;
3824
+ if (process.argv.some((a) => a.startsWith("--debug="))) return true;
3825
+ if (process.env.NODE_ENV === "test" || process.env.BUN_TEST === "1") return false;
3826
+ const entry = process.argv[1] ?? "";
3827
+ if (entry.includes(`${"/"}src${"/"}`)) return true;
3828
+ return false;
3829
+ }
3830
+ var ENABLED = isLocalDev();
3831
+ function resolveLogDir() {
3832
+ let dir = new URL(".", import.meta.url).pathname;
3833
+ if (dir.endsWith("/")) dir = dir.slice(0, -1);
3834
+ for (let i = 0; i < 8; i++) {
3835
+ if (existsSync8(join4(dir, "package.json"))) return join4(dir, "log");
3836
+ const parent = join4(dir, "..");
3837
+ if (parent === dir) break;
3838
+ dir = parent;
3839
+ }
3840
+ return join4(process.cwd(), "log");
3841
+ }
3842
+ var LOG_ROOT = ENABLED ? resolveLogDir() : "";
3843
+ var SENSITIVE_FLAG_RE = /^--?(api[-_]?key|auth[-_]?token|token|password|secret|bearer)(=|$)/i;
3844
+ function redactArgv(argv) {
3845
+ const out = [];
3846
+ for (let i = 0; i < argv.length; i++) {
3847
+ const a = argv[i] ?? "";
3848
+ const m = SENSITIVE_FLAG_RE.exec(a);
3849
+ if (!m) {
3850
+ out.push(a);
3851
+ continue;
3852
+ }
3853
+ if (m[2] === "=") {
3854
+ out.push(`${a.slice(0, a.indexOf("=") + 1)}<redacted>`);
3855
+ } else {
3856
+ out.push(a);
3857
+ if (i + 1 < argv.length) {
3858
+ out.push("<redacted>");
3859
+ i += 1;
3860
+ }
3861
+ }
3862
+ }
3863
+ return out;
3864
+ }
3865
+ var states = /* @__PURE__ */ new Map();
3866
+ var rotated = false;
3867
+ function todayStr() {
3868
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3869
+ }
3870
+ function rotateOnce() {
3871
+ if (rotated || !ENABLED) return;
3872
+ rotated = true;
3873
+ try {
3874
+ if (!existsSync8(LOG_ROOT)) return;
3875
+ const cutoff = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1e3;
3876
+ for (const entry of readdirSync3(LOG_ROOT)) {
3877
+ const full = join4(LOG_ROOT, entry);
3878
+ try {
3879
+ const st = statSync(full);
3880
+ if (st.isDirectory() && st.mtimeMs < cutoff) {
3881
+ rmSync(full, { recursive: true, force: true });
3882
+ }
3883
+ } catch {
3884
+ }
3885
+ }
3886
+ } catch {
3887
+ }
3888
+ }
3889
+ function ensureState(sid) {
3890
+ if (!ENABLED) return null;
3891
+ rotateOnce();
3892
+ let st = states.get(sid);
3893
+ if (st) return st;
3894
+ const dateStr = todayStr();
3895
+ const dayDir = join4(LOG_ROOT, dateStr);
3896
+ const path = join4(dayDir, `session-${sid}.jsonl`);
3897
+ try {
3898
+ if (!existsSync8(dayDir)) mkdirSync2(dayDir, { recursive: true });
3899
+ } catch {
3900
+ return null;
3901
+ }
3902
+ st = { path, startedAt: Date.now() };
3903
+ states.set(sid, st);
3904
+ return st;
3905
+ }
3906
+ function write(sid, event) {
3907
+ const st = ensureState(sid);
3908
+ if (!st) return;
3909
+ const record = { t: (/* @__PURE__ */ new Date()).toISOString(), sid, ...event };
3910
+ try {
3911
+ appendFileSync(st.path, JSON.stringify(record) + "\n", "utf-8");
3912
+ } catch {
3913
+ }
3914
+ }
3915
+ function recordSessionStart(sid, info) {
3916
+ if (!ENABLED) return;
3917
+ write(sid, {
3918
+ type: "session_start",
3919
+ pid: process.pid,
3920
+ argv: redactArgv(process.argv.slice(1)),
3921
+ cwd: info.cwd,
3922
+ model: info.model,
3923
+ provider: info.provider,
3924
+ permissionMode: info.permissionMode,
3925
+ resumed: info.resumed,
3926
+ task: info.task
3927
+ });
3928
+ }
3929
+ function recordLLMRequest(sid, req, reqId) {
3930
+ if (!ENABLED) return;
3931
+ write(sid, {
3932
+ type: "llm.request",
3933
+ reqId,
3934
+ provider: req.provider,
3935
+ model: req.model,
3936
+ stream: req.stream,
3937
+ temperature: req.temperature,
3938
+ maxTokens: req.maxTokens,
3939
+ toolCount: Array.isArray(req.tools) ? req.tools.length : 0,
3940
+ messageCount: Array.isArray(req.messages) ? req.messages.length : void 0,
3941
+ systemPrompt: req.systemPrompt,
3942
+ tools: req.tools,
3943
+ messages: req.messages
3944
+ });
3945
+ }
3946
+ function recordLLMResponse(sid, resp, reqId) {
3947
+ if (!ENABLED) return;
3948
+ write(sid, {
3949
+ type: "llm.response",
3950
+ reqId,
3951
+ stopReason: resp.stopReason,
3952
+ durationMs: resp.durationMs,
3953
+ ttftMs: resp.ttftMs,
3954
+ usage: resp.usage,
3955
+ toolCallCount: resp.toolCalls?.length ?? 0,
3956
+ text: resp.text,
3957
+ toolCalls: resp.toolCalls
3958
+ });
3959
+ }
3960
+ function recordLLMError(sid, reqId, err, durationMs) {
3961
+ if (!ENABLED) return;
3962
+ write(sid, {
3963
+ type: "llm.error",
3964
+ reqId,
3965
+ durationMs,
3966
+ message: err instanceof Error ? err.message : String(err),
3967
+ stack: err instanceof Error ? err.stack : void 0
3968
+ });
3969
+ }
3970
+ function recordToolCall(sid, call) {
3971
+ if (!ENABLED) return;
3972
+ write(sid, {
3973
+ type: "tool.call",
3974
+ toolCallId: call.id,
3975
+ toolName: call.toolName,
3976
+ args: call.args
3977
+ });
3978
+ }
3979
+ function recordToolResult(sid, result) {
3980
+ if (!ENABLED) return;
3981
+ write(sid, {
3982
+ type: "tool.result",
3983
+ toolCallId: result.id,
3984
+ toolName: result.toolName,
3985
+ ok: result.ok,
3986
+ durationMs: result.durationMs,
3987
+ chars: (result.output ?? result.error ?? "").length,
3988
+ output: clip(result.output),
3989
+ error: clip(result.error)
3990
+ });
3991
+ }
3992
+ function recordSessionEnd(sid, info) {
3993
+ if (!ENABLED) return;
3994
+ write(sid, {
3995
+ type: "session_end",
3996
+ reason: info.reason,
3997
+ turns: info.turns,
3998
+ durationMs: info.durationMs,
3999
+ cost: info.cost
4000
+ });
4001
+ states.delete(sid);
4002
+ }
4003
+
3748
4004
  // src/tool-system/validation.ts
3749
4005
  init_esm_shims();
3750
4006
  function validateToolArgs(toolName, args, schema) {
@@ -3794,6 +4050,13 @@ var ToolExecutor = class {
3794
4050
  * (tests, ad-hoc tool runs) still write log lines, just without turn tags.
3795
4051
  */
3796
4052
  log = logger;
4053
+ guard;
4054
+ setInvestigationGuard(guard) {
4055
+ this.guard = guard;
4056
+ }
4057
+ getInvestigationGuard() {
4058
+ return this.guard;
4059
+ }
3797
4060
  /** Set the abort signal for cascading cancellation. */
3798
4061
  setSignal(signal) {
3799
4062
  this.signal = signal;
@@ -3867,6 +4130,16 @@ var ToolExecutor = class {
3867
4130
  isError: true
3868
4131
  };
3869
4132
  }
4133
+ const guardDecision = this.guard?.preToolCheck(call);
4134
+ if (guardDecision?.block) {
4135
+ this.log.info("guard.block", { cat: "guard", tool: call.toolName, reason: guardDecision.block.slice(0, 200) });
4136
+ return {
4137
+ id: call.id,
4138
+ toolName: call.toolName,
4139
+ error: guardDecision.block,
4140
+ isError: true
4141
+ };
4142
+ }
3870
4143
  const decision = this.permission.classify(call.toolName, call.args);
3871
4144
  this.log.info("permission.classify", {
3872
4145
  cat: "permission",
@@ -3904,6 +4177,9 @@ var ToolExecutor = class {
3904
4177
  toolCallId: call.id,
3905
4178
  args: JSON.stringify(call.args).slice(0, 2e3)
3906
4179
  });
4180
+ const sid = getCurrentSid();
4181
+ recordToolCall(sid, { id: call.id, toolName: call.toolName, args: call.args });
4182
+ const toolStartedAt = Date.now();
3907
4183
  let result;
3908
4184
  try {
3909
4185
  result = await this.registry.executeTool(call.toolName, call.args, {
@@ -3912,9 +4188,20 @@ var ToolExecutor = class {
3912
4188
  });
3913
4189
  } catch (err) {
3914
4190
  span.fail(err);
4191
+ recordToolResult(sid, {
4192
+ id: call.id,
4193
+ toolName: call.toolName,
4194
+ ok: false,
4195
+ durationMs: Date.now() - toolStartedAt,
4196
+ error: err instanceof Error ? err.stack ?? err.message : String(err)
4197
+ });
3915
4198
  throw err;
3916
4199
  }
3917
4200
  result.id = call.id;
4201
+ if (guardDecision?.prepend && !result.error && result.result) {
4202
+ result.result = `${guardDecision.prepend}
4203
+ ${result.result}`;
4204
+ }
3918
4205
  const payload = result.result ?? result.error ?? "";
3919
4206
  span.end({
3920
4207
  ok: !result.error,
@@ -3924,6 +4211,14 @@ var ToolExecutor = class {
3924
4211
  // usually contain the message we need.
3925
4212
  ...result.error ? { errorSnippet: payload.slice(0, 500) } : {}
3926
4213
  });
4214
+ recordToolResult(sid, {
4215
+ id: call.id,
4216
+ toolName: call.toolName,
4217
+ ok: !result.error,
4218
+ durationMs: Date.now() - toolStartedAt,
4219
+ output: result.error ? void 0 : result.result,
4220
+ error: result.error
4221
+ });
3927
4222
  await this.hooks.emit("on_tool_end", {
3928
4223
  toolName: call.toolName,
3929
4224
  toolCallId: call.id,
@@ -4503,8 +4798,20 @@ var PermissionClassifier = class {
4503
4798
  const argVal = String(args[key] ?? "");
4504
4799
  if (pattern instanceof RegExp) {
4505
4800
  if (!pattern.test(argVal)) return false;
4506
- } else {
4507
- if (!argVal.includes(pattern)) return false;
4801
+ } else if (typeof pattern === "string") {
4802
+ const looksLikeRegex = /[\\^$.*+?()[\]|{}]/.test(pattern);
4803
+ if (looksLikeRegex) {
4804
+ let re;
4805
+ try {
4806
+ re = new RegExp(pattern);
4807
+ } catch {
4808
+ if (!argVal.includes(pattern)) return false;
4809
+ continue;
4810
+ }
4811
+ if (!re.test(argVal)) return false;
4812
+ } else if (!argVal.includes(pattern)) {
4813
+ return false;
4814
+ }
4508
4815
  }
4509
4816
  }
4510
4817
  }
@@ -4606,15 +4913,201 @@ var HookRegistry = class {
4606
4913
  }
4607
4914
  };
4608
4915
 
4916
+ // src/context/tool-result-storage.ts
4917
+ init_esm_shims();
4918
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
4919
+ import { dirname as dirname3, join as join5 } from "path";
4920
+ var DEFAULT_PERSIST_THRESHOLD = 5e4;
4921
+ var PER_MESSAGE_AGGREGATE_CAP = 2e5;
4922
+ var PREVIEW_SIZE = 2e3;
4923
+ var PERSISTED_OPEN = "<persisted-output>";
4924
+ var PERSISTED_CLOSE = "</persisted-output>";
4925
+ var CLEARED_PREFIX = "[Old tool result cleared";
4926
+ function createContentReplacementState() {
4927
+ return { seenIds: /* @__PURE__ */ new Set(), replacements: /* @__PURE__ */ new Map() };
4928
+ }
4929
+ function reconstructContentReplacementState(messages) {
4930
+ const state = createContentReplacementState();
4931
+ for (const msg of messages) {
4932
+ if (!Array.isArray(msg.content)) continue;
4933
+ for (const block of msg.content) {
4934
+ if (block.type !== "tool_result" || !block.tool_use_id) continue;
4935
+ state.seenIds.add(block.tool_use_id);
4936
+ if (typeof block.content === "string" && block.content.startsWith(PERSISTED_OPEN)) {
4937
+ state.replacements.set(block.tool_use_id, block.content);
4938
+ }
4939
+ }
4940
+ }
4941
+ return state;
4942
+ }
4943
+ function resolveToolResultsDir(transcriptPath) {
4944
+ return join5(dirname3(transcriptPath), "tool-results");
4945
+ }
4946
+ function ensureDir(dir) {
4947
+ try {
4948
+ mkdirSync3(dir, { recursive: true });
4949
+ } catch {
4950
+ }
4951
+ }
4952
+ function persistToFile(dir, toolUseId, content) {
4953
+ ensureDir(dir);
4954
+ const filepath = join5(dir, `${toolUseId}.txt`);
4955
+ try {
4956
+ writeFileSync3(filepath, content, { encoding: "utf-8", flag: "wx" });
4957
+ } catch (err) {
4958
+ const code = err.code;
4959
+ if (code !== "EEXIST") {
4960
+ logger.debug("tool_result.persist_failed", {
4961
+ toolUseId,
4962
+ code,
4963
+ error: err.message
4964
+ });
4965
+ throw err;
4966
+ }
4967
+ }
4968
+ return filepath;
4969
+ }
4970
+ function buildReplacement(filepath, originalSize, content) {
4971
+ const sizeKb = (originalSize / 1024).toFixed(1);
4972
+ const previewRaw = content.slice(0, PREVIEW_SIZE);
4973
+ const lastNl = previewRaw.lastIndexOf("\n");
4974
+ const preview = lastNl > PREVIEW_SIZE * 0.5 ? previewRaw.slice(0, lastNl) : previewRaw;
4975
+ const hasMore = content.length > preview.length;
4976
+ return `${PERSISTED_OPEN}
4977
+ Output too large (${sizeKb} KB). Full output saved to: ${filepath}
4978
+
4979
+ Preview (first ${preview.length} chars${hasMore ? ", truncated" : ""}):
4980
+ ` + preview + (hasMore ? "\n..." : "") + `
4981
+ ${PERSISTED_CLOSE}`;
4982
+ }
4983
+ function collectCandidates(msg) {
4984
+ if (!Array.isArray(msg.content)) return [];
4985
+ const out = [];
4986
+ for (const block of msg.content) {
4987
+ if (block.type !== "tool_result" || !block.tool_use_id) continue;
4988
+ if (typeof block.content !== "string") continue;
4989
+ if (block.content.startsWith(PERSISTED_OPEN) || block.content.startsWith(CLEARED_PREFIX))
4990
+ continue;
4991
+ out.push({
4992
+ toolUseId: block.tool_use_id,
4993
+ block,
4994
+ content: block.content,
4995
+ size: block.content.length
4996
+ });
4997
+ }
4998
+ return out;
4999
+ }
5000
+ function applyToolResultPersistence(messages, options) {
5001
+ const {
5002
+ perResultThreshold = DEFAULT_PERSIST_THRESHOLD,
5003
+ perMessageCap = PER_MESSAGE_AGGREGATE_CAP,
5004
+ toolResultsDir,
5005
+ state,
5006
+ onPersist
5007
+ } = options;
5008
+ const newReplacements = /* @__PURE__ */ new Map();
5009
+ for (const msg of messages) {
5010
+ if (msg.role !== "user") continue;
5011
+ const candidates = collectCandidates(msg);
5012
+ if (candidates.length === 0) continue;
5013
+ const fresh = [];
5014
+ for (const c of candidates) {
5015
+ if (state.replacements.has(c.toolUseId)) {
5016
+ continue;
5017
+ }
5018
+ if (state.seenIds.has(c.toolUseId)) {
5019
+ continue;
5020
+ }
5021
+ fresh.push(c);
5022
+ }
5023
+ if (fresh.length === 0) continue;
5024
+ const toPersist = /* @__PURE__ */ new Set();
5025
+ for (const c of fresh) {
5026
+ if (c.size > perResultThreshold) {
5027
+ toPersist.add(c.toolUseId);
5028
+ }
5029
+ }
5030
+ const frozenSize = candidates.filter((c) => state.seenIds.has(c.toolUseId) && !state.replacements.has(c.toolUseId)).reduce((s, c) => s + c.size, 0);
5031
+ let projected = frozenSize + fresh.reduce((s, c) => s + (toPersist.has(c.toolUseId) ? 0 : c.size), 0);
5032
+ if (projected > perMessageCap) {
5033
+ const remaining = fresh.filter((c) => !toPersist.has(c.toolUseId)).sort((a, b) => b.size - a.size);
5034
+ for (const c of remaining) {
5035
+ if (projected <= perMessageCap) break;
5036
+ toPersist.add(c.toolUseId);
5037
+ projected -= c.size;
5038
+ }
5039
+ }
5040
+ for (const c of fresh) {
5041
+ if (toPersist.has(c.toolUseId)) {
5042
+ try {
5043
+ const filepath = persistToFile(toolResultsDir, c.toolUseId, c.content);
5044
+ const replacement = buildReplacement(filepath, c.size, c.content);
5045
+ state.replacements.set(c.toolUseId, replacement);
5046
+ newReplacements.set(c.toolUseId, replacement);
5047
+ state.seenIds.add(c.toolUseId);
5048
+ onPersist?.({
5049
+ toolUseId: c.toolUseId,
5050
+ filepath,
5051
+ originalSize: c.size,
5052
+ reason: c.size > perResultThreshold ? "per-result-cap" : "per-message-budget"
5053
+ });
5054
+ } catch {
5055
+ state.seenIds.add(c.toolUseId);
5056
+ }
5057
+ } else {
5058
+ state.seenIds.add(c.toolUseId);
5059
+ }
5060
+ }
5061
+ }
5062
+ if (state.replacements.size === 0) return messages;
5063
+ let mutated = false;
5064
+ const out = messages.map((msg) => {
5065
+ if (msg.role !== "user" || !Array.isArray(msg.content)) return msg;
5066
+ let blockChanged = false;
5067
+ const newContent = msg.content.map((block) => {
5068
+ if (block.type !== "tool_result" || !block.tool_use_id) return block;
5069
+ const replacement = state.replacements.get(block.tool_use_id);
5070
+ if (replacement === void 0) return block;
5071
+ if (block.content === replacement) return block;
5072
+ if (typeof block.content === "string" && block.content.startsWith(CLEARED_PREFIX)) {
5073
+ return block;
5074
+ }
5075
+ blockChanged = true;
5076
+ return { ...block, content: replacement };
5077
+ });
5078
+ if (!blockChanged) return msg;
5079
+ mutated = true;
5080
+ return { ...msg, content: newContent };
5081
+ });
5082
+ if (mutated && newReplacements.size > 0) {
5083
+ logger.info("tool_result.persisted", {
5084
+ count: newReplacements.size,
5085
+ ids: [...newReplacements.keys()]
5086
+ });
5087
+ }
5088
+ return mutated ? out : messages;
5089
+ }
5090
+ function isPersistedReplacement(s) {
5091
+ return s.startsWith(PERSISTED_OPEN);
5092
+ }
5093
+
4609
5094
  // src/context/manager.ts
4610
5095
  init_esm_shims();
4611
5096
  init_compaction();
4612
5097
  var DEFAULT_CONFIG = {
5098
+ // 0.85 ≈ effectiveContextWindow − reserved-output buffer (CC's
5099
+ // autoCompactThreshold pattern: window − 13k buffer − 20k output budget
5100
+ // ≈ 0.83 on a 200k window, 0.97 on 1M). 0.6 was wasting 40% of any model
5101
+ // with a window over ~150k.
4613
5102
  maxTokens: 2e5,
4614
- compactAtRatio: 0.6,
4615
- summarizeAtRatio: 0.8,
4616
- maxToolResultChars: 3e4
5103
+ compactAtRatio: 0.85,
5104
+ summarizeAtRatio: 0.92,
5105
+ maxToolResultChars: 3e4,
5106
+ microcompactFloorRatio: 0.3
4617
5107
  };
5108
+ function defaultKeepRecent(maxTokens) {
5109
+ return Math.max(5, Math.floor(maxTokens / 1e5));
5110
+ }
4618
5111
  var ContextManager = class {
4619
5112
  config;
4620
5113
  toolCallHashes = /* @__PURE__ */ new Map();
@@ -4627,8 +5120,15 @@ var ContextManager = class {
4627
5120
  lastActualAtMessageCount;
4628
5121
  /** Path to session transcript — passed to summary compaction for on-demand access. */
4629
5122
  transcriptPath;
4630
- /** Notified whenever a non-trivial compaction tier fires (skips microcompact). */
5123
+ /** Notified whenever any compaction tier fires, including microcompact. */
4631
5124
  onCompact;
5125
+ /**
5126
+ * Tool-result persistence state. Created lazily once we know where to
5127
+ * write files (i.e. once setTranscriptPath has been called).
5128
+ * `null` = persistence has been explicitly disabled.
5129
+ */
5130
+ replacementState;
5131
+ toolResultsDir;
4632
5132
  constructor(config) {
4633
5133
  this.config = { ...DEFAULT_CONFIG, ...config };
4634
5134
  }
@@ -4662,10 +5162,23 @@ var ContextManager = class {
4662
5162
  this.summarizeFn = fn;
4663
5163
  }
4664
5164
  /**
4665
- * Set the transcript path so compaction can reference it.
5165
+ * Set the transcript path so compaction can reference it. The
5166
+ * tool-results directory lives alongside the transcript file:
5167
+ * <transcriptDir>/tool-results/<toolUseId>.txt
4666
5168
  */
4667
5169
  setTranscriptPath(path) {
4668
5170
  this.transcriptPath = path;
5171
+ this.toolResultsDir = resolveToolResultsDir(path);
5172
+ }
5173
+ /**
5174
+ * Initialize tool-result-persistence state from already-loaded messages.
5175
+ * Call this once per session, after setTranscriptPath, when resuming so
5176
+ * the manager re-enters the same "frozen decision" state it had on the
5177
+ * previous run. Without this, a resumed session would re-evaluate
5178
+ * every result against the current threshold and might flip choices.
5179
+ */
5180
+ initReplacementStateFromMessages(messages) {
5181
+ this.replacementState = reconstructContentReplacementState(messages);
4669
5182
  }
4670
5183
  /**
4671
5184
  * Apply progressive context management (sync path).
@@ -4673,11 +5186,36 @@ var ContextManager = class {
4673
5186
  */
4674
5187
  manage(messages) {
4675
5188
  let result = messages;
5189
+ result = this.persistLargeToolResults(result);
4676
5190
  result = this.truncateToolResults(result);
4677
5191
  result = applyToolResultBudget(result);
4678
- result = microcompact(result);
5192
+ const preTier1Tokens = this.estimateTokensHybrid(result);
5193
+ if (preTier1Tokens > this.config.maxTokens * this.config.microcompactFloorRatio) {
5194
+ const keepRecentN = this.config.microcompactKeepRecent ?? defaultKeepRecent(this.config.maxTokens);
5195
+ let clearedInfo = null;
5196
+ result = microcompact(result, {
5197
+ keepRecentN,
5198
+ onClear: (info) => {
5199
+ clearedInfo = info;
5200
+ }
5201
+ });
5202
+ if (clearedInfo) {
5203
+ const after = this.estimateTokensHybrid(result);
5204
+ logger.info("context.microcompact", {
5205
+ before: preTier1Tokens,
5206
+ after,
5207
+ keepRecentN,
5208
+ clearedRounds: clearedInfo.clearedRounds,
5209
+ toolNames: clearedInfo.toolNames
5210
+ });
5211
+ this.onCompact?.({ strategy: "micro", before: preTier1Tokens, after });
5212
+ }
5213
+ }
4679
5214
  let tokens = this.estimateTokensHybrid(result);
4680
- if (tokens > this.config.maxTokens * this.config.compactAtRatio) {
5215
+ const snipGate = this.config.maxTokens * this.config.compactAtRatio;
5216
+ const windowGate = this.config.maxTokens * (this.config.compactAtRatio + 0.05);
5217
+ const emergencyGate = this.config.maxTokens * this.config.summarizeAtRatio;
5218
+ if (tokens > snipGate) {
4681
5219
  const before = tokens;
4682
5220
  if (this.lastSummary) {
4683
5221
  const keepN = Math.max(8, Math.floor(result.length * 0.3));
@@ -4686,19 +5224,19 @@ var ContextManager = class {
4686
5224
  tokens = this.estimateTokensHybrid(result);
4687
5225
  this.onCompact?.({ strategy: "summary", before, after: tokens });
4688
5226
  } else {
4689
- const keepN = Math.max(10, Math.floor(result.length * 0.4));
4690
- result = windowCompact(result, keepN);
5227
+ result = snipCompact(result, 3, 8);
4691
5228
  tokens = this.estimateTokensHybrid(result);
4692
- this.onCompact?.({ strategy: "window", before, after: tokens });
5229
+ this.onCompact?.({ strategy: "snip", before, after: tokens });
4693
5230
  }
4694
5231
  }
4695
- if (tokens > this.config.maxTokens * 0.7) {
5232
+ if (tokens > windowGate) {
4696
5233
  const before = tokens;
4697
- result = snipCompact(result, 3, 8);
5234
+ const keepN = Math.max(10, Math.floor(result.length * 0.4));
5235
+ result = windowCompact(result, keepN);
4698
5236
  tokens = this.estimateTokensHybrid(result);
4699
- this.onCompact?.({ strategy: "snip", before, after: tokens });
5237
+ this.onCompact?.({ strategy: "window", before, after: tokens });
4700
5238
  }
4701
- if (tokens > this.config.maxTokens * this.config.summarizeAtRatio) {
5239
+ if (tokens > emergencyGate) {
4702
5240
  const before = tokens;
4703
5241
  result = windowCompact(result, 6);
4704
5242
  tokens = this.estimateTokensHybrid(result);
@@ -4712,9 +5250,31 @@ var ContextManager = class {
4712
5250
  */
4713
5251
  async manageAsync(messages) {
4714
5252
  let result = messages;
5253
+ result = this.persistLargeToolResults(result);
4715
5254
  result = this.truncateToolResults(result);
4716
5255
  result = applyToolResultBudget(result);
4717
- result = microcompact(result);
5256
+ const preTier1Tokens = this.estimateTokensHybrid(result);
5257
+ if (preTier1Tokens > this.config.maxTokens * this.config.microcompactFloorRatio) {
5258
+ const keepRecentN = this.config.microcompactKeepRecent ?? defaultKeepRecent(this.config.maxTokens);
5259
+ let clearedInfo = null;
5260
+ result = microcompact(result, {
5261
+ keepRecentN,
5262
+ onClear: (info) => {
5263
+ clearedInfo = info;
5264
+ }
5265
+ });
5266
+ if (clearedInfo) {
5267
+ const after = this.estimateTokensHybrid(result);
5268
+ logger.info("context.microcompact", {
5269
+ before: preTier1Tokens,
5270
+ after,
5271
+ keepRecentN,
5272
+ clearedRounds: clearedInfo.clearedRounds,
5273
+ toolNames: clearedInfo.toolNames
5274
+ });
5275
+ this.onCompact?.({ strategy: "micro", before: preTier1Tokens, after });
5276
+ }
5277
+ }
4718
5278
  const tokens = this.estimateTokensHybrid(result);
4719
5279
  const ratio = tokens / this.config.maxTokens;
4720
5280
  if (ratio >= this.config.compactAtRatio && this.summarizeFn && this.consecutiveSummaryFailures < 3) {
@@ -4747,20 +5307,23 @@ var ContextManager = class {
4747
5307
  }
4748
5308
  }
4749
5309
  let live = tokens;
4750
- if (live > this.config.maxTokens * this.config.compactAtRatio) {
5310
+ const snipGate = this.config.maxTokens * this.config.compactAtRatio;
5311
+ const windowGate = this.config.maxTokens * (this.config.compactAtRatio + 0.05);
5312
+ const emergencyGate = this.config.maxTokens * this.config.summarizeAtRatio;
5313
+ if (live > snipGate) {
4751
5314
  const before = live;
4752
- const keepN = Math.max(10, Math.floor(result.length * 0.4));
4753
- result = windowCompact(result, keepN);
5315
+ result = snipCompact(result, 3, 8);
4754
5316
  live = this.estimateTokensHybrid(result);
4755
- this.onCompact?.({ strategy: "window", before, after: live });
5317
+ this.onCompact?.({ strategy: "snip", before, after: live });
4756
5318
  }
4757
- if (live > this.config.maxTokens * 0.7) {
5319
+ if (live > windowGate) {
4758
5320
  const before = live;
4759
- result = snipCompact(result, 3, 8);
5321
+ const keepN = Math.max(10, Math.floor(result.length * 0.4));
5322
+ result = windowCompact(result, keepN);
4760
5323
  live = this.estimateTokensHybrid(result);
4761
- this.onCompact?.({ strategy: "snip", before, after: live });
5324
+ this.onCompact?.({ strategy: "window", before, after: live });
4762
5325
  }
4763
- if (live > this.config.maxTokens * this.config.summarizeAtRatio) {
5326
+ if (live > emergencyGate) {
4764
5327
  const before = live;
4765
5328
  result = windowCompact(result, 6);
4766
5329
  live = this.estimateTokensHybrid(result);
@@ -4768,6 +5331,24 @@ var ContextManager = class {
4768
5331
  }
4769
5332
  return result;
4770
5333
  }
5334
+ /**
5335
+ * Persist large tool_result blocks to disk and replace them with a
5336
+ * preview + filepath. No-op when transcript path hasn't been set
5337
+ * (e.g. unit tests that exercise compaction directly without a session).
5338
+ */
5339
+ persistLargeToolResults(messages) {
5340
+ if (!this.toolResultsDir) return messages;
5341
+ if (!this.replacementState) {
5342
+ this.replacementState = createContentReplacementState();
5343
+ }
5344
+ return applyToolResultPersistence(messages, {
5345
+ toolResultsDir: this.toolResultsDir,
5346
+ state: this.replacementState,
5347
+ onPersist: (info) => {
5348
+ logger.info("context.tool_result_persisted", info);
5349
+ }
5350
+ });
5351
+ }
4771
5352
  /**
4772
5353
  * Truncate oversized tool results in messages.
4773
5354
  */
@@ -4797,7 +5378,7 @@ var ContextManager = class {
4797
5378
  shouldReactiveCompact(messages, currentResponseTokens) {
4798
5379
  const msgTokens = this.estimateTokensHybrid(messages);
4799
5380
  const total = msgTokens + currentResponseTokens;
4800
- return total > this.config.maxTokens * 0.9;
5381
+ return total > this.config.maxTokens * this.config.summarizeAtRatio;
4801
5382
  }
4802
5383
  /**
4803
5384
  * Check if context is approaching limits.
@@ -4879,8 +5460,8 @@ var SectionCache = class {
4879
5460
 
4880
5461
  // src/prompt/instruction-scanner.ts
4881
5462
  init_esm_shims();
4882
- import { readFileSync as readFileSync4, existsSync as existsSync8, readdirSync as readdirSync3, statSync } from "fs";
4883
- import { join as join4, dirname as dirname3, resolve as resolve4 } from "path";
5463
+ import { readFileSync as readFileSync4, existsSync as existsSync9, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
5464
+ import { join as join6, dirname as dirname4, resolve as resolve4 } from "path";
4884
5465
  import { execSync as execSync5 } from "child_process";
4885
5466
  import { homedir as homedir2 } from "os";
4886
5467
  function scanInstructions(cwd, options = {}) {
@@ -4889,32 +5470,32 @@ function scanInstructions(cwd, options = {}) {
4889
5470
  const allNames = [fileName, ...compatNames];
4890
5471
  const configDirs = [".codeshell", ...options.scanDirs ?? [], ".claude"];
4891
5472
  const entries = [];
4892
- const homeConfigDir = join4(homedir2(), ".code-shell");
4893
- const homeCompatDir = join4(homedir2(), ".claude");
5473
+ const homeConfigDir = join6(homedir2(), ".code-shell");
5474
+ const homeCompatDir = join6(homedir2(), ".claude");
4894
5475
  for (const name of allNames) {
4895
- tryAddFile(join4(homeConfigDir, name), "user", -1, entries);
5476
+ tryAddFile(join6(homeConfigDir, name), "user", -1, entries);
4896
5477
  }
4897
- tryAddRulesDir(join4(homeConfigDir, "rules"), "user", -1, entries);
5478
+ tryAddRulesDir(join6(homeConfigDir, "rules"), "user", -1, entries);
4898
5479
  for (const name of compatNames) {
4899
- tryAddFile(join4(homeCompatDir, name), "user", -1, entries);
5480
+ tryAddFile(join6(homeCompatDir, name), "user", -1, entries);
4900
5481
  }
4901
- tryAddRulesDir(join4(homeCompatDir, "rules"), "user", -1, entries);
5482
+ tryAddRulesDir(join6(homeCompatDir, "rules"), "user", -1, entries);
4902
5483
  const ceiling = options.ignoreGitBoundary ? "/" : findGitRoot2(cwd) ?? "/";
4903
5484
  const dirs = collectDirsDownward(resolve4(cwd), ceiling);
4904
5485
  for (let depth = 0; depth < dirs.length; depth++) {
4905
5486
  const dir = dirs[depth];
4906
5487
  for (const name of allNames) {
4907
- tryAddFile(join4(dir, name), "project", depth, entries);
5488
+ tryAddFile(join6(dir, name), "project", depth, entries);
4908
5489
  }
4909
5490
  for (const cfgDir of configDirs) {
4910
5491
  for (const name of allNames) {
4911
- tryAddFile(join4(dir, cfgDir, name), "project", depth, entries);
5492
+ tryAddFile(join6(dir, cfgDir, name), "project", depth, entries);
4912
5493
  }
4913
- tryAddRulesDir(join4(dir, cfgDir, "rules"), "project", depth, entries);
5494
+ tryAddRulesDir(join6(dir, cfgDir, "rules"), "project", depth, entries);
4914
5495
  }
4915
5496
  for (const name of allNames) {
4916
5497
  const localName = name.replace(/\.md$/, ".local.md");
4917
- tryAddFile(join4(dir, localName), "local", depth, entries);
5498
+ tryAddFile(join6(dir, localName), "local", depth, entries);
4918
5499
  }
4919
5500
  }
4920
5501
  return dedup(entries);
@@ -4929,9 +5510,9 @@ ${e.content}`;
4929
5510
  }).join("\n\n---\n\n");
4930
5511
  }
4931
5512
  function tryAddFile(path, source, depth, entries) {
4932
- if (!existsSync8(path)) return;
5513
+ if (!existsSync9(path)) return;
4933
5514
  try {
4934
- const stat5 = statSync(path);
5515
+ const stat5 = statSync2(path);
4935
5516
  if (!stat5.isFile()) return;
4936
5517
  const content = readFileSync4(path, "utf-8").trim();
4937
5518
  if (content) {
@@ -4941,11 +5522,11 @@ function tryAddFile(path, source, depth, entries) {
4941
5522
  }
4942
5523
  }
4943
5524
  function tryAddRulesDir(dir, source, depth, entries) {
4944
- if (!existsSync8(dir)) return;
5525
+ if (!existsSync9(dir)) return;
4945
5526
  try {
4946
- const files = readdirSync3(dir).filter((f) => f.endsWith(".md")).sort();
5527
+ const files = readdirSync4(dir).filter((f) => f.endsWith(".md")).sort();
4947
5528
  for (const file of files) {
4948
- tryAddFile(join4(dir, file), source, depth, entries);
5529
+ tryAddFile(join6(dir, file), source, depth, entries);
4949
5530
  }
4950
5531
  } catch {
4951
5532
  }
@@ -4970,7 +5551,7 @@ function collectDirsDownward(target, ceiling) {
4970
5551
  while (true) {
4971
5552
  dirs.push(dir);
4972
5553
  if (dir === resolvedCeiling) break;
4973
- const parent = dirname3(dir);
5554
+ const parent = dirname4(dir);
4974
5555
  if (parent === dir) break;
4975
5556
  dir = parent;
4976
5557
  }
@@ -4999,28 +5580,33 @@ function sourceLabel(entry) {
4999
5580
 
5000
5581
  // src/skills/scanner.ts
5001
5582
  init_esm_shims();
5002
- import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
5003
- import { join as join5, resolve as resolve5 } from "path";
5583
+ import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
5584
+ import { dirname as dirname5, join as join7, resolve as resolve5 } from "path";
5004
5585
  import { homedir as homedir3 } from "os";
5586
+ import { fileURLToPath } from "url";
5005
5587
  function scanSkills(cwd) {
5006
5588
  const skills = [];
5007
5589
  const seen = /* @__PURE__ */ new Set();
5590
+ const builtin = resolveBuiltinSkillsDir();
5008
5591
  const dirs = [
5009
5592
  // Project-level
5010
- join5(cwd, ".code-shell", "skills"),
5011
- join5(cwd, ".claude", "skills"),
5593
+ join7(cwd, ".code-shell", "skills"),
5594
+ join7(cwd, ".claude", "skills"),
5012
5595
  // User-level
5013
- join5(homedir3(), ".code-shell", "skills"),
5014
- join5(homedir3(), ".claude", "skills")
5596
+ join7(homedir3(), ".code-shell", "skills"),
5597
+ join7(homedir3(), ".claude", "skills"),
5598
+ // Built-in (shipped inside the npm package). Listed last so user/project
5599
+ // skills with the same filename win deduplication on filePath.
5600
+ ...builtin ? [builtin] : []
5015
5601
  ];
5016
5602
  for (const dir of dirs) {
5017
- if (!existsSync9(dir)) continue;
5603
+ if (!existsSync10(dir)) continue;
5018
5604
  try {
5019
- const files = readdirSync4(dir).filter(
5020
- (f) => f.endsWith(".md") && statSync2(join5(dir, f)).isFile()
5605
+ const files = readdirSync5(dir).filter(
5606
+ (f) => f.endsWith(".md") && statSync3(join7(dir, f)).isFile()
5021
5607
  );
5022
5608
  for (const file of files) {
5023
- const filePath = resolve5(join5(dir, file));
5609
+ const filePath = resolve5(join7(dir, file));
5024
5610
  if (seen.has(filePath)) continue;
5025
5611
  seen.add(filePath);
5026
5612
  const skill = parseSkillFile(filePath);
@@ -5031,6 +5617,30 @@ function scanSkills(cwd) {
5031
5617
  }
5032
5618
  return skills;
5033
5619
  }
5620
+ function resolveBuiltinSkillsDir() {
5621
+ let here;
5622
+ try {
5623
+ here = typeof import.meta !== "undefined" && import.meta.url ? dirname5(fileURLToPath(import.meta.url)) : __dirname;
5624
+ } catch {
5625
+ return null;
5626
+ }
5627
+ let dir = here;
5628
+ for (let i = 0; i < 6; i++) {
5629
+ const pkgPath = join7(dir, "package.json");
5630
+ try {
5631
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
5632
+ if (pkg?.name === "@cjhyy/code-shell") {
5633
+ const candidate = join7(dir, "skills-builtin");
5634
+ return existsSync10(candidate) ? candidate : null;
5635
+ }
5636
+ } catch {
5637
+ }
5638
+ const parent = dirname5(dir);
5639
+ if (parent === dir) break;
5640
+ dir = parent;
5641
+ }
5642
+ return null;
5643
+ }
5034
5644
  function parseSkillFile(filePath) {
5035
5645
  try {
5036
5646
  const raw = readFileSync5(filePath, "utf-8");
@@ -5137,6 +5747,13 @@ The default job in this preset is software engineering in the current working di
5137
5747
  - Do not propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first. Understand existing code before suggesting modifications.
5138
5748
  - Read code before editing it, prefer modifying existing files, and avoid unrelated refactors.
5139
5749
 
5750
+ ## Investigation has a budget \u2014 looking is not acting
5751
+ - Read, search, and investigate freely, but treat read-only exploration as a means to action, not an end in itself. After about 3 read-only tool calls (Read/Grep/Glob) on the same question, you should either (a) make a change, (b) run a command with side effects (Bash, debug log, repro), or (c) ask the user a specific question. Continuing to read more files usually means your current strategy isn't working \u2014 change strategy instead.
5752
+ - Never re-read a file (or the same line range of a file) you've already read in this conversation. The content is already in your context. If you've re-derived the same conclusion twice from the same code, the answer isn't in static reading \u2014 switch to runtime verification.
5753
+ - For bug reports where static analysis hasn't pinpointed the cause in 2-3 reads, the right next step is almost always to add a debug log line and ask the user to reproduce, or to inspect runtime state \u2014 not to read more files.
5754
+ - Don't narrate the same code path twice in your reasoning. If you've already traced "input X \u2192 parser Y \u2192 handler Z" once and concluded "this looks correct", stop. Do not re-trace it from a different angle hoping for a different answer. Trust your first analysis and move to verification.
5755
+ - These rules are enforced at runtime by an investigation guard. A second read of the same target prepends a \`<system-reminder>\` to that tool's result; a third read of the same target is hard-blocked with an error. Four or more consecutive read-only calls without a side-effecting action also surface a reminder, and several consecutive turns of read-only work with no text update to the user surface another. When you see one of these reminders, treat it as a signal that your current strategy isn't producing new information \u2014 switch to a side-effecting verification (Bash, debug log, repro, edit) or ask the user a specific question rather than reading more.
5756
+
5140
5757
  ## Code quality discipline
5141
5758
  - Don't add features, refactor code, or make "improvements" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident.
5142
5759
  - Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code.
@@ -5158,6 +5775,7 @@ The default job in this preset is software engineering in the current working di
5158
5775
  // src/prompt/sections/tone.md
5159
5776
  var tone_default = `# Tone and output
5160
5777
  - Only use emojis if the user explicitly requests it.
5778
+ - Match the language of the user's latest message in your reply; keep code, paths, and identifiers in their original form.
5161
5779
  - Be concise, direct, and collaborative. Your responses should be short.
5162
5780
  - Do not use a colon before tool calls. Your tool calls may not be shown directly in the output, so text like "Let me read the file:" followed by a read tool call should just be "Let me read the file." with a period.
5163
5781
  - Lead with the answer or action, not the reasoning. Skip filler words, preamble, and unnecessary transitions. Do not restate what the user said.
@@ -5480,6 +6098,124 @@ ${toolLines.join("\n\n")}`;
5480
6098
  // src/engine/engine.ts
5481
6099
  init_esm_shims();
5482
6100
 
6101
+ // src/tool-system/investigation-guard.ts
6102
+ init_esm_shims();
6103
+ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["Read", "Grep", "Glob", "WebFetch", "WebSearch", "ToolSearch"]);
6104
+ var READ_BUDGET = 3;
6105
+ var SILENT_TURN_BUDGET = 3;
6106
+ var DEDUPE_OFFSET_BUCKET = 50;
6107
+ var READ_HISTORY_MAX = 512;
6108
+ var InvestigationGuard = class {
6109
+ readHistory = /* @__PURE__ */ new Map();
6110
+ consecutiveReads = 0;
6111
+ silentTurns = 0;
6112
+ turnHasText = false;
6113
+ turnHasSideEffect = false;
6114
+ lastReminderSilent = -1;
6115
+ /**
6116
+ * Soft mode replaces hard blocks (3rd-read dedupe) with a stronger
6117
+ * reminder. Used in headless runs (`code-shell run …`) where there is
6118
+ * no human to retry the task, so an unattended automation should not
6119
+ * be killed by the guard — it should still get the signal to change
6120
+ * strategy, but the tool call goes through.
6121
+ */
6122
+ softMode = false;
6123
+ setSoftMode(soft) {
6124
+ this.softMode = soft;
6125
+ }
6126
+ preToolCheck(call) {
6127
+ const tool = call.toolName;
6128
+ if (!READ_ONLY_TOOLS.has(tool)) {
6129
+ if (this.isMutatingTool(tool)) {
6130
+ this.consecutiveReads = 0;
6131
+ this.turnHasSideEffect = true;
6132
+ }
6133
+ return void 0;
6134
+ }
6135
+ const key = this.buildKey(call);
6136
+ if (!key) return void 0;
6137
+ const hits = (this.readHistory.get(key.signature) ?? 0) + 1;
6138
+ this.readHistory.set(key.signature, hits);
6139
+ if (this.readHistory.size > READ_HISTORY_MAX) {
6140
+ const oldest = this.readHistory.keys().next().value;
6141
+ if (oldest !== void 0) this.readHistory.delete(oldest);
6142
+ }
6143
+ this.consecutiveReads += 1;
6144
+ const prependParts = [];
6145
+ if (hits >= 3) {
6146
+ const message = `Investigation guard: ${tool} on this exact target has been called ${hits} times in this session. Re-reading the same content will not produce new information. Switch strategy: run a command with side effects (Bash, debug log, repro), make a code change, or ask the user a specific question. If you genuinely need this content, summarize what you already know about it from prior reads instead of re-fetching.`;
6147
+ if (this.softMode) {
6148
+ prependParts.push(`<system-reminder>${message}</system-reminder>`);
6149
+ } else {
6150
+ return { block: message };
6151
+ }
6152
+ }
6153
+ if (hits === 2) {
6154
+ prependParts.push(
6155
+ `<system-reminder>Investigation guard: you already read this target once in this session. Re-reading rarely yields new information. After this result, change strategy (Bash side-effect, edit, or ask user) rather than reading further.</system-reminder>`
6156
+ );
6157
+ }
6158
+ if (this.consecutiveReads > READ_BUDGET) {
6159
+ prependParts.push(
6160
+ `<system-reminder>Investigation guard: ${this.consecutiveReads} consecutive read-only calls with no action taken. Per coding.md, change strategy now \u2014 make a code change, run a command with side effects, or ask the user.</system-reminder>`
6161
+ );
6162
+ }
6163
+ return prependParts.length ? { prepend: prependParts.join("\n") } : void 0;
6164
+ }
6165
+ noteText(text) {
6166
+ if (text && text.trim().length > 0) this.turnHasText = true;
6167
+ }
6168
+ noteToolResult(_call, _result) {
6169
+ }
6170
+ turnEnded(turnNumber) {
6171
+ if (!this.turnHasText && !this.turnHasSideEffect) {
6172
+ this.silentTurns += 1;
6173
+ } else {
6174
+ this.silentTurns = 0;
6175
+ }
6176
+ const wasSilent = this.silentTurns;
6177
+ this.turnHasText = false;
6178
+ this.turnHasSideEffect = false;
6179
+ if (wasSilent >= SILENT_TURN_BUDGET && turnNumber !== this.lastReminderSilent) {
6180
+ this.lastReminderSilent = turnNumber;
6181
+ return `<system-reminder>Investigation guard: you have taken ${wasSilent} consecutive turns of read-only investigation without any text update to the user or side-effecting action. Either surface a status update (what have you ruled out, what's your current hypothesis?), or change tactic \u2014 verify at runtime (logs, repro, Bash) instead of reading further.</system-reminder>`;
6182
+ }
6183
+ return void 0;
6184
+ }
6185
+ isMutatingTool(name) {
6186
+ if (name === "Bash") return true;
6187
+ if (name === "Edit" || name === "Write" || name === "NotebookEdit") return true;
6188
+ if (name === "AskUserQuestion") return true;
6189
+ if (name === "TaskCreate" || name === "TaskUpdate") return false;
6190
+ return false;
6191
+ }
6192
+ buildKey(call) {
6193
+ const tool = call.toolName;
6194
+ const args = call.args ?? {};
6195
+ if (tool === "Read") {
6196
+ const fp = typeof args.file_path === "string" ? args.file_path : "";
6197
+ if (!fp) return void 0;
6198
+ const off = typeof args.offset === "number" ? args.offset : 0;
6199
+ const bucket = Math.floor(off / DEDUPE_OFFSET_BUCKET);
6200
+ return { tool, signature: `Read::${fp}::${bucket}` };
6201
+ }
6202
+ if (tool === "Grep") {
6203
+ const pat = typeof args.pattern === "string" ? args.pattern : "";
6204
+ const path = typeof args.path === "string" ? args.path : "";
6205
+ const glob2 = typeof args.glob === "string" ? args.glob : "";
6206
+ if (!pat) return void 0;
6207
+ return { tool, signature: `Grep::${pat}::${path}::${glob2}` };
6208
+ }
6209
+ if (tool === "Glob") {
6210
+ const pat = typeof args.pattern === "string" ? args.pattern : "";
6211
+ const path = typeof args.path === "string" ? args.path : "";
6212
+ if (!pat) return void 0;
6213
+ return { tool, signature: `Glob::${pat}::${path}` };
6214
+ }
6215
+ return void 0;
6216
+ }
6217
+ };
6218
+
5483
6219
  // src/engine/model-facade.ts
5484
6220
  init_esm_shims();
5485
6221
 
@@ -5521,6 +6257,11 @@ function addOutputTokens(n) {
5521
6257
  }
5522
6258
 
5523
6259
  // src/engine/model-facade.ts
6260
+ var _reqSeq = 0;
6261
+ function nextReqId() {
6262
+ _reqSeq += 1;
6263
+ return `r${_reqSeq.toString(36)}`;
6264
+ }
5524
6265
  var ModelFacade = class {
5525
6266
  constructor(client, transcript) {
5526
6267
  this.client = client;
@@ -5529,34 +6270,54 @@ var ModelFacade = class {
5529
6270
  async call(systemPrompt, messages, tools, onStream, signal) {
5530
6271
  const startMs = Date.now();
5531
6272
  const msgCount = messages.length;
5532
- const response = await this.client.createMessage({
5533
- systemPrompt,
5534
- messages,
5535
- tools,
5536
- stream: true,
5537
- onChunk: (chunk) => {
5538
- if (!onStream) return;
5539
- if (chunk.type === "text" && chunk.text) {
5540
- onStream({ type: "text_delta", text: chunk.text });
5541
- } else if (chunk.type === "tool_use_start" && chunk.toolCall) {
5542
- onStream({
5543
- type: "tool_use_start",
5544
- toolCall: {
5545
- id: chunk.toolCall.id ?? "",
5546
- toolName: chunk.toolCall.toolName ?? "",
5547
- args: chunk.toolCall.args ?? {}
5548
- }
5549
- });
5550
- } else if (chunk.type === "tool_use_delta" && chunk.toolCall?.id) {
5551
- onStream({
5552
- type: "tool_use_args_delta",
5553
- toolCallId: chunk.toolCall.id,
5554
- args: chunk.toolCall.args ?? {}
5555
- });
5556
- }
6273
+ const sid = getCurrentSid();
6274
+ const reqId = nextReqId();
6275
+ recordLLMRequest(
6276
+ sid,
6277
+ {
6278
+ provider: this.client.provider ?? "?",
6279
+ model: this.client.model ?? "?",
6280
+ stream: true,
6281
+ messages,
6282
+ tools,
6283
+ systemPrompt
5557
6284
  },
5558
- signal
5559
- });
6285
+ reqId
6286
+ );
6287
+ let response;
6288
+ try {
6289
+ response = await this.client.createMessage({
6290
+ systemPrompt,
6291
+ messages,
6292
+ tools,
6293
+ stream: true,
6294
+ onChunk: (chunk) => {
6295
+ if (!onStream) return;
6296
+ if (chunk.type === "text" && chunk.text) {
6297
+ onStream({ type: "text_delta", text: chunk.text });
6298
+ } else if (chunk.type === "tool_use_start" && chunk.toolCall) {
6299
+ onStream({
6300
+ type: "tool_use_start",
6301
+ toolCall: {
6302
+ id: chunk.toolCall.id ?? "",
6303
+ toolName: chunk.toolCall.toolName ?? "",
6304
+ args: chunk.toolCall.args ?? {}
6305
+ }
6306
+ });
6307
+ } else if (chunk.type === "tool_use_delta" && chunk.toolCall?.id) {
6308
+ onStream({
6309
+ type: "tool_use_args_delta",
6310
+ toolCallId: chunk.toolCall.id,
6311
+ args: chunk.toolCall.args ?? {}
6312
+ });
6313
+ }
6314
+ },
6315
+ signal
6316
+ });
6317
+ } catch (err) {
6318
+ recordLLMError(sid, reqId, err, Date.now() - startMs);
6319
+ throw err;
6320
+ }
5560
6321
  const latencyMs = Date.now() - startMs;
5561
6322
  logger.info("llm.request", {
5562
6323
  stream: true,
@@ -5567,6 +6328,17 @@ var ModelFacade = class {
5567
6328
  textLen: response.text?.length ?? 0,
5568
6329
  usage: response.usage
5569
6330
  });
6331
+ recordLLMResponse(
6332
+ sid,
6333
+ {
6334
+ text: response.text,
6335
+ toolCalls: response.toolCalls,
6336
+ stopReason: response.stopReason,
6337
+ usage: response.usage,
6338
+ durationMs: latencyMs
6339
+ },
6340
+ reqId
6341
+ );
5570
6342
  this.recordUsage(response, latencyMs);
5571
6343
  this.recordResponse(response);
5572
6344
  return response;
@@ -5577,13 +6349,33 @@ var ModelFacade = class {
5577
6349
  async callWithoutStreaming(systemPrompt, messages, tools, signal) {
5578
6350
  const startMs = Date.now();
5579
6351
  const msgCount = messages.length;
5580
- const response = await this.client.createMessage({
5581
- systemPrompt,
5582
- messages,
5583
- tools,
5584
- stream: false,
5585
- signal
5586
- });
6352
+ const sid = getCurrentSid();
6353
+ const reqId = nextReqId();
6354
+ recordLLMRequest(
6355
+ sid,
6356
+ {
6357
+ provider: this.client.provider ?? "?",
6358
+ model: this.client.model ?? "?",
6359
+ stream: false,
6360
+ messages,
6361
+ tools,
6362
+ systemPrompt
6363
+ },
6364
+ reqId
6365
+ );
6366
+ let response;
6367
+ try {
6368
+ response = await this.client.createMessage({
6369
+ systemPrompt,
6370
+ messages,
6371
+ tools,
6372
+ stream: false,
6373
+ signal
6374
+ });
6375
+ } catch (err) {
6376
+ recordLLMError(sid, reqId, err, Date.now() - startMs);
6377
+ throw err;
6378
+ }
5587
6379
  const latencyMs = Date.now() - startMs;
5588
6380
  logger.info("llm.request", {
5589
6381
  stream: false,
@@ -5594,6 +6386,17 @@ var ModelFacade = class {
5594
6386
  textLen: response.text?.length ?? 0,
5595
6387
  usage: response.usage
5596
6388
  });
6389
+ recordLLMResponse(
6390
+ sid,
6391
+ {
6392
+ text: response.text,
6393
+ toolCalls: response.toolCalls,
6394
+ stopReason: response.stopReason,
6395
+ usage: response.usage,
6396
+ durationMs: latencyMs
6397
+ },
6398
+ reqId
6399
+ );
5597
6400
  this.recordUsage(response, latencyMs);
5598
6401
  this.recordResponse(response);
5599
6402
  return response;
@@ -5781,7 +6584,7 @@ var TurnLoop = class {
5781
6584
  response = await this.callModelWithFallback(messages);
5782
6585
  } catch (err) {
5783
6586
  if (err instanceof ContextLimitError) {
5784
- const { dropOldestRounds } = await import("./compaction-YNWCEF7B.js");
6587
+ const { dropOldestRounds } = await import("./compaction-6VU2T643.js");
5785
6588
  let recovered = false;
5786
6589
  for (let retry = 1; retry <= 3; retry++) {
5787
6590
  tlog.warn("turn.ptl_recovery", { cat: "turn", retry, roundsToDrop: retry });
@@ -5811,6 +6614,12 @@ var TurnLoop = class {
5811
6614
  return { text: finalText, reason: "model_error", messages };
5812
6615
  }
5813
6616
  }
6617
+ if (response.usage?.promptTokens !== void 0) {
6618
+ this.config.onStream?.({
6619
+ type: "usage_update",
6620
+ promptTokens: response.usage.promptTokens
6621
+ });
6622
+ }
5814
6623
  if (response.usage?.promptTokens !== void 0) {
5815
6624
  this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length);
5816
6625
  }
@@ -5845,6 +6654,12 @@ var TurnLoop = class {
5845
6654
  }
5846
6655
  response = { ...response, text: combinedText };
5847
6656
  }
6657
+ if (response.usage?.promptTokens !== void 0) {
6658
+ this.config.onStream?.({
6659
+ type: "usage_update",
6660
+ promptTokens: response.usage.promptTokens
6661
+ });
6662
+ }
5848
6663
  if (this.config.signal?.aborted) {
5849
6664
  return { text: finalText, reason: "aborted_streaming", messages };
5850
6665
  }
@@ -5903,7 +6718,7 @@ var TurnLoop = class {
5903
6718
  this.config.onStream?.({ type: "tool_result", result });
5904
6719
  }
5905
6720
  if (this.config.onStream) {
5906
- import("./tool-summary-2RUFL5QK.js").then(({ generateToolUseSummary }) => {
6721
+ import("./tool-summary-NP5CDBKG.js").then(({ generateToolUseSummary }) => {
5907
6722
  if (!this.deps.model.summarize) return;
5908
6723
  generateToolUseSummary(toolCalls, results, this.deps.model.summarize).then((summary) => {
5909
6724
  if (summary) {
@@ -5938,12 +6753,22 @@ var TurnLoop = class {
5938
6753
  content: "<system-reminder>You are approaching the token budget limit. Please start wrapping up your work and provide a summary.</system-reminder>"
5939
6754
  });
5940
6755
  }
6756
+ const guard = this.deps.toolExecutor.getInvestigationGuard();
6757
+ if (guard) {
6758
+ guard.noteText(response.text);
6759
+ const turnReminder = guard.turnEnded(this.turnCount);
6760
+ if (turnReminder) {
6761
+ messages.push({ role: "user", content: turnReminder });
6762
+ tlog.info("guard.silent_turn", { cat: "guard", turn: this.turnCount });
6763
+ }
6764
+ }
5941
6765
  await this.deps.hooks.emit("on_turn_end", {
5942
6766
  turnNumber: this.turnCount,
5943
6767
  hasToolUse: true,
5944
6768
  toolCallCount: toolCalls.length
5945
6769
  });
5946
6770
  this.deps.transcript.appendTurnBoundary();
6771
+ this.config.onTurnBoundary?.(this.turnCount);
5947
6772
  tlog.info("turn.end", {
5948
6773
  cat: "turn",
5949
6774
  duration_ms: Date.now() - turnStartedAt,
@@ -6112,9 +6937,83 @@ var TurnLoop = class {
6112
6937
  }
6113
6938
  };
6114
6939
 
6940
+ // src/llm/provider-catalog.ts
6941
+ init_esm_shims();
6942
+ var ProviderCatalog = class {
6943
+ byKey = /* @__PURE__ */ new Map();
6944
+ constructor(entries) {
6945
+ for (const e of entries ?? []) this.byKey.set(e.key, e);
6946
+ }
6947
+ list() {
6948
+ return [...this.byKey.values()];
6949
+ }
6950
+ get(key) {
6951
+ return this.byKey.get(key);
6952
+ }
6953
+ has(key) {
6954
+ return this.byKey.has(key);
6955
+ }
6956
+ add(entry) {
6957
+ if (this.byKey.has(entry.key)) {
6958
+ throw new Error(`duplicate provider key: ${entry.key}`);
6959
+ }
6960
+ this.byKey.set(entry.key, entry);
6961
+ }
6962
+ update(key, patch) {
6963
+ const cur = this.byKey.get(key);
6964
+ if (!cur) throw new Error(`no such provider: ${key}`);
6965
+ this.byKey.set(key, { ...cur, ...patch, key: cur.key });
6966
+ }
6967
+ remove(key, opts) {
6968
+ if (opts.referencedBy.length) {
6969
+ throw new Error(
6970
+ `provider ${key} still referenced by models: ${opts.referencedBy.join(", ")}`
6971
+ );
6972
+ }
6973
+ this.byKey.delete(key);
6974
+ }
6975
+ /** Compute an unused key for a new provider, given a desired base. */
6976
+ deriveKey(base) {
6977
+ if (!this.byKey.has(base)) return base;
6978
+ for (let i = 2; ; i++) {
6979
+ const candidate = `${base}-${i}`;
6980
+ if (!this.byKey.has(candidate)) return candidate;
6981
+ }
6982
+ }
6983
+ };
6984
+
6985
+ // src/utils/task-sanitizer.ts
6986
+ init_esm_shims();
6987
+ var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
6988
+ var BOX_RE = /[─━│┃┌┐└┘├┤┬┴┼╔╗╚╝═║╠╣╦╩╬▀▄█▌▐░▒▓·]+/g;
6989
+ function detectPastedNoise(task) {
6990
+ const original = task.length;
6991
+ if (original < 10) return { isNoise: false, reason: "short", cleaned: task };
6992
+ const stripped = task.replace(ANSI_RE, "").replace(BOX_RE, "").replace(/\r/g, "\n").trim();
6993
+ if (stripped.length === 0) {
6994
+ return { isNoise: true, reason: "all ANSI/box chars", cleaned: stripped };
6995
+ }
6996
+ const signalRatio = stripped.length / original;
6997
+ if (signalRatio < 0.3 && original > 100) {
6998
+ return {
6999
+ isNoise: true,
7000
+ reason: `signal/noise ratio ${(signalRatio * 100).toFixed(0)}%`,
7001
+ cleaned: stripped
7002
+ };
7003
+ }
7004
+ return { isNoise: false, reason: "ok", cleaned: stripped };
7005
+ }
7006
+
6115
7007
  // src/engine/engine.ts
6116
- import { join as join6 } from "path";
7008
+ import { join as join8 } from "path";
6117
7009
  import { homedir as homedir4 } from "os";
7010
+ import {
7011
+ existsSync as existsSync11,
7012
+ mkdirSync as mkdirSync4,
7013
+ readFileSync as readFileSync6,
7014
+ renameSync,
7015
+ writeFileSync as writeFileSync4
7016
+ } from "fs";
6118
7017
  var Engine = class _Engine {
6119
7018
  constructor(config) {
6120
7019
  this.config = config;
@@ -6132,28 +7031,7 @@ var Engine = class _Engine {
6132
7031
  }
6133
7032
  this.sessionManager = new SessionManager(config.sessionStorageDir);
6134
7033
  this.modelPool = new ModelPool();
6135
- try {
6136
- const sm = new SettingsManager(config.cwd);
6137
- const settings = sm.get();
6138
- if (settings.models?.length) {
6139
- for (const m of settings.models) {
6140
- this.modelPool.register({
6141
- key: m.key,
6142
- label: m.label,
6143
- provider: m.provider,
6144
- model: m.model,
6145
- baseUrl: m.baseUrl,
6146
- apiKey: m.apiKey,
6147
- maxOutputTokens: m.maxOutputTokens,
6148
- maxContextTokens: m.maxContextTokens
6149
- });
6150
- }
6151
- const currentModel = config.llm.model;
6152
- const match = settings.models.find((m) => m.model === currentModel);
6153
- if (match) this.modelPool.switch(match.key);
6154
- }
6155
- } catch {
6156
- }
7034
+ this.populateModelPoolFromSettings();
6157
7035
  }
6158
7036
  preset;
6159
7037
  toolRegistry;
@@ -6172,10 +7050,99 @@ var Engine = class _Engine {
6172
7050
  lastSessionId;
6173
7051
  compactedMessagesBySession = /* @__PURE__ */ new Map();
6174
7052
  activePermission;
7053
+ /** Public accessor so UI/clients can read the resolved per-model window. */
7054
+ get maxContextTokens() {
7055
+ return this.resolveMaxContextTokens();
7056
+ }
6175
7057
  resolveMaxContextTokens() {
6176
7058
  const modelEntry = this.modelPool.get();
6177
7059
  return modelEntry?.maxContextTokens ?? this.config.maxContextTokens ?? 2e5;
6178
7060
  }
7061
+ /**
7062
+ * Load models[] / providers[] from settings into the active ModelPool and
7063
+ * resync this.config.llm with the matching entry. Called from the ctor and
7064
+ * from reloadModelPool() (e.g. after onboarding writes new entries to disk).
7065
+ */
7066
+ populateModelPoolFromSettings() {
7067
+ try {
7068
+ const sm = this.getSettingsManager();
7069
+ sm.invalidate();
7070
+ const settings = sm.get();
7071
+ if (settings.models?.length) {
7072
+ for (const m of settings.models) {
7073
+ this.modelPool.register({
7074
+ key: m.key,
7075
+ label: m.label,
7076
+ provider: m.provider ?? "",
7077
+ model: m.model,
7078
+ baseUrl: m.baseUrl,
7079
+ apiKey: m.apiKey,
7080
+ maxOutputTokens: m.maxOutputTokens,
7081
+ maxContextTokens: m.maxContextTokens,
7082
+ providerKey: m.providerKey
7083
+ });
7084
+ }
7085
+ if (settings.providers?.length) {
7086
+ this.modelPool.setProviderCatalog(
7087
+ new ProviderCatalog(settings.providers)
7088
+ );
7089
+ }
7090
+ this.modelPool.setCacheDir(defaultCacheDir());
7091
+ this.modelPool.reloadCachedContextWindows();
7092
+ const activeKey = settings.activeKey;
7093
+ let match;
7094
+ if (activeKey) {
7095
+ match = settings.models.find((m) => m.key === activeKey);
7096
+ }
7097
+ if (!match) {
7098
+ const currentModel = this.config.llm.model;
7099
+ match = settings.models.find(
7100
+ (m) => m.model === currentModel || currentModel && m.model?.endsWith(`/${currentModel}`)
7101
+ );
7102
+ }
7103
+ if (match) {
7104
+ const entry = this.modelPool.switch(match.key);
7105
+ this.config = {
7106
+ ...this.config,
7107
+ llm: this.modelPool.toLLMConfig(entry, this.config.llm)
7108
+ };
7109
+ }
7110
+ } else if (this.config.llm.apiKey) {
7111
+ this.autoPopulatePool(this.config.llm.apiKey, this.config.llm.baseUrl);
7112
+ }
7113
+ } catch {
7114
+ }
7115
+ }
7116
+ /**
7117
+ * Re-read settings and refresh the model pool. Used after onboarding /login
7118
+ * writes new providers[] / models[] to disk so the running engine picks them
7119
+ * up without a process restart. Existing pool entries are kept (re-registering
7120
+ * the same key overwrites them), so callers don't need to clear first.
7121
+ */
7122
+ reloadModelPool() {
7123
+ this.populateModelPoolFromSettings();
7124
+ }
7125
+ /**
7126
+ * Auto-populate the model pool when settings.models[] is empty but
7127
+ * the user has configured an API key. Detects the provider from the
7128
+ * key prefix / baseUrl and registers all its known models.
7129
+ */
7130
+ autoPopulatePool(apiKey, baseUrl) {
7131
+ const provider = detectProviderFromApiKey(apiKey, baseUrl);
7132
+ if (!provider) return;
7133
+ const entries = buildModelPool(provider, apiKey);
7134
+ for (const e of entries) {
7135
+ this.modelPool.register(e);
7136
+ }
7137
+ const defaultEntry = provider.defaultModel ? entries.find((e) => e.model === provider.defaultModel) : entries[0];
7138
+ if (defaultEntry) {
7139
+ const entry = this.modelPool.switch(defaultEntry.key);
7140
+ this.config = {
7141
+ ...this.config,
7142
+ llm: this.modelPool.toLLMConfig(entry, this.config.llm)
7143
+ };
7144
+ }
7145
+ }
6179
7146
  /**
6180
7147
  * Register a custom tool (from product adapter) into the tool registry.
6181
7148
  * Must be called before run().
@@ -6196,6 +7163,20 @@ var Engine = class _Engine {
6196
7163
  */
6197
7164
  async run(task, options) {
6198
7165
  const cwd = this.config.cwd ?? process.cwd();
7166
+ const noise = detectPastedNoise(task);
7167
+ if (noise.isNoise) {
7168
+ const hint = `Your input looks like pasted terminal output (${noise.reason}). I didn't start a task. What would you like to ask?` + (noise.cleaned ? `
7169
+
7170
+ Extracted text: ${noise.cleaned.slice(0, 200)}` : "");
7171
+ logger.info("engine.run.rejected", { reason: noise.reason, len: task.length });
7172
+ return {
7173
+ text: hint,
7174
+ reason: "completed",
7175
+ sessionId: options?.sessionId ?? "noise-rejected",
7176
+ turnCount: 0,
7177
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
7178
+ };
7179
+ }
6199
7180
  const subAgentSpawner = {
6200
7181
  parentStream: options?.onStream,
6201
7182
  describe: () => ({
@@ -6247,6 +7228,7 @@ var Engine = class _Engine {
6247
7228
  const userMsg = { role: "user", content: task };
6248
7229
  messages.push(userMsg);
6249
7230
  session.transcript.appendMessage("user", task);
7231
+ this.sessionManager.saveState(session.state);
6250
7232
  } else {
6251
7233
  session = this.sessionManager.create(cwd, this.config.llm.model, this.config.llm.provider);
6252
7234
  messages = [{ role: "user", content: task }];
@@ -6255,6 +7237,14 @@ var Engine = class _Engine {
6255
7237
  this.sessionManager.saveState(session.state);
6256
7238
  }
6257
7239
  logger.setSid(session.state.sessionId);
7240
+ recordSessionStart(session.state.sessionId, {
7241
+ task,
7242
+ cwd,
7243
+ model: this.config.llm.model,
7244
+ provider: this.config.llm.provider,
7245
+ permissionMode: this.config.permissionMode ?? "acceptEdits",
7246
+ resumed: !!options?.sessionId
7247
+ });
6258
7248
  options?.onStream?.({ type: "session_started", sessionId: session.state.sessionId });
6259
7249
  const llmClientPromise = createLLMClient(this.config.llm);
6260
7250
  const mode = this.config.permissionMode ?? "acceptEdits";
@@ -6268,6 +7258,9 @@ var Engine = class _Engine {
6268
7258
  });
6269
7259
  }
6270
7260
  const toolExecutor = new ToolExecutor(this.toolRegistry, permission, this.hooks);
7261
+ const investigationGuard = new InvestigationGuard();
7262
+ if (this.config.headless) investigationGuard.setSoftMode(true);
7263
+ toolExecutor.setInvestigationGuard(investigationGuard);
6271
7264
  toolExecutor.setSignal(options?.signal);
6272
7265
  toolExecutor.setContext(toolCtx);
6273
7266
  const contextManager = new ContextManager({
@@ -6319,6 +7312,7 @@ var Engine = class _Engine {
6319
7312
  this.lastSessionId = session.state.sessionId;
6320
7313
  this.lastMessages = messages;
6321
7314
  contextManager.setTranscriptPath(session.transcript.getFilePath());
7315
+ contextManager.initReplacementStateFromMessages(messages);
6322
7316
  contextManager.setSummarizeFn(async (prompt) => {
6323
7317
  const summaryResponse = await llmClient.createMessage({
6324
7318
  systemPrompt: "You are a conversation summarizer. Be concise and factual.",
@@ -6338,12 +7332,23 @@ var Engine = class _Engine {
6338
7332
  systemPrompt: sysPrompt,
6339
7333
  messages: [{ role: "user", content: userMsg }],
6340
7334
  tools: [],
6341
- maxTokens: 256
7335
+ maxTokens: 256,
7336
+ recordUsage: false
7337
+ });
7338
+ logger.debug("summarize.call", {
7339
+ sysPromptLen: sysPrompt.length,
7340
+ userMsgLen: userMsg.length,
7341
+ userMsgPreview: userMsg.slice(0, 300),
7342
+ completionLen: resp.text.length,
7343
+ completionPreview: resp.text.slice(0, 300),
7344
+ stopReason: resp.stopReason,
7345
+ promptTokens: resp.usage?.promptTokens,
7346
+ completionTokens: resp.usage?.completionTokens
6342
7347
  });
6343
7348
  return resp.text;
6344
7349
  };
6345
- const sessionDir = join6(
6346
- this.config.sessionStorageDir ?? join6(homedir4(), ".code-shell", "sessions"),
7350
+ const sessionDir = join8(
7351
+ this.config.sessionStorageDir ?? join8(homedir4(), ".code-shell", "sessions"),
6347
7352
  session.state.sessionId
6348
7353
  );
6349
7354
  const fileHistory = FileHistory.loadFromDir(sessionDir);
@@ -6382,7 +7387,24 @@ var Engine = class _Engine {
6382
7387
  maxTurns: this.config.maxTurns ?? 100,
6383
7388
  maxToolCallsPerTurn: this.config.maxToolCallsPerTurn ?? 10,
6384
7389
  onStream: options?.onStream,
6385
- signal: options?.signal
7390
+ signal: options?.signal,
7391
+ // Heartbeat: flush turnCount + tokens to state.json after every turn
7392
+ // so external observers (other CLI processes, /sid, the session list)
7393
+ // see live progress instead of a stale snapshot from the last
7394
+ // completed run.
7395
+ onTurnBoundary: (turnCount) => {
7396
+ session.state.turnCount = turnCount;
7397
+ const u = modelFacade.getUsage();
7398
+ session.state.tokenUsage = {
7399
+ promptTokens: u.totalPromptTokens,
7400
+ completionTokens: u.totalCompletionTokens,
7401
+ totalTokens: u.totalTokens
7402
+ };
7403
+ if (this.config.costStore) {
7404
+ session.state.costState = this.config.costStore.serialize();
7405
+ }
7406
+ this.sessionManager.saveState(session.state);
7407
+ }
6386
7408
  }
6387
7409
  );
6388
7410
  const result = await turnLoop.run(messages);
@@ -6397,8 +7419,13 @@ var Engine = class _Engine {
6397
7419
  turns: turnLoop.currentTurn,
6398
7420
  tokens: modelFacade.getUsage().totalTokens
6399
7421
  });
7422
+ recordSessionEnd(session.state.sessionId, {
7423
+ reason: result.reason,
7424
+ turns: turnLoop.currentTurn,
7425
+ cost: modelFacade.getUsage()
7426
+ });
6400
7427
  session.state.turnCount = turnLoop.currentTurn;
6401
- session.state.status = result.reason === "completed" ? "completed" : "errored";
7428
+ session.state.status = result.reason;
6402
7429
  const usage = modelFacade.getUsage();
6403
7430
  session.state.tokenUsage = {
6404
7431
  promptTokens: usage.totalPromptTokens,
@@ -6433,15 +7460,67 @@ var Engine = class _Engine {
6433
7460
  /**
6434
7461
  * Switch the active model by pool key. Takes effect on the next run() call.
6435
7462
  * Returns the new model entry.
7463
+ *
7464
+ * Persists settings.activeKey (and a legacy settings.model.* mirror) so the
7465
+ * next process startup defaults to the same model — without this, switches
7466
+ * only live in memory and every restart reverts to the previously persisted
7467
+ * activeKey.
6436
7468
  */
6437
7469
  switchModel(key) {
6438
7470
  const entry = this.modelPool.switch(key);
6439
- this.config = {
6440
- ...this.config,
6441
- llm: this.modelPool.toLLMConfig(entry, this.config.llm)
6442
- };
7471
+ const nextLlm = this.modelPool.toLLMConfig(entry, this.config.llm);
7472
+ this.config = { ...this.config, llm: nextLlm };
7473
+ this.persistActiveModel(entry, nextLlm);
6443
7474
  return entry;
6444
7475
  }
7476
+ /**
7477
+ * Write the active model selection to ~/.code-shell/settings.json.
7478
+ *
7479
+ * We mirror into the legacy settings.model.* block (provider/name/apiKey/
7480
+ * baseUrl) because boot paths in cli/main.ts, repl.ts, run.ts still read it.
7481
+ * The mirror uses resolved llm values (not raw entry.*) so credentials that
7482
+ * live on settings.providers[] flow through correctly — entry.apiKey is
7483
+ * undefined when the entry resolves via providerCatalog.
7484
+ */
7485
+ persistActiveModel(entry, llm) {
7486
+ try {
7487
+ const dir = join8(homedir4(), ".code-shell");
7488
+ const file = join8(dir, "settings.json");
7489
+ mkdirSync4(dir, { recursive: true });
7490
+ let existing = {};
7491
+ if (existsSync11(file)) {
7492
+ try {
7493
+ existing = JSON.parse(readFileSync6(file, "utf-8"));
7494
+ } catch {
7495
+ return;
7496
+ }
7497
+ }
7498
+ const prevModel = typeof existing.model === "object" && existing.model ? existing.model : {};
7499
+ const updated = {
7500
+ ...existing,
7501
+ activeKey: entry.key,
7502
+ model: {
7503
+ ...prevModel,
7504
+ provider: llm.provider,
7505
+ name: entry.model,
7506
+ apiKey: llm.apiKey,
7507
+ baseUrl: llm.baseUrl
7508
+ }
7509
+ };
7510
+ const tmp = `${file}.${process.pid}.tmp`;
7511
+ const payload = JSON.stringify(updated, null, 2) + "\n";
7512
+ writeFileSync4(tmp, payload, "utf-8");
7513
+ try {
7514
+ renameSync(tmp, file);
7515
+ } catch {
7516
+ writeFileSync4(file, payload, "utf-8");
7517
+ }
7518
+ } catch (err) {
7519
+ logger.warn(
7520
+ `persistActiveModel failed: ${err.message}`
7521
+ );
7522
+ }
7523
+ }
6445
7524
  /** Get the model pool. */
6446
7525
  getModelPool() {
6447
7526
  return this.modelPool;
@@ -6463,10 +7542,18 @@ var Engine = class _Engine {
6463
7542
  * Inject context into a session's transcript without triggering a LLM turn.
6464
7543
  * The injected content appears as an assistant message so the LLM can see it
6465
7544
  * in subsequent conversations. The transcript auto-flushes to disk.
7545
+ *
7546
+ * Also updates the in-memory compacted message cache so the next
7547
+ * engine.run() call for this session picks up the injected content
7548
+ * instead of a stale snapshot from the previous run.
6466
7549
  */
6467
7550
  injectContext(sessionId, content) {
6468
7551
  const session = this.sessionManager.resume(sessionId);
6469
7552
  session.transcript.appendMessage("assistant", content);
7553
+ const cached = this.compactedMessagesBySession.get(sessionId);
7554
+ if (cached) {
7555
+ cached.push({ role: "assistant", content });
7556
+ }
6470
7557
  }
6471
7558
  /**
6472
7559
  * Force context compaction on the current session.
@@ -6593,83 +7680,83 @@ var VALID_TRANSITIONS = {
6593
7680
  // src/run/FileRunStore.ts
6594
7681
  init_esm_shims();
6595
7682
  import {
6596
- mkdirSync as mkdirSync2,
6597
- existsSync as existsSync10,
6598
- readFileSync as readFileSync6,
6599
- writeFileSync as writeFileSync3,
6600
- appendFileSync,
6601
- readdirSync as readdirSync5,
6602
- renameSync,
6603
- rmSync
7683
+ mkdirSync as mkdirSync5,
7684
+ existsSync as existsSync12,
7685
+ readFileSync as readFileSync7,
7686
+ writeFileSync as writeFileSync5,
7687
+ appendFileSync as appendFileSync2,
7688
+ readdirSync as readdirSync6,
7689
+ renameSync as renameSync2,
7690
+ rmSync as rmSync2
6604
7691
  } from "fs";
6605
- import { join as join7 } from "path";
7692
+ import { join as join9 } from "path";
6606
7693
  import { homedir as homedir5 } from "os";
6607
7694
  var FileRunStore = class {
6608
7695
  runsDir;
6609
7696
  constructor(storageDir) {
6610
- this.runsDir = storageDir ?? join7(homedir5(), ".code-shell", "runs");
6611
- mkdirSync2(this.runsDir, { recursive: true });
7697
+ this.runsDir = storageDir ?? join9(homedir5(), ".code-shell", "runs");
7698
+ mkdirSync5(this.runsDir, { recursive: true });
6612
7699
  }
6613
7700
  // ─── Helpers ───────────────────────────────────────────────────
6614
7701
  runDir(runId) {
6615
- return join7(this.runsDir, runId);
7702
+ return join9(this.runsDir, runId);
6616
7703
  }
6617
7704
  ensureRunDir(runId) {
6618
7705
  const dir = this.runDir(runId);
6619
- mkdirSync2(dir, { recursive: true });
6620
- mkdirSync2(join7(dir, "checkpoints"), { recursive: true });
6621
- mkdirSync2(join7(dir, "approvals"), { recursive: true });
6622
- mkdirSync2(join7(dir, "artifacts"), { recursive: true });
7706
+ mkdirSync5(dir, { recursive: true });
7707
+ mkdirSync5(join9(dir, "checkpoints"), { recursive: true });
7708
+ mkdirSync5(join9(dir, "approvals"), { recursive: true });
7709
+ mkdirSync5(join9(dir, "artifacts"), { recursive: true });
6623
7710
  return dir;
6624
7711
  }
6625
7712
  writeJson(filePath, data) {
6626
7713
  const tmp = filePath + ".tmp";
6627
- writeFileSync3(tmp, JSON.stringify(data, null, 2), "utf-8");
6628
- renameSync(tmp, filePath);
7714
+ writeFileSync5(tmp, JSON.stringify(data, null, 2), "utf-8");
7715
+ renameSync2(tmp, filePath);
6629
7716
  }
6630
7717
  readJson(filePath) {
6631
- if (!existsSync10(filePath)) return null;
6632
- return JSON.parse(readFileSync6(filePath, "utf-8"));
7718
+ if (!existsSync12(filePath)) return null;
7719
+ return JSON.parse(readFileSync7(filePath, "utf-8"));
6633
7720
  }
6634
7721
  /** Serializes concurrent JSONL appends per file path. */
6635
7722
  appendLocks = /* @__PURE__ */ new Map();
6636
7723
  async appendJsonl(filePath, data) {
6637
7724
  const prev = this.appendLocks.get(filePath) ?? Promise.resolve();
6638
7725
  const current = prev.then(() => {
6639
- appendFileSync(filePath, JSON.stringify(data) + "\n", "utf-8");
7726
+ appendFileSync2(filePath, JSON.stringify(data) + "\n", "utf-8");
6640
7727
  });
6641
7728
  this.appendLocks.set(filePath, current);
6642
7729
  await current;
6643
7730
  }
6644
7731
  readJsonl(filePath) {
6645
- if (!existsSync10(filePath)) return [];
6646
- const content = readFileSync6(filePath, "utf-8");
7732
+ if (!existsSync12(filePath)) return [];
7733
+ const content = readFileSync7(filePath, "utf-8");
6647
7734
  if (!content) return [];
6648
7735
  return content.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
6649
7736
  }
6650
7737
  // ─── Snapshot ──────────────────────────────────────────────────
6651
7738
  async create(snapshot) {
6652
7739
  const dir = this.ensureRunDir(snapshot.runId);
6653
- this.writeJson(join7(dir, "run.json"), snapshot);
7740
+ this.writeJson(join9(dir, "run.json"), snapshot);
6654
7741
  }
6655
7742
  async update(snapshot) {
6656
7743
  const dir = this.runDir(snapshot.runId);
6657
- if (!existsSync10(dir)) {
7744
+ if (!existsSync12(dir)) {
6658
7745
  throw new Error(`Run not found: ${snapshot.runId}`);
6659
7746
  }
6660
- this.writeJson(join7(dir, "run.json"), snapshot);
7747
+ this.writeJson(join9(dir, "run.json"), snapshot);
6661
7748
  }
6662
7749
  async get(runId) {
6663
- return this.readJson(join7(this.runDir(runId), "run.json"));
7750
+ return this.readJson(join9(this.runDir(runId), "run.json"));
6664
7751
  }
6665
7752
  async list(query) {
6666
- if (!existsSync10(this.runsDir)) return [];
6667
- const entries = readdirSync5(this.runsDir, { withFileTypes: true });
7753
+ if (!existsSync12(this.runsDir)) return [];
7754
+ const entries = readdirSync6(this.runsDir, { withFileTypes: true });
6668
7755
  const snapshots = [];
6669
7756
  for (const entry of entries) {
6670
7757
  if (!entry.isDirectory()) continue;
6671
7758
  const snapshot = this.readJson(
6672
- join7(this.runsDir, entry.name, "run.json")
7759
+ join9(this.runsDir, entry.name, "run.json")
6673
7760
  );
6674
7761
  if (!snapshot) continue;
6675
7762
  if (query?.status) {
@@ -6686,32 +7773,32 @@ var FileRunStore = class {
6686
7773
  }
6687
7774
  async delete(runId) {
6688
7775
  const dir = this.runDir(runId);
6689
- if (existsSync10(dir)) {
6690
- rmSync(dir, { recursive: true, force: true });
7776
+ if (existsSync12(dir)) {
7777
+ rmSync2(dir, { recursive: true, force: true });
6691
7778
  }
6692
7779
  }
6693
7780
  // ─── Events ────────────────────────────────────────────────────
6694
7781
  async appendEvent(event) {
6695
7782
  this.ensureRunDir(event.runId);
6696
7783
  const dir = this.runDir(event.runId);
6697
- await this.appendJsonl(join7(dir, "events.jsonl"), event);
7784
+ await this.appendJsonl(join9(dir, "events.jsonl"), event);
6698
7785
  }
6699
7786
  async listEvents(runId) {
6700
- return this.readJsonl(join7(this.runDir(runId), "events.jsonl"));
7787
+ return this.readJsonl(join9(this.runDir(runId), "events.jsonl"));
6701
7788
  }
6702
7789
  // ─── Checkpoints ───────────────────────────────────────────────
6703
7790
  async saveCheckpoint(cp) {
6704
7791
  const dir = this.runDir(cp.runId);
6705
- this.writeJson(join7(dir, "checkpoints", `${cp.checkpointId}.json`), cp);
7792
+ this.writeJson(join9(dir, "checkpoints", `${cp.checkpointId}.json`), cp);
6706
7793
  }
6707
7794
  async getLatestCheckpoint(runId) {
6708
- const cpDir = join7(this.runDir(runId), "checkpoints");
6709
- if (!existsSync10(cpDir)) return null;
6710
- const files = readdirSync5(cpDir).filter((f) => f.endsWith(".json"));
7795
+ const cpDir = join9(this.runDir(runId), "checkpoints");
7796
+ if (!existsSync12(cpDir)) return null;
7797
+ const files = readdirSync6(cpDir).filter((f) => f.endsWith(".json"));
6711
7798
  if (files.length === 0) return null;
6712
7799
  let latest = null;
6713
7800
  for (const file of files) {
6714
- const cp = this.readJson(join7(cpDir, file));
7801
+ const cp = this.readJson(join9(cpDir, file));
6715
7802
  if (cp && (!latest || cp.createdAt > latest.createdAt)) {
6716
7803
  latest = cp;
6717
7804
  }
@@ -6722,21 +7809,21 @@ var FileRunStore = class {
6722
7809
  async saveApproval(approval) {
6723
7810
  const dir = this.runDir(approval.runId);
6724
7811
  this.writeJson(
6725
- join7(dir, "approvals", `${approval.approvalId}.json`),
7812
+ join9(dir, "approvals", `${approval.approvalId}.json`),
6726
7813
  approval
6727
7814
  );
6728
7815
  }
6729
7816
  async getApproval(runId, approvalId) {
6730
7817
  return this.readJson(
6731
- join7(this.runDir(runId), "approvals", `${approvalId}.json`)
7818
+ join9(this.runDir(runId), "approvals", `${approvalId}.json`)
6732
7819
  );
6733
7820
  }
6734
7821
  async getPendingApproval(runId) {
6735
- const approvalDir = join7(this.runDir(runId), "approvals");
6736
- if (!existsSync10(approvalDir)) return null;
6737
- const files = readdirSync5(approvalDir).filter((f) => f.endsWith(".json"));
7822
+ const approvalDir = join9(this.runDir(runId), "approvals");
7823
+ if (!existsSync12(approvalDir)) return null;
7824
+ const files = readdirSync6(approvalDir).filter((f) => f.endsWith(".json"));
6738
7825
  for (const file of files) {
6739
- const approval = this.readJson(join7(approvalDir, file));
7826
+ const approval = this.readJson(join9(approvalDir, file));
6740
7827
  if (approval?.status === "pending") return approval;
6741
7828
  }
6742
7829
  return null;
@@ -6745,11 +7832,11 @@ var FileRunStore = class {
6745
7832
  async appendArtifactRef(ref) {
6746
7833
  this.ensureRunDir(ref.runId);
6747
7834
  const dir = this.runDir(ref.runId);
6748
- await this.appendJsonl(join7(dir, "artifacts", "refs.jsonl"), ref);
7835
+ await this.appendJsonl(join9(dir, "artifacts", "refs.jsonl"), ref);
6749
7836
  }
6750
7837
  async listArtifactRefs(runId) {
6751
7838
  return this.readJsonl(
6752
- join7(this.runDir(runId), "artifacts", "refs.jsonl")
7839
+ join9(this.runDir(runId), "artifacts", "refs.jsonl")
6753
7840
  );
6754
7841
  }
6755
7842
  };
@@ -7116,8 +8203,8 @@ function extractFileName(filePath) {
7116
8203
 
7117
8204
  // src/run/RunLock.ts
7118
8205
  init_esm_shims();
7119
- import { existsSync as existsSync11 } from "fs";
7120
- import { join as join8 } from "path";
8206
+ import { existsSync as existsSync13 } from "fs";
8207
+ import { join as join10 } from "path";
7121
8208
  import { homedir as homedir6 } from "os";
7122
8209
 
7123
8210
  // src/utils/lockfile.ts
@@ -7145,7 +8232,7 @@ var RunLock = class {
7145
8232
  staleMs;
7146
8233
  releaseFns = /* @__PURE__ */ new Map();
7147
8234
  constructor(config) {
7148
- this.runsDir = config?.runsDir ?? join8(homedir6(), ".code-shell", "runs");
8235
+ this.runsDir = config?.runsDir ?? join10(homedir6(), ".code-shell", "runs");
7149
8236
  this.staleMs = config?.staleMs ?? 6e4;
7150
8237
  }
7151
8238
  /**
@@ -7153,7 +8240,7 @@ var RunLock = class {
7153
8240
  */
7154
8241
  async acquire(runId) {
7155
8242
  const lockTarget = this.lockTarget(runId);
7156
- if (!existsSync11(lockTarget)) return false;
8243
+ if (!existsSync13(lockTarget)) return false;
7157
8244
  try {
7158
8245
  const release = await lock(lockTarget, {
7159
8246
  stale: this.staleMs,
@@ -7185,7 +8272,7 @@ var RunLock = class {
7185
8272
  */
7186
8273
  async isLocked(runId) {
7187
8274
  const lockTarget = this.lockTarget(runId);
7188
- if (!existsSync11(lockTarget)) return false;
8275
+ if (!existsSync13(lockTarget)) return false;
7189
8276
  try {
7190
8277
  return await check(lockTarget, { stale: this.staleMs });
7191
8278
  } catch {
@@ -7197,7 +8284,7 @@ var RunLock = class {
7197
8284
  */
7198
8285
  async forceUnlock(runId) {
7199
8286
  const lockTarget = this.lockTarget(runId);
7200
- if (!existsSync11(lockTarget)) return;
8287
+ if (!existsSync13(lockTarget)) return;
7201
8288
  try {
7202
8289
  await unlock(lockTarget);
7203
8290
  logger.info("run.lock.force_unlocked", { runId });
@@ -7213,21 +8300,21 @@ var RunLock = class {
7213
8300
  }
7214
8301
  }
7215
8302
  lockTarget(runId) {
7216
- return join8(this.runsDir, runId, "run.json");
8303
+ return join10(this.runsDir, runId, "run.json");
7217
8304
  }
7218
8305
  };
7219
8306
 
7220
8307
  // src/run/Heartbeat.ts
7221
8308
  init_esm_shims();
7222
- import { existsSync as existsSync12, readFileSync as readFileSync7, writeFileSync as writeFileSync4, unlinkSync } from "fs";
7223
- import { join as join9 } from "path";
8309
+ import { existsSync as existsSync14, readFileSync as readFileSync8, writeFileSync as writeFileSync6, unlinkSync } from "fs";
8310
+ import { join as join11 } from "path";
7224
8311
  import { homedir as homedir7 } from "os";
7225
8312
  var Heartbeat = class {
7226
8313
  runsDir;
7227
8314
  intervalMs;
7228
8315
  timers = /* @__PURE__ */ new Map();
7229
8316
  constructor(config) {
7230
- this.runsDir = config?.runsDir ?? join9(homedir7(), ".code-shell", "runs");
8317
+ this.runsDir = config?.runsDir ?? join11(homedir7(), ".code-shell", "runs");
7231
8318
  this.intervalMs = config?.intervalMs ?? 5e3;
7232
8319
  }
7233
8320
  /**
@@ -7265,9 +8352,9 @@ var Heartbeat = class {
7265
8352
  */
7266
8353
  read(runId) {
7267
8354
  const filePath = this.filePath(runId);
7268
- if (!existsSync12(filePath)) return null;
8355
+ if (!existsSync14(filePath)) return null;
7269
8356
  try {
7270
- return JSON.parse(readFileSync7(filePath, "utf-8"));
8357
+ return JSON.parse(readFileSync8(filePath, "utf-8"));
7271
8358
  } catch {
7272
8359
  return null;
7273
8360
  }
@@ -7302,21 +8389,21 @@ var Heartbeat = class {
7302
8389
  runId
7303
8390
  };
7304
8391
  try {
7305
- writeFileSync4(this.filePath(runId), JSON.stringify(data), "utf-8");
8392
+ writeFileSync6(this.filePath(runId), JSON.stringify(data), "utf-8");
7306
8393
  } catch {
7307
8394
  }
7308
8395
  }
7309
8396
  remove(runId) {
7310
8397
  try {
7311
8398
  const filePath = this.filePath(runId);
7312
- if (existsSync12(filePath)) {
8399
+ if (existsSync14(filePath)) {
7313
8400
  unlinkSync(filePath);
7314
8401
  }
7315
8402
  } catch {
7316
8403
  }
7317
8404
  }
7318
8405
  filePath(runId) {
7319
- return join9(this.runsDir, runId, "heartbeat");
8406
+ return join11(this.runsDir, runId, "heartbeat");
7320
8407
  }
7321
8408
  };
7322
8409
 
@@ -7957,6 +9044,14 @@ export {
7957
9044
  setRuntimeBypass,
7958
9045
  PermissionClassifier,
7959
9046
  HookRegistry,
9047
+ DEFAULT_PERSIST_THRESHOLD,
9048
+ PER_MESSAGE_AGGREGATE_CAP,
9049
+ PREVIEW_SIZE,
9050
+ createContentReplacementState,
9051
+ reconstructContentReplacementState,
9052
+ resolveToolResultsDir,
9053
+ applyToolResultPersistence,
9054
+ isPersistedReplacement,
7960
9055
  ContextManager,
7961
9056
  SectionCache,
7962
9057
  scanInstructions,