evolcore 0.0.7 → 0.0.9

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 (230) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +38 -4
  3. package/dist/agents/baseagent.js +117 -24
  4. package/dist/agents/claude-runner.js +31 -2
  5. package/dist/agents/codex-app-server-client.js +37 -2
  6. package/dist/agents/codex-runner.js +11 -3
  7. package/dist/aun/aid/control-aid.js +40 -27
  8. package/dist/aun/aid/domain.js +23 -0
  9. package/dist/aun/msg/group.js +9 -9
  10. package/dist/aun/msg/p2p.js +11 -6
  11. package/dist/aun/msg/upload.js +43 -29
  12. package/dist/aun/outbox.js +48 -5
  13. package/dist/channels/aun.js +225 -71
  14. package/dist/cli/aun-commands.js +10 -7
  15. package/dist/cli/bench.js +4 -3
  16. package/dist/cli/daemon-commands.js +208 -68
  17. package/dist/cli/data-command.js +62 -35
  18. package/dist/cli/index.js +11 -1
  19. package/dist/cli/init-channel.js +1 -1
  20. package/dist/cli/init.js +112 -17
  21. package/dist/cli/model.js +5 -5
  22. package/dist/cli/restart-monitor.js +102 -22
  23. package/dist/config/builtin-role-templates.js +4 -3
  24. package/dist/config/config-field-policy.js +6 -2
  25. package/dist/config/config-manager.js +79 -11
  26. package/dist/config/gateway-config.js +80 -35
  27. package/dist/config/role-schema.js +18 -3
  28. package/dist/config/role-service.js +3 -3
  29. package/dist/config/role-store.js +4 -3
  30. package/dist/config-store.js +19 -3
  31. package/dist/core/auth/operation-authorizer.js +3 -1
  32. package/dist/core/baseagent-loader.js +5 -3
  33. package/dist/core/bootstrap-service.js +0 -12
  34. package/dist/core/capability/providers/codex-capability-provider.js +2 -2
  35. package/dist/core/channel-loader.js +15 -2
  36. package/dist/core/command/command-handler.js +1 -1
  37. package/dist/core/command/evol-menu-version-gate.js +3 -2
  38. package/dist/core/command/menu-handler.js +50 -28
  39. package/dist/core/command/menu-protocol.js +7 -4
  40. package/dist/core/command/slash-handler.js +4 -4
  41. package/dist/core/data-migration.js +517 -24
  42. package/dist/core/inference/text-inference.js +18 -5
  43. package/dist/core/message/message-bridge.js +17 -6
  44. package/dist/core/message/response-engine.js +22 -3
  45. package/dist/core/model/config-scope.js +3 -3
  46. package/dist/core/permission/tool-policy.js +18 -0
  47. package/dist/core/protected-paths.js +12 -1
  48. package/dist/index.js +794 -725
  49. package/dist/ipc.js +3 -1
  50. package/dist/product.js +1 -0
  51. package/dist/utils/autostart.js +27 -0
  52. package/dist/utils/codex-cli.js +39 -0
  53. package/dist/utils/cross-platform.js +147 -18
  54. package/dist/utils/ecweb-utils.js +15 -2
  55. package/dist/utils/instance-registry.js +43 -6
  56. package/dist/utils/macos-autostart.js +179 -0
  57. package/dist/utils/process-introspect.js +7 -3
  58. package/dist/utils/windows-autostart.js +130 -0
  59. package/ecagent/dist/agent-loop.d.ts +1 -1
  60. package/ecagent/dist/agent-loop.js +1 -1
  61. package/ecagent/dist/agent.d.ts +1 -1
  62. package/ecagent/dist/agent.js +1 -1
  63. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  64. package/ecagent/dist/harness/compaction/branch-summarization.d.ts +1 -1
  65. package/ecagent/dist/harness/compaction/compaction.d.ts +1 -1
  66. package/ecagent/dist/harness/compaction/utils.d.ts +1 -1
  67. package/ecagent/dist/harness/messages.d.ts +1 -1
  68. package/ecagent/dist/harness/session/session.d.ts +1 -1
  69. package/ecagent/dist/harness/types.d.ts +1 -1
  70. package/ecagent/dist/proxy.d.ts +1 -1
  71. package/ecagent/dist/proxy.js +1 -1
  72. package/ecagent/dist/types.d.ts +1 -1
  73. package/kits/migrations/migrate-role-config-v5.mjs +27 -2
  74. package/kits/rules/01-overview.md +3 -2
  75. package/kits/schemas/_meta.json +2 -1
  76. package/kits/schemas/agent-config.schema.10.json +19 -1
  77. package/kits/schemas/daemon.schema.4.json +136 -0
  78. package/kits/schemas/defaults.schema.4.json +15 -1
  79. package/kits/schemas/relation-config.schema.8.json +22 -1
  80. package/kits/schemas/role-config.schema.1.json +2 -2
  81. package/package.json +2 -4
  82. package/ecagent/dist/agent-loop.d.ts.map +0 -1
  83. package/ecagent/dist/agent-loop.js.map +0 -1
  84. package/ecagent/dist/agent.d.ts.map +0 -1
  85. package/ecagent/dist/agent.js.map +0 -1
  86. package/ecagent/dist/harness/agent-harness.d.ts.map +0 -1
  87. package/ecagent/dist/harness/agent-harness.js.map +0 -1
  88. package/ecagent/dist/harness/compaction/branch-summarization.d.ts.map +0 -1
  89. package/ecagent/dist/harness/compaction/branch-summarization.js.map +0 -1
  90. package/ecagent/dist/harness/compaction/compaction.d.ts.map +0 -1
  91. package/ecagent/dist/harness/compaction/compaction.js.map +0 -1
  92. package/ecagent/dist/harness/compaction/utils.d.ts.map +0 -1
  93. package/ecagent/dist/harness/compaction/utils.js.map +0 -1
  94. package/ecagent/dist/harness/env/nodejs.d.ts.map +0 -1
  95. package/ecagent/dist/harness/env/nodejs.js.map +0 -1
  96. package/ecagent/dist/harness/messages.d.ts.map +0 -1
  97. package/ecagent/dist/harness/messages.js.map +0 -1
  98. package/ecagent/dist/harness/prompt-templates.d.ts.map +0 -1
  99. package/ecagent/dist/harness/prompt-templates.js.map +0 -1
  100. package/ecagent/dist/harness/session/jsonl-repo.d.ts.map +0 -1
  101. package/ecagent/dist/harness/session/jsonl-repo.js.map +0 -1
  102. package/ecagent/dist/harness/session/jsonl-storage.d.ts.map +0 -1
  103. package/ecagent/dist/harness/session/jsonl-storage.js.map +0 -1
  104. package/ecagent/dist/harness/session/memory-repo.d.ts.map +0 -1
  105. package/ecagent/dist/harness/session/memory-repo.js.map +0 -1
  106. package/ecagent/dist/harness/session/memory-storage.d.ts.map +0 -1
  107. package/ecagent/dist/harness/session/memory-storage.js.map +0 -1
  108. package/ecagent/dist/harness/session/repo-utils.d.ts.map +0 -1
  109. package/ecagent/dist/harness/session/repo-utils.js.map +0 -1
  110. package/ecagent/dist/harness/session/session.d.ts.map +0 -1
  111. package/ecagent/dist/harness/session/session.js.map +0 -1
  112. package/ecagent/dist/harness/session/uuid.d.ts.map +0 -1
  113. package/ecagent/dist/harness/session/uuid.js.map +0 -1
  114. package/ecagent/dist/harness/skills.d.ts.map +0 -1
  115. package/ecagent/dist/harness/skills.js.map +0 -1
  116. package/ecagent/dist/harness/system-prompt.d.ts.map +0 -1
  117. package/ecagent/dist/harness/system-prompt.js.map +0 -1
  118. package/ecagent/dist/harness/types.d.ts.map +0 -1
  119. package/ecagent/dist/harness/types.js.map +0 -1
  120. package/ecagent/dist/harness/utils/shell-output.d.ts.map +0 -1
  121. package/ecagent/dist/harness/utils/shell-output.js.map +0 -1
  122. package/ecagent/dist/harness/utils/truncate.d.ts.map +0 -1
  123. package/ecagent/dist/harness/utils/truncate.js.map +0 -1
  124. package/ecagent/dist/index.d.ts.map +0 -1
  125. package/ecagent/dist/index.js.map +0 -1
  126. package/ecagent/dist/node.d.ts.map +0 -1
  127. package/ecagent/dist/node.js.map +0 -1
  128. package/ecagent/dist/proxy.d.ts.map +0 -1
  129. package/ecagent/dist/proxy.js.map +0 -1
  130. package/ecagent/dist/runtime/api/lazy.d.ts.map +0 -1
  131. package/ecagent/dist/runtime/api/lazy.js.map +0 -1
  132. package/ecagent/dist/runtime/api/pi-messages.d.ts.map +0 -1
  133. package/ecagent/dist/runtime/api/pi-messages.js.map +0 -1
  134. package/ecagent/dist/runtime/api/pi-messages.lazy.d.ts.map +0 -1
  135. package/ecagent/dist/runtime/api/pi-messages.lazy.js.map +0 -1
  136. package/ecagent/dist/runtime/auth/context.d.ts.map +0 -1
  137. package/ecagent/dist/runtime/auth/context.js.map +0 -1
  138. package/ecagent/dist/runtime/auth/credential-store.d.ts.map +0 -1
  139. package/ecagent/dist/runtime/auth/credential-store.js.map +0 -1
  140. package/ecagent/dist/runtime/auth/helpers.d.ts.map +0 -1
  141. package/ecagent/dist/runtime/auth/helpers.js.map +0 -1
  142. package/ecagent/dist/runtime/auth/resolve.d.ts.map +0 -1
  143. package/ecagent/dist/runtime/auth/resolve.js.map +0 -1
  144. package/ecagent/dist/runtime/auth/types.d.ts.map +0 -1
  145. package/ecagent/dist/runtime/auth/types.js.map +0 -1
  146. package/ecagent/dist/runtime/compat.d.ts.map +0 -1
  147. package/ecagent/dist/runtime/compat.js.map +0 -1
  148. package/ecagent/dist/runtime/index.d.ts.map +0 -1
  149. package/ecagent/dist/runtime/index.js.map +0 -1
  150. package/ecagent/dist/runtime/models.d.ts.map +0 -1
  151. package/ecagent/dist/runtime/models.js.map +0 -1
  152. package/ecagent/dist/runtime/oauth.d.ts.map +0 -1
  153. package/ecagent/dist/runtime/oauth.js.map +0 -1
  154. package/ecagent/dist/runtime/providers/faux.d.ts.map +0 -1
  155. package/ecagent/dist/runtime/providers/faux.js.map +0 -1
  156. package/ecagent/dist/runtime/types.d.ts.map +0 -1
  157. package/ecagent/dist/runtime/types.js.map +0 -1
  158. package/ecagent/dist/runtime/utils/diagnostics.d.ts.map +0 -1
  159. package/ecagent/dist/runtime/utils/diagnostics.js.map +0 -1
  160. package/ecagent/dist/runtime/utils/event-stream.d.ts.map +0 -1
  161. package/ecagent/dist/runtime/utils/event-stream.js.map +0 -1
  162. package/ecagent/dist/runtime/utils/headers.d.ts.map +0 -1
  163. package/ecagent/dist/runtime/utils/headers.js.map +0 -1
  164. package/ecagent/dist/runtime/utils/json-parse.d.ts.map +0 -1
  165. package/ecagent/dist/runtime/utils/json-parse.js.map +0 -1
  166. package/ecagent/dist/runtime/utils/oauth/types.d.ts.map +0 -1
  167. package/ecagent/dist/runtime/utils/oauth/types.js.map +0 -1
  168. package/ecagent/dist/runtime/utils/overflow.d.ts.map +0 -1
  169. package/ecagent/dist/runtime/utils/overflow.js.map +0 -1
  170. package/ecagent/dist/runtime/utils/provider-env.d.ts.map +0 -1
  171. package/ecagent/dist/runtime/utils/provider-env.js.map +0 -1
  172. package/ecagent/dist/runtime/utils/retry.d.ts.map +0 -1
  173. package/ecagent/dist/runtime/utils/retry.js.map +0 -1
  174. package/ecagent/dist/runtime/utils/typebox-helpers.d.ts.map +0 -1
  175. package/ecagent/dist/runtime/utils/typebox-helpers.js.map +0 -1
  176. package/ecagent/dist/runtime/utils/validation.d.ts.map +0 -1
  177. package/ecagent/dist/runtime/utils/validation.js.map +0 -1
  178. package/ecagent/dist/types.d.ts.map +0 -1
  179. package/ecagent/dist/types.js.map +0 -1
  180. package/ecagent/package.json +0 -93
  181. package/ecagent/src/agent-loop.ts +0 -792
  182. package/ecagent/src/agent.ts +0 -575
  183. package/ecagent/src/harness/agent-harness.ts +0 -1029
  184. package/ecagent/src/harness/compaction/branch-summarization.ts +0 -261
  185. package/ecagent/src/harness/compaction/compaction.ts +0 -753
  186. package/ecagent/src/harness/compaction/utils.ts +0 -144
  187. package/ecagent/src/harness/env/nodejs.ts +0 -569
  188. package/ecagent/src/harness/messages.ts +0 -164
  189. package/ecagent/src/harness/prompt-templates.ts +0 -267
  190. package/ecagent/src/harness/session/jsonl-repo.ts +0 -179
  191. package/ecagent/src/harness/session/jsonl-storage.ts +0 -314
  192. package/ecagent/src/harness/session/memory-repo.ts +0 -50
  193. package/ecagent/src/harness/session/memory-storage.ts +0 -133
  194. package/ecagent/src/harness/session/repo-utils.ts +0 -51
  195. package/ecagent/src/harness/session/session.ts +0 -338
  196. package/ecagent/src/harness/session/uuid.ts +0 -54
  197. package/ecagent/src/harness/skills.ts +0 -375
  198. package/ecagent/src/harness/system-prompt.ts +0 -34
  199. package/ecagent/src/harness/types.ts +0 -838
  200. package/ecagent/src/harness/utils/shell-output.ts +0 -135
  201. package/ecagent/src/harness/utils/truncate.ts +0 -344
  202. package/ecagent/src/index.ts +0 -46
  203. package/ecagent/src/node.ts +0 -2
  204. package/ecagent/src/proxy.ts +0 -367
  205. package/ecagent/src/runtime/api/lazy.ts +0 -70
  206. package/ecagent/src/runtime/api/pi-messages.lazy.ts +0 -4
  207. package/ecagent/src/runtime/api/pi-messages.ts +0 -436
  208. package/ecagent/src/runtime/auth/context.ts +0 -45
  209. package/ecagent/src/runtime/auth/credential-store.ts +0 -47
  210. package/ecagent/src/runtime/auth/helpers.ts +0 -46
  211. package/ecagent/src/runtime/auth/resolve.ts +0 -141
  212. package/ecagent/src/runtime/auth/types.ts +0 -182
  213. package/ecagent/src/runtime/compat.ts +0 -158
  214. package/ecagent/src/runtime/index.ts +0 -31
  215. package/ecagent/src/runtime/models.ts +0 -452
  216. package/ecagent/src/runtime/oauth.ts +0 -1
  217. package/ecagent/src/runtime/providers/faux.ts +0 -538
  218. package/ecagent/src/runtime/types.ts +0 -718
  219. package/ecagent/src/runtime/utils/diagnostics.ts +0 -45
  220. package/ecagent/src/runtime/utils/event-stream.ts +0 -88
  221. package/ecagent/src/runtime/utils/headers.ts +0 -18
  222. package/ecagent/src/runtime/utils/json-parse.ts +0 -124
  223. package/ecagent/src/runtime/utils/oauth/types.ts +0 -79
  224. package/ecagent/src/runtime/utils/overflow.ts +0 -165
  225. package/ecagent/src/runtime/utils/provider-env.ts +0 -52
  226. package/ecagent/src/runtime/utils/retry.ts +0 -101
  227. package/ecagent/src/runtime/utils/typebox-helpers.ts +0 -24
  228. package/ecagent/src/runtime/utils/validation.ts +0 -310
  229. package/ecagent/src/types.ts +0 -430
  230. package/ecagent/tsconfig.build.json +0 -32
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ import { EvolAgentRegistry } from './core/evolagent-registry.js';
69
69
  import { buildReloadHooks } from './core/channel-loader.js';
70
70
  import { IpcServer } from './ipc.js';
71
71
  import { logger, setLogLevel } from './utils/logger.js';
72
- import { fetchEcwebPairCode } from './utils/ecweb-utils.js';
72
+ import { ecwebVersionRequirementError, fetchEcwebPairCode } from './utils/ecweb-utils.js';
73
73
  import { writeMain, removeAll, isMainWinner, scanInstances } from './utils/instance-registry.js';
74
74
  import { detectDuplicates } from './core/evolagent-registry.js';
75
75
  import { loadKitManifest, cleanEckDebug, invalidateKitCache } from './eck/kit-renderer.js';
@@ -88,6 +88,7 @@ import { validateModelSelectionForRole } from './core/model/model-permission.js'
88
88
  import { constrainRuntimePermissionMode, validateRuntimeStringFieldOverride, } from './core/role/runtime-policy.js';
89
89
  import { atomicWriteJson } from './core/session/session-fs-store.js';
90
90
  import { appendMessageLog, buildOutboundEntry, classifyAunPayloadForLog } from './core/message/message-log.js';
91
+ import { MAIN_PACKAGE_NAME } from './product.js';
91
92
  import fs from 'fs';
92
93
  import crypto from 'crypto';
93
94
  import { fileURLToPath } from 'url';
@@ -277,7 +278,7 @@ function seedUpgradeCheckTrigger(manager, owner) {
277
278
  'fi',
278
279
  '',
279
280
  'local_version="$(node -e \'try { console.log(require(process.argv[1]).version || "") } catch { process.exit(0) }\' "$PACKAGE_JSON")"',
280
- 'remote_version="$(npm view ec version 2>/dev/null || true)"',
281
+ `remote_version="$(npm view ${MAIN_PACKAGE_NAME} version 2>/dev/null || true)"`,
281
282
  '',
282
283
  'if [ -z "$remote_version" ]; then',
283
284
  ' json "error" "failed to check evolcore latest version"',
@@ -367,6 +368,8 @@ async function runBindBootstrapDaemon(daemonCfg) {
367
368
  agentName: daemonCfg.aid,
368
369
  channelName: 'control',
369
370
  pureIdentity: true,
371
+ gatewayUrl: daemonCfg.aun?.gatewayUrl,
372
+ defaultEncrypt: daemonCfg.aun?.defaultEncrypt,
370
373
  aunTrace: daemonCfg.debug?.aunTrace,
371
374
  aunSdkLog: daemonCfg.debug?.aunSdkLog,
372
375
  });
@@ -397,7 +400,20 @@ async function runBindBootstrapDaemon(daemonCfg) {
397
400
  channels: {},
398
401
  channelsByType: {},
399
402
  queue: { pending: 0, processing: 0 },
400
- controlAid: { aid: daemonCfg.aid, connected: controlChannel?.getAidState().status === 'connected' },
403
+ controlAid: (() => {
404
+ const aidState = controlChannel?.getAidState();
405
+ return {
406
+ aid: daemonCfg.aid,
407
+ connected: aidState?.status === 'connected',
408
+ ...(aidState ? {
409
+ status: aidState.status,
410
+ gatewayUrl: aidState.gatewayUrl,
411
+ reconnectCount: aidState.reconnectCount,
412
+ lastConnectedAt: aidState.lastConnectedAt,
413
+ lastError: aidState.lastError,
414
+ } : {}),
415
+ };
416
+ })(),
401
417
  }));
402
418
  ipcServer.setBindExecutor({
403
419
  begin: (cmd) => bindService.begin(cmd),
@@ -859,11 +875,12 @@ async function main() {
859
875
  agentLoader.register(new CodexAgentPlugin());
860
876
  agentLoader.register(new GeminiAgentPlugin());
861
877
  agentLoader.register(new EcagentAgentPlugin());
878
+ const creationErrors = [];
862
879
  const agentInstances = agentLoader.createAll(agentRegistry, {
863
880
  onSessionIdUpdate: async (sessionId, agentSessionId) => {
864
881
  await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
865
882
  },
866
- });
883
+ }, creationErrors);
867
884
  // agentMap 复合键:${aid}::${baseagent}
868
885
  const agentMap = new Map();
869
886
  for (const inst of agentInstances) {
@@ -871,10 +888,18 @@ async function main() {
871
888
  }
872
889
  const primaryBaseagent = primaryAgent?.baseagent ?? 'claude';
873
890
  let primaryRunnerKey = primaryAgent ? `${primaryAgent.aid}::${primaryBaseagent}` : '<empty>::claude';
874
- const agentRunner = agentMap.get(primaryRunnerKey) || agentInstances[0]?.agent;
891
+ const agentRunner = primaryAgent
892
+ ? agentMap.get(primaryRunnerKey)
893
+ : agentInstances[0]?.agent;
875
894
  if (primaryAgent && !agentRunner) {
876
895
  agentRuntimeState = 'error';
877
- agentRuntimeError = 'No agent backend available. Check baseagents config (no runners created).';
896
+ const primaryCreationErrors = creationErrors.filter(error => error.evolagentName === primaryAgent.name);
897
+ const creationDetail = primaryCreationErrors.length > 0
898
+ ? primaryCreationErrors.map(({ baseagent, message }) => `${baseagent}: ${message}`).join('; ')
899
+ : `active_baseagent=${primaryAgent.config.active_baseagent ?? '(unset)'}, configured=${Object.keys(primaryAgent.config.baseagents ?? {}).join(',') || '(none)'}`;
900
+ agentRuntimeError = primaryCreationErrors.length > 0
901
+ ? creationDetail
902
+ : `No agent runner created for ${primaryAgent.name}: ${creationDetail}`;
878
903
  primaryAgent.status = 'error';
879
904
  primaryAgent.error = agentRuntimeError;
880
905
  logger.error(agentRuntimeError);
@@ -927,7 +952,7 @@ async function main() {
927
952
  messageCache.cleanupExpired();
928
953
  }, 60 * 60 * 1000);
929
954
  // 渠道插件系统
930
- const channelLoader = new ChannelLoader(daemonCfg.debug);
955
+ const channelLoader = new ChannelLoader(daemonCfg.debug, daemonCfg.aun?.defaultEncrypt);
931
956
  channelLoader.register(new FeishuChannelPlugin());
932
957
  channelLoader.register(new WechatChannelPlugin());
933
958
  channelLoader.register(new AUNChannelPlugin());
@@ -1202,7 +1227,7 @@ async function main() {
1202
1227
  const effortDecision = validateRuntimeStringFieldOverride({
1203
1228
  selfAid: definition.agentAid,
1204
1229
  role: actor.role,
1205
- field: `baseagents.${baseagent}.${baseagent === 'codex' ? 'reasoning' : 'effort'}`,
1230
+ field: `baseagents.${baseagent}.effort`,
1206
1231
  value: definition.execution.effort,
1207
1232
  });
1208
1233
  if (!effortDecision.ok)
@@ -1430,7 +1455,7 @@ async function main() {
1430
1455
  originSessionId: originSession.id,
1431
1456
  originMessageId: input.runId,
1432
1457
  payload,
1433
- encrypt: false,
1458
+ encrypt: typeof channel.getDefaultEncrypt === 'function' ? channel.getDefaultEncrypt() : true,
1434
1459
  thread: target.session === 'thread' ? target.threadId : undefined,
1435
1460
  targetChatType,
1436
1461
  explicitReturnPolicy: 'none',
@@ -1871,6 +1896,78 @@ async function main() {
1871
1896
  if (inst)
1872
1897
  connectedChannels.delete(inst.adapter.channelKey);
1873
1898
  };
1899
+ // Bind the per-HOME IPC endpoint before any AUN connection is attempted.
1900
+ // A failed bind (for example, a stale Windows named pipe) must abort startup
1901
+ // without creating a second AUN session that can kick the old daemon.
1902
+ let controlChannel;
1903
+ const ipcServer = new IpcServer(resolvePaths().socket, () => {
1904
+ const channels = {};
1905
+ const channelsByType = {};
1906
+ for (const inst of channelInstances) {
1907
+ const name = inst.adapter.channelName;
1908
+ const status = inst.channel.getStatus?.() ?? { connected: true };
1909
+ const channelType = inst.channelType || name;
1910
+ channels[name] = { ...status, channelType };
1911
+ if (!channelsByType[channelType])
1912
+ channelsByType[channelType] = [];
1913
+ channelsByType[channelType].push(name);
1914
+ }
1915
+ const snap = statsCollector.getSnapshot();
1916
+ const agentListForStatus = agentRegistry.list();
1917
+ return {
1918
+ pid: process.pid,
1919
+ uptime: snap.uptimeMs,
1920
+ controlPlane: {
1921
+ ready: true,
1922
+ owned: processLevelOwners.length > 0,
1923
+ },
1924
+ agentRuntime: {
1925
+ state: agentRuntimeState,
1926
+ runnableAgents: agentListForStatus.filter((a) => a.status !== 'error' && a.status !== 'disabled').length,
1927
+ runningAgents: agentListForStatus.filter((a) => a.status === 'running').length,
1928
+ ...(agentRuntimeError ? { error: agentRuntimeError } : {}),
1929
+ },
1930
+ channels,
1931
+ channelsByType,
1932
+ queue: {
1933
+ pending: messageQueue.getGlobalQueueLength(),
1934
+ processing: messageQueue.getGlobalProcessingCount(),
1935
+ },
1936
+ stats: {
1937
+ received: snap.lastHour.received,
1938
+ sent: snap.lastHour.sent,
1939
+ completed: snap.lastHour.completed,
1940
+ errors: snap.lastHour.errors,
1941
+ avgResponseMs: snap.lastHour.avgResponseMs,
1942
+ },
1943
+ controlAid: (() => {
1944
+ if (!daemonCfg.aid)
1945
+ return undefined;
1946
+ const aidState = controlChannel?.getAidState();
1947
+ return {
1948
+ aid: daemonCfg.aid,
1949
+ connected: aidState?.status === 'connected',
1950
+ ...(aidState ? {
1951
+ status: aidState.status,
1952
+ gatewayUrl: aidState.gatewayUrl,
1953
+ reconnectCount: aidState.reconnectCount,
1954
+ lastConnectedAt: aidState.lastConnectedAt,
1955
+ lastError: aidState.lastError,
1956
+ } : {}),
1957
+ };
1958
+ })(),
1959
+ };
1960
+ }, async (cmd, sessionId, delegationToken, delegationCommandHash) => {
1961
+ const delegation = agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1962
+ if (!delegation.ok)
1963
+ return { ok: false, code: delegation.code, error: delegation.reason };
1964
+ return cmdHandler.handleCtl(cmd, sessionId);
1965
+ });
1966
+ // Register every IPC executor/provider before exposing the endpoint. The
1967
+ // function declaration is hoisted, while its invocation remains here so a
1968
+ // failed bind still aborts before any AUN connection is attempted.
1969
+ configureIpc();
1970
+ await ipcServer.start();
1874
1971
  // ── 连接所有渠道(后台首连,AUN/任意渠道故障不阻塞 daemon 主流程)──
1875
1972
  logger.info(`🚀 EvolCore core is ready; connecting ${channelInstances.length} channel(s) in background`);
1876
1973
  const connectAllPromise = channelLoader.connectAll(channelInstances, {
@@ -1913,13 +2010,14 @@ async function main() {
1913
2010
  logger.warn(`控制 AID 证书缺失:${daemonCfg.aid}(AUN 控制通道后台重连;如需重建运行 ec init)`);
1914
2011
  }
1915
2012
  }
1916
- let controlChannel;
1917
2013
  if (daemonCfg.aid) {
1918
2014
  controlChannel = new AUNChannel({
1919
2015
  aid: daemonCfg.aid,
1920
2016
  agentName: daemonCfg.aid,
1921
2017
  channelName: 'control',
1922
2018
  pureIdentity: true,
2019
+ gatewayUrl: daemonCfg.aun?.gatewayUrl,
2020
+ defaultEncrypt: daemonCfg.aun?.defaultEncrypt,
1923
2021
  aunTrace: daemonCfg.debug?.aunTrace,
1924
2022
  aunSdkLog: daemonCfg.debug?.aunSdkLog,
1925
2023
  });
@@ -1938,26 +2036,38 @@ async function main() {
1938
2036
  const ecwebPath = path.join(resolvePaths().root, 'ecweb');
1939
2037
  const ecwebEntry = path.join(ecwebPath, 'dist', 'index.js');
1940
2038
  if (fs.existsSync(ecwebEntry)) {
2039
+ let ecwebVersion;
1941
2040
  try {
1942
- const ecwebProc = spawn('node', [ecwebEntry], {
1943
- cwd: ecwebPath,
1944
- detached: true,
1945
- stdio: 'ignore',
1946
- env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
1947
- });
1948
- ecwebProc.unref();
1949
- logger.info(`✓ ECWeb 已启动 (PID: ${ecwebProc.pid})`);
1950
- onShutdown(() => {
1951
- try {
1952
- if (ecwebProc.pid)
1953
- platform.killProcess(ecwebProc.pid, true);
1954
- logger.info(`✓ ECWeb 已停止`);
1955
- }
1956
- catch { }
1957
- });
2041
+ const pkg = JSON.parse(fs.readFileSync(path.join(ecwebPath, 'package.json'), 'utf-8'));
2042
+ ecwebVersion = pkg.version;
1958
2043
  }
1959
- catch (e) {
1960
- logger.warn(`ECWeb 启动失败: ${e?.message || e}`);
2044
+ catch { }
2045
+ const versionError = ecwebVersionRequirementError(ecwebVersion);
2046
+ if (versionError) {
2047
+ logger.error(`ECWeb 未启动: ${versionError}`);
2048
+ }
2049
+ else {
2050
+ try {
2051
+ const ecwebProc = spawn('node', [ecwebEntry], {
2052
+ cwd: ecwebPath,
2053
+ detached: true,
2054
+ stdio: 'ignore',
2055
+ env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
2056
+ });
2057
+ ecwebProc.unref();
2058
+ logger.info(`✓ ECWeb 已启动 (PID: ${ecwebProc.pid})`);
2059
+ onShutdown(() => {
2060
+ try {
2061
+ if (ecwebProc.pid)
2062
+ platform.killProcess(ecwebProc.pid, true);
2063
+ logger.info(`✓ ECWeb 已停止`);
2064
+ }
2065
+ catch { }
2066
+ });
2067
+ }
2068
+ catch (e) {
2069
+ logger.warn(`ECWeb 启动失败: ${e?.message || e}`);
2070
+ }
1961
2071
  }
1962
2072
  }
1963
2073
  else {
@@ -1994,7 +2104,7 @@ async function main() {
1994
2104
  const isRoleMenu = menuControl.isMenu
1995
2105
  && (menuControl.name === 'role' || menuControl.request.cmd === '/role');
1996
2106
  const menuResponseContext = () => ({
1997
- metadata: evolMenuResponseTransportMetadata(opts.encrypted),
2107
+ metadata: evolMenuResponseTransportMetadata(),
1998
2108
  });
1999
2109
  if (menuControl.isMenu && !hasValidMenuId(menuControl)) {
2000
2110
  logger.warn(`[ControlMenu] dropped malformed request without id type=${menuControl.type}`);
@@ -2199,756 +2309,715 @@ async function main() {
2199
2309
  });
2200
2310
  }
2201
2311
  }
2202
- // IPC server — 供 CLI 查询实时状态 + Agent ctl 指令执行
2203
- const ipcServer = new IpcServer(resolvePaths().socket, () => {
2204
- const channels = {};
2205
- const channelsByType = {};
2206
- for (const inst of channelInstances) {
2207
- const name = inst.adapter.channelName;
2208
- const status = inst.channel.getStatus?.() ?? { connected: true };
2209
- const channelType = inst.channelType || name;
2210
- channels[name] = { ...status, channelType };
2211
- if (!channelsByType[channelType])
2212
- channelsByType[channelType] = [];
2213
- channelsByType[channelType].push(name);
2312
+ function configureIpc() {
2313
+ // M3: direct call (not cast) wire EvolAgentRegistry into IPC for evolagent.* handlers
2314
+ ipcServer.setAgentRegistry(agentRegistry);
2315
+ ipcServer.setDingtalkContactBindExecutor({
2316
+ register: (cmd) => registerPendingDingtalkContactBind(cmd),
2317
+ isChannelReady: isContactBindChannelReady,
2318
+ });
2319
+ ipcServer.setFeishuContactBindExecutor({
2320
+ register: (cmd) => registerPendingFeishuContactBind(cmd),
2321
+ isChannelReady: isContactBindChannelReady,
2322
+ });
2323
+ ipcServer.setQQBotContactBindExecutor({
2324
+ register: (cmd) => registerPendingQQBotContactBind(cmd),
2325
+ isChannelReady: isContactBindChannelReady,
2326
+ });
2327
+ ipcServer.setWecomContactBindExecutor({
2328
+ register: (cmd) => registerPendingWecomContactBind(cmd),
2329
+ isChannelReady: isContactBindChannelReady,
2330
+ });
2331
+ ipcServer.setWechatContactBindExecutor({
2332
+ register: (cmd) => registerPendingWechatContactBind(cmd),
2333
+ isChannelReady: isContactBindChannelReady,
2334
+ });
2335
+ ipcServer.setMenuExecutor((payload, auth) => cmdHandler.execMenuForEcweb(payload, auth));
2336
+ ipcServer.setConfigOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleConfigOperation(argv, sessionId, delegationToken, delegationCommandHash));
2337
+ ipcServer.setContactOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleContactOperation(argv, sessionId, delegationToken, delegationCommandHash));
2338
+ cmdHandler.setDaemonStatusProvider(() => {
2339
+ const aidState = controlChannel?.getAidState?.();
2340
+ return {
2341
+ aid: daemonCfg.aid ?? null,
2342
+ aun: aidState ? {
2343
+ connected: aidState.status === 'connected',
2344
+ status: aidState.status,
2345
+ reconnectCount: aidState.reconnectCount ?? 0,
2346
+ flapCount: aidState.flapCount ?? 0,
2347
+ ...(aidState.lastError ? { lastError: String(aidState.lastError).slice(0, 80) } : {}),
2348
+ ...(aidState.kickDetail?.reason ? { kickReason: String(aidState.kickDetail.reason).slice(0, 80) } : {}),
2349
+ } : {
2350
+ connected: false,
2351
+ status: daemonCfg.aid ? 'disconnected' : 'disabled',
2352
+ },
2353
+ };
2354
+ });
2355
+ if (bindService) {
2356
+ ipcServer.setBindExecutor({
2357
+ begin: (cmd) => bindService.begin(cmd),
2358
+ status: (taskId) => bindService.status(taskId),
2359
+ cancel: (taskId) => bindService.cancel(taskId),
2360
+ });
2214
2361
  }
2215
- const snap = statsCollector.getSnapshot();
2216
- const agentListForStatus = agentRegistry.list();
2217
- return {
2218
- pid: process.pid,
2219
- uptime: snap.uptimeMs,
2220
- controlPlane: {
2221
- ready: true,
2222
- owned: processLevelOwners.length > 0,
2223
- },
2224
- agentRuntime: {
2225
- state: agentRuntimeState,
2226
- runnableAgents: agentListForStatus.filter((a) => a.status !== 'error' && a.status !== 'disabled').length,
2227
- runningAgents: agentListForStatus.filter((a) => a.status === 'running').length,
2228
- ...(agentRuntimeError ? { error: agentRuntimeError } : {}),
2229
- },
2230
- channels,
2231
- channelsByType,
2232
- queue: {
2233
- pending: messageQueue.getGlobalQueueLength(),
2234
- processing: messageQueue.getGlobalProcessingCount(),
2235
- },
2236
- stats: {
2237
- received: snap.lastHour.received,
2238
- sent: snap.lastHour.sent,
2239
- completed: snap.lastHour.completed,
2240
- errors: snap.lastHour.errors,
2241
- avgResponseMs: snap.lastHour.avgResponseMs,
2242
- },
2243
- controlAid: daemonCfg.aid
2244
- ? { aid: daemonCfg.aid, connected: controlChannel?.getAidState().status === 'connected' }
2245
- : undefined,
2246
- };
2247
- }, async (cmd, sessionId, delegationToken, delegationCommandHash) => {
2248
- const delegation = agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
2249
- if (!delegation.ok)
2250
- return { ok: false, code: delegation.code, error: delegation.reason };
2251
- return cmdHandler.handleCtl(cmd, sessionId);
2252
- });
2253
- // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
2254
- ipcServer.setAgentRegistry(agentRegistry);
2255
- ipcServer.setDingtalkContactBindExecutor({
2256
- register: (cmd) => registerPendingDingtalkContactBind(cmd),
2257
- isChannelReady: isContactBindChannelReady,
2258
- });
2259
- ipcServer.setFeishuContactBindExecutor({
2260
- register: (cmd) => registerPendingFeishuContactBind(cmd),
2261
- isChannelReady: isContactBindChannelReady,
2262
- });
2263
- ipcServer.setQQBotContactBindExecutor({
2264
- register: (cmd) => registerPendingQQBotContactBind(cmd),
2265
- isChannelReady: isContactBindChannelReady,
2266
- });
2267
- ipcServer.setWecomContactBindExecutor({
2268
- register: (cmd) => registerPendingWecomContactBind(cmd),
2269
- isChannelReady: isContactBindChannelReady,
2270
- });
2271
- ipcServer.setWechatContactBindExecutor({
2272
- register: (cmd) => registerPendingWechatContactBind(cmd),
2273
- isChannelReady: isContactBindChannelReady,
2274
- });
2275
- ipcServer.setMenuExecutor((payload, auth) => cmdHandler.execMenuForEcweb(payload, auth));
2276
- ipcServer.setConfigOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleConfigOperation(argv, sessionId, delegationToken, delegationCommandHash));
2277
- ipcServer.setContactOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleContactOperation(argv, sessionId, delegationToken, delegationCommandHash));
2278
- cmdHandler.setDaemonStatusProvider(() => {
2279
- const aidState = controlChannel?.getAidState?.();
2280
- return {
2281
- aid: daemonCfg.aid ?? null,
2282
- aun: aidState ? {
2283
- connected: aidState.status === 'connected',
2284
- status: aidState.status,
2285
- reconnectCount: aidState.reconnectCount ?? 0,
2286
- flapCount: aidState.flapCount ?? 0,
2287
- ...(aidState.lastError ? { lastError: String(aidState.lastError).slice(0, 80) } : {}),
2288
- ...(aidState.kickDetail?.reason ? { kickReason: String(aidState.kickDetail.reason).slice(0, 80) } : {}),
2289
- } : {
2290
- connected: false,
2291
- status: daemonCfg.aid ? 'disconnected' : 'disabled',
2292
- },
2293
- };
2294
- });
2295
- if (bindService) {
2296
- ipcServer.setBindExecutor({
2297
- begin: (cmd) => bindService.begin(cmd),
2298
- status: (taskId) => bindService.status(taskId),
2299
- cancel: (taskId) => bindService.cancel(taskId),
2362
+ // 注入 AUN AID 状态聚合器:遍历所有 aun 类型 channel,调 getAidState() 收集
2363
+ ipcServer.setAunAidProvider(() => {
2364
+ const out = [];
2365
+ for (const inst of channelInstances) {
2366
+ if (inst.channelType !== 'aun')
2367
+ continue;
2368
+ const ch = inst.channel;
2369
+ if (typeof ch?.getAidState === 'function') {
2370
+ try {
2371
+ const aidState = ch.getAidState();
2372
+ // 增强:添加队列状态
2373
+ const agentName = aidState.agentName || aidState.aid;
2374
+ const processing = messageQueue.getProcessingCountByAgent(agentName);
2375
+ const queued = messageQueue.getQueueLengthByAgent(agentName);
2376
+ out.push({
2377
+ ...aidState,
2378
+ queueStatus: { processing, queued }
2379
+ });
2380
+ }
2381
+ catch { /* ignore */ }
2382
+ }
2383
+ }
2384
+ return out;
2300
2385
  });
2301
- }
2302
- // 注入 AUN AID 状态聚合器:遍历所有 aun 类型 channel,调 getAidState() 收集
2303
- ipcServer.setAunAidProvider(() => {
2304
- const out = [];
2386
+ // 注入 Per-AID 统计收集器到所有 AUN channel 实例
2305
2387
  for (const inst of channelInstances) {
2306
2388
  if (inst.channelType !== 'aun')
2307
2389
  continue;
2308
2390
  const ch = inst.channel;
2309
- if (typeof ch?.getAidState === 'function') {
2310
- try {
2311
- const aidState = ch.getAidState();
2312
- // 增强:添加队列状态
2313
- const agentName = aidState.agentName || aidState.aid;
2314
- const processing = messageQueue.getProcessingCountByAgent(agentName);
2315
- const queued = messageQueue.getQueueLengthByAgent(agentName);
2316
- out.push({
2317
- ...aidState,
2318
- queueStatus: { processing, queued }
2319
- });
2320
- }
2321
- catch { /* ignore */ }
2322
- }
2323
- }
2324
- return out;
2325
- });
2326
- // 注入 Per-AID 统计收集器到所有 AUN channel 实例
2327
- for (const inst of channelInstances) {
2328
- if (inst.channelType !== 'aun')
2329
- continue;
2330
- const ch = inst.channel;
2331
- if (typeof ch?.setAidStatsCollector === 'function') {
2332
- ch.setAidStatsCollector(aidStatsCollector);
2333
- }
2334
- }
2335
- // 注入 Per-AID 统计 IPC provider
2336
- aidStatsCollector.setQueueStatsProvider((agentName) => ({
2337
- processing: messageQueue.getProcessingCountByAgent(agentName),
2338
- queued: messageQueue.getQueueLengthByAgent(agentName),
2339
- muted: messageQueue.isAgentMuted(agentName),
2340
- }));
2341
- ipcServer.setAunAidStatsProvider(() => aidStatsCollector.getAllSnapshots());
2342
- ipcServer.setAunAidStatsRecorder((params) => {
2343
- aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode, 'send');
2344
- });
2345
- ipcServer.setTaskRuntimeContextProvider(({ sessionId }) => responseEngine.getTaskRuntimeContext(sessionId));
2346
- ipcServer.setHandoffReturnExecutor((params) => responseEngine.returnHandoffResult(params));
2347
- ipcServer.setHandoffStatusExecutor(async (params) => {
2348
- if (!params.sessionId)
2349
- return { ok: false, code: 'HANDOFF_CALL_SESSION_REQUIRED', error: 'current session is required' };
2350
- const runtime = responseEngine.getTaskRuntimeContext(params.sessionId);
2351
- const session = await sessionManager.getSessionById(params.sessionId);
2352
- const selfAid = runtime?.selfAid || session?.selfAID;
2353
- if (!selfAid)
2354
- return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2355
- return handoffRuntime.status(selfAid, params.handoffId)
2356
- ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2357
- });
2358
- const resolveHandoffQueryAid = async (params) => {
2359
- if (params.sessionId) {
2391
+ if (typeof ch?.setAidStatsCollector === 'function') {
2392
+ ch.setAidStatsCollector(aidStatsCollector);
2393
+ }
2394
+ }
2395
+ // 注入 Per-AID 统计 IPC provider
2396
+ aidStatsCollector.setQueueStatsProvider((agentName) => ({
2397
+ processing: messageQueue.getProcessingCountByAgent(agentName),
2398
+ queued: messageQueue.getQueueLengthByAgent(agentName),
2399
+ muted: messageQueue.isAgentMuted(agentName),
2400
+ }));
2401
+ ipcServer.setAunAidStatsProvider(() => aidStatsCollector.getAllSnapshots());
2402
+ ipcServer.setAunAidStatsRecorder((params) => {
2403
+ aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode, 'send');
2404
+ });
2405
+ 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' };
2360
2410
  const runtime = responseEngine.getTaskRuntimeContext(params.sessionId);
2361
2411
  const session = await sessionManager.getSessionById(params.sessionId);
2362
2412
  const selfAid = runtime?.selfAid || session?.selfAID;
2363
- if (!selfAid) {
2364
- return { ok: false, code: 'HANDOFF_CALL_SESSION_INVALID', error: 'current session is invalid' };
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) => {
2419
+ 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 };
2365
2430
  }
2366
- if (params.agent && params.agent !== selfAid) {
2367
- return { ok: false, code: 'HANDOFF_AGENT_SCOPE_MISMATCH', error: 'agent does not match the current session' };
2431
+ if (!params.agent) {
2432
+ return { ok: false, code: 'HANDOFF_AGENT_REQUIRED', error: '--agent is required outside a task context' };
2368
2433
  }
2369
- return { ok: true, aid: selfAid };
2370
- }
2371
- if (!params.agent) {
2372
- return { ok: false, code: 'HANDOFF_AGENT_REQUIRED', error: '--agent is required outside a task context' };
2373
- }
2374
- if (!agentRegistry.get(params.agent)) {
2375
- return { ok: false, code: 'HANDOFF_AGENT_NOT_FOUND', error: `agent not found: ${params.agent}` };
2376
- }
2377
- return { ok: true, aid: params.agent };
2378
- };
2379
- ipcServer.setHandoffListExecutor(async (params) => {
2380
- const scope = await resolveHandoffQueryAid(params);
2381
- if (!scope.ok)
2382
- return scope;
2383
- return handoffRuntime.listHandoffs({
2384
- selfAid: scope.aid,
2385
- state: params.state,
2386
- sessionId: params.filterSessionId,
2387
- limit: params.limit,
2434
+ if (!agentRegistry.get(params.agent)) {
2435
+ return { ok: false, code: 'HANDOFF_AGENT_NOT_FOUND', error: `agent not found: ${params.agent}` };
2436
+ }
2437
+ return { ok: true, aid: params.agent };
2438
+ };
2439
+ ipcServer.setHandoffListExecutor(async (params) => {
2440
+ const scope = await resolveHandoffQueryAid(params);
2441
+ if (!scope.ok)
2442
+ return scope;
2443
+ return handoffRuntime.listHandoffs({
2444
+ selfAid: scope.aid,
2445
+ state: params.state,
2446
+ sessionId: params.filterSessionId,
2447
+ limit: params.limit,
2448
+ });
2388
2449
  });
2389
- });
2390
- ipcServer.setHandoffTraceExecutor(async (params) => {
2391
- const scope = await resolveHandoffQueryAid(params);
2392
- if (!scope.ok)
2393
- return scope;
2394
- return handoffRuntime.traceHandoff(scope.aid, params.handoffId, params.limit)
2395
- ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2396
- });
2397
- ipcServer.setAunMsgSender(async (params) => {
2398
- const runtimeCausation = params.originSessionId
2399
- ? responseEngine.getTaskRuntimeContext(params.originSessionId)?.causation
2400
- : undefined;
2401
- const delegation = authorizeDelegatedAunMsgSend(agentDelegationRegistry, {
2402
- delegationToken: params.delegationToken,
2403
- delegationCommandHash: params.delegationCommandHash,
2404
- sessionId: params.originSessionId,
2405
- messageId: params.originMessageId,
2406
- aid: params.aid,
2407
- to: params.to,
2408
- scope: params.scope,
2409
- action: params.file ? 'file' : 'send',
2450
+ ipcServer.setHandoffTraceExecutor(async (params) => {
2451
+ const scope = await resolveHandoffQueryAid(params);
2452
+ if (!scope.ok)
2453
+ return scope;
2454
+ return handoffRuntime.traceHandoff(scope.aid, params.handoffId, params.limit)
2455
+ ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2410
2456
  });
2411
- if (!delegation.ok) {
2412
- return { ok: false, error: delegation.reason, code: delegation.code };
2413
- }
2414
- const inst = channelInstances.find((candidate) => {
2415
- if (candidate.channelType !== 'aun')
2416
- return false;
2417
- const ch = candidate.channel;
2418
- try {
2419
- const aidState = typeof ch?.getAidState === 'function' ? ch.getAidState() : null;
2420
- if (aidState?.aid === params.aid)
2421
- return true;
2422
- if (typeof ch?.getAid === 'function' && ch.getAid() === params.aid)
2423
- return true;
2457
+ ipcServer.setAunMsgSender(async (params) => {
2458
+ const runtimeCausation = params.originSessionId
2459
+ ? responseEngine.getTaskRuntimeContext(params.originSessionId)?.causation
2460
+ : undefined;
2461
+ const delegation = authorizeDelegatedAunMsgSend(agentDelegationRegistry, {
2462
+ delegationToken: params.delegationToken,
2463
+ delegationCommandHash: params.delegationCommandHash,
2464
+ sessionId: params.originSessionId,
2465
+ messageId: params.originMessageId,
2466
+ aid: params.aid,
2467
+ to: params.to,
2468
+ scope: params.scope,
2469
+ action: params.file ? 'file' : 'send',
2470
+ });
2471
+ if (!delegation.ok) {
2472
+ return { ok: false, error: delegation.reason, code: delegation.code };
2424
2473
  }
2425
- catch { /* ignore */ }
2426
- return false;
2427
- });
2428
- const ch = inst?.channel;
2429
- if (!ch) {
2430
- return { ok: false, error: `AUN channel not found for ${params.aid}`, code: 'AUN_CHANNEL_NOT_FOUND' };
2431
- }
2432
- const targetIsGroup = typeof ch.isGroupId === 'function' && ch.isGroupId(params.to);
2433
- if ((params.scope === 'group') !== targetIsGroup) {
2434
- return { ok: false, error: 'AUN target does not match the delegated send scope', code: 'UNSUPPORTED_TARGET' };
2435
- }
2436
- let payload = params.payload;
2437
- if (params.file) {
2438
- if (isHClassPath(params.file.filePath)) {
2439
- return {
2440
- ok: false,
2441
- error: 'H-class protected files cannot be uploaded by an agent task',
2442
- code: 'H_CLASS_PROTECTED',
2443
- };
2474
+ const inst = channelInstances.find((candidate) => {
2475
+ if (candidate.channelType !== 'aun')
2476
+ return false;
2477
+ const ch = candidate.channel;
2478
+ try {
2479
+ const aidState = typeof ch?.getAidState === 'function' ? ch.getAidState() : null;
2480
+ if (aidState?.aid === params.aid)
2481
+ return true;
2482
+ if (typeof ch?.getAid === 'function' && ch.getAid() === params.aid)
2483
+ return true;
2484
+ }
2485
+ catch { /* ignore */ }
2486
+ return false;
2487
+ });
2488
+ const ch = inst?.channel;
2489
+ if (!ch) {
2490
+ return { ok: false, error: `AUN channel not found for ${params.aid}`, code: 'AUN_CHANNEL_NOT_FOUND' };
2491
+ }
2492
+ const targetIsGroup = typeof ch.isGroupId === 'function' && ch.isGroupId(params.to);
2493
+ if ((params.scope === 'group') !== targetIsGroup) {
2494
+ return { ok: false, error: 'AUN target does not match the delegated send scope', code: 'UNSUPPORTED_TARGET' };
2495
+ }
2496
+ const encrypt = typeof params.encrypt === 'boolean'
2497
+ ? params.encrypt
2498
+ : (typeof ch.getDefaultEncrypt === 'function' ? ch.getDefaultEncrypt() : true);
2499
+ let payload = params.payload;
2500
+ if (params.file) {
2501
+ if (isHClassPath(params.file.filePath)) {
2502
+ return {
2503
+ ok: false,
2504
+ error: 'H-class protected files cannot be uploaded by an agent task',
2505
+ code: 'H_CLASS_PROTECTED',
2506
+ };
2507
+ }
2508
+ if (typeof ch.buildDaemonFilePayload !== 'function') {
2509
+ return { ok: false, error: 'AUN channel does not support daemon file uploads', code: 'AUN_FILE_UNSUPPORTED' };
2510
+ }
2511
+ try {
2512
+ payload = await ch.buildDaemonFilePayload(params.file);
2513
+ }
2514
+ catch (error) {
2515
+ return {
2516
+ ok: false,
2517
+ error: error instanceof Error ? error.message : String(error),
2518
+ code: error?.code ?? 'AUN_FILE_UPLOAD_FAILED',
2519
+ };
2520
+ }
2444
2521
  }
2445
- if (typeof ch.buildDaemonFilePayload !== 'function') {
2446
- return { ok: false, error: 'AUN channel does not support daemon file uploads', code: 'AUN_FILE_UNSUPPORTED' };
2522
+ if (!payload) {
2523
+ return { ok: false, error: 'message payload is required', code: 'INVALID_PAYLOAD' };
2447
2524
  }
2448
- try {
2449
- payload = await ch.buildDaemonFilePayload(params.file);
2525
+ if (params.thread && typeof payload.thread_id !== 'string') {
2526
+ payload = { ...payload, thread_id: params.thread };
2450
2527
  }
2451
- catch (error) {
2452
- return {
2453
- ok: false,
2454
- error: error instanceof Error ? error.message : String(error),
2455
- code: error?.code ?? 'AUN_FILE_UPLOAD_FAILED',
2456
- };
2528
+ if (params.scope === 'group' && params.mentions?.length) {
2529
+ payload = { ...payload, mentions: params.mentions };
2457
2530
  }
2458
- }
2459
- if (!payload) {
2460
- return { ok: false, error: 'message payload is required', code: 'INVALID_PAYLOAD' };
2461
- }
2462
- if (params.thread && typeof payload.thread_id !== 'string') {
2463
- payload = { ...payload, thread_id: params.thread };
2464
- }
2465
- if (params.scope === 'group' && params.mentions?.length) {
2466
- payload = { ...payload, mentions: params.mentions };
2467
- }
2468
- let targetSessionId;
2469
- if (params.originSessionId && params.originMessageId) {
2470
- try {
2471
- const created = await handoffRuntime.createOutbound({
2472
- selfAid: params.aid,
2473
- to: params.to,
2474
- originSessionId: params.originSessionId,
2475
- originMessageId: params.originMessageId,
2476
- payload,
2477
- encrypt: params.encrypt === true,
2478
- thread: params.thread,
2479
- targetChatType: params.scope === 'group' ? 'group' : 'private',
2480
- explicitReturnPolicy: params.returnPolicy,
2481
- originAuthorization: {
2482
- actorId: delegation.grant.actorId,
2483
- channelKey: delegation.grant.channel,
2484
- channelType: delegation.grant.channelType,
2485
- chatType: delegation.grant.chatType,
2486
- peerKey: delegation.grant.peerKey,
2487
- },
2488
- causation: runtimeCausation,
2489
- });
2490
- targetSessionId = created.targetSession.id;
2491
- if (created.crossSession && created.handoff) {
2531
+ let targetSessionId;
2532
+ if (params.originSessionId && params.originMessageId) {
2533
+ try {
2534
+ const created = await handoffRuntime.createOutbound({
2535
+ selfAid: params.aid,
2536
+ to: params.to,
2537
+ originSessionId: params.originSessionId,
2538
+ originMessageId: params.originMessageId,
2539
+ payload,
2540
+ encrypt,
2541
+ thread: params.thread,
2542
+ targetChatType: params.scope === 'group' ? 'group' : 'private',
2543
+ explicitReturnPolicy: params.returnPolicy,
2544
+ originAuthorization: {
2545
+ actorId: delegation.grant.actorId,
2546
+ channelKey: delegation.grant.channel,
2547
+ channelType: delegation.grant.channelType,
2548
+ chatType: delegation.grant.chatType,
2549
+ peerKey: delegation.grant.peerKey,
2550
+ },
2551
+ causation: runtimeCausation,
2552
+ });
2553
+ targetSessionId = created.targetSession.id;
2554
+ if (created.crossSession && created.handoff) {
2555
+ return {
2556
+ ok: true,
2557
+ status: created.handoff.state === 'target_sent' ? 'delivered' : 'queued',
2558
+ message_id: created.handoff.target_message_id ?? undefined,
2559
+ handoff_id: created.handoff.handoff_id,
2560
+ target_session_id: created.targetSession.id,
2561
+ };
2562
+ }
2563
+ if (params.scope === 'msg' && !payload.ref_message_id) {
2564
+ payload.ref_message_id = params.originMessageId;
2565
+ }
2566
+ }
2567
+ catch (error) {
2568
+ const code = error?.code;
2569
+ const handoffId = error?.handoffId
2570
+ ?? error?.blockingHandoffId;
2492
2571
  return {
2493
- ok: true,
2494
- status: created.handoff.state === 'target_sent' ? 'delivered' : 'queued',
2495
- message_id: created.handoff.target_message_id ?? undefined,
2496
- handoff_id: created.handoff.handoff_id,
2497
- target_session_id: created.targetSession.id,
2572
+ ok: false,
2573
+ error: error instanceof Error ? error.message : String(error),
2574
+ code,
2575
+ ...(handoffId ? { handoff_id: handoffId } : {}),
2498
2576
  };
2499
2577
  }
2500
- if (params.scope === 'msg' && !payload.ref_message_id) {
2501
- payload.ref_message_id = params.originMessageId;
2502
- }
2503
2578
  }
2504
- catch (error) {
2505
- const code = error?.code;
2506
- const handoffId = error?.handoffId
2507
- ?? error?.blockingHandoffId;
2508
- return {
2509
- ok: false,
2510
- error: error instanceof Error ? error.message : String(error),
2511
- code,
2512
- ...(handoffId ? { handoff_id: handoffId } : {}),
2513
- };
2579
+ if (params.scope === 'group') {
2580
+ if (typeof ch.sendDaemonGroupMsg !== 'function') {
2581
+ return { ok: false, error: 'AUN channel does not support daemon group sends', code: 'AUN_GROUP_UNSUPPORTED' };
2582
+ }
2583
+ return await ch.sendDaemonGroupMsg({
2584
+ groupId: params.to,
2585
+ payload,
2586
+ mentions: params.mentions,
2587
+ encrypt,
2588
+ log: params.log ? { ...params.log, sessionId: targetSessionId ?? params.log.sessionId } : undefined,
2589
+ });
2514
2590
  }
2515
- }
2516
- if (params.scope === 'group') {
2517
- if (typeof ch.sendDaemonGroupMsg !== 'function') {
2518
- return { ok: false, error: 'AUN channel does not support daemon group sends', code: 'AUN_GROUP_UNSUPPORTED' };
2591
+ if (typeof ch.sendDaemonMsg !== 'function') {
2592
+ return { ok: false, error: 'AUN channel does not support daemon private sends', code: 'AUN_MSG_UNSUPPORTED' };
2519
2593
  }
2520
- return await ch.sendDaemonGroupMsg({
2521
- groupId: params.to,
2594
+ return await ch.sendDaemonMsg({
2595
+ to: params.to,
2522
2596
  payload,
2523
- mentions: params.mentions,
2524
- encrypt: params.encrypt,
2597
+ encrypt,
2598
+ causation: runtimeCausation,
2525
2599
  log: params.log ? { ...params.log, sessionId: targetSessionId ?? params.log.sessionId } : undefined,
2526
2600
  });
2527
- }
2528
- if (typeof ch.sendDaemonMsg !== 'function') {
2529
- return { ok: false, error: 'AUN channel does not support daemon private sends', code: 'AUN_MSG_UNSUPPORTED' };
2530
- }
2531
- return await ch.sendDaemonMsg({
2532
- to: params.to,
2533
- payload,
2534
- encrypt: params.encrypt,
2535
- causation: runtimeCausation,
2536
- log: params.log ? { ...params.log, sessionId: targetSessionId ?? params.log.sessionId } : undefined,
2537
2601
  });
2538
- });
2539
- // ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
2540
- const reloadHooks = buildReloadHooks({
2541
- channelLoader,
2542
- channelInstances,
2543
- registerChannelInstance,
2544
- unregisterChannelInstance: (channelName) => {
2545
- markChannelDisconnected(channelName);
2546
- processor.unregisterChannel(channelName);
2547
- cmdHandler.unregisterChannel(channelName);
2548
- msgBridge.removeChannel(channelName);
2549
- },
2550
- onChannelStarted: (inst) => {
2551
- // startChannel 重建渠道时重新注入 AidStatsCollector(与 hot-load 路径对齐)
2552
- if (inst.channelType === 'aun') {
2553
- const ch = inst.channel;
2554
- if (typeof ch?.setAidStatsCollector === 'function')
2555
- ch.setAidStatsCollector(aidStatsCollector);
2556
- }
2557
- },
2558
- onChannelConnected: markChannelConnected,
2559
- messageQueue,
2560
- handoffRuntime,
2561
- });
2562
- // Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
2563
- globalThis.__evolcore_reloadHooks = reloadHooks;
2564
- // Hot-load handler: dynamically add a new agent at runtime
2565
- globalThis.__evolcore_hotLoadAgent = async (aid) => {
2566
- handoffRuntime.pauseAgent(aid);
2567
- let resumed = false;
2568
- try {
2569
- agentRuntimeState = 'starting';
2570
- agentRuntimeError = undefined;
2571
- const agent = agentRegistry.loadNewAgent(aid);
2572
- if (!agent) {
2573
- agentRuntimeState = agentRegistry.runnableAgents().length > 0 ? 'running' : 'error';
2574
- agentRuntimeError = `Failed to load agent ${aid}`;
2575
- throw new Error(agentRuntimeError);
2576
- }
2577
- const newAgentInstances = agentLoader.createForAgent(agent, {
2578
- onSessionIdUpdate: async (sessionId, agentSessionId) => {
2579
- await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
2580
- },
2581
- });
2582
- for (const inst of newAgentInstances) {
2583
- agentMap.set(`${inst.evolagentName}::${inst.baseagent}`, inst.agent);
2584
- inst.agent.setPermissionGateway?.(permissionGateway);
2585
- inst.agent.setCompactStartCallback?.((sessionId) => {
2586
- processor.handleCompactStart(sessionId);
2587
- });
2588
- }
2589
- if (newAgentInstances.length === 0) {
2590
- agent.status = 'error';
2591
- agent.error = 'No baseagent runner created for hot-loaded agent';
2592
- agentRuntimeState = 'error';
2593
- agentRuntimeError = agent.error;
2594
- throw new Error(agent.error);
2595
- }
2596
- // 创建 channels
2597
- const instances = await channelLoader.createForAgent(agent);
2598
- for (const inst of instances) {
2599
- registerChannelInstance(inst);
2602
+ // ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
2603
+ const reloadHooks = buildReloadHooks({
2604
+ channelLoader,
2605
+ channelInstances,
2606
+ registerChannelInstance,
2607
+ unregisterChannelInstance: (channelName) => {
2608
+ markChannelDisconnected(channelName);
2609
+ processor.unregisterChannel(channelName);
2610
+ cmdHandler.unregisterChannel(channelName);
2611
+ msgBridge.removeChannel(channelName);
2612
+ },
2613
+ onChannelStarted: (inst) => {
2614
+ // startChannel 重建渠道时重新注入 AidStatsCollector(与 hot-load 路径对齐)
2600
2615
  if (inst.channelType === 'aun') {
2601
2616
  const ch = inst.channel;
2602
2617
  if (typeof ch?.setAidStatsCollector === 'function')
2603
2618
  ch.setAidStatsCollector(aidStatsCollector);
2604
2619
  }
2605
- agent.channels.set(inst.adapter.channelKey, inst.adapter);
2606
- channelInstances.push(inst);
2607
- }
2608
- agent.status = 'running';
2609
- // 连接
2610
- await channelLoader.connectAll(instances, { onConnected: markChannelConnected });
2611
- await handoffRuntime.recover([aid]);
2612
- await ensureTriggerSchedulerStarted(agent);
2613
- handoffRuntime.resumeAgent(aid);
2614
- resumed = true;
2615
- agentRuntimeState = 'running';
2616
- agentRuntimeError = undefined;
2617
- logger.info(`[HotLoad] Agent ${aid} online with ${instances.length} channel(s)`);
2618
- }
2619
- finally {
2620
- if (!resumed)
2621
- handoffRuntime.resumeAgent(aid);
2622
- }
2623
- };
2624
- // Full resync handler: scan disk, load new agents, unload removed/disabled, reload changed
2625
- globalThis.__evolcore_resyncAgents = async () => {
2626
- // 先清 kit 缓存:'kits' 组(manifest / fragment / schema / 角色模板)走 on-reload
2627
- // 策略,平时不查盘。放在扫盘与 reload 之前,本轮上下线的 agent 才能读到磁盘上的
2628
- // 最新版本——放在末尾的话,这一轮全用旧值,改动要等下一次 resync 才生效。
2629
- invalidateKitCache();
2630
- const { loadAllAgents: scanAgents, loadDefaults: readDefaults } = await import('./config-store.js');
2631
- const { resolveEffective } = await import('./config/config-manager.js');
2632
- const freshDefaults = readDefaults();
2633
- const { agents: diskAgents } = scanAgents();
2634
- const diskAidSet = new Set(diskAgents.map(a => a.aid));
2635
- const results = [];
2636
- // 1. 下线:运行时有但磁盘上没有 / disabled 的
2637
- for (const [aid, agent] of [...agentRegistry.agents.entries()]) {
2638
- const diskCfg = diskAgents.find(a => a.aid === aid);
2639
- if (!diskCfg || diskCfg.enabled === false) {
2640
- handoffRuntime.pauseAgent(aid);
2641
- try {
2642
- await handoffRuntime.drainAgent(aid);
2620
+ },
2621
+ onChannelConnected: markChannelConnected,
2622
+ messageQueue,
2623
+ handoffRuntime,
2624
+ });
2625
+ // Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
2626
+ globalThis.__evolcore_reloadHooks = reloadHooks;
2627
+ // Hot-load handler: dynamically add a new agent at runtime
2628
+ globalThis.__evolcore_hotLoadAgent = async (aid) => {
2629
+ handoffRuntime.pauseAgent(aid);
2630
+ let resumed = false;
2631
+ try {
2632
+ agentRuntimeState = 'starting';
2633
+ agentRuntimeError = undefined;
2634
+ const agent = agentRegistry.loadNewAgent(aid);
2635
+ if (!agent) {
2636
+ agentRuntimeState = agentRegistry.runnableAgents().length > 0 ? 'running' : 'error';
2637
+ agentRuntimeError = `Failed to load agent ${aid}`;
2638
+ throw new Error(agentRuntimeError);
2643
2639
  }
2644
- catch (error) {
2645
- handoffRuntime.resumeAgent(aid);
2646
- results.push(`⚠ ${aid}: ${error instanceof Error ? error.message : String(error)}`);
2647
- continue;
2640
+ const creationErrors = [];
2641
+ const newAgentInstances = agentLoader.createForAgent(agent, {
2642
+ onSessionIdUpdate: async (sessionId, agentSessionId) => {
2643
+ await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
2644
+ },
2645
+ }, creationErrors);
2646
+ for (const inst of newAgentInstances) {
2647
+ agentMap.set(`${inst.evolagentName}::${inst.baseagent}`, inst.agent);
2648
+ inst.agent.setPermissionGateway?.(permissionGateway);
2649
+ inst.agent.setCompactStartCallback?.((sessionId) => {
2650
+ processor.handleCompactStart(sessionId);
2651
+ });
2648
2652
  }
2649
- await triggerSchedulers.get(aid)?.stop();
2650
- triggerSchedulers.delete(aid);
2651
- triggerSchedulerStarts.delete(aid);
2652
- // 断开所有 channels
2653
- for (const chName of agent.channelInstanceNames()) {
2654
- const inst = channelInstances.find(i => i.adapter.channelName === chName);
2655
- if (inst) {
2656
- try {
2657
- await inst.disconnect();
2658
- }
2659
- catch { }
2660
- markChannelDisconnected(inst.adapter.channelName);
2661
- const idx = channelInstances.indexOf(inst);
2662
- if (idx >= 0)
2663
- channelInstances.splice(idx, 1);
2653
+ if (newAgentInstances.length === 0) {
2654
+ const creationDetail = creationErrors.length > 0
2655
+ ? creationErrors.map(({ baseagent, message }) => `${baseagent}: ${message}`).join('; ')
2656
+ : `active_baseagent=${agent.config.active_baseagent ?? '(unset)'}, configured=${Object.keys(agent.config.baseagents ?? {}).join(',') || '(none)'}`;
2657
+ agent.status = 'error';
2658
+ agent.error = creationErrors.length > 0
2659
+ ? creationDetail
2660
+ : `No baseagent runner created for hot-loaded agent: ${creationDetail}`;
2661
+ agentRuntimeState = 'error';
2662
+ agentRuntimeError = agent.error;
2663
+ throw new Error(agent.error);
2664
+ }
2665
+ // 创建 channels
2666
+ const instances = await channelLoader.createForAgent(agent);
2667
+ for (const inst of instances) {
2668
+ registerChannelInstance(inst);
2669
+ if (inst.channelType === 'aun') {
2670
+ const ch = inst.channel;
2671
+ if (typeof ch?.setAidStatsCollector === 'function')
2672
+ ch.setAidStatsCollector(aidStatsCollector);
2664
2673
  }
2674
+ agent.channels.set(inst.adapter.channelKey, inst.adapter);
2675
+ channelInstances.push(inst);
2665
2676
  }
2666
- agentRegistry.agents.delete(aid);
2667
- results.push(`- ${aid} (offline)`);
2668
- continue;
2669
- }
2670
- }
2671
- // 2. 新增:磁盘上有但运行时没有的
2672
- for (const cfg of diskAgents) {
2673
- if (cfg.enabled === false)
2674
- continue;
2675
- if (agentRegistry.agents.has(cfg.aid))
2676
- continue;
2677
- try {
2678
- await globalThis.__evolcore_hotLoadAgent(cfg.aid);
2679
- results.push(`+ ${cfg.aid} (online)`);
2680
- }
2681
- catch (e) {
2682
- results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
2683
- }
2684
- }
2685
- // 3. 已有的:重新 reload(config 可能改了)
2686
- const hooks = globalThis.__evolcore_reloadHooks;
2687
- for (const cfg of diskAgents) {
2688
- if (cfg.enabled === false)
2689
- continue;
2690
- if (!agentRegistry.agents.has(cfg.aid))
2691
- continue;
2692
- // 只有磁盘上存在且运行时也存在的才 reload
2693
- try {
2694
- await agentRegistry.reload(cfg.aid, hooks);
2695
- const runtimeAgent = agentRegistry.get(cfg.aid);
2696
- if (runtimeAgent)
2697
- await startTriggerScheduler(runtimeAgent);
2698
- results.push(`↻ ${cfg.aid} (reloaded)`);
2677
+ agent.status = 'running';
2678
+ // 连接
2679
+ await channelLoader.connectAll(instances, { onConnected: markChannelConnected });
2680
+ await handoffRuntime.recover([aid]);
2681
+ await ensureTriggerSchedulerStarted(agent);
2682
+ handoffRuntime.resumeAgent(aid);
2683
+ resumed = true;
2684
+ agentRuntimeState = 'running';
2685
+ agentRuntimeError = undefined;
2686
+ logger.info(`[HotLoad] ✓ Agent ${aid} online with ${instances.length} channel(s)`);
2699
2687
  }
2700
- catch (e) {
2701
- results.push(`⚠ ${cfg.aid}: ${e?.message || e}`);
2688
+ finally {
2689
+ if (!resumed)
2690
+ handoffRuntime.resumeAgent(aid);
2702
2691
  }
2703
- }
2704
- // 重建 channel index(kit 缓存已在本轮开头清过)
2705
- agentRegistry.channelIndex.clear();
2706
- agentRegistry.buildChannelIndex();
2707
- logger.info(`[Resync] Done: ${results.length} agent(s) processed`);
2708
- return results;
2709
- };
2710
- ipcServer.setStatsProvider(() => statsCollector.getSnapshot());
2711
- ipcServer.setAgentStatsProvider(() => agentRegistry.list().map((agent) => {
2712
- const snap = statsCollector.getSnapshot(agent.aid);
2713
- return {
2714
- aid: agent.aid,
2715
- received: snap.lastHour.received,
2716
- sent: snap.lastHour.sent,
2717
- completed: snap.lastHour.completed,
2718
- errors: snap.lastHour.errors,
2719
- interrupts: snap.lastHour.interrupts,
2720
- avgResponseMs: snap.lastHour.avgResponseMs,
2721
- processing: messageQueue.getProcessingCountByAgent(agent.aid),
2722
- queued: messageQueue.getQueueLengthByAgent(agent.aid),
2723
- muted: messageQueue.isAgentMuted(agent.aid),
2724
2692
  };
2725
- }));
2726
- // Queue snapshot & action (for ec queue --agent CLI)
2727
- ipcServer.setQueueSnapshotProvider((params) => {
2728
- const handle = agentRegistry.get(params.agent);
2729
- const agentName = handle?.name;
2730
- if (!agentName)
2731
- return [];
2732
- return messageQueue.getQueueItemsByAgent(agentName);
2733
- });
2734
- ipcServer.setQueueActionExecutor(async (params) => {
2735
- const handle = agentRegistry.get(params.agent);
2736
- const agentName = handle?.name;
2737
- if (!agentName)
2738
- return { ok: false, error: `agent not found: ${params.agent}` };
2739
- switch (params.action) {
2740
- case 'clear':
2741
- return { ok: true, cleared: messageQueue.clearByAgent(agentName) };
2742
- case 'cancel':
2743
- if (!params.messageId)
2744
- return { ok: false, error: 'missing messageId' };
2745
- return { ok: true, cancelled: messageQueue.cancelMessageById(agentName, params.messageId) };
2746
- case 'interrupt':
2747
- if (!params.sessionKey)
2748
- return { ok: false, error: 'missing sessionKey' };
2749
- {
2750
- const sessionId = messageQueue.findSessionIdBySessionKey(params.sessionKey);
2751
- if (!sessionId)
2752
- return { ok: false, error: `session not found: ${params.sessionKey}` };
2753
- return { ok: true, interrupted: await messageQueue.interruptBySession(sessionId) };
2693
+ // Full resync handler: scan disk, load new agents, unload removed/disabled, reload changed
2694
+ 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();
2727
+ }
2728
+ catch { }
2729
+ markChannelDisconnected(inst.adapter.channelName);
2730
+ const idx = channelInstances.indexOf(inst);
2731
+ if (idx >= 0)
2732
+ channelInstances.splice(idx, 1);
2733
+ }
2734
+ }
2735
+ agentRegistry.agents.delete(aid);
2736
+ results.push(`- ${aid} (offline)`);
2737
+ continue;
2754
2738
  }
2755
- default:
2756
- return { ok: false, error: `unknown action: ${params.action}` };
2757
- }
2758
- });
2759
- ipcServer.setTriggerExecutor(async (cmd) => {
2760
- const schedulerFor = (agentAid) => {
2761
- const scheduler = triggerSchedulers.get(agentAid);
2762
- if (!scheduler)
2763
- throw new Error(`trigger scheduler not found for agent: ${agentAid}`);
2764
- return scheduler;
2765
- };
2766
- const requireAgent = (agentAid) => {
2767
- if (typeof agentAid !== 'string' || !agentAid)
2768
- throw new Error('missing agentAid');
2769
- if (agentAid === daemonTriggerOwner.aid)
2770
- return agentAid;
2771
- if (!agentRegistry.get(agentAid))
2772
- throw new Error(`agent not found: ${agentAid}`);
2773
- return agentAid;
2774
- };
2775
- const authorizeTrigger = async (agentAid, operation, triggerId) => {
2776
- const actor = await authenticatedTriggerActor(agentAid, cmd.actorSessionId, cmd.delegationToken, cmd.delegationCommandHash, cmd.controlToken);
2777
- const isCrossAgent = !actor.control && actor.selfAid !== agentAid;
2778
- if (!isCrossAgentTriggerOperationAllowed({
2779
- control: actor.control,
2780
- taskAgentAid: actor.selfAid,
2781
- targetAgentAid: agentAid,
2782
- targetManagement: actor.management,
2783
- operation,
2784
- })) {
2785
- if (isCrossAgent && !actor.management) {
2786
- throw new Error('cross-agent trigger audit requires owner/admin access on the target agent');
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)`);
2787
2749
  }
2788
- if (isCrossAgent && !isCrossAgentTriggerReadOperation(operation)) {
2789
- throw new Error('cross-agent trigger operations are limited to list, show, and history');
2750
+ catch (e) {
2751
+ results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
2790
2752
  }
2791
- throw new Error('trigger agent does not match authenticated task agent');
2792
2753
  }
2793
- const operationDecision = authorizeOperation({
2794
- source: actor.control ? 'control' : 'agent-tool',
2795
- subject: actor.subject,
2796
- intent: {
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
+ }
2772
+ }
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
+ };
2779
+ ipcServer.setStatsProvider(() => statsCollector.getSnapshot());
2780
+ ipcServer.setAgentStatsProvider(() => agentRegistry.list().map((agent) => {
2781
+ const snap = statsCollector.getSnapshot(agent.aid);
2782
+ return {
2783
+ aid: agent.aid,
2784
+ received: snap.lastHour.received,
2785
+ sent: snap.lastHour.sent,
2786
+ completed: snap.lastHour.completed,
2787
+ errors: snap.lastHour.errors,
2788
+ interrupts: snap.lastHour.interrupts,
2789
+ avgResponseMs: snap.lastHour.avgResponseMs,
2790
+ processing: messageQueue.getProcessingCountByAgent(agent.aid),
2791
+ queued: messageQueue.getQueueLengthByAgent(agent.aid),
2792
+ muted: messageQueue.isAgentMuted(agent.aid),
2793
+ };
2794
+ }));
2795
+ // Queue snapshot & action (for ec queue --agent CLI)
2796
+ ipcServer.setQueueSnapshotProvider((params) => {
2797
+ const handle = agentRegistry.get(params.agent);
2798
+ const agentName = handle?.name;
2799
+ if (!agentName)
2800
+ return [];
2801
+ return messageQueue.getQueueItemsByAgent(agentName);
2802
+ });
2803
+ ipcServer.setQueueActionExecutor(async (params) => {
2804
+ const handle = agentRegistry.get(params.agent);
2805
+ const agentName = handle?.name;
2806
+ if (!agentName)
2807
+ return { ok: false, error: `agent not found: ${params.agent}` };
2808
+ switch (params.action) {
2809
+ case 'clear':
2810
+ return { ok: true, cleared: messageQueue.clearByAgent(agentName) };
2811
+ case 'cancel':
2812
+ if (!params.messageId)
2813
+ return { ok: false, error: 'missing messageId' };
2814
+ return { ok: true, cancelled: messageQueue.cancelMessageById(agentName, params.messageId) };
2815
+ case 'interrupt':
2816
+ if (!params.sessionKey)
2817
+ return { ok: false, error: 'missing sessionKey' };
2818
+ {
2819
+ const sessionId = messageQueue.findSessionIdBySessionKey(params.sessionKey);
2820
+ if (!sessionId)
2821
+ return { ok: false, error: `session not found: ${params.sessionKey}` };
2822
+ return { ok: true, interrupted: await messageQueue.interruptBySession(sessionId) };
2823
+ }
2824
+ default:
2825
+ return { ok: false, error: `unknown action: ${params.action}` };
2826
+ }
2827
+ });
2828
+ ipcServer.setTriggerExecutor(async (cmd) => {
2829
+ const schedulerFor = (agentAid) => {
2830
+ const scheduler = triggerSchedulers.get(agentAid);
2831
+ if (!scheduler)
2832
+ throw new Error(`trigger scheduler not found for agent: ${agentAid}`);
2833
+ return scheduler;
2834
+ };
2835
+ const requireAgent = (agentAid) => {
2836
+ if (typeof agentAid !== 'string' || !agentAid)
2837
+ throw new Error('missing agentAid');
2838
+ if (agentAid === daemonTriggerOwner.aid)
2839
+ return agentAid;
2840
+ if (!agentRegistry.get(agentAid))
2841
+ throw new Error(`agent not found: ${agentAid}`);
2842
+ return agentAid;
2843
+ };
2844
+ const authorizeTrigger = async (agentAid, operation, triggerId) => {
2845
+ const actor = await authenticatedTriggerActor(agentAid, cmd.actorSessionId, cmd.delegationToken, cmd.delegationCommandHash, cmd.controlToken);
2846
+ const isCrossAgent = !actor.control && actor.selfAid !== agentAid;
2847
+ if (!isCrossAgentTriggerOperationAllowed({
2848
+ control: actor.control,
2849
+ taskAgentAid: actor.selfAid,
2850
+ targetAgentAid: agentAid,
2851
+ targetManagement: actor.management,
2797
2852
  operation,
2798
- scope: 'relation',
2853
+ })) {
2854
+ if (isCrossAgent && !actor.management) {
2855
+ throw new Error('cross-agent trigger audit requires owner/admin access on the target agent');
2856
+ }
2857
+ if (isCrossAgent && !isCrossAgentTriggerReadOperation(operation)) {
2858
+ throw new Error('cross-agent trigger operations are limited to list, show, and history');
2859
+ }
2860
+ throw new Error('trigger agent does not match authenticated task agent');
2861
+ }
2862
+ const operationDecision = authorizeOperation({
2799
2863
  source: actor.control ? 'control' : 'agent-tool',
2800
- args: {
2801
- self: agentAid,
2802
- peer: actor.origin.peerId,
2803
- peerKey: actor.subject.peerKey,
2804
- ...(triggerId ? { triggerId } : {}),
2864
+ subject: actor.subject,
2865
+ intent: {
2866
+ operation,
2867
+ scope: 'relation',
2868
+ source: actor.control ? 'control' : 'agent-tool',
2869
+ args: {
2870
+ self: agentAid,
2871
+ peer: actor.origin.peerId,
2872
+ peerKey: actor.subject.peerKey,
2873
+ ...(triggerId ? { triggerId } : {}),
2874
+ },
2805
2875
  },
2806
- },
2807
- });
2808
- if (!operationDecision.allow)
2809
- throw new Error(operationDecision.reason);
2810
- if (actor.management)
2811
- return actor;
2812
- if (!triggerId)
2876
+ });
2877
+ if (!operationDecision.allow)
2878
+ throw new Error(operationDecision.reason);
2879
+ if (actor.management)
2880
+ return actor;
2881
+ if (!triggerId)
2882
+ return actor;
2883
+ const definition = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === triggerId);
2884
+ if (!definition
2885
+ || definition.origin?.peerId !== actor.origin?.peerId
2886
+ || definition.origin?.channelKey !== actor.origin?.channelKey) {
2887
+ throw new Error('trigger not found or access denied');
2888
+ }
2813
2889
  return actor;
2814
- const definition = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === triggerId);
2815
- if (!definition
2816
- || definition.origin?.peerId !== actor.origin?.peerId
2817
- || definition.origin?.channelKey !== actor.origin?.channelKey) {
2818
- throw new Error('trigger not found or access denied');
2819
- }
2820
- return actor;
2821
- };
2822
- switch (cmd.type) {
2823
- case 'trigger.list': {
2824
- const agentAid = requireAgent(cmd.agentAid);
2825
- const actor = await authorizeTrigger(agentAid, 'trigger.list');
2826
- const scheduler = schedulerFor(agentAid);
2827
- const definitions = scheduler.list({ all: cmd.all === true }).filter(trigger => actor.management
2828
- || (trigger.origin?.peerId === actor.origin?.peerId && trigger.origin?.channelKey === actor.origin?.channelKey));
2829
- return { ok: true, triggers: scheduler.listItems(definitions) };
2830
- }
2831
- case 'trigger.show': {
2832
- const agentAid = requireAgent(cmd.agentAid);
2833
- if (!cmd.triggerId)
2834
- throw new Error('missing triggerId');
2835
- await authorizeTrigger(agentAid, 'trigger.show', cmd.triggerId);
2836
- return {
2837
- ok: true,
2838
- ...schedulerFor(agentAid).show(cmd.triggerId, {
2839
- includeScriptPreview: cmd.includeScriptPreview !== false,
2840
- }),
2841
- };
2842
- }
2843
- case 'trigger.history': {
2844
- const agentAid = requireAgent(cmd.agentAid);
2845
- const limit = cmd.limit === undefined ? 100 : Number(cmd.limit);
2846
- if (!Number.isInteger(limit) || limit <= 0 || limit > 10_000)
2847
- throw new Error('invalid history limit');
2848
- if (cmd.triggerId !== undefined && (typeof cmd.triggerId !== 'string' || !cmd.triggerId)) {
2849
- throw new Error('invalid triggerId');
2890
+ };
2891
+ switch (cmd.type) {
2892
+ case 'trigger.list': {
2893
+ const agentAid = requireAgent(cmd.agentAid);
2894
+ const actor = await authorizeTrigger(agentAid, 'trigger.list');
2895
+ const scheduler = schedulerFor(agentAid);
2896
+ const definitions = scheduler.list({ all: cmd.all === true }).filter(trigger => actor.management
2897
+ || (trigger.origin?.peerId === actor.origin?.peerId && trigger.origin?.channelKey === actor.origin?.channelKey));
2898
+ return { ok: true, triggers: scheduler.listItems(definitions) };
2850
2899
  }
2851
- if (cmd.triggerId)
2852
- await authorizeTrigger(agentAid, 'trigger.history', cmd.triggerId);
2853
- else if (!(await authorizeTrigger(agentAid, 'trigger.history')).management) {
2854
- throw new Error('triggerId is required for non-management history access');
2900
+ case 'trigger.show': {
2901
+ const agentAid = requireAgent(cmd.agentAid);
2902
+ if (!cmd.triggerId)
2903
+ throw new Error('missing triggerId');
2904
+ await authorizeTrigger(agentAid, 'trigger.show', cmd.triggerId);
2905
+ return {
2906
+ ok: true,
2907
+ ...schedulerFor(agentAid).show(cmd.triggerId, {
2908
+ includeScriptPreview: cmd.includeScriptPreview !== false,
2909
+ }),
2910
+ };
2855
2911
  }
2856
- return { ok: true, events: schedulerFor(agentAid).history(cmd.triggerId, limit) };
2857
- }
2858
- case 'trigger.eventCatalog': {
2859
- const agentAid = requireAgent(cmd.agentAid);
2860
- await authorizeTrigger(agentAid, 'trigger.eventCatalog');
2861
- return { ok: true, ...getEventCatalog({ includeInternal: cmd.includeInternal === true }) };
2862
- }
2863
- case 'trigger.create': {
2864
- const rawDefinition = cmd.definition;
2865
- if (!rawDefinition || typeof rawDefinition !== 'object' || Array.isArray(rawDefinition)) {
2866
- throw new Error('trigger definition must be an object');
2912
+ case 'trigger.history': {
2913
+ const agentAid = requireAgent(cmd.agentAid);
2914
+ const limit = cmd.limit === undefined ? 100 : Number(cmd.limit);
2915
+ if (!Number.isInteger(limit) || limit <= 0 || limit > 10_000)
2916
+ throw new Error('invalid history limit');
2917
+ if (cmd.triggerId !== undefined && (typeof cmd.triggerId !== 'string' || !cmd.triggerId)) {
2918
+ throw new Error('invalid triggerId');
2919
+ }
2920
+ if (cmd.triggerId)
2921
+ await authorizeTrigger(agentAid, 'trigger.history', cmd.triggerId);
2922
+ else if (!(await authorizeTrigger(agentAid, 'trigger.history')).management) {
2923
+ throw new Error('triggerId is required for non-management history access');
2924
+ }
2925
+ return { ok: true, events: schedulerFor(agentAid).history(cmd.triggerId, limit) };
2867
2926
  }
2868
- const agentAid = requireAgent(rawDefinition.agentAid);
2869
- const actor = await authorizeTrigger(agentAid, 'trigger.create');
2870
- const materialized = materializeTriggerCreateBaseagent({ ...rawDefinition, origin: actor.origin }, agentAid);
2871
- const definition = normalizeTriggerDefinition(materialized);
2872
- validateTriggerDefinitionForActor(definition, actor);
2873
- requireAgent(definition.agentAid);
2874
- validateTriggerFeedbackChannels(definition);
2875
- const trigger = schedulerFor(definition.agentAid).create(definition, cmd.files ?? [], { enable: cmd.enable });
2876
- return { ok: true, trigger };
2877
- }
2878
- case 'trigger.update': {
2879
- const agentAid = requireAgent(cmd.agentAid);
2880
- if (!cmd.triggerId)
2881
- throw new Error('missing triggerId');
2882
- const actor = await authorizeTrigger(agentAid, 'trigger.update', cmd.triggerId);
2883
- const existing = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === cmd.triggerId);
2884
- if (!existing)
2885
- throw new Error(`trigger not found: ${cmd.triggerId}`);
2886
- const currentRevision = definitionRevision(existing);
2887
- if (cmd.expectedRevision !== undefined && cmd.expectedRevision !== currentRevision) {
2888
- throw new Error(`trigger revision conflict: expected ${cmd.expectedRevision}, current ${currentRevision}`);
2927
+ case 'trigger.eventCatalog': {
2928
+ const agentAid = requireAgent(cmd.agentAid);
2929
+ await authorizeTrigger(agentAid, 'trigger.eventCatalog');
2930
+ return { ok: true, ...getEventCatalog({ includeInternal: cmd.includeInternal === true }) };
2889
2931
  }
2890
- const definition = applyTriggerPatch(existing, cmd.patch, { fromPromptFile: cmd.promptFile === true });
2891
- validateTriggerDefinitionForActor(definition, actor);
2892
- validateTriggerFeedbackChannels(definition);
2893
- const scheduler = schedulerFor(agentAid);
2894
- const trigger = scheduler.update(cmd.triggerId, definition);
2895
- return { ok: true, trigger: scheduler.listItem(trigger), revision: definitionRevision(trigger) };
2896
- }
2897
- case 'trigger.setEnabled': {
2898
- const agentAid = requireAgent(cmd.agentAid);
2899
- if (!cmd.triggerId)
2900
- throw new Error('missing triggerId');
2901
- if (typeof cmd.enabled !== 'boolean')
2902
- throw new Error('missing enabled');
2903
- await authorizeTrigger(agentAid, 'trigger.setEnabled', cmd.triggerId);
2904
- const trigger = schedulerFor(agentAid).setEnabled(cmd.triggerId, cmd.enabled);
2905
- return { ok: true, trigger };
2906
- }
2907
- case 'trigger.cancel': {
2908
- const agentAid = requireAgent(cmd.agentAid);
2909
- if (!cmd.triggerId)
2910
- throw new Error('missing triggerId');
2911
- await authorizeTrigger(agentAid, 'trigger.cancel', cmd.triggerId);
2912
- const trigger = schedulerFor(agentAid).cancel(cmd.triggerId);
2913
- return { ok: true, trigger };
2914
- }
2915
- case 'trigger.delete': {
2916
- const agentAid = requireAgent(cmd.agentAid);
2917
- if (!cmd.triggerId)
2918
- throw new Error('missing triggerId');
2919
- await authorizeTrigger(agentAid, 'trigger.delete', cmd.triggerId);
2920
- const trigger = schedulerFor(agentAid).delete(cmd.triggerId);
2921
- return { ok: true, trigger };
2922
- }
2923
- case 'trigger.run': {
2924
- const agentAid = requireAgent(cmd.agentAid);
2925
- if (!cmd.triggerId)
2926
- throw new Error('missing triggerId');
2927
- await authorizeTrigger(agentAid, 'trigger.run', cmd.triggerId);
2928
- const result = await schedulerFor(agentAid).run(cmd.triggerId, {
2929
- dryRun: cmd.dryRun === true,
2930
- ...(cmd.eventPayload !== undefined ? { eventPayload: cmd.eventPayload } : {}),
2931
- });
2932
- return {
2933
- ok: result.ok,
2934
- result,
2935
- runId: result.runId,
2936
- triggerId: result.triggerId,
2937
- status: result.status,
2938
- reason: result.reason,
2939
- conflictRunId: result.conflictRunId,
2940
- error: result.error,
2941
- audit: result.audit,
2942
- };
2932
+ case 'trigger.create': {
2933
+ const rawDefinition = cmd.definition;
2934
+ if (!rawDefinition || typeof rawDefinition !== 'object' || Array.isArray(rawDefinition)) {
2935
+ throw new Error('trigger definition must be an object');
2936
+ }
2937
+ const agentAid = requireAgent(rawDefinition.agentAid);
2938
+ const actor = await authorizeTrigger(agentAid, 'trigger.create');
2939
+ const materialized = materializeTriggerCreateBaseagent({ ...rawDefinition, origin: actor.origin }, agentAid);
2940
+ const definition = normalizeTriggerDefinition(materialized);
2941
+ validateTriggerDefinitionForActor(definition, actor);
2942
+ requireAgent(definition.agentAid);
2943
+ validateTriggerFeedbackChannels(definition);
2944
+ const trigger = schedulerFor(definition.agentAid).create(definition, cmd.files ?? [], { enable: cmd.enable });
2945
+ return { ok: true, trigger };
2946
+ }
2947
+ case 'trigger.update': {
2948
+ const agentAid = requireAgent(cmd.agentAid);
2949
+ if (!cmd.triggerId)
2950
+ throw new Error('missing triggerId');
2951
+ const actor = await authorizeTrigger(agentAid, 'trigger.update', cmd.triggerId);
2952
+ const existing = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === cmd.triggerId);
2953
+ if (!existing)
2954
+ throw new Error(`trigger not found: ${cmd.triggerId}`);
2955
+ const currentRevision = definitionRevision(existing);
2956
+ if (cmd.expectedRevision !== undefined && cmd.expectedRevision !== currentRevision) {
2957
+ throw new Error(`trigger revision conflict: expected ${cmd.expectedRevision}, current ${currentRevision}`);
2958
+ }
2959
+ const definition = applyTriggerPatch(existing, cmd.patch, { fromPromptFile: cmd.promptFile === true });
2960
+ validateTriggerDefinitionForActor(definition, actor);
2961
+ validateTriggerFeedbackChannels(definition);
2962
+ const scheduler = schedulerFor(agentAid);
2963
+ const trigger = scheduler.update(cmd.triggerId, definition);
2964
+ return { ok: true, trigger: scheduler.listItem(trigger), revision: definitionRevision(trigger) };
2965
+ }
2966
+ case 'trigger.setEnabled': {
2967
+ const agentAid = requireAgent(cmd.agentAid);
2968
+ if (!cmd.triggerId)
2969
+ throw new Error('missing triggerId');
2970
+ if (typeof cmd.enabled !== 'boolean')
2971
+ throw new Error('missing enabled');
2972
+ await authorizeTrigger(agentAid, 'trigger.setEnabled', cmd.triggerId);
2973
+ const trigger = schedulerFor(agentAid).setEnabled(cmd.triggerId, cmd.enabled);
2974
+ return { ok: true, trigger };
2975
+ }
2976
+ case 'trigger.cancel': {
2977
+ const agentAid = requireAgent(cmd.agentAid);
2978
+ if (!cmd.triggerId)
2979
+ throw new Error('missing triggerId');
2980
+ await authorizeTrigger(agentAid, 'trigger.cancel', cmd.triggerId);
2981
+ const trigger = schedulerFor(agentAid).cancel(cmd.triggerId);
2982
+ return { ok: true, trigger };
2983
+ }
2984
+ case 'trigger.delete': {
2985
+ const agentAid = requireAgent(cmd.agentAid);
2986
+ if (!cmd.triggerId)
2987
+ throw new Error('missing triggerId');
2988
+ await authorizeTrigger(agentAid, 'trigger.delete', cmd.triggerId);
2989
+ const trigger = schedulerFor(agentAid).delete(cmd.triggerId);
2990
+ return { ok: true, trigger };
2991
+ }
2992
+ case 'trigger.run': {
2993
+ const agentAid = requireAgent(cmd.agentAid);
2994
+ if (!cmd.triggerId)
2995
+ throw new Error('missing triggerId');
2996
+ await authorizeTrigger(agentAid, 'trigger.run', cmd.triggerId);
2997
+ const result = await schedulerFor(agentAid).run(cmd.triggerId, {
2998
+ dryRun: cmd.dryRun === true,
2999
+ ...(cmd.eventPayload !== undefined ? { eventPayload: cmd.eventPayload } : {}),
3000
+ });
3001
+ return {
3002
+ ok: result.ok,
3003
+ result,
3004
+ runId: result.runId,
3005
+ triggerId: result.triggerId,
3006
+ status: result.status,
3007
+ reason: result.reason,
3008
+ conflictRunId: result.conflictRunId,
3009
+ error: result.error,
3010
+ audit: result.audit,
3011
+ };
3012
+ }
3013
+ default:
3014
+ return { ok: false, error: `unknown trigger command: ${cmd.type}` };
2943
3015
  }
2944
- default:
2945
- return { ok: false, error: `unknown trigger command: ${cmd.type}` };
2946
- }
2947
- });
3016
+ });
3017
+ }
2948
3018
  ipcServer.startCpuTracking();
2949
- // I3: start IPC server after all hooks/executors/providers are registered.
2950
- await ipcServer.start();
2951
- // 写入 ready 信号(Control Plane 已可通过 IPC 查询;channel 连接不阻塞启动判定)
3019
+ // IPC was bound before channel connection. At this point all handlers are
3020
+ // registered, so the ready signal can safely expose the control plane.
2952
3021
  const readySignalPath = resolvePaths().readySignal;
2953
3022
  fs.writeFileSync(readySignalPath, String(Date.now()));
2954
3023
  logger.info(`✓ Ready signal written: ${readySignalPath}`);