evolcore 0.0.11 → 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 (102) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/dist/agents/claude-runner.js +35 -2
  4. package/dist/agents/codex-app-server-client.js +120 -14
  5. package/dist/agents/codex-runner.js +273 -35
  6. package/dist/agents/ecagent-runner.js +36 -4
  7. package/dist/agents/gemini-runner.js +1 -0
  8. package/dist/aun/aid/client.js +11 -21
  9. package/dist/aun/aid/managed-operation.js +107 -0
  10. package/dist/aun/msg/group.js +81 -12
  11. package/dist/aun/msg/managed-operation.js +804 -0
  12. package/dist/aun/msg/mention-schema.js +20 -0
  13. package/dist/aun/msg/p2p.js +7 -0
  14. package/dist/channels/aun.js +288 -108
  15. package/dist/channels/daemon.js +13 -0
  16. package/dist/cli/agent-command.js +56 -8
  17. package/dist/cli/agent.js +170 -6
  18. package/dist/cli/aun-commands.js +270 -69
  19. package/dist/cli/bench.js +12 -3
  20. package/dist/cli/daemon-commands.js +31 -31
  21. package/dist/cli/fs-command.js +60 -0
  22. package/dist/cli/handoff-command.js +3 -0
  23. package/dist/cli/index.js +30 -2
  24. package/dist/cli/managed-command-guard.js +41 -0
  25. package/dist/cli/model.js +11 -4
  26. package/dist/cli/queue-command.js +11 -1
  27. package/dist/cli/response.js +71 -0
  28. package/dist/cli/restart-monitor.js +4 -18
  29. package/dist/cli/trigger-command.js +4 -0
  30. package/dist/config/builtin-roles.js +4 -0
  31. package/dist/config/contact-book-store.js +11 -3
  32. package/dist/config/contact-request-service.js +161 -15
  33. package/dist/config/mention-mode.js +8 -24
  34. package/dist/core/auth/agent-delegation.js +0 -1
  35. package/dist/core/auth/operation-authorizer.js +30 -7
  36. package/dist/core/auth/operation-catalog.js +187 -25
  37. package/dist/core/bootstrap-messages.js +18 -0
  38. package/dist/core/bootstrap-service.js +63 -4
  39. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  40. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  41. package/dist/core/capability/skill-discovery.js +55 -0
  42. package/dist/core/channel-loader.js +1 -1
  43. package/dist/core/command/cli-intent-parser.js +9 -0
  44. package/dist/core/command/command-handler.js +467 -6
  45. package/dist/core/command/connect-menu.js +15 -0
  46. package/dist/core/command/group-menu.js +4 -4
  47. package/dist/core/command/menu-catalog.js +366 -0
  48. package/dist/core/command/menu-handler.js +147 -35
  49. package/dist/core/command/menu-protocol.js +71 -0
  50. package/dist/core/command/slash-gate.js +5 -2
  51. package/dist/core/command/slash-handler.js +41 -21
  52. package/dist/core/daemon-file-cache.js +2 -1
  53. package/dist/core/data-migration.js +9 -2
  54. package/dist/core/evolagent.js +4 -2
  55. package/dist/core/handoff/dispatcher.js +5 -2
  56. package/dist/core/handoff/runtime.js +145 -21
  57. package/dist/core/handoff/store.js +24 -0
  58. package/dist/core/handoff/types.js +1 -0
  59. package/dist/core/message/logical-queue-bridge.js +1 -0
  60. package/dist/core/message/mention-schema.js +126 -0
  61. package/dist/core/message/message-bridge.js +236 -78
  62. package/dist/core/message/message-log.js +1 -0
  63. package/dist/core/message/message-queue.js +1 -0
  64. package/dist/core/message/response-engine.js +54 -36
  65. package/dist/core/permission/ec-command-parser.js +156 -10
  66. package/dist/core/permission/sandbox-runtime.js +22 -1
  67. package/dist/core/permission/tool-policy.js +73 -30
  68. package/dist/core/protected-paths.js +7 -2
  69. package/dist/core/session/session-manager.js +21 -2
  70. package/dist/eck/manifest-engine.js +39 -13
  71. package/dist/index.js +384 -98
  72. package/dist/ipc.js +218 -9
  73. package/dist/response-system/coordinator.js +6 -4
  74. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  75. package/dist/response-system/modes/single-session/index.js +14 -1
  76. package/dist/response-system/selector.js +6 -5
  77. package/dist/trigger/anomaly-store.js +4 -0
  78. package/dist/trigger/script-executor.js +1 -0
  79. package/dist/utils/codex-app-server-registry.js +90 -0
  80. package/dist/utils/codex-cli.js +5 -1
  81. package/dist/utils/cross-platform.js +15 -0
  82. package/dist/utils/npm-ops.js +5 -12
  83. package/dist/utils/tool-summary.js +11 -0
  84. package/kits/docs/channels/aun.md +1 -1
  85. package/kits/docs/context-assembly.md +2 -2
  86. package/kits/docs/evolcore/agent.md +11 -6
  87. package/kits/docs/evolcore/aid.md +14 -0
  88. package/kits/docs/prompt-loading-architecture.md +1 -2
  89. package/kits/docs/venues/group.md +1 -1
  90. package/kits/eck_manifest.json +0 -12
  91. package/kits/rules/04-relation.md +1 -1
  92. package/kits/rules/05-venue.md +2 -2
  93. package/kits/schemas/single-session.schema.1.json +3 -3
  94. package/kits/schemas/single-session.schema.2.json +3 -3
  95. package/kits/templates/roles/admin.json +22 -0
  96. package/kits/templates/roles/member.json +27 -0
  97. package/kits/templates/roles/visitor.json +33 -3
  98. package/kits/templates/system-fragments/bootstrap.md +1 -1
  99. package/kits/templates/system-fragments/channel.md +1 -1
  100. package/kits/templates/system-fragments/session.md +1 -1
  101. package/package.json +2 -2
  102. package/dist/core/command/evol-menu-version-gate.js +0 -39
@@ -15,7 +15,7 @@ import { execFileSync } from 'child_process';
15
15
  import { CronExpressionParser } from 'cron-parser';
16
16
  import { parseDuration } from '../../trigger/parser.js';
17
17
  import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions, resolveGlobalPkg } from '../../utils/npm-ops.js';
18
- import { commandExists } from '../../utils/cross-platform.js';
18
+ import { commandExists, spawnDetachedNode } from '../../utils/cross-platform.js';
19
19
  import { loadDefaults, loadDaemonConfig } from '../../config-store.js';
20
20
  import { WEB_PACKAGE_NAME } from '../../product.js';
21
21
  import { read as cfgRead, resolveEffective, resolveEffectiveFieldWithSource, routeFieldPath, write as cfgWrite, ConfigTarget, } from '../../config/config-manager.js';
@@ -36,11 +36,11 @@ import { isManagementRole } from '../../config/builtin-roles.js';
36
36
  import { SYSTEM_CONTROL_CHANNEL } from '../system-channels.js';
37
37
  import { logger } from '../../utils/logger.js';
38
38
  import { shouldSuppressRealRestart } from '../../utils/restart-safety.js';
39
- import { dispatchToMentionMode } from '../../config/mention-mode.js';
40
- import { menuFailure, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest } from './menu-protocol.js';
39
+ import { evolMenuResponseTransportMetadata, menuFailure, menuCommandForName, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest, } from './menu-protocol.js';
41
40
  import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
42
41
  import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
43
42
  import { groupMenuAuthorizationArgs, groupMenuIntent, handleGroupMenu, } from './group-menu.js';
43
+ import { getMenuCatalog, execMenuValueBatch, resolveExplicitMenuExecutionContext, validateMenuCatalogArgs, validateMenuValueBatchArgs } from './menu-catalog.js';
44
44
  /**
45
45
  * 获取 baseagent CLI 的版本号(claude/gemini/codex)。
46
46
  * 失败返回 null(命令不存在或执行失败)。
@@ -60,6 +60,7 @@ function getBaseagentVersion(cmd) {
60
60
  encoding: 'utf-8',
61
61
  timeout: 3000,
62
62
  stdio: ['ignore', 'pipe', 'pipe'],
63
+ windowsHide: process.platform === 'win32',
63
64
  });
64
65
  const versionOutput = String(output).trim();
65
66
  // claude: "2.1.187 (Claude Code)" → 提取 "2.1.187"
@@ -880,7 +881,7 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
880
881
  if (cmdBase === '/topic')
881
882
  return intent('session.topic.current', 'relation');
882
883
  if (cmdBase === '/baseagent')
883
- return intent('agent.baseagent.current', fromControlChannel ? 'agent' : 'relation');
884
+ return intent('agent.baseagent.current', fromControlChannel || args?.scope === 'agent' ? 'agent' : 'relation');
884
885
  if (cmdBase === '/file')
885
886
  return intent('file.fetch', 'filesystem', { filePath: args?.path });
886
887
  if (cmdBase === '/model')
@@ -938,6 +939,8 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
938
939
  return intent('permission.current', args?.scope === 'agent' ? 'agent' : 'relation');
939
940
  }
940
941
  if (verb === 'update') {
942
+ if (cmdBase === '/baseagent')
943
+ return intent('agent.baseagent.update', 'agent', { baseagent: value });
941
944
  if (cmdBase === '/model')
942
945
  return intent('model.use', modelScope, { model: value });
943
946
  if (cmdBase === '/effort')
@@ -946,6 +949,10 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
946
949
  return intent('chatmode.update', args?.scope === 'agent' ? 'agent' : 'relation', { value });
947
950
  if (cmdBase === '/mentionmode')
948
951
  return intent('mentionmode.update', args?.scope === 'agent' ? 'agent' : 'relation', { value });
952
+ if (cmdBase === '/activity')
953
+ return intent('activity.update', args?.scope === 'agent' ? 'agent' : 'relation', { value });
954
+ if (cmdBase === '/observable')
955
+ return intent('observable.update', 'agent', { value });
949
956
  if (cmdBase === '/gateway')
950
957
  return intent('gateway.write', 'process');
951
958
  if (cmdBase === '/config')
@@ -1089,6 +1096,9 @@ function buildRoleMenuContext(owner, channel, subject) {
1089
1096
  }
1090
1097
  const aunAvailable = (owningAgent?.channelInstanceNames?.() ?? [])
1091
1098
  .some((channelName) => owner.hasRegisteredChannel?.(channelName, 'aun') === true);
1099
+ const aunChannelName = (owningAgent?.channelInstanceNames?.() ?? [])
1100
+ .find((name) => String(name).split('#', 1)[0].toLowerCase() === 'aun');
1101
+ const contactAdapter = aunChannelName ? owner.getAdapter?.(aunChannelName) : undefined;
1092
1102
  return {
1093
1103
  self,
1094
1104
  actorRole: subject.role,
@@ -1097,6 +1107,26 @@ function buildRoleMenuContext(owner, channel, subject) {
1097
1107
  actorAid: subject.actorId,
1098
1108
  chatType: subject.chatType,
1099
1109
  aunAvailable,
1110
+ ...(contactAdapter?.send ? {
1111
+ notifyContactRequestReviewed: async ({ applicantAid, requestId, expiresAt, status }) => {
1112
+ await contactAdapter.send(buildEnvelope({
1113
+ taskId: `contact-request-${requestId}`,
1114
+ channel: contactAdapter.channelName,
1115
+ channelId: applicantAid,
1116
+ agentName: owningAgent?.name ?? '<unknown>',
1117
+ replyContext: { metadata: evolMenuResponseTransportMetadata() },
1118
+ }), {
1119
+ kind: 'custom',
1120
+ channelType: 'aun',
1121
+ payload: {
1122
+ type: 'contact.request.response',
1123
+ status,
1124
+ request_id: requestId,
1125
+ expires_at: expiresAt,
1126
+ },
1127
+ });
1128
+ },
1129
+ } : {}),
1100
1130
  availableBaseagents: Array.from(new Set([
1101
1131
  ...Object.keys(owningAgent?.config?.baseagents ?? {}),
1102
1132
  ...(owningAgent?.baseagent ? [owningAgent.baseagent] : []),
@@ -1285,6 +1315,7 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent', authSu
1285
1315
  fromControlChannel: isControlScope,
1286
1316
  }).allow;
1287
1317
  };
1318
+ const canReadModel = canOperation('model.list', 'relation') || canOperation('model.current', 'relation');
1288
1319
  const canUseModel = canOperation('model.use', 'relation');
1289
1320
  const canSetEffort = canOperation('model.effort', 'relation');
1290
1321
  const canSetChatmode = canOperation('chatmode.update', 'relation');
@@ -1293,7 +1324,12 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent', authSu
1293
1324
  const canUseFile = canOperation('file.fetch', 'filesystem') || canOperation('file.list', 'filesystem');
1294
1325
  const canManageProcess = authSubject ? authSubject.isDaemonOwner : isOwner;
1295
1326
  const configurableSettings = [
1296
- ...(canUseModel ? [{ cmd: '/model', label: '切换模型', desc: '切换当前 Agent 使用的模型版本', next: { type: 'select', dynamic: true } }] : []),
1327
+ ...(canReadModel ? [{
1328
+ cmd: '/model',
1329
+ label: canUseModel ? '切换模型' : '查看模型',
1330
+ desc: canUseModel ? '切换当前 Agent 使用的模型版本' : '查看当前 Agent 使用的模型版本',
1331
+ next: canUseModel ? { type: 'select', dynamic: true } : undefined,
1332
+ }] : []),
1297
1333
  ...(canSetEffort ? [{ cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', items: [
1298
1334
  { value: 'low', label: 'Low' },
1299
1335
  { value: 'medium', label: 'Medium' },
@@ -1729,7 +1765,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1729
1765
  });
1730
1766
  if ('error' in target)
1731
1767
  throw { code: target.code, message: target.error };
1732
- const fallback = dispatchToMentionMode(session?.metadata?.dispatchMode) ?? null;
1768
+ const fallback = session?.metadata?.mentionMode ?? null;
1733
1769
  const currentMode = readMenuMentionMode(target, fallback, fallback === null ? null : 'session').value;
1734
1770
  return [
1735
1771
  { value: 'mention-only', label: '@提及时响应', selected: currentMode === 'mention-only' },
@@ -1755,6 +1791,10 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1755
1791
  const cmdBase = cmd.trim().split(' ')[0];
1756
1792
  if (!cmdBase)
1757
1793
  return { error: '缺少命令', code: 'MISSING_CMD' };
1794
+ const isRequesterRoleQuery = cmdBase === '/role' && args?.view === 'requester';
1795
+ if (isRequesterRoleQuery && Object.keys(args).some(key => key !== 'view')) {
1796
+ return { error: 'role requester query only accepts args.view', code: 'INVALID_ARGUMENT' };
1797
+ }
1758
1798
  const gated = gateControlScope.call(this, { cmdBase, args, channel, fromControlChannel });
1759
1799
  if (gated)
1760
1800
  return gated;
@@ -1770,6 +1810,18 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1770
1810
  fromControlChannel,
1771
1811
  subject: authSubject,
1772
1812
  });
1813
+ if (isRequesterRoleQuery) {
1814
+ if (!subject.allowAccess) {
1815
+ return { error: `Role ${subject.role} is not allowed to access this agent`, code: 'ROLE_ACCESS_DENIED' };
1816
+ }
1817
+ return {
1818
+ data: {
1819
+ roleId: subject.role,
1820
+ source: subject.roleSource,
1821
+ allowAccess: subject.allowAccess,
1822
+ },
1823
+ };
1824
+ }
1773
1825
  if (cmdBase === '/role') {
1774
1826
  const authorized = await authorizeRoleMenu(this, {
1775
1827
  kind: 'query', args, identity, subject, session, channel, channelId,
@@ -2091,8 +2143,7 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2091
2143
  });
2092
2144
  if ('error' in target)
2093
2145
  return target;
2094
- // session metadata 是 AUN 协议词汇,翻译成 mentionMode 词汇;evolagent.config 已是 mentionMode 字段
2095
- const fallback = dispatchToMentionMode(session?.metadata?.dispatchMode) ?? null;
2146
+ const fallback = session?.metadata?.mentionMode ?? null;
2096
2147
  const current = readMenuMentionMode(target, fallback, fallback === null ? null : 'session');
2097
2148
  return {
2098
2149
  data: {
@@ -2605,8 +2656,6 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2605
2656
  const newMode = modeMap[arg];
2606
2657
  if (!newMode)
2607
2658
  return { error: `无效模式: ${arg},可选: all / text / none`, code: 'INVALID_VALUE' };
2608
- if (identity.role !== 'owner')
2609
- return { error: '中间输出模式切换仅限 owner', code: 'NO_PERMISSION' };
2610
2659
  const target = resolveMenuActivityTarget.call(this, {
2611
2660
  args,
2612
2661
  session,
@@ -3269,12 +3318,9 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3269
3318
  const controlDir = daemonControlDir();
3270
3319
  fs.mkdirSync(controlDir, { recursive: true });
3271
3320
  fs.writeFileSync(path.join(controlDir, 'restart-pending.json'), JSON.stringify(restartInfo));
3272
- const { spawn } = await import('child_process');
3273
- spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
3274
- detached: true,
3275
- stdio: 'ignore',
3276
- env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
3277
- }).unref();
3321
+ spawnDetachedNode([path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
3322
+ EVOLCORE_HOME: resolvePaths().root,
3323
+ });
3278
3324
  }
3279
3325
  else {
3280
3326
  logger.info('[System] Suppressed real menu restart in test runtime');
@@ -3310,7 +3356,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3310
3356
  data: {
3311
3357
  devMode,
3312
3358
  evolcore: { local: localEvolcore, remote: evolcoreRemote, hasUpdate: cmp(localEvolcore, evolcoreRemote) },
3313
- fastaun: { local: localFastaun, remote: fastaunRemote, hasUpdate: cmp(localFastaun, fastaunRemote) },
3359
+ fastaun: { local: localFastaun, remote: fastaunRemote, hasUpdate: cmp(localFastaun, fastaunRemote), managedBy: 'evolcore' },
3314
3360
  // ecweb 本地版本由 ECWeb 进程自身注入(data.ecwebVersion),此处仅给 remote
3315
3361
  ecweb: { remote: ecwebRemote },
3316
3362
  },
@@ -3491,16 +3537,6 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3491
3537
  }
3492
3538
  return { error: `不支持 action: ${cmdBase}`, code: 'NOT_SUPPORTED' };
3493
3539
  }
3494
- const SYSTEM_CONTROL_NAME_MAP = {
3495
- pwd: '/pwd', session: '/session', baseagent: '/baseagent', model: '/model',
3496
- topic: '/topic',
3497
- effort: '/effort', chatmode: '/chatmode', mentionmode: '/mentionmode',
3498
- permission: '/perm', activity: '/activity', system: '/system',
3499
- observable: '/observable',
3500
- agent: '/agent', trigger: '/trigger', file: '/file', gateway: '/gateway',
3501
- config: '/config', capability: '/capability', role: '/role',
3502
- group: '/group', connect: '/connect',
3503
- };
3504
3540
  function menuCommandToken(cmd) {
3505
3541
  if (typeof cmd !== 'string')
3506
3542
  return undefined;
@@ -3511,6 +3547,13 @@ function isProcessLevelMenu(name, cmd) {
3511
3547
  return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config'
3512
3548
  || cmdToken === '/system' || cmdToken === '/agent' || cmdToken === '/gateway' || cmdToken === '/config';
3513
3549
  }
3550
+ function isProcessContextMenu(payload) {
3551
+ const isCatalog = payload?.type === 'menu.list' && payload?.args?.view === 'catalog';
3552
+ const isValueBatch = payload?.type === 'menu.query'
3553
+ && payload?.name === 'menu'
3554
+ && payload?.args?.view === 'values';
3555
+ return (isCatalog || isValueBatch) && payload?.args?.context?.kind === 'process';
3556
+ }
3514
3557
  async function execMenuForSystemControl(payload, context) {
3515
3558
  const id = payload?.id ?? '';
3516
3559
  const name = payload?.name;
@@ -3531,13 +3574,15 @@ async function execMenuForSystemControl(payload, context) {
3531
3574
  : 'cli 不在控制 channel 范围';
3532
3575
  return { type: 'menu.response', id, name, error: { code: 'NOT_SUPPORTED', message } };
3533
3576
  }
3534
- const isProcessLevel = isProcessLevelMenu(name, payload?.cmd);
3577
+ const isProcessLevel = isProcessLevelMenu(name, payload?.cmd) || isProcessContextMenu(payload);
3578
+ const requestedContextKind = payload?.args?.context?.kind;
3535
3579
  const isRoleRequest = name === 'role' || compatCmd === '/role';
3536
3580
  const isConnectRequest = name === 'connect' || compatCmd === '/connect';
3537
3581
  const requiresAgentTarget = isRoleRequest || isConnectRequest || [
3538
3582
  'observable', 'baseagent', 'model', 'effort', 'trigger', 'capability', 'group',
3539
3583
  ].includes(name)
3540
- || ['/observable', '/baseagent', '/model', '/effort', '/trigger', '/capability', '/group'].includes(compatCmd ?? '');
3584
+ || ['/observable', '/baseagent', '/model', '/effort', '/trigger', '/capability', '/group'].includes(compatCmd ?? '')
3585
+ || (!isProcessLevel && requestedContextKind !== undefined && requestedContextKind !== 'process');
3541
3586
  const target = isProcessLevel
3542
3587
  ? { channel: SYSTEM_CONTROL_CHANNEL }
3543
3588
  : resolveExternalMenuAgentChannel(this, payload, SYSTEM_CONTROL_CHANNEL, requiresAgentTarget);
@@ -3558,21 +3603,71 @@ async function execMenuForSystemControl(payload, context) {
3558
3603
  fromControlChannel: true,
3559
3604
  });
3560
3605
  const trustedIdentity = trustedSubject.identity;
3561
- const cmd = name ? (SYSTEM_CONTROL_NAME_MAP[name] ?? payload.cmd) : payload.cmd;
3606
+ const cmd = name ? (menuCommandForName(name) ?? payload.cmd) : payload.cmd;
3562
3607
  try {
3563
3608
  switch (payload?.type) {
3564
3609
  case 'menu.list':
3610
+ if (payload?.args?.view === 'catalog') {
3611
+ validateMenuCatalogArgs(payload.args);
3612
+ const data = await getMenuCatalog({
3613
+ subject: trustedSubject,
3614
+ channelType: 'aun',
3615
+ channelScope: 'control',
3616
+ channelId: targetChannel,
3617
+ request: payload.args.context,
3618
+ });
3619
+ return menuSuccess({ id, ...(name ? { name } : {}) }, data);
3620
+ }
3621
+ if (payload?.args?.view !== undefined)
3622
+ return ecwebErr(id, name, 'NOT_SUPPORTED', `Unsupported menu.list view: ${String(payload.args.view)}`);
3565
3623
  return menuSuccess({ id }, this.getMenuItems(trustedIdentity.role, 'private', 'control', trustedSubject));
3566
3624
  case 'menu.query': {
3625
+ if (name === 'menu' && payload?.args?.view === 'values') {
3626
+ validateMenuValueBatchArgs(payload.args);
3627
+ const data = await execMenuValueBatch({
3628
+ subject: trustedSubject,
3629
+ channelType: 'aun',
3630
+ channelScope: 'control',
3631
+ channelId: targetChannel,
3632
+ request: payload.args.context,
3633
+ items: payload.args.items,
3634
+ schemaVersion: payload.args.schemaVersion,
3635
+ consistency: payload.args.consistency,
3636
+ execQuery: async (entryCmd, resolvedChannelId, queryArgs, catalogContext) => this.execMenuQuery(entryCmd, targetChannel, resolvedChannelId, catalogContext.subject.actorId, queryArgs, catalogContext.subject.chatType, true, catalogContext.subject.identity, catalogContext.subject, context.source),
3637
+ });
3638
+ return menuSuccess({ id, ...(name ? { name } : {}) }, data);
3639
+ }
3640
+ if (name === 'menu')
3641
+ return ecwebErr(id, name, 'NOT_SUPPORTED', `Unsupported menu.query view: ${String(payload?.args?.view)}`);
3567
3642
  if (!cmd)
3568
3643
  return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
3569
- const r = await this.execMenuQuery(cmd, targetChannel, targetChannel, context.actorAid, payload.args, undefined, true, trustedIdentity, trustedSubject, context.source);
3644
+ const hasExplicitContext = !!payload?.args && Object.prototype.hasOwnProperty.call(payload.args, 'context');
3645
+ const execution = await resolveExplicitMenuExecutionContext({
3646
+ subject: trustedSubject,
3647
+ channelType: 'aun',
3648
+ channelScope: 'control',
3649
+ channelId: targetChannel,
3650
+ args: payload.args,
3651
+ });
3652
+ const r = hasExplicitContext
3653
+ ? await this.execMenuQuery(cmd, targetChannel, execution.channelId, execution.subject.actorId, execution.args, execution.subject.chatType, true, execution.subject.identity, execution.subject, context.source)
3654
+ : await this.execMenuQuery(cmd, targetChannel, targetChannel, context.actorAid, payload.args, undefined, true, trustedIdentity, trustedSubject, context.source);
3570
3655
  return ecwebResp(id, name, r);
3571
3656
  }
3572
3657
  case 'menu.options': {
3573
3658
  if (!cmd)
3574
3659
  return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
3575
- const data = await this.getSubMenuItems(cmd, targetChannel, targetChannel, context.actorAid, payload.args, trustedIdentity, undefined, true, trustedSubject, context.source) ?? [];
3660
+ const hasExplicitContext = !!payload?.args && Object.prototype.hasOwnProperty.call(payload.args, 'context');
3661
+ const execution = await resolveExplicitMenuExecutionContext({
3662
+ subject: trustedSubject,
3663
+ channelType: 'aun',
3664
+ channelScope: 'control',
3665
+ channelId: targetChannel,
3666
+ args: payload.args,
3667
+ });
3668
+ const data = hasExplicitContext
3669
+ ? await this.getSubMenuItems(cmd, targetChannel, execution.channelId, execution.subject.actorId, execution.args, execution.subject.identity, execution.subject.chatType, true, execution.subject, context.source) ?? []
3670
+ : await this.getSubMenuItems(cmd, targetChannel, targetChannel, context.actorAid, payload.args, trustedIdentity, undefined, true, trustedSubject, context.source) ?? [];
3576
3671
  return menuSuccess({ id, ...(name ? { name } : {}) }, data);
3577
3672
  }
3578
3673
  case 'menu.update': {
@@ -3580,7 +3675,14 @@ async function execMenuForSystemControl(payload, context) {
3580
3675
  return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
3581
3676
  if (!payload.value)
3582
3677
  return ecwebErr(id, name, 'MISSING_VALUE', '缺少 value');
3583
- const r = await this.execMenuUpdate(cmd, payload.value, targetChannel, targetChannel, context.actorAid, trustedIdentity, true, payload.args, trustedSubject, context.source);
3678
+ const execution = await resolveExplicitMenuExecutionContext({
3679
+ subject: trustedSubject,
3680
+ channelType: 'aun',
3681
+ channelScope: 'control',
3682
+ channelId: targetChannel,
3683
+ args: payload.args,
3684
+ });
3685
+ const r = await this.execMenuUpdate(cmd, payload.value, targetChannel, execution.channelId, execution.subject.actorId, execution.subject.identity, true, execution.args, execution.subject, context.source);
3584
3686
  return ecwebResp(id, name, r);
3585
3687
  }
3586
3688
  case 'menu.action': {
@@ -3588,7 +3690,17 @@ async function execMenuForSystemControl(payload, context) {
3588
3690
  return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
3589
3691
  if (!payload.action)
3590
3692
  return ecwebErr(id, name, 'MISSING_VALUE', '缺少 action');
3591
- const r = await this.execMenuAction(cmd, payload.action, payload.args, targetChannel, targetChannel, context.actorAid, trustedIdentity, undefined, id, true, trustedSubject, context.source);
3693
+ const hasExplicitContext = !!payload?.args && Object.prototype.hasOwnProperty.call(payload.args, 'context');
3694
+ const execution = await resolveExplicitMenuExecutionContext({
3695
+ subject: trustedSubject,
3696
+ channelType: 'aun',
3697
+ channelScope: 'control',
3698
+ channelId: targetChannel,
3699
+ args: payload.args,
3700
+ });
3701
+ const r = hasExplicitContext
3702
+ ? await this.execMenuAction(cmd, payload.action, execution.args, targetChannel, execution.channelId, execution.subject.actorId, execution.subject.identity, execution.subject.chatType, id, true, execution.subject, context.source)
3703
+ : await this.execMenuAction(cmd, payload.action, payload.args, targetChannel, targetChannel, context.actorAid, trustedIdentity, undefined, id, true, trustedSubject, context.source);
3592
3704
  return ecwebResp(id, name, r);
3593
3705
  }
3594
3706
  default:
@@ -3606,7 +3718,7 @@ export async function execMenuForEcweb(payload, trusted) {
3606
3718
  const validationError = validateMenuRequest(payload ?? {});
3607
3719
  if (validationError)
3608
3720
  return menuFailure({ id, ...(name ? { name } : {}) }, validationError);
3609
- const isProcessLevel = isProcessLevelMenu(name, payload?.cmd);
3721
+ const isProcessLevel = isProcessLevelMenu(name, payload?.cmd) || isProcessContextMenu(payload);
3610
3722
  const owners = loadDaemonConfig().owners ?? [];
3611
3723
  if (isProcessLevel && owners.length === 0) {
3612
3724
  return { type: 'menu.response', id, name, error: { code: 'FORBIDDEN', message: '请在 daemon.json 配置 owners 后使用进程级操作' } };
@@ -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;
@@ -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');