weacpx 0.3.1 → 0.3.2

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
@@ -360,7 +360,7 @@ var init_config_store = __esm(() => {
360
360
 
361
361
  // src/config/ensure-config.ts
362
362
  import { readFile as readFile2 } from "node:fs/promises";
363
- async function ensureConfigExists(path2) {
363
+ async function ensureConfigExists(path2, options = {}) {
364
364
  try {
365
365
  await loadConfig(path2);
366
366
  } catch (error) {
@@ -368,16 +368,24 @@ async function ensureConfigExists(path2) {
368
368
  throw error;
369
369
  }
370
370
  const store = new ConfigStore(path2);
371
- await store.save(await loadDefaultConfigTemplate());
371
+ await store.save(await loadDefaultConfigTemplate(options));
372
372
  }
373
373
  }
374
- async function loadDefaultConfigTemplate() {
374
+ async function loadDefaultConfigTemplate(options = {}) {
375
+ if (options.readDefaultConfigTemplate) {
376
+ try {
377
+ return normalizeDefaultConfigTemplate(await options.readDefaultConfigTemplate());
378
+ } catch (error) {
379
+ if (!isMissingFileError(error)) {
380
+ throw error;
381
+ }
382
+ }
383
+ }
375
384
  const candidates = [
376
385
  new URL("../../config.example.json", import.meta.url),
377
386
  new URL("../config.example.json", import.meta.url)
378
387
  ];
379
388
  let raw;
380
- let lastError;
381
389
  for (const candidate of candidates) {
382
390
  try {
383
391
  raw = await readFile2(candidate, "utf8");
@@ -386,11 +394,10 @@ async function loadDefaultConfigTemplate() {
386
394
  if (!isMissingFileError(error)) {
387
395
  throw error;
388
396
  }
389
- lastError = error;
390
397
  }
391
398
  }
392
399
  if (!raw) {
393
- throw lastError;
400
+ return normalizeDefaultConfigTemplate(BUILTIN_DEFAULT_CONFIG_TEMPLATE);
394
401
  }
395
402
  return normalizeDefaultConfigTemplate(JSON.parse(raw));
396
403
  }
@@ -411,9 +418,32 @@ function normalizeDefaultConfigTemplate(raw) {
411
418
  function isMissingFileError(error) {
412
419
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
413
420
  }
421
+ var BUILTIN_DEFAULT_CONFIG_TEMPLATE;
414
422
  var init_ensure_config = __esm(() => {
415
423
  init_config_store();
416
424
  init_load_config();
425
+ BUILTIN_DEFAULT_CONFIG_TEMPLATE = {
426
+ transport: {
427
+ type: "acpx-bridge",
428
+ sessionInitTimeoutMs: 120000,
429
+ permissionMode: "approve-all",
430
+ nonInteractivePermissions: "deny"
431
+ },
432
+ logging: {
433
+ level: "info",
434
+ maxSizeBytes: 2 * 1024 * 1024,
435
+ maxFiles: 5,
436
+ retentionDays: 7
437
+ },
438
+ wechat: {
439
+ replyMode: "stream"
440
+ },
441
+ agents: {
442
+ codex: { driver: "codex" },
443
+ claude: { driver: "claude" }
444
+ },
445
+ workspaces: {}
446
+ };
417
447
  });
418
448
 
419
449
  // src/daemon/daemon-status.ts
@@ -7518,6 +7548,13 @@ function renderTaskHeartbeat(task, elapsedSeconds) {
7518
7548
  return `⏳ 任务「${task.taskId}」已运行 ${minutes} 分钟,等待中...`;
7519
7549
  }
7520
7550
 
7551
+ // src/orchestration/task-wait-timeouts.ts
7552
+ 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;
7553
+ var init_task_wait_timeouts = __esm(() => {
7554
+ DEFAULT_TASK_WAIT_TIMEOUT_MS = 5 * 60000;
7555
+ MAX_TASK_WAIT_TIMEOUT_MS = 20 * 60000;
7556
+ });
7557
+
7521
7558
  // src/weixin/messaging/quota-errors.ts
7522
7559
  function isQuotaDeferredError(error2) {
7523
7560
  return error2 instanceof QuotaDeferredError;
@@ -7534,8 +7571,337 @@ var init_quota_errors = __esm(() => {
7534
7571
  };
7535
7572
  });
7536
7573
 
7574
+ // src/orchestration/orchestration-types.ts
7575
+ function createEmptyOrchestrationState() {
7576
+ return {
7577
+ tasks: {},
7578
+ workerBindings: {},
7579
+ groups: {},
7580
+ humanQuestionPackages: {},
7581
+ coordinatorQuestionState: {},
7582
+ coordinatorRoutes: {},
7583
+ externalCoordinators: {}
7584
+ };
7585
+ }
7586
+
7587
+ // src/state/types.ts
7588
+ function createEmptyState() {
7589
+ return {
7590
+ sessions: {},
7591
+ chat_contexts: {},
7592
+ orchestration: createEmptyOrchestrationState()
7593
+ };
7594
+ }
7595
+ var init_types = () => {};
7596
+
7597
+ // src/state/state-store.ts
7598
+ import { readFile as readFile5 } from "node:fs/promises";
7599
+ function isRecord2(value) {
7600
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7601
+ }
7602
+ function isString(value) {
7603
+ return typeof value === "string";
7604
+ }
7605
+ function isOptionalString(value) {
7606
+ return value === undefined || typeof value === "string";
7607
+ }
7608
+ function isOptionalBoolean(value) {
7609
+ return value === undefined || typeof value === "boolean";
7610
+ }
7611
+ function isTaskStatus(value) {
7612
+ return value === "pending" || value === "needs_confirmation" || value === "running" || value === "blocked" || value === "waiting_for_human" || value === "completed" || value === "failed" || value === "cancelled";
7613
+ }
7614
+ function isSourceKind(value) {
7615
+ return value === "human" || value === "coordinator" || value === "worker";
7616
+ }
7617
+ function isOpenQuestionRecord(value) {
7618
+ if (!isRecord2(value)) {
7619
+ return false;
7620
+ }
7621
+ return isString(value.questionId) && isString(value.question) && isString(value.whyBlocked) && isString(value.whatIsNeeded) && isString(value.askedAt) && (value.status === "open" || value.status === "answered" || value.status === "superseded") && isOptionalString(value.answeredAt) && (value.answerSource === undefined || value.answerSource === "coordinator" || value.answerSource === "human") && isOptionalString(value.answerText) && isOptionalString(value.packageId) && isOptionalString(value.lastWakeError) && isOptionalString(value.lastResumeError);
7622
+ }
7623
+ function isReviewPendingRecord(value) {
7624
+ if (!isRecord2(value)) {
7625
+ return false;
7626
+ }
7627
+ return isString(value.reviewId) && value.reason === "misrouted_answer" && isString(value.createdAt) && isString(value.resultId) && isString(value.resultText);
7628
+ }
7629
+ function isCorrectionPendingRecord(value) {
7630
+ if (!isRecord2(value)) {
7631
+ return false;
7632
+ }
7633
+ return isString(value.requestedAt) && value.reason === "misrouted_answer";
7634
+ }
7635
+ function isTaskRecord(value) {
7636
+ if (!isRecord2(value)) {
7637
+ return false;
7638
+ }
7639
+ 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));
7640
+ }
7641
+ function isExternalCoordinatorRecord(value) {
7642
+ if (!isRecord2(value)) {
7643
+ return false;
7644
+ }
7645
+ return isString(value.coordinatorSession) && isOptionalString(value.workspace) && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.defaultTargetAgent);
7646
+ }
7647
+ function isWorkerBindingRecord(value) {
7648
+ if (!isRecord2(value)) {
7649
+ return false;
7650
+ }
7651
+ return isString(value.sourceHandle) && isString(value.coordinatorSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role);
7652
+ }
7653
+ function isGroupRecord(value) {
7654
+ if (!isRecord2(value)) {
7655
+ return false;
7656
+ }
7657
+ return isString(value.groupId) && isString(value.coordinatorSession) && isString(value.title) && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.coordinatorInjectedAt) && isOptionalBoolean(value.injectionPending) && isOptionalString(value.injectionAppliedAt) && isOptionalString(value.lastInjectionError);
7658
+ }
7659
+ function isQueuedQuestionRecord(value) {
7660
+ if (!isRecord2(value)) {
7661
+ return false;
7662
+ }
7663
+ return isString(value.taskId) && isString(value.questionId) && isString(value.enqueuedAt);
7664
+ }
7665
+ function isCoordinatorQuestionStateRecord(value) {
7666
+ if (!isRecord2(value)) {
7667
+ return false;
7668
+ }
7669
+ const queuedQuestions = value.queuedQuestions;
7670
+ if (queuedQuestions !== undefined && !Array.isArray(queuedQuestions)) {
7671
+ return false;
7672
+ }
7673
+ return (value.activePackageId === undefined || isString(value.activePackageId)) && (queuedQuestions === undefined || queuedQuestions.every(isQueuedQuestionRecord));
7674
+ }
7675
+ function isCoordinatorRouteContextRecord(value) {
7676
+ if (!isRecord2(value)) {
7677
+ return false;
7678
+ }
7679
+ return isString(value.coordinatorSession) && isString(value.chatKey) && isOptionalString(value.accountId) && isOptionalString(value.replyContextToken) && isString(value.updatedAt);
7680
+ }
7681
+ function isHumanQuestionPackageMessageRecord(value) {
7682
+ if (!isRecord2(value)) {
7683
+ return false;
7684
+ }
7685
+ return isString(value.messageId) && (value.kind === "initial" || value.kind === "follow_up") && isString(value.promptText) && isString(value.createdAt) && isOptionalString(value.deliveredAt) && isOptionalString(value.deliveredChatKey) && isOptionalString(value.deliveryAccountId) && isOptionalString(value.lastDeliveryError);
7686
+ }
7687
+ function isHumanQuestionPackageRecord(value) {
7688
+ if (!isRecord2(value)) {
7689
+ return false;
7690
+ }
7691
+ const initialTaskIds = value.initialTaskIds;
7692
+ const openTaskIds = value.openTaskIds;
7693
+ const resolvedTaskIds = value.resolvedTaskIds;
7694
+ const messages = value.messages;
7695
+ return isString(value.packageId) && isString(value.coordinatorSession) && (value.status === "active" || value.status === "closed") && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.closedAt) && Array.isArray(initialTaskIds) && initialTaskIds.every(isString) && Array.isArray(openTaskIds) && openTaskIds.every(isString) && Array.isArray(resolvedTaskIds) && resolvedTaskIds.every(isString) && Array.isArray(messages) && messages.every(isHumanQuestionPackageMessageRecord) && isOptionalString(value.awaitingReplyMessageId);
7696
+ }
7697
+ function parseOrchestrationState(raw, path3) {
7698
+ if (raw === undefined) {
7699
+ return createEmptyOrchestrationState();
7700
+ }
7701
+ if (!isRecord2(raw)) {
7702
+ throw new Error(`state file "${path3}" must contain an object field "orchestration"`);
7703
+ }
7704
+ const tasks = raw.tasks;
7705
+ if (tasks !== undefined && !isRecord2(tasks)) {
7706
+ throw new Error(`state file "${path3}" must contain an object field "orchestration.tasks"`);
7707
+ }
7708
+ const workerBindings = raw.workerBindings;
7709
+ if (workerBindings !== undefined && !isRecord2(workerBindings)) {
7710
+ throw new Error(`state file "${path3}" must contain an object field "orchestration.workerBindings"`);
7711
+ }
7712
+ const groups = raw.groups;
7713
+ if (groups !== undefined && !isRecord2(groups)) {
7714
+ throw new Error(`state file "${path3}" must contain an object field "orchestration.groups"`);
7715
+ }
7716
+ const humanQuestionPackages = raw.humanQuestionPackages;
7717
+ if (humanQuestionPackages !== undefined && !isRecord2(humanQuestionPackages)) {
7718
+ throw new Error(`state file "${path3}" must contain an object field "orchestration.humanQuestionPackages"`);
7719
+ }
7720
+ const coordinatorQuestionState = raw.coordinatorQuestionState;
7721
+ if (coordinatorQuestionState !== undefined && !isRecord2(coordinatorQuestionState)) {
7722
+ throw new Error(`state file "${path3}" must contain an object field "orchestration.coordinatorQuestionState"`);
7723
+ }
7724
+ const coordinatorRoutes = raw.coordinatorRoutes;
7725
+ if (coordinatorRoutes !== undefined && !isRecord2(coordinatorRoutes)) {
7726
+ throw new Error(`state file "${path3}" must contain an object field "orchestration.coordinatorRoutes"`);
7727
+ }
7728
+ const externalCoordinators = raw.externalCoordinators;
7729
+ if (externalCoordinators !== undefined && !isRecord2(externalCoordinators)) {
7730
+ throw new Error(`state file "${path3}" must contain an object field "orchestration.externalCoordinators"`);
7731
+ }
7732
+ const parsedTasks = {};
7733
+ for (const [taskId, task] of Object.entries(tasks ?? {})) {
7734
+ if (!isTaskRecord(task)) {
7735
+ throw new Error(`state file "${path3}" contains an invalid orchestration task at "${taskId}"`);
7736
+ }
7737
+ parsedTasks[taskId] = task;
7738
+ }
7739
+ const parsedWorkerBindings = {};
7740
+ for (const [workerSession, binding] of Object.entries(workerBindings ?? {})) {
7741
+ if (!isWorkerBindingRecord(binding)) {
7742
+ throw new Error(`state file "${path3}" contains an invalid orchestration worker binding at "${workerSession}"`);
7743
+ }
7744
+ parsedWorkerBindings[workerSession] = binding;
7745
+ }
7746
+ const parsedGroups = {};
7747
+ for (const [groupId, group] of Object.entries(groups ?? {})) {
7748
+ if (!isGroupRecord(group)) {
7749
+ throw new Error(`state file "${path3}" contains an invalid orchestration group at "${groupId}"`);
7750
+ }
7751
+ parsedGroups[groupId] = group;
7752
+ }
7753
+ const parsedHumanQuestionPackages = {};
7754
+ for (const [packageId, packageRecord] of Object.entries(humanQuestionPackages ?? {})) {
7755
+ if (!isHumanQuestionPackageRecord(packageRecord)) {
7756
+ throw new Error(`state file "${path3}" contains an invalid human question package at "${packageId}"`);
7757
+ }
7758
+ parsedHumanQuestionPackages[packageId] = packageRecord;
7759
+ }
7760
+ const parsedCoordinatorQuestionState = {};
7761
+ for (const [coordinatorSession, questionState] of Object.entries(coordinatorQuestionState ?? {})) {
7762
+ if (!isCoordinatorQuestionStateRecord(questionState)) {
7763
+ throw new Error(`state file "${path3}" contains an invalid coordinator question state at "${coordinatorSession}"`);
7764
+ }
7765
+ parsedCoordinatorQuestionState[coordinatorSession] = {
7766
+ activePackageId: questionState.activePackageId,
7767
+ queuedQuestions: (questionState.queuedQuestions ?? []).map((question) => ({ ...question }))
7768
+ };
7769
+ }
7770
+ const parsedCoordinatorRoutes = {};
7771
+ for (const [coordinatorSession, route] of Object.entries(coordinatorRoutes ?? {})) {
7772
+ if (!isCoordinatorRouteContextRecord(route)) {
7773
+ throw new Error(`state file "${path3}" contains an invalid coordinator route at "${coordinatorSession}"`);
7774
+ }
7775
+ parsedCoordinatorRoutes[coordinatorSession] = route;
7776
+ }
7777
+ const parsedExternalCoordinators = {};
7778
+ for (const [coordinatorSession, externalCoordinator] of Object.entries(externalCoordinators ?? {})) {
7779
+ if (!isExternalCoordinatorRecord(externalCoordinator)) {
7780
+ throw new Error(`state file "${path3}" contains an invalid external coordinator at "${coordinatorSession}"`);
7781
+ }
7782
+ if (externalCoordinator.coordinatorSession !== coordinatorSession) {
7783
+ throw new Error(`state file "${path3}" contains an external coordinator key mismatch at "${coordinatorSession}"`);
7784
+ }
7785
+ parsedExternalCoordinators[coordinatorSession] = externalCoordinator;
7786
+ }
7787
+ return {
7788
+ tasks: parsedTasks,
7789
+ workerBindings: parsedWorkerBindings,
7790
+ groups: parsedGroups,
7791
+ humanQuestionPackages: parsedHumanQuestionPackages,
7792
+ coordinatorQuestionState: parsedCoordinatorQuestionState,
7793
+ coordinatorRoutes: parsedCoordinatorRoutes,
7794
+ externalCoordinators: parsedExternalCoordinators
7795
+ };
7796
+ }
7797
+ function isReplyMode(value) {
7798
+ return value === "stream" || value === "final" || value === "verbose";
7799
+ }
7800
+ function isSessionRecord(value) {
7801
+ if (!isRecord2(value)) {
7802
+ return false;
7803
+ }
7804
+ return isString(value.alias) && isString(value.agent) && isString(value.workspace) && isString(value.transport_session) && isOptionalString(value.transport_agent_command) && isOptionalString(value.mode_id) && (value.reply_mode === undefined || isReplyMode(value.reply_mode)) && isString(value.created_at) && isString(value.last_used_at);
7805
+ }
7806
+ function parseSessions(raw, path3) {
7807
+ const sessions = {};
7808
+ for (const [alias, value] of Object.entries(raw)) {
7809
+ if (!isSessionRecord(value)) {
7810
+ throw new Error(`state file "${path3}" contains malformed session record "${alias}"`);
7811
+ }
7812
+ sessions[alias] = value;
7813
+ }
7814
+ return sessions;
7815
+ }
7816
+ function isChatContextRecord(value) {
7817
+ return isRecord2(value) && isString(value.current_session);
7818
+ }
7819
+ function parseChatContexts(raw, path3) {
7820
+ const chatContexts = {};
7821
+ for (const [chatKey, value] of Object.entries(raw)) {
7822
+ if (!isChatContextRecord(value)) {
7823
+ throw new Error(`state file "${path3}" contains malformed chat context record "${chatKey}"`);
7824
+ }
7825
+ chatContexts[chatKey] = value;
7826
+ }
7827
+ return chatContexts;
7828
+ }
7829
+ function parseState(raw, path3) {
7830
+ if (!isRecord2(raw)) {
7831
+ throw new Error(`state file "${path3}" must contain a JSON object`);
7832
+ }
7833
+ const sessions = raw.sessions;
7834
+ if (!isRecord2(sessions)) {
7835
+ throw new Error(`state file "${path3}" must contain an object field "sessions"`);
7836
+ }
7837
+ const chatContexts = raw.chat_contexts;
7838
+ if (!isRecord2(chatContexts)) {
7839
+ throw new Error(`state file "${path3}" must contain an object field "chat_contexts"`);
7840
+ }
7841
+ const parsedSessions = parseSessions(sessions, path3);
7842
+ const orchestration = parseOrchestrationState(raw.orchestration, path3);
7843
+ validateExternalCoordinatorIdentityCollisions(parsedSessions, orchestration, path3);
7844
+ return {
7845
+ sessions: parsedSessions,
7846
+ chat_contexts: parseChatContexts(chatContexts, path3),
7847
+ orchestration
7848
+ };
7849
+ }
7850
+ function validateExternalCoordinatorIdentityCollisions(sessions, orchestration, path3) {
7851
+ for (const coordinatorSession of Object.keys(orchestration.externalCoordinators)) {
7852
+ if (Object.values(sessions).some((session) => session.transport_session === coordinatorSession)) {
7853
+ throw new Error(`state file "${path3}" contains external coordinator "${coordinatorSession}" that conflicts with a logical session`);
7854
+ }
7855
+ if (orchestration.workerBindings[coordinatorSession]) {
7856
+ throw new Error(`state file "${path3}" contains external coordinator "${coordinatorSession}" that conflicts with a worker binding`);
7857
+ }
7858
+ if (Object.values(orchestration.tasks).some((task) => task.workerSession === coordinatorSession && (!isTerminalTaskStatus(task.status) || task.reviewPending !== undefined))) {
7859
+ throw new Error(`state file "${path3}" contains external coordinator "${coordinatorSession}" that conflicts with an active task worker session`);
7860
+ }
7861
+ }
7862
+ }
7863
+ function isTerminalTaskStatus(status) {
7864
+ return status === "completed" || status === "failed" || status === "cancelled";
7865
+ }
7866
+
7867
+ class StateStore {
7868
+ path;
7869
+ constructor(path3) {
7870
+ this.path = path3;
7871
+ }
7872
+ async load() {
7873
+ try {
7874
+ const content = await readFile5(this.path, "utf8");
7875
+ if (content.trim() === "") {
7876
+ return createEmptyState();
7877
+ }
7878
+ let parsed;
7879
+ try {
7880
+ parsed = JSON.parse(content);
7881
+ } catch (error2) {
7882
+ throw new Error(`failed to parse state file "${this.path}"`, {
7883
+ cause: error2
7884
+ });
7885
+ }
7886
+ return parseState(parsed, this.path);
7887
+ } catch (error2) {
7888
+ if (error2.code === "ENOENT") {
7889
+ return createEmptyState();
7890
+ }
7891
+ throw error2;
7892
+ }
7893
+ }
7894
+ async save(state) {
7895
+ await writePrivateFileAtomic(this.path, JSON.stringify(state, null, 2));
7896
+ }
7897
+ }
7898
+ var init_state_store = __esm(() => {
7899
+ init_private_file();
7900
+ init_types();
7901
+ });
7902
+
7537
7903
  // src/weixin/monitor/consumer-lock.ts
7538
- import { mkdir as mkdir6, open as open2, readFile as readFile5, rm as rm4 } from "node:fs/promises";
7904
+ import { mkdir as mkdir6, open as open2, readFile as readFile6, rm as rm4 } from "node:fs/promises";
7539
7905
  import { dirname as dirname6, join as join4 } from "node:path";
7540
7906
  import { homedir as homedir3 } from "node:os";
7541
7907
  function createWeixinConsumerLock(options = {}) {
@@ -7617,7 +7983,7 @@ function createWeixinConsumerLock(options = {}) {
7617
7983
  }
7618
7984
  async function loadLockMetadata(path3) {
7619
7985
  try {
7620
- const raw = await readFile5(path3, "utf8");
7986
+ const raw = await readFile6(path3, "utf8");
7621
7987
  const parsed = JSON.parse(raw);
7622
7988
  if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
7623
7989
  return null;
@@ -9752,7 +10118,7 @@ function createConversationExecutor() {
9752
10118
 
9753
10119
  // src/weixin/api/types.ts
9754
10120
  var UploadMediaType, MessageType, MessageItemType, MessageState, TypingStatus;
9755
- var init_types = __esm(() => {
10121
+ var init_types2 = __esm(() => {
9756
10122
  UploadMediaType = {
9757
10123
  IMAGE: 1,
9758
10124
  VIDEO: 2,
@@ -9871,7 +10237,7 @@ function buildCdnUploadUrl(params) {
9871
10237
  }
9872
10238
 
9873
10239
  // src/weixin/cdn/pic-decrypt.ts
9874
- async function fetchCdnBytes(url, label) {
10240
+ async function fetchCdnBytes(url, label, maxBytes) {
9875
10241
  let res;
9876
10242
  try {
9877
10243
  res = await fetch(url);
@@ -9887,7 +10253,41 @@ async function fetchCdnBytes(url, label) {
9887
10253
  logger.error(msg);
9888
10254
  throw new Error(msg);
9889
10255
  }
9890
- return Buffer.from(await res.arrayBuffer());
10256
+ const contentLength = res.headers.get("content-length");
10257
+ if (maxBytes !== undefined && contentLength) {
10258
+ const parsedLength = Number(contentLength);
10259
+ if (Number.isFinite(parsedLength) && parsedLength > maxBytes) {
10260
+ await res.body?.cancel().catch(() => {});
10261
+ throw new Error(`${label}: CDN download exceeds ${maxBytes} bytes`);
10262
+ }
10263
+ }
10264
+ if (!res.body) {
10265
+ const buffer = Buffer.from(await res.arrayBuffer());
10266
+ if (maxBytes !== undefined && buffer.byteLength > maxBytes) {
10267
+ throw new Error(`${label}: CDN download exceeds ${maxBytes} bytes`);
10268
+ }
10269
+ return buffer;
10270
+ }
10271
+ const reader = res.body.getReader();
10272
+ const chunks = [];
10273
+ let total = 0;
10274
+ try {
10275
+ while (true) {
10276
+ const { done, value } = await reader.read();
10277
+ if (done)
10278
+ break;
10279
+ const chunk = Buffer.from(value);
10280
+ total += chunk.byteLength;
10281
+ if (maxBytes !== undefined && total > maxBytes) {
10282
+ await reader.cancel().catch(() => {});
10283
+ throw new Error(`${label}: CDN download exceeds ${maxBytes} bytes`);
10284
+ }
10285
+ chunks.push(chunk);
10286
+ }
10287
+ } finally {
10288
+ reader.releaseLock();
10289
+ }
10290
+ return Buffer.concat(chunks, total);
9891
10291
  }
9892
10292
  function parseAesKey(aesKeyBase64, label) {
9893
10293
  const decoded = Buffer.from(aesKeyBase64, "base64");
@@ -9901,20 +10301,24 @@ function parseAesKey(aesKeyBase64, label) {
9901
10301
  logger.error(msg);
9902
10302
  throw new Error(msg);
9903
10303
  }
9904
- async function downloadAndDecryptBuffer(encryptedQueryParam, aesKeyBase64, cdnBaseUrl, label, fullUrl) {
10304
+ async function downloadAndDecryptBuffer(encryptedQueryParam, aesKeyBase64, cdnBaseUrl, label, fullUrl, maxBytes) {
9905
10305
  const key = parseAesKey(aesKeyBase64, label);
9906
10306
  const url = fullUrl || buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);
9907
10307
  logger.debug(`${label}: fetching url=${url}`);
9908
- const encrypted = await fetchCdnBytes(url, label);
10308
+ const encryptedMaxBytes = maxBytes === undefined ? undefined : maxBytes + 16;
10309
+ const encrypted = await fetchCdnBytes(url, label, encryptedMaxBytes);
9909
10310
  logger.debug(`${label}: downloaded ${encrypted.byteLength} bytes, decrypting`);
9910
10311
  const decrypted = decryptAesEcb(encrypted, key);
10312
+ if (maxBytes !== undefined && decrypted.byteLength > maxBytes) {
10313
+ throw new Error(`${label}: decrypted media exceeds ${maxBytes} bytes`);
10314
+ }
9911
10315
  logger.debug(`${label}: decrypted ${decrypted.length} bytes`);
9912
10316
  return decrypted;
9913
10317
  }
9914
- async function downloadPlainCdnBuffer(encryptedQueryParam, cdnBaseUrl, label, fullUrl) {
10318
+ async function downloadPlainCdnBuffer(encryptedQueryParam, cdnBaseUrl, label, fullUrl, maxBytes) {
9915
10319
  const url = fullUrl || buildCdnDownloadUrl(encryptedQueryParam, cdnBaseUrl);
9916
10320
  logger.debug(`${label}: fetching url=${url}`);
9917
- return fetchCdnBytes(url, label);
10321
+ return fetchCdnBytes(url, label, maxBytes);
9918
10322
  }
9919
10323
  var init_pic_decrypt = __esm(() => {
9920
10324
  init_aes_ecb();
@@ -9986,20 +10390,21 @@ async function downloadMediaFromItem(item, deps) {
9986
10390
  const aesKeyBase64 = img.aeskey ? Buffer.from(img.aeskey, "hex").toString("base64") : img.media.aes_key;
9987
10391
  logger.debug(`${label} image: encrypt_query_param=${(img.media.encrypt_query_param ?? "").slice(0, 40)}... hasAesKey=${Boolean(aesKeyBase64)} aeskeySource=${img.aeskey ? "image_item.aeskey" : "media.aes_key"} full_url=${Boolean(img.media.full_url)}`);
9988
10392
  try {
9989
- const buf = aesKeyBase64 ? await downloadAndDecryptBuffer(img.media.encrypt_query_param ?? "", aesKeyBase64, cdnBaseUrl, `${label} image`, img.media.full_url) : await downloadPlainCdnBuffer(img.media.encrypt_query_param ?? "", cdnBaseUrl, `${label} image-plain`, img.media.full_url);
10393
+ const buf = aesKeyBase64 ? await downloadAndDecryptBuffer(img.media.encrypt_query_param ?? "", aesKeyBase64, cdnBaseUrl, `${label} image`, img.media.full_url, WEIXIN_MEDIA_MAX_BYTES) : await downloadPlainCdnBuffer(img.media.encrypt_query_param ?? "", cdnBaseUrl, `${label} image-plain`, img.media.full_url, WEIXIN_MEDIA_MAX_BYTES);
9990
10394
  const saved = await saveMedia(buf, undefined, "inbound", WEIXIN_MEDIA_MAX_BYTES);
9991
10395
  result.decryptedPicPath = saved.path;
9992
10396
  logger.debug(`${label} image saved: ${saved.path}`);
9993
10397
  } catch (err) {
9994
10398
  logger.error(`${label} image download/decrypt failed: ${String(err)}`);
9995
10399
  errLog(`weixin ${label} image download/decrypt failed: ${String(err)}`);
10400
+ throw err;
9996
10401
  }
9997
10402
  } else if (item.type === MessageItemType.VOICE) {
9998
10403
  const voice = item.voice_item;
9999
10404
  if (!voice?.media?.encrypt_query_param && !voice?.media?.full_url || !voice?.media?.aes_key)
10000
10405
  return result;
10001
10406
  try {
10002
- const silkBuf = await downloadAndDecryptBuffer(voice.media.encrypt_query_param ?? "", voice.media.aes_key, cdnBaseUrl, `${label} voice`, voice.media.full_url);
10407
+ const silkBuf = await downloadAndDecryptBuffer(voice.media.encrypt_query_param ?? "", voice.media.aes_key, cdnBaseUrl, `${label} voice`, voice.media.full_url, WEIXIN_MEDIA_MAX_BYTES);
10003
10408
  logger.debug(`${label} voice: decrypted ${silkBuf.length} bytes, attempting silk transcode`);
10004
10409
  const wavBuf = await silkToWav(silkBuf);
10005
10410
  if (wavBuf) {
@@ -10022,7 +10427,7 @@ async function downloadMediaFromItem(item, deps) {
10022
10427
  if (!fileItem?.media?.encrypt_query_param && !fileItem?.media?.full_url || !fileItem?.media?.aes_key)
10023
10428
  return result;
10024
10429
  try {
10025
- const buf = await downloadAndDecryptBuffer(fileItem.media.encrypt_query_param ?? "", fileItem.media.aes_key, cdnBaseUrl, `${label} file`, fileItem.media.full_url);
10430
+ const buf = await downloadAndDecryptBuffer(fileItem.media.encrypt_query_param ?? "", fileItem.media.aes_key, cdnBaseUrl, `${label} file`, fileItem.media.full_url, WEIXIN_MEDIA_MAX_BYTES);
10026
10431
  const mime = getMimeFromFilename(fileItem.file_name ?? "file.bin");
10027
10432
  const saved = await saveMedia(buf, mime, "inbound", WEIXIN_MEDIA_MAX_BYTES, fileItem.file_name ?? undefined);
10028
10433
  result.decryptedFilePath = saved.path;
@@ -10037,7 +10442,7 @@ async function downloadMediaFromItem(item, deps) {
10037
10442
  if (!videoItem?.media?.encrypt_query_param && !videoItem?.media?.full_url || !videoItem?.media?.aes_key)
10038
10443
  return result;
10039
10444
  try {
10040
- const buf = await downloadAndDecryptBuffer(videoItem.media.encrypt_query_param ?? "", videoItem.media.aes_key, cdnBaseUrl, `${label} video`, videoItem.media.full_url);
10445
+ const buf = await downloadAndDecryptBuffer(videoItem.media.encrypt_query_param ?? "", videoItem.media.aes_key, cdnBaseUrl, `${label} video`, videoItem.media.full_url, WEIXIN_MEDIA_MAX_BYTES);
10041
10446
  const saved = await saveMedia(buf, "video/mp4", "inbound", WEIXIN_MEDIA_MAX_BYTES);
10042
10447
  result.decryptedVideoPath = saved.path;
10043
10448
  logger.debug(`${label} video: saved to ${saved.path}`);
@@ -10054,7 +10459,7 @@ var init_media_download = __esm(() => {
10054
10459
  init_mime();
10055
10460
  init_pic_decrypt();
10056
10461
  init_silk_transcode();
10057
- init_types();
10462
+ init_types2();
10058
10463
  WEIXIN_MEDIA_MAX_BYTES = 100 * 1024 * 1024;
10059
10464
  });
10060
10465
 
@@ -10144,7 +10549,7 @@ var contextTokenStore;
10144
10549
  var init_inbound = __esm(() => {
10145
10550
  init_logger();
10146
10551
  init_random();
10147
- init_types();
10552
+ init_types2();
10148
10553
  contextTokenStore = new Map;
10149
10554
  });
10150
10555
 
@@ -10306,7 +10711,7 @@ var init_send = __esm(() => {
10306
10711
  init_api();
10307
10712
  init_logger();
10308
10713
  init_random();
10309
- init_types();
10714
+ init_types2();
10310
10715
  });
10311
10716
 
10312
10717
  // src/weixin/messaging/error-notice.ts
@@ -10468,7 +10873,7 @@ var init_upload = __esm(() => {
10468
10873
  init_logger();
10469
10874
  init_mime();
10470
10875
  init_random();
10471
- init_types();
10876
+ init_types2();
10472
10877
  });
10473
10878
 
10474
10879
  // src/weixin/messaging/send-media.ts
@@ -10819,7 +11224,10 @@ function isPathInside(candidate, root) {
10819
11224
  return relative === "" || !relative.startsWith("..") && !path9.isAbsolute(relative);
10820
11225
  }
10821
11226
  function createSaveMediaBuffer(mediaTempDir) {
10822
- return async function saveMediaBuffer(buffer, contentType, subdir, _maxBytes, originalFilename) {
11227
+ return async function saveMediaBuffer(buffer, contentType, subdir, maxBytes, originalFilename) {
11228
+ if (maxBytes !== undefined && buffer.byteLength > maxBytes) {
11229
+ throw new Error(`media exceeds ${maxBytes} bytes`);
11230
+ }
10823
11231
  const dir = path9.join(resolveMediaTempDir(mediaTempDir), subdir ?? "");
10824
11232
  await fs6.mkdir(dir, { recursive: true });
10825
11233
  let ext = ".bin";
@@ -10834,6 +11242,33 @@ function createSaveMediaBuffer(mediaTempDir) {
10834
11242
  return { path: filePath };
10835
11243
  };
10836
11244
  }
11245
+ function inboundMediaUnavailableMessage(item) {
11246
+ if (item.type === MessageItemType.IMAGE) {
11247
+ return "图片读取失败,请重试。";
11248
+ }
11249
+ return "暂不支持处理该类型消息,请发送文字或图片。";
11250
+ }
11251
+ function inboundImageFailureMessage(error2) {
11252
+ const message = error2 instanceof Error ? error2.message : String(error2);
11253
+ return message.includes("exceeds 104857600 bytes") || message.includes("exceeds 100MB") ? "图片超过 100MB,无法处理。" : "图片读取失败,请重试。";
11254
+ }
11255
+ async function sendInboundMediaUnavailableNotice(input) {
11256
+ const { to, notice, contextToken, deps } = input;
11257
+ const reserved = deps.reserveFinal ? deps.reserveFinal(to) : true;
11258
+ if (!reserved) {
11259
+ deps.errLog(`weixin.final.dropped reason=quota_exhausted kind=media_unavailable chatKey=${to}`);
11260
+ return;
11261
+ }
11262
+ try {
11263
+ await sendMessageWeixin({
11264
+ to,
11265
+ text: notice,
11266
+ opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
11267
+ });
11268
+ } catch (err) {
11269
+ deps.errLog(`media unavailable notice failed: ${String(err)}`);
11270
+ }
11271
+ }
10837
11272
  function extractTextBody(itemList) {
10838
11273
  if (!itemList?.length)
10839
11274
  return "";
@@ -10854,13 +11289,22 @@ function getWeixinMessageTurnLane(full) {
10854
11289
  const textBody = extractTextBody(full.item_list).trim().toLowerCase();
10855
11290
  return textBody === "/cancel" || textBody === "/stop" || textBody === "/jx" ? "control" : "normal";
10856
11291
  }
10857
- function findMediaItem(itemList) {
11292
+ function findUnsupportedMediaItem(itemList) {
10858
11293
  if (!itemList?.length)
10859
11294
  return;
10860
- const direct = itemList.find((item) => item.type === MessageItemType.IMAGE && hasDownloadableMedia(item.image_item?.media)) ?? itemList.find((item) => item.type === MessageItemType.VIDEO && hasDownloadableMedia(item.video_item?.media)) ?? itemList.find((item) => item.type === MessageItemType.FILE && hasDownloadableMedia(item.file_item?.media)) ?? itemList.find((item) => item.type === MessageItemType.VOICE && hasDownloadableMedia(item.voice_item?.media) && !item.voice_item?.text);
11295
+ const direct = itemList.find((item) => item.type === MessageItemType.VIDEO || item.type === MessageItemType.FILE || item.type === MessageItemType.VOICE);
10861
11296
  if (direct)
10862
11297
  return direct;
10863
- const refItem = itemList.find((item) => item.type === MessageItemType.TEXT && item.ref_msg?.message_item && isMediaItem(item.ref_msg.message_item));
11298
+ const refItem = itemList.find((item) => item.type === MessageItemType.TEXT && item.ref_msg?.message_item && item.ref_msg.message_item.type !== MessageItemType.IMAGE && isMediaItem(item.ref_msg.message_item));
11299
+ return refItem?.ref_msg?.message_item ?? undefined;
11300
+ }
11301
+ function findImageMediaItem(itemList) {
11302
+ if (!itemList?.length)
11303
+ return;
11304
+ const direct = itemList.find((item) => item.type === MessageItemType.IMAGE);
11305
+ if (direct)
11306
+ return direct;
11307
+ const refItem = itemList.find((item) => item.type === MessageItemType.TEXT && item.ref_msg?.message_item?.type === MessageItemType.IMAGE);
10864
11308
  return refItem?.ref_msg?.message_item ?? undefined;
10865
11309
  }
10866
11310
  async function handleWeixinMessageTurn(full, deps) {
@@ -10906,6 +11350,20 @@ async function handleWeixinMessageTurn(full, deps) {
10906
11350
  }
10907
11351
  }).catch(() => {});
10908
11352
  };
11353
+ const contextToken = full.context_token;
11354
+ if (contextToken) {
11355
+ setContextToken(deps.accountId, full.from_user_id ?? "", contextToken);
11356
+ }
11357
+ const unsupportedMediaItem = findUnsupportedMediaItem(full.item_list);
11358
+ if (unsupportedMediaItem) {
11359
+ await sendInboundMediaUnavailableNotice({
11360
+ to,
11361
+ notice: inboundMediaUnavailableMessage(unsupportedMediaItem),
11362
+ contextToken,
11363
+ deps
11364
+ });
11365
+ return;
11366
+ }
10909
11367
  if (textBody.startsWith("/")) {
10910
11368
  const shouldTypeForSlash = isClearSlashCommand(textBody);
10911
11369
  if (shouldTypeForSlash) {
@@ -10936,14 +11394,12 @@ async function handleWeixinMessageTurn(full, deps) {
10936
11394
  }
10937
11395
  }
10938
11396
  }
10939
- const contextToken = full.context_token;
10940
- if (contextToken) {
10941
- setContextToken(deps.accountId, full.from_user_id ?? "", contextToken);
10942
- }
10943
11397
  startTypingIndicator();
10944
11398
  let media;
10945
- const mediaItem = findMediaItem(full.item_list);
11399
+ let inboundImagePath;
11400
+ const mediaItem = findImageMediaItem(full.item_list);
10946
11401
  if (mediaItem) {
11402
+ let mediaUnavailableNotice;
10947
11403
  try {
10948
11404
  const downloaded = await downloadMediaFromItem(mediaItem, {
10949
11405
  cdnBaseUrl: deps.cdnBaseUrl,
@@ -10953,24 +11409,24 @@ async function handleWeixinMessageTurn(full, deps) {
10953
11409
  label: "inbound"
10954
11410
  });
10955
11411
  if (downloaded.decryptedPicPath) {
11412
+ inboundImagePath = downloaded.decryptedPicPath;
10956
11413
  media = { type: "image", filePath: downloaded.decryptedPicPath, mimeType: "image/*" };
10957
- } else if (downloaded.decryptedVideoPath) {
10958
- media = { type: "video", filePath: downloaded.decryptedVideoPath, mimeType: "video/mp4" };
10959
- } else if (downloaded.decryptedFilePath) {
10960
- media = {
10961
- type: "file",
10962
- filePath: downloaded.decryptedFilePath,
10963
- mimeType: downloaded.fileMediaType ?? "application/octet-stream"
10964
- };
10965
- } else if (downloaded.decryptedVoicePath) {
10966
- media = {
10967
- type: "audio",
10968
- filePath: downloaded.decryptedVoicePath,
10969
- mimeType: downloaded.voiceMediaType ?? "audio/wav"
10970
- };
11414
+ } else {
11415
+ mediaUnavailableNotice = inboundMediaUnavailableMessage(mediaItem);
10971
11416
  }
10972
11417
  } catch (err) {
10973
11418
  deps.errLog(`media download failed: ${String(err)}`);
11419
+ mediaUnavailableNotice = inboundImageFailureMessage(err);
11420
+ }
11421
+ if (!media) {
11422
+ await sendInboundMediaUnavailableNotice({
11423
+ to,
11424
+ notice: mediaUnavailableNotice ?? inboundMediaUnavailableMessage(mediaItem),
11425
+ contextToken,
11426
+ deps
11427
+ });
11428
+ stopTypingIndicator();
11429
+ return;
10974
11430
  }
10975
11431
  }
10976
11432
  const sendReplySegment = async (text) => {
@@ -11108,13 +11564,18 @@ ${buildFinalHeadsUp({
11108
11564
  });
11109
11565
  }
11110
11566
  } finally {
11567
+ if (inboundImagePath) {
11568
+ await fs6.rm(inboundImagePath, { force: true }).catch((err) => {
11569
+ deps.errLog(`inbound image cleanup failed: ${String(err)}`);
11570
+ });
11571
+ }
11111
11572
  stopTypingIndicator();
11112
11573
  }
11113
11574
  }
11114
- var MAX_FINAL_CHUNK_BYTES = 1800, hasDownloadableMedia = (media) => media?.encrypt_query_param || media?.full_url;
11575
+ var MAX_FINAL_CHUNK_BYTES = 1800;
11115
11576
  var init_handle_weixin_message_turn = __esm(() => {
11116
11577
  init_api();
11117
- init_types();
11578
+ init_types2();
11118
11579
  init_media_download();
11119
11580
  init_mime();
11120
11581
  init_inbound();
@@ -11344,7 +11805,7 @@ var init_monitor = __esm(() => {
11344
11805
  init_config_cache();
11345
11806
  init_session_guard();
11346
11807
  init_handle_weixin_message_turn();
11347
- init_types();
11808
+ init_types2();
11348
11809
  init_sync_buf();
11349
11810
  init_logger();
11350
11811
  });
@@ -11630,7 +12091,7 @@ var init_app_logger = __esm(() => {
11630
12091
  });
11631
12092
 
11632
12093
  // src/transport/acpx-session-index.ts
11633
- import { readFile as readFile6 } from "node:fs/promises";
12094
+ import { readFile as readFile7 } from "node:fs/promises";
11634
12095
  import { homedir as homedir4 } from "node:os";
11635
12096
  import { resolve } from "node:path";
11636
12097
  async function resolveSessionAgentCommandFromIndex(session) {
@@ -11639,7 +12100,7 @@ async function resolveSessionAgentCommandFromIndex(session) {
11639
12100
  return;
11640
12101
  }
11641
12102
  try {
11642
- const raw = await readFile6(resolve(home, ".acpx", "sessions", "index.json"), "utf8");
12103
+ const raw = await readFile7(resolve(home, ".acpx", "sessions", "index.json"), "utf8");
11643
12104
  const parsed = JSON.parse(raw);
11644
12105
  const targetCwd = resolve(session.cwd);
11645
12106
  const match = parsed.entries?.find((entry) => entry.name === session.transportSession && entry.cwd === targetCwd && typeof entry.agentCommand === "string" && entry.agentCommand.trim().length > 0);
@@ -12853,50 +13314,60 @@ async function handleSessions(context, chatKey) {
12853
13314
  }
12854
13315
  async function handleSessionNew(context, chatKey, alias, agent, workspace) {
12855
13316
  const session = context.lifecycle.resolveSession(alias, agent, workspace, `${workspace}:${alias}`);
13317
+ const releaseTransportReservation = await context.lifecycle.reserveTransportSession(session.transportSession);
12856
13318
  try {
12857
- await context.lifecycle.ensureTransportSession(session);
12858
- const exists = await context.lifecycle.checkTransportSession(session);
12859
- if (!exists) {
12860
- return context.recovery.renderSessionCreationVerificationError(session);
13319
+ try {
13320
+ await context.lifecycle.ensureTransportSession(session);
13321
+ const exists = await context.lifecycle.checkTransportSession(session);
13322
+ if (!exists) {
13323
+ return context.recovery.renderSessionCreationVerificationError(session);
13324
+ }
13325
+ } catch (error2) {
13326
+ return context.recovery.renderSessionCreationError(session, error2);
12861
13327
  }
12862
- } catch (error2) {
12863
- return context.recovery.renderSessionCreationError(session, error2);
13328
+ await context.sessions.attachSession(alias, agent, workspace, session.transportSession);
13329
+ await context.lifecycle.refreshSessionTransportAgentCommand(alias);
13330
+ await context.sessions.useSession(chatKey, alias);
13331
+ await context.logger.info("session.created", "created and selected logical session", {
13332
+ alias,
13333
+ agent,
13334
+ workspace
13335
+ });
13336
+ return { text: `会话「${alias}」已创建并切换` };
13337
+ } finally {
13338
+ await releaseTransportReservation();
12864
13339
  }
12865
- await context.sessions.attachSession(alias, agent, workspace, session.transportSession);
12866
- await context.lifecycle.refreshSessionTransportAgentCommand(alias);
12867
- await context.sessions.useSession(chatKey, alias);
12868
- await context.logger.info("session.created", "created and selected logical session", {
12869
- alias,
12870
- agent,
12871
- workspace
12872
- });
12873
- return { text: `会话「${alias}」已创建并切换` };
12874
13340
  }
12875
13341
  async function handleSessionShortcut(context, chatKey, agent, target, createNew) {
12876
13342
  return await context.lifecycle.handleSessionShortcut(chatKey, agent, target, createNew);
12877
13343
  }
12878
13344
  async function handleSessionAttach(context, chatKey, alias, agent, workspace, transportSession) {
12879
13345
  const attached = context.lifecycle.resolveSession(alias, agent, workspace, transportSession);
12880
- const exists = await context.lifecycle.checkTransportSession(attached);
12881
- if (!exists) {
12882
- return {
12883
- text: [
12884
- "没有找到可绑定的已有会话。",
12885
- `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${workspace} --name <会话名>`
12886
- ].join(`
13346
+ const releaseTransportReservation = await context.lifecycle.reserveTransportSession(attached.transportSession);
13347
+ try {
13348
+ const exists = await context.lifecycle.checkTransportSession(attached);
13349
+ if (!exists) {
13350
+ return {
13351
+ text: [
13352
+ "没有找到可绑定的已有会话。",
13353
+ `请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${workspace} --name <会话名>`
13354
+ ].join(`
12887
13355
  `)
12888
- };
13356
+ };
13357
+ }
13358
+ await context.sessions.attachSession(alias, agent, workspace, transportSession);
13359
+ await context.lifecycle.refreshSessionTransportAgentCommand(alias);
13360
+ await context.sessions.useSession(chatKey, alias);
13361
+ await context.logger.info("session.attached", "attached existing transport session", {
13362
+ alias,
13363
+ agent,
13364
+ workspace,
13365
+ transportSession
13366
+ });
13367
+ return { text: `会话「${alias}」已绑定并切换` };
13368
+ } finally {
13369
+ await releaseTransportReservation();
12889
13370
  }
12890
- await context.sessions.attachSession(alias, agent, workspace, transportSession);
12891
- await context.lifecycle.refreshSessionTransportAgentCommand(alias);
12892
- await context.sessions.useSession(chatKey, alias);
12893
- await context.logger.info("session.attached", "attached existing transport session", {
12894
- alias,
12895
- agent,
12896
- workspace,
12897
- transportSession
12898
- });
12899
- return { text: `会话「${alias}」已绑定并切换` };
12900
13371
  }
12901
13372
  async function handleSessionUse(context, chatKey, alias) {
12902
13373
  await context.sessions.useSession(chatKey, alias);
@@ -13069,7 +13540,7 @@ async function handleSessionRemove(context, chatKey, alias) {
13069
13540
  return { text: lines.join(`
13070
13541
  `) };
13071
13542
  }
13072
- async function promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId) {
13543
+ async function promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media) {
13073
13544
  const effectiveReplyMode = session.replyMode ?? context.config?.wechat.replyMode ?? "verbose";
13074
13545
  const transportReply = effectiveReplyMode !== "final" ? reply : undefined;
13075
13546
  if (context.orchestration) {
@@ -13092,7 +13563,7 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
13092
13563
  const { promptText, taskIds, groupIds, claimHumanReply } = await preparePromptWithFallback(context, session, chatKey, text, replyContextToken, accountId);
13093
13564
  try {
13094
13565
  const replyContext = transportReply && context.quota ? { chatKey, quota: context.quota } : undefined;
13095
- const result = await context.interaction.promptTransportSession(session, promptText, transportReply, replyContext);
13566
+ const result = await context.interaction.promptTransportSession(session, promptText, transportReply, replyContext, media);
13096
13567
  if (claimHumanReply) {
13097
13568
  try {
13098
13569
  await context.orchestration?.claimActiveHumanReply?.(claimHumanReply);
@@ -13112,17 +13583,17 @@ async function promptWithSession(context, session, chatKey, text, reply, replyCo
13112
13583
  throw error2;
13113
13584
  }
13114
13585
  }
13115
- async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId) {
13586
+ async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media) {
13116
13587
  const session = await context.sessions.getCurrentSession(chatKey);
13117
13588
  if (!session) {
13118
13589
  return { text: NO_CURRENT_SESSION_TEXT };
13119
13590
  }
13120
13591
  try {
13121
- return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId);
13592
+ return await promptWithSession(context, session, chatKey, text, reply, replyContextToken, accountId, media);
13122
13593
  } catch (error2) {
13123
13594
  const recovered = await context.recovery.tryRecoverMissingSession(session, error2);
13124
13595
  if (recovered) {
13125
- return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId);
13596
+ return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media);
13126
13597
  }
13127
13598
  return context.recovery.renderTransportError(session, error2);
13128
13599
  }
@@ -13850,34 +14321,39 @@ async function handleSessionShortcutCommand(context, ops, chatKey, agent, target
13850
14321
  };
13851
14322
  }
13852
14323
  const session = ops.resolveSession(alias, agent, workspace.name, alias);
14324
+ const releaseTransportReservation = await ops.reserveTransportSession(session.transportSession);
13853
14325
  try {
13854
- await ops.ensureTransportSession(session);
13855
- const exists = await ops.checkTransportSession(session);
13856
- if (!exists) {
14326
+ try {
14327
+ await ops.ensureTransportSession(session);
14328
+ const exists = await ops.checkTransportSession(session);
14329
+ if (!exists) {
14330
+ return renderShortcutSessionCreationError(workspace, alias);
14331
+ }
14332
+ } catch (err) {
14333
+ if (err instanceof AutoInstallFailedError)
14334
+ throw err;
13857
14335
  return renderShortcutSessionCreationError(workspace, alias);
13858
14336
  }
13859
- } catch (err) {
13860
- if (err instanceof AutoInstallFailedError)
13861
- throw err;
13862
- return renderShortcutSessionCreationError(workspace, alias);
13863
- }
13864
- await context.sessions.attachSession(alias, agent, workspace.name, session.transportSession);
13865
- await ops.refreshSessionTransportAgentCommand(alias);
13866
- await context.sessions.useSession(chatKey, alias);
13867
- await context.logger.info("session.shortcut.created", "created new logical session from shortcut", {
13868
- alias,
13869
- workspace: workspace.name,
13870
- agent,
13871
- workspaceReused: workspace.reused
13872
- });
13873
- return {
13874
- text: [
13875
- `已创建并切换到会话「${alias}」`,
13876
- workspace.reused ? `- 复用工作区:${workspace.name}` : `- 新增工作区:${workspace.name} -> ${workspace.cwd}`,
13877
- `- 新增会话:${alias}`
13878
- ].join(`
14337
+ await context.sessions.attachSession(alias, agent, workspace.name, session.transportSession);
14338
+ await ops.refreshSessionTransportAgentCommand(alias);
14339
+ await context.sessions.useSession(chatKey, alias);
14340
+ await context.logger.info("session.shortcut.created", "created new logical session from shortcut", {
14341
+ alias,
14342
+ workspace: workspace.name,
14343
+ agent,
14344
+ workspaceReused: workspace.reused
14345
+ });
14346
+ return {
14347
+ text: [
14348
+ `已创建并切换到会话「${alias}」`,
14349
+ workspace.reused ? `- 复用工作区:${workspace.name}` : `- 新增工作区:${workspace.name} -> ${workspace.cwd}`,
14350
+ `- 新增会话:${alias}`
14351
+ ].join(`
13879
14352
  `)
13880
- };
14353
+ };
14354
+ } finally {
14355
+ await releaseTransportReservation();
14356
+ }
13881
14357
  }
13882
14358
  async function resolveShortcutWorkspace(context, target) {
13883
14359
  if (target.workspace) {
@@ -14357,31 +14833,36 @@ async function handleSessionResetCommand(context, ops, chatKey) {
14357
14833
  return { text: NO_CURRENT_SESSION_TEXT3 };
14358
14834
  }
14359
14835
  const resetSession = ops.resolveSession(session.alias, session.agent, session.workspace, buildResetTransportSessionName(session, ops.now()));
14836
+ const releaseTransportReservation = await ops.reserveTransportSession(resetSession.transportSession);
14360
14837
  try {
14361
- await ops.ensureTransportSession(resetSession);
14362
- const exists = await ops.checkTransportSession(resetSession);
14363
- if (!exists) {
14364
- return {
14365
- text: [
14366
- `会话「${session.alias}」重置失败。`,
14367
- "新的后端会话未创建成功,请稍后重试。"
14368
- ].join(`
14838
+ try {
14839
+ await ops.ensureTransportSession(resetSession);
14840
+ const exists = await ops.checkTransportSession(resetSession);
14841
+ if (!exists) {
14842
+ return {
14843
+ text: [
14844
+ `会话「${session.alias}」重置失败。`,
14845
+ "新的后端会话未创建成功,请稍后重试。"
14846
+ ].join(`
14369
14847
  `)
14370
- };
14371
- }
14372
- } catch (error2) {
14373
- return renderTransportError(resetSession, error2);
14374
- }
14375
- await context.sessions.attachSession(resetSession.alias, resetSession.agent, resetSession.workspace, resetSession.transportSession);
14376
- await ops.refreshSessionTransportAgentCommand(resetSession.alias);
14377
- await context.sessions.useSession(chatKey, resetSession.alias);
14378
- await context.logger.info("session.reset", "reset current logical session", {
14379
- alias: resetSession.alias,
14380
- agent: resetSession.agent,
14381
- workspace: resetSession.workspace,
14382
- transportSession: resetSession.transportSession,
14383
- chatKey
14384
- });
14848
+ };
14849
+ }
14850
+ } catch (error2) {
14851
+ return renderTransportError(resetSession, error2);
14852
+ }
14853
+ await context.sessions.attachSession(resetSession.alias, resetSession.agent, resetSession.workspace, resetSession.transportSession);
14854
+ await ops.refreshSessionTransportAgentCommand(resetSession.alias);
14855
+ await context.sessions.useSession(chatKey, resetSession.alias);
14856
+ await context.logger.info("session.reset", "reset current logical session", {
14857
+ alias: resetSession.alias,
14858
+ agent: resetSession.agent,
14859
+ workspace: resetSession.workspace,
14860
+ transportSession: resetSession.transportSession,
14861
+ chatKey
14862
+ });
14863
+ } finally {
14864
+ await releaseTransportReservation();
14865
+ }
14385
14866
  return { text: `会话「${resetSession.alias}」已重置` };
14386
14867
  }
14387
14868
  function buildResetTransportSessionName(session, now) {
@@ -14420,7 +14901,7 @@ class CommandRouter {
14420
14901
  this.quota = quota;
14421
14902
  this.logger = logger2 ?? createNoopAppLogger();
14422
14903
  }
14423
- async handle(chatKey, input, reply, replyContextToken, accountId) {
14904
+ async handle(chatKey, input, reply, replyContextToken, accountId, media) {
14424
14905
  const startedAt = Date.now();
14425
14906
  const command = parseCommand(input);
14426
14907
  await this.logger.debug("command.parsed", "parsed inbound command", {
@@ -14523,7 +15004,7 @@ class CommandRouter {
14523
15004
  case "task.cancel":
14524
15005
  return await handleTaskCancel(this.createHandlerContext(), chatKey, command.taskId);
14525
15006
  case "prompt":
14526
- return await handlePrompt(this.createSessionHandlerContext(), chatKey, command.text, reply, replyContextToken, accountId);
15007
+ return await handlePrompt(this.createSessionHandlerContext(), chatKey, command.text, reply, replyContextToken, accountId, media);
14527
15008
  }
14528
15009
  });
14529
15010
  }
@@ -14555,6 +15036,7 @@ class CommandRouter {
14555
15036
  resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
14556
15037
  ensureTransportSession: (session, replyOverride) => this.ensureTransportSession(session, replyOverride ?? reply),
14557
15038
  checkTransportSession: (session) => this.checkTransportSession(session),
15039
+ reserveTransportSession: (transportSession) => this.reserveLogicalTransportSession(transportSession),
14558
15040
  handleSessionShortcut: async (chatKey, agent, target, createNew, replyOverride) => {
14559
15041
  try {
14560
15042
  return await handleSessionShortcutCommand(this.createHandlerContext(), this.createSessionShortcutOps(replyOverride ?? reply), chatKey, agent, target, createNew);
@@ -14574,7 +15056,7 @@ class CommandRouter {
14574
15056
  return {
14575
15057
  setModeTransportSession: (session, modeId) => this.setModeTransportSession(session, modeId),
14576
15058
  cancelTransportSession: (session) => this.cancelTransportSession(session),
14577
- promptTransportSession: (session, text, reply, replyContext) => this.promptTransportSession(session, text, reply, replyContext)
15059
+ promptTransportSession: (session, text, reply, replyContext, media) => this.promptTransportSession(session, text, reply, replyContext, media)
14578
15060
  };
14579
15061
  }
14580
15062
  createSessionRenderRecoveryOps() {
@@ -14589,6 +15071,7 @@ class CommandRouter {
14589
15071
  return {
14590
15072
  ensureTransportSession: (session, replyOverride) => this.ensureTransportSession(session, replyOverride ?? reply),
14591
15073
  checkTransportSession: (session) => this.checkTransportSession(session),
15074
+ reserveTransportSession: (transportSession) => this.reserveLogicalTransportSession(transportSession),
14592
15075
  resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
14593
15076
  refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias),
14594
15077
  now: () => Date.now()
@@ -14606,9 +15089,16 @@ class CommandRouter {
14606
15089
  resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
14607
15090
  ensureTransportSession: (session, replyOverride) => this.ensureTransportSession(session, replyOverride ?? reply),
14608
15091
  checkTransportSession: (session) => this.checkTransportSession(session),
15092
+ reserveTransportSession: (transportSession) => this.reserveLogicalTransportSession(transportSession),
14609
15093
  refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias)
14610
15094
  };
14611
15095
  }
15096
+ async reserveLogicalTransportSession(transportSession) {
15097
+ if (this.orchestration?.reserveLogicalTransportSession) {
15098
+ return await this.orchestration.reserveLogicalTransportSession(transportSession);
15099
+ }
15100
+ return async () => {};
15101
+ }
14612
15102
  replaceConfig(updated) {
14613
15103
  if (!this.config) {
14614
15104
  return;
@@ -14722,9 +15212,9 @@ class CommandRouter {
14722
15212
  async checkTransportSession(session) {
14723
15213
  return await this.measureTransportCall("has_session", session, () => this.transport.hasSession(session));
14724
15214
  }
14725
- async promptTransportSession(session, text, reply, replyContext) {
15215
+ async promptTransportSession(session, text, reply, replyContext, media) {
14726
15216
  session.mcpCoordinatorSession ??= session.transportSession;
14727
- return await this.measureTransportCall("prompt", session, () => this.transport.prompt(session, text, reply, replyContext));
15217
+ return await this.measureTransportCall("prompt", session, () => this.transport.prompt(session, text, reply, replyContext, media ? { media } : undefined));
14728
15218
  }
14729
15219
  async setModeTransportSession(session, modeId) {
14730
15220
  return await this.measureTransportCall("set_mode", session, () => this.transport.setMode(session, modeId));
@@ -14852,15 +15342,21 @@ class ConsoleAgent {
14852
15342
  this.logger = logger2 ?? createNoopAppLogger();
14853
15343
  }
14854
15344
  async chat(request) {
14855
- if (!request.text.trim()) {
15345
+ const hasText = request.text.trim().length > 0;
15346
+ if (!hasText && !request.media) {
14856
15347
  return { text: "消息内容为空。" };
14857
15348
  }
15349
+ if (request.media && request.media.type !== "image") {
15350
+ return {
15351
+ text: hasText ? "暂不支持处理该类型附件;请发送文字或图片。" : "暂不支持处理该类型消息,请发送文字或图片。"
15352
+ };
15353
+ }
14858
15354
  await this.logger.info("chat.received", "received inbound chat message", {
14859
15355
  chatKey: request.conversationId,
14860
15356
  kind: request.text.trim().startsWith("/") ? "command" : "prompt",
14861
15357
  text: summarizeText(request.text)
14862
15358
  });
14863
- return await this.router.handle(request.conversationId, request.text, request.reply, request.replyContextToken, request.accountId);
15359
+ return await this.router.handle(request.conversationId, request.text, request.reply, request.replyContextToken, request.accountId, request.media);
14864
15360
  }
14865
15361
  async clearSession(conversationId) {
14866
15362
  await this.router.clearSession?.(conversationId);
@@ -14877,6 +15373,24 @@ var init_console_agent = __esm(() => {
14877
15373
  init_app_logger();
14878
15374
  });
14879
15375
 
15376
+ // src/orchestration/async-mutex.ts
15377
+ class AsyncMutex {
15378
+ tail = Promise.resolve();
15379
+ async run(critical) {
15380
+ const previous = this.tail;
15381
+ let release;
15382
+ this.tail = new Promise((resolve2) => {
15383
+ release = resolve2;
15384
+ });
15385
+ await previous;
15386
+ try {
15387
+ return await critical();
15388
+ } finally {
15389
+ release();
15390
+ }
15391
+ }
15392
+ }
15393
+
14880
15394
  // src/orchestration/orchestration-server.ts
14881
15395
  import { rm as rm6 } from "node:fs/promises";
14882
15396
  import { createConnection as createConnection2, createServer } from "node:net";
@@ -14976,37 +15490,52 @@ class OrchestrationServer {
14976
15490
  }
14977
15491
  async dispatch(method, params) {
14978
15492
  switch (method) {
15493
+ case "coordinator.register_external":
15494
+ return await this.handlers.registerExternalCoordinator(this.parseRegisterExternalCoordinatorInput(params));
14979
15495
  case "delegate.request":
14980
- return await this.handlers.requestDelegate(params);
15496
+ return await this.handlers.requestDelegate(this.parseRequestDelegateRpcInput(params));
14981
15497
  case "task.get":
14982
15498
  return await this.dispatchTaskGet(params);
14983
15499
  case "task.list":
14984
- return await this.handlers.listTasks(requireOptionalObject(params, "filter"));
15500
+ return await this.handlers.listTasks(this.parseTaskListFilter(params));
15501
+ case "task.wait":
15502
+ return await this.handlers.waitTask(this.parseWaitTaskInput(params));
14985
15503
  case "task.approve":
15504
+ requireOnlyKeys(params, ["taskId", "coordinatorSession"], "params");
14986
15505
  return await this.handlers.approveTask({
14987
15506
  taskId: requireString(params, "taskId"),
14988
15507
  coordinatorSession: requireString(params, "coordinatorSession")
14989
15508
  });
14990
15509
  case "task.reject":
15510
+ requireOnlyKeys(params, ["taskId", "coordinatorSession"], "params");
14991
15511
  return await this.handlers.rejectTask({
14992
15512
  taskId: requireString(params, "taskId"),
14993
15513
  coordinatorSession: requireString(params, "coordinatorSession")
14994
15514
  });
14995
15515
  case "task.cancel":
14996
- return await this.handlers.cancelTask(params);
15516
+ return await this.handlers.cancelTask(this.parseCancelTaskInput(params));
14997
15517
  case "worker.reply":
14998
- await this.handlers.recordWorkerReply(params);
15518
+ await this.handlers.recordWorkerReply(this.parseRecordWorkerReplyInput(params));
14999
15519
  return { accepted: true };
15000
15520
  case "worker.raise_question":
15001
15521
  return await this.handlers.workerRaiseQuestion(this.parseWorkerRaiseQuestionInput(params));
15002
15522
  case "coordinator.answer_question":
15523
+ requireOnlyKeys(params, ["coordinatorSession", "taskId", "questionId", "answer"], "params");
15003
15524
  return await this.handlers.coordinatorAnswerQuestion({
15004
15525
  coordinatorSession: requireString(params, "coordinatorSession"),
15005
15526
  taskId: requireString(params, "taskId"),
15006
15527
  questionId: requireString(params, "questionId"),
15007
15528
  answer: requireString(params, "answer")
15008
15529
  });
15530
+ case "coordinator.retract_answer":
15531
+ requireOnlyKeys(params, ["coordinatorSession", "taskId", "questionId"], "params");
15532
+ return await this.handlers.coordinatorRetractAnswer({
15533
+ coordinatorSession: requireString(params, "coordinatorSession"),
15534
+ taskId: requireString(params, "taskId"),
15535
+ questionId: requireString(params, "questionId")
15536
+ });
15009
15537
  case "coordinator.request_human_input": {
15538
+ requireOnlyKeys(params, ["coordinatorSession", "taskQuestions", "promptText", "expectedActivePackageId"], "params");
15010
15539
  const expectedActivePackageId = requireOptionalString(params, "expectedActivePackageId");
15011
15540
  return await this.handlers.coordinatorRequestHumanInput({
15012
15541
  coordinatorSession: requireString(params, "coordinatorSession"),
@@ -15016,6 +15545,7 @@ class OrchestrationServer {
15016
15545
  });
15017
15546
  }
15018
15547
  case "coordinator.follow_up_human_package":
15548
+ requireOnlyKeys(params, ["coordinatorSession", "packageId", "priorMessageId", "taskQuestions", "promptText"], "params");
15019
15549
  return await this.handlers.coordinatorFollowUpHumanPackage({
15020
15550
  coordinatorSession: requireString(params, "coordinatorSession"),
15021
15551
  packageId: requireString(params, "packageId"),
@@ -15024,6 +15554,7 @@ class OrchestrationServer {
15024
15554
  promptText: requireString(params, "promptText")
15025
15555
  });
15026
15556
  case "coordinator.review_contested_result":
15557
+ requireOnlyKeys(params, ["coordinatorSession", "taskId", "reviewId", "decision"], "params");
15027
15558
  return await this.handlers.coordinatorReviewContestedResult({
15028
15559
  coordinatorSession: requireString(params, "coordinatorSession"),
15029
15560
  taskId: requireString(params, "taskId"),
@@ -15031,11 +15562,13 @@ class OrchestrationServer {
15031
15562
  decision: requireEnum(params, "decision", ["accept", "discard"])
15032
15563
  });
15033
15564
  case "group.new":
15565
+ requireOnlyKeys(params, ["coordinatorSession", "title"], "params");
15034
15566
  return await this.handlers.createGroup({
15035
15567
  coordinatorSession: requireString(params, "coordinatorSession"),
15036
15568
  title: requireString(params, "title")
15037
15569
  });
15038
15570
  case "group.get":
15571
+ requireOnlyKeys(params, ["coordinatorSession", "groupId"], "params");
15039
15572
  return await this.handlers.getGroupSummary({
15040
15573
  coordinatorSession: requireString(params, "coordinatorSession"),
15041
15574
  groupId: requireString(params, "groupId")
@@ -15043,6 +15576,7 @@ class OrchestrationServer {
15043
15576
  case "group.list":
15044
15577
  return await this.handlers.listGroupSummaries(this.parseGroupListFilter(params));
15045
15578
  case "group.cancel":
15579
+ requireOnlyKeys(params, ["coordinatorSession", "groupId"], "params");
15046
15580
  return await this.handlers.cancelGroup({
15047
15581
  coordinatorSession: requireString(params, "coordinatorSession"),
15048
15582
  groupId: requireString(params, "groupId")
@@ -15051,19 +15585,110 @@ class OrchestrationServer {
15051
15585
  throw new OrchestrationInvalidRequestError(`unsupported orchestration method: ${method}`);
15052
15586
  }
15053
15587
  }
15588
+ parseRegisterExternalCoordinatorInput(params) {
15589
+ requireOnlyKeys(params, ["coordinatorSession", "workspace", "defaultTargetAgent"], "params");
15590
+ const workspace = requireOptionalString(params, "workspace");
15591
+ const defaultTargetAgent = requireOptionalString(params, "defaultTargetAgent");
15592
+ return {
15593
+ coordinatorSession: requireString(params, "coordinatorSession"),
15594
+ ...workspace !== undefined ? { workspace } : {},
15595
+ ...defaultTargetAgent !== undefined ? { defaultTargetAgent } : {}
15596
+ };
15597
+ }
15054
15598
  async dispatchTaskGet(params) {
15599
+ requireOnlyKeys(params, ["taskId", "coordinatorSession"], "params");
15055
15600
  const taskId = requireString(params, "taskId");
15056
- const coordinatorSession = requireOptionalString(params, "coordinatorSession");
15601
+ const coordinatorSession = requireString(params, "coordinatorSession");
15057
15602
  const task = await this.handlers.getTask(taskId);
15058
15603
  if (!task) {
15059
15604
  return null;
15060
15605
  }
15061
- if (coordinatorSession !== undefined && task.coordinatorSession !== coordinatorSession) {
15606
+ if (task.coordinatorSession !== coordinatorSession) {
15062
15607
  return null;
15063
15608
  }
15064
15609
  return task;
15065
15610
  }
15611
+ parseRequestDelegateRpcInput(params) {
15612
+ requireOnlyKeys(params, ["sourceHandle", "targetAgent", "task", "cwd", "role", "groupId"], "params");
15613
+ const cwd = requireOptionalString(params, "cwd");
15614
+ const role = requireOptionalString(params, "role");
15615
+ const groupId = requireOptionalString(params, "groupId");
15616
+ return {
15617
+ sourceHandle: requireString(params, "sourceHandle"),
15618
+ targetAgent: requireString(params, "targetAgent"),
15619
+ task: requireString(params, "task"),
15620
+ ...cwd !== undefined ? { cwd } : {},
15621
+ ...role !== undefined ? { role } : {},
15622
+ ...groupId !== undefined ? { groupId } : {}
15623
+ };
15624
+ }
15625
+ parseTaskListFilter(params) {
15626
+ requireOnlyKeys(params, ["filter"], "params");
15627
+ const filter = requireOptionalObject(params, "filter");
15628
+ if (!filter) {
15629
+ throw new OrchestrationInvalidRequestError("filter must include coordinatorSession");
15630
+ }
15631
+ requireOnlyKeys(filter, ["coordinatorSession", "status", "stuck", "sort", "order"], "filter");
15632
+ const status = requireOptionalEnum(filter, "status", [
15633
+ "pending",
15634
+ "needs_confirmation",
15635
+ "running",
15636
+ "blocked",
15637
+ "waiting_for_human",
15638
+ "completed",
15639
+ "failed",
15640
+ "cancelled"
15641
+ ]);
15642
+ const stuck = requireOptionalBoolean(filter, "stuck");
15643
+ const sort = requireOptionalEnum(filter, "sort", ["updatedAt", "createdAt"]);
15644
+ const order = requireOptionalEnum(filter, "order", ["asc", "desc"]);
15645
+ return {
15646
+ coordinatorSession: requireString(filter, "coordinatorSession"),
15647
+ ...status !== undefined ? { status } : {},
15648
+ ...stuck !== undefined ? { stuck } : {},
15649
+ ...sort !== undefined ? { sort } : {},
15650
+ ...order !== undefined ? { order } : {}
15651
+ };
15652
+ }
15653
+ parseCancelTaskInput(params) {
15654
+ requireOnlyKeys(params, ["taskId", "sourceHandle", "coordinatorSession"], "params");
15655
+ const sourceHandle = requireOptionalString(params, "sourceHandle");
15656
+ const coordinatorSession = requireOptionalString(params, "coordinatorSession");
15657
+ if (sourceHandle === undefined && coordinatorSession === undefined) {
15658
+ throw new OrchestrationInvalidRequestError("task.cancel requires sourceHandle or coordinatorSession");
15659
+ }
15660
+ return {
15661
+ taskId: requireString(params, "taskId"),
15662
+ ...sourceHandle !== undefined ? { sourceHandle } : {},
15663
+ ...coordinatorSession !== undefined ? { coordinatorSession } : {}
15664
+ };
15665
+ }
15666
+ parseRecordWorkerReplyInput(params) {
15667
+ requireOnlyKeys(params, ["taskId", "sourceHandle", "status", "summary", "resultText"], "params");
15668
+ const status = requireOptionalEnum(params, "status", ["completed", "failed", "cancelled"]);
15669
+ const summary = requireOptionalString(params, "summary");
15670
+ const resultText = requireOptionalString(params, "resultText");
15671
+ return {
15672
+ taskId: requireString(params, "taskId"),
15673
+ sourceHandle: requireString(params, "sourceHandle"),
15674
+ ...status !== undefined ? { status } : {},
15675
+ ...summary !== undefined ? { summary } : {},
15676
+ ...resultText !== undefined ? { resultText } : {}
15677
+ };
15678
+ }
15679
+ parseWaitTaskInput(params) {
15680
+ requireOnlyKeys(params, ["coordinatorSession", "taskId", "timeoutMs", "pollIntervalMs"], "params");
15681
+ const timeoutMs = requireOptionalIntegerInRange(params, "timeoutMs", 0, MAX_TASK_WAIT_TIMEOUT_MS);
15682
+ const pollIntervalMs = requireOptionalIntegerInRange(params, "pollIntervalMs", 1, MAX_TASK_WAIT_POLL_INTERVAL_MS);
15683
+ return {
15684
+ coordinatorSession: requireString(params, "coordinatorSession"),
15685
+ taskId: requireString(params, "taskId"),
15686
+ ...timeoutMs !== undefined ? { timeoutMs } : {},
15687
+ ...pollIntervalMs !== undefined ? { pollIntervalMs } : {}
15688
+ };
15689
+ }
15066
15690
  parseWorkerRaiseQuestionInput(params) {
15691
+ requireOnlyKeys(params, ["taskId", "sourceHandle", "question", "whyBlocked", "whatIsNeeded"], "params");
15067
15692
  return {
15068
15693
  taskId: requireString(params, "taskId"),
15069
15694
  sourceHandle: requireString(params, "sourceHandle"),
@@ -15073,6 +15698,7 @@ class OrchestrationServer {
15073
15698
  };
15074
15699
  }
15075
15700
  parseGroupListFilter(params) {
15701
+ requireOnlyKeys(params, ["coordinatorSession", "status", "stuck", "sort", "order"], "params");
15076
15702
  const status = requireOptionalEnum(params, "status", ["pending", "running", "terminal"]);
15077
15703
  const stuck = requireOptionalBoolean(params, "stuck");
15078
15704
  const sort = requireOptionalEnum(params, "sort", ["updatedAt", "createdAt"]);
@@ -15161,6 +15787,26 @@ function requireString(params, key) {
15161
15787
  }
15162
15788
  return value;
15163
15789
  }
15790
+ function requireOptionalNumber(params, key) {
15791
+ const value = params[key];
15792
+ if (value === undefined) {
15793
+ return;
15794
+ }
15795
+ if (typeof value !== "number" || !Number.isFinite(value)) {
15796
+ throw new OrchestrationInvalidRequestError(`${key} must be a finite number when provided`);
15797
+ }
15798
+ return value;
15799
+ }
15800
+ function requireOptionalIntegerInRange(params, key, min, max) {
15801
+ const value = requireOptionalNumber(params, key);
15802
+ if (value === undefined) {
15803
+ return;
15804
+ }
15805
+ if (!Number.isInteger(value) || value < min || value > max) {
15806
+ throw new OrchestrationInvalidRequestError(`${key} must be an integer between ${min} and ${max} when provided`);
15807
+ }
15808
+ return value;
15809
+ }
15164
15810
  function requireOptionalString(params, key) {
15165
15811
  const value = params[key];
15166
15812
  if (value === undefined) {
@@ -15208,6 +15854,14 @@ function requireOptionalObject(params, key) {
15208
15854
  }
15209
15855
  return value;
15210
15856
  }
15857
+ function requireOnlyKeys(params, allowed, label) {
15858
+ const allowedSet = new Set(allowed);
15859
+ for (const key of Object.keys(params)) {
15860
+ if (!allowedSet.has(key)) {
15861
+ throw new OrchestrationInvalidRequestError(`${label}.${key} is not supported`);
15862
+ }
15863
+ }
15864
+ }
15211
15865
  function requireTaskQuestions(params, key) {
15212
15866
  const value = params[key];
15213
15867
  if (!Array.isArray(value) || value.length === 0) {
@@ -15267,18 +15921,22 @@ function isServerNotRunningError(error2) {
15267
15921
  var OrchestrationInvalidRequestError, ORCHESTRATION_RPC_METHODS;
15268
15922
  var init_orchestration_server = __esm(() => {
15269
15923
  init_orchestration_ipc();
15924
+ init_task_wait_timeouts();
15270
15925
  OrchestrationInvalidRequestError = class OrchestrationInvalidRequestError extends Error {
15271
15926
  };
15272
15927
  ORCHESTRATION_RPC_METHODS = new Set([
15928
+ "coordinator.register_external",
15273
15929
  "delegate.request",
15274
15930
  "task.get",
15275
15931
  "task.list",
15932
+ "task.wait",
15276
15933
  "task.approve",
15277
15934
  "task.reject",
15278
15935
  "task.cancel",
15279
15936
  "worker.reply",
15280
15937
  "worker.raise_question",
15281
15938
  "coordinator.answer_question",
15939
+ "coordinator.retract_answer",
15282
15940
  "coordinator.request_human_input",
15283
15941
  "coordinator.follow_up_human_package",
15284
15942
  "coordinator.review_contested_result",
@@ -15289,24 +15947,6 @@ var init_orchestration_server = __esm(() => {
15289
15947
  ]);
15290
15948
  });
15291
15949
 
15292
- // src/orchestration/async-mutex.ts
15293
- class AsyncMutex {
15294
- tail = Promise.resolve();
15295
- async run(critical) {
15296
- const previous = this.tail;
15297
- let release;
15298
- this.tail = new Promise((resolve2) => {
15299
- release = resolve2;
15300
- });
15301
- await previous;
15302
- try {
15303
- return await critical();
15304
- } finally {
15305
- release();
15306
- }
15307
- }
15308
- }
15309
-
15310
15950
  // src/orchestration/progress-line-parser.ts
15311
15951
  class ProgressLineBuffer {
15312
15952
  feed(segment) {
@@ -15331,15 +15971,67 @@ function stripProgressLines(text) {
15331
15971
  var PROGRESS_PREFIX = "[PROGRESS]";
15332
15972
 
15333
15973
  // src/orchestration/orchestration-service.ts
15974
+ import { createHash as createHash2 } from "node:crypto";
15975
+ import { basename as basename3, isAbsolute, normalize } from "node:path";
15976
+
15334
15977
  class OrchestrationService {
15335
15978
  deps;
15336
- stateMutex = new AsyncMutex;
15979
+ stateMutex;
15980
+ pendingWorkerSessions = new Map;
15981
+ pendingLogicalTransportSessions = new Map;
15337
15982
  constructor(deps) {
15338
15983
  this.deps = deps;
15984
+ this.stateMutex = deps.stateMutex ?? new AsyncMutex;
15339
15985
  }
15340
15986
  async mutate(critical) {
15341
15987
  return await this.stateMutex.run(critical);
15342
15988
  }
15989
+ async registerExternalCoordinator(input) {
15990
+ const coordinatorSession = input.coordinatorSession.trim();
15991
+ const workspace = input.workspace?.trim();
15992
+ const defaultTargetAgent = input.defaultTargetAgent?.trim();
15993
+ if (!coordinatorSession) {
15994
+ throw new Error("coordinatorSession must be a non-empty string");
15995
+ }
15996
+ if (workspace && !this.deps.config.workspaces[workspace]) {
15997
+ throw new Error(`workspace "${workspace}" is not configured`);
15998
+ }
15999
+ return await this.mutate(async () => {
16000
+ const state = await this.deps.loadState();
16001
+ const externalCoordinators = this.ensureExternalCoordinators(state);
16002
+ const existing = externalCoordinators[coordinatorSession];
16003
+ if ((this.pendingWorkerSessions.get(coordinatorSession) ?? 0) > 0) {
16004
+ throw new Error(`coordinatorSession "${coordinatorSession}" conflicts with an existing worker session`);
16005
+ }
16006
+ if (state.orchestration.workerBindings[coordinatorSession]) {
16007
+ throw new Error(`coordinatorSession "${coordinatorSession}" conflicts with an existing worker session`);
16008
+ }
16009
+ if (this.hasActiveTaskWorkerSession(state, coordinatorSession)) {
16010
+ throw new Error(`coordinatorSession "${coordinatorSession}" conflicts with an existing worker session`);
16011
+ }
16012
+ if ((this.pendingLogicalTransportSessions.get(coordinatorSession) ?? 0) > 0) {
16013
+ throw new Error(`coordinatorSession "${coordinatorSession}" conflicts with an existing logical session`);
16014
+ }
16015
+ if (Object.values(state.sessions).some((session) => session.transport_session === coordinatorSession)) {
16016
+ throw new Error(`coordinatorSession "${coordinatorSession}" conflicts with an existing logical session`);
16017
+ }
16018
+ if (existing?.workspace && workspace && existing.workspace !== workspace) {
16019
+ throw new Error(`coordinatorSession "${coordinatorSession}" is already bound to workspace "${existing.workspace}"; use a new coordinator session for workspace "${workspace}"`);
16020
+ }
16021
+ const now = this.deps.now().toISOString();
16022
+ const effectiveDefaultTargetAgent = defaultTargetAgent || existing?.defaultTargetAgent;
16023
+ const record3 = {
16024
+ coordinatorSession,
16025
+ ...workspace ? { workspace } : existing?.workspace ? { workspace: existing.workspace } : {},
16026
+ createdAt: existing?.createdAt ?? now,
16027
+ updatedAt: now,
16028
+ ...effectiveDefaultTargetAgent ? { defaultTargetAgent: effectiveDefaultTargetAgent } : {}
16029
+ };
16030
+ externalCoordinators[coordinatorSession] = record3;
16031
+ await this.deps.saveState(state);
16032
+ return { ...record3 };
16033
+ });
16034
+ }
15343
16035
  async createGroup(input) {
15344
16036
  if (input.coordinatorSession.trim().length === 0) {
15345
16037
  throw new Error("coordinatorSession must be a non-empty string");
@@ -15460,70 +16152,89 @@ class OrchestrationService {
15460
16152
  const normalizedGroupId = this.normalizeGroupId(input.groupId);
15461
16153
  const taskId = this.deps.createId();
15462
16154
  const workerSession = await this.resolveWorkerSession(input);
15463
- const ensuredWorkerSession = await this.deps.ensureWorkerSession({
15464
- workerSession,
15465
- sourceHandle: input.sourceHandle,
15466
- sourceKind: input.sourceKind,
15467
- coordinatorSession: input.coordinatorSession,
15468
- workspace: input.workspace,
15469
- targetAgent: input.targetAgent,
15470
- role
15471
- });
15472
- const prepared = await this.mutate(async () => {
15473
- const state = await this.deps.loadState();
15474
- const now = this.deps.now().toISOString();
15475
- if (normalizedGroupId) {
15476
- this.assertGroupOwnership(this.ensureGroups(state)[normalizedGroupId], normalizedGroupId, input.coordinatorSession);
15477
- }
15478
- const task = {
15479
- taskId,
16155
+ const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession);
16156
+ let ensuredWorkerSession = workerSession;
16157
+ let prepared;
16158
+ try {
16159
+ ensuredWorkerSession = await this.ensureReservedWorkerSession({
16160
+ workerSession,
15480
16161
  sourceHandle: input.sourceHandle,
15481
16162
  sourceKind: input.sourceKind,
15482
16163
  coordinatorSession: input.coordinatorSession,
15483
- workerSession: ensuredWorkerSession,
15484
- workspace: input.workspace,
15485
- targetAgent: input.targetAgent,
15486
- ...role ? { role } : {},
15487
- ...normalizedGroupId ? { groupId: normalizedGroupId } : {},
15488
- task: input.task,
15489
- status: "running",
15490
- summary: "",
15491
- resultText: "",
15492
- createdAt: now,
15493
- updatedAt: now,
15494
- ...input.chatKey ? { chatKey: input.chatKey } : {},
15495
- ...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
15496
- ...input.accountId ? { accountId: input.accountId } : {}
15497
- };
15498
- state.orchestration.tasks[taskId] = task;
15499
- if (normalizedGroupId) {
15500
- const group = this.ensureGroups(state)[normalizedGroupId];
15501
- group.updatedAt = now;
15502
- group.coordinatorInjectedAt = undefined;
15503
- group.injectionPending = undefined;
15504
- group.injectionAppliedAt = undefined;
15505
- group.lastInjectionError = undefined;
15506
- }
15507
- const previousBinding = state.orchestration.workerBindings[ensuredWorkerSession];
15508
- state.orchestration.workerBindings[ensuredWorkerSession] = {
15509
- sourceHandle: ensuredWorkerSession,
15510
- coordinatorSession: input.coordinatorSession,
15511
16164
  workspace: input.workspace,
16165
+ ...input.cwd ? { cwd: input.cwd } : {},
15512
16166
  targetAgent: input.targetAgent,
15513
16167
  role
15514
- };
15515
- await this.deps.saveState(state);
15516
- return {
15517
- task: { ...task },
15518
- previousBinding
15519
- };
15520
- });
16168
+ });
16169
+ prepared = await this.mutate(async () => {
16170
+ const state = await this.deps.loadState();
16171
+ const now = this.deps.now().toISOString();
16172
+ if (normalizedGroupId) {
16173
+ this.assertGroupOwnership(this.ensureGroups(state)[normalizedGroupId], normalizedGroupId, input.coordinatorSession);
16174
+ }
16175
+ const task = {
16176
+ taskId,
16177
+ sourceHandle: input.sourceHandle,
16178
+ sourceKind: input.sourceKind,
16179
+ coordinatorSession: input.coordinatorSession,
16180
+ workerSession: ensuredWorkerSession,
16181
+ workspace: input.workspace,
16182
+ ...input.cwd ? { cwd: input.cwd } : {},
16183
+ targetAgent: input.targetAgent,
16184
+ ...role ? { role } : {},
16185
+ ...normalizedGroupId ? { groupId: normalizedGroupId } : {},
16186
+ task: input.task,
16187
+ status: "running",
16188
+ summary: "",
16189
+ resultText: "",
16190
+ createdAt: now,
16191
+ updatedAt: now,
16192
+ ...input.chatKey ? { chatKey: input.chatKey } : {},
16193
+ ...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
16194
+ ...input.accountId ? { accountId: input.accountId } : {}
16195
+ };
16196
+ let previousGroup;
16197
+ if (normalizedGroupId) {
16198
+ const group = this.ensureGroups(state)[normalizedGroupId];
16199
+ previousGroup = { ...group };
16200
+ group.updatedAt = now;
16201
+ group.coordinatorInjectedAt = undefined;
16202
+ group.injectionPending = undefined;
16203
+ group.injectionAppliedAt = undefined;
16204
+ group.lastInjectionError = undefined;
16205
+ }
16206
+ const previousBinding = state.orchestration.workerBindings[ensuredWorkerSession];
16207
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, ensuredWorkerSession);
16208
+ this.assertWorkerSessionAvailable(state, ensuredWorkerSession, undefined, { allowCurrentReservation: true });
16209
+ state.orchestration.tasks[taskId] = task;
16210
+ state.orchestration.workerBindings[ensuredWorkerSession] = {
16211
+ sourceHandle: ensuredWorkerSession,
16212
+ coordinatorSession: input.coordinatorSession,
16213
+ workspace: input.workspace,
16214
+ ...input.cwd ? { cwd: input.cwd } : {},
16215
+ targetAgent: input.targetAgent,
16216
+ role
16217
+ };
16218
+ await this.deps.saveState(state);
16219
+ return {
16220
+ task: { ...task },
16221
+ previousBinding,
16222
+ previousGroup,
16223
+ normalizedGroupId
16224
+ };
16225
+ });
16226
+ } catch (error2) {
16227
+ await releaseWorkerReservation();
16228
+ throw error2;
16229
+ }
16230
+ await releaseWorkerReservation();
15521
16231
  try {
15522
16232
  await this.deps.dispatchWorkerTask({
15523
16233
  taskId,
15524
16234
  workerSession: ensuredWorkerSession,
15525
16235
  coordinatorSession: input.coordinatorSession,
15526
16236
  workspace: input.workspace,
16237
+ ...input.cwd ? { cwd: input.cwd } : {},
15527
16238
  targetAgent: input.targetAgent,
15528
16239
  ...role ? { role } : {},
15529
16240
  task: input.task
@@ -15537,6 +16248,9 @@ class OrchestrationService {
15537
16248
  } else {
15538
16249
  delete state.orchestration.workerBindings[ensuredWorkerSession];
15539
16250
  }
16251
+ if (prepared.normalizedGroupId && prepared.previousGroup) {
16252
+ this.ensureGroups(state)[prepared.normalizedGroupId] = prepared.previousGroup;
16253
+ }
15540
16254
  await this.deps.saveState(state);
15541
16255
  });
15542
16256
  throw error2;
@@ -15553,115 +16267,288 @@ class OrchestrationService {
15553
16267
  const preflight = await this.mutate(async () => {
15554
16268
  const state = await this.deps.loadState();
15555
16269
  const sourceContext = this.resolveRpcSourceContext(state, input.sourceHandle);
16270
+ const targetLocation = this.resolveRpcTargetLocation(sourceContext, input.cwd);
15556
16271
  const role = this.normalizeRole(input.role);
15557
16272
  this.assertRpcRequestAllowed(state, sourceContext.sourceKind, sourceContext.coordinatorSession, input.targetAgent, role);
15558
16273
  const normalizedGroupId = this.normalizeGroupId(input.groupId);
15559
16274
  if (normalizedGroupId) {
15560
16275
  this.assertGroupOwnership(this.ensureGroups(state)[normalizedGroupId], normalizedGroupId, sourceContext.coordinatorSession);
15561
16276
  }
15562
- return { sourceContext, role, normalizedGroupId };
16277
+ return { sourceContext, targetLocation, role, normalizedGroupId };
15563
16278
  });
15564
16279
  const autoRun = preflight.sourceContext.sourceKind === "coordinator";
15565
16280
  const workerSessionName = await this.resolveWorkerSession({
15566
16281
  sourceHandle: input.sourceHandle,
15567
16282
  sourceKind: preflight.sourceContext.sourceKind,
15568
16283
  coordinatorSession: preflight.sourceContext.coordinatorSession,
15569
- workspace: preflight.sourceContext.workspace,
16284
+ workspace: preflight.targetLocation.workspace,
16285
+ ...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
15570
16286
  targetAgent: input.targetAgent,
15571
16287
  task: input.task,
15572
16288
  ...preflight.role ? { role: preflight.role } : {}
15573
16289
  });
15574
- const ensuredWorkerSession = autoRun ? await this.deps.ensureWorkerSession({
15575
- workerSession: workerSessionName,
15576
- sourceHandle: input.sourceHandle,
15577
- sourceKind: preflight.sourceContext.sourceKind,
15578
- coordinatorSession: preflight.sourceContext.coordinatorSession,
15579
- workspace: preflight.sourceContext.workspace,
15580
- targetAgent: input.targetAgent,
15581
- role: preflight.role
15582
- }) : workerSessionName;
15583
- const prepared = await this.mutate(async () => {
15584
- const state = await this.deps.loadState();
15585
- const now = this.deps.now().toISOString();
15586
- const taskId = this.deps.createId();
15587
- const status = autoRun ? "running" : "needs_confirmation";
15588
- const task = {
15589
- taskId,
15590
- sourceHandle: input.sourceHandle,
15591
- sourceKind: preflight.sourceContext.sourceKind,
15592
- coordinatorSession: preflight.sourceContext.coordinatorSession,
15593
- workerSession: ensuredWorkerSession,
15594
- workspace: preflight.sourceContext.workspace,
15595
- targetAgent: input.targetAgent,
15596
- ...preflight.role ? { role: preflight.role } : {},
15597
- ...preflight.normalizedGroupId ? { groupId: preflight.normalizedGroupId } : {},
15598
- task: input.task,
15599
- status,
15600
- summary: "",
15601
- resultText: "",
15602
- createdAt: now,
15603
- updatedAt: now
15604
- };
15605
- state.orchestration.tasks[taskId] = task;
15606
- let previousGroup;
15607
- if (preflight.normalizedGroupId) {
15608
- const group = this.ensureGroups(state)[preflight.normalizedGroupId];
15609
- previousGroup = { ...group };
15610
- group.updatedAt = now;
15611
- group.coordinatorInjectedAt = undefined;
15612
- group.injectionPending = undefined;
15613
- group.injectionAppliedAt = undefined;
15614
- group.lastInjectionError = undefined;
15615
- }
15616
- let previousBinding;
15617
- if (autoRun) {
15618
- previousBinding = state.orchestration.workerBindings[ensuredWorkerSession];
15619
- state.orchestration.workerBindings[ensuredWorkerSession] = {
15620
- sourceHandle: ensuredWorkerSession,
16290
+ const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSessionName);
16291
+ let prepared;
16292
+ try {
16293
+ prepared = await this.mutate(async () => {
16294
+ const state = await this.deps.loadState();
16295
+ this.assertRpcRequestAllowed(state, preflight.sourceContext.sourceKind, preflight.sourceContext.coordinatorSession, input.targetAgent, preflight.role);
16296
+ const now = this.deps.now().toISOString();
16297
+ const taskId = this.deps.createId();
16298
+ const status = autoRun ? "running" : "needs_confirmation";
16299
+ const task = {
16300
+ taskId,
16301
+ sourceHandle: input.sourceHandle,
16302
+ sourceKind: preflight.sourceContext.sourceKind,
15621
16303
  coordinatorSession: preflight.sourceContext.coordinatorSession,
15622
- workspace: preflight.sourceContext.workspace,
16304
+ workerSession: workerSessionName,
16305
+ workspace: preflight.targetLocation.workspace,
16306
+ ...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
15623
16307
  targetAgent: input.targetAgent,
15624
- role: preflight.role
16308
+ ...preflight.role ? { role: preflight.role } : {},
16309
+ ...preflight.normalizedGroupId ? { groupId: preflight.normalizedGroupId } : {},
16310
+ task: input.task,
16311
+ status,
16312
+ summary: "",
16313
+ resultText: "",
16314
+ createdAt: now,
16315
+ updatedAt: now
15625
16316
  };
15626
- }
15627
- await this.deps.saveState(state);
15628
- return { task: { ...task }, status, previousBinding, previousGroup, normalizedGroupId: preflight.normalizedGroupId };
15629
- });
16317
+ if (preflight.normalizedGroupId) {
16318
+ const group = this.ensureGroups(state)[preflight.normalizedGroupId];
16319
+ group.updatedAt = now;
16320
+ group.coordinatorInjectedAt = undefined;
16321
+ group.injectionPending = undefined;
16322
+ group.injectionAppliedAt = undefined;
16323
+ group.lastInjectionError = undefined;
16324
+ }
16325
+ let previousBinding;
16326
+ if (autoRun) {
16327
+ previousBinding = state.orchestration.workerBindings[workerSessionName];
16328
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
16329
+ this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
16330
+ state.orchestration.tasks[taskId] = task;
16331
+ state.orchestration.workerBindings[workerSessionName] = {
16332
+ sourceHandle: workerSessionName,
16333
+ coordinatorSession: preflight.sourceContext.coordinatorSession,
16334
+ workspace: preflight.targetLocation.workspace,
16335
+ ...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
16336
+ targetAgent: input.targetAgent,
16337
+ role: preflight.role
16338
+ };
16339
+ } else {
16340
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
16341
+ this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
16342
+ state.orchestration.tasks[taskId] = task;
16343
+ }
16344
+ await this.deps.saveState(state);
16345
+ return { task: { ...task }, status, previousBinding, normalizedGroupId: preflight.normalizedGroupId };
16346
+ });
16347
+ } catch (error2) {
16348
+ await releaseWorkerReservation();
16349
+ throw error2;
16350
+ }
16351
+ await releaseWorkerReservation();
15630
16352
  if (autoRun) {
15631
- try {
15632
- await this.deps.dispatchWorkerTask({
15633
- taskId: prepared.task.taskId,
15634
- workerSession: ensuredWorkerSession,
15635
- coordinatorSession: prepared.task.coordinatorSession,
15636
- workspace: prepared.task.workspace,
15637
- targetAgent: prepared.task.targetAgent,
15638
- ...prepared.task.role ? { role: prepared.task.role } : {},
15639
- task: prepared.task.task
16353
+ this.runAutoRunRpcWorkerTask({
16354
+ task: prepared.task,
16355
+ previousBinding: prepared.previousBinding
16356
+ });
16357
+ }
16358
+ this.logEvent("orchestration.task.created", "delegated task created", this.taskContext(prepared.task));
16359
+ return {
16360
+ taskId: prepared.task.taskId,
16361
+ status: prepared.status,
16362
+ ...autoRun ? { workerSession: workerSessionName } : {}
16363
+ };
16364
+ }
16365
+ async runAutoRunRpcWorkerTask(input) {
16366
+ const { task } = input;
16367
+ try {
16368
+ const ensuredWorkerSession = await this.ensureReservedWorkerSession({
16369
+ workerSession: task.workerSession,
16370
+ sourceHandle: task.sourceHandle,
16371
+ sourceKind: task.sourceKind,
16372
+ coordinatorSession: task.coordinatorSession,
16373
+ workspace: task.workspace,
16374
+ ...task.cwd ? { cwd: task.cwd } : {},
16375
+ targetAgent: task.targetAgent,
16376
+ ...task.role ? { role: task.role } : {}
16377
+ });
16378
+ const startupAction = await this.mutate(async () => {
16379
+ const state = await this.deps.loadState();
16380
+ const current = state.orchestration.tasks[task.taskId];
16381
+ if (current?.workerSession === ensuredWorkerSession && current.status === "running" && current.cancelRequestedAt !== undefined) {
16382
+ return "completeCancellation";
16383
+ }
16384
+ return current !== undefined && current.workerSession === ensuredWorkerSession && current.status === "running" ? "dispatch" : "skip";
16385
+ });
16386
+ if (startupAction === "completeCancellation") {
16387
+ const completed = await this.completeAutoRunStartupCancellation({
16388
+ task,
16389
+ previousBinding: input.previousBinding
15640
16390
  });
15641
- } catch (error2) {
15642
- await this.mutate(async () => {
15643
- const state = await this.deps.loadState();
15644
- delete state.orchestration.tasks[prepared.task.taskId];
15645
- if (prepared.previousBinding) {
15646
- state.orchestration.workerBindings[ensuredWorkerSession] = prepared.previousBinding;
15647
- } else {
15648
- delete state.orchestration.workerBindings[ensuredWorkerSession];
16391
+ if (completed) {
16392
+ this.logEvent("orchestration.task.cancel_completed", "task cancellation completed", {
16393
+ ...this.taskContext(task),
16394
+ status: "cancelled"
16395
+ });
16396
+ }
16397
+ return;
16398
+ }
16399
+ if (startupAction !== "dispatch") {
16400
+ await this.cleanupAutoRunStartupBinding({
16401
+ task,
16402
+ previousBinding: input.previousBinding
16403
+ });
16404
+ return;
16405
+ }
16406
+ const preDispatchAction = await this.mutate(async () => {
16407
+ const state = await this.deps.loadState();
16408
+ const current = state.orchestration.tasks[task.taskId];
16409
+ if (current?.workerSession === ensuredWorkerSession && current.status === "running" && current.cancelRequestedAt !== undefined) {
16410
+ return "completeCancellation";
16411
+ }
16412
+ return current !== undefined && current.workerSession === ensuredWorkerSession && current.status === "running" ? "dispatch" : "skip";
16413
+ });
16414
+ if (preDispatchAction === "completeCancellation") {
16415
+ const completed = await this.completeAutoRunStartupCancellation({
16416
+ task,
16417
+ previousBinding: input.previousBinding
16418
+ });
16419
+ if (completed) {
16420
+ this.logEvent("orchestration.task.cancel_completed", "task cancellation completed", {
16421
+ ...this.taskContext(task),
16422
+ status: "cancelled"
16423
+ });
16424
+ }
16425
+ return;
16426
+ }
16427
+ if (preDispatchAction !== "dispatch") {
16428
+ await this.cleanupAutoRunStartupBinding({
16429
+ task,
16430
+ previousBinding: input.previousBinding
16431
+ });
16432
+ return;
16433
+ }
16434
+ await this.deps.dispatchWorkerTask({
16435
+ taskId: task.taskId,
16436
+ workerSession: ensuredWorkerSession,
16437
+ coordinatorSession: task.coordinatorSession,
16438
+ workspace: task.workspace,
16439
+ ...task.cwd ? { cwd: task.cwd } : {},
16440
+ targetAgent: task.targetAgent,
16441
+ ...task.role ? { role: task.role } : {},
16442
+ task: task.task
16443
+ });
16444
+ } catch (error2) {
16445
+ const message = error2 instanceof Error ? error2.message : String(error2);
16446
+ const completedCancellation = await this.completeAutoRunStartupCancellation({
16447
+ task,
16448
+ previousBinding: input.previousBinding
16449
+ });
16450
+ if (completedCancellation) {
16451
+ this.logEvent("orchestration.task.cancel_completed", "task cancellation completed", {
16452
+ ...this.taskContext(task),
16453
+ status: "cancelled"
16454
+ });
16455
+ return;
16456
+ }
16457
+ const taskMarkedFailed = await this.mutate(async () => {
16458
+ const state = await this.deps.loadState();
16459
+ const current = state.orchestration.tasks[task.taskId];
16460
+ const workerSession = task.workerSession;
16461
+ const taskStillOwnsWorkerSession = current?.workerSession === workerSession;
16462
+ const currentBinding = state.orchestration.workerBindings[workerSession];
16463
+ const bindingStillBelongsToThisStartup = currentBinding?.sourceHandle === workerSession && currentBinding.coordinatorSession === task.coordinatorSession && currentBinding.workspace === task.workspace && currentBinding.cwd === task.cwd && currentBinding.targetAgent === task.targetAgent && currentBinding.role === task.role;
16464
+ const otherActiveOwner = Object.values(state.orchestration.tasks).some((candidate) => candidate.taskId !== task.taskId && candidate.workerSession === workerSession && (!this.isTerminalStatus(candidate.status) || candidate.reviewPending !== undefined));
16465
+ const restoreOrDeleteBinding = () => {
16466
+ if (!bindingStillBelongsToThisStartup || otherActiveOwner) {
16467
+ return;
15649
16468
  }
15650
- if (prepared.normalizedGroupId && prepared.previousGroup) {
15651
- const groups = state.orchestration.groups ?? {};
15652
- groups[prepared.normalizedGroupId] = prepared.previousGroup;
16469
+ if (input.previousBinding) {
16470
+ state.orchestration.workerBindings[workerSession] = input.previousBinding;
16471
+ } else {
16472
+ delete state.orchestration.workerBindings[workerSession];
15653
16473
  }
16474
+ };
16475
+ if (current && taskStillOwnsWorkerSession && current.status === "cancelled") {
16476
+ restoreOrDeleteBinding();
15654
16477
  await this.deps.saveState(state);
16478
+ return false;
16479
+ }
16480
+ if (current && taskStillOwnsWorkerSession && current.cancelRequestedAt === undefined && !this.isTerminalStatus(current.status)) {
16481
+ const now = this.deps.now().toISOString();
16482
+ current.status = "failed";
16483
+ current.summary = message;
16484
+ current.resultText = "";
16485
+ current.updatedAt = now;
16486
+ restoreOrDeleteBinding();
16487
+ await this.deps.saveState(state);
16488
+ return true;
16489
+ }
16490
+ await this.deps.saveState(state);
16491
+ return false;
16492
+ });
16493
+ if (taskMarkedFailed) {
16494
+ this.logEvent("orchestration.task.failed", "task failed", {
16495
+ ...this.taskContext(task),
16496
+ error: message
15655
16497
  });
15656
- throw error2;
15657
16498
  }
15658
16499
  }
15659
- this.logEvent("orchestration.task.created", "delegated task created", this.taskContext(prepared.task));
15660
- return {
15661
- taskId: prepared.task.taskId,
15662
- status: prepared.status,
15663
- ...autoRun ? { workerSession: ensuredWorkerSession } : {}
15664
- };
16500
+ }
16501
+ async completeAutoRunStartupCancellation(input) {
16502
+ const { task } = input;
16503
+ return await this.mutate(async () => {
16504
+ const state = await this.deps.loadState();
16505
+ const workerSession = task.workerSession;
16506
+ const current = state.orchestration.tasks[task.taskId];
16507
+ if (!current || current.workerSession !== workerSession || current.status !== "running" || current.cancelRequestedAt === undefined) {
16508
+ return false;
16509
+ }
16510
+ const now = this.deps.now().toISOString();
16511
+ current.status = "cancelled";
16512
+ current.cancelCompletedAt = now;
16513
+ current.lastCancelError = undefined;
16514
+ current.updatedAt = now;
16515
+ this.bumpGroupUpdated(state, current.groupId, now);
16516
+ const currentBinding = state.orchestration.workerBindings[workerSession];
16517
+ const bindingStillBelongsToThisStartup = currentBinding?.sourceHandle === workerSession && currentBinding.coordinatorSession === task.coordinatorSession && currentBinding.workspace === task.workspace && currentBinding.cwd === task.cwd && currentBinding.targetAgent === task.targetAgent && currentBinding.role === task.role;
16518
+ const otherActiveOwner = Object.values(state.orchestration.tasks).some((candidate) => candidate.taskId !== task.taskId && candidate.workerSession === workerSession && (!this.isTerminalStatus(candidate.status) || candidate.reviewPending !== undefined));
16519
+ if (bindingStillBelongsToThisStartup && !otherActiveOwner) {
16520
+ if (input.previousBinding) {
16521
+ state.orchestration.workerBindings[workerSession] = input.previousBinding;
16522
+ } else {
16523
+ delete state.orchestration.workerBindings[workerSession];
16524
+ }
16525
+ }
16526
+ await this.deps.saveState(state);
16527
+ return true;
16528
+ });
16529
+ }
16530
+ async cleanupAutoRunStartupBinding(input) {
16531
+ const { task } = input;
16532
+ return await this.mutate(async () => {
16533
+ const state = await this.deps.loadState();
16534
+ const workerSession = task.workerSession;
16535
+ const currentBinding = state.orchestration.workerBindings[workerSession];
16536
+ const bindingStillBelongsToThisStartup = currentBinding?.sourceHandle === workerSession && currentBinding.coordinatorSession === task.coordinatorSession && currentBinding.workspace === task.workspace && currentBinding.cwd === task.cwd && currentBinding.targetAgent === task.targetAgent && currentBinding.role === task.role;
16537
+ if (!bindingStillBelongsToThisStartup) {
16538
+ return false;
16539
+ }
16540
+ const otherActiveOwner = Object.values(state.orchestration.tasks).some((candidate) => candidate.taskId !== task.taskId && candidate.workerSession === workerSession && (!this.isTerminalStatus(candidate.status) || candidate.reviewPending !== undefined));
16541
+ if (otherActiveOwner) {
16542
+ return false;
16543
+ }
16544
+ if (input.previousBinding) {
16545
+ state.orchestration.workerBindings[workerSession] = input.previousBinding;
16546
+ } else {
16547
+ delete state.orchestration.workerBindings[workerSession];
16548
+ }
16549
+ await this.deps.saveState(state);
16550
+ return true;
16551
+ });
15665
16552
  }
15666
16553
  async recordWorkerReply(input) {
15667
16554
  const task = await this.mutate(async () => {
@@ -15689,9 +16576,15 @@ class OrchestrationService {
15689
16576
  task2.summary = input.summary ?? "";
15690
16577
  task2.resultText = stripProgressLines(input.resultText ?? "");
15691
16578
  if (task2.status === "completed" || task2.status === "failed") {
15692
- task2.injectionPending = true;
15693
- task2.injectionAppliedAt = undefined;
15694
- task2.lastInjectionError = undefined;
16579
+ if (!this.isExternalCoordinatorSession(state, task2.coordinatorSession)) {
16580
+ task2.injectionPending = true;
16581
+ task2.injectionAppliedAt = undefined;
16582
+ task2.lastInjectionError = undefined;
16583
+ } else {
16584
+ task2.injectionPending = undefined;
16585
+ task2.injectionAppliedAt = undefined;
16586
+ task2.lastInjectionError = undefined;
16587
+ }
15695
16588
  if (!isContestedResult && task2.chatKey && task2.replyContextToken) {
15696
16589
  task2.noticePending = true;
15697
16590
  task2.noticeSentAt = undefined;
@@ -15794,6 +16687,30 @@ class OrchestrationService {
15794
16687
  const task = state.orchestration.tasks[taskId];
15795
16688
  return task ? { ...task } : null;
15796
16689
  }
16690
+ async waitTask(input) {
16691
+ const timeoutMs = clampWaitTimeout(input.timeoutMs);
16692
+ const pollIntervalMs = clampPollInterval(input.pollIntervalMs);
16693
+ const deadline = Date.now() + timeoutMs;
16694
+ while (true) {
16695
+ const state = await this.deps.loadState();
16696
+ const task = state.orchestration.tasks[input.taskId];
16697
+ if (!task || task.coordinatorSession !== input.coordinatorSession) {
16698
+ return { status: "not_found", task: null };
16699
+ }
16700
+ const snapshot = { ...task };
16701
+ if (isTerminalTaskStatus2(task.status) && task.reviewPending === undefined) {
16702
+ return { status: "terminal", task: snapshot };
16703
+ }
16704
+ if (isAttentionRequiredTask(task)) {
16705
+ return { status: "attention_required", task: snapshot };
16706
+ }
16707
+ const remainingMs = deadline - Date.now();
16708
+ if (remainingMs <= 0) {
16709
+ return { status: "timeout", task: snapshot };
16710
+ }
16711
+ await sleep2(Math.min(pollIntervalMs, remainingMs));
16712
+ }
16713
+ }
15797
16714
  async recordCoordinatorRouteContext(input) {
15798
16715
  if (input.coordinatorSession.trim().length === 0) {
15799
16716
  throw new Error("coordinatorSession must be a non-empty string");
@@ -15878,13 +16795,16 @@ class OrchestrationService {
15878
16795
  return {
15879
16796
  taskId: task.taskId,
15880
16797
  questionId,
15881
- coordinatorSession: task.coordinatorSession
16798
+ coordinatorSession: task.coordinatorSession,
16799
+ externalCoordinator: this.isExternalCoordinatorSession(state, task.coordinatorSession)
15882
16800
  };
15883
16801
  });
15884
16802
  try {
15885
- await this.deps.wakeCoordinatorSession?.({
15886
- coordinatorSession: prepared.coordinatorSession
15887
- });
16803
+ if (!prepared.externalCoordinator) {
16804
+ await this.deps.wakeCoordinatorSession?.({
16805
+ coordinatorSession: prepared.coordinatorSession
16806
+ });
16807
+ }
15888
16808
  } catch (error2) {
15889
16809
  await this.recordOpenQuestionWakeError(prepared.taskId, prepared.questionId, error2 instanceof Error ? error2.message : String(error2));
15890
16810
  }
@@ -15941,6 +16861,7 @@ class OrchestrationService {
15941
16861
  workerSession: prepared.task.workerSession,
15942
16862
  coordinatorSession: prepared.task.coordinatorSession,
15943
16863
  workspace: prepared.task.workspace,
16864
+ ...prepared.task.cwd ? { cwd: prepared.task.cwd } : {},
15944
16865
  targetAgent: prepared.task.targetAgent,
15945
16866
  answer
15946
16867
  });
@@ -16026,6 +16947,9 @@ class OrchestrationService {
16026
16947
  }
16027
16948
  const prepared = await this.mutate(async () => {
16028
16949
  const state = await this.deps.loadState();
16950
+ if (this.isExternalCoordinatorSession(state, input.coordinatorSession)) {
16951
+ throw new Error("human input routing is not configured for external coordinator");
16952
+ }
16029
16953
  const coordinatorState = this.ensureCoordinatorQuestionState(state, input.coordinatorSession);
16030
16954
  if (input.expectedActivePackageId !== undefined && coordinatorState.activePackageId !== input.expectedActivePackageId) {
16031
16955
  throw new Error(`coordinator "${input.coordinatorSession}" active package is "${coordinatorState.activePackageId ?? ""}", not "${input.expectedActivePackageId}"`);
@@ -16137,6 +17061,9 @@ class OrchestrationService {
16137
17061
  }
16138
17062
  const prepared = await this.mutate(async () => {
16139
17063
  const state = await this.deps.loadState();
17064
+ if (this.isExternalCoordinatorSession(state, input.coordinatorSession)) {
17065
+ throw new Error("human input routing is not configured for external coordinator");
17066
+ }
16140
17067
  const coordinatorState = this.ensureCoordinatorQuestionState(state, input.coordinatorSession);
16141
17068
  if (coordinatorState.activePackageId !== input.packageId) {
16142
17069
  throw new Error(`package "${input.packageId}" is not the active package for coordinator "${input.coordinatorSession}"`);
@@ -16205,6 +17132,9 @@ class OrchestrationService {
16205
17132
  async retryHumanQuestionPackageDelivery(input) {
16206
17133
  const prepared = await this.mutate(async () => {
16207
17134
  const state = await this.deps.loadState();
17135
+ if (this.isExternalCoordinatorSession(state, input.coordinatorSession)) {
17136
+ throw new Error("human input routing is not configured for external coordinator");
17137
+ }
16208
17138
  const coordinatorState = this.ensureCoordinatorQuestionState(state, input.coordinatorSession);
16209
17139
  if (coordinatorState.activePackageId !== input.packageId) {
16210
17140
  throw new Error(`package "${input.packageId}" is not the active package for coordinator "${input.coordinatorSession}"`);
@@ -16253,6 +17183,9 @@ class OrchestrationService {
16253
17183
  async claimActiveHumanReply(input) {
16254
17184
  return await this.mutate(async () => {
16255
17185
  const state = await this.deps.loadState();
17186
+ if (this.isExternalCoordinatorSession(state, input.coordinatorSession)) {
17187
+ return null;
17188
+ }
16256
17189
  const coordinatorState = this.ensureCoordinatorQuestionState(state, input.coordinatorSession);
16257
17190
  if (!coordinatorState.activePackageId || coordinatorState.activePackageId !== input.packageId) {
16258
17191
  return null;
@@ -16291,6 +17224,9 @@ class OrchestrationService {
16291
17224
  }
16292
17225
  async getActiveHumanQuestionPackage(coordinatorSession) {
16293
17226
  const state = await this.deps.loadState();
17227
+ if (this.isExternalCoordinatorSession(state, coordinatorSession)) {
17228
+ return null;
17229
+ }
16294
17230
  const coordinatorState = state.orchestration.coordinatorQuestionState[coordinatorSession];
16295
17231
  const activePackageId = coordinatorState?.activePackageId;
16296
17232
  if (!activePackageId) {
@@ -16368,10 +17304,11 @@ class OrchestrationService {
16368
17304
  await this.deps.saveState(state);
16369
17305
  return {
16370
17306
  task: { ...task },
16371
- replacementQuestionId
17307
+ replacementQuestionId,
17308
+ externalCoordinator: this.isExternalCoordinatorSession(state, task.coordinatorSession)
16372
17309
  };
16373
17310
  });
16374
- if (prepared.replacementQuestionId) {
17311
+ if (prepared.replacementQuestionId && !prepared.externalCoordinator) {
16375
17312
  try {
16376
17313
  await this.deps.wakeCoordinatorSession?.({
16377
17314
  coordinatorSession: prepared.task.coordinatorSession
@@ -16478,20 +17415,32 @@ class OrchestrationService {
16478
17415
  }
16479
17416
  async listPendingCoordinatorResults(coordinatorSession) {
16480
17417
  const state = await this.deps.loadState();
17418
+ if (this.isExternalCoordinatorSession(state, coordinatorSession)) {
17419
+ return [];
17420
+ }
16481
17421
  return Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && this.canInjectTaskIntoCoordinator(state, task) && (task.injectionPending === true || task.coordinatorInjectedAt === undefined)).sort((left, right) => left.updatedAt.localeCompare(right.updatedAt)).map((task) => ({ ...task }));
16482
17422
  }
16483
17423
  async listPendingCoordinatorBlockers(coordinatorSession) {
16484
17424
  const state = await this.deps.loadState();
17425
+ if (this.isExternalCoordinatorSession(state, coordinatorSession)) {
17426
+ return [];
17427
+ }
16485
17428
  const coordinatorState = state.orchestration.coordinatorQuestionState[coordinatorSession];
16486
17429
  const hiddenQueuedQuestionKeys = coordinatorState?.activePackageId ? new Set((coordinatorState.queuedQuestions ?? []).map((entry) => `${entry.taskId}:${entry.questionId}`)) : null;
16487
17430
  return Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && task.status === "blocked" && task.openQuestion?.status === "open" && !hiddenQueuedQuestionKeys?.has(`${task.taskId}:${task.openQuestion.questionId}`)).sort((left, right) => left.updatedAt.localeCompare(right.updatedAt)).map((task) => ({ ...task }));
16488
17431
  }
16489
17432
  async listContestedCoordinatorResults(coordinatorSession) {
16490
17433
  const state = await this.deps.loadState();
17434
+ if (this.isExternalCoordinatorSession(state, coordinatorSession)) {
17435
+ return [];
17436
+ }
16491
17437
  return Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && task.reviewPending !== undefined).sort((left, right) => left.updatedAt.localeCompare(right.updatedAt)).map((task) => ({ ...task }));
16492
17438
  }
16493
17439
  async listPendingCoordinatorGroups(coordinatorSession) {
16494
17440
  const state = await this.deps.loadState();
17441
+ if (this.isExternalCoordinatorSession(state, coordinatorSession)) {
17442
+ return [];
17443
+ }
16495
17444
  const groups = this.ensureGroups(state);
16496
17445
  const tasks = Object.values(state.orchestration.tasks);
16497
17446
  return Object.values(groups).filter((group) => group.coordinatorSession === coordinatorSession).filter((group) => {
@@ -16758,19 +17707,25 @@ class OrchestrationService {
16758
17707
  task.updatedAt = now;
16759
17708
  this.bumpGroupUpdated(state, task.groupId, now);
16760
17709
  await this.deps.saveState(state);
16761
- return { task: { ...task }, replacementQuestionId };
17710
+ return {
17711
+ task: { ...task },
17712
+ replacementQuestionId,
17713
+ externalCoordinator: this.isExternalCoordinatorSession(state, task.coordinatorSession)
17714
+ };
16762
17715
  });
16763
17716
  if (prepared.replacementQuestionId) {
16764
17717
  this.logEvent("orchestration.task.correction_reopened", "task correction reopened blocker", {
16765
17718
  ...this.taskContext(prepared.task),
16766
17719
  replacement_question_id: prepared.replacementQuestionId
16767
17720
  });
16768
- try {
16769
- await this.deps.wakeCoordinatorSession?.({
16770
- coordinatorSession: prepared.task.coordinatorSession
16771
- });
16772
- } catch (error2) {
16773
- await this.recordOpenQuestionWakeError(prepared.task.taskId, prepared.replacementQuestionId, error2 instanceof Error ? error2.message : String(error2));
17721
+ if (!prepared.externalCoordinator) {
17722
+ try {
17723
+ await this.deps.wakeCoordinatorSession?.({
17724
+ coordinatorSession: prepared.task.coordinatorSession
17725
+ });
17726
+ } catch (error2) {
17727
+ await this.recordOpenQuestionWakeError(prepared.task.taskId, prepared.replacementQuestionId, error2 instanceof Error ? error2.message : String(error2));
17728
+ }
16774
17729
  }
16775
17730
  return prepared.task;
16776
17731
  }
@@ -16810,54 +17765,71 @@ class OrchestrationService {
16810
17765
  sourceKind: currentTask.sourceKind,
16811
17766
  coordinatorSession: currentTask.coordinatorSession,
16812
17767
  workspace: currentTask.workspace,
17768
+ ...currentTask.cwd ? { cwd: currentTask.cwd } : {},
16813
17769
  targetAgent: currentTask.targetAgent,
16814
17770
  task: currentTask.task,
16815
17771
  ...currentTask.role ? { role: currentTask.role } : {}
16816
17772
  });
16817
- const ensuredWorkerSession = await this.deps.ensureWorkerSession({
16818
- workerSession,
16819
- sourceHandle: currentTask.sourceHandle,
16820
- sourceKind: currentTask.sourceKind,
16821
- coordinatorSession: currentTask.coordinatorSession,
16822
- workspace: currentTask.workspace,
16823
- targetAgent: currentTask.targetAgent,
16824
- role: currentTask.role
16825
- });
16826
- const prepared = await this.mutate(async () => {
16827
- const state = await this.deps.loadState();
16828
- const task = state.orchestration.tasks[input.taskId];
16829
- if (!task) {
16830
- throw new Error(`task "${input.taskId}" does not exist`);
16831
- }
16832
- this.assertCoordinatorOwnership(task, input.coordinatorSession);
16833
- this.assertNeedsConfirmation(task);
16834
- const previousStatus = task.status;
16835
- const previousUpdatedAt = task.updatedAt;
16836
- const previousBinding = state.orchestration.workerBindings[ensuredWorkerSession];
16837
- task.workerSession = ensuredWorkerSession;
16838
- task.status = "running";
16839
- task.updatedAt = this.deps.now().toISOString();
16840
- state.orchestration.workerBindings[ensuredWorkerSession] = {
16841
- sourceHandle: ensuredWorkerSession,
16842
- coordinatorSession: task.coordinatorSession,
16843
- workspace: task.workspace,
16844
- targetAgent: task.targetAgent,
16845
- role: task.role
16846
- };
16847
- await this.deps.saveState(state);
16848
- return {
16849
- task: { ...task },
16850
- previousStatus,
16851
- previousUpdatedAt,
16852
- previousBinding
16853
- };
16854
- });
17773
+ const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession, input.taskId);
17774
+ let ensuredWorkerSession = workerSession;
17775
+ let prepared;
17776
+ try {
17777
+ ensuredWorkerSession = await this.ensureReservedWorkerSession({
17778
+ workerSession,
17779
+ sourceHandle: currentTask.sourceHandle,
17780
+ sourceKind: currentTask.sourceKind,
17781
+ coordinatorSession: currentTask.coordinatorSession,
17782
+ workspace: currentTask.workspace,
17783
+ ...currentTask.cwd ? { cwd: currentTask.cwd } : {},
17784
+ targetAgent: currentTask.targetAgent,
17785
+ role: currentTask.role
17786
+ });
17787
+ prepared = await this.mutate(async () => {
17788
+ const state = await this.deps.loadState();
17789
+ const task = state.orchestration.tasks[input.taskId];
17790
+ if (!task) {
17791
+ throw new Error(`task "${input.taskId}" does not exist`);
17792
+ }
17793
+ this.assertCoordinatorOwnership(task, input.coordinatorSession);
17794
+ this.assertNeedsConfirmation(task);
17795
+ const previousStatus = task.status;
17796
+ const previousUpdatedAt = task.updatedAt;
17797
+ const previousWorkerSession = task.workerSession;
17798
+ const previousBinding = state.orchestration.workerBindings[ensuredWorkerSession];
17799
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, ensuredWorkerSession);
17800
+ this.assertWorkerSessionAvailable(state, ensuredWorkerSession, input.taskId, { allowCurrentReservation: true });
17801
+ task.workerSession = ensuredWorkerSession;
17802
+ task.status = "running";
17803
+ task.updatedAt = this.deps.now().toISOString();
17804
+ state.orchestration.workerBindings[ensuredWorkerSession] = {
17805
+ sourceHandle: ensuredWorkerSession,
17806
+ coordinatorSession: task.coordinatorSession,
17807
+ workspace: task.workspace,
17808
+ ...task.cwd ? { cwd: task.cwd } : {},
17809
+ targetAgent: task.targetAgent,
17810
+ role: task.role
17811
+ };
17812
+ await this.deps.saveState(state);
17813
+ return {
17814
+ task: { ...task },
17815
+ previousStatus,
17816
+ previousUpdatedAt,
17817
+ previousWorkerSession,
17818
+ previousBinding
17819
+ };
17820
+ });
17821
+ } catch (error2) {
17822
+ await releaseWorkerReservation();
17823
+ throw error2;
17824
+ }
17825
+ await releaseWorkerReservation();
16855
17826
  try {
16856
17827
  await this.deps.dispatchWorkerTask({
16857
17828
  taskId: prepared.task.taskId,
16858
17829
  workerSession: ensuredWorkerSession,
16859
17830
  coordinatorSession: prepared.task.coordinatorSession,
16860
17831
  workspace: prepared.task.workspace,
17832
+ ...prepared.task.cwd ? { cwd: prepared.task.cwd } : {},
16861
17833
  targetAgent: prepared.task.targetAgent,
16862
17834
  ...prepared.task.role ? { role: prepared.task.role } : {},
16863
17835
  task: prepared.task.task
@@ -16869,6 +17841,11 @@ class OrchestrationService {
16869
17841
  if (task) {
16870
17842
  task.status = prepared.previousStatus;
16871
17843
  task.updatedAt = prepared.previousUpdatedAt;
17844
+ if (prepared.previousWorkerSession === undefined) {
17845
+ delete task.workerSession;
17846
+ } else {
17847
+ task.workerSession = prepared.previousWorkerSession;
17848
+ }
16872
17849
  }
16873
17850
  if (prepared.previousBinding) {
16874
17851
  state.orchestration.workerBindings[ensuredWorkerSession] = prepared.previousBinding;
@@ -16907,13 +17884,68 @@ class OrchestrationService {
16907
17884
  sourceKind: input.sourceKind,
16908
17885
  coordinatorSession: input.coordinatorSession,
16909
17886
  workspace: input.workspace,
17887
+ ...input.cwd ? { cwd: input.cwd } : {},
16910
17888
  targetAgent: input.targetAgent,
16911
17889
  role
16912
17890
  });
16913
17891
  if (reusable && reusable.trim().length > 0) {
16914
17892
  return reusable.trim();
16915
17893
  }
16916
- return [input.workspace, input.targetAgent, role, input.coordinatorSession].filter((part) => typeof part === "string" && part.trim().length > 0).map((part) => part.trim()).join(":");
17894
+ return [input.workspace, input.cwd ? this.cwdWorkerSessionPart(input.cwd) : undefined, input.targetAgent, role, input.coordinatorSession].filter((part) => typeof part === "string" && part.trim().length > 0).map((part) => part.trim()).join(":");
17895
+ }
17896
+ async reserveProposedWorkerSession(workerSession, excludingTaskId) {
17897
+ await this.mutate(async () => {
17898
+ const state = await this.deps.loadState();
17899
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSession);
17900
+ this.assertWorkerSessionAvailable(state, workerSession, excludingTaskId);
17901
+ this.pendingWorkerSessions.set(workerSession, (this.pendingWorkerSessions.get(workerSession) ?? 0) + 1);
17902
+ });
17903
+ let released = false;
17904
+ return async () => {
17905
+ if (released) {
17906
+ return;
17907
+ }
17908
+ released = true;
17909
+ await this.mutate(async () => {
17910
+ const count = this.pendingWorkerSessions.get(workerSession) ?? 0;
17911
+ if (count <= 1) {
17912
+ this.pendingWorkerSessions.delete(workerSession);
17913
+ } else {
17914
+ this.pendingWorkerSessions.set(workerSession, count - 1);
17915
+ }
17916
+ });
17917
+ };
17918
+ }
17919
+ async ensureReservedWorkerSession(request) {
17920
+ const ensuredWorkerSession = await this.deps.ensureWorkerSession(request);
17921
+ if (ensuredWorkerSession !== request.workerSession) {
17922
+ throw new Error(`ensureWorkerSession returned "${ensuredWorkerSession}", expected "${request.workerSession}"`);
17923
+ }
17924
+ return ensuredWorkerSession;
17925
+ }
17926
+ async reserveLogicalTransportSession(transportSession) {
17927
+ await this.mutate(async () => {
17928
+ const state = await this.deps.loadState();
17929
+ if (this.isExternalCoordinatorSession(state, transportSession)) {
17930
+ throw new Error(`transport session "${transportSession}" conflicts with an external coordinator`);
17931
+ }
17932
+ this.pendingLogicalTransportSessions.set(transportSession, (this.pendingLogicalTransportSessions.get(transportSession) ?? 0) + 1);
17933
+ });
17934
+ let released = false;
17935
+ return async () => {
17936
+ if (released) {
17937
+ return;
17938
+ }
17939
+ released = true;
17940
+ await this.mutate(async () => {
17941
+ const count = this.pendingLogicalTransportSessions.get(transportSession) ?? 0;
17942
+ if (count <= 1) {
17943
+ this.pendingLogicalTransportSessions.delete(transportSession);
17944
+ } else {
17945
+ this.pendingLogicalTransportSessions.set(transportSession, count - 1);
17946
+ }
17947
+ });
17948
+ };
16917
17949
  }
16918
17950
  buildGroupSummary(group, tasks) {
16919
17951
  const sortedTasks = tasks.slice().sort((left, right) => left.createdAt.localeCompare(right.createdAt)).map((task) => ({ ...task }));
@@ -16939,6 +17971,9 @@ class OrchestrationService {
16939
17971
  if (!group) {
16940
17972
  return false;
16941
17973
  }
17974
+ if (this.isExternalCoordinatorSession(state, group.coordinatorSession)) {
17975
+ return false;
17976
+ }
16942
17977
  if (groupTasks.length === 0) {
16943
17978
  return false;
16944
17979
  }
@@ -16951,6 +17986,9 @@ class OrchestrationService {
16951
17986
  return groupTasks.every((task) => task.status === "completed" || task.status === "failed");
16952
17987
  }
16953
17988
  canInjectTaskIntoCoordinator(state, task) {
17989
+ if (this.isExternalCoordinatorSession(state, task.coordinatorSession)) {
17990
+ return false;
17991
+ }
16954
17992
  if (task.status !== "completed" && task.status !== "failed" || task.reviewPending !== undefined) {
16955
17993
  return false;
16956
17994
  }
@@ -16965,7 +18003,8 @@ class OrchestrationService {
16965
18003
  return {
16966
18004
  sourceKind: "worker",
16967
18005
  coordinatorSession: binding.coordinatorSession,
16968
- workspace: binding.workspace
18006
+ workspace: binding.workspace,
18007
+ ...binding.cwd ? { cwd: binding.cwd } : {}
16969
18008
  };
16970
18009
  }
16971
18010
  const coordinatorSession = Object.values(state.sessions).find((session) => session.transport_session === sourceHandle);
@@ -16976,8 +18015,29 @@ class OrchestrationService {
16976
18015
  workspace: coordinatorSession.workspace
16977
18016
  };
16978
18017
  }
18018
+ const externalCoordinator = this.ensureExternalCoordinators(state)[sourceHandle];
18019
+ if (externalCoordinator) {
18020
+ return {
18021
+ sourceKind: "coordinator",
18022
+ coordinatorSession: externalCoordinator.coordinatorSession,
18023
+ ...externalCoordinator.workspace ? { workspace: externalCoordinator.workspace } : {}
18024
+ };
18025
+ }
16979
18026
  throw new Error(`sourceHandle "${sourceHandle}" is not a registered coordinator or worker session`);
16980
18027
  }
18028
+ resolveRpcTargetLocation(sourceContext, rawCwd) {
18029
+ const cwd = rawCwd !== undefined ? this.normalizeWorkingDirectory(rawCwd) : sourceContext.cwd;
18030
+ if (cwd) {
18031
+ return {
18032
+ workspace: sourceContext.workspace ?? this.workspaceLabelFromCwd(cwd),
18033
+ cwd
18034
+ };
18035
+ }
18036
+ if (sourceContext.workspace) {
18037
+ return { workspace: sourceContext.workspace };
18038
+ }
18039
+ throw new Error("workingDirectory is required when the external coordinator has no default workspace");
18040
+ }
16981
18041
  assertRpcRequestAllowed(state, sourceKind, coordinatorSession, targetAgent, role) {
16982
18042
  const policy = this.deps.config.orchestration;
16983
18043
  if (sourceKind === "worker" && !policy.allowWorkerChainedRequests) {
@@ -17022,6 +18082,25 @@ class OrchestrationService {
17022
18082
  throw new Error("task must be a non-empty string");
17023
18083
  }
17024
18084
  }
18085
+ normalizeWorkingDirectory(cwd) {
18086
+ const normalized = normalize(cwd.trim());
18087
+ if (normalized.length === 0 || normalized === ".") {
18088
+ throw new Error("workingDirectory must be a non-empty absolute path");
18089
+ }
18090
+ if (!isAbsolute(normalized)) {
18091
+ throw new Error("workingDirectory must be an absolute path");
18092
+ }
18093
+ return normalized;
18094
+ }
18095
+ workspaceLabelFromCwd(cwd) {
18096
+ const base = basename3(cwd).trim() || "cwd";
18097
+ return base.replace(/[^a-zA-Z0-9._-]+/g, "_") || "cwd";
18098
+ }
18099
+ cwdWorkerSessionPart(cwd) {
18100
+ const label = this.workspaceLabelFromCwd(cwd);
18101
+ const hash = createHash2("sha256").update(cwd).digest("hex").slice(0, 8);
18102
+ return `${label}-${hash}`;
18103
+ }
17025
18104
  normalizeRole(role) {
17026
18105
  const normalized = role?.trim();
17027
18106
  return normalized && normalized.length > 0 ? normalized : undefined;
@@ -17114,6 +18193,37 @@ class OrchestrationService {
17114
18193
  }
17115
18194
  return state.orchestration.coordinatorRoutes;
17116
18195
  }
18196
+ isExternalCoordinatorSession(state, coordinatorSession) {
18197
+ return this.ensureExternalCoordinators(state)[coordinatorSession] !== undefined;
18198
+ }
18199
+ assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSession) {
18200
+ if (this.isExternalCoordinatorSession(state, workerSession)) {
18201
+ throw new Error(`worker session "${workerSession}" conflicts with an external coordinator`);
18202
+ }
18203
+ }
18204
+ assertWorkerSessionAvailable(state, workerSession, excludingTaskId, options = {}) {
18205
+ const pendingCount = this.pendingWorkerSessions.get(workerSession) ?? 0;
18206
+ const allowedPendingCount = options.allowCurrentReservation ? 1 : 0;
18207
+ if (pendingCount > allowedPendingCount) {
18208
+ throw new Error(`worker session "${workerSession}" is already in use`);
18209
+ }
18210
+ if (this.hasActiveTaskWorkerSession(state, workerSession, excludingTaskId)) {
18211
+ throw new Error(`worker session "${workerSession}" is already in use`);
18212
+ }
18213
+ }
18214
+ hasActiveTaskWorkerSession(state, workerSession, excludingTaskId) {
18215
+ return Object.values(state.orchestration.tasks).some((task) => task.taskId !== excludingTaskId && task.workerSession === workerSession && (!this.isTerminalStatus(task.status) || task.reviewPending !== undefined));
18216
+ }
18217
+ async assertProposedWorkerSessionDoesNotConflictExternalCoordinator(workerSession) {
18218
+ const state = await this.deps.loadState();
18219
+ this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSession);
18220
+ }
18221
+ ensureExternalCoordinators(state) {
18222
+ if (!("externalCoordinators" in state.orchestration) || !state.orchestration.externalCoordinators) {
18223
+ state.orchestration.externalCoordinators = {};
18224
+ }
18225
+ return state.orchestration.externalCoordinators;
18226
+ }
17117
18227
  ensureGroups(state) {
17118
18228
  if (!("groups" in state.orchestration) || !state.orchestration.groups) {
17119
18229
  state.orchestration.groups = {};
@@ -17324,11 +18434,11 @@ class OrchestrationService {
17324
18434
  });
17325
18435
  }
17326
18436
  async handoffQueuedQuestions(coordinatorSession, closedPackageId) {
17327
- const queuedQuestions = await this.mutate(async () => {
18437
+ const prepared = await this.mutate(async () => {
17328
18438
  const state = await this.deps.loadState();
17329
18439
  const coordinatorState = this.ensureCoordinatorQuestionState(state, coordinatorSession);
17330
18440
  if (coordinatorState.activePackageId === closedPackageId) {
17331
- return [];
18441
+ return { externalCoordinator: this.isExternalCoordinatorSession(state, coordinatorSession), queuedQuestions: [] };
17332
18442
  }
17333
18443
  const validQueuedQuestions = coordinatorState.queuedQuestions.filter((entry) => {
17334
18444
  const task = state.orchestration.tasks[entry.taskId];
@@ -17338,9 +18448,12 @@ class OrchestrationService {
17338
18448
  coordinatorState.queuedQuestions = validQueuedQuestions;
17339
18449
  await this.deps.saveState(state);
17340
18450
  }
17341
- return validQueuedQuestions;
18451
+ return {
18452
+ externalCoordinator: this.isExternalCoordinatorSession(state, coordinatorSession),
18453
+ queuedQuestions: validQueuedQuestions
18454
+ };
17342
18455
  });
17343
- if (queuedQuestions.length === 0) {
18456
+ if (prepared.queuedQuestions.length === 0 || prepared.externalCoordinator) {
17344
18457
  return;
17345
18458
  }
17346
18459
  try {
@@ -17350,12 +18463,12 @@ class OrchestrationService {
17350
18463
  await this.mutate(async () => {
17351
18464
  const state = await this.deps.loadState();
17352
18465
  const coordinatorState = this.ensureCoordinatorQuestionState(state, coordinatorSession);
17353
- coordinatorState.queuedQuestions = coordinatorState.queuedQuestions.filter((entry) => !queuedQuestions.some((queued) => queued.taskId === entry.taskId && queued.questionId === entry.questionId));
18466
+ coordinatorState.queuedQuestions = coordinatorState.queuedQuestions.filter((entry) => !prepared.queuedQuestions.some((queued) => queued.taskId === entry.taskId && queued.questionId === entry.questionId));
17354
18467
  await this.deps.saveState(state);
17355
18468
  });
17356
18469
  } catch (error2) {
17357
18470
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
17358
- await Promise.all(queuedQuestions.map(async ({ taskId, questionId }) => {
18471
+ await Promise.all(prepared.queuedQuestions.map(async ({ taskId, questionId }) => {
17359
18472
  const state = await this.deps.loadState();
17360
18473
  const task = state.orchestration.tasks[taskId];
17361
18474
  if (!task?.openQuestion || task.openQuestion.status !== "open" || task.openQuestion.questionId !== questionId) {
@@ -17581,6 +18694,7 @@ class OrchestrationService {
17581
18694
  taskId: task.taskId,
17582
18695
  workerSession: freshTask.workerSession,
17583
18696
  workspace: freshTask.workspace,
18697
+ ...freshTask.cwd ? { cwd: freshTask.cwd } : {},
17584
18698
  targetAgent: freshTask.targetAgent
17585
18699
  });
17586
18700
  await this.completeTaskCancellation(task.taskId);
@@ -17590,11 +18704,39 @@ class OrchestrationService {
17590
18704
  })();
17591
18705
  }
17592
18706
  }
18707
+ function isTerminalTaskStatus2(status) {
18708
+ return status === "completed" || status === "failed" || status === "cancelled";
18709
+ }
18710
+ function isAttentionRequiredTask(task) {
18711
+ return task.reviewPending !== undefined || task.status === "pending" || task.status === "needs_confirmation" || task.status === "blocked" || task.status === "waiting_for_human";
18712
+ }
18713
+ function clampWaitTimeout(timeoutMs) {
18714
+ if (timeoutMs === undefined) {
18715
+ return DEFAULT_TASK_WAIT_TIMEOUT_MS;
18716
+ }
18717
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
18718
+ return 0;
18719
+ }
18720
+ return Math.min(Math.floor(timeoutMs), MAX_TASK_WAIT_TIMEOUT_MS);
18721
+ }
18722
+ function clampPollInterval(pollIntervalMs) {
18723
+ if (pollIntervalMs === undefined) {
18724
+ return DEFAULT_TASK_WAIT_POLL_INTERVAL_MS;
18725
+ }
18726
+ if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) {
18727
+ return 1;
18728
+ }
18729
+ return Math.min(Math.floor(pollIntervalMs), MAX_TASK_WAIT_POLL_INTERVAL_MS);
18730
+ }
18731
+ async function sleep2(ms) {
18732
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
18733
+ }
17593
18734
  function isRequestDelegateInput(input) {
17594
18735
  return "sourceKind" in input;
17595
18736
  }
17596
18737
  var init_orchestration_service = __esm(() => {
17597
18738
  init_quota_errors();
18739
+ init_task_wait_timeouts();
17598
18740
  });
17599
18741
 
17600
18742
  // src/orchestration/worker-prompts.ts
@@ -17627,10 +18769,12 @@ class SessionService {
17627
18769
  config;
17628
18770
  stateStore;
17629
18771
  state;
17630
- constructor(config2, stateStore, state) {
18772
+ stateMutex;
18773
+ constructor(config2, stateStore, state, options = {}) {
17631
18774
  this.config = config2;
17632
18775
  this.stateStore = stateStore;
17633
18776
  this.state = state;
18777
+ this.stateMutex = options.stateMutex ?? new AsyncMutex;
17634
18778
  }
17635
18779
  async createSession(alias, agent, workspace) {
17636
18780
  return await this.createLogicalSession(alias, agent, workspace, `${workspace}:${alias}`);
@@ -17665,61 +18809,69 @@ class SessionService {
17665
18809
  return preferred ? this.toResolvedSession(preferred) : null;
17666
18810
  }
17667
18811
  async useSession(chatKey, alias) {
17668
- const session = this.state.sessions[alias];
17669
- if (!session) {
17670
- throw new Error(`session "${alias}" does not exist`);
17671
- }
17672
- session.last_used_at = new Date().toISOString();
17673
- this.state.chat_contexts[chatKey] = { current_session: alias };
17674
- await this.persist();
18812
+ await this.mutate(async () => {
18813
+ const session = this.state.sessions[alias];
18814
+ if (!session) {
18815
+ throw new Error(`session "${alias}" does not exist`);
18816
+ }
18817
+ session.last_used_at = new Date().toISOString();
18818
+ this.state.chat_contexts[chatKey] = { current_session: alias };
18819
+ await this.persist();
18820
+ });
17675
18821
  }
17676
18822
  async setCurrentSessionMode(chatKey, modeId) {
17677
- const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
17678
- if (!currentAlias) {
17679
- throw new Error("no current session selected");
17680
- }
17681
- const session = this.state.sessions[currentAlias];
17682
- if (!session) {
17683
- throw new Error("no current session selected");
17684
- }
17685
- const normalizedModeId = modeId?.trim();
17686
- if (normalizedModeId) {
17687
- session.mode_id = normalizedModeId;
17688
- } else {
17689
- delete session.mode_id;
17690
- }
17691
- session.last_used_at = new Date().toISOString();
17692
- await this.persist();
18823
+ await this.mutate(async () => {
18824
+ const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
18825
+ if (!currentAlias) {
18826
+ throw new Error("no current session selected");
18827
+ }
18828
+ const session = this.state.sessions[currentAlias];
18829
+ if (!session) {
18830
+ throw new Error("no current session selected");
18831
+ }
18832
+ const normalizedModeId = modeId?.trim();
18833
+ if (normalizedModeId) {
18834
+ session.mode_id = normalizedModeId;
18835
+ } else {
18836
+ delete session.mode_id;
18837
+ }
18838
+ session.last_used_at = new Date().toISOString();
18839
+ await this.persist();
18840
+ });
17693
18841
  }
17694
18842
  async setCurrentSessionReplyMode(chatKey, replyMode) {
17695
- const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
17696
- if (!currentAlias) {
17697
- throw new Error("no current session selected");
17698
- }
17699
- const session = this.state.sessions[currentAlias];
17700
- if (!session) {
17701
- throw new Error("no current session selected");
17702
- }
17703
- if (replyMode) {
17704
- session.reply_mode = replyMode;
17705
- } else {
17706
- delete session.reply_mode;
17707
- }
17708
- session.last_used_at = new Date().toISOString();
17709
- await this.persist();
18843
+ await this.mutate(async () => {
18844
+ const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
18845
+ if (!currentAlias) {
18846
+ throw new Error("no current session selected");
18847
+ }
18848
+ const session = this.state.sessions[currentAlias];
18849
+ if (!session) {
18850
+ throw new Error("no current session selected");
18851
+ }
18852
+ if (replyMode) {
18853
+ session.reply_mode = replyMode;
18854
+ } else {
18855
+ delete session.reply_mode;
18856
+ }
18857
+ session.last_used_at = new Date().toISOString();
18858
+ await this.persist();
18859
+ });
17710
18860
  }
17711
18861
  async getCurrentSession(chatKey) {
17712
- const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
17713
- if (!currentAlias) {
17714
- return null;
17715
- }
17716
- const session = this.state.sessions[currentAlias];
17717
- if (!session) {
17718
- return null;
17719
- }
17720
- session.last_used_at = new Date().toISOString();
17721
- await this.persist();
17722
- return this.toResolvedSession(session);
18862
+ return await this.mutate(async () => {
18863
+ const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
18864
+ if (!currentAlias) {
18865
+ return null;
18866
+ }
18867
+ const session = this.state.sessions[currentAlias];
18868
+ if (!session) {
18869
+ return null;
18870
+ }
18871
+ session.last_used_at = new Date().toISOString();
18872
+ await this.persist();
18873
+ return this.toResolvedSession(session);
18874
+ });
17723
18875
  }
17724
18876
  async listSessions(chatKey) {
17725
18877
  const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
@@ -17744,19 +18896,21 @@ class SessionService {
17744
18896
  return count;
17745
18897
  }
17746
18898
  async removeSession(alias) {
17747
- const session = this.state.sessions[alias];
17748
- if (!session) {
17749
- throw new Error(`session "${alias}" does not exist`);
17750
- }
17751
- const wasActive = Object.values(this.state.chat_contexts).some((ctx) => ctx.current_session === alias);
17752
- delete this.state.sessions[alias];
17753
- for (const [chatKey, ctx] of Object.entries(this.state.chat_contexts)) {
17754
- if (ctx.current_session === alias) {
17755
- delete this.state.chat_contexts[chatKey];
18899
+ return await this.mutate(async () => {
18900
+ const session = this.state.sessions[alias];
18901
+ if (!session) {
18902
+ throw new Error(`session "${alias}" does not exist`);
17756
18903
  }
17757
- }
17758
- await this.persist();
17759
- return { wasActive };
18904
+ const wasActive = Object.values(this.state.chat_contexts).some((ctx) => ctx.current_session === alias);
18905
+ delete this.state.sessions[alias];
18906
+ for (const [chatKey, ctx] of Object.entries(this.state.chat_contexts)) {
18907
+ if (ctx.current_session === alias) {
18908
+ delete this.state.chat_contexts[chatKey];
18909
+ }
18910
+ }
18911
+ await this.persist();
18912
+ return { wasActive };
18913
+ });
17760
18914
  }
17761
18915
  toResolvedSession(session) {
17762
18916
  const agentConfig = this.config.agents[session.agent];
@@ -17779,41 +18933,51 @@ class SessionService {
17779
18933
  };
17780
18934
  }
17781
18935
  async setSessionTransportAgentCommand(alias, transportAgentCommand) {
17782
- const session = this.state.sessions[alias];
17783
- if (!session) {
17784
- throw new Error(`session "${alias}" does not exist`);
17785
- }
17786
- const normalized = transportAgentCommand?.trim();
17787
- if (normalized) {
17788
- session.transport_agent_command = normalized;
17789
- } else {
17790
- delete session.transport_agent_command;
17791
- }
17792
- session.last_used_at = new Date().toISOString();
17793
- await this.persist();
18936
+ await this.mutate(async () => {
18937
+ const session = this.state.sessions[alias];
18938
+ if (!session) {
18939
+ throw new Error(`session "${alias}" does not exist`);
18940
+ }
18941
+ const normalized = transportAgentCommand?.trim();
18942
+ if (normalized) {
18943
+ session.transport_agent_command = normalized;
18944
+ } else {
18945
+ delete session.transport_agent_command;
18946
+ }
18947
+ session.last_used_at = new Date().toISOString();
18948
+ await this.persist();
18949
+ });
18950
+ }
18951
+ async mutate(critical) {
18952
+ return await this.stateMutex.run(critical);
17794
18953
  }
17795
18954
  async persist() {
17796
18955
  await this.stateStore.save(this.state);
17797
18956
  }
17798
18957
  async createLogicalSession(alias, agent, workspace, transportSession, transportAgentCommand) {
17799
- this.validateSession(alias, agent, workspace);
17800
- const existingSession = this.state.sessions[alias];
17801
- const now = new Date().toISOString();
17802
- const normalizedTransportAgentCommand = transportAgentCommand?.trim();
17803
- const session = {
17804
- alias,
17805
- agent,
17806
- workspace,
17807
- transport_session: transportSession,
17808
- ...normalizedTransportAgentCommand ? { transport_agent_command: normalizedTransportAgentCommand } : existingSession?.transport_agent_command ? { transport_agent_command: existingSession.transport_agent_command } : {},
17809
- mode_id: existingSession?.mode_id,
17810
- reply_mode: existingSession?.reply_mode,
17811
- created_at: existingSession?.created_at ?? now,
17812
- last_used_at: now
17813
- };
17814
- this.state.sessions[alias] = session;
17815
- await this.persist();
17816
- return this.toResolvedSession(session);
18958
+ return await this.mutate(async () => {
18959
+ this.validateSession(alias, agent, workspace);
18960
+ if (this.state.orchestration.externalCoordinators[transportSession]) {
18961
+ throw new Error(`transport session "${transportSession}" conflicts with an external coordinator`);
18962
+ }
18963
+ const existingSession = this.state.sessions[alias];
18964
+ const now = new Date().toISOString();
18965
+ const normalizedTransportAgentCommand = transportAgentCommand?.trim();
18966
+ const session = {
18967
+ alias,
18968
+ agent,
18969
+ workspace,
18970
+ transport_session: transportSession,
18971
+ ...normalizedTransportAgentCommand ? { transport_agent_command: normalizedTransportAgentCommand } : existingSession?.transport_agent_command ? { transport_agent_command: existingSession.transport_agent_command } : {},
18972
+ mode_id: existingSession?.mode_id,
18973
+ reply_mode: existingSession?.reply_mode,
18974
+ created_at: existingSession?.created_at ?? now,
18975
+ last_used_at: now
18976
+ };
18977
+ this.state.sessions[alias] = session;
18978
+ await this.persist();
18979
+ return this.toResolvedSession(session);
18980
+ });
17817
18981
  }
17818
18982
  validateSession(alias, agent, workspace) {
17819
18983
  if (alias.trim().length === 0) {
@@ -17835,295 +18999,6 @@ class SessionService {
17835
18999
  }
17836
19000
  var init_session_service = () => {};
17837
19001
 
17838
- // src/orchestration/orchestration-types.ts
17839
- function createEmptyOrchestrationState() {
17840
- return {
17841
- tasks: {},
17842
- workerBindings: {},
17843
- groups: {},
17844
- humanQuestionPackages: {},
17845
- coordinatorQuestionState: {},
17846
- coordinatorRoutes: {}
17847
- };
17848
- }
17849
-
17850
- // src/state/types.ts
17851
- function createEmptyState() {
17852
- return {
17853
- sessions: {},
17854
- chat_contexts: {},
17855
- orchestration: createEmptyOrchestrationState()
17856
- };
17857
- }
17858
- var init_types2 = () => {};
17859
-
17860
- // src/state/state-store.ts
17861
- import { readFile as readFile7 } from "node:fs/promises";
17862
- function isRecord2(value) {
17863
- return typeof value === "object" && value !== null && !Array.isArray(value);
17864
- }
17865
- function isString(value) {
17866
- return typeof value === "string";
17867
- }
17868
- function isOptionalString(value) {
17869
- return value === undefined || typeof value === "string";
17870
- }
17871
- function isOptionalBoolean(value) {
17872
- return value === undefined || typeof value === "boolean";
17873
- }
17874
- function isTaskStatus(value) {
17875
- return value === "pending" || value === "needs_confirmation" || value === "running" || value === "blocked" || value === "waiting_for_human" || value === "completed" || value === "failed" || value === "cancelled";
17876
- }
17877
- function isSourceKind(value) {
17878
- return value === "human" || value === "coordinator" || value === "worker";
17879
- }
17880
- function isOpenQuestionRecord(value) {
17881
- if (!isRecord2(value)) {
17882
- return false;
17883
- }
17884
- return isString(value.questionId) && isString(value.question) && isString(value.whyBlocked) && isString(value.whatIsNeeded) && isString(value.askedAt) && (value.status === "open" || value.status === "answered" || value.status === "superseded") && isOptionalString(value.answeredAt) && (value.answerSource === undefined || value.answerSource === "coordinator" || value.answerSource === "human") && isOptionalString(value.answerText) && isOptionalString(value.packageId) && isOptionalString(value.lastWakeError) && isOptionalString(value.lastResumeError);
17885
- }
17886
- function isReviewPendingRecord(value) {
17887
- if (!isRecord2(value)) {
17888
- return false;
17889
- }
17890
- return isString(value.reviewId) && value.reason === "misrouted_answer" && isString(value.createdAt) && isString(value.resultId) && isString(value.resultText);
17891
- }
17892
- function isCorrectionPendingRecord(value) {
17893
- if (!isRecord2(value)) {
17894
- return false;
17895
- }
17896
- return isString(value.requestedAt) && value.reason === "misrouted_answer";
17897
- }
17898
- function isTaskRecord(value) {
17899
- if (!isRecord2(value)) {
17900
- return false;
17901
- }
17902
- return isString(value.taskId) && isString(value.sourceHandle) && isSourceKind(value.sourceKind) && isString(value.coordinatorSession) && isOptionalString(value.workerSession) && isString(value.workspace) && 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));
17903
- }
17904
- function isWorkerBindingRecord(value) {
17905
- if (!isRecord2(value)) {
17906
- return false;
17907
- }
17908
- return isString(value.sourceHandle) && isString(value.coordinatorSession) && isString(value.workspace) && isString(value.targetAgent) && isOptionalString(value.role);
17909
- }
17910
- function isGroupRecord(value) {
17911
- if (!isRecord2(value)) {
17912
- return false;
17913
- }
17914
- return isString(value.groupId) && isString(value.coordinatorSession) && isString(value.title) && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.coordinatorInjectedAt) && isOptionalBoolean(value.injectionPending) && isOptionalString(value.injectionAppliedAt) && isOptionalString(value.lastInjectionError);
17915
- }
17916
- function isQueuedQuestionRecord(value) {
17917
- if (!isRecord2(value)) {
17918
- return false;
17919
- }
17920
- return isString(value.taskId) && isString(value.questionId) && isString(value.enqueuedAt);
17921
- }
17922
- function isCoordinatorQuestionStateRecord(value) {
17923
- if (!isRecord2(value)) {
17924
- return false;
17925
- }
17926
- const queuedQuestions = value.queuedQuestions;
17927
- if (queuedQuestions !== undefined && !Array.isArray(queuedQuestions)) {
17928
- return false;
17929
- }
17930
- return (value.activePackageId === undefined || isString(value.activePackageId)) && (queuedQuestions === undefined || queuedQuestions.every(isQueuedQuestionRecord));
17931
- }
17932
- function isCoordinatorRouteContextRecord(value) {
17933
- if (!isRecord2(value)) {
17934
- return false;
17935
- }
17936
- return isString(value.coordinatorSession) && isString(value.chatKey) && isOptionalString(value.accountId) && isOptionalString(value.replyContextToken) && isString(value.updatedAt);
17937
- }
17938
- function isHumanQuestionPackageMessageRecord(value) {
17939
- if (!isRecord2(value)) {
17940
- return false;
17941
- }
17942
- return isString(value.messageId) && (value.kind === "initial" || value.kind === "follow_up") && isString(value.promptText) && isString(value.createdAt) && isOptionalString(value.deliveredAt) && isOptionalString(value.deliveredChatKey) && isOptionalString(value.deliveryAccountId) && isOptionalString(value.lastDeliveryError);
17943
- }
17944
- function isHumanQuestionPackageRecord(value) {
17945
- if (!isRecord2(value)) {
17946
- return false;
17947
- }
17948
- const initialTaskIds = value.initialTaskIds;
17949
- const openTaskIds = value.openTaskIds;
17950
- const resolvedTaskIds = value.resolvedTaskIds;
17951
- const messages = value.messages;
17952
- return isString(value.packageId) && isString(value.coordinatorSession) && (value.status === "active" || value.status === "closed") && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.closedAt) && Array.isArray(initialTaskIds) && initialTaskIds.every(isString) && Array.isArray(openTaskIds) && openTaskIds.every(isString) && Array.isArray(resolvedTaskIds) && resolvedTaskIds.every(isString) && Array.isArray(messages) && messages.every(isHumanQuestionPackageMessageRecord) && isOptionalString(value.awaitingReplyMessageId);
17953
- }
17954
- function parseOrchestrationState(raw, path11) {
17955
- if (raw === undefined) {
17956
- return createEmptyOrchestrationState();
17957
- }
17958
- if (!isRecord2(raw)) {
17959
- throw new Error(`state file "${path11}" must contain an object field "orchestration"`);
17960
- }
17961
- const tasks = raw.tasks;
17962
- if (tasks !== undefined && !isRecord2(tasks)) {
17963
- throw new Error(`state file "${path11}" must contain an object field "orchestration.tasks"`);
17964
- }
17965
- const workerBindings = raw.workerBindings;
17966
- if (workerBindings !== undefined && !isRecord2(workerBindings)) {
17967
- throw new Error(`state file "${path11}" must contain an object field "orchestration.workerBindings"`);
17968
- }
17969
- const groups = raw.groups;
17970
- if (groups !== undefined && !isRecord2(groups)) {
17971
- throw new Error(`state file "${path11}" must contain an object field "orchestration.groups"`);
17972
- }
17973
- const humanQuestionPackages = raw.humanQuestionPackages;
17974
- if (humanQuestionPackages !== undefined && !isRecord2(humanQuestionPackages)) {
17975
- throw new Error(`state file "${path11}" must contain an object field "orchestration.humanQuestionPackages"`);
17976
- }
17977
- const coordinatorQuestionState = raw.coordinatorQuestionState;
17978
- if (coordinatorQuestionState !== undefined && !isRecord2(coordinatorQuestionState)) {
17979
- throw new Error(`state file "${path11}" must contain an object field "orchestration.coordinatorQuestionState"`);
17980
- }
17981
- const coordinatorRoutes = raw.coordinatorRoutes;
17982
- if (coordinatorRoutes !== undefined && !isRecord2(coordinatorRoutes)) {
17983
- throw new Error(`state file "${path11}" must contain an object field "orchestration.coordinatorRoutes"`);
17984
- }
17985
- const parsedTasks = {};
17986
- for (const [taskId, task] of Object.entries(tasks ?? {})) {
17987
- if (!isTaskRecord(task)) {
17988
- throw new Error(`state file "${path11}" contains an invalid orchestration task at "${taskId}"`);
17989
- }
17990
- parsedTasks[taskId] = task;
17991
- }
17992
- const parsedWorkerBindings = {};
17993
- for (const [workerSession, binding] of Object.entries(workerBindings ?? {})) {
17994
- if (!isWorkerBindingRecord(binding)) {
17995
- throw new Error(`state file "${path11}" contains an invalid orchestration worker binding at "${workerSession}"`);
17996
- }
17997
- parsedWorkerBindings[workerSession] = binding;
17998
- }
17999
- const parsedGroups = {};
18000
- for (const [groupId, group] of Object.entries(groups ?? {})) {
18001
- if (!isGroupRecord(group)) {
18002
- throw new Error(`state file "${path11}" contains an invalid orchestration group at "${groupId}"`);
18003
- }
18004
- parsedGroups[groupId] = group;
18005
- }
18006
- const parsedHumanQuestionPackages = {};
18007
- for (const [packageId, packageRecord] of Object.entries(humanQuestionPackages ?? {})) {
18008
- if (!isHumanQuestionPackageRecord(packageRecord)) {
18009
- throw new Error(`state file "${path11}" contains an invalid human question package at "${packageId}"`);
18010
- }
18011
- parsedHumanQuestionPackages[packageId] = packageRecord;
18012
- }
18013
- const parsedCoordinatorQuestionState = {};
18014
- for (const [coordinatorSession, questionState] of Object.entries(coordinatorQuestionState ?? {})) {
18015
- if (!isCoordinatorQuestionStateRecord(questionState)) {
18016
- throw new Error(`state file "${path11}" contains an invalid coordinator question state at "${coordinatorSession}"`);
18017
- }
18018
- parsedCoordinatorQuestionState[coordinatorSession] = {
18019
- activePackageId: questionState.activePackageId,
18020
- queuedQuestions: (questionState.queuedQuestions ?? []).map((question) => ({ ...question }))
18021
- };
18022
- }
18023
- const parsedCoordinatorRoutes = {};
18024
- for (const [coordinatorSession, route] of Object.entries(coordinatorRoutes ?? {})) {
18025
- if (!isCoordinatorRouteContextRecord(route)) {
18026
- throw new Error(`state file "${path11}" contains an invalid coordinator route at "${coordinatorSession}"`);
18027
- }
18028
- parsedCoordinatorRoutes[coordinatorSession] = route;
18029
- }
18030
- return {
18031
- tasks: parsedTasks,
18032
- workerBindings: parsedWorkerBindings,
18033
- groups: parsedGroups,
18034
- humanQuestionPackages: parsedHumanQuestionPackages,
18035
- coordinatorQuestionState: parsedCoordinatorQuestionState,
18036
- coordinatorRoutes: parsedCoordinatorRoutes
18037
- };
18038
- }
18039
- function isReplyMode(value) {
18040
- return value === "stream" || value === "final" || value === "verbose";
18041
- }
18042
- function isSessionRecord(value) {
18043
- if (!isRecord2(value)) {
18044
- return false;
18045
- }
18046
- return isString(value.alias) && isString(value.agent) && isString(value.workspace) && isString(value.transport_session) && isOptionalString(value.transport_agent_command) && isOptionalString(value.mode_id) && (value.reply_mode === undefined || isReplyMode(value.reply_mode)) && isString(value.created_at) && isString(value.last_used_at);
18047
- }
18048
- function parseSessions(raw, path11) {
18049
- const sessions = {};
18050
- for (const [alias, value] of Object.entries(raw)) {
18051
- if (!isSessionRecord(value)) {
18052
- throw new Error(`state file "${path11}" contains malformed session record "${alias}"`);
18053
- }
18054
- sessions[alias] = value;
18055
- }
18056
- return sessions;
18057
- }
18058
- function isChatContextRecord(value) {
18059
- return isRecord2(value) && isString(value.current_session);
18060
- }
18061
- function parseChatContexts(raw, path11) {
18062
- const chatContexts = {};
18063
- for (const [chatKey, value] of Object.entries(raw)) {
18064
- if (!isChatContextRecord(value)) {
18065
- throw new Error(`state file "${path11}" contains malformed chat context record "${chatKey}"`);
18066
- }
18067
- chatContexts[chatKey] = value;
18068
- }
18069
- return chatContexts;
18070
- }
18071
- function parseState(raw, path11) {
18072
- if (!isRecord2(raw)) {
18073
- throw new Error(`state file "${path11}" must contain a JSON object`);
18074
- }
18075
- const sessions = raw.sessions;
18076
- if (!isRecord2(sessions)) {
18077
- throw new Error(`state file "${path11}" must contain an object field "sessions"`);
18078
- }
18079
- const chatContexts = raw.chat_contexts;
18080
- if (!isRecord2(chatContexts)) {
18081
- throw new Error(`state file "${path11}" must contain an object field "chat_contexts"`);
18082
- }
18083
- const orchestration = parseOrchestrationState(raw.orchestration, path11);
18084
- return {
18085
- sessions: parseSessions(sessions, path11),
18086
- chat_contexts: parseChatContexts(chatContexts, path11),
18087
- orchestration
18088
- };
18089
- }
18090
-
18091
- class StateStore {
18092
- path;
18093
- constructor(path11) {
18094
- this.path = path11;
18095
- }
18096
- async load() {
18097
- try {
18098
- const content = await readFile7(this.path, "utf8");
18099
- if (content.trim() === "") {
18100
- return createEmptyState();
18101
- }
18102
- let parsed;
18103
- try {
18104
- parsed = JSON.parse(content);
18105
- } catch (error2) {
18106
- throw new Error(`failed to parse state file "${this.path}"`, {
18107
- cause: error2
18108
- });
18109
- }
18110
- return parseState(parsed, this.path);
18111
- } catch (error2) {
18112
- if (error2.code === "ENOENT") {
18113
- return createEmptyState();
18114
- }
18115
- throw error2;
18116
- }
18117
- }
18118
- async save(state) {
18119
- await writePrivateFileAtomic(this.path, JSON.stringify(state, null, 2));
18120
- }
18121
- }
18122
- var init_state_store = __esm(() => {
18123
- init_private_file();
18124
- init_types2();
18125
- });
18126
-
18127
19002
  // src/run-console.ts
18128
19003
  var exports_run_console = {};
18129
19004
  __export(exports_run_console, {
@@ -18700,19 +19575,30 @@ class AcpxBridgeTransport {
18700
19575
  }
18701
19576
  } : undefined);
18702
19577
  }
18703
- async prompt(session, text, reply, replyContext) {
19578
+ async prompt(session, text, reply, replyContext, options) {
18704
19579
  const sink = reply ? createQuotaGatedReplySink({
18705
19580
  reply,
18706
19581
  ...replyContext ? { replyContext } : {}
18707
19582
  }) : null;
19583
+ let segmentError;
19584
+ let segmentChain = Promise.resolve();
18708
19585
  const result = await this.client.request("prompt", {
18709
19586
  ...this.toParams(session),
18710
- text
19587
+ text,
19588
+ ...options?.media ? { media: options.media } : {}
18711
19589
  }, (event) => {
18712
19590
  if (event.type === "prompt.segment") {
19591
+ const onSegment = options?.onSegment;
19592
+ if (onSegment) {
19593
+ const segmentText = event.text;
19594
+ segmentChain = segmentChain.then(() => onSegment(segmentText)).catch((error2) => {
19595
+ segmentError ??= error2;
19596
+ });
19597
+ }
18713
19598
  sink?.feedSegment(event.text);
18714
19599
  }
18715
19600
  });
19601
+ await segmentChain;
18716
19602
  if (sink) {
18717
19603
  const { overflowCount } = sink.finalize();
18718
19604
  await sink.drain({ timeoutMs: 30000 });
@@ -18721,10 +19607,16 @@ class AcpxBridgeTransport {
18721
19607
  throw deferred;
18722
19608
  }
18723
19609
  const summary = buildOverflowSummary(overflowCount);
19610
+ if (segmentError) {
19611
+ throw segmentError;
19612
+ }
18724
19613
  return { text: summary ? `${summary}
18725
19614
 
18726
19615
  ${result.text}` : "" };
18727
19616
  }
19617
+ if (segmentError) {
19618
+ throw segmentError;
19619
+ }
18728
19620
  return result;
18729
19621
  }
18730
19622
  async setMode(session, modeId) {
@@ -18780,6 +19672,115 @@ var init_spawn_command = __esm(() => {
18780
19672
  SCRIPT_FILE_PATTERN = /\.(c|m)?js$/i;
18781
19673
  });
18782
19674
 
19675
+ // src/transport/prompt-media.ts
19676
+ import { mkdtemp, open as open3, rm as rm7, writeFile as writeFile6 } from "node:fs/promises";
19677
+ import { tmpdir as defaultTmpdir } from "node:os";
19678
+ import path11 from "node:path";
19679
+ async function createStructuredPromptFile(text, media, deps = defaultStructuredPromptFileDeps) {
19680
+ if (!media) {
19681
+ return null;
19682
+ }
19683
+ if (media.type !== "image") {
19684
+ throw new Error("prompt media type is not supported; only image media is supported");
19685
+ }
19686
+ const imageData = await deps.readImageFile(media.filePath, MAX_STRUCTURED_IMAGE_BYTES);
19687
+ if (imageData.byteLength === 0) {
19688
+ throw new Error("image prompt must not be empty");
19689
+ }
19690
+ if (imageData.byteLength > MAX_STRUCTURED_IMAGE_BYTES) {
19691
+ throw new Error(`image prompt exceeds ${MAX_STRUCTURED_IMAGE_BYTES} bytes`);
19692
+ }
19693
+ const blocks = [];
19694
+ if (text.trim().length > 0) {
19695
+ blocks.push({ type: "text", text });
19696
+ }
19697
+ blocks.push({
19698
+ type: "image",
19699
+ mimeType: resolveImageMimeType(imageData, media.mimeType),
19700
+ data: imageData.toString("base64")
19701
+ });
19702
+ let dir = "";
19703
+ try {
19704
+ dir = await deps.mkdtemp(path11.join(deps.tmpdir(), "weacpx-acp-prompt-"));
19705
+ const filePath = path11.join(dir, "prompt.json");
19706
+ await deps.writeFile(filePath, JSON.stringify(blocks), "utf8");
19707
+ return {
19708
+ filePath,
19709
+ cleanup: async () => {
19710
+ await deps.rm(dir, { recursive: true, force: true });
19711
+ }
19712
+ };
19713
+ } catch (error2) {
19714
+ if (dir) {
19715
+ try {
19716
+ await deps.rm(dir, { recursive: true, force: true });
19717
+ } catch {}
19718
+ }
19719
+ throw error2;
19720
+ }
19721
+ }
19722
+ async function readImageFileBounded(filePath, maxBytes) {
19723
+ const handle = await open3(filePath, "r");
19724
+ try {
19725
+ const imageStats = await handle.stat();
19726
+ if (!imageStats.isFile()) {
19727
+ throw new Error("image prompt path must be a regular file");
19728
+ }
19729
+ if (imageStats.size > maxBytes) {
19730
+ throw new Error(`image prompt exceeds ${maxBytes} bytes`);
19731
+ }
19732
+ const chunks = [];
19733
+ let total = 0;
19734
+ let position = 0;
19735
+ const chunkSize = 1024 * 1024;
19736
+ while (total <= maxBytes) {
19737
+ const buffer = Buffer.allocUnsafe(Math.min(chunkSize, maxBytes + 1 - total));
19738
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
19739
+ if (bytesRead === 0)
19740
+ break;
19741
+ chunks.push(buffer.subarray(0, bytesRead));
19742
+ total += bytesRead;
19743
+ position += bytesRead;
19744
+ }
19745
+ return Buffer.concat(chunks, total);
19746
+ } finally {
19747
+ await handle.close();
19748
+ }
19749
+ }
19750
+ function resolveImageMimeType(buffer, declaredMimeType) {
19751
+ if (/^image\/[A-Za-z0-9.+-]+$/.test(declaredMimeType) && declaredMimeType !== "image/*") {
19752
+ return declaredMimeType;
19753
+ }
19754
+ if (buffer.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"))) {
19755
+ return "image/png";
19756
+ }
19757
+ if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
19758
+ return "image/jpeg";
19759
+ }
19760
+ const header6 = buffer.subarray(0, 6).toString("ascii");
19761
+ if (header6 === "GIF87a" || header6 === "GIF89a") {
19762
+ return "image/gif";
19763
+ }
19764
+ if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
19765
+ return "image/webp";
19766
+ }
19767
+ if (buffer.length >= 2 && buffer.subarray(0, 2).toString("ascii") === "BM") {
19768
+ return "image/bmp";
19769
+ }
19770
+ return "image/png";
19771
+ }
19772
+ var MAX_STRUCTURED_IMAGE_BYTES, defaultStructuredPromptFileDeps;
19773
+ var init_prompt_media = __esm(() => {
19774
+ MAX_STRUCTURED_IMAGE_BYTES = 100 * 1024 * 1024;
19775
+ defaultStructuredPromptFileDeps = {
19776
+ readImageFile: readImageFileBounded,
19777
+ mkdtemp,
19778
+ writeFile: writeFile6,
19779
+ rm: rm7,
19780
+ tmpdir: defaultTmpdir
19781
+ };
19782
+ });
19783
+
18783
19784
  // src/transport/streaming-prompt.ts
18784
19785
  function createStreamingPromptState(formatToolCalls = false) {
18785
19786
  return {
@@ -18945,7 +19946,7 @@ async function ensureNodePtyHelperExecutable(helperPath, chmod2 = chmodFs) {
18945
19946
  var init_node_pty_helper = () => {};
18946
19947
 
18947
19948
  // src/transport/acpx-queue-owner-launcher.ts
18948
- import { createHash as createHash2 } from "node:crypto";
19949
+ import { createHash as createHash3 } from "node:crypto";
18949
19950
  import { spawn as spawn6 } from "node:child_process";
18950
19951
  import { readFile as readFile8, unlink as unlink2 } from "node:fs/promises";
18951
19952
  import { homedir as homedir7 } from "node:os";
@@ -19114,7 +20115,7 @@ function queueLockFilePath(sessionId) {
19114
20115
  return join9(homedir7(), ".acpx", "queues", `${shortHash(sessionId, 24)}.lock`);
19115
20116
  }
19116
20117
  function shortHash(value, length) {
19117
- return createHash2("sha256").update(value).digest("hex").slice(0, length);
20118
+ return createHash3("sha256").update(value).digest("hex").slice(0, length);
19118
20119
  }
19119
20120
  function resolveDefaultWeacpxCommand(env) {
19120
20121
  if (env.WEACPX_CLI_COMMAND?.trim()) {
@@ -19250,20 +20251,30 @@ class AcpxCliTransport {
19250
20251
  timeoutMs: this.sessionInitTimeoutMs
19251
20252
  });
19252
20253
  }
19253
- async prompt(session, text, reply, replyContext) {
20254
+ async prompt(session, text, reply, replyContext, options) {
19254
20255
  await this.launchMcpQueueOwnerIfNeeded(session);
19255
- const args = this.buildPromptArgs(session, text);
19256
- if (reply) {
19257
- const formatToolCalls = session.replyMode === "verbose";
19258
- const { result: result2, overflowCount } = await this.runStreamingPrompt(this.command, args, reply, 30000, formatToolCalls, replyContext);
19259
- const baseText = getPromptText(result2);
19260
- const summary = buildOverflowSummary(overflowCount);
19261
- return { text: summary ? `${summary}
20256
+ const structuredPrompt = await createStructuredPromptFile(text, options?.media);
20257
+ const args = this.buildPromptArgs(session, text, structuredPrompt?.filePath);
20258
+ try {
20259
+ if (reply || options?.onSegment) {
20260
+ const formatToolCalls = session.replyMode === "verbose";
20261
+ const { result: result2, overflowCount } = await this.runStreamingPrompt(this.command, args, reply, 30000, formatToolCalls, replyContext, options?.onSegment);
20262
+ const baseText = getPromptText(result2);
20263
+ if (!reply) {
20264
+ return { text: baseText };
20265
+ }
20266
+ const summary = buildOverflowSummary(overflowCount);
20267
+ return { text: summary ? `${summary}
19262
20268
 
19263
20269
  ${baseText}` : "" };
20270
+ }
20271
+ const result = await this.runCommand(this.command, args);
20272
+ return { text: getPromptText(result) };
20273
+ } finally {
20274
+ try {
20275
+ await structuredPrompt?.cleanup();
20276
+ } catch {}
19264
20277
  }
19265
- const result = await this.runCommand(this.command, args);
19266
- return { text: getPromptText(result) };
19267
20278
  }
19268
20279
  async setMode(session, modeId) {
19269
20280
  await this.run(this.buildArgs(session, [
@@ -19384,7 +20395,7 @@ ${baseText}` : "" };
19384
20395
  })
19385
20396
  ]);
19386
20397
  }
19387
- async runStreamingPrompt(command, args, reply, maxSegmentWaitMs = 30000, formatToolCalls = false, replyContext) {
20398
+ async runStreamingPrompt(command, args, reply, maxSegmentWaitMs = 30000, formatToolCalls = false, replyContext, onSegment) {
19388
20399
  return await new Promise((resolve2, reject) => {
19389
20400
  const spawnSpec = resolveSpawnCommand(command, args);
19390
20401
  const child = spawn7(spawnSpec.command, spawnSpec.args, { stdio: ["ignore", "pipe", "pipe"] });
@@ -19392,16 +20403,26 @@ ${baseText}` : "" };
19392
20403
  let stderr = "";
19393
20404
  const state = createStreamingPromptState(formatToolCalls);
19394
20405
  let lastReplyAt = Date.now();
19395
- const sink = createQuotaGatedReplySink({
20406
+ let segmentChain = Promise.resolve();
20407
+ let segmentError;
20408
+ const sink = reply ? createQuotaGatedReplySink({
19396
20409
  reply,
19397
20410
  ...replyContext ? { replyContext } : {}
19398
- });
20411
+ }) : null;
20412
+ const feedSegment = (segment) => {
20413
+ if (onSegment) {
20414
+ segmentChain = segmentChain.then(() => onSegment(segment)).catch((error2) => {
20415
+ segmentError ??= error2;
20416
+ });
20417
+ }
20418
+ sink?.feedSegment(segment);
20419
+ lastReplyAt = Date.now();
20420
+ };
19399
20421
  const flushBuffer = () => {
19400
20422
  const remaining = state.buffer.trim();
19401
20423
  if (remaining.length > 0) {
19402
20424
  state.buffer = "";
19403
- sink.feedSegment(remaining);
19404
- lastReplyAt = Date.now();
20425
+ feedSegment(remaining);
19405
20426
  }
19406
20427
  };
19407
20428
  const timer = setInterval(() => {
@@ -19414,8 +20435,7 @@ ${baseText}` : "" };
19414
20435
  stdout2 += String(chunk);
19415
20436
  parseStreamingDataChunk(state, String(chunk));
19416
20437
  for (const segment of state.segments.splice(0)) {
19417
- sink.feedSegment(segment);
19418
- lastReplyAt = Date.now();
20438
+ feedSegment(segment);
19419
20439
  }
19420
20440
  });
19421
20441
  child.stderr.on("data", (chunk) => {
@@ -19429,19 +20449,28 @@ ${baseText}` : "" };
19429
20449
  clearInterval(timer);
19430
20450
  const remaining = state.finalize();
19431
20451
  if (remaining.length > 0) {
19432
- sink.feedSegment(remaining);
19433
- }
19434
- const { overflowCount } = sink.finalize();
19435
- sink.drain({ timeoutMs: 30000 }).then(() => {
19436
- const deferred = sink.getPendingError();
20452
+ feedSegment(remaining);
20453
+ }
20454
+ const { overflowCount } = sink?.finalize() ?? { overflowCount: 0 };
20455
+ Promise.all([
20456
+ sink?.drain({ timeoutMs: 30000 }) ?? Promise.resolve(),
20457
+ segmentChain
20458
+ ]).then(() => {
20459
+ const deferred = sink?.getPendingError();
19437
20460
  if (deferred) {
19438
20461
  reject(deferred);
19439
20462
  return;
19440
20463
  }
20464
+ if (segmentError) {
20465
+ reject(segmentError);
20466
+ return;
20467
+ }
19441
20468
  resolve2({
19442
20469
  result: { code: code ?? 1, stdout: stdout2, stderr },
19443
20470
  overflowCount
19444
20471
  });
20472
+ }).catch((error2) => {
20473
+ reject(error2);
19445
20474
  });
19446
20475
  });
19447
20476
  });
@@ -19459,7 +20488,7 @@ ${baseText}` : "" };
19459
20488
  }
19460
20489
  return [...prefix, session.agent, ...tail2];
19461
20490
  }
19462
- buildPromptArgs(session, text) {
20491
+ buildPromptArgs(session, text, promptFile) {
19463
20492
  const prefix = [
19464
20493
  "--format",
19465
20494
  "json",
@@ -19468,7 +20497,7 @@ ${baseText}` : "" };
19468
20497
  session.cwd,
19469
20498
  ...this.buildPermissionArgs()
19470
20499
  ];
19471
- const tail2 = ["prompt", "-s", session.transportSession, text];
20500
+ const tail2 = promptFile ? ["prompt", "-s", session.transportSession, "--file", promptFile] : ["prompt", "-s", session.transportSession, text];
19472
20501
  if (session.agentCommand) {
19473
20502
  return [...prefix, "--agent", session.agentCommand, ...tail2];
19474
20503
  }
@@ -19507,6 +20536,7 @@ var require4;
19507
20536
  var init_acpx_cli_transport = __esm(() => {
19508
20537
  init_spawn_command();
19509
20538
  init_prompt_output();
20539
+ init_prompt_media();
19510
20540
  init_streaming_prompt();
19511
20541
  init_quota_gated_reply_sink();
19512
20542
  init_node_pty_helper();
@@ -19908,8 +20938,9 @@ async function buildApp(paths, deps = {}) {
19908
20938
  await logger2.cleanup();
19909
20939
  const acpxCommand = resolveAcpxCommand({ configuredCommand: config2.transport.command });
19910
20940
  const stateStore = new StateStore(paths.statePath);
19911
- let state = await stateStore.load();
19912
- const sessions = new SessionService(config2, stateStore, state);
20941
+ const state = await stateStore.load();
20942
+ const stateMutex = new AsyncMutex;
20943
+ const sessions = new SessionService(config2, stateStore, state, { stateMutex });
19913
20944
  const pendingWorkerDispatches = new Set;
19914
20945
  const transport = config2.transport.type === "acpx-bridge" ? await (deps.createBridgeTransport?.() ?? Promise.resolve(new AcpxBridgeTransport(await spawnAcpxBridgeClient({
19915
20946
  acpxCommand,
@@ -20058,34 +21089,53 @@ async function buildApp(paths, deps = {}) {
20058
21089
  return;
20059
21090
  }
20060
21091
  };
21092
+ const resolveWorkerRuntimeSession = (input) => {
21093
+ if (!input.cwd) {
21094
+ return sessions.resolveSession(input.workerSession, input.targetAgent, input.workspace, input.workerSession);
21095
+ }
21096
+ const agentConfig = config2.agents[input.targetAgent];
21097
+ if (!agentConfig) {
21098
+ throw new Error(`agent "${input.targetAgent}" is not configured`);
21099
+ }
21100
+ return {
21101
+ alias: input.workerSession,
21102
+ agent: input.targetAgent,
21103
+ agentCommand: resolveAgentCommand(agentConfig.driver, agentConfig.command),
21104
+ workspace: input.workspace,
21105
+ transportSession: input.workerSession,
21106
+ cwd: input.cwd
21107
+ };
21108
+ };
20061
21109
  const launchWorkerTurn = (input) => {
20062
- const session = sessions.resolveSession(input.workerSession, input.targetAgent, input.workspace, input.workerSession);
21110
+ const session = resolveWorkerRuntimeSession(input);
20063
21111
  session.mcpCoordinatorSession = input.coordinatorSession;
20064
21112
  session.mcpSourceHandle = input.workerSession;
20065
21113
  const workerDispatch = (async () => {
20066
21114
  let taskRecord;
20067
21115
  try {
20068
21116
  const progressBuffer = new ProgressLineBuffer;
20069
- const result = await transport.prompt(session, input.promptText, async (chunk) => {
20070
- const summaries = progressBuffer.feed(chunk);
20071
- for (const summary of summaries) {
20072
- try {
20073
- await orchestration.recordTaskProgress(input.taskId);
20074
- const taskState = await orchestration.getTask(input.taskId);
20075
- if (taskState?.chatKey && taskState.replyContextToken) {
20076
- await deliverOrchestrationTaskProgress(taskState, renderTaskProgress(taskState, summary), {
20077
- listAccountIds: () => listWeixinAccountIds(),
20078
- resolveAccount: (accountId) => resolveWeixinAccount(accountId),
20079
- getContextToken: (accountId, userId) => getContextToken(accountId, userId),
20080
- reserveMidSegment: (chatKey) => quota.reserveMidSegment(chatKey),
20081
- logger: logger2
21117
+ const result = await transport.prompt(session, input.promptText, undefined, undefined, {
21118
+ onSegment: async (chunk) => {
21119
+ const summaries = progressBuffer.feed(chunk);
21120
+ for (const summary of summaries) {
21121
+ try {
21122
+ await orchestration.recordTaskProgress(input.taskId);
21123
+ const taskState = await orchestration.getTask(input.taskId);
21124
+ if (taskState?.chatKey && taskState.replyContextToken) {
21125
+ await deliverOrchestrationTaskProgress(taskState, renderTaskProgress(taskState, summary), {
21126
+ listAccountIds: () => listWeixinAccountIds(),
21127
+ resolveAccount: (accountId) => resolveWeixinAccount(accountId),
21128
+ getContextToken: (accountId, userId) => getContextToken(accountId, userId),
21129
+ reserveMidSegment: (chatKey) => quota.reserveMidSegment(chatKey),
21130
+ logger: logger2
21131
+ });
21132
+ }
21133
+ } catch (error2) {
21134
+ await logger2.error("orchestration.progress.send_failed", "failed to send task progress", {
21135
+ taskId: input.taskId,
21136
+ message: error2 instanceof Error ? error2.message : String(error2)
20082
21137
  });
20083
21138
  }
20084
- } catch (error2) {
20085
- await logger2.error("orchestration.progress.send_failed", "failed to send task progress", {
20086
- taskId: input.taskId,
20087
- message: error2 instanceof Error ? error2.message : String(error2)
20088
- });
20089
21139
  }
20090
21140
  }
20091
21141
  });
@@ -20121,7 +21171,7 @@ async function buildApp(paths, deps = {}) {
20121
21171
  });
20122
21172
  }
20123
21173
  }
20124
- if (taskRecord) {
21174
+ if (taskRecord && !isRuntimeExternalCoordinator(taskRecord.coordinatorSession)) {
20125
21175
  try {
20126
21176
  await wakeCoordinator(taskRecord.coordinatorSession);
20127
21177
  } catch (wakeError) {
@@ -20138,6 +21188,9 @@ async function buildApp(paths, deps = {}) {
20138
21188
  pendingWorkerDispatches.delete(workerDispatch);
20139
21189
  });
20140
21190
  };
21191
+ const isRuntimeExternalCoordinator = (coordinatorSession) => {
21192
+ return Boolean(state.orchestration.externalCoordinators[coordinatorSession]);
21193
+ };
20141
21194
  orchestration = new OrchestrationService({
20142
21195
  now: deps.loggerNow ?? (() => new Date),
20143
21196
  createId: () => randomUUID3(),
@@ -20145,39 +21198,42 @@ async function buildApp(paths, deps = {}) {
20145
21198
  loadState: async () => JSON.parse(JSON.stringify(state)),
20146
21199
  saveState: async (nextState) => {
20147
21200
  await stateStore.save(nextState);
20148
- state = nextState;
21201
+ replaceRuntimeState(state, nextState);
20149
21202
  },
20150
- ensureWorkerSession: async ({ workerSession, targetAgent, workspace, coordinatorSession }) => {
20151
- const session = sessions.resolveSession(workerSession, targetAgent, workspace, workerSession);
21203
+ stateMutex,
21204
+ ensureWorkerSession: async ({ workerSession, targetAgent, workspace, cwd, coordinatorSession }) => {
21205
+ const session = resolveWorkerRuntimeSession({ workerSession, targetAgent, workspace, ...cwd ? { cwd } : {} });
20152
21206
  session.mcpCoordinatorSession = coordinatorSession;
20153
21207
  session.mcpSourceHandle = workerSession;
20154
21208
  await transport.ensureSession(session);
20155
21209
  return workerSession;
20156
21210
  },
20157
- dispatchWorkerTask: async ({ workerSession, coordinatorSession, targetAgent, workspace, taskId, role, task }) => {
21211
+ dispatchWorkerTask: async ({ workerSession, coordinatorSession, targetAgent, workspace, cwd, taskId, role, task }) => {
20158
21212
  launchWorkerTurn({
20159
21213
  taskId,
20160
21214
  workerSession,
20161
21215
  coordinatorSession,
20162
21216
  targetAgent,
20163
21217
  workspace,
21218
+ ...cwd ? { cwd } : {},
20164
21219
  promptText: buildWorkerTaskPrompt({ taskId, workerSession, role, task })
20165
21220
  });
20166
21221
  },
20167
- cancelWorkerTask: async ({ workerSession, targetAgent, workspace }) => {
20168
- const session = sessions.resolveSession(workerSession, targetAgent, workspace, workerSession);
21222
+ cancelWorkerTask: async ({ workerSession, targetAgent, workspace, cwd }) => {
21223
+ const session = resolveWorkerRuntimeSession({ workerSession, targetAgent, workspace, ...cwd ? { cwd } : {} });
20169
21224
  const result = await transport.cancel(session);
20170
21225
  if (!result.cancelled) {
20171
21226
  throw new Error(result.message || "worker task cancel was not acknowledged");
20172
21227
  }
20173
21228
  },
20174
- resumeWorkerTask: async ({ taskId, workerSession, coordinatorSession, targetAgent, workspace, answer }) => {
21229
+ resumeWorkerTask: async ({ taskId, workerSession, coordinatorSession, targetAgent, workspace, cwd, answer }) => {
20175
21230
  launchWorkerTurn({
20176
21231
  taskId,
20177
21232
  workerSession,
20178
21233
  coordinatorSession,
20179
21234
  targetAgent,
20180
21235
  workspace,
21236
+ ...cwd ? { cwd } : {},
20181
21237
  promptText: buildWorkerAnswerPrompt(answer)
20182
21238
  });
20183
21239
  },
@@ -20187,15 +21243,15 @@ async function buildApp(paths, deps = {}) {
20187
21243
  deliverCoordinatorMessage: async (input) => {
20188
21244
  await sendCoordinatorMessage(input);
20189
21245
  },
20190
- interruptWorkerTask: async ({ workerSession, targetAgent, workspace }) => {
20191
- const session = sessions.resolveSession(workerSession, targetAgent, workspace, workerSession);
21246
+ interruptWorkerTask: async ({ workerSession, targetAgent, workspace, cwd }) => {
21247
+ const session = resolveWorkerRuntimeSession({ workerSession, targetAgent, workspace, ...cwd ? { cwd } : {} });
20192
21248
  const result = await transport.cancel(session);
20193
21249
  if (!result.cancelled) {
20194
21250
  throw new Error(result.message || "worker interrupt was not acknowledged");
20195
21251
  }
20196
21252
  },
20197
- findReusableWorkerSession: async ({ coordinatorSession, workspace, targetAgent, role }) => {
20198
- const binding = Object.entries(state.orchestration.workerBindings).find(([, current]) => current.coordinatorSession === coordinatorSession && current.workspace === workspace && current.targetAgent === targetAgent && current.role === role);
21253
+ findReusableWorkerSession: async ({ coordinatorSession, workspace, cwd, targetAgent, role }) => {
21254
+ const binding = Object.entries(state.orchestration.workerBindings).find(([, current]) => current.coordinatorSession === coordinatorSession && current.workspace === workspace && current.cwd === cwd && current.targetAgent === targetAgent && current.role === role);
20199
21255
  return binding?.[0] ?? null;
20200
21256
  },
20201
21257
  logger: logger2
@@ -20241,6 +21297,11 @@ async function buildApp(paths, deps = {}) {
20241
21297
  }
20242
21298
  };
20243
21299
  }
21300
+ function replaceRuntimeState(target, source) {
21301
+ target.sessions = source.sessions;
21302
+ target.chat_contexts = source.chat_contexts;
21303
+ target.orchestration = source.orchestration;
21304
+ }
20244
21305
  async function main2() {
20245
21306
  const paths = resolveRuntimePaths();
20246
21307
  try {
@@ -20722,107 +21783,107 @@ async function checkRuntime(options = {}) {
20722
21783
  }
20723
21784
  function createRuntimeFsProbe() {
20724
21785
  return {
20725
- stat: async (path11) => await stat2(path11),
20726
- access: async (path11, mode) => await access3(path11, mode)
21786
+ stat: async (path12) => await stat2(path12),
21787
+ access: async (path12, mode) => await access3(path12, mode)
20727
21788
  };
20728
21789
  }
20729
- async function checkDirectoryCreatable(label, path11, probe, platform) {
21790
+ async function checkDirectoryCreatable(label, path12, probe, platform) {
20730
21791
  try {
20731
- const stats = await probe.stat(path11);
21792
+ const stats = await probe.stat(path12);
20732
21793
  if (!stats.isDirectory()) {
20733
21794
  return {
20734
21795
  ok: false,
20735
- detail: `${label}: ${path11} (exists but is not a directory)`
21796
+ detail: `${label}: ${path12} (exists but is not a directory)`
20736
21797
  };
20737
21798
  }
20738
- await probe.access(path11, directoryAccessMode(platform));
21799
+ await probe.access(path12, directoryAccessMode(platform));
20739
21800
  return {
20740
21801
  ok: true,
20741
- detail: `${label}: ${path11} (writable)`
21802
+ detail: `${label}: ${path12} (writable)`
20742
21803
  };
20743
21804
  } catch (error2) {
20744
21805
  if (!isMissingPathError(error2)) {
20745
21806
  return {
20746
21807
  ok: false,
20747
- detail: `${label}: ${path11} (unusable: ${formatError6(error2)})`
21808
+ detail: `${label}: ${path12} (unusable: ${formatError6(error2)})`
20748
21809
  };
20749
21810
  }
20750
- const parentCheck = await checkCreatableAncestorDirectory(path11, probe, platform);
21811
+ const parentCheck = await checkCreatableAncestorDirectory(path12, probe, platform);
20751
21812
  if (!parentCheck.ok) {
20752
21813
  return {
20753
21814
  ok: false,
20754
- detail: `${label}: ${path11} (parent not writable: ${parentCheck.blockingPath})`
21815
+ detail: `${label}: ${path12} (parent not writable: ${parentCheck.blockingPath})`
20755
21816
  };
20756
21817
  }
20757
21818
  return {
20758
21819
  ok: true,
20759
- detail: `${label}: ${path11} (creatable via ${parentCheck.creatableFrom})`
21820
+ detail: `${label}: ${path12} (creatable via ${parentCheck.creatableFrom})`
20760
21821
  };
20761
21822
  }
20762
21823
  }
20763
- async function checkFileCreatable(label, path11, probe, platform) {
21824
+ async function checkFileCreatable(label, path12, probe, platform) {
20764
21825
  try {
20765
- const stats = await probe.stat(path11);
21826
+ const stats = await probe.stat(path12);
20766
21827
  if (stats.isDirectory()) {
20767
21828
  return {
20768
21829
  ok: false,
20769
- detail: `${label}: ${path11} (exists but is a directory)`
21830
+ detail: `${label}: ${path12} (exists but is a directory)`
20770
21831
  };
20771
21832
  }
20772
- await probe.access(path11, constants.W_OK);
21833
+ await probe.access(path12, constants.W_OK);
20773
21834
  return {
20774
21835
  ok: true,
20775
- detail: `${label}: ${path11} (writable)`
21836
+ detail: `${label}: ${path12} (writable)`
20776
21837
  };
20777
21838
  } catch (error2) {
20778
21839
  if (!isMissingPathError(error2)) {
20779
21840
  return {
20780
21841
  ok: false,
20781
- detail: `${label}: ${path11} (unusable: ${formatError6(error2)})`
21842
+ detail: `${label}: ${path12} (unusable: ${formatError6(error2)})`
20782
21843
  };
20783
21844
  }
20784
- const parentCheck = await checkCreatableAncestorDirectory(dirname11(path11), probe, platform);
21845
+ const parentCheck = await checkCreatableAncestorDirectory(dirname11(path12), probe, platform);
20785
21846
  if (!parentCheck.ok) {
20786
21847
  return {
20787
21848
  ok: false,
20788
- detail: `${label}: ${path11} (parent not writable: ${parentCheck.blockingPath})`
21849
+ detail: `${label}: ${path12} (parent not writable: ${parentCheck.blockingPath})`
20789
21850
  };
20790
21851
  }
20791
21852
  return {
20792
21853
  ok: true,
20793
- detail: `${label}: ${path11} (creatable via ${parentCheck.creatableFrom})`
21854
+ detail: `${label}: ${path12} (creatable via ${parentCheck.creatableFrom})`
20794
21855
  };
20795
21856
  }
20796
21857
  }
20797
- async function checkCreatableAncestorDirectory(path11, probe, platform) {
21858
+ async function checkCreatableAncestorDirectory(path12, probe, platform) {
20798
21859
  try {
20799
- const stats = await probe.stat(path11);
21860
+ const stats = await probe.stat(path12);
20800
21861
  if (!stats.isDirectory()) {
20801
21862
  return {
20802
21863
  ok: false,
20803
- creatableFrom: path11,
20804
- blockingPath: path11
21864
+ creatableFrom: path12,
21865
+ blockingPath: path12
20805
21866
  };
20806
21867
  }
20807
- await probe.access(path11, directoryAccessMode(platform));
21868
+ await probe.access(path12, directoryAccessMode(platform));
20808
21869
  return {
20809
21870
  ok: true,
20810
- creatableFrom: path11
21871
+ creatableFrom: path12
20811
21872
  };
20812
21873
  } catch (error2) {
20813
21874
  if (!isMissingPathError(error2)) {
20814
21875
  return {
20815
21876
  ok: false,
20816
- creatableFrom: path11,
20817
- blockingPath: path11
21877
+ creatableFrom: path12,
21878
+ blockingPath: path12
20818
21879
  };
20819
21880
  }
20820
- const parent = dirname11(path11);
20821
- if (parent === path11) {
21881
+ const parent = dirname11(path12);
21882
+ if (parent === path12) {
20822
21883
  return {
20823
21884
  ok: false,
20824
- creatableFrom: path11,
20825
- blockingPath: path11
21885
+ creatableFrom: path12,
21886
+ blockingPath: path12
20826
21887
  };
20827
21888
  }
20828
21889
  const parentCheck = await checkCreatableAncestorDirectory(parent, probe, platform);
@@ -21393,9 +22454,11 @@ var init_doctor2 = __esm(async () => {
21393
22454
 
21394
22455
  // src/cli.ts
21395
22456
  init_config_store();
22457
+ init_load_config();
21396
22458
  init_ensure_config();
21397
22459
  init_create_daemon_controller();
21398
22460
  init_daemon_files();
22461
+ import { randomUUID as randomUUID4 } from "node:crypto";
21399
22462
  import { homedir as homedir12 } from "node:os";
21400
22463
  import { sep } from "node:path";
21401
22464
  import { fileURLToPath as fileURLToPath5 } from "node:url";
@@ -33832,6 +34895,7 @@ function requireHome(env) {
33832
34895
  }
33833
34896
 
33834
34897
  // src/mcp/weacpx-mcp-tools.ts
34898
+ init_task_wait_timeouts();
33835
34899
  init_quota_errors();
33836
34900
  var groupStatusSchema = exports_external.enum(["pending", "running", "terminal"]);
33837
34901
  var taskStatusSchema = exports_external.enum([
@@ -33860,6 +34924,7 @@ function buildWeacpxMcpToolRegistry(input) {
33860
34924
  inputSchema: exports_external.object({
33861
34925
  targetAgent: exports_external.string().min(1),
33862
34926
  task: exports_external.string().min(1),
34927
+ workingDirectory: exports_external.string().min(1).optional(),
33863
34928
  role: exports_external.string().min(1).optional(),
33864
34929
  groupId: exports_external.string().min(1).optional()
33865
34930
  }).strict(),
@@ -34013,6 +35078,22 @@ function buildWeacpxMcpToolRegistry(input) {
34013
35078
  return createSuccessResult(`任务「${task.taskId}」已请求取消。`, task);
34014
35079
  })
34015
35080
  },
35081
+ {
35082
+ name: "task_wait",
35083
+ description: "Wait for an orchestration task to finish or require attention using a bounded timeout.",
35084
+ inputSchema: exports_external.object({
35085
+ taskId: exports_external.string().min(1),
35086
+ timeoutMs: exports_external.number().int().min(0).max(MAX_TASK_WAIT_TIMEOUT_MS).optional(),
35087
+ pollIntervalMs: exports_external.number().int().min(1).max(MAX_TASK_WAIT_POLL_INTERVAL_MS).optional()
35088
+ }).strict(),
35089
+ handler: async (args) => await asToolResult(async () => {
35090
+ const result = await transport.waitTask({
35091
+ coordinatorSession,
35092
+ ...args
35093
+ });
35094
+ return createSuccessResult(renderTaskWaitResult(result), result);
35095
+ })
35096
+ },
34016
35097
  {
34017
35098
  name: "worker_raise_question",
34018
35099
  description: "Raise a blocker question for the current bound session.",
@@ -34129,6 +35210,21 @@ async function asToolResult(action) {
34129
35210
  return createErrorResult(formatToolError(error2));
34130
35211
  }
34131
35212
  }
35213
+ function renderTaskWaitResult(result) {
35214
+ if (result.status === "not_found") {
35215
+ return "Task not found.";
35216
+ }
35217
+ if (!result.task) {
35218
+ return `Task wait ${result.status.replace("_", " ")}; current state is unavailable.`;
35219
+ }
35220
+ if (result.status === "timeout") {
35221
+ return `Task ${result.task.taskId} wait timed out; current state is ${result.task.status}.`;
35222
+ }
35223
+ if (result.status === "attention_required") {
35224
+ return `Task ${result.task.taskId} requires attention; current state is ${result.task.status}.`;
35225
+ }
35226
+ return `Task ${result.task.taskId} reached terminal state ${result.task.status}.`;
35227
+ }
34132
35228
  function createSuccessResult(text, structuredContent) {
34133
35229
  return {
34134
35230
  content: [{ type: "text", text }],
@@ -34151,6 +35247,7 @@ function formatToolError(error2) {
34151
35247
 
34152
35248
  // src/orchestration/orchestration-client.ts
34153
35249
  init_orchestration_ipc();
35250
+ init_task_wait_timeouts();
34154
35251
  import { randomUUID } from "node:crypto";
34155
35252
  import { createConnection } from "node:net";
34156
35253
 
@@ -34163,17 +35260,20 @@ class OrchestrationClient {
34163
35260
  this.createId = deps.createId ?? (() => randomUUID());
34164
35261
  this.timeoutMs = deps.timeoutMs ?? 30000;
34165
35262
  }
35263
+ async registerExternalCoordinator(input) {
35264
+ return await this.request("coordinator.register_external", input);
35265
+ }
34166
35266
  async delegateRequest(input) {
34167
35267
  return await this.request("delegate.request", input);
34168
35268
  }
34169
- async getTask(taskId) {
34170
- return await this.request("task.get", { taskId });
34171
- }
34172
35269
  async getTaskForCoordinator(input) {
34173
35270
  return await this.request("task.get", input);
34174
35271
  }
34175
35272
  async listTasks(filter) {
34176
- return await this.request("task.list", filter ? { filter } : {});
35273
+ return await this.request("task.list", { filter });
35274
+ }
35275
+ async waitTask(input) {
35276
+ return await this.request("task.wait", input, getWaitRequestTimeoutMs(input.timeoutMs, this.timeoutMs));
34177
35277
  }
34178
35278
  async approveTask(input) {
34179
35279
  return await this.request("task.approve", input);
@@ -34220,7 +35320,7 @@ class OrchestrationClient {
34220
35320
  async cancelGroup(input) {
34221
35321
  return await this.request("group.cancel", input);
34222
35322
  }
34223
- async request(method, params) {
35323
+ async request(method, params, timeoutMs = this.timeoutMs) {
34224
35324
  const id = this.createId();
34225
35325
  return await new Promise((resolve, reject) => {
34226
35326
  const socket = createConnection(this.endpoint.path);
@@ -34239,8 +35339,8 @@ class OrchestrationClient {
34239
35339
  reject(error2);
34240
35340
  };
34241
35341
  timer = setTimeout(() => {
34242
- fail(new Error(`orchestration RPC timeout after ${this.timeoutMs}ms: ${method}`));
34243
- }, this.timeoutMs);
35342
+ fail(new Error(`orchestration RPC timeout after ${timeoutMs}ms: ${method}`));
35343
+ }, timeoutMs);
34244
35344
  socket.setEncoding("utf8");
34245
35345
  socket.once("error", fail);
34246
35346
  socket.once("connect", () => {
@@ -34286,6 +35386,11 @@ class OrchestrationClient {
34286
35386
  });
34287
35387
  }
34288
35388
  }
35389
+ function getWaitRequestTimeoutMs(waitTimeoutMs, defaultTimeoutMs) {
35390
+ const requestedWaitTimeoutMs = waitTimeoutMs === undefined ? undefined : Number.isFinite(waitTimeoutMs) ? waitTimeoutMs : 0;
35391
+ const boundedWaitTimeoutMs = Math.min(Math.max(Math.floor(requestedWaitTimeoutMs ?? DEFAULT_TASK_WAIT_TIMEOUT_MS), 0), MAX_TASK_WAIT_TIMEOUT_MS);
35392
+ return Math.max(defaultTimeoutMs, boundedWaitTimeoutMs + TASK_WAIT_RPC_TIMEOUT_PADDING_MS);
35393
+ }
34289
35394
 
34290
35395
  // src/mcp/weacpx-mcp-transport.ts
34291
35396
  function createOrchestrationTransport(endpoint, deps = {}) {
@@ -34295,6 +35400,7 @@ function createOrchestrationTransport(endpoint, deps = {}) {
34295
35400
  sourceHandle: input.sourceHandle ?? input.coordinatorSession,
34296
35401
  targetAgent: input.targetAgent,
34297
35402
  task: input.task,
35403
+ ...input.workingDirectory ? { cwd: input.workingDirectory } : {},
34298
35404
  ...input.role ? { role: input.role } : {},
34299
35405
  ...input.groupId ? { groupId: input.groupId } : {}
34300
35406
  }),
@@ -34313,6 +35419,7 @@ function createOrchestrationTransport(endpoint, deps = {}) {
34313
35419
  approveTask: async (input) => await client.approveTask(input),
34314
35420
  rejectTask: async (input) => await client.rejectTask(input),
34315
35421
  cancelTask: async (input) => await client.cancelTaskForCoordinator(input),
35422
+ waitTask: async (input) => await client.waitTask(input),
34316
35423
  workerRaiseQuestion: async (input) => {
34317
35424
  const sourceHandle = input.sourceHandle.trim();
34318
35425
  if (sourceHandle.length === 0) {
@@ -34343,16 +35450,39 @@ function createWeacpxMcpServer(options) {
34343
35450
  tools: {}
34344
35451
  }
34345
35452
  });
34346
- const tools = buildWeacpxMcpToolRegistry(options);
34347
- const toolMap = new Map(tools.map((tool) => [tool.name, tool]));
34348
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
34349
- tools: tools.map((tool) => ({
34350
- name: tool.name,
34351
- description: tool.description,
34352
- inputSchema: normalizeInputSchemaJson(zodToJsonSchema(tool.inputSchema))
34353
- }))
34354
- }));
35453
+ let toolState = null;
35454
+ let toolStatePromise = null;
35455
+ async function getToolState() {
35456
+ if (toolState) {
35457
+ return toolState;
35458
+ }
35459
+ if (toolStatePromise) {
35460
+ return await toolStatePromise;
35461
+ }
35462
+ toolStatePromise = resolveMcpIdentity(server, options).then((identity) => {
35463
+ toolState = buildToolState({
35464
+ transport: options.transport,
35465
+ coordinatorSession: identity.coordinatorSession,
35466
+ ...identity.sourceHandle ? { sourceHandle: identity.sourceHandle } : {}
35467
+ });
35468
+ return toolState;
35469
+ }).finally(() => {
35470
+ toolStatePromise = null;
35471
+ });
35472
+ return await toolStatePromise;
35473
+ }
35474
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
35475
+ const tools = (await getToolState()).tools;
35476
+ return {
35477
+ tools: tools.map((tool) => ({
35478
+ name: tool.name,
35479
+ description: tool.description,
35480
+ inputSchema: normalizeInputSchemaJson(zodToJsonSchema(tool.inputSchema))
35481
+ }))
35482
+ };
35483
+ });
34355
35484
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
35485
+ const toolMap = (await getToolState()).toolMap;
34356
35486
  const tool = toolMap.get(request.params.name);
34357
35487
  if (!tool) {
34358
35488
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
@@ -34365,12 +35495,35 @@ function createWeacpxMcpServer(options) {
34365
35495
  });
34366
35496
  return server;
34367
35497
  }
35498
+ function buildToolState(options) {
35499
+ const tools = buildWeacpxMcpToolRegistry(options);
35500
+ return {
35501
+ tools,
35502
+ toolMap: new Map(tools.map((tool) => [tool.name, tool]))
35503
+ };
35504
+ }
35505
+ async function resolveMcpIdentity(server, options) {
35506
+ if (options.resolveIdentity) {
35507
+ return await options.resolveIdentity({
35508
+ clientName: server.getClientVersion()?.name,
35509
+ listRoots: async () => (await server.listRoots()).roots
35510
+ });
35511
+ }
35512
+ if (options.coordinatorSession) {
35513
+ return {
35514
+ coordinatorSession: options.coordinatorSession,
35515
+ ...options.sourceHandle ? { sourceHandle: options.sourceHandle } : {}
35516
+ };
35517
+ }
35518
+ throw new McpError(ErrorCode.InvalidRequest, "weacpx MCP identity is not configured; run through `weacpx mcp-stdio` or provide --coordinator-session");
35519
+ }
34368
35520
  async function runWeacpxMcpServer(options) {
34369
35521
  const transport = createOrchestrationTransport(options.endpoint ?? resolveDefaultOrchestrationEndpoint(process.env, process.platform));
34370
35522
  const server = createWeacpxMcpServer({
34371
35523
  transport,
34372
- coordinatorSession: options.coordinatorSession,
34373
- ...options.sourceHandle ? { sourceHandle: options.sourceHandle } : {}
35524
+ ...options.coordinatorSession ? { coordinatorSession: options.coordinatorSession } : {},
35525
+ ...options.sourceHandle ? { sourceHandle: options.sourceHandle } : {},
35526
+ ...options.resolveIdentity ? { resolveIdentity: options.resolveIdentity } : {}
34374
35527
  });
34375
35528
  const stdio = new StdioServerTransport(stdin, stdout);
34376
35529
  await server.connect(stdio);
@@ -34387,12 +35540,55 @@ function formatZodError(error2) {
34387
35540
  }).join("; ");
34388
35541
  }
34389
35542
 
35543
+ // src/mcp/infer-coordinator-identity.ts
35544
+ init_workspace_path();
35545
+ function inferExternalCoordinatorSession(input) {
35546
+ const suffix = input.workspace?.trim() || input.instanceId?.trim() || "instance";
35547
+ return `external_${sanitizeMcpClientName(input.clientName)}:${suffix}`;
35548
+ }
35549
+ function sanitizeMcpClientName(input) {
35550
+ const normalized = (input ?? "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
35551
+ return normalized.length > 0 ? normalized : "mcp-host";
35552
+ }
35553
+
35554
+ // src/mcp/parse-coordinator-workspace.ts
35555
+ function parseCoordinatorWorkspace(args, env = process.env) {
35556
+ let fromFlag = null;
35557
+ for (let index = 0;index < args.length; index += 1) {
35558
+ if (args[index] === "--workspace") {
35559
+ const value = args[index + 1];
35560
+ if (value === undefined) {
35561
+ throw new Error("--workspace requires a non-empty value");
35562
+ }
35563
+ const trimmedValue = value.trim();
35564
+ if (trimmedValue.length === 0 || trimmedValue.startsWith("-")) {
35565
+ throw new Error("--workspace requires a non-empty value");
35566
+ }
35567
+ fromFlag = value;
35568
+ }
35569
+ }
35570
+ const trimmedFlag = fromFlag?.trim();
35571
+ if (trimmedFlag && trimmedFlag.length > 0) {
35572
+ return trimmedFlag;
35573
+ }
35574
+ const trimmedEnv = env.WEACPX_COORDINATOR_WORKSPACE?.trim();
35575
+ return trimmedEnv && trimmedEnv.length > 0 ? trimmedEnv : null;
35576
+ }
35577
+
34390
35578
  // src/mcp/parse-coordinator-session.ts
34391
35579
  function parseCoordinatorSession(args, env = process.env) {
34392
35580
  let fromFlag = null;
34393
- for (let index = 0;index < args.length - 1; index += 1) {
35581
+ for (let index = 0;index < args.length; index += 1) {
34394
35582
  if (args[index] === "--coordinator-session") {
34395
- fromFlag = args[index + 1] ?? null;
35583
+ const value = args[index + 1];
35584
+ if (value === undefined) {
35585
+ throw new Error("--coordinator-session requires a non-empty value");
35586
+ }
35587
+ const trimmedValue = value.trim();
35588
+ if (trimmedValue.length === 0 || trimmedValue.startsWith("-")) {
35589
+ throw new Error("--coordinator-session requires a non-empty value");
35590
+ }
35591
+ fromFlag = value;
34396
35592
  }
34397
35593
  }
34398
35594
  const trimmedFlag = fromFlag?.trim();
@@ -34406,9 +35602,17 @@ function parseCoordinatorSession(args, env = process.env) {
34406
35602
  // src/mcp/parse-source-handle.ts
34407
35603
  function parseSourceHandle(args, env = process.env) {
34408
35604
  let fromFlag = null;
34409
- for (let index = 0;index < args.length - 1; index += 1) {
35605
+ for (let index = 0;index < args.length; index += 1) {
34410
35606
  if (args[index] === "--source-handle") {
34411
- fromFlag = args[index + 1] ?? null;
35607
+ const value = args[index + 1];
35608
+ if (value === undefined) {
35609
+ throw new Error("--source-handle requires a non-empty value");
35610
+ }
35611
+ const trimmedValue = value.trim();
35612
+ if (trimmedValue.length === 0 || trimmedValue.startsWith("-")) {
35613
+ throw new Error("--source-handle requires a non-empty value");
35614
+ }
35615
+ fromFlag = value;
34412
35616
  }
34413
35617
  }
34414
35618
  const trimmedFlag = fromFlag?.trim();
@@ -34421,8 +35625,99 @@ function parseSourceHandle(args, env = process.env) {
34421
35625
 
34422
35626
  // src/cli.ts
34423
35627
  init_workspace_path();
35628
+ init_state_store();
34424
35629
  init_version();
34425
35630
  init_consumer_lock();
35631
+ async function prepareMcpCoordinatorStartup(input) {
35632
+ const coordinatorSession = input.coordinatorSession.trim();
35633
+ const existingSession = Object.values(input.state.sessions).find((session) => session.transport_session === coordinatorSession);
35634
+ const workspace = input.workspace?.trim();
35635
+ if (workspace) {
35636
+ if (existingSession) {
35637
+ throw new Error(`coordinatorSession "${coordinatorSession}" conflicts with an existing logical session`);
35638
+ }
35639
+ const existingExternalCoordinator2 = input.state.orchestration?.externalCoordinators?.[coordinatorSession];
35640
+ if (existingExternalCoordinator2?.workspace && existingExternalCoordinator2.workspace !== workspace) {
35641
+ throw new Error(`coordinatorSession "${coordinatorSession}" is already bound to workspace "${existingExternalCoordinator2.workspace}"; use a new coordinator session for workspace "${workspace}"`);
35642
+ }
35643
+ if (!input.config.workspaces[workspace]) {
35644
+ if (existingExternalCoordinator2?.workspace === workspace) {
35645
+ throw new Error(`workspace "${workspace}" is not configured for coordinatorSession "${coordinatorSession}"; restore that workspace config or use a new coordinator session for a different workspace`);
35646
+ }
35647
+ throw new Error(`workspace "${workspace}" is not configured`);
35648
+ }
35649
+ await registerExternalCoordinatorOrThrow(input.client, { coordinatorSession, workspace });
35650
+ return { kind: "external-coordinator", workspace };
35651
+ }
35652
+ if (existingSession) {
35653
+ return { kind: "existing-session" };
35654
+ }
35655
+ const existingExternalCoordinator = input.state.orchestration?.externalCoordinators?.[coordinatorSession];
35656
+ if (existingExternalCoordinator) {
35657
+ if (existingExternalCoordinator.workspace && !input.config.workspaces[existingExternalCoordinator.workspace]) {
35658
+ throw new Error(`workspace "${existingExternalCoordinator.workspace}" is not configured for coordinatorSession "${coordinatorSession}"; restore that workspace config or use a new coordinator session for a different workspace`);
35659
+ }
35660
+ await registerExternalCoordinatorOrThrow(input.client, {
35661
+ coordinatorSession,
35662
+ ...existingExternalCoordinator.workspace ? { workspace: existingExternalCoordinator.workspace } : {}
35663
+ });
35664
+ return {
35665
+ kind: "external-coordinator",
35666
+ ...existingExternalCoordinator.workspace ? { workspace: existingExternalCoordinator.workspace } : {}
35667
+ };
35668
+ }
35669
+ await registerExternalCoordinatorOrThrow(input.client, { coordinatorSession });
35670
+ return { kind: "external-coordinator" };
35671
+ }
35672
+ function createMcpStdioIdentityResolver(input) {
35673
+ const instanceId = randomUUID4().slice(0, 8);
35674
+ return async (context) => {
35675
+ const parsedCoordinatorSession = input.parsedCoordinatorSession?.trim() || null;
35676
+ const workspace = input.workspace?.trim() || null;
35677
+ const sourceHandle = input.sourceHandle?.trim() || null;
35678
+ const resolvedWorkspace = workspace;
35679
+ const resolvedCoordinatorSession = parsedCoordinatorSession ?? inferExternalCoordinatorSession({
35680
+ clientName: context.clientName,
35681
+ ...resolvedWorkspace ? { workspace: resolvedWorkspace } : { instanceId }
35682
+ });
35683
+ await prepareMcpCoordinatorStartup({
35684
+ coordinatorSession: resolvedCoordinatorSession,
35685
+ ...resolvedWorkspace ? { workspace: resolvedWorkspace } : {},
35686
+ config: input.config,
35687
+ state: input.state,
35688
+ client: input.client
35689
+ });
35690
+ return {
35691
+ coordinatorSession: resolvedCoordinatorSession,
35692
+ ...sourceHandle ? { sourceHandle } : {}
35693
+ };
35694
+ };
35695
+ }
35696
+ async function registerExternalCoordinatorOrThrow(client, input) {
35697
+ try {
35698
+ await client.registerExternalCoordinator(input);
35699
+ } catch (error2) {
35700
+ if (isUnavailableOrchestrationIpcError(error2)) {
35701
+ throw new Error("weacpx daemon orchestration IPC is unavailable; run `weacpx start` and check `weacpx status`");
35702
+ }
35703
+ if (input.workspace && isDaemonWorkspaceNotConfiguredError(error2, input.workspace)) {
35704
+ throw new Error(`workspace "${input.workspace}" is not configured in the running daemon; restart it with \`weacpx stop && weacpx start\``);
35705
+ }
35706
+ throw error2;
35707
+ }
35708
+ }
35709
+ function isDaemonWorkspaceNotConfiguredError(error2, workspace) {
35710
+ const message = error2 instanceof Error ? error2.message : String(error2);
35711
+ return message === `workspace "${workspace}" is not configured`;
35712
+ }
35713
+ function isUnavailableOrchestrationIpcError(error2) {
35714
+ const code = typeof error2 === "object" && error2 !== null && "code" in error2 ? String(error2.code) : "";
35715
+ if (code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET") {
35716
+ return true;
35717
+ }
35718
+ const message = error2 instanceof Error ? error2.message : String(error2);
35719
+ return /connect (ENOENT|ECONNREFUSED|ECONNRESET)\b/.test(message);
35720
+ }
34426
35721
  var HELP_LINES = [
34427
35722
  "用法:",
34428
35723
  "weacpx login - 微信登录",
@@ -34434,7 +35729,7 @@ var HELP_LINES = [
34434
35729
  "weacpx doctor - 运行诊断",
34435
35730
  "weacpx version - 查看版本",
34436
35731
  "weacpx workspace list|add|rm - 管理本机工作区(别名:ws)",
34437
- "weacpx mcp-stdio --coordinator-session <session> [--source-handle <handle>] - 启动 MCP stdio 服务"
35732
+ "weacpx mcp-stdio [--coordinator-session <session>] [--source-handle <handle>] [--workspace <name>] - 启动 MCP stdio 服务"
34438
35733
  ];
34439
35734
  async function runCli(args, deps = {}) {
34440
35735
  const command = args[0];
@@ -34652,17 +35947,41 @@ async function defaultDoctor(options) {
34652
35947
  return await main4(options);
34653
35948
  }
34654
35949
  async function defaultMcpStdio(args, deps = {}) {
34655
- const coordinatorSession = parseCoordinatorSession(args, process.env);
34656
- const sourceHandle = parseSourceHandle(args, process.env);
34657
- if (!coordinatorSession) {
34658
- (deps.stderr ?? ((text) => process.stderr.write(text)))(`weacpx mcp-stdio 需要 --coordinator-session <handle> 或 WEACPX_COORDINATOR_SESSION 环境变量
35950
+ let coordinatorSession;
35951
+ let sourceHandle;
35952
+ let endpoint;
35953
+ let identityResolver;
35954
+ try {
35955
+ const parsedCoordinatorSession = parseCoordinatorSession(args, process.env);
35956
+ sourceHandle = parseSourceHandle(args, process.env);
35957
+ const workspace = parseCoordinatorWorkspace(args, process.env);
35958
+ endpoint = resolveDefaultOrchestrationEndpoint(process.env, process.platform);
35959
+ const client = new OrchestrationClient(endpoint);
35960
+ const runtimePaths = (await init_main().then(() => exports_main)).resolveRuntimePaths();
35961
+ await ensureConfigExists(runtimePaths.configPath);
35962
+ const config2 = await loadConfig(runtimePaths.configPath);
35963
+ const state = await new StateStore(runtimePaths.statePath).load();
35964
+ const resolveIdentity = createMcpStdioIdentityResolver({
35965
+ parsedCoordinatorSession,
35966
+ sourceHandle,
35967
+ workspace,
35968
+ config: config2,
35969
+ state,
35970
+ client
35971
+ });
35972
+ const eagerIdentity = parsedCoordinatorSession && workspace ? await resolveIdentity({ clientName: undefined, listRoots: async () => [] }) : null;
35973
+ coordinatorSession = eagerIdentity?.coordinatorSession ?? "";
35974
+ identityResolver = eagerIdentity ? undefined : resolveIdentity;
35975
+ } catch (error2) {
35976
+ (deps.stderr ?? ((text) => process.stderr.write(text)))(`${error2 instanceof Error ? error2.message : String(error2)}
34659
35977
  `);
34660
35978
  return 2;
34661
35979
  }
34662
35980
  await runWeacpxMcpServer({
34663
- endpoint: resolveDefaultOrchestrationEndpoint(process.env, process.platform),
34664
- coordinatorSession,
34665
- ...sourceHandle ? { sourceHandle } : {}
35981
+ endpoint,
35982
+ ...coordinatorSession ? { coordinatorSession } : {},
35983
+ ...sourceHandle ? { sourceHandle } : {},
35984
+ ...identityResolver ? { resolveIdentity: identityResolver } : {}
34666
35985
  });
34667
35986
  return 0;
34668
35987
  }
@@ -34727,5 +36046,7 @@ if (__require.main == __require.module) {
34727
36046
  process.exitCode = await runCli(process.argv.slice(2));
34728
36047
  }
34729
36048
  export {
34730
- runCli
36049
+ runCli,
36050
+ prepareMcpCoordinatorStartup,
36051
+ createMcpStdioIdentityResolver
34731
36052
  };