evolcore 0.0.22 → 0.0.24

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.
Files changed (117) hide show
  1. package/CHANGELOG.md +74 -14
  2. package/README.md +4 -0
  3. package/bin/codex-managed-acl.ps1 +160 -0
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +384 -95
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +30 -2
  9. package/dist/agents/claude-runner.js +179 -240
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +369 -0
  14. package/dist/agents/codex-runner.js +839 -192
  15. package/dist/agents/ecagent-runner.js +27 -34
  16. package/dist/agents/gemini-runner.js +62 -8
  17. package/dist/agents/runner-types.js +1 -1
  18. package/dist/aun/aid/managed-operation.js +28 -6
  19. package/dist/aun/aid/store.js +43 -0
  20. package/dist/aun/menu-client.js +272 -0
  21. package/dist/aun/msg/group.js +65 -4
  22. package/dist/aun/msg/index.js +1 -1
  23. package/dist/aun/msg/managed-operation.js +57 -5
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +7 -1
  26. package/dist/channels/aun.js +178 -77
  27. package/dist/channels/daemon.js +7 -1
  28. package/dist/channels/dingtalk.js +1 -0
  29. package/dist/channels/feishu.js +10 -4
  30. package/dist/channels/wechat.js +81 -10
  31. package/dist/channels/wecom.js +2 -1
  32. package/dist/cli/agent-command.js +29 -3
  33. package/dist/cli/agent-create.js +538 -0
  34. package/dist/cli/agent.js +26 -675
  35. package/dist/cli/aun-commands.js +108 -24
  36. package/dist/cli/command-log.js +21 -2
  37. package/dist/cli/ctl-command.js +2 -0
  38. package/dist/cli/daemon-commands.js +289 -61
  39. package/dist/cli/index.js +22 -2
  40. package/dist/cli/init.js +2 -10
  41. package/dist/cli/managed-command-guard.js +3 -0
  42. package/dist/cli/menu-client.js +95 -0
  43. package/dist/cli/model.js +23 -8
  44. package/dist/cli/restart-monitor.js +432 -8
  45. package/dist/cli/stats.js +110 -0
  46. package/dist/cli/task-context.js +12 -2
  47. package/dist/cli/trigger-command.js +9 -1
  48. package/dist/config/builtin-roles.js +2 -0
  49. package/dist/config/schema-migration.js +14 -2
  50. package/dist/core/auth/agent-delegation.js +10 -7
  51. package/dist/core/auth/auth-gateway.js +3 -2
  52. package/dist/core/auth/authorization-audit.js +75 -18
  53. package/dist/core/auth/operation-authorizer.js +63 -3
  54. package/dist/core/auth/operation-catalog.js +14 -2
  55. package/dist/core/auth/remote-access-store.js +136 -0
  56. package/dist/core/baseagent-loader.js +4 -4
  57. package/dist/core/bootstrap-messages.js +39 -18
  58. package/dist/core/bootstrap-service.js +32 -26
  59. package/dist/core/capability/capability-manager.js +8 -4
  60. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  61. package/dist/core/command/agent-control.js +7 -2
  62. package/dist/core/command/command-handler.js +128 -30
  63. package/dist/core/command/daemon-diagnostics.js +448 -0
  64. package/dist/core/command/menu-catalog.js +6 -1
  65. package/dist/core/command/menu-handler.js +410 -98
  66. package/dist/core/command/menu-protocol.js +123 -0
  67. package/dist/core/command/slash-gate.js +4 -6
  68. package/dist/core/command/slash-handler.js +199 -70
  69. package/dist/core/data-migration.js +5 -15
  70. package/dist/core/event-catalog.js +3 -0
  71. package/dist/core/handoff/runtime.js +3 -1
  72. package/dist/core/handoff/store.js +7 -1
  73. package/dist/core/message/im-renderer.js +13 -6
  74. package/dist/core/message/message-bridge.js +5 -36
  75. package/dist/core/message/message-queue.js +20 -15
  76. package/dist/core/message/message-utils.js +6 -3
  77. package/dist/core/message/response-engine.js +147 -94
  78. package/dist/core/model/catalog-service.js +310 -0
  79. package/dist/core/model/model-catalog.js +134 -334
  80. package/dist/core/model/model-diagnostics.js +4 -5
  81. package/dist/core/model/model-fallback.js +9 -16
  82. package/dist/core/model/model-permission.js +5 -2
  83. package/dist/core/permission/approval-gateway.js +183 -32
  84. package/dist/core/permission/protected-paths.js +63 -4
  85. package/dist/core/permission/sandbox-runtime.js +4 -2
  86. package/dist/core/permission/tool-policy.js +257 -31
  87. package/dist/core/runtime-lock.js +13 -8
  88. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  89. package/dist/core/session/session-manager.js +49 -13
  90. package/dist/core/session/session-renew.js +1 -0
  91. package/dist/index.js +214 -57
  92. package/dist/ipc.js +191 -102
  93. package/dist/paths.js +23 -1
  94. package/dist/stats/billing.js +18 -4
  95. package/dist/trigger/manager.js +3 -1
  96. package/dist/trigger/parser.js +77 -13
  97. package/dist/trigger/patch.js +104 -1
  98. package/dist/utils/atomic-write.js +26 -2
  99. package/dist/utils/cross-platform.js +436 -1
  100. package/dist/utils/ecweb-supervisor.js +11 -2
  101. package/dist/utils/ecweb-utils.js +8 -26
  102. package/dist/utils/error-utils.js +16 -2
  103. package/dist/utils/model-prices.jsonl +23 -20
  104. package/dist/utils/npm-ops.js +70 -15
  105. package/dist/utils/windows-autostart.js +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -0
  107. package/kits/docs/evolcore/agent-create.md +13 -0
  108. package/kits/docs/evolcore/group-collaboration.md +7 -2
  109. package/kits/docs/evolcore/menu-client.md +34 -0
  110. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  111. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  112. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  113. package/kits/templates/roles/admin.json +2 -0
  114. package/kits/templates/roles/visitor.json +1 -0
  115. package/kits/templates/system-fragments/bootstrap.md +8 -1
  116. package/package.json +16 -4
  117. package/dist/utils/codex-cli.js +0 -43
@@ -1,5 +1,4 @@
1
1
  import { hasModelSwitcher, hasPermissionController } from '../../agents/runner-types.js';
2
- import { getCodexEfforts } from '../../agents/codex-runner.js';
3
2
  import { normalizeBaseagent } from '../../agents/baseagent.js';
4
3
  import { buildEnvelope, isDeliveryTargetForChannel } from '../message/message-utils.js';
5
4
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
@@ -10,13 +9,14 @@ import path from 'path';
10
9
  import fs from 'fs';
11
10
  import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../../utils/npm-ops.js';
12
11
  import { loadDaemonConfig } from '../../config-store.js';
12
+ import { remoteAccessService } from '../auth/remote-access-store.js';
13
13
  import { read as cfgRead, resolveEffective, routeFieldPath, write as cfgWrite, } from '../../config/config-manager.js';
14
14
  import { execAgentAction } from './agent-control.js';
15
15
  import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
16
16
  import { authorizationNextStep, formatAuthorizationDenial } from '../auth/authorization-denial.js';
17
17
  import { resolvePermissionMode, writeScope } from '../model/config-scope.js';
18
18
  import { formatPeerKey } from '../relation/peer-identity.js';
19
- import { modelMatches } from '../model/model-catalog.js';
19
+ import { modelMatches, getCatalog, assertCatalogSelectable, resolveCatalogModel } from '../model/model-catalog.js';
20
20
  import { formatModelCheck, runModelCheck } from '../model/model-diagnostics.js';
21
21
  import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
22
22
  import { resolveRuntimeStringField } from '../role/runtime-policy.js';
@@ -25,10 +25,11 @@ import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-m
25
25
  import { normalizePermissionMode as normalizePermissionModeContract, PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
26
26
  import { isManagementRole } from '../../config/builtin-roles.js';
27
27
  import { isSystemControlChannel } from '../system-channels.js';
28
- import { spawnDetachedNode } from '../../utils/cross-platform.js';
28
+ import { launchMonitorAndConfirm, resolveMonitorLaunch, cleanupMonitorStage } from '../../utils/cross-platform.js';
29
+ import { scanInstances } from '../../utils/instance-registry.js';
29
30
  import { inspectDataMigrationRequirement } from '../data-migration.js';
30
31
  import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, probeSessionActivity, } from './slash-gate.js';
31
- const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
32
+ const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
32
33
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
33
34
  function defaultSlashChatmode(field) {
34
35
  return field === 'private' ? 'interactive' : 'proactive';
@@ -127,12 +128,8 @@ function resolveSlashRelationTarget(params) {
127
128
  role: params.role,
128
129
  };
129
130
  }
130
- function getAvailableEfforts(agent, model) {
131
- if (agent.name === 'claude')
132
- return allEfforts;
133
- if (agent.name === 'codex')
134
- return getCodexEfforts(model);
135
- return [];
131
+ async function getAvailableEfforts(agent, model) {
132
+ return (await agent.getAvailableEfforts?.(model) ?? []);
136
133
  }
137
134
  function modelDisplayLabel(agent, model) {
138
135
  const full = agent.resolveModelId?.(model);
@@ -487,7 +484,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
487
484
  const isProcessLevelSlash = normalizedContent === '/restart'
488
485
  || normalizedContent === '/upgrade'
489
486
  || normalizedContent === '/reload'
490
- || normalizedContent.startsWith('/reload ');
487
+ || normalizedContent.startsWith('/reload ')
488
+ || normalizedContent === '/pair';
491
489
  const roleGuard = guardRoleCommand(normalizedContent, activeChatType, isAdmin || (isDaemonOwner && isProcessLevelSlash));
492
490
  if (roleGuard)
493
491
  return roleGuard;
@@ -635,7 +633,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
635
633
  const appendProcessCommands = (lines) => {
636
634
  if (!isDaemonOwner)
637
635
  return;
638
- lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置', ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)');
636
+ lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置', ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)', ' /pair - 获取远程控制配对码');
639
637
  };
640
638
  if (!isAdmin && activeChatType === 'group') {
641
639
  const lines = [
@@ -775,6 +773,23 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
775
773
  ];
776
774
  return { kind: 'command.result', text: lines.join('\n') };
777
775
  }
776
+ // /pair is also recognized on an Agent channel so it cannot fall through
777
+ // into model input. The daemon control channel has its own fast path; this
778
+ // branch preserves the same daemon-owner boundary for ordinary AUN chats.
779
+ if (normalizedContent === '/pair') {
780
+ if (!isDaemonOwner) {
781
+ return {
782
+ kind: 'command.error',
783
+ text: '❌ /pair 仅限 daemon-owner 使用;请向 daemon 控制 AID 发送 /pair。',
784
+ };
785
+ }
786
+ const pair = remoteAccessService.getPairingCode(userId);
787
+ const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
788
+ return {
789
+ kind: 'command.result',
790
+ text: `远程控制配对码:${pair.code}(约 ${mins} 分钟内有效)\n可用于 ECWeb 登录或远程 Menu 客户端授权`,
791
+ };
792
+ }
778
793
  // /evolhelp 命令:返回 JSON 格式的命令列表(供程序解析)
779
794
  if (normalizedContent === '/evolhelp') {
780
795
  const cmds = [];
@@ -835,6 +850,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
835
850
  cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['daemon-owner'] });
836
851
  cmds.push({ command: '/upgrade', description: '检查版本更新', category: '运维', roles: ['daemon-owner'] });
837
852
  cmds.push({ command: '/reload', args: '[aid]', description: '热重载 Agent 配置', category: '运维', roles: ['daemon-owner'] });
853
+ cmds.push({ command: '/pair', description: '获取远程控制配对码', category: '运维', roles: ['daemon-owner'] });
838
854
  }
839
855
  if (!isDaemonOwner && isAdmin) {
840
856
  cmds.push({ command: '/reload', description: '热重载当前 Agent 配置', category: '运维', roles: ['admin', 'owner'] });
@@ -1048,11 +1064,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1048
1064
  const activeBaseagent = owningAgent?.baseagent || this.parseDefaultBaseagent();
1049
1065
  // 尝试发送 CommandCard 卡片
1050
1066
  if (this.interactionRouter && available.length > 1) {
1067
+ const baseagentSession = await getExistingSessionForCommand();
1051
1068
  const interaction = {
1052
1069
  type: 'interaction',
1053
1070
  id: `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1054
1071
  channelId,
1055
- sessionId: activeSession?.id || `agent-${Date.now()}`,
1072
+ sessionId: baseagentSession?.id || `agent-${Date.now()}`,
1056
1073
  initiatorId: userId,
1057
1074
  kind: {
1058
1075
  kind: 'command-card',
@@ -1065,7 +1082,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1065
1082
  })),
1066
1083
  },
1067
1084
  };
1068
- const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
1085
+ const replyCtx = baseagentSession ? this.getReplyContext(baseagentSession) : replyContext;
1069
1086
  const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
1070
1087
  if (cardResult === null)
1071
1088
  return null;
@@ -1085,29 +1102,79 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1085
1102
  if (!owningAgent) {
1086
1103
  return { kind: 'command.error', text: '❌ 当前 channel 无绑定 agent,无法设置 active_baseagent' };
1087
1104
  }
1088
- await interruptPausedSessionBeforeReplacement(await getExistingSessionForCommand());
1089
- const previousDefaultBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
1105
+ const currentSession = await getExistingSessionForCommand();
1106
+ const previousBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
1107
+ if (currentSession?.threadId) {
1108
+ const rotation = await withIdleTopicBackend(currentSession.id, currentSession.threadId, async (current) => {
1109
+ try {
1110
+ owningAgent.setActiveBaseagent(args);
1111
+ }
1112
+ catch (error) {
1113
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
1114
+ }
1115
+ try {
1116
+ const result = await this.sessionManager.rotateTopicBackend({
1117
+ sessionId: current.id,
1118
+ expectedThreadId: current.threadId,
1119
+ baseagent: args,
1120
+ });
1121
+ return { ok: true, result };
1122
+ }
1123
+ catch (error) {
1124
+ try {
1125
+ owningAgent.setActiveBaseagent(previousBaseagent);
1126
+ }
1127
+ catch { }
1128
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
1129
+ }
1130
+ }, currentSession);
1131
+ if (!rotation.ok) {
1132
+ if (rotation.reason === 'busy')
1133
+ return { kind: 'command.error', text: '⚠️ 当前话题会话忙碌,无法切换 baseagent,请稍后再试' };
1134
+ if (rotation.reason === 'unknown')
1135
+ return { kind: 'command.error', text: '⚠️ 当前话题会话状态未知,无法安全切换 baseagent,请稍后再试' };
1136
+ return { kind: 'command.error', text: `❌ baseagent 切换失败:${rotation.reason === 'missing' ? '当前话题会话不存在' : '状态未知'}` };
1137
+ }
1138
+ if (!rotation.value.ok)
1139
+ return { kind: 'command.error', text: `❌ baseagent 切换失败:${rotation.value.error}` };
1140
+ this.eventBus.publish({ type: 'agent:baseagent-changed', aid: owningAgent.aid, baseagent: args, previousBaseagent, scope: 'agent', timestamp: Date.now() });
1141
+ return {
1142
+ kind: 'system.notice',
1143
+ text: `✓ 当前话题已切换 Baseagent: ${previousBaseagent} → ${args}\n 话题会话 ID 保持不变: ${currentSession.id}`,
1144
+ subtype: 'session.created',
1145
+ };
1146
+ }
1147
+ await interruptPausedSessionBeforeReplacement(currentSession);
1090
1148
  try {
1091
1149
  owningAgent.setActiveBaseagent(args);
1092
1150
  }
1093
1151
  catch (error) {
1094
1152
  return { kind: 'command.error', text: `❌ ${error instanceof Error ? error.message : String(error)}` };
1095
1153
  }
1096
- this.eventBus.publish({
1097
- type: 'agent:baseagent-changed',
1098
- aid: owningAgent.aid,
1099
- baseagent: args,
1100
- previousBaseagent: previousDefaultBaseagent,
1101
- scope: 'agent',
1102
- timestamp: Date.now(),
1103
- });
1104
- const projectName = this.getProjectName(owningAgent.projectPath);
1105
- const agentSwitchResponse = [
1106
- `✓ 已设置 Baseagent: ${args}`,
1107
- ` Agent: ${owningAgent.name}`,
1108
- ` 项目: ${projectName}`,
1109
- ].join('\n');
1110
- return { kind: 'command.result', text: agentSwitchResponse };
1154
+ let newSession;
1155
+ try {
1156
+ newSession = await this.sessionManager.createNewSession(channel, channelId, owningAgent.projectPath, undefined, args, currentSession?.metadata, { previousBaseagent, baseagentChanged: previousBaseagent !== args });
1157
+ }
1158
+ catch (error) {
1159
+ try {
1160
+ owningAgent.setActiveBaseagent(previousBaseagent);
1161
+ }
1162
+ catch { }
1163
+ return { kind: 'command.error', text: `❌ 创建新会话失败:${error instanceof Error ? error.message : String(error)}` };
1164
+ }
1165
+ if (currentSession) {
1166
+ const previousAgent = getActiveAgentIfAvailable();
1167
+ if (previousAgent) {
1168
+ await previousAgent.clearSession(currentSession.id, currentSession.agentSessionId || '', currentSession.projectPath);
1169
+ await previousAgent.closeSession(currentSession.id);
1170
+ }
1171
+ }
1172
+ this.eventBus.publish({ type: 'agent:baseagent-changed', aid: owningAgent.aid, baseagent: args, previousBaseagent, scope: 'agent', timestamp: Date.now() });
1173
+ return {
1174
+ kind: 'system.notice',
1175
+ text: `✓ 已切换 Baseagent: ${previousBaseagent} → ${args}\n 已创建新的主会话: ${newSession.id}\n 之前的对话历史已保留,可通过 /s 查看`,
1176
+ subtype: 'session.created',
1177
+ };
1111
1178
  }
1112
1179
  // /model 命令:查看或切换模型/推理强度
1113
1180
  if (normalizedContent.startsWith('/model')) {
@@ -1260,12 +1327,14 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1260
1327
  catch { }
1261
1328
  const configuredModel = modelConfig.model;
1262
1329
  const configuredEffort = modelConfig.effort;
1263
- const rawModels = hasModelSwitcher(modelAgent) ? await modelAgent.listModels() : [];
1330
+ const catalog = await getCatalog(modelSelector.self, fallbackBaseagent);
1331
+ assertCatalogSelectable(catalog);
1332
+ const rawModels = catalog.models.map(m => m.id);
1264
1333
  const models = hasModelSwitcher(modelAgent)
1265
- ? filterModelsForRole(identity.role, fallbackBaseagent, rawModels, modelAgent.resolveModelId?.bind(modelAgent))
1334
+ ? filterModelsForRole(identity.role, fallbackBaseagent, rawModels, model => resolveCatalogModel(catalog, model), modelSelector.self)
1266
1335
  : rawModels;
1267
1336
  const currentModel = hasModelSwitcher(modelAgent) ? (configuredModel || modelAgent.getModel()) : modelAgent.name;
1268
- const efforts = getAvailableEfforts(modelAgent, currentModel);
1337
+ const efforts = await getAvailableEfforts(modelAgent, currentModel);
1269
1338
  const currentEffort = configuredEffort ?? modelAgent.getEffort?.() ?? 'auto';
1270
1339
  const canReadModelList = await canReadSlashModelList({
1271
1340
  subject: authSubject,
@@ -1290,7 +1359,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1290
1359
  kind: 'command-card',
1291
1360
  title: '🤖 切换模型',
1292
1361
  buttons: models.map((m) => {
1293
- const display = modelDisplayLabel(modelAgent, m);
1362
+ const display = modelDisplayLabel(modelAgent, m) + (catalog.models.find(e => e.id === m)?.verification === 'unverified' ? ' [unverified]' : '');
1294
1363
  return {
1295
1364
  label: modelMatches(modelAgent, m, currentModel) ? `✓ ${display}` : display,
1296
1365
  command: `/model ${m}`,
@@ -1300,13 +1369,14 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1300
1369
  }),
1301
1370
  },
1302
1371
  };
1303
- const replyCtx = modelSession ? this.getReplyContext(modelSession) : undefined;
1372
+ const replyCtx = modelSession ? this.getReplyContext(modelSession) : replyContext;
1304
1373
  const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1305
1374
  if (cardResult === null)
1306
1375
  return null;
1307
1376
  return { kind: 'command.result', text: cardResult };
1308
1377
  }
1309
1378
  // 降级:文本
1379
+ const catalogHint = `[${catalog.source}/${catalog.freshness}] ${catalog.diagnostics.map(d => d.message).join('; ')}`;
1310
1380
  const modelList = models.map((m) => ` ${modelMatches(modelAgent, m, currentModel) ? '✓' : ' '} ${modelDisplayLabel(modelAgent, m)}`).join('\n');
1311
1381
  const effortHint = efforts.length > 0
1312
1382
  ? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
@@ -1322,8 +1392,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1322
1392
  const usage = !args && isAdmin ? '\n\n用法: /model <模型>' : '';
1323
1393
  return {
1324
1394
  kind: 'command.result',
1325
- text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}\n\n可用模型:\n${modelList}${usage}`,
1326
- ...(args === 'list' ? { structured: { ok: true, model: currentModel, effort: currentEffort, baseagent: fallbackBaseagent, models } } : {}),
1395
+ text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}${rawModels.includes(currentModel) ? '' : ' [当前配置,未列入目录]'}\n${catalogHint}\n\n可用模型:\n${modelList}${usage}`,
1396
+ ...(args === 'list' ? { structured: { ok: true, model: currentModel, effort: currentEffort, baseagent: fallbackBaseagent, models, revision: catalog.revision, source: catalog.source, freshness: catalog.freshness, diagnostics: catalog.diagnostics } } : {}),
1327
1397
  };
1328
1398
  }
1329
1399
  return { kind: 'command.result', text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}` };
@@ -1384,7 +1454,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1384
1454
  catch { }
1385
1455
  const models = hasModelSwitcher(modelAgent) ? await modelAgent.listModels() : [];
1386
1456
  const allowedModels = hasModelSwitcher(modelAgent)
1387
- ? filterModelsForRole(identity.role, fallbackBaseagent, models, modelAgent.resolveModelId?.bind(modelAgent))
1457
+ ? filterModelsForRole(identity.role, fallbackBaseagent, models, modelAgent.resolveModelId?.bind(modelAgent), selfAID ?? this.resolveSelfAID(channel))
1388
1458
  : models;
1389
1459
  const parts = args.split(/\s+/);
1390
1460
  let newModel;
@@ -1392,7 +1462,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1392
1462
  if (parts.length === 1) {
1393
1463
  const arg = parts[0];
1394
1464
  const currentModel = hasModelSwitcher(modelAgent) ? (configuredModel || modelAgent.getModel()) : modelAgent.name;
1395
- const efforts = getAvailableEfforts(modelAgent, currentModel);
1465
+ const efforts = await getAvailableEfforts(modelAgent, currentModel);
1396
1466
  // effort 相关参数统一转发到 /effort
1397
1467
  if (efforts.includes(arg) || arg === 'auto') {
1398
1468
  const delegated = await this.handle(`/effort ${arg}`, channel, channelId, undefined, userId, threadId);
@@ -1425,7 +1495,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1425
1495
  if (!models.includes(modelArg)) {
1426
1496
  return { kind: 'command.error', text: `❌ 无效的模型ID: ${modelArgRaw}` };
1427
1497
  }
1428
- const targetEfforts = getAvailableEfforts(modelAgent, modelArg);
1498
+ const targetEfforts = await getAvailableEfforts(modelAgent, modelArg);
1429
1499
  if (targetEfforts.length === 0) {
1430
1500
  return { kind: 'command.error', text: `⚠️ ${modelArg} 不支持推理强度设置` };
1431
1501
  }
@@ -1583,7 +1653,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1583
1653
  const configuredModel = modelConfig.model;
1584
1654
  const configuredEffort = modelConfig.effort;
1585
1655
  const currentModel = hasModelSwitcher(effortAgent) ? (configuredModel || effortAgent.getModel()) : effortAgent.name;
1586
- const efforts = getAvailableEfforts(effortAgent, currentModel);
1656
+ const efforts = await getAvailableEfforts(effortAgent, currentModel);
1587
1657
  const currentEffort = configuredEffort ?? effortAgent.getEffort?.() ?? 'auto';
1588
1658
  if (efforts.length === 0) {
1589
1659
  return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
@@ -1608,7 +1678,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1608
1678
  })),
1609
1679
  },
1610
1680
  };
1611
- const replyCtx = effortSession ? this.getReplyContext(effortSession) : undefined;
1681
+ const replyCtx = effortSession ? this.getReplyContext(effortSession) : replyContext;
1612
1682
  const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1613
1683
  if (cardResult === null)
1614
1684
  return null;
@@ -1645,7 +1715,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1645
1715
  const configuredModel = modelConfig.model;
1646
1716
  const configuredEffort = modelConfig.effort;
1647
1717
  const currentModel = hasModelSwitcher(effortAgent) ? (configuredModel || effortAgent.getModel()) : effortAgent.name;
1648
- const efforts = getAvailableEfforts(effortAgent, currentModel);
1718
+ const efforts = await getAvailableEfforts(effortAgent, currentModel);
1649
1719
  const currentEffort = configuredEffort ?? effortAgent.getEffort?.() ?? 'auto';
1650
1720
  if (efforts.length === 0) {
1651
1721
  return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
@@ -1861,7 +1931,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1861
1931
  })),
1862
1932
  },
1863
1933
  };
1864
- const replyCtx = replyContext ?? (activitySession ? this.getReplyContext(activitySession) : undefined);
1934
+ // A topic session's persisted route is authoritative; an inbound
1935
+ // replyContext may be a stale/main-session snapshot (notably for card
1936
+ // callbacks), so prefer the resolved session context whenever present.
1937
+ const replyCtx = activitySession ? this.getReplyContext(activitySession) : replyContext;
1865
1938
  const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
1866
1939
  if (cardResult === null)
1867
1940
  return null;
@@ -1929,7 +2002,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1929
2002
  })),
1930
2003
  },
1931
2004
  };
1932
- const replyCtx = existingChatmodeSession ? this.getReplyContext(existingChatmodeSession) : undefined;
2005
+ const replyCtx = existingChatmodeSession ? this.getReplyContext(existingChatmodeSession) : replyContext;
1933
2006
  const cardResult = await this.sendCommandCard({
1934
2007
  channel,
1935
2008
  channelId,
@@ -2235,7 +2308,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2235
2308
  }
2236
2309
  return {
2237
2310
  kind: 'system.notice',
2238
- text: `✅ backend 已切换${outcome.wasBound ? '' : '(当前未绑定 backend)'};下一条消息将初始化新 backend(阶段一不携带历史摘要)`,
2311
+ text: '已切换到新会话。下一条消息将在新会话中处理。',
2239
2312
  subtype: 'session.renewed',
2240
2313
  };
2241
2314
  }
@@ -2272,7 +2345,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2272
2345
  // The runner instance's model is only the final fallback; without this
2273
2346
  // override Claude's /compact command could resume with a stale agent
2274
2347
  // default (for example glm-5.3) while the turn itself used gpt-5.6-sol.
2275
- const compactBaseagentRaw = session.baseagent || currentSessionAgent.name;
2348
+ const compactBaseagentRaw = session.baseagent;
2349
+ if (!compactBaseagentRaw) {
2350
+ return { kind: 'command.error', text: '❌ 当前会话缺少 baseagent 绑定,拒绝隐式切换' };
2351
+ }
2276
2352
  const compactBaseagentNormalized = normalizeBaseagent(compactBaseagentRaw);
2277
2353
  const compactBaseagent = compactBaseagentNormalized.canonical === 'unknown'
2278
2354
  ? compactBaseagentRaw
@@ -2502,6 +2578,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2502
2578
  if (session && previousAgent) {
2503
2579
  // Reset agent backend state so the new
2504
2580
  // session starts with a fresh conversation history
2581
+ // Clearing is a pure backend lifecycle operation; model selection is
2582
+ // resolved only when the new session performs its next turn.
2505
2583
  await previousAgent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
2506
2584
  await previousAgent.closeSession(session.id);
2507
2585
  }
@@ -2590,7 +2668,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2590
2668
  groups.set(type, []);
2591
2669
  groups.get(type).push({ name, status });
2592
2670
  }
2593
- if (!isAdmin && !isDaemonOwner) {
2671
+ // ECWeb 的系统控制面需要结构化健康数据(尤其是 daemon.aun.connected)。
2672
+ // 这里不能把全局 system 通道降级成仅文本摘要,否则前端会因缺少
2673
+ // daemonStatusProvider 返回值而把已连接的控制 AID 显示为 disconnect。
2674
+ if (!isAdmin && !isDaemonOwner && !isGlobalCheckChannel) {
2594
2675
  // visitor/member: 仅显示实例通道摘要
2595
2676
  const total = [...groups.values()].flat().length;
2596
2677
  const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
@@ -2743,6 +2824,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2743
2824
  // 进程级操作:必须是 daemon owner(daemon.json.owners),与 menu 协议 /system restart 一致。
2744
2825
  // agent-channel 的 owner/admin 角色不足以重启整个 daemon。
2745
2826
  const restartSelfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
2827
+ const commandSession = await getExistingSessionForCommand();
2746
2828
  const authDenied = await authorizeIntent({
2747
2829
  intent: {
2748
2830
  operation: 'system.restart',
@@ -2752,7 +2834,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2752
2834
  dangerous: true,
2753
2835
  },
2754
2836
  identity,
2755
- session: activeSession,
2837
+ session: commandSession ?? activeSession,
2756
2838
  channel,
2757
2839
  channelId,
2758
2840
  userId,
@@ -2764,7 +2846,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2764
2846
  const selfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
2765
2847
  // 排除当前会话(如果存在)。如果 activeSession 不存在,说明还没建立会话,
2766
2848
  // 此时检查所有任务;如果有其他会话在处理,仍然阻塞重启。
2767
- const excludeKey = activeSession?.id;
2849
+ // A topic command owns its topic session even when the channel's active
2850
+ // session remains the main conversation. Exclude that exact queue key
2851
+ // from the busy check so the current topic cannot block its own restart.
2852
+ const excludeKey = commandSession?.id ?? activeSession?.id;
2768
2853
  const busyInfo = getAgentBusyInfo(this, selfAid, excludeKey);
2769
2854
  if (busyInfo && busyInfo.count > 0) {
2770
2855
  // busyInfo.count > 0 说明【除当前会话外】还有其他会话的任务在执行
@@ -2789,10 +2874,14 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2789
2874
  const suppressRealRestart = shouldSuppressRealRestart();
2790
2875
  let restartReplyContext = replyContext;
2791
2876
  try {
2792
- if (!restartReplyContext?.delivery && threadId) {
2877
+ // A topic's persisted session context is authoritative for card
2878
+ // callbacks and for inbound messages that only carry a partial route.
2879
+ // Do not let a stale/main-session replyContext win merely because it
2880
+ // happens to contain a delivery target.
2881
+ if (threadId) {
2793
2882
  const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
2794
2883
  if (threadSession)
2795
- restartReplyContext = this.getReplyContext(threadSession);
2884
+ restartReplyContext = { ...(replyContext ?? {}), ...(this.getReplyContext(threadSession) ?? {}), threadId };
2796
2885
  }
2797
2886
  else if (!restartReplyContext?.delivery && activeSession) {
2798
2887
  restartReplyContext = this.getReplyContext(activeSession);
@@ -2834,16 +2923,39 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2834
2923
  channel,
2835
2924
  channelId,
2836
2925
  timestamp: Date.now(),
2926
+ ...(restartReplyContext?.threadId ? { threadId: restartReplyContext.threadId } : {}),
2837
2927
  ...(restartReplyContext?.delivery ? { delivery: restartReplyContext.delivery } : {}),
2838
2928
  ...(restartReplyContext?.replyToMessageId ? { rootId: restartReplyContext.replyToMessageId } : {}),
2839
2929
  };
2840
2930
  if (!suppressRealRestart) {
2931
+ // 这条路径上 daemon 会在下面 kill 掉自己,所以必须先确认 monitor 真的活起来:
2932
+ // 只查文件存在不够 —— 开发仓下 monitor 走 tsx,源码语法错误会让它加载即退出;
2933
+ // 安装包下 monitor 先复制到 stage 再启动,避免升级替换掉自己脚下的文件。
2934
+ const monitorEntry = resolveMonitorLaunch(getPackageRoot(), path.join(resolvePaths().root, 'data', 'monitor-stage'));
2935
+ if (monitorEntry.note)
2936
+ logger.info(`[System] ${monitorEntry.note}`);
2937
+ if (!fs.existsSync(monitorEntry.entry)) {
2938
+ logger.error(`[System] Refusing restart: restart-monitor entry not found: ${monitorEntry.entry}`);
2939
+ return { code: 'RESTART_ENTRY_MISSING', entry: monitorEntry.entry };
2940
+ }
2841
2941
  const controlDir = daemonControlDir();
2842
2942
  fs.mkdirSync(controlDir, { recursive: true });
2843
2943
  fs.writeFileSync(path.join(controlDir, 'restart-pending.json'), JSON.stringify(restartInfo));
2844
- spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
2845
- EVOLCORE_HOME: resolvePaths().root,
2846
- });
2944
+ const confirmation = await launchMonitorAndConfirm([...monitorEntry.args, 'restart-monitor'], { EVOLCORE_HOME: resolvePaths().root, ...monitorEntry.env }, pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid === pid), pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid !== pid));
2945
+ if (!confirmation.ok) {
2946
+ // 没有看门狗就不要停服务:撤掉回执文件,让本进程继续跑。
2947
+ if (monitorEntry.stageDir)
2948
+ cleanupMonitorStage(monitorEntry.stageDir);
2949
+ try {
2950
+ fs.rmSync(path.join(controlDir, 'restart-pending.json'), { force: true });
2951
+ }
2952
+ catch { }
2953
+ logger.error(`[System] Refusing restart: restart-monitor did not come up (${confirmation.reason})`);
2954
+ for (const line of (confirmation.stderrTail ?? '').split(/\r?\n/).filter(Boolean).slice(-8)) {
2955
+ logger.error(`[System] | ${line}`);
2956
+ }
2957
+ return { code: 'RESTART_MONITOR_DEAD', reason: confirmation.reason ?? 'unknown' };
2958
+ }
2847
2959
  }
2848
2960
  else {
2849
2961
  logger.info('[System] Suppressed real restart in test runtime');
@@ -2874,15 +2986,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2874
2986
  logger.error('[System] Failed to send restart notification:', err);
2875
2987
  }
2876
2988
  }
2877
- // 发 SIGTERM 而非直接 process.exit(0),让 index.ts 的 shutdown() 先
2878
- // 正常关闭所有 channel(包括 Feishu WebSocket close frame),
2879
- // 避免 Feishu 服务端因连接异常断开而重推未 ack 的消息给新进程。
2880
- if (!suppressRealRestart) {
2881
- setTimeout(() => {
2882
- logger.info('[System] Restarting by user command...');
2883
- process.kill(process.pid, 'SIGTERM');
2884
- }, 1000);
2885
- }
2989
+ // restart-monitor owns the old daemon shutdown and the replacement
2990
+ // startup. Do not kill this process here: the monitor may have exited
2991
+ // after a read-only migration check, in which case the current service
2992
+ // must remain available.
2886
2993
  return true;
2887
2994
  };
2888
2995
  // 文本确认流程
@@ -2911,7 +3018,21 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2911
3018
  if (typeof restarted === 'object' && restarted.code === 'DATA_MIGRATION_REQUIRED') {
2912
3019
  return {
2913
3020
  kind: 'command.error',
2914
- text: `❌ 待处理的用户数据迁移(${restarted.operationCount} 项)阻止重启。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
3021
+ text: `❌ 待处理的用户数据迁移(${restarted.operationCount} 项)阻止重启;本次仅检查,未停止当前服务。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
3022
+ reason: restarted.code,
3023
+ };
3024
+ }
3025
+ if (typeof restarted === 'object' && restarted.code === 'RESTART_ENTRY_MISSING') {
3026
+ return {
3027
+ kind: 'command.error',
3028
+ text: `❌ 重启已中止:找不到 restart-monitor 入口 ${restarted.entry}。服务未重启,当前进程继续运行。请先执行 npm run build 重建产物。`,
3029
+ reason: restarted.code,
3030
+ };
3031
+ }
3032
+ if (typeof restarted === 'object' && restarted.code === 'RESTART_MONITOR_DEAD') {
3033
+ return {
3034
+ kind: 'command.error',
3035
+ text: `❌ 重启已中止:看门狗进程未能启动(${restarted.reason})。服务未停止,仍在运行当前版本。开发仓下这通常是源码语法错误导致 tsx 无法加载,请执行 npx tsc --noEmit 检查。`,
2915
3036
  reason: restarted.code,
2916
3037
  };
2917
3038
  }
@@ -3079,11 +3200,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3079
3200
  ? { delivery: { chatType: 'private' } }
3080
3201
  : sendSession
3081
3202
  ? this.getReplyContext(sendSession)
3082
- : chatType === 'group'
3203
+ : replyContext ?? (chatType === 'group'
3083
3204
  ? { delivery: { chatType: 'group', groupId: channelId } }
3084
3205
  : chatType === 'private'
3085
3206
  ? { delivery: { chatType: 'private' } }
3086
- : undefined;
3207
+ : undefined);
3087
3208
  await targetAdapter.send(buildEnvelope({ channel: targetAdapter.channelName, channelId: targetChannelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath });
3088
3209
  const sizeStr = stat.size < 1024 ? `${stat.size} B`
3089
3210
  : stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
@@ -3309,7 +3430,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3309
3430
  projectPaths.unshift(session.projectPath);
3310
3431
  }
3311
3432
  for (const projectPath of projectPaths) {
3312
- const currentBaseagent = session?.baseagent || this.primaryRunnerKey;
3433
+ const currentBaseagent = session?.baseagent
3434
+ ?? this.agentRegistry?.resolveByChannel(channel)?.baseagent
3435
+ ?? (this.agentMap?.size === 1 ? this.parseDefaultBaseagent() : undefined);
3436
+ if (!currentBaseagent) {
3437
+ return { kind: 'command.error', text: '❌ 无法确定当前会话的 baseagent,拒绝导入 CLI 会话' };
3438
+ }
3313
3439
  const cliSessions = currentBaseagent === 'ecagent'
3314
3440
  ? await this.sessionManager.listImportableCliSessions(projectPath, currentBaseagent, selfAID ?? session?.selfAID)
3315
3441
  : await this.sessionManager.listImportableCliSessions(projectPath, currentBaseagent);
@@ -3484,7 +3610,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3484
3610
  const session = await getExistingSessionForCommand();
3485
3611
  if (!session)
3486
3612
  return { kind: 'command.error', text: '❌ 当前没有活跃会话' };
3487
- const rewindBaseagent = session.baseagent ?? session.agentId;
3613
+ const rewindBaseagent = session.baseagent;
3614
+ if (!rewindBaseagent) {
3615
+ return { kind: 'command.error', text: '❌ 当前会话缺少 baseagent 绑定,拒绝隐式切换' };
3616
+ }
3488
3617
  const rewindAgent = this.getAgent(channel, rewindBaseagent);
3489
3618
  if (!session.agentSessionId) {
3490
3619
  return { kind: 'command.error', text: '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind' };
@@ -752,21 +752,11 @@ export function planDataMigration(root) {
752
752
  }
753
753
  }
754
754
  catch { }
755
- const legacyOutbox = path.join(root, 'data', 'outbox');
756
- try {
757
- for (const entry of fs.readdirSync(legacyOutbox, { withFileTypes: true })) {
758
- if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
759
- continue;
760
- const aid = entry.name.slice(0, -'.jsonl'.length);
761
- if (!aids.has(aid)) {
762
- warnings.push(`Unresolved outbox owner retained: ${entry.name}`);
763
- continue;
764
- }
765
- const operationId = nextId('outbox');
766
- addCopy(operations, operationId, path.join(legacyOutbox, entry.name), path.join(root, 'agents', aid, 'data', 'outbox.jsonl'), stageFor(operationId));
767
- }
768
- }
769
- catch { }
755
+ // Outbox layouts are imported lazily by src/aun/outbox.ts when a queue is
756
+ // first read or written. They are append-only delivery state, so including
757
+ // them in the maintenance migration would unnecessarily block startup and
758
+ // could race a live connector. Keep the explicit migration reserved for
759
+ // data that needs operator review or an exclusive maintenance window.
770
760
  const contactAudit = path.join(root, 'data', 'contact-book-audit.jsonl');
771
761
  // The legacy audit file may be provisioned as an empty placeholder (for
772
762
  // example by a protected-path sandbox). An empty source has nothing to
@@ -89,6 +89,9 @@ const CATALOG = [
89
89
  { path: 'name', type: 'string', optional: true },
90
90
  { path: 'chatType', type: 'string', optional: true },
91
91
  { path: 'threadId', type: 'string', optional: true },
92
+ { path: 'baseagent', type: 'string', optional: true },
93
+ { path: 'previousBaseagent', type: 'string', optional: true },
94
+ { path: 'baseagentChanged', type: 'boolean', optional: true },
92
95
  { path: 'cause', type: 'string', optional: true },
93
96
  { path: 'sourceSessionId', type: 'string', optional: true },
94
97
  { path: 'renewSource', type: 'string', optional: true },
@@ -118,6 +118,7 @@ export class HandoffRuntime {
118
118
  targetSessionId: target.id,
119
119
  payload: input.payload,
120
120
  encrypt: input.encrypt,
121
+ contextOnly: input.contextOnly,
121
122
  delivery: targetChatType === 'group'
122
123
  ? { chatType: 'group', groupId: input.to }
123
124
  : { chatType: 'private' },
@@ -131,7 +132,8 @@ export class HandoffRuntime {
131
132
  status: 'started',
132
133
  refs: { handoffId: handoff.handoff_id, sessionId: origin.id, messageId: input.originMessageId },
133
134
  });
134
- await this.dispatcher.drain(input.selfAid, target.id);
135
+ if (!input.contextOnly)
136
+ await this.dispatcher.drain(input.selfAid, target.id);
135
137
  const dispatched = this.store.get(input.selfAid, handoff.handoff_id) ?? handoff;
136
138
  const replacedSessionId = [origin.id, target.id].find(sessionId => ((this.sessionDiscardSequence.get(this.sessionKey(input.selfAid, sessionId)) ?? 0) > operationSequence));
137
139
  if (dispatched.state === 'discarded' || replacedSessionId) {