weacpx 0.4.3 → 0.4.4

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.js CHANGED
@@ -2225,6 +2225,28 @@ function parseConfig(raw, options = {}) {
2225
2225
  throw new Error(`logging.${field} must be a positive number`);
2226
2226
  }
2227
2227
  }
2228
+ if (isRecord(logging) && "perf" in logging) {
2229
+ if (!isRecord(logging.perf)) {
2230
+ throw new Error("logging.perf must be an object");
2231
+ }
2232
+ if ("enabled" in logging.perf && typeof logging.perf.enabled !== "boolean") {
2233
+ throw new Error("logging.perf.enabled must be boolean");
2234
+ }
2235
+ for (const field of ["maxSizeBytes", "maxFiles", "retentionDays"]) {
2236
+ if (field in logging.perf) {
2237
+ const value = logging.perf[field];
2238
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2239
+ throw new Error(`logging.perf.${field} must be a finite number`);
2240
+ }
2241
+ if (field === "maxFiles" && value < 0) {
2242
+ throw new Error(`logging.perf.${field} must be non-negative`);
2243
+ }
2244
+ if (field !== "maxFiles" && value <= 0) {
2245
+ throw new Error(`logging.perf.${field} must be a positive number`);
2246
+ }
2247
+ }
2248
+ }
2249
+ }
2228
2250
  for (const [name, agent] of Object.entries(raw.agents)) {
2229
2251
  if (!isRecord(agent) || typeof agent.driver !== "string" || agent.driver.length === 0) {
2230
2252
  throw new Error(`agent "${name}" must define a non-empty driver`);
@@ -2280,7 +2302,16 @@ function parseConfig(raw, options = {}) {
2280
2302
  level: resolvedLoggingLevel,
2281
2303
  maxSizeBytes: typeof logging?.maxSizeBytes === "number" ? logging.maxSizeBytes : DEFAULT_LOGGING_CONFIG.maxSizeBytes,
2282
2304
  maxFiles: typeof logging?.maxFiles === "number" ? logging.maxFiles : DEFAULT_LOGGING_CONFIG.maxFiles,
2283
- retentionDays: typeof logging?.retentionDays === "number" ? logging.retentionDays : DEFAULT_LOGGING_CONFIG.retentionDays
2305
+ retentionDays: typeof logging?.retentionDays === "number" ? logging.retentionDays : DEFAULT_LOGGING_CONFIG.retentionDays,
2306
+ perf: (() => {
2307
+ const perfRaw = isRecord(logging?.perf) ? logging.perf : undefined;
2308
+ return {
2309
+ enabled: typeof perfRaw?.enabled === "boolean" ? perfRaw.enabled : DEFAULT_PERF_LOG_CONFIG.enabled,
2310
+ maxSizeBytes: typeof perfRaw?.maxSizeBytes === "number" && Number.isFinite(perfRaw.maxSizeBytes) && perfRaw.maxSizeBytes > 0 ? perfRaw.maxSizeBytes : DEFAULT_PERF_LOG_CONFIG.maxSizeBytes,
2311
+ maxFiles: typeof perfRaw?.maxFiles === "number" && Number.isFinite(perfRaw.maxFiles) && perfRaw.maxFiles >= 0 ? perfRaw.maxFiles : DEFAULT_PERF_LOG_CONFIG.maxFiles,
2312
+ retentionDays: typeof perfRaw?.retentionDays === "number" && Number.isFinite(perfRaw.retentionDays) && perfRaw.retentionDays > 0 ? perfRaw.retentionDays : DEFAULT_PERF_LOG_CONFIG.retentionDays
2313
+ };
2314
+ })()
2284
2315
  },
2285
2316
  channel: channelConfig,
2286
2317
  channels: channelsConfig,
@@ -2390,14 +2421,21 @@ function parseOrchestrationConfig(raw) {
2390
2421
  progressHeartbeatSeconds: typeof raw.progressHeartbeatSeconds === "number" && Number.isFinite(raw.progressHeartbeatSeconds) ? raw.progressHeartbeatSeconds : DEFAULT_ORCHESTRATION_CONFIG.progressHeartbeatSeconds
2391
2422
  };
2392
2423
  }
2393
- var DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_CHANNEL_CONFIG, DEFAULT_ORCHESTRATION_CONFIG;
2424
+ var DEFAULT_PERF_LOG_CONFIG, DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_CHANNEL_CONFIG, DEFAULT_ORCHESTRATION_CONFIG;
2394
2425
  var init_load_config = __esm(() => {
2395
2426
  init_workspace_path();
2427
+ DEFAULT_PERF_LOG_CONFIG = {
2428
+ enabled: false,
2429
+ maxSizeBytes: 5 * 1024 * 1024,
2430
+ maxFiles: 3,
2431
+ retentionDays: 7
2432
+ };
2396
2433
  DEFAULT_LOGGING_CONFIG = {
2397
2434
  level: "info",
2398
2435
  maxSizeBytes: 2 * 1024 * 1024,
2399
2436
  maxFiles: 5,
2400
- retentionDays: 7
2437
+ retentionDays: 7,
2438
+ perf: DEFAULT_PERF_LOG_CONFIG
2401
2439
  };
2402
2440
  DEFAULT_CHANNEL_CONFIG = {
2403
2441
  type: "weixin",
@@ -2552,7 +2590,13 @@ var init_ensure_config = __esm(() => {
2552
2590
  level: "info",
2553
2591
  maxSizeBytes: 2 * 1024 * 1024,
2554
2592
  maxFiles: 5,
2555
- retentionDays: 7
2593
+ retentionDays: 7,
2594
+ perf: {
2595
+ enabled: false,
2596
+ maxSizeBytes: 5242880,
2597
+ maxFiles: 3,
2598
+ retentionDays: 7
2599
+ }
2556
2600
  },
2557
2601
  channel: {
2558
2602
  type: "weixin",
@@ -2837,6 +2881,7 @@ function buildSpawnRequest(paths, options, stdoutFd, stderrFd, spawnOptions = {}
2837
2881
  WEACPX_DAEMON_ARG0: options.cliEntryPath,
2838
2882
  WEACPX_DAEMON_ARG1: "run",
2839
2883
  WEACPX_DAEMON_CWD: options.cwd,
2884
+ WEACPX_DAEMON_RUN: "1",
2840
2885
  WEACPX_DAEMON_STDOUT: paths.stdoutLog,
2841
2886
  WEACPX_DAEMON_STDERR: paths.stderrLog,
2842
2887
  ...spawnOptions.firstRunOnboarding ? { WEACPX_FIRST_RUN_ONBOARDING: spawnOptions.firstRunOnboarding } : {}
@@ -2855,6 +2900,7 @@ function buildSpawnRequest(paths, options, stdoutFd, stderrFd, spawnOptions = {}
2855
2900
  detached: true,
2856
2901
  env: {
2857
2902
  ...options.env,
2903
+ WEACPX_DAEMON_RUN: "1",
2858
2904
  ...spawnOptions.firstRunOnboarding ? { WEACPX_FIRST_RUN_ONBOARDING: spawnOptions.firstRunOnboarding } : {}
2859
2905
  },
2860
2906
  stdio: ["ignore", stdoutFd, stderrFd]
@@ -2863,6 +2909,7 @@ function buildSpawnRequest(paths, options, stdoutFd, stderrFd, spawnOptions = {}
2863
2909
  }
2864
2910
  function buildWindowsLauncherScript() {
2865
2911
  const script = [
2912
+ "$env:WEACPX_DAEMON_RUN = '1'",
2866
2913
  "$process = Start-Process -FilePath $env:WEACPX_DAEMON_COMMAND `",
2867
2914
  " -ArgumentList @($env:WEACPX_DAEMON_ARG0, $env:WEACPX_DAEMON_ARG1) `",
2868
2915
  " -WorkingDirectory $env:WEACPX_DAEMON_CWD `",
@@ -13147,6 +13194,296 @@ function normalizeMediaArray(media) {
13147
13194
  return Array.isArray(media) ? media : [media];
13148
13195
  }
13149
13196
 
13197
+ // src/logging/rotating-file-writer.ts
13198
+ import { readdir as readdir2, rename, rm as rm5, stat as stat2 } from "node:fs/promises";
13199
+ import { basename, dirname as dirname6, join as join4 } from "node:path";
13200
+ async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
13201
+ let currentSize = 0;
13202
+ try {
13203
+ currentSize = (await stat2(filePath)).size;
13204
+ } catch (error2) {
13205
+ if (!isMissingFileError2(error2)) {
13206
+ throw error2;
13207
+ }
13208
+ }
13209
+ if (currentSize + incomingSize <= maxSizeBytes) {
13210
+ return;
13211
+ }
13212
+ if (currentSize === 0) {
13213
+ return;
13214
+ }
13215
+ if (maxFiles <= 0) {
13216
+ await rm5(filePath, { force: true });
13217
+ return;
13218
+ }
13219
+ await rm5(`${filePath}.${maxFiles}`, { force: true });
13220
+ for (let index = maxFiles - 1;index >= 1; index -= 1) {
13221
+ const source = `${filePath}.${index}`;
13222
+ try {
13223
+ await rename(source, `${filePath}.${index + 1}`);
13224
+ } catch (error2) {
13225
+ if (!isMissingFileError2(error2)) {
13226
+ throw error2;
13227
+ }
13228
+ }
13229
+ }
13230
+ await rename(filePath, `${filePath}.1`);
13231
+ }
13232
+ async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
13233
+ const parentDir = dirname6(filePath);
13234
+ const prefix = `${basename(filePath)}.`;
13235
+ const cutoff = now().getTime() - retentionDays * 24 * 60 * 60 * 1000;
13236
+ let files = [];
13237
+ try {
13238
+ files = await readdir2(parentDir);
13239
+ } catch (error2) {
13240
+ if (isMissingFileError2(error2)) {
13241
+ return;
13242
+ }
13243
+ throw error2;
13244
+ }
13245
+ for (const file of files) {
13246
+ if (!file.startsWith(prefix) || !/^\d+$/.test(file.slice(prefix.length))) {
13247
+ continue;
13248
+ }
13249
+ const candidate = join4(parentDir, file);
13250
+ const details = await stat2(candidate);
13251
+ if (details.mtime.getTime() < cutoff) {
13252
+ await rm5(candidate, { force: true });
13253
+ }
13254
+ }
13255
+ }
13256
+ function isMissingFileError2(error2) {
13257
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
13258
+ }
13259
+ var init_rotating_file_writer = () => {};
13260
+
13261
+ // src/perf/perf-log-writer.ts
13262
+ import { appendFile as fsAppendFile, mkdir as fsMkdir } from "node:fs/promises";
13263
+ import { dirname as dirname7 } from "node:path";
13264
+ function createPerfLogWriter(options) {
13265
+ const append = options.appendImpl ?? ((p, d) => fsAppendFile(p, d, "utf8"));
13266
+ const mkdir8 = options.mkdirImpl ?? ((p, o) => fsMkdir(p, o).then(() => {
13267
+ return;
13268
+ }));
13269
+ const now = options.now ?? (() => new Date);
13270
+ const threshold = options.failureThreshold ?? 5;
13271
+ let pending = [];
13272
+ let writeChain = Promise.resolve();
13273
+ let consecutiveFailures = 0;
13274
+ let disabled = false;
13275
+ let notified = false;
13276
+ const writer = {
13277
+ enqueue(line) {
13278
+ if (disabled)
13279
+ return;
13280
+ pending.push(line);
13281
+ scheduleDrain();
13282
+ },
13283
+ async flush() {
13284
+ await scheduleDrain();
13285
+ await writeChain;
13286
+ },
13287
+ async cleanup() {
13288
+ if (disabled)
13289
+ return;
13290
+ try {
13291
+ await cleanupExpiredRotatedLogs(options.filePath, options.retentionDays ?? 7, now);
13292
+ } catch {}
13293
+ },
13294
+ isDisabled() {
13295
+ return disabled;
13296
+ }
13297
+ };
13298
+ return writer;
13299
+ function scheduleDrain() {
13300
+ if (disabled || pending.length === 0) {
13301
+ return writeChain;
13302
+ }
13303
+ const batch = pending;
13304
+ pending = [];
13305
+ writeChain = writeChain.catch(() => {}).then(() => drainBatch(batch));
13306
+ return writeChain;
13307
+ }
13308
+ async function drainBatch(batch) {
13309
+ if (disabled)
13310
+ return;
13311
+ const data = batch.join("");
13312
+ try {
13313
+ await mkdir8(dirname7(options.filePath), { recursive: true });
13314
+ await rotateIfNeeded(options.filePath, Buffer.byteLength(data), options.maxSizeBytes, options.maxFiles);
13315
+ await append(options.filePath, data);
13316
+ consecutiveFailures = 0;
13317
+ } catch (err) {
13318
+ consecutiveFailures += 1;
13319
+ if (consecutiveFailures >= threshold) {
13320
+ disabled = true;
13321
+ pending = [];
13322
+ if (!notified) {
13323
+ notified = true;
13324
+ options.onPermanentFailure({
13325
+ perfLogPath: options.filePath,
13326
+ failureCount: consecutiveFailures,
13327
+ lastError: err instanceof Error ? err.message : String(err)
13328
+ });
13329
+ }
13330
+ }
13331
+ }
13332
+ }
13333
+ }
13334
+ var init_perf_log_writer = __esm(() => {
13335
+ init_rotating_file_writer();
13336
+ });
13337
+
13338
+ // src/perf/perf-tracer.ts
13339
+ import { randomBytes } from "node:crypto";
13340
+ function createNoopPerfTracer() {
13341
+ return {
13342
+ async wrapTurn(_seed, run) {
13343
+ return run(NOOP_SPAN);
13344
+ },
13345
+ async flush() {},
13346
+ async cleanup() {}
13347
+ };
13348
+ }
13349
+ function createPerfTracer(options) {
13350
+ const now = options.now ?? (() => performance.now());
13351
+ const isoNow = options.isoNow ?? (() => new Date);
13352
+ const randomId = options.randomId ?? defaultRandomId;
13353
+ const formatLine = options.formatLine ?? defaultFormatLine;
13354
+ const formatSummary = options.formatSummaryLine ?? defaultFormatSummaryLine;
13355
+ let disabled = false;
13356
+ const writer = createPerfLogWriter({
13357
+ filePath: options.filePath,
13358
+ maxSizeBytes: options.maxSizeBytes,
13359
+ maxFiles: options.maxFiles,
13360
+ retentionDays: options.retentionDays,
13361
+ onPermanentFailure: (info) => {
13362
+ disabled = true;
13363
+ options.appLogger.error("perf.disabled_due_to_io_error", "perf logging disabled after repeated IO failures", {
13364
+ perfLogPath: info.perfLogPath,
13365
+ failureCount: info.failureCount,
13366
+ lastError: info.lastError
13367
+ }).catch(() => {});
13368
+ }
13369
+ });
13370
+ return {
13371
+ async wrapTurn(seed, run) {
13372
+ if (disabled) {
13373
+ return run(NOOP_SPAN);
13374
+ }
13375
+ const traceId = randomId();
13376
+ let startTime;
13377
+ const marks = [];
13378
+ let lastMarkTime;
13379
+ let explicitOutcome;
13380
+ let outcomeContext;
13381
+ const span = {
13382
+ traceId,
13383
+ mark(event, context) {
13384
+ if (disabled)
13385
+ return;
13386
+ try {
13387
+ const t = now();
13388
+ if (startTime === undefined) {
13389
+ startTime = t;
13390
+ lastMarkTime = t;
13391
+ }
13392
+ const since = t - startTime;
13393
+ const sinceLast = t - lastMarkTime;
13394
+ lastMarkTime = t;
13395
+ marks.push({ e: event, t: Math.round(since) });
13396
+ const line = formatLine({
13397
+ isoNow: isoNow(),
13398
+ event,
13399
+ traceId,
13400
+ chatKey: seed.chatKey,
13401
+ sinceStartMs: Math.round(since),
13402
+ sinceLastMs: Math.round(sinceLast),
13403
+ context
13404
+ });
13405
+ writer.enqueue(line);
13406
+ } catch {}
13407
+ },
13408
+ setOutcome(outcome, context) {
13409
+ explicitOutcome = outcome;
13410
+ outcomeContext = context;
13411
+ }
13412
+ };
13413
+ let thrown;
13414
+ try {
13415
+ return await run(span);
13416
+ } catch (err) {
13417
+ thrown = err;
13418
+ throw err;
13419
+ } finally {
13420
+ try {
13421
+ if (!disabled) {
13422
+ let outcome;
13423
+ if (explicitOutcome !== undefined) {
13424
+ outcome = explicitOutcome;
13425
+ } else if (thrown !== undefined) {
13426
+ outcome = "error";
13427
+ } else {
13428
+ outcome = "ok";
13429
+ }
13430
+ const t = now();
13431
+ const effectiveStart = startTime ?? t;
13432
+ const summary = formatSummary({
13433
+ isoNow: isoNow(),
13434
+ traceId,
13435
+ chatKey: seed.chatKey,
13436
+ kind: seed.kind,
13437
+ outcome,
13438
+ totalMs: Math.round(t - effectiveStart),
13439
+ marks,
13440
+ outcomeContext
13441
+ });
13442
+ writer.enqueue(summary);
13443
+ }
13444
+ } catch {}
13445
+ }
13446
+ },
13447
+ async flush() {
13448
+ await writer.flush();
13449
+ },
13450
+ async cleanup() {
13451
+ await writer.cleanup();
13452
+ }
13453
+ };
13454
+ }
13455
+ function defaultRandomId() {
13456
+ return randomBytes(6).toString("hex");
13457
+ }
13458
+ function defaultFormatLine(args) {
13459
+ const ctxFields = args.context ? Object.entries(args.context).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ") : "";
13460
+ const ctxPrefix = ctxFields ? ` ${ctxFields}` : "";
13461
+ return `${args.isoNow.toISOString()} PERF ${args.event} trace=${args.traceId} chatKey=${formatValue(args.chatKey)}${ctxPrefix} sinceStartMs=${args.sinceStartMs} sinceLastMs=${args.sinceLastMs}
13462
+ `;
13463
+ }
13464
+ function defaultFormatSummaryLine(args) {
13465
+ const extra = args.outcomeContext ? " " + Object.entries(args.outcomeContext).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ") : "";
13466
+ const marksJson = JSON.stringify(args.marks);
13467
+ return `${args.isoNow.toISOString()} PERF turn.done trace=${args.traceId} chatKey=${formatValue(args.chatKey)} kind=${formatValue(args.kind)} outcome=${formatValue(args.outcome)} totalMs=${args.totalMs}${extra} marks=${JSON.stringify(marksJson)}
13468
+ `;
13469
+ }
13470
+ function formatValue(value) {
13471
+ if (value === null)
13472
+ return "null";
13473
+ if (typeof value === "number" || typeof value === "boolean")
13474
+ return String(value);
13475
+ return JSON.stringify(value);
13476
+ }
13477
+ var NOOP_SPAN;
13478
+ var init_perf_tracer = __esm(() => {
13479
+ init_perf_log_writer();
13480
+ NOOP_SPAN = {
13481
+ traceId: "-",
13482
+ mark: () => {},
13483
+ setOutcome: () => {}
13484
+ };
13485
+ });
13486
+
13150
13487
  // src/weixin/messaging/handle-weixin-message-turn.ts
13151
13488
  import crypto4 from "node:crypto";
13152
13489
  import fs8 from "node:fs/promises";
@@ -13278,6 +13615,9 @@ function isClearSlashCommand(textBody) {
13278
13615
  const command = spaceIdx === -1 ? trimmed.toLowerCase() : trimmed.slice(0, spaceIdx).toLowerCase();
13279
13616
  return command === "/clear";
13280
13617
  }
13618
+ function isSlashCommandText(textBody) {
13619
+ return textBody.startsWith("/");
13620
+ }
13281
13621
  function getWeixinMessageTurnLane(full) {
13282
13622
  const textBody = extractTextBody(full.item_list).trim().toLowerCase();
13283
13623
  return textBody === "/cancel" || textBody === "/stop" || textBody === "/jx" ? "control" : "normal";
@@ -13343,230 +13683,300 @@ async function handleWeixinMessageTurn(full, deps) {
13343
13683
  }
13344
13684
  }).catch(() => {});
13345
13685
  };
13346
- const contextToken = full.context_token;
13347
- if (contextToken) {
13348
- setContextToken(deps.accountId, full.from_user_id ?? "", contextToken);
13349
- }
13350
- if (textBody.startsWith("/")) {
13351
- const shouldTypeForSlash = isClearSlashCommand(textBody);
13352
- if (shouldTypeForSlash) {
13353
- startTypingIndicator();
13354
- }
13355
- const chatKey = buildWeixinChatKey(deps.accountId, full.from_user_id ?? "");
13356
- try {
13357
- const slashResult = await handleSlashCommand(textBody, {
13358
- to,
13359
- contextToken: full.context_token,
13360
- baseUrl: deps.baseUrl,
13361
- token: deps.token,
13362
- accountId: deps.accountId,
13363
- log: deps.log,
13364
- errLog: deps.errLog,
13365
- onClear: () => deps.agent.clearSession?.(chatKey),
13366
- ...deps.hasPendingFinal ? { hasPendingFinal: deps.hasPendingFinal } : {},
13367
- ...deps.drainPendingFinal ? { drainPendingFinal: deps.drainPendingFinal } : {},
13368
- ...deps.prependPendingFinal ? { prependPendingFinal: deps.prependPendingFinal } : {},
13369
- ...deps.reserveFinal ? { reserveFinal: deps.reserveFinal } : {},
13370
- ...deps.finalRemaining ? { finalRemaining: deps.finalRemaining } : {}
13371
- }, receivedAt, full.create_time_ms);
13372
- if (slashResult.handled)
13373
- return;
13374
- } finally {
13686
+ const chatKey = buildWeixinChatKey(deps.accountId, fromUserId);
13687
+ const initialMediaCount = extractWeixinMediaDescriptors(full.item_list).length;
13688
+ const isSlashCommand = isSlashCommandText(textBody);
13689
+ const tracer = deps.perfTracer ?? createNoopPerfTracer();
13690
+ return await tracer.wrapTurn({ chatKey, kind: isSlashCommand ? "command" : "prompt" }, async (perfSpan) => {
13691
+ perfSpan.mark("turn.received", {
13692
+ textLen: textBody.length,
13693
+ hasMedia: initialMediaCount > 0,
13694
+ mediaCount: initialMediaCount
13695
+ });
13696
+ const contextToken = full.context_token;
13697
+ if (contextToken) {
13698
+ setContextToken(deps.accountId, full.from_user_id ?? "", contextToken);
13699
+ }
13700
+ if (isSlashCommand) {
13701
+ const shouldTypeForSlash = isClearSlashCommand(textBody);
13375
13702
  if (shouldTypeForSlash) {
13376
- stopTypingIndicator();
13377
- }
13378
- }
13379
- }
13380
- startTypingIndicator();
13381
- const mediaStore = deps.mediaStore ?? new RuntimeMediaStore({ rootDir: resolveMediaTempDir(deps.mediaTempDir) });
13382
- const media = [];
13383
- const attachmentNotes = [];
13384
- const descriptors = extractWeixinMediaDescriptors(full.item_list).slice(0, DEFAULT_MAX_ATTACHMENTS_PER_MESSAGE);
13385
- const download = deps.downloadMediaFromItemFn ?? downloadMediaFromItem;
13386
- for (const descriptor of descriptors) {
13387
- try {
13388
- const downloaded = await download(descriptor.item, {
13389
- cdnBaseUrl: deps.cdnBaseUrl,
13390
- saveMedia: createSaveMediaBuffer(deps.mediaTempDir),
13391
- log: deps.log,
13392
- errLog: deps.errLog,
13393
- label: "inbound"
13394
- });
13395
- const filePath = downloaded.decryptedPicPath ?? downloaded.decryptedVideoPath ?? downloaded.decryptedFilePath ?? downloaded.decryptedVoicePath;
13396
- if (!filePath) {
13397
- attachmentNotes.push(`Skipped ${descriptor.kind}: media was unavailable.`);
13398
- continue;
13703
+ startTypingIndicator();
13399
13704
  }
13400
13705
  try {
13401
- const buffer = await fs8.readFile(filePath);
13402
- const mimeType = downloaded.fileMediaType ?? downloaded.voiceMediaType ?? defaultWeixinMime(descriptor.kind);
13403
- media.push(await mediaStore.saveMediaBuffer({
13404
- channelId: "weixin",
13706
+ const slashResult = await handleSlashCommand(textBody, {
13707
+ to,
13708
+ contextToken: full.context_token,
13709
+ baseUrl: deps.baseUrl,
13710
+ token: deps.token,
13405
13711
  accountId: deps.accountId,
13406
- chatKey: buildWeixinChatKey(deps.accountId, full.from_user_id ?? ""),
13407
- messageId: full.message_id ? String(full.message_id) : full.context_token ?? String(full.create_time_ms ?? Date.now()),
13408
- fileName: descriptor.fileName,
13409
- mimeType,
13410
- kind: descriptor.kind,
13411
- buffer,
13412
- maxBytes: descriptor.kind === "image" ? DEFAULT_IMAGE_MAX_BYTES : DEFAULT_ATTACHMENT_MAX_BYTES
13413
- }));
13712
+ log: deps.log,
13713
+ errLog: deps.errLog,
13714
+ onClear: () => deps.agent.clearSession?.(chatKey),
13715
+ ...deps.hasPendingFinal ? { hasPendingFinal: deps.hasPendingFinal } : {},
13716
+ ...deps.drainPendingFinal ? { drainPendingFinal: deps.drainPendingFinal } : {},
13717
+ ...deps.prependPendingFinal ? { prependPendingFinal: deps.prependPendingFinal } : {},
13718
+ ...deps.reserveFinal ? { reserveFinal: deps.reserveFinal } : {},
13719
+ ...deps.finalRemaining ? { finalRemaining: deps.finalRemaining } : {}
13720
+ }, receivedAt, full.create_time_ms);
13721
+ if (slashResult.handled)
13722
+ return;
13414
13723
  } finally {
13415
- await fs8.rm(filePath, { force: true }).catch(() => {});
13724
+ if (shouldTypeForSlash) {
13725
+ stopTypingIndicator();
13726
+ }
13416
13727
  }
13417
- } catch (err) {
13418
- deps.errLog(`media download failed: ${String(err)}`);
13419
- attachmentNotes.push(`Skipped ${descriptor.kind}: ${err instanceof Error ? err.message : String(err)}`);
13420
13728
  }
13421
- }
13422
- const sendReplySegment = async (text) => {
13423
- const plainText = markdownToPlainText(text).trim();
13424
- if (plainText.length === 0) {
13425
- return false;
13729
+ startTypingIndicator();
13730
+ const mediaStore = deps.mediaStore ?? new RuntimeMediaStore({ rootDir: resolveMediaTempDir(deps.mediaTempDir) });
13731
+ const media = [];
13732
+ const attachmentNotes = [];
13733
+ const descriptors = extractWeixinMediaDescriptors(full.item_list).slice(0, DEFAULT_MAX_ATTACHMENTS_PER_MESSAGE);
13734
+ const download = deps.downloadMediaFromItemFn ?? downloadMediaFromItem;
13735
+ for (const descriptor of descriptors) {
13736
+ try {
13737
+ const downloaded = await download(descriptor.item, {
13738
+ cdnBaseUrl: deps.cdnBaseUrl,
13739
+ saveMedia: createSaveMediaBuffer(deps.mediaTempDir),
13740
+ log: deps.log,
13741
+ errLog: deps.errLog,
13742
+ label: "inbound"
13743
+ });
13744
+ const filePath = downloaded.decryptedPicPath ?? downloaded.decryptedVideoPath ?? downloaded.decryptedFilePath ?? downloaded.decryptedVoicePath;
13745
+ if (!filePath) {
13746
+ attachmentNotes.push(`Skipped ${descriptor.kind}: media was unavailable.`);
13747
+ continue;
13748
+ }
13749
+ try {
13750
+ const buffer = await fs8.readFile(filePath);
13751
+ const mimeType = downloaded.fileMediaType ?? downloaded.voiceMediaType ?? defaultWeixinMime(descriptor.kind);
13752
+ media.push(await mediaStore.saveMediaBuffer({
13753
+ channelId: "weixin",
13754
+ accountId: deps.accountId,
13755
+ chatKey: buildWeixinChatKey(deps.accountId, full.from_user_id ?? ""),
13756
+ messageId: full.message_id ? String(full.message_id) : full.context_token ?? String(full.create_time_ms ?? Date.now()),
13757
+ fileName: descriptor.fileName,
13758
+ mimeType,
13759
+ kind: descriptor.kind,
13760
+ buffer,
13761
+ maxBytes: descriptor.kind === "image" ? DEFAULT_IMAGE_MAX_BYTES : DEFAULT_ATTACHMENT_MAX_BYTES
13762
+ }));
13763
+ } finally {
13764
+ await fs8.rm(filePath, { force: true }).catch(() => {});
13765
+ }
13766
+ } catch (err) {
13767
+ deps.errLog(`media download failed: ${String(err)}`);
13768
+ attachmentNotes.push(`Skipped ${descriptor.kind}: ${err instanceof Error ? err.message : String(err)}`);
13769
+ }
13426
13770
  }
13771
+ let midFirstSent = false;
13772
+ const sendReplySegment = async (text) => {
13773
+ const plainText = markdownToPlainText(text).trim();
13774
+ if (plainText.length === 0) {
13775
+ return false;
13776
+ }
13777
+ try {
13778
+ await sendMessageWeixin({
13779
+ to,
13780
+ text: plainText,
13781
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
13782
+ });
13783
+ if (!midFirstSent) {
13784
+ midFirstSent = true;
13785
+ perfSpan.mark("reply.mid_first_sent", { bytes: utf8ByteLength(plainText) });
13786
+ }
13787
+ return true;
13788
+ } catch (err) {
13789
+ deps.errLog(`intermediate reply failed: ${String(err)}`);
13790
+ return false;
13791
+ }
13792
+ };
13793
+ const requestText = appendAttachmentNotes(bodyFromItemList(full.item_list), attachmentNotes);
13794
+ const request = {
13795
+ accountId: deps.accountId,
13796
+ conversationId: buildWeixinChatKey(deps.accountId, full.from_user_id ?? ""),
13797
+ text: requestText,
13798
+ ...media.length > 0 ? { media } : {},
13799
+ replyContextToken: contextToken,
13800
+ perfSpan
13801
+ };
13427
13802
  try {
13428
- await sendMessageWeixin({
13429
- to,
13430
- text: plainText,
13431
- opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
13803
+ const turn = await executeChatTurn({
13804
+ agent: deps.agent,
13805
+ request,
13806
+ onReplySegment: sendReplySegment
13432
13807
  });
13433
- return true;
13434
- } catch (err) {
13435
- deps.errLog(`intermediate reply failed: ${String(err)}`);
13436
- return false;
13437
- }
13438
- };
13439
- const requestText = appendAttachmentNotes(bodyFromItemList(full.item_list), attachmentNotes);
13440
- const request = {
13441
- accountId: deps.accountId,
13442
- conversationId: buildWeixinChatKey(deps.accountId, full.from_user_id ?? ""),
13443
- text: requestText,
13444
- ...media.length > 0 ? { media } : {},
13445
- replyContextToken: contextToken
13446
- };
13447
- try {
13448
- const turn = await executeChatTurn({
13449
- agent: deps.agent,
13450
- request,
13451
- onReplySegment: sendReplySegment
13452
- });
13453
- const outboundMedia = normalizeMediaArray(turn.media);
13454
- if (turn.text) {
13455
- const finalText = markdownToPlainText(turn.text).trim();
13456
- if (finalText.length > 0) {
13457
- const rawChunks = chunkFinalText(finalText, MAX_FINAL_CHUNK_BYTES);
13458
- if (rawChunks.length > 0) {
13459
- const total = rawChunks.length;
13460
- if (total === 1) {
13461
- const reserved = deps.reserveFinal ? deps.reserveFinal(to) : true;
13462
- if (!reserved) {
13463
- deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=text chatKey=${to}`);
13464
- } else {
13465
- await sendMessageWeixin({
13466
- to,
13467
- text: rawChunks[0],
13468
- opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
13469
- });
13470
- }
13471
- } else {
13472
- const prefixed = rawChunks.map((body, i) => `(${i + 1}/${total}) ${body}`);
13473
- const available = deps.finalRemaining ? deps.finalRemaining(to) : total;
13474
- const waveSize = Math.max(Math.min(available, total), 0);
13475
- const wave = prefixed.slice(0, waveSize);
13476
- const rest = prefixed.slice(waveSize);
13477
- if (wave.length > 0 && rest.length > 0) {
13478
- const sentSoFar = wave.length;
13479
- wave[wave.length - 1] = `${wave[wave.length - 1]}
13480
-
13481
- ${buildFinalHeadsUp({
13482
- total,
13483
- sentSoFar
13484
- })}`;
13485
- }
13486
- let sent = 0;
13487
- for (let i = 0;i < wave.length; i += 1) {
13808
+ const outboundMedia = normalizeMediaArray(turn.media);
13809
+ let finalFirstSent = false;
13810
+ let finalChunksSent = 0;
13811
+ let finalChunksPending = 0;
13812
+ let finalDropped = false;
13813
+ if (turn.text) {
13814
+ const finalText = markdownToPlainText(turn.text).trim();
13815
+ if (finalText.length > 0) {
13816
+ const rawChunks = chunkFinalText(finalText, MAX_FINAL_CHUNK_BYTES);
13817
+ if (rawChunks.length > 0) {
13818
+ const total = rawChunks.length;
13819
+ if (total === 1) {
13488
13820
  const reserved = deps.reserveFinal ? deps.reserveFinal(to) : true;
13489
13821
  if (!reserved) {
13490
- deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=text_paginated chatKey=${to} chunk=${i + 1}/${total}`);
13491
- break;
13492
- }
13493
- try {
13822
+ finalDropped = true;
13823
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=text chatKey=${to}`);
13824
+ } else {
13494
13825
  await sendMessageWeixin({
13495
13826
  to,
13496
- text: wave[i],
13827
+ text: rawChunks[0],
13497
13828
  opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
13498
13829
  });
13499
- sent += 1;
13500
- } catch (sendErr) {
13501
- deps.errLog(`weixin.final.dropped reason=send_failed kind=text_paginated chatKey=${to} chunk=${i + 1}/${total} err=${String(sendErr)}`);
13502
- break;
13830
+ finalChunksSent += 1;
13831
+ if (!finalFirstSent) {
13832
+ finalFirstSent = true;
13833
+ perfSpan.mark("reply.final_first_sent", { bytes: utf8ByteLength(rawChunks[0]), chunkIndex: 1 });
13834
+ }
13835
+ }
13836
+ } else {
13837
+ const prefixed = rawChunks.map((body, i) => `(${i + 1}/${total}) ${body}`);
13838
+ const available = deps.finalRemaining ? deps.finalRemaining(to) : total;
13839
+ const waveSize = Math.max(Math.min(available, total), 0);
13840
+ const wave = prefixed.slice(0, waveSize);
13841
+ const rest = prefixed.slice(waveSize);
13842
+ if (wave.length > 0 && rest.length > 0) {
13843
+ const sentSoFar = wave.length;
13844
+ wave[wave.length - 1] = `${wave[wave.length - 1]}
13845
+
13846
+ ${buildFinalHeadsUp({
13847
+ total,
13848
+ sentSoFar
13849
+ })}`;
13850
+ }
13851
+ let sent = 0;
13852
+ for (let i = 0;i < wave.length; i += 1) {
13853
+ const reserved = deps.reserveFinal ? deps.reserveFinal(to) : true;
13854
+ if (!reserved) {
13855
+ finalDropped = true;
13856
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=text_paginated chatKey=${to} chunk=${i + 1}/${total}`);
13857
+ break;
13858
+ }
13859
+ try {
13860
+ await sendMessageWeixin({
13861
+ to,
13862
+ text: wave[i],
13863
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
13864
+ });
13865
+ sent += 1;
13866
+ finalChunksSent += 1;
13867
+ if (!finalFirstSent) {
13868
+ finalFirstSent = true;
13869
+ perfSpan.mark("reply.final_first_sent", { bytes: utf8ByteLength(wave[i]), chunkIndex: i + 1 });
13870
+ }
13871
+ } catch (sendErr) {
13872
+ finalDropped = true;
13873
+ deps.errLog(`weixin.final.dropped reason=send_failed kind=text_paginated chatKey=${to} chunk=${i + 1}/${total} err=${String(sendErr)}`);
13874
+ break;
13875
+ }
13876
+ }
13877
+ const restToPark = prefixed.slice(sent);
13878
+ finalChunksPending = restToPark.length;
13879
+ if (restToPark.length > 0 && deps.enqueuePendingFinal) {
13880
+ const pending = restToPark.map((text, idx) => {
13881
+ const seq = sent + idx + 1;
13882
+ const entry = { text, seq, total };
13883
+ if (contextToken !== undefined)
13884
+ entry.contextToken = contextToken;
13885
+ if (deps.accountId !== undefined)
13886
+ entry.accountId = deps.accountId;
13887
+ return entry;
13888
+ });
13889
+ deps.enqueuePendingFinal(to, pending);
13503
13890
  }
13504
- }
13505
- const restToPark = prefixed.slice(sent);
13506
- if (restToPark.length > 0 && deps.enqueuePendingFinal) {
13507
- const pending = restToPark.map((text, idx) => {
13508
- const seq = sent + idx + 1;
13509
- const entry = { text, seq, total };
13510
- if (contextToken !== undefined)
13511
- entry.contextToken = contextToken;
13512
- if (deps.accountId !== undefined)
13513
- entry.accountId = deps.accountId;
13514
- return entry;
13515
- });
13516
- deps.enqueuePendingFinal(to, pending);
13517
13891
  }
13518
13892
  }
13519
13893
  }
13894
+ perfSpan.mark("reply.final_done", {
13895
+ chunksSent: finalChunksSent,
13896
+ chunksPending: finalChunksPending,
13897
+ dropped: finalDropped
13898
+ });
13520
13899
  }
13521
- }
13522
- for (const mediaItem of outboundMedia) {
13523
- const filePath = await resolveSafeOutboundMediaPath(mediaItem.filePath, [mediaStore.rootDir, resolveMediaTempDir(deps.mediaTempDir), ...deps.allowedMediaRoots ?? []]);
13524
- if (!filePath) {
13525
- deps.errLog(`outbound media rejected: path=${mediaItem.filePath}`);
13526
- continue;
13900
+ let mediaSent = 0;
13901
+ let mediaFailed = 0;
13902
+ let mediaRejected = 0;
13903
+ let mediaDropped = 0;
13904
+ for (const mediaItem of outboundMedia) {
13905
+ const filePath = await resolveSafeOutboundMediaPath(mediaItem.filePath, [mediaStore.rootDir, resolveMediaTempDir(deps.mediaTempDir), ...deps.allowedMediaRoots ?? []]);
13906
+ if (!filePath) {
13907
+ mediaRejected += 1;
13908
+ deps.errLog(`outbound media rejected: path=${mediaItem.filePath}`);
13909
+ continue;
13910
+ }
13911
+ const caption = mediaItem.caption ? markdownToPlainText(mediaItem.caption) : "";
13912
+ const captionReserve = caption && deps.reserveFinal ? deps.reserveFinal(to) : true;
13913
+ if (!captionReserve) {
13914
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=media_caption chatKey=${to}`);
13915
+ }
13916
+ const reservedMedia = deps.reserveFinal ? deps.reserveFinal(to) : true;
13917
+ if (!reservedMedia) {
13918
+ mediaDropped += 1;
13919
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=media chatKey=${to}`);
13920
+ continue;
13921
+ }
13922
+ try {
13923
+ const sent = await sendWeixinMediaFile({
13924
+ media: mediaItem,
13925
+ filePath,
13926
+ to,
13927
+ text: captionReserve ? caption : "",
13928
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
13929
+ cdnBaseUrl: deps.cdnBaseUrl
13930
+ });
13931
+ mediaSent += 1;
13932
+ perfSpan.mark("reply.media_sent", {
13933
+ kind: mediaItem.kind,
13934
+ index: mediaSent + mediaFailed + mediaRejected + mediaDropped,
13935
+ messageId: sent.messageId
13936
+ });
13937
+ } catch (err) {
13938
+ mediaFailed += 1;
13939
+ deps.errLog(`outbound media send failed: ${String(err)}`);
13940
+ }
13527
13941
  }
13528
- const caption = mediaItem.caption ? markdownToPlainText(mediaItem.caption) : "";
13529
- const captionReserve = caption && deps.reserveFinal ? deps.reserveFinal(to) : true;
13530
- if (!captionReserve) {
13531
- deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=media_caption chatKey=${to}`);
13942
+ if (outboundMedia.length > 0) {
13943
+ perfSpan.mark("reply.media_done", {
13944
+ mediaCount: outboundMedia.length,
13945
+ sent: mediaSent,
13946
+ failed: mediaFailed,
13947
+ rejected: mediaRejected,
13948
+ dropped: mediaDropped
13949
+ });
13532
13950
  }
13533
- const reservedMedia = deps.reserveFinal ? deps.reserveFinal(to) : true;
13534
- if (!reservedMedia) {
13535
- deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=media chatKey=${to}`);
13536
- continue;
13951
+ } catch (err) {
13952
+ if (isAbortError(err)) {
13953
+ perfSpan.setOutcome("aborted", { reason: "user_cancel" });
13954
+ deps.log(`handleWeixinMessageTurn: turn aborted: ${err.message}`);
13955
+ return;
13537
13956
  }
13538
- try {
13539
- await sendWeixinMediaFile({
13540
- media: mediaItem,
13541
- filePath,
13957
+ perfSpan.setOutcome("error", { reason: "turn_error" });
13958
+ const errorText = err instanceof Error ? err.stack ?? err.message : JSON.stringify(err);
13959
+ deps.errLog(`handleWeixinMessageTurn: agent or send failed: ${errorText}`);
13960
+ const reservedErr = deps.reserveFinal ? deps.reserveFinal(to) : true;
13961
+ if (!reservedErr) {
13962
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=error_notice chatKey=${to}`);
13963
+ } else {
13964
+ sendWeixinErrorNotice({
13542
13965
  to,
13543
- text: captionReserve ? caption : "",
13544
- opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
13545
- cdnBaseUrl: deps.cdnBaseUrl
13966
+ contextToken,
13967
+ message: `⚠️ 过程失败:${err instanceof Error ? err.message : JSON.stringify(err)}`,
13968
+ baseUrl: deps.baseUrl,
13969
+ token: deps.token,
13970
+ errLog: deps.errLog
13546
13971
  });
13547
- } catch (err) {
13548
- deps.errLog(`outbound media send failed: ${String(err)}`);
13549
13972
  }
13973
+ } finally {
13974
+ stopTypingIndicator();
13550
13975
  }
13551
- } catch (err) {
13552
- const errorText = err instanceof Error ? err.stack ?? err.message : JSON.stringify(err);
13553
- deps.errLog(`handleWeixinMessageTurn: agent or send failed: ${errorText}`);
13554
- const reservedErr = deps.reserveFinal ? deps.reserveFinal(to) : true;
13555
- if (!reservedErr) {
13556
- deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=error_notice chatKey=${to}`);
13557
- } else {
13558
- sendWeixinErrorNotice({
13559
- to,
13560
- contextToken,
13561
- message: `⚠️ 过程失败:${err instanceof Error ? err.message : JSON.stringify(err)}`,
13562
- baseUrl: deps.baseUrl,
13563
- token: deps.token,
13564
- errLog: deps.errLog
13565
- });
13566
- }
13567
- } finally {
13568
- stopTypingIndicator();
13569
- }
13976
+ });
13977
+ }
13978
+ function isAbortError(error2) {
13979
+ return error2 instanceof Error && error2.name === "AbortError";
13570
13980
  }
13571
13981
  var MAX_FINAL_CHUNK_BYTES = 1800;
13572
13982
  var init_handle_weixin_message_turn = __esm(() => {
@@ -13581,6 +13991,7 @@ var init_handle_weixin_message_turn = __esm(() => {
13581
13991
  init_send_media();
13582
13992
  init_send();
13583
13993
  init_slash_commands();
13994
+ init_perf_tracer();
13584
13995
  });
13585
13996
 
13586
13997
  // src/weixin/storage/sync-buf.ts
@@ -13769,7 +14180,8 @@ async function monitorWeixinProvider(opts) {
13769
14180
  ...opts.drainPendingFinal ? { drainPendingFinal: opts.drainPendingFinal } : {},
13770
14181
  ...opts.prependPendingFinal ? { prependPendingFinal: opts.prependPendingFinal } : {},
13771
14182
  ...opts.mediaStore ? { mediaStore: opts.mediaStore } : {},
13772
- ...opts.allowedMediaRoots ? { allowedMediaRoots: opts.allowedMediaRoots } : {}
14183
+ ...opts.allowedMediaRoots ? { allowedMediaRoots: opts.allowedMediaRoots } : {},
14184
+ ...opts.perfTracer ? { perfTracer: opts.perfTracer } : {}
13773
14185
  })).catch((err) => {
13774
14186
  errLog(`[weixin] message turn failed: ${String(err)}`);
13775
14187
  });
@@ -13912,7 +14324,8 @@ async function start(agent, opts) {
13912
14324
  ...opts?.prependPendingFinal ? { prependPendingFinal: opts.prependPendingFinal } : {},
13913
14325
  ...opts?.enqueuePendingFinal ? { enqueuePendingFinal: opts.enqueuePendingFinal } : {},
13914
14326
  ...opts?.dropPendingFinal ? { dropPendingFinal: opts.dropPendingFinal } : {},
13915
- ...opts?.mediaStore ? { mediaStore: opts.mediaStore } : {}
14327
+ ...opts?.mediaStore ? { mediaStore: opts.mediaStore } : {},
14328
+ ...opts?.perfTracer ? { perfTracer: opts.perfTracer } : {}
13916
14329
  });
13917
14330
  }
13918
14331
  var init_bot = __esm(() => {
@@ -14168,16 +14581,16 @@ var init_deliver_coordinator_message = __esm(() => {
14168
14581
  });
14169
14582
 
14170
14583
  // src/weixin/monitor/consumer-lock.ts
14171
- import { mkdir as mkdir8, open as open2, readFile as readFile6, rm as rm5 } from "node:fs/promises";
14172
- import { dirname as dirname6, join as join4 } from "node:path";
14584
+ import { mkdir as mkdir8, open as open2, readFile as readFile6, rm as rm6 } from "node:fs/promises";
14585
+ import { dirname as dirname8, join as join5 } from "node:path";
14173
14586
  import { homedir as homedir4 } from "node:os";
14174
14587
  function createWeixinConsumerLock(options = {}) {
14175
- const lockFilePath = options.lockFilePath ?? join4(homedir4(), ".weacpx", "runtime", "weixin-consumer.lock.json");
14588
+ const lockFilePath = options.lockFilePath ?? join5(homedir4(), ".weacpx", "runtime", "weixin-consumer.lock.json");
14176
14589
  const isProcessRunning = options.isProcessRunning ?? defaultIsProcessRunning4;
14177
14590
  const onDiagnostic = options.onDiagnostic;
14178
14591
  return {
14179
14592
  async acquire(meta2) {
14180
- await mkdir8(dirname6(lockFilePath), { recursive: true });
14593
+ await mkdir8(dirname8(lockFilePath), { recursive: true });
14181
14594
  while (true) {
14182
14595
  try {
14183
14596
  const handle = await open2(lockFilePath, "wx");
@@ -14208,7 +14621,7 @@ function createWeixinConsumerLock(options = {}) {
14208
14621
  });
14209
14622
  const existing = await loadLockMetadata(lockFilePath);
14210
14623
  if (!existing) {
14211
- await rm5(lockFilePath, { force: true });
14624
+ await rm6(lockFilePath, { force: true });
14212
14625
  await onDiagnostic?.("lock_invalid_removed", {
14213
14626
  lockFilePath,
14214
14627
  reason: "invalid_or_unreadable_metadata"
@@ -14216,7 +14629,7 @@ function createWeixinConsumerLock(options = {}) {
14216
14629
  continue;
14217
14630
  }
14218
14631
  if (!isProcessRunning(existing.pid)) {
14219
- await rm5(lockFilePath, { force: true });
14632
+ await rm6(lockFilePath, { force: true });
14220
14633
  await onDiagnostic?.("lock_stale_removed", {
14221
14634
  lockFilePath,
14222
14635
  stalePid: existing.pid,
@@ -14241,7 +14654,7 @@ function createWeixinConsumerLock(options = {}) {
14241
14654
  }
14242
14655
  },
14243
14656
  async release() {
14244
- await rm5(lockFilePath, { force: true });
14657
+ await rm6(lockFilePath, { force: true });
14245
14658
  await onDiagnostic?.("lock_released", {
14246
14659
  lockFilePath
14247
14660
  });
@@ -14340,7 +14753,8 @@ class WeixinChannel {
14340
14753
  drainPendingFinal: (chatKey, available) => input.quota.drainPendingFinalUpToBudget(chatKey, available),
14341
14754
  prependPendingFinal: (chatKey, chunks) => input.quota.prependPendingFinal(chatKey, chunks),
14342
14755
  enqueuePendingFinal: (chatKey, chunks) => input.quota.enqueuePendingFinal(chatKey, chunks),
14343
- dropPendingFinal: (chatKey) => input.quota.clearPendingFinal(chatKey)
14756
+ dropPendingFinal: (chatKey) => input.quota.clearPendingFinal(chatKey),
14757
+ ...input.perfTracer ? { perfTracer: input.perfTracer } : {}
14344
14758
  });
14345
14759
  }
14346
14760
  async notifyTaskCompletion(task) {
@@ -14849,9 +15263,9 @@ __export(exports_plugin_loader, {
14849
15263
  });
14850
15264
  import { createRequire as createRequire2 } from "node:module";
14851
15265
  import { pathToFileURL } from "node:url";
14852
- import { join as join5 } from "node:path";
15266
+ import { join as join6 } from "node:path";
14853
15267
  async function importPluginFromHome(packageName, pluginHome) {
14854
- const requireFromHome = createRequire2(join5(pluginHome, "package.json"));
15268
+ const requireFromHome = createRequire2(join6(pluginHome, "package.json"));
14855
15269
  const entry = requireFromHome.resolve(packageName);
14856
15270
  return await import(pathToFileURL(entry).href);
14857
15271
  }
@@ -14899,8 +15313,8 @@ var init_bootstrap = __esm(() => {
14899
15313
  });
14900
15314
 
14901
15315
  // src/logging/app-logger.ts
14902
- import { appendFile, mkdir as mkdir9, readdir as readdir2, rename, rm as rm6, stat as stat2 } from "node:fs/promises";
14903
- import { basename, dirname as dirname8, join as join9 } from "node:path";
15316
+ import { appendFile, mkdir as mkdir9 } from "node:fs/promises";
15317
+ import { dirname as dirname10 } from "node:path";
14904
15318
  function createNoopAppLogger() {
14905
15319
  return {
14906
15320
  debug: async () => {},
@@ -14940,74 +15354,18 @@ function createAppLogger(options) {
14940
15354
  return;
14941
15355
  }
14942
15356
  const line = formatLogLine(now(), level, event, message, context);
14943
- await mkdir9(dirname8(options.filePath), { recursive: true });
15357
+ await mkdir9(dirname10(options.filePath), { recursive: true });
14944
15358
  await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
14945
15359
  await appendFile(options.filePath, line, "utf8");
14946
15360
  }
14947
15361
  }
14948
- async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
14949
- let currentSize = 0;
14950
- try {
14951
- currentSize = (await stat2(filePath)).size;
14952
- } catch (error2) {
14953
- if (!isMissingFileError2(error2)) {
14954
- throw error2;
14955
- }
14956
- }
14957
- if (currentSize + incomingSize <= maxSizeBytes) {
14958
- return;
14959
- }
14960
- if (currentSize === 0) {
14961
- return;
14962
- }
14963
- if (maxFiles <= 0) {
14964
- await rm6(filePath, { force: true });
14965
- return;
14966
- }
14967
- await rm6(`${filePath}.${maxFiles}`, { force: true });
14968
- for (let index = maxFiles - 1;index >= 1; index -= 1) {
14969
- const source = `${filePath}.${index}`;
14970
- try {
14971
- await rename(source, `${filePath}.${index + 1}`);
14972
- } catch (error2) {
14973
- if (!isMissingFileError2(error2)) {
14974
- throw error2;
14975
- }
14976
- }
14977
- }
14978
- await rename(filePath, `${filePath}.1`);
14979
- }
14980
- async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
14981
- const parentDir = dirname8(filePath);
14982
- const prefix = `${basename(filePath)}.`;
14983
- const cutoff = now().getTime() - retentionDays * 24 * 60 * 60 * 1000;
14984
- let files = [];
14985
- try {
14986
- files = await readdir2(parentDir);
14987
- } catch (error2) {
14988
- if (isMissingFileError2(error2)) {
14989
- return;
14990
- }
14991
- throw error2;
14992
- }
14993
- for (const file of files) {
14994
- if (!file.startsWith(prefix) || !/^\d+$/.test(file.slice(prefix.length))) {
14995
- continue;
14996
- }
14997
- const candidate = join9(parentDir, file);
14998
- const details = await stat2(candidate);
14999
- if (details.mtime.getTime() < cutoff) {
15000
- await rm6(candidate, { force: true });
15001
- }
15002
- }
15003
- }
15004
15362
  function formatLogLine(time3, level, event, message, context) {
15005
- const fields = Object.entries(context).filter(([, value]) => value !== undefined).map(([key, value]) => `${key}=${formatValue(value)}`);
15363
+ const fields = Object.entries(context).filter(([, value]) => value !== undefined).map(([key, value]) => `${key}=${formatValue2(value)}`);
15006
15364
  const suffix = fields.length > 0 ? ` ${fields.join(" ")}` : "";
15007
- return `${time3.toISOString()} ${level.toUpperCase()} ${event} message=${formatValue(message)}${suffix}
15365
+ return `${time3.toISOString()} ${level.toUpperCase()} ${event} message=${formatValue2(message)}${suffix}
15008
15366
  `;
15009
15367
  }
15010
- function formatValue(value) {
15368
+ function formatValue2(value) {
15011
15369
  if (value === null) {
15012
15370
  return "null";
15013
15371
  }
@@ -15016,11 +15374,9 @@ function formatValue(value) {
15016
15374
  }
15017
15375
  return JSON.stringify(value);
15018
15376
  }
15019
- function isMissingFileError2(error2) {
15020
- return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
15021
- }
15022
15377
  var LEVEL_ORDER;
15023
15378
  var init_app_logger = __esm(() => {
15379
+ init_rotating_file_writer();
15024
15380
  LEVEL_ORDER = {
15025
15381
  error: 0,
15026
15382
  info: 1,
@@ -16440,6 +16796,7 @@ async function handleSessionAttach(context, chatKey, alias, agent, workspace, tr
16440
16796
  `)
16441
16797
  };
16442
16798
  }
16799
+ context.lifecycle.markSessionReady?.(attached);
16443
16800
  await context.sessions.attachSession(internalAlias, agent, workspace, transportSession);
16444
16801
  await context.sessions.useSession(chatKey, internalAlias);
16445
16802
  await refreshSessionTransportAgentCommandBestEffort(context, internalAlias, "session.attach.agent_command_refresh_failed");
@@ -16636,7 +16993,7 @@ async function handleSessionRemove(context, chatKey, alias) {
16636
16993
  return { text: lines.join(`
16637
16994
  `) };
16638
16995
  }
16639
- async function promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent) {
16996
+ async function promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan) {
16640
16997
  const effectiveReplyMode = session.replyMode ?? context.config?.channel.replyMode ?? "verbose";
16641
16998
  if (!session.replyMode)
16642
16999
  session.replyMode = effectiveReplyMode;
@@ -16661,7 +17018,7 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
16661
17018
  const { promptText, taskIds, groupIds, claimHumanReply } = await preparePromptWithFallback(context, session, chatKey, text, replyContextToken, accountId);
16662
17019
  try {
16663
17020
  const replyContext = transportReply && context.quota && getChannelIdFromChatKey(chatKey) === "weixin" ? { chatKey, quota: context.quota } : undefined;
16664
- const result = await context.interaction.promptTransportSession(session, promptText, transportReply, replyContext, media, abortSignal, onToolEvent);
17021
+ const result = await context.interaction.promptTransportSession(session, promptText, transportReply, replyContext, media, abortSignal, onToolEvent, perfSpan);
16665
17022
  if (claimHumanReply) {
16666
17023
  try {
16667
17024
  await context.orchestration?.claimActiveHumanReply?.(claimHumanReply);
@@ -16681,17 +17038,17 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
16681
17038
  throw error2;
16682
17039
  }
16683
17040
  }
16684
- async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent) {
17041
+ async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan) {
16685
17042
  const session = await context.sessions.getCurrentSession(chatKey);
16686
17043
  if (!session) {
16687
17044
  return { text: NO_CURRENT_SESSION_TEXT };
16688
17045
  }
16689
17046
  try {
16690
- return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent);
17047
+ return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan);
16691
17048
  } catch (error2) {
16692
17049
  const recovered = await context.recovery.tryRecoverMissingSession(session, error2);
16693
17050
  if (recovered) {
16694
- return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent);
17051
+ return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan);
16695
17052
  }
16696
17053
  return context.recovery.renderTransportError(session, error2);
16697
17054
  }
@@ -17955,7 +18312,7 @@ import { spawn as spawn6 } from "node:child_process";
17955
18312
  import { createRequire as createRequire3 } from "node:module";
17956
18313
  import { access as access3 } from "node:fs/promises";
17957
18314
  import { homedir as homedir7 } from "node:os";
17958
- import { dirname as dirname9, join as join11 } from "node:path";
18315
+ import { dirname as dirname11, join as join11 } from "node:path";
17959
18316
  function deriveParentPackageName(platformPackage) {
17960
18317
  return platformPackage.replace(/-(?:linux|darwin|win32|windows|freebsd|openbsd|sunos|aix)(?:-(?:x64|arm64|ia32|arm|ppc64|s390x))?(?:-(?:baseline|musl|gnu|gnueabihf|musleabihf|msvc))?$/, "");
17961
18318
  }
@@ -18028,7 +18385,7 @@ function defaultResolveFromCwd(name, cwd) {
18028
18385
  const pkgJson = require2.resolve(`${name}/package.json`, {
18029
18386
  paths: [cwd, ...require2.resolve.paths(name) ?? []]
18030
18387
  });
18031
- return dirname9(pkgJson);
18388
+ return dirname11(pkgJson);
18032
18389
  } catch {
18033
18390
  return null;
18034
18391
  }
@@ -18186,7 +18543,7 @@ class CommandRouter {
18186
18543
  this.quota = quota;
18187
18544
  this.logger = logger2 ?? createNoopAppLogger();
18188
18545
  }
18189
- async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent) {
18546
+ async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, perfSpan) {
18190
18547
  const startedAt = Date.now();
18191
18548
  const command = parseCommand(input);
18192
18549
  await this.logger.debug("command.parsed", "parsed inbound command", {
@@ -18194,6 +18551,7 @@ class CommandRouter {
18194
18551
  kind: command.kind
18195
18552
  });
18196
18553
  const access4 = authorizeCommandForChat(command, metadata);
18554
+ perfSpan?.mark("router.authorized", { decision: access4.allowed ? "allow" : "deny" });
18197
18555
  if (!access4.allowed) {
18198
18556
  await this.logger.info("command.blocked", "blocked command by chat policy", {
18199
18557
  chatKey,
@@ -18205,6 +18563,7 @@ class CommandRouter {
18205
18563
  return { text: renderCommandAccessDenied(command) };
18206
18564
  }
18207
18565
  await this.refreshConfigFromStore();
18566
+ perfSpan?.mark("router.config_refreshed");
18208
18567
  return await this.executeCommand(chatKey, command.kind, startedAt, async () => {
18209
18568
  switch (command.kind) {
18210
18569
  case "invalid":
@@ -18247,35 +18606,35 @@ class CommandRouter {
18247
18606
  case "workspace.rm":
18248
18607
  return await handleWorkspaceRemove(this.createHandlerContext(), command.name);
18249
18608
  case "sessions":
18250
- return await handleSessions(this.createSessionHandlerContext(), chatKey);
18609
+ return await handleSessions(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18251
18610
  case "session.new":
18252
- return await handleSessionNew(this.createSessionHandlerContext(reply), chatKey, command.alias, command.agent, command.workspace);
18611
+ return await handleSessionNew(this.createSessionHandlerContext(reply, perfSpan), chatKey, command.alias, command.agent, command.workspace);
18253
18612
  case "session.shortcut":
18254
- return await handleSessionShortcut(this.createSessionHandlerContext(reply), chatKey, command.agent, command, false);
18613
+ return await handleSessionShortcut(this.createSessionHandlerContext(reply, perfSpan), chatKey, command.agent, command, false);
18255
18614
  case "session.shortcut.new":
18256
- return await handleSessionShortcut(this.createSessionHandlerContext(reply), chatKey, command.agent, command, true);
18615
+ return await handleSessionShortcut(this.createSessionHandlerContext(reply, perfSpan), chatKey, command.agent, command, true);
18257
18616
  case "session.attach":
18258
- return await handleSessionAttach(this.createSessionHandlerContext(reply), chatKey, command.alias, command.agent, command.workspace, command.transportSession);
18617
+ return await handleSessionAttach(this.createSessionHandlerContext(reply, perfSpan), chatKey, command.alias, command.agent, command.workspace, command.transportSession);
18259
18618
  case "session.use":
18260
- return await handleSessionUse(this.createSessionHandlerContext(), chatKey, command.alias);
18619
+ return await handleSessionUse(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.alias);
18261
18620
  case "mode.show":
18262
- return await handleModeShow(this.createSessionHandlerContext(), chatKey);
18621
+ return await handleModeShow(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18263
18622
  case "mode.set":
18264
- return await handleModeSet(this.createSessionHandlerContext(), chatKey, command.modeId);
18623
+ return await handleModeSet(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.modeId);
18265
18624
  case "replymode.show":
18266
- return await handleReplyModeShow(this.createSessionHandlerContext(), chatKey);
18625
+ return await handleReplyModeShow(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18267
18626
  case "replymode.set":
18268
- return await handleReplyModeSet(this.createSessionHandlerContext(), chatKey, command.replyMode);
18627
+ return await handleReplyModeSet(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.replyMode);
18269
18628
  case "replymode.reset":
18270
- return await handleReplyModeReset(this.createSessionHandlerContext(), chatKey);
18629
+ return await handleReplyModeReset(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18271
18630
  case "status":
18272
- return await handleStatus(this.createSessionHandlerContext(), chatKey);
18631
+ return await handleStatus(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18273
18632
  case "cancel":
18274
- return await handleCancel(this.createSessionHandlerContext(), chatKey);
18633
+ return await handleCancel(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18275
18634
  case "session.reset":
18276
- return await handleSessionReset(this.createSessionHandlerContext(reply), chatKey);
18635
+ return await handleSessionReset(this.createSessionHandlerContext(reply, perfSpan), chatKey);
18277
18636
  case "session.rm":
18278
- return await handleSessionRemove(this.createSessionHandlerContext(), chatKey, command.alias);
18637
+ return await handleSessionRemove(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.alias);
18279
18638
  case "groups":
18280
18639
  return await handleGroupList(this.createHandlerContext(), chatKey, command.filter);
18281
18640
  case "group.new":
@@ -18301,7 +18660,7 @@ class CommandRouter {
18301
18660
  case "task.cancel":
18302
18661
  return await handleTaskCancel(this.createHandlerContext(), chatKey, command.taskId);
18303
18662
  case "prompt":
18304
- return await handlePrompt(this.createSessionHandlerContext(), chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent);
18663
+ return await handlePrompt(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan);
18305
18664
  }
18306
18665
  });
18307
18666
  }
@@ -18320,23 +18679,24 @@ class CommandRouter {
18320
18679
  ...this.quota ? { quota: this.quota } : {}
18321
18680
  };
18322
18681
  }
18323
- createSessionHandlerContext(reply) {
18682
+ createSessionHandlerContext(reply, perfSpan) {
18324
18683
  return {
18325
18684
  ...this.createHandlerContext(),
18326
- lifecycle: this.createSessionLifecycleOps(reply),
18327
- interaction: this.createSessionInteractionOps(),
18685
+ lifecycle: this.createSessionLifecycleOps(reply, perfSpan),
18686
+ interaction: this.createSessionInteractionOps(perfSpan),
18328
18687
  recovery: this.createSessionRenderRecoveryOps()
18329
18688
  };
18330
18689
  }
18331
- createSessionLifecycleOps(reply) {
18690
+ createSessionLifecycleOps(reply, perfSpan) {
18332
18691
  return {
18333
18692
  resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
18334
- ensureTransportSession: (session, replyOverride) => this.ensureTransportSession(session, replyOverride ?? reply),
18693
+ ensureTransportSession: (session, replyOverride, perfSpanOverride) => this.ensureTransportSession(session, replyOverride ?? reply, perfSpanOverride ?? perfSpan),
18335
18694
  checkTransportSession: (session) => this.checkTransportSession(session),
18695
+ markSessionReady: () => perfSpan?.mark("session.ready"),
18336
18696
  reserveTransportSession: (transportSession) => this.reserveLogicalTransportSession(transportSession),
18337
18697
  handleSessionShortcut: async (chatKey, agent, target, createNew, replyOverride) => {
18338
18698
  try {
18339
- return await handleSessionShortcutCommand(this.createHandlerContext(), this.createSessionShortcutOps(replyOverride ?? reply), chatKey, agent, target, createNew);
18699
+ return await handleSessionShortcutCommand(this.createHandlerContext(), this.createSessionShortcutOps(replyOverride ?? reply, perfSpan), chatKey, agent, target, createNew);
18340
18700
  } catch (err) {
18341
18701
  if (err instanceof AutoInstallFailedError) {
18342
18702
  const session = this.sessions.resolveSession(`${agent}`, agent, target.workspace ?? "", `${agent}`);
@@ -18345,15 +18705,15 @@ class CommandRouter {
18345
18705
  throw err;
18346
18706
  }
18347
18707
  },
18348
- resetCurrentSession: (chatKey, replyOverride) => handleSessionResetCommand(this.createHandlerContext(), this.createSessionResetOps(replyOverride ?? reply), chatKey),
18708
+ resetCurrentSession: (chatKey, replyOverride) => handleSessionResetCommand(this.createHandlerContext(), this.createSessionResetOps(replyOverride ?? reply, perfSpan), chatKey),
18349
18709
  refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias)
18350
18710
  };
18351
18711
  }
18352
- createSessionInteractionOps() {
18712
+ createSessionInteractionOps(perfSpan) {
18353
18713
  return {
18354
18714
  setModeTransportSession: (session, modeId) => this.setModeTransportSession(session, modeId),
18355
18715
  cancelTransportSession: (session) => this.cancelTransportSession(session),
18356
- promptTransportSession: (session, text, reply, replyContext, media, abortSignal, onToolEvent) => this.promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent)
18716
+ promptTransportSession: (session, text, reply, replyContext, media, abortSignal, onToolEvent, perfSpanOverride) => this.promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent, perfSpanOverride ?? perfSpan)
18357
18717
  };
18358
18718
  }
18359
18719
  createSessionRenderRecoveryOps() {
@@ -18364,9 +18724,9 @@ class CommandRouter {
18364
18724
  renderTransportError: (session, error2) => renderTransportError(session, error2)
18365
18725
  };
18366
18726
  }
18367
- createSessionResetOps(reply) {
18727
+ createSessionResetOps(reply, perfSpan) {
18368
18728
  return {
18369
- ensureTransportSession: (session, replyOverride) => this.ensureTransportSession(session, replyOverride ?? reply),
18729
+ ensureTransportSession: (session, replyOverride, perfSpanOverride) => this.ensureTransportSession(session, replyOverride ?? reply, perfSpanOverride ?? perfSpan),
18370
18730
  checkTransportSession: (session) => this.checkTransportSession(session),
18371
18731
  reserveTransportSession: (transportSession) => this.reserveLogicalTransportSession(transportSession),
18372
18732
  resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
@@ -18381,10 +18741,10 @@ class CommandRouter {
18381
18741
  getSession: (alias) => this.sessions.getSession(alias)
18382
18742
  };
18383
18743
  }
18384
- createSessionShortcutOps(reply) {
18744
+ createSessionShortcutOps(reply, perfSpan) {
18385
18745
  return {
18386
18746
  resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
18387
- ensureTransportSession: (session, replyOverride) => this.ensureTransportSession(session, replyOverride ?? reply),
18747
+ ensureTransportSession: (session, replyOverride, perfSpanOverride) => this.ensureTransportSession(session, replyOverride ?? reply, perfSpanOverride ?? perfSpan),
18388
18748
  checkTransportSession: (session) => this.checkTransportSession(session),
18389
18749
  reserveTransportSession: (transportSession) => this.reserveLogicalTransportSession(transportSession),
18390
18750
  refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias)
@@ -18445,13 +18805,14 @@ class CommandRouter {
18445
18805
  throw error2;
18446
18806
  }
18447
18807
  }
18448
- async ensureTransportSession(session, reply) {
18808
+ async ensureTransportSession(session, reply, perfSpan) {
18449
18809
  const attemptSession = (operation) => {
18450
18810
  const { handler, dispose } = this.createProgressHandler(session, reply);
18451
18811
  return this.measureTransportCall(operation, session, () => this.transport.ensureSession(session, handler)).finally(dispose);
18452
18812
  };
18453
18813
  try {
18454
18814
  await attemptSession("ensure_session");
18815
+ perfSpan?.mark("session.ready");
18455
18816
  } catch (err) {
18456
18817
  if (!(err instanceof MissingOptionalDepError))
18457
18818
  throw err;
@@ -18464,6 +18825,7 @@ class CommandRouter {
18464
18825
  await reply?.(`\uD83D\uDD04 安装完成,正在验证会话启动…`);
18465
18826
  try {
18466
18827
  await attemptSession("ensure_session.verify");
18828
+ perfSpan?.mark("session.ready");
18467
18829
  return true;
18468
18830
  } catch (retryErr) {
18469
18831
  if (retryErr instanceof MissingOptionalDepError)
@@ -18529,11 +18891,13 @@ class CommandRouter {
18529
18891
  async checkTransportSession(session) {
18530
18892
  return await this.measureTransportCall("has_session", session, () => this.transport.hasSession(session));
18531
18893
  }
18532
- async promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent) {
18894
+ async promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent, perfSpan) {
18533
18895
  session.mcpCoordinatorSession ??= session.transportSession;
18534
18896
  let done = false;
18897
+ let abortRequested = false;
18535
18898
  let cancelOnAbort;
18536
18899
  const fireCancel = () => {
18900
+ abortRequested = true;
18537
18901
  if (done)
18538
18902
  return;
18539
18903
  try {
@@ -18557,20 +18921,42 @@ class CommandRouter {
18557
18921
  });
18558
18922
  }
18559
18923
  };
18924
+ let localOutcome = "ok";
18560
18925
  if (abortSignal) {
18561
18926
  if (abortSignal.aborted) {
18562
- done = true;
18563
- throw new DOMException("Aborted before prompt started", "AbortError");
18927
+ abortRequested = true;
18928
+ } else {
18929
+ cancelOnAbort = fireCancel;
18930
+ abortSignal.addEventListener("abort", cancelOnAbort, { once: true });
18564
18931
  }
18565
- cancelOnAbort = fireCancel;
18566
- abortSignal.addEventListener("abort", cancelOnAbort, { once: true });
18567
18932
  }
18933
+ let firstChunkFired = false;
18934
+ const onSegment = (_segment) => {
18935
+ if (!firstChunkFired) {
18936
+ firstChunkFired = true;
18937
+ perfSpan?.mark("transport.first_chunk");
18938
+ }
18939
+ };
18568
18940
  try {
18941
+ if (abortRequested) {
18942
+ throw new DOMException("Aborted before prompt started", "AbortError");
18943
+ }
18944
+ perfSpan?.mark("transport.prompt_dispatched", {
18945
+ transportKind: this.config?.transport.type ?? inferTransportKind(this.transport)
18946
+ });
18569
18947
  return await this.measureTransportCall("prompt", session, () => this.transport.prompt(session, text, reply, replyContext, {
18570
18948
  ...media ? { media } : {},
18949
+ ...reply ? { onSegment } : {},
18571
18950
  ...onToolEvent ? { onToolEvent } : {}
18572
18951
  }));
18952
+ } catch (error2) {
18953
+ localOutcome = isAbortError2(error2) || abortRequested ? "aborted" : "error";
18954
+ throw error2;
18573
18955
  } finally {
18956
+ if (abortRequested && localOutcome === "ok") {
18957
+ localOutcome = "aborted";
18958
+ }
18959
+ perfSpan?.mark("transport.prompt_done", { localOutcome });
18574
18960
  done = true;
18575
18961
  if (cancelOnAbort && abortSignal) {
18576
18962
  abortSignal.removeEventListener("abort", cancelOnAbort);
@@ -18631,6 +19017,12 @@ class CommandRouter {
18631
19017
  }
18632
19018
  }
18633
19019
  }
19020
+ function isAbortError2(error2) {
19021
+ return error2 instanceof Error && error2.name === "AbortError";
19022
+ }
19023
+ function inferTransportKind(transport) {
19024
+ return transport.constructor.name.includes("Bridge") ? "acpx-bridge" : "acpx-cli";
19025
+ }
18634
19026
  var init_command_router = __esm(() => {
18635
19027
  init_app_logger();
18636
19028
  init_acpx_session_index();
@@ -18720,7 +19112,8 @@ class ConsoleAgent {
18720
19112
  mimeType: m.mimeType,
18721
19113
  ...m.fileName ? { fileName: m.fileName } : {}
18722
19114
  })) : undefined;
18723
- return await this.router.handle(request.conversationId, request.text, request.reply, request.replyContextToken, request.accountId, promptMedia, request.metadata, request.abortSignal, request.onToolEvent);
19115
+ request.perfSpan?.mark("agent.dispatched");
19116
+ return await this.router.handle(request.conversationId, request.text, request.reply, request.replyContextToken, request.accountId, promptMedia, request.metadata, request.abortSignal, request.onToolEvent, request.perfSpan);
18724
19117
  }
18725
19118
  isKnownCommand(text) {
18726
19119
  return isKnownWeacpxCommandText(text);
@@ -22738,12 +23131,21 @@ async function runConsole(paths, deps) {
22738
23131
  runtimeForGc.orchestration.service.purgeExpiredResetCoordinators({ cutoffDays: 7, trigger: "interval" }).catch(() => {});
22739
23132
  }, 86400000);
22740
23133
  }
22741
- await deps.channels.startAll({
22742
- agent: runtime.agent,
22743
- abortSignal: shutdownController.signal,
22744
- quota: runtime.quota,
22745
- logger: runtime.logger
22746
- });
23134
+ try {
23135
+ await deps.channels.startAll({
23136
+ agent: runtime.agent,
23137
+ abortSignal: shutdownController.signal,
23138
+ quota: runtime.quota,
23139
+ logger: runtime.logger,
23140
+ perfTracer: runtime.perfTracer
23141
+ });
23142
+ } catch (error2) {
23143
+ if (deps.channelStartupPolicy !== "best-effort") {
23144
+ throw error2;
23145
+ }
23146
+ await runtime.logger.error("daemon.channels.start_failed", "all channels failed to start; daemon remains alive for orchestration IPC", { error: error2 instanceof Error ? error2.message : String(error2) });
23147
+ await waitForShutdown(shutdownController.signal);
23148
+ }
22747
23149
  } finally {
22748
23150
  await runCleanupSequence({
22749
23151
  removeProcessListener,
@@ -22761,6 +23163,14 @@ async function runConsole(paths, deps) {
22761
23163
  });
22762
23164
  }
22763
23165
  }
23166
+ async function waitForShutdown(signal) {
23167
+ if (signal.aborted) {
23168
+ return;
23169
+ }
23170
+ await new Promise((resolve3) => {
23171
+ signal.addEventListener("abort", () => resolve3(), { once: true });
23172
+ });
23173
+ }
22764
23174
  async function runCleanupSequence(input) {
22765
23175
  let cleanupError = null;
22766
23176
  input.removeProcessListener("SIGINT", input.signalHandler);
@@ -23782,12 +24192,12 @@ var init_streaming_prompt = __esm(() => {
23782
24192
 
23783
24193
  // src/transport/acpx-cli/node-pty-helper.ts
23784
24194
  import { chmod as chmodFs } from "node:fs/promises";
23785
- import { dirname as dirname10, join as join12 } from "node:path";
24195
+ import { dirname as dirname12, join as join12 } from "node:path";
23786
24196
  function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
23787
24197
  if (platform === "win32") {
23788
24198
  return null;
23789
24199
  }
23790
- return join12(dirname10(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
24200
+ return join12(dirname12(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
23791
24201
  }
23792
24202
  async function ensureNodePtyHelperExecutable(helperPath, chmod2 = chmodFs) {
23793
24203
  if (!helperPath) {
@@ -24622,7 +25032,7 @@ __export(exports_main, {
24622
25032
  });
24623
25033
  import { randomUUID as randomUUID3 } from "node:crypto";
24624
25034
  import { homedir as homedir9 } from "node:os";
24625
- import { dirname as dirname11, join as join14 } from "node:path";
25035
+ import { dirname as dirname13, join as join14 } from "node:path";
24626
25036
  import { fileURLToPath as fileURLToPath4 } from "node:url";
24627
25037
  function startProgressHeartbeat(orchestration, config2, logger2, channel) {
24628
25038
  const thresholdSeconds = config2.orchestration.progressHeartbeatSeconds;
@@ -24669,6 +25079,15 @@ async function buildApp(paths, deps = {}) {
24669
25079
  now: deps.loggerNow
24670
25080
  });
24671
25081
  await logger2.cleanup();
25082
+ const perfLogPath = paths.perfLogPath ?? resolvePerfLogPath(paths.configPath);
25083
+ const perfTracer = config2.logging.perf.enabled ? createPerfTracer({
25084
+ filePath: perfLogPath,
25085
+ maxSizeBytes: config2.logging.perf.maxSizeBytes,
25086
+ maxFiles: config2.logging.perf.maxFiles,
25087
+ retentionDays: config2.logging.perf.retentionDays,
25088
+ appLogger: logger2
25089
+ }) : createNoopPerfTracer();
25090
+ await perfTracer.cleanup();
24672
25091
  const acpxCommand = resolveAcpxCommand({ configuredCommand: config2.transport.command });
24673
25092
  const stateStore = new StateStore(paths.statePath);
24674
25093
  const state = await stateStore.load();
@@ -25027,6 +25446,7 @@ async function buildApp(paths, deps = {}) {
25027
25446
  stateStore,
25028
25447
  configStore,
25029
25448
  logger: logger2,
25449
+ perfTracer,
25030
25450
  quota,
25031
25451
  transport,
25032
25452
  orchestration: {
@@ -25043,6 +25463,13 @@ async function buildApp(paths, deps = {}) {
25043
25463
  if ("dispose" in transport && typeof transport.dispose === "function") {
25044
25464
  await transport.dispose();
25045
25465
  }
25466
+ try {
25467
+ await perfTracer.flush();
25468
+ } catch (err) {
25469
+ await logger2.error("perf.flush_failed", "perf tracer flush failed during shutdown", {
25470
+ error: err instanceof Error ? err.message : String(err)
25471
+ }).catch(() => {});
25472
+ }
25046
25473
  await logger2.flush();
25047
25474
  }
25048
25475
  };
@@ -25078,7 +25505,7 @@ async function main() {
25078
25505
  }
25079
25506
  }
25080
25507
  async function prepareChannelMedia(configPath, config2) {
25081
- const runtimeDir = join14(dirname11(configPath), "runtime");
25508
+ const runtimeDir = join14(dirname13(configPath), "runtime");
25082
25509
  const mediaRootDir = join14(runtimeDir, "media");
25083
25510
  const mediaStore = new RuntimeMediaStore({ rootDir: mediaRootDir });
25084
25511
  await mediaStore.cleanupExpired().catch((error2) => {
@@ -25093,10 +25520,11 @@ function resolveRuntimePaths() {
25093
25520
  throw new Error("Unable to resolve the current user home directory");
25094
25521
  }
25095
25522
  const configPath = process.env.WEACPX_CONFIG ?? `${home}/.weacpx/config.json`;
25096
- const runtimeDir = join14(dirname11(configPath), "runtime");
25523
+ const runtimeDir = join14(dirname13(configPath), "runtime");
25097
25524
  return {
25098
25525
  configPath,
25099
25526
  statePath: process.env.WEACPX_STATE ?? `${home}/.weacpx/state.json`,
25527
+ perfLogPath: join14(runtimeDir, "perf.log"),
25100
25528
  orchestrationSocketPath: process.env.WEACPX_ORCHESTRATION_SOCKET ?? resolveDaemonOrchestrationSocketPath(runtimeDir)
25101
25529
  };
25102
25530
  }
@@ -25107,10 +25535,15 @@ function resolveBridgeEntryPath() {
25107
25535
  return fileURLToPath4(new URL("./bridge/bridge-main.ts", import.meta.url));
25108
25536
  }
25109
25537
  function resolveAppLogPath(configPath) {
25110
- const rootDir = dirname11(configPath);
25538
+ const rootDir = dirname13(configPath);
25111
25539
  const runtimeDir = join14(rootDir, "runtime");
25112
25540
  return join14(runtimeDir, "app.log");
25113
25541
  }
25542
+ function resolvePerfLogPath(configPath) {
25543
+ const rootDir = dirname13(configPath);
25544
+ const runtimeDir = join14(rootDir, "runtime");
25545
+ return join14(runtimeDir, "perf.log");
25546
+ }
25114
25547
  function resolveOrchestrationSocketPathFromConfigPath(configPath) {
25115
25548
  const runtimeDir = resolveRuntimeDirFromConfigPath(configPath);
25116
25549
  return resolveDaemonOrchestrationSocketPath(runtimeDir);
@@ -25141,6 +25574,7 @@ var init_main = __esm(async () => {
25141
25574
  init_media_store();
25142
25575
  init_quota_errors();
25143
25576
  init_inbound();
25577
+ init_perf_tracer();
25144
25578
  init_bootstrap();
25145
25579
  if (false) {}
25146
25580
  });
@@ -25483,7 +25917,7 @@ async function checkOrchestrationHealth(options) {
25483
25917
  // src/doctor/checks/runtime-check.ts
25484
25918
  import { constants } from "node:fs";
25485
25919
  import { access as access4, stat as stat3 } from "node:fs/promises";
25486
- import { dirname as dirname12 } from "node:path";
25920
+ import { dirname as dirname14 } from "node:path";
25487
25921
  import { homedir as homedir11 } from "node:os";
25488
25922
  async function checkRuntime(options = {}) {
25489
25923
  const home = options.home ?? process.env.HOME ?? homedir11();
@@ -25580,7 +26014,7 @@ async function checkFileCreatable(label, path14, probe, platform) {
25580
26014
  detail: `${label}: ${path14} (unusable: ${formatError6(error2)})`
25581
26015
  };
25582
26016
  }
25583
- const parentCheck = await checkCreatableAncestorDirectory(dirname12(path14), probe, platform);
26017
+ const parentCheck = await checkCreatableAncestorDirectory(dirname14(path14), probe, platform);
25584
26018
  if (!parentCheck.ok) {
25585
26019
  return {
25586
26020
  ok: false,
@@ -25616,7 +26050,7 @@ async function checkCreatableAncestorDirectory(path14, probe, platform) {
25616
26050
  blockingPath: path14
25617
26051
  };
25618
26052
  }
25619
- const parent = dirname12(path14);
26053
+ const parent = dirname14(path14);
25620
26054
  if (parent === path14) {
25621
26055
  return {
25622
26056
  ok: false,
@@ -26198,7 +26632,7 @@ init_create_daemon_controller();
26198
26632
  init_daemon_files();
26199
26633
  import { randomUUID as randomUUID4 } from "node:crypto";
26200
26634
  import { homedir as homedir13 } from "node:os";
26201
- import { dirname as dirname13, join as join16, sep } from "node:path";
26635
+ import { dirname as dirname15, join as join16, sep } from "node:path";
26202
26636
  import { fileURLToPath as fileURLToPath6 } from "node:url";
26203
26637
 
26204
26638
  // src/daemon/daemon-runtime.ts
@@ -39743,7 +40177,7 @@ function sanitizeName(input, fallback) {
39743
40177
  init_plugin_home();
39744
40178
  import { spawn as spawn4 } from "node:child_process";
39745
40179
  import { readFile as readFile7 } from "node:fs/promises";
39746
- import { dirname as dirname7, join as join6 } from "node:path";
40180
+ import { dirname as dirname9, join as join7 } from "node:path";
39747
40181
  import { fileURLToPath as fileURLToPath2 } from "node:url";
39748
40182
 
39749
40183
  // src/plugins/package-manager.ts
@@ -40021,8 +40455,8 @@ async function runInherit(command, args) {
40021
40455
  }
40022
40456
  async function readPackageName() {
40023
40457
  try {
40024
- const here = dirname7(fileURLToPath2(import.meta.url));
40025
- for (const candidate of [join6(here, "..", "package.json"), join6(here, "..", "..", "package.json")]) {
40458
+ const here = dirname9(fileURLToPath2(import.meta.url));
40459
+ for (const candidate of [join7(here, "..", "package.json"), join7(here, "..", "..", "package.json")]) {
40026
40460
  try {
40027
40461
  const parsed = JSON.parse(await readFile7(candidate, "utf8"));
40028
40462
  if (typeof parsed.name === "string" && parsed.name.trim())
@@ -40648,7 +41082,7 @@ async function setChannelAccountEnabled(type, accountId, enabled, rawArgs, deps)
40648
41082
  // src/plugins/plugin-cli.ts
40649
41083
  init_plugin_home();
40650
41084
  import { readFile as readFile9 } from "node:fs/promises";
40651
- import { isAbsolute, join as join8, resolve } from "node:path";
41085
+ import { isAbsolute, join as join9, resolve } from "node:path";
40652
41086
  init_plugin_loader();
40653
41087
  init_validate_plugin();
40654
41088
 
@@ -40658,13 +41092,13 @@ init_plugin_loader();
40658
41092
  init_validate_plugin();
40659
41093
  init_known_plugins();
40660
41094
  import { readFile as readFile8 } from "node:fs/promises";
40661
- import { join as join7 } from "node:path";
41095
+ import { join as join8 } from "node:path";
40662
41096
  function suggestedPluginPackageForChannel(type) {
40663
41097
  return findKnownPluginByChannel(type)?.packageName ?? `<npm-package-that-provides-${type}>`;
40664
41098
  }
40665
41099
  async function readDependencyEntries(pluginHome) {
40666
41100
  try {
40667
- const raw = await readFile8(join7(pluginHome, "package.json"), "utf8");
41101
+ const raw = await readFile8(join8(pluginHome, "package.json"), "utf8");
40668
41102
  const parsed = JSON.parse(raw);
40669
41103
  const out = {};
40670
41104
  for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
@@ -40766,7 +41200,7 @@ function looksLikePath(spec) {
40766
41200
  }
40767
41201
  async function readDependencyEntries2(pluginHome) {
40768
41202
  try {
40769
- const raw = await readFile9(join8(pluginHome, "package.json"), "utf8");
41203
+ const raw = await readFile9(join9(pluginHome, "package.json"), "utf8");
40770
41204
  const parsed = JSON.parse(raw);
40771
41205
  const out = {};
40772
41206
  for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
@@ -40792,7 +41226,7 @@ async function resolveLocalPluginName(installSpec, pluginHome, namesBeforeInstal
40792
41226
  return name;
40793
41227
  }
40794
41228
  try {
40795
- const raw = await readFile9(join8(installSpec, "package.json"), "utf8");
41229
+ const raw = await readFile9(join9(installSpec, "package.json"), "utf8");
40796
41230
  const parsed = JSON.parse(raw);
40797
41231
  if (typeof parsed.name === "string" && parsed.name.trim())
40798
41232
  return parsed.name.trim();
@@ -41741,6 +42175,7 @@ async function defaultLoadConfiguredPluginsForChannelCli() {
41741
42175
  const { loadConfiguredPlugins: loadConfiguredPlugins2 } = await Promise.resolve().then(() => (init_plugin_loader(), exports_plugin_loader));
41742
42176
  await loadConfiguredPlugins2({ plugins: config2.plugins });
41743
42177
  }
42178
+ var DAEMON_RUN_ENV = "WEACPX_DAEMON_RUN";
41744
42179
  async function defaultRun(options = {}) {
41745
42180
  const [{ buildApp: buildApp2, resolveRuntimePaths: resolveRuntimePaths2, prepareChannelMedia: prepareChannelMedia2 }, { runConsole: runConsole2 }] = await Promise.all([
41746
42181
  init_main().then(() => exports_main),
@@ -41769,6 +42204,7 @@ async function defaultRun(options = {}) {
41769
42204
  await createFirstRunSession(runtime, firstRunOnboarding);
41770
42205
  } : undefined,
41771
42206
  channels: channelRegistry,
42207
+ channelStartupPolicy: process.env[DAEMON_RUN_ENV] === "1" ? "best-effort" : "require-one",
41772
42208
  daemonRuntime,
41773
42209
  ...firstLockCreator ? {
41774
42210
  consumerLockFactory: (runtime) => firstLockCreator.create({
@@ -42063,7 +42499,7 @@ function safeDaemonLogPaths() {
42063
42499
  const configPath = process.env.WEACPX_CONFIG ?? `${requireHome2()}/.weacpx/config.json`;
42064
42500
  const paths = resolveDaemonPaths({ home: requireHome2() });
42065
42501
  return {
42066
- appLog: join16(dirname13(configPath), "runtime", "app.log"),
42502
+ appLog: join16(dirname15(configPath), "runtime", "app.log"),
42067
42503
  stderrLog: paths.stderrLog
42068
42504
  };
42069
42505
  } catch {