evolcore 0.0.17 → 0.0.18

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 (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/codex-managed-hook.mjs +16 -7
  3. package/bin/install-codex-managed-hooks.mjs +4 -2
  4. package/dist/agents/claude-runner.js +113 -24
  5. package/dist/agents/codex-app-server-client.js +6 -1
  6. package/dist/agents/codex-runner.js +21 -6
  7. package/dist/agents/ecagent-runner.js +39 -10
  8. package/dist/agents/gemini-runner.js +90 -19
  9. package/dist/aun/aid/store.js +36 -0
  10. package/dist/aun/msg/p2p.js +20 -8
  11. package/dist/channels/aun.js +159 -21
  12. package/dist/cli/agent-command.js +67 -6
  13. package/dist/cli/agent.js +26 -0
  14. package/dist/cli/command-log.js +23 -4
  15. package/dist/cli/daemon-commands.js +53 -12
  16. package/dist/cli/init.js +21 -5
  17. package/dist/cli/restart-monitor.js +13 -6
  18. package/dist/cli/watch-logs.js +2 -2
  19. package/dist/config/builtin-roles.js +5 -1
  20. package/dist/config/role-ranks.js +4 -0
  21. package/dist/core/audit/event-key.js +29 -0
  22. package/dist/core/audit/log-integrity.js +13 -3
  23. package/dist/core/auth/auth-gateway.js +14 -18
  24. package/dist/core/auth/authorization-audit.js +110 -3
  25. package/dist/core/auth/authorization-denial.js +17 -0
  26. package/dist/core/auth/operation-authorizer.js +143 -18
  27. package/dist/core/auth/operation-catalog.js +21 -5
  28. package/dist/core/bootstrap-messages.js +11 -6
  29. package/dist/core/bootstrap-service.js +26 -4
  30. package/dist/core/causation/aun-association.js +7 -4
  31. package/dist/core/command/agent-control.js +25 -16
  32. package/dist/core/command/command-handler.js +50 -4
  33. package/dist/core/command/group-menu.js +1 -1
  34. package/dist/core/command/menu-catalog.js +32 -7
  35. package/dist/core/command/menu-handler.js +59 -23
  36. package/dist/core/command/menu-protocol.js +196 -0
  37. package/dist/core/command/slash-gate.js +14 -5
  38. package/dist/core/command/slash-handler.js +81 -99
  39. package/dist/core/event-catalog.js +18 -0
  40. package/dist/core/message/message-bridge.js +72 -9
  41. package/dist/core/message/pause-controller.js +53 -0
  42. package/dist/core/message/response-engine.js +97 -11
  43. package/dist/core/permission/sandbox-runtime.js +79 -13
  44. package/dist/core/permission/tool-policy.js +1 -1
  45. package/dist/index.js +357 -48
  46. package/dist/ipc.js +75 -4
  47. package/dist/utils/atomic-write.js +45 -11
  48. package/dist/utils/logger.js +27 -0
  49. package/dist/utils/windows-autostart.js +740 -83
  50. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  51. package/ecagent/dist/harness/agent-harness.js +6 -4
  52. package/kits/docs/evolcore/config.md +1 -1
  53. package/kits/docs/evolcore/group-rules.md +2 -1
  54. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  55. package/kits/eck_manifest.json +25 -16
  56. package/kits/rules/01-overview.md +5 -5
  57. package/kits/rules/03-identity.md +1 -1
  58. package/kits/rules/04-relation.md +4 -4
  59. package/kits/rules/05-venue.md +5 -5
  60. package/kits/templates/bootstrap-welcome.md +3 -1
  61. package/kits/templates/system-fragments/bootstrap.md +17 -9
  62. package/package.json +1 -1
@@ -7,18 +7,18 @@ import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
7
7
  import crypto from 'crypto';
8
8
  import path from 'path';
9
9
  import fs from 'fs';
10
- import os from 'os';
11
10
  import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../../utils/npm-ops.js';
12
11
  import { loadDaemonConfig } from '../../config-store.js';
13
12
  import { read as cfgRead, resolveEffective, routeFieldPath, write as cfgWrite, } from '../../config/config-manager.js';
14
13
  import { execAgentAction } from './agent-control.js';
15
14
  import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
15
+ import { authorizationNextStep, formatAuthorizationDenial } from '../auth/authorization-denial.js';
16
16
  import { resolvePermissionMode, writeScope } from '../model/config-scope.js';
17
17
  import { formatPeerKey } from '../relation/peer-identity.js';
18
18
  import { modelMatches } from '../model/model-catalog.js';
19
19
  import { formatModelCheck, runModelCheck } from '../model/model-diagnostics.js';
20
20
  import { filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
21
- import { displaySessionTitle, isSyntheticCliPrompt } from '../session/session-title.js';
21
+ import { displaySessionTitle } from '../session/session-title.js';
22
22
  import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
23
23
  import { normalizePermissionMode as normalizePermissionModeContract, PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
24
24
  import { isManagementRole } from '../../config/builtin-roles.js';
@@ -157,7 +157,7 @@ async function authorizeSlashIntent(params) {
157
157
  const channelType = channel.split('#')[0];
158
158
  const peerKey = channelType && peerKeyId ? formatPeerKey(channelType, peerKeyId) : undefined;
159
159
  const subject = params.subject
160
- ? { ...params.subject, role: identity.role, identity }
160
+ ? { ...params.subject, identity }
161
161
  : buildAuthSubject({
162
162
  selfAid,
163
163
  actorId: userId,
@@ -175,7 +175,13 @@ async function authorizeSlashIntent(params) {
175
175
  }
176
176
  const decision = await authorizeOperation({ source: 'slash', intent, subject });
177
177
  if (!decision.allow) {
178
- return { kind: 'command.error', text: decision.reason };
178
+ const reasonCode = decision.command?.reasonCode;
179
+ return {
180
+ kind: 'command.error',
181
+ text: formatAuthorizationDenial(decision.reason, reasonCode),
182
+ reason: decision.code,
183
+ ...(reasonCode ? { reasonCode, nextStep: authorizationNextStep(reasonCode) } : {}),
184
+ };
179
185
  }
180
186
  return null;
181
187
  }
@@ -471,6 +477,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
471
477
  activeAgent: getActiveAgentIfAvailable(),
472
478
  sessionManager: this.sessionManager,
473
479
  messageQueue: this.messageQueue,
480
+ isSessionPaused: sessionId => this.processor?.isPauseRequested?.(sessionId) ?? false,
474
481
  getAgentForSession: session => this.getAgent(channel, session.baseagent),
475
482
  });
476
483
  if (idleGuard)
@@ -481,6 +488,17 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
481
488
  const isCmd = isRecognizedSlashCommand(normalizedContent);
482
489
  if (!isCmd)
483
490
  return undefined;
491
+ const interruptPausedSessionBeforeReplacement = async (commandSession) => {
492
+ if (commandSession && (this.processor?.isPauseRequested?.(commandSession.id) ?? false)) {
493
+ this.eventBus.publish({
494
+ type: 'task:interrupted',
495
+ sessionId: commandSession.id,
496
+ reason: 'stop',
497
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
498
+ });
499
+ await this.processor.interruptSession(commandSession.id, 'stop');
500
+ }
501
+ };
484
502
  // /help 命令不需要会话
485
503
  if (normalizedContent === '/help') {
486
504
  const canReadModel = canAccessSlashOperation('model.current', 'relation')
@@ -541,6 +559,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
541
559
  ' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
542
560
  ' /name <新名称> - 重命名当前会话',
543
561
  ' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
562
+ ' /pause - 在下一次工具调用前暂停当前任务',
563
+ ' /resume - 继续已暂停的当前任务',
564
+ ' /stop - 中断当前任务',
544
565
  ' /status - 显示会话状态',
545
566
  ' /check - 检查 EvolAgent 实例健康',
546
567
  ...((canReadModel || canUseModel || canSetEffort) ? [
@@ -587,6 +608,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
587
608
  ' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
588
609
  ' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
589
610
  ' /compact - 压缩会话上下文(减少 token 用量)',
611
+ ' /pause - 在下一次工具调用前暂停当前任务',
612
+ ' /resume - 继续已暂停的当前任务',
613
+ ' /stop - 中断当前任务',
590
614
  '',
591
615
  '🤖 Agent 与模型:',
592
616
  ' /baseagent [name] - 查看或切换 Agent 后端(别名: /base)',
@@ -605,6 +629,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
605
629
  '',
606
630
  '🛠️ 运维:',
607
631
  ' /status - 显示会话状态',
632
+ ' /pause - 在下一次工具调用前暂停当前任务',
633
+ ' /resume - 继续已暂停的当前任务',
608
634
  ' /stop - 中断当前任务',
609
635
  ' /check - 检查 EvolAgent 实例健康',
610
636
  ...(isOwner ? [
@@ -670,6 +696,8 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
670
696
  cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: [identity.role] });
671
697
  // 运维
672
698
  cmds.push({ command: '/status', description: '显示会话状态', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] });
699
+ cmds.push({ command: '/pause', description: '在下一次工具调用前暂停当前任务', category: '运维', roles: ['admin', 'owner'] });
700
+ cmds.push({ command: '/resume', description: '继续已暂停的当前任务', category: '运维', roles: ['admin', 'owner'] });
673
701
  cmds.push({ command: '/stop', description: '中断当前任务', category: '运维', roles: ['admin', 'owner'] });
674
702
  cmds.push({ command: '/check', description: '检查 EvolAgent 实例健康', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] });
675
703
  if (isAdmin) {
@@ -851,92 +879,36 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
851
879
  return { kind: 'command.result', text: fb.result ?? '✓ 已回答' };
852
880
  return { kind: 'command.error', text: '❌ 当前没有待回答的问题' };
853
881
  }
854
- // /resume 命令:返回当前项目的 Claude 会话记录(JSON)
855
- if (normalizedContent === '/resume' || normalizedContent.startsWith('/resume ')) {
882
+ // /pause and /resume control the current session's next-tool gate.
883
+ if (normalizedContent.startsWith('/pause ')) {
884
+ return { kind: 'command.error', text: '用法: /pause' };
885
+ }
886
+ if (normalizedContent === '/pause') {
887
+ const pauseSession = await getExistingSessionForCommand();
888
+ if (!pauseSession)
889
+ return { kind: 'command.result', text: '当前没有活跃会话' };
890
+ const pauseAgent = this.getAgent(channel, pauseSession.baseagent);
891
+ const hasActiveTask = pauseAgent.hasActiveStream(pauseSession.id)
892
+ || this.messageQueue.isProcessing(pauseSession.id);
893
+ if (!hasActiveTask)
894
+ return { kind: 'command.result', text: '当前没有正在处理的任务' };
895
+ const requested = this.processor.pauseSession(pauseSession.id);
896
+ return {
897
+ kind: 'command.result',
898
+ text: requested
899
+ ? '✓ 已请求暂停,任务将在下一次工具调用前暂停'
900
+ : '当前任务已请求暂停或正在暂停',
901
+ };
902
+ }
903
+ if (normalizedContent.startsWith('/resume ')) {
904
+ return { kind: 'command.error', text: '用法: /resume' };
905
+ }
906
+ if (normalizedContent === '/resume') {
856
907
  const resumeSession = await getExistingSessionForCommand();
857
908
  if (!resumeSession)
858
909
  return { kind: 'command.result', text: '当前没有活跃会话' };
859
- try {
860
- const { encodePath } = await import('../../utils/cross-platform.js');
861
- const homeDir = os.homedir();
862
- const encodedPath = encodePath(resumeSession.projectPath);
863
- const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
864
- if (!fs.existsSync(projectDir)) {
865
- return { kind: 'command.error', text: '❌ 未找到 Claude 会话记录目录' };
866
- }
867
- const jsonlFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
868
- if (jsonlFiles.length === 0) {
869
- return { kind: 'command.error', text: '❌ 当前项目没有 Claude 会话记录' };
870
- }
871
- const sessions = [];
872
- for (const file of jsonlFiles) {
873
- const filePath = path.join(projectDir, file);
874
- const sessionId = file.replace('.jsonl', '');
875
- let lastTimestamp = '';
876
- let firstUserMessage = '';
877
- let model = '';
878
- let branch = '';
879
- let turns = 0;
880
- try {
881
- const content = fs.readFileSync(filePath, 'utf-8');
882
- const lines = content.split('\n').filter(l => l.trim());
883
- for (const line of lines) {
884
- const event = JSON.parse(line);
885
- if (event.timestamp && event.timestamp > lastTimestamp) {
886
- lastTimestamp = event.timestamp;
887
- }
888
- if (event.gitBranch && !branch) {
889
- branch = event.gitBranch;
890
- }
891
- if (event.type === 'user' && event.message?.role === 'user') {
892
- const msgContent = event.message.content;
893
- const isToolResult = Array.isArray(msgContent) && msgContent.every((c) => c.type === 'tool_result');
894
- if (!isToolResult) {
895
- turns++;
896
- if (!firstUserMessage) {
897
- let candidate = '';
898
- if (typeof msgContent === 'string') {
899
- candidate = msgContent;
900
- }
901
- else if (Array.isArray(msgContent)) {
902
- const textBlock = msgContent.find((c) => c.type === 'text');
903
- if (textBlock?.text) {
904
- candidate = textBlock.text;
905
- }
906
- }
907
- // 跳过 Claude Code 注入的脚手架 prompt,取第一条真人消息
908
- if (candidate && !isSyntheticCliPrompt(candidate, 'claude')) {
909
- firstUserMessage = candidate.slice(0, 100);
910
- }
911
- }
912
- }
913
- }
914
- if (event.type === 'assistant' && event.message?.model && !model) {
915
- model = event.message.model;
916
- }
917
- }
918
- }
919
- catch {
920
- continue;
921
- }
922
- if (!lastTimestamp)
923
- continue;
924
- sessions.push({
925
- sessionId,
926
- lastMessageTime: lastTimestamp,
927
- firstUserMessage: firstUserMessage || '(无消息)',
928
- model: model || 'unknown',
929
- turns,
930
- branch: branch || 'unknown',
931
- });
932
- }
933
- sessions.sort((a, b) => b.lastMessageTime.localeCompare(a.lastMessageTime));
934
- return { kind: 'command.result', text: JSON.stringify(sessions, null, 2) };
935
- }
936
- catch (error) {
937
- logger.error('[CommandHandler] /resume failed:', error);
938
- return { kind: 'command.error', text: `❌ 读取会话记录失败: ${error instanceof Error ? error.message : '未知错误'}` };
939
- }
910
+ const resumed = this.processor.resumeSession(resumeSession.id);
911
+ return { kind: 'command.result', text: resumed ? '✓ 已继续当前任务' : '当前任务未处于暂停状态' };
940
912
  }
941
913
  // /baseagent 命令:查看或切换 Agent 后端
942
914
  if (normalizedContent === '/baseagent' || normalizedContent.startsWith('/baseagent ')) {
@@ -988,6 +960,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
988
960
  if (!owningAgent) {
989
961
  return { kind: 'command.error', text: '❌ 当前 channel 无绑定 agent,无法设置 active_baseagent' };
990
962
  }
963
+ await interruptPausedSessionBeforeReplacement(await getExistingSessionForCommand());
991
964
  const previousDefaultBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
992
965
  owningAgent.setActiveBaseagent(args);
993
966
  this.eventBus.publish({
@@ -1646,11 +1619,6 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1646
1619
  if (normalizedContent === '/reload' || normalizedContent.startsWith('/reload ')) {
1647
1620
  const aidArg = normalizedContent.slice('/reload'.length).trim() || undefined;
1648
1621
  const selfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
1649
- // agent channel 的 owner/admin 不能跨 agent reload;先返回领域内错误,避免落到
1650
- // 底层 daemon-owner 约束的英文 reason。
1651
- if (!isDaemonOwner && aidArg && aidArg !== selfAid) {
1652
- return { kind: 'command.error', text: '❌ 无权限:跨 agent reload 仅限 daemon owner 使用' };
1653
- }
1654
1622
  const targetAid = aidArg ?? selfAid;
1655
1623
  if (!targetAid) {
1656
1624
  return { kind: 'command.error', text: '❌ 无法确定目标 agent,请指定 aid:/reload <aid>' };
@@ -1664,7 +1632,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1664
1632
  args: { ...(aidArg ? { aid: aidArg } : {}), ...(selfAid ? { self: selfAid } : {}) },
1665
1633
  dangerous: true,
1666
1634
  },
1667
- identity: isDaemonOwner ? { ...identity, role: 'owner' } : identity,
1635
+ identity,
1668
1636
  session: activeSession,
1669
1637
  channel,
1670
1638
  channelId,
@@ -2010,12 +1978,16 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2010
1978
  return { kind: 'command.result', text: `✅ @ 处理模式已切换: ${currentMode ?? '未设置'} → ${arg}` };
2011
1979
  }
2012
1980
  // /stop 命令:中断当前任务
1981
+ if (normalizedContent.startsWith('/stop ')) {
1982
+ return { kind: 'command.error', text: '用法: /stop' };
1983
+ }
2013
1984
  if (normalizedContent === '/stop') {
2014
1985
  const stopSession = await getExistingSessionForCommand();
2015
1986
  if (!stopSession)
2016
1987
  return { kind: 'command.result', text: '当前没有正在处理的任务' };
2017
1988
  const stopAgent = this.getAgent(channel, stopSession.baseagent);
2018
1989
  const sessionKey = stopSession.id;
1990
+ this.processor?.clearPauseSession?.(sessionKey);
2019
1991
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
2020
1992
  const hasActive = stopAgent.hasActiveStream(sessionKey);
2021
1993
  const isProcessing = this.messageQueue.isProcessing(sessionKey);
@@ -2049,6 +2021,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2049
2021
  if (!session.agentSessionId) {
2050
2022
  return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需压缩' };
2051
2023
  }
2024
+ await interruptPausedSessionBeforeReplacement(session);
2052
2025
  const projectPath = path.isAbsolute(session.projectPath)
2053
2026
  ? session.projectPath
2054
2027
  : path.resolve(process.cwd(), session.projectPath);
@@ -2168,6 +2141,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2168
2141
  return { kind: 'command.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称` };
2169
2142
  }
2170
2143
  }
2144
+ await interruptPausedSessionBeforeReplacement(session || activeSession);
2171
2145
  const projectPath = this.getEffectiveDefaultPath(channel);
2172
2146
  if (sendMessage && session) {
2173
2147
  await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session));
@@ -2291,7 +2265,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2291
2265
  groups.set(type, []);
2292
2266
  groups.get(type).push({ name, status });
2293
2267
  }
2294
- if (!isAdmin) {
2268
+ if (!isAdmin && !isDaemonOwner) {
2295
2269
  // visitor/member: 仅显示实例通道摘要
2296
2270
  const total = [...groups.values()].flat().length;
2297
2271
  const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
@@ -2443,9 +2417,6 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2443
2417
  if (normalizedContent === '/restart') {
2444
2418
  // 进程级操作:必须是 daemon owner(daemon.json.owners),与 menu 协议 /system restart 一致。
2445
2419
  // agent-channel 的 owner/admin 角色不足以重启整个 daemon。
2446
- if (!isDaemonOwner) {
2447
- return { kind: 'command.error', text: '❌ 无权限:服务重启仅限 daemon owner 使用' };
2448
- }
2449
2420
  const restartSelfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
2450
2421
  const authDenied = await authorizeIntent({
2451
2422
  intent: {
@@ -2455,7 +2426,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2455
2426
  args: {},
2456
2427
  dangerous: true,
2457
2428
  },
2458
- identity: { ...identity, role: 'owner' },
2429
+ identity,
2459
2430
  session: activeSession,
2460
2431
  channel,
2461
2432
  channelId,
@@ -2624,7 +2595,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2624
2595
  args: {},
2625
2596
  dangerous: true,
2626
2597
  },
2627
- identity: isDaemonOwner ? { ...identity, role: 'owner' } : identity,
2598
+ identity,
2628
2599
  session: activeSession,
2629
2600
  channel,
2630
2601
  channelId,
@@ -3008,6 +2979,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3008
2979
  : await this.sessionManager.listImportableCliSessions(projectPath, currentBaseagent);
3009
2980
  const cliSession = cliSessions.find((c) => c.uuid.startsWith(sessionName));
3010
2981
  if (cliSession) {
2982
+ // Importing a CLI session replaces the active session. Cancel any
2983
+ // pending tool-boundary pause only after the target has been
2984
+ // validated, so an invalid UUID cannot interrupt the current task.
2985
+ await interruptPausedSessionBeforeReplacement(session || activeSession);
3011
2986
  const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentBaseagent, selfAID ?? session?.selfAID);
3012
2987
  this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
3013
2988
  const projectName = this.getProjectName(projectPath);
@@ -3038,6 +3013,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3038
3013
  if (!session.threadId && targetSession.threadId) {
3039
3014
  return { kind: 'command.error', text: `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用` };
3040
3015
  }
3016
+ await interruptPausedSessionBeforeReplacement(session);
3017
+ this.processor?.clearPauseSession?.(session.id);
3018
+ this.processor?.clearPauseSession?.(targetSession.id);
3041
3019
  const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
3042
3020
  if (!switched) {
3043
3021
  return { kind: 'command.error', text: `❌ 切换会话失败` };
@@ -3125,6 +3103,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3125
3103
  }
3126
3104
  this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
3127
3105
  const targetAgent = this.getAgent(channel, targetSession.baseagent);
3106
+ this.processor?.clearPauseSession?.(targetSession.id);
3128
3107
  await targetAgent.closeSession(targetSession.id);
3129
3108
  return { kind: 'command.result', text: `✓ 已删除会话: ${displaySessionTitle(targetSession.name, sessionName)}\n会话文件已保留,可通过 CLI 访问` };
3130
3109
  }
@@ -3141,6 +3120,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3141
3120
  if (!forkAgent.capabilities?.fork) {
3142
3121
  return { kind: 'command.error', text: `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代` };
3143
3122
  }
3123
+ await interruptPausedSessionBeforeReplacement(session);
3144
3124
  try {
3145
3125
  const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
3146
3126
  const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
@@ -3195,6 +3175,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3195
3175
  if (!['chat', 'file', 'all'].includes(mode)) {
3196
3176
  return { kind: 'command.error', text: `❌ 无效模式 "${mode}",可选:chat | file | all` };
3197
3177
  }
3178
+ await interruptPausedSessionBeforeReplacement(session);
3198
3179
  return { kind: 'command.result', text: await this.handleRewind(session, rewindAgent, turnNum, mode) };
3199
3180
  }
3200
3181
  // /repair 命令:检查并修复会话文件
@@ -3204,6 +3185,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
3204
3185
  return { kind: 'command.result', text: '当前没有活跃会话' };
3205
3186
  const repairAgent = this.getAgent(channel, repairSession.baseagent);
3206
3187
  const { checkSessionFile, backupSessionFile } = await import('../session/session-file-health.js');
3188
+ await interruptPausedSessionBeforeReplacement(repairSession);
3207
3189
  try {
3208
3190
  if (!repairSession.agentSessionId) {
3209
3191
  await this.sessionManager.resetHealthStatus(repairSession.id);
@@ -313,10 +313,16 @@ const CATALOG = [
313
313
  { path: 'sessionId', type: 'string' },
314
314
  { path: 'toolName', type: 'string' },
315
315
  { path: 'input', type: 'object' },
316
+ { path: 'eventKey', type: 'string' },
317
+ { path: 'eventPhase', type: 'string' },
316
318
  { path: 'callId', type: 'string', optional: true },
317
319
  { path: 'correlationId', type: 'string', optional: true },
318
320
  { path: 'agentAid', type: 'string', optional: true },
319
321
  { path: 'permissionMode', type: 'string', optional: true },
322
+ { path: 'decision', type: 'string' },
323
+ { path: 'decisionSource', type: 'string' },
324
+ { path: 'executed', type: 'boolean' },
325
+ { path: 'executionState', type: 'string' },
320
326
  { path: 'timestamp', type: 'number', optional: true },
321
327
  ],
322
328
  },
@@ -328,6 +334,8 @@ const CATALOG = [
328
334
  fields: [
329
335
  { path: 'sessionId', type: 'string' },
330
336
  { path: 'toolName', type: 'string' },
337
+ { path: 'eventKey', type: 'string' },
338
+ { path: 'eventPhase', type: 'string' },
331
339
  { path: 'isError', type: 'boolean', optional: true },
332
340
  { path: 'errorCode', type: 'string', optional: true },
333
341
  { path: 'agentName', type: 'string', optional: true },
@@ -335,6 +343,10 @@ const CATALOG = [
335
343
  { path: 'correlationId', type: 'string', optional: true },
336
344
  { path: 'agentAid', type: 'string', optional: true },
337
345
  { path: 'permissionMode', type: 'string', optional: true },
346
+ { path: 'decision', type: 'string' },
347
+ { path: 'decisionSource', type: 'string' },
348
+ { path: 'executed', type: 'boolean' },
349
+ { path: 'executionState', type: 'string' },
338
350
  { path: 'timestamp', type: 'number', optional: true },
339
351
  ],
340
352
  },
@@ -846,3 +858,9 @@ export function getEventCatalog(opts = {}) {
846
858
  const namespaces = [...new Set(events.map(entry => entry.namespace))].sort();
847
859
  return { namespaces, events };
848
860
  }
861
+ /** Whether an event Trigger pattern can subscribe to daemon-internal events. */
862
+ export function eventPatternIncludesInternal(pattern) {
863
+ return CATALOG.some(entry => entry.internal === true && (pattern === '*'
864
+ || pattern === entry.type
865
+ || (pattern.endsWith(':*') && entry.namespace === pattern.slice(0, -2))));
866
+ }
@@ -17,7 +17,7 @@ import { handlePendingQQBotContactBindMessage, registerPendingQQBotContactBind }
17
17
  import { handlePendingWecomContactBindMessage, registerPendingWecomContactBind } from '../../channels/wecom.js';
18
18
  import { handlePendingWechatContactBindMessage, registerPendingWechatContactBind } from '../../channels/wechat.js';
19
19
  import { authorizeAccess, buildAuthSubject } from '../auth/auth-gateway.js';
20
- import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, isAunMenuTokenRequired, menuCommandForName, } from '../command/menu-protocol.js';
20
+ import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, isAunMenuTokenRequired, menuCommandForName, normalizeMenuResponseTiming, withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived, } from '../command/menu-protocol.js';
21
21
  import { resolveExplicitMenuExecutionContext, validateMenuCatalogArgs, validateMenuValueBatchArgs } from '../command/menu-catalog.js';
22
22
  import { MenuTokenStore } from '../command/menu-token-store.js';
23
23
  import { SessionRenewService } from '../session/session-renew.js';
@@ -691,9 +691,16 @@ export class MessageBridge {
691
691
  // that timestamp through session resolution and queue processing.
692
692
  // The fallback is only for legacy adapters that do not provide it.
693
693
  const receivedAt = msg.receivedAt ?? Date.now();
694
+ const menuTiming = normalizeMenuResponseTiming(msg);
694
695
  const channelKey = adapter?.channelKey || channelName;
695
696
  const owningAgent = this.agentRegistry?.resolveByChannel(channelKey)
696
697
  ?? this.agentRegistry?.resolveByChannel(channelName);
698
+ // Agent-owned business controls bypass ResponseEngine entirely, so
699
+ // expose them only after bootstrap has explicitly reached active.
700
+ // Missing lifecycle fails closed; control channels without an owning
701
+ // EvolAgent keep their existing command behavior.
702
+ const allowsNormalCommandRouting = !owningAgent
703
+ || owningAgent.config?.lifecycle === 'active';
697
704
  const parsedChannelKey = tryParseChannelKey(channelKey);
698
705
  const chatType = msg.chatType || 'private';
699
706
  const delivery = chatType === 'group'
@@ -772,14 +779,21 @@ export class MessageBridge {
772
779
  logger.info(`[MessageBridge] Inbound admission denied before business routing: self=${selfAid} actor=${actorId ?? '<none>'} channel=${channelKey} reason=${admissionDecision.reason}`);
773
780
  return;
774
781
  }
775
- const menuControl = resolvedChannelType === 'aun'
782
+ const menuControl = allowsNormalCommandRouting && resolvedChannelType === 'aun'
776
783
  ? parseMenuControl(content)
777
784
  : { isMenu: false };
778
785
  let menuResponseWarning;
779
786
  if (menuControl.isMenu) {
787
+ // AUN captures this mark at its adapter boundary. Legacy/test
788
+ // adapters fall back to the bridge entry mark captured above.
789
+ Object.assign(msg, menuTiming);
780
790
  this.logMenuInbound(channelName, msg, menuControl);
781
791
  if (!hasValidMenuId(menuControl)) {
782
792
  this.logMenuDiagnostic('missing-id', channelName, msg, menuControl);
793
+ logMenuRequestCompleted(menuControl.request, undefined, this.menuFlowContext(channelName, msg), {
794
+ delivery: 'dropped',
795
+ reason: 'missing-id',
796
+ });
783
797
  return;
784
798
  }
785
799
  const versionError = evaluateEvolMenuVersionGate({
@@ -916,7 +930,9 @@ export class MessageBridge {
916
930
  // 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
917
931
  const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
918
932
  const cmdContent = contentForCmd || content;
919
- const isCmd = msg.source !== 'handoff' && this.cmdHandler.isCommand(cmdContent);
933
+ const isCmd = allowsNormalCommandRouting
934
+ && msg.source !== 'handoff'
935
+ && this.cmdHandler.isCommand(cmdContent);
920
936
  if (isCmd) {
921
937
  logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
922
938
  // 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
@@ -953,11 +969,13 @@ export class MessageBridge {
953
969
  logger.debug(`[MessageBridge] Failed to log inbound command: ${e}`);
954
970
  }
955
971
  }
956
- if (msg.source !== 'handoff' && await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
957
- const taskId = `cmd-${msg.messageId || Date.now()}`;
958
- logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId, correlationId: taskId, sessionId: msg.replyContext?.sessionId, agentAid: msg.selfAID, payload: { kind: 'command.result', text } });
959
- return sendReply(msg.channelId, text, msg.replyContext);
960
- }, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext, msg.messageId, msg.selfAID, authSubject))
972
+ if (allowsNormalCommandRouting
973
+ && msg.source !== 'handoff'
974
+ && await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
975
+ const taskId = `cmd-${msg.messageId || Date.now()}`;
976
+ logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId, correlationId: taskId, sessionId: msg.replyContext?.sessionId, agentAid: msg.selfAID, payload: { kind: 'command.result', text } });
977
+ return sendReply(msg.channelId, text, msg.replyContext);
978
+ }, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext, msg.messageId, msg.selfAID, authSubject))
961
979
  return;
962
980
  // 3. session 解析(使用 Channel 层填充的 chatType)
963
981
  if (!(await this.canCreateThreadSession(channelName, msg, chatType))) {
@@ -1408,6 +1426,9 @@ export class MessageBridge {
1408
1426
  }
1409
1427
  }
1410
1428
  async sendMenuResponse(adapter, channel, msg, response) {
1429
+ const request = this.menuRequestForLog(msg, response);
1430
+ const flowContext = this.menuFlowContext(channel, msg);
1431
+ const timedResponse = withMenuProcessingTime(response, msg);
1411
1432
  if (!adapter?.send) {
1412
1433
  this.logMenuDiagnostic('transport-unavailable', channel, msg, {
1413
1434
  isMenu: true,
@@ -1417,6 +1438,10 @@ export class MessageBridge {
1417
1438
  id: response.id,
1418
1439
  name: response.name,
1419
1440
  }, 'TEMPORARILY_UNAVAILABLE');
1441
+ logMenuRequestCompleted(request, timedResponse, flowContext, {
1442
+ delivery: 'dropped',
1443
+ reason: 'transport-unavailable',
1444
+ });
1420
1445
  return;
1421
1446
  }
1422
1447
  const agentName = this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>';
@@ -1429,6 +1454,10 @@ export class MessageBridge {
1429
1454
  };
1430
1455
  if (msg.channelType === 'aun' && !replyContext.delivery) {
1431
1456
  logger.warn(`[MessageBridge] dropping AUN menu response without trusted delivery route: channel=${channel} channelId=${msg.channelId}`);
1457
+ logMenuRequestCompleted(request, timedResponse, flowContext, {
1458
+ delivery: 'dropped',
1459
+ reason: 'missing-delivery-route',
1460
+ });
1432
1461
  return;
1433
1462
  }
1434
1463
  const envelope = buildEnvelope({
@@ -1438,7 +1467,18 @@ export class MessageBridge {
1438
1467
  agentName,
1439
1468
  replyContext,
1440
1469
  });
1441
- await adapter.send(envelope, { kind: 'custom', channelType: channel, payload: response });
1470
+ try {
1471
+ await adapter.send(envelope, { kind: 'custom', channelType: channel, payload: timedResponse });
1472
+ logMenuRequestCompleted(request, timedResponse, flowContext, { delivery: 'sent' });
1473
+ }
1474
+ catch (error) {
1475
+ logMenuRequestCompleted(request, timedResponse, flowContext, {
1476
+ delivery: 'failed',
1477
+ reason: 'transport-error',
1478
+ transportError: error,
1479
+ });
1480
+ throw error;
1481
+ }
1442
1482
  }
1443
1483
  menuTokenOwnerAid(channel, msg) {
1444
1484
  return msg.selfAID || this.agentRegistry?.resolveByChannel(channel)?.aid;
@@ -1466,6 +1506,7 @@ export class MessageBridge {
1466
1506
  return normalized.length > 0 && normalized.length <= 256 ? normalized : undefined;
1467
1507
  }
1468
1508
  logMenuInbound(channel, msg, parsed) {
1509
+ logMenuRequestReceived(parsed.request, this.menuFlowContext(channel, msg));
1469
1510
  logger.channelIn({
1470
1511
  channel,
1471
1512
  channelId: this.shortHash(msg.channelId),
@@ -1482,6 +1523,21 @@ export class MessageBridge {
1482
1523
  },
1483
1524
  });
1484
1525
  }
1526
+ menuRequestForLog(msg, response) {
1527
+ try {
1528
+ const parsed = JSON.parse(msg.content);
1529
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
1530
+ return parsed;
1531
+ }
1532
+ catch {
1533
+ // Fall through to a minimal correlation record.
1534
+ }
1535
+ return {
1536
+ type: 'menu.unknown',
1537
+ id: response.id,
1538
+ ...(response.name ? { name: response.name } : {}),
1539
+ };
1540
+ }
1485
1541
  logMenuDiagnostic(category, channel, msg, parsed, code) {
1486
1542
  const scope = msg.chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
1487
1543
  const key = `${category}:${channel}:${this.shortHash(scope)}`;
@@ -1611,6 +1667,13 @@ export class MessageBridge {
1611
1667
  * 撤回消息:先查 debounce 窗口,再查 message queue,最后查正在执行的任务。
1612
1668
  * @returns true 如果找到并取消/中断
1613
1669
  */
1670
+ menuFlowContext(channel, msg) {
1671
+ return {
1672
+ source: 'agent-aun',
1673
+ selfAid: msg.selfAID ?? this.agentRegistry?.resolveByChannel(channel)?.aid,
1674
+ messageId: msg.messageId,
1675
+ };
1676
+ }
1614
1677
  cancel(messageId, selfAID) {
1615
1678
  // 阶段 1: debounce 窗口(尚未入队)
1616
1679
  for (const d of this.debouncers.values()) {
@@ -0,0 +1,53 @@
1
+ /** Per-session gate used only at the next tool boundary. */
2
+ export class PauseController {
3
+ state = 'running';
4
+ waiters = new Set();
5
+ requestPause() {
6
+ if (this.state !== 'running')
7
+ return false;
8
+ this.state = 'pause_requested';
9
+ return true;
10
+ }
11
+ isPauseRequested() { return this.state === 'pause_requested'; }
12
+ isPaused() { return this.state === 'paused'; }
13
+ waitAtToolBoundary(signal) {
14
+ if (signal?.aborted)
15
+ return Promise.resolve('cancelled');
16
+ if (this.state === 'running')
17
+ return Promise.resolve('none');
18
+ this.state = 'paused';
19
+ return new Promise(resolve => {
20
+ const waiter = { resolve: () => { }, abort: undefined };
21
+ const finish = (result) => {
22
+ this.waiters.delete(waiter);
23
+ if (waiter.abort)
24
+ signal?.removeEventListener('abort', waiter.abort);
25
+ resolve(result);
26
+ };
27
+ waiter.resolve = finish;
28
+ if (signal) {
29
+ waiter.abort = () => finish('cancelled');
30
+ signal.addEventListener('abort', waiter.abort, { once: true });
31
+ }
32
+ this.waiters.add(waiter);
33
+ if (this.state === 'running')
34
+ finish('released');
35
+ else if (signal?.aborted)
36
+ finish('cancelled');
37
+ });
38
+ }
39
+ resume() {
40
+ if (this.state === 'running')
41
+ return false;
42
+ this.state = 'running';
43
+ for (const waiter of [...this.waiters])
44
+ waiter.resolve('released');
45
+ return true;
46
+ }
47
+ cancel() {
48
+ this.state = 'running';
49
+ for (const waiter of [...this.waiters])
50
+ waiter.resolve('cancelled');
51
+ }
52
+ clear() { this.cancel(); }
53
+ }