@fangyb/ahchat-bridge 0.1.35 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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."),
@@ -28230,6 +28427,9 @@ function emitUsageReported(proc, emit, base, usage, messageId) {
28230
28427
  function isGroupTask(proc) {
28231
28428
  return proc.currentTask?.groupId != null;
28232
28429
  }
28430
+ function shouldStreamInternals(proc) {
28431
+ return !isGroupTask(proc) || proc.spectating === true;
28432
+ }
28233
28433
  function extractTodosFromInput(input) {
28234
28434
  if (!input || typeof input !== "object") return null;
28235
28435
  const raw = input.todos;
@@ -28361,7 +28561,6 @@ function emitGroupSegment(proc, emit, base, content, contentBlocks, isSilent = f
28361
28561
  contentLen: content.length,
28362
28562
  blockCount: contentBlocks.length,
28363
28563
  blockTypes: contentBlocks.map((b) => b.type),
28364
- contentSample: content.slice(0, 200),
28365
28564
  traceId: base.traceId,
28366
28565
  isAuditOnly: content.length === 0,
28367
28566
  isSilent
@@ -28417,9 +28616,24 @@ function flushTextSegmentOnBlockStop(proc, emit, base) {
28417
28616
  }
28418
28617
  proc.segmentBuffer = "";
28419
28618
  }
28619
+ function describeSdkEvent(message) {
28620
+ const rec = message;
28621
+ const str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
28622
+ return {
28623
+ type: str(rec.type) ?? "unknown",
28624
+ subtype: str(rec.subtype),
28625
+ toolName: str(rec.last_tool_name),
28626
+ subagentType: str(rec.subagent_type),
28627
+ toolUseId: str(rec.tool_use_id),
28628
+ taskId: str(rec.task_id),
28629
+ at: Date.now()
28630
+ };
28631
+ }
28420
28632
  function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProviderApiError) {
28421
28633
  const emit = rawEmit;
28422
28634
  proc.lastSdkEventAt = Date.now();
28635
+ proc.lastSdkEventInfo = describeSdkEvent(message);
28636
+ proc.stallWarned = false;
28423
28637
  switch (message.type) {
28424
28638
  case "system": {
28425
28639
  const sysMsg = message;
@@ -28458,11 +28672,29 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28458
28672
  sessionId: proc.ccSessionId
28459
28673
  });
28460
28674
  } else {
28675
+ const sysRec = sysMsg;
28676
+ const pick2 = (k) => typeof sysRec[k] === "string" || typeof sysRec[k] === "number" ? sysRec[k] : void 0;
28677
+ const descriptionLen = typeof sysRec.description === "string" ? sysRec.description.length : void 0;
28678
+ const subagentTaskId = typeof sysRec.task_id === "string" ? sysRec.task_id : void 0;
28679
+ if (subagentTaskId) {
28680
+ if (sysMsg.subtype === "task_started") {
28681
+ (proc.activeSubagentTaskIds ??= /* @__PURE__ */ new Set()).add(subagentTaskId);
28682
+ } else if (sysMsg.subtype === "task_notification") {
28683
+ proc.activeSubagentTaskIds?.delete(subagentTaskId);
28684
+ }
28685
+ }
28461
28686
  logger10.info("SDK system subtype unhandled", {
28462
28687
  agentId: proc.agentId,
28463
28688
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
28464
28689
  subtype: sysMsg.subtype ?? "(none)",
28465
- keys: Object.keys(sysMsg).slice(0, 12)
28690
+ taskId: pick2("task_id"),
28691
+ toolUseId: pick2("tool_use_id"),
28692
+ subagentType: pick2("subagent_type"),
28693
+ taskType: pick2("task_type"),
28694
+ lastToolName: pick2("last_tool_name"),
28695
+ hasDescription: descriptionLen != null,
28696
+ descriptionLen,
28697
+ keys: Object.keys(sysMsg).slice(0, 16)
28466
28698
  });
28467
28699
  }
28468
28700
  break;
@@ -28488,13 +28720,14 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28488
28720
  } else if (block.type === "tool_use") {
28489
28721
  proc.currentBlockType = "tool_use";
28490
28722
  proc.currentToolName = block.name ?? "unknown";
28723
+ proc.activeToolUseStartedAt = Date.now();
28491
28724
  proc.accumulatedToolInput = "";
28492
28725
  const toolName = block.name ?? "unknown";
28493
28726
  proc.suppressCurrentToolUse = proc.officialMediaGenerationSatisfied === true && isOfficialMediaGenerationToolName(toolName);
28494
28727
  const isMcpTool = parseMcpRuntimeToolName(toolName) != null;
28495
28728
  proc.currentMcpInvocationId = isMcpTool ? createMcpToolInvocationId() : null;
28496
28729
  proc.currentMcpInvocationStartedAt = isMcpTool ? (/* @__PURE__ */ new Date()).toISOString() : null;
28497
- if (!isGroupTask(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
28730
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
28498
28731
  emit({
28499
28732
  type: "agent:tool_use",
28500
28733
  payload: {
@@ -28519,7 +28752,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28519
28752
  if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
28520
28753
  if (proc.suppressCurrentThinking) break;
28521
28754
  proc.accumulatedThinking += delta.thinking;
28522
- if (!isGroupTask(proc)) {
28755
+ if (shouldStreamInternals(proc)) {
28523
28756
  emit({
28524
28757
  type: "agent:thinking_chunk",
28525
28758
  payload: { ...wireBase(base), chunk: delta.thinking }
@@ -28530,7 +28763,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28530
28763
  if (typeof partial2 === "string") {
28531
28764
  proc.accumulatedToolInput += partial2;
28532
28765
  const liveInput = extractLiveToolInput(proc.currentToolName, proc.accumulatedToolInput);
28533
- if (!isGroupTask(proc) && liveInput && proc.currentToolName != null) {
28766
+ if (shouldStreamInternals(proc) && liveInput && proc.currentToolName != null) {
28534
28767
  emit({
28535
28768
  type: "agent:tool_input_update",
28536
28769
  payload: {
@@ -28564,7 +28797,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28564
28797
  }
28565
28798
  case "content_block_stop": {
28566
28799
  if (proc.currentBlockType === "thinking") {
28567
- if (!isGroupTask(proc) && !proc.suppressCurrentThinking) {
28800
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentThinking) {
28568
28801
  emit({
28569
28802
  type: "agent:thinking_done",
28570
28803
  payload: wireBase(getTaskBase(proc))
@@ -28589,8 +28822,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28589
28822
  error: error51,
28590
28823
  agentId: proc.agentId,
28591
28824
  toolName: proc.currentToolName,
28592
- inputLen: proc.accumulatedToolInput.length,
28593
- sample: proc.accumulatedToolInput.slice(0, 200)
28825
+ inputLen: proc.accumulatedToolInput.length
28594
28826
  });
28595
28827
  }
28596
28828
  }
@@ -28599,7 +28831,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28599
28831
  if (lastToolUse && lastToolUse.type === "tool_use") {
28600
28832
  lastToolUse.input = parsedInput;
28601
28833
  }
28602
- if (!isGroupTask(proc) && proc.currentToolName != null && LIVE_INPUT_PREVIEW_TOOLS.has(proc.currentToolName) && Object.keys(parsedInput).length > 0) {
28834
+ if (shouldStreamInternals(proc) && proc.currentToolName != null && LIVE_INPUT_PREVIEW_TOOLS.has(proc.currentToolName) && Object.keys(parsedInput).length > 0) {
28603
28835
  emit({
28604
28836
  type: "agent:tool_input_update",
28605
28837
  payload: {
@@ -28723,7 +28955,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28723
28955
  blockTypes,
28724
28956
  hasToolResult,
28725
28957
  hasPlainText,
28726
- contentSample: typeof content === "string" ? content.slice(0, 200) : JSON.stringify(content).slice(0, 200)
28958
+ contentLen: typeof content === "string" ? content.length : JSON.stringify(content).length
28727
28959
  });
28728
28960
  break;
28729
28961
  }
@@ -28732,7 +28964,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28732
28964
  agentId: proc.agentId,
28733
28965
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
28734
28966
  blockTypes,
28735
- contentSample: JSON.stringify(content).slice(0, 300),
28967
+ contentLen: JSON.stringify(content).length,
28736
28968
  replyMessageId: base.replyMessageId
28737
28969
  });
28738
28970
  }
@@ -28753,6 +28985,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28753
28985
  });
28754
28986
  proc.currentMcpInvocationId = null;
28755
28987
  proc.currentMcpInvocationStartedAt = null;
28988
+ proc.activeToolUseStartedAt = void 0;
28756
28989
  proc.currentToolName = null;
28757
28990
  continue;
28758
28991
  }
@@ -28760,6 +28993,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28760
28993
  proc.officialMediaGenerationSatisfied = true;
28761
28994
  }
28762
28995
  if (isAskUserQuestionToolName(toolName)) {
28996
+ proc.activeToolUseStartedAt = void 0;
28763
28997
  proc.currentToolName = null;
28764
28998
  continue;
28765
28999
  }
@@ -28787,7 +29021,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28787
29021
  proc.currentMcpInvocationId = null;
28788
29022
  proc.currentMcpInvocationStartedAt = null;
28789
29023
  }
28790
- if (!isGroupTask(proc)) {
29024
+ if (shouldStreamInternals(proc)) {
28791
29025
  emit({
28792
29026
  type: "agent:tool_result",
28793
29027
  payload: {
@@ -28811,6 +29045,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28811
29045
  }
28812
29046
  }
28813
29047
  }
29048
+ proc.activeToolUseStartedAt = void 0;
28814
29049
  }
28815
29050
  }
28816
29051
  }
@@ -28889,7 +29124,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28889
29124
  groupId,
28890
29125
  compactScheduled: proc.compactRequested === true,
28891
29126
  fullTextLen: proc.accumulatedText.length,
28892
- fullTextSample: proc.accumulatedText.slice(0, 200),
28893
29127
  accumulatedBlockCount: proc.contentBlocks.length,
28894
29128
  accumulatedBlockTypes: proc.contentBlocks.map((b) => b.type),
28895
29129
  silentSegmentEmitted: groupMode
@@ -28933,7 +29167,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28933
29167
  segmentCount: proc.segmentCount,
28934
29168
  compactScheduled: proc.compactRequested === true,
28935
29169
  fullTextLen: proc.accumulatedText.length,
28936
- fullTextSample: proc.accumulatedText.slice(0, 200),
28937
29170
  traceId: base.traceId
28938
29171
  });
28939
29172
  emit({
@@ -28984,7 +29217,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28984
29217
  ackId: base.replyMessageId,
28985
29218
  messageId: carrierMessageId,
28986
29219
  textLen: proc.accumulatedText.length,
28987
- textSample: proc.accumulatedText.slice(0, 200),
28988
29220
  tokenCount: usage.tokenCount,
28989
29221
  traceId: base.traceId
28990
29222
  });
@@ -29177,8 +29409,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
29177
29409
  logger10.info("Captured non-streamed assistant message", {
29178
29410
  agentId: proc.agentId,
29179
29411
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
29180
- textLen: text.length,
29181
- textSample: text.slice(0, 100)
29412
+ textLen: text.length
29182
29413
  });
29183
29414
  } else {
29184
29415
  proc.lastAssistantContentDescription = describeAssistantContent(am.message?.content);
@@ -29202,6 +29433,7 @@ function resetAccumulators(proc) {
29202
29433
  proc.currentToolName = null;
29203
29434
  proc.currentMcpInvocationId = null;
29204
29435
  proc.currentMcpInvocationStartedAt = null;
29436
+ proc.activeToolUseStartedAt = void 0;
29205
29437
  proc.segmentBuffer = "";
29206
29438
  proc.segmentCount = 0;
29207
29439
  proc.accumulatedToolInput = "";
@@ -29211,6 +29443,7 @@ function resetAccumulators(proc) {
29211
29443
  proc.officialMediaGenerationSatisfied = false;
29212
29444
  proc.suppressCurrentThinking = false;
29213
29445
  proc.suppressCurrentToolUse = false;
29446
+ proc.activeSubagentTaskIds?.clear();
29214
29447
  }
29215
29448
 
29216
29449
  // src/forkHistoryReplay.ts
@@ -29398,7 +29631,7 @@ function missingSubscriptionMessage(subscriptionId) {
29398
29631
  }
29399
29632
  var NODE_USER_UID = 1e3;
29400
29633
  var POST_MERGE_CONTINUATION_ROUTE_MS = 15e3;
29401
- var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-prompt-v2";
29634
+ var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-mcp-abi-prompt-v4";
29402
29635
  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
29636
  var DOCUMENT_READING_RULES = `DOCUMENT READING:
29404
29637
  - The built-in Read tool cannot read binary office documents such as .docx, .xls, .xlsx, .pptx, .pdf, .odt, .ods, .odp, or .rtf.
@@ -29413,6 +29646,16 @@ var MEDIA_GENERATION_RULES = `MEDIA GENERATION:
29413
29646
  - 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
29647
  - 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
29648
  - 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.`;
29649
+ function stableFingerprintValue(value) {
29650
+ if (Array.isArray(value)) return value.map(stableFingerprintValue);
29651
+ if (!value || typeof value !== "object") return value;
29652
+ const out = {};
29653
+ for (const key of Object.keys(value).sort()) {
29654
+ const normalized = stableFingerprintValue(value[key]);
29655
+ if (normalized !== void 0) out[key] = normalized;
29656
+ }
29657
+ return out;
29658
+ }
29416
29659
  function isRecoveryDispatchTask(task) {
29417
29660
  return task.dispatchKind === "manual_continue" || task.dispatchKind === "regenerate";
29418
29661
  }
@@ -29670,13 +29913,15 @@ var AgentManager = class {
29670
29913
  agents = /* @__PURE__ */ new Map();
29671
29914
  lastUsedAt = /* @__PURE__ */ new Map();
29672
29915
  /** Scopes 被 zombie_watchdog 关闭后的"入睡"标记,acquire 重建时清除并 emit awake。 */
29673
- dormantScopes = /* @__PURE__ */ new Set();
29916
+ dormantScopes = /* @__PURE__ */ new Map();
29674
29917
  /**
29675
29918
  * zombie_watchdog 拆 runtime 时,把该 (agentId, scope) 的 groupInbox 快照到这里,
29676
29919
  * 让下一次 getOrCreate 重建 runtime 时可以恢复未读消息。仅 in-memory;
29677
29920
  * bridge 进程崩溃 / shutdownAll 时丢失,与现有 inbox 内存语义一致。
29678
29921
  */
29679
29922
  dormantGroupInboxes = /* @__PURE__ */ new Map();
29923
+ /** Spectate requested before runtime existed; value = activatedAt epoch ms. */
29924
+ pendingSpectate = /* @__PURE__ */ new Map();
29680
29925
  sessionStore;
29681
29926
  dispatchMemory = new GroupDispatchMemoryStore();
29682
29927
  dataDir;
@@ -29842,6 +30087,7 @@ var AgentManager = class {
29842
30087
  }
29843
30088
  async resolveRuntimeCwd(agentConfig, scope, requestedCwd) {
29844
30089
  let cwd = this.remapServerWorkspaceCwd(agentConfig, scope, requestedCwd);
30090
+ let fallbackForensicsId;
29845
30091
  if (!isFullyQualifiedAbsolutePath(cwd)) {
29846
30092
  const fallback = path13.join(this.workspacesDir, this.localScopeDirName(agentConfig, scope));
29847
30093
  logger13.error(
@@ -29856,6 +30102,23 @@ var AgentManager = class {
29856
30102
  error: new Error("workdir_not_usable_on_this_machine")
29857
30103
  }
29858
30104
  );
30105
+ fallbackForensicsId = createFallbackId();
30106
+ logFallback(logger13, {
30107
+ fallbackId: fallbackForensicsId,
30108
+ type: "cwd_sandbox",
30109
+ phase: "applied",
30110
+ expected: false,
30111
+ context: {
30112
+ agentId: agentConfig.id,
30113
+ scope: scopeKey(scope),
30114
+ platform: process.platform,
30115
+ requested: requestedCwd,
30116
+ resolved: cwd,
30117
+ reason: "not_fully_qualified",
30118
+ fallback
30119
+ },
30120
+ outcome: { result: "sandbox_fallback" }
30121
+ });
29859
30122
  cwd = fallback;
29860
30123
  }
29861
30124
  if (isRunningAsRoot() && cwd.startsWith("/root/")) {
@@ -29874,6 +30137,21 @@ var AgentManager = class {
29874
30137
  fallback,
29875
30138
  error: e
29876
30139
  });
30140
+ const fbId = fallbackForensicsId ?? createFallbackId();
30141
+ logFallback(logger13, {
30142
+ fallbackId: fbId,
30143
+ type: "cwd_sandbox",
30144
+ phase: "applied",
30145
+ expected: false,
30146
+ context: {
30147
+ agentId: agentConfig.id,
30148
+ scope: scopeKey(scope),
30149
+ reason: "mkdir_failed",
30150
+ requested: cwd,
30151
+ fallback
30152
+ },
30153
+ outcome: { result: "second_layer_fallback" }
30154
+ });
29877
30155
  await fs6.mkdir(fallback, { recursive: true });
29878
30156
  return fallback;
29879
30157
  }
@@ -30096,17 +30374,24 @@ var AgentManager = class {
30096
30374
  });
30097
30375
  return null;
30098
30376
  }
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");
30377
+ scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection, externalMcpFingerprint) {
30378
+ 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
30379
  }
30102
- discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint) {
30380
+ externalMcpFingerprint(externalMcp) {
30381
+ const serverNames = Object.keys(externalMcp.mcpServers).sort();
30382
+ const allowedTools = [...externalMcp.allowedTools].sort();
30383
+ const toolAbi = [...externalMcp.toolAbi ?? []].sort((a, b) => a.serverName.localeCompare(b.serverName)).map(stableFingerprintValue);
30384
+ if (serverNames.length === 0 && allowedTools.length === 0 && toolAbi.length === 0) return "";
30385
+ return JSON.stringify({ serverNames, allowedTools, toolAbi });
30386
+ }
30387
+ discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint, options = {}) {
30103
30388
  const previous = this.sessionStore.getPromptFingerprint(agentConfig.id, scope);
30104
30389
  if (!sessionId) {
30105
30390
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
30106
30391
  return null;
30107
30392
  }
30108
30393
  if (previous === fingerprint) return sessionId;
30109
- if (!previous && scope.kind === "single") {
30394
+ if (!previous && scope.kind === "single" && options.clearLegacySingleSession !== true) {
30110
30395
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
30111
30396
  logger13.info("Retaining legacy single-scope session while recording prompt fingerprint", {
30112
30397
  agentId: agentConfig.id,
@@ -30126,7 +30411,8 @@ var AgentManager = class {
30126
30411
  sessionId,
30127
30412
  previousFingerprint: previous,
30128
30413
  nextFingerprint: fingerprint,
30129
- revision: SCOPE_PROMPT_FINGERPRINT_REVISION
30414
+ revision: SCOPE_PROMPT_FINGERPRINT_REVISION,
30415
+ reason: options.reason ?? "scope_prompt_changed"
30130
30416
  });
30131
30417
  return null;
30132
30418
  }
@@ -30175,6 +30461,7 @@ var AgentManager = class {
30175
30461
  logger13.info("Evicting idle Agent query", { agentId: proc.agentId, scope: scopeKey(proc.scope) });
30176
30462
  const runtime = this.asRuntime(proc);
30177
30463
  this.clearQuietFlushTimer(runtime);
30464
+ this.teardownSpectate(runtime);
30178
30465
  try {
30179
30466
  runtime.inputController.close();
30180
30467
  await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
@@ -30194,6 +30481,7 @@ var AgentManager = class {
30194
30481
  const runtime = this.asRuntime(proc);
30195
30482
  const key = runtimeKey(proc.agentId, proc.scope);
30196
30483
  this.clearQuietFlushTimer(runtime);
30484
+ this.teardownSpectate(runtime);
30197
30485
  runtime.currentTask = null;
30198
30486
  runtime.injectedTasks = [];
30199
30487
  runtime.mergedTasks = [];
@@ -30230,6 +30518,7 @@ var AgentManager = class {
30230
30518
  evictIdle() {
30231
30519
  const now = Date.now();
30232
30520
  const { idleTimeoutMs, workingSilenceTimeoutMs } = this.queryConfig;
30521
+ const stallWarnAfterMs = Math.min(9e4, this.queryConfig.replyStallTimeoutMs);
30233
30522
  for (const [key, proc] of this.agents) {
30234
30523
  if (!this.isEvictable(proc)) continue;
30235
30524
  const runtime = this.asRuntime(proc);
@@ -30240,26 +30529,62 @@ var AgentManager = class {
30240
30529
  for (const [, proc] of this.agents) {
30241
30530
  if (proc.status !== "working") continue;
30242
30531
  const runtime = this.asRuntime(proc);
30532
+ if (runtime.currentTask) {
30533
+ const sinceEventMs = now - proc.lastSdkEventAt;
30534
+ if (sinceEventMs > stallWarnAfterMs && !proc.stallWarned) {
30535
+ proc.stallWarned = true;
30536
+ const openTool = this.latestOpenToolUse(proc);
30537
+ logger13.warn("Reply stall onset: in-flight reply silent", {
30538
+ agentId: proc.agentId,
30539
+ scope: scopeKey(proc.scope),
30540
+ sinceEventMs,
30541
+ replyStallTimeoutMs: this.queryConfig.replyStallTimeoutMs,
30542
+ workingSilenceTimeoutMs,
30543
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
30544
+ replyMessageId: runtime.currentTask.replyMessageId,
30545
+ model: proc.model ?? "(unknown)",
30546
+ lastSdkEvent: proc.lastSdkEventInfo,
30547
+ hasActiveToolUse: runtime.activeToolUseStartedAt != null || openTool != null,
30548
+ activeToolUseAgeMs: runtime.activeToolUseStartedAt != null ? now - runtime.activeToolUseStartedAt : null,
30549
+ openToolName: openTool?.toolName ?? proc.currentToolName ?? null,
30550
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null,
30551
+ subagentInFlight: (runtime.activeSubagentTaskIds?.size ?? 0) > 0,
30552
+ activeSubagentCount: runtime.activeSubagentTaskIds?.size ?? 0,
30553
+ busyReason: this.busyReason(proc)
30554
+ });
30555
+ }
30556
+ }
30557
+ const busyReason = this.busyReason(runtime);
30558
+ const busy = busyReason !== null;
30559
+ if (runtime.currentTask && !busy && now - proc.lastSdkEventAt > this.queryConfig.replyStallTimeoutMs) {
30560
+ void this.recoverStalledReply(proc, now - proc.lastSdkEventAt);
30561
+ continue;
30562
+ }
30243
30563
  const hasInjectedBacklog = runtime.injectedTasks.length > 0;
30244
- const effectiveTimeoutMs = hasInjectedBacklog ? workingSilenceTimeoutMs * 2 : workingSilenceTimeoutMs;
30564
+ const baseCeilingMs = busy ? Math.max(workingSilenceTimeoutMs, this.queryConfig.busySilenceTimeoutMs ?? 0) : workingSilenceTimeoutMs;
30565
+ const effectiveTimeoutMs = hasInjectedBacklog ? baseCeilingMs * 2 : baseCeilingMs;
30245
30566
  const silentMs = now - proc.lastSdkEventAt;
30246
30567
  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
- );
30568
+ if (silentMs > workingSilenceTimeoutMs) {
30569
+ logger13.warn("Zombie watchdog: working runtime silent past base timeout; granting extended grace", {
30570
+ agentId: proc.agentId,
30571
+ scope: scopeKey(proc.scope),
30572
+ silentMs,
30573
+ baseTimeoutMs: workingSilenceTimeoutMs,
30574
+ baseCeilingMs,
30575
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
30576
+ effectiveTimeoutMs,
30577
+ busy,
30578
+ busyReason,
30579
+ injectedTaskCount: runtime.injectedTasks.length,
30580
+ replyMessageId: proc.currentTask?.replyMessageId
30581
+ });
30260
30582
  }
30261
30583
  continue;
30262
30584
  }
30585
+ const zombieOpenTool = this.latestOpenToolUse(proc);
30586
+ const watchdogDetectedAt = Date.now();
30587
+ const watchdogFallbackId = createFallbackId();
30263
30588
  logger13.warn("Zombie watchdog: working runtime silent too long, tearing down", {
30264
30589
  agentId: proc.agentId,
30265
30590
  scope: scopeKey(proc.scope),
@@ -30267,12 +30592,46 @@ var AgentManager = class {
30267
30592
  lastSdkEventAt: new Date(proc.lastSdkEventAt).toISOString(),
30268
30593
  workingSilenceTimeoutMs,
30269
30594
  effectiveTimeoutMs,
30595
+ baseCeilingMs,
30596
+ busy,
30597
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
30270
30598
  replyMessageId: proc.currentTask?.replyMessageId,
30271
30599
  injectedTaskCount: runtime.injectedTasks.length,
30272
30600
  hadInjectedBacklog: hasInjectedBacklog,
30273
- inboxSize: proc.groupInbox.length
30601
+ inboxSize: proc.groupInbox.length,
30602
+ fallbackId: watchdogFallbackId,
30603
+ // Breadcrumb: what the turn was waiting on when it timed out. A hung subagent
30604
+ // (open `Task` tool_use) lands here now that the fast path skips active tools.
30605
+ model: proc.model ?? "(unknown)",
30606
+ lastSdkEvent: proc.lastSdkEventInfo,
30607
+ openToolName: zombieOpenTool?.toolName ?? proc.currentToolName ?? null,
30608
+ activeToolUseAgeMs: runtime.activeToolUseStartedAt != null ? now - runtime.activeToolUseStartedAt : null,
30609
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null,
30610
+ subagentInFlight: (runtime.activeSubagentTaskIds?.size ?? 0) > 0,
30611
+ activeSubagentCount: runtime.activeSubagentTaskIds?.size ?? 0,
30612
+ busyReason: this.busyReason(proc)
30613
+ });
30614
+ logFallback(logger13, {
30615
+ fallbackId: watchdogFallbackId,
30616
+ type: "zombie_watchdog",
30617
+ phase: "detected",
30618
+ expected: false,
30619
+ traceId: proc.currentTask?.traceId,
30620
+ context: {
30621
+ agentId: proc.agentId,
30622
+ scope: scopeKey(proc.scope),
30623
+ silentMs,
30624
+ replyMessageId: proc.currentTask?.replyMessageId ?? null,
30625
+ openToolName: zombieOpenTool?.toolName ?? proc.currentToolName ?? null,
30626
+ lastSdkEventType: proc.lastSdkEventInfo?.type ?? null,
30627
+ injectedTaskCount: runtime.injectedTasks.length,
30628
+ inboxSize: proc.groupInbox.length
30629
+ }
30630
+ });
30631
+ void this.closeRuntime(proc, "zombie_watchdog", {
30632
+ fallbackId: watchdogFallbackId,
30633
+ detectedAt: watchdogDetectedAt
30274
30634
  });
30275
- void this.closeRuntime(proc, "zombie_watchdog");
30276
30635
  }
30277
30636
  }
30278
30637
  /**
@@ -30519,7 +30878,7 @@ ${cfg.instructions.trim()}` : "";
30519
30878
  agentId: agentConfig.id,
30520
30879
  capabilityTier: cfg.capabilityTier,
30521
30880
  isSmith: smithAgent
30522
- }) ?? { mcpServers: {}, allowedTools: [] };
30881
+ }) ?? { mcpServers: {}, allowedTools: [], toolAbi: [] };
30523
30882
  logger13.info("External MCP resolved for runtime", {
30524
30883
  agentId: agentConfig.id,
30525
30884
  scope: scopeKey(scope),
@@ -30536,11 +30895,16 @@ ${cfg.instructions.trim()}` : "";
30536
30895
  }
30537
30896
  const notebookSection = this.buildNotebookSection(agentConfig.id);
30538
30897
  const scopesSection = this.buildScopesSection(agentConfig, scope, agentCwd);
30898
+ const externalMcpFingerprint = this.externalMcpFingerprint(externalMcp);
30539
30899
  savedSessionId = this.discardSessionIfScopePromptChanged(
30540
30900
  agentConfig,
30541
30901
  scope,
30542
30902
  savedSessionId,
30543
- this.scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection)
30903
+ this.scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection, externalMcpFingerprint),
30904
+ {
30905
+ clearLegacySingleSession: externalMcpFingerprint.length > 0,
30906
+ reason: externalMcpFingerprint.length > 0 ? "external_mcp_abi_changed" : "scope_prompt_changed"
30907
+ }
30544
30908
  );
30545
30909
  let forkHistorySection = "";
30546
30910
  if (!savedSessionId && scope.kind === "single") {
@@ -30561,6 +30925,8 @@ ${cfg.instructions.trim()}` : "";
30561
30925
  }
30562
30926
  }
30563
30927
  const cronLockSnapshot = readCronLockSnapshot();
30928
+ const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
30929
+ const disallowedToolsForRuntime = builtinWebSearchAllowed ? [] : ["WebSearch"];
30564
30930
  logger13.info("Creating Agent query", {
30565
30931
  agentId: agentConfig.id,
30566
30932
  scope: scopeKey(scope),
@@ -30569,6 +30935,8 @@ ${cfg.instructions.trim()}` : "";
30569
30935
  sessionId: savedSessionId,
30570
30936
  forkHistoryReplay: forkHistorySection.length > 0,
30571
30937
  model: cfg.model ?? "(default)",
30938
+ builtinWebSearchAllowed,
30939
+ disallowedTools: disallowedToolsForRuntime,
30572
30940
  // Diagnostic: who currently owns Claude's global cron lock (~/.claude/scheduled_tasks.lock).
30573
30941
  // Cron is process-internal but the binary uses this singleton lock to elect ONE scheduler
30574
30942
  // among concurrent claude subprocesses. If lock is held by another session at spawn time,
@@ -30580,7 +30948,6 @@ ${cfg.instructions.trim()}` : "";
30580
30948
  });
30581
30949
  const planModeRef = { active: false, denyCount: 0 };
30582
30950
  const mediaGenerationTurnGuard = createOfficialMediaGenerationTurnGuard();
30583
- const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
30584
30951
  const options = {
30585
30952
  cwd: agentCwd,
30586
30953
  systemPrompt: {
@@ -30647,6 +31014,8 @@ ${cfg.instructions.trim()}` : "";
30647
31014
  ] : [],
30648
31015
  ...externalMcp.allowedTools
30649
31016
  ],
31017
+ // Server-side WebSearch bypasses canUseTool; disallowedTools removes it from model context.
31018
+ disallowedTools: disallowedToolsForRuntime,
30650
31019
  mcpServers: { ...externalMcp.mcpServers, neural: neuralServer },
30651
31020
  includePartialMessages: true,
30652
31021
  // Plan mode custom workflow instructions. When setPermissionMode('plan') is
@@ -30973,6 +31342,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
30973
31342
  currentTask: null,
30974
31343
  currentTaskStartedAt: 0,
30975
31344
  lastSdkEventAt: Date.now(),
31345
+ model: (typeof options.model === "string" ? options.model : cfg.model) ?? null,
30976
31346
  compactRequested: false,
30977
31347
  compactInProgress: false,
30978
31348
  contextOverflowLockedUntil: 0,
@@ -30984,13 +31354,18 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
30984
31354
  currentToolName: null,
30985
31355
  currentMcpInvocationId: null,
30986
31356
  currentMcpInvocationStartedAt: null,
31357
+ activeSubagentTaskIds: /* @__PURE__ */ new Set(),
30987
31358
  mcpAuditRecorder: this.mcpAuditRecorder,
30988
31359
  segmentBuffer: "",
30989
31360
  segmentCount: 0,
30990
31361
  accumulatedToolInput: "",
30991
31362
  planModeRef,
30992
31363
  mediaGenerationTurnGuard,
30993
- groupInbox: []
31364
+ groupInbox: [],
31365
+ spectating: false,
31366
+ spectateActivatedAt: 0,
31367
+ spectateViewing: false,
31368
+ spectateTtlExpired: false
30994
31369
  };
30995
31370
  const runtime = Object.assign(proc, {
30996
31371
  query: agentQuery,
@@ -31002,7 +31377,8 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
31002
31377
  createdAt: Date.now(),
31003
31378
  supportsVision: modelInputMode === "vision" && cfg.supportsVision !== false,
31004
31379
  modelInputMode,
31005
- quietFlushTimer: null
31380
+ quietFlushTimer: null,
31381
+ spectateRevertTimer: null
31006
31382
  });
31007
31383
  logger13.info("Agent model input mode resolved", {
31008
31384
  agentId: agentConfig.id,
@@ -31027,6 +31403,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
31027
31403
  } else {
31028
31404
  this.dormantGroupInboxes.delete(key);
31029
31405
  }
31406
+ const dormantMeta = this.dormantScopes.get(key);
31030
31407
  if (this.dormantScopes.delete(key)) {
31031
31408
  this.emit({
31032
31409
  type: "agent:awake",
@@ -31038,7 +31415,44 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
31038
31415
  });
31039
31416
  logger13.info("Agent scope awakened after dormant", {
31040
31417
  agentId: agentConfig.id,
31041
- scope: scopeKey(scope)
31418
+ scope: scopeKey(scope),
31419
+ ...dormantMeta ? {
31420
+ fallbackId: dormantMeta.fallbackId,
31421
+ dormantDurationMs: Date.now() - dormantMeta.detectedAt,
31422
+ dataLossSuspected: dormantMeta.droppedTaskCount > 0
31423
+ } : {}
31424
+ });
31425
+ if (dormantMeta) {
31426
+ logFallback(logger13, {
31427
+ fallbackId: dormantMeta.fallbackId,
31428
+ type: "zombie_watchdog",
31429
+ phase: "outcome",
31430
+ expected: false,
31431
+ context: {
31432
+ agentId: agentConfig.id,
31433
+ scope: scopeKey(scope)
31434
+ },
31435
+ outcome: {
31436
+ result: "recovered_rebuilt",
31437
+ durationMs: Date.now() - dormantMeta.detectedAt,
31438
+ dataLossSuspected: dormantMeta.droppedTaskCount > 0
31439
+ }
31440
+ });
31441
+ }
31442
+ }
31443
+ const pendingSpectateAt = this.pendingSpectate.get(key);
31444
+ if (pendingSpectateAt != null) {
31445
+ this.pendingSpectate.delete(key);
31446
+ runtime.spectating = true;
31447
+ runtime.spectateViewing = false;
31448
+ runtime.spectateActivatedAt = pendingSpectateAt;
31449
+ runtime.spectateTtlExpired = false;
31450
+ this.armSpectateTimer(runtime);
31451
+ this.emitSpectateState(runtime, true, "started");
31452
+ logger13.info("Applied pending spectate on runtime create", {
31453
+ agentId: agentConfig.id,
31454
+ scope: scopeKey(scope),
31455
+ activatedAt: pendingSpectateAt
31042
31456
  });
31043
31457
  }
31044
31458
  if (proc.groupInbox.length > 0 && this.isRuntimeIdleForInboxFlush(runtime)) {
@@ -32176,7 +32590,6 @@ ${lines.join("\n")}`;
32176
32590
  compactTrigger: "context_watermark",
32177
32591
  injectedTasksWaiting: runtime.injectedTasks.length,
32178
32592
  compactPromptLen: compactPrompt.length,
32179
- promptSample: compactPrompt.slice(0, 80),
32180
32593
  traceId: compactTraceId
32181
32594
  });
32182
32595
  runtime.inputController.push(compactPrompt, runtime.ccSessionId ?? "");
@@ -32458,7 +32871,7 @@ ${lines.join("\n")}`;
32458
32871
  const enveloped = buildInnerVoiceEnvelope(payloadWithTrigger, ctx);
32459
32872
  const task = {
32460
32873
  content: enveloped,
32461
- replyMessageId: createMessageId(),
32874
+ replyMessageId: createNeuralSendReplyMessageId(),
32462
32875
  conversationId: payload.conversationId,
32463
32876
  traceId: createTraceId(),
32464
32877
  groupId: payload.groupId
@@ -32704,7 +33117,7 @@ ${lines.join("\n")}`;
32704
33117
  this.dormantScopes.delete(key);
32705
33118
  this.dormantGroupInboxes.delete(key);
32706
33119
  }
32707
- for (const key of [...this.dormantScopes].filter(
33120
+ for (const key of [...this.dormantScopes.keys()].filter(
32708
33121
  (k) => k === agentId || k.startsWith(`${agentId}::`)
32709
33122
  )) {
32710
33123
  this.dormantScopes.delete(key);
@@ -32752,7 +33165,7 @@ ${lines.join("\n")}`;
32752
33165
  async reloadAgentScopes(agentId, reason) {
32753
33166
  this.sessionStore.deleteAllForAgent(agentId);
32754
33167
  this.dispatchMemory.deleteAllForAgent(agentId);
32755
- for (const key of [...this.dormantScopes].filter(
33168
+ for (const key of [...this.dormantScopes.keys()].filter(
32756
33169
  (k) => k === agentId || k.startsWith(`${agentId}::`)
32757
33170
  )) {
32758
33171
  this.dormantScopes.delete(key);
@@ -32904,6 +33317,125 @@ ${lines.join("\n")}`;
32904
33317
  void this.terminateScope(proc.agentId, proc.scope);
32905
33318
  }
32906
33319
  }
33320
+ /** Control spectate capture/push for one scoped runtime. */
33321
+ async setSpectate(agentId, scope, action) {
33322
+ const key = runtimeKey(agentId, scope);
33323
+ const proc = this.agents.get(key);
33324
+ if (!proc || proc.status === "dead") {
33325
+ if (action === "start") {
33326
+ this.pendingSpectate.set(key, Date.now());
33327
+ logger13.info("setSpectate: runtime missing, pending start", { agentId, scope: scopeKey(scope) });
33328
+ }
33329
+ return;
33330
+ }
33331
+ const runtime = this.asRuntime(proc);
33332
+ switch (action) {
33333
+ case "start":
33334
+ runtime.spectating = true;
33335
+ runtime.spectateViewing = true;
33336
+ runtime.spectateActivatedAt = Date.now();
33337
+ runtime.spectateTtlExpired = false;
33338
+ this.pendingSpectate.delete(key);
33339
+ this.armSpectateTimer(runtime);
33340
+ this.emitSpectateState(runtime, true, "started");
33341
+ logger13.info("Spectate started", { agentId, scope: scopeKey(scope) });
33342
+ break;
33343
+ case "enter_view":
33344
+ runtime.spectateViewing = true;
33345
+ logger13.info("Spectate enter_view", { agentId, scope: scopeKey(scope) });
33346
+ break;
33347
+ case "leave_view":
33348
+ runtime.spectateViewing = false;
33349
+ logger13.info("Spectate leave_view", {
33350
+ agentId,
33351
+ scope: scopeKey(scope),
33352
+ ttlExpired: runtime.spectateTtlExpired
33353
+ });
33354
+ if (runtime.spectateTtlExpired) {
33355
+ this.stopSpectate(runtime, "ttl_expired");
33356
+ }
33357
+ break;
33358
+ case "stop":
33359
+ this.stopSpectate(runtime, "stopped");
33360
+ this.pendingSpectate.delete(key);
33361
+ logger13.info("Spectate stopped", { agentId, scope: scopeKey(scope) });
33362
+ break;
33363
+ default:
33364
+ break;
33365
+ }
33366
+ }
33367
+ spectateTtlMs() {
33368
+ return Number(process.env.AHCHAT_BRIDGE_SPECTATE_TTL_MS) || 36e5;
33369
+ }
33370
+ armSpectateTimer(runtime) {
33371
+ this.clearSpectateTimer(runtime);
33372
+ if (!runtime.spectating || runtime.spectateActivatedAt <= 0) return;
33373
+ const ttlMs = this.spectateTtlMs();
33374
+ const elapsed = Date.now() - runtime.spectateActivatedAt;
33375
+ const delay = Math.max(0, ttlMs - elapsed);
33376
+ runtime.spectateRevertTimer = setTimeout(() => {
33377
+ runtime.spectateRevertTimer = null;
33378
+ runtime.spectateTtlExpired = true;
33379
+ logger13.info("Spectate TTL expired", {
33380
+ agentId: runtime.agentId,
33381
+ scope: scopeKey(runtime.scope),
33382
+ viewing: runtime.spectateViewing
33383
+ });
33384
+ if (!runtime.spectateViewing) {
33385
+ this.stopSpectate(runtime, "ttl_expired");
33386
+ }
33387
+ }, delay);
33388
+ }
33389
+ clearSpectateTimer(runtime) {
33390
+ if (runtime.spectateRevertTimer != null) {
33391
+ clearTimeout(runtime.spectateRevertTimer);
33392
+ runtime.spectateRevertTimer = null;
33393
+ }
33394
+ }
33395
+ stopSpectate(runtime, reason) {
33396
+ const wasActive = runtime.spectating;
33397
+ this.clearSpectateTimer(runtime);
33398
+ runtime.spectating = false;
33399
+ runtime.spectateViewing = false;
33400
+ runtime.spectateTtlExpired = false;
33401
+ runtime.spectateActivatedAt = 0;
33402
+ if (wasActive) {
33403
+ this.emitSpectateState(runtime, false, reason);
33404
+ logger13.info("Spectate deactivated", {
33405
+ agentId: runtime.agentId,
33406
+ scope: scopeKey(runtime.scope),
33407
+ reason
33408
+ });
33409
+ }
33410
+ }
33411
+ emitSpectateState(runtime, active, reason) {
33412
+ const scopePayload = runtime.scope.kind === "single" ? { kind: "single" } : { kind: "group", groupId: runtime.scope.groupId };
33413
+ this.emit({
33414
+ type: "spectate:state",
33415
+ payload: {
33416
+ agentId: runtime.agentId,
33417
+ scope: scopePayload,
33418
+ active,
33419
+ expiresAt: active ? runtime.spectateActivatedAt + this.spectateTtlMs() : void 0,
33420
+ reason,
33421
+ traceId: createTraceId()
33422
+ }
33423
+ });
33424
+ logger13.info("Spectate state emitted", {
33425
+ agentId: runtime.agentId,
33426
+ scope: scopeKey(runtime.scope),
33427
+ active,
33428
+ reason,
33429
+ expiresAt: active ? runtime.spectateActivatedAt + this.spectateTtlMs() : void 0
33430
+ });
33431
+ }
33432
+ teardownSpectate(runtime) {
33433
+ if (runtime.spectating) {
33434
+ this.stopSpectate(runtime, "runtime_gone");
33435
+ } else {
33436
+ this.clearSpectateTimer(runtime);
33437
+ }
33438
+ }
32907
33439
  /** Stop one scoped SDK runtime (workdir change). */
32908
33440
  async terminateScope(agentId, scope) {
32909
33441
  const key = runtimeKey(agentId, scope);
@@ -32912,6 +33444,7 @@ ${lines.join("\n")}`;
32912
33444
  logger13.info("terminateScope: no active runtime", { agentId, scope: scopeKey(scope) });
32913
33445
  this.dormantScopes.delete(key);
32914
33446
  this.dormantGroupInboxes.delete(key);
33447
+ this.pendingSpectate.delete(key);
32915
33448
  this.sessionStore.delete(agentId, scope);
32916
33449
  this.dispatchMemory.deleteScope(agentId, scope);
32917
33450
  return;
@@ -32934,7 +33467,7 @@ ${lines.join("\n")}`;
32934
33467
  this.dispatchMemory.deleteScope(agentId, scope);
32935
33468
  logger13.info("terminateScope: scoped query removed", { agentId, scope: scopeKey(scope) });
32936
33469
  }
32937
- async closeRuntime(proc, reason) {
33470
+ async closeRuntime(proc, reason, watchdogForensics) {
32938
33471
  const key = runtimeKey(proc.agentId, proc.scope);
32939
33472
  if (proc.status === "dead") return;
32940
33473
  const runtime = this.asRuntime(proc);
@@ -33011,12 +33544,13 @@ ${lines.join("\n")}`;
33011
33544
  runtime.currentTask = null;
33012
33545
  if (isWatchdog) {
33013
33546
  const preservedInbox = proc.groupInbox;
33014
- if (preservedInbox.length > 0) {
33547
+ const preservedInboxSize = preservedInbox.length;
33548
+ if (preservedInboxSize > 0) {
33015
33549
  this.dormantGroupInboxes.set(key, [...preservedInbox]);
33016
33550
  logger13.info("Preserving groupInbox for dormant agent", {
33017
33551
  agentId,
33018
33552
  scope: scopeKey(proc.scope),
33019
- preservedInboxSize: preservedInbox.length,
33553
+ preservedInboxSize,
33020
33554
  preservedEntries: preservedInbox.map((e) => ({
33021
33555
  ackId: e.ackId,
33022
33556
  sender: e.senderName,
@@ -33025,7 +33559,26 @@ ${lines.join("\n")}`;
33025
33559
  }))
33026
33560
  });
33027
33561
  }
33028
- this.dormantScopes.add(key);
33562
+ const effectiveFallbackId = watchdogForensics?.fallbackId ?? createFallbackId();
33563
+ logFallback(logger13, {
33564
+ fallbackId: effectiveFallbackId,
33565
+ type: "zombie_watchdog",
33566
+ phase: "applied",
33567
+ expected: false,
33568
+ traceId: dormantTraceId,
33569
+ context: {
33570
+ agentId,
33571
+ scope: scopeKey(proc.scope),
33572
+ droppedTaskCount: droppedAckIds.length,
33573
+ preservedInboxSize,
33574
+ sessionDeleted: false
33575
+ }
33576
+ });
33577
+ this.dormantScopes.set(key, {
33578
+ fallbackId: effectiveFallbackId,
33579
+ detectedAt: watchdogForensics?.detectedAt ?? Date.now(),
33580
+ droppedTaskCount: droppedAckIds.length
33581
+ });
33029
33582
  this.emit({
33030
33583
  type: "agent:dormant",
33031
33584
  payload: {
@@ -33040,13 +33593,15 @@ ${lines.join("\n")}`;
33040
33593
  agentId,
33041
33594
  scope: scopeKey(proc.scope),
33042
33595
  droppedTaskCount: droppedAckIds.length,
33043
- preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0
33596
+ preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0,
33597
+ fallbackId: effectiveFallbackId
33044
33598
  });
33045
33599
  }
33046
33600
  proc.status = "dead";
33047
33601
  this.agents.delete(key);
33048
33602
  this.lastUsedAt.delete(key);
33049
33603
  this.clearQuietFlushTimer(runtime);
33604
+ this.teardownSpectate(runtime);
33050
33605
  try {
33051
33606
  runtime.inputController.close();
33052
33607
  await this.awaitQueryReturn(runtime.query, 5e3, agentId);
@@ -33063,6 +33618,165 @@ ${lines.join("\n")}`;
33063
33618
  cwd: proc.cwd
33064
33619
  });
33065
33620
  }
33621
+ /**
33622
+ * Emit `agent:error` for the active reply and every queued/merged/buffered task,
33623
+ * then clear those queues. Used by both the SDK stream-crash path and the
33624
+ * reply-stall watchdog so a torn-down runtime never leaves a carrier reply
33625
+ * stuck in-flight on the server (which would keep absorbing new user messages
33626
+ * as steers of a dead turn).
33627
+ */
33628
+ failPendingTasksWithError(runtime, errorText, fallbackId) {
33629
+ const pending = [];
33630
+ if (runtime.currentTask) pending.push(runtime.currentTask);
33631
+ pending.push(...runtime.injectedTasks, ...runtime.mergedTasks, ...runtime.planModeBuffer);
33632
+ runtime.currentTask = null;
33633
+ runtime.injectedTasks = [];
33634
+ runtime.mergedTasks = [];
33635
+ runtime.planModeBuffer = [];
33636
+ if (pending.length === 0) return { pendingCount: 0 };
33637
+ const carrier = pending[0];
33638
+ const mergedTasks = pending.slice(1);
33639
+ logger13.warn("Pending tasks failure consolidated", {
33640
+ agentId: runtime.agentId,
33641
+ scope: scopeKey(runtime.scope),
33642
+ pendingCount: pending.length,
33643
+ carrierAckId: carrier.replyMessageId,
33644
+ mergedAckIds: mergedTasks.map((t) => t.replyMessageId),
33645
+ traceId: carrier.traceId,
33646
+ ...fallbackId ? { fallbackId } : {}
33647
+ });
33648
+ this.emit({
33649
+ type: "agent:error",
33650
+ payload: {
33651
+ agentId: runtime.agentId,
33652
+ conversationId: carrier.conversationId,
33653
+ ackId: carrier.replyMessageId,
33654
+ traceId: carrier.traceId,
33655
+ error: errorText
33656
+ }
33657
+ });
33658
+ for (const task of mergedTasks) {
33659
+ this.emit({
33660
+ type: "agent:merged",
33661
+ payload: {
33662
+ agentId: runtime.agentId,
33663
+ conversationId: task.conversationId,
33664
+ ackId: task.replyMessageId,
33665
+ mergedIntoAckId: carrier.replyMessageId,
33666
+ groupId: task.groupId,
33667
+ traceId: task.traceId
33668
+ }
33669
+ });
33670
+ }
33671
+ return { pendingCount: pending.length };
33672
+ }
33673
+ /**
33674
+ * Recover an in-flight reply that started but went silent past
33675
+ * `replyStallTimeoutMs` (see the reply-stall fast path in `evictIdle`). The
33676
+ * underlying SDK turn is wedged with no observable progress and no error, so:
33677
+ * 1. clear the (likely interrupted/dangling) session so the next dispatch
33678
+ * starts fresh instead of resuming the same wedged transcript;
33679
+ * 2. release the carrier reply + queued steers via `agent:error` so the
33680
+ * client stops waiting and the next user message starts a brand-new reply;
33681
+ * 3. tear the wedged runtime down.
33682
+ */
33683
+ async recoverStalledReply(proc, silentMs) {
33684
+ if (proc.status === "dead") return;
33685
+ const runtime = this.asRuntime(proc);
33686
+ const key = runtimeKey(proc.agentId, proc.scope);
33687
+ const replyStallFallbackId = createFallbackId();
33688
+ const stallTraceId = proc.currentTask?.traceId;
33689
+ logger13.warn("Reply stall watchdog: in-flight reply silent too long, recovering", {
33690
+ agentId: proc.agentId,
33691
+ scope: scopeKey(proc.scope),
33692
+ silentMs,
33693
+ replyStallTimeoutMs: this.queryConfig.replyStallTimeoutMs,
33694
+ replyMessageId: proc.currentTask?.replyMessageId,
33695
+ injectedTaskCount: runtime.injectedTasks.length,
33696
+ lastSdkEventAt: new Date(proc.lastSdkEventAt).toISOString(),
33697
+ fallbackId: replyStallFallbackId,
33698
+ // Breadcrumb: what the wedged turn was doing the instant it went silent.
33699
+ // (subagent Task call? mid tool_use? which provider?) — the difference
33700
+ // between a one-off and a systemic provider/tool stall.
33701
+ model: proc.model ?? "(unknown)",
33702
+ lastSdkEvent: proc.lastSdkEventInfo,
33703
+ currentBlockType: proc.currentBlockType,
33704
+ currentToolName: proc.currentToolName,
33705
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null
33706
+ });
33707
+ logFallback(logger13, {
33708
+ fallbackId: replyStallFallbackId,
33709
+ type: "reply_stall",
33710
+ phase: "detected",
33711
+ expected: false,
33712
+ traceId: stallTraceId,
33713
+ context: {
33714
+ agentId: proc.agentId,
33715
+ scope: scopeKey(proc.scope),
33716
+ silentMs,
33717
+ model: proc.model ?? "(unknown)",
33718
+ replyMessageId: proc.currentTask?.replyMessageId ?? null,
33719
+ currentToolName: proc.currentToolName ?? null,
33720
+ lastSdkEventType: proc.lastSdkEventInfo?.type ?? null,
33721
+ injectedTaskCount: runtime.injectedTasks.length,
33722
+ mergedTaskCount: runtime.mergedTasks.length,
33723
+ planModeBufferCount: runtime.planModeBuffer.length
33724
+ }
33725
+ });
33726
+ this.sessionStore.delete(proc.agentId, proc.scope);
33727
+ this.dispatchMemory.deleteScope(proc.agentId, proc.scope);
33728
+ const failSummary = this.failPendingTasksWithError(
33729
+ runtime,
33730
+ "\u56DE\u590D\u957F\u65F6\u95F4\u65E0\u54CD\u5E94\uFF0C\u5DF2\u91CD\u7F6E\u8BE5\u4F1A\u8BDD\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\u3002",
33731
+ replyStallFallbackId
33732
+ );
33733
+ proc.status = "dead";
33734
+ this.agents.delete(key);
33735
+ this.lastUsedAt.delete(key);
33736
+ this.clearQuietFlushTimer(runtime);
33737
+ let queryCloseOk = true;
33738
+ try {
33739
+ runtime.inputController.close();
33740
+ await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
33741
+ } catch (e) {
33742
+ queryCloseOk = false;
33743
+ logger13.error("reply_stall: close query failed", {
33744
+ agentId: proc.agentId,
33745
+ scope: scopeKey(proc.scope),
33746
+ error: e
33747
+ });
33748
+ }
33749
+ logFallback(logger13, {
33750
+ fallbackId: replyStallFallbackId,
33751
+ type: "reply_stall",
33752
+ phase: "applied",
33753
+ expected: false,
33754
+ traceId: stallTraceId,
33755
+ context: {
33756
+ agentId: proc.agentId,
33757
+ scope: scopeKey(proc.scope),
33758
+ sessionDeleted: true,
33759
+ failedTaskCount: failSummary.pendingCount,
33760
+ queryClosed: queryCloseOk
33761
+ }
33762
+ });
33763
+ logFallback(logger13, {
33764
+ fallbackId: replyStallFallbackId,
33765
+ type: "reply_stall",
33766
+ phase: "outcome",
33767
+ expected: false,
33768
+ traceId: stallTraceId,
33769
+ context: {
33770
+ agentId: proc.agentId,
33771
+ scope: scopeKey(proc.scope),
33772
+ failedTaskCount: failSummary.pendingCount
33773
+ },
33774
+ outcome: {
33775
+ result: "session_reset_awaiting_user",
33776
+ dataLossSuspected: failSummary.pendingCount > 0
33777
+ }
33778
+ });
33779
+ }
33066
33780
  async recoverFromRestart(agents) {
33067
33781
  const lockSnapshot = readCronLockSnapshot();
33068
33782
  logger13.info("Recovering Agent sessions after restart", {
@@ -33185,58 +33899,7 @@ ${lines.join("\n")}`;
33185
33899
  this.lastUsedAt.delete(key);
33186
33900
  const errorText = isResumeFail ? `\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\uFF08${errMsg}\uFF09` : `Agent query crashed: ${errMsg}`;
33187
33901
  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 = [];
33902
+ this.failPendingTasksWithError(runtime, emittedErrorText);
33240
33903
  }
33241
33904
  }
33242
33905
  getStatus(agentId, scope = { kind: "single" }) {
@@ -33250,6 +33913,18 @@ ${lines.join("\n")}`;
33250
33913
  }
33251
33914
  return [...ids];
33252
33915
  }
33916
+ /** Unified signal: is the turn legitimately waiting on a live external call that emits no
33917
+ * parent heartbeat? Used by BOTH the reply-stall fast path and the zombie watchdog so neither
33918
+ * tears down a turn that is merely slow. Returns the reason for diagnostics, or null when idle.
33919
+ * - open_tool: regular tool, MCP tool, AskUserQuestion wait, or ExitPlanMode (tool_use open).
33920
+ * - subagent: Task/Agent in flight (its inner tool_results clear activeToolUseStartedAt).
33921
+ * - compact: bridge-injected /compact running. */
33922
+ busyReason(proc) {
33923
+ if (proc.activeToolUseStartedAt != null || this.latestOpenToolUse(proc) != null) return "open_tool";
33924
+ if ((proc.activeSubagentTaskIds?.size ?? 0) > 0) return "subagent";
33925
+ if (proc.compactInProgress === true) return "compact";
33926
+ return null;
33927
+ }
33253
33928
  latestOpenToolUse(proc) {
33254
33929
  for (let i = proc.contentBlocks.length - 1; i >= 0; i -= 1) {
33255
33930
  const block = proc.contentBlocks[i];
@@ -33398,7 +34073,7 @@ ${lines.join("\n")}`;
33398
34073
  }
33399
34074
  const task = {
33400
34075
  content: notice,
33401
- replyMessageId: createMessageId(),
34076
+ replyMessageId: createScopeNoticeReplyMessageId(),
33402
34077
  conversationId,
33403
34078
  traceId: createTraceId(),
33404
34079
  groupId: proc.scope.kind === "group" ? proc.scope.groupId : void 0
@@ -34480,6 +35155,7 @@ var HttpMcpRegistry = class {
34480
35155
  buildForAgent(ctx) {
34481
35156
  const mcpServers = {};
34482
35157
  const allowedTools = [];
35158
+ const toolAbi = [];
34483
35159
  const usedNames = /* @__PURE__ */ new Set();
34484
35160
  for (const connection of this.allConnections()) {
34485
35161
  if (!this.connectionAppliesToAgent(connection, ctx)) continue;
@@ -34488,12 +35164,18 @@ var HttpMcpRegistry = class {
34488
35164
  const serverName = uniqueServerName(normalizeMcpServerName(connection.serverName), usedNames);
34489
35165
  usedNames.add(serverName);
34490
35166
  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
- }
35167
+ const visibleTools = connection.tools.filter((tool2) => tool2.enabled && tool2.permissionPolicy !== "always_deny");
35168
+ for (const tool2 of visibleTools) allowedTools.push(mcpRuntimeToolName(serverName, tool2.name));
35169
+ toolAbi.push({
35170
+ serverName,
35171
+ providerId: connection.providerId,
35172
+ transport: connection.transport,
35173
+ alwaysLoad: connection.alwaysLoad,
35174
+ isBuiltin: connection.isBuiltin,
35175
+ tools: visibleTools.map((tool2) => runtimeToolAbi(serverName, tool2)).sort((a, b) => a.runtimeToolName.localeCompare(b.runtimeToolName))
35176
+ });
34495
35177
  }
34496
- return { mcpServers, allowedTools };
35178
+ return { mcpServers, allowedTools, toolAbi };
34497
35179
  }
34498
35180
  allConnections() {
34499
35181
  return [...this.serverConnections.values(), ...this.localConnections.values()];
@@ -34679,6 +35361,18 @@ function uniqueServerName(serverName, usedNames) {
34679
35361
  while (usedNames.has(`${serverName}_${idx}`)) idx += 1;
34680
35362
  return `${serverName}_${idx}`;
34681
35363
  }
35364
+ function runtimeToolAbi(serverName, tool2) {
35365
+ return {
35366
+ name: tool2.name,
35367
+ runtimeToolName: mcpRuntimeToolName(serverName, tool2.name),
35368
+ displayName: tool2.displayName,
35369
+ description: tool2.description,
35370
+ category: tool2.category,
35371
+ riskLevel: tool2.riskLevel,
35372
+ permissionPolicy: tool2.permissionPolicy,
35373
+ ...tool2.inputSchema !== void 0 ? { inputSchema: tool2.inputSchema } : {}
35374
+ };
35375
+ }
34682
35376
  function buildHeaders(authType, authSecret, customHeaders) {
34683
35377
  const headers = {};
34684
35378
  for (const header of customHeaders) {
@@ -35330,6 +36024,7 @@ var ServerConnector = class {
35330
36024
  case "agent:terminate":
35331
36025
  case "agent:runtime_reload":
35332
36026
  case "agent:terminate_scope":
36027
+ case "spectate:set":
35333
36028
  case "agent:created":
35334
36029
  case "agent:updated":
35335
36030
  case "agent:workdir-updated":
@@ -36305,24 +37000,6 @@ function normalizeLocalPath(targetPath) {
36305
37000
  const expanded = trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\") ? path18.join(os10.homedir(), trimmed.slice(2)) : trimmed;
36306
37001
  return path18.normalize(path18.resolve(expanded));
36307
37002
  }
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
37003
  function normalizeClipboardIdentityKey(value) {
36327
37004
  return process.platform === "win32" ? value.toLowerCase() : value;
36328
37005
  }
@@ -36360,18 +37037,15 @@ function mimeTypeForFileName(fileName) {
36360
37037
  }
36361
37038
  function parseWindowsClipboardResult(stdout) {
36362
37039
  const raw = stdout.trim();
36363
- if (!raw) return { files: [], text: "" };
37040
+ if (!raw) return { files: [] };
36364
37041
  try {
36365
37042
  const parsed = JSON.parse(raw);
36366
- if (!isRecord5(parsed)) return { files: [], text: "" };
37043
+ if (!isRecord5(parsed)) return { files: [] };
36367
37044
  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
- };
37045
+ return { files };
36372
37046
  } catch (e) {
36373
37047
  logger24.debug("Windows clipboard JSON parse skipped", { error: e });
36374
- return { files: clipboardTextToPathCandidates(stdout), text: "" };
37048
+ return { files: [] };
36375
37049
  }
36376
37050
  }
36377
37051
  async function readWindowsClipboardPathCandidates() {
@@ -36384,9 +37058,7 @@ async function readWindowsClipboardPathCandidates() {
36384
37058
  " $drop = [System.Windows.Forms.Clipboard]::GetFileDropList();",
36385
37059
  " foreach ($file in $drop) { $files += [string]$file }",
36386
37060
  "} catch {}",
36387
- '$text = "";',
36388
- "try { $text = [System.Windows.Forms.Clipboard]::GetText() } catch {}",
36389
- "[pscustomobject]@{ files = $files; text = $text } | ConvertTo-Json -Compress;"
37061
+ "[pscustomobject]@{ files = $files } | ConvertTo-Json -Compress;"
36390
37062
  ].join(" ");
36391
37063
  try {
36392
37064
  const { stdout } = await execFileAsync2("powershell.exe", [
@@ -36404,7 +37076,7 @@ async function readWindowsClipboardPathCandidates() {
36404
37076
  maxBuffer: 1024 * 1024
36405
37077
  });
36406
37078
  const result = parseWindowsClipboardResult(stdout);
36407
- return [...result.files, ...clipboardTextToPathCandidates(result.text)];
37079
+ return result.files;
36408
37080
  } catch (e) {
36409
37081
  logger24.debug("Windows clipboard file read skipped", { error: e });
36410
37082
  return [];
@@ -36807,7 +37479,7 @@ async function readStreamText(filePath, start) {
36807
37479
  function parseProcessedLines(raw, cursor, fileName, fingerprint) {
36808
37480
  const lastNewline = raw.lastIndexOf("\n");
36809
37481
  if (lastNewline < 0) {
36810
- return { entries: [], nextCursor: cursor, advanced: false };
37482
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "partial_line" };
36811
37483
  }
36812
37484
  const processed = raw.slice(0, lastNewline + 1);
36813
37485
  const lines = processed.split(/\r?\n/);
@@ -36833,7 +37505,8 @@ function parseProcessedLines(raw, cursor, fileName, fingerprint) {
36833
37505
  lineNum,
36834
37506
  ...fingerprint ? { fingerprint } : {}
36835
37507
  },
36836
- advanced: true
37508
+ advanced: true,
37509
+ reason: "advanced"
36837
37510
  };
36838
37511
  }
36839
37512
  function chunkEntries(entries, size) {
@@ -36891,24 +37564,55 @@ var BridgeLogUploader = class {
36891
37564
  async flushOnce() {
36892
37565
  if (this.running || this.stopped) return;
36893
37566
  this.running = true;
37567
+ const startedAt = Date.now();
37568
+ const summary = {
37569
+ targetCount: 0,
37570
+ advancedTargetCount: 0,
37571
+ missingTargetCount: 0,
37572
+ idleTargetCount: 0,
37573
+ partialLineTargetCount: 0,
37574
+ failedTargetCount: 0,
37575
+ parsedEntryCount: 0,
37576
+ bridgeEntryCount: 0,
37577
+ uploadedChunkCount: 0,
37578
+ accepted: 0,
37579
+ skipped: 0
37580
+ };
36894
37581
  try {
36895
37582
  const targets = await this.resolveTargets();
37583
+ summary.targetCount = targets.length;
36896
37584
  for (const target of targets) {
36897
37585
  try {
36898
37586
  const cursor = await readCursor(target.cursorFile);
36899
37587
  const batch = await this.readNewEntries(target, cursor);
36900
- if (!batch.advanced) continue;
37588
+ if (!batch.advanced) {
37589
+ summary.idleTargetCount += 1;
37590
+ if (batch.reason === "missing_file") summary.missingTargetCount += 1;
37591
+ if (batch.reason === "partial_line") summary.partialLineTargetCount += 1;
37592
+ continue;
37593
+ }
37594
+ summary.advancedTargetCount += 1;
37595
+ summary.parsedEntryCount += batch.entries.length;
36901
37596
  if (batch.entries.length > 0) {
36902
- await this.uploadEntries(batch.entries);
37597
+ const result = await this.uploadEntries(batch.entries);
37598
+ summary.bridgeEntryCount += result.bridgeEntryCount;
37599
+ summary.uploadedChunkCount += result.uploadedChunkCount;
37600
+ summary.accepted += result.accepted;
37601
+ summary.skipped += result.skipped;
36903
37602
  }
36904
37603
  await writeCursor(target.cursorFile, batch.nextCursor);
36905
37604
  } catch (e) {
37605
+ summary.failedTargetCount += 1;
36906
37606
  logger27.warn("Bridge log upload target failed", { error: e, logFile: target.logFile });
36907
37607
  }
36908
37608
  }
36909
37609
  } catch (e) {
36910
37610
  logger27.warn("Bridge log upload cycle failed", { error: e });
36911
37611
  } finally {
37612
+ logger27.info("Bridge log upload cycle summary", {
37613
+ ...summary,
37614
+ durationMs: Date.now() - startedAt
37615
+ });
36912
37616
  this.running = false;
36913
37617
  }
36914
37618
  }
@@ -36931,7 +37635,7 @@ var BridgeLogUploader = class {
36931
37635
  } catch (e) {
36932
37636
  if (e instanceof Error && "code" in e && e.code === "ENOENT") {
36933
37637
  logger27.debug("Bridge log file not found for upload yet", { logFile: target.logFile });
36934
- return { entries: [], nextCursor: cursor, advanced: false };
37638
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "missing_file" };
36935
37639
  }
36936
37640
  throw e;
36937
37641
  }
@@ -36939,13 +37643,19 @@ var BridgeLogUploader = class {
36939
37643
  const samePhysicalFile = !fingerprint || !cursor.fingerprint || cursor.fingerprint === fingerprint;
36940
37644
  const normalizedCursor = stat3.size < cursor.offset || !samePhysicalFile ? { offset: 0, lineNum: 0, ...fingerprint ? { fingerprint } : {} } : { ...cursor, ...fingerprint ? { fingerprint } : {} };
36941
37645
  if (stat3.size <= normalizedCursor.offset) {
36942
- return { entries: [], nextCursor: normalizedCursor, advanced: false };
37646
+ return { entries: [], nextCursor: normalizedCursor, advanced: false, reason: "no_new_entries" };
36943
37647
  }
36944
37648
  const raw = await readStreamText(target.logFile, normalizedCursor.offset);
36945
37649
  return parseProcessedLines(raw, normalizedCursor, target.uploadedFileName, fingerprint);
36946
37650
  }
36947
37651
  async uploadEntries(entries) {
36948
37652
  const bridgeEntries = entries.filter((entry) => entry.source === "bridge");
37653
+ const result = {
37654
+ bridgeEntryCount: bridgeEntries.length,
37655
+ uploadedChunkCount: 0,
37656
+ accepted: 0,
37657
+ skipped: 0
37658
+ };
36949
37659
  for (const chunk of chunkEntries(bridgeEntries, this.options.batchSize)) {
36950
37660
  const res = await fetch(`${this.options.serverApiUrl}/api/logs/upload`, {
36951
37661
  method: "POST",
@@ -36960,14 +37670,18 @@ var BridgeLogUploader = class {
36960
37670
  })
36961
37671
  });
36962
37672
  if (!res.ok) {
36963
- const body = await res.text().catch((e) => {
37673
+ const body2 = await res.text().catch((e) => {
36964
37674
  logger27.debug("Failed to read log upload error body", { error: e });
36965
37675
  return "";
36966
37676
  });
36967
- throw new Error(`upload failed HTTP ${res.status}: ${body.slice(0, 160)}`);
37677
+ throw new Error(`upload failed HTTP ${res.status}: ${body2.slice(0, 160)}`);
36968
37678
  }
36969
- await res.json();
37679
+ const body = await res.json();
37680
+ result.uploadedChunkCount += 1;
37681
+ result.accepted += typeof body.accepted === "number" ? body.accepted : 0;
37682
+ result.skipped += typeof body.skipped === "number" ? body.skipped : 0;
36970
37683
  }
37684
+ return result;
36971
37685
  }
36972
37686
  };
36973
37687
 
@@ -38052,8 +38766,10 @@ function resolvePnpmRuntimeBinary(sdkDir, target) {
38052
38766
  } catch {
38053
38767
  return void 0;
38054
38768
  }
38769
+ const matches = [];
38055
38770
  for (const entry of entries) {
38056
38771
  if (!entry.startsWith(`${encodedName}@`)) continue;
38772
+ const version2 = entry.slice(encodedName.length + 1);
38057
38773
  const candidate = path27.join(
38058
38774
  pnpmStoreDir,
38059
38775
  entry,
@@ -38061,9 +38777,22 @@ function resolvePnpmRuntimeBinary(sdkDir, target) {
38061
38777
  ...target.packageName.split("/"),
38062
38778
  target.binaryName
38063
38779
  );
38064
- if (existsSync2(candidate)) return candidate;
38780
+ if (existsSync2(candidate)) matches.push({ version: version2, candidate });
38065
38781
  }
38066
- return void 0;
38782
+ if (matches.length === 0) return void 0;
38783
+ matches.sort((a, b) => compareRuntimeVersion(b.version, a.version));
38784
+ return matches[0].candidate;
38785
+ }
38786
+ function compareRuntimeVersion(a, b) {
38787
+ const parse3 = (v) => v.split(/[.+_-]/).map((n) => Number.parseInt(n, 10));
38788
+ const pa = parse3(a);
38789
+ const pb = parse3(b);
38790
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
38791
+ const da = Number.isFinite(pa[i]) ? pa[i] : 0;
38792
+ const db = Number.isFinite(pb[i]) ? pb[i] : 0;
38793
+ if (da !== db) return da - db;
38794
+ }
38795
+ return 0;
38067
38796
  }
38068
38797
  function resolveSdkRuntimeBinary(target) {
38069
38798
  const directPath = safeResolve(`${target.packageName}/${target.binaryName}`);
@@ -39222,6 +39951,32 @@ function syncLocalRuntimeSkills(skillStore, localSkills, options = {}) {
39222
39951
  ]);
39223
39952
  }
39224
39953
 
39954
+ // src/processOutput.ts
39955
+ var protectedStreams2 = /* @__PURE__ */ new WeakSet();
39956
+ var reportedErrors = /* @__PURE__ */ new WeakSet();
39957
+ function reportWriteError(error51, onError) {
39958
+ if (typeof error51 === "object" && error51 !== null) {
39959
+ if (reportedErrors.has(error51)) return;
39960
+ reportedErrors.add(error51);
39961
+ }
39962
+ onError(error51);
39963
+ }
39964
+ function safeWriteProcessOutput(stream, text, onError) {
39965
+ if (!stream) return;
39966
+ if (stream.destroyed || stream.writableEnded) return;
39967
+ if (typeof stream === "object" && typeof stream.on === "function" && !protectedStreams2.has(stream)) {
39968
+ protectedStreams2.add(stream);
39969
+ stream.on("error", (e) => reportWriteError(e, onError));
39970
+ }
39971
+ try {
39972
+ stream.write(text, (error51) => {
39973
+ if (error51) reportWriteError(error51, onError);
39974
+ });
39975
+ } catch (e) {
39976
+ reportWriteError(e, onError);
39977
+ }
39978
+ }
39979
+
39225
39980
  // src/start.ts
39226
39981
  var logger41 = createModuleLogger("bridge");
39227
39982
  var NODE_USER_UID2 = 1e3;
@@ -39308,14 +40063,16 @@ async function startBridge(config2) {
39308
40063
  const claudeRuntime = resolveClaudeRuntime();
39309
40064
  logClaudeRuntimeResolution(claudeRuntime);
39310
40065
  if (!claudeRuntime.ok || !claudeRuntime.path) {
39311
- process.stderr.write(
40066
+ safeWriteProcessOutput(
40067
+ process.stderr,
39312
40068
  `
39313
40069
  Claude runtime is unavailable.
39314
40070
 
39315
40071
  ${claudeRuntime.error ?? "Install Claude Code manually or use the bundled desktop runtime."}
39316
40072
 
39317
40073
  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
- `
40074
+ `,
40075
+ (e) => logger41.error("Bridge process stderr write failed", { error: e })
39319
40076
  );
39320
40077
  process.exit(1);
39321
40078
  }
@@ -39372,12 +40129,14 @@ Reinstall @fangyb/ahchat-bridge with npm optional dependencies, set AHCHAT_CLAUD
39372
40129
  claudeRuntimeVersion: claudeRuntime.version ?? null
39373
40130
  });
39374
40131
  const shouldPrintRawBridgeToken = process.stdout.isTTY && process.env.AHCHAT_SUPPRESS_BRIDGE_TOKEN_STDOUT !== "1";
39375
- process.stdout.write(
40132
+ safeWriteProcessOutput(
40133
+ process.stdout,
39376
40134
  `
39377
40135
  Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\u673A\u5668):
39378
40136
  ${shouldPrintRawBridgeToken ? config2.bridgeToken : "***"}
39379
40137
 
39380
- `
40138
+ `,
40139
+ (e) => logger41.error("Bridge process stdout write failed", { error: e })
39381
40140
  );
39382
40141
  wsMetrics.start(5e3);
39383
40142
  const sessionStore = new SessionStore(config2.dataDir);
@@ -40176,6 +40935,19 @@ Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\
40176
40935
  });
40177
40936
  await agentManager.terminateScope(msg.payload.agentId, msg.payload.scope);
40178
40937
  break;
40938
+ case "spectate:set":
40939
+ logger41.info("spectate:set received", {
40940
+ agentId: msg.payload.agentId,
40941
+ scope: msg.payload.scope,
40942
+ action: msg.payload.action,
40943
+ traceId: msg.payload.traceId
40944
+ });
40945
+ await agentManager.setSpectate(
40946
+ msg.payload.agentId,
40947
+ msg.payload.scope,
40948
+ msg.payload.action
40949
+ );
40950
+ break;
40179
40951
  case "agent:created":
40180
40952
  agentRegistry.upsert(msg.payload.agent);
40181
40953
  ensureLocalWorkdirPath(msg.payload.agent.workingDirectory, "agent:created", {
@@ -40392,8 +41164,12 @@ function writeAlreadyRunningMessage(error51) {
40392
41164
  ` ${buildStopCommand(error51.pid)}`,
40393
41165
  ""
40394
41166
  ];
40395
- process.stdout.write(`${lines.join("\n")}
40396
- `);
41167
+ safeWriteProcessOutput(
41168
+ process.stdout,
41169
+ `${lines.join("\n")}
41170
+ `,
41171
+ (e) => logger42.error("Bridge already-running message write failed", { error: e })
41172
+ );
40397
41173
  }
40398
41174
  function handleBridgeStartError(e, message) {
40399
41175
  if (isBridgeAlreadyRunningError(e)) {