evolcore 0.0.10 → 0.0.11

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 (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +3 -3
  3. package/dist/agents/baseagent.js +4 -0
  4. package/dist/agents/claude-runner.js +123 -42
  5. package/dist/agents/codex-app-server-client.js +33 -9
  6. package/dist/agents/codex-runner.js +58 -8
  7. package/dist/agents/ecagent-runner.js +17 -2
  8. package/dist/agents/request-identity.js +55 -0
  9. package/dist/aun/outbox.js +28 -31
  10. package/dist/channels/aun.js +131 -128
  11. package/dist/cli/agent-command.js +16 -9
  12. package/dist/cli/agent.js +82 -19
  13. package/dist/cli/daemon-commands.js +21 -2
  14. package/dist/cli/index.js +76 -61
  15. package/dist/cli/init-cancel.js +208 -0
  16. package/dist/cli/init-channel.js +343 -195
  17. package/dist/cli/init.js +21 -9
  18. package/dist/config/builtin-roles.js +1 -0
  19. package/dist/config/gateway-config.js +26 -10
  20. package/dist/core/agent-reload-coordinator.js +53 -0
  21. package/dist/core/auth/operation-authorizer.js +32 -147
  22. package/dist/core/auth/operation-catalog.js +80 -0
  23. package/dist/core/bootstrap-messages.js +50 -0
  24. package/dist/core/bootstrap-service.js +85 -10
  25. package/dist/core/channel-loader.js +23 -6
  26. package/dist/core/command/agent-control.js +14 -11
  27. package/dist/core/command/menu-handler.js +67 -76
  28. package/dist/core/command/slash-handler.js +4 -4
  29. package/dist/core/evolagent-registry.js +125 -35
  30. package/dist/core/evolagent.js +8 -3
  31. package/dist/core/inference/text-inference.js +38 -4
  32. package/dist/core/message/message-bridge.js +1 -1
  33. package/dist/core/message/message-log.js +22 -0
  34. package/dist/core/message/message-queue.js +19 -4
  35. package/dist/core/model/model-catalog.js +143 -24
  36. package/dist/core/model/model-diagnostics.js +28 -10
  37. package/dist/core/permission/index.js +1 -0
  38. package/dist/core/permission/readonly-shell-query.js +532 -0
  39. package/dist/core/permission/shell-environment.js +46 -0
  40. package/dist/core/permission/tool-policy.js +231 -93
  41. package/dist/core/protected-paths.js +10 -7
  42. package/dist/core/runner-reload-transaction.js +57 -0
  43. package/dist/index.js +262 -84
  44. package/dist/ipc.js +29 -11
  45. package/dist/utils/aid-bind.js +3 -8
  46. package/dist/utils/log-writer.js +6 -10
  47. package/dist/utils/logger.js +5 -5
  48. package/kits/docs/evolcore/msg.md +13 -0
  49. package/kits/rules/01-overview.md +9 -0
  50. package/kits/schemas/agent-config.schema.3.json +1 -1
  51. package/kits/schemas/agent-config.schema.4.json +1 -1
  52. package/kits/schemas/relation-config.schema.2.json +1 -1
  53. package/kits/schemas/role-config.schema.1.json +1 -1
  54. package/kits/templates/roles/admin.json +5 -0
  55. package/kits/templates/roles/member.json +17 -0
  56. package/kits/templates/roles/visitor.json +8 -0
  57. package/kits/templates/system-fragments/bootstrap.md +12 -6
  58. package/kits/templates/system-fragments/channel.md +6 -0
  59. package/kits/templates/system-fragments/session.md +2 -0
  60. package/package.json +2 -1
  61. package/skills/eclink/SKILL.md +15 -3
  62. package/skills/eclink/agents/openai.yaml +3 -3
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';
@@ -50,7 +50,7 @@ import { evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata } from '
50
50
  import { readInstalledEvolcoreVersion } from './utils/evolcore-version.js';
51
51
  import { recoverAllRoleMutationsSync } from './core/command/role-menu.js';
52
52
  import { HandoffRuntime } from './core/handoff/runtime.js';
53
- import { BootstrapService } from './core/bootstrap-service.js';
53
+ import { BootstrapService, completeBootstrapWithWelcome } from './core/bootstrap-service.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';
@@ -66,6 +66,8 @@ import { isHClassPath } from './core/protected-paths.js';
66
66
  import { ChannelLoader, tryParseChannelKey } from './core/channel-loader.js';
67
67
  import { AgentLoader } from './core/baseagent-loader.js';
68
68
  import { EvolAgentRegistry } from './core/evolagent-registry.js';
69
+ import { createRunnerReloadTransaction, disposeAgentInstances } from './core/runner-reload-transaction.js';
70
+ import { AgentReloadBusyError, AgentReloadCoordinator } from './core/agent-reload-coordinator.js';
69
71
  import { buildReloadHooks } from './core/channel-loader.js';
70
72
  import { IpcServer } from './ipc.js';
71
73
  import { logger, setLogLevel } from './utils/logger.js';
@@ -974,6 +976,7 @@ async function main() {
974
976
  }
975
977
  const channelInstances = evolagentInstances;
976
978
  logger.info(`✓ Created ${channelInstances.length} channel instance(s)`);
979
+ let bootstrapService;
977
980
  const contactRequestExpiryTimer = setInterval(() => {
978
981
  for (const agent of agentRegistry.runnableAgents()) {
979
982
  void expirePendingContactRequests(agent.aid).catch(error => {
@@ -988,6 +991,35 @@ async function main() {
988
991
  getAvailableBaseagents: detectAvailableBaseagentsForBind,
989
992
  getUptimeSeconds: () => Math.floor(process.uptime()),
990
993
  onDaemonOwnersUpdated: (owners) => { processLevelOwners = owners; },
994
+ onAgentOwnerUpdated: (agentAid, ownerAid) => {
995
+ const service = bootstrapService;
996
+ if (!service)
997
+ return;
998
+ const instance = channelInstances.find(candidate => {
999
+ const owner = agentRegistry.resolveByChannel(candidate.adapter.channelKey)
1000
+ ?? agentRegistry.resolveByChannel(candidate.adapter.channelName);
1001
+ return owner?.aid === agentAid;
1002
+ });
1003
+ if (!instance)
1004
+ return;
1005
+ eventBus.publish({
1006
+ type: 'channel:owner-bound',
1007
+ channel: instance.channelType || instance.adapter.channelName,
1008
+ channelName: instance.adapter.channelName,
1009
+ userId: ownerAid,
1010
+ });
1011
+ void service.tryStartBootstrap({
1012
+ adapter: instance.adapter,
1013
+ channelKey: instance.adapter.channelKey,
1014
+ channelType: instance.channelType,
1015
+ agentAid,
1016
+ recipientId: ownerAid,
1017
+ channelId: instance.channelType === 'aun' ? ownerAid : undefined,
1018
+ source: 'owner-bound',
1019
+ }).catch((error) => {
1020
+ logger.warn(`[Bootstrap] Owner-bound start failed for ${agentAid}: ${error instanceof Error ? error.message : String(error)}`);
1021
+ });
1022
+ },
991
1023
  })
992
1024
  : null;
993
1025
  bindService?.startCleanup();
@@ -1541,7 +1573,7 @@ async function main() {
1541
1573
  msgBridge.setInteractionRouter(interactionRouter);
1542
1574
  msgBridge.setContactBindRuntimeChecker(isContactBindChannelReady);
1543
1575
  msgBridge.setAidStatsCollector(aidStatsCollector);
1544
- const bootstrapService = new BootstrapService(agentRegistry, eventBus);
1576
+ bootstrapService = new BootstrapService(agentRegistry, eventBus);
1545
1577
  msgBridge.setBootstrapService(bootstrapService);
1546
1578
  msgBridge.setHandoffRuntime(handoffRuntime);
1547
1579
  // ── Channel instance registration (shared by startup and hot-load) ──
@@ -2312,6 +2344,17 @@ async function main() {
2312
2344
  function configureIpc() {
2313
2345
  // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
2314
2346
  ipcServer.setAgentRegistry(agentRegistry);
2347
+ ipcServer.setBootstrapCompleteExecutor(async (aid) => {
2348
+ const aunInstance = channelInstances.find(candidate => {
2349
+ if (candidate.channelType !== 'aun')
2350
+ return false;
2351
+ const owner = agentRegistry.resolveByChannel(candidate.adapter.channelKey)
2352
+ ?? agentRegistry.resolveByChannel(candidate.adapter.channelName);
2353
+ return owner?.aid === aid;
2354
+ });
2355
+ const welcome = aunInstance?.channel;
2356
+ return completeBootstrapWithWelcome(bootstrapService, aid, welcome);
2357
+ });
2315
2358
  ipcServer.setDingtalkContactBindExecutor({
2316
2359
  register: (cmd) => registerPendingDingtalkContactBind(cmd),
2317
2360
  isChannelReady: isContactBindChannelReady,
@@ -2600,16 +2643,17 @@ async function main() {
2600
2643
  });
2601
2644
  });
2602
2645
  // ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
2646
+ const unregisterRuntimeChannel = (channelName) => {
2647
+ markChannelDisconnected(channelName);
2648
+ processor.unregisterChannel(channelName);
2649
+ cmdHandler.unregisterChannel(channelName);
2650
+ msgBridge.removeChannel(channelName);
2651
+ };
2603
2652
  const reloadHooks = buildReloadHooks({
2604
2653
  channelLoader,
2605
2654
  channelInstances,
2606
2655
  registerChannelInstance,
2607
- unregisterChannelInstance: (channelName) => {
2608
- markChannelDisconnected(channelName);
2609
- processor.unregisterChannel(channelName);
2610
- cmdHandler.unregisterChannel(channelName);
2611
- msgBridge.removeChannel(channelName);
2612
- },
2656
+ unregisterChannelInstance: unregisterRuntimeChannel,
2613
2657
  onChannelStarted: (inst) => {
2614
2658
  // startChannel 重建渠道时重新注入 AidStatsCollector(与 hot-load 路径对齐)
2615
2659
  if (inst.channelType === 'aun') {
@@ -2619,9 +2663,112 @@ async function main() {
2619
2663
  }
2620
2664
  },
2621
2665
  onChannelConnected: markChannelConnected,
2666
+ onAgentReloaded: async (agent) => {
2667
+ const instance = channelInstances.find(candidate => {
2668
+ const owner = agentRegistry.resolveByChannel(candidate.adapter.channelKey)
2669
+ ?? agentRegistry.resolveByChannel(candidate.adapter.channelName);
2670
+ return owner?.aid === agent.aid;
2671
+ });
2672
+ if (!instance)
2673
+ return;
2674
+ await bootstrapService.tryStartBootstrap({
2675
+ adapter: instance.adapter,
2676
+ channelKey: instance.adapter.channelKey,
2677
+ channelType: instance.channelType,
2678
+ agentAid: agent.aid,
2679
+ source: 'owner-bound',
2680
+ });
2681
+ },
2622
2682
  messageQueue,
2623
2683
  handoffRuntime,
2684
+ stageAgentRunners: async (candidate) => {
2685
+ const creationErrors = [];
2686
+ const staged = candidate.config.enabled === false
2687
+ ? []
2688
+ : agentLoader.createForAgent(candidate, {
2689
+ onSessionIdUpdate: async (sessionId, agentSessionId) => {
2690
+ await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
2691
+ },
2692
+ }, creationErrors);
2693
+ const onDisposeError = (instance, error) => {
2694
+ logger.warn(`[Reload] Failed to dispose runner ${instance.evolagentName}::${instance.baseagent}: ${error instanceof Error ? error.message : String(error)}`);
2695
+ };
2696
+ if (creationErrors.length > 0) {
2697
+ await disposeAgentInstances(staged, onDisposeError);
2698
+ throw new Error(creationErrors.map(error => `${error.baseagent}: ${error.message}`).join('; '));
2699
+ }
2700
+ if (candidate.config.enabled !== false && staged.length === 0 && candidate.config.active_baseagent) {
2701
+ throw new Error(`No runner could be created for ${candidate.aid}::${candidate.config.active_baseagent}`);
2702
+ }
2703
+ for (const instance of staged) {
2704
+ instance.agent.setPermissionGateway?.(permissionGateway);
2705
+ instance.agent.setCompactStartCallback?.((sessionId) => {
2706
+ processor.handleCompactStart(sessionId);
2707
+ });
2708
+ }
2709
+ return createRunnerReloadTransaction({
2710
+ aid: candidate.aid,
2711
+ agentMap,
2712
+ staged,
2713
+ onDisposeError,
2714
+ });
2715
+ },
2624
2716
  });
2717
+ const reloadOwnedMutes = new Set();
2718
+ const reloadCoordinator = new AgentReloadCoordinator({
2719
+ registry: agentRegistry,
2720
+ hooks: reloadHooks,
2721
+ getBusyCount: (aid) => {
2722
+ const agentName = agentRegistry.get(aid)?.name;
2723
+ if (!agentName)
2724
+ return 0;
2725
+ return messageQueue.getProcessingCountByAgent(agentName)
2726
+ + messageQueue.getQueueLengthByAgent(agentName);
2727
+ },
2728
+ beforeReload: invalidateKitCache,
2729
+ forceReload: async (aid) => {
2730
+ const agentName = agentRegistry.get(aid)?.name;
2731
+ if (!agentName)
2732
+ return;
2733
+ messageQueue.clearByAgent(agentName);
2734
+ await messageQueue.interruptByAgentAndWait(agentName);
2735
+ const deadline = Date.now() + 5_000;
2736
+ while (messageQueue.getProcessingCountByAgent(agentName) > 0) {
2737
+ if (Date.now() >= deadline) {
2738
+ throw new Error(`Timed out waiting for Agent "${aid}" tasks to stop`);
2739
+ }
2740
+ await new Promise(resolve => setTimeout(resolve, 25));
2741
+ }
2742
+ },
2743
+ acquireWorkGate: (aid) => {
2744
+ const agentName = agentRegistry.get(aid)?.name;
2745
+ if (!agentName)
2746
+ return;
2747
+ const inheritedReloadMute = reloadOwnedMutes.delete(agentName);
2748
+ const alreadyMuted = messageQueue.isAgentMuted(agentName);
2749
+ const ownsMute = inheritedReloadMute || !alreadyMuted;
2750
+ if (!alreadyMuted)
2751
+ messageQueue.muteAgent(agentName);
2752
+ return () => {
2753
+ const current = agentRegistry.get(aid);
2754
+ if (!current) {
2755
+ messageQueue.clearByAgent(agentName);
2756
+ if (ownsMute)
2757
+ messageQueue.unmuteAgent(agentName);
2758
+ return;
2759
+ }
2760
+ if (current.status === 'disabled') {
2761
+ messageQueue.clearByAgent(agentName);
2762
+ if (ownsMute)
2763
+ reloadOwnedMutes.add(agentName);
2764
+ return;
2765
+ }
2766
+ if (ownsMute)
2767
+ messageQueue.unmuteAgent(agentName);
2768
+ };
2769
+ },
2770
+ });
2771
+ ipcServer.setAgentReloadExecutor((aid, options) => reloadCoordinator.reload(aid, options));
2625
2772
  // Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
2626
2773
  globalThis.__evolcore_reloadHooks = reloadHooks;
2627
2774
  // Hot-load handler: dynamically add a new agent at runtime
@@ -2691,90 +2838,121 @@ async function main() {
2691
2838
  }
2692
2839
  };
2693
2840
  // Full resync handler: scan disk, load new agents, unload removed/disabled, reload changed
2841
+ let resyncInFlight;
2694
2842
  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();
2843
+ if (resyncInFlight)
2844
+ return resyncInFlight;
2845
+ const operation = (async () => {
2846
+ // 先清 kit 缓存:'kits' 组(manifest / fragment / schema / 角色模板)走 on-reload
2847
+ // 策略,平时不查盘。放在扫盘与 reload 之前,本轮上下线的 agent 才能读到磁盘上的
2848
+ // 最新版本——放在末尾的话,这一轮全用旧值,改动要等下一次 resync 才生效。
2849
+ invalidateKitCache();
2850
+ const { loadAllAgents: scanAgents } = await import('./config-store.js');
2851
+ const { agents: diskAgents } = scanAgents();
2852
+ const results = [];
2853
+ // 1. 下线:运行时有但磁盘上没有 / disabled 的
2854
+ for (const [aid] of [...agentRegistry.agents.entries()]) {
2855
+ const diskCfg = diskAgents.find(a => a.aid === aid);
2856
+ if (!diskCfg || diskCfg.enabled === false) {
2857
+ try {
2858
+ const offlined = await reloadCoordinator.runExclusive(aid, async () => {
2859
+ const latestDiskCfg = readConfig(ConfigTarget.Agent, { self: aid }, { cache: false, expand: true });
2860
+ if (latestDiskCfg && latestDiskCfg.enabled !== false)
2861
+ return false;
2862
+ const runtimeAgent = agentRegistry.get(aid);
2863
+ if (!runtimeAgent)
2864
+ return false;
2865
+ let removed = false;
2866
+ handoffRuntime.pauseAgent(aid);
2867
+ try {
2868
+ await handoffRuntime.drainAgent(aid);
2869
+ await triggerSchedulers.get(aid)?.stop();
2870
+ triggerSchedulers.delete(aid);
2871
+ triggerSchedulerStarts.delete(aid);
2872
+ // 断开所有 channels
2873
+ for (const chName of runtimeAgent.channelInstanceNames()) {
2874
+ const inst = channelInstances.find(i => i.adapter.channelName === chName);
2875
+ if (inst) {
2876
+ try {
2877
+ await inst.disconnect();
2878
+ }
2879
+ catch { }
2880
+ const idx = channelInstances.indexOf(inst);
2881
+ if (idx >= 0)
2882
+ channelInstances.splice(idx, 1);
2883
+ }
2884
+ unregisterRuntimeChannel(chName);
2885
+ }
2886
+ agentRegistry.agents.delete(aid);
2887
+ messageQueue.clearByAgent(runtimeAgent.name);
2888
+ removed = true;
2889
+ return true;
2890
+ }
2891
+ finally {
2892
+ if (!removed)
2893
+ handoffRuntime.resumeAgent(aid);
2894
+ }
2895
+ });
2896
+ if (offlined)
2897
+ results.push(`- ${aid} (offline)`);
2898
+ }
2899
+ catch (error) {
2900
+ if (error instanceof AgentReloadBusyError) {
2901
+ results.push(`⏸ ${aid} (busy: ${error.busyCount} task(s); reload skipped)`);
2902
+ }
2903
+ else {
2904
+ results.push(`⚠ ${aid}: ${error instanceof Error ? error.message : String(error)}`);
2727
2905
  }
2728
- catch { }
2729
- markChannelDisconnected(inst.adapter.channelName);
2730
- const idx = channelInstances.indexOf(inst);
2731
- if (idx >= 0)
2732
- channelInstances.splice(idx, 1);
2733
2906
  }
2907
+ continue;
2734
2908
  }
2735
- agentRegistry.agents.delete(aid);
2736
- results.push(`- ${aid} (offline)`);
2737
- continue;
2738
2909
  }
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)`);
2910
+ // 2. 新增:磁盘上有但运行时没有的
2911
+ for (const cfg of diskAgents) {
2912
+ if (cfg.enabled === false)
2913
+ continue;
2914
+ if (agentRegistry.agents.has(cfg.aid))
2915
+ continue;
2916
+ try {
2917
+ await globalThis.__evolcore_hotLoadAgent(cfg.aid);
2918
+ results.push(`+ ${cfg.aid} (online)`);
2919
+ }
2920
+ catch (e) {
2921
+ results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
2922
+ }
2749
2923
  }
2750
- catch (e) {
2751
- results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
2924
+ // 3. 已有的:重新 reload(config 可能改了)
2925
+ for (const cfg of diskAgents) {
2926
+ if (cfg.enabled === false)
2927
+ continue;
2928
+ if (!agentRegistry.agents.has(cfg.aid))
2929
+ continue;
2930
+ // 只有磁盘上存在且运行时也存在的才 reload
2931
+ try {
2932
+ await reloadCoordinator.reload(cfg.aid);
2933
+ const runtimeAgent = agentRegistry.get(cfg.aid);
2934
+ if (runtimeAgent)
2935
+ await startTriggerScheduler(runtimeAgent);
2936
+ results.push(`↻ ${cfg.aid} (reloaded)`);
2937
+ }
2938
+ catch (e) {
2939
+ results.push(`⚠ ${cfg.aid}: ${e?.message || e}`);
2940
+ }
2752
2941
  }
2942
+ // 重建 channel index(kit 缓存已在本轮开头清过)
2943
+ agentRegistry.channelIndex.clear();
2944
+ agentRegistry.buildChannelIndex();
2945
+ logger.info(`[Resync] Done: ${results.length} agent(s) processed`);
2946
+ return results;
2947
+ })();
2948
+ resyncInFlight = operation;
2949
+ try {
2950
+ return await operation;
2753
2951
  }
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
- }
2952
+ finally {
2953
+ if (resyncInFlight === operation)
2954
+ resyncInFlight = undefined;
2772
2955
  }
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
2956
  };
2779
2957
  ipcServer.setStatsProvider(() => statsCollector.getSnapshot());
2780
2958
  ipcServer.setAgentStatsProvider(() => agentRegistry.list().map((agent) => {
package/dist/ipc.js CHANGED
@@ -5,6 +5,7 @@ import path from 'path';
5
5
  import { logger } from './utils/logger.js';
6
6
  import { fileCache } from './core/daemon-file-cache.js';
7
7
  import { HANDOFF_QUERY_MAX_LIMIT, HANDOFF_STATES } from './core/handoff/types.js';
8
+ import { AgentReloadBusyError } from './core/agent-reload-coordinator.js';
8
9
  import { resolvePaths } from './paths.js';
9
10
  import { getProcessStartTime, isSameOrOlderProcess } from './utils/process-introspect.js';
10
11
  import { classifyProcessTree, ProcessTreeSampler } from './utils/process-tree-stats.js';
@@ -39,6 +40,8 @@ export class IpcServer {
39
40
  queueSnapshotProvider;
40
41
  queueActionExecutor;
41
42
  triggerExecutor;
43
+ bootstrapCompleteExecutor;
44
+ agentReloadExecutor;
42
45
  taskRuntimeContextProvider;
43
46
  aunMsgSender;
44
47
  handoffReturnExecutor;
@@ -85,6 +88,9 @@ export class IpcServer {
85
88
  setContactOperationExecutor(executor) {
86
89
  this.contactOperationExecutor = executor;
87
90
  }
91
+ setAgentReloadExecutor(executor) {
92
+ this.agentReloadExecutor = executor;
93
+ }
88
94
  /** Inject AUN AID state aggregator for aun-aids IPC handler */
89
95
  setAunAidProvider(provider) {
90
96
  this.aunAidProvider = provider;
@@ -117,6 +123,10 @@ export class IpcServer {
117
123
  setTriggerExecutor(executor) {
118
124
  this.triggerExecutor = executor;
119
125
  }
126
+ /** Complete an agent bootstrap in the daemon so disk and runtime state change together. */
127
+ setBootstrapCompleteExecutor(executor) {
128
+ this.bootstrapCompleteExecutor = executor;
129
+ }
120
130
  /** Inject active task runtime-context provider for in-task CLI routing. */
121
131
  setTaskRuntimeContextProvider(provider) {
122
132
  this.taskRuntimeContextProvider = provider;
@@ -325,7 +335,6 @@ export class IpcServer {
325
335
  bindType: cmd.bindType,
326
336
  targetAid: cmd.targetAid,
327
337
  agentName: cmd.agentName,
328
- ownerMode: cmd.ownerMode,
329
338
  ttlMs: cmd.ttlMs,
330
339
  });
331
340
  }
@@ -668,29 +677,38 @@ export class IpcServer {
668
677
  return { ok: false, error: `Agent "${name}" found but info missing (race?)` };
669
678
  return { ok: true, agent: info };
670
679
  }
680
+ case 'evolagent.bootstrapComplete': {
681
+ const aid = cmd.aid;
682
+ if (!aid || typeof aid !== 'string')
683
+ return { ok: false, error: 'missing aid' };
684
+ if (!this.bootstrapCompleteExecutor)
685
+ return { ok: false, error: 'bootstrap complete executor not configured' };
686
+ try {
687
+ return await this.bootstrapCompleteExecutor(aid);
688
+ }
689
+ catch (e) {
690
+ return { ok: false, error: e?.message || String(e) };
691
+ }
692
+ }
671
693
  case 'evolagent.reload': {
672
694
  if (!this.agentRegistry)
673
695
  return { ok: false, error: 'EvolAgentRegistry not available' };
674
696
  const name = cmd.name;
675
697
  if (!name || typeof name !== 'string')
676
698
  return { ok: false, error: 'missing name' };
677
- const hooks = globalThis.__evolcore_reloadHooks;
678
- if (!hooks)
679
- return { ok: false, error: 'Reload hooks not initialized' };
699
+ if (!this.agentReloadExecutor)
700
+ return { ok: false, error: 'Agent reload coordinator not initialized' };
680
701
  try {
681
702
  const a = this.agentRegistry.get(name);
682
703
  if (!a)
683
704
  return { ok: false, error: `Agent "${name}" not found` };
684
- if (!this.agentRegistry.reload)
685
- return { ok: false, error: 'EvolAgentRegistry.reload not available' };
686
- // 'kits' 组(manifest / fragment / schema / 角色模板)走 on-reload 策略,平时不查盘:
687
- // reload 是它唯一的刷新时机,必须先清再 reload,否则重载出来的 agent 仍用旧模板。
688
- const { invalidateKitCache } = await import('./eck/kit-renderer.js');
689
- invalidateKitCache();
690
- await this.agentRegistry.reload(name, hooks);
705
+ await this.agentReloadExecutor(name, { force: cmd.force === true });
691
706
  return { ok: true, result: `Agent "${name}" reloaded` };
692
707
  }
693
708
  catch (e) {
709
+ if (e instanceof AgentReloadBusyError) {
710
+ return { ok: false, code: e.code, busyCount: e.busyCount, error: e.message };
711
+ }
694
712
  return { ok: false, error: e?.message || String(e) };
695
713
  }
696
714
  }
@@ -45,7 +45,6 @@ export class BindService {
45
45
  }
46
46
  const ttlMs = clampTtl(req.ttlMs);
47
47
  const expiresAt = Date.now() + ttlMs;
48
- const ownerMode = req.ownerMode === 'replace' ? 'replace' : 'append';
49
48
  const task = {
50
49
  taskId,
51
50
  bindType: req.bindType,
@@ -56,7 +55,6 @@ export class BindService {
56
55
  createdAt: Date.now(),
57
56
  expiresAt,
58
57
  status: 'pending',
59
- ownerMode,
60
58
  };
61
59
  this.tasks.set(taskId, task);
62
60
  this.taskIdsByTokenHash.set(tokenHash, taskId);
@@ -209,9 +207,7 @@ export class BindService {
209
207
  if (task.bindType === 'daemon') {
210
208
  const cfg = loadDaemonConfig();
211
209
  const current = cfg.owners ?? [];
212
- const owners = task.ownerMode === 'replace'
213
- ? [ownerAid]
214
- : [ownerAid, ...current.filter(o => o !== ownerAid)];
210
+ const owners = [ownerAid, ...current.filter(o => o !== ownerAid)];
215
211
  saveDaemonConfig({ ...cfg, $schema_version: cfg.$schema_version ?? 1, owners });
216
212
  this.opts.onDaemonOwnersUpdated?.(owners);
217
213
  return;
@@ -220,10 +216,9 @@ export class BindService {
220
216
  if (!agent)
221
217
  throw new Error(`agent not found: ${task.targetAid}`);
222
218
  const current = agent.owners ?? [];
223
- const owners = task.ownerMode === 'replace'
224
- ? [ownerAid]
225
- : [ownerAid, ...current.filter(o => o !== ownerAid)];
219
+ const owners = [ownerAid, ...current.filter(o => o !== ownerAid)];
226
220
  saveAgent({ ...agent, owners });
221
+ this.opts.onAgentOwnerUpdated?.(task.targetAid, ownerAid);
227
222
  }
228
223
  cleanup() {
229
224
  const now = Date.now();
@@ -231,19 +231,14 @@ export class LogWriter {
231
231
  return `${day}-${pad(d.getHours())}`;
232
232
  }
233
233
  cleanupOldArchives() {
234
- LogWriter.cleanupArchivesIn(this.logDir, this.retentionMs);
234
+ LogWriter.cleanupArchivesIn(this.logDir, this.baseName, this.retentionMs);
235
235
  }
236
236
  /**
237
- * 全局清理:扫整个 logDir,按 retention 删除所有匹配 LogWriter 归档命名约定的文件。
237
+ * 清理当前日志类型的归档文件,不能跨 baseName 清理。
238
238
  *
239
239
  * 命名约定:`<baseName>-YYYYMMDD-HH.log`(hourly)或 `<baseName>-YYYYMMDD.log`(daily),
240
- * 其中 baseName 由字母/数字/连字符组成。
241
- *
242
- * 这条规则跨 baseName 统一——只要文件按这个 pattern 命名就认为受 LogWriter 体系管辖。
243
- * 这样 conditional 启用的 LogWriter(如 aun trace 关闭时)不会留下永久无人清的归档:
244
- * 任意 LogWriter 实例化都会顺便清掉它们。
245
240
  */
246
- static cleanupArchivesIn(logDir, retentionMs) {
241
+ static cleanupArchivesIn(logDir, baseName, retentionMs) {
247
242
  if (retentionMs <= 0)
248
243
  return;
249
244
  const cutoff = Date.now() - retentionMs;
@@ -254,9 +249,10 @@ export class LogWriter {
254
249
  catch {
255
250
  return;
256
251
  }
257
- const generalPattern = /^[A-Za-z0-9][A-Za-z0-9_-]*-\d{8}(?:-\d{2})?\.log$/;
252
+ const escapedBaseName = baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
253
+ const archivePattern = new RegExp(`^${escapedBaseName}-\\d{8}(?:-\\d{2})?\\.log$`);
258
254
  for (const name of entries) {
259
- if (!generalPattern.test(name))
255
+ if (!archivePattern.test(name))
260
256
  continue;
261
257
  const full = path.join(logDir, name);
262
258
  try {
@@ -24,15 +24,15 @@ function getWriters() {
24
24
  closeWriters(writers);
25
25
  writers = {
26
26
  logDir,
27
- main: new LogWriter({ baseName: 'daemon', logDir, rotation: 'hourly', retention: { hours: 24 } }),
27
+ main: new LogWriter({ baseName: 'daemon', logDir, rotation: 'hourly', retention: { hours: 48 } }),
28
28
  message: config.messageLog
29
- ? new LogWriter({ baseName: 'messages', logDir, rotation: 'hourly', retention: { hours: 24 } })
29
+ ? new LogWriter({ baseName: 'messages', logDir, rotation: 'hourly', retention: { hours: 48 } })
30
30
  : null,
31
31
  event: config.eventLog
32
- ? new LogWriter({ baseName: 'events', logDir, rotation: 'hourly', retention: { hours: 24 } })
32
+ ? new LogWriter({ baseName: 'events', logDir, rotation: 'hourly', retention: { hours: 48 } })
33
33
  : null,
34
- channelIn: new LogWriter({ baseName: 'channel-in', logDir, rotation: 'hourly', retention: { hours: 24 } }),
35
- channelOut: new LogWriter({ baseName: 'channel-out', logDir, rotation: 'hourly', retention: { hours: 24 } }),
34
+ channelIn: new LogWriter({ baseName: 'channel-in', logDir, rotation: 'hourly', retention: { hours: 48 } }),
35
+ channelOut: new LogWriter({ baseName: 'channel-out', logDir, rotation: 'hourly', retention: { hours: 48 } }),
36
36
  };
37
37
  return writers;
38
38
  }
@@ -2,6 +2,13 @@
2
2
 
3
3
  私聊场景下收发消息的命令集。触发词:回复/发消息/拉取/撤回/查在线。
4
4
 
5
+ ## Agent 会话中的 Shell 调用边界
6
+
7
+ 在 EvolCore 托管会话中,每次工具调用只能执行一条完整的 `ec` 命令。禁止把
8
+ `command -v`、`ec aid`、`&&`、`||`、`;`、`|`、重定向、子 shell、`bash -lc` 或其它脚本
9
+ 和 `ec msg` 拼在一起。不要做单独的 CLI 探测;直接调用本页的单条命令,读取结构化结果后
10
+ 再决定下一步。工具审批被拒绝时应报告原始错误并停止,不要通过追加命令或提权参数重试。
11
+
5
12
  以自己的 AID 为发送者(`<from>`),对端 AID 为 `<to>`。
6
13
 
7
14
  ## 发送消息
@@ -29,6 +36,8 @@ ec msg send <from> <to> --link <url> [--title "<title>"] [--description "<desc>"
29
36
  ec msg send <from> <to> --payload '<json>'
30
37
  ```
31
38
 
39
+ 每一行都必须单独执行,且是该次 shell 调用中的唯一命令。
40
+
32
41
  发送相关选项:
33
42
  - `--text-from-file <path>` — 从文件读取文本(UTF-8),用于超长消息或避免 Shell 转义
34
43
  - `--encrypt` — 端到端加密
@@ -39,6 +48,10 @@ ec msg send <from> <to> --payload '<json>'
39
48
  - `--text <说>` — 附件说明文字(仅 `--file`)
40
49
  - `--transcript <text>` — 语音转写(仅 `--as voice`)
41
50
 
51
+ 在 EvolCore 会话内,发送还会经过当前 session 的 self/peer/role 鉴权。`visitor` 或 `member`
52
+ 默认只允许回复当前对端;从一个 agent 会话转发给第三个 agent 属于 relay 操作,可能因
53
+ `ownPeerOnly` 被拒绝。没有成功 JSON 回执(含 `message_id`/`status`)不得声称已发送。
54
+
42
55
  成功输出:`✓ 已发送 <message_id> seq=<n> status=<status>`
43
56
 
44
57
  > 正文写法:含空格/换行/特殊字符时用引号包起来;纯短词可不包,但多空格会被压成一个。