evolcore 0.0.20 → 0.0.21

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 (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. 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,7 +28,7 @@ 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';
@@ -38,12 +38,17 @@ import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOu
38
38
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
39
39
  import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
40
40
  import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
41
+ import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
41
42
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
42
43
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
43
44
  const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
44
45
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
45
46
  const AUN_ATTACHMENT_DOWNLOAD_ATTEMPTS = 3;
46
47
  const AUN_ATTACHMENT_RETRY_DELAYS_MS = [250, 750];
48
+ const AUN_SEND_LOG_TEXT_MAX_LENGTH = 60;
49
+ export function formatAunSendLogText(text) {
50
+ return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
51
+ }
47
52
  function attachmentDownloadHost(url) {
48
53
  try {
49
54
  return new URL(url).host || '<unknown>';
@@ -326,7 +331,7 @@ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
326
331
  const permanentHttpStatus = httpStatus !== undefined
327
332
  && httpStatus >= 400 && httpStatus < 500
328
333
  && !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);
334
+ 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
335
  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
336
  const acceptedDispatch = details.dispatchStatus !== undefined
332
337
  && AUN_ACCEPTED_DISPATCH_STATUSES.has(details.dispatchStatus);
@@ -397,8 +402,10 @@ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
397
402
  : {}),
398
403
  };
399
404
  }
400
- function sentOutboxResult() {
401
- return { status: 'sent' };
405
+ function sentOutboxResult(messageId) {
406
+ return messageId
407
+ ? { status: 'sent', messageId }
408
+ : { status: 'permanent', error: 'AUN send completed without a remote message_id', code: 'MISSING_MESSAGE_ID' };
402
409
  }
403
410
  function setIfDefined(target, key, value) {
404
411
  if (value !== undefined)
@@ -551,6 +558,35 @@ export class AUNChannel {
551
558
  * 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + daemon.log 失败日志)。
552
559
  * 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
553
560
  */
561
+ callClient(method, params) {
562
+ return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)));
563
+ }
564
+ withOutboundSendGate(method, run) {
565
+ if (!AUNChannel.OUTBOUND_SEND_METHODS.has(method))
566
+ return run();
567
+ return new Promise((resolve, reject) => {
568
+ this.outboundSendQueue.push({
569
+ run: run,
570
+ resolve: resolve,
571
+ reject,
572
+ });
573
+ this.pumpOutboundSendGate();
574
+ });
575
+ }
576
+ pumpOutboundSendGate() {
577
+ while (this.outboundSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY
578
+ && this.outboundSendQueue.length > 0) {
579
+ const job = this.outboundSendQueue.shift();
580
+ this.outboundSendActive++;
581
+ Promise.resolve()
582
+ .then(job.run)
583
+ .then(job.resolve, job.reject)
584
+ .finally(() => {
585
+ this.outboundSendActive--;
586
+ this.pumpOutboundSendGate();
587
+ });
588
+ }
589
+ }
554
590
  async callAndTrace(method, params, opts) {
555
591
  this.trace('OUT', method, params);
556
592
  // RPC 往返计时:区分「网关慢」与「本地队列堵塞」。message.send/group.send 的
@@ -560,7 +596,7 @@ export class AUNChannel {
560
596
  // SLOW_RPC_WARN_MS 需明显低于 SDK 默认 10s 超时,以便在真正超时前提前告警。
561
597
  const SLOW_RPC_WARN_MS = 3000;
562
598
  try {
563
- const result = await this.client.call(method, params);
599
+ const result = await this.callClient(method, params);
564
600
  const durationMs = Date.now() - rpcStart;
565
601
  if (!opts?.silentOk) {
566
602
  const r = result;
@@ -1283,6 +1319,16 @@ export class AUNChannel {
1283
1319
  aidState;
1284
1320
  aidStatsCollector;
1285
1321
  outboxInFlight = new Set();
1322
+ /** Shared gateway-facing send limit for replies, activity, observer and thought messages. */
1323
+ static OUTBOUND_SEND_METHODS = new Set([
1324
+ 'message.send',
1325
+ 'group.send',
1326
+ 'message.thought.put',
1327
+ 'group.thought.put',
1328
+ ]);
1329
+ static OUTBOUND_SEND_CONCURRENCY = 4;
1330
+ outboundSendActive = 0;
1331
+ outboundSendQueue = [];
1286
1332
  constructor(config) {
1287
1333
  this.config = config;
1288
1334
  this.agentDir = agentDirPath(config.aid);
@@ -3083,7 +3129,11 @@ export class AUNChannel {
3083
3129
  }
3084
3130
  }
3085
3131
  removeDeliveredOutboxEntry(entry) {
3086
- if (!outbox.removeIfRouteMatches(this.config.aid, entry)) {
3132
+ const messageId = entry.deliveryReceipt?.messageId;
3133
+ const finalized = messageId
3134
+ ? outbox.markDelivered(this.config.aid, entry.id, messageId, entry)
3135
+ : outbox.removeIfRouteMatches(this.config.aid, entry);
3136
+ if (!finalized) {
3087
3137
  logger.warn(`${this.logPrefix()} Preserved outbox entry whose route changed while delivery was in flight: id=${entry.id} submittedChannel=${entry.channelId}`);
3088
3138
  }
3089
3139
  }
@@ -3115,7 +3165,7 @@ export class AUNChannel {
3115
3165
  return `aun_status=${receipt.status ?? 'unknown'} seq=${receipt.seq ?? 'unknown'} delivery_mode=${receipt.deliveryMode ?? 'unknown'} gateway_timestamp=${receipt.timestamp ?? 'unknown'}`;
3116
3166
  }
3117
3167
  logAunSendAccepted(method, target, messageId, encrypt, result, text) {
3118
- const preview = text === undefined ? '' : ` text=${text.slice(0, 60)}`;
3168
+ const preview = text === undefined ? '' : ` text=${formatAunSendLogText(text)}`;
3119
3169
  logger.info(`${this.logPrefix()} ${method} accepted by AUN: target=${target} mid=${messageId} encrypt=${encrypt} ${this.receiptLogFields(result)}${preview}`);
3120
3170
  }
3121
3171
  stripUndefinedDeep(value) {
@@ -3326,8 +3376,8 @@ export class AUNChannel {
3326
3376
  }
3327
3377
  messageIds.add(messageId);
3328
3378
  const now = Date.now();
3329
- const mapTtl = action.expiresAt && action.expiresAt > now
3330
- ? action.expiresAt - now
3379
+ const mapTtl = typeof action.expiresAt === 'number' && Number.isFinite(action.expiresAt)
3380
+ ? Math.max(0, action.expiresAt - now)
3331
3381
  : AUN_INTERACTION_CARD_TTL_MS;
3332
3382
  const mapTimer = setTimeout(() => {
3333
3383
  this.cardMessageIdMap.delete(messageId);
@@ -3390,7 +3440,7 @@ export class AUNChannel {
3390
3440
  params.to = targetAid;
3391
3441
  const callOnce = async (sendParams, fallback) => {
3392
3442
  const result = fallback
3393
- ? await this.client.call(method, sendParams)
3443
+ ? await this.callClient(method, sendParams)
3394
3444
  : await this.callAndTrace(method, sendParams);
3395
3445
  const mid = this.messageIdFromSendResult(result);
3396
3446
  if (!mid) {
@@ -3479,39 +3529,81 @@ export class AUNChannel {
3479
3529
  // turn malformed `mentions: [undefined]` into an apparently valid `[]`.
3480
3530
  const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, context));
3481
3531
  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)}`);
3532
+ const expiresAt = opts.postSend?.type === 'register_interaction_card'
3533
+ ? opts.postSend.expiresAt
3534
+ : undefined;
3535
+ if (typeof expiresAt === 'number' && Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
3536
+ return {
3537
+ status: 'permanent',
3538
+ error: 'interaction card has expired',
3539
+ code: 'INTERACTION_EXPIRED',
3540
+ };
3541
+ }
3542
+ const remainingTtl = typeof expiresAt === 'number' && Number.isFinite(expiresAt)
3543
+ ? Math.max(0, expiresAt - Date.now())
3544
+ : undefined;
3545
+ const requestedTtl = opts.ttl ?? outbox.defaultTtl(opts.queue);
3546
+ const ttl = remainingTtl === undefined ? opts.ttl : Math.min(requestedTtl, remainingTtl);
3547
+ let entry;
3548
+ try {
3549
+ entry = outbox.enqueue(this.config.aid, {
3550
+ queue: opts.queue,
3551
+ channelId,
3552
+ delivery,
3553
+ type: 'payload',
3554
+ contentKind: opts.contentKind,
3555
+ payload: finalPayload,
3556
+ context,
3557
+ logText,
3558
+ ttl,
3559
+ postSend: opts.postSend,
3560
+ });
3561
+ }
3562
+ catch (error) {
3563
+ // Queue saturation is a caller-visible failure. Keep route/schema
3564
+ // validation errors as exceptions so their existing fail-closed path is
3565
+ // preserved, but expose OUTBOX_FULL as a structured send result.
3566
+ if (error?.code !== 'OUTBOX_FULL')
3567
+ throw error;
3568
+ return {
3569
+ status: 'permanent',
3570
+ error: error instanceof Error ? error.message : String(error),
3571
+ code: 'OUTBOX_FULL',
3572
+ };
3573
+ }
3574
+ 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
3575
  if (!this.connected || !this.client) {
3495
3576
  logger.warn(`${this.logPrefix()} Not connected, payload queued in outbox (id=${entry.id}, kind=${opts.contentKind}). Triggering reconnect.`);
3496
3577
  if (!this.reconnectTimer && !this.client) {
3497
3578
  this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendContentPayload failed: ${e}`));
3498
3579
  }
3499
- return { queued: true };
3580
+ return {
3581
+ queued: true,
3582
+ outboxId: entry.id,
3583
+ error: 'AUN channel is not connected',
3584
+ code: 'AUN_NOT_CONNECTED',
3585
+ };
3500
3586
  }
3501
3587
  const result = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
3502
3588
  if (result.ok) {
3503
3589
  this.removeDeliveredOutboxEntry(entry);
3504
3590
  return { messageId: result.messageId };
3505
3591
  }
3506
- if (result.status === 'permanent') {
3592
+ if (result.status === 'permanent' || result.status === 'failed') {
3507
3593
  this.markPermanentOutboxFailure(entry, result);
3508
3594
  return {
3509
3595
  status: 'permanent',
3596
+ outboxId: entry.id,
3510
3597
  ...(result.error !== undefined ? { error: result.error } : {}),
3511
3598
  ...(result.code !== undefined ? { code: result.code } : {}),
3512
3599
  };
3513
3600
  }
3514
- return { queued: true };
3601
+ return {
3602
+ queued: true,
3603
+ outboxId: entry.id,
3604
+ ...(result.error !== undefined ? { error: result.error } : {}),
3605
+ ...(result.code !== undefined ? { code: result.code } : {}),
3606
+ };
3515
3607
  }
3516
3608
  buildTaskPayloadBase(envelope, context) {
3517
3609
  const base = {};
@@ -3629,16 +3721,22 @@ export class AUNChannel {
3629
3721
  };
3630
3722
  }
3631
3723
  async sendReliableStructured(channelId, payload, context, logText) {
3632
- await this.sendContentPayload(channelId, payload, {
3724
+ const result = await this.sendContentPayload(channelId, payload, {
3633
3725
  contentKind: 'custom',
3634
3726
  context,
3635
3727
  logText: logText ?? this.payloadLogText(payload, 'custom'),
3636
3728
  });
3729
+ if (result.status === 'permanent' || result.status === 'failed') {
3730
+ throw Object.assign(new Error(result.error ?? 'AUN structured send failed'), {
3731
+ code: result.code ?? 'AUN_SEND_FAILED',
3732
+ outboxId: result.outboxId,
3733
+ });
3734
+ }
3637
3735
  }
3638
3736
  async sendMessage(channelId, text, context) {
3639
3737
  if (!text?.trim()) {
3640
3738
  logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
3641
- return;
3739
+ return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
3642
3740
  }
3643
3741
  const delivery = this.requireDelivery(channelId, context);
3644
3742
  const routedContext = this.withDelivery(context, delivery);
@@ -3676,13 +3774,14 @@ export class AUNChannel {
3676
3774
  : undefined;
3677
3775
  if (operationId) {
3678
3776
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
3679
- if (hasMessageLogOperation(chatDir, operationId)) {
3777
+ const completedMessageId = findMessageLogOperationMessageId(chatDir, operationId);
3778
+ if (completedMessageId) {
3680
3779
  const duplicate = outbox.findByDedupeKey(this.config.aid, operationId);
3681
3780
  if (duplicate) {
3682
3781
  outbox.remove(this.config.aid, duplicate.id);
3683
3782
  logger.info(`${this.logPrefix()} Removed stale outbox entry for completed operation: ${operationId}`);
3684
3783
  }
3685
- return;
3784
+ return { status: 'sent', messageId: completedMessageId };
3686
3785
  }
3687
3786
  }
3688
3787
  // Write-ahead: persist to outbox before attempting send
@@ -3698,9 +3797,19 @@ export class AUNChannel {
3698
3797
  ? routedContext.metadata.outboxTtl
3699
3798
  : undefined,
3700
3799
  });
3800
+ if (entry.deliveryResult?.messageId || entry.deliveryReceipt?.messageId) {
3801
+ const messageId = entry.deliveryResult?.messageId ?? entry.deliveryReceipt.messageId;
3802
+ logger.info(`${this.logPrefix()} Reusing completed durable operation: operation=${operationId ?? '<none>'} entry=${entry.id} mid=${messageId}`);
3803
+ return { status: 'sent', messageId };
3804
+ }
3701
3805
  if (entry.terminal) {
3702
3806
  logger.warn(`${this.logPrefix()} Skipping previously terminated durable operation: operation=${operationId ?? '<none>'} entry=${entry.id} code=${entry.lastErrorCode ?? 'unknown'}`);
3703
- return;
3807
+ return {
3808
+ status: 'failed',
3809
+ outboxId: entry.id,
3810
+ error: entry.lastError ?? entry.terminal.error,
3811
+ ...(entry.lastErrorCode !== undefined ? { code: entry.lastErrorCode } : {}),
3812
+ };
3704
3813
  }
3705
3814
  logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
3706
3815
  // 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
@@ -3713,16 +3822,40 @@ export class AUNChannel {
3713
3822
  if (!this.reconnectTimer && !this.client) {
3714
3823
  this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendMessage failed: ${e}`));
3715
3824
  }
3716
- return;
3825
+ return { status: 'queued', outboxId: entry.id, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
3717
3826
  }
3718
3827
  // Attempt immediate delivery
3719
3828
  const result = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
3720
3829
  if (result.status === 'sent') {
3830
+ const messageId = result.messageId ?? entry.deliveryReceipt?.messageId;
3831
+ if (!messageId) {
3832
+ const error = 'AUN send completed without a remote message_id';
3833
+ this.markPermanentOutboxFailure(entry, { error, code: 'MISSING_MESSAGE_ID' });
3834
+ return { status: 'failed', outboxId: entry.id, error, code: 'MISSING_MESSAGE_ID' };
3835
+ }
3721
3836
  this.removeDeliveredOutboxEntry(entry);
3837
+ return { status: 'sent', messageId };
3722
3838
  }
3723
3839
  else if (result.status === 'permanent') {
3724
3840
  this.markPermanentOutboxFailure(entry, result);
3841
+ return {
3842
+ status: 'failed',
3843
+ outboxId: entry.id,
3844
+ error: result.error ?? 'permanent AUN send failure',
3845
+ ...(result.code !== undefined ? { code: result.code } : {}),
3846
+ };
3725
3847
  }
3848
+ // A gateway receipt may already have been checkpointed even if local
3849
+ // post-send bookkeeping needs a retry. Remote acceptance still wins.
3850
+ if (entry.deliveryReceipt?.messageId) {
3851
+ return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
3852
+ }
3853
+ return {
3854
+ status: 'queued',
3855
+ outboxId: entry.id,
3856
+ ...(result.error !== undefined ? { error: result.error } : {}),
3857
+ ...(result.code !== undefined ? { code: result.code } : {}),
3858
+ };
3726
3859
  }
3727
3860
  /** Daemon-side transport for `ec msg send` running inside an agent task. */
3728
3861
  async sendDaemonMsg(args) {
@@ -4005,9 +4138,10 @@ export class AUNChannel {
4005
4138
  || (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
4006
4139
  if (operationId) {
4007
4140
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
4008
- if (hasMessageLogOperation(chatDir, operationId)) {
4141
+ const completedMessageId = findMessageLogOperationMessageId(chatDir, operationId);
4142
+ if (completedMessageId) {
4009
4143
  logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
4010
- return sentOutboxResult();
4144
+ return sentOutboxResult(completedMessageId);
4011
4145
  }
4012
4146
  if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
4013
4147
  const agentConfig = loadAgent(this.config.aid);
@@ -4054,7 +4188,7 @@ export class AUNChannel {
4054
4188
  source,
4055
4189
  transport: entry.deliveryReceipt.transport,
4056
4190
  });
4057
- return sentOutboxResult();
4191
+ return sentOutboxResult(entry.deliveryReceipt.messageId);
4058
4192
  }
4059
4193
  const encryptTarget = isGroup ? channelId : targetAid;
4060
4194
  const encrypt = context?.metadata?.encrypted != null
@@ -4063,6 +4197,7 @@ export class AUNChannel {
4063
4197
  const params = { payload, encrypt };
4064
4198
  if (context?.metadata?.persistRequired === true)
4065
4199
  params.persist_required = true;
4200
+ let acceptedMessageId;
4066
4201
  try {
4067
4202
  if (isGroup) {
4068
4203
  params.group_id = channelId;
@@ -4082,6 +4217,7 @@ export class AUNChannel {
4082
4217
  return failure;
4083
4218
  }
4084
4219
  else {
4220
+ acceptedMessageId = mid;
4085
4221
  this.logAunSendAccepted('group.send', channelId, mid, encrypt, result, finalText);
4086
4222
  this.checkpointTextDelivery(entry, mid, encrypt, result);
4087
4223
  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 +4240,7 @@ export class AUNChannel {
4104
4240
  return classifyAunSendFailure(result, 'message.send returned no message_id');
4105
4241
  }
4106
4242
  else {
4243
+ acceptedMessageId = mid;
4107
4244
  this.logAunSendAccepted('message.send', this.peerLabel(targetAid), mid, encrypt, result, finalText);
4108
4245
  this.checkpointTextDelivery(entry, mid, encrypt, result);
4109
4246
  const causation = normalizeCausation(context?.metadata?.causation);
@@ -4124,7 +4261,7 @@ export class AUNChannel {
4124
4261
  this.forwardOutbound(result);
4125
4262
  }
4126
4263
  }
4127
- return sentOutboxResult();
4264
+ return sentOutboxResult(acceptedMessageId);
4128
4265
  }
4129
4266
  catch (e) {
4130
4267
  if (entry.deliveryReceipt) {
@@ -4139,7 +4276,7 @@ export class AUNChannel {
4139
4276
  try {
4140
4277
  if (isGroup) {
4141
4278
  this.trace('OUT', 'group.send.fallback', params);
4142
- const result = await this.client.call('group.send', params);
4279
+ const result = await this.callClient('group.send', params);
4143
4280
  const mid = this.messageIdFromSendResult(result);
4144
4281
  if (!mid) {
4145
4282
  const resultRecord = errorRecord(result);
@@ -4148,6 +4285,7 @@ export class AUNChannel {
4148
4285
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
4149
4286
  return classifyAunSendFailure(result, 'group.send plaintext fallback returned no message_id');
4150
4287
  }
4288
+ acceptedMessageId = mid;
4151
4289
  this.trace('OUT', 'group.send.fallback.ok', { message_id: mid });
4152
4290
  this.checkpointTextDelivery(entry, mid, false, result);
4153
4291
  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 +4299,14 @@ export class AUNChannel {
4161
4299
  }
4162
4300
  else {
4163
4301
  this.trace('OUT', 'message.send.fallback', params);
4164
- const result = await this.client.call('message.send', params);
4302
+ const result = await this.callClient('message.send', params);
4165
4303
  const mid = this.messageIdFromSendResult(result);
4166
4304
  if (!mid) {
4167
4305
  this.trace('OUT', 'message.send.fallback.missing_id', {});
4168
4306
  logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
4169
4307
  return classifyAunSendFailure(result, 'message.send plaintext fallback returned no message_id');
4170
4308
  }
4309
+ acceptedMessageId = mid;
4171
4310
  this.trace('OUT', 'message.send.fallback.ok', { message_id: mid });
4172
4311
  this.checkpointTextDelivery(entry, mid, false, result);
4173
4312
  const causation = normalizeCausation(context?.metadata?.causation);
@@ -4186,7 +4325,7 @@ export class AUNChannel {
4186
4325
  });
4187
4326
  this.forwardOutbound(result);
4188
4327
  }
4189
- return sentOutboxResult();
4328
+ return sentOutboxResult(acceptedMessageId);
4190
4329
  }
4191
4330
  catch (e2) {
4192
4331
  if (entry.deliveryReceipt) {
@@ -4214,6 +4353,7 @@ export class AUNChannel {
4214
4353
  id: entry.id,
4215
4354
  channelId: entry.channelId,
4216
4355
  delivery: entry.delivery,
4356
+ queue: entry.queue,
4217
4357
  };
4218
4358
  const receipt = {
4219
4359
  messageId,
@@ -4238,6 +4378,20 @@ export class AUNChannel {
4238
4378
  logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
4239
4379
  return { ok: true, status: 'sent' };
4240
4380
  }
4381
+ const interactionExpiresAt = entry.postSend?.type === 'register_interaction_card'
4382
+ ? entry.postSend.expiresAt
4383
+ : undefined;
4384
+ if (typeof interactionExpiresAt === 'number'
4385
+ && Number.isFinite(interactionExpiresAt)
4386
+ && interactionExpiresAt <= Date.now()) {
4387
+ logger.info(`${this.logPrefix()} Discarded expired interaction from durable outbox: request=${interactionId ?? '<unknown>'} entry=${entry.id}`);
4388
+ return {
4389
+ ok: false,
4390
+ status: 'permanent',
4391
+ error: 'interaction card has expired',
4392
+ code: 'INTERACTION_EXPIRED',
4393
+ };
4394
+ }
4241
4395
  const channelId = entry.channelId;
4242
4396
  const payload = entry.payload;
4243
4397
  if (!payload) {
@@ -4696,7 +4850,7 @@ export class AUNChannel {
4696
4850
  return { status: 'permanent', error: 'image outbox entry disappeared during upload', code: 'OUTBOX_ENTRY_MISSING' };
4697
4851
  }
4698
4852
  const sent = await this.deliverPayloadEntry(entry);
4699
- return sent.ok ? sentOutboxResult() : sent;
4853
+ return sent.ok ? sentOutboxResult(sent.messageId) : sent;
4700
4854
  }
4701
4855
  catch (error) {
4702
4856
  const failure = classifyAunSendFailure(error, 'image upload or send failed');
@@ -4728,7 +4882,7 @@ export class AUNChannel {
4728
4882
  source,
4729
4883
  transport: entry.deliveryReceipt.transport,
4730
4884
  });
4731
- return sentOutboxResult();
4885
+ return sentOutboxResult(entry.deliveryReceipt.messageId);
4732
4886
  }
4733
4887
  catch (error) {
4734
4888
  const detail = error instanceof Error ? error.message : String(error);
@@ -4805,7 +4959,7 @@ export class AUNChannel {
4805
4959
  if (isGroup) {
4806
4960
  params.group_id = delivery.groupId;
4807
4961
  this.trace('OUT', 'group.send.file', params);
4808
- const result = await this.client.call('group.send', params);
4962
+ const result = await this.callClient('group.send', params);
4809
4963
  sendResult = result;
4810
4964
  const fileMid = this.messageIdFromSendResult(result);
4811
4965
  sentMid = fileMid ?? null;
@@ -4818,7 +4972,7 @@ export class AUNChannel {
4818
4972
  else {
4819
4973
  params.to = fileTargetAid;
4820
4974
  this.trace('OUT', 'message.send.file', params);
4821
- const result = await this.client.call('message.send', params);
4975
+ const result = await this.callClient('message.send', params);
4822
4976
  sendResult = result;
4823
4977
  sentMid = this.messageIdFromSendResult(result);
4824
4978
  this.trace('OUT', 'message.send.file.ok', { message_id: sentMid });
@@ -4839,7 +4993,7 @@ export class AUNChannel {
4839
4993
  params.encrypt = false;
4840
4994
  if (isGroup) {
4841
4995
  this.trace('OUT', 'group.send.file.fallback', params);
4842
- const result = await this.client.call('group.send', params);
4996
+ const result = await this.callClient('group.send', params);
4843
4997
  sendResult = result;
4844
4998
  const fbMid = this.messageIdFromSendResult(result);
4845
4999
  sentMid = fbMid ?? null;
@@ -4851,7 +5005,7 @@ export class AUNChannel {
4851
5005
  }
4852
5006
  else {
4853
5007
  this.trace('OUT', 'message.send.file.fallback', params);
4854
- const result = await this.client.call('message.send', params);
5008
+ const result = await this.callClient('message.send', params);
4855
5009
  sendResult = result;
4856
5010
  sentMid = this.messageIdFromSendResult(result);
4857
5011
  this.trace('OUT', 'message.send.file.fallback.ok', { message_id: sentMid });
@@ -4887,7 +5041,7 @@ export class AUNChannel {
4887
5041
  });
4888
5042
  if (sendResult)
4889
5043
  this.forwardOutbound(sendResult);
4890
- return sentOutboxResult();
5044
+ return sentOutboxResult(sentMid);
4891
5045
  }
4892
5046
  catch (e) {
4893
5047
  if (entry.deliveryReceipt) {
@@ -4907,7 +5061,7 @@ export class AUNChannel {
4907
5061
  if (this.outboxTimer)
4908
5062
  return;
4909
5063
  this.outboxTimer = setInterval(() => {
4910
- if (this.connected && this.client && outbox.hasPending(this.config.aid)) {
5064
+ if (this.connected && this.client && (outbox.hasPending(this.config.aid) || outbox.hasPending(this.config.aid, 'activity'))) {
4911
5065
  this.drainOutbox();
4912
5066
  }
4913
5067
  }, 30_000);
@@ -4922,36 +5076,42 @@ export class AUNChannel {
4922
5076
  if (!this.connected || !this.client)
4923
5077
  return;
4924
5078
  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;
5079
+ const drainQueue = async (queue) => {
5080
+ if (!outbox.hasPending(this.config.aid, queue))
5081
+ return;
5082
+ logger.info(`${this.logPrefix()} Draining ${queue} outbox...`);
5083
+ const result = await outbox.drain(this.config.aid, async (entry) => {
5084
+ if (!isDeliveryTarget(entry.delivery)) {
5085
+ logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
5086
+ return {
5087
+ status: 'permanent',
5088
+ error: 'outbox entry has no valid delivery route',
5089
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
5090
+ };
5091
+ }
5092
+ if (entry.type === 'text') {
5093
+ return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
5094
+ }
5095
+ else if (entry.type === 'file') {
5096
+ return this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
5097
+ }
5098
+ else if (entry.type === 'image') {
5099
+ return this.withOutboxInFlight(entry, () => this.deliverImageEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
5100
+ }
5101
+ else if (entry.type === 'payload') {
5102
+ const sent = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false, status: 'retry', error: 'outbox send is already in flight' });
5103
+ return sent.ok
5104
+ ? { status: 'sent', ...(sent.messageId ? { messageId: sent.messageId } : {}) }
5105
+ : sent;
5106
+ }
5107
+ return { status: 'permanent', error: `unsupported outbox entry type: ${entry.type}`, code: 'UNSUPPORTED_OUTBOX_TYPE' };
5108
+ }, queue);
5109
+ if (result.sent > 0 || result.expired > 0 || result.permanent) {
5110
+ logger.info(`${this.logPrefix()} ${queue} outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed} permanent=${result.permanent ?? 0}`);
4949
5111
  }
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
- }
5112
+ };
5113
+ await drainQueue('default');
5114
+ await drainQueue('activity');
4955
5115
  }
4956
5116
  /** Repair bootstrap outbox entries to the configured personal Owner route. */
4957
5117
  repairBootstrapRoutes() {
@@ -5325,6 +5485,13 @@ export class AUNChannelPlugin {
5325
5485
  const delivery = requireEnvelopeDelivery(channelId, envelope?.delivery, envelope?.replyContext?.delivery);
5326
5486
  const parentCausation = normalizeCausation(envelope.causation ?? envelope.replyContext?.metadata?.causation);
5327
5487
  const outboundCausation = parentCausation ? deriveCausation(parentCausation) : undefined;
5488
+ const operationId = envelope.operationId ?? envelope.taskId;
5489
+ // A task can emit multiple independent intermediate text chunks. Keep
5490
+ // the task operation ID in the receipt, but do not use it as the
5491
+ // durable-send dedupe key for each non-final chunk.
5492
+ const dedupeOperationId = payload.kind === 'result.text' && payload.isFinal === false
5493
+ ? undefined
5494
+ : operationId;
5328
5495
  const replyCtx = outboundCausation
5329
5496
  ? {
5330
5497
  ...(envelope.replyContext ?? {}),
@@ -5345,11 +5512,61 @@ export class AUNChannelPlugin {
5345
5512
  case 'result.text':
5346
5513
  case 'command.result':
5347
5514
  case 'command.error': {
5348
- const sendCtx = { ...(replyCtx ?? {}) };
5515
+ const sendCtx = {
5516
+ ...(replyCtx ?? {}),
5517
+ metadata: {
5518
+ ...Object.fromEntries(Object.entries(replyCtx?.metadata ?? {})
5519
+ .filter(([key]) => key !== 'operationId')),
5520
+ ...(dedupeOperationId ? { operationId: dedupeOperationId } : {}),
5521
+ },
5522
+ };
5349
5523
  if (payload.kind === 'result.text' && payload.isFinal)
5350
5524
  sendCtx.title = '✅ 最终回复:';
5351
- await channel.sendMessage(channelId, payload.text, sendCtx);
5352
- return;
5525
+ let result;
5526
+ try {
5527
+ result = await channel.sendMessage(channelId, payload.text, sendCtx);
5528
+ }
5529
+ catch (error) {
5530
+ if (error?.code !== 'OUTBOX_FULL')
5531
+ throw error;
5532
+ return {
5533
+ status: 'failed',
5534
+ operationId,
5535
+ messages: [],
5536
+ error: error instanceof Error ? error.message : String(error),
5537
+ code: 'OUTBOX_FULL',
5538
+ };
5539
+ }
5540
+ if (result.status === 'sent') {
5541
+ return {
5542
+ status: 'sent',
5543
+ operationId,
5544
+ messageId: result.messageId,
5545
+ messages: [{
5546
+ messageId: result.messageId,
5547
+ partIndex: 0,
5548
+ ...(sendCtx.threadId ? { threadId: sendCtx.threadId } : {}),
5549
+ }],
5550
+ };
5551
+ }
5552
+ if (result.status === 'queued') {
5553
+ return {
5554
+ status: 'queued',
5555
+ operationId,
5556
+ messages: [],
5557
+ outboxId: result.outboxId,
5558
+ ...(result.error !== undefined ? { error: result.error } : {}),
5559
+ ...(result.code !== undefined ? { code: result.code } : {}),
5560
+ };
5561
+ }
5562
+ return {
5563
+ status: 'failed',
5564
+ operationId,
5565
+ messages: [],
5566
+ ...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
5567
+ error: result.error,
5568
+ ...(result.code !== undefined ? { code: result.code } : {}),
5569
+ };
5353
5570
  }
5354
5571
  case 'system.notice': {
5355
5572
  const noticePayload = {
@@ -5402,6 +5619,9 @@ export class AUNChannelPlugin {
5402
5619
  }
5403
5620
  case 'activity.batch': {
5404
5621
  const items = Array.isArray(payload.items) ? payload.items : [];
5622
+ const messageIds = [];
5623
+ let queuedResult;
5624
+ let failedResult;
5405
5625
  for (const item of items) {
5406
5626
  if (item?.kind === 'progress') {
5407
5627
  const metadata = { activityType: 'progress' };
@@ -5417,10 +5637,51 @@ export class AUNChannelPlugin {
5417
5637
  await channel.sendThought(channelId, envelope.taskId, aunPayload, replyCtx);
5418
5638
  }
5419
5639
  else {
5420
- await channel.sendReliableStructured(channelId, aunPayload, replyCtx, channel.activityLogText(item));
5640
+ const result = await channel.sendContentPayload(channelId, aunPayload, {
5641
+ queue: 'activity',
5642
+ contentKind: 'custom',
5643
+ context: replyCtx,
5644
+ logText: channel.activityLogText(item),
5645
+ });
5646
+ if (result.status === 'permanent' || result.status === 'failed')
5647
+ failedResult ??= result;
5648
+ else if (result.status === 'retry' || result.status === 'queued' || result.queued)
5649
+ queuedResult ??= result;
5650
+ else if (result.messageId)
5651
+ messageIds.push(result.messageId);
5421
5652
  }
5422
5653
  }
5423
- return;
5654
+ if (failedResult) {
5655
+ return {
5656
+ status: 'failed',
5657
+ operationId,
5658
+ messages: [],
5659
+ ...(failedResult.outboxId !== undefined ? { outboxId: failedResult.outboxId } : {}),
5660
+ error: failedResult.error ?? 'AUN activity delivery failed',
5661
+ ...(failedResult.code !== undefined ? { code: failedResult.code } : {}),
5662
+ };
5663
+ }
5664
+ if (queuedResult) {
5665
+ return queuedResult.outboxId
5666
+ ? {
5667
+ status: 'queued',
5668
+ operationId,
5669
+ messages: [],
5670
+ outboxId: queuedResult.outboxId,
5671
+ ...(queuedResult.error !== undefined ? { error: queuedResult.error } : {}),
5672
+ ...(queuedResult.code !== undefined ? { code: queuedResult.code } : {}),
5673
+ }
5674
+ : {
5675
+ status: 'failed',
5676
+ operationId,
5677
+ messages: [],
5678
+ error: 'queued activity is missing its outbox id',
5679
+ code: 'MISSING_OUTBOX_ID',
5680
+ };
5681
+ }
5682
+ if (messageIds.length > 0)
5683
+ return sentReceipt(envelope, messageIds, replyCtx?.threadId);
5684
+ return suppressedReceipt(envelope, 'empty_activity');
5424
5685
  }
5425
5686
  case 'status.progress':
5426
5687
  channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
@@ -5449,6 +5710,44 @@ export class AUNChannelPlugin {
5449
5710
  case 'interaction': {
5450
5711
  const req = payload.interaction;
5451
5712
  const cardTtlMs = AUN_INTERACTION_CARD_TTL_MS;
5713
+ const cardExpiresAt = typeof req.expiresAt === 'number' && Number.isFinite(req.expiresAt)
5714
+ ? req.expiresAt
5715
+ : Date.now() + cardTtlMs;
5716
+ const toInteractionReceipt = (result) => {
5717
+ if ((result.status === undefined || result.status === 'sent') && result.messageId) {
5718
+ return sentReceipt(envelope, [result.messageId], replyCtx?.threadId);
5719
+ }
5720
+ if (result.status === 'retry' || result.status === 'queued' || result.queued) {
5721
+ if (!result.outboxId) {
5722
+ return {
5723
+ status: 'failed',
5724
+ operationId,
5725
+ messages: [],
5726
+ error: 'queued interaction is missing its outbox id',
5727
+ code: 'MISSING_OUTBOX_ID',
5728
+ };
5729
+ }
5730
+ return {
5731
+ status: 'queued',
5732
+ operationId,
5733
+ messages: [],
5734
+ outboxId: result.outboxId,
5735
+ ...(result.error !== undefined ? { error: result.error } : {}),
5736
+ ...(result.code !== undefined ? { code: result.code } : {}),
5737
+ };
5738
+ }
5739
+ if (result.status === 'permanent' || result.status === 'failed') {
5740
+ return {
5741
+ status: 'failed',
5742
+ operationId,
5743
+ messages: [],
5744
+ ...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
5745
+ error: result.error ?? 'AUN interaction delivery failed',
5746
+ ...(result.code !== undefined ? { code: result.code } : {}),
5747
+ };
5748
+ }
5749
+ return suppressedReceipt(envelope, 'interaction_not_sent');
5750
+ };
5452
5751
  if (req.kind.kind === 'action') {
5453
5752
  const action = req.kind;
5454
5753
  const aunCard = {
@@ -5472,7 +5771,7 @@ export class AUNChannelPlugin {
5472
5771
  aunCard.initiator = req.initiatorId;
5473
5772
  if (replyCtx?.threadId)
5474
5773
  aunCard.thread_id = replyCtx.threadId;
5475
- await channel.sendContentPayload(channelId, aunCard, {
5774
+ const result = await channel.sendContentPayload(channelId, aunCard, {
5476
5775
  contentKind: 'card',
5477
5776
  context: replyCtx,
5478
5777
  logText: action.title ? `[card] ${action.title}` : '[card]',
@@ -5482,9 +5781,10 @@ export class AUNChannelPlugin {
5482
5781
  isCommandCard: false,
5483
5782
  initiatorAid: req.initiatorId,
5484
5783
  delivery: replyCtx?.delivery,
5485
- expiresAt: Date.now() + cardTtlMs,
5784
+ expiresAt: cardExpiresAt,
5486
5785
  },
5487
5786
  });
5787
+ return toInteractionReceipt(result);
5488
5788
  }
5489
5789
  else if (req.kind.kind === 'command-card') {
5490
5790
  const card = req.kind;
@@ -5510,7 +5810,7 @@ export class AUNChannelPlugin {
5510
5810
  aunCard.initiator = req.initiatorId;
5511
5811
  if (replyCtx?.threadId)
5512
5812
  aunCard.thread_id = replyCtx.threadId;
5513
- await channel.sendContentPayload(channelId, aunCard, {
5813
+ const result = await channel.sendContentPayload(channelId, aunCard, {
5514
5814
  contentKind: 'card',
5515
5815
  context: replyCtx,
5516
5816
  logText: card.title ? `[card] ${card.title}` : '[card]',
@@ -5520,14 +5820,35 @@ export class AUNChannelPlugin {
5520
5820
  isCommandCard: true,
5521
5821
  initiatorAid: req.initiatorId,
5522
5822
  delivery: replyCtx?.delivery,
5523
- expiresAt: Date.now() + cardTtlMs,
5823
+ expiresAt: cardExpiresAt,
5524
5824
  },
5525
5825
  });
5826
+ return toInteractionReceipt(result);
5526
5827
  }
5527
5828
  else if (payload.fallbackText) {
5528
- await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
5829
+ const result = await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
5830
+ if (result.status === 'sent')
5831
+ return sentReceipt(envelope, [result.messageId], replyCtx?.threadId);
5832
+ if (result.status === 'queued') {
5833
+ return {
5834
+ status: 'queued',
5835
+ operationId,
5836
+ messages: [],
5837
+ outboxId: result.outboxId,
5838
+ ...(result.error !== undefined ? { error: result.error } : {}),
5839
+ ...(result.code !== undefined ? { code: result.code } : {}),
5840
+ };
5841
+ }
5842
+ return {
5843
+ status: 'failed',
5844
+ operationId,
5845
+ messages: [],
5846
+ ...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
5847
+ error: result.error,
5848
+ ...(result.code !== undefined ? { code: result.code } : {}),
5849
+ };
5529
5850
  }
5530
- return;
5851
+ return suppressedReceipt(envelope, 'empty_interaction');
5531
5852
  }
5532
5853
  case 'custom': {
5533
5854
  const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
@@ -5570,7 +5891,7 @@ export class AUNChannelPlugin {
5570
5891
  registerBridge(bridge, channelType) {
5571
5892
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
5572
5893
  handler(aunOptsToInbound(opts, adapter.channelName, channelType));
5573
- }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
5894
+ }), async (channelId, text, replyContext) => { await channel.sendMessage(channelId, text, replyContext); }, adapter, channelType);
5574
5895
  },
5575
5896
  registerHooks(hookCtx) {
5576
5897
  channel.setEventBus(hookCtx.eventBus);