jinzd-ai-cli 0.4.304 → 0.4.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/{batch-RY2F7BX5.js → batch-JLGWQ7HU.js} +2 -2
  2. package/dist/{chunk-3FI4TE5S.js → chunk-3RDJEYF7.js} +3 -3
  3. package/dist/{chunk-RWM2GFRC.js → chunk-45UVJXFQ.js} +6 -2
  4. package/dist/{chunk-23SJVOQD.js → chunk-72AR23R2.js} +26 -5
  5. package/dist/{chunk-I2WJ44D3.js → chunk-7SSCSSC6.js} +1 -1
  6. package/dist/{chunk-F3EJ4MIP.js → chunk-AYLK4EGA.js} +12 -11
  7. package/dist/{chunk-VOIOUJXI.js → chunk-C2Z77RTU.js} +13 -5
  8. package/dist/{chunk-2GRGLTAK.js → chunk-JDT6UG6R.js} +3 -1
  9. package/dist/{chunk-OECPGCHP.js → chunk-JFNBJHR6.js} +3 -3
  10. package/dist/{chunk-6AGTDWO3.js → chunk-K5SDDTN7.js} +1 -1
  11. package/dist/{chunk-7XU4BATV.js → chunk-KBDKKHXB.js} +15 -6
  12. package/dist/{chunk-7L7T46PI.js → chunk-LAP7TPPK.js} +1 -1
  13. package/dist/{chunk-4DEMGCK4.js → chunk-LF67HWWR.js} +37 -13
  14. package/dist/{chunk-TBTVV3KK.js → chunk-NLB7JZSO.js} +1 -1
  15. package/dist/{chunk-BXP6YZ2P.js → chunk-NRSYBKWA.js} +5 -4
  16. package/dist/{chunk-IEQAE3QG.js → chunk-OAXLMF3U.js} +5 -4
  17. package/dist/{chunk-XIMM47EG.js → chunk-OC2XHG5L.js} +1 -1
  18. package/dist/{chunk-RHCRE6XE.js → chunk-T6WQEQI4.js} +3 -1
  19. package/dist/{chunk-PPIISCFN.js → chunk-UFW4S3RU.js} +37 -13
  20. package/dist/{chunk-2PLYXL4P.js → chunk-VLDB2SVN.js} +33 -21
  21. package/dist/{chunk-BSBGPDCX.js → chunk-W6PLV3EY.js} +1 -1
  22. package/dist/{chunk-CKH4KQ4E.js → chunk-WQ3OMEC4.js} +6 -2
  23. package/dist/{chunk-DNGUSTL6.js → chunk-WZWRA7R2.js} +1 -1
  24. package/dist/{chunk-UUSRWSSX.js → chunk-XJOK5353.js} +15 -6
  25. package/dist/{ci-QYTLBM6Z.js → ci-6PHEL3C7.js} +4 -4
  26. package/dist/{ci-format-E5X27NC2.js → ci-format-AZYUP6BL.js} +2 -2
  27. package/dist/{constants-JDPKWZJP.js → constants-AXBD2EEB.js} +3 -1
  28. package/dist/{constants-F5WA5VPO.js → constants-CPNWIUGZ.js} +3 -1
  29. package/dist/{doctor-cli-7NSEVW63.js → doctor-cli-DDHHDMMJ.js} +4 -4
  30. package/dist/electron-server.js +253 -117
  31. package/dist/{hub-43PWTGGO.js → hub-DDYAWLF2.js} +4 -4
  32. package/dist/{hub-server-RF6LURXY.js → hub-server-CBKCP5VD.js} +1 -1
  33. package/dist/index.js +25 -24
  34. package/dist/{indexer-RBEKMFT4.js → indexer-FPKY7LCT.js} +2 -2
  35. package/dist/{indexer-TNDWGFRI.js → indexer-P3FPRTPJ.js} +2 -2
  36. package/dist/{persistent-memory-SHG3CJCP.js → persistent-memory-6XYYOMSC.js} +2 -2
  37. package/dist/{persistent-memory-A7RPDLR7.js → persistent-memory-XZISXRWT.js} +2 -2
  38. package/dist/{pr-LW755EL7.js → pr-UY4ZCZNI.js} +4 -4
  39. package/dist/{run-tests-HV37CM5U.js → run-tests-BAMLVLO6.js} +2 -2
  40. package/dist/{run-tests-MJ3SCW27.js → run-tests-UEE3OMLS.js} +2 -2
  41. package/dist/{semantic-HLAE2O4F.js → semantic-7GM5M4MU.js} +3 -3
  42. package/dist/{semantic-VPNKXKKK.js → semantic-QOBAXEQ4.js} +3 -3
  43. package/dist/{server-WI3Q4EDH.js → server-6IC33CJK.js} +9 -9
  44. package/dist/{server-EZ5ULLDP.js → server-DBHLVQVP.js} +193 -82
  45. package/dist/{store-MWNHVGJT.js → store-CMSSWZF7.js} +3 -1
  46. package/dist/{store-VO37H6LS.js → store-SIKHCUYJ.js} +3 -1
  47. package/dist/{task-orchestrator-PWW5I2KE.js → task-orchestrator-RDDETDW2.js} +9 -9
  48. package/dist/{usage-MAIV6XUZ.js → usage-D473ZVIP.js} +2 -2
  49. package/dist/{vector-store-JBAE6PS4.js → vector-store-JAXWDZYW.js} +1 -1
  50. package/dist/{vector-store-BBDXB5IQ.js → vector-store-PWIOQFTP.js} +1 -1
  51. package/dist/web/client/app.js +135 -60
  52. package/dist/web/client/app.js.br +0 -0
  53. package/dist/web/client/app.js.gz +0 -0
  54. package/dist/web/client/chat-input.js +38 -0
  55. package/dist/web/client/chat-input.js.br +0 -0
  56. package/dist/web/client/chat-input.js.gz +0 -0
  57. package/dist/web/client/index.html +1 -1
  58. package/dist/web/client/index.html.br +0 -0
  59. package/dist/web/client/index.html.gz +0 -0
  60. package/dist/web/client/stream-render.js +33 -0
  61. package/dist/web/client/style.css +2 -1
  62. package/dist/web/client/style.css.br +0 -0
  63. package/dist/web/client/style.css.gz +0 -0
  64. package/package.json +1 -1
@@ -3,8 +3,8 @@ import {
3
3
  DiscussionOrchestrator,
4
4
  getPreset,
5
5
  resolveRoleProviders
6
- } from "./chunk-23SJVOQD.js";
7
- import "./chunk-VOIOUJXI.js";
6
+ } from "./chunk-72AR23R2.js";
7
+ import "./chunk-C2Z77RTU.js";
8
8
  import {
9
9
  AuthManager,
10
10
  TOKEN_EXPIRY_MS
@@ -81,7 +81,7 @@ import {
81
81
  skillHistoryNote,
82
82
  snapshotFile,
83
83
  stripRoutingTags
84
- } from "./chunk-F3EJ4MIP.js";
84
+ } from "./chunk-AYLK4EGA.js";
85
85
  import {
86
86
  CNY_PER_USD,
87
87
  CostTracker,
@@ -92,6 +92,7 @@ import {
92
92
  setCostCurrency
93
93
  } from "./chunk-YKJO3SI4.js";
94
94
  import {
95
+ DEFAULT_MAX_TOOL_OUTPUT_CHARS,
95
96
  ToolExecutor,
96
97
  ToolRegistry,
97
98
  askUserContext,
@@ -105,6 +106,7 @@ import {
105
106
  estimateTokens,
106
107
  formatPermissionProfileWarning,
107
108
  gateMessages,
109
+ getMaxOutputChars,
108
110
  getPreferredAgentName,
109
111
  getRecentlyDeniedAutoActions,
110
112
  googleSearchContext,
@@ -120,13 +122,11 @@ import {
120
122
  resolveWriteFileArgs,
121
123
  runSafePhases,
122
124
  runWithSessionKey,
123
- setContextWindow,
124
- setMaxOutputCap,
125
125
  setPreferredAgentName,
126
126
  spawnAgentContext,
127
127
  truncateOutput,
128
128
  undoStack
129
- } from "./chunk-2PLYXL4P.js";
129
+ } from "./chunk-VLDB2SVN.js";
130
130
  import {
131
131
  buildReviewPrompt,
132
132
  buildSecurityReviewPrompt
@@ -153,11 +153,11 @@ import {
153
153
  restoreMemoryEntries,
154
154
  updateMemoryApproval,
155
155
  updateMemoryEntry
156
- } from "./chunk-7L7T46PI.js";
157
- import "./chunk-4DEMGCK4.js";
158
- import "./chunk-UUSRWSSX.js";
159
- import "./chunk-CKH4KQ4E.js";
160
- import "./chunk-BXP6YZ2P.js";
156
+ } from "./chunk-LAP7TPPK.js";
157
+ import "./chunk-LF67HWWR.js";
158
+ import "./chunk-XJOK5353.js";
159
+ import "./chunk-WQ3OMEC4.js";
160
+ import "./chunk-NRSYBKWA.js";
161
161
  import "./chunk-KHYD3WXE.js";
162
162
  import "./chunk-5C76XDDF.js";
163
163
  import {
@@ -172,7 +172,7 @@ import {
172
172
  buildDoctorReport,
173
173
  formatDoctorReport,
174
174
  loadContextFiles
175
- } from "./chunk-OECPGCHP.js";
175
+ } from "./chunk-JFNBJHR6.js";
176
176
  import {
177
177
  describePlugin,
178
178
  getActivePluginAssets,
@@ -187,14 +187,14 @@ import {
187
187
  runTool,
188
188
  setPluginEnabled,
189
189
  trustPlugin
190
- } from "./chunk-I2WJ44D3.js";
190
+ } from "./chunk-7SSCSSC6.js";
191
191
  import "./chunk-RS7SKYTS.js";
192
192
  import {
193
193
  ProviderRegistry
194
194
  } from "./chunk-VXEF2SWG.js";
195
195
  import {
196
196
  ConfigManager
197
- } from "./chunk-XIMM47EG.js";
197
+ } from "./chunk-OC2XHG5L.js";
198
198
  import {
199
199
  CONTENT_ONLY_STREAM_REMINDER,
200
200
  TEE_FINAL_USER_NUDGE,
@@ -225,7 +225,7 @@ import {
225
225
  maskConfigSecrets
226
226
  } from "./chunk-XNFIYS7G.js";
227
227
  import "./chunk-4BKXL7SM.js";
228
- import "./chunk-BSBGPDCX.js";
228
+ import "./chunk-W6PLV3EY.js";
229
229
  import {
230
230
  AGENTIC_BEHAVIOR_GUIDELINE,
231
231
  AUTHOR,
@@ -241,12 +241,30 @@ import {
241
241
  SKILLS_DIR_NAME,
242
242
  VERSION,
243
243
  buildUserIdentityPrompt
244
- } from "./chunk-RHCRE6XE.js";
244
+ } from "./chunk-T6WQEQI4.js";
245
245
 
246
246
  // src/web/server.ts
247
247
  import express from "express";
248
248
  import { createServer } from "http";
249
249
  import { WebSocketServer } from "ws";
250
+
251
+ // src/web/chat-limits.ts
252
+ var MAX_CHAT_BYTES = 16 * 1024 * 1024;
253
+ var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
254
+ var MAX_IMAGE_BASE64_CHARS = Math.ceil(MAX_IMAGE_BYTES / 3) * 4;
255
+ var IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
256
+ function imageValidationError(images) {
257
+ if (images === void 0) return;
258
+ if (!Array.isArray(images)) return "Images must be an array.";
259
+ for (const image of images) {
260
+ if (!image || typeof image.data !== "string" || !IMAGE_MIMES.has(image.mime)) {
261
+ return "Unsupported image format. Use PNG, JPEG, GIF, WebP or SVG.";
262
+ }
263
+ if (image.data.length > MAX_IMAGE_BASE64_CHARS) return "Image too large (maximum 10 MiB before base64 encoding).";
264
+ }
265
+ }
266
+
267
+ // src/web/server.ts
250
268
  import { join as join2, dirname as dirname3, resolve as resolve3, relative, sep as sep2, extname, resolve as resolvePath } from "path";
251
269
  import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2, realpathSync, createReadStream } from "fs";
252
270
  import { networkInterfaces } from "os";
@@ -254,6 +272,27 @@ import { networkInterfaces } from "os";
254
272
  // src/web/tool-executor-web.ts
255
273
  import { randomUUID } from "crypto";
256
274
  import { tmpdir } from "os";
275
+
276
+ // src/web/socket-send.ts
277
+ var MAX_PENDING_OUTPUT_BYTES = 16 * 1024 * 1024;
278
+ var stalled = /* @__PURE__ */ new WeakSet();
279
+ function sendSocketMessage(ws, message) {
280
+ if (ws.readyState !== ws.OPEN || stalled.has(ws)) return false;
281
+ const data = JSON.stringify(message);
282
+ if ((ws.bufferedAmount || 0) + Buffer.byteLength(data) > MAX_PENDING_OUTPUT_BYTES) {
283
+ stalled.add(ws);
284
+ ws.close(1013, "Output backlog exceeded; reconnect to reload session");
285
+ const timer = setTimeout(() => {
286
+ if (ws.readyState !== ws.CLOSED) ws.terminate();
287
+ }, 1e3);
288
+ timer.unref();
289
+ return false;
290
+ }
291
+ ws.send(data);
292
+ return true;
293
+ }
294
+
295
+ // src/web/tool-executor-web.ts
257
296
  import { existsSync, readFileSync } from "fs";
258
297
  var ToolExecutorWeb = class _ToolExecutorWeb {
259
298
  constructor(registry, ws) {
@@ -283,6 +322,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
283
322
  // 5 minutes
284
323
  /** Publicly readable by SessionHandler to check if confirm is active */
285
324
  confirming = false;
325
+ maxOutputChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
286
326
  /** Session-level auto-approve toggle (/yolo command) */
287
327
  sessionAutoApprove = false;
288
328
  /** Session-level Auto Mode: rule-based low-risk auto approval. */
@@ -365,9 +405,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
365
405
  this.confirming = false;
366
406
  }
367
407
  send(msg) {
368
- if (this.ws.readyState === this.ws.OPEN) {
369
- this.ws.send(JSON.stringify(msg));
370
- }
408
+ sendSocketMessage(this.ws, msg);
371
409
  }
372
410
  sendToolCallStart(call) {
373
411
  const dangerLevel = getDangerLevel(call.name, call.arguments);
@@ -560,7 +598,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
560
598
  if (!toolCallStarted) this.sendToolCallStart(call);
561
599
  try {
562
600
  const rawContent = await runTool(tool, call.arguments, call.name);
563
- const content = truncateOutput(rawContent, call.name);
601
+ const content = truncateOutput(rawContent, call.name, this.maxOutputChars);
564
602
  this.sendToolCallResult(call, rawContent, false);
565
603
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
566
604
  runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
@@ -625,7 +663,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
625
663
  }
626
664
  try {
627
665
  const rawContent = await runTool(tool, call.arguments, call.name);
628
- const content = truncateOutput(rawContent, call.name);
666
+ const content = truncateOutput(rawContent, call.name, this.maxOutputChars);
629
667
  this.sendToolCallResult(call, rawContent, false);
630
668
  results[i] = { callId: call.id, content, isError: false };
631
669
  } catch (err) {
@@ -701,11 +739,11 @@ import { mkdirSync as mkdirSync2, createWriteStream as createWriteStream2 } from
701
739
  import { dirname as dirname2 } from "path";
702
740
 
703
741
  // src/web/commands/provider-commands.ts
704
- function syncSessionProvider(ctx) {
742
+ async function syncSessionProvider(ctx) {
705
743
  const session = ctx.sessions.current;
706
744
  if (!session) return;
707
745
  session.updateProvider(ctx.currentProvider, ctx.currentModel);
708
- ctx.saveIfNeeded();
746
+ await ctx.saveIfNeeded();
709
747
  }
710
748
  async function handleProvider(args, ctx) {
711
749
  const id = args[0];
@@ -748,7 +786,7 @@ async function handleProvider(args, ctx) {
748
786
  ctx.currentProvider = outcome.providerId;
749
787
  ctx.currentModel = outcome.model;
750
788
  ctx.updateContextWindow();
751
- syncSessionProvider(ctx);
789
+ await syncSessionProvider(ctx);
752
790
  ctx.send({ type: "info", message: outcome.message });
753
791
  ctx.sendStatus();
754
792
  }
@@ -802,7 +840,7 @@ async function handleModel(args, ctx) {
802
840
  }
803
841
  ctx.currentModel = outcome.model;
804
842
  ctx.updateContextWindow();
805
- syncSessionProvider(ctx);
843
+ await syncSessionProvider(ctx);
806
844
  ctx.send({
807
845
  type: "info",
808
846
  message: outcome.warning ? `${outcome.message} \u2014 ${outcome.warning}` : outcome.message
@@ -908,7 +946,7 @@ async function handleClear(_args, ctx) {
908
946
  const prior = ctx.sessions.current;
909
947
  const priorId = prior?.id ?? null;
910
948
  const priorCount = prior?.messages.length ?? 0;
911
- ctx.saveIfNeeded();
949
+ await ctx.saveIfNeeded();
912
950
  const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
913
951
  ctx.unsavedSessions.set(created.id, created);
914
952
  ctx.resetWebSessionUsage();
@@ -930,7 +968,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
930
968
  Cost: ${formatCost(cost)}` : "";
931
969
  let memoryLine = "";
932
970
  try {
933
- const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-SHG3CJCP.js");
971
+ const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-6XYYOMSC.js");
934
972
  const pending = countPendingMemories2(ctx.config.getConfigDir());
935
973
  if (pending > 0) memoryLine = `
936
974
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -946,8 +984,13 @@ Tokens: in=${ctx.sessionTokenUsage.inputTokens} out=${ctx.sessionTokenUsage.outp
946
984
  }
947
985
  async function handleSession(args, ctx) {
948
986
  const sub = args[0];
987
+ if (sub === "save") {
988
+ await ctx.saveIfNeeded();
989
+ if (!ctx.sessions.current?.dirty) ctx.send({ type: "info", message: "Session saved." });
990
+ return;
991
+ }
949
992
  if (sub === "new") {
950
- ctx.saveIfNeeded();
993
+ await ctx.saveIfNeeded();
951
994
  const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
952
995
  ctx.unsavedSessions.set(created.id, created);
953
996
  ctx.resetWebSessionUsage();
@@ -958,7 +1001,7 @@ async function handleSession(args, ctx) {
958
1001
  }
959
1002
  if (sub === "load" && args[1]) {
960
1003
  const targetId = args[1];
961
- ctx.saveIfNeeded();
1004
+ await ctx.saveIfNeeded();
962
1005
  const cachedExact = ctx.unsavedSessions.get(targetId);
963
1006
  const cached = cachedExact ?? [...ctx.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
964
1007
  if (cached) {
@@ -1854,7 +1897,7 @@ async function handleTest(args, ctx) {
1854
1897
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
1855
1898
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
1856
1899
  }
1857
- const runTests = ctx.runTests ?? (await import("./run-tests-HV37CM5U.js")).executeTests;
1900
+ const runTests = ctx.runTests ?? (await import("./run-tests-BAMLVLO6.js")).executeTests;
1858
1901
  const report = await runTests(testArgs);
1859
1902
  ctx.send({ type: "info", message: report });
1860
1903
  } catch (err) {
@@ -2031,9 +2074,9 @@ async function handleContext(_args, ctx) {
2031
2074
  async function handleIndex(args, ctx) {
2032
2075
  const sub = (args[0] ?? "status").toLowerCase();
2033
2076
  const root = process.cwd();
2034
- const { loadIndex, clearIndex } = await import("./store-MWNHVGJT.js");
2035
- const { indexProject } = await import("./indexer-TNDWGFRI.js");
2036
- const { loadVectorStore, clearVectorStore } = await import("./vector-store-BBDXB5IQ.js");
2077
+ const { loadIndex, clearIndex } = await import("./store-CMSSWZF7.js");
2078
+ const { indexProject } = await import("./indexer-P3FPRTPJ.js");
2079
+ const { loadVectorStore, clearVectorStore } = await import("./vector-store-PWIOQFTP.js");
2037
2080
  if (sub === "status") {
2038
2081
  const idx = loadIndex(root);
2039
2082
  const vec = loadVectorStore(root);
@@ -2056,6 +2099,7 @@ async function handleIndex(args, ctx) {
2056
2099
  } else if (sub === "rebuild") {
2057
2100
  ctx.send({ type: "info", message: `Indexing ${root}\u2026` });
2058
2101
  const { stats } = await indexProject(root, { force: true });
2102
+ if (stats.incomplete) ctx.send({ type: "info", message: "Partial index: scan limit or unreadable files reached. Unscanned entries were retained." });
2059
2103
  ctx.send({
2060
2104
  type: "info",
2061
2105
  message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
@@ -2082,7 +2126,7 @@ async function handleIndex(args, ctx) {
2082
2126
  message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
2083
2127
  });
2084
2128
  try {
2085
- const { rebuildSemanticIndex } = await import("./semantic-HLAE2O4F.js");
2129
+ const { rebuildSemanticIndex } = await import("./semantic-7GM5M4MU.js");
2086
2130
  const stats = await rebuildSemanticIndex(root);
2087
2131
  const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
2088
2132
  ctx.send({
@@ -2129,7 +2173,7 @@ async function handleMemory(args, ctx) {
2129
2173
  }
2130
2174
  if (intent.kind === "status") {
2131
2175
  const { getChatIndexStatus } = await import("./chat-index-2D5GK2DB.js");
2132
- const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-SHG3CJCP.js");
2176
+ const { listMemoryEntries: listMemoryEntries2, isMemoryExpired: isMemoryExpired2, formatMemoryForPrompt: formatMemoryForPrompt2 } = await import("./persistent-memory-6XYYOMSC.js");
2133
2177
  const memories = listMemoryEntries2(configDir, { includeExpired: true, includeRejected: true });
2134
2178
  const injected = formatMemoryForPrompt2(configDir, process.cwd(), ctx.config.get("memory")?.maxChars);
2135
2179
  const view = buildMemoryStatusView({
@@ -2152,7 +2196,7 @@ async function handleMemory(args, ctx) {
2152
2196
  return;
2153
2197
  }
2154
2198
  if (intent.kind === "recall") {
2155
- const { searchPersistentMemories, touchMemoryReferences } = await import("./persistent-memory-SHG3CJCP.js");
2199
+ const { searchPersistentMemories, touchMemoryReferences } = await import("./persistent-memory-6XYYOMSC.js");
2156
2200
  const { searchChatMemory } = await import("./chat-index-2D5GK2DB.js");
2157
2201
  const persistentHits = searchPersistentMemories(configDir, intent.query, process.cwd(), 3);
2158
2202
  if (persistentHits.length > 0) {
@@ -2273,7 +2317,7 @@ async function handleMemory(args, ctx) {
2273
2317
  ctx.handleMemoryManage(intent.action, args[1], intent.action === "expire" ? args[2] : void 0);
2274
2318
  } else if (intent.kind === "export") {
2275
2319
  if (args[1] === "md") {
2276
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-SHG3CJCP.js");
2320
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-6XYYOMSC.js");
2277
2321
  ctx.send({
2278
2322
  type: "export_data",
2279
2323
  format: "md",
@@ -2692,7 +2736,7 @@ var webCommandHandlers = {
2692
2736
  search: handleSearch,
2693
2737
  undo: handleUndo
2694
2738
  };
2695
- var SessionHandler = class {
2739
+ var SessionHandler = class _SessionHandler {
2696
2740
  ws;
2697
2741
  config;
2698
2742
  costTracker;
@@ -2743,6 +2787,7 @@ var SessionHandler = class {
2743
2787
  abortController = null;
2744
2788
  userInterjection = null;
2745
2789
  processing = false;
2790
+ disconnected = false;
2746
2791
  /** P4: active hub discussion orchestrator (web room), if any. */
2747
2792
  hubOrchestrator = null;
2748
2793
  /** P4b: pending between-rounds human-steer prompts (requestId → resolver). */
@@ -2769,9 +2814,13 @@ var SessionHandler = class {
2769
2814
  * 会话一旦保存到磁盘(有消息后),会从此 Map 中移除。
2770
2815
  */
2771
2816
  unsavedSessions = /* @__PURE__ */ new Map();
2817
+ // Failed saves survive a socket reconnect, scoped to the owning config directory.
2818
+ static recovery = /* @__PURE__ */ new Map();
2819
+ static acceptedChats = /* @__PURE__ */ new Map();
2772
2820
  constructor(ws, shared) {
2773
2821
  this.ws = ws;
2774
2822
  this.config = shared.config;
2823
+ this.unsavedSessions = new Map(_SessionHandler.recovery.get(this.config.getConfigDir()));
2775
2824
  this.costTracker = shared.costTracker;
2776
2825
  this.providers = shared.providers;
2777
2826
  this.sessions = shared.sessions;
@@ -2839,9 +2888,7 @@ var SessionHandler = class {
2839
2888
  * 接口之间的任何漂移(少一个成员、签名改了)都会当场编译失败,而不是等到运行时。
2840
2889
  */
2841
2890
  send(msg) {
2842
- if (this.ws.readyState === this.ws.OPEN) {
2843
- this.ws.send(JSON.stringify(msg));
2844
- }
2891
+ sendSocketMessage(this.ws, msg);
2845
2892
  }
2846
2893
  sendStatus() {
2847
2894
  const providerList = this.providers.listAvailable().map((p) => ({
@@ -2895,6 +2942,7 @@ var SessionHandler = class {
2895
2942
  });
2896
2943
  }
2897
2944
  async handleMessage(raw) {
2945
+ if (this.disconnected) return;
2898
2946
  let msg;
2899
2947
  try {
2900
2948
  msg = JSON.parse(raw);
@@ -2903,8 +2951,33 @@ var SessionHandler = class {
2903
2951
  return;
2904
2952
  }
2905
2953
  switch (msg.type) {
2906
- case "chat":
2954
+ case "chat": {
2955
+ if (msg.messageId) {
2956
+ if (typeof msg.messageId !== "string" || msg.messageId.length > 200) throw new Error("Invalid message ID");
2957
+ const scope = this.config.getConfigDir();
2958
+ const accepted = _SessionHandler.acceptedChats.get(scope) ?? /* @__PURE__ */ new Map();
2959
+ const prior = accepted.get(msg.messageId);
2960
+ if (prior) {
2961
+ this.send({ type: "chat_ack", messageId: msg.messageId, accepted: true, duplicate: true, sessionId: prior });
2962
+ return;
2963
+ }
2964
+ if (this.processing || typeof msg.content !== "string") {
2965
+ 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." });
2966
+ return;
2967
+ }
2968
+ const imageError = imageValidationError(msg.images);
2969
+ if (imageError) {
2970
+ this.send({ type: "chat_ack", messageId: msg.messageId, accepted: false, error: imageError });
2971
+ return;
2972
+ }
2973
+ this.ensureSession();
2974
+ accepted.set(msg.messageId, this.sessions.current.id);
2975
+ while (accepted.size > 1e3) accepted.delete(accepted.keys().next().value);
2976
+ _SessionHandler.acceptedChats.set(scope, accepted);
2977
+ this.send({ type: "chat_ack", messageId: msg.messageId, accepted: true, sessionId: this.sessions.current.id });
2978
+ }
2907
2979
  return this.handleChat(msg.content, msg.images);
2980
+ }
2908
2981
  case "command":
2909
2982
  return this.handleCommand(msg.name, msg.args);
2910
2983
  case "confirm_response":
@@ -2946,7 +3019,7 @@ var SessionHandler = class {
2946
3019
  case "hub_start":
2947
3020
  return this.handleHubStart(msg);
2948
3021
  case "hub_abort":
2949
- this.hubOrchestrator?.abort();
3022
+ this.cancelHub();
2950
3023
  return;
2951
3024
  case "hub_steer": {
2952
3025
  const resolve4 = this.pendingHubReview.get(msg.requestId);
@@ -2961,7 +3034,10 @@ var SessionHandler = class {
2961
3034
  return;
2962
3035
  }
2963
3036
  }
2964
- onDisconnect() {
3037
+ onDisconnect(closeSocket = true) {
3038
+ if (this.disconnected) return;
3039
+ this.disconnected = true;
3040
+ if (closeSocket && this.ws.readyState === this.ws.OPEN) this.ws.close(1001, "Connection closed; reconnect to restore session");
2965
3041
  runLifecycleHooks(mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()), "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
2966
3042
  this.toolExecutor.cancelAll();
2967
3043
  if (this.abortController) this.abortController.abort();
@@ -2969,18 +3045,24 @@ var SessionHandler = class {
2969
3045
  this.pendingAskUser.clear();
2970
3046
  for (const resolve4 of this.pendingAutoPause.values()) resolve4({ action: "stop" });
2971
3047
  this.pendingAutoPause.clear();
2972
- this.saveIfNeeded();
3048
+ this.cancelHub();
3049
+ void this.saveIfNeeded();
2973
3050
  if (this._bashSessionKey) clearBashSession(this._bashSessionKey);
2974
3051
  }
3052
+ cancelHub() {
3053
+ this.hubOrchestrator?.abort();
3054
+ for (const resolve4 of this.pendingHubReview.values()) resolve4({ action: "stop" });
3055
+ this.pendingHubReview.clear();
3056
+ }
2975
3057
  /** 根据当前模型 context window 更新工具输出截断上限 */
2976
3058
  updateContextWindow() {
2977
3059
  try {
2978
3060
  const cfgOutputCap = this.config.get("maxToolOutputChars");
2979
- if (typeof cfgOutputCap === "number" && cfgOutputCap > 0) setMaxOutputCap(cfgOutputCap);
2980
3061
  const provider = this.providers.get(this.currentProvider);
2981
3062
  const modelInfo = provider?.info.models.find((m) => m.id === this.currentModel);
2982
- if (modelInfo?.contextWindow) setContextWindow(modelInfo.contextWindow);
3063
+ this.toolExecutor.maxOutputChars = getMaxOutputChars(modelInfo?.contextWindow, cfgOutputCap ?? 0);
2983
3064
  } catch {
3065
+ this.toolExecutor.maxOutputChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
2984
3066
  }
2985
3067
  }
2986
3068
  /** 获取当前模型的 context window 大小(0 表示未知)*/
@@ -3032,12 +3114,24 @@ var SessionHandler = class {
3032
3114
  return total;
3033
3115
  }
3034
3116
  /** Save session only if it exists and has messages (never persist empty "Untitled" sessions). */
3035
- saveIfNeeded() {
3036
- if (this.sessions.current && this.sessions.current.messages.length > 0) {
3037
- autoTrimSessionIfNeeded(this.sessions.current);
3038
- const id = this.sessions.current.id;
3039
- this.sessions.save();
3040
- this.unsavedSessions.delete(id);
3117
+ async saveIfNeeded() {
3118
+ const session = this.sessions.current;
3119
+ if (!session?.messages.length) return;
3120
+ const scope = this.config.getConfigDir();
3121
+ try {
3122
+ if (session.dirty) autoTrimSessionIfNeeded(session);
3123
+ await this.sessions.save();
3124
+ if (session.dirty) return;
3125
+ this.unsavedSessions.delete(session.id);
3126
+ _SessionHandler.recovery.get(scope)?.delete(session.id);
3127
+ } catch (err) {
3128
+ this.unsavedSessions.set(session.id, session);
3129
+ const recovered = _SessionHandler.recovery.get(scope) ?? /* @__PURE__ */ new Map();
3130
+ recovered.set(session.id, session);
3131
+ _SessionHandler.recovery.set(scope, recovered);
3132
+ 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.`;
3133
+ this.send({ type: "info", message });
3134
+ process.stderr.write(message + "\n");
3041
3135
  }
3042
3136
  }
3043
3137
  /** Lazily create a session if none exists yet (deferred from constructor). */
@@ -3162,20 +3256,15 @@ var SessionHandler = class {
3162
3256
  const kinds = [...new Set(secretHits.map((h) => h.kind))].join(", ");
3163
3257
  this.send({ type: "info", message: "Possible secret in prompt (" + kinds + ")." });
3164
3258
  }
3165
- const ALLOWED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
3166
- const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
3259
+ const imageError = imageValidationError(images);
3260
+ if (imageError) {
3261
+ this.send({ type: "error", message: imageError });
3262
+ return;
3263
+ }
3167
3264
  let msgContent;
3168
3265
  if (images && images.length > 0) {
3169
3266
  const parts = [];
3170
3267
  for (const img of images) {
3171
- if (!ALLOWED_IMAGE_MIMES.has(img.mime)) {
3172
- this.send({ type: "error", message: `Rejected image: unsupported MIME type "${img.mime}"` });
3173
- return;
3174
- }
3175
- if (img.data && img.data.length > MAX_IMAGE_SIZE) {
3176
- this.send({ type: "error", message: `Rejected image: too large (${(img.data.length / 1024 / 1024).toFixed(1)} MB, max 10 MB)` });
3177
- return;
3178
- }
3179
3268
  parts.push({
3180
3269
  type: "image_url",
3181
3270
  image_url: { url: `data:${img.mime};base64,${img.data}` }
@@ -3229,7 +3318,7 @@ var SessionHandler = class {
3229
3318
  this.send({ type: "error", message });
3230
3319
  } finally {
3231
3320
  this.processing = false;
3232
- this.saveIfNeeded();
3321
+ await this.saveIfNeeded();
3233
3322
  this.sendStatus();
3234
3323
  this.sendSessionList();
3235
3324
  }
@@ -3746,6 +3835,10 @@ ${summaryContent}`,
3746
3835
  }
3747
3836
  // ── Commands ─────────────────────────────────────────────────────
3748
3837
  async handleCommand(name, args) {
3838
+ if (this.processing && ["session", "clear", "provider", "model", "rewind", "branch", "compact"].includes(name)) {
3839
+ this.send({ type: "info", message: "Stop the current request before changing sessions or conversation settings." });
3840
+ return;
3841
+ }
3749
3842
  const cliOnlyReason = CLI_ONLY_WEB_COMMANDS.get(name);
3750
3843
  if (cliOnlyReason) {
3751
3844
  this.send({
@@ -4374,7 +4467,7 @@ async function startWebServer(options = {}) {
4374
4467
  next();
4375
4468
  });
4376
4469
  const server = createServer(app);
4377
- const WS_MAX_PAYLOAD = 1 * 1024 * 1024;
4470
+ const WS_MAX_PAYLOAD = MAX_CHAT_BYTES;
4378
4471
  const WS_MSG_RATE_PER_SEC = 30;
4379
4472
  const WS_MSG_BURST = 60;
4380
4473
  const WS_PREAUTH_TIMEOUT_MS = 3e4;
@@ -4447,6 +4540,7 @@ async function startWebServer(options = {}) {
4447
4540
  app.get("/api/status", (_req, res) => {
4448
4541
  res.json({
4449
4542
  version: VERSION,
4543
+ chatLimits: { maxChatBytes: MAX_CHAT_BYTES, maxImageBytes: MAX_IMAGE_BYTES },
4450
4544
  providers: availableProviders.map((p) => ({
4451
4545
  id: p.info.id,
4452
4546
  displayName: p.info.displayName,
@@ -4655,11 +4749,6 @@ async function startWebServer(options = {}) {
4655
4749
  const tabId = params.get("tabId") || `tab-${Date.now()}`;
4656
4750
  const cookies = parseCookie(req.headers.cookie ?? "");
4657
4751
  const token = cookies["aicli_token"] || "";
4658
- const existing = handlers.get(tabId);
4659
- if (existing) {
4660
- existing.onDisconnect();
4661
- handlers.delete(tabId);
4662
- }
4663
4752
  let tokens = WS_MSG_BURST;
4664
4753
  let lastRefill = Date.now();
4665
4754
  const takeToken = () => {
@@ -4675,6 +4764,32 @@ async function startWebServer(options = {}) {
4675
4764
  };
4676
4765
  let authenticatedUser = null;
4677
4766
  let handler = null;
4767
+ let handlerKey = "";
4768
+ const attachHandler = (resources) => {
4769
+ if (handler) {
4770
+ handler.onDisconnect(false);
4771
+ if (handlers.get(handlerKey) === handler) handlers.delete(handlerKey);
4772
+ }
4773
+ handlerKey = JSON.stringify([authenticatedUser, tabId]);
4774
+ const existing = handlers.get(handlerKey);
4775
+ existing?.onDisconnect();
4776
+ const attached = new SessionHandler(ws, { ...resources, sessions: new SessionManager(resources.config) });
4777
+ handlers.set(handlerKey, attached);
4778
+ return attached;
4779
+ };
4780
+ let alive = true;
4781
+ ws.on("pong", () => {
4782
+ alive = true;
4783
+ });
4784
+ const heartbeat = setInterval(() => {
4785
+ if (!alive) {
4786
+ ws.terminate();
4787
+ return;
4788
+ }
4789
+ alive = false;
4790
+ if (ws.readyState === ws.OPEN) ws.ping();
4791
+ }, 3e4);
4792
+ heartbeat.unref();
4678
4793
  let preAuthTimer = setTimeout(() => {
4679
4794
  if (!handler && ws.readyState === ws.OPEN) {
4680
4795
  try {
@@ -4695,14 +4810,12 @@ async function startWebServer(options = {}) {
4695
4810
  const authRequired = authManager.isEnabled();
4696
4811
  if (!authRequired) {
4697
4812
  console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (no auth) (total: ${handlers.size + 1})`);
4698
- handler = new SessionHandler(ws, shared);
4699
- handlers.set(tabId, handler);
4813
+ handler = attachHandler(shared);
4700
4814
  clearPreAuthTimer();
4701
4815
  } else if (authenticatedUser) {
4702
4816
  console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (user: ${authenticatedUser}) (total: ${handlers.size + 1})`);
4703
4817
  const userShared = getUserShared(authenticatedUser);
4704
- handler = new SessionHandler(ws, userShared);
4705
- handlers.set(tabId, handler);
4818
+ handler = attachHandler(userShared);
4706
4819
  clearPreAuthTimer();
4707
4820
  } else {
4708
4821
  console.log(` \u23F3 Tab waiting auth: ${tabId.slice(0, 12)}`);
@@ -4747,8 +4860,7 @@ async function startWebServer(options = {}) {
4747
4860
  authManager.login(username, password);
4748
4861
  authenticatedUser = username;
4749
4862
  const userShared = getUserShared(username);
4750
- handler = new SessionHandler(ws, userShared);
4751
- handlers.set(tabId, handler);
4863
+ handler = attachHandler(userShared);
4752
4864
  clearPreAuthTimer();
4753
4865
  console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
4754
4866
  ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
@@ -4763,8 +4875,7 @@ async function startWebServer(options = {}) {
4763
4875
  }
4764
4876
  authenticatedUser = verifiedUser;
4765
4877
  const userShared = getUserShared(verifiedUser);
4766
- handler = new SessionHandler(ws, userShared);
4767
- handlers.set(tabId, handler);
4878
+ handler = attachHandler(userShared);
4768
4879
  clearPreAuthTimer();
4769
4880
  console.log(` \u2713 Token auth: ${verifiedUser} (tab: ${tabId.slice(0, 12)})`);
4770
4881
  ws.send(JSON.stringify({ type: "auth_result", success: true, username: verifiedUser }));
@@ -4778,8 +4889,7 @@ async function startWebServer(options = {}) {
4778
4889
  }
4779
4890
  authenticatedUser = username;
4780
4891
  const userShared = getUserShared(username);
4781
- handler = new SessionHandler(ws, userShared);
4782
- handlers.set(tabId, handler);
4892
+ handler = attachHandler(userShared);
4783
4893
  clearPreAuthTimer();
4784
4894
  console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
4785
4895
  ws.send(JSON.stringify({ type: "auth_result", success: true, username }));
@@ -4807,10 +4917,11 @@ async function startWebServer(options = {}) {
4807
4917
  }
4808
4918
  });
4809
4919
  ws.on("close", () => {
4920
+ clearInterval(heartbeat);
4810
4921
  clearPreAuthTimer();
4811
- const wasTracked = handlers.has(tabId);
4922
+ const wasTracked = handlers.get(handlerKey) === handler;
4812
4923
  if (handler) handler.onDisconnect();
4813
- if (wasTracked) handlers.delete(tabId);
4924
+ if (wasTracked) handlers.delete(handlerKey);
4814
4925
  console.log(` \u2717 Tab disconnected: ${tabId.slice(0, 12)} (total: ${handlers.size})`);
4815
4926
  });
4816
4927
  ws.on("error", (err) => {
@@ -5,8 +5,9 @@ import {
5
5
  loadIndex,
6
6
  removeFile,
7
7
  saveIndex,
8
+ symbolFingerprint,
8
9
  upsertFileSymbols
9
- } from "./chunk-CKH4KQ4E.js";
10
+ } from "./chunk-WQ3OMEC4.js";
10
11
  import "./chunk-IW3Q7AE5.js";
11
12
  export {
12
13
  clearIndex,
@@ -14,5 +15,6 @@ export {
14
15
  loadIndex,
15
16
  removeFile,
16
17
  saveIndex,
18
+ symbolFingerprint,
17
19
  upsertFileSymbols
18
20
  };