jinzd-ai-cli 0.4.304 → 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-RY2F7BX5.js → batch-OX27K4GG.js} +2 -2
  2. package/dist/{chunk-7L7T46PI.js → chunk-3ZABGT3S.js} +1 -1
  3. package/dist/{chunk-RWM2GFRC.js → chunk-45UVJXFQ.js} +6 -2
  4. package/dist/{chunk-6AGTDWO3.js → chunk-4DH5LZR5.js} +1 -1
  5. package/dist/{chunk-23SJVOQD.js → chunk-72AR23R2.js} +26 -5
  6. package/dist/{chunk-OECPGCHP.js → chunk-AXKERUXH.js} +3 -3
  7. package/dist/{chunk-VOIOUJXI.js → chunk-C2Z77RTU.js} +13 -5
  8. package/dist/{chunk-F3EJ4MIP.js → chunk-GOIDKTBE.js} +12 -11
  9. package/dist/{chunk-BSBGPDCX.js → chunk-I6RATYRF.js} +1 -1
  10. package/dist/{chunk-2PLYXL4P.js → chunk-IPUTC5EX.js} +12 -10
  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-RHCRE6XE.js → chunk-NBYSFFCO.js} +1 -1
  14. package/dist/{chunk-XIMM47EG.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-2GRGLTAK.js → chunk-PA4C452B.js} +1 -1
  18. package/dist/{chunk-I2WJ44D3.js → chunk-Q4DAKI7I.js} +1 -1
  19. package/dist/{chunk-TBTVV3KK.js → chunk-SZ4226XY.js} +1 -1
  20. package/dist/{chunk-3FI4TE5S.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-DNGUSTL6.js → chunk-YPOWDFOV.js} +1 -1
  25. package/dist/{ci-QYTLBM6Z.js → ci-IIMLOXFH.js} +4 -4
  26. package/dist/{ci-format-E5X27NC2.js → ci-format-FMBJC3EY.js} +2 -2
  27. package/dist/{constants-JDPKWZJP.js → constants-55QQ3RYE.js} +1 -1
  28. package/dist/{constants-F5WA5VPO.js → constants-7NVAPQVR.js} +1 -1
  29. package/dist/{doctor-cli-7NSEVW63.js → doctor-cli-ZHZ2XOF3.js} +4 -4
  30. package/dist/electron-server.js +232 -106
  31. package/dist/{hub-43PWTGGO.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-A7RPDLR7.js → persistent-memory-LCOKUDTM.js} +2 -2
  37. package/dist/{persistent-memory-SHG3CJCP.js → persistent-memory-NACQW2VB.js} +2 -2
  38. package/dist/{pr-LW755EL7.js → pr-NHWE5FW7.js} +4 -4
  39. package/dist/{run-tests-MJ3SCW27.js → run-tests-EUPJLDXS.js} +2 -2
  40. package/dist/{run-tests-HV37CM5U.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-EZ5ULLDP.js → server-PG7QCH53.js} +193 -82
  44. package/dist/{server-WI3Q4EDH.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-PWW5I2KE.js → task-orchestrator-WAJTCE6D.js} +9 -9
  48. package/dist/{usage-MAIV6XUZ.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-DNGUSTL6.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-TBTVV3KK.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-2GRGLTAK.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";
@@ -7816,26 +7834,11 @@ function checkNetworkPolicy(toolName, args, policy) {
7816
7834
  // src/tools/truncate.ts
7817
7835
  var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
7818
7836
  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) {
7837
+ function getMaxOutputChars(contextWindow, cap = activeCap) {
7830
7838
  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));
7839
+ return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), cap > 0 ? cap : DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP));
7832
7840
  }
7833
7841
  var activeMaxChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
7834
- var lastContextWindow = 0;
7835
- function setContextWindow(contextWindow) {
7836
- lastContextWindow = contextWindow;
7837
- activeMaxChars = getMaxOutputChars(contextWindow);
7838
- }
7839
7842
  function getActiveMaxChars() {
7840
7843
  return activeMaxChars;
7841
7844
  }
@@ -8965,7 +8968,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
8965
8968
  const mode = appendMode ? "appended" : "written";
8966
8969
  void (async () => {
8967
8970
  try {
8968
- const { updateFile } = await import("./indexer-RBEKMFT4.js");
8971
+ const { updateFile } = await import("./indexer-FPKY7LCT.js");
8969
8972
  await updateFile(process.cwd(), filePath);
8970
8973
  } catch {
8971
8974
  }
@@ -14096,6 +14099,27 @@ function suggestSkillName(names, input) {
14096
14099
  // src/web/tool-executor-web.ts
14097
14100
  import { randomUUID as randomUUID2 } from "crypto";
14098
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
14099
14123
  import { existsSync as existsSync22, readFileSync as readFileSync17 } from "fs";
14100
14124
  var ToolExecutorWeb = class _ToolExecutorWeb {
14101
14125
  constructor(registry, ws) {
@@ -14125,6 +14149,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14125
14149
  // 5 minutes
14126
14150
  /** Publicly readable by SessionHandler to check if confirm is active */
14127
14151
  confirming = false;
14152
+ maxOutputChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
14128
14153
  /** Session-level auto-approve toggle (/yolo command) */
14129
14154
  sessionAutoApprove = false;
14130
14155
  /** Session-level Auto Mode: rule-based low-risk auto approval. */
@@ -14207,9 +14232,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14207
14232
  this.confirming = false;
14208
14233
  }
14209
14234
  send(msg) {
14210
- if (this.ws.readyState === this.ws.OPEN) {
14211
- this.ws.send(JSON.stringify(msg));
14212
- }
14235
+ sendSocketMessage(this.ws, msg);
14213
14236
  }
14214
14237
  sendToolCallStart(call) {
14215
14238
  const dangerLevel = getDangerLevel(call.name, call.arguments);
@@ -14402,7 +14425,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14402
14425
  if (!toolCallStarted) this.sendToolCallStart(call);
14403
14426
  try {
14404
14427
  const rawContent = await runTool(tool, call.arguments, call.name);
14405
- const content = truncateOutput(rawContent, call.name);
14428
+ const content = truncateOutput(rawContent, call.name, this.maxOutputChars);
14406
14429
  this.sendToolCallResult(call, rawContent, false);
14407
14430
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
14408
14431
  runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
@@ -14467,7 +14490,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
14467
14490
  }
14468
14491
  try {
14469
14492
  const rawContent = await runTool(tool, call.arguments, call.name);
14470
- const content = truncateOutput(rawContent, call.name);
14493
+ const content = truncateOutput(rawContent, call.name, this.maxOutputChars);
14471
14494
  this.sendToolCallResult(call, rawContent, false);
14472
14495
  results[i] = { callId: call.id, content, isError: false };
14473
14496
  } catch (err) {
@@ -15416,6 +15439,7 @@ var HubAgent = class {
15416
15439
  model: this.modelId,
15417
15440
  systemPrompt,
15418
15441
  stream: false,
15442
+ signal: opts?.signal,
15419
15443
  temperature: 0.7,
15420
15444
  maxTokens: 4096,
15421
15445
  ...this.thinking ?? {}
@@ -15450,15 +15474,21 @@ var HubAgent = class {
15450
15474
  model: this.modelId,
15451
15475
  systemPrompt,
15452
15476
  stream: true,
15477
+ signal: opts?.signal,
15453
15478
  temperature: 0.7,
15454
15479
  maxTokens: 4096,
15455
15480
  ...this.thinking ?? {}
15456
15481
  });
15457
- for await (const chunk of stream) {
15458
- if (chunk.delta) {
15459
- raw += chunk.delta;
15460
- 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;
15461
15489
  }
15490
+ } catch (error) {
15491
+ if (!opts?.signal?.aborted) throw error;
15462
15492
  }
15463
15493
  const { content, passed, converged } = parseTurn(raw);
15464
15494
  return {
@@ -15473,7 +15503,7 @@ var HubAgent = class {
15473
15503
  /**
15474
15504
  * Generate a summary of the entire discussion from this agent's perspective.
15475
15505
  */
15476
- async summarize(topic, history) {
15506
+ async summarize(topic, history, signal) {
15477
15507
  const provider = this.providers.get(this.providerId);
15478
15508
  if (!provider) {
15479
15509
  throw new Error(`Provider "${this.providerId}" not available`);
@@ -15490,6 +15520,7 @@ var HubAgent = class {
15490
15520
  model: this.modelId,
15491
15521
  stream: false,
15492
15522
  temperature: 0.3,
15523
+ signal,
15493
15524
  maxTokens: 4096,
15494
15525
  ...this.thinking ?? {}
15495
15526
  });
@@ -15583,6 +15614,8 @@ var DiscussionOrchestrator = class {
15583
15614
  agents = [];
15584
15615
  state;
15585
15616
  aborted = false;
15617
+ controller = new AbortController();
15618
+ cancelReview;
15586
15619
  humanSteer;
15587
15620
  voteConverge;
15588
15621
  /** Callback for rendering events */
@@ -15622,6 +15655,8 @@ var DiscussionOrchestrator = class {
15622
15655
  /** Signal the orchestrator to stop after current turn */
15623
15656
  abort() {
15624
15657
  this.aborted = true;
15658
+ this.controller.abort();
15659
+ this.cancelReview?.();
15625
15660
  }
15626
15661
  /**
15627
15662
  * Run the full discussion on a given topic.
@@ -15632,6 +15667,7 @@ var DiscussionOrchestrator = class {
15632
15667
  this.state.round = 0;
15633
15668
  this.state.finished = false;
15634
15669
  this.aborted = false;
15670
+ this.controller = new AbortController();
15635
15671
  try {
15636
15672
  for (let round = 1; round <= this.state.maxRounds; round++) {
15637
15673
  if (this.aborted) {
@@ -15652,7 +15688,7 @@ var DiscussionOrchestrator = class {
15652
15688
  round,
15653
15689
  this.state.maxRounds,
15654
15690
  (token) => this.emit({ type: "agent_token", roleId: agent.role.id, token }),
15655
- { voteConverge: this.voteConverge }
15691
+ { voteConverge: this.voteConverge, signal: this.controller.signal }
15656
15692
  );
15657
15693
  this.state.messages.push(message);
15658
15694
  if (message.converged) convergedCount++;
@@ -15664,6 +15700,7 @@ var DiscussionOrchestrator = class {
15664
15700
  this.emit({ type: "agent_spoke", roleId: agent.role.id, message });
15665
15701
  }
15666
15702
  } catch (err) {
15703
+ if (this.aborted) break;
15667
15704
  const errMsg = err instanceof Error ? err.message : String(err);
15668
15705
  this.emit({ type: "error", roleId: agent.role.id, message: errMsg });
15669
15706
  this.state.messages.push({
@@ -15675,6 +15712,10 @@ var DiscussionOrchestrator = class {
15675
15712
  }
15676
15713
  }
15677
15714
  this.emit({ type: "round_end", round });
15715
+ if (this.aborted) {
15716
+ this.emit({ type: "discussion_end", reason: "user_interrupt" });
15717
+ break;
15718
+ }
15678
15719
  if (allPassed && round > 1) {
15679
15720
  this.emit({ type: "discussion_end", reason: "consensus" });
15680
15721
  break;
@@ -15691,7 +15732,18 @@ var DiscussionOrchestrator = class {
15691
15732
  break;
15692
15733
  }
15693
15734
  if (this.humanSteer && this.onRoundReview && !this.aborted) {
15694
- 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
+ }
15695
15747
  if (steer.action === "stop") {
15696
15748
  this.emit({ type: "discussion_end", reason: "human_stop" });
15697
15749
  break;
@@ -15712,7 +15764,7 @@ var DiscussionOrchestrator = class {
15712
15764
  const errMsg = err instanceof Error ? err.message : String(err);
15713
15765
  this.emit({ type: "error", message: errMsg });
15714
15766
  }
15715
- await this.generateSummary();
15767
+ if (!this.aborted) await this.generateSummary();
15716
15768
  this.state.finished = true;
15717
15769
  return this.state;
15718
15770
  }
@@ -15730,7 +15782,7 @@ var DiscussionOrchestrator = class {
15730
15782
  return;
15731
15783
  }
15732
15784
  try {
15733
- 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);
15734
15786
  this.emit({ type: "summary", content: this.state.summary });
15735
15787
  } catch (err) {
15736
15788
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -16206,11 +16258,11 @@ function routeDecisionRows(message, currentModel, decision) {
16206
16258
  }
16207
16259
 
16208
16260
  // src/web/commands/provider-commands.ts
16209
- function syncSessionProvider(ctx) {
16261
+ async function syncSessionProvider(ctx) {
16210
16262
  const session = ctx.sessions.current;
16211
16263
  if (!session) return;
16212
16264
  session.updateProvider(ctx.currentProvider, ctx.currentModel);
16213
- ctx.saveIfNeeded();
16265
+ await ctx.saveIfNeeded();
16214
16266
  }
16215
16267
  async function handleProvider(args, ctx) {
16216
16268
  const id = args[0];
@@ -16253,7 +16305,7 @@ async function handleProvider(args, ctx) {
16253
16305
  ctx.currentProvider = outcome.providerId;
16254
16306
  ctx.currentModel = outcome.model;
16255
16307
  ctx.updateContextWindow();
16256
- syncSessionProvider(ctx);
16308
+ await syncSessionProvider(ctx);
16257
16309
  ctx.send({ type: "info", message: outcome.message });
16258
16310
  ctx.sendStatus();
16259
16311
  }
@@ -16307,7 +16359,7 @@ async function handleModel(args, ctx) {
16307
16359
  }
16308
16360
  ctx.currentModel = outcome.model;
16309
16361
  ctx.updateContextWindow();
16310
- syncSessionProvider(ctx);
16362
+ await syncSessionProvider(ctx);
16311
16363
  ctx.send({
16312
16364
  type: "info",
16313
16365
  message: outcome.warning ? `${outcome.message} \u2014 ${outcome.warning}` : outcome.message
@@ -17192,7 +17244,7 @@ async function handleClear(_args, ctx) {
17192
17244
  const prior = ctx.sessions.current;
17193
17245
  const priorId = prior?.id ?? null;
17194
17246
  const priorCount = prior?.messages.length ?? 0;
17195
- ctx.saveIfNeeded();
17247
+ await ctx.saveIfNeeded();
17196
17248
  const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
17197
17249
  ctx.unsavedSessions.set(created.id, created);
17198
17250
  ctx.resetWebSessionUsage();
@@ -17214,7 +17266,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
17214
17266
  Cost: ${formatCost(cost)}` : "";
17215
17267
  let memoryLine = "";
17216
17268
  try {
17217
- const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-A7RPDLR7.js");
17269
+ const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-LCOKUDTM.js");
17218
17270
  const pending = countPendingMemories2(ctx.config.getConfigDir());
17219
17271
  if (pending > 0) memoryLine = `
17220
17272
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -17230,8 +17282,13 @@ Tokens: in=${ctx.sessionTokenUsage.inputTokens} out=${ctx.sessionTokenUsage.outp
17230
17282
  }
17231
17283
  async function handleSession(args, ctx) {
17232
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
+ }
17233
17290
  if (sub === "new") {
17234
- ctx.saveIfNeeded();
17291
+ await ctx.saveIfNeeded();
17235
17292
  const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
17236
17293
  ctx.unsavedSessions.set(created.id, created);
17237
17294
  ctx.resetWebSessionUsage();
@@ -17242,7 +17299,7 @@ async function handleSession(args, ctx) {
17242
17299
  }
17243
17300
  if (sub === "load" && args[1]) {
17244
17301
  const targetId = args[1];
17245
- ctx.saveIfNeeded();
17302
+ await ctx.saveIfNeeded();
17246
17303
  const cachedExact = ctx.unsavedSessions.get(targetId);
17247
17304
  const cached = cachedExact ?? [...ctx.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
17248
17305
  if (cached) {
@@ -18197,7 +18254,7 @@ async function handleTest(args, ctx) {
18197
18254
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
18198
18255
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
18199
18256
  }
18200
- const runTests = ctx.runTests ?? (await import("./run-tests-MJ3SCW27.js")).executeTests;
18257
+ const runTests = ctx.runTests ?? (await import("./run-tests-EUPJLDXS.js")).executeTests;
18201
18258
  const report = await runTests(testArgs);
18202
18259
  ctx.send({ type: "info", message: report });
18203
18260
  } catch (err) {
@@ -18489,9 +18546,9 @@ async function handleContext(_args, ctx) {
18489
18546
  async function handleIndex(args, ctx) {
18490
18547
  const sub = (args[0] ?? "status").toLowerCase();
18491
18548
  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");
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");
18495
18552
  if (sub === "status") {
18496
18553
  const idx = loadIndex2(root);
18497
18554
  const vec = loadVectorStore(root);
@@ -18514,6 +18571,7 @@ async function handleIndex(args, ctx) {
18514
18571
  } else if (sub === "rebuild") {
18515
18572
  ctx.send({ type: "info", message: `Indexing ${root}\u2026` });
18516
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." });
18517
18575
  ctx.send({
18518
18576
  type: "info",
18519
18577
  message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
@@ -18540,7 +18598,7 @@ async function handleIndex(args, ctx) {
18540
18598
  message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
18541
18599
  });
18542
18600
  try {
18543
- const { rebuildSemanticIndex } = await import("./semantic-VPNKXKKK.js");
18601
+ const { rebuildSemanticIndex } = await import("./semantic-QOBAXEQ4.js");
18544
18602
  const stats = await rebuildSemanticIndex(root);
18545
18603
  const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
18546
18604
  ctx.send({
@@ -18587,7 +18645,7 @@ async function handleMemory(args, ctx) {
18587
18645
  }
18588
18646
  if (intent.kind === "status") {
18589
18647
  const { getChatIndexStatus: getChatIndexStatus2 } = await import("./chat-index-DGUOUUQW.js");
18590
- const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-A7RPDLR7.js");
18648
+ const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-LCOKUDTM.js");
18591
18649
  const memories = listMemoryEntries2(configDir, { includeExpired: true, includeRejected: true });
18592
18650
  const injected = formatMemoryForPrompt2(configDir, process.cwd(), ctx.config.get("memory")?.maxChars);
18593
18651
  const view = buildMemoryStatusView({
@@ -18610,7 +18668,7 @@ async function handleMemory(args, ctx) {
18610
18668
  return;
18611
18669
  }
18612
18670
  if (intent.kind === "recall") {
18613
- const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-A7RPDLR7.js");
18671
+ const { searchPersistentMemories: searchPersistentMemories2, touchMemoryReferences: touchMemoryReferences2 } = await import("./persistent-memory-LCOKUDTM.js");
18614
18672
  const { searchChatMemory: searchChatMemory2 } = await import("./chat-index-DGUOUUQW.js");
18615
18673
  const persistentHits = searchPersistentMemories2(configDir, intent.query, process.cwd(), 3);
18616
18674
  if (persistentHits.length > 0) {
@@ -18731,7 +18789,7 @@ async function handleMemory(args, ctx) {
18731
18789
  ctx.handleMemoryManage(intent.action, args[1], intent.action === "expire" ? args[2] : void 0);
18732
18790
  } else if (intent.kind === "export") {
18733
18791
  if (args[1] === "md") {
18734
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-A7RPDLR7.js");
18792
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-LCOKUDTM.js");
18735
18793
  ctx.send({
18736
18794
  type: "export_data",
18737
18795
  format: "md",
@@ -19736,7 +19794,7 @@ No tools match "${filter}".
19736
19794
  const { join: join29 } = await import("path");
19737
19795
  const { existsSync: existsSync33 } = await import("fs");
19738
19796
  const { getGitRoot: getGitRoot2 } = await import("./git-context-VNCA2GU7.js");
19739
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-JDPKWZJP.js");
19797
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-55QQ3RYE.js");
19740
19798
  const { approveProject, hashMcpFile } = await import("./project-trust-MUG325AW.js");
19741
19799
  const cwd = process.cwd();
19742
19800
  const projectRoot = getGitRoot2(cwd) ?? cwd;
@@ -20655,7 +20713,7 @@ function createReviewCommands() {
20655
20713
  usage: "/test [command|filter]",
20656
20714
  async execute(args, ctx) {
20657
20715
  try {
20658
- const { executeTests } = await import("./run-tests-MJ3SCW27.js");
20716
+ const { executeTests } = await import("./run-tests-EUPJLDXS.js");
20659
20717
  const argStr = args.join(" ").trim();
20660
20718
  let testArgs = {};
20661
20719
  if (argStr) {
@@ -20906,9 +20964,9 @@ function createMemoryCommands() {
20906
20964
  async execute(args, ctx) {
20907
20965
  const sub = (args[0] ?? "status").toLowerCase();
20908
20966
  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");
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");
20912
20970
  if (sub === "status") {
20913
20971
  const idx = loadIndex2(root);
20914
20972
  const vec = loadVectorStore(root);
@@ -20939,6 +20997,7 @@ function createMemoryCommands() {
20939
20997
  }
20940
20998
  });
20941
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."));
20942
21001
  console.log(theme.success(
20943
21002
  ` \u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
20944
21003
  ));
@@ -20967,7 +21026,7 @@ function createMemoryCommands() {
20967
21026
  }
20968
21027
  console.log(theme.dim(` Building semantic index for ${idx.symbolCount} symbols\u2026`));
20969
21028
  console.log(theme.dim(" (First run downloads ~117 MB embedding model to ~/.aicli/models/)"));
20970
- const { rebuildSemanticIndex } = await import("./semantic-VPNKXKKK.js");
21029
+ const { rebuildSemanticIndex } = await import("./semantic-QOBAXEQ4.js");
20971
21030
  try {
20972
21031
  const stats = await rebuildSemanticIndex(root, {
20973
21032
  onProgress: (done, total) => {
@@ -22566,7 +22625,7 @@ var webCommandHandlers = {
22566
22625
  search: handleSearch,
22567
22626
  undo: handleUndo
22568
22627
  };
22569
- var SessionHandler = class {
22628
+ var SessionHandler = class _SessionHandler {
22570
22629
  ws;
22571
22630
  config;
22572
22631
  costTracker;
@@ -22617,6 +22676,7 @@ var SessionHandler = class {
22617
22676
  abortController = null;
22618
22677
  userInterjection = null;
22619
22678
  processing = false;
22679
+ disconnected = false;
22620
22680
  /** P4: active hub discussion orchestrator (web room), if any. */
22621
22681
  hubOrchestrator = null;
22622
22682
  /** P4b: pending between-rounds human-steer prompts (requestId → resolver). */
@@ -22643,9 +22703,13 @@ var SessionHandler = class {
22643
22703
  * 会话一旦保存到磁盘(有消息后),会从此 Map 中移除。
22644
22704
  */
22645
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();
22646
22709
  constructor(ws, shared) {
22647
22710
  this.ws = ws;
22648
22711
  this.config = shared.config;
22712
+ this.unsavedSessions = new Map(_SessionHandler.recovery.get(this.config.getConfigDir()));
22649
22713
  this.costTracker = shared.costTracker;
22650
22714
  this.providers = shared.providers;
22651
22715
  this.sessions = shared.sessions;
@@ -22713,9 +22777,7 @@ var SessionHandler = class {
22713
22777
  * 接口之间的任何漂移(少一个成员、签名改了)都会当场编译失败,而不是等到运行时。
22714
22778
  */
22715
22779
  send(msg) {
22716
- if (this.ws.readyState === this.ws.OPEN) {
22717
- this.ws.send(JSON.stringify(msg));
22718
- }
22780
+ sendSocketMessage(this.ws, msg);
22719
22781
  }
22720
22782
  sendStatus() {
22721
22783
  const providerList = this.providers.listAvailable().map((p) => ({
@@ -22769,6 +22831,7 @@ var SessionHandler = class {
22769
22831
  });
22770
22832
  }
22771
22833
  async handleMessage(raw) {
22834
+ if (this.disconnected) return;
22772
22835
  let msg;
22773
22836
  try {
22774
22837
  msg = JSON.parse(raw);
@@ -22777,8 +22840,33 @@ var SessionHandler = class {
22777
22840
  return;
22778
22841
  }
22779
22842
  switch (msg.type) {
22780
- 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
+ }
22781
22868
  return this.handleChat(msg.content, msg.images);
22869
+ }
22782
22870
  case "command":
22783
22871
  return this.handleCommand(msg.name, msg.args);
22784
22872
  case "confirm_response":
@@ -22820,7 +22908,7 @@ var SessionHandler = class {
22820
22908
  case "hub_start":
22821
22909
  return this.handleHubStart(msg);
22822
22910
  case "hub_abort":
22823
- this.hubOrchestrator?.abort();
22911
+ this.cancelHub();
22824
22912
  return;
22825
22913
  case "hub_steer": {
22826
22914
  const resolve13 = this.pendingHubReview.get(msg.requestId);
@@ -22835,7 +22923,10 @@ var SessionHandler = class {
22835
22923
  return;
22836
22924
  }
22837
22925
  }
22838
- 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");
22839
22930
  runLifecycleHooks(mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()), "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
22840
22931
  this.toolExecutor.cancelAll();
22841
22932
  if (this.abortController) this.abortController.abort();
@@ -22843,18 +22934,24 @@ var SessionHandler = class {
22843
22934
  this.pendingAskUser.clear();
22844
22935
  for (const resolve13 of this.pendingAutoPause.values()) resolve13({ action: "stop" });
22845
22936
  this.pendingAutoPause.clear();
22846
- this.saveIfNeeded();
22937
+ this.cancelHub();
22938
+ void this.saveIfNeeded();
22847
22939
  if (this._bashSessionKey) clearBashSession(this._bashSessionKey);
22848
22940
  }
22941
+ cancelHub() {
22942
+ this.hubOrchestrator?.abort();
22943
+ for (const resolve13 of this.pendingHubReview.values()) resolve13({ action: "stop" });
22944
+ this.pendingHubReview.clear();
22945
+ }
22849
22946
  /** 根据当前模型 context window 更新工具输出截断上限 */
22850
22947
  updateContextWindow() {
22851
22948
  try {
22852
22949
  const cfgOutputCap = this.config.get("maxToolOutputChars");
22853
- if (typeof cfgOutputCap === "number" && cfgOutputCap > 0) setMaxOutputCap(cfgOutputCap);
22854
22950
  const provider = this.providers.get(this.currentProvider);
22855
22951
  const modelInfo = provider?.info.models.find((m) => m.id === this.currentModel);
22856
- if (modelInfo?.contextWindow) setContextWindow(modelInfo.contextWindow);
22952
+ this.toolExecutor.maxOutputChars = getMaxOutputChars(modelInfo?.contextWindow, cfgOutputCap ?? 0);
22857
22953
  } catch {
22954
+ this.toolExecutor.maxOutputChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
22858
22955
  }
22859
22956
  }
22860
22957
  /** 获取当前模型的 context window 大小(0 表示未知)*/
@@ -22906,12 +23003,24 @@ var SessionHandler = class {
22906
23003
  return total;
22907
23004
  }
22908
23005
  /** 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);
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");
22915
23024
  }
22916
23025
  }
22917
23026
  /** Lazily create a session if none exists yet (deferred from constructor). */
@@ -23036,20 +23145,15 @@ var SessionHandler = class {
23036
23145
  const kinds = [...new Set(secretHits.map((h) => h.kind))].join(", ");
23037
23146
  this.send({ type: "info", message: "Possible secret in prompt (" + kinds + ")." });
23038
23147
  }
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;
23148
+ const imageError = imageValidationError(images);
23149
+ if (imageError) {
23150
+ this.send({ type: "error", message: imageError });
23151
+ return;
23152
+ }
23041
23153
  let msgContent;
23042
23154
  if (images && images.length > 0) {
23043
23155
  const parts = [];
23044
23156
  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
23157
  parts.push({
23054
23158
  type: "image_url",
23055
23159
  image_url: { url: `data:${img.mime};base64,${img.data}` }
@@ -23103,7 +23207,7 @@ var SessionHandler = class {
23103
23207
  this.send({ type: "error", message });
23104
23208
  } finally {
23105
23209
  this.processing = false;
23106
- this.saveIfNeeded();
23210
+ await this.saveIfNeeded();
23107
23211
  this.sendStatus();
23108
23212
  this.sendSessionList();
23109
23213
  }
@@ -23620,6 +23724,10 @@ ${summaryContent}`,
23620
23724
  }
23621
23725
  // ── Commands ─────────────────────────────────────────────────────
23622
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
+ }
23623
23731
  const cliOnlyReason = CLI_ONLY_WEB_COMMANDS.get(name);
23624
23732
  if (cliOnlyReason) {
23625
23733
  this.send({
@@ -24566,7 +24674,7 @@ async function startWebServer(options = {}) {
24566
24674
  next();
24567
24675
  });
24568
24676
  const server = createServer(app);
24569
- const WS_MAX_PAYLOAD = 1 * 1024 * 1024;
24677
+ const WS_MAX_PAYLOAD = MAX_CHAT_BYTES;
24570
24678
  const WS_MSG_RATE_PER_SEC = 30;
24571
24679
  const WS_MSG_BURST = 60;
24572
24680
  const WS_PREAUTH_TIMEOUT_MS = 3e4;
@@ -24639,6 +24747,7 @@ async function startWebServer(options = {}) {
24639
24747
  app.get("/api/status", (_req, res) => {
24640
24748
  res.json({
24641
24749
  version: VERSION,
24750
+ chatLimits: { maxChatBytes: MAX_CHAT_BYTES, maxImageBytes: MAX_IMAGE_BYTES },
24642
24751
  providers: availableProviders.map((p) => ({
24643
24752
  id: p.info.id,
24644
24753
  displayName: p.info.displayName,
@@ -24847,11 +24956,6 @@ async function startWebServer(options = {}) {
24847
24956
  const tabId = params.get("tabId") || `tab-${Date.now()}`;
24848
24957
  const cookies = parseCookie(req.headers.cookie ?? "");
24849
24958
  const token = cookies["aicli_token"] || "";
24850
- const existing = handlers.get(tabId);
24851
- if (existing) {
24852
- existing.onDisconnect();
24853
- handlers.delete(tabId);
24854
- }
24855
24959
  let tokens = WS_MSG_BURST;
24856
24960
  let lastRefill = Date.now();
24857
24961
  const takeToken = () => {
@@ -24867,6 +24971,32 @@ async function startWebServer(options = {}) {
24867
24971
  };
24868
24972
  let authenticatedUser = null;
24869
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();
24870
25000
  let preAuthTimer = setTimeout(() => {
24871
25001
  if (!handler && ws.readyState === ws.OPEN) {
24872
25002
  try {
@@ -24887,14 +25017,12 @@ async function startWebServer(options = {}) {
24887
25017
  const authRequired = authManager.isEnabled();
24888
25018
  if (!authRequired) {
24889
25019
  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);
25020
+ handler = attachHandler(shared);
24892
25021
  clearPreAuthTimer();
24893
25022
  } else if (authenticatedUser) {
24894
25023
  console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (user: ${authenticatedUser}) (total: ${handlers.size + 1})`);
24895
25024
  const userShared = getUserShared(authenticatedUser);
24896
- handler = new SessionHandler(ws, userShared);
24897
- handlers.set(tabId, handler);
25025
+ handler = attachHandler(userShared);
24898
25026
  clearPreAuthTimer();
24899
25027
  } else {
24900
25028
  console.log(` \u23F3 Tab waiting auth: ${tabId.slice(0, 12)}`);
@@ -24939,8 +25067,7 @@ async function startWebServer(options = {}) {
24939
25067
  authManager.login(username, password);
24940
25068
  authenticatedUser = username;
24941
25069
  const userShared = getUserShared(username);
24942
- handler = new SessionHandler(ws, userShared);
24943
- handlers.set(tabId, handler);
25070
+ handler = attachHandler(userShared);
24944
25071
  clearPreAuthTimer();
24945
25072
  console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
24946
25073
  ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
@@ -24955,8 +25082,7 @@ async function startWebServer(options = {}) {
24955
25082
  }
24956
25083
  authenticatedUser = verifiedUser;
24957
25084
  const userShared = getUserShared(verifiedUser);
24958
- handler = new SessionHandler(ws, userShared);
24959
- handlers.set(tabId, handler);
25085
+ handler = attachHandler(userShared);
24960
25086
  clearPreAuthTimer();
24961
25087
  console.log(` \u2713 Token auth: ${verifiedUser} (tab: ${tabId.slice(0, 12)})`);
24962
25088
  ws.send(JSON.stringify({ type: "auth_result", success: true, username: verifiedUser }));
@@ -24970,8 +25096,7 @@ async function startWebServer(options = {}) {
24970
25096
  }
24971
25097
  authenticatedUser = username;
24972
25098
  const userShared = getUserShared(username);
24973
- handler = new SessionHandler(ws, userShared);
24974
- handlers.set(tabId, handler);
25099
+ handler = attachHandler(userShared);
24975
25100
  clearPreAuthTimer();
24976
25101
  console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
24977
25102
  ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
@@ -24999,10 +25124,11 @@ async function startWebServer(options = {}) {
24999
25124
  }
25000
25125
  });
25001
25126
  ws.on("close", () => {
25127
+ clearInterval(heartbeat);
25002
25128
  clearPreAuthTimer();
25003
- const wasTracked = handlers.has(tabId);
25129
+ const wasTracked = handlers.get(handlerKey) === handler;
25004
25130
  if (handler) handler.onDisconnect();
25005
- if (wasTracked) handlers.delete(tabId);
25131
+ if (wasTracked) handlers.delete(handlerKey);
25006
25132
  console.log(` \u2717 Tab disconnected: ${tabId.slice(0, 12)} (total: ${handlers.size})`);
25007
25133
  });
25008
25134
  ws.on("error", (err) => {