evolcore 0.0.24 → 0.0.26

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/CHANGELOG.md +59 -0
  2. package/bin/ec.js +12 -1
  3. package/bin/install-codex-managed-hooks.mjs +28 -1
  4. package/dist/agents/baseagent.js +96 -46
  5. package/dist/agents/claude-runner.js +54 -13
  6. package/dist/agents/codex/app-server-client.js +13 -5
  7. package/dist/agents/codex/catalog.js +1 -1
  8. package/dist/agents/codex/diagnostics.js +34 -1
  9. package/dist/agents/codex-runner.js +128 -42
  10. package/dist/agents/ecagent-runner.js +14 -18
  11. package/dist/agents/gemini-runner.js +24 -9
  12. package/dist/agents/runner-types.js +25 -17
  13. package/dist/aun/aid/agentmd.js +135 -10
  14. package/dist/aun/aid/client.js +1 -1
  15. package/dist/aun/aid/identity.js +1 -1
  16. package/dist/aun/aid/index.js +2 -1
  17. package/dist/aun/aid/managed-operation.js +53 -1
  18. package/dist/aun/aid/store.js +17 -13
  19. package/dist/aun/menu-client.js +74 -0
  20. package/dist/aun/msg/group.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +1 -1
  22. package/dist/aun/msg/p2p.js +6 -1
  23. package/dist/aun/msg/upload.js +30 -22
  24. package/dist/aun/storage/client.js +4 -1
  25. package/dist/aun/storage/index.js +1 -1
  26. package/dist/channels/aun.js +329 -94
  27. package/dist/cli/agent-command.js +125 -5
  28. package/dist/cli/agent-create.js +151 -11
  29. package/dist/cli/agent.js +119 -6
  30. package/dist/cli/aun-commands.js +74 -7
  31. package/dist/cli/config.js +27 -8
  32. package/dist/cli/contact.js +63 -6
  33. package/dist/cli/daemon-commands.js +90 -25
  34. package/dist/cli/diag.js +31 -0
  35. package/dist/cli/handoff-command.js +35 -3
  36. package/dist/cli/index.js +7 -1
  37. package/dist/cli/managed-command-guard.js +3 -0
  38. package/dist/cli/menu-client.js +38 -23
  39. package/dist/cli/restart-monitor.js +57 -10
  40. package/dist/config/builtin-roles.js +2 -0
  41. package/dist/config/config-manager.js +15 -0
  42. package/dist/config/config-operation-service.js +165 -3
  43. package/dist/config/contact-operation-service.js +96 -7
  44. package/dist/config/resolved-config-op.js +66 -8
  45. package/dist/core/agent-application-service.js +2 -1
  46. package/dist/core/auth/authorization-audit.js +20 -12
  47. package/dist/core/auth/operation-authorizer.js +87 -8
  48. package/dist/core/auth/operation-catalog.js +51 -4
  49. package/dist/core/auth/remote-access-store.js +40 -1
  50. package/dist/core/bootstrap-service.js +1 -1
  51. package/dist/core/command/agent-control.js +54 -25
  52. package/dist/core/command/cli-intent-parser.js +18 -0
  53. package/dist/core/command/command-handler.js +152 -20
  54. package/dist/core/command/diag-service.js +118 -0
  55. package/dist/core/command/menu-catalog.js +13 -3
  56. package/dist/core/command/menu-handler.js +88 -31
  57. package/dist/core/command/menu-protocol.js +6 -0
  58. package/dist/core/command/menu-token-store.js +41 -8
  59. package/dist/core/command/slash-gate.js +34 -4
  60. package/dist/core/command/slash-handler.js +77 -12
  61. package/dist/core/event-catalog.js +5 -0
  62. package/dist/core/evolagent.js +3 -3
  63. package/dist/core/handoff/runtime.js +205 -27
  64. package/dist/core/handoff/store.js +193 -4
  65. package/dist/core/message/im-renderer.js +72 -21
  66. package/dist/core/message/message-bridge.js +69 -9
  67. package/dist/core/message/message-queue.js +26 -0
  68. package/dist/core/message/message-utils.js +1 -0
  69. package/dist/core/message/response-engine.js +48 -23
  70. package/dist/core/model/catalog-service.js +79 -49
  71. package/dist/core/model/context-window-suffix.js +33 -0
  72. package/dist/core/model/model-catalog.js +1 -1
  73. package/dist/core/model/model-permission.js +12 -3
  74. package/dist/core/permission/ec-command-parser.js +51 -12
  75. package/dist/core/permission/sandbox-runtime.js +36 -14
  76. package/dist/core/permission/tool-policy.js +47 -7
  77. package/dist/index.js +195 -31
  78. package/dist/ipc.js +54 -2
  79. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  80. package/dist/stats/billing.js +41 -15
  81. package/dist/stats/price-resolver.js +28 -10
  82. package/dist/utils/avatar-upload.js +282 -23
  83. package/dist/utils/cross-platform.js +30 -1
  84. package/dist/utils/error-utils.js +21 -0
  85. package/dist/utils/evolcore-version.js +129 -0
  86. package/dist/utils/logger.js +3 -1
  87. package/dist/utils/model-prices.jsonl +3 -3
  88. package/dist/utils/npm-ops.js +176 -35
  89. package/kits/docs/context-assembly.md +1 -0
  90. package/kits/docs/evolcore/INDEX.md +3 -1
  91. package/kits/docs/evolcore/agent-create.md +30 -11
  92. package/kits/docs/evolcore/agent.md +18 -8
  93. package/kits/docs/evolcore/aid.md +6 -4
  94. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
  95. package/kits/docs/evolcore/config.md +7 -0
  96. package/kits/docs/evolcore/contact.md +12 -5
  97. package/kits/docs/evolcore/menu-client.md +11 -3
  98. package/kits/docs/evolcore/msg.md +1 -0
  99. package/kits/docs/prompt-loading-architecture.md +1 -0
  100. package/kits/docs/venues/group.md +12 -0
  101. package/kits/eck_manifest.json +12 -0
  102. package/kits/rules/03-identity.md +3 -1
  103. package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
  104. package/kits/templates/roles/admin.json +14 -2
  105. package/kits/templates/roles/member.json +3 -2
  106. package/kits/templates/roles/visitor.json +4 -3
  107. package/kits/templates/system-fragments/bootstrap.md +16 -5
  108. package/kits/templates/system-fragments/channel.md +1 -1
  109. package/kits/templates/system-fragments/session.md +1 -1
  110. package/package.json +6 -4
  111. package/skills/eclink/SKILL.md +0 -84
  112. package/skills/eclink/agents/openai.yaml +0 -4
@@ -28,7 +28,7 @@ import { isSystemControlChannel } from '../system-channels.js';
28
28
  import { launchMonitorAndConfirm, resolveMonitorLaunch, cleanupMonitorStage } from '../../utils/cross-platform.js';
29
29
  import { scanInstances } from '../../utils/instance-registry.js';
30
30
  import { inspectDataMigrationRequirement } from '../data-migration.js';
31
- import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, probeSessionActivity, } from './slash-gate.js';
31
+ import { guardIdleCommand, guardGroupSlashCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, probeSessionActivity, } from './slash-gate.js';
32
32
  const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
33
33
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
34
34
  function defaultSlashChatmode(field) {
@@ -368,7 +368,7 @@ async function getGitWorkingDirInfo(projectPath) {
368
368
  return null; // git 命令失败时静默返回 null
369
369
  }
370
370
  }
371
- export async function handleSlashCommand(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, overrideSubject, replyContext) {
371
+ export async function handleSlashCommand(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, overrideSubject, replyContext, applyGroupSlashPolicy = false) {
372
372
  // 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
373
373
  // 不应覆盖 session 中已有的正确值
374
374
  if (source === 'card-trigger')
@@ -434,9 +434,18 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
434
434
  logger.warn(`[CommandHandler] topic session lookup failed: thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
435
435
  }
436
436
  }
437
- const activeChatType = topicAuthSession?.chatType
437
+ const activeChatType = (applyGroupSlashPolicy
438
+ ? replyContext?.delivery?.chatType || overrideSubject?.chatType || chatType
439
+ : undefined)
440
+ || topicAuthSession?.chatType
438
441
  || activeSession?.chatType
439
442
  || (chatType === 'group' ? 'group' : 'private');
443
+ const groupSlash = applyGroupSlashPolicy && activeChatType === 'group';
444
+ if (applyGroupSlashPolicy) {
445
+ const groupGuard = guardGroupSlashCommand(normalizedContent, activeChatType);
446
+ if (groupGuard)
447
+ return groupGuard;
448
+ }
440
449
  const getExistingSessionForCommand = async () => {
441
450
  if (threadId) {
442
451
  if (topicAuthSessionLookupFailed)
@@ -485,7 +494,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
485
494
  || normalizedContent === '/upgrade'
486
495
  || normalizedContent === '/reload'
487
496
  || normalizedContent.startsWith('/reload ')
488
- || normalizedContent === '/pair';
497
+ || normalizedContent === '/pair'
498
+ || /^\/diag(?:\s|$)/.test(normalizedContent);
489
499
  const roleGuard = guardRoleCommand(normalizedContent, activeChatType, isAdmin || (isDaemonOwner && isProcessLevelSlash));
490
500
  if (roleGuard)
491
501
  return roleGuard;
@@ -617,6 +627,30 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
617
627
  };
618
628
  // /help 命令不需要会话
619
629
  if (normalizedContent === '/help') {
630
+ if (groupSlash) {
631
+ const lines = [
632
+ '群聊可用命令:',
633
+ ' /help - 显示帮助',
634
+ ' /status - 显示会话状态',
635
+ ' /check - 检查 Agent 健康',
636
+ ' /model [current|list|check] - 查询模型或检查网关',
637
+ ' /effort - 查看推理强度',
638
+ ' /baseagent - 查看 Agent 后端',
639
+ ' /perm - 查看权限模式',
640
+ ' /mentionmode - 查看群聊 @ 处理模式',
641
+ ' /ask <选项> - 回答待处理问题',
642
+ ' /pause - 在下一次工具调用前暂停任务',
643
+ ' /resume - 继续已暂停的任务',
644
+ ' /stop - 中断当前任务',
645
+ ...(isAdmin ? [
646
+ ' /compact - 压缩会话上下文',
647
+ ...(threadId ? [' /renew - 轮换当前话题的后端会话'] : []),
648
+ ] : []),
649
+ '',
650
+ '模型、推理强度、后端、权限与 @ 模式在群聊中仅支持查询。',
651
+ ];
652
+ return { kind: 'command.result', text: lines.join('\n') };
653
+ }
620
654
  const canReadModel = canAccessSlashOperation('model.current', 'relation')
621
655
  || canAccessSlashOperation('model.list', 'relation');
622
656
  const canUseModel = canAccessSlashOperation('model.use', 'relation');
@@ -633,7 +667,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
633
667
  const appendProcessCommands = (lines) => {
634
668
  if (!isDaemonOwner)
635
669
  return;
636
- lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置', ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)', ' /pair - 获取远程控制配对码');
670
+ lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置', ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)', ' /pair - 获取远程控制配对码', ' /diag <问题描述> - 邀请远程诊断;/diag stop 撤销授权');
637
671
  };
638
672
  if (!isAdmin && activeChatType === 'group') {
639
673
  const lines = [
@@ -773,6 +807,14 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
773
807
  ];
774
808
  return { kind: 'command.result', text: lines.join('\n') };
775
809
  }
810
+ if (/^\/diag(?:\s|$)/.test(normalizedContent)) {
811
+ if (!isDaemonOwner || !userId || !this.diagService)
812
+ return { kind: 'command.error', text: '❌ /diag 需要本机 DaemonOwner 和可用的诊断入口' };
813
+ const problem = content.trim().slice(5).trim();
814
+ const result = await this.diagService.execute(problem.toLowerCase() === 'stop'
815
+ ? { action: 'stop' } : { action: 'start', problem }, userId);
816
+ return { kind: result.ok ? 'command.result' : 'command.error', text: result.message };
817
+ }
776
818
  // /pair is also recognized on an Agent channel so it cannot fall through
777
819
  // into model input. The daemon control channel has its own fast path; this
778
820
  // branch preserves the same daemon-owner boundary for ordinary AUN chats.
@@ -792,6 +834,26 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
792
834
  }
793
835
  // /evolhelp 命令:返回 JSON 格式的命令列表(供程序解析)
794
836
  if (normalizedContent === '/evolhelp') {
837
+ if (groupSlash) {
838
+ const commands = [
839
+ { command: '/help', description: '显示帮助信息', category: '帮助', roles: ['visitor', 'member', 'admin', 'owner'] },
840
+ { command: '/status', description: '显示会话状态', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] },
841
+ { command: '/check', description: '检查 EvolAgent 实例健康', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] },
842
+ { command: '/model', args: '[current|list|check]', description: '查询模型或检查模型网关', category: 'Agent 与模型', roles: [identity.role] },
843
+ { command: '/effort', description: '查看推理强度', category: 'Agent 与模型', roles: [identity.role] },
844
+ { command: '/baseagent', description: '查看 Agent 后端', category: 'Agent 与模型', roles: [identity.role] },
845
+ { command: '/perm', description: '查看当前权限模式', category: '权限管理', roles: [identity.role] },
846
+ { command: '/mentionmode', description: '查看群聊 @ 处理模式', category: '聊天设置', roles: [identity.role] },
847
+ { command: '/ask', args: '<选项>', description: '回答 Agent 的交互式问题', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] },
848
+ { command: '/pause', description: '在下一次工具调用前暂停当前任务', category: '运维', roles: ['admin', 'owner'] },
849
+ { command: '/resume', description: '继续已暂停的当前任务', category: '运维', roles: ['admin', 'owner'] },
850
+ { command: '/stop', description: '中断当前任务', category: '运维', roles: ['admin', 'owner'] },
851
+ ];
852
+ if (isAdmin) {
853
+ commands.push({ command: '/compact', description: '压缩会话上下文', category: '会话管理', roles: ['admin', 'owner'] }, ...(threadId ? [{ command: '/renew', description: '轮换当前话题 backend', category: '会话管理', roles: ['admin', 'owner'] }] : []));
854
+ }
855
+ return { kind: 'command.result', text: JSON.stringify({ commands, categories: [...new Set(commands.map(c => c.category))] }) };
856
+ }
795
857
  const cmds = [];
796
858
  const canReadModel = canAccessSlashOperation('model.current', 'relation')
797
859
  || canAccessSlashOperation('model.list', 'relation');
@@ -851,6 +913,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
851
913
  cmds.push({ command: '/upgrade', description: '检查版本更新', category: '运维', roles: ['daemon-owner'] });
852
914
  cmds.push({ command: '/reload', args: '[aid]', description: '热重载 Agent 配置', category: '运维', roles: ['daemon-owner'] });
853
915
  cmds.push({ command: '/pair', description: '获取远程控制配对码', category: '运维', roles: ['daemon-owner'] });
916
+ cmds.push({ command: '/diag', args: '<问题描述>|stop', description: '邀请远程诊断或撤销授权', category: '运维', roles: ['daemon-owner'] });
854
917
  }
855
918
  if (!isDaemonOwner && isAdmin) {
856
919
  cmds.push({ command: '/reload', description: '热重载当前 Agent 配置', category: '运维', roles: ['admin', 'owner'] });
@@ -1063,7 +1126,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1063
1126
  if (!args) {
1064
1127
  const activeBaseagent = owningAgent?.baseagent || this.parseDefaultBaseagent();
1065
1128
  // 尝试发送 CommandCard 卡片
1066
- if (this.interactionRouter && available.length > 1) {
1129
+ if (!groupSlash && this.interactionRouter && available.length > 1) {
1067
1130
  const baseagentSession = await getExistingSessionForCommand();
1068
1131
  const interaction = {
1069
1132
  type: 'interaction',
@@ -1090,7 +1153,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1090
1153
  }
1091
1154
  // 降级:文本
1092
1155
  const list = available.map((a) => `${a === activeBaseagent ? ' ✓' : ' '} ${a}`).join('\n');
1093
- const canSwitchAgent = isOwner;
1156
+ const canSwitchAgent = isOwner && !groupSlash;
1094
1157
  if (canSwitchAgent) {
1095
1158
  return { kind: 'command.result', text: `当前 Baseagent: ${activeBaseagent}\n\n可用:\n${list}\n用法: /baseagent <name>` };
1096
1159
  }
@@ -1348,7 +1411,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1348
1411
  isDaemonOwner,
1349
1412
  });
1350
1413
  // 尝试发送 CommandCard 卡片
1351
- if (!args && canReadModelList && this.interactionRouter && models.length > 0) {
1414
+ if (!groupSlash && !args && canReadModelList && this.interactionRouter && models.length > 0) {
1352
1415
  const interaction = {
1353
1416
  type: 'interaction',
1354
1417
  id: `model-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
@@ -1379,7 +1442,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1379
1442
  const catalogHint = `[${catalog.source}/${catalog.freshness}] ${catalog.diagnostics.map(d => d.message).join('; ')}`;
1380
1443
  const modelList = models.map((m) => ` ${modelMatches(modelAgent, m, currentModel) ? '✓' : ' '} ${modelDisplayLabel(modelAgent, m)}`).join('\n');
1381
1444
  const effortHint = efforts.length > 0
1382
- ? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
1445
+ ? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort}${groupSlash ? '' : ' (使用 /effort 调整)'}`
1383
1446
  : '';
1384
1447
  if (args === 'current') {
1385
1448
  return {
@@ -1389,7 +1452,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1389
1452
  };
1390
1453
  }
1391
1454
  if (canReadModelList) {
1392
- const usage = !args && isAdmin ? '\n\n用法: /model <模型>' : '';
1455
+ const usage = !groupSlash && !args && isAdmin ? '\n\n用法: /model <模型>' : '';
1393
1456
  return {
1394
1457
  kind: 'command.result',
1395
1458
  text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}${rawModels.includes(currentModel) ? '' : ' [当前配置,未列入目录]'}\n${catalogHint}\n\n可用模型:\n${modelList}${usage}`,
@@ -1659,7 +1722,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1659
1722
  return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
1660
1723
  }
1661
1724
  // /effort(无参数):显示当前推理强度 + 发送 CommandCard 卡片
1662
- if (this.interactionRouter) {
1725
+ if (!groupSlash && this.interactionRouter) {
1663
1726
  const allItems = [...efforts, 'auto'];
1664
1727
  const interaction = {
1665
1728
  type: 'interaction',
@@ -1687,7 +1750,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1687
1750
  // 降级:文本
1688
1751
  const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
1689
1752
  const effortOptions = [...efforts, 'auto'].join(' / ');
1690
- if (isAdmin) {
1753
+ if (isAdmin && !groupSlash) {
1691
1754
  return { kind: 'command.result', text: `推理强度: ${effortDisplay} 可选: ${effortOptions} 用法: /effort <level>` };
1692
1755
  }
1693
1756
  return { kind: 'command.result', text: `推理强度: ${effortDisplay}` };
@@ -2109,6 +2172,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2109
2172
  const currentMode = readSlashMentionMode(mentionTarget, mentionFallback);
2110
2173
  if (!arg) {
2111
2174
  const displayMode = currentMode ?? '未设置(跟随群设置)';
2175
+ if (groupSlash)
2176
+ return { kind: 'command.result', text: `@ 处理模式: ${displayMode}` };
2112
2177
  // 尝试发送 CommandCard 卡片
2113
2178
  if (isAdmin) {
2114
2179
  const modes = [
@@ -796,6 +796,11 @@ const CATALOG = [
796
796
  fields: [
797
797
  { path: 'aid', type: 'string' },
798
798
  { path: 'avatar', type: 'string' },
799
+ { path: 'sha256', type: 'string', optional: true },
800
+ { path: 'mime', type: 'string', optional: true },
801
+ { path: 'width', type: 'number', optional: true },
802
+ { path: 'height', type: 'number', optional: true },
803
+ { path: 'aspectRatio', type: 'number', optional: true },
799
804
  { path: 'timestamp', type: 'number', optional: true },
800
805
  ],
801
806
  },
@@ -275,9 +275,9 @@ export class EvolAgent {
275
275
  return `agent-files:${this.aid}`;
276
276
  }
277
277
  /**
278
- * 读取 personal/persona.md 内容。走 fileCache(mtime 门控):persona 没有任何
279
- * 写入命令、由 agent 自己带外改写,与 working memory 同样改了即应生效,故每次读
280
- * stat 比对、变了自动重读。文件不存在返回 null。
278
+ * 读取 personal/persona.md 内容。走 fileCache(mtime 门控):persona 可由
279
+ * `ec agent persona` 或 agent 自己更新,与 working memory 一样改了即应生效,
280
+ * 故每次读 stat 比对、变了自动重读。文件不存在返回 null。
281
281
  */
282
282
  getPersona() {
283
283
  const personaPath = path.join(agentPersonalDir(this.aid), 'persona.md');
@@ -11,6 +11,8 @@ const HANDOFF_STALE_WARN_MS = 72 * 60 * 60 * 1000;
11
11
  const HANDOFF_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
12
12
  const HANDOFF_LARGE_AGGREGATE_COUNT = 10;
13
13
  const HANDOFF_LARGE_AGGREGATE_CHARS = 50_000;
14
+ /** Keep origin-side context bounded; the persisted handoff remains complete. */
15
+ const HANDOFF_ORIGIN_REQUEST_MAX_CHARS = 800;
14
16
  export class HandoffTargetBlockedError extends Error {
15
17
  blockingHandoffId;
16
18
  attentionReason;
@@ -93,6 +95,8 @@ export class HandoffRuntime {
93
95
  }, undefined, input.to, targetChatType, origin.baseagent, input.selfAid, 'aun', targetChatType === 'group' ? 'group' : 'agent');
94
96
  throwIfAborted(input.signal);
95
97
  if (target.id === origin.id) {
98
+ if (input.handoffContext !== undefined)
99
+ throw new Error('handoff context requires a different target session');
96
100
  return { crossSession: false, targetSession: target };
97
101
  }
98
102
  if (!input.explicitReturnPolicy)
@@ -119,6 +123,7 @@ export class HandoffRuntime {
119
123
  payload: input.payload,
120
124
  encrypt: input.encrypt,
121
125
  contextOnly: input.contextOnly,
126
+ handoffContext: input.handoffContext,
122
127
  delivery: targetChatType === 'group'
123
128
  ? { chatType: 'group', groupId: input.to }
124
129
  : { chatType: 'private' },
@@ -193,28 +198,108 @@ export class HandoffRuntime {
193
198
  }
194
199
  async returnHandoff(input) {
195
200
  const returnCausation = deriveCausation(normalizeCausation(input.causation) ?? createRootCausation());
196
- const response = this.store.returnHandoff({ ...input, returnCausation });
201
+ const response = await this.mutexes.forKey(this.returnKey(input.selfAid, input.handoffId ?? input.currentSessionId))
202
+ .runExclusive(async () => this.store.returnHandoff({ ...input, returnCausation }));
197
203
  if (response.ok && !response.idempotent) {
198
204
  recordCausationSpan(returnCausation, 'handoff.response', {
199
205
  status: 'completed',
200
- refs: { handoffId: response.handoff_id, sessionId: input.currentSessionId },
206
+ refs: {
207
+ handoffId: response.handoff_id,
208
+ sessionId: input.currentSessionId,
209
+ ...(response.result_id ? { resultId: response.result_id } : {}),
210
+ },
211
+ });
212
+ }
213
+ if (response.ok && !response.idempotent && response.result_id) {
214
+ void this.enqueueOrigin(input.selfAid, response.handoff_id, response.result_id);
215
+ }
216
+ else if (response.ok && !response.idempotent && response.state === 'origin_queued') {
217
+ void this.enqueueOrigin(input.selfAid, response.handoff_id);
218
+ }
219
+ return response;
220
+ }
221
+ /** Recover a return from the original owner session after target-session replacement. */
222
+ async recoverReturnHandoff(input) {
223
+ const instance = this.store.get(input.selfAid, input.handoffId);
224
+ if (!instance || instance.origin_session_id !== input.recoverySessionId) {
225
+ return {
226
+ ok: false,
227
+ code: 'HANDOFF_NOT_FOUND',
228
+ error: 'handoff is not owned by the recovery session',
229
+ handoff_id: input.handoffId,
230
+ };
231
+ }
232
+ const recovery = {
233
+ actorId: input.actorId,
234
+ recoverySessionId: input.recoverySessionId,
235
+ };
236
+ const returnCausation = deriveCausation(normalizeCausation(input.causation) ?? createRootCausation());
237
+ const response = await this.mutexes.forKey(this.returnKey(input.selfAid, input.handoffId))
238
+ .runExclusive(async () => this.store.returnHandoff({
239
+ selfAid: input.selfAid,
240
+ currentSessionId: input.recoverySessionId,
241
+ handoffId: input.handoffId,
242
+ content: input.content,
243
+ returnCausation,
244
+ recovery,
245
+ }));
246
+ if (response.ok && !response.idempotent) {
247
+ recordCausationSpan(returnCausation, 'handoff.response', {
248
+ status: 'completed',
249
+ refs: {
250
+ handoffId: response.handoff_id,
251
+ sessionId: input.recoverySessionId,
252
+ ...(response.result_id ? { resultId: response.result_id } : {}),
253
+ },
201
254
  });
202
255
  }
203
- if (response.ok && !response.idempotent && response.state === 'origin_queued') {
256
+ if (response.ok && !response.idempotent && response.result_id) {
257
+ void this.enqueueOrigin(input.selfAid, response.handoff_id, response.result_id);
258
+ }
259
+ else if (response.ok && !response.idempotent && response.state === 'origin_queued') {
204
260
  void this.enqueueOrigin(input.selfAid, response.handoff_id);
205
261
  }
206
262
  return response;
207
263
  }
208
- async enqueueOrigin(selfAid, handoffId) {
264
+ async enqueueOrigin(selfAid, handoffId, resultId) {
265
+ const operationSequence = ++this.lifecycleSequence;
266
+ await this.mutexes.forKey(this.returnKey(selfAid, handoffId)).runExclusive(async () => {
267
+ await this.enqueueOriginUnlocked(selfAid, handoffId, resultId, operationSequence);
268
+ });
269
+ }
270
+ async enqueueOriginUnlocked(selfAid, handoffId, resultId, operationSequence) {
209
271
  const instance = this.store.get(selfAid, handoffId);
210
- if (!instance || instance.state !== 'origin_queued' || !instance.return_content)
272
+ const result = resultId
273
+ ? instance?.return_results?.find(item => item.result_id === resultId)
274
+ : instance?.return_results?.find(item => item.state === 'origin_queued');
275
+ const resultStream = Array.isArray(instance?.return_results);
276
+ if (resultStream) {
277
+ if (!instance || !result || result.state !== 'origin_queued' || !result.content)
278
+ return;
279
+ }
280
+ else if (!instance || instance.state !== 'origin_queued' || !instance.return_content)
281
+ return;
282
+ const deliveryContent = result?.content ?? instance?.return_content;
283
+ if (!instance || !deliveryContent)
211
284
  return;
212
285
  const origin = await this.sessionManager.getSessionById(instance.origin_session_id);
213
286
  if (!origin) {
214
- this.store.recordOriginEnqueueFailed(selfAid, handoffId, 'origin session not found');
287
+ if (result)
288
+ this.store.recordOriginResultEnqueueFailed(selfAid, handoffId, result.result_id, 'origin session not found');
289
+ else
290
+ this.store.recordOriginEnqueueFailed(selfAid, handoffId, 'origin session not found');
291
+ return;
292
+ }
293
+ if ((this.sessionDiscardSequence.get(this.sessionKey(selfAid, instance.origin_session_id)) ?? 0) > operationSequence) {
215
294
  return;
216
295
  }
217
- const messageId = `origin-deliver:${handoffId}:${origin.id}`;
296
+ const deliverable = this.store.get(selfAid, handoffId);
297
+ const deliveryResultId = result?.result_id ?? resultId;
298
+ if (!this.isOriginDeliveryPending(deliverable, deliveryResultId))
299
+ return;
300
+ const messageId = result
301
+ ? `origin-deliver:${handoffId}:${result.result_id}:${origin.id}`
302
+ : `origin-deliver:${handoffId}:${origin.id}`;
218
303
  const message = {
219
304
  channel: origin.channel,
220
305
  channelType: origin.channelType,
@@ -226,21 +311,24 @@ export class HandoffRuntime {
226
311
  peerId: origin.channelId,
227
312
  peerName: origin.metadata?.peerName || origin.channelId,
228
313
  peerType: origin.metadata?.peerType,
229
- content: instance.return_content,
314
+ content: deliveryContent,
230
315
  messageId,
231
316
  timestamp: Date.now(),
232
317
  source: 'handoff',
233
318
  groupId: origin.chatType === 'group' ? (origin.metadata?.groupId || origin.channelId) : undefined,
234
319
  replyContext: replyContextFromSession(origin),
235
320
  boundSessionId: origin.id,
236
- causation: instance.return_causation
237
- ? deriveCausation(instance.return_causation)
238
- : instance.causation
239
- ? deriveCausation(instance.causation)
240
- : undefined,
321
+ causation: result?.return_causation
322
+ ? deriveCausation(result.return_causation)
323
+ : instance.return_causation
324
+ ? deriveCausation(instance.return_causation)
325
+ : instance.causation
326
+ ? deriveCausation(instance.causation)
327
+ : undefined,
241
328
  handoffDelivery: {
242
329
  direction: 'origin',
243
330
  handoffId,
331
+ ...(result ? { resultId: result.result_id } : {}),
244
332
  ...(instance.origin_authorization
245
333
  ? { originAuthorization: { ...instance.origin_authorization } }
246
334
  : {}),
@@ -255,19 +343,42 @@ export class HandoffRuntime {
255
343
  sessionKeyField: origin.sessionKey,
256
344
  selfAID: origin.selfAID,
257
345
  onPersisted: () => {
258
- if (this.store.get(selfAid, handoffId)?.state !== 'origin_queued') {
259
- throw new Error(`handoff is no longer deliverable: ${handoffId}`);
346
+ const current = this.store.get(selfAid, handoffId);
347
+ if (result) {
348
+ if (!this.isOriginDeliveryPending(current, result.result_id)) {
349
+ throw new Error(`handoff result is no longer deliverable: ${handoffId}/${result.result_id}`);
350
+ }
351
+ this.store.recordOriginResultEnqueued(selfAid, handoffId, result.result_id, messageId);
352
+ }
353
+ else {
354
+ if (current?.state !== 'origin_queued') {
355
+ throw new Error(`handoff is no longer deliverable: ${handoffId}`);
356
+ }
357
+ this.store.recordOriginEnqueued(selfAid, handoffId, messageId);
260
358
  }
261
- this.store.recordOriginEnqueued(selfAid, handoffId, messageId);
262
359
  },
263
360
  });
264
361
  }
265
362
  catch (error) {
266
- if (this.store.get(selfAid, handoffId)?.state === 'origin_queued') {
363
+ const current = this.store.get(selfAid, handoffId);
364
+ if (result) {
365
+ if (current?.return_results?.find(item => item.result_id === result.result_id)?.state === 'origin_queued') {
366
+ this.store.recordOriginResultEnqueueFailed(selfAid, handoffId, result.result_id, error instanceof Error ? error.message : String(error));
367
+ }
368
+ }
369
+ else if (current?.state === 'origin_queued') {
267
370
  this.store.recordOriginEnqueueFailed(selfAid, handoffId, error instanceof Error ? error.message : String(error));
268
371
  }
269
372
  }
270
373
  }
374
+ isOriginDeliveryPending(instance, resultId) {
375
+ if (!instance || instance.state === 'discarded' || instance.state === 'failed')
376
+ return false;
377
+ if (resultId) {
378
+ return instance.return_results?.find(item => item.result_id === resultId)?.state === 'origin_queued';
379
+ }
380
+ return instance.state === 'origin_queued' && !!instance.return_content;
381
+ }
271
382
  buildPromptItem(message, fallbackPeerRole) {
272
383
  return this.buildPromptItems(message, fallbackPeerRole)[0] ?? null;
273
384
  }
@@ -297,6 +408,17 @@ export class HandoffRuntime {
297
408
  if (instance.state !== expectedState)
298
409
  continue;
299
410
  }
411
+ else if (Array.isArray(instance.return_results)) {
412
+ const resultIds = delivery.resultIds?.length
413
+ ? delivery.resultIds
414
+ : (delivery.resultId ? [delivery.resultId] : []);
415
+ const results = resultIds
416
+ .map(resultId => instance.return_results.find(result => result.result_id === resultId))
417
+ .filter((result) => !!result && result.state === 'origin_delivered');
418
+ if (results.length === 0)
419
+ continue;
420
+ instance.__deliveryResults = results;
421
+ }
300
422
  else if (instance.state !== 'origin_delivered') {
301
423
  continue;
302
424
  }
@@ -307,6 +429,13 @@ export class HandoffRuntime {
307
429
  if (delivery.direction === 'origin') {
308
430
  this.warnIfLargeAggregate(message, instances, 'origin');
309
431
  const first = instances[0];
432
+ const deliveryResultRefs = instances.flatMap(instance => ((instance.__deliveryResults ?? [])
433
+ .map(result => ({ handoffId: instance.handoff_id, resultId: result.result_id, result }))));
434
+ const originRequestSummaries = new Map();
435
+ const originRequestShown = new Set();
436
+ for (const instance of instances) {
437
+ originRequestSummaries.set(instance.handoff_id, this.originRequestSummary(instance));
438
+ }
310
439
  return [{
311
440
  kind: 'handoff',
312
441
  peerId: message.peerId,
@@ -329,11 +458,26 @@ export class HandoffRuntime {
329
458
  kind: 'response_to_origin',
330
459
  handoffId: first.handoff_id,
331
460
  handoffIds: instances.map(instance => instance.handoff_id),
332
- originEntries: instances.map(instance => ({
333
- previousContent: this.requestSummary(instance),
334
- responseContent: instance.return_content ?? '',
335
- previousMessageId: instance.target_message_id,
336
- })),
461
+ ...(deliveryResultRefs.length > 0
462
+ ? {
463
+ resultIds: deliveryResultRefs.map(({ result }) => result.result_id),
464
+ resultRefs: deliveryResultRefs.map(({ handoffId, result }) => ({ handoffId, resultId: result.result_id })),
465
+ }
466
+ : {}),
467
+ originEntries: deliveryResultRefs.length > 0
468
+ ? deliveryResultRefs.map(({ result, handoffId }) => ({
469
+ previousContent: originRequestShown.has(handoffId)
470
+ ? '[同一 handoff 的后续结果,原请求见上方]'
471
+ : (originRequestShown.add(handoffId), originRequestSummaries.get(handoffId) ?? this.originRequestSummary(first)),
472
+ responseContent: result.content,
473
+ previousMessageId: (instances.find(instance => instance.handoff_id === handoffId) ?? first).target_message_id,
474
+ resultId: result.result_id,
475
+ }))
476
+ : instances.map(instance => ({
477
+ previousContent: originRequestSummaries.get(instance.handoff_id) ?? this.originRequestSummary(instance),
478
+ responseContent: instance.return_content ?? '',
479
+ previousMessageId: instance.target_message_id,
480
+ })),
337
481
  previousContent: this.requestSummary(first),
338
482
  previousMessageId: first.target_message_id,
339
483
  returnPolicy: first.return_policy,
@@ -385,9 +529,14 @@ export class HandoffRuntime {
385
529
  },
386
530
  }];
387
531
  }
388
- completeOriginContext(selfAid, handoffId) {
532
+ completeOriginContext(selfAid, handoffId, resultId) {
389
533
  const instance = this.store.get(selfAid, handoffId);
390
- if (instance?.state === 'origin_delivered')
534
+ if (resultId) {
535
+ if (instance?.return_results?.some(result => result.result_id === resultId && result.state === 'origin_delivered')) {
536
+ this.store.completeOriginResult(selfAid, handoffId, resultId);
537
+ }
538
+ }
539
+ else if (instance?.state === 'origin_delivered')
391
540
  this.store.completeOriginContext(selfAid, handoffId);
392
541
  }
393
542
  completeTargetContext(selfAid, handoffId) {
@@ -535,6 +684,11 @@ export class HandoffRuntime {
535
684
  else if (instance.state === 'origin_queued') {
536
685
  originEnqueues.push(this.enqueueOrigin(selfAid, instance.handoff_id));
537
686
  }
687
+ else if (Array.isArray(instance.return_results)) {
688
+ for (const result of instance.return_results.filter(item => item.state === 'origin_queued')) {
689
+ originEnqueues.push(this.enqueueOrigin(selfAid, instance.handoff_id, result.result_id));
690
+ }
691
+ }
538
692
  }
539
693
  }
540
694
  await Promise.all(originEnqueues);
@@ -559,10 +713,20 @@ export class HandoffRuntime {
559
713
  return true;
560
714
  }
561
715
  async cancelOriginDelivery(instance) {
716
+ const resultDeliveries = instance.return_results?.filter(result => (result.state === 'origin_queued' || result.state === 'origin_delivered')) ?? [];
717
+ if (resultDeliveries.length > 0) {
718
+ for (const result of resultDeliveries) {
719
+ await this.cancelOriginMessage(instance, result.origin_delivery_message_id
720
+ ?? `origin-deliver:${instance.handoff_id}:${result.result_id}:${instance.origin_session_id}`);
721
+ }
722
+ return;
723
+ }
562
724
  if (instance.state !== 'origin_queued' && instance.state !== 'origin_delivered')
563
725
  return;
564
- const messageId = instance.origin_delivery_message_id
565
- ?? `origin-deliver:${instance.handoff_id}:${instance.origin_session_id}`;
726
+ await this.cancelOriginMessage(instance, instance.origin_delivery_message_id
727
+ ?? `origin-deliver:${instance.handoff_id}:${instance.origin_session_id}`);
728
+ }
729
+ async cancelOriginMessage(instance, messageId) {
566
730
  const queue = this.messageQueue;
567
731
  try {
568
732
  const queued = queue.cancelMessageByIdInSession?.(instance.origin_session_id, messageId) ?? false;
@@ -592,6 +756,9 @@ export class HandoffRuntime {
592
756
  sessionKey(selfAid, sessionId) {
593
757
  return `${selfAid}\u0000${sessionId}`;
594
758
  }
759
+ returnKey(selfAid, handoffOrSessionId) {
760
+ return `${selfAid}\u0000handoff-return\u0000${handoffOrSessionId}`;
761
+ }
595
762
  warnIfLargeAggregate(message, instances, direction) {
596
763
  const chars = message.content.length
597
764
  + instances.reduce((total, instance) => total + this.requestSummary(instance).length, 0);
@@ -606,7 +773,18 @@ export class HandoffRuntime {
606
773
  + ` count=${instances.length} chars=${chars} responseMessage=${message.messageId ?? '<none>'}`);
607
774
  }
608
775
  requestSummary(instance) {
609
- return classifyAunPayloadForLog(instance.request.payload).content || '[payload]';
776
+ const sent = classifyAunPayloadForLog(instance.request.payload).content || '[payload]';
777
+ return instance.request.context ? `${sent}\n\n[本地交接上下文]\n${instance.request.context}` : sent;
778
+ }
779
+ originRequestSummary(instance) {
780
+ const content = this.requestSummary(instance);
781
+ if (content.length <= HANDOFF_ORIGIN_REQUEST_MAX_CHARS)
782
+ return content;
783
+ const marker = `\n\n[原请求过长,回流提示仅保留首尾;完整内容共 ${content.length} 字,已省略 ${content.length - HANDOFF_ORIGIN_REQUEST_MAX_CHARS} 字]\n\n`;
784
+ const available = HANDOFF_ORIGIN_REQUEST_MAX_CHARS - marker.length;
785
+ const head = Math.ceil(available * 0.6);
786
+ const tail = available - head;
787
+ return `${content.slice(0, head)}${marker}${content.slice(-tail)}`;
610
788
  }
611
789
  }
612
790
  function throwIfAborted(signal) {