evolcore 0.0.16 → 0.0.18

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 (65) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/bin/codex-managed-hook.mjs +16 -7
  3. package/bin/install-codex-managed-hooks.mjs +4 -2
  4. package/dist/agents/claude-runner.js +132 -14
  5. package/dist/agents/codex-app-server-client.js +6 -1
  6. package/dist/agents/codex-runner.js +21 -6
  7. package/dist/agents/ecagent-runner.js +39 -10
  8. package/dist/agents/gemini-runner.js +90 -19
  9. package/dist/aun/aid/store.js +36 -0
  10. package/dist/aun/msg/group.js +3 -1
  11. package/dist/aun/msg/p2p.js +23 -9
  12. package/dist/channels/aun.js +159 -21
  13. package/dist/cli/agent-command.js +67 -6
  14. package/dist/cli/agent.js +26 -0
  15. package/dist/cli/command-log.js +23 -4
  16. package/dist/cli/daemon-commands.js +53 -12
  17. package/dist/cli/init.js +21 -5
  18. package/dist/cli/restart-monitor.js +13 -6
  19. package/dist/cli/task-context.js +46 -1
  20. package/dist/cli/watch-logs.js +2 -2
  21. package/dist/config/builtin-roles.js +5 -1
  22. package/dist/config/role-ranks.js +4 -0
  23. package/dist/core/audit/event-key.js +29 -0
  24. package/dist/core/audit/log-integrity.js +13 -3
  25. package/dist/core/auth/auth-gateway.js +14 -18
  26. package/dist/core/auth/authorization-audit.js +110 -3
  27. package/dist/core/auth/authorization-denial.js +17 -0
  28. package/dist/core/auth/operation-authorizer.js +143 -18
  29. package/dist/core/auth/operation-catalog.js +21 -5
  30. package/dist/core/bootstrap-messages.js +11 -6
  31. package/dist/core/bootstrap-service.js +26 -4
  32. package/dist/core/causation/aun-association.js +7 -4
  33. package/dist/core/command/agent-control.js +25 -16
  34. package/dist/core/command/command-handler.js +50 -4
  35. package/dist/core/command/group-menu.js +1 -1
  36. package/dist/core/command/menu-catalog.js +32 -7
  37. package/dist/core/command/menu-handler.js +59 -23
  38. package/dist/core/command/menu-protocol.js +196 -0
  39. package/dist/core/command/slash-gate.js +14 -5
  40. package/dist/core/command/slash-handler.js +81 -99
  41. package/dist/core/event-catalog.js +18 -0
  42. package/dist/core/message/message-bridge.js +72 -9
  43. package/dist/core/message/pause-controller.js +53 -0
  44. package/dist/core/message/response-engine.js +98 -11
  45. package/dist/core/permission/sandbox-runtime.js +79 -13
  46. package/dist/core/permission/tool-policy.js +1 -1
  47. package/dist/index.js +357 -48
  48. package/dist/ipc.js +75 -4
  49. package/dist/utils/atomic-write.js +45 -11
  50. package/dist/utils/error-utils.js +38 -0
  51. package/dist/utils/logger.js +27 -0
  52. package/dist/utils/windows-autostart.js +740 -83
  53. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  54. package/ecagent/dist/harness/agent-harness.js +6 -4
  55. package/kits/docs/evolcore/config.md +1 -1
  56. package/kits/docs/evolcore/group-rules.md +2 -1
  57. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  58. package/kits/eck_manifest.json +25 -16
  59. package/kits/rules/01-overview.md +5 -5
  60. package/kits/rules/03-identity.md +1 -1
  61. package/kits/rules/04-relation.md +4 -4
  62. package/kits/rules/05-venue.md +5 -5
  63. package/kits/templates/bootstrap-welcome.md +3 -1
  64. package/kits/templates/system-fragments/bootstrap.md +17 -9
  65. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { E2EEError } from '@agentunion/fastaun';
2
2
  import crypto from 'crypto';
3
+ import { performance } from 'node:perf_hooks';
3
4
  import fs from 'fs';
4
5
  import path from 'path';
5
6
  import os from 'os';
@@ -15,7 +16,7 @@ import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
15
16
  import { chatDirPath } from '../core/session/session-fs-store.js';
16
17
  import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
17
18
  import { appendAidLifecycle } from '../aun/aid/identity.js';
18
- import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
19
+ import { enableFullGroupPullPagination, getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
19
20
  import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
20
21
  import { loadAgent } from '../config-store.js';
21
22
  import { normalizeAgentLifecycle } from '../config/lifecycle.js';
@@ -33,7 +34,7 @@ import { deriveCausation, normalizeCausation } from '../core/causation/context.j
33
34
  import { recordCausationSpan } from '../core/causation/audit.js';
34
35
  import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
35
36
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
36
- import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
37
+ import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationId, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
37
38
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
38
39
  import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
39
40
  import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
@@ -224,6 +225,7 @@ export function aunOptsToInbound(opts, channel, channelType) {
224
225
  protectedHeaders: opts.protectedHeaders,
225
226
  messageId: opts.messageId,
226
227
  receivedAt: opts.receivedAt,
228
+ receivedAtMono: opts.receivedAtMono,
227
229
  gatewaySeq: opts.gatewaySeq,
228
230
  causation: opts.causation,
229
231
  mentions: opts.mentions,
@@ -1053,6 +1055,8 @@ export class AUNChannel {
1053
1055
  reconnectTimer = null;
1054
1056
  reconnectGeneration = 0; // 防止并发 initClient:每次 takeoverReconnect 递增,回调中校验
1055
1057
  onChannelDown;
1058
+ /** Prevent a legacy state alias from double-counting the same SDK transition. */
1059
+ lastConnectionStateSignature = '';
1056
1060
  // initClient concurrency guard: 防止多个 initClient 并发执行
1057
1061
  initInProgress = false;
1058
1062
  // Flap detection: 连接寿命 < FLAP_WINDOW_MS 累计 FLAP_THRESHOLD 次,判定为持续被踢
@@ -1165,6 +1169,7 @@ export class AUNChannel {
1165
1169
  this.store = null;
1166
1170
  }
1167
1171
  this.connected = false;
1172
+ this.lastConnectionStateSignature = '';
1168
1173
  const aunPath = this.config.keystorePath || resolveRoot();
1169
1174
  const aidName = this.config.aid;
1170
1175
  // encryptionSeed 由 getAidStore 内部解析(config / env / 'evol')
@@ -1183,7 +1188,11 @@ export class AUNChannel {
1183
1188
  });
1184
1189
  this.store = store;
1185
1190
  const client = await loadClient(store, aidName);
1191
+ enableFullGroupPullPagination(client);
1186
1192
  this.client = client;
1193
+ // A reconnect replaces the SDK client. Late events from the retired
1194
+ // instance must not mutate the new instance's health or message state.
1195
+ const isCurrentClient = () => this.client === client;
1187
1196
  // fastaun gives a preset in-memory gateway precedence over its metadata
1188
1197
  // cache and AID discovery. authenticate({ gateway }) is deliberately
1189
1198
  // rejected by its public API, so set the documented client preset before
@@ -1193,6 +1202,10 @@ export class AUNChannel {
1193
1202
  this.gatewayUrl = configuredGateway;
1194
1203
  // Register event handlers before connecting
1195
1204
  client.on('message.received', (data) => {
1205
+ if (!isCurrentClient())
1206
+ return;
1207
+ this.aidState.lastInboundAt = Date.now();
1208
+ this.aidState.lastTransportEventAt = this.aidState.lastInboundAt;
1196
1209
  this.trace('IN', 'message.received', data);
1197
1210
  const kind = (data && typeof data === 'object') ? data.kind ?? '' : '';
1198
1211
  const keys = (data && typeof data === 'object') ? Object.keys(data).join(',') : typeof data;
@@ -1202,6 +1215,10 @@ export class AUNChannel {
1202
1215
  // pureIdentity(控制 AID):协议层不接群消息,不注册 group 创建监听
1203
1216
  if (!this.config.pureIdentity) {
1204
1217
  client.on('group.message_created', (data) => {
1218
+ if (!isCurrentClient())
1219
+ return;
1220
+ this.aidState.lastInboundAt = Date.now();
1221
+ this.aidState.lastTransportEventAt = this.aidState.lastInboundAt;
1205
1222
  this.trace('IN', 'group.message_created', data);
1206
1223
  const env = (data && typeof data === 'object') ? data.envelope ?? {} : {};
1207
1224
  const gid = env.group_id ?? '';
@@ -1210,16 +1227,26 @@ export class AUNChannel {
1210
1227
  this.handleIncomingGroupMessage(data);
1211
1228
  });
1212
1229
  }
1213
- client.on('connection.state', (data) => {
1214
- // trace is handled inside handleConnectionState with throttling
1215
- this.handleConnectionState(data);
1216
- });
1230
+ const handleSdkState = (source) => (data) => {
1231
+ if (!isCurrentClient())
1232
+ return;
1233
+ // fastaun 0.5.x publishes state_change with nine-state values. Keep the
1234
+ // legacy event subscription for older stores/SDKs during rolling upgrades.
1235
+ this.handleConnectionState(data, source);
1236
+ };
1237
+ client.on('state_change', handleSdkState('state_change'));
1238
+ client.on('connection.state', handleSdkState('connection.state'));
1217
1239
  // gateway 被踢/服务端主动断开(含同槽位互踢的 self/new extra_info)
1218
1240
  client.on('gateway.disconnect', (data) => {
1241
+ if (!isCurrentClient())
1242
+ return;
1243
+ this.aidState.lastTransportEventAt = Date.now();
1219
1244
  this.trace('IN', 'gateway.disconnect', data);
1220
1245
  this.handleGatewayDisconnect(data);
1221
1246
  });
1222
1247
  client.on('message.recalled', (data) => {
1248
+ if (!isCurrentClient())
1249
+ return;
1223
1250
  this.trace('IN', 'message.recalled', data);
1224
1251
  if (data && typeof data === 'object') {
1225
1252
  const ids = data.message_ids;
@@ -1234,6 +1261,8 @@ export class AUNChannel {
1234
1261
  }
1235
1262
  });
1236
1263
  client.on('message.undecryptable', (data) => {
1264
+ if (!isCurrentClient())
1265
+ return;
1237
1266
  this.trace('IN', 'message.undecryptable', data);
1238
1267
  const d = data;
1239
1268
  const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
@@ -1242,6 +1271,8 @@ export class AUNChannel {
1242
1271
  // pureIdentity(控制 AID):不注册 group 解密失败监听
1243
1272
  if (!this.config.pureIdentity) {
1244
1273
  client.on('group.message_undecryptable', (data) => {
1274
+ if (!isCurrentClient())
1275
+ return;
1245
1276
  this.trace('IN', 'group.message_undecryptable', data);
1246
1277
  const d = data;
1247
1278
  const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
@@ -1250,6 +1281,8 @@ export class AUNChannel {
1250
1281
  // 群消息撤回(SDK 0.4.10 在线 push 通道):与私聊 message.recalled 同构,
1251
1282
  // 逐个 message_id 交给 recallHandler → msgBridge.cancel(排队中删除 / 处理中中断)。
1252
1283
  client.on('group.message_recalled', (data) => {
1284
+ if (!isCurrentClient())
1285
+ return;
1253
1286
  this.trace('IN', 'group.message_recalled', data);
1254
1287
  if (data && typeof data === 'object') {
1255
1288
  const d = data;
@@ -1357,15 +1390,45 @@ export class AUNChannel {
1357
1390
  }
1358
1391
  const operationId = postBootstrapWelcomeOperationId(aidName);
1359
1392
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
1393
+ if (hasMessageLogOperation(chatDir, operationId))
1394
+ return true;
1360
1395
  const prepared = outbox.findByDedupeKey(aidName, operationId);
1361
- if (hasMessageLogOperation(chatDir, operationId)
1362
- || (prepared?.delivery?.chatType === 'private' && prepared.channelId === owner)) {
1396
+ const initial = outbox.findByDedupeKey(aidName, bootstrapInitialMessageOperationId(aidName));
1397
+ const initialDelivery = initial && outbox.isDeliveryForChannel(initial.delivery, owner)
1398
+ ? initial.delivery
1399
+ : undefined;
1400
+ // During bootstrap completion the lifecycle is intentionally still
1401
+ // non-active. Persisting the welcome must stay local and must not block
1402
+ // on a best-effort group RPC before the lifecycle commit. If the initial
1403
+ // bootstrap route is already known, preserve it; otherwise migration
1404
+ // will resolve a group route after the channel is active.
1405
+ const lifecycle = normalizeAgentLifecycle(agentConfig).lifecycle;
1406
+ const ownerIsGroup = initialDelivery
1407
+ ? initialDelivery.chatType === 'group'
1408
+ : lifecycle === 'active' ? await this.isGroup(owner) : undefined;
1409
+ const delivery = initialDelivery ?? (ownerIsGroup === true
1410
+ ? { chatType: 'group', groupId: owner }
1411
+ : { chatType: 'private' });
1412
+ if (prepared
1413
+ && prepared.channelId === owner
1414
+ && isDeliveryTarget(prepared.delivery)
1415
+ && sameDeliveryTarget(prepared.delivery, delivery)) {
1363
1416
  return true;
1364
1417
  }
1365
1418
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
1366
1419
  let ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
1420
+ // Keep lifecycle=bootstrapping preparation entirely local. Resolving an
1421
+ // uncached peer may download agent.md, and welcome display metadata must
1422
+ // never delay the durable write-ahead record or the lifecycle commit.
1423
+ const cachedOwnerInfo = this.peerInfoCached(owner);
1367
1424
  try {
1368
- const ownerInfo = await this.fetchPeerInfo(owner);
1425
+ const ownerInfo = cachedOwnerInfo
1426
+ ?? (lifecycle === 'active' ? await this.fetchPeerInfo(owner) : undefined);
1427
+ if (!ownerInfo) {
1428
+ preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName, delivery);
1429
+ logger.info(`${this.logPrefix()} Post-bootstrap welcome durably prepared for owner: ${owner}`);
1430
+ return true;
1431
+ }
1369
1432
  ownerDisplayName = (ownerInfo.name || ownerDisplayName).slice(0, 12);
1370
1433
  if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
1371
1434
  logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human).`);
@@ -1374,7 +1437,7 @@ export class AUNChannel {
1374
1437
  catch (e) {
1375
1438
  logger.warn(`${this.logPrefix()} Failed to resolve owner display name; using AID label: ${e}`);
1376
1439
  }
1377
- preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName);
1440
+ preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName, delivery);
1378
1441
  logger.info(`${this.logPrefix()} Post-bootstrap welcome durably prepared for owner: ${owner}`);
1379
1442
  return true;
1380
1443
  }
@@ -1415,7 +1478,7 @@ export class AUNChannel {
1415
1478
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
1416
1479
  return hasMessageLogOperation(chatDir, operationId);
1417
1480
  }
1418
- if (entry.delivery?.chatType !== 'private') {
1481
+ if (!outbox.isDeliveryForChannel(entry.delivery, entry.channelId)) {
1419
1482
  logger.warn(`${this.logPrefix()} Discarding unrouted legacy post-bootstrap welcome: id=${entry.id}`);
1420
1483
  outbox.remove(aid, entry.id);
1421
1484
  return false;
@@ -1679,6 +1742,7 @@ export class AUNChannel {
1679
1742
  return;
1680
1743
  const msg = data;
1681
1744
  const receivedAt = Date.now();
1745
+ const receivedAtMono = performance.now();
1682
1746
  // Claim before any payload extraction or command parsing. Retransmits may
1683
1747
  // carry a new seq, but the application identity remains message_id.
1684
1748
  const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
@@ -1772,7 +1836,7 @@ export class AUNChannel {
1772
1836
  this.dispatchMessage({
1773
1837
  channelId: chatId, userId: fromAid,
1774
1838
  text: JSON.stringify(payload),
1775
- chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
1839
+ chatType: 'private', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq,
1776
1840
  peerName: displayName || undefined,
1777
1841
  peerType: peerIdentity.type,
1778
1842
  encrypted: msgEncrypted,
@@ -1860,6 +1924,7 @@ export class AUNChannel {
1860
1924
  return;
1861
1925
  const msg = data;
1862
1926
  const receivedAt = Date.now();
1927
+ const receivedAtMono = performance.now();
1863
1928
  // Deduplicate at the adapter boundary, before mention/slash routing.
1864
1929
  const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
1865
1930
  const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
@@ -1960,7 +2025,7 @@ export class AUNChannel {
1960
2025
  this.dispatchMessage({
1961
2026
  channelId: groupId, userId: senderAid,
1962
2027
  text: JSON.stringify(payload),
1963
- chatType: 'group', messageId, seq, receivedAt, gatewaySeq: seq, groupId,
2028
+ chatType: 'group', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq, groupId,
1964
2029
  encrypted: menuEncrypted,
1965
2030
  protectedHeaders,
1966
2031
  mentions: payloadMentionEntries,
@@ -2199,6 +2264,7 @@ export class AUNChannel {
2199
2264
  protectedHeaders: event.protectedHeaders,
2200
2265
  messageId: event.messageId,
2201
2266
  receivedAt: event.receivedAt,
2267
+ receivedAtMono: event.receivedAtMono,
2202
2268
  gatewaySeq: event.gatewaySeq ?? event.seq,
2203
2269
  causation: event.causation,
2204
2270
  threadId: event.threadId,
@@ -2496,21 +2562,47 @@ export class AUNChannel {
2496
2562
  logger.warn(`${this.logPrefix()} Server-initiated disconnect: code=${code} reason=${reason} detail=${JSON.stringify(detail)}`);
2497
2563
  }
2498
2564
  }
2499
- handleConnectionState(data) {
2565
+ handleConnectionState(data, source = 'state_change') {
2500
2566
  if (!data || typeof data !== 'object')
2501
2567
  return;
2502
- const state = data.state ?? '';
2568
+ const rawState = String(data.state ?? '');
2569
+ const statePayload = data;
2570
+ const state = rawState === 'ready' ? 'connected'
2571
+ : rawState === 'standby' || rawState === 'authenticated' || rawState === 'connecting' || rawState === 'retry_backoff'
2572
+ ? 'reconnecting'
2573
+ : rawState === 'connection_failed' || rawState === 'no_identity'
2574
+ ? 'terminal_failed'
2575
+ : rawState === 'closed'
2576
+ ? 'disconnected'
2577
+ : rawState;
2578
+ const signature = [
2579
+ rawState,
2580
+ String(statePayload.attempt ?? ''),
2581
+ String(statePayload.reason ?? ''),
2582
+ String(statePayload.code ?? ''),
2583
+ String(statePayload.error ?? ''),
2584
+ String(statePayload.gateway ?? ''),
2585
+ ].join('|');
2586
+ this.aidState.lastTransportEventAt = Date.now();
2587
+ if (signature === this.lastConnectionStateSignature)
2588
+ return;
2589
+ this.lastConnectionStateSignature = signature;
2590
+ if (this.intentionalDisconnect && state === 'disconnected') {
2591
+ this.connected = false;
2592
+ this.connectedAt = 0;
2593
+ return;
2594
+ }
2503
2595
  if (state === 'connected') {
2504
2596
  this.connected = true;
2505
2597
  this.connectedAt = Date.now();
2506
2598
  this.lastReconnectLogTime = 0;
2507
2599
  this.lastReconnectLogAttempt = 0;
2508
- // connection.state 事件 payload 带实际连接的 gateway,更新本地缓存
2600
+ // SDK 状态事件 payload 带实际连接的 gateway,更新本地缓存
2509
2601
  const evtGateway = data.gateway;
2510
2602
  if (typeof evtGateway === 'string' && evtGateway)
2511
2603
  this.gatewayUrl = evtGateway;
2512
2604
  this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
2513
- this.trace('IN', 'connection.state', data);
2605
+ this.trace('IN', source, data);
2514
2606
  logger.info(`${this.logPrefix()} Connected`);
2515
2607
  // 不在这里清 flapCount —— 短命连接一上来就会触发本分支,
2516
2608
  // 必须等 disconnected 时根据 lifetime 决定是否清零
@@ -2519,7 +2611,11 @@ export class AUNChannel {
2519
2611
  else if (state === 'disconnected' || state === 'reconnecting') {
2520
2612
  const wasConnected = this.connected;
2521
2613
  this.connected = false;
2522
- this.aidState.reconnectCount++;
2614
+ // The nine-state SDK emits standby -> retry_backoff -> reconnecting for
2615
+ // one reconnect cycle. Count the connected-to-down edge once instead of
2616
+ // inflating the metric for every intermediate state.
2617
+ if (wasConnected)
2618
+ this.aidState.reconnectCount++;
2523
2619
  this.aidState.lastAttemptAt = Date.now();
2524
2620
  this.setAidStatus('reconnecting');
2525
2621
  // Flap 检测:仅在从 connected 状态过渡时统计
@@ -2545,7 +2641,7 @@ export class AUNChannel {
2545
2641
  }
2546
2642
  }
2547
2643
  if (state === 'disconnected') {
2548
- this.trace('IN', 'connection.state', data);
2644
+ this.trace('IN', source, data);
2549
2645
  logger.warn(`${this.logPrefix()} Disconnected: ${data.error ?? 'unknown'}`);
2550
2646
  }
2551
2647
  else {
@@ -2561,14 +2657,14 @@ export class AUNChannel {
2561
2657
  logger.info(`${this.logPrefix()} SDK reconnecting (attempt ${attempt}${suffix})`);
2562
2658
  this.lastReconnectLogTime = now;
2563
2659
  this.lastReconnectLogAttempt = attempt;
2564
- this.trace('IN', 'connection.state', data);
2660
+ this.trace('IN', source, data);
2565
2661
  }
2566
2662
  }
2567
2663
  }
2568
2664
  else if (state === 'terminal_failed') {
2569
2665
  this.connected = false;
2570
2666
  this.connectedAt = 0;
2571
- this.trace('IN', 'connection.state', data);
2667
+ this.trace('IN', source, data);
2572
2668
  const d = data;
2573
2669
  const reason = d.reason ?? '';
2574
2670
  const error = d.error ?? 'unknown';
@@ -4423,6 +4519,7 @@ export class AUNChannel {
4423
4519
  async drainOutbox() {
4424
4520
  if (!this.connected || !this.client)
4425
4521
  return;
4522
+ await this.migrateLegacyBootstrapRoutes();
4426
4523
  if (!outbox.hasPending(this.config.aid))
4427
4524
  return;
4428
4525
  logger.info(`${this.logPrefix()} Draining outbox...`);
@@ -4450,6 +4547,32 @@ export class AUNChannel {
4450
4547
  logger.info(`${this.logPrefix()} Outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed}`);
4451
4548
  }
4452
4549
  }
4550
+ /** Repair pre-route bootstrap entries once the AUN group endpoint is available. */
4551
+ async migrateLegacyBootstrapRoutes() {
4552
+ const operationIds = new Set([
4553
+ bootstrapInitialMessageOperationId(this.config.aid),
4554
+ postBootstrapWelcomeOperationId(this.config.aid),
4555
+ ]);
4556
+ const entries = outbox.load(this.config.aid).filter(entry => typeof entry.dedupeKey === 'string'
4557
+ && operationIds.has(entry.dedupeKey)
4558
+ && entry.channelId
4559
+ && entry.delivery?.chatType === 'private');
4560
+ for (const entry of entries) {
4561
+ const group = await this.isGroup(entry.channelId);
4562
+ if (group !== true)
4563
+ continue;
4564
+ const delivery = { chatType: 'group', groupId: entry.channelId };
4565
+ if (!outbox.replace(this.config.aid, {
4566
+ ...entry,
4567
+ delivery,
4568
+ context: entry.context
4569
+ ? { ...entry.context, delivery }
4570
+ : entry.context,
4571
+ }))
4572
+ continue;
4573
+ logger.warn(`${this.logPrefix()} Migrated legacy bootstrap outbox route to group.send: entry=${entry.id} group=${entry.channelId}`);
4574
+ }
4575
+ }
4453
4576
  acknowledge(messageId) {
4454
4577
  // Gateway auto-delivery-ack is sufficient; skip explicit message.ack RPC
4455
4578
  // to avoid duplicate "已送达" at the sender CLI
@@ -4622,6 +4745,8 @@ export class AUNChannel {
4622
4745
  aid: this._aid,
4623
4746
  flapCount: this.flapCount,
4624
4747
  plaintextRecv: this.plaintextRecv,
4748
+ lastTransportEventAt: this.aidState.lastTransportEventAt,
4749
+ lastInboundAt: this.aidState.lastInboundAt,
4625
4750
  };
4626
4751
  }
4627
4752
  /** 读取本地 agent.md 中的 name(用于身份上下文展示),若本地不存在则尝试远程拉取 */
@@ -4714,6 +4839,18 @@ export class AUNChannel {
4714
4839
  return undefined; // 不写缓存,下次仍可重试
4715
4840
  }
4716
4841
  }
4842
+ /** Query the authoritative group endpoint without guessing from an AID string. */
4843
+ async isGroup(groupId) {
4844
+ if (!groupId || !this.client)
4845
+ return undefined;
4846
+ try {
4847
+ const result = await this.callAndTrace('group.get_info', { group_id: groupId });
4848
+ return !!(result?.group || result?.group_id || result?.groupId);
4849
+ }
4850
+ catch {
4851
+ return undefined;
4852
+ }
4853
+ }
4717
4854
  /** 统计关系键继续使用群 ID,仅为智能体预览补充可读群名。 */
4718
4855
  async groupStatsContext(groupId) {
4719
4856
  return {
@@ -4996,6 +5133,7 @@ export class AUNChannelPlugin {
4996
5133
  uploadAgentMd: (content) => channel.uploadAgentMd(content),
4997
5134
  downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
4998
5135
  getGroupName: (groupId) => channel.getGroupName(groupId),
5136
+ isGroup: (groupId) => channel.isGroup(groupId),
4999
5137
  getGroupMemberRole: (groupId, aid) => channel.getGroupMemberRole(groupId, aid),
5000
5138
  _selfAid: () => channel.getStatus().aid,
5001
5139
  _selfName: () => channel.getSelfName(),
@@ -26,6 +26,15 @@ export async function cmdAgent(args) {
26
26
  const sub = args[0];
27
27
  const formatJson = getArgValue(args, '--format') === 'json';
28
28
  const commands = new Set(['list', 'show', 'new', 'enable', 'disable', 'get', 'set', 'rename', 'ready', 'reload', 'delete']);
29
+ if (sub === 'create') {
30
+ const error = { ok: false, code: 'INVALID_COMMAND', error: '`ec agent create` is not a command; use `ec agent new`.' };
31
+ if (formatJson)
32
+ console.log(JSON.stringify(error));
33
+ else
34
+ console.error(`❌ [${error.code}] ${error.error}`);
35
+ process.exitCode = 1;
36
+ return;
37
+ }
29
38
  if (!sub || isHelpFlag(sub) || (wantsHelp(args) && !commands.has(sub))) {
30
39
  console.log(`用法: ec agent <command>
31
40
 
@@ -59,14 +68,66 @@ Options:
59
68
  ec agent delete mybot.agentid.pub --purge`);
60
69
  return;
61
70
  }
71
+ const { agentList, agentShow, agentCreateInteractive, agentCreateNonInteractive, agentReload, agentEnable, agentDisable, agentGet, agentSet, agentRename, agentDelete, agentReady, runManagedAgentOperation, } = await import('./agent.js');
62
72
  if (process.env.EVOLCORE_SESSION_ID && !wantsHelp(args) && !new Set(['get', 'set', 'ready']).has(sub)) {
63
- if (formatJson)
64
- console.log(JSON.stringify({ ok: false, code: 'NOT_ALLOWED', error: `agent ${sub} is not available in a managed task` }));
65
- else
66
- console.error(`❌ [NOT_ALLOWED] agent ${sub} is not available in a managed task`);
67
- process.exit(1);
73
+ if (sub === 'new' && !args.includes('--non-interactive')) {
74
+ const error = {
75
+ ok: false,
76
+ code: 'MANAGED_NON_INTERACTIVE_REQUIRED',
77
+ error: 'Managed Agent tasks must use `ec agent new <aid> --non-interactive`; interactive creation is unavailable here. Use ECWeb for interactive setup.',
78
+ data: { reasonCode: 'MANAGED_NON_INTERACTIVE_REQUIRED', nextStep: 'Use `ec agent new <aid> --non-interactive` or ECWeb.' },
79
+ };
80
+ if (formatJson)
81
+ console.log(JSON.stringify(error));
82
+ else
83
+ console.error(`❌ [${error.code}] ${error.error}`);
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+ const managedArgs = commands.has(sub) ? args : ['show', ...args];
88
+ const result = await runManagedAgentOperation(managedArgs);
89
+ if (!result.ok) {
90
+ if (formatJson)
91
+ console.log(JSON.stringify(result));
92
+ else {
93
+ console.error(`❌ [${result.code || 'NOT_ALLOWED'}] ${result.error}`);
94
+ if (result.data?.nextStep)
95
+ console.error(`Next step: ${result.data.nextStep}`);
96
+ }
97
+ process.exitCode = 1;
98
+ return;
99
+ }
100
+ if (formatJson) {
101
+ console.log(JSON.stringify(result, null, 2));
102
+ return;
103
+ }
104
+ if (sub === 'show' || (!sub.startsWith('new') && result.identity && result.config)) {
105
+ printAgentShowHuman(result);
106
+ }
107
+ else if (sub === 'list' && Array.isArray(result.agents)) {
108
+ if (result.agents.length === 0)
109
+ console.log('No agents configured.');
110
+ else
111
+ for (const agent of result.agents)
112
+ console.log(`${agent.name || agent.aid}\t${agent.status || 'stopped'}\t${agent.aid}`);
113
+ }
114
+ else if (result.accepted) {
115
+ console.log(`✓ Agent "${result.aid}" creation accepted`);
116
+ }
117
+ else if (result.aid && result.enabled !== undefined) {
118
+ console.log(`✓ ${result.aid} ${result.enabled ? 'enabled' : 'disabled'}${result.reloaded ? ' (hot-reloaded)' : ''}`);
119
+ }
120
+ else if (result.aid && result.purged !== undefined) {
121
+ console.log(`✓ ${result.aid} deleted${result.purged ? ' (purged)' : ''}`);
122
+ }
123
+ else if (result.aid && result.reloaded) {
124
+ console.log(`✓ Agent "${result.aid}" reloaded`);
125
+ }
126
+ else {
127
+ console.log(JSON.stringify(result));
128
+ }
129
+ return;
68
130
  }
69
- const { agentList, agentShow, agentCreateInteractive, agentCreateNonInteractive, agentReload, agentEnable, agentDisable, agentGet, agentSet, agentRename, agentDelete, agentReady, } = await import('./agent.js');
70
131
  // --- list ---
71
132
  if (sub === 'list') {
72
133
  if (wantsHelp(args)) {
package/dist/cli/agent.js CHANGED
@@ -26,6 +26,32 @@ function withStaticOwner(config, owner) {
26
26
  return config;
27
27
  return { ...config, owners: [...owners, value] };
28
28
  }
29
+ /** Route agent lifecycle commands from a managed task through daemon IPC. */
30
+ export async function runManagedAgentOperation(argv) {
31
+ const sessionId = process.env.EVOLCORE_SESSION_ID;
32
+ if (!sessionId)
33
+ return { ok: false, code: 'MANAGED_ONLY', error: 'agent operation is not running in a managed task' };
34
+ const delegationToken = process.env[AGENT_DELEGATION_TOKEN_ENV];
35
+ if (!delegationToken) {
36
+ return { ok: false, code: 'DELEGATION_REQUIRED', error: `${AGENT_DELEGATION_TOKEN_ENV} is not set for the active task` };
37
+ }
38
+ try {
39
+ const response = await ipcQuery(resolvePaths().socket, {
40
+ type: 'agent.op',
41
+ argv: ['agent', ...argv],
42
+ sessionId,
43
+ delegationToken,
44
+ }, 60_000);
45
+ if (!response)
46
+ return { ok: false, code: 'DAEMON_UNAVAILABLE', error: 'Unable to connect to the evolcore daemon' };
47
+ if (!response.ok)
48
+ return { ok: false, code: response.code, error: response.error || 'agent operation failed', ...(response.data ? { data: response.data } : {}) };
49
+ return response.result ?? { ok: true, ...(response.data ?? {}) };
50
+ }
51
+ catch (error) {
52
+ return { ok: false, code: 'DAEMON_UNAVAILABLE', error: error instanceof Error ? error.message : String(error) };
53
+ }
54
+ }
29
55
  // ==================== Helpers ====================
30
56
  const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini', 'ecagent'];
31
57
  const AUTO_BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
@@ -18,14 +18,33 @@ import path from 'path';
18
18
  import { agentLogsDir, resolvePaths } from '../paths.js';
19
19
  import { getArgValue } from './help.js';
20
20
  import { readTaskRuntimeContextFromEnv } from './task-context.js';
21
+ /** Keep path derivation from accepting arbitrary option values. */
22
+ function isSafeAid(value) {
23
+ if (!value)
24
+ return false;
25
+ const labels = value.split('.');
26
+ return labels.length >= 3
27
+ && labels.every(label => /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label));
28
+ }
21
29
  /**
22
30
  * 同步解析当前执行者 self-AID(零延迟,绝不走 IPC)。
23
- * 优先级:--as <aid> > EVOLCORE_SELF_AID > runtime context 里的 selfAid。
31
+ *
32
+ * `msg/group send` 的第二个选项 `--as` 是附件类型(image/video/voice/file),
33
+ * 不是操作者 AID;这两类命令的发送方位于固定的第三个位置参数。其它
34
+ * 支持 `--as <aid>` 的命令继续使用显式参数优先。
24
35
  */
25
36
  function resolveSelfAid(args) {
26
- return (getArgValue(args, '--as') ||
27
- process.env.EVOLCORE_SELF_AID ||
28
- readTaskRuntimeContextFromEnv()?.selfAid);
37
+ const command = args[0];
38
+ const subcommand = args[1];
39
+ const positionalAid = (command === 'msg' || command === 'group')
40
+ && subcommand === 'send'
41
+ ? args[2]
42
+ : undefined;
43
+ const explicitAid = (command === 'msg' || command === 'group') && subcommand === 'send'
44
+ ? undefined
45
+ : getArgValue(args, '--as');
46
+ const runtimeAid = process.env.EVOLCORE_SELF_AID || readTaskRuntimeContextFromEnv()?.selfAid;
47
+ return [positionalAid, explicitAid, runtimeAid].find(isSafeAid);
29
48
  }
30
49
  /**
31
50
  * 记录一条命令执行。同步、best-effort。