@cjhyy/code-shell 0.1.0-alpha.2 → 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.
- package/CHANGELOG.md +77 -0
- package/dist/anthropic-IBTI4WL2.cjs +10 -0
- package/dist/anthropic-PRZZKHEZ.js +10 -0
- package/dist/arena/index.cjs +7 -6
- package/dist/arena/index.js +6 -5
- package/dist/arena/model-presets.d.ts +10 -1
- package/dist/{arena-HHDYYVHJ.cjs → arena-KBQOPLFV.cjs} +25 -23
- package/dist/{arena-ENHBHB2W.js → arena-O2L4TN6A.js} +9 -7
- package/dist/{chunk-SW3L7HIM.js → chunk-2EP3CLEN.js} +2 -2
- package/dist/{chunk-EL2RL5DG.cjs → chunk-2YW4FPFG.cjs} +2 -2
- package/dist/chunk-3OA5G4TM.js +47 -0
- package/dist/chunk-52LGZKT3.cjs +47 -0
- package/dist/{chunk-KOGV6PDQ.cjs → chunk-55XGPMAP.cjs} +44 -9
- package/dist/chunk-5ECBT4YA.js +450 -0
- package/dist/{chunk-YD7XTI7T.cjs → chunk-6QVAYW7C.cjs} +116 -28
- package/dist/{chunk-SCDKLTN3.js → chunk-6S4BJ65N.js} +5 -4
- package/dist/{chunk-ZLPJJZUW.js → chunk-76HYEPDM.js} +2 -2
- package/dist/{chunk-FA5KD34T.js → chunk-76W334S4.js} +2 -2
- package/dist/{chunk-GZBD6QRS.cjs → chunk-7EG3V4JY.cjs} +13 -12
- package/dist/{chunk-LMEKCQX4.js → chunk-7JVTYEEC.js} +6 -6
- package/dist/{chunk-ID6EJUHP.js → chunk-7K6SDRRF.js} +1262 -212
- package/dist/{chunk-5C5HIOML.js → chunk-7Y6S3OCX.js} +109 -21
- package/dist/{chunk-G76E5FOB.cjs → chunk-A5LCRPDP.cjs} +4 -4
- package/dist/{chunk-F5BS3GUC.js → chunk-BSW22MPA.js} +39 -4
- package/dist/{chunk-VWFET3MG.cjs → chunk-BUU5PZK2.cjs} +1474 -423
- package/dist/{chunk-HOXQLX5R.cjs → chunk-BWTOS5BH.cjs} +444 -103
- package/dist/{chunk-ZRVPCXI7.js → chunk-BXCY2FSE.js} +2 -1
- package/dist/{chunk-Y7TSCDF6.js → chunk-C5YQBIRK.js} +347 -6
- package/dist/{chunk-C7C7ZZY4.cjs → chunk-HEKQCI7B.cjs} +10 -10
- package/dist/{chunk-JHXNOB5G.cjs → chunk-HS2MFNUC.cjs} +6 -6
- package/dist/{chunk-JHNCK7JP.cjs → chunk-IK4EWJBP.cjs} +4 -3
- package/dist/{chunk-M2QI5X3X.cjs → chunk-JMR33QVF.cjs} +2 -2
- package/dist/{chunk-YAFCN7ZW.cjs → chunk-KOVYSDHZ.cjs} +15 -15
- package/dist/{chunk-IJSHQGMP.cjs → chunk-KS3RYAYL.cjs} +4 -4
- package/dist/{chunk-DI7RDLOS.js → chunk-MD32R22I.js} +7 -4
- package/dist/chunk-MY5XHADU.cjs +216 -0
- package/dist/{chunk-OP57WUWQ.cjs → chunk-NUNC3VFB.cjs} +6 -6
- package/dist/{chunk-TATCTZSB.cjs → chunk-QRM7IF2S.cjs} +133 -175
- package/dist/chunk-R47ROHGL.cjs +298 -0
- package/dist/{chunk-OUC2DFA7.js → chunk-RMYKO74M.js} +3 -3
- package/dist/{chunk-FUOKLKR2.js → chunk-SFXHZSRF.js} +1 -1
- package/dist/{chunk-2EUAE7AY.js → chunk-SIHHIQLO.js} +2 -2
- package/dist/{chunk-3CAZIWUA.cjs → chunk-SNT563K3.cjs} +9 -9
- package/dist/{chunk-SK62UVS6.js → chunk-T6VYG46N.js} +1 -1
- package/dist/{chunk-3QR52LL5.cjs → chunk-TUY2T5KP.cjs} +2 -2
- package/dist/{chunk-I57ZD5IQ.js → chunk-VIAGTYYW.js} +1 -1
- package/dist/{chunk-X2DNIMPC.cjs → chunk-VYL4EFRC.cjs} +32 -32
- package/dist/chunk-W22P377T.js +299 -0
- package/dist/chunk-WASS5YWB.js +215 -0
- package/dist/{chunk-OMRGA6TI.js → chunk-XDIQWQJ4.js} +132 -174
- package/dist/{chunk-BUJ5KYCI.cjs → chunk-YIQBXAYU.cjs} +13 -13
- package/dist/{chunk-REFCCVDM.js → chunk-YJ77GQK5.js} +2 -2
- package/dist/chunk-YUI524OE.cjs +450 -0
- package/dist/{chunk-M2SVOKC6.js → chunk-ZBPSQCPU.js} +6 -6
- package/dist/cli/commands/builtin/export-md.d.ts +19 -0
- package/dist/cli/commands/builtin/init/detect.d.ts +25 -0
- package/dist/cli/commands/builtin/init/index.d.ts +19 -0
- package/dist/cli/main.cjs +2374 -1455
- package/dist/cli/main.js +2505 -1586
- package/dist/cli/migrate-models.d.ts +45 -0
- package/dist/cli/onboarding.d.ts +110 -5
- package/dist/cli/updater.d.ts +51 -4
- package/dist/client-base-HTF236ZD.cjs +9 -0
- package/dist/client-base-QKRKCOWB.js +9 -0
- package/dist/{compaction-YNWCEF7B.js → compaction-6VU2T643.js} +4 -2
- package/dist/compaction-OKUNTCA7.cjs +33 -0
- package/dist/context/compaction.d.ts +35 -3
- package/dist/context/manager.d.ts +40 -3
- package/dist/context/tool-result-storage.d.ts +96 -0
- package/dist/devtools-T2NIZBJK.cjs +6 -0
- package/dist/{devtools-IRQQPIRY.js → devtools-YXWHOVKR.js} +1 -1
- package/dist/engine/engine.d.ts +40 -0
- package/dist/engine/turn-loop.d.ts +6 -0
- package/dist/export-md-RXDW6F5W.cjs +135 -0
- package/dist/export-md-XXKZANW5.js +135 -0
- package/dist/file-history-KYPE3ECT.cjs +7 -0
- package/dist/file-history-LF4F7X26.js +7 -0
- package/dist/index.cjs +42 -22
- package/dist/index.d.ts +5 -2
- package/dist/index.js +40 -20
- package/dist/llm/client-base.d.ts +1 -1
- package/dist/llm/model-cache.d.ts +21 -0
- package/dist/llm/model-fetcher.d.ts +29 -0
- package/dist/llm/model-pool.d.ts +13 -0
- package/dist/llm/provider-catalog.d.ts +30 -0
- package/dist/llm/provider-kinds.d.ts +21 -0
- package/dist/llm/types.d.ts +8 -0
- package/dist/logging/session-recorder.d.ts +77 -0
- package/dist/manager-C2IUXKRC.js +7 -0
- package/dist/manager-YYKWYTRM.cjs +7 -0
- package/dist/mcp-manager-QMKWPBZ4.js +8 -0
- package/dist/mcp-manager-Z3OKI74O.cjs +8 -0
- package/dist/memory-5LWZ6FI2.cjs +9 -0
- package/dist/{memory-EPFDBPII.js → memory-VIJLBAMQ.js} +2 -2
- package/dist/model-cache-5KIYL5SL.js +13 -0
- package/dist/model-cache-OED3IM4R.cjs +13 -0
- package/dist/model-fetcher-HKGICACL.js +8 -0
- package/dist/model-fetcher-UR6WJOQP.cjs +8 -0
- package/dist/openai-JR2WEKOF.js +10 -0
- package/dist/openai-YBTOET4O.cjs +10 -0
- package/dist/openrouter-models-OU4GRZQY.cjs +15 -0
- package/dist/{openrouter-models-OL725CL6.js → openrouter-models-TGKDKY52.js} +2 -2
- package/dist/openrouter-sync-BBHUM3R6.cjs +11 -0
- package/dist/{openrouter-sync-CWD5VNW2.js → openrouter-sync-UXUSJCIR.js} +3 -3
- package/dist/product/index.cjs +17 -15
- package/dist/product/index.js +16 -14
- package/dist/protocol/server.d.ts +3 -1
- package/dist/protocol/types.d.ts +36 -2
- package/dist/run/index.cjs +17 -15
- package/dist/run/index.js +16 -14
- package/dist/services/index.d.ts +0 -1
- package/dist/session-manager-Q22BBMD3.js +8 -0
- package/dist/session-manager-XODQVL3C.cjs +8 -0
- package/dist/settings/schema.d.ts +267 -42
- package/dist/{tool-summary-RYNR4DDB.cjs → tool-summary-63DPIS7G.cjs} +4 -4
- package/dist/{tool-summary-2RUFL5QK.js → tool-summary-NP5CDBKG.js} +2 -2
- package/dist/tool-system/builtin/ask-user.d.ts +9 -1
- package/dist/tool-system/context.d.ts +31 -2
- package/dist/tool-system/executor.d.ts +4 -0
- package/dist/tool-system/investigation-guard.d.ts +37 -0
- package/dist/types.d.ts +13 -2
- package/dist/ui/components/AskUserPrompt.d.ts +8 -1
- package/dist/ui/components/DiffLine.d.ts +33 -0
- package/dist/ui/components/ModelManager.d.ts +34 -1
- package/dist/ui/components/OnboardingPrompt.d.ts +10 -1
- package/dist/ui/components/ProviderModelFlow.d.ts +58 -0
- package/dist/ui/components/UpdateBanner.d.ts +1 -0
- package/dist/updater-QW473OF4.cjs +17 -0
- package/dist/updater-VFUAHCVY.js +17 -0
- package/dist/utils/semver.d.ts +1 -0
- package/dist/utils/task-sanitizer.d.ts +6 -0
- package/package.json +4 -2
- package/scripts/check-node.cjs +21 -0
- package/dist/anthropic-P5VETAFI.cjs +0 -10
- package/dist/anthropic-W2GU3D76.js +0 -10
- package/dist/chunk-75E4BBFC.js +0 -224
- package/dist/chunk-FV2ARONZ.cjs +0 -224
- package/dist/client-base-TOVE25FA.js +0 -9
- package/dist/client-base-VDDIADAC.cjs +0 -9
- package/dist/compaction-D3KV2NWY.cjs +0 -31
- package/dist/devtools-WTC3NFNE.cjs +0 -6
- package/dist/file-history-HZWO2WQ5.js +0 -7
- package/dist/file-history-UQQILIY7.cjs +0 -7
- package/dist/manager-FWLOVEEK.cjs +0 -7
- package/dist/manager-GEXJLOIS.js +0 -7
- package/dist/mcp-manager-37GQLFGX.js +0 -8
- package/dist/mcp-manager-U77NEXBY.cjs +0 -8
- package/dist/memory-ADGOWADP.cjs +0 -9
- package/dist/openai-RXU5URYI.js +0 -10
- package/dist/openai-SQW4H2K4.cjs +0 -10
- package/dist/openrouter-models-UM3AOGZ4.cjs +0 -15
- package/dist/openrouter-sync-HYZZC6RX.cjs +0 -11
- package/dist/services/compact.d.ts +0 -56
- package/dist/session-manager-HRSKENKB.cjs +0 -8
- package/dist/session-manager-NEKFEHF3.js +0 -8
- package/dist/updater-M7L42ASZ.cjs +0 -183
- package/dist/updater-ZRTONIR4.js +0 -183
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ModelPool
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BSW22MPA.js";
|
|
4
|
+
import {
|
|
5
|
+
defaultCacheDir
|
|
6
|
+
} from "./chunk-3OA5G4TM.js";
|
|
4
7
|
import {
|
|
5
8
|
MCPManager
|
|
6
|
-
} from "./chunk-
|
|
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-
|
|
21
|
+
} from "./chunk-7Y6S3OCX.js";
|
|
19
22
|
import {
|
|
20
23
|
MemoryManager
|
|
21
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-VIAGTYYW.js";
|
|
22
25
|
import {
|
|
23
26
|
SessionManager
|
|
24
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-76HYEPDM.js";
|
|
25
28
|
import {
|
|
26
29
|
SettingsManager
|
|
27
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-5ECBT4YA.js";
|
|
28
31
|
import {
|
|
29
32
|
FileHistory
|
|
30
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-T6VYG46N.js";
|
|
31
34
|
import {
|
|
32
35
|
webFetchTool,
|
|
33
36
|
webFetchToolDef,
|
|
34
37
|
webSearchTool,
|
|
35
38
|
webSearchToolDef
|
|
36
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
56
|
+
} from "./chunk-SFXHZSRF.js";
|
|
52
57
|
import {
|
|
58
|
+
getCurrentSid,
|
|
53
59
|
logger
|
|
54
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-BXCY2FSE.js";
|
|
55
61
|
import {
|
|
56
62
|
__require,
|
|
57
63
|
__toCommonJS,
|
|
58
64
|
init_esm_shims
|
|
59
|
-
} from "./chunk-
|
|
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: {
|
|
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:
|
|
2383
|
-
const text =
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
2796
|
-
const { join:
|
|
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 =
|
|
2799
|
-
|
|
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 =
|
|
2809
|
-
|
|
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
|
-
|
|
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.
|
|
4615
|
-
summarizeAtRatio: 0.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4690
|
-
result = windowCompact(result, keepN);
|
|
5226
|
+
result = snipCompact(result, 3, 8);
|
|
4691
5227
|
tokens = this.estimateTokensHybrid(result);
|
|
4692
|
-
this.onCompact?.({ strategy: "
|
|
5228
|
+
this.onCompact?.({ strategy: "snip", before, after: tokens });
|
|
4693
5229
|
}
|
|
4694
5230
|
}
|
|
4695
|
-
if (tokens >
|
|
5231
|
+
if (tokens > windowGate) {
|
|
4696
5232
|
const before = tokens;
|
|
4697
|
-
|
|
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: "
|
|
5236
|
+
this.onCompact?.({ strategy: "window", before, after: tokens });
|
|
4700
5237
|
}
|
|
4701
|
-
if (tokens >
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4753
|
-
result = windowCompact(result, keepN);
|
|
5314
|
+
result = snipCompact(result, 3, 8);
|
|
4754
5315
|
live = this.estimateTokensHybrid(result);
|
|
4755
|
-
this.onCompact?.({ strategy: "
|
|
5316
|
+
this.onCompact?.({ strategy: "snip", before, after: live });
|
|
4756
5317
|
}
|
|
4757
|
-
if (live >
|
|
5318
|
+
if (live > windowGate) {
|
|
4758
5319
|
const before = live;
|
|
4759
|
-
|
|
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: "
|
|
5323
|
+
this.onCompact?.({ strategy: "window", before, after: live });
|
|
4762
5324
|
}
|
|
4763
|
-
if (live >
|
|
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 *
|
|
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
|
|
4883
|
-
import { join as
|
|
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 =
|
|
4893
|
-
const homeCompatDir =
|
|
5472
|
+
const homeConfigDir = join6(homedir2(), ".code-shell");
|
|
5473
|
+
const homeCompatDir = join6(homedir2(), ".claude");
|
|
4894
5474
|
for (const name of allNames) {
|
|
4895
|
-
tryAddFile(
|
|
5475
|
+
tryAddFile(join6(homeConfigDir, name), "user", -1, entries);
|
|
4896
5476
|
}
|
|
4897
|
-
tryAddRulesDir(
|
|
5477
|
+
tryAddRulesDir(join6(homeConfigDir, "rules"), "user", -1, entries);
|
|
4898
5478
|
for (const name of compatNames) {
|
|
4899
|
-
tryAddFile(
|
|
5479
|
+
tryAddFile(join6(homeCompatDir, name), "user", -1, entries);
|
|
4900
5480
|
}
|
|
4901
|
-
tryAddRulesDir(
|
|
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(
|
|
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(
|
|
5491
|
+
tryAddFile(join6(dir, cfgDir, name), "project", depth, entries);
|
|
4912
5492
|
}
|
|
4913
|
-
tryAddRulesDir(
|
|
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(
|
|
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 (!
|
|
5512
|
+
if (!existsSync9(path)) return;
|
|
4933
5513
|
try {
|
|
4934
|
-
const stat5 =
|
|
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 (!
|
|
5524
|
+
if (!existsSync9(dir)) return;
|
|
4945
5525
|
try {
|
|
4946
|
-
const files =
|
|
5526
|
+
const files = readdirSync4(dir).filter((f) => f.endsWith(".md")).sort();
|
|
4947
5527
|
for (const file of files) {
|
|
4948
|
-
tryAddFile(
|
|
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 =
|
|
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
|
|
5003
|
-
import { join as
|
|
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
|
-
|
|
5011
|
-
|
|
5590
|
+
join7(cwd, ".code-shell", "skills"),
|
|
5591
|
+
join7(cwd, ".claude", "skills"),
|
|
5012
5592
|
// User-level
|
|
5013
|
-
|
|
5014
|
-
|
|
5593
|
+
join7(homedir3(), ".code-shell", "skills"),
|
|
5594
|
+
join7(homedir3(), ".claude", "skills")
|
|
5015
5595
|
];
|
|
5016
5596
|
for (const dir of dirs) {
|
|
5017
|
-
if (!
|
|
5597
|
+
if (!existsSync10(dir)) continue;
|
|
5018
5598
|
try {
|
|
5019
|
-
const files =
|
|
5020
|
-
(f) => f.endsWith(".md") &&
|
|
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(
|
|
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
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
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
|
-
|
|
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
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
6346
|
-
this.config.sessionStorageDir ??
|
|
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
|
|
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
|
-
|
|
6441
|
-
|
|
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
|
|
6597
|
-
existsSync as
|
|
6598
|
-
readFileSync as
|
|
6599
|
-
writeFileSync as
|
|
6600
|
-
appendFileSync,
|
|
6601
|
-
readdirSync as
|
|
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
|
|
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 ??
|
|
6611
|
-
|
|
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
|
|
7657
|
+
return join9(this.runsDir, runId);
|
|
6616
7658
|
}
|
|
6617
7659
|
ensureRunDir(runId) {
|
|
6618
7660
|
const dir = this.runDir(runId);
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
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
|
-
|
|
6628
|
-
|
|
7669
|
+
writeFileSync5(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
7670
|
+
renameSync2(tmp, filePath);
|
|
6629
7671
|
}
|
|
6630
7672
|
readJson(filePath) {
|
|
6631
|
-
if (!
|
|
6632
|
-
return JSON.parse(
|
|
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
|
-
|
|
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 (!
|
|
6646
|
-
const content =
|
|
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(
|
|
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 (!
|
|
7699
|
+
if (!existsSync12(dir)) {
|
|
6658
7700
|
throw new Error(`Run not found: ${snapshot.runId}`);
|
|
6659
7701
|
}
|
|
6660
|
-
this.writeJson(
|
|
7702
|
+
this.writeJson(join9(dir, "run.json"), snapshot);
|
|
6661
7703
|
}
|
|
6662
7704
|
async get(runId) {
|
|
6663
|
-
return this.readJson(
|
|
7705
|
+
return this.readJson(join9(this.runDir(runId), "run.json"));
|
|
6664
7706
|
}
|
|
6665
7707
|
async list(query) {
|
|
6666
|
-
if (!
|
|
6667
|
-
const entries =
|
|
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
|
-
|
|
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 (
|
|
6690
|
-
|
|
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(
|
|
7739
|
+
await this.appendJsonl(join9(dir, "events.jsonl"), event);
|
|
6698
7740
|
}
|
|
6699
7741
|
async listEvents(runId) {
|
|
6700
|
-
return this.readJsonl(
|
|
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(
|
|
7747
|
+
this.writeJson(join9(dir, "checkpoints", `${cp.checkpointId}.json`), cp);
|
|
6706
7748
|
}
|
|
6707
7749
|
async getLatestCheckpoint(runId) {
|
|
6708
|
-
const cpDir =
|
|
6709
|
-
if (!
|
|
6710
|
-
const files =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
7773
|
+
join9(this.runDir(runId), "approvals", `${approvalId}.json`)
|
|
6732
7774
|
);
|
|
6733
7775
|
}
|
|
6734
7776
|
async getPendingApproval(runId) {
|
|
6735
|
-
const approvalDir =
|
|
6736
|
-
if (!
|
|
6737
|
-
const files =
|
|
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(
|
|
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(
|
|
7790
|
+
await this.appendJsonl(join9(dir, "artifacts", "refs.jsonl"), ref);
|
|
6749
7791
|
}
|
|
6750
7792
|
async listArtifactRefs(runId) {
|
|
6751
7793
|
return this.readJsonl(
|
|
6752
|
-
|
|
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
|
|
7120
|
-
import { join as
|
|
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 ??
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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
|
|
7223
|
-
import { join as
|
|
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 ??
|
|
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 (!
|
|
8310
|
+
if (!existsSync14(filePath)) return null;
|
|
7269
8311
|
try {
|
|
7270
|
-
return JSON.parse(
|
|
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
|
-
|
|
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 (
|
|
8354
|
+
if (existsSync14(filePath)) {
|
|
7313
8355
|
unlinkSync(filePath);
|
|
7314
8356
|
}
|
|
7315
8357
|
} catch {
|
|
7316
8358
|
}
|
|
7317
8359
|
}
|
|
7318
8360
|
filePath(runId) {
|
|
7319
|
-
return
|
|
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,
|