@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/cli.cjs CHANGED
@@ -91836,6 +91836,8 @@ var DEFAULT_QUERY_CONFIG = {
91836
91836
  maxActive: 5040,
91837
91837
  idleTimeoutMs: 6e5,
91838
91838
  workingSilenceTimeoutMs: 12e5,
91839
+ replyStallTimeoutMs: 3e5,
91840
+ busySilenceTimeoutMs: 18e5,
91839
91841
  evictionIntervalMs: 6e4,
91840
91842
  statusReportIntervalMs: 6e4,
91841
91843
  allowBuiltinWebSearch: false,
@@ -91893,6 +91895,14 @@ function mergeQueryConfig(file2) {
91893
91895
  "AHCHAT_BRIDGE_WORKING_SILENCE_TIMEOUT_MS",
91894
91896
  q?.workingSilenceTimeoutMs ?? DEFAULT_QUERY_CONFIG.workingSilenceTimeoutMs
91895
91897
  ),
91898
+ replyStallTimeoutMs: readEnvInt(
91899
+ "AHCHAT_BRIDGE_REPLY_STALL_TIMEOUT_MS",
91900
+ q?.replyStallTimeoutMs ?? DEFAULT_QUERY_CONFIG.replyStallTimeoutMs
91901
+ ),
91902
+ busySilenceTimeoutMs: readEnvInt(
91903
+ "AHCHAT_BRIDGE_BUSY_SILENCE_TIMEOUT_MS",
91904
+ q?.busySilenceTimeoutMs ?? DEFAULT_QUERY_CONFIG.busySilenceTimeoutMs ?? 18e5
91905
+ ),
91896
91906
  evictionIntervalMs: readEnvInt(
91897
91907
  "AHCHAT_BRIDGE_EVICTION_INTERVAL_MS",
91898
91908
  q?.evictionIntervalMs ?? DEFAULT_QUERY_CONFIG.evictionIntervalMs
@@ -91973,7 +91983,11 @@ function loadBridgeConfig(opts) {
91973
91983
  ) || null,
91974
91984
  logUploadIntervalMs: readEnvInt(
91975
91985
  "AHCHAT_LOG_UPLOAD_INTERVAL_MS",
91976
- fileConfig.logUploadIntervalMs ?? 24 * 60 * 60 * 1e3
91986
+ // Flush every 60s instead of once a day. Daily flushing let logs pile up for hours,
91987
+ // then dumped tens of thousands of entries in one cycle on the next process start,
91988
+ // blowing past the server's per-minute upload quota (3000 entries / 3MB) and getting
91989
+ // 429'd. Small frequent batches stay well under that ceiling.
91990
+ fileConfig.logUploadIntervalMs ?? 6e4
91977
91991
  ),
91978
91992
  queryConfig: mergeQueryConfig(fileConfig)
91979
91993
  };
@@ -92709,9 +92723,34 @@ ${entry.error.stack}` : ""}`
92709
92723
  return `${ts} ${level} ${scope} ${entry.msg}${data}${trace}${errPart}`;
92710
92724
  };
92711
92725
 
92726
+ // ../logger/src/fallback.ts
92727
+ init_cjs_shims();
92728
+ function logFallback(logger45, event) {
92729
+ const payload = {
92730
+ ...event.traceId ? { traceId: event.traceId } : {},
92731
+ fallback: {
92732
+ fallbackId: event.fallbackId,
92733
+ type: event.type,
92734
+ phase: event.phase,
92735
+ expected: event.expected,
92736
+ ...event.context ? { context: event.context } : {},
92737
+ ...event.outcome ? { outcome: event.outcome } : {}
92738
+ }
92739
+ };
92740
+ const msg = `[FALLBACK] ${event.type}:${event.phase}`;
92741
+ const useDebug = event.expected && event.phase !== "outcome";
92742
+ if (useDebug) {
92743
+ logger45.debug(msg, payload);
92744
+ } else {
92745
+ logger45.warn(msg, payload);
92746
+ }
92747
+ }
92748
+
92712
92749
  // ../logger/src/transports/console.ts
92713
92750
  init_cjs_shims();
92714
92751
  var protectedStreams = /* @__PURE__ */ new WeakSet();
92752
+ function ignoreWriteError(_error) {
92753
+ }
92715
92754
  function defaultStream(kind) {
92716
92755
  const maybeGlobal = globalThis;
92717
92756
  return maybeGlobal.process?.[kind];
@@ -92724,12 +92763,13 @@ function safeWriteLine(stream, line, fallback) {
92724
92763
  if (stream.destroyed || stream.writableEnded) return;
92725
92764
  if (typeof stream === "object" && typeof stream.on === "function" && !protectedStreams.has(stream)) {
92726
92765
  protectedStreams.add(stream);
92727
- stream.on("error", () => void 0);
92766
+ stream.on("error", ignoreWriteError);
92728
92767
  }
92729
92768
  try {
92730
92769
  stream.write(`${line}
92731
- `, () => void 0);
92732
- } catch {
92770
+ `, ignoreWriteError);
92771
+ } catch (e) {
92772
+ ignoreWriteError(e);
92733
92773
  }
92734
92774
  }
92735
92775
  function consoleTransport(opts) {
@@ -93364,8 +93404,31 @@ function parseSize(maxSize) {
93364
93404
  return trimmed;
93365
93405
  }
93366
93406
  var streamCache = /* @__PURE__ */ new Map();
93407
+ var droppedEntryCount = 0;
93408
+ var lastReportedDroppedTotal = 0;
93409
+ function buildLogDroppedSentinel(fmt, source, droppedTotal) {
93410
+ const entry = {
93411
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
93412
+ level: "WARN",
93413
+ source,
93414
+ module: "logger.file",
93415
+ msg: "log_dropped",
93416
+ data: { droppedTotal }
93417
+ };
93418
+ return fmt(entry);
93419
+ }
93420
+ function writeWithDroppedSentinel(stream, fmt, line, source) {
93421
+ stream.write(`${line}
93422
+ `);
93423
+ if (droppedEntryCount > lastReportedDroppedTotal) {
93424
+ lastReportedDroppedTotal = droppedEntryCount;
93425
+ stream.write(`${buildLogDroppedSentinel(fmt, source, droppedEntryCount)}
93426
+ `);
93427
+ }
93428
+ }
93367
93429
  function fileTransport(opts) {
93368
93430
  const fmt = opts.formatter ?? jsonFormatter;
93431
+ const logSource = opts.source ?? "server";
93369
93432
  const resolved = import_node_path2.default.resolve(opts.path);
93370
93433
  let cached2 = streamCache.get(resolved);
93371
93434
  if (!cached2) {
@@ -93380,11 +93443,14 @@ function fileTransport(opts) {
93380
93443
  streamCache.set(resolved, cached2);
93381
93444
  }
93382
93445
  return (entry) => {
93383
- if (cached2.closed || cached2.stream.destroyed || cached2.stream.writableEnded) return;
93446
+ if (cached2.closed || cached2.stream.destroyed || cached2.stream.writableEnded) {
93447
+ droppedEntryCount += 1;
93448
+ return;
93449
+ }
93384
93450
  try {
93385
- cached2.stream.write(`${fmt(entry)}
93386
- `);
93451
+ writeWithDroppedSentinel(cached2.stream, fmt, fmt(entry), logSource);
93387
93452
  } catch {
93453
+ droppedEntryCount += 1;
93388
93454
  }
93389
93455
  };
93390
93456
  }
@@ -93770,8 +93836,10 @@ function resolvePnpmRuntimeBinary(sdkDir, target) {
93770
93836
  } catch {
93771
93837
  return void 0;
93772
93838
  }
93839
+ const matches = [];
93773
93840
  for (const entry of entries) {
93774
93841
  if (!entry.startsWith(`${encodedName}@`)) continue;
93842
+ const version4 = entry.slice(encodedName.length + 1);
93775
93843
  const candidate = import_node_path5.default.join(
93776
93844
  pnpmStoreDir,
93777
93845
  entry,
@@ -93779,9 +93847,22 @@ function resolvePnpmRuntimeBinary(sdkDir, target) {
93779
93847
  ...target.packageName.split("/"),
93780
93848
  target.binaryName
93781
93849
  );
93782
- if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
93850
+ if ((0, import_node_fs3.existsSync)(candidate)) matches.push({ version: version4, candidate });
93783
93851
  }
93784
- return void 0;
93852
+ if (matches.length === 0) return void 0;
93853
+ matches.sort((a, b) => compareRuntimeVersion(b.version, a.version));
93854
+ return matches[0].candidate;
93855
+ }
93856
+ function compareRuntimeVersion(a, b) {
93857
+ const parse3 = (v) => v.split(/[.+_-]/).map((n) => Number.parseInt(n, 10));
93858
+ const pa = parse3(a);
93859
+ const pb = parse3(b);
93860
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
93861
+ const da = Number.isFinite(pa[i]) ? pa[i] : 0;
93862
+ const db = Number.isFinite(pb[i]) ? pb[i] : 0;
93863
+ if (da !== db) return da - db;
93864
+ }
93865
+ return 0;
93785
93866
  }
93786
93867
  function resolveSdkRuntimeBinary(target) {
93787
93868
  const directPath = safeResolve(`${target.packageName}/${target.binaryName}`);
@@ -94608,6 +94689,9 @@ init_cjs_shims();
94608
94689
  // ../shared/src/types/index.ts
94609
94690
  init_cjs_shims();
94610
94691
 
94692
+ // ../shared/src/types/usage.ts
94693
+ init_cjs_shims();
94694
+
94611
94695
  // ../shared/src/types/message.ts
94612
94696
  init_cjs_shims();
94613
94697
 
@@ -94680,6 +94764,9 @@ function createMessageId() {
94680
94764
  function createTraceId() {
94681
94765
  return `tr_${Date.now().toString(36)}_${nanoid(6)}`;
94682
94766
  }
94767
+ function createFallbackId() {
94768
+ return `flb_${Date.now().toString(36)}_${nanoid(6)}`;
94769
+ }
94683
94770
  function createRequestId() {
94684
94771
  return `req_${Date.now().toString(36)}_${nanoid(6)}`;
94685
94772
  }
@@ -94689,6 +94776,12 @@ function createCronReplyMessageId() {
94689
94776
  function createInboxFlushReplyMessageId() {
94690
94777
  return `msg_inbox_${Date.now().toString(36)}_${nanoid(6)}`;
94691
94778
  }
94779
+ function createNeuralSendReplyMessageId() {
94780
+ return `msg_nsend_${Date.now().toString(36)}_${nanoid(6)}`;
94781
+ }
94782
+ function createScopeNoticeReplyMessageId() {
94783
+ return `msg_scopenotice_${Date.now().toString(36)}_${nanoid(6)}`;
94784
+ }
94692
94785
  function createCronTraceId() {
94693
94786
  return `tr_cron_${Date.now().toString(36)}_${nanoid(6)}`;
94694
94787
  }
@@ -94733,6 +94826,24 @@ function assertNumberPayloadField(type, payload, field) {
94733
94826
  throw invalidWsMessage(type, field);
94734
94827
  }
94735
94828
  }
94829
+ function assertOptionalNumberPayloadField(type, payload, field) {
94830
+ if (payload[field] === void 0) return;
94831
+ assertNumberPayloadField(type, payload, field);
94832
+ }
94833
+ function assertNullableStringPayloadField(type, payload, field) {
94834
+ if (payload[field] === null) return;
94835
+ assertStringPayloadField(type, payload, field);
94836
+ }
94837
+ function assertStringPayloadOneOf(type, payload, field, allowed) {
94838
+ assertStringPayloadField(type, payload, field);
94839
+ if (!allowed.includes(payload[field])) {
94840
+ throw invalidWsMessage(type, field);
94841
+ }
94842
+ }
94843
+ function assertOptionalStringPayloadOneOf(type, payload, field, allowed) {
94844
+ if (payload[field] === void 0) return;
94845
+ assertStringPayloadOneOf(type, payload, field, allowed);
94846
+ }
94736
94847
  function assertArrayPayloadField(type, payload, field) {
94737
94848
  if (!Array.isArray(payload[field])) {
94738
94849
  throw invalidWsMessage(type, field);
@@ -94813,6 +94924,25 @@ function validateWSMessageShape(msg) {
94813
94924
  case "agent:error": {
94814
94925
  assertPayloadRecord(type, payload);
94815
94926
  validateRequiredStrings(type, payload, ["ackId", "agentId", "conversationId", "error", "traceId"]);
94927
+ assertOptionalStringPayloadOneOf(type, payload, "reason", ["user_quota_exceeded", "company_quota_exceeded"]);
94928
+ return;
94929
+ }
94930
+ case "directory:register": {
94931
+ assertPayloadRecord(type, payload);
94932
+ validateRequiredStrings(type, payload, ["handle", "viaChild", "traceId"]);
94933
+ assertStringPayloadOneOf(type, payload, "op", ["add", "remove", "move"]);
94934
+ return;
94935
+ }
94936
+ case "directory:resolve": {
94937
+ assertPayloadRecord(type, payload);
94938
+ validateRequiredStrings(type, payload, ["handle", "fromNode", "traceId"]);
94939
+ assertOptionalNumberPayloadField(type, payload, "hop");
94940
+ return;
94941
+ }
94942
+ case "directory:resolve_result": {
94943
+ assertPayloadRecord(type, payload);
94944
+ validateRequiredStrings(type, payload, ["handle", "traceId"]);
94945
+ assertNullableStringPayloadField(type, payload, "canonicalAddress");
94816
94946
  return;
94817
94947
  }
94818
94948
  default:
@@ -96174,6 +96304,9 @@ init_cjs_shims();
96174
96304
  // ../shared/src/utils/serverUrl.ts
96175
96305
  init_cjs_shims();
96176
96306
 
96307
+ // ../shared/src/utils/phone.ts
96308
+ init_cjs_shims();
96309
+
96177
96310
  // ../shared/src/utils/mediaPreviewHtml.ts
96178
96311
  init_cjs_shims();
96179
96312
 
@@ -96827,8 +96960,8 @@ var VOLCENGINE_SEEDANCE_MCP_PROVIDER = {
96827
96960
  };
96828
96961
  var OFFICIAL_MCP_PROVIDERS = [
96829
96962
  ...ALIYUN_IQS_MCP_PROVIDERS,
96830
- VOLCENGINE_SEEDANCE_MCP_PROVIDER,
96831
- VOLCENGINE_SEEDREAM_MCP_PROVIDER
96963
+ VOLCENGINE_SEEDREAM_MCP_PROVIDER,
96964
+ VOLCENGINE_SEEDANCE_MCP_PROVIDER
96832
96965
  ];
96833
96966
  var MCP_STORE_PROVIDERS = [
96834
96967
  {
@@ -97640,8 +97773,7 @@ var AskQuestionRegistry = class {
97640
97773
  questionId,
97641
97774
  agentId: entry.agentId,
97642
97775
  waitedMs: Date.now() - entry.askedAt,
97643
- answerLen: answerText2.length,
97644
- answerSample: answerText2.slice(0, 200)
97776
+ answerLen: answerText2.length
97645
97777
  });
97646
97778
  entry.resolve(answerText2);
97647
97779
  return true;
@@ -97786,7 +97918,7 @@ function makeAskUserQuestionGuard(deps) {
97786
97918
  bundleIndex,
97787
97919
  bundleSize,
97788
97920
  replyMessageId: task.replyMessageId,
97789
- question: q.question.slice(0, 200),
97921
+ questionLen: q.question.length,
97790
97922
  optionCount: options.length,
97791
97923
  multiSelect,
97792
97924
  traceId: task.traceId
@@ -97885,7 +98017,7 @@ function makeAskUserQuestionGuard(deps) {
97885
98017
  bundleId,
97886
98018
  bundleSize,
97887
98019
  replyMessageId: task.replyMessageId,
97888
- combinedSample: combined.slice(0, 200),
98020
+ combinedLen: combined.length,
97889
98021
  traceId: task.traceId
97890
98022
  });
97891
98023
  return { behavior: "deny", message: combined };
@@ -112870,6 +113002,17 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
112870
113002
  ".yaml",
112871
113003
  ".yml"
112872
113004
  ]);
113005
+ var OFFICE_DOCUMENT_EXTENSIONS = /* @__PURE__ */ new Set([
113006
+ ".docx",
113007
+ ".xlsx",
113008
+ ".pptx",
113009
+ ".pdf",
113010
+ ".odt",
113011
+ ".ods",
113012
+ ".odp",
113013
+ ".rtf"
113014
+ ]);
113015
+ var PLAIN_TEXT_BINARY_SNIFF_BYTES = 8192;
112873
113016
  var DEFAULT_MAX_CHARS = 5e5;
112874
113017
  var DEFAULT_TIMEOUT_MS = 45e3;
112875
113018
  function isReadableDocumentPath(filePath) {
@@ -112889,9 +113032,6 @@ function resolveDocumentPath(inputPath, cwd) {
112889
113032
  async function readDocumentAsMarkdown(inputPath, opts = {}) {
112890
113033
  const resolvedPath = opts.cwd ? resolveDocumentPath(inputPath, opts.cwd) : import_node_path10.default.resolve(resolveUserPath(inputPath));
112891
113034
  const ext = import_node_path10.default.extname(resolvedPath).toLowerCase();
112892
- if (!isReadableDocumentPath(resolvedPath)) {
112893
- throw new Error(`unsupported document type: ${ext || "(no extension)"}`);
112894
- }
112895
113035
  const stat3 = await import_promises2.default.stat(resolvedPath);
112896
113036
  if (!stat3.isFile()) throw new Error("path is not a file");
112897
113037
  const warnings = [];
@@ -112900,8 +113040,10 @@ async function readDocumentAsMarkdown(inputPath, opts = {}) {
112900
113040
  markdown = await import_promises2.default.readFile(resolvedPath, "utf-8");
112901
113041
  } else if (ext === ".xls") {
112902
113042
  markdown = await convertLegacyExcelDocument(resolvedPath);
112903
- } else {
113043
+ } else if (OFFICE_DOCUMENT_EXTENSIONS.has(ext)) {
112904
113044
  markdown = await convertOfficeDocument(resolvedPath, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
113045
+ } else {
113046
+ markdown = await readPlainTextDocument(resolvedPath, ext);
112905
113047
  }
112906
113048
  markdown = normalizeDocumentText(markdown);
112907
113049
  const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
@@ -113040,6 +113182,14 @@ async function convertDocxWithOfficeCli(filePath, timeoutMs) {
113040
113182
  });
113041
113183
  return text;
113042
113184
  }
113185
+ async function readPlainTextDocument(filePath, ext) {
113186
+ const buffer = await import_promises2.default.readFile(filePath);
113187
+ const sample = buffer.subarray(0, Math.min(buffer.length, PLAIN_TEXT_BINARY_SNIFF_BYTES));
113188
+ if (sample.includes(0)) {
113189
+ throw new Error(`unsupported document type: ${ext || "(no extension)"}`);
113190
+ }
113191
+ return buffer.toString("utf-8");
113192
+ }
113043
113193
  async function convertLegacyExcelDocument(filePath) {
113044
113194
  const XLSX2 = await Promise.resolve().then(() => (init_xlsx(), xlsx_exports));
113045
113195
  const workbook = XLSX2.readFile(filePath, { cellDates: true });
@@ -113509,8 +113659,7 @@ async function createNeuralMcpServer(deps) {
113509
113659
  agentId: deps.agentId,
113510
113660
  fromScope: currentScopeKey,
113511
113661
  rawTargetScope: args.target_scope,
113512
- messageLen: args.message.length,
113513
- messageSample: args.message.slice(0, 120)
113662
+ messageLen: args.message.length
113514
113663
  });
113515
113664
  const trimmed = args.message.trim();
113516
113665
  if (!trimmed) {
@@ -113604,7 +113753,7 @@ async function createNeuralMcpServer(deps) {
113604
113753
  toScope: resolvedKey,
113605
113754
  repeatsInWindow: sendHistory.length,
113606
113755
  windowMs: NEURAL_DEDUP_WINDOW_MS,
113607
- messageSample: trimmed.slice(0, 120)
113756
+ messageLen: trimmed.length
113608
113757
  });
113609
113758
  return {
113610
113759
  content: [{
@@ -113631,8 +113780,7 @@ async function createNeuralMcpServer(deps) {
113631
113780
  agentId: deps.agentId,
113632
113781
  fromScope: currentScopeKey,
113633
113782
  toScope: resolvedKey,
113634
- messageLen: trimmed.length,
113635
- messageSample: trimmed.slice(0, 120)
113783
+ messageLen: trimmed.length
113636
113784
  });
113637
113785
  return {
113638
113786
  content: [{ type: "text", text: `[neural_send] \u5DF2\u9001\u8FBE\u5230\u300C${toLabel}\u300D(scope: ${resolvedKey})\u3002` }]
@@ -113989,6 +114137,7 @@ action="append" \u8FFD\u52A0\u65B0\u5185\u5BB9\uFF08\u6700\u5E38\u7528\uFF0Ccont
113989
114137
  `Read a document from the current working directory and return extracted Markdown text.
113990
114138
  Use this instead of the built-in Read tool for binary documents such as .docx, .xls, .xlsx, .pptx, .pdf, .odt, .ods, .odp, or .rtf.
113991
114139
  Also supports text-like files such as .csv, .md, .txt, .json, .xml, .yaml, and .html.
114140
+ Plain-text and source/config files (e.g. .py, .ts, Makefile, Dockerfile, and other extension-less text files) are read as-is; only true binaries are rejected.
113992
114141
  Pass either a relative path from the current working directory or an absolute path inside it.`,
113993
114142
  {
113994
114143
  path: external_exports.string().min(1).describe("Document path, relative to the current working directory or absolute inside it."),
@@ -116686,7 +116835,11 @@ function parseJsonRecord(text) {
116686
116835
  try {
116687
116836
  const parsed = JSON.parse(text);
116688
116837
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
116689
- } catch {
116838
+ } catch (error51) {
116839
+ logger11.warn("SDK tool result output was not JSON", {
116840
+ error: error51,
116841
+ outputLength: text.length
116842
+ });
116690
116843
  return null;
116691
116844
  }
116692
116845
  }
@@ -117048,6 +117201,9 @@ function emitUsageReported(proc, emit, base, usage, messageId) {
117048
117201
  function isGroupTask(proc) {
117049
117202
  return proc.currentTask?.groupId != null;
117050
117203
  }
117204
+ function shouldStreamInternals(proc) {
117205
+ return !isGroupTask(proc) || proc.spectating === true;
117206
+ }
117051
117207
  function extractTodosFromInput(input) {
117052
117208
  if (!input || typeof input !== "object") return null;
117053
117209
  const raw = input.todos;
@@ -117179,7 +117335,6 @@ function emitGroupSegment(proc, emit, base, content, contentBlocks, isSilent = f
117179
117335
  contentLen: content.length,
117180
117336
  blockCount: contentBlocks.length,
117181
117337
  blockTypes: contentBlocks.map((b) => b.type),
117182
- contentSample: content.slice(0, 200),
117183
117338
  traceId: base.traceId,
117184
117339
  isAuditOnly: content.length === 0,
117185
117340
  isSilent
@@ -117235,9 +117390,24 @@ function flushTextSegmentOnBlockStop(proc, emit, base) {
117235
117390
  }
117236
117391
  proc.segmentBuffer = "";
117237
117392
  }
117393
+ function describeSdkEvent(message) {
117394
+ const rec = message;
117395
+ const str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
117396
+ return {
117397
+ type: str(rec.type) ?? "unknown",
117398
+ subtype: str(rec.subtype),
117399
+ toolName: str(rec.last_tool_name),
117400
+ subagentType: str(rec.subagent_type),
117401
+ toolUseId: str(rec.tool_use_id),
117402
+ taskId: str(rec.task_id),
117403
+ at: Date.now()
117404
+ };
117405
+ }
117238
117406
  function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProviderApiError) {
117239
117407
  const emit = rawEmit;
117240
117408
  proc.lastSdkEventAt = Date.now();
117409
+ proc.lastSdkEventInfo = describeSdkEvent(message);
117410
+ proc.stallWarned = false;
117241
117411
  switch (message.type) {
117242
117412
  case "system": {
117243
117413
  const sysMsg = message;
@@ -117276,11 +117446,29 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117276
117446
  sessionId: proc.ccSessionId
117277
117447
  });
117278
117448
  } else {
117449
+ const sysRec = sysMsg;
117450
+ const pick2 = (k) => typeof sysRec[k] === "string" || typeof sysRec[k] === "number" ? sysRec[k] : void 0;
117451
+ const descriptionLen = typeof sysRec.description === "string" ? sysRec.description.length : void 0;
117452
+ const subagentTaskId = typeof sysRec.task_id === "string" ? sysRec.task_id : void 0;
117453
+ if (subagentTaskId) {
117454
+ if (sysMsg.subtype === "task_started") {
117455
+ (proc.activeSubagentTaskIds ??= /* @__PURE__ */ new Set()).add(subagentTaskId);
117456
+ } else if (sysMsg.subtype === "task_notification") {
117457
+ proc.activeSubagentTaskIds?.delete(subagentTaskId);
117458
+ }
117459
+ }
117279
117460
  logger11.info("SDK system subtype unhandled", {
117280
117461
  agentId: proc.agentId,
117281
117462
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
117282
117463
  subtype: sysMsg.subtype ?? "(none)",
117283
- keys: Object.keys(sysMsg).slice(0, 12)
117464
+ taskId: pick2("task_id"),
117465
+ toolUseId: pick2("tool_use_id"),
117466
+ subagentType: pick2("subagent_type"),
117467
+ taskType: pick2("task_type"),
117468
+ lastToolName: pick2("last_tool_name"),
117469
+ hasDescription: descriptionLen != null,
117470
+ descriptionLen,
117471
+ keys: Object.keys(sysMsg).slice(0, 16)
117284
117472
  });
117285
117473
  }
117286
117474
  break;
@@ -117306,13 +117494,14 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117306
117494
  } else if (block.type === "tool_use") {
117307
117495
  proc.currentBlockType = "tool_use";
117308
117496
  proc.currentToolName = block.name ?? "unknown";
117497
+ proc.activeToolUseStartedAt = Date.now();
117309
117498
  proc.accumulatedToolInput = "";
117310
117499
  const toolName = block.name ?? "unknown";
117311
117500
  proc.suppressCurrentToolUse = proc.officialMediaGenerationSatisfied === true && isOfficialMediaGenerationToolName(toolName);
117312
117501
  const isMcpTool = parseMcpRuntimeToolName(toolName) != null;
117313
117502
  proc.currentMcpInvocationId = isMcpTool ? createMcpToolInvocationId() : null;
117314
117503
  proc.currentMcpInvocationStartedAt = isMcpTool ? (/* @__PURE__ */ new Date()).toISOString() : null;
117315
- if (!isGroupTask(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
117504
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentToolUse && toolName !== "ExitPlanMode" && !isAskUserQuestionToolName(toolName)) {
117316
117505
  emit({
117317
117506
  type: "agent:tool_use",
117318
117507
  payload: {
@@ -117337,7 +117526,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117337
117526
  if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
117338
117527
  if (proc.suppressCurrentThinking) break;
117339
117528
  proc.accumulatedThinking += delta.thinking;
117340
- if (!isGroupTask(proc)) {
117529
+ if (shouldStreamInternals(proc)) {
117341
117530
  emit({
117342
117531
  type: "agent:thinking_chunk",
117343
117532
  payload: { ...wireBase(base), chunk: delta.thinking }
@@ -117348,7 +117537,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117348
117537
  if (typeof partial2 === "string") {
117349
117538
  proc.accumulatedToolInput += partial2;
117350
117539
  const liveInput = extractLiveToolInput(proc.currentToolName, proc.accumulatedToolInput);
117351
- if (!isGroupTask(proc) && liveInput && proc.currentToolName != null) {
117540
+ if (shouldStreamInternals(proc) && liveInput && proc.currentToolName != null) {
117352
117541
  emit({
117353
117542
  type: "agent:tool_input_update",
117354
117543
  payload: {
@@ -117382,7 +117571,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117382
117571
  }
117383
117572
  case "content_block_stop": {
117384
117573
  if (proc.currentBlockType === "thinking") {
117385
- if (!isGroupTask(proc) && !proc.suppressCurrentThinking) {
117574
+ if (shouldStreamInternals(proc) && !proc.suppressCurrentThinking) {
117386
117575
  emit({
117387
117576
  type: "agent:thinking_done",
117388
117577
  payload: wireBase(getTaskBase(proc))
@@ -117402,12 +117591,12 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117402
117591
  if (proc.accumulatedToolInput.length > 0) {
117403
117592
  try {
117404
117593
  parsedInput = JSON.parse(proc.accumulatedToolInput);
117405
- } catch {
117594
+ } catch (error51) {
117406
117595
  logger11.warn("Failed to parse tool input JSON", {
117596
+ error: error51,
117407
117597
  agentId: proc.agentId,
117408
117598
  toolName: proc.currentToolName,
117409
- inputLen: proc.accumulatedToolInput.length,
117410
- sample: proc.accumulatedToolInput.slice(0, 200)
117599
+ inputLen: proc.accumulatedToolInput.length
117411
117600
  });
117412
117601
  }
117413
117602
  }
@@ -117416,7 +117605,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117416
117605
  if (lastToolUse && lastToolUse.type === "tool_use") {
117417
117606
  lastToolUse.input = parsedInput;
117418
117607
  }
117419
- if (!isGroupTask(proc) && proc.currentToolName != null && LIVE_INPUT_PREVIEW_TOOLS.has(proc.currentToolName) && Object.keys(parsedInput).length > 0) {
117608
+ if (shouldStreamInternals(proc) && proc.currentToolName != null && LIVE_INPUT_PREVIEW_TOOLS.has(proc.currentToolName) && Object.keys(parsedInput).length > 0) {
117420
117609
  emit({
117421
117610
  type: "agent:tool_input_update",
117422
117611
  payload: {
@@ -117540,7 +117729,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117540
117729
  blockTypes,
117541
117730
  hasToolResult,
117542
117731
  hasPlainText,
117543
- contentSample: typeof content === "string" ? content.slice(0, 200) : JSON.stringify(content).slice(0, 200)
117732
+ contentLen: typeof content === "string" ? content.length : JSON.stringify(content).length
117544
117733
  });
117545
117734
  break;
117546
117735
  }
@@ -117549,7 +117738,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117549
117738
  agentId: proc.agentId,
117550
117739
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
117551
117740
  blockTypes,
117552
- contentSample: JSON.stringify(content).slice(0, 300),
117741
+ contentLen: JSON.stringify(content).length,
117553
117742
  replyMessageId: base.replyMessageId
117554
117743
  });
117555
117744
  }
@@ -117570,14 +117759,15 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117570
117759
  });
117571
117760
  proc.currentMcpInvocationId = null;
117572
117761
  proc.currentMcpInvocationStartedAt = null;
117762
+ proc.activeToolUseStartedAt = void 0;
117573
117763
  proc.currentToolName = null;
117574
117764
  continue;
117575
117765
  }
117576
117766
  if (isSuccessfulOfficialMediaOutput(toolName, output)) {
117577
117767
  proc.officialMediaGenerationSatisfied = true;
117578
- proc.officialMediaSessionRecycleRequested = true;
117579
117768
  }
117580
117769
  if (isAskUserQuestionToolName(toolName)) {
117770
+ proc.activeToolUseStartedAt = void 0;
117581
117771
  proc.currentToolName = null;
117582
117772
  continue;
117583
117773
  }
@@ -117605,7 +117795,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117605
117795
  proc.currentMcpInvocationId = null;
117606
117796
  proc.currentMcpInvocationStartedAt = null;
117607
117797
  }
117608
- if (!isGroupTask(proc)) {
117798
+ if (shouldStreamInternals(proc)) {
117609
117799
  emit({
117610
117800
  type: "agent:tool_result",
117611
117801
  payload: {
@@ -117629,6 +117819,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117629
117819
  }
117630
117820
  }
117631
117821
  }
117822
+ proc.activeToolUseStartedAt = void 0;
117632
117823
  }
117633
117824
  }
117634
117825
  }
@@ -117707,7 +117898,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117707
117898
  groupId,
117708
117899
  compactScheduled: proc.compactRequested === true,
117709
117900
  fullTextLen: proc.accumulatedText.length,
117710
- fullTextSample: proc.accumulatedText.slice(0, 200),
117711
117901
  accumulatedBlockCount: proc.contentBlocks.length,
117712
117902
  accumulatedBlockTypes: proc.contentBlocks.map((b) => b.type),
117713
117903
  silentSegmentEmitted: groupMode
@@ -117751,7 +117941,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117751
117941
  segmentCount: proc.segmentCount,
117752
117942
  compactScheduled: proc.compactRequested === true,
117753
117943
  fullTextLen: proc.accumulatedText.length,
117754
- fullTextSample: proc.accumulatedText.slice(0, 200),
117755
117944
  traceId: base.traceId
117756
117945
  });
117757
117946
  emit({
@@ -117802,7 +117991,6 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117802
117991
  ackId: base.replyMessageId,
117803
117992
  messageId: carrierMessageId,
117804
117993
  textLen: proc.accumulatedText.length,
117805
- textSample: proc.accumulatedText.slice(0, 200),
117806
117994
  tokenCount: usage.tokenCount,
117807
117995
  traceId: base.traceId
117808
117996
  });
@@ -117995,8 +118183,7 @@ function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted, onProv
117995
118183
  logger11.info("Captured non-streamed assistant message", {
117996
118184
  agentId: proc.agentId,
117997
118185
  scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
117998
- textLen: text.length,
117999
- textSample: text.slice(0, 100)
118186
+ textLen: text.length
118000
118187
  });
118001
118188
  } else {
118002
118189
  proc.lastAssistantContentDescription = describeAssistantContent(am.message?.content);
@@ -118020,6 +118207,7 @@ function resetAccumulators(proc) {
118020
118207
  proc.currentToolName = null;
118021
118208
  proc.currentMcpInvocationId = null;
118022
118209
  proc.currentMcpInvocationStartedAt = null;
118210
+ proc.activeToolUseStartedAt = void 0;
118023
118211
  proc.segmentBuffer = "";
118024
118212
  proc.segmentCount = 0;
118025
118213
  proc.accumulatedToolInput = "";
@@ -118029,6 +118217,7 @@ function resetAccumulators(proc) {
118029
118217
  proc.officialMediaGenerationSatisfied = false;
118030
118218
  proc.suppressCurrentThinking = false;
118031
118219
  proc.suppressCurrentToolUse = false;
118220
+ proc.activeSubagentTaskIds?.clear();
118032
118221
  }
118033
118222
 
118034
118223
  // src/forkHistoryReplay.ts
@@ -118220,7 +118409,7 @@ function missingSubscriptionMessage(subscriptionId) {
118220
118409
  }
118221
118410
  var NODE_USER_UID = 1e3;
118222
118411
  var POST_MERGE_CONTINUATION_ROUTE_MS = 15e3;
118223
- var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-prompt-v2";
118412
+ var SCOPE_PROMPT_FINGERPRINT_REVISION = "workdir-scope-mcp-abi-prompt-v4";
118224
118413
  var BINARY_ATTACHMENT_EXT_RE = /\.(?:7z|bmp|csv|doc|docx|gif|jpeg|jpg|m4a|mov|mp3|mp4|pdf|png|ppt|pptx|rar|rtf|wav|webm|webp|xls|xlsx|zip)$/i;
118225
118414
  var DOCUMENT_READING_RULES = `DOCUMENT READING:
118226
118415
  - The built-in Read tool cannot read binary office documents such as .docx, .xls, .xlsx, .pptx, .pdf, .odt, .ods, .odp, or .rtf.
@@ -118235,6 +118424,16 @@ var MEDIA_GENERATION_RULES = `MEDIA GENERATION:
118235
118424
  - Keep media replies short. Do not print raw media URLs, request_id, task_id, polling logs, or "let me check again" narration unless the user explicitly asks for diagnostics.
118236
118425
  - When a media task is submitted or completed, write only a natural one-line note such as "\u5DF2\u5F00\u59CB\u751F\u6210\uFF0C\u6211\u4F1A\u5728\u8FD9\u91CC\u66F4\u65B0\u7ED3\u679C\u3002" or "\u751F\u6210\u597D\u4E86\uFF0C\u53EF\u4EE5\u5728\u5361\u7247\u91CC\u67E5\u770B\u3002"; let the media card show status, preview, download, copy, and regenerate actions.
118237
118426
  - If the user asks whether a Seedance task is ready, call mcp__seedance__seedance_check_task once and answer from that result. Do not loop, sleep, or invent external Seedance API endpoints.`;
118427
+ function stableFingerprintValue(value) {
118428
+ if (Array.isArray(value)) return value.map(stableFingerprintValue);
118429
+ if (!value || typeof value !== "object") return value;
118430
+ const out = {};
118431
+ for (const key of Object.keys(value).sort()) {
118432
+ const normalized = stableFingerprintValue(value[key]);
118433
+ if (normalized !== void 0) out[key] = normalized;
118434
+ }
118435
+ return out;
118436
+ }
118238
118437
  function isRecoveryDispatchTask(task) {
118239
118438
  return task.dispatchKind === "manual_continue" || task.dispatchKind === "regenerate";
118240
118439
  }
@@ -118492,13 +118691,15 @@ var AgentManager = class {
118492
118691
  agents = /* @__PURE__ */ new Map();
118493
118692
  lastUsedAt = /* @__PURE__ */ new Map();
118494
118693
  /** Scopes 被 zombie_watchdog 关闭后的"入睡"标记,acquire 重建时清除并 emit awake。 */
118495
- dormantScopes = /* @__PURE__ */ new Set();
118694
+ dormantScopes = /* @__PURE__ */ new Map();
118496
118695
  /**
118497
118696
  * zombie_watchdog 拆 runtime 时,把该 (agentId, scope) 的 groupInbox 快照到这里,
118498
118697
  * 让下一次 getOrCreate 重建 runtime 时可以恢复未读消息。仅 in-memory;
118499
118698
  * bridge 进程崩溃 / shutdownAll 时丢失,与现有 inbox 内存语义一致。
118500
118699
  */
118501
118700
  dormantGroupInboxes = /* @__PURE__ */ new Map();
118701
+ /** Spectate requested before runtime existed; value = activatedAt epoch ms. */
118702
+ pendingSpectate = /* @__PURE__ */ new Map();
118502
118703
  sessionStore;
118503
118704
  dispatchMemory = new GroupDispatchMemoryStore();
118504
118705
  dataDir;
@@ -118664,6 +118865,7 @@ var AgentManager = class {
118664
118865
  }
118665
118866
  async resolveRuntimeCwd(agentConfig, scope, requestedCwd) {
118666
118867
  let cwd = this.remapServerWorkspaceCwd(agentConfig, scope, requestedCwd);
118868
+ let fallbackForensicsId;
118667
118869
  if (!isFullyQualifiedAbsolutePath(cwd)) {
118668
118870
  const fallback = import_node_path13.default.join(this.workspacesDir, this.localScopeDirName(agentConfig, scope));
118669
118871
  logger14.error(
@@ -118678,6 +118880,23 @@ var AgentManager = class {
118678
118880
  error: new Error("workdir_not_usable_on_this_machine")
118679
118881
  }
118680
118882
  );
118883
+ fallbackForensicsId = createFallbackId();
118884
+ logFallback(logger14, {
118885
+ fallbackId: fallbackForensicsId,
118886
+ type: "cwd_sandbox",
118887
+ phase: "applied",
118888
+ expected: false,
118889
+ context: {
118890
+ agentId: agentConfig.id,
118891
+ scope: scopeKey(scope),
118892
+ platform: process.platform,
118893
+ requested: requestedCwd,
118894
+ resolved: cwd,
118895
+ reason: "not_fully_qualified",
118896
+ fallback
118897
+ },
118898
+ outcome: { result: "sandbox_fallback" }
118899
+ });
118681
118900
  cwd = fallback;
118682
118901
  }
118683
118902
  if (isRunningAsRoot() && cwd.startsWith("/root/")) {
@@ -118696,6 +118915,21 @@ var AgentManager = class {
118696
118915
  fallback,
118697
118916
  error: e
118698
118917
  });
118918
+ const fbId = fallbackForensicsId ?? createFallbackId();
118919
+ logFallback(logger14, {
118920
+ fallbackId: fbId,
118921
+ type: "cwd_sandbox",
118922
+ phase: "applied",
118923
+ expected: false,
118924
+ context: {
118925
+ agentId: agentConfig.id,
118926
+ scope: scopeKey(scope),
118927
+ reason: "mkdir_failed",
118928
+ requested: cwd,
118929
+ fallback
118930
+ },
118931
+ outcome: { result: "second_layer_fallback" }
118932
+ });
118699
118933
  await import_promises3.default.mkdir(fallback, { recursive: true });
118700
118934
  return fallback;
118701
118935
  }
@@ -118918,17 +119152,24 @@ var AgentManager = class {
118918
119152
  });
118919
119153
  return null;
118920
119154
  }
118921
- scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection) {
118922
- return (0, import_node_crypto3.createHash)("sha256").update(SCOPE_PROMPT_FINGERPRINT_REVISION).update("\0").update(agentConfig.id).update("\0").update(agentConfig.name).update("\0").update(scopeKey(scope)).update("\0").update(import_node_path13.default.normalize(agentCwd)).update("\0").update(scopesSection).digest("hex");
119155
+ scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection, externalMcpFingerprint) {
119156
+ return (0, import_node_crypto3.createHash)("sha256").update(SCOPE_PROMPT_FINGERPRINT_REVISION).update("\0").update(agentConfig.id).update("\0").update(agentConfig.name).update("\0").update(scopeKey(scope)).update("\0").update(import_node_path13.default.normalize(agentCwd)).update("\0").update(scopesSection).update("\0").update(externalMcpFingerprint).digest("hex");
119157
+ }
119158
+ externalMcpFingerprint(externalMcp) {
119159
+ const serverNames = Object.keys(externalMcp.mcpServers).sort();
119160
+ const allowedTools = [...externalMcp.allowedTools].sort();
119161
+ const toolAbi = [...externalMcp.toolAbi ?? []].sort((a, b) => a.serverName.localeCompare(b.serverName)).map(stableFingerprintValue);
119162
+ if (serverNames.length === 0 && allowedTools.length === 0 && toolAbi.length === 0) return "";
119163
+ return JSON.stringify({ serverNames, allowedTools, toolAbi });
118923
119164
  }
118924
- discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint) {
119165
+ discardSessionIfScopePromptChanged(agentConfig, scope, sessionId, fingerprint, options = {}) {
118925
119166
  const previous = this.sessionStore.getPromptFingerprint(agentConfig.id, scope);
118926
119167
  if (!sessionId) {
118927
119168
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
118928
119169
  return null;
118929
119170
  }
118930
119171
  if (previous === fingerprint) return sessionId;
118931
- if (!previous && scope.kind === "single") {
119172
+ if (!previous && scope.kind === "single" && options.clearLegacySingleSession !== true) {
118932
119173
  this.sessionStore.setPromptFingerprint(agentConfig.id, scope, fingerprint);
118933
119174
  logger14.info("Retaining legacy single-scope session while recording prompt fingerprint", {
118934
119175
  agentId: agentConfig.id,
@@ -118948,7 +119189,8 @@ var AgentManager = class {
118948
119189
  sessionId,
118949
119190
  previousFingerprint: previous,
118950
119191
  nextFingerprint: fingerprint,
118951
- revision: SCOPE_PROMPT_FINGERPRINT_REVISION
119192
+ revision: SCOPE_PROMPT_FINGERPRINT_REVISION,
119193
+ reason: options.reason ?? "scope_prompt_changed"
118952
119194
  });
118953
119195
  return null;
118954
119196
  }
@@ -118997,6 +119239,7 @@ var AgentManager = class {
118997
119239
  logger14.info("Evicting idle Agent query", { agentId: proc.agentId, scope: scopeKey(proc.scope) });
118998
119240
  const runtime = this.asRuntime(proc);
118999
119241
  this.clearQuietFlushTimer(runtime);
119242
+ this.teardownSpectate(runtime);
119000
119243
  try {
119001
119244
  runtime.inputController.close();
119002
119245
  await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
@@ -119016,6 +119259,7 @@ var AgentManager = class {
119016
119259
  const runtime = this.asRuntime(proc);
119017
119260
  const key = runtimeKey(proc.agentId, proc.scope);
119018
119261
  this.clearQuietFlushTimer(runtime);
119262
+ this.teardownSpectate(runtime);
119019
119263
  runtime.currentTask = null;
119020
119264
  runtime.injectedTasks = [];
119021
119265
  runtime.mergedTasks = [];
@@ -119052,6 +119296,7 @@ var AgentManager = class {
119052
119296
  evictIdle() {
119053
119297
  const now = Date.now();
119054
119298
  const { idleTimeoutMs, workingSilenceTimeoutMs } = this.queryConfig;
119299
+ const stallWarnAfterMs = Math.min(9e4, this.queryConfig.replyStallTimeoutMs);
119055
119300
  for (const [key, proc] of this.agents) {
119056
119301
  if (!this.isEvictable(proc)) continue;
119057
119302
  const runtime = this.asRuntime(proc);
@@ -119062,26 +119307,62 @@ var AgentManager = class {
119062
119307
  for (const [, proc] of this.agents) {
119063
119308
  if (proc.status !== "working") continue;
119064
119309
  const runtime = this.asRuntime(proc);
119310
+ if (runtime.currentTask) {
119311
+ const sinceEventMs = now - proc.lastSdkEventAt;
119312
+ if (sinceEventMs > stallWarnAfterMs && !proc.stallWarned) {
119313
+ proc.stallWarned = true;
119314
+ const openTool = this.latestOpenToolUse(proc);
119315
+ logger14.warn("Reply stall onset: in-flight reply silent", {
119316
+ agentId: proc.agentId,
119317
+ scope: scopeKey(proc.scope),
119318
+ sinceEventMs,
119319
+ replyStallTimeoutMs: this.queryConfig.replyStallTimeoutMs,
119320
+ workingSilenceTimeoutMs,
119321
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
119322
+ replyMessageId: runtime.currentTask.replyMessageId,
119323
+ model: proc.model ?? "(unknown)",
119324
+ lastSdkEvent: proc.lastSdkEventInfo,
119325
+ hasActiveToolUse: runtime.activeToolUseStartedAt != null || openTool != null,
119326
+ activeToolUseAgeMs: runtime.activeToolUseStartedAt != null ? now - runtime.activeToolUseStartedAt : null,
119327
+ openToolName: openTool?.toolName ?? proc.currentToolName ?? null,
119328
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null,
119329
+ subagentInFlight: (runtime.activeSubagentTaskIds?.size ?? 0) > 0,
119330
+ activeSubagentCount: runtime.activeSubagentTaskIds?.size ?? 0,
119331
+ busyReason: this.busyReason(proc)
119332
+ });
119333
+ }
119334
+ }
119335
+ const busyReason = this.busyReason(runtime);
119336
+ const busy = busyReason !== null;
119337
+ if (runtime.currentTask && !busy && now - proc.lastSdkEventAt > this.queryConfig.replyStallTimeoutMs) {
119338
+ void this.recoverStalledReply(proc, now - proc.lastSdkEventAt);
119339
+ continue;
119340
+ }
119065
119341
  const hasInjectedBacklog = runtime.injectedTasks.length > 0;
119066
- const effectiveTimeoutMs = hasInjectedBacklog ? workingSilenceTimeoutMs * 2 : workingSilenceTimeoutMs;
119342
+ const baseCeilingMs = busy ? Math.max(workingSilenceTimeoutMs, this.queryConfig.busySilenceTimeoutMs ?? 0) : workingSilenceTimeoutMs;
119343
+ const effectiveTimeoutMs = hasInjectedBacklog ? baseCeilingMs * 2 : baseCeilingMs;
119067
119344
  const silentMs = now - proc.lastSdkEventAt;
119068
119345
  if (silentMs <= effectiveTimeoutMs) {
119069
- if (hasInjectedBacklog && silentMs > workingSilenceTimeoutMs) {
119070
- logger14.warn(
119071
- "Zombie watchdog: working runtime silent past base timeout but has queued tasks; granting extended grace",
119072
- {
119073
- agentId: proc.agentId,
119074
- scope: scopeKey(proc.scope),
119075
- silentMs,
119076
- baseTimeoutMs: workingSilenceTimeoutMs,
119077
- effectiveTimeoutMs,
119078
- injectedTaskCount: runtime.injectedTasks.length,
119079
- replyMessageId: proc.currentTask?.replyMessageId
119080
- }
119081
- );
119346
+ if (silentMs > workingSilenceTimeoutMs) {
119347
+ logger14.warn("Zombie watchdog: working runtime silent past base timeout; granting extended grace", {
119348
+ agentId: proc.agentId,
119349
+ scope: scopeKey(proc.scope),
119350
+ silentMs,
119351
+ baseTimeoutMs: workingSilenceTimeoutMs,
119352
+ baseCeilingMs,
119353
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
119354
+ effectiveTimeoutMs,
119355
+ busy,
119356
+ busyReason,
119357
+ injectedTaskCount: runtime.injectedTasks.length,
119358
+ replyMessageId: proc.currentTask?.replyMessageId
119359
+ });
119082
119360
  }
119083
119361
  continue;
119084
119362
  }
119363
+ const zombieOpenTool = this.latestOpenToolUse(proc);
119364
+ const watchdogDetectedAt = Date.now();
119365
+ const watchdogFallbackId = createFallbackId();
119085
119366
  logger14.warn("Zombie watchdog: working runtime silent too long, tearing down", {
119086
119367
  agentId: proc.agentId,
119087
119368
  scope: scopeKey(proc.scope),
@@ -119089,12 +119370,46 @@ var AgentManager = class {
119089
119370
  lastSdkEventAt: new Date(proc.lastSdkEventAt).toISOString(),
119090
119371
  workingSilenceTimeoutMs,
119091
119372
  effectiveTimeoutMs,
119373
+ baseCeilingMs,
119374
+ busy,
119375
+ busySilenceTimeoutMs: this.queryConfig.busySilenceTimeoutMs ?? null,
119092
119376
  replyMessageId: proc.currentTask?.replyMessageId,
119093
119377
  injectedTaskCount: runtime.injectedTasks.length,
119094
119378
  hadInjectedBacklog: hasInjectedBacklog,
119095
- inboxSize: proc.groupInbox.length
119379
+ inboxSize: proc.groupInbox.length,
119380
+ fallbackId: watchdogFallbackId,
119381
+ // Breadcrumb: what the turn was waiting on when it timed out. A hung subagent
119382
+ // (open `Task` tool_use) lands here now that the fast path skips active tools.
119383
+ model: proc.model ?? "(unknown)",
119384
+ lastSdkEvent: proc.lastSdkEventInfo,
119385
+ openToolName: zombieOpenTool?.toolName ?? proc.currentToolName ?? null,
119386
+ activeToolUseAgeMs: runtime.activeToolUseStartedAt != null ? now - runtime.activeToolUseStartedAt : null,
119387
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null,
119388
+ subagentInFlight: (runtime.activeSubagentTaskIds?.size ?? 0) > 0,
119389
+ activeSubagentCount: runtime.activeSubagentTaskIds?.size ?? 0,
119390
+ busyReason: this.busyReason(proc)
119391
+ });
119392
+ logFallback(logger14, {
119393
+ fallbackId: watchdogFallbackId,
119394
+ type: "zombie_watchdog",
119395
+ phase: "detected",
119396
+ expected: false,
119397
+ traceId: proc.currentTask?.traceId,
119398
+ context: {
119399
+ agentId: proc.agentId,
119400
+ scope: scopeKey(proc.scope),
119401
+ silentMs,
119402
+ replyMessageId: proc.currentTask?.replyMessageId ?? null,
119403
+ openToolName: zombieOpenTool?.toolName ?? proc.currentToolName ?? null,
119404
+ lastSdkEventType: proc.lastSdkEventInfo?.type ?? null,
119405
+ injectedTaskCount: runtime.injectedTasks.length,
119406
+ inboxSize: proc.groupInbox.length
119407
+ }
119408
+ });
119409
+ void this.closeRuntime(proc, "zombie_watchdog", {
119410
+ fallbackId: watchdogFallbackId,
119411
+ detectedAt: watchdogDetectedAt
119096
119412
  });
119097
- void this.closeRuntime(proc, "zombie_watchdog");
119098
119413
  }
119099
119414
  }
119100
119415
  /**
@@ -119341,7 +119656,7 @@ ${cfg.instructions.trim()}` : "";
119341
119656
  agentId: agentConfig.id,
119342
119657
  capabilityTier: cfg.capabilityTier,
119343
119658
  isSmith: smithAgent
119344
- }) ?? { mcpServers: {}, allowedTools: [] };
119659
+ }) ?? { mcpServers: {}, allowedTools: [], toolAbi: [] };
119345
119660
  logger14.info("External MCP resolved for runtime", {
119346
119661
  agentId: agentConfig.id,
119347
119662
  scope: scopeKey(scope),
@@ -119358,11 +119673,16 @@ ${cfg.instructions.trim()}` : "";
119358
119673
  }
119359
119674
  const notebookSection = this.buildNotebookSection(agentConfig.id);
119360
119675
  const scopesSection = this.buildScopesSection(agentConfig, scope, agentCwd);
119676
+ const externalMcpFingerprint = this.externalMcpFingerprint(externalMcp);
119361
119677
  savedSessionId = this.discardSessionIfScopePromptChanged(
119362
119678
  agentConfig,
119363
119679
  scope,
119364
119680
  savedSessionId,
119365
- this.scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection)
119681
+ this.scopePromptFingerprint(agentConfig, scope, agentCwd, scopesSection, externalMcpFingerprint),
119682
+ {
119683
+ clearLegacySingleSession: externalMcpFingerprint.length > 0,
119684
+ reason: externalMcpFingerprint.length > 0 ? "external_mcp_abi_changed" : "scope_prompt_changed"
119685
+ }
119366
119686
  );
119367
119687
  let forkHistorySection = "";
119368
119688
  if (!savedSessionId && scope.kind === "single") {
@@ -119383,6 +119703,8 @@ ${cfg.instructions.trim()}` : "";
119383
119703
  }
119384
119704
  }
119385
119705
  const cronLockSnapshot = readCronLockSnapshot();
119706
+ const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
119707
+ const disallowedToolsForRuntime = builtinWebSearchAllowed ? [] : ["WebSearch"];
119386
119708
  logger14.info("Creating Agent query", {
119387
119709
  agentId: agentConfig.id,
119388
119710
  scope: scopeKey(scope),
@@ -119391,6 +119713,8 @@ ${cfg.instructions.trim()}` : "";
119391
119713
  sessionId: savedSessionId,
119392
119714
  forkHistoryReplay: forkHistorySection.length > 0,
119393
119715
  model: cfg.model ?? "(default)",
119716
+ builtinWebSearchAllowed,
119717
+ disallowedTools: disallowedToolsForRuntime,
119394
119718
  // Diagnostic: who currently owns Claude's global cron lock (~/.claude/scheduled_tasks.lock).
119395
119719
  // Cron is process-internal but the binary uses this singleton lock to elect ONE scheduler
119396
119720
  // among concurrent claude subprocesses. If lock is held by another session at spawn time,
@@ -119402,7 +119726,6 @@ ${cfg.instructions.trim()}` : "";
119402
119726
  });
119403
119727
  const planModeRef = { active: false, denyCount: 0 };
119404
119728
  const mediaGenerationTurnGuard = createOfficialMediaGenerationTurnGuard();
119405
- const builtinWebSearchAllowed = this.queryConfig.allowBuiltinWebSearch;
119406
119729
  const options = {
119407
119730
  cwd: agentCwd,
119408
119731
  systemPrompt: {
@@ -119469,6 +119792,8 @@ ${cfg.instructions.trim()}` : "";
119469
119792
  ] : [],
119470
119793
  ...externalMcp.allowedTools
119471
119794
  ],
119795
+ // Server-side WebSearch bypasses canUseTool; disallowedTools removes it from model context.
119796
+ disallowedTools: disallowedToolsForRuntime,
119472
119797
  mcpServers: { ...externalMcp.mcpServers, neural: neuralServer },
119473
119798
  includePartialMessages: true,
119474
119799
  // Plan mode custom workflow instructions. When setPermissionMode('plan') is
@@ -119795,6 +120120,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119795
120120
  currentTask: null,
119796
120121
  currentTaskStartedAt: 0,
119797
120122
  lastSdkEventAt: Date.now(),
120123
+ model: (typeof options.model === "string" ? options.model : cfg.model) ?? null,
119798
120124
  compactRequested: false,
119799
120125
  compactInProgress: false,
119800
120126
  contextOverflowLockedUntil: 0,
@@ -119806,13 +120132,18 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119806
120132
  currentToolName: null,
119807
120133
  currentMcpInvocationId: null,
119808
120134
  currentMcpInvocationStartedAt: null,
120135
+ activeSubagentTaskIds: /* @__PURE__ */ new Set(),
119809
120136
  mcpAuditRecorder: this.mcpAuditRecorder,
119810
120137
  segmentBuffer: "",
119811
120138
  segmentCount: 0,
119812
120139
  accumulatedToolInput: "",
119813
120140
  planModeRef,
119814
120141
  mediaGenerationTurnGuard,
119815
- groupInbox: []
120142
+ groupInbox: [],
120143
+ spectating: false,
120144
+ spectateActivatedAt: 0,
120145
+ spectateViewing: false,
120146
+ spectateTtlExpired: false
119816
120147
  };
119817
120148
  const runtime = Object.assign(proc, {
119818
120149
  query: agentQuery,
@@ -119824,7 +120155,8 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119824
120155
  createdAt: Date.now(),
119825
120156
  supportsVision: modelInputMode === "vision" && cfg.supportsVision !== false,
119826
120157
  modelInputMode,
119827
- quietFlushTimer: null
120158
+ quietFlushTimer: null,
120159
+ spectateRevertTimer: null
119828
120160
  });
119829
120161
  logger14.info("Agent model input mode resolved", {
119830
120162
  agentId: agentConfig.id,
@@ -119849,6 +120181,7 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119849
120181
  } else {
119850
120182
  this.dormantGroupInboxes.delete(key);
119851
120183
  }
120184
+ const dormantMeta = this.dormantScopes.get(key);
119852
120185
  if (this.dormantScopes.delete(key)) {
119853
120186
  this.emit({
119854
120187
  type: "agent:awake",
@@ -119860,7 +120193,44 @@ Do NOT use "..." as content \u2014 write specific, project-relevant content.`;
119860
120193
  });
119861
120194
  logger14.info("Agent scope awakened after dormant", {
119862
120195
  agentId: agentConfig.id,
119863
- scope: scopeKey(scope)
120196
+ scope: scopeKey(scope),
120197
+ ...dormantMeta ? {
120198
+ fallbackId: dormantMeta.fallbackId,
120199
+ dormantDurationMs: Date.now() - dormantMeta.detectedAt,
120200
+ dataLossSuspected: dormantMeta.droppedTaskCount > 0
120201
+ } : {}
120202
+ });
120203
+ if (dormantMeta) {
120204
+ logFallback(logger14, {
120205
+ fallbackId: dormantMeta.fallbackId,
120206
+ type: "zombie_watchdog",
120207
+ phase: "outcome",
120208
+ expected: false,
120209
+ context: {
120210
+ agentId: agentConfig.id,
120211
+ scope: scopeKey(scope)
120212
+ },
120213
+ outcome: {
120214
+ result: "recovered_rebuilt",
120215
+ durationMs: Date.now() - dormantMeta.detectedAt,
120216
+ dataLossSuspected: dormantMeta.droppedTaskCount > 0
120217
+ }
120218
+ });
120219
+ }
120220
+ }
120221
+ const pendingSpectateAt = this.pendingSpectate.get(key);
120222
+ if (pendingSpectateAt != null) {
120223
+ this.pendingSpectate.delete(key);
120224
+ runtime.spectating = true;
120225
+ runtime.spectateViewing = false;
120226
+ runtime.spectateActivatedAt = pendingSpectateAt;
120227
+ runtime.spectateTtlExpired = false;
120228
+ this.armSpectateTimer(runtime);
120229
+ this.emitSpectateState(runtime, true, "started");
120230
+ logger14.info("Applied pending spectate on runtime create", {
120231
+ agentId: agentConfig.id,
120232
+ scope: scopeKey(scope),
120233
+ activatedAt: pendingSpectateAt
119864
120234
  });
119865
120235
  }
119866
120236
  if (proc.groupInbox.length > 0 && this.isRuntimeIdleForInboxFlush(runtime)) {
@@ -120998,7 +121368,6 @@ ${lines.join("\n")}`;
120998
121368
  compactTrigger: "context_watermark",
120999
121369
  injectedTasksWaiting: runtime.injectedTasks.length,
121000
121370
  compactPromptLen: compactPrompt.length,
121001
- promptSample: compactPrompt.slice(0, 80),
121002
121371
  traceId: compactTraceId
121003
121372
  });
121004
121373
  runtime.inputController.push(compactPrompt, runtime.ccSessionId ?? "");
@@ -121280,7 +121649,7 @@ ${lines.join("\n")}`;
121280
121649
  const enveloped = buildInnerVoiceEnvelope(payloadWithTrigger, ctx);
121281
121650
  const task = {
121282
121651
  content: enveloped,
121283
- replyMessageId: createMessageId(),
121652
+ replyMessageId: createNeuralSendReplyMessageId(),
121284
121653
  conversationId: payload.conversationId,
121285
121654
  traceId: createTraceId(),
121286
121655
  groupId: payload.groupId
@@ -121526,7 +121895,7 @@ ${lines.join("\n")}`;
121526
121895
  this.dormantScopes.delete(key);
121527
121896
  this.dormantGroupInboxes.delete(key);
121528
121897
  }
121529
- for (const key of [...this.dormantScopes].filter(
121898
+ for (const key of [...this.dormantScopes.keys()].filter(
121530
121899
  (k) => k === agentId || k.startsWith(`${agentId}::`)
121531
121900
  )) {
121532
121901
  this.dormantScopes.delete(key);
@@ -121574,7 +121943,7 @@ ${lines.join("\n")}`;
121574
121943
  async reloadAgentScopes(agentId, reason) {
121575
121944
  this.sessionStore.deleteAllForAgent(agentId);
121576
121945
  this.dispatchMemory.deleteAllForAgent(agentId);
121577
- for (const key of [...this.dormantScopes].filter(
121946
+ for (const key of [...this.dormantScopes.keys()].filter(
121578
121947
  (k) => k === agentId || k.startsWith(`${agentId}::`)
121579
121948
  )) {
121580
121949
  this.dormantScopes.delete(key);
@@ -121726,6 +122095,125 @@ ${lines.join("\n")}`;
121726
122095
  void this.terminateScope(proc.agentId, proc.scope);
121727
122096
  }
121728
122097
  }
122098
+ /** Control spectate capture/push for one scoped runtime. */
122099
+ async setSpectate(agentId, scope, action) {
122100
+ const key = runtimeKey(agentId, scope);
122101
+ const proc = this.agents.get(key);
122102
+ if (!proc || proc.status === "dead") {
122103
+ if (action === "start") {
122104
+ this.pendingSpectate.set(key, Date.now());
122105
+ logger14.info("setSpectate: runtime missing, pending start", { agentId, scope: scopeKey(scope) });
122106
+ }
122107
+ return;
122108
+ }
122109
+ const runtime = this.asRuntime(proc);
122110
+ switch (action) {
122111
+ case "start":
122112
+ runtime.spectating = true;
122113
+ runtime.spectateViewing = true;
122114
+ runtime.spectateActivatedAt = Date.now();
122115
+ runtime.spectateTtlExpired = false;
122116
+ this.pendingSpectate.delete(key);
122117
+ this.armSpectateTimer(runtime);
122118
+ this.emitSpectateState(runtime, true, "started");
122119
+ logger14.info("Spectate started", { agentId, scope: scopeKey(scope) });
122120
+ break;
122121
+ case "enter_view":
122122
+ runtime.spectateViewing = true;
122123
+ logger14.info("Spectate enter_view", { agentId, scope: scopeKey(scope) });
122124
+ break;
122125
+ case "leave_view":
122126
+ runtime.spectateViewing = false;
122127
+ logger14.info("Spectate leave_view", {
122128
+ agentId,
122129
+ scope: scopeKey(scope),
122130
+ ttlExpired: runtime.spectateTtlExpired
122131
+ });
122132
+ if (runtime.spectateTtlExpired) {
122133
+ this.stopSpectate(runtime, "ttl_expired");
122134
+ }
122135
+ break;
122136
+ case "stop":
122137
+ this.stopSpectate(runtime, "stopped");
122138
+ this.pendingSpectate.delete(key);
122139
+ logger14.info("Spectate stopped", { agentId, scope: scopeKey(scope) });
122140
+ break;
122141
+ default:
122142
+ break;
122143
+ }
122144
+ }
122145
+ spectateTtlMs() {
122146
+ return Number(process.env.AHCHAT_BRIDGE_SPECTATE_TTL_MS) || 36e5;
122147
+ }
122148
+ armSpectateTimer(runtime) {
122149
+ this.clearSpectateTimer(runtime);
122150
+ if (!runtime.spectating || runtime.spectateActivatedAt <= 0) return;
122151
+ const ttlMs = this.spectateTtlMs();
122152
+ const elapsed = Date.now() - runtime.spectateActivatedAt;
122153
+ const delay = Math.max(0, ttlMs - elapsed);
122154
+ runtime.spectateRevertTimer = setTimeout(() => {
122155
+ runtime.spectateRevertTimer = null;
122156
+ runtime.spectateTtlExpired = true;
122157
+ logger14.info("Spectate TTL expired", {
122158
+ agentId: runtime.agentId,
122159
+ scope: scopeKey(runtime.scope),
122160
+ viewing: runtime.spectateViewing
122161
+ });
122162
+ if (!runtime.spectateViewing) {
122163
+ this.stopSpectate(runtime, "ttl_expired");
122164
+ }
122165
+ }, delay);
122166
+ }
122167
+ clearSpectateTimer(runtime) {
122168
+ if (runtime.spectateRevertTimer != null) {
122169
+ clearTimeout(runtime.spectateRevertTimer);
122170
+ runtime.spectateRevertTimer = null;
122171
+ }
122172
+ }
122173
+ stopSpectate(runtime, reason) {
122174
+ const wasActive = runtime.spectating;
122175
+ this.clearSpectateTimer(runtime);
122176
+ runtime.spectating = false;
122177
+ runtime.spectateViewing = false;
122178
+ runtime.spectateTtlExpired = false;
122179
+ runtime.spectateActivatedAt = 0;
122180
+ if (wasActive) {
122181
+ this.emitSpectateState(runtime, false, reason);
122182
+ logger14.info("Spectate deactivated", {
122183
+ agentId: runtime.agentId,
122184
+ scope: scopeKey(runtime.scope),
122185
+ reason
122186
+ });
122187
+ }
122188
+ }
122189
+ emitSpectateState(runtime, active, reason) {
122190
+ const scopePayload = runtime.scope.kind === "single" ? { kind: "single" } : { kind: "group", groupId: runtime.scope.groupId };
122191
+ this.emit({
122192
+ type: "spectate:state",
122193
+ payload: {
122194
+ agentId: runtime.agentId,
122195
+ scope: scopePayload,
122196
+ active,
122197
+ expiresAt: active ? runtime.spectateActivatedAt + this.spectateTtlMs() : void 0,
122198
+ reason,
122199
+ traceId: createTraceId()
122200
+ }
122201
+ });
122202
+ logger14.info("Spectate state emitted", {
122203
+ agentId: runtime.agentId,
122204
+ scope: scopeKey(runtime.scope),
122205
+ active,
122206
+ reason,
122207
+ expiresAt: active ? runtime.spectateActivatedAt + this.spectateTtlMs() : void 0
122208
+ });
122209
+ }
122210
+ teardownSpectate(runtime) {
122211
+ if (runtime.spectating) {
122212
+ this.stopSpectate(runtime, "runtime_gone");
122213
+ } else {
122214
+ this.clearSpectateTimer(runtime);
122215
+ }
122216
+ }
121729
122217
  /** Stop one scoped SDK runtime (workdir change). */
121730
122218
  async terminateScope(agentId, scope) {
121731
122219
  const key = runtimeKey(agentId, scope);
@@ -121734,6 +122222,7 @@ ${lines.join("\n")}`;
121734
122222
  logger14.info("terminateScope: no active runtime", { agentId, scope: scopeKey(scope) });
121735
122223
  this.dormantScopes.delete(key);
121736
122224
  this.dormantGroupInboxes.delete(key);
122225
+ this.pendingSpectate.delete(key);
121737
122226
  this.sessionStore.delete(agentId, scope);
121738
122227
  this.dispatchMemory.deleteScope(agentId, scope);
121739
122228
  return;
@@ -121756,7 +122245,7 @@ ${lines.join("\n")}`;
121756
122245
  this.dispatchMemory.deleteScope(agentId, scope);
121757
122246
  logger14.info("terminateScope: scoped query removed", { agentId, scope: scopeKey(scope) });
121758
122247
  }
121759
- async closeRuntime(proc, reason) {
122248
+ async closeRuntime(proc, reason, watchdogForensics) {
121760
122249
  const key = runtimeKey(proc.agentId, proc.scope);
121761
122250
  if (proc.status === "dead") return;
121762
122251
  const runtime = this.asRuntime(proc);
@@ -121833,12 +122322,13 @@ ${lines.join("\n")}`;
121833
122322
  runtime.currentTask = null;
121834
122323
  if (isWatchdog) {
121835
122324
  const preservedInbox = proc.groupInbox;
121836
- if (preservedInbox.length > 0) {
122325
+ const preservedInboxSize = preservedInbox.length;
122326
+ if (preservedInboxSize > 0) {
121837
122327
  this.dormantGroupInboxes.set(key, [...preservedInbox]);
121838
122328
  logger14.info("Preserving groupInbox for dormant agent", {
121839
122329
  agentId,
121840
122330
  scope: scopeKey(proc.scope),
121841
- preservedInboxSize: preservedInbox.length,
122331
+ preservedInboxSize,
121842
122332
  preservedEntries: preservedInbox.map((e) => ({
121843
122333
  ackId: e.ackId,
121844
122334
  sender: e.senderName,
@@ -121847,7 +122337,26 @@ ${lines.join("\n")}`;
121847
122337
  }))
121848
122338
  });
121849
122339
  }
121850
- this.dormantScopes.add(key);
122340
+ const effectiveFallbackId = watchdogForensics?.fallbackId ?? createFallbackId();
122341
+ logFallback(logger14, {
122342
+ fallbackId: effectiveFallbackId,
122343
+ type: "zombie_watchdog",
122344
+ phase: "applied",
122345
+ expected: false,
122346
+ traceId: dormantTraceId,
122347
+ context: {
122348
+ agentId,
122349
+ scope: scopeKey(proc.scope),
122350
+ droppedTaskCount: droppedAckIds.length,
122351
+ preservedInboxSize,
122352
+ sessionDeleted: false
122353
+ }
122354
+ });
122355
+ this.dormantScopes.set(key, {
122356
+ fallbackId: effectiveFallbackId,
122357
+ detectedAt: watchdogForensics?.detectedAt ?? Date.now(),
122358
+ droppedTaskCount: droppedAckIds.length
122359
+ });
121851
122360
  this.emit({
121852
122361
  type: "agent:dormant",
121853
122362
  payload: {
@@ -121862,13 +122371,15 @@ ${lines.join("\n")}`;
121862
122371
  agentId,
121863
122372
  scope: scopeKey(proc.scope),
121864
122373
  droppedTaskCount: droppedAckIds.length,
121865
- preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0
122374
+ preservedInboxSize: this.dormantGroupInboxes.get(key)?.length ?? 0,
122375
+ fallbackId: effectiveFallbackId
121866
122376
  });
121867
122377
  }
121868
122378
  proc.status = "dead";
121869
122379
  this.agents.delete(key);
121870
122380
  this.lastUsedAt.delete(key);
121871
122381
  this.clearQuietFlushTimer(runtime);
122382
+ this.teardownSpectate(runtime);
121872
122383
  try {
121873
122384
  runtime.inputController.close();
121874
122385
  await this.awaitQueryReturn(runtime.query, 5e3, agentId);
@@ -121885,6 +122396,165 @@ ${lines.join("\n")}`;
121885
122396
  cwd: proc.cwd
121886
122397
  });
121887
122398
  }
122399
+ /**
122400
+ * Emit `agent:error` for the active reply and every queued/merged/buffered task,
122401
+ * then clear those queues. Used by both the SDK stream-crash path and the
122402
+ * reply-stall watchdog so a torn-down runtime never leaves a carrier reply
122403
+ * stuck in-flight on the server (which would keep absorbing new user messages
122404
+ * as steers of a dead turn).
122405
+ */
122406
+ failPendingTasksWithError(runtime, errorText, fallbackId) {
122407
+ const pending = [];
122408
+ if (runtime.currentTask) pending.push(runtime.currentTask);
122409
+ pending.push(...runtime.injectedTasks, ...runtime.mergedTasks, ...runtime.planModeBuffer);
122410
+ runtime.currentTask = null;
122411
+ runtime.injectedTasks = [];
122412
+ runtime.mergedTasks = [];
122413
+ runtime.planModeBuffer = [];
122414
+ if (pending.length === 0) return { pendingCount: 0 };
122415
+ const carrier = pending[0];
122416
+ const mergedTasks = pending.slice(1);
122417
+ logger14.warn("Pending tasks failure consolidated", {
122418
+ agentId: runtime.agentId,
122419
+ scope: scopeKey(runtime.scope),
122420
+ pendingCount: pending.length,
122421
+ carrierAckId: carrier.replyMessageId,
122422
+ mergedAckIds: mergedTasks.map((t) => t.replyMessageId),
122423
+ traceId: carrier.traceId,
122424
+ ...fallbackId ? { fallbackId } : {}
122425
+ });
122426
+ this.emit({
122427
+ type: "agent:error",
122428
+ payload: {
122429
+ agentId: runtime.agentId,
122430
+ conversationId: carrier.conversationId,
122431
+ ackId: carrier.replyMessageId,
122432
+ traceId: carrier.traceId,
122433
+ error: errorText
122434
+ }
122435
+ });
122436
+ for (const task of mergedTasks) {
122437
+ this.emit({
122438
+ type: "agent:merged",
122439
+ payload: {
122440
+ agentId: runtime.agentId,
122441
+ conversationId: task.conversationId,
122442
+ ackId: task.replyMessageId,
122443
+ mergedIntoAckId: carrier.replyMessageId,
122444
+ groupId: task.groupId,
122445
+ traceId: task.traceId
122446
+ }
122447
+ });
122448
+ }
122449
+ return { pendingCount: pending.length };
122450
+ }
122451
+ /**
122452
+ * Recover an in-flight reply that started but went silent past
122453
+ * `replyStallTimeoutMs` (see the reply-stall fast path in `evictIdle`). The
122454
+ * underlying SDK turn is wedged with no observable progress and no error, so:
122455
+ * 1. clear the (likely interrupted/dangling) session so the next dispatch
122456
+ * starts fresh instead of resuming the same wedged transcript;
122457
+ * 2. release the carrier reply + queued steers via `agent:error` so the
122458
+ * client stops waiting and the next user message starts a brand-new reply;
122459
+ * 3. tear the wedged runtime down.
122460
+ */
122461
+ async recoverStalledReply(proc, silentMs) {
122462
+ if (proc.status === "dead") return;
122463
+ const runtime = this.asRuntime(proc);
122464
+ const key = runtimeKey(proc.agentId, proc.scope);
122465
+ const replyStallFallbackId = createFallbackId();
122466
+ const stallTraceId = proc.currentTask?.traceId;
122467
+ logger14.warn("Reply stall watchdog: in-flight reply silent too long, recovering", {
122468
+ agentId: proc.agentId,
122469
+ scope: scopeKey(proc.scope),
122470
+ silentMs,
122471
+ replyStallTimeoutMs: this.queryConfig.replyStallTimeoutMs,
122472
+ replyMessageId: proc.currentTask?.replyMessageId,
122473
+ injectedTaskCount: runtime.injectedTasks.length,
122474
+ lastSdkEventAt: new Date(proc.lastSdkEventAt).toISOString(),
122475
+ fallbackId: replyStallFallbackId,
122476
+ // Breadcrumb: what the wedged turn was doing the instant it went silent.
122477
+ // (subagent Task call? mid tool_use? which provider?) — the difference
122478
+ // between a one-off and a systemic provider/tool stall.
122479
+ model: proc.model ?? "(unknown)",
122480
+ lastSdkEvent: proc.lastSdkEventInfo,
122481
+ currentBlockType: proc.currentBlockType,
122482
+ currentToolName: proc.currentToolName,
122483
+ openMcpInvocationId: proc.currentMcpInvocationId ?? null
122484
+ });
122485
+ logFallback(logger14, {
122486
+ fallbackId: replyStallFallbackId,
122487
+ type: "reply_stall",
122488
+ phase: "detected",
122489
+ expected: false,
122490
+ traceId: stallTraceId,
122491
+ context: {
122492
+ agentId: proc.agentId,
122493
+ scope: scopeKey(proc.scope),
122494
+ silentMs,
122495
+ model: proc.model ?? "(unknown)",
122496
+ replyMessageId: proc.currentTask?.replyMessageId ?? null,
122497
+ currentToolName: proc.currentToolName ?? null,
122498
+ lastSdkEventType: proc.lastSdkEventInfo?.type ?? null,
122499
+ injectedTaskCount: runtime.injectedTasks.length,
122500
+ mergedTaskCount: runtime.mergedTasks.length,
122501
+ planModeBufferCount: runtime.planModeBuffer.length
122502
+ }
122503
+ });
122504
+ this.sessionStore.delete(proc.agentId, proc.scope);
122505
+ this.dispatchMemory.deleteScope(proc.agentId, proc.scope);
122506
+ const failSummary = this.failPendingTasksWithError(
122507
+ runtime,
122508
+ "\u56DE\u590D\u957F\u65F6\u95F4\u65E0\u54CD\u5E94\uFF0C\u5DF2\u91CD\u7F6E\u8BE5\u4F1A\u8BDD\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\u3002",
122509
+ replyStallFallbackId
122510
+ );
122511
+ proc.status = "dead";
122512
+ this.agents.delete(key);
122513
+ this.lastUsedAt.delete(key);
122514
+ this.clearQuietFlushTimer(runtime);
122515
+ let queryCloseOk = true;
122516
+ try {
122517
+ runtime.inputController.close();
122518
+ await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
122519
+ } catch (e) {
122520
+ queryCloseOk = false;
122521
+ logger14.error("reply_stall: close query failed", {
122522
+ agentId: proc.agentId,
122523
+ scope: scopeKey(proc.scope),
122524
+ error: e
122525
+ });
122526
+ }
122527
+ logFallback(logger14, {
122528
+ fallbackId: replyStallFallbackId,
122529
+ type: "reply_stall",
122530
+ phase: "applied",
122531
+ expected: false,
122532
+ traceId: stallTraceId,
122533
+ context: {
122534
+ agentId: proc.agentId,
122535
+ scope: scopeKey(proc.scope),
122536
+ sessionDeleted: true,
122537
+ failedTaskCount: failSummary.pendingCount,
122538
+ queryClosed: queryCloseOk
122539
+ }
122540
+ });
122541
+ logFallback(logger14, {
122542
+ fallbackId: replyStallFallbackId,
122543
+ type: "reply_stall",
122544
+ phase: "outcome",
122545
+ expected: false,
122546
+ traceId: stallTraceId,
122547
+ context: {
122548
+ agentId: proc.agentId,
122549
+ scope: scopeKey(proc.scope),
122550
+ failedTaskCount: failSummary.pendingCount
122551
+ },
122552
+ outcome: {
122553
+ result: "session_reset_awaiting_user",
122554
+ dataLossSuspected: failSummary.pendingCount > 0
122555
+ }
122556
+ });
122557
+ }
121888
122558
  async recoverFromRestart(agents) {
121889
122559
  const lockSnapshot = readCronLockSnapshot();
121890
122560
  logger14.info("Recovering Agent sessions after restart", {
@@ -122007,58 +122677,7 @@ ${lines.join("\n")}`;
122007
122677
  this.lastUsedAt.delete(key);
122008
122678
  const errorText = isResumeFail ? `\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\uFF08${errMsg}\uFF09` : `Agent query crashed: ${errMsg}`;
122009
122679
  const emittedErrorText = isUnsupportedVisionInput ? "Current model/backend does not support image multimodal input. This image message failed; AHChat has cleared this scope session and future messages will send attachments as paths/text references." : errorText;
122010
- if (runtime.currentTask) {
122011
- this.emit({
122012
- type: "agent:error",
122013
- payload: {
122014
- agentId: runtime.agentId,
122015
- conversationId: runtime.currentTask.conversationId,
122016
- ackId: runtime.currentTask.replyMessageId,
122017
- traceId: runtime.currentTask.traceId,
122018
- error: emittedErrorText
122019
- }
122020
- });
122021
- runtime.currentTask = null;
122022
- }
122023
- for (const task of runtime.injectedTasks) {
122024
- this.emit({
122025
- type: "agent:error",
122026
- payload: {
122027
- agentId: runtime.agentId,
122028
- conversationId: task.conversationId,
122029
- ackId: task.replyMessageId,
122030
- traceId: task.traceId,
122031
- error: emittedErrorText
122032
- }
122033
- });
122034
- }
122035
- runtime.injectedTasks = [];
122036
- for (const task of runtime.mergedTasks) {
122037
- this.emit({
122038
- type: "agent:error",
122039
- payload: {
122040
- agentId: runtime.agentId,
122041
- conversationId: task.conversationId,
122042
- ackId: task.replyMessageId,
122043
- traceId: task.traceId,
122044
- error: emittedErrorText
122045
- }
122046
- });
122047
- }
122048
- runtime.mergedTasks = [];
122049
- for (const task of runtime.planModeBuffer) {
122050
- this.emit({
122051
- type: "agent:error",
122052
- payload: {
122053
- agentId: runtime.agentId,
122054
- conversationId: task.conversationId,
122055
- ackId: task.replyMessageId,
122056
- traceId: task.traceId,
122057
- error: emittedErrorText
122058
- }
122059
- });
122060
- }
122061
- runtime.planModeBuffer = [];
122680
+ this.failPendingTasksWithError(runtime, emittedErrorText);
122062
122681
  }
122063
122682
  }
122064
122683
  getStatus(agentId, scope = { kind: "single" }) {
@@ -122072,6 +122691,18 @@ ${lines.join("\n")}`;
122072
122691
  }
122073
122692
  return [...ids];
122074
122693
  }
122694
+ /** Unified signal: is the turn legitimately waiting on a live external call that emits no
122695
+ * parent heartbeat? Used by BOTH the reply-stall fast path and the zombie watchdog so neither
122696
+ * tears down a turn that is merely slow. Returns the reason for diagnostics, or null when idle.
122697
+ * - open_tool: regular tool, MCP tool, AskUserQuestion wait, or ExitPlanMode (tool_use open).
122698
+ * - subagent: Task/Agent in flight (its inner tool_results clear activeToolUseStartedAt).
122699
+ * - compact: bridge-injected /compact running. */
122700
+ busyReason(proc) {
122701
+ if (proc.activeToolUseStartedAt != null || this.latestOpenToolUse(proc) != null) return "open_tool";
122702
+ if ((proc.activeSubagentTaskIds?.size ?? 0) > 0) return "subagent";
122703
+ if (proc.compactInProgress === true) return "compact";
122704
+ return null;
122705
+ }
122075
122706
  latestOpenToolUse(proc) {
122076
122707
  for (let i = proc.contentBlocks.length - 1; i >= 0; i -= 1) {
122077
122708
  const block = proc.contentBlocks[i];
@@ -122220,7 +122851,7 @@ ${lines.join("\n")}`;
122220
122851
  }
122221
122852
  const task = {
122222
122853
  content: notice,
122223
- replyMessageId: createMessageId(),
122854
+ replyMessageId: createScopeNoticeReplyMessageId(),
122224
122855
  conversationId,
122225
122856
  traceId: createTraceId(),
122226
122857
  groupId: proc.scope.kind === "group" ? proc.scope.groupId : void 0
@@ -123307,6 +123938,7 @@ var HttpMcpRegistry = class {
123307
123938
  buildForAgent(ctx) {
123308
123939
  const mcpServers = {};
123309
123940
  const allowedTools = [];
123941
+ const toolAbi = [];
123310
123942
  const usedNames = /* @__PURE__ */ new Set();
123311
123943
  for (const connection of this.allConnections()) {
123312
123944
  if (!this.connectionAppliesToAgent(connection, ctx)) continue;
@@ -123315,12 +123947,18 @@ var HttpMcpRegistry = class {
123315
123947
  const serverName = uniqueServerName(normalizeMcpServerName(connection.serverName), usedNames);
123316
123948
  usedNames.add(serverName);
123317
123949
  mcpServers[serverName] = sdkConfig;
123318
- for (const tool2 of connection.tools) {
123319
- if (!tool2.enabled || tool2.permissionPolicy === "always_deny") continue;
123320
- allowedTools.push(mcpRuntimeToolName(serverName, tool2.name));
123321
- }
123950
+ const visibleTools = connection.tools.filter((tool2) => tool2.enabled && tool2.permissionPolicy !== "always_deny");
123951
+ for (const tool2 of visibleTools) allowedTools.push(mcpRuntimeToolName(serverName, tool2.name));
123952
+ toolAbi.push({
123953
+ serverName,
123954
+ providerId: connection.providerId,
123955
+ transport: connection.transport,
123956
+ alwaysLoad: connection.alwaysLoad,
123957
+ isBuiltin: connection.isBuiltin,
123958
+ tools: visibleTools.map((tool2) => runtimeToolAbi(serverName, tool2)).sort((a, b) => a.runtimeToolName.localeCompare(b.runtimeToolName))
123959
+ });
123322
123960
  }
123323
- return { mcpServers, allowedTools };
123961
+ return { mcpServers, allowedTools, toolAbi };
123324
123962
  }
123325
123963
  allConnections() {
123326
123964
  return [...this.serverConnections.values(), ...this.localConnections.values()];
@@ -123506,6 +124144,18 @@ function uniqueServerName(serverName, usedNames) {
123506
124144
  while (usedNames.has(`${serverName}_${idx}`)) idx += 1;
123507
124145
  return `${serverName}_${idx}`;
123508
124146
  }
124147
+ function runtimeToolAbi(serverName, tool2) {
124148
+ return {
124149
+ name: tool2.name,
124150
+ runtimeToolName: mcpRuntimeToolName(serverName, tool2.name),
124151
+ displayName: tool2.displayName,
124152
+ description: tool2.description,
124153
+ category: tool2.category,
124154
+ riskLevel: tool2.riskLevel,
124155
+ permissionPolicy: tool2.permissionPolicy,
124156
+ ...tool2.inputSchema !== void 0 ? { inputSchema: tool2.inputSchema } : {}
124157
+ };
124158
+ }
123509
124159
  function buildHeaders(authType, authSecret, customHeaders) {
123510
124160
  const headers = {};
123511
124161
  for (const header of customHeaders) {
@@ -124162,6 +124812,7 @@ var ServerConnector = class {
124162
124812
  case "agent:terminate":
124163
124813
  case "agent:runtime_reload":
124164
124814
  case "agent:terminate_scope":
124815
+ case "spectate:set":
124165
124816
  case "agent:created":
124166
124817
  case "agent:updated":
124167
124818
  case "agent:workdir-updated":
@@ -125139,24 +125790,6 @@ function normalizeLocalPath(targetPath) {
125139
125790
  const expanded = trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\") ? import_node_path18.default.join(import_node_os10.default.homedir(), trimmed.slice(2)) : trimmed;
125140
125791
  return import_node_path18.default.normalize(import_node_path18.default.resolve(expanded));
125141
125792
  }
125142
- function isAbsoluteLocalPathCandidate(value) {
125143
- if (process.platform === "win32") {
125144
- return /^[a-zA-Z]:[\\/]/.test(value) || /^\\\\[^\\]/.test(value);
125145
- }
125146
- return value.startsWith("/");
125147
- }
125148
- function clipboardTextToPathCandidates(value) {
125149
- return value.replace(/\0/g, "\n").split(/\r?\n/).map((line) => line.trim().replace(/^"(.+)"$/, "$1")).map((line) => {
125150
- if (!line.toLowerCase().startsWith("file://")) return line;
125151
- try {
125152
- const url2 = new URL(line);
125153
- return decodeURIComponent(url2.pathname.replace(/^\/([a-zA-Z]:\/)/, "$1"));
125154
- } catch (e) {
125155
- logger25.debug("Failed to parse clipboard file URL", { error: e });
125156
- return "";
125157
- }
125158
- }).filter((line) => line.length > 0 && isAbsoluteLocalPathCandidate(line));
125159
- }
125160
125793
  function normalizeClipboardIdentityKey(value) {
125161
125794
  return process.platform === "win32" ? value.toLowerCase() : value;
125162
125795
  }
@@ -125194,18 +125827,15 @@ function mimeTypeForFileName(fileName) {
125194
125827
  }
125195
125828
  function parseWindowsClipboardResult(stdout) {
125196
125829
  const raw = stdout.trim();
125197
- if (!raw) return { files: [], text: "" };
125830
+ if (!raw) return { files: [] };
125198
125831
  try {
125199
125832
  const parsed = JSON.parse(raw);
125200
- if (!isRecord5(parsed)) return { files: [], text: "" };
125833
+ if (!isRecord5(parsed)) return { files: [] };
125201
125834
  const files = Array.isArray(parsed.files) ? parsed.files.filter((item) => typeof item === "string") : [];
125202
- return {
125203
- files,
125204
- text: typeof parsed.text === "string" ? parsed.text : ""
125205
- };
125835
+ return { files };
125206
125836
  } catch (e) {
125207
125837
  logger25.debug("Windows clipboard JSON parse skipped", { error: e });
125208
- return { files: clipboardTextToPathCandidates(stdout), text: "" };
125838
+ return { files: [] };
125209
125839
  }
125210
125840
  }
125211
125841
  async function readWindowsClipboardPathCandidates() {
@@ -125218,9 +125848,7 @@ async function readWindowsClipboardPathCandidates() {
125218
125848
  " $drop = [System.Windows.Forms.Clipboard]::GetFileDropList();",
125219
125849
  " foreach ($file in $drop) { $files += [string]$file }",
125220
125850
  "} catch {}",
125221
- '$text = "";',
125222
- "try { $text = [System.Windows.Forms.Clipboard]::GetText() } catch {}",
125223
- "[pscustomobject]@{ files = $files; text = $text } | ConvertTo-Json -Compress;"
125851
+ "[pscustomobject]@{ files = $files } | ConvertTo-Json -Compress;"
125224
125852
  ].join(" ");
125225
125853
  try {
125226
125854
  const { stdout } = await execFileAsync2("powershell.exe", [
@@ -125238,7 +125866,7 @@ async function readWindowsClipboardPathCandidates() {
125238
125866
  maxBuffer: 1024 * 1024
125239
125867
  });
125240
125868
  const result = parseWindowsClipboardResult(stdout);
125241
- return [...result.files, ...clipboardTextToPathCandidates(result.text)];
125869
+ return result.files;
125242
125870
  } catch (e) {
125243
125871
  logger25.debug("Windows clipboard file read skipped", { error: e });
125244
125872
  return [];
@@ -125644,7 +126272,7 @@ async function readStreamText(filePath, start) {
125644
126272
  function parseProcessedLines(raw, cursor, fileName, fingerprint) {
125645
126273
  const lastNewline = raw.lastIndexOf("\n");
125646
126274
  if (lastNewline < 0) {
125647
- return { entries: [], nextCursor: cursor, advanced: false };
126275
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "partial_line" };
125648
126276
  }
125649
126277
  const processed = raw.slice(0, lastNewline + 1);
125650
126278
  const lines = processed.split(/\r?\n/);
@@ -125670,7 +126298,8 @@ function parseProcessedLines(raw, cursor, fileName, fingerprint) {
125670
126298
  lineNum,
125671
126299
  ...fingerprint ? { fingerprint } : {}
125672
126300
  },
125673
- advanced: true
126301
+ advanced: true,
126302
+ reason: "advanced"
125674
126303
  };
125675
126304
  }
125676
126305
  function chunkEntries(entries, size) {
@@ -125728,24 +126357,55 @@ var BridgeLogUploader = class {
125728
126357
  async flushOnce() {
125729
126358
  if (this.running || this.stopped) return;
125730
126359
  this.running = true;
126360
+ const startedAt = Date.now();
126361
+ const summary = {
126362
+ targetCount: 0,
126363
+ advancedTargetCount: 0,
126364
+ missingTargetCount: 0,
126365
+ idleTargetCount: 0,
126366
+ partialLineTargetCount: 0,
126367
+ failedTargetCount: 0,
126368
+ parsedEntryCount: 0,
126369
+ bridgeEntryCount: 0,
126370
+ uploadedChunkCount: 0,
126371
+ accepted: 0,
126372
+ skipped: 0
126373
+ };
125731
126374
  try {
125732
126375
  const targets = await this.resolveTargets();
126376
+ summary.targetCount = targets.length;
125733
126377
  for (const target of targets) {
125734
126378
  try {
125735
126379
  const cursor = await readCursor(target.cursorFile);
125736
126380
  const batch = await this.readNewEntries(target, cursor);
125737
- if (!batch.advanced) continue;
126381
+ if (!batch.advanced) {
126382
+ summary.idleTargetCount += 1;
126383
+ if (batch.reason === "missing_file") summary.missingTargetCount += 1;
126384
+ if (batch.reason === "partial_line") summary.partialLineTargetCount += 1;
126385
+ continue;
126386
+ }
126387
+ summary.advancedTargetCount += 1;
126388
+ summary.parsedEntryCount += batch.entries.length;
125738
126389
  if (batch.entries.length > 0) {
125739
- await this.uploadEntries(batch.entries);
126390
+ const result = await this.uploadEntries(batch.entries);
126391
+ summary.bridgeEntryCount += result.bridgeEntryCount;
126392
+ summary.uploadedChunkCount += result.uploadedChunkCount;
126393
+ summary.accepted += result.accepted;
126394
+ summary.skipped += result.skipped;
125740
126395
  }
125741
126396
  await writeCursor(target.cursorFile, batch.nextCursor);
125742
126397
  } catch (e) {
126398
+ summary.failedTargetCount += 1;
125743
126399
  logger28.warn("Bridge log upload target failed", { error: e, logFile: target.logFile });
125744
126400
  }
125745
126401
  }
125746
126402
  } catch (e) {
125747
126403
  logger28.warn("Bridge log upload cycle failed", { error: e });
125748
126404
  } finally {
126405
+ logger28.info("Bridge log upload cycle summary", {
126406
+ ...summary,
126407
+ durationMs: Date.now() - startedAt
126408
+ });
125749
126409
  this.running = false;
125750
126410
  }
125751
126411
  }
@@ -125768,7 +126428,7 @@ var BridgeLogUploader = class {
125768
126428
  } catch (e) {
125769
126429
  if (e instanceof Error && "code" in e && e.code === "ENOENT") {
125770
126430
  logger28.debug("Bridge log file not found for upload yet", { logFile: target.logFile });
125771
- return { entries: [], nextCursor: cursor, advanced: false };
126431
+ return { entries: [], nextCursor: cursor, advanced: false, reason: "missing_file" };
125772
126432
  }
125773
126433
  throw e;
125774
126434
  }
@@ -125776,13 +126436,19 @@ var BridgeLogUploader = class {
125776
126436
  const samePhysicalFile = !fingerprint || !cursor.fingerprint || cursor.fingerprint === fingerprint;
125777
126437
  const normalizedCursor = stat3.size < cursor.offset || !samePhysicalFile ? { offset: 0, lineNum: 0, ...fingerprint ? { fingerprint } : {} } : { ...cursor, ...fingerprint ? { fingerprint } : {} };
125778
126438
  if (stat3.size <= normalizedCursor.offset) {
125779
- return { entries: [], nextCursor: normalizedCursor, advanced: false };
126439
+ return { entries: [], nextCursor: normalizedCursor, advanced: false, reason: "no_new_entries" };
125780
126440
  }
125781
126441
  const raw = await readStreamText(target.logFile, normalizedCursor.offset);
125782
126442
  return parseProcessedLines(raw, normalizedCursor, target.uploadedFileName, fingerprint);
125783
126443
  }
125784
126444
  async uploadEntries(entries) {
125785
126445
  const bridgeEntries = entries.filter((entry) => entry.source === "bridge");
126446
+ const result = {
126447
+ bridgeEntryCount: bridgeEntries.length,
126448
+ uploadedChunkCount: 0,
126449
+ accepted: 0,
126450
+ skipped: 0
126451
+ };
125786
126452
  for (const chunk of chunkEntries(bridgeEntries, this.options.batchSize)) {
125787
126453
  const res = await fetch(`${this.options.serverApiUrl}/api/logs/upload`, {
125788
126454
  method: "POST",
@@ -125797,14 +126463,18 @@ var BridgeLogUploader = class {
125797
126463
  })
125798
126464
  });
125799
126465
  if (!res.ok) {
125800
- const body = await res.text().catch((e) => {
126466
+ const body2 = await res.text().catch((e) => {
125801
126467
  logger28.debug("Failed to read log upload error body", { error: e });
125802
126468
  return "";
125803
126469
  });
125804
- throw new Error(`upload failed HTTP ${res.status}: ${body.slice(0, 160)}`);
126470
+ throw new Error(`upload failed HTTP ${res.status}: ${body2.slice(0, 160)}`);
125805
126471
  }
125806
- await res.json();
126472
+ const body = await res.json();
126473
+ result.uploadedChunkCount += 1;
126474
+ result.accepted += typeof body.accepted === "number" ? body.accepted : 0;
126475
+ result.skipped += typeof body.skipped === "number" ? body.skipped : 0;
125807
126476
  }
126477
+ return result;
125808
126478
  }
125809
126479
  };
125810
126480
 
@@ -127802,6 +128472,33 @@ function syncLocalRuntimeSkills(skillStore, localSkills, options = {}) {
127802
128472
  ]);
127803
128473
  }
127804
128474
 
128475
+ // src/processOutput.ts
128476
+ init_cjs_shims();
128477
+ var protectedStreams2 = /* @__PURE__ */ new WeakSet();
128478
+ var reportedErrors = /* @__PURE__ */ new WeakSet();
128479
+ function reportWriteError(error51, onError) {
128480
+ if (typeof error51 === "object" && error51 !== null) {
128481
+ if (reportedErrors.has(error51)) return;
128482
+ reportedErrors.add(error51);
128483
+ }
128484
+ onError(error51);
128485
+ }
128486
+ function safeWriteProcessOutput(stream, text, onError) {
128487
+ if (!stream) return;
128488
+ if (stream.destroyed || stream.writableEnded) return;
128489
+ if (typeof stream === "object" && typeof stream.on === "function" && !protectedStreams2.has(stream)) {
128490
+ protectedStreams2.add(stream);
128491
+ stream.on("error", (e) => reportWriteError(e, onError));
128492
+ }
128493
+ try {
128494
+ stream.write(text, (error51) => {
128495
+ if (error51) reportWriteError(error51, onError);
128496
+ });
128497
+ } catch (e) {
128498
+ reportWriteError(e, onError);
128499
+ }
128500
+ }
128501
+
127805
128502
  // src/start.ts
127806
128503
  var logger41 = createModuleLogger("bridge");
127807
128504
  var NODE_USER_UID2 = 1e3;
@@ -127888,14 +128585,16 @@ async function startBridge(config2) {
127888
128585
  const claudeRuntime = resolveClaudeRuntime();
127889
128586
  logClaudeRuntimeResolution(claudeRuntime);
127890
128587
  if (!claudeRuntime.ok || !claudeRuntime.path) {
127891
- process.stderr.write(
128588
+ safeWriteProcessOutput(
128589
+ process.stderr,
127892
128590
  `
127893
128591
  Claude runtime is unavailable.
127894
128592
 
127895
128593
  ${claudeRuntime.error ?? "Install Claude Code manually or use the bundled desktop runtime."}
127896
128594
 
127897
128595
  Reinstall @fangyb/ahchat-bridge with npm optional dependencies, set AHCHAT_CLAUDE_EXECUTABLE to a valid Claude Code binary path, install Claude Code, or use ALL-CAN Desktop with its bundled runtime.
127898
- `
128596
+ `,
128597
+ (e) => logger41.error("Bridge process stderr write failed", { error: e })
127899
128598
  );
127900
128599
  process.exit(1);
127901
128600
  }
@@ -127952,12 +128651,14 @@ Reinstall @fangyb/ahchat-bridge with npm optional dependencies, set AHCHAT_CLAUD
127952
128651
  claudeRuntimeVersion: claudeRuntime.version ?? null
127953
128652
  });
127954
128653
  const shouldPrintRawBridgeToken = process.stdout.isTTY && process.env.AHCHAT_SUPPRESS_BRIDGE_TOKEN_STDOUT !== "1";
127955
- process.stdout.write(
128654
+ safeWriteProcessOutput(
128655
+ process.stdout,
127956
128656
  `
127957
128657
  Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\u673A\u5668):
127958
128658
  ${shouldPrintRawBridgeToken ? config2.bridgeToken : "***"}
127959
128659
 
127960
- `
128660
+ `,
128661
+ (e) => logger41.error("Bridge process stdout write failed", { error: e })
127961
128662
  );
127962
128663
  wsMetrics.start(5e3);
127963
128664
  const sessionStore = new SessionStore(config2.dataDir);
@@ -128756,6 +129457,19 @@ Bridge token (register this machine at Settings \u2192 \u5DF2\u8FDE\u63A5\u7684\
128756
129457
  });
128757
129458
  await agentManager.terminateScope(msg.payload.agentId, msg.payload.scope);
128758
129459
  break;
129460
+ case "spectate:set":
129461
+ logger41.info("spectate:set received", {
129462
+ agentId: msg.payload.agentId,
129463
+ scope: msg.payload.scope,
129464
+ action: msg.payload.action,
129465
+ traceId: msg.payload.traceId
129466
+ });
129467
+ await agentManager.setSpectate(
129468
+ msg.payload.agentId,
129469
+ msg.payload.scope,
129470
+ msg.payload.action
129471
+ );
129472
+ break;
128759
129473
  case "agent:created":
128760
129474
  agentRegistry.upsert(msg.payload.agent);
128761
129475
  ensureLocalWorkdirPath(msg.payload.agent.workingDirectory, "agent:created", {
@@ -128973,8 +129687,12 @@ function writeAlreadyRunningMessage(error51) {
128973
129687
  ` ${buildStopCommand(error51.pid)}`,
128974
129688
  ""
128975
129689
  ];
128976
- process.stdout.write(`${lines.join("\n")}
128977
- `);
129690
+ safeWriteProcessOutput(
129691
+ process.stdout,
129692
+ `${lines.join("\n")}
129693
+ `,
129694
+ (e) => logger42.error("Bridge already-running message write failed", { error: e })
129695
+ );
128978
129696
  }
128979
129697
  function handleBridgeStartError(e, message) {
128980
129698
  if (isBridgeAlreadyRunningError(e)) {