@fangyb/ahchat-bridge 0.1.34 → 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
  }
@@ -5960,6 +6034,24 @@ function assertNumberPayloadField(type, payload, field) {
5960
6034
  throw invalidWsMessage(type, field);
5961
6035
  }
5962
6036
  }
6037
+ function assertOptionalNumberPayloadField(type, payload, field) {
6038
+ if (payload[field] === void 0) return;
6039
+ assertNumberPayloadField(type, payload, field);
6040
+ }
6041
+ function assertNullableStringPayloadField(type, payload, field) {
6042
+ if (payload[field] === null) return;
6043
+ assertStringPayloadField(type, payload, field);
6044
+ }
6045
+ function assertStringPayloadOneOf(type, payload, field, allowed) {
6046
+ assertStringPayloadField(type, payload, field);
6047
+ if (!allowed.includes(payload[field])) {
6048
+ throw invalidWsMessage(type, field);
6049
+ }
6050
+ }
6051
+ function assertOptionalStringPayloadOneOf(type, payload, field, allowed) {
6052
+ if (payload[field] === void 0) return;
6053
+ assertStringPayloadOneOf(type, payload, field, allowed);
6054
+ }
5963
6055
  function assertArrayPayloadField(type, payload, field) {
5964
6056
  if (!Array.isArray(payload[field])) {
5965
6057
  throw invalidWsMessage(type, field);
@@ -6040,6 +6132,25 @@ function validateWSMessageShape(msg) {
6040
6132
  case "agent:error": {
6041
6133
  assertPayloadRecord(type, payload);
6042
6134
  validateRequiredStrings(type, payload, ["ackId", "agentId", "conversationId", "error", "traceId"]);
6135
+ assertOptionalStringPayloadOneOf(type, payload, "reason", ["user_quota_exceeded", "company_quota_exceeded"]);
6136
+ return;
6137
+ }
6138
+ case "directory:register": {
6139
+ assertPayloadRecord(type, payload);
6140
+ validateRequiredStrings(type, payload, ["handle", "viaChild", "traceId"]);
6141
+ assertStringPayloadOneOf(type, payload, "op", ["add", "remove", "move"]);
6142
+ return;
6143
+ }
6144
+ case "directory:resolve": {
6145
+ assertPayloadRecord(type, payload);
6146
+ validateRequiredStrings(type, payload, ["handle", "fromNode", "traceId"]);
6147
+ assertOptionalNumberPayloadField(type, payload, "hop");
6148
+ return;
6149
+ }
6150
+ case "directory:resolve_result": {
6151
+ assertPayloadRecord(type, payload);
6152
+ validateRequiredStrings(type, payload, ["handle", "traceId"]);
6153
+ assertNullableStringPayloadField(type, payload, "canonicalAddress");
6043
6154
  return;
6044
6155
  }
6045
6156
  default:
@@ -8017,8 +8128,8 @@ var VOLCENGINE_SEEDANCE_MCP_PROVIDER = {
8017
8128
  };
8018
8129
  var OFFICIAL_MCP_PROVIDERS = [
8019
8130
  ...ALIYUN_IQS_MCP_PROVIDERS,
8020
- VOLCENGINE_SEEDANCE_MCP_PROVIDER,
8021
- VOLCENGINE_SEEDREAM_MCP_PROVIDER
8131
+ VOLCENGINE_SEEDREAM_MCP_PROVIDER,
8132
+ VOLCENGINE_SEEDANCE_MCP_PROVIDER
8022
8133
  ];
8023
8134
  var MCP_STORE_PROVIDERS = [
8024
8135
  {
@@ -8860,8 +8971,7 @@ var AskQuestionRegistry = class {
8860
8971
  questionId,
8861
8972
  agentId: entry.agentId,
8862
8973
  waitedMs: Date.now() - entry.askedAt,
8863
- answerLen: answerText2.length,
8864
- answerSample: answerText2.slice(0, 200)
8974
+ answerLen: answerText2.length
8865
8975
  });
8866
8976
  entry.resolve(answerText2);
8867
8977
  return true;
@@ -9002,7 +9112,7 @@ function makeAskUserQuestionGuard(deps) {
9002
9112
  bundleIndex,
9003
9113
  bundleSize,
9004
9114
  replyMessageId: task.replyMessageId,
9005
- question: q.question.slice(0, 200),
9115
+ questionLen: q.question.length,
9006
9116
  optionCount: options.length,
9007
9117
  multiSelect,
9008
9118
  traceId: task.traceId
@@ -9101,7 +9211,7 @@ function makeAskUserQuestionGuard(deps) {
9101
9211
  bundleId,
9102
9212
  bundleSize,
9103
9213
  replyMessageId: task.replyMessageId,
9104
- combinedSample: combined.slice(0, 200),
9214
+ combinedLen: combined.length,
9105
9215
  traceId: task.traceId
9106
9216
  });
9107
9217
  return { behavior: "deny", message: combined };
@@ -24124,6 +24234,17 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
24124
24234
  ".yaml",
24125
24235
  ".yml"
24126
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;
24127
24248
  var DEFAULT_MAX_CHARS = 5e5;
24128
24249
  var DEFAULT_TIMEOUT_MS = 45e3;
24129
24250
  function isReadableDocumentPath(filePath) {
@@ -24143,9 +24264,6 @@ function resolveDocumentPath(inputPath, cwd) {
24143
24264
  async function readDocumentAsMarkdown(inputPath, opts = {}) {
24144
24265
  const resolvedPath = opts.cwd ? resolveDocumentPath(inputPath, opts.cwd) : path9.resolve(resolveUserPath(inputPath));
24145
24266
  const ext = path9.extname(resolvedPath).toLowerCase();
24146
- if (!isReadableDocumentPath(resolvedPath)) {
24147
- throw new Error(`unsupported document type: ${ext || "(no extension)"}`);
24148
- }
24149
24267
  const stat3 = await fs4.stat(resolvedPath);
24150
24268
  if (!stat3.isFile()) throw new Error("path is not a file");
24151
24269
  const warnings = [];
@@ -24154,8 +24272,10 @@ async function readDocumentAsMarkdown(inputPath, opts = {}) {
24154
24272
  markdown = await fs4.readFile(resolvedPath, "utf-8");
24155
24273
  } else if (ext === ".xls") {
24156
24274
  markdown = await convertLegacyExcelDocument(resolvedPath);
24157
- } else {
24275
+ } else if (OFFICE_DOCUMENT_EXTENSIONS.has(ext)) {
24158
24276
  markdown = await convertOfficeDocument(resolvedPath, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
24277
+ } else {
24278
+ markdown = await readPlainTextDocument(resolvedPath, ext);
24159
24279
  }
24160
24280
  markdown = normalizeDocumentText(markdown);
24161
24281
  const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
@@ -24294,6 +24414,14 @@ async function convertDocxWithOfficeCli(filePath, timeoutMs) {
24294
24414
  });
24295
24415
  return text;
24296
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
+ }
24297
24425
  async function convertLegacyExcelDocument(filePath) {
24298
24426
  const XLSX = await import("./xlsx-E4ZR5JHK.js");
24299
24427
  const workbook = XLSX.readFile(filePath, { cellDates: true });
@@ -24763,8 +24891,7 @@ async function createNeuralMcpServer(deps) {
24763
24891
  agentId: deps.agentId,
24764
24892
  fromScope: currentScopeKey,
24765
24893
  rawTargetScope: args.target_scope,
24766
- messageLen: args.message.length,
24767
- messageSample: args.message.slice(0, 120)
24894
+ messageLen: args.message.length
24768
24895
  });
24769
24896
  const trimmed = args.message.trim();
24770
24897
  if (!trimmed) {
@@ -24858,7 +24985,7 @@ async function createNeuralMcpServer(deps) {
24858
24985
  toScope: resolvedKey,
24859
24986
  repeatsInWindow: sendHistory.length,
24860
24987
  windowMs: NEURAL_DEDUP_WINDOW_MS,
24861
- messageSample: trimmed.slice(0, 120)
24988
+ messageLen: trimmed.length
24862
24989
  });
24863
24990
  return {
24864
24991
  content: [{
@@ -24885,8 +25012,7 @@ async function createNeuralMcpServer(deps) {
24885
25012
  agentId: deps.agentId,
24886
25013
  fromScope: currentScopeKey,
24887
25014
  toScope: resolvedKey,
24888
- messageLen: trimmed.length,
24889
- messageSample: trimmed.slice(0, 120)
25015
+ messageLen: trimmed.length
24890
25016
  });
24891
25017
  return {
24892
25018
  content: [{ type: "text", text: `[neural_send] \u5DF2\u9001\u8FBE\u5230\u300C${toLabel}\u300D(scope: ${resolvedKey})\u3002` }]
@@ -25243,6 +25369,7 @@ action="append" \u8FFD\u52A0\u65B0\u5185\u5BB9\uFF08\u6700\u5E38\u7528\uFF0Ccont
25243
25369
  `Read a document from the current working directory and return extracted Markdown text.
25244
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.
25245
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.
25246
25373
  Pass either a relative path from the current working directory or an absolute path inside it.`,
25247
25374
  {
25248
25375
  path: external_exports.string().min(1).describe("Document path, relative to the current working directory or absolute inside it."),
@@ -27934,7 +28061,11 @@ function parseJsonRecord(text) {
27934
28061
  try {
27935
28062
  const parsed = JSON.parse(text);
27936
28063
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
27937
- } catch {
28064
+ } catch (error51) {
28065
+ logger10.warn("SDK tool result output was not JSON", {
28066
+ error: error51,
28067
+ outputLength: text.length
28068
+ });
27938
28069
  return null;
27939
28070
  }
27940
28071
  }
@@ -28296,6 +28427,9 @@ function emitUsageReported(proc, emit, base, usage, messageId) {
28296
28427
  function isGroupTask(proc) {
28297
28428
  return proc.currentTask?.groupId != null;
28298
28429
  }
28430
+ function shouldStreamInternals(proc) {
28431
+ return !isGroupTask(proc) || proc.spectating === true;
28432
+ }
28299
28433
  function extractTodosFromInput(input) {
28300
28434
  if (!input || typeof input !== "object") return null;
28301
28435
  const raw = input.todos;
@@ -28427,7 +28561,6 @@ function emitGroupSegment(proc, emit, base, content, contentBlocks, isSilent = f
28427
28561
  contentLen: content.length,
28428
28562
  blockCount: contentBlocks.length,
28429
28563
  blockTypes: contentBlocks.map((b) => b.type),
28430
- contentSample: content.slice(0, 200),
28431
28564
  traceId: base.traceId,
28432
28565
  isAuditOnly: content.length === 0,
28433
28566
  isSilent
@@ -28483,9 +28616,24 @@ function flushTextSegmentOnBlockStop(proc, emit, base) {
28483
28616
  }
28484
28617
  proc.segmentBuffer = "";
28485
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
+ }
28486
28632
  function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProviderApiError) {
28487
28633
  const emit = rawEmit;
28488
28634
  proc.lastSdkEventAt = Date.now();
28635
+ proc.lastSdkEventInfo = describeSdkEvent(message);
28636
+ proc.stallWarned = false;
28489
28637
  switch (message.type) {
28490
28638
  case "system": {
28491
28639
  const sysMsg = message;
@@ -28524,11 +28672,29 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28524
28672
  sessionId: proc.ccSessionId
28525
28673
  });
28526
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
+ }
28527
28686
  logger10.info("SDK system subtype unhandled", {
28528
28687
  agentId: proc.agentId,
28529
28688
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
28530
28689
  subtype: sysMsg.subtype ?? "(none)",
28531
- 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)
28532
28698
  });
28533
28699
  }
28534
28700
  break;
@@ -28554,13 +28720,14 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28554
28720
  } else if (block.type === "tool_use") {
28555
28721
  proc.currentBlockType = "tool_use";
28556
28722
  proc.currentToolName = block.name ?? "unknown";
28723
+ proc.activeToolUseStartedAt = Date.now();
28557
28724
  proc.accumulatedToolInput = "";
28558
28725
  const toolName = block.name ?? "unknown";
28559
28726
  proc.suppressCurrentToolUse = proc.officialMediaGenerationSatisfied === true && isOfficialMediaGenerationToolName(toolName);
28560
28727
  const isMcpTool = parseMcpRuntimeToolName(toolName) != null;
28561
28728
  proc.currentMcpInvocationId = isMcpTool ? createMcpToolInvocationId() : null;
28562
28729
  proc.currentMcpInvocationStartedAt = isMcpTool ? (/* @__PURE__ */ new Date()).toISOString() : null;
28563
- if (!isGroupTask(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
28730
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
28564
28731
  emit({
28565
28732
  type: "agent:tool_use",
28566
28733
  payload: {
@@ -28585,7 +28752,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28585
28752
  if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
28586
28753
  if (proc.suppressCurrentThinking) break;
28587
28754
  proc.accumulatedThinking += delta.thinking;
28588
- if (!isGroupTask(proc)) {
28755
+ if (shouldStreamInternals(proc)) {
28589
28756
  emit({
28590
28757
  type: "agent:thinking_chunk",
28591
28758
  payload: { ...wireBase(base), chunk: delta.thinking }
@@ -28596,7 +28763,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28596
28763
  if (typeof partial2 === "string") {
28597
28764
  proc.accumulatedToolInput += partial2;
28598
28765
  const liveInput = extractLiveToolInput(proc.currentToolName, proc.accumulatedToolInput);
28599
- if (!isGroupTask(proc) && liveInput && proc.currentToolName != null) {
28766
+ if (shouldStreamInternals(proc) && liveInput && proc.currentToolName != null) {
28600
28767
  emit({
28601
28768
  type: "agent:tool_input_update",
28602
28769
  payload: {
@@ -28630,7 +28797,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28630
28797
  }
28631
28798
  case "content_block_stop": {
28632
28799
  if (proc.currentBlockType === "thinking") {
28633
- if (!isGroupTask(proc) && !proc.suppressCurrentThinking) {
28800
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentThinking) {
28634
28801
  emit({
28635
28802
  type: "agent:thinking_done",
28636
28803
  payload: wireBase(getTaskBase(proc))
@@ -28650,12 +28817,12 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28650
28817
  if (proc.accumulatedToolInput.length > 0) {
28651
28818
  try {
28652
28819
  parsedInput = JSON.parse(proc.accumulatedToolInput);
28653
- } catch {
28820
+ } catch (error51) {
28654
28821
  logger10.warn("Failed to parse tool input JSON", {
28822
+ error: error51,
28655
28823
  agentId: proc.agentId,
28656
28824
  toolName: proc.currentToolName,
28657
- inputLen: proc.accumulatedToolInput.length,
28658
- sample: proc.accumulatedToolInput.slice(0, 200)
28825
+ inputLen: proc.accumulatedToolInput.length
28659
28826
  });
28660
28827
  }
28661
28828
  }
@@ -28664,7 +28831,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28664
28831
  if (lastToolUse && lastToolUse.type === "tool_use") {
28665
28832
  lastToolUse.input = parsedInput;
28666
28833
  }
28667
- 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) {
28668
28835
  emit({
28669
28836
  type: "agent:tool_input_update",
28670
28837
  payload: {
@@ -28788,7 +28955,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28788
28955
  blockTypes,
28789
28956
  hasToolResult,
28790
28957
  hasPlainText,
28791
- 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
28792
28959
  });
28793
28960
  break;
28794
28961
  }
@@ -28797,7 +28964,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28797
28964
  agentId: proc.agentId,
28798
28965
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
28799
28966
  blockTypes,
28800
- contentSample: JSON.stringify(content).slice(0, 300),
28967
+ contentLen: JSON.stringify(content).length,
28801
28968
  replyMessageId: base.replyMessageId
28802
28969
  });
28803
28970
  }
@@ -28818,14 +28985,15 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28818
28985
  });
28819
28986
  proc.currentMcpInvocationId = null;
28820
28987
  proc.currentMcpInvocationStartedAt = null;
28988
+ proc.activeToolUseStartedAt = void 0;
28821
28989
  proc.currentToolName = null;
28822
28990
  continue;
28823
28991
  }
28824
28992
  if (isSuccessfulOfficialMediaOutput(toolName, output)) {
28825
28993
  proc.officialMediaGenerationSatisfied = true;
28826
- proc.officialMediaSessionRecycleRequested = true;
28827
28994
  }
28828
28995
  if (isAskUserQuestionToolName(toolName)) {
28996
+ proc.activeToolUseStartedAt = void 0;
28829
28997
  proc.currentToolName = null;
28830
28998
  continue;
28831
28999
  }
@@ -28853,7 +29021,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28853
29021
  proc.currentMcpInvocationId = null;
28854
29022
  proc.currentMcpInvocationStartedAt = null;
28855
29023
  }
28856
- if (!isGroupTask(proc)) {
29024
+ if (shouldStreamInternals(proc)) {
28857
29025
  emit({
28858
29026
  type: "agent:tool_result",
28859
29027
  payload: {
@@ -28877,6 +29045,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28877
29045
  }
28878
29046
  }
28879
29047
  }
29048
+ proc.activeToolUseStartedAt = void 0;
28880
29049
  }
28881
29050
  }
28882
29051
  }
@@ -28955,7 +29124,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28955
29124
  groupId,
28956
29125
  compactScheduled: proc.compactRequested === true,
28957
29126
  fullTextLen: proc.accumulatedText.length,
28958
- fullTextSample: proc.accumulatedText.slice(0, 200),
28959
29127
  accumulatedBlockCount: proc.contentBlocks.length,
28960
29128
  accumulatedBlockTypes: proc.contentBlocks.map((b) => b.type),
28961
29129
  silentSegmentEmitted: groupMode
@@ -28999,7 +29167,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
28999
29167
  segmentCount: proc.segmentCount,
29000
29168
  compactScheduled: proc.compactRequested === true,
29001
29169
  fullTextLen: proc.accumulatedText.length,
29002
- fullTextSample: proc.accumulatedText.slice(0, 200),
29003
29170
  traceId: base.traceId
29004
29171
  });
29005
29172
  emit({
@@ -29050,7 +29217,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
29050
29217
  ackId: base.replyMessageId,
29051
29218
  messageId: carrierMessageId,
29052
29219
  textLen: proc.accumulatedText.length,
29053
- textSample: proc.accumulatedText.slice(0, 200),
29054
29220
  tokenCount: usage.tokenCount,
29055
29221
  traceId: base.traceId
29056
29222
  });
@@ -29243,8 +29409,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
29243
29409
  logger10.info("Captured non-streamed assistant message", {
29244
29410
  agentId: proc.agentId,
29245
29411
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
29246
- textLen: text.length,
29247
- textSample: text.slice(0, 100)
29412
+ textLen: text.length
29248
29413
  });
29249
29414
  } else {
29250
29415
  proc.lastAssistantContentDescription = describeAssistantContent(am.message?.content);
@@ -29268,6 +29433,7 @@ function resetAccumulators(proc) {
29268
29433
  proc.currentToolName = null;
29269
29434
  proc.currentMcpInvocationId = null;
29270
29435
  proc.currentMcpInvocationStartedAt = null;
29436
+ proc.activeToolUseStartedAt = void 0;
29271
29437
  proc.segmentBuffer = "";
29272
29438
  proc.segmentCount = 0;
29273
29439
  proc.accumulatedToolInput = "";
@@ -29277,6 +29443,7 @@ function resetAccumulators(proc) {
29277
29443
  proc.officialMediaGenerationSatisfied = false;
29278
29444
  proc.suppressCurrentThinking = false;
29279
29445
  proc.suppressCurrentToolUse = false;
29446
+ proc.activeSubagentTaskIds?.clear();
29280
29447
  }
29281
29448
 
29282
29449
  // src/forkHistoryReplay.ts
@@ -29464,7 +29631,7 @@ function missingSubscriptionMessage(subscriptionId) {
29464
29631
  }
29465
29632
  var NODE_USER_UID = 1e3;
29466
29633
  var POST_MERGE_CONTINUATION_ROUTE_MS = 15e3;
29467
- var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-prompt-v2";
29634
+ var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-mcp-abi-prompt-v4";
29468
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;
29469
29636
  var DOCUMENT_READING_RULES = `DOCUMENT READING:
29470
29637
  - The built-in Read tool cannot read binary office documents such as .docx, .xls, .xlsx, .pptx, .pdf, .odt, .ods, .odp, or .rtf.
@@ -29479,6 +29646,16 @@ var MEDIA_GENERATION_RULES = `MEDIA GENERATION:
29479
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.
29480
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.
29481
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
+ }
29482
29659
  function isRecoveryDispatchTask(task) {
29483
29660
  return task.dispatchKind === "manual_continue" || task.dispatchKind === "regenerate";
29484
29661
  }
@@ -29736,13 +29913,15 @@ var AgentManager = class {
29736
29913
  agents = /* @__PURE__ */ new Map();
29737
29914
  lastUsedAt = /* @__PURE__ */ new Map();
29738
29915
  /** Scopes 被 zombie_watchdog 关闭后的"入睡"标记,acquire 重建时清除并 emit awake。 */
29739
- dormantScopes = /* @__PURE__ */ new Set();
29916
+ dormantScopes = /* @__PURE__ */ new Map();
29740
29917
  /**
29741
29918
  * zombie_watchdog 拆 runtime 时,把该 (agentId, scope) 的 groupInbox 快照到这里,
29742
29919
  * 让下一次 getOrCreate 重建 runtime 时可以恢复未读消息。仅 in-memory;
29743
29920
  * bridge 进程崩溃 / shutdownAll 时丢失,与现有 inbox 内存语义一致。
29744
29921
  */
29745
29922
  dormantGroupInboxes = /* @__PURE__ */ new Map();
29923
+ /** Spectate requested before runtime existed; value = activatedAt epoch ms. */
29924
+ pendingSpectate = /* @__PURE__ */ new Map();
29746
29925
  sessionStore;
29747
29926
  dispatchMemory = new GroupDispatchMemoryStore();
29748
29927
  dataDir;
@@ -29908,6 +30087,7 @@ var AgentManager = class {
29908
30087
  }
29909
30088
  async resolveRuntimeCwd(agentConfig, scope, requestedCwd) {
29910
30089
  let cwd = this.remapServerWorkspaceCwd(agentConfig, scope, requestedCwd);
30090
+ let fallbackForensicsId;
29911
30091
  if (!isFullyQualifiedAbsolutePath(cwd)) {
29912
30092
  const fallback = path13.join(this.workspacesDir, this.localScopeDirName(agentConfig, scope));
29913
30093
  logger13.error(
@@ -29922,6 +30102,23 @@ var AgentManager = class {
29922
30102
  error: new Error("workdir_not_usable_on_this_machine")
29923
30103
  }
29924
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
+ });
29925
30122
  cwd = fallback;
29926
30123
  }
29927
30124
  if (isRunningAsRoot() && cwd.startsWith("/root/")) {
@@ -29940,6 +30137,21 @@ var AgentManager = class {
29940
30137
  fallback,
29941
30138
  error: e
29942
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
+ });
29943
30155
  await fs6.mkdir(fallback, { recursive: true });
29944
30156
  return fallback;
29945
30157
  }
@@ -30162,17 +30374,24 @@ var AgentManager = class {
30162
30374
  });
30163
30375
  return null;
30164
30376
  }
30165
- scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection) {
30166
- 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");
30379
+ }
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 });
30167
30386
  }
30168
- discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint) {
30387
+ discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint, options = {}) {
30169
30388
  const previous = this.sessionStore.getPromptFingerprint(agentConfig.id, scope);
30170
30389
  if (!sessionId) {
30171
30390
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
30172
30391
  return null;
30173
30392
  }
30174
30393
  if (previous === fingerprint) return sessionId;
30175
- if (!previous && scope.kind === "single") {
30394
+ if (!previous && scope.kind === "single" && options.clearLegacySingleSession !== true) {
30176
30395
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
30177
30396
  logger13.info("Retaining legacy single-scope session while recording prompt fingerprint", {
30178
30397
  agentId: agentConfig.id,
@@ -30192,7 +30411,8 @@ var AgentManager = class {
30192
30411
  sessionId,
30193
30412
  previousFingerprint: previous,
30194
30413
  nextFingerprint: fingerprint,
30195
- revision: SCOPE_PROMPT_FINGERPRINT_REVISION
30414
+ revision: SCOPE_PROMPT_FINGERPRINT_REVISION,
30415
+ reason: options.reason ?? "scope_prompt_changed"
30196
30416
  });
30197
30417
  return null;
30198
30418
  }
@@ -30241,6 +30461,7 @@ var AgentManager = class {
30241
30461
  logger13.info("Evicting idle Agent query", { agentId: proc.agentId, scope: scopeKey(proc.scope) });
30242
30462
  const runtime = this.asRuntime(proc);
30243
30463
  this.clearQuietFlushTimer(runtime);
30464
+ this.teardownSpectate(runtime);
30244
30465
  try {
30245
30466
  runtime.inputController.close();
30246
30467
  await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
@@ -30260,6 +30481,7 @@ var AgentManager = class {
30260
30481
  const runtime = this.asRuntime(proc);
30261
30482
  const key = runtimeKey(proc.agentId, proc.scope);
30262
30483
  this.clearQuietFlushTimer(runtime);
30484
+ this.teardownSpectate(runtime);
30263
30485
  runtime.currentTask = null;
30264
30486
  runtime.injectedTasks = [];
30265
30487
  runtime.mergedTasks = [];
@@ -30296,6 +30518,7 @@ var AgentManager = class {
30296
30518
  evictIdle() {
30297
30519
  const now = Date.now();
30298
30520
  const { idleTimeoutMs, workingSilenceTimeoutMs } = this.queryConfig;
30521
+ const stallWarnAfterMs = Math.min(9e4, this.queryConfig.replyStallTimeoutMs);
30299
30522
  for (const [key, proc] of this.agents) {
30300
30523
  if (!this.isEvictable(proc)) continue;
30301
30524
  const runtime = this.asRuntime(proc);
@@ -30306,26 +30529,62 @@ var AgentManager = class {
30306
30529
  for (const [, proc] of this.agents) {
30307
30530
  if (proc.status !== "working") continue;
30308
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
+ }
30309
30563
  const hasInjectedBacklog = runtime.injectedTasks.length > 0;
30310
- 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;
30311
30566
  const silentMs = now - proc.lastSdkEventAt;
30312
30567
  if (silentMs <= effectiveTimeoutMs) {
30313
- if (hasInjectedBacklog && silentMs > workingSilenceTimeoutMs) {
30314
- logger13.warn(
30315
- "Zombie watchdog: working runtime silent past base timeout but has queued tasks; granting extended grace",
30316
- {
30317
- agentId: proc.agentId,
30318
- scope: scopeKey(proc.scope),
30319
- silentMs,
30320
- baseTimeoutMs: workingSilenceTimeoutMs,
30321
- effectiveTimeoutMs,
30322
- injectedTaskCount: runtime.injectedTasks.length,
30323
- replyMessageId: proc.currentTask?.replyMessageId
30324
- }
30325
- );
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
+ });
30326
30582
  }
30327
30583
  continue;
30328
30584
  }
30585
+ const zombieOpenTool = this.latestOpenToolUse(proc);
30586
+ const watchdogDetectedAt = Date.now();
30587
+ const watchdogFallbackId = createFallbackId();
30329
30588
  logger13.warn("Zombie watchdog: working runtime silent too long, tearing down", {
30330
30589
  agentId: proc.agentId,
30331
30590
  scope: scopeKey(proc.scope),
@@ -30333,12 +30592,46 @@ var AgentManager = class {
30333
30592
  lastSdkEventAt: new Date(proc.lastSdkEventAt).toISOString(),
30334
30593
  workingSilenceTimeoutMs,
30335
30594
  effectiveTimeoutMs,
30595
+ baseCeilingMs,
30596
+ busy,
30597
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
30336
30598
  replyMessageId: proc.currentTask?.replyMessageId,
30337
30599
  injectedTaskCount: runtime.injectedTasks.length,
30338
30600
  hadInjectedBacklog: hasInjectedBacklog,
30339
- 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
30340
30634
  });
30341
- void this.closeRuntime(proc, "zombie_watchdog");
30342
30635
  }
30343
30636
  }
30344
30637
  /**
@@ -30585,7 +30878,7 @@ ${cfg.instructions.trim()}` : "";
30585
30878
  agentId: agentConfig.id,
30586
30879
  capabilityTier: cfg.capabilityTier,
30587
30880
  isSmith: smithAgent
30588
- }) ?? { mcpServers: {}, allowedTools: [] };
30881
+ }) ?? { mcpServers: {}, allowedTools: [], toolAbi: [] };
30589
30882
  logger13.info("External MCP resolved for runtime", {
30590
30883
  agentId: agentConfig.id,
30591
30884
  scope: scopeKey(scope),
@@ -30602,11 +30895,16 @@ ${cfg.instructions.trim()}` : "";
30602
30895
  }
30603
30896
  const notebookSection = this.buildNotebookSection(agentConfig.id);
30604
30897
  const scopesSection = this.buildScopesSection(agentConfig, scope, agentCwd);
30898
+ const externalMcpFingerprint = this.externalMcpFingerprint(externalMcp);
30605
30899
  savedSessionId = this.discardSessionIfScopePromptChanged(
30606
30900
  agentConfig,
30607
30901
  scope,
30608
30902
  savedSessionId,
30609
- 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
+ }
30610
30908
  );
30611
30909
  let forkHistorySection = "";
30612
30910
  if (!savedSessionId && scope.kind === "single") {
@@ -30627,6 +30925,8 @@ ${cfg.instructions.trim()}` : "";
30627
30925
  }
30628
30926
  }
30629
30927
  const cronLockSnapshot = readCronLockSnapshot();
30928
+ const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
30929
+ const disallowedToolsForRuntime = builtinWebSearchAllowed ? [] : ["WebSearch"];
30630
30930
  logger13.info("Creating Agent query", {
30631
30931
  agentId: agentConfig.id,
30632
30932
  scope: scopeKey(scope),
@@ -30635,6 +30935,8 @@ ${cfg.instructions.trim()}` : "";
30635
30935
  sessionId: savedSessionId,
30636
30936
  forkHistoryReplay: forkHistorySection.length > 0,
30637
30937
  model: cfg.model ?? "(default)",
30938
+ builtinWebSearchAllowed,
30939
+ disallowedTools: disallowedToolsForRuntime,
30638
30940
  // Diagnostic: who currently owns Claude's global cron lock (~/.claude/scheduled_tasks.lock).
30639
30941
  // Cron is process-internal but the binary uses this singleton lock to elect ONE scheduler
30640
30942
  // among concurrent claude subprocesses. If lock is held by another session at spawn time,
@@ -30646,7 +30948,6 @@ ${cfg.instructions.trim()}` : "";
30646
30948
  });
30647
30949
  const planModeRef = { active: false, denyCount: 0 };
30648
30950
  const mediaGenerationTurnGuard = createOfficialMediaGenerationTurnGuard();
30649
- const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
30650
30951
  const options = {
30651
30952
  cwd: agentCwd,
30652
30953
  systemPrompt: {
@@ -30713,6 +31014,8 @@ ${cfg.instructions.trim()}` : "";
30713
31014
  ] : [],
30714
31015
  ...externalMcp.allowedTools
30715
31016
  ],
31017
+ // Server-side WebSearch bypasses canUseTool; disallowedTools removes it from model context.
31018
+ disallowedTools: disallowedToolsForRuntime,
30716
31019
  mcpServers: { ...externalMcp.mcpServers, neural: neuralServer },
30717
31020
  includePartialMessages: true,
30718
31021
  // Plan mode custom workflow instructions. When setPermissionMode('plan') is
@@ -31039,6 +31342,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
31039
31342
  currentTask: null,
31040
31343
  currentTaskStartedAt: 0,
31041
31344
  lastSdkEventAt: Date.now(),
31345
+ model: (typeof options.model === "string" ? options.model : cfg.model) ?? null,
31042
31346
  compactRequested: false,
31043
31347
  compactInProgress: false,
31044
31348
  contextOverflowLockedUntil: 0,
@@ -31050,13 +31354,18 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
31050
31354
  currentToolName: null,
31051
31355
  currentMcpInvocationId: null,
31052
31356
  currentMcpInvocationStartedAt: null,
31357
+ activeSubagentTaskIds: /* @__PURE__ */ new Set(),
31053
31358
  mcpAuditRecorder: this.mcpAuditRecorder,
31054
31359
  segmentBuffer: "",
31055
31360
  segmentCount: 0,
31056
31361
  accumulatedToolInput: "",
31057
31362
  planModeRef,
31058
31363
  mediaGenerationTurnGuard,
31059
- groupInbox: []
31364
+ groupInbox: [],
31365
+ spectating: false,
31366
+ spectateActivatedAt: 0,
31367
+ spectateViewing: false,
31368
+ spectateTtlExpired: false
31060
31369
  };
31061
31370
  const runtime = Object.assign(proc, {
31062
31371
  query: agentQuery,
@@ -31068,7 +31377,8 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
31068
31377
  createdAt: Date.now(),
31069
31378
  supportsVision: modelInputMode === "vision" && cfg.supportsVision !== false,
31070
31379
  modelInputMode,
31071
- quietFlushTimer: null
31380
+ quietFlushTimer: null,
31381
+ spectateRevertTimer: null
31072
31382
  });
31073
31383
  logger13.info("Agent model input mode resolved", {
31074
31384
  agentId: agentConfig.id,
@@ -31093,6 +31403,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
31093
31403
  } else {
31094
31404
  this.dormantGroupInboxes.delete(key);
31095
31405
  }
31406
+ const dormantMeta = this.dormantScopes.get(key);
31096
31407
  if (this.dormantScopes.delete(key)) {
31097
31408
  this.emit({
31098
31409
  type: "agent:awake",
@@ -31104,7 +31415,44 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
31104
31415
  });
31105
31416
  logger13.info("Agent scope awakened after dormant", {
31106
31417
  agentId: agentConfig.id,
31107
- 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
31108
31456
  });
31109
31457
  }
31110
31458
  if (proc.groupInbox.length > 0 && this.isRuntimeIdleForInboxFlush(runtime)) {
@@ -32242,7 +32590,6 @@ ${lines.join("\n")}`;
32242
32590
  compactTrigger: "context_watermark",
32243
32591
  injectedTasksWaiting: runtime.injectedTasks.length,
32244
32592
  compactPromptLen: compactPrompt.length,
32245
- promptSample: compactPrompt.slice(0, 80),
32246
32593
  traceId: compactTraceId
32247
32594
  });
32248
32595
  runtime.inputController.push(compactPrompt, runtime.ccSessionId ?? "");
@@ -32524,7 +32871,7 @@ ${lines.join("\n")}`;
32524
32871
  const enveloped = buildInnerVoiceEnvelope(payloadWithTrigger, ctx);
32525
32872
  const task = {
32526
32873
  content: enveloped,
32527
- replyMessageId: createMessageId(),
32874
+ replyMessageId: createNeuralSendReplyMessageId(),
32528
32875
  conversationId: payload.conversationId,
32529
32876
  traceId: createTraceId(),
32530
32877
  groupId: payload.groupId
@@ -32770,7 +33117,7 @@ ${lines.join("\n")}`;
32770
33117
  this.dormantScopes.delete(key);
32771
33118
  this.dormantGroupInboxes.delete(key);
32772
33119
  }
32773
- for (const key of [...this.dormantScopes].filter(
33120
+ for (const key of [...this.dormantScopes.keys()].filter(
32774
33121
  (k) => k === agentId || k.startsWith(`${agentId}::`)
32775
33122
  )) {
32776
33123
  this.dormantScopes.delete(key);
@@ -32818,7 +33165,7 @@ ${lines.join("\n")}`;
32818
33165
  async reloadAgentScopes(agentId, reason) {
32819
33166
  this.sessionStore.deleteAllForAgent(agentId);
32820
33167
  this.dispatchMemory.deleteAllForAgent(agentId);
32821
- for (const key of [...this.dormantScopes].filter(
33168
+ for (const key of [...this.dormantScopes.keys()].filter(
32822
33169
  (k) => k === agentId || k.startsWith(`${agentId}::`)
32823
33170
  )) {
32824
33171
  this.dormantScopes.delete(key);
@@ -32970,6 +33317,125 @@ ${lines.join("\n")}`;
32970
33317
  void this.terminateScope(proc.agentId, proc.scope);
32971
33318
  }
32972
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
+ }
32973
33439
  /** Stop one scoped SDK runtime (workdir change). */
32974
33440
  async terminateScope(agentId, scope) {
32975
33441
  const key = runtimeKey(agentId, scope);
@@ -32978,6 +33444,7 @@ ${lines.join("\n")}`;
32978
33444
  logger13.info("terminateScope: no active runtime", { agentId, scope: scopeKey(scope) });
32979
33445
  this.dormantScopes.delete(key);
32980
33446
  this.dormantGroupInboxes.delete(key);
33447
+ this.pendingSpectate.delete(key);
32981
33448
  this.sessionStore.delete(agentId, scope);
32982
33449
  this.dispatchMemory.deleteScope(agentId, scope);
32983
33450
  return;
@@ -33000,7 +33467,7 @@ ${lines.join("\n")}`;
33000
33467
  this.dispatchMemory.deleteScope(agentId, scope);
33001
33468
  logger13.info("terminateScope: scoped query removed", { agentId, scope: scopeKey(scope) });
33002
33469
  }
33003
- async closeRuntime(proc, reason) {
33470
+ async closeRuntime(proc, reason, watchdogForensics) {
33004
33471
  const key = runtimeKey(proc.agentId, proc.scope);
33005
33472
  if (proc.status === "dead") return;
33006
33473
  const runtime = this.asRuntime(proc);
@@ -33077,12 +33544,13 @@ ${lines.join("\n")}`;
33077
33544
  runtime.currentTask = null;
33078
33545
  if (isWatchdog) {
33079
33546
  const preservedInbox = proc.groupInbox;
33080
- if (preservedInbox.length > 0) {
33547
+ const preservedInboxSize = preservedInbox.length;
33548
+ if (preservedInboxSize > 0) {
33081
33549
  this.dormantGroupInboxes.set(key, [...preservedInbox]);
33082
33550
  logger13.info("Preserving groupInbox for dormant agent", {
33083
33551
  agentId,
33084
33552
  scope: scopeKey(proc.scope),
33085
- preservedInboxSize: preservedInbox.length,
33553
+ preservedInboxSize,
33086
33554
  preservedEntries: preservedInbox.map((e) => ({
33087
33555
  ackId: e.ackId,
33088
33556
  sender: e.senderName,
@@ -33091,7 +33559,26 @@ ${lines.join("\n")}`;
33091
33559
  }))
33092
33560
  });
33093
33561
  }
33094
- 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
+ });
33095
33582
  this.emit({
33096
33583
  type: "agent:dormant",
33097
33584
  payload: {
@@ -33106,13 +33593,15 @@ ${lines.join("\n")}`;
33106
33593
  agentId,
33107
33594
  scope: scopeKey(proc.scope),
33108
33595
  droppedTaskCount: droppedAckIds.length,
33109
- preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0
33596
+ preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0,
33597
+ fallbackId: effectiveFallbackId
33110
33598
  });
33111
33599
  }
33112
33600
  proc.status = "dead";
33113
33601
  this.agents.delete(key);
33114
33602
  this.lastUsedAt.delete(key);
33115
33603
  this.clearQuietFlushTimer(runtime);
33604
+ this.teardownSpectate(runtime);
33116
33605
  try {
33117
33606
  runtime.inputController.close();
33118
33607
  await this.awaitQueryReturn(runtime.query, 5e3, agentId);
@@ -33129,6 +33618,165 @@ ${lines.join("\n")}`;
33129
33618
  cwd: proc.cwd
33130
33619
  });
33131
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
+ }
33132
33780
  async recoverFromRestart(agents) {
33133
33781
  const lockSnapshot = readCronLockSnapshot();
33134
33782
  logger13.info("Recovering Agent sessions after restart", {
@@ -33251,58 +33899,7 @@ ${lines.join("\n")}`;
33251
33899
  this.lastUsedAt.delete(key);
33252
33900
  const errorText = isResumeFail ? `\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\uFF08${errMsg}\uFF09` : `Agent query crashed: ${errMsg}`;
33253
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;
33254
- if (runtime.currentTask) {
33255
- this.emit({
33256
- type: "agent:error",
33257
- payload: {
33258
- agentId: runtime.agentId,
33259
- conversationId: runtime.currentTask.conversationId,
33260
- ackId: runtime.currentTask.replyMessageId,
33261
- traceId: runtime.currentTask.traceId,
33262
- error: emittedErrorText
33263
- }
33264
- });
33265
- runtime.currentTask = null;
33266
- }
33267
- for (const task of runtime.injectedTasks) {
33268
- this.emit({
33269
- type: "agent:error",
33270
- payload: {
33271
- agentId: runtime.agentId,
33272
- conversationId: task.conversationId,
33273
- ackId: task.replyMessageId,
33274
- traceId: task.traceId,
33275
- error: emittedErrorText
33276
- }
33277
- });
33278
- }
33279
- runtime.injectedTasks = [];
33280
- for (const task of runtime.mergedTasks) {
33281
- this.emit({
33282
- type: "agent:error",
33283
- payload: {
33284
- agentId: runtime.agentId,
33285
- conversationId: task.conversationId,
33286
- ackId: task.replyMessageId,
33287
- traceId: task.traceId,
33288
- error: emittedErrorText
33289
- }
33290
- });
33291
- }
33292
- runtime.mergedTasks = [];
33293
- for (const task of runtime.planModeBuffer) {
33294
- this.emit({
33295
- type: "agent:error",
33296
- payload: {
33297
- agentId: runtime.agentId,
33298
- conversationId: task.conversationId,
33299
- ackId: task.replyMessageId,
33300
- traceId: task.traceId,
33301
- error: emittedErrorText
33302
- }
33303
- });
33304
- }
33305
- runtime.planModeBuffer = [];
33902
+ this.failPendingTasksWithError(runtime, emittedErrorText);
33306
33903
  }
33307
33904
  }
33308
33905
  getStatus(agentId, scope = { kind: "single" }) {
@@ -33316,6 +33913,18 @@ ${lines.join("\n")}`;
33316
33913
  }
33317
33914
  return [...ids];
33318
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
+ }
33319
33928
  latestOpenToolUse(proc) {
33320
33929
  for (let i = proc.contentBlocks.length - 1; i >= 0; i -= 1) {
33321
33930
  const block = proc.contentBlocks[i];
@@ -33464,7 +34073,7 @@ ${lines.join("\n")}`;
33464
34073
  }
33465
34074
  const task = {
33466
34075
  content: notice,
33467
- replyMessageId: createMessageId(),
34076
+ replyMessageId: createScopeNoticeReplyMessageId(),
33468
34077
  conversationId,
33469
34078
  traceId: createTraceId(),
33470
34079
  groupId: proc.scope.kind === "group" ? proc.scope.groupId : void 0
@@ -34546,6 +35155,7 @@ var HttpMcpRegistry = class {
34546
35155
  buildForAgent(ctx) {
34547
35156
  const mcpServers = {};
34548
35157
  const allowedTools = [];
35158
+ const toolAbi = [];
34549
35159
  const usedNames = /* @__PURE__ */ new Set();
34550
35160
  for (const connection of this.allConnections()) {
34551
35161
  if (!this.connectionAppliesToAgent(connection, ctx)) continue;
@@ -34554,12 +35164,18 @@ var HttpMcpRegistry = class {
34554
35164
  const serverName = uniqueServerName(normalizeMcpServerName(connection.serverName), usedNames);
34555
35165
  usedNames.add(serverName);
34556
35166
  mcpServers[serverName] = sdkConfig;
34557
- for (const tool2 of connection.tools) {
34558
- if (!tool2.enabled || tool2.permissionPolicy === "always_deny") continue;
34559
- allowedTools.push(mcpRuntimeToolName(serverName, tool2.name));
34560
- }
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
+ });
34561
35177
  }
34562
- return { mcpServers, allowedTools };
35178
+ return { mcpServers, allowedTools, toolAbi };
34563
35179
  }
34564
35180
  allConnections() {
34565
35181
  return [...this.serverConnections.values(), ...this.localConnections.values()];
@@ -34745,6 +35361,18 @@ function uniqueServerName(serverName, usedNames) {
34745
35361
  while (usedNames.has(`${serverName}_${idx}`)) idx += 1;
34746
35362
  return `${serverName}_${idx}`;
34747
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
+ }
34748
35376
  function buildHeaders(authType, authSecret, customHeaders) {
34749
35377
  const headers = {};
34750
35378
  for (const header of customHeaders) {
@@ -35396,6 +36024,7 @@ var ServerConnector = class {
35396
36024
  case "agent:terminate":
35397
36025
  case "agent:runtime_reload":
35398
36026
  case "agent:terminate_scope":
36027
+ case "spectate:set":
35399
36028
  case "agent:created":
35400
36029
  case "agent:updated":
35401
36030
  case "agent:workdir-updated":
@@ -36371,24 +37000,6 @@ function normalizeLocalPath(targetPath) {
36371
37000
  const expanded = trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\") ? path18.join(os10.homedir(), trimmed.slice(2)) : trimmed;
36372
37001
  return path18.normalize(path18.resolve(expanded));
36373
37002
  }
36374
- function isAbsoluteLocalPathCandidate(value) {
36375
- if (process.platform === "win32") {
36376
- return /^[a-zA-Z]:[\\/]/.test(value) || /^\\\\[^\\]/.test(value);
36377
- }
36378
- return value.startsWith("/");
36379
- }
36380
- function clipboardTextToPathCandidates(value) {
36381
- return value.replace(/\0/g, "\n").split(/\r?\n/).map((line) => line.trim().replace(/^"(.+)"$/, "$1")).map((line) => {
36382
- if (!line.toLowerCase().startsWith("file://")) return line;
36383
- try {
36384
- const url2 = new URL(line);
36385
- return decodeURIComponent(url2.pathname.replace(/^\/([a-zA-Z]:\/)/, "$1"));
36386
- } catch (e) {
36387
- logger24.debug("Failed to parse clipboard file URL", { error: e });
36388
- return "";
36389
- }
36390
- }).filter((line) => line.length > 0 && isAbsoluteLocalPathCandidate(line));
36391
- }
36392
37003
  function normalizeClipboardIdentityKey(value) {
36393
37004
  return process.platform === "win32" ? value.toLowerCase() : value;
36394
37005
  }
@@ -36426,18 +37037,15 @@ function mimeTypeForFileName(fileName) {
36426
37037
  }
36427
37038
  function parseWindowsClipboardResult(stdout) {
36428
37039
  const raw = stdout.trim();
36429
- if (!raw) return { files: [], text: "" };
37040
+ if (!raw) return { files: [] };
36430
37041
  try {
36431
37042
  const parsed = JSON.parse(raw);
36432
- if (!isRecord5(parsed)) return { files: [], text: "" };
37043
+ if (!isRecord5(parsed)) return { files: [] };
36433
37044
  const files = Array.isArray(parsed.files) ? parsed.files.filter((item) => typeof item === "string") : [];
36434
- return {
36435
- files,
36436
- text: typeof parsed.text === "string" ? parsed.text : ""
36437
- };
37045
+ return { files };
36438
37046
  } catch (e) {
36439
37047
  logger24.debug("Windows clipboard JSON parse skipped", { error: e });
36440
- return { files: clipboardTextToPathCandidates(stdout), text: "" };
37048
+ return { files: [] };
36441
37049
  }
36442
37050
  }
36443
37051
  async function readWindowsClipboardPathCandidates() {
@@ -36450,9 +37058,7 @@ async function readWindowsClipboardPathCandidates() {
36450
37058
  " $drop = [System.Windows.Forms.Clipboard]::GetFileDropList();",
36451
37059
  " foreach ($file in $drop) { $files += [string]$file }",
36452
37060
  "} catch {}",
36453
- '$text = "";',
36454
- "try { $text = [System.Windows.Forms.Clipboard]::GetText() } catch {}",
36455
- "[pscustomobject]@{ files = $files; text = $text } | ConvertTo-Json -Compress;"
37061
+ "[pscustomobject]@{ files = $files } | ConvertTo-Json -Compress;"
36456
37062
  ].join(" ");
36457
37063
  try {
36458
37064
  const { stdout } = await execFileAsync2("powershell.exe", [
@@ -36470,7 +37076,7 @@ async function readWindowsClipboardPathCandidates() {
36470
37076
  maxBuffer: 1024 * 1024
36471
37077
  });
36472
37078
  const result = parseWindowsClipboardResult(stdout);
36473
- return [...result.files, ...clipboardTextToPathCandidates(result.text)];
37079
+ return result.files;
36474
37080
  } catch (e) {
36475
37081
  logger24.debug("Windows clipboard file read skipped", { error: e });
36476
37082
  return [];
@@ -36873,7 +37479,7 @@ async function readStreamText(filePath, start) {
36873
37479
  function parseProcessedLines(raw, cursor, fileName, fingerprint) {
36874
37480
  const lastNewline = raw.lastIndexOf("\n");
36875
37481
  if (lastNewline < 0) {
36876
- return { entries: [], nextCursor: cursor, advanced: false };
37482
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "partial_line" };
36877
37483
  }
36878
37484
  const processed = raw.slice(0, lastNewline + 1);
36879
37485
  const lines = processed.split(/\r?\n/);
@@ -36899,7 +37505,8 @@ function parseProcessedLines(raw, cursor, fileName, fingerprint) {
36899
37505
  lineNum,
36900
37506
  ...fingerprint ? { fingerprint } : {}
36901
37507
  },
36902
- advanced: true
37508
+ advanced: true,
37509
+ reason: "advanced"
36903
37510
  };
36904
37511
  }
36905
37512
  function chunkEntries(entries, size) {
@@ -36957,24 +37564,55 @@ var BridgeLogUploader = class {
36957
37564
  async flushOnce() {
36958
37565
  if (this.running || this.stopped) return;
36959
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
+ };
36960
37581
  try {
36961
37582
  const targets = await this.resolveTargets();
37583
+ summary.targetCount = targets.length;
36962
37584
  for (const target of targets) {
36963
37585
  try {
36964
37586
  const cursor = await readCursor(target.cursorFile);
36965
37587
  const batch = await this.readNewEntries(target, cursor);
36966
- 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;
36967
37596
  if (batch.entries.length > 0) {
36968
- 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;
36969
37602
  }
36970
37603
  await writeCursor(target.cursorFile, batch.nextCursor);
36971
37604
  } catch (e) {
37605
+ summary.failedTargetCount += 1;
36972
37606
  logger27.warn("Bridge log upload target failed", { error: e, logFile: target.logFile });
36973
37607
  }
36974
37608
  }
36975
37609
  } catch (e) {
36976
37610
  logger27.warn("Bridge log upload cycle failed", { error: e });
36977
37611
  } finally {
37612
+ logger27.info("Bridge log upload cycle summary", {
37613
+ ...summary,
37614
+ durationMs: Date.now() - startedAt
37615
+ });
36978
37616
  this.running = false;
36979
37617
  }
36980
37618
  }
@@ -36997,7 +37635,7 @@ var BridgeLogUploader = class {
36997
37635
  } catch (e) {
36998
37636
  if (e instanceof Error && "code" in e && e.code === "ENOENT") {
36999
37637
  logger27.debug("Bridge log file not found for upload yet", { logFile: target.logFile });
37000
- return { entries: [], nextCursor: cursor, advanced: false };
37638
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "missing_file" };
37001
37639
  }
37002
37640
  throw e;
37003
37641
  }
@@ -37005,13 +37643,19 @@ var BridgeLogUploader = class {
37005
37643
  const samePhysicalFile = !fingerprint || !cursor.fingerprint || cursor.fingerprint === fingerprint;
37006
37644
  const normalizedCursor = stat3.size < cursor.offset || !samePhysicalFile ? { offset: 0, lineNum: 0, ...fingerprint ? { fingerprint } : {} } : { ...cursor, ...fingerprint ? { fingerprint } : {} };
37007
37645
  if (stat3.size <= normalizedCursor.offset) {
37008
- return { entries: [], nextCursor: normalizedCursor, advanced: false };
37646
+ return { entries: [], nextCursor: normalizedCursor, advanced: false, reason: "no_new_entries" };
37009
37647
  }
37010
37648
  const raw = await readStreamText(target.logFile, normalizedCursor.offset);
37011
37649
  return parseProcessedLines(raw, normalizedCursor, target.uploadedFileName, fingerprint);
37012
37650
  }
37013
37651
  async uploadEntries(entries) {
37014
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
+ };
37015
37659
  for (const chunk of chunkEntries(bridgeEntries, this.options.batchSize)) {
37016
37660
  const res = await fetch(`${this.options.serverApiUrl}/api/logs/upload`, {
37017
37661
  method: "POST",
@@ -37026,14 +37670,18 @@ var BridgeLogUploader = class {
37026
37670
  })
37027
37671
  });
37028
37672
  if (!res.ok) {
37029
- const body = await res.text().catch((e) => {
37673
+ const body2 = await res.text().catch((e) => {
37030
37674
  logger27.debug("Failed to read log upload error body", { error: e });
37031
37675
  return "";
37032
37676
  });
37033
- 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)}`);
37034
37678
  }
37035
- 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;
37036
37683
  }
37684
+ return result;
37037
37685
  }
37038
37686
  };
37039
37687
 
@@ -38118,8 +38766,10 @@ function resolvePnpmRuntimeBinary(sdkDir, target) {
38118
38766
  } catch {
38119
38767
  return void 0;
38120
38768
  }
38769
+ const matches = [];
38121
38770
  for (const entry of entries) {
38122
38771
  if (!entry.startsWith(`${encodedName}@`)) continue;
38772
+ const version2 = entry.slice(encodedName.length + 1);
38123
38773
  const candidate = path27.join(
38124
38774
  pnpmStoreDir,
38125
38775
  entry,
@@ -38127,9 +38777,22 @@ function resolvePnpmRuntimeBinary(sdkDir, target) {
38127
38777
  ...target.packageName.split("/"),
38128
38778
  target.binaryName
38129
38779
  );
38130
- if (existsSync2(candidate)) return candidate;
38780
+ if (existsSync2(candidate)) matches.push({ version: version2, candidate });
38131
38781
  }
38132
- 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;
38133
38796
  }
38134
38797
  function resolveSdkRuntimeBinary(target) {
38135
38798
  const directPath = safeResolve(`${target.packageName}/${target.binaryName}`);
@@ -39288,6 +39951,32 @@ function syncLocalRuntimeSkills(skillStore, localSkills, options = {}) {
39288
39951
  ]);
39289
39952
  }
39290
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
+
39291
39980
  // src/start.ts
39292
39981
  var logger41 = createModuleLogger("bridge");
39293
39982
  var NODE_USER_UID2 = 1e3;
@@ -39374,14 +40063,16 @@ async function startBridge(config2) {
39374
40063
  const claudeRuntime = resolveClaudeRuntime();
39375
40064
  logClaudeRuntimeResolution(claudeRuntime);
39376
40065
  if (!claudeRuntime.ok || !claudeRuntime.path) {
39377
- process.stderr.write(
40066
+ safeWriteProcessOutput(
40067
+ process.stderr,
39378
40068
  `
39379
40069
  Claude runtime is unavailable.
39380
40070
 
39381
40071
  ${claudeRuntime.error ?? "Install Claude Code manually or use the bundled desktop runtime."}
39382
40072
 
39383
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.
39384
- `
40074
+ `,
40075
+ (e) => logger41.error("Bridge process stderr write failed", { error: e })
39385
40076
  );
39386
40077
  process.exit(1);
39387
40078
  }
@@ -39438,12 +40129,14 @@ Reinstall @fangyb/ahchat-bridge with npm optional dependencies, set AHCHAT_CLAUD
39438
40129
  claudeRuntimeVersion: claudeRuntime.version ?? null
39439
40130
  });
39440
40131
  const shouldPrintRawBridgeToken = process.stdout.isTTY && process.env.AHCHAT_SUPPRESS_BRIDGE_TOKEN_STDOUT !== "1";
39441
- process.stdout.write(
40132
+ safeWriteProcessOutput(
40133
+ process.stdout,
39442
40134
  `
39443
40135
  Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\u673A\u5668):
39444
40136
  ${shouldPrintRawBridgeToken ? config2.bridgeToken : "***"}
39445
40137
 
39446
- `
40138
+ `,
40139
+ (e) => logger41.error("Bridge process stdout write failed", { error: e })
39447
40140
  );
39448
40141
  wsMetrics.start(5e3);
39449
40142
  const sessionStore = new SessionStore(config2.dataDir);
@@ -40242,6 +40935,19 @@ Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\
40242
40935
  });
40243
40936
  await agentManager.terminateScope(msg.payload.agentId, msg.payload.scope);
40244
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;
40245
40951
  case "agent:created":
40246
40952
  agentRegistry.upsert(msg.payload.agent);
40247
40953
  ensureLocalWorkdirPath(msg.payload.agent.workingDirectory, "agent:created", {
@@ -40458,8 +41164,12 @@ function writeAlreadyRunningMessage(error51) {
40458
41164
  ` ${buildStopCommand(error51.pid)}`,
40459
41165
  ""
40460
41166
  ];
40461
- process.stdout.write(`${lines.join("\n")}
40462
- `);
41167
+ safeWriteProcessOutput(
41168
+ process.stdout,
41169
+ `${lines.join("\n")}
41170
+ `,
41171
+ (e) => logger42.error("Bridge already-running message write failed", { error: e })
41172
+ );
40463
41173
  }
40464
41174
  function handleBridgeStartError(e, message) {
40465
41175
  if (isBridgeAlreadyRunningError(e)) {