evolcore 0.0.10 → 0.0.12

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 (131) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +3 -3
  3. package/bin/codex-managed-hook.mjs +80 -0
  4. package/dist/agents/baseagent.js +4 -0
  5. package/dist/agents/claude-runner.js +158 -44
  6. package/dist/agents/codex-app-server-client.js +153 -23
  7. package/dist/agents/codex-runner.js +331 -43
  8. package/dist/agents/ecagent-runner.js +53 -6
  9. package/dist/agents/gemini-runner.js +1 -0
  10. package/dist/agents/request-identity.js +55 -0
  11. package/dist/aun/aid/client.js +11 -21
  12. package/dist/aun/aid/managed-operation.js +107 -0
  13. package/dist/aun/msg/group.js +81 -12
  14. package/dist/aun/msg/managed-operation.js +804 -0
  15. package/dist/aun/msg/mention-schema.js +20 -0
  16. package/dist/aun/msg/p2p.js +7 -0
  17. package/dist/aun/outbox.js +28 -31
  18. package/dist/channels/aun.js +418 -235
  19. package/dist/channels/daemon.js +13 -0
  20. package/dist/cli/agent-command.js +72 -17
  21. package/dist/cli/agent.js +249 -22
  22. package/dist/cli/aun-commands.js +270 -69
  23. package/dist/cli/bench.js +12 -3
  24. package/dist/cli/daemon-commands.js +52 -33
  25. package/dist/cli/fs-command.js +60 -0
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +106 -63
  28. package/dist/cli/init-cancel.js +208 -0
  29. package/dist/cli/init-channel.js +343 -195
  30. package/dist/cli/init.js +21 -9
  31. package/dist/cli/managed-command-guard.js +41 -0
  32. package/dist/cli/model.js +11 -4
  33. package/dist/cli/queue-command.js +11 -1
  34. package/dist/cli/response.js +71 -0
  35. package/dist/cli/restart-monitor.js +4 -18
  36. package/dist/cli/trigger-command.js +4 -0
  37. package/dist/config/builtin-roles.js +5 -0
  38. package/dist/config/contact-book-store.js +11 -3
  39. package/dist/config/contact-request-service.js +161 -15
  40. package/dist/config/gateway-config.js +26 -10
  41. package/dist/config/mention-mode.js +8 -24
  42. package/dist/core/agent-reload-coordinator.js +53 -0
  43. package/dist/core/auth/agent-delegation.js +0 -1
  44. package/dist/core/auth/operation-authorizer.js +62 -154
  45. package/dist/core/auth/operation-catalog.js +264 -22
  46. package/dist/core/bootstrap-messages.js +68 -0
  47. package/dist/core/bootstrap-service.js +144 -10
  48. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  49. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  50. package/dist/core/capability/skill-discovery.js +55 -0
  51. package/dist/core/channel-loader.js +24 -7
  52. package/dist/core/command/agent-control.js +14 -11
  53. package/dist/core/command/cli-intent-parser.js +9 -0
  54. package/dist/core/command/command-handler.js +467 -6
  55. package/dist/core/command/connect-menu.js +15 -0
  56. package/dist/core/command/group-menu.js +4 -4
  57. package/dist/core/command/menu-catalog.js +366 -0
  58. package/dist/core/command/menu-handler.js +213 -110
  59. package/dist/core/command/menu-protocol.js +71 -0
  60. package/dist/core/command/slash-gate.js +5 -2
  61. package/dist/core/command/slash-handler.js +45 -25
  62. package/dist/core/daemon-file-cache.js +2 -1
  63. package/dist/core/data-migration.js +9 -2
  64. package/dist/core/evolagent-registry.js +125 -35
  65. package/dist/core/evolagent.js +12 -5
  66. package/dist/core/handoff/dispatcher.js +5 -2
  67. package/dist/core/handoff/runtime.js +145 -21
  68. package/dist/core/handoff/store.js +24 -0
  69. package/dist/core/handoff/types.js +1 -0
  70. package/dist/core/inference/text-inference.js +38 -4
  71. package/dist/core/message/logical-queue-bridge.js +1 -0
  72. package/dist/core/message/mention-schema.js +126 -0
  73. package/dist/core/message/message-bridge.js +237 -79
  74. package/dist/core/message/message-log.js +23 -0
  75. package/dist/core/message/message-queue.js +20 -4
  76. package/dist/core/message/response-engine.js +54 -36
  77. package/dist/core/model/model-catalog.js +143 -24
  78. package/dist/core/model/model-diagnostics.js +28 -10
  79. package/dist/core/permission/ec-command-parser.js +156 -10
  80. package/dist/core/permission/index.js +1 -0
  81. package/dist/core/permission/readonly-shell-query.js +532 -0
  82. package/dist/core/permission/sandbox-runtime.js +22 -1
  83. package/dist/core/permission/shell-environment.js +46 -0
  84. package/dist/core/permission/tool-policy.js +292 -111
  85. package/dist/core/protected-paths.js +17 -9
  86. package/dist/core/runner-reload-transaction.js +57 -0
  87. package/dist/core/session/session-manager.js +21 -2
  88. package/dist/eck/manifest-engine.js +39 -13
  89. package/dist/index.js +644 -180
  90. package/dist/ipc.js +246 -19
  91. package/dist/response-system/coordinator.js +6 -4
  92. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  93. package/dist/response-system/modes/single-session/index.js +14 -1
  94. package/dist/response-system/selector.js +6 -5
  95. package/dist/trigger/anomaly-store.js +4 -0
  96. package/dist/trigger/script-executor.js +1 -0
  97. package/dist/utils/aid-bind.js +3 -8
  98. package/dist/utils/codex-app-server-registry.js +90 -0
  99. package/dist/utils/codex-cli.js +5 -1
  100. package/dist/utils/cross-platform.js +15 -0
  101. package/dist/utils/log-writer.js +6 -10
  102. package/dist/utils/logger.js +5 -5
  103. package/dist/utils/npm-ops.js +5 -12
  104. package/dist/utils/tool-summary.js +11 -0
  105. package/kits/docs/channels/aun.md +1 -1
  106. package/kits/docs/context-assembly.md +2 -2
  107. package/kits/docs/evolcore/agent.md +11 -6
  108. package/kits/docs/evolcore/aid.md +14 -0
  109. package/kits/docs/evolcore/msg.md +13 -0
  110. package/kits/docs/prompt-loading-architecture.md +1 -2
  111. package/kits/docs/venues/group.md +1 -1
  112. package/kits/eck_manifest.json +0 -12
  113. package/kits/rules/01-overview.md +9 -0
  114. package/kits/rules/04-relation.md +1 -1
  115. package/kits/rules/05-venue.md +2 -2
  116. package/kits/schemas/agent-config.schema.3.json +1 -1
  117. package/kits/schemas/agent-config.schema.4.json +1 -1
  118. package/kits/schemas/relation-config.schema.2.json +1 -1
  119. package/kits/schemas/role-config.schema.1.json +1 -1
  120. package/kits/schemas/single-session.schema.1.json +3 -3
  121. package/kits/schemas/single-session.schema.2.json +3 -3
  122. package/kits/templates/roles/admin.json +27 -0
  123. package/kits/templates/roles/member.json +44 -0
  124. package/kits/templates/roles/visitor.json +40 -2
  125. package/kits/templates/system-fragments/bootstrap.md +12 -6
  126. package/kits/templates/system-fragments/channel.md +6 -0
  127. package/kits/templates/system-fragments/session.md +2 -0
  128. package/package.json +3 -2
  129. package/skills/eclink/SKILL.md +15 -3
  130. package/skills/eclink/agents/openai.yaml +3 -3
  131. package/dist/core/command/evol-menu-version-gate.js +0 -39
@@ -1,4 +1,7 @@
1
1
  import { createHash } from 'crypto';
2
+ import { loadDaemonConfig } from '../../config-store.js';
3
+ import { readInstalledEvolcoreVersion } from '../../utils/evolcore-version.js';
4
+ import { compareStableSemver, parseStableSemver } from '../../utils/stable-semver.js';
2
5
  export const MENU_REQUEST_TYPES = new Set([
3
6
  'menu.token.request',
4
7
  'menu.list',
@@ -7,6 +10,72 @@ export const MENU_REQUEST_TYPES = new Set([
7
10
  'menu.update',
8
11
  'menu.action',
9
12
  ]);
13
+ export const MENU_NAME_COMMANDS = Object.freeze({
14
+ pwd: '/pwd',
15
+ session: '/session',
16
+ topic: '/topic',
17
+ baseagent: '/baseagent',
18
+ model: '/model',
19
+ effort: '/effort',
20
+ chatmode: '/chatmode',
21
+ mentionmode: '/mentionmode',
22
+ group: '/group',
23
+ permission: '/perm',
24
+ activity: '/activity',
25
+ dispatch: '/dispatch',
26
+ observable: '/observable',
27
+ system: '/system',
28
+ cli: '/cli',
29
+ agent: '/agent',
30
+ trigger: '/trigger',
31
+ file: '/file',
32
+ gateway: '/gateway',
33
+ config: '/config',
34
+ capability: '/capability',
35
+ role: '/role',
36
+ connect: '/connect',
37
+ });
38
+ export function menuCommandForName(name) {
39
+ if (typeof name !== 'string')
40
+ return undefined;
41
+ return MENU_NAME_COMMANDS[name];
42
+ }
43
+ export function evaluateEvolMenuVersionGate(input) {
44
+ const minimum = loadDaemonConfig().aun?.minEvolVersion;
45
+ if (!minimum)
46
+ return null;
47
+ const minimumVersion = parseStableSemver(minimum);
48
+ if (!minimumVersion) {
49
+ throw new Error('daemon.json.aun.minEvolVersion must use stable X.Y.Z format');
50
+ }
51
+ const received = input.protectedHeaders?.evol_version;
52
+ const receivedVersion = input.encrypted ? parseStableSemver(received) : null;
53
+ if (!receivedVersion || compareStableSemver(receivedVersion, minimumVersion) < 0) {
54
+ return {
55
+ code: 'UPGRADE_REQUIRED',
56
+ message: `Evol ${minimum} or later is required`,
57
+ data: {
58
+ minimum,
59
+ received: receivedVersion ? received : null,
60
+ },
61
+ };
62
+ }
63
+ return null;
64
+ }
65
+ export function isEvolMenuVersionGateEnabled() {
66
+ return loadDaemonConfig().aun?.minEvolVersion !== undefined;
67
+ }
68
+ /** Token may be issued while disabled, but only this flag makes it an access requirement. */
69
+ export function isAunMenuTokenRequired() {
70
+ return loadDaemonConfig().aun?.menuTokenRequired === true;
71
+ }
72
+ export function evolMenuResponseTransportMetadata() {
73
+ return {
74
+ // Clear inherited request state so AUNChannel applies daemon aun.defaultEncrypt.
75
+ encrypted: undefined,
76
+ protectedHeaders: { ec_version: readInstalledEvolcoreVersion() },
77
+ };
78
+ }
10
79
  const EXPLICIT_SCOPE_UPDATE_NAMES = new Set([
11
80
  'model',
12
81
  'effort',
@@ -155,6 +224,8 @@ const STABLE_CODES = new Set([
155
224
  'METHOD_NOT_FOUND', 'NOT_FOUND', 'CONFLICT', 'EXPIRED', 'EXECUTION_TIMEOUT',
156
225
  'TEMPORARILY_UNAVAILABLE', 'INTERNAL_ERROR', 'MISSING_SCOPE', 'UPGRADE_REQUIRED',
157
226
  'MENU_TOKEN_REQUIRED', 'MENU_TOKEN_REJECTED', 'MENU_TOKEN_ENCRYPTION_REQUIRED',
227
+ 'INVALID_CONTEXT', 'SCHEMA_VERSION_UNSUPPORTED', 'DEPENDENCY_UNAVAILABLE',
228
+ 'RATE_LIMITED', 'CATALOG_STALE',
158
229
  ]);
159
230
  export function normalizeMenuError(error) {
160
231
  const source = error && typeof error === 'object' ? error : {};
@@ -114,7 +114,9 @@ export async function guardIdleCommand(opts) {
114
114
  catch {
115
115
  // Runner mismatch should not block recovery commands such as /baseagent.
116
116
  }
117
- const isBusy = hasActiveStream || opts.messageQueue?.isProcessing(threadSession.id);
117
+ const isBusy = hasActiveStream
118
+ || opts.messageQueue?.isProcessing(threadSession.id)
119
+ || (opts.messageQueue?.getQueueLength(threadSession.id) ?? 0) > 0;
118
120
  if (isBusy) {
119
121
  return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
120
122
  }
@@ -122,7 +124,8 @@ export async function guardIdleCommand(opts) {
122
124
  }
123
125
  else if (opts.activeSession) {
124
126
  const isBusy = (opts.activeAgent?.hasActiveStream(opts.activeSession.id) ?? false) ||
125
- opts.messageQueue?.isProcessing(opts.activeSession.id);
127
+ opts.messageQueue?.isProcessing(opts.activeSession.id) ||
128
+ (opts.messageQueue?.getQueueLength(opts.activeSession.id) ?? 0) > 0;
126
129
  if (isBusy) {
127
130
  return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
128
131
  }
@@ -21,9 +21,9 @@ import { filterModelsForRole, validateModelSelectionForRole } from '../model/mod
21
21
  import { displaySessionTitle, isSyntheticCliPrompt } 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
- import { dispatchToMentionMode } from '../../config/mention-mode.js';
25
24
  import { isManagementRole } from '../../config/builtin-roles.js';
26
25
  import { isSystemControlChannel } from '../system-channels.js';
26
+ import { spawnDetachedNode } from '../../utils/cross-platform.js';
27
27
  import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, } from './slash-gate.js';
28
28
  const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
29
29
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
@@ -259,7 +259,8 @@ async function getGitWorkingDirInfo(projectPath) {
259
259
  cwd: projectPath,
260
260
  encoding: 'utf8',
261
261
  timeout: 1000,
262
- stdio: ['ignore', 'pipe', 'ignore']
262
+ stdio: ['ignore', 'pipe', 'ignore'],
263
+ windowsHide: process.platform === 'win32',
263
264
  }).trim();
264
265
  if (isInsideWorkTree !== 'true')
265
266
  return null;
@@ -268,14 +269,16 @@ async function getGitWorkingDirInfo(projectPath) {
268
269
  cwd: projectPath,
269
270
  encoding: 'utf8',
270
271
  timeout: 1000,
271
- stdio: ['ignore', 'pipe', 'ignore']
272
+ stdio: ['ignore', 'pipe', 'ignore'],
273
+ windowsHide: process.platform === 'win32',
272
274
  }).trim();
273
275
  if (!branch) {
274
276
  branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
275
277
  cwd: projectPath,
276
278
  encoding: 'utf8',
277
279
  timeout: 1000,
278
- stdio: ['ignore', 'pipe', 'ignore']
280
+ stdio: ['ignore', 'pipe', 'ignore'],
281
+ windowsHide: process.platform === 'win32',
279
282
  }).trim();
280
283
  }
281
284
  if (!branch)
@@ -285,7 +288,8 @@ async function getGitWorkingDirInfo(projectPath) {
285
288
  cwd: projectPath,
286
289
  encoding: 'utf8',
287
290
  timeout: 1000,
288
- stdio: ['ignore', 'pipe', 'ignore']
291
+ stdio: ['ignore', 'pipe', 'ignore'],
292
+ windowsHide: process.platform === 'win32',
289
293
  });
290
294
  // 解析文件状态统计
291
295
  const stats = { modified: 0, added: 0, deleted: 0, untracked: 0 };
@@ -328,7 +332,13 @@ async function getGitWorkingDirInfo(projectPath) {
328
332
  }
329
333
  // 获取 ahead/behind 信息
330
334
  try {
331
- const revOutput = execFileSync('git', ['rev-list', '--left-right', '--count', '@{upstream}...HEAD'], { cwd: projectPath, timeout: 1000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
335
+ const revOutput = execFileSync('git', ['rev-list', '--left-right', '--count', '@{upstream}...HEAD'], {
336
+ cwd: projectPath,
337
+ timeout: 1000,
338
+ encoding: 'utf8',
339
+ stdio: ['ignore', 'pipe', 'ignore'],
340
+ windowsHide: process.platform === 'win32',
341
+ });
332
342
  const revParts = revOutput.trim().split(/\s+/);
333
343
  if (revParts.length === 2) {
334
344
  const behind = parseInt(revParts[0], 10) || 0;
@@ -1641,6 +1651,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1641
1651
  if (!isDaemonOwner && aidArg && aidArg !== selfAid) {
1642
1652
  return { kind: 'command.error', text: '❌ 无权限:跨 agent reload 仅限 daemon owner 使用' };
1643
1653
  }
1654
+ const targetAid = aidArg ?? selfAid;
1655
+ if (!targetAid) {
1656
+ return { kind: 'command.error', text: '❌ 无法确定目标 agent,请指定 aid:/reload <aid>' };
1657
+ }
1644
1658
  const reloadScope = 'agent';
1645
1659
  const authDenied = await authorizeIntent({
1646
1660
  intent: {
@@ -1664,10 +1678,6 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1664
1678
  if (!isDaemonOwner && !isAdmin) {
1665
1679
  return { kind: 'command.error', text: '❌ 无权限:/reload 仅限 daemon owner 或 agent owner/admin 使用' };
1666
1680
  }
1667
- const targetAid = aidArg ?? selfAid;
1668
- if (!targetAid) {
1669
- return { kind: 'command.error', text: '❌ 无法确定目标 agent,请指定 aid:/reload <aid>' };
1670
- }
1671
1681
  // 繁忙检查(同 menu /agent reload)
1672
1682
  const busyInfo = getAgentBusyInfo(this, targetAid);
1673
1683
  if (busyInfo && busyInfo.count > 0) {
@@ -1924,8 +1934,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
1924
1934
  if ('error' in mentionTarget) {
1925
1935
  return { kind: 'command.error', text: `❌ ${mentionTarget.error}` };
1926
1936
  }
1927
- // session.metadata.dispatchMode AUN 协议词汇(mention/broadcast),翻译成 mentionMode 词汇兜底
1928
- const mentionFallback = dispatchToMentionMode(mentionSession.metadata?.dispatchMode) ?? null;
1937
+ const mentionFallback = mentionSession.metadata?.mentionMode ?? null;
1929
1938
  const currentMode = readSlashMentionMode(mentionTarget, mentionFallback);
1930
1939
  if (!arg) {
1931
1940
  const displayMode = currentMode ?? '未设置(跟随群设置)';
@@ -2121,20 +2130,19 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2121
2130
  selfAID,
2122
2131
  role: sessionRole,
2123
2132
  });
2124
- // session.metadata.dispatchMode AUN 协议词汇,翻译成 mentionMode 词汇兜底
2125
- const mentionModeFallback = dispatchToMentionMode(session.metadata?.dispatchMode) ?? null;
2133
+ const mentionModeFallback = session.metadata?.mentionMode ?? null;
2126
2134
  const mentionMode = 'error' in mentionModeTarget
2127
2135
  ? (mentionModeFallback ?? '未设置(跟随群设置)')
2128
2136
  : (readSlashMentionMode(mentionModeTarget, mentionModeFallback) ?? '未设置(跟随群设置)');
2129
2137
  const chatModeLine = `会话模式: ${chatMode}`;
2130
- const dispatchModeLine = session.chatType === 'group' ? `@ 处理模式: ${mentionMode}` : null;
2138
+ const mentionModeLine = session.chatType === 'group' ? `@ 处理模式: ${mentionMode}` : null;
2131
2139
  if (isAdmin) {
2132
2140
  const gitInfo = await getGitWorkingDirInfo(session.projectPath);
2133
2141
  lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${displaySessionTitle(session.name, '(未命名)')}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`);
2134
2142
  if (gitInfo) {
2135
2143
  lines.push(`Git: ${gitInfo}`);
2136
2144
  }
2137
- lines.push(`会话状态: ${sessionStatus} / 轮数: ${sessionTurns}`, sessionRoleLine, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []));
2145
+ lines.push(`会话状态: ${sessionStatus} / 轮数: ${sessionTurns}`, sessionRoleLine, chatModeLine, ...(mentionModeLine ? [mentionModeLine] : []));
2138
2146
  if (health.consecutiveErrors > 0) {
2139
2147
  lines.push(`异常计数: ${health.consecutiveErrors}`);
2140
2148
  }
@@ -2142,7 +2150,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2142
2150
  }
2143
2151
  else {
2144
2152
  lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${channel} / 项目: ${projectName} / ${session.baseagent}会话`);
2145
- lines.push(`状态: ${sessionStatus} / 轮数: ${sessionTurns}`, sessionRoleLine, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []), `最后活跃: ${timeStr}`);
2153
+ lines.push(`状态: ${sessionStatus} / 轮数: ${sessionTurns}`, sessionRoleLine, chatModeLine, ...(mentionModeLine ? [mentionModeLine] : []), `最后活跃: ${timeStr}`);
2146
2154
  }
2147
2155
  if (health.lastError) {
2148
2156
  lines.push('');
@@ -2171,7 +2179,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2171
2179
  ...(previousMetadata.channelKey ? { channelKey: previousMetadata.channelKey } : {}),
2172
2180
  groupId: previousMetadata.groupId || channelId,
2173
2181
  ...(previousMetadata.groupName ? { groupName: previousMetadata.groupName } : {}),
2174
- ...(previousMetadata.dispatchMode ? { dispatchMode: previousMetadata.dispatchMode } : {}),
2182
+ ...(previousMetadata.mentionMode ? { mentionMode: previousMetadata.mentionMode } : {}),
2175
2183
  }
2176
2184
  : {
2177
2185
  ...(previousMetadata.channelKey ? { channelKey: previousMetadata.channelKey } : {}),
@@ -2180,6 +2188,21 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2180
2188
  ...(previousMetadata.peerType ? { peerType: previousMetadata.peerType } : {}),
2181
2189
  };
2182
2190
  const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, newSessionBaseagent, newSessionIdentityMetadata);
2191
+ const previousSession = session || activeSession;
2192
+ const handoffSelfAid = previousSession?.selfAID
2193
+ || selfAID
2194
+ || this.getOwningAgent(channel)?.aid
2195
+ || this.resolveSelfAID(channel);
2196
+ let handoffDiscardWarning;
2197
+ if (previousSession?.id && handoffSelfAid && this.handoffRuntime) {
2198
+ try {
2199
+ await this.handoffRuntime.discardSession(handoffSelfAid, previousSession.id);
2200
+ }
2201
+ catch (error) {
2202
+ handoffDiscardWarning = '⚠️ 原会话 Handoff 清理失败,请检查 daemon 日志';
2203
+ logger.error(`[Handoff] failed to discard handoffs after /new: session=${previousSession.id}`, error);
2204
+ }
2205
+ }
2183
2206
  const previousAgent = getActiveAgentIfAvailable();
2184
2207
  if (session && previousAgent) {
2185
2208
  // Reset agent backend state so the new
@@ -2214,7 +2237,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2214
2237
  ?? newRunner.getEffort?.()
2215
2238
  ?? newAgent?.effort;
2216
2239
  const backendBits = [newBaseagent, backendModel, backendEffort].filter(Boolean).join(' · ');
2217
- return { kind: 'command.result', text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 后端: ${backendBits}\n 之前的对话历史已保留,可通过 /s 查看` };
2240
+ return { kind: 'command.result', text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 后端: ${backendBits}\n 之前的对话历史已保留,可通过 /s 查看${handoffDiscardWarning ? `\n${handoffDiscardWarning}` : ''}` };
2218
2241
  }
2219
2242
  // /check 命令:检查 EvolAgent 实例健康(visitor/member 可用,详情仅 admin)
2220
2243
  if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
@@ -2485,12 +2508,9 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2485
2508
  const controlDir = daemonControlDir();
2486
2509
  fs.mkdirSync(controlDir, { recursive: true });
2487
2510
  fs.writeFileSync(path.join(controlDir, 'restart-pending.json'), JSON.stringify(restartInfo));
2488
- const { spawn } = await import('child_process');
2489
- spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
2490
- detached: true,
2491
- stdio: 'ignore',
2492
- env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
2493
- }).unref();
2511
+ spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
2512
+ EVOLCORE_HOME: resolvePaths().root,
2513
+ });
2494
2514
  }
2495
2515
  else {
2496
2516
  logger.info('[System] Suppressed real restart in test runtime');
@@ -10,7 +10,7 @@
10
10
  * - 只缓存"文件 → 解析后内容",不缓存按 vars 渲染后的结果。
11
11
  *
12
12
  * 策略(reload/重启永远全量失效,无视策略;策略只决定"平时每次读怎么检查"):
13
- * - on-reload:平时不检查,直接用缓存(kits 文件、persona)。靠 reload/重启刷新。
13
+ * - on-reload:平时不检查,直接用缓存(kits 文件)。靠 reload/重启刷新。
14
14
  * - manual:同 on-reload,额外支持显式 invalidate(file) 单刷。
15
15
  * - mtime:每次读 statSync 门控 size + mtime,变了自动重读(working.md、config.json)。
16
16
  *
@@ -35,6 +35,7 @@ function newCounters() {
35
35
  */
36
36
  const GROUP_CAPS = {
37
37
  'relation-prefs': 512,
38
+ 'eck-runtime': 512,
38
39
  };
39
40
  export class FileCache {
40
41
  cache = new Map();
@@ -1303,7 +1303,10 @@ function verifyArchiveContains(archivePath, sourcePaths) {
1303
1303
  .replace(/\/+$/, '');
1304
1304
  let entries;
1305
1305
  try {
1306
- entries = new Set(execFileSync(tar, ['-tzf', archivePath], { encoding: 'utf8' })
1306
+ entries = new Set(execFileSync(tar, ['-tzf', archivePath], {
1307
+ encoding: 'utf8',
1308
+ windowsHide: process.platform === 'win32',
1309
+ })
1307
1310
  .split('\n')
1308
1311
  .map(normalizeArchivePath)
1309
1312
  .filter(Boolean));
@@ -1401,7 +1404,11 @@ export function finalizeDataMigration(root, manifest) {
1401
1404
  const temporary = `${archivePath}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
1402
1405
  const tar = process.platform === 'win32' ? 'tar.exe' : 'tar';
1403
1406
  try {
1404
- execFileSync(tar, ['-czf', temporary, ...sourcePaths], { cwd: root, stdio: 'pipe' });
1407
+ execFileSync(tar, ['-czf', temporary, ...sourcePaths], {
1408
+ cwd: root,
1409
+ stdio: 'pipe',
1410
+ windowsHide: process.platform === 'win32',
1411
+ });
1405
1412
  verifyArchiveContains(temporary, sourcePaths);
1406
1413
  fs.renameSync(temporary, archivePath);
1407
1414
  }
@@ -5,7 +5,7 @@ import { logger } from '../utils/logger.js';
5
5
  import { agentPersonalDir } from '../paths.js';
6
6
  import { invalidateAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
7
7
  import { loadAllAgents, ensureAgentDirSkeleton, loadAgent, validateAgentConfig, } from '../config-store.js';
8
- import { resolveEffective } from '../config/config-manager.js';
8
+ import { ConfigTarget, read as readConfig, resolveEffective } from '../config/config-manager.js';
9
9
  // ── Channel Fingerprint ───────────────────────────────────────────────────
10
10
  // 用于检测多 agent 之间复用同一外部凭证的冲突(appId、aid、token 等)。
11
11
  // 格式:{type}:{primaryKey}
@@ -84,18 +84,20 @@ export class EvolAgentRegistry {
84
84
  this.skipped = [];
85
85
  const { agents: rawAgents, skipped, invalidAgents = [] } = loadAllAgents({ includeInvalid: true });
86
86
  this.skipped = skipped;
87
- for (const raw of rawAgents) {
87
+ for (const expandedRaw of rawAgents) {
88
88
  try {
89
- const merged = resolveEffective({ self: raw.aid });
89
+ const raw = readConfig(ConfigTarget.Agent, { self: expandedRaw.aid }, { cache: true }) ?? expandedRaw;
90
+ const merged = resolveEffective({ self: expandedRaw.aid }, { expand: true });
90
91
  const agent = new EvolAgent(raw, merged);
91
- ensureAgentDirSkeleton(raw.aid);
92
+ ensureAgentDirSkeleton(expandedRaw.aid);
92
93
  this.agents.set(agent.aid, agent);
93
94
  }
94
95
  catch (e) {
95
- logger.warn(`[EvolAgentRegistry] failed to construct agent ${raw.aid}: ${e}`);
96
+ logger.warn(`[EvolAgentRegistry] failed to construct agent ${expandedRaw.aid}: ${e}`);
96
97
  }
97
98
  }
98
- for (const { agent: raw, reason } of invalidAgents) {
99
+ for (const { agent: expandedRaw, reason } of invalidAgents) {
100
+ const raw = readConfig(ConfigTarget.Agent, { self: expandedRaw.aid }, { cache: true }) ?? expandedRaw;
99
101
  this.registerErrorAgent(raw, reason);
100
102
  }
101
103
  this.detectAndFlagConflicts();
@@ -119,7 +121,7 @@ export class EvolAgentRegistry {
119
121
  }
120
122
  resolveMergedForErrorAgent(raw, reason) {
121
123
  try {
122
- return resolveEffective({ self: raw.aid });
124
+ return resolveEffective({ self: raw.aid }, { expand: true });
123
125
  }
124
126
  catch (e) {
125
127
  const message = e instanceof Error ? e.message : String(e);
@@ -225,9 +227,9 @@ export class EvolAgentRegistry {
225
227
  logger.info(`[EvolAgentRegistry] agent ${aid} already loaded, skipping`);
226
228
  return this.agents.get(aid);
227
229
  }
228
- let raw = null;
230
+ let expandedRaw = null;
229
231
  try {
230
- raw = loadAgent(aid);
232
+ expandedRaw = loadAgent(aid);
231
233
  }
232
234
  catch (e) {
233
235
  const reason = e instanceof Error ? e.message : String(e);
@@ -239,22 +241,23 @@ export class EvolAgentRegistry {
239
241
  channels: [],
240
242
  }, reason);
241
243
  }
242
- if (!raw) {
244
+ if (!expandedRaw) {
243
245
  logger.warn(`[EvolAgentRegistry] loadNewAgent: ${aid}/config.json not found`);
244
246
  return null;
245
247
  }
246
- const errs = validateAgentConfig(raw);
248
+ const errs = validateAgentConfig(expandedRaw);
247
249
  if (errs.length > 0) {
248
250
  const reason = errs.join('; ');
249
251
  logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${reason}`);
250
- return this.registerErrorAgent(raw, reason);
252
+ return this.registerErrorAgent(expandedRaw, reason);
251
253
  }
252
- const conflict = this.checkConflictForReload(raw, aid);
254
+ const conflict = this.checkConflictForReload(expandedRaw, aid);
253
255
  if (conflict) {
254
256
  logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${conflict}`);
255
- return this.registerErrorAgent(raw, `Channel conflict: ${conflict}`);
257
+ return this.registerErrorAgent(expandedRaw, `Channel conflict: ${conflict}`);
256
258
  }
257
- const merged = resolveEffective({ self: aid });
259
+ const raw = readConfig(ConfigTarget.Agent, { self: aid }, { cache: true }) ?? expandedRaw;
260
+ const merged = resolveEffective({ self: aid }, { expand: true });
258
261
  const agent = new EvolAgent(raw, merged);
259
262
  ensureAgentDirSkeleton(aid);
260
263
  this.agents.set(aid, agent);
@@ -268,13 +271,14 @@ export class EvolAgentRegistry {
268
271
  const oldAgent = this.agents.get(aidOrName);
269
272
  if (!oldAgent)
270
273
  throw new Error(`Agent "${aidOrName}" not found`);
271
- const raw = loadAgent(oldAgent.aid);
272
- if (!raw)
274
+ const expandedRaw = loadAgent(oldAgent.aid);
275
+ if (!expandedRaw)
273
276
  throw new Error(`Agent ${oldAgent.aid}/config.json missing on reload`);
274
- const errs = validateAgentConfig(raw);
277
+ const errs = validateAgentConfig(expandedRaw);
275
278
  if (errs.length > 0)
276
279
  throw new Error(`Invalid config after edit: ${errs.join('; ')}`);
277
- const merged = resolveEffective({ self: raw.aid });
280
+ const raw = readConfig(ConfigTarget.Agent, { self: oldAgent.aid }, { cache: true }) ?? expandedRaw;
281
+ const merged = resolveEffective({ self: raw.aid }, { expand: true });
278
282
  if (oldAgent.status === 'disabled' && raw.enabled !== false) {
279
283
  oldAgent.swapConfig(raw, merged);
280
284
  const hotLoad = globalThis.__evolcore_hotLoadAgent;
@@ -289,32 +293,69 @@ export class EvolAgentRegistry {
289
293
  }
290
294
  if (oldAgent.status !== 'disabled' && raw.enabled === false) {
291
295
  let prepared = false;
296
+ let runnerTransaction;
297
+ const disconnectedChannels = [];
298
+ const previousConfig = oldAgent.captureConfigSnapshot();
299
+ const previousStatus = oldAgent.status;
300
+ const previousError = oldAgent.error;
292
301
  try {
293
302
  await hooks.prepareHandoffReload?.(oldAgent.aid);
294
303
  prepared = true;
304
+ runnerTransaction = await hooks.stageAgentRunners?.(new EvolAgent(raw, merged));
295
305
  for (const ch of oldAgent.channelInstanceNames()) {
296
306
  try {
297
307
  await hooks.drainChannel(ch);
298
308
  }
299
309
  catch { }
300
- try {
301
- await hooks.disconnectChannel(ch);
302
- }
303
- catch { }
310
+ await hooks.disconnectChannel(ch);
311
+ disconnectedChannels.push(ch);
304
312
  }
305
313
  oldAgent.swapConfig(raw, merged);
306
314
  oldAgent.status = 'disabled';
307
315
  this.channelIndex.clear();
308
316
  this.buildChannelIndex();
317
+ runnerTransaction?.commit();
318
+ try {
319
+ await runnerTransaction?.finalize();
320
+ }
321
+ catch (error) {
322
+ logger.warn(`[Reload] Failed to dispose previous runners after disabling "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
323
+ }
324
+ prepared = false;
309
325
  logger.info(`[Reload] Agent "${aidOrName}" disabled`);
310
326
  return;
311
327
  }
312
328
  catch (error) {
329
+ const rollbackErrors = [];
330
+ oldAgent.swapConfig(previousConfig.rawAgent, previousConfig.merged);
331
+ oldAgent.invalidatePersonaCache();
332
+ oldAgent.status = previousStatus;
333
+ oldAgent.error = previousError;
334
+ await runnerTransaction?.rollback().catch(rollbackError => {
335
+ rollbackErrors.push(`runners: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
336
+ });
337
+ for (const ch of disconnectedChannels) {
338
+ try {
339
+ await hooks.startChannel(oldAgent, ch);
340
+ }
341
+ catch (restartError) {
342
+ rollbackErrors.push(`restart ${ch}: ${restartError instanceof Error ? restartError.message : String(restartError)}`);
343
+ }
344
+ }
345
+ this.channelIndex.clear();
346
+ this.buildChannelIndex();
313
347
  if (prepared) {
314
348
  try {
315
349
  await hooks.completeHandoffReload?.(oldAgent.aid);
316
350
  }
317
- catch { }
351
+ catch (resumeError) {
352
+ rollbackErrors.push(`handoff: ${resumeError instanceof Error ? resumeError.message : String(resumeError)}`);
353
+ }
354
+ }
355
+ if (rollbackErrors.length > 0) {
356
+ oldAgent.status = 'error';
357
+ oldAgent.error = `Disable failed; rollback incomplete: ${rollbackErrors.join('; ')}`;
358
+ logger.error(`[Reload] Disable rollback incomplete for "${aidOrName}": ${rollbackErrors.join('; ')}`);
318
359
  }
319
360
  throw error;
320
361
  }
@@ -324,13 +365,18 @@ export class EvolAgentRegistry {
324
365
  throw new Error(`Channel conflict: ${conflict}`);
325
366
  const removedSuccessfully = [];
326
367
  const addedSuccessfully = [];
368
+ let runnerTransaction;
327
369
  let prepared = false;
370
+ const previousConfig = oldAgent.captureConfigSnapshot();
371
+ const previousStatus = oldAgent.status;
372
+ const previousError = oldAgent.error;
328
373
  try {
329
374
  await hooks.prepareHandoffReload?.(oldAgent.aid);
330
375
  prepared = true;
376
+ runnerTransaction = await hooks.stageAgentRunners?.(new EvolAgent(raw, merged));
331
377
  const oldChannels = new Set(oldAgent.channelInstanceNames());
332
378
  const aunKey = oldAgent.effectiveChannelName('aun', 'main');
333
- const otherKeys = raw.channels.filter(c => c.type !== 'aun').map(c => oldAgent.effectiveChannelName(c.type, c.name));
379
+ const otherKeys = merged.channels.filter(c => c.type !== 'aun').map(c => oldAgent.effectiveChannelName(c.type, c.name));
334
380
  const newChannels = new Set([aunKey, ...otherKeys]);
335
381
  const toRemove = [...oldChannels].filter(c => !newChannels.has(c));
336
382
  const toAdd = [...newChannels].filter(c => !oldChannels.has(c));
@@ -338,7 +384,7 @@ export class EvolAgentRegistry {
338
384
  const credentialsChanged = [];
339
385
  for (const ch of kept) {
340
386
  const oldInst = oldAgent.findChannelInstance(ch);
341
- const newInst = findInstanceByKey(raw, oldAgent, ch);
387
+ const newInst = findInstanceByKey(merged, oldAgent, ch);
342
388
  if (oldInst && newInst && JSON.stringify(oldInst) !== JSON.stringify(newInst)) {
343
389
  credentialsChanged.push(ch);
344
390
  }
@@ -360,28 +406,72 @@ export class EvolAgentRegistry {
360
406
  oldAgent.status = 'running';
361
407
  this.channelIndex.clear();
362
408
  this.buildChannelIndex();
363
- await hooks.completeHandoffReload?.(oldAgent.aid);
364
- prepared = false;
409
+ runnerTransaction?.commit();
410
+ try {
411
+ await hooks.completeHandoffReload?.(oldAgent.aid);
412
+ }
413
+ catch (error) {
414
+ logger.warn(`[Reload] Handoff recovery failed after commit for "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
415
+ }
416
+ finally {
417
+ prepared = false;
418
+ }
419
+ try {
420
+ await hooks.afterReload?.(oldAgent);
421
+ }
422
+ catch (error) {
423
+ logger.warn(`[Reload] Post-reload hook failed after commit for "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
424
+ }
425
+ try {
426
+ await runnerTransaction?.finalize();
427
+ }
428
+ catch (error) {
429
+ logger.warn(`[Reload] Failed to dispose previous runners for "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
430
+ }
365
431
  }
366
432
  catch (err) {
367
- logger.error(`[Reload] Failed: ${err}. Attempting rollback for "${aidOrName}".`);
368
- for (const ch of addedSuccessfully) {
433
+ logger.error(`[Reload] Failed before commit: ${err}. Attempting rollback for "${aidOrName}".`);
434
+ const rollbackErrors = [];
435
+ for (const ch of [...addedSuccessfully].reverse()) {
369
436
  try {
370
437
  await hooks.disconnectChannel(ch);
371
438
  }
372
- catch { }
439
+ catch (error) {
440
+ rollbackErrors.push(`disconnect ${ch}: ${error instanceof Error ? error.message : String(error)}`);
441
+ }
373
442
  }
443
+ oldAgent.swapConfig(previousConfig.rawAgent, previousConfig.merged);
444
+ oldAgent.invalidatePersonaCache();
445
+ await runnerTransaction?.rollback().catch(error => {
446
+ rollbackErrors.push(`runners: ${error instanceof Error ? error.message : String(error)}`);
447
+ });
448
+ for (const ch of [...new Set(removedSuccessfully)]) {
449
+ try {
450
+ await hooks.startChannel(oldAgent, ch);
451
+ }
452
+ catch (error) {
453
+ rollbackErrors.push(`restart ${ch}: ${error instanceof Error ? error.message : String(error)}`);
454
+ }
455
+ }
456
+ this.channelIndex.clear();
457
+ this.buildChannelIndex();
374
458
  if (prepared) {
375
459
  try {
376
460
  await hooks.completeHandoffReload?.(oldAgent.aid);
377
461
  }
378
462
  catch (resumeError) {
379
- logger.error(`[Reload] Failed to resume Handoff dispatcher for "${aidOrName}":`, resumeError);
463
+ rollbackErrors.push(`handoff: ${resumeError instanceof Error ? resumeError.message : String(resumeError)}`);
380
464
  }
381
465
  }
382
- void removedSuccessfully;
383
- oldAgent.status = 'error';
384
- oldAgent.error = `Reload failed (rollback partial): ${err instanceof Error ? err.message : String(err)}`;
466
+ if (rollbackErrors.length === 0) {
467
+ oldAgent.status = previousStatus;
468
+ oldAgent.error = previousError;
469
+ }
470
+ else {
471
+ oldAgent.status = 'error';
472
+ oldAgent.error = `Reload failed; rollback incomplete: ${rollbackErrors.join('; ')}`;
473
+ logger.error(`[Reload] Rollback incomplete for "${aidOrName}": ${rollbackErrors.join('; ')}`);
474
+ }
385
475
  throw err;
386
476
  }
387
477
  }