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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +77 -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-HHDYYVHJ.cjs → arena-KBQOPLFV.cjs} +25 -23
  8. package/dist/{arena-ENHBHB2W.js → arena-O2L4TN6A.js} +9 -7
  9. package/dist/{chunk-SW3L7HIM.js → chunk-2EP3CLEN.js} +2 -2
  10. package/dist/{chunk-EL2RL5DG.cjs → chunk-2YW4FPFG.cjs} +2 -2
  11. package/dist/chunk-3OA5G4TM.js +47 -0
  12. package/dist/chunk-52LGZKT3.cjs +47 -0
  13. package/dist/{chunk-KOGV6PDQ.cjs → chunk-55XGPMAP.cjs} +44 -9
  14. package/dist/chunk-5ECBT4YA.js +450 -0
  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-TDXBHD24.js → chunk-76W334S4.js} +2 -2
  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-ID6EJUHP.js → chunk-7K6SDRRF.js} +1262 -212
  22. package/dist/{chunk-5C5HIOML.js → chunk-7Y6S3OCX.js} +109 -21
  23. package/dist/{chunk-VIPLU4RY.cjs → chunk-A5LCRPDP.cjs} +4 -4
  24. package/dist/{chunk-F5BS3GUC.js → chunk-BSW22MPA.js} +39 -4
  25. package/dist/{chunk-VWFET3MG.cjs → chunk-BUU5PZK2.cjs} +1474 -423
  26. package/dist/{chunk-HOXQLX5R.cjs → chunk-BWTOS5BH.cjs} +444 -103
  27. package/dist/{chunk-ZRVPCXI7.js → chunk-BXCY2FSE.js} +2 -1
  28. package/dist/{chunk-Y7TSCDF6.js → chunk-C5YQBIRK.js} +347 -6
  29. package/dist/{chunk-C7C7ZZY4.cjs → chunk-HEKQCI7B.cjs} +10 -10
  30. package/dist/{chunk-JHXNOB5G.cjs → chunk-HS2MFNUC.cjs} +6 -6
  31. package/dist/{chunk-JHNCK7JP.cjs → chunk-IK4EWJBP.cjs} +4 -3
  32. package/dist/{chunk-M2QI5X3X.cjs → chunk-JMR33QVF.cjs} +2 -2
  33. package/dist/{chunk-YAFCN7ZW.cjs → chunk-KOVYSDHZ.cjs} +15 -15
  34. package/dist/{chunk-IJSHQGMP.cjs → chunk-KS3RYAYL.cjs} +4 -4
  35. package/dist/{chunk-DI7RDLOS.js → chunk-MD32R22I.js} +7 -4
  36. package/dist/chunk-MY5XHADU.cjs +216 -0
  37. package/dist/{chunk-OP57WUWQ.cjs → chunk-NUNC3VFB.cjs} +6 -6
  38. package/dist/{chunk-BF4LXLQV.cjs → chunk-QRM7IF2S.cjs} +133 -175
  39. package/dist/chunk-R47ROHGL.cjs +298 -0
  40. package/dist/{chunk-OUC2DFA7.js → chunk-RMYKO74M.js} +3 -3
  41. package/dist/{chunk-FUOKLKR2.js → chunk-SFXHZSRF.js} +1 -1
  42. package/dist/{chunk-2EUAE7AY.js → chunk-SIHHIQLO.js} +2 -2
  43. package/dist/{chunk-3CAZIWUA.cjs → chunk-SNT563K3.cjs} +9 -9
  44. package/dist/{chunk-SK62UVS6.js → chunk-T6VYG46N.js} +1 -1
  45. package/dist/{chunk-3QR52LL5.cjs → chunk-TUY2T5KP.cjs} +2 -2
  46. package/dist/{chunk-I57ZD5IQ.js → chunk-VIAGTYYW.js} +1 -1
  47. package/dist/{chunk-X2DNIMPC.cjs → chunk-VYL4EFRC.cjs} +32 -32
  48. package/dist/chunk-W22P377T.js +299 -0
  49. package/dist/chunk-WASS5YWB.js +215 -0
  50. package/dist/{chunk-FPFTIKEJ.js → chunk-XDIQWQJ4.js} +132 -174
  51. package/dist/{chunk-BUJ5KYCI.cjs → chunk-YIQBXAYU.cjs} +13 -13
  52. package/dist/{chunk-REFCCVDM.js → chunk-YJ77GQK5.js} +2 -2
  53. package/dist/chunk-YUI524OE.cjs +450 -0
  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 +2343 -1447
  59. package/dist/cli/main.js +2476 -1580
  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 +51 -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 +40 -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 +40 -20
  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-C2IUXKRC.js +7 -0
  90. package/dist/manager-YYKWYTRM.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-OU4GRZQY.cjs +15 -0
  102. package/dist/{openrouter-models-2FM3H4SQ.js → openrouter-models-TGKDKY52.js} +2 -2
  103. package/dist/openrouter-sync-BBHUM3R6.cjs +11 -0
  104. package/dist/{openrouter-sync-SO3AWEAM.js → openrouter-sync-UXUSJCIR.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 +37 -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-QW473OF4.cjs +17 -0
  129. package/dist/updater-VFUAHCVY.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 +1 -1
  133. package/dist/anthropic-P5VETAFI.cjs +0 -10
  134. package/dist/anthropic-W2GU3D76.js +0 -10
  135. package/dist/chunk-75E4BBFC.js +0 -224
  136. package/dist/chunk-FV2ARONZ.cjs +0 -224
  137. package/dist/client-base-TOVE25FA.js +0 -9
  138. package/dist/client-base-VDDIADAC.cjs +0 -9
  139. package/dist/compaction-D3KV2NWY.cjs +0 -31
  140. package/dist/devtools-WTC3NFNE.cjs +0 -6
  141. package/dist/file-history-HZWO2WQ5.js +0 -7
  142. package/dist/file-history-UQQILIY7.cjs +0 -7
  143. package/dist/manager-FWLOVEEK.cjs +0 -7
  144. package/dist/manager-GEXJLOIS.js +0 -7
  145. package/dist/mcp-manager-37GQLFGX.js +0 -8
  146. package/dist/mcp-manager-U77NEXBY.cjs +0 -8
  147. package/dist/memory-ADGOWADP.cjs +0 -9
  148. package/dist/openai-RXU5URYI.js +0 -10
  149. package/dist/openai-SQW4H2K4.cjs +0 -10
  150. package/dist/openrouter-models-PLCWZZYE.cjs +0 -15
  151. package/dist/openrouter-sync-K2WK3A2A.cjs +0 -11
  152. package/dist/services/compact.d.ts +0 -56
  153. package/dist/session-manager-HRSKENKB.cjs +0 -8
  154. package/dist/session-manager-NEKFEHF3.js +0 -8
  155. package/dist/updater-J7L7R6BY.cjs +0 -185
  156. 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,51 @@ 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-5ECBT4YA.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-RMYKO74M.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-C5YQBIRK.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 {
56
62
  __require,
57
63
  __toCommonJS,
58
64
  init_esm_shims
59
- } from "./chunk-DI7RDLOS.js";
65
+ } from "./chunk-MD32R22I.js";
60
66
 
61
67
  // src/tool-system/builtin/task.ts
62
68
  init_esm_shims();
@@ -380,7 +386,7 @@ var fileCache = new FileStateCache();
380
386
  // src/tool-system/builtin/read.ts
381
387
  var readToolDef = {
382
388
  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.",
389
+ 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
390
  inputSchema: {
385
391
  type: "object",
386
392
  properties: {
@@ -1364,11 +1370,40 @@ ${output}`;
1364
1370
  init_esm_shims();
1365
1371
  var askUserToolDef = {
1366
1372
  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.",
1373
+ 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
1374
  inputSchema: {
1369
1375
  type: "object",
1370
1376
  properties: {
1371
- question: { type: "string", description: "The question to ask the user" }
1377
+ question: {
1378
+ type: "string",
1379
+ description: "The question to ask the user \u2014 in the user's language"
1380
+ },
1381
+ header: {
1382
+ type: "string",
1383
+ 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."
1384
+ },
1385
+ options: {
1386
+ type: "array",
1387
+ description: "Optional 2-4 multiple-choice options. Omit for free-text input. An implicit 'Other...' entry is appended automatically by the framework.",
1388
+ items: {
1389
+ type: "object",
1390
+ properties: {
1391
+ label: {
1392
+ type: "string",
1393
+ description: "Short display text (1-5 words) \u2014 in the user's language"
1394
+ },
1395
+ description: {
1396
+ type: "string",
1397
+ description: "What this option means or implies \u2014 shown next to the label, in the user's language"
1398
+ }
1399
+ },
1400
+ required: ["label", "description"]
1401
+ }
1402
+ },
1403
+ multiSelect: {
1404
+ type: "boolean",
1405
+ description: "If true, the user can select multiple options. Defaults to false (single choice)."
1406
+ }
1372
1407
  },
1373
1408
  required: ["question"]
1374
1409
  }
@@ -1379,13 +1414,28 @@ async function askUserTool(args, ctx) {
1379
1414
  if (!ctx?.askUser) {
1380
1415
  return "Error: AskUserQuestion is not available in headless mode. Make a reasonable assumption and proceed.";
1381
1416
  }
1417
+ const opts = parseOptions(args);
1382
1418
  try {
1383
- const answer = await ctx.askUser(question);
1419
+ const answer = await ctx.askUser(question, opts);
1384
1420
  return answer || "(user provided empty response)";
1385
1421
  } catch (err) {
1386
1422
  return `Error asking user: ${err.message}`;
1387
1423
  }
1388
1424
  }
1425
+ function parseOptions(args) {
1426
+ const rawOptions = args.options;
1427
+ const header = typeof args.header === "string" ? args.header : void 0;
1428
+ const multiSelect = args.multiSelect === true;
1429
+ if (!Array.isArray(rawOptions) || rawOptions.length === 0) {
1430
+ if (header === void 0 && !multiSelect) return void 0;
1431
+ return { header, multiSelect, options: void 0 };
1432
+ }
1433
+ const options = rawOptions.filter((o) => {
1434
+ return typeof o === "object" && o !== null && typeof o.label === "string" && typeof o.description === "string";
1435
+ }).map((o) => ({ label: o.label, description: o.description }));
1436
+ if (options.length === 0) return { header, multiSelect, options: void 0 };
1437
+ return { header, multiSelect, options };
1438
+ }
1389
1439
 
1390
1440
  // src/tool-system/builtin/agent.ts
1391
1441
  init_esm_shims();
@@ -2379,8 +2429,8 @@ async function lspTool(args) {
2379
2429
  ${content}`;
2380
2430
  }
2381
2431
  case "getDiagnostics": {
2382
- const { readFileSync: readFileSync8 } = await import("fs");
2383
- const text = readFileSync8(filePath, "utf-8");
2432
+ const { readFileSync: readFileSync9 } = await import("fs");
2433
+ const text = readFileSync9(filePath, "utf-8");
2384
2434
  await client.notify("textDocument/didOpen", {
2385
2435
  textDocument: { uri, languageId: serverConfig.language, version: 1, text }
2386
2436
  });
@@ -2695,7 +2745,7 @@ async function mcpToolExecute(args) {
2695
2745
  const tool = args.tool;
2696
2746
  const toolArgs = args.arguments ?? {};
2697
2747
  try {
2698
- const { MCPManager: MCPManager2 } = await import("./mcp-manager-37GQLFGX.js");
2748
+ const { MCPManager: MCPManager2 } = await import("./mcp-manager-QMKWPBZ4.js");
2699
2749
  const manager = MCPManager2.getInstance();
2700
2750
  const result = await manager.callTool(server, tool, toolArgs);
2701
2751
  return typeof result === "string" ? result : JSON.stringify(result, null, 2);
@@ -2719,7 +2769,7 @@ var listMcpResourcesToolDef = {
2719
2769
  async function listMcpResourcesTool(args) {
2720
2770
  const server = args.server ?? "";
2721
2771
  try {
2722
- const { MCPManager: MCPManager2 } = await import("./mcp-manager-37GQLFGX.js");
2772
+ const { MCPManager: MCPManager2 } = await import("./mcp-manager-QMKWPBZ4.js");
2723
2773
  const manager = MCPManager2.getInstance();
2724
2774
  const resources = await manager.listResources(server || void 0);
2725
2775
  if (!resources || resources.length === 0) {
@@ -2754,7 +2804,7 @@ async function readMcpResourceTool(args) {
2754
2804
  const server = args.server;
2755
2805
  const uri = args.uri;
2756
2806
  try {
2757
- const { MCPManager: MCPManager2 } = await import("./mcp-manager-37GQLFGX.js");
2807
+ const { MCPManager: MCPManager2 } = await import("./mcp-manager-QMKWPBZ4.js");
2758
2808
  const manager = MCPManager2.getInstance();
2759
2809
  const content = await manager.readResource(server, uri);
2760
2810
  return typeof content === "string" ? content : JSON.stringify(content, null, 2);
@@ -2792,11 +2842,11 @@ async function remoteTriggerTool(args) {
2792
2842
  const prompt = args.prompt;
2793
2843
  const config = args.config;
2794
2844
  try {
2795
- const { existsSync: existsSync13, readFileSync: readFileSync8, writeFileSync: writeFileSync5, mkdirSync: mkdirSync3 } = await import("fs");
2796
- const { join: join10 } = await import("path");
2845
+ const { existsSync: existsSync15, readFileSync: readFileSync9, writeFileSync: writeFileSync7, mkdirSync: mkdirSync6 } = await import("fs");
2846
+ const { join: join12 } = await import("path");
2797
2847
  const { homedir: homedir8 } = await import("os");
2798
- const triggerDir = join10(homedir8(), ".code-shell", "triggers");
2799
- mkdirSync3(triggerDir, { recursive: true });
2848
+ const triggerDir = join12(homedir8(), ".code-shell", "triggers");
2849
+ mkdirSync6(triggerDir, { recursive: true });
2800
2850
  const trigger = {
2801
2851
  id: `trigger_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
2802
2852
  name,
@@ -2805,8 +2855,8 @@ async function remoteTriggerTool(args) {
2805
2855
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2806
2856
  status: "pending"
2807
2857
  };
2808
- const triggerFile = join10(triggerDir, `${trigger.id}.json`);
2809
- writeFileSync5(triggerFile, JSON.stringify(trigger, null, 2), "utf-8");
2858
+ const triggerFile = join12(triggerDir, `${trigger.id}.json`);
2859
+ writeFileSync7(triggerFile, JSON.stringify(trigger, null, 2), "utf-8");
2810
2860
  return `Remote trigger "${name}" dispatched (ID: ${trigger.id}).
2811
2861
  Stored at: ${triggerFile}`;
2812
2862
  } catch (err) {
@@ -3306,7 +3356,9 @@ var BUILTIN_TOOLS = [
3306
3356
  source: "builtin",
3307
3357
  permissionDefault: "allow",
3308
3358
  isReadOnly: true,
3309
- isConcurrencySafe: false
3359
+ isConcurrencySafe: false,
3360
+ timeoutMs: 0
3361
+ // 纯等待用户操作 — 不设超时,由用户 Esc/取消
3310
3362
  },
3311
3363
  execute: askUserTool
3312
3364
  },
@@ -3745,6 +3797,209 @@ var ToolRegistry = class {
3745
3797
  // src/tool-system/executor.ts
3746
3798
  init_esm_shims();
3747
3799
 
3800
+ // src/logging/session-recorder.ts
3801
+ init_esm_shims();
3802
+ import {
3803
+ appendFileSync,
3804
+ existsSync as existsSync8,
3805
+ mkdirSync as mkdirSync2,
3806
+ readdirSync as readdirSync3,
3807
+ rmSync,
3808
+ statSync
3809
+ } from "fs";
3810
+ import { join as join4 } from "path";
3811
+ var RETENTION_DAYS = 7;
3812
+ var MAX_RECORD_BYTES = 256 * 1024;
3813
+ function clip(s) {
3814
+ if (s === void 0) return void 0;
3815
+ if (s.length <= MAX_RECORD_BYTES) return s;
3816
+ return s.slice(0, MAX_RECORD_BYTES) + `
3817
+ \u2026(truncated, ${s.length - MAX_RECORD_BYTES} more bytes)`;
3818
+ }
3819
+ function isLocalDev() {
3820
+ if (process.env.CODE_SHELL_VERBOSE_LOG === "0") return false;
3821
+ if (process.env.CODE_SHELL_DEV === "1") return true;
3822
+ if (process.argv.includes("--debug") || process.argv.includes("-d")) return true;
3823
+ if (process.argv.some((a) => a.startsWith("--debug="))) return true;
3824
+ if (process.env.NODE_ENV === "test" || process.env.BUN_TEST === "1") return false;
3825
+ const entry = process.argv[1] ?? "";
3826
+ if (entry.includes(`${"/"}src${"/"}`)) return true;
3827
+ return false;
3828
+ }
3829
+ var ENABLED = isLocalDev();
3830
+ function resolveLogDir() {
3831
+ let dir = new URL(".", import.meta.url).pathname;
3832
+ if (dir.endsWith("/")) dir = dir.slice(0, -1);
3833
+ for (let i = 0; i < 8; i++) {
3834
+ if (existsSync8(join4(dir, "package.json"))) return join4(dir, "log");
3835
+ const parent = join4(dir, "..");
3836
+ if (parent === dir) break;
3837
+ dir = parent;
3838
+ }
3839
+ return join4(process.cwd(), "log");
3840
+ }
3841
+ var LOG_ROOT = ENABLED ? resolveLogDir() : "";
3842
+ var SENSITIVE_FLAG_RE = /^--?(api[-_]?key|auth[-_]?token|token|password|secret|bearer)(=|$)/i;
3843
+ function redactArgv(argv) {
3844
+ const out = [];
3845
+ for (let i = 0; i < argv.length; i++) {
3846
+ const a = argv[i] ?? "";
3847
+ const m = SENSITIVE_FLAG_RE.exec(a);
3848
+ if (!m) {
3849
+ out.push(a);
3850
+ continue;
3851
+ }
3852
+ if (m[2] === "=") {
3853
+ out.push(`${a.slice(0, a.indexOf("=") + 1)}<redacted>`);
3854
+ } else {
3855
+ out.push(a);
3856
+ if (i + 1 < argv.length) {
3857
+ out.push("<redacted>");
3858
+ i += 1;
3859
+ }
3860
+ }
3861
+ }
3862
+ return out;
3863
+ }
3864
+ var states = /* @__PURE__ */ new Map();
3865
+ var rotated = false;
3866
+ function todayStr() {
3867
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3868
+ }
3869
+ function rotateOnce() {
3870
+ if (rotated || !ENABLED) return;
3871
+ rotated = true;
3872
+ try {
3873
+ if (!existsSync8(LOG_ROOT)) return;
3874
+ const cutoff = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1e3;
3875
+ for (const entry of readdirSync3(LOG_ROOT)) {
3876
+ const full = join4(LOG_ROOT, entry);
3877
+ try {
3878
+ const st = statSync(full);
3879
+ if (st.isDirectory() && st.mtimeMs < cutoff) {
3880
+ rmSync(full, { recursive: true, force: true });
3881
+ }
3882
+ } catch {
3883
+ }
3884
+ }
3885
+ } catch {
3886
+ }
3887
+ }
3888
+ function ensureState(sid) {
3889
+ if (!ENABLED) return null;
3890
+ rotateOnce();
3891
+ let st = states.get(sid);
3892
+ if (st) return st;
3893
+ const dateStr = todayStr();
3894
+ const dayDir = join4(LOG_ROOT, dateStr);
3895
+ const path = join4(dayDir, `session-${sid}.jsonl`);
3896
+ try {
3897
+ if (!existsSync8(dayDir)) mkdirSync2(dayDir, { recursive: true });
3898
+ } catch {
3899
+ return null;
3900
+ }
3901
+ st = { path, startedAt: Date.now() };
3902
+ states.set(sid, st);
3903
+ return st;
3904
+ }
3905
+ function write(sid, event) {
3906
+ const st = ensureState(sid);
3907
+ if (!st) return;
3908
+ const record = { t: (/* @__PURE__ */ new Date()).toISOString(), sid, ...event };
3909
+ try {
3910
+ appendFileSync(st.path, JSON.stringify(record) + "\n", "utf-8");
3911
+ } catch {
3912
+ }
3913
+ }
3914
+ function recordSessionStart(sid, info) {
3915
+ if (!ENABLED) return;
3916
+ write(sid, {
3917
+ type: "session_start",
3918
+ pid: process.pid,
3919
+ argv: redactArgv(process.argv.slice(1)),
3920
+ cwd: info.cwd,
3921
+ model: info.model,
3922
+ provider: info.provider,
3923
+ permissionMode: info.permissionMode,
3924
+ resumed: info.resumed,
3925
+ task: info.task
3926
+ });
3927
+ }
3928
+ function recordLLMRequest(sid, req, reqId) {
3929
+ if (!ENABLED) return;
3930
+ write(sid, {
3931
+ type: "llm.request",
3932
+ reqId,
3933
+ provider: req.provider,
3934
+ model: req.model,
3935
+ stream: req.stream,
3936
+ temperature: req.temperature,
3937
+ maxTokens: req.maxTokens,
3938
+ toolCount: Array.isArray(req.tools) ? req.tools.length : 0,
3939
+ messageCount: Array.isArray(req.messages) ? req.messages.length : void 0,
3940
+ systemPrompt: req.systemPrompt,
3941
+ tools: req.tools,
3942
+ messages: req.messages
3943
+ });
3944
+ }
3945
+ function recordLLMResponse(sid, resp, reqId) {
3946
+ if (!ENABLED) return;
3947
+ write(sid, {
3948
+ type: "llm.response",
3949
+ reqId,
3950
+ stopReason: resp.stopReason,
3951
+ durationMs: resp.durationMs,
3952
+ ttftMs: resp.ttftMs,
3953
+ usage: resp.usage,
3954
+ toolCallCount: resp.toolCalls?.length ?? 0,
3955
+ text: resp.text,
3956
+ toolCalls: resp.toolCalls
3957
+ });
3958
+ }
3959
+ function recordLLMError(sid, reqId, err, durationMs) {
3960
+ if (!ENABLED) return;
3961
+ write(sid, {
3962
+ type: "llm.error",
3963
+ reqId,
3964
+ durationMs,
3965
+ message: err instanceof Error ? err.message : String(err),
3966
+ stack: err instanceof Error ? err.stack : void 0
3967
+ });
3968
+ }
3969
+ function recordToolCall(sid, call) {
3970
+ if (!ENABLED) return;
3971
+ write(sid, {
3972
+ type: "tool.call",
3973
+ toolCallId: call.id,
3974
+ toolName: call.toolName,
3975
+ args: call.args
3976
+ });
3977
+ }
3978
+ function recordToolResult(sid, result) {
3979
+ if (!ENABLED) return;
3980
+ write(sid, {
3981
+ type: "tool.result",
3982
+ toolCallId: result.id,
3983
+ toolName: result.toolName,
3984
+ ok: result.ok,
3985
+ durationMs: result.durationMs,
3986
+ chars: (result.output ?? result.error ?? "").length,
3987
+ output: clip(result.output),
3988
+ error: clip(result.error)
3989
+ });
3990
+ }
3991
+ function recordSessionEnd(sid, info) {
3992
+ if (!ENABLED) return;
3993
+ write(sid, {
3994
+ type: "session_end",
3995
+ reason: info.reason,
3996
+ turns: info.turns,
3997
+ durationMs: info.durationMs,
3998
+ cost: info.cost
3999
+ });
4000
+ states.delete(sid);
4001
+ }
4002
+
3748
4003
  // src/tool-system/validation.ts
3749
4004
  init_esm_shims();
3750
4005
  function validateToolArgs(toolName, args, schema) {
@@ -3794,6 +4049,13 @@ var ToolExecutor = class {
3794
4049
  * (tests, ad-hoc tool runs) still write log lines, just without turn tags.
3795
4050
  */
3796
4051
  log = logger;
4052
+ guard;
4053
+ setInvestigationGuard(guard) {
4054
+ this.guard = guard;
4055
+ }
4056
+ getInvestigationGuard() {
4057
+ return this.guard;
4058
+ }
3797
4059
  /** Set the abort signal for cascading cancellation. */
3798
4060
  setSignal(signal) {
3799
4061
  this.signal = signal;
@@ -3867,6 +4129,16 @@ var ToolExecutor = class {
3867
4129
  isError: true
3868
4130
  };
3869
4131
  }
4132
+ const guardDecision = this.guard?.preToolCheck(call);
4133
+ if (guardDecision?.block) {
4134
+ this.log.info("guard.block", { cat: "guard", tool: call.toolName, reason: guardDecision.block.slice(0, 200) });
4135
+ return {
4136
+ id: call.id,
4137
+ toolName: call.toolName,
4138
+ error: guardDecision.block,
4139
+ isError: true
4140
+ };
4141
+ }
3870
4142
  const decision = this.permission.classify(call.toolName, call.args);
3871
4143
  this.log.info("permission.classify", {
3872
4144
  cat: "permission",
@@ -3904,6 +4176,9 @@ var ToolExecutor = class {
3904
4176
  toolCallId: call.id,
3905
4177
  args: JSON.stringify(call.args).slice(0, 2e3)
3906
4178
  });
4179
+ const sid = getCurrentSid();
4180
+ recordToolCall(sid, { id: call.id, toolName: call.toolName, args: call.args });
4181
+ const toolStartedAt = Date.now();
3907
4182
  let result;
3908
4183
  try {
3909
4184
  result = await this.registry.executeTool(call.toolName, call.args, {
@@ -3912,9 +4187,20 @@ var ToolExecutor = class {
3912
4187
  });
3913
4188
  } catch (err) {
3914
4189
  span.fail(err);
4190
+ recordToolResult(sid, {
4191
+ id: call.id,
4192
+ toolName: call.toolName,
4193
+ ok: false,
4194
+ durationMs: Date.now() - toolStartedAt,
4195
+ error: err instanceof Error ? err.stack ?? err.message : String(err)
4196
+ });
3915
4197
  throw err;
3916
4198
  }
3917
4199
  result.id = call.id;
4200
+ if (guardDecision?.prepend && !result.error && result.result) {
4201
+ result.result = `${guardDecision.prepend}
4202
+ ${result.result}`;
4203
+ }
3918
4204
  const payload = result.result ?? result.error ?? "";
3919
4205
  span.end({
3920
4206
  ok: !result.error,
@@ -3924,6 +4210,14 @@ var ToolExecutor = class {
3924
4210
  // usually contain the message we need.
3925
4211
  ...result.error ? { errorSnippet: payload.slice(0, 500) } : {}
3926
4212
  });
4213
+ recordToolResult(sid, {
4214
+ id: call.id,
4215
+ toolName: call.toolName,
4216
+ ok: !result.error,
4217
+ durationMs: Date.now() - toolStartedAt,
4218
+ output: result.error ? void 0 : result.result,
4219
+ error: result.error
4220
+ });
3927
4221
  await this.hooks.emit("on_tool_end", {
3928
4222
  toolName: call.toolName,
3929
4223
  toolCallId: call.id,
@@ -4503,8 +4797,20 @@ var PermissionClassifier = class {
4503
4797
  const argVal = String(args[key] ?? "");
4504
4798
  if (pattern instanceof RegExp) {
4505
4799
  if (!pattern.test(argVal)) return false;
4506
- } else {
4507
- if (!argVal.includes(pattern)) return false;
4800
+ } else if (typeof pattern === "string") {
4801
+ const looksLikeRegex = /[\\^$.*+?()[\]|{}]/.test(pattern);
4802
+ if (looksLikeRegex) {
4803
+ let re;
4804
+ try {
4805
+ re = new RegExp(pattern);
4806
+ } catch {
4807
+ if (!argVal.includes(pattern)) return false;
4808
+ continue;
4809
+ }
4810
+ if (!re.test(argVal)) return false;
4811
+ } else if (!argVal.includes(pattern)) {
4812
+ return false;
4813
+ }
4508
4814
  }
4509
4815
  }
4510
4816
  }
@@ -4606,15 +4912,201 @@ var HookRegistry = class {
4606
4912
  }
4607
4913
  };
4608
4914
 
4915
+ // src/context/tool-result-storage.ts
4916
+ init_esm_shims();
4917
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
4918
+ import { dirname as dirname3, join as join5 } from "path";
4919
+ var DEFAULT_PERSIST_THRESHOLD = 5e4;
4920
+ var PER_MESSAGE_AGGREGATE_CAP = 2e5;
4921
+ var PREVIEW_SIZE = 2e3;
4922
+ var PERSISTED_OPEN = "<persisted-output>";
4923
+ var PERSISTED_CLOSE = "</persisted-output>";
4924
+ var CLEARED_PREFIX = "[Old tool result cleared";
4925
+ function createContentReplacementState() {
4926
+ return { seenIds: /* @__PURE__ */ new Set(), replacements: /* @__PURE__ */ new Map() };
4927
+ }
4928
+ function reconstructContentReplacementState(messages) {
4929
+ const state = createContentReplacementState();
4930
+ for (const msg of messages) {
4931
+ if (!Array.isArray(msg.content)) continue;
4932
+ for (const block of msg.content) {
4933
+ if (block.type !== "tool_result" || !block.tool_use_id) continue;
4934
+ state.seenIds.add(block.tool_use_id);
4935
+ if (typeof block.content === "string" && block.content.startsWith(PERSISTED_OPEN)) {
4936
+ state.replacements.set(block.tool_use_id, block.content);
4937
+ }
4938
+ }
4939
+ }
4940
+ return state;
4941
+ }
4942
+ function resolveToolResultsDir(transcriptPath) {
4943
+ return join5(dirname3(transcriptPath), "tool-results");
4944
+ }
4945
+ function ensureDir(dir) {
4946
+ try {
4947
+ mkdirSync3(dir, { recursive: true });
4948
+ } catch {
4949
+ }
4950
+ }
4951
+ function persistToFile(dir, toolUseId, content) {
4952
+ ensureDir(dir);
4953
+ const filepath = join5(dir, `${toolUseId}.txt`);
4954
+ try {
4955
+ writeFileSync3(filepath, content, { encoding: "utf-8", flag: "wx" });
4956
+ } catch (err) {
4957
+ const code = err.code;
4958
+ if (code !== "EEXIST") {
4959
+ logger.warn("tool_result.persist_failed", {
4960
+ toolUseId,
4961
+ code,
4962
+ error: err.message
4963
+ });
4964
+ throw err;
4965
+ }
4966
+ }
4967
+ return filepath;
4968
+ }
4969
+ function buildReplacement(filepath, originalSize, content) {
4970
+ const sizeKb = (originalSize / 1024).toFixed(1);
4971
+ const previewRaw = content.slice(0, PREVIEW_SIZE);
4972
+ const lastNl = previewRaw.lastIndexOf("\n");
4973
+ const preview = lastNl > PREVIEW_SIZE * 0.5 ? previewRaw.slice(0, lastNl) : previewRaw;
4974
+ const hasMore = content.length > preview.length;
4975
+ return `${PERSISTED_OPEN}
4976
+ Output too large (${sizeKb} KB). Full output saved to: ${filepath}
4977
+
4978
+ Preview (first ${preview.length} chars${hasMore ? ", truncated" : ""}):
4979
+ ` + preview + (hasMore ? "\n..." : "") + `
4980
+ ${PERSISTED_CLOSE}`;
4981
+ }
4982
+ function collectCandidates(msg) {
4983
+ if (!Array.isArray(msg.content)) return [];
4984
+ const out = [];
4985
+ for (const block of msg.content) {
4986
+ if (block.type !== "tool_result" || !block.tool_use_id) continue;
4987
+ if (typeof block.content !== "string") continue;
4988
+ if (block.content.startsWith(PERSISTED_OPEN) || block.content.startsWith(CLEARED_PREFIX))
4989
+ continue;
4990
+ out.push({
4991
+ toolUseId: block.tool_use_id,
4992
+ block,
4993
+ content: block.content,
4994
+ size: block.content.length
4995
+ });
4996
+ }
4997
+ return out;
4998
+ }
4999
+ function applyToolResultPersistence(messages, options) {
5000
+ const {
5001
+ perResultThreshold = DEFAULT_PERSIST_THRESHOLD,
5002
+ perMessageCap = PER_MESSAGE_AGGREGATE_CAP,
5003
+ toolResultsDir,
5004
+ state,
5005
+ onPersist
5006
+ } = options;
5007
+ const newReplacements = /* @__PURE__ */ new Map();
5008
+ for (const msg of messages) {
5009
+ if (msg.role !== "user") continue;
5010
+ const candidates = collectCandidates(msg);
5011
+ if (candidates.length === 0) continue;
5012
+ const fresh = [];
5013
+ for (const c of candidates) {
5014
+ if (state.replacements.has(c.toolUseId)) {
5015
+ continue;
5016
+ }
5017
+ if (state.seenIds.has(c.toolUseId)) {
5018
+ continue;
5019
+ }
5020
+ fresh.push(c);
5021
+ }
5022
+ if (fresh.length === 0) continue;
5023
+ const toPersist = /* @__PURE__ */ new Set();
5024
+ for (const c of fresh) {
5025
+ if (c.size > perResultThreshold) {
5026
+ toPersist.add(c.toolUseId);
5027
+ }
5028
+ }
5029
+ const frozenSize = candidates.filter((c) => state.seenIds.has(c.toolUseId) && !state.replacements.has(c.toolUseId)).reduce((s, c) => s + c.size, 0);
5030
+ let projected = frozenSize + fresh.reduce((s, c) => s + (toPersist.has(c.toolUseId) ? 0 : c.size), 0);
5031
+ if (projected > perMessageCap) {
5032
+ const remaining = fresh.filter((c) => !toPersist.has(c.toolUseId)).sort((a, b) => b.size - a.size);
5033
+ for (const c of remaining) {
5034
+ if (projected <= perMessageCap) break;
5035
+ toPersist.add(c.toolUseId);
5036
+ projected -= c.size;
5037
+ }
5038
+ }
5039
+ for (const c of fresh) {
5040
+ if (toPersist.has(c.toolUseId)) {
5041
+ try {
5042
+ const filepath = persistToFile(toolResultsDir, c.toolUseId, c.content);
5043
+ const replacement = buildReplacement(filepath, c.size, c.content);
5044
+ state.replacements.set(c.toolUseId, replacement);
5045
+ newReplacements.set(c.toolUseId, replacement);
5046
+ state.seenIds.add(c.toolUseId);
5047
+ onPersist?.({
5048
+ toolUseId: c.toolUseId,
5049
+ filepath,
5050
+ originalSize: c.size,
5051
+ reason: c.size > perResultThreshold ? "per-result-cap" : "per-message-budget"
5052
+ });
5053
+ } catch {
5054
+ state.seenIds.add(c.toolUseId);
5055
+ }
5056
+ } else {
5057
+ state.seenIds.add(c.toolUseId);
5058
+ }
5059
+ }
5060
+ }
5061
+ if (state.replacements.size === 0) return messages;
5062
+ let mutated = false;
5063
+ const out = messages.map((msg) => {
5064
+ if (msg.role !== "user" || !Array.isArray(msg.content)) return msg;
5065
+ let blockChanged = false;
5066
+ const newContent = msg.content.map((block) => {
5067
+ if (block.type !== "tool_result" || !block.tool_use_id) return block;
5068
+ const replacement = state.replacements.get(block.tool_use_id);
5069
+ if (replacement === void 0) return block;
5070
+ if (block.content === replacement) return block;
5071
+ if (typeof block.content === "string" && block.content.startsWith(CLEARED_PREFIX)) {
5072
+ return block;
5073
+ }
5074
+ blockChanged = true;
5075
+ return { ...block, content: replacement };
5076
+ });
5077
+ if (!blockChanged) return msg;
5078
+ mutated = true;
5079
+ return { ...msg, content: newContent };
5080
+ });
5081
+ if (mutated && newReplacements.size > 0) {
5082
+ logger.info("tool_result.persisted", {
5083
+ count: newReplacements.size,
5084
+ ids: [...newReplacements.keys()]
5085
+ });
5086
+ }
5087
+ return mutated ? out : messages;
5088
+ }
5089
+ function isPersistedReplacement(s) {
5090
+ return s.startsWith(PERSISTED_OPEN);
5091
+ }
5092
+
4609
5093
  // src/context/manager.ts
4610
5094
  init_esm_shims();
4611
5095
  init_compaction();
4612
5096
  var DEFAULT_CONFIG = {
5097
+ // 0.85 ≈ effectiveContextWindow − reserved-output buffer (CC's
5098
+ // autoCompactThreshold pattern: window − 13k buffer − 20k output budget
5099
+ // ≈ 0.83 on a 200k window, 0.97 on 1M). 0.6 was wasting 40% of any model
5100
+ // with a window over ~150k.
4613
5101
  maxTokens: 2e5,
4614
- compactAtRatio: 0.6,
4615
- summarizeAtRatio: 0.8,
4616
- maxToolResultChars: 3e4
5102
+ compactAtRatio: 0.85,
5103
+ summarizeAtRatio: 0.92,
5104
+ maxToolResultChars: 3e4,
5105
+ microcompactFloorRatio: 0.3
4617
5106
  };
5107
+ function defaultKeepRecent(maxTokens) {
5108
+ return Math.max(5, Math.floor(maxTokens / 1e5));
5109
+ }
4618
5110
  var ContextManager = class {
4619
5111
  config;
4620
5112
  toolCallHashes = /* @__PURE__ */ new Map();
@@ -4627,8 +5119,15 @@ var ContextManager = class {
4627
5119
  lastActualAtMessageCount;
4628
5120
  /** Path to session transcript — passed to summary compaction for on-demand access. */
4629
5121
  transcriptPath;
4630
- /** Notified whenever a non-trivial compaction tier fires (skips microcompact). */
5122
+ /** Notified whenever any compaction tier fires, including microcompact. */
4631
5123
  onCompact;
5124
+ /**
5125
+ * Tool-result persistence state. Created lazily once we know where to
5126
+ * write files (i.e. once setTranscriptPath has been called).
5127
+ * `null` = persistence has been explicitly disabled.
5128
+ */
5129
+ replacementState;
5130
+ toolResultsDir;
4632
5131
  constructor(config) {
4633
5132
  this.config = { ...DEFAULT_CONFIG, ...config };
4634
5133
  }
@@ -4662,10 +5161,23 @@ var ContextManager = class {
4662
5161
  this.summarizeFn = fn;
4663
5162
  }
4664
5163
  /**
4665
- * Set the transcript path so compaction can reference it.
5164
+ * Set the transcript path so compaction can reference it. The
5165
+ * tool-results directory lives alongside the transcript file:
5166
+ * <transcriptDir>/tool-results/<toolUseId>.txt
4666
5167
  */
4667
5168
  setTranscriptPath(path) {
4668
5169
  this.transcriptPath = path;
5170
+ this.toolResultsDir = resolveToolResultsDir(path);
5171
+ }
5172
+ /**
5173
+ * Initialize tool-result-persistence state from already-loaded messages.
5174
+ * Call this once per session, after setTranscriptPath, when resuming so
5175
+ * the manager re-enters the same "frozen decision" state it had on the
5176
+ * previous run. Without this, a resumed session would re-evaluate
5177
+ * every result against the current threshold and might flip choices.
5178
+ */
5179
+ initReplacementStateFromMessages(messages) {
5180
+ this.replacementState = reconstructContentReplacementState(messages);
4669
5181
  }
4670
5182
  /**
4671
5183
  * Apply progressive context management (sync path).
@@ -4673,11 +5185,36 @@ var ContextManager = class {
4673
5185
  */
4674
5186
  manage(messages) {
4675
5187
  let result = messages;
5188
+ result = this.persistLargeToolResults(result);
4676
5189
  result = this.truncateToolResults(result);
4677
5190
  result = applyToolResultBudget(result);
4678
- result = microcompact(result);
5191
+ const preTier1Tokens = this.estimateTokensHybrid(result);
5192
+ if (preTier1Tokens > this.config.maxTokens * this.config.microcompactFloorRatio) {
5193
+ const keepRecentN = this.config.microcompactKeepRecent ?? defaultKeepRecent(this.config.maxTokens);
5194
+ let clearedInfo = null;
5195
+ result = microcompact(result, {
5196
+ keepRecentN,
5197
+ onClear: (info) => {
5198
+ clearedInfo = info;
5199
+ }
5200
+ });
5201
+ if (clearedInfo) {
5202
+ const after = this.estimateTokensHybrid(result);
5203
+ logger.info("context.microcompact", {
5204
+ before: preTier1Tokens,
5205
+ after,
5206
+ keepRecentN,
5207
+ clearedRounds: clearedInfo.clearedRounds,
5208
+ toolNames: clearedInfo.toolNames
5209
+ });
5210
+ this.onCompact?.({ strategy: "micro", before: preTier1Tokens, after });
5211
+ }
5212
+ }
4679
5213
  let tokens = this.estimateTokensHybrid(result);
4680
- if (tokens > this.config.maxTokens * this.config.compactAtRatio) {
5214
+ const snipGate = this.config.maxTokens * this.config.compactAtRatio;
5215
+ const windowGate = this.config.maxTokens * (this.config.compactAtRatio + 0.05);
5216
+ const emergencyGate = this.config.maxTokens * this.config.summarizeAtRatio;
5217
+ if (tokens > snipGate) {
4681
5218
  const before = tokens;
4682
5219
  if (this.lastSummary) {
4683
5220
  const keepN = Math.max(8, Math.floor(result.length * 0.3));
@@ -4686,19 +5223,19 @@ var ContextManager = class {
4686
5223
  tokens = this.estimateTokensHybrid(result);
4687
5224
  this.onCompact?.({ strategy: "summary", before, after: tokens });
4688
5225
  } else {
4689
- const keepN = Math.max(10, Math.floor(result.length * 0.4));
4690
- result = windowCompact(result, keepN);
5226
+ result = snipCompact(result, 3, 8);
4691
5227
  tokens = this.estimateTokensHybrid(result);
4692
- this.onCompact?.({ strategy: "window", before, after: tokens });
5228
+ this.onCompact?.({ strategy: "snip", before, after: tokens });
4693
5229
  }
4694
5230
  }
4695
- if (tokens > this.config.maxTokens * 0.7) {
5231
+ if (tokens > windowGate) {
4696
5232
  const before = tokens;
4697
- result = snipCompact(result, 3, 8);
5233
+ const keepN = Math.max(10, Math.floor(result.length * 0.4));
5234
+ result = windowCompact(result, keepN);
4698
5235
  tokens = this.estimateTokensHybrid(result);
4699
- this.onCompact?.({ strategy: "snip", before, after: tokens });
5236
+ this.onCompact?.({ strategy: "window", before, after: tokens });
4700
5237
  }
4701
- if (tokens > this.config.maxTokens * this.config.summarizeAtRatio) {
5238
+ if (tokens > emergencyGate) {
4702
5239
  const before = tokens;
4703
5240
  result = windowCompact(result, 6);
4704
5241
  tokens = this.estimateTokensHybrid(result);
@@ -4712,9 +5249,31 @@ var ContextManager = class {
4712
5249
  */
4713
5250
  async manageAsync(messages) {
4714
5251
  let result = messages;
5252
+ result = this.persistLargeToolResults(result);
4715
5253
  result = this.truncateToolResults(result);
4716
5254
  result = applyToolResultBudget(result);
4717
- result = microcompact(result);
5255
+ const preTier1Tokens = this.estimateTokensHybrid(result);
5256
+ if (preTier1Tokens > this.config.maxTokens * this.config.microcompactFloorRatio) {
5257
+ const keepRecentN = this.config.microcompactKeepRecent ?? defaultKeepRecent(this.config.maxTokens);
5258
+ let clearedInfo = null;
5259
+ result = microcompact(result, {
5260
+ keepRecentN,
5261
+ onClear: (info) => {
5262
+ clearedInfo = info;
5263
+ }
5264
+ });
5265
+ if (clearedInfo) {
5266
+ const after = this.estimateTokensHybrid(result);
5267
+ logger.info("context.microcompact", {
5268
+ before: preTier1Tokens,
5269
+ after,
5270
+ keepRecentN,
5271
+ clearedRounds: clearedInfo.clearedRounds,
5272
+ toolNames: clearedInfo.toolNames
5273
+ });
5274
+ this.onCompact?.({ strategy: "micro", before: preTier1Tokens, after });
5275
+ }
5276
+ }
4718
5277
  const tokens = this.estimateTokensHybrid(result);
4719
5278
  const ratio = tokens / this.config.maxTokens;
4720
5279
  if (ratio >= this.config.compactAtRatio && this.summarizeFn && this.consecutiveSummaryFailures < 3) {
@@ -4747,20 +5306,23 @@ var ContextManager = class {
4747
5306
  }
4748
5307
  }
4749
5308
  let live = tokens;
4750
- if (live > this.config.maxTokens * this.config.compactAtRatio) {
5309
+ const snipGate = this.config.maxTokens * this.config.compactAtRatio;
5310
+ const windowGate = this.config.maxTokens * (this.config.compactAtRatio + 0.05);
5311
+ const emergencyGate = this.config.maxTokens * this.config.summarizeAtRatio;
5312
+ if (live > snipGate) {
4751
5313
  const before = live;
4752
- const keepN = Math.max(10, Math.floor(result.length * 0.4));
4753
- result = windowCompact(result, keepN);
5314
+ result = snipCompact(result, 3, 8);
4754
5315
  live = this.estimateTokensHybrid(result);
4755
- this.onCompact?.({ strategy: "window", before, after: live });
5316
+ this.onCompact?.({ strategy: "snip", before, after: live });
4756
5317
  }
4757
- if (live > this.config.maxTokens * 0.7) {
5318
+ if (live > windowGate) {
4758
5319
  const before = live;
4759
- result = snipCompact(result, 3, 8);
5320
+ const keepN = Math.max(10, Math.floor(result.length * 0.4));
5321
+ result = windowCompact(result, keepN);
4760
5322
  live = this.estimateTokensHybrid(result);
4761
- this.onCompact?.({ strategy: "snip", before, after: live });
5323
+ this.onCompact?.({ strategy: "window", before, after: live });
4762
5324
  }
4763
- if (live > this.config.maxTokens * this.config.summarizeAtRatio) {
5325
+ if (live > emergencyGate) {
4764
5326
  const before = live;
4765
5327
  result = windowCompact(result, 6);
4766
5328
  live = this.estimateTokensHybrid(result);
@@ -4768,6 +5330,24 @@ var ContextManager = class {
4768
5330
  }
4769
5331
  return result;
4770
5332
  }
5333
+ /**
5334
+ * Persist large tool_result blocks to disk and replace them with a
5335
+ * preview + filepath. No-op when transcript path hasn't been set
5336
+ * (e.g. unit tests that exercise compaction directly without a session).
5337
+ */
5338
+ persistLargeToolResults(messages) {
5339
+ if (!this.toolResultsDir) return messages;
5340
+ if (!this.replacementState) {
5341
+ this.replacementState = createContentReplacementState();
5342
+ }
5343
+ return applyToolResultPersistence(messages, {
5344
+ toolResultsDir: this.toolResultsDir,
5345
+ state: this.replacementState,
5346
+ onPersist: (info) => {
5347
+ logger.info("context.tool_result_persisted", info);
5348
+ }
5349
+ });
5350
+ }
4771
5351
  /**
4772
5352
  * Truncate oversized tool results in messages.
4773
5353
  */
@@ -4797,7 +5377,7 @@ var ContextManager = class {
4797
5377
  shouldReactiveCompact(messages, currentResponseTokens) {
4798
5378
  const msgTokens = this.estimateTokensHybrid(messages);
4799
5379
  const total = msgTokens + currentResponseTokens;
4800
- return total > this.config.maxTokens * 0.9;
5380
+ return total > this.config.maxTokens * this.config.summarizeAtRatio;
4801
5381
  }
4802
5382
  /**
4803
5383
  * Check if context is approaching limits.
@@ -4879,8 +5459,8 @@ var SectionCache = class {
4879
5459
 
4880
5460
  // src/prompt/instruction-scanner.ts
4881
5461
  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";
5462
+ import { readFileSync as readFileSync4, existsSync as existsSync9, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
5463
+ import { join as join6, dirname as dirname4, resolve as resolve4 } from "path";
4884
5464
  import { execSync as execSync5 } from "child_process";
4885
5465
  import { homedir as homedir2 } from "os";
4886
5466
  function scanInstructions(cwd, options = {}) {
@@ -4889,32 +5469,32 @@ function scanInstructions(cwd, options = {}) {
4889
5469
  const allNames = [fileName, ...compatNames];
4890
5470
  const configDirs = [".codeshell", ...options.scanDirs ?? [], ".claude"];
4891
5471
  const entries = [];
4892
- const homeConfigDir = join4(homedir2(), ".code-shell");
4893
- const homeCompatDir = join4(homedir2(), ".claude");
5472
+ const homeConfigDir = join6(homedir2(), ".code-shell");
5473
+ const homeCompatDir = join6(homedir2(), ".claude");
4894
5474
  for (const name of allNames) {
4895
- tryAddFile(join4(homeConfigDir, name), "user", -1, entries);
5475
+ tryAddFile(join6(homeConfigDir, name), "user", -1, entries);
4896
5476
  }
4897
- tryAddRulesDir(join4(homeConfigDir, "rules"), "user", -1, entries);
5477
+ tryAddRulesDir(join6(homeConfigDir, "rules"), "user", -1, entries);
4898
5478
  for (const name of compatNames) {
4899
- tryAddFile(join4(homeCompatDir, name), "user", -1, entries);
5479
+ tryAddFile(join6(homeCompatDir, name), "user", -1, entries);
4900
5480
  }
4901
- tryAddRulesDir(join4(homeCompatDir, "rules"), "user", -1, entries);
5481
+ tryAddRulesDir(join6(homeCompatDir, "rules"), "user", -1, entries);
4902
5482
  const ceiling = options.ignoreGitBoundary ? "/" : findGitRoot2(cwd) ?? "/";
4903
5483
  const dirs = collectDirsDownward(resolve4(cwd), ceiling);
4904
5484
  for (let depth = 0; depth < dirs.length; depth++) {
4905
5485
  const dir = dirs[depth];
4906
5486
  for (const name of allNames) {
4907
- tryAddFile(join4(dir, name), "project", depth, entries);
5487
+ tryAddFile(join6(dir, name), "project", depth, entries);
4908
5488
  }
4909
5489
  for (const cfgDir of configDirs) {
4910
5490
  for (const name of allNames) {
4911
- tryAddFile(join4(dir, cfgDir, name), "project", depth, entries);
5491
+ tryAddFile(join6(dir, cfgDir, name), "project", depth, entries);
4912
5492
  }
4913
- tryAddRulesDir(join4(dir, cfgDir, "rules"), "project", depth, entries);
5493
+ tryAddRulesDir(join6(dir, cfgDir, "rules"), "project", depth, entries);
4914
5494
  }
4915
5495
  for (const name of allNames) {
4916
5496
  const localName = name.replace(/\.md$/, ".local.md");
4917
- tryAddFile(join4(dir, localName), "local", depth, entries);
5497
+ tryAddFile(join6(dir, localName), "local", depth, entries);
4918
5498
  }
4919
5499
  }
4920
5500
  return dedup(entries);
@@ -4929,9 +5509,9 @@ ${e.content}`;
4929
5509
  }).join("\n\n---\n\n");
4930
5510
  }
4931
5511
  function tryAddFile(path, source, depth, entries) {
4932
- if (!existsSync8(path)) return;
5512
+ if (!existsSync9(path)) return;
4933
5513
  try {
4934
- const stat5 = statSync(path);
5514
+ const stat5 = statSync2(path);
4935
5515
  if (!stat5.isFile()) return;
4936
5516
  const content = readFileSync4(path, "utf-8").trim();
4937
5517
  if (content) {
@@ -4941,11 +5521,11 @@ function tryAddFile(path, source, depth, entries) {
4941
5521
  }
4942
5522
  }
4943
5523
  function tryAddRulesDir(dir, source, depth, entries) {
4944
- if (!existsSync8(dir)) return;
5524
+ if (!existsSync9(dir)) return;
4945
5525
  try {
4946
- const files = readdirSync3(dir).filter((f) => f.endsWith(".md")).sort();
5526
+ const files = readdirSync4(dir).filter((f) => f.endsWith(".md")).sort();
4947
5527
  for (const file of files) {
4948
- tryAddFile(join4(dir, file), source, depth, entries);
5528
+ tryAddFile(join6(dir, file), source, depth, entries);
4949
5529
  }
4950
5530
  } catch {
4951
5531
  }
@@ -4970,7 +5550,7 @@ function collectDirsDownward(target, ceiling) {
4970
5550
  while (true) {
4971
5551
  dirs.push(dir);
4972
5552
  if (dir === resolvedCeiling) break;
4973
- const parent = dirname3(dir);
5553
+ const parent = dirname4(dir);
4974
5554
  if (parent === dir) break;
4975
5555
  dir = parent;
4976
5556
  }
@@ -4999,28 +5579,28 @@ function sourceLabel(entry) {
4999
5579
 
5000
5580
  // src/skills/scanner.ts
5001
5581
  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";
5582
+ import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
5583
+ import { join as join7, resolve as resolve5 } from "path";
5004
5584
  import { homedir as homedir3 } from "os";
5005
5585
  function scanSkills(cwd) {
5006
5586
  const skills = [];
5007
5587
  const seen = /* @__PURE__ */ new Set();
5008
5588
  const dirs = [
5009
5589
  // Project-level
5010
- join5(cwd, ".code-shell", "skills"),
5011
- join5(cwd, ".claude", "skills"),
5590
+ join7(cwd, ".code-shell", "skills"),
5591
+ join7(cwd, ".claude", "skills"),
5012
5592
  // User-level
5013
- join5(homedir3(), ".code-shell", "skills"),
5014
- join5(homedir3(), ".claude", "skills")
5593
+ join7(homedir3(), ".code-shell", "skills"),
5594
+ join7(homedir3(), ".claude", "skills")
5015
5595
  ];
5016
5596
  for (const dir of dirs) {
5017
- if (!existsSync9(dir)) continue;
5597
+ if (!existsSync10(dir)) continue;
5018
5598
  try {
5019
- const files = readdirSync4(dir).filter(
5020
- (f) => f.endsWith(".md") && statSync2(join5(dir, f)).isFile()
5599
+ const files = readdirSync5(dir).filter(
5600
+ (f) => f.endsWith(".md") && statSync3(join7(dir, f)).isFile()
5021
5601
  );
5022
5602
  for (const file of files) {
5023
- const filePath = resolve5(join5(dir, file));
5603
+ const filePath = resolve5(join7(dir, file));
5024
5604
  if (seen.has(filePath)) continue;
5025
5605
  seen.add(filePath);
5026
5606
  const skill = parseSkillFile(filePath);
@@ -5137,6 +5717,13 @@ The default job in this preset is software engineering in the current working di
5137
5717
  - 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
5718
  - Read code before editing it, prefer modifying existing files, and avoid unrelated refactors.
5139
5719
 
5720
+ ## Investigation has a budget \u2014 looking is not acting
5721
+ - 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.
5722
+ - 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.
5723
+ - 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.
5724
+ - 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.
5725
+ - 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.
5726
+
5140
5727
  ## Code quality discipline
5141
5728
  - 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
5729
  - 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 +5745,7 @@ The default job in this preset is software engineering in the current working di
5158
5745
  // src/prompt/sections/tone.md
5159
5746
  var tone_default = `# Tone and output
5160
5747
  - Only use emojis if the user explicitly requests it.
5748
+ - Match the language of the user's latest message in your reply; keep code, paths, and identifiers in their original form.
5161
5749
  - Be concise, direct, and collaborative. Your responses should be short.
5162
5750
  - 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
5751
  - 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 +6068,110 @@ ${toolLines.join("\n\n")}`;
5480
6068
  // src/engine/engine.ts
5481
6069
  init_esm_shims();
5482
6070
 
6071
+ // src/tool-system/investigation-guard.ts
6072
+ init_esm_shims();
6073
+ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["Read", "Grep", "Glob", "WebFetch", "WebSearch", "ToolSearch"]);
6074
+ var READ_BUDGET = 3;
6075
+ var SILENT_TURN_BUDGET = 3;
6076
+ var DEDUPE_OFFSET_BUCKET = 50;
6077
+ var READ_HISTORY_MAX = 512;
6078
+ var InvestigationGuard = class {
6079
+ readHistory = /* @__PURE__ */ new Map();
6080
+ consecutiveReads = 0;
6081
+ silentTurns = 0;
6082
+ turnHasText = false;
6083
+ turnHasSideEffect = false;
6084
+ lastReminderSilent = -1;
6085
+ preToolCheck(call) {
6086
+ const tool = call.toolName;
6087
+ if (!READ_ONLY_TOOLS.has(tool)) {
6088
+ if (this.isMutatingTool(tool)) {
6089
+ this.consecutiveReads = 0;
6090
+ this.turnHasSideEffect = true;
6091
+ }
6092
+ return void 0;
6093
+ }
6094
+ const key = this.buildKey(call);
6095
+ if (!key) return void 0;
6096
+ const hits = (this.readHistory.get(key.signature) ?? 0) + 1;
6097
+ this.readHistory.set(key.signature, hits);
6098
+ if (this.readHistory.size > READ_HISTORY_MAX) {
6099
+ const oldest = this.readHistory.keys().next().value;
6100
+ if (oldest !== void 0) this.readHistory.delete(oldest);
6101
+ }
6102
+ this.consecutiveReads += 1;
6103
+ const prependParts = [];
6104
+ if (hits >= 3) {
6105
+ return {
6106
+ block: `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.`
6107
+ };
6108
+ }
6109
+ if (hits === 2) {
6110
+ prependParts.push(
6111
+ `<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>`
6112
+ );
6113
+ }
6114
+ if (this.consecutiveReads > READ_BUDGET) {
6115
+ prependParts.push(
6116
+ `<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>`
6117
+ );
6118
+ }
6119
+ return prependParts.length ? { prepend: prependParts.join("\n") } : void 0;
6120
+ }
6121
+ noteText(text) {
6122
+ if (text && text.trim().length > 0) this.turnHasText = true;
6123
+ }
6124
+ noteToolResult(_call, _result) {
6125
+ }
6126
+ turnEnded(turnNumber) {
6127
+ if (!this.turnHasText && !this.turnHasSideEffect) {
6128
+ this.silentTurns += 1;
6129
+ } else {
6130
+ this.silentTurns = 0;
6131
+ }
6132
+ const wasSilent = this.silentTurns;
6133
+ this.turnHasText = false;
6134
+ this.turnHasSideEffect = false;
6135
+ if (wasSilent >= SILENT_TURN_BUDGET && turnNumber !== this.lastReminderSilent) {
6136
+ this.lastReminderSilent = turnNumber;
6137
+ 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>`;
6138
+ }
6139
+ return void 0;
6140
+ }
6141
+ isMutatingTool(name) {
6142
+ if (name === "Bash") return true;
6143
+ if (name === "Edit" || name === "Write" || name === "NotebookEdit") return true;
6144
+ if (name === "AskUserQuestion") return true;
6145
+ if (name === "TaskCreate" || name === "TaskUpdate") return false;
6146
+ return false;
6147
+ }
6148
+ buildKey(call) {
6149
+ const tool = call.toolName;
6150
+ const args = call.args ?? {};
6151
+ if (tool === "Read") {
6152
+ const fp = typeof args.file_path === "string" ? args.file_path : "";
6153
+ if (!fp) return void 0;
6154
+ const off = typeof args.offset === "number" ? args.offset : 0;
6155
+ const bucket = Math.floor(off / DEDUPE_OFFSET_BUCKET);
6156
+ return { tool, signature: `Read::${fp}::${bucket}` };
6157
+ }
6158
+ if (tool === "Grep") {
6159
+ const pat = typeof args.pattern === "string" ? args.pattern : "";
6160
+ const path = typeof args.path === "string" ? args.path : "";
6161
+ const glob2 = typeof args.glob === "string" ? args.glob : "";
6162
+ if (!pat) return void 0;
6163
+ return { tool, signature: `Grep::${pat}::${path}::${glob2}` };
6164
+ }
6165
+ if (tool === "Glob") {
6166
+ const pat = typeof args.pattern === "string" ? args.pattern : "";
6167
+ const path = typeof args.path === "string" ? args.path : "";
6168
+ if (!pat) return void 0;
6169
+ return { tool, signature: `Glob::${pat}::${path}` };
6170
+ }
6171
+ return void 0;
6172
+ }
6173
+ };
6174
+
5483
6175
  // src/engine/model-facade.ts
5484
6176
  init_esm_shims();
5485
6177
 
@@ -5521,6 +6213,11 @@ function addOutputTokens(n) {
5521
6213
  }
5522
6214
 
5523
6215
  // src/engine/model-facade.ts
6216
+ var _reqSeq = 0;
6217
+ function nextReqId() {
6218
+ _reqSeq += 1;
6219
+ return `r${_reqSeq.toString(36)}`;
6220
+ }
5524
6221
  var ModelFacade = class {
5525
6222
  constructor(client, transcript) {
5526
6223
  this.client = client;
@@ -5529,34 +6226,54 @@ var ModelFacade = class {
5529
6226
  async call(systemPrompt, messages, tools, onStream, signal) {
5530
6227
  const startMs = Date.now();
5531
6228
  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
- }
6229
+ const sid = getCurrentSid();
6230
+ const reqId = nextReqId();
6231
+ recordLLMRequest(
6232
+ sid,
6233
+ {
6234
+ provider: this.client.provider ?? "?",
6235
+ model: this.client.model ?? "?",
6236
+ stream: true,
6237
+ messages,
6238
+ tools,
6239
+ systemPrompt
5557
6240
  },
5558
- signal
5559
- });
6241
+ reqId
6242
+ );
6243
+ let response;
6244
+ try {
6245
+ response = await this.client.createMessage({
6246
+ systemPrompt,
6247
+ messages,
6248
+ tools,
6249
+ stream: true,
6250
+ onChunk: (chunk) => {
6251
+ if (!onStream) return;
6252
+ if (chunk.type === "text" && chunk.text) {
6253
+ onStream({ type: "text_delta", text: chunk.text });
6254
+ } else if (chunk.type === "tool_use_start" && chunk.toolCall) {
6255
+ onStream({
6256
+ type: "tool_use_start",
6257
+ toolCall: {
6258
+ id: chunk.toolCall.id ?? "",
6259
+ toolName: chunk.toolCall.toolName ?? "",
6260
+ args: chunk.toolCall.args ?? {}
6261
+ }
6262
+ });
6263
+ } else if (chunk.type === "tool_use_delta" && chunk.toolCall?.id) {
6264
+ onStream({
6265
+ type: "tool_use_args_delta",
6266
+ toolCallId: chunk.toolCall.id,
6267
+ args: chunk.toolCall.args ?? {}
6268
+ });
6269
+ }
6270
+ },
6271
+ signal
6272
+ });
6273
+ } catch (err) {
6274
+ recordLLMError(sid, reqId, err, Date.now() - startMs);
6275
+ throw err;
6276
+ }
5560
6277
  const latencyMs = Date.now() - startMs;
5561
6278
  logger.info("llm.request", {
5562
6279
  stream: true,
@@ -5567,6 +6284,17 @@ var ModelFacade = class {
5567
6284
  textLen: response.text?.length ?? 0,
5568
6285
  usage: response.usage
5569
6286
  });
6287
+ recordLLMResponse(
6288
+ sid,
6289
+ {
6290
+ text: response.text,
6291
+ toolCalls: response.toolCalls,
6292
+ stopReason: response.stopReason,
6293
+ usage: response.usage,
6294
+ durationMs: latencyMs
6295
+ },
6296
+ reqId
6297
+ );
5570
6298
  this.recordUsage(response, latencyMs);
5571
6299
  this.recordResponse(response);
5572
6300
  return response;
@@ -5577,13 +6305,33 @@ var ModelFacade = class {
5577
6305
  async callWithoutStreaming(systemPrompt, messages, tools, signal) {
5578
6306
  const startMs = Date.now();
5579
6307
  const msgCount = messages.length;
5580
- const response = await this.client.createMessage({
5581
- systemPrompt,
5582
- messages,
5583
- tools,
5584
- stream: false,
5585
- signal
5586
- });
6308
+ const sid = getCurrentSid();
6309
+ const reqId = nextReqId();
6310
+ recordLLMRequest(
6311
+ sid,
6312
+ {
6313
+ provider: this.client.provider ?? "?",
6314
+ model: this.client.model ?? "?",
6315
+ stream: false,
6316
+ messages,
6317
+ tools,
6318
+ systemPrompt
6319
+ },
6320
+ reqId
6321
+ );
6322
+ let response;
6323
+ try {
6324
+ response = await this.client.createMessage({
6325
+ systemPrompt,
6326
+ messages,
6327
+ tools,
6328
+ stream: false,
6329
+ signal
6330
+ });
6331
+ } catch (err) {
6332
+ recordLLMError(sid, reqId, err, Date.now() - startMs);
6333
+ throw err;
6334
+ }
5587
6335
  const latencyMs = Date.now() - startMs;
5588
6336
  logger.info("llm.request", {
5589
6337
  stream: false,
@@ -5594,6 +6342,17 @@ var ModelFacade = class {
5594
6342
  textLen: response.text?.length ?? 0,
5595
6343
  usage: response.usage
5596
6344
  });
6345
+ recordLLMResponse(
6346
+ sid,
6347
+ {
6348
+ text: response.text,
6349
+ toolCalls: response.toolCalls,
6350
+ stopReason: response.stopReason,
6351
+ usage: response.usage,
6352
+ durationMs: latencyMs
6353
+ },
6354
+ reqId
6355
+ );
5597
6356
  this.recordUsage(response, latencyMs);
5598
6357
  this.recordResponse(response);
5599
6358
  return response;
@@ -5781,7 +6540,7 @@ var TurnLoop = class {
5781
6540
  response = await this.callModelWithFallback(messages);
5782
6541
  } catch (err) {
5783
6542
  if (err instanceof ContextLimitError) {
5784
- const { dropOldestRounds } = await import("./compaction-YNWCEF7B.js");
6543
+ const { dropOldestRounds } = await import("./compaction-6VU2T643.js");
5785
6544
  let recovered = false;
5786
6545
  for (let retry = 1; retry <= 3; retry++) {
5787
6546
  tlog.warn("turn.ptl_recovery", { cat: "turn", retry, roundsToDrop: retry });
@@ -5811,6 +6570,12 @@ var TurnLoop = class {
5811
6570
  return { text: finalText, reason: "model_error", messages };
5812
6571
  }
5813
6572
  }
6573
+ if (response.usage?.promptTokens !== void 0) {
6574
+ this.config.onStream?.({
6575
+ type: "usage_update",
6576
+ promptTokens: response.usage.promptTokens
6577
+ });
6578
+ }
5814
6579
  if (response.usage?.promptTokens !== void 0) {
5815
6580
  this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length);
5816
6581
  }
@@ -5845,6 +6610,12 @@ var TurnLoop = class {
5845
6610
  }
5846
6611
  response = { ...response, text: combinedText };
5847
6612
  }
6613
+ if (response.usage?.promptTokens !== void 0) {
6614
+ this.config.onStream?.({
6615
+ type: "usage_update",
6616
+ promptTokens: response.usage.promptTokens
6617
+ });
6618
+ }
5848
6619
  if (this.config.signal?.aborted) {
5849
6620
  return { text: finalText, reason: "aborted_streaming", messages };
5850
6621
  }
@@ -5903,7 +6674,7 @@ var TurnLoop = class {
5903
6674
  this.config.onStream?.({ type: "tool_result", result });
5904
6675
  }
5905
6676
  if (this.config.onStream) {
5906
- import("./tool-summary-2RUFL5QK.js").then(({ generateToolUseSummary }) => {
6677
+ import("./tool-summary-NP5CDBKG.js").then(({ generateToolUseSummary }) => {
5907
6678
  if (!this.deps.model.summarize) return;
5908
6679
  generateToolUseSummary(toolCalls, results, this.deps.model.summarize).then((summary) => {
5909
6680
  if (summary) {
@@ -5938,12 +6709,22 @@ var TurnLoop = class {
5938
6709
  content: "<system-reminder>You are approaching the token budget limit. Please start wrapping up your work and provide a summary.</system-reminder>"
5939
6710
  });
5940
6711
  }
6712
+ const guard = this.deps.toolExecutor.getInvestigationGuard();
6713
+ if (guard) {
6714
+ guard.noteText(response.text);
6715
+ const turnReminder = guard.turnEnded(this.turnCount);
6716
+ if (turnReminder) {
6717
+ messages.push({ role: "user", content: turnReminder });
6718
+ tlog.info("guard.silent_turn", { cat: "guard", turn: this.turnCount });
6719
+ }
6720
+ }
5941
6721
  await this.deps.hooks.emit("on_turn_end", {
5942
6722
  turnNumber: this.turnCount,
5943
6723
  hasToolUse: true,
5944
6724
  toolCallCount: toolCalls.length
5945
6725
  });
5946
6726
  this.deps.transcript.appendTurnBoundary();
6727
+ this.config.onTurnBoundary?.(this.turnCount);
5947
6728
  tlog.info("turn.end", {
5948
6729
  cat: "turn",
5949
6730
  duration_ms: Date.now() - turnStartedAt,
@@ -6112,9 +6893,83 @@ var TurnLoop = class {
6112
6893
  }
6113
6894
  };
6114
6895
 
6896
+ // src/llm/provider-catalog.ts
6897
+ init_esm_shims();
6898
+ var ProviderCatalog = class {
6899
+ byKey = /* @__PURE__ */ new Map();
6900
+ constructor(entries) {
6901
+ for (const e of entries ?? []) this.byKey.set(e.key, e);
6902
+ }
6903
+ list() {
6904
+ return [...this.byKey.values()];
6905
+ }
6906
+ get(key) {
6907
+ return this.byKey.get(key);
6908
+ }
6909
+ has(key) {
6910
+ return this.byKey.has(key);
6911
+ }
6912
+ add(entry) {
6913
+ if (this.byKey.has(entry.key)) {
6914
+ throw new Error(`duplicate provider key: ${entry.key}`);
6915
+ }
6916
+ this.byKey.set(entry.key, entry);
6917
+ }
6918
+ update(key, patch) {
6919
+ const cur = this.byKey.get(key);
6920
+ if (!cur) throw new Error(`no such provider: ${key}`);
6921
+ this.byKey.set(key, { ...cur, ...patch, key: cur.key });
6922
+ }
6923
+ remove(key, opts) {
6924
+ if (opts.referencedBy.length) {
6925
+ throw new Error(
6926
+ `provider ${key} still referenced by models: ${opts.referencedBy.join(", ")}`
6927
+ );
6928
+ }
6929
+ this.byKey.delete(key);
6930
+ }
6931
+ /** Compute an unused key for a new provider, given a desired base. */
6932
+ deriveKey(base) {
6933
+ if (!this.byKey.has(base)) return base;
6934
+ for (let i = 2; ; i++) {
6935
+ const candidate = `${base}-${i}`;
6936
+ if (!this.byKey.has(candidate)) return candidate;
6937
+ }
6938
+ }
6939
+ };
6940
+
6941
+ // src/utils/task-sanitizer.ts
6942
+ init_esm_shims();
6943
+ var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
6944
+ var BOX_RE = /[─━│┃┌┐└┘├┤┬┴┼╔╗╚╝═║╠╣╦╩╬▀▄█▌▐░▒▓·]+/g;
6945
+ function detectPastedNoise(task) {
6946
+ const original = task.length;
6947
+ if (original < 10) return { isNoise: false, reason: "short", cleaned: task };
6948
+ const stripped = task.replace(ANSI_RE, "").replace(BOX_RE, "").replace(/\r/g, "\n").trim();
6949
+ if (stripped.length === 0) {
6950
+ return { isNoise: true, reason: "all ANSI/box chars", cleaned: stripped };
6951
+ }
6952
+ const signalRatio = stripped.length / original;
6953
+ if (signalRatio < 0.3 && original > 100) {
6954
+ return {
6955
+ isNoise: true,
6956
+ reason: `signal/noise ratio ${(signalRatio * 100).toFixed(0)}%`,
6957
+ cleaned: stripped
6958
+ };
6959
+ }
6960
+ return { isNoise: false, reason: "ok", cleaned: stripped };
6961
+ }
6962
+
6115
6963
  // src/engine/engine.ts
6116
- import { join as join6 } from "path";
6964
+ import { join as join8 } from "path";
6117
6965
  import { homedir as homedir4 } from "os";
6966
+ import {
6967
+ existsSync as existsSync11,
6968
+ mkdirSync as mkdirSync4,
6969
+ readFileSync as readFileSync6,
6970
+ renameSync,
6971
+ writeFileSync as writeFileSync4
6972
+ } from "fs";
6118
6973
  var Engine = class _Engine {
6119
6974
  constructor(config) {
6120
6975
  this.config = config;
@@ -6132,28 +6987,7 @@ var Engine = class _Engine {
6132
6987
  }
6133
6988
  this.sessionManager = new SessionManager(config.sessionStorageDir);
6134
6989
  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
- }
6990
+ this.populateModelPoolFromSettings();
6157
6991
  }
6158
6992
  preset;
6159
6993
  toolRegistry;
@@ -6172,10 +7006,99 @@ var Engine = class _Engine {
6172
7006
  lastSessionId;
6173
7007
  compactedMessagesBySession = /* @__PURE__ */ new Map();
6174
7008
  activePermission;
7009
+ /** Public accessor so UI/clients can read the resolved per-model window. */
7010
+ get maxContextTokens() {
7011
+ return this.resolveMaxContextTokens();
7012
+ }
6175
7013
  resolveMaxContextTokens() {
6176
7014
  const modelEntry = this.modelPool.get();
6177
7015
  return modelEntry?.maxContextTokens ?? this.config.maxContextTokens ?? 2e5;
6178
7016
  }
7017
+ /**
7018
+ * Load models[] / providers[] from settings into the active ModelPool and
7019
+ * resync this.config.llm with the matching entry. Called from the ctor and
7020
+ * from reloadModelPool() (e.g. after onboarding writes new entries to disk).
7021
+ */
7022
+ populateModelPoolFromSettings() {
7023
+ try {
7024
+ const sm = this.getSettingsManager();
7025
+ sm.invalidate();
7026
+ const settings = sm.get();
7027
+ if (settings.models?.length) {
7028
+ for (const m of settings.models) {
7029
+ this.modelPool.register({
7030
+ key: m.key,
7031
+ label: m.label,
7032
+ provider: m.provider ?? "",
7033
+ model: m.model,
7034
+ baseUrl: m.baseUrl,
7035
+ apiKey: m.apiKey,
7036
+ maxOutputTokens: m.maxOutputTokens,
7037
+ maxContextTokens: m.maxContextTokens,
7038
+ providerKey: m.providerKey
7039
+ });
7040
+ }
7041
+ if (settings.providers?.length) {
7042
+ this.modelPool.setProviderCatalog(
7043
+ new ProviderCatalog(settings.providers)
7044
+ );
7045
+ }
7046
+ this.modelPool.setCacheDir(defaultCacheDir());
7047
+ this.modelPool.reloadCachedContextWindows();
7048
+ const activeKey = settings.activeKey;
7049
+ let match;
7050
+ if (activeKey) {
7051
+ match = settings.models.find((m) => m.key === activeKey);
7052
+ }
7053
+ if (!match) {
7054
+ const currentModel = this.config.llm.model;
7055
+ match = settings.models.find(
7056
+ (m) => m.model === currentModel || currentModel && m.model?.endsWith(`/${currentModel}`)
7057
+ );
7058
+ }
7059
+ if (match) {
7060
+ const entry = this.modelPool.switch(match.key);
7061
+ this.config = {
7062
+ ...this.config,
7063
+ llm: this.modelPool.toLLMConfig(entry, this.config.llm)
7064
+ };
7065
+ }
7066
+ } else if (this.config.llm.apiKey) {
7067
+ this.autoPopulatePool(this.config.llm.apiKey, this.config.llm.baseUrl);
7068
+ }
7069
+ } catch {
7070
+ }
7071
+ }
7072
+ /**
7073
+ * Re-read settings and refresh the model pool. Used after onboarding /login
7074
+ * writes new providers[] / models[] to disk so the running engine picks them
7075
+ * up without a process restart. Existing pool entries are kept (re-registering
7076
+ * the same key overwrites them), so callers don't need to clear first.
7077
+ */
7078
+ reloadModelPool() {
7079
+ this.populateModelPoolFromSettings();
7080
+ }
7081
+ /**
7082
+ * Auto-populate the model pool when settings.models[] is empty but
7083
+ * the user has configured an API key. Detects the provider from the
7084
+ * key prefix / baseUrl and registers all its known models.
7085
+ */
7086
+ autoPopulatePool(apiKey, baseUrl) {
7087
+ const provider = detectProviderFromApiKey(apiKey, baseUrl);
7088
+ if (!provider) return;
7089
+ const entries = buildModelPool(provider, apiKey);
7090
+ for (const e of entries) {
7091
+ this.modelPool.register(e);
7092
+ }
7093
+ const defaultEntry = provider.defaultModel ? entries.find((e) => e.model === provider.defaultModel) : entries[0];
7094
+ if (defaultEntry) {
7095
+ const entry = this.modelPool.switch(defaultEntry.key);
7096
+ this.config = {
7097
+ ...this.config,
7098
+ llm: this.modelPool.toLLMConfig(entry, this.config.llm)
7099
+ };
7100
+ }
7101
+ }
6179
7102
  /**
6180
7103
  * Register a custom tool (from product adapter) into the tool registry.
6181
7104
  * Must be called before run().
@@ -6196,6 +7119,20 @@ var Engine = class _Engine {
6196
7119
  */
6197
7120
  async run(task, options) {
6198
7121
  const cwd = this.config.cwd ?? process.cwd();
7122
+ const noise = detectPastedNoise(task);
7123
+ if (noise.isNoise) {
7124
+ 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 ? `
7125
+
7126
+ Extracted text: ${noise.cleaned.slice(0, 200)}` : "");
7127
+ logger.info("engine.run.rejected", { reason: noise.reason, len: task.length });
7128
+ return {
7129
+ text: hint,
7130
+ reason: "completed",
7131
+ sessionId: options?.sessionId ?? "noise-rejected",
7132
+ turnCount: 0,
7133
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 }
7134
+ };
7135
+ }
6199
7136
  const subAgentSpawner = {
6200
7137
  parentStream: options?.onStream,
6201
7138
  describe: () => ({
@@ -6247,6 +7184,7 @@ var Engine = class _Engine {
6247
7184
  const userMsg = { role: "user", content: task };
6248
7185
  messages.push(userMsg);
6249
7186
  session.transcript.appendMessage("user", task);
7187
+ this.sessionManager.saveState(session.state);
6250
7188
  } else {
6251
7189
  session = this.sessionManager.create(cwd, this.config.llm.model, this.config.llm.provider);
6252
7190
  messages = [{ role: "user", content: task }];
@@ -6255,6 +7193,14 @@ var Engine = class _Engine {
6255
7193
  this.sessionManager.saveState(session.state);
6256
7194
  }
6257
7195
  logger.setSid(session.state.sessionId);
7196
+ recordSessionStart(session.state.sessionId, {
7197
+ task,
7198
+ cwd,
7199
+ model: this.config.llm.model,
7200
+ provider: this.config.llm.provider,
7201
+ permissionMode: this.config.permissionMode ?? "acceptEdits",
7202
+ resumed: !!options?.sessionId
7203
+ });
6258
7204
  options?.onStream?.({ type: "session_started", sessionId: session.state.sessionId });
6259
7205
  const llmClientPromise = createLLMClient(this.config.llm);
6260
7206
  const mode = this.config.permissionMode ?? "acceptEdits";
@@ -6268,6 +7214,8 @@ var Engine = class _Engine {
6268
7214
  });
6269
7215
  }
6270
7216
  const toolExecutor = new ToolExecutor(this.toolRegistry, permission, this.hooks);
7217
+ const investigationGuard = new InvestigationGuard();
7218
+ toolExecutor.setInvestigationGuard(investigationGuard);
6271
7219
  toolExecutor.setSignal(options?.signal);
6272
7220
  toolExecutor.setContext(toolCtx);
6273
7221
  const contextManager = new ContextManager({
@@ -6319,6 +7267,7 @@ var Engine = class _Engine {
6319
7267
  this.lastSessionId = session.state.sessionId;
6320
7268
  this.lastMessages = messages;
6321
7269
  contextManager.setTranscriptPath(session.transcript.getFilePath());
7270
+ contextManager.initReplacementStateFromMessages(messages);
6322
7271
  contextManager.setSummarizeFn(async (prompt) => {
6323
7272
  const summaryResponse = await llmClient.createMessage({
6324
7273
  systemPrompt: "You are a conversation summarizer. Be concise and factual.",
@@ -6338,12 +7287,23 @@ var Engine = class _Engine {
6338
7287
  systemPrompt: sysPrompt,
6339
7288
  messages: [{ role: "user", content: userMsg }],
6340
7289
  tools: [],
6341
- maxTokens: 256
7290
+ maxTokens: 256,
7291
+ recordUsage: false
7292
+ });
7293
+ logger.debug("summarize.call", {
7294
+ sysPromptLen: sysPrompt.length,
7295
+ userMsgLen: userMsg.length,
7296
+ userMsgPreview: userMsg.slice(0, 300),
7297
+ completionLen: resp.text.length,
7298
+ completionPreview: resp.text.slice(0, 300),
7299
+ stopReason: resp.stopReason,
7300
+ promptTokens: resp.usage?.promptTokens,
7301
+ completionTokens: resp.usage?.completionTokens
6342
7302
  });
6343
7303
  return resp.text;
6344
7304
  };
6345
- const sessionDir = join6(
6346
- this.config.sessionStorageDir ?? join6(homedir4(), ".code-shell", "sessions"),
7305
+ const sessionDir = join8(
7306
+ this.config.sessionStorageDir ?? join8(homedir4(), ".code-shell", "sessions"),
6347
7307
  session.state.sessionId
6348
7308
  );
6349
7309
  const fileHistory = FileHistory.loadFromDir(sessionDir);
@@ -6382,7 +7342,24 @@ var Engine = class _Engine {
6382
7342
  maxTurns: this.config.maxTurns ?? 100,
6383
7343
  maxToolCallsPerTurn: this.config.maxToolCallsPerTurn ?? 10,
6384
7344
  onStream: options?.onStream,
6385
- signal: options?.signal
7345
+ signal: options?.signal,
7346
+ // Heartbeat: flush turnCount + tokens to state.json after every turn
7347
+ // so external observers (other CLI processes, /sid, the session list)
7348
+ // see live progress instead of a stale snapshot from the last
7349
+ // completed run.
7350
+ onTurnBoundary: (turnCount) => {
7351
+ session.state.turnCount = turnCount;
7352
+ const u = modelFacade.getUsage();
7353
+ session.state.tokenUsage = {
7354
+ promptTokens: u.totalPromptTokens,
7355
+ completionTokens: u.totalCompletionTokens,
7356
+ totalTokens: u.totalTokens
7357
+ };
7358
+ if (this.config.costStore) {
7359
+ session.state.costState = this.config.costStore.serialize();
7360
+ }
7361
+ this.sessionManager.saveState(session.state);
7362
+ }
6386
7363
  }
6387
7364
  );
6388
7365
  const result = await turnLoop.run(messages);
@@ -6397,8 +7374,13 @@ var Engine = class _Engine {
6397
7374
  turns: turnLoop.currentTurn,
6398
7375
  tokens: modelFacade.getUsage().totalTokens
6399
7376
  });
7377
+ recordSessionEnd(session.state.sessionId, {
7378
+ reason: result.reason,
7379
+ turns: turnLoop.currentTurn,
7380
+ cost: modelFacade.getUsage()
7381
+ });
6400
7382
  session.state.turnCount = turnLoop.currentTurn;
6401
- session.state.status = result.reason === "completed" ? "completed" : "errored";
7383
+ session.state.status = result.reason;
6402
7384
  const usage = modelFacade.getUsage();
6403
7385
  session.state.tokenUsage = {
6404
7386
  promptTokens: usage.totalPromptTokens,
@@ -6433,15 +7415,67 @@ var Engine = class _Engine {
6433
7415
  /**
6434
7416
  * Switch the active model by pool key. Takes effect on the next run() call.
6435
7417
  * Returns the new model entry.
7418
+ *
7419
+ * Persists settings.activeKey (and a legacy settings.model.* mirror) so the
7420
+ * next process startup defaults to the same model — without this, switches
7421
+ * only live in memory and every restart reverts to the previously persisted
7422
+ * activeKey.
6436
7423
  */
6437
7424
  switchModel(key) {
6438
7425
  const entry = this.modelPool.switch(key);
6439
- this.config = {
6440
- ...this.config,
6441
- llm: this.modelPool.toLLMConfig(entry, this.config.llm)
6442
- };
7426
+ const nextLlm = this.modelPool.toLLMConfig(entry, this.config.llm);
7427
+ this.config = { ...this.config, llm: nextLlm };
7428
+ this.persistActiveModel(entry, nextLlm);
6443
7429
  return entry;
6444
7430
  }
7431
+ /**
7432
+ * Write the active model selection to ~/.code-shell/settings.json.
7433
+ *
7434
+ * We mirror into the legacy settings.model.* block (provider/name/apiKey/
7435
+ * baseUrl) because boot paths in cli/main.ts, repl.ts, run.ts still read it.
7436
+ * The mirror uses resolved llm values (not raw entry.*) so credentials that
7437
+ * live on settings.providers[] flow through correctly — entry.apiKey is
7438
+ * undefined when the entry resolves via providerCatalog.
7439
+ */
7440
+ persistActiveModel(entry, llm) {
7441
+ try {
7442
+ const dir = join8(homedir4(), ".code-shell");
7443
+ const file = join8(dir, "settings.json");
7444
+ mkdirSync4(dir, { recursive: true });
7445
+ let existing = {};
7446
+ if (existsSync11(file)) {
7447
+ try {
7448
+ existing = JSON.parse(readFileSync6(file, "utf-8"));
7449
+ } catch {
7450
+ return;
7451
+ }
7452
+ }
7453
+ const prevModel = typeof existing.model === "object" && existing.model ? existing.model : {};
7454
+ const updated = {
7455
+ ...existing,
7456
+ activeKey: entry.key,
7457
+ model: {
7458
+ ...prevModel,
7459
+ provider: llm.provider,
7460
+ name: entry.model,
7461
+ apiKey: llm.apiKey,
7462
+ baseUrl: llm.baseUrl
7463
+ }
7464
+ };
7465
+ const tmp = `${file}.${process.pid}.tmp`;
7466
+ const payload = JSON.stringify(updated, null, 2) + "\n";
7467
+ writeFileSync4(tmp, payload, "utf-8");
7468
+ try {
7469
+ renameSync(tmp, file);
7470
+ } catch {
7471
+ writeFileSync4(file, payload, "utf-8");
7472
+ }
7473
+ } catch (err) {
7474
+ logger.warn(
7475
+ `persistActiveModel failed: ${err.message}`
7476
+ );
7477
+ }
7478
+ }
6445
7479
  /** Get the model pool. */
6446
7480
  getModelPool() {
6447
7481
  return this.modelPool;
@@ -6463,10 +7497,18 @@ var Engine = class _Engine {
6463
7497
  * Inject context into a session's transcript without triggering a LLM turn.
6464
7498
  * The injected content appears as an assistant message so the LLM can see it
6465
7499
  * in subsequent conversations. The transcript auto-flushes to disk.
7500
+ *
7501
+ * Also updates the in-memory compacted message cache so the next
7502
+ * engine.run() call for this session picks up the injected content
7503
+ * instead of a stale snapshot from the previous run.
6466
7504
  */
6467
7505
  injectContext(sessionId, content) {
6468
7506
  const session = this.sessionManager.resume(sessionId);
6469
7507
  session.transcript.appendMessage("assistant", content);
7508
+ const cached = this.compactedMessagesBySession.get(sessionId);
7509
+ if (cached) {
7510
+ cached.push({ role: "assistant", content });
7511
+ }
6470
7512
  }
6471
7513
  /**
6472
7514
  * Force context compaction on the current session.
@@ -6593,83 +7635,83 @@ var VALID_TRANSITIONS = {
6593
7635
  // src/run/FileRunStore.ts
6594
7636
  init_esm_shims();
6595
7637
  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
7638
+ mkdirSync as mkdirSync5,
7639
+ existsSync as existsSync12,
7640
+ readFileSync as readFileSync7,
7641
+ writeFileSync as writeFileSync5,
7642
+ appendFileSync as appendFileSync2,
7643
+ readdirSync as readdirSync6,
7644
+ renameSync as renameSync2,
7645
+ rmSync as rmSync2
6604
7646
  } from "fs";
6605
- import { join as join7 } from "path";
7647
+ import { join as join9 } from "path";
6606
7648
  import { homedir as homedir5 } from "os";
6607
7649
  var FileRunStore = class {
6608
7650
  runsDir;
6609
7651
  constructor(storageDir) {
6610
- this.runsDir = storageDir ?? join7(homedir5(), ".code-shell", "runs");
6611
- mkdirSync2(this.runsDir, { recursive: true });
7652
+ this.runsDir = storageDir ?? join9(homedir5(), ".code-shell", "runs");
7653
+ mkdirSync5(this.runsDir, { recursive: true });
6612
7654
  }
6613
7655
  // ─── Helpers ───────────────────────────────────────────────────
6614
7656
  runDir(runId) {
6615
- return join7(this.runsDir, runId);
7657
+ return join9(this.runsDir, runId);
6616
7658
  }
6617
7659
  ensureRunDir(runId) {
6618
7660
  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 });
7661
+ mkdirSync5(dir, { recursive: true });
7662
+ mkdirSync5(join9(dir, "checkpoints"), { recursive: true });
7663
+ mkdirSync5(join9(dir, "approvals"), { recursive: true });
7664
+ mkdirSync5(join9(dir, "artifacts"), { recursive: true });
6623
7665
  return dir;
6624
7666
  }
6625
7667
  writeJson(filePath, data) {
6626
7668
  const tmp = filePath + ".tmp";
6627
- writeFileSync3(tmp, JSON.stringify(data, null, 2), "utf-8");
6628
- renameSync(tmp, filePath);
7669
+ writeFileSync5(tmp, JSON.stringify(data, null, 2), "utf-8");
7670
+ renameSync2(tmp, filePath);
6629
7671
  }
6630
7672
  readJson(filePath) {
6631
- if (!existsSync10(filePath)) return null;
6632
- return JSON.parse(readFileSync6(filePath, "utf-8"));
7673
+ if (!existsSync12(filePath)) return null;
7674
+ return JSON.parse(readFileSync7(filePath, "utf-8"));
6633
7675
  }
6634
7676
  /** Serializes concurrent JSONL appends per file path. */
6635
7677
  appendLocks = /* @__PURE__ */ new Map();
6636
7678
  async appendJsonl(filePath, data) {
6637
7679
  const prev = this.appendLocks.get(filePath) ?? Promise.resolve();
6638
7680
  const current = prev.then(() => {
6639
- appendFileSync(filePath, JSON.stringify(data) + "\n", "utf-8");
7681
+ appendFileSync2(filePath, JSON.stringify(data) + "\n", "utf-8");
6640
7682
  });
6641
7683
  this.appendLocks.set(filePath, current);
6642
7684
  await current;
6643
7685
  }
6644
7686
  readJsonl(filePath) {
6645
- if (!existsSync10(filePath)) return [];
6646
- const content = readFileSync6(filePath, "utf-8");
7687
+ if (!existsSync12(filePath)) return [];
7688
+ const content = readFileSync7(filePath, "utf-8");
6647
7689
  if (!content) return [];
6648
7690
  return content.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
6649
7691
  }
6650
7692
  // ─── Snapshot ──────────────────────────────────────────────────
6651
7693
  async create(snapshot) {
6652
7694
  const dir = this.ensureRunDir(snapshot.runId);
6653
- this.writeJson(join7(dir, "run.json"), snapshot);
7695
+ this.writeJson(join9(dir, "run.json"), snapshot);
6654
7696
  }
6655
7697
  async update(snapshot) {
6656
7698
  const dir = this.runDir(snapshot.runId);
6657
- if (!existsSync10(dir)) {
7699
+ if (!existsSync12(dir)) {
6658
7700
  throw new Error(`Run not found: ${snapshot.runId}`);
6659
7701
  }
6660
- this.writeJson(join7(dir, "run.json"), snapshot);
7702
+ this.writeJson(join9(dir, "run.json"), snapshot);
6661
7703
  }
6662
7704
  async get(runId) {
6663
- return this.readJson(join7(this.runDir(runId), "run.json"));
7705
+ return this.readJson(join9(this.runDir(runId), "run.json"));
6664
7706
  }
6665
7707
  async list(query) {
6666
- if (!existsSync10(this.runsDir)) return [];
6667
- const entries = readdirSync5(this.runsDir, { withFileTypes: true });
7708
+ if (!existsSync12(this.runsDir)) return [];
7709
+ const entries = readdirSync6(this.runsDir, { withFileTypes: true });
6668
7710
  const snapshots = [];
6669
7711
  for (const entry of entries) {
6670
7712
  if (!entry.isDirectory()) continue;
6671
7713
  const snapshot = this.readJson(
6672
- join7(this.runsDir, entry.name, "run.json")
7714
+ join9(this.runsDir, entry.name, "run.json")
6673
7715
  );
6674
7716
  if (!snapshot) continue;
6675
7717
  if (query?.status) {
@@ -6686,32 +7728,32 @@ var FileRunStore = class {
6686
7728
  }
6687
7729
  async delete(runId) {
6688
7730
  const dir = this.runDir(runId);
6689
- if (existsSync10(dir)) {
6690
- rmSync(dir, { recursive: true, force: true });
7731
+ if (existsSync12(dir)) {
7732
+ rmSync2(dir, { recursive: true, force: true });
6691
7733
  }
6692
7734
  }
6693
7735
  // ─── Events ────────────────────────────────────────────────────
6694
7736
  async appendEvent(event) {
6695
7737
  this.ensureRunDir(event.runId);
6696
7738
  const dir = this.runDir(event.runId);
6697
- await this.appendJsonl(join7(dir, "events.jsonl"), event);
7739
+ await this.appendJsonl(join9(dir, "events.jsonl"), event);
6698
7740
  }
6699
7741
  async listEvents(runId) {
6700
- return this.readJsonl(join7(this.runDir(runId), "events.jsonl"));
7742
+ return this.readJsonl(join9(this.runDir(runId), "events.jsonl"));
6701
7743
  }
6702
7744
  // ─── Checkpoints ───────────────────────────────────────────────
6703
7745
  async saveCheckpoint(cp) {
6704
7746
  const dir = this.runDir(cp.runId);
6705
- this.writeJson(join7(dir, "checkpoints", `${cp.checkpointId}.json`), cp);
7747
+ this.writeJson(join9(dir, "checkpoints", `${cp.checkpointId}.json`), cp);
6706
7748
  }
6707
7749
  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"));
7750
+ const cpDir = join9(this.runDir(runId), "checkpoints");
7751
+ if (!existsSync12(cpDir)) return null;
7752
+ const files = readdirSync6(cpDir).filter((f) => f.endsWith(".json"));
6711
7753
  if (files.length === 0) return null;
6712
7754
  let latest = null;
6713
7755
  for (const file of files) {
6714
- const cp = this.readJson(join7(cpDir, file));
7756
+ const cp = this.readJson(join9(cpDir, file));
6715
7757
  if (cp && (!latest || cp.createdAt > latest.createdAt)) {
6716
7758
  latest = cp;
6717
7759
  }
@@ -6722,21 +7764,21 @@ var FileRunStore = class {
6722
7764
  async saveApproval(approval) {
6723
7765
  const dir = this.runDir(approval.runId);
6724
7766
  this.writeJson(
6725
- join7(dir, "approvals", `${approval.approvalId}.json`),
7767
+ join9(dir, "approvals", `${approval.approvalId}.json`),
6726
7768
  approval
6727
7769
  );
6728
7770
  }
6729
7771
  async getApproval(runId, approvalId) {
6730
7772
  return this.readJson(
6731
- join7(this.runDir(runId), "approvals", `${approvalId}.json`)
7773
+ join9(this.runDir(runId), "approvals", `${approvalId}.json`)
6732
7774
  );
6733
7775
  }
6734
7776
  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"));
7777
+ const approvalDir = join9(this.runDir(runId), "approvals");
7778
+ if (!existsSync12(approvalDir)) return null;
7779
+ const files = readdirSync6(approvalDir).filter((f) => f.endsWith(".json"));
6738
7780
  for (const file of files) {
6739
- const approval = this.readJson(join7(approvalDir, file));
7781
+ const approval = this.readJson(join9(approvalDir, file));
6740
7782
  if (approval?.status === "pending") return approval;
6741
7783
  }
6742
7784
  return null;
@@ -6745,11 +7787,11 @@ var FileRunStore = class {
6745
7787
  async appendArtifactRef(ref) {
6746
7788
  this.ensureRunDir(ref.runId);
6747
7789
  const dir = this.runDir(ref.runId);
6748
- await this.appendJsonl(join7(dir, "artifacts", "refs.jsonl"), ref);
7790
+ await this.appendJsonl(join9(dir, "artifacts", "refs.jsonl"), ref);
6749
7791
  }
6750
7792
  async listArtifactRefs(runId) {
6751
7793
  return this.readJsonl(
6752
- join7(this.runDir(runId), "artifacts", "refs.jsonl")
7794
+ join9(this.runDir(runId), "artifacts", "refs.jsonl")
6753
7795
  );
6754
7796
  }
6755
7797
  };
@@ -7116,8 +8158,8 @@ function extractFileName(filePath) {
7116
8158
 
7117
8159
  // src/run/RunLock.ts
7118
8160
  init_esm_shims();
7119
- import { existsSync as existsSync11 } from "fs";
7120
- import { join as join8 } from "path";
8161
+ import { existsSync as existsSync13 } from "fs";
8162
+ import { join as join10 } from "path";
7121
8163
  import { homedir as homedir6 } from "os";
7122
8164
 
7123
8165
  // src/utils/lockfile.ts
@@ -7145,7 +8187,7 @@ var RunLock = class {
7145
8187
  staleMs;
7146
8188
  releaseFns = /* @__PURE__ */ new Map();
7147
8189
  constructor(config) {
7148
- this.runsDir = config?.runsDir ?? join8(homedir6(), ".code-shell", "runs");
8190
+ this.runsDir = config?.runsDir ?? join10(homedir6(), ".code-shell", "runs");
7149
8191
  this.staleMs = config?.staleMs ?? 6e4;
7150
8192
  }
7151
8193
  /**
@@ -7153,7 +8195,7 @@ var RunLock = class {
7153
8195
  */
7154
8196
  async acquire(runId) {
7155
8197
  const lockTarget = this.lockTarget(runId);
7156
- if (!existsSync11(lockTarget)) return false;
8198
+ if (!existsSync13(lockTarget)) return false;
7157
8199
  try {
7158
8200
  const release = await lock(lockTarget, {
7159
8201
  stale: this.staleMs,
@@ -7185,7 +8227,7 @@ var RunLock = class {
7185
8227
  */
7186
8228
  async isLocked(runId) {
7187
8229
  const lockTarget = this.lockTarget(runId);
7188
- if (!existsSync11(lockTarget)) return false;
8230
+ if (!existsSync13(lockTarget)) return false;
7189
8231
  try {
7190
8232
  return await check(lockTarget, { stale: this.staleMs });
7191
8233
  } catch {
@@ -7197,7 +8239,7 @@ var RunLock = class {
7197
8239
  */
7198
8240
  async forceUnlock(runId) {
7199
8241
  const lockTarget = this.lockTarget(runId);
7200
- if (!existsSync11(lockTarget)) return;
8242
+ if (!existsSync13(lockTarget)) return;
7201
8243
  try {
7202
8244
  await unlock(lockTarget);
7203
8245
  logger.info("run.lock.force_unlocked", { runId });
@@ -7213,21 +8255,21 @@ var RunLock = class {
7213
8255
  }
7214
8256
  }
7215
8257
  lockTarget(runId) {
7216
- return join8(this.runsDir, runId, "run.json");
8258
+ return join10(this.runsDir, runId, "run.json");
7217
8259
  }
7218
8260
  };
7219
8261
 
7220
8262
  // src/run/Heartbeat.ts
7221
8263
  init_esm_shims();
7222
- import { existsSync as existsSync12, readFileSync as readFileSync7, writeFileSync as writeFileSync4, unlinkSync } from "fs";
7223
- import { join as join9 } from "path";
8264
+ import { existsSync as existsSync14, readFileSync as readFileSync8, writeFileSync as writeFileSync6, unlinkSync } from "fs";
8265
+ import { join as join11 } from "path";
7224
8266
  import { homedir as homedir7 } from "os";
7225
8267
  var Heartbeat = class {
7226
8268
  runsDir;
7227
8269
  intervalMs;
7228
8270
  timers = /* @__PURE__ */ new Map();
7229
8271
  constructor(config) {
7230
- this.runsDir = config?.runsDir ?? join9(homedir7(), ".code-shell", "runs");
8272
+ this.runsDir = config?.runsDir ?? join11(homedir7(), ".code-shell", "runs");
7231
8273
  this.intervalMs = config?.intervalMs ?? 5e3;
7232
8274
  }
7233
8275
  /**
@@ -7265,9 +8307,9 @@ var Heartbeat = class {
7265
8307
  */
7266
8308
  read(runId) {
7267
8309
  const filePath = this.filePath(runId);
7268
- if (!existsSync12(filePath)) return null;
8310
+ if (!existsSync14(filePath)) return null;
7269
8311
  try {
7270
- return JSON.parse(readFileSync7(filePath, "utf-8"));
8312
+ return JSON.parse(readFileSync8(filePath, "utf-8"));
7271
8313
  } catch {
7272
8314
  return null;
7273
8315
  }
@@ -7302,21 +8344,21 @@ var Heartbeat = class {
7302
8344
  runId
7303
8345
  };
7304
8346
  try {
7305
- writeFileSync4(this.filePath(runId), JSON.stringify(data), "utf-8");
8347
+ writeFileSync6(this.filePath(runId), JSON.stringify(data), "utf-8");
7306
8348
  } catch {
7307
8349
  }
7308
8350
  }
7309
8351
  remove(runId) {
7310
8352
  try {
7311
8353
  const filePath = this.filePath(runId);
7312
- if (existsSync12(filePath)) {
8354
+ if (existsSync14(filePath)) {
7313
8355
  unlinkSync(filePath);
7314
8356
  }
7315
8357
  } catch {
7316
8358
  }
7317
8359
  }
7318
8360
  filePath(runId) {
7319
- return join9(this.runsDir, runId, "heartbeat");
8361
+ return join11(this.runsDir, runId, "heartbeat");
7320
8362
  }
7321
8363
  };
7322
8364
 
@@ -7957,6 +8999,14 @@ export {
7957
8999
  setRuntimeBypass,
7958
9000
  PermissionClassifier,
7959
9001
  HookRegistry,
9002
+ DEFAULT_PERSIST_THRESHOLD,
9003
+ PER_MESSAGE_AGGREGATE_CAP,
9004
+ PREVIEW_SIZE,
9005
+ createContentReplacementState,
9006
+ reconstructContentReplacementState,
9007
+ resolveToolResultsDir,
9008
+ applyToolResultPersistence,
9009
+ isPersistedReplacement,
7960
9010
  ContextManager,
7961
9011
  SectionCache,
7962
9012
  scanInstructions,