evolcore 0.0.20 → 0.0.22

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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. 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,16 @@ 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 { inspectDataMigrationRequirement } from '../data-migration.js';
30
+ import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, probeSessionActivity, } from './slash-gate.js';
28
31
  const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
29
32
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
30
33
  function defaultSlashChatmode(field) {
@@ -114,7 +117,7 @@ function resolveSlashRelationTarget(params) {
114
117
  const actualChatType = params.session?.chatType || params.chatType;
115
118
  const peerId = actualChatType === 'group'
116
119
  ? (params.session?.metadata?.groupId || params.channelId)
117
- : (params.userId || params.session?.metadata?.peerId);
120
+ : (params.userId || params.session?.metadata?.peerId || params.session?.channelId || params.channelId);
118
121
  if (!peerId)
119
122
  return { error: 'missing current peer id', code: 'MISSING_PEER' };
120
123
  const channelType = params.session?.channelType || this.resolveChannelType?.(params.channel) || params.channel.split('#')[0];
@@ -419,10 +422,30 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
419
422
  // 权限检查:区分用户级命令和管理级命令
420
423
  const isOwner = identity.role === 'owner';
421
424
  const isAdmin = isManagementRole(identity.role);
422
- const activeChatType = activeSession?.chatType || (chatType === 'group' ? 'group' : 'private');
425
+ // A topic's latest snapshot is authoritative and intentionally strict. Do
426
+ // not let a corrupt committed record turn a backend-management command into
427
+ // an unhandled quick-command failure; /renew will report it as UNKNOWN and
428
+ // leave the binding untouched.
429
+ let topicAuthSession;
430
+ let topicAuthSessionLookupFailed = false;
431
+ if (threadId) {
432
+ try {
433
+ topicAuthSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
434
+ }
435
+ catch (error) {
436
+ topicAuthSessionLookupFailed = true;
437
+ logger.warn(`[CommandHandler] topic session lookup failed: thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
438
+ }
439
+ }
440
+ const activeChatType = topicAuthSession?.chatType
441
+ || activeSession?.chatType
442
+ || (chatType === 'group' ? 'group' : 'private');
423
443
  const getExistingSessionForCommand = async () => {
424
- if (threadId)
425
- return await this.sessionManager.getThreadSession(channel, channelId, threadId);
444
+ if (threadId) {
445
+ if (topicAuthSessionLookupFailed)
446
+ return undefined;
447
+ return topicAuthSession ?? await this.sessionManager.getThreadSession(channel, channelId, threadId);
448
+ }
426
449
  return activeSession;
427
450
  };
428
451
  const getEffectiveChatmode = (session, fallbackChatType = activeChatType) => {
@@ -497,7 +520,102 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
497
520
  agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
498
521
  });
499
522
  await this.processor.interruptSession(commandSession.id, 'stop');
523
+ this.processor?.clearPauseSession?.(commandSession.id);
524
+ }
525
+ };
526
+ /**
527
+ * Boundary-scoped activity probe shared by /renew and /compact. The queue
528
+ * snapshot is taken at gate installation; later arrivals are deliberately
529
+ * excluded because they are cutoff-after work and remain durably queued.
530
+ */
531
+ const probeBackendActivity = (current, gateSnapshot) => {
532
+ const probes = {
533
+ paused: () => this.processor?.isPauseRequested?.(current.id) ?? false,
534
+ activeStream: () => this.getAgent(channel, current.baseagent).hasActiveStream(current.id),
535
+ };
536
+ if (this.permissionGateway) {
537
+ probes.pendingPermission = () => this.permissionGateway.getPendingRequests(current.id).length > 0;
538
+ }
539
+ if (this.interactionRouter) {
540
+ probes.pendingInteraction = () => this.interactionRouter.getPending(current.id).length > 0;
541
+ }
542
+ const selfAid = current.selfAID || selfAID || this.getOwningAgent(channel)?.aid;
543
+ if (this.handoffRuntime) {
544
+ probes.activeHandoff = () => {
545
+ if (!selfAid)
546
+ throw new Error('handoff activity owner unavailable');
547
+ return this.handoffRuntime.hasActiveSessionWork(selfAid, current.id);
548
+ };
549
+ probes.openHandoffTarget = () => {
550
+ if (!selfAid)
551
+ throw new Error('handoff activity owner unavailable');
552
+ return this.handoffRuntime.hasOpenTarget(selfAid, current.id);
553
+ };
554
+ }
555
+ return probeSessionActivity({
556
+ prequeue: gateSnapshot.prequeue,
557
+ active: gateSnapshot.active,
558
+ queued: gateSnapshot.queued,
559
+ dispatching: gateSnapshot.dispatching,
560
+ processing: !!current.processingState,
561
+ activeTurn: !!current.metadata?.turnState?.active,
562
+ }, probes);
563
+ };
564
+ const withIdleTopicBackend = async (sessionId, expectedThreadId, action, fallbackSession) => {
565
+ // Main-session /compact keeps its existing idle guard and provider path.
566
+ // Only topic backend operations require the stronger dequeue barrier.
567
+ if (!expectedThreadId) {
568
+ let current = fallbackSession;
569
+ if (!current) {
570
+ try {
571
+ current = typeof this.sessionManager.getSessionById === 'function'
572
+ ? await this.sessionManager.getSessionById(sessionId)
573
+ : undefined;
574
+ }
575
+ catch (error) {
576
+ logger.warn(`[CommandHandler] main session snapshot read failed: ${error instanceof Error ? error.message : String(error)}`);
577
+ return { ok: false, reason: 'unknown' };
578
+ }
579
+ }
580
+ if (!current || current.threadId)
581
+ return { ok: false, reason: 'missing' };
582
+ // Preserve the legacy main-session /compact exclusion. Topic commands
583
+ // use the stronger barrier below; main sessions retain the historical
584
+ // keyed dequeue lock and provider behavior.
585
+ const releaseLock = typeof this.messageQueue.acquireLock === 'function'
586
+ ? this.messageQueue.acquireLock(sessionId)
587
+ : undefined;
588
+ try {
589
+ return { ok: true, value: await action(current) };
590
+ }
591
+ finally {
592
+ releaseLock?.();
593
+ }
594
+ }
595
+ if (typeof this.messageQueue.withSessionBarrier !== 'function') {
596
+ return { ok: false, reason: 'unknown' };
500
597
  }
598
+ return this.messageQueue.withSessionBarrier(sessionId, async (snapshot) => {
599
+ let current;
600
+ try {
601
+ current = typeof this.sessionManager.getSessionById === 'function'
602
+ ? await this.sessionManager.getSessionById(sessionId)
603
+ : expectedThreadId
604
+ ? await this.sessionManager.getThreadSession(channel, channelId, expectedThreadId)
605
+ : fallbackSession;
606
+ }
607
+ catch (error) {
608
+ logger.warn(`[CommandHandler] session snapshot read failed: ${error instanceof Error ? error.message : String(error)}`);
609
+ return { ok: false, reason: 'unknown' };
610
+ }
611
+ if (!current || (current.threadId || undefined) !== expectedThreadId) {
612
+ return { ok: false, reason: 'missing' };
613
+ }
614
+ const activity = probeBackendActivity(current, snapshot);
615
+ if (activity !== 'idle')
616
+ return { ok: false, reason: activity };
617
+ return { ok: true, value: await action(current, snapshot) };
618
+ });
501
619
  };
502
620
  // /help 命令不需要会话
503
621
  if (normalizedContent === '/help') {
@@ -517,7 +635,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
517
635
  const appendProcessCommands = (lines) => {
518
636
  if (!isDaemonOwner)
519
637
  return;
520
- lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置');
638
+ lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /upgrade - 检查版本更新', ' /reload [aid] - 热重载 Agent 配置', ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)');
521
639
  };
522
640
  if (!isAdmin && activeChatType === 'group') {
523
641
  const lines = [
@@ -559,6 +677,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
559
677
  ' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
560
678
  ' /name <新名称> - 重命名当前会话',
561
679
  ' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
680
+ ...(threadId ? [' /renew - 在当前话题中轮换 backend'] : []),
562
681
  ' /pause - 在下一次工具调用前暂停当前任务',
563
682
  ' /resume - 继续已暂停的当前任务',
564
683
  ' /stop - 中断当前任务',
@@ -608,6 +727,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
608
727
  ' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
609
728
  ' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
610
729
  ' /compact - 压缩会话上下文(减少 token 用量)',
730
+ ' /renew - 在话题中轮换 backend(下一条消息懒初始化)',
611
731
  ' /pause - 在下一次工具调用前暂停当前任务',
612
732
  ' /resume - 继续已暂停的当前任务',
613
733
  ' /stop - 中断当前任务',
@@ -639,6 +759,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
639
759
  ...(isDaemonOwner ? [
640
760
  ' /restart - 重启服务',
641
761
  ' /reload [aid] - 热重载 Agent 配置',
762
+ ' /fa <提示词> - 单次使用宿主级 fullaccess 执行(别名: /fullaccess)',
642
763
  ] : []),
643
764
  ...(!isDaemonOwner && isAdmin ? [
644
765
  ' /reload - 热重载当前 Agent 配置',
@@ -682,6 +803,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
682
803
  cmds.push({ command: '/rewind', aliases: ['/rw'], args: '[N] [chat|file|all]', description: '查看历史/撤销指定轮次', category: '会话管理', roles: ['admin', 'owner'] });
683
804
  cmds.push({ command: '/compact', description: '压缩会话上下文(减少 token 用量)', category: '会话管理', roles: ['admin', 'owner'] });
684
805
  }
806
+ if (threadId && (activeChatType !== 'group' || isAdmin)) {
807
+ cmds.push({ command: '/renew', description: '轮换当前话题 backend', category: '会话管理', roles: activeChatType === 'group' ? ['admin', 'owner'] : ['visitor', 'member', 'admin', 'owner'] });
808
+ }
685
809
  // Agent 与模型
686
810
  if (isAdmin)
687
811
  cmds.push({ command: '/baseagent', aliases: ['/base'], args: '[name]', description: '查看或切换 Agent 后端', category: 'Agent 与模型', roles: ['admin', 'owner'] });
@@ -707,6 +831,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
707
831
  cmds.push({ command: '/observable', args: '[true|false]', description: '查看或切换观察者模式', category: '运维', roles: ['owner'] });
708
832
  }
709
833
  if (isDaemonOwner) {
834
+ cmds.push({ command: '/fa', aliases: ['/fullaccess'], args: '<提示词>', description: '仅让当前提示词对应的 run 使用宿主级 fullaccess', category: '权限管理', roles: ['daemon-owner'] });
710
835
  cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['daemon-owner'] });
711
836
  cmds.push({ command: '/upgrade', description: '检查版本更新', category: '运维', roles: ['daemon-owner'] });
712
837
  cmds.push({ command: '/reload', args: '[aid]', description: '热重载 Agent 配置', category: '运维', roles: ['daemon-owner'] });
@@ -962,7 +1087,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
962
1087
  }
963
1088
  await interruptPausedSessionBeforeReplacement(await getExistingSessionForCommand());
964
1089
  const previousDefaultBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
965
- owningAgent.setActiveBaseagent(args);
1090
+ try {
1091
+ owningAgent.setActiveBaseagent(args);
1092
+ }
1093
+ catch (error) {
1094
+ return { kind: 'command.error', text: `❌ ${error instanceof Error ? error.message : String(error)}` };
1095
+ }
966
1096
  this.eventBus.publish({
967
1097
  type: 'agent:baseagent-changed',
968
1098
  aid: owningAgent.aid,
@@ -1655,7 +1785,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1655
1785
  }).join('\n');
1656
1786
  return { kind: 'command.error', text: `❌ 该 Agent 有 ${busyInfo.count} 个任务执行中,无法 reload。\n\n处理中:\n${processingLines}\n\n等待任务完成后重试,通常 30-60 秒。` };
1657
1787
  }
1658
- const res = await execAgentAction('reload', { aid: targetAid }, userId ?? '', this.eventBus);
1788
+ const res = await execAgentAction('reload', { aid: targetAid }, userId ?? '', this.eventBus, this.agentApplicationService);
1659
1789
  if ('error' in res)
1660
1790
  return { kind: 'command.error', text: `❌ reload 失败:${res.error}` };
1661
1791
  return { kind: 'command.result', text: `✅ Agent ${targetAid} 配置已重载` };
@@ -2009,41 +2139,228 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2009
2139
  if (normalizedContent === '/clear') {
2010
2140
  return { kind: 'command.error', text: '⚠️ /clear 已移除\n\n请使用 /new [名称] 创建新会话来开始全新上下文。旧会话会保留,可通过 /s 查看或切换。' };
2011
2141
  }
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` };
2142
+ // /renew:只支持话题会话。它废止当前 backend 绑定;下一条普通消息
2143
+ // 才会在新的 TurnLease 中懒创建并绑定新 backend。
2144
+ if (normalizedContent === '/renew' || /^\/renew\s/.test(normalizedContent)) {
2145
+ if (normalizedContent !== '/renew') {
2146
+ return { kind: 'system.error', text: '❌ /renew 不接受参数,请直接使用 /renew', subtype: 'session.renew.invalid_arguments', recoverable: true };
2020
2147
  }
2021
- if (!session.agentSessionId) {
2022
- return { kind: 'command.error', text: ' 当前会话没有历史记录,无需压缩' };
2148
+ if (!threadId) {
2149
+ return { kind: 'system.error', text: '⚠️ /renew 仅支持话题会话', subtype: 'session.renew.unsupported', recoverable: true };
2023
2150
  }
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);
2151
+ // Card callbacks use synthetic message IDs on several adapters. They are
2152
+ // suitable for UI correlation, but not for the durable inbound claim.
2153
+ if (source === 'card-trigger') {
2154
+ return { kind: 'system.error', text: '⚠️ /renew 仅支持携带原生消息 ID 的普通消息', subtype: 'session.renew.invalid_source', recoverable: true };
2155
+ }
2156
+ let renewSession;
2029
2157
  try {
2158
+ // Re-read rather than trusting the authorization lookup above: only a
2159
+ // fresh strict snapshot may be used to choose the barrier key.
2160
+ renewSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
2161
+ }
2162
+ catch (error) {
2163
+ logger.warn(`[CommandHandler] /renew topic lookup failed: thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
2164
+ return { kind: 'system.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试', subtype: 'session.renew.state_unknown', recoverable: true };
2165
+ }
2166
+ if (!renewSession) {
2167
+ return { kind: 'system.error', text: '❌ 找不到当前话题会话', subtype: 'session.renew.session_missing', recoverable: true };
2168
+ }
2169
+ const inboundMessageId = messageId?.trim();
2170
+ if (!inboundMessageId) {
2171
+ return { kind: 'system.error', text: '❌ /renew 缺少稳定的消息 ID,当前渠道不支持安全重试', subtype: 'session.renew.message_id_missing', recoverable: true };
2172
+ }
2173
+ if (typeof this.sessionManager.rotateTopicBackend !== 'function'
2174
+ || typeof this.messageQueue.withSessionBarrier !== 'function'
2175
+ || typeof this.messageQueue.claimInbound !== 'function') {
2176
+ return { kind: 'system.error', text: '❌ 当前运行环境不支持安全 backend 轮换', subtype: 'session.renew.unavailable', recoverable: true };
2177
+ }
2178
+ try {
2179
+ const outcome = await this.messageQueue.withSessionBarrier(renewSession.id, async (snapshot) => {
2180
+ let current;
2181
+ try {
2182
+ current = typeof this.sessionManager.getSessionById === 'function'
2183
+ ? await this.sessionManager.getSessionById(renewSession.id)
2184
+ : undefined;
2185
+ }
2186
+ catch (error) {
2187
+ logger.warn(`[CommandHandler] /renew strict session read failed: session=${renewSession.id}: ${error instanceof Error ? error.message : String(error)}`);
2188
+ return { kind: 'unknown' };
2189
+ }
2190
+ if (!current || current.threadId !== threadId)
2191
+ return { kind: 'missing' };
2192
+ let claim;
2193
+ try {
2194
+ claim = this.messageQueue.claimInbound(current.id, inboundMessageId, selfAID ?? current.selfAID);
2195
+ }
2196
+ catch (error) {
2197
+ logger.error(`[CommandHandler] /renew inbound claim failed: session=${current.id}: ${error instanceof Error ? error.message : String(error)}`);
2198
+ return { kind: 'claim_failed' };
2199
+ }
2200
+ if (claim === 'duplicate')
2201
+ return { kind: 'duplicate' };
2202
+ const activity = probeBackendActivity(current, snapshot);
2203
+ if (activity !== 'idle')
2204
+ return { kind: activity };
2205
+ const result = await this.sessionManager.rotateTopicBackend({
2206
+ sessionId: current.id,
2207
+ expectedThreadId: threadId,
2208
+ });
2209
+ return {
2210
+ kind: 'rotated',
2211
+ wasBound: result.wasBound,
2212
+ };
2213
+ });
2214
+ if (outcome.kind === 'missing') {
2215
+ return { kind: 'system.error', text: '❌ 当前话题会话已不存在或归属已变化', subtype: 'session.renew.session_changed', recoverable: true };
2216
+ }
2217
+ if (outcome.kind === 'claim_failed') {
2218
+ return { kind: 'system.error', text: '❌ 当前渠道无法安全记录该请求,未执行 backend 轮换', subtype: 'session.renew.claim_failed', recoverable: true };
2219
+ }
2220
+ if (outcome.kind === 'duplicate') {
2221
+ return { kind: 'system.notice', text: 'ℹ️ 该请求已接收过,不再重复执行', subtype: 'session.renew.duplicate' };
2222
+ }
2223
+ if (outcome.kind === 'busy' || outcome.kind === 'unknown') {
2224
+ return {
2225
+ kind: 'system.error',
2226
+ text: outcome.kind === 'busy'
2227
+ ? '⚠️ 当前会话忙碌,无法安全轮换,请稍后再试'
2228
+ : '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试',
2229
+ subtype: outcome.kind === 'busy' ? 'session.renew.busy' : 'session.renew.state_unknown',
2230
+ recoverable: true,
2231
+ };
2232
+ }
2233
+ if (outcome.kind !== 'rotated') {
2234
+ return { kind: 'system.error', text: '⚠️ 当前会话状态未知,无法安全轮换,请稍后再试', subtype: 'session.renew.state_unknown', recoverable: true };
2235
+ }
2236
+ return {
2237
+ kind: 'system.notice',
2238
+ text: `✅ backend 已切换${outcome.wasBound ? '' : '(当前未绑定 backend)'};下一条消息将初始化新 backend(阶段一不携带历史摘要)`,
2239
+ subtype: 'session.renewed',
2240
+ };
2241
+ }
2242
+ catch (error) {
2243
+ logger.error(`[CommandHandler] /renew failed: session=${renewSession.id}: ${error instanceof Error ? error.message : String(error)}`);
2244
+ return { kind: 'system.error', text: `❌ backend 轮换失败:${error instanceof Error ? error.message : String(error)}`, subtype: 'session.renew.failed', recoverable: true };
2245
+ }
2246
+ }
2247
+ // /compact 命令:手动压缩会话上下文
2248
+ if (normalizedContent === '/compact') {
2249
+ const session = await getExistingSessionForCommand();
2250
+ if (!session) {
2251
+ return topicAuthSessionLookupFailed
2252
+ ? { kind: 'command.error', text: '⚠️ 当前会话状态未知,无法安全压缩,请稍后再试' }
2253
+ : { kind: 'command.error', text: '❌ 当前没有活跃会话,无需压缩' };
2254
+ }
2255
+ if (!session.threadId)
2256
+ await interruptPausedSessionBeforeReplacement(session);
2257
+ const outcome = await withIdleTopicBackend(session.id, session.threadId || undefined, async (session) => {
2258
+ if (!session.agentSessionId) {
2259
+ return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需压缩' };
2260
+ }
2261
+ const projectPath = path.isAbsolute(session.projectPath)
2262
+ ? session.projectPath
2263
+ : path.resolve(process.cwd(), session.projectPath);
2264
+ const currentSessionAgent = this.getAgent(channel, session.baseagent);
2265
+ if (!currentSessionAgent.capabilities?.compact) {
2266
+ return { kind: 'command.error', text: `❌ 当前 Agent (${currentSessionAgent.name}) 不支持 /compact` };
2267
+ }
2030
2268
  if (sendMessage) {
2031
2269
  await sendMessage(channelId, '⏳ 正在压缩会话上下文...', this.getReplyContext(session));
2032
2270
  }
2033
- const compacted = await sessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
2034
- if (compacted) {
2271
+ // Resolve the same relation/agent effective model used for normal turns.
2272
+ // The runner instance's model is only the final fallback; without this
2273
+ // override Claude's /compact command could resume with a stale agent
2274
+ // default (for example glm-5.3) while the turn itself used gpt-5.6-sol.
2275
+ const compactBaseagentRaw = session.baseagent || currentSessionAgent.name;
2276
+ const compactBaseagentNormalized = normalizeBaseagent(compactBaseagentRaw);
2277
+ const compactBaseagent = compactBaseagentNormalized.canonical === 'unknown'
2278
+ ? compactBaseagentRaw
2279
+ : compactBaseagentNormalized.canonical;
2280
+ const compactChatType = session.chatType;
2281
+ const compactRelation = resolveSlashRelationTarget.call(this, {
2282
+ session,
2283
+ channel,
2284
+ channelId,
2285
+ userId,
2286
+ selfAID,
2287
+ role: session.identity?.role || identity.role,
2288
+ chatType: compactChatType,
2289
+ });
2290
+ const compactSelector = 'error' in compactRelation
2291
+ ? { self: selfAID ?? session.selfAID ?? this.resolveSelfAID(channel) }
2292
+ : compactRelation;
2293
+ let compactConfig = {};
2294
+ try {
2295
+ compactConfig = (resolveEffective(compactSelector, { cache: true }).baseagents || {})[compactBaseagent] || {};
2296
+ }
2297
+ catch {
2298
+ compactConfig = {};
2299
+ }
2300
+ const compactModelDecision = constrainResolvedModelForRole({
2301
+ role: session.identity?.role || identity.role,
2302
+ baseagent: compactBaseagent,
2303
+ model: typeof compactConfig.model === 'string' ? compactConfig.model : undefined,
2304
+ resolveModelId: typeof currentSessionAgent.resolveModelId === 'function'
2305
+ ? currentSessionAgent.resolveModelId.bind(currentSessionAgent)
2306
+ : undefined,
2307
+ selfAid: compactSelector.self,
2308
+ });
2309
+ const compactModel = compactModelDecision.model
2310
+ || (typeof currentSessionAgent.getModel === 'function' ? currentSessionAgent.getModel() : undefined);
2311
+ const compactEffortDecision = resolveRuntimeStringField({
2312
+ selfAid: compactSelector.self,
2313
+ role: session.identity?.role || identity.role,
2314
+ field: `baseagents.${compactBaseagent}.effort`,
2315
+ configuredValue: compactConfig.effort,
2316
+ });
2317
+ const compactConfiguredEffort = compactEffortDecision.effectiveValue;
2318
+ const compactEffort = compactConfiguredEffort && compactConfiguredEffort !== 'auto'
2319
+ ? compactConfiguredEffort
2320
+ : (compactConfiguredEffort === 'auto' ? undefined : currentSessionAgent.getEffort?.());
2321
+ // Only send an explicit override when config/role resolution decided a
2322
+ // value. If neither scope provides one, the runner's own getModel/getEffort
2323
+ // fallback is the same effective value and preserving an omitted fourth
2324
+ // argument keeps older runner adapters source-compatible.
2325
+ const hasExplicitCompactModel = typeof compactConfig.model === 'string' && compactConfig.model.trim().length > 0
2326
+ || compactModelDecision.constrained;
2327
+ const hasExplicitCompactEffort = typeof compactConfig.effort === 'string' && compactConfig.effort.trim().length > 0
2328
+ || compactEffortDecision.decidedBy === 'role';
2329
+ const compactOverride = (hasExplicitCompactModel && compactModel) || (hasExplicitCompactEffort && compactEffort)
2330
+ ? {
2331
+ ...(hasExplicitCompactModel && compactModel ? { model: compactModel } : {}),
2332
+ ...(hasExplicitCompactEffort && compactEffort ? { effort: compactEffort } : {}),
2333
+ }
2334
+ : undefined;
2335
+ const compacted = compactOverride
2336
+ ? await currentSessionAgent.compactSession(session.id, session.agentSessionId, projectPath, compactOverride)
2337
+ : await currentSessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
2338
+ const compactResult = compacted;
2339
+ if (compactResult === true || compactResult?.ok === true) {
2035
2340
  return {
2036
2341
  kind: 'command.result',
2037
2342
  text: '✅ 会话压缩完成',
2038
2343
  };
2039
2344
  }
2040
2345
  else {
2041
- return { kind: 'command.error', text: '❌ 会话压缩失败,请稍后重试' };
2346
+ return {
2347
+ kind: 'command.error',
2348
+ text: `❌ 会话压缩失败(${compactResult?.code ?? 'sdk_error'}):${compactResult?.message ?? 'compact failed'}`,
2349
+ };
2042
2350
  }
2351
+ }, session);
2352
+ if (!outcome.ok) {
2353
+ if (outcome.reason === 'missing') {
2354
+ return { kind: 'command.error', text: '❌ 当前会话已不存在或归属已变化' };
2355
+ }
2356
+ return {
2357
+ kind: 'command.error',
2358
+ text: outcome.reason === 'busy'
2359
+ ? '⚠️ 当前会话忙碌,请稍后再试\n使用 /stop 中断当前任务后重试'
2360
+ : '⚠️ 当前会话状态未知,无法安全压缩,请稍后再试',
2361
+ };
2043
2362
  }
2044
- finally {
2045
- releaseLock();
2046
- }
2363
+ return outcome.value;
2047
2364
  }
2048
2365
  // 后续命令可读取现有会话,但不应在这里隐式创建新会话。
2049
2366
  // 真正需要新会话的命令应显式调用 createNewSession()。
@@ -2138,13 +2455,17 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2138
2455
  if (sessionName) {
2139
2456
  const existing = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
2140
2457
  if (existing) {
2141
- return { kind: 'command.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称` };
2458
+ return { kind: 'system.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称`, subtype: 'session.new.name_conflict', recoverable: true };
2142
2459
  }
2143
2460
  }
2144
2461
  await interruptPausedSessionBeforeReplacement(session || activeSession);
2145
2462
  const projectPath = this.getEffectiveDefaultPath(channel);
2146
2463
  if (sendMessage && session) {
2147
- await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session));
2464
+ await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session), {
2465
+ kind: 'system.notice',
2466
+ text: `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`,
2467
+ subtype: 'session.creating',
2468
+ });
2148
2469
  }
2149
2470
  const newSessionBaseagent = this.agentRegistry?.resolveByChannel(channel)?.baseagent || this.parseDefaultBaseagent();
2150
2471
  const previousMetadata = session?.metadata || activeSession?.metadata || {};
@@ -2211,7 +2532,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2211
2532
  ?? newRunner.getEffort?.()
2212
2533
  ?? newAgent?.effort;
2213
2534
  const backendBits = [newBaseagent, backendModel, backendEffort].filter(Boolean).join(' · ');
2214
- return { kind: 'command.result', text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 后端: ${backendBits}\n 之前的对话历史已保留,可通过 /s 查看${handoffDiscardWarning ? `\n${handoffDiscardWarning}` : ''}` };
2535
+ return {
2536
+ kind: 'system.notice',
2537
+ text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 后端: ${backendBits}\n 之前的对话历史已保留,可通过 /s 查看${handoffDiscardWarning ? `\n${handoffDiscardWarning}` : ''}`,
2538
+ subtype: 'session.created',
2539
+ };
2215
2540
  }
2216
2541
  // /check 命令:检查 EvolAgent 实例健康(visitor/member 可用,详情仅 admin)
2217
2542
  if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
@@ -2460,7 +2785,6 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2460
2785
  const count = this.messageCache.getCount(s.id);
2461
2786
  return `${s.projectPath} 有 ${count} 条新消息`;
2462
2787
  });
2463
- // 执行重启逻辑(共用于卡片回调和文本确认)
2464
2788
  const executeRestart = async () => {
2465
2789
  const suppressRealRestart = shouldSuppressRealRestart();
2466
2790
  let restartReplyContext = replyContext;
@@ -2501,6 +2825,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2501
2825
  logger.warn(`[System] Refusing restart without a valid explicit delivery route: channel=${channel} channelId=${channelId}`);
2502
2826
  return false;
2503
2827
  }
2828
+ const migrationRequirement = inspectDataMigrationRequirement(resolvePaths().root);
2829
+ if (migrationRequirement.required) {
2830
+ logger.warn(`[System] Refusing restart while data migration is pending: operations=${migrationRequirement.operationCount}`);
2831
+ return { code: 'DATA_MIGRATION_REQUIRED', operationCount: migrationRequirement.operationCount };
2832
+ }
2504
2833
  const restartInfo = {
2505
2834
  channel,
2506
2835
  channelId,
@@ -2579,6 +2908,13 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2579
2908
  }
2580
2909
  }
2581
2910
  const restarted = await executeRestart();
2911
+ if (typeof restarted === 'object' && restarted.code === 'DATA_MIGRATION_REQUIRED') {
2912
+ return {
2913
+ kind: 'command.error',
2914
+ text: `❌ 待处理的用户数据迁移(${restarted.operationCount} 项)阻止重启。请先运行 ec data migrate --dry-run,然后运行 ec data migrate --apply。`,
2915
+ reason: restarted.code,
2916
+ };
2917
+ }
2582
2918
  if (!restarted) {
2583
2919
  return { kind: 'command.error', text: '❌ 无法确定重启通知的出站路由,请从可信会话上下文重试' };
2584
2920
  }
@@ -3203,7 +3539,12 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3203
3539
  const backupPath = await backupSessionFile(sessionFile);
3204
3540
  const fsPromises = await import('fs/promises');
3205
3541
  await fsPromises.unlink(sessionFile);
3206
- await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
3542
+ // A topic backend binding may only be changed by an explicit boundary
3543
+ // rotation/activation. Repair clears runner-local state, but must not
3544
+ // turn a legacy empty callback into an authoritative binding mutation.
3545
+ if (!repairSession.threadId) {
3546
+ await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
3547
+ }
3207
3548
  repairAgent.updateSessionId(repairSession.id, '');
3208
3549
  await this.sessionManager.resetHealthStatus(repairSession.id);
3209
3550
  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 +3581,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3240
3581
  });
3241
3582
  if (triggerAuthDenied)
3242
3583
  return triggerAuthDenied;
3243
- const text = await this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin, messageId, chatType, threadId);
3584
+ const text = await this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin, messageId, chatType, threadId, isDaemonOwner, authSubject.canApprovePersistentFullAccess);
3244
3585
  return { kind: 'command.result', text };
3245
3586
  }
3246
3587
  return null;
@@ -768,7 +768,17 @@ export function planDataMigration(root) {
768
768
  }
769
769
  catch { }
770
770
  const contactAudit = path.join(root, 'data', 'contact-book-audit.jsonl');
771
- if (fs.existsSync(contactAudit)) {
771
+ // The legacy audit file may be provisioned as an empty placeholder (for
772
+ // example by a protected-path sandbox). An empty source has nothing to
773
+ // partition and must not block daemon startup.
774
+ let hasContactAuditRecords = false;
775
+ try {
776
+ const stat = fs.statSync(contactAudit);
777
+ hasContactAuditRecords = stat.isFile()
778
+ && fs.readFileSync(contactAudit, 'utf8').trim().length > 0;
779
+ }
780
+ catch { }
781
+ if (hasContactAuditRecords) {
772
782
  const operationId = nextId('contact-audit');
773
783
  operations.push({ id: operationId, kind: 'contact-audit-partition', source: contactAudit, staging: path.join(stagingDir, operationId), status: 'planned' });
774
784
  }