evolcore 0.0.20 → 0.0.21

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 (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -1,5 +1,6 @@
1
1
  import { hasModelSwitcher, hasPermissionController } from '../../agents/runner-types.js';
2
2
  import { getCodexEfforts } from '../../agents/codex-runner.js';
3
+ import { normalizeBaseagent } from '../../agents/baseagent.js';
3
4
  import { buildEnvelope, isDeliveryTargetForChannel } from '../message/message-utils.js';
4
5
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
5
6
  import { logger } from '../../utils/logger.js';
@@ -17,14 +18,15 @@ import { resolvePermissionMode, writeScope } from '../model/config-scope.js';
17
18
  import { formatPeerKey } from '../relation/peer-identity.js';
18
19
  import { modelMatches } from '../model/model-catalog.js';
19
20
  import { formatModelCheck, runModelCheck } from '../model/model-diagnostics.js';
20
- import { filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
21
+ import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
22
+ import { resolveRuntimeStringField } from '../role/runtime-policy.js';
21
23
  import { displaySessionTitle } from '../session/session-title.js';
22
24
  import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
23
25
  import { normalizePermissionMode as normalizePermissionModeContract, PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
24
26
  import { isManagementRole } from '../../config/builtin-roles.js';
25
27
  import { isSystemControlChannel } from '../system-channels.js';
26
28
  import { spawnDetachedNode } from '../../utils/cross-platform.js';
27
- import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, } from './slash-gate.js';
29
+ import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, probeSessionActivity, } from './slash-gate.js';
28
30
  const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
29
31
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
30
32
  function defaultSlashChatmode(field) {
@@ -114,7 +116,7 @@ function resolveSlashRelationTarget(params) {
114
116
  const actualChatType = params.session?.chatType || params.chatType;
115
117
  const peerId = actualChatType === 'group'
116
118
  ? (params.session?.metadata?.groupId || params.channelId)
117
- : (params.userId || params.session?.metadata?.peerId);
119
+ : (params.userId || params.session?.metadata?.peerId || params.session?.channelId || params.channelId);
118
120
  if (!peerId)
119
121
  return { error: 'missing current peer id', code: 'MISSING_PEER' };
120
122
  const channelType = params.session?.channelType || this.resolveChannelType?.(params.channel) || params.channel.split('#')[0];
@@ -419,10 +421,30 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
419
421
  // 权限检查:区分用户级命令和管理级命令
420
422
  const isOwner = identity.role === 'owner';
421
423
  const isAdmin = isManagementRole(identity.role);
422
- const activeChatType = activeSession?.chatType || (chatType === 'group' ? 'group' : 'private');
424
+ // A topic's latest snapshot is authoritative and intentionally strict. Do
425
+ // not let a corrupt committed record turn a backend-management command into
426
+ // an unhandled quick-command failure; /renew will report it as UNKNOWN and
427
+ // leave the binding untouched.
428
+ let topicAuthSession;
429
+ let topicAuthSessionLookupFailed = false;
430
+ if (threadId) {
431
+ try {
432
+ topicAuthSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
433
+ }
434
+ catch (error) {
435
+ topicAuthSessionLookupFailed = true;
436
+ logger.warn(`[CommandHandler] topic session lookup failed: thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
437
+ }
438
+ }
439
+ const activeChatType = topicAuthSession?.chatType
440
+ || activeSession?.chatType
441
+ || (chatType === 'group' ? 'group' : 'private');
423
442
  const getExistingSessionForCommand = async () => {
424
- if (threadId)
425
- return await this.sessionManager.getThreadSession(channel, channelId, threadId);
443
+ if (threadId) {
444
+ if (topicAuthSessionLookupFailed)
445
+ return undefined;
446
+ return topicAuthSession ?? await this.sessionManager.getThreadSession(channel, channelId, threadId);
447
+ }
426
448
  return activeSession;
427
449
  };
428
450
  const getEffectiveChatmode = (session, fallbackChatType = activeChatType) => {
@@ -497,8 +519,103 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
497
519
  agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
498
520
  });
499
521
  await this.processor.interruptSession(commandSession.id, 'stop');
522
+ this.processor?.clearPauseSession?.(commandSession.id);
500
523
  }
501
524
  };
525
+ /**
526
+ * Boundary-scoped activity probe shared by /renew and /compact. The queue
527
+ * snapshot is taken at gate installation; later arrivals are deliberately
528
+ * excluded because they are cutoff-after work and remain durably queued.
529
+ */
530
+ const probeBackendActivity = (current, gateSnapshot) => {
531
+ const probes = {
532
+ paused: () => this.processor?.isPauseRequested?.(current.id) ?? false,
533
+ activeStream: () => this.getAgent(channel, current.baseagent).hasActiveStream(current.id),
534
+ };
535
+ if (this.permissionGateway) {
536
+ probes.pendingPermission = () => this.permissionGateway.getPendingRequests(current.id).length > 0;
537
+ }
538
+ if (this.interactionRouter) {
539
+ probes.pendingInteraction = () => this.interactionRouter.getPending(current.id).length > 0;
540
+ }
541
+ const selfAid = current.selfAID || selfAID || this.getOwningAgent(channel)?.aid;
542
+ if (this.handoffRuntime) {
543
+ probes.activeHandoff = () => {
544
+ if (!selfAid)
545
+ throw new Error('handoff activity owner unavailable');
546
+ return this.handoffRuntime.hasActiveSessionWork(selfAid, current.id);
547
+ };
548
+ probes.openHandoffTarget = () => {
549
+ if (!selfAid)
550
+ throw new Error('handoff activity owner unavailable');
551
+ return this.handoffRuntime.hasOpenTarget(selfAid, current.id);
552
+ };
553
+ }
554
+ return probeSessionActivity({
555
+ prequeue: gateSnapshot.prequeue,
556
+ active: gateSnapshot.active,
557
+ queued: gateSnapshot.queued,
558
+ dispatching: gateSnapshot.dispatching,
559
+ processing: !!current.processingState,
560
+ activeTurn: !!current.metadata?.turnState?.active,
561
+ }, probes);
562
+ };
563
+ const withIdleTopicBackend = async (sessionId, expectedThreadId, action, fallbackSession) => {
564
+ // Main-session /compact keeps its existing idle guard and provider path.
565
+ // Only topic backend operations require the stronger dequeue barrier.
566
+ if (!expectedThreadId) {
567
+ let current = fallbackSession;
568
+ if (!current) {
569
+ try {
570
+ current = typeof this.sessionManager.getSessionById === 'function'
571
+ ? await this.sessionManager.getSessionById(sessionId)
572
+ : undefined;
573
+ }
574
+ catch (error) {
575
+ logger.warn(`[CommandHandler] main session snapshot read failed: ${error instanceof Error ? error.message : String(error)}`);
576
+ return { ok: false, reason: 'unknown' };
577
+ }
578
+ }
579
+ if (!current || current.threadId)
580
+ return { ok: false, reason: 'missing' };
581
+ // Preserve the legacy main-session /compact exclusion. Topic commands
582
+ // use the stronger barrier below; main sessions retain the historical
583
+ // keyed dequeue lock and provider behavior.
584
+ const releaseLock = typeof this.messageQueue.acquireLock === 'function'
585
+ ? this.messageQueue.acquireLock(sessionId)
586
+ : undefined;
587
+ try {
588
+ return { ok: true, value: await action(current) };
589
+ }
590
+ finally {
591
+ releaseLock?.();
592
+ }
593
+ }
594
+ if (typeof this.messageQueue.withSessionBarrier !== 'function') {
595
+ return { ok: false, reason: 'unknown' };
596
+ }
597
+ return this.messageQueue.withSessionBarrier(sessionId, async (snapshot) => {
598
+ let current;
599
+ try {
600
+ current = typeof this.sessionManager.getSessionById === 'function'
601
+ ? await this.sessionManager.getSessionById(sessionId)
602
+ : expectedThreadId
603
+ ? await this.sessionManager.getThreadSession(channel, channelId, expectedThreadId)
604
+ : fallbackSession;
605
+ }
606
+ catch (error) {
607
+ logger.warn(`[CommandHandler] session snapshot read failed: ${error instanceof Error ? error.message : String(error)}`);
608
+ return { ok: false, reason: 'unknown' };
609
+ }
610
+ if (!current || (current.threadId || undefined) !== expectedThreadId) {
611
+ return { ok: false, reason: 'missing' };
612
+ }
613
+ const activity = probeBackendActivity(current, snapshot);
614
+ if (activity !== 'idle')
615
+ return { ok: false, reason: activity };
616
+ return { ok: true, value: await action(current, snapshot) };
617
+ });
618
+ };
502
619
  // /help 命令不需要会话
503
620
  if (normalizedContent === '/help') {
504
621
  const canReadModel = canAccessSlashOperation('model.current', 'relation')
@@ -517,7 +634,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
517
634
  const appendProcessCommands = (lines) => {
518
635
  if (!isDaemonOwner)
519
636
  return;
520
- lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置');
637
+ lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置', ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)');
521
638
  };
522
639
  if (!isAdmin && activeChatType === 'group') {
523
640
  const lines = [
@@ -559,6 +676,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
559
676
  ' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
560
677
  ' /name <新名称> - 重命名当前会话',
561
678
  ' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
679
+ ...(threadId ? [' /renew - 在当前话题中轮换 backend'] : []),
562
680
  ' /pause - 在下一次工具调用前暂停当前任务',
563
681
  ' /resume - 继续已暂停的当前任务',
564
682
  ' /stop - 中断当前任务',
@@ -608,6 +726,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
608
726
  ' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
609
727
  ' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
610
728
  ' /compact - 压缩会话上下文(减少 token 用量)',
729
+ ' /renew - 在话题中轮换 backend(下一条消息懒初始化)',
611
730
  ' /pause - 在下一次工具调用前暂停当前任务',
612
731
  ' /resume - 继续已暂停的当前任务',
613
732
  ' /stop - 中断当前任务',
@@ -639,6 +758,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
639
758
  ...(isDaemonOwner ? [
640
759
  ' /restart - 重启服务',
641
760
  ' /reload [aid] - 热重载 Agent 配置',
761
+ ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)',
642
762
  ] : []),
643
763
  ...(!isDaemonOwner && isAdmin ? [
644
764
  ' /reload - 热重载当前 Agent 配置',
@@ -682,6 +802,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
682
802
  cmds.push({ command: '/rewind', aliases: ['/rw'], args: '[N] [chat|file|all]', description: '查看历史/撤销指定轮次', category: '会话管理', roles: ['admin', 'owner'] });
683
803
  cmds.push({ command: '/compact', description: '压缩会话上下文(减少 token 用量)', category: '会话管理', roles: ['admin', 'owner'] });
684
804
  }
805
+ if (threadId && (activeChatType !== 'group' || isAdmin)) {
806
+ cmds.push({ command: '/renew', description: '轮换当前话题 backend', category: '会话管理', roles: activeChatType === 'group' ? ['admin', 'owner'] : ['visitor', 'member', 'admin', 'owner'] });
807
+ }
685
808
  // Agent 与模型
686
809
  if (isAdmin)
687
810
  cmds.push({ command: '/baseagent', aliases: ['/base'], args: '[name]', description: '查看或切换 Agent 后端', category: 'Agent 与模型', roles: ['admin', 'owner'] });
@@ -707,6 +830,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
707
830
  cmds.push({ command: '/observable', args: '[true|false]', description: '查看或切换观察者模式', category: '运维', roles: ['owner'] });
708
831
  }
709
832
  if (isDaemonOwner) {
833
+ cmds.push({ command: '/fa', aliases: ['/fullaccess'], args: '<提示词>', description: '仅让当前提示词对应的 run 使用宿主级 fullaccess', category: '权限管理', roles: ['daemon-owner'] });
710
834
  cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['daemon-owner'] });
711
835
  cmds.push({ command: '/upgrade', description: '检查版本更新', category: '运维', roles: ['daemon-owner'] });
712
836
  cmds.push({ command: '/reload', args: '[aid]', description: '热重载 Agent 配置', category: '运维', roles: ['daemon-owner'] });
@@ -962,7 +1086,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
962
1086
  }
963
1087
  await interruptPausedSessionBeforeReplacement(await getExistingSessionForCommand());
964
1088
  const previousDefaultBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
965
- owningAgent.setActiveBaseagent(args);
1089
+ try {
1090
+ owningAgent.setActiveBaseagent(args);
1091
+ }
1092
+ catch (error) {
1093
+ return { kind: 'command.error', text: `❌ ${error instanceof Error ? error.message : String(error)}` };
1094
+ }
966
1095
  this.eventBus.publish({
967
1096
  type: 'agent:baseagent-changed',
968
1097
  aid: owningAgent.aid,
@@ -1655,7 +1784,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1655
1784
  }).join('\n');
1656
1785
  return { kind: 'command.error', text: `❌ 该 Agent 有 ${busyInfo.count} 个任务执行中,无法 reload。\n\n处理中:\n${processingLines}\n\n等待任务完成后重试,通常 30-60 秒。` };
1657
1786
  }
1658
- const res = await execAgentAction('reload', { aid: targetAid }, userId ?? '', this.eventBus);
1787
+ const res = await execAgentAction('reload', { aid: targetAid }, userId ?? '', this.eventBus, this.agentApplicationService);
1659
1788
  if ('error' in res)
1660
1789
  return { kind: 'command.error', text: `❌ reload 失败:${res.error}` };
1661
1790
  return { kind: 'command.result', text: `✅ Agent ${targetAid} 配置已重载` };
@@ -2009,41 +2138,225 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2009
2138
  if (normalizedContent === '/clear') {
2010
2139
  return { kind: 'command.error', text: '⚠️ /clear 已移除\n\n请使用 /new [名称] 创建新会话来开始全新上下文。旧会话会保留,可通过 /s 查看或切换。' };
2011
2140
  }
2012
- // /compact 命令:手动压缩会话上下文
2013
- if (normalizedContent === '/compact') {
2014
- const session = await getExistingSessionForCommand();
2015
- if (!session)
2016
- return { kind: 'command.error', text: '❌ 当前没有活跃会话,无需压缩' };
2017
- const sessionAgent = this.getAgent(channel, session.baseagent);
2018
- if (!sessionAgent.capabilities?.compact) {
2019
- return { kind: 'command.error', text: `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact` };
2141
+ // /renew:只支持话题会话。它废止当前 backend 绑定;下一条普通消息
2142
+ // 才会在新的 TurnLease 中懒创建并绑定新 backend。
2143
+ if (normalizedContent === '/renew' || /^\/renew\s/.test(normalizedContent)) {
2144
+ if (normalizedContent !== '/renew') {
2145
+ return { kind: 'command.error', text: '❌ /renew 不接受参数,请直接使用 /renew' };
2020
2146
  }
2021
- if (!session.agentSessionId) {
2022
- return { kind: 'command.error', text: ' 当前会话没有历史记录,无需压缩' };
2147
+ if (!threadId) {
2148
+ return { kind: 'command.error', text: '⚠️ /renew 仅支持话题会话' };
2149
+ }
2150
+ // Card callbacks use synthetic message IDs on several adapters. They are
2151
+ // suitable for UI correlation, but not for the durable inbound claim.
2152
+ if (source === 'card-trigger') {
2153
+ return { kind: 'command.error', text: '⚠️ /renew 仅支持携带原生消息 ID 的普通消息' };
2154
+ }
2155
+ let renewSession;
2156
+ try {
2157
+ // Re-read rather than trusting the authorization lookup above: only a
2158
+ // fresh strict snapshot may be used to choose the barrier key.
2159
+ renewSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
2160
+ }
2161
+ catch (error) {
2162
+ logger.warn(`[CommandHandler] /renew topic lookup failed: thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
2163
+ return { kind: 'command.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试' };
2164
+ }
2165
+ if (!renewSession) {
2166
+ return { kind: 'command.error', text: '❌ 找不到当前话题会话' };
2167
+ }
2168
+ const inboundMessageId = messageId?.trim();
2169
+ if (!inboundMessageId) {
2170
+ return { kind: 'command.error', text: '❌ /renew 缺少稳定的消息 ID,当前渠道不支持安全重试' };
2171
+ }
2172
+ if (typeof this.sessionManager.rotateTopicBackend !== 'function'
2173
+ || typeof this.messageQueue.withSessionBarrier !== 'function'
2174
+ || typeof this.messageQueue.claimInbound !== 'function') {
2175
+ return { kind: 'command.error', text: '❌ 当前运行环境不支持安全 backend 轮换' };
2023
2176
  }
2024
- await interruptPausedSessionBeforeReplacement(session);
2025
- const projectPath = path.isAbsolute(session.projectPath)
2026
- ? session.projectPath
2027
- : path.resolve(process.cwd(), session.projectPath);
2028
- const releaseLock = this.messageQueue.acquireLock(session.id);
2029
2177
  try {
2178
+ const outcome = await this.messageQueue.withSessionBarrier(renewSession.id, async (snapshot) => {
2179
+ let current;
2180
+ try {
2181
+ current = typeof this.sessionManager.getSessionById === 'function'
2182
+ ? await this.sessionManager.getSessionById(renewSession.id)
2183
+ : undefined;
2184
+ }
2185
+ catch (error) {
2186
+ logger.warn(`[CommandHandler] /renew strict session read failed: session=${renewSession.id}: ${error instanceof Error ? error.message : String(error)}`);
2187
+ return { kind: 'unknown' };
2188
+ }
2189
+ if (!current || current.threadId !== threadId)
2190
+ return { kind: 'missing' };
2191
+ let claim;
2192
+ try {
2193
+ claim = this.messageQueue.claimInbound(current.id, inboundMessageId, selfAID ?? current.selfAID);
2194
+ }
2195
+ catch (error) {
2196
+ logger.error(`[CommandHandler] /renew inbound claim failed: session=${current.id}: ${error instanceof Error ? error.message : String(error)}`);
2197
+ return { kind: 'claim_failed' };
2198
+ }
2199
+ if (claim === 'duplicate')
2200
+ return { kind: 'duplicate' };
2201
+ const activity = probeBackendActivity(current, snapshot);
2202
+ if (activity !== 'idle')
2203
+ return { kind: activity };
2204
+ const result = await this.sessionManager.rotateTopicBackend({
2205
+ sessionId: current.id,
2206
+ expectedThreadId: threadId,
2207
+ });
2208
+ return {
2209
+ kind: 'rotated',
2210
+ wasBound: result.wasBound,
2211
+ };
2212
+ });
2213
+ if (outcome.kind === 'missing') {
2214
+ return { kind: 'command.error', text: '❌ 当前话题会话已不存在或归属已变化' };
2215
+ }
2216
+ if (outcome.kind === 'claim_failed') {
2217
+ return { kind: 'command.error', text: '❌ 当前渠道无法安全记录该请求,未执行 backend 轮换' };
2218
+ }
2219
+ if (outcome.kind === 'duplicate') {
2220
+ return { kind: 'command.result', text: 'ℹ️ 该请求已接收过,不再重复执行' };
2221
+ }
2222
+ if (outcome.kind === 'busy' || outcome.kind === 'unknown') {
2223
+ return {
2224
+ kind: 'command.error',
2225
+ text: outcome.kind === 'busy'
2226
+ ? '⚠️ 当前会话忙碌,无法安全轮换,请稍后再试'
2227
+ : '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试',
2228
+ };
2229
+ }
2230
+ if (outcome.kind !== 'rotated') {
2231
+ return { kind: 'command.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试' };
2232
+ }
2233
+ return {
2234
+ kind: 'command.result',
2235
+ text: `✅ backend 已切换${outcome.wasBound ? '' : '(当前未绑定 backend)'};下一条消息将初始化新 backend(阶段一不携带历史摘要)`,
2236
+ };
2237
+ }
2238
+ catch (error) {
2239
+ 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)}` };
2241
+ }
2242
+ }
2243
+ // /compact 命令:手动压缩会话上下文
2244
+ if (normalizedContent === '/compact') {
2245
+ const session = await getExistingSessionForCommand();
2246
+ if (!session) {
2247
+ return topicAuthSessionLookupFailed
2248
+ ? { kind: 'command.error', text: '⚠️ 当前会话状态未知,无法安全压缩,请稍后再试' }
2249
+ : { kind: 'command.error', text: '❌ 当前没有活跃会话,无需压缩' };
2250
+ }
2251
+ if (!session.threadId)
2252
+ await interruptPausedSessionBeforeReplacement(session);
2253
+ const outcome = await withIdleTopicBackend(session.id, session.threadId || undefined, async (session) => {
2254
+ if (!session.agentSessionId) {
2255
+ return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需压缩' };
2256
+ }
2257
+ const projectPath = path.isAbsolute(session.projectPath)
2258
+ ? session.projectPath
2259
+ : path.resolve(process.cwd(), session.projectPath);
2260
+ const currentSessionAgent = this.getAgent(channel, session.baseagent);
2261
+ if (!currentSessionAgent.capabilities?.compact) {
2262
+ return { kind: 'command.error', text: `❌ 当前 Agent (${currentSessionAgent.name}) 不支持 /compact` };
2263
+ }
2030
2264
  if (sendMessage) {
2031
2265
  await sendMessage(channelId, '⏳ 正在压缩会话上下文...', this.getReplyContext(session));
2032
2266
  }
2033
- const compacted = await sessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
2034
- if (compacted) {
2267
+ // Resolve the same relation/agent effective model used for normal turns.
2268
+ // The runner instance's model is only the final fallback; without this
2269
+ // override Claude's /compact command could resume with a stale agent
2270
+ // default (for example glm-5.3) while the turn itself used gpt-5.6-sol.
2271
+ const compactBaseagentRaw = session.baseagent || currentSessionAgent.name;
2272
+ const compactBaseagentNormalized = normalizeBaseagent(compactBaseagentRaw);
2273
+ const compactBaseagent = compactBaseagentNormalized.canonical === 'unknown'
2274
+ ? compactBaseagentRaw
2275
+ : compactBaseagentNormalized.canonical;
2276
+ const compactChatType = session.chatType;
2277
+ const compactRelation = resolveSlashRelationTarget.call(this, {
2278
+ session,
2279
+ channel,
2280
+ channelId,
2281
+ userId,
2282
+ selfAID,
2283
+ role: session.identity?.role || identity.role,
2284
+ chatType: compactChatType,
2285
+ });
2286
+ const compactSelector = 'error' in compactRelation
2287
+ ? { self: selfAID ?? session.selfAID ?? this.resolveSelfAID(channel) }
2288
+ : compactRelation;
2289
+ let compactConfig = {};
2290
+ try {
2291
+ compactConfig = (resolveEffective(compactSelector, { cache: true }).baseagents || {})[compactBaseagent] || {};
2292
+ }
2293
+ catch {
2294
+ compactConfig = {};
2295
+ }
2296
+ const compactModelDecision = constrainResolvedModelForRole({
2297
+ role: session.identity?.role || identity.role,
2298
+ baseagent: compactBaseagent,
2299
+ model: typeof compactConfig.model === 'string' ? compactConfig.model : undefined,
2300
+ resolveModelId: typeof currentSessionAgent.resolveModelId === 'function'
2301
+ ? currentSessionAgent.resolveModelId.bind(currentSessionAgent)
2302
+ : undefined,
2303
+ selfAid: compactSelector.self,
2304
+ });
2305
+ const compactModel = compactModelDecision.model
2306
+ || (typeof currentSessionAgent.getModel === 'function' ? currentSessionAgent.getModel() : undefined);
2307
+ const compactEffortDecision = resolveRuntimeStringField({
2308
+ selfAid: compactSelector.self,
2309
+ role: session.identity?.role || identity.role,
2310
+ field: `baseagents.${compactBaseagent}.effort`,
2311
+ configuredValue: compactConfig.effort,
2312
+ });
2313
+ const compactConfiguredEffort = compactEffortDecision.effectiveValue;
2314
+ const compactEffort = compactConfiguredEffort && compactConfiguredEffort !== 'auto'
2315
+ ? compactConfiguredEffort
2316
+ : (compactConfiguredEffort === 'auto' ? undefined : currentSessionAgent.getEffort?.());
2317
+ // Only send an explicit override when config/role resolution decided a
2318
+ // value. If neither scope provides one, the runner's own getModel/getEffort
2319
+ // fallback is the same effective value and preserving an omitted fourth
2320
+ // argument keeps older runner adapters source-compatible.
2321
+ const hasExplicitCompactModel = typeof compactConfig.model === 'string' && compactConfig.model.trim().length > 0
2322
+ || compactModelDecision.constrained;
2323
+ const hasExplicitCompactEffort = typeof compactConfig.effort === 'string' && compactConfig.effort.trim().length > 0
2324
+ || compactEffortDecision.decidedBy === 'role';
2325
+ const compactOverride = (hasExplicitCompactModel && compactModel) || (hasExplicitCompactEffort && compactEffort)
2326
+ ? {
2327
+ ...(hasExplicitCompactModel && compactModel ? { model: compactModel } : {}),
2328
+ ...(hasExplicitCompactEffort && compactEffort ? { effort: compactEffort } : {}),
2329
+ }
2330
+ : undefined;
2331
+ const compacted = compactOverride
2332
+ ? await currentSessionAgent.compactSession(session.id, session.agentSessionId, projectPath, compactOverride)
2333
+ : await currentSessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
2334
+ const compactResult = compacted;
2335
+ if (compactResult === true || compactResult?.ok === true) {
2035
2336
  return {
2036
2337
  kind: 'command.result',
2037
2338
  text: '✅ 会话压缩完成',
2038
2339
  };
2039
2340
  }
2040
2341
  else {
2041
- return { kind: 'command.error', text: '❌ 会话压缩失败,请稍后重试' };
2342
+ return {
2343
+ kind: 'command.error',
2344
+ text: `❌ 会话压缩失败(${compactResult?.code ?? 'sdk_error'}):${compactResult?.message ?? 'compact failed'}`,
2345
+ };
2042
2346
  }
2347
+ }, session);
2348
+ if (!outcome.ok) {
2349
+ if (outcome.reason === 'missing') {
2350
+ return { kind: 'command.error', text: '❌ 当前会话已不存在或归属已变化' };
2351
+ }
2352
+ return {
2353
+ kind: 'command.error',
2354
+ text: outcome.reason === 'busy'
2355
+ ? '⚠️ 当前会话忙碌,请稍后再试\n使用 /stop 中断当前任务后重试'
2356
+ : '⚠️ 当前会话状态未知,无法安全压缩,请稍后再试',
2357
+ };
2043
2358
  }
2044
- finally {
2045
- releaseLock();
2046
- }
2359
+ return outcome.value;
2047
2360
  }
2048
2361
  // 后续命令可读取现有会话,但不应在这里隐式创建新会话。
2049
2362
  // 真正需要新会话的命令应显式调用 createNewSession()。
@@ -3203,7 +3516,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3203
3516
  const backupPath = await backupSessionFile(sessionFile);
3204
3517
  const fsPromises = await import('fs/promises');
3205
3518
  await fsPromises.unlink(sessionFile);
3206
- await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
3519
+ // A topic backend binding may only be changed by an explicit boundary
3520
+ // rotation/activation. Repair clears runner-local state, but must not
3521
+ // turn a legacy empty callback into an authoritative binding mutation.
3522
+ if (!repairSession.threadId) {
3523
+ await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
3524
+ }
3207
3525
  repairAgent.updateSessionId(repairSession.id, '');
3208
3526
  await this.sessionManager.resetHealthStatus(repairSession.id);
3209
3527
  return { kind: 'command.result', text: `✓ 修复完成\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}` };
@@ -3240,7 +3558,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3240
3558
  });
3241
3559
  if (triggerAuthDenied)
3242
3560
  return triggerAuthDenied;
3243
- const text = await this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin, messageId, chatType, threadId);
3561
+ const text = await this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin, messageId, chatType, threadId, isDaemonOwner, authSubject.canApprovePersistentFullAccess);
3244
3562
  return { kind: 'command.result', text };
3245
3563
  }
3246
3564
  return null;
@@ -317,6 +317,7 @@ const CATALOG = [
317
317
  { path: 'eventPhase', type: 'string' },
318
318
  { path: 'callId', type: 'string', optional: true },
319
319
  { path: 'correlationId', type: 'string', optional: true },
320
+ { path: 'agentName', type: 'string', optional: true },
320
321
  { path: 'agentAid', type: 'string', optional: true },
321
322
  { path: 'permissionMode', type: 'string', optional: true },
322
323
  { path: 'decision', type: 'string' },
@@ -373,6 +374,8 @@ const CATALOG = [
373
374
  { path: 'approved', type: 'boolean' },
374
375
  { path: 'toolName', type: 'string', optional: true },
375
376
  { path: 'reason', type: 'string', optional: true },
377
+ { path: 'decisionSource', type: 'string', optional: true },
378
+ { path: 'policyCode', type: 'string', optional: true },
376
379
  ],
377
380
  },
378
381
  {
@@ -385,6 +388,8 @@ const CATALOG = [
385
388
  { path: 'requestId', type: 'string' },
386
389
  { path: 'toolName', type: 'string', optional: true },
387
390
  { path: 'reason', type: 'string', optional: true },
391
+ { path: 'decisionSource', type: 'string', optional: true },
392
+ { path: 'policyCode', type: 'string', optional: true },
388
393
  ],
389
394
  },
390
395
  {
@@ -6,6 +6,7 @@ import { ConfigTarget, read as cfgRead, write as cfgWrite, ensureFile as cfgEnsu
6
6
  import { withLifecycleForWrite } from '../config/lifecycle.js';
7
7
  import { isStaticAgentAdmin, isStaticAgentOwner } from '../config/peer-role-resolver.js';
8
8
  import { resolvePrincipal } from '../config/contact-book.js';
9
+ import { isSupportedBaseagent } from '../agents/baseagent.js';
9
10
  /**
10
11
  * EvolAgent —— 一个 self-agent 的运行时表示。
11
12
  *
@@ -168,6 +169,9 @@ export class EvolAgent {
168
169
  // ── Baseagent 字段写入 ────────────────────────────
169
170
  /** 切换当前活跃 baseagent(写入 config.json)。 */
170
171
  setActiveBaseagent(value) {
172
+ if (value !== undefined && !isSupportedBaseagent(value)) {
173
+ throw new Error(`Unsupported baseagent: ${value} (choose: claude/codex/gemini/ecagent)`);
174
+ }
171
175
  this.merged.active_baseagent = value;
172
176
  this.updateAgentConfig(b => {
173
177
  if (value === undefined)
@@ -63,6 +63,10 @@ export class HandoffDispatcher {
63
63
  isAgentPaused(selfAid) {
64
64
  return this.pausedAgents.has(selfAid);
65
65
  }
66
+ /** Whether delivery work for this exact target is currently executing. */
67
+ isRunning(selfAid, targetSessionId) {
68
+ return this.running.has(this.key(selfAid, targetSessionId));
69
+ }
66
70
  async drain(selfAid, targetSessionId) {
67
71
  const key = this.key(selfAid, targetSessionId);
68
72
  if (this.running.has(key)) {
@@ -150,6 +150,16 @@ export class HandoffRuntime {
150
150
  .some(instance => instance.state === 'target_sent'
151
151
  || (instance.state === 'return_pending' && instance.return_policy === 'required'));
152
152
  }
153
+ /**
154
+ * Probe only in-flight delivery/return work. Persisted handoffs that are
155
+ * merely waiting in the store may cross a backend generation; the queue or
156
+ * interaction probe handles the active parts separately.
157
+ */
158
+ hasActiveSessionWork(selfAid, sessionId) {
159
+ return this.dispatcher.isRunning(selfAid, sessionId)
160
+ || this.store.list(selfAid).some(instance => (instance.origin_session_id === sessionId
161
+ || instance.target_session_id === sessionId) && instance.state === 'origin_queued' && this.dispatcher.isRunning(selfAid, instance.target_session_id));
162
+ }
153
163
  async bindReply(input) {
154
164
  const key = `${input.selfAid}\u0000${input.targetSessionId}`;
155
165
  return this.mutexes.forKey(key).runExclusive(async () => {