weacpx 0.4.3 → 0.4.5

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",
@@ -2567,6 +2611,82 @@ var init_ensure_config = __esm(() => {
2567
2611
  };
2568
2612
  });
2569
2613
 
2614
+ // src/config/agent-templates.ts
2615
+ function getAgentTemplate(name) {
2616
+ const template = TEMPLATES[name];
2617
+ if (!template) {
2618
+ return null;
2619
+ }
2620
+ return {
2621
+ ...template
2622
+ };
2623
+ }
2624
+ function listAgentTemplates() {
2625
+ return Object.keys(TEMPLATES);
2626
+ }
2627
+ function sameAgentConfig(left, right) {
2628
+ return left.driver === right.driver && (left.command ?? "") === (right.command ?? "");
2629
+ }
2630
+ var TEMPLATES;
2631
+ var init_agent_templates = __esm(() => {
2632
+ TEMPLATES = {
2633
+ codex: {
2634
+ driver: "codex"
2635
+ },
2636
+ claude: {
2637
+ driver: "claude"
2638
+ },
2639
+ pi: {
2640
+ driver: "pi"
2641
+ },
2642
+ openclaw: {
2643
+ driver: "openclaw"
2644
+ },
2645
+ gemini: {
2646
+ driver: "gemini"
2647
+ },
2648
+ cursor: {
2649
+ driver: "cursor"
2650
+ },
2651
+ copilot: {
2652
+ driver: "copilot"
2653
+ },
2654
+ droid: {
2655
+ driver: "droid"
2656
+ },
2657
+ "factory-droid": {
2658
+ driver: "factory-droid"
2659
+ },
2660
+ factorydroid: {
2661
+ driver: "factorydroid"
2662
+ },
2663
+ iflow: {
2664
+ driver: "iflow"
2665
+ },
2666
+ kilocode: {
2667
+ driver: "kilocode"
2668
+ },
2669
+ kimi: {
2670
+ driver: "kimi"
2671
+ },
2672
+ kiro: {
2673
+ driver: "kiro"
2674
+ },
2675
+ opencode: {
2676
+ driver: "opencode"
2677
+ },
2678
+ qoder: {
2679
+ driver: "qoder"
2680
+ },
2681
+ qwen: {
2682
+ driver: "qwen"
2683
+ },
2684
+ trae: {
2685
+ driver: "trae"
2686
+ }
2687
+ };
2688
+ });
2689
+
2570
2690
  // src/daemon/daemon-status.ts
2571
2691
  import { mkdir as mkdir2, readFile as readFile3, rm, writeFile as writeFile2 } from "node:fs/promises";
2572
2692
  import { dirname as dirname2 } from "node:path";
@@ -2837,6 +2957,7 @@ function buildSpawnRequest(paths, options, stdoutFd, stderrFd, spawnOptions = {}
2837
2957
  WEACPX_DAEMON_ARG0: options.cliEntryPath,
2838
2958
  WEACPX_DAEMON_ARG1: "run",
2839
2959
  WEACPX_DAEMON_CWD: options.cwd,
2960
+ WEACPX_DAEMON_RUN: "1",
2840
2961
  WEACPX_DAEMON_STDOUT: paths.stdoutLog,
2841
2962
  WEACPX_DAEMON_STDERR: paths.stderrLog,
2842
2963
  ...spawnOptions.firstRunOnboarding ? { WEACPX_FIRST_RUN_ONBOARDING: spawnOptions.firstRunOnboarding } : {}
@@ -2855,6 +2976,7 @@ function buildSpawnRequest(paths, options, stdoutFd, stderrFd, spawnOptions = {}
2855
2976
  detached: true,
2856
2977
  env: {
2857
2978
  ...options.env,
2979
+ WEACPX_DAEMON_RUN: "1",
2858
2980
  ...spawnOptions.firstRunOnboarding ? { WEACPX_FIRST_RUN_ONBOARDING: spawnOptions.firstRunOnboarding } : {}
2859
2981
  },
2860
2982
  stdio: ["ignore", stdoutFd, stderrFd]
@@ -2863,6 +2985,7 @@ function buildSpawnRequest(paths, options, stdoutFd, stderrFd, spawnOptions = {}
2863
2985
  }
2864
2986
  function buildWindowsLauncherScript() {
2865
2987
  const script = [
2988
+ "$env:WEACPX_DAEMON_RUN = '1'",
2866
2989
  "$process = Start-Process -FilePath $env:WEACPX_DAEMON_COMMAND `",
2867
2990
  " -ArgumentList @($env:WEACPX_DAEMON_ARG0, $env:WEACPX_DAEMON_ARG1) `",
2868
2991
  " -WorkingDirectory $env:WEACPX_DAEMON_CWD `",
@@ -9495,10 +9618,11 @@ var PACKAGE_NAME = "weacpx";
9495
9618
  var init_version = () => {};
9496
9619
 
9497
9620
  // src/orchestration/task-wait-timeouts.ts
9498
- var DEFAULT_TASK_WAIT_TIMEOUT_MS, MAX_TASK_WAIT_TIMEOUT_MS, DEFAULT_TASK_WAIT_POLL_INTERVAL_MS = 1000, MAX_TASK_WAIT_POLL_INTERVAL_MS = 1e4, TASK_WAIT_RPC_TIMEOUT_PADDING_MS = 5000;
9621
+ var DEFAULT_TASK_WAIT_TIMEOUT_MS, MAX_TASK_WAIT_TIMEOUT_MS, DEFAULT_TASK_WAIT_POLL_INTERVAL_MS = 1000, MAX_TASK_WAIT_POLL_INTERVAL_MS = 1e4, TASK_WAIT_RPC_TIMEOUT_PADDING_MS = 5000, DEFAULT_TASK_WATCH_TIMEOUT_MS = 60000, MAX_TASK_WATCH_TIMEOUT_MS, DEFAULT_TASK_WATCH_POLL_INTERVAL_MS = 1000, MAX_TASK_WATCH_POLL_INTERVAL_MS = 1e4, TASK_WATCH_RPC_TIMEOUT_PADDING_MS = 5000;
9499
9622
  var init_task_wait_timeouts = __esm(() => {
9500
9623
  DEFAULT_TASK_WAIT_TIMEOUT_MS = 5 * 60000;
9501
9624
  MAX_TASK_WAIT_TIMEOUT_MS = 20 * 60000;
9625
+ MAX_TASK_WATCH_TIMEOUT_MS = 20 * 60000;
9502
9626
  });
9503
9627
 
9504
9628
  // src/weixin/messaging/quota-errors.ts
@@ -9560,6 +9684,15 @@ function isTaskStatus(value) {
9560
9684
  function isSourceKind(value) {
9561
9685
  return value === "human" || value === "coordinator" || value === "worker";
9562
9686
  }
9687
+ function isOptionalNumber(value) {
9688
+ return value === undefined || typeof value === "number";
9689
+ }
9690
+ function isTaskEventRecord(value) {
9691
+ if (!isRecord2(value)) {
9692
+ return false;
9693
+ }
9694
+ return typeof value.seq === "number" && isString(value.at) && (value.type === "created" || value.type === "progress" || value.type === "status_changed" || value.type === "attention_required" || value.type === "cancel_requested") && (value.status === undefined || isTaskStatus(value.status)) && isOptionalString(value.summary) && isOptionalString(value.message);
9695
+ }
9563
9696
  function isOpenQuestionRecord(value) {
9564
9697
  if (!isRecord2(value)) {
9565
9698
  return false;
@@ -9582,7 +9715,7 @@ function isTaskRecord(value) {
9582
9715
  if (!isRecord2(value)) {
9583
9716
  return false;
9584
9717
  }
9585
- return isString(value.taskId) && isString(value.sourceHandle) && isSourceKind(value.sourceKind) && isString(value.coordinatorSession) && isOptionalString(value.workerSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role) && isString(value.task) && isTaskStatus(value.status) && isString(value.summary) && isString(value.resultText) && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.chatKey) && isOptionalString(value.replyContextToken) && isOptionalString(value.accountId) && isOptionalString(value.deliveryAccountId) && isOptionalString(value.coordinatorInjectedAt) && isOptionalString(value.cancelRequestedAt) && isOptionalString(value.cancelCompletedAt) && isOptionalString(value.lastCancelError) && isOptionalBoolean(value.noticePending) && isOptionalString(value.noticeSentAt) && isOptionalString(value.lastNoticeError) && isOptionalBoolean(value.injectionPending) && isOptionalString(value.injectionAppliedAt) && isOptionalString(value.lastInjectionError) && isOptionalString(value.lastProgressAt) && isOptionalString(value.groupId) && (value.openQuestion === undefined || isOpenQuestionRecord(value.openQuestion)) && (value.reviewPending === undefined || isReviewPendingRecord(value.reviewPending)) && (value.correctionPending === undefined || isCorrectionPendingRecord(value.correctionPending));
9718
+ return isString(value.taskId) && isString(value.sourceHandle) && isSourceKind(value.sourceKind) && isString(value.coordinatorSession) && isOptionalString(value.workerSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role) && isString(value.task) && isTaskStatus(value.status) && isString(value.summary) && isString(value.resultText) && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.chatKey) && isOptionalString(value.replyContextToken) && isOptionalString(value.accountId) && isOptionalString(value.deliveryAccountId) && isOptionalString(value.coordinatorInjectedAt) && isOptionalString(value.cancelRequestedAt) && isOptionalString(value.cancelCompletedAt) && isOptionalString(value.lastCancelError) && isOptionalBoolean(value.noticePending) && isOptionalString(value.noticeSentAt) && isOptionalString(value.lastNoticeError) && isOptionalBoolean(value.injectionPending) && isOptionalString(value.injectionAppliedAt) && isOptionalString(value.lastInjectionError) && isOptionalString(value.lastProgressAt) && isOptionalString(value.lastProgressSummary) && isOptionalString(value.groupId) && (value.openQuestion === undefined || isOpenQuestionRecord(value.openQuestion)) && (value.reviewPending === undefined || isReviewPendingRecord(value.reviewPending)) && (value.correctionPending === undefined || isCorrectionPendingRecord(value.correctionPending)) && isOptionalNumber(value.eventSeq) && (value.events === undefined || Array.isArray(value.events) && value.events.every(isTaskEventRecord));
9586
9719
  }
9587
9720
  function isExternalCoordinatorRecord(value) {
9588
9721
  if (!isRecord2(value)) {
@@ -9846,37 +9979,6 @@ var init_state_store = __esm(() => {
9846
9979
  init_types();
9847
9980
  });
9848
9981
 
9849
- // src/config/agent-templates.ts
9850
- function getAgentTemplate(name) {
9851
- const template = TEMPLATES[name];
9852
- if (!template) {
9853
- return null;
9854
- }
9855
- return {
9856
- ...template
9857
- };
9858
- }
9859
- function listAgentTemplates() {
9860
- return Object.keys(TEMPLATES);
9861
- }
9862
- var TEMPLATES;
9863
- var init_agent_templates = __esm(() => {
9864
- TEMPLATES = {
9865
- codex: {
9866
- driver: "codex"
9867
- },
9868
- claude: {
9869
- driver: "claude"
9870
- },
9871
- opencode: {
9872
- driver: "opencode"
9873
- },
9874
- gemini: {
9875
- driver: "gemini"
9876
- }
9877
- };
9878
- });
9879
-
9880
9982
  // src/plugins/plugin-home.ts
9881
9983
  import { mkdir as mkdir6, writeFile as writeFile5 } from "node:fs/promises";
9882
9984
  import { homedir as homedir3 } from "node:os";
@@ -13147,6 +13249,296 @@ function normalizeMediaArray(media) {
13147
13249
  return Array.isArray(media) ? media : [media];
13148
13250
  }
13149
13251
 
13252
+ // src/logging/rotating-file-writer.ts
13253
+ import { readdir as readdir2, rename, rm as rm5, stat as stat2 } from "node:fs/promises";
13254
+ import { basename, dirname as dirname6, join as join4 } from "node:path";
13255
+ async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
13256
+ let currentSize = 0;
13257
+ try {
13258
+ currentSize = (await stat2(filePath)).size;
13259
+ } catch (error2) {
13260
+ if (!isMissingFileError2(error2)) {
13261
+ throw error2;
13262
+ }
13263
+ }
13264
+ if (currentSize + incomingSize <= maxSizeBytes) {
13265
+ return;
13266
+ }
13267
+ if (currentSize === 0) {
13268
+ return;
13269
+ }
13270
+ if (maxFiles <= 0) {
13271
+ await rm5(filePath, { force: true });
13272
+ return;
13273
+ }
13274
+ await rm5(`${filePath}.${maxFiles}`, { force: true });
13275
+ for (let index = maxFiles - 1;index >= 1; index -= 1) {
13276
+ const source = `${filePath}.${index}`;
13277
+ try {
13278
+ await rename(source, `${filePath}.${index + 1}`);
13279
+ } catch (error2) {
13280
+ if (!isMissingFileError2(error2)) {
13281
+ throw error2;
13282
+ }
13283
+ }
13284
+ }
13285
+ await rename(filePath, `${filePath}.1`);
13286
+ }
13287
+ async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
13288
+ const parentDir = dirname6(filePath);
13289
+ const prefix = `${basename(filePath)}.`;
13290
+ const cutoff = now().getTime() - retentionDays * 24 * 60 * 60 * 1000;
13291
+ let files = [];
13292
+ try {
13293
+ files = await readdir2(parentDir);
13294
+ } catch (error2) {
13295
+ if (isMissingFileError2(error2)) {
13296
+ return;
13297
+ }
13298
+ throw error2;
13299
+ }
13300
+ for (const file of files) {
13301
+ if (!file.startsWith(prefix) || !/^\d+$/.test(file.slice(prefix.length))) {
13302
+ continue;
13303
+ }
13304
+ const candidate = join4(parentDir, file);
13305
+ const details = await stat2(candidate);
13306
+ if (details.mtime.getTime() < cutoff) {
13307
+ await rm5(candidate, { force: true });
13308
+ }
13309
+ }
13310
+ }
13311
+ function isMissingFileError2(error2) {
13312
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
13313
+ }
13314
+ var init_rotating_file_writer = () => {};
13315
+
13316
+ // src/perf/perf-log-writer.ts
13317
+ import { appendFile as fsAppendFile, mkdir as fsMkdir } from "node:fs/promises";
13318
+ import { dirname as dirname7 } from "node:path";
13319
+ function createPerfLogWriter(options) {
13320
+ const append = options.appendImpl ?? ((p, d) => fsAppendFile(p, d, "utf8"));
13321
+ const mkdir8 = options.mkdirImpl ?? ((p, o) => fsMkdir(p, o).then(() => {
13322
+ return;
13323
+ }));
13324
+ const now = options.now ?? (() => new Date);
13325
+ const threshold = options.failureThreshold ?? 5;
13326
+ let pending = [];
13327
+ let writeChain = Promise.resolve();
13328
+ let consecutiveFailures = 0;
13329
+ let disabled = false;
13330
+ let notified = false;
13331
+ const writer = {
13332
+ enqueue(line) {
13333
+ if (disabled)
13334
+ return;
13335
+ pending.push(line);
13336
+ scheduleDrain();
13337
+ },
13338
+ async flush() {
13339
+ await scheduleDrain();
13340
+ await writeChain;
13341
+ },
13342
+ async cleanup() {
13343
+ if (disabled)
13344
+ return;
13345
+ try {
13346
+ await cleanupExpiredRotatedLogs(options.filePath, options.retentionDays ?? 7, now);
13347
+ } catch {}
13348
+ },
13349
+ isDisabled() {
13350
+ return disabled;
13351
+ }
13352
+ };
13353
+ return writer;
13354
+ function scheduleDrain() {
13355
+ if (disabled || pending.length === 0) {
13356
+ return writeChain;
13357
+ }
13358
+ const batch = pending;
13359
+ pending = [];
13360
+ writeChain = writeChain.catch(() => {}).then(() => drainBatch(batch));
13361
+ return writeChain;
13362
+ }
13363
+ async function drainBatch(batch) {
13364
+ if (disabled)
13365
+ return;
13366
+ const data = batch.join("");
13367
+ try {
13368
+ await mkdir8(dirname7(options.filePath), { recursive: true });
13369
+ await rotateIfNeeded(options.filePath, Buffer.byteLength(data), options.maxSizeBytes, options.maxFiles);
13370
+ await append(options.filePath, data);
13371
+ consecutiveFailures = 0;
13372
+ } catch (err) {
13373
+ consecutiveFailures += 1;
13374
+ if (consecutiveFailures >= threshold) {
13375
+ disabled = true;
13376
+ pending = [];
13377
+ if (!notified) {
13378
+ notified = true;
13379
+ options.onPermanentFailure({
13380
+ perfLogPath: options.filePath,
13381
+ failureCount: consecutiveFailures,
13382
+ lastError: err instanceof Error ? err.message : String(err)
13383
+ });
13384
+ }
13385
+ }
13386
+ }
13387
+ }
13388
+ }
13389
+ var init_perf_log_writer = __esm(() => {
13390
+ init_rotating_file_writer();
13391
+ });
13392
+
13393
+ // src/perf/perf-tracer.ts
13394
+ import { randomBytes } from "node:crypto";
13395
+ function createNoopPerfTracer() {
13396
+ return {
13397
+ async wrapTurn(_seed, run) {
13398
+ return run(NOOP_SPAN);
13399
+ },
13400
+ async flush() {},
13401
+ async cleanup() {}
13402
+ };
13403
+ }
13404
+ function createPerfTracer(options) {
13405
+ const now = options.now ?? (() => performance.now());
13406
+ const isoNow = options.isoNow ?? (() => new Date);
13407
+ const randomId = options.randomId ?? defaultRandomId;
13408
+ const formatLine = options.formatLine ?? defaultFormatLine;
13409
+ const formatSummary = options.formatSummaryLine ?? defaultFormatSummaryLine;
13410
+ let disabled = false;
13411
+ const writer = createPerfLogWriter({
13412
+ filePath: options.filePath,
13413
+ maxSizeBytes: options.maxSizeBytes,
13414
+ maxFiles: options.maxFiles,
13415
+ retentionDays: options.retentionDays,
13416
+ onPermanentFailure: (info) => {
13417
+ disabled = true;
13418
+ options.appLogger.error("perf.disabled_due_to_io_error", "perf logging disabled after repeated IO failures", {
13419
+ perfLogPath: info.perfLogPath,
13420
+ failureCount: info.failureCount,
13421
+ lastError: info.lastError
13422
+ }).catch(() => {});
13423
+ }
13424
+ });
13425
+ return {
13426
+ async wrapTurn(seed, run) {
13427
+ if (disabled) {
13428
+ return run(NOOP_SPAN);
13429
+ }
13430
+ const traceId = randomId();
13431
+ let startTime;
13432
+ const marks = [];
13433
+ let lastMarkTime;
13434
+ let explicitOutcome;
13435
+ let outcomeContext;
13436
+ const span = {
13437
+ traceId,
13438
+ mark(event, context) {
13439
+ if (disabled)
13440
+ return;
13441
+ try {
13442
+ const t = now();
13443
+ if (startTime === undefined) {
13444
+ startTime = t;
13445
+ lastMarkTime = t;
13446
+ }
13447
+ const since = t - startTime;
13448
+ const sinceLast = t - lastMarkTime;
13449
+ lastMarkTime = t;
13450
+ marks.push({ e: event, t: Math.round(since) });
13451
+ const line = formatLine({
13452
+ isoNow: isoNow(),
13453
+ event,
13454
+ traceId,
13455
+ chatKey: seed.chatKey,
13456
+ sinceStartMs: Math.round(since),
13457
+ sinceLastMs: Math.round(sinceLast),
13458
+ context
13459
+ });
13460
+ writer.enqueue(line);
13461
+ } catch {}
13462
+ },
13463
+ setOutcome(outcome, context) {
13464
+ explicitOutcome = outcome;
13465
+ outcomeContext = context;
13466
+ }
13467
+ };
13468
+ let thrown;
13469
+ try {
13470
+ return await run(span);
13471
+ } catch (err) {
13472
+ thrown = err;
13473
+ throw err;
13474
+ } finally {
13475
+ try {
13476
+ if (!disabled) {
13477
+ let outcome;
13478
+ if (explicitOutcome !== undefined) {
13479
+ outcome = explicitOutcome;
13480
+ } else if (thrown !== undefined) {
13481
+ outcome = "error";
13482
+ } else {
13483
+ outcome = "ok";
13484
+ }
13485
+ const t = now();
13486
+ const effectiveStart = startTime ?? t;
13487
+ const summary = formatSummary({
13488
+ isoNow: isoNow(),
13489
+ traceId,
13490
+ chatKey: seed.chatKey,
13491
+ kind: seed.kind,
13492
+ outcome,
13493
+ totalMs: Math.round(t - effectiveStart),
13494
+ marks,
13495
+ outcomeContext
13496
+ });
13497
+ writer.enqueue(summary);
13498
+ }
13499
+ } catch {}
13500
+ }
13501
+ },
13502
+ async flush() {
13503
+ await writer.flush();
13504
+ },
13505
+ async cleanup() {
13506
+ await writer.cleanup();
13507
+ }
13508
+ };
13509
+ }
13510
+ function defaultRandomId() {
13511
+ return randomBytes(6).toString("hex");
13512
+ }
13513
+ function defaultFormatLine(args) {
13514
+ const ctxFields = args.context ? Object.entries(args.context).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ") : "";
13515
+ const ctxPrefix = ctxFields ? ` ${ctxFields}` : "";
13516
+ return `${args.isoNow.toISOString()} PERF ${args.event} trace=${args.traceId} chatKey=${formatValue(args.chatKey)}${ctxPrefix} sinceStartMs=${args.sinceStartMs} sinceLastMs=${args.sinceLastMs}
13517
+ `;
13518
+ }
13519
+ function defaultFormatSummaryLine(args) {
13520
+ const extra = args.outcomeContext ? " " + Object.entries(args.outcomeContext).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}=${formatValue(v)}`).join(" ") : "";
13521
+ const marksJson = JSON.stringify(args.marks);
13522
+ 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)}
13523
+ `;
13524
+ }
13525
+ function formatValue(value) {
13526
+ if (value === null)
13527
+ return "null";
13528
+ if (typeof value === "number" || typeof value === "boolean")
13529
+ return String(value);
13530
+ return JSON.stringify(value);
13531
+ }
13532
+ var NOOP_SPAN;
13533
+ var init_perf_tracer = __esm(() => {
13534
+ init_perf_log_writer();
13535
+ NOOP_SPAN = {
13536
+ traceId: "-",
13537
+ mark: () => {},
13538
+ setOutcome: () => {}
13539
+ };
13540
+ });
13541
+
13150
13542
  // src/weixin/messaging/handle-weixin-message-turn.ts
13151
13543
  import crypto4 from "node:crypto";
13152
13544
  import fs8 from "node:fs/promises";
@@ -13278,6 +13670,9 @@ function isClearSlashCommand(textBody) {
13278
13670
  const command = spaceIdx === -1 ? trimmed.toLowerCase() : trimmed.slice(0, spaceIdx).toLowerCase();
13279
13671
  return command === "/clear";
13280
13672
  }
13673
+ function isSlashCommandText(textBody) {
13674
+ return textBody.startsWith("/");
13675
+ }
13281
13676
  function getWeixinMessageTurnLane(full) {
13282
13677
  const textBody = extractTextBody(full.item_list).trim().toLowerCase();
13283
13678
  return textBody === "/cancel" || textBody === "/stop" || textBody === "/jx" ? "control" : "normal";
@@ -13343,230 +13738,300 @@ async function handleWeixinMessageTurn(full, deps) {
13343
13738
  }
13344
13739
  }).catch(() => {});
13345
13740
  };
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 {
13741
+ const chatKey = buildWeixinChatKey(deps.accountId, fromUserId);
13742
+ const initialMediaCount = extractWeixinMediaDescriptors(full.item_list).length;
13743
+ const isSlashCommand = isSlashCommandText(textBody);
13744
+ const tracer = deps.perfTracer ?? createNoopPerfTracer();
13745
+ return await tracer.wrapTurn({ chatKey, kind: isSlashCommand ? "command" : "prompt" }, async (perfSpan) => {
13746
+ perfSpan.mark("turn.received", {
13747
+ textLen: textBody.length,
13748
+ hasMedia: initialMediaCount > 0,
13749
+ mediaCount: initialMediaCount
13750
+ });
13751
+ const contextToken = full.context_token;
13752
+ if (contextToken) {
13753
+ setContextToken(deps.accountId, full.from_user_id ?? "", contextToken);
13754
+ }
13755
+ if (isSlashCommand) {
13756
+ const shouldTypeForSlash = isClearSlashCommand(textBody);
13375
13757
  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;
13758
+ startTypingIndicator();
13399
13759
  }
13400
13760
  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",
13761
+ const slashResult = await handleSlashCommand(textBody, {
13762
+ to,
13763
+ contextToken: full.context_token,
13764
+ baseUrl: deps.baseUrl,
13765
+ token: deps.token,
13405
13766
  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
- }));
13767
+ log: deps.log,
13768
+ errLog: deps.errLog,
13769
+ onClear: () => deps.agent.clearSession?.(chatKey),
13770
+ ...deps.hasPendingFinal ? { hasPendingFinal: deps.hasPendingFinal } : {},
13771
+ ...deps.drainPendingFinal ? { drainPendingFinal: deps.drainPendingFinal } : {},
13772
+ ...deps.prependPendingFinal ? { prependPendingFinal: deps.prependPendingFinal } : {},
13773
+ ...deps.reserveFinal ? { reserveFinal: deps.reserveFinal } : {},
13774
+ ...deps.finalRemaining ? { finalRemaining: deps.finalRemaining } : {}
13775
+ }, receivedAt, full.create_time_ms);
13776
+ if (slashResult.handled)
13777
+ return;
13414
13778
  } finally {
13415
- await fs8.rm(filePath, { force: true }).catch(() => {});
13779
+ if (shouldTypeForSlash) {
13780
+ stopTypingIndicator();
13781
+ }
13416
13782
  }
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
13783
  }
13421
- }
13422
- const sendReplySegment = async (text) => {
13423
- const plainText = markdownToPlainText(text).trim();
13424
- if (plainText.length === 0) {
13425
- return false;
13784
+ startTypingIndicator();
13785
+ const mediaStore = deps.mediaStore ?? new RuntimeMediaStore({ rootDir: resolveMediaTempDir(deps.mediaTempDir) });
13786
+ const media = [];
13787
+ const attachmentNotes = [];
13788
+ const descriptors = extractWeixinMediaDescriptors(full.item_list).slice(0, DEFAULT_MAX_ATTACHMENTS_PER_MESSAGE);
13789
+ const download = deps.downloadMediaFromItemFn ?? downloadMediaFromItem;
13790
+ for (const descriptor of descriptors) {
13791
+ try {
13792
+ const downloaded = await download(descriptor.item, {
13793
+ cdnBaseUrl: deps.cdnBaseUrl,
13794
+ saveMedia: createSaveMediaBuffer(deps.mediaTempDir),
13795
+ log: deps.log,
13796
+ errLog: deps.errLog,
13797
+ label: "inbound"
13798
+ });
13799
+ const filePath = downloaded.decryptedPicPath ?? downloaded.decryptedVideoPath ?? downloaded.decryptedFilePath ?? downloaded.decryptedVoicePath;
13800
+ if (!filePath) {
13801
+ attachmentNotes.push(`Skipped ${descriptor.kind}: media was unavailable.`);
13802
+ continue;
13803
+ }
13804
+ try {
13805
+ const buffer = await fs8.readFile(filePath);
13806
+ const mimeType = downloaded.fileMediaType ?? downloaded.voiceMediaType ?? defaultWeixinMime(descriptor.kind);
13807
+ media.push(await mediaStore.saveMediaBuffer({
13808
+ channelId: "weixin",
13809
+ accountId: deps.accountId,
13810
+ chatKey: buildWeixinChatKey(deps.accountId, full.from_user_id ?? ""),
13811
+ messageId: full.message_id ? String(full.message_id) : full.context_token ?? String(full.create_time_ms ?? Date.now()),
13812
+ fileName: descriptor.fileName,
13813
+ mimeType,
13814
+ kind: descriptor.kind,
13815
+ buffer,
13816
+ maxBytes: descriptor.kind === "image" ? DEFAULT_IMAGE_MAX_BYTES : DEFAULT_ATTACHMENT_MAX_BYTES
13817
+ }));
13818
+ } finally {
13819
+ await fs8.rm(filePath, { force: true }).catch(() => {});
13820
+ }
13821
+ } catch (err) {
13822
+ deps.errLog(`media download failed: ${String(err)}`);
13823
+ attachmentNotes.push(`Skipped ${descriptor.kind}: ${err instanceof Error ? err.message : String(err)}`);
13824
+ }
13426
13825
  }
13826
+ let midFirstSent = false;
13827
+ const sendReplySegment = async (text) => {
13828
+ const plainText = markdownToPlainText(text).trim();
13829
+ if (plainText.length === 0) {
13830
+ return false;
13831
+ }
13832
+ try {
13833
+ await sendMessageWeixin({
13834
+ to,
13835
+ text: plainText,
13836
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
13837
+ });
13838
+ if (!midFirstSent) {
13839
+ midFirstSent = true;
13840
+ perfSpan.mark("reply.mid_first_sent", { bytes: utf8ByteLength(plainText) });
13841
+ }
13842
+ return true;
13843
+ } catch (err) {
13844
+ deps.errLog(`intermediate reply failed: ${String(err)}`);
13845
+ return false;
13846
+ }
13847
+ };
13848
+ const requestText = appendAttachmentNotes(bodyFromItemList(full.item_list), attachmentNotes);
13849
+ const request = {
13850
+ accountId: deps.accountId,
13851
+ conversationId: buildWeixinChatKey(deps.accountId, full.from_user_id ?? ""),
13852
+ text: requestText,
13853
+ ...media.length > 0 ? { media } : {},
13854
+ replyContextToken: contextToken,
13855
+ perfSpan
13856
+ };
13427
13857
  try {
13428
- await sendMessageWeixin({
13429
- to,
13430
- text: plainText,
13431
- opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
13858
+ const turn = await executeChatTurn({
13859
+ agent: deps.agent,
13860
+ request,
13861
+ onReplySegment: sendReplySegment
13432
13862
  });
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) {
13863
+ const outboundMedia = normalizeMediaArray(turn.media);
13864
+ let finalFirstSent = false;
13865
+ let finalChunksSent = 0;
13866
+ let finalChunksPending = 0;
13867
+ let finalDropped = false;
13868
+ if (turn.text) {
13869
+ const finalText = markdownToPlainText(turn.text).trim();
13870
+ if (finalText.length > 0) {
13871
+ const rawChunks = chunkFinalText(finalText, MAX_FINAL_CHUNK_BYTES);
13872
+ if (rawChunks.length > 0) {
13873
+ const total = rawChunks.length;
13874
+ if (total === 1) {
13488
13875
  const reserved = deps.reserveFinal ? deps.reserveFinal(to) : true;
13489
13876
  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 {
13877
+ finalDropped = true;
13878
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=text chatKey=${to}`);
13879
+ } else {
13494
13880
  await sendMessageWeixin({
13495
13881
  to,
13496
- text: wave[i],
13882
+ text: rawChunks[0],
13497
13883
  opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
13498
13884
  });
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;
13885
+ finalChunksSent += 1;
13886
+ if (!finalFirstSent) {
13887
+ finalFirstSent = true;
13888
+ perfSpan.mark("reply.final_first_sent", { bytes: utf8ByteLength(rawChunks[0]), chunkIndex: 1 });
13889
+ }
13890
+ }
13891
+ } else {
13892
+ const prefixed = rawChunks.map((body, i) => `(${i + 1}/${total}) ${body}`);
13893
+ const available = deps.finalRemaining ? deps.finalRemaining(to) : total;
13894
+ const waveSize = Math.max(Math.min(available, total), 0);
13895
+ const wave = prefixed.slice(0, waveSize);
13896
+ const rest = prefixed.slice(waveSize);
13897
+ if (wave.length > 0 && rest.length > 0) {
13898
+ const sentSoFar = wave.length;
13899
+ wave[wave.length - 1] = `${wave[wave.length - 1]}
13900
+
13901
+ ${buildFinalHeadsUp({
13902
+ total,
13903
+ sentSoFar
13904
+ })}`;
13905
+ }
13906
+ let sent = 0;
13907
+ for (let i = 0;i < wave.length; i += 1) {
13908
+ const reserved = deps.reserveFinal ? deps.reserveFinal(to) : true;
13909
+ if (!reserved) {
13910
+ finalDropped = true;
13911
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=text_paginated chatKey=${to} chunk=${i + 1}/${total}`);
13912
+ break;
13913
+ }
13914
+ try {
13915
+ await sendMessageWeixin({
13916
+ to,
13917
+ text: wave[i],
13918
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
13919
+ });
13920
+ sent += 1;
13921
+ finalChunksSent += 1;
13922
+ if (!finalFirstSent) {
13923
+ finalFirstSent = true;
13924
+ perfSpan.mark("reply.final_first_sent", { bytes: utf8ByteLength(wave[i]), chunkIndex: i + 1 });
13925
+ }
13926
+ } catch (sendErr) {
13927
+ finalDropped = true;
13928
+ deps.errLog(`weixin.final.dropped reason=send_failed kind=text_paginated chatKey=${to} chunk=${i + 1}/${total} err=${String(sendErr)}`);
13929
+ break;
13930
+ }
13931
+ }
13932
+ const restToPark = prefixed.slice(sent);
13933
+ finalChunksPending = restToPark.length;
13934
+ if (restToPark.length > 0 && deps.enqueuePendingFinal) {
13935
+ const pending = restToPark.map((text, idx) => {
13936
+ const seq = sent + idx + 1;
13937
+ const entry = { text, seq, total };
13938
+ if (contextToken !== undefined)
13939
+ entry.contextToken = contextToken;
13940
+ if (deps.accountId !== undefined)
13941
+ entry.accountId = deps.accountId;
13942
+ return entry;
13943
+ });
13944
+ deps.enqueuePendingFinal(to, pending);
13503
13945
  }
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
13946
  }
13518
13947
  }
13519
13948
  }
13949
+ perfSpan.mark("reply.final_done", {
13950
+ chunksSent: finalChunksSent,
13951
+ chunksPending: finalChunksPending,
13952
+ dropped: finalDropped
13953
+ });
13520
13954
  }
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;
13955
+ let mediaSent = 0;
13956
+ let mediaFailed = 0;
13957
+ let mediaRejected = 0;
13958
+ let mediaDropped = 0;
13959
+ for (const mediaItem of outboundMedia) {
13960
+ const filePath = await resolveSafeOutboundMediaPath(mediaItem.filePath, [mediaStore.rootDir, resolveMediaTempDir(deps.mediaTempDir), ...deps.allowedMediaRoots ?? []]);
13961
+ if (!filePath) {
13962
+ mediaRejected += 1;
13963
+ deps.errLog(`outbound media rejected: path=${mediaItem.filePath}`);
13964
+ continue;
13965
+ }
13966
+ const caption = mediaItem.caption ? markdownToPlainText(mediaItem.caption) : "";
13967
+ const captionReserve = caption && deps.reserveFinal ? deps.reserveFinal(to) : true;
13968
+ if (!captionReserve) {
13969
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=media_caption chatKey=${to}`);
13970
+ }
13971
+ const reservedMedia = deps.reserveFinal ? deps.reserveFinal(to) : true;
13972
+ if (!reservedMedia) {
13973
+ mediaDropped += 1;
13974
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=media chatKey=${to}`);
13975
+ continue;
13976
+ }
13977
+ try {
13978
+ const sent = await sendWeixinMediaFile({
13979
+ media: mediaItem,
13980
+ filePath,
13981
+ to,
13982
+ text: captionReserve ? caption : "",
13983
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
13984
+ cdnBaseUrl: deps.cdnBaseUrl
13985
+ });
13986
+ mediaSent += 1;
13987
+ perfSpan.mark("reply.media_sent", {
13988
+ kind: mediaItem.kind,
13989
+ index: mediaSent + mediaFailed + mediaRejected + mediaDropped,
13990
+ messageId: sent.messageId
13991
+ });
13992
+ } catch (err) {
13993
+ mediaFailed += 1;
13994
+ deps.errLog(`outbound media send failed: ${String(err)}`);
13995
+ }
13527
13996
  }
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}`);
13997
+ if (outboundMedia.length > 0) {
13998
+ perfSpan.mark("reply.media_done", {
13999
+ mediaCount: outboundMedia.length,
14000
+ sent: mediaSent,
14001
+ failed: mediaFailed,
14002
+ rejected: mediaRejected,
14003
+ dropped: mediaDropped
14004
+ });
13532
14005
  }
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;
14006
+ } catch (err) {
14007
+ if (isAbortError(err)) {
14008
+ perfSpan.setOutcome("aborted", { reason: "user_cancel" });
14009
+ deps.log(`handleWeixinMessageTurn: turn aborted: ${err.message}`);
14010
+ return;
13537
14011
  }
13538
- try {
13539
- await sendWeixinMediaFile({
13540
- media: mediaItem,
13541
- filePath,
14012
+ perfSpan.setOutcome("error", { reason: "turn_error" });
14013
+ const errorText = err instanceof Error ? err.stack ?? err.message : JSON.stringify(err);
14014
+ deps.errLog(`handleWeixinMessageTurn: agent or send failed: ${errorText}`);
14015
+ const reservedErr = deps.reserveFinal ? deps.reserveFinal(to) : true;
14016
+ if (!reservedErr) {
14017
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=error_notice chatKey=${to}`);
14018
+ } else {
14019
+ sendWeixinErrorNotice({
13542
14020
  to,
13543
- text: captionReserve ? caption : "",
13544
- opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
13545
- cdnBaseUrl: deps.cdnBaseUrl
14021
+ contextToken,
14022
+ message: `⚠️ 过程失败:${err instanceof Error ? err.message : JSON.stringify(err)}`,
14023
+ baseUrl: deps.baseUrl,
14024
+ token: deps.token,
14025
+ errLog: deps.errLog
13546
14026
  });
13547
- } catch (err) {
13548
- deps.errLog(`outbound media send failed: ${String(err)}`);
13549
14027
  }
14028
+ } finally {
14029
+ stopTypingIndicator();
13550
14030
  }
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
- }
14031
+ });
14032
+ }
14033
+ function isAbortError(error2) {
14034
+ return error2 instanceof Error && error2.name === "AbortError";
13570
14035
  }
13571
14036
  var MAX_FINAL_CHUNK_BYTES = 1800;
13572
14037
  var init_handle_weixin_message_turn = __esm(() => {
@@ -13581,6 +14046,7 @@ var init_handle_weixin_message_turn = __esm(() => {
13581
14046
  init_send_media();
13582
14047
  init_send();
13583
14048
  init_slash_commands();
14049
+ init_perf_tracer();
13584
14050
  });
13585
14051
 
13586
14052
  // src/weixin/storage/sync-buf.ts
@@ -13769,7 +14235,8 @@ async function monitorWeixinProvider(opts) {
13769
14235
  ...opts.drainPendingFinal ? { drainPendingFinal: opts.drainPendingFinal } : {},
13770
14236
  ...opts.prependPendingFinal ? { prependPendingFinal: opts.prependPendingFinal } : {},
13771
14237
  ...opts.mediaStore ? { mediaStore: opts.mediaStore } : {},
13772
- ...opts.allowedMediaRoots ? { allowedMediaRoots: opts.allowedMediaRoots } : {}
14238
+ ...opts.allowedMediaRoots ? { allowedMediaRoots: opts.allowedMediaRoots } : {},
14239
+ ...opts.perfTracer ? { perfTracer: opts.perfTracer } : {}
13773
14240
  })).catch((err) => {
13774
14241
  errLog(`[weixin] message turn failed: ${String(err)}`);
13775
14242
  });
@@ -13912,7 +14379,8 @@ async function start(agent, opts) {
13912
14379
  ...opts?.prependPendingFinal ? { prependPendingFinal: opts.prependPendingFinal } : {},
13913
14380
  ...opts?.enqueuePendingFinal ? { enqueuePendingFinal: opts.enqueuePendingFinal } : {},
13914
14381
  ...opts?.dropPendingFinal ? { dropPendingFinal: opts.dropPendingFinal } : {},
13915
- ...opts?.mediaStore ? { mediaStore: opts.mediaStore } : {}
14382
+ ...opts?.mediaStore ? { mediaStore: opts.mediaStore } : {},
14383
+ ...opts?.perfTracer ? { perfTracer: opts.perfTracer } : {}
13916
14384
  });
13917
14385
  }
13918
14386
  var init_bot = __esm(() => {
@@ -14168,16 +14636,16 @@ var init_deliver_coordinator_message = __esm(() => {
14168
14636
  });
14169
14637
 
14170
14638
  // 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";
14639
+ import { mkdir as mkdir8, open as open2, readFile as readFile6, rm as rm6 } from "node:fs/promises";
14640
+ import { dirname as dirname8, join as join5 } from "node:path";
14173
14641
  import { homedir as homedir4 } from "node:os";
14174
14642
  function createWeixinConsumerLock(options = {}) {
14175
- const lockFilePath = options.lockFilePath ?? join4(homedir4(), ".weacpx", "runtime", "weixin-consumer.lock.json");
14643
+ const lockFilePath = options.lockFilePath ?? join5(homedir4(), ".weacpx", "runtime", "weixin-consumer.lock.json");
14176
14644
  const isProcessRunning = options.isProcessRunning ?? defaultIsProcessRunning4;
14177
14645
  const onDiagnostic = options.onDiagnostic;
14178
14646
  return {
14179
14647
  async acquire(meta2) {
14180
- await mkdir8(dirname6(lockFilePath), { recursive: true });
14648
+ await mkdir8(dirname8(lockFilePath), { recursive: true });
14181
14649
  while (true) {
14182
14650
  try {
14183
14651
  const handle = await open2(lockFilePath, "wx");
@@ -14208,7 +14676,7 @@ function createWeixinConsumerLock(options = {}) {
14208
14676
  });
14209
14677
  const existing = await loadLockMetadata(lockFilePath);
14210
14678
  if (!existing) {
14211
- await rm5(lockFilePath, { force: true });
14679
+ await rm6(lockFilePath, { force: true });
14212
14680
  await onDiagnostic?.("lock_invalid_removed", {
14213
14681
  lockFilePath,
14214
14682
  reason: "invalid_or_unreadable_metadata"
@@ -14216,7 +14684,7 @@ function createWeixinConsumerLock(options = {}) {
14216
14684
  continue;
14217
14685
  }
14218
14686
  if (!isProcessRunning(existing.pid)) {
14219
- await rm5(lockFilePath, { force: true });
14687
+ await rm6(lockFilePath, { force: true });
14220
14688
  await onDiagnostic?.("lock_stale_removed", {
14221
14689
  lockFilePath,
14222
14690
  stalePid: existing.pid,
@@ -14241,7 +14709,7 @@ function createWeixinConsumerLock(options = {}) {
14241
14709
  }
14242
14710
  },
14243
14711
  async release() {
14244
- await rm5(lockFilePath, { force: true });
14712
+ await rm6(lockFilePath, { force: true });
14245
14713
  await onDiagnostic?.("lock_released", {
14246
14714
  lockFilePath
14247
14715
  });
@@ -14340,7 +14808,8 @@ class WeixinChannel {
14340
14808
  drainPendingFinal: (chatKey, available) => input.quota.drainPendingFinalUpToBudget(chatKey, available),
14341
14809
  prependPendingFinal: (chatKey, chunks) => input.quota.prependPendingFinal(chatKey, chunks),
14342
14810
  enqueuePendingFinal: (chatKey, chunks) => input.quota.enqueuePendingFinal(chatKey, chunks),
14343
- dropPendingFinal: (chatKey) => input.quota.clearPendingFinal(chatKey)
14811
+ dropPendingFinal: (chatKey) => input.quota.clearPendingFinal(chatKey),
14812
+ ...input.perfTracer ? { perfTracer: input.perfTracer } : {}
14344
14813
  });
14345
14814
  }
14346
14815
  async notifyTaskCompletion(task) {
@@ -14849,9 +15318,9 @@ __export(exports_plugin_loader, {
14849
15318
  });
14850
15319
  import { createRequire as createRequire2 } from "node:module";
14851
15320
  import { pathToFileURL } from "node:url";
14852
- import { join as join5 } from "node:path";
15321
+ import { join as join6 } from "node:path";
14853
15322
  async function importPluginFromHome(packageName, pluginHome) {
14854
- const requireFromHome = createRequire2(join5(pluginHome, "package.json"));
15323
+ const requireFromHome = createRequire2(join6(pluginHome, "package.json"));
14855
15324
  const entry = requireFromHome.resolve(packageName);
14856
15325
  return await import(pathToFileURL(entry).href);
14857
15326
  }
@@ -14899,8 +15368,8 @@ var init_bootstrap = __esm(() => {
14899
15368
  });
14900
15369
 
14901
15370
  // 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";
15371
+ import { appendFile, mkdir as mkdir9 } from "node:fs/promises";
15372
+ import { dirname as dirname10 } from "node:path";
14904
15373
  function createNoopAppLogger() {
14905
15374
  return {
14906
15375
  debug: async () => {},
@@ -14940,74 +15409,18 @@ function createAppLogger(options) {
14940
15409
  return;
14941
15410
  }
14942
15411
  const line = formatLogLine(now(), level, event, message, context);
14943
- await mkdir9(dirname8(options.filePath), { recursive: true });
15412
+ await mkdir9(dirname10(options.filePath), { recursive: true });
14944
15413
  await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
14945
15414
  await appendFile(options.filePath, line, "utf8");
14946
15415
  }
14947
15416
  }
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
15417
  function formatLogLine(time3, level, event, message, context) {
15005
- const fields = Object.entries(context).filter(([, value]) => value !== undefined).map(([key, value]) => `${key}=${formatValue(value)}`);
15418
+ const fields = Object.entries(context).filter(([, value]) => value !== undefined).map(([key, value]) => `${key}=${formatValue2(value)}`);
15006
15419
  const suffix = fields.length > 0 ? ` ${fields.join(" ")}` : "";
15007
- return `${time3.toISOString()} ${level.toUpperCase()} ${event} message=${formatValue(message)}${suffix}
15420
+ return `${time3.toISOString()} ${level.toUpperCase()} ${event} message=${formatValue2(message)}${suffix}
15008
15421
  `;
15009
15422
  }
15010
- function formatValue(value) {
15423
+ function formatValue2(value) {
15011
15424
  if (value === null) {
15012
15425
  return "null";
15013
15426
  }
@@ -15016,11 +15429,9 @@ function formatValue(value) {
15016
15429
  }
15017
15430
  return JSON.stringify(value);
15018
15431
  }
15019
- function isMissingFileError2(error2) {
15020
- return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
15021
- }
15022
15432
  var LEVEL_ORDER;
15023
15433
  var init_app_logger = __esm(() => {
15434
+ init_rotating_file_writer();
15024
15435
  LEVEL_ORDER = {
15025
15436
  error: 0,
15026
15437
  info: 1,
@@ -15091,38 +15502,24 @@ function extractPromptFailureMessage(result) {
15091
15502
  function extractPromptOutput(output) {
15092
15503
  const lines = output.split(`
15093
15504
  `).map((line) => line.trim()).filter((line) => line.length > 0);
15094
- const messageSegments = [];
15095
- let currentSegment = "";
15505
+ let text = "";
15096
15506
  let hasAgentMessage = false;
15097
15507
  for (const line of lines) {
15508
+ let event;
15098
15509
  try {
15099
- const event = JSON.parse(line);
15100
- const isMessageChunk = event.method === "session/update" && event.params?.update?.sessionUpdate === "agent_message_chunk" && event.params.update.content?.type === "text" && typeof event.params.update.content.text === "string";
15101
- if (isMessageChunk) {
15102
- hasAgentMessage = true;
15103
- const chunk = event.params.update.content.text ?? "";
15104
- if (chunk.length > 0) {
15105
- currentSegment += chunk;
15106
- }
15107
- continue;
15108
- }
15109
- if (currentSegment.trim().length > 0) {
15110
- messageSegments.push(currentSegment.trim());
15111
- }
15112
- currentSegment = "";
15510
+ event = JSON.parse(line);
15113
15511
  } catch {
15114
- if (currentSegment.trim().length > 0) {
15115
- messageSegments.push(currentSegment.trim());
15116
- currentSegment = "";
15117
- }
15512
+ continue;
15513
+ }
15514
+ const isMessageChunk = event.method === "session/update" && event.params?.update?.sessionUpdate === "agent_message_chunk" && event.params.update.content?.type === "text" && typeof event.params.update.content.text === "string";
15515
+ if (isMessageChunk) {
15516
+ hasAgentMessage = true;
15517
+ text += event.params.update.content.text ?? "";
15118
15518
  }
15119
15519
  }
15120
- if (currentSegment.trim().length > 0) {
15121
- messageSegments.push(currentSegment.trim());
15122
- }
15123
- if (messageSegments.length > 0) {
15520
+ if (hasAgentMessage && text.trim().length > 0) {
15124
15521
  return {
15125
- text: messageSegments[messageSegments.length - 1],
15522
+ text: text.trim(),
15126
15523
  hasAgentMessage
15127
15524
  };
15128
15525
  }
@@ -16440,6 +16837,7 @@ async function handleSessionAttach(context, chatKey, alias, agent, workspace, tr
16440
16837
  `)
16441
16838
  };
16442
16839
  }
16840
+ context.lifecycle.markSessionReady?.(attached);
16443
16841
  await context.sessions.attachSession(internalAlias, agent, workspace, transportSession);
16444
16842
  await context.sessions.useSession(chatKey, internalAlias);
16445
16843
  await refreshSessionTransportAgentCommandBestEffort(context, internalAlias, "session.attach.agent_command_refresh_failed");
@@ -16636,7 +17034,7 @@ async function handleSessionRemove(context, chatKey, alias) {
16636
17034
  return { text: lines.join(`
16637
17035
  `) };
16638
17036
  }
16639
- async function promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent) {
17037
+ async function promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan) {
16640
17038
  const effectiveReplyMode = session.replyMode ?? context.config?.channel.replyMode ?? "verbose";
16641
17039
  if (!session.replyMode)
16642
17040
  session.replyMode = effectiveReplyMode;
@@ -16661,7 +17059,7 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
16661
17059
  const { promptText, taskIds, groupIds, claimHumanReply } = await preparePromptWithFallback(context, session, chatKey, text, replyContextToken, accountId);
16662
17060
  try {
16663
17061
  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);
17062
+ const result = await context.interaction.promptTransportSession(session, promptText, transportReply, replyContext, media, abortSignal, onToolEvent, perfSpan);
16665
17063
  if (claimHumanReply) {
16666
17064
  try {
16667
17065
  await context.orchestration?.claimActiveHumanReply?.(claimHumanReply);
@@ -16681,17 +17079,17 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
16681
17079
  throw error2;
16682
17080
  }
16683
17081
  }
16684
- async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent) {
17082
+ async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan) {
16685
17083
  const session = await context.sessions.getCurrentSession(chatKey);
16686
17084
  if (!session) {
16687
17085
  return { text: NO_CURRENT_SESSION_TEXT };
16688
17086
  }
16689
17087
  try {
16690
- return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent);
17088
+ return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan);
16691
17089
  } catch (error2) {
16692
17090
  const recovered = await context.recovery.tryRecoverMissingSession(session, error2);
16693
17091
  if (recovered) {
16694
- return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent);
17092
+ return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan);
16695
17093
  }
16696
17094
  return context.recovery.renderTransportError(session, error2);
16697
17095
  }
@@ -16921,6 +17319,8 @@ function renderTaskSummary2(task) {
16921
17319
  header.push(`- 任务:${task.task}`);
16922
17320
  if (task.summary.trim().length > 0)
16923
17321
  header.push(`- 摘要:${task.summary}`);
17322
+ if (task.lastProgressSummary)
17323
+ header.push(`- 最新进展:${task.lastProgressSummary}`);
16924
17324
  if (task.resultText.trim().length > 0)
16925
17325
  header.push(`- 结果:${task.resultText}`);
16926
17326
  const events = [];
@@ -17364,6 +17764,13 @@ async function handleAgentAdd(context, templateName) {
17364
17764
  if (!template) {
17365
17765
  return { text: `暂不支持这个 Agent 模板。当前可用:${listAgentTemplates().join("、")}` };
17366
17766
  }
17767
+ const existing = context.config.agents[templateName];
17768
+ if (existing) {
17769
+ if (sameAgentConfig(existing, template)) {
17770
+ return { text: `Agent「${templateName}」已存在` };
17771
+ }
17772
+ return { text: `Agent「${templateName}」已存在且配置不同。请先执行 /agent rm ${templateName}` };
17773
+ }
17367
17774
  const updated = await context.configStore.upsertAgent(templateName, template);
17368
17775
  context.replaceConfig(updated);
17369
17776
  return { text: `Agent「${templateName}」已保存` };
@@ -17388,7 +17795,7 @@ var init_agent_handler = __esm(() => {
17388
17795
  summary: "管理已注册的 Agent。",
17389
17796
  commands: [
17390
17797
  { usage: "/agents", description: "查看当前已注册的 Agent" },
17391
- { usage: "/agent add <codex|claude|opencode|gemini>", description: "添加内置 Agent 模板" },
17798
+ { usage: `/agent add <${listAgentTemplates().join("|")}>`, description: "添加内置 Agent 模板" },
17392
17799
  { usage: "/agent rm <name>", description: "删除一个 Agent" }
17393
17800
  ],
17394
17801
  examples: ["/agent add claude", "/agent rm codex"]
@@ -17955,7 +18362,7 @@ import { spawn as spawn6 } from "node:child_process";
17955
18362
  import { createRequire as createRequire3 } from "node:module";
17956
18363
  import { access as access3 } from "node:fs/promises";
17957
18364
  import { homedir as homedir7 } from "node:os";
17958
- import { dirname as dirname9, join as join11 } from "node:path";
18365
+ import { dirname as dirname11, join as join11 } from "node:path";
17959
18366
  function deriveParentPackageName(platformPackage) {
17960
18367
  return platformPackage.replace(/-(?:linux|darwin|win32|windows|freebsd|openbsd|sunos|aix)(?:-(?:x64|arm64|ia32|arm|ppc64|s390x))?(?:-(?:baseline|musl|gnu|gnueabihf|musleabihf|msvc))?$/, "");
17961
18368
  }
@@ -18028,7 +18435,7 @@ function defaultResolveFromCwd(name, cwd) {
18028
18435
  const pkgJson = require2.resolve(`${name}/package.json`, {
18029
18436
  paths: [cwd, ...require2.resolve.paths(name) ?? []]
18030
18437
  });
18031
- return dirname9(pkgJson);
18438
+ return dirname11(pkgJson);
18032
18439
  } catch {
18033
18440
  return null;
18034
18441
  }
@@ -18186,7 +18593,7 @@ class CommandRouter {
18186
18593
  this.quota = quota;
18187
18594
  this.logger = logger2 ?? createNoopAppLogger();
18188
18595
  }
18189
- async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent) {
18596
+ async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, perfSpan) {
18190
18597
  const startedAt = Date.now();
18191
18598
  const command = parseCommand(input);
18192
18599
  await this.logger.debug("command.parsed", "parsed inbound command", {
@@ -18194,6 +18601,7 @@ class CommandRouter {
18194
18601
  kind: command.kind
18195
18602
  });
18196
18603
  const access4 = authorizeCommandForChat(command, metadata);
18604
+ perfSpan?.mark("router.authorized", { decision: access4.allowed ? "allow" : "deny" });
18197
18605
  if (!access4.allowed) {
18198
18606
  await this.logger.info("command.blocked", "blocked command by chat policy", {
18199
18607
  chatKey,
@@ -18205,6 +18613,7 @@ class CommandRouter {
18205
18613
  return { text: renderCommandAccessDenied(command) };
18206
18614
  }
18207
18615
  await this.refreshConfigFromStore();
18616
+ perfSpan?.mark("router.config_refreshed");
18208
18617
  return await this.executeCommand(chatKey, command.kind, startedAt, async () => {
18209
18618
  switch (command.kind) {
18210
18619
  case "invalid":
@@ -18247,35 +18656,35 @@ class CommandRouter {
18247
18656
  case "workspace.rm":
18248
18657
  return await handleWorkspaceRemove(this.createHandlerContext(), command.name);
18249
18658
  case "sessions":
18250
- return await handleSessions(this.createSessionHandlerContext(), chatKey);
18659
+ return await handleSessions(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18251
18660
  case "session.new":
18252
- return await handleSessionNew(this.createSessionHandlerContext(reply), chatKey, command.alias, command.agent, command.workspace);
18661
+ return await handleSessionNew(this.createSessionHandlerContext(reply, perfSpan), chatKey, command.alias, command.agent, command.workspace);
18253
18662
  case "session.shortcut":
18254
- return await handleSessionShortcut(this.createSessionHandlerContext(reply), chatKey, command.agent, command, false);
18663
+ return await handleSessionShortcut(this.createSessionHandlerContext(reply, perfSpan), chatKey, command.agent, command, false);
18255
18664
  case "session.shortcut.new":
18256
- return await handleSessionShortcut(this.createSessionHandlerContext(reply), chatKey, command.agent, command, true);
18665
+ return await handleSessionShortcut(this.createSessionHandlerContext(reply, perfSpan), chatKey, command.agent, command, true);
18257
18666
  case "session.attach":
18258
- return await handleSessionAttach(this.createSessionHandlerContext(reply), chatKey, command.alias, command.agent, command.workspace, command.transportSession);
18667
+ return await handleSessionAttach(this.createSessionHandlerContext(reply, perfSpan), chatKey, command.alias, command.agent, command.workspace, command.transportSession);
18259
18668
  case "session.use":
18260
- return await handleSessionUse(this.createSessionHandlerContext(), chatKey, command.alias);
18669
+ return await handleSessionUse(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.alias);
18261
18670
  case "mode.show":
18262
- return await handleModeShow(this.createSessionHandlerContext(), chatKey);
18671
+ return await handleModeShow(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18263
18672
  case "mode.set":
18264
- return await handleModeSet(this.createSessionHandlerContext(), chatKey, command.modeId);
18673
+ return await handleModeSet(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.modeId);
18265
18674
  case "replymode.show":
18266
- return await handleReplyModeShow(this.createSessionHandlerContext(), chatKey);
18675
+ return await handleReplyModeShow(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18267
18676
  case "replymode.set":
18268
- return await handleReplyModeSet(this.createSessionHandlerContext(), chatKey, command.replyMode);
18677
+ return await handleReplyModeSet(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.replyMode);
18269
18678
  case "replymode.reset":
18270
- return await handleReplyModeReset(this.createSessionHandlerContext(), chatKey);
18679
+ return await handleReplyModeReset(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18271
18680
  case "status":
18272
- return await handleStatus(this.createSessionHandlerContext(), chatKey);
18681
+ return await handleStatus(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18273
18682
  case "cancel":
18274
- return await handleCancel(this.createSessionHandlerContext(), chatKey);
18683
+ return await handleCancel(this.createSessionHandlerContext(undefined, perfSpan), chatKey);
18275
18684
  case "session.reset":
18276
- return await handleSessionReset(this.createSessionHandlerContext(reply), chatKey);
18685
+ return await handleSessionReset(this.createSessionHandlerContext(reply, perfSpan), chatKey);
18277
18686
  case "session.rm":
18278
- return await handleSessionRemove(this.createSessionHandlerContext(), chatKey, command.alias);
18687
+ return await handleSessionRemove(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.alias);
18279
18688
  case "groups":
18280
18689
  return await handleGroupList(this.createHandlerContext(), chatKey, command.filter);
18281
18690
  case "group.new":
@@ -18301,7 +18710,7 @@ class CommandRouter {
18301
18710
  case "task.cancel":
18302
18711
  return await handleTaskCancel(this.createHandlerContext(), chatKey, command.taskId);
18303
18712
  case "prompt":
18304
- return await handlePrompt(this.createSessionHandlerContext(), chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent);
18713
+ return await handlePrompt(this.createSessionHandlerContext(undefined, perfSpan), chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, perfSpan);
18305
18714
  }
18306
18715
  });
18307
18716
  }
@@ -18320,23 +18729,24 @@ class CommandRouter {
18320
18729
  ...this.quota ? { quota: this.quota } : {}
18321
18730
  };
18322
18731
  }
18323
- createSessionHandlerContext(reply) {
18732
+ createSessionHandlerContext(reply, perfSpan) {
18324
18733
  return {
18325
18734
  ...this.createHandlerContext(),
18326
- lifecycle: this.createSessionLifecycleOps(reply),
18327
- interaction: this.createSessionInteractionOps(),
18735
+ lifecycle: this.createSessionLifecycleOps(reply, perfSpan),
18736
+ interaction: this.createSessionInteractionOps(perfSpan),
18328
18737
  recovery: this.createSessionRenderRecoveryOps()
18329
18738
  };
18330
18739
  }
18331
- createSessionLifecycleOps(reply) {
18740
+ createSessionLifecycleOps(reply, perfSpan) {
18332
18741
  return {
18333
18742
  resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
18334
- ensureTransportSession: (session, replyOverride) => this.ensureTransportSession(session, replyOverride ?? reply),
18743
+ ensureTransportSession: (session, replyOverride, perfSpanOverride) => this.ensureTransportSession(session, replyOverride ?? reply, perfSpanOverride ?? perfSpan),
18335
18744
  checkTransportSession: (session) => this.checkTransportSession(session),
18745
+ markSessionReady: () => perfSpan?.mark("session.ready"),
18336
18746
  reserveTransportSession: (transportSession) => this.reserveLogicalTransportSession(transportSession),
18337
18747
  handleSessionShortcut: async (chatKey, agent, target, createNew, replyOverride) => {
18338
18748
  try {
18339
- return await handleSessionShortcutCommand(this.createHandlerContext(), this.createSessionShortcutOps(replyOverride ?? reply), chatKey, agent, target, createNew);
18749
+ return await handleSessionShortcutCommand(this.createHandlerContext(), this.createSessionShortcutOps(replyOverride ?? reply, perfSpan), chatKey, agent, target, createNew);
18340
18750
  } catch (err) {
18341
18751
  if (err instanceof AutoInstallFailedError) {
18342
18752
  const session = this.sessions.resolveSession(`${agent}`, agent, target.workspace ?? "", `${agent}`);
@@ -18345,15 +18755,15 @@ class CommandRouter {
18345
18755
  throw err;
18346
18756
  }
18347
18757
  },
18348
- resetCurrentSession: (chatKey, replyOverride) => handleSessionResetCommand(this.createHandlerContext(), this.createSessionResetOps(replyOverride ?? reply), chatKey),
18758
+ resetCurrentSession: (chatKey, replyOverride) => handleSessionResetCommand(this.createHandlerContext(), this.createSessionResetOps(replyOverride ?? reply, perfSpan), chatKey),
18349
18759
  refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias)
18350
18760
  };
18351
18761
  }
18352
- createSessionInteractionOps() {
18762
+ createSessionInteractionOps(perfSpan) {
18353
18763
  return {
18354
18764
  setModeTransportSession: (session, modeId) => this.setModeTransportSession(session, modeId),
18355
18765
  cancelTransportSession: (session) => this.cancelTransportSession(session),
18356
- promptTransportSession: (session, text, reply, replyContext, media, abortSignal, onToolEvent) => this.promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent)
18766
+ promptTransportSession: (session, text, reply, replyContext, media, abortSignal, onToolEvent, perfSpanOverride) => this.promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent, perfSpanOverride ?? perfSpan)
18357
18767
  };
18358
18768
  }
18359
18769
  createSessionRenderRecoveryOps() {
@@ -18364,9 +18774,9 @@ class CommandRouter {
18364
18774
  renderTransportError: (session, error2) => renderTransportError(session, error2)
18365
18775
  };
18366
18776
  }
18367
- createSessionResetOps(reply) {
18777
+ createSessionResetOps(reply, perfSpan) {
18368
18778
  return {
18369
- ensureTransportSession: (session, replyOverride) => this.ensureTransportSession(session, replyOverride ?? reply),
18779
+ ensureTransportSession: (session, replyOverride, perfSpanOverride) => this.ensureTransportSession(session, replyOverride ?? reply, perfSpanOverride ?? perfSpan),
18370
18780
  checkTransportSession: (session) => this.checkTransportSession(session),
18371
18781
  reserveTransportSession: (transportSession) => this.reserveLogicalTransportSession(transportSession),
18372
18782
  resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
@@ -18381,10 +18791,10 @@ class CommandRouter {
18381
18791
  getSession: (alias) => this.sessions.getSession(alias)
18382
18792
  };
18383
18793
  }
18384
- createSessionShortcutOps(reply) {
18794
+ createSessionShortcutOps(reply, perfSpan) {
18385
18795
  return {
18386
18796
  resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
18387
- ensureTransportSession: (session, replyOverride) => this.ensureTransportSession(session, replyOverride ?? reply),
18797
+ ensureTransportSession: (session, replyOverride, perfSpanOverride) => this.ensureTransportSession(session, replyOverride ?? reply, perfSpanOverride ?? perfSpan),
18388
18798
  checkTransportSession: (session) => this.checkTransportSession(session),
18389
18799
  reserveTransportSession: (transportSession) => this.reserveLogicalTransportSession(transportSession),
18390
18800
  refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias)
@@ -18445,13 +18855,14 @@ class CommandRouter {
18445
18855
  throw error2;
18446
18856
  }
18447
18857
  }
18448
- async ensureTransportSession(session, reply) {
18858
+ async ensureTransportSession(session, reply, perfSpan) {
18449
18859
  const attemptSession = (operation) => {
18450
18860
  const { handler, dispose } = this.createProgressHandler(session, reply);
18451
18861
  return this.measureTransportCall(operation, session, () => this.transport.ensureSession(session, handler)).finally(dispose);
18452
18862
  };
18453
18863
  try {
18454
18864
  await attemptSession("ensure_session");
18865
+ perfSpan?.mark("session.ready");
18455
18866
  } catch (err) {
18456
18867
  if (!(err instanceof MissingOptionalDepError))
18457
18868
  throw err;
@@ -18464,6 +18875,7 @@ class CommandRouter {
18464
18875
  await reply?.(`\uD83D\uDD04 安装完成,正在验证会话启动…`);
18465
18876
  try {
18466
18877
  await attemptSession("ensure_session.verify");
18878
+ perfSpan?.mark("session.ready");
18467
18879
  return true;
18468
18880
  } catch (retryErr) {
18469
18881
  if (retryErr instanceof MissingOptionalDepError)
@@ -18529,11 +18941,13 @@ class CommandRouter {
18529
18941
  async checkTransportSession(session) {
18530
18942
  return await this.measureTransportCall("has_session", session, () => this.transport.hasSession(session));
18531
18943
  }
18532
- async promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent) {
18944
+ async promptTransportSession(session, text, reply, replyContext, media, abortSignal, onToolEvent, perfSpan) {
18533
18945
  session.mcpCoordinatorSession ??= session.transportSession;
18534
18946
  let done = false;
18947
+ let abortRequested = false;
18535
18948
  let cancelOnAbort;
18536
18949
  const fireCancel = () => {
18950
+ abortRequested = true;
18537
18951
  if (done)
18538
18952
  return;
18539
18953
  try {
@@ -18557,20 +18971,42 @@ class CommandRouter {
18557
18971
  });
18558
18972
  }
18559
18973
  };
18974
+ let localOutcome = "ok";
18560
18975
  if (abortSignal) {
18561
18976
  if (abortSignal.aborted) {
18562
- done = true;
18563
- throw new DOMException("Aborted before prompt started", "AbortError");
18977
+ abortRequested = true;
18978
+ } else {
18979
+ cancelOnAbort = fireCancel;
18980
+ abortSignal.addEventListener("abort", cancelOnAbort, { once: true });
18564
18981
  }
18565
- cancelOnAbort = fireCancel;
18566
- abortSignal.addEventListener("abort", cancelOnAbort, { once: true });
18567
18982
  }
18983
+ let firstChunkFired = false;
18984
+ const onSegment = (_segment) => {
18985
+ if (!firstChunkFired) {
18986
+ firstChunkFired = true;
18987
+ perfSpan?.mark("transport.first_chunk");
18988
+ }
18989
+ };
18568
18990
  try {
18991
+ if (abortRequested) {
18992
+ throw new DOMException("Aborted before prompt started", "AbortError");
18993
+ }
18994
+ perfSpan?.mark("transport.prompt_dispatched", {
18995
+ transportKind: this.config?.transport.type ?? inferTransportKind(this.transport)
18996
+ });
18569
18997
  return await this.measureTransportCall("prompt", session, () => this.transport.prompt(session, text, reply, replyContext, {
18570
18998
  ...media ? { media } : {},
18999
+ ...reply ? { onSegment } : {},
18571
19000
  ...onToolEvent ? { onToolEvent } : {}
18572
19001
  }));
19002
+ } catch (error2) {
19003
+ localOutcome = isAbortError2(error2) || abortRequested ? "aborted" : "error";
19004
+ throw error2;
18573
19005
  } finally {
19006
+ if (abortRequested && localOutcome === "ok") {
19007
+ localOutcome = "aborted";
19008
+ }
19009
+ perfSpan?.mark("transport.prompt_done", { localOutcome });
18574
19010
  done = true;
18575
19011
  if (cancelOnAbort && abortSignal) {
18576
19012
  abortSignal.removeEventListener("abort", cancelOnAbort);
@@ -18631,6 +19067,12 @@ class CommandRouter {
18631
19067
  }
18632
19068
  }
18633
19069
  }
19070
+ function isAbortError2(error2) {
19071
+ return error2 instanceof Error && error2.name === "AbortError";
19072
+ }
19073
+ function inferTransportKind(transport) {
19074
+ return transport.constructor.name.includes("Bridge") ? "acpx-bridge" : "acpx-cli";
19075
+ }
18634
19076
  var init_command_router = __esm(() => {
18635
19077
  init_app_logger();
18636
19078
  init_acpx_session_index();
@@ -18720,7 +19162,8 @@ class ConsoleAgent {
18720
19162
  mimeType: m.mimeType,
18721
19163
  ...m.fileName ? { fileName: m.fileName } : {}
18722
19164
  })) : undefined;
18723
- return await this.router.handle(request.conversationId, request.text, request.reply, request.replyContextToken, request.accountId, promptMedia, request.metadata, request.abortSignal, request.onToolEvent);
19165
+ request.perfSpan?.mark("agent.dispatched");
19166
+ 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
19167
  }
18725
19168
  isKnownCommand(text) {
18726
19169
  return isKnownWeacpxCommandText(text);
@@ -18868,6 +19311,8 @@ class OrchestrationServer {
18868
19311
  return await this.handlers.listTasks(this.parseTaskListFilter(params));
18869
19312
  case "task.wait":
18870
19313
  return await this.handlers.waitTask(this.parseWaitTaskInput(params));
19314
+ case "task.watch":
19315
+ return await this.handlers.watchTask(this.parseWatchTaskInput(params));
18871
19316
  case "task.approve":
18872
19317
  requireOnlyKeys(params, ["taskId", "coordinatorSession"], "params");
18873
19318
  return await this.handlers.approveTask({
@@ -19054,6 +19499,23 @@ class OrchestrationServer {
19054
19499
  ...pollIntervalMs !== undefined ? { pollIntervalMs } : {}
19055
19500
  };
19056
19501
  }
19502
+ parseWatchTaskInput(params) {
19503
+ requireOnlyKeys(params, ["coordinatorSession", "taskId", "afterSeq", "mode", "includeProgress", "timeoutMs", "pollIntervalMs"], "params");
19504
+ const afterSeq = requireOptionalIntegerInRange(params, "afterSeq", 0, Number.MAX_SAFE_INTEGER);
19505
+ const mode = requireOptionalEnum(params, "mode", ["next_event", "until_attention_or_terminal"]);
19506
+ const includeProgress = requireOptionalBoolean(params, "includeProgress");
19507
+ const timeoutMs = requireOptionalIntegerInRange(params, "timeoutMs", 0, MAX_TASK_WATCH_TIMEOUT_MS);
19508
+ const pollIntervalMs = requireOptionalIntegerInRange(params, "pollIntervalMs", 1, MAX_TASK_WATCH_POLL_INTERVAL_MS);
19509
+ return {
19510
+ coordinatorSession: requireString(params, "coordinatorSession"),
19511
+ taskId: requireString(params, "taskId"),
19512
+ ...afterSeq !== undefined ? { afterSeq } : {},
19513
+ ...mode !== undefined ? { mode } : {},
19514
+ ...includeProgress !== undefined ? { includeProgress } : {},
19515
+ ...timeoutMs !== undefined ? { timeoutMs } : {},
19516
+ ...pollIntervalMs !== undefined ? { pollIntervalMs } : {}
19517
+ };
19518
+ }
19057
19519
  parseWorkerRaiseQuestionInput(params) {
19058
19520
  requireOnlyKeys(params, ["taskId", "sourceHandle", "question", "whyBlocked", "whatIsNeeded"], "params");
19059
19521
  return {
@@ -19297,6 +19759,7 @@ var init_orchestration_server = __esm(() => {
19297
19759
  "task.get",
19298
19760
  "task.list",
19299
19761
  "task.wait",
19762
+ "task.watch",
19300
19763
  "task.approve",
19301
19764
  "task.reject",
19302
19765
  "task.cancel",
@@ -19316,30 +19779,92 @@ var init_orchestration_server = __esm(() => {
19316
19779
 
19317
19780
  // src/orchestration/progress-line-parser.ts
19318
19781
  class ProgressLineBuffer {
19319
- feed(segment) {
19782
+ pending = "";
19783
+ feed(segment, options = {}) {
19784
+ const hadPending = this.pending.length > 0;
19785
+ this.pending += segment;
19320
19786
  const summaries = [];
19321
- for (const line of segment.split(`
19322
- `)) {
19323
- if (line.startsWith(PROGRESS_PREFIX)) {
19324
- const summary = line.slice(PROGRESS_PREFIX.length).trim();
19325
- if (summary.length > 0) {
19326
- summaries.push(summary);
19327
- }
19328
- }
19787
+ let newlineIndex = this.pending.indexOf(`
19788
+ `);
19789
+ while (newlineIndex !== -1) {
19790
+ const line = this.pending.slice(0, newlineIndex).replace(/\r$/, "");
19791
+ this.pending = this.pending.slice(newlineIndex + 1);
19792
+ this.extractLine(line, summaries);
19793
+ newlineIndex = this.pending.indexOf(`
19794
+ `);
19795
+ }
19796
+ if (options.segmentComplete === true && !hadPending && this.pending.startsWith(PROGRESS_PREFIX)) {
19797
+ this.extractLine(this.pending.replace(/\r$/, ""), summaries);
19798
+ this.pending = "";
19799
+ return summaries;
19800
+ }
19801
+ this.trimPendingIfHopeless();
19802
+ return summaries;
19803
+ }
19804
+ flush() {
19805
+ const summaries = [];
19806
+ if (this.pending.length > 0) {
19807
+ this.extractLine(this.pending.replace(/\r$/, ""), summaries);
19808
+ this.pending = "";
19329
19809
  }
19330
19810
  return summaries;
19331
19811
  }
19812
+ extractLine(line, summaries) {
19813
+ if (line.startsWith(PROGRESS_PREFIX)) {
19814
+ const summary = sanitizeProgressSummary(line.slice(PROGRESS_PREFIX.length));
19815
+ if (summary.length > 0) {
19816
+ summaries.push(summary);
19817
+ }
19818
+ }
19819
+ }
19820
+ trimPendingIfHopeless() {
19821
+ if (this.pending.length === 0)
19822
+ return;
19823
+ if (PROGRESS_PREFIX.startsWith(this.pending) || this.pending.startsWith(PROGRESS_PREFIX)) {
19824
+ if (this.pending.length > MAX_PENDING_LINE_LENGTH) {
19825
+ this.pending = "";
19826
+ }
19827
+ return;
19828
+ }
19829
+ this.pending = "";
19830
+ }
19831
+ }
19832
+ function sanitizeProgressSummary(summary) {
19833
+ const cleaned = summary.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").trim();
19834
+ if (cleaned.length <= MAX_PROGRESS_SUMMARY_LENGTH) {
19835
+ return cleaned;
19836
+ }
19837
+ return `${cleaned.slice(0, MAX_PROGRESS_SUMMARY_LENGTH - 3)}...`;
19332
19838
  }
19333
19839
  function stripProgressLines(text) {
19334
- return text.split(`
19335
- `).filter((line) => !line.startsWith(PROGRESS_PREFIX)).join(`
19840
+ return text.split(/\r\n|\n|\r/).filter((line) => {
19841
+ const normalized = normalizeProgressLinePrefix(line);
19842
+ return !normalized.startsWith(PROGRESS_PREFIX) && !(line.length > 0 && normalized.length === 0);
19843
+ }).join(`
19336
19844
  `).trim();
19337
19845
  }
19338
- var PROGRESS_PREFIX = "[PROGRESS]";
19846
+ function normalizeProgressLinePrefix(line) {
19847
+ return line.replace(/^(?:\r+|\u001B\[[0-?]*[ -/]*[@-~])+/, "");
19848
+ }
19849
+ var PROGRESS_PREFIX = "[PROGRESS]", MAX_PROGRESS_SUMMARY_LENGTH = 500, MAX_PENDING_LINE_LENGTH = 4096;
19339
19850
 
19340
19851
  // src/orchestration/orchestration-service.ts
19341
19852
  import { createHash as createHash2 } from "node:crypto";
19342
19853
  import { basename as basename2, isAbsolute as isAbsolute2, normalize } from "node:path";
19854
+ function clampWatchTimeout(value) {
19855
+ if (value === undefined)
19856
+ return DEFAULT_TASK_WATCH_TIMEOUT_MS;
19857
+ if (!Number.isFinite(value) || value < 0)
19858
+ return 0;
19859
+ return Math.min(Math.floor(value), MAX_TASK_WATCH_TIMEOUT_MS);
19860
+ }
19861
+ function clampWatchPollInterval(value) {
19862
+ if (value === undefined)
19863
+ return DEFAULT_TASK_WATCH_POLL_INTERVAL_MS;
19864
+ if (!Number.isFinite(value) || value < 1)
19865
+ return DEFAULT_TASK_WATCH_POLL_INTERVAL_MS;
19866
+ return Math.min(value, MAX_TASK_WATCH_POLL_INTERVAL_MS);
19867
+ }
19343
19868
 
19344
19869
  class OrchestrationService {
19345
19870
  deps;
@@ -19556,6 +20081,8 @@ class OrchestrationService {
19556
20081
  resultText: "",
19557
20082
  createdAt: now,
19558
20083
  updatedAt: now,
20084
+ eventSeq: 1,
20085
+ events: [{ seq: 1, at: now, type: "created", status: "running", message: "Task created" }],
19559
20086
  ...input.chatKey ? { chatKey: input.chatKey } : {},
19560
20087
  ...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
19561
20088
  ...input.accountId ? { accountId: input.accountId } : {}
@@ -19679,7 +20206,9 @@ class OrchestrationService {
19679
20206
  summary: "",
19680
20207
  resultText: "",
19681
20208
  createdAt: now,
19682
- updatedAt: now
20209
+ updatedAt: now,
20210
+ eventSeq: 1,
20211
+ events: [{ seq: 1, at: now, type: "created", status, message: "Task created" }]
19683
20212
  };
19684
20213
  if (preflight.normalizedGroupId) {
19685
20214
  const group = this.ensureGroups(state)[preflight.normalizedGroupId];
@@ -19850,6 +20379,11 @@ class OrchestrationService {
19850
20379
  current.summary = message;
19851
20380
  current.resultText = "";
19852
20381
  current.updatedAt = now;
20382
+ this.appendTaskEvent(current, now, "status_changed", {
20383
+ status: "failed",
20384
+ summary: message,
20385
+ message: "Task failed during startup"
20386
+ });
19853
20387
  restoreOrDeleteBinding();
19854
20388
  await this.deps.saveState(state);
19855
20389
  return true;
@@ -19942,6 +20476,11 @@ class OrchestrationService {
19942
20476
  task2.status = input.status ?? "completed";
19943
20477
  task2.summary = input.summary ?? "";
19944
20478
  task2.resultText = stripProgressLines(input.resultText ?? "");
20479
+ this.appendTaskEvent(task2, updatedAt, "status_changed", {
20480
+ status: task2.status,
20481
+ summary: task2.summary,
20482
+ message: task2.status === "completed" ? "Task completed" : task2.status === "failed" ? "Task failed" : "Task cancelled"
20483
+ });
19945
20484
  if (task2.status === "completed" || task2.status === "failed") {
19946
20485
  if (!this.isExternalCoordinatorSession(state, task2.coordinatorSession)) {
19947
20486
  task2.injectionPending = true;
@@ -19970,6 +20509,10 @@ class OrchestrationService {
19970
20509
  resultId: this.deps.createId(),
19971
20510
  resultText: task2.resultText
19972
20511
  };
20512
+ this.appendTaskEvent(task2, updatedAt, "attention_required", {
20513
+ status: task2.status,
20514
+ message: "Task result requires contested review"
20515
+ });
19973
20516
  task2.correctionPending = undefined;
19974
20517
  task2.cancelRequestedAt = undefined;
19975
20518
  task2.cancelCompletedAt = undefined;
@@ -20078,6 +20621,64 @@ class OrchestrationService {
20078
20621
  await sleep2(Math.min(pollIntervalMs, remainingMs));
20079
20622
  }
20080
20623
  }
20624
+ async watchTask(input) {
20625
+ const timeoutMs = clampWatchTimeout(input.timeoutMs);
20626
+ const pollIntervalMs = clampWatchPollInterval(input.pollIntervalMs);
20627
+ const afterSeq = Math.max(0, Math.floor(input.afterSeq ?? 0));
20628
+ const mode = input.mode ?? "until_attention_or_terminal";
20629
+ const includeProgress = input.includeProgress ?? true;
20630
+ const deadline = Date.now() + timeoutMs;
20631
+ while (true) {
20632
+ const state = await this.deps.loadState();
20633
+ const task = state.orchestration.tasks[input.taskId];
20634
+ if (!task || task.coordinatorSession !== input.coordinatorSession) {
20635
+ return { status: "not_found", task: null, events: [], nextAfterSeq: afterSeq };
20636
+ }
20637
+ const snapshot = { ...task };
20638
+ const allEvents = task.events ?? [];
20639
+ const filteredEvents = allEvents.filter((event) => event.seq > afterSeq).filter((event) => includeProgress || event.type !== "progress");
20640
+ const nextAfterSeq = task.eventSeq ?? allEvents.at(-1)?.seq ?? afterSeq;
20641
+ const historyTruncated = allEvents.length > 0 && afterSeq < allEvents[0].seq - 1;
20642
+ if (isTerminalTaskStatus2(task.status) && task.reviewPending === undefined) {
20643
+ return {
20644
+ status: "terminal",
20645
+ task: snapshot,
20646
+ events: filteredEvents.map((event) => ({ ...event })),
20647
+ nextAfterSeq,
20648
+ ...historyTruncated ? { historyTruncated } : {}
20649
+ };
20650
+ }
20651
+ if (isAttentionRequiredTask(task)) {
20652
+ return {
20653
+ status: "attention_required",
20654
+ task: snapshot,
20655
+ events: filteredEvents.map((event) => ({ ...event })),
20656
+ nextAfterSeq,
20657
+ ...historyTruncated ? { historyTruncated } : {}
20658
+ };
20659
+ }
20660
+ if (filteredEvents.length > 0 && mode === "next_event") {
20661
+ return {
20662
+ status: "event",
20663
+ task: snapshot,
20664
+ events: filteredEvents.map((event) => ({ ...event })),
20665
+ nextAfterSeq,
20666
+ ...historyTruncated ? { historyTruncated } : {}
20667
+ };
20668
+ }
20669
+ const remainingMs = deadline - Date.now();
20670
+ if (remainingMs <= 0) {
20671
+ return {
20672
+ status: "timeout",
20673
+ task: snapshot,
20674
+ events: filteredEvents.map((event) => ({ ...event })),
20675
+ nextAfterSeq,
20676
+ ...historyTruncated ? { historyTruncated } : {}
20677
+ };
20678
+ }
20679
+ await sleep2(Math.min(pollIntervalMs, remainingMs));
20680
+ }
20681
+ }
20081
20682
  async recordCoordinatorRouteContext(input) {
20082
20683
  if (input.coordinatorSession.trim().length === 0) {
20083
20684
  throw new Error("coordinatorSession must be a non-empty string");
@@ -20157,6 +20758,10 @@ class OrchestrationService {
20157
20758
  status: "open"
20158
20759
  };
20159
20760
  task.updatedAt = now;
20761
+ this.appendTaskEvent(task, now, "attention_required", {
20762
+ status: "blocked",
20763
+ message: input.question.trim()
20764
+ });
20160
20765
  this.bumpGroupUpdated(state, task.groupId, now);
20161
20766
  await this.deps.saveState(state);
20162
20767
  return {
@@ -20214,6 +20819,10 @@ class OrchestrationService {
20214
20819
  lastResumeError: undefined
20215
20820
  };
20216
20821
  task.updatedAt = now;
20822
+ this.appendTaskEvent(task, now, "status_changed", {
20823
+ status: "running",
20824
+ message: "Blocker question answered"
20825
+ });
20217
20826
  this.bumpGroupUpdated(state, task.groupId, now);
20218
20827
  await this.deps.saveState(state);
20219
20828
  return {
@@ -20268,6 +20877,10 @@ class OrchestrationService {
20268
20877
  };
20269
20878
  task.cancelRequestedAt = task.cancelRequestedAt ?? now;
20270
20879
  task.updatedAt = now;
20880
+ this.appendTaskEvent(task, now, "cancel_requested", {
20881
+ status: task.status,
20882
+ message: "Correction requested for misrouted answer"
20883
+ });
20271
20884
  this.bumpGroupUpdated(state, task.groupId, now);
20272
20885
  await this.deps.saveState(state);
20273
20886
  return {
@@ -20286,6 +20899,10 @@ class OrchestrationService {
20286
20899
  task.noticePending = false;
20287
20900
  task.lastNoticeError = undefined;
20288
20901
  task.updatedAt = now;
20902
+ this.appendTaskEvent(task, now, "attention_required", {
20903
+ status: task.status,
20904
+ message: "Task result requires contested review"
20905
+ });
20289
20906
  this.bumpGroupUpdated(state, task.groupId, now);
20290
20907
  await this.deps.saveState(state);
20291
20908
  return {
@@ -20392,6 +21009,10 @@ class OrchestrationService {
20392
21009
  packageId
20393
21010
  };
20394
21011
  task.updatedAt = now;
21012
+ this.appendTaskEvent(task, now, "attention_required", {
21013
+ status: "waiting_for_human",
21014
+ message: task.openQuestion.question
21015
+ });
20395
21016
  this.bumpGroupUpdated(state, task.groupId, now);
20396
21017
  }
20397
21018
  this.ensureHumanQuestionPackages(state)[packageId] = packageRecord;
@@ -20479,6 +21100,10 @@ class OrchestrationService {
20479
21100
  packageId: input.packageId
20480
21101
  };
20481
21102
  task.updatedAt = now;
21103
+ this.appendTaskEvent(task, now, "attention_required", {
21104
+ status: "waiting_for_human",
21105
+ message: task.openQuestion.question
21106
+ });
20482
21107
  this.bumpGroupUpdated(state, task.groupId, now);
20483
21108
  }
20484
21109
  await this.deps.saveState(state);
@@ -20662,11 +21287,21 @@ class OrchestrationService {
20662
21287
  task.summary = "";
20663
21288
  task.resultText = "";
20664
21289
  task.openQuestion = this.buildReplacementOpenQuestion(task, replacementQuestionId, now, packageId);
21290
+ this.appendTaskEvent(task, now, "attention_required", {
21291
+ status: task.status,
21292
+ message: task.openQuestion.question
21293
+ });
20665
21294
  } else if ((task.status === "completed" || task.status === "failed") && task.chatKey && task.replyContextToken && task.noticeSentAt === undefined) {
20666
21295
  task.noticePending = true;
20667
21296
  task.lastNoticeError = undefined;
20668
21297
  }
20669
21298
  task.updatedAt = now;
21299
+ if (input.decision === "accept") {
21300
+ this.appendTaskEvent(task, now, "status_changed", {
21301
+ status: task.status,
21302
+ message: "Contested result accepted"
21303
+ });
21304
+ }
20670
21305
  this.bumpGroupUpdated(state, task.groupId, now);
20671
21306
  await this.deps.saveState(state);
20672
21307
  return {
@@ -21044,7 +21679,7 @@ class OrchestrationService {
21044
21679
  });
21045
21680
  }
21046
21681
  }
21047
- async recordTaskProgress(taskId) {
21682
+ async recordTaskProgress(taskId, summary) {
21048
21683
  return await this.mutate(async () => {
21049
21684
  const state = await this.deps.loadState();
21050
21685
  const task = state.orchestration.tasks[taskId];
@@ -21052,6 +21687,21 @@ class OrchestrationService {
21052
21687
  throw new Error(`task "${taskId}" does not exist`);
21053
21688
  }
21054
21689
  task.lastProgressAt = this.deps.now().toISOString();
21690
+ if (summary !== undefined) {
21691
+ const cleaned = sanitizeProgressSummary(summary);
21692
+ if (cleaned.length > 0) {
21693
+ task.lastProgressSummary = cleaned;
21694
+ this.appendTaskEvent(task, task.lastProgressAt, "progress", {
21695
+ status: task.status,
21696
+ summary: cleaned
21697
+ });
21698
+ }
21699
+ } else {
21700
+ this.appendTaskEvent(task, task.lastProgressAt, "progress", {
21701
+ status: task.status,
21702
+ message: "heartbeat"
21703
+ });
21704
+ }
21055
21705
  task.updatedAt = task.lastProgressAt;
21056
21706
  await this.deps.saveState(state);
21057
21707
  return { ...task };
@@ -21099,6 +21749,12 @@ class OrchestrationService {
21099
21749
  const shouldPropagate = task.cancelRequestedAt === undefined;
21100
21750
  task.cancelRequestedAt = task.cancelRequestedAt ?? now;
21101
21751
  task.updatedAt = now;
21752
+ if (shouldPropagate) {
21753
+ this.appendTaskEvent(task, now, "cancel_requested", {
21754
+ status: task.status,
21755
+ message: "Cancellation requested"
21756
+ });
21757
+ }
21102
21758
  this.bumpGroupUpdated(state, task.groupId, now);
21103
21759
  await this.deps.saveState(state);
21104
21760
  return { task: { ...task }, shouldPropagate, closedPackageId: undefined };
@@ -21110,6 +21766,10 @@ class OrchestrationService {
21110
21766
  task.cancelCompletedAt = now;
21111
21767
  task.lastCancelError = undefined;
21112
21768
  task.updatedAt = now;
21769
+ this.appendTaskEvent(task, now, "status_changed", {
21770
+ status: "cancelled",
21771
+ message: "Task cancelled"
21772
+ });
21113
21773
  this.bumpGroupUpdated(state, task.groupId, now);
21114
21774
  await this.deps.saveState(state);
21115
21775
  return { task: { ...task }, shouldPropagate: false, closedPackageId };
@@ -21144,10 +21804,18 @@ class OrchestrationService {
21144
21804
  task.cancelRequestedAt = undefined;
21145
21805
  task.cancelCompletedAt = undefined;
21146
21806
  task.lastCancelError = undefined;
21807
+ this.appendTaskEvent(task, now, "attention_required", {
21808
+ status: task.status,
21809
+ message: task.openQuestion.question
21810
+ });
21147
21811
  } else {
21148
21812
  task.status = "cancelled";
21149
21813
  task.cancelCompletedAt = now;
21150
21814
  task.lastCancelError = undefined;
21815
+ this.appendTaskEvent(task, now, "status_changed", {
21816
+ status: "cancelled",
21817
+ message: "Task cancelled"
21818
+ });
21151
21819
  }
21152
21820
  task.updatedAt = now;
21153
21821
  this.bumpGroupUpdated(state, task.groupId, now);
@@ -21189,6 +21857,10 @@ class OrchestrationService {
21189
21857
  }
21190
21858
  task2.lastCancelError = errorMessage;
21191
21859
  task2.updatedAt = this.deps.now().toISOString();
21860
+ this.appendTaskEvent(task2, task2.updatedAt, "progress", {
21861
+ status: task2.status,
21862
+ message: `Cancellation failed: ${errorMessage}`
21863
+ });
21192
21864
  await this.deps.saveState(state);
21193
21865
  return { ...task2 };
21194
21866
  });
@@ -21246,6 +21918,10 @@ class OrchestrationService {
21246
21918
  task.workerSession = ensuredWorkerSession;
21247
21919
  task.status = "running";
21248
21920
  task.updatedAt = this.deps.now().toISOString();
21921
+ this.appendTaskEvent(task, task.updatedAt, "status_changed", {
21922
+ status: "running",
21923
+ message: "Task approved"
21924
+ });
21249
21925
  state.orchestration.workerBindings[ensuredWorkerSession] = {
21250
21926
  sourceHandle: ensuredWorkerSession,
21251
21927
  coordinatorSession: task.coordinatorSession,
@@ -21316,6 +21992,11 @@ class OrchestrationService {
21316
21992
  task2.status = "cancelled";
21317
21993
  task2.summary = "rejected";
21318
21994
  task2.updatedAt = this.deps.now().toISOString();
21995
+ this.appendTaskEvent(task2, task2.updatedAt, "status_changed", {
21996
+ status: "cancelled",
21997
+ summary: "rejected",
21998
+ message: "Task rejected"
21999
+ });
21319
22000
  await this.deps.saveState(state);
21320
22001
  return { ...task2 };
21321
22002
  });
@@ -22244,6 +22925,20 @@ class OrchestrationService {
22244
22925
  }
22245
22926
  })();
22246
22927
  }
22928
+ appendTaskEvent(task, at, type, details = {}) {
22929
+ const nextSeq = (task.eventSeq ?? 0) + 1;
22930
+ task.eventSeq = nextSeq;
22931
+ const events = task.events ?? [];
22932
+ events.push({
22933
+ seq: nextSeq,
22934
+ at,
22935
+ type,
22936
+ ...details.status ? { status: details.status } : {},
22937
+ ...details.summary ? { summary: details.summary } : {},
22938
+ ...details.message ? { message: details.message } : {}
22939
+ });
22940
+ task.events = events.slice(-MAX_TASK_EVENTS_PER_TASK);
22941
+ }
22247
22942
  }
22248
22943
  function isTerminalTaskStatus2(status) {
22249
22944
  return status === "completed" || status === "failed" || status === "cancelled";
@@ -22275,6 +22970,7 @@ async function sleep2(ms) {
22275
22970
  function isRequestDelegateInput(input) {
22276
22971
  return "sourceKind" in input;
22277
22972
  }
22973
+ var MAX_TASK_EVENTS_PER_TASK = 200;
22278
22974
  var init_orchestration_service = __esm(() => {
22279
22975
  init_quota_errors();
22280
22976
  init_task_wait_timeouts();
@@ -22738,12 +23434,21 @@ async function runConsole(paths, deps) {
22738
23434
  runtimeForGc.orchestration.service.purgeExpiredResetCoordinators({ cutoffDays: 7, trigger: "interval" }).catch(() => {});
22739
23435
  }, 86400000);
22740
23436
  }
22741
- await deps.channels.startAll({
22742
- agent: runtime.agent,
22743
- abortSignal: shutdownController.signal,
22744
- quota: runtime.quota,
22745
- logger: runtime.logger
22746
- });
23437
+ try {
23438
+ await deps.channels.startAll({
23439
+ agent: runtime.agent,
23440
+ abortSignal: shutdownController.signal,
23441
+ quota: runtime.quota,
23442
+ logger: runtime.logger,
23443
+ perfTracer: runtime.perfTracer
23444
+ });
23445
+ } catch (error2) {
23446
+ if (deps.channelStartupPolicy !== "best-effort") {
23447
+ throw error2;
23448
+ }
23449
+ 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) });
23450
+ await waitForShutdown(shutdownController.signal);
23451
+ }
22747
23452
  } finally {
22748
23453
  await runCleanupSequence({
22749
23454
  removeProcessListener,
@@ -22761,6 +23466,14 @@ async function runConsole(paths, deps) {
22761
23466
  });
22762
23467
  }
22763
23468
  }
23469
+ async function waitForShutdown(signal) {
23470
+ if (signal.aborted) {
23471
+ return;
23472
+ }
23473
+ await new Promise((resolve3) => {
23474
+ signal.addEventListener("abort", () => resolve3(), { once: true });
23475
+ });
23476
+ }
22764
23477
  async function runCleanupSequence(input) {
22765
23478
  let cleanupError = null;
22766
23479
  input.removeProcessListener("SIGINT", input.signalHandler);
@@ -23782,12 +24495,12 @@ var init_streaming_prompt = __esm(() => {
23782
24495
 
23783
24496
  // src/transport/acpx-cli/node-pty-helper.ts
23784
24497
  import { chmod as chmodFs } from "node:fs/promises";
23785
- import { dirname as dirname10, join as join12 } from "node:path";
24498
+ import { dirname as dirname12, join as join12 } from "node:path";
23786
24499
  function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
23787
24500
  if (platform === "win32") {
23788
24501
  return null;
23789
24502
  }
23790
- return join12(dirname10(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
24503
+ return join12(dirname12(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
23791
24504
  }
23792
24505
  async function ensureNodePtyHelperExecutable(helperPath, chmod2 = chmodFs) {
23793
24506
  if (!helperPath) {
@@ -24622,7 +25335,7 @@ __export(exports_main, {
24622
25335
  });
24623
25336
  import { randomUUID as randomUUID3 } from "node:crypto";
24624
25337
  import { homedir as homedir9 } from "node:os";
24625
- import { dirname as dirname11, join as join14 } from "node:path";
25338
+ import { dirname as dirname13, join as join14 } from "node:path";
24626
25339
  import { fileURLToPath as fileURLToPath4 } from "node:url";
24627
25340
  function startProgressHeartbeat(orchestration, config2, logger2, channel) {
24628
25341
  const thresholdSeconds = config2.orchestration.progressHeartbeatSeconds;
@@ -24660,6 +25373,11 @@ async function buildApp(paths, deps = {}) {
24660
25373
  const config2 = await loadConfig(paths.configPath, {
24661
25374
  defaultLoggingLevel: deps.defaultLoggingLevel
24662
25375
  });
25376
+ const reloadRuntimeConfig = async () => {
25377
+ const updated = await configStore.load();
25378
+ replaceRuntimeConfig(config2, updated);
25379
+ return config2;
25380
+ };
24663
25381
  const logger2 = createAppLogger({
24664
25382
  filePath: resolveAppLogPath(paths.configPath),
24665
25383
  level: config2.logging.level,
@@ -24669,6 +25387,15 @@ async function buildApp(paths, deps = {}) {
24669
25387
  now: deps.loggerNow
24670
25388
  });
24671
25389
  await logger2.cleanup();
25390
+ const perfLogPath = paths.perfLogPath ?? resolvePerfLogPath(paths.configPath);
25391
+ const perfTracer = config2.logging.perf.enabled ? createPerfTracer({
25392
+ filePath: perfLogPath,
25393
+ maxSizeBytes: config2.logging.perf.maxSizeBytes,
25394
+ maxFiles: config2.logging.perf.maxFiles,
25395
+ retentionDays: config2.logging.perf.retentionDays,
25396
+ appLogger: logger2
25397
+ }) : createNoopPerfTracer();
25398
+ await perfTracer.cleanup();
24672
25399
  const acpxCommand = resolveAcpxCommand({ configuredCommand: config2.transport.command });
24673
25400
  const stateStore = new StateStore(paths.statePath);
24674
25401
  const state = await stateStore.load();
@@ -24849,32 +25576,39 @@ async function buildApp(paths, deps = {}) {
24849
25576
  };
24850
25577
  };
24851
25578
  const launchWorkerTurn = (input) => {
24852
- const session = resolveWorkerRuntimeSession(input);
24853
- session.mcpCoordinatorSession = input.coordinatorSession;
24854
- session.mcpSourceHandle = input.workerSession;
24855
25579
  const workerDispatch = (async () => {
24856
25580
  let taskRecord;
24857
25581
  try {
25582
+ await reloadRuntimeConfig();
25583
+ const session = resolveWorkerRuntimeSession(input);
25584
+ session.mcpCoordinatorSession = input.coordinatorSession;
25585
+ session.mcpSourceHandle = input.workerSession;
24858
25586
  const progressBuffer = new ProgressLineBuffer;
25587
+ const recordProgress = async (summary) => {
25588
+ try {
25589
+ await orchestration.recordTaskProgress(input.taskId, summary);
25590
+ const taskState = await orchestration.getTask(input.taskId);
25591
+ if (taskState?.chatKey && taskState.replyContextToken && deps.channel) {
25592
+ await deps.channel.notifyTaskProgress(taskState, renderTaskProgress(taskState, summary));
25593
+ }
25594
+ } catch (error2) {
25595
+ await logger2.error("orchestration.progress.send_failed", "failed to send task progress", {
25596
+ taskId: input.taskId,
25597
+ message: error2 instanceof Error ? error2.message : String(error2)
25598
+ });
25599
+ }
25600
+ };
24859
25601
  const result = await transport.prompt(session, input.promptText, undefined, undefined, {
24860
25602
  onSegment: async (chunk) => {
24861
- const summaries = progressBuffer.feed(chunk);
25603
+ const summaries = progressBuffer.feed(chunk, { segmentComplete: true });
24862
25604
  for (const summary of summaries) {
24863
- try {
24864
- await orchestration.recordTaskProgress(input.taskId);
24865
- const taskState = await orchestration.getTask(input.taskId);
24866
- if (taskState?.chatKey && taskState.replyContextToken && deps.channel) {
24867
- await deps.channel.notifyTaskProgress(taskState, renderTaskProgress(taskState, summary));
24868
- }
24869
- } catch (error2) {
24870
- await logger2.error("orchestration.progress.send_failed", "failed to send task progress", {
24871
- taskId: input.taskId,
24872
- message: error2 instanceof Error ? error2.message : String(error2)
24873
- });
24874
- }
25605
+ await recordProgress(summary);
24875
25606
  }
24876
25607
  }
24877
25608
  });
25609
+ for (const summary of progressBuffer.flush()) {
25610
+ await recordProgress(summary);
25611
+ }
24878
25612
  taskRecord = await finalizeWorkerTurn({
24879
25613
  taskId: input.taskId,
24880
25614
  workerSession: input.workerSession,
@@ -24938,6 +25672,7 @@ async function buildApp(paths, deps = {}) {
24938
25672
  },
24939
25673
  stateMutex,
24940
25674
  ensureWorkerSession: async ({ workerSession, targetAgent, workspace, cwd, coordinatorSession }) => {
25675
+ await reloadRuntimeConfig();
24941
25676
  const session = resolveWorkerRuntimeSession({ workerSession, targetAgent, workspace, ...cwd ? { cwd } : {} });
24942
25677
  session.mcpCoordinatorSession = coordinatorSession;
24943
25678
  session.mcpSourceHandle = workerSession;
@@ -25027,6 +25762,7 @@ async function buildApp(paths, deps = {}) {
25027
25762
  stateStore,
25028
25763
  configStore,
25029
25764
  logger: logger2,
25765
+ perfTracer,
25030
25766
  quota,
25031
25767
  transport,
25032
25768
  orchestration: {
@@ -25043,6 +25779,13 @@ async function buildApp(paths, deps = {}) {
25043
25779
  if ("dispose" in transport && typeof transport.dispose === "function") {
25044
25780
  await transport.dispose();
25045
25781
  }
25782
+ try {
25783
+ await perfTracer.flush();
25784
+ } catch (err) {
25785
+ await logger2.error("perf.flush_failed", "perf tracer flush failed during shutdown", {
25786
+ error: err instanceof Error ? err.message : String(err)
25787
+ }).catch(() => {});
25788
+ }
25046
25789
  await logger2.flush();
25047
25790
  }
25048
25791
  };
@@ -25052,6 +25795,9 @@ function replaceRuntimeState(target, source) {
25052
25795
  target.chat_contexts = source.chat_contexts;
25053
25796
  target.orchestration = source.orchestration;
25054
25797
  }
25798
+ function replaceRuntimeConfig(target, source) {
25799
+ Object.assign(target, source);
25800
+ }
25055
25801
  async function main() {
25056
25802
  const paths = resolveRuntimePaths();
25057
25803
  try {
@@ -25078,7 +25824,7 @@ async function main() {
25078
25824
  }
25079
25825
  }
25080
25826
  async function prepareChannelMedia(configPath, config2) {
25081
- const runtimeDir = join14(dirname11(configPath), "runtime");
25827
+ const runtimeDir = join14(dirname13(configPath), "runtime");
25082
25828
  const mediaRootDir = join14(runtimeDir, "media");
25083
25829
  const mediaStore = new RuntimeMediaStore({ rootDir: mediaRootDir });
25084
25830
  await mediaStore.cleanupExpired().catch((error2) => {
@@ -25093,10 +25839,11 @@ function resolveRuntimePaths() {
25093
25839
  throw new Error("Unable to resolve the current user home directory");
25094
25840
  }
25095
25841
  const configPath = process.env.WEACPX_CONFIG ?? `${home}/.weacpx/config.json`;
25096
- const runtimeDir = join14(dirname11(configPath), "runtime");
25842
+ const runtimeDir = join14(dirname13(configPath), "runtime");
25097
25843
  return {
25098
25844
  configPath,
25099
25845
  statePath: process.env.WEACPX_STATE ?? `${home}/.weacpx/state.json`,
25846
+ perfLogPath: join14(runtimeDir, "perf.log"),
25100
25847
  orchestrationSocketPath: process.env.WEACPX_ORCHESTRATION_SOCKET ?? resolveDaemonOrchestrationSocketPath(runtimeDir)
25101
25848
  };
25102
25849
  }
@@ -25107,10 +25854,15 @@ function resolveBridgeEntryPath() {
25107
25854
  return fileURLToPath4(new URL("./bridge/bridge-main.ts", import.meta.url));
25108
25855
  }
25109
25856
  function resolveAppLogPath(configPath) {
25110
- const rootDir = dirname11(configPath);
25857
+ const rootDir = dirname13(configPath);
25111
25858
  const runtimeDir = join14(rootDir, "runtime");
25112
25859
  return join14(runtimeDir, "app.log");
25113
25860
  }
25861
+ function resolvePerfLogPath(configPath) {
25862
+ const rootDir = dirname13(configPath);
25863
+ const runtimeDir = join14(rootDir, "runtime");
25864
+ return join14(runtimeDir, "perf.log");
25865
+ }
25114
25866
  function resolveOrchestrationSocketPathFromConfigPath(configPath) {
25115
25867
  const runtimeDir = resolveRuntimeDirFromConfigPath(configPath);
25116
25868
  return resolveDaemonOrchestrationSocketPath(runtimeDir);
@@ -25141,6 +25893,7 @@ var init_main = __esm(async () => {
25141
25893
  init_media_store();
25142
25894
  init_quota_errors();
25143
25895
  init_inbound();
25896
+ init_perf_tracer();
25144
25897
  init_bootstrap();
25145
25898
  if (false) {}
25146
25899
  });
@@ -25483,7 +26236,7 @@ async function checkOrchestrationHealth(options) {
25483
26236
  // src/doctor/checks/runtime-check.ts
25484
26237
  import { constants } from "node:fs";
25485
26238
  import { access as access4, stat as stat3 } from "node:fs/promises";
25486
- import { dirname as dirname12 } from "node:path";
26239
+ import { dirname as dirname14 } from "node:path";
25487
26240
  import { homedir as homedir11 } from "node:os";
25488
26241
  async function checkRuntime(options = {}) {
25489
26242
  const home = options.home ?? process.env.HOME ?? homedir11();
@@ -25580,7 +26333,7 @@ async function checkFileCreatable(label, path14, probe, platform) {
25580
26333
  detail: `${label}: ${path14} (unusable: ${formatError6(error2)})`
25581
26334
  };
25582
26335
  }
25583
- const parentCheck = await checkCreatableAncestorDirectory(dirname12(path14), probe, platform);
26336
+ const parentCheck = await checkCreatableAncestorDirectory(dirname14(path14), probe, platform);
25584
26337
  if (!parentCheck.ok) {
25585
26338
  return {
25586
26339
  ok: false,
@@ -25616,7 +26369,7 @@ async function checkCreatableAncestorDirectory(path14, probe, platform) {
25616
26369
  blockingPath: path14
25617
26370
  };
25618
26371
  }
25619
- const parent = dirname12(path14);
26372
+ const parent = dirname14(path14);
25620
26373
  if (parent === path14) {
25621
26374
  return {
25622
26375
  ok: false,
@@ -26194,11 +26947,12 @@ var init_doctor2 = __esm(async () => {
26194
26947
  init_config_store();
26195
26948
  init_load_config();
26196
26949
  init_ensure_config();
26950
+ init_agent_templates();
26197
26951
  init_create_daemon_controller();
26198
26952
  init_daemon_files();
26199
26953
  import { randomUUID as randomUUID4 } from "node:crypto";
26200
26954
  import { homedir as homedir13 } from "node:os";
26201
- import { dirname as dirname13, join as join16, sep } from "node:path";
26955
+ import { dirname as dirname15, join as join16, sep } from "node:path";
26202
26956
  import { fileURLToPath as fileURLToPath6 } from "node:url";
26203
26957
 
26204
26958
  // src/daemon/daemon-runtime.ts
@@ -38648,6 +39402,7 @@ var taskStatusSchema = exports_external.enum([
38648
39402
  var sortSchema = exports_external.enum(["updatedAt", "createdAt"]);
38649
39403
  var orderSchema = exports_external.enum(["asc", "desc"]);
38650
39404
  var contestedDecisionSchema = exports_external.enum(["accept", "discard"]);
39405
+ var taskWatchModeSchema = exports_external.enum(["next_event", "until_attention_or_terminal"]);
38651
39406
  var taskQuestionSchema = exports_external.object({
38652
39407
  taskId: exports_external.string().min(1),
38653
39408
  questionId: exports_external.string().min(1)
@@ -38657,7 +39412,8 @@ function buildWeacpxMcpToolRegistry(input) {
38657
39412
  const tools = [
38658
39413
  {
38659
39414
  name: "delegate_request",
38660
- description: `Delegate a subtask to another agent under the current coordinator. Pass an absolute workingDirectory for the worker. After this returns status=running, call task_wait with the returned taskId to wait for completion before reporting back to the user; if status=needs_confirmation, wait for the user to approve (task_approve / task_reject) and do not call task_wait yet.${availableAgents && availableAgents.length > 0 ? ` Available agents: ${availableAgents.join(", ")}.` : ""}`,
39415
+ description: `Delegate a subtask to another agent under the current coordinator. Pass an absolute workingDirectory for the worker. Supports MCP Tasks when the client requests task execution: the tool can return a native task handle immediately, then clients can use tasks/get, tasks/result, tasks/list, and tasks/cancel. For legacy clients, after this returns status=running, keep the returned taskId and use task_get/task_list for non-blocking progress snapshots; call task_wait only when the user explicitly wants you to wait/block until completion or attention is required. If status=needs_confirmation, wait for the user to approve (task_approve / task_reject) and do not call task_wait yet.${availableAgents && availableAgents.length > 0 ? ` Available agents: ${availableAgents.join(", ")}.` : ""}`,
39416
+ execution: { taskSupport: "optional" },
38661
39417
  inputSchema: exports_external.object({
38662
39418
  targetAgent: exports_external.string().min(1),
38663
39419
  task: exports_external.string().min(1),
@@ -38740,7 +39496,7 @@ function buildWeacpxMcpToolRegistry(input) {
38740
39496
  },
38741
39497
  {
38742
39498
  name: "task_get",
38743
- description: "Fetch a single task under the current coordinator, including the worker's final result and any pending question. Use after task_wait returns to read the actual output before summarizing it for the user, or to inspect a task that requires attention.",
39499
+ description: "Fetch a single task under the current coordinator, including the worker's final result and any pending question. Use to inspect a task snapshot non-blockingly, read the actual output after completion, or inspect a task that requires attention.",
38744
39500
  inputSchema: exports_external.object({
38745
39501
  taskId: exports_external.string().min(1)
38746
39502
  }).strict(),
@@ -38775,7 +39531,7 @@ function buildWeacpxMcpToolRegistry(input) {
38775
39531
  },
38776
39532
  {
38777
39533
  name: "task_approve",
38778
- description: "Approve a pending task under the current coordinator. Use when delegate_request returned status=needs_confirmation and the user has authorized it; after approval, call task_wait.",
39534
+ description: "Approve a pending task under the current coordinator. Use when delegate_request returned status=needs_confirmation and the user has authorized it; after approval, use task_get/task_list for snapshots or task_wait only if intentionally blocking.",
38779
39535
  inputSchema: exports_external.object({
38780
39536
  taskId: exports_external.string().min(1)
38781
39537
  }).strict(),
@@ -38817,7 +39573,7 @@ function buildWeacpxMcpToolRegistry(input) {
38817
39573
  },
38818
39574
  {
38819
39575
  name: "task_wait",
38820
- description: `Wait for a task to finish or require attention. Call this immediately after delegate_request (when status=running) unless you intend a fire-and-forget. Returns status=terminal (done; call task_get for the result), status=attention_required (call task_get first to read the task's current status, then branch: needs_confirmation -> task_approve or task_reject; blocked or waiting_for_human -> coordinator_answer_question; reviewPending set -> coordinator_review_contested_result; after resolving, call task_wait again), or status=timeout (still running; call task_wait again or task_get for a snapshot). Defaults: timeout ${DEFAULT_TASK_WAIT_TIMEOUT_MS} ms, poll interval ${DEFAULT_TASK_WAIT_POLL_INTERVAL_MS} ms. Maximums: timeout ${MAX_TASK_WAIT_TIMEOUT_MS} ms, poll interval ${MAX_TASK_WAIT_POLL_INTERVAL_MS} ms.`,
39576
+ description: `Wait for a task to finish or require attention. This is a blocking legacy compatibility tool; do not call it automatically after delegate_request when the user only asked to start/delegate work. Use task_get/task_list for non-blocking progress snapshots; call task_wait when the user explicitly asks to wait, or when your next step truly depends on completion. Returns status=terminal (done; call task_get for the result), status=attention_required (call task_get first to read the task's current status, then branch: needs_confirmation -> task_approve or task_reject; blocked or waiting_for_human -> coordinator_answer_question; reviewPending set -> coordinator_review_contested_result; after resolving, use task_get/task_list snapshots or task_wait only if intentionally blocking), or status=timeout (still running; use task_get for a snapshot, or task_wait only if intentionally blocking). Defaults: timeout ${DEFAULT_TASK_WAIT_TIMEOUT_MS} ms, poll interval ${DEFAULT_TASK_WAIT_POLL_INTERVAL_MS} ms. Maximums: timeout ${MAX_TASK_WAIT_TIMEOUT_MS} ms, poll interval ${MAX_TASK_WAIT_POLL_INTERVAL_MS} ms.`,
38821
39577
  inputSchema: exports_external.object({
38822
39578
  taskId: exports_external.string().min(1),
38823
39579
  timeoutMs: exports_external.number().int().min(0).max(MAX_TASK_WAIT_TIMEOUT_MS).optional(),
@@ -38831,9 +39587,29 @@ function buildWeacpxMcpToolRegistry(input) {
38831
39587
  return createSuccessResult(renderTaskWaitResult(result), result);
38832
39588
  })
38833
39589
  },
39590
+ {
39591
+ name: "task_watch",
39592
+ description: `Long-poll a task for the next event, attention-required state, or terminal state. This is the recommended legacy way to watch a delegated task without repeatedly calling task_wait. For MCP-task-capable clients, request task execution for this tool to create a background watcher: the call returns a native task handle immediately, and tasks/result returns when the watch condition is met. The native watcher is single-shot: it runs one watch cycle then terminates, so to keep watching start another task_watch with afterSeq set to the returned nextAfterSeq. Defaults: timeout ${DEFAULT_TASK_WATCH_TIMEOUT_MS} ms, poll interval ${DEFAULT_TASK_WATCH_POLL_INTERVAL_MS} ms. Maximums: timeout ${MAX_TASK_WATCH_TIMEOUT_MS} ms, poll interval ${MAX_TASK_WATCH_POLL_INTERVAL_MS} ms.`,
39593
+ execution: { taskSupport: "optional" },
39594
+ inputSchema: exports_external.object({
39595
+ taskId: exports_external.string().min(1),
39596
+ afterSeq: exports_external.number().int().min(0).optional(),
39597
+ mode: taskWatchModeSchema.optional(),
39598
+ includeProgress: exports_external.boolean().optional(),
39599
+ timeoutMs: exports_external.number().int().min(0).max(MAX_TASK_WATCH_TIMEOUT_MS).optional(),
39600
+ pollIntervalMs: exports_external.number().int().min(1).max(MAX_TASK_WATCH_POLL_INTERVAL_MS).optional()
39601
+ }).strict(),
39602
+ handler: async (args) => await asToolResult(async () => {
39603
+ const result = await transport.watchTask({
39604
+ coordinatorSession,
39605
+ ...args
39606
+ });
39607
+ return createSuccessResult(renderTaskWatchResult(result), result);
39608
+ })
39609
+ },
38834
39610
  {
38835
39611
  name: "worker_raise_question",
38836
- description: "Raise a blocker question for the current bound worker session. Worker-side only: call this from inside a delegated task when you are blocked and need the coordinator's input. Coordinators waiting on a delegation should not call this; use task_wait instead.",
39612
+ description: "Raise a blocker question for the current bound worker session. Worker-side only: call this from inside a delegated task when you are blocked and need the coordinator's input. Coordinators waiting on a delegation should not call this; use task_get/task_list for snapshots, or task_wait only if intentionally blocking.",
38837
39613
  inputSchema: exports_external.object({
38838
39614
  taskId: exports_external.string().min(1),
38839
39615
  question: exports_external.string().min(1),
@@ -38853,7 +39629,7 @@ function buildWeacpxMcpToolRegistry(input) {
38853
39629
  },
38854
39630
  {
38855
39631
  name: "coordinator_answer_question",
38856
- description: "Answer a blocked worker question under the current coordinator. Use after task_wait returns status=attention_required and task_get shows a pending question; after answering, call task_wait again to keep waiting for the worker to finish.",
39632
+ description: "Answer a blocked worker question under the current coordinator. Use when task_get shows a pending question; after answering, use task_get/task_list for snapshots or task_wait only if intentionally blocking for the worker to finish.",
38857
39633
  inputSchema: exports_external.object({
38858
39634
  taskId: exports_external.string().min(1),
38859
39635
  questionId: exports_external.string().min(1),
@@ -38957,7 +39733,7 @@ function renderTaskWaitResult(result) {
38957
39733
  if (result.status === "timeout") {
38958
39734
  return [
38959
39735
  `Task ${result.task.taskId} wait timed out; current state is ${result.task.status}.`,
38960
- `Next: call task_wait again with this taskId to keep waiting, or task_get for a snapshot.`
39736
+ `Next: call task_get for a snapshot, or call task_wait again only if you intentionally want to keep blocking.`
38961
39737
  ].join(`
38962
39738
  `);
38963
39739
  }
@@ -38968,7 +39744,7 @@ function renderTaskWaitResult(result) {
38968
39744
  ` - status=needs_confirmation -> task_approve or task_reject`,
38969
39745
  ` - status=blocked or waiting_for_human -> coordinator_answer_question`,
38970
39746
  ` - reviewPending set -> coordinator_review_contested_result`,
38971
- `After resolving, call task_wait again to keep waiting for the worker to finish.`
39747
+ `After resolving, use task_get/task_list for snapshots, or call task_wait again only if you intentionally want to keep blocking.`
38972
39748
  ].join(`
38973
39749
  `);
38974
39750
  }
@@ -38978,6 +39754,26 @@ function renderTaskWaitResult(result) {
38978
39754
  ].join(`
38979
39755
  `);
38980
39756
  }
39757
+ function renderTaskWatchResult(result) {
39758
+ if (result.status === "not_found" || !result.task) {
39759
+ return "Task not found.";
39760
+ }
39761
+ const header = [
39762
+ `Task ${result.task.taskId} watch ${result.status.replace("_", " ")}; current state is ${result.task.status}.`,
39763
+ `- nextAfterSeq: ${result.nextAfterSeq}`,
39764
+ result.historyTruncated ? "- historyTruncated: true" : ""
39765
+ ].filter((line) => line.length > 0);
39766
+ const events = result.events.length > 0 ? [
39767
+ "- Events:",
39768
+ ...result.events.map((event) => {
39769
+ const detail = event.summary ?? event.message ?? event.status ?? "";
39770
+ return ` - #${event.seq} ${event.type} at ${event.at}${detail ? `: ${detail}` : ""}`;
39771
+ })
39772
+ ] : ["- Events: none"];
39773
+ const next = result.status === "terminal" ? "Next: call task_get to read the final result." : result.status === "attention_required" ? "Next: call task_get to inspect openQuestion / reviewPending, then resolve it with the recommended action tool." : `Next: call task_watch again with afterSeq=${result.nextAfterSeq} to keep watching, preferably as an MCP task if your client supports background task execution.`;
39774
+ return [...header, ...events, next].join(`
39775
+ `);
39776
+ }
38981
39777
  function createSuccessResult(text, structuredContent) {
38982
39778
  return {
38983
39779
  content: [{ type: "text", text }],
@@ -38991,7 +39787,7 @@ function createErrorResult(message) {
38991
39787
  };
38992
39788
  }
38993
39789
  function renderDelegateSuccess(result) {
38994
- const next = result.status === "needs_confirmation" ? `Next: this delegation requires user approval; do not call task_wait yet. Tell the user, then call task_approve or task_reject based on their response.` : `Next: call task_wait with taskId="${result.taskId}" to wait for the worker to finish, then task_get to read the result before reporting back.`;
39790
+ const next = result.status === "needs_confirmation" ? `Next: this delegation requires user approval; do not call task_wait yet. Tell the user, then call task_approve or task_reject based on their response.` : `Next: task "${result.taskId}" is running. Return this taskId to the user, or call task_get/task_list for non-blocking progress snapshots (or task_watch to long-poll for the next event). Call task_wait only if the user explicitly asks you to wait/block until completion.`;
38995
39791
  return [`Delegation task "${result.taskId}" created.`, `- Status: ${result.status}`, next].join(`
38996
39792
  `);
38997
39793
  }
@@ -39096,6 +39892,8 @@ function renderTaskSummary(task) {
39096
39892
  header.push(`- Task: ${task.task}`);
39097
39893
  if (task.summary.trim().length > 0)
39098
39894
  header.push(`- Summary: ${task.summary}`);
39895
+ if (task.lastProgressSummary)
39896
+ header.push(`- Latest progress: ${task.lastProgressSummary}`);
39099
39897
  if (task.resultText.trim().length > 0)
39100
39898
  header.push(`- Result: ${task.resultText}`);
39101
39899
  const events = [];
@@ -39140,7 +39938,7 @@ function renderTaskApprovalSuccess(task) {
39140
39938
  return [
39141
39939
  `Task "${task.taskId}" approved.`,
39142
39940
  `- Current status: ${task.status}`,
39143
- `Next: call task_wait with taskId="${task.taskId}" to wait for the worker to finish, then task_get to read the result before reporting back.`
39941
+ `Next: use task_get/task_list for non-blocking progress snapshots, or call task_wait only if you intentionally want to block until the worker finishes; then task_get to read the final result.`
39144
39942
  ].join(`
39145
39943
  `);
39146
39944
  }
@@ -39210,6 +40008,9 @@ class OrchestrationClient {
39210
40008
  async waitTask(input) {
39211
40009
  return await this.request("task.wait", input, getWaitRequestTimeoutMs(input.timeoutMs, this.timeoutMs));
39212
40010
  }
40011
+ async watchTask(input) {
40012
+ return await this.request("task.watch", input, getWatchRequestTimeoutMs(input.timeoutMs, this.timeoutMs));
40013
+ }
39213
40014
  async approveTask(input) {
39214
40015
  return await this.request("task.approve", input);
39215
40016
  }
@@ -39326,6 +40127,11 @@ function getWaitRequestTimeoutMs(waitTimeoutMs, defaultTimeoutMs) {
39326
40127
  const boundedWaitTimeoutMs = Math.min(Math.max(Math.floor(requestedWaitTimeoutMs ?? DEFAULT_TASK_WAIT_TIMEOUT_MS), 0), MAX_TASK_WAIT_TIMEOUT_MS);
39327
40128
  return Math.max(defaultTimeoutMs, boundedWaitTimeoutMs + TASK_WAIT_RPC_TIMEOUT_PADDING_MS);
39328
40129
  }
40130
+ function getWatchRequestTimeoutMs(watchTimeoutMs, defaultTimeoutMs) {
40131
+ const requestedWatchTimeoutMs = watchTimeoutMs === undefined ? undefined : Number.isFinite(watchTimeoutMs) ? watchTimeoutMs : 0;
40132
+ const boundedWatchTimeoutMs = Math.min(Math.max(Math.floor(requestedWatchTimeoutMs ?? DEFAULT_TASK_WATCH_TIMEOUT_MS), 0), MAX_TASK_WATCH_TIMEOUT_MS);
40133
+ return Math.max(defaultTimeoutMs, boundedWatchTimeoutMs + TASK_WATCH_RPC_TIMEOUT_PADDING_MS);
40134
+ }
39329
40135
 
39330
40136
  // src/mcp/weacpx-mcp-transport.ts
39331
40137
  function createOrchestrationTransport(endpoint, deps = {}) {
@@ -39355,6 +40161,7 @@ function createOrchestrationTransport(endpoint, deps = {}) {
39355
40161
  rejectTask: async (input) => await client.rejectTask(input),
39356
40162
  cancelTask: async (input) => await client.cancelTaskForCoordinator(input),
39357
40163
  waitTask: async (input) => await client.waitTask(input),
40164
+ watchTask: async (input) => await client.watchTask(input),
39358
40165
  workerRaiseQuestion: async (input) => {
39359
40166
  const sourceHandle = input.sourceHandle.trim();
39360
40167
  if (sourceHandle.length === 0) {
@@ -39376,21 +40183,31 @@ function createOrchestrationTransport(endpoint, deps = {}) {
39376
40183
  }
39377
40184
 
39378
40185
  // src/mcp/weacpx-mcp-server.ts
40186
+ var TASK_OPTIONS_CACHE_LIMIT = 1000;
40187
+ var TASKS_LIST_PAGE_SIZE = 100;
40188
+ var WATCH_TASKS_CACHE_LIMIT = 256;
39379
40189
  var WEACPX_MCP_SERVER_INSTRUCTIONS = [
39380
40190
  "Use these tools to orchestrate work across other agents under your coordinator session.",
39381
40191
  "",
39382
40192
  "Typical lifecycle for a single delegation:",
40193
+ "Preferred MCP Tasks lifecycle (for clients that support task-augmented tools/call):",
40194
+ "1. Call delegate_request with task execution requested. It returns a native MCP task handle immediately.",
40195
+ "2. Use task_watch with MCP task execution to start a background watcher, or use tasks/get / tasks/list to poll status. Use tasks/result after terminal status, or on input_required to receive an actionable next-step package; use tasks/cancel to cancel.",
40196
+ " - When tasks/result returns input_required, that result stream is complete. Call the recommended tool, then resume polling with tasks/get / tasks/result.",
40197
+ "3. Status mapping: working = running, input_required = needs_confirmation / blocked / waiting_for_human / contested review, completed / failed / cancelled are terminal.",
40198
+ "",
40199
+ "Legacy tool lifecycle for clients without MCP Tasks support:",
39383
40200
  "1. delegate_request → returns { taskId, status }.",
39384
- " - status=running: the worker has started; go to step 2.",
39385
- " - status=needs_confirmation: tell the user, then call task_approve or task_reject based on their response. After task_approve, return to step 2 to wait for the worker. Do not call task_wait before approval.",
39386
- "2. task_wait(taskId) → blocks until the task is done, needs attention, or times out.",
40201
+ " - status=running: the worker has started. Return the taskId to the user, use task_get / task_list for non-blocking snapshots, or task_watch to long-poll for the next event; only go to step 2 when you intentionally want to block waiting.",
40202
+ " - status=needs_confirmation: tell the user, then call task_approve or task_reject based on their response. After task_approve, use task_get/task_list for snapshots or step 2 only if intentionally blocking. Do not call task_wait before approval.",
40203
+ "2. Optional blocking wait: task_wait(taskId) → blocks until the task is done, needs attention, or times out. Do not call it automatically when the user asked to delegate and continue.",
39387
40204
  " - status=terminal: go to step 3.",
39388
40205
  " - status=attention_required: the task is in needs_confirmation / blocked / waiting_for_human, or has reviewPending set. Call task_get(taskId) to read the actual status and any openQuestion / reviewPending fields, then branch:",
39389
- " * needs_confirmation -> task_approve or task_reject (after approval, go back to step 2)",
40206
+ " * needs_confirmation -> task_approve or task_reject (after approval, use snapshots or optional blocking wait only if needed)",
39390
40207
  " * blocked or waiting_for_human -> coordinator_answer_question (the answer can come from you or be relayed from a human you consulted)",
39391
40208
  " * reviewPending set -> coordinator_review_contested_result with accept or discard",
39392
- " After resolving, call task_wait again to keep waiting.",
39393
- " - status=timeout: the task is still running. Call task_wait again to keep waiting, or task_get for a snapshot.",
40209
+ " After resolving, use task_get / task_list for snapshots, or step 2 only if intentionally blocking.",
40210
+ " - status=timeout: the task is still running. Use task_get for a snapshot, or call task_wait again only if you still intentionally want to block.",
39394
40211
  "3. The task is terminal. Call task_get(taskId) to read the worker's final result, then summarize it for the user. Do not invent results that did not come from task_get.",
39395
40212
  "",
39396
40213
  "Batching: use group_new before a wave of delegate_request calls and pass groupId on each, then group_get / group_list / group_cancel to manage the batch.",
@@ -39401,18 +40218,27 @@ var WEACPX_MCP_SERVER_INSTRUCTIONS = [
39401
40218
  ].join(`
39402
40219
  `);
39403
40220
  function createWeacpxMcpServer(options) {
40221
+ let getToolState;
40222
+ const taskOptionsById = new Map;
40223
+ const watchTasksById = new Map;
39404
40224
  const server = new Server({
39405
40225
  name: "weacpx-orchestration",
39406
40226
  version: readVersion()
39407
40227
  }, {
39408
40228
  capabilities: {
39409
- tools: {}
40229
+ tools: {},
40230
+ tasks: {
40231
+ list: {},
40232
+ cancel: {},
40233
+ requests: { tools: { call: {} } }
40234
+ }
39410
40235
  },
39411
- instructions: WEACPX_MCP_SERVER_INSTRUCTIONS
40236
+ instructions: WEACPX_MCP_SERVER_INSTRUCTIONS,
40237
+ taskStore: createWeacpxTaskStore(async () => await getToolState(), taskOptionsById, watchTasksById)
39412
40238
  });
39413
40239
  let toolState = null;
39414
40240
  let toolStatePromise = null;
39415
- async function getToolState() {
40241
+ getToolState = async function getToolState2() {
39416
40242
  if (toolState) {
39417
40243
  return toolState;
39418
40244
  }
@@ -39435,14 +40261,15 @@ function createWeacpxMcpServer(options) {
39435
40261
  toolStatePromise = null;
39436
40262
  });
39437
40263
  return await toolStatePromise;
39438
- }
40264
+ };
39439
40265
  server.setRequestHandler(ListToolsRequestSchema, async () => {
39440
40266
  const tools = (await getToolState()).tools;
39441
40267
  return {
39442
40268
  tools: tools.map((tool) => ({
39443
40269
  name: tool.name,
39444
40270
  description: tool.description,
39445
- inputSchema: normalizeInputSchemaJson(zodToJsonSchema(tool.inputSchema))
40271
+ inputSchema: normalizeInputSchemaJson(zodToJsonSchema(tool.inputSchema)),
40272
+ ...tool.execution ? { execution: tool.execution } : {}
39446
40273
  }))
39447
40274
  };
39448
40275
  });
@@ -39456,15 +40283,399 @@ function createWeacpxMcpServer(options) {
39456
40283
  if (!parsed.success) {
39457
40284
  throw new McpError(ErrorCode.InvalidParams, formatZodError(parsed.error));
39458
40285
  }
40286
+ if (request.params.task) {
40287
+ if (tool.name !== "delegate_request" && tool.name !== "task_watch") {
40288
+ throw new McpError(ErrorCode.InvalidParams, `Tool ${tool.name} does not support MCP task execution`);
40289
+ }
40290
+ if (tool.name === "delegate_request") {
40291
+ return await createDelegationMcpTask({
40292
+ state: await getToolState(),
40293
+ args: parsed.data,
40294
+ taskParams: request.params.task,
40295
+ taskOptionsById
40296
+ });
40297
+ }
40298
+ return await createWatchMcpTask({
40299
+ state: await getToolState(),
40300
+ args: parsed.data,
40301
+ taskParams: request.params.task,
40302
+ taskOptionsById,
40303
+ watchTasksById
40304
+ });
40305
+ }
39459
40306
  return await tool.handler(parsed.data);
39460
40307
  });
40308
+ server.setRequestHandler(GetTaskPayloadRequestSchema, async (request) => {
40309
+ const watchTask = watchTasksById.get(request.params.taskId);
40310
+ if (watchTask) {
40311
+ if (!watchTask.result) {
40312
+ throw new McpError(ErrorCode.InvalidRequest, `Task ${request.params.taskId} is still ${watchTask.task.status}`);
40313
+ }
40314
+ watchTasksById.delete(request.params.taskId);
40315
+ return watchTask.result;
40316
+ }
40317
+ const state = await getToolState();
40318
+ const task = await state.transport.getTask({
40319
+ coordinatorSession: state.coordinatorSession,
40320
+ taskId: request.params.taskId
40321
+ });
40322
+ if (!task) {
40323
+ throw new McpError(ErrorCode.InvalidParams, `Task not found: ${request.params.taskId}`);
40324
+ }
40325
+ return withRelatedTaskMeta(renderNativeTaskPayloadResult(task), task.taskId);
40326
+ });
39461
40327
  return server;
39462
40328
  }
39463
40329
  function buildToolState(options) {
39464
40330
  const tools = buildWeacpxMcpToolRegistry(options);
39465
40331
  return {
39466
40332
  tools,
39467
- toolMap: new Map(tools.map((tool) => [tool.name, tool]))
40333
+ toolMap: new Map(tools.map((tool) => [tool.name, tool])),
40334
+ transport: options.transport,
40335
+ coordinatorSession: options.coordinatorSession,
40336
+ sourceHandle: options.sourceHandle
40337
+ };
40338
+ }
40339
+ async function createDelegationMcpTask(input) {
40340
+ const delegateTool = input.state.toolMap.get("delegate_request");
40341
+ if (!delegateTool) {
40342
+ throw new McpError(ErrorCode.MethodNotFound, "delegate_request is not registered");
40343
+ }
40344
+ const result = await delegateTool.handler(input.args);
40345
+ if (result.isError) {
40346
+ throw new McpError(ErrorCode.InvalidRequest, result.content.map((item) => item.type === "text" ? item.text : "").filter(Boolean).join(`
40347
+ `) || "Delegation failed");
40348
+ }
40349
+ const structured = result.structuredContent;
40350
+ const taskId = typeof structured?.taskId === "string" ? structured.taskId : undefined;
40351
+ if (!taskId) {
40352
+ throw new McpError(ErrorCode.InternalError, "delegate_request did not return a taskId");
40353
+ }
40354
+ rememberTaskOptions(input.taskOptionsById, taskId, input.taskParams);
40355
+ const task = await input.state.transport.getTask({
40356
+ coordinatorSession: input.state.coordinatorSession,
40357
+ taskId
40358
+ });
40359
+ if (!task) {
40360
+ throw new McpError(ErrorCode.InternalError, `delegate_request created task "${taskId}" but it was not readable from orchestration state`);
40361
+ }
40362
+ return {
40363
+ task: toMcpTask(task, input.taskParams)
40364
+ };
40365
+ }
40366
+ async function createWatchMcpTask(input) {
40367
+ const taskId = input.args.taskId;
40368
+ if (typeof taskId !== "string" || taskId.length === 0) {
40369
+ throw new McpError(ErrorCode.InvalidParams, "task_watch requires taskId");
40370
+ }
40371
+ const baseTask = await input.state.transport.getTask({
40372
+ coordinatorSession: input.state.coordinatorSession,
40373
+ taskId
40374
+ });
40375
+ if (!baseTask) {
40376
+ throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
40377
+ }
40378
+ const now = new Date().toISOString();
40379
+ const watchTaskId = `watch:${taskId}:${Date.now()}:${Math.random().toString(36).slice(2, 10)}`;
40380
+ rememberTaskOptions(input.taskOptionsById, watchTaskId, input.taskParams);
40381
+ const watchTask = toMcpTask({
40382
+ taskId: watchTaskId,
40383
+ status: "running",
40384
+ summary: `Watching task ${taskId}`,
40385
+ createdAt: now,
40386
+ updatedAt: now
40387
+ }, input.taskParams);
40388
+ registerWatchTask(input.watchTasksById, watchTaskId, { task: watchTask });
40389
+ runWatchMcpTask({
40390
+ state: input.state,
40391
+ args: input.args,
40392
+ watchTaskId,
40393
+ taskOptions: input.taskParams,
40394
+ watchTasksById: input.watchTasksById
40395
+ });
40396
+ return {
40397
+ task: watchTask
40398
+ };
40399
+ }
40400
+ async function runWatchMcpTask(input) {
40401
+ const args = input.args;
40402
+ try {
40403
+ const result = await input.state.transport.watchTask({
40404
+ coordinatorSession: input.state.coordinatorSession,
40405
+ ...args
40406
+ });
40407
+ if (!input.watchTasksById.has(input.watchTaskId))
40408
+ return;
40409
+ const now = new Date().toISOString();
40410
+ const mcpStatus = result.status === "attention_required" ? "input_required" : result.status === "not_found" ? "failed" : "completed";
40411
+ input.watchTasksById.set(input.watchTaskId, {
40412
+ task: {
40413
+ taskId: input.watchTaskId,
40414
+ status: mcpStatus,
40415
+ ttl: input.taskOptions.ttl ?? null,
40416
+ createdAt: input.watchTasksById.get(input.watchTaskId)?.task.createdAt ?? now,
40417
+ lastUpdatedAt: now,
40418
+ ...input.taskOptions.pollInterval !== undefined ? { pollInterval: input.taskOptions.pollInterval } : {},
40419
+ statusMessage: renderWatchTaskStatusMessage(result)
40420
+ },
40421
+ result: withRelatedTaskMeta(renderWatchMcpTaskResult(result, input.watchTaskId), result.task?.taskId ?? input.watchTaskId)
40422
+ });
40423
+ } catch (error2) {
40424
+ if (!input.watchTasksById.has(input.watchTaskId))
40425
+ return;
40426
+ const now = new Date().toISOString();
40427
+ const message = error2 instanceof Error ? error2.message : String(error2);
40428
+ input.watchTasksById.set(input.watchTaskId, {
40429
+ task: {
40430
+ taskId: input.watchTaskId,
40431
+ status: "failed",
40432
+ ttl: input.taskOptions.ttl ?? null,
40433
+ createdAt: input.watchTasksById.get(input.watchTaskId)?.task.createdAt ?? now,
40434
+ lastUpdatedAt: now,
40435
+ statusMessage: message
40436
+ },
40437
+ result: {
40438
+ content: [{ type: "text", text: `Task watch "${input.watchTaskId}" failed: ${message}` }],
40439
+ structuredContent: { watchTaskId: input.watchTaskId, error: message },
40440
+ isError: true
40441
+ }
40442
+ });
40443
+ }
40444
+ }
40445
+ function renderWatchTaskStatusMessage(result) {
40446
+ if (!result.task)
40447
+ return `Watch finished: ${result.status}`;
40448
+ return `Watch finished for ${result.task.taskId}: ${result.status}; task status ${result.task.status}; events ${result.events?.length ?? 0}`;
40449
+ }
40450
+ function renderWatchMcpTaskResult(result, watchTaskId) {
40451
+ if (result.status === "not_found" || !result.task) {
40452
+ return {
40453
+ content: [{ type: "text", text: `Task watch "${watchTaskId}" finished: watched task not found.` }],
40454
+ structuredContent: { watchTaskId, ...result },
40455
+ isError: true
40456
+ };
40457
+ }
40458
+ const header = [
40459
+ `Task watch "${watchTaskId}" finished with ${result.status.replace("_", " ")}.`,
40460
+ `Watched task ${result.task.taskId} is ${result.task.status}.`,
40461
+ `nextAfterSeq: ${result.nextAfterSeq}`,
40462
+ result.historyTruncated ? "historyTruncated: true" : ""
40463
+ ].filter((line) => line.length > 0);
40464
+ const events = result.events.length > 0 ? [
40465
+ "Events:",
40466
+ ...result.events.map((event) => {
40467
+ const detail = event.summary ?? event.message ?? event.status ?? "";
40468
+ return `- #${event.seq} ${event.type} at ${event.at}${detail ? `: ${detail}` : ""}`;
40469
+ })
40470
+ ] : ["Events: none"];
40471
+ const next = result.status === "terminal" ? "Next: call task_get on the watched task to read the final result." : result.status === "attention_required" ? "Next: call task_get on the watched task, then resolve openQuestion / reviewPending with the recommended action tool." : `Next: call task_watch again with afterSeq=${result.nextAfterSeq} to keep watching.`;
40472
+ return {
40473
+ content: [{ type: "text", text: [...header, ...events, next].join(`
40474
+ `) }],
40475
+ structuredContent: { watchTaskId, ...result }
40476
+ };
40477
+ }
40478
+ function createWeacpxTaskStore(resolveState, taskOptionsById, watchTasksById) {
40479
+ return {
40480
+ createTask: async () => {
40481
+ throw new Error("weacpx native MCP tasks are created by delegate_request");
40482
+ },
40483
+ getTask: async (taskId) => {
40484
+ const watchTask = watchTasksById.get(taskId);
40485
+ if (watchTask)
40486
+ return watchTask.task;
40487
+ const state = await resolveState();
40488
+ const task = await state.transport.getTask({ coordinatorSession: state.coordinatorSession, taskId });
40489
+ return task ? toMcpTask(task, taskOptionsById.get(taskId)) : null;
40490
+ },
40491
+ storeTaskResult: async () => {
40492
+ throw new Error("weacpx native MCP task results are stored by orchestration");
40493
+ },
40494
+ getTaskResult: async (taskId) => {
40495
+ const watchTask = watchTasksById.get(taskId);
40496
+ if (watchTask) {
40497
+ if (!watchTask.result) {
40498
+ throw new Error(`Task ${taskId} is still ${watchTask.task.status}`);
40499
+ }
40500
+ watchTasksById.delete(taskId);
40501
+ return watchTask.result;
40502
+ }
40503
+ const state = await resolveState();
40504
+ const task = await state.transport.getTask({ coordinatorSession: state.coordinatorSession, taskId });
40505
+ if (!task) {
40506
+ throw new Error(`Task not found: ${taskId}`);
40507
+ }
40508
+ return renderNativeTaskPayloadResult(task);
40509
+ },
40510
+ updateTaskStatus: async (taskId, status, statusMessage) => {
40511
+ const state = await resolveState();
40512
+ if (status === "cancelled") {
40513
+ await state.transport.cancelTask({ coordinatorSession: state.coordinatorSession, taskId });
40514
+ return;
40515
+ }
40516
+ throw new Error(`weacpx MCP task status is read-only (${status}${statusMessage ? `: ${statusMessage}` : ""})`);
40517
+ },
40518
+ listTasks: async (cursor) => {
40519
+ const state = await resolveState();
40520
+ const tasks = await state.transport.listTasks({
40521
+ coordinatorSession: state.coordinatorSession,
40522
+ sort: "updatedAt",
40523
+ order: "desc"
40524
+ });
40525
+ const watchTasks = Array.from(watchTasksById.values()).map((record3) => record3.task);
40526
+ pruneTaskOptions(taskOptionsById, new Set([...tasks.map((task) => task.taskId), ...watchTasks.map((task) => task.taskId)]));
40527
+ const offset = parseTaskListCursor(cursor);
40528
+ const allTasks = [
40529
+ ...watchTasks,
40530
+ ...tasks.map((task) => toMcpTask(task, taskOptionsById.get(task.taskId)))
40531
+ ].sort((a, b) => b.lastUpdatedAt.localeCompare(a.lastUpdatedAt));
40532
+ const page = allTasks.slice(offset, offset + TASKS_LIST_PAGE_SIZE);
40533
+ const nextOffset = offset + page.length;
40534
+ return {
40535
+ tasks: page,
40536
+ ...nextOffset < allTasks.length ? { nextCursor: String(nextOffset) } : {}
40537
+ };
40538
+ }
40539
+ };
40540
+ }
40541
+ function rememberTaskOptions(taskOptionsById, taskId, options) {
40542
+ taskOptionsById.set(taskId, normalizeCreateTaskOptions(options));
40543
+ while (taskOptionsById.size > TASK_OPTIONS_CACHE_LIMIT) {
40544
+ const oldestKey = taskOptionsById.keys().next().value;
40545
+ if (oldestKey === undefined)
40546
+ break;
40547
+ taskOptionsById.delete(oldestKey);
40548
+ }
40549
+ }
40550
+ function registerWatchTask(watchTasksById, watchTaskId, record3) {
40551
+ watchTasksById.set(watchTaskId, record3);
40552
+ while (watchTasksById.size > WATCH_TASKS_CACHE_LIMIT) {
40553
+ const oldestKey = watchTasksById.keys().next().value;
40554
+ if (oldestKey === undefined || oldestKey === watchTaskId)
40555
+ break;
40556
+ watchTasksById.delete(oldestKey);
40557
+ }
40558
+ }
40559
+ function pruneTaskOptions(taskOptionsById, taskIds) {
40560
+ for (const taskId of taskOptionsById.keys()) {
40561
+ if (!taskIds.has(taskId)) {
40562
+ taskOptionsById.delete(taskId);
40563
+ }
40564
+ }
40565
+ }
40566
+ function parseTaskListCursor(cursor) {
40567
+ if (!cursor)
40568
+ return 0;
40569
+ const offset = Number(cursor);
40570
+ if (!Number.isInteger(offset) || offset < 0) {
40571
+ throw new McpError(ErrorCode.InvalidParams, `Invalid tasks/list cursor: ${cursor}`);
40572
+ }
40573
+ return offset;
40574
+ }
40575
+ function renderNativeTaskPayloadResult(task) {
40576
+ if (toMcpTaskStatus(task) === "input_required") {
40577
+ return renderInputRequiredTaskResult(task);
40578
+ }
40579
+ if (task.status === "completed" || task.status === "failed" || task.status === "cancelled") {
40580
+ return renderNativeTaskResult(task);
40581
+ }
40582
+ throw new McpError(ErrorCode.InvalidRequest, `Task ${task.taskId} is still ${task.status}; use tasks/get until it is terminal or input_required`);
40583
+ }
40584
+ function withRelatedTaskMeta(result, taskId) {
40585
+ return {
40586
+ ...result,
40587
+ _meta: {
40588
+ ...result._meta,
40589
+ [RELATED_TASK_META_KEY]: { taskId }
40590
+ }
40591
+ };
40592
+ }
40593
+ function renderNativeTaskResult(task) {
40594
+ const isError = task.status === "failed" || task.status === "cancelled";
40595
+ const text = [
40596
+ `Task "${task.taskId}" finished with status ${task.status}.`,
40597
+ task.resultText.trim().length > 0 ? task.resultText : task.summary
40598
+ ].filter((line) => line.trim().length > 0).join(`
40599
+ `);
40600
+ return {
40601
+ content: [{ type: "text", text }],
40602
+ structuredContent: { task },
40603
+ ...isError ? { isError: true } : {}
40604
+ };
40605
+ }
40606
+ function renderInputRequiredTaskResult(task) {
40607
+ const actions = inputRequiredActions(task);
40608
+ const text = [
40609
+ `Task "${task.taskId}" requires input before it can continue.`,
40610
+ task.summary.trim().length > 0 ? task.summary : "",
40611
+ task.openQuestion ? `Open question: ${task.openQuestion.question}` : "",
40612
+ `Next: call task_get("${task.taskId}") to inspect details, then ${actions.join(" or ")}.`
40613
+ ].filter((line) => line.trim().length > 0).join(`
40614
+ `);
40615
+ return {
40616
+ content: [{ type: "text", text }],
40617
+ structuredContent: {
40618
+ task,
40619
+ nextAction: {
40620
+ kind: "input_required",
40621
+ taskId: task.taskId,
40622
+ recommendedTools: actions
40623
+ }
40624
+ }
40625
+ };
40626
+ }
40627
+ function inputRequiredActions(task) {
40628
+ const actions = [];
40629
+ if (task.status === "needs_confirmation") {
40630
+ actions.push("task_approve", "task_reject");
40631
+ }
40632
+ if (task.status === "blocked" || task.status === "waiting_for_human" || task.openQuestion) {
40633
+ actions.push("coordinator_answer_question");
40634
+ }
40635
+ if (task.reviewPending) {
40636
+ actions.push("coordinator_review_contested_result");
40637
+ }
40638
+ return actions.length > 0 ? actions : ["task_get"];
40639
+ }
40640
+ function toMcpTask(task, options = {}) {
40641
+ const statusMessage = mcpTaskStatusMessage(task);
40642
+ return {
40643
+ taskId: task.taskId,
40644
+ status: toMcpTaskStatus(task),
40645
+ ttl: options.ttl ?? null,
40646
+ createdAt: task.createdAt,
40647
+ lastUpdatedAt: task.updatedAt,
40648
+ ...options.pollInterval !== undefined ? { pollInterval: options.pollInterval } : {},
40649
+ ...statusMessage ? { statusMessage } : {}
40650
+ };
40651
+ }
40652
+ function mcpTaskStatusMessage(task) {
40653
+ const lines = [
40654
+ task.summary.trim().length > 0 ? task.summary : "",
40655
+ task.lastProgressSummary ? `Latest progress: ${task.lastProgressSummary}` : "",
40656
+ task.lastProgressAt ? `Last progress at: ${task.lastProgressAt}` : ""
40657
+ ].filter((line) => line.trim().length > 0);
40658
+ return lines.length > 0 ? lines.join(`
40659
+ `) : undefined;
40660
+ }
40661
+ function toMcpTaskStatus(task) {
40662
+ if (task.reviewPending !== undefined)
40663
+ return "input_required";
40664
+ if (task.status === "completed")
40665
+ return "completed";
40666
+ if (task.status === "failed")
40667
+ return "failed";
40668
+ if (task.status === "cancelled")
40669
+ return "cancelled";
40670
+ if (task.status === "needs_confirmation" || task.status === "blocked" || task.status === "waiting_for_human") {
40671
+ return "input_required";
40672
+ }
40673
+ return "working";
40674
+ }
40675
+ function normalizeCreateTaskOptions(options) {
40676
+ return {
40677
+ ttl: options.ttl ?? null,
40678
+ ...options.pollInterval !== undefined ? { pollInterval: options.pollInterval } : {}
39468
40679
  };
39469
40680
  }
39470
40681
  async function resolveMcpIdentity(server, options) {
@@ -39743,7 +40954,7 @@ function sanitizeName(input, fallback) {
39743
40954
  init_plugin_home();
39744
40955
  import { spawn as spawn4 } from "node:child_process";
39745
40956
  import { readFile as readFile7 } from "node:fs/promises";
39746
- import { dirname as dirname7, join as join6 } from "node:path";
40957
+ import { dirname as dirname9, join as join7 } from "node:path";
39747
40958
  import { fileURLToPath as fileURLToPath2 } from "node:url";
39748
40959
 
39749
40960
  // src/plugins/package-manager.ts
@@ -40021,8 +41232,8 @@ async function runInherit(command, args) {
40021
41232
  }
40022
41233
  async function readPackageName() {
40023
41234
  try {
40024
- const here = dirname7(fileURLToPath2(import.meta.url));
40025
- for (const candidate of [join6(here, "..", "package.json"), join6(here, "..", "..", "package.json")]) {
41235
+ const here = dirname9(fileURLToPath2(import.meta.url));
41236
+ for (const candidate of [join7(here, "..", "package.json"), join7(here, "..", "..", "package.json")]) {
40026
41237
  try {
40027
41238
  const parsed = JSON.parse(await readFile7(candidate, "utf8"));
40028
41239
  if (typeof parsed.name === "string" && parsed.name.trim())
@@ -40648,7 +41859,7 @@ async function setChannelAccountEnabled(type, accountId, enabled, rawArgs, deps)
40648
41859
  // src/plugins/plugin-cli.ts
40649
41860
  init_plugin_home();
40650
41861
  import { readFile as readFile9 } from "node:fs/promises";
40651
- import { isAbsolute, join as join8, resolve } from "node:path";
41862
+ import { isAbsolute, join as join9, resolve } from "node:path";
40652
41863
  init_plugin_loader();
40653
41864
  init_validate_plugin();
40654
41865
 
@@ -40658,13 +41869,13 @@ init_plugin_loader();
40658
41869
  init_validate_plugin();
40659
41870
  init_known_plugins();
40660
41871
  import { readFile as readFile8 } from "node:fs/promises";
40661
- import { join as join7 } from "node:path";
41872
+ import { join as join8 } from "node:path";
40662
41873
  function suggestedPluginPackageForChannel(type) {
40663
41874
  return findKnownPluginByChannel(type)?.packageName ?? `<npm-package-that-provides-${type}>`;
40664
41875
  }
40665
41876
  async function readDependencyEntries(pluginHome) {
40666
41877
  try {
40667
- const raw = await readFile8(join7(pluginHome, "package.json"), "utf8");
41878
+ const raw = await readFile8(join8(pluginHome, "package.json"), "utf8");
40668
41879
  const parsed = JSON.parse(raw);
40669
41880
  const out = {};
40670
41881
  for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
@@ -40766,7 +41977,7 @@ function looksLikePath(spec) {
40766
41977
  }
40767
41978
  async function readDependencyEntries2(pluginHome) {
40768
41979
  try {
40769
- const raw = await readFile9(join8(pluginHome, "package.json"), "utf8");
41980
+ const raw = await readFile9(join9(pluginHome, "package.json"), "utf8");
40770
41981
  const parsed = JSON.parse(raw);
40771
41982
  const out = {};
40772
41983
  for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
@@ -40792,7 +42003,7 @@ async function resolveLocalPluginName(installSpec, pluginHome, namesBeforeInstal
40792
42003
  return name;
40793
42004
  }
40794
42005
  try {
40795
- const raw = await readFile9(join8(installSpec, "package.json"), "utf8");
42006
+ const raw = await readFile9(join9(installSpec, "package.json"), "utf8");
40796
42007
  const parsed = JSON.parse(raw);
40797
42008
  if (typeof parsed.name === "string" && parsed.name.trim())
40798
42009
  return parsed.name.trim();
@@ -41400,6 +42611,7 @@ var HELP_LINES = [
41400
42611
  "weacpx plugin list|add|update|remove|enable|disable|doctor|known - 管理插件",
41401
42612
  "weacpx doctor - 运行诊断",
41402
42613
  "weacpx version - 查看版本",
42614
+ "weacpx agent|agents list|add|rm|templates - 管理本机 Agent",
41403
42615
  "weacpx workspace list|add|rm - 管理本机工作区(别名:ws)",
41404
42616
  "weacpx mcp-stdio [--coordinator-session <session>] [--source-handle <handle>] [--workspace <name>] - 启动 MCP stdio 服务"
41405
42617
  ];
@@ -41477,6 +42689,17 @@ async function runCli(args, deps = {}) {
41477
42689
  }
41478
42690
  return result;
41479
42691
  }
42692
+ case "agent":
42693
+ case "agents": {
42694
+ const result = await handleAgentCli(args.slice(1), { print });
42695
+ if (result === null) {
42696
+ for (const line of HELP_LINES) {
42697
+ print(line);
42698
+ }
42699
+ return 1;
42700
+ }
42701
+ return result;
42702
+ }
41480
42703
  case "plugin": {
41481
42704
  const result = await handlePluginCli(args.slice(1), await createPluginCliDeps({
41482
42705
  print,
@@ -41718,6 +42941,93 @@ async function workspaceRemove(rawName, print) {
41718
42941
  print(`工作区「${name}」已删除`);
41719
42942
  return 0;
41720
42943
  }
42944
+ async function handleAgentCli(args, deps) {
42945
+ const subcommand = args[0];
42946
+ switch (subcommand) {
42947
+ case "list":
42948
+ if (args.length !== 1)
42949
+ return null;
42950
+ return await agentList(deps.print);
42951
+ case "templates":
42952
+ if (args.length !== 1)
42953
+ return null;
42954
+ return agentTemplates(deps.print);
42955
+ case "add":
42956
+ if (args.length !== 2 || !args[1])
42957
+ return null;
42958
+ return await agentAdd(args[1], deps.print);
42959
+ case "rm":
42960
+ if (args.length !== 2 || !args[1])
42961
+ return null;
42962
+ return await agentRemove(args[1], deps.print);
42963
+ default:
42964
+ return null;
42965
+ }
42966
+ }
42967
+ async function agentList(print) {
42968
+ const store = await createCliConfigStore();
42969
+ const config2 = await store.load();
42970
+ const entries = Object.entries(config2.agents);
42971
+ if (entries.length === 0) {
42972
+ print("还没有 Agent。");
42973
+ return 0;
42974
+ }
42975
+ print("Agent 列表:");
42976
+ for (const [name, agent] of entries) {
42977
+ const command = agent.command ? ` command=${agent.command}` : "";
42978
+ print(`- ${name}: driver=${agent.driver}${command}`);
42979
+ }
42980
+ return 0;
42981
+ }
42982
+ function agentTemplates(print) {
42983
+ print("可用 Agent 模板:");
42984
+ for (const name of listAgentTemplates()) {
42985
+ print(`- ${name}`);
42986
+ }
42987
+ return 0;
42988
+ }
42989
+ async function agentAdd(rawName, print) {
42990
+ const name = rawName.trim();
42991
+ if (name.length === 0) {
42992
+ print("Agent 名称不能为空。");
42993
+ return 1;
42994
+ }
42995
+ const template = getAgentTemplate(name);
42996
+ if (!template) {
42997
+ print(`暂不支持这个 Agent 模板。当前可用:${listAgentTemplates().join("、")}`);
42998
+ return 1;
42999
+ }
43000
+ const store = await createCliConfigStore();
43001
+ const config2 = await store.load();
43002
+ const existing = config2.agents[name];
43003
+ if (existing) {
43004
+ if (sameAgentConfig(existing, template)) {
43005
+ print(`Agent「${name}」已存在`);
43006
+ return 0;
43007
+ }
43008
+ print(`Agent「${name}」已存在且配置不同。请先执行:weacpx agent rm ${name}`);
43009
+ return 1;
43010
+ }
43011
+ await store.upsertAgent(name, template);
43012
+ print(`Agent「${name}」已保存`);
43013
+ return 0;
43014
+ }
43015
+ async function agentRemove(rawName, print) {
43016
+ const name = rawName.trim();
43017
+ if (name.length === 0) {
43018
+ print("Agent 名称不能为空。");
43019
+ return 1;
43020
+ }
43021
+ const store = await createCliConfigStore();
43022
+ const config2 = await store.load();
43023
+ if (!config2.agents[name]) {
43024
+ print(`没有找到 Agent「${name}」。`);
43025
+ return 1;
43026
+ }
43027
+ await store.removeAgent(name);
43028
+ print(`Agent「${name}」已删除`);
43029
+ return 0;
43030
+ }
41721
43031
  async function createCliConfigStore() {
41722
43032
  const configPath = process.env.WEACPX_CONFIG ?? `${requireHome2()}/.weacpx/config.json`;
41723
43033
  await ensureConfigExists(configPath);
@@ -41741,6 +43051,7 @@ async function defaultLoadConfiguredPluginsForChannelCli() {
41741
43051
  const { loadConfiguredPlugins: loadConfiguredPlugins2 } = await Promise.resolve().then(() => (init_plugin_loader(), exports_plugin_loader));
41742
43052
  await loadConfiguredPlugins2({ plugins: config2.plugins });
41743
43053
  }
43054
+ var DAEMON_RUN_ENV = "WEACPX_DAEMON_RUN";
41744
43055
  async function defaultRun(options = {}) {
41745
43056
  const [{ buildApp: buildApp2, resolveRuntimePaths: resolveRuntimePaths2, prepareChannelMedia: prepareChannelMedia2 }, { runConsole: runConsole2 }] = await Promise.all([
41746
43057
  init_main().then(() => exports_main),
@@ -41769,6 +43080,7 @@ async function defaultRun(options = {}) {
41769
43080
  await createFirstRunSession(runtime, firstRunOnboarding);
41770
43081
  } : undefined,
41771
43082
  channels: channelRegistry,
43083
+ channelStartupPolicy: process.env[DAEMON_RUN_ENV] === "1" ? "best-effort" : "require-one",
41772
43084
  daemonRuntime,
41773
43085
  ...firstLockCreator ? {
41774
43086
  consumerLockFactory: (runtime) => firstLockCreator.create({
@@ -42063,7 +43375,7 @@ function safeDaemonLogPaths() {
42063
43375
  const configPath = process.env.WEACPX_CONFIG ?? `${requireHome2()}/.weacpx/config.json`;
42064
43376
  const paths = resolveDaemonPaths({ home: requireHome2() });
42065
43377
  return {
42066
- appLog: join16(dirname13(configPath), "runtime", "app.log"),
43378
+ appLog: join16(dirname15(configPath), "runtime", "app.log"),
42067
43379
  stderrLog: paths.stderrLog
42068
43380
  };
42069
43381
  } catch {