jinzd-ai-cli 0.4.180 → 0.4.182

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/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  saveDevState,
19
19
  sessionHasMeaningfulContent,
20
20
  setupProxy
21
- } from "./chunk-FQHEXIYP.js";
21
+ } from "./chunk-C6UJBTZO.js";
22
22
  import {
23
23
  ToolExecutor,
24
24
  ToolRegistry,
@@ -37,10 +37,10 @@ import {
37
37
  spawnAgentContext,
38
38
  theme,
39
39
  undoStack
40
- } from "./chunk-7WFMYFHC.js";
40
+ } from "./chunk-XI7EUUL7.js";
41
41
  import "./chunk-HDSKW7Q3.js";
42
42
  import "./chunk-ZWVIDFGY.js";
43
- import "./chunk-H7TXZO6D.js";
43
+ import "./chunk-QUYLXQRU.js";
44
44
  import {
45
45
  SessionManager,
46
46
  getContentText
@@ -49,15 +49,25 @@ import {
49
49
  getConfigDirUsage,
50
50
  listRecentCrashes,
51
51
  writeCrashLog
52
- } from "./chunk-ZUUXMR6Z.js";
52
+ } from "./chunk-4NPR3MFZ.js";
53
53
  import {
54
+ BudgetWarner,
54
55
  CONTENT_ONLY_STREAM_REMINDER,
56
+ ContextPressureMonitor,
57
+ EmptyResponseGuard,
58
+ FreeRoundTracker,
55
59
  HALLUCINATION_CORRECTION_MESSAGE,
56
60
  ProviderRegistry,
57
61
  TEE_FINAL_USER_NUDGE,
58
62
  TOOL_CALL_REMINDER,
63
+ ThinkTagFilter,
64
+ accumulateUsage,
59
65
  buildPhantomCorrectionMessage,
66
+ buildRoundBudgetHint,
67
+ buildRoundsExhaustedPrompt,
68
+ buildUserStopMessage,
60
69
  buildWriteRoundReminder,
70
+ consumeToolCallStream,
61
71
  detectMetaNarration,
62
72
  detectPseudoToolCalls,
63
73
  detectsHallucinatedFileOp,
@@ -67,18 +77,19 @@ import {
67
77
  hadPreviousWriteToolCalls,
68
78
  looksLikeDocumentBody,
69
79
  stripPseudoToolCalls,
70
- stripToolCallReminder
71
- } from "./chunk-2TWARH5X.js";
80
+ stripToolCallReminder,
81
+ summarizeRecentTools
82
+ } from "./chunk-5LK7H45B.js";
72
83
  import {
73
84
  getStatsSnapshot,
74
85
  getTopFailingTools,
75
86
  getTopUsedTools,
76
87
  installFlushOnExit
77
- } from "./chunk-WY2DDJTH.js";
88
+ } from "./chunk-LWZ6P73G.js";
78
89
  import "./chunk-HIU2SH4V.js";
79
90
  import {
80
91
  ConfigManager
81
- } from "./chunk-2UGK2RXK.js";
92
+ } from "./chunk-YDIR3MXD.js";
82
93
  import {
83
94
  AuthError,
84
95
  ProviderError,
@@ -105,7 +116,7 @@ import {
105
116
  SKILLS_DIR_NAME,
106
117
  VERSION,
107
118
  buildUserIdentityPrompt
108
- } from "./chunk-KRU4DFRH.js";
119
+ } from "./chunk-ISO5KVEJ.js";
109
120
  import {
110
121
  formatGitContextForPrompt,
111
122
  getGitContext,
@@ -208,13 +219,6 @@ function isInterruptedSession(messages) {
208
219
  import chalk from "chalk";
209
220
  import { createWriteStream, mkdirSync } from "fs";
210
221
  import { dirname } from "path";
211
- function partialTagTail(s, tag) {
212
- const max = Math.min(s.length, tag.length - 1);
213
- for (let k = max; k > 0; k--) {
214
- if (s.endsWith(tag.slice(0, k))) return k;
215
- }
216
- return 0;
217
- }
218
222
  function fmtContextWindow(tokens) {
219
223
  if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
220
224
  if (tokens >= 1e3) return `${Math.round(tokens / 1024)}K`;
@@ -495,48 +499,15 @@ var Renderer = class {
495
499
  }
496
500
  let fullContent = "";
497
501
  let usage;
498
- let inThinking = false;
499
- let thinkBuf = "";
500
- const emitText = (raw) => {
501
- thinkBuf += raw;
502
- let out = "";
503
- while (thinkBuf.length > 0) {
504
- if (!inThinking) {
505
- const open = thinkBuf.indexOf("<think>");
506
- if (open === -1) {
507
- const keep = partialTagTail(thinkBuf, "<think>");
508
- out += thinkBuf.slice(0, thinkBuf.length - keep);
509
- thinkBuf = thinkBuf.slice(thinkBuf.length - keep);
510
- break;
511
- }
512
- out += thinkBuf.slice(0, open);
513
- thinkBuf = thinkBuf.slice(open + "<think>".length);
514
- inThinking = true;
515
- } else {
516
- const close = thinkBuf.indexOf("</think>");
517
- if (close === -1) {
518
- const keep = partialTagTail(thinkBuf, "</think>");
519
- thinkBuf = thinkBuf.slice(thinkBuf.length - keep);
520
- break;
521
- }
522
- thinkBuf = thinkBuf.slice(close + "</think>".length);
523
- inThinking = false;
524
- }
525
- }
526
- if (out) {
527
- process.stdout.write(out);
528
- if (fileStream) fileStream.write(out);
529
- fullContent += out;
530
- }
531
- };
532
- const flushTail = () => {
533
- if (!inThinking && thinkBuf) {
534
- process.stdout.write(thinkBuf);
535
- if (fileStream) fileStream.write(thinkBuf);
536
- fullContent += thinkBuf;
537
- thinkBuf = "";
538
- }
502
+ const thinkFilter = new ThinkTagFilter();
503
+ const writeVisible = (visible) => {
504
+ if (!visible) return;
505
+ process.stdout.write(visible);
506
+ if (fileStream) fileStream.write(visible);
507
+ fullContent += visible;
539
508
  };
509
+ const emitText = (raw) => writeVisible(thinkFilter.push(raw));
510
+ const flushTail = () => writeVisible(thinkFilter.flush());
540
511
  let interrupted = false;
541
512
  let streamErr = null;
542
513
  try {
@@ -1016,6 +987,61 @@ function copyToClipboard(text) {
1016
987
  }
1017
988
  }
1018
989
  }
990
+ var SESSION_PAGE_SIZE = 20;
991
+ function oneLinePreview(s, max) {
992
+ const flat = s.replace(/\s+/g, " ").trim();
993
+ return flat.length > max ? flat.slice(0, max - 1) + "\u2026" : flat;
994
+ }
995
+ function renderTranscriptMessage(msg, renderer) {
996
+ if (msg.role === "user") {
997
+ const text = getContentText(msg.content);
998
+ console.log(theme.success("You: ") + (text.includes("\n") ? "\n" + text : text));
999
+ console.log();
1000
+ } else if (msg.role === "assistant") {
1001
+ const text = getContentText(msg.content);
1002
+ if (msg.toolCalls && msg.toolCalls.length > 0) {
1003
+ if (text.trim()) {
1004
+ console.log(theme.dim(`Assistant: ${oneLinePreview(text, 100)}`));
1005
+ }
1006
+ for (const tc of msg.toolCalls) {
1007
+ console.log(theme.dim(` \u2699 ${tc.name} ${oneLinePreview(JSON.stringify(tc.arguments ?? {}), 80)}`));
1008
+ }
1009
+ } else if (text.trim()) {
1010
+ renderer.renderResponse(text);
1011
+ console.log();
1012
+ }
1013
+ } else if (msg.role === "tool") {
1014
+ const preview = oneLinePreview(getContentText(msg.content), 80);
1015
+ const paint = msg.isError ? theme.error : theme.dim;
1016
+ console.log(paint(` \u2192 ${msg.toolName ?? "tool"}: ${preview}`));
1017
+ }
1018
+ }
1019
+ function renderTranscriptPage(messages, renderer, requestedPage) {
1020
+ const total = messages.length;
1021
+ if (total === 0) {
1022
+ renderer.printInfo("Session has no messages yet.");
1023
+ return;
1024
+ }
1025
+ const totalPages = Math.max(1, Math.ceil(total / SESSION_PAGE_SIZE));
1026
+ let page = requestedPage === void 0 || !Number.isFinite(requestedPage) ? totalPages : requestedPage;
1027
+ if (page < 1) page = 1;
1028
+ if (page > totalPages) page = totalPages;
1029
+ const start = (page - 1) * SESSION_PAGE_SIZE;
1030
+ const slice = messages.slice(start, start + SESSION_PAGE_SIZE);
1031
+ console.log();
1032
+ console.log(theme.info(` \u2500\u2500 Transcript page ${page}/${totalPages} (messages ${start + 1}\u2013${start + slice.length} of ${total}) \u2500\u2500`));
1033
+ console.log();
1034
+ for (const msg of slice) {
1035
+ renderTranscriptMessage(msg, renderer);
1036
+ }
1037
+ const hints = [];
1038
+ if (page > 1) hints.push(`/session page ${page - 1} = earlier`);
1039
+ if (page < totalPages) hints.push(`/session page ${page + 1} = later`);
1040
+ if (hints.length > 0) {
1041
+ console.log(theme.dim(` \u2500\u2500 ${hints.join(" \xB7 ")} \u2500\u2500`));
1042
+ }
1043
+ console.log();
1044
+ }
1019
1045
  var CommandRegistry = class {
1020
1046
  commands = /* @__PURE__ */ new Map();
1021
1047
  register(command) {
@@ -1089,7 +1115,7 @@ function createDefaultCommands() {
1089
1115
  rows: [
1090
1116
  ["/clear", "Clear conversation history"],
1091
1117
  ["/compact [instruction]", "Summarize & compress history"],
1092
- ["/session new|list|load <id>", "Manage saved sessions"],
1118
+ ["/session new|list|load|show|page|remove", "Manage saved sessions (show = transcript + continue)"],
1093
1119
  ["/checkpoint [save|restore|delete] <name>", "Session checkpoints"],
1094
1120
  ["/fork [checkpoint]", "Fork session from checkpoint or current"],
1095
1121
  ["/branch [list|new|switch|delete|rename|diff|cherry-pick]", "Conversation branches"],
@@ -1363,8 +1389,8 @@ No commands match "${filter}".
1363
1389
  {
1364
1390
  name: "session",
1365
1391
  description: "Session management",
1366
- usage: "/session new|list|load <id>",
1367
- execute(args, ctx) {
1392
+ usage: "/session new|list|load <id>|show <id> [page]|page <n>|remove <id>",
1393
+ async execute(args, ctx) {
1368
1394
  const sub = args[0];
1369
1395
  if (sub === "new") {
1370
1396
  const provider = ctx.getCurrentProvider();
@@ -1391,10 +1417,10 @@ No commands match "${filter}".
1391
1417
  s.title ?? "(untitled)"
1392
1418
  ])
1393
1419
  );
1394
- } else if (sub === "load") {
1420
+ } else if (sub === "load" || sub === "show") {
1395
1421
  const id = args[1];
1396
1422
  if (!id) {
1397
- ctx.renderer.renderError("Usage: /session load <id>");
1423
+ ctx.renderer.renderError(`Usage: /session ${sub} <id>${sub === "show" ? " [page]" : ""}`);
1398
1424
  return;
1399
1425
  }
1400
1426
  const sessions = ctx.sessions.listSessions();
@@ -1416,8 +1442,70 @@ No commands match "${filter}".
1416
1442
  ctx.renderer.printSuccess(
1417
1443
  `Loaded session ${match.id.slice(0, 8)}: ${match.title ?? "(untitled)"}`
1418
1444
  );
1445
+ if (sub === "show") {
1446
+ const pageArg = args[2] !== void 0 ? Number.parseInt(args[2], 10) : void 0;
1447
+ const cur = ctx.sessions.current;
1448
+ if (cur) renderTranscriptPage(cur.messages, ctx.renderer, pageArg);
1449
+ }
1450
+ } else if (sub === "page") {
1451
+ const cur = ctx.sessions.current;
1452
+ if (!cur) {
1453
+ ctx.renderer.renderError("No active session. Use /session show <id> first.");
1454
+ return;
1455
+ }
1456
+ const n = Number.parseInt(args[1] ?? "", 10);
1457
+ if (Number.isNaN(n)) {
1458
+ ctx.renderer.renderError("Usage: /session page <n>");
1459
+ return;
1460
+ }
1461
+ renderTranscriptPage(cur.messages, ctx.renderer, n);
1462
+ } else if (sub === "remove" || sub === "delete") {
1463
+ const id = args[1];
1464
+ if (!id) {
1465
+ ctx.renderer.renderError("Usage: /session remove <id>");
1466
+ return;
1467
+ }
1468
+ const sessions = ctx.sessions.listSessions();
1469
+ const matches2 = sessions.filter((s) => s.id.startsWith(id));
1470
+ if (matches2.length === 0) {
1471
+ ctx.renderer.renderError(`Session '${id}' not found.`);
1472
+ return;
1473
+ }
1474
+ if (matches2.length > 1) {
1475
+ ctx.renderer.renderError(`Ambiguous prefix '${id}' matches ${matches2.length} sessions \u2014 be more specific:`);
1476
+ for (const m of matches2.slice(0, 10)) {
1477
+ console.log(theme.dim(` ${m.id.slice(0, 12)} ${String(m.messageCount).padStart(4)} msgs ${m.updated.toLocaleDateString()} ${m.title ?? "(untitled)"}`));
1478
+ }
1479
+ return;
1480
+ }
1481
+ const match = matches2[0];
1482
+ const isCurrent = ctx.sessions.current?.id === match.id;
1483
+ console.log();
1484
+ console.log(theme.warning(" About to permanently delete session (no undo):"));
1485
+ console.log(` ${theme.info(match.id.slice(0, 12))} ${match.provider}/${match.model} ${match.messageCount} messages ${match.updated.toLocaleDateString()}`);
1486
+ console.log(` ${match.title ?? "(untitled)"}${isCurrent ? theme.warning(" \u2190 current session") : ""}`);
1487
+ console.log();
1488
+ const choice = await ctx.select("Delete this session?", [
1489
+ { label: "No \u2014 keep it", value: "no" },
1490
+ { label: "Yes \u2014 delete permanently", value: "yes", hint: "unlinks the history file" }
1491
+ ]);
1492
+ if (choice !== "yes") {
1493
+ ctx.renderer.printInfo("Cancelled \u2014 session kept.");
1494
+ return;
1495
+ }
1496
+ if (!ctx.sessions.deleteSession(match.id)) {
1497
+ ctx.renderer.renderError("Delete failed \u2014 session file not found on disk.");
1498
+ return;
1499
+ }
1500
+ ctx.renderer.printSuccess(`Deleted session ${match.id.slice(0, 8)}: ${match.title ?? "(untitled)"}`);
1501
+ if (isCurrent) {
1502
+ ctx.sessions.createSession(ctx.getCurrentProvider(), ctx.getCurrentModel());
1503
+ ctx.resetSessionTokenUsage();
1504
+ ctx.clearDevState();
1505
+ ctx.renderer.printInfo("Current session was deleted \u2014 started a fresh session.");
1506
+ }
1419
1507
  } else {
1420
- ctx.renderer.renderError("Usage: /session new|list|load <id>");
1508
+ ctx.renderer.renderError("Usage: /session new|list|load <id>|show <id> [page]|page <n>|remove <id>");
1421
1509
  }
1422
1510
  }
1423
1511
  },
@@ -1786,7 +1874,7 @@ No tools match "${filter}".
1786
1874
  const { join: join6 } = await import("path");
1787
1875
  const { existsSync: existsSync6 } = await import("fs");
1788
1876
  const { getGitRoot: getGitRoot2 } = await import("./git-context-7KIP4X2V.js");
1789
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-FGPUBYCX.js");
1877
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-JI7VPTMJ.js");
1790
1878
  const { approveProject, hashMcpFile } = await import("./project-trust-IFM7FXEV.js");
1791
1879
  const cwd = process.cwd();
1792
1880
  const projectRoot = getGitRoot2(cwd) ?? cwd;
@@ -2847,7 +2935,7 @@ ${hint}` : "")
2847
2935
  usage: "/test [command|filter]",
2848
2936
  async execute(args, ctx) {
2849
2937
  try {
2850
- const { executeTests } = await import("./run-tests-QFTKAWW6.js");
2938
+ const { executeTests } = await import("./run-tests-GFOHEIWM.js");
2851
2939
  const argStr = args.join(" ").trim();
2852
2940
  let testArgs = {};
2853
2941
  if (argStr) {
@@ -4694,17 +4782,8 @@ ${content}
4694
4782
  parts.push(...imageParts);
4695
4783
  return { parts, hasImage: imageParts.length > 0, refs };
4696
4784
  }
4697
- var FREE_ROUND_TOOLS = /* @__PURE__ */ new Set(["write_todos"]);
4698
- var MAX_CONSECUTIVE_FREE_ROUNDS = 3;
4699
4785
  var MAX_REPEATED_TOOL_CALLS = 2;
4700
4786
  var DEFAULT_AUTO_PAUSE_INTERVAL = 50;
4701
- function partialTagTail2(s, tag) {
4702
- const max = Math.min(s.length, tag.length - 1);
4703
- for (let k = max; k > 0; k--) {
4704
- if (s.endsWith(tag.slice(0, k))) return k;
4705
- }
4706
- return 0;
4707
- }
4708
4787
  function fmtTokens(n) {
4709
4788
  if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
4710
4789
  if (n >= 1e3) return `${Math.round(n / 1024)}K`;
@@ -6407,14 +6486,12 @@ Session '${this.resumeSessionId}' not found.
6407
6486
  /**
6408
6487
  * 消费流式工具调用事件生成器,实时渲染文本内容和工具名称,
6409
6488
  * 累积完整工具调用参数后返回结构化结果。
6489
+ *
6490
+ * v0.4.181: 累积与决策(<think> 折叠 / 截断 JSON 修复 / done 守卫 /
6491
+ * index 键累积)委托给 core/agent-loop 的统一消费器(与 Web 端同一实现),
6492
+ * 此处只保留终端呈现(spinner / theme 渲染)。
6410
6493
  */
6411
6494
  async consumeToolStream(stream, spinner) {
6412
- const textParts = [];
6413
- const toolCallAccumulators = /* @__PURE__ */ new Map();
6414
- let usage;
6415
- let rawContent;
6416
- let reasoningContent;
6417
- let finishReason;
6418
6495
  let spinnerStopped = false;
6419
6496
  const stopSpinner = () => {
6420
6497
  if (!spinnerStopped) {
@@ -6422,135 +6499,36 @@ Session '${this.resumeSessionId}' not found.
6422
6499
  spinnerStopped = true;
6423
6500
  }
6424
6501
  };
6425
- let inThink = false;
6426
- let thinkShown = false;
6427
- let thinkBuf = "";
6428
- const emitText = (raw) => {
6429
- thinkBuf += raw;
6430
- let out = "";
6431
- while (thinkBuf.length > 0) {
6432
- if (!inThink) {
6433
- const open = thinkBuf.indexOf("<think>");
6434
- if (open === -1) {
6435
- const keep = partialTagTail2(thinkBuf, "<think>");
6436
- out += thinkBuf.slice(0, thinkBuf.length - keep);
6437
- thinkBuf = thinkBuf.slice(thinkBuf.length - keep);
6438
- break;
6439
- }
6440
- out += thinkBuf.slice(0, open);
6441
- thinkBuf = thinkBuf.slice(open + "<think>".length);
6442
- inThink = true;
6443
- thinkShown = true;
6444
- } else {
6445
- const close = thinkBuf.indexOf("</think>");
6446
- if (close === -1) {
6447
- const keep = partialTagTail2(thinkBuf, "</think>");
6448
- thinkBuf = thinkBuf.slice(thinkBuf.length - keep);
6449
- break;
6450
- }
6451
- thinkBuf = thinkBuf.slice(close + "</think>".length);
6452
- inThink = false;
6453
- }
6454
- }
6455
- if (out) {
6456
- process.stdout.write(out);
6457
- textParts.push(out);
6458
- }
6459
- };
6460
- try {
6461
- for await (const event of stream) {
6462
- switch (event.type) {
6463
- case "text_delta":
6464
- stopSpinner();
6465
- emitText(event.delta);
6466
- break;
6467
- case "thinking_start":
6468
- stopSpinner();
6469
- process.stdout.write(theme.dim("<think>"));
6470
- break;
6471
- case "thinking_delta":
6472
- break;
6473
- case "thinking_end":
6474
- process.stdout.write(theme.dim("</think>"));
6475
- break;
6476
- case "tool_call_start":
6477
- stopSpinner();
6478
- process.stdout.write(
6479
- theme.dim(`
6480
- \u2699 Streaming: `) + theme.toolCall(event.name) + theme.dim("...\n")
6481
- );
6482
- toolCallAccumulators.set(event.index, {
6483
- id: event.id,
6484
- name: event.name,
6485
- arguments: ""
6486
- });
6487
- break;
6488
- case "tool_call_delta": {
6489
- const acc = toolCallAccumulators.get(event.index);
6490
- if (acc) {
6491
- acc.arguments += event.argumentsDelta;
6492
- }
6493
- break;
6494
- }
6495
- case "tool_call_end":
6496
- break;
6497
- case "done":
6498
- if (event.usage) usage = event.usage;
6499
- if (event.rawContent) rawContent = event.rawContent;
6500
- if (event.reasoningContent) reasoningContent = event.reasoningContent;
6501
- if (event.finishReason) finishReason = event.finishReason;
6502
- break;
6503
- }
6504
- }
6505
- } catch (err) {
6506
- if (err instanceof Error && (err.name === "AbortError" || err.message.includes("aborted"))) {
6502
+ const result = await consumeToolCallStream(stream, {
6503
+ onText: (visible) => {
6507
6504
  stopSpinner();
6508
- process.stdout.write(theme.dim("\n[interrupted]\n"));
6509
- return {
6510
- textContent: textParts.join(""),
6511
- toolCalls: [],
6512
- usage,
6513
- rawContent,
6514
- reasoningContent,
6515
- finishReason
6516
- };
6517
- }
6518
- throw err;
6519
- }
6520
- if (!inThink && thinkBuf) {
6521
- process.stdout.write(thinkBuf);
6522
- textParts.push(thinkBuf);
6523
- thinkBuf = "";
6524
- }
6525
- const toolCalls = [];
6526
- for (const [, acc] of toolCallAccumulators) {
6527
- let parsedArgs;
6528
- try {
6529
- parsedArgs = JSON.parse(acc.arguments || "{}");
6530
- } catch {
6531
- const truncated = acc.arguments.trimEnd();
6532
- const lastComma = truncated.lastIndexOf(",");
6533
- const fixed = lastComma > 0 ? truncated.slice(0, lastComma) + "}" : truncated.slice(0, truncated.indexOf("{") + 1) + "}";
6534
- try {
6535
- parsedArgs = JSON.parse(fixed);
6536
- } catch {
6537
- parsedArgs = {};
6538
- }
6505
+ process.stdout.write(visible);
6506
+ },
6507
+ onThinkingStart: () => {
6508
+ stopSpinner();
6509
+ process.stdout.write(theme.dim("<think>"));
6510
+ },
6511
+ // thinking_delta 内容折叠,不显示详情(与现有行为一致)
6512
+ onThinkingEnd: () => {
6513
+ process.stdout.write(theme.dim("</think>"));
6514
+ },
6515
+ onToolCallStart: (_index, _id, name) => {
6516
+ stopSpinner();
6517
+ process.stdout.write(
6518
+ theme.dim(`
6519
+ \u2699 Streaming: `) + theme.toolCall(name) + theme.dim("...\n")
6520
+ );
6521
+ },
6522
+ onWarn: (message) => {
6523
+ process.stderr.write(`[warn] ${message}
6524
+ `);
6539
6525
  }
6540
- toolCalls.push({
6541
- id: acc.id,
6542
- name: acc.name,
6543
- arguments: parsedArgs
6544
- });
6526
+ });
6527
+ if (result.aborted) {
6528
+ stopSpinner();
6529
+ process.stdout.write(theme.dim("\n[interrupted]\n"));
6545
6530
  }
6546
- return {
6547
- textContent: textParts.join(""),
6548
- toolCalls,
6549
- usage,
6550
- rawContent,
6551
- reasoningContent,
6552
- finishReason
6553
- };
6531
+ return result;
6554
6532
  }
6555
6533
  async handleChatWithTools(provider, messages, modelOverride) {
6556
6534
  const session = this.sessions.current;
@@ -6592,25 +6570,7 @@ Session '${this.resumeSessionId}' not found.
6592
6570
  const autoPauseIntervalRaw = this.config.get("autoPauseInterval");
6593
6571
  const autoPauseInterval = typeof autoPauseIntervalRaw === "number" ? autoPauseIntervalRaw : DEFAULT_AUTO_PAUSE_INTERVAL;
6594
6572
  const { stable: toolStable, volatile: toolVolatile } = this.buildCurrentSystemPrompt();
6595
- const pauseHint = autoPauseInterval > 0 ? `
6596
- - Every ${autoPauseInterval} rounds the user will be asked whether to continue \u2014 use this as a natural checkpoint to report progress.` : "";
6597
- const roundBudgetHint = this.planMode ? `
6598
-
6599
- [Tool Round Budget \u2014 Plan Mode]
6600
- You have a maximum of ${maxToolRounds} tool call rounds. You are in READ-ONLY Plan Mode:
6601
- - Only use: read_file, list_dir, grep_files, glob_files, ask_user, write_todos
6602
- - Do NOT attempt to call bash, write_file, edit_file \u2014 they are disabled
6603
- - Do NOT write shell commands or code blocks as a substitute for tool calls
6604
- - Do NOT read the same file more than once
6605
- - Call write_todos ONCE to present your plan, then give a text summary
6606
- - If the user asks you to execute anything, respond: "Please type /plan execute to switch to execute mode."${pauseHint}` : `
6607
-
6608
- [Tool Round Budget]
6609
- You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan efficiently:
6610
- - Prefer batch operations (e.g. global find-and-replace) over repetitive single edits.
6611
- - Do NOT read the same file more than once \u2014 use the content from previous reads.
6612
- - Prioritize the most critical tasks first in case rounds run out.
6613
- - When remaining rounds are low, focus on completing the current task and summarizing.${pauseHint}`;
6573
+ const roundBudgetHint = buildRoundBudgetHint({ maxToolRounds, autoPauseInterval, planMode: this.planMode });
6614
6574
  const systemPrompt = toolStable + TOOL_CALL_REMINDER + roundBudgetHint + (mcpBudgetNote ? `
6615
6575
 
6616
6576
  ${mcpBudgetNote}` : "");
@@ -6620,22 +6580,15 @@ ${mcpBudgetNote}` : "");
6620
6580
  const spinner = this.renderer.showSpinner("Thinking...");
6621
6581
  const roundUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
6622
6582
  const supportsStreamingTools = useStreaming && typeof provider.chatWithToolsStream === "function";
6623
- let consecutiveFreeRounds = 0;
6624
6583
  let lastToolCallSignature = "";
6625
6584
  let repeatedToolCallCount = 0;
6626
- let emptyResponseRetries = 0;
6627
- let warnedCtx80 = false;
6628
6585
  const roundToolHistory = [];
6586
+ const budgetWarner = new BudgetWarner(maxToolRounds);
6587
+ const emptyGuard = new EmptyResponseGuard();
6588
+ const ctxMonitor = new ContextPressureMonitor();
6589
+ const freeRounds = new FreeRoundTracker();
6629
6590
  this.setupInterjectionListener();
6630
6591
  try {
6631
- const warnNoteAt = Math.max(10, Math.floor(maxToolRounds * 0.2));
6632
- const warnLowAt = Math.max(5, Math.floor(maxToolRounds * 0.1));
6633
- const warnCriticalAt = Math.max(3, Math.floor(maxToolRounds * 0.05));
6634
- const warnLowEff = Math.min(warnLowAt, warnNoteAt - 1);
6635
- const warnCriticalEff = Math.min(warnCriticalAt, warnLowEff - 1);
6636
- let warnedNote = false;
6637
- let warnedLow = false;
6638
- let warnedCritical = false;
6639
6592
  for (let round = 0; round < maxToolRounds; round++) {
6640
6593
  this.toolExecutor.setRoundInfo(round + 1, maxToolRounds);
6641
6594
  if (this.toolExecutor.pendingSlashCommand) {
@@ -6652,29 +6605,14 @@ ${mcpBudgetNote}` : "");
6652
6605
  `));
6653
6606
  extraMessages.push({ role: "user", content: cmd });
6654
6607
  }
6655
- const roundsLeft = maxToolRounds - round;
6656
- if (!warnedCritical && roundsLeft <= warnCriticalEff) {
6657
- warnedCritical = true;
6658
- extraMessages.push({
6659
- role: "user",
6660
- content: `\u{1F6A8} Critical budget: Only ${roundsLeft} rounds left! Wrap up NOW \u2014 complete the current operation and give a final summary. Do NOT start new tasks.`
6661
- });
6662
- process.stdout.write(theme.error(` \u{1F6A8} Critical: ${roundsLeft} rounds remaining
6663
- `));
6664
- } else if (!warnedLow && roundsLeft <= warnLowEff) {
6665
- warnedLow = true;
6666
- extraMessages.push({
6667
- role: "user",
6668
- content: `\u26A0\uFE0F Budget warning: Only ${roundsLeft} tool rounds remaining. Prioritize completing the most critical task. Use efficient approaches (batch edits, fewer reads). If you cannot finish everything, summarize what's done and what remains.`
6669
- });
6670
- process.stdout.write(theme.warning(` \u26A0\uFE0F Low budget: ${roundsLeft} rounds remaining
6608
+ const budgetWarning = budgetWarner.check(maxToolRounds - round);
6609
+ if (budgetWarning) {
6610
+ extraMessages.push({ role: "user", content: budgetWarning.injectMessage });
6611
+ if (budgetWarning.displayMessage) {
6612
+ const paint = budgetWarning.level === "critical" ? theme.error : theme.warning;
6613
+ process.stdout.write(paint(` ${budgetWarning.displayMessage}
6671
6614
  `));
6672
- } else if (!warnedNote && roundsLeft <= warnNoteAt) {
6673
- warnedNote = true;
6674
- extraMessages.push({
6675
- role: "user",
6676
- content: `\u{1F4CA} Budget note: ${roundsLeft} tool rounds remaining out of ${maxToolRounds}. Plan your remaining work efficiently \u2014 use batch operations (e.g., replaceAll) when possible.`
6677
- });
6615
+ }
6678
6616
  }
6679
6617
  if (this._userInterjection) {
6680
6618
  const msg = this._userInterjection;
@@ -6686,13 +6624,13 @@ ${mcpBudgetNote}` : "");
6686
6624
  const ctxWindow = this.getContextWindowSize();
6687
6625
  if (ctxWindow > 0) {
6688
6626
  const reqTokens = this.estimateRequestTokens(systemPrompt, extraMessages);
6689
- const reqRatio = reqTokens / ctxWindow;
6690
- if (reqRatio >= 0.95) {
6627
+ const pressure = ctxMonitor.check(reqTokens, ctxWindow);
6628
+ if (pressure.action === "abort") {
6691
6629
  spinner.stop();
6692
6630
  process.stderr.write(
6693
6631
  theme.error(
6694
6632
  `
6695
- \u26A0 Context at ${Math.round(reqRatio * 100)}% of ${fmtTokens(ctxWindow)} \u2014 aborting agentic loop before API rejection.
6633
+ \u26A0 Context at ${Math.round(pressure.ratio * 100)}% of ${fmtTokens(ctxWindow)} \u2014 aborting agentic loop before API rejection.
6696
6634
  `
6697
6635
  )
6698
6636
  );
@@ -6712,20 +6650,16 @@ ${mcpBudgetNote}` : "");
6712
6650
  }
6713
6651
  }
6714
6652
  return;
6715
- } else if (reqRatio >= 0.8 && !warnedCtx80) {
6716
- warnedCtx80 = true;
6653
+ } else if (pressure.action === "warn") {
6717
6654
  spinner.stop();
6718
6655
  process.stdout.write(
6719
6656
  theme.warning(
6720
6657
  `
6721
- \u26A0 Context at ${Math.round(reqRatio * 100)}% of ${fmtTokens(ctxWindow)} \u2014 asking AI to wrap up.
6658
+ \u26A0 Context at ${Math.round(pressure.ratio * 100)}% of ${fmtTokens(ctxWindow)} \u2014 asking AI to wrap up.
6722
6659
  `
6723
6660
  )
6724
6661
  );
6725
- extraMessages.push({
6726
- role: "user",
6727
- content: `\u26A0\uFE0F Context pressure: ~${Math.round(reqRatio * 100)}% of the ${fmtTokens(ctxWindow)} context window is used. Avoid reading more files or running broad scans. Finish the current critical step, then produce a final summary. Every unnecessary tool call now risks breaking the conversation.`
6728
- });
6662
+ extraMessages.push({ role: "user", content: pressure.injectMessage });
6729
6663
  spinner.start(`Thinking... (round ${round + 1}/${maxToolRounds})`);
6730
6664
  }
6731
6665
  }
@@ -6797,12 +6731,7 @@ ${mcpBudgetNote}` : "");
6797
6731
  (p, m) => p.chatWithTools({ ...chatRequest, model: m }, toolDefs)
6798
6732
  );
6799
6733
  }
6800
- if (result.usage) {
6801
- roundUsage.inputTokens += result.usage.inputTokens;
6802
- roundUsage.outputTokens += result.usage.outputTokens;
6803
- roundUsage.cacheCreationTokens += result.usage.cacheCreationTokens ?? 0;
6804
- roundUsage.cacheReadTokens += result.usage.cacheReadTokens ?? 0;
6805
- }
6734
+ accumulateUsage(roundUsage, result.usage);
6806
6735
  if ("content" in result) {
6807
6736
  const hasWriteTools = toolDefs.some((t) => t.name === "write_file" || t.name === "edit_file");
6808
6737
  const alreadyWrote = hadPreviousWriteToolCalls(extraMessages);
@@ -6831,32 +6760,25 @@ ${mcpBudgetNote}` : "");
6831
6760
  }
6832
6761
  if (!result.content || result.content.trim() === "") {
6833
6762
  const fr = "finishReason" in result ? result.finishReason : void 0;
6834
- const reasonLabel = fr === "length" ? "output limit reached (finish_reason=length)" : fr === "content_filter" ? "content blocked (finish_reason=content_filter)" : fr ? `empty response (finish_reason=${fr})` : "empty response";
6835
- if (emptyResponseRetries === 0 && round < maxToolRounds - 1) {
6836
- emptyResponseRetries++;
6763
+ const decision = emptyGuard.onEmpty(round < maxToolRounds - 1, fr);
6764
+ if (decision.action === "nudge") {
6837
6765
  spinner.stop();
6838
6766
  if (alreadyRendered) process.stdout.write("\n");
6839
- process.stderr.write(
6840
- theme.warning(`\u26A0 ${reasonLabel} \u2014 nudging AI to continue...
6841
- `)
6842
- );
6843
- extraMessages.push({
6844
- role: "user",
6845
- content: "Your previous response was empty \u2014 no text and no tool calls. This usually means the context window is nearly full. Please either: (1) continue the task by calling the next tool you need, or (2) give a concise final text summary of what has been accomplished so far and what remains. Do NOT repeat earlier long outputs."
6846
- });
6767
+ process.stderr.write(theme.warning(`${decision.displayMessage}
6768
+ `));
6769
+ extraMessages.push({ role: "user", content: decision.injectMessage });
6847
6770
  spinner.start(`Retrying... (round ${round + 2}/${maxToolRounds})`);
6848
6771
  continue;
6849
6772
  }
6850
6773
  spinner.stop();
6851
6774
  if (alreadyRendered) process.stdout.write("\n");
6852
- const frHint = fr === "length" ? "Output token limit hit \u2014 try /compact to reduce context, raise maxTokens, or /model to switch." : fr === "content_filter" ? "Content was blocked by the provider filter." : "Context window may be exhausted or max_tokens too low.";
6853
6775
  process.stderr.write(
6854
6776
  theme.error(`
6855
- \u26A0 AI returned empty responses twice in a row. Stopping agentic loop.
6777
+ ${decision.displayMessage}
6856
6778
  `)
6857
6779
  );
6858
6780
  process.stderr.write(
6859
- theme.dim(` ${frHint}
6781
+ theme.dim(` ${decision.hint}
6860
6782
  Try: /compact, /clear, or /model to switch.
6861
6783
 
6862
6784
  `)
@@ -6870,7 +6792,7 @@ ${mcpBudgetNote}` : "");
6870
6792
  }
6871
6793
  return;
6872
6794
  }
6873
- emptyResponseRetries = 0;
6795
+ emptyGuard.onNonEmpty();
6874
6796
  spinner.stop();
6875
6797
  const finalContent = result.content;
6876
6798
  if (!alreadyRendered) {
@@ -7181,14 +7103,8 @@ ${systemPromptVolatile}` : systemPrompt;
7181
7103
  });
7182
7104
  }
7183
7105
  }
7184
- const allFree = result.toolCalls.every((tc) => FREE_ROUND_TOOLS.has(tc.name));
7185
- if (allFree) {
7186
- consecutiveFreeRounds++;
7187
- if (consecutiveFreeRounds <= MAX_CONSECUTIVE_FREE_ROUNDS) {
7188
- round--;
7189
- }
7190
- } else {
7191
- consecutiveFreeRounds = 0;
7106
+ if (freeRounds.apply(result.toolCalls.map((tc) => tc.name))) {
7107
+ round--;
7192
7108
  }
7193
7109
  const currentSignature = result.toolCalls.map((tc) => `${tc.name}:${JSON.stringify(tc.arguments)}`).join("|");
7194
7110
  if (currentSignature === lastToolCallSignature) {
@@ -7226,15 +7142,8 @@ ${systemPromptVolatile}` : systemPrompt;
7226
7142
  process.stdout.write("\n");
7227
7143
  process.stdout.write(theme.warning(`\u23F8 Auto-pause: ${effectiveRound}/${maxToolRounds} rounds used, ${remaining} remaining
7228
7144
  `));
7229
- const recentHistory = roundToolHistory.slice(-autoPauseInterval);
7230
- if (recentHistory.length > 0) {
7231
- const toolCounts = /* @__PURE__ */ new Map();
7232
- for (const rh of recentHistory) {
7233
- for (const t of rh.tools) {
7234
- toolCounts.set(t, (toolCounts.get(t) || 0) + 1);
7235
- }
7236
- }
7237
- const summary = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).map(([name, count]) => count > 1 ? `${name}\xD7${count}` : name).join(", ");
7145
+ const summary = summarizeRecentTools(roundToolHistory, autoPauseInterval);
7146
+ if (summary) {
7238
7147
  process.stdout.write(theme.dim(` Tools used: ${summary}
7239
7148
  `));
7240
7149
  }
@@ -7253,10 +7162,7 @@ ${systemPromptVolatile}` : systemPrompt;
7253
7162
  this.setupInterjectionListener();
7254
7163
  if (pauseResponse === "n" || pauseResponse === "N" || pauseResponse === "\x1B") {
7255
7164
  process.stdout.write(theme.warning("\u26A1 Stopped by user at auto-pause checkpoint\n"));
7256
- extraMessages.push({
7257
- role: "user",
7258
- content: `The user has stopped the task at round ${effectiveRound}/${maxToolRounds}. Do not call any more tools. Summarize what has been completed and what remains.`
7259
- });
7165
+ extraMessages.push({ role: "user", content: buildUserStopMessage(effectiveRound, maxToolRounds) });
7260
7166
  break;
7261
7167
  } else if (pauseResponse && pauseResponse !== "y" && pauseResponse !== "Y" && pauseResponse !== "") {
7262
7168
  process.stdout.write(theme.warning(`\u26A1 Redirect: "${pauseResponse}"
@@ -7276,13 +7182,7 @@ ${systemPromptVolatile}` : systemPrompt;
7276
7182
  spinner.start("Generating summary...");
7277
7183
  const summaryExtra = [
7278
7184
  ...extraMessages,
7279
- {
7280
- role: "user",
7281
- content: `You have used all ${maxToolRounds} tool call rounds. Do not call any more tools. Summarize in text:
7282
- 1. What work has been completed so far
7283
- 2. What tasks remain unfinished
7284
- 3. What the user can do next (e.g. send another request to continue)`
7285
- }
7185
+ { role: "user", content: buildRoundsExhaustedPrompt(maxToolRounds) }
7286
7186
  ];
7287
7187
  const summaryResult = await provider.chatWithTools(
7288
7188
  {
@@ -7633,7 +7533,7 @@ program.command("web").description("Start Web UI server with browser-based chat
7633
7533
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
7634
7534
  process.exit(1);
7635
7535
  }
7636
- const { startWebServer } = await import("./server-KT6GRCM7.js");
7536
+ const { startWebServer } = await import("./server-2CBNRT2W.js");
7637
7537
  await startWebServer({ port, host: options.host });
7638
7538
  });
7639
7539
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | logout-all <name> | migrate <name>)").action(async (action, username) => {
@@ -7800,12 +7700,12 @@ program.command("sessions").description("List recent conversation sessions").opt
7800
7700
  console.log(footer + "\n");
7801
7701
  });
7802
7702
  program.command("doctor").description("Health check: API keys, config, MCP, recent crashes, tool usage, disk usage").option("--json", "Output as JSON (for scripting)").option("--reset-stats", "Reset accumulated tool usage statistics").action(async (options) => {
7803
- const { runDoctorCli } = await import("./doctor-cli-C54OKMG2.js");
7703
+ const { runDoctorCli } = await import("./doctor-cli-KSB3MLFV.js");
7804
7704
  await runDoctorCli({ json: !!options.json, resetStats: !!options.resetStats });
7805
7705
  });
7806
7706
  program.command("batch <action> [arg] [arg2]").description("Anthropic Message Batches: submit | list | status <id> | results <id> [out] | cancel <id>").option("--dry-run", "Parse and validate input without submitting (submit only)").action(async (action, arg, arg2, options) => {
7807
7707
  try {
7808
- const batch = await import("./batch-OJJI6TKB.js");
7708
+ const batch = await import("./batch-ZBKVDSMZ.js");
7809
7709
  switch (action) {
7810
7710
  case "submit":
7811
7711
  if (!arg) {
@@ -7848,7 +7748,7 @@ program.command("batch <action> [arg] [arg2]").description("Anthropic Message Ba
7848
7748
  }
7849
7749
  });
7850
7750
  program.command("mcp-serve").description("Start an MCP server over STDIO, exposing aicli's built-in tools to Claude Desktop / Cursor / other MCP clients").option("--allow-destructive", "Allow bash / run_interactive / task_create (always destructive in MCP mode)").option("--allow-outside-cwd", "Allow tool path arguments to escape the sandbox root \u2014 disabled by default").option("--tools <list>", "Comma-separated whitelist of tools to expose (default: all eligible tools)").option("--cwd <path>", "Working directory AND sandbox root (default: current directory)").action(async (options) => {
7851
- const { startMcpServer } = await import("./server-I6Y5TP2Z.js");
7751
+ const { startMcpServer } = await import("./server-TXA5VTOS.js");
7852
7752
  await startMcpServer({
7853
7753
  allowDestructive: !!options.allowDestructive,
7854
7754
  allowOutsideCwd: !!options.allowOutsideCwd,
@@ -7857,7 +7757,7 @@ program.command("mcp-serve").description("Start an MCP server over STDIO, exposi
7857
7757
  });
7858
7758
  });
7859
7759
  program.command("ci").description("Headless PR review (code + security) \u2014 reads git/gh diff, optionally posts to PR. Designed for GitHub Actions.").option("--pr <num>", "PR number; diff fetched via `gh pr diff <num>`", (v) => parseInt(v, 10)).option("--base <ref>", "Base ref for `git diff <ref>...HEAD` (ignored when --pr set)").option("--post", "Post review as a PR comment (requires gh CLI + GH_TOKEN, needs --pr)").option("--no-update", "Always create a new comment instead of updating the previous aicli review").option("--skip-code", "Skip the code review section").option("--skip-security", "Skip the security review section").option("--detailed", "Use the detailed code-review prompt").option("--max-diff <n>", "Max diff chars sent to the model (default 30000)", (v) => parseInt(v, 10)).option("--provider <id>", "Override provider (default: config.defaultProvider)").option("--model <id>", "Override model").option("--dry-run", "Print result to stdout instead of posting (overrides --post)").action(async (options) => {
7860
- const { runCi } = await import("./ci-EBN6VQ2Z.js");
7760
+ const { runCi } = await import("./ci-6WGF6ID6.js");
7861
7761
  const result = await runCi({
7862
7762
  pr: options.pr,
7863
7763
  base: options.base,
@@ -8002,7 +7902,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
8002
7902
  }),
8003
7903
  config.get("customProviders")
8004
7904
  );
8005
- const { startHub } = await import("./hub-YVNTGDKW.js");
7905
+ const { startHub } = await import("./hub-DGEYFJPP.js");
8006
7906
  await startHub(
8007
7907
  {
8008
7908
  topic: topic ?? "",