jinzd-ai-cli 0.4.303 → 0.4.305

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-PEH5MTMC.js → batch-OX27K4GG.js} +2 -2
  2. package/dist/{chunk-DHQCJC2B.js → chunk-3ZABGT3S.js} +1 -1
  3. package/dist/{chunk-RWM2GFRC.js → chunk-45UVJXFQ.js} +6 -2
  4. package/dist/{chunk-55AANMEM.js → chunk-4DH5LZR5.js} +1 -1
  5. package/dist/{chunk-23SJVOQD.js → chunk-72AR23R2.js} +26 -5
  6. package/dist/{chunk-6ZRLM3NG.js → chunk-AXKERUXH.js} +3 -3
  7. package/dist/{chunk-VOIOUJXI.js → chunk-C2Z77RTU.js} +13 -5
  8. package/dist/{chunk-ZMCZMR4R.js → chunk-GOIDKTBE.js} +12 -11
  9. package/dist/{chunk-ORDEOJXX.js → chunk-I6RATYRF.js} +1 -1
  10. package/dist/{chunk-WCJR5HCF.js → chunk-IPUTC5EX.js} +30 -12
  11. package/dist/{chunk-7XU4BATV.js → chunk-KBDKKHXB.js} +15 -6
  12. package/dist/{chunk-4DEMGCK4.js → chunk-LF67HWWR.js} +37 -13
  13. package/dist/{chunk-7WD2TBWY.js → chunk-NBYSFFCO.js} +1 -1
  14. package/dist/{chunk-XEKMGE2G.js → chunk-NFLE34QG.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-3GOHR45I.js → chunk-PA4C452B.js} +1 -1
  18. package/dist/{chunk-Q24BN6WF.js → chunk-Q4DAKI7I.js} +1 -1
  19. package/dist/{chunk-EMLRCVY2.js → chunk-SZ4226XY.js} +1 -1
  20. package/dist/{chunk-YK42KYKN.js → chunk-UFK5CTBU.js} +3 -3
  21. package/dist/{chunk-PPIISCFN.js → chunk-UFW4S3RU.js} +37 -13
  22. package/dist/{chunk-CKH4KQ4E.js → chunk-WQ3OMEC4.js} +6 -2
  23. package/dist/{chunk-UUSRWSSX.js → chunk-XJOK5353.js} +15 -6
  24. package/dist/{chunk-PEDN3AWT.js → chunk-YPOWDFOV.js} +1 -1
  25. package/dist/{ci-UXULLQMN.js → ci-IIMLOXFH.js} +4 -4
  26. package/dist/{ci-format-23FWBZN6.js → ci-format-FMBJC3EY.js} +2 -2
  27. package/dist/{constants-A6VKRPMA.js → constants-55QQ3RYE.js} +1 -1
  28. package/dist/{constants-HL7DYRDJ.js → constants-7NVAPQVR.js} +1 -1
  29. package/dist/{doctor-cli-FXBLI3EA.js → doctor-cli-ZHZ2XOF3.js} +4 -4
  30. package/dist/electron-server.js +250 -108
  31. package/dist/{hub-NLULKDKU.js → hub-MF53NIM2.js} +4 -4
  32. package/dist/{hub-server-RF6LURXY.js → hub-server-CBKCP5VD.js} +1 -1
  33. package/dist/index.js +24 -23
  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-NLXNNWOW.js → persistent-memory-LCOKUDTM.js} +2 -2
  37. package/dist/{persistent-memory-R4KLRI5Q.js → persistent-memory-NACQW2VB.js} +2 -2
  38. package/dist/{pr-K6Z73QYN.js → pr-NHWE5FW7.js} +4 -4
  39. package/dist/{run-tests-RYUB6PVF.js → run-tests-EUPJLDXS.js} +2 -2
  40. package/dist/{run-tests-PQ3SUDDN.js → run-tests-ZG5X25BJ.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-QCCH3277.js → server-PG7QCH53.js} +193 -82
  44. package/dist/{server-UJDYIBVR.js → server-PNYRE3AL.js} +9 -9
  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-QAQFB7DG.js → task-orchestrator-WAJTCE6D.js} +9 -9
  48. package/dist/{usage-PMHXXU2P.js → usage-DOES3DOS.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-PEDN3AWT.js";
53
+ } from "./chunk-YPOWDFOV.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-EMLRCVY2.js";
69
+ } from "./chunk-SZ4226XY.js";
70
70
  import {
71
71
  AGENTIC_BEHAVIOR_GUIDELINE,
72
72
  APP_NAME,
@@ -100,12 +100,30 @@ import {
100
100
  SUBAGENT_MAX_ROUNDS_LIMIT,
101
101
  VERSION,
102
102
  buildUserIdentityPrompt
103
- } from "./chunk-3GOHR45I.js";
103
+ } from "./chunk-PA4C452B.js";
104
104
 
105
105
  // src/web/server.ts
106
106
  import express from "express";
107
107
  import { createServer } from "http";
108
108
  import { WebSocketServer } from "ws";
109
+
110
+ // src/web/chat-limits.ts
111
+ var MAX_CHAT_BYTES = 16 * 1024 * 1024;
112
+ var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
113
+ var MAX_IMAGE_BASE64_CHARS = Math.ceil(MAX_IMAGE_BYTES / 3) * 4;
114
+ var IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
115
+ function imageValidationError(images) {
116
+ if (images === void 0) return;
117
+ if (!Array.isArray(images)) return "Images must be an array.";
118
+ for (const image of images) {
119
+ if (!image || typeof image.data !== "string" || !IMAGE_MIMES.has(image.mime)) {
120
+ return "Unsupported image format. Use PNG, JPEG, GIF, WebP or SVG.";
121
+ }
122
+ if (image.data.length > MAX_IMAGE_BASE64_CHARS) return "Image too large (maximum 10 MiB before base64 encoding).";
123
+ }
124
+ }
125
+
126
+ // src/web/server.ts
109
127
  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
128
  import { existsSync as existsSync32, readFileSync as readFileSync25, readdirSync as readdirSync15, statSync as statSync16, realpathSync as realpathSync3, createReadStream } from "fs";
111
129
  import { networkInterfaces } from "os";
@@ -6448,6 +6466,11 @@ function buildErrorHint(command, stderr) {
6448
6466
  `Hint: On Windows, "-ErrorAction SilentlyContinue" silences the error MESSAGE but does NOT change the exit code \u2014 the process still exits 1 when the path doesn't exist (because powershell.exe -NonInteractive flips the exit code on any non-terminating error, silenced or not). If you're just probing for existence, use Test-Path instead \u2014 it returns $true/$false without raising: \`if (Test-Path ./mydir) { Get-ChildItem ./mydir }\` or unconditionally use \`New-Item -ItemType Directory -Force -Path ./mydir\` which is idempotent. Don't retry the same -ErrorAction SilentlyContinue probe expecting a different result.`
6449
6467
  );
6450
6468
  }
6469
+ if (IS_WINDOWS3 && /Select-Object\s+(?:[^|]*\s)?-(?:First|Last)\s+\d+/i.test(command) && /\|/.test(command)) {
6470
+ hints.push(
6471
+ `Hint: "Select-Object -First N" stops the upstream pipeline as soon as it has N items (PipelineStoppedException), and PowerShell reports that as a FAILURE even when the upstream command did its job \u2014 check the output above: if it shows the work completed (e.g. "EXPORTED 25 slide(s)"), this is a false failure and you should NOT retry. To avoid it, capture first and slice after: \`$out = <command>; $out | Select-Object -First N\`, or drop the -First and let the full output through.`
6472
+ );
6473
+ }
6451
6474
  if (IS_WINDOWS3 && /\bWrite-(?:Content|File)\b/i.test(command) && /(not recognized|无法将.*识别|不是.*cmdlet|CommandNotFoundException)/i.test(stderr)) {
6452
6475
  hints.push(
6453
6476
  `Hint: 'Write-Content' / 'Write-File' are NOT PowerShell cmdlets (common LLM hallucination). To write a file, prefer the built-in 'write_file' tool \u2014 it handles encoding/escaping cleanly and supports diff preview. If you must stay in bash: use 'Set-Content -Path <file> -Value <string> -Encoding utf8' (overwrites) or 'Add-Content' (appends) or 'Out-File -FilePath <file> -Encoding utf8'.`
@@ -7811,26 +7834,11 @@ function checkNetworkPolicy(toolName, args, policy) {
7811
7834
  // src/tools/truncate.ts
7812
7835
  var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
7813
7836
  var activeCap = DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP;
7814
- function setMaxOutputCap(cap) {
7815
- if (!cap || cap <= 0) {
7816
- activeCap = DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP;
7817
- } else {
7818
- activeCap = Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, cap);
7819
- }
7820
- if (lastContextWindow > 0) {
7821
- activeMaxChars = getMaxOutputChars(lastContextWindow);
7822
- }
7823
- }
7824
- function getMaxOutputChars(contextWindow) {
7837
+ function getMaxOutputChars(contextWindow, cap = activeCap) {
7825
7838
  if (!contextWindow || contextWindow <= 0) return DEFAULT_MAX_TOOL_OUTPUT_CHARS;
7826
- return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), activeCap));
7839
+ return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), cap > 0 ? cap : DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP));
7827
7840
  }
7828
7841
  var activeMaxChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
7829
- var lastContextWindow = 0;
7830
- function setContextWindow(contextWindow) {
7831
- lastContextWindow = contextWindow;
7832
- activeMaxChars = getMaxOutputChars(contextWindow);
7833
- }
7834
7842
  function getActiveMaxChars() {
7835
7843
  return activeMaxChars;
7836
7844
  }
@@ -8228,7 +8236,12 @@ function buildErrorPreview(content) {
8228
8236
  undecorated.push(raw[i]);
8229
8237
  }
8230
8238
  const meaningful = undecorated.filter(
8231
- (l) => !/^(?:\[[^\]]+\]\s*)?Exit code \d+:$/.test(l.trim()) && !/^Traceback \(most recent call last\):$/.test(l.trim()) && !l.startsWith("[Command failed") && !l.startsWith("\u{1F4A1} ") && l.trim() !== ""
8239
+ (l) => !/^(?:\[[^\]]+\]\s*)?Exit code \d+:$/.test(l.trim()) && // `formatFailureHeader()` 在拿不到退出码时打的那句 —— 与 "Exit code N:" 同类噪声。
8240
+ // 不过滤的话它会占掉第一个展示位:2026-09-05 实战里 `export_slides.ps1 | Select-Object
8241
+ // -First 3` 提前掐断管道被 PowerShell 报成失败,预览成了
8242
+ // 「Command failed before exit code could be determined: · …\slide-02.png」——
8243
+ // 两段都没用,而真正有信息的 `EXPORTED 25 slide(s)` 就在第二行、从没上屏。
8244
+ !/^(?:\[[^\]]+\]\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() !== ""
8232
8245
  );
8233
8246
  let primary = -1;
8234
8247
  for (let i = meaningful.length - 1; i >= 0; i--) {
@@ -8729,7 +8742,7 @@ var ToolExecutor = class {
8729
8742
  ... (${lines.length - maxLines} more lines)`) : "";
8730
8743
  const truncatedNote = wasTruncated ? theme.warning(`
8731
8744
  \u26A1 Output truncated to ${getActiveMaxChars()} chars before sending to AI`) : "";
8732
- console.log(theme.toolResult("\u2713 Result: ") + theme.dim(preview) + moreLines + truncatedNote);
8745
+ console.log(theme.toolResult(`\u2713 ${name} result: `) + theme.dim(preview) + moreLines + truncatedNote);
8733
8746
  }
8734
8747
  console.log();
8735
8748
  }
@@ -8955,7 +8968,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
8955
8968
  const mode = appendMode ? "appended" : "written";
8956
8969
  void (async () => {
8957
8970
  try {
8958
- const { updateFile } = await import("./indexer-RBEKMFT4.js");
8971
+ const { updateFile } = await import("./indexer-FPKY7LCT.js");
8959
8972
  await updateFile(process.cwd(), filePath);
8960
8973
  } catch {
8961
8974
  }
@@ -9382,6 +9395,9 @@ Note: Path can be absolute or relative to cwd.`,
9382
9395
  fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
9383
9396
  atomicWriteFileSync(filePath, res.content);
9384
9397
  }
9398
+ if (!res.ok) {
9399
+ throw new ToolError("edit_file", lines.join("\n"));
9400
+ }
9385
9401
  return lines.join("\n");
9386
9402
  }
9387
9403
  if (args["edits"] !== void 0) {
@@ -9434,6 +9450,9 @@ Note: Path can be absolute or relative to cwd.`,
9434
9450
  lines.push(` \u2717 #${r.index + 1}: ${r.error}`);
9435
9451
  }
9436
9452
  }
9453
+ if (anyFailed && stopOnError) {
9454
+ throw new ToolError("edit_file", lines.join("\n"));
9455
+ }
9437
9456
  return lines.join("\n");
9438
9457
  }
9439
9458
  const replaceArgs = normalizeReplaceArgs(args);
@@ -14080,6 +14099,27 @@ function suggestSkillName(names, input) {
14080
14099
  // src/web/tool-executor-web.ts
14081
14100
  import { randomUUID as randomUUID2 } from "crypto";
14082
14101
  import { tmpdir as tmpdir2 } from "os";
14102
+
14103
+ // src/web/socket-send.ts
14104
+ var MAX_PENDING_OUTPUT_BYTES = 16 * 1024 * 1024;
14105
+ var stalled = /* @__PURE__ */ new WeakSet();
14106
+ function sendSocketMessage(ws, message) {
14107
+ if (ws.readyState !== ws.OPEN || stalled.has(ws)) return false;
14108
+ const data = JSON.stringify(message);
14109
+ if ((ws.bufferedAmount || 0) + Buffer.byteLength(data) > MAX_PENDING_OUTPUT_BYTES) {
14110
+ stalled.add(ws);
14111
+ ws.close(1013, "Output backlog exceeded; reconnect to reload session");
14112
+ const timer = setTimeout(() => {
14113
+ if (ws.readyState !== ws.CLOSED) ws.terminate();
14114
+ }, 1e3);
14115
+ timer.unref();
14116
+ return false;
14117
+ }
14118
+ ws.send(data);
14119
+ return true;
14120
+ }
14121
+
14122
+ // src/web/tool-executor-web.ts
14083
14123
  import { existsSync as existsSync22, readFileSync as readFileSync17 } from "fs";
14084
14124
  var ToolExecutorWeb = class _ToolExecutorWeb {
14085
14125
  constructor(registry, ws) {
@@ -14109,6 +14149,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14109
14149
  // 5 minutes
14110
14150
  /** Publicly readable by SessionHandler to check if confirm is active */
14111
14151
  confirming = false;
14152
+ maxOutputChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
14112
14153
  /** Session-level auto-approve toggle (/yolo command) */
14113
14154
  sessionAutoApprove = false;
14114
14155
  /** Session-level Auto Mode: rule-based low-risk auto approval. */
@@ -14191,9 +14232,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14191
14232
  this.confirming = false;
14192
14233
  }
14193
14234
  send(msg) {
14194
- if (this.ws.readyState === this.ws.OPEN) {
14195
- this.ws.send(JSON.stringify(msg));
14196
- }
14235
+ sendSocketMessage(this.ws, msg);
14197
14236
  }
14198
14237
  sendToolCallStart(call) {
14199
14238
  const dangerLevel = getDangerLevel(call.name, call.arguments);
@@ -14386,7 +14425,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14386
14425
  if (!toolCallStarted) this.sendToolCallStart(call);
14387
14426
  try {
14388
14427
  const rawContent = await runTool(tool, call.arguments, call.name);
14389
- const content = truncateOutput(rawContent, call.name);
14428
+ const content = truncateOutput(rawContent, call.name, this.maxOutputChars);
14390
14429
  this.sendToolCallResult(call, rawContent, false);
14391
14430
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
14392
14431
  runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
@@ -14451,7 +14490,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14451
14490
  }
14452
14491
  try {
14453
14492
  const rawContent = await runTool(tool, call.arguments, call.name);
14454
- const content = truncateOutput(rawContent, call.name);
14493
+ const content = truncateOutput(rawContent, call.name, this.maxOutputChars);
14455
14494
  this.sendToolCallResult(call, rawContent, false);
14456
14495
  results[i] = { callId: call.id, content, isError: false };
14457
14496
  } catch (err) {
@@ -15400,6 +15439,7 @@ var HubAgent = class {
15400
15439
  model: this.modelId,
15401
15440
  systemPrompt,
15402
15441
  stream: false,
15442
+ signal: opts?.signal,
15403
15443
  temperature: 0.7,
15404
15444
  maxTokens: 4096,
15405
15445
  ...this.thinking ?? {}
@@ -15434,15 +15474,21 @@ var HubAgent = class {
15434
15474
  model: this.modelId,
15435
15475
  systemPrompt,
15436
15476
  stream: true,
15477
+ signal: opts?.signal,
15437
15478
  temperature: 0.7,
15438
15479
  maxTokens: 4096,
15439
15480
  ...this.thinking ?? {}
15440
15481
  });
15441
- for await (const chunk of stream) {
15442
- if (chunk.delta) {
15443
- raw += chunk.delta;
15444
- onToken?.(chunk.delta);
15482
+ try {
15483
+ for await (const chunk of stream) {
15484
+ if (chunk.delta) {
15485
+ raw += chunk.delta;
15486
+ onToken?.(chunk.delta);
15487
+ }
15488
+ if (opts?.signal?.aborted) break;
15445
15489
  }
15490
+ } catch (error) {
15491
+ if (!opts?.signal?.aborted) throw error;
15446
15492
  }
15447
15493
  const { content, passed, converged } = parseTurn(raw);
15448
15494
  return {
@@ -15457,7 +15503,7 @@ var HubAgent = class {
15457
15503
  /**
15458
15504
  * Generate a summary of the entire discussion from this agent's perspective.
15459
15505
  */
15460
- async summarize(topic, history) {
15506
+ async summarize(topic, history, signal) {
15461
15507
  const provider = this.providers.get(this.providerId);
15462
15508
  if (!provider) {
15463
15509
  throw new Error(`Provider "${this.providerId}" not available`);
@@ -15474,6 +15520,7 @@ var HubAgent = class {
15474
15520
  model: this.modelId,
15475
15521
  stream: false,
15476
15522
  temperature: 0.3,
15523
+ signal,
15477
15524
  maxTokens: 4096,
15478
15525
  ...this.thinking ?? {}
15479
15526
  });
@@ -15567,6 +15614,8 @@ var DiscussionOrchestrator = class {
15567
15614
  agents = [];
15568
15615
  state;
15569
15616
  aborted = false;
15617
+ controller = new AbortController();
15618
+ cancelReview;
15570
15619
  humanSteer;
15571
15620
  voteConverge;
15572
15621
  /** Callback for rendering events */
@@ -15606,6 +15655,8 @@ var DiscussionOrchestrator = class {
15606
15655
  /** Signal the orchestrator to stop after current turn */
15607
15656
  abort() {
15608
15657
  this.aborted = true;
15658
+ this.controller.abort();
15659
+ this.cancelReview?.();
15609
15660
  }
15610
15661
  /**
15611
15662
  * Run the full discussion on a given topic.
@@ -15616,6 +15667,7 @@ var DiscussionOrchestrator = class {
15616
15667
  this.state.round = 0;
15617
15668
  this.state.finished = false;
15618
15669
  this.aborted = false;
15670
+ this.controller = new AbortController();
15619
15671
  try {
15620
15672
  for (let round = 1; round <= this.state.maxRounds; round++) {
15621
15673
  if (this.aborted) {
@@ -15636,7 +15688,7 @@ var DiscussionOrchestrator = class {
15636
15688
  round,
15637
15689
  this.state.maxRounds,
15638
15690
  (token) => this.emit({ type: "agent_token", roleId: agent.role.id, token }),
15639
- { voteConverge: this.voteConverge }
15691
+ { voteConverge: this.voteConverge, signal: this.controller.signal }
15640
15692
  );
15641
15693
  this.state.messages.push(message);
15642
15694
  if (message.converged) convergedCount++;
@@ -15648,6 +15700,7 @@ var DiscussionOrchestrator = class {
15648
15700
  this.emit({ type: "agent_spoke", roleId: agent.role.id, message });
15649
15701
  }
15650
15702
  } catch (err) {
15703
+ if (this.aborted) break;
15651
15704
  const errMsg = err instanceof Error ? err.message : String(err);
15652
15705
  this.emit({ type: "error", roleId: agent.role.id, message: errMsg });
15653
15706
  this.state.messages.push({
@@ -15659,6 +15712,10 @@ var DiscussionOrchestrator = class {
15659
15712
  }
15660
15713
  }
15661
15714
  this.emit({ type: "round_end", round });
15715
+ if (this.aborted) {
15716
+ this.emit({ type: "discussion_end", reason: "user_interrupt" });
15717
+ break;
15718
+ }
15662
15719
  if (allPassed && round > 1) {
15663
15720
  this.emit({ type: "discussion_end", reason: "consensus" });
15664
15721
  break;
@@ -15675,7 +15732,18 @@ var DiscussionOrchestrator = class {
15675
15732
  break;
15676
15733
  }
15677
15734
  if (this.humanSteer && this.onRoundReview && !this.aborted) {
15678
- const steer = await this.onRoundReview({ round, maxRounds: this.state.maxRounds, state: this.state });
15735
+ const stopped = new Promise((resolve13) => {
15736
+ this.cancelReview = () => resolve13({ action: "stop" });
15737
+ });
15738
+ let steer;
15739
+ try {
15740
+ steer = await Promise.race([
15741
+ this.onRoundReview({ round, maxRounds: this.state.maxRounds, state: this.state }),
15742
+ stopped
15743
+ ]);
15744
+ } finally {
15745
+ this.cancelReview = void 0;
15746
+ }
15679
15747
  if (steer.action === "stop") {
15680
15748
  this.emit({ type: "discussion_end", reason: "human_stop" });
15681
15749
  break;
@@ -15696,7 +15764,7 @@ var DiscussionOrchestrator = class {
15696
15764
  const errMsg = err instanceof Error ? err.message : String(err);
15697
15765
  this.emit({ type: "error", message: errMsg });
15698
15766
  }
15699
- await this.generateSummary();
15767
+ if (!this.aborted) await this.generateSummary();
15700
15768
  this.state.finished = true;
15701
15769
  return this.state;
15702
15770
  }
@@ -15714,7 +15782,7 @@ var DiscussionOrchestrator = class {
15714
15782
  return;
15715
15783
  }
15716
15784
  try {
15717
- this.state.summary = await summarizer.summarize(this.state.topic, this.state.messages);
15785
+ this.state.summary = await summarizer.summarize(this.state.topic, this.state.messages, this.controller.signal);
15718
15786
  this.emit({ type: "summary", content: this.state.summary });
15719
15787
  } catch (err) {
15720
15788
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -16190,11 +16258,11 @@ function routeDecisionRows(message, currentModel, decision) {
16190
16258
  }
16191
16259
 
16192
16260
  // src/web/commands/provider-commands.ts
16193
- function syncSessionProvider(ctx) {
16261
+ async function syncSessionProvider(ctx) {
16194
16262
  const session = ctx.sessions.current;
16195
16263
  if (!session) return;
16196
16264
  session.updateProvider(ctx.currentProvider, ctx.currentModel);
16197
- ctx.saveIfNeeded();
16265
+ await ctx.saveIfNeeded();
16198
16266
  }
16199
16267
  async function handleProvider(args, ctx) {
16200
16268
  const id = args[0];
@@ -16237,7 +16305,7 @@ async function handleProvider(args, ctx) {
16237
16305
  ctx.currentProvider = outcome.providerId;
16238
16306
  ctx.currentModel = outcome.model;
16239
16307
  ctx.updateContextWindow();
16240
- syncSessionProvider(ctx);
16308
+ await syncSessionProvider(ctx);
16241
16309
  ctx.send({ type: "info", message: outcome.message });
16242
16310
  ctx.sendStatus();
16243
16311
  }
@@ -16291,7 +16359,7 @@ async function handleModel(args, ctx) {
16291
16359
  }
16292
16360
  ctx.currentModel = outcome.model;
16293
16361
  ctx.updateContextWindow();
16294
- syncSessionProvider(ctx);
16362
+ await syncSessionProvider(ctx);
16295
16363
  ctx.send({
16296
16364
  type: "info",
16297
16365
  message: outcome.warning ? `${outcome.message} \u2014 ${outcome.warning}` : outcome.message
@@ -17176,7 +17244,7 @@ async function handleClear(_args, ctx) {
17176
17244
  const prior = ctx.sessions.current;
17177
17245
  const priorId = prior?.id ?? null;
17178
17246
  const priorCount = prior?.messages.length ?? 0;
17179
- ctx.saveIfNeeded();
17247
+ await ctx.saveIfNeeded();
17180
17248
  const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
17181
17249
  ctx.unsavedSessions.set(created.id, created);
17182
17250
  ctx.resetWebSessionUsage();
@@ -17198,7 +17266,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
17198
17266
  Cost: ${formatCost(cost)}` : "";
17199
17267
  let memoryLine = "";
17200
17268
  try {
17201
- const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-NLXNNWOW.js");
17269
+ const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-LCOKUDTM.js");
17202
17270
  const pending = countPendingMemories2(ctx.config.getConfigDir());
17203
17271
  if (pending > 0) memoryLine = `
17204
17272
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -17214,8 +17282,13 @@ Tokens: in=${ctx.sessionTokenUsage.inputTokens} out=${ctx.sessionTokenUsage.outp
17214
17282
  }
17215
17283
  async function handleSession(args, ctx) {
17216
17284
  const sub = args[0];
17285
+ if (sub === "save") {
17286
+ await ctx.saveIfNeeded();
17287
+ if (!ctx.sessions.current?.dirty) ctx.send({ type: "info", message: "Session saved." });
17288
+ return;
17289
+ }
17217
17290
  if (sub === "new") {
17218
- ctx.saveIfNeeded();
17291
+ await ctx.saveIfNeeded();
17219
17292
  const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
17220
17293
  ctx.unsavedSessions.set(created.id, created);
17221
17294
  ctx.resetWebSessionUsage();
@@ -17226,7 +17299,7 @@ async function handleSession(args, ctx) {
17226
17299
  }
17227
17300
  if (sub === "load" && args[1]) {
17228
17301
  const targetId = args[1];
17229
- ctx.saveIfNeeded();
17302
+ await ctx.saveIfNeeded();
17230
17303
  const cachedExact = ctx.unsavedSessions.get(targetId);
17231
17304
  const cached = cachedExact ?? [...ctx.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
17232
17305
  if (cached) {
@@ -18181,7 +18254,7 @@ async function handleTest(args, ctx) {
18181
18254
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
18182
18255
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
18183
18256
  }
18184
- const runTests = ctx.runTests ?? (await import("./run-tests-RYUB6PVF.js")).executeTests;
18257
+ const runTests = ctx.runTests ?? (await import("./run-tests-EUPJLDXS.js")).executeTests;
18185
18258
  const report = await runTests(testArgs);
18186
18259
  ctx.send({ type: "info", message: report });
18187
18260
  } catch (err) {
@@ -18473,9 +18546,9 @@ async function handleContext(_args, ctx) {
18473
18546
  async function handleIndex(args, ctx) {
18474
18547
  const sub = (args[0] ?? "status").toLowerCase();
18475
18548
  const root = process.cwd();
18476
- const { loadIndex: loadIndex2, clearIndex } = await import("./store-VO37H6LS.js");
18477
- const { indexProject: indexProject2 } = await import("./indexer-RBEKMFT4.js");
18478
- const { loadVectorStore, clearVectorStore } = await import("./vector-store-JBAE6PS4.js");
18549
+ const { loadIndex: loadIndex2, clearIndex } = await import("./store-SIKHCUYJ.js");
18550
+ const { indexProject: indexProject2 } = await import("./indexer-FPKY7LCT.js");
18551
+ const { loadVectorStore, clearVectorStore } = await import("./vector-store-JAXWDZYW.js");
18479
18552
  if (sub === "status") {
18480
18553
  const idx = loadIndex2(root);
18481
18554
  const vec = loadVectorStore(root);
@@ -18498,6 +18571,7 @@ async function handleIndex(args, ctx) {
18498
18571
  } else if (sub === "rebuild") {
18499
18572
  ctx.send({ type: "info", message: `Indexing ${root}\u2026` });
18500
18573
  const { stats } = await indexProject2(root, { force: true });
18574
+ if (stats.incomplete) ctx.send({ type: "info", message: "Partial index: scan limit or unreadable files reached. Unscanned entries were retained." });
18501
18575
  ctx.send({
18502
18576
  type: "info",
18503
18577
  message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
@@ -18524,7 +18598,7 @@ async function handleIndex(args, ctx) {
18524
18598
  message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
18525
18599
  });
18526
18600
  try {
18527
- const { rebuildSemanticIndex } = await import("./semantic-VPNKXKKK.js");
18601
+ const { rebuildSemanticIndex } = await import("./semantic-QOBAXEQ4.js");
18528
18602
  const stats = await rebuildSemanticIndex(root);
18529
18603
  const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
18530
18604
  ctx.send({
@@ -18571,7 +18645,7 @@ async function handleMemory(args, ctx) {
18571
18645
  }
18572
18646
  if (intent.kind === "status") {
18573
18647
  const { getChatIndexStatus: getChatIndexStatus2 } = await import("./chat-index-DGUOUUQW.js");
18574
- const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-NLXNNWOW.js");
18648
+ const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-LCOKUDTM.js");
18575
18649
  const memories = listMemoryEntries2(configDir, { includeExpired: true, includeRejected: true });
18576
18650
  const injected = formatMemoryForPrompt2(configDir, process.cwd(), ctx.config.get("memory")?.maxChars);
18577
18651
  const view = buildMemoryStatusView({
@@ -18594,7 +18668,7 @@ async function handleMemory(args, ctx) {
18594
18668
  return;
18595
18669
  }
18596
18670
  if (intent.kind === "recall") {
18597
- const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-NLXNNWOW.js");
18671
+ const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-LCOKUDTM.js");
18598
18672
  const { searchChatMemory: searchChatMemory2 } = await import("./chat-index-DGUOUUQW.js");
18599
18673
  const persistentHits = searchPersistentMemories2(configDir, intent.query, process.cwd(), 3);
18600
18674
  if (persistentHits.length > 0) {
@@ -18715,7 +18789,7 @@ async function handleMemory(args, ctx) {
18715
18789
  ctx.handleMemoryManage(intent.action, args[1], intent.action === "expire" ? args[2] : void 0);
18716
18790
  } else if (intent.kind === "export") {
18717
18791
  if (args[1] === "md") {
18718
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-NLXNNWOW.js");
18792
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-LCOKUDTM.js");
18719
18793
  ctx.send({
18720
18794
  type: "export_data",
18721
18795
  format: "md",
@@ -19720,7 +19794,7 @@ No tools match "${filter}".
19720
19794
  const { join: join29 } = await import("path");
19721
19795
  const { existsSync: existsSync33 } = await import("fs");
19722
19796
  const { getGitRoot: getGitRoot2 } = await import("./git-context-VNCA2GU7.js");
19723
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-A6VKRPMA.js");
19797
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-55QQ3RYE.js");
19724
19798
  const { approveProject, hashMcpFile } = await import("./project-trust-MUG325AW.js");
19725
19799
  const cwd = process.cwd();
19726
19800
  const projectRoot = getGitRoot2(cwd) ?? cwd;
@@ -20639,7 +20713,7 @@ function createReviewCommands() {
20639
20713
  usage: "/test [command|filter]",
20640
20714
  async execute(args, ctx) {
20641
20715
  try {
20642
- const { executeTests } = await import("./run-tests-RYUB6PVF.js");
20716
+ const { executeTests } = await import("./run-tests-EUPJLDXS.js");
20643
20717
  const argStr = args.join(" ").trim();
20644
20718
  let testArgs = {};
20645
20719
  if (argStr) {
@@ -20890,9 +20964,9 @@ function createMemoryCommands() {
20890
20964
  async execute(args, ctx) {
20891
20965
  const sub = (args[0] ?? "status").toLowerCase();
20892
20966
  const root = process.cwd();
20893
- const { loadIndex: loadIndex2, clearIndex } = await import("./store-VO37H6LS.js");
20894
- const { indexProject: indexProject2 } = await import("./indexer-RBEKMFT4.js");
20895
- const { loadVectorStore, clearVectorStore } = await import("./vector-store-JBAE6PS4.js");
20967
+ const { loadIndex: loadIndex2, clearIndex } = await import("./store-SIKHCUYJ.js");
20968
+ const { indexProject: indexProject2 } = await import("./indexer-FPKY7LCT.js");
20969
+ const { loadVectorStore, clearVectorStore } = await import("./vector-store-JAXWDZYW.js");
20896
20970
  if (sub === "status") {
20897
20971
  const idx = loadIndex2(root);
20898
20972
  const vec = loadVectorStore(root);
@@ -20923,6 +20997,7 @@ function createMemoryCommands() {
20923
20997
  }
20924
20998
  });
20925
20999
  process.stdout.write("\r\x1B[K");
21000
+ if (stats.incomplete) console.log(theme.dim(" Partial index: scan limit or unreadable files reached; unscanned entries retained."));
20926
21001
  console.log(theme.success(
20927
21002
  ` \u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
20928
21003
  ));
@@ -20951,7 +21026,7 @@ function createMemoryCommands() {
20951
21026
  }
20952
21027
  console.log(theme.dim(` Building semantic index for ${idx.symbolCount} symbols\u2026`));
20953
21028
  console.log(theme.dim(" (First run downloads ~117 MB embedding model to ~/.aicli/models/)"));
20954
- const { rebuildSemanticIndex } = await import("./semantic-VPNKXKKK.js");
21029
+ const { rebuildSemanticIndex } = await import("./semantic-QOBAXEQ4.js");
20955
21030
  try {
20956
21031
  const stats = await rebuildSemanticIndex(root, {
20957
21032
  onProgress: (done, total) => {
@@ -22550,7 +22625,7 @@ var webCommandHandlers = {
22550
22625
  search: handleSearch,
22551
22626
  undo: handleUndo
22552
22627
  };
22553
- var SessionHandler = class {
22628
+ var SessionHandler = class _SessionHandler {
22554
22629
  ws;
22555
22630
  config;
22556
22631
  costTracker;
@@ -22601,6 +22676,7 @@ var SessionHandler = class {
22601
22676
  abortController = null;
22602
22677
  userInterjection = null;
22603
22678
  processing = false;
22679
+ disconnected = false;
22604
22680
  /** P4: active hub discussion orchestrator (web room), if any. */
22605
22681
  hubOrchestrator = null;
22606
22682
  /** P4b: pending between-rounds human-steer prompts (requestId → resolver). */
@@ -22627,9 +22703,13 @@ var SessionHandler = class {
22627
22703
  * 会话一旦保存到磁盘(有消息后),会从此 Map 中移除。
22628
22704
  */
22629
22705
  unsavedSessions = /* @__PURE__ */ new Map();
22706
+ // Failed saves survive a socket reconnect, scoped to the owning config directory.
22707
+ static recovery = /* @__PURE__ */ new Map();
22708
+ static acceptedChats = /* @__PURE__ */ new Map();
22630
22709
  constructor(ws, shared) {
22631
22710
  this.ws = ws;
22632
22711
  this.config = shared.config;
22712
+ this.unsavedSessions = new Map(_SessionHandler.recovery.get(this.config.getConfigDir()));
22633
22713
  this.costTracker = shared.costTracker;
22634
22714
  this.providers = shared.providers;
22635
22715
  this.sessions = shared.sessions;
@@ -22697,9 +22777,7 @@ var SessionHandler = class {
22697
22777
  * 接口之间的任何漂移(少一个成员、签名改了)都会当场编译失败,而不是等到运行时。
22698
22778
  */
22699
22779
  send(msg) {
22700
- if (this.ws.readyState === this.ws.OPEN) {
22701
- this.ws.send(JSON.stringify(msg));
22702
- }
22780
+ sendSocketMessage(this.ws, msg);
22703
22781
  }
22704
22782
  sendStatus() {
22705
22783
  const providerList = this.providers.listAvailable().map((p) => ({
@@ -22753,6 +22831,7 @@ var SessionHandler = class {
22753
22831
  });
22754
22832
  }
22755
22833
  async handleMessage(raw) {
22834
+ if (this.disconnected) return;
22756
22835
  let msg;
22757
22836
  try {
22758
22837
  msg = JSON.parse(raw);
@@ -22761,8 +22840,33 @@ var SessionHandler = class {
22761
22840
  return;
22762
22841
  }
22763
22842
  switch (msg.type) {
22764
- case "chat":
22843
+ case "chat": {
22844
+ if (msg.messageId) {
22845
+ if (typeof msg.messageId !== "string" || msg.messageId.length > 200) throw new Error("Invalid message ID");
22846
+ const scope = this.config.getConfigDir();
22847
+ const accepted = _SessionHandler.acceptedChats.get(scope) ?? /* @__PURE__ */ new Map();
22848
+ const prior = accepted.get(msg.messageId);
22849
+ if (prior) {
22850
+ this.send({ type: "chat_ack", messageId: msg.messageId, accepted: true, duplicate: true, sessionId: prior });
22851
+ return;
22852
+ }
22853
+ if (this.processing || typeof msg.content !== "string") {
22854
+ 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." });
22855
+ return;
22856
+ }
22857
+ const imageError = imageValidationError(msg.images);
22858
+ if (imageError) {
22859
+ this.send({ type: "chat_ack", messageId: msg.messageId, accepted: false, error: imageError });
22860
+ return;
22861
+ }
22862
+ this.ensureSession();
22863
+ accepted.set(msg.messageId, this.sessions.current.id);
22864
+ while (accepted.size > 1e3) accepted.delete(accepted.keys().next().value);
22865
+ _SessionHandler.acceptedChats.set(scope, accepted);
22866
+ this.send({ type: "chat_ack", messageId: msg.messageId, accepted: true, sessionId: this.sessions.current.id });
22867
+ }
22765
22868
  return this.handleChat(msg.content, msg.images);
22869
+ }
22766
22870
  case "command":
22767
22871
  return this.handleCommand(msg.name, msg.args);
22768
22872
  case "confirm_response":
@@ -22804,7 +22908,7 @@ var SessionHandler = class {
22804
22908
  case "hub_start":
22805
22909
  return this.handleHubStart(msg);
22806
22910
  case "hub_abort":
22807
- this.hubOrchestrator?.abort();
22911
+ this.cancelHub();
22808
22912
  return;
22809
22913
  case "hub_steer": {
22810
22914
  const resolve13 = this.pendingHubReview.get(msg.requestId);
@@ -22819,7 +22923,10 @@ var SessionHandler = class {
22819
22923
  return;
22820
22924
  }
22821
22925
  }
22822
- onDisconnect() {
22926
+ onDisconnect(closeSocket = true) {
22927
+ if (this.disconnected) return;
22928
+ this.disconnected = true;
22929
+ if (closeSocket && this.ws.readyState === this.ws.OPEN) this.ws.close(1001, "Connection closed; reconnect to restore session");
22823
22930
  runLifecycleHooks(mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()), "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
22824
22931
  this.toolExecutor.cancelAll();
22825
22932
  if (this.abortController) this.abortController.abort();
@@ -22827,18 +22934,24 @@ var SessionHandler = class {
22827
22934
  this.pendingAskUser.clear();
22828
22935
  for (const resolve13 of this.pendingAutoPause.values()) resolve13({ action: "stop" });
22829
22936
  this.pendingAutoPause.clear();
22830
- this.saveIfNeeded();
22937
+ this.cancelHub();
22938
+ void this.saveIfNeeded();
22831
22939
  if (this._bashSessionKey) clearBashSession(this._bashSessionKey);
22832
22940
  }
22941
+ cancelHub() {
22942
+ this.hubOrchestrator?.abort();
22943
+ for (const resolve13 of this.pendingHubReview.values()) resolve13({ action: "stop" });
22944
+ this.pendingHubReview.clear();
22945
+ }
22833
22946
  /** 根据当前模型 context window 更新工具输出截断上限 */
22834
22947
  updateContextWindow() {
22835
22948
  try {
22836
22949
  const cfgOutputCap = this.config.get("maxToolOutputChars");
22837
- if (typeof cfgOutputCap === "number" && cfgOutputCap > 0) setMaxOutputCap(cfgOutputCap);
22838
22950
  const provider = this.providers.get(this.currentProvider);
22839
22951
  const modelInfo = provider?.info.models.find((m) => m.id === this.currentModel);
22840
- if (modelInfo?.contextWindow) setContextWindow(modelInfo.contextWindow);
22952
+ this.toolExecutor.maxOutputChars = getMaxOutputChars(modelInfo?.contextWindow, cfgOutputCap ?? 0);
22841
22953
  } catch {
22954
+ this.toolExecutor.maxOutputChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
22842
22955
  }
22843
22956
  }
22844
22957
  /** 获取当前模型的 context window 大小(0 表示未知)*/
@@ -22890,12 +23003,24 @@ var SessionHandler = class {
22890
23003
  return total;
22891
23004
  }
22892
23005
  /** Save session only if it exists and has messages (never persist empty "Untitled" sessions). */
22893
- saveIfNeeded() {
22894
- if (this.sessions.current && this.sessions.current.messages.length > 0) {
22895
- autoTrimSessionIfNeeded(this.sessions.current);
22896
- const id = this.sessions.current.id;
22897
- this.sessions.save();
22898
- this.unsavedSessions.delete(id);
23006
+ async saveIfNeeded() {
23007
+ const session = this.sessions.current;
23008
+ if (!session?.messages.length) return;
23009
+ const scope = this.config.getConfigDir();
23010
+ try {
23011
+ if (session.dirty) autoTrimSessionIfNeeded(session);
23012
+ await this.sessions.save();
23013
+ if (session.dirty) return;
23014
+ this.unsavedSessions.delete(session.id);
23015
+ _SessionHandler.recovery.get(scope)?.delete(session.id);
23016
+ } catch (err) {
23017
+ this.unsavedSessions.set(session.id, session);
23018
+ const recovered = _SessionHandler.recovery.get(scope) ?? /* @__PURE__ */ new Map();
23019
+ recovered.set(session.id, session);
23020
+ _SessionHandler.recovery.set(scope, recovered);
23021
+ 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.`;
23022
+ this.send({ type: "info", message });
23023
+ process.stderr.write(message + "\n");
22899
23024
  }
22900
23025
  }
22901
23026
  /** Lazily create a session if none exists yet (deferred from constructor). */
@@ -23020,20 +23145,15 @@ var SessionHandler = class {
23020
23145
  const kinds = [...new Set(secretHits.map((h) => h.kind))].join(", ");
23021
23146
  this.send({ type: "info", message: "Possible secret in prompt (" + kinds + ")." });
23022
23147
  }
23023
- const ALLOWED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
23024
- const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
23148
+ const imageError = imageValidationError(images);
23149
+ if (imageError) {
23150
+ this.send({ type: "error", message: imageError });
23151
+ return;
23152
+ }
23025
23153
  let msgContent;
23026
23154
  if (images && images.length > 0) {
23027
23155
  const parts = [];
23028
23156
  for (const img of images) {
23029
- if (!ALLOWED_IMAGE_MIMES.has(img.mime)) {
23030
- this.send({ type: "error", message: `Rejected image: unsupported MIME type "${img.mime}"` });
23031
- return;
23032
- }
23033
- if (img.data && img.data.length > MAX_IMAGE_SIZE) {
23034
- this.send({ type: "error", message: `Rejected image: too large (${(img.data.length / 1024 / 1024).toFixed(1)} MB, max 10 MB)` });
23035
- return;
23036
- }
23037
23157
  parts.push({
23038
23158
  type: "image_url",
23039
23159
  image_url: { url: `data:${img.mime};base64,${img.data}` }
@@ -23087,7 +23207,7 @@ var SessionHandler = class {
23087
23207
  this.send({ type: "error", message });
23088
23208
  } finally {
23089
23209
  this.processing = false;
23090
- this.saveIfNeeded();
23210
+ await this.saveIfNeeded();
23091
23211
  this.sendStatus();
23092
23212
  this.sendSessionList();
23093
23213
  }
@@ -23604,6 +23724,10 @@ ${summaryContent}`,
23604
23724
  }
23605
23725
  // ── Commands ─────────────────────────────────────────────────────
23606
23726
  async handleCommand(name, args) {
23727
+ if (this.processing && ["session", "clear", "provider", "model", "rewind", "branch", "compact"].includes(name)) {
23728
+ this.send({ type: "info", message: "Stop the current request before changing sessions or conversation settings." });
23729
+ return;
23730
+ }
23607
23731
  const cliOnlyReason = CLI_ONLY_WEB_COMMANDS.get(name);
23608
23732
  if (cliOnlyReason) {
23609
23733
  this.send({
@@ -24550,7 +24674,7 @@ async function startWebServer(options = {}) {
24550
24674
  next();
24551
24675
  });
24552
24676
  const server = createServer(app);
24553
- const WS_MAX_PAYLOAD = 1 * 1024 * 1024;
24677
+ const WS_MAX_PAYLOAD = MAX_CHAT_BYTES;
24554
24678
  const WS_MSG_RATE_PER_SEC = 30;
24555
24679
  const WS_MSG_BURST = 60;
24556
24680
  const WS_PREAUTH_TIMEOUT_MS = 3e4;
@@ -24623,6 +24747,7 @@ async function startWebServer(options = {}) {
24623
24747
  app.get("/api/status", (_req, res) => {
24624
24748
  res.json({
24625
24749
  version: VERSION,
24750
+ chatLimits: { maxChatBytes: MAX_CHAT_BYTES, maxImageBytes: MAX_IMAGE_BYTES },
24626
24751
  providers: availableProviders.map((p) => ({
24627
24752
  id: p.info.id,
24628
24753
  displayName: p.info.displayName,
@@ -24831,11 +24956,6 @@ async function startWebServer(options = {}) {
24831
24956
  const tabId = params.get("tabId") || `tab-${Date.now()}`;
24832
24957
  const cookies = parseCookie(req.headers.cookie ?? "");
24833
24958
  const token = cookies["aicli_token"] || "";
24834
- const existing = handlers.get(tabId);
24835
- if (existing) {
24836
- existing.onDisconnect();
24837
- handlers.delete(tabId);
24838
- }
24839
24959
  let tokens = WS_MSG_BURST;
24840
24960
  let lastRefill = Date.now();
24841
24961
  const takeToken = () => {
@@ -24851,6 +24971,32 @@ async function startWebServer(options = {}) {
24851
24971
  };
24852
24972
  let authenticatedUser = null;
24853
24973
  let handler = null;
24974
+ let handlerKey = "";
24975
+ const attachHandler = (resources) => {
24976
+ if (handler) {
24977
+ handler.onDisconnect(false);
24978
+ if (handlers.get(handlerKey) === handler) handlers.delete(handlerKey);
24979
+ }
24980
+ handlerKey = JSON.stringify([authenticatedUser, tabId]);
24981
+ const existing = handlers.get(handlerKey);
24982
+ existing?.onDisconnect();
24983
+ const attached = new SessionHandler(ws, { ...resources, sessions: new SessionManager(resources.config) });
24984
+ handlers.set(handlerKey, attached);
24985
+ return attached;
24986
+ };
24987
+ let alive = true;
24988
+ ws.on("pong", () => {
24989
+ alive = true;
24990
+ });
24991
+ const heartbeat = setInterval(() => {
24992
+ if (!alive) {
24993
+ ws.terminate();
24994
+ return;
24995
+ }
24996
+ alive = false;
24997
+ if (ws.readyState === ws.OPEN) ws.ping();
24998
+ }, 3e4);
24999
+ heartbeat.unref();
24854
25000
  let preAuthTimer = setTimeout(() => {
24855
25001
  if (!handler && ws.readyState === ws.OPEN) {
24856
25002
  try {
@@ -24871,14 +25017,12 @@ async function startWebServer(options = {}) {
24871
25017
  const authRequired = authManager.isEnabled();
24872
25018
  if (!authRequired) {
24873
25019
  console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (no auth) (total: ${handlers.size + 1})`);
24874
- handler = new SessionHandler(ws, shared);
24875
- handlers.set(tabId, handler);
25020
+ handler = attachHandler(shared);
24876
25021
  clearPreAuthTimer();
24877
25022
  } else if (authenticatedUser) {
24878
25023
  console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (user: ${authenticatedUser}) (total: ${handlers.size + 1})`);
24879
25024
  const userShared = getUserShared(authenticatedUser);
24880
- handler = new SessionHandler(ws, userShared);
24881
- handlers.set(tabId, handler);
25025
+ handler = attachHandler(userShared);
24882
25026
  clearPreAuthTimer();
24883
25027
  } else {
24884
25028
  console.log(` \u23F3 Tab waiting auth: ${tabId.slice(0, 12)}`);
@@ -24923,8 +25067,7 @@ async function startWebServer(options = {}) {
24923
25067
  authManager.login(username, password);
24924
25068
  authenticatedUser = username;
24925
25069
  const userShared = getUserShared(username);
24926
- handler = new SessionHandler(ws, userShared);
24927
- handlers.set(tabId, handler);
25070
+ handler = attachHandler(userShared);
24928
25071
  clearPreAuthTimer();
24929
25072
  console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
24930
25073
  ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
@@ -24939,8 +25082,7 @@ async function startWebServer(options = {}) {
24939
25082
  }
24940
25083
  authenticatedUser = verifiedUser;
24941
25084
  const userShared = getUserShared(verifiedUser);
24942
- handler = new SessionHandler(ws, userShared);
24943
- handlers.set(tabId, handler);
25085
+ handler = attachHandler(userShared);
24944
25086
  clearPreAuthTimer();
24945
25087
  console.log(` \u2713 Token auth: ${verifiedUser} (tab: ${tabId.slice(0, 12)})`);
24946
25088
  ws.send(JSON.stringify({ type: "auth_result", success: true, username: verifiedUser }));
@@ -24954,8 +25096,7 @@ async function startWebServer(options = {}) {
24954
25096
  }
24955
25097
  authenticatedUser = username;
24956
25098
  const userShared = getUserShared(username);
24957
- handler = new SessionHandler(ws, userShared);
24958
- handlers.set(tabId, handler);
25099
+ handler = attachHandler(userShared);
24959
25100
  clearPreAuthTimer();
24960
25101
  console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
24961
25102
  ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
@@ -24983,10 +25124,11 @@ async function startWebServer(options = {}) {
24983
25124
  }
24984
25125
  });
24985
25126
  ws.on("close", () => {
25127
+ clearInterval(heartbeat);
24986
25128
  clearPreAuthTimer();
24987
- const wasTracked = handlers.has(tabId);
25129
+ const wasTracked = handlers.get(handlerKey) === handler;
24988
25130
  if (handler) handler.onDisconnect();
24989
- if (wasTracked) handlers.delete(tabId);
25131
+ if (wasTracked) handlers.delete(handlerKey);
24990
25132
  console.log(` \u2717 Tab disconnected: ${tabId.slice(0, 12)} (total: ${handlers.size})`);
24991
25133
  });
24992
25134
  ws.on("error", (err) => {