evolcore 0.0.11 → 0.0.13

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 (164) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/bin/ec-safe-output.js +26 -7
  4. package/dist/agents/claude-runner.js +86 -7
  5. package/dist/agents/codex-app-server-client.js +174 -20
  6. package/dist/agents/codex-runner.js +375 -92
  7. package/dist/agents/ecagent-runner.js +39 -4
  8. package/dist/agents/gemini-runner.js +1 -0
  9. package/dist/aun/aid/client.js +11 -21
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/managed-operation.js +107 -0
  12. package/dist/aun/aid/store.js +13 -7
  13. package/dist/aun/group-fs-download.js +56 -0
  14. package/dist/aun/group-identity.js +1 -2
  15. package/dist/aun/msg/group.js +91 -44
  16. package/dist/aun/msg/index.js +1 -2
  17. package/dist/aun/msg/managed-operation.js +865 -0
  18. package/dist/aun/msg/mention-schema.js +20 -0
  19. package/dist/aun/msg/p2p.js +7 -0
  20. package/dist/aun/msg/upload.js +16 -1
  21. package/dist/aun/outbox.js +31 -2
  22. package/dist/aun/rpc/client.js +66 -0
  23. package/dist/aun/rpc/index.js +1 -2
  24. package/dist/aun/storage/{upload.js → client.js} +11 -2
  25. package/dist/aun/storage/index.js +1 -2
  26. package/dist/channels/aun.js +655 -240
  27. package/dist/channels/daemon.js +13 -0
  28. package/dist/cli/agent-command.js +56 -8
  29. package/dist/cli/agent.js +108 -31
  30. package/dist/cli/aun-commands.js +270 -69
  31. package/dist/cli/bench.js +12 -3
  32. package/dist/cli/cli-argv.js +10 -0
  33. package/dist/cli/config.js +9 -17
  34. package/dist/cli/ctl-command.js +1 -1
  35. package/dist/cli/daemon-commands.js +70 -55
  36. package/dist/cli/fs-command.js +79 -10
  37. package/dist/cli/handoff-command.js +3 -0
  38. package/dist/cli/index.js +30 -2
  39. package/dist/cli/init.js +77 -8
  40. package/dist/cli/managed-command-guard.js +41 -0
  41. package/dist/cli/model.js +11 -4
  42. package/dist/cli/queue-command.js +53 -1
  43. package/dist/cli/response.js +71 -0
  44. package/dist/cli/restart-monitor.js +7 -32
  45. package/dist/cli/trigger-command.js +11 -0
  46. package/dist/config/access-policy.js +1 -2
  47. package/dist/config/builtin-roles.js +9 -0
  48. package/dist/config/config-manager.js +21 -1
  49. package/dist/config/contact-book-store.js +11 -3
  50. package/dist/config/contact-request-service.js +161 -15
  51. package/dist/config/mention-mode.js +8 -24
  52. package/dist/config/peer-role-resolver.js +6 -2
  53. package/dist/config/role-migration-startup.js +53 -0
  54. package/dist/config/role-store.js +1 -1
  55. package/dist/core/auth/agent-delegation.js +1 -2
  56. package/dist/core/auth/auth-gateway.js +29 -1
  57. package/dist/core/auth/authorization-audit.js +15 -0
  58. package/dist/core/auth/operation-authorizer.js +31 -7
  59. package/dist/core/auth/operation-catalog.js +196 -25
  60. package/dist/core/bootstrap-messages.js +25 -0
  61. package/dist/core/bootstrap-service.js +64 -4
  62. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  63. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  64. package/dist/core/capability/skill-discovery.js +55 -0
  65. package/dist/core/channel-loader.js +1 -1
  66. package/dist/core/command/cli-intent-parser.js +9 -0
  67. package/dist/core/command/command-handler.js +650 -42
  68. package/dist/core/command/connect-menu.js +15 -0
  69. package/dist/core/command/group-menu.js +4 -4
  70. package/dist/core/command/menu-catalog.js +370 -0
  71. package/dist/core/command/menu-handler.js +230 -51
  72. package/dist/core/command/menu-protocol.js +94 -2
  73. package/dist/core/command/slash-gate.js +5 -2
  74. package/dist/core/command/slash-handler.js +99 -31
  75. package/dist/core/daemon-file-cache.js +2 -1
  76. package/dist/core/data-migration.js +9 -2
  77. package/dist/core/event-catalog.js +6 -0
  78. package/dist/core/evolagent.js +4 -2
  79. package/dist/core/handoff/dispatcher.js +5 -2
  80. package/dist/core/handoff/runtime.js +157 -22
  81. package/dist/core/handoff/store.js +29 -1
  82. package/dist/core/handoff/types.js +1 -0
  83. package/dist/core/interaction-router.js +10 -0
  84. package/dist/core/message/im-renderer.js +19 -0
  85. package/dist/core/message/logical-queue-bridge.js +1 -0
  86. package/dist/core/message/mention-schema.js +126 -0
  87. package/dist/core/message/message-bridge.js +298 -88
  88. package/dist/core/message/message-log.js +3 -0
  89. package/dist/core/message/message-queue.js +268 -39
  90. package/dist/core/message/message-utils.js +81 -4
  91. package/dist/core/message/response-engine.js +161 -77
  92. package/dist/core/message/stream-debouncer.js +10 -2
  93. package/dist/core/permission/approval-gateway.js +1 -0
  94. package/dist/core/permission/ec-command-parser.js +243 -18
  95. package/dist/core/permission/index.js +1 -1
  96. package/dist/core/permission/mode.js +15 -0
  97. package/dist/core/permission/readonly-shell-query.js +148 -8
  98. package/dist/core/permission/sandbox-runtime.js +43 -1
  99. package/dist/core/permission/tool-policy.js +322 -57
  100. package/dist/core/protected-paths.js +114 -4
  101. package/dist/core/session/session-manager.js +42 -4
  102. package/dist/core/session/session-mapper.js +20 -1
  103. package/dist/eck/detect.js +18 -1
  104. package/dist/eck/group-rules-sync.js +15 -44
  105. package/dist/eck/manifest-engine.js +39 -13
  106. package/dist/index.js +541 -180
  107. package/dist/ipc.js +232 -10
  108. package/dist/paths.js +3 -0
  109. package/dist/response-system/coordinator.js +6 -4
  110. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  111. package/dist/response-system/modes/single-session/index.js +14 -1
  112. package/dist/response-system/selector.js +6 -5
  113. package/dist/trigger/anomaly-store.js +4 -0
  114. package/dist/trigger/feedback.js +64 -11
  115. package/dist/trigger/manager.js +21 -1
  116. package/dist/trigger/parser.js +14 -2
  117. package/dist/trigger/scheduler.js +1 -0
  118. package/dist/trigger/script-executor.js +1 -0
  119. package/dist/trigger/validation.js +67 -6
  120. package/dist/utils/codex-app-server-registry.js +90 -0
  121. package/dist/utils/codex-cli.js +5 -1
  122. package/dist/utils/cross-platform.js +15 -0
  123. package/dist/utils/ecweb-supervisor.js +332 -0
  124. package/dist/utils/error-utils.js +10 -0
  125. package/dist/utils/npm-ops.js +5 -12
  126. package/dist/utils/tool-summary.js +11 -0
  127. package/kits/docs/channels/aun.md +1 -1
  128. package/kits/docs/context-assembly.md +2 -2
  129. package/kits/docs/evolcore/agent.md +11 -6
  130. package/kits/docs/evolcore/aid.md +14 -0
  131. package/kits/docs/evolcore/trigger.md +17 -1
  132. package/kits/docs/prompt-loading-architecture.md +1 -2
  133. package/kits/docs/venues/group.md +1 -1
  134. package/kits/eck_manifest.json +0 -12
  135. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  136. package/kits/rules/04-relation.md +1 -1
  137. package/kits/rules/05-venue.md +2 -2
  138. package/kits/schemas/role-config.schema.1.json +1 -0
  139. package/kits/schemas/single-session.schema.1.json +3 -3
  140. package/kits/schemas/single-session.schema.2.json +3 -3
  141. package/kits/templates/roles/admin.json +27 -0
  142. package/kits/templates/roles/member.json +34 -1
  143. package/kits/templates/roles/visitor.json +35 -3
  144. package/kits/templates/system-fragments/bootstrap.md +1 -1
  145. package/kits/templates/system-fragments/channel.md +1 -1
  146. package/kits/templates/system-fragments/session.md +1 -1
  147. package/package.json +2 -2
  148. package/dist/aun/msg/payload-type.js +0 -27
  149. package/dist/aun/rpc/caller.js +0 -42
  150. package/dist/aun/rpc/connection.js +0 -25
  151. package/dist/aun/storage/manage.js +0 -10
  152. package/dist/config/access-policy-domain.js +0 -18
  153. package/dist/config/config-batch-get.js +0 -11
  154. package/dist/config/owner-policy.js +0 -4
  155. package/dist/config/role-config-v4-startup.js +0 -32
  156. package/dist/config/role-config-v5-startup.js +0 -27
  157. package/dist/core/auth/trigger-authorization.js +0 -15
  158. package/dist/core/command/evol-menu-version-gate.js +0 -39
  159. package/dist/core/interaction-registration.js +0 -10
  160. package/dist/core/permission/execution-sandbox.js +0 -16
  161. package/dist/core/relation/peer-key.js +0 -1
  162. package/dist/core/session/session-key.js +0 -24
  163. package/dist/eck/baseagent-caps.js +0 -18
  164. package/dist/eck/rules-loader.js +0 -28
@@ -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 : {};
@@ -198,6 +269,9 @@ export class MenuRequestDeduper {
198
269
  ttlMs;
199
270
  maxEntries;
200
271
  entries = new Map();
272
+ /** Retain failed request identities briefly so a subsequent identical
273
+ * request is observable as a retry instead of an unrelated first attempt. */
274
+ failures = new Map();
201
275
  constructor(ttlMs = 30_000, maxEntries = 2_048) {
202
276
  this.ttlMs = ttlMs;
203
277
  this.maxEntries = maxEntries;
@@ -208,7 +282,13 @@ export class MenuRequestDeduper {
208
282
  if (existing) {
209
283
  if (existing.fingerprint !== fingerprint)
210
284
  return { conflict: true };
211
- return { value: await existing.promise, replayed: true };
285
+ return { value: await existing.promise, replayed: true, retry: false };
286
+ }
287
+ const previousFailure = this.failures.get(key);
288
+ if (previousFailure) {
289
+ if (previousFailure.fingerprint !== fingerprint)
290
+ return { conflict: true };
291
+ this.failures.delete(key);
212
292
  }
213
293
  const promise = operation();
214
294
  const entry = { fingerprint, promise, expiresAt: Date.now() + this.ttlMs };
@@ -217,10 +297,12 @@ export class MenuRequestDeduper {
217
297
  try {
218
298
  const value = await promise;
219
299
  entry.expiresAt = Date.now() + this.ttlMs;
220
- return { value, replayed: false };
300
+ return { value, replayed: false, retry: !!previousFailure };
221
301
  }
222
302
  catch (error) {
223
303
  this.entries.delete(key);
304
+ this.failures.set(key, { fingerprint, expiresAt: Date.now() + this.ttlMs });
305
+ this.enforceLimit();
224
306
  throw error;
225
307
  }
226
308
  }
@@ -230,6 +312,10 @@ export class MenuRequestDeduper {
230
312
  if (entry.expiresAt <= now)
231
313
  this.entries.delete(key);
232
314
  }
315
+ for (const [key, entry] of this.failures) {
316
+ if (entry.expiresAt <= now)
317
+ this.failures.delete(key);
318
+ }
233
319
  }
234
320
  enforceLimit() {
235
321
  while (this.entries.size > this.maxEntries) {
@@ -238,6 +324,12 @@ export class MenuRequestDeduper {
238
324
  break;
239
325
  this.entries.delete(oldest);
240
326
  }
327
+ while (this.failures.size > this.maxEntries) {
328
+ const oldest = this.failures.keys().next().value;
329
+ if (!oldest)
330
+ break;
331
+ this.failures.delete(oldest);
332
+ }
241
333
  }
242
334
  }
243
335
  export class MenuDiagnosticLimiter {
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  import { hasModelSwitcher, hasPermissionController } from '../../agents/runner-types.js';
2
2
  import { getCodexEfforts } from '../../agents/codex-runner.js';
3
- import { buildEnvelope } from '../message/message-utils.js';
3
+ import { buildEnvelope, isDeliveryTargetForChannel } from '../message/message-utils.js';
4
4
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
5
5
  import { logger } from '../../utils/logger.js';
6
6
  import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
@@ -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;
@@ -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 ')) {
@@ -2469,28 +2492,58 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2469
2492
  // 执行重启逻辑(共用于卡片回调和文本确认)
2470
2493
  const executeRestart = async () => {
2471
2494
  const suppressRealRestart = shouldSuppressRealRestart();
2472
- let replyContext;
2473
- if (threadId) {
2474
- const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
2475
- if (threadSession)
2476
- replyContext = this.getReplyContext(threadSession);
2495
+ let restartReplyContext = replyContext;
2496
+ try {
2497
+ if (!restartReplyContext?.delivery && threadId) {
2498
+ const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
2499
+ if (threadSession)
2500
+ restartReplyContext = this.getReplyContext(threadSession);
2501
+ }
2502
+ else if (!restartReplyContext?.delivery && activeSession) {
2503
+ restartReplyContext = this.getReplyContext(activeSession);
2504
+ }
2505
+ }
2506
+ catch (error) {
2507
+ const code = error?.code;
2508
+ if (channel.split('#', 1)[0].toLowerCase() !== 'aun'
2509
+ || (code !== 'AUN_OUTBOUND_ROUTE_REQUIRED' && code !== 'AUN_OUTBOUND_ROUTE_MISMATCH')) {
2510
+ throw error;
2511
+ }
2512
+ logger.warn(`[System] Refusing restart from invalid persisted AUN session route: channel=${channel} channelId=${channelId} code=${code}`);
2513
+ return false;
2514
+ }
2515
+ // The authenticated inbound transport context takes precedence over a
2516
+ // potentially stale session snapshot. When no session exists, the
2517
+ // explicit command chat type is the only acceptable local fallback.
2518
+ if (restartReplyContext?.delivery === undefined) {
2519
+ restartReplyContext = {
2520
+ ...(restartReplyContext ?? {}),
2521
+ ...(chatType === 'group'
2522
+ ? { delivery: { chatType: 'group', groupId: channelId } }
2523
+ : chatType === 'private'
2524
+ ? { delivery: { chatType: 'private' } }
2525
+ : {}),
2526
+ };
2527
+ }
2528
+ if (channel.split('#', 1)[0].toLowerCase() === 'aun'
2529
+ && !isDeliveryTargetForChannel(restartReplyContext?.delivery, channelId)) {
2530
+ logger.warn(`[System] Refusing restart without a valid explicit delivery route: channel=${channel} channelId=${channelId}`);
2531
+ return false;
2477
2532
  }
2478
2533
  const restartInfo = {
2479
2534
  channel,
2480
2535
  channelId,
2481
2536
  timestamp: Date.now(),
2482
- ...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
2537
+ ...(restartReplyContext?.delivery ? { delivery: restartReplyContext.delivery } : {}),
2538
+ ...(restartReplyContext?.replyToMessageId ? { rootId: restartReplyContext.replyToMessageId } : {}),
2483
2539
  };
2484
2540
  if (!suppressRealRestart) {
2485
2541
  const controlDir = daemonControlDir();
2486
2542
  fs.mkdirSync(controlDir, { recursive: true });
2487
2543
  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();
2544
+ spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
2545
+ EVOLCORE_HOME: resolvePaths().root,
2546
+ });
2494
2547
  }
2495
2548
  else {
2496
2549
  logger.info('[System] Suppressed real restart in test runtime');
@@ -2507,7 +2560,11 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2507
2560
  channelId,
2508
2561
  agentName: 'system',
2509
2562
  chatmode: 'interactive',
2510
- replyContext,
2563
+ replyContext: restartReplyContext ?? (chatType === 'group'
2564
+ ? { delivery: { chatType: 'group', groupId: channelId } }
2565
+ : chatType === 'private'
2566
+ ? { delivery: { chatType: 'private' } }
2567
+ : undefined),
2511
2568
  });
2512
2569
  await adapter.send(envelope, { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' });
2513
2570
  // 等待消息发送完成后再延迟 kill
@@ -2550,7 +2607,10 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2550
2607
  return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
2551
2608
  }
2552
2609
  }
2553
- await executeRestart();
2610
+ const restarted = await executeRestart();
2611
+ if (!restarted) {
2612
+ return { kind: 'command.error', text: '❌ 无法确定重启通知的出站路由,请从可信会话上下文重试' };
2613
+ }
2554
2614
  // executeRestart 内部已经发送了反馈消息,这里返回 null 避免重复发送
2555
2615
  return null;
2556
2616
  }
@@ -2708,7 +2768,15 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
2708
2768
  }
2709
2769
  // 发送文件
2710
2770
  try {
2711
- const replyCtx = !isCrossChannel && sendSession ? this.getReplyContext(sendSession) : undefined;
2771
+ const replyCtx = isCrossChannel
2772
+ ? { delivery: { chatType: 'private' } }
2773
+ : sendSession
2774
+ ? this.getReplyContext(sendSession)
2775
+ : chatType === 'group'
2776
+ ? { delivery: { chatType: 'group', groupId: channelId } }
2777
+ : chatType === 'private'
2778
+ ? { delivery: { chatType: 'private' } }
2779
+ : undefined;
2712
2780
  await targetAdapter.send(buildEnvelope({ channel: targetAdapter.channelName, channelId: targetChannelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath });
2713
2781
  const sizeStr = stat.size < 1024 ? `${stat.size} B`
2714
2782
  : stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
@@ -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
  }
@@ -313,6 +313,9 @@ const CATALOG = [
313
313
  { path: 'sessionId', type: 'string' },
314
314
  { path: 'toolName', type: 'string' },
315
315
  { path: 'input', type: 'object' },
316
+ { path: 'callId', type: 'string', optional: true },
317
+ { path: 'agentAid', type: 'string', optional: true },
318
+ { path: 'permissionMode', type: 'string', optional: true },
316
319
  { path: 'timestamp', type: 'number', optional: true },
317
320
  ],
318
321
  },
@@ -326,6 +329,9 @@ const CATALOG = [
326
329
  { path: 'toolName', type: 'string' },
327
330
  { path: 'isError', type: 'boolean', optional: true },
328
331
  { path: 'agentName', type: 'string', optional: true },
332
+ { path: 'callId', type: 'string', optional: true },
333
+ { path: 'agentAid', type: 'string', optional: true },
334
+ { path: 'permissionMode', type: 'string', optional: true },
329
335
  { path: 'timestamp', type: 'number', optional: true },
330
336
  ],
331
337
  },
@@ -276,7 +276,8 @@ export class EvolAgent {
276
276
  */
277
277
  getPersona() {
278
278
  const personaPath = path.join(agentPersonalDir(this.aid), 'persona.md');
279
- return fileCache.get(personaPath, (raw) => (raw === null ? null : (raw.trim() || null)), { policy: 'mtime', group: this.agentFilesGroup() });
279
+ const raw = fileCache.getText(personaPath, { policy: 'mtime', group: this.agentFilesGroup() });
280
+ return raw === null ? null : (raw.trim() || null);
280
281
  }
281
282
  /**
282
283
  * 读取 personal/memory/working.md 内容。走 fileCache(mtime 门控):
@@ -285,7 +286,8 @@ export class EvolAgent {
285
286
  */
286
287
  getWorkingMemory() {
287
288
  const workingPath = path.join(agentPersonalDir(this.aid), 'memory', 'working.md');
288
- return fileCache.get(workingPath, (raw) => (raw === null ? null : (raw.trim() || null)), { policy: 'mtime', group: this.agentFilesGroup() });
289
+ const raw = fileCache.getText(workingPath, { policy: 'mtime', group: this.agentFilesGroup() });
290
+ return raw === null ? null : (raw.trim() || null);
289
291
  }
290
292
  /** 清除本 agent 身份层缓存(reload 后重新读取)。只失效自己的文件组,不波及他人。 */
291
293
  invalidatePersonaCache() {
@@ -117,8 +117,9 @@ export class HandoffDispatcher {
117
117
  if (this.pausedAgents.has(selfAid))
118
118
  return false;
119
119
  const current = this.store.get(selfAid, next.handoff_id);
120
- if (!current || current.state !== 'queued' || current.attention_required)
121
- return current?.state === 'target_sent';
120
+ if (!current || current.state !== 'queued' || current.attention_required) {
121
+ return current?.state === 'target_sent' || current?.state === 'discarded';
122
+ }
122
123
  this.store.recordSendStarted(selfAid, current.handoff_id);
123
124
  try {
124
125
  const send = await this.sendWithinQueueDeadline(current);
@@ -171,6 +172,8 @@ export class HandoffDispatcher {
171
172
  await new Promise(resolve => setTimeout(resolve, this.retryDelaysMs[Math.min(attempt - 1, this.retryDelaysMs.length - 1)] ?? 0));
172
173
  }
173
174
  }
175
+ if (this.store.get(selfAid, next.handoff_id)?.state === 'discarded')
176
+ continue;
174
177
  if (!sent) {
175
178
  this.store.recordSendExhausted(selfAid, next.handoff_id);
176
179
  logger.error(`[Handoff] target send retries exhausted: ${next.handoff_id}`);