evolcore 0.0.22 → 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 (117) hide show
  1. package/CHANGELOG.md +74 -14
  2. package/README.md +4 -0
  3. package/bin/codex-managed-acl.ps1 +160 -0
  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 +384 -95
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +30 -2
  9. package/dist/agents/claude-runner.js +179 -240
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +369 -0
  14. package/dist/agents/codex-runner.js +839 -192
  15. package/dist/agents/ecagent-runner.js +27 -34
  16. package/dist/agents/gemini-runner.js +62 -8
  17. package/dist/agents/runner-types.js +1 -1
  18. package/dist/aun/aid/managed-operation.js +28 -6
  19. package/dist/aun/aid/store.js +43 -0
  20. package/dist/aun/menu-client.js +272 -0
  21. package/dist/aun/msg/group.js +65 -4
  22. package/dist/aun/msg/index.js +1 -1
  23. package/dist/aun/msg/managed-operation.js +57 -5
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +7 -1
  26. package/dist/channels/aun.js +178 -77
  27. package/dist/channels/daemon.js +7 -1
  28. package/dist/channels/dingtalk.js +1 -0
  29. package/dist/channels/feishu.js +10 -4
  30. package/dist/channels/wechat.js +81 -10
  31. package/dist/channels/wecom.js +2 -1
  32. package/dist/cli/agent-command.js +29 -3
  33. package/dist/cli/agent-create.js +538 -0
  34. package/dist/cli/agent.js +26 -675
  35. package/dist/cli/aun-commands.js +108 -24
  36. package/dist/cli/command-log.js +21 -2
  37. package/dist/cli/ctl-command.js +2 -0
  38. package/dist/cli/daemon-commands.js +289 -61
  39. package/dist/cli/index.js +22 -2
  40. package/dist/cli/init.js +2 -10
  41. package/dist/cli/managed-command-guard.js +3 -0
  42. package/dist/cli/menu-client.js +95 -0
  43. package/dist/cli/model.js +23 -8
  44. package/dist/cli/restart-monitor.js +432 -8
  45. package/dist/cli/stats.js +110 -0
  46. package/dist/cli/task-context.js +12 -2
  47. package/dist/cli/trigger-command.js +9 -1
  48. package/dist/config/builtin-roles.js +2 -0
  49. package/dist/config/schema-migration.js +14 -2
  50. package/dist/core/auth/agent-delegation.js +10 -7
  51. package/dist/core/auth/auth-gateway.js +3 -2
  52. package/dist/core/auth/authorization-audit.js +75 -18
  53. package/dist/core/auth/operation-authorizer.js +63 -3
  54. package/dist/core/auth/operation-catalog.js +14 -2
  55. package/dist/core/auth/remote-access-store.js +136 -0
  56. package/dist/core/baseagent-loader.js +4 -4
  57. package/dist/core/bootstrap-messages.js +39 -18
  58. package/dist/core/bootstrap-service.js +32 -26
  59. package/dist/core/capability/capability-manager.js +8 -4
  60. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  61. package/dist/core/command/agent-control.js +7 -2
  62. package/dist/core/command/command-handler.js +128 -30
  63. package/dist/core/command/daemon-diagnostics.js +448 -0
  64. package/dist/core/command/menu-catalog.js +6 -1
  65. package/dist/core/command/menu-handler.js +410 -98
  66. package/dist/core/command/menu-protocol.js +123 -0
  67. package/dist/core/command/slash-gate.js +4 -6
  68. package/dist/core/command/slash-handler.js +199 -70
  69. package/dist/core/data-migration.js +5 -15
  70. package/dist/core/event-catalog.js +3 -0
  71. package/dist/core/handoff/runtime.js +3 -1
  72. package/dist/core/handoff/store.js +7 -1
  73. package/dist/core/message/im-renderer.js +13 -6
  74. package/dist/core/message/message-bridge.js +5 -36
  75. package/dist/core/message/message-queue.js +20 -15
  76. package/dist/core/message/message-utils.js +6 -3
  77. package/dist/core/message/response-engine.js +147 -94
  78. package/dist/core/model/catalog-service.js +310 -0
  79. package/dist/core/model/model-catalog.js +134 -334
  80. package/dist/core/model/model-diagnostics.js +4 -5
  81. package/dist/core/model/model-fallback.js +9 -16
  82. package/dist/core/model/model-permission.js +5 -2
  83. package/dist/core/permission/approval-gateway.js +183 -32
  84. package/dist/core/permission/protected-paths.js +63 -4
  85. package/dist/core/permission/sandbox-runtime.js +4 -2
  86. package/dist/core/permission/tool-policy.js +257 -31
  87. package/dist/core/runtime-lock.js +13 -8
  88. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  89. package/dist/core/session/session-manager.js +49 -13
  90. package/dist/core/session/session-renew.js +1 -0
  91. package/dist/index.js +214 -57
  92. package/dist/ipc.js +191 -102
  93. package/dist/paths.js +23 -1
  94. package/dist/stats/billing.js +18 -4
  95. package/dist/trigger/manager.js +3 -1
  96. package/dist/trigger/parser.js +77 -13
  97. package/dist/trigger/patch.js +104 -1
  98. package/dist/utils/atomic-write.js +26 -2
  99. package/dist/utils/cross-platform.js +436 -1
  100. package/dist/utils/ecweb-supervisor.js +11 -2
  101. package/dist/utils/ecweb-utils.js +8 -26
  102. package/dist/utils/error-utils.js +16 -2
  103. package/dist/utils/model-prices.jsonl +23 -20
  104. package/dist/utils/npm-ops.js +70 -15
  105. package/dist/utils/windows-autostart.js +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -0
  107. package/kits/docs/evolcore/agent-create.md +13 -0
  108. package/kits/docs/evolcore/group-collaboration.md +7 -2
  109. package/kits/docs/evolcore/menu-client.md +34 -0
  110. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  111. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  112. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  113. package/kits/templates/roles/admin.json +2 -0
  114. package/kits/templates/roles/visitor.json +1 -0
  115. package/kits/templates/system-fragments/bootstrap.md +8 -1
  116. package/package.json +16 -4
  117. 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';
@@ -484,7 +483,8 @@ function redactMenuTokensForTrace(value, seen = new WeakSet()) {
484
483
  return value.map(item => redactMenuTokensForTrace(item, seen));
485
484
  const redacted = {};
486
485
  for (const [key, item] of Object.entries(value)) {
487
- redacted[key] = key.toLowerCase() === 'menu_token'
486
+ const normalizedKey = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toLowerCase();
487
+ redacted[key] = /^(?:menu|ecweb)_token$/.test(normalizedKey)
488
488
  ? '[REDACTED]'
489
489
  : redactMenuTokensForTrace(item, seen);
490
490
  }
@@ -728,7 +728,10 @@ export class AUNChannel {
728
728
  const actionValue = isComposeReply ? '_custom_input'
729
729
  : typeof obj.value === 'string' ? obj.value
730
730
  : typeof obj.action_value === 'string' ? obj.action_value : text;
731
- 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);
732
735
  // 卡片点击者身份:只信认证信封(senderAid 参数,由调用方从 msg.from / msg.sender_aid 提取)。
733
736
  // payload 自报字段(from / sender_aid / user_id)不可信,可被客户端伪造,不读取。
734
737
  // 两类卡片共用:CommandCard → 伪入站消息的 peerId,ActionInteraction → operatorId。
@@ -770,6 +773,10 @@ export class AUNChannel {
770
773
  peerId: cardClickerAid,
771
774
  peerName: typeof obj.label === 'string' ? obj.label : typeof obj.action_label === 'string' ? obj.action_label : undefined,
772
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),
773
780
  source: 'card-trigger',
774
781
  })).catch((err) => {
775
782
  logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
@@ -1465,12 +1472,13 @@ export class AUNChannel {
1465
1472
  this.store = store;
1466
1473
  const client = await loadClient(store, aidName);
1467
1474
  enableFullGroupPullPagination(client);
1475
+ enableReliableMessageDelivery(client);
1468
1476
  this.client = client;
1469
1477
  // A reconnect replaces the SDK client. Late events from the retired
1470
1478
  // instance must not mutate the new instance's health or message state.
1471
1479
  const isCurrentClient = () => this.client === client;
1472
1480
  // Register event handlers before connecting
1473
- client.on('message.received', (data) => {
1481
+ client.on('message.received', async (data) => {
1474
1482
  if (!isCurrentClient())
1475
1483
  return;
1476
1484
  this.aidState.lastInboundAt = Date.now();
@@ -1479,11 +1487,11 @@ export class AUNChannel {
1479
1487
  const kind = (data && typeof data === 'object') ? data.kind ?? '' : '';
1480
1488
  const keys = (data && typeof data === 'object') ? Object.keys(data).join(',') : typeof data;
1481
1489
  logger.debug(`${this.logPrefix()}[DIAG] message.received: kind=${kind} keys=${keys}`);
1482
- this.handleIncomingPrivateMessage(data);
1490
+ await this.handleIncomingPrivateMessage(data);
1483
1491
  });
1484
1492
  // pureIdentity(控制 AID):协议层不接群消息,不注册 group 创建监听
1485
1493
  if (!this.config.pureIdentity) {
1486
- client.on('group.message_created', (data) => {
1494
+ client.on('group.message_created', async (data) => {
1487
1495
  if (!isCurrentClient())
1488
1496
  return;
1489
1497
  this.aidState.lastInboundAt = Date.now();
@@ -1493,7 +1501,7 @@ export class AUNChannel {
1493
1501
  const gid = env.group_id ?? '';
1494
1502
  const sender = env.from ?? '';
1495
1503
  logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
1496
- this.handleIncomingGroupMessage(data);
1504
+ await this.handleIncomingGroupMessage(data);
1497
1505
  });
1498
1506
  client.on('group.changed', (data) => {
1499
1507
  if (!isCurrentClient())
@@ -1504,6 +1512,35 @@ export class AUNChannel {
1504
1512
  : {};
1505
1513
  this.invalidateObserverGroupMembers(envelope.group_id ?? record.group_id);
1506
1514
  });
1515
+ // fastaun 0.5.12+ exposes targeted invitations as a local, aggregated
1516
+ // event. It is a control signal, not a user message, so keep it out of
1517
+ // the Agent routing path while retaining it in diagnostics.
1518
+ client.on('group.invite_received', (data) => {
1519
+ if (!isCurrentClient())
1520
+ return;
1521
+ const record = data && typeof data === 'object' ? data : {};
1522
+ const groupId = record.group_id ?? record.group_aid ?? 'unknown';
1523
+ const inviteIds = Array.isArray(record.invite_ids) ? record.invite_ids : [];
1524
+ this.trace('IN', 'group.invite_received', data);
1525
+ logger.info(`${this.logPrefix()} Group invitation received: group=${groupId} pending=${inviteIds.length || 1}`);
1526
+ });
1527
+ }
1528
+ // 0.5.10/0.5.11 local lifecycle events summarize SDK delivery and Pull
1529
+ // progress. They must never be interpreted as inbound chat messages.
1530
+ for (const eventName of ['delivery.changed', 'sync.started', 'sync.progress', 'sync.stopped']) {
1531
+ client.on(eventName, (data) => {
1532
+ if (!isCurrentClient())
1533
+ return;
1534
+ this.trace('IN', eventName, data);
1535
+ if (eventName === 'sync.progress') {
1536
+ const record = data && typeof data === 'object' ? data : {};
1537
+ logger.debug(`${this.logPrefix()} AUN sync progress: namespace=${record.namespace ?? 'unknown'} page=${record.page ?? '?'} pulled=${record.page_pulled_count ?? record.pulled_count ?? 0}`);
1538
+ }
1539
+ else if (eventName === 'sync.stopped') {
1540
+ const record = data && typeof data === 'object' ? data : {};
1541
+ logger.info(`${this.logPrefix()} AUN sync stopped: reason=${record.reason ?? 'unknown'}`);
1542
+ }
1543
+ });
1507
1544
  }
1508
1545
  const handleSdkState = (source) => (data) => {
1509
1546
  if (!isCurrentClient())
@@ -1629,6 +1666,14 @@ export class AUNChannel {
1629
1666
  this.connected = true;
1630
1667
  this.connectedAt = Date.now();
1631
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);
1632
1677
  // Workaround: SDK e2ee uses _identity.cert for sender_cert_fingerprint;
1633
1678
  // if cert is missing, it falls back to public key SPKI fingerprint which
1634
1679
  // causes peer cert lookup failures. Backfill from keystore if needed.
@@ -1661,51 +1706,50 @@ export class AUNChannel {
1661
1706
  logger.warn(`${this.logPrefix()} agent config not found for ${aidName}, skipping welcome message`);
1662
1707
  return false;
1663
1708
  }
1664
- const owner = getFirstStaticAgentOwner(aidName);
1665
- if (!owner) {
1709
+ const owners = Array.from(new Set(agentConfig.owners ?? []));
1710
+ if (owners.length === 0) {
1666
1711
  logger.info(`${this.logPrefix()} No owner configured, skipping post-bootstrap welcome message`);
1667
1712
  return false;
1668
1713
  }
1669
- const operationId = postBootstrapWelcomeOperationId(aidName);
1670
- const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
1671
- if (hasMessageLogOperation(chatDir, operationId))
1672
- return true;
1673
- const prepared = outbox.findByDedupeKey(aidName, operationId);
1714
+ this.repairBootstrapRoutes();
1674
1715
  // Bootstrap welcomes target the configured personal Owner AID. They are
1675
1716
  // not replies to an inbound group message, so their route is always
1676
1717
  // private and must not be inferred with group.get_info.
1677
1718
  const lifecycle = resolveAgentLifecycle(agentConfig);
1678
1719
  const delivery = { chatType: 'private' };
1679
- if (prepared
1680
- && prepared.channelId === owner
1681
- && isDeliveryTarget(prepared.delivery)
1682
- && sameDeliveryTarget(prepared.delivery, delivery)) {
1683
- return true;
1684
- }
1685
- const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
1686
- let ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
1687
- // Keep lifecycle=bootstrapping preparation entirely local. Resolving an
1688
- // uncached peer may download agent.md, and welcome display metadata must
1689
- // never delay the durable write-ahead record or the lifecycle commit.
1690
- const cachedOwnerInfo = this.peerInfoCached(owner);
1691
- try {
1692
- const ownerInfo = cachedOwnerInfo
1693
- ?? (lifecycle === 'active' ? await this.fetchPeerInfo(owner) : undefined);
1694
- if (!ownerInfo) {
1695
- preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName, delivery);
1696
- logger.info(`${this.logPrefix()} Post-bootstrap welcome durably prepared for owner: ${owner}`);
1697
- 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
+ }
1698
1746
  }
1699
- ownerDisplayName = (ownerInfo.name || ownerDisplayName).slice(0, 12);
1700
- if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
1701
- 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}`);
1702
1749
  }
1750
+ preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName, delivery);
1703
1751
  }
1704
- catch (e) {
1705
- logger.warn(`${this.logPrefix()} Failed to resolve owner display name; using AID label: ${e}`);
1706
- }
1707
- preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName, delivery);
1708
- 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)`);
1709
1753
  return true;
1710
1754
  }
1711
1755
  catch (e) {
@@ -1723,9 +1767,48 @@ export class AUNChannel {
1723
1767
  const aid = this.config.aid.replace(/^@/, '');
1724
1768
  return hasPendingPostBootstrapWelcomeOutbox(aid);
1725
1769
  }
1726
- async bindPostBootstrapWelcomeToSession(sessionId) {
1770
+ async bindPostBootstrapWelcomeToSession(sessionId, owner) {
1727
1771
  const aid = this.config.aid.replace(/^@/, '');
1728
- 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);
1729
1812
  }
1730
1813
  /**
1731
1814
  * Resume delivery of an already prepared completion welcome.
@@ -1738,20 +1821,9 @@ export class AUNChannel {
1738
1821
  this.repairBootstrapRoutes();
1739
1822
  const configuredAid = this.config.aid;
1740
1823
  const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
1741
- const operationId = postBootstrapWelcomeOperationId(aid);
1742
- const entry = outbox.findByDedupeKey(aid, operationId);
1743
- if (!entry) {
1744
- const owner = getFirstStaticAgentOwner(aid);
1745
- if (!owner)
1746
- return false;
1747
- const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
1748
- return hasMessageLogOperation(chatDir, operationId);
1749
- }
1750
- if (!outbox.isDeliveryForChannel(entry.delivery, entry.channelId)) {
1751
- logger.warn(`${this.logPrefix()} Discarding unrouted legacy post-bootstrap welcome: id=${entry.id}`);
1752
- outbox.remove(aid, entry.id);
1824
+ const entries = findPostBootstrapWelcomeOutbox(aid);
1825
+ if (entries.length === 0)
1753
1826
  return false;
1754
- }
1755
1827
  const agentConfig = loadAgent(aid);
1756
1828
  if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
1757
1829
  logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
@@ -1759,11 +1831,15 @@ export class AUNChannel {
1759
1831
  }
1760
1832
  if (!this.connected || !this.client)
1761
1833
  return true;
1762
- const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
1763
- if (sent.status === 'sent')
1764
- this.removeDeliveredOutboxEntry(entry);
1765
- else if (sent.status === 'permanent')
1766
- 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
+ }
1767
1843
  return true;
1768
1844
  }
1769
1845
  // ── Event handlers ──────────────────────────────────────────
@@ -3523,7 +3599,7 @@ export class AUNChannel {
3523
3599
  if (ids && ids.size === 0)
3524
3600
  this.interactionCardMessageIds.delete(interactionId);
3525
3601
  }
3526
- registerCardPostSend(messageId, action, channelId, delivery) {
3602
+ registerCardPostSend(messageId, action, channelId, delivery, replyContext) {
3527
3603
  if (action.type !== 'register_interaction_card')
3528
3604
  return;
3529
3605
  this.cardMessageIdMap.set(messageId, {
@@ -3532,6 +3608,7 @@ export class AUNChannel {
3532
3608
  initiatorAid: action.initiatorAid,
3533
3609
  channelId,
3534
3610
  delivery: delivery ?? action.delivery,
3611
+ replyContext,
3535
3612
  });
3536
3613
  this.ownedCardMsgIds.add(messageId);
3537
3614
  let messageIds = this.interactionCardMessageIds.get(action.requestId);
@@ -3564,7 +3641,7 @@ export class AUNChannel {
3564
3641
  if (!entry.postSend)
3565
3642
  return;
3566
3643
  if (entry.postSend.type === 'register_interaction_card') {
3567
- this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery);
3644
+ this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery, entry.context);
3568
3645
  }
3569
3646
  }
3570
3647
  async sendAunPayload(channelId, payload, context, label) {
@@ -3893,6 +3970,9 @@ export class AUNChannel {
3893
3970
  }
3894
3971
  async sendReliableStructured(channelId, payload, context, logText) {
3895
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,
3896
3976
  contentKind: 'custom',
3897
3977
  context,
3898
3978
  logText: logText ?? this.payloadLogText(payload, 'custom'),
@@ -3903,6 +3983,7 @@ export class AUNChannel {
3903
3983
  outboxId: result.outboxId,
3904
3984
  });
3905
3985
  }
3986
+ return result;
3906
3987
  }
3907
3988
  async sendMessage(channelId, text, context) {
3908
3989
  if (!text?.trim()) {
@@ -4314,7 +4395,7 @@ export class AUNChannel {
4314
4395
  logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
4315
4396
  return sentOutboxResult(completedMessageId);
4316
4397
  }
4317
- if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
4398
+ if (isPostBootstrapWelcomeOperationId(operationId, this.config.aid)) {
4318
4399
  const agentConfig = loadAgent(this.config.aid);
4319
4400
  if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
4320
4401
  return { status: 'retry', error: 'post-bootstrap welcome is waiting for active lifecycle', code: 'BOOTSTRAP_NOT_ACTIVE' };
@@ -5287,15 +5368,19 @@ export class AUNChannel {
5287
5368
  /** Repair bootstrap outbox entries to the configured personal Owner route. */
5288
5369
  repairBootstrapRoutes() {
5289
5370
  const aid = this.config.aid.replace(/^@/, '');
5290
- const owner = getFirstStaticAgentOwner(aid);
5291
- if (!owner)
5292
- return;
5293
- const completionEntry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid), { includeTerminal: true });
5371
+ const owners = Array.from(new Set(loadAgent(aid)?.owners ?? []));
5294
5372
  const entries = [
5295
5373
  ...outbox.findByDedupePrefix(aid, bootstrapInitialMessageOperationPrefix(aid), { includeTerminal: true }),
5296
- ...(completionEntry ? [completionEntry] : []),
5374
+ ...findPostBootstrapWelcomeOutbox(aid, { includeTerminal: true }),
5297
5375
  ].filter((entry) => !!entry?.channelId);
5298
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;
5299
5384
  const delivery = { chatType: 'private' };
5300
5385
  const nestedDelivery = entry.context?.delivery;
5301
5386
  const routeAlreadyPrivate = entry.channelId === owner
@@ -5357,6 +5442,7 @@ export class AUNChannel {
5357
5442
  timeout: 'timeout',
5358
5443
  queued: 'queued',
5359
5444
  progress: 'progress',
5445
+ notice: 'notice',
5360
5446
  requires_action: 'requires_action',
5361
5447
  };
5362
5448
  const statusPayload = {
@@ -5416,6 +5502,16 @@ export class AUNChannel {
5416
5502
  logger.info(logTaskStatus);
5417
5503
  }
5418
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
+ }
5419
5515
  // SDK 0.3.0 E2EE requires payload to be an object
5420
5516
  let payloadObj;
5421
5517
  try {
@@ -5806,12 +5902,14 @@ export class AUNChannelPlugin {
5806
5902
  await channel.sendThought(channelId, envelope.taskId, aunPayload, replyCtx);
5807
5903
  }
5808
5904
  else {
5809
- const result = await channel.sendContentPayload(channelId, aunPayload, {
5810
- queue: 'activity',
5811
- contentKind: 'custom',
5812
- context: replyCtx,
5813
- logText: channel.activityLogText(item),
5814
- });
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;
5815
5913
  if (result.status === 'permanent' || result.status === 'failed')
5816
5914
  failedResult ??= result;
5817
5915
  else if (result.status === 'retry' || result.status === 'queued' || result.queued)
@@ -5855,6 +5953,9 @@ export class AUNChannelPlugin {
5855
5953
  case 'status.progress':
5856
5954
  channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
5857
5955
  return;
5956
+ case 'status.notice':
5957
+ channel.sendProcessingStatus(channelId, 'notice', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
5958
+ return;
5858
5959
  case 'status.requires_action':
5859
5960
  channel.sendProcessingStatus(channelId, 'requires_action', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
5860
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,