jinzd-ai-cli 0.4.304 → 0.4.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/{batch-RY2F7BX5.js → batch-JLGWQ7HU.js} +2 -2
  2. package/dist/{chunk-3FI4TE5S.js → chunk-3RDJEYF7.js} +3 -3
  3. package/dist/{chunk-RWM2GFRC.js → chunk-45UVJXFQ.js} +6 -2
  4. package/dist/{chunk-23SJVOQD.js → chunk-72AR23R2.js} +26 -5
  5. package/dist/{chunk-I2WJ44D3.js → chunk-7SSCSSC6.js} +1 -1
  6. package/dist/{chunk-F3EJ4MIP.js → chunk-AYLK4EGA.js} +12 -11
  7. package/dist/{chunk-VOIOUJXI.js → chunk-C2Z77RTU.js} +13 -5
  8. package/dist/{chunk-2GRGLTAK.js → chunk-JDT6UG6R.js} +3 -1
  9. package/dist/{chunk-OECPGCHP.js → chunk-JFNBJHR6.js} +3 -3
  10. package/dist/{chunk-6AGTDWO3.js → chunk-K5SDDTN7.js} +1 -1
  11. package/dist/{chunk-7XU4BATV.js → chunk-KBDKKHXB.js} +15 -6
  12. package/dist/{chunk-7L7T46PI.js → chunk-LAP7TPPK.js} +1 -1
  13. package/dist/{chunk-4DEMGCK4.js → chunk-LF67HWWR.js} +37 -13
  14. package/dist/{chunk-TBTVV3KK.js → chunk-NLB7JZSO.js} +1 -1
  15. package/dist/{chunk-BXP6YZ2P.js → chunk-NRSYBKWA.js} +5 -4
  16. package/dist/{chunk-IEQAE3QG.js → chunk-OAXLMF3U.js} +5 -4
  17. package/dist/{chunk-XIMM47EG.js → chunk-OC2XHG5L.js} +1 -1
  18. package/dist/{chunk-RHCRE6XE.js → chunk-T6WQEQI4.js} +3 -1
  19. package/dist/{chunk-PPIISCFN.js → chunk-UFW4S3RU.js} +37 -13
  20. package/dist/{chunk-2PLYXL4P.js → chunk-VLDB2SVN.js} +33 -21
  21. package/dist/{chunk-BSBGPDCX.js → chunk-W6PLV3EY.js} +1 -1
  22. package/dist/{chunk-CKH4KQ4E.js → chunk-WQ3OMEC4.js} +6 -2
  23. package/dist/{chunk-DNGUSTL6.js → chunk-WZWRA7R2.js} +1 -1
  24. package/dist/{chunk-UUSRWSSX.js → chunk-XJOK5353.js} +15 -6
  25. package/dist/{ci-QYTLBM6Z.js → ci-6PHEL3C7.js} +4 -4
  26. package/dist/{ci-format-E5X27NC2.js → ci-format-AZYUP6BL.js} +2 -2
  27. package/dist/{constants-JDPKWZJP.js → constants-AXBD2EEB.js} +3 -1
  28. package/dist/{constants-F5WA5VPO.js → constants-CPNWIUGZ.js} +3 -1
  29. package/dist/{doctor-cli-7NSEVW63.js → doctor-cli-DDHHDMMJ.js} +4 -4
  30. package/dist/electron-server.js +253 -117
  31. package/dist/{hub-43PWTGGO.js → hub-DDYAWLF2.js} +4 -4
  32. package/dist/{hub-server-RF6LURXY.js → hub-server-CBKCP5VD.js} +1 -1
  33. package/dist/index.js +25 -24
  34. package/dist/{indexer-RBEKMFT4.js → indexer-FPKY7LCT.js} +2 -2
  35. package/dist/{indexer-TNDWGFRI.js → indexer-P3FPRTPJ.js} +2 -2
  36. package/dist/{persistent-memory-SHG3CJCP.js → persistent-memory-6XYYOMSC.js} +2 -2
  37. package/dist/{persistent-memory-A7RPDLR7.js → persistent-memory-XZISXRWT.js} +2 -2
  38. package/dist/{pr-LW755EL7.js → pr-UY4ZCZNI.js} +4 -4
  39. package/dist/{run-tests-HV37CM5U.js → run-tests-BAMLVLO6.js} +2 -2
  40. package/dist/{run-tests-MJ3SCW27.js → run-tests-UEE3OMLS.js} +2 -2
  41. package/dist/{semantic-HLAE2O4F.js → semantic-7GM5M4MU.js} +3 -3
  42. package/dist/{semantic-VPNKXKKK.js → semantic-QOBAXEQ4.js} +3 -3
  43. package/dist/{server-WI3Q4EDH.js → server-6IC33CJK.js} +9 -9
  44. package/dist/{server-EZ5ULLDP.js → server-DBHLVQVP.js} +193 -82
  45. package/dist/{store-MWNHVGJT.js → store-CMSSWZF7.js} +3 -1
  46. package/dist/{store-VO37H6LS.js → store-SIKHCUYJ.js} +3 -1
  47. package/dist/{task-orchestrator-PWW5I2KE.js → task-orchestrator-RDDETDW2.js} +9 -9
  48. package/dist/{usage-MAIV6XUZ.js → usage-D473ZVIP.js} +2 -2
  49. package/dist/{vector-store-JBAE6PS4.js → vector-store-JAXWDZYW.js} +1 -1
  50. package/dist/{vector-store-BBDXB5IQ.js → vector-store-PWIOQFTP.js} +1 -1
  51. package/dist/web/client/app.js +135 -60
  52. package/dist/web/client/app.js.br +0 -0
  53. package/dist/web/client/app.js.gz +0 -0
  54. package/dist/web/client/chat-input.js +38 -0
  55. package/dist/web/client/chat-input.js.br +0 -0
  56. package/dist/web/client/chat-input.js.gz +0 -0
  57. package/dist/web/client/index.html +1 -1
  58. package/dist/web/client/index.html.br +0 -0
  59. package/dist/web/client/index.html.gz +0 -0
  60. package/dist/web/client/stream-render.js +33 -0
  61. package/dist/web/client/style.css +2 -1
  62. package/dist/web/client/style.css.br +0 -0
  63. package/dist/web/client/style.css.gz +0 -0
  64. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  hasSemanticIndex,
3
3
  semanticSearch
4
- } from "./chunk-7XU4BATV.js";
5
- import "./chunk-IEQAE3QG.js";
4
+ } from "./chunk-KBDKKHXB.js";
5
+ import "./chunk-OAXLMF3U.js";
6
6
  import {
7
7
  buildChatIndex,
8
8
  clearChatIndex,
@@ -22,10 +22,10 @@ import {
22
22
  } from "./chunk-SKET65WZ.js";
23
23
  import {
24
24
  indexProject
25
- } from "./chunk-PPIISCFN.js";
25
+ } from "./chunk-UFW4S3RU.js";
26
26
  import {
27
27
  loadIndex
28
- } from "./chunk-RWM2GFRC.js";
28
+ } from "./chunk-45UVJXFQ.js";
29
29
  import {
30
30
  addMemoryEntry,
31
31
  backupMemoryStore,
@@ -50,7 +50,7 @@ import {
50
50
  touchMemoryReferences,
51
51
  updateMemoryApproval,
52
52
  updateMemoryEntry
53
- } from "./chunk-DNGUSTL6.js";
53
+ } from "./chunk-WZWRA7R2.js";
54
54
  import {
55
55
  DEFAULT_PATTERNS,
56
56
  redactJson,
@@ -66,7 +66,7 @@ import {
66
66
  } from "./chunk-CGLYJRFA.js";
67
67
  import {
68
68
  runTestsTool
69
- } from "./chunk-TBTVV3KK.js";
69
+ } from "./chunk-NLB7JZSO.js";
70
70
  import {
71
71
  AGENTIC_BEHAVIOR_GUIDELINE,
72
72
  APP_NAME,
@@ -84,6 +84,7 @@ import {
84
84
  DESCRIPTION,
85
85
  DEV_STATE_FILE_NAME,
86
86
  HISTORY_DIR_NAME,
87
+ MAX_GLOB_MATCHES,
87
88
  MCP_CALL_TIMEOUT,
88
89
  MCP_CONNECT_TIMEOUT,
89
90
  MCP_PROJECT_CONFIG_NAME,
@@ -100,12 +101,30 @@ import {
100
101
  SUBAGENT_MAX_ROUNDS_LIMIT,
101
102
  VERSION,
102
103
  buildUserIdentityPrompt
103
- } from "./chunk-2GRGLTAK.js";
104
+ } from "./chunk-JDT6UG6R.js";
104
105
 
105
106
  // src/web/server.ts
106
107
  import express from "express";
107
108
  import { createServer } from "http";
108
109
  import { WebSocketServer } from "ws";
110
+
111
+ // src/web/chat-limits.ts
112
+ var MAX_CHAT_BYTES = 16 * 1024 * 1024;
113
+ var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
114
+ var MAX_IMAGE_BASE64_CHARS = Math.ceil(MAX_IMAGE_BYTES / 3) * 4;
115
+ var IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
116
+ function imageValidationError(images) {
117
+ if (images === void 0) return;
118
+ if (!Array.isArray(images)) return "Images must be an array.";
119
+ for (const image of images) {
120
+ if (!image || typeof image.data !== "string" || !IMAGE_MIMES.has(image.mime)) {
121
+ return "Unsupported image format. Use PNG, JPEG, GIF, WebP or SVG.";
122
+ }
123
+ if (image.data.length > MAX_IMAGE_BASE64_CHARS) return "Image too large (maximum 10 MiB before base64 encoding).";
124
+ }
125
+ }
126
+
127
+ // src/web/server.ts
109
128
  import { join as join28, dirname as dirname11, resolve as resolve12, relative as relative6, sep as sep5, extname as extname3, resolve as resolvePath } from "path";
110
129
  import { existsSync as existsSync32, readFileSync as readFileSync25, readdirSync as readdirSync15, statSync as statSync16, realpathSync as realpathSync3, createReadStream } from "fs";
111
130
  import { networkInterfaces } from "os";
@@ -7013,7 +7032,7 @@ ${err.message}`;
7013
7032
  if (IMAGE_EXTENSIONS.has(ext)) {
7014
7033
  return `[Image file: ${filePath} (${ext})]
7015
7034
  read_file cannot return image content \u2014 tool results are text-only, so no tool can put an image into your context.
7016
- To have an image looked at, ask the USER to reference it from their input line with @${filePath} (they can use a glob for a batch, e.g. @dir/*.png, up to 20 at a time). A vision-capable model must be active, or the reference is rejected.
7035
+ To have an image looked at, ask the USER to reference it from their input line with @${filePath} (they can use a glob for a batch, e.g. @dir/*.png, up to ${MAX_GLOB_MATCHES} at a time). A vision-capable model must be active, or the reference is rejected.
7017
7036
  Do NOT claim you inspected this image \u2014 you have not seen it.`;
7018
7037
  }
7019
7038
  return `[Binary file: ${filePath} (${ext})]
@@ -7816,26 +7835,11 @@ function checkNetworkPolicy(toolName, args, policy) {
7816
7835
  // src/tools/truncate.ts
7817
7836
  var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
7818
7837
  var activeCap = DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP;
7819
- function setMaxOutputCap(cap) {
7820
- if (!cap || cap <= 0) {
7821
- activeCap = DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP;
7822
- } else {
7823
- activeCap = Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, cap);
7824
- }
7825
- if (lastContextWindow > 0) {
7826
- activeMaxChars = getMaxOutputChars(lastContextWindow);
7827
- }
7828
- }
7829
- function getMaxOutputChars(contextWindow) {
7838
+ function getMaxOutputChars(contextWindow, cap = activeCap) {
7830
7839
  if (!contextWindow || contextWindow <= 0) return DEFAULT_MAX_TOOL_OUTPUT_CHARS;
7831
- return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), activeCap));
7840
+ return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), cap > 0 ? cap : DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP));
7832
7841
  }
7833
7842
  var activeMaxChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
7834
- var lastContextWindow = 0;
7835
- function setContextWindow(contextWindow) {
7836
- lastContextWindow = contextWindow;
7837
- activeMaxChars = getMaxOutputChars(contextWindow);
7838
- }
7839
7843
  function getActiveMaxChars() {
7840
7844
  return activeMaxChars;
7841
7845
  }
@@ -8204,7 +8208,7 @@ var RuleBasedActionClassifier = class {
8204
8208
  var defaultActionClassifier = new RuleBasedActionClassifier();
8205
8209
 
8206
8210
  // src/tools/executor.ts
8207
- var PS_ERROR_RECORD_HEAD = /^(?:[\w.\-]+\s:\s)?(?:At line:\d+ char:\s*\d+|所在位置\s+行:\d+\s+字符:\s*\d+)\s*$/;
8211
+ var PS_ERROR_RECORD_HEAD = /^(?:[\w.\-]+\s:\s)?(?:At (?:line:\d+|\S+:\d+) char:\s*\d+|所在位置\s+(?:行|\S+):\d+\s+字符:\s*\d+)\s*$/;
8208
8212
  var ERROR_SIGNATURE = new RegExp(
8209
8213
  [
8210
8214
  // Python / .NET / Java: "OSError: …", "pkg.SomeException: …"
@@ -8220,11 +8224,22 @@ var ERROR_SIGNATURE = new RegExp(
8220
8224
  "i"
8221
8225
  );
8222
8226
  var PY_TRACEBACK_FRAME = /^\s*File ".*", line \d+/;
8227
+ function isPreviewNoise(line) {
8228
+ const t = line.trim();
8229
+ return /^(?:\[[^\]]+\]\s*)?Exit code \d+:$/.test(t) || // `formatFailureHeader()` 在拿不到退出码时打的那句 —— 与 "Exit code N:" 同类噪声。
8230
+ // 不过滤的话它会占掉第一个展示位:2026-09-05 实战里 `export_slides.ps1 | Select-Object
8231
+ // -First 3` 提前掐断管道被 PowerShell 报成失败,预览成了
8232
+ // 「Command failed before exit code could be determined: · …\slide-02.png」——
8233
+ // 两段都没用,而真正有信息的 `EXPORTED 25 slide(s)` 就在第二行、从没上屏。
8234
+ /^(?:\[[^\]]+\]\s*)?Command failed before exit code could be determined:$/.test(t) || /^Traceback \(most recent call last\):$/.test(t) || line.startsWith("[Command failed") || line.startsWith("\u{1F4A1} ") || t === "";
8235
+ }
8223
8236
  function buildErrorPreview(content) {
8224
8237
  const raw = content.split("\n").map((l) => l.replace(/\r+$/, ""));
8225
8238
  const undecorated = [];
8239
+ const decorationNeighbours = [];
8226
8240
  for (let i = 0; i < raw.length; i++) {
8227
8241
  if (PS_ERROR_RECORD_HEAD.test(raw[i])) {
8242
+ if (undecorated.length > 0) decorationNeighbours.push(undecorated[undecorated.length - 1]);
8228
8243
  i++;
8229
8244
  while (i < raw.length && /^\s*\+ /.test(raw[i])) i++;
8230
8245
  i--;
@@ -8232,14 +8247,7 @@ function buildErrorPreview(content) {
8232
8247
  }
8233
8248
  undecorated.push(raw[i]);
8234
8249
  }
8235
- const meaningful = undecorated.filter(
8236
- (l) => !/^(?:\[[^\]]+\]\s*)?Exit code \d+:$/.test(l.trim()) && // `formatFailureHeader()` 在拿不到退出码时打的那句 —— 与 "Exit code N:" 同类噪声。
8237
- // 不过滤的话它会占掉第一个展示位:2026-09-05 实战里 `export_slides.ps1 | Select-Object
8238
- // -First 3` 提前掐断管道被 PowerShell 报成失败,预览成了
8239
- // 「Command failed before exit code could be determined: · …\slide-02.png」——
8240
- // 两段都没用,而真正有信息的 `EXPORTED 25 slide(s)` 就在第二行、从没上屏。
8241
- !/^(?:\[[^\]]+\]\s*)?Command failed before exit code could be determined:$/.test(l.trim()) && !/^Traceback \(most recent call last\):$/.test(l.trim()) && !l.startsWith("[Command failed") && !l.startsWith("\u{1F4A1} ") && l.trim() !== ""
8242
- );
8250
+ const meaningful = undecorated.filter((l) => !isPreviewNoise(l));
8243
8251
  let primary = -1;
8244
8252
  for (let i = meaningful.length - 1; i >= 0; i--) {
8245
8253
  if (ERROR_SIGNATURE.test(meaningful[i])) {
@@ -8258,7 +8266,12 @@ function buildErrorPreview(content) {
8258
8266
  }
8259
8267
  previewLines = frame !== null ? [frame, meaningful[primary]] : [meaningful[primary]];
8260
8268
  } else {
8261
- previewLines = meaningful.length > 2 ? [meaningful[0], meaningful[meaningful.length - 1]] : meaningful.slice(0, 2);
8269
+ const neighbour = decorationNeighbours.find((l) => !isPreviewNoise(l));
8270
+ if (neighbour !== void 0) {
8271
+ previewLines = [neighbour];
8272
+ } else {
8273
+ previewLines = meaningful.length > 2 ? [meaningful[0], meaningful[meaningful.length - 1]] : meaningful.slice(0, 2);
8274
+ }
8262
8275
  }
8263
8276
  const preview = previewLines.map((l) => l.length > 160 ? l.slice(0, 160) + "\u2026" : l).join(" \xB7 ") || content.slice(0, 120);
8264
8277
  return preview.slice(0, 240);
@@ -8965,7 +8978,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
8965
8978
  const mode = appendMode ? "appended" : "written";
8966
8979
  void (async () => {
8967
8980
  try {
8968
- const { updateFile } = await import("./indexer-RBEKMFT4.js");
8981
+ const { updateFile } = await import("./indexer-FPKY7LCT.js");
8969
8982
  await updateFile(process.cwd(), filePath);
8970
8983
  } catch {
8971
8984
  }
@@ -14096,6 +14109,27 @@ function suggestSkillName(names, input) {
14096
14109
  // src/web/tool-executor-web.ts
14097
14110
  import { randomUUID as randomUUID2 } from "crypto";
14098
14111
  import { tmpdir as tmpdir2 } from "os";
14112
+
14113
+ // src/web/socket-send.ts
14114
+ var MAX_PENDING_OUTPUT_BYTES = 16 * 1024 * 1024;
14115
+ var stalled = /* @__PURE__ */ new WeakSet();
14116
+ function sendSocketMessage(ws, message) {
14117
+ if (ws.readyState !== ws.OPEN || stalled.has(ws)) return false;
14118
+ const data = JSON.stringify(message);
14119
+ if ((ws.bufferedAmount || 0) + Buffer.byteLength(data) > MAX_PENDING_OUTPUT_BYTES) {
14120
+ stalled.add(ws);
14121
+ ws.close(1013, "Output backlog exceeded; reconnect to reload session");
14122
+ const timer = setTimeout(() => {
14123
+ if (ws.readyState !== ws.CLOSED) ws.terminate();
14124
+ }, 1e3);
14125
+ timer.unref();
14126
+ return false;
14127
+ }
14128
+ ws.send(data);
14129
+ return true;
14130
+ }
14131
+
14132
+ // src/web/tool-executor-web.ts
14099
14133
  import { existsSync as existsSync22, readFileSync as readFileSync17 } from "fs";
14100
14134
  var ToolExecutorWeb = class _ToolExecutorWeb {
14101
14135
  constructor(registry, ws) {
@@ -14125,6 +14159,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14125
14159
  // 5 minutes
14126
14160
  /** Publicly readable by SessionHandler to check if confirm is active */
14127
14161
  confirming = false;
14162
+ maxOutputChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
14128
14163
  /** Session-level auto-approve toggle (/yolo command) */
14129
14164
  sessionAutoApprove = false;
14130
14165
  /** Session-level Auto Mode: rule-based low-risk auto approval. */
@@ -14207,9 +14242,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14207
14242
  this.confirming = false;
14208
14243
  }
14209
14244
  send(msg) {
14210
- if (this.ws.readyState === this.ws.OPEN) {
14211
- this.ws.send(JSON.stringify(msg));
14212
- }
14245
+ sendSocketMessage(this.ws, msg);
14213
14246
  }
14214
14247
  sendToolCallStart(call) {
14215
14248
  const dangerLevel = getDangerLevel(call.name, call.arguments);
@@ -14402,7 +14435,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14402
14435
  if (!toolCallStarted) this.sendToolCallStart(call);
14403
14436
  try {
14404
14437
  const rawContent = await runTool(tool, call.arguments, call.name);
14405
- const content = truncateOutput(rawContent, call.name);
14438
+ const content = truncateOutput(rawContent, call.name, this.maxOutputChars);
14406
14439
  this.sendToolCallResult(call, rawContent, false);
14407
14440
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
14408
14441
  runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
@@ -14467,7 +14500,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14467
14500
  }
14468
14501
  try {
14469
14502
  const rawContent = await runTool(tool, call.arguments, call.name);
14470
- const content = truncateOutput(rawContent, call.name);
14503
+ const content = truncateOutput(rawContent, call.name, this.maxOutputChars);
14471
14504
  this.sendToolCallResult(call, rawContent, false);
14472
14505
  results[i] = { callId: call.id, content, isError: false };
14473
14506
  } catch (err) {
@@ -15416,6 +15449,7 @@ var HubAgent = class {
15416
15449
  model: this.modelId,
15417
15450
  systemPrompt,
15418
15451
  stream: false,
15452
+ signal: opts?.signal,
15419
15453
  temperature: 0.7,
15420
15454
  maxTokens: 4096,
15421
15455
  ...this.thinking ?? {}
@@ -15450,15 +15484,21 @@ var HubAgent = class {
15450
15484
  model: this.modelId,
15451
15485
  systemPrompt,
15452
15486
  stream: true,
15487
+ signal: opts?.signal,
15453
15488
  temperature: 0.7,
15454
15489
  maxTokens: 4096,
15455
15490
  ...this.thinking ?? {}
15456
15491
  });
15457
- for await (const chunk of stream) {
15458
- if (chunk.delta) {
15459
- raw += chunk.delta;
15460
- onToken?.(chunk.delta);
15492
+ try {
15493
+ for await (const chunk of stream) {
15494
+ if (chunk.delta) {
15495
+ raw += chunk.delta;
15496
+ onToken?.(chunk.delta);
15497
+ }
15498
+ if (opts?.signal?.aborted) break;
15461
15499
  }
15500
+ } catch (error) {
15501
+ if (!opts?.signal?.aborted) throw error;
15462
15502
  }
15463
15503
  const { content, passed, converged } = parseTurn(raw);
15464
15504
  return {
@@ -15473,7 +15513,7 @@ var HubAgent = class {
15473
15513
  /**
15474
15514
  * Generate a summary of the entire discussion from this agent's perspective.
15475
15515
  */
15476
- async summarize(topic, history) {
15516
+ async summarize(topic, history, signal) {
15477
15517
  const provider = this.providers.get(this.providerId);
15478
15518
  if (!provider) {
15479
15519
  throw new Error(`Provider "${this.providerId}" not available`);
@@ -15490,6 +15530,7 @@ var HubAgent = class {
15490
15530
  model: this.modelId,
15491
15531
  stream: false,
15492
15532
  temperature: 0.3,
15533
+ signal,
15493
15534
  maxTokens: 4096,
15494
15535
  ...this.thinking ?? {}
15495
15536
  });
@@ -15583,6 +15624,8 @@ var DiscussionOrchestrator = class {
15583
15624
  agents = [];
15584
15625
  state;
15585
15626
  aborted = false;
15627
+ controller = new AbortController();
15628
+ cancelReview;
15586
15629
  humanSteer;
15587
15630
  voteConverge;
15588
15631
  /** Callback for rendering events */
@@ -15622,6 +15665,8 @@ var DiscussionOrchestrator = class {
15622
15665
  /** Signal the orchestrator to stop after current turn */
15623
15666
  abort() {
15624
15667
  this.aborted = true;
15668
+ this.controller.abort();
15669
+ this.cancelReview?.();
15625
15670
  }
15626
15671
  /**
15627
15672
  * Run the full discussion on a given topic.
@@ -15632,6 +15677,7 @@ var DiscussionOrchestrator = class {
15632
15677
  this.state.round = 0;
15633
15678
  this.state.finished = false;
15634
15679
  this.aborted = false;
15680
+ this.controller = new AbortController();
15635
15681
  try {
15636
15682
  for (let round = 1; round <= this.state.maxRounds; round++) {
15637
15683
  if (this.aborted) {
@@ -15652,7 +15698,7 @@ var DiscussionOrchestrator = class {
15652
15698
  round,
15653
15699
  this.state.maxRounds,
15654
15700
  (token) => this.emit({ type: "agent_token", roleId: agent.role.id, token }),
15655
- { voteConverge: this.voteConverge }
15701
+ { voteConverge: this.voteConverge, signal: this.controller.signal }
15656
15702
  );
15657
15703
  this.state.messages.push(message);
15658
15704
  if (message.converged) convergedCount++;
@@ -15664,6 +15710,7 @@ var DiscussionOrchestrator = class {
15664
15710
  this.emit({ type: "agent_spoke", roleId: agent.role.id, message });
15665
15711
  }
15666
15712
  } catch (err) {
15713
+ if (this.aborted) break;
15667
15714
  const errMsg = err instanceof Error ? err.message : String(err);
15668
15715
  this.emit({ type: "error", roleId: agent.role.id, message: errMsg });
15669
15716
  this.state.messages.push({
@@ -15675,6 +15722,10 @@ var DiscussionOrchestrator = class {
15675
15722
  }
15676
15723
  }
15677
15724
  this.emit({ type: "round_end", round });
15725
+ if (this.aborted) {
15726
+ this.emit({ type: "discussion_end", reason: "user_interrupt" });
15727
+ break;
15728
+ }
15678
15729
  if (allPassed && round > 1) {
15679
15730
  this.emit({ type: "discussion_end", reason: "consensus" });
15680
15731
  break;
@@ -15691,7 +15742,18 @@ var DiscussionOrchestrator = class {
15691
15742
  break;
15692
15743
  }
15693
15744
  if (this.humanSteer && this.onRoundReview && !this.aborted) {
15694
- const steer = await this.onRoundReview({ round, maxRounds: this.state.maxRounds, state: this.state });
15745
+ const stopped = new Promise((resolve13) => {
15746
+ this.cancelReview = () => resolve13({ action: "stop" });
15747
+ });
15748
+ let steer;
15749
+ try {
15750
+ steer = await Promise.race([
15751
+ this.onRoundReview({ round, maxRounds: this.state.maxRounds, state: this.state }),
15752
+ stopped
15753
+ ]);
15754
+ } finally {
15755
+ this.cancelReview = void 0;
15756
+ }
15695
15757
  if (steer.action === "stop") {
15696
15758
  this.emit({ type: "discussion_end", reason: "human_stop" });
15697
15759
  break;
@@ -15712,7 +15774,7 @@ var DiscussionOrchestrator = class {
15712
15774
  const errMsg = err instanceof Error ? err.message : String(err);
15713
15775
  this.emit({ type: "error", message: errMsg });
15714
15776
  }
15715
- await this.generateSummary();
15777
+ if (!this.aborted) await this.generateSummary();
15716
15778
  this.state.finished = true;
15717
15779
  return this.state;
15718
15780
  }
@@ -15730,7 +15792,7 @@ var DiscussionOrchestrator = class {
15730
15792
  return;
15731
15793
  }
15732
15794
  try {
15733
- this.state.summary = await summarizer.summarize(this.state.topic, this.state.messages);
15795
+ this.state.summary = await summarizer.summarize(this.state.topic, this.state.messages, this.controller.signal);
15734
15796
  this.emit({ type: "summary", content: this.state.summary });
15735
15797
  } catch (err) {
15736
15798
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -16206,11 +16268,11 @@ function routeDecisionRows(message, currentModel, decision) {
16206
16268
  }
16207
16269
 
16208
16270
  // src/web/commands/provider-commands.ts
16209
- function syncSessionProvider(ctx) {
16271
+ async function syncSessionProvider(ctx) {
16210
16272
  const session = ctx.sessions.current;
16211
16273
  if (!session) return;
16212
16274
  session.updateProvider(ctx.currentProvider, ctx.currentModel);
16213
- ctx.saveIfNeeded();
16275
+ await ctx.saveIfNeeded();
16214
16276
  }
16215
16277
  async function handleProvider(args, ctx) {
16216
16278
  const id = args[0];
@@ -16253,7 +16315,7 @@ async function handleProvider(args, ctx) {
16253
16315
  ctx.currentProvider = outcome.providerId;
16254
16316
  ctx.currentModel = outcome.model;
16255
16317
  ctx.updateContextWindow();
16256
- syncSessionProvider(ctx);
16318
+ await syncSessionProvider(ctx);
16257
16319
  ctx.send({ type: "info", message: outcome.message });
16258
16320
  ctx.sendStatus();
16259
16321
  }
@@ -16307,7 +16369,7 @@ async function handleModel(args, ctx) {
16307
16369
  }
16308
16370
  ctx.currentModel = outcome.model;
16309
16371
  ctx.updateContextWindow();
16310
- syncSessionProvider(ctx);
16372
+ await syncSessionProvider(ctx);
16311
16373
  ctx.send({
16312
16374
  type: "info",
16313
16375
  message: outcome.warning ? `${outcome.message} \u2014 ${outcome.warning}` : outcome.message
@@ -17192,7 +17254,7 @@ async function handleClear(_args, ctx) {
17192
17254
  const prior = ctx.sessions.current;
17193
17255
  const priorId = prior?.id ?? null;
17194
17256
  const priorCount = prior?.messages.length ?? 0;
17195
- ctx.saveIfNeeded();
17257
+ await ctx.saveIfNeeded();
17196
17258
  const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
17197
17259
  ctx.unsavedSessions.set(created.id, created);
17198
17260
  ctx.resetWebSessionUsage();
@@ -17214,7 +17276,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
17214
17276
  Cost: ${formatCost(cost)}` : "";
17215
17277
  let memoryLine = "";
17216
17278
  try {
17217
- const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-A7RPDLR7.js");
17279
+ const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-XZISXRWT.js");
17218
17280
  const pending = countPendingMemories2(ctx.config.getConfigDir());
17219
17281
  if (pending > 0) memoryLine = `
17220
17282
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -17230,8 +17292,13 @@ Tokens: in=${ctx.sessionTokenUsage.inputTokens} out=${ctx.sessionTokenUsage.outp
17230
17292
  }
17231
17293
  async function handleSession(args, ctx) {
17232
17294
  const sub = args[0];
17295
+ if (sub === "save") {
17296
+ await ctx.saveIfNeeded();
17297
+ if (!ctx.sessions.current?.dirty) ctx.send({ type: "info", message: "Session saved." });
17298
+ return;
17299
+ }
17233
17300
  if (sub === "new") {
17234
- ctx.saveIfNeeded();
17301
+ await ctx.saveIfNeeded();
17235
17302
  const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
17236
17303
  ctx.unsavedSessions.set(created.id, created);
17237
17304
  ctx.resetWebSessionUsage();
@@ -17242,7 +17309,7 @@ async function handleSession(args, ctx) {
17242
17309
  }
17243
17310
  if (sub === "load" && args[1]) {
17244
17311
  const targetId = args[1];
17245
- ctx.saveIfNeeded();
17312
+ await ctx.saveIfNeeded();
17246
17313
  const cachedExact = ctx.unsavedSessions.get(targetId);
17247
17314
  const cached = cachedExact ?? [...ctx.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
17248
17315
  if (cached) {
@@ -18197,7 +18264,7 @@ async function handleTest(args, ctx) {
18197
18264
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
18198
18265
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
18199
18266
  }
18200
- const runTests = ctx.runTests ?? (await import("./run-tests-MJ3SCW27.js")).executeTests;
18267
+ const runTests = ctx.runTests ?? (await import("./run-tests-UEE3OMLS.js")).executeTests;
18201
18268
  const report = await runTests(testArgs);
18202
18269
  ctx.send({ type: "info", message: report });
18203
18270
  } catch (err) {
@@ -18489,9 +18556,9 @@ async function handleContext(_args, ctx) {
18489
18556
  async function handleIndex(args, ctx) {
18490
18557
  const sub = (args[0] ?? "status").toLowerCase();
18491
18558
  const root = process.cwd();
18492
- const { loadIndex: loadIndex2, clearIndex } = await import("./store-VO37H6LS.js");
18493
- const { indexProject: indexProject2 } = await import("./indexer-RBEKMFT4.js");
18494
- const { loadVectorStore, clearVectorStore } = await import("./vector-store-JBAE6PS4.js");
18559
+ const { loadIndex: loadIndex2, clearIndex } = await import("./store-SIKHCUYJ.js");
18560
+ const { indexProject: indexProject2 } = await import("./indexer-FPKY7LCT.js");
18561
+ const { loadVectorStore, clearVectorStore } = await import("./vector-store-JAXWDZYW.js");
18495
18562
  if (sub === "status") {
18496
18563
  const idx = loadIndex2(root);
18497
18564
  const vec = loadVectorStore(root);
@@ -18514,6 +18581,7 @@ async function handleIndex(args, ctx) {
18514
18581
  } else if (sub === "rebuild") {
18515
18582
  ctx.send({ type: "info", message: `Indexing ${root}\u2026` });
18516
18583
  const { stats } = await indexProject2(root, { force: true });
18584
+ if (stats.incomplete) ctx.send({ type: "info", message: "Partial index: scan limit or unreadable files reached. Unscanned entries were retained." });
18517
18585
  ctx.send({
18518
18586
  type: "info",
18519
18587
  message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
@@ -18540,7 +18608,7 @@ async function handleIndex(args, ctx) {
18540
18608
  message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
18541
18609
  });
18542
18610
  try {
18543
- const { rebuildSemanticIndex } = await import("./semantic-VPNKXKKK.js");
18611
+ const { rebuildSemanticIndex } = await import("./semantic-QOBAXEQ4.js");
18544
18612
  const stats = await rebuildSemanticIndex(root);
18545
18613
  const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
18546
18614
  ctx.send({
@@ -18587,7 +18655,7 @@ async function handleMemory(args, ctx) {
18587
18655
  }
18588
18656
  if (intent.kind === "status") {
18589
18657
  const { getChatIndexStatus: getChatIndexStatus2 } = await import("./chat-index-DGUOUUQW.js");
18590
- const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-A7RPDLR7.js");
18658
+ const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-XZISXRWT.js");
18591
18659
  const memories = listMemoryEntries2(configDir, { includeExpired: true, includeRejected: true });
18592
18660
  const injected = formatMemoryForPrompt2(configDir, process.cwd(), ctx.config.get("memory")?.maxChars);
18593
18661
  const view = buildMemoryStatusView({
@@ -18610,7 +18678,7 @@ async function handleMemory(args, ctx) {
18610
18678
  return;
18611
18679
  }
18612
18680
  if (intent.kind === "recall") {
18613
- const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-A7RPDLR7.js");
18681
+ const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-XZISXRWT.js");
18614
18682
  const { searchChatMemory: searchChatMemory2 } = await import("./chat-index-DGUOUUQW.js");
18615
18683
  const persistentHits = searchPersistentMemories2(configDir, intent.query, process.cwd(), 3);
18616
18684
  if (persistentHits.length > 0) {
@@ -18731,7 +18799,7 @@ async function handleMemory(args, ctx) {
18731
18799
  ctx.handleMemoryManage(intent.action, args[1], intent.action === "expire" ? args[2] : void 0);
18732
18800
  } else if (intent.kind === "export") {
18733
18801
  if (args[1] === "md") {
18734
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-A7RPDLR7.js");
18802
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-XZISXRWT.js");
18735
18803
  ctx.send({
18736
18804
  type: "export_data",
18737
18805
  format: "md",
@@ -19736,7 +19804,7 @@ No tools match "${filter}".
19736
19804
  const { join: join29 } = await import("path");
19737
19805
  const { existsSync: existsSync33 } = await import("fs");
19738
19806
  const { getGitRoot: getGitRoot2 } = await import("./git-context-VNCA2GU7.js");
19739
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-JDPKWZJP.js");
19807
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-AXBD2EEB.js");
19740
19808
  const { approveProject, hashMcpFile } = await import("./project-trust-MUG325AW.js");
19741
19809
  const cwd = process.cwd();
19742
19810
  const projectRoot = getGitRoot2(cwd) ?? cwd;
@@ -20655,7 +20723,7 @@ function createReviewCommands() {
20655
20723
  usage: "/test [command|filter]",
20656
20724
  async execute(args, ctx) {
20657
20725
  try {
20658
- const { executeTests } = await import("./run-tests-MJ3SCW27.js");
20726
+ const { executeTests } = await import("./run-tests-UEE3OMLS.js");
20659
20727
  const argStr = args.join(" ").trim();
20660
20728
  let testArgs = {};
20661
20729
  if (argStr) {
@@ -20906,9 +20974,9 @@ function createMemoryCommands() {
20906
20974
  async execute(args, ctx) {
20907
20975
  const sub = (args[0] ?? "status").toLowerCase();
20908
20976
  const root = process.cwd();
20909
- const { loadIndex: loadIndex2, clearIndex } = await import("./store-VO37H6LS.js");
20910
- const { indexProject: indexProject2 } = await import("./indexer-RBEKMFT4.js");
20911
- const { loadVectorStore, clearVectorStore } = await import("./vector-store-JBAE6PS4.js");
20977
+ const { loadIndex: loadIndex2, clearIndex } = await import("./store-SIKHCUYJ.js");
20978
+ const { indexProject: indexProject2 } = await import("./indexer-FPKY7LCT.js");
20979
+ const { loadVectorStore, clearVectorStore } = await import("./vector-store-JAXWDZYW.js");
20912
20980
  if (sub === "status") {
20913
20981
  const idx = loadIndex2(root);
20914
20982
  const vec = loadVectorStore(root);
@@ -20939,6 +21007,7 @@ function createMemoryCommands() {
20939
21007
  }
20940
21008
  });
20941
21009
  process.stdout.write("\r\x1B[K");
21010
+ if (stats.incomplete) console.log(theme.dim(" Partial index: scan limit or unreadable files reached; unscanned entries retained."));
20942
21011
  console.log(theme.success(
20943
21012
  ` \u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
20944
21013
  ));
@@ -20967,7 +21036,7 @@ function createMemoryCommands() {
20967
21036
  }
20968
21037
  console.log(theme.dim(` Building semantic index for ${idx.symbolCount} symbols\u2026`));
20969
21038
  console.log(theme.dim(" (First run downloads ~117 MB embedding model to ~/.aicli/models/)"));
20970
- const { rebuildSemanticIndex } = await import("./semantic-VPNKXKKK.js");
21039
+ const { rebuildSemanticIndex } = await import("./semantic-QOBAXEQ4.js");
20971
21040
  try {
20972
21041
  const stats = await rebuildSemanticIndex(root, {
20973
21042
  onProgress: (done, total) => {
@@ -22566,7 +22635,7 @@ var webCommandHandlers = {
22566
22635
  search: handleSearch,
22567
22636
  undo: handleUndo
22568
22637
  };
22569
- var SessionHandler = class {
22638
+ var SessionHandler = class _SessionHandler {
22570
22639
  ws;
22571
22640
  config;
22572
22641
  costTracker;
@@ -22617,6 +22686,7 @@ var SessionHandler = class {
22617
22686
  abortController = null;
22618
22687
  userInterjection = null;
22619
22688
  processing = false;
22689
+ disconnected = false;
22620
22690
  /** P4: active hub discussion orchestrator (web room), if any. */
22621
22691
  hubOrchestrator = null;
22622
22692
  /** P4b: pending between-rounds human-steer prompts (requestId → resolver). */
@@ -22643,9 +22713,13 @@ var SessionHandler = class {
22643
22713
  * 会话一旦保存到磁盘(有消息后),会从此 Map 中移除。
22644
22714
  */
22645
22715
  unsavedSessions = /* @__PURE__ */ new Map();
22716
+ // Failed saves survive a socket reconnect, scoped to the owning config directory.
22717
+ static recovery = /* @__PURE__ */ new Map();
22718
+ static acceptedChats = /* @__PURE__ */ new Map();
22646
22719
  constructor(ws, shared) {
22647
22720
  this.ws = ws;
22648
22721
  this.config = shared.config;
22722
+ this.unsavedSessions = new Map(_SessionHandler.recovery.get(this.config.getConfigDir()));
22649
22723
  this.costTracker = shared.costTracker;
22650
22724
  this.providers = shared.providers;
22651
22725
  this.sessions = shared.sessions;
@@ -22713,9 +22787,7 @@ var SessionHandler = class {
22713
22787
  * 接口之间的任何漂移(少一个成员、签名改了)都会当场编译失败,而不是等到运行时。
22714
22788
  */
22715
22789
  send(msg) {
22716
- if (this.ws.readyState === this.ws.OPEN) {
22717
- this.ws.send(JSON.stringify(msg));
22718
- }
22790
+ sendSocketMessage(this.ws, msg);
22719
22791
  }
22720
22792
  sendStatus() {
22721
22793
  const providerList = this.providers.listAvailable().map((p) => ({
@@ -22769,6 +22841,7 @@ var SessionHandler = class {
22769
22841
  });
22770
22842
  }
22771
22843
  async handleMessage(raw) {
22844
+ if (this.disconnected) return;
22772
22845
  let msg;
22773
22846
  try {
22774
22847
  msg = JSON.parse(raw);
@@ -22777,8 +22850,33 @@ var SessionHandler = class {
22777
22850
  return;
22778
22851
  }
22779
22852
  switch (msg.type) {
22780
- case "chat":
22853
+ case "chat": {
22854
+ if (msg.messageId) {
22855
+ if (typeof msg.messageId !== "string" || msg.messageId.length > 200) throw new Error("Invalid message ID");
22856
+ const scope = this.config.getConfigDir();
22857
+ const accepted = _SessionHandler.acceptedChats.get(scope) ?? /* @__PURE__ */ new Map();
22858
+ const prior = accepted.get(msg.messageId);
22859
+ if (prior) {
22860
+ this.send({ type: "chat_ack", messageId: msg.messageId, accepted: true, duplicate: true, sessionId: prior });
22861
+ return;
22862
+ }
22863
+ if (this.processing || typeof msg.content !== "string") {
22864
+ this.send({ type: "chat_ack", messageId: msg.messageId, accepted: false, error: "A request is already running, or the message is invalid. Your draft was kept." });
22865
+ return;
22866
+ }
22867
+ const imageError = imageValidationError(msg.images);
22868
+ if (imageError) {
22869
+ this.send({ type: "chat_ack", messageId: msg.messageId, accepted: false, error: imageError });
22870
+ return;
22871
+ }
22872
+ this.ensureSession();
22873
+ accepted.set(msg.messageId, this.sessions.current.id);
22874
+ while (accepted.size > 1e3) accepted.delete(accepted.keys().next().value);
22875
+ _SessionHandler.acceptedChats.set(scope, accepted);
22876
+ this.send({ type: "chat_ack", messageId: msg.messageId, accepted: true, sessionId: this.sessions.current.id });
22877
+ }
22781
22878
  return this.handleChat(msg.content, msg.images);
22879
+ }
22782
22880
  case "command":
22783
22881
  return this.handleCommand(msg.name, msg.args);
22784
22882
  case "confirm_response":
@@ -22820,7 +22918,7 @@ var SessionHandler = class {
22820
22918
  case "hub_start":
22821
22919
  return this.handleHubStart(msg);
22822
22920
  case "hub_abort":
22823
- this.hubOrchestrator?.abort();
22921
+ this.cancelHub();
22824
22922
  return;
22825
22923
  case "hub_steer": {
22826
22924
  const resolve13 = this.pendingHubReview.get(msg.requestId);
@@ -22835,7 +22933,10 @@ var SessionHandler = class {
22835
22933
  return;
22836
22934
  }
22837
22935
  }
22838
- onDisconnect() {
22936
+ onDisconnect(closeSocket = true) {
22937
+ if (this.disconnected) return;
22938
+ this.disconnected = true;
22939
+ if (closeSocket && this.ws.readyState === this.ws.OPEN) this.ws.close(1001, "Connection closed; reconnect to restore session");
22839
22940
  runLifecycleHooks(mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()), "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
22840
22941
  this.toolExecutor.cancelAll();
22841
22942
  if (this.abortController) this.abortController.abort();
@@ -22843,18 +22944,24 @@ var SessionHandler = class {
22843
22944
  this.pendingAskUser.clear();
22844
22945
  for (const resolve13 of this.pendingAutoPause.values()) resolve13({ action: "stop" });
22845
22946
  this.pendingAutoPause.clear();
22846
- this.saveIfNeeded();
22947
+ this.cancelHub();
22948
+ void this.saveIfNeeded();
22847
22949
  if (this._bashSessionKey) clearBashSession(this._bashSessionKey);
22848
22950
  }
22951
+ cancelHub() {
22952
+ this.hubOrchestrator?.abort();
22953
+ for (const resolve13 of this.pendingHubReview.values()) resolve13({ action: "stop" });
22954
+ this.pendingHubReview.clear();
22955
+ }
22849
22956
  /** 根据当前模型 context window 更新工具输出截断上限 */
22850
22957
  updateContextWindow() {
22851
22958
  try {
22852
22959
  const cfgOutputCap = this.config.get("maxToolOutputChars");
22853
- if (typeof cfgOutputCap === "number" && cfgOutputCap > 0) setMaxOutputCap(cfgOutputCap);
22854
22960
  const provider = this.providers.get(this.currentProvider);
22855
22961
  const modelInfo = provider?.info.models.find((m) => m.id === this.currentModel);
22856
- if (modelInfo?.contextWindow) setContextWindow(modelInfo.contextWindow);
22962
+ this.toolExecutor.maxOutputChars = getMaxOutputChars(modelInfo?.contextWindow, cfgOutputCap ?? 0);
22857
22963
  } catch {
22964
+ this.toolExecutor.maxOutputChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
22858
22965
  }
22859
22966
  }
22860
22967
  /** 获取当前模型的 context window 大小(0 表示未知)*/
@@ -22906,12 +23013,24 @@ var SessionHandler = class {
22906
23013
  return total;
22907
23014
  }
22908
23015
  /** Save session only if it exists and has messages (never persist empty "Untitled" sessions). */
22909
- saveIfNeeded() {
22910
- if (this.sessions.current && this.sessions.current.messages.length > 0) {
22911
- autoTrimSessionIfNeeded(this.sessions.current);
22912
- const id = this.sessions.current.id;
22913
- this.sessions.save();
22914
- this.unsavedSessions.delete(id);
23016
+ async saveIfNeeded() {
23017
+ const session = this.sessions.current;
23018
+ if (!session?.messages.length) return;
23019
+ const scope = this.config.getConfigDir();
23020
+ try {
23021
+ if (session.dirty) autoTrimSessionIfNeeded(session);
23022
+ await this.sessions.save();
23023
+ if (session.dirty) return;
23024
+ this.unsavedSessions.delete(session.id);
23025
+ _SessionHandler.recovery.get(scope)?.delete(session.id);
23026
+ } catch (err) {
23027
+ this.unsavedSessions.set(session.id, session);
23028
+ const recovered = _SessionHandler.recovery.get(scope) ?? /* @__PURE__ */ new Map();
23029
+ recovered.set(session.id, session);
23030
+ _SessionHandler.recovery.set(scope, recovered);
23031
+ const message = `Session ${session.id} was NOT saved: ${err instanceof Error ? err.message : String(err)}. Content is retained in memory; use /session save to retry. Do not stop the server before saving.`;
23032
+ this.send({ type: "info", message });
23033
+ process.stderr.write(message + "\n");
22915
23034
  }
22916
23035
  }
22917
23036
  /** Lazily create a session if none exists yet (deferred from constructor). */
@@ -23036,20 +23155,15 @@ var SessionHandler = class {
23036
23155
  const kinds = [...new Set(secretHits.map((h) => h.kind))].join(", ");
23037
23156
  this.send({ type: "info", message: "Possible secret in prompt (" + kinds + ")." });
23038
23157
  }
23039
- const ALLOWED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
23040
- const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
23158
+ const imageError = imageValidationError(images);
23159
+ if (imageError) {
23160
+ this.send({ type: "error", message: imageError });
23161
+ return;
23162
+ }
23041
23163
  let msgContent;
23042
23164
  if (images && images.length > 0) {
23043
23165
  const parts = [];
23044
23166
  for (const img of images) {
23045
- if (!ALLOWED_IMAGE_MIMES.has(img.mime)) {
23046
- this.send({ type: "error", message: `Rejected image: unsupported MIME type "${img.mime}"` });
23047
- return;
23048
- }
23049
- if (img.data && img.data.length > MAX_IMAGE_SIZE) {
23050
- this.send({ type: "error", message: `Rejected image: too large (${(img.data.length / 1024 / 1024).toFixed(1)} MB, max 10 MB)` });
23051
- return;
23052
- }
23053
23167
  parts.push({
23054
23168
  type: "image_url",
23055
23169
  image_url: { url: `data:${img.mime};base64,${img.data}` }
@@ -23103,7 +23217,7 @@ var SessionHandler = class {
23103
23217
  this.send({ type: "error", message });
23104
23218
  } finally {
23105
23219
  this.processing = false;
23106
- this.saveIfNeeded();
23220
+ await this.saveIfNeeded();
23107
23221
  this.sendStatus();
23108
23222
  this.sendSessionList();
23109
23223
  }
@@ -23620,6 +23734,10 @@ ${summaryContent}`,
23620
23734
  }
23621
23735
  // ── Commands ─────────────────────────────────────────────────────
23622
23736
  async handleCommand(name, args) {
23737
+ if (this.processing && ["session", "clear", "provider", "model", "rewind", "branch", "compact"].includes(name)) {
23738
+ this.send({ type: "info", message: "Stop the current request before changing sessions or conversation settings." });
23739
+ return;
23740
+ }
23623
23741
  const cliOnlyReason = CLI_ONLY_WEB_COMMANDS.get(name);
23624
23742
  if (cliOnlyReason) {
23625
23743
  this.send({
@@ -24566,7 +24684,7 @@ async function startWebServer(options = {}) {
24566
24684
  next();
24567
24685
  });
24568
24686
  const server = createServer(app);
24569
- const WS_MAX_PAYLOAD = 1 * 1024 * 1024;
24687
+ const WS_MAX_PAYLOAD = MAX_CHAT_BYTES;
24570
24688
  const WS_MSG_RATE_PER_SEC = 30;
24571
24689
  const WS_MSG_BURST = 60;
24572
24690
  const WS_PREAUTH_TIMEOUT_MS = 3e4;
@@ -24639,6 +24757,7 @@ async function startWebServer(options = {}) {
24639
24757
  app.get("/api/status", (_req, res) => {
24640
24758
  res.json({
24641
24759
  version: VERSION,
24760
+ chatLimits: { maxChatBytes: MAX_CHAT_BYTES, maxImageBytes: MAX_IMAGE_BYTES },
24642
24761
  providers: availableProviders.map((p) => ({
24643
24762
  id: p.info.id,
24644
24763
  displayName: p.info.displayName,
@@ -24847,11 +24966,6 @@ async function startWebServer(options = {}) {
24847
24966
  const tabId = params.get("tabId") || `tab-${Date.now()}`;
24848
24967
  const cookies = parseCookie(req.headers.cookie ?? "");
24849
24968
  const token = cookies["aicli_token"] || "";
24850
- const existing = handlers.get(tabId);
24851
- if (existing) {
24852
- existing.onDisconnect();
24853
- handlers.delete(tabId);
24854
- }
24855
24969
  let tokens = WS_MSG_BURST;
24856
24970
  let lastRefill = Date.now();
24857
24971
  const takeToken = () => {
@@ -24867,6 +24981,32 @@ async function startWebServer(options = {}) {
24867
24981
  };
24868
24982
  let authenticatedUser = null;
24869
24983
  let handler = null;
24984
+ let handlerKey = "";
24985
+ const attachHandler = (resources) => {
24986
+ if (handler) {
24987
+ handler.onDisconnect(false);
24988
+ if (handlers.get(handlerKey) === handler) handlers.delete(handlerKey);
24989
+ }
24990
+ handlerKey = JSON.stringify([authenticatedUser, tabId]);
24991
+ const existing = handlers.get(handlerKey);
24992
+ existing?.onDisconnect();
24993
+ const attached = new SessionHandler(ws, { ...resources, sessions: new SessionManager(resources.config) });
24994
+ handlers.set(handlerKey, attached);
24995
+ return attached;
24996
+ };
24997
+ let alive = true;
24998
+ ws.on("pong", () => {
24999
+ alive = true;
25000
+ });
25001
+ const heartbeat = setInterval(() => {
25002
+ if (!alive) {
25003
+ ws.terminate();
25004
+ return;
25005
+ }
25006
+ alive = false;
25007
+ if (ws.readyState === ws.OPEN) ws.ping();
25008
+ }, 3e4);
25009
+ heartbeat.unref();
24870
25010
  let preAuthTimer = setTimeout(() => {
24871
25011
  if (!handler && ws.readyState === ws.OPEN) {
24872
25012
  try {
@@ -24887,14 +25027,12 @@ async function startWebServer(options = {}) {
24887
25027
  const authRequired = authManager.isEnabled();
24888
25028
  if (!authRequired) {
24889
25029
  console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (no auth) (total: ${handlers.size + 1})`);
24890
- handler = new SessionHandler(ws, shared);
24891
- handlers.set(tabId, handler);
25030
+ handler = attachHandler(shared);
24892
25031
  clearPreAuthTimer();
24893
25032
  } else if (authenticatedUser) {
24894
25033
  console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (user: ${authenticatedUser}) (total: ${handlers.size + 1})`);
24895
25034
  const userShared = getUserShared(authenticatedUser);
24896
- handler = new SessionHandler(ws, userShared);
24897
- handlers.set(tabId, handler);
25035
+ handler = attachHandler(userShared);
24898
25036
  clearPreAuthTimer();
24899
25037
  } else {
24900
25038
  console.log(` \u23F3 Tab waiting auth: ${tabId.slice(0, 12)}`);
@@ -24939,8 +25077,7 @@ async function startWebServer(options = {}) {
24939
25077
  authManager.login(username, password);
24940
25078
  authenticatedUser = username;
24941
25079
  const userShared = getUserShared(username);
24942
- handler = new SessionHandler(ws, userShared);
24943
- handlers.set(tabId, handler);
25080
+ handler = attachHandler(userShared);
24944
25081
  clearPreAuthTimer();
24945
25082
  console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
24946
25083
  ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
@@ -24955,8 +25092,7 @@ async function startWebServer(options = {}) {
24955
25092
  }
24956
25093
  authenticatedUser = verifiedUser;
24957
25094
  const userShared = getUserShared(verifiedUser);
24958
- handler = new SessionHandler(ws, userShared);
24959
- handlers.set(tabId, handler);
25095
+ handler = attachHandler(userShared);
24960
25096
  clearPreAuthTimer();
24961
25097
  console.log(` \u2713 Token auth: ${verifiedUser} (tab: ${tabId.slice(0, 12)})`);
24962
25098
  ws.send(JSON.stringify({ type: "auth_result", success: true, username: verifiedUser }));
@@ -24970,8 +25106,7 @@ async function startWebServer(options = {}) {
24970
25106
  }
24971
25107
  authenticatedUser = username;
24972
25108
  const userShared = getUserShared(username);
24973
- handler = new SessionHandler(ws, userShared);
24974
- handlers.set(tabId, handler);
25109
+ handler = attachHandler(userShared);
24975
25110
  clearPreAuthTimer();
24976
25111
  console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
24977
25112
  ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
@@ -24999,10 +25134,11 @@ async function startWebServer(options = {}) {
24999
25134
  }
25000
25135
  });
25001
25136
  ws.on("close", () => {
25137
+ clearInterval(heartbeat);
25002
25138
  clearPreAuthTimer();
25003
- const wasTracked = handlers.has(tabId);
25139
+ const wasTracked = handlers.get(handlerKey) === handler;
25004
25140
  if (handler) handler.onDisconnect();
25005
- if (wasTracked) handlers.delete(tabId);
25141
+ if (wasTracked) handlers.delete(handlerKey);
25006
25142
  console.log(` \u2717 Tab disconnected: ${tabId.slice(0, 12)} (total: ${handlers.size})`);
25007
25143
  });
25008
25144
  ws.on("error", (err) => {