evolcore 0.0.21 → 0.0.23

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 (112) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/codex-managed-hook.mjs +3 -0
  6. package/bin/install-codex-managed-hooks.mjs +376 -96
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +1 -1
  9. package/dist/agents/claude-runner.js +44 -226
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
  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 +154 -0
  14. package/dist/agents/codex-runner.js +1292 -219
  15. package/dist/agents/ecagent-runner.js +10 -31
  16. package/dist/agents/gemini-runner.js +2 -6
  17. package/dist/aun/aid/store.js +2 -0
  18. package/dist/aun/menu-client.js +272 -0
  19. package/dist/aun/msg/group.js +65 -4
  20. package/dist/aun/msg/index.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +51 -4
  22. package/dist/aun/outbox.js +7 -0
  23. package/dist/aun/rpc/client.js +8 -1
  24. package/dist/channels/aun.js +240 -36
  25. package/dist/channels/wechat.js +81 -10
  26. package/dist/cli/aun-commands.js +48 -4
  27. package/dist/cli/command-log.js +21 -2
  28. package/dist/cli/daemon-commands.js +169 -51
  29. package/dist/cli/index.js +20 -0
  30. package/dist/cli/init.js +2 -10
  31. package/dist/cli/menu-client.js +95 -0
  32. package/dist/cli/model.js +23 -8
  33. package/dist/cli/task-context.js +16 -2
  34. package/dist/cli/trigger-command.js +22 -5
  35. package/dist/config/config-field-policy.js +3 -0
  36. package/dist/config/config-manager.js +32 -5
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/schema-migration.js +14 -2
  39. package/dist/core/auth/agent-delegation.js +15 -0
  40. package/dist/core/auth/auth-gateway.js +8 -0
  41. package/dist/core/auth/authorization-audit.js +70 -6
  42. package/dist/core/auth/operation-catalog.js +4 -0
  43. package/dist/core/auth/remote-access-store.js +135 -0
  44. package/dist/core/bootstrap-messages.js +8 -0
  45. package/dist/core/bootstrap-service.js +93 -25
  46. package/dist/core/command/command-handler.js +62 -5
  47. package/dist/core/command/daemon-diagnostics.js +448 -0
  48. package/dist/core/command/menu-catalog.js +3 -0
  49. package/dist/core/command/menu-handler.js +150 -33
  50. package/dist/core/command/menu-protocol.js +120 -1
  51. package/dist/core/command/slash-gate.js +3 -3
  52. package/dist/core/command/slash-handler.js +83 -40
  53. package/dist/core/data-migration.js +11 -1
  54. package/dist/core/event-catalog.js +32 -0
  55. package/dist/core/handoff/runtime.js +23 -3
  56. package/dist/core/message/im-renderer.js +7 -3
  57. package/dist/core/message/message-bridge.js +60 -2
  58. package/dist/core/message/message-log.js +33 -0
  59. package/dist/core/message/message-queue.js +41 -15
  60. package/dist/core/message/response-engine.js +246 -114
  61. package/dist/core/model/catalog-service.js +308 -0
  62. package/dist/core/model/model-catalog.js +124 -334
  63. package/dist/core/model/model-diagnostics.js +4 -5
  64. package/dist/core/model/model-fallback.js +9 -16
  65. package/dist/core/model/model-permission.js +1 -1
  66. package/dist/core/permission/approval-gateway.js +16 -1
  67. package/dist/core/permission/ec-command-parser.js +272 -70
  68. package/dist/core/permission/protected-paths.js +74 -14
  69. package/dist/core/permission/sandbox-runtime.js +4 -2
  70. package/dist/core/permission/tool-error-code.js +12 -0
  71. package/dist/core/permission/tool-policy.js +290 -30
  72. package/dist/core/runtime-lock.js +13 -8
  73. package/dist/core/session/session-manager.js +30 -0
  74. package/dist/core/session/session-renew.js +18 -1
  75. package/dist/core/session/session-turn-coordinator.js +5 -1
  76. package/dist/index.js +104 -34
  77. package/dist/ipc.js +218 -96
  78. package/dist/paths.js +18 -0
  79. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  80. package/dist/stats/price-resolver.js +4 -0
  81. package/dist/trigger/feedback.js +14 -2
  82. package/dist/trigger/manager.js +3 -1
  83. package/dist/trigger/parser.js +86 -13
  84. package/dist/trigger/patch.js +104 -1
  85. package/dist/trigger/scheduler.js +20 -3
  86. package/dist/utils/ecweb-supervisor.js +11 -2
  87. package/dist/utils/ecweb-utils.js +8 -26
  88. package/dist/utils/error-utils.js +7 -0
  89. package/dist/utils/logger.js +9 -4
  90. package/dist/utils/npm-ops.js +47 -15
  91. package/dist/utils/tool-summary.js +59 -0
  92. package/dist/utils/windows-shell-trust.js +201 -0
  93. package/kits/docs/evolcore/INDEX.md +3 -2
  94. package/kits/docs/evolcore/agent-create.md +159 -0
  95. package/kits/docs/evolcore/agent.md +6 -0
  96. package/kits/docs/evolcore/group-collaboration.md +256 -0
  97. package/kits/docs/evolcore/group-rules.md +1 -19
  98. package/kits/docs/evolcore/group.md +3 -1
  99. package/kits/docs/evolcore/menu-client.md +34 -0
  100. package/kits/docs/evolcore/trigger.md +6 -3
  101. package/kits/docs/prompt-loading-architecture.md +6 -0
  102. package/kits/eck_message_manifest.json +6 -6
  103. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  104. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  105. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  106. package/kits/schemas/_meta.json +3 -2
  107. package/kits/schemas/agent-config.schema.12.json +427 -0
  108. package/kits/templates/message-fragments/item.md +1 -1
  109. package/kits/templates/roles/visitor.json +1 -0
  110. package/kits/templates/system-fragments/bootstrap.md +10 -2
  111. package/kits/templates/system-fragments/commands.md +2 -2
  112. package/package.json +11 -3
@@ -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';
@@ -26,8 +26,9 @@ import { normalizePermissionMode as normalizePermissionModeContract, PUBLIC_PERM
26
26
  import { isManagementRole } from '../../config/builtin-roles.js';
27
27
  import { isSystemControlChannel } from '../system-channels.js';
28
28
  import { spawnDetachedNode } from '../../utils/cross-platform.js';
29
+ import { inspectDataMigrationRequirement } from '../data-migration.js';
29
30
  import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, probeSessionActivity, } from './slash-gate.js';
30
- const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
31
+ const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
31
32
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
32
33
  function defaultSlashChatmode(field) {
33
34
  return field === 'private' ? 'interactive' : 'proactive';
@@ -126,12 +127,8 @@ function resolveSlashRelationTarget(params) {
126
127
  role: params.role,
127
128
  };
128
129
  }
129
- function getAvailableEfforts(agent, model) {
130
- if (agent.name === 'claude')
131
- return allEfforts;
132
- if (agent.name === 'codex')
133
- return getCodexEfforts(model);
134
- return [];
130
+ async function getAvailableEfforts(agent, model) {
131
+ return (await agent.getAvailableEfforts?.(model) ?? []);
135
132
  }
136
133
  function modelDisplayLabel(agent, model) {
137
134
  const full = agent.resolveModelId?.(model);
@@ -486,7 +483,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
486
483
  const isProcessLevelSlash = normalizedContent === '/restart'
487
484
  || normalizedContent === '/upgrade'
488
485
  || normalizedContent === '/reload'
489
- || normalizedContent.startsWith('/reload ');
486
+ || normalizedContent.startsWith('/reload ')
487
+ || normalizedContent === '/pair';
490
488
  const roleGuard = guardRoleCommand(normalizedContent, activeChatType, isAdmin || (isDaemonOwner && isProcessLevelSlash));
491
489
  if (roleGuard)
492
490
  return roleGuard;
@@ -634,7 +632,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
634
632
  const appendProcessCommands = (lines) => {
635
633
  if (!isDaemonOwner)
636
634
  return;
637
- lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置', ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)');
635
+ lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置', ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)', ' /pair - 获取远程控制配对码');
638
636
  };
639
637
  if (!isAdmin && activeChatType === 'group') {
640
638
  const lines = [
@@ -774,6 +772,23 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
774
772
  ];
775
773
  return { kind: 'command.result', text: lines.join('\n') };
776
774
  }
775
+ // /pair is also recognized on an Agent channel so it cannot fall through
776
+ // into model input. The daemon control channel has its own fast path; this
777
+ // branch preserves the same daemon-owner boundary for ordinary AUN chats.
778
+ if (normalizedContent === '/pair') {
779
+ if (!isDaemonOwner) {
780
+ return {
781
+ kind: 'command.error',
782
+ text: '❌ /pair 仅限 daemon-owner 使用;请向 daemon 控制 AID 发送 /pair。',
783
+ };
784
+ }
785
+ const pair = remoteAccessService.getPairingCode(userId);
786
+ const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
787
+ return {
788
+ kind: 'command.result',
789
+ text: `远程控制配对码:${pair.code}(约 ${mins} 分钟内有效)\n可用于 ECWeb 登录或远程 Menu 客户端授权`,
790
+ };
791
+ }
777
792
  // /evolhelp 命令:返回 JSON 格式的命令列表(供程序解析)
778
793
  if (normalizedContent === '/evolhelp') {
779
794
  const cmds = [];
@@ -834,6 +849,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
834
849
  cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['daemon-owner'] });
835
850
  cmds.push({ command: '/upgrade', description: '检查版本更新', category: '运维', roles: ['daemon-owner'] });
836
851
  cmds.push({ command: '/reload', args: '[aid]', description: '热重载 Agent 配置', category: '运维', roles: ['daemon-owner'] });
852
+ cmds.push({ command: '/pair', description: '获取远程控制配对码', category: '运维', roles: ['daemon-owner'] });
837
853
  }
838
854
  if (!isDaemonOwner && isAdmin) {
839
855
  cmds.push({ command: '/reload', description: '热重载当前 Agent 配置', category: '运维', roles: ['admin', 'owner'] });
@@ -1259,12 +1275,14 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1259
1275
  catch { }
1260
1276
  const configuredModel = modelConfig.model;
1261
1277
  const configuredEffort = modelConfig.effort;
1262
- const rawModels = hasModelSwitcher(modelAgent) ? await modelAgent.listModels() : [];
1278
+ const catalog = await getCatalog(modelSelector.self, fallbackBaseagent);
1279
+ assertCatalogSelectable(catalog);
1280
+ const rawModels = catalog.models.map(m => m.id);
1263
1281
  const models = hasModelSwitcher(modelAgent)
1264
- ? filterModelsForRole(identity.role, fallbackBaseagent, rawModels, modelAgent.resolveModelId?.bind(modelAgent))
1282
+ ? filterModelsForRole(identity.role, fallbackBaseagent, rawModels, model => resolveCatalogModel(catalog, model), modelSelector.self)
1265
1283
  : rawModels;
1266
1284
  const currentModel = hasModelSwitcher(modelAgent) ? (configuredModel || modelAgent.getModel()) : modelAgent.name;
1267
- const efforts = getAvailableEfforts(modelAgent, currentModel);
1285
+ const efforts = await getAvailableEfforts(modelAgent, currentModel);
1268
1286
  const currentEffort = configuredEffort ?? modelAgent.getEffort?.() ?? 'auto';
1269
1287
  const canReadModelList = await canReadSlashModelList({
1270
1288
  subject: authSubject,
@@ -1289,7 +1307,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1289
1307
  kind: 'command-card',
1290
1308
  title: '🤖 切换模型',
1291
1309
  buttons: models.map((m) => {
1292
- const display = modelDisplayLabel(modelAgent, m);
1310
+ const display = modelDisplayLabel(modelAgent, m) + (catalog.models.find(e => e.id === m)?.verification === 'unverified' ? ' [unverified]' : '');
1293
1311
  return {
1294
1312
  label: modelMatches(modelAgent, m, currentModel) ? `✓ ${display}` : display,
1295
1313
  command: `/model ${m}`,
@@ -1306,6 +1324,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1306
1324
  return { kind: 'command.result', text: cardResult };
1307
1325
  }
1308
1326
  // 降级:文本
1327
+ const catalogHint = `[${catalog.source}/${catalog.freshness}] ${catalog.diagnostics.map(d => d.message).join('; ')}`;
1309
1328
  const modelList = models.map((m) => ` ${modelMatches(modelAgent, m, currentModel) ? '✓' : ' '} ${modelDisplayLabel(modelAgent, m)}`).join('\n');
1310
1329
  const effortHint = efforts.length > 0
1311
1330
  ? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
@@ -1321,8 +1340,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1321
1340
  const usage = !args && isAdmin ? '\n\n用法: /model <模型>' : '';
1322
1341
  return {
1323
1342
  kind: 'command.result',
1324
- text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}\n\n可用模型:\n${modelList}${usage}`,
1325
- ...(args === 'list' ? { structured: { ok: true, model: currentModel, effort: currentEffort, baseagent: fallbackBaseagent, models } } : {}),
1343
+ text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}${rawModels.includes(currentModel) ? '' : ' [当前配置,未列入目录]'}\n${catalogHint}\n\n可用模型:\n${modelList}${usage}`,
1344
+ ...(args === 'list' ? { structured: { ok: true, model: currentModel, effort: currentEffort, baseagent: fallbackBaseagent, models, revision: catalog.revision, source: catalog.source, freshness: catalog.freshness, diagnostics: catalog.diagnostics } } : {}),
1326
1345
  };
1327
1346
  }
1328
1347
  return { kind: 'command.result', text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}` };
@@ -1383,7 +1402,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1383
1402
  catch { }
1384
1403
  const models = hasModelSwitcher(modelAgent) ? await modelAgent.listModels() : [];
1385
1404
  const allowedModels = hasModelSwitcher(modelAgent)
1386
- ? filterModelsForRole(identity.role, fallbackBaseagent, models, modelAgent.resolveModelId?.bind(modelAgent))
1405
+ ? filterModelsForRole(identity.role, fallbackBaseagent, models, modelAgent.resolveModelId?.bind(modelAgent), selfAID ?? this.resolveSelfAID(channel))
1387
1406
  : models;
1388
1407
  const parts = args.split(/\s+/);
1389
1408
  let newModel;
@@ -1391,7 +1410,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1391
1410
  if (parts.length === 1) {
1392
1411
  const arg = parts[0];
1393
1412
  const currentModel = hasModelSwitcher(modelAgent) ? (configuredModel || modelAgent.getModel()) : modelAgent.name;
1394
- const efforts = getAvailableEfforts(modelAgent, currentModel);
1413
+ const efforts = await getAvailableEfforts(modelAgent, currentModel);
1395
1414
  // effort 相关参数统一转发到 /effort
1396
1415
  if (efforts.includes(arg) || arg === 'auto') {
1397
1416
  const delegated = await this.handle(`/effort ${arg}`, channel, channelId, undefined, userId, threadId);
@@ -1424,7 +1443,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1424
1443
  if (!models.includes(modelArg)) {
1425
1444
  return { kind: 'command.error', text: `❌ 无效的模型ID: ${modelArgRaw}` };
1426
1445
  }
1427
- const targetEfforts = getAvailableEfforts(modelAgent, modelArg);
1446
+ const targetEfforts = await getAvailableEfforts(modelAgent, modelArg);
1428
1447
  if (targetEfforts.length === 0) {
1429
1448
  return { kind: 'command.error', text: `⚠️ ${modelArg} 不支持推理强度设置` };
1430
1449
  }
@@ -1582,7 +1601,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1582
1601
  const configuredModel = modelConfig.model;
1583
1602
  const configuredEffort = modelConfig.effort;
1584
1603
  const currentModel = hasModelSwitcher(effortAgent) ? (configuredModel || effortAgent.getModel()) : effortAgent.name;
1585
- const efforts = getAvailableEfforts(effortAgent, currentModel);
1604
+ const efforts = await getAvailableEfforts(effortAgent, currentModel);
1586
1605
  const currentEffort = configuredEffort ?? effortAgent.getEffort?.() ?? 'auto';
1587
1606
  if (efforts.length === 0) {
1588
1607
  return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
@@ -1644,7 +1663,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1644
1663
  const configuredModel = modelConfig.model;
1645
1664
  const configuredEffort = modelConfig.effort;
1646
1665
  const currentModel = hasModelSwitcher(effortAgent) ? (configuredModel || effortAgent.getModel()) : effortAgent.name;
1647
- const efforts = getAvailableEfforts(effortAgent, currentModel);
1666
+ const efforts = await getAvailableEfforts(effortAgent, currentModel);
1648
1667
  const currentEffort = configuredEffort ?? effortAgent.getEffort?.() ?? 'auto';
1649
1668
  if (efforts.length === 0) {
1650
1669
  return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
@@ -2142,15 +2161,15 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2142
2161
  // 才会在新的 TurnLease 中懒创建并绑定新 backend。
2143
2162
  if (normalizedContent === '/renew' || /^\/renew\s/.test(normalizedContent)) {
2144
2163
  if (normalizedContent !== '/renew') {
2145
- return { kind: 'command.error', text: '❌ /renew 不接受参数,请直接使用 /renew' };
2164
+ return { kind: 'system.error', text: '❌ /renew 不接受参数,请直接使用 /renew', subtype: 'session.renew.invalid_arguments', recoverable: true };
2146
2165
  }
2147
2166
  if (!threadId) {
2148
- return { kind: 'command.error', text: '⚠️ /renew 仅支持话题会话' };
2167
+ return { kind: 'system.error', text: '⚠️ /renew 仅支持话题会话', subtype: 'session.renew.unsupported', recoverable: true };
2149
2168
  }
2150
2169
  // Card callbacks use synthetic message IDs on several adapters. They are
2151
2170
  // suitable for UI correlation, but not for the durable inbound claim.
2152
2171
  if (source === 'card-trigger') {
2153
- return { kind: 'command.error', text: '⚠️ /renew 仅支持携带原生消息 ID 的普通消息' };
2172
+ return { kind: 'system.error', text: '⚠️ /renew 仅支持携带原生消息 ID 的普通消息', subtype: 'session.renew.invalid_source', recoverable: true };
2154
2173
  }
2155
2174
  let renewSession;
2156
2175
  try {
@@ -2160,19 +2179,19 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2160
2179
  }
2161
2180
  catch (error) {
2162
2181
  logger.warn(`[CommandHandler] /renew topic lookup failed: thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
2163
- return { kind: 'command.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试' };
2182
+ return { kind: 'system.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试', subtype: 'session.renew.state_unknown', recoverable: true };
2164
2183
  }
2165
2184
  if (!renewSession) {
2166
- return { kind: 'command.error', text: '❌ 找不到当前话题会话' };
2185
+ return { kind: 'system.error', text: '❌ 找不到当前话题会话', subtype: 'session.renew.session_missing', recoverable: true };
2167
2186
  }
2168
2187
  const inboundMessageId = messageId?.trim();
2169
2188
  if (!inboundMessageId) {
2170
- return { kind: 'command.error', text: '❌ /renew 缺少稳定的消息 ID,当前渠道不支持安全重试' };
2189
+ return { kind: 'system.error', text: '❌ /renew 缺少稳定的消息 ID,当前渠道不支持安全重试', subtype: 'session.renew.message_id_missing', recoverable: true };
2171
2190
  }
2172
2191
  if (typeof this.sessionManager.rotateTopicBackend !== 'function'
2173
2192
  || typeof this.messageQueue.withSessionBarrier !== 'function'
2174
2193
  || typeof this.messageQueue.claimInbound !== 'function') {
2175
- return { kind: 'command.error', text: '❌ 当前运行环境不支持安全 backend 轮换' };
2194
+ return { kind: 'system.error', text: '❌ 当前运行环境不支持安全 backend 轮换', subtype: 'session.renew.unavailable', recoverable: true };
2176
2195
  }
2177
2196
  try {
2178
2197
  const outcome = await this.messageQueue.withSessionBarrier(renewSession.id, async (snapshot) => {
@@ -2211,33 +2230,36 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2211
2230
  };
2212
2231
  });
2213
2232
  if (outcome.kind === 'missing') {
2214
- return { kind: 'command.error', text: '❌ 当前话题会话已不存在或归属已变化' };
2233
+ return { kind: 'system.error', text: '❌ 当前话题会话已不存在或归属已变化', subtype: 'session.renew.session_changed', recoverable: true };
2215
2234
  }
2216
2235
  if (outcome.kind === 'claim_failed') {
2217
- return { kind: 'command.error', text: '❌ 当前渠道无法安全记录该请求,未执行 backend 轮换' };
2236
+ return { kind: 'system.error', text: '❌ 当前渠道无法安全记录该请求,未执行 backend 轮换', subtype: 'session.renew.claim_failed', recoverable: true };
2218
2237
  }
2219
2238
  if (outcome.kind === 'duplicate') {
2220
- return { kind: 'command.result', text: 'ℹ️ 该请求已接收过,不再重复执行' };
2239
+ return { kind: 'system.notice', text: 'ℹ️ 该请求已接收过,不再重复执行', subtype: 'session.renew.duplicate' };
2221
2240
  }
2222
2241
  if (outcome.kind === 'busy' || outcome.kind === 'unknown') {
2223
2242
  return {
2224
- kind: 'command.error',
2243
+ kind: 'system.error',
2225
2244
  text: outcome.kind === 'busy'
2226
2245
  ? '⚠️ 当前会话忙碌,无法安全轮换,请稍后再试'
2227
2246
  : '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试',
2247
+ subtype: outcome.kind === 'busy' ? 'session.renew.busy' : 'session.renew.state_unknown',
2248
+ recoverable: true,
2228
2249
  };
2229
2250
  }
2230
2251
  if (outcome.kind !== 'rotated') {
2231
- return { kind: 'command.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试' };
2252
+ return { kind: 'system.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试', subtype: 'session.renew.state_unknown', recoverable: true };
2232
2253
  }
2233
2254
  return {
2234
- kind: 'command.result',
2255
+ kind: 'system.notice',
2235
2256
  text: `✅ backend 已切换${outcome.wasBound ? '' : '(当前未绑定 backend)'};下一条消息将初始化新 backend(阶段一不携带历史摘要)`,
2257
+ subtype: 'session.renewed',
2236
2258
  };
2237
2259
  }
2238
2260
  catch (error) {
2239
2261
  logger.error(`[CommandHandler] /renew failed: session=${renewSession.id}: ${error instanceof Error ? error.message : String(error)}`);
2240
- return { kind: 'command.error', text: `❌ backend 轮换失败:${error instanceof Error ? error.message : String(error)}` };
2262
+ return { kind: 'system.error', text: `❌ backend 轮换失败:${error instanceof Error ? error.message : String(error)}`, subtype: 'session.renew.failed', recoverable: true };
2241
2263
  }
2242
2264
  }
2243
2265
  // /compact 命令:手动压缩会话上下文
@@ -2451,13 +2473,17 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2451
2473
  if (sessionName) {
2452
2474
  const existing = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
2453
2475
  if (existing) {
2454
- return { kind: 'command.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称` };
2476
+ return { kind: 'system.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称`, subtype: 'session.new.name_conflict', recoverable: true };
2455
2477
  }
2456
2478
  }
2457
2479
  await interruptPausedSessionBeforeReplacement(session || activeSession);
2458
2480
  const projectPath = this.getEffectiveDefaultPath(channel);
2459
2481
  if (sendMessage && session) {
2460
- await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session));
2482
+ await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session), {
2483
+ kind: 'system.notice',
2484
+ text: `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`,
2485
+ subtype: 'session.creating',
2486
+ });
2461
2487
  }
2462
2488
  const newSessionBaseagent = this.agentRegistry?.resolveByChannel(channel)?.baseagent || this.parseDefaultBaseagent();
2463
2489
  const previousMetadata = session?.metadata || activeSession?.metadata || {};
@@ -2494,6 +2520,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2494
2520
  if (session && previousAgent) {
2495
2521
  // Reset agent backend state so the new
2496
2522
  // session starts with a fresh conversation history
2523
+ // Clearing is a pure backend lifecycle operation; model selection is
2524
+ // resolved only when the new session performs its next turn.
2497
2525
  await previousAgent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
2498
2526
  await previousAgent.closeSession(session.id);
2499
2527
  }
@@ -2524,7 +2552,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2524
2552
  ?? newRunner.getEffort?.()
2525
2553
  ?? newAgent?.effort;
2526
2554
  const backendBits = [newBaseagent, backendModel, backendEffort].filter(Boolean).join(' · ');
2527
- return { kind: 'command.result', text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 后端: ${backendBits}\n 之前的对话历史已保留,可通过 /s 查看${handoffDiscardWarning ? `\n${handoffDiscardWarning}` : ''}` };
2555
+ return {
2556
+ kind: 'system.notice',
2557
+ text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 后端: ${backendBits}\n 之前的对话历史已保留,可通过 /s 查看${handoffDiscardWarning ? `\n${handoffDiscardWarning}` : ''}`,
2558
+ subtype: 'session.created',
2559
+ };
2528
2560
  }
2529
2561
  // /check 命令:检查 EvolAgent 实例健康(visitor/member 可用,详情仅 admin)
2530
2562
  if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
@@ -2773,7 +2805,6 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2773
2805
  const count = this.messageCache.getCount(s.id);
2774
2806
  return `${s.projectPath} 有 ${count} 条新消息`;
2775
2807
  });
2776
- // 执行重启逻辑(共用于卡片回调和文本确认)
2777
2808
  const executeRestart = async () => {
2778
2809
  const suppressRealRestart = shouldSuppressRealRestart();
2779
2810
  let restartReplyContext = replyContext;
@@ -2814,6 +2845,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2814
2845
  logger.warn(`[System] Refusing restart without a valid explicit delivery route: channel=${channel} channelId=${channelId}`);
2815
2846
  return false;
2816
2847
  }
2848
+ const migrationRequirement = inspectDataMigrationRequirement(resolvePaths().root);
2849
+ if (migrationRequirement.required) {
2850
+ logger.warn(`[System] Refusing restart while data migration is pending: operations=${migrationRequirement.operationCount}`);
2851
+ return { code: 'DATA_MIGRATION_REQUIRED', operationCount: migrationRequirement.operationCount };
2852
+ }
2817
2853
  const restartInfo = {
2818
2854
  channel,
2819
2855
  channelId,
@@ -2892,6 +2928,13 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2892
2928
  }
2893
2929
  }
2894
2930
  const restarted = await executeRestart();
2931
+ if (typeof restarted === 'object' && restarted.code === 'DATA_MIGRATION_REQUIRED') {
2932
+ return {
2933
+ kind: 'command.error',
2934
+ text: `❌ 待处理的用户数据迁移(${restarted.operationCount} 项)阻止重启。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
2935
+ reason: restarted.code,
2936
+ };
2937
+ }
2895
2938
  if (!restarted) {
2896
2939
  return { kind: 'command.error', text: '❌ 无法确定重启通知的出站路由,请从可信会话上下文重试' };
2897
2940
  }
@@ -768,7 +768,17 @@ export function planDataMigration(root) {
768
768
  }
769
769
  catch { }
770
770
  const contactAudit = path.join(root, 'data', 'contact-book-audit.jsonl');
771
- if (fs.existsSync(contactAudit)) {
771
+ // The legacy audit file may be provisioned as an empty placeholder (for
772
+ // example by a protected-path sandbox). An empty source has nothing to
773
+ // partition and must not block daemon startup.
774
+ let hasContactAuditRecords = false;
775
+ try {
776
+ const stat = fs.statSync(contactAudit);
777
+ hasContactAuditRecords = stat.isFile()
778
+ && fs.readFileSync(contactAudit, 'utf8').trim().length > 0;
779
+ }
780
+ catch { }
781
+ if (hasContactAuditRecords) {
772
782
  const operationId = nextId('contact-audit');
773
783
  operations.push({ id: operationId, kind: 'contact-audit-partition', source: contactAudit, staging: path.join(stagingDir, operationId), status: 'planned' });
774
784
  }
@@ -246,6 +246,8 @@ const CATALOG = [
246
246
  description: 'agent 任务开始',
247
247
  fields: [
248
248
  { path: 'sessionId', type: 'string' },
249
+ { path: 'taskId', type: 'string', optional: true },
250
+ { path: 'generation', type: 'number', optional: true },
249
251
  { path: 'agentName', type: 'string', optional: true },
250
252
  { path: 'encrypt', type: 'boolean', optional: true },
251
253
  { path: 'chatmode', type: 'string', optional: true },
@@ -269,6 +271,8 @@ const CATALOG = [
269
271
  description: 'agent 任务完成',
270
272
  fields: [
271
273
  { path: 'sessionId', type: 'string' },
274
+ { path: 'taskId', type: 'string', optional: true },
275
+ { path: 'generation', type: 'number', optional: true },
272
276
  { path: 'channel', type: 'string' },
273
277
  { path: 'channelName', type: 'string', optional: true },
274
278
  { path: 'channelId', type: 'string' },
@@ -287,6 +291,8 @@ const CATALOG = [
287
291
  description: 'agent 任务失败',
288
292
  fields: [
289
293
  { path: 'sessionId', type: 'string' },
294
+ { path: 'taskId', type: 'string', optional: true },
295
+ { path: 'generation', type: 'number', optional: true },
290
296
  { path: 'error', type: 'string' },
291
297
  { path: 'errorType', type: 'string' },
292
298
  { path: 'terminalReason', type: 'string', optional: true },
@@ -300,6 +306,8 @@ const CATALOG = [
300
306
  description: 'agent 任务被中断',
301
307
  fields: [
302
308
  { path: 'sessionId', type: 'string' },
309
+ { path: 'taskId', type: 'string', optional: true },
310
+ { path: 'generation', type: 'number', optional: true },
303
311
  { path: 'reason', type: 'string', optional: true },
304
312
  { path: 'agentName', type: 'string', optional: true },
305
313
  ],
@@ -339,13 +347,17 @@ const CATALOG = [
339
347
  { path: 'eventPhase', type: 'string' },
340
348
  { path: 'isError', type: 'boolean', optional: true },
341
349
  { path: 'errorCode', type: 'string', optional: true },
350
+ { path: 'classificationMissing', type: 'boolean', optional: true },
342
351
  { path: 'agentName', type: 'string', optional: true },
343
352
  { path: 'callId', type: 'string', optional: true },
344
353
  { path: 'correlationId', type: 'string', optional: true },
354
+ { path: 'requestId', type: 'string', optional: true },
345
355
  { path: 'agentAid', type: 'string', optional: true },
346
356
  { path: 'permissionMode', type: 'string', optional: true },
347
357
  { path: 'decision', type: 'string' },
348
358
  { path: 'decisionSource', type: 'string' },
359
+ { path: 'policyCode', type: 'string', optional: true },
360
+ { path: 'reason', type: 'string', optional: true },
349
361
  { path: 'executed', type: 'boolean' },
350
362
  { path: 'executionState', type: 'string' },
351
363
  { path: 'timestamp', type: 'number', optional: true },
@@ -399,6 +411,21 @@ const CATALOG = [
399
411
  description: 'runner 开始压缩',
400
412
  fields: [{ path: 'sessionId', type: 'string' }],
401
413
  },
414
+ {
415
+ type: 'runner:proactive-reminder',
416
+ namespace: 'runner',
417
+ name: 'proactive-reminder',
418
+ description: '主动模式提醒已生成并请求注入模型上下文',
419
+ fields: [
420
+ { path: 'sessionId', type: 'string' },
421
+ { path: 'kind', type: 'string' },
422
+ { path: 'text', type: 'string' },
423
+ { path: 'queueLength', type: 'number', optional: true },
424
+ { path: 'toolCount', type: 'number', optional: true },
425
+ { path: 'injection', type: 'string' },
426
+ { path: 'timestamp', type: 'number', optional: true },
427
+ ],
428
+ },
402
429
  {
403
430
  type: 'runner:compact-complete',
404
431
  namespace: 'runner',
@@ -690,6 +717,11 @@ const CATALOG = [
690
717
  { path: 'runId', type: 'string' },
691
718
  { path: 'originTriggerId', type: 'string' },
692
719
  { path: 'reason', type: 'string' },
720
+ { path: 'reasonCode', type: 'string', optional: true },
721
+ { path: 'decisionSource', type: 'string', optional: true },
722
+ { path: 'executionState', type: 'string', optional: true },
723
+ { path: 'generation', type: 'number', optional: true },
724
+ { path: 'currentGeneration', type: 'number', optional: true },
693
725
  { path: 'targetChannel', type: 'string' },
694
726
  { path: 'targetChannelId', type: 'string' },
695
727
  { path: 'fireTime', type: 'number', optional: true },
@@ -266,8 +266,8 @@ export class HandoffRuntime {
266
266
  }
267
267
  }
268
268
  }
269
- buildPromptItem(message) {
270
- return this.buildPromptItems(message)[0] ?? null;
269
+ buildPromptItem(message, fallbackPeerRole) {
270
+ return this.buildPromptItems(message, fallbackPeerRole)[0] ?? null;
271
271
  }
272
272
  isDiscardedDelivery(message) {
273
273
  const delivery = message.handoffDelivery;
@@ -277,7 +277,7 @@ export class HandoffRuntime {
277
277
  const ids = delivery.handoffIds?.length ? delivery.handoffIds : [delivery.handoffId];
278
278
  return ids.length > 0 && ids.every(handoffId => this.store.get(selfAid, handoffId)?.state === 'discarded');
279
279
  }
280
- buildPromptItems(message) {
280
+ buildPromptItems(message, fallbackPeerRole) {
281
281
  const delivery = message.handoffDelivery;
282
282
  const selfAid = message.selfAID;
283
283
  if (!delivery || !selfAid)
@@ -310,8 +310,18 @@ export class HandoffRuntime {
310
310
  peerId: message.peerId,
311
311
  peerName: message.peerName,
312
312
  peerType: message.peerType,
313
+ peerRole: message.batchRole ?? message.resolvedIdentity?.role ?? fallbackPeerRole,
314
+ sameDevice: message.sameDevice,
315
+ sameNetwork: message.sameNetwork,
316
+ sameEgressIp: message.sameEgressIp,
317
+ encrypted: message.encrypted,
313
318
  content: message.content,
314
319
  timestamp: message.timestamp,
320
+ receivedAt: message.receivedAt,
321
+ gatewaySeq: message.gatewaySeq,
322
+ images: message.images,
323
+ mentions: message.mentions,
324
+ mentionAids: message.mentionAids,
315
325
  causation: message.causation,
316
326
  handoff: {
317
327
  kind: 'response_to_origin',
@@ -348,8 +358,18 @@ export class HandoffRuntime {
348
358
  peerId: message.peerId,
349
359
  peerName: message.peerName,
350
360
  peerType: message.peerType,
361
+ peerRole: message.batchRole ?? message.resolvedIdentity?.role ?? fallbackPeerRole,
362
+ sameDevice: message.sameDevice,
363
+ sameNetwork: message.sameNetwork,
364
+ sameEgressIp: message.sameEgressIp,
365
+ encrypted: message.encrypted,
351
366
  content: message.content,
352
367
  timestamp: message.timestamp,
368
+ receivedAt: message.receivedAt,
369
+ gatewaySeq: message.gatewaySeq,
370
+ images: message.images,
371
+ mentions: message.mentions,
372
+ mentionAids: message.mentionAids,
353
373
  causation: message.causation,
354
374
  handoff: {
355
375
  kind: 'request_to_target',
@@ -1,5 +1,5 @@
1
1
  import { logger } from '../../utils/logger.js';
2
- import { summarizeToolInput } from '../../utils/tool-summary.js';
2
+ import { summarizeToolInput, toolInputForDisplay } from '../../utils/tool-summary.js';
3
3
  import { getErrorMessage, isContextTooLongText } from '../../utils/error-utils.js';
4
4
  import { DEFAULT_FLUSH_DELAY_MS } from '../../types.js';
5
5
  export class IMRenderer {
@@ -34,6 +34,9 @@ export class IMRenderer {
34
34
  const correlatedEvent = event.type === 'tool_use' || event.type === 'tool_result'
35
35
  ? { ...event, correlationId: event.correlationId ?? event.callId }
36
36
  : event;
37
+ const permissionMode = typeof this.opts.permissionMode === 'function'
38
+ ? this.opts.permissionMode()
39
+ : this.opts.permissionMode;
37
40
  logger.event({
38
41
  source: 'runner',
39
42
  taskId: this.opts.envelope.taskId,
@@ -41,6 +44,7 @@ export class IMRenderer {
41
44
  channelId: this.opts.envelope.channelId,
42
45
  agentAid: this.opts.agentAid,
43
46
  agentName: this.opts.envelope.agentName,
47
+ ...(permissionMode ? { permissionMode } : {}),
44
48
  ...(correlatedEvent.type === 'tool_use' || correlatedEvent.type === 'tool_result'
45
49
  ? { correlationId: correlatedEvent.correlationId, toolName: correlatedEvent.name }
46
50
  : {}),
@@ -184,7 +188,7 @@ export class IMRenderer {
184
188
  kind: 'tool_call',
185
189
  call_id: callId || this.synthCallId(),
186
190
  name,
187
- arguments: input,
191
+ arguments: toolInputForDisplay(name, input),
188
192
  text: descText,
189
193
  });
190
194
  this.messageTimestamps.push(Date.now());
@@ -568,7 +572,7 @@ export class IMRenderer {
568
572
  text: desc,
569
573
  };
570
574
  if (event.input !== undefined)
571
- item.arguments = event.input;
575
+ item.arguments = toolInputForDisplay(event.name, event.input);
572
576
  return item;
573
577
  }
574
578
  case 'tool_result':