evolcore 0.0.2 → 0.0.3

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 (124) hide show
  1. package/CHANGELOG.md +44 -793
  2. package/dist/agents/claude-runner.js +197 -17
  3. package/dist/agents/codex-runner.js +46 -3
  4. package/dist/aun/outbox.js +8 -0
  5. package/dist/channels/aun.js +21 -4
  6. package/dist/channels/contact-bind-code.js +134 -0
  7. package/dist/channels/dingtalk.js +979 -149
  8. package/dist/channels/feishu.js +130 -54
  9. package/dist/channels/wecom-card.js +101 -0
  10. package/dist/channels/wecom-onboarding.js +82 -0
  11. package/dist/channels/wecom-state.js +191 -0
  12. package/dist/channels/wecom.js +755 -163
  13. package/dist/cli/agent-command.js +2 -1
  14. package/dist/cli/aun-commands.js +88 -33
  15. package/dist/cli/bench.js +2 -2
  16. package/dist/cli/contact.js +71 -0
  17. package/dist/cli/ctl-command.js +2 -2
  18. package/dist/cli/daemon-commands.js +77 -214
  19. package/dist/cli/handoff-command.js +2 -2
  20. package/dist/cli/help.js +9 -5
  21. package/dist/cli/index.js +132 -116
  22. package/dist/cli/init-channel.js +92 -97
  23. package/dist/cli/init.js +63 -26
  24. package/dist/cli/model.js +2 -1
  25. package/dist/cli/net-check.js +2 -2
  26. package/dist/cli/queue-command.js +30 -6
  27. package/dist/cli/raw-key-input.js +25 -0
  28. package/dist/cli/response.js +5 -6
  29. package/dist/cli/restart-monitor.js +25 -1
  30. package/dist/cli/stats.js +6 -4
  31. package/dist/cli/trigger-command.js +55 -15
  32. package/dist/cli/version.js +6 -1
  33. package/dist/config/builtin-role-templates.js +22 -10
  34. package/dist/config/builtin-roles.js +7 -1
  35. package/dist/config/config-manager.js +221 -17
  36. package/dist/config/contact-alias.js +68 -0
  37. package/dist/config/contact-book-store.js +454 -0
  38. package/dist/config/contact-book-v2-startup.js +35 -0
  39. package/dist/config/contact-book.js +156 -303
  40. package/dist/config/contact-operation-service.js +110 -0
  41. package/dist/config/peer-role-resolver.js +133 -54
  42. package/dist/config/role-ranks.js +18 -0
  43. package/dist/config/role-service.js +16 -19
  44. package/dist/config/role-store.js +16 -5
  45. package/dist/config/roles.js +10 -1
  46. package/dist/config-store.js +0 -2
  47. package/dist/core/auth/authorization-audit.js +10 -1
  48. package/dist/core/auth/operation-authorizer.js +2 -0
  49. package/dist/core/auth/operation-catalog.js +56 -0
  50. package/dist/core/command/command-handler.js +105 -10
  51. package/dist/core/command/connect-menu.js +374 -0
  52. package/dist/core/command/menu-handler.js +114 -16
  53. package/dist/core/command/role-menu.js +128 -29
  54. package/dist/core/command/slash-handler.js +28 -18
  55. package/dist/core/daemon-file-cache.js +12 -6
  56. package/dist/core/event-catalog.js +70 -0
  57. package/dist/core/evolagent-registry.js +0 -1
  58. package/dist/core/evolagent.js +20 -9
  59. package/dist/core/message/im-renderer.js +2 -0
  60. package/dist/core/message/message-bridge.js +79 -8
  61. package/dist/core/message/message-queue.js +10 -0
  62. package/dist/core/message/message-utils.js +8 -2
  63. package/dist/core/message/response-engine.js +269 -25
  64. package/dist/core/message/send-receipt.js +24 -0
  65. package/dist/core/message/stream-debouncer.js +11 -2
  66. package/dist/core/permission/tool-policy.js +47 -15
  67. package/dist/core/protected-paths.js +2 -0
  68. package/dist/core/session/session-fs-store.js +44 -3
  69. package/dist/core/session/session-manager.js +61 -5
  70. package/dist/index.js +62 -6
  71. package/dist/ipc.js +34 -5
  72. package/dist/stats/billing.js +20 -8
  73. package/dist/trigger/manager.js +3 -0
  74. package/dist/trigger/parser.js +77 -2
  75. package/dist/trigger/patch.js +8 -1
  76. package/dist/trigger/scheduler.js +3 -1
  77. package/dist/trigger/validation.js +13 -0
  78. package/dist/utils/aid-bind.js +43 -29
  79. package/dist/utils/instance-registry.js +14 -7
  80. package/dist/utils/log-writer.js +46 -0
  81. package/dist/utils/media-cache.js +4 -1
  82. package/dist/utils/model-prices.jsonl +6 -3
  83. package/dist/utils/restart-safety.js +31 -0
  84. package/dist/utils/system-memory.js +62 -0
  85. package/kits/docs/INDEX.md +2 -1
  86. package/kits/docs/evolcore/INDEX.md +5 -3
  87. package/kits/docs/evolcore/agent.md +9 -1
  88. package/kits/docs/evolcore/aid.md +5 -2
  89. package/kits/docs/evolcore/contact.md +57 -0
  90. package/kits/docs/evolcore/fs.md +9 -0
  91. package/kits/docs/evolcore/group.md +12 -3
  92. package/kits/docs/evolcore/model.md +4 -1
  93. package/kits/docs/evolcore/msg.md +9 -3
  94. package/kits/docs/evolcore/response.md +16 -21
  95. package/kits/docs/evolcore/rpc.md +2 -0
  96. package/kits/docs/evolcore/stats.md +15 -2
  97. package/kits/docs/evolcore/storage.md +1 -0
  98. package/kits/docs/evolcore/trigger.md +17 -2
  99. package/kits/eck_manifest.json +12 -0
  100. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  101. package/kits/schemas/_meta.json +7 -4
  102. package/kits/schemas/agent-config.schema.6.json +322 -0
  103. package/kits/schemas/contact-book.schema.2.json +43 -0
  104. package/kits/schemas/relation-config.schema.5.json +47 -0
  105. package/kits/schemas/role-config.schema.1.json +1 -0
  106. package/kits/schemas/role-registry.schema.1.json +2 -2
  107. package/kits/templates/roles/admin.json +1 -0
  108. package/kits/templates/roles/member.json +1 -0
  109. package/kits/templates/roles/owner.json +1 -0
  110. package/kits/templates/roles/visitor.json +1 -0
  111. package/kits/templates/system-fragments/commands.md +3 -1
  112. package/package.json +4 -4
  113. package/assets/brand/evolcore/README.md +0 -19
  114. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  115. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  116. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  117. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  118. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  119. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  120. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  121. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  122. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  123. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  124. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -719,7 +719,7 @@ export class AgentRunner {
719
719
  if (!permCtx?.adapter || !permCtx?.channelId) {
720
720
  return this.handleAskUserQuestionFallback(sessionId, input, questions, options);
721
721
  }
722
- const adapterHasInteractionPath = !!permCtx.adapter.send;
722
+ const adapterHasInteractionPath = permCtx.adapter.capabilities?.interaction === true;
723
723
  if (!adapterHasInteractionPath || !permCtx.interactionRouter) {
724
724
  return this.handleAskUserQuestionFallback(sessionId, input, questions, options);
725
725
  }
@@ -756,6 +756,7 @@ export class AgentRunner {
756
756
  },
757
757
  channelId: permCtx.channelId,
758
758
  sessionId,
759
+ initiatorId: permCtx.userId,
759
760
  };
760
761
  }
761
762
  else {
@@ -783,6 +784,7 @@ export class AgentRunner {
783
784
  },
784
785
  channelId: permCtx.channelId,
785
786
  sessionId,
787
+ initiatorId: permCtx.userId,
786
788
  };
787
789
  }
788
790
  let cardSent = false;
@@ -797,7 +799,10 @@ export class AgentRunner {
797
799
  replyContext: permCtx.replyContext,
798
800
  });
799
801
  const optionLines = q.options.map((o, idx) => ` ${idx + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
800
- const fallbackText = `💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optionLines}`;
802
+ const answerHint = q.multiSelect
803
+ ? '回复 /ask 1,2(多选用逗号分隔),或 /ask <自定义内容>'
804
+ : '回复 /ask 1,或 /ask <自定义内容>';
805
+ const fallbackText = `💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optionLines}\n\n${answerHint}`;
801
806
  const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
802
807
  cardSent = !!result;
803
808
  }
@@ -958,7 +963,9 @@ export class AgentRunner {
958
963
  command: 'ask',
959
964
  buttonArgMap: Object.fromEntries(q.options.map((_, i) => [`opt-${i}`, String(i + 1)])),
960
965
  acceptFreeText: true,
961
- freeTextHint: '或回复 /ask <自定义内容>',
966
+ freeTextHint: q.multiSelect
967
+ ? '多选请回复 /ask 1,2;也可回复 /ask <自定义内容>'
968
+ : '或回复 /ask <自定义内容>',
962
969
  },
963
970
  };
964
971
  await sendPrompt(renderActionAsText(interaction));
@@ -990,12 +997,25 @@ export class AgentRunner {
990
997
  finish({ kind: 'cancelled', reason: 'aborted' });
991
998
  };
992
999
  permCtx.interactionRouter.register(requestId, sessionId, (action) => {
993
- const num = parseInt(action.trim(), 10);
994
- if (num >= 1 && num <= q.options.length) {
995
- finish({ kind: 'answered', value: q.options[num - 1].label });
1000
+ const answer = action.trim();
1001
+ const selectionParts = answer.split(/[,,]/).map(part => part.trim());
1002
+ const selectedIndexes = selectionParts.map(part => Number(part));
1003
+ const hasValidNumericSelection = selectionParts.length > 0
1004
+ && selectionParts.every((part, index) => (/^\d+$/.test(part)
1005
+ && Number.isInteger(selectedIndexes[index])
1006
+ && selectedIndexes[index] >= 1
1007
+ && selectedIndexes[index] <= q.options.length));
1008
+ if (q.multiSelect && hasValidNumericSelection) {
1009
+ finish({
1010
+ kind: 'answered',
1011
+ value: [...new Set(selectedIndexes)].map(num => q.options[num - 1].label),
1012
+ });
1013
+ }
1014
+ else if (!q.multiSelect && hasValidNumericSelection && selectedIndexes.length === 1) {
1015
+ finish({ kind: 'answered', value: q.options[selectedIndexes[0] - 1].label });
996
1016
  }
997
1017
  else {
998
- finish({ kind: 'answered', value: action.trim() });
1018
+ finish({ kind: 'answered', value: answer });
999
1019
  }
1000
1020
  }, {
1001
1021
  initiatorId: permCtx.userId,
@@ -1181,7 +1201,7 @@ export class AgentRunner {
1181
1201
  };
1182
1202
  // 尝试发送交互卡片
1183
1203
  let cardSent = false;
1184
- if (permCtx.adapter?.send) {
1204
+ if (permCtx.adapter?.capabilities?.interaction === true) {
1185
1205
  // 发送计划内容:找 plans 目录中最新修改的 .md 文件
1186
1206
  if (sendPrompt) {
1187
1207
  try {
@@ -1283,10 +1303,20 @@ export class AgentRunner {
1283
1303
  * SDK 原始事件 → 标准 AgentEvent 转换
1284
1304
  * 所有 SDK 特有的事件类型引用封装在此方法内
1285
1305
  */
1286
- async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel) {
1306
+ async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false) {
1287
1307
  let lastSessionId;
1308
+ let hasTurnActivity = false;
1309
+ let ignoredPreTurnResult = false;
1310
+ let waitingForBackgroundCompletion = false;
1311
+ let liveBackgroundTasks = new Set();
1312
+ const unsettledTasks = new Set();
1313
+ const terminalTasks = new Set();
1314
+ const ambientTasks = new Set();
1315
+ const awaitedTaskNotifications = new Set();
1316
+ const taskMetadata = new Map();
1288
1317
  // tool_use_id → tool_name 映射,用于从 SDKUserMessage 的 tool_result 块中还原工具名
1289
1318
  const toolUseNames = new Map();
1319
+ const emittedToolResultIds = new Set();
1290
1320
  let turnCount = 0;
1291
1321
  const seenMessageIds = new Set();
1292
1322
  let lastModelCall;
@@ -1302,11 +1332,99 @@ export class AgentRunner {
1302
1332
  yield { type: 'session_id', sessionId: event.session_id };
1303
1333
  }
1304
1334
  if (event.type === 'user' && event.uuid === inputId) {
1335
+ hasTurnActivity = true;
1305
1336
  yield { type: 'input_accepted', inputId };
1306
1337
  }
1338
+ if (event.type === 'system' && event.subtype === 'task_started' && typeof event.task_id === 'string') {
1339
+ const taskKind = event.subagent_type || event.task_type === 'remote_agent'
1340
+ ? 'agent'
1341
+ : event.workflow_name || event.task_type === 'local_workflow'
1342
+ ? 'workflow'
1343
+ : 'background';
1344
+ taskMetadata.set(event.task_id, {
1345
+ taskKind,
1346
+ description: event.description,
1347
+ subagentType: event.subagent_type,
1348
+ taskType: event.task_type,
1349
+ });
1350
+ if (event.skip_transcript === true) {
1351
+ ambientTasks.add(event.task_id);
1352
+ unsettledTasks.delete(event.task_id);
1353
+ liveBackgroundTasks.delete(event.task_id);
1354
+ }
1355
+ else if (!terminalTasks.has(event.task_id)) {
1356
+ unsettledTasks.add(event.task_id);
1357
+ hasTurnActivity = true;
1358
+ }
1359
+ yield {
1360
+ type: 'task_started',
1361
+ taskId: event.task_id,
1362
+ taskKind,
1363
+ toolUseId: event.tool_use_id,
1364
+ description: event.description || event.task_id,
1365
+ subagentType: event.subagent_type,
1366
+ taskType: event.task_type,
1367
+ workflowName: event.workflow_name,
1368
+ prompt: event.prompt,
1369
+ skipTranscript: event.skip_transcript,
1370
+ };
1371
+ continue;
1372
+ }
1373
+ if (event.type === 'system' && event.subtype === 'background_tasks_changed') {
1374
+ const tasks = Array.isArray(event.tasks) ? event.tasks : [];
1375
+ liveBackgroundTasks = new Set(tasks
1376
+ .map((task) => task?.task_id)
1377
+ .filter((taskId) => (typeof taskId === 'string'
1378
+ && !terminalTasks.has(taskId)
1379
+ && !ambientTasks.has(taskId))));
1380
+ yield {
1381
+ type: 'background_tasks_changed',
1382
+ tasks: tasks.map((task) => ({
1383
+ taskId: task.task_id,
1384
+ taskKind: task.task_type === 'remote_agent'
1385
+ ? 'agent'
1386
+ : task.task_type === 'local_workflow'
1387
+ ? 'workflow'
1388
+ : taskMetadata.get(task.task_id)?.taskKind || 'background',
1389
+ taskType: task.task_type,
1390
+ description: task.description,
1391
+ })),
1392
+ };
1393
+ continue;
1394
+ }
1395
+ if (event.type === 'system' && event.subtype === 'task_notification' && typeof event.task_id === 'string') {
1396
+ const metadata = taskMetadata.get(event.task_id);
1397
+ const taskKind = metadata?.taskKind
1398
+ || (event.tool_use_id && toolUseNames.get(event.tool_use_id) === 'Agent' ? 'agent' : undefined)
1399
+ || (typeof event.summary === 'string' && /^Agent\b/.test(event.summary) ? 'agent' : 'background');
1400
+ terminalTasks.add(event.task_id);
1401
+ ambientTasks.delete(event.task_id);
1402
+ unsettledTasks.delete(event.task_id);
1403
+ liveBackgroundTasks.delete(event.task_id);
1404
+ awaitedTaskNotifications.delete(event.task_id);
1405
+ yield {
1406
+ type: 'task_notification',
1407
+ taskId: event.task_id,
1408
+ taskKind,
1409
+ toolUseId: event.tool_use_id,
1410
+ description: metadata?.description,
1411
+ subagentType: metadata?.subagentType,
1412
+ status: event.status || 'completed',
1413
+ outputFile: event.output_file,
1414
+ summary: event.summary,
1415
+ usage: event.usage ? {
1416
+ totalTokens: event.usage.total_tokens,
1417
+ toolUses: event.usage.tool_uses,
1418
+ durationMs: event.usage.duration_ms,
1419
+ } : undefined,
1420
+ skipTranscript: event.skip_transcript,
1421
+ };
1422
+ continue;
1423
+ }
1307
1424
  if (event.type === 'stream_event') {
1308
1425
  const streamEvent = event.event;
1309
1426
  if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
1427
+ hasTurnActivity = true;
1310
1428
  lastModelCall = {
1311
1429
  uuid: event.uuid,
1312
1430
  model: streamEvent.message.model,
@@ -1347,11 +1465,21 @@ export class AgentRunner {
1347
1465
  }
1348
1466
  // system: task_progress → task_progress
1349
1467
  if (event.type === 'system' && event.subtype === 'task_progress') {
1468
+ const metadata = typeof event.task_id === 'string' ? taskMetadata.get(event.task_id) : undefined;
1469
+ if (!event.task_id || !ambientTasks.has(event.task_id))
1470
+ hasTurnActivity = true;
1350
1471
  yield {
1351
1472
  type: 'task_progress',
1473
+ taskId: event.task_id,
1474
+ taskKind: metadata?.taskKind || (event.subagent_type ? 'agent' : undefined),
1475
+ toolUseId: event.tool_use_id,
1476
+ description: event.description,
1477
+ subagentType: event.subagent_type || metadata?.subagentType,
1352
1478
  summary: event.summary,
1353
- toolUses: event.tool_uses,
1354
- durationMs: event.duration_ms,
1479
+ lastToolName: event.last_tool_name,
1480
+ totalTokens: event.usage?.total_tokens,
1481
+ toolUses: event.usage?.tool_uses ?? event.tool_uses,
1482
+ durationMs: event.usage?.duration_ms ?? event.duration_ms,
1355
1483
  };
1356
1484
  }
1357
1485
  // system: session_state_changed → state_changed
@@ -1360,6 +1488,7 @@ export class AgentRunner {
1360
1488
  }
1361
1489
  // assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
1362
1490
  if (event.type === 'assistant' && event.message?.content) {
1491
+ hasTurnActivity = true;
1363
1492
  lastAssistantUuid = event.uuid ?? lastAssistantUuid;
1364
1493
  const msgId = event.message.id;
1365
1494
  if (!msgId || !seenMessageIds.has(msgId)) {
@@ -1410,6 +1539,8 @@ export class AgentRunner {
1410
1539
  const resultContent = typeof block.content === 'string'
1411
1540
  ? block.content
1412
1541
  : block.content != null ? JSON.stringify(block.content) : '';
1542
+ if (block.tool_use_id)
1543
+ emittedToolResultIds.add(block.tool_use_id);
1413
1544
  yield {
1414
1545
  type: 'tool_result',
1415
1546
  name: toolName,
@@ -1423,15 +1554,22 @@ export class AgentRunner {
1423
1554
  }
1424
1555
  // result → complete(含 permission_denials 提取)
1425
1556
  if (event.type === 'result') {
1426
- // 先发出被拒绝的权限事件
1557
+ // Most SDK streams surface denials as user-message tool_result blocks.
1558
+ // The terminal summary is only a fallback for incomplete streams.
1427
1559
  if (Array.isArray(event.permission_denials)) {
1428
1560
  for (const denial of event.permission_denials) {
1561
+ const callId = typeof denial.tool_use_id === 'string' ? denial.tool_use_id : undefined;
1562
+ if (callId && emittedToolResultIds.has(callId))
1563
+ continue;
1564
+ if (callId)
1565
+ emittedToolResultIds.add(callId);
1429
1566
  yield {
1430
1567
  type: 'tool_result',
1431
1568
  name: denial.tool_name || '',
1432
1569
  result: '',
1433
1570
  isError: true,
1434
- error: `权限被拒绝: ${denial.tool_name}`,
1571
+ error: `权限被拒绝: ${denial.tool_name || 'unknown tool'}`,
1572
+ callId,
1435
1573
  };
1436
1574
  }
1437
1575
  }
@@ -1502,7 +1640,7 @@ export class AgentRunner {
1502
1640
  // 降级:无逐次数据,写一条累计行
1503
1641
  modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, contextUsage: contextUsageForCall(u), degraded: true }];
1504
1642
  }
1505
- yield {
1643
+ const completeEvent = {
1506
1644
  type: 'complete',
1507
1645
  result: cleanResult,
1508
1646
  subtype: event.subtype,
@@ -1520,10 +1658,53 @@ export class AgentRunner {
1520
1658
  modelCalls,
1521
1659
  assistantUuid: lastAssistantUuid,
1522
1660
  };
1523
- // result SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
1661
+ // Fatal results belong to the query transport itself and must terminate
1662
+ // even when the current input was not echoed back by the CLI.
1663
+ if (event.is_error) {
1664
+ yield { ...completeEvent, queryFinal: true };
1665
+ return;
1666
+ }
1667
+ if (isResuming && !hasTurnActivity && !ignoredPreTurnResult) {
1668
+ ignoredPreTurnResult = true;
1669
+ logger.info(`[AgentRunner] Ignoring pre-turn result while resuming session=${sessionId} input=${inputId}`);
1670
+ continue;
1671
+ }
1672
+ const hasLiveTasks = liveBackgroundTasks.size > 0 || unsettledTasks.size > 0;
1673
+ if (hasLiveTasks) {
1674
+ waitingForBackgroundCompletion = true;
1675
+ for (const taskId of liveBackgroundTasks)
1676
+ awaitedTaskNotifications.add(taskId);
1677
+ for (const taskId of unsettledTasks)
1678
+ awaitedTaskNotifications.add(taskId);
1679
+ yield { ...completeEvent, queryFinal: false };
1680
+ hasTurnActivity = false;
1681
+ ignoredPreTurnResult = false;
1682
+ continue;
1683
+ }
1684
+ // background_tasks_changed may report an empty set before the matching
1685
+ // task_notification. Do not close in that gap: the notification wakes the
1686
+ // parent agent for the final synthesis turn.
1687
+ if (waitingForBackgroundCompletion && awaitedTaskNotifications.size > 0) {
1688
+ yield { ...completeEvent, queryFinal: false };
1689
+ hasTurnActivity = false;
1690
+ ignoredPreTurnResult = false;
1691
+ continue;
1692
+ }
1693
+ yield { ...completeEvent, queryFinal: true };
1524
1694
  return;
1525
1695
  }
1526
1696
  }
1697
+ if (waitingForBackgroundCompletion) {
1698
+ yield {
1699
+ type: 'complete',
1700
+ isError: true,
1701
+ subtype: 'protocol_incomplete',
1702
+ terminalReason: 'background_tasks_incomplete',
1703
+ protocolIncompleteReason: 'background_tasks_incomplete',
1704
+ errors: ['Claude SDK stream ended before background tasks produced a final parent result'],
1705
+ queryFinal: true,
1706
+ };
1707
+ }
1527
1708
  }
1528
1709
  catch (err) {
1529
1710
  // 子进程崩溃(如 exited with code 1)时,把缓冲的 stderr 打出来还原真实原因。
@@ -2107,7 +2288,6 @@ export class AgentRunner {
2107
2288
  logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
2108
2289
  logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
2109
2290
  msgStream.push(prompt, images, inputId);
2110
- msgStream.end();
2111
2291
  sdkStream = createQuery(msgStream);
2112
2292
  }
2113
2293
  else {
@@ -2124,7 +2304,7 @@ export class AgentRunner {
2124
2304
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
2125
2305
  }
2126
2306
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
2127
- const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel);
2307
+ const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId);
2128
2308
  const self = this;
2129
2309
  return (async function* () {
2130
2310
  try {
@@ -704,11 +704,13 @@ export class CodexRunner {
704
704
  controller.signal.addEventListener('abort', () => queue.end(), { once: true });
705
705
  const state = {
706
706
  threadId,
707
+ model: callModel,
707
708
  streamedAgentMessageIds: new Set(),
708
709
  agentMessageDeltaText: new Map(),
709
710
  completedItemIds: new Set(),
710
711
  emittedEditCallIds: new Set(),
711
712
  completedTurnIds: new Set(),
713
+ openToolCalls: new Map(),
712
714
  };
713
715
  const unsubscribe = appServer.onNotification(notification => {
714
716
  // 仅从 turn/started 锁定权威 turnId — resume 时会有上一轮 turn 的残留通知
@@ -2083,7 +2085,10 @@ export class CodexRunner {
2083
2085
  break;
2084
2086
  }
2085
2087
  case 'item/started': {
2086
- yield* this.mapAppServerItemStarted(params.item, state);
2088
+ for (const event of this.mapAppServerItemStarted(params.item, state)) {
2089
+ this.trackAppServerToolLifecycle(event, state);
2090
+ yield event;
2091
+ }
2087
2092
  break;
2088
2093
  }
2089
2094
  case 'item/agentMessage/delta': {
@@ -2099,11 +2104,17 @@ export class CodexRunner {
2099
2104
  const item = params.item;
2100
2105
  if (item?.id)
2101
2106
  state.completedItemIds.add(item.id);
2102
- yield* this.mapAppServerItemCompleted(item, state);
2107
+ for (const event of this.mapAppServerItemCompleted(item, state)) {
2108
+ this.trackAppServerToolLifecycle(event, state);
2109
+ yield event;
2110
+ }
2103
2111
  break;
2104
2112
  }
2105
2113
  case 'item/fileChange/patchUpdated': {
2106
- yield* this.mapAppServerFileChangePatchUpdated(params, state);
2114
+ for (const event of this.mapAppServerFileChangePatchUpdated(params, state)) {
2115
+ this.trackAppServerToolLifecycle(event, state);
2116
+ yield event;
2117
+ }
2107
2118
  break;
2108
2119
  }
2109
2120
  case 'turn/plan/updated': {
@@ -2131,6 +2142,7 @@ export class CodexRunner {
2131
2142
  if (turnId)
2132
2143
  state.completedTurnIds.add(turnId);
2133
2144
  this.activeTurns.delete(sessionId);
2145
+ yield* this.reconcileOpenAppServerToolCalls(state, turnId);
2134
2146
  if (turn.status === 'failed' && turn.error?.message) {
2135
2147
  if (isRetryableError(new Error(turn.error.message))) {
2136
2148
  throw new Error(turn.error.message);
@@ -2154,6 +2166,36 @@ export class CodexRunner {
2154
2166
  }
2155
2167
  }
2156
2168
  }
2169
+ trackAppServerToolLifecycle(event, state) {
2170
+ if (event.type !== 'tool_use' && event.type !== 'tool_result')
2171
+ return;
2172
+ if (!event.callId)
2173
+ return;
2174
+ if (event.type === 'tool_use') {
2175
+ state.openToolCalls.set(event.callId, event.name);
2176
+ }
2177
+ else if (event.type === 'tool_result') {
2178
+ state.openToolCalls.delete(event.callId);
2179
+ }
2180
+ }
2181
+ *reconcileOpenAppServerToolCalls(state, turnId) {
2182
+ if (state.openToolCalls.size === 0)
2183
+ return;
2184
+ const callIds = [...state.openToolCalls.keys()];
2185
+ logger.warn(`[CodexRunner] Turn completed with ${callIds.length} open tool call(s): ` +
2186
+ `thread=${state.threadId} turn=${turnId || state.turnId || 'unknown'} calls=${callIds.join(',')}`);
2187
+ for (const [callId, name] of state.openToolCalls) {
2188
+ yield {
2189
+ type: 'tool_result',
2190
+ name,
2191
+ result: '',
2192
+ isError: true,
2193
+ error: 'Codex app-server ended the turn without an item/completed notification',
2194
+ callId,
2195
+ };
2196
+ }
2197
+ state.openToolCalls.clear();
2198
+ }
2157
2199
  *mapAppServerItemStarted(item, state) {
2158
2200
  if (!item)
2159
2201
  return;
@@ -2458,6 +2500,7 @@ export class CodexRunner {
2458
2500
  costUsd: this.pickNumber(turn.costUsd, turn.totalCostUsd, turn.total_cost_usd),
2459
2501
  sessionTitle: typeof turn.sessionTitle === 'string' ? turn.sessionTitle : typeof turn.session_title === 'string' ? turn.session_title : undefined,
2460
2502
  numTurns: this.pickNumber(turn.numTurns, turn.num_turns),
2503
+ model: state.model,
2461
2504
  tokenUsage,
2462
2505
  contextUsage,
2463
2506
  };
@@ -158,3 +158,11 @@ export function hasPending(aid) {
158
158
  return false;
159
159
  }
160
160
  }
161
+ /**
162
+ * 当前 outbox 中待发送条目数。用于诊断发送管线堵塞:depth 持续增长说明
163
+ * message.send 出队速度跟不上入队速度(网关慢 / 链路抖动 / 卡片洪泛),
164
+ * 是命令「回复慢/无回复」的先行指标。
165
+ */
166
+ export function pendingCount(aid) {
167
+ return load(aid).length;
168
+ }
@@ -195,25 +195,37 @@ export class AUNChannel {
195
195
  */
196
196
  async callAndTrace(method, params, opts) {
197
197
  this.trace('OUT', method, params);
198
+ // RPC 往返计时:区分「网关慢」与「本地队列堵塞」。message.send/group.send 的
199
+ // duration 若普遍逼近 SDK 的 rpc 超时(默认 10s),说明发送管线已饱和或链路抖动,
200
+ // 是 /evolhelp 等命令「回复慢/无回复」的直接可观测信号。
201
+ const rpcStart = Date.now();
202
+ // SLOW_RPC_WARN_MS 需明显低于 SDK 默认 10s 超时,以便在真正超时前提前告警。
203
+ const SLOW_RPC_WARN_MS = 3000;
198
204
  try {
199
205
  const result = await this.client.call(method, params);
206
+ const durationMs = Date.now() - rpcStart;
200
207
  if (!opts?.silentOk) {
201
208
  const r = result;
202
209
  const snap = r && typeof r === 'object'
203
- ? { message_id: r.message_id, ok: r.ok, thought_id: r.thought_id }
204
- : undefined;
205
- this.trace('OUT', `${method}.ok`, snap ?? {});
210
+ ? { message_id: r.message_id, ok: r.ok, thought_id: r.thought_id, duration_ms: durationMs }
211
+ : { duration_ms: durationMs };
212
+ this.trace('OUT', `${method}.ok`, snap);
213
+ }
214
+ if (durationMs >= SLOW_RPC_WARN_MS) {
215
+ logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms (发送管线可能饱和或链路抖动)`);
206
216
  }
207
217
  return result;
208
218
  }
209
219
  catch (e) {
220
+ const durationMs = Date.now() - rpcStart;
210
221
  this.trace('OUT', `${method}.error`, {
211
222
  error: e?.message ?? String(e),
212
223
  code: e?.code,
213
224
  name: e?.name,
225
+ duration_ms: durationMs,
214
226
  });
215
227
  if (!opts?.silentError) {
216
- logger.warn(`${this.logPrefix()} rpc ${method} failed: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
228
+ logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
217
229
  }
218
230
  throw e;
219
231
  }
@@ -2752,6 +2764,11 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2752
2764
  context,
2753
2765
  });
2754
2766
  logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
2767
+ // 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
2768
+ const backlog = outbox.pendingCount(this.config.aid);
2769
+ if (backlog >= 5) {
2770
+ logger.warn(`${this.logPrefix()} Outbox backlog=${backlog} channel=${channelId} (发送管线堵塞,回复将延迟)`);
2771
+ }
2755
2772
  if (!this.connected || !this.client) {
2756
2773
  logger.warn(`${this.logPrefix()} Not connected, message queued in outbox (id=${entry.id}). Triggering reconnect.`);
2757
2774
  if (!this.reconnectTimer && !this.client) {
@@ -0,0 +1,134 @@
1
+ import { randomInt } from 'crypto';
2
+ import { isValidAid } from '../aun/aid/validation.js';
3
+ import { bindContactAlias } from '../config/contact-book.js';
4
+ export const CONTACT_BIND_CODE_TTL_MS = 10 * 60 * 1000;
5
+ export const CONTACT_BIND_MAX_FAILED_ATTEMPTS = 5;
6
+ /** In-memory, one-time channel identity proof used before writing a Contact alias. */
7
+ export class ContactBindCodeRegistry {
8
+ options;
9
+ pending = new Map();
10
+ constructor(options) {
11
+ this.options = options;
12
+ }
13
+ register(req) {
14
+ const selfAid = String(req.selfAid || '').trim();
15
+ const channelName = String(req.channelName || '').trim();
16
+ const primaryId = String(req.primaryId || '').trim();
17
+ const now = req.now ?? Date.now();
18
+ const code = req.code ? String(req.code).trim() : generateBindCode();
19
+ if (!isValidAid(selfAid))
20
+ return { ok: false, error: `invalid selfAid: ${req.selfAid}` };
21
+ if (!channelName)
22
+ return { ok: false, error: 'missing channelName' };
23
+ if (!isValidAid(primaryId))
24
+ return { ok: false, error: `invalid primaryId: ${req.primaryId}` };
25
+ if (!/^\d{6}$/.test(code))
26
+ return { ok: false, error: 'binding code must be 6 digits' };
27
+ const key = pendingBindKey(selfAid, channelName);
28
+ const replaced = this.pending.has(key);
29
+ const item = {
30
+ selfAid,
31
+ channelName,
32
+ primaryId,
33
+ code,
34
+ createdAt: now,
35
+ expiresAt: now + CONTACT_BIND_CODE_TTL_MS,
36
+ failedAttempts: 0,
37
+ maxFailedAttempts: CONTACT_BIND_MAX_FAILED_ATTEMPTS,
38
+ };
39
+ this.pending.set(key, item);
40
+ return { ok: true, code, expiresAt: item.expiresAt, replaced };
41
+ }
42
+ handle(ctx) {
43
+ if (ctx.channelType !== this.options.channelType)
44
+ return { handled: false };
45
+ const selfAid = String(ctx.selfAid || '').trim();
46
+ const channelName = String(ctx.channelName || '').trim();
47
+ if (!selfAid || !channelName)
48
+ return { handled: false };
49
+ const key = pendingBindKey(selfAid, channelName);
50
+ const item = this.pending.get(key);
51
+ if (!item)
52
+ return { handled: false };
53
+ const now = ctx.now ?? Date.now();
54
+ if (now >= item.expiresAt) {
55
+ this.pending.delete(key);
56
+ return {
57
+ handled: true,
58
+ status: 'expired',
59
+ remainingAttempts: 0,
60
+ reply: `${this.options.displayName}身份绑定码已超时,本次绑定失败。请重新执行 ${this.options.initCommand} 并再次绑定。`,
61
+ };
62
+ }
63
+ if (ctx.chatType !== 'private')
64
+ return { handled: false };
65
+ const input = String(ctx.content ?? '').trim();
66
+ if (!/^\d{6}$/.test(input)) {
67
+ return {
68
+ handled: true,
69
+ status: 'format',
70
+ reply: '请直接发送 6 位数字绑定码。',
71
+ remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
72
+ };
73
+ }
74
+ if (input !== item.code) {
75
+ item.failedAttempts += 1;
76
+ const remaining = Math.max(0, item.maxFailedAttempts - item.failedAttempts);
77
+ if (remaining === 0) {
78
+ this.pending.delete(key);
79
+ return {
80
+ handled: true,
81
+ status: 'failed',
82
+ remainingAttempts: 0,
83
+ reply: `绑定码无效,本次${this.options.displayName}身份绑定失败。请重新执行 ${this.options.initCommand} 并再次绑定。`,
84
+ };
85
+ }
86
+ return {
87
+ handled: true,
88
+ status: 'wrong-code',
89
+ remainingAttempts: remaining,
90
+ reply: '绑定码错误,请重新发送 6 位数字绑定码。',
91
+ };
92
+ }
93
+ const actorId = String(ctx.actorId || '').trim();
94
+ if (!actorId) {
95
+ return {
96
+ handled: true,
97
+ status: 'missing-actor',
98
+ remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
99
+ reply: `无法识别当前${this.options.displayName}发送者身份,未建立绑定。请重新发送绑定码或重新执行绑定流程。`,
100
+ };
101
+ }
102
+ try {
103
+ bindContactAlias(item.selfAid, item.primaryId, this.options.channelType, actorId, item.channelName);
104
+ this.pending.delete(key);
105
+ return {
106
+ handled: true,
107
+ status: 'bound',
108
+ reply: `${this.options.displayName}身份绑定成功:${item.channelName}:${encodeURIComponent(actorId)} -> ${item.primaryId}`,
109
+ };
110
+ }
111
+ catch (error) {
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ return {
114
+ handled: true,
115
+ status: 'write-failed',
116
+ remainingAttempts: item.maxFailedAttempts - item.failedAttempts,
117
+ reply: `${this.options.displayName}身份绑定写入失败:${message}`,
118
+ };
119
+ }
120
+ }
121
+ get(selfAid, channelName) {
122
+ const item = this.pending.get(pendingBindKey(selfAid, channelName));
123
+ return item ? { ...item } : null;
124
+ }
125
+ clear() {
126
+ this.pending.clear();
127
+ }
128
+ }
129
+ function generateBindCode() {
130
+ return String(randomInt(0, 1_000_000)).padStart(6, '0');
131
+ }
132
+ function pendingBindKey(selfAid, channelName) {
133
+ return `${String(selfAid || '').trim()}\u0000${String(channelName || '').trim()}`;
134
+ }