evolcore 0.0.23 → 0.0.24

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 (84) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -1
  3. package/bin/codex-managed-acl.ps1 +7 -2
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +17 -6
  7. package/dist/agents/baseagent-availability.js +4 -4
  8. package/dist/agents/baseagent.js +29 -1
  9. package/dist/agents/claude-runner.js +148 -13
  10. package/dist/agents/codex/app-server-client.js +18 -8
  11. package/dist/agents/codex/catalog.js +1 -1
  12. package/dist/agents/codex/diagnostics.js +215 -0
  13. package/dist/agents/codex-runner.js +438 -59
  14. package/dist/agents/ecagent-runner.js +17 -3
  15. package/dist/agents/gemini-runner.js +60 -2
  16. package/dist/agents/runner-types.js +1 -1
  17. package/dist/aun/aid/managed-operation.js +28 -6
  18. package/dist/aun/aid/store.js +41 -0
  19. package/dist/aun/msg/managed-operation.js +6 -1
  20. package/dist/aun/outbox.js +56 -18
  21. package/dist/aun/rpc/client.js +4 -5
  22. package/dist/channels/aun.js +147 -76
  23. package/dist/channels/daemon.js +7 -1
  24. package/dist/channels/dingtalk.js +1 -0
  25. package/dist/channels/feishu.js +10 -4
  26. package/dist/channels/wecom.js +2 -1
  27. package/dist/cli/agent-command.js +29 -3
  28. package/dist/cli/agent-create.js +538 -0
  29. package/dist/cli/agent.js +26 -675
  30. package/dist/cli/aun-commands.js +60 -20
  31. package/dist/cli/ctl-command.js +2 -0
  32. package/dist/cli/daemon-commands.js +148 -17
  33. package/dist/cli/index.js +2 -2
  34. package/dist/cli/managed-command-guard.js +3 -0
  35. package/dist/cli/restart-monitor.js +432 -8
  36. package/dist/cli/stats.js +110 -0
  37. package/dist/config/builtin-roles.js +2 -0
  38. package/dist/core/auth/auth-gateway.js +3 -2
  39. package/dist/core/auth/authorization-audit.js +72 -19
  40. package/dist/core/auth/operation-authorizer.js +63 -3
  41. package/dist/core/auth/operation-catalog.js +10 -2
  42. package/dist/core/auth/remote-access-store.js +3 -2
  43. package/dist/core/baseagent-loader.js +4 -4
  44. package/dist/core/bootstrap-messages.js +39 -18
  45. package/dist/core/bootstrap-service.js +32 -26
  46. package/dist/core/capability/capability-manager.js +8 -4
  47. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  48. package/dist/core/command/agent-control.js +7 -2
  49. package/dist/core/command/command-handler.js +87 -25
  50. package/dist/core/command/menu-catalog.js +3 -1
  51. package/dist/core/command/menu-handler.js +274 -70
  52. package/dist/core/command/menu-protocol.js +6 -2
  53. package/dist/core/command/slash-gate.js +1 -3
  54. package/dist/core/command/slash-handler.js +157 -48
  55. package/dist/core/data-migration.js +5 -15
  56. package/dist/core/event-catalog.js +3 -0
  57. package/dist/core/handoff/runtime.js +3 -1
  58. package/dist/core/handoff/store.js +7 -1
  59. package/dist/core/message/im-renderer.js +13 -6
  60. package/dist/core/message/message-bridge.js +5 -36
  61. package/dist/core/message/message-utils.js +6 -3
  62. package/dist/core/message/response-engine.js +73 -21
  63. package/dist/core/model/catalog-service.js +3 -1
  64. package/dist/core/model/model-catalog.js +10 -0
  65. package/dist/core/model/model-permission.js +4 -1
  66. package/dist/core/permission/approval-gateway.js +169 -33
  67. package/dist/core/permission/tool-policy.js +13 -6
  68. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  69. package/dist/core/session/session-manager.js +49 -13
  70. package/dist/core/session/session-renew.js +1 -0
  71. package/dist/index.js +174 -28
  72. package/dist/ipc.js +68 -21
  73. package/dist/paths.js +23 -1
  74. package/dist/stats/billing.js +18 -4
  75. package/dist/utils/atomic-write.js +26 -2
  76. package/dist/utils/cross-platform.js +436 -1
  77. package/dist/utils/error-utils.js +11 -4
  78. package/dist/utils/model-prices.jsonl +23 -20
  79. package/dist/utils/npm-ops.js +24 -1
  80. package/dist/utils/windows-autostart.js +1 -1
  81. package/kits/templates/roles/admin.json +2 -0
  82. package/package.json +7 -3
  83. package/AGENTS.md +0 -62
  84. package/dist/utils/codex-cli.js +0 -43
@@ -16,7 +16,7 @@ import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
16
16
  import { chatDirPath } from '../core/session/session-fs-store.js';
17
17
  import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
18
18
  import { appendAidLifecycle } from '../aun/aid/identity.js';
19
- import { enableFullGroupPullPagination, getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
19
+ import { enableFullGroupPullPagination, enableReliableMessageDelivery, getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
20
20
  import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
21
21
  import { loadAgent } from '../config-store.js';
22
22
  import { resolveAgentLifecycle } from '../config/lifecycle.js';
@@ -27,7 +27,6 @@ import { getProcessStartTime } from '../utils/process-introspect.js';
27
27
  import * as outbox from '../aun/outbox.js';
28
28
  import { guessMime, formatSize } from '../utils/media-cache.js';
29
29
  import { formatPeerKey, PeerIdentityCache } from '../core/relation/peer-identity.js';
30
- import { getFirstStaticAgentOwner } from '../config/peer-role-resolver.js';
31
30
  import { isHClassPath } from '../core/permission/protected-paths.js';
32
31
  import { consumeAunCausation, registerAunCausation } from '../core/causation/aun-association.js';
33
32
  import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
@@ -35,7 +34,7 @@ import { recordCausationSpan } from '../core/causation/audit.js';
35
34
  import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
36
35
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
37
36
  import { toolInputForDisplay } from '../utils/tool-summary.js';
38
- import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
37
+ import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, pendingPostBootstrapWelcomeOwners, isPostBootstrapWelcomeOperationId, findPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
39
38
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
40
39
  import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
41
40
  import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
@@ -729,7 +728,10 @@ export class AUNChannel {
729
728
  const actionValue = isComposeReply ? '_custom_input'
730
729
  : typeof obj.value === 'string' ? obj.value
731
730
  : typeof obj.action_value === 'string' ? obj.action_value : text;
732
- const threadId = typeof obj.thread_id === 'string' ? obj.thread_id : undefined;
731
+ // Prefer the trusted context persisted with the card. A callback's
732
+ // thread_id is client-controlled and may be absent or stale.
733
+ const threadId = cardInfo?.replyContext?.threadId
734
+ ?? (typeof obj.thread_id === 'string' ? obj.thread_id : undefined);
733
735
  // 卡片点击者身份:只信认证信封(senderAid 参数,由调用方从 msg.from / msg.sender_aid 提取)。
734
736
  // payload 自报字段(from / sender_aid / user_id)不可信,可被客户端伪造,不读取。
735
737
  // 两类卡片共用:CommandCard → 伪入站消息的 peerId,ActionInteraction → operatorId。
@@ -771,6 +773,10 @@ export class AUNChannel {
771
773
  peerId: cardClickerAid,
772
774
  peerName: typeof obj.label === 'string' ? obj.label : typeof obj.action_label === 'string' ? obj.action_label : undefined,
773
775
  messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
776
+ threadId,
777
+ replyContext: cardInfo?.replyContext ?? ((threadId || cardDelivery)
778
+ ? { ...(threadId ? { threadId } : {}), ...(cardDelivery ? { delivery: cardDelivery } : {}) }
779
+ : undefined),
774
780
  source: 'card-trigger',
775
781
  })).catch((err) => {
776
782
  logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
@@ -1466,12 +1472,13 @@ export class AUNChannel {
1466
1472
  this.store = store;
1467
1473
  const client = await loadClient(store, aidName);
1468
1474
  enableFullGroupPullPagination(client);
1475
+ enableReliableMessageDelivery(client);
1469
1476
  this.client = client;
1470
1477
  // A reconnect replaces the SDK client. Late events from the retired
1471
1478
  // instance must not mutate the new instance's health or message state.
1472
1479
  const isCurrentClient = () => this.client === client;
1473
1480
  // Register event handlers before connecting
1474
- client.on('message.received', (data) => {
1481
+ client.on('message.received', async (data) => {
1475
1482
  if (!isCurrentClient())
1476
1483
  return;
1477
1484
  this.aidState.lastInboundAt = Date.now();
@@ -1480,11 +1487,11 @@ export class AUNChannel {
1480
1487
  const kind = (data && typeof data === 'object') ? data.kind ?? '' : '';
1481
1488
  const keys = (data && typeof data === 'object') ? Object.keys(data).join(',') : typeof data;
1482
1489
  logger.debug(`${this.logPrefix()}[DIAG] message.received: kind=${kind} keys=${keys}`);
1483
- this.handleIncomingPrivateMessage(data);
1490
+ await this.handleIncomingPrivateMessage(data);
1484
1491
  });
1485
1492
  // pureIdentity(控制 AID):协议层不接群消息,不注册 group 创建监听
1486
1493
  if (!this.config.pureIdentity) {
1487
- client.on('group.message_created', (data) => {
1494
+ client.on('group.message_created', async (data) => {
1488
1495
  if (!isCurrentClient())
1489
1496
  return;
1490
1497
  this.aidState.lastInboundAt = Date.now();
@@ -1494,7 +1501,7 @@ export class AUNChannel {
1494
1501
  const gid = env.group_id ?? '';
1495
1502
  const sender = env.from ?? '';
1496
1503
  logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
1497
- this.handleIncomingGroupMessage(data);
1504
+ await this.handleIncomingGroupMessage(data);
1498
1505
  });
1499
1506
  client.on('group.changed', (data) => {
1500
1507
  if (!isCurrentClient())
@@ -1659,6 +1666,14 @@ export class AUNChannel {
1659
1666
  this.connected = true;
1660
1667
  this.connectedAt = Date.now();
1661
1668
  this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
1669
+ // AUN seq_tracker is transport-scoped, while this channel's durable
1670
+ // dedup log is application-scoped. A diagnostic consumer can advance
1671
+ // the former without the application having seen those messages. On a
1672
+ // reconnect, repair only the in-memory P2P cursor to the last message
1673
+ // the application actually claimed, then force a Forward pull. The SDK
1674
+ // persists cursors monotonically, so this does not rewrite SQLite and
1675
+ // the normal pull will restore the high watermark after delivery.
1676
+ this.scheduleInboundP2PCursorRepair(client, isCurrentClient);
1662
1677
  // Workaround: SDK e2ee uses _identity.cert for sender_cert_fingerprint;
1663
1678
  // if cert is missing, it falls back to public key SPKI fingerprint which
1664
1679
  // causes peer cert lookup failures. Backfill from keystore if needed.
@@ -1691,51 +1706,50 @@ export class AUNChannel {
1691
1706
  logger.warn(`${this.logPrefix()} agent config not found for ${aidName}, skipping welcome message`);
1692
1707
  return false;
1693
1708
  }
1694
- const owner = getFirstStaticAgentOwner(aidName);
1695
- if (!owner) {
1709
+ const owners = Array.from(new Set(agentConfig.owners ?? []));
1710
+ if (owners.length === 0) {
1696
1711
  logger.info(`${this.logPrefix()} No owner configured, skipping post-bootstrap welcome message`);
1697
1712
  return false;
1698
1713
  }
1699
- const operationId = postBootstrapWelcomeOperationId(aidName);
1700
- const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
1701
- if (hasMessageLogOperation(chatDir, operationId))
1702
- return true;
1703
- const prepared = outbox.findByDedupeKey(aidName, operationId);
1714
+ this.repairBootstrapRoutes();
1704
1715
  // Bootstrap welcomes target the configured personal Owner AID. They are
1705
1716
  // not replies to an inbound group message, so their route is always
1706
1717
  // private and must not be inferred with group.get_info.
1707
1718
  const lifecycle = resolveAgentLifecycle(agentConfig);
1708
1719
  const delivery = { chatType: 'private' };
1709
- if (prepared
1710
- && prepared.channelId === owner
1711
- && isDeliveryTarget(prepared.delivery)
1712
- && sameDeliveryTarget(prepared.delivery, delivery)) {
1713
- return true;
1714
- }
1715
- const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
1716
- let ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
1717
- // Keep lifecycle=bootstrapping preparation entirely local. Resolving an
1718
- // uncached peer may download agent.md, and welcome display metadata must
1719
- // never delay the durable write-ahead record or the lifecycle commit.
1720
- const cachedOwnerInfo = this.peerInfoCached(owner);
1721
- try {
1722
- const ownerInfo = cachedOwnerInfo
1723
- ?? (lifecycle === 'active' ? await this.fetchPeerInfo(owner) : undefined);
1724
- if (!ownerInfo) {
1725
- preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName, delivery);
1726
- logger.info(`${this.logPrefix()} Post-bootstrap welcome durably prepared for owner: ${owner}`);
1727
- return true;
1720
+ for (const owner of owners) {
1721
+ const operationId = postBootstrapWelcomeOperationId(aidName, owner);
1722
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
1723
+ if (hasMessageLogOperation(chatDir, operationId)
1724
+ || hasMessageLogOperation(chatDir, postBootstrapWelcomeOperationId(aidName)))
1725
+ continue;
1726
+ const prepared = outbox.findByDedupeKey(aidName, operationId);
1727
+ if (prepared && prepared.channelId === owner && isDeliveryTarget(prepared.delivery) && sameDeliveryTarget(prepared.delivery, delivery))
1728
+ continue;
1729
+ const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
1730
+ let ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
1731
+ // Keep lifecycle=bootstrapping preparation entirely local. Resolving an
1732
+ // uncached peer may download agent.md, and welcome display metadata must
1733
+ // never delay the durable write-ahead record or the lifecycle commit.
1734
+ const cachedOwnerInfo = this.peerInfoCached(owner);
1735
+ try {
1736
+ const ownerInfo = cachedOwnerInfo
1737
+ ?? (lifecycle === 'active' ? await this.fetchPeerInfo(owner) : undefined);
1738
+ if (!ownerInfo) {
1739
+ preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName, delivery);
1740
+ continue;
1741
+ }
1742
+ ownerDisplayName = (ownerInfo.name || ownerDisplayName).slice(0, 12);
1743
+ if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
1744
+ logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human).`);
1745
+ }
1728
1746
  }
1729
- ownerDisplayName = (ownerInfo.name || ownerDisplayName).slice(0, 12);
1730
- if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
1731
- logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human).`);
1747
+ catch (e) {
1748
+ logger.warn(`${this.logPrefix()} Failed to resolve owner display name; using AID label: ${e}`);
1732
1749
  }
1750
+ preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName, delivery);
1733
1751
  }
1734
- catch (e) {
1735
- logger.warn(`${this.logPrefix()} Failed to resolve owner display name; using AID label: ${e}`);
1736
- }
1737
- preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName, delivery);
1738
- logger.info(`${this.logPrefix()} Post-bootstrap welcome durably prepared for owner: ${owner}`);
1752
+ logger.info(`${this.logPrefix()} Post-bootstrap welcomes durably prepared for ${owners.length} owner(s)`);
1739
1753
  return true;
1740
1754
  }
1741
1755
  catch (e) {
@@ -1753,9 +1767,48 @@ export class AUNChannel {
1753
1767
  const aid = this.config.aid.replace(/^@/, '');
1754
1768
  return hasPendingPostBootstrapWelcomeOutbox(aid);
1755
1769
  }
1756
- async bindPostBootstrapWelcomeToSession(sessionId) {
1770
+ async bindPostBootstrapWelcomeToSession(sessionId, owner) {
1757
1771
  const aid = this.config.aid.replace(/^@/, '');
1758
- return bindPostBootstrapWelcomeOutboxSession(aid, sessionId);
1772
+ return bindPostBootstrapWelcomeOutboxSession(aid, sessionId, owner);
1773
+ }
1774
+ scheduleInboundP2PCursorRepair(client, isCurrentClient) {
1775
+ const tracker = client?._seqTracker;
1776
+ const repair = tracker?.repairContiguousSeq;
1777
+ const pull = client?._pullV2;
1778
+ if (typeof repair !== 'function' || typeof pull !== 'function')
1779
+ return;
1780
+ const ns = `p2p:${this.config.aid}`;
1781
+ let appSeq = 0;
1782
+ for (const [key, value] of this.inboundSeenMessages) {
1783
+ // Group sequence spaces are independent from the P2P space.
1784
+ if (!key.includes(':aun:private'))
1785
+ continue;
1786
+ const seq = Number(value.seq);
1787
+ if (Number.isSafeInteger(seq) && seq > appSeq)
1788
+ appSeq = seq;
1789
+ }
1790
+ if (appSeq <= 0)
1791
+ return;
1792
+ setTimeout(() => {
1793
+ if (!isCurrentClient() || !this.connected)
1794
+ return;
1795
+ const transportSeq = Number(tracker.getContiguousSeq?.(ns) ?? 0);
1796
+ if (!Number.isSafeInteger(transportSeq) || transportSeq <= appSeq + 1)
1797
+ return;
1798
+ try {
1799
+ repair.call(tracker, ns, appSeq);
1800
+ logger.warn(`${this.logPrefix()} Rewinding in-memory P2P cursor for application recovery: transport=${transportSeq}, app=${appSeq}`);
1801
+ Promise.resolve(pull.call(client, appSeq, 50, { force: true }))
1802
+ .then(() => logger.info(`${this.logPrefix()} Replayed P2P messages after cursor mismatch: after_seq=${appSeq}`))
1803
+ .catch((error) => logger.warn(`${this.logPrefix()} P2P cursor recovery pull failed: ${error instanceof Error ? error.message : String(error)}`));
1804
+ }
1805
+ catch (error) {
1806
+ logger.warn(`${this.logPrefix()} P2P cursor repair failed: ${error instanceof Error ? error.message : String(error)}`);
1807
+ }
1808
+ }, 250);
1809
+ }
1810
+ pendingPostBootstrapWelcomeOwners() {
1811
+ return pendingPostBootstrapWelcomeOwners(this.config.aid);
1759
1812
  }
1760
1813
  /**
1761
1814
  * Resume delivery of an already prepared completion welcome.
@@ -1768,20 +1821,9 @@ export class AUNChannel {
1768
1821
  this.repairBootstrapRoutes();
1769
1822
  const configuredAid = this.config.aid;
1770
1823
  const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
1771
- const operationId = postBootstrapWelcomeOperationId(aid);
1772
- const entry = outbox.findByDedupeKey(aid, operationId);
1773
- if (!entry) {
1774
- const owner = getFirstStaticAgentOwner(aid);
1775
- if (!owner)
1776
- return false;
1777
- const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
1778
- return hasMessageLogOperation(chatDir, operationId);
1779
- }
1780
- if (!outbox.isDeliveryForChannel(entry.delivery, entry.channelId)) {
1781
- logger.warn(`${this.logPrefix()} Discarding unrouted legacy post-bootstrap welcome: id=${entry.id}`);
1782
- outbox.remove(aid, entry.id);
1824
+ const entries = findPostBootstrapWelcomeOutbox(aid);
1825
+ if (entries.length === 0)
1783
1826
  return false;
1784
- }
1785
1827
  const agentConfig = loadAgent(aid);
1786
1828
  if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
1787
1829
  logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
@@ -1789,11 +1831,15 @@ export class AUNChannel {
1789
1831
  }
1790
1832
  if (!this.connected || !this.client)
1791
1833
  return true;
1792
- const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
1793
- if (sent.status === 'sent')
1794
- this.removeDeliveredOutboxEntry(entry);
1795
- else if (sent.status === 'permanent')
1796
- this.markPermanentOutboxFailure(entry, sent);
1834
+ for (const entry of entries) {
1835
+ if (!outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
1836
+ continue;
1837
+ const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
1838
+ if (sent.status === 'sent')
1839
+ this.removeDeliveredOutboxEntry(entry);
1840
+ else if (sent.status === 'permanent')
1841
+ this.markPermanentOutboxFailure(entry, sent);
1842
+ }
1797
1843
  return true;
1798
1844
  }
1799
1845
  // ── Event handlers ──────────────────────────────────────────
@@ -3553,7 +3599,7 @@ export class AUNChannel {
3553
3599
  if (ids && ids.size === 0)
3554
3600
  this.interactionCardMessageIds.delete(interactionId);
3555
3601
  }
3556
- registerCardPostSend(messageId, action, channelId, delivery) {
3602
+ registerCardPostSend(messageId, action, channelId, delivery, replyContext) {
3557
3603
  if (action.type !== 'register_interaction_card')
3558
3604
  return;
3559
3605
  this.cardMessageIdMap.set(messageId, {
@@ -3562,6 +3608,7 @@ export class AUNChannel {
3562
3608
  initiatorAid: action.initiatorAid,
3563
3609
  channelId,
3564
3610
  delivery: delivery ?? action.delivery,
3611
+ replyContext,
3565
3612
  });
3566
3613
  this.ownedCardMsgIds.add(messageId);
3567
3614
  let messageIds = this.interactionCardMessageIds.get(action.requestId);
@@ -3594,7 +3641,7 @@ export class AUNChannel {
3594
3641
  if (!entry.postSend)
3595
3642
  return;
3596
3643
  if (entry.postSend.type === 'register_interaction_card') {
3597
- this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery);
3644
+ this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery, entry.context);
3598
3645
  }
3599
3646
  }
3600
3647
  async sendAunPayload(channelId, payload, context, label) {
@@ -3923,6 +3970,9 @@ export class AUNChannel {
3923
3970
  }
3924
3971
  async sendReliableStructured(channelId, payload, context, logText) {
3925
3972
  const result = await this.sendContentPayload(channelId, payload, {
3973
+ // Activity items use the durable activity queue so reconnect draining
3974
+ // preserves their ordering relative to other lifecycle events.
3975
+ queue: payload.type === 'activity' ? 'activity' : undefined,
3926
3976
  contentKind: 'custom',
3927
3977
  context,
3928
3978
  logText: logText ?? this.payloadLogText(payload, 'custom'),
@@ -3933,6 +3983,7 @@ export class AUNChannel {
3933
3983
  outboxId: result.outboxId,
3934
3984
  });
3935
3985
  }
3986
+ return result;
3936
3987
  }
3937
3988
  async sendMessage(channelId, text, context) {
3938
3989
  if (!text?.trim()) {
@@ -4344,7 +4395,7 @@ export class AUNChannel {
4344
4395
  logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
4345
4396
  return sentOutboxResult(completedMessageId);
4346
4397
  }
4347
- if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
4398
+ if (isPostBootstrapWelcomeOperationId(operationId, this.config.aid)) {
4348
4399
  const agentConfig = loadAgent(this.config.aid);
4349
4400
  if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
4350
4401
  return { status: 'retry', error: 'post-bootstrap welcome is waiting for active lifecycle', code: 'BOOTSTRAP_NOT_ACTIVE' };
@@ -5317,15 +5368,19 @@ export class AUNChannel {
5317
5368
  /** Repair bootstrap outbox entries to the configured personal Owner route. */
5318
5369
  repairBootstrapRoutes() {
5319
5370
  const aid = this.config.aid.replace(/^@/, '');
5320
- const owner = getFirstStaticAgentOwner(aid);
5321
- if (!owner)
5322
- return;
5323
- const completionEntry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid), { includeTerminal: true });
5371
+ const owners = Array.from(new Set(loadAgent(aid)?.owners ?? []));
5324
5372
  const entries = [
5325
5373
  ...outbox.findByDedupePrefix(aid, bootstrapInitialMessageOperationPrefix(aid), { includeTerminal: true }),
5326
- ...(completionEntry ? [completionEntry] : []),
5374
+ ...findPostBootstrapWelcomeOutbox(aid, { includeTerminal: true }),
5327
5375
  ].filter((entry) => !!entry?.channelId);
5328
5376
  for (const entry of entries) {
5377
+ const isInitial = entry.dedupeKey?.startsWith(bootstrapInitialMessageOperationPrefix(aid));
5378
+ const isOwnerSpecific = entry.dedupeKey?.startsWith(`bootstrap-complete:v2:${aid}:`);
5379
+ const owner = isInitial ? owners[0]
5380
+ : isOwnerSpecific ? owners.find(candidate => postBootstrapWelcomeOperationId(aid, candidate) === entry.dedupeKey)
5381
+ : (owners.includes(entry.channelId) ? entry.channelId : owners[0]);
5382
+ if (!owner)
5383
+ continue;
5329
5384
  const delivery = { chatType: 'private' };
5330
5385
  const nestedDelivery = entry.context?.delivery;
5331
5386
  const routeAlreadyPrivate = entry.channelId === owner
@@ -5387,6 +5442,7 @@ export class AUNChannel {
5387
5442
  timeout: 'timeout',
5388
5443
  queued: 'queued',
5389
5444
  progress: 'progress',
5445
+ notice: 'notice',
5390
5446
  requires_action: 'requires_action',
5391
5447
  };
5392
5448
  const statusPayload = {
@@ -5446,6 +5502,16 @@ export class AUNChannel {
5446
5502
  logger.info(logTaskStatus);
5447
5503
  }
5448
5504
  async sendCustomPayload(channelId, payload, context) {
5505
+ // Validate the route even when disconnected; callers must never be able
5506
+ // to bypass the fail-closed AUN routing contract. This compatibility API
5507
+ // is intentionally best-effort while offline, so a valid route simply
5508
+ // returns until the channel reconnects.
5509
+ const delivery = this.requireDelivery(channelId, context);
5510
+ context = this.withDelivery(context, delivery);
5511
+ if (!this.connected || !this.client) {
5512
+ logger.debug(`${this.logPrefix()} Custom payload skipped while disconnected: channel=${channelId}`);
5513
+ return;
5514
+ }
5449
5515
  // SDK 0.3.0 E2EE requires payload to be an object
5450
5516
  let payloadObj;
5451
5517
  try {
@@ -5836,12 +5902,14 @@ export class AUNChannelPlugin {
5836
5902
  await channel.sendThought(channelId, envelope.taskId, aunPayload, replyCtx);
5837
5903
  }
5838
5904
  else {
5839
- const result = await channel.sendContentPayload(channelId, aunPayload, {
5840
- queue: 'activity',
5841
- contentKind: 'custom',
5842
- context: replyCtx,
5843
- logText: channel.activityLogText(item),
5844
- });
5905
+ // Interactive activities are reliable structured payloads;
5906
+ // sendReliableStructured internally selects the activity
5907
+ // outbox queue while preserving the common delivery contract.
5908
+ const result = await channel.sendReliableStructured(channelId, aunPayload, replyCtx, channel.activityLogText(item));
5909
+ // Test and legacy adapters may stub this method without a
5910
+ // structured result; the call itself still proves acceptance.
5911
+ if (!result)
5912
+ continue;
5845
5913
  if (result.status === 'permanent' || result.status === 'failed')
5846
5914
  failedResult ??= result;
5847
5915
  else if (result.status === 'retry' || result.status === 'queued' || result.queued)
@@ -5885,6 +5953,9 @@ export class AUNChannelPlugin {
5885
5953
  case 'status.progress':
5886
5954
  channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
5887
5955
  return;
5956
+ case 'status.notice':
5957
+ channel.sendProcessingStatus(channelId, 'notice', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
5958
+ return;
5888
5959
  case 'status.requires_action':
5889
5960
  channel.sendProcessingStatus(channelId, 'requires_action', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
5890
5961
  return;
@@ -133,8 +133,12 @@ export class DaemonChannel {
133
133
  const slot = this.pending.get(runId);
134
134
  if (!slot || slot.terminalClaim)
135
135
  return;
136
- if (payload.kind !== 'status.requires_action')
136
+ // A task notice reports an observation (for example, idle detection); it
137
+ // is not evidence that the runner made progress and must not renew the
138
+ // outer trigger watchdog.
139
+ if (payload.kind !== 'status.requires_action' && payload.kind !== 'status.notice') {
137
140
  this.resetWatchdog(runId, slot);
141
+ }
138
142
  switch (payload.kind) {
139
143
  case 'activity.batch':
140
144
  for (const item of payload.items) {
@@ -171,6 +175,8 @@ export class DaemonChannel {
171
175
  if (payload.metadata?.activityType === 'tool_call')
172
176
  this.recordToolCall(slot, payload.metadata.callId);
173
177
  break;
178
+ case 'status.notice':
179
+ break;
174
180
  case 'status.requires_action':
175
181
  slot.waitingForAction = true;
176
182
  if (slot.watchdogTimer)
@@ -1319,6 +1319,7 @@ export class DingtalkChannelPlugin {
1319
1319
  case 'status.started':
1320
1320
  case 'status.queued':
1321
1321
  case 'status.progress':
1322
+ case 'status.notice':
1322
1323
  return suppressedReceipt(envelope, 'reaction_lifecycle');
1323
1324
  case 'status.completed':
1324
1325
  await channel.completeAckReaction(envelope.taskId, true);
@@ -393,6 +393,7 @@ export class FeishuChannel {
393
393
  const chatId = data.context?.open_chat_id || data.open_chat_id;
394
394
  const cardMessageId = data.open_message_id || data.context?.open_message_id;
395
395
  const formValues = action.form_value || {};
396
+ const entry = this.cardMetaStore.get(value._id);
396
397
  const decision = routeCardAction({ value, formValues, operatorId }, this.cardMetaStore);
397
398
  switch (decision.kind) {
398
399
  case 'ignore':
@@ -413,6 +414,9 @@ export class FeishuChannel {
413
414
  content: decision.command,
414
415
  peerId: operatorId,
415
416
  messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
417
+ threadId: entry?.replyContext?.threadId,
418
+ chatType: entry?.replyContext?.delivery?.chatType,
419
+ replyContext: entry?.replyContext,
416
420
  source: 'card-trigger',
417
421
  });
418
422
  }
@@ -1197,6 +1201,7 @@ export class FeishuChannel {
1197
1201
  const invalidationReason = this.invalidatedInteractions.get(interaction.id);
1198
1202
  this.cardMetaStore.set(interaction.id, {
1199
1203
  interaction, chatId, messageId, resolved: false,
1204
+ replyContext: options,
1200
1205
  invalidated: !!invalidationReason,
1201
1206
  invalidatedReason: invalidationReason,
1202
1207
  });
@@ -1750,6 +1755,7 @@ export class FeishuChannelPlugin {
1750
1755
  case 'status.started':
1751
1756
  case 'status.queued':
1752
1757
  case 'status.progress':
1758
+ case 'status.notice':
1753
1759
  return suppressedReceipt(envelope, 'reaction_lifecycle');
1754
1760
  case 'status.completed':
1755
1761
  await channel.finalizeAckReaction(envelope.taskId, 'CheckMark');
@@ -1804,16 +1810,16 @@ export class FeishuChannelPlugin {
1804
1810
  disconnect: () => channel.disconnect(),
1805
1811
  onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
1806
1812
  registerBridge(bridge, channelType) {
1807
- bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, mentionAids, threadId, rootId, chatType, source }) => {
1808
- const replyContext = peerId || threadId
1813
+ bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, mentionAids, threadId, rootId, chatType, replyContext: inboundReplyContext, source }) => {
1814
+ const replyContext = inboundReplyContext ?? (peerId || threadId
1809
1815
  ? {
1810
1816
  ...(peerId ? { peerId } : {}),
1811
1817
  ...(threadId ? { threadId, replyToMessageId: rootId ?? threadId, replyInThread: true } : {}),
1812
1818
  }
1813
- : undefined;
1819
+ : undefined);
1814
1820
  await handler({
1815
1821
  channel: adapter.channelName, channelType, channelId: chatId, content, images,
1816
- selfAID: ctx.agentName, chatType: chatType || 'private',
1822
+ selfAID: ctx.agentName, chatType: chatType || replyContext?.delivery?.chatType || 'private',
1817
1823
  peerId: peerId || '', peerName, messageId, mentions, mentionAids, threadId,
1818
1824
  replyContext,
1819
1825
  source,
@@ -1461,7 +1461,8 @@ export class WecomChannelPlugin {
1461
1461
  return;
1462
1462
  case 'status.started':
1463
1463
  case 'status.queued':
1464
- case 'status.progress': return;
1464
+ case 'status.progress':
1465
+ case 'status.notice': return;
1465
1466
  case 'status.requires_action':
1466
1467
  await channel.sendMessage(channelId, '等待 owner 审批', envelope.replyContext);
1467
1468
  return;
@@ -69,6 +69,7 @@ Options:
69
69
  return;
70
70
  }
71
71
  const { agentList, agentShow, agentCreateInteractive, agentCreateNonInteractive, agentReload, agentEnable, agentDisable, agentGet, agentSet, agentRename, agentDelete, agentReady, runManagedAgentOperation, } = await import('./agent.js');
72
+ const { parseAgentOwnerArgs, resolveAgentCreatePlan, validateAgentCreatePlan } = await import('./agent-create.js');
72
73
  if (process.env.EVOLCORE_SESSION_ID && !wantsHelp(args) && !new Set(['get', 'set', 'ready']).has(sub)) {
73
74
  if (sub === 'new' && !args.includes('--non-interactive')) {
74
75
  const error = {
@@ -210,6 +211,8 @@ Options:
210
211
  --baseagent <claude|codex|gemini|ecagent>
211
212
  默认: defaults 配置,否则检测本机可用基座
212
213
  --owner <aid> 默认: daemon.json 第一个 owner
214
+ --owners <json-array> 多个 Owner;与 --owner 互斥
215
+ --handoff <context> 当前任务的 Bootstrap 上下文
213
216
  --name <display-name> 默认: package.json name,否则目录名
214
217
  --description <text> 默认: package.json description,否则使用 EvolCore 默认描述
215
218
  --dry-run 只预览最终参数,不注册 AID、不写文件
@@ -223,6 +226,15 @@ Options:
223
226
  ec agent new mybot.agentid.pub --non-interactive --project ../project --baseagent claude`);
224
227
  return;
225
228
  }
229
+ if (args.includes('--handoff')) {
230
+ const result = { ok: false, code: 'MANAGED_ONLY', error: '--handoff requires a managed Agent task' };
231
+ if (formatJson)
232
+ console.log(JSON.stringify(result));
233
+ else
234
+ console.error(result.error);
235
+ process.exitCode = 1;
236
+ return;
237
+ }
226
238
  const name = args[1];
227
239
  const nonInteractive = args.includes('--non-interactive');
228
240
  if (nonInteractive) {
@@ -234,12 +246,25 @@ Options:
234
246
  const idx = args.indexOf(flag);
235
247
  return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
236
248
  };
237
- const { resolveAgentCreatePlan, validateAgentCreatePlan } = await import('./agent.js');
249
+ let ownerOptions;
250
+ try {
251
+ ownerOptions = parseAgentOwnerArgs(args);
252
+ }
253
+ catch (error) {
254
+ const result = { ok: false, error: error instanceof Error ? error.message : String(error) };
255
+ if (formatJson)
256
+ console.log(JSON.stringify(result));
257
+ else
258
+ console.error(`❌ ${result.error}`);
259
+ process.exitCode = 1;
260
+ return;
261
+ }
238
262
  const plan = resolveAgentCreatePlan({
239
263
  aid: name,
240
264
  project: getArg('--project'),
241
265
  baseagent: getArg('--baseagent'),
242
- owner: getArg('--owner'),
266
+ ...ownerOptions,
267
+ handoff: getArg('--handoff'),
243
268
  name: getArg('--name'),
244
269
  description: getArg('--description'),
245
270
  force: args.includes('--force'),
@@ -263,7 +288,7 @@ Options:
263
288
  console.log(` AID: ${plan.aid}`);
264
289
  console.log(` Project: ${plan.project}`);
265
290
  console.log(` Baseagent: ${plan.baseagent} (${plan.baseagentSource})`);
266
- console.log(` Owner: ${plan.owner || '未设置'} (${plan.ownerSource})`);
291
+ console.log(` Owner: ${plan.owners?.join(', ') || plan.owner || '未设置'} (${plan.ownerSource})`);
267
292
  console.log(` Name: ${plan.name} (${plan.nameSource})`);
268
293
  if (plan.description)
269
294
  console.log(` Description: ${plan.description} (${plan.descriptionSource})`);
@@ -276,6 +301,7 @@ Options:
276
301
  baseagent: plan.baseagent,
277
302
  project: plan.project,
278
303
  owner: plan.owner,
304
+ owners: plan.owners,
279
305
  name: plan.name,
280
306
  description: plan.description,
281
307
  force: args.includes('--force'),