evolcore 0.0.6 → 0.0.8

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.
package/dist/index.js CHANGED
@@ -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,7 @@ async function runBindBootstrapDaemon(daemonCfg) {
367
368
  agentName: daemonCfg.aid,
368
369
  channelName: 'control',
369
370
  pureIdentity: true,
371
+ gatewayUrl: daemonCfg.aun?.gatewayUrl,
370
372
  aunTrace: daemonCfg.debug?.aunTrace,
371
373
  aunSdkLog: daemonCfg.debug?.aunSdkLog,
372
374
  });
@@ -397,7 +399,20 @@ async function runBindBootstrapDaemon(daemonCfg) {
397
399
  channels: {},
398
400
  channelsByType: {},
399
401
  queue: { pending: 0, processing: 0 },
400
- controlAid: { aid: daemonCfg.aid, connected: controlChannel?.getAidState().status === 'connected' },
402
+ controlAid: (() => {
403
+ const aidState = controlChannel?.getAidState();
404
+ return {
405
+ aid: daemonCfg.aid,
406
+ connected: aidState?.status === 'connected',
407
+ ...(aidState ? {
408
+ status: aidState.status,
409
+ gatewayUrl: aidState.gatewayUrl,
410
+ reconnectCount: aidState.reconnectCount,
411
+ lastConnectedAt: aidState.lastConnectedAt,
412
+ lastError: aidState.lastError,
413
+ } : {}),
414
+ };
415
+ })(),
401
416
  }));
402
417
  ipcServer.setBindExecutor({
403
418
  begin: (cmd) => bindService.begin(cmd),
@@ -859,11 +874,12 @@ async function main() {
859
874
  agentLoader.register(new CodexAgentPlugin());
860
875
  agentLoader.register(new GeminiAgentPlugin());
861
876
  agentLoader.register(new EcagentAgentPlugin());
877
+ const creationErrors = [];
862
878
  const agentInstances = agentLoader.createAll(agentRegistry, {
863
879
  onSessionIdUpdate: async (sessionId, agentSessionId) => {
864
880
  await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
865
881
  },
866
- });
882
+ }, creationErrors);
867
883
  // agentMap 复合键:${aid}::${baseagent}
868
884
  const agentMap = new Map();
869
885
  for (const inst of agentInstances) {
@@ -871,10 +887,18 @@ async function main() {
871
887
  }
872
888
  const primaryBaseagent = primaryAgent?.baseagent ?? 'claude';
873
889
  let primaryRunnerKey = primaryAgent ? `${primaryAgent.aid}::${primaryBaseagent}` : '<empty>::claude';
874
- const agentRunner = agentMap.get(primaryRunnerKey) || agentInstances[0]?.agent;
890
+ const agentRunner = primaryAgent
891
+ ? agentMap.get(primaryRunnerKey)
892
+ : agentInstances[0]?.agent;
875
893
  if (primaryAgent && !agentRunner) {
876
894
  agentRuntimeState = 'error';
877
- agentRuntimeError = 'No agent backend available. Check baseagents config (no runners created).';
895
+ const primaryCreationErrors = creationErrors.filter(error => error.evolagentName === primaryAgent.name);
896
+ const creationDetail = primaryCreationErrors.length > 0
897
+ ? primaryCreationErrors.map(({ baseagent, message }) => `${baseagent}: ${message}`).join('; ')
898
+ : `active_baseagent=${primaryAgent.config.active_baseagent ?? '(unset)'}, configured=${Object.keys(primaryAgent.config.baseagents ?? {}).join(',') || '(none)'}`;
899
+ agentRuntimeError = primaryCreationErrors.length > 0
900
+ ? creationDetail
901
+ : `No agent runner created for ${primaryAgent.name}: ${creationDetail}`;
878
902
  primaryAgent.status = 'error';
879
903
  primaryAgent.error = agentRuntimeError;
880
904
  logger.error(agentRuntimeError);
@@ -1871,6 +1895,78 @@ async function main() {
1871
1895
  if (inst)
1872
1896
  connectedChannels.delete(inst.adapter.channelKey);
1873
1897
  };
1898
+ // Bind the per-HOME IPC endpoint before any AUN connection is attempted.
1899
+ // A failed bind (for example, a stale Windows named pipe) must abort startup
1900
+ // without creating a second AUN session that can kick the old daemon.
1901
+ let controlChannel;
1902
+ const ipcServer = new IpcServer(resolvePaths().socket, () => {
1903
+ const channels = {};
1904
+ const channelsByType = {};
1905
+ for (const inst of channelInstances) {
1906
+ const name = inst.adapter.channelName;
1907
+ const status = inst.channel.getStatus?.() ?? { connected: true };
1908
+ const channelType = inst.channelType || name;
1909
+ channels[name] = { ...status, channelType };
1910
+ if (!channelsByType[channelType])
1911
+ channelsByType[channelType] = [];
1912
+ channelsByType[channelType].push(name);
1913
+ }
1914
+ const snap = statsCollector.getSnapshot();
1915
+ const agentListForStatus = agentRegistry.list();
1916
+ return {
1917
+ pid: process.pid,
1918
+ uptime: snap.uptimeMs,
1919
+ controlPlane: {
1920
+ ready: true,
1921
+ owned: processLevelOwners.length > 0,
1922
+ },
1923
+ agentRuntime: {
1924
+ state: agentRuntimeState,
1925
+ runnableAgents: agentListForStatus.filter((a) => a.status !== 'error' && a.status !== 'disabled').length,
1926
+ runningAgents: agentListForStatus.filter((a) => a.status === 'running').length,
1927
+ ...(agentRuntimeError ? { error: agentRuntimeError } : {}),
1928
+ },
1929
+ channels,
1930
+ channelsByType,
1931
+ queue: {
1932
+ pending: messageQueue.getGlobalQueueLength(),
1933
+ processing: messageQueue.getGlobalProcessingCount(),
1934
+ },
1935
+ stats: {
1936
+ received: snap.lastHour.received,
1937
+ sent: snap.lastHour.sent,
1938
+ completed: snap.lastHour.completed,
1939
+ errors: snap.lastHour.errors,
1940
+ avgResponseMs: snap.lastHour.avgResponseMs,
1941
+ },
1942
+ controlAid: (() => {
1943
+ if (!daemonCfg.aid)
1944
+ return undefined;
1945
+ const aidState = controlChannel?.getAidState();
1946
+ return {
1947
+ aid: daemonCfg.aid,
1948
+ connected: aidState?.status === 'connected',
1949
+ ...(aidState ? {
1950
+ status: aidState.status,
1951
+ gatewayUrl: aidState.gatewayUrl,
1952
+ reconnectCount: aidState.reconnectCount,
1953
+ lastConnectedAt: aidState.lastConnectedAt,
1954
+ lastError: aidState.lastError,
1955
+ } : {}),
1956
+ };
1957
+ })(),
1958
+ };
1959
+ }, async (cmd, sessionId, delegationToken, delegationCommandHash) => {
1960
+ const delegation = agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
1961
+ if (!delegation.ok)
1962
+ return { ok: false, code: delegation.code, error: delegation.reason };
1963
+ return cmdHandler.handleCtl(cmd, sessionId);
1964
+ });
1965
+ // Register every IPC executor/provider before exposing the endpoint. The
1966
+ // function declaration is hoisted, while its invocation remains here so a
1967
+ // failed bind still aborts before any AUN connection is attempted.
1968
+ configureIpc();
1969
+ await ipcServer.start();
1874
1970
  // ── 连接所有渠道(后台首连,AUN/任意渠道故障不阻塞 daemon 主流程)──
1875
1971
  logger.info(`🚀 EvolCore core is ready; connecting ${channelInstances.length} channel(s) in background`);
1876
1972
  const connectAllPromise = channelLoader.connectAll(channelInstances, {
@@ -1913,13 +2009,13 @@ async function main() {
1913
2009
  logger.warn(`控制 AID 证书缺失:${daemonCfg.aid}(AUN 控制通道后台重连;如需重建运行 ec init)`);
1914
2010
  }
1915
2011
  }
1916
- let controlChannel;
1917
2012
  if (daemonCfg.aid) {
1918
2013
  controlChannel = new AUNChannel({
1919
2014
  aid: daemonCfg.aid,
1920
2015
  agentName: daemonCfg.aid,
1921
2016
  channelName: 'control',
1922
2017
  pureIdentity: true,
2018
+ gatewayUrl: daemonCfg.aun?.gatewayUrl,
1923
2019
  aunTrace: daemonCfg.debug?.aunTrace,
1924
2020
  aunSdkLog: daemonCfg.debug?.aunSdkLog,
1925
2021
  });
@@ -1961,7 +2057,9 @@ async function main() {
1961
2057
  }
1962
2058
  }
1963
2059
  else {
1964
- logger.warn(`ECWeb 配置已启用但未找到 ${ecwebEntry}`);
2060
+ // 正式安装时 ECWeb 是独立的全局 ec-web 包,由 ec start/restart 的
2061
+ // 启动链路管理;runtime 目录没有内嵌构建产物是正常状态。
2062
+ logger.debug(`ECWeb runtime entry absent (managed externally): ${ecwebEntry}`);
1965
2063
  }
1966
2064
  }
1967
2065
  // 控制 AID 接收 owner 指令:
@@ -2197,756 +2295,712 @@ async function main() {
2197
2295
  });
2198
2296
  }
2199
2297
  }
2200
- // IPC server — 供 CLI 查询实时状态 + Agent ctl 指令执行
2201
- const ipcServer = new IpcServer(resolvePaths().socket, () => {
2202
- const channels = {};
2203
- const channelsByType = {};
2204
- for (const inst of channelInstances) {
2205
- const name = inst.adapter.channelName;
2206
- const status = inst.channel.getStatus?.() ?? { connected: true };
2207
- const channelType = inst.channelType || name;
2208
- channels[name] = { ...status, channelType };
2209
- if (!channelsByType[channelType])
2210
- channelsByType[channelType] = [];
2211
- channelsByType[channelType].push(name);
2298
+ function configureIpc() {
2299
+ // M3: direct call (not cast) wire EvolAgentRegistry into IPC for evolagent.* handlers
2300
+ ipcServer.setAgentRegistry(agentRegistry);
2301
+ ipcServer.setDingtalkContactBindExecutor({
2302
+ register: (cmd) => registerPendingDingtalkContactBind(cmd),
2303
+ isChannelReady: isContactBindChannelReady,
2304
+ });
2305
+ ipcServer.setFeishuContactBindExecutor({
2306
+ register: (cmd) => registerPendingFeishuContactBind(cmd),
2307
+ isChannelReady: isContactBindChannelReady,
2308
+ });
2309
+ ipcServer.setQQBotContactBindExecutor({
2310
+ register: (cmd) => registerPendingQQBotContactBind(cmd),
2311
+ isChannelReady: isContactBindChannelReady,
2312
+ });
2313
+ ipcServer.setWecomContactBindExecutor({
2314
+ register: (cmd) => registerPendingWecomContactBind(cmd),
2315
+ isChannelReady: isContactBindChannelReady,
2316
+ });
2317
+ ipcServer.setWechatContactBindExecutor({
2318
+ register: (cmd) => registerPendingWechatContactBind(cmd),
2319
+ isChannelReady: isContactBindChannelReady,
2320
+ });
2321
+ ipcServer.setMenuExecutor((payload, auth) => cmdHandler.execMenuForEcweb(payload, auth));
2322
+ ipcServer.setConfigOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleConfigOperation(argv, sessionId, delegationToken, delegationCommandHash));
2323
+ ipcServer.setContactOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleContactOperation(argv, sessionId, delegationToken, delegationCommandHash));
2324
+ cmdHandler.setDaemonStatusProvider(() => {
2325
+ const aidState = controlChannel?.getAidState?.();
2326
+ return {
2327
+ aid: daemonCfg.aid ?? null,
2328
+ aun: aidState ? {
2329
+ connected: aidState.status === 'connected',
2330
+ status: aidState.status,
2331
+ reconnectCount: aidState.reconnectCount ?? 0,
2332
+ flapCount: aidState.flapCount ?? 0,
2333
+ ...(aidState.lastError ? { lastError: String(aidState.lastError).slice(0, 80) } : {}),
2334
+ ...(aidState.kickDetail?.reason ? { kickReason: String(aidState.kickDetail.reason).slice(0, 80) } : {}),
2335
+ } : {
2336
+ connected: false,
2337
+ status: daemonCfg.aid ? 'disconnected' : 'disabled',
2338
+ },
2339
+ };
2340
+ });
2341
+ if (bindService) {
2342
+ ipcServer.setBindExecutor({
2343
+ begin: (cmd) => bindService.begin(cmd),
2344
+ status: (taskId) => bindService.status(taskId),
2345
+ cancel: (taskId) => bindService.cancel(taskId),
2346
+ });
2212
2347
  }
2213
- const snap = statsCollector.getSnapshot();
2214
- const agentListForStatus = agentRegistry.list();
2215
- return {
2216
- pid: process.pid,
2217
- uptime: snap.uptimeMs,
2218
- controlPlane: {
2219
- ready: true,
2220
- owned: processLevelOwners.length > 0,
2221
- },
2222
- agentRuntime: {
2223
- state: agentRuntimeState,
2224
- runnableAgents: agentListForStatus.filter((a) => a.status !== 'error' && a.status !== 'disabled').length,
2225
- runningAgents: agentListForStatus.filter((a) => a.status === 'running').length,
2226
- ...(agentRuntimeError ? { error: agentRuntimeError } : {}),
2227
- },
2228
- channels,
2229
- channelsByType,
2230
- queue: {
2231
- pending: messageQueue.getGlobalQueueLength(),
2232
- processing: messageQueue.getGlobalProcessingCount(),
2233
- },
2234
- stats: {
2235
- received: snap.lastHour.received,
2236
- sent: snap.lastHour.sent,
2237
- completed: snap.lastHour.completed,
2238
- errors: snap.lastHour.errors,
2239
- avgResponseMs: snap.lastHour.avgResponseMs,
2240
- },
2241
- controlAid: daemonCfg.aid
2242
- ? { aid: daemonCfg.aid, connected: controlChannel?.getAidState().status === 'connected' }
2243
- : undefined,
2244
- };
2245
- }, async (cmd, sessionId, delegationToken, delegationCommandHash) => {
2246
- const delegation = agentDelegationRegistry.validate(delegationToken, sessionId, delegationCommandHash);
2247
- if (!delegation.ok)
2248
- return { ok: false, code: delegation.code, error: delegation.reason };
2249
- return cmdHandler.handleCtl(cmd, sessionId);
2250
- });
2251
- // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
2252
- ipcServer.setAgentRegistry(agentRegistry);
2253
- ipcServer.setDingtalkContactBindExecutor({
2254
- register: (cmd) => registerPendingDingtalkContactBind(cmd),
2255
- isChannelReady: isContactBindChannelReady,
2256
- });
2257
- ipcServer.setFeishuContactBindExecutor({
2258
- register: (cmd) => registerPendingFeishuContactBind(cmd),
2259
- isChannelReady: isContactBindChannelReady,
2260
- });
2261
- ipcServer.setQQBotContactBindExecutor({
2262
- register: (cmd) => registerPendingQQBotContactBind(cmd),
2263
- isChannelReady: isContactBindChannelReady,
2264
- });
2265
- ipcServer.setWecomContactBindExecutor({
2266
- register: (cmd) => registerPendingWecomContactBind(cmd),
2267
- isChannelReady: isContactBindChannelReady,
2268
- });
2269
- ipcServer.setWechatContactBindExecutor({
2270
- register: (cmd) => registerPendingWechatContactBind(cmd),
2271
- isChannelReady: isContactBindChannelReady,
2272
- });
2273
- ipcServer.setMenuExecutor((payload, auth) => cmdHandler.execMenuForEcweb(payload, auth));
2274
- ipcServer.setConfigOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleConfigOperation(argv, sessionId, delegationToken, delegationCommandHash));
2275
- ipcServer.setContactOperationExecutor((argv, sessionId, delegationToken, delegationCommandHash) => cmdHandler.handleContactOperation(argv, sessionId, delegationToken, delegationCommandHash));
2276
- cmdHandler.setDaemonStatusProvider(() => {
2277
- const aidState = controlChannel?.getAidState?.();
2278
- return {
2279
- aid: daemonCfg.aid ?? null,
2280
- aun: aidState ? {
2281
- connected: aidState.status === 'connected',
2282
- status: aidState.status,
2283
- reconnectCount: aidState.reconnectCount ?? 0,
2284
- flapCount: aidState.flapCount ?? 0,
2285
- ...(aidState.lastError ? { lastError: String(aidState.lastError).slice(0, 80) } : {}),
2286
- ...(aidState.kickDetail?.reason ? { kickReason: String(aidState.kickDetail.reason).slice(0, 80) } : {}),
2287
- } : {
2288
- connected: false,
2289
- status: daemonCfg.aid ? 'disconnected' : 'disabled',
2290
- },
2291
- };
2292
- });
2293
- if (bindService) {
2294
- ipcServer.setBindExecutor({
2295
- begin: (cmd) => bindService.begin(cmd),
2296
- status: (taskId) => bindService.status(taskId),
2297
- cancel: (taskId) => bindService.cancel(taskId),
2348
+ // 注入 AUN AID 状态聚合器:遍历所有 aun 类型 channel,调 getAidState() 收集
2349
+ ipcServer.setAunAidProvider(() => {
2350
+ const out = [];
2351
+ for (const inst of channelInstances) {
2352
+ if (inst.channelType !== 'aun')
2353
+ continue;
2354
+ const ch = inst.channel;
2355
+ if (typeof ch?.getAidState === 'function') {
2356
+ try {
2357
+ const aidState = ch.getAidState();
2358
+ // 增强:添加队列状态
2359
+ const agentName = aidState.agentName || aidState.aid;
2360
+ const processing = messageQueue.getProcessingCountByAgent(agentName);
2361
+ const queued = messageQueue.getQueueLengthByAgent(agentName);
2362
+ out.push({
2363
+ ...aidState,
2364
+ queueStatus: { processing, queued }
2365
+ });
2366
+ }
2367
+ catch { /* ignore */ }
2368
+ }
2369
+ }
2370
+ return out;
2298
2371
  });
2299
- }
2300
- // 注入 AUN AID 状态聚合器:遍历所有 aun 类型 channel,调 getAidState() 收集
2301
- ipcServer.setAunAidProvider(() => {
2302
- const out = [];
2372
+ // 注入 Per-AID 统计收集器到所有 AUN channel 实例
2303
2373
  for (const inst of channelInstances) {
2304
2374
  if (inst.channelType !== 'aun')
2305
2375
  continue;
2306
2376
  const ch = inst.channel;
2307
- if (typeof ch?.getAidState === 'function') {
2308
- try {
2309
- const aidState = ch.getAidState();
2310
- // 增强:添加队列状态
2311
- const agentName = aidState.agentName || aidState.aid;
2312
- const processing = messageQueue.getProcessingCountByAgent(agentName);
2313
- const queued = messageQueue.getQueueLengthByAgent(agentName);
2314
- out.push({
2315
- ...aidState,
2316
- queueStatus: { processing, queued }
2317
- });
2318
- }
2319
- catch { /* ignore */ }
2320
- }
2321
- }
2322
- return out;
2323
- });
2324
- // 注入 Per-AID 统计收集器到所有 AUN channel 实例
2325
- for (const inst of channelInstances) {
2326
- if (inst.channelType !== 'aun')
2327
- continue;
2328
- const ch = inst.channel;
2329
- if (typeof ch?.setAidStatsCollector === 'function') {
2330
- ch.setAidStatsCollector(aidStatsCollector);
2331
- }
2332
- }
2333
- // 注入 Per-AID 统计 IPC provider
2334
- aidStatsCollector.setQueueStatsProvider((agentName) => ({
2335
- processing: messageQueue.getProcessingCountByAgent(agentName),
2336
- queued: messageQueue.getQueueLengthByAgent(agentName),
2337
- muted: messageQueue.isAgentMuted(agentName),
2338
- }));
2339
- ipcServer.setAunAidStatsProvider(() => aidStatsCollector.getAllSnapshots());
2340
- ipcServer.setAunAidStatsRecorder((params) => {
2341
- aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode, 'send');
2342
- });
2343
- ipcServer.setTaskRuntimeContextProvider(({ sessionId }) => responseEngine.getTaskRuntimeContext(sessionId));
2344
- ipcServer.setHandoffReturnExecutor((params) => responseEngine.returnHandoffResult(params));
2345
- ipcServer.setHandoffStatusExecutor(async (params) => {
2346
- if (!params.sessionId)
2347
- return { ok: false, code: 'HANDOFF_CALL_SESSION_REQUIRED', error: 'current session is required' };
2348
- const runtime = responseEngine.getTaskRuntimeContext(params.sessionId);
2349
- const session = await sessionManager.getSessionById(params.sessionId);
2350
- const selfAid = runtime?.selfAid || session?.selfAID;
2351
- if (!selfAid)
2352
- return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2353
- return handoffRuntime.status(selfAid, params.handoffId)
2354
- ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2355
- });
2356
- const resolveHandoffQueryAid = async (params) => {
2357
- if (params.sessionId) {
2377
+ if (typeof ch?.setAidStatsCollector === 'function') {
2378
+ ch.setAidStatsCollector(aidStatsCollector);
2379
+ }
2380
+ }
2381
+ // 注入 Per-AID 统计 IPC provider
2382
+ aidStatsCollector.setQueueStatsProvider((agentName) => ({
2383
+ processing: messageQueue.getProcessingCountByAgent(agentName),
2384
+ queued: messageQueue.getQueueLengthByAgent(agentName),
2385
+ muted: messageQueue.isAgentMuted(agentName),
2386
+ }));
2387
+ ipcServer.setAunAidStatsProvider(() => aidStatsCollector.getAllSnapshots());
2388
+ ipcServer.setAunAidStatsRecorder((params) => {
2389
+ aidStatsCollector.recordOutbound(params.aid, params.toPeer, Buffer.byteLength(params.text || '', 'utf-8'), params.text, false, params.encrypt, params.chatmode, 'send');
2390
+ });
2391
+ ipcServer.setTaskRuntimeContextProvider(({ sessionId }) => responseEngine.getTaskRuntimeContext(sessionId));
2392
+ ipcServer.setHandoffReturnExecutor((params) => responseEngine.returnHandoffResult(params));
2393
+ ipcServer.setHandoffStatusExecutor(async (params) => {
2394
+ if (!params.sessionId)
2395
+ return { ok: false, code: 'HANDOFF_CALL_SESSION_REQUIRED', error: 'current session is required' };
2358
2396
  const runtime = responseEngine.getTaskRuntimeContext(params.sessionId);
2359
2397
  const session = await sessionManager.getSessionById(params.sessionId);
2360
2398
  const selfAid = runtime?.selfAid || session?.selfAID;
2361
- if (!selfAid) {
2362
- return { ok: false, code: 'HANDOFF_CALL_SESSION_INVALID', error: 'current session is invalid' };
2399
+ if (!selfAid)
2400
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2401
+ return handoffRuntime.status(selfAid, params.handoffId)
2402
+ ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2403
+ });
2404
+ const resolveHandoffQueryAid = async (params) => {
2405
+ if (params.sessionId) {
2406
+ const runtime = responseEngine.getTaskRuntimeContext(params.sessionId);
2407
+ const session = await sessionManager.getSessionById(params.sessionId);
2408
+ const selfAid = runtime?.selfAid || session?.selfAID;
2409
+ if (!selfAid) {
2410
+ return { ok: false, code: 'HANDOFF_CALL_SESSION_INVALID', error: 'current session is invalid' };
2411
+ }
2412
+ if (params.agent && params.agent !== selfAid) {
2413
+ return { ok: false, code: 'HANDOFF_AGENT_SCOPE_MISMATCH', error: 'agent does not match the current session' };
2414
+ }
2415
+ return { ok: true, aid: selfAid };
2363
2416
  }
2364
- if (params.agent && params.agent !== selfAid) {
2365
- return { ok: false, code: 'HANDOFF_AGENT_SCOPE_MISMATCH', error: 'agent does not match the current session' };
2417
+ if (!params.agent) {
2418
+ return { ok: false, code: 'HANDOFF_AGENT_REQUIRED', error: '--agent is required outside a task context' };
2366
2419
  }
2367
- return { ok: true, aid: selfAid };
2368
- }
2369
- if (!params.agent) {
2370
- return { ok: false, code: 'HANDOFF_AGENT_REQUIRED', error: '--agent is required outside a task context' };
2371
- }
2372
- if (!agentRegistry.get(params.agent)) {
2373
- return { ok: false, code: 'HANDOFF_AGENT_NOT_FOUND', error: `agent not found: ${params.agent}` };
2374
- }
2375
- return { ok: true, aid: params.agent };
2376
- };
2377
- ipcServer.setHandoffListExecutor(async (params) => {
2378
- const scope = await resolveHandoffQueryAid(params);
2379
- if (!scope.ok)
2380
- return scope;
2381
- return handoffRuntime.listHandoffs({
2382
- selfAid: scope.aid,
2383
- state: params.state,
2384
- sessionId: params.filterSessionId,
2385
- limit: params.limit,
2420
+ if (!agentRegistry.get(params.agent)) {
2421
+ return { ok: false, code: 'HANDOFF_AGENT_NOT_FOUND', error: `agent not found: ${params.agent}` };
2422
+ }
2423
+ return { ok: true, aid: params.agent };
2424
+ };
2425
+ ipcServer.setHandoffListExecutor(async (params) => {
2426
+ const scope = await resolveHandoffQueryAid(params);
2427
+ if (!scope.ok)
2428
+ return scope;
2429
+ return handoffRuntime.listHandoffs({
2430
+ selfAid: scope.aid,
2431
+ state: params.state,
2432
+ sessionId: params.filterSessionId,
2433
+ limit: params.limit,
2434
+ });
2386
2435
  });
2387
- });
2388
- ipcServer.setHandoffTraceExecutor(async (params) => {
2389
- const scope = await resolveHandoffQueryAid(params);
2390
- if (!scope.ok)
2391
- return scope;
2392
- return handoffRuntime.traceHandoff(scope.aid, params.handoffId, params.limit)
2393
- ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2394
- });
2395
- ipcServer.setAunMsgSender(async (params) => {
2396
- const runtimeCausation = params.originSessionId
2397
- ? responseEngine.getTaskRuntimeContext(params.originSessionId)?.causation
2398
- : undefined;
2399
- const delegation = authorizeDelegatedAunMsgSend(agentDelegationRegistry, {
2400
- delegationToken: params.delegationToken,
2401
- delegationCommandHash: params.delegationCommandHash,
2402
- sessionId: params.originSessionId,
2403
- messageId: params.originMessageId,
2404
- aid: params.aid,
2405
- to: params.to,
2406
- scope: params.scope,
2407
- action: params.file ? 'file' : 'send',
2436
+ ipcServer.setHandoffTraceExecutor(async (params) => {
2437
+ const scope = await resolveHandoffQueryAid(params);
2438
+ if (!scope.ok)
2439
+ return scope;
2440
+ return handoffRuntime.traceHandoff(scope.aid, params.handoffId, params.limit)
2441
+ ?? { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff not found' };
2408
2442
  });
2409
- if (!delegation.ok) {
2410
- return { ok: false, error: delegation.reason, code: delegation.code };
2411
- }
2412
- const inst = channelInstances.find((candidate) => {
2413
- if (candidate.channelType !== 'aun')
2443
+ ipcServer.setAunMsgSender(async (params) => {
2444
+ const runtimeCausation = params.originSessionId
2445
+ ? responseEngine.getTaskRuntimeContext(params.originSessionId)?.causation
2446
+ : undefined;
2447
+ const delegation = authorizeDelegatedAunMsgSend(agentDelegationRegistry, {
2448
+ delegationToken: params.delegationToken,
2449
+ delegationCommandHash: params.delegationCommandHash,
2450
+ sessionId: params.originSessionId,
2451
+ messageId: params.originMessageId,
2452
+ aid: params.aid,
2453
+ to: params.to,
2454
+ scope: params.scope,
2455
+ action: params.file ? 'file' : 'send',
2456
+ });
2457
+ if (!delegation.ok) {
2458
+ return { ok: false, error: delegation.reason, code: delegation.code };
2459
+ }
2460
+ const inst = channelInstances.find((candidate) => {
2461
+ if (candidate.channelType !== 'aun')
2462
+ return false;
2463
+ const ch = candidate.channel;
2464
+ try {
2465
+ const aidState = typeof ch?.getAidState === 'function' ? ch.getAidState() : null;
2466
+ if (aidState?.aid === params.aid)
2467
+ return true;
2468
+ if (typeof ch?.getAid === 'function' && ch.getAid() === params.aid)
2469
+ return true;
2470
+ }
2471
+ catch { /* ignore */ }
2414
2472
  return false;
2415
- const ch = candidate.channel;
2416
- try {
2417
- const aidState = typeof ch?.getAidState === 'function' ? ch.getAidState() : null;
2418
- if (aidState?.aid === params.aid)
2419
- return true;
2420
- if (typeof ch?.getAid === 'function' && ch.getAid() === params.aid)
2421
- return true;
2473
+ });
2474
+ const ch = inst?.channel;
2475
+ if (!ch) {
2476
+ return { ok: false, error: `AUN channel not found for ${params.aid}`, code: 'AUN_CHANNEL_NOT_FOUND' };
2422
2477
  }
2423
- catch { /* ignore */ }
2424
- return false;
2425
- });
2426
- const ch = inst?.channel;
2427
- if (!ch) {
2428
- return { ok: false, error: `AUN channel not found for ${params.aid}`, code: 'AUN_CHANNEL_NOT_FOUND' };
2429
- }
2430
- const targetIsGroup = typeof ch.isGroupId === 'function' && ch.isGroupId(params.to);
2431
- if ((params.scope === 'group') !== targetIsGroup) {
2432
- return { ok: false, error: 'AUN target does not match the delegated send scope', code: 'UNSUPPORTED_TARGET' };
2433
- }
2434
- let payload = params.payload;
2435
- if (params.file) {
2436
- if (isHClassPath(params.file.filePath)) {
2437
- return {
2438
- ok: false,
2439
- error: 'H-class protected files cannot be uploaded by an agent task',
2440
- code: 'H_CLASS_PROTECTED',
2441
- };
2478
+ const targetIsGroup = typeof ch.isGroupId === 'function' && ch.isGroupId(params.to);
2479
+ if ((params.scope === 'group') !== targetIsGroup) {
2480
+ return { ok: false, error: 'AUN target does not match the delegated send scope', code: 'UNSUPPORTED_TARGET' };
2481
+ }
2482
+ let payload = params.payload;
2483
+ if (params.file) {
2484
+ if (isHClassPath(params.file.filePath)) {
2485
+ return {
2486
+ ok: false,
2487
+ error: 'H-class protected files cannot be uploaded by an agent task',
2488
+ code: 'H_CLASS_PROTECTED',
2489
+ };
2490
+ }
2491
+ if (typeof ch.buildDaemonFilePayload !== 'function') {
2492
+ return { ok: false, error: 'AUN channel does not support daemon file uploads', code: 'AUN_FILE_UNSUPPORTED' };
2493
+ }
2494
+ try {
2495
+ payload = await ch.buildDaemonFilePayload(params.file);
2496
+ }
2497
+ catch (error) {
2498
+ return {
2499
+ ok: false,
2500
+ error: error instanceof Error ? error.message : String(error),
2501
+ code: error?.code ?? 'AUN_FILE_UPLOAD_FAILED',
2502
+ };
2503
+ }
2442
2504
  }
2443
- if (typeof ch.buildDaemonFilePayload !== 'function') {
2444
- return { ok: false, error: 'AUN channel does not support daemon file uploads', code: 'AUN_FILE_UNSUPPORTED' };
2505
+ if (!payload) {
2506
+ return { ok: false, error: 'message payload is required', code: 'INVALID_PAYLOAD' };
2445
2507
  }
2446
- try {
2447
- payload = await ch.buildDaemonFilePayload(params.file);
2508
+ if (params.thread && typeof payload.thread_id !== 'string') {
2509
+ payload = { ...payload, thread_id: params.thread };
2448
2510
  }
2449
- catch (error) {
2450
- return {
2451
- ok: false,
2452
- error: error instanceof Error ? error.message : String(error),
2453
- code: error?.code ?? 'AUN_FILE_UPLOAD_FAILED',
2454
- };
2511
+ if (params.scope === 'group' && params.mentions?.length) {
2512
+ payload = { ...payload, mentions: params.mentions };
2455
2513
  }
2456
- }
2457
- if (!payload) {
2458
- return { ok: false, error: 'message payload is required', code: 'INVALID_PAYLOAD' };
2459
- }
2460
- if (params.thread && typeof payload.thread_id !== 'string') {
2461
- payload = { ...payload, thread_id: params.thread };
2462
- }
2463
- if (params.scope === 'group' && params.mentions?.length) {
2464
- payload = { ...payload, mentions: params.mentions };
2465
- }
2466
- let targetSessionId;
2467
- if (params.originSessionId && params.originMessageId) {
2468
- try {
2469
- const created = await handoffRuntime.createOutbound({
2470
- selfAid: params.aid,
2471
- to: params.to,
2472
- originSessionId: params.originSessionId,
2473
- originMessageId: params.originMessageId,
2474
- payload,
2475
- encrypt: params.encrypt === true,
2476
- thread: params.thread,
2477
- targetChatType: params.scope === 'group' ? 'group' : 'private',
2478
- explicitReturnPolicy: params.returnPolicy,
2479
- originAuthorization: {
2480
- actorId: delegation.grant.actorId,
2481
- channelKey: delegation.grant.channel,
2482
- channelType: delegation.grant.channelType,
2483
- chatType: delegation.grant.chatType,
2484
- peerKey: delegation.grant.peerKey,
2485
- },
2486
- causation: runtimeCausation,
2487
- });
2488
- targetSessionId = created.targetSession.id;
2489
- if (created.crossSession && created.handoff) {
2514
+ let targetSessionId;
2515
+ if (params.originSessionId && params.originMessageId) {
2516
+ try {
2517
+ const created = await handoffRuntime.createOutbound({
2518
+ selfAid: params.aid,
2519
+ to: params.to,
2520
+ originSessionId: params.originSessionId,
2521
+ originMessageId: params.originMessageId,
2522
+ payload,
2523
+ encrypt: params.encrypt === true,
2524
+ thread: params.thread,
2525
+ targetChatType: params.scope === 'group' ? 'group' : 'private',
2526
+ explicitReturnPolicy: params.returnPolicy,
2527
+ originAuthorization: {
2528
+ actorId: delegation.grant.actorId,
2529
+ channelKey: delegation.grant.channel,
2530
+ channelType: delegation.grant.channelType,
2531
+ chatType: delegation.grant.chatType,
2532
+ peerKey: delegation.grant.peerKey,
2533
+ },
2534
+ causation: runtimeCausation,
2535
+ });
2536
+ targetSessionId = created.targetSession.id;
2537
+ if (created.crossSession && created.handoff) {
2538
+ return {
2539
+ ok: true,
2540
+ status: created.handoff.state === 'target_sent' ? 'delivered' : 'queued',
2541
+ message_id: created.handoff.target_message_id ?? undefined,
2542
+ handoff_id: created.handoff.handoff_id,
2543
+ target_session_id: created.targetSession.id,
2544
+ };
2545
+ }
2546
+ if (params.scope === 'msg' && !payload.ref_message_id) {
2547
+ payload.ref_message_id = params.originMessageId;
2548
+ }
2549
+ }
2550
+ catch (error) {
2551
+ const code = error?.code;
2552
+ const handoffId = error?.handoffId
2553
+ ?? error?.blockingHandoffId;
2490
2554
  return {
2491
- ok: true,
2492
- status: created.handoff.state === 'target_sent' ? 'delivered' : 'queued',
2493
- message_id: created.handoff.target_message_id ?? undefined,
2494
- handoff_id: created.handoff.handoff_id,
2495
- target_session_id: created.targetSession.id,
2555
+ ok: false,
2556
+ error: error instanceof Error ? error.message : String(error),
2557
+ code,
2558
+ ...(handoffId ? { handoff_id: handoffId } : {}),
2496
2559
  };
2497
2560
  }
2498
- if (params.scope === 'msg' && !payload.ref_message_id) {
2499
- payload.ref_message_id = params.originMessageId;
2500
- }
2501
2561
  }
2502
- catch (error) {
2503
- const code = error?.code;
2504
- const handoffId = error?.handoffId
2505
- ?? error?.blockingHandoffId;
2506
- return {
2507
- ok: false,
2508
- error: error instanceof Error ? error.message : String(error),
2509
- code,
2510
- ...(handoffId ? { handoff_id: handoffId } : {}),
2511
- };
2562
+ if (params.scope === 'group') {
2563
+ if (typeof ch.sendDaemonGroupMsg !== 'function') {
2564
+ return { ok: false, error: 'AUN channel does not support daemon group sends', code: 'AUN_GROUP_UNSUPPORTED' };
2565
+ }
2566
+ return await ch.sendDaemonGroupMsg({
2567
+ groupId: params.to,
2568
+ payload,
2569
+ mentions: params.mentions,
2570
+ encrypt: params.encrypt,
2571
+ log: params.log ? { ...params.log, sessionId: targetSessionId ?? params.log.sessionId } : undefined,
2572
+ });
2512
2573
  }
2513
- }
2514
- if (params.scope === 'group') {
2515
- if (typeof ch.sendDaemonGroupMsg !== 'function') {
2516
- return { ok: false, error: 'AUN channel does not support daemon group sends', code: 'AUN_GROUP_UNSUPPORTED' };
2574
+ if (typeof ch.sendDaemonMsg !== 'function') {
2575
+ return { ok: false, error: 'AUN channel does not support daemon private sends', code: 'AUN_MSG_UNSUPPORTED' };
2517
2576
  }
2518
- return await ch.sendDaemonGroupMsg({
2519
- groupId: params.to,
2577
+ return await ch.sendDaemonMsg({
2578
+ to: params.to,
2520
2579
  payload,
2521
- mentions: params.mentions,
2522
2580
  encrypt: params.encrypt,
2581
+ causation: runtimeCausation,
2523
2582
  log: params.log ? { ...params.log, sessionId: targetSessionId ?? params.log.sessionId } : undefined,
2524
2583
  });
2525
- }
2526
- if (typeof ch.sendDaemonMsg !== 'function') {
2527
- return { ok: false, error: 'AUN channel does not support daemon private sends', code: 'AUN_MSG_UNSUPPORTED' };
2528
- }
2529
- return await ch.sendDaemonMsg({
2530
- to: params.to,
2531
- payload,
2532
- encrypt: params.encrypt,
2533
- causation: runtimeCausation,
2534
- log: params.log ? { ...params.log, sessionId: targetSessionId ?? params.log.sessionId } : undefined,
2535
2584
  });
2536
- });
2537
- // ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
2538
- const reloadHooks = buildReloadHooks({
2539
- channelLoader,
2540
- channelInstances,
2541
- registerChannelInstance,
2542
- unregisterChannelInstance: (channelName) => {
2543
- markChannelDisconnected(channelName);
2544
- processor.unregisterChannel(channelName);
2545
- cmdHandler.unregisterChannel(channelName);
2546
- msgBridge.removeChannel(channelName);
2547
- },
2548
- onChannelStarted: (inst) => {
2549
- // startChannel 重建渠道时重新注入 AidStatsCollector(与 hot-load 路径对齐)
2550
- if (inst.channelType === 'aun') {
2551
- const ch = inst.channel;
2552
- if (typeof ch?.setAidStatsCollector === 'function')
2553
- ch.setAidStatsCollector(aidStatsCollector);
2554
- }
2555
- },
2556
- onChannelConnected: markChannelConnected,
2557
- messageQueue,
2558
- handoffRuntime,
2559
- });
2560
- // Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
2561
- globalThis.__evolcore_reloadHooks = reloadHooks;
2562
- // Hot-load handler: dynamically add a new agent at runtime
2563
- globalThis.__evolcore_hotLoadAgent = async (aid) => {
2564
- handoffRuntime.pauseAgent(aid);
2565
- let resumed = false;
2566
- try {
2567
- agentRuntimeState = 'starting';
2568
- agentRuntimeError = undefined;
2569
- const agent = agentRegistry.loadNewAgent(aid);
2570
- if (!agent) {
2571
- agentRuntimeState = agentRegistry.runnableAgents().length > 0 ? 'running' : 'error';
2572
- agentRuntimeError = `Failed to load agent ${aid}`;
2573
- throw new Error(agentRuntimeError);
2574
- }
2575
- const newAgentInstances = agentLoader.createForAgent(agent, {
2576
- onSessionIdUpdate: async (sessionId, agentSessionId) => {
2577
- await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
2578
- },
2579
- });
2580
- for (const inst of newAgentInstances) {
2581
- agentMap.set(`${inst.evolagentName}::${inst.baseagent}`, inst.agent);
2582
- inst.agent.setPermissionGateway?.(permissionGateway);
2583
- inst.agent.setCompactStartCallback?.((sessionId) => {
2584
- processor.handleCompactStart(sessionId);
2585
- });
2586
- }
2587
- if (newAgentInstances.length === 0) {
2588
- agent.status = 'error';
2589
- agent.error = 'No baseagent runner created for hot-loaded agent';
2590
- agentRuntimeState = 'error';
2591
- agentRuntimeError = agent.error;
2592
- throw new Error(agent.error);
2593
- }
2594
- // 创建 channels
2595
- const instances = await channelLoader.createForAgent(agent);
2596
- for (const inst of instances) {
2597
- registerChannelInstance(inst);
2585
+ // ── Reload hooks: enable agentRegistry.reload() to drain/disconnect/restart channels ──
2586
+ const reloadHooks = buildReloadHooks({
2587
+ channelLoader,
2588
+ channelInstances,
2589
+ registerChannelInstance,
2590
+ unregisterChannelInstance: (channelName) => {
2591
+ markChannelDisconnected(channelName);
2592
+ processor.unregisterChannel(channelName);
2593
+ cmdHandler.unregisterChannel(channelName);
2594
+ msgBridge.removeChannel(channelName);
2595
+ },
2596
+ onChannelStarted: (inst) => {
2597
+ // startChannel 重建渠道时重新注入 AidStatsCollector(与 hot-load 路径对齐)
2598
2598
  if (inst.channelType === 'aun') {
2599
2599
  const ch = inst.channel;
2600
2600
  if (typeof ch?.setAidStatsCollector === 'function')
2601
2601
  ch.setAidStatsCollector(aidStatsCollector);
2602
2602
  }
2603
- agent.channels.set(inst.adapter.channelKey, inst.adapter);
2604
- channelInstances.push(inst);
2605
- }
2606
- agent.status = 'running';
2607
- // 连接
2608
- await channelLoader.connectAll(instances, { onConnected: markChannelConnected });
2609
- await handoffRuntime.recover([aid]);
2610
- await ensureTriggerSchedulerStarted(agent);
2611
- handoffRuntime.resumeAgent(aid);
2612
- resumed = true;
2613
- agentRuntimeState = 'running';
2614
- agentRuntimeError = undefined;
2615
- logger.info(`[HotLoad] Agent ${aid} online with ${instances.length} channel(s)`);
2616
- }
2617
- finally {
2618
- if (!resumed)
2619
- handoffRuntime.resumeAgent(aid);
2620
- }
2621
- };
2622
- // Full resync handler: scan disk, load new agents, unload removed/disabled, reload changed
2623
- globalThis.__evolcore_resyncAgents = async () => {
2624
- // 先清 kit 缓存:'kits' 组(manifest / fragment / schema / 角色模板)走 on-reload
2625
- // 策略,平时不查盘。放在扫盘与 reload 之前,本轮上下线的 agent 才能读到磁盘上的
2626
- // 最新版本——放在末尾的话,这一轮全用旧值,改动要等下一次 resync 才生效。
2627
- invalidateKitCache();
2628
- const { loadAllAgents: scanAgents, loadDefaults: readDefaults } = await import('./config-store.js');
2629
- const { resolveEffective } = await import('./config/config-manager.js');
2630
- const freshDefaults = readDefaults();
2631
- const { agents: diskAgents } = scanAgents();
2632
- const diskAidSet = new Set(diskAgents.map(a => a.aid));
2633
- const results = [];
2634
- // 1. 下线:运行时有但磁盘上没有 / disabled 的
2635
- for (const [aid, agent] of [...agentRegistry.agents.entries()]) {
2636
- const diskCfg = diskAgents.find(a => a.aid === aid);
2637
- if (!diskCfg || diskCfg.enabled === false) {
2638
- handoffRuntime.pauseAgent(aid);
2639
- try {
2640
- await handoffRuntime.drainAgent(aid);
2603
+ },
2604
+ onChannelConnected: markChannelConnected,
2605
+ messageQueue,
2606
+ handoffRuntime,
2607
+ });
2608
+ // Make reload hooks accessible to IPC handler & ctl handler (both run in this process)
2609
+ globalThis.__evolcore_reloadHooks = reloadHooks;
2610
+ // Hot-load handler: dynamically add a new agent at runtime
2611
+ globalThis.__evolcore_hotLoadAgent = async (aid) => {
2612
+ handoffRuntime.pauseAgent(aid);
2613
+ let resumed = false;
2614
+ try {
2615
+ agentRuntimeState = 'starting';
2616
+ agentRuntimeError = undefined;
2617
+ const agent = agentRegistry.loadNewAgent(aid);
2618
+ if (!agent) {
2619
+ agentRuntimeState = agentRegistry.runnableAgents().length > 0 ? 'running' : 'error';
2620
+ agentRuntimeError = `Failed to load agent ${aid}`;
2621
+ throw new Error(agentRuntimeError);
2641
2622
  }
2642
- catch (error) {
2643
- handoffRuntime.resumeAgent(aid);
2644
- results.push(`⚠ ${aid}: ${error instanceof Error ? error.message : String(error)}`);
2645
- continue;
2623
+ const creationErrors = [];
2624
+ const newAgentInstances = agentLoader.createForAgent(agent, {
2625
+ onSessionIdUpdate: async (sessionId, agentSessionId) => {
2626
+ await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
2627
+ },
2628
+ }, creationErrors);
2629
+ for (const inst of newAgentInstances) {
2630
+ agentMap.set(`${inst.evolagentName}::${inst.baseagent}`, inst.agent);
2631
+ inst.agent.setPermissionGateway?.(permissionGateway);
2632
+ inst.agent.setCompactStartCallback?.((sessionId) => {
2633
+ processor.handleCompactStart(sessionId);
2634
+ });
2646
2635
  }
2647
- await triggerSchedulers.get(aid)?.stop();
2648
- triggerSchedulers.delete(aid);
2649
- triggerSchedulerStarts.delete(aid);
2650
- // 断开所有 channels
2651
- for (const chName of agent.channelInstanceNames()) {
2652
- const inst = channelInstances.find(i => i.adapter.channelName === chName);
2653
- if (inst) {
2654
- try {
2655
- await inst.disconnect();
2656
- }
2657
- catch { }
2658
- markChannelDisconnected(inst.adapter.channelName);
2659
- const idx = channelInstances.indexOf(inst);
2660
- if (idx >= 0)
2661
- channelInstances.splice(idx, 1);
2636
+ if (newAgentInstances.length === 0) {
2637
+ const creationDetail = creationErrors.length > 0
2638
+ ? creationErrors.map(({ baseagent, message }) => `${baseagent}: ${message}`).join('; ')
2639
+ : `active_baseagent=${agent.config.active_baseagent ?? '(unset)'}, configured=${Object.keys(agent.config.baseagents ?? {}).join(',') || '(none)'}`;
2640
+ agent.status = 'error';
2641
+ agent.error = creationErrors.length > 0
2642
+ ? creationDetail
2643
+ : `No baseagent runner created for hot-loaded agent: ${creationDetail}`;
2644
+ agentRuntimeState = 'error';
2645
+ agentRuntimeError = agent.error;
2646
+ throw new Error(agent.error);
2647
+ }
2648
+ // 创建 channels
2649
+ const instances = await channelLoader.createForAgent(agent);
2650
+ for (const inst of instances) {
2651
+ registerChannelInstance(inst);
2652
+ if (inst.channelType === 'aun') {
2653
+ const ch = inst.channel;
2654
+ if (typeof ch?.setAidStatsCollector === 'function')
2655
+ ch.setAidStatsCollector(aidStatsCollector);
2662
2656
  }
2657
+ agent.channels.set(inst.adapter.channelKey, inst.adapter);
2658
+ channelInstances.push(inst);
2663
2659
  }
2664
- agentRegistry.agents.delete(aid);
2665
- results.push(`- ${aid} (offline)`);
2666
- continue;
2667
- }
2668
- }
2669
- // 2. 新增:磁盘上有但运行时没有的
2670
- for (const cfg of diskAgents) {
2671
- if (cfg.enabled === false)
2672
- continue;
2673
- if (agentRegistry.agents.has(cfg.aid))
2674
- continue;
2675
- try {
2676
- await globalThis.__evolcore_hotLoadAgent(cfg.aid);
2677
- results.push(`+ ${cfg.aid} (online)`);
2678
- }
2679
- catch (e) {
2680
- results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
2681
- }
2682
- }
2683
- // 3. 已有的:重新 reload(config 可能改了)
2684
- const hooks = globalThis.__evolcore_reloadHooks;
2685
- for (const cfg of diskAgents) {
2686
- if (cfg.enabled === false)
2687
- continue;
2688
- if (!agentRegistry.agents.has(cfg.aid))
2689
- continue;
2690
- // 只有磁盘上存在且运行时也存在的才 reload
2691
- try {
2692
- await agentRegistry.reload(cfg.aid, hooks);
2693
- const runtimeAgent = agentRegistry.get(cfg.aid);
2694
- if (runtimeAgent)
2695
- await startTriggerScheduler(runtimeAgent);
2696
- results.push(`↻ ${cfg.aid} (reloaded)`);
2660
+ agent.status = 'running';
2661
+ // 连接
2662
+ await channelLoader.connectAll(instances, { onConnected: markChannelConnected });
2663
+ await handoffRuntime.recover([aid]);
2664
+ await ensureTriggerSchedulerStarted(agent);
2665
+ handoffRuntime.resumeAgent(aid);
2666
+ resumed = true;
2667
+ agentRuntimeState = 'running';
2668
+ agentRuntimeError = undefined;
2669
+ logger.info(`[HotLoad] ✓ Agent ${aid} online with ${instances.length} channel(s)`);
2697
2670
  }
2698
- catch (e) {
2699
- results.push(`⚠ ${cfg.aid}: ${e?.message || e}`);
2671
+ finally {
2672
+ if (!resumed)
2673
+ handoffRuntime.resumeAgent(aid);
2700
2674
  }
2701
- }
2702
- // 重建 channel index(kit 缓存已在本轮开头清过)
2703
- agentRegistry.channelIndex.clear();
2704
- agentRegistry.buildChannelIndex();
2705
- logger.info(`[Resync] Done: ${results.length} agent(s) processed`);
2706
- return results;
2707
- };
2708
- ipcServer.setStatsProvider(() => statsCollector.getSnapshot());
2709
- ipcServer.setAgentStatsProvider(() => agentRegistry.list().map((agent) => {
2710
- const snap = statsCollector.getSnapshot(agent.aid);
2711
- return {
2712
- aid: agent.aid,
2713
- received: snap.lastHour.received,
2714
- sent: snap.lastHour.sent,
2715
- completed: snap.lastHour.completed,
2716
- errors: snap.lastHour.errors,
2717
- interrupts: snap.lastHour.interrupts,
2718
- avgResponseMs: snap.lastHour.avgResponseMs,
2719
- processing: messageQueue.getProcessingCountByAgent(agent.aid),
2720
- queued: messageQueue.getQueueLengthByAgent(agent.aid),
2721
- muted: messageQueue.isAgentMuted(agent.aid),
2722
2675
  };
2723
- }));
2724
- // Queue snapshot & action (for ec queue --agent CLI)
2725
- ipcServer.setQueueSnapshotProvider((params) => {
2726
- const handle = agentRegistry.get(params.agent);
2727
- const agentName = handle?.name;
2728
- if (!agentName)
2729
- return [];
2730
- return messageQueue.getQueueItemsByAgent(agentName);
2731
- });
2732
- ipcServer.setQueueActionExecutor(async (params) => {
2733
- const handle = agentRegistry.get(params.agent);
2734
- const agentName = handle?.name;
2735
- if (!agentName)
2736
- return { ok: false, error: `agent not found: ${params.agent}` };
2737
- switch (params.action) {
2738
- case 'clear':
2739
- return { ok: true, cleared: messageQueue.clearByAgent(agentName) };
2740
- case 'cancel':
2741
- if (!params.messageId)
2742
- return { ok: false, error: 'missing messageId' };
2743
- return { ok: true, cancelled: messageQueue.cancelMessageById(agentName, params.messageId) };
2744
- case 'interrupt':
2745
- if (!params.sessionKey)
2746
- return { ok: false, error: 'missing sessionKey' };
2747
- {
2748
- const sessionId = messageQueue.findSessionIdBySessionKey(params.sessionKey);
2749
- if (!sessionId)
2750
- return { ok: false, error: `session not found: ${params.sessionKey}` };
2751
- return { ok: true, interrupted: await messageQueue.interruptBySession(sessionId) };
2676
+ // Full resync handler: scan disk, load new agents, unload removed/disabled, reload changed
2677
+ globalThis.__evolcore_resyncAgents = async () => {
2678
+ // 先清 kit 缓存:'kits' 组(manifest / fragment / schema / 角色模板)走 on-reload
2679
+ // 策略,平时不查盘。放在扫盘与 reload 之前,本轮上下线的 agent 才能读到磁盘上的
2680
+ // 最新版本——放在末尾的话,这一轮全用旧值,改动要等下一次 resync 才生效。
2681
+ invalidateKitCache();
2682
+ const { loadAllAgents: scanAgents, loadDefaults: readDefaults } = await import('./config-store.js');
2683
+ const { resolveEffective } = await import('./config/config-manager.js');
2684
+ const freshDefaults = readDefaults();
2685
+ const { agents: diskAgents } = scanAgents();
2686
+ const diskAidSet = new Set(diskAgents.map(a => a.aid));
2687
+ const results = [];
2688
+ // 1. 下线:运行时有但磁盘上没有 / disabled 的
2689
+ for (const [aid, agent] of [...agentRegistry.agents.entries()]) {
2690
+ const diskCfg = diskAgents.find(a => a.aid === aid);
2691
+ if (!diskCfg || diskCfg.enabled === false) {
2692
+ handoffRuntime.pauseAgent(aid);
2693
+ try {
2694
+ await handoffRuntime.drainAgent(aid);
2695
+ }
2696
+ catch (error) {
2697
+ handoffRuntime.resumeAgent(aid);
2698
+ results.push(`⚠ ${aid}: ${error instanceof Error ? error.message : String(error)}`);
2699
+ continue;
2700
+ }
2701
+ await triggerSchedulers.get(aid)?.stop();
2702
+ triggerSchedulers.delete(aid);
2703
+ triggerSchedulerStarts.delete(aid);
2704
+ // 断开所有 channels
2705
+ for (const chName of agent.channelInstanceNames()) {
2706
+ const inst = channelInstances.find(i => i.adapter.channelName === chName);
2707
+ if (inst) {
2708
+ try {
2709
+ await inst.disconnect();
2710
+ }
2711
+ catch { }
2712
+ markChannelDisconnected(inst.adapter.channelName);
2713
+ const idx = channelInstances.indexOf(inst);
2714
+ if (idx >= 0)
2715
+ channelInstances.splice(idx, 1);
2716
+ }
2717
+ }
2718
+ agentRegistry.agents.delete(aid);
2719
+ results.push(`- ${aid} (offline)`);
2720
+ continue;
2752
2721
  }
2753
- default:
2754
- return { ok: false, error: `unknown action: ${params.action}` };
2755
- }
2756
- });
2757
- ipcServer.setTriggerExecutor(async (cmd) => {
2758
- const schedulerFor = (agentAid) => {
2759
- const scheduler = triggerSchedulers.get(agentAid);
2760
- if (!scheduler)
2761
- throw new Error(`trigger scheduler not found for agent: ${agentAid}`);
2762
- return scheduler;
2763
- };
2764
- const requireAgent = (agentAid) => {
2765
- if (typeof agentAid !== 'string' || !agentAid)
2766
- throw new Error('missing agentAid');
2767
- if (agentAid === daemonTriggerOwner.aid)
2768
- return agentAid;
2769
- if (!agentRegistry.get(agentAid))
2770
- throw new Error(`agent not found: ${agentAid}`);
2771
- return agentAid;
2772
- };
2773
- const authorizeTrigger = async (agentAid, operation, triggerId) => {
2774
- const actor = await authenticatedTriggerActor(agentAid, cmd.actorSessionId, cmd.delegationToken, cmd.delegationCommandHash, cmd.controlToken);
2775
- const isCrossAgent = !actor.control && actor.selfAid !== agentAid;
2776
- if (!isCrossAgentTriggerOperationAllowed({
2777
- control: actor.control,
2778
- taskAgentAid: actor.selfAid,
2779
- targetAgentAid: agentAid,
2780
- targetManagement: actor.management,
2781
- operation,
2782
- })) {
2783
- if (isCrossAgent && !actor.management) {
2784
- throw new Error('cross-agent trigger audit requires owner/admin access on the target agent');
2722
+ }
2723
+ // 2. 新增:磁盘上有但运行时没有的
2724
+ for (const cfg of diskAgents) {
2725
+ if (cfg.enabled === false)
2726
+ continue;
2727
+ if (agentRegistry.agents.has(cfg.aid))
2728
+ continue;
2729
+ try {
2730
+ await globalThis.__evolcore_hotLoadAgent(cfg.aid);
2731
+ results.push(`+ ${cfg.aid} (online)`);
2785
2732
  }
2786
- if (isCrossAgent && !isCrossAgentTriggerReadOperation(operation)) {
2787
- throw new Error('cross-agent trigger operations are limited to list, show, and history');
2733
+ catch (e) {
2734
+ results.push(`✗ ${cfg.aid}: ${e?.message || e}`);
2788
2735
  }
2789
- throw new Error('trigger agent does not match authenticated task agent');
2790
2736
  }
2791
- const operationDecision = authorizeOperation({
2792
- source: actor.control ? 'control' : 'agent-tool',
2793
- subject: actor.subject,
2794
- intent: {
2737
+ // 3. 已有的:重新 reload(config 可能改了)
2738
+ const hooks = globalThis.__evolcore_reloadHooks;
2739
+ for (const cfg of diskAgents) {
2740
+ if (cfg.enabled === false)
2741
+ continue;
2742
+ if (!agentRegistry.agents.has(cfg.aid))
2743
+ continue;
2744
+ // 只有磁盘上存在且运行时也存在的才 reload
2745
+ try {
2746
+ await agentRegistry.reload(cfg.aid, hooks);
2747
+ const runtimeAgent = agentRegistry.get(cfg.aid);
2748
+ if (runtimeAgent)
2749
+ await startTriggerScheduler(runtimeAgent);
2750
+ results.push(`↻ ${cfg.aid} (reloaded)`);
2751
+ }
2752
+ catch (e) {
2753
+ results.push(`⚠ ${cfg.aid}: ${e?.message || e}`);
2754
+ }
2755
+ }
2756
+ // 重建 channel index(kit 缓存已在本轮开头清过)
2757
+ agentRegistry.channelIndex.clear();
2758
+ agentRegistry.buildChannelIndex();
2759
+ logger.info(`[Resync] Done: ${results.length} agent(s) processed`);
2760
+ return results;
2761
+ };
2762
+ ipcServer.setStatsProvider(() => statsCollector.getSnapshot());
2763
+ ipcServer.setAgentStatsProvider(() => agentRegistry.list().map((agent) => {
2764
+ const snap = statsCollector.getSnapshot(agent.aid);
2765
+ return {
2766
+ aid: agent.aid,
2767
+ received: snap.lastHour.received,
2768
+ sent: snap.lastHour.sent,
2769
+ completed: snap.lastHour.completed,
2770
+ errors: snap.lastHour.errors,
2771
+ interrupts: snap.lastHour.interrupts,
2772
+ avgResponseMs: snap.lastHour.avgResponseMs,
2773
+ processing: messageQueue.getProcessingCountByAgent(agent.aid),
2774
+ queued: messageQueue.getQueueLengthByAgent(agent.aid),
2775
+ muted: messageQueue.isAgentMuted(agent.aid),
2776
+ };
2777
+ }));
2778
+ // Queue snapshot & action (for ec queue --agent CLI)
2779
+ ipcServer.setQueueSnapshotProvider((params) => {
2780
+ const handle = agentRegistry.get(params.agent);
2781
+ const agentName = handle?.name;
2782
+ if (!agentName)
2783
+ return [];
2784
+ return messageQueue.getQueueItemsByAgent(agentName);
2785
+ });
2786
+ ipcServer.setQueueActionExecutor(async (params) => {
2787
+ const handle = agentRegistry.get(params.agent);
2788
+ const agentName = handle?.name;
2789
+ if (!agentName)
2790
+ return { ok: false, error: `agent not found: ${params.agent}` };
2791
+ switch (params.action) {
2792
+ case 'clear':
2793
+ return { ok: true, cleared: messageQueue.clearByAgent(agentName) };
2794
+ case 'cancel':
2795
+ if (!params.messageId)
2796
+ return { ok: false, error: 'missing messageId' };
2797
+ return { ok: true, cancelled: messageQueue.cancelMessageById(agentName, params.messageId) };
2798
+ case 'interrupt':
2799
+ if (!params.sessionKey)
2800
+ return { ok: false, error: 'missing sessionKey' };
2801
+ {
2802
+ const sessionId = messageQueue.findSessionIdBySessionKey(params.sessionKey);
2803
+ if (!sessionId)
2804
+ return { ok: false, error: `session not found: ${params.sessionKey}` };
2805
+ return { ok: true, interrupted: await messageQueue.interruptBySession(sessionId) };
2806
+ }
2807
+ default:
2808
+ return { ok: false, error: `unknown action: ${params.action}` };
2809
+ }
2810
+ });
2811
+ ipcServer.setTriggerExecutor(async (cmd) => {
2812
+ const schedulerFor = (agentAid) => {
2813
+ const scheduler = triggerSchedulers.get(agentAid);
2814
+ if (!scheduler)
2815
+ throw new Error(`trigger scheduler not found for agent: ${agentAid}`);
2816
+ return scheduler;
2817
+ };
2818
+ const requireAgent = (agentAid) => {
2819
+ if (typeof agentAid !== 'string' || !agentAid)
2820
+ throw new Error('missing agentAid');
2821
+ if (agentAid === daemonTriggerOwner.aid)
2822
+ return agentAid;
2823
+ if (!agentRegistry.get(agentAid))
2824
+ throw new Error(`agent not found: ${agentAid}`);
2825
+ return agentAid;
2826
+ };
2827
+ const authorizeTrigger = async (agentAid, operation, triggerId) => {
2828
+ const actor = await authenticatedTriggerActor(agentAid, cmd.actorSessionId, cmd.delegationToken, cmd.delegationCommandHash, cmd.controlToken);
2829
+ const isCrossAgent = !actor.control && actor.selfAid !== agentAid;
2830
+ if (!isCrossAgentTriggerOperationAllowed({
2831
+ control: actor.control,
2832
+ taskAgentAid: actor.selfAid,
2833
+ targetAgentAid: agentAid,
2834
+ targetManagement: actor.management,
2795
2835
  operation,
2796
- scope: 'relation',
2836
+ })) {
2837
+ if (isCrossAgent && !actor.management) {
2838
+ throw new Error('cross-agent trigger audit requires owner/admin access on the target agent');
2839
+ }
2840
+ if (isCrossAgent && !isCrossAgentTriggerReadOperation(operation)) {
2841
+ throw new Error('cross-agent trigger operations are limited to list, show, and history');
2842
+ }
2843
+ throw new Error('trigger agent does not match authenticated task agent');
2844
+ }
2845
+ const operationDecision = authorizeOperation({
2797
2846
  source: actor.control ? 'control' : 'agent-tool',
2798
- args: {
2799
- self: agentAid,
2800
- peer: actor.origin.peerId,
2801
- peerKey: actor.subject.peerKey,
2802
- ...(triggerId ? { triggerId } : {}),
2847
+ subject: actor.subject,
2848
+ intent: {
2849
+ operation,
2850
+ scope: 'relation',
2851
+ source: actor.control ? 'control' : 'agent-tool',
2852
+ args: {
2853
+ self: agentAid,
2854
+ peer: actor.origin.peerId,
2855
+ peerKey: actor.subject.peerKey,
2856
+ ...(triggerId ? { triggerId } : {}),
2857
+ },
2803
2858
  },
2804
- },
2805
- });
2806
- if (!operationDecision.allow)
2807
- throw new Error(operationDecision.reason);
2808
- if (actor.management)
2809
- return actor;
2810
- if (!triggerId)
2859
+ });
2860
+ if (!operationDecision.allow)
2861
+ throw new Error(operationDecision.reason);
2862
+ if (actor.management)
2863
+ return actor;
2864
+ if (!triggerId)
2865
+ return actor;
2866
+ const definition = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === triggerId);
2867
+ if (!definition
2868
+ || definition.origin?.peerId !== actor.origin?.peerId
2869
+ || definition.origin?.channelKey !== actor.origin?.channelKey) {
2870
+ throw new Error('trigger not found or access denied');
2871
+ }
2811
2872
  return actor;
2812
- const definition = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === triggerId);
2813
- if (!definition
2814
- || definition.origin?.peerId !== actor.origin?.peerId
2815
- || definition.origin?.channelKey !== actor.origin?.channelKey) {
2816
- throw new Error('trigger not found or access denied');
2817
- }
2818
- return actor;
2819
- };
2820
- switch (cmd.type) {
2821
- case 'trigger.list': {
2822
- const agentAid = requireAgent(cmd.agentAid);
2823
- const actor = await authorizeTrigger(agentAid, 'trigger.list');
2824
- const scheduler = schedulerFor(agentAid);
2825
- const definitions = scheduler.list({ all: cmd.all === true }).filter(trigger => actor.management
2826
- || (trigger.origin?.peerId === actor.origin?.peerId && trigger.origin?.channelKey === actor.origin?.channelKey));
2827
- return { ok: true, triggers: scheduler.listItems(definitions) };
2828
- }
2829
- case 'trigger.show': {
2830
- const agentAid = requireAgent(cmd.agentAid);
2831
- if (!cmd.triggerId)
2832
- throw new Error('missing triggerId');
2833
- await authorizeTrigger(agentAid, 'trigger.show', cmd.triggerId);
2834
- return {
2835
- ok: true,
2836
- ...schedulerFor(agentAid).show(cmd.triggerId, {
2837
- includeScriptPreview: cmd.includeScriptPreview !== false,
2838
- }),
2839
- };
2840
- }
2841
- case 'trigger.history': {
2842
- const agentAid = requireAgent(cmd.agentAid);
2843
- const limit = cmd.limit === undefined ? 100 : Number(cmd.limit);
2844
- if (!Number.isInteger(limit) || limit <= 0 || limit > 10_000)
2845
- throw new Error('invalid history limit');
2846
- if (cmd.triggerId !== undefined && (typeof cmd.triggerId !== 'string' || !cmd.triggerId)) {
2847
- throw new Error('invalid triggerId');
2873
+ };
2874
+ switch (cmd.type) {
2875
+ case 'trigger.list': {
2876
+ const agentAid = requireAgent(cmd.agentAid);
2877
+ const actor = await authorizeTrigger(agentAid, 'trigger.list');
2878
+ const scheduler = schedulerFor(agentAid);
2879
+ const definitions = scheduler.list({ all: cmd.all === true }).filter(trigger => actor.management
2880
+ || (trigger.origin?.peerId === actor.origin?.peerId && trigger.origin?.channelKey === actor.origin?.channelKey));
2881
+ return { ok: true, triggers: scheduler.listItems(definitions) };
2848
2882
  }
2849
- if (cmd.triggerId)
2850
- await authorizeTrigger(agentAid, 'trigger.history', cmd.triggerId);
2851
- else if (!(await authorizeTrigger(agentAid, 'trigger.history')).management) {
2852
- throw new Error('triggerId is required for non-management history access');
2883
+ case 'trigger.show': {
2884
+ const agentAid = requireAgent(cmd.agentAid);
2885
+ if (!cmd.triggerId)
2886
+ throw new Error('missing triggerId');
2887
+ await authorizeTrigger(agentAid, 'trigger.show', cmd.triggerId);
2888
+ return {
2889
+ ok: true,
2890
+ ...schedulerFor(agentAid).show(cmd.triggerId, {
2891
+ includeScriptPreview: cmd.includeScriptPreview !== false,
2892
+ }),
2893
+ };
2853
2894
  }
2854
- return { ok: true, events: schedulerFor(agentAid).history(cmd.triggerId, limit) };
2855
- }
2856
- case 'trigger.eventCatalog': {
2857
- const agentAid = requireAgent(cmd.agentAid);
2858
- await authorizeTrigger(agentAid, 'trigger.eventCatalog');
2859
- return { ok: true, ...getEventCatalog({ includeInternal: cmd.includeInternal === true }) };
2860
- }
2861
- case 'trigger.create': {
2862
- const rawDefinition = cmd.definition;
2863
- if (!rawDefinition || typeof rawDefinition !== 'object' || Array.isArray(rawDefinition)) {
2864
- throw new Error('trigger definition must be an object');
2895
+ case 'trigger.history': {
2896
+ const agentAid = requireAgent(cmd.agentAid);
2897
+ const limit = cmd.limit === undefined ? 100 : Number(cmd.limit);
2898
+ if (!Number.isInteger(limit) || limit <= 0 || limit > 10_000)
2899
+ throw new Error('invalid history limit');
2900
+ if (cmd.triggerId !== undefined && (typeof cmd.triggerId !== 'string' || !cmd.triggerId)) {
2901
+ throw new Error('invalid triggerId');
2902
+ }
2903
+ if (cmd.triggerId)
2904
+ await authorizeTrigger(agentAid, 'trigger.history', cmd.triggerId);
2905
+ else if (!(await authorizeTrigger(agentAid, 'trigger.history')).management) {
2906
+ throw new Error('triggerId is required for non-management history access');
2907
+ }
2908
+ return { ok: true, events: schedulerFor(agentAid).history(cmd.triggerId, limit) };
2865
2909
  }
2866
- const agentAid = requireAgent(rawDefinition.agentAid);
2867
- const actor = await authorizeTrigger(agentAid, 'trigger.create');
2868
- const materialized = materializeTriggerCreateBaseagent({ ...rawDefinition, origin: actor.origin }, agentAid);
2869
- const definition = normalizeTriggerDefinition(materialized);
2870
- validateTriggerDefinitionForActor(definition, actor);
2871
- requireAgent(definition.agentAid);
2872
- validateTriggerFeedbackChannels(definition);
2873
- const trigger = schedulerFor(definition.agentAid).create(definition, cmd.files ?? [], { enable: cmd.enable });
2874
- return { ok: true, trigger };
2875
- }
2876
- case 'trigger.update': {
2877
- const agentAid = requireAgent(cmd.agentAid);
2878
- if (!cmd.triggerId)
2879
- throw new Error('missing triggerId');
2880
- const actor = await authorizeTrigger(agentAid, 'trigger.update', cmd.triggerId);
2881
- const existing = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === cmd.triggerId);
2882
- if (!existing)
2883
- throw new Error(`trigger not found: ${cmd.triggerId}`);
2884
- const currentRevision = definitionRevision(existing);
2885
- if (cmd.expectedRevision !== undefined && cmd.expectedRevision !== currentRevision) {
2886
- throw new Error(`trigger revision conflict: expected ${cmd.expectedRevision}, current ${currentRevision}`);
2910
+ case 'trigger.eventCatalog': {
2911
+ const agentAid = requireAgent(cmd.agentAid);
2912
+ await authorizeTrigger(agentAid, 'trigger.eventCatalog');
2913
+ return { ok: true, ...getEventCatalog({ includeInternal: cmd.includeInternal === true }) };
2887
2914
  }
2888
- const definition = applyTriggerPatch(existing, cmd.patch, { fromPromptFile: cmd.promptFile === true });
2889
- validateTriggerDefinitionForActor(definition, actor);
2890
- validateTriggerFeedbackChannels(definition);
2891
- const scheduler = schedulerFor(agentAid);
2892
- const trigger = scheduler.update(cmd.triggerId, definition);
2893
- return { ok: true, trigger: scheduler.listItem(trigger), revision: definitionRevision(trigger) };
2894
- }
2895
- case 'trigger.setEnabled': {
2896
- const agentAid = requireAgent(cmd.agentAid);
2897
- if (!cmd.triggerId)
2898
- throw new Error('missing triggerId');
2899
- if (typeof cmd.enabled !== 'boolean')
2900
- throw new Error('missing enabled');
2901
- await authorizeTrigger(agentAid, 'trigger.setEnabled', cmd.triggerId);
2902
- const trigger = schedulerFor(agentAid).setEnabled(cmd.triggerId, cmd.enabled);
2903
- return { ok: true, trigger };
2904
- }
2905
- case 'trigger.cancel': {
2906
- const agentAid = requireAgent(cmd.agentAid);
2907
- if (!cmd.triggerId)
2908
- throw new Error('missing triggerId');
2909
- await authorizeTrigger(agentAid, 'trigger.cancel', cmd.triggerId);
2910
- const trigger = schedulerFor(agentAid).cancel(cmd.triggerId);
2911
- return { ok: true, trigger };
2912
- }
2913
- case 'trigger.delete': {
2914
- const agentAid = requireAgent(cmd.agentAid);
2915
- if (!cmd.triggerId)
2916
- throw new Error('missing triggerId');
2917
- await authorizeTrigger(agentAid, 'trigger.delete', cmd.triggerId);
2918
- const trigger = schedulerFor(agentAid).delete(cmd.triggerId);
2919
- return { ok: true, trigger };
2920
- }
2921
- case 'trigger.run': {
2922
- const agentAid = requireAgent(cmd.agentAid);
2923
- if (!cmd.triggerId)
2924
- throw new Error('missing triggerId');
2925
- await authorizeTrigger(agentAid, 'trigger.run', cmd.triggerId);
2926
- const result = await schedulerFor(agentAid).run(cmd.triggerId, {
2927
- dryRun: cmd.dryRun === true,
2928
- ...(cmd.eventPayload !== undefined ? { eventPayload: cmd.eventPayload } : {}),
2929
- });
2930
- return {
2931
- ok: result.ok,
2932
- result,
2933
- runId: result.runId,
2934
- triggerId: result.triggerId,
2935
- status: result.status,
2936
- reason: result.reason,
2937
- conflictRunId: result.conflictRunId,
2938
- error: result.error,
2939
- audit: result.audit,
2940
- };
2915
+ case 'trigger.create': {
2916
+ const rawDefinition = cmd.definition;
2917
+ if (!rawDefinition || typeof rawDefinition !== 'object' || Array.isArray(rawDefinition)) {
2918
+ throw new Error('trigger definition must be an object');
2919
+ }
2920
+ const agentAid = requireAgent(rawDefinition.agentAid);
2921
+ const actor = await authorizeTrigger(agentAid, 'trigger.create');
2922
+ const materialized = materializeTriggerCreateBaseagent({ ...rawDefinition, origin: actor.origin }, agentAid);
2923
+ const definition = normalizeTriggerDefinition(materialized);
2924
+ validateTriggerDefinitionForActor(definition, actor);
2925
+ requireAgent(definition.agentAid);
2926
+ validateTriggerFeedbackChannels(definition);
2927
+ const trigger = schedulerFor(definition.agentAid).create(definition, cmd.files ?? [], { enable: cmd.enable });
2928
+ return { ok: true, trigger };
2929
+ }
2930
+ case 'trigger.update': {
2931
+ const agentAid = requireAgent(cmd.agentAid);
2932
+ if (!cmd.triggerId)
2933
+ throw new Error('missing triggerId');
2934
+ const actor = await authorizeTrigger(agentAid, 'trigger.update', cmd.triggerId);
2935
+ const existing = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === cmd.triggerId);
2936
+ if (!existing)
2937
+ throw new Error(`trigger not found: ${cmd.triggerId}`);
2938
+ const currentRevision = definitionRevision(existing);
2939
+ if (cmd.expectedRevision !== undefined && cmd.expectedRevision !== currentRevision) {
2940
+ throw new Error(`trigger revision conflict: expected ${cmd.expectedRevision}, current ${currentRevision}`);
2941
+ }
2942
+ const definition = applyTriggerPatch(existing, cmd.patch, { fromPromptFile: cmd.promptFile === true });
2943
+ validateTriggerDefinitionForActor(definition, actor);
2944
+ validateTriggerFeedbackChannels(definition);
2945
+ const scheduler = schedulerFor(agentAid);
2946
+ const trigger = scheduler.update(cmd.triggerId, definition);
2947
+ return { ok: true, trigger: scheduler.listItem(trigger), revision: definitionRevision(trigger) };
2948
+ }
2949
+ case 'trigger.setEnabled': {
2950
+ const agentAid = requireAgent(cmd.agentAid);
2951
+ if (!cmd.triggerId)
2952
+ throw new Error('missing triggerId');
2953
+ if (typeof cmd.enabled !== 'boolean')
2954
+ throw new Error('missing enabled');
2955
+ await authorizeTrigger(agentAid, 'trigger.setEnabled', cmd.triggerId);
2956
+ const trigger = schedulerFor(agentAid).setEnabled(cmd.triggerId, cmd.enabled);
2957
+ return { ok: true, trigger };
2958
+ }
2959
+ case 'trigger.cancel': {
2960
+ const agentAid = requireAgent(cmd.agentAid);
2961
+ if (!cmd.triggerId)
2962
+ throw new Error('missing triggerId');
2963
+ await authorizeTrigger(agentAid, 'trigger.cancel', cmd.triggerId);
2964
+ const trigger = schedulerFor(agentAid).cancel(cmd.triggerId);
2965
+ return { ok: true, trigger };
2966
+ }
2967
+ case 'trigger.delete': {
2968
+ const agentAid = requireAgent(cmd.agentAid);
2969
+ if (!cmd.triggerId)
2970
+ throw new Error('missing triggerId');
2971
+ await authorizeTrigger(agentAid, 'trigger.delete', cmd.triggerId);
2972
+ const trigger = schedulerFor(agentAid).delete(cmd.triggerId);
2973
+ return { ok: true, trigger };
2974
+ }
2975
+ case 'trigger.run': {
2976
+ const agentAid = requireAgent(cmd.agentAid);
2977
+ if (!cmd.triggerId)
2978
+ throw new Error('missing triggerId');
2979
+ await authorizeTrigger(agentAid, 'trigger.run', cmd.triggerId);
2980
+ const result = await schedulerFor(agentAid).run(cmd.triggerId, {
2981
+ dryRun: cmd.dryRun === true,
2982
+ ...(cmd.eventPayload !== undefined ? { eventPayload: cmd.eventPayload } : {}),
2983
+ });
2984
+ return {
2985
+ ok: result.ok,
2986
+ result,
2987
+ runId: result.runId,
2988
+ triggerId: result.triggerId,
2989
+ status: result.status,
2990
+ reason: result.reason,
2991
+ conflictRunId: result.conflictRunId,
2992
+ error: result.error,
2993
+ audit: result.audit,
2994
+ };
2995
+ }
2996
+ default:
2997
+ return { ok: false, error: `unknown trigger command: ${cmd.type}` };
2941
2998
  }
2942
- default:
2943
- return { ok: false, error: `unknown trigger command: ${cmd.type}` };
2944
- }
2945
- });
2999
+ });
3000
+ }
2946
3001
  ipcServer.startCpuTracking();
2947
- // I3: start IPC server after all hooks/executors/providers are registered.
2948
- await ipcServer.start();
2949
- // 写入 ready 信号(Control Plane 已可通过 IPC 查询;channel 连接不阻塞启动判定)
3002
+ // IPC was bound before channel connection. At this point all handlers are
3003
+ // registered, so the ready signal can safely expose the control plane.
2950
3004
  const readySignalPath = resolvePaths().readySignal;
2951
3005
  fs.writeFileSync(readySignalPath, String(Date.now()));
2952
3006
  logger.info(`✓ Ready signal written: ${readySignalPath}`);