evolcore 0.0.12 → 0.0.14

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 (116) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/ec-safe-output.js +26 -7
  4. package/bin/install-codex-managed-hooks.mjs +201 -0
  5. package/dist/agents/claude-runner.js +105 -11
  6. package/dist/agents/codex-app-server-client.js +176 -7
  7. package/dist/agents/codex-runner.js +248 -84
  8. package/dist/agents/ecagent-runner.js +21 -2
  9. package/dist/agents/gemini-runner.js +9 -4
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/store.js +13 -7
  12. package/dist/aun/group-fs-download.js +56 -0
  13. package/dist/aun/group-identity.js +1 -2
  14. package/dist/aun/msg/group.js +11 -33
  15. package/dist/aun/msg/index.js +1 -2
  16. package/dist/aun/msg/managed-operation.js +176 -55
  17. package/dist/aun/msg/upload.js +16 -1
  18. package/dist/aun/outbox.js +31 -2
  19. package/dist/aun/rpc/client.js +66 -0
  20. package/dist/aun/rpc/index.js +1 -2
  21. package/dist/aun/storage/{upload.js → client.js} +11 -2
  22. package/dist/aun/storage/index.js +1 -2
  23. package/dist/channels/aun.js +514 -150
  24. package/dist/channels/daemon.js +2 -0
  25. package/dist/cli/agent.js +9 -96
  26. package/dist/cli/aun-commands.js +1 -1
  27. package/dist/cli/cli-argv.js +10 -0
  28. package/dist/cli/config.js +9 -17
  29. package/dist/cli/ctl-command.js +1 -1
  30. package/dist/cli/daemon-commands.js +39 -24
  31. package/dist/cli/fs-command.js +65 -19
  32. package/dist/cli/init.js +77 -8
  33. package/dist/cli/queue-command.js +42 -0
  34. package/dist/cli/restart-monitor.js +3 -14
  35. package/dist/cli/task-context.js +172 -0
  36. package/dist/cli/trigger-command.js +7 -0
  37. package/dist/config/access-policy.js +1 -2
  38. package/dist/config/builtin-roles.js +7 -0
  39. package/dist/config/config-manager.js +27 -3
  40. package/dist/config/contact-book-store.js +7 -2
  41. package/dist/config/peer-role-resolver.js +6 -2
  42. package/dist/config/role-migration-startup.js +53 -0
  43. package/dist/config/role-store.js +1 -1
  44. package/dist/core/auth/agent-delegation.js +1 -1
  45. package/dist/core/auth/auth-gateway.js +30 -1
  46. package/dist/core/auth/authorization-audit.js +47 -0
  47. package/dist/core/auth/operation-authorizer.js +1 -0
  48. package/dist/core/auth/operation-catalog.js +10 -1
  49. package/dist/core/bootstrap-messages.js +9 -2
  50. package/dist/core/bootstrap-service.js +8 -1
  51. package/dist/core/command/command-handler.js +201 -51
  52. package/dist/core/command/menu-catalog.js +4 -0
  53. package/dist/core/command/menu-handler.js +85 -18
  54. package/dist/core/command/menu-protocol.js +23 -2
  55. package/dist/core/command/slash-handler.js +58 -10
  56. package/dist/core/event-catalog.js +8 -0
  57. package/dist/core/handoff/runtime.js +12 -1
  58. package/dist/core/handoff/store.js +5 -1
  59. package/dist/core/interaction-router.js +10 -0
  60. package/dist/core/message/im-renderer.js +34 -1
  61. package/dist/core/message/message-bridge.js +68 -13
  62. package/dist/core/message/message-log.js +2 -0
  63. package/dist/core/message/message-queue.js +267 -39
  64. package/dist/core/message/message-utils.js +81 -4
  65. package/dist/core/message/response-engine.js +242 -48
  66. package/dist/core/message/stream-debouncer.js +10 -2
  67. package/dist/core/permission/approval-gateway.js +1 -0
  68. package/dist/core/permission/ec-command-parser.js +649 -18
  69. package/dist/core/permission/index.js +1 -1
  70. package/dist/core/permission/mode.js +15 -0
  71. package/dist/core/permission/readonly-shell-query.js +148 -8
  72. package/dist/core/permission/sandbox-runtime.js +21 -0
  73. package/dist/core/permission/tool-policy.js +266 -56
  74. package/dist/core/protected-paths.js +107 -2
  75. package/dist/core/runtime-lock.js +101 -0
  76. package/dist/core/session/session-manager.js +21 -2
  77. package/dist/core/session/session-mapper.js +20 -1
  78. package/dist/eck/detect.js +18 -1
  79. package/dist/eck/group-rules-sync.js +15 -44
  80. package/dist/index.js +191 -89
  81. package/dist/ipc.js +16 -3
  82. package/dist/paths.js +3 -0
  83. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  84. package/dist/response-system/modes/single-session/index.js +3 -0
  85. package/dist/trigger/feedback.js +64 -11
  86. package/dist/trigger/history.js +42 -7
  87. package/dist/trigger/manager.js +21 -1
  88. package/dist/trigger/parser.js +14 -2
  89. package/dist/trigger/scheduler.js +1 -0
  90. package/dist/trigger/validation.js +67 -6
  91. package/dist/utils/ecweb-supervisor.js +332 -0
  92. package/dist/utils/error-utils.js +17 -0
  93. package/dist/utils/logger.js +37 -4
  94. package/kits/docs/evolcore/trigger.md +17 -1
  95. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  96. package/kits/schemas/role-config.schema.1.json +1 -0
  97. package/kits/templates/roles/admin.json +7 -0
  98. package/kits/templates/roles/member.json +8 -1
  99. package/kits/templates/roles/visitor.json +2 -0
  100. package/package.json +1 -1
  101. package/dist/aun/msg/payload-type.js +0 -27
  102. package/dist/aun/rpc/caller.js +0 -42
  103. package/dist/aun/rpc/connection.js +0 -25
  104. package/dist/aun/storage/manage.js +0 -10
  105. package/dist/config/access-policy-domain.js +0 -18
  106. package/dist/config/config-batch-get.js +0 -11
  107. package/dist/config/owner-policy.js +0 -4
  108. package/dist/config/role-config-v4-startup.js +0 -32
  109. package/dist/config/role-config-v5-startup.js +0 -27
  110. package/dist/core/auth/trigger-authorization.js +0 -15
  111. package/dist/core/interaction-registration.js +0 -10
  112. package/dist/core/permission/execution-sandbox.js +0 -16
  113. package/dist/core/relation/peer-key.js +0 -1
  114. package/dist/core/session/session-key.js +0 -24
  115. package/dist/eck/baseagent-caps.js +0 -18
  116. package/dist/eck/rules-loader.js +0 -28
@@ -7,7 +7,7 @@ import { logger, localTimestamp } from '../utils/logger.js';
7
7
  import { LogWriter } from '../utils/log-writer.js';
8
8
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
9
9
  import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
10
- import { resolvePaths, agentDir as agentDirPath, resolveRoot } from '../paths.js';
10
+ import { resolvePaths, agentDir as agentDirPath, resolveRoot, channelStatePath } from '../paths.js';
11
11
  import { saveToUploads, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
12
12
  import { appendAidEvent } from '../utils/instance-registry.js';
13
13
  import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, hasMessageLogOperation } from '../core/message/message-log.js';
@@ -31,15 +31,70 @@ import { isHClassPath } from '../core/protected-paths.js';
31
31
  import { consumeAunCausation, registerAunCausation } from '../core/causation/aun-association.js';
32
32
  import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
33
33
  import { recordCausationSpan } from '../core/causation/audit.js';
34
- import { isExplicitGroupId } from '../aun/group-identity.js';
35
34
  import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
36
35
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
37
36
  import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
38
37
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
39
38
  import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
39
+ import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
40
40
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
41
41
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
42
+ const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
42
43
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
44
+ // AUN limits the complete encrypted thought envelope to 8192 bytes. The
45
+ // encrypted group envelope grows with the number of recipient devices, so a
46
+ // conservative plaintext budget avoids rejecting otherwise valid thoughts.
47
+ const AUN_THOUGHT_PLAINTEXT_MAX_BYTES = 4096;
48
+ const AUN_THOUGHT_TRUNCATION_SUFFIX = '\n\n[thought truncated]';
49
+ function truncateUtf8ToBytes(value, maxBytes) {
50
+ if (Buffer.byteLength(value, 'utf8') <= maxBytes)
51
+ return value;
52
+ if (maxBytes <= 0)
53
+ return '';
54
+ let bytes = 0;
55
+ let end = 0;
56
+ for (const char of value) {
57
+ const charBytes = Buffer.byteLength(char, 'utf8');
58
+ if (bytes + charBytes > maxBytes)
59
+ break;
60
+ bytes += charBytes;
61
+ end += char.length;
62
+ }
63
+ return value.slice(0, end);
64
+ }
65
+ function truncateUtf8WithSuffix(value, maxBytes, suffix) {
66
+ if (Buffer.byteLength(value, 'utf8') <= maxBytes)
67
+ return value;
68
+ if (maxBytes <= 0)
69
+ return '';
70
+ const fittedSuffix = truncateUtf8ToBytes(suffix, maxBytes);
71
+ const headBytes = Math.max(0, maxBytes - Buffer.byteLength(fittedSuffix, 'utf8'));
72
+ return `${truncateUtf8ToBytes(value, headBytes)}${fittedSuffix}`;
73
+ }
74
+ function isThoughtTooLargeError(error) {
75
+ const message = error instanceof Error ? error.message : String(error ?? '');
76
+ return /thought too large/i.test(message);
77
+ }
78
+ function requireEnvelopeDelivery(channelId, envelopeDelivery, nestedDelivery) {
79
+ if (!isDeliveryTarget(envelopeDelivery)) {
80
+ throw Object.assign(new Error(`AUN outbound envelope requires a top-level delivery route for channelId=${channelId}`), {
81
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
82
+ });
83
+ }
84
+ if (nestedDelivery !== undefined) {
85
+ if (!isDeliveryTarget(nestedDelivery) || !sameDeliveryTarget(envelopeDelivery, nestedDelivery)) {
86
+ throw Object.assign(new Error(`AUN outbound envelope has conflicting delivery routes for channelId=${channelId}`), {
87
+ code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
88
+ });
89
+ }
90
+ }
91
+ if (envelopeDelivery.chatType === 'group' && envelopeDelivery.groupId !== channelId) {
92
+ throw Object.assign(new Error(`AUN group delivery does not match channelId=${channelId}`), {
93
+ code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
94
+ });
95
+ }
96
+ return envelopeDelivery;
97
+ }
43
98
  function normalizeImageMimeType(mimeType) {
44
99
  const normalized = mimeType?.split(';', 1)[0].trim().toLowerCase();
45
100
  if (!normalized)
@@ -133,6 +188,8 @@ export function aunOptsToInbound(opts, channel, channelType) {
133
188
  encrypted: opts.encrypted,
134
189
  protectedHeaders: opts.protectedHeaders,
135
190
  messageId: opts.messageId,
191
+ receivedAt: opts.receivedAt,
192
+ gatewaySeq: opts.gatewaySeq,
136
193
  causation: opts.causation,
137
194
  mentions: opts.mentions,
138
195
  mentionAids: opts.mentionAids,
@@ -289,14 +346,36 @@ export class AUNChannel {
289
346
  throw e;
290
347
  }
291
348
  }
292
- /** 判断 channelId 是否为群组 ID
293
- * - 新格式:group.{aidDomain}/{group_no|group_name}
294
- * - 数字群号:{group_no}.{aidDomain}(如 11117.example.com)
295
- * - 兼容旧格式:grp_xxx、g-xxx.agentid.pub
349
+ /** Resolve the authenticated transport route for an ordinary outbound send.
350
+ * The channel id is an address only; it is intentionally never used to infer
351
+ * whether the address is a group.
296
352
  */
297
- /** 判断 channelId 是否群组 ID(public:plugin adapter 闭包需调用) */
298
- isGroupId(id) {
299
- return isExplicitGroupId(id);
353
+ requireDelivery(channelId, context, explicit) {
354
+ const supplied = explicit ?? context?.delivery;
355
+ if (supplied !== undefined && !isDeliveryTarget(supplied)) {
356
+ throw Object.assign(new Error(`AUN outbound send has an invalid delivery route for channelId=${channelId}`), {
357
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
358
+ });
359
+ }
360
+ if (!supplied) {
361
+ throw Object.assign(new Error(`AUN outbound send requires an explicit delivery route for channelId=${channelId}`), {
362
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
363
+ });
364
+ }
365
+ if (supplied.chatType === 'group' && supplied.groupId !== channelId) {
366
+ throw Object.assign(new Error(`AUN group delivery does not match channelId=${channelId}`), {
367
+ code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
368
+ });
369
+ }
370
+ return supplied;
371
+ }
372
+ requirePersistedDelivery(channelId, delivery) {
373
+ // Persisted sends only trust the top-level route. In particular, old
374
+ // outbox entries cannot recover routing from their nested reply context.
375
+ return this.requireDelivery(channelId, undefined, delivery);
376
+ }
377
+ withDelivery(context, delivery) {
378
+ return { ...(context ?? {}), delivery };
300
379
  }
301
380
  getShortAid(aid) {
302
381
  if (!aid)
@@ -318,15 +397,19 @@ export class AUNChannel {
318
397
  const name = cached?.name;
319
398
  return name && name !== short ? `${short}(${name})` : short;
320
399
  }
321
- notifyCardActionFailure(channelId, text, operatorId, threadId) {
400
+ notifyCardActionFailure(channelId, text, operatorId, threadId, delivery) {
322
401
  if (!channelId) {
323
402
  logger.warn(`${this.logPrefix()} Card action failure without channelId: ${text}`);
324
403
  return;
325
404
  }
326
- const context = {};
405
+ if (!delivery) {
406
+ logger.warn(`${this.logPrefix()} Card action failure without trusted delivery route: ${text}`);
407
+ return;
408
+ }
409
+ const context = { delivery };
327
410
  if (threadId)
328
411
  context.threadId = threadId;
329
- if (operatorId && this.isGroupId(channelId))
412
+ if (operatorId && delivery.chatType === 'group')
330
413
  context.peerId = operatorId;
331
414
  void this.sendMessage(channelId, text, context).catch((err) => {
332
415
  logger.error(`${this.logPrefix()} Failed to send card action error:`, err);
@@ -336,7 +419,7 @@ export class AUNChannel {
336
419
  logger.info(`${this.logPrefix()} ActionInteraction audit: behavior=${behavior ?? 'reply'} `
337
420
  + `cardId=${cardMessageId} interactionId=${interactionId} result=${result} textLength=${textLength}`);
338
421
  }
339
- extractTextPayload(payload, channelId, senderAid) {
422
+ extractTextPayload(payload, channelId, senderAid, delivery) {
340
423
  if (typeof payload === 'string')
341
424
  return payload;
342
425
  if (payload && typeof payload === 'object') {
@@ -347,6 +430,7 @@ export class AUNChannel {
347
430
  const cardMsgId = typeof obj.ref_message_id === 'string' ? obj.ref_message_id
348
431
  : typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
349
432
  const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
433
+ const cardDelivery = cardInfo?.delivery ?? delivery;
350
434
  if (cardInfo) {
351
435
  // compose 按钮由新版客户端在卡片内采集输入;旧版客户端可继续从主编辑器
352
436
  // 采集。两者最终都回传 action_card_reply{behavior:"compose", text}。
@@ -371,25 +455,25 @@ export class AUNChannel {
371
455
  // payloads omit senderAid. A group channelId is only a conversation
372
456
  // identifier and must never be treated as the clicking operator.
373
457
  const cardClickerAid = senderAid
374
- || (channelId && !this.isGroupId(channelId) ? channelId : '')
458
+ || (cardDelivery?.chatType === 'private' && channelId ? channelId : '')
375
459
  || '';
376
460
  if (cardInfo.isCommandCard) {
377
461
  // CommandCard:action_value 是完整 slash 命令,构造伪入站消息。
378
462
  // 先完成点击者鉴权,再消费 mapping;否则群成员的一次误点会让发起者
379
463
  // 无法继续操作同一张卡片。
380
- const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
464
+ const chatType = cardDelivery?.chatType ?? 'private';
381
465
  if (this.messageHandler && actionValue.startsWith('/')) {
382
466
  // Initiator 校验:仅群聊需要(私聊信道一对一,点击者恒为对端 = initiator)。
383
467
  // 身份只信认证信封提取的 cardClickerAid,非 payload 自报。
384
468
  if (chatType === 'group' && !cardClickerAid) {
385
469
  logger.info(`${this.logPrefix()} CommandCard rejected: missing authenticated clicker mid=${cardMsgId}`);
386
- this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId);
470
+ this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId, cardDelivery);
387
471
  return '';
388
472
  }
389
473
  if (chatType === 'group' && cardInfo.initiatorAid
390
474
  && cardClickerAid !== cardInfo.initiatorAid) {
391
475
  logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
392
- this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
476
+ this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId, cardDelivery);
393
477
  return '';
394
478
  }
395
479
  // 已通过鉴权并即将进入处理流程;此时才消费 mapping,避免非发起者
@@ -408,28 +492,28 @@ export class AUNChannel {
408
492
  })).catch((err) => {
409
493
  logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
410
494
  const message = err instanceof Error && err.message ? err.message : String(err || '未知错误');
411
- this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId);
495
+ this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId, cardDelivery);
412
496
  });
413
497
  }
414
498
  else if (!this.messageHandler) {
415
- this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId);
499
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId, cardDelivery);
416
500
  }
417
501
  else {
418
- this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId);
502
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId, cardDelivery);
419
503
  }
420
504
  }
421
505
  else {
422
506
  // ActionInteraction:后端明确接受后才消费 mapping。撤权、过期或
423
507
  // 重复点击返回 false 时保持 fail-closed,并提示用户卡片已失效。
424
- if (channelId && this.isGroupId(channelId) && !cardClickerAid) {
508
+ if (cardDelivery?.chatType === 'group' && !cardClickerAid) {
425
509
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_unauthenticated', text.length);
426
- this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId);
510
+ this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId, cardDelivery);
427
511
  return '';
428
512
  }
429
- if (channelId && this.isGroupId(channelId) && cardInfo.initiatorAid
513
+ if (cardDelivery?.chatType === 'group' && cardInfo.initiatorAid
430
514
  && cardClickerAid !== cardInfo.initiatorAid) {
431
515
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_non_initiator', text.length);
432
- this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
516
+ this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId, cardDelivery);
433
517
  return '';
434
518
  }
435
519
  // Compose reply 入参校验:text 必须非空且未超长。
@@ -439,19 +523,19 @@ export class AUNChannel {
439
523
  const MAX_COMPOSE_TEXT_LENGTH = 64 * 1024; // 64K 字符
440
524
  if (!trimmedText) {
441
525
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_empty', text.length);
442
- this.notifyCardActionFailure(channelId, '⚠️ 输入内容不能为空,请重新输入', cardClickerAid, threadId);
526
+ this.notifyCardActionFailure(channelId, '⚠️ 输入内容不能为空,请重新输入', cardClickerAid, threadId, cardDelivery);
443
527
  return '';
444
528
  }
445
529
  if (text.length > MAX_COMPOSE_TEXT_LENGTH) {
446
530
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_too_long', text.length);
447
- this.notifyCardActionFailure(channelId, `⚠️ 输入内容过长(${text.length} 字符,限制 ${MAX_COMPOSE_TEXT_LENGTH}),请缩短后重试`, cardClickerAid, threadId);
531
+ this.notifyCardActionFailure(channelId, `⚠️ 输入内容过长(${text.length} 字符,限制 ${MAX_COMPOSE_TEXT_LENGTH}),请缩短后重试`, cardClickerAid, threadId, cardDelivery);
448
532
  return '';
449
533
  }
450
534
  }
451
535
  if (this.interactionCallback) {
452
536
  if (this.interactionActionsInFlight.has(cardMsgId)) {
453
537
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_in_flight', text.length);
454
- this.notifyCardActionFailure(channelId, '⏳ 操作正在处理,请勿重复点击', cardClickerAid, threadId);
538
+ this.notifyCardActionFailure(channelId, '⏳ 操作正在处理,请勿重复点击', cardClickerAid, threadId, cardDelivery);
455
539
  return '';
456
540
  }
457
541
  this.interactionActionsInFlight.add(cardMsgId);
@@ -484,7 +568,7 @@ export class AUNChannel {
484
568
  // captured the message id before the callback returned.
485
569
  this.forgetInteractionCard(cardMsgId, cardInfo.requestId);
486
570
  }
487
- this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', cardClickerAid, threadId);
571
+ this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', cardClickerAid, threadId, cardDelivery);
488
572
  };
489
573
  try {
490
574
  const result = this.interactionCallback(response);
@@ -493,7 +577,7 @@ export class AUNChannel {
493
577
  this.interactionActionsInFlight.delete(cardMsgId);
494
578
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'callback_error', text.length);
495
579
  logger.error(`${this.logPrefix()} ActionInteraction callback failed: request=${cardInfo.requestId}`, err);
496
- this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId);
580
+ this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId, cardDelivery);
497
581
  });
498
582
  }
499
583
  else {
@@ -504,19 +588,19 @@ export class AUNChannel {
504
588
  this.interactionActionsInFlight.delete(cardMsgId);
505
589
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'callback_error', text.length);
506
590
  logger.error(`${this.logPrefix()} ActionInteraction callback failed: request=${cardInfo.requestId}`, err);
507
- this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId);
591
+ this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId, cardDelivery);
508
592
  }
509
593
  }
510
594
  else {
511
595
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'handler_unavailable', text.length);
512
- this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId);
596
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId, cardDelivery);
513
597
  }
514
598
  }
515
599
  }
516
600
  else if (this.ownedCardMsgIds.has(cardMsgId)) {
517
601
  // 本 agent 发出的卡片,但 entry 已过期(24h TTL)
518
602
  logger.debug(`${this.logPrefix()} action_card_reply expired: cardMsgId=${cardMsgId}`);
519
- this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起');
603
+ this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', undefined, undefined, delivery);
520
604
  }
521
605
  else {
522
606
  // 非本 agent 发出的卡片(broadcast 模式下其他 agent 的卡)→ 静默忽略
@@ -751,8 +835,13 @@ export class AUNChannel {
751
835
  return sanitized;
752
836
  return undefined;
753
837
  }
754
- buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName) {
755
- const replyContext = { metadata: { encrypted, chatmode } };
838
+ buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId) {
839
+ if (!groupId)
840
+ throw new Error('buildGroupReplyContext requires groupId');
841
+ const replyContext = {
842
+ metadata: { encrypted, chatmode },
843
+ delivery: { chatType: 'group', groupId },
844
+ };
756
845
  if (threadId)
757
846
  replyContext.threadId = threadId;
758
847
  replyContext.peerId = senderAid;
@@ -766,6 +855,20 @@ export class AUNChannel {
766
855
  }
767
856
  return replyContext;
768
857
  }
858
+ buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId) {
859
+ const replyContext = {
860
+ metadata: { encrypted, chatmode, refMessageId },
861
+ delivery: { chatType: 'private' },
862
+ peerId: senderAid,
863
+ };
864
+ if (threadId)
865
+ replyContext.threadId = threadId;
866
+ if (messageId)
867
+ replyContext.replyToMessageId = messageId;
868
+ if (topicName && threadId)
869
+ replyContext.metadata.topicName = topicName;
870
+ return replyContext;
871
+ }
769
872
  claimInboundMessage(kind, messageId, seq) {
770
873
  if (!messageId)
771
874
  return true;
@@ -788,9 +891,72 @@ export class AUNChannel {
788
891
  this.inboundSeenMessages.set(dedupeKey, { seenAt: Date.now(), seq });
789
892
  if (seq !== undefined)
790
893
  this.messageSeqMap.set(messageId, seq);
791
- setTimeout(() => this.inboundSeenMessages.delete(dedupeKey), 5 * 60 * 1000);
894
+ this.appendInboundDedupe(dedupeKey, messageId, kind, seq);
895
+ setTimeout(() => this.inboundSeenMessages.delete(dedupeKey), AUN_INBOUND_DEDUP_TTL_MS);
792
896
  return true;
793
897
  }
898
+ inboundDedupeFile() {
899
+ const channelName = this.config.channelName ?? 'aun';
900
+ return channelStatePath(channelName, 'dedup', 'aun', `${encodeURIComponent(this.config.aid)}.jsonl`);
901
+ }
902
+ loadInboundDedupe() {
903
+ const file = this.inboundDedupeFile();
904
+ const cutoff = Date.now() - AUN_INBOUND_DEDUP_TTL_MS;
905
+ let lines;
906
+ try {
907
+ lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
908
+ }
909
+ catch {
910
+ return;
911
+ }
912
+ let needsRewrite = false;
913
+ for (const line of lines) {
914
+ try {
915
+ const entry = JSON.parse(line);
916
+ if (typeof entry.key !== 'string' || typeof entry.seenAt !== 'number' || !Number.isFinite(entry.seenAt) || entry.seenAt <= cutoff) {
917
+ needsRewrite = true;
918
+ continue;
919
+ }
920
+ this.inboundSeenMessages.set(entry.key, {
921
+ seenAt: entry.seenAt,
922
+ seq: typeof entry.seq === 'number' && Number.isFinite(entry.seq) ? entry.seq : undefined,
923
+ });
924
+ }
925
+ catch {
926
+ needsRewrite = true;
927
+ }
928
+ }
929
+ if (needsRewrite)
930
+ this.rewriteInboundDedupe();
931
+ }
932
+ appendInboundDedupe(key, messageId, kind, seq) {
933
+ try {
934
+ const file = this.inboundDedupeFile();
935
+ fs.mkdirSync(path.dirname(file), { recursive: true });
936
+ fs.appendFileSync(file, JSON.stringify({ key, messageId, kind, seenAt: Date.now(), seq }) + '\n');
937
+ }
938
+ catch (error) {
939
+ logger.warn(`${this.logPrefix()} Failed to persist inbound dedup: ${error instanceof Error ? error.message : String(error)}`);
940
+ }
941
+ }
942
+ rewriteInboundDedupe() {
943
+ const cutoff = Date.now() - AUN_INBOUND_DEDUP_TTL_MS;
944
+ for (const [key, value] of this.inboundSeenMessages) {
945
+ if (value.seenAt <= cutoff)
946
+ this.inboundSeenMessages.delete(key);
947
+ }
948
+ try {
949
+ const file = this.inboundDedupeFile();
950
+ fs.mkdirSync(path.dirname(file), { recursive: true });
951
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
952
+ const lines = [...this.inboundSeenMessages].map(([key, value]) => JSON.stringify({ key, seenAt: value.seenAt, seq: value.seq }));
953
+ fs.writeFileSync(tmp, lines.length > 0 ? `${lines.join('\n')}\n` : '');
954
+ fs.renameSync(tmp, file);
955
+ }
956
+ catch (error) {
957
+ logger.warn(`${this.logPrefix()} Failed to compact inbound dedup: ${error instanceof Error ? error.message : String(error)}`);
958
+ }
959
+ }
794
960
  acknowledgeImmediately(messageId, _seq) {
795
961
  // SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
796
962
  // as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
@@ -875,6 +1041,7 @@ export class AUNChannel {
875
1041
  constructor(config) {
876
1042
  this.config = config;
877
1043
  this.agentDir = agentDirPath(config.aid);
1044
+ this.loadInboundDedupe();
878
1045
  if (config.aunTrace) {
879
1046
  this.traceWriter = new LogWriter({
880
1047
  baseName: 'aun',
@@ -972,7 +1139,7 @@ export class AUNChannel {
972
1139
  // gateway metadata cache when well-known is temporarily unavailable.
973
1140
  const configuredGateway = this.config.gatewayUrl || '';
974
1141
  logger.info(`${this.logPrefix()} Initializing: aid=${aidName}, gateway=${configuredGateway || '<sdk-discovery>'}, aun_path=${aunPath}`);
975
- // 构造 AIDStore(slot=evolcore daemon,与 cli/netcheck 共享 evolcore 隔离键)
1142
+ // 构造 AIDStoredaemon 使用独立 slot,CLI/netcheck 不能触碰业务入站游标。
976
1143
  // encryptionSeed / rootCaPath 由 getAidStore 内部注入
977
1144
  const store = await getAidStore({
978
1145
  slotId: SLOT.daemon,
@@ -1025,7 +1192,7 @@ export class AUNChannel {
1025
1192
  for (const id of ids) {
1026
1193
  if (typeof id === 'string') {
1027
1194
  logger.info(`${this.logPrefix()} Message recalled: ${id}`);
1028
- this.recallHandler?.(id);
1195
+ this.recallHandler?.(id, this._aid || this.config.aid);
1029
1196
  }
1030
1197
  }
1031
1198
  }
@@ -1057,7 +1224,7 @@ export class AUNChannel {
1057
1224
  for (const id of ids) {
1058
1225
  if (typeof id === 'string') {
1059
1226
  logger.info(`${this.logPrefix()} Group message recalled: group=${env.group_id ?? ''} mid=${id}`);
1060
- this.recallHandler?.(id);
1227
+ this.recallHandler?.(id, this._aid || this.config.aid);
1061
1228
  }
1062
1229
  }
1063
1230
  }
@@ -1095,7 +1262,7 @@ export class AUNChannel {
1095
1262
  });
1096
1263
  this.trace('OUT', 'client.connect', { gateway: this.gatewayUrl, extra_info: extraInfo });
1097
1264
  await client.connect({
1098
- // connection_kind 默认 long;slot 已由 AID 携带(evolcore daemon)
1265
+ // connection_kind 默认 long;slot 已由 AID 携带(evolcore-daemon)
1099
1266
  // extra_info:互踢诊断名片(0.4.3 公开 connect 已支持透传)
1100
1267
  extra_info: extraInfo,
1101
1268
  // max_attempts=0 = 无限重试(与 Go/Python 对齐),交由 SDK 自己跑指数退避
@@ -1155,7 +1322,9 @@ export class AUNChannel {
1155
1322
  }
1156
1323
  const operationId = postBootstrapWelcomeOperationId(aidName);
1157
1324
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
1158
- if (hasMessageLogOperation(chatDir, operationId) || outbox.findByDedupeKey(aidName, operationId)) {
1325
+ const prepared = outbox.findByDedupeKey(aidName, operationId);
1326
+ if (hasMessageLogOperation(chatDir, operationId)
1327
+ || (prepared?.delivery?.chatType === 'private' && prepared.channelId === owner)) {
1159
1328
  return true;
1160
1329
  }
1161
1330
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
@@ -1211,6 +1380,11 @@ export class AUNChannel {
1211
1380
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
1212
1381
  return hasMessageLogOperation(chatDir, operationId);
1213
1382
  }
1383
+ if (entry.delivery?.chatType !== 'private') {
1384
+ logger.warn(`${this.logPrefix()} Discarding unrouted legacy post-bootstrap welcome: id=${entry.id}`);
1385
+ outbox.remove(aid, entry.id);
1386
+ return false;
1387
+ }
1214
1388
  const agentConfig = loadAgent(aid);
1215
1389
  if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active') {
1216
1390
  logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
@@ -1234,7 +1408,7 @@ export class AUNChannel {
1234
1408
  * @param channelId 下载归属(私聊 fromAid / 群聊 groupId)
1235
1409
  * @param preCollected 已收集的附件(群聊路径会提前 collect,避免重复)
1236
1410
  */
1237
- async processAttachments(payload, baseText, channelId, preCollected) {
1411
+ async processAttachments(payload, baseText, channelId, preCollected, delivery) {
1238
1412
  const rawAttachments = preCollected ?? this.collectAllAttachments(payload);
1239
1413
  const images = [];
1240
1414
  let finalText = baseText;
@@ -1243,7 +1417,7 @@ export class AUNChannel {
1243
1417
  }
1244
1418
  const fileParts = [];
1245
1419
  for (const att of rawAttachments) {
1246
- const filePath = await this.downloadAttachment(att, channelId);
1420
+ const filePath = await this.downloadAttachment(att, channelId, delivery);
1247
1421
  if (!filePath)
1248
1422
  continue;
1249
1423
  const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
@@ -1275,8 +1449,11 @@ export class AUNChannel {
1275
1449
  logger.info(`${this.logPrefix()} [attachments] count=${rawAttachments.length} images=${images.length} files=${fileParts.length}`);
1276
1450
  return { finalText, images };
1277
1451
  }
1278
- async downloadAttachment(att, channelId) {
1279
- const ownerAid = att.owner_aid || (!this.isGroupId(channelId) ? channelId : '') || this._aid || '';
1452
+ async downloadAttachment(att, channelId, delivery) {
1453
+ const ownerAid = att.owner_aid
1454
+ || (delivery?.chatType === 'private' ? channelId : '')
1455
+ || this._aid
1456
+ || '';
1280
1457
  const objectKey = att.object_key;
1281
1458
  if (!objectKey) {
1282
1459
  logger.warn(`${this.logPrefix()} Attachment missing object_key, skipping`);
@@ -1448,6 +1625,7 @@ export class AUNChannel {
1448
1625
  if (!data || typeof data !== 'object')
1449
1626
  return;
1450
1627
  const msg = data;
1628
+ const receivedAt = Date.now();
1451
1629
  // Claim before any payload extraction or command parsing. Retransmits may
1452
1630
  // carry a new seq, but the application identity remains message_id.
1453
1631
  const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
@@ -1467,7 +1645,7 @@ export class AUNChannel {
1467
1645
  ? payload._evolcore_causation_ref
1468
1646
  : undefined;
1469
1647
  const logDescriptor = classifyAunPayloadForLog(payload);
1470
- const text = this.extractTextPayload(payload, fromAid, fromAid);
1648
+ const text = this.extractTextPayload(payload, fromAid, fromAid, { chatType: 'private' });
1471
1649
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1472
1650
  const refMessageId = typeof payload === 'object' && payload !== null && typeof payload.ref_message_id === 'string'
1473
1651
  ? payload.ref_message_id
@@ -1513,7 +1691,7 @@ export class AUNChannel {
1513
1691
  mentions.push({ aid: this._aid });
1514
1692
  }
1515
1693
  // Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
1516
- const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid);
1694
+ const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid, undefined, { chatType: 'private' });
1517
1695
  // 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
1518
1696
  // device_id 仅 SDK 内部多实例去重用,evolcore session 层面跨端共享会话
1519
1697
  const chatId = fromAid;
@@ -1529,7 +1707,7 @@ export class AUNChannel {
1529
1707
  logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${p2pPayloadType} chatId=${chatId} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
1530
1708
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1531
1709
  if (p2pPayloadType === 'action_card_reply') {
1532
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1710
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1533
1711
  return;
1534
1712
  }
1535
1713
  // menu.* 与联系人本人状态查询:需要原始 payload JSON 传递给 bridge。
@@ -1537,18 +1715,18 @@ export class AUNChannel {
1537
1715
  || p2pPayloadType === AUNChannel.CONTACT_REQUEST_QUERY_TYPE
1538
1716
  || p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
1539
1717
  this.acknowledgeImmediately(messageId, seq);
1540
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1718
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1541
1719
  this.dispatchMessage({
1542
1720
  channelId: chatId, userId: fromAid,
1543
1721
  text: JSON.stringify(payload),
1544
- chatType: 'private', messageId, seq,
1722
+ chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
1545
1723
  peerName: displayName || undefined,
1546
1724
  peerType: peerIdentity.type,
1547
1725
  encrypted: msgEncrypted,
1548
1726
  protectedHeaders,
1549
1727
  mentions,
1550
1728
  mentionAids: mentionEntryAids(mentions),
1551
- replyContext: this.buildGroupReplyContext(threadId, fromAid, msgEncrypted, messageId),
1729
+ replyContext: this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
1552
1730
  msgType: logDescriptor.msgType,
1553
1731
  payloadType: logDescriptor.payloadType,
1554
1732
  payloadSummary: logDescriptor.payloadSummary,
@@ -1568,11 +1746,11 @@ export class AUNChannel {
1568
1746
  // 透传 encrypted:bind.response 须与请求的加密/明文对称(见 onMessage 处理)。
1569
1747
  if (p2pPayloadType === AUNChannel.BIND_REQUEST_TYPE) {
1570
1748
  this.acknowledgeImmediately(messageId, seq);
1571
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1749
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1572
1750
  this.dispatchMessage({
1573
1751
  channelId: chatId, userId: fromAid,
1574
1752
  text: JSON.stringify(payload),
1575
- chatType: 'private', messageId, seq,
1753
+ chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
1576
1754
  peerName: displayName || undefined,
1577
1755
  peerType: peerIdentity.type,
1578
1756
  encrypted: msgEncrypted,
@@ -1588,7 +1766,7 @@ export class AUNChannel {
1588
1766
  // payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
1589
1767
  if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
1590
1768
  this.acknowledgeImmediately(messageId, seq);
1591
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1769
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1592
1770
  logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
1593
1771
  return;
1594
1772
  }
@@ -1597,24 +1775,15 @@ export class AUNChannel {
1597
1775
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
1598
1776
  const isSystemP2P = p2pPayloadType === 'event';
1599
1777
  this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
1600
- const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode, refMessageId } };
1601
- if (threadId)
1602
- replyContext.threadId = threadId;
1603
- replyContext.peerId = fromAid;
1604
- if (messageId)
1605
- replyContext.replyToMessageId = messageId;
1606
- // 协议 v2.4 §6.4/§12.1: 话题会话创建时传入 topicName
1607
- if (topicName && threadId) {
1608
- if (!replyContext.metadata)
1609
- replyContext.metadata = {};
1610
- replyContext.metadata.topicName = topicName;
1611
- }
1778
+ const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId);
1612
1779
  this.dispatchMessage({
1613
1780
  channelId: chatId,
1614
1781
  userId: fromAid,
1615
1782
  text: finalText,
1616
1783
  chatType: 'private',
1617
1784
  messageId,
1785
+ receivedAt,
1786
+ gatewaySeq: seq,
1618
1787
  causation,
1619
1788
  seq,
1620
1789
  threadId,
@@ -1637,6 +1806,7 @@ export class AUNChannel {
1637
1806
  if (!data || typeof data !== 'object')
1638
1807
  return;
1639
1808
  const msg = data;
1809
+ const receivedAt = Date.now();
1640
1810
  // Deduplicate at the adapter boundary, before mention/slash routing.
1641
1811
  const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
1642
1812
  const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
@@ -1654,7 +1824,7 @@ export class AUNChannel {
1654
1824
  return;
1655
1825
  const payloadMentionAids = mentionEntryAids(payloadMentionEntries);
1656
1826
  const logDescriptor = classifyAunPayloadForLog(payload);
1657
- const text = this.extractTextPayload(payload, groupId, senderAid);
1827
+ const text = this.extractTextPayload(payload, groupId, senderAid, { chatType: 'group', groupId });
1658
1828
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1659
1829
  const topicName = (typeof payload === 'object' && payload !== null)
1660
1830
  ? this.sanitizeTopicName(payload.topicName)
@@ -1710,7 +1880,7 @@ export class AUNChannel {
1710
1880
  peerName: displayName,
1711
1881
  peerType: peerInfo?.type || 'unknown',
1712
1882
  seq,
1713
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName),
1883
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId),
1714
1884
  createdAt,
1715
1885
  });
1716
1886
  return;
@@ -1720,7 +1890,7 @@ export class AUNChannel {
1720
1890
  {
1721
1891
  const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1722
1892
  if (payloadType === 'action_card_reply') {
1723
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId);
1893
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId, receivedAt, seq);
1724
1894
  return;
1725
1895
  }
1726
1896
  }
@@ -1733,18 +1903,18 @@ export class AUNChannel {
1733
1903
  if (payloadType.startsWith('menu.')) {
1734
1904
  const menuEncrypted = !!env.encrypted;
1735
1905
  this.acknowledgeImmediately(messageId, seq);
1736
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId);
1906
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId, receivedAt, seq);
1737
1907
  this.dispatchMessage({
1738
1908
  channelId: groupId, userId: senderAid,
1739
1909
  text: JSON.stringify(payload),
1740
- chatType: 'group', messageId, seq, groupId,
1910
+ chatType: 'group', messageId, seq, receivedAt, gatewaySeq: seq, groupId,
1741
1911
  encrypted: menuEncrypted,
1742
1912
  protectedHeaders,
1743
1913
  mentions: payloadMentionEntries,
1744
1914
  mentionAids: payloadMentionAids,
1745
1915
  isMentioned: hasMentionAll(payloadMentionEntries)
1746
1916
  || (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
1747
- replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName),
1917
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName, groupId),
1748
1918
  msgType: logDescriptor.msgType,
1749
1919
  payloadType: logDescriptor.payloadType,
1750
1920
  payloadSummary: logDescriptor.payloadSummary,
@@ -1754,7 +1924,7 @@ export class AUNChannel {
1754
1924
  }
1755
1925
  if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
1756
1926
  this.acknowledgeImmediately(messageId, seq);
1757
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId);
1927
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId, receivedAt, seq);
1758
1928
  logger.info(`${this.logPrefix()} Group dropped (type deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
1759
1929
  return;
1760
1930
  }
@@ -1795,7 +1965,7 @@ export class AUNChannel {
1795
1965
  this.pendingEchoMessages.set(messageId, {
1796
1966
  text: echoText,
1797
1967
  channelId: groupId,
1798
- context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined),
1968
+ context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined, groupId),
1799
1969
  receiveTs: Date.now(),
1800
1970
  });
1801
1971
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
@@ -1817,7 +1987,7 @@ export class AUNChannel {
1817
1987
  // TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
1818
1988
  if (enforceMention && !isMentioned && !isOwnHandoff) {
1819
1989
  this.acknowledgeImmediately(messageId, seq);
1820
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId);
1990
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
1821
1991
  logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
1822
1992
  return;
1823
1993
  }
@@ -1838,7 +2008,7 @@ export class AUNChannel {
1838
2008
  }
1839
2009
  const mentions = payloadMentionEntries;
1840
2010
  // Process attachments
1841
- const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments);
2011
+ const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments, { chatType: 'group', groupId });
1842
2012
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
1843
2013
  const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
1844
2014
  const shortAid = this.getShortAid(senderAid);
@@ -1882,6 +2052,8 @@ export class AUNChannel {
1882
2052
  text: finalText,
1883
2053
  chatType: 'group',
1884
2054
  messageId,
2055
+ receivedAt,
2056
+ gatewaySeq: seq,
1885
2057
  seq,
1886
2058
  threadId,
1887
2059
  mentions,
@@ -1889,7 +2061,7 @@ export class AUNChannel {
1889
2061
  isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
1890
2062
  source: isOwnHandoff ? 'handoff' : undefined,
1891
2063
  encrypted: msgEncrypted,
1892
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName),
2064
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId),
1893
2065
  mentionMode: serverMentionMode,
1894
2066
  images: inboundImages.length > 0 ? inboundImages : undefined,
1895
2067
  msgType: logDescriptor.msgType,
@@ -1899,6 +2071,19 @@ export class AUNChannel {
1899
2071
  });
1900
2072
  }
1901
2073
  dispatchMessage(event) {
2074
+ const authenticatedDelivery = event.chatType === 'group'
2075
+ ? { chatType: 'group', groupId: event.groupId ?? event.channelId }
2076
+ : { chatType: 'private' };
2077
+ const suppliedDelivery = event.replyContext?.delivery;
2078
+ const suppliedMatches = suppliedDelivery?.chatType === 'private'
2079
+ ? authenticatedDelivery.chatType === 'private'
2080
+ : suppliedDelivery?.chatType === 'group'
2081
+ && authenticatedDelivery.chatType === 'group'
2082
+ && suppliedDelivery.groupId === authenticatedDelivery.groupId;
2083
+ const delivery = suppliedDelivery && suppliedMatches ? suppliedDelivery : authenticatedDelivery;
2084
+ if (suppliedDelivery && delivery !== suppliedDelivery) {
2085
+ logger.warn(`${this.logPrefix()} Ignoring mismatched inbound reply route: channel=${event.channelId} chatType=${event.chatType}`);
2086
+ }
1902
2087
  // Inbound events are claimed in handleIncomingPrivateMessage / GroupMessage
1903
2088
  // before parsing. Keeping dedupe there prevents duplicate slash commands
1904
2089
  // from reaching routing and mention policy code.
@@ -1926,19 +2111,24 @@ export class AUNChannel {
1926
2111
  this.pendingEchoMessages.set(event.messageId, {
1927
2112
  text: echoText,
1928
2113
  channelId: event.channelId,
1929
- context: event.replyContext ? { metadata: event.replyContext.metadata } : undefined,
2114
+ context: {
2115
+ ...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
2116
+ delivery,
2117
+ },
1930
2118
  receiveTs: Date.now(),
1931
2119
  });
1932
2120
  logger.info(`${this.logPrefix()} [Echo] long echo stored: mid=${event.messageId} channelId=${event.channelId}`);
1933
2121
  }
1934
2122
  if (!this.messageHandler)
1935
2123
  return;
1936
- // Use caller-supplied replyContext (group path builds mentionUserIds);
1937
- // fall back to simple threadId-only context for private messages
1938
- let replyContext = event.replyContext;
1939
- if (!replyContext && event.threadId) {
1940
- replyContext = { threadId: event.threadId };
1941
- }
2124
+ // Preserve the caller-supplied context (group paths add mention fields),
2125
+ // but always attach the route derived from this authenticated event when
2126
+ // a synthetic/test event omitted it. Never infer the route from the AID.
2127
+ const replyContext = {
2128
+ ...(event.replyContext ?? {}),
2129
+ delivery,
2130
+ ...(event.threadId && !event.replyContext?.threadId ? { threadId: event.threadId } : {}),
2131
+ };
1942
2132
  this.messageHandler({
1943
2133
  channelId: event.channelId || '',
1944
2134
  channelType: 'aun',
@@ -1955,6 +2145,8 @@ export class AUNChannel {
1955
2145
  encrypted: event.encrypted,
1956
2146
  protectedHeaders: event.protectedHeaders,
1957
2147
  messageId: event.messageId,
2148
+ receivedAt: event.receivedAt,
2149
+ gatewaySeq: event.gatewaySeq ?? event.seq,
1958
2150
  causation: event.causation,
1959
2151
  threadId: event.threadId,
1960
2152
  mentions: event.mentions,
@@ -1972,7 +2164,7 @@ export class AUNChannel {
1972
2164
  logger.error(`${this.logPrefix()} Message handler error:`, err);
1973
2165
  });
1974
2166
  }
1975
- appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId) {
2167
+ appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq) {
1976
2168
  try {
1977
2169
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
1978
2170
  appendMessageLog(chatDir, buildInboundEntry({
@@ -1987,6 +2179,9 @@ export class AUNChannel {
1987
2179
  msgType: descriptor.msgType,
1988
2180
  payloadType: descriptor.payloadType,
1989
2181
  payloadSummary: descriptor.payloadSummary,
2182
+ timestamp: receivedAt,
2183
+ receivedAt,
2184
+ gatewaySeq,
1990
2185
  }));
1991
2186
  }
1992
2187
  catch (e) {
@@ -2190,7 +2385,14 @@ export class AUNChannel {
2190
2385
  }
2191
2386
  const replyTarget = event.channelId;
2192
2387
  // 不传 peerId,避免 sendMessage 在头部追加 @peer
2193
- const context = { threadId: event.threadId, metadata: event.replyContext?.metadata };
2388
+ const authenticatedDelivery = event.chatType === 'group'
2389
+ ? { chatType: 'group', groupId: event.channelId }
2390
+ : { chatType: 'private' };
2391
+ const context = {
2392
+ threadId: event.threadId,
2393
+ metadata: event.replyContext?.metadata,
2394
+ delivery: authenticatedDelivery,
2395
+ };
2194
2396
  const sendStart = Date.now();
2195
2397
  this.isEchoSending = true;
2196
2398
  this.sendMessage(replyTarget, echoText, context).then(() => {
@@ -2606,19 +2808,100 @@ export class AUNChannel {
2606
2808
  mentions: normalizeAunMentionEntries(payload.mentions),
2607
2809
  };
2608
2810
  }
2811
+ /**
2812
+ * Keep thought payloads below the encrypted AUN envelope limit. Thought
2813
+ * payloads are usually activity records, so preserving their structure and
2814
+ * truncating large string leaves is more useful than dropping the item.
2815
+ */
2816
+ limitThoughtPayload(payload) {
2817
+ let serialized;
2818
+ try {
2819
+ serialized = JSON.stringify(payload);
2820
+ }
2821
+ catch {
2822
+ return { type: 'thought', text: '[thought unavailable: payload is not serializable]' };
2823
+ }
2824
+ if (typeof serialized !== 'string') {
2825
+ return { type: 'thought', text: '[thought unavailable: payload is not serializable]' };
2826
+ }
2827
+ if (Buffer.byteLength(serialized, 'utf8') <= AUN_THOUGHT_PLAINTEXT_MAX_BYTES)
2828
+ return payload;
2829
+ const limited = JSON.parse(serialized);
2830
+ limited.thought_truncated = true;
2831
+ const stringLeaves = () => {
2832
+ const leaves = [];
2833
+ const stack = [limited];
2834
+ while (stack.length > 0) {
2835
+ const current = stack.pop();
2836
+ if (!current || typeof current !== 'object')
2837
+ continue;
2838
+ for (const key of Object.keys(current)) {
2839
+ const value = current[key];
2840
+ if (typeof value === 'string') {
2841
+ leaves.push({ parent: current, key, value, bytes: Buffer.byteLength(value, 'utf8') });
2842
+ }
2843
+ else if (value && typeof value === 'object') {
2844
+ stack.push(value);
2845
+ }
2846
+ }
2847
+ }
2848
+ return leaves;
2849
+ };
2850
+ let limitedBytes = Buffer.byteLength(JSON.stringify(limited), 'utf8');
2851
+ while (limitedBytes > AUN_THOUGHT_PLAINTEXT_MAX_BYTES) {
2852
+ const leaf = stringLeaves().sort((a, b) => b.bytes - a.bytes)[0];
2853
+ if (!leaf || leaf.bytes === 0)
2854
+ break;
2855
+ if (!Array.isArray(leaf.parent) && typeof leaf.key === 'string') {
2856
+ leaf.parent[`${leaf.key}_truncated`] = true;
2857
+ }
2858
+ limitedBytes = Buffer.byteLength(JSON.stringify(limited), 'utf8');
2859
+ const bytesToRemove = limitedBytes - AUN_THOUGHT_PLAINTEXT_MAX_BYTES + 32;
2860
+ const targetBytes = Math.max(0, leaf.bytes - bytesToRemove);
2861
+ const source = leaf.value.endsWith(AUN_THOUGHT_TRUNCATION_SUFFIX)
2862
+ ? leaf.value.slice(0, -AUN_THOUGHT_TRUNCATION_SUFFIX.length)
2863
+ : leaf.value;
2864
+ leaf.parent[leaf.key] = truncateUtf8WithSuffix(source, targetBytes, AUN_THOUGHT_TRUNCATION_SUFFIX);
2865
+ const nextBytes = Buffer.byteLength(JSON.stringify(limited), 'utf8');
2866
+ if (nextBytes >= limitedBytes)
2867
+ break;
2868
+ limitedBytes = nextBytes;
2869
+ }
2870
+ if (limitedBytes <= AUN_THOUGHT_PLAINTEXT_MAX_BYTES) {
2871
+ logger.info(`${this.logPrefix()} Thought payload truncated: ${Buffer.byteLength(serialized, 'utf8')} -> ${limitedBytes} bytes`);
2872
+ return limited;
2873
+ }
2874
+ return this.thoughtPayloadFallback(limited);
2875
+ }
2876
+ thoughtPayloadFallback(payload) {
2877
+ const text = this.activityLogText(payload);
2878
+ const fallback = {
2879
+ type: 'thought',
2880
+ text: truncateUtf8WithSuffix(text, 256, AUN_THOUGHT_TRUNCATION_SUFFIX),
2881
+ thought_truncated: true,
2882
+ };
2883
+ if (typeof payload.chatmode === 'string')
2884
+ fallback.chatmode = truncateUtf8ToBytes(payload.chatmode, 32);
2885
+ logger.info(`${this.logPrefix()} Thought payload reduced to fallback (${Buffer.byteLength(JSON.stringify(fallback), 'utf8')} bytes)`);
2886
+ return fallback;
2887
+ }
2609
2888
  rememberInvalidatedInteraction(interactionId, reason) {
2610
2889
  this.invalidatedInteractions.set(interactionId, reason);
2611
2890
  const timer = setTimeout(() => this.invalidatedInteractions.delete(interactionId), 24 * 60 * 60 * 1000);
2612
2891
  timer.unref?.();
2613
2892
  }
2614
- async recallInteractionCard(messageId, channelId) {
2893
+ async recallInteractionCard(messageId, channelId, delivery) {
2615
2894
  if (!this.client || !this.connected)
2616
2895
  return false;
2617
2896
  try {
2618
- const isGroup = channelId ? this.isGroupId(channelId) : false;
2897
+ if (!channelId || !delivery) {
2898
+ logger.warn(`${this.logPrefix()} Cannot recall interaction card without trusted delivery route: mid=${messageId}`);
2899
+ return false;
2900
+ }
2901
+ const isGroup = delivery.chatType === 'group';
2619
2902
  const method = isGroup ? 'group.recall' : 'message.recall';
2620
2903
  const params = isGroup
2621
- ? { group_id: channelId, message_ids: [messageId] }
2904
+ ? { group_id: delivery.groupId, message_ids: [messageId] }
2622
2905
  : { message_ids: [messageId] };
2623
2906
  await this.callAndTrace(method, params, { silentError: true });
2624
2907
  return true;
@@ -2641,9 +2924,10 @@ export class AUNChannel {
2641
2924
  }
2642
2925
  const messageIds = [...(this.interactionCardMessageIds.get(interactionId) ?? [])];
2643
2926
  for (const messageId of messageIds) {
2644
- const channelId = this.cardMessageIdMap.get(messageId)?.channelId;
2927
+ const card = this.cardMessageIdMap.get(messageId);
2928
+ const channelId = card?.channelId;
2645
2929
  this.forgetInteractionCard(messageId, interactionId);
2646
- await this.recallInteractionCard(messageId, channelId);
2930
+ await this.recallInteractionCard(messageId, channelId, card?.delivery);
2647
2931
  }
2648
2932
  }
2649
2933
  forgetInteractionCard(messageId, interactionId) {
@@ -2653,7 +2937,7 @@ export class AUNChannel {
2653
2937
  if (ids && ids.size === 0)
2654
2938
  this.interactionCardMessageIds.delete(interactionId);
2655
2939
  }
2656
- registerCardPostSend(messageId, action, channelId) {
2940
+ registerCardPostSend(messageId, action, channelId, delivery) {
2657
2941
  if (action.type !== 'register_interaction_card')
2658
2942
  return;
2659
2943
  this.cardMessageIdMap.set(messageId, {
@@ -2661,6 +2945,7 @@ export class AUNChannel {
2661
2945
  isCommandCard: action.isCommandCard,
2662
2946
  initiatorAid: action.initiatorAid,
2663
2947
  channelId,
2948
+ delivery: delivery ?? action.delivery,
2664
2949
  });
2665
2950
  this.ownedCardMsgIds.add(messageId);
2666
2951
  let messageIds = this.interactionCardMessageIds.get(action.requestId);
@@ -2686,17 +2971,18 @@ export class AUNChannel {
2686
2971
  const invalidationReason = this.invalidatedInteractions.get(action.requestId);
2687
2972
  if (invalidationReason) {
2688
2973
  this.cardMessageIdMap.delete(messageId);
2689
- void this.recallInteractionCard(messageId, channelId);
2974
+ void this.recallInteractionCard(messageId, channelId, delivery ?? action.delivery);
2690
2975
  }
2691
2976
  }
2692
2977
  runPostSend(entry, messageId) {
2693
2978
  if (!entry.postSend)
2694
2979
  return;
2695
2980
  if (entry.postSend.type === 'register_interaction_card') {
2696
- this.registerCardPostSend(messageId, entry.postSend, entry.channelId);
2981
+ this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery);
2697
2982
  }
2698
2983
  }
2699
2984
  async sendAunPayload(channelId, payload, context, label) {
2985
+ const delivery = this.requireDelivery(channelId, context);
2700
2986
  let normalizedPayload;
2701
2987
  try {
2702
2988
  normalizedPayload = this.normalizeAunPayloadMentions(payload);
@@ -2709,7 +2995,7 @@ export class AUNChannel {
2709
2995
  }
2710
2996
  if (!this.client || !this.connected)
2711
2997
  return { ok: false };
2712
- const isGroup = this.isGroupId(channelId);
2998
+ const isGroup = delivery.chatType === 'group';
2713
2999
  const targetAid = channelId;
2714
3000
  const encryptTarget = isGroup ? channelId : targetAid;
2715
3001
  const encrypt = context?.metadata?.encrypted != null
@@ -2727,7 +3013,7 @@ export class AUNChannel {
2727
3013
  const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
2728
3014
  const params = { payload: truncatedPayload, encrypt };
2729
3015
  if (isGroup)
2730
- params.group_id = channelId;
3016
+ params.group_id = delivery.groupId;
2731
3017
  else
2732
3018
  params.to = targetAid;
2733
3019
  const callOnce = async (sendParams, fallback) => {
@@ -2797,6 +3083,8 @@ export class AUNChannel {
2797
3083
  this.forwardOutbound(result);
2798
3084
  }
2799
3085
  async sendContentPayload(channelId, payload, opts) {
3086
+ const delivery = this.requireDelivery(channelId, opts.context);
3087
+ const context = this.withDelivery(opts.context, delivery);
2800
3088
  // Validate the caller's raw mentions before any early-return policy (such
2801
3089
  // as a cancelled interaction) can hide a malformed wire payload.
2802
3090
  const validatedPayload = this.normalizeAunPayloadMentions(payload);
@@ -2812,14 +3100,15 @@ export class AUNChannel {
2812
3100
  // Validate the caller's raw mentions before deep-cleaning: that helper
2813
3101
  // intentionally removes undefined array elements, which would otherwise
2814
3102
  // turn malformed `mentions: [undefined]` into an apparently valid `[]`.
2815
- const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, opts.context));
3103
+ const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, context));
2816
3104
  const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
2817
3105
  const entry = outbox.enqueue(this.config.aid, {
2818
3106
  channelId,
3107
+ delivery,
2819
3108
  type: 'payload',
2820
3109
  contentKind: opts.contentKind,
2821
3110
  payload: finalPayload,
2822
- context: opts.context,
3111
+ context,
2823
3112
  logText,
2824
3113
  ttl: opts.ttl,
2825
3114
  postSend: opts.postSend,
@@ -2966,13 +3255,15 @@ export class AUNChannel {
2966
3255
  logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
2967
3256
  return;
2968
3257
  }
3258
+ const delivery = this.requireDelivery(channelId, context);
3259
+ const routedContext = this.withDelivery(context, delivery);
2969
3260
  // 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
2970
3261
  if (!this.isEchoSending) {
2971
- await this.flushPendingEcho(channelId);
3262
+ await this.flushPendingEcho(channelId, delivery);
2972
3263
  }
2973
3264
  let finalText = text;
2974
3265
  this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
2975
- if (this.isGroupId(channelId)) {
3266
+ if (delivery.chatType === 'group') {
2976
3267
  const requestedMentions = Array.from(new Set((context?.mentionUserIds ?? [])
2977
3268
  .filter((id) => typeof id === 'string' && id.trim().length > 0)
2978
3269
  .map(id => id.trim())));
@@ -2995,24 +3286,31 @@ export class AUNChannel {
2995
3286
  }
2996
3287
  }
2997
3288
  }
2998
- const operationId = typeof context?.metadata?.operationId === 'string'
2999
- ? context.metadata.operationId
3289
+ const operationId = typeof routedContext.metadata?.operationId === 'string'
3290
+ ? routedContext.metadata.operationId
3000
3291
  : undefined;
3001
3292
  if (operationId) {
3002
3293
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
3003
- if (hasMessageLogOperation(chatDir, operationId))
3294
+ if (hasMessageLogOperation(chatDir, operationId)) {
3295
+ const duplicate = outbox.findByDedupeKey(this.config.aid, operationId);
3296
+ if (duplicate) {
3297
+ outbox.remove(this.config.aid, duplicate.id);
3298
+ logger.info(`${this.logPrefix()} Removed stale outbox entry for completed operation: ${operationId}`);
3299
+ }
3004
3300
  return;
3301
+ }
3005
3302
  }
3006
3303
  // Write-ahead: persist to outbox before attempting send
3007
3304
  const entry = outbox.enqueue(this.config.aid, {
3008
3305
  channelId,
3306
+ delivery,
3009
3307
  dedupeKey: operationId,
3010
- critical: context?.metadata?.criticalDelivery === true,
3308
+ critical: routedContext.metadata?.criticalDelivery === true,
3011
3309
  type: 'text',
3012
3310
  text: finalText,
3013
- context,
3014
- ttl: typeof context?.metadata?.outboxTtl === 'number'
3015
- ? context.metadata.outboxTtl
3311
+ context: routedContext,
3312
+ ttl: typeof routedContext.metadata?.outboxTtl === 'number'
3313
+ ? routedContext.metadata.outboxTtl
3016
3314
  : undefined,
3017
3315
  });
3018
3316
  logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
@@ -3036,8 +3334,8 @@ export class AUNChannel {
3036
3334
  }
3037
3335
  /** Daemon-side transport for `ec msg send` running inside an agent task. */
3038
3336
  async sendDaemonMsg(args) {
3039
- if (!args.to || this.isGroupId(args.to)) {
3040
- return { ok: false, error: 'aun-msg-send only supports private AID targets', code: 'UNSUPPORTED_TARGET' };
3337
+ if (!args.to) {
3338
+ return { ok: false, error: 'aun-msg-send requires a private target', code: 'UNSUPPORTED_TARGET' };
3041
3339
  }
3042
3340
  if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
3043
3341
  return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
@@ -3161,8 +3459,8 @@ export class AUNChannel {
3161
3459
  }
3162
3460
  /** Daemon-side transport for `ec group send` running inside an agent task. */
3163
3461
  async sendDaemonGroupMsg(args) {
3164
- if (!args.groupId || !this.isGroupId(args.groupId)) {
3165
- return { ok: false, error: 'aun-msg-send requires a valid group target', code: 'UNSUPPORTED_TARGET' };
3462
+ if (!args.groupId) {
3463
+ return { ok: false, error: 'aun group send requires a group target', code: 'UNSUPPORTED_TARGET' };
3166
3464
  }
3167
3465
  if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
3168
3466
  return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
@@ -3246,7 +3544,7 @@ export class AUNChannel {
3246
3544
  return { ok: false, error: e?.message || String(e), code: e?.code };
3247
3545
  }
3248
3546
  }
3249
- async flushPendingEcho(channelId) {
3547
+ async flushPendingEcho(channelId, delivery) {
3250
3548
  // 查找该 channelId 是否有 pending echo(长 echo 等待 agent 首次回复)
3251
3549
  for (const [key, echo] of this.pendingEchoMessages) {
3252
3550
  if (echo.channelId === channelId) {
@@ -3270,18 +3568,32 @@ export class AUNChannel {
3270
3568
  ts: Date.now(),
3271
3569
  aid: this.config.aid,
3272
3570
  channelId,
3571
+ delivery,
3273
3572
  type: 'text',
3274
3573
  text: echoText,
3574
+ context: this.withDelivery(echo.context, delivery),
3275
3575
  ttl: 300_000,
3276
3576
  };
3277
3577
  const ok = await this.deliverTextEntry(echoEntry);
3278
3578
  if (!ok) {
3279
- outbox.enqueue(this.config.aid, { channelId, type: 'text', text: echoText });
3579
+ outbox.enqueue(this.config.aid, {
3580
+ channelId,
3581
+ delivery,
3582
+ type: 'text',
3583
+ text: echoText,
3584
+ context: this.withDelivery(echo.context, delivery),
3585
+ });
3280
3586
  }
3281
3587
  logger.info(`${this.logPrefix()} [Echo] long echo trace delivered=${ok} to ${channelId} (agent ${agentDuration}ms)`);
3282
3588
  }
3283
3589
  else {
3284
- outbox.enqueue(this.config.aid, { channelId, type: 'text', text: echoText });
3590
+ outbox.enqueue(this.config.aid, {
3591
+ channelId,
3592
+ delivery,
3593
+ type: 'text',
3594
+ text: echoText,
3595
+ context: this.withDelivery(echo.context, delivery),
3596
+ });
3285
3597
  logger.warn(`${this.logPrefix()} [Echo] not connected, echo trace queued in outbox`);
3286
3598
  }
3287
3599
  break;
@@ -3291,7 +3603,8 @@ export class AUNChannel {
3291
3603
  async deliverTextEntry(entry) {
3292
3604
  const channelId = entry.channelId;
3293
3605
  const finalText = entry.text;
3294
- const context = entry.context;
3606
+ const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
3607
+ const context = this.withDelivery(entry.context, delivery);
3295
3608
  const operationId = entry.dedupeKey
3296
3609
  || (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
3297
3610
  if (operationId) {
@@ -3309,7 +3622,7 @@ export class AUNChannel {
3309
3622
  // 从 context.metadata.source 读取 source,默认为 'daemon'
3310
3623
  const source = context?.metadata?.source ?? 'daemon';
3311
3624
  const payload = { type: 'text', text: finalText };
3312
- if (this.isGroupId(channelId)) {
3625
+ if (delivery.chatType === 'group') {
3313
3626
  const extracted = this.extractMentionEntriesFromText(finalText);
3314
3627
  if (extracted.length > 0)
3315
3628
  payload.mentions = extracted;
@@ -3323,7 +3636,7 @@ export class AUNChannel {
3323
3636
  if (context?.metadata?.chatmode)
3324
3637
  payload.chatmode = context.metadata.chatmode;
3325
3638
  const causation = normalizeCausation(context?.metadata?.causation);
3326
- const causationRef = !this.isGroupId(channelId) && causation
3639
+ const causationRef = delivery.chatType !== 'group' && causation
3327
3640
  ? `caus_${crypto.randomUUID().replace(/-/g, '')}`
3328
3641
  : undefined;
3329
3642
  if (causationRef && causation) {
@@ -3332,7 +3645,7 @@ export class AUNChannel {
3332
3645
  }
3333
3646
  // 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
3334
3647
  logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${finalText.length}`);
3335
- const isGroup = this.isGroupId(channelId);
3648
+ const isGroup = delivery.chatType === 'group';
3336
3649
  const targetAid = channelId;
3337
3650
  if (operationId && entry.deliveryReceipt) {
3338
3651
  this.appendOutboundJsonl(channelId, {
@@ -3506,7 +3819,8 @@ export class AUNChannel {
3506
3819
  }
3507
3820
  const contentKind = entry.contentKind;
3508
3821
  const logText = entry.logText ?? this.payloadLogText(payload, contentKind);
3509
- const context = entry.context;
3822
+ const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
3823
+ const context = this.withDelivery(entry.context, delivery);
3510
3824
  logger.info(`${this.logPrefix()} deliverPayloadEntry: id=${entry.id} kind=${contentKind ?? payload.type ?? 'payload'} channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} textLen=${logText.length}`);
3511
3825
  const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
3512
3826
  if (sent.discarded) {
@@ -3515,7 +3829,7 @@ export class AUNChannel {
3515
3829
  }
3516
3830
  if (!sent.ok || !sent.messageId)
3517
3831
  return sent;
3518
- const isGroup = this.isGroupId(channelId);
3832
+ const isGroup = delivery.chatType === 'group';
3519
3833
  const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
3520
3834
  this.logAunSendAccepted(isGroup ? 'group.send' : 'message.send', isGroup ? channelId : this.peerLabel(channelId), sent.messageId, !!sent.encrypt, sent.result, logText);
3521
3835
  this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result, statsContext);
@@ -3577,6 +3891,8 @@ export class AUNChannel {
3577
3891
  async sendThought(channelId, taskId, payload, context) {
3578
3892
  if (!taskId)
3579
3893
  return;
3894
+ const delivery = this.requireDelivery(channelId, context);
3895
+ context = this.withDelivery(context, delivery);
3580
3896
  const validatedPayload = this.normalizeAunPayloadMentions(payload);
3581
3897
  const finalPayload = this.normalizeAunPayloadMentions(this.stripUndefinedDeep(validatedPayload));
3582
3898
  if (!this.connected || !this.client)
@@ -3586,18 +3902,17 @@ export class AUNChannel {
3586
3902
  const encrypt = context?.metadata?.encrypted != null
3587
3903
  ? !!(context.metadata.encrypted)
3588
3904
  : this.shouldEncrypt(targetId);
3905
+ const payloadWithChatmode = context?.metadata?.chatmode && !finalPayload.chatmode
3906
+ ? { ...finalPayload, chatmode: context.metadata.chatmode }
3907
+ : finalPayload;
3589
3908
  const params = {
3590
3909
  context: { type: 'task', id: taskId },
3591
- payload: finalPayload,
3910
+ payload: this.limitThoughtPayload(payloadWithChatmode),
3592
3911
  encrypt,
3593
3912
  };
3594
- // 补齐 chatmode(与 deliverTextEntry/applyReplyContextToPayload 对齐)
3595
- // 前端强依赖此字段用于消息过滤(proactive 模式的 thought 不显示在聊天流)
3596
- if (context?.metadata?.chatmode && !params.payload.chatmode) {
3597
- params.payload.chatmode = context.metadata.chatmode;
3598
- }
3599
3913
  try {
3600
- const items = finalPayload?.items;
3914
+ let thoughtPayload = params.payload;
3915
+ const items = thoughtPayload?.items;
3601
3916
  const itemCount = Array.isArray(items) ? items.length : 1;
3602
3917
  const stage = finalPayload?.stage ?? (finalPayload?.kind ? `kind=${finalPayload.kind}` : `items=${itemCount}`);
3603
3918
  // 提取 thought 文本:兼容旧 items[] 和新扁平 activity payload。
@@ -3619,12 +3934,28 @@ export class AUNChannel {
3619
3934
  }
3620
3935
  }
3621
3936
  else {
3622
- thoughtText = this.activityLogText(finalPayload);
3937
+ thoughtText = this.activityLogText(thoughtPayload);
3623
3938
  }
3624
- if (this.isGroupId(channelId)) {
3625
- params.group_id = targetId;
3626
- const putRes = await this.callAndTrace('group.thought.put', params);
3627
- const tid = putRes?.thought_id;
3939
+ const method = delivery.chatType === 'group' ? 'group.thought.put' : 'message.thought.put';
3940
+ if (delivery.chatType === 'group')
3941
+ params.group_id = delivery.groupId;
3942
+ else
3943
+ params.to = targetId;
3944
+ let putRes;
3945
+ try {
3946
+ putRes = await this.callAndTrace(method, params);
3947
+ }
3948
+ catch (error) {
3949
+ if (!isThoughtTooLargeError(error))
3950
+ throw error;
3951
+ thoughtPayload = this.thoughtPayloadFallback(thoughtPayload);
3952
+ params.payload = thoughtPayload;
3953
+ thoughtText = this.activityLogText(thoughtPayload);
3954
+ logger.warn(`${this.logPrefix()} Retrying ${method} with minimal payload after server size rejection`);
3955
+ putRes = await this.callAndTrace(method, params);
3956
+ }
3957
+ const tid = putRes?.thought_id;
3958
+ if (delivery.chatType === 'group') {
3628
3959
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
3629
3960
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
3630
3961
  this.forwardOutbound(putRes);
@@ -3634,9 +3965,6 @@ export class AUNChannel {
3634
3965
  }
3635
3966
  }
3636
3967
  else {
3637
- params.to = targetId;
3638
- const putRes = await this.callAndTrace('message.thought.put', params);
3639
- const tid = putRes?.thought_id;
3640
3968
  logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
3641
3969
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
3642
3970
  this.forwardOutbound(putRes);
@@ -3652,13 +3980,15 @@ export class AUNChannel {
3652
3980
  }
3653
3981
  /** 发送结构化 AUN payload;transient 协议载荷不写本地 messages.jsonl。 */
3654
3982
  async sendStructured(channelId, payload, context) {
3983
+ const delivery = this.requireDelivery(channelId, context);
3984
+ context = this.withDelivery(context, delivery);
3655
3985
  // Validate before checking connection state so malformed producer payloads
3656
3986
  // are reported deterministically instead of being hidden by a disconnect.
3657
3987
  const validatedPayload = this.normalizeAunPayloadMentions(payload);
3658
3988
  const finalPayload = this.stripUndefinedDeep(validatedPayload);
3659
3989
  if (!this.connected || !this.client)
3660
3990
  return null;
3661
- const isGroup = this.isGroupId(channelId);
3991
+ const isGroup = delivery.chatType === 'group';
3662
3992
  const targetAid = channelId;
3663
3993
  const encryptTarget = isGroup ? channelId : targetAid;
3664
3994
  const encrypt = context?.metadata?.encrypted != null
@@ -3684,7 +4014,7 @@ export class AUNChannel {
3684
4014
  }
3685
4015
  try {
3686
4016
  if (isGroup) {
3687
- params.group_id = channelId;
4017
+ params.group_id = delivery.groupId;
3688
4018
  const result = await this.callAndTrace('group.send', params);
3689
4019
  const mid = result?.message?.message_id ?? result?.message_id ?? null;
3690
4020
  logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
@@ -3708,6 +4038,8 @@ export class AUNChannel {
3708
4038
  }
3709
4039
  }
3710
4040
  async sendFile(channelId, filePath, context) {
4041
+ const delivery = this.requireDelivery(channelId, context);
4042
+ context = this.withDelivery(context, delivery);
3711
4043
  const absPath = path.resolve(filePath);
3712
4044
  if (!fs.existsSync(absPath)) {
3713
4045
  logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
@@ -3725,6 +4057,7 @@ export class AUNChannel {
3725
4057
  // Write-ahead: persist to outbox
3726
4058
  const entry = outbox.enqueue(this.config.aid, {
3727
4059
  channelId,
4060
+ delivery,
3728
4061
  type: 'file',
3729
4062
  filePath: absPath,
3730
4063
  context,
@@ -3743,6 +4076,8 @@ export class AUNChannel {
3743
4076
  }
3744
4077
  }
3745
4078
  async sendImage(channelId, data, mimeType, alt, context) {
4079
+ const delivery = this.requireDelivery(channelId, context);
4080
+ context = this.withDelivery(context, delivery);
3746
4081
  if (!Buffer.isBuffer(data)) {
3747
4082
  throw new Error('result.image data must be a Buffer');
3748
4083
  }
@@ -3764,6 +4099,7 @@ export class AUNChannel {
3764
4099
  const logText = alt ? `[image] ${alt}` : '[image]';
3765
4100
  const entry = outbox.enqueue(this.config.aid, {
3766
4101
  channelId,
4102
+ delivery,
3767
4103
  type: 'image',
3768
4104
  contentKind: 'image',
3769
4105
  image: { dataBase64: data.toString('base64'), mimeType: normalizedMimeType, alt },
@@ -3822,6 +4158,9 @@ export class AUNChannel {
3822
4158
  logger.warn(`${this.logPrefix()} deliverImageEntry: missing image data (outbox id=${entry.id})`);
3823
4159
  return true;
3824
4160
  }
4161
+ const delivery = this.requirePersistedDelivery(entry.channelId, entry.delivery);
4162
+ entry.delivery = delivery;
4163
+ entry.context = this.withDelivery(entry.context, delivery);
3825
4164
  const data = Buffer.from(image.dataBase64, 'base64');
3826
4165
  if (data.length === 0) {
3827
4166
  logger.warn(`${this.logPrefix()} deliverImageEntry: invalid image data (outbox id=${entry.id})`);
@@ -3839,6 +4178,7 @@ export class AUNChannel {
3839
4178
  if (!outbox.replace(this.config.aid, entry)) {
3840
4179
  payloadEntry = outbox.enqueue(this.config.aid, {
3841
4180
  channelId: entry.channelId,
4181
+ delivery,
3842
4182
  type: 'payload',
3843
4183
  contentKind: 'image',
3844
4184
  payload: entry.payload,
@@ -3856,7 +4196,8 @@ export class AUNChannel {
3856
4196
  async deliverFileEntry(entry) {
3857
4197
  const channelId = entry.channelId;
3858
4198
  const absPath = entry.filePath;
3859
- const context = entry.context;
4199
+ const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
4200
+ const context = this.withDelivery(entry.context, delivery);
3860
4201
  if (!fs.existsSync(absPath)) {
3861
4202
  logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
3862
4203
  return true; // remove from outbox, file no longer exists
@@ -3914,7 +4255,7 @@ export class AUNChannel {
3914
4255
  attachment,
3915
4256
  context,
3916
4257
  });
3917
- const isGroup = this.isGroupId(channelId);
4258
+ const isGroup = delivery.chatType === 'group';
3918
4259
  const fileTargetAid = channelId;
3919
4260
  const encryptTarget = isGroup ? channelId : fileTargetAid;
3920
4261
  const encrypt = context?.metadata?.encrypted != null
@@ -3925,7 +4266,7 @@ export class AUNChannel {
3925
4266
  let sentMid = null;
3926
4267
  try {
3927
4268
  if (isGroup) {
3928
- params.group_id = channelId;
4269
+ params.group_id = delivery.groupId;
3929
4270
  this.trace('OUT', 'group.send.file', params);
3930
4271
  const result = await this.client.call('group.send', params);
3931
4272
  sendResult = result;
@@ -4033,6 +4374,10 @@ export class AUNChannel {
4033
4374
  return;
4034
4375
  logger.info(`${this.logPrefix()} Draining outbox...`);
4035
4376
  const result = await outbox.drain(this.config.aid, async (entry) => {
4377
+ if (!isDeliveryTarget(entry.delivery)) {
4378
+ logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
4379
+ return true;
4380
+ }
4036
4381
  if (entry.type === 'text') {
4037
4382
  return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
4038
4383
  }
@@ -4058,11 +4403,23 @@ export class AUNChannel {
4058
4403
  this.messageSeqMap.delete(messageId);
4059
4404
  }
4060
4405
  sendProcessingStatus(channelId, status, sessionId, taskId, context, extraMeta) {
4406
+ let delivery;
4407
+ try {
4408
+ delivery = this.requireDelivery(channelId, context);
4409
+ }
4410
+ catch (error) {
4411
+ logger.warn(`${this.logPrefix()} task.${status} notify skipped without trusted delivery route: channel=${channelId} task=${taskId} error=${error instanceof Error ? error.message : String(error)}`);
4412
+ return;
4413
+ }
4414
+ context = this.withDelivery(context, delivery);
4061
4415
  if (status === 'start')
4062
4416
  this.sentCount.delete(channelId); // 新任务开始,重置计数
4063
4417
  if (!this.client || !this.connected)
4064
4418
  return;
4065
- const severity = status === 'error' || status === 'timeout' ? 'error' : 'info';
4419
+ const requestedSeverity = extraMeta?.severity;
4420
+ const severity = requestedSeverity === 'warn'
4421
+ ? 'warn'
4422
+ : status === 'error' || status === 'timeout' ? 'error' : 'info';
4066
4423
  const stateMap = {
4067
4424
  start: 'started',
4068
4425
  done: 'completed',
@@ -4081,7 +4438,10 @@ export class AUNChannel {
4081
4438
  severity,
4082
4439
  terminal: ['done', 'interrupted', 'error', 'timeout'].includes(status),
4083
4440
  };
4084
- const metadata = extraMeta ? this.stripUndefinedDeep(extraMeta) : undefined;
4441
+ const metadataInput = extraMeta ? { ...extraMeta } : undefined;
4442
+ if (metadataInput)
4443
+ delete metadataInput.severity;
4444
+ const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
4085
4445
  if (metadata && Object.keys(metadata).length > 0)
4086
4446
  statusPayload.metadata = metadata;
4087
4447
  if (context?.threadId)
@@ -4093,8 +4453,8 @@ export class AUNChannel {
4093
4453
  if (context?.replyToMessageId)
4094
4454
  statusPayload.ref_message_id = context.replyToMessageId;
4095
4455
  const notifyOptions = { ttlMs: 60_000 };
4096
- if (this.isGroupId(channelId))
4097
- notifyOptions.groupId = channelId;
4456
+ if (delivery.chatType === 'group')
4457
+ notifyOptions.groupId = delivery.groupId;
4098
4458
  else
4099
4459
  notifyOptions.to = channelId;
4100
4460
  this.trace('OUT', 'notify.task_status', {
@@ -4115,7 +4475,7 @@ export class AUNChannel {
4115
4475
  }).catch(() => { });
4116
4476
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true, undefined, undefined, 'notify');
4117
4477
  // 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
4118
- const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
4478
+ const targetLabel = delivery.chatType === 'group' ? delivery.groupId : this.peerLabel(channelId);
4119
4479
  const chatmode = context?.metadata?.chatmode ?? '?';
4120
4480
  const initiator = statusPayload.initiator ?? '';
4121
4481
  const refMsgId = statusPayload.ref_message_id ?? '';
@@ -4127,8 +4487,6 @@ export class AUNChannel {
4127
4487
  logger.info(logTaskStatus);
4128
4488
  }
4129
4489
  async sendCustomPayload(channelId, payload, context) {
4130
- if (!this.client || !this.connected)
4131
- return;
4132
4490
  // SDK 0.3.0 E2EE requires payload to be an object
4133
4491
  let payloadObj;
4134
4492
  try {
@@ -4362,21 +4720,25 @@ export class AUNChannelPlugin {
4362
4720
  channelKey: inst.name, // channelName 实际上就是 channelKey
4363
4721
  capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true, authenticatedApproval: true },
4364
4722
  send: async (envelope, payload) => {
4723
+ const channelId = envelope?.channelId;
4724
+ const delivery = requireEnvelopeDelivery(channelId, envelope?.delivery, envelope?.replyContext?.delivery);
4365
4725
  const parentCausation = normalizeCausation(envelope.causation ?? envelope.replyContext?.metadata?.causation);
4366
4726
  const outboundCausation = parentCausation ? deriveCausation(parentCausation) : undefined;
4367
4727
  const replyCtx = outboundCausation
4368
4728
  ? {
4369
4729
  ...(envelope.replyContext ?? {}),
4730
+ delivery,
4370
4731
  metadata: { ...(envelope.replyContext?.metadata ?? {}), causation: outboundCausation },
4371
4732
  }
4372
- : envelope.replyContext;
4733
+ : (envelope.replyContext || envelope.delivery)
4734
+ ? { ...(envelope.replyContext ?? {}), delivery }
4735
+ : undefined;
4373
4736
  if (outboundCausation) {
4374
4737
  recordCausationSpan(outboundCausation, 'message.outbound', {
4375
4738
  status: 'started',
4376
4739
  refs: { taskId: envelope.taskId, sessionId: envelope.sessionId },
4377
4740
  });
4378
4741
  }
4379
- const channelId = envelope.channelId;
4380
4742
  const taskBase = () => channel.buildTaskPayloadBase(envelope, replyCtx);
4381
4743
  switch (payload.kind) {
4382
4744
  case 'result.text':
@@ -4505,7 +4867,7 @@ export class AUNChannelPlugin {
4505
4867
  aunCard.text = action.body;
4506
4868
  setIfDefined(aunCard, 'format', action.bodyFormat);
4507
4869
  }
4508
- if (req.initiatorId && channel.isGroupId(channelId))
4870
+ if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
4509
4871
  aunCard.initiator = req.initiatorId;
4510
4872
  if (replyCtx?.threadId)
4511
4873
  aunCard.thread_id = replyCtx.threadId;
@@ -4518,6 +4880,7 @@ export class AUNChannelPlugin {
4518
4880
  requestId: req.id,
4519
4881
  isCommandCard: false,
4520
4882
  initiatorAid: req.initiatorId,
4883
+ delivery: replyCtx?.delivery,
4521
4884
  expiresAt: Date.now() + cardTtlMs,
4522
4885
  },
4523
4886
  });
@@ -4542,7 +4905,7 @@ export class AUNChannelPlugin {
4542
4905
  aunCard.text = card.body;
4543
4906
  setIfDefined(aunCard, 'format', card.bodyFormat);
4544
4907
  }
4545
- if (req.initiatorId && channel.isGroupId(channelId))
4908
+ if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
4546
4909
  aunCard.initiator = req.initiatorId;
4547
4910
  if (replyCtx?.threadId)
4548
4911
  aunCard.thread_id = replyCtx.threadId;
@@ -4555,6 +4918,7 @@ export class AUNChannelPlugin {
4555
4918
  requestId: req.id,
4556
4919
  isCommandCard: true,
4557
4920
  initiatorAid: req.initiatorId,
4921
+ delivery: replyCtx?.delivery,
4558
4922
  expiresAt: Date.now() + cardTtlMs,
4559
4923
  },
4560
4924
  });