jinzd-ai-cli 0.4.51 → 0.4.53

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/README.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  - **8 Built-in Providers** — Claude, Gemini, DeepSeek, OpenAI, Zhipu GLM, Kimi, OpenRouter (300+ models), **Ollama** (local models, no API key needed)
25
25
  - **3 Interfaces** — Terminal CLI, browser Web UI (`aicli web`), Electron desktop app
26
- - **Agentic Tool Calling** — AI autonomously runs shell commands, reads/writes files, searches code, fetches web, runs tests (up to 25 rounds)
26
+ - **Agentic Tool Calling** — AI autonomously runs shell commands, reads/writes files, searches code, fetches web, runs tests (default 200 rounds, configurable up to 10000 via `config.maxToolRounds` or `--max-tool-rounds`)
27
27
  - **Streaming Tool Use** — Real-time streaming of AI reasoning and tool calls as they happen
28
28
  - **Sub-Agents** — Delegate complex subtasks to isolated child agents with independent tool loops
29
29
  - **Extended Thinking** — Claude deep reasoning mode with `/think` toggle
package/README.zh-CN.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  - **8 大内置 Provider** — Claude、Gemini、DeepSeek、OpenAI、智谱 GLM、Kimi、OpenRouter(300+ 模型)、**Ollama**(本地模型,无需 API Key)
17
17
  - **三种使用方式** — 终端 CLI、浏览器 Web UI(`aicli web`)、Electron 桌面应用
18
- - **Agentic 工具调用** — AI 自主执行 bash 命令、读写文件、搜索代码、抓取网页、运行测试(每轮最多 25 次)
18
+ - **Agentic 工具调用** — AI 自主执行 bash 命令、读写文件、搜索代码、抓取网页、运行测试(默认 200 轮,可通过 `config.maxToolRounds` 或 `--max-tool-rounds` 调整,上限 10000)
19
19
  - **流式工具调用** — 实时流式展示 AI 推理过程和工具调用
20
20
  - **子代理系统** — 将复杂子任务委派给独立子代理执行
21
21
  - **深度推理** — Claude Extended Thinking,`/think` 一键切换
@@ -7,7 +7,7 @@ import {
7
7
  ProviderNotFoundError,
8
8
  RateLimitError,
9
9
  schemaToJsonSchema
10
- } from "./chunk-VTMHZCWZ.js";
10
+ } from "./chunk-IXDGWT2Z.js";
11
11
  import {
12
12
  APP_NAME,
13
13
  CONFIG_DIR_NAME,
@@ -20,7 +20,7 @@ import {
20
20
  MCP_TOOL_PREFIX,
21
21
  PLUGINS_DIR_NAME,
22
22
  VERSION
23
- } from "./chunk-KGJYHTC2.js";
23
+ } from "./chunk-YIMTDKUW.js";
24
24
 
25
25
  // src/config/config-manager.ts
26
26
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -167,6 +167,16 @@ var ConfigSchema = z.object({
167
167
  // 当对话估算 token 数超过模型 contextWindow 的 80% 时,自动触发 compact 压缩旧消息
168
168
  // 默认开启。设为 false 则仅在手动 /compact 时压缩
169
169
  autoCompact: z.boolean().default(true),
170
+ // Agentic 工具调用循环单次对话最大轮次(默认 200)。
171
+ // 超过此值后 AI 被强制停止调用工具并生成总结。
172
+ // 建议范围:25(保守)~ 1000(宽松,接近 Claude Code)。
173
+ // CLI `--max-tool-rounds <n>` 可覆盖此值。
174
+ maxToolRounds: z.number().int().min(1).max(1e4).default(200),
175
+ // 单次工具输出(如 read_file、bash、grep_files)返回给 AI 的最大字符数上限。
176
+ // 默认 500_000 (~500K chars ≈ 6000-8000 行代码)。
177
+ // 实际上限还会受模型 contextWindow 动态约束(取 contextWindow/4 作为下限)。
178
+ // 设置为 0 或未配置时使用默认值;不建议设为小于 12_000 或大于模型 contextWindow/2。
179
+ maxToolOutputChars: z.number().int().min(0).default(5e5),
170
180
  // 插件加载开关(安全控制)
171
181
  // 默认 false:不自动加载 ~/.aicli/plugins/ 中的插件文件。
172
182
  // 插件以完整 Node.js 权限在主进程中执行(可读写文件、访问网络、执行命令),
@@ -2527,6 +2537,19 @@ var SessionManager = class {
2527
2537
  this._current = session;
2528
2538
  return session;
2529
2539
  }
2540
+ /**
2541
+ * 直接设置当前会话(用于从内存缓存恢复未保存的会话)。
2542
+ * 与 `loadSession` 不同,此方法不读取磁盘,也不抛出错误。
2543
+ * Web 多 Tab 场景下,SessionHandler 会维护一份未保存会话的内存缓存,
2544
+ * 切换 Tab 时通过此方法将缓存中的会话设为当前会话,避免"Session not found"。
2545
+ */
2546
+ setCurrent(session) {
2547
+ this._current = session;
2548
+ }
2549
+ /** 清除当前会话引用(下次访问将触发 lazy 创建)。 */
2550
+ clearCurrent() {
2551
+ this._current = null;
2552
+ }
2530
2553
  async save() {
2531
2554
  if (!this._current) return;
2532
2555
  mkdirSync2(this.historyDir, { recursive: true });
@@ -4,12 +4,13 @@ import {
4
4
  } from "./chunk-4BKXL7SM.js";
5
5
  import {
6
6
  CONFIG_DIR_NAME,
7
+ DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
7
8
  MEMORY_FILE_NAME,
8
9
  SUBAGENT_ALLOWED_TOOLS,
9
10
  SUBAGENT_DEFAULT_MAX_ROUNDS,
10
11
  SUBAGENT_MAX_ROUNDS_LIMIT,
11
12
  runTestsTool
12
- } from "./chunk-KGJYHTC2.js";
13
+ } from "./chunk-YIMTDKUW.js";
13
14
 
14
15
  // src/tools/builtin/bash.ts
15
16
  import { execSync } from "child_process";
@@ -624,7 +625,7 @@ function tryExtractPdfText(absPath) {
624
625
  var readFileTool = {
625
626
  definition: {
626
627
  name: "read_file",
627
- description: "Read text file contents. Automatically detects binary files (PDF/images/etc) and returns a hint instead of garbled output.",
628
+ description: "Read text file contents. Automatically detects binary files (PDF/images/etc) and returns a hint instead of garbled output. For very large files, use offset/limit to read specific line ranges (1-based line numbers).",
628
629
  parameters: {
629
630
  path: {
630
631
  type: "string",
@@ -636,6 +637,16 @@ var readFileTool = {
636
637
  description: "Encoding format, defaults to utf-8",
637
638
  enum: ["utf-8", "utf8", "ascii", "base64"],
638
639
  required: false
640
+ },
641
+ offset: {
642
+ type: "number",
643
+ description: "Starting line number (1-based). When set, only lines from this line onwards are returned. Useful for paging through very large files.",
644
+ required: false
645
+ },
646
+ limit: {
647
+ type: "number",
648
+ description: "Maximum number of lines to return starting from offset. When omitted, reads to end of file. Combine with offset to read a specific line range.",
649
+ required: false
639
650
  }
640
651
  },
641
652
  dangerous: false
@@ -643,6 +654,10 @@ var readFileTool = {
643
654
  async execute(args) {
644
655
  const filePath = String(args["path"] ?? "");
645
656
  const encoding = args["encoding"] ?? "utf-8";
657
+ const rawOffset = args["offset"];
658
+ const rawLimit = args["limit"];
659
+ const offsetLine = typeof rawOffset === "number" && Number.isFinite(rawOffset) && rawOffset > 0 ? Math.floor(rawOffset) : void 0;
660
+ const limitLines = typeof rawLimit === "number" && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : void 0;
646
661
  if (!filePath) throw new ToolError("read_file", "path is required");
647
662
  const normalizedPath = resolve2(filePath);
648
663
  if (!existsSync3(normalizedPath)) {
@@ -679,8 +694,8 @@ Use the bash tool to read in segments, e.g.:
679
694
  if (ext === ".pdf") {
680
695
  const pdfText = tryExtractPdfText(normalizedPath);
681
696
  if (pdfText) {
682
- const lines2 = pdfText.split("\n").length;
683
- return `[PDF extracted: ${filePath} | ${lines2} lines]
697
+ const lines = pdfText.split("\n").length;
698
+ return `[PDF extracted: ${filePath} | ${lines} lines]
684
699
 
685
700
  ${pdfText}`;
686
701
  }
@@ -724,8 +739,26 @@ This file contains binary data and cannot be read as text.
724
739
  If needed, use the bash tool to run an appropriate conversion program.`;
725
740
  }
726
741
  const content = buf.toString(encoding);
727
- const lines = content.split("\n").length;
728
- return `${sensitiveWarning}[File: ${filePath} | ${lines} lines]
742
+ const allLines = content.split("\n");
743
+ const totalLines = allLines.length;
744
+ if (offsetLine !== void 0 || limitLines !== void 0) {
745
+ const startIdx = offsetLine !== void 0 ? Math.max(0, offsetLine - 1) : 0;
746
+ if (startIdx >= totalLines) {
747
+ return `${sensitiveWarning}[File: ${filePath} | ${totalLines} lines | offset=${offsetLine} beyond EOF]
748
+
749
+ The file has only ${totalLines} lines. Requested offset ${offsetLine} is past end-of-file.`;
750
+ }
751
+ const endIdx = limitLines !== void 0 ? Math.min(totalLines, startIdx + limitLines) : totalLines;
752
+ const slice = allLines.slice(startIdx, endIdx).join("\n");
753
+ const actualStart = startIdx + 1;
754
+ const actualEnd = endIdx;
755
+ const remaining = totalLines - endIdx;
756
+ const rangeNote = remaining > 0 ? `${totalLines} lines total, showing ${actualStart}-${actualEnd} (${remaining} more lines after this range; read again with offset=${endIdx + 1} to continue)` : `${totalLines} lines total, showing ${actualStart}-${actualEnd} (end of file)`;
757
+ return `${sensitiveWarning}[File: ${filePath} | ${rangeNote}]
758
+
759
+ ${slice}`;
760
+ }
761
+ return `${sensitiveWarning}[File: ${filePath} | ${totalLines} lines]
729
762
 
730
763
  ${content}`;
731
764
  }
@@ -983,12 +1016,25 @@ function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "co
983
1016
 
984
1017
  // src/tools/truncate.ts
985
1018
  var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
1019
+ var activeCap = DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP;
1020
+ function setMaxOutputCap(cap) {
1021
+ if (!cap || cap <= 0) {
1022
+ activeCap = DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP;
1023
+ } else {
1024
+ activeCap = Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, cap);
1025
+ }
1026
+ if (lastContextWindow > 0) {
1027
+ activeMaxChars = getMaxOutputChars(lastContextWindow);
1028
+ }
1029
+ }
986
1030
  function getMaxOutputChars(contextWindow) {
987
1031
  if (!contextWindow || contextWindow <= 0) return DEFAULT_MAX_TOOL_OUTPUT_CHARS;
988
- return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), 12e4));
1032
+ return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), activeCap));
989
1033
  }
990
1034
  var activeMaxChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
1035
+ var lastContextWindow = 0;
991
1036
  function setContextWindow(contextWindow) {
1037
+ lastContextWindow = contextWindow;
992
1038
  activeMaxChars = getMaxOutputChars(contextWindow);
993
1039
  }
994
1040
  function getActiveMaxChars() {
@@ -4139,6 +4185,7 @@ export {
4139
4185
  renderDiff,
4140
4186
  runHook,
4141
4187
  checkPermission,
4188
+ setMaxOutputCap,
4142
4189
  setContextWindow,
4143
4190
  truncateOutput,
4144
4191
  ToolExecutor,
@@ -6,7 +6,7 @@ import { platform } from "os";
6
6
  import chalk from "chalk";
7
7
 
8
8
  // src/core/constants.ts
9
- var VERSION = "0.4.51";
9
+ var VERSION = "0.4.53";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -19,6 +19,8 @@ var MEMORY_FILE_NAME = "memory.md";
19
19
  var MEMORY_MAX_CHARS = 1e4;
20
20
  var DEV_STATE_FILE_NAME = "dev-state.md";
21
21
  var DEFAULT_MAX_TOKENS = 8192;
22
+ var DEFAULT_MAX_TOOL_ROUNDS = 200;
23
+ var DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP = 5e5;
22
24
  var MCP_TOOL_PREFIX = "mcp__";
23
25
  var MCP_PROJECT_CONFIG_NAME = ".mcp.json";
24
26
  var MCP_CONNECT_TIMEOUT = 3e4;
@@ -487,6 +489,8 @@ export {
487
489
  MEMORY_MAX_CHARS,
488
490
  DEV_STATE_FILE_NAME,
489
491
  DEFAULT_MAX_TOKENS,
492
+ DEFAULT_MAX_TOOL_ROUNDS,
493
+ DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
490
494
  MCP_TOOL_PREFIX,
491
495
  MCP_PROJECT_CONFIG_NAME,
492
496
  MCP_CONNECT_TIMEOUT,
@@ -8,7 +8,7 @@ import { platform } from "os";
8
8
  import chalk from "chalk";
9
9
 
10
10
  // src/core/constants.ts
11
- var VERSION = "0.4.51";
11
+ var VERSION = "0.4.53";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
@@ -21,6 +21,8 @@ var MEMORY_FILE_NAME = "memory.md";
21
21
  var MEMORY_MAX_CHARS = 1e4;
22
22
  var DEV_STATE_FILE_NAME = "dev-state.md";
23
23
  var DEFAULT_MAX_TOKENS = 8192;
24
+ var DEFAULT_MAX_TOOL_ROUNDS = 200;
25
+ var DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP = 5e5;
24
26
  var MCP_TOOL_PREFIX = "mcp__";
25
27
  var MCP_PROJECT_CONFIG_NAME = ".mcp.json";
26
28
  var MCP_CONNECT_TIMEOUT = 3e4;
@@ -491,6 +493,8 @@ export {
491
493
  MEMORY_MAX_CHARS,
492
494
  DEV_STATE_FILE_NAME,
493
495
  DEFAULT_MAX_TOKENS,
496
+ DEFAULT_MAX_TOOL_ROUNDS,
497
+ DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
494
498
  MCP_TOOL_PREFIX,
495
499
  MCP_PROJECT_CONFIG_NAME,
496
500
  MCP_CONNECT_TIMEOUT,
@@ -385,7 +385,7 @@ ${content}`);
385
385
  }
386
386
  }
387
387
  async function runTaskMode(config, providers, configManager, topic) {
388
- const { TaskOrchestrator } = await import("./task-orchestrator-ZKJOZB5S.js");
388
+ const { TaskOrchestrator } = await import("./task-orchestrator-C42TNHE6.js");
389
389
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
390
390
  let interrupted = false;
391
391
  const onSigint = () => {
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  saveDevState,
25
25
  sessionHasMeaningfulContent,
26
26
  setupProxy
27
- } from "./chunk-5JJKUQTS.js";
27
+ } from "./chunk-6I5FUNPR.js";
28
28
  import {
29
29
  ToolExecutor,
30
30
  ToolRegistry,
@@ -34,10 +34,11 @@ import {
34
34
  lastResponseStore,
35
35
  renderDiff,
36
36
  setContextWindow,
37
+ setMaxOutputCap,
37
38
  spawnAgentContext,
38
39
  theme,
39
40
  undoStack
40
- } from "./chunk-VTMHZCWZ.js";
41
+ } from "./chunk-IXDGWT2Z.js";
41
42
  import {
42
43
  fileCheckpoints
43
44
  } from "./chunk-4BKXL7SM.js";
@@ -50,6 +51,7 @@ import {
50
51
  CONTEXT_PRESSURE_THRESHOLD,
51
52
  CUSTOM_COMMANDS_DIR_NAME,
52
53
  DEFAULT_MAX_TOKENS,
54
+ DEFAULT_MAX_TOOL_ROUNDS,
53
55
  DESCRIPTION,
54
56
  DEV_STATE_FILE_NAME,
55
57
  MCP_PROJECT_CONFIG_NAME,
@@ -61,7 +63,7 @@ import {
61
63
  SKILLS_DIR_NAME,
62
64
  VERSION,
63
65
  buildUserIdentityPrompt
64
- } from "./chunk-KGJYHTC2.js";
66
+ } from "./chunk-YIMTDKUW.js";
65
67
 
66
68
  // src/index.ts
67
69
  import { program } from "commander";
@@ -2104,7 +2106,7 @@ ${hint}` : "")
2104
2106
  usage: "/test [command|filter]",
2105
2107
  async execute(args, ctx) {
2106
2108
  try {
2107
- const { executeTests } = await import("./run-tests-Q2JYJVLK.js");
2109
+ const { executeTests } = await import("./run-tests-3NNL7Z2E.js");
2108
2110
  const argStr = args.join(" ").trim();
2109
2111
  let testArgs = {};
2110
2112
  if (argStr) {
@@ -3277,7 +3279,6 @@ ${content}
3277
3279
  parts.push(...imageParts);
3278
3280
  return { parts, hasImage: imageParts.length > 0, refs };
3279
3281
  }
3280
- var MAX_TOOL_ROUNDS = 25;
3281
3282
  var FREE_ROUND_TOOLS = /* @__PURE__ */ new Set(["write_todos"]);
3282
3283
  var MAX_CONSECUTIVE_FREE_ROUNDS = 3;
3283
3284
  var MAX_REPEATED_TOOL_CALLS = 2;
@@ -3328,6 +3329,7 @@ var Repl = class {
3328
3329
  if (options?.allowedTools) this.allowedTools = options.allowedTools;
3329
3330
  if (options?.blockedTools) this.blockedTools = options.blockedTools;
3330
3331
  if (options?.resumeSessionId) this.resumeSessionId = options.resumeSessionId;
3332
+ if (options?.maxToolRoundsOverride !== void 0) this.maxToolRoundsOverride = options.maxToolRoundsOverride;
3331
3333
  }
3332
3334
  rl;
3333
3335
  currentProvider;
@@ -3381,6 +3383,8 @@ var Repl = class {
3381
3383
  * 防止 selectFromList 结束后 stdin.pause()/resume() 释放的残留字节被解析为新命令。
3382
3384
  */
3383
3385
  selecting = false;
3386
+ /** CLI --max-tool-rounds 覆盖值;未指定时从 config.maxToolRounds 读取 */
3387
+ maxToolRoundsOverride;
3384
3388
  // ── /add-dir 目录上下文支持 ────────────────────────────────────────────────
3385
3389
  /**
3386
3390
  * 扫描目录内容,返回格式化字符串(含目录树 + 关键文件内容)。
@@ -3751,10 +3755,11 @@ ${projectContext}`);
3751
3755
  const envInfo = `OS: ${osName}
3752
3756
  ${shellInfo}
3753
3757
  Working directory: ${process.cwd()}`;
3758
+ const effectiveMaxRounds = this.maxToolRoundsOverride ?? this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
3754
3759
  const budgetInfo = `
3755
3760
 
3756
3761
  # Tool Round Budget
3757
- You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds per conversation turn. Plan your work accordingly:
3762
+ You have a maximum of ${effectiveMaxRounds} tool call rounds per conversation turn. Plan your work accordingly:
3758
3763
  - Use efficient approaches: batch edits (replaceAll), read multiple files in one round, avoid re-reading files you already read.
3759
3764
  - On Windows, use PowerShell cmdlets (Invoke-RestMethod, Get-ChildItem) instead of Unix commands (curl, grep, find).
3760
3765
  - If starting a long-running server process via bash, use background execution \u2014 do not block the tool round.
@@ -4009,6 +4014,8 @@ Session '${this.resumeSessionId}' not found.
4009
4014
  const welcomeModelInfo = welcomeProvider?.info.models.find((m) => m.id === this.currentModel);
4010
4015
  const profileNickname = this.config.get("userProfile")?.nickname || this.config.get("userProfile")?.name;
4011
4016
  this.renderer.printWelcome(this.currentProvider, this.currentModel, welcomeModelInfo?.contextWindow, profileNickname);
4017
+ const cfgOutputCap = this.config.get("maxToolOutputChars");
4018
+ if (typeof cfgOutputCap === "number" && cfgOutputCap > 0) setMaxOutputCap(cfgOutputCap);
4012
4019
  if (welcomeModelInfo?.contextWindow) setContextWindow(welcomeModelInfo.contextWindow);
4013
4020
  if (this.resumeSessionId) {
4014
4021
  const session = this.sessions.current;
@@ -4765,11 +4772,12 @@ Session '${this.resumeSessionId}' not found.
4765
4772
  }
4766
4773
  const apiMessages = [...messages];
4767
4774
  const extraMessages = [];
4775
+ const maxToolRounds = this.maxToolRoundsOverride ?? this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
4768
4776
  const baseSystemPrompt = (this.buildCurrentSystemPrompt() ?? "") + TOOL_CALL_REMINDER;
4769
4777
  const roundBudgetHint = this.planMode ? `
4770
4778
 
4771
4779
  [Tool Round Budget \u2014 Plan Mode]
4772
- You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds. You are in READ-ONLY Plan Mode:
4780
+ You have a maximum of ${maxToolRounds} tool call rounds. You are in READ-ONLY Plan Mode:
4773
4781
  - Only use: read_file, list_dir, grep_files, glob_files, ask_user, write_todos
4774
4782
  - Do NOT attempt to call bash, write_file, edit_file \u2014 they are disabled
4775
4783
  - Do NOT write shell commands or code blocks as a substitute for tool calls
@@ -4779,11 +4787,11 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds. You are in READ-ONLY
4779
4787
  - Every ${AUTO_PAUSE_INTERVAL} rounds the user will be asked whether to continue.` : `
4780
4788
 
4781
4789
  [Tool Round Budget]
4782
- You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan efficiently:
4790
+ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan efficiently:
4783
4791
  - Prefer batch operations (e.g. global find-and-replace) over repetitive single edits.
4784
4792
  - Do NOT read the same file more than once \u2014 use the content from previous reads.
4785
4793
  - Prioritize the most critical tasks first in case rounds run out.
4786
- - When remaining rounds are low (\u22645), focus on completing the current task and summarizing.
4794
+ - When remaining rounds are low, focus on completing the current task and summarizing.
4787
4795
  - Every ${AUTO_PAUSE_INTERVAL} rounds the user will be asked whether to continue \u2014 use this as a natural checkpoint to report progress.`;
4788
4796
  const systemPrompt = baseSystemPrompt + roundBudgetHint;
4789
4797
  const modelParams = this.getModelParams();
@@ -4797,8 +4805,16 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
4797
4805
  const roundToolHistory = [];
4798
4806
  this.setupInterjectionListener();
4799
4807
  try {
4800
- for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
4801
- this.toolExecutor.setRoundInfo(round + 1, MAX_TOOL_ROUNDS);
4808
+ const warnNoteAt = Math.max(10, Math.floor(maxToolRounds * 0.2));
4809
+ const warnLowAt = Math.max(5, Math.floor(maxToolRounds * 0.1));
4810
+ const warnCriticalAt = Math.max(3, Math.floor(maxToolRounds * 0.05));
4811
+ const warnLowEff = Math.min(warnLowAt, warnNoteAt - 1);
4812
+ const warnCriticalEff = Math.min(warnCriticalAt, warnLowEff - 1);
4813
+ let warnedNote = false;
4814
+ let warnedLow = false;
4815
+ let warnedCritical = false;
4816
+ for (let round = 0; round < maxToolRounds; round++) {
4817
+ this.toolExecutor.setRoundInfo(round + 1, maxToolRounds);
4802
4818
  if (this.toolExecutor.pendingSlashCommand) {
4803
4819
  const cmd = this.toolExecutor.pendingSlashCommand;
4804
4820
  this.toolExecutor.pendingSlashCommand = null;
@@ -4813,26 +4829,29 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
4813
4829
  `));
4814
4830
  extraMessages.push({ role: "user", content: cmd });
4815
4831
  }
4816
- const roundsLeft = MAX_TOOL_ROUNDS - round;
4817
- if (roundsLeft === 10) {
4832
+ const roundsLeft = maxToolRounds - round;
4833
+ if (!warnedCritical && roundsLeft <= warnCriticalEff) {
4834
+ warnedCritical = true;
4818
4835
  extraMessages.push({
4819
4836
  role: "user",
4820
- content: `\u{1F4CA} Budget note: ${roundsLeft} tool rounds remaining out of ${MAX_TOOL_ROUNDS}. Plan your remaining work efficiently \u2014 use batch operations (e.g., replaceAll) when possible.`
4837
+ 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.`
4821
4838
  });
4822
- } else if (roundsLeft === 5) {
4839
+ process.stdout.write(theme.error(` \u{1F6A8} Critical: ${roundsLeft} rounds remaining
4840
+ `));
4841
+ } else if (!warnedLow && roundsLeft <= warnLowEff) {
4842
+ warnedLow = true;
4823
4843
  extraMessages.push({
4824
4844
  role: "user",
4825
4845
  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.`
4826
4846
  });
4827
4847
  process.stdout.write(theme.warning(` \u26A0\uFE0F Low budget: ${roundsLeft} rounds remaining
4828
4848
  `));
4829
- } else if (roundsLeft === 3) {
4849
+ } else if (!warnedNote && roundsLeft <= warnNoteAt) {
4850
+ warnedNote = true;
4830
4851
  extraMessages.push({
4831
4852
  role: "user",
4832
- 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.`
4853
+ 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.`
4833
4854
  });
4834
- process.stdout.write(theme.error(` \u{1F6A8} Critical: ${roundsLeft} rounds remaining
4835
- `));
4836
4855
  }
4837
4856
  if (this._userInterjection) {
4838
4857
  const msg = this._userInterjection;
@@ -4891,7 +4910,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
4891
4910
  const alreadyWrote = hadPreviousWriteToolCalls(extraMessages);
4892
4911
  const coarseHallucination = !this.planMode && hasWriteTools && !alreadyWrote && !!result.content && detectsHallucinatedFileOp(result.content);
4893
4912
  const phantomPaths = (coarseHallucination || alreadyWrote) && !this.planMode && hasWriteTools && result.content ? findPhantomClaims(result.content, extraMessages) : [];
4894
- if ((phantomPaths.length > 0 || coarseHallucination) && round < MAX_TOOL_ROUNDS - 1) {
4913
+ if ((phantomPaths.length > 0 || coarseHallucination) && round < maxToolRounds - 1) {
4895
4914
  const providerName = this.currentProvider;
4896
4915
  const detail = phantomPaths.length > 0 ? ` phantom files: ${phantomPaths.join(", ")}` : "";
4897
4916
  process.stderr.write(
@@ -4906,7 +4925,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
4906
4925
  { role: "assistant", content: result.content },
4907
4926
  { role: "user", content: correctionMsg }
4908
4927
  );
4909
- spinner.start(`Retrying... (round ${round + 2}/${MAX_TOOL_ROUNDS})`);
4928
+ spinner.start(`Retrying... (round ${round + 2}/${maxToolRounds})`);
4910
4929
  continue;
4911
4930
  }
4912
4931
  spinner.stop();
@@ -5096,11 +5115,11 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
5096
5115
  extraMessages.push({ role: "user", content: msg });
5097
5116
  }
5098
5117
  const effectiveRound = round + 1;
5099
- const remaining = MAX_TOOL_ROUNDS - effectiveRound;
5118
+ const remaining = maxToolRounds - effectiveRound;
5100
5119
  if (AUTO_PAUSE_INTERVAL > 0 && effectiveRound > 0 && effectiveRound % AUTO_PAUSE_INTERVAL === 0 && remaining > 0) {
5101
5120
  spinner.stop();
5102
5121
  process.stdout.write("\n");
5103
- process.stdout.write(theme.warning(`\u23F8 Auto-pause: ${effectiveRound}/${MAX_TOOL_ROUNDS} rounds used, ${remaining} remaining
5122
+ process.stdout.write(theme.warning(`\u23F8 Auto-pause: ${effectiveRound}/${maxToolRounds} rounds used, ${remaining} remaining
5104
5123
  `));
5105
5124
  const recentHistory = roundToolHistory.slice(-AUTO_PAUSE_INTERVAL);
5106
5125
  if (recentHistory.length > 0) {
@@ -5131,7 +5150,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
5131
5150
  process.stdout.write(theme.warning("\u26A1 Stopped by user at auto-pause checkpoint\n"));
5132
5151
  extraMessages.push({
5133
5152
  role: "user",
5134
- content: `The user has stopped the task at round ${effectiveRound}/${MAX_TOOL_ROUNDS}. Do not call any more tools. Summarize what has been completed and what remains.`
5153
+ 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.`
5135
5154
  });
5136
5155
  break;
5137
5156
  } else if (pauseResponse && pauseResponse !== "y" && pauseResponse !== "Y" && pauseResponse !== "") {
@@ -5144,7 +5163,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
5144
5163
  }
5145
5164
  const nextRound = round + 2;
5146
5165
  spinner.start(
5147
- nextRound <= MAX_TOOL_ROUNDS ? `Thinking... (round ${nextRound}/${MAX_TOOL_ROUNDS})` : "Thinking..."
5166
+ nextRound <= maxToolRounds ? `Thinking... (round ${nextRound}/${maxToolRounds})` : "Thinking..."
5148
5167
  );
5149
5168
  }
5150
5169
  spinner.stop();
@@ -5154,7 +5173,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
5154
5173
  ...extraMessages,
5155
5174
  {
5156
5175
  role: "user",
5157
- content: `You have used all ${MAX_TOOL_ROUNDS} tool call rounds. Do not call any more tools. Summarize in text:
5176
+ content: `You have used all ${maxToolRounds} tool call rounds. Do not call any more tools. Summarize in text:
5158
5177
  1. What work has been completed so far
5159
5178
  2. What tasks remain unfinished
5160
5179
  3. What the user can do next (e.g. send another request to continue)`
@@ -5178,7 +5197,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
5178
5197
  );
5179
5198
  spinner.stop();
5180
5199
  if ("content" in summaryResult) {
5181
- this.renderer.renderError(`Reached maximum tool call rounds (${MAX_TOOL_ROUNDS}). Here is a summary:`);
5200
+ this.renderer.renderError(`Reached maximum tool call rounds (${maxToolRounds}). Here is a summary:`);
5182
5201
  this.renderer.renderResponse(summaryResult.content);
5183
5202
  lastResponseStore.content = summaryResult.content;
5184
5203
  session.addMessage({ role: "assistant", content: summaryResult.content, timestamp: /* @__PURE__ */ new Date() });
@@ -5188,13 +5207,13 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
5188
5207
  }
5189
5208
  } else {
5190
5209
  this.renderer.renderError(
5191
- `Reached maximum tool call rounds (${MAX_TOOL_ROUNDS}). Stopping.
5210
+ `Reached maximum tool call rounds (${maxToolRounds}). Stopping.
5192
5211
  Tip: You can continue the conversation by asking the AI to proceed.`
5193
5212
  );
5194
5213
  }
5195
5214
  } catch {
5196
5215
  this.renderer.renderError(
5197
- `Reached maximum tool call rounds (${MAX_TOOL_ROUNDS}). Stopping.
5216
+ `Reached maximum tool call rounds (${maxToolRounds}). Stopping.
5198
5217
  Tip: You can continue the conversation by asking the AI to proceed.`
5199
5218
  );
5200
5219
  }
@@ -5428,7 +5447,7 @@ process.on("unhandledRejection", (reason) => {
5428
5447
  process.exit(1);
5429
5448
  });
5430
5449
  program.name("ai-cli").description("Cross-platform REPL-style AI CLI with multi-provider support").version(VERSION);
5431
- program.option("--provider <name>", "AI provider to use (claude/gemini/deepseek/zhipu/kimi or any custom provider)").option("-m, --model <name>", "Model to use").option("-p, --prompt <text>", "Headless mode: send a single prompt and exit (reads stdin as context if piped)").option("--system <prompt>", "System prompt override (headless mode)").option("--json", "Output result as JSON object (headless mode only)").option("--output-format <format>", "Output format: text (default) | streaming-json (NDJSON per-chunk, headless only)").option("--no-stream", "Disable streaming output").option("--allowed-tools <tools>", "Comma-separated whitelist of tools (e.g. bash,read_file)").option("--blocked-tools <tools>", "Comma-separated blacklist of tools (e.g. bash,write_file)").option("--resume <id>", "Resume a previous session by ID (prefix match supported)").action(async (options) => {
5450
+ program.option("--provider <name>", "AI provider to use (claude/gemini/deepseek/zhipu/kimi or any custom provider)").option("-m, --model <name>", "Model to use").option("-p, --prompt <text>", "Headless mode: send a single prompt and exit (reads stdin as context if piped)").option("--system <prompt>", "System prompt override (headless mode)").option("--json", "Output result as JSON object (headless mode only)").option("--output-format <format>", "Output format: text (default) | streaming-json (NDJSON per-chunk, headless only)").option("--no-stream", "Disable streaming output").option("--allowed-tools <tools>", "Comma-separated whitelist of tools (e.g. bash,read_file)").option("--blocked-tools <tools>", "Comma-separated blacklist of tools (e.g. bash,write_file)").option("--resume <id>", "Resume a previous session by ID (prefix match supported)").option("--max-tool-rounds <n>", "Max agentic tool-call rounds per conversation (default: 200)").action(async (options) => {
5432
5451
  if (options.prompt !== void 0) {
5433
5452
  await runHeadless(options);
5434
5453
  } else {
@@ -5474,7 +5493,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5474
5493
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5475
5494
  process.exit(1);
5476
5495
  }
5477
- const { startWebServer } = await import("./server-4K2VEKN7.js");
5496
+ const { startWebServer } = await import("./server-PFHWO3HL.js");
5478
5497
  await startWebServer({ port, host: options.host });
5479
5498
  });
5480
5499
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -5707,7 +5726,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
5707
5726
  }),
5708
5727
  config.get("customProviders")
5709
5728
  );
5710
- const { startHub } = await import("./hub-F4TORNUA.js");
5729
+ const { startHub } = await import("./hub-4DNFD6JK.js");
5711
5730
  await startHub(
5712
5731
  {
5713
5732
  topic: topic ?? "",
@@ -5943,7 +5962,23 @@ Provider '${options.provider}' is not configured. Run: ai-cli config
5943
5962
  process.stderr.write(`[info] Tool blacklist: ${[...blockedTools].join(", ")}
5944
5963
  `);
5945
5964
  }
5946
- const repl = new Repl(providers, sessions, config, events, { allowedTools, blockedTools, resumeSessionId: options.resume });
5965
+ let maxToolRoundsOverride;
5966
+ if (options.maxToolRounds !== void 0) {
5967
+ const parsed = parseInt(String(options.maxToolRounds), 10);
5968
+ if (!Number.isFinite(parsed) || parsed < 1) {
5969
+ console.error(`[error] --max-tool-rounds must be a positive integer, got: ${options.maxToolRounds}`);
5970
+ process.exit(1);
5971
+ }
5972
+ maxToolRoundsOverride = parsed;
5973
+ process.stderr.write(`[info] Max tool rounds override: ${parsed}
5974
+ `);
5975
+ }
5976
+ const repl = new Repl(providers, sessions, config, events, {
5977
+ allowedTools,
5978
+ blockedTools,
5979
+ resumeSessionId: options.resume,
5980
+ maxToolRoundsOverride
5981
+ });
5947
5982
  if (options.model) {
5948
5983
  const defaultModels = config.get("defaultModels");
5949
5984
  const provider = options.provider ?? config.getDefaultProvider();
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-KGJYHTC2.js";
5
+ } from "./chunk-YIMTDKUW.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-3QRYIBN3.js";
4
+ } from "./chunk-W6AK76UM.js";
5
5
  export {
6
6
  executeTests,
7
7
  runTestsTool
@@ -15,7 +15,7 @@ import {
15
15
  hadPreviousWriteToolCalls,
16
16
  loadDevState,
17
17
  setupProxy
18
- } from "./chunk-5JJKUQTS.js";
18
+ } from "./chunk-6I5FUNPR.js";
19
19
  import {
20
20
  AuthManager
21
21
  } from "./chunk-BYNY5JPB.js";
@@ -30,10 +30,11 @@ import {
30
30
  renderDiff,
31
31
  runHook,
32
32
  setContextWindow,
33
+ setMaxOutputCap,
33
34
  spawnAgentContext,
34
35
  truncateOutput,
35
36
  undoStack
36
- } from "./chunk-VTMHZCWZ.js";
37
+ } from "./chunk-IXDGWT2Z.js";
37
38
  import "./chunk-4BKXL7SM.js";
38
39
  import {
39
40
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -42,6 +43,7 @@ import {
42
43
  CONTEXT_FILE_CANDIDATES,
43
44
  CUSTOM_COMMANDS_DIR_NAME,
44
45
  DEFAULT_MAX_TOKENS,
46
+ DEFAULT_MAX_TOOL_ROUNDS,
45
47
  DESCRIPTION,
46
48
  MCP_PROJECT_CONFIG_NAME,
47
49
  MEMORY_FILE_NAME,
@@ -52,7 +54,7 @@ import {
52
54
  SKILLS_DIR_NAME,
53
55
  VERSION,
54
56
  buildUserIdentityPrompt
55
- } from "./chunk-KGJYHTC2.js";
57
+ } from "./chunk-YIMTDKUW.js";
56
58
 
57
59
  // src/web/server.ts
58
60
  import express from "express";
@@ -466,7 +468,6 @@ function loadMemoryContent(configDir) {
466
468
  import { existsSync as existsSync3, readFileSync as readFileSync3, appendFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
467
469
  import { join as join2, resolve } from "path";
468
470
  import { execSync } from "child_process";
469
- var MAX_TOOL_ROUNDS = 25;
470
471
  var FREE_ROUND_TOOLS = /* @__PURE__ */ new Set(["write_todos"]);
471
472
  var MAX_CONSECUTIVE_FREE_ROUNDS = 5;
472
473
  var SessionHandler = class _SessionHandler {
@@ -492,6 +493,15 @@ var SessionHandler = class _SessionHandler {
492
493
  activeSystemPrompt;
493
494
  /** Directories added via /add-dir */
494
495
  addedDirs = /* @__PURE__ */ new Set();
496
+ /**
497
+ * 未保存会话的内存缓存(per-handler)。
498
+ * 当客户端通过 `session new` 创建新会话时,会话仅存在于 SessionManager._current 中,
499
+ * 未写入磁盘(saveIfNeeded 跳过空会话)。Web UI 多 Tab 场景下每个 Tab 都会创建自己的
500
+ * 空会话,切换 Tab 时 `session load <id>` 会读取磁盘并报"Session not found"。
501
+ * 此 Map 在本 SessionHandler 生命周期内持有这些未保存会话,load 时优先查询。
502
+ * 会话一旦保存到磁盘(有消息后),会从此 Map 中移除。
503
+ */
504
+ unsavedSessions = /* @__PURE__ */ new Map();
495
505
  constructor(ws, shared) {
496
506
  this.ws = ws;
497
507
  this.config = shared.config;
@@ -598,6 +608,8 @@ var SessionHandler = class _SessionHandler {
598
608
  /** 根据当前模型 context window 更新工具输出截断上限 */
599
609
  updateContextWindow() {
600
610
  try {
611
+ const cfgOutputCap = this.config.get("maxToolOutputChars");
612
+ if (typeof cfgOutputCap === "number" && cfgOutputCap > 0) setMaxOutputCap(cfgOutputCap);
601
613
  const provider = this.providers.get(this.currentProvider);
602
614
  const modelInfo = provider?.info.models.find((m) => m.id === this.currentModel);
603
615
  if (modelInfo?.contextWindow) setContextWindow(modelInfo.contextWindow);
@@ -607,13 +619,16 @@ var SessionHandler = class _SessionHandler {
607
619
  /** Save session only if it exists and has messages (never persist empty "Untitled" sessions). */
608
620
  saveIfNeeded() {
609
621
  if (this.sessions.current && this.sessions.current.messages.length > 0) {
622
+ const id = this.sessions.current.id;
610
623
  this.sessions.save();
624
+ this.unsavedSessions.delete(id);
611
625
  }
612
626
  }
613
627
  /** Lazily create a session if none exists yet (deferred from constructor). */
614
628
  ensureSession() {
615
629
  if (!this.sessions.current) {
616
- this.sessions.createSession(this.currentProvider, this.currentModel);
630
+ const created = this.sessions.createSession(this.currentProvider, this.currentModel);
631
+ this.unsavedSessions.set(created.id, created);
617
632
  }
618
633
  }
619
634
  // ── Chat handling ────────────────────────────────────────────────
@@ -733,33 +748,56 @@ var SessionHandler = class _SessionHandler {
733
748
  const session = this.sessions.current;
734
749
  const apiMessages = [...messages];
735
750
  const extraMessages = [];
751
+ const maxToolRounds = this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
736
752
  const baseSystemPrompt = (this.buildSystemPrompt() ?? "") + TOOL_CALL_REMINDER;
737
753
  const roundBudgetHint = `
738
754
 
739
755
  [Tool Round Budget]
740
- You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan efficiently:
756
+ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan efficiently:
741
757
  - Prefer batch operations (e.g. global find-and-replace) over repetitive single edits.
742
758
  - Prioritize the most critical tasks first in case rounds run out.
743
- - When remaining rounds are low (\u22645), focus on completing the current task and summarizing.`;
759
+ - When remaining rounds are low, focus on completing the current task and summarizing.`;
744
760
  const systemPrompt = baseSystemPrompt + roundBudgetHint;
745
761
  const modelParams = this.getModelParams();
746
762
  const roundUsage = { inputTokens: 0, outputTokens: 0 };
747
763
  const supportsStreamingTools = typeof provider.chatWithToolsStream === "function";
748
764
  let consecutiveFreeRounds = 0;
765
+ const warnNoteAt = Math.max(10, Math.floor(maxToolRounds * 0.2));
766
+ const warnLowAt = Math.max(5, Math.floor(maxToolRounds * 0.1));
767
+ const warnCriticalAt = Math.max(3, Math.floor(maxToolRounds * 0.05));
768
+ const warnLowEff = Math.min(warnLowAt, warnNoteAt - 1);
769
+ const warnCriticalEff = Math.min(warnCriticalAt, warnLowEff - 1);
770
+ let warnedNote = false;
771
+ let warnedLow = false;
772
+ let warnedCritical = false;
749
773
  const ac = new AbortController();
750
774
  this.abortController = ac;
751
775
  try {
752
- for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
776
+ for (let round = 0; round < maxToolRounds; round++) {
753
777
  if (ac.signal.aborted) break;
754
- this.toolExecutor.setRoundInfo(round + 1, MAX_TOOL_ROUNDS);
755
- this.send({ type: "round_progress", current: round + 1, total: MAX_TOOL_ROUNDS });
756
- const roundsLeft = MAX_TOOL_ROUNDS - round;
757
- if (roundsLeft === 5) {
778
+ this.toolExecutor.setRoundInfo(round + 1, maxToolRounds);
779
+ this.send({ type: "round_progress", current: round + 1, total: maxToolRounds });
780
+ const roundsLeft = maxToolRounds - round;
781
+ if (!warnedCritical && roundsLeft <= warnCriticalEff) {
782
+ warnedCritical = true;
783
+ extraMessages.push({
784
+ role: "user",
785
+ 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.`
786
+ });
787
+ this.send({ type: "info", message: `\u{1F6A8} Critical: ${roundsLeft} rounds remaining` });
788
+ } else if (!warnedLow && roundsLeft <= warnLowEff) {
789
+ warnedLow = true;
758
790
  extraMessages.push({
759
791
  role: "user",
760
792
  content: `\u26A0\uFE0F Budget warning: Only ${roundsLeft} tool rounds remaining. Prioritize completing the most critical task. If you cannot finish everything, summarize what's done and what remains.`
761
793
  });
762
794
  this.send({ type: "info", message: `\u26A0\uFE0F Low budget: ${roundsLeft} rounds remaining` });
795
+ } else if (!warnedNote && roundsLeft <= warnNoteAt) {
796
+ warnedNote = true;
797
+ extraMessages.push({
798
+ role: "user",
799
+ 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.`
800
+ });
763
801
  }
764
802
  if (this.userInterjection) {
765
803
  const msg = this.userInterjection;
@@ -795,7 +833,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
795
833
  if (result.content && !result.toolCalls) {
796
834
  const hasWriteTools = toolDefs.some((t) => t.name === "write_file" || t.name === "edit_file");
797
835
  const alreadyWrote = hadPreviousWriteToolCalls(extraMessages);
798
- if (hasWriteTools && !alreadyWrote && detectsHallucinatedFileOp(result.content) && round < MAX_TOOL_ROUNDS - 1) {
836
+ if (hasWriteTools && !alreadyWrote && detectsHallucinatedFileOp(result.content) && round < maxToolRounds - 1) {
799
837
  this.send({ type: "info", message: "\u26A0 Hallucinated completion detected, forcing retry..." });
800
838
  extraMessages.push(
801
839
  { role: "assistant", content: result.content },
@@ -844,7 +882,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
844
882
  ...extraMessages,
845
883
  {
846
884
  role: "user",
847
- content: `You have used all ${MAX_TOOL_ROUNDS} tool call rounds. Do not call any more tools. Summarize in text:
885
+ content: `You have used all ${maxToolRounds} tool call rounds. Do not call any more tools. Summarize in text:
848
886
  1. What work has been completed so far
849
887
  2. What tasks remain unfinished
850
888
  3. What the user can do next`
@@ -866,7 +904,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
866
904
  if ("content" in summaryResult && summaryResult.content) {
867
905
  this.send({
868
906
  type: "response_done",
869
- content: `\u26A0 Reached maximum tool call rounds (${MAX_TOOL_ROUNDS}).
907
+ content: `\u26A0 Reached maximum tool call rounds (${maxToolRounds}).
870
908
 
871
909
  ${summaryResult.content}`,
872
910
  usage: roundUsage
@@ -876,7 +914,7 @@ ${summaryResult.content}`,
876
914
  } catch {
877
915
  this.send({
878
916
  type: "error",
879
- message: `Reached maximum tool call rounds (${MAX_TOOL_ROUNDS}). You can continue by asking the AI to proceed.`
917
+ message: `Reached maximum tool call rounds (${maxToolRounds}). You can continue by asking the AI to proceed.`
880
918
  });
881
919
  }
882
920
  this.sessionTokenUsage.inputTokens += roundUsage.inputTokens;
@@ -1043,7 +1081,8 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1043
1081
  const sub = args[0];
1044
1082
  if (sub === "new") {
1045
1083
  this.saveIfNeeded();
1046
- this.sessions.createSession(this.currentProvider, this.currentModel);
1084
+ const created = this.sessions.createSession(this.currentProvider, this.currentModel);
1085
+ this.unsavedSessions.set(created.id, created);
1047
1086
  this.sessionTokenUsage = { inputTokens: 0, outputTokens: 0 };
1048
1087
  this.send({ type: "info", message: "New session created." });
1049
1088
  this.sendStatus();
@@ -1051,6 +1090,20 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1051
1090
  } else if (sub === "load" && args[1]) {
1052
1091
  const targetId = args[1];
1053
1092
  this.saveIfNeeded();
1093
+ const cachedExact = this.unsavedSessions.get(targetId);
1094
+ const cached = cachedExact ?? [...this.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
1095
+ if (cached) {
1096
+ this.sessions.setCurrent(cached);
1097
+ this.sessionTokenUsage = { inputTokens: 0, outputTokens: 0 };
1098
+ this.send({
1099
+ type: "info",
1100
+ message: `Loaded session: ${cached.id.slice(0, 8)} "${cached.title ?? ""}" (${cached.messages.length} messages)`
1101
+ });
1102
+ this.sendSessionMessages();
1103
+ this.sendStatus();
1104
+ this.sendSessionList();
1105
+ break;
1106
+ }
1054
1107
  const list = this.sessions.listSessions();
1055
1108
  const found = list.find((s) => s.id.startsWith(targetId));
1056
1109
  if (found) {
@@ -1061,12 +1114,30 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1061
1114
  this.sendStatus();
1062
1115
  this.sendSessionList();
1063
1116
  } else {
1064
- this.send({ type: "error", message: `Session not found: ${targetId}` });
1117
+ const recreated = this.sessions.createSession(this.currentProvider, this.currentModel);
1118
+ this.unsavedSessions.set(recreated.id, recreated);
1119
+ this.sessionTokenUsage = { inputTokens: 0, outputTokens: 0 };
1120
+ this.send({
1121
+ type: "info",
1122
+ message: `Previous session (${targetId.slice(0, 8)}) is no longer available \u2014 started a new one.`
1123
+ });
1124
+ this.sendStatus();
1125
+ this.sendSessionList();
1065
1126
  }
1066
1127
  } else if (sub === "list") {
1067
1128
  this.sendSessionList();
1068
1129
  } else if (sub === "delete" && args[1]) {
1069
1130
  const targetId = args[1];
1131
+ const cachedKey = this.unsavedSessions.has(targetId) ? targetId : [...this.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
1132
+ if (cachedKey) {
1133
+ this.unsavedSessions.delete(cachedKey);
1134
+ if (this.sessions.current?.id === cachedKey) {
1135
+ this.sessions.clearCurrent();
1136
+ }
1137
+ this.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
1138
+ this.sendSessionList();
1139
+ break;
1140
+ }
1070
1141
  const list = this.sessions.listSessions();
1071
1142
  const found = list.find((s) => s.id.startsWith(targetId));
1072
1143
  if (found) {
@@ -1085,6 +1156,12 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1085
1156
  const list = this.sessions.listSessions();
1086
1157
  let deleted = 0;
1087
1158
  for (const targetId of ids) {
1159
+ const cachedKey = this.unsavedSessions.has(targetId) ? targetId : [...this.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
1160
+ if (cachedKey) {
1161
+ this.unsavedSessions.delete(cachedKey);
1162
+ deleted++;
1163
+ continue;
1164
+ }
1088
1165
  const found = list.find((s) => s.id.startsWith(targetId));
1089
1166
  if (found) {
1090
1167
  this.sessions.deleteSession(found.id);
@@ -1100,6 +1177,14 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1100
1177
  this.send({ type: "error", message: "Title cannot be empty." });
1101
1178
  break;
1102
1179
  }
1180
+ const cachedKey = this.unsavedSessions.has(targetId) ? targetId : [...this.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
1181
+ if (cachedKey) {
1182
+ const session = this.unsavedSessions.get(cachedKey);
1183
+ session.title = newTitle;
1184
+ this.send({ type: "info", message: `Renamed session: "${newTitle}"` });
1185
+ this.sendSessionList();
1186
+ break;
1187
+ }
1103
1188
  const list = this.sessions.listSessions();
1104
1189
  const found = list.find((s) => s.id.startsWith(targetId));
1105
1190
  if (found) {
@@ -1606,7 +1691,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1606
1691
  case "test": {
1607
1692
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1608
1693
  try {
1609
- const { executeTests } = await import("./run-tests-Q2JYJVLK.js");
1694
+ const { executeTests } = await import("./run-tests-3NNL7Z2E.js");
1610
1695
  const argStr = args.join(" ").trim();
1611
1696
  let testArgs = {};
1612
1697
  if (argStr) {
@@ -4,11 +4,11 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-VTMHZCWZ.js";
7
+ } from "./chunk-IXDGWT2Z.js";
8
8
  import "./chunk-4BKXL7SM.js";
9
9
  import {
10
10
  SUBAGENT_ALLOWED_TOOLS
11
- } from "./chunk-KGJYHTC2.js";
11
+ } from "./chunk-YIMTDKUW.js";
12
12
 
13
13
  // src/hub/task-orchestrator.ts
14
14
  import { createInterface } from "readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.51",
3
+ "version": "0.4.53",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",