evolcore 0.0.17 → 0.0.19

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 (66) hide show
  1. package/CHANGELOG.md +51 -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 +43 -24
  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 +82 -14
  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/data-migration.js +10 -4
  40. package/dist/core/event-catalog.js +18 -0
  41. package/dist/core/message/message-bridge.js +66 -8
  42. package/dist/core/message/response-engine.js +147 -10
  43. package/dist/core/permission/ec-command-parser.js +148 -22
  44. package/dist/core/permission/sandbox-runtime.js +79 -13
  45. package/dist/core/permission/tool-policy.js +19 -7
  46. package/dist/index.js +357 -48
  47. package/dist/ipc.js +81 -5
  48. package/dist/paths.js +0 -3
  49. package/dist/utils/atomic-write.js +45 -11
  50. package/dist/utils/logger.js +27 -0
  51. package/dist/utils/windows-autostart.js +740 -83
  52. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  53. package/ecagent/dist/harness/agent-harness.js +6 -4
  54. package/kits/docs/evolcore/config.md +1 -1
  55. package/kits/docs/evolcore/group-rules.md +2 -1
  56. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  57. package/kits/docs/path-registry.md +1 -1
  58. package/kits/eck_manifest.json +25 -16
  59. package/kits/rules/01-overview.md +5 -5
  60. package/kits/rules/02-navigation.md +2 -2
  61. package/kits/rules/03-identity.md +1 -1
  62. package/kits/rules/04-relation.md +4 -4
  63. package/kits/rules/05-venue.md +5 -5
  64. package/kits/templates/bootstrap-welcome.md +3 -1
  65. package/kits/templates/system-fragments/bootstrap.md +17 -9
  66. package/package.json +1 -1
@@ -1,7 +1,9 @@
1
1
  import { createHash } from 'crypto';
2
+ import { performance } from 'node:perf_hooks';
2
3
  import { loadDaemonConfig } from '../../config-store.js';
3
4
  import { readInstalledEvolcoreVersion } from '../../utils/evolcore-version.js';
4
5
  import { compareStableSemver, parseStableSemver } from '../../utils/stable-semver.js';
6
+ import { logger } from '../../utils/logger.js';
5
7
  export const MENU_REQUEST_TYPES = new Set([
6
8
  'menu.token.request',
7
9
  'menu.list',
@@ -40,6 +42,51 @@ export function menuCommandForName(name) {
40
42
  return undefined;
41
43
  return MENU_NAME_COMMANDS[name];
42
44
  }
45
+ /**
46
+ * Preserve a valid adapter receive mark, or capture a local monotonic fallback
47
+ * at the earliest boundary available to the caller.
48
+ */
49
+ export function normalizeMenuResponseTiming(timing, clock = {}) {
50
+ const receivedAtMono = typeof timing.receivedAtMono === 'number' && Number.isFinite(timing.receivedAtMono)
51
+ ? timing.receivedAtMono
52
+ : undefined;
53
+ const receivedAt = typeof timing.receivedAt === 'number' && Number.isFinite(timing.receivedAt)
54
+ ? timing.receivedAt
55
+ : undefined;
56
+ if (receivedAtMono !== undefined) {
57
+ return { ...(receivedAt !== undefined ? { receivedAt } : {}), receivedAtMono };
58
+ }
59
+ if (receivedAt !== undefined)
60
+ return { receivedAt };
61
+ return {
62
+ receivedAt: clock.wallNow ?? Date.now(),
63
+ receivedAtMono: clock.monotonicNow ?? performance.now(),
64
+ };
65
+ }
66
+ /**
67
+ * Finalize a Menu response at an external transport boundary.
68
+ *
69
+ * Keeping this outside menuSuccess/menuFailure is intentional: dedupe caches
70
+ * response drafts, while every inbound request (including a replay) must get
71
+ * its own inbound-to-outbound processing time.
72
+ */
73
+ export function withMenuProcessingTime(response, timing, clock = {}) {
74
+ const receivedAtMono = typeof timing.receivedAtMono === 'number' && Number.isFinite(timing.receivedAtMono)
75
+ ? timing.receivedAtMono
76
+ : undefined;
77
+ const receivedAt = typeof timing.receivedAt === 'number' && Number.isFinite(timing.receivedAt)
78
+ ? timing.receivedAt
79
+ : undefined;
80
+ const elapsed = receivedAtMono !== undefined
81
+ ? (clock.monotonicNow ?? performance.now()) - receivedAtMono
82
+ : receivedAt !== undefined
83
+ ? (clock.wallNow ?? Date.now()) - receivedAt
84
+ : 0;
85
+ return {
86
+ ...response,
87
+ processing_ms: Math.max(0, Math.round(Number.isFinite(elapsed) ? elapsed : 0)),
88
+ };
89
+ }
43
90
  export function evaluateEvolMenuVersionGate(input) {
44
91
  const minimum = loadDaemonConfig().aun?.minEvolVersion;
45
92
  if (!minimum)
@@ -346,3 +393,152 @@ export class MenuDiagnosticLimiter {
346
393
  return { log: false, suppressed: previous.suppressed };
347
394
  }
348
395
  }
396
+ const MAX_LOG_DEPTH = 8;
397
+ const MAX_LOG_KEYS = 100;
398
+ const MAX_LOG_ARRAY_ITEMS = 100;
399
+ const MAX_LOG_STRING_LENGTH = 4096;
400
+ const SENSITIVE_LOG_KEY = /(?:^|[_-])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|menu[_-]?token|token|secret(?:[_-]?key)?|password|passwd|credentials?|authorization|cookie|private[_-]?key|seed)(?:$|[_-])/i;
401
+ const SENSITIVE_LOG_ARG = /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|menu[_-]?token|token|secret|password|passwd|credential|authorization|cookie|private[_-]?key|seed)/i;
402
+ function isSensitiveLogKey(key) {
403
+ const normalized = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2');
404
+ return SENSITIVE_LOG_KEY.test(normalized);
405
+ }
406
+ function sanitizeInlineLogSecrets(value) {
407
+ return value
408
+ .replace(/\b(authorization\s*:\s*Bearer\s+)[^\s"',;]+/gi, '$1[REDACTED]')
409
+ .replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]')
410
+ .replace(/\b(api[_-]?key|access[_-]?token|refresh[_-]?token|menu[_-]?token|token|password|passwd|secret|credential|authorization|cookie)\s*([=:])\s*(?:"[^"]*"|'[^']*'|\S+)/gi, '$1$2[REDACTED]')
411
+ .replace(/(--(?:api-key|access-token|refresh-token|menu-token|token|password|secret|credential))(?:=|\s+)(?:"[^"]*"|'[^']*'|\S+)/gi, '$1 [REDACTED]');
412
+ }
413
+ function sanitizeLogString(value) {
414
+ let sanitized = value;
415
+ const trimmed = value.trim();
416
+ if ((trimmed.startsWith('{') || trimmed.startsWith('[')) && trimmed.length <= 64 * 1024) {
417
+ try {
418
+ const parsed = JSON.parse(trimmed);
419
+ if (parsed && typeof parsed === 'object')
420
+ sanitized = JSON.stringify(sanitizeMenuLogValue(parsed));
421
+ }
422
+ catch {
423
+ // Ordinary strings may start with JSON punctuation; keep their original shape.
424
+ }
425
+ }
426
+ sanitized = sanitizeInlineLogSecrets(sanitized);
427
+ if (sanitized.length <= MAX_LOG_STRING_LENGTH)
428
+ return sanitized;
429
+ return `${sanitized.slice(0, MAX_LOG_STRING_LENGTH)}...[TRUNCATED ${sanitized.length - MAX_LOG_STRING_LENGTH} chars]`;
430
+ }
431
+ function sanitizeLogArgv(value, depth, seen) {
432
+ const output = [];
433
+ let redactNext = false;
434
+ for (const item of value.slice(0, MAX_LOG_ARRAY_ITEMS)) {
435
+ if (typeof item !== 'string') {
436
+ output.push(sanitizeMenuLogValue(item, '', depth + 1, seen));
437
+ redactNext = false;
438
+ continue;
439
+ }
440
+ if (redactNext) {
441
+ output.push('[REDACTED]');
442
+ redactNext = false;
443
+ continue;
444
+ }
445
+ const sanitized = sanitizeLogString(item);
446
+ output.push(sanitized);
447
+ const token = item.replace(/^--?/, '').replace(/=.*$/, '');
448
+ redactNext = SENSITIVE_LOG_ARG.test(token) && !item.includes('=');
449
+ }
450
+ if (value.length > MAX_LOG_ARRAY_ITEMS)
451
+ output.push(`[TRUNCATED ${value.length - MAX_LOG_ARRAY_ITEMS} items]`);
452
+ return output;
453
+ }
454
+ export function sanitizeMenuLogValue(value, key = '', depth = 0, seen = new WeakSet()) {
455
+ if (isSensitiveLogKey(key))
456
+ return '[REDACTED]';
457
+ if (value === null || value === undefined || typeof value === 'number' || typeof value === 'boolean')
458
+ return value;
459
+ if (typeof value === 'bigint')
460
+ return value.toString();
461
+ if (typeof value === 'string')
462
+ return sanitizeLogString(value);
463
+ if (typeof value !== 'object')
464
+ return String(value);
465
+ if (depth >= MAX_LOG_DEPTH)
466
+ return '[MAX_DEPTH]';
467
+ if (seen.has(value))
468
+ return '[CIRCULAR]';
469
+ seen.add(value);
470
+ if (Array.isArray(value)) {
471
+ const items = value.slice(0, MAX_LOG_ARRAY_ITEMS).map(item => sanitizeMenuLogValue(item, '', depth + 1, seen));
472
+ if (value.length > MAX_LOG_ARRAY_ITEMS)
473
+ items.push(`[TRUNCATED ${value.length - MAX_LOG_ARRAY_ITEMS} items]`);
474
+ return items;
475
+ }
476
+ const output = {};
477
+ const entries = Object.entries(value);
478
+ for (const [entryKey, entryValue] of entries.slice(0, MAX_LOG_KEYS)) {
479
+ output[entryKey] = entryKey === 'argv' && Array.isArray(entryValue)
480
+ ? sanitizeLogArgv(entryValue, depth + 1, seen)
481
+ : sanitizeMenuLogValue(entryValue, entryKey, depth + 1, seen);
482
+ }
483
+ if (entries.length > MAX_LOG_KEYS)
484
+ output.$truncated_keys = entries.length - MAX_LOG_KEYS;
485
+ return output;
486
+ }
487
+ function menuRequestId(request) {
488
+ return typeof request.id === 'string' && request.id.trim() ? request.id : undefined;
489
+ }
490
+ function menuFlowBaseRecord(request, context) {
491
+ return {
492
+ source: context.source,
493
+ requestId: menuRequestId(request),
494
+ messageId: context.messageId,
495
+ selfAid: context.selfAid,
496
+ };
497
+ }
498
+ function menuRequestBusinessFields(request) {
499
+ return sanitizeMenuLogValue({
500
+ type: request.type,
501
+ name: request.name,
502
+ agent: request.agent,
503
+ action: request.action,
504
+ cmd: request.cmd,
505
+ args: request.args,
506
+ value: request.value,
507
+ });
508
+ }
509
+ function writeMenuFlowRecord(record) {
510
+ try {
511
+ logger.menu(record);
512
+ }
513
+ catch {
514
+ // Observability must never affect Menu execution.
515
+ }
516
+ }
517
+ export function logMenuRequestReceived(request, context) {
518
+ writeMenuFlowRecord({
519
+ ...menuFlowBaseRecord(request, context),
520
+ event: 'menu.request.received',
521
+ request: menuRequestBusinessFields(request),
522
+ });
523
+ }
524
+ export function logMenuRequestCompleted(request, response, context, options) {
525
+ const error = response && 'error' in response ? response.error : undefined;
526
+ const transportError = options.transportError instanceof Error
527
+ ? { message: sanitizeLogString(options.transportError.message) }
528
+ : options.transportError === undefined
529
+ ? undefined
530
+ : { message: sanitizeLogString(String(options.transportError)) };
531
+ const processingMs = response && typeof response.processing_ms === 'number'
532
+ ? response.processing_ms
533
+ : undefined;
534
+ writeMenuFlowRecord({
535
+ ...menuFlowBaseRecord(request, context),
536
+ event: options.delivery === 'failed' ? 'menu.request.failed' : 'menu.request.completed',
537
+ delivery: options.delivery,
538
+ processingMs,
539
+ reason: options.reason,
540
+ result: response && 'data' in response ? sanitizeMenuLogValue(response.data) : undefined,
541
+ warning: response && 'warning' in response ? sanitizeMenuLogValue(response.warning) : undefined,
542
+ error: error ? sanitizeMenuLogValue(error) : transportError,
543
+ });
544
+ }
@@ -1,6 +1,7 @@
1
1
  // 支持的命令列表
2
- const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/compact', '/repair', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
2
+ const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/pause', '/compact', '/repair', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
3
3
  const deprecatedCommands = ['/clear'];
4
+ const exactBoundaryCommands = new Set(['/pause', '/resume', '/stop']);
4
5
  // 命令别名映射
5
6
  const aliases = {
6
7
  '/s': '/session',
@@ -9,7 +10,7 @@ const aliases = {
9
10
  '/base': '/baseagent',
10
11
  };
11
12
  // 命令快速路径前缀(所有命令都不进入消息队列)
12
- const quickCommandPrefixes = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/del', '/perm', '/file', '/check', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/base ', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
13
+ const quickCommandPrefixes = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/reload', '/model', '/effort', '/baseagent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/pause', '/clear', '/compact', '/del', '/perm', '/file', '/check', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode', '/mentionmode', '/ask', '/resume', '/base ', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
13
14
  /**
14
15
  * 计算两个字符串的 Levenshtein 距离(编辑距离)
15
16
  */
@@ -39,7 +40,9 @@ function levenshteinDistance(str1, str2) {
39
40
  return matrix[len1][len2];
40
41
  }
41
42
  export function isQuickCommand(content) {
42
- return content === '/s' || quickCommandPrefixes.some(cmd => content.startsWith(cmd));
43
+ return content === '/s' || quickCommandPrefixes.some(cmd => exactBoundaryCommands.has(cmd)
44
+ ? content === cmd || content.startsWith(cmd + ' ')
45
+ : content.startsWith(cmd));
43
46
  }
44
47
  export function normalizeSlashContent(content) {
45
48
  for (const [alias, full] of Object.entries(aliases)) {
@@ -50,7 +53,9 @@ export function normalizeSlashContent(content) {
50
53
  return content;
51
54
  }
52
55
  export function isRecognizedSlashCommand(content) {
53
- return commands.some(cmd => content.startsWith(cmd)) ||
56
+ return commands.some(cmd => exactBoundaryCommands.has(cmd)
57
+ ? content === cmd || content.startsWith(cmd + ' ')
58
+ : content.startsWith(cmd)) ||
54
59
  deprecatedCommands.some(cmd => content === cmd || content.startsWith(cmd + ' '));
55
60
  }
56
61
  export function guardThreadCommand(content, threadId) {
@@ -70,7 +75,7 @@ export function guardRoleCommand(content, activeChatType, isAdmin) {
70
75
  // visitor/member 在群聊和私聊中均可访问的只读命令:纯查询形态(带参写操作由各 handler 内部守卫拦截)
71
76
  const userGroupCommands = [
72
77
  '/status', '/help', '/evolhelp', '/check', '/chatmode', '/mentionmode',
73
- '/model', '/effort', '/baseagent', '/perm', '/activity', '/stop',
78
+ '/model', '/effort', '/baseagent', '/perm', '/activity', '/stop', '/pause',
74
79
  '/resume', '/trigger', '/file',
75
80
  ];
76
81
  const userCommands = activeChatType === 'group' && !isAdmin
@@ -107,6 +112,8 @@ export async function guardIdleCommand(opts) {
107
112
  // 话题中:检查话题 session 是否在处理(不创建)
108
113
  const threadSession = await opts.sessionManager.getThreadSession(opts.channel, opts.channelId, opts.threadId);
109
114
  if (threadSession) {
115
+ if (opts.isSessionPaused?.(threadSession.id))
116
+ return undefined;
110
117
  let hasActiveStream = false;
111
118
  try {
112
119
  hasActiveStream = opts.getAgentForSession(threadSession).hasActiveStream(threadSession.id);
@@ -123,6 +130,8 @@ export async function guardIdleCommand(opts) {
123
130
  }
124
131
  }
125
132
  else if (opts.activeSession) {
133
+ if (opts.isSessionPaused?.(opts.activeSession.id))
134
+ return undefined;
126
135
  const isBusy = (opts.activeAgent?.hasActiveStream(opts.activeSession.id) ?? false) ||
127
136
  opts.messageQueue?.isProcessing(opts.activeSession.id) ||
128
137
  (opts.messageQueue?.getQueueLength(opts.activeSession.id) ?? 0) > 0;
@@ -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);