@fangyb/ahchat-bridge 0.1.35 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -91836,6 +91836,8 @@ var DEFAULT_QUERY_CONFIG = {
91836
91836
  maxActive: 5040,
91837
91837
  idleTimeoutMs: 6e5,
91838
91838
  workingSilenceTimeoutMs: 12e5,
91839
+ replyStallTimeoutMs: 3e5,
91840
+ busySilenceTimeoutMs: 18e5,
91839
91841
  evictionIntervalMs: 6e4,
91840
91842
  statusReportIntervalMs: 6e4,
91841
91843
  allowBuiltinWebSearch: false,
@@ -91893,6 +91895,14 @@ function mergeQueryConfig(file2) {
91893
91895
  "AHCHAT_BRIDGE_WORKING_SILENCE_TIMEOUT_MS",
91894
91896
  q?.workingSilenceTimeoutMs ?? DEFAULT_QUERY_CONFIG.workingSilenceTimeoutMs
91895
91897
  ),
91898
+ replyStallTimeoutMs: readEnvInt(
91899
+ "AHCHAT_BRIDGE_REPLY_STALL_TIMEOUT_MS",
91900
+ q?.replyStallTimeoutMs ?? DEFAULT_QUERY_CONFIG.replyStallTimeoutMs
91901
+ ),
91902
+ busySilenceTimeoutMs: readEnvInt(
91903
+ "AHCHAT_BRIDGE_BUSY_SILENCE_TIMEOUT_MS",
91904
+ q?.busySilenceTimeoutMs ?? DEFAULT_QUERY_CONFIG.busySilenceTimeoutMs ?? 18e5
91905
+ ),
91896
91906
  evictionIntervalMs: readEnvInt(
91897
91907
  "AHCHAT_BRIDGE_EVICTION_INTERVAL_MS",
91898
91908
  q?.evictionIntervalMs ?? DEFAULT_QUERY_CONFIG.evictionIntervalMs
@@ -91973,7 +91983,11 @@ function loadBridgeConfig(opts) {
91973
91983
  ) || null,
91974
91984
  logUploadIntervalMs: readEnvInt(
91975
91985
  "AHCHAT_LOG_UPLOAD_INTERVAL_MS",
91976
- fileConfig.logUploadIntervalMs ?? 24 * 60 * 60 * 1e3
91986
+ // Flush every 60s instead of once a day. Daily flushing let logs pile up for hours,
91987
+ // then dumped tens of thousands of entries in one cycle on the next process start,
91988
+ // blowing past the server's per-minute upload quota (3000 entries / 3MB) and getting
91989
+ // 429'd. Small frequent batches stay well under that ceiling.
91990
+ fileConfig.logUploadIntervalMs ?? 6e4
91977
91991
  ),
91978
91992
  queryConfig: mergeQueryConfig(fileConfig)
91979
91993
  };
@@ -92709,9 +92723,34 @@ ${entry.error.stack}` : ""}`
92709
92723
  return `${ts} ${level} ${scope} ${entry.msg}${data}${trace}${errPart}`;
92710
92724
  };
92711
92725
 
92726
+ // ../logger/src/fallback.ts
92727
+ init_cjs_shims();
92728
+ function logFallback(logger45, event) {
92729
+ const payload = {
92730
+ ...event.traceId ? { traceId: event.traceId } : {},
92731
+ fallback: {
92732
+ fallbackId: event.fallbackId,
92733
+ type: event.type,
92734
+ phase: event.phase,
92735
+ expected: event.expected,
92736
+ ...event.context ? { context: event.context } : {},
92737
+ ...event.outcome ? { outcome: event.outcome } : {}
92738
+ }
92739
+ };
92740
+ const msg = `[FALLBACK] ${event.type}:${event.phase}`;
92741
+ const useDebug = event.expected && event.phase !== "outcome";
92742
+ if (useDebug) {
92743
+ logger45.debug(msg, payload);
92744
+ } else {
92745
+ logger45.warn(msg, payload);
92746
+ }
92747
+ }
92748
+
92712
92749
  // ../logger/src/transports/console.ts
92713
92750
  init_cjs_shims();
92714
92751
  var protectedStreams = /* @__PURE__ */ new WeakSet();
92752
+ function ignoreWriteError(_error) {
92753
+ }
92715
92754
  function defaultStream(kind) {
92716
92755
  const maybeGlobal = globalThis;
92717
92756
  return maybeGlobal.process?.[kind];
@@ -92724,12 +92763,13 @@ function safeWriteLine(stream, line, fallback) {
92724
92763
  if (stream.destroyed || stream.writableEnded) return;
92725
92764
  if (typeof stream === "object" && typeof stream.on === "function" && !protectedStreams.has(stream)) {
92726
92765
  protectedStreams.add(stream);
92727
- stream.on("error", () => void 0);
92766
+ stream.on("error", ignoreWriteError);
92728
92767
  }
92729
92768
  try {
92730
92769
  stream.write(`${line}
92731
- `, () => void 0);
92732
- } catch {
92770
+ `, ignoreWriteError);
92771
+ } catch (e) {
92772
+ ignoreWriteError(e);
92733
92773
  }
92734
92774
  }
92735
92775
  function consoleTransport(opts) {
@@ -93364,8 +93404,31 @@ function parseSize(maxSize) {
93364
93404
  return trimmed;
93365
93405
  }
93366
93406
  var streamCache = /* @__PURE__ */ new Map();
93407
+ var droppedEntryCount = 0;
93408
+ var lastReportedDroppedTotal = 0;
93409
+ function buildLogDroppedSentinel(fmt, source, droppedTotal) {
93410
+ const entry = {
93411
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
93412
+ level: "WARN",
93413
+ source,
93414
+ module: "logger.file",
93415
+ msg: "log_dropped",
93416
+ data: { droppedTotal }
93417
+ };
93418
+ return fmt(entry);
93419
+ }
93420
+ function writeWithDroppedSentinel(stream, fmt, line, source) {
93421
+ stream.write(`${line}
93422
+ `);
93423
+ if (droppedEntryCount > lastReportedDroppedTotal) {
93424
+ lastReportedDroppedTotal = droppedEntryCount;
93425
+ stream.write(`${buildLogDroppedSentinel(fmt, source, droppedEntryCount)}
93426
+ `);
93427
+ }
93428
+ }
93367
93429
  function fileTransport(opts) {
93368
93430
  const fmt = opts.formatter ?? jsonFormatter;
93431
+ const logSource = opts.source ?? "server";
93369
93432
  const resolved = import_node_path2.default.resolve(opts.path);
93370
93433
  let cached2 = streamCache.get(resolved);
93371
93434
  if (!cached2) {
@@ -93380,11 +93443,14 @@ function fileTransport(opts) {
93380
93443
  streamCache.set(resolved, cached2);
93381
93444
  }
93382
93445
  return (entry) => {
93383
- if (cached2.closed || cached2.stream.destroyed || cached2.stream.writableEnded) return;
93446
+ if (cached2.closed || cached2.stream.destroyed || cached2.stream.writableEnded) {
93447
+ droppedEntryCount += 1;
93448
+ return;
93449
+ }
93384
93450
  try {
93385
- cached2.stream.write(`${fmt(entry)}
93386
- `);
93451
+ writeWithDroppedSentinel(cached2.stream, fmt, fmt(entry), logSource);
93387
93452
  } catch {
93453
+ droppedEntryCount += 1;
93388
93454
  }
93389
93455
  };
93390
93456
  }
@@ -93770,8 +93836,10 @@ function resolvePnpmRuntimeBinary(sdkDir, target) {
93770
93836
  } catch {
93771
93837
  return void 0;
93772
93838
  }
93839
+ const matches = [];
93773
93840
  for (const entry of entries) {
93774
93841
  if (!entry.startsWith(`${encodedName}@`)) continue;
93842
+ const version4 = entry.slice(encodedName.length + 1);
93775
93843
  const candidate = import_node_path5.default.join(
93776
93844
  pnpmStoreDir,
93777
93845
  entry,
@@ -93779,9 +93847,22 @@ function resolvePnpmRuntimeBinary(sdkDir, target) {
93779
93847
  ...target.packageName.split("/"),
93780
93848
  target.binaryName
93781
93849
  );
93782
- if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
93850
+ if ((0, import_node_fs3.existsSync)(candidate)) matches.push({ version: version4, candidate });
93783
93851
  }
93784
- return void 0;
93852
+ if (matches.length === 0) return void 0;
93853
+ matches.sort((a, b) => compareRuntimeVersion(b.version, a.version));
93854
+ return matches[0].candidate;
93855
+ }
93856
+ function compareRuntimeVersion(a, b) {
93857
+ const parse3 = (v) => v.split(/[.+_-]/).map((n) => Number.parseInt(n, 10));
93858
+ const pa = parse3(a);
93859
+ const pb = parse3(b);
93860
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
93861
+ const da = Number.isFinite(pa[i]) ? pa[i] : 0;
93862
+ const db = Number.isFinite(pb[i]) ? pb[i] : 0;
93863
+ if (da !== db) return da - db;
93864
+ }
93865
+ return 0;
93785
93866
  }
93786
93867
  function resolveSdkRuntimeBinary(target) {
93787
93868
  const directPath = safeResolve(`${target.packageName}/${target.binaryName}`);
@@ -94683,6 +94764,9 @@ function createMessageId() {
94683
94764
  function createTraceId() {
94684
94765
  return `tr_${Date.now().toString(36)}_${nanoid(6)}`;
94685
94766
  }
94767
+ function createFallbackId() {
94768
+ return `flb_${Date.now().toString(36)}_${nanoid(6)}`;
94769
+ }
94686
94770
  function createRequestId() {
94687
94771
  return `req_${Date.now().toString(36)}_${nanoid(6)}`;
94688
94772
  }
@@ -94692,6 +94776,12 @@ function createCronReplyMessageId() {
94692
94776
  function createInboxFlushReplyMessageId() {
94693
94777
  return `msg_inbox_${Date.now().toString(36)}_${nanoid(6)}`;
94694
94778
  }
94779
+ function createNeuralSendReplyMessageId() {
94780
+ return `msg_nsend_${Date.now().toString(36)}_${nanoid(6)}`;
94781
+ }
94782
+ function createScopeNoticeReplyMessageId() {
94783
+ return `msg_scopenotice_${Date.now().toString(36)}_${nanoid(6)}`;
94784
+ }
94695
94785
  function createCronTraceId() {
94696
94786
  return `tr_cron_${Date.now().toString(36)}_${nanoid(6)}`;
94697
94787
  }
@@ -94750,6 +94840,10 @@ function assertStringPayloadOneOf(type, payload, field, allowed) {
94750
94840
  throw invalidWsMessage(type, field);
94751
94841
  }
94752
94842
  }
94843
+ function assertOptionalStringPayloadOneOf(type, payload, field, allowed) {
94844
+ if (payload[field] === void 0) return;
94845
+ assertStringPayloadOneOf(type, payload, field, allowed);
94846
+ }
94753
94847
  function assertArrayPayloadField(type, payload, field) {
94754
94848
  if (!Array.isArray(payload[field])) {
94755
94849
  throw invalidWsMessage(type, field);
@@ -94830,6 +94924,7 @@ function validateWSMessageShape(msg) {
94830
94924
  case "agent:error": {
94831
94925
  assertPayloadRecord(type, payload);
94832
94926
  validateRequiredStrings(type, payload, ["ackId", "agentId", "conversationId", "error", "traceId"]);
94927
+ assertOptionalStringPayloadOneOf(type, payload, "reason", ["user_quota_exceeded", "company_quota_exceeded"]);
94833
94928
  return;
94834
94929
  }
94835
94930
  case "directory:register": {
@@ -96209,6 +96304,9 @@ init_cjs_shims();
96209
96304
  // ../shared/src/utils/serverUrl.ts
96210
96305
  init_cjs_shims();
96211
96306
 
96307
+ // ../shared/src/utils/phone.ts
96308
+ init_cjs_shims();
96309
+
96212
96310
  // ../shared/src/utils/mediaPreviewHtml.ts
96213
96311
  init_cjs_shims();
96214
96312
 
@@ -96454,6 +96552,60 @@ var readpageScrapeTool = {
96454
96552
  enabled: true,
96455
96553
  permissionPolicy: "always_allow"
96456
96554
  };
96555
+ var seedreamGenerateImageTool = {
96556
+ name: "generate_image",
96557
+ displayName: "Seedream \u751F\u56FE",
96558
+ description: "\u4F7F\u7528\u706B\u5C71\u65B9\u821F Seedream \u751F\u6210\u5355\u5F20\u56FE\u7247\uFF0C\u652F\u6301\u6587\u672C\u751F\u56FE\u4E0E\u53C2\u8003\u56FE\u751F\u56FE\uFF0C\u4F1A\u4EA7\u751F\u516C\u53F8 Ark \u8D26\u53F7\u7528\u91CF\u3002",
96559
+ category: "media",
96560
+ riskLevel: "medium",
96561
+ enabled: true,
96562
+ permissionPolicy: "always_ask"
96563
+ };
96564
+ var seedreamEditImageTool = {
96565
+ name: "edit_image",
96566
+ displayName: "Seedream \u6539\u56FE",
96567
+ description: "\u4F7F\u7528\u706B\u5C71\u65B9\u821F Seedream \u6839\u636E\u53C2\u8003\u56FE\u548C\u63D0\u793A\u8BCD\u7F16\u8F91\u6216\u91CD\u7ED8\u5355\u5F20\u56FE\u7247\uFF0C\u4F1A\u4EA7\u751F\u516C\u53F8 Ark \u8D26\u53F7\u7528\u91CF\u3002",
96568
+ category: "media",
96569
+ riskLevel: "medium",
96570
+ enabled: true,
96571
+ permissionPolicy: "always_ask"
96572
+ };
96573
+ var seedreamGenerateImageGroupTool = {
96574
+ name: "generate_image_group",
96575
+ displayName: "Seedream \u5355\u56FE",
96576
+ description: "\u517C\u5BB9\u65E7\u540D\u79F0\u7684 Seedream \u5355\u56FE\u751F\u6210\u5DE5\u5177\uFF0C\u4F1A\u4EA7\u751F\u516C\u53F8 Ark \u8D26\u53F7\u7528\u91CF\u3002\u5F53\u524D\u6BCF\u6B21\u8BF7\u6C42\u53EA\u751F\u6210 1 \u5F20\u56FE\u7247\u3002",
96577
+ category: "media",
96578
+ riskLevel: "medium",
96579
+ enabled: true,
96580
+ permissionPolicy: "always_ask"
96581
+ };
96582
+ var seedanceUsageGuideTool = {
96583
+ name: "seedance_usage_guide",
96584
+ displayName: "Seedance \u4F7F\u7528\u8BF4\u660E",
96585
+ description: "\u67E5\u770B Seedance \u89C6\u9891\u751F\u6210\u6D41\u7A0B\u4E0E\u53C2\u6570\u8BF4\u660E\uFF0C\u4E0D\u521B\u5EFA\u65B0\u7684\u751F\u6210\u4EFB\u52A1\u3002",
96586
+ category: "media",
96587
+ riskLevel: "low",
96588
+ enabled: true,
96589
+ permissionPolicy: "always_allow"
96590
+ };
96591
+ var seedanceCreateTaskTool = {
96592
+ name: "seedance_create_task",
96593
+ displayName: "Seedance \u751F\u89C6\u9891",
96594
+ description: "\u4F7F\u7528\u706B\u5C71\u65B9\u821F Seedance \u521B\u5EFA\u5355\u4E2A\u89C6\u9891\u751F\u6210\u4EFB\u52A1\uFF0C\u4F1A\u4EA7\u751F\u516C\u53F8 Ark \u8D26\u53F7\u7528\u91CF\u3002\u5F53\u524D\u6BCF\u6B21\u8BF7\u6C42\u53EA\u751F\u6210 1 \u4E2A\u89C6\u9891\u3002",
96595
+ category: "media",
96596
+ riskLevel: "high",
96597
+ enabled: true,
96598
+ permissionPolicy: "always_ask"
96599
+ };
96600
+ var seedanceCheckTaskTool = {
96601
+ name: "seedance_check_task",
96602
+ displayName: "Seedance \u67E5\u7ED3\u679C",
96603
+ description: "\u67E5\u8BE2 Seedance \u89C6\u9891\u751F\u6210\u4EFB\u52A1\u72B6\u6001\u4E0E\u89C6\u9891 URL\uFF0C\u4E0D\u521B\u5EFA\u65B0\u7684\u751F\u6210\u4EFB\u52A1\u3002",
96604
+ category: "media",
96605
+ riskLevel: "low",
96606
+ enabled: true,
96607
+ permissionPolicy: "always_allow"
96608
+ };
96457
96609
  var context7ResolveLibraryTool = {
96458
96610
  name: "resolve-library-id",
96459
96611
  displayName: "\u89E3\u6790\u6587\u6863\u5E93",
@@ -96760,8 +96912,56 @@ var ALIYUN_IQS_MCP_PROVIDERS = [
96760
96912
  tools: [readpageBasicTool, readpageScrapeTool]
96761
96913
  }
96762
96914
  ];
96915
+ var VOLCENGINE_SEEDREAM_MCP_PROVIDER = {
96916
+ providerId: "volcengine_seedream",
96917
+ name: "Volcengine Seedream",
96918
+ summary: "\u706B\u5C71\u65B9\u821F Seedream \u56FE\u7247\u751F\u6210/\u7F16\u8F91 MCP\uFF0C\u4F7F\u7528\u516C\u53F8\u7EDF\u4E00 Ark Key\uFF0C\u5E76\u8FDB\u5165 AHChat \u8C03\u7528\u5BA1\u8BA1\u3002",
96919
+ serverName: "seedream",
96920
+ transport: "stdio",
96921
+ url: null,
96922
+ command: "ahchat-builtin",
96923
+ args: ["seedream-mcp"],
96924
+ env: {
96925
+ ARK_API_KEY: "",
96926
+ ARK_BASE_URL: "https://ark.cn-beijing.volces.com/api/v3"
96927
+ },
96928
+ authType: "x_api_key",
96929
+ customHeaders: [],
96930
+ enabled: true,
96931
+ alwaysLoad: true,
96932
+ tools: [
96933
+ seedreamGenerateImageTool,
96934
+ seedreamEditImageTool,
96935
+ seedreamGenerateImageGroupTool
96936
+ ]
96937
+ };
96938
+ var VOLCENGINE_SEEDANCE_MCP_PROVIDER = {
96939
+ providerId: "volcengine_seedance",
96940
+ name: "Volcengine Seedance",
96941
+ summary: "\u706B\u5C71\u65B9\u821F Seedance \u89C6\u9891\u751F\u6210 MCP\uFF0C\u4F7F\u7528\u516C\u53F8\u7EDF\u4E00 Ark Key\uFF0C\u5E76\u8FDB\u5165 AHChat \u8C03\u7528\u5BA1\u8BA1\u3002",
96942
+ serverName: "seedance",
96943
+ transport: "stdio",
96944
+ url: null,
96945
+ command: "ahchat-builtin",
96946
+ args: ["seedance-mcp"],
96947
+ env: {
96948
+ ARK_API_KEY: "",
96949
+ ARK_BASE_URL: "https://ark.cn-beijing.volces.com/api/v3"
96950
+ },
96951
+ authType: "x_api_key",
96952
+ customHeaders: [],
96953
+ enabled: true,
96954
+ alwaysLoad: true,
96955
+ tools: [
96956
+ seedanceUsageGuideTool,
96957
+ seedanceCreateTaskTool,
96958
+ seedanceCheckTaskTool
96959
+ ]
96960
+ };
96763
96961
  var OFFICIAL_MCP_PROVIDERS = [
96764
- ...ALIYUN_IQS_MCP_PROVIDERS
96962
+ ...ALIYUN_IQS_MCP_PROVIDERS,
96963
+ VOLCENGINE_SEEDREAM_MCP_PROVIDER,
96964
+ VOLCENGINE_SEEDANCE_MCP_PROVIDER
96765
96965
  ];
96766
96966
  var MCP_STORE_PROVIDERS = [
96767
96967
  {
@@ -97573,8 +97773,7 @@ var AskQuestionRegistry = class {
97573
97773
  questionId,
97574
97774
  agentId: entry.agentId,
97575
97775
  waitedMs: Date.now() - entry.askedAt,
97576
- answerLen: answerText2.length,
97577
- answerSample: answerText2.slice(0, 200)
97776
+ answerLen: answerText2.length
97578
97777
  });
97579
97778
  entry.resolve(answerText2);
97580
97779
  return true;
@@ -97719,7 +97918,7 @@ function makeAskUserQuestionGuard(deps) {
97719
97918
  bundleIndex,
97720
97919
  bundleSize,
97721
97920
  replyMessageId: task.replyMessageId,
97722
- question: q.question.slice(0, 200),
97921
+ questionLen: q.question.length,
97723
97922
  optionCount: options.length,
97724
97923
  multiSelect,
97725
97924
  traceId: task.traceId
@@ -97818,7 +98017,7 @@ function makeAskUserQuestionGuard(deps) {
97818
98017
  bundleId,
97819
98018
  bundleSize,
97820
98019
  replyMessageId: task.replyMessageId,
97821
- combinedSample: combined.slice(0, 200),
98020
+ combinedLen: combined.length,
97822
98021
  traceId: task.traceId
97823
98022
  });
97824
98023
  return { behavior: "deny", message: combined };
@@ -112803,6 +113002,17 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
112803
113002
  ".yaml",
112804
113003
  ".yml"
112805
113004
  ]);
113005
+ var OFFICE_DOCUMENT_EXTENSIONS = /* @__PURE__ */ new Set([
113006
+ ".docx",
113007
+ ".xlsx",
113008
+ ".pptx",
113009
+ ".pdf",
113010
+ ".odt",
113011
+ ".ods",
113012
+ ".odp",
113013
+ ".rtf"
113014
+ ]);
113015
+ var PLAIN_TEXT_BINARY_SNIFF_BYTES = 8192;
112806
113016
  var DEFAULT_MAX_CHARS = 5e5;
112807
113017
  var DEFAULT_TIMEOUT_MS = 45e3;
112808
113018
  function isReadableDocumentPath(filePath) {
@@ -112822,9 +113032,6 @@ function resolveDocumentPath(inputPath, cwd) {
112822
113032
  async function readDocumentAsMarkdown(inputPath, opts = {}) {
112823
113033
  const resolvedPath = opts.cwd ? resolveDocumentPath(inputPath, opts.cwd) : import_node_path10.default.resolve(resolveUserPath(inputPath));
112824
113034
  const ext = import_node_path10.default.extname(resolvedPath).toLowerCase();
112825
- if (!isReadableDocumentPath(resolvedPath)) {
112826
- throw new Error(`unsupported document type: ${ext || "(no extension)"}`);
112827
- }
112828
113035
  const stat3 = await import_promises2.default.stat(resolvedPath);
112829
113036
  if (!stat3.isFile()) throw new Error("path is not a file");
112830
113037
  const warnings = [];
@@ -112833,8 +113040,10 @@ async function readDocumentAsMarkdown(inputPath, opts = {}) {
112833
113040
  markdown = await import_promises2.default.readFile(resolvedPath, "utf-8");
112834
113041
  } else if (ext === ".xls") {
112835
113042
  markdown = await convertLegacyExcelDocument(resolvedPath);
112836
- } else {
113043
+ } else if (OFFICE_DOCUMENT_EXTENSIONS.has(ext)) {
112837
113044
  markdown = await convertOfficeDocument(resolvedPath, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
113045
+ } else {
113046
+ markdown = await readPlainTextDocument(resolvedPath, ext);
112838
113047
  }
112839
113048
  markdown = normalizeDocumentText(markdown);
112840
113049
  const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
@@ -112973,6 +113182,14 @@ async function convertDocxWithOfficeCli(filePath, timeoutMs) {
112973
113182
  });
112974
113183
  return text;
112975
113184
  }
113185
+ async function readPlainTextDocument(filePath, ext) {
113186
+ const buffer = await import_promises2.default.readFile(filePath);
113187
+ const sample = buffer.subarray(0, Math.min(buffer.length, PLAIN_TEXT_BINARY_SNIFF_BYTES));
113188
+ if (sample.includes(0)) {
113189
+ throw new Error(`unsupported document type: ${ext || "(no extension)"}`);
113190
+ }
113191
+ return buffer.toString("utf-8");
113192
+ }
112976
113193
  async function convertLegacyExcelDocument(filePath) {
112977
113194
  const XLSX2 = await Promise.resolve().then(() => (init_xlsx(), xlsx_exports));
112978
113195
  const workbook = XLSX2.readFile(filePath, { cellDates: true });
@@ -113442,8 +113659,7 @@ async function createNeuralMcpServer(deps) {
113442
113659
  agentId: deps.agentId,
113443
113660
  fromScope: currentScopeKey,
113444
113661
  rawTargetScope: args.target_scope,
113445
- messageLen: args.message.length,
113446
- messageSample: args.message.slice(0, 120)
113662
+ messageLen: args.message.length
113447
113663
  });
113448
113664
  const trimmed = args.message.trim();
113449
113665
  if (!trimmed) {
@@ -113537,7 +113753,7 @@ async function createNeuralMcpServer(deps) {
113537
113753
  toScope: resolvedKey,
113538
113754
  repeatsInWindow: sendHistory.length,
113539
113755
  windowMs: NEURAL_DEDUP_WINDOW_MS,
113540
- messageSample: trimmed.slice(0, 120)
113756
+ messageLen: trimmed.length
113541
113757
  });
113542
113758
  return {
113543
113759
  content: [{
@@ -113564,8 +113780,7 @@ async function createNeuralMcpServer(deps) {
113564
113780
  agentId: deps.agentId,
113565
113781
  fromScope: currentScopeKey,
113566
113782
  toScope: resolvedKey,
113567
- messageLen: trimmed.length,
113568
- messageSample: trimmed.slice(0, 120)
113783
+ messageLen: trimmed.length
113569
113784
  });
113570
113785
  return {
113571
113786
  content: [{ type: "text", text: `[neural_send] \u5DF2\u9001\u8FBE\u5230\u300C${toLabel}\u300D(scope: ${resolvedKey})\u3002` }]
@@ -113922,6 +114137,7 @@ action="append" \u8FFD\u52A0\u65B0\u5185\u5BB9\uFF08\u6700\u5E38\u7528\uFF0Ccont
113922
114137
  `Read a document from the current working directory and return extracted Markdown text.
113923
114138
  Use this instead of the built-in Read tool for binary documents such as .docx, .xls, .xlsx, .pptx, .pdf, .odt, .ods, .odp, or .rtf.
113924
114139
  Also supports text-like files such as .csv, .md, .txt, .json, .xml, .yaml, and .html.
114140
+ Plain-text and source/config files (e.g. .py, .ts, Makefile, Dockerfile, and other extension-less text files) are read as-is; only true binaries are rejected.
113925
114141
  Pass either a relative path from the current working directory or an absolute path inside it.`,
113926
114142
  {
113927
114143
  path: external_exports.string().min(1).describe("Document path, relative to the current working directory or absolute inside it."),
@@ -116985,6 +117201,9 @@ function emitUsageReported(proc, emit, base, usage, messageId) {
116985
117201
  function isGroupTask(proc) {
116986
117202
  return proc.currentTask?.groupId != null;
116987
117203
  }
117204
+ function shouldStreamInternals(proc) {
117205
+ return !isGroupTask(proc) || proc.spectating === true;
117206
+ }
116988
117207
  function extractTodosFromInput(input) {
116989
117208
  if (!input || typeof input !== "object") return null;
116990
117209
  const raw = input.todos;
@@ -117116,7 +117335,6 @@ function emitGroupSegment(proc, emit, base, content, contentBlocks, isSilent = f
117116
117335
  contentLen: content.length,
117117
117336
  blockCount: contentBlocks.length,
117118
117337
  blockTypes: contentBlocks.map((b) => b.type),
117119
- contentSample: content.slice(0, 200),
117120
117338
  traceId: base.traceId,
117121
117339
  isAuditOnly: content.length === 0,
117122
117340
  isSilent
@@ -117172,9 +117390,24 @@ function flushTextSegmentOnBlockStop(proc, emit, base) {
117172
117390
  }
117173
117391
  proc.segmentBuffer = "";
117174
117392
  }
117393
+ function describeSdkEvent(message) {
117394
+ const rec = message;
117395
+ const str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
117396
+ return {
117397
+ type: str(rec.type) ?? "unknown",
117398
+ subtype: str(rec.subtype),
117399
+ toolName: str(rec.last_tool_name),
117400
+ subagentType: str(rec.subagent_type),
117401
+ toolUseId: str(rec.tool_use_id),
117402
+ taskId: str(rec.task_id),
117403
+ at: Date.now()
117404
+ };
117405
+ }
117175
117406
  function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProviderApiError) {
117176
117407
  const emit = rawEmit;
117177
117408
  proc.lastSdkEventAt = Date.now();
117409
+ proc.lastSdkEventInfo = describeSdkEvent(message);
117410
+ proc.stallWarned = false;
117178
117411
  switch (message.type) {
117179
117412
  case "system": {
117180
117413
  const sysMsg = message;
@@ -117213,11 +117446,29 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117213
117446
  sessionId: proc.ccSessionId
117214
117447
  });
117215
117448
  } else {
117449
+ const sysRec = sysMsg;
117450
+ const pick2 = (k) => typeof sysRec[k] === "string" || typeof sysRec[k] === "number" ? sysRec[k] : void 0;
117451
+ const descriptionLen = typeof sysRec.description === "string" ? sysRec.description.length : void 0;
117452
+ const subagentTaskId = typeof sysRec.task_id === "string" ? sysRec.task_id : void 0;
117453
+ if (subagentTaskId) {
117454
+ if (sysMsg.subtype === "task_started") {
117455
+ (proc.activeSubagentTaskIds ??= /* @__PURE__ */ new Set()).add(subagentTaskId);
117456
+ } else if (sysMsg.subtype === "task_notification") {
117457
+ proc.activeSubagentTaskIds?.delete(subagentTaskId);
117458
+ }
117459
+ }
117216
117460
  logger11.info("SDK system subtype unhandled", {
117217
117461
  agentId: proc.agentId,
117218
117462
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
117219
117463
  subtype: sysMsg.subtype ?? "(none)",
117220
- keys: Object.keys(sysMsg).slice(0, 12)
117464
+ taskId: pick2("task_id"),
117465
+ toolUseId: pick2("tool_use_id"),
117466
+ subagentType: pick2("subagent_type"),
117467
+ taskType: pick2("task_type"),
117468
+ lastToolName: pick2("last_tool_name"),
117469
+ hasDescription: descriptionLen != null,
117470
+ descriptionLen,
117471
+ keys: Object.keys(sysMsg).slice(0, 16)
117221
117472
  });
117222
117473
  }
117223
117474
  break;
@@ -117243,13 +117494,14 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117243
117494
  } else if (block.type === "tool_use") {
117244
117495
  proc.currentBlockType = "tool_use";
117245
117496
  proc.currentToolName = block.name ?? "unknown";
117497
+ proc.activeToolUseStartedAt = Date.now();
117246
117498
  proc.accumulatedToolInput = "";
117247
117499
  const toolName = block.name ?? "unknown";
117248
117500
  proc.suppressCurrentToolUse = proc.officialMediaGenerationSatisfied === true && isOfficialMediaGenerationToolName(toolName);
117249
117501
  const isMcpTool = parseMcpRuntimeToolName(toolName) != null;
117250
117502
  proc.currentMcpInvocationId = isMcpTool ? createMcpToolInvocationId() : null;
117251
117503
  proc.currentMcpInvocationStartedAt = isMcpTool ? (/* @__PURE__ */ new Date()).toISOString() : null;
117252
- if (!isGroupTask(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
117504
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
117253
117505
  emit({
117254
117506
  type: "agent:tool_use",
117255
117507
  payload: {
@@ -117274,7 +117526,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117274
117526
  if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
117275
117527
  if (proc.suppressCurrentThinking) break;
117276
117528
  proc.accumulatedThinking += delta.thinking;
117277
- if (!isGroupTask(proc)) {
117529
+ if (shouldStreamInternals(proc)) {
117278
117530
  emit({
117279
117531
  type: "agent:thinking_chunk",
117280
117532
  payload: { ...wireBase(base), chunk: delta.thinking }
@@ -117285,7 +117537,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117285
117537
  if (typeof partial2 === "string") {
117286
117538
  proc.accumulatedToolInput += partial2;
117287
117539
  const liveInput = extractLiveToolInput(proc.currentToolName, proc.accumulatedToolInput);
117288
- if (!isGroupTask(proc) && liveInput && proc.currentToolName != null) {
117540
+ if (shouldStreamInternals(proc) && liveInput && proc.currentToolName != null) {
117289
117541
  emit({
117290
117542
  type: "agent:tool_input_update",
117291
117543
  payload: {
@@ -117319,7 +117571,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117319
117571
  }
117320
117572
  case "content_block_stop": {
117321
117573
  if (proc.currentBlockType === "thinking") {
117322
- if (!isGroupTask(proc) && !proc.suppressCurrentThinking) {
117574
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentThinking) {
117323
117575
  emit({
117324
117576
  type: "agent:thinking_done",
117325
117577
  payload: wireBase(getTaskBase(proc))
@@ -117344,8 +117596,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117344
117596
  error: error51,
117345
117597
  agentId: proc.agentId,
117346
117598
  toolName: proc.currentToolName,
117347
- inputLen: proc.accumulatedToolInput.length,
117348
- sample: proc.accumulatedToolInput.slice(0, 200)
117599
+ inputLen: proc.accumulatedToolInput.length
117349
117600
  });
117350
117601
  }
117351
117602
  }
@@ -117354,7 +117605,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117354
117605
  if (lastToolUse && lastToolUse.type === "tool_use") {
117355
117606
  lastToolUse.input = parsedInput;
117356
117607
  }
117357
- if (!isGroupTask(proc) && proc.currentToolName != null && LIVE_INPUT_PREVIEW_TOOLS.has(proc.currentToolName) && Object.keys(parsedInput).length > 0) {
117608
+ if (shouldStreamInternals(proc) && proc.currentToolName != null && LIVE_INPUT_PREVIEW_TOOLS.has(proc.currentToolName) && Object.keys(parsedInput).length > 0) {
117358
117609
  emit({
117359
117610
  type: "agent:tool_input_update",
117360
117611
  payload: {
@@ -117478,7 +117729,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117478
117729
  blockTypes,
117479
117730
  hasToolResult,
117480
117731
  hasPlainText,
117481
- contentSample: typeof content === "string" ? content.slice(0, 200) : JSON.stringify(content).slice(0, 200)
117732
+ contentLen: typeof content === "string" ? content.length : JSON.stringify(content).length
117482
117733
  });
117483
117734
  break;
117484
117735
  }
@@ -117487,7 +117738,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117487
117738
  agentId: proc.agentId,
117488
117739
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
117489
117740
  blockTypes,
117490
- contentSample: JSON.stringify(content).slice(0, 300),
117741
+ contentLen: JSON.stringify(content).length,
117491
117742
  replyMessageId: base.replyMessageId
117492
117743
  });
117493
117744
  }
@@ -117508,6 +117759,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117508
117759
  });
117509
117760
  proc.currentMcpInvocationId = null;
117510
117761
  proc.currentMcpInvocationStartedAt = null;
117762
+ proc.activeToolUseStartedAt = void 0;
117511
117763
  proc.currentToolName = null;
117512
117764
  continue;
117513
117765
  }
@@ -117515,6 +117767,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117515
117767
  proc.officialMediaGenerationSatisfied = true;
117516
117768
  }
117517
117769
  if (isAskUserQuestionToolName(toolName)) {
117770
+ proc.activeToolUseStartedAt = void 0;
117518
117771
  proc.currentToolName = null;
117519
117772
  continue;
117520
117773
  }
@@ -117542,7 +117795,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117542
117795
  proc.currentMcpInvocationId = null;
117543
117796
  proc.currentMcpInvocationStartedAt = null;
117544
117797
  }
117545
- if (!isGroupTask(proc)) {
117798
+ if (shouldStreamInternals(proc)) {
117546
117799
  emit({
117547
117800
  type: "agent:tool_result",
117548
117801
  payload: {
@@ -117566,6 +117819,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117566
117819
  }
117567
117820
  }
117568
117821
  }
117822
+ proc.activeToolUseStartedAt = void 0;
117569
117823
  }
117570
117824
  }
117571
117825
  }
@@ -117644,7 +117898,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117644
117898
  groupId,
117645
117899
  compactScheduled: proc.compactRequested === true,
117646
117900
  fullTextLen: proc.accumulatedText.length,
117647
- fullTextSample: proc.accumulatedText.slice(0, 200),
117648
117901
  accumulatedBlockCount: proc.contentBlocks.length,
117649
117902
  accumulatedBlockTypes: proc.contentBlocks.map((b) => b.type),
117650
117903
  silentSegmentEmitted: groupMode
@@ -117688,7 +117941,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117688
117941
  segmentCount: proc.segmentCount,
117689
117942
  compactScheduled: proc.compactRequested === true,
117690
117943
  fullTextLen: proc.accumulatedText.length,
117691
- fullTextSample: proc.accumulatedText.slice(0, 200),
117692
117944
  traceId: base.traceId
117693
117945
  });
117694
117946
  emit({
@@ -117739,7 +117991,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117739
117991
  ackId: base.replyMessageId,
117740
117992
  messageId: carrierMessageId,
117741
117993
  textLen: proc.accumulatedText.length,
117742
- textSample: proc.accumulatedText.slice(0, 200),
117743
117994
  tokenCount: usage.tokenCount,
117744
117995
  traceId: base.traceId
117745
117996
  });
@@ -117932,8 +118183,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117932
118183
  logger11.info("Captured non-streamed assistant message", {
117933
118184
  agentId: proc.agentId,
117934
118185
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
117935
- textLen: text.length,
117936
- textSample: text.slice(0, 100)
118186
+ textLen: text.length
117937
118187
  });
117938
118188
  } else {
117939
118189
  proc.lastAssistantContentDescription = describeAssistantContent(am.message?.content);
@@ -117957,6 +118207,7 @@ function resetAccumulators(proc) {
117957
118207
  proc.currentToolName = null;
117958
118208
  proc.currentMcpInvocationId = null;
117959
118209
  proc.currentMcpInvocationStartedAt = null;
118210
+ proc.activeToolUseStartedAt = void 0;
117960
118211
  proc.segmentBuffer = "";
117961
118212
  proc.segmentCount = 0;
117962
118213
  proc.accumulatedToolInput = "";
@@ -117966,6 +118217,7 @@ function resetAccumulators(proc) {
117966
118217
  proc.officialMediaGenerationSatisfied = false;
117967
118218
  proc.suppressCurrentThinking = false;
117968
118219
  proc.suppressCurrentToolUse = false;
118220
+ proc.activeSubagentTaskIds?.clear();
117969
118221
  }
117970
118222
 
117971
118223
  // src/forkHistoryReplay.ts
@@ -118157,7 +118409,7 @@ function missingSubscriptionMessage(subscriptionId) {
118157
118409
  }
118158
118410
  var NODE_USER_UID = 1e3;
118159
118411
  var POST_MERGE_CONTINUATION_ROUTE_MS = 15e3;
118160
- var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-prompt-v2";
118412
+ var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-mcp-abi-prompt-v4";
118161
118413
  var BINARY_ATTACHMENT_EXT_RE = /\.(?:7z|bmp|csv|doc|docx|gif|jpeg|jpg|m4a|mov|mp3|mp4|pdf|png|ppt|pptx|rar|rtf|wav|webm|webp|xls|xlsx|zip)$/i;
118162
118414
  var DOCUMENT_READING_RULES = `DOCUMENT READING:
118163
118415
  - The built-in Read tool cannot read binary office documents such as .docx, .xls, .xlsx, .pptx, .pdf, .odt, .ods, .odp, or .rtf.
@@ -118172,6 +118424,16 @@ var MEDIA_GENERATION_RULES = `MEDIA GENERATION:
118172
118424
  - Keep media replies short. Do not print raw media URLs, request_id, task_id, polling logs, or "let me check again" narration unless the user explicitly asks for diagnostics.
118173
118425
  - When a media task is submitted or completed, write only a natural one-line note such as "\u5DF2\u5F00\u59CB\u751F\u6210\uFF0C\u6211\u4F1A\u5728\u8FD9\u91CC\u66F4\u65B0\u7ED3\u679C\u3002" or "\u751F\u6210\u597D\u4E86\uFF0C\u53EF\u4EE5\u5728\u5361\u7247\u91CC\u67E5\u770B\u3002"; let the media card show status, preview, download, copy, and regenerate actions.
118174
118426
  - If the user asks whether a Seedance task is ready, call mcp__seedance__seedance_check_task once and answer from that result. Do not loop, sleep, or invent external Seedance API endpoints.`;
118427
+ function stableFingerprintValue(value) {
118428
+ if (Array.isArray(value)) return value.map(stableFingerprintValue);
118429
+ if (!value || typeof value !== "object") return value;
118430
+ const out = {};
118431
+ for (const key of Object.keys(value).sort()) {
118432
+ const normalized = stableFingerprintValue(value[key]);
118433
+ if (normalized !== void 0) out[key] = normalized;
118434
+ }
118435
+ return out;
118436
+ }
118175
118437
  function isRecoveryDispatchTask(task) {
118176
118438
  return task.dispatchKind === "manual_continue" || task.dispatchKind === "regenerate";
118177
118439
  }
@@ -118429,13 +118691,15 @@ var AgentManager = class {
118429
118691
  agents = /* @__PURE__ */ new Map();
118430
118692
  lastUsedAt = /* @__PURE__ */ new Map();
118431
118693
  /** Scopes 被 zombie_watchdog 关闭后的"入睡"标记,acquire 重建时清除并 emit awake。 */
118432
- dormantScopes = /* @__PURE__ */ new Set();
118694
+ dormantScopes = /* @__PURE__ */ new Map();
118433
118695
  /**
118434
118696
  * zombie_watchdog 拆 runtime 时,把该 (agentId, scope) 的 groupInbox 快照到这里,
118435
118697
  * 让下一次 getOrCreate 重建 runtime 时可以恢复未读消息。仅 in-memory;
118436
118698
  * bridge 进程崩溃 / shutdownAll 时丢失,与现有 inbox 内存语义一致。
118437
118699
  */
118438
118700
  dormantGroupInboxes = /* @__PURE__ */ new Map();
118701
+ /** Spectate requested before runtime existed; value = activatedAt epoch ms. */
118702
+ pendingSpectate = /* @__PURE__ */ new Map();
118439
118703
  sessionStore;
118440
118704
  dispatchMemory = new GroupDispatchMemoryStore();
118441
118705
  dataDir;
@@ -118601,6 +118865,7 @@ var AgentManager = class {
118601
118865
  }
118602
118866
  async resolveRuntimeCwd(agentConfig, scope, requestedCwd) {
118603
118867
  let cwd = this.remapServerWorkspaceCwd(agentConfig, scope, requestedCwd);
118868
+ let fallbackForensicsId;
118604
118869
  if (!isFullyQualifiedAbsolutePath(cwd)) {
118605
118870
  const fallback = import_node_path13.default.join(this.workspacesDir, this.localScopeDirName(agentConfig, scope));
118606
118871
  logger14.error(
@@ -118615,6 +118880,23 @@ var AgentManager = class {
118615
118880
  error: new Error("workdir_not_usable_on_this_machine")
118616
118881
  }
118617
118882
  );
118883
+ fallbackForensicsId = createFallbackId();
118884
+ logFallback(logger14, {
118885
+ fallbackId: fallbackForensicsId,
118886
+ type: "cwd_sandbox",
118887
+ phase: "applied",
118888
+ expected: false,
118889
+ context: {
118890
+ agentId: agentConfig.id,
118891
+ scope: scopeKey(scope),
118892
+ platform: process.platform,
118893
+ requested: requestedCwd,
118894
+ resolved: cwd,
118895
+ reason: "not_fully_qualified",
118896
+ fallback
118897
+ },
118898
+ outcome: { result: "sandbox_fallback" }
118899
+ });
118618
118900
  cwd = fallback;
118619
118901
  }
118620
118902
  if (isRunningAsRoot() && cwd.startsWith("/root/")) {
@@ -118633,6 +118915,21 @@ var AgentManager = class {
118633
118915
  fallback,
118634
118916
  error: e
118635
118917
  });
118918
+ const fbId = fallbackForensicsId ?? createFallbackId();
118919
+ logFallback(logger14, {
118920
+ fallbackId: fbId,
118921
+ type: "cwd_sandbox",
118922
+ phase: "applied",
118923
+ expected: false,
118924
+ context: {
118925
+ agentId: agentConfig.id,
118926
+ scope: scopeKey(scope),
118927
+ reason: "mkdir_failed",
118928
+ requested: cwd,
118929
+ fallback
118930
+ },
118931
+ outcome: { result: "second_layer_fallback" }
118932
+ });
118636
118933
  await import_promises3.default.mkdir(fallback, { recursive: true });
118637
118934
  return fallback;
118638
118935
  }
@@ -118855,17 +119152,24 @@ var AgentManager = class {
118855
119152
  });
118856
119153
  return null;
118857
119154
  }
118858
- scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection) {
118859
- return (0, import_node_crypto3.createHash)("sha256").update(SCOPE_PROMPT_FINGERPRINT_REVISION).update("\0").update(agentConfig.id).update("\0").update(agentConfig.name).update("\0").update(scopeKey(scope)).update("\0").update(import_node_path13.default.normalize(agentCwd)).update("\0").update(scopesSection).digest("hex");
119155
+ scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection, externalMcpFingerprint) {
119156
+ return (0, import_node_crypto3.createHash)("sha256").update(SCOPE_PROMPT_FINGERPRINT_REVISION).update("\0").update(agentConfig.id).update("\0").update(agentConfig.name).update("\0").update(scopeKey(scope)).update("\0").update(import_node_path13.default.normalize(agentCwd)).update("\0").update(scopesSection).update("\0").update(externalMcpFingerprint).digest("hex");
118860
119157
  }
118861
- discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint) {
119158
+ externalMcpFingerprint(externalMcp) {
119159
+ const serverNames = Object.keys(externalMcp.mcpServers).sort();
119160
+ const allowedTools = [...externalMcp.allowedTools].sort();
119161
+ const toolAbi = [...externalMcp.toolAbi ?? []].sort((a, b) => a.serverName.localeCompare(b.serverName)).map(stableFingerprintValue);
119162
+ if (serverNames.length === 0 && allowedTools.length === 0 && toolAbi.length === 0) return "";
119163
+ return JSON.stringify({ serverNames, allowedTools, toolAbi });
119164
+ }
119165
+ discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint, options = {}) {
118862
119166
  const previous = this.sessionStore.getPromptFingerprint(agentConfig.id, scope);
118863
119167
  if (!sessionId) {
118864
119168
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
118865
119169
  return null;
118866
119170
  }
118867
119171
  if (previous === fingerprint) return sessionId;
118868
- if (!previous && scope.kind === "single") {
119172
+ if (!previous && scope.kind === "single" && options.clearLegacySingleSession !== true) {
118869
119173
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
118870
119174
  logger14.info("Retaining legacy single-scope session while recording prompt fingerprint", {
118871
119175
  agentId: agentConfig.id,
@@ -118885,7 +119189,8 @@ var AgentManager = class {
118885
119189
  sessionId,
118886
119190
  previousFingerprint: previous,
118887
119191
  nextFingerprint: fingerprint,
118888
- revision: SCOPE_PROMPT_FINGERPRINT_REVISION
119192
+ revision: SCOPE_PROMPT_FINGERPRINT_REVISION,
119193
+ reason: options.reason ?? "scope_prompt_changed"
118889
119194
  });
118890
119195
  return null;
118891
119196
  }
@@ -118934,6 +119239,7 @@ var AgentManager = class {
118934
119239
  logger14.info("Evicting idle Agent query", { agentId: proc.agentId, scope: scopeKey(proc.scope) });
118935
119240
  const runtime = this.asRuntime(proc);
118936
119241
  this.clearQuietFlushTimer(runtime);
119242
+ this.teardownSpectate(runtime);
118937
119243
  try {
118938
119244
  runtime.inputController.close();
118939
119245
  await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
@@ -118953,6 +119259,7 @@ var AgentManager = class {
118953
119259
  const runtime = this.asRuntime(proc);
118954
119260
  const key = runtimeKey(proc.agentId, proc.scope);
118955
119261
  this.clearQuietFlushTimer(runtime);
119262
+ this.teardownSpectate(runtime);
118956
119263
  runtime.currentTask = null;
118957
119264
  runtime.injectedTasks = [];
118958
119265
  runtime.mergedTasks = [];
@@ -118989,6 +119296,7 @@ var AgentManager = class {
118989
119296
  evictIdle() {
118990
119297
  const now = Date.now();
118991
119298
  const { idleTimeoutMs, workingSilenceTimeoutMs } = this.queryConfig;
119299
+ const stallWarnAfterMs = Math.min(9e4, this.queryConfig.replyStallTimeoutMs);
118992
119300
  for (const [key, proc] of this.agents) {
118993
119301
  if (!this.isEvictable(proc)) continue;
118994
119302
  const runtime = this.asRuntime(proc);
@@ -118999,26 +119307,62 @@ var AgentManager = class {
118999
119307
  for (const [, proc] of this.agents) {
119000
119308
  if (proc.status !== "working") continue;
119001
119309
  const runtime = this.asRuntime(proc);
119310
+ if (runtime.currentTask) {
119311
+ const sinceEventMs = now - proc.lastSdkEventAt;
119312
+ if (sinceEventMs > stallWarnAfterMs && !proc.stallWarned) {
119313
+ proc.stallWarned = true;
119314
+ const openTool = this.latestOpenToolUse(proc);
119315
+ logger14.warn("Reply stall onset: in-flight reply silent", {
119316
+ agentId: proc.agentId,
119317
+ scope: scopeKey(proc.scope),
119318
+ sinceEventMs,
119319
+ replyStallTimeoutMs: this.queryConfig.replyStallTimeoutMs,
119320
+ workingSilenceTimeoutMs,
119321
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
119322
+ replyMessageId: runtime.currentTask.replyMessageId,
119323
+ model: proc.model ?? "(unknown)",
119324
+ lastSdkEvent: proc.lastSdkEventInfo,
119325
+ hasActiveToolUse: runtime.activeToolUseStartedAt != null || openTool != null,
119326
+ activeToolUseAgeMs: runtime.activeToolUseStartedAt != null ? now - runtime.activeToolUseStartedAt : null,
119327
+ openToolName: openTool?.toolName ?? proc.currentToolName ?? null,
119328
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null,
119329
+ subagentInFlight: (runtime.activeSubagentTaskIds?.size ?? 0) > 0,
119330
+ activeSubagentCount: runtime.activeSubagentTaskIds?.size ?? 0,
119331
+ busyReason: this.busyReason(proc)
119332
+ });
119333
+ }
119334
+ }
119335
+ const busyReason = this.busyReason(runtime);
119336
+ const busy = busyReason !== null;
119337
+ if (runtime.currentTask && !busy && now - proc.lastSdkEventAt > this.queryConfig.replyStallTimeoutMs) {
119338
+ void this.recoverStalledReply(proc, now - proc.lastSdkEventAt);
119339
+ continue;
119340
+ }
119002
119341
  const hasInjectedBacklog = runtime.injectedTasks.length > 0;
119003
- const effectiveTimeoutMs = hasInjectedBacklog ? workingSilenceTimeoutMs * 2 : workingSilenceTimeoutMs;
119342
+ const baseCeilingMs = busy ? Math.max(workingSilenceTimeoutMs, this.queryConfig.busySilenceTimeoutMs ?? 0) : workingSilenceTimeoutMs;
119343
+ const effectiveTimeoutMs = hasInjectedBacklog ? baseCeilingMs * 2 : baseCeilingMs;
119004
119344
  const silentMs = now - proc.lastSdkEventAt;
119005
119345
  if (silentMs <= effectiveTimeoutMs) {
119006
- if (hasInjectedBacklog && silentMs > workingSilenceTimeoutMs) {
119007
- logger14.warn(
119008
- "Zombie watchdog: working runtime silent past base timeout but has queued tasks; granting extended grace",
119009
- {
119010
- agentId: proc.agentId,
119011
- scope: scopeKey(proc.scope),
119012
- silentMs,
119013
- baseTimeoutMs: workingSilenceTimeoutMs,
119014
- effectiveTimeoutMs,
119015
- injectedTaskCount: runtime.injectedTasks.length,
119016
- replyMessageId: proc.currentTask?.replyMessageId
119017
- }
119018
- );
119346
+ if (silentMs > workingSilenceTimeoutMs) {
119347
+ logger14.warn("Zombie watchdog: working runtime silent past base timeout; granting extended grace", {
119348
+ agentId: proc.agentId,
119349
+ scope: scopeKey(proc.scope),
119350
+ silentMs,
119351
+ baseTimeoutMs: workingSilenceTimeoutMs,
119352
+ baseCeilingMs,
119353
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
119354
+ effectiveTimeoutMs,
119355
+ busy,
119356
+ busyReason,
119357
+ injectedTaskCount: runtime.injectedTasks.length,
119358
+ replyMessageId: proc.currentTask?.replyMessageId
119359
+ });
119019
119360
  }
119020
119361
  continue;
119021
119362
  }
119363
+ const zombieOpenTool = this.latestOpenToolUse(proc);
119364
+ const watchdogDetectedAt = Date.now();
119365
+ const watchdogFallbackId = createFallbackId();
119022
119366
  logger14.warn("Zombie watchdog: working runtime silent too long, tearing down", {
119023
119367
  agentId: proc.agentId,
119024
119368
  scope: scopeKey(proc.scope),
@@ -119026,12 +119370,46 @@ var AgentManager = class {
119026
119370
  lastSdkEventAt: new Date(proc.lastSdkEventAt).toISOString(),
119027
119371
  workingSilenceTimeoutMs,
119028
119372
  effectiveTimeoutMs,
119373
+ baseCeilingMs,
119374
+ busy,
119375
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
119029
119376
  replyMessageId: proc.currentTask?.replyMessageId,
119030
119377
  injectedTaskCount: runtime.injectedTasks.length,
119031
119378
  hadInjectedBacklog: hasInjectedBacklog,
119032
- inboxSize: proc.groupInbox.length
119379
+ inboxSize: proc.groupInbox.length,
119380
+ fallbackId: watchdogFallbackId,
119381
+ // Breadcrumb: what the turn was waiting on when it timed out. A hung subagent
119382
+ // (open `Task` tool_use) lands here now that the fast path skips active tools.
119383
+ model: proc.model ?? "(unknown)",
119384
+ lastSdkEvent: proc.lastSdkEventInfo,
119385
+ openToolName: zombieOpenTool?.toolName ?? proc.currentToolName ?? null,
119386
+ activeToolUseAgeMs: runtime.activeToolUseStartedAt != null ? now - runtime.activeToolUseStartedAt : null,
119387
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null,
119388
+ subagentInFlight: (runtime.activeSubagentTaskIds?.size ?? 0) > 0,
119389
+ activeSubagentCount: runtime.activeSubagentTaskIds?.size ?? 0,
119390
+ busyReason: this.busyReason(proc)
119391
+ });
119392
+ logFallback(logger14, {
119393
+ fallbackId: watchdogFallbackId,
119394
+ type: "zombie_watchdog",
119395
+ phase: "detected",
119396
+ expected: false,
119397
+ traceId: proc.currentTask?.traceId,
119398
+ context: {
119399
+ agentId: proc.agentId,
119400
+ scope: scopeKey(proc.scope),
119401
+ silentMs,
119402
+ replyMessageId: proc.currentTask?.replyMessageId ?? null,
119403
+ openToolName: zombieOpenTool?.toolName ?? proc.currentToolName ?? null,
119404
+ lastSdkEventType: proc.lastSdkEventInfo?.type ?? null,
119405
+ injectedTaskCount: runtime.injectedTasks.length,
119406
+ inboxSize: proc.groupInbox.length
119407
+ }
119408
+ });
119409
+ void this.closeRuntime(proc, "zombie_watchdog", {
119410
+ fallbackId: watchdogFallbackId,
119411
+ detectedAt: watchdogDetectedAt
119033
119412
  });
119034
- void this.closeRuntime(proc, "zombie_watchdog");
119035
119413
  }
119036
119414
  }
119037
119415
  /**
@@ -119278,7 +119656,7 @@ ${cfg.instructions.trim()}` : "";
119278
119656
  agentId: agentConfig.id,
119279
119657
  capabilityTier: cfg.capabilityTier,
119280
119658
  isSmith: smithAgent
119281
- }) ?? { mcpServers: {}, allowedTools: [] };
119659
+ }) ?? { mcpServers: {}, allowedTools: [], toolAbi: [] };
119282
119660
  logger14.info("External MCP resolved for runtime", {
119283
119661
  agentId: agentConfig.id,
119284
119662
  scope: scopeKey(scope),
@@ -119295,11 +119673,16 @@ ${cfg.instructions.trim()}` : "";
119295
119673
  }
119296
119674
  const notebookSection = this.buildNotebookSection(agentConfig.id);
119297
119675
  const scopesSection = this.buildScopesSection(agentConfig, scope, agentCwd);
119676
+ const externalMcpFingerprint = this.externalMcpFingerprint(externalMcp);
119298
119677
  savedSessionId = this.discardSessionIfScopePromptChanged(
119299
119678
  agentConfig,
119300
119679
  scope,
119301
119680
  savedSessionId,
119302
- this.scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection)
119681
+ this.scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection, externalMcpFingerprint),
119682
+ {
119683
+ clearLegacySingleSession: externalMcpFingerprint.length > 0,
119684
+ reason: externalMcpFingerprint.length > 0 ? "external_mcp_abi_changed" : "scope_prompt_changed"
119685
+ }
119303
119686
  );
119304
119687
  let forkHistorySection = "";
119305
119688
  if (!savedSessionId && scope.kind === "single") {
@@ -119320,6 +119703,8 @@ ${cfg.instructions.trim()}` : "";
119320
119703
  }
119321
119704
  }
119322
119705
  const cronLockSnapshot = readCronLockSnapshot();
119706
+ const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
119707
+ const disallowedToolsForRuntime = builtinWebSearchAllowed ? [] : ["WebSearch"];
119323
119708
  logger14.info("Creating Agent query", {
119324
119709
  agentId: agentConfig.id,
119325
119710
  scope: scopeKey(scope),
@@ -119328,6 +119713,8 @@ ${cfg.instructions.trim()}` : "";
119328
119713
  sessionId: savedSessionId,
119329
119714
  forkHistoryReplay: forkHistorySection.length > 0,
119330
119715
  model: cfg.model ?? "(default)",
119716
+ builtinWebSearchAllowed,
119717
+ disallowedTools: disallowedToolsForRuntime,
119331
119718
  // Diagnostic: who currently owns Claude's global cron lock (~/.claude/scheduled_tasks.lock).
119332
119719
  // Cron is process-internal but the binary uses this singleton lock to elect ONE scheduler
119333
119720
  // among concurrent claude subprocesses. If lock is held by another session at spawn time,
@@ -119339,7 +119726,6 @@ ${cfg.instructions.trim()}` : "";
119339
119726
  });
119340
119727
  const planModeRef = { active: false, denyCount: 0 };
119341
119728
  const mediaGenerationTurnGuard = createOfficialMediaGenerationTurnGuard();
119342
- const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
119343
119729
  const options = {
119344
119730
  cwd: agentCwd,
119345
119731
  systemPrompt: {
@@ -119406,6 +119792,8 @@ ${cfg.instructions.trim()}` : "";
119406
119792
  ] : [],
119407
119793
  ...externalMcp.allowedTools
119408
119794
  ],
119795
+ // Server-side WebSearch bypasses canUseTool; disallowedTools removes it from model context.
119796
+ disallowedTools: disallowedToolsForRuntime,
119409
119797
  mcpServers: { ...externalMcp.mcpServers, neural: neuralServer },
119410
119798
  includePartialMessages: true,
119411
119799
  // Plan mode custom workflow instructions. When setPermissionMode('plan') is
@@ -119732,6 +120120,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119732
120120
  currentTask: null,
119733
120121
  currentTaskStartedAt: 0,
119734
120122
  lastSdkEventAt: Date.now(),
120123
+ model: (typeof options.model === "string" ? options.model : cfg.model) ?? null,
119735
120124
  compactRequested: false,
119736
120125
  compactInProgress: false,
119737
120126
  contextOverflowLockedUntil: 0,
@@ -119743,13 +120132,18 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119743
120132
  currentToolName: null,
119744
120133
  currentMcpInvocationId: null,
119745
120134
  currentMcpInvocationStartedAt: null,
120135
+ activeSubagentTaskIds: /* @__PURE__ */ new Set(),
119746
120136
  mcpAuditRecorder: this.mcpAuditRecorder,
119747
120137
  segmentBuffer: "",
119748
120138
  segmentCount: 0,
119749
120139
  accumulatedToolInput: "",
119750
120140
  planModeRef,
119751
120141
  mediaGenerationTurnGuard,
119752
- groupInbox: []
120142
+ groupInbox: [],
120143
+ spectating: false,
120144
+ spectateActivatedAt: 0,
120145
+ spectateViewing: false,
120146
+ spectateTtlExpired: false
119753
120147
  };
119754
120148
  const runtime = Object.assign(proc, {
119755
120149
  query: agentQuery,
@@ -119761,7 +120155,8 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119761
120155
  createdAt: Date.now(),
119762
120156
  supportsVision: modelInputMode === "vision" && cfg.supportsVision !== false,
119763
120157
  modelInputMode,
119764
- quietFlushTimer: null
120158
+ quietFlushTimer: null,
120159
+ spectateRevertTimer: null
119765
120160
  });
119766
120161
  logger14.info("Agent model input mode resolved", {
119767
120162
  agentId: agentConfig.id,
@@ -119786,6 +120181,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119786
120181
  } else {
119787
120182
  this.dormantGroupInboxes.delete(key);
119788
120183
  }
120184
+ const dormantMeta = this.dormantScopes.get(key);
119789
120185
  if (this.dormantScopes.delete(key)) {
119790
120186
  this.emit({
119791
120187
  type: "agent:awake",
@@ -119797,7 +120193,44 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119797
120193
  });
119798
120194
  logger14.info("Agent scope awakened after dormant", {
119799
120195
  agentId: agentConfig.id,
119800
- scope: scopeKey(scope)
120196
+ scope: scopeKey(scope),
120197
+ ...dormantMeta ? {
120198
+ fallbackId: dormantMeta.fallbackId,
120199
+ dormantDurationMs: Date.now() - dormantMeta.detectedAt,
120200
+ dataLossSuspected: dormantMeta.droppedTaskCount > 0
120201
+ } : {}
120202
+ });
120203
+ if (dormantMeta) {
120204
+ logFallback(logger14, {
120205
+ fallbackId: dormantMeta.fallbackId,
120206
+ type: "zombie_watchdog",
120207
+ phase: "outcome",
120208
+ expected: false,
120209
+ context: {
120210
+ agentId: agentConfig.id,
120211
+ scope: scopeKey(scope)
120212
+ },
120213
+ outcome: {
120214
+ result: "recovered_rebuilt",
120215
+ durationMs: Date.now() - dormantMeta.detectedAt,
120216
+ dataLossSuspected: dormantMeta.droppedTaskCount > 0
120217
+ }
120218
+ });
120219
+ }
120220
+ }
120221
+ const pendingSpectateAt = this.pendingSpectate.get(key);
120222
+ if (pendingSpectateAt != null) {
120223
+ this.pendingSpectate.delete(key);
120224
+ runtime.spectating = true;
120225
+ runtime.spectateViewing = false;
120226
+ runtime.spectateActivatedAt = pendingSpectateAt;
120227
+ runtime.spectateTtlExpired = false;
120228
+ this.armSpectateTimer(runtime);
120229
+ this.emitSpectateState(runtime, true, "started");
120230
+ logger14.info("Applied pending spectate on runtime create", {
120231
+ agentId: agentConfig.id,
120232
+ scope: scopeKey(scope),
120233
+ activatedAt: pendingSpectateAt
119801
120234
  });
119802
120235
  }
119803
120236
  if (proc.groupInbox.length > 0 && this.isRuntimeIdleForInboxFlush(runtime)) {
@@ -120935,7 +121368,6 @@ ${lines.join("\n")}`;
120935
121368
  compactTrigger: "context_watermark",
120936
121369
  injectedTasksWaiting: runtime.injectedTasks.length,
120937
121370
  compactPromptLen: compactPrompt.length,
120938
- promptSample: compactPrompt.slice(0, 80),
120939
121371
  traceId: compactTraceId
120940
121372
  });
120941
121373
  runtime.inputController.push(compactPrompt, runtime.ccSessionId ?? "");
@@ -121217,7 +121649,7 @@ ${lines.join("\n")}`;
121217
121649
  const enveloped = buildInnerVoiceEnvelope(payloadWithTrigger, ctx);
121218
121650
  const task = {
121219
121651
  content: enveloped,
121220
- replyMessageId: createMessageId(),
121652
+ replyMessageId: createNeuralSendReplyMessageId(),
121221
121653
  conversationId: payload.conversationId,
121222
121654
  traceId: createTraceId(),
121223
121655
  groupId: payload.groupId
@@ -121463,7 +121895,7 @@ ${lines.join("\n")}`;
121463
121895
  this.dormantScopes.delete(key);
121464
121896
  this.dormantGroupInboxes.delete(key);
121465
121897
  }
121466
- for (const key of [...this.dormantScopes].filter(
121898
+ for (const key of [...this.dormantScopes.keys()].filter(
121467
121899
  (k) => k === agentId || k.startsWith(`${agentId}::`)
121468
121900
  )) {
121469
121901
  this.dormantScopes.delete(key);
@@ -121511,7 +121943,7 @@ ${lines.join("\n")}`;
121511
121943
  async reloadAgentScopes(agentId, reason) {
121512
121944
  this.sessionStore.deleteAllForAgent(agentId);
121513
121945
  this.dispatchMemory.deleteAllForAgent(agentId);
121514
- for (const key of [...this.dormantScopes].filter(
121946
+ for (const key of [...this.dormantScopes.keys()].filter(
121515
121947
  (k) => k === agentId || k.startsWith(`${agentId}::`)
121516
121948
  )) {
121517
121949
  this.dormantScopes.delete(key);
@@ -121663,6 +122095,125 @@ ${lines.join("\n")}`;
121663
122095
  void this.terminateScope(proc.agentId, proc.scope);
121664
122096
  }
121665
122097
  }
122098
+ /** Control spectate capture/push for one scoped runtime. */
122099
+ async setSpectate(agentId, scope, action) {
122100
+ const key = runtimeKey(agentId, scope);
122101
+ const proc = this.agents.get(key);
122102
+ if (!proc || proc.status === "dead") {
122103
+ if (action === "start") {
122104
+ this.pendingSpectate.set(key, Date.now());
122105
+ logger14.info("setSpectate: runtime missing, pending start", { agentId, scope: scopeKey(scope) });
122106
+ }
122107
+ return;
122108
+ }
122109
+ const runtime = this.asRuntime(proc);
122110
+ switch (action) {
122111
+ case "start":
122112
+ runtime.spectating = true;
122113
+ runtime.spectateViewing = true;
122114
+ runtime.spectateActivatedAt = Date.now();
122115
+ runtime.spectateTtlExpired = false;
122116
+ this.pendingSpectate.delete(key);
122117
+ this.armSpectateTimer(runtime);
122118
+ this.emitSpectateState(runtime, true, "started");
122119
+ logger14.info("Spectate started", { agentId, scope: scopeKey(scope) });
122120
+ break;
122121
+ case "enter_view":
122122
+ runtime.spectateViewing = true;
122123
+ logger14.info("Spectate enter_view", { agentId, scope: scopeKey(scope) });
122124
+ break;
122125
+ case "leave_view":
122126
+ runtime.spectateViewing = false;
122127
+ logger14.info("Spectate leave_view", {
122128
+ agentId,
122129
+ scope: scopeKey(scope),
122130
+ ttlExpired: runtime.spectateTtlExpired
122131
+ });
122132
+ if (runtime.spectateTtlExpired) {
122133
+ this.stopSpectate(runtime, "ttl_expired");
122134
+ }
122135
+ break;
122136
+ case "stop":
122137
+ this.stopSpectate(runtime, "stopped");
122138
+ this.pendingSpectate.delete(key);
122139
+ logger14.info("Spectate stopped", { agentId, scope: scopeKey(scope) });
122140
+ break;
122141
+ default:
122142
+ break;
122143
+ }
122144
+ }
122145
+ spectateTtlMs() {
122146
+ return Number(process.env.AHCHAT_BRIDGE_SPECTATE_TTL_MS) || 36e5;
122147
+ }
122148
+ armSpectateTimer(runtime) {
122149
+ this.clearSpectateTimer(runtime);
122150
+ if (!runtime.spectating || runtime.spectateActivatedAt <= 0) return;
122151
+ const ttlMs = this.spectateTtlMs();
122152
+ const elapsed = Date.now() - runtime.spectateActivatedAt;
122153
+ const delay = Math.max(0, ttlMs - elapsed);
122154
+ runtime.spectateRevertTimer = setTimeout(() => {
122155
+ runtime.spectateRevertTimer = null;
122156
+ runtime.spectateTtlExpired = true;
122157
+ logger14.info("Spectate TTL expired", {
122158
+ agentId: runtime.agentId,
122159
+ scope: scopeKey(runtime.scope),
122160
+ viewing: runtime.spectateViewing
122161
+ });
122162
+ if (!runtime.spectateViewing) {
122163
+ this.stopSpectate(runtime, "ttl_expired");
122164
+ }
122165
+ }, delay);
122166
+ }
122167
+ clearSpectateTimer(runtime) {
122168
+ if (runtime.spectateRevertTimer != null) {
122169
+ clearTimeout(runtime.spectateRevertTimer);
122170
+ runtime.spectateRevertTimer = null;
122171
+ }
122172
+ }
122173
+ stopSpectate(runtime, reason) {
122174
+ const wasActive = runtime.spectating;
122175
+ this.clearSpectateTimer(runtime);
122176
+ runtime.spectating = false;
122177
+ runtime.spectateViewing = false;
122178
+ runtime.spectateTtlExpired = false;
122179
+ runtime.spectateActivatedAt = 0;
122180
+ if (wasActive) {
122181
+ this.emitSpectateState(runtime, false, reason);
122182
+ logger14.info("Spectate deactivated", {
122183
+ agentId: runtime.agentId,
122184
+ scope: scopeKey(runtime.scope),
122185
+ reason
122186
+ });
122187
+ }
122188
+ }
122189
+ emitSpectateState(runtime, active, reason) {
122190
+ const scopePayload = runtime.scope.kind === "single" ? { kind: "single" } : { kind: "group", groupId: runtime.scope.groupId };
122191
+ this.emit({
122192
+ type: "spectate:state",
122193
+ payload: {
122194
+ agentId: runtime.agentId,
122195
+ scope: scopePayload,
122196
+ active,
122197
+ expiresAt: active ? runtime.spectateActivatedAt + this.spectateTtlMs() : void 0,
122198
+ reason,
122199
+ traceId: createTraceId()
122200
+ }
122201
+ });
122202
+ logger14.info("Spectate state emitted", {
122203
+ agentId: runtime.agentId,
122204
+ scope: scopeKey(runtime.scope),
122205
+ active,
122206
+ reason,
122207
+ expiresAt: active ? runtime.spectateActivatedAt + this.spectateTtlMs() : void 0
122208
+ });
122209
+ }
122210
+ teardownSpectate(runtime) {
122211
+ if (runtime.spectating) {
122212
+ this.stopSpectate(runtime, "runtime_gone");
122213
+ } else {
122214
+ this.clearSpectateTimer(runtime);
122215
+ }
122216
+ }
121666
122217
  /** Stop one scoped SDK runtime (workdir change). */
121667
122218
  async terminateScope(agentId, scope) {
121668
122219
  const key = runtimeKey(agentId, scope);
@@ -121671,6 +122222,7 @@ ${lines.join("\n")}`;
121671
122222
  logger14.info("terminateScope: no active runtime", { agentId, scope: scopeKey(scope) });
121672
122223
  this.dormantScopes.delete(key);
121673
122224
  this.dormantGroupInboxes.delete(key);
122225
+ this.pendingSpectate.delete(key);
121674
122226
  this.sessionStore.delete(agentId, scope);
121675
122227
  this.dispatchMemory.deleteScope(agentId, scope);
121676
122228
  return;
@@ -121693,7 +122245,7 @@ ${lines.join("\n")}`;
121693
122245
  this.dispatchMemory.deleteScope(agentId, scope);
121694
122246
  logger14.info("terminateScope: scoped query removed", { agentId, scope: scopeKey(scope) });
121695
122247
  }
121696
- async closeRuntime(proc, reason) {
122248
+ async closeRuntime(proc, reason, watchdogForensics) {
121697
122249
  const key = runtimeKey(proc.agentId, proc.scope);
121698
122250
  if (proc.status === "dead") return;
121699
122251
  const runtime = this.asRuntime(proc);
@@ -121770,12 +122322,13 @@ ${lines.join("\n")}`;
121770
122322
  runtime.currentTask = null;
121771
122323
  if (isWatchdog) {
121772
122324
  const preservedInbox = proc.groupInbox;
121773
- if (preservedInbox.length > 0) {
122325
+ const preservedInboxSize = preservedInbox.length;
122326
+ if (preservedInboxSize > 0) {
121774
122327
  this.dormantGroupInboxes.set(key, [...preservedInbox]);
121775
122328
  logger14.info("Preserving groupInbox for dormant agent", {
121776
122329
  agentId,
121777
122330
  scope: scopeKey(proc.scope),
121778
- preservedInboxSize: preservedInbox.length,
122331
+ preservedInboxSize,
121779
122332
  preservedEntries: preservedInbox.map((e) => ({
121780
122333
  ackId: e.ackId,
121781
122334
  sender: e.senderName,
@@ -121784,7 +122337,26 @@ ${lines.join("\n")}`;
121784
122337
  }))
121785
122338
  });
121786
122339
  }
121787
- this.dormantScopes.add(key);
122340
+ const effectiveFallbackId = watchdogForensics?.fallbackId ?? createFallbackId();
122341
+ logFallback(logger14, {
122342
+ fallbackId: effectiveFallbackId,
122343
+ type: "zombie_watchdog",
122344
+ phase: "applied",
122345
+ expected: false,
122346
+ traceId: dormantTraceId,
122347
+ context: {
122348
+ agentId,
122349
+ scope: scopeKey(proc.scope),
122350
+ droppedTaskCount: droppedAckIds.length,
122351
+ preservedInboxSize,
122352
+ sessionDeleted: false
122353
+ }
122354
+ });
122355
+ this.dormantScopes.set(key, {
122356
+ fallbackId: effectiveFallbackId,
122357
+ detectedAt: watchdogForensics?.detectedAt ?? Date.now(),
122358
+ droppedTaskCount: droppedAckIds.length
122359
+ });
121788
122360
  this.emit({
121789
122361
  type: "agent:dormant",
121790
122362
  payload: {
@@ -121799,13 +122371,15 @@ ${lines.join("\n")}`;
121799
122371
  agentId,
121800
122372
  scope: scopeKey(proc.scope),
121801
122373
  droppedTaskCount: droppedAckIds.length,
121802
- preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0
122374
+ preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0,
122375
+ fallbackId: effectiveFallbackId
121803
122376
  });
121804
122377
  }
121805
122378
  proc.status = "dead";
121806
122379
  this.agents.delete(key);
121807
122380
  this.lastUsedAt.delete(key);
121808
122381
  this.clearQuietFlushTimer(runtime);
122382
+ this.teardownSpectate(runtime);
121809
122383
  try {
121810
122384
  runtime.inputController.close();
121811
122385
  await this.awaitQueryReturn(runtime.query, 5e3, agentId);
@@ -121822,6 +122396,165 @@ ${lines.join("\n")}`;
121822
122396
  cwd: proc.cwd
121823
122397
  });
121824
122398
  }
122399
+ /**
122400
+ * Emit `agent:error` for the active reply and every queued/merged/buffered task,
122401
+ * then clear those queues. Used by both the SDK stream-crash path and the
122402
+ * reply-stall watchdog so a torn-down runtime never leaves a carrier reply
122403
+ * stuck in-flight on the server (which would keep absorbing new user messages
122404
+ * as steers of a dead turn).
122405
+ */
122406
+ failPendingTasksWithError(runtime, errorText, fallbackId) {
122407
+ const pending = [];
122408
+ if (runtime.currentTask) pending.push(runtime.currentTask);
122409
+ pending.push(...runtime.injectedTasks, ...runtime.mergedTasks, ...runtime.planModeBuffer);
122410
+ runtime.currentTask = null;
122411
+ runtime.injectedTasks = [];
122412
+ runtime.mergedTasks = [];
122413
+ runtime.planModeBuffer = [];
122414
+ if (pending.length === 0) return { pendingCount: 0 };
122415
+ const carrier = pending[0];
122416
+ const mergedTasks = pending.slice(1);
122417
+ logger14.warn("Pending tasks failure consolidated", {
122418
+ agentId: runtime.agentId,
122419
+ scope: scopeKey(runtime.scope),
122420
+ pendingCount: pending.length,
122421
+ carrierAckId: carrier.replyMessageId,
122422
+ mergedAckIds: mergedTasks.map((t) => t.replyMessageId),
122423
+ traceId: carrier.traceId,
122424
+ ...fallbackId ? { fallbackId } : {}
122425
+ });
122426
+ this.emit({
122427
+ type: "agent:error",
122428
+ payload: {
122429
+ agentId: runtime.agentId,
122430
+ conversationId: carrier.conversationId,
122431
+ ackId: carrier.replyMessageId,
122432
+ traceId: carrier.traceId,
122433
+ error: errorText
122434
+ }
122435
+ });
122436
+ for (const task of mergedTasks) {
122437
+ this.emit({
122438
+ type: "agent:merged",
122439
+ payload: {
122440
+ agentId: runtime.agentId,
122441
+ conversationId: task.conversationId,
122442
+ ackId: task.replyMessageId,
122443
+ mergedIntoAckId: carrier.replyMessageId,
122444
+ groupId: task.groupId,
122445
+ traceId: task.traceId
122446
+ }
122447
+ });
122448
+ }
122449
+ return { pendingCount: pending.length };
122450
+ }
122451
+ /**
122452
+ * Recover an in-flight reply that started but went silent past
122453
+ * `replyStallTimeoutMs` (see the reply-stall fast path in `evictIdle`). The
122454
+ * underlying SDK turn is wedged with no observable progress and no error, so:
122455
+ * 1. clear the (likely interrupted/dangling) session so the next dispatch
122456
+ * starts fresh instead of resuming the same wedged transcript;
122457
+ * 2. release the carrier reply + queued steers via `agent:error` so the
122458
+ * client stops waiting and the next user message starts a brand-new reply;
122459
+ * 3. tear the wedged runtime down.
122460
+ */
122461
+ async recoverStalledReply(proc, silentMs) {
122462
+ if (proc.status === "dead") return;
122463
+ const runtime = this.asRuntime(proc);
122464
+ const key = runtimeKey(proc.agentId, proc.scope);
122465
+ const replyStallFallbackId = createFallbackId();
122466
+ const stallTraceId = proc.currentTask?.traceId;
122467
+ logger14.warn("Reply stall watchdog: in-flight reply silent too long, recovering", {
122468
+ agentId: proc.agentId,
122469
+ scope: scopeKey(proc.scope),
122470
+ silentMs,
122471
+ replyStallTimeoutMs: this.queryConfig.replyStallTimeoutMs,
122472
+ replyMessageId: proc.currentTask?.replyMessageId,
122473
+ injectedTaskCount: runtime.injectedTasks.length,
122474
+ lastSdkEventAt: new Date(proc.lastSdkEventAt).toISOString(),
122475
+ fallbackId: replyStallFallbackId,
122476
+ // Breadcrumb: what the wedged turn was doing the instant it went silent.
122477
+ // (subagent Task call? mid tool_use? which provider?) — the difference
122478
+ // between a one-off and a systemic provider/tool stall.
122479
+ model: proc.model ?? "(unknown)",
122480
+ lastSdkEvent: proc.lastSdkEventInfo,
122481
+ currentBlockType: proc.currentBlockType,
122482
+ currentToolName: proc.currentToolName,
122483
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null
122484
+ });
122485
+ logFallback(logger14, {
122486
+ fallbackId: replyStallFallbackId,
122487
+ type: "reply_stall",
122488
+ phase: "detected",
122489
+ expected: false,
122490
+ traceId: stallTraceId,
122491
+ context: {
122492
+ agentId: proc.agentId,
122493
+ scope: scopeKey(proc.scope),
122494
+ silentMs,
122495
+ model: proc.model ?? "(unknown)",
122496
+ replyMessageId: proc.currentTask?.replyMessageId ?? null,
122497
+ currentToolName: proc.currentToolName ?? null,
122498
+ lastSdkEventType: proc.lastSdkEventInfo?.type ?? null,
122499
+ injectedTaskCount: runtime.injectedTasks.length,
122500
+ mergedTaskCount: runtime.mergedTasks.length,
122501
+ planModeBufferCount: runtime.planModeBuffer.length
122502
+ }
122503
+ });
122504
+ this.sessionStore.delete(proc.agentId, proc.scope);
122505
+ this.dispatchMemory.deleteScope(proc.agentId, proc.scope);
122506
+ const failSummary = this.failPendingTasksWithError(
122507
+ runtime,
122508
+ "\u56DE\u590D\u957F\u65F6\u95F4\u65E0\u54CD\u5E94\uFF0C\u5DF2\u91CD\u7F6E\u8BE5\u4F1A\u8BDD\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\u3002",
122509
+ replyStallFallbackId
122510
+ );
122511
+ proc.status = "dead";
122512
+ this.agents.delete(key);
122513
+ this.lastUsedAt.delete(key);
122514
+ this.clearQuietFlushTimer(runtime);
122515
+ let queryCloseOk = true;
122516
+ try {
122517
+ runtime.inputController.close();
122518
+ await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
122519
+ } catch (e) {
122520
+ queryCloseOk = false;
122521
+ logger14.error("reply_stall: close query failed", {
122522
+ agentId: proc.agentId,
122523
+ scope: scopeKey(proc.scope),
122524
+ error: e
122525
+ });
122526
+ }
122527
+ logFallback(logger14, {
122528
+ fallbackId: replyStallFallbackId,
122529
+ type: "reply_stall",
122530
+ phase: "applied",
122531
+ expected: false,
122532
+ traceId: stallTraceId,
122533
+ context: {
122534
+ agentId: proc.agentId,
122535
+ scope: scopeKey(proc.scope),
122536
+ sessionDeleted: true,
122537
+ failedTaskCount: failSummary.pendingCount,
122538
+ queryClosed: queryCloseOk
122539
+ }
122540
+ });
122541
+ logFallback(logger14, {
122542
+ fallbackId: replyStallFallbackId,
122543
+ type: "reply_stall",
122544
+ phase: "outcome",
122545
+ expected: false,
122546
+ traceId: stallTraceId,
122547
+ context: {
122548
+ agentId: proc.agentId,
122549
+ scope: scopeKey(proc.scope),
122550
+ failedTaskCount: failSummary.pendingCount
122551
+ },
122552
+ outcome: {
122553
+ result: "session_reset_awaiting_user",
122554
+ dataLossSuspected: failSummary.pendingCount > 0
122555
+ }
122556
+ });
122557
+ }
121825
122558
  async recoverFromRestart(agents) {
121826
122559
  const lockSnapshot = readCronLockSnapshot();
121827
122560
  logger14.info("Recovering Agent sessions after restart", {
@@ -121944,58 +122677,7 @@ ${lines.join("\n")}`;
121944
122677
  this.lastUsedAt.delete(key);
121945
122678
  const errorText = isResumeFail ? `\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\uFF08${errMsg}\uFF09` : `Agent query crashed: ${errMsg}`;
121946
122679
  const emittedErrorText = isUnsupportedVisionInput ? "Current model/backend does not support image multimodal input. This image message failed; AHChat has cleared this scope session and future messages will send attachments as paths/text references." : errorText;
121947
- if (runtime.currentTask) {
121948
- this.emit({
121949
- type: "agent:error",
121950
- payload: {
121951
- agentId: runtime.agentId,
121952
- conversationId: runtime.currentTask.conversationId,
121953
- ackId: runtime.currentTask.replyMessageId,
121954
- traceId: runtime.currentTask.traceId,
121955
- error: emittedErrorText
121956
- }
121957
- });
121958
- runtime.currentTask = null;
121959
- }
121960
- for (const task of runtime.injectedTasks) {
121961
- this.emit({
121962
- type: "agent:error",
121963
- payload: {
121964
- agentId: runtime.agentId,
121965
- conversationId: task.conversationId,
121966
- ackId: task.replyMessageId,
121967
- traceId: task.traceId,
121968
- error: emittedErrorText
121969
- }
121970
- });
121971
- }
121972
- runtime.injectedTasks = [];
121973
- for (const task of runtime.mergedTasks) {
121974
- this.emit({
121975
- type: "agent:error",
121976
- payload: {
121977
- agentId: runtime.agentId,
121978
- conversationId: task.conversationId,
121979
- ackId: task.replyMessageId,
121980
- traceId: task.traceId,
121981
- error: emittedErrorText
121982
- }
121983
- });
121984
- }
121985
- runtime.mergedTasks = [];
121986
- for (const task of runtime.planModeBuffer) {
121987
- this.emit({
121988
- type: "agent:error",
121989
- payload: {
121990
- agentId: runtime.agentId,
121991
- conversationId: task.conversationId,
121992
- ackId: task.replyMessageId,
121993
- traceId: task.traceId,
121994
- error: emittedErrorText
121995
- }
121996
- });
121997
- }
121998
- runtime.planModeBuffer = [];
122680
+ this.failPendingTasksWithError(runtime, emittedErrorText);
121999
122681
  }
122000
122682
  }
122001
122683
  getStatus(agentId, scope = { kind: "single" }) {
@@ -122009,6 +122691,18 @@ ${lines.join("\n")}`;
122009
122691
  }
122010
122692
  return [...ids];
122011
122693
  }
122694
+ /** Unified signal: is the turn legitimately waiting on a live external call that emits no
122695
+ * parent heartbeat? Used by BOTH the reply-stall fast path and the zombie watchdog so neither
122696
+ * tears down a turn that is merely slow. Returns the reason for diagnostics, or null when idle.
122697
+ * - open_tool: regular tool, MCP tool, AskUserQuestion wait, or ExitPlanMode (tool_use open).
122698
+ * - subagent: Task/Agent in flight (its inner tool_results clear activeToolUseStartedAt).
122699
+ * - compact: bridge-injected /compact running. */
122700
+ busyReason(proc) {
122701
+ if (proc.activeToolUseStartedAt != null || this.latestOpenToolUse(proc) != null) return "open_tool";
122702
+ if ((proc.activeSubagentTaskIds?.size ?? 0) > 0) return "subagent";
122703
+ if (proc.compactInProgress === true) return "compact";
122704
+ return null;
122705
+ }
122012
122706
  latestOpenToolUse(proc) {
122013
122707
  for (let i = proc.contentBlocks.length - 1; i >= 0; i -= 1) {
122014
122708
  const block = proc.contentBlocks[i];
@@ -122157,7 +122851,7 @@ ${lines.join("\n")}`;
122157
122851
  }
122158
122852
  const task = {
122159
122853
  content: notice,
122160
- replyMessageId: createMessageId(),
122854
+ replyMessageId: createScopeNoticeReplyMessageId(),
122161
122855
  conversationId,
122162
122856
  traceId: createTraceId(),
122163
122857
  groupId: proc.scope.kind === "group" ? proc.scope.groupId : void 0
@@ -123244,6 +123938,7 @@ var HttpMcpRegistry = class {
123244
123938
  buildForAgent(ctx) {
123245
123939
  const mcpServers = {};
123246
123940
  const allowedTools = [];
123941
+ const toolAbi = [];
123247
123942
  const usedNames = /* @__PURE__ */ new Set();
123248
123943
  for (const connection of this.allConnections()) {
123249
123944
  if (!this.connectionAppliesToAgent(connection, ctx)) continue;
@@ -123252,12 +123947,18 @@ var HttpMcpRegistry = class {
123252
123947
  const serverName = uniqueServerName(normalizeMcpServerName(connection.serverName), usedNames);
123253
123948
  usedNames.add(serverName);
123254
123949
  mcpServers[serverName] = sdkConfig;
123255
- for (const tool2 of connection.tools) {
123256
- if (!tool2.enabled || tool2.permissionPolicy !== "always_allow") continue;
123257
- allowedTools.push(mcpRuntimeToolName(serverName, tool2.name));
123258
- }
123950
+ const visibleTools = connection.tools.filter((tool2) => tool2.enabled && tool2.permissionPolicy !== "always_deny");
123951
+ for (const tool2 of visibleTools) allowedTools.push(mcpRuntimeToolName(serverName, tool2.name));
123952
+ toolAbi.push({
123953
+ serverName,
123954
+ providerId: connection.providerId,
123955
+ transport: connection.transport,
123956
+ alwaysLoad: connection.alwaysLoad,
123957
+ isBuiltin: connection.isBuiltin,
123958
+ tools: visibleTools.map((tool2) => runtimeToolAbi(serverName, tool2)).sort((a, b) => a.runtimeToolName.localeCompare(b.runtimeToolName))
123959
+ });
123259
123960
  }
123260
- return { mcpServers, allowedTools };
123961
+ return { mcpServers, allowedTools, toolAbi };
123261
123962
  }
123262
123963
  allConnections() {
123263
123964
  return [...this.serverConnections.values(), ...this.localConnections.values()];
@@ -123443,6 +124144,18 @@ function uniqueServerName(serverName, usedNames) {
123443
124144
  while (usedNames.has(`${serverName}_${idx}`)) idx += 1;
123444
124145
  return `${serverName}_${idx}`;
123445
124146
  }
124147
+ function runtimeToolAbi(serverName, tool2) {
124148
+ return {
124149
+ name: tool2.name,
124150
+ runtimeToolName: mcpRuntimeToolName(serverName, tool2.name),
124151
+ displayName: tool2.displayName,
124152
+ description: tool2.description,
124153
+ category: tool2.category,
124154
+ riskLevel: tool2.riskLevel,
124155
+ permissionPolicy: tool2.permissionPolicy,
124156
+ ...tool2.inputSchema !== void 0 ? { inputSchema: tool2.inputSchema } : {}
124157
+ };
124158
+ }
123446
124159
  function buildHeaders(authType, authSecret, customHeaders) {
123447
124160
  const headers = {};
123448
124161
  for (const header of customHeaders) {
@@ -124099,6 +124812,7 @@ var ServerConnector = class {
124099
124812
  case "agent:terminate":
124100
124813
  case "agent:runtime_reload":
124101
124814
  case "agent:terminate_scope":
124815
+ case "spectate:set":
124102
124816
  case "agent:created":
124103
124817
  case "agent:updated":
124104
124818
  case "agent:workdir-updated":
@@ -125076,24 +125790,6 @@ function normalizeLocalPath(targetPath) {
125076
125790
  const expanded = trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\") ? import_node_path18.default.join(import_node_os10.default.homedir(), trimmed.slice(2)) : trimmed;
125077
125791
  return import_node_path18.default.normalize(import_node_path18.default.resolve(expanded));
125078
125792
  }
125079
- function isAbsoluteLocalPathCandidate(value) {
125080
- if (process.platform === "win32") {
125081
- return /^[a-zA-Z]:[\\/]/.test(value) || /^\\\\[^\\]/.test(value);
125082
- }
125083
- return value.startsWith("/");
125084
- }
125085
- function clipboardTextToPathCandidates(value) {
125086
- return value.replace(/\0/g, "\n").split(/\r?\n/).map((line) => line.trim().replace(/^"(.+)"$/, "$1")).map((line) => {
125087
- if (!line.toLowerCase().startsWith("file://")) return line;
125088
- try {
125089
- const url2 = new URL(line);
125090
- return decodeURIComponent(url2.pathname.replace(/^\/([a-zA-Z]:\/)/, "$1"));
125091
- } catch (e) {
125092
- logger25.debug("Failed to parse clipboard file URL", { error: e });
125093
- return "";
125094
- }
125095
- }).filter((line) => line.length > 0 && isAbsoluteLocalPathCandidate(line));
125096
- }
125097
125793
  function normalizeClipboardIdentityKey(value) {
125098
125794
  return process.platform === "win32" ? value.toLowerCase() : value;
125099
125795
  }
@@ -125131,18 +125827,15 @@ function mimeTypeForFileName(fileName) {
125131
125827
  }
125132
125828
  function parseWindowsClipboardResult(stdout) {
125133
125829
  const raw = stdout.trim();
125134
- if (!raw) return { files: [], text: "" };
125830
+ if (!raw) return { files: [] };
125135
125831
  try {
125136
125832
  const parsed = JSON.parse(raw);
125137
- if (!isRecord5(parsed)) return { files: [], text: "" };
125833
+ if (!isRecord5(parsed)) return { files: [] };
125138
125834
  const files = Array.isArray(parsed.files) ? parsed.files.filter((item) => typeof item === "string") : [];
125139
- return {
125140
- files,
125141
- text: typeof parsed.text === "string" ? parsed.text : ""
125142
- };
125835
+ return { files };
125143
125836
  } catch (e) {
125144
125837
  logger25.debug("Windows clipboard JSON parse skipped", { error: e });
125145
- return { files: clipboardTextToPathCandidates(stdout), text: "" };
125838
+ return { files: [] };
125146
125839
  }
125147
125840
  }
125148
125841
  async function readWindowsClipboardPathCandidates() {
@@ -125155,9 +125848,7 @@ async function readWindowsClipboardPathCandidates() {
125155
125848
  " $drop = [System.Windows.Forms.Clipboard]::GetFileDropList();",
125156
125849
  " foreach ($file in $drop) { $files += [string]$file }",
125157
125850
  "} catch {}",
125158
- '$text = "";',
125159
- "try { $text = [System.Windows.Forms.Clipboard]::GetText() } catch {}",
125160
- "[pscustomobject]@{ files = $files; text = $text } | ConvertTo-Json -Compress;"
125851
+ "[pscustomobject]@{ files = $files } | ConvertTo-Json -Compress;"
125161
125852
  ].join(" ");
125162
125853
  try {
125163
125854
  const { stdout } = await execFileAsync2("powershell.exe", [
@@ -125175,7 +125866,7 @@ async function readWindowsClipboardPathCandidates() {
125175
125866
  maxBuffer: 1024 * 1024
125176
125867
  });
125177
125868
  const result = parseWindowsClipboardResult(stdout);
125178
- return [...result.files, ...clipboardTextToPathCandidates(result.text)];
125869
+ return result.files;
125179
125870
  } catch (e) {
125180
125871
  logger25.debug("Windows clipboard file read skipped", { error: e });
125181
125872
  return [];
@@ -125581,7 +126272,7 @@ async function readStreamText(filePath, start) {
125581
126272
  function parseProcessedLines(raw, cursor, fileName, fingerprint) {
125582
126273
  const lastNewline = raw.lastIndexOf("\n");
125583
126274
  if (lastNewline < 0) {
125584
- return { entries: [], nextCursor: cursor, advanced: false };
126275
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "partial_line" };
125585
126276
  }
125586
126277
  const processed = raw.slice(0, lastNewline + 1);
125587
126278
  const lines = processed.split(/\r?\n/);
@@ -125607,7 +126298,8 @@ function parseProcessedLines(raw, cursor, fileName, fingerprint) {
125607
126298
  lineNum,
125608
126299
  ...fingerprint ? { fingerprint } : {}
125609
126300
  },
125610
- advanced: true
126301
+ advanced: true,
126302
+ reason: "advanced"
125611
126303
  };
125612
126304
  }
125613
126305
  function chunkEntries(entries, size) {
@@ -125665,24 +126357,55 @@ var BridgeLogUploader = class {
125665
126357
  async flushOnce() {
125666
126358
  if (this.running || this.stopped) return;
125667
126359
  this.running = true;
126360
+ const startedAt = Date.now();
126361
+ const summary = {
126362
+ targetCount: 0,
126363
+ advancedTargetCount: 0,
126364
+ missingTargetCount: 0,
126365
+ idleTargetCount: 0,
126366
+ partialLineTargetCount: 0,
126367
+ failedTargetCount: 0,
126368
+ parsedEntryCount: 0,
126369
+ bridgeEntryCount: 0,
126370
+ uploadedChunkCount: 0,
126371
+ accepted: 0,
126372
+ skipped: 0
126373
+ };
125668
126374
  try {
125669
126375
  const targets = await this.resolveTargets();
126376
+ summary.targetCount = targets.length;
125670
126377
  for (const target of targets) {
125671
126378
  try {
125672
126379
  const cursor = await readCursor(target.cursorFile);
125673
126380
  const batch = await this.readNewEntries(target, cursor);
125674
- if (!batch.advanced) continue;
126381
+ if (!batch.advanced) {
126382
+ summary.idleTargetCount += 1;
126383
+ if (batch.reason === "missing_file") summary.missingTargetCount += 1;
126384
+ if (batch.reason === "partial_line") summary.partialLineTargetCount += 1;
126385
+ continue;
126386
+ }
126387
+ summary.advancedTargetCount += 1;
126388
+ summary.parsedEntryCount += batch.entries.length;
125675
126389
  if (batch.entries.length > 0) {
125676
- await this.uploadEntries(batch.entries);
126390
+ const result = await this.uploadEntries(batch.entries);
126391
+ summary.bridgeEntryCount += result.bridgeEntryCount;
126392
+ summary.uploadedChunkCount += result.uploadedChunkCount;
126393
+ summary.accepted += result.accepted;
126394
+ summary.skipped += result.skipped;
125677
126395
  }
125678
126396
  await writeCursor(target.cursorFile, batch.nextCursor);
125679
126397
  } catch (e) {
126398
+ summary.failedTargetCount += 1;
125680
126399
  logger28.warn("Bridge log upload target failed", { error: e, logFile: target.logFile });
125681
126400
  }
125682
126401
  }
125683
126402
  } catch (e) {
125684
126403
  logger28.warn("Bridge log upload cycle failed", { error: e });
125685
126404
  } finally {
126405
+ logger28.info("Bridge log upload cycle summary", {
126406
+ ...summary,
126407
+ durationMs: Date.now() - startedAt
126408
+ });
125686
126409
  this.running = false;
125687
126410
  }
125688
126411
  }
@@ -125705,7 +126428,7 @@ var BridgeLogUploader = class {
125705
126428
  } catch (e) {
125706
126429
  if (e instanceof Error && "code" in e && e.code === "ENOENT") {
125707
126430
  logger28.debug("Bridge log file not found for upload yet", { logFile: target.logFile });
125708
- return { entries: [], nextCursor: cursor, advanced: false };
126431
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "missing_file" };
125709
126432
  }
125710
126433
  throw e;
125711
126434
  }
@@ -125713,13 +126436,19 @@ var BridgeLogUploader = class {
125713
126436
  const samePhysicalFile = !fingerprint || !cursor.fingerprint || cursor.fingerprint === fingerprint;
125714
126437
  const normalizedCursor = stat3.size < cursor.offset || !samePhysicalFile ? { offset: 0, lineNum: 0, ...fingerprint ? { fingerprint } : {} } : { ...cursor, ...fingerprint ? { fingerprint } : {} };
125715
126438
  if (stat3.size <= normalizedCursor.offset) {
125716
- return { entries: [], nextCursor: normalizedCursor, advanced: false };
126439
+ return { entries: [], nextCursor: normalizedCursor, advanced: false, reason: "no_new_entries" };
125717
126440
  }
125718
126441
  const raw = await readStreamText(target.logFile, normalizedCursor.offset);
125719
126442
  return parseProcessedLines(raw, normalizedCursor, target.uploadedFileName, fingerprint);
125720
126443
  }
125721
126444
  async uploadEntries(entries) {
125722
126445
  const bridgeEntries = entries.filter((entry) => entry.source === "bridge");
126446
+ const result = {
126447
+ bridgeEntryCount: bridgeEntries.length,
126448
+ uploadedChunkCount: 0,
126449
+ accepted: 0,
126450
+ skipped: 0
126451
+ };
125723
126452
  for (const chunk of chunkEntries(bridgeEntries, this.options.batchSize)) {
125724
126453
  const res = await fetch(`${this.options.serverApiUrl}/api/logs/upload`, {
125725
126454
  method: "POST",
@@ -125734,14 +126463,18 @@ var BridgeLogUploader = class {
125734
126463
  })
125735
126464
  });
125736
126465
  if (!res.ok) {
125737
- const body = await res.text().catch((e) => {
126466
+ const body2 = await res.text().catch((e) => {
125738
126467
  logger28.debug("Failed to read log upload error body", { error: e });
125739
126468
  return "";
125740
126469
  });
125741
- throw new Error(`upload failed HTTP ${res.status}: ${body.slice(0, 160)}`);
126470
+ throw new Error(`upload failed HTTP ${res.status}: ${body2.slice(0, 160)}`);
125742
126471
  }
125743
- await res.json();
126472
+ const body = await res.json();
126473
+ result.uploadedChunkCount += 1;
126474
+ result.accepted += typeof body.accepted === "number" ? body.accepted : 0;
126475
+ result.skipped += typeof body.skipped === "number" ? body.skipped : 0;
125744
126476
  }
126477
+ return result;
125745
126478
  }
125746
126479
  };
125747
126480
 
@@ -127739,6 +128472,33 @@ function syncLocalRuntimeSkills(skillStore, localSkills, options = {}) {
127739
128472
  ]);
127740
128473
  }
127741
128474
 
128475
+ // src/processOutput.ts
128476
+ init_cjs_shims();
128477
+ var protectedStreams2 = /* @__PURE__ */ new WeakSet();
128478
+ var reportedErrors = /* @__PURE__ */ new WeakSet();
128479
+ function reportWriteError(error51, onError) {
128480
+ if (typeof error51 === "object" && error51 !== null) {
128481
+ if (reportedErrors.has(error51)) return;
128482
+ reportedErrors.add(error51);
128483
+ }
128484
+ onError(error51);
128485
+ }
128486
+ function safeWriteProcessOutput(stream, text, onError) {
128487
+ if (!stream) return;
128488
+ if (stream.destroyed || stream.writableEnded) return;
128489
+ if (typeof stream === "object" && typeof stream.on === "function" && !protectedStreams2.has(stream)) {
128490
+ protectedStreams2.add(stream);
128491
+ stream.on("error", (e) => reportWriteError(e, onError));
128492
+ }
128493
+ try {
128494
+ stream.write(text, (error51) => {
128495
+ if (error51) reportWriteError(error51, onError);
128496
+ });
128497
+ } catch (e) {
128498
+ reportWriteError(e, onError);
128499
+ }
128500
+ }
128501
+
127742
128502
  // src/start.ts
127743
128503
  var logger41 = createModuleLogger("bridge");
127744
128504
  var NODE_USER_UID2 = 1e3;
@@ -127825,14 +128585,16 @@ async function startBridge(config2) {
127825
128585
  const claudeRuntime = resolveClaudeRuntime();
127826
128586
  logClaudeRuntimeResolution(claudeRuntime);
127827
128587
  if (!claudeRuntime.ok || !claudeRuntime.path) {
127828
- process.stderr.write(
128588
+ safeWriteProcessOutput(
128589
+ process.stderr,
127829
128590
  `
127830
128591
  Claude runtime is unavailable.
127831
128592
 
127832
128593
  ${claudeRuntime.error ?? "Install Claude Code manually or use the bundled desktop runtime."}
127833
128594
 
127834
128595
  Reinstall @fangyb/ahchat-bridge with npm optional dependencies, set AHCHAT_CLAUDE_EXECUTABLE to a valid Claude Code binary path, install Claude Code, or use ALL-CAN Desktop with its bundled runtime.
127835
- `
128596
+ `,
128597
+ (e) => logger41.error("Bridge process stderr write failed", { error: e })
127836
128598
  );
127837
128599
  process.exit(1);
127838
128600
  }
@@ -127889,12 +128651,14 @@ Reinstall @fangyb/ahchat-bridge with npm optional dependencies, set AHCHAT_CLAUD
127889
128651
  claudeRuntimeVersion: claudeRuntime.version ?? null
127890
128652
  });
127891
128653
  const shouldPrintRawBridgeToken = process.stdout.isTTY && process.env.AHCHAT_SUPPRESS_BRIDGE_TOKEN_STDOUT !== "1";
127892
- process.stdout.write(
128654
+ safeWriteProcessOutput(
128655
+ process.stdout,
127893
128656
  `
127894
128657
  Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\u673A\u5668):
127895
128658
  ${shouldPrintRawBridgeToken ? config2.bridgeToken : "***"}
127896
128659
 
127897
- `
128660
+ `,
128661
+ (e) => logger41.error("Bridge process stdout write failed", { error: e })
127898
128662
  );
127899
128663
  wsMetrics.start(5e3);
127900
128664
  const sessionStore = new SessionStore(config2.dataDir);
@@ -128693,6 +129457,19 @@ Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\
128693
129457
  });
128694
129458
  await agentManager.terminateScope(msg.payload.agentId, msg.payload.scope);
128695
129459
  break;
129460
+ case "spectate:set":
129461
+ logger41.info("spectate:set received", {
129462
+ agentId: msg.payload.agentId,
129463
+ scope: msg.payload.scope,
129464
+ action: msg.payload.action,
129465
+ traceId: msg.payload.traceId
129466
+ });
129467
+ await agentManager.setSpectate(
129468
+ msg.payload.agentId,
129469
+ msg.payload.scope,
129470
+ msg.payload.action
129471
+ );
129472
+ break;
128696
129473
  case "agent:created":
128697
129474
  agentRegistry.upsert(msg.payload.agent);
128698
129475
  ensureLocalWorkdirPath(msg.payload.agent.workingDirectory, "agent:created", {
@@ -128910,8 +129687,12 @@ function writeAlreadyRunningMessage(error51) {
128910
129687
  ` ${buildStopCommand(error51.pid)}`,
128911
129688
  ""
128912
129689
  ];
128913
- process.stdout.write(`${lines.join("\n")}
128914
- `);
129690
+ safeWriteProcessOutput(
129691
+ process.stdout,
129692
+ `${lines.join("\n")}
129693
+ `,
129694
+ (e) => logger42.error("Bridge already-running message write failed", { error: e })
129695
+ );
128915
129696
  }
128916
129697
  function handleBridgeStartError(e, message) {
128917
129698
  if (isBridgeAlreadyRunningError(e)) {