@fangyb/ahchat-bridge 0.1.35 → 0.1.37

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."),
@@ -116645,9 +116861,12 @@ function isAuthFailureText(text, sdkError) {
116645
116861
  function isProviderApiErrorText(text) {
116646
116862
  return /^API Error:\s*\d+/i.test(text.trim());
116647
116863
  }
116648
- function isNoReplyText(text) {
116864
+ function isSdkSyntheticNoResponseText(normalizedText) {
116865
+ return normalizedText.replace(/[.!。]+$/g, "") === "no response requested";
116866
+ }
116867
+ function isNoReplyText(text, opts) {
116649
116868
  const normalized = text.trim().replace(/^`+|`+$/g, "").trim().toLowerCase();
116650
- return normalized === NO_REPLY_TOKEN || normalized === "<no-reply>" || normalized === "no-reply" || normalized === "no_reply" || normalized === "no reply";
116869
+ return normalized === NO_REPLY_TOKEN || normalized === "<no-reply>" || normalized === "no-reply" || normalized === "no_reply" || normalized === "no reply" || opts?.allowSdkSyntheticNoResponse === true && isSdkSyntheticNoResponseText(normalized);
116651
116870
  }
116652
116871
  function decodeJsonStringFragment(raw) {
116653
116872
  let out = "";
@@ -116985,6 +117204,9 @@ function emitUsageReported(proc, emit, base, usage, messageId) {
116985
117204
  function isGroupTask(proc) {
116986
117205
  return proc.currentTask?.groupId != null;
116987
117206
  }
117207
+ function shouldStreamInternals(proc) {
117208
+ return !isGroupTask(proc) || proc.spectating === true;
117209
+ }
116988
117210
  function extractTodosFromInput(input) {
116989
117211
  if (!input || typeof input !== "object") return null;
116990
117212
  const raw = input.todos;
@@ -117116,7 +117338,6 @@ function emitGroupSegment(proc, emit, base, content, contentBlocks, isSilent = f
117116
117338
  contentLen: content.length,
117117
117339
  blockCount: contentBlocks.length,
117118
117340
  blockTypes: contentBlocks.map((b) => b.type),
117119
- contentSample: content.slice(0, 200),
117120
117341
  traceId: base.traceId,
117121
117342
  isAuditOnly: content.length === 0,
117122
117343
  isSilent
@@ -117135,7 +117356,7 @@ function emitGroupSegment(proc, emit, base, content, contentBlocks, isSilent = f
117135
117356
  }
117136
117357
  function flushTextSegmentOnBlockStop(proc, emit, base) {
117137
117358
  const trimmed = proc.segmentBuffer.trim();
117138
- const isSilent = isNoReplyText(trimmed);
117359
+ const isSilent = isNoReplyText(trimmed, { allowSdkSyntheticNoResponse: true });
117139
117360
  const isEmpty = trimmed.length === 0;
117140
117361
  if (!isEmpty) {
117141
117362
  const blocksForSegment = [...proc.contentBlocks, { type: "text", content: proc.segmentBuffer }];
@@ -117172,9 +117393,24 @@ function flushTextSegmentOnBlockStop(proc, emit, base) {
117172
117393
  }
117173
117394
  proc.segmentBuffer = "";
117174
117395
  }
117396
+ function describeSdkEvent(message) {
117397
+ const rec = message;
117398
+ const str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
117399
+ return {
117400
+ type: str(rec.type) ?? "unknown",
117401
+ subtype: str(rec.subtype),
117402
+ toolName: str(rec.last_tool_name),
117403
+ subagentType: str(rec.subagent_type),
117404
+ toolUseId: str(rec.tool_use_id),
117405
+ taskId: str(rec.task_id),
117406
+ at: Date.now()
117407
+ };
117408
+ }
117175
117409
  function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProviderApiError) {
117176
117410
  const emit = rawEmit;
117177
117411
  proc.lastSdkEventAt = Date.now();
117412
+ proc.lastSdkEventInfo = describeSdkEvent(message);
117413
+ proc.stallWarned = false;
117178
117414
  switch (message.type) {
117179
117415
  case "system": {
117180
117416
  const sysMsg = message;
@@ -117213,11 +117449,29 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117213
117449
  sessionId: proc.ccSessionId
117214
117450
  });
117215
117451
  } else {
117452
+ const sysRec = sysMsg;
117453
+ const pick2 = (k) => typeof sysRec[k] === "string" || typeof sysRec[k] === "number" ? sysRec[k] : void 0;
117454
+ const descriptionLen = typeof sysRec.description === "string" ? sysRec.description.length : void 0;
117455
+ const subagentTaskId = typeof sysRec.task_id === "string" ? sysRec.task_id : void 0;
117456
+ if (subagentTaskId) {
117457
+ if (sysMsg.subtype === "task_started") {
117458
+ (proc.activeSubagentTaskIds ??= /* @__PURE__ */ new Set()).add(subagentTaskId);
117459
+ } else if (sysMsg.subtype === "task_notification") {
117460
+ proc.activeSubagentTaskIds?.delete(subagentTaskId);
117461
+ }
117462
+ }
117216
117463
  logger11.info("SDK system subtype unhandled", {
117217
117464
  agentId: proc.agentId,
117218
117465
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
117219
117466
  subtype: sysMsg.subtype ?? "(none)",
117220
- keys: Object.keys(sysMsg).slice(0, 12)
117467
+ taskId: pick2("task_id"),
117468
+ toolUseId: pick2("tool_use_id"),
117469
+ subagentType: pick2("subagent_type"),
117470
+ taskType: pick2("task_type"),
117471
+ lastToolName: pick2("last_tool_name"),
117472
+ hasDescription: descriptionLen != null,
117473
+ descriptionLen,
117474
+ keys: Object.keys(sysMsg).slice(0, 16)
117221
117475
  });
117222
117476
  }
117223
117477
  break;
@@ -117243,13 +117497,14 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117243
117497
  } else if (block.type === "tool_use") {
117244
117498
  proc.currentBlockType = "tool_use";
117245
117499
  proc.currentToolName = block.name ?? "unknown";
117500
+ proc.activeToolUseStartedAt = Date.now();
117246
117501
  proc.accumulatedToolInput = "";
117247
117502
  const toolName = block.name ?? "unknown";
117248
117503
  proc.suppressCurrentToolUse = proc.officialMediaGenerationSatisfied === true && isOfficialMediaGenerationToolName(toolName);
117249
117504
  const isMcpTool = parseMcpRuntimeToolName(toolName) != null;
117250
117505
  proc.currentMcpInvocationId = isMcpTool ? createMcpToolInvocationId() : null;
117251
117506
  proc.currentMcpInvocationStartedAt = isMcpTool ? (/* @__PURE__ */ new Date()).toISOString() : null;
117252
- if (!isGroupTask(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
117507
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
117253
117508
  emit({
117254
117509
  type: "agent:tool_use",
117255
117510
  payload: {
@@ -117274,7 +117529,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117274
117529
  if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
117275
117530
  if (proc.suppressCurrentThinking) break;
117276
117531
  proc.accumulatedThinking += delta.thinking;
117277
- if (!isGroupTask(proc)) {
117532
+ if (shouldStreamInternals(proc)) {
117278
117533
  emit({
117279
117534
  type: "agent:thinking_chunk",
117280
117535
  payload: { ...wireBase(base), chunk: delta.thinking }
@@ -117285,7 +117540,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117285
117540
  if (typeof partial2 === "string") {
117286
117541
  proc.accumulatedToolInput += partial2;
117287
117542
  const liveInput = extractLiveToolInput(proc.currentToolName, proc.accumulatedToolInput);
117288
- if (!isGroupTask(proc) && liveInput && proc.currentToolName != null) {
117543
+ if (shouldStreamInternals(proc) && liveInput && proc.currentToolName != null) {
117289
117544
  emit({
117290
117545
  type: "agent:tool_input_update",
117291
117546
  payload: {
@@ -117319,7 +117574,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117319
117574
  }
117320
117575
  case "content_block_stop": {
117321
117576
  if (proc.currentBlockType === "thinking") {
117322
- if (!isGroupTask(proc) && !proc.suppressCurrentThinking) {
117577
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentThinking) {
117323
117578
  emit({
117324
117579
  type: "agent:thinking_done",
117325
117580
  payload: wireBase(getTaskBase(proc))
@@ -117344,8 +117599,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117344
117599
  error: error51,
117345
117600
  agentId: proc.agentId,
117346
117601
  toolName: proc.currentToolName,
117347
- inputLen: proc.accumulatedToolInput.length,
117348
- sample: proc.accumulatedToolInput.slice(0, 200)
117602
+ inputLen: proc.accumulatedToolInput.length
117349
117603
  });
117350
117604
  }
117351
117605
  }
@@ -117354,7 +117608,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117354
117608
  if (lastToolUse && lastToolUse.type === "tool_use") {
117355
117609
  lastToolUse.input = parsedInput;
117356
117610
  }
117357
- if (!isGroupTask(proc) && proc.currentToolName != null && LIVE_INPUT_PREVIEW_TOOLS.has(proc.currentToolName) && Object.keys(parsedInput).length > 0) {
117611
+ if (shouldStreamInternals(proc) && proc.currentToolName != null && LIVE_INPUT_PREVIEW_TOOLS.has(proc.currentToolName) && Object.keys(parsedInput).length > 0) {
117358
117612
  emit({
117359
117613
  type: "agent:tool_input_update",
117360
117614
  payload: {
@@ -117478,7 +117732,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117478
117732
  blockTypes,
117479
117733
  hasToolResult,
117480
117734
  hasPlainText,
117481
- contentSample: typeof content === "string" ? content.slice(0, 200) : JSON.stringify(content).slice(0, 200)
117735
+ contentLen: typeof content === "string" ? content.length : JSON.stringify(content).length
117482
117736
  });
117483
117737
  break;
117484
117738
  }
@@ -117487,7 +117741,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117487
117741
  agentId: proc.agentId,
117488
117742
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
117489
117743
  blockTypes,
117490
- contentSample: JSON.stringify(content).slice(0, 300),
117744
+ contentLen: JSON.stringify(content).length,
117491
117745
  replyMessageId: base.replyMessageId
117492
117746
  });
117493
117747
  }
@@ -117508,6 +117762,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117508
117762
  });
117509
117763
  proc.currentMcpInvocationId = null;
117510
117764
  proc.currentMcpInvocationStartedAt = null;
117765
+ proc.activeToolUseStartedAt = void 0;
117511
117766
  proc.currentToolName = null;
117512
117767
  continue;
117513
117768
  }
@@ -117515,6 +117770,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117515
117770
  proc.officialMediaGenerationSatisfied = true;
117516
117771
  }
117517
117772
  if (isAskUserQuestionToolName(toolName)) {
117773
+ proc.activeToolUseStartedAt = void 0;
117518
117774
  proc.currentToolName = null;
117519
117775
  continue;
117520
117776
  }
@@ -117542,7 +117798,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117542
117798
  proc.currentMcpInvocationId = null;
117543
117799
  proc.currentMcpInvocationStartedAt = null;
117544
117800
  }
117545
- if (!isGroupTask(proc)) {
117801
+ if (shouldStreamInternals(proc)) {
117546
117802
  emit({
117547
117803
  type: "agent:tool_result",
117548
117804
  payload: {
@@ -117566,6 +117822,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117566
117822
  }
117567
117823
  }
117568
117824
  }
117825
+ proc.activeToolUseStartedAt = void 0;
117569
117826
  }
117570
117827
  }
117571
117828
  }
@@ -117622,7 +117879,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117622
117879
  const groupMode = groupId != null;
117623
117880
  const usage = extractUsage(successMsg);
117624
117881
  const watermarkUsage = proc.peakContextUsage ?? usage;
117625
- if (isNoReplyText(trimmed)) {
117882
+ if (isNoReplyText(trimmed, { allowSdkSyntheticNoResponse: groupMode })) {
117626
117883
  checkInputTokenWatermark(proc, watermarkUsage, base.traceId);
117627
117884
  emitUsageReported(proc, emit, base, usage);
117628
117885
  if (groupMode && proc.contentBlocks.length > 0) {
@@ -117644,7 +117901,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117644
117901
  groupId,
117645
117902
  compactScheduled: proc.compactRequested === true,
117646
117903
  fullTextLen: proc.accumulatedText.length,
117647
- fullTextSample: proc.accumulatedText.slice(0, 200),
117648
117904
  accumulatedBlockCount: proc.contentBlocks.length,
117649
117905
  accumulatedBlockTypes: proc.contentBlocks.map((b) => b.type),
117650
117906
  silentSegmentEmitted: groupMode
@@ -117688,7 +117944,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117688
117944
  segmentCount: proc.segmentCount,
117689
117945
  compactScheduled: proc.compactRequested === true,
117690
117946
  fullTextLen: proc.accumulatedText.length,
117691
- fullTextSample: proc.accumulatedText.slice(0, 200),
117692
117947
  traceId: base.traceId
117693
117948
  });
117694
117949
  emit({
@@ -117739,7 +117994,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117739
117994
  ackId: base.replyMessageId,
117740
117995
  messageId: carrierMessageId,
117741
117996
  textLen: proc.accumulatedText.length,
117742
- textSample: proc.accumulatedText.slice(0, 200),
117743
117997
  tokenCount: usage.tokenCount,
117744
117998
  traceId: base.traceId
117745
117999
  });
@@ -117932,8 +118186,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117932
118186
  logger11.info("Captured non-streamed assistant message", {
117933
118187
  agentId: proc.agentId,
117934
118188
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
117935
- textLen: text.length,
117936
- textSample: text.slice(0, 100)
118189
+ textLen: text.length
117937
118190
  });
117938
118191
  } else {
117939
118192
  proc.lastAssistantContentDescription = describeAssistantContent(am.message?.content);
@@ -117957,6 +118210,7 @@ function resetAccumulators(proc) {
117957
118210
  proc.currentToolName = null;
117958
118211
  proc.currentMcpInvocationId = null;
117959
118212
  proc.currentMcpInvocationStartedAt = null;
118213
+ proc.activeToolUseStartedAt = void 0;
117960
118214
  proc.segmentBuffer = "";
117961
118215
  proc.segmentCount = 0;
117962
118216
  proc.accumulatedToolInput = "";
@@ -117966,6 +118220,7 @@ function resetAccumulators(proc) {
117966
118220
  proc.officialMediaGenerationSatisfied = false;
117967
118221
  proc.suppressCurrentThinking = false;
117968
118222
  proc.suppressCurrentToolUse = false;
118223
+ proc.activeSubagentTaskIds?.clear();
117969
118224
  }
117970
118225
 
117971
118226
  // src/forkHistoryReplay.ts
@@ -118157,7 +118412,7 @@ function missingSubscriptionMessage(subscriptionId) {
118157
118412
  }
118158
118413
  var NODE_USER_UID = 1e3;
118159
118414
  var POST_MERGE_CONTINUATION_ROUTE_MS = 15e3;
118160
- var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-prompt-v2";
118415
+ var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-mcp-abi-prompt-v4";
118161
118416
  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
118417
  var DOCUMENT_READING_RULES = `DOCUMENT READING:
118163
118418
  - The built-in Read tool cannot read binary office documents such as .docx, .xls, .xlsx, .pptx, .pdf, .odt, .ods, .odp, or .rtf.
@@ -118172,6 +118427,16 @@ var MEDIA_GENERATION_RULES = `MEDIA GENERATION:
118172
118427
  - 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
118428
  - 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
118429
  - 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.`;
118430
+ function stableFingerprintValue(value) {
118431
+ if (Array.isArray(value)) return value.map(stableFingerprintValue);
118432
+ if (!value || typeof value !== "object") return value;
118433
+ const out = {};
118434
+ for (const key of Object.keys(value).sort()) {
118435
+ const normalized = stableFingerprintValue(value[key]);
118436
+ if (normalized !== void 0) out[key] = normalized;
118437
+ }
118438
+ return out;
118439
+ }
118175
118440
  function isRecoveryDispatchTask(task) {
118176
118441
  return task.dispatchKind === "manual_continue" || task.dispatchKind === "regenerate";
118177
118442
  }
@@ -118429,13 +118694,15 @@ var AgentManager = class {
118429
118694
  agents = /* @__PURE__ */ new Map();
118430
118695
  lastUsedAt = /* @__PURE__ */ new Map();
118431
118696
  /** Scopes 被 zombie_watchdog 关闭后的"入睡"标记,acquire 重建时清除并 emit awake。 */
118432
- dormantScopes = /* @__PURE__ */ new Set();
118697
+ dormantScopes = /* @__PURE__ */ new Map();
118433
118698
  /**
118434
118699
  * zombie_watchdog 拆 runtime 时,把该 (agentId, scope) 的 groupInbox 快照到这里,
118435
118700
  * 让下一次 getOrCreate 重建 runtime 时可以恢复未读消息。仅 in-memory;
118436
118701
  * bridge 进程崩溃 / shutdownAll 时丢失,与现有 inbox 内存语义一致。
118437
118702
  */
118438
118703
  dormantGroupInboxes = /* @__PURE__ */ new Map();
118704
+ /** Spectate requested before runtime existed; value = activatedAt epoch ms. */
118705
+ pendingSpectate = /* @__PURE__ */ new Map();
118439
118706
  sessionStore;
118440
118707
  dispatchMemory = new GroupDispatchMemoryStore();
118441
118708
  dataDir;
@@ -118601,6 +118868,7 @@ var AgentManager = class {
118601
118868
  }
118602
118869
  async resolveRuntimeCwd(agentConfig, scope, requestedCwd) {
118603
118870
  let cwd = this.remapServerWorkspaceCwd(agentConfig, scope, requestedCwd);
118871
+ let fallbackForensicsId;
118604
118872
  if (!isFullyQualifiedAbsolutePath(cwd)) {
118605
118873
  const fallback = import_node_path13.default.join(this.workspacesDir, this.localScopeDirName(agentConfig, scope));
118606
118874
  logger14.error(
@@ -118615,6 +118883,23 @@ var AgentManager = class {
118615
118883
  error: new Error("workdir_not_usable_on_this_machine")
118616
118884
  }
118617
118885
  );
118886
+ fallbackForensicsId = createFallbackId();
118887
+ logFallback(logger14, {
118888
+ fallbackId: fallbackForensicsId,
118889
+ type: "cwd_sandbox",
118890
+ phase: "applied",
118891
+ expected: false,
118892
+ context: {
118893
+ agentId: agentConfig.id,
118894
+ scope: scopeKey(scope),
118895
+ platform: process.platform,
118896
+ requested: requestedCwd,
118897
+ resolved: cwd,
118898
+ reason: "not_fully_qualified",
118899
+ fallback
118900
+ },
118901
+ outcome: { result: "sandbox_fallback" }
118902
+ });
118618
118903
  cwd = fallback;
118619
118904
  }
118620
118905
  if (isRunningAsRoot() && cwd.startsWith("/root/")) {
@@ -118633,6 +118918,21 @@ var AgentManager = class {
118633
118918
  fallback,
118634
118919
  error: e
118635
118920
  });
118921
+ const fbId = fallbackForensicsId ?? createFallbackId();
118922
+ logFallback(logger14, {
118923
+ fallbackId: fbId,
118924
+ type: "cwd_sandbox",
118925
+ phase: "applied",
118926
+ expected: false,
118927
+ context: {
118928
+ agentId: agentConfig.id,
118929
+ scope: scopeKey(scope),
118930
+ reason: "mkdir_failed",
118931
+ requested: cwd,
118932
+ fallback
118933
+ },
118934
+ outcome: { result: "second_layer_fallback" }
118935
+ });
118636
118936
  await import_promises3.default.mkdir(fallback, { recursive: true });
118637
118937
  return fallback;
118638
118938
  }
@@ -118855,17 +119155,24 @@ var AgentManager = class {
118855
119155
  });
118856
119156
  return null;
118857
119157
  }
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");
119158
+ scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection, externalMcpFingerprint) {
119159
+ 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");
119160
+ }
119161
+ externalMcpFingerprint(externalMcp) {
119162
+ const serverNames = Object.keys(externalMcp.mcpServers).sort();
119163
+ const allowedTools = [...externalMcp.allowedTools].sort();
119164
+ const toolAbi = [...externalMcp.toolAbi ?? []].sort((a, b) => a.serverName.localeCompare(b.serverName)).map(stableFingerprintValue);
119165
+ if (serverNames.length === 0 && allowedTools.length === 0 && toolAbi.length === 0) return "";
119166
+ return JSON.stringify({ serverNames, allowedTools, toolAbi });
118860
119167
  }
118861
- discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint) {
119168
+ discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint, options = {}) {
118862
119169
  const previous = this.sessionStore.getPromptFingerprint(agentConfig.id, scope);
118863
119170
  if (!sessionId) {
118864
119171
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
118865
119172
  return null;
118866
119173
  }
118867
119174
  if (previous === fingerprint) return sessionId;
118868
- if (!previous && scope.kind === "single") {
119175
+ if (!previous && scope.kind === "single" && options.clearLegacySingleSession !== true) {
118869
119176
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
118870
119177
  logger14.info("Retaining legacy single-scope session while recording prompt fingerprint", {
118871
119178
  agentId: agentConfig.id,
@@ -118885,7 +119192,8 @@ var AgentManager = class {
118885
119192
  sessionId,
118886
119193
  previousFingerprint: previous,
118887
119194
  nextFingerprint: fingerprint,
118888
- revision: SCOPE_PROMPT_FINGERPRINT_REVISION
119195
+ revision: SCOPE_PROMPT_FINGERPRINT_REVISION,
119196
+ reason: options.reason ?? "scope_prompt_changed"
118889
119197
  });
118890
119198
  return null;
118891
119199
  }
@@ -118934,6 +119242,7 @@ var AgentManager = class {
118934
119242
  logger14.info("Evicting idle Agent query", { agentId: proc.agentId, scope: scopeKey(proc.scope) });
118935
119243
  const runtime = this.asRuntime(proc);
118936
119244
  this.clearQuietFlushTimer(runtime);
119245
+ this.teardownSpectate(runtime);
118937
119246
  try {
118938
119247
  runtime.inputController.close();
118939
119248
  await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
@@ -118953,6 +119262,7 @@ var AgentManager = class {
118953
119262
  const runtime = this.asRuntime(proc);
118954
119263
  const key = runtimeKey(proc.agentId, proc.scope);
118955
119264
  this.clearQuietFlushTimer(runtime);
119265
+ this.teardownSpectate(runtime);
118956
119266
  runtime.currentTask = null;
118957
119267
  runtime.injectedTasks = [];
118958
119268
  runtime.mergedTasks = [];
@@ -118989,6 +119299,7 @@ var AgentManager = class {
118989
119299
  evictIdle() {
118990
119300
  const now = Date.now();
118991
119301
  const { idleTimeoutMs, workingSilenceTimeoutMs } = this.queryConfig;
119302
+ const stallWarnAfterMs = Math.min(9e4, this.queryConfig.replyStallTimeoutMs);
118992
119303
  for (const [key, proc] of this.agents) {
118993
119304
  if (!this.isEvictable(proc)) continue;
118994
119305
  const runtime = this.asRuntime(proc);
@@ -118999,26 +119310,62 @@ var AgentManager = class {
118999
119310
  for (const [, proc] of this.agents) {
119000
119311
  if (proc.status !== "working") continue;
119001
119312
  const runtime = this.asRuntime(proc);
119313
+ if (runtime.currentTask) {
119314
+ const sinceEventMs = now - proc.lastSdkEventAt;
119315
+ if (sinceEventMs > stallWarnAfterMs && !proc.stallWarned) {
119316
+ proc.stallWarned = true;
119317
+ const openTool = this.latestOpenToolUse(proc);
119318
+ logger14.warn("Reply stall onset: in-flight reply silent", {
119319
+ agentId: proc.agentId,
119320
+ scope: scopeKey(proc.scope),
119321
+ sinceEventMs,
119322
+ replyStallTimeoutMs: this.queryConfig.replyStallTimeoutMs,
119323
+ workingSilenceTimeoutMs,
119324
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
119325
+ replyMessageId: runtime.currentTask.replyMessageId,
119326
+ model: proc.model ?? "(unknown)",
119327
+ lastSdkEvent: proc.lastSdkEventInfo,
119328
+ hasActiveToolUse: runtime.activeToolUseStartedAt != null || openTool != null,
119329
+ activeToolUseAgeMs: runtime.activeToolUseStartedAt != null ? now - runtime.activeToolUseStartedAt : null,
119330
+ openToolName: openTool?.toolName ?? proc.currentToolName ?? null,
119331
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null,
119332
+ subagentInFlight: (runtime.activeSubagentTaskIds?.size ?? 0) > 0,
119333
+ activeSubagentCount: runtime.activeSubagentTaskIds?.size ?? 0,
119334
+ busyReason: this.busyReason(proc)
119335
+ });
119336
+ }
119337
+ }
119338
+ const busyReason = this.busyReason(runtime);
119339
+ const busy = busyReason !== null;
119340
+ if (runtime.currentTask && !busy && now - proc.lastSdkEventAt > this.queryConfig.replyStallTimeoutMs) {
119341
+ void this.recoverStalledReply(proc, now - proc.lastSdkEventAt);
119342
+ continue;
119343
+ }
119002
119344
  const hasInjectedBacklog = runtime.injectedTasks.length > 0;
119003
- const effectiveTimeoutMs = hasInjectedBacklog ? workingSilenceTimeoutMs * 2 : workingSilenceTimeoutMs;
119345
+ const baseCeilingMs = busy ? Math.max(workingSilenceTimeoutMs, this.queryConfig.busySilenceTimeoutMs ?? 0) : workingSilenceTimeoutMs;
119346
+ const effectiveTimeoutMs = hasInjectedBacklog ? baseCeilingMs * 2 : baseCeilingMs;
119004
119347
  const silentMs = now - proc.lastSdkEventAt;
119005
119348
  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
- );
119349
+ if (silentMs > workingSilenceTimeoutMs) {
119350
+ logger14.warn("Zombie watchdog: working runtime silent past base timeout; granting extended grace", {
119351
+ agentId: proc.agentId,
119352
+ scope: scopeKey(proc.scope),
119353
+ silentMs,
119354
+ baseTimeoutMs: workingSilenceTimeoutMs,
119355
+ baseCeilingMs,
119356
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
119357
+ effectiveTimeoutMs,
119358
+ busy,
119359
+ busyReason,
119360
+ injectedTaskCount: runtime.injectedTasks.length,
119361
+ replyMessageId: proc.currentTask?.replyMessageId
119362
+ });
119019
119363
  }
119020
119364
  continue;
119021
119365
  }
119366
+ const zombieOpenTool = this.latestOpenToolUse(proc);
119367
+ const watchdogDetectedAt = Date.now();
119368
+ const watchdogFallbackId = createFallbackId();
119022
119369
  logger14.warn("Zombie watchdog: working runtime silent too long, tearing down", {
119023
119370
  agentId: proc.agentId,
119024
119371
  scope: scopeKey(proc.scope),
@@ -119026,12 +119373,46 @@ var AgentManager = class {
119026
119373
  lastSdkEventAt: new Date(proc.lastSdkEventAt).toISOString(),
119027
119374
  workingSilenceTimeoutMs,
119028
119375
  effectiveTimeoutMs,
119376
+ baseCeilingMs,
119377
+ busy,
119378
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
119029
119379
  replyMessageId: proc.currentTask?.replyMessageId,
119030
119380
  injectedTaskCount: runtime.injectedTasks.length,
119031
119381
  hadInjectedBacklog: hasInjectedBacklog,
119032
- inboxSize: proc.groupInbox.length
119382
+ inboxSize: proc.groupInbox.length,
119383
+ fallbackId: watchdogFallbackId,
119384
+ // Breadcrumb: what the turn was waiting on when it timed out. A hung subagent
119385
+ // (open `Task` tool_use) lands here now that the fast path skips active tools.
119386
+ model: proc.model ?? "(unknown)",
119387
+ lastSdkEvent: proc.lastSdkEventInfo,
119388
+ openToolName: zombieOpenTool?.toolName ?? proc.currentToolName ?? null,
119389
+ activeToolUseAgeMs: runtime.activeToolUseStartedAt != null ? now - runtime.activeToolUseStartedAt : null,
119390
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null,
119391
+ subagentInFlight: (runtime.activeSubagentTaskIds?.size ?? 0) > 0,
119392
+ activeSubagentCount: runtime.activeSubagentTaskIds?.size ?? 0,
119393
+ busyReason: this.busyReason(proc)
119394
+ });
119395
+ logFallback(logger14, {
119396
+ fallbackId: watchdogFallbackId,
119397
+ type: "zombie_watchdog",
119398
+ phase: "detected",
119399
+ expected: false,
119400
+ traceId: proc.currentTask?.traceId,
119401
+ context: {
119402
+ agentId: proc.agentId,
119403
+ scope: scopeKey(proc.scope),
119404
+ silentMs,
119405
+ replyMessageId: proc.currentTask?.replyMessageId ?? null,
119406
+ openToolName: zombieOpenTool?.toolName ?? proc.currentToolName ?? null,
119407
+ lastSdkEventType: proc.lastSdkEventInfo?.type ?? null,
119408
+ injectedTaskCount: runtime.injectedTasks.length,
119409
+ inboxSize: proc.groupInbox.length
119410
+ }
119411
+ });
119412
+ void this.closeRuntime(proc, "zombie_watchdog", {
119413
+ fallbackId: watchdogFallbackId,
119414
+ detectedAt: watchdogDetectedAt
119033
119415
  });
119034
- void this.closeRuntime(proc, "zombie_watchdog");
119035
119416
  }
119036
119417
  }
119037
119418
  /**
@@ -119278,7 +119659,7 @@ ${cfg.instructions.trim()}` : "";
119278
119659
  agentId: agentConfig.id,
119279
119660
  capabilityTier: cfg.capabilityTier,
119280
119661
  isSmith: smithAgent
119281
- }) ?? { mcpServers: {}, allowedTools: [] };
119662
+ }) ?? { mcpServers: {}, allowedTools: [], toolAbi: [] };
119282
119663
  logger14.info("External MCP resolved for runtime", {
119283
119664
  agentId: agentConfig.id,
119284
119665
  scope: scopeKey(scope),
@@ -119295,11 +119676,16 @@ ${cfg.instructions.trim()}` : "";
119295
119676
  }
119296
119677
  const notebookSection = this.buildNotebookSection(agentConfig.id);
119297
119678
  const scopesSection = this.buildScopesSection(agentConfig, scope, agentCwd);
119679
+ const externalMcpFingerprint = this.externalMcpFingerprint(externalMcp);
119298
119680
  savedSessionId = this.discardSessionIfScopePromptChanged(
119299
119681
  agentConfig,
119300
119682
  scope,
119301
119683
  savedSessionId,
119302
- this.scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection)
119684
+ this.scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection, externalMcpFingerprint),
119685
+ {
119686
+ clearLegacySingleSession: externalMcpFingerprint.length > 0,
119687
+ reason: externalMcpFingerprint.length > 0 ? "external_mcp_abi_changed" : "scope_prompt_changed"
119688
+ }
119303
119689
  );
119304
119690
  let forkHistorySection = "";
119305
119691
  if (!savedSessionId && scope.kind === "single") {
@@ -119320,6 +119706,8 @@ ${cfg.instructions.trim()}` : "";
119320
119706
  }
119321
119707
  }
119322
119708
  const cronLockSnapshot = readCronLockSnapshot();
119709
+ const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
119710
+ const disallowedToolsForRuntime = builtinWebSearchAllowed ? [] : ["WebSearch"];
119323
119711
  logger14.info("Creating Agent query", {
119324
119712
  agentId: agentConfig.id,
119325
119713
  scope: scopeKey(scope),
@@ -119328,6 +119716,8 @@ ${cfg.instructions.trim()}` : "";
119328
119716
  sessionId: savedSessionId,
119329
119717
  forkHistoryReplay: forkHistorySection.length > 0,
119330
119718
  model: cfg.model ?? "(default)",
119719
+ builtinWebSearchAllowed,
119720
+ disallowedTools: disallowedToolsForRuntime,
119331
119721
  // Diagnostic: who currently owns Claude's global cron lock (~/.claude/scheduled_tasks.lock).
119332
119722
  // Cron is process-internal but the binary uses this singleton lock to elect ONE scheduler
119333
119723
  // among concurrent claude subprocesses. If lock is held by another session at spawn time,
@@ -119339,7 +119729,6 @@ ${cfg.instructions.trim()}` : "";
119339
119729
  });
119340
119730
  const planModeRef = { active: false, denyCount: 0 };
119341
119731
  const mediaGenerationTurnGuard = createOfficialMediaGenerationTurnGuard();
119342
- const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
119343
119732
  const options = {
119344
119733
  cwd: agentCwd,
119345
119734
  systemPrompt: {
@@ -119406,6 +119795,8 @@ ${cfg.instructions.trim()}` : "";
119406
119795
  ] : [],
119407
119796
  ...externalMcp.allowedTools
119408
119797
  ],
119798
+ // Server-side WebSearch bypasses canUseTool; disallowedTools removes it from model context.
119799
+ disallowedTools: disallowedToolsForRuntime,
119409
119800
  mcpServers: { ...externalMcp.mcpServers, neural: neuralServer },
119410
119801
  includePartialMessages: true,
119411
119802
  // Plan mode custom workflow instructions. When setPermissionMode('plan') is
@@ -119732,6 +120123,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119732
120123
  currentTask: null,
119733
120124
  currentTaskStartedAt: 0,
119734
120125
  lastSdkEventAt: Date.now(),
120126
+ model: (typeof options.model === "string" ? options.model : cfg.model) ?? null,
119735
120127
  compactRequested: false,
119736
120128
  compactInProgress: false,
119737
120129
  contextOverflowLockedUntil: 0,
@@ -119743,13 +120135,18 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119743
120135
  currentToolName: null,
119744
120136
  currentMcpInvocationId: null,
119745
120137
  currentMcpInvocationStartedAt: null,
120138
+ activeSubagentTaskIds: /* @__PURE__ */ new Set(),
119746
120139
  mcpAuditRecorder: this.mcpAuditRecorder,
119747
120140
  segmentBuffer: "",
119748
120141
  segmentCount: 0,
119749
120142
  accumulatedToolInput: "",
119750
120143
  planModeRef,
119751
120144
  mediaGenerationTurnGuard,
119752
- groupInbox: []
120145
+ groupInbox: [],
120146
+ spectating: false,
120147
+ spectateActivatedAt: 0,
120148
+ spectateViewing: false,
120149
+ spectateTtlExpired: false
119753
120150
  };
119754
120151
  const runtime = Object.assign(proc, {
119755
120152
  query: agentQuery,
@@ -119761,7 +120158,8 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119761
120158
  createdAt: Date.now(),
119762
120159
  supportsVision: modelInputMode === "vision" && cfg.supportsVision !== false,
119763
120160
  modelInputMode,
119764
- quietFlushTimer: null
120161
+ quietFlushTimer: null,
120162
+ spectateRevertTimer: null
119765
120163
  });
119766
120164
  logger14.info("Agent model input mode resolved", {
119767
120165
  agentId: agentConfig.id,
@@ -119786,6 +120184,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119786
120184
  } else {
119787
120185
  this.dormantGroupInboxes.delete(key);
119788
120186
  }
120187
+ const dormantMeta = this.dormantScopes.get(key);
119789
120188
  if (this.dormantScopes.delete(key)) {
119790
120189
  this.emit({
119791
120190
  type: "agent:awake",
@@ -119797,7 +120196,44 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119797
120196
  });
119798
120197
  logger14.info("Agent scope awakened after dormant", {
119799
120198
  agentId: agentConfig.id,
119800
- scope: scopeKey(scope)
120199
+ scope: scopeKey(scope),
120200
+ ...dormantMeta ? {
120201
+ fallbackId: dormantMeta.fallbackId,
120202
+ dormantDurationMs: Date.now() - dormantMeta.detectedAt,
120203
+ dataLossSuspected: dormantMeta.droppedTaskCount > 0
120204
+ } : {}
120205
+ });
120206
+ if (dormantMeta) {
120207
+ logFallback(logger14, {
120208
+ fallbackId: dormantMeta.fallbackId,
120209
+ type: "zombie_watchdog",
120210
+ phase: "outcome",
120211
+ expected: false,
120212
+ context: {
120213
+ agentId: agentConfig.id,
120214
+ scope: scopeKey(scope)
120215
+ },
120216
+ outcome: {
120217
+ result: "recovered_rebuilt",
120218
+ durationMs: Date.now() - dormantMeta.detectedAt,
120219
+ dataLossSuspected: dormantMeta.droppedTaskCount > 0
120220
+ }
120221
+ });
120222
+ }
120223
+ }
120224
+ const pendingSpectateAt = this.pendingSpectate.get(key);
120225
+ if (pendingSpectateAt != null) {
120226
+ this.pendingSpectate.delete(key);
120227
+ runtime.spectating = true;
120228
+ runtime.spectateViewing = false;
120229
+ runtime.spectateActivatedAt = pendingSpectateAt;
120230
+ runtime.spectateTtlExpired = false;
120231
+ this.armSpectateTimer(runtime);
120232
+ this.emitSpectateState(runtime, true, "started");
120233
+ logger14.info("Applied pending spectate on runtime create", {
120234
+ agentId: agentConfig.id,
120235
+ scope: scopeKey(scope),
120236
+ activatedAt: pendingSpectateAt
119801
120237
  });
119802
120238
  }
119803
120239
  if (proc.groupInbox.length > 0 && this.isRuntimeIdleForInboxFlush(runtime)) {
@@ -120935,7 +121371,6 @@ ${lines.join("\n")}`;
120935
121371
  compactTrigger: "context_watermark",
120936
121372
  injectedTasksWaiting: runtime.injectedTasks.length,
120937
121373
  compactPromptLen: compactPrompt.length,
120938
- promptSample: compactPrompt.slice(0, 80),
120939
121374
  traceId: compactTraceId
120940
121375
  });
120941
121376
  runtime.inputController.push(compactPrompt, runtime.ccSessionId ?? "");
@@ -121217,7 +121652,7 @@ ${lines.join("\n")}`;
121217
121652
  const enveloped = buildInnerVoiceEnvelope(payloadWithTrigger, ctx);
121218
121653
  const task = {
121219
121654
  content: enveloped,
121220
- replyMessageId: createMessageId(),
121655
+ replyMessageId: createNeuralSendReplyMessageId(),
121221
121656
  conversationId: payload.conversationId,
121222
121657
  traceId: createTraceId(),
121223
121658
  groupId: payload.groupId
@@ -121463,7 +121898,7 @@ ${lines.join("\n")}`;
121463
121898
  this.dormantScopes.delete(key);
121464
121899
  this.dormantGroupInboxes.delete(key);
121465
121900
  }
121466
- for (const key of [...this.dormantScopes].filter(
121901
+ for (const key of [...this.dormantScopes.keys()].filter(
121467
121902
  (k) => k === agentId || k.startsWith(`${agentId}::`)
121468
121903
  )) {
121469
121904
  this.dormantScopes.delete(key);
@@ -121511,7 +121946,7 @@ ${lines.join("\n")}`;
121511
121946
  async reloadAgentScopes(agentId, reason) {
121512
121947
  this.sessionStore.deleteAllForAgent(agentId);
121513
121948
  this.dispatchMemory.deleteAllForAgent(agentId);
121514
- for (const key of [...this.dormantScopes].filter(
121949
+ for (const key of [...this.dormantScopes.keys()].filter(
121515
121950
  (k) => k === agentId || k.startsWith(`${agentId}::`)
121516
121951
  )) {
121517
121952
  this.dormantScopes.delete(key);
@@ -121663,6 +122098,125 @@ ${lines.join("\n")}`;
121663
122098
  void this.terminateScope(proc.agentId, proc.scope);
121664
122099
  }
121665
122100
  }
122101
+ /** Control spectate capture/push for one scoped runtime. */
122102
+ async setSpectate(agentId, scope, action) {
122103
+ const key = runtimeKey(agentId, scope);
122104
+ const proc = this.agents.get(key);
122105
+ if (!proc || proc.status === "dead") {
122106
+ if (action === "start") {
122107
+ this.pendingSpectate.set(key, Date.now());
122108
+ logger14.info("setSpectate: runtime missing, pending start", { agentId, scope: scopeKey(scope) });
122109
+ }
122110
+ return;
122111
+ }
122112
+ const runtime = this.asRuntime(proc);
122113
+ switch (action) {
122114
+ case "start":
122115
+ runtime.spectating = true;
122116
+ runtime.spectateViewing = true;
122117
+ runtime.spectateActivatedAt = Date.now();
122118
+ runtime.spectateTtlExpired = false;
122119
+ this.pendingSpectate.delete(key);
122120
+ this.armSpectateTimer(runtime);
122121
+ this.emitSpectateState(runtime, true, "started");
122122
+ logger14.info("Spectate started", { agentId, scope: scopeKey(scope) });
122123
+ break;
122124
+ case "enter_view":
122125
+ runtime.spectateViewing = true;
122126
+ logger14.info("Spectate enter_view", { agentId, scope: scopeKey(scope) });
122127
+ break;
122128
+ case "leave_view":
122129
+ runtime.spectateViewing = false;
122130
+ logger14.info("Spectate leave_view", {
122131
+ agentId,
122132
+ scope: scopeKey(scope),
122133
+ ttlExpired: runtime.spectateTtlExpired
122134
+ });
122135
+ if (runtime.spectateTtlExpired) {
122136
+ this.stopSpectate(runtime, "ttl_expired");
122137
+ }
122138
+ break;
122139
+ case "stop":
122140
+ this.stopSpectate(runtime, "stopped");
122141
+ this.pendingSpectate.delete(key);
122142
+ logger14.info("Spectate stopped", { agentId, scope: scopeKey(scope) });
122143
+ break;
122144
+ default:
122145
+ break;
122146
+ }
122147
+ }
122148
+ spectateTtlMs() {
122149
+ return Number(process.env.AHCHAT_BRIDGE_SPECTATE_TTL_MS) || 36e5;
122150
+ }
122151
+ armSpectateTimer(runtime) {
122152
+ this.clearSpectateTimer(runtime);
122153
+ if (!runtime.spectating || runtime.spectateActivatedAt <= 0) return;
122154
+ const ttlMs = this.spectateTtlMs();
122155
+ const elapsed = Date.now() - runtime.spectateActivatedAt;
122156
+ const delay = Math.max(0, ttlMs - elapsed);
122157
+ runtime.spectateRevertTimer = setTimeout(() => {
122158
+ runtime.spectateRevertTimer = null;
122159
+ runtime.spectateTtlExpired = true;
122160
+ logger14.info("Spectate TTL expired", {
122161
+ agentId: runtime.agentId,
122162
+ scope: scopeKey(runtime.scope),
122163
+ viewing: runtime.spectateViewing
122164
+ });
122165
+ if (!runtime.spectateViewing) {
122166
+ this.stopSpectate(runtime, "ttl_expired");
122167
+ }
122168
+ }, delay);
122169
+ }
122170
+ clearSpectateTimer(runtime) {
122171
+ if (runtime.spectateRevertTimer != null) {
122172
+ clearTimeout(runtime.spectateRevertTimer);
122173
+ runtime.spectateRevertTimer = null;
122174
+ }
122175
+ }
122176
+ stopSpectate(runtime, reason) {
122177
+ const wasActive = runtime.spectating;
122178
+ this.clearSpectateTimer(runtime);
122179
+ runtime.spectating = false;
122180
+ runtime.spectateViewing = false;
122181
+ runtime.spectateTtlExpired = false;
122182
+ runtime.spectateActivatedAt = 0;
122183
+ if (wasActive) {
122184
+ this.emitSpectateState(runtime, false, reason);
122185
+ logger14.info("Spectate deactivated", {
122186
+ agentId: runtime.agentId,
122187
+ scope: scopeKey(runtime.scope),
122188
+ reason
122189
+ });
122190
+ }
122191
+ }
122192
+ emitSpectateState(runtime, active, reason) {
122193
+ const scopePayload = runtime.scope.kind === "single" ? { kind: "single" } : { kind: "group", groupId: runtime.scope.groupId };
122194
+ this.emit({
122195
+ type: "spectate:state",
122196
+ payload: {
122197
+ agentId: runtime.agentId,
122198
+ scope: scopePayload,
122199
+ active,
122200
+ expiresAt: active ? runtime.spectateActivatedAt + this.spectateTtlMs() : void 0,
122201
+ reason,
122202
+ traceId: createTraceId()
122203
+ }
122204
+ });
122205
+ logger14.info("Spectate state emitted", {
122206
+ agentId: runtime.agentId,
122207
+ scope: scopeKey(runtime.scope),
122208
+ active,
122209
+ reason,
122210
+ expiresAt: active ? runtime.spectateActivatedAt + this.spectateTtlMs() : void 0
122211
+ });
122212
+ }
122213
+ teardownSpectate(runtime) {
122214
+ if (runtime.spectating) {
122215
+ this.stopSpectate(runtime, "runtime_gone");
122216
+ } else {
122217
+ this.clearSpectateTimer(runtime);
122218
+ }
122219
+ }
121666
122220
  /** Stop one scoped SDK runtime (workdir change). */
121667
122221
  async terminateScope(agentId, scope) {
121668
122222
  const key = runtimeKey(agentId, scope);
@@ -121671,6 +122225,7 @@ ${lines.join("\n")}`;
121671
122225
  logger14.info("terminateScope: no active runtime", { agentId, scope: scopeKey(scope) });
121672
122226
  this.dormantScopes.delete(key);
121673
122227
  this.dormantGroupInboxes.delete(key);
122228
+ this.pendingSpectate.delete(key);
121674
122229
  this.sessionStore.delete(agentId, scope);
121675
122230
  this.dispatchMemory.deleteScope(agentId, scope);
121676
122231
  return;
@@ -121693,7 +122248,7 @@ ${lines.join("\n")}`;
121693
122248
  this.dispatchMemory.deleteScope(agentId, scope);
121694
122249
  logger14.info("terminateScope: scoped query removed", { agentId, scope: scopeKey(scope) });
121695
122250
  }
121696
- async closeRuntime(proc, reason) {
122251
+ async closeRuntime(proc, reason, watchdogForensics) {
121697
122252
  const key = runtimeKey(proc.agentId, proc.scope);
121698
122253
  if (proc.status === "dead") return;
121699
122254
  const runtime = this.asRuntime(proc);
@@ -121770,12 +122325,13 @@ ${lines.join("\n")}`;
121770
122325
  runtime.currentTask = null;
121771
122326
  if (isWatchdog) {
121772
122327
  const preservedInbox = proc.groupInbox;
121773
- if (preservedInbox.length > 0) {
122328
+ const preservedInboxSize = preservedInbox.length;
122329
+ if (preservedInboxSize > 0) {
121774
122330
  this.dormantGroupInboxes.set(key, [...preservedInbox]);
121775
122331
  logger14.info("Preserving groupInbox for dormant agent", {
121776
122332
  agentId,
121777
122333
  scope: scopeKey(proc.scope),
121778
- preservedInboxSize: preservedInbox.length,
122334
+ preservedInboxSize,
121779
122335
  preservedEntries: preservedInbox.map((e) => ({
121780
122336
  ackId: e.ackId,
121781
122337
  sender: e.senderName,
@@ -121784,7 +122340,26 @@ ${lines.join("\n")}`;
121784
122340
  }))
121785
122341
  });
121786
122342
  }
121787
- this.dormantScopes.add(key);
122343
+ const effectiveFallbackId = watchdogForensics?.fallbackId ?? createFallbackId();
122344
+ logFallback(logger14, {
122345
+ fallbackId: effectiveFallbackId,
122346
+ type: "zombie_watchdog",
122347
+ phase: "applied",
122348
+ expected: false,
122349
+ traceId: dormantTraceId,
122350
+ context: {
122351
+ agentId,
122352
+ scope: scopeKey(proc.scope),
122353
+ droppedTaskCount: droppedAckIds.length,
122354
+ preservedInboxSize,
122355
+ sessionDeleted: false
122356
+ }
122357
+ });
122358
+ this.dormantScopes.set(key, {
122359
+ fallbackId: effectiveFallbackId,
122360
+ detectedAt: watchdogForensics?.detectedAt ?? Date.now(),
122361
+ droppedTaskCount: droppedAckIds.length
122362
+ });
121788
122363
  this.emit({
121789
122364
  type: "agent:dormant",
121790
122365
  payload: {
@@ -121799,13 +122374,15 @@ ${lines.join("\n")}`;
121799
122374
  agentId,
121800
122375
  scope: scopeKey(proc.scope),
121801
122376
  droppedTaskCount: droppedAckIds.length,
121802
- preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0
122377
+ preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0,
122378
+ fallbackId: effectiveFallbackId
121803
122379
  });
121804
122380
  }
121805
122381
  proc.status = "dead";
121806
122382
  this.agents.delete(key);
121807
122383
  this.lastUsedAt.delete(key);
121808
122384
  this.clearQuietFlushTimer(runtime);
122385
+ this.teardownSpectate(runtime);
121809
122386
  try {
121810
122387
  runtime.inputController.close();
121811
122388
  await this.awaitQueryReturn(runtime.query, 5e3, agentId);
@@ -121822,6 +122399,165 @@ ${lines.join("\n")}`;
121822
122399
  cwd: proc.cwd
121823
122400
  });
121824
122401
  }
122402
+ /**
122403
+ * Emit `agent:error` for the active reply and every queued/merged/buffered task,
122404
+ * then clear those queues. Used by both the SDK stream-crash path and the
122405
+ * reply-stall watchdog so a torn-down runtime never leaves a carrier reply
122406
+ * stuck in-flight on the server (which would keep absorbing new user messages
122407
+ * as steers of a dead turn).
122408
+ */
122409
+ failPendingTasksWithError(runtime, errorText, fallbackId) {
122410
+ const pending = [];
122411
+ if (runtime.currentTask) pending.push(runtime.currentTask);
122412
+ pending.push(...runtime.injectedTasks, ...runtime.mergedTasks, ...runtime.planModeBuffer);
122413
+ runtime.currentTask = null;
122414
+ runtime.injectedTasks = [];
122415
+ runtime.mergedTasks = [];
122416
+ runtime.planModeBuffer = [];
122417
+ if (pending.length === 0) return { pendingCount: 0 };
122418
+ const carrier = pending[0];
122419
+ const mergedTasks = pending.slice(1);
122420
+ logger14.warn("Pending tasks failure consolidated", {
122421
+ agentId: runtime.agentId,
122422
+ scope: scopeKey(runtime.scope),
122423
+ pendingCount: pending.length,
122424
+ carrierAckId: carrier.replyMessageId,
122425
+ mergedAckIds: mergedTasks.map((t) => t.replyMessageId),
122426
+ traceId: carrier.traceId,
122427
+ ...fallbackId ? { fallbackId } : {}
122428
+ });
122429
+ this.emit({
122430
+ type: "agent:error",
122431
+ payload: {
122432
+ agentId: runtime.agentId,
122433
+ conversationId: carrier.conversationId,
122434
+ ackId: carrier.replyMessageId,
122435
+ traceId: carrier.traceId,
122436
+ error: errorText
122437
+ }
122438
+ });
122439
+ for (const task of mergedTasks) {
122440
+ this.emit({
122441
+ type: "agent:merged",
122442
+ payload: {
122443
+ agentId: runtime.agentId,
122444
+ conversationId: task.conversationId,
122445
+ ackId: task.replyMessageId,
122446
+ mergedIntoAckId: carrier.replyMessageId,
122447
+ groupId: task.groupId,
122448
+ traceId: task.traceId
122449
+ }
122450
+ });
122451
+ }
122452
+ return { pendingCount: pending.length };
122453
+ }
122454
+ /**
122455
+ * Recover an in-flight reply that started but went silent past
122456
+ * `replyStallTimeoutMs` (see the reply-stall fast path in `evictIdle`). The
122457
+ * underlying SDK turn is wedged with no observable progress and no error, so:
122458
+ * 1. clear the (likely interrupted/dangling) session so the next dispatch
122459
+ * starts fresh instead of resuming the same wedged transcript;
122460
+ * 2. release the carrier reply + queued steers via `agent:error` so the
122461
+ * client stops waiting and the next user message starts a brand-new reply;
122462
+ * 3. tear the wedged runtime down.
122463
+ */
122464
+ async recoverStalledReply(proc, silentMs) {
122465
+ if (proc.status === "dead") return;
122466
+ const runtime = this.asRuntime(proc);
122467
+ const key = runtimeKey(proc.agentId, proc.scope);
122468
+ const replyStallFallbackId = createFallbackId();
122469
+ const stallTraceId = proc.currentTask?.traceId;
122470
+ logger14.warn("Reply stall watchdog: in-flight reply silent too long, recovering", {
122471
+ agentId: proc.agentId,
122472
+ scope: scopeKey(proc.scope),
122473
+ silentMs,
122474
+ replyStallTimeoutMs: this.queryConfig.replyStallTimeoutMs,
122475
+ replyMessageId: proc.currentTask?.replyMessageId,
122476
+ injectedTaskCount: runtime.injectedTasks.length,
122477
+ lastSdkEventAt: new Date(proc.lastSdkEventAt).toISOString(),
122478
+ fallbackId: replyStallFallbackId,
122479
+ // Breadcrumb: what the wedged turn was doing the instant it went silent.
122480
+ // (subagent Task call? mid tool_use? which provider?) — the difference
122481
+ // between a one-off and a systemic provider/tool stall.
122482
+ model: proc.model ?? "(unknown)",
122483
+ lastSdkEvent: proc.lastSdkEventInfo,
122484
+ currentBlockType: proc.currentBlockType,
122485
+ currentToolName: proc.currentToolName,
122486
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null
122487
+ });
122488
+ logFallback(logger14, {
122489
+ fallbackId: replyStallFallbackId,
122490
+ type: "reply_stall",
122491
+ phase: "detected",
122492
+ expected: false,
122493
+ traceId: stallTraceId,
122494
+ context: {
122495
+ agentId: proc.agentId,
122496
+ scope: scopeKey(proc.scope),
122497
+ silentMs,
122498
+ model: proc.model ?? "(unknown)",
122499
+ replyMessageId: proc.currentTask?.replyMessageId ?? null,
122500
+ currentToolName: proc.currentToolName ?? null,
122501
+ lastSdkEventType: proc.lastSdkEventInfo?.type ?? null,
122502
+ injectedTaskCount: runtime.injectedTasks.length,
122503
+ mergedTaskCount: runtime.mergedTasks.length,
122504
+ planModeBufferCount: runtime.planModeBuffer.length
122505
+ }
122506
+ });
122507
+ this.sessionStore.delete(proc.agentId, proc.scope);
122508
+ this.dispatchMemory.deleteScope(proc.agentId, proc.scope);
122509
+ const failSummary = this.failPendingTasksWithError(
122510
+ runtime,
122511
+ "\u56DE\u590D\u957F\u65F6\u95F4\u65E0\u54CD\u5E94\uFF0C\u5DF2\u91CD\u7F6E\u8BE5\u4F1A\u8BDD\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\u3002",
122512
+ replyStallFallbackId
122513
+ );
122514
+ proc.status = "dead";
122515
+ this.agents.delete(key);
122516
+ this.lastUsedAt.delete(key);
122517
+ this.clearQuietFlushTimer(runtime);
122518
+ let queryCloseOk = true;
122519
+ try {
122520
+ runtime.inputController.close();
122521
+ await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
122522
+ } catch (e) {
122523
+ queryCloseOk = false;
122524
+ logger14.error("reply_stall: close query failed", {
122525
+ agentId: proc.agentId,
122526
+ scope: scopeKey(proc.scope),
122527
+ error: e
122528
+ });
122529
+ }
122530
+ logFallback(logger14, {
122531
+ fallbackId: replyStallFallbackId,
122532
+ type: "reply_stall",
122533
+ phase: "applied",
122534
+ expected: false,
122535
+ traceId: stallTraceId,
122536
+ context: {
122537
+ agentId: proc.agentId,
122538
+ scope: scopeKey(proc.scope),
122539
+ sessionDeleted: true,
122540
+ failedTaskCount: failSummary.pendingCount,
122541
+ queryClosed: queryCloseOk
122542
+ }
122543
+ });
122544
+ logFallback(logger14, {
122545
+ fallbackId: replyStallFallbackId,
122546
+ type: "reply_stall",
122547
+ phase: "outcome",
122548
+ expected: false,
122549
+ traceId: stallTraceId,
122550
+ context: {
122551
+ agentId: proc.agentId,
122552
+ scope: scopeKey(proc.scope),
122553
+ failedTaskCount: failSummary.pendingCount
122554
+ },
122555
+ outcome: {
122556
+ result: "session_reset_awaiting_user",
122557
+ dataLossSuspected: failSummary.pendingCount > 0
122558
+ }
122559
+ });
122560
+ }
121825
122561
  async recoverFromRestart(agents) {
121826
122562
  const lockSnapshot = readCronLockSnapshot();
121827
122563
  logger14.info("Recovering Agent sessions after restart", {
@@ -121944,58 +122680,7 @@ ${lines.join("\n")}`;
121944
122680
  this.lastUsedAt.delete(key);
121945
122681
  const errorText = isResumeFail ? `\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\uFF08${errMsg}\uFF09` : `Agent query crashed: ${errMsg}`;
121946
122682
  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 = [];
122683
+ this.failPendingTasksWithError(runtime, emittedErrorText);
121999
122684
  }
122000
122685
  }
122001
122686
  getStatus(agentId, scope = { kind: "single" }) {
@@ -122009,6 +122694,18 @@ ${lines.join("\n")}`;
122009
122694
  }
122010
122695
  return [...ids];
122011
122696
  }
122697
+ /** Unified signal: is the turn legitimately waiting on a live external call that emits no
122698
+ * parent heartbeat? Used by BOTH the reply-stall fast path and the zombie watchdog so neither
122699
+ * tears down a turn that is merely slow. Returns the reason for diagnostics, or null when idle.
122700
+ * - open_tool: regular tool, MCP tool, AskUserQuestion wait, or ExitPlanMode (tool_use open).
122701
+ * - subagent: Task/Agent in flight (its inner tool_results clear activeToolUseStartedAt).
122702
+ * - compact: bridge-injected /compact running. */
122703
+ busyReason(proc) {
122704
+ if (proc.activeToolUseStartedAt != null || this.latestOpenToolUse(proc) != null) return "open_tool";
122705
+ if ((proc.activeSubagentTaskIds?.size ?? 0) > 0) return "subagent";
122706
+ if (proc.compactInProgress === true) return "compact";
122707
+ return null;
122708
+ }
122012
122709
  latestOpenToolUse(proc) {
122013
122710
  for (let i = proc.contentBlocks.length - 1; i >= 0; i -= 1) {
122014
122711
  const block = proc.contentBlocks[i];
@@ -122157,7 +122854,7 @@ ${lines.join("\n")}`;
122157
122854
  }
122158
122855
  const task = {
122159
122856
  content: notice,
122160
- replyMessageId: createMessageId(),
122857
+ replyMessageId: createScopeNoticeReplyMessageId(),
122161
122858
  conversationId,
122162
122859
  traceId: createTraceId(),
122163
122860
  groupId: proc.scope.kind === "group" ? proc.scope.groupId : void 0
@@ -123244,6 +123941,7 @@ var HttpMcpRegistry = class {
123244
123941
  buildForAgent(ctx) {
123245
123942
  const mcpServers = {};
123246
123943
  const allowedTools = [];
123944
+ const toolAbi = [];
123247
123945
  const usedNames = /* @__PURE__ */ new Set();
123248
123946
  for (const connection of this.allConnections()) {
123249
123947
  if (!this.connectionAppliesToAgent(connection, ctx)) continue;
@@ -123252,12 +123950,18 @@ var HttpMcpRegistry = class {
123252
123950
  const serverName = uniqueServerName(normalizeMcpServerName(connection.serverName), usedNames);
123253
123951
  usedNames.add(serverName);
123254
123952
  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
- }
123953
+ const visibleTools = connection.tools.filter((tool2) => tool2.enabled && tool2.permissionPolicy !== "always_deny");
123954
+ for (const tool2 of visibleTools) allowedTools.push(mcpRuntimeToolName(serverName, tool2.name));
123955
+ toolAbi.push({
123956
+ serverName,
123957
+ providerId: connection.providerId,
123958
+ transport: connection.transport,
123959
+ alwaysLoad: connection.alwaysLoad,
123960
+ isBuiltin: connection.isBuiltin,
123961
+ tools: visibleTools.map((tool2) => runtimeToolAbi(serverName, tool2)).sort((a, b) => a.runtimeToolName.localeCompare(b.runtimeToolName))
123962
+ });
123259
123963
  }
123260
- return { mcpServers, allowedTools };
123964
+ return { mcpServers, allowedTools, toolAbi };
123261
123965
  }
123262
123966
  allConnections() {
123263
123967
  return [...this.serverConnections.values(), ...this.localConnections.values()];
@@ -123443,6 +124147,18 @@ function uniqueServerName(serverName, usedNames) {
123443
124147
  while (usedNames.has(`${serverName}_${idx}`)) idx += 1;
123444
124148
  return `${serverName}_${idx}`;
123445
124149
  }
124150
+ function runtimeToolAbi(serverName, tool2) {
124151
+ return {
124152
+ name: tool2.name,
124153
+ runtimeToolName: mcpRuntimeToolName(serverName, tool2.name),
124154
+ displayName: tool2.displayName,
124155
+ description: tool2.description,
124156
+ category: tool2.category,
124157
+ riskLevel: tool2.riskLevel,
124158
+ permissionPolicy: tool2.permissionPolicy,
124159
+ ...tool2.inputSchema !== void 0 ? { inputSchema: tool2.inputSchema } : {}
124160
+ };
124161
+ }
123446
124162
  function buildHeaders(authType, authSecret, customHeaders) {
123447
124163
  const headers = {};
123448
124164
  for (const header of customHeaders) {
@@ -124099,6 +124815,7 @@ var ServerConnector = class {
124099
124815
  case "agent:terminate":
124100
124816
  case "agent:runtime_reload":
124101
124817
  case "agent:terminate_scope":
124818
+ case "spectate:set":
124102
124819
  case "agent:created":
124103
124820
  case "agent:updated":
124104
124821
  case "agent:workdir-updated":
@@ -125076,24 +125793,6 @@ function normalizeLocalPath(targetPath) {
125076
125793
  const expanded = trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\") ? import_node_path18.default.join(import_node_os10.default.homedir(), trimmed.slice(2)) : trimmed;
125077
125794
  return import_node_path18.default.normalize(import_node_path18.default.resolve(expanded));
125078
125795
  }
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
125796
  function normalizeClipboardIdentityKey(value) {
125098
125797
  return process.platform === "win32" ? value.toLowerCase() : value;
125099
125798
  }
@@ -125131,18 +125830,15 @@ function mimeTypeForFileName(fileName) {
125131
125830
  }
125132
125831
  function parseWindowsClipboardResult(stdout) {
125133
125832
  const raw = stdout.trim();
125134
- if (!raw) return { files: [], text: "" };
125833
+ if (!raw) return { files: [] };
125135
125834
  try {
125136
125835
  const parsed = JSON.parse(raw);
125137
- if (!isRecord5(parsed)) return { files: [], text: "" };
125836
+ if (!isRecord5(parsed)) return { files: [] };
125138
125837
  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
- };
125838
+ return { files };
125143
125839
  } catch (e) {
125144
125840
  logger25.debug("Windows clipboard JSON parse skipped", { error: e });
125145
- return { files: clipboardTextToPathCandidates(stdout), text: "" };
125841
+ return { files: [] };
125146
125842
  }
125147
125843
  }
125148
125844
  async function readWindowsClipboardPathCandidates() {
@@ -125155,9 +125851,7 @@ async function readWindowsClipboardPathCandidates() {
125155
125851
  " $drop = [System.Windows.Forms.Clipboard]::GetFileDropList();",
125156
125852
  " foreach ($file in $drop) { $files += [string]$file }",
125157
125853
  "} catch {}",
125158
- '$text = "";',
125159
- "try { $text = [System.Windows.Forms.Clipboard]::GetText() } catch {}",
125160
- "[pscustomobject]@{ files = $files; text = $text } | ConvertTo-Json -Compress;"
125854
+ "[pscustomobject]@{ files = $files } | ConvertTo-Json -Compress;"
125161
125855
  ].join(" ");
125162
125856
  try {
125163
125857
  const { stdout } = await execFileAsync2("powershell.exe", [
@@ -125175,7 +125869,7 @@ async function readWindowsClipboardPathCandidates() {
125175
125869
  maxBuffer: 1024 * 1024
125176
125870
  });
125177
125871
  const result = parseWindowsClipboardResult(stdout);
125178
- return [...result.files, ...clipboardTextToPathCandidates(result.text)];
125872
+ return result.files;
125179
125873
  } catch (e) {
125180
125874
  logger25.debug("Windows clipboard file read skipped", { error: e });
125181
125875
  return [];
@@ -125581,7 +126275,7 @@ async function readStreamText(filePath, start) {
125581
126275
  function parseProcessedLines(raw, cursor, fileName, fingerprint) {
125582
126276
  const lastNewline = raw.lastIndexOf("\n");
125583
126277
  if (lastNewline < 0) {
125584
- return { entries: [], nextCursor: cursor, advanced: false };
126278
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "partial_line" };
125585
126279
  }
125586
126280
  const processed = raw.slice(0, lastNewline + 1);
125587
126281
  const lines = processed.split(/\r?\n/);
@@ -125607,7 +126301,8 @@ function parseProcessedLines(raw, cursor, fileName, fingerprint) {
125607
126301
  lineNum,
125608
126302
  ...fingerprint ? { fingerprint } : {}
125609
126303
  },
125610
- advanced: true
126304
+ advanced: true,
126305
+ reason: "advanced"
125611
126306
  };
125612
126307
  }
125613
126308
  function chunkEntries(entries, size) {
@@ -125665,24 +126360,55 @@ var BridgeLogUploader = class {
125665
126360
  async flushOnce() {
125666
126361
  if (this.running || this.stopped) return;
125667
126362
  this.running = true;
126363
+ const startedAt = Date.now();
126364
+ const summary = {
126365
+ targetCount: 0,
126366
+ advancedTargetCount: 0,
126367
+ missingTargetCount: 0,
126368
+ idleTargetCount: 0,
126369
+ partialLineTargetCount: 0,
126370
+ failedTargetCount: 0,
126371
+ parsedEntryCount: 0,
126372
+ bridgeEntryCount: 0,
126373
+ uploadedChunkCount: 0,
126374
+ accepted: 0,
126375
+ skipped: 0
126376
+ };
125668
126377
  try {
125669
126378
  const targets = await this.resolveTargets();
126379
+ summary.targetCount = targets.length;
125670
126380
  for (const target of targets) {
125671
126381
  try {
125672
126382
  const cursor = await readCursor(target.cursorFile);
125673
126383
  const batch = await this.readNewEntries(target, cursor);
125674
- if (!batch.advanced) continue;
126384
+ if (!batch.advanced) {
126385
+ summary.idleTargetCount += 1;
126386
+ if (batch.reason === "missing_file") summary.missingTargetCount += 1;
126387
+ if (batch.reason === "partial_line") summary.partialLineTargetCount += 1;
126388
+ continue;
126389
+ }
126390
+ summary.advancedTargetCount += 1;
126391
+ summary.parsedEntryCount += batch.entries.length;
125675
126392
  if (batch.entries.length > 0) {
125676
- await this.uploadEntries(batch.entries);
126393
+ const result = await this.uploadEntries(batch.entries);
126394
+ summary.bridgeEntryCount += result.bridgeEntryCount;
126395
+ summary.uploadedChunkCount += result.uploadedChunkCount;
126396
+ summary.accepted += result.accepted;
126397
+ summary.skipped += result.skipped;
125677
126398
  }
125678
126399
  await writeCursor(target.cursorFile, batch.nextCursor);
125679
126400
  } catch (e) {
126401
+ summary.failedTargetCount += 1;
125680
126402
  logger28.warn("Bridge log upload target failed", { error: e, logFile: target.logFile });
125681
126403
  }
125682
126404
  }
125683
126405
  } catch (e) {
125684
126406
  logger28.warn("Bridge log upload cycle failed", { error: e });
125685
126407
  } finally {
126408
+ logger28.info("Bridge log upload cycle summary", {
126409
+ ...summary,
126410
+ durationMs: Date.now() - startedAt
126411
+ });
125686
126412
  this.running = false;
125687
126413
  }
125688
126414
  }
@@ -125705,7 +126431,7 @@ var BridgeLogUploader = class {
125705
126431
  } catch (e) {
125706
126432
  if (e instanceof Error && "code" in e && e.code === "ENOENT") {
125707
126433
  logger28.debug("Bridge log file not found for upload yet", { logFile: target.logFile });
125708
- return { entries: [], nextCursor: cursor, advanced: false };
126434
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "missing_file" };
125709
126435
  }
125710
126436
  throw e;
125711
126437
  }
@@ -125713,13 +126439,19 @@ var BridgeLogUploader = class {
125713
126439
  const samePhysicalFile = !fingerprint || !cursor.fingerprint || cursor.fingerprint === fingerprint;
125714
126440
  const normalizedCursor = stat3.size < cursor.offset || !samePhysicalFile ? { offset: 0, lineNum: 0, ...fingerprint ? { fingerprint } : {} } : { ...cursor, ...fingerprint ? { fingerprint } : {} };
125715
126441
  if (stat3.size <= normalizedCursor.offset) {
125716
- return { entries: [], nextCursor: normalizedCursor, advanced: false };
126442
+ return { entries: [], nextCursor: normalizedCursor, advanced: false, reason: "no_new_entries" };
125717
126443
  }
125718
126444
  const raw = await readStreamText(target.logFile, normalizedCursor.offset);
125719
126445
  return parseProcessedLines(raw, normalizedCursor, target.uploadedFileName, fingerprint);
125720
126446
  }
125721
126447
  async uploadEntries(entries) {
125722
126448
  const bridgeEntries = entries.filter((entry) => entry.source === "bridge");
126449
+ const result = {
126450
+ bridgeEntryCount: bridgeEntries.length,
126451
+ uploadedChunkCount: 0,
126452
+ accepted: 0,
126453
+ skipped: 0
126454
+ };
125723
126455
  for (const chunk of chunkEntries(bridgeEntries, this.options.batchSize)) {
125724
126456
  const res = await fetch(`${this.options.serverApiUrl}/api/logs/upload`, {
125725
126457
  method: "POST",
@@ -125734,14 +126466,18 @@ var BridgeLogUploader = class {
125734
126466
  })
125735
126467
  });
125736
126468
  if (!res.ok) {
125737
- const body = await res.text().catch((e) => {
126469
+ const body2 = await res.text().catch((e) => {
125738
126470
  logger28.debug("Failed to read log upload error body", { error: e });
125739
126471
  return "";
125740
126472
  });
125741
- throw new Error(`upload failed HTTP ${res.status}: ${body.slice(0, 160)}`);
126473
+ throw new Error(`upload failed HTTP ${res.status}: ${body2.slice(0, 160)}`);
125742
126474
  }
125743
- await res.json();
126475
+ const body = await res.json();
126476
+ result.uploadedChunkCount += 1;
126477
+ result.accepted += typeof body.accepted === "number" ? body.accepted : 0;
126478
+ result.skipped += typeof body.skipped === "number" ? body.skipped : 0;
125744
126479
  }
126480
+ return result;
125745
126481
  }
125746
126482
  };
125747
126483
 
@@ -127739,6 +128475,33 @@ function syncLocalRuntimeSkills(skillStore, localSkills, options = {}) {
127739
128475
  ]);
127740
128476
  }
127741
128477
 
128478
+ // src/processOutput.ts
128479
+ init_cjs_shims();
128480
+ var protectedStreams2 = /* @__PURE__ */ new WeakSet();
128481
+ var reportedErrors = /* @__PURE__ */ new WeakSet();
128482
+ function reportWriteError(error51, onError) {
128483
+ if (typeof error51 === "object" && error51 !== null) {
128484
+ if (reportedErrors.has(error51)) return;
128485
+ reportedErrors.add(error51);
128486
+ }
128487
+ onError(error51);
128488
+ }
128489
+ function safeWriteProcessOutput(stream, text, onError) {
128490
+ if (!stream) return;
128491
+ if (stream.destroyed || stream.writableEnded) return;
128492
+ if (typeof stream === "object" && typeof stream.on === "function" && !protectedStreams2.has(stream)) {
128493
+ protectedStreams2.add(stream);
128494
+ stream.on("error", (e) => reportWriteError(e, onError));
128495
+ }
128496
+ try {
128497
+ stream.write(text, (error51) => {
128498
+ if (error51) reportWriteError(error51, onError);
128499
+ });
128500
+ } catch (e) {
128501
+ reportWriteError(e, onError);
128502
+ }
128503
+ }
128504
+
127742
128505
  // src/start.ts
127743
128506
  var logger41 = createModuleLogger("bridge");
127744
128507
  var NODE_USER_UID2 = 1e3;
@@ -127825,14 +128588,16 @@ async function startBridge(config2) {
127825
128588
  const claudeRuntime = resolveClaudeRuntime();
127826
128589
  logClaudeRuntimeResolution(claudeRuntime);
127827
128590
  if (!claudeRuntime.ok || !claudeRuntime.path) {
127828
- process.stderr.write(
128591
+ safeWriteProcessOutput(
128592
+ process.stderr,
127829
128593
  `
127830
128594
  Claude runtime is unavailable.
127831
128595
 
127832
128596
  ${claudeRuntime.error ?? "Install Claude Code manually or use the bundled desktop runtime."}
127833
128597
 
127834
128598
  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
- `
128599
+ `,
128600
+ (e) => logger41.error("Bridge process stderr write failed", { error: e })
127836
128601
  );
127837
128602
  process.exit(1);
127838
128603
  }
@@ -127889,12 +128654,14 @@ Reinstall @fangyb/ahchat-bridge with npm optional dependencies, set AHCHAT_CLAUD
127889
128654
  claudeRuntimeVersion: claudeRuntime.version ?? null
127890
128655
  });
127891
128656
  const shouldPrintRawBridgeToken = process.stdout.isTTY && process.env.AHCHAT_SUPPRESS_BRIDGE_TOKEN_STDOUT !== "1";
127892
- process.stdout.write(
128657
+ safeWriteProcessOutput(
128658
+ process.stdout,
127893
128659
  `
127894
128660
  Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\u673A\u5668):
127895
128661
  ${shouldPrintRawBridgeToken ? config2.bridgeToken : "***"}
127896
128662
 
127897
- `
128663
+ `,
128664
+ (e) => logger41.error("Bridge process stdout write failed", { error: e })
127898
128665
  );
127899
128666
  wsMetrics.start(5e3);
127900
128667
  const sessionStore = new SessionStore(config2.dataDir);
@@ -128693,6 +129460,19 @@ Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\
128693
129460
  });
128694
129461
  await agentManager.terminateScope(msg.payload.agentId, msg.payload.scope);
128695
129462
  break;
129463
+ case "spectate:set":
129464
+ logger41.info("spectate:set received", {
129465
+ agentId: msg.payload.agentId,
129466
+ scope: msg.payload.scope,
129467
+ action: msg.payload.action,
129468
+ traceId: msg.payload.traceId
129469
+ });
129470
+ await agentManager.setSpectate(
129471
+ msg.payload.agentId,
129472
+ msg.payload.scope,
129473
+ msg.payload.action
129474
+ );
129475
+ break;
128696
129476
  case "agent:created":
128697
129477
  agentRegistry.upsert(msg.payload.agent);
128698
129478
  ensureLocalWorkdirPath(msg.payload.agent.workingDirectory, "agent:created", {
@@ -128910,8 +129690,12 @@ function writeAlreadyRunningMessage(error51) {
128910
129690
  ` ${buildStopCommand(error51.pid)}`,
128911
129691
  ""
128912
129692
  ];
128913
- process.stdout.write(`${lines.join("\n")}
128914
- `);
129693
+ safeWriteProcessOutput(
129694
+ process.stdout,
129695
+ `${lines.join("\n")}
129696
+ `,
129697
+ (e) => logger42.error("Bridge already-running message write failed", { error: e })
129698
+ );
128915
129699
  }
128916
129700
  function handleBridgeStartError(e, message) {
128917
129701
  if (isBridgeAlreadyRunningError(e)) {