evolcore 0.0.20 → 0.0.22

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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. package/dist/config/aun-gateway-config.js +0 -2
@@ -11,7 +11,7 @@ import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
11
11
  import { resolvePaths, agentDir as agentDirPath, resolveRoot, channelStatePath } from '../paths.js';
12
12
  import { saveToUploads, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
13
13
  import { appendAidEvent } from '../utils/instance-registry.js';
14
- import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, hasMessageLogOperation } from '../core/message/message-log.js';
14
+ import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, findMessageLogOperationMessageId, hasMessageLogOperation } from '../core/message/message-log.js';
15
15
  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';
@@ -28,22 +28,31 @@ 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
30
  import { getFirstStaticAgentOwner } from '../config/peer-role-resolver.js';
31
- import { isHClassPath } from '../core/protected-paths.js';
31
+ import { isHClassPath } from '../core/permission/protected-paths.js';
32
32
  import { consumeAunCausation, registerAunCausation } from '../core/causation/aun-association.js';
33
33
  import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
34
34
  import { recordCausationSpan } from '../core/causation/audit.js';
35
35
  import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
36
36
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
37
- import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationId, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
37
+ import { toolInputForDisplay } from '../utils/tool-summary.js';
38
+ import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
38
39
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
39
40
  import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
40
41
  import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
42
+ import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
41
43
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
42
44
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
43
45
  const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
44
46
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
45
47
  const AUN_ATTACHMENT_DOWNLOAD_ATTEMPTS = 3;
46
48
  const AUN_ATTACHMENT_RETRY_DELAYS_MS = [250, 750];
49
+ const OBSERVER_GROUP_MEMBERS_TTL_MS = 60_000;
50
+ const OBSERVER_GROUP_MEMBERS_PAGE_SIZE = 200;
51
+ const OBSERVER_GROUP_MEMBERS_MAX_PAGES = 100;
52
+ const AUN_SEND_LOG_TEXT_MAX_LENGTH = 60;
53
+ export function formatAunSendLogText(text) {
54
+ return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
55
+ }
47
56
  function attachmentDownloadHost(url) {
48
57
  try {
49
58
  return new URL(url).host || '<unknown>';
@@ -326,7 +335,7 @@ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
326
335
  const permanentHttpStatus = httpStatus !== undefined
327
336
  && httpStatus >= 400 && httpStatus < 500
328
337
  && !transientHttpStatus;
329
- const permanentByText = !relayTargetMissing && /(?:group|peer|agent|recipient|target|member|object|task|stream)\s*(?:id\s*)?(?:not found|does not exist|不存在)|group[_ -]?not[_ -]?found|not[_ -]?a[_ -]?member|(?:permission|access|role).*(?:denied|forbidden|拒绝|无权限)|(?:invalid|malformed|bad)\s*(?:argument|param|request)|unauthori[sz]ed|authentication failed|signature invalid/i.test(text);
338
+ const permanentByText = !relayTargetMissing && /\b(?:group|peer|agent|recipient|target|member|object|task|stream)(?:[\s_-]+(?:id|aid))?[\s_-]*(?:not[\s_-]*found|does[\s_-]*not[\s_-]*exist)|不存在|group[_ -]?not[_ -]?found|not[_ -]?a[_ -]?member|(?:permission|access|role).*(?:denied|forbidden|拒绝|无权限)|(?:invalid|malformed|bad)\s*(?:argument|param|request)|unauthori[sz]ed|authentication failed|signature invalid/i.test(text);
330
339
  const retryByText = /timeout|timed out|temporar|unavailable|overload|rate.?limit|too many requests|try again|not connected|connection|network|socket|econn|eai_again|etimedout|epipe|broken pipe|connection refused|connect timeout|fetch failed|dns|reset by peer|gateway service degraded|upstream/i.test(text);
331
340
  const acceptedDispatch = details.dispatchStatus !== undefined
332
341
  && AUN_ACCEPTED_DISPATCH_STATUSES.has(details.dispatchStatus);
@@ -397,8 +406,10 @@ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
397
406
  : {}),
398
407
  };
399
408
  }
400
- function sentOutboxResult() {
401
- return { status: 'sent' };
409
+ function sentOutboxResult(messageId) {
410
+ return messageId
411
+ ? { status: 'sent', messageId }
412
+ : { status: 'permanent', error: 'AUN send completed without a remote message_id', code: 'MISSING_MESSAGE_ID' };
402
413
  }
403
414
  function setIfDefined(target, key, value) {
404
415
  if (value !== undefined)
@@ -551,6 +562,35 @@ export class AUNChannel {
551
562
  * 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + daemon.log 失败日志)。
552
563
  * 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
553
564
  */
565
+ callClient(method, params) {
566
+ return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)));
567
+ }
568
+ withOutboundSendGate(method, run) {
569
+ if (!AUNChannel.OUTBOUND_SEND_METHODS.has(method))
570
+ return run();
571
+ return new Promise((resolve, reject) => {
572
+ this.outboundSendQueue.push({
573
+ run: run,
574
+ resolve: resolve,
575
+ reject,
576
+ });
577
+ this.pumpOutboundSendGate();
578
+ });
579
+ }
580
+ pumpOutboundSendGate() {
581
+ while (this.outboundSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY
582
+ && this.outboundSendQueue.length > 0) {
583
+ const job = this.outboundSendQueue.shift();
584
+ this.outboundSendActive++;
585
+ Promise.resolve()
586
+ .then(job.run)
587
+ .then(job.resolve, job.reject)
588
+ .finally(() => {
589
+ this.outboundSendActive--;
590
+ this.pumpOutboundSendGate();
591
+ });
592
+ }
593
+ }
554
594
  async callAndTrace(method, params, opts) {
555
595
  this.trace('OUT', method, params);
556
596
  // RPC 往返计时:区分「网关慢」与「本地队列堵塞」。message.send/group.send 的
@@ -560,7 +600,7 @@ export class AUNChannel {
560
600
  // SLOW_RPC_WARN_MS 需明显低于 SDK 默认 10s 超时,以便在真正超时前提前告警。
561
601
  const SLOW_RPC_WARN_MS = 3000;
562
602
  try {
563
- const result = await this.client.call(method, params);
603
+ const result = await this.callClient(method, params);
564
604
  const durationMs = Date.now() - rpcStart;
565
605
  if (!opts?.silentOk) {
566
606
  const r = result;
@@ -1026,6 +1066,18 @@ export class AUNChannel {
1026
1066
  .replace(/[ \t]+/g, ' ')
1027
1067
  .trim();
1028
1068
  }
1069
+ /** Normalize a command after a client-rendered friendly mention label. */
1070
+ normalizeSlashCommandText(text) {
1071
+ const trimmed = text.trim();
1072
+ const leadingLabels = trimmed.match(/^(?:@[^\s]+\s+)+(?=\/)/u);
1073
+ const commandText = leadingLabels
1074
+ ? trimmed.slice(leadingLabels[0].length).trimStart()
1075
+ : trimmed;
1076
+ // Keep the command contract consistent for mixed friendly/canonical @
1077
+ // tokens: selector labels are removed above, then known AID-shaped tokens
1078
+ // anywhere in the command are removed by the existing helper.
1079
+ return this.stripAllMentions(commandText);
1080
+ }
1029
1081
  parsePayloadMentionsOrReject(payload, context, messageId, seq) {
1030
1082
  const raw = payload && typeof payload === 'object' && !Array.isArray(payload)
1031
1083
  ? payload.mentions
@@ -1241,6 +1293,8 @@ export class AUNChannel {
1241
1293
  /** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
1242
1294
  invalidatedInteractions = new Map();
1243
1295
  mentionModeResolver;
1296
+ /** Agent-level policy controlling whether group slash commands require structured mentions. */
1297
+ groupSlashRequireMentionsResolver;
1244
1298
  static PROACTIVE_ALLOW_TYPES = new Set([
1245
1299
  'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
1246
1300
  'merge', 'link', 'location', 'personal_card',
@@ -1283,6 +1337,16 @@ export class AUNChannel {
1283
1337
  aidState;
1284
1338
  aidStatsCollector;
1285
1339
  outboxInFlight = new Set();
1340
+ /** Shared gateway-facing send limit for replies, activity, observer and thought messages. */
1341
+ static OUTBOUND_SEND_METHODS = new Set([
1342
+ 'message.send',
1343
+ 'group.send',
1344
+ 'message.thought.put',
1345
+ 'group.thought.put',
1346
+ ]);
1347
+ static OUTBOUND_SEND_CONCURRENCY = 4;
1348
+ outboundSendActive = 0;
1349
+ outboundSendQueue = [];
1286
1350
  constructor(config) {
1287
1351
  this.config = config;
1288
1352
  this.agentDir = agentDirPath(config.aid);
@@ -1355,6 +1419,14 @@ export class AUNChannel {
1355
1419
  }
1356
1420
  }
1357
1421
  async _initClientInner() {
1422
+ this.observerGroupMembers.clear();
1423
+ for (const groupId of new Set([
1424
+ ...this.observerGroupMembershipEpoch.keys(),
1425
+ ...this.observerGroupMemberFetches.keys(),
1426
+ ])) {
1427
+ this.observerGroupMembershipEpoch.set(groupId, (this.observerGroupMembershipEpoch.get(groupId) ?? 0) + 1);
1428
+ }
1429
+ this.observerGroupMemberFetches.clear();
1358
1430
  // Clean up existing client if any
1359
1431
  if (this.client) {
1360
1432
  this.trace('OUT', 'client.close', { reason: 'initClient' });
@@ -1423,6 +1495,15 @@ export class AUNChannel {
1423
1495
  logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
1424
1496
  this.handleIncomingGroupMessage(data);
1425
1497
  });
1498
+ client.on('group.changed', (data) => {
1499
+ if (!isCurrentClient())
1500
+ return;
1501
+ const record = data && typeof data === 'object' ? data : {};
1502
+ const envelope = record.envelope && typeof record.envelope === 'object'
1503
+ ? record.envelope
1504
+ : {};
1505
+ this.invalidateObserverGroupMembers(envelope.group_id ?? record.group_id);
1506
+ });
1426
1507
  }
1427
1508
  const handleSdkState = (source) => (data) => {
1428
1509
  if (!isCurrentClient())
@@ -1988,7 +2069,7 @@ export class AUNChannel {
1988
2069
  // 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
1989
2070
  // (即便日后 from-owner 排除规则调整,也不会泄漏)。
1990
2071
  if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
1991
- this.forwardInbound(msg);
2072
+ void this.forwardInbound(msg);
1992
2073
  }
1993
2074
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
1994
2075
  // 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
@@ -2159,7 +2240,7 @@ export class AUNChannel {
2159
2240
  }
2160
2241
  // Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
2161
2242
  // forwardInbound 内部排除 self-echo 与 from-owner。
2162
- this.forwardInbound(msg);
2243
+ void this.forwardInbound(msg);
2163
2244
  logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
2164
2245
  if (!groupId || !senderAid) {
2165
2246
  this.acknowledgeImmediately(messageId, seq);
@@ -2270,9 +2351,13 @@ export class AUNChannel {
2270
2351
  const firstLineGroup = text.split('\n')[0] || '';
2271
2352
  const hasEvolCoreTraceGroup = /\[EvolCore\.(receive|reply|agent)\]/.test(text);
2272
2353
  const isEchoMsg = /echo/i.test(firstLineGroup) && !hasEvolCoreTraceGroup;
2354
+ const slashCommandText = this.normalizeSlashCommandText(text);
2273
2355
  // 命令判定:剥离所有 @ 后看是否 / 开头(多 @ 场景如 @a @b /status 也能正确识别)。
2274
2356
  // echo 消息走独立的 trace 流程,不参与命令语义判定。
2275
- const isCommandMsg = !isEchoMsg && this.stripAllMentions(text).startsWith('/');
2357
+ const isCommandMsg = !isEchoMsg && slashCommandText.startsWith('/');
2358
+ const groupSlashRequireMentions = this.groupSlashRequireMentionsResolver
2359
+ ? await this.groupSlashRequireMentionsResolver().catch(() => undefined) ?? true
2360
+ : true;
2276
2361
  if (isEchoMsg) {
2277
2362
  // 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
2278
2363
  // >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
@@ -2299,9 +2384,10 @@ export class AUNChannel {
2299
2384
  }
2300
2385
  else {
2301
2386
  // 非 echo 消息:mention 标记(不再过滤,交给响应模式决定)
2302
- // slash 命令在任何 mentionMode 下都强制走 mention-only 语义
2303
- // 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
2304
- const enforceMention = mentionMode === 'mention-only' || isCommandMsg;
2387
+ // slash 命令默认要求结构化 @;Agent 可通过
2388
+ // groupSlashRequireMentions=false 选择仅按正文命令格式处理。
2389
+ const enforceMention = (mentionMode === 'mention-only' && !isCommandMsg)
2390
+ || (isCommandMsg && groupSlashRequireMentions);
2305
2391
  const isMentioned = mentionedSelf || mentionedAll;
2306
2392
  // 过滤逻辑下移到响应层:这里只标记,不过滤
2307
2393
  // 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
@@ -2309,14 +2395,14 @@ export class AUNChannel {
2309
2395
  if (enforceMention && !isMentioned && !isOwnHandoff) {
2310
2396
  this.acknowledgeImmediately(messageId, seq);
2311
2397
  this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
2312
- logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
2398
+ logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
2313
2399
  return;
2314
2400
  }
2315
2401
  }
2316
2402
  // 命令消息:剥离所有 @(多 agent 被 @ 时各自拿到干净的 /status 各自执行);
2317
2403
  // 普通消息:仅在唯一 @ 是自己时剥离,保留其他 @ 供 agent 感知。
2318
2404
  const strippedText = isCommandMsg
2319
- ? this.stripAllMentions(text)
2405
+ ? slashCommandText
2320
2406
  : this.stripSelfMentionIfOnly(text, this._aid);
2321
2407
  // Detect attachments before the empty-text guard (顶层 + 嵌套)
2322
2408
  const rawAttachments = this.collectAllAttachments(payload);
@@ -2347,7 +2433,7 @@ export class AUNChannel {
2347
2433
  : mentionedSelf
2348
2434
  ? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
2349
2435
  : `${mentionMode}.no-mention`;
2350
- logger.info(`${this.logPrefix()} Group mention decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} mentionMode=${mentionMode} reason=${reason} structMentions=${JSON.stringify(payloadMentionEntries)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
2436
+ logger.info(`${this.logPrefix()} Group mention decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} reason=${reason} structMentions=${JSON.stringify(payloadMentionEntries)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
2351
2437
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
2352
2438
  if (payloadType === 'action_card_reply')
2353
2439
  return;
@@ -2518,6 +2604,9 @@ export class AUNChannel {
2518
2604
  // observable / owners 不在此处缓存——由 daemon 注入 resolver,从 EvolAgent 的
2519
2605
  // in-memory merged config(启动/重启/热重载时统一更新的唯一缓存)读取,避免重复缓存。
2520
2606
  observerConfigResolver;
2607
+ observerGroupMembers = new Map();
2608
+ observerGroupMemberFetches = new Map();
2609
+ observerGroupMembershipEpoch = new Map();
2521
2610
  /** 注入观察者配置读取器(daemon 侧从 EvolAgent merged config 读)。 */
2522
2611
  setObserverConfigResolver(fn) {
2523
2612
  this.observerConfigResolver = fn;
@@ -2536,23 +2625,129 @@ export class AUNChannel {
2536
2625
  * data 为 SDK message.received / group.message_created 回调的整个对象,
2537
2626
  * 不拆解、不重组——SDK 信封结构变化不影响此处。
2538
2627
  */
2628
+ async resolveObserverGroupMembers(groupId) {
2629
+ if (!groupId || !this.client)
2630
+ return undefined;
2631
+ const cached = this.observerGroupMembers.get(groupId);
2632
+ if (cached && cached.expiresAt > Date.now())
2633
+ return cached.aids;
2634
+ const pending = this.observerGroupMemberFetches.get(groupId);
2635
+ if (pending)
2636
+ return pending;
2637
+ const epoch = this.observerGroupMembershipEpoch.get(groupId) ?? 0;
2638
+ let request;
2639
+ request = (async () => {
2640
+ try {
2641
+ const result = await this.callAndTrace('group.get_members', {
2642
+ group_id: groupId,
2643
+ page: 1,
2644
+ size: OBSERVER_GROUP_MEMBERS_PAGE_SIZE,
2645
+ });
2646
+ const rawMembers = Array.isArray(result?.members)
2647
+ ? result.members
2648
+ : Array.isArray(result?.items) ? result.items : undefined;
2649
+ if (!rawMembers)
2650
+ return undefined;
2651
+ const hasTotal = Number.isFinite(Number(result?.total));
2652
+ const hasMore = result?.has_more === true || result?.hasMore === true;
2653
+ if (!hasTotal && (hasMore || rawMembers.length >= OBSERVER_GROUP_MEMBERS_PAGE_SIZE))
2654
+ return undefined;
2655
+ const allMembers = [...rawMembers];
2656
+ const total = Number(result?.total);
2657
+ const pageSize = Number(result?.size) > 0 ? Number(result.size) : OBSERVER_GROUP_MEMBERS_PAGE_SIZE;
2658
+ const pageCount = Number.isFinite(total) && total > allMembers.length
2659
+ ? Math.ceil(total / pageSize)
2660
+ : 1;
2661
+ if (pageCount > OBSERVER_GROUP_MEMBERS_MAX_PAGES)
2662
+ return undefined;
2663
+ for (let page = 2; page <= pageCount; page++) {
2664
+ const next = await this.callAndTrace('group.get_members', {
2665
+ group_id: groupId,
2666
+ page,
2667
+ size: OBSERVER_GROUP_MEMBERS_PAGE_SIZE,
2668
+ });
2669
+ const nextMembers = Array.isArray(next?.members)
2670
+ ? next.members
2671
+ : Array.isArray(next?.items) ? next.items : undefined;
2672
+ if (!nextMembers)
2673
+ return undefined;
2674
+ allMembers.push(...nextMembers);
2675
+ }
2676
+ if (hasTotal && total > allMembers.length)
2677
+ return undefined;
2678
+ const aids = new Set();
2679
+ for (const member of allMembers) {
2680
+ const aid = typeof member === 'string'
2681
+ ? member.trim()
2682
+ : typeof member?.aid === 'string' ? member.aid.trim()
2683
+ : typeof member?.member_aid === 'string' ? member.member_aid.trim() : '';
2684
+ if (aid)
2685
+ aids.add(aid);
2686
+ }
2687
+ if ((this.observerGroupMembershipEpoch.get(groupId) ?? 0) === epoch) {
2688
+ this.observerGroupMembers.set(groupId, { aids, expiresAt: Date.now() + OBSERVER_GROUP_MEMBERS_TTL_MS });
2689
+ }
2690
+ return aids;
2691
+ }
2692
+ catch (error) {
2693
+ logger.debug(`${this.logPrefix()} observer group membership lookup failed group=${groupId}: ${error}`);
2694
+ return undefined;
2695
+ }
2696
+ finally {
2697
+ if (this.observerGroupMemberFetches.get(groupId) === request) {
2698
+ this.observerGroupMemberFetches.delete(groupId);
2699
+ }
2700
+ }
2701
+ })();
2702
+ this.observerGroupMemberFetches.set(groupId, request);
2703
+ return request;
2704
+ }
2705
+ invalidateObserverGroupMembers(groupId) {
2706
+ if (typeof groupId !== 'string' || !groupId.trim())
2707
+ return;
2708
+ const key = groupId.trim();
2709
+ this.observerGroupMembers.delete(key);
2710
+ this.observerGroupMembershipEpoch.set(key, (this.observerGroupMembershipEpoch.get(key) ?? 0) + 1);
2711
+ }
2712
+ async observerRecipients(owners, envelope) {
2713
+ if (owners.length === 0)
2714
+ return owners;
2715
+ const groupId = typeof envelope.group_id === 'string' ? envelope.group_id.trim() : '';
2716
+ const members = groupId ? await this.resolveObserverGroupMembers(groupId) : undefined;
2717
+ // Unknown membership is fail-open for observation: only an explicit member
2718
+ // response may suppress the redundant owner DM.
2719
+ return owners.filter(owner => !(members?.has(owner)));
2720
+ }
2539
2721
  forwardInbound(data) {
2540
2722
  if (!this.connected || !this.client)
2541
- return;
2723
+ return Promise.resolve();
2542
2724
  if (payloadTypeForObserver(data)?.startsWith('menu.'))
2543
- return;
2725
+ return Promise.resolve();
2544
2726
  const { observable, owners } = this.getObserverConfig();
2545
2727
  if (!observable || owners.length === 0)
2546
- return;
2547
- const env = (data?.envelope && typeof data.envelope === 'object') ? data.envelope : {};
2728
+ return Promise.resolve();
2729
+ const env = (data?.envelope && typeof data.envelope === 'object')
2730
+ ? { ...data.envelope }
2731
+ : {};
2732
+ if (!env.group_id && typeof data.group_id === 'string')
2733
+ env.group_id = data.group_id;
2548
2734
  const from = env.from ?? '';
2549
2735
  if (this._aid && from === this._aid)
2550
- return; // self-echo:已在出站转过
2736
+ return Promise.resolve(); // self-echo:已在出站转过
2551
2737
  // 排除来源 owner(不把"owner A 发来的"再转回 A),但仍转给其他 owner。
2552
- const recipientOwners = owners.filter(o => o !== from);
2553
- if (recipientOwners.length === 0)
2554
- return;
2555
- this.emitForward('inbound', data, recipientOwners);
2738
+ const candidateOwners = owners.filter(o => o !== from);
2739
+ if (candidateOwners.length === 0)
2740
+ return Promise.resolve();
2741
+ if (typeof env.group_id !== 'string' || !env.group_id.trim()) {
2742
+ this.emitForward('inbound', data, candidateOwners);
2743
+ return Promise.resolve();
2744
+ }
2745
+ return this.observerRecipients(candidateOwners, env)
2746
+ .then(recipientOwners => {
2747
+ if (recipientOwners.length > 0)
2748
+ this.emitForward('inbound', data, recipientOwners);
2749
+ })
2750
+ .catch(error => logger.debug(`${this.logPrefix()} observer inbound recipient lookup failed: ${error}`));
2556
2751
  }
2557
2752
  /**
2558
2753
  * 出站转发:Agent 经 AUN 真实发出的消息原样转发给 owner。
@@ -2561,20 +2756,33 @@ export class AUNChannel {
2561
2756
  */
2562
2757
  forwardOutbound(result) {
2563
2758
  if (!this.connected || !this.client)
2564
- return;
2759
+ return Promise.resolve();
2565
2760
  if (payloadTypeForObserver(result)?.startsWith('menu.'))
2566
- return;
2761
+ return Promise.resolve();
2567
2762
  const { observable, owners } = this.getObserverConfig();
2568
2763
  if (!observable || owners.length === 0)
2569
- return;
2570
- const env = (result?.envelope && typeof result.envelope === 'object') ? result.envelope : {};
2764
+ return Promise.resolve();
2765
+ const env = (result?.envelope && typeof result.envelope === 'object')
2766
+ ? { ...result.envelope }
2767
+ : {};
2768
+ if (!env.group_id && typeof result.group_id === 'string')
2769
+ env.group_id = result.group_id;
2571
2770
  const to = env.to ?? env.group_id ?? '';
2572
2771
  // 过滤:若对端本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
2573
2772
  // 但仍转发给其他 owner。
2574
- const recipientOwners = owners.filter(o => o !== to);
2575
- if (recipientOwners.length === 0)
2576
- return;
2577
- this.emitForward('outbound', result, recipientOwners);
2773
+ const candidateOwners = owners.filter(o => o !== to);
2774
+ if (candidateOwners.length === 0)
2775
+ return Promise.resolve();
2776
+ if (typeof env.group_id !== 'string' || !env.group_id.trim()) {
2777
+ this.emitForward('outbound', result, candidateOwners);
2778
+ return Promise.resolve();
2779
+ }
2780
+ return this.observerRecipients(candidateOwners, env)
2781
+ .then(recipientOwners => {
2782
+ if (recipientOwners.length > 0)
2783
+ this.emitForward('outbound', result, recipientOwners);
2784
+ })
2785
+ .catch(error => logger.debug(`${this.logPrefix()} observer outbound recipient lookup failed: ${error}`));
2578
2786
  }
2579
2787
  /**
2580
2788
  * 实际投递 observer.forward 给每个 owner,使用 daemon 的全局 AUN 默认加密设置。
@@ -3058,6 +3266,9 @@ export class AUNChannel {
3058
3266
  setMentionModeResolver(resolver) {
3059
3267
  this.mentionModeResolver = resolver;
3060
3268
  }
3269
+ setGroupSlashRequireMentionsResolver(resolver) {
3270
+ this.groupSlashRequireMentionsResolver = resolver;
3271
+ }
3061
3272
  onRecall(handler) {
3062
3273
  this.recallHandler = handler;
3063
3274
  }
@@ -3083,7 +3294,11 @@ export class AUNChannel {
3083
3294
  }
3084
3295
  }
3085
3296
  removeDeliveredOutboxEntry(entry) {
3086
- if (!outbox.removeIfRouteMatches(this.config.aid, entry)) {
3297
+ const messageId = entry.deliveryReceipt?.messageId;
3298
+ const finalized = messageId
3299
+ ? outbox.markDelivered(this.config.aid, entry.id, messageId, entry)
3300
+ : outbox.removeIfRouteMatches(this.config.aid, entry);
3301
+ if (!finalized) {
3087
3302
  logger.warn(`${this.logPrefix()} Preserved outbox entry whose route changed while delivery was in flight: id=${entry.id} submittedChannel=${entry.channelId}`);
3088
3303
  }
3089
3304
  }
@@ -3115,7 +3330,7 @@ export class AUNChannel {
3115
3330
  return `aun_status=${receipt.status ?? 'unknown'} seq=${receipt.seq ?? 'unknown'} delivery_mode=${receipt.deliveryMode ?? 'unknown'} gateway_timestamp=${receipt.timestamp ?? 'unknown'}`;
3116
3331
  }
3117
3332
  logAunSendAccepted(method, target, messageId, encrypt, result, text) {
3118
- const preview = text === undefined ? '' : ` text=${text.slice(0, 60)}`;
3333
+ const preview = text === undefined ? '' : ` text=${formatAunSendLogText(text)}`;
3119
3334
  logger.info(`${this.logPrefix()} ${method} accepted by AUN: target=${target} mid=${messageId} encrypt=${encrypt} ${this.receiptLogFields(result)}${preview}`);
3120
3335
  }
3121
3336
  stripUndefinedDeep(value) {
@@ -3326,8 +3541,8 @@ export class AUNChannel {
3326
3541
  }
3327
3542
  messageIds.add(messageId);
3328
3543
  const now = Date.now();
3329
- const mapTtl = action.expiresAt && action.expiresAt > now
3330
- ? action.expiresAt - now
3544
+ const mapTtl = typeof action.expiresAt === 'number' && Number.isFinite(action.expiresAt)
3545
+ ? Math.max(0, action.expiresAt - now)
3331
3546
  : AUN_INTERACTION_CARD_TTL_MS;
3332
3547
  const mapTimer = setTimeout(() => {
3333
3548
  this.cardMessageIdMap.delete(messageId);
@@ -3390,7 +3605,7 @@ export class AUNChannel {
3390
3605
  params.to = targetAid;
3391
3606
  const callOnce = async (sendParams, fallback) => {
3392
3607
  const result = fallback
3393
- ? await this.client.call(method, sendParams)
3608
+ ? await this.callClient(method, sendParams)
3394
3609
  : await this.callAndTrace(method, sendParams);
3395
3610
  const mid = this.messageIdFromSendResult(result);
3396
3611
  if (!mid) {
@@ -3479,39 +3694,81 @@ export class AUNChannel {
3479
3694
  // turn malformed `mentions: [undefined]` into an apparently valid `[]`.
3480
3695
  const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, context));
3481
3696
  const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
3482
- const entry = outbox.enqueue(this.config.aid, {
3483
- channelId,
3484
- delivery,
3485
- type: 'payload',
3486
- contentKind: opts.contentKind,
3487
- payload: finalPayload,
3488
- context,
3489
- logText,
3490
- ttl: opts.ttl,
3491
- postSend: opts.postSend,
3492
- });
3493
- logger.debug(`${this.logPrefix()} Outbox enqueued payload: id=${entry.id} kind=${opts.contentKind} channel=${channelId} text=${logText.slice(0, 40)}`);
3697
+ const expiresAt = opts.postSend?.type === 'register_interaction_card'
3698
+ ? opts.postSend.expiresAt
3699
+ : undefined;
3700
+ if (typeof expiresAt === 'number' && Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
3701
+ return {
3702
+ status: 'permanent',
3703
+ error: 'interaction card has expired',
3704
+ code: 'INTERACTION_EXPIRED',
3705
+ };
3706
+ }
3707
+ const remainingTtl = typeof expiresAt === 'number' && Number.isFinite(expiresAt)
3708
+ ? Math.max(0, expiresAt - Date.now())
3709
+ : undefined;
3710
+ const requestedTtl = opts.ttl ?? outbox.defaultTtl(opts.queue);
3711
+ const ttl = remainingTtl === undefined ? opts.ttl : Math.min(requestedTtl, remainingTtl);
3712
+ let entry;
3713
+ try {
3714
+ entry = outbox.enqueue(this.config.aid, {
3715
+ queue: opts.queue,
3716
+ channelId,
3717
+ delivery,
3718
+ type: 'payload',
3719
+ contentKind: opts.contentKind,
3720
+ payload: finalPayload,
3721
+ context,
3722
+ logText,
3723
+ ttl,
3724
+ postSend: opts.postSend,
3725
+ });
3726
+ }
3727
+ catch (error) {
3728
+ // Queue saturation is a caller-visible failure. Keep route/schema
3729
+ // validation errors as exceptions so their existing fail-closed path is
3730
+ // preserved, but expose OUTBOX_FULL as a structured send result.
3731
+ if (error?.code !== 'OUTBOX_FULL')
3732
+ throw error;
3733
+ return {
3734
+ status: 'permanent',
3735
+ error: error instanceof Error ? error.message : String(error),
3736
+ code: 'OUTBOX_FULL',
3737
+ };
3738
+ }
3739
+ logger.debug(`${this.logPrefix()} Outbox enqueued payload: id=${entry.id} queue=${opts.queue ?? 'default'} kind=${opts.contentKind} channel=${channelId} text=${logText.slice(0, 40)}`);
3494
3740
  if (!this.connected || !this.client) {
3495
3741
  logger.warn(`${this.logPrefix()} Not connected, payload queued in outbox (id=${entry.id}, kind=${opts.contentKind}). Triggering reconnect.`);
3496
3742
  if (!this.reconnectTimer && !this.client) {
3497
3743
  this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendContentPayload failed: ${e}`));
3498
3744
  }
3499
- return { queued: true };
3745
+ return {
3746
+ queued: true,
3747
+ outboxId: entry.id,
3748
+ error: 'AUN channel is not connected',
3749
+ code: 'AUN_NOT_CONNECTED',
3750
+ };
3500
3751
  }
3501
3752
  const result = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
3502
3753
  if (result.ok) {
3503
3754
  this.removeDeliveredOutboxEntry(entry);
3504
3755
  return { messageId: result.messageId };
3505
3756
  }
3506
- if (result.status === 'permanent') {
3757
+ if (result.status === 'permanent' || result.status === 'failed') {
3507
3758
  this.markPermanentOutboxFailure(entry, result);
3508
3759
  return {
3509
3760
  status: 'permanent',
3761
+ outboxId: entry.id,
3510
3762
  ...(result.error !== undefined ? { error: result.error } : {}),
3511
3763
  ...(result.code !== undefined ? { code: result.code } : {}),
3512
3764
  };
3513
3765
  }
3514
- return { queued: true };
3766
+ return {
3767
+ queued: true,
3768
+ outboxId: entry.id,
3769
+ ...(result.error !== undefined ? { error: result.error } : {}),
3770
+ ...(result.code !== undefined ? { code: result.code } : {}),
3771
+ };
3515
3772
  }
3516
3773
  buildTaskPayloadBase(envelope, context) {
3517
3774
  const base = {};
@@ -3619,9 +3876,15 @@ export class AUNChannel {
3619
3876
  return truncated;
3620
3877
  }
3621
3878
  buildActivityPayload(envelope, context, item) {
3622
- const activityItem = item && typeof item === 'object' && !Array.isArray(item)
3879
+ const rawItem = item && typeof item === 'object' && !Array.isArray(item)
3623
3880
  ? { ...item }
3624
3881
  : { kind: 'unknown', text: String(item ?? '') };
3882
+ const displayArguments = rawItem.kind === 'tool_call'
3883
+ ? toolInputForDisplay(rawItem.name, rawItem.arguments)
3884
+ : undefined;
3885
+ const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
3886
+ ? { ...rawItem, arguments: displayArguments }
3887
+ : rawItem;
3625
3888
  return {
3626
3889
  ...this.buildTaskPayloadBase(envelope, context),
3627
3890
  type: 'activity',
@@ -3629,16 +3892,22 @@ export class AUNChannel {
3629
3892
  };
3630
3893
  }
3631
3894
  async sendReliableStructured(channelId, payload, context, logText) {
3632
- await this.sendContentPayload(channelId, payload, {
3895
+ const result = await this.sendContentPayload(channelId, payload, {
3633
3896
  contentKind: 'custom',
3634
3897
  context,
3635
3898
  logText: logText ?? this.payloadLogText(payload, 'custom'),
3636
3899
  });
3900
+ if (result.status === 'permanent' || result.status === 'failed') {
3901
+ throw Object.assign(new Error(result.error ?? 'AUN structured send failed'), {
3902
+ code: result.code ?? 'AUN_SEND_FAILED',
3903
+ outboxId: result.outboxId,
3904
+ });
3905
+ }
3637
3906
  }
3638
3907
  async sendMessage(channelId, text, context) {
3639
3908
  if (!text?.trim()) {
3640
3909
  logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
3641
- return;
3910
+ return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
3642
3911
  }
3643
3912
  const delivery = this.requireDelivery(channelId, context);
3644
3913
  const routedContext = this.withDelivery(context, delivery);
@@ -3676,13 +3945,14 @@ export class AUNChannel {
3676
3945
  : undefined;
3677
3946
  if (operationId) {
3678
3947
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
3679
- if (hasMessageLogOperation(chatDir, operationId)) {
3948
+ const completedMessageId = findMessageLogOperationMessageId(chatDir, operationId);
3949
+ if (completedMessageId) {
3680
3950
  const duplicate = outbox.findByDedupeKey(this.config.aid, operationId);
3681
3951
  if (duplicate) {
3682
3952
  outbox.remove(this.config.aid, duplicate.id);
3683
3953
  logger.info(`${this.logPrefix()} Removed stale outbox entry for completed operation: ${operationId}`);
3684
3954
  }
3685
- return;
3955
+ return { status: 'sent', messageId: completedMessageId };
3686
3956
  }
3687
3957
  }
3688
3958
  // Write-ahead: persist to outbox before attempting send
@@ -3698,9 +3968,19 @@ export class AUNChannel {
3698
3968
  ? routedContext.metadata.outboxTtl
3699
3969
  : undefined,
3700
3970
  });
3971
+ if (entry.deliveryResult?.messageId || entry.deliveryReceipt?.messageId) {
3972
+ const messageId = entry.deliveryResult?.messageId ?? entry.deliveryReceipt.messageId;
3973
+ logger.info(`${this.logPrefix()} Reusing completed durable operation: operation=${operationId ?? '<none>'} entry=${entry.id} mid=${messageId}`);
3974
+ return { status: 'sent', messageId };
3975
+ }
3701
3976
  if (entry.terminal) {
3702
3977
  logger.warn(`${this.logPrefix()} Skipping previously terminated durable operation: operation=${operationId ?? '<none>'} entry=${entry.id} code=${entry.lastErrorCode ?? 'unknown'}`);
3703
- return;
3978
+ return {
3979
+ status: 'failed',
3980
+ outboxId: entry.id,
3981
+ error: entry.lastError ?? entry.terminal.error,
3982
+ ...(entry.lastErrorCode !== undefined ? { code: entry.lastErrorCode } : {}),
3983
+ };
3704
3984
  }
3705
3985
  logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
3706
3986
  // 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
@@ -3713,16 +3993,40 @@ export class AUNChannel {
3713
3993
  if (!this.reconnectTimer && !this.client) {
3714
3994
  this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendMessage failed: ${e}`));
3715
3995
  }
3716
- return;
3996
+ return { status: 'queued', outboxId: entry.id, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
3717
3997
  }
3718
3998
  // Attempt immediate delivery
3719
3999
  const result = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
3720
4000
  if (result.status === 'sent') {
4001
+ const messageId = result.messageId ?? entry.deliveryReceipt?.messageId;
4002
+ if (!messageId) {
4003
+ const error = 'AUN send completed without a remote message_id';
4004
+ this.markPermanentOutboxFailure(entry, { error, code: 'MISSING_MESSAGE_ID' });
4005
+ return { status: 'failed', outboxId: entry.id, error, code: 'MISSING_MESSAGE_ID' };
4006
+ }
3721
4007
  this.removeDeliveredOutboxEntry(entry);
4008
+ return { status: 'sent', messageId };
3722
4009
  }
3723
4010
  else if (result.status === 'permanent') {
3724
4011
  this.markPermanentOutboxFailure(entry, result);
4012
+ return {
4013
+ status: 'failed',
4014
+ outboxId: entry.id,
4015
+ error: result.error ?? 'permanent AUN send failure',
4016
+ ...(result.code !== undefined ? { code: result.code } : {}),
4017
+ };
3725
4018
  }
4019
+ // A gateway receipt may already have been checkpointed even if local
4020
+ // post-send bookkeeping needs a retry. Remote acceptance still wins.
4021
+ if (entry.deliveryReceipt?.messageId) {
4022
+ return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
4023
+ }
4024
+ return {
4025
+ status: 'queued',
4026
+ outboxId: entry.id,
4027
+ ...(result.error !== undefined ? { error: result.error } : {}),
4028
+ ...(result.code !== undefined ? { code: result.code } : {}),
4029
+ };
3726
4030
  }
3727
4031
  /** Daemon-side transport for `ec msg send` running inside an agent task. */
3728
4032
  async sendDaemonMsg(args) {
@@ -4005,9 +4309,10 @@ export class AUNChannel {
4005
4309
  || (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
4006
4310
  if (operationId) {
4007
4311
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
4008
- if (hasMessageLogOperation(chatDir, operationId)) {
4312
+ const completedMessageId = findMessageLogOperationMessageId(chatDir, operationId);
4313
+ if (completedMessageId) {
4009
4314
  logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
4010
- return sentOutboxResult();
4315
+ return sentOutboxResult(completedMessageId);
4011
4316
  }
4012
4317
  if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
4013
4318
  const agentConfig = loadAgent(this.config.aid);
@@ -4054,7 +4359,7 @@ export class AUNChannel {
4054
4359
  source,
4055
4360
  transport: entry.deliveryReceipt.transport,
4056
4361
  });
4057
- return sentOutboxResult();
4362
+ return sentOutboxResult(entry.deliveryReceipt.messageId);
4058
4363
  }
4059
4364
  const encryptTarget = isGroup ? channelId : targetAid;
4060
4365
  const encrypt = context?.metadata?.encrypted != null
@@ -4063,6 +4368,7 @@ export class AUNChannel {
4063
4368
  const params = { payload, encrypt };
4064
4369
  if (context?.metadata?.persistRequired === true)
4065
4370
  params.persist_required = true;
4371
+ let acceptedMessageId;
4066
4372
  try {
4067
4373
  if (isGroup) {
4068
4374
  params.group_id = channelId;
@@ -4082,6 +4388,7 @@ export class AUNChannel {
4082
4388
  return failure;
4083
4389
  }
4084
4390
  else {
4391
+ acceptedMessageId = mid;
4085
4392
  this.logAunSendAccepted('group.send', channelId, mid, encrypt, result, finalText);
4086
4393
  this.checkpointTextDelivery(entry, mid, encrypt, result);
4087
4394
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
@@ -4104,6 +4411,7 @@ export class AUNChannel {
4104
4411
  return classifyAunSendFailure(result, 'message.send returned no message_id');
4105
4412
  }
4106
4413
  else {
4414
+ acceptedMessageId = mid;
4107
4415
  this.logAunSendAccepted('message.send', this.peerLabel(targetAid), mid, encrypt, result, finalText);
4108
4416
  this.checkpointTextDelivery(entry, mid, encrypt, result);
4109
4417
  const causation = normalizeCausation(context?.metadata?.causation);
@@ -4124,7 +4432,7 @@ export class AUNChannel {
4124
4432
  this.forwardOutbound(result);
4125
4433
  }
4126
4434
  }
4127
- return sentOutboxResult();
4435
+ return sentOutboxResult(acceptedMessageId);
4128
4436
  }
4129
4437
  catch (e) {
4130
4438
  if (entry.deliveryReceipt) {
@@ -4139,7 +4447,7 @@ export class AUNChannel {
4139
4447
  try {
4140
4448
  if (isGroup) {
4141
4449
  this.trace('OUT', 'group.send.fallback', params);
4142
- const result = await this.client.call('group.send', params);
4450
+ const result = await this.callClient('group.send', params);
4143
4451
  const mid = this.messageIdFromSendResult(result);
4144
4452
  if (!mid) {
4145
4453
  const resultRecord = errorRecord(result);
@@ -4148,6 +4456,7 @@ export class AUNChannel {
4148
4456
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
4149
4457
  return classifyAunSendFailure(result, 'group.send plaintext fallback returned no message_id');
4150
4458
  }
4459
+ acceptedMessageId = mid;
4151
4460
  this.trace('OUT', 'group.send.fallback.ok', { message_id: mid });
4152
4461
  this.checkpointTextDelivery(entry, mid, false, result);
4153
4462
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
@@ -4161,13 +4470,14 @@ export class AUNChannel {
4161
4470
  }
4162
4471
  else {
4163
4472
  this.trace('OUT', 'message.send.fallback', params);
4164
- const result = await this.client.call('message.send', params);
4473
+ const result = await this.callClient('message.send', params);
4165
4474
  const mid = this.messageIdFromSendResult(result);
4166
4475
  if (!mid) {
4167
4476
  this.trace('OUT', 'message.send.fallback.missing_id', {});
4168
4477
  logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
4169
4478
  return classifyAunSendFailure(result, 'message.send plaintext fallback returned no message_id');
4170
4479
  }
4480
+ acceptedMessageId = mid;
4171
4481
  this.trace('OUT', 'message.send.fallback.ok', { message_id: mid });
4172
4482
  this.checkpointTextDelivery(entry, mid, false, result);
4173
4483
  const causation = normalizeCausation(context?.metadata?.causation);
@@ -4186,7 +4496,7 @@ export class AUNChannel {
4186
4496
  });
4187
4497
  this.forwardOutbound(result);
4188
4498
  }
4189
- return sentOutboxResult();
4499
+ return sentOutboxResult(acceptedMessageId);
4190
4500
  }
4191
4501
  catch (e2) {
4192
4502
  if (entry.deliveryReceipt) {
@@ -4214,6 +4524,7 @@ export class AUNChannel {
4214
4524
  id: entry.id,
4215
4525
  channelId: entry.channelId,
4216
4526
  delivery: entry.delivery,
4527
+ queue: entry.queue,
4217
4528
  };
4218
4529
  const receipt = {
4219
4530
  messageId,
@@ -4238,6 +4549,20 @@ export class AUNChannel {
4238
4549
  logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
4239
4550
  return { ok: true, status: 'sent' };
4240
4551
  }
4552
+ const interactionExpiresAt = entry.postSend?.type === 'register_interaction_card'
4553
+ ? entry.postSend.expiresAt
4554
+ : undefined;
4555
+ if (typeof interactionExpiresAt === 'number'
4556
+ && Number.isFinite(interactionExpiresAt)
4557
+ && interactionExpiresAt <= Date.now()) {
4558
+ logger.info(`${this.logPrefix()} Discarded expired interaction from durable outbox: request=${interactionId ?? '<unknown>'} entry=${entry.id}`);
4559
+ return {
4560
+ ok: false,
4561
+ status: 'permanent',
4562
+ error: 'interaction card has expired',
4563
+ code: 'INTERACTION_EXPIRED',
4564
+ };
4565
+ }
4241
4566
  const channelId = entry.channelId;
4242
4567
  const payload = entry.payload;
4243
4568
  if (!payload) {
@@ -4696,7 +5021,7 @@ export class AUNChannel {
4696
5021
  return { status: 'permanent', error: 'image outbox entry disappeared during upload', code: 'OUTBOX_ENTRY_MISSING' };
4697
5022
  }
4698
5023
  const sent = await this.deliverPayloadEntry(entry);
4699
- return sent.ok ? sentOutboxResult() : sent;
5024
+ return sent.ok ? sentOutboxResult(sent.messageId) : sent;
4700
5025
  }
4701
5026
  catch (error) {
4702
5027
  const failure = classifyAunSendFailure(error, 'image upload or send failed');
@@ -4728,7 +5053,7 @@ export class AUNChannel {
4728
5053
  source,
4729
5054
  transport: entry.deliveryReceipt.transport,
4730
5055
  });
4731
- return sentOutboxResult();
5056
+ return sentOutboxResult(entry.deliveryReceipt.messageId);
4732
5057
  }
4733
5058
  catch (error) {
4734
5059
  const detail = error instanceof Error ? error.message : String(error);
@@ -4805,7 +5130,7 @@ export class AUNChannel {
4805
5130
  if (isGroup) {
4806
5131
  params.group_id = delivery.groupId;
4807
5132
  this.trace('OUT', 'group.send.file', params);
4808
- const result = await this.client.call('group.send', params);
5133
+ const result = await this.callClient('group.send', params);
4809
5134
  sendResult = result;
4810
5135
  const fileMid = this.messageIdFromSendResult(result);
4811
5136
  sentMid = fileMid ?? null;
@@ -4818,7 +5143,7 @@ export class AUNChannel {
4818
5143
  else {
4819
5144
  params.to = fileTargetAid;
4820
5145
  this.trace('OUT', 'message.send.file', params);
4821
- const result = await this.client.call('message.send', params);
5146
+ const result = await this.callClient('message.send', params);
4822
5147
  sendResult = result;
4823
5148
  sentMid = this.messageIdFromSendResult(result);
4824
5149
  this.trace('OUT', 'message.send.file.ok', { message_id: sentMid });
@@ -4839,7 +5164,7 @@ export class AUNChannel {
4839
5164
  params.encrypt = false;
4840
5165
  if (isGroup) {
4841
5166
  this.trace('OUT', 'group.send.file.fallback', params);
4842
- const result = await this.client.call('group.send', params);
5167
+ const result = await this.callClient('group.send', params);
4843
5168
  sendResult = result;
4844
5169
  const fbMid = this.messageIdFromSendResult(result);
4845
5170
  sentMid = fbMid ?? null;
@@ -4851,7 +5176,7 @@ export class AUNChannel {
4851
5176
  }
4852
5177
  else {
4853
5178
  this.trace('OUT', 'message.send.file.fallback', params);
4854
- const result = await this.client.call('message.send', params);
5179
+ const result = await this.callClient('message.send', params);
4855
5180
  sendResult = result;
4856
5181
  sentMid = this.messageIdFromSendResult(result);
4857
5182
  this.trace('OUT', 'message.send.file.fallback.ok', { message_id: sentMid });
@@ -4887,7 +5212,7 @@ export class AUNChannel {
4887
5212
  });
4888
5213
  if (sendResult)
4889
5214
  this.forwardOutbound(sendResult);
4890
- return sentOutboxResult();
5215
+ return sentOutboxResult(sentMid);
4891
5216
  }
4892
5217
  catch (e) {
4893
5218
  if (entry.deliveryReceipt) {
@@ -4907,7 +5232,7 @@ export class AUNChannel {
4907
5232
  if (this.outboxTimer)
4908
5233
  return;
4909
5234
  this.outboxTimer = setInterval(() => {
4910
- if (this.connected && this.client && outbox.hasPending(this.config.aid)) {
5235
+ if (this.connected && this.client && (outbox.hasPending(this.config.aid) || outbox.hasPending(this.config.aid, 'activity'))) {
4911
5236
  this.drainOutbox();
4912
5237
  }
4913
5238
  }, 30_000);
@@ -4922,36 +5247,42 @@ export class AUNChannel {
4922
5247
  if (!this.connected || !this.client)
4923
5248
  return;
4924
5249
  this.repairBootstrapRoutes();
4925
- if (!outbox.hasPending(this.config.aid))
4926
- return;
4927
- logger.info(`${this.logPrefix()} Draining outbox...`);
4928
- const result = await outbox.drain(this.config.aid, async (entry) => {
4929
- if (!isDeliveryTarget(entry.delivery)) {
4930
- logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
4931
- return {
4932
- status: 'permanent',
4933
- error: 'outbox entry has no valid delivery route',
4934
- code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
4935
- };
4936
- }
4937
- if (entry.type === 'text') {
4938
- return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
4939
- }
4940
- else if (entry.type === 'file') {
4941
- return this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
4942
- }
4943
- else if (entry.type === 'image') {
4944
- return this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
4945
- }
4946
- else if (entry.type === 'payload') {
4947
- const sent = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
4948
- return sent.ok ? { status: 'sent' } : sent;
5250
+ const drainQueue = async (queue) => {
5251
+ if (!outbox.hasPending(this.config.aid, queue))
5252
+ return;
5253
+ logger.info(`${this.logPrefix()} Draining ${queue} outbox...`);
5254
+ const result = await outbox.drain(this.config.aid, async (entry) => {
5255
+ if (!isDeliveryTarget(entry.delivery)) {
5256
+ logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
5257
+ return {
5258
+ status: 'permanent',
5259
+ error: 'outbox entry has no valid delivery route',
5260
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
5261
+ };
5262
+ }
5263
+ if (entry.type === 'text') {
5264
+ return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
5265
+ }
5266
+ else if (entry.type === 'file') {
5267
+ return this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
5268
+ }
5269
+ else if (entry.type === 'image') {
5270
+ return this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
5271
+ }
5272
+ else if (entry.type === 'payload') {
5273
+ const sent = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
5274
+ return sent.ok
5275
+ ? { status: 'sent', ...(sent.messageId ? { messageId: sent.messageId } : {}) }
5276
+ : sent;
5277
+ }
5278
+ return { status: 'permanent', error: `unsupported outbox entry type: ${entry.type}`, code: 'UNSUPPORTED_OUTBOX_TYPE' };
5279
+ }, queue);
5280
+ if (result.sent > 0 || result.expired > 0 || result.permanent) {
5281
+ logger.info(`${this.logPrefix()} ${queue} outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}`);
4949
5282
  }
4950
- return { status: 'permanent', error: `unsupported outbox entry type: ${entry.type}`, code: 'UNSUPPORTED_OUTBOX_TYPE' };
4951
- });
4952
- if (result.sent > 0 || result.expired > 0 || result.permanent) {
4953
- logger.info(`${this.logPrefix()} Outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}`);
4954
- }
5283
+ };
5284
+ await drainQueue('default');
5285
+ await drainQueue('activity');
4955
5286
  }
4956
5287
  /** Repair bootstrap outbox entries to the configured personal Owner route. */
4957
5288
  repairBootstrapRoutes() {
@@ -4959,13 +5290,11 @@ export class AUNChannel {
4959
5290
  const owner = getFirstStaticAgentOwner(aid);
4960
5291
  if (!owner)
4961
5292
  return;
4962
- const operationIds = new Set([
4963
- bootstrapInitialMessageOperationId(aid),
4964
- postBootstrapWelcomeOperationId(aid),
4965
- ]);
4966
- const entries = [...operationIds]
4967
- .map(operationId => outbox.findByDedupeKey(aid, operationId, { includeTerminal: true }))
4968
- .filter((entry) => !!entry?.channelId);
5293
+ const completionEntry = outbox.findByDedupeKey(aid, postBootstrapWelcomeOperationId(aid), { includeTerminal: true });
5294
+ const entries = [
5295
+ ...outbox.findByDedupePrefix(aid, bootstrapInitialMessageOperationPrefix(aid), { includeTerminal: true }),
5296
+ ...(completionEntry ? [completionEntry] : []),
5297
+ ].filter((entry) => !!entry?.channelId);
4969
5298
  for (const entry of entries) {
4970
5299
  const delivery = { chatType: 'private' };
4971
5300
  const nestedDelivery = entry.context?.delivery;
@@ -5325,6 +5654,13 @@ export class AUNChannelPlugin {
5325
5654
  const delivery = requireEnvelopeDelivery(channelId, envelope?.delivery, envelope?.replyContext?.delivery);
5326
5655
  const parentCausation = normalizeCausation(envelope.causation ?? envelope.replyContext?.metadata?.causation);
5327
5656
  const outboundCausation = parentCausation ? deriveCausation(parentCausation) : undefined;
5657
+ const operationId = envelope.operationId ?? envelope.taskId;
5658
+ // A task can emit multiple independent intermediate text chunks. Keep
5659
+ // the task operation ID in the receipt, but do not use it as the
5660
+ // durable-send dedupe key for each non-final chunk.
5661
+ const dedupeOperationId = payload.kind === 'result.text' && payload.isFinal === false
5662
+ ? undefined
5663
+ : operationId;
5328
5664
  const replyCtx = outboundCausation
5329
5665
  ? {
5330
5666
  ...(envelope.replyContext ?? {}),
@@ -5345,11 +5681,61 @@ export class AUNChannelPlugin {
5345
5681
  case 'result.text':
5346
5682
  case 'command.result':
5347
5683
  case 'command.error': {
5348
- const sendCtx = { ...(replyCtx ?? {}) };
5684
+ const sendCtx = {
5685
+ ...(replyCtx ?? {}),
5686
+ metadata: {
5687
+ ...Object.fromEntries(Object.entries(replyCtx?.metadata ?? {})
5688
+ .filter(([key]) => key !== 'operationId')),
5689
+ ...(dedupeOperationId ? { operationId: dedupeOperationId } : {}),
5690
+ },
5691
+ };
5349
5692
  if (payload.kind === 'result.text' && payload.isFinal)
5350
5693
  sendCtx.title = '✅ 最终回复:';
5351
- await channel.sendMessage(channelId, payload.text, sendCtx);
5352
- return;
5694
+ let result;
5695
+ try {
5696
+ result = await channel.sendMessage(channelId, payload.text, sendCtx);
5697
+ }
5698
+ catch (error) {
5699
+ if (error?.code !== 'OUTBOX_FULL')
5700
+ throw error;
5701
+ return {
5702
+ status: 'failed',
5703
+ operationId,
5704
+ messages: [],
5705
+ error: error instanceof Error ? error.message : String(error),
5706
+ code: 'OUTBOX_FULL',
5707
+ };
5708
+ }
5709
+ if (result.status === 'sent') {
5710
+ return {
5711
+ status: 'sent',
5712
+ operationId,
5713
+ messageId: result.messageId,
5714
+ messages: [{
5715
+ messageId: result.messageId,
5716
+ partIndex: 0,
5717
+ ...(sendCtx.threadId ? { threadId: sendCtx.threadId } : {}),
5718
+ }],
5719
+ };
5720
+ }
5721
+ if (result.status === 'queued') {
5722
+ return {
5723
+ status: 'queued',
5724
+ operationId,
5725
+ messages: [],
5726
+ outboxId: result.outboxId,
5727
+ ...(result.error !== undefined ? { error: result.error } : {}),
5728
+ ...(result.code !== undefined ? { code: result.code } : {}),
5729
+ };
5730
+ }
5731
+ return {
5732
+ status: 'failed',
5733
+ operationId,
5734
+ messages: [],
5735
+ ...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
5736
+ error: result.error,
5737
+ ...(result.code !== undefined ? { code: result.code } : {}),
5738
+ };
5353
5739
  }
5354
5740
  case 'system.notice': {
5355
5741
  const noticePayload = {
@@ -5402,6 +5788,9 @@ export class AUNChannelPlugin {
5402
5788
  }
5403
5789
  case 'activity.batch': {
5404
5790
  const items = Array.isArray(payload.items) ? payload.items : [];
5791
+ const messageIds = [];
5792
+ let queuedResult;
5793
+ let failedResult;
5405
5794
  for (const item of items) {
5406
5795
  if (item?.kind === 'progress') {
5407
5796
  const metadata = { activityType: 'progress' };
@@ -5417,10 +5806,51 @@ export class AUNChannelPlugin {
5417
5806
  await channel.sendThought(channelId, envelope.taskId, aunPayload, replyCtx);
5418
5807
  }
5419
5808
  else {
5420
- await channel.sendReliableStructured(channelId, aunPayload, replyCtx, channel.activityLogText(item));
5809
+ const result = await channel.sendContentPayload(channelId, aunPayload, {
5810
+ queue: 'activity',
5811
+ contentKind: 'custom',
5812
+ context: replyCtx,
5813
+ logText: channel.activityLogText(item),
5814
+ });
5815
+ if (result.status === 'permanent' || result.status === 'failed')
5816
+ failedResult ??= result;
5817
+ else if (result.status === 'retry' || result.status === 'queued' || result.queued)
5818
+ queuedResult ??= result;
5819
+ else if (result.messageId)
5820
+ messageIds.push(result.messageId);
5421
5821
  }
5422
5822
  }
5423
- return;
5823
+ if (failedResult) {
5824
+ return {
5825
+ status: 'failed',
5826
+ operationId,
5827
+ messages: [],
5828
+ ...(failedResult.outboxId !== undefined ? { outboxId: failedResult.outboxId } : {}),
5829
+ error: failedResult.error ?? 'AUN activity delivery failed',
5830
+ ...(failedResult.code !== undefined ? { code: failedResult.code } : {}),
5831
+ };
5832
+ }
5833
+ if (queuedResult) {
5834
+ return queuedResult.outboxId
5835
+ ? {
5836
+ status: 'queued',
5837
+ operationId,
5838
+ messages: [],
5839
+ outboxId: queuedResult.outboxId,
5840
+ ...(queuedResult.error !== undefined ? { error: queuedResult.error } : {}),
5841
+ ...(queuedResult.code !== undefined ? { code: queuedResult.code } : {}),
5842
+ }
5843
+ : {
5844
+ status: 'failed',
5845
+ operationId,
5846
+ messages: [],
5847
+ error: 'queued activity is missing its outbox id',
5848
+ code: 'MISSING_OUTBOX_ID',
5849
+ };
5850
+ }
5851
+ if (messageIds.length > 0)
5852
+ return sentReceipt(envelope, messageIds, replyCtx?.threadId);
5853
+ return suppressedReceipt(envelope, 'empty_activity');
5424
5854
  }
5425
5855
  case 'status.progress':
5426
5856
  channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
@@ -5449,6 +5879,44 @@ export class AUNChannelPlugin {
5449
5879
  case 'interaction': {
5450
5880
  const req = payload.interaction;
5451
5881
  const cardTtlMs = AUN_INTERACTION_CARD_TTL_MS;
5882
+ const cardExpiresAt = typeof req.expiresAt === 'number' && Number.isFinite(req.expiresAt)
5883
+ ? req.expiresAt
5884
+ : Date.now() + cardTtlMs;
5885
+ const toInteractionReceipt = (result) => {
5886
+ if ((result.status === undefined || result.status === 'sent') && result.messageId) {
5887
+ return sentReceipt(envelope, [result.messageId], replyCtx?.threadId);
5888
+ }
5889
+ if (result.status === 'retry' || result.status === 'queued' || result.queued) {
5890
+ if (!result.outboxId) {
5891
+ return {
5892
+ status: 'failed',
5893
+ operationId,
5894
+ messages: [],
5895
+ error: 'queued interaction is missing its outbox id',
5896
+ code: 'MISSING_OUTBOX_ID',
5897
+ };
5898
+ }
5899
+ return {
5900
+ status: 'queued',
5901
+ operationId,
5902
+ messages: [],
5903
+ outboxId: result.outboxId,
5904
+ ...(result.error !== undefined ? { error: result.error } : {}),
5905
+ ...(result.code !== undefined ? { code: result.code } : {}),
5906
+ };
5907
+ }
5908
+ if (result.status === 'permanent' || result.status === 'failed') {
5909
+ return {
5910
+ status: 'failed',
5911
+ operationId,
5912
+ messages: [],
5913
+ ...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
5914
+ error: result.error ?? 'AUN interaction delivery failed',
5915
+ ...(result.code !== undefined ? { code: result.code } : {}),
5916
+ };
5917
+ }
5918
+ return suppressedReceipt(envelope, 'interaction_not_sent');
5919
+ };
5452
5920
  if (req.kind.kind === 'action') {
5453
5921
  const action = req.kind;
5454
5922
  const aunCard = {
@@ -5472,7 +5940,7 @@ export class AUNChannelPlugin {
5472
5940
  aunCard.initiator = req.initiatorId;
5473
5941
  if (replyCtx?.threadId)
5474
5942
  aunCard.thread_id = replyCtx.threadId;
5475
- await channel.sendContentPayload(channelId, aunCard, {
5943
+ const result = await channel.sendContentPayload(channelId, aunCard, {
5476
5944
  contentKind: 'card',
5477
5945
  context: replyCtx,
5478
5946
  logText: action.title ? `[card] ${action.title}` : '[card]',
@@ -5482,9 +5950,10 @@ export class AUNChannelPlugin {
5482
5950
  isCommandCard: false,
5483
5951
  initiatorAid: req.initiatorId,
5484
5952
  delivery: replyCtx?.delivery,
5485
- expiresAt: Date.now() + cardTtlMs,
5953
+ expiresAt: cardExpiresAt,
5486
5954
  },
5487
5955
  });
5956
+ return toInteractionReceipt(result);
5488
5957
  }
5489
5958
  else if (req.kind.kind === 'command-card') {
5490
5959
  const card = req.kind;
@@ -5510,7 +5979,7 @@ export class AUNChannelPlugin {
5510
5979
  aunCard.initiator = req.initiatorId;
5511
5980
  if (replyCtx?.threadId)
5512
5981
  aunCard.thread_id = replyCtx.threadId;
5513
- await channel.sendContentPayload(channelId, aunCard, {
5982
+ const result = await channel.sendContentPayload(channelId, aunCard, {
5514
5983
  contentKind: 'card',
5515
5984
  context: replyCtx,
5516
5985
  logText: card.title ? `[card] ${card.title}` : '[card]',
@@ -5520,14 +5989,35 @@ export class AUNChannelPlugin {
5520
5989
  isCommandCard: true,
5521
5990
  initiatorAid: req.initiatorId,
5522
5991
  delivery: replyCtx?.delivery,
5523
- expiresAt: Date.now() + cardTtlMs,
5992
+ expiresAt: cardExpiresAt,
5524
5993
  },
5525
5994
  });
5995
+ return toInteractionReceipt(result);
5526
5996
  }
5527
5997
  else if (payload.fallbackText) {
5528
- await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
5998
+ const result = await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
5999
+ if (result.status === 'sent')
6000
+ return sentReceipt(envelope, [result.messageId], replyCtx?.threadId);
6001
+ if (result.status === 'queued') {
6002
+ return {
6003
+ status: 'queued',
6004
+ operationId,
6005
+ messages: [],
6006
+ outboxId: result.outboxId,
6007
+ ...(result.error !== undefined ? { error: result.error } : {}),
6008
+ ...(result.code !== undefined ? { code: result.code } : {}),
6009
+ };
6010
+ }
6011
+ return {
6012
+ status: 'failed',
6013
+ operationId,
6014
+ messages: [],
6015
+ ...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
6016
+ error: result.error,
6017
+ ...(result.code !== undefined ? { code: result.code } : {}),
6018
+ };
5529
6019
  }
5530
- return;
6020
+ return suppressedReceipt(envelope, 'empty_interaction');
5531
6021
  }
5532
6022
  case 'custom': {
5533
6023
  const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
@@ -5570,7 +6060,7 @@ export class AUNChannelPlugin {
5570
6060
  registerBridge(bridge, channelType) {
5571
6061
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
5572
6062
  handler(aunOptsToInbound(opts, adapter.channelName, channelType));
5573
- }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
6063
+ }), async (channelId, text, replyContext) => { await channel.sendMessage(channelId, text, replyContext); }, adapter, channelType);
5574
6064
  },
5575
6065
  registerHooks(hookCtx) {
5576
6066
  channel.setEventBus(hookCtx.eventBus);
@@ -5594,6 +6084,11 @@ export class AUNChannelPlugin {
5594
6084
  }, { cache: true }).mentionMode;
5595
6085
  });
5596
6086
  }
6087
+ if (typeof channel.setGroupSlashRequireMentionsResolver === 'function') {
6088
+ channel.setGroupSlashRequireMentionsResolver(async () => {
6089
+ return resolveEffective({ self: aid }, { cache: true }).groupSlashRequireMentions ?? true;
6090
+ });
6091
+ }
5597
6092
  },
5598
6093
  };
5599
6094
  }