@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/index.js CHANGED
@@ -3635,6 +3635,8 @@ var DEFAULT_QUERY_CONFIG = {
3635
3635
  maxActive: 5040,
3636
3636
  idleTimeoutMs: 6e5,
3637
3637
  workingSilenceTimeoutMs: 12e5,
3638
+ replyStallTimeoutMs: 3e5,
3639
+ busySilenceTimeoutMs: 18e5,
3638
3640
  evictionIntervalMs: 6e4,
3639
3641
  statusReportIntervalMs: 6e4,
3640
3642
  allowBuiltinWebSearch: false,
@@ -3692,6 +3694,14 @@ function mergeQueryConfig(file2) {
3692
3694
  "AHCHAT_BRIDGE_WORKING_SILENCE_TIMEOUT_MS",
3693
3695
  q?.workingSilenceTimeoutMs ?? DEFAULT_QUERY_CONFIG.workingSilenceTimeoutMs
3694
3696
  ),
3697
+ replyStallTimeoutMs: readEnvInt(
3698
+ "AHCHAT_BRIDGE_REPLY_STALL_TIMEOUT_MS",
3699
+ q?.replyStallTimeoutMs ?? DEFAULT_QUERY_CONFIG.replyStallTimeoutMs
3700
+ ),
3701
+ busySilenceTimeoutMs: readEnvInt(
3702
+ "AHCHAT_BRIDGE_BUSY_SILENCE_TIMEOUT_MS",
3703
+ q?.busySilenceTimeoutMs ?? DEFAULT_QUERY_CONFIG.busySilenceTimeoutMs ?? 18e5
3704
+ ),
3695
3705
  evictionIntervalMs: readEnvInt(
3696
3706
  "AHCHAT_BRIDGE_EVICTION_INTERVAL_MS",
3697
3707
  q?.evictionIntervalMs ?? DEFAULT_QUERY_CONFIG.evictionIntervalMs
@@ -3772,7 +3782,11 @@ function loadBridgeConfig(opts) {
3772
3782
  ) || null,
3773
3783
  logUploadIntervalMs: readEnvInt(
3774
3784
  "AHCHAT_LOG_UPLOAD_INTERVAL_MS",
3775
- fileConfig.logUploadIntervalMs ?? 24 * 60 * 60 * 1e3
3785
+ // Flush every 60s instead of once a day. Daily flushing let logs pile up for hours,
3786
+ // then dumped tens of thousands of entries in one cycle on the next process start,
3787
+ // blowing past the server's per-minute upload quota (3000 entries / 3MB) and getting
3788
+ // 429'd. Small frequent batches stay well under that ceiling.
3789
+ fileConfig.logUploadIntervalMs ?? 6e4
3776
3790
  ),
3777
3791
  queryConfig: mergeQueryConfig(fileConfig)
3778
3792
  };
@@ -4485,8 +4499,32 @@ ${entry.error.stack}` : ""}`
4485
4499
  return `${ts} ${level} ${scope} ${entry.msg}${data}${trace}${errPart}`;
4486
4500
  };
4487
4501
 
4502
+ // ../logger/src/fallback.ts
4503
+ function logFallback(logger43, event) {
4504
+ const payload = {
4505
+ ...event.traceId ? { traceId: event.traceId } : {},
4506
+ fallback: {
4507
+ fallbackId: event.fallbackId,
4508
+ type: event.type,
4509
+ phase: event.phase,
4510
+ expected: event.expected,
4511
+ ...event.context ? { context: event.context } : {},
4512
+ ...event.outcome ? { outcome: event.outcome } : {}
4513
+ }
4514
+ };
4515
+ const msg = `[FALLBACK] ${event.type}:${event.phase}`;
4516
+ const useDebug = event.expected && event.phase !== "outcome";
4517
+ if (useDebug) {
4518
+ logger43.debug(msg, payload);
4519
+ } else {
4520
+ logger43.warn(msg, payload);
4521
+ }
4522
+ }
4523
+
4488
4524
  // ../logger/src/transports/console.ts
4489
4525
  var protectedStreams = /* @__PURE__ */ new WeakSet();
4526
+ function ignoreWriteError(_error) {
4527
+ }
4490
4528
  function defaultStream(kind) {
4491
4529
  const maybeGlobal = globalThis;
4492
4530
  return maybeGlobal.process?.[kind];
@@ -4499,12 +4537,13 @@ function safeWriteLine(stream, line, fallback) {
4499
4537
  if (stream.destroyed || stream.writableEnded) return;
4500
4538
  if (typeof stream === "object" && typeof stream.on === "function" && !protectedStreams.has(stream)) {
4501
4539
  protectedStreams.add(stream);
4502
- stream.on("error", () => void 0);
4540
+ stream.on("error", ignoreWriteError);
4503
4541
  }
4504
4542
  try {
4505
4543
  stream.write(`${line}
4506
- `, () => void 0);
4507
- } catch {
4544
+ `, ignoreWriteError);
4545
+ } catch (e) {
4546
+ ignoreWriteError(e);
4508
4547
  }
4509
4548
  }
4510
4549
  function consoleTransport(opts) {
@@ -5137,8 +5176,31 @@ function parseSize(maxSize) {
5137
5176
  return trimmed;
5138
5177
  }
5139
5178
  var streamCache = /* @__PURE__ */ new Map();
5179
+ var droppedEntryCount = 0;
5180
+ var lastReportedDroppedTotal = 0;
5181
+ function buildLogDroppedSentinel(fmt, source, droppedTotal) {
5182
+ const entry = {
5183
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5184
+ level: "WARN",
5185
+ source,
5186
+ module: "logger.file",
5187
+ msg: "log_dropped",
5188
+ data: { droppedTotal }
5189
+ };
5190
+ return fmt(entry);
5191
+ }
5192
+ function writeWithDroppedSentinel(stream, fmt, line, source) {
5193
+ stream.write(`${line}
5194
+ `);
5195
+ if (droppedEntryCount > lastReportedDroppedTotal) {
5196
+ lastReportedDroppedTotal = droppedEntryCount;
5197
+ stream.write(`${buildLogDroppedSentinel(fmt, source, droppedEntryCount)}
5198
+ `);
5199
+ }
5200
+ }
5140
5201
  function fileTransport(opts) {
5141
5202
  const fmt = opts.formatter ?? jsonFormatter;
5203
+ const logSource = opts.source ?? "server";
5142
5204
  const resolved = path2.resolve(opts.path);
5143
5205
  let cached2 = streamCache.get(resolved);
5144
5206
  if (!cached2) {
@@ -5153,11 +5215,14 @@ function fileTransport(opts) {
5153
5215
  streamCache.set(resolved, cached2);
5154
5216
  }
5155
5217
  return (entry) => {
5156
- if (cached2.closed || cached2.stream.destroyed || cached2.stream.writableEnded) return;
5218
+ if (cached2.closed || cached2.stream.destroyed || cached2.stream.writableEnded) {
5219
+ droppedEntryCount += 1;
5220
+ return;
5221
+ }
5157
5222
  try {
5158
- cached2.stream.write(`${fmt(entry)}
5159
- `);
5223
+ writeWithDroppedSentinel(cached2.stream, fmt, fmt(entry), logSource);
5160
5224
  } catch {
5225
+ droppedEntryCount += 1;
5161
5226
  }
5162
5227
  };
5163
5228
  }
@@ -5907,6 +5972,9 @@ function createMessageId() {
5907
5972
  function createTraceId() {
5908
5973
  return `tr_${Date.now().toString(36)}_${nanoid(6)}`;
5909
5974
  }
5975
+ function createFallbackId() {
5976
+ return `flb_${Date.now().toString(36)}_${nanoid(6)}`;
5977
+ }
5910
5978
  function createRequestId() {
5911
5979
  return `req_${Date.now().toString(36)}_${nanoid(6)}`;
5912
5980
  }
@@ -5916,6 +5984,12 @@ function createCronReplyMessageId() {
5916
5984
  function createInboxFlushReplyMessageId() {
5917
5985
  return `msg_inbox_${Date.now().toString(36)}_${nanoid(6)}`;
5918
5986
  }
5987
+ function createNeuralSendReplyMessageId() {
5988
+ return `msg_nsend_${Date.now().toString(36)}_${nanoid(6)}`;
5989
+ }
5990
+ function createScopeNoticeReplyMessageId() {
5991
+ return `msg_scopenotice_${Date.now().toString(36)}_${nanoid(6)}`;
5992
+ }
5919
5993
  function createCronTraceId() {
5920
5994
  return `tr_cron_${Date.now().toString(36)}_${nanoid(6)}`;
5921
5995
  }
@@ -5974,6 +6048,10 @@ function assertStringPayloadOneOf(type, payload, field, allowed) {
5974
6048
  throw invalidWsMessage(type, field);
5975
6049
  }
5976
6050
  }
6051
+ function assertOptionalStringPayloadOneOf(type, payload, field, allowed) {
6052
+ if (payload[field] === void 0) return;
6053
+ assertStringPayloadOneOf(type, payload, field, allowed);
6054
+ }
5977
6055
  function assertArrayPayloadField(type, payload, field) {
5978
6056
  if (!Array.isArray(payload[field])) {
5979
6057
  throw invalidWsMessage(type, field);
@@ -6054,6 +6132,7 @@ function validateWSMessageShape(msg) {
6054
6132
  case "agent:error": {
6055
6133
  assertPayloadRecord(type, payload);
6056
6134
  validateRequiredStrings(type, payload, ["ackId", "agentId", "conversationId", "error", "traceId"]);
6135
+ assertOptionalStringPayloadOneOf(type, payload, "reason", ["user_quota_exceeded", "company_quota_exceeded"]);
6057
6136
  return;
6058
6137
  }
6059
6138
  case "directory:register": {
@@ -7641,6 +7720,60 @@ var readpageScrapeTool = {
7641
7720
  enabled: true,
7642
7721
  permissionPolicy: "always_allow"
7643
7722
  };
7723
+ var seedreamGenerateImageTool = {
7724
+ name: "generate_image",
7725
+ displayName: "Seedream \u751F\u56FE",
7726
+ 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",
7727
+ category: "media",
7728
+ riskLevel: "medium",
7729
+ enabled: true,
7730
+ permissionPolicy: "always_ask"
7731
+ };
7732
+ var seedreamEditImageTool = {
7733
+ name: "edit_image",
7734
+ displayName: "Seedream \u6539\u56FE",
7735
+ 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",
7736
+ category: "media",
7737
+ riskLevel: "medium",
7738
+ enabled: true,
7739
+ permissionPolicy: "always_ask"
7740
+ };
7741
+ var seedreamGenerateImageGroupTool = {
7742
+ name: "generate_image_group",
7743
+ displayName: "Seedream \u5355\u56FE",
7744
+ 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",
7745
+ category: "media",
7746
+ riskLevel: "medium",
7747
+ enabled: true,
7748
+ permissionPolicy: "always_ask"
7749
+ };
7750
+ var seedanceUsageGuideTool = {
7751
+ name: "seedance_usage_guide",
7752
+ displayName: "Seedance \u4F7F\u7528\u8BF4\u660E",
7753
+ 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",
7754
+ category: "media",
7755
+ riskLevel: "low",
7756
+ enabled: true,
7757
+ permissionPolicy: "always_allow"
7758
+ };
7759
+ var seedanceCreateTaskTool = {
7760
+ name: "seedance_create_task",
7761
+ displayName: "Seedance \u751F\u89C6\u9891",
7762
+ 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",
7763
+ category: "media",
7764
+ riskLevel: "high",
7765
+ enabled: true,
7766
+ permissionPolicy: "always_ask"
7767
+ };
7768
+ var seedanceCheckTaskTool = {
7769
+ name: "seedance_check_task",
7770
+ displayName: "Seedance \u67E5\u7ED3\u679C",
7771
+ 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",
7772
+ category: "media",
7773
+ riskLevel: "low",
7774
+ enabled: true,
7775
+ permissionPolicy: "always_allow"
7776
+ };
7644
7777
  var context7ResolveLibraryTool = {
7645
7778
  name: "resolve-library-id",
7646
7779
  displayName: "\u89E3\u6790\u6587\u6863\u5E93",
@@ -7947,8 +8080,56 @@ var ALIYUN_IQS_MCP_PROVIDERS = [
7947
8080
  tools: [readpageBasicTool, readpageScrapeTool]
7948
8081
  }
7949
8082
  ];
8083
+ var VOLCENGINE_SEEDREAM_MCP_PROVIDER = {
8084
+ providerId: "volcengine_seedream",
8085
+ name: "Volcengine Seedream",
8086
+ 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",
8087
+ serverName: "seedream",
8088
+ transport: "stdio",
8089
+ url: null,
8090
+ command: "ahchat-builtin",
8091
+ args: ["seedream-mcp"],
8092
+ env: {
8093
+ ARK_API_KEY: "",
8094
+ ARK_BASE_URL: "https://ark.cn-beijing.volces.com/api/v3"
8095
+ },
8096
+ authType: "x_api_key",
8097
+ customHeaders: [],
8098
+ enabled: true,
8099
+ alwaysLoad: true,
8100
+ tools: [
8101
+ seedreamGenerateImageTool,
8102
+ seedreamEditImageTool,
8103
+ seedreamGenerateImageGroupTool
8104
+ ]
8105
+ };
8106
+ var VOLCENGINE_SEEDANCE_MCP_PROVIDER = {
8107
+ providerId: "volcengine_seedance",
8108
+ name: "Volcengine Seedance",
8109
+ 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",
8110
+ serverName: "seedance",
8111
+ transport: "stdio",
8112
+ url: null,
8113
+ command: "ahchat-builtin",
8114
+ args: ["seedance-mcp"],
8115
+ env: {
8116
+ ARK_API_KEY: "",
8117
+ ARK_BASE_URL: "https://ark.cn-beijing.volces.com/api/v3"
8118
+ },
8119
+ authType: "x_api_key",
8120
+ customHeaders: [],
8121
+ enabled: true,
8122
+ alwaysLoad: true,
8123
+ tools: [
8124
+ seedanceUsageGuideTool,
8125
+ seedanceCreateTaskTool,
8126
+ seedanceCheckTaskTool
8127
+ ]
8128
+ };
7950
8129
  var OFFICIAL_MCP_PROVIDERS = [
7951
- ...ALIYUN_IQS_MCP_PROVIDERS
8130
+ ...ALIYUN_IQS_MCP_PROVIDERS,
8131
+ VOLCENGINE_SEEDREAM_MCP_PROVIDER,
8132
+ VOLCENGINE_SEEDANCE_MCP_PROVIDER
7952
8133
  ];
7953
8134
  var MCP_STORE_PROVIDERS = [
7954
8135
  {
@@ -8790,8 +8971,7 @@ var AskQuestionRegistry = class {
8790
8971
  questionId,
8791
8972
  agentId: entry.agentId,
8792
8973
  waitedMs: Date.now() - entry.askedAt,
8793
- answerLen: answerText2.length,
8794
- answerSample: answerText2.slice(0, 200)
8974
+ answerLen: answerText2.length
8795
8975
  });
8796
8976
  entry.resolve(answerText2);
8797
8977
  return true;
@@ -8932,7 +9112,7 @@ function makeAskUserQuestionGuard(deps) {
8932
9112
  bundleIndex,
8933
9113
  bundleSize,
8934
9114
  replyMessageId: task.replyMessageId,
8935
- question: q.question.slice(0, 200),
9115
+ questionLen: q.question.length,
8936
9116
  optionCount: options.length,
8937
9117
  multiSelect,
8938
9118
  traceId: task.traceId
@@ -9031,7 +9211,7 @@ function makeAskUserQuestionGuard(deps) {
9031
9211
  bundleId,
9032
9212
  bundleSize,
9033
9213
  replyMessageId: task.replyMessageId,
9034
- combinedSample: combined.slice(0, 200),
9214
+ combinedLen: combined.length,
9035
9215
  traceId: task.traceId
9036
9216
  });
9037
9217
  return { behavior: "deny", message: combined };
@@ -24054,6 +24234,17 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
24054
24234
  ".yaml",
24055
24235
  ".yml"
24056
24236
  ]);
24237
+ var OFFICE_DOCUMENT_EXTENSIONS = /* @__PURE__ */ new Set([
24238
+ ".docx",
24239
+ ".xlsx",
24240
+ ".pptx",
24241
+ ".pdf",
24242
+ ".odt",
24243
+ ".ods",
24244
+ ".odp",
24245
+ ".rtf"
24246
+ ]);
24247
+ var PLAIN_TEXT_BINARY_SNIFF_BYTES = 8192;
24057
24248
  var DEFAULT_MAX_CHARS = 5e5;
24058
24249
  var DEFAULT_TIMEOUT_MS = 45e3;
24059
24250
  function isReadableDocumentPath(filePath) {
@@ -24073,9 +24264,6 @@ function resolveDocumentPath(inputPath, cwd) {
24073
24264
  async function readDocumentAsMarkdown(inputPath, opts = {}) {
24074
24265
  const resolvedPath = opts.cwd ? resolveDocumentPath(inputPath, opts.cwd) : path9.resolve(resolveUserPath(inputPath));
24075
24266
  const ext = path9.extname(resolvedPath).toLowerCase();
24076
- if (!isReadableDocumentPath(resolvedPath)) {
24077
- throw new Error(`unsupported document type: ${ext || "(no extension)"}`);
24078
- }
24079
24267
  const stat3 = await fs4.stat(resolvedPath);
24080
24268
  if (!stat3.isFile()) throw new Error("path is not a file");
24081
24269
  const warnings = [];
@@ -24084,8 +24272,10 @@ async function readDocumentAsMarkdown(inputPath, opts = {}) {
24084
24272
  markdown = await fs4.readFile(resolvedPath, "utf-8");
24085
24273
  } else if (ext === ".xls") {
24086
24274
  markdown = await convertLegacyExcelDocument(resolvedPath);
24087
- } else {
24275
+ } else if (OFFICE_DOCUMENT_EXTENSIONS.has(ext)) {
24088
24276
  markdown = await convertOfficeDocument(resolvedPath, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
24277
+ } else {
24278
+ markdown = await readPlainTextDocument(resolvedPath, ext);
24089
24279
  }
24090
24280
  markdown = normalizeDocumentText(markdown);
24091
24281
  const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
@@ -24224,6 +24414,14 @@ async function convertDocxWithOfficeCli(filePath, timeoutMs) {
24224
24414
  });
24225
24415
  return text;
24226
24416
  }
24417
+ async function readPlainTextDocument(filePath, ext) {
24418
+ const buffer = await fs4.readFile(filePath);
24419
+ const sample = buffer.subarray(0, Math.min(buffer.length, PLAIN_TEXT_BINARY_SNIFF_BYTES));
24420
+ if (sample.includes(0)) {
24421
+ throw new Error(`unsupported document type: ${ext || "(no extension)"}`);
24422
+ }
24423
+ return buffer.toString("utf-8");
24424
+ }
24227
24425
  async function convertLegacyExcelDocument(filePath) {
24228
24426
  const XLSX = await import("./xlsx-E4ZR5JHK.js");
24229
24427
  const workbook = XLSX.readFile(filePath, { cellDates: true });
@@ -24693,8 +24891,7 @@ async function createNeuralMcpServer(deps) {
24693
24891
  agentId: deps.agentId,
24694
24892
  fromScope: currentScopeKey,
24695
24893
  rawTargetScope: args.target_scope,
24696
- messageLen: args.message.length,
24697
- messageSample: args.message.slice(0, 120)
24894
+ messageLen: args.message.length
24698
24895
  });
24699
24896
  const trimmed = args.message.trim();
24700
24897
  if (!trimmed) {
@@ -24788,7 +24985,7 @@ async function createNeuralMcpServer(deps) {
24788
24985
  toScope: resolvedKey,
24789
24986
  repeatsInWindow: sendHistory.length,
24790
24987
  windowMs: NEURAL_DEDUP_WINDOW_MS,
24791
- messageSample: trimmed.slice(0, 120)
24988
+ messageLen: trimmed.length
24792
24989
  });
24793
24990
  return {
24794
24991
  content: [{
@@ -24815,8 +25012,7 @@ async function createNeuralMcpServer(deps) {
24815
25012
  agentId: deps.agentId,
24816
25013
  fromScope: currentScopeKey,
24817
25014
  toScope: resolvedKey,
24818
- messageLen: trimmed.length,
24819
- messageSample: trimmed.slice(0, 120)
25015
+ messageLen: trimmed.length
24820
25016
  });
24821
25017
  return {
24822
25018
  content: [{ type: "text", text: `[neural_send] \u5DF2\u9001\u8FBE\u5230\u300C${toLabel}\u300D(scope: ${resolvedKey})\u3002` }]
@@ -25173,6 +25369,7 @@ action="append" \u8FFD\u52A0\u65B0\u5185\u5BB9\uFF08\u6700\u5E38\u7528\uFF0Ccont
25173
25369
  `Read a document from the current working directory and return extracted Markdown text.
25174
25370
  Use this instead of the built-in Read tool for binary documents such as .docx, .xls, .xlsx, .pptx, .pdf, .odt, .ods, .odp, or .rtf.
25175
25371
  Also supports text-like files such as .csv, .md, .txt, .json, .xml, .yaml, and .html.
25372
+ 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.
25176
25373
  Pass either a relative path from the current working directory or an absolute path inside it.`,
25177
25374
  {
25178
25375
  path: external_exports.string().min(1).describe("Document path, relative to the current working directory or absolute inside it."),
@@ -27890,9 +28087,12 @@ function isAuthFailureText(text, sdkError) {
27890
28087
  function isProviderApiErrorText(text) {
27891
28088
  return /^API Error:\s*\d+/i.test(text.trim());
27892
28089
  }
27893
- function isNoReplyText(text) {
28090
+ function isSdkSyntheticNoResponseText(normalizedText) {
28091
+ return normalizedText.replace(/[.!。]+$/g, "") === "no response requested";
28092
+ }
28093
+ function isNoReplyText(text, opts) {
27894
28094
  const normalized = text.trim().replace(/^`+|`+$/g, "").trim().toLowerCase();
27895
- return normalized === NO_REPLY_TOKEN || normalized === "<no-reply>" || normalized === "no-reply" || normalized === "no_reply" || normalized === "no reply";
28095
+ return normalized === NO_REPLY_TOKEN || normalized === "<no-reply>" || normalized === "no-reply" || normalized === "no_reply" || normalized === "no reply" || opts?.allowSdkSyntheticNoResponse === true && isSdkSyntheticNoResponseText(normalized);
27896
28096
  }
27897
28097
  function decodeJsonStringFragment(raw) {
27898
28098
  let out = "";
@@ -28230,6 +28430,9 @@ function emitUsageReported(proc, emit, base, usage, messageId) {
28230
28430
  function isGroupTask(proc) {
28231
28431
  return proc.currentTask?.groupId != null;
28232
28432
  }
28433
+ function shouldStreamInternals(proc) {
28434
+ return !isGroupTask(proc) || proc.spectating === true;
28435
+ }
28233
28436
  function extractTodosFromInput(input) {
28234
28437
  if (!input || typeof input !== "object") return null;
28235
28438
  const raw = input.todos;
@@ -28361,7 +28564,6 @@ function emitGroupSegment(proc, emit, base, content, contentBlocks, isSilent = f
28361
28564
  contentLen: content.length,
28362
28565
  blockCount: contentBlocks.length,
28363
28566
  blockTypes: contentBlocks.map((b) => b.type),
28364
- contentSample: content.slice(0, 200),
28365
28567
  traceId: base.traceId,
28366
28568
  isAuditOnly: content.length === 0,
28367
28569
  isSilent
@@ -28380,7 +28582,7 @@ function emitGroupSegment(proc, emit, base, content, contentBlocks, isSilent = f
28380
28582
  }
28381
28583
  function flushTextSegmentOnBlockStop(proc, emit, base) {
28382
28584
  const trimmed = proc.segmentBuffer.trim();
28383
- const isSilent = isNoReplyText(trimmed);
28585
+ const isSilent = isNoReplyText(trimmed, { allowSdkSyntheticNoResponse: true });
28384
28586
  const isEmpty = trimmed.length === 0;
28385
28587
  if (!isEmpty) {
28386
28588
  const blocksForSegment = [...proc.contentBlocks, { type: "text", content: proc.segmentBuffer }];
@@ -28417,9 +28619,24 @@ function flushTextSegmentOnBlockStop(proc, emit, base) {
28417
28619
  }
28418
28620
  proc.segmentBuffer = "";
28419
28621
  }
28622
+ function describeSdkEvent(message) {
28623
+ const rec = message;
28624
+ const str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
28625
+ return {
28626
+ type: str(rec.type) ?? "unknown",
28627
+ subtype: str(rec.subtype),
28628
+ toolName: str(rec.last_tool_name),
28629
+ subagentType: str(rec.subagent_type),
28630
+ toolUseId: str(rec.tool_use_id),
28631
+ taskId: str(rec.task_id),
28632
+ at: Date.now()
28633
+ };
28634
+ }
28420
28635
  function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProviderApiError) {
28421
28636
  const emit = rawEmit;
28422
28637
  proc.lastSdkEventAt = Date.now();
28638
+ proc.lastSdkEventInfo = describeSdkEvent(message);
28639
+ proc.stallWarned = false;
28423
28640
  switch (message.type) {
28424
28641
  case "system": {
28425
28642
  const sysMsg = message;
@@ -28458,11 +28675,29 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28458
28675
  sessionId: proc.ccSessionId
28459
28676
  });
28460
28677
  } else {
28678
+ const sysRec = sysMsg;
28679
+ const pick2 = (k) => typeof sysRec[k] === "string" || typeof sysRec[k] === "number" ? sysRec[k] : void 0;
28680
+ const descriptionLen = typeof sysRec.description === "string" ? sysRec.description.length : void 0;
28681
+ const subagentTaskId = typeof sysRec.task_id === "string" ? sysRec.task_id : void 0;
28682
+ if (subagentTaskId) {
28683
+ if (sysMsg.subtype === "task_started") {
28684
+ (proc.activeSubagentTaskIds ??= /* @__PURE__ */ new Set()).add(subagentTaskId);
28685
+ } else if (sysMsg.subtype === "task_notification") {
28686
+ proc.activeSubagentTaskIds?.delete(subagentTaskId);
28687
+ }
28688
+ }
28461
28689
  logger10.info("SDK system subtype unhandled", {
28462
28690
  agentId: proc.agentId,
28463
28691
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
28464
28692
  subtype: sysMsg.subtype ?? "(none)",
28465
- keys: Object.keys(sysMsg).slice(0, 12)
28693
+ taskId: pick2("task_id"),
28694
+ toolUseId: pick2("tool_use_id"),
28695
+ subagentType: pick2("subagent_type"),
28696
+ taskType: pick2("task_type"),
28697
+ lastToolName: pick2("last_tool_name"),
28698
+ hasDescription: descriptionLen != null,
28699
+ descriptionLen,
28700
+ keys: Object.keys(sysMsg).slice(0, 16)
28466
28701
  });
28467
28702
  }
28468
28703
  break;
@@ -28488,13 +28723,14 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28488
28723
  } else if (block.type === "tool_use") {
28489
28724
  proc.currentBlockType = "tool_use";
28490
28725
  proc.currentToolName = block.name ?? "unknown";
28726
+ proc.activeToolUseStartedAt = Date.now();
28491
28727
  proc.accumulatedToolInput = "";
28492
28728
  const toolName = block.name ?? "unknown";
28493
28729
  proc.suppressCurrentToolUse = proc.officialMediaGenerationSatisfied === true && isOfficialMediaGenerationToolName(toolName);
28494
28730
  const isMcpTool = parseMcpRuntimeToolName(toolName) != null;
28495
28731
  proc.currentMcpInvocationId = isMcpTool ? createMcpToolInvocationId() : null;
28496
28732
  proc.currentMcpInvocationStartedAt = isMcpTool ? (/* @__PURE__ */ new Date()).toISOString() : null;
28497
- if (!isGroupTask(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
28733
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
28498
28734
  emit({
28499
28735
  type: "agent:tool_use",
28500
28736
  payload: {
@@ -28519,7 +28755,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28519
28755
  if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
28520
28756
  if (proc.suppressCurrentThinking) break;
28521
28757
  proc.accumulatedThinking += delta.thinking;
28522
- if (!isGroupTask(proc)) {
28758
+ if (shouldStreamInternals(proc)) {
28523
28759
  emit({
28524
28760
  type: "agent:thinking_chunk",
28525
28761
  payload: { ...wireBase(base), chunk: delta.thinking }
@@ -28530,7 +28766,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28530
28766
  if (typeof partial2 === "string") {
28531
28767
  proc.accumulatedToolInput += partial2;
28532
28768
  const liveInput = extractLiveToolInput(proc.currentToolName, proc.accumulatedToolInput);
28533
- if (!isGroupTask(proc) && liveInput && proc.currentToolName != null) {
28769
+ if (shouldStreamInternals(proc) && liveInput && proc.currentToolName != null) {
28534
28770
  emit({
28535
28771
  type: "agent:tool_input_update",
28536
28772
  payload: {
@@ -28564,7 +28800,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28564
28800
  }
28565
28801
  case "content_block_stop": {
28566
28802
  if (proc.currentBlockType === "thinking") {
28567
- if (!isGroupTask(proc) && !proc.suppressCurrentThinking) {
28803
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentThinking) {
28568
28804
  emit({
28569
28805
  type: "agent:thinking_done",
28570
28806
  payload: wireBase(getTaskBase(proc))
@@ -28589,8 +28825,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28589
28825
  error: error51,
28590
28826
  agentId: proc.agentId,
28591
28827
  toolName: proc.currentToolName,
28592
- inputLen: proc.accumulatedToolInput.length,
28593
- sample: proc.accumulatedToolInput.slice(0, 200)
28828
+ inputLen: proc.accumulatedToolInput.length
28594
28829
  });
28595
28830
  }
28596
28831
  }
@@ -28599,7 +28834,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28599
28834
  if (lastToolUse && lastToolUse.type === "tool_use") {
28600
28835
  lastToolUse.input = parsedInput;
28601
28836
  }
28602
- if (!isGroupTask(proc) && proc.currentToolName != null && LIVE_INPUT_PREVIEW_TOOLS.has(proc.currentToolName) && Object.keys(parsedInput).length > 0) {
28837
+ if (shouldStreamInternals(proc) && proc.currentToolName != null && LIVE_INPUT_PREVIEW_TOOLS.has(proc.currentToolName) && Object.keys(parsedInput).length > 0) {
28603
28838
  emit({
28604
28839
  type: "agent:tool_input_update",
28605
28840
  payload: {
@@ -28723,7 +28958,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28723
28958
  blockTypes,
28724
28959
  hasToolResult,
28725
28960
  hasPlainText,
28726
- contentSample: typeof content === "string" ? content.slice(0, 200) : JSON.stringify(content).slice(0, 200)
28961
+ contentLen: typeof content === "string" ? content.length : JSON.stringify(content).length
28727
28962
  });
28728
28963
  break;
28729
28964
  }
@@ -28732,7 +28967,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28732
28967
  agentId: proc.agentId,
28733
28968
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
28734
28969
  blockTypes,
28735
- contentSample: JSON.stringify(content).slice(0, 300),
28970
+ contentLen: JSON.stringify(content).length,
28736
28971
  replyMessageId: base.replyMessageId
28737
28972
  });
28738
28973
  }
@@ -28753,6 +28988,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28753
28988
  });
28754
28989
  proc.currentMcpInvocationId = null;
28755
28990
  proc.currentMcpInvocationStartedAt = null;
28991
+ proc.activeToolUseStartedAt = void 0;
28756
28992
  proc.currentToolName = null;
28757
28993
  continue;
28758
28994
  }
@@ -28760,6 +28996,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28760
28996
  proc.officialMediaGenerationSatisfied = true;
28761
28997
  }
28762
28998
  if (isAskUserQuestionToolName(toolName)) {
28999
+ proc.activeToolUseStartedAt = void 0;
28763
29000
  proc.currentToolName = null;
28764
29001
  continue;
28765
29002
  }
@@ -28787,7 +29024,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28787
29024
  proc.currentMcpInvocationId = null;
28788
29025
  proc.currentMcpInvocationStartedAt = null;
28789
29026
  }
28790
- if (!isGroupTask(proc)) {
29027
+ if (shouldStreamInternals(proc)) {
28791
29028
  emit({
28792
29029
  type: "agent:tool_result",
28793
29030
  payload: {
@@ -28811,6 +29048,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28811
29048
  }
28812
29049
  }
28813
29050
  }
29051
+ proc.activeToolUseStartedAt = void 0;
28814
29052
  }
28815
29053
  }
28816
29054
  }
@@ -28867,7 +29105,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28867
29105
  const groupMode = groupId != null;
28868
29106
  const usage = extractUsage(successMsg);
28869
29107
  const watermarkUsage = proc.peakContextUsage ?? usage;
28870
- if (isNoReplyText(trimmed)) {
29108
+ if (isNoReplyText(trimmed, { allowSdkSyntheticNoResponse: groupMode })) {
28871
29109
  checkInputTokenWatermark(proc, watermarkUsage, base.traceId);
28872
29110
  emitUsageReported(proc, emit, base, usage);
28873
29111
  if (groupMode && proc.contentBlocks.length > 0) {
@@ -28889,7 +29127,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28889
29127
  groupId,
28890
29128
  compactScheduled: proc.compactRequested === true,
28891
29129
  fullTextLen: proc.accumulatedText.length,
28892
- fullTextSample: proc.accumulatedText.slice(0, 200),
28893
29130
  accumulatedBlockCount: proc.contentBlocks.length,
28894
29131
  accumulatedBlockTypes: proc.contentBlocks.map((b) => b.type),
28895
29132
  silentSegmentEmitted: groupMode
@@ -28933,7 +29170,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28933
29170
  segmentCount: proc.segmentCount,
28934
29171
  compactScheduled: proc.compactRequested === true,
28935
29172
  fullTextLen: proc.accumulatedText.length,
28936
- fullTextSample: proc.accumulatedText.slice(0, 200),
28937
29173
  traceId: base.traceId
28938
29174
  });
28939
29175
  emit({
@@ -28984,7 +29220,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28984
29220
  ackId: base.replyMessageId,
28985
29221
  messageId: carrierMessageId,
28986
29222
  textLen: proc.accumulatedText.length,
28987
- textSample: proc.accumulatedText.slice(0, 200),
28988
29223
  tokenCount: usage.tokenCount,
28989
29224
  traceId: base.traceId
28990
29225
  });
@@ -29177,8 +29412,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
29177
29412
  logger10.info("Captured non-streamed assistant message", {
29178
29413
  agentId: proc.agentId,
29179
29414
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
29180
- textLen: text.length,
29181
- textSample: text.slice(0, 100)
29415
+ textLen: text.length
29182
29416
  });
29183
29417
  } else {
29184
29418
  proc.lastAssistantContentDescription = describeAssistantContent(am.message?.content);
@@ -29202,6 +29436,7 @@ function resetAccumulators(proc) {
29202
29436
  proc.currentToolName = null;
29203
29437
  proc.currentMcpInvocationId = null;
29204
29438
  proc.currentMcpInvocationStartedAt = null;
29439
+ proc.activeToolUseStartedAt = void 0;
29205
29440
  proc.segmentBuffer = "";
29206
29441
  proc.segmentCount = 0;
29207
29442
  proc.accumulatedToolInput = "";
@@ -29211,6 +29446,7 @@ function resetAccumulators(proc) {
29211
29446
  proc.officialMediaGenerationSatisfied = false;
29212
29447
  proc.suppressCurrentThinking = false;
29213
29448
  proc.suppressCurrentToolUse = false;
29449
+ proc.activeSubagentTaskIds?.clear();
29214
29450
  }
29215
29451
 
29216
29452
  // src/forkHistoryReplay.ts
@@ -29398,7 +29634,7 @@ function missingSubscriptionMessage(subscriptionId) {
29398
29634
  }
29399
29635
  var NODE_USER_UID = 1e3;
29400
29636
  var POST_MERGE_CONTINUATION_ROUTE_MS = 15e3;
29401
- var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-prompt-v2";
29637
+ var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-mcp-abi-prompt-v4";
29402
29638
  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;
29403
29639
  var DOCUMENT_READING_RULES = `DOCUMENT READING:
29404
29640
  - The built-in Read tool cannot read binary office documents such as .docx, .xls, .xlsx, .pptx, .pdf, .odt, .ods, .odp, or .rtf.
@@ -29413,6 +29649,16 @@ var MEDIA_GENERATION_RULES = `MEDIA GENERATION:
29413
29649
  - 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.
29414
29650
  - 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.
29415
29651
  - 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.`;
29652
+ function stableFingerprintValue(value) {
29653
+ if (Array.isArray(value)) return value.map(stableFingerprintValue);
29654
+ if (!value || typeof value !== "object") return value;
29655
+ const out = {};
29656
+ for (const key of Object.keys(value).sort()) {
29657
+ const normalized = stableFingerprintValue(value[key]);
29658
+ if (normalized !== void 0) out[key] = normalized;
29659
+ }
29660
+ return out;
29661
+ }
29416
29662
  function isRecoveryDispatchTask(task) {
29417
29663
  return task.dispatchKind === "manual_continue" || task.dispatchKind === "regenerate";
29418
29664
  }
@@ -29670,13 +29916,15 @@ var AgentManager = class {
29670
29916
  agents = /* @__PURE__ */ new Map();
29671
29917
  lastUsedAt = /* @__PURE__ */ new Map();
29672
29918
  /** Scopes 被 zombie_watchdog 关闭后的"入睡"标记,acquire 重建时清除并 emit awake。 */
29673
- dormantScopes = /* @__PURE__ */ new Set();
29919
+ dormantScopes = /* @__PURE__ */ new Map();
29674
29920
  /**
29675
29921
  * zombie_watchdog 拆 runtime 时,把该 (agentId, scope) 的 groupInbox 快照到这里,
29676
29922
  * 让下一次 getOrCreate 重建 runtime 时可以恢复未读消息。仅 in-memory;
29677
29923
  * bridge 进程崩溃 / shutdownAll 时丢失,与现有 inbox 内存语义一致。
29678
29924
  */
29679
29925
  dormantGroupInboxes = /* @__PURE__ */ new Map();
29926
+ /** Spectate requested before runtime existed; value = activatedAt epoch ms. */
29927
+ pendingSpectate = /* @__PURE__ */ new Map();
29680
29928
  sessionStore;
29681
29929
  dispatchMemory = new GroupDispatchMemoryStore();
29682
29930
  dataDir;
@@ -29842,6 +30090,7 @@ var AgentManager = class {
29842
30090
  }
29843
30091
  async resolveRuntimeCwd(agentConfig, scope, requestedCwd) {
29844
30092
  let cwd = this.remapServerWorkspaceCwd(agentConfig, scope, requestedCwd);
30093
+ let fallbackForensicsId;
29845
30094
  if (!isFullyQualifiedAbsolutePath(cwd)) {
29846
30095
  const fallback = path13.join(this.workspacesDir, this.localScopeDirName(agentConfig, scope));
29847
30096
  logger13.error(
@@ -29856,6 +30105,23 @@ var AgentManager = class {
29856
30105
  error: new Error("workdir_not_usable_on_this_machine")
29857
30106
  }
29858
30107
  );
30108
+ fallbackForensicsId = createFallbackId();
30109
+ logFallback(logger13, {
30110
+ fallbackId: fallbackForensicsId,
30111
+ type: "cwd_sandbox",
30112
+ phase: "applied",
30113
+ expected: false,
30114
+ context: {
30115
+ agentId: agentConfig.id,
30116
+ scope: scopeKey(scope),
30117
+ platform: process.platform,
30118
+ requested: requestedCwd,
30119
+ resolved: cwd,
30120
+ reason: "not_fully_qualified",
30121
+ fallback
30122
+ },
30123
+ outcome: { result: "sandbox_fallback" }
30124
+ });
29859
30125
  cwd = fallback;
29860
30126
  }
29861
30127
  if (isRunningAsRoot() && cwd.startsWith("/root/")) {
@@ -29874,6 +30140,21 @@ var AgentManager = class {
29874
30140
  fallback,
29875
30141
  error: e
29876
30142
  });
30143
+ const fbId = fallbackForensicsId ?? createFallbackId();
30144
+ logFallback(logger13, {
30145
+ fallbackId: fbId,
30146
+ type: "cwd_sandbox",
30147
+ phase: "applied",
30148
+ expected: false,
30149
+ context: {
30150
+ agentId: agentConfig.id,
30151
+ scope: scopeKey(scope),
30152
+ reason: "mkdir_failed",
30153
+ requested: cwd,
30154
+ fallback
30155
+ },
30156
+ outcome: { result: "second_layer_fallback" }
30157
+ });
29877
30158
  await fs6.mkdir(fallback, { recursive: true });
29878
30159
  return fallback;
29879
30160
  }
@@ -30096,17 +30377,24 @@ var AgentManager = class {
30096
30377
  });
30097
30378
  return null;
30098
30379
  }
30099
- scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection) {
30100
- return 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(path13.normalize(agentCwd)).update("\0").update(scopesSection).digest("hex");
30380
+ scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection, externalMcpFingerprint) {
30381
+ return 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(path13.normalize(agentCwd)).update("\0").update(scopesSection).update("\0").update(externalMcpFingerprint).digest("hex");
30101
30382
  }
30102
- discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint) {
30383
+ externalMcpFingerprint(externalMcp) {
30384
+ const serverNames = Object.keys(externalMcp.mcpServers).sort();
30385
+ const allowedTools = [...externalMcp.allowedTools].sort();
30386
+ const toolAbi = [...externalMcp.toolAbi ?? []].sort((a, b) => a.serverName.localeCompare(b.serverName)).map(stableFingerprintValue);
30387
+ if (serverNames.length === 0 && allowedTools.length === 0 && toolAbi.length === 0) return "";
30388
+ return JSON.stringify({ serverNames, allowedTools, toolAbi });
30389
+ }
30390
+ discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint, options = {}) {
30103
30391
  const previous = this.sessionStore.getPromptFingerprint(agentConfig.id, scope);
30104
30392
  if (!sessionId) {
30105
30393
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
30106
30394
  return null;
30107
30395
  }
30108
30396
  if (previous === fingerprint) return sessionId;
30109
- if (!previous && scope.kind === "single") {
30397
+ if (!previous && scope.kind === "single" && options.clearLegacySingleSession !== true) {
30110
30398
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
30111
30399
  logger13.info("Retaining legacy single-scope session while recording prompt fingerprint", {
30112
30400
  agentId: agentConfig.id,
@@ -30126,7 +30414,8 @@ var AgentManager = class {
30126
30414
  sessionId,
30127
30415
  previousFingerprint: previous,
30128
30416
  nextFingerprint: fingerprint,
30129
- revision: SCOPE_PROMPT_FINGERPRINT_REVISION
30417
+ revision: SCOPE_PROMPT_FINGERPRINT_REVISION,
30418
+ reason: options.reason ?? "scope_prompt_changed"
30130
30419
  });
30131
30420
  return null;
30132
30421
  }
@@ -30175,6 +30464,7 @@ var AgentManager = class {
30175
30464
  logger13.info("Evicting idle Agent query", { agentId: proc.agentId, scope: scopeKey(proc.scope) });
30176
30465
  const runtime = this.asRuntime(proc);
30177
30466
  this.clearQuietFlushTimer(runtime);
30467
+ this.teardownSpectate(runtime);
30178
30468
  try {
30179
30469
  runtime.inputController.close();
30180
30470
  await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
@@ -30194,6 +30484,7 @@ var AgentManager = class {
30194
30484
  const runtime = this.asRuntime(proc);
30195
30485
  const key = runtimeKey(proc.agentId, proc.scope);
30196
30486
  this.clearQuietFlushTimer(runtime);
30487
+ this.teardownSpectate(runtime);
30197
30488
  runtime.currentTask = null;
30198
30489
  runtime.injectedTasks = [];
30199
30490
  runtime.mergedTasks = [];
@@ -30230,6 +30521,7 @@ var AgentManager = class {
30230
30521
  evictIdle() {
30231
30522
  const now = Date.now();
30232
30523
  const { idleTimeoutMs, workingSilenceTimeoutMs } = this.queryConfig;
30524
+ const stallWarnAfterMs = Math.min(9e4, this.queryConfig.replyStallTimeoutMs);
30233
30525
  for (const [key, proc] of this.agents) {
30234
30526
  if (!this.isEvictable(proc)) continue;
30235
30527
  const runtime = this.asRuntime(proc);
@@ -30240,26 +30532,62 @@ var AgentManager = class {
30240
30532
  for (const [, proc] of this.agents) {
30241
30533
  if (proc.status !== "working") continue;
30242
30534
  const runtime = this.asRuntime(proc);
30535
+ if (runtime.currentTask) {
30536
+ const sinceEventMs = now - proc.lastSdkEventAt;
30537
+ if (sinceEventMs > stallWarnAfterMs && !proc.stallWarned) {
30538
+ proc.stallWarned = true;
30539
+ const openTool = this.latestOpenToolUse(proc);
30540
+ logger13.warn("Reply stall onset: in-flight reply silent", {
30541
+ agentId: proc.agentId,
30542
+ scope: scopeKey(proc.scope),
30543
+ sinceEventMs,
30544
+ replyStallTimeoutMs: this.queryConfig.replyStallTimeoutMs,
30545
+ workingSilenceTimeoutMs,
30546
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
30547
+ replyMessageId: runtime.currentTask.replyMessageId,
30548
+ model: proc.model ?? "(unknown)",
30549
+ lastSdkEvent: proc.lastSdkEventInfo,
30550
+ hasActiveToolUse: runtime.activeToolUseStartedAt != null || openTool != null,
30551
+ activeToolUseAgeMs: runtime.activeToolUseStartedAt != null ? now - runtime.activeToolUseStartedAt : null,
30552
+ openToolName: openTool?.toolName ?? proc.currentToolName ?? null,
30553
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null,
30554
+ subagentInFlight: (runtime.activeSubagentTaskIds?.size ?? 0) > 0,
30555
+ activeSubagentCount: runtime.activeSubagentTaskIds?.size ?? 0,
30556
+ busyReason: this.busyReason(proc)
30557
+ });
30558
+ }
30559
+ }
30560
+ const busyReason = this.busyReason(runtime);
30561
+ const busy = busyReason !== null;
30562
+ if (runtime.currentTask && !busy && now - proc.lastSdkEventAt > this.queryConfig.replyStallTimeoutMs) {
30563
+ void this.recoverStalledReply(proc, now - proc.lastSdkEventAt);
30564
+ continue;
30565
+ }
30243
30566
  const hasInjectedBacklog = runtime.injectedTasks.length > 0;
30244
- const effectiveTimeoutMs = hasInjectedBacklog ? workingSilenceTimeoutMs * 2 : workingSilenceTimeoutMs;
30567
+ const baseCeilingMs = busy ? Math.max(workingSilenceTimeoutMs, this.queryConfig.busySilenceTimeoutMs ?? 0) : workingSilenceTimeoutMs;
30568
+ const effectiveTimeoutMs = hasInjectedBacklog ? baseCeilingMs * 2 : baseCeilingMs;
30245
30569
  const silentMs = now - proc.lastSdkEventAt;
30246
30570
  if (silentMs <= effectiveTimeoutMs) {
30247
- if (hasInjectedBacklog && silentMs > workingSilenceTimeoutMs) {
30248
- logger13.warn(
30249
- "Zombie watchdog: working runtime silent past base timeout but has queued tasks; granting extended grace",
30250
- {
30251
- agentId: proc.agentId,
30252
- scope: scopeKey(proc.scope),
30253
- silentMs,
30254
- baseTimeoutMs: workingSilenceTimeoutMs,
30255
- effectiveTimeoutMs,
30256
- injectedTaskCount: runtime.injectedTasks.length,
30257
- replyMessageId: proc.currentTask?.replyMessageId
30258
- }
30259
- );
30571
+ if (silentMs > workingSilenceTimeoutMs) {
30572
+ logger13.warn("Zombie watchdog: working runtime silent past base timeout; granting extended grace", {
30573
+ agentId: proc.agentId,
30574
+ scope: scopeKey(proc.scope),
30575
+ silentMs,
30576
+ baseTimeoutMs: workingSilenceTimeoutMs,
30577
+ baseCeilingMs,
30578
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
30579
+ effectiveTimeoutMs,
30580
+ busy,
30581
+ busyReason,
30582
+ injectedTaskCount: runtime.injectedTasks.length,
30583
+ replyMessageId: proc.currentTask?.replyMessageId
30584
+ });
30260
30585
  }
30261
30586
  continue;
30262
30587
  }
30588
+ const zombieOpenTool = this.latestOpenToolUse(proc);
30589
+ const watchdogDetectedAt = Date.now();
30590
+ const watchdogFallbackId = createFallbackId();
30263
30591
  logger13.warn("Zombie watchdog: working runtime silent too long, tearing down", {
30264
30592
  agentId: proc.agentId,
30265
30593
  scope: scopeKey(proc.scope),
@@ -30267,12 +30595,46 @@ var AgentManager = class {
30267
30595
  lastSdkEventAt: new Date(proc.lastSdkEventAt).toISOString(),
30268
30596
  workingSilenceTimeoutMs,
30269
30597
  effectiveTimeoutMs,
30598
+ baseCeilingMs,
30599
+ busy,
30600
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
30270
30601
  replyMessageId: proc.currentTask?.replyMessageId,
30271
30602
  injectedTaskCount: runtime.injectedTasks.length,
30272
30603
  hadInjectedBacklog: hasInjectedBacklog,
30273
- inboxSize: proc.groupInbox.length
30604
+ inboxSize: proc.groupInbox.length,
30605
+ fallbackId: watchdogFallbackId,
30606
+ // Breadcrumb: what the turn was waiting on when it timed out. A hung subagent
30607
+ // (open `Task` tool_use) lands here now that the fast path skips active tools.
30608
+ model: proc.model ?? "(unknown)",
30609
+ lastSdkEvent: proc.lastSdkEventInfo,
30610
+ openToolName: zombieOpenTool?.toolName ?? proc.currentToolName ?? null,
30611
+ activeToolUseAgeMs: runtime.activeToolUseStartedAt != null ? now - runtime.activeToolUseStartedAt : null,
30612
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null,
30613
+ subagentInFlight: (runtime.activeSubagentTaskIds?.size ?? 0) > 0,
30614
+ activeSubagentCount: runtime.activeSubagentTaskIds?.size ?? 0,
30615
+ busyReason: this.busyReason(proc)
30616
+ });
30617
+ logFallback(logger13, {
30618
+ fallbackId: watchdogFallbackId,
30619
+ type: "zombie_watchdog",
30620
+ phase: "detected",
30621
+ expected: false,
30622
+ traceId: proc.currentTask?.traceId,
30623
+ context: {
30624
+ agentId: proc.agentId,
30625
+ scope: scopeKey(proc.scope),
30626
+ silentMs,
30627
+ replyMessageId: proc.currentTask?.replyMessageId ?? null,
30628
+ openToolName: zombieOpenTool?.toolName ?? proc.currentToolName ?? null,
30629
+ lastSdkEventType: proc.lastSdkEventInfo?.type ?? null,
30630
+ injectedTaskCount: runtime.injectedTasks.length,
30631
+ inboxSize: proc.groupInbox.length
30632
+ }
30633
+ });
30634
+ void this.closeRuntime(proc, "zombie_watchdog", {
30635
+ fallbackId: watchdogFallbackId,
30636
+ detectedAt: watchdogDetectedAt
30274
30637
  });
30275
- void this.closeRuntime(proc, "zombie_watchdog");
30276
30638
  }
30277
30639
  }
30278
30640
  /**
@@ -30519,7 +30881,7 @@ ${cfg.instructions.trim()}` : "";
30519
30881
  agentId: agentConfig.id,
30520
30882
  capabilityTier: cfg.capabilityTier,
30521
30883
  isSmith: smithAgent
30522
- }) ?? { mcpServers: {}, allowedTools: [] };
30884
+ }) ?? { mcpServers: {}, allowedTools: [], toolAbi: [] };
30523
30885
  logger13.info("External MCP resolved for runtime", {
30524
30886
  agentId: agentConfig.id,
30525
30887
  scope: scopeKey(scope),
@@ -30536,11 +30898,16 @@ ${cfg.instructions.trim()}` : "";
30536
30898
  }
30537
30899
  const notebookSection = this.buildNotebookSection(agentConfig.id);
30538
30900
  const scopesSection = this.buildScopesSection(agentConfig, scope, agentCwd);
30901
+ const externalMcpFingerprint = this.externalMcpFingerprint(externalMcp);
30539
30902
  savedSessionId = this.discardSessionIfScopePromptChanged(
30540
30903
  agentConfig,
30541
30904
  scope,
30542
30905
  savedSessionId,
30543
- this.scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection)
30906
+ this.scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection, externalMcpFingerprint),
30907
+ {
30908
+ clearLegacySingleSession: externalMcpFingerprint.length > 0,
30909
+ reason: externalMcpFingerprint.length > 0 ? "external_mcp_abi_changed" : "scope_prompt_changed"
30910
+ }
30544
30911
  );
30545
30912
  let forkHistorySection = "";
30546
30913
  if (!savedSessionId && scope.kind === "single") {
@@ -30561,6 +30928,8 @@ ${cfg.instructions.trim()}` : "";
30561
30928
  }
30562
30929
  }
30563
30930
  const cronLockSnapshot = readCronLockSnapshot();
30931
+ const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
30932
+ const disallowedToolsForRuntime = builtinWebSearchAllowed ? [] : ["WebSearch"];
30564
30933
  logger13.info("Creating Agent query", {
30565
30934
  agentId: agentConfig.id,
30566
30935
  scope: scopeKey(scope),
@@ -30569,6 +30938,8 @@ ${cfg.instructions.trim()}` : "";
30569
30938
  sessionId: savedSessionId,
30570
30939
  forkHistoryReplay: forkHistorySection.length > 0,
30571
30940
  model: cfg.model ?? "(default)",
30941
+ builtinWebSearchAllowed,
30942
+ disallowedTools: disallowedToolsForRuntime,
30572
30943
  // Diagnostic: who currently owns Claude's global cron lock (~/.claude/scheduled_tasks.lock).
30573
30944
  // Cron is process-internal but the binary uses this singleton lock to elect ONE scheduler
30574
30945
  // among concurrent claude subprocesses. If lock is held by another session at spawn time,
@@ -30580,7 +30951,6 @@ ${cfg.instructions.trim()}` : "";
30580
30951
  });
30581
30952
  const planModeRef = { active: false, denyCount: 0 };
30582
30953
  const mediaGenerationTurnGuard = createOfficialMediaGenerationTurnGuard();
30583
- const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
30584
30954
  const options = {
30585
30955
  cwd: agentCwd,
30586
30956
  systemPrompt: {
@@ -30647,6 +31017,8 @@ ${cfg.instructions.trim()}` : "";
30647
31017
  ] : [],
30648
31018
  ...externalMcp.allowedTools
30649
31019
  ],
31020
+ // Server-side WebSearch bypasses canUseTool; disallowedTools removes it from model context.
31021
+ disallowedTools: disallowedToolsForRuntime,
30650
31022
  mcpServers: { ...externalMcp.mcpServers, neural: neuralServer },
30651
31023
  includePartialMessages: true,
30652
31024
  // Plan mode custom workflow instructions. When setPermissionMode('plan') is
@@ -30973,6 +31345,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
30973
31345
  currentTask: null,
30974
31346
  currentTaskStartedAt: 0,
30975
31347
  lastSdkEventAt: Date.now(),
31348
+ model: (typeof options.model === "string" ? options.model : cfg.model) ?? null,
30976
31349
  compactRequested: false,
30977
31350
  compactInProgress: false,
30978
31351
  contextOverflowLockedUntil: 0,
@@ -30984,13 +31357,18 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
30984
31357
  currentToolName: null,
30985
31358
  currentMcpInvocationId: null,
30986
31359
  currentMcpInvocationStartedAt: null,
31360
+ activeSubagentTaskIds: /* @__PURE__ */ new Set(),
30987
31361
  mcpAuditRecorder: this.mcpAuditRecorder,
30988
31362
  segmentBuffer: "",
30989
31363
  segmentCount: 0,
30990
31364
  accumulatedToolInput: "",
30991
31365
  planModeRef,
30992
31366
  mediaGenerationTurnGuard,
30993
- groupInbox: []
31367
+ groupInbox: [],
31368
+ spectating: false,
31369
+ spectateActivatedAt: 0,
31370
+ spectateViewing: false,
31371
+ spectateTtlExpired: false
30994
31372
  };
30995
31373
  const runtime = Object.assign(proc, {
30996
31374
  query: agentQuery,
@@ -31002,7 +31380,8 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
31002
31380
  createdAt: Date.now(),
31003
31381
  supportsVision: modelInputMode === "vision" && cfg.supportsVision !== false,
31004
31382
  modelInputMode,
31005
- quietFlushTimer: null
31383
+ quietFlushTimer: null,
31384
+ spectateRevertTimer: null
31006
31385
  });
31007
31386
  logger13.info("Agent model input mode resolved", {
31008
31387
  agentId: agentConfig.id,
@@ -31027,6 +31406,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
31027
31406
  } else {
31028
31407
  this.dormantGroupInboxes.delete(key);
31029
31408
  }
31409
+ const dormantMeta = this.dormantScopes.get(key);
31030
31410
  if (this.dormantScopes.delete(key)) {
31031
31411
  this.emit({
31032
31412
  type: "agent:awake",
@@ -31038,7 +31418,44 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
31038
31418
  });
31039
31419
  logger13.info("Agent scope awakened after dormant", {
31040
31420
  agentId: agentConfig.id,
31041
- scope: scopeKey(scope)
31421
+ scope: scopeKey(scope),
31422
+ ...dormantMeta ? {
31423
+ fallbackId: dormantMeta.fallbackId,
31424
+ dormantDurationMs: Date.now() - dormantMeta.detectedAt,
31425
+ dataLossSuspected: dormantMeta.droppedTaskCount > 0
31426
+ } : {}
31427
+ });
31428
+ if (dormantMeta) {
31429
+ logFallback(logger13, {
31430
+ fallbackId: dormantMeta.fallbackId,
31431
+ type: "zombie_watchdog",
31432
+ phase: "outcome",
31433
+ expected: false,
31434
+ context: {
31435
+ agentId: agentConfig.id,
31436
+ scope: scopeKey(scope)
31437
+ },
31438
+ outcome: {
31439
+ result: "recovered_rebuilt",
31440
+ durationMs: Date.now() - dormantMeta.detectedAt,
31441
+ dataLossSuspected: dormantMeta.droppedTaskCount > 0
31442
+ }
31443
+ });
31444
+ }
31445
+ }
31446
+ const pendingSpectateAt = this.pendingSpectate.get(key);
31447
+ if (pendingSpectateAt != null) {
31448
+ this.pendingSpectate.delete(key);
31449
+ runtime.spectating = true;
31450
+ runtime.spectateViewing = false;
31451
+ runtime.spectateActivatedAt = pendingSpectateAt;
31452
+ runtime.spectateTtlExpired = false;
31453
+ this.armSpectateTimer(runtime);
31454
+ this.emitSpectateState(runtime, true, "started");
31455
+ logger13.info("Applied pending spectate on runtime create", {
31456
+ agentId: agentConfig.id,
31457
+ scope: scopeKey(scope),
31458
+ activatedAt: pendingSpectateAt
31042
31459
  });
31043
31460
  }
31044
31461
  if (proc.groupInbox.length > 0 && this.isRuntimeIdleForInboxFlush(runtime)) {
@@ -32176,7 +32593,6 @@ ${lines.join("\n")}`;
32176
32593
  compactTrigger: "context_watermark",
32177
32594
  injectedTasksWaiting: runtime.injectedTasks.length,
32178
32595
  compactPromptLen: compactPrompt.length,
32179
- promptSample: compactPrompt.slice(0, 80),
32180
32596
  traceId: compactTraceId
32181
32597
  });
32182
32598
  runtime.inputController.push(compactPrompt, runtime.ccSessionId ?? "");
@@ -32458,7 +32874,7 @@ ${lines.join("\n")}`;
32458
32874
  const enveloped = buildInnerVoiceEnvelope(payloadWithTrigger, ctx);
32459
32875
  const task = {
32460
32876
  content: enveloped,
32461
- replyMessageId: createMessageId(),
32877
+ replyMessageId: createNeuralSendReplyMessageId(),
32462
32878
  conversationId: payload.conversationId,
32463
32879
  traceId: createTraceId(),
32464
32880
  groupId: payload.groupId
@@ -32704,7 +33120,7 @@ ${lines.join("\n")}`;
32704
33120
  this.dormantScopes.delete(key);
32705
33121
  this.dormantGroupInboxes.delete(key);
32706
33122
  }
32707
- for (const key of [...this.dormantScopes].filter(
33123
+ for (const key of [...this.dormantScopes.keys()].filter(
32708
33124
  (k) => k === agentId || k.startsWith(`${agentId}::`)
32709
33125
  )) {
32710
33126
  this.dormantScopes.delete(key);
@@ -32752,7 +33168,7 @@ ${lines.join("\n")}`;
32752
33168
  async reloadAgentScopes(agentId, reason) {
32753
33169
  this.sessionStore.deleteAllForAgent(agentId);
32754
33170
  this.dispatchMemory.deleteAllForAgent(agentId);
32755
- for (const key of [...this.dormantScopes].filter(
33171
+ for (const key of [...this.dormantScopes.keys()].filter(
32756
33172
  (k) => k === agentId || k.startsWith(`${agentId}::`)
32757
33173
  )) {
32758
33174
  this.dormantScopes.delete(key);
@@ -32904,6 +33320,125 @@ ${lines.join("\n")}`;
32904
33320
  void this.terminateScope(proc.agentId, proc.scope);
32905
33321
  }
32906
33322
  }
33323
+ /** Control spectate capture/push for one scoped runtime. */
33324
+ async setSpectate(agentId, scope, action) {
33325
+ const key = runtimeKey(agentId, scope);
33326
+ const proc = this.agents.get(key);
33327
+ if (!proc || proc.status === "dead") {
33328
+ if (action === "start") {
33329
+ this.pendingSpectate.set(key, Date.now());
33330
+ logger13.info("setSpectate: runtime missing, pending start", { agentId, scope: scopeKey(scope) });
33331
+ }
33332
+ return;
33333
+ }
33334
+ const runtime = this.asRuntime(proc);
33335
+ switch (action) {
33336
+ case "start":
33337
+ runtime.spectating = true;
33338
+ runtime.spectateViewing = true;
33339
+ runtime.spectateActivatedAt = Date.now();
33340
+ runtime.spectateTtlExpired = false;
33341
+ this.pendingSpectate.delete(key);
33342
+ this.armSpectateTimer(runtime);
33343
+ this.emitSpectateState(runtime, true, "started");
33344
+ logger13.info("Spectate started", { agentId, scope: scopeKey(scope) });
33345
+ break;
33346
+ case "enter_view":
33347
+ runtime.spectateViewing = true;
33348
+ logger13.info("Spectate enter_view", { agentId, scope: scopeKey(scope) });
33349
+ break;
33350
+ case "leave_view":
33351
+ runtime.spectateViewing = false;
33352
+ logger13.info("Spectate leave_view", {
33353
+ agentId,
33354
+ scope: scopeKey(scope),
33355
+ ttlExpired: runtime.spectateTtlExpired
33356
+ });
33357
+ if (runtime.spectateTtlExpired) {
33358
+ this.stopSpectate(runtime, "ttl_expired");
33359
+ }
33360
+ break;
33361
+ case "stop":
33362
+ this.stopSpectate(runtime, "stopped");
33363
+ this.pendingSpectate.delete(key);
33364
+ logger13.info("Spectate stopped", { agentId, scope: scopeKey(scope) });
33365
+ break;
33366
+ default:
33367
+ break;
33368
+ }
33369
+ }
33370
+ spectateTtlMs() {
33371
+ return Number(process.env.AHCHAT_BRIDGE_SPECTATE_TTL_MS) || 36e5;
33372
+ }
33373
+ armSpectateTimer(runtime) {
33374
+ this.clearSpectateTimer(runtime);
33375
+ if (!runtime.spectating || runtime.spectateActivatedAt <= 0) return;
33376
+ const ttlMs = this.spectateTtlMs();
33377
+ const elapsed = Date.now() - runtime.spectateActivatedAt;
33378
+ const delay = Math.max(0, ttlMs - elapsed);
33379
+ runtime.spectateRevertTimer = setTimeout(() => {
33380
+ runtime.spectateRevertTimer = null;
33381
+ runtime.spectateTtlExpired = true;
33382
+ logger13.info("Spectate TTL expired", {
33383
+ agentId: runtime.agentId,
33384
+ scope: scopeKey(runtime.scope),
33385
+ viewing: runtime.spectateViewing
33386
+ });
33387
+ if (!runtime.spectateViewing) {
33388
+ this.stopSpectate(runtime, "ttl_expired");
33389
+ }
33390
+ }, delay);
33391
+ }
33392
+ clearSpectateTimer(runtime) {
33393
+ if (runtime.spectateRevertTimer != null) {
33394
+ clearTimeout(runtime.spectateRevertTimer);
33395
+ runtime.spectateRevertTimer = null;
33396
+ }
33397
+ }
33398
+ stopSpectate(runtime, reason) {
33399
+ const wasActive = runtime.spectating;
33400
+ this.clearSpectateTimer(runtime);
33401
+ runtime.spectating = false;
33402
+ runtime.spectateViewing = false;
33403
+ runtime.spectateTtlExpired = false;
33404
+ runtime.spectateActivatedAt = 0;
33405
+ if (wasActive) {
33406
+ this.emitSpectateState(runtime, false, reason);
33407
+ logger13.info("Spectate deactivated", {
33408
+ agentId: runtime.agentId,
33409
+ scope: scopeKey(runtime.scope),
33410
+ reason
33411
+ });
33412
+ }
33413
+ }
33414
+ emitSpectateState(runtime, active, reason) {
33415
+ const scopePayload = runtime.scope.kind === "single" ? { kind: "single" } : { kind: "group", groupId: runtime.scope.groupId };
33416
+ this.emit({
33417
+ type: "spectate:state",
33418
+ payload: {
33419
+ agentId: runtime.agentId,
33420
+ scope: scopePayload,
33421
+ active,
33422
+ expiresAt: active ? runtime.spectateActivatedAt + this.spectateTtlMs() : void 0,
33423
+ reason,
33424
+ traceId: createTraceId()
33425
+ }
33426
+ });
33427
+ logger13.info("Spectate state emitted", {
33428
+ agentId: runtime.agentId,
33429
+ scope: scopeKey(runtime.scope),
33430
+ active,
33431
+ reason,
33432
+ expiresAt: active ? runtime.spectateActivatedAt + this.spectateTtlMs() : void 0
33433
+ });
33434
+ }
33435
+ teardownSpectate(runtime) {
33436
+ if (runtime.spectating) {
33437
+ this.stopSpectate(runtime, "runtime_gone");
33438
+ } else {
33439
+ this.clearSpectateTimer(runtime);
33440
+ }
33441
+ }
32907
33442
  /** Stop one scoped SDK runtime (workdir change). */
32908
33443
  async terminateScope(agentId, scope) {
32909
33444
  const key = runtimeKey(agentId, scope);
@@ -32912,6 +33447,7 @@ ${lines.join("\n")}`;
32912
33447
  logger13.info("terminateScope: no active runtime", { agentId, scope: scopeKey(scope) });
32913
33448
  this.dormantScopes.delete(key);
32914
33449
  this.dormantGroupInboxes.delete(key);
33450
+ this.pendingSpectate.delete(key);
32915
33451
  this.sessionStore.delete(agentId, scope);
32916
33452
  this.dispatchMemory.deleteScope(agentId, scope);
32917
33453
  return;
@@ -32934,7 +33470,7 @@ ${lines.join("\n")}`;
32934
33470
  this.dispatchMemory.deleteScope(agentId, scope);
32935
33471
  logger13.info("terminateScope: scoped query removed", { agentId, scope: scopeKey(scope) });
32936
33472
  }
32937
- async closeRuntime(proc, reason) {
33473
+ async closeRuntime(proc, reason, watchdogForensics) {
32938
33474
  const key = runtimeKey(proc.agentId, proc.scope);
32939
33475
  if (proc.status === "dead") return;
32940
33476
  const runtime = this.asRuntime(proc);
@@ -33011,12 +33547,13 @@ ${lines.join("\n")}`;
33011
33547
  runtime.currentTask = null;
33012
33548
  if (isWatchdog) {
33013
33549
  const preservedInbox = proc.groupInbox;
33014
- if (preservedInbox.length > 0) {
33550
+ const preservedInboxSize = preservedInbox.length;
33551
+ if (preservedInboxSize > 0) {
33015
33552
  this.dormantGroupInboxes.set(key, [...preservedInbox]);
33016
33553
  logger13.info("Preserving groupInbox for dormant agent", {
33017
33554
  agentId,
33018
33555
  scope: scopeKey(proc.scope),
33019
- preservedInboxSize: preservedInbox.length,
33556
+ preservedInboxSize,
33020
33557
  preservedEntries: preservedInbox.map((e) => ({
33021
33558
  ackId: e.ackId,
33022
33559
  sender: e.senderName,
@@ -33025,7 +33562,26 @@ ${lines.join("\n")}`;
33025
33562
  }))
33026
33563
  });
33027
33564
  }
33028
- this.dormantScopes.add(key);
33565
+ const effectiveFallbackId = watchdogForensics?.fallbackId ?? createFallbackId();
33566
+ logFallback(logger13, {
33567
+ fallbackId: effectiveFallbackId,
33568
+ type: "zombie_watchdog",
33569
+ phase: "applied",
33570
+ expected: false,
33571
+ traceId: dormantTraceId,
33572
+ context: {
33573
+ agentId,
33574
+ scope: scopeKey(proc.scope),
33575
+ droppedTaskCount: droppedAckIds.length,
33576
+ preservedInboxSize,
33577
+ sessionDeleted: false
33578
+ }
33579
+ });
33580
+ this.dormantScopes.set(key, {
33581
+ fallbackId: effectiveFallbackId,
33582
+ detectedAt: watchdogForensics?.detectedAt ?? Date.now(),
33583
+ droppedTaskCount: droppedAckIds.length
33584
+ });
33029
33585
  this.emit({
33030
33586
  type: "agent:dormant",
33031
33587
  payload: {
@@ -33040,13 +33596,15 @@ ${lines.join("\n")}`;
33040
33596
  agentId,
33041
33597
  scope: scopeKey(proc.scope),
33042
33598
  droppedTaskCount: droppedAckIds.length,
33043
- preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0
33599
+ preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0,
33600
+ fallbackId: effectiveFallbackId
33044
33601
  });
33045
33602
  }
33046
33603
  proc.status = "dead";
33047
33604
  this.agents.delete(key);
33048
33605
  this.lastUsedAt.delete(key);
33049
33606
  this.clearQuietFlushTimer(runtime);
33607
+ this.teardownSpectate(runtime);
33050
33608
  try {
33051
33609
  runtime.inputController.close();
33052
33610
  await this.awaitQueryReturn(runtime.query, 5e3, agentId);
@@ -33063,6 +33621,165 @@ ${lines.join("\n")}`;
33063
33621
  cwd: proc.cwd
33064
33622
  });
33065
33623
  }
33624
+ /**
33625
+ * Emit `agent:error` for the active reply and every queued/merged/buffered task,
33626
+ * then clear those queues. Used by both the SDK stream-crash path and the
33627
+ * reply-stall watchdog so a torn-down runtime never leaves a carrier reply
33628
+ * stuck in-flight on the server (which would keep absorbing new user messages
33629
+ * as steers of a dead turn).
33630
+ */
33631
+ failPendingTasksWithError(runtime, errorText, fallbackId) {
33632
+ const pending = [];
33633
+ if (runtime.currentTask) pending.push(runtime.currentTask);
33634
+ pending.push(...runtime.injectedTasks, ...runtime.mergedTasks, ...runtime.planModeBuffer);
33635
+ runtime.currentTask = null;
33636
+ runtime.injectedTasks = [];
33637
+ runtime.mergedTasks = [];
33638
+ runtime.planModeBuffer = [];
33639
+ if (pending.length === 0) return { pendingCount: 0 };
33640
+ const carrier = pending[0];
33641
+ const mergedTasks = pending.slice(1);
33642
+ logger13.warn("Pending tasks failure consolidated", {
33643
+ agentId: runtime.agentId,
33644
+ scope: scopeKey(runtime.scope),
33645
+ pendingCount: pending.length,
33646
+ carrierAckId: carrier.replyMessageId,
33647
+ mergedAckIds: mergedTasks.map((t) => t.replyMessageId),
33648
+ traceId: carrier.traceId,
33649
+ ...fallbackId ? { fallbackId } : {}
33650
+ });
33651
+ this.emit({
33652
+ type: "agent:error",
33653
+ payload: {
33654
+ agentId: runtime.agentId,
33655
+ conversationId: carrier.conversationId,
33656
+ ackId: carrier.replyMessageId,
33657
+ traceId: carrier.traceId,
33658
+ error: errorText
33659
+ }
33660
+ });
33661
+ for (const task of mergedTasks) {
33662
+ this.emit({
33663
+ type: "agent:merged",
33664
+ payload: {
33665
+ agentId: runtime.agentId,
33666
+ conversationId: task.conversationId,
33667
+ ackId: task.replyMessageId,
33668
+ mergedIntoAckId: carrier.replyMessageId,
33669
+ groupId: task.groupId,
33670
+ traceId: task.traceId
33671
+ }
33672
+ });
33673
+ }
33674
+ return { pendingCount: pending.length };
33675
+ }
33676
+ /**
33677
+ * Recover an in-flight reply that started but went silent past
33678
+ * `replyStallTimeoutMs` (see the reply-stall fast path in `evictIdle`). The
33679
+ * underlying SDK turn is wedged with no observable progress and no error, so:
33680
+ * 1. clear the (likely interrupted/dangling) session so the next dispatch
33681
+ * starts fresh instead of resuming the same wedged transcript;
33682
+ * 2. release the carrier reply + queued steers via `agent:error` so the
33683
+ * client stops waiting and the next user message starts a brand-new reply;
33684
+ * 3. tear the wedged runtime down.
33685
+ */
33686
+ async recoverStalledReply(proc, silentMs) {
33687
+ if (proc.status === "dead") return;
33688
+ const runtime = this.asRuntime(proc);
33689
+ const key = runtimeKey(proc.agentId, proc.scope);
33690
+ const replyStallFallbackId = createFallbackId();
33691
+ const stallTraceId = proc.currentTask?.traceId;
33692
+ logger13.warn("Reply stall watchdog: in-flight reply silent too long, recovering", {
33693
+ agentId: proc.agentId,
33694
+ scope: scopeKey(proc.scope),
33695
+ silentMs,
33696
+ replyStallTimeoutMs: this.queryConfig.replyStallTimeoutMs,
33697
+ replyMessageId: proc.currentTask?.replyMessageId,
33698
+ injectedTaskCount: runtime.injectedTasks.length,
33699
+ lastSdkEventAt: new Date(proc.lastSdkEventAt).toISOString(),
33700
+ fallbackId: replyStallFallbackId,
33701
+ // Breadcrumb: what the wedged turn was doing the instant it went silent.
33702
+ // (subagent Task call? mid tool_use? which provider?) — the difference
33703
+ // between a one-off and a systemic provider/tool stall.
33704
+ model: proc.model ?? "(unknown)",
33705
+ lastSdkEvent: proc.lastSdkEventInfo,
33706
+ currentBlockType: proc.currentBlockType,
33707
+ currentToolName: proc.currentToolName,
33708
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null
33709
+ });
33710
+ logFallback(logger13, {
33711
+ fallbackId: replyStallFallbackId,
33712
+ type: "reply_stall",
33713
+ phase: "detected",
33714
+ expected: false,
33715
+ traceId: stallTraceId,
33716
+ context: {
33717
+ agentId: proc.agentId,
33718
+ scope: scopeKey(proc.scope),
33719
+ silentMs,
33720
+ model: proc.model ?? "(unknown)",
33721
+ replyMessageId: proc.currentTask?.replyMessageId ?? null,
33722
+ currentToolName: proc.currentToolName ?? null,
33723
+ lastSdkEventType: proc.lastSdkEventInfo?.type ?? null,
33724
+ injectedTaskCount: runtime.injectedTasks.length,
33725
+ mergedTaskCount: runtime.mergedTasks.length,
33726
+ planModeBufferCount: runtime.planModeBuffer.length
33727
+ }
33728
+ });
33729
+ this.sessionStore.delete(proc.agentId, proc.scope);
33730
+ this.dispatchMemory.deleteScope(proc.agentId, proc.scope);
33731
+ const failSummary = this.failPendingTasksWithError(
33732
+ runtime,
33733
+ "\u56DE\u590D\u957F\u65F6\u95F4\u65E0\u54CD\u5E94\uFF0C\u5DF2\u91CD\u7F6E\u8BE5\u4F1A\u8BDD\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\u3002",
33734
+ replyStallFallbackId
33735
+ );
33736
+ proc.status = "dead";
33737
+ this.agents.delete(key);
33738
+ this.lastUsedAt.delete(key);
33739
+ this.clearQuietFlushTimer(runtime);
33740
+ let queryCloseOk = true;
33741
+ try {
33742
+ runtime.inputController.close();
33743
+ await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
33744
+ } catch (e) {
33745
+ queryCloseOk = false;
33746
+ logger13.error("reply_stall: close query failed", {
33747
+ agentId: proc.agentId,
33748
+ scope: scopeKey(proc.scope),
33749
+ error: e
33750
+ });
33751
+ }
33752
+ logFallback(logger13, {
33753
+ fallbackId: replyStallFallbackId,
33754
+ type: "reply_stall",
33755
+ phase: "applied",
33756
+ expected: false,
33757
+ traceId: stallTraceId,
33758
+ context: {
33759
+ agentId: proc.agentId,
33760
+ scope: scopeKey(proc.scope),
33761
+ sessionDeleted: true,
33762
+ failedTaskCount: failSummary.pendingCount,
33763
+ queryClosed: queryCloseOk
33764
+ }
33765
+ });
33766
+ logFallback(logger13, {
33767
+ fallbackId: replyStallFallbackId,
33768
+ type: "reply_stall",
33769
+ phase: "outcome",
33770
+ expected: false,
33771
+ traceId: stallTraceId,
33772
+ context: {
33773
+ agentId: proc.agentId,
33774
+ scope: scopeKey(proc.scope),
33775
+ failedTaskCount: failSummary.pendingCount
33776
+ },
33777
+ outcome: {
33778
+ result: "session_reset_awaiting_user",
33779
+ dataLossSuspected: failSummary.pendingCount > 0
33780
+ }
33781
+ });
33782
+ }
33066
33783
  async recoverFromRestart(agents) {
33067
33784
  const lockSnapshot = readCronLockSnapshot();
33068
33785
  logger13.info("Recovering Agent sessions after restart", {
@@ -33185,58 +33902,7 @@ ${lines.join("\n")}`;
33185
33902
  this.lastUsedAt.delete(key);
33186
33903
  const errorText = isResumeFail ? `\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\uFF08${errMsg}\uFF09` : `Agent query crashed: ${errMsg}`;
33187
33904
  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;
33188
- if (runtime.currentTask) {
33189
- this.emit({
33190
- type: "agent:error",
33191
- payload: {
33192
- agentId: runtime.agentId,
33193
- conversationId: runtime.currentTask.conversationId,
33194
- ackId: runtime.currentTask.replyMessageId,
33195
- traceId: runtime.currentTask.traceId,
33196
- error: emittedErrorText
33197
- }
33198
- });
33199
- runtime.currentTask = null;
33200
- }
33201
- for (const task of runtime.injectedTasks) {
33202
- this.emit({
33203
- type: "agent:error",
33204
- payload: {
33205
- agentId: runtime.agentId,
33206
- conversationId: task.conversationId,
33207
- ackId: task.replyMessageId,
33208
- traceId: task.traceId,
33209
- error: emittedErrorText
33210
- }
33211
- });
33212
- }
33213
- runtime.injectedTasks = [];
33214
- for (const task of runtime.mergedTasks) {
33215
- this.emit({
33216
- type: "agent:error",
33217
- payload: {
33218
- agentId: runtime.agentId,
33219
- conversationId: task.conversationId,
33220
- ackId: task.replyMessageId,
33221
- traceId: task.traceId,
33222
- error: emittedErrorText
33223
- }
33224
- });
33225
- }
33226
- runtime.mergedTasks = [];
33227
- for (const task of runtime.planModeBuffer) {
33228
- this.emit({
33229
- type: "agent:error",
33230
- payload: {
33231
- agentId: runtime.agentId,
33232
- conversationId: task.conversationId,
33233
- ackId: task.replyMessageId,
33234
- traceId: task.traceId,
33235
- error: emittedErrorText
33236
- }
33237
- });
33238
- }
33239
- runtime.planModeBuffer = [];
33905
+ this.failPendingTasksWithError(runtime, emittedErrorText);
33240
33906
  }
33241
33907
  }
33242
33908
  getStatus(agentId, scope = { kind: "single" }) {
@@ -33250,6 +33916,18 @@ ${lines.join("\n")}`;
33250
33916
  }
33251
33917
  return [...ids];
33252
33918
  }
33919
+ /** Unified signal: is the turn legitimately waiting on a live external call that emits no
33920
+ * parent heartbeat? Used by BOTH the reply-stall fast path and the zombie watchdog so neither
33921
+ * tears down a turn that is merely slow. Returns the reason for diagnostics, or null when idle.
33922
+ * - open_tool: regular tool, MCP tool, AskUserQuestion wait, or ExitPlanMode (tool_use open).
33923
+ * - subagent: Task/Agent in flight (its inner tool_results clear activeToolUseStartedAt).
33924
+ * - compact: bridge-injected /compact running. */
33925
+ busyReason(proc) {
33926
+ if (proc.activeToolUseStartedAt != null || this.latestOpenToolUse(proc) != null) return "open_tool";
33927
+ if ((proc.activeSubagentTaskIds?.size ?? 0) > 0) return "subagent";
33928
+ if (proc.compactInProgress === true) return "compact";
33929
+ return null;
33930
+ }
33253
33931
  latestOpenToolUse(proc) {
33254
33932
  for (let i = proc.contentBlocks.length - 1; i >= 0; i -= 1) {
33255
33933
  const block = proc.contentBlocks[i];
@@ -33398,7 +34076,7 @@ ${lines.join("\n")}`;
33398
34076
  }
33399
34077
  const task = {
33400
34078
  content: notice,
33401
- replyMessageId: createMessageId(),
34079
+ replyMessageId: createScopeNoticeReplyMessageId(),
33402
34080
  conversationId,
33403
34081
  traceId: createTraceId(),
33404
34082
  groupId: proc.scope.kind === "group" ? proc.scope.groupId : void 0
@@ -34480,6 +35158,7 @@ var HttpMcpRegistry = class {
34480
35158
  buildForAgent(ctx) {
34481
35159
  const mcpServers = {};
34482
35160
  const allowedTools = [];
35161
+ const toolAbi = [];
34483
35162
  const usedNames = /* @__PURE__ */ new Set();
34484
35163
  for (const connection of this.allConnections()) {
34485
35164
  if (!this.connectionAppliesToAgent(connection, ctx)) continue;
@@ -34488,12 +35167,18 @@ var HttpMcpRegistry = class {
34488
35167
  const serverName = uniqueServerName(normalizeMcpServerName(connection.serverName), usedNames);
34489
35168
  usedNames.add(serverName);
34490
35169
  mcpServers[serverName] = sdkConfig;
34491
- for (const tool2 of connection.tools) {
34492
- if (!tool2.enabled || tool2.permissionPolicy !== "always_allow") continue;
34493
- allowedTools.push(mcpRuntimeToolName(serverName, tool2.name));
34494
- }
35170
+ const visibleTools = connection.tools.filter((tool2) => tool2.enabled && tool2.permissionPolicy !== "always_deny");
35171
+ for (const tool2 of visibleTools) allowedTools.push(mcpRuntimeToolName(serverName, tool2.name));
35172
+ toolAbi.push({
35173
+ serverName,
35174
+ providerId: connection.providerId,
35175
+ transport: connection.transport,
35176
+ alwaysLoad: connection.alwaysLoad,
35177
+ isBuiltin: connection.isBuiltin,
35178
+ tools: visibleTools.map((tool2) => runtimeToolAbi(serverName, tool2)).sort((a, b) => a.runtimeToolName.localeCompare(b.runtimeToolName))
35179
+ });
34495
35180
  }
34496
- return { mcpServers, allowedTools };
35181
+ return { mcpServers, allowedTools, toolAbi };
34497
35182
  }
34498
35183
  allConnections() {
34499
35184
  return [...this.serverConnections.values(), ...this.localConnections.values()];
@@ -34679,6 +35364,18 @@ function uniqueServerName(serverName, usedNames) {
34679
35364
  while (usedNames.has(`${serverName}_${idx}`)) idx += 1;
34680
35365
  return `${serverName}_${idx}`;
34681
35366
  }
35367
+ function runtimeToolAbi(serverName, tool2) {
35368
+ return {
35369
+ name: tool2.name,
35370
+ runtimeToolName: mcpRuntimeToolName(serverName, tool2.name),
35371
+ displayName: tool2.displayName,
35372
+ description: tool2.description,
35373
+ category: tool2.category,
35374
+ riskLevel: tool2.riskLevel,
35375
+ permissionPolicy: tool2.permissionPolicy,
35376
+ ...tool2.inputSchema !== void 0 ? { inputSchema: tool2.inputSchema } : {}
35377
+ };
35378
+ }
34682
35379
  function buildHeaders(authType, authSecret, customHeaders) {
34683
35380
  const headers = {};
34684
35381
  for (const header of customHeaders) {
@@ -35330,6 +36027,7 @@ var ServerConnector = class {
35330
36027
  case "agent:terminate":
35331
36028
  case "agent:runtime_reload":
35332
36029
  case "agent:terminate_scope":
36030
+ case "spectate:set":
35333
36031
  case "agent:created":
35334
36032
  case "agent:updated":
35335
36033
  case "agent:workdir-updated":
@@ -36305,24 +37003,6 @@ function normalizeLocalPath(targetPath) {
36305
37003
  const expanded = trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\") ? path18.join(os10.homedir(), trimmed.slice(2)) : trimmed;
36306
37004
  return path18.normalize(path18.resolve(expanded));
36307
37005
  }
36308
- function isAbsoluteLocalPathCandidate(value) {
36309
- if (process.platform === "win32") {
36310
- return /^[a-zA-Z]:[\\/]/.test(value) || /^\\\\[^\\]/.test(value);
36311
- }
36312
- return value.startsWith("/");
36313
- }
36314
- function clipboardTextToPathCandidates(value) {
36315
- return value.replace(/\0/g, "\n").split(/\r?\n/).map((line) => line.trim().replace(/^"(.+)"$/, "$1")).map((line) => {
36316
- if (!line.toLowerCase().startsWith("file://")) return line;
36317
- try {
36318
- const url2 = new URL(line);
36319
- return decodeURIComponent(url2.pathname.replace(/^\/([a-zA-Z]:\/)/, "$1"));
36320
- } catch (e) {
36321
- logger24.debug("Failed to parse clipboard file URL", { error: e });
36322
- return "";
36323
- }
36324
- }).filter((line) => line.length > 0 && isAbsoluteLocalPathCandidate(line));
36325
- }
36326
37006
  function normalizeClipboardIdentityKey(value) {
36327
37007
  return process.platform === "win32" ? value.toLowerCase() : value;
36328
37008
  }
@@ -36360,18 +37040,15 @@ function mimeTypeForFileName(fileName) {
36360
37040
  }
36361
37041
  function parseWindowsClipboardResult(stdout) {
36362
37042
  const raw = stdout.trim();
36363
- if (!raw) return { files: [], text: "" };
37043
+ if (!raw) return { files: [] };
36364
37044
  try {
36365
37045
  const parsed = JSON.parse(raw);
36366
- if (!isRecord5(parsed)) return { files: [], text: "" };
37046
+ if (!isRecord5(parsed)) return { files: [] };
36367
37047
  const files = Array.isArray(parsed.files) ? parsed.files.filter((item) => typeof item === "string") : [];
36368
- return {
36369
- files,
36370
- text: typeof parsed.text === "string" ? parsed.text : ""
36371
- };
37048
+ return { files };
36372
37049
  } catch (e) {
36373
37050
  logger24.debug("Windows clipboard JSON parse skipped", { error: e });
36374
- return { files: clipboardTextToPathCandidates(stdout), text: "" };
37051
+ return { files: [] };
36375
37052
  }
36376
37053
  }
36377
37054
  async function readWindowsClipboardPathCandidates() {
@@ -36384,9 +37061,7 @@ async function readWindowsClipboardPathCandidates() {
36384
37061
  " $drop = [System.Windows.Forms.Clipboard]::GetFileDropList();",
36385
37062
  " foreach ($file in $drop) { $files += [string]$file }",
36386
37063
  "} catch {}",
36387
- '$text = "";',
36388
- "try { $text = [System.Windows.Forms.Clipboard]::GetText() } catch {}",
36389
- "[pscustomobject]@{ files = $files; text = $text } | ConvertTo-Json -Compress;"
37064
+ "[pscustomobject]@{ files = $files } | ConvertTo-Json -Compress;"
36390
37065
  ].join(" ");
36391
37066
  try {
36392
37067
  const { stdout } = await execFileAsync2("powershell.exe", [
@@ -36404,7 +37079,7 @@ async function readWindowsClipboardPathCandidates() {
36404
37079
  maxBuffer: 1024 * 1024
36405
37080
  });
36406
37081
  const result = parseWindowsClipboardResult(stdout);
36407
- return [...result.files, ...clipboardTextToPathCandidates(result.text)];
37082
+ return result.files;
36408
37083
  } catch (e) {
36409
37084
  logger24.debug("Windows clipboard file read skipped", { error: e });
36410
37085
  return [];
@@ -36807,7 +37482,7 @@ async function readStreamText(filePath, start) {
36807
37482
  function parseProcessedLines(raw, cursor, fileName, fingerprint) {
36808
37483
  const lastNewline = raw.lastIndexOf("\n");
36809
37484
  if (lastNewline < 0) {
36810
- return { entries: [], nextCursor: cursor, advanced: false };
37485
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "partial_line" };
36811
37486
  }
36812
37487
  const processed = raw.slice(0, lastNewline + 1);
36813
37488
  const lines = processed.split(/\r?\n/);
@@ -36833,7 +37508,8 @@ function parseProcessedLines(raw, cursor, fileName, fingerprint) {
36833
37508
  lineNum,
36834
37509
  ...fingerprint ? { fingerprint } : {}
36835
37510
  },
36836
- advanced: true
37511
+ advanced: true,
37512
+ reason: "advanced"
36837
37513
  };
36838
37514
  }
36839
37515
  function chunkEntries(entries, size) {
@@ -36891,24 +37567,55 @@ var BridgeLogUploader = class {
36891
37567
  async flushOnce() {
36892
37568
  if (this.running || this.stopped) return;
36893
37569
  this.running = true;
37570
+ const startedAt = Date.now();
37571
+ const summary = {
37572
+ targetCount: 0,
37573
+ advancedTargetCount: 0,
37574
+ missingTargetCount: 0,
37575
+ idleTargetCount: 0,
37576
+ partialLineTargetCount: 0,
37577
+ failedTargetCount: 0,
37578
+ parsedEntryCount: 0,
37579
+ bridgeEntryCount: 0,
37580
+ uploadedChunkCount: 0,
37581
+ accepted: 0,
37582
+ skipped: 0
37583
+ };
36894
37584
  try {
36895
37585
  const targets = await this.resolveTargets();
37586
+ summary.targetCount = targets.length;
36896
37587
  for (const target of targets) {
36897
37588
  try {
36898
37589
  const cursor = await readCursor(target.cursorFile);
36899
37590
  const batch = await this.readNewEntries(target, cursor);
36900
- if (!batch.advanced) continue;
37591
+ if (!batch.advanced) {
37592
+ summary.idleTargetCount += 1;
37593
+ if (batch.reason === "missing_file") summary.missingTargetCount += 1;
37594
+ if (batch.reason === "partial_line") summary.partialLineTargetCount += 1;
37595
+ continue;
37596
+ }
37597
+ summary.advancedTargetCount += 1;
37598
+ summary.parsedEntryCount += batch.entries.length;
36901
37599
  if (batch.entries.length > 0) {
36902
- await this.uploadEntries(batch.entries);
37600
+ const result = await this.uploadEntries(batch.entries);
37601
+ summary.bridgeEntryCount += result.bridgeEntryCount;
37602
+ summary.uploadedChunkCount += result.uploadedChunkCount;
37603
+ summary.accepted += result.accepted;
37604
+ summary.skipped += result.skipped;
36903
37605
  }
36904
37606
  await writeCursor(target.cursorFile, batch.nextCursor);
36905
37607
  } catch (e) {
37608
+ summary.failedTargetCount += 1;
36906
37609
  logger27.warn("Bridge log upload target failed", { error: e, logFile: target.logFile });
36907
37610
  }
36908
37611
  }
36909
37612
  } catch (e) {
36910
37613
  logger27.warn("Bridge log upload cycle failed", { error: e });
36911
37614
  } finally {
37615
+ logger27.info("Bridge log upload cycle summary", {
37616
+ ...summary,
37617
+ durationMs: Date.now() - startedAt
37618
+ });
36912
37619
  this.running = false;
36913
37620
  }
36914
37621
  }
@@ -36931,7 +37638,7 @@ var BridgeLogUploader = class {
36931
37638
  } catch (e) {
36932
37639
  if (e instanceof Error && "code" in e && e.code === "ENOENT") {
36933
37640
  logger27.debug("Bridge log file not found for upload yet", { logFile: target.logFile });
36934
- return { entries: [], nextCursor: cursor, advanced: false };
37641
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "missing_file" };
36935
37642
  }
36936
37643
  throw e;
36937
37644
  }
@@ -36939,13 +37646,19 @@ var BridgeLogUploader = class {
36939
37646
  const samePhysicalFile = !fingerprint || !cursor.fingerprint || cursor.fingerprint === fingerprint;
36940
37647
  const normalizedCursor = stat3.size < cursor.offset || !samePhysicalFile ? { offset: 0, lineNum: 0, ...fingerprint ? { fingerprint } : {} } : { ...cursor, ...fingerprint ? { fingerprint } : {} };
36941
37648
  if (stat3.size <= normalizedCursor.offset) {
36942
- return { entries: [], nextCursor: normalizedCursor, advanced: false };
37649
+ return { entries: [], nextCursor: normalizedCursor, advanced: false, reason: "no_new_entries" };
36943
37650
  }
36944
37651
  const raw = await readStreamText(target.logFile, normalizedCursor.offset);
36945
37652
  return parseProcessedLines(raw, normalizedCursor, target.uploadedFileName, fingerprint);
36946
37653
  }
36947
37654
  async uploadEntries(entries) {
36948
37655
  const bridgeEntries = entries.filter((entry) => entry.source === "bridge");
37656
+ const result = {
37657
+ bridgeEntryCount: bridgeEntries.length,
37658
+ uploadedChunkCount: 0,
37659
+ accepted: 0,
37660
+ skipped: 0
37661
+ };
36949
37662
  for (const chunk of chunkEntries(bridgeEntries, this.options.batchSize)) {
36950
37663
  const res = await fetch(`${this.options.serverApiUrl}/api/logs/upload`, {
36951
37664
  method: "POST",
@@ -36960,14 +37673,18 @@ var BridgeLogUploader = class {
36960
37673
  })
36961
37674
  });
36962
37675
  if (!res.ok) {
36963
- const body = await res.text().catch((e) => {
37676
+ const body2 = await res.text().catch((e) => {
36964
37677
  logger27.debug("Failed to read log upload error body", { error: e });
36965
37678
  return "";
36966
37679
  });
36967
- throw new Error(`upload failed HTTP ${res.status}: ${body.slice(0, 160)}`);
37680
+ throw new Error(`upload failed HTTP ${res.status}: ${body2.slice(0, 160)}`);
36968
37681
  }
36969
- await res.json();
37682
+ const body = await res.json();
37683
+ result.uploadedChunkCount += 1;
37684
+ result.accepted += typeof body.accepted === "number" ? body.accepted : 0;
37685
+ result.skipped += typeof body.skipped === "number" ? body.skipped : 0;
36970
37686
  }
37687
+ return result;
36971
37688
  }
36972
37689
  };
36973
37690
 
@@ -38052,8 +38769,10 @@ function resolvePnpmRuntimeBinary(sdkDir, target) {
38052
38769
  } catch {
38053
38770
  return void 0;
38054
38771
  }
38772
+ const matches = [];
38055
38773
  for (const entry of entries) {
38056
38774
  if (!entry.startsWith(`${encodedName}@`)) continue;
38775
+ const version2 = entry.slice(encodedName.length + 1);
38057
38776
  const candidate = path27.join(
38058
38777
  pnpmStoreDir,
38059
38778
  entry,
@@ -38061,9 +38780,22 @@ function resolvePnpmRuntimeBinary(sdkDir, target) {
38061
38780
  ...target.packageName.split("/"),
38062
38781
  target.binaryName
38063
38782
  );
38064
- if (existsSync2(candidate)) return candidate;
38783
+ if (existsSync2(candidate)) matches.push({ version: version2, candidate });
38065
38784
  }
38066
- return void 0;
38785
+ if (matches.length === 0) return void 0;
38786
+ matches.sort((a, b) => compareRuntimeVersion(b.version, a.version));
38787
+ return matches[0].candidate;
38788
+ }
38789
+ function compareRuntimeVersion(a, b) {
38790
+ const parse3 = (v) => v.split(/[.+_-]/).map((n) => Number.parseInt(n, 10));
38791
+ const pa = parse3(a);
38792
+ const pb = parse3(b);
38793
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
38794
+ const da = Number.isFinite(pa[i]) ? pa[i] : 0;
38795
+ const db = Number.isFinite(pb[i]) ? pb[i] : 0;
38796
+ if (da !== db) return da - db;
38797
+ }
38798
+ return 0;
38067
38799
  }
38068
38800
  function resolveSdkRuntimeBinary(target) {
38069
38801
  const directPath = safeResolve(`${target.packageName}/${target.binaryName}`);
@@ -39222,6 +39954,32 @@ function syncLocalRuntimeSkills(skillStore, localSkills, options = {}) {
39222
39954
  ]);
39223
39955
  }
39224
39956
 
39957
+ // src/processOutput.ts
39958
+ var protectedStreams2 = /* @__PURE__ */ new WeakSet();
39959
+ var reportedErrors = /* @__PURE__ */ new WeakSet();
39960
+ function reportWriteError(error51, onError) {
39961
+ if (typeof error51 === "object" && error51 !== null) {
39962
+ if (reportedErrors.has(error51)) return;
39963
+ reportedErrors.add(error51);
39964
+ }
39965
+ onError(error51);
39966
+ }
39967
+ function safeWriteProcessOutput(stream, text, onError) {
39968
+ if (!stream) return;
39969
+ if (stream.destroyed || stream.writableEnded) return;
39970
+ if (typeof stream === "object" && typeof stream.on === "function" && !protectedStreams2.has(stream)) {
39971
+ protectedStreams2.add(stream);
39972
+ stream.on("error", (e) => reportWriteError(e, onError));
39973
+ }
39974
+ try {
39975
+ stream.write(text, (error51) => {
39976
+ if (error51) reportWriteError(error51, onError);
39977
+ });
39978
+ } catch (e) {
39979
+ reportWriteError(e, onError);
39980
+ }
39981
+ }
39982
+
39225
39983
  // src/start.ts
39226
39984
  var logger41 = createModuleLogger("bridge");
39227
39985
  var NODE_USER_UID2 = 1e3;
@@ -39308,14 +40066,16 @@ async function startBridge(config2) {
39308
40066
  const claudeRuntime = resolveClaudeRuntime();
39309
40067
  logClaudeRuntimeResolution(claudeRuntime);
39310
40068
  if (!claudeRuntime.ok || !claudeRuntime.path) {
39311
- process.stderr.write(
40069
+ safeWriteProcessOutput(
40070
+ process.stderr,
39312
40071
  `
39313
40072
  Claude runtime is unavailable.
39314
40073
 
39315
40074
  ${claudeRuntime.error ?? "Install Claude Code manually or use the bundled desktop runtime."}
39316
40075
 
39317
40076
  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.
39318
- `
40077
+ `,
40078
+ (e) => logger41.error("Bridge process stderr write failed", { error: e })
39319
40079
  );
39320
40080
  process.exit(1);
39321
40081
  }
@@ -39372,12 +40132,14 @@ Reinstall @fangyb/ahchat-bridge with npm optional dependencies, set AHCHAT_CLAUD
39372
40132
  claudeRuntimeVersion: claudeRuntime.version ?? null
39373
40133
  });
39374
40134
  const shouldPrintRawBridgeToken = process.stdout.isTTY && process.env.AHCHAT_SUPPRESS_BRIDGE_TOKEN_STDOUT !== "1";
39375
- process.stdout.write(
40135
+ safeWriteProcessOutput(
40136
+ process.stdout,
39376
40137
  `
39377
40138
  Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\u673A\u5668):
39378
40139
  ${shouldPrintRawBridgeToken ? config2.bridgeToken : "***"}
39379
40140
 
39380
- `
40141
+ `,
40142
+ (e) => logger41.error("Bridge process stdout write failed", { error: e })
39381
40143
  );
39382
40144
  wsMetrics.start(5e3);
39383
40145
  const sessionStore = new SessionStore(config2.dataDir);
@@ -40176,6 +40938,19 @@ Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\
40176
40938
  });
40177
40939
  await agentManager.terminateScope(msg.payload.agentId, msg.payload.scope);
40178
40940
  break;
40941
+ case "spectate:set":
40942
+ logger41.info("spectate:set received", {
40943
+ agentId: msg.payload.agentId,
40944
+ scope: msg.payload.scope,
40945
+ action: msg.payload.action,
40946
+ traceId: msg.payload.traceId
40947
+ });
40948
+ await agentManager.setSpectate(
40949
+ msg.payload.agentId,
40950
+ msg.payload.scope,
40951
+ msg.payload.action
40952
+ );
40953
+ break;
40179
40954
  case "agent:created":
40180
40955
  agentRegistry.upsert(msg.payload.agent);
40181
40956
  ensureLocalWorkdirPath(msg.payload.agent.workingDirectory, "agent:created", {
@@ -40392,8 +41167,12 @@ function writeAlreadyRunningMessage(error51) {
40392
41167
  ` ${buildStopCommand(error51.pid)}`,
40393
41168
  ""
40394
41169
  ];
40395
- process.stdout.write(`${lines.join("\n")}
40396
- `);
41170
+ safeWriteProcessOutput(
41171
+ process.stdout,
41172
+ `${lines.join("\n")}
41173
+ `,
41174
+ (e) => logger42.error("Bridge already-running message write failed", { error: e })
41175
+ );
40397
41176
  }
40398
41177
  function handleBridgeStartError(e, message) {
40399
41178
  if (isBridgeAlreadyRunningError(e)) {