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
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-f
14
14
  import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
15
15
  import { EcagentSessionFileAdapter } from './core/session/adapters/ecagent-session-file-adapter.js';
16
16
  import { loadDefaults, loadAllAgents, migrateIdentitiesIfNeeded, loadDaemonConfig, initializeEckSnapshotsConfig } from './config-store.js';
17
- import { ConfigTarget, initConfigManager, onConfigWrite } from './config/config-manager.js';
17
+ import { ConfigTarget, initConfigManager, onConfigWrite, read as readConfig } from './config/config-manager.js';
18
18
  import { ensureRoleConfigV4OnStartup } from './config/role-config-v4-startup.js';
19
19
  import { ensureRoleConfigV5OnStartup } from './config/role-config-v5-startup.js';
20
20
  import { ensureContactBookV2OnStartup } from './config/contact-book-v2-startup.js';
@@ -45,12 +45,12 @@ import { buildEnvelope } from './core/message/message-utils.js';
45
45
  import { ResponseEngine } from './core/message/response-engine.js';
46
46
  import { MessageQueue } from './core/message/message-queue.js';
47
47
  import { MessageBridge } from './core/message/message-bridge.js';
48
- import { MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, parseMenuControl, validateMenuRequest } from './core/command/menu-protocol.js';
49
- import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata } from './core/command/evol-menu-version-gate.js';
48
+ import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, parseMenuControl, validateMenuRequest } from './core/command/menu-protocol.js';
50
49
  import { readInstalledEvolcoreVersion } from './utils/evolcore-version.js';
51
50
  import { recoverAllRoleMutationsSync } from './core/command/role-menu.js';
52
51
  import { HandoffRuntime } from './core/handoff/runtime.js';
53
- import { BootstrapService } from './core/bootstrap-service.js';
52
+ import { BootstrapService, completeBootstrapWithWelcome } from './core/bootstrap-service.js';
53
+ import { postBootstrapWelcomeOperationId } from './core/bootstrap-messages.js';
54
54
  import { MessageCache } from './core/message/message-cache.js';
55
55
  import { CommandHandler, isProcessLevelOwner } from './core/command/command-handler.js';
56
56
  import { EventBus } from './core/event-bus.js';
@@ -62,10 +62,13 @@ import { InteractionRouter } from './core/interaction-router.js';
62
62
  import { registerAdapterInteractions } from './core/interaction-registration.js';
63
63
  import { AgentDelegationRegistry, authorizeDelegatedAunMsgSend } from './core/auth/agent-delegation.js';
64
64
  import { authorizeOperation, buildAuthSubject } from './core/auth/auth-gateway.js';
65
+ import { parsePeerKey } from './core/relation/peer-identity.js';
65
66
  import { isHClassPath } from './core/protected-paths.js';
66
67
  import { ChannelLoader, tryParseChannelKey } from './core/channel-loader.js';
67
68
  import { AgentLoader } from './core/baseagent-loader.js';
68
69
  import { EvolAgentRegistry } from './core/evolagent-registry.js';
70
+ import { createRunnerReloadTransaction, disposeAgentInstances } from './core/runner-reload-transaction.js';
71
+ import { AgentReloadBusyError, AgentReloadCoordinator } from './core/agent-reload-coordinator.js';
69
72
  import { buildReloadHooks } from './core/channel-loader.js';
70
73
  import { IpcServer } from './ipc.js';
71
74
  import { logger, setLogLevel } from './utils/logger.js';
@@ -88,6 +91,7 @@ import { validateModelSelectionForRole } from './core/model/model-permission.js'
88
91
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from './core/role/runtime-policy.js';
89
92
  import { atomicWriteJson } from './core/session/session-fs-store.js';
90
93
  import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from './core/message/message-log.js';
94
+ import { normalizeAunMentionEntries } from './aun/msg/mention-schema.js';
91
95
  import { MAIN_PACKAGE_NAME } from './product.js';
92
96
  import fs from 'fs';
93
97
  import crypto from 'crypto';
@@ -205,7 +209,7 @@ function originFromActorSession(session, authenticatedPeerId, authenticatedChann
205
209
  }
206
210
  function seedUpgradeCheckTrigger(manager, owner) {
207
211
  const now = Date.now();
208
- const scriptName = 'upgrade-check.sh';
212
+ const scriptName = 'upgrade-check.cjs';
209
213
  const definition = normalizeTriggerDefinition({
210
214
  $schema_version: 3.1,
211
215
  id: '__upgrade-check',
@@ -232,7 +236,7 @@ function seedUpgradeCheckTrigger(manager, owner) {
232
236
  type: 'script',
233
237
  script: {
234
238
  path: scriptName,
235
- runtime: 'bash',
239
+ runtime: 'node',
236
240
  timeoutMs: 60_000,
237
241
  },
238
242
  permissionMode: 'bypass',
@@ -258,67 +262,107 @@ function seedUpgradeCheckTrigger(manager, owner) {
258
262
  const cliPath = path.join(packageRoot, 'dist', 'cli', 'index.js');
259
263
  const packageJsonPath = path.join(packageRoot, 'package.json');
260
264
  const devMode = fs.existsSync(path.join(packageRoot, 'src', 'index.ts'));
261
- fs.writeFileSync(scriptPath, [
262
- '#!/usr/bin/env bash',
263
- 'set -euo pipefail',
264
- '',
265
- `CLI_PATH=${JSON.stringify(cliPath)}`,
266
- `PACKAGE_JSON=${JSON.stringify(packageJsonPath)}`,
267
- `DEV_MODE=${devMode ? '1' : '0'}`,
268
- '',
269
- 'json() {',
270
- ' local outcome="$1"',
271
- ' local text="$2"',
272
- ' node -e \'console.log(JSON.stringify({ outcome: process.argv[1], text: process.argv[2], files: [] }))\' "$outcome" "$text"',
273
- '}',
274
- '',
275
- 'if [ "$DEV_MODE" = "1" ]; then',
276
- ' json "noop" "upgrade check skipped in dev mode"',
277
- ' exit 0',
278
- 'fi',
279
- '',
280
- 'local_version="$(node -e \'try { console.log(require(process.argv[1]).version || "") } catch { process.exit(0) }\' "$PACKAGE_JSON")"',
281
- `remote_version="$(npm view ${MAIN_PACKAGE_NAME} version 2>/dev/null || true)"`,
282
- '',
283
- 'if [ -z "$remote_version" ]; then',
284
- ' json "error" "failed to check evolcore latest version"',
285
- ' exit 0',
286
- 'fi',
287
- '',
288
- 'if [ -z "$local_version" ]; then',
289
- ' json "error" "failed to read local ec version"',
290
- ' exit 0',
291
- 'fi',
292
- '',
293
- 'version_cmp="$(node -e \'',
294
- 'const [a, b] = process.argv.slice(1);',
295
- 'const pa = a.split("-")[0].split(".").map(Number);',
296
- 'const pb = b.split("-")[0].split(".").map(Number);',
297
- 'const n = Math.max(pa.length, pb.length);',
298
- 'let out = 0;',
299
- 'for (let i = 0; i < n; i++) {',
300
- ' const av = Number.isFinite(pa[i]) ? pa[i] : 0;',
301
- ' const bv = Number.isFinite(pb[i]) ? pb[i] : 0;',
302
- ' if (av < bv) { out = -1; break; }',
303
- ' if (av > bv) { out = 1; break; }',
304
- '}',
305
- 'console.log(out);',
306
- '\' "$local_version" "$remote_version")"',
307
- '',
308
- 'if [ "$version_cmp" != "-1" ]; then',
309
- ' json "noop" "evolcore is already up to date ($local_version; latest $remote_version)"',
310
- ' exit 0',
311
- 'fi',
312
- '',
313
- 'if [ ! -f "$CLI_PATH" ]; then',
314
- ' json "error" "restart-monitor entry not found: $CLI_PATH"',
315
- ' exit 0',
316
- 'fi',
317
- '',
318
- 'nohup node "$CLI_PATH" restart-monitor >/dev/null 2>&1 &',
319
- 'json "success" "evolcore upgrade available: ${local_version:-unknown} -> $remote_version; restart-monitor started"',
320
- '',
321
- ].join('\n'));
265
+ fs.writeFileSync(scriptPath, `#!/usr/bin/env node
266
+ const { existsSync, readFileSync } = require('node:fs');
267
+ const { spawn } = require('node:child_process');
268
+
269
+ const CLI_PATH = ${JSON.stringify(cliPath)};
270
+ const PACKAGE_JSON = ${JSON.stringify(packageJsonPath)};
271
+ const PACKAGE_NAME = ${JSON.stringify(MAIN_PACKAGE_NAME)};
272
+ const DEV_MODE = ${devMode ? 'true' : 'false'};
273
+
274
+ function emit(outcome, text) {
275
+ process.stdout.write(JSON.stringify({ outcome, text, files: [] }));
276
+ }
277
+
278
+ function compareVersions(a, b) {
279
+ const pa = a.split('-')[0].split('.').map(Number);
280
+ const pb = b.split('-')[0].split('.').map(Number);
281
+ const count = Math.max(pa.length, pb.length);
282
+ for (let i = 0; i < count; i += 1) {
283
+ const av = Number.isFinite(pa[i]) ? pa[i] : 0;
284
+ const bv = Number.isFinite(pb[i]) ? pb[i] : 0;
285
+ if (av < bv) return -1;
286
+ if (av > bv) return 1;
287
+ }
288
+ return 0;
289
+ }
290
+
291
+ async function latestVersion() {
292
+ const controller = new AbortController();
293
+ const timer = setTimeout(() => controller.abort(), 10_000);
294
+ try {
295
+ const response = await fetch('https://registry.npmjs.org/' + encodeURIComponent(PACKAGE_NAME) + '/latest', {
296
+ signal: controller.signal,
297
+ headers: { Accept: 'application/json' },
298
+ });
299
+ if (!response.ok) return null;
300
+ const data = await response.json();
301
+ return typeof data.version === 'string' ? data.version : null;
302
+ } finally {
303
+ clearTimeout(timer);
304
+ }
305
+ }
306
+
307
+ async function main() {
308
+ if (DEV_MODE) {
309
+ emit('noop', 'upgrade check skipped in dev mode');
310
+ return;
311
+ }
312
+
313
+ let localVersion;
314
+ try {
315
+ localVersion = JSON.parse(readFileSync(PACKAGE_JSON, 'utf8')).version;
316
+ } catch {
317
+ emit('error', 'failed to read local ec version');
318
+ return;
319
+ }
320
+ if (typeof localVersion !== 'string' || !localVersion) {
321
+ emit('error', 'failed to read local ec version');
322
+ return;
323
+ }
324
+
325
+ let remoteVersion;
326
+ try {
327
+ remoteVersion = await latestVersion();
328
+ } catch {
329
+ remoteVersion = null;
330
+ }
331
+ if (!remoteVersion) {
332
+ emit('error', 'failed to check evolcore latest version');
333
+ return;
334
+ }
335
+ if (compareVersions(localVersion, remoteVersion) >= 0) {
336
+ emit('noop', 'evolcore is already up to date (' + localVersion + '; latest ' + remoteVersion + ')');
337
+ return;
338
+ }
339
+ if (!existsSync(CLI_PATH)) {
340
+ emit('error', 'restart-monitor entry not found: ' + CLI_PATH);
341
+ return;
342
+ }
343
+
344
+ try {
345
+ const child = spawn(process.execPath, [CLI_PATH, 'restart-monitor'], {
346
+ detached: true,
347
+ stdio: 'ignore',
348
+ windowsHide: process.platform === 'win32',
349
+ env: { ...process.env },
350
+ });
351
+ child.unref();
352
+ emit('success', 'evolcore upgrade available: ' + localVersion + ' -> ' + remoteVersion + '; restart-monitor started');
353
+ } catch (error) {
354
+ emit('error', 'failed to start restart-monitor: ' + (error && error.message ? error.message : String(error)));
355
+ }
356
+ }
357
+
358
+ main().catch((error) => {
359
+ emit('error', 'upgrade check failed: ' + (error && error.message ? error.message : String(error)));
360
+ });
361
+ `);
362
+ // Remove the pre-Node script left by older installations so it cannot be
363
+ // selected by a stale trigger definition or run manually by accident.
364
+ fs.rmSync(path.join(dir, 'upgrade-check.sh'), { force: true });
365
+ fs.rmSync(path.join(dir, 'upgrade-check.js'), { force: true });
322
366
  fs.chmodSync(scriptPath, 0o755);
323
367
  atomicWriteJson(manager.definitionPath(definition.id), definition);
324
368
  fs.rmSync(manager.activePath(definition.id), { force: true });
@@ -336,7 +380,9 @@ function removeLegacyAgentUpgradeCheck(manager) {
336
380
  return;
337
381
  const isLegacySystemOrigin = existing.origin?.channelKey === '__system__' || existing.origin?.channelKey === 'daemon';
338
382
  const isUpgradeScript = existing.execution.type === 'script'
339
- && existing.execution.script?.path === 'upgrade-check.sh';
383
+ && (existing.execution.script?.path === 'upgrade-check.sh'
384
+ || existing.execution.script?.path === 'upgrade-check.js'
385
+ || existing.execution.script?.path === 'upgrade-check.cjs');
340
386
  if (!isLegacySystemOrigin || !isUpgradeScript)
341
387
  return;
342
388
  fs.rmSync(manager.triggerDir('__upgrade-check'), { recursive: true, force: true });
@@ -394,6 +440,13 @@ async function runBindBootstrapDaemon(daemonCfg) {
394
440
  await controlChannel.sendMessage(opts.channelId, JSON.stringify(response));
395
441
  }
396
442
  });
443
+ const controlToken = crypto.randomBytes(32).toString('base64url');
444
+ fs.mkdirSync(path.dirname(resolvePaths().controlToken), { recursive: true });
445
+ fs.writeFileSync(resolvePaths().controlToken, controlToken, { mode: 0o600 });
446
+ try {
447
+ fs.chmodSync(resolvePaths().controlToken, 0o600);
448
+ }
449
+ catch { }
397
450
  const ipcServer = new IpcServer(resolvePaths().socket, () => ({
398
451
  pid: process.pid,
399
452
  uptime: Math.round(process.uptime() * 1000),
@@ -415,6 +468,7 @@ async function runBindBootstrapDaemon(daemonCfg) {
415
468
  };
416
469
  })(),
417
470
  }));
471
+ ipcServer.setControlToken(controlToken);
418
472
  ipcServer.setBindExecutor({
419
473
  begin: (cmd) => bindService.begin(cmd),
420
474
  status: (taskId) => bindService.status(taskId),
@@ -974,6 +1028,7 @@ async function main() {
974
1028
  }
975
1029
  const channelInstances = evolagentInstances;
976
1030
  logger.info(`✓ Created ${channelInstances.length} channel instance(s)`);
1031
+ let bootstrapService;
977
1032
  const contactRequestExpiryTimer = setInterval(() => {
978
1033
  for (const agent of agentRegistry.runnableAgents()) {
979
1034
  void expirePendingContactRequests(agent.aid).catch(error => {
@@ -988,6 +1043,35 @@ async function main() {
988
1043
  getAvailableBaseagents: detectAvailableBaseagentsForBind,
989
1044
  getUptimeSeconds: () => Math.floor(process.uptime()),
990
1045
  onDaemonOwnersUpdated: (owners) => { processLevelOwners = owners; },
1046
+ onAgentOwnerUpdated: (agentAid, ownerAid) => {
1047
+ const service = bootstrapService;
1048
+ if (!service)
1049
+ return;
1050
+ const instance = channelInstances.find(candidate => {
1051
+ const owner = agentRegistry.resolveByChannel(candidate.adapter.channelKey)
1052
+ ?? agentRegistry.resolveByChannel(candidate.adapter.channelName);
1053
+ return owner?.aid === agentAid;
1054
+ });
1055
+ if (!instance)
1056
+ return;
1057
+ eventBus.publish({
1058
+ type: 'channel:owner-bound',
1059
+ channel: instance.channelType || instance.adapter.channelName,
1060
+ channelName: instance.adapter.channelName,
1061
+ userId: ownerAid,
1062
+ });
1063
+ void service.tryStartBootstrap({
1064
+ adapter: instance.adapter,
1065
+ channelKey: instance.adapter.channelKey,
1066
+ channelType: instance.channelType,
1067
+ agentAid,
1068
+ recipientId: ownerAid,
1069
+ channelId: instance.channelType === 'aun' ? ownerAid : undefined,
1070
+ source: 'owner-bound',
1071
+ }).catch((error) => {
1072
+ logger.warn(`[Bootstrap] Owner-bound start failed for ${agentAid}: ${error instanceof Error ? error.message : String(error)}`);
1073
+ });
1074
+ },
991
1075
  })
992
1076
  : null;
993
1077
  bindService?.startCleanup();
@@ -1097,6 +1181,7 @@ async function main() {
1097
1181
  return { ok: result.ok, message_id: result.message_id, error: result.error };
1098
1182
  }, undefined, { queueTtlMs: taskExecutionTtlMs });
1099
1183
  responseEngine.setHandoffRuntime(handoffRuntime);
1184
+ cmdHandler.setHandoffRuntime(handoffRuntime);
1100
1185
  // Trigger runtime: daemon-level script + feedback scheduler.
1101
1186
  const triggerSchedulers = new Map();
1102
1187
  const targetSessionLocks = new TargetSessionLockRegistry();
@@ -1541,7 +1626,7 @@ async function main() {
1541
1626
  msgBridge.setInteractionRouter(interactionRouter);
1542
1627
  msgBridge.setContactBindRuntimeChecker(isContactBindChannelReady);
1543
1628
  msgBridge.setAidStatsCollector(aidStatsCollector);
1544
- const bootstrapService = new BootstrapService(agentRegistry, eventBus);
1629
+ bootstrapService = new BootstrapService(agentRegistry, eventBus);
1545
1630
  msgBridge.setBootstrapService(bootstrapService);
1546
1631
  msgBridge.setHandoffRuntime(handoffRuntime);
1547
1632
  // ── Channel instance registration (shared by startup and hot-load) ──
@@ -1961,7 +2046,7 @@ async function main() {
1961
2046
  const delegation = agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1962
2047
  if (!delegation.ok)
1963
2048
  return { ok: false, code: delegation.code, error: delegation.reason };
1964
- return cmdHandler.handleCtl(cmd, sessionId);
2049
+ return cmdHandler.handleCtl(cmd, sessionId, delegation.grant);
1965
2050
  });
1966
2051
  // Register every IPC executor/provider before exposing the endpoint. The
1967
2052
  // function declaration is hoisted, while its invocation remains here so a
@@ -2048,10 +2133,11 @@ async function main() {
2048
2133
  }
2049
2134
  else {
2050
2135
  try {
2051
- const ecwebProc = spawn('node', [ecwebEntry], {
2136
+ const ecwebProc = spawn(process.execPath, [ecwebEntry], {
2052
2137
  cwd: ecwebPath,
2053
2138
  detached: true,
2054
2139
  stdio: 'ignore',
2140
+ windowsHide: platform.isWindows,
2055
2141
  env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
2056
2142
  });
2057
2143
  ecwebProc.unref();
@@ -2227,6 +2313,9 @@ async function main() {
2227
2313
  const recoveryAids = triggerStartupAgents.map(agent => agent.aid);
2228
2314
  for (const aid of recoveryAids) {
2229
2315
  try {
2316
+ const discarded = await handoffRuntime.discardExpired([aid]);
2317
+ if (discarded > 0)
2318
+ logger.info(`[Handoff] discarded ${discarded} expired handoff(s) for ${aid}`);
2230
2319
  const expired = handoffRuntime.expireQueued([aid]);
2231
2320
  if (expired > 0)
2232
2321
  logger.info(`[Handoff] expired ${expired} stale queued delivery item(s) for ${aid}`);
@@ -2312,6 +2401,60 @@ async function main() {
2312
2401
  function configureIpc() {
2313
2402
  // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
2314
2403
  ipcServer.setAgentRegistry(agentRegistry);
2404
+ ipcServer.setControlToken(triggerControlToken);
2405
+ ipcServer.setBootstrapCompleteExecutor(async (aid, auth) => {
2406
+ if (auth?.sessionId) {
2407
+ const authorization = await authorizeManagedAgentOperation({
2408
+ sessionId: auth.sessionId,
2409
+ delegationToken: auth.delegationToken,
2410
+ delegationCommandHash: auth.delegationCommandHash,
2411
+ targetAid: aid,
2412
+ }, 'agent.bootstrapComplete');
2413
+ if (!authorization.ok)
2414
+ return authorization;
2415
+ }
2416
+ else {
2417
+ const providedToken = typeof auth?.controlToken === 'string' ? Buffer.from(auth.controlToken) : undefined;
2418
+ const expectedToken = Buffer.from(triggerControlToken);
2419
+ if (!providedToken
2420
+ || providedToken.length !== expectedToken.length
2421
+ || !crypto.timingSafeEqual(providedToken, expectedToken)) {
2422
+ return { ok: false, code: 'INVALID_CONTROL_TOKEN', error: 'valid local control token is required' };
2423
+ }
2424
+ }
2425
+ const aunInstance = channelInstances.find(candidate => {
2426
+ if (candidate.channelType !== 'aun')
2427
+ return false;
2428
+ const owner = agentRegistry.resolveByChannel(candidate.adapter.channelKey)
2429
+ ?? agentRegistry.resolveByChannel(candidate.adapter.channelName);
2430
+ return owner?.aid === aid;
2431
+ });
2432
+ const welcome = aunInstance?.channel;
2433
+ const ownerAid = getFirstStaticAgentOwner(aid);
2434
+ const owningAgent = agentRegistry.get(aid);
2435
+ const sessions = aunInstance && ownerAid && owningAgent
2436
+ ? {
2437
+ ensurePostBootstrapMainSession: async () => {
2438
+ const channel = aunInstance.adapter.channelName;
2439
+ const active = await sessionManager.getActiveSession(channel, ownerAid);
2440
+ const ensured = await sessionManager.ensureNewMainSessionForOperation({
2441
+ channel,
2442
+ channelId: ownerAid,
2443
+ projectPath: active?.projectPath || owningAgent.projectPath || defaultProjectPath,
2444
+ baseagent: owningAgent.baseagent || active?.baseagent || primaryBaseagent,
2445
+ operationId: postBootstrapWelcomeOperationId(aid),
2446
+ identityMetadata: {
2447
+ ...(active?.metadata ?? {}),
2448
+ channelKey: aunInstance.adapter.channelKey,
2449
+ peerId: ownerAid,
2450
+ },
2451
+ });
2452
+ return { sessionId: ensured.session.id, created: ensured.created };
2453
+ },
2454
+ }
2455
+ : undefined;
2456
+ return completeBootstrapWithWelcome(bootstrapService, aid, welcome, sessions);
2457
+ });
2315
2458
  ipcServer.setDingtalkContactBindExecutor({
2316
2459
  register: (cmd) => registerPendingDingtalkContactBind(cmd),
2317
2460
  isChannelReady: isContactBindChannelReady,
@@ -2335,6 +2478,10 @@ async function main() {
2335
2478
  ipcServer.setMenuExecutor((payload, auth) => cmdHandler.execMenuForEcweb(payload, auth));
2336
2479
  ipcServer.setConfigOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleConfigOperation(argv, sessionId, delegationToken, delegationCommandHash));
2337
2480
  ipcServer.setContactOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleContactOperation(argv, sessionId, delegationToken, delegationCommandHash));
2481
+ ipcServer.setAidOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleAidOperation(argv, sessionId, delegationToken, delegationCommandHash));
2482
+ ipcServer.setGroupOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleGroupOperation(argv, sessionId, delegationToken, delegationCommandHash));
2483
+ ipcServer.setMsgOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleMsgOperation(argv, sessionId, delegationToken, delegationCommandHash));
2484
+ ipcServer.setFsOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleFsOperation(argv, sessionId, delegationToken, delegationCommandHash));
2338
2485
  cmdHandler.setDaemonStatusProvider(() => {
2339
2486
  const aidState = controlChannel?.getAidState?.();
2340
2487
  return {
@@ -2403,30 +2550,82 @@ async function main() {
2403
2550
  aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode, 'send');
2404
2551
  });
2405
2552
  ipcServer.setTaskRuntimeContextProvider(({ sessionId }) => responseEngine.getTaskRuntimeContext(sessionId));
2406
- ipcServer.setHandoffReturnExecutor((params) => responseEngine.returnHandoffResult(params));
2407
- ipcServer.setHandoffStatusExecutor(async (params) => {
2408
- if (!params.sessionId)
2409
- return { ok: false, code: 'HANDOFF_CALL_SESSION_REQUIRED', error: 'current session is required' };
2553
+ ipcServer.setCodexPreToolUseExecutor(async (params) => {
2554
+ for (const runner of new Set(agentMap.values())) {
2555
+ if (typeof runner?.evaluatePreToolUse !== 'function')
2556
+ continue;
2557
+ const result = await runner.evaluatePreToolUse(params.threadId, params.toolName, params.toolInput);
2558
+ if (result.applicable)
2559
+ return result;
2560
+ }
2561
+ return {
2562
+ ok: false,
2563
+ applicable: true,
2564
+ decision: 'deny',
2565
+ reason: 'No active EvolCore Codex runner accepted this managed hook request',
2566
+ };
2567
+ });
2568
+ const resolveManagedHandoffScope = async (params, operation) => {
2569
+ const delegation = agentDelegationRegistry.validate(params.delegationToken, params.sessionId, params.delegationCommandHash);
2570
+ if (!delegation.ok)
2571
+ return { ok: false, code: delegation.code, error: delegation.reason };
2410
2572
  const runtime = responseEngine.getTaskRuntimeContext(params.sessionId);
2411
2573
  const session = await sessionManager.getSessionById(params.sessionId);
2412
2574
  const selfAid = runtime?.selfAid || session?.selfAID;
2413
- if (!selfAid)
2414
- return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2415
- return handoffRuntime.status(selfAid, params.handoffId)
2416
- ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2417
- });
2418
- const resolveHandoffQueryAid = async (params) => {
2575
+ if (!selfAid || !session) {
2576
+ return { ok: false, code: 'HANDOFF_CALL_SESSION_INVALID', error: 'current session is invalid' };
2577
+ }
2578
+ if (delegation.grant.selfAid !== selfAid) {
2579
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'delegation self agent does not match current session' };
2580
+ }
2581
+ if (params.agent && params.agent !== selfAid) {
2582
+ return { ok: false, code: 'HANDOFF_AGENT_SCOPE_MISMATCH', error: 'agent does not match the current session' };
2583
+ }
2584
+ let conversationId;
2585
+ try {
2586
+ conversationId = parsePeerKey(delegation.grant.peerKey).channelId;
2587
+ }
2588
+ catch {
2589
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'delegation contains an invalid relation key' };
2590
+ }
2591
+ const subject = buildAuthSubject({
2592
+ selfAid,
2593
+ actorId: delegation.grant.actorId,
2594
+ channel: delegation.grant.channel,
2595
+ channelType: delegation.grant.channelType,
2596
+ channelId: conversationId,
2597
+ chatType: delegation.grant.chatType,
2598
+ conversationId,
2599
+ processOwners: daemonCfg.owners ?? [],
2600
+ });
2601
+ const managementRole = isManagementRole(subject.role);
2602
+ const decision = authorizeOperation({
2603
+ source: 'agent-tool',
2604
+ subject,
2605
+ intent: {
2606
+ operation,
2607
+ scope: managementRole ? 'agent' : 'relation',
2608
+ source: 'agent-tool',
2609
+ args: {
2610
+ self: selfAid,
2611
+ peer: conversationId,
2612
+ peerKey: delegation.grant.peerKey,
2613
+ },
2614
+ },
2615
+ auditMetadata: { taskId: delegation.grant.taskId, messageId: delegation.grant.messageId },
2616
+ });
2617
+ if (!decision.allow) {
2618
+ return {
2619
+ ok: false,
2620
+ code: decision.command?.code ?? 'ROLE_ACCESS_DENIED',
2621
+ error: decision.reason,
2622
+ };
2623
+ }
2624
+ return { ok: true, aid: selfAid, currentSessionId: params.sessionId, managementRole };
2625
+ };
2626
+ const resolveHandoffQueryScope = async (params, operation) => {
2419
2627
  if (params.sessionId) {
2420
- const runtime = responseEngine.getTaskRuntimeContext(params.sessionId);
2421
- const session = await sessionManager.getSessionById(params.sessionId);
2422
- const selfAid = runtime?.selfAid || session?.selfAID;
2423
- if (!selfAid) {
2424
- return { ok: false, code: 'HANDOFF_CALL_SESSION_INVALID', error: 'current session is invalid' };
2425
- }
2426
- if (params.agent && params.agent !== selfAid) {
2427
- return { ok: false, code: 'HANDOFF_AGENT_SCOPE_MISMATCH', error: 'agent does not match the current session' };
2428
- }
2429
- return { ok: true, aid: selfAid };
2628
+ return resolveManagedHandoffScope({ ...params, sessionId: params.sessionId }, operation);
2430
2629
  }
2431
2630
  if (!params.agent) {
2432
2631
  return { ok: false, code: 'HANDOFF_AGENT_REQUIRED', error: '--agent is required outside a task context' };
@@ -2434,23 +2633,56 @@ async function main() {
2434
2633
  if (!agentRegistry.get(params.agent)) {
2435
2634
  return { ok: false, code: 'HANDOFF_AGENT_NOT_FOUND', error: `agent not found: ${params.agent}` };
2436
2635
  }
2437
- return { ok: true, aid: params.agent };
2636
+ return { ok: true, aid: params.agent, currentSessionId: undefined, managementRole: true };
2438
2637
  };
2638
+ ipcServer.setHandoffReturnExecutor(async (params) => {
2639
+ if (!params.sessionId)
2640
+ return responseEngine.returnHandoffResult(params);
2641
+ const scope = await resolveManagedHandoffScope({ ...params, sessionId: params.sessionId }, 'ec.handoff.return');
2642
+ if (!scope.ok)
2643
+ return scope;
2644
+ return responseEngine.returnAuthorizedHandoffResult({ sessionId: params.sessionId, handoffId: params.handoffId, content: params.content }, scope.aid);
2645
+ });
2646
+ ipcServer.setHandoffStatusExecutor(async (params) => {
2647
+ const scope = await resolveHandoffQueryScope(params, 'ec.handoff.status');
2648
+ if (!scope.ok)
2649
+ return scope;
2650
+ const status = handoffRuntime.status(scope.aid, params.handoffId);
2651
+ if (!status)
2652
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2653
+ if (!scope.managementRole
2654
+ && status.origin_session_id !== scope.currentSessionId
2655
+ && status.target_session_id !== scope.currentSessionId) {
2656
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found in the current session' };
2657
+ }
2658
+ return status;
2659
+ });
2439
2660
  ipcServer.setHandoffListExecutor(async (params) => {
2440
- const scope = await resolveHandoffQueryAid(params);
2661
+ const scope = await resolveHandoffQueryScope(params, 'ec.handoff.list');
2441
2662
  if (!scope.ok)
2442
2663
  return scope;
2664
+ if (!scope.managementRole && params.filterSessionId && params.filterSessionId !== scope.currentSessionId) {
2665
+ return { ok: false, code: 'HANDOFF_SESSION_SCOPE_MISMATCH', error: 'only the current session can be queried' };
2666
+ }
2443
2667
  return handoffRuntime.listHandoffs({
2444
2668
  selfAid: scope.aid,
2445
2669
  state: params.state,
2446
- sessionId: params.filterSessionId,
2670
+ sessionId: scope.managementRole ? params.filterSessionId : scope.currentSessionId,
2447
2671
  limit: params.limit,
2448
2672
  });
2449
2673
  });
2450
2674
  ipcServer.setHandoffTraceExecutor(async (params) => {
2451
- const scope = await resolveHandoffQueryAid(params);
2675
+ const scope = await resolveHandoffQueryScope(params, 'ec.handoff.trace');
2452
2676
  if (!scope.ok)
2453
2677
  return scope;
2678
+ if (!scope.managementRole) {
2679
+ const status = handoffRuntime.status(scope.aid, params.handoffId);
2680
+ if (!status
2681
+ || (status.origin_session_id !== scope.currentSessionId
2682
+ && status.target_session_id !== scope.currentSessionId)) {
2683
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found in the current session' };
2684
+ }
2685
+ }
2454
2686
  return handoffRuntime.traceHandoff(scope.aid, params.handoffId, params.limit)
2455
2687
  ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2456
2688
  });
@@ -2522,12 +2754,27 @@ async function main() {
2522
2754
  if (!payload) {
2523
2755
  return { ok: false, error: 'message payload is required', code: 'INVALID_PAYLOAD' };
2524
2756
  }
2757
+ try {
2758
+ if (Object.prototype.hasOwnProperty.call(payload, 'mentions')) {
2759
+ payload = { ...payload, mentions: normalizeAunMentionEntries(payload.mentions) };
2760
+ }
2761
+ if (params.scope === 'group' && params.mentions !== undefined) {
2762
+ const normalizedMentions = normalizeAunMentionEntries(params.mentions);
2763
+ if (normalizedMentions.length > 0) {
2764
+ payload = { ...payload, mentions: normalizedMentions };
2765
+ }
2766
+ }
2767
+ }
2768
+ catch (error) {
2769
+ return {
2770
+ ok: false,
2771
+ error: error instanceof Error ? error.message : String(error),
2772
+ code: error?.code ?? 'INVALID_MENTION_SCHEMA',
2773
+ };
2774
+ }
2525
2775
  if (params.thread && typeof payload.thread_id !== 'string') {
2526
2776
  payload = { ...payload, thread_id: params.thread };
2527
2777
  }
2528
- if (params.scope === 'group' && params.mentions?.length) {
2529
- payload = { ...payload, mentions: params.mentions };
2530
- }
2531
2778
  let targetSessionId;
2532
2779
  if (params.originSessionId && params.originMessageId) {
2533
2780
  try {
@@ -2600,16 +2847,17 @@ async function main() {
2600
2847
  });
2601
2848
  });
2602
2849
  // ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
2850
+ const unregisterRuntimeChannel = (channelName) => {
2851
+ markChannelDisconnected(channelName);
2852
+ processor.unregisterChannel(channelName);
2853
+ cmdHandler.unregisterChannel(channelName);
2854
+ msgBridge.removeChannel(channelName);
2855
+ };
2603
2856
  const reloadHooks = buildReloadHooks({
2604
2857
  channelLoader,
2605
2858
  channelInstances,
2606
2859
  registerChannelInstance,
2607
- unregisterChannelInstance: (channelName) => {
2608
- markChannelDisconnected(channelName);
2609
- processor.unregisterChannel(channelName);
2610
- cmdHandler.unregisterChannel(channelName);
2611
- msgBridge.removeChannel(channelName);
2612
- },
2860
+ unregisterChannelInstance: unregisterRuntimeChannel,
2613
2861
  onChannelStarted: (inst) => {
2614
2862
  // startChannel 重建渠道时重新注入 AidStatsCollector(与 hot-load 路径对齐)
2615
2863
  if (inst.channelType === 'aun') {
@@ -2619,9 +2867,112 @@ async function main() {
2619
2867
  }
2620
2868
  },
2621
2869
  onChannelConnected: markChannelConnected,
2870
+ onAgentReloaded: async (agent) => {
2871
+ const instance = channelInstances.find(candidate => {
2872
+ const owner = agentRegistry.resolveByChannel(candidate.adapter.channelKey)
2873
+ ?? agentRegistry.resolveByChannel(candidate.adapter.channelName);
2874
+ return owner?.aid === agent.aid;
2875
+ });
2876
+ if (!instance)
2877
+ return;
2878
+ await bootstrapService.tryStartBootstrap({
2879
+ adapter: instance.adapter,
2880
+ channelKey: instance.adapter.channelKey,
2881
+ channelType: instance.channelType,
2882
+ agentAid: agent.aid,
2883
+ source: 'owner-bound',
2884
+ });
2885
+ },
2622
2886
  messageQueue,
2623
2887
  handoffRuntime,
2888
+ stageAgentRunners: async (candidate) => {
2889
+ const creationErrors = [];
2890
+ const staged = candidate.config.enabled === false
2891
+ ? []
2892
+ : agentLoader.createForAgent(candidate, {
2893
+ onSessionIdUpdate: async (sessionId, agentSessionId) => {
2894
+ await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
2895
+ },
2896
+ }, creationErrors);
2897
+ const onDisposeError = (instance, error) => {
2898
+ logger.warn(`[Reload] Failed to dispose runner ${instance.evolagentName}::${instance.baseagent}: ${error instanceof Error ? error.message : String(error)}`);
2899
+ };
2900
+ if (creationErrors.length > 0) {
2901
+ await disposeAgentInstances(staged, onDisposeError);
2902
+ throw new Error(creationErrors.map(error => `${error.baseagent}: ${error.message}`).join('; '));
2903
+ }
2904
+ if (candidate.config.enabled !== false && staged.length === 0 && candidate.config.active_baseagent) {
2905
+ throw new Error(`No runner could be created for ${candidate.aid}::${candidate.config.active_baseagent}`);
2906
+ }
2907
+ for (const instance of staged) {
2908
+ instance.agent.setPermissionGateway?.(permissionGateway);
2909
+ instance.agent.setCompactStartCallback?.((sessionId) => {
2910
+ processor.handleCompactStart(sessionId);
2911
+ });
2912
+ }
2913
+ return createRunnerReloadTransaction({
2914
+ aid: candidate.aid,
2915
+ agentMap,
2916
+ staged,
2917
+ onDisposeError,
2918
+ });
2919
+ },
2920
+ });
2921
+ const reloadOwnedMutes = new Set();
2922
+ const reloadCoordinator = new AgentReloadCoordinator({
2923
+ registry: agentRegistry,
2924
+ hooks: reloadHooks,
2925
+ getBusyCount: (aid) => {
2926
+ const agentName = agentRegistry.get(aid)?.name;
2927
+ if (!agentName)
2928
+ return 0;
2929
+ return messageQueue.getProcessingCountByAgent(agentName)
2930
+ + messageQueue.getQueueLengthByAgent(agentName);
2931
+ },
2932
+ beforeReload: invalidateKitCache,
2933
+ forceReload: async (aid) => {
2934
+ const agentName = agentRegistry.get(aid)?.name;
2935
+ if (!agentName)
2936
+ return;
2937
+ messageQueue.clearByAgent(agentName);
2938
+ await messageQueue.interruptByAgentAndWait(agentName);
2939
+ const deadline = Date.now() + 5_000;
2940
+ while (messageQueue.getProcessingCountByAgent(agentName) > 0) {
2941
+ if (Date.now() >= deadline) {
2942
+ throw new Error(`Timed out waiting for Agent "${aid}" tasks to stop`);
2943
+ }
2944
+ await new Promise(resolve => setTimeout(resolve, 25));
2945
+ }
2946
+ },
2947
+ acquireWorkGate: (aid) => {
2948
+ const agentName = agentRegistry.get(aid)?.name;
2949
+ if (!agentName)
2950
+ return;
2951
+ const inheritedReloadMute = reloadOwnedMutes.delete(agentName);
2952
+ const alreadyMuted = messageQueue.isAgentMuted(agentName);
2953
+ const ownsMute = inheritedReloadMute || !alreadyMuted;
2954
+ if (!alreadyMuted)
2955
+ messageQueue.muteAgent(agentName);
2956
+ return () => {
2957
+ const current = agentRegistry.get(aid);
2958
+ if (!current) {
2959
+ messageQueue.clearByAgent(agentName);
2960
+ if (ownsMute)
2961
+ messageQueue.unmuteAgent(agentName);
2962
+ return;
2963
+ }
2964
+ if (current.status === 'disabled') {
2965
+ messageQueue.clearByAgent(agentName);
2966
+ if (ownsMute)
2967
+ reloadOwnedMutes.add(agentName);
2968
+ return;
2969
+ }
2970
+ if (ownsMute)
2971
+ messageQueue.unmuteAgent(agentName);
2972
+ };
2973
+ },
2624
2974
  });
2975
+ ipcServer.setAgentReloadExecutor((aid, options) => reloadCoordinator.reload(aid, options));
2625
2976
  // Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
2626
2977
  globalThis.__evolcore_reloadHooks = reloadHooks;
2627
2978
  // Hot-load handler: dynamically add a new agent at runtime
@@ -2677,7 +3028,7 @@ async function main() {
2677
3028
  agent.status = 'running';
2678
3029
  // 连接
2679
3030
  await channelLoader.connectAll(instances, { onConnected: markChannelConnected });
2680
- await handoffRuntime.recover([aid]);
3031
+ await handoffRuntime.recover([aid], { discardExpired: false });
2681
3032
  await ensureTriggerSchedulerStarted(agent);
2682
3033
  handoffRuntime.resumeAgent(aid);
2683
3034
  resumed = true;
@@ -2691,90 +3042,121 @@ async function main() {
2691
3042
  }
2692
3043
  };
2693
3044
  // Full resync handler: scan disk, load new agents, unload removed/disabled, reload changed
3045
+ let resyncInFlight;
2694
3046
  globalThis.__evolcore_resyncAgents = async () => {
2695
- // 先清 kit 缓存:'kits' 组(manifest / fragment / schema / 角色模板)走 on-reload
2696
- // 策略,平时不查盘。放在扫盘与 reload 之前,本轮上下线的 agent 才能读到磁盘上的
2697
- // 最新版本——放在末尾的话,这一轮全用旧值,改动要等下一次 resync 才生效。
2698
- invalidateKitCache();
2699
- const { loadAllAgents: scanAgents, loadDefaults: readDefaults } = await import('./config-store.js');
2700
- const { resolveEffective } = await import('./config/config-manager.js');
2701
- const freshDefaults = readDefaults();
2702
- const { agents: diskAgents } = scanAgents();
2703
- const diskAidSet = new Set(diskAgents.map(a => a.aid));
2704
- const results = [];
2705
- // 1. 下线:运行时有但磁盘上没有 / disabled 的
2706
- for (const [aid, agent] of [...agentRegistry.agents.entries()]) {
2707
- const diskCfg = diskAgents.find(a => a.aid === aid);
2708
- if (!diskCfg || diskCfg.enabled === false) {
2709
- handoffRuntime.pauseAgent(aid);
2710
- try {
2711
- await handoffRuntime.drainAgent(aid);
2712
- }
2713
- catch (error) {
2714
- handoffRuntime.resumeAgent(aid);
2715
- results.push(`⚠ ${aid}: ${error instanceof Error ? error.message : String(error)}`);
2716
- continue;
2717
- }
2718
- await triggerSchedulers.get(aid)?.stop();
2719
- triggerSchedulers.delete(aid);
2720
- triggerSchedulerStarts.delete(aid);
2721
- // 断开所有 channels
2722
- for (const chName of agent.channelInstanceNames()) {
2723
- const inst = channelInstances.find(i => i.adapter.channelName === chName);
2724
- if (inst) {
2725
- try {
2726
- await inst.disconnect();
3047
+ if (resyncInFlight)
3048
+ return resyncInFlight;
3049
+ const operation = (async () => {
3050
+ // 先清 kit 缓存:'kits' 组(manifest / fragment / schema / 角色模板)走 on-reload
3051
+ // 策略,平时不查盘。放在扫盘与 reload 之前,本轮上下线的 agent 才能读到磁盘上的
3052
+ // 最新版本——放在末尾的话,这一轮全用旧值,改动要等下一次 resync 才生效。
3053
+ invalidateKitCache();
3054
+ const { loadAllAgents: scanAgents } = await import('./config-store.js');
3055
+ const { agents: diskAgents } = scanAgents();
3056
+ const results = [];
3057
+ // 1. 下线:运行时有但磁盘上没有 / disabled 的
3058
+ for (const [aid] of [...agentRegistry.agents.entries()]) {
3059
+ const diskCfg = diskAgents.find(a => a.aid === aid);
3060
+ if (!diskCfg || diskCfg.enabled === false) {
3061
+ try {
3062
+ const offlined = await reloadCoordinator.runExclusive(aid, async () => {
3063
+ const latestDiskCfg = readConfig(ConfigTarget.Agent, { self: aid }, { cache: false, expand: true });
3064
+ if (latestDiskCfg && latestDiskCfg.enabled !== false)
3065
+ return false;
3066
+ const runtimeAgent = agentRegistry.get(aid);
3067
+ if (!runtimeAgent)
3068
+ return false;
3069
+ let removed = false;
3070
+ handoffRuntime.pauseAgent(aid);
3071
+ try {
3072
+ await handoffRuntime.drainAgent(aid);
3073
+ await triggerSchedulers.get(aid)?.stop();
3074
+ triggerSchedulers.delete(aid);
3075
+ triggerSchedulerStarts.delete(aid);
3076
+ // 断开所有 channels
3077
+ for (const chName of runtimeAgent.channelInstanceNames()) {
3078
+ const inst = channelInstances.find(i => i.adapter.channelName === chName);
3079
+ if (inst) {
3080
+ try {
3081
+ await inst.disconnect();
3082
+ }
3083
+ catch { }
3084
+ const idx = channelInstances.indexOf(inst);
3085
+ if (idx >= 0)
3086
+ channelInstances.splice(idx, 1);
3087
+ }
3088
+ unregisterRuntimeChannel(chName);
3089
+ }
3090
+ agentRegistry.agents.delete(aid);
3091
+ messageQueue.clearByAgent(runtimeAgent.name);
3092
+ removed = true;
3093
+ return true;
3094
+ }
3095
+ finally {
3096
+ if (!removed)
3097
+ handoffRuntime.resumeAgent(aid);
3098
+ }
3099
+ });
3100
+ if (offlined)
3101
+ results.push(`- ${aid} (offline)`);
3102
+ }
3103
+ catch (error) {
3104
+ if (error instanceof AgentReloadBusyError) {
3105
+ results.push(`⏸ ${aid} (busy: ${error.busyCount} task(s); reload skipped)`);
3106
+ }
3107
+ else {
3108
+ results.push(`⚠ ${aid}: ${error instanceof Error ? error.message : String(error)}`);
2727
3109
  }
2728
- catch { }
2729
- markChannelDisconnected(inst.adapter.channelName);
2730
- const idx = channelInstances.indexOf(inst);
2731
- if (idx >= 0)
2732
- channelInstances.splice(idx, 1);
2733
3110
  }
3111
+ continue;
2734
3112
  }
2735
- agentRegistry.agents.delete(aid);
2736
- results.push(`- ${aid} (offline)`);
2737
- continue;
2738
3113
  }
2739
- }
2740
- // 2. 新增:磁盘上有但运行时没有的
2741
- for (const cfg of diskAgents) {
2742
- if (cfg.enabled === false)
2743
- continue;
2744
- if (agentRegistry.agents.has(cfg.aid))
2745
- continue;
2746
- try {
2747
- await globalThis.__evolcore_hotLoadAgent(cfg.aid);
2748
- results.push(`+ ${cfg.aid} (online)`);
3114
+ // 2. 新增:磁盘上有但运行时没有的
3115
+ for (const cfg of diskAgents) {
3116
+ if (cfg.enabled === false)
3117
+ continue;
3118
+ if (agentRegistry.agents.has(cfg.aid))
3119
+ continue;
3120
+ try {
3121
+ await globalThis.__evolcore_hotLoadAgent(cfg.aid);
3122
+ results.push(`+ ${cfg.aid} (online)`);
3123
+ }
3124
+ catch (e) {
3125
+ results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
3126
+ }
2749
3127
  }
2750
- catch (e) {
2751
- results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
3128
+ // 3. 已有的:重新 reload(config 可能改了)
3129
+ for (const cfg of diskAgents) {
3130
+ if (cfg.enabled === false)
3131
+ continue;
3132
+ if (!agentRegistry.agents.has(cfg.aid))
3133
+ continue;
3134
+ // 只有磁盘上存在且运行时也存在的才 reload
3135
+ try {
3136
+ await reloadCoordinator.reload(cfg.aid);
3137
+ const runtimeAgent = agentRegistry.get(cfg.aid);
3138
+ if (runtimeAgent)
3139
+ await startTriggerScheduler(runtimeAgent);
3140
+ results.push(`↻ ${cfg.aid} (reloaded)`);
3141
+ }
3142
+ catch (e) {
3143
+ results.push(`⚠ ${cfg.aid}: ${e?.message || e}`);
3144
+ }
2752
3145
  }
3146
+ // 重建 channel index(kit 缓存已在本轮开头清过)
3147
+ agentRegistry.channelIndex.clear();
3148
+ agentRegistry.buildChannelIndex();
3149
+ logger.info(`[Resync] Done: ${results.length} agent(s) processed`);
3150
+ return results;
3151
+ })();
3152
+ resyncInFlight = operation;
3153
+ try {
3154
+ return await operation;
2753
3155
  }
2754
- // 3. 已有的:重新 reload(config 可能改了)
2755
- const hooks = globalThis.__evolcore_reloadHooks;
2756
- for (const cfg of diskAgents) {
2757
- if (cfg.enabled === false)
2758
- continue;
2759
- if (!agentRegistry.agents.has(cfg.aid))
2760
- continue;
2761
- // 只有磁盘上存在且运行时也存在的才 reload
2762
- try {
2763
- await agentRegistry.reload(cfg.aid, hooks);
2764
- const runtimeAgent = agentRegistry.get(cfg.aid);
2765
- if (runtimeAgent)
2766
- await startTriggerScheduler(runtimeAgent);
2767
- results.push(`↻ ${cfg.aid} (reloaded)`);
2768
- }
2769
- catch (e) {
2770
- results.push(`⚠ ${cfg.aid}: ${e?.message || e}`);
2771
- }
3156
+ finally {
3157
+ if (resyncInFlight === operation)
3158
+ resyncInFlight = undefined;
2772
3159
  }
2773
- // 重建 channel index(kit 缓存已在本轮开头清过)
2774
- agentRegistry.channelIndex.clear();
2775
- agentRegistry.buildChannelIndex();
2776
- logger.info(`[Resync] Done: ${results.length} agent(s) processed`);
2777
- return results;
2778
3160
  };
2779
3161
  ipcServer.setStatsProvider(() => statsCollector.getSnapshot());
2780
3162
  ipcServer.setAgentStatsProvider(() => agentRegistry.list().map((agent) => {
@@ -2825,6 +3207,88 @@ async function main() {
2825
3207
  return { ok: false, error: `unknown action: ${params.action}` };
2826
3208
  }
2827
3209
  });
3210
+ async function authorizeManagedAgentOperation(params, operation) {
3211
+ const delegation = agentDelegationRegistry.validate(params.delegationToken, params.sessionId, params.delegationCommandHash);
3212
+ if (!delegation.ok)
3213
+ return { ok: false, code: delegation.code, error: delegation.reason };
3214
+ const session = await sessionManager.getSessionById(params.sessionId);
3215
+ if (!session || session.selfAID !== delegation.grant.selfAid) {
3216
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'delegation does not match the current session' };
3217
+ }
3218
+ if (params.targetAid && params.targetAid !== delegation.grant.selfAid) {
3219
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'only the current Agent can be targeted' };
3220
+ }
3221
+ let conversationId;
3222
+ try {
3223
+ conversationId = parsePeerKey(delegation.grant.peerKey).channelId;
3224
+ }
3225
+ catch {
3226
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'delegation contains an invalid relation key' };
3227
+ }
3228
+ const subject = buildAuthSubject({
3229
+ selfAid: delegation.grant.selfAid,
3230
+ actorId: delegation.grant.actorId,
3231
+ channel: delegation.grant.channel,
3232
+ channelType: delegation.grant.channelType,
3233
+ channelId: conversationId,
3234
+ chatType: delegation.grant.chatType,
3235
+ conversationId,
3236
+ processOwners: daemonCfg.owners ?? [],
3237
+ });
3238
+ if (!isManagementRole(subject.role)) {
3239
+ return { ok: false, code: 'ROLE_ACCESS_DENIED', error: `${operation} requires Agent admin permission` };
3240
+ }
3241
+ const decision = authorizeOperation({
3242
+ source: 'agent-tool',
3243
+ subject,
3244
+ intent: {
3245
+ operation,
3246
+ scope: operation === 'system.status' ? 'process' : 'agent',
3247
+ source: 'agent-tool',
3248
+ args: { self: delegation.grant.selfAid, aid: params.targetAid ?? delegation.grant.selfAid },
3249
+ },
3250
+ auditMetadata: { taskId: delegation.grant.taskId, messageId: delegation.grant.messageId },
3251
+ });
3252
+ return decision.allow
3253
+ ? { ok: true, aid: delegation.grant.selfAid }
3254
+ : { ok: false, code: decision.code, error: decision.reason };
3255
+ }
3256
+ ipcServer.setQueueOperationExecutor(async (params) => {
3257
+ const operation = `ec.queue.${params.action ?? 'read'}`;
3258
+ const authorization = await authorizeManagedAgentOperation(params, operation);
3259
+ if (!authorization.ok)
3260
+ return authorization;
3261
+ const handle = agentRegistry.get(authorization.aid);
3262
+ if (!handle?.name)
3263
+ return { ok: false, code: 'AGENT_NOT_FOUND', error: 'current Agent not found' };
3264
+ if (!params.action)
3265
+ return { ok: true, items: messageQueue.getQueueItemsByAgent(handle.name) };
3266
+ if (params.action === 'clear')
3267
+ return { ok: true, cleared: messageQueue.clearByAgent(handle.name) };
3268
+ if (params.action === 'cancel') {
3269
+ if (!params.messageId)
3270
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'missing messageId' };
3271
+ return { ok: true, cancelled: messageQueue.cancelMessageById(handle.name, params.messageId) };
3272
+ }
3273
+ if (!params.sessionKey)
3274
+ return { ok: false, code: 'INVALID_ARGUMENT', error: 'missing sessionKey' };
3275
+ const targetSessionId = messageQueue.findSessionIdBySessionKey(params.sessionKey);
3276
+ if (!targetSessionId)
3277
+ return { ok: false, code: 'SESSION_NOT_FOUND', error: `session not found: ${params.sessionKey}` };
3278
+ const targetSession = await sessionManager.getSessionById(targetSessionId);
3279
+ if (!targetSession || targetSession.selfAID !== authorization.aid) {
3280
+ return { ok: false, code: 'SCOPE_MISMATCH', error: 'session does not belong to the current Agent' };
3281
+ }
3282
+ return { ok: true, interrupted: await messageQueue.interruptBySession(targetSessionId) };
3283
+ });
3284
+ ipcServer.setStatusAuthorizationExecutor(async (params) => {
3285
+ const authorization = await authorizeManagedAgentOperation(params, 'system.status');
3286
+ return authorization.ok ? { ok: true } : authorization;
3287
+ });
3288
+ ipcServer.setStorageAuthorizationExecutor(async (params) => {
3289
+ const authorization = await authorizeManagedAgentOperation({ ...params, targetAid: params.aid }, params.operation);
3290
+ return authorization.ok ? { ok: true } : authorization;
3291
+ });
2828
3292
  ipcServer.setTriggerExecutor(async (cmd) => {
2829
3293
  const schedulerFor = (agentAid) => {
2830
3294
  const scheduler = triggerSchedulers.get(agentAid);