evolcore 0.0.11 → 0.0.13

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 (164) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/bin/ec-safe-output.js +26 -7
  4. package/dist/agents/claude-runner.js +86 -7
  5. package/dist/agents/codex-app-server-client.js +174 -20
  6. package/dist/agents/codex-runner.js +375 -92
  7. package/dist/agents/ecagent-runner.js +39 -4
  8. package/dist/agents/gemini-runner.js +1 -0
  9. package/dist/aun/aid/client.js +11 -21
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/managed-operation.js +107 -0
  12. package/dist/aun/aid/store.js +13 -7
  13. package/dist/aun/group-fs-download.js +56 -0
  14. package/dist/aun/group-identity.js +1 -2
  15. package/dist/aun/msg/group.js +91 -44
  16. package/dist/aun/msg/index.js +1 -2
  17. package/dist/aun/msg/managed-operation.js +865 -0
  18. package/dist/aun/msg/mention-schema.js +20 -0
  19. package/dist/aun/msg/p2p.js +7 -0
  20. package/dist/aun/msg/upload.js +16 -1
  21. package/dist/aun/outbox.js +31 -2
  22. package/dist/aun/rpc/client.js +66 -0
  23. package/dist/aun/rpc/index.js +1 -2
  24. package/dist/aun/storage/{upload.js → client.js} +11 -2
  25. package/dist/aun/storage/index.js +1 -2
  26. package/dist/channels/aun.js +655 -240
  27. package/dist/channels/daemon.js +13 -0
  28. package/dist/cli/agent-command.js +56 -8
  29. package/dist/cli/agent.js +108 -31
  30. package/dist/cli/aun-commands.js +270 -69
  31. package/dist/cli/bench.js +12 -3
  32. package/dist/cli/cli-argv.js +10 -0
  33. package/dist/cli/config.js +9 -17
  34. package/dist/cli/ctl-command.js +1 -1
  35. package/dist/cli/daemon-commands.js +70 -55
  36. package/dist/cli/fs-command.js +79 -10
  37. package/dist/cli/handoff-command.js +3 -0
  38. package/dist/cli/index.js +30 -2
  39. package/dist/cli/init.js +77 -8
  40. package/dist/cli/managed-command-guard.js +41 -0
  41. package/dist/cli/model.js +11 -4
  42. package/dist/cli/queue-command.js +53 -1
  43. package/dist/cli/response.js +71 -0
  44. package/dist/cli/restart-monitor.js +7 -32
  45. package/dist/cli/trigger-command.js +11 -0
  46. package/dist/config/access-policy.js +1 -2
  47. package/dist/config/builtin-roles.js +9 -0
  48. package/dist/config/config-manager.js +21 -1
  49. package/dist/config/contact-book-store.js +11 -3
  50. package/dist/config/contact-request-service.js +161 -15
  51. package/dist/config/mention-mode.js +8 -24
  52. package/dist/config/peer-role-resolver.js +6 -2
  53. package/dist/config/role-migration-startup.js +53 -0
  54. package/dist/config/role-store.js +1 -1
  55. package/dist/core/auth/agent-delegation.js +1 -2
  56. package/dist/core/auth/auth-gateway.js +29 -1
  57. package/dist/core/auth/authorization-audit.js +15 -0
  58. package/dist/core/auth/operation-authorizer.js +31 -7
  59. package/dist/core/auth/operation-catalog.js +196 -25
  60. package/dist/core/bootstrap-messages.js +25 -0
  61. package/dist/core/bootstrap-service.js +64 -4
  62. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  63. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  64. package/dist/core/capability/skill-discovery.js +55 -0
  65. package/dist/core/channel-loader.js +1 -1
  66. package/dist/core/command/cli-intent-parser.js +9 -0
  67. package/dist/core/command/command-handler.js +650 -42
  68. package/dist/core/command/connect-menu.js +15 -0
  69. package/dist/core/command/group-menu.js +4 -4
  70. package/dist/core/command/menu-catalog.js +370 -0
  71. package/dist/core/command/menu-handler.js +230 -51
  72. package/dist/core/command/menu-protocol.js +94 -2
  73. package/dist/core/command/slash-gate.js +5 -2
  74. package/dist/core/command/slash-handler.js +99 -31
  75. package/dist/core/daemon-file-cache.js +2 -1
  76. package/dist/core/data-migration.js +9 -2
  77. package/dist/core/event-catalog.js +6 -0
  78. package/dist/core/evolagent.js +4 -2
  79. package/dist/core/handoff/dispatcher.js +5 -2
  80. package/dist/core/handoff/runtime.js +157 -22
  81. package/dist/core/handoff/store.js +29 -1
  82. package/dist/core/handoff/types.js +1 -0
  83. package/dist/core/interaction-router.js +10 -0
  84. package/dist/core/message/im-renderer.js +19 -0
  85. package/dist/core/message/logical-queue-bridge.js +1 -0
  86. package/dist/core/message/mention-schema.js +126 -0
  87. package/dist/core/message/message-bridge.js +298 -88
  88. package/dist/core/message/message-log.js +3 -0
  89. package/dist/core/message/message-queue.js +268 -39
  90. package/dist/core/message/message-utils.js +81 -4
  91. package/dist/core/message/response-engine.js +161 -77
  92. package/dist/core/message/stream-debouncer.js +10 -2
  93. package/dist/core/permission/approval-gateway.js +1 -0
  94. package/dist/core/permission/ec-command-parser.js +243 -18
  95. package/dist/core/permission/index.js +1 -1
  96. package/dist/core/permission/mode.js +15 -0
  97. package/dist/core/permission/readonly-shell-query.js +148 -8
  98. package/dist/core/permission/sandbox-runtime.js +43 -1
  99. package/dist/core/permission/tool-policy.js +322 -57
  100. package/dist/core/protected-paths.js +114 -4
  101. package/dist/core/session/session-manager.js +42 -4
  102. package/dist/core/session/session-mapper.js +20 -1
  103. package/dist/eck/detect.js +18 -1
  104. package/dist/eck/group-rules-sync.js +15 -44
  105. package/dist/eck/manifest-engine.js +39 -13
  106. package/dist/index.js +541 -180
  107. package/dist/ipc.js +232 -10
  108. package/dist/paths.js +3 -0
  109. package/dist/response-system/coordinator.js +6 -4
  110. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  111. package/dist/response-system/modes/single-session/index.js +14 -1
  112. package/dist/response-system/selector.js +6 -5
  113. package/dist/trigger/anomaly-store.js +4 -0
  114. package/dist/trigger/feedback.js +64 -11
  115. package/dist/trigger/manager.js +21 -1
  116. package/dist/trigger/parser.js +14 -2
  117. package/dist/trigger/scheduler.js +1 -0
  118. package/dist/trigger/script-executor.js +1 -0
  119. package/dist/trigger/validation.js +67 -6
  120. package/dist/utils/codex-app-server-registry.js +90 -0
  121. package/dist/utils/codex-cli.js +5 -1
  122. package/dist/utils/cross-platform.js +15 -0
  123. package/dist/utils/ecweb-supervisor.js +332 -0
  124. package/dist/utils/error-utils.js +10 -0
  125. package/dist/utils/npm-ops.js +5 -12
  126. package/dist/utils/tool-summary.js +11 -0
  127. package/kits/docs/channels/aun.md +1 -1
  128. package/kits/docs/context-assembly.md +2 -2
  129. package/kits/docs/evolcore/agent.md +11 -6
  130. package/kits/docs/evolcore/aid.md +14 -0
  131. package/kits/docs/evolcore/trigger.md +17 -1
  132. package/kits/docs/prompt-loading-architecture.md +1 -2
  133. package/kits/docs/venues/group.md +1 -1
  134. package/kits/eck_manifest.json +0 -12
  135. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  136. package/kits/rules/04-relation.md +1 -1
  137. package/kits/rules/05-venue.md +2 -2
  138. package/kits/schemas/role-config.schema.1.json +1 -0
  139. package/kits/schemas/single-session.schema.1.json +3 -3
  140. package/kits/schemas/single-session.schema.2.json +3 -3
  141. package/kits/templates/roles/admin.json +27 -0
  142. package/kits/templates/roles/member.json +34 -1
  143. package/kits/templates/roles/visitor.json +35 -3
  144. package/kits/templates/system-fragments/bootstrap.md +1 -1
  145. package/kits/templates/system-fragments/channel.md +1 -1
  146. package/kits/templates/system-fragments/session.md +1 -1
  147. package/package.json +2 -2
  148. package/dist/aun/msg/payload-type.js +0 -27
  149. package/dist/aun/rpc/caller.js +0 -42
  150. package/dist/aun/rpc/connection.js +0 -25
  151. package/dist/aun/storage/manage.js +0 -10
  152. package/dist/config/access-policy-domain.js +0 -18
  153. package/dist/config/config-batch-get.js +0 -11
  154. package/dist/config/owner-policy.js +0 -4
  155. package/dist/config/role-config-v4-startup.js +0 -32
  156. package/dist/config/role-config-v5-startup.js +0 -27
  157. package/dist/core/auth/trigger-authorization.js +0 -15
  158. package/dist/core/command/evol-menu-version-gate.js +0 -39
  159. package/dist/core/interaction-registration.js +0 -10
  160. package/dist/core/permission/execution-sandbox.js +0 -16
  161. package/dist/core/relation/peer-key.js +0 -1
  162. package/dist/core/session/session-key.js +0 -24
  163. package/dist/eck/baseagent-caps.js +0 -18
  164. 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';
@@ -21,7 +21,7 @@ import { loadAgent } from '../config-store.js';
21
21
  import { normalizeAgentLifecycle } from '../config/lifecycle.js';
22
22
  import { resolveEffective } from '../config/config-manager.js';
23
23
  import { isManagementRole } from '../config/builtin-roles.js';
24
- import { mentionModeToDispatch } from '../config/mention-mode.js';
24
+ import { normalizeMentionMode } from '../config/mention-mode.js';
25
25
  import { getProcessStartTime } from '../utils/process-introspect.js';
26
26
  import * as outbox from '../aun/outbox.js';
27
27
  import { guessMime, formatSize } from '../utils/media-cache.js';
@@ -31,13 +31,36 @@ 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
- import { postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox } from '../core/bootstrap-messages.js';
36
+ import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
37
+ import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
38
+ import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
39
+ import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
38
40
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
39
41
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
42
+ const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
40
43
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
44
+ function requireEnvelopeDelivery(channelId, envelopeDelivery, nestedDelivery) {
45
+ if (!isDeliveryTarget(envelopeDelivery)) {
46
+ throw Object.assign(new Error(`AUN outbound envelope requires a top-level delivery route for channelId=${channelId}`), {
47
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
48
+ });
49
+ }
50
+ if (nestedDelivery !== undefined) {
51
+ if (!isDeliveryTarget(nestedDelivery) || !sameDeliveryTarget(envelopeDelivery, nestedDelivery)) {
52
+ throw Object.assign(new Error(`AUN outbound envelope has conflicting delivery routes for channelId=${channelId}`), {
53
+ code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
54
+ });
55
+ }
56
+ }
57
+ if (envelopeDelivery.chatType === 'group' && envelopeDelivery.groupId !== channelId) {
58
+ throw Object.assign(new Error(`AUN group delivery does not match channelId=${channelId}`), {
59
+ code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
60
+ });
61
+ }
62
+ return envelopeDelivery;
63
+ }
41
64
  function normalizeImageMimeType(mimeType) {
42
65
  const normalized = mimeType?.split(';', 1)[0].trim().toLowerCase();
43
66
  if (!normalized)
@@ -131,6 +154,8 @@ export function aunOptsToInbound(opts, channel, channelType) {
131
154
  encrypted: opts.encrypted,
132
155
  protectedHeaders: opts.protectedHeaders,
133
156
  messageId: opts.messageId,
157
+ receivedAt: opts.receivedAt,
158
+ gatewaySeq: opts.gatewaySeq,
134
159
  causation: opts.causation,
135
160
  mentions: opts.mentions,
136
161
  mentionAids: opts.mentionAids,
@@ -139,7 +164,7 @@ export function aunOptsToInbound(opts, channel, channelType) {
139
164
  replyContext: opts.replyContext,
140
165
  source: opts.source,
141
166
  images: opts.images,
142
- dispatchMode: opts.dispatchMode,
167
+ mentionMode: opts.mentionMode,
143
168
  };
144
169
  }
145
170
  function verifiedProtectedHeaders(msg, env) {
@@ -287,14 +312,36 @@ export class AUNChannel {
287
312
  throw e;
288
313
  }
289
314
  }
290
- /** 判断 channelId 是否为群组 ID
291
- * - 新格式:group.{aidDomain}/{group_no|group_name}
292
- * - 数字群号:{group_no}.{aidDomain}(如 11117.example.com)
293
- * - 兼容旧格式:grp_xxx、g-xxx.agentid.pub
315
+ /** Resolve the authenticated transport route for an ordinary outbound send.
316
+ * The channel id is an address only; it is intentionally never used to infer
317
+ * whether the address is a group.
294
318
  */
295
- /** 判断 channelId 是否群组 ID(public:plugin adapter 闭包需调用) */
296
- isGroupId(id) {
297
- return isExplicitGroupId(id);
319
+ requireDelivery(channelId, context, explicit) {
320
+ const supplied = explicit ?? context?.delivery;
321
+ if (supplied !== undefined && !isDeliveryTarget(supplied)) {
322
+ throw Object.assign(new Error(`AUN outbound send has an invalid delivery route for channelId=${channelId}`), {
323
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
324
+ });
325
+ }
326
+ if (!supplied) {
327
+ throw Object.assign(new Error(`AUN outbound send requires an explicit delivery route for channelId=${channelId}`), {
328
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
329
+ });
330
+ }
331
+ if (supplied.chatType === 'group' && supplied.groupId !== channelId) {
332
+ throw Object.assign(new Error(`AUN group delivery does not match channelId=${channelId}`), {
333
+ code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
334
+ });
335
+ }
336
+ return supplied;
337
+ }
338
+ requirePersistedDelivery(channelId, delivery) {
339
+ // Persisted sends only trust the top-level route. In particular, old
340
+ // outbox entries cannot recover routing from their nested reply context.
341
+ return this.requireDelivery(channelId, undefined, delivery);
342
+ }
343
+ withDelivery(context, delivery) {
344
+ return { ...(context ?? {}), delivery };
298
345
  }
299
346
  getShortAid(aid) {
300
347
  if (!aid)
@@ -316,15 +363,19 @@ export class AUNChannel {
316
363
  const name = cached?.name;
317
364
  return name && name !== short ? `${short}(${name})` : short;
318
365
  }
319
- notifyCardActionFailure(channelId, text, operatorId, threadId) {
366
+ notifyCardActionFailure(channelId, text, operatorId, threadId, delivery) {
320
367
  if (!channelId) {
321
368
  logger.warn(`${this.logPrefix()} Card action failure without channelId: ${text}`);
322
369
  return;
323
370
  }
324
- const context = {};
371
+ if (!delivery) {
372
+ logger.warn(`${this.logPrefix()} Card action failure without trusted delivery route: ${text}`);
373
+ return;
374
+ }
375
+ const context = { delivery };
325
376
  if (threadId)
326
377
  context.threadId = threadId;
327
- if (operatorId && this.isGroupId(channelId))
378
+ if (operatorId && delivery.chatType === 'group')
328
379
  context.peerId = operatorId;
329
380
  void this.sendMessage(channelId, text, context).catch((err) => {
330
381
  logger.error(`${this.logPrefix()} Failed to send card action error:`, err);
@@ -334,7 +385,7 @@ export class AUNChannel {
334
385
  logger.info(`${this.logPrefix()} ActionInteraction audit: behavior=${behavior ?? 'reply'} `
335
386
  + `cardId=${cardMessageId} interactionId=${interactionId} result=${result} textLength=${textLength}`);
336
387
  }
337
- extractTextPayload(payload, channelId, senderAid) {
388
+ extractTextPayload(payload, channelId, senderAid, delivery) {
338
389
  if (typeof payload === 'string')
339
390
  return payload;
340
391
  if (payload && typeof payload === 'object') {
@@ -345,6 +396,7 @@ export class AUNChannel {
345
396
  const cardMsgId = typeof obj.ref_message_id === 'string' ? obj.ref_message_id
346
397
  : typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
347
398
  const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
399
+ const cardDelivery = cardInfo?.delivery ?? delivery;
348
400
  if (cardInfo) {
349
401
  // compose 按钮由新版客户端在卡片内采集输入;旧版客户端可继续从主编辑器
350
402
  // 采集。两者最终都回传 action_card_reply{behavior:"compose", text}。
@@ -369,25 +421,25 @@ export class AUNChannel {
369
421
  // payloads omit senderAid. A group channelId is only a conversation
370
422
  // identifier and must never be treated as the clicking operator.
371
423
  const cardClickerAid = senderAid
372
- || (channelId && !this.isGroupId(channelId) ? channelId : '')
424
+ || (cardDelivery?.chatType === 'private' && channelId ? channelId : '')
373
425
  || '';
374
426
  if (cardInfo.isCommandCard) {
375
427
  // CommandCard:action_value 是完整 slash 命令,构造伪入站消息。
376
428
  // 先完成点击者鉴权,再消费 mapping;否则群成员的一次误点会让发起者
377
429
  // 无法继续操作同一张卡片。
378
- const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
430
+ const chatType = cardDelivery?.chatType ?? 'private';
379
431
  if (this.messageHandler && actionValue.startsWith('/')) {
380
432
  // Initiator 校验:仅群聊需要(私聊信道一对一,点击者恒为对端 = initiator)。
381
433
  // 身份只信认证信封提取的 cardClickerAid,非 payload 自报。
382
434
  if (chatType === 'group' && !cardClickerAid) {
383
435
  logger.info(`${this.logPrefix()} CommandCard rejected: missing authenticated clicker mid=${cardMsgId}`);
384
- this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId);
436
+ this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId, cardDelivery);
385
437
  return '';
386
438
  }
387
439
  if (chatType === 'group' && cardInfo.initiatorAid
388
440
  && cardClickerAid !== cardInfo.initiatorAid) {
389
441
  logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
390
- this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
442
+ this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId, cardDelivery);
391
443
  return '';
392
444
  }
393
445
  // 已通过鉴权并即将进入处理流程;此时才消费 mapping,避免非发起者
@@ -406,28 +458,28 @@ export class AUNChannel {
406
458
  })).catch((err) => {
407
459
  logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
408
460
  const message = err instanceof Error && err.message ? err.message : String(err || '未知错误');
409
- this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId);
461
+ this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId, cardDelivery);
410
462
  });
411
463
  }
412
464
  else if (!this.messageHandler) {
413
- this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId);
465
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId, cardDelivery);
414
466
  }
415
467
  else {
416
- this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId);
468
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId, cardDelivery);
417
469
  }
418
470
  }
419
471
  else {
420
472
  // ActionInteraction:后端明确接受后才消费 mapping。撤权、过期或
421
473
  // 重复点击返回 false 时保持 fail-closed,并提示用户卡片已失效。
422
- if (channelId && this.isGroupId(channelId) && !cardClickerAid) {
474
+ if (cardDelivery?.chatType === 'group' && !cardClickerAid) {
423
475
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_unauthenticated', text.length);
424
- this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId);
476
+ this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId, cardDelivery);
425
477
  return '';
426
478
  }
427
- if (channelId && this.isGroupId(channelId) && cardInfo.initiatorAid
479
+ if (cardDelivery?.chatType === 'group' && cardInfo.initiatorAid
428
480
  && cardClickerAid !== cardInfo.initiatorAid) {
429
481
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_non_initiator', text.length);
430
- this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
482
+ this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId, cardDelivery);
431
483
  return '';
432
484
  }
433
485
  // Compose reply 入参校验:text 必须非空且未超长。
@@ -437,19 +489,19 @@ export class AUNChannel {
437
489
  const MAX_COMPOSE_TEXT_LENGTH = 64 * 1024; // 64K 字符
438
490
  if (!trimmedText) {
439
491
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_empty', text.length);
440
- this.notifyCardActionFailure(channelId, '⚠️ 输入内容不能为空,请重新输入', cardClickerAid, threadId);
492
+ this.notifyCardActionFailure(channelId, '⚠️ 输入内容不能为空,请重新输入', cardClickerAid, threadId, cardDelivery);
441
493
  return '';
442
494
  }
443
495
  if (text.length > MAX_COMPOSE_TEXT_LENGTH) {
444
496
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_too_long', text.length);
445
- this.notifyCardActionFailure(channelId, `⚠️ 输入内容过长(${text.length} 字符,限制 ${MAX_COMPOSE_TEXT_LENGTH}),请缩短后重试`, cardClickerAid, threadId);
497
+ this.notifyCardActionFailure(channelId, `⚠️ 输入内容过长(${text.length} 字符,限制 ${MAX_COMPOSE_TEXT_LENGTH}),请缩短后重试`, cardClickerAid, threadId, cardDelivery);
446
498
  return '';
447
499
  }
448
500
  }
449
501
  if (this.interactionCallback) {
450
502
  if (this.interactionActionsInFlight.has(cardMsgId)) {
451
503
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_in_flight', text.length);
452
- this.notifyCardActionFailure(channelId, '⏳ 操作正在处理,请勿重复点击', cardClickerAid, threadId);
504
+ this.notifyCardActionFailure(channelId, '⏳ 操作正在处理,请勿重复点击', cardClickerAid, threadId, cardDelivery);
453
505
  return '';
454
506
  }
455
507
  this.interactionActionsInFlight.add(cardMsgId);
@@ -482,7 +534,7 @@ export class AUNChannel {
482
534
  // captured the message id before the callback returned.
483
535
  this.forgetInteractionCard(cardMsgId, cardInfo.requestId);
484
536
  }
485
- this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', cardClickerAid, threadId);
537
+ this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', cardClickerAid, threadId, cardDelivery);
486
538
  };
487
539
  try {
488
540
  const result = this.interactionCallback(response);
@@ -491,7 +543,7 @@ export class AUNChannel {
491
543
  this.interactionActionsInFlight.delete(cardMsgId);
492
544
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'callback_error', text.length);
493
545
  logger.error(`${this.logPrefix()} ActionInteraction callback failed: request=${cardInfo.requestId}`, err);
494
- this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId);
546
+ this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId, cardDelivery);
495
547
  });
496
548
  }
497
549
  else {
@@ -502,19 +554,19 @@ export class AUNChannel {
502
554
  this.interactionActionsInFlight.delete(cardMsgId);
503
555
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'callback_error', text.length);
504
556
  logger.error(`${this.logPrefix()} ActionInteraction callback failed: request=${cardInfo.requestId}`, err);
505
- this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId);
557
+ this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId, cardDelivery);
506
558
  }
507
559
  }
508
560
  else {
509
561
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'handler_unavailable', text.length);
510
- this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId);
562
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId, cardDelivery);
511
563
  }
512
564
  }
513
565
  }
514
566
  else if (this.ownedCardMsgIds.has(cardMsgId)) {
515
567
  // 本 agent 发出的卡片,但 entry 已过期(24h TTL)
516
568
  logger.debug(`${this.logPrefix()} action_card_reply expired: cardMsgId=${cardMsgId}`);
517
- this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起');
569
+ this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', undefined, undefined, delivery);
518
570
  }
519
571
  else {
520
572
  // 非本 agent 发出的卡片(broadcast 模式下其他 agent 的卡)→ 静默忽略
@@ -698,24 +750,20 @@ export class AUNChannel {
698
750
  .replace(/[ \t]+/g, ' ')
699
751
  .trim();
700
752
  }
701
- extractMentionAids(mentions) {
702
- const aids = [];
703
- for (const m of mentions) {
704
- if (typeof m === 'string')
705
- aids.push(m);
706
- else if (m && typeof m === 'object' && typeof m.aid === 'string')
707
- aids.push(m.aid);
753
+ parsePayloadMentionsOrReject(payload, context, messageId, seq) {
754
+ const raw = payload && typeof payload === 'object' && !Array.isArray(payload)
755
+ ? payload.mentions
756
+ : undefined;
757
+ try {
758
+ return normalizeAunMentionEntries(raw);
708
759
  }
709
- return aids;
710
- }
711
- hasMentionAll(mentions) {
712
- for (const m of mentions) {
713
- if (m === 'all')
714
- return true;
715
- if (m && typeof m === 'object' && m.scope === 'all')
716
- return true;
760
+ catch (error) {
761
+ this.acknowledgeImmediately(messageId, seq);
762
+ const detail = error instanceof Error ? error.message : String(error);
763
+ const code = error?.code ?? 'INVALID_MENTION_SCHEMA';
764
+ logger.error(`${this.logPrefix()} Dropped ${context}: invalid payload.mentions (${detail}) code=${code} mid=${messageId}`);
765
+ return undefined;
717
766
  }
718
- return false;
719
767
  }
720
768
  /** 从正文提取 @aid(AID 格式:至少三段域名),用于出站填充 payload.mentions */
721
769
  static MENTION_AID_RE = /@([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?){2,})/g;
@@ -731,6 +779,19 @@ export class AUNChannel {
731
779
  }
732
780
  return out;
733
781
  }
782
+ /**
783
+ * Convert the AUN text mention notation into the canonical wire entries.
784
+ * `@all` is intentionally represented by its scope discriminator instead
785
+ * of being treated as an AID.
786
+ */
787
+ extractMentionEntriesFromText(text) {
788
+ const entries = [];
789
+ if (this.hasExplicitMention(text, 'all'))
790
+ entries.push({ scope: 'all' });
791
+ for (const aid of this.extractMentionAidsFromText(text))
792
+ entries.push({ aid });
793
+ return entries;
794
+ }
734
795
  /** Sanitize a raw topicName from untrusted payload: trim, strip control chars, enforce length. */
735
796
  sanitizeTopicName(raw) {
736
797
  if (typeof raw !== 'string')
@@ -740,8 +801,13 @@ export class AUNChannel {
740
801
  return sanitized;
741
802
  return undefined;
742
803
  }
743
- buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName) {
744
- const replyContext = { metadata: { encrypted, chatmode } };
804
+ buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId) {
805
+ if (!groupId)
806
+ throw new Error('buildGroupReplyContext requires groupId');
807
+ const replyContext = {
808
+ metadata: { encrypted, chatmode },
809
+ delivery: { chatType: 'group', groupId },
810
+ };
745
811
  if (threadId)
746
812
  replyContext.threadId = threadId;
747
813
  replyContext.peerId = senderAid;
@@ -755,6 +821,108 @@ export class AUNChannel {
755
821
  }
756
822
  return replyContext;
757
823
  }
824
+ buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId) {
825
+ const replyContext = {
826
+ metadata: { encrypted, chatmode, refMessageId },
827
+ delivery: { chatType: 'private' },
828
+ peerId: senderAid,
829
+ };
830
+ if (threadId)
831
+ replyContext.threadId = threadId;
832
+ if (messageId)
833
+ replyContext.replyToMessageId = messageId;
834
+ if (topicName && threadId)
835
+ replyContext.metadata.topicName = topicName;
836
+ return replyContext;
837
+ }
838
+ claimInboundMessage(kind, messageId, seq) {
839
+ if (!messageId)
840
+ return true;
841
+ // Channel instance names are commonly reused across agents (the default
842
+ // is simply `aun`). Include the receiving AID so a server-scoped
843
+ // message_id cannot suppress an unrelated delivery in another channel.
844
+ const namespace = `${this.config.aid}:${this.config.channelName ?? 'aun'}:aun:${kind}`;
845
+ const dedupeKey = JSON.stringify([namespace, messageId]);
846
+ const existing = this.inboundSeenMessages.get(dedupeKey);
847
+ if (existing) {
848
+ if (seq !== undefined && existing.seq !== undefined && seq !== existing.seq) {
849
+ logger.warn(`${this.logPrefix()} Duplicate message_id with conflicting seq: namespace=${namespace} mid=${messageId} firstSeq=${existing.seq} duplicateSeq=${seq}`);
850
+ }
851
+ else {
852
+ logger.debug(`${this.logPrefix()} Duplicate inbound message dropped: namespace=${namespace} mid=${messageId} seq=${seq ?? 'unknown'}`);
853
+ }
854
+ this.acknowledgeImmediately(messageId, seq);
855
+ return false;
856
+ }
857
+ this.inboundSeenMessages.set(dedupeKey, { seenAt: Date.now(), seq });
858
+ if (seq !== undefined)
859
+ this.messageSeqMap.set(messageId, seq);
860
+ this.appendInboundDedupe(dedupeKey, messageId, kind, seq);
861
+ setTimeout(() => this.inboundSeenMessages.delete(dedupeKey), AUN_INBOUND_DEDUP_TTL_MS);
862
+ return true;
863
+ }
864
+ inboundDedupeFile() {
865
+ const channelName = this.config.channelName ?? 'aun';
866
+ return channelStatePath(channelName, 'dedup', 'aun', `${encodeURIComponent(this.config.aid)}.jsonl`);
867
+ }
868
+ loadInboundDedupe() {
869
+ const file = this.inboundDedupeFile();
870
+ const cutoff = Date.now() - AUN_INBOUND_DEDUP_TTL_MS;
871
+ let lines;
872
+ try {
873
+ lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
874
+ }
875
+ catch {
876
+ return;
877
+ }
878
+ let needsRewrite = false;
879
+ for (const line of lines) {
880
+ try {
881
+ const entry = JSON.parse(line);
882
+ if (typeof entry.key !== 'string' || typeof entry.seenAt !== 'number' || !Number.isFinite(entry.seenAt) || entry.seenAt <= cutoff) {
883
+ needsRewrite = true;
884
+ continue;
885
+ }
886
+ this.inboundSeenMessages.set(entry.key, {
887
+ seenAt: entry.seenAt,
888
+ seq: typeof entry.seq === 'number' && Number.isFinite(entry.seq) ? entry.seq : undefined,
889
+ });
890
+ }
891
+ catch {
892
+ needsRewrite = true;
893
+ }
894
+ }
895
+ if (needsRewrite)
896
+ this.rewriteInboundDedupe();
897
+ }
898
+ appendInboundDedupe(key, messageId, kind, seq) {
899
+ try {
900
+ const file = this.inboundDedupeFile();
901
+ fs.mkdirSync(path.dirname(file), { recursive: true });
902
+ fs.appendFileSync(file, JSON.stringify({ key, messageId, kind, seenAt: Date.now(), seq }) + '\n');
903
+ }
904
+ catch (error) {
905
+ logger.warn(`${this.logPrefix()} Failed to persist inbound dedup: ${error instanceof Error ? error.message : String(error)}`);
906
+ }
907
+ }
908
+ rewriteInboundDedupe() {
909
+ const cutoff = Date.now() - AUN_INBOUND_DEDUP_TTL_MS;
910
+ for (const [key, value] of this.inboundSeenMessages) {
911
+ if (value.seenAt <= cutoff)
912
+ this.inboundSeenMessages.delete(key);
913
+ }
914
+ try {
915
+ const file = this.inboundDedupeFile();
916
+ fs.mkdirSync(path.dirname(file), { recursive: true });
917
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
918
+ const lines = [...this.inboundSeenMessages].map(([key, value]) => JSON.stringify({ key, seenAt: value.seenAt, seq: value.seq }));
919
+ fs.writeFileSync(tmp, lines.length > 0 ? `${lines.join('\n')}\n` : '');
920
+ fs.renameSync(tmp, file);
921
+ }
922
+ catch (error) {
923
+ logger.warn(`${this.logPrefix()} Failed to compact inbound dedup: ${error instanceof Error ? error.message : String(error)}`);
924
+ }
925
+ }
758
926
  acknowledgeImmediately(messageId, _seq) {
759
927
  // SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
760
928
  // as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
@@ -771,10 +939,16 @@ export class AUNChannel {
771
939
  _aid;
772
940
  _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
773
941
  _chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
774
- seenMessages = new Map();
942
+ /**
943
+ * Inbound application dedupe. The AUN SDK may redeliver the same message
944
+ * with a different transport sequence, so message_id is the identity and
945
+ * seq is only retained for ACK/conflict diagnostics. Claiming happens at
946
+ * the event handler boundary, before payload/slash/mention parsing.
947
+ */
948
+ inboundSeenMessages = new Map();
775
949
  groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
776
950
  peerInfoCache = new Map();
777
- messageSeqMap = new Map(); // messageId → seq (for ack)
951
+ messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
778
952
  sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
779
953
  peerE2ee = new Map();
780
954
  static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
@@ -789,7 +963,7 @@ export class AUNChannel {
789
963
  interactionCardMessageIds = new Map();
790
964
  /** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
791
965
  invalidatedInteractions = new Map();
792
- dispatchModeResolver;
966
+ mentionModeResolver;
793
967
  static PROACTIVE_ALLOW_TYPES = new Set([
794
968
  'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
795
969
  'merge', 'link', 'location', 'personal_card',
@@ -798,6 +972,10 @@ export class AUNChannel {
798
972
  static INJECT_REQUEST_TYPE = 'observer.inject';
799
973
  /** 绑定请求类型(Evol App → 控制 AID):快速路径,绕过白名单透传原始 payload JSON 给 bridge。 */
800
974
  static BIND_REQUEST_TYPE = 'bind.request';
975
+ /** 联系人申请本人状态查询:在普通消息准入前交给 bridge,身份只取认证信封。 */
976
+ static CONTACT_REQUEST_QUERY_TYPE = 'contact.request.query';
977
+ /** App 联系人申请提交:结构化控制请求,不是 slash command。 */
978
+ static CONTACT_REQUEST_SUBMIT_TYPE = 'contact.request.submit';
801
979
  // Reconnect state
802
980
  // SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
803
981
  // 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
@@ -829,6 +1007,7 @@ export class AUNChannel {
829
1007
  constructor(config) {
830
1008
  this.config = config;
831
1009
  this.agentDir = agentDirPath(config.aid);
1010
+ this.loadInboundDedupe();
832
1011
  if (config.aunTrace) {
833
1012
  this.traceWriter = new LogWriter({
834
1013
  baseName: 'aun',
@@ -926,7 +1105,7 @@ export class AUNChannel {
926
1105
  // gateway metadata cache when well-known is temporarily unavailable.
927
1106
  const configuredGateway = this.config.gatewayUrl || '';
928
1107
  logger.info(`${this.logPrefix()} Initializing: aid=${aidName}, gateway=${configuredGateway || '<sdk-discovery>'}, aun_path=${aunPath}`);
929
- // 构造 AIDStore(slot=evolcore daemon,与 cli/netcheck 共享 evolcore 隔离键)
1108
+ // 构造 AIDStoredaemon 使用独立 slot,CLI/netcheck 不能触碰业务入站游标。
930
1109
  // encryptionSeed / rootCaPath 由 getAidStore 内部注入
931
1110
  const store = await getAidStore({
932
1111
  slotId: SLOT.daemon,
@@ -979,7 +1158,7 @@ export class AUNChannel {
979
1158
  for (const id of ids) {
980
1159
  if (typeof id === 'string') {
981
1160
  logger.info(`${this.logPrefix()} Message recalled: ${id}`);
982
- this.recallHandler?.(id);
1161
+ this.recallHandler?.(id, this._aid || this.config.aid);
983
1162
  }
984
1163
  }
985
1164
  }
@@ -1011,7 +1190,7 @@ export class AUNChannel {
1011
1190
  for (const id of ids) {
1012
1191
  if (typeof id === 'string') {
1013
1192
  logger.info(`${this.logPrefix()} Group message recalled: group=${env.group_id ?? ''} mid=${id}`);
1014
- this.recallHandler?.(id);
1193
+ this.recallHandler?.(id, this._aid || this.config.aid);
1015
1194
  }
1016
1195
  }
1017
1196
  }
@@ -1049,7 +1228,7 @@ export class AUNChannel {
1049
1228
  });
1050
1229
  this.trace('OUT', 'client.connect', { gateway: this.gatewayUrl, extra_info: extraInfo });
1051
1230
  await client.connect({
1052
- // connection_kind 默认 long;slot 已由 AID 携带(evolcore daemon)
1231
+ // connection_kind 默认 long;slot 已由 AID 携带(evolcore-daemon)
1053
1232
  // extra_info:互踢诊断名片(0.4.3 公开 connect 已支持透传)
1054
1233
  extra_info: extraInfo,
1055
1234
  // max_attempts=0 = 无限重试(与 Go/Python 对齐),交由 SDK 自己跑指数退避
@@ -1109,7 +1288,9 @@ export class AUNChannel {
1109
1288
  }
1110
1289
  const operationId = postBootstrapWelcomeOperationId(aidName);
1111
1290
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
1112
- if (hasMessageLogOperation(chatDir, operationId) || outbox.findByDedupeKey(aidName, operationId)) {
1291
+ const prepared = outbox.findByDedupeKey(aidName, operationId);
1292
+ if (hasMessageLogOperation(chatDir, operationId)
1293
+ || (prepared?.delivery?.chatType === 'private' && prepared.channelId === owner)) {
1113
1294
  return true;
1114
1295
  }
1115
1296
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
@@ -1138,6 +1319,14 @@ export class AUNChannel {
1138
1319
  return false;
1139
1320
  return this.reconcilePostBootstrapWelcome();
1140
1321
  }
1322
+ async hasPendingPostBootstrapWelcome() {
1323
+ const aid = this.config.aid.replace(/^@/, '');
1324
+ return hasPendingPostBootstrapWelcomeOutbox(aid);
1325
+ }
1326
+ async bindPostBootstrapWelcomeToSession(sessionId) {
1327
+ const aid = this.config.aid.replace(/^@/, '');
1328
+ return bindPostBootstrapWelcomeOutboxSession(aid, sessionId);
1329
+ }
1141
1330
  /**
1142
1331
  * Resume delivery of an already prepared completion welcome.
1143
1332
  *
@@ -1157,6 +1346,11 @@ export class AUNChannel {
1157
1346
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
1158
1347
  return hasMessageLogOperation(chatDir, operationId);
1159
1348
  }
1349
+ if (entry.delivery?.chatType !== 'private') {
1350
+ logger.warn(`${this.logPrefix()} Discarding unrouted legacy post-bootstrap welcome: id=${entry.id}`);
1351
+ outbox.remove(aid, entry.id);
1352
+ return false;
1353
+ }
1160
1354
  const agentConfig = loadAgent(aid);
1161
1355
  if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active') {
1162
1356
  logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
@@ -1180,7 +1374,7 @@ export class AUNChannel {
1180
1374
  * @param channelId 下载归属(私聊 fromAid / 群聊 groupId)
1181
1375
  * @param preCollected 已收集的附件(群聊路径会提前 collect,避免重复)
1182
1376
  */
1183
- async processAttachments(payload, baseText, channelId, preCollected) {
1377
+ async processAttachments(payload, baseText, channelId, preCollected, delivery) {
1184
1378
  const rawAttachments = preCollected ?? this.collectAllAttachments(payload);
1185
1379
  const images = [];
1186
1380
  let finalText = baseText;
@@ -1189,7 +1383,7 @@ export class AUNChannel {
1189
1383
  }
1190
1384
  const fileParts = [];
1191
1385
  for (const att of rawAttachments) {
1192
- const filePath = await this.downloadAttachment(att, channelId);
1386
+ const filePath = await this.downloadAttachment(att, channelId, delivery);
1193
1387
  if (!filePath)
1194
1388
  continue;
1195
1389
  const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
@@ -1221,8 +1415,11 @@ export class AUNChannel {
1221
1415
  logger.info(`${this.logPrefix()} [attachments] count=${rawAttachments.length} images=${images.length} files=${fileParts.length}`);
1222
1416
  return { finalText, images };
1223
1417
  }
1224
- async downloadAttachment(att, channelId) {
1225
- const ownerAid = att.owner_aid || (!this.isGroupId(channelId) ? channelId : '') || this._aid || '';
1418
+ async downloadAttachment(att, channelId, delivery) {
1419
+ const ownerAid = att.owner_aid
1420
+ || (delivery?.chatType === 'private' ? channelId : '')
1421
+ || this._aid
1422
+ || '';
1226
1423
  const objectKey = att.object_key;
1227
1424
  if (!objectKey) {
1228
1425
  logger.warn(`${this.logPrefix()} Attachment missing object_key, skipping`);
@@ -1394,17 +1591,27 @@ export class AUNChannel {
1394
1591
  if (!data || typeof data !== 'object')
1395
1592
  return;
1396
1593
  const msg = data;
1594
+ const receivedAt = Date.now();
1595
+ // Claim before any payload extraction or command parsing. Retransmits may
1596
+ // carry a new seq, but the application identity remains message_id.
1597
+ const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
1598
+ const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
1599
+ if (!this.claimInboundMessage('private', messageId, seq))
1600
+ return;
1397
1601
  // SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1398
1602
  // message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
1399
1603
  const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1400
1604
  const protectedHeaders = verifiedProtectedHeaders(msg, env);
1401
1605
  const fromAid = env.from ?? '';
1402
1606
  const payload = msg.payload ?? '';
1607
+ const mentions = this.parsePayloadMentionsOrReject(payload, 'p2p.inbound', messageId, seq);
1608
+ if (!mentions)
1609
+ return;
1403
1610
  const causationRef = payload && typeof payload === 'object' && typeof payload._evolcore_causation_ref === 'string'
1404
1611
  ? payload._evolcore_causation_ref
1405
1612
  : undefined;
1406
1613
  const logDescriptor = classifyAunPayloadForLog(payload);
1407
- const text = this.extractTextPayload(payload, fromAid, fromAid);
1614
+ const text = this.extractTextPayload(payload, fromAid, fromAid, { chatType: 'private' });
1408
1615
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1409
1616
  const refMessageId = typeof payload === 'object' && payload !== null && typeof payload.ref_message_id === 'string'
1410
1617
  ? payload.ref_message_id
@@ -1413,15 +1620,19 @@ export class AUNChannel {
1413
1620
  const topicName = (typeof payload === 'object' && payload !== null)
1414
1621
  ? this.sanitizeTopicName(payload.topicName)
1415
1622
  : undefined;
1416
- const messageId = msg.message_id ?? '';
1417
1623
  const causation = consumeAunCausation(causationRef || messageId, fromAid, this._aid || this.config.aid)
1418
1624
  ?? (causationRef ? consumeAunCausation(messageId, fromAid, this._aid || this.config.aid) : undefined);
1419
- const seq = msg.seq;
1625
+ const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
1626
+ // 终态错误只作为展示信号,不能进入 Agent,也不能被 observer 再次传播。
1627
+ if (inboundType === 'error') {
1628
+ this.acknowledgeImmediately(messageId, seq);
1629
+ logger.info(`${this.logPrefix()} P2P dropped terminal error: from=${fromAid || 'unknown'} mid=${messageId}`);
1630
+ return;
1631
+ }
1420
1632
  // Observer forward (inbound):在所有过滤之前转发原始明文 payload。
1421
1633
  // forwardInbound 内部排除 self-echo 与 from-owner。
1422
1634
  // 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
1423
1635
  // (即便日后 from-owner 排除规则调整,也不会泄漏)。
1424
- const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
1425
1636
  if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
1426
1637
  this.forwardInbound(msg);
1427
1638
  }
@@ -1437,13 +1648,16 @@ export class AUNChannel {
1437
1648
  const msgEncrypted = !!env.encrypted;
1438
1649
  if (!msgEncrypted)
1439
1650
  this.plaintextRecv++;
1440
- // Detect @mentions
1441
- const mentions = [];
1442
- if (this._aid && text.includes(`@${this._aid}`)) {
1443
- mentions.push(this._aid);
1651
+ // Preserve structured mentions for the shared inbound message model.
1652
+ // Text scanning remains only a rendering fallback and is not promoted to
1653
+ // a structured @all decision.
1654
+ if (mentions.length === 0 && this._aid && text.includes(`@${this._aid}`)) {
1655
+ // Private channels historically exposed a self mention even when the
1656
+ // sender omitted payload.mentions; keep that compatibility signal.
1657
+ mentions.push({ aid: this._aid });
1444
1658
  }
1445
1659
  // Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
1446
- const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid);
1660
+ const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid, undefined, { chatType: 'private' });
1447
1661
  // 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
1448
1662
  // device_id 仅 SDK 内部多实例去重用,evolcore session 层面跨端共享会话
1449
1663
  const chatId = fromAid;
@@ -1459,22 +1673,26 @@ export class AUNChannel {
1459
1673
  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))}`);
1460
1674
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1461
1675
  if (p2pPayloadType === 'action_card_reply') {
1462
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1676
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1463
1677
  return;
1464
1678
  }
1465
- // menu.* 协议:自定义消息快速路径,需要原始 payload JSON 传递给 bridge
1466
- if (p2pPayloadType.startsWith('menu.')) {
1679
+ // menu.* 与联系人本人状态查询:需要原始 payload JSON 传递给 bridge
1680
+ if (p2pPayloadType.startsWith('menu.')
1681
+ || p2pPayloadType === AUNChannel.CONTACT_REQUEST_QUERY_TYPE
1682
+ || p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
1467
1683
  this.acknowledgeImmediately(messageId, seq);
1468
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1684
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1469
1685
  this.dispatchMessage({
1470
1686
  channelId: chatId, userId: fromAid,
1471
1687
  text: JSON.stringify(payload),
1472
- chatType: 'private', messageId, seq,
1688
+ chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
1473
1689
  peerName: displayName || undefined,
1474
1690
  peerType: peerIdentity.type,
1475
1691
  encrypted: msgEncrypted,
1476
1692
  protectedHeaders,
1477
- replyContext: this.buildGroupReplyContext(threadId, fromAid, msgEncrypted, messageId),
1693
+ mentions,
1694
+ mentionAids: mentionEntryAids(mentions),
1695
+ replyContext: this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
1478
1696
  msgType: logDescriptor.msgType,
1479
1697
  payloadType: logDescriptor.payloadType,
1480
1698
  payloadSummary: logDescriptor.payloadSummary,
@@ -1494,14 +1712,16 @@ export class AUNChannel {
1494
1712
  // 透传 encrypted:bind.response 须与请求的加密/明文对称(见 onMessage 处理)。
1495
1713
  if (p2pPayloadType === AUNChannel.BIND_REQUEST_TYPE) {
1496
1714
  this.acknowledgeImmediately(messageId, seq);
1497
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1715
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1498
1716
  this.dispatchMessage({
1499
1717
  channelId: chatId, userId: fromAid,
1500
1718
  text: JSON.stringify(payload),
1501
- chatType: 'private', messageId, seq,
1719
+ chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
1502
1720
  peerName: displayName || undefined,
1503
1721
  peerType: peerIdentity.type,
1504
1722
  encrypted: msgEncrypted,
1723
+ mentions,
1724
+ mentionAids: mentionEntryAids(mentions),
1505
1725
  msgType: logDescriptor.msgType,
1506
1726
  payloadType: logDescriptor.payloadType,
1507
1727
  payloadSummary: logDescriptor.payloadSummary,
@@ -1512,7 +1732,7 @@ export class AUNChannel {
1512
1732
  // payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
1513
1733
  if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
1514
1734
  this.acknowledgeImmediately(messageId, seq);
1515
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1735
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1516
1736
  logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
1517
1737
  return;
1518
1738
  }
@@ -1521,24 +1741,15 @@ export class AUNChannel {
1521
1741
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
1522
1742
  const isSystemP2P = p2pPayloadType === 'event';
1523
1743
  this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
1524
- const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode, refMessageId } };
1525
- if (threadId)
1526
- replyContext.threadId = threadId;
1527
- replyContext.peerId = fromAid;
1528
- if (messageId)
1529
- replyContext.replyToMessageId = messageId;
1530
- // 协议 v2.4 §6.4/§12.1: 话题会话创建时传入 topicName
1531
- if (topicName && threadId) {
1532
- if (!replyContext.metadata)
1533
- replyContext.metadata = {};
1534
- replyContext.metadata.topicName = topicName;
1535
- }
1744
+ const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId);
1536
1745
  this.dispatchMessage({
1537
1746
  channelId: chatId,
1538
1747
  userId: fromAid,
1539
1748
  text: finalText,
1540
1749
  chatType: 'private',
1541
1750
  messageId,
1751
+ receivedAt,
1752
+ gatewaySeq: seq,
1542
1753
  causation,
1543
1754
  seq,
1544
1755
  threadId,
@@ -1561,28 +1772,39 @@ export class AUNChannel {
1561
1772
  if (!data || typeof data !== 'object')
1562
1773
  return;
1563
1774
  const msg = data;
1775
+ const receivedAt = Date.now();
1776
+ // Deduplicate at the adapter boundary, before mention/slash routing.
1777
+ const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
1778
+ const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
1779
+ if (!this.claimInboundMessage('group', messageId, seq))
1780
+ return;
1564
1781
  // SDK 0.5.* 移除了顶层 from/sender_aid/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1565
- // message_id / seq / payload / same_* / dispatch_mode 等仍是顶层独立字段,不在 envelope 内。
1782
+ // message_id / seq / payload / same_* / mention_mode 等仍是顶层独立字段,不在 envelope 内。
1566
1783
  const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1567
1784
  const protectedHeaders = verifiedProtectedHeaders(msg, env);
1568
1785
  const groupId = env.group_id ?? '';
1569
1786
  const senderAid = env.from ?? '';
1570
1787
  const payload = msg.payload ?? '';
1788
+ const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId, seq);
1789
+ if (!payloadMentionEntries)
1790
+ return;
1791
+ const payloadMentionAids = mentionEntryAids(payloadMentionEntries);
1571
1792
  const logDescriptor = classifyAunPayloadForLog(payload);
1572
- const text = this.extractTextPayload(payload, groupId, senderAid);
1793
+ const text = this.extractTextPayload(payload, groupId, senderAid, { chatType: 'group', groupId });
1573
1794
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1574
1795
  const topicName = (typeof payload === 'object' && payload !== null)
1575
1796
  ? this.sanitizeTopicName(payload.topicName)
1576
1797
  : undefined;
1577
- const messageId = msg.message_id ?? '';
1578
- const seq = msg.seq;
1798
+ const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
1799
+ // 终态错误只作为展示信号,不能进入 Agent,也不能被 observer 再次传播。
1800
+ if (inboundType === 'error') {
1801
+ this.acknowledgeImmediately(messageId, seq);
1802
+ logger.info(`${this.logPrefix()} Group dropped terminal error: group=${groupId || 'unknown'} sender=${senderAid || 'unknown'} mid=${messageId}`);
1803
+ return;
1804
+ }
1579
1805
  // Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
1580
1806
  // forwardInbound 内部排除 self-echo 与 from-owner。
1581
1807
  this.forwardInbound(msg);
1582
- // Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
1583
- const payloadMentions = Array.isArray(payload?.mentions)
1584
- ? payload.mentions.filter((m) => typeof m === 'string')
1585
- : [];
1586
1808
  logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
1587
1809
  if (!groupId || !senderAid) {
1588
1810
  this.acknowledgeImmediately(messageId, seq);
@@ -1624,7 +1846,7 @@ export class AUNChannel {
1624
1846
  peerName: displayName,
1625
1847
  peerType: peerInfo?.type || 'unknown',
1626
1848
  seq,
1627
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName),
1849
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId),
1628
1850
  createdAt,
1629
1851
  });
1630
1852
  return;
@@ -1634,7 +1856,7 @@ export class AUNChannel {
1634
1856
  {
1635
1857
  const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1636
1858
  if (payloadType === 'action_card_reply') {
1637
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId);
1859
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId, receivedAt, seq);
1638
1860
  return;
1639
1861
  }
1640
1862
  }
@@ -1647,14 +1869,18 @@ export class AUNChannel {
1647
1869
  if (payloadType.startsWith('menu.')) {
1648
1870
  const menuEncrypted = !!env.encrypted;
1649
1871
  this.acknowledgeImmediately(messageId, seq);
1650
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId);
1872
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId, receivedAt, seq);
1651
1873
  this.dispatchMessage({
1652
1874
  channelId: groupId, userId: senderAid,
1653
1875
  text: JSON.stringify(payload),
1654
- chatType: 'group', messageId, seq, groupId,
1876
+ chatType: 'group', messageId, seq, receivedAt, gatewaySeq: seq, groupId,
1655
1877
  encrypted: menuEncrypted,
1656
1878
  protectedHeaders,
1657
- replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName),
1879
+ mentions: payloadMentionEntries,
1880
+ mentionAids: payloadMentionAids,
1881
+ isMentioned: hasMentionAll(payloadMentionEntries)
1882
+ || (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
1883
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName, groupId),
1658
1884
  msgType: logDescriptor.msgType,
1659
1885
  payloadType: logDescriptor.payloadType,
1660
1886
  payloadSummary: logDescriptor.payloadSummary,
@@ -1664,7 +1890,7 @@ export class AUNChannel {
1664
1890
  }
1665
1891
  if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
1666
1892
  this.acknowledgeImmediately(messageId, seq);
1667
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId);
1893
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId, receivedAt, seq);
1668
1894
  logger.info(`${this.logPrefix()} Group dropped (type deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
1669
1895
  return;
1670
1896
  }
@@ -1673,17 +1899,17 @@ export class AUNChannel {
1673
1899
  const msgEncrypted = !!env.encrypted;
1674
1900
  if (!msgEncrypted)
1675
1901
  this.plaintextRecv++;
1676
- // dispatch_mode: 本地设置优先,fallback 到服务器参数
1677
- const serverDispatchMode = msg.dispatch_mode ?? payload?.dispatch_mode ?? 'mention';
1678
- const localDispatchMode = this.dispatchModeResolver
1679
- ? await this.dispatchModeResolver(groupId).catch(() => undefined)
1680
- : undefined;
1681
- const dispatchMode = localDispatchMode || serverDispatchMode;
1682
- const mentionedSelf = this._aid
1683
- ? (this.hasExplicitMention(text, this._aid) || payloadMentions.includes(this._aid))
1684
- : false;
1685
- // @all 仅认结构化 mentions(payload.mentions),不扫描正文 — 避免引述性 "@all" 误判
1686
- const mentionedAll = payloadMentions.includes('all');
1902
+ // mention_mode 是唯一的服务端群 @ 策略字段;缺失/非法时使用协议内置默认 disabled。
1903
+ const serverMentionMode = normalizeMentionMode(msg.mention_mode)
1904
+ ?? normalizeMentionMode(payload?.mention_mode)
1905
+ ?? 'disabled';
1906
+ const localMentionMode = normalizeMentionMode(this.mentionModeResolver
1907
+ ? await this.mentionModeResolver(groupId).catch(() => undefined)
1908
+ : undefined);
1909
+ const mentionMode = localMentionMode ?? serverMentionMode;
1910
+ const mentionedSelf = !!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid);
1911
+ // @all 仅认结构化 mentions(payload.mentions),不扫描正文,避免引述性 "@all" 误判。
1912
+ const mentionedAll = hasMentionAll(payloadMentionEntries);
1687
1913
  // Echo 机制优先于 mention 过滤:消息第一行包含 echo 时触发
1688
1914
  // 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
1689
1915
  const firstLineGroup = text.split('\n')[0] || '';
@@ -1705,7 +1931,7 @@ export class AUNChannel {
1705
1931
  this.pendingEchoMessages.set(messageId, {
1706
1932
  text: echoText,
1707
1933
  channelId: groupId,
1708
- context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined),
1934
+ context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined, groupId),
1709
1935
  receiveTs: Date.now(),
1710
1936
  });
1711
1937
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
@@ -1718,16 +1944,17 @@ export class AUNChannel {
1718
1944
  }
1719
1945
  else {
1720
1946
  // 非 echo 消息:mention 标记(不再过滤,交给响应模式决定)
1721
- // 命令豁免 broadcast:slash 命令在任何 dispatchMode 下都强制走 mention 语义,
1947
+ // slash 命令在任何 mentionMode 下都强制走 mention-only 语义
1722
1948
  // 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
1723
- const enforceMention = dispatchMode === 'mention' || isCommandMsg;
1949
+ const enforceMention = mentionMode === 'mention-only' || isCommandMsg;
1724
1950
  const isMentioned = mentionedSelf || mentionedAll;
1725
1951
  // 过滤逻辑下移到响应层:这里只标记,不过滤
1726
1952
  // 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
1727
1953
  // TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
1728
1954
  if (enforceMention && !isMentioned && !isOwnHandoff) {
1729
1955
  this.acknowledgeImmediately(messageId, seq);
1730
- logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mode=${dispatchMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
1956
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
1957
+ logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
1731
1958
  return;
1732
1959
  }
1733
1960
  }
@@ -1745,11 +1972,9 @@ export class AUNChannel {
1745
1972
  logger.debug(`${this.logPrefix()} Group dropped: empty text and no attachments (group=${groupId} sender=${senderAid} mid=${messageId})`);
1746
1973
  return;
1747
1974
  }
1748
- const mentions = mentionedAll
1749
- ? ['all']
1750
- : mentionedSelf && this._aid ? [this._aid] : [];
1975
+ const mentions = payloadMentionEntries;
1751
1976
  // Process attachments
1752
- const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments);
1977
+ const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments, { chatType: 'group', groupId });
1753
1978
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
1754
1979
  const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
1755
1980
  const shortAid = this.getShortAid(senderAid);
@@ -1758,27 +1983,27 @@ export class AUNChannel {
1758
1983
  const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1759
1984
  const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
1760
1985
  const textMentionAll = this.hasExplicitMention(text, 'all');
1761
- const structMentionSelf = this._aid ? payloadMentions.includes(this._aid) : false;
1762
- const structMentionAll = payloadMentions.includes('all');
1986
+ const structMentionSelf = this._aid ? payloadMentionAids.includes(this._aid) : false;
1987
+ const structMentionAll = mentionedAll;
1763
1988
  const reason = isOwnHandoff
1764
1989
  ? 'handoff.self'
1765
1990
  : mentionedAll
1766
1991
  ? 'mention.all(struct)'
1767
1992
  : mentionedSelf
1768
1993
  ? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
1769
- : `${dispatchMode}.no-mention`;
1770
- logger.info(`${this.logPrefix()} Group dispatch decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} dispatchMode=${dispatchMode} reason=${reason} structMentions=${JSON.stringify(payloadMentions)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
1994
+ : `${mentionMode}.no-mention`;
1995
+ logger.info(`${this.logPrefix()} Group mention decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} mentionMode=${mentionMode} reason=${reason} structMentions=${JSON.stringify(payloadMentionEntries)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
1771
1996
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1772
1997
  if (payloadType === 'action_card_reply')
1773
1998
  return;
1774
1999
  const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1775
- logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
2000
+ logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mentionMode=${mentionMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
1776
2001
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: senderAid, msgId: messageId, kind: 'text', len: finalText.length, groupId });
1777
2002
  this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode, payloadType === 'event' ? 'notify' : 'send');
1778
2003
  // 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
1779
2004
  // 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
1780
2005
  const renderMentionAids = Array.from(new Set([
1781
- ...payloadMentions,
2006
+ ...payloadMentionAids,
1782
2007
  ...this.extractMentionAidsFromText(text),
1783
2008
  ]));
1784
2009
  this.dispatchMessage({
@@ -1793,6 +2018,8 @@ export class AUNChannel {
1793
2018
  text: finalText,
1794
2019
  chatType: 'group',
1795
2020
  messageId,
2021
+ receivedAt,
2022
+ gatewaySeq: seq,
1796
2023
  seq,
1797
2024
  threadId,
1798
2025
  mentions,
@@ -1800,8 +2027,8 @@ export class AUNChannel {
1800
2027
  isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
1801
2028
  source: isOwnHandoff ? 'handoff' : undefined,
1802
2029
  encrypted: msgEncrypted,
1803
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName),
1804
- dispatchMode: serverDispatchMode,
2030
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId),
2031
+ mentionMode: serverMentionMode,
1805
2032
  images: inboundImages.length > 0 ? inboundImages : undefined,
1806
2033
  msgType: logDescriptor.msgType,
1807
2034
  payloadType: logDescriptor.payloadType,
@@ -1810,17 +2037,22 @@ export class AUNChannel {
1810
2037
  });
1811
2038
  }
1812
2039
  dispatchMessage(event) {
1813
- // Dedup
1814
- if (event.messageId) {
1815
- if (this.seenMessages.has(event.messageId))
1816
- return;
1817
- this.seenMessages.set(event.messageId, Date.now());
1818
- setTimeout(() => this.seenMessages.delete(event.messageId), 5 * 60 * 1000);
1819
- // Track seq for acknowledge
1820
- if (event.seq != null) {
1821
- this.messageSeqMap.set(event.messageId, event.seq);
1822
- }
1823
- }
2040
+ const authenticatedDelivery = event.chatType === 'group'
2041
+ ? { chatType: 'group', groupId: event.groupId ?? event.channelId }
2042
+ : { chatType: 'private' };
2043
+ const suppliedDelivery = event.replyContext?.delivery;
2044
+ const suppliedMatches = suppliedDelivery?.chatType === 'private'
2045
+ ? authenticatedDelivery.chatType === 'private'
2046
+ : suppliedDelivery?.chatType === 'group'
2047
+ && authenticatedDelivery.chatType === 'group'
2048
+ && suppliedDelivery.groupId === authenticatedDelivery.groupId;
2049
+ const delivery = suppliedDelivery && suppliedMatches ? suppliedDelivery : authenticatedDelivery;
2050
+ if (suppliedDelivery && delivery !== suppliedDelivery) {
2051
+ logger.warn(`${this.logPrefix()} Ignoring mismatched inbound reply route: channel=${event.channelId} chatType=${event.chatType}`);
2052
+ }
2053
+ // Inbound events are claimed in handleIncomingPrivateMessage / GroupMessage
2054
+ // before parsing. Keeping dedupe there prevents duplicate slash commands
2055
+ // from reaching routing and mention policy code.
1824
2056
  // Echo 机制:消息第一行包含 "echo"(不区分大小写)且原始内容 ≤10 字符时,直接回声
1825
2057
  // 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
1826
2058
  const firstLine = event.text.split('\n')[0] || '';
@@ -1845,20 +2077,24 @@ export class AUNChannel {
1845
2077
  this.pendingEchoMessages.set(event.messageId, {
1846
2078
  text: echoText,
1847
2079
  channelId: event.channelId,
1848
- context: event.replyContext ? { metadata: event.replyContext.metadata } : undefined,
2080
+ context: {
2081
+ ...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
2082
+ delivery,
2083
+ },
1849
2084
  receiveTs: Date.now(),
1850
2085
  });
1851
2086
  logger.info(`${this.logPrefix()} [Echo] long echo stored: mid=${event.messageId} channelId=${event.channelId}`);
1852
2087
  }
1853
2088
  if (!this.messageHandler)
1854
2089
  return;
1855
- const mentionObjects = event.mentions?.map(aid => ({ userId: aid }));
1856
- // Use caller-supplied replyContext (group path builds mentionUserIds);
1857
- // fall back to simple threadId-only context for private messages
1858
- let replyContext = event.replyContext;
1859
- if (!replyContext && event.threadId) {
1860
- replyContext = { threadId: event.threadId };
1861
- }
2090
+ // Preserve the caller-supplied context (group paths add mention fields),
2091
+ // but always attach the route derived from this authenticated event when
2092
+ // a synthetic/test event omitted it. Never infer the route from the AID.
2093
+ const replyContext = {
2094
+ ...(event.replyContext ?? {}),
2095
+ delivery,
2096
+ ...(event.threadId && !event.replyContext?.threadId ? { threadId: event.threadId } : {}),
2097
+ };
1862
2098
  this.messageHandler({
1863
2099
  channelId: event.channelId || '',
1864
2100
  channelType: 'aun',
@@ -1875,14 +2111,16 @@ export class AUNChannel {
1875
2111
  encrypted: event.encrypted,
1876
2112
  protectedHeaders: event.protectedHeaders,
1877
2113
  messageId: event.messageId,
2114
+ receivedAt: event.receivedAt,
2115
+ gatewaySeq: event.gatewaySeq ?? event.seq,
1878
2116
  causation: event.causation,
1879
2117
  threadId: event.threadId,
1880
- mentions: mentionObjects,
2118
+ mentions: event.mentions,
1881
2119
  mentionAids: event.mentionAids,
1882
2120
  isMentioned: event.isMentioned, // ← 新增:传递 isMentioned 标记
1883
2121
  replyContext,
1884
2122
  source: event.source,
1885
- dispatchMode: event.dispatchMode,
2123
+ mentionMode: event.mentionMode,
1886
2124
  images: event.images,
1887
2125
  msgType: event.msgType,
1888
2126
  payloadType: event.payloadType,
@@ -1892,7 +2130,7 @@ export class AUNChannel {
1892
2130
  logger.error(`${this.logPrefix()} Message handler error:`, err);
1893
2131
  });
1894
2132
  }
1895
- appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId) {
2133
+ appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq) {
1896
2134
  try {
1897
2135
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
1898
2136
  appendMessageLog(chatDir, buildInboundEntry({
@@ -1907,6 +2145,9 @@ export class AUNChannel {
1907
2145
  msgType: descriptor.msgType,
1908
2146
  payloadType: descriptor.payloadType,
1909
2147
  payloadSummary: descriptor.payloadSummary,
2148
+ timestamp: receivedAt,
2149
+ receivedAt,
2150
+ gatewaySeq,
1910
2151
  }));
1911
2152
  }
1912
2153
  catch (e) {
@@ -2110,7 +2351,14 @@ export class AUNChannel {
2110
2351
  }
2111
2352
  const replyTarget = event.channelId;
2112
2353
  // 不传 peerId,避免 sendMessage 在头部追加 @peer
2113
- const context = { threadId: event.threadId, metadata: event.replyContext?.metadata };
2354
+ const authenticatedDelivery = event.chatType === 'group'
2355
+ ? { chatType: 'group', groupId: event.channelId }
2356
+ : { chatType: 'private' };
2357
+ const context = {
2358
+ threadId: event.threadId,
2359
+ metadata: event.replyContext?.metadata,
2360
+ delivery: authenticatedDelivery,
2361
+ };
2114
2362
  const sendStart = Date.now();
2115
2363
  this.isEchoSending = true;
2116
2364
  this.sendMessage(replyTarget, echoText, context).then(() => {
@@ -2421,8 +2669,8 @@ export class AUNChannel {
2421
2669
  onMessage(handler) {
2422
2670
  this.messageHandler = handler;
2423
2671
  }
2424
- setDispatchModeResolver(resolver) {
2425
- this.dispatchModeResolver = resolver;
2672
+ setMentionModeResolver(resolver) {
2673
+ this.mentionModeResolver = resolver;
2426
2674
  }
2427
2675
  onRecall(handler) {
2428
2676
  this.recallHandler = handler;
@@ -2511,19 +2759,38 @@ export class AUNChannel {
2511
2759
  finalPayload.chatmode = context.metadata.chatmode;
2512
2760
  return finalPayload;
2513
2761
  }
2762
+ /**
2763
+ * Validate the AUN-specific structured field before it crosses the durable
2764
+ * send boundary. Return a copy when mentions are present so callers do not
2765
+ * retain an unvalidated array reference.
2766
+ */
2767
+ normalizeAunPayloadMentions(payload) {
2768
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
2769
+ return payload;
2770
+ if (!Object.prototype.hasOwnProperty.call(payload, 'mentions'))
2771
+ return payload;
2772
+ return {
2773
+ ...payload,
2774
+ mentions: normalizeAunMentionEntries(payload.mentions),
2775
+ };
2776
+ }
2514
2777
  rememberInvalidatedInteraction(interactionId, reason) {
2515
2778
  this.invalidatedInteractions.set(interactionId, reason);
2516
2779
  const timer = setTimeout(() => this.invalidatedInteractions.delete(interactionId), 24 * 60 * 60 * 1000);
2517
2780
  timer.unref?.();
2518
2781
  }
2519
- async recallInteractionCard(messageId, channelId) {
2782
+ async recallInteractionCard(messageId, channelId, delivery) {
2520
2783
  if (!this.client || !this.connected)
2521
2784
  return false;
2522
2785
  try {
2523
- const isGroup = channelId ? this.isGroupId(channelId) : false;
2786
+ if (!channelId || !delivery) {
2787
+ logger.warn(`${this.logPrefix()} Cannot recall interaction card without trusted delivery route: mid=${messageId}`);
2788
+ return false;
2789
+ }
2790
+ const isGroup = delivery.chatType === 'group';
2524
2791
  const method = isGroup ? 'group.recall' : 'message.recall';
2525
2792
  const params = isGroup
2526
- ? { group_id: channelId, message_ids: [messageId] }
2793
+ ? { group_id: delivery.groupId, message_ids: [messageId] }
2527
2794
  : { message_ids: [messageId] };
2528
2795
  await this.callAndTrace(method, params, { silentError: true });
2529
2796
  return true;
@@ -2546,9 +2813,10 @@ export class AUNChannel {
2546
2813
  }
2547
2814
  const messageIds = [...(this.interactionCardMessageIds.get(interactionId) ?? [])];
2548
2815
  for (const messageId of messageIds) {
2549
- const channelId = this.cardMessageIdMap.get(messageId)?.channelId;
2816
+ const card = this.cardMessageIdMap.get(messageId);
2817
+ const channelId = card?.channelId;
2550
2818
  this.forgetInteractionCard(messageId, interactionId);
2551
- await this.recallInteractionCard(messageId, channelId);
2819
+ await this.recallInteractionCard(messageId, channelId, card?.delivery);
2552
2820
  }
2553
2821
  }
2554
2822
  forgetInteractionCard(messageId, interactionId) {
@@ -2558,7 +2826,7 @@ export class AUNChannel {
2558
2826
  if (ids && ids.size === 0)
2559
2827
  this.interactionCardMessageIds.delete(interactionId);
2560
2828
  }
2561
- registerCardPostSend(messageId, action, channelId) {
2829
+ registerCardPostSend(messageId, action, channelId, delivery) {
2562
2830
  if (action.type !== 'register_interaction_card')
2563
2831
  return;
2564
2832
  this.cardMessageIdMap.set(messageId, {
@@ -2566,6 +2834,7 @@ export class AUNChannel {
2566
2834
  isCommandCard: action.isCommandCard,
2567
2835
  initiatorAid: action.initiatorAid,
2568
2836
  channelId,
2837
+ delivery: delivery ?? action.delivery,
2569
2838
  });
2570
2839
  this.ownedCardMsgIds.add(messageId);
2571
2840
  let messageIds = this.interactionCardMessageIds.get(action.requestId);
@@ -2591,20 +2860,31 @@ export class AUNChannel {
2591
2860
  const invalidationReason = this.invalidatedInteractions.get(action.requestId);
2592
2861
  if (invalidationReason) {
2593
2862
  this.cardMessageIdMap.delete(messageId);
2594
- void this.recallInteractionCard(messageId, channelId);
2863
+ void this.recallInteractionCard(messageId, channelId, delivery ?? action.delivery);
2595
2864
  }
2596
2865
  }
2597
2866
  runPostSend(entry, messageId) {
2598
2867
  if (!entry.postSend)
2599
2868
  return;
2600
2869
  if (entry.postSend.type === 'register_interaction_card') {
2601
- this.registerCardPostSend(messageId, entry.postSend, entry.channelId);
2870
+ this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery);
2602
2871
  }
2603
2872
  }
2604
2873
  async sendAunPayload(channelId, payload, context, label) {
2874
+ const delivery = this.requireDelivery(channelId, context);
2875
+ let normalizedPayload;
2876
+ try {
2877
+ normalizedPayload = this.normalizeAunPayloadMentions(payload);
2878
+ }
2879
+ catch (error) {
2880
+ const detail = error instanceof Error ? error.message : String(error);
2881
+ const code = error?.code ?? 'INVALID_MENTION_SCHEMA';
2882
+ logger.error(`${this.logPrefix()} Dropped durable AUN payload: invalid payload.mentions (${detail}) code=${code} channel=${channelId}`);
2883
+ return { ok: false, discarded: true };
2884
+ }
2605
2885
  if (!this.client || !this.connected)
2606
2886
  return { ok: false };
2607
- const isGroup = this.isGroupId(channelId);
2887
+ const isGroup = delivery.chatType === 'group';
2608
2888
  const targetAid = channelId;
2609
2889
  const encryptTarget = isGroup ? channelId : targetAid;
2610
2890
  const encrypt = context?.metadata?.encrypted != null
@@ -2616,13 +2896,13 @@ export class AUNChannel {
2616
2896
  if (causationRef && causation)
2617
2897
  registerAunCausation(causationRef, this.config.aid, targetAid, causation);
2618
2898
  const payloadWithCausationRef = causationRef
2619
- ? { ...payload, _evolcore_causation_ref: causationRef }
2620
- : payload;
2899
+ ? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
2900
+ : normalizedPayload;
2621
2901
  // Truncate payload if too large (AUN SDK limit: 1MB)
2622
2902
  const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
2623
2903
  const params = { payload: truncatedPayload, encrypt };
2624
2904
  if (isGroup)
2625
- params.group_id = channelId;
2905
+ params.group_id = delivery.groupId;
2626
2906
  else
2627
2907
  params.to = targetAid;
2628
2908
  const callOnce = async (sendParams, fallback) => {
@@ -2692,6 +2972,11 @@ export class AUNChannel {
2692
2972
  this.forwardOutbound(result);
2693
2973
  }
2694
2974
  async sendContentPayload(channelId, payload, opts) {
2975
+ const delivery = this.requireDelivery(channelId, opts.context);
2976
+ const context = this.withDelivery(opts.context, delivery);
2977
+ // Validate the caller's raw mentions before any early-return policy (such
2978
+ // as a cancelled interaction) can hide a malformed wire payload.
2979
+ const validatedPayload = this.normalizeAunPayloadMentions(payload);
2695
2980
  const interactionId = opts.postSend?.type === 'register_interaction_card'
2696
2981
  ? opts.postSend.requestId
2697
2982
  : undefined;
@@ -2699,14 +2984,20 @@ export class AUNChannel {
2699
2984
  logger.info(`${this.logPrefix()} Dropped invalidated interaction before durable enqueue: request=${interactionId}`);
2700
2985
  return {};
2701
2986
  }
2702
- const finalPayload = this.applyReplyContextToPayload(payload, opts.context);
2987
+ // Validate before write-ahead enqueue. Invalid AUN payloads must not be
2988
+ // hidden in the outbox and emitted later after a reconnect.
2989
+ // Validate the caller's raw mentions before deep-cleaning: that helper
2990
+ // intentionally removes undefined array elements, which would otherwise
2991
+ // turn malformed `mentions: [undefined]` into an apparently valid `[]`.
2992
+ const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, context));
2703
2993
  const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
2704
2994
  const entry = outbox.enqueue(this.config.aid, {
2705
2995
  channelId,
2996
+ delivery,
2706
2997
  type: 'payload',
2707
2998
  contentKind: opts.contentKind,
2708
2999
  payload: finalPayload,
2709
- context: opts.context,
3000
+ context,
2710
3001
  logText,
2711
3002
  ttl: opts.ttl,
2712
3003
  postSend: opts.postSend,
@@ -2853,19 +3144,39 @@ export class AUNChannel {
2853
3144
  logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
2854
3145
  return;
2855
3146
  }
3147
+ const delivery = this.requireDelivery(channelId, context);
3148
+ const routedContext = this.withDelivery(context, delivery);
2856
3149
  // 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
2857
3150
  if (!this.isEchoSending) {
2858
- await this.flushPendingEcho(channelId);
3151
+ await this.flushPendingEcho(channelId, delivery);
2859
3152
  }
2860
3153
  let finalText = text;
2861
3154
  this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
2862
- if (this.isGroupId(channelId) && context?.peerId) {
2863
- if (!finalText.includes(`@${context.peerId}`)) {
2864
- finalText = `@${context.peerId} ` + finalText;
3155
+ if (delivery.chatType === 'group') {
3156
+ const requestedMentions = Array.from(new Set((context?.mentionUserIds ?? [])
3157
+ .filter((id) => typeof id === 'string' && id.trim().length > 0)
3158
+ .map(id => id.trim())));
3159
+ // AUN's textual representation gives @all precedence over individual
3160
+ // targets, matching the cross-channel mention contract.
3161
+ const textMentionsAll = this.hasExplicitMention(finalText, 'all');
3162
+ const mentionTargets = requestedMentions.includes('all') || textMentionsAll
3163
+ ? ['all']
3164
+ : requestedMentions;
3165
+ if (mentionTargets.length > 0) {
3166
+ for (const target of mentionTargets.reverse()) {
3167
+ if (!this.hasExplicitMention(finalText, target)) {
3168
+ finalText = `@${target} ` + finalText;
3169
+ }
3170
+ }
3171
+ }
3172
+ else if (context?.peerId && !context.suppressPeerMention && !textMentionsAll) {
3173
+ if (!this.hasExplicitMention(finalText, context.peerId)) {
3174
+ finalText = `@${context.peerId} ` + finalText;
3175
+ }
2865
3176
  }
2866
3177
  }
2867
- const operationId = typeof context?.metadata?.operationId === 'string'
2868
- ? context.metadata.operationId
3178
+ const operationId = typeof routedContext.metadata?.operationId === 'string'
3179
+ ? routedContext.metadata.operationId
2869
3180
  : undefined;
2870
3181
  if (operationId) {
2871
3182
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
@@ -2875,13 +3186,14 @@ export class AUNChannel {
2875
3186
  // Write-ahead: persist to outbox before attempting send
2876
3187
  const entry = outbox.enqueue(this.config.aid, {
2877
3188
  channelId,
3189
+ delivery,
2878
3190
  dedupeKey: operationId,
2879
- critical: context?.metadata?.criticalDelivery === true,
3191
+ critical: routedContext.metadata?.criticalDelivery === true,
2880
3192
  type: 'text',
2881
3193
  text: finalText,
2882
- context,
2883
- ttl: typeof context?.metadata?.outboxTtl === 'number'
2884
- ? context.metadata.outboxTtl
3194
+ context: routedContext,
3195
+ ttl: typeof routedContext.metadata?.outboxTtl === 'number'
3196
+ ? routedContext.metadata.outboxTtl
2885
3197
  : undefined,
2886
3198
  });
2887
3199
  logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
@@ -2905,16 +3217,27 @@ export class AUNChannel {
2905
3217
  }
2906
3218
  /** Daemon-side transport for `ec msg send` running inside an agent task. */
2907
3219
  async sendDaemonMsg(args) {
2908
- if (!this.connected || !this.client) {
2909
- return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
2910
- }
2911
- if (!args.to || this.isGroupId(args.to)) {
2912
- return { ok: false, error: 'aun-msg-send only supports private AID targets', code: 'UNSUPPORTED_TARGET' };
3220
+ if (!args.to) {
3221
+ return { ok: false, error: 'aun-msg-send requires a private target', code: 'UNSUPPORTED_TARGET' };
2913
3222
  }
2914
3223
  if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
2915
3224
  return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
2916
3225
  }
2917
- const finalPayload = this.stripUndefinedDeep({ ...args.payload });
3226
+ let finalPayload;
3227
+ try {
3228
+ const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
3229
+ finalPayload = this.stripUndefinedDeep(validatedPayload);
3230
+ }
3231
+ catch (e) {
3232
+ return {
3233
+ ok: false,
3234
+ error: e?.message || String(e),
3235
+ code: e?.code,
3236
+ };
3237
+ }
3238
+ if (!this.connected || !this.client) {
3239
+ return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
3240
+ }
2918
3241
  const info = await this.fetchPeerInfo(args.to);
2919
3242
  const chatmode = info.type === 'human' ? 'interactive' : 'proactive';
2920
3243
  if (!finalPayload.chatmode)
@@ -3019,20 +3342,34 @@ export class AUNChannel {
3019
3342
  }
3020
3343
  /** Daemon-side transport for `ec group send` running inside an agent task. */
3021
3344
  async sendDaemonGroupMsg(args) {
3022
- if (!this.connected || !this.client) {
3023
- return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
3024
- }
3025
- if (!args.groupId || !this.isGroupId(args.groupId)) {
3026
- return { ok: false, error: 'aun-msg-send requires a valid group target', code: 'UNSUPPORTED_TARGET' };
3345
+ if (!args.groupId) {
3346
+ return { ok: false, error: 'aun group send requires a group target', code: 'UNSUPPORTED_TARGET' };
3027
3347
  }
3028
3348
  if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
3029
3349
  return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
3030
3350
  }
3031
- const finalPayload = this.stripUndefinedDeep({ ...args.payload });
3032
- if (args.mentions?.length)
3033
- finalPayload.mentions = args.mentions;
3034
- const encrypt = args.encrypt ?? this.shouldEncrypt(args.groupId);
3035
3351
  try {
3352
+ // Validate before deep-cleaning so undefined entries cannot be silently
3353
+ // removed from a malformed mentions array.
3354
+ const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
3355
+ const finalPayload = this.stripUndefinedDeep(validatedPayload);
3356
+ const payloadMentions = Object.prototype.hasOwnProperty.call(finalPayload, 'mentions')
3357
+ ? normalizeAunMentionEntries(finalPayload.mentions)
3358
+ : undefined;
3359
+ if (args.mentions !== undefined) {
3360
+ const normalizedMentions = normalizeAunMentionEntries(args.mentions);
3361
+ if (normalizedMentions.length > 0)
3362
+ finalPayload.mentions = normalizedMentions;
3363
+ else if (payloadMentions !== undefined)
3364
+ finalPayload.mentions = payloadMentions;
3365
+ }
3366
+ else if (payloadMentions !== undefined) {
3367
+ finalPayload.mentions = payloadMentions;
3368
+ }
3369
+ if (!this.connected || !this.client) {
3370
+ return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
3371
+ }
3372
+ const encrypt = args.encrypt ?? this.shouldEncrypt(args.groupId);
3036
3373
  const result = await this.callAndTrace('group.send', {
3037
3374
  group_id: args.groupId,
3038
3375
  payload: this.truncatePayloadIfNeeded(finalPayload, 'group-send-ipc'),
@@ -3090,7 +3427,7 @@ export class AUNChannel {
3090
3427
  return { ok: false, error: e?.message || String(e), code: e?.code };
3091
3428
  }
3092
3429
  }
3093
- async flushPendingEcho(channelId) {
3430
+ async flushPendingEcho(channelId, delivery) {
3094
3431
  // 查找该 channelId 是否有 pending echo(长 echo 等待 agent 首次回复)
3095
3432
  for (const [key, echo] of this.pendingEchoMessages) {
3096
3433
  if (echo.channelId === channelId) {
@@ -3114,18 +3451,32 @@ export class AUNChannel {
3114
3451
  ts: Date.now(),
3115
3452
  aid: this.config.aid,
3116
3453
  channelId,
3454
+ delivery,
3117
3455
  type: 'text',
3118
3456
  text: echoText,
3457
+ context: this.withDelivery(echo.context, delivery),
3119
3458
  ttl: 300_000,
3120
3459
  };
3121
3460
  const ok = await this.deliverTextEntry(echoEntry);
3122
3461
  if (!ok) {
3123
- outbox.enqueue(this.config.aid, { channelId, type: 'text', text: echoText });
3462
+ outbox.enqueue(this.config.aid, {
3463
+ channelId,
3464
+ delivery,
3465
+ type: 'text',
3466
+ text: echoText,
3467
+ context: this.withDelivery(echo.context, delivery),
3468
+ });
3124
3469
  }
3125
3470
  logger.info(`${this.logPrefix()} [Echo] long echo trace delivered=${ok} to ${channelId} (agent ${agentDuration}ms)`);
3126
3471
  }
3127
3472
  else {
3128
- outbox.enqueue(this.config.aid, { channelId, type: 'text', text: echoText });
3473
+ outbox.enqueue(this.config.aid, {
3474
+ channelId,
3475
+ delivery,
3476
+ type: 'text',
3477
+ text: echoText,
3478
+ context: this.withDelivery(echo.context, delivery),
3479
+ });
3129
3480
  logger.warn(`${this.logPrefix()} [Echo] not connected, echo trace queued in outbox`);
3130
3481
  }
3131
3482
  break;
@@ -3135,7 +3486,8 @@ export class AUNChannel {
3135
3486
  async deliverTextEntry(entry) {
3136
3487
  const channelId = entry.channelId;
3137
3488
  const finalText = entry.text;
3138
- const context = entry.context;
3489
+ const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
3490
+ const context = this.withDelivery(entry.context, delivery);
3139
3491
  const operationId = entry.dedupeKey
3140
3492
  || (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
3141
3493
  if (operationId) {
@@ -3153,8 +3505,8 @@ export class AUNChannel {
3153
3505
  // 从 context.metadata.source 读取 source,默认为 'daemon'
3154
3506
  const source = context?.metadata?.source ?? 'daemon';
3155
3507
  const payload = { type: 'text', text: finalText };
3156
- if (this.isGroupId(channelId)) {
3157
- const extracted = this.extractMentionAidsFromText(finalText);
3508
+ if (delivery.chatType === 'group') {
3509
+ const extracted = this.extractMentionEntriesFromText(finalText);
3158
3510
  if (extracted.length > 0)
3159
3511
  payload.mentions = extracted;
3160
3512
  }
@@ -3167,7 +3519,7 @@ export class AUNChannel {
3167
3519
  if (context?.metadata?.chatmode)
3168
3520
  payload.chatmode = context.metadata.chatmode;
3169
3521
  const causation = normalizeCausation(context?.metadata?.causation);
3170
- const causationRef = !this.isGroupId(channelId) && causation
3522
+ const causationRef = delivery.chatType !== 'group' && causation
3171
3523
  ? `caus_${crypto.randomUUID().replace(/-/g, '')}`
3172
3524
  : undefined;
3173
3525
  if (causationRef && causation) {
@@ -3176,7 +3528,7 @@ export class AUNChannel {
3176
3528
  }
3177
3529
  // 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
3178
3530
  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}`);
3179
- const isGroup = this.isGroupId(channelId);
3531
+ const isGroup = delivery.chatType === 'group';
3180
3532
  const targetAid = channelId;
3181
3533
  if (operationId && entry.deliveryReceipt) {
3182
3534
  this.appendOutboundJsonl(channelId, {
@@ -3350,12 +3702,17 @@ export class AUNChannel {
3350
3702
  }
3351
3703
  const contentKind = entry.contentKind;
3352
3704
  const logText = entry.logText ?? this.payloadLogText(payload, contentKind);
3353
- const context = entry.context;
3705
+ const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
3706
+ const context = this.withDelivery(entry.context, delivery);
3354
3707
  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}`);
3355
3708
  const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
3709
+ if (sent.discarded) {
3710
+ logger.warn(`${this.logPrefix()} Removed malformed durable payload from outbox: id=${entry.id} channel=${channelId}`);
3711
+ return { ok: true, discarded: true };
3712
+ }
3356
3713
  if (!sent.ok || !sent.messageId)
3357
3714
  return sent;
3358
- const isGroup = this.isGroupId(channelId);
3715
+ const isGroup = delivery.chatType === 'group';
3359
3716
  const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
3360
3717
  this.logAunSendAccepted(isGroup ? 'group.send' : 'message.send', isGroup ? channelId : this.peerLabel(channelId), sent.messageId, !!sent.encrypt, sent.result, logText);
3361
3718
  this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result, statsContext);
@@ -3415,13 +3772,16 @@ export class AUNChannel {
3415
3772
  * 存储键:group_id/peer_aid + sender_aid + context.type + context.id
3416
3773
  */
3417
3774
  async sendThought(channelId, taskId, payload, context) {
3418
- if (!this.connected || !this.client)
3419
- return;
3420
3775
  if (!taskId)
3421
3776
  return;
3777
+ const delivery = this.requireDelivery(channelId, context);
3778
+ context = this.withDelivery(context, delivery);
3779
+ const validatedPayload = this.normalizeAunPayloadMentions(payload);
3780
+ const finalPayload = this.normalizeAunPayloadMentions(this.stripUndefinedDeep(validatedPayload));
3781
+ if (!this.connected || !this.client)
3782
+ return;
3422
3783
  // 私聊 channelId = 对端 AID(不含 device_id)
3423
3784
  const targetId = channelId;
3424
- const finalPayload = this.stripUndefinedDeep(payload);
3425
3785
  const encrypt = context?.metadata?.encrypted != null
3426
3786
  ? !!(context.metadata.encrypted)
3427
3787
  : this.shouldEncrypt(targetId);
@@ -3460,8 +3820,8 @@ export class AUNChannel {
3460
3820
  else {
3461
3821
  thoughtText = this.activityLogText(finalPayload);
3462
3822
  }
3463
- if (this.isGroupId(channelId)) {
3464
- params.group_id = targetId;
3823
+ if (delivery.chatType === 'group') {
3824
+ params.group_id = delivery.groupId;
3465
3825
  const putRes = await this.callAndTrace('group.thought.put', params);
3466
3826
  const tid = putRes?.thought_id;
3467
3827
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
@@ -3491,15 +3851,20 @@ export class AUNChannel {
3491
3851
  }
3492
3852
  /** 发送结构化 AUN payload;transient 协议载荷不写本地 messages.jsonl。 */
3493
3853
  async sendStructured(channelId, payload, context) {
3854
+ const delivery = this.requireDelivery(channelId, context);
3855
+ context = this.withDelivery(context, delivery);
3856
+ // Validate before checking connection state so malformed producer payloads
3857
+ // are reported deterministically instead of being hidden by a disconnect.
3858
+ const validatedPayload = this.normalizeAunPayloadMentions(payload);
3859
+ const finalPayload = this.stripUndefinedDeep(validatedPayload);
3494
3860
  if (!this.connected || !this.client)
3495
3861
  return null;
3496
- const isGroup = this.isGroupId(channelId);
3862
+ const isGroup = delivery.chatType === 'group';
3497
3863
  const targetAid = channelId;
3498
3864
  const encryptTarget = isGroup ? channelId : targetAid;
3499
3865
  const encrypt = context?.metadata?.encrypted != null
3500
3866
  ? !!(context.metadata.encrypted)
3501
3867
  : this.shouldEncrypt(encryptTarget);
3502
- const finalPayload = this.stripUndefinedDeep({ ...payload });
3503
3868
  if (context?.threadId && !finalPayload.thread_id)
3504
3869
  finalPayload.thread_id = context.threadId;
3505
3870
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
@@ -3520,7 +3885,7 @@ export class AUNChannel {
3520
3885
  }
3521
3886
  try {
3522
3887
  if (isGroup) {
3523
- params.group_id = channelId;
3888
+ params.group_id = delivery.groupId;
3524
3889
  const result = await this.callAndTrace('group.send', params);
3525
3890
  const mid = result?.message?.message_id ?? result?.message_id ?? null;
3526
3891
  logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
@@ -3544,6 +3909,8 @@ export class AUNChannel {
3544
3909
  }
3545
3910
  }
3546
3911
  async sendFile(channelId, filePath, context) {
3912
+ const delivery = this.requireDelivery(channelId, context);
3913
+ context = this.withDelivery(context, delivery);
3547
3914
  const absPath = path.resolve(filePath);
3548
3915
  if (!fs.existsSync(absPath)) {
3549
3916
  logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
@@ -3561,6 +3928,7 @@ export class AUNChannel {
3561
3928
  // Write-ahead: persist to outbox
3562
3929
  const entry = outbox.enqueue(this.config.aid, {
3563
3930
  channelId,
3931
+ delivery,
3564
3932
  type: 'file',
3565
3933
  filePath: absPath,
3566
3934
  context,
@@ -3579,6 +3947,8 @@ export class AUNChannel {
3579
3947
  }
3580
3948
  }
3581
3949
  async sendImage(channelId, data, mimeType, alt, context) {
3950
+ const delivery = this.requireDelivery(channelId, context);
3951
+ context = this.withDelivery(context, delivery);
3582
3952
  if (!Buffer.isBuffer(data)) {
3583
3953
  throw new Error('result.image data must be a Buffer');
3584
3954
  }
@@ -3600,6 +3970,7 @@ export class AUNChannel {
3600
3970
  const logText = alt ? `[image] ${alt}` : '[image]';
3601
3971
  const entry = outbox.enqueue(this.config.aid, {
3602
3972
  channelId,
3973
+ delivery,
3603
3974
  type: 'image',
3604
3975
  contentKind: 'image',
3605
3976
  image: { dataBase64: data.toString('base64'), mimeType: normalizedMimeType, alt },
@@ -3658,6 +4029,9 @@ export class AUNChannel {
3658
4029
  logger.warn(`${this.logPrefix()} deliverImageEntry: missing image data (outbox id=${entry.id})`);
3659
4030
  return true;
3660
4031
  }
4032
+ const delivery = this.requirePersistedDelivery(entry.channelId, entry.delivery);
4033
+ entry.delivery = delivery;
4034
+ entry.context = this.withDelivery(entry.context, delivery);
3661
4035
  const data = Buffer.from(image.dataBase64, 'base64');
3662
4036
  if (data.length === 0) {
3663
4037
  logger.warn(`${this.logPrefix()} deliverImageEntry: invalid image data (outbox id=${entry.id})`);
@@ -3675,6 +4049,7 @@ export class AUNChannel {
3675
4049
  if (!outbox.replace(this.config.aid, entry)) {
3676
4050
  payloadEntry = outbox.enqueue(this.config.aid, {
3677
4051
  channelId: entry.channelId,
4052
+ delivery,
3678
4053
  type: 'payload',
3679
4054
  contentKind: 'image',
3680
4055
  payload: entry.payload,
@@ -3692,7 +4067,8 @@ export class AUNChannel {
3692
4067
  async deliverFileEntry(entry) {
3693
4068
  const channelId = entry.channelId;
3694
4069
  const absPath = entry.filePath;
3695
- const context = entry.context;
4070
+ const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
4071
+ const context = this.withDelivery(entry.context, delivery);
3696
4072
  if (!fs.existsSync(absPath)) {
3697
4073
  logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
3698
4074
  return true; // remove from outbox, file no longer exists
@@ -3750,7 +4126,7 @@ export class AUNChannel {
3750
4126
  attachment,
3751
4127
  context,
3752
4128
  });
3753
- const isGroup = this.isGroupId(channelId);
4129
+ const isGroup = delivery.chatType === 'group';
3754
4130
  const fileTargetAid = channelId;
3755
4131
  const encryptTarget = isGroup ? channelId : fileTargetAid;
3756
4132
  const encrypt = context?.metadata?.encrypted != null
@@ -3761,7 +4137,7 @@ export class AUNChannel {
3761
4137
  let sentMid = null;
3762
4138
  try {
3763
4139
  if (isGroup) {
3764
- params.group_id = channelId;
4140
+ params.group_id = delivery.groupId;
3765
4141
  this.trace('OUT', 'group.send.file', params);
3766
4142
  const result = await this.client.call('group.send', params);
3767
4143
  sendResult = result;
@@ -3869,6 +4245,10 @@ export class AUNChannel {
3869
4245
  return;
3870
4246
  logger.info(`${this.logPrefix()} Draining outbox...`);
3871
4247
  const result = await outbox.drain(this.config.aid, async (entry) => {
4248
+ if (!isDeliveryTarget(entry.delivery)) {
4249
+ logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
4250
+ return true;
4251
+ }
3872
4252
  if (entry.type === 'text') {
3873
4253
  return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
3874
4254
  }
@@ -3894,11 +4274,23 @@ export class AUNChannel {
3894
4274
  this.messageSeqMap.delete(messageId);
3895
4275
  }
3896
4276
  sendProcessingStatus(channelId, status, sessionId, taskId, context, extraMeta) {
4277
+ let delivery;
4278
+ try {
4279
+ delivery = this.requireDelivery(channelId, context);
4280
+ }
4281
+ catch (error) {
4282
+ logger.warn(`${this.logPrefix()} task.${status} notify skipped without trusted delivery route: channel=${channelId} task=${taskId} error=${error instanceof Error ? error.message : String(error)}`);
4283
+ return;
4284
+ }
4285
+ context = this.withDelivery(context, delivery);
3897
4286
  if (status === 'start')
3898
4287
  this.sentCount.delete(channelId); // 新任务开始,重置计数
3899
4288
  if (!this.client || !this.connected)
3900
4289
  return;
3901
- const severity = status === 'error' || status === 'timeout' ? 'error' : 'info';
4290
+ const requestedSeverity = extraMeta?.severity;
4291
+ const severity = requestedSeverity === 'warn'
4292
+ ? 'warn'
4293
+ : status === 'error' || status === 'timeout' ? 'error' : 'info';
3902
4294
  const stateMap = {
3903
4295
  start: 'started',
3904
4296
  done: 'completed',
@@ -3917,7 +4309,10 @@ export class AUNChannel {
3917
4309
  severity,
3918
4310
  terminal: ['done', 'interrupted', 'error', 'timeout'].includes(status),
3919
4311
  };
3920
- const metadata = extraMeta ? this.stripUndefinedDeep(extraMeta) : undefined;
4312
+ const metadataInput = extraMeta ? { ...extraMeta } : undefined;
4313
+ if (metadataInput)
4314
+ delete metadataInput.severity;
4315
+ const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
3921
4316
  if (metadata && Object.keys(metadata).length > 0)
3922
4317
  statusPayload.metadata = metadata;
3923
4318
  if (context?.threadId)
@@ -3929,8 +4324,8 @@ export class AUNChannel {
3929
4324
  if (context?.replyToMessageId)
3930
4325
  statusPayload.ref_message_id = context.replyToMessageId;
3931
4326
  const notifyOptions = { ttlMs: 60_000 };
3932
- if (this.isGroupId(channelId))
3933
- notifyOptions.groupId = channelId;
4327
+ if (delivery.chatType === 'group')
4328
+ notifyOptions.groupId = delivery.groupId;
3934
4329
  else
3935
4330
  notifyOptions.to = channelId;
3936
4331
  this.trace('OUT', 'notify.task_status', {
@@ -3951,7 +4346,7 @@ export class AUNChannel {
3951
4346
  }).catch(() => { });
3952
4347
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true, undefined, undefined, 'notify');
3953
4348
  // 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
3954
- const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
4349
+ const targetLabel = delivery.chatType === 'group' ? delivery.groupId : this.peerLabel(channelId);
3955
4350
  const chatmode = context?.metadata?.chatmode ?? '?';
3956
4351
  const initiator = statusPayload.initiator ?? '';
3957
4352
  const refMsgId = statusPayload.ref_message_id ?? '';
@@ -3963,8 +4358,6 @@ export class AUNChannel {
3963
4358
  logger.info(logTaskStatus);
3964
4359
  }
3965
4360
  async sendCustomPayload(channelId, payload, context) {
3966
- if (!this.client || !this.connected)
3967
- return;
3968
4361
  // SDK 0.3.0 E2EE requires payload to be an object
3969
4362
  let payloadObj;
3970
4363
  try {
@@ -4198,21 +4591,25 @@ export class AUNChannelPlugin {
4198
4591
  channelKey: inst.name, // channelName 实际上就是 channelKey
4199
4592
  capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true, authenticatedApproval: true },
4200
4593
  send: async (envelope, payload) => {
4594
+ const channelId = envelope?.channelId;
4595
+ const delivery = requireEnvelopeDelivery(channelId, envelope?.delivery, envelope?.replyContext?.delivery);
4201
4596
  const parentCausation = normalizeCausation(envelope.causation ?? envelope.replyContext?.metadata?.causation);
4202
4597
  const outboundCausation = parentCausation ? deriveCausation(parentCausation) : undefined;
4203
4598
  const replyCtx = outboundCausation
4204
4599
  ? {
4205
4600
  ...(envelope.replyContext ?? {}),
4601
+ delivery,
4206
4602
  metadata: { ...(envelope.replyContext?.metadata ?? {}), causation: outboundCausation },
4207
4603
  }
4208
- : envelope.replyContext;
4604
+ : (envelope.replyContext || envelope.delivery)
4605
+ ? { ...(envelope.replyContext ?? {}), delivery }
4606
+ : undefined;
4209
4607
  if (outboundCausation) {
4210
4608
  recordCausationSpan(outboundCausation, 'message.outbound', {
4211
4609
  status: 'started',
4212
4610
  refs: { taskId: envelope.taskId, sessionId: envelope.sessionId },
4213
4611
  });
4214
4612
  }
4215
- const channelId = envelope.channelId;
4216
4613
  const taskBase = () => channel.buildTaskPayloadBase(envelope, replyCtx);
4217
4614
  switch (payload.kind) {
4218
4615
  case 'result.text':
@@ -4236,11 +4633,30 @@ export class AUNChannelPlugin {
4236
4633
  return;
4237
4634
  }
4238
4635
  case 'system.error': {
4239
- await channel.sendMessage(channelId, payload.text, replyCtx);
4636
+ const errorPayload = {
4637
+ type: 'error',
4638
+ ...taskBase(),
4639
+ subtype: payload.subtype,
4640
+ text: payload.text,
4641
+ message: payload.text,
4642
+ user_message: payload.text,
4643
+ recoverable: payload.recoverable === true,
4644
+ terminal: payload.recoverable !== true,
4645
+ };
4646
+ await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
4240
4647
  return;
4241
4648
  }
4242
4649
  case 'result.error': {
4243
- await channel.sendMessage(channelId, payload.text, replyCtx);
4650
+ const errorPayload = {
4651
+ type: 'error',
4652
+ ...taskBase(),
4653
+ reason: payload.reason,
4654
+ text: payload.text,
4655
+ message: payload.text,
4656
+ user_message: payload.text,
4657
+ terminal: true,
4658
+ };
4659
+ await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
4244
4660
  return;
4245
4661
  }
4246
4662
  case 'result.file': {
@@ -4322,7 +4738,7 @@ export class AUNChannelPlugin {
4322
4738
  aunCard.text = action.body;
4323
4739
  setIfDefined(aunCard, 'format', action.bodyFormat);
4324
4740
  }
4325
- if (req.initiatorId && channel.isGroupId(channelId))
4741
+ if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
4326
4742
  aunCard.initiator = req.initiatorId;
4327
4743
  if (replyCtx?.threadId)
4328
4744
  aunCard.thread_id = replyCtx.threadId;
@@ -4335,6 +4751,7 @@ export class AUNChannelPlugin {
4335
4751
  requestId: req.id,
4336
4752
  isCommandCard: false,
4337
4753
  initiatorAid: req.initiatorId,
4754
+ delivery: replyCtx?.delivery,
4338
4755
  expiresAt: Date.now() + cardTtlMs,
4339
4756
  },
4340
4757
  });
@@ -4359,7 +4776,7 @@ export class AUNChannelPlugin {
4359
4776
  aunCard.text = card.body;
4360
4777
  setIfDefined(aunCard, 'format', card.bodyFormat);
4361
4778
  }
4362
- if (req.initiatorId && channel.isGroupId(channelId))
4779
+ if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
4363
4780
  aunCard.initiator = req.initiatorId;
4364
4781
  if (replyCtx?.threadId)
4365
4782
  aunCard.thread_id = replyCtx.threadId;
@@ -4372,6 +4789,7 @@ export class AUNChannelPlugin {
4372
4789
  requestId: req.id,
4373
4790
  isCommandCard: true,
4374
4791
  initiatorAid: req.initiatorId,
4792
+ delivery: replyCtx?.delivery,
4375
4793
  expiresAt: Date.now() + cardTtlMs,
4376
4794
  },
4377
4795
  });
@@ -4438,15 +4856,12 @@ export class AUNChannelPlugin {
4438
4856
  });
4439
4857
  });
4440
4858
  }
4441
- if (typeof channel.setDispatchModeResolver === 'function') {
4442
- channel.setDispatchModeResolver(async (channelId) => {
4443
- // 配置层用 mentionMode(mention-only/disabled),翻译成 AUN 协议
4444
- // dispatch_mode(mention/broadcast);未设值返回 undefined,回退服务端下发值。
4445
- const mentionMode = resolveEffective({
4859
+ if (typeof channel.setMentionModeResolver === 'function') {
4860
+ channel.setMentionModeResolver(async (channelId) => {
4861
+ return resolveEffective({
4446
4862
  self: aid,
4447
4863
  peerKey: formatPeerKey('aun', channelId),
4448
4864
  }, { cache: true }).mentionMode;
4449
- return mentionModeToDispatch(mentionMode);
4450
4865
  });
4451
4866
  }
4452
4867
  },