evolcore 0.0.11 → 0.0.12

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 (102) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/dist/agents/claude-runner.js +35 -2
  4. package/dist/agents/codex-app-server-client.js +120 -14
  5. package/dist/agents/codex-runner.js +273 -35
  6. package/dist/agents/ecagent-runner.js +36 -4
  7. package/dist/agents/gemini-runner.js +1 -0
  8. package/dist/aun/aid/client.js +11 -21
  9. package/dist/aun/aid/managed-operation.js +107 -0
  10. package/dist/aun/msg/group.js +81 -12
  11. package/dist/aun/msg/managed-operation.js +804 -0
  12. package/dist/aun/msg/mention-schema.js +20 -0
  13. package/dist/aun/msg/p2p.js +7 -0
  14. package/dist/channels/aun.js +288 -108
  15. package/dist/channels/daemon.js +13 -0
  16. package/dist/cli/agent-command.js +56 -8
  17. package/dist/cli/agent.js +170 -6
  18. package/dist/cli/aun-commands.js +270 -69
  19. package/dist/cli/bench.js +12 -3
  20. package/dist/cli/daemon-commands.js +31 -31
  21. package/dist/cli/fs-command.js +60 -0
  22. package/dist/cli/handoff-command.js +3 -0
  23. package/dist/cli/index.js +30 -2
  24. package/dist/cli/managed-command-guard.js +41 -0
  25. package/dist/cli/model.js +11 -4
  26. package/dist/cli/queue-command.js +11 -1
  27. package/dist/cli/response.js +71 -0
  28. package/dist/cli/restart-monitor.js +4 -18
  29. package/dist/cli/trigger-command.js +4 -0
  30. package/dist/config/builtin-roles.js +4 -0
  31. package/dist/config/contact-book-store.js +11 -3
  32. package/dist/config/contact-request-service.js +161 -15
  33. package/dist/config/mention-mode.js +8 -24
  34. package/dist/core/auth/agent-delegation.js +0 -1
  35. package/dist/core/auth/operation-authorizer.js +30 -7
  36. package/dist/core/auth/operation-catalog.js +187 -25
  37. package/dist/core/bootstrap-messages.js +18 -0
  38. package/dist/core/bootstrap-service.js +63 -4
  39. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  40. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  41. package/dist/core/capability/skill-discovery.js +55 -0
  42. package/dist/core/channel-loader.js +1 -1
  43. package/dist/core/command/cli-intent-parser.js +9 -0
  44. package/dist/core/command/command-handler.js +467 -6
  45. package/dist/core/command/connect-menu.js +15 -0
  46. package/dist/core/command/group-menu.js +4 -4
  47. package/dist/core/command/menu-catalog.js +366 -0
  48. package/dist/core/command/menu-handler.js +147 -35
  49. package/dist/core/command/menu-protocol.js +71 -0
  50. package/dist/core/command/slash-gate.js +5 -2
  51. package/dist/core/command/slash-handler.js +41 -21
  52. package/dist/core/daemon-file-cache.js +2 -1
  53. package/dist/core/data-migration.js +9 -2
  54. package/dist/core/evolagent.js +4 -2
  55. package/dist/core/handoff/dispatcher.js +5 -2
  56. package/dist/core/handoff/runtime.js +145 -21
  57. package/dist/core/handoff/store.js +24 -0
  58. package/dist/core/handoff/types.js +1 -0
  59. package/dist/core/message/logical-queue-bridge.js +1 -0
  60. package/dist/core/message/mention-schema.js +126 -0
  61. package/dist/core/message/message-bridge.js +236 -78
  62. package/dist/core/message/message-log.js +1 -0
  63. package/dist/core/message/message-queue.js +1 -0
  64. package/dist/core/message/response-engine.js +54 -36
  65. package/dist/core/permission/ec-command-parser.js +156 -10
  66. package/dist/core/permission/sandbox-runtime.js +22 -1
  67. package/dist/core/permission/tool-policy.js +73 -30
  68. package/dist/core/protected-paths.js +7 -2
  69. package/dist/core/session/session-manager.js +21 -2
  70. package/dist/eck/manifest-engine.js +39 -13
  71. package/dist/index.js +384 -98
  72. package/dist/ipc.js +218 -9
  73. package/dist/response-system/coordinator.js +6 -4
  74. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  75. package/dist/response-system/modes/single-session/index.js +14 -1
  76. package/dist/response-system/selector.js +6 -5
  77. package/dist/trigger/anomaly-store.js +4 -0
  78. package/dist/trigger/script-executor.js +1 -0
  79. package/dist/utils/codex-app-server-registry.js +90 -0
  80. package/dist/utils/codex-cli.js +5 -1
  81. package/dist/utils/cross-platform.js +15 -0
  82. package/dist/utils/npm-ops.js +5 -12
  83. package/dist/utils/tool-summary.js +11 -0
  84. package/kits/docs/channels/aun.md +1 -1
  85. package/kits/docs/context-assembly.md +2 -2
  86. package/kits/docs/evolcore/agent.md +11 -6
  87. package/kits/docs/evolcore/aid.md +14 -0
  88. package/kits/docs/prompt-loading-architecture.md +1 -2
  89. package/kits/docs/venues/group.md +1 -1
  90. package/kits/eck_manifest.json +0 -12
  91. package/kits/rules/04-relation.md +1 -1
  92. package/kits/rules/05-venue.md +2 -2
  93. package/kits/schemas/single-session.schema.1.json +3 -3
  94. package/kits/schemas/single-session.schema.2.json +3 -3
  95. package/kits/templates/roles/admin.json +22 -0
  96. package/kits/templates/roles/member.json +27 -0
  97. package/kits/templates/roles/visitor.json +33 -3
  98. package/kits/templates/system-fragments/bootstrap.md +1 -1
  99. package/kits/templates/system-fragments/channel.md +1 -1
  100. package/kits/templates/system-fragments/session.md +1 -1
  101. package/package.json +2 -2
  102. package/dist/core/command/evol-menu-version-gate.js +0 -39
@@ -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';
@@ -34,7 +34,9 @@ import { recordCausationSpan } from '../core/causation/audit.js';
34
34
  import { isExplicitGroupId } from '../aun/group-identity.js';
35
35
  import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
36
36
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
37
- import { postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox } from '../core/bootstrap-messages.js';
37
+ import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
38
+ import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
39
+ import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
38
40
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
39
41
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
40
42
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
@@ -139,7 +141,7 @@ export function aunOptsToInbound(opts, channel, channelType) {
139
141
  replyContext: opts.replyContext,
140
142
  source: opts.source,
141
143
  images: opts.images,
142
- dispatchMode: opts.dispatchMode,
144
+ mentionMode: opts.mentionMode,
143
145
  };
144
146
  }
145
147
  function verifiedProtectedHeaders(msg, env) {
@@ -698,24 +700,20 @@ export class AUNChannel {
698
700
  .replace(/[ \t]+/g, ' ')
699
701
  .trim();
700
702
  }
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);
703
+ parsePayloadMentionsOrReject(payload, context, messageId, seq) {
704
+ const raw = payload && typeof payload === 'object' && !Array.isArray(payload)
705
+ ? payload.mentions
706
+ : undefined;
707
+ try {
708
+ return normalizeAunMentionEntries(raw);
708
709
  }
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;
710
+ catch (error) {
711
+ this.acknowledgeImmediately(messageId, seq);
712
+ const detail = error instanceof Error ? error.message : String(error);
713
+ const code = error?.code ?? 'INVALID_MENTION_SCHEMA';
714
+ logger.error(`${this.logPrefix()} Dropped ${context}: invalid payload.mentions (${detail}) code=${code} mid=${messageId}`);
715
+ return undefined;
717
716
  }
718
- return false;
719
717
  }
720
718
  /** 从正文提取 @aid(AID 格式:至少三段域名),用于出站填充 payload.mentions */
721
719
  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 +729,19 @@ export class AUNChannel {
731
729
  }
732
730
  return out;
733
731
  }
732
+ /**
733
+ * Convert the AUN text mention notation into the canonical wire entries.
734
+ * `@all` is intentionally represented by its scope discriminator instead
735
+ * of being treated as an AID.
736
+ */
737
+ extractMentionEntriesFromText(text) {
738
+ const entries = [];
739
+ if (this.hasExplicitMention(text, 'all'))
740
+ entries.push({ scope: 'all' });
741
+ for (const aid of this.extractMentionAidsFromText(text))
742
+ entries.push({ aid });
743
+ return entries;
744
+ }
734
745
  /** Sanitize a raw topicName from untrusted payload: trim, strip control chars, enforce length. */
735
746
  sanitizeTopicName(raw) {
736
747
  if (typeof raw !== 'string')
@@ -755,6 +766,31 @@ export class AUNChannel {
755
766
  }
756
767
  return replyContext;
757
768
  }
769
+ claimInboundMessage(kind, messageId, seq) {
770
+ if (!messageId)
771
+ return true;
772
+ // Channel instance names are commonly reused across agents (the default
773
+ // is simply `aun`). Include the receiving AID so a server-scoped
774
+ // message_id cannot suppress an unrelated delivery in another channel.
775
+ const namespace = `${this.config.aid}:${this.config.channelName ?? 'aun'}:aun:${kind}`;
776
+ const dedupeKey = JSON.stringify([namespace, messageId]);
777
+ const existing = this.inboundSeenMessages.get(dedupeKey);
778
+ if (existing) {
779
+ if (seq !== undefined && existing.seq !== undefined && seq !== existing.seq) {
780
+ logger.warn(`${this.logPrefix()} Duplicate message_id with conflicting seq: namespace=${namespace} mid=${messageId} firstSeq=${existing.seq} duplicateSeq=${seq}`);
781
+ }
782
+ else {
783
+ logger.debug(`${this.logPrefix()} Duplicate inbound message dropped: namespace=${namespace} mid=${messageId} seq=${seq ?? 'unknown'}`);
784
+ }
785
+ this.acknowledgeImmediately(messageId, seq);
786
+ return false;
787
+ }
788
+ this.inboundSeenMessages.set(dedupeKey, { seenAt: Date.now(), seq });
789
+ if (seq !== undefined)
790
+ this.messageSeqMap.set(messageId, seq);
791
+ setTimeout(() => this.inboundSeenMessages.delete(dedupeKey), 5 * 60 * 1000);
792
+ return true;
793
+ }
758
794
  acknowledgeImmediately(messageId, _seq) {
759
795
  // SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
760
796
  // as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
@@ -771,10 +807,16 @@ export class AUNChannel {
771
807
  _aid;
772
808
  _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
773
809
  _chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
774
- seenMessages = new Map();
810
+ /**
811
+ * Inbound application dedupe. The AUN SDK may redeliver the same message
812
+ * with a different transport sequence, so message_id is the identity and
813
+ * seq is only retained for ACK/conflict diagnostics. Claiming happens at
814
+ * the event handler boundary, before payload/slash/mention parsing.
815
+ */
816
+ inboundSeenMessages = new Map();
775
817
  groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
776
818
  peerInfoCache = new Map();
777
- messageSeqMap = new Map(); // messageId → seq (for ack)
819
+ messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
778
820
  sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
779
821
  peerE2ee = new Map();
780
822
  static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
@@ -789,7 +831,7 @@ export class AUNChannel {
789
831
  interactionCardMessageIds = new Map();
790
832
  /** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
791
833
  invalidatedInteractions = new Map();
792
- dispatchModeResolver;
834
+ mentionModeResolver;
793
835
  static PROACTIVE_ALLOW_TYPES = new Set([
794
836
  'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
795
837
  'merge', 'link', 'location', 'personal_card',
@@ -798,6 +840,10 @@ export class AUNChannel {
798
840
  static INJECT_REQUEST_TYPE = 'observer.inject';
799
841
  /** 绑定请求类型(Evol App → 控制 AID):快速路径,绕过白名单透传原始 payload JSON 给 bridge。 */
800
842
  static BIND_REQUEST_TYPE = 'bind.request';
843
+ /** 联系人申请本人状态查询:在普通消息准入前交给 bridge,身份只取认证信封。 */
844
+ static CONTACT_REQUEST_QUERY_TYPE = 'contact.request.query';
845
+ /** App 联系人申请提交:结构化控制请求,不是 slash command。 */
846
+ static CONTACT_REQUEST_SUBMIT_TYPE = 'contact.request.submit';
801
847
  // Reconnect state
802
848
  // SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
803
849
  // 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
@@ -1138,6 +1184,14 @@ export class AUNChannel {
1138
1184
  return false;
1139
1185
  return this.reconcilePostBootstrapWelcome();
1140
1186
  }
1187
+ async hasPendingPostBootstrapWelcome() {
1188
+ const aid = this.config.aid.replace(/^@/, '');
1189
+ return hasPendingPostBootstrapWelcomeOutbox(aid);
1190
+ }
1191
+ async bindPostBootstrapWelcomeToSession(sessionId) {
1192
+ const aid = this.config.aid.replace(/^@/, '');
1193
+ return bindPostBootstrapWelcomeOutboxSession(aid, sessionId);
1194
+ }
1141
1195
  /**
1142
1196
  * Resume delivery of an already prepared completion welcome.
1143
1197
  *
@@ -1394,12 +1448,21 @@ export class AUNChannel {
1394
1448
  if (!data || typeof data !== 'object')
1395
1449
  return;
1396
1450
  const msg = data;
1451
+ // Claim before any payload extraction or command parsing. Retransmits may
1452
+ // carry a new seq, but the application identity remains message_id.
1453
+ const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
1454
+ const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
1455
+ if (!this.claimInboundMessage('private', messageId, seq))
1456
+ return;
1397
1457
  // SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1398
1458
  // message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
1399
1459
  const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1400
1460
  const protectedHeaders = verifiedProtectedHeaders(msg, env);
1401
1461
  const fromAid = env.from ?? '';
1402
1462
  const payload = msg.payload ?? '';
1463
+ const mentions = this.parsePayloadMentionsOrReject(payload, 'p2p.inbound', messageId, seq);
1464
+ if (!mentions)
1465
+ return;
1403
1466
  const causationRef = payload && typeof payload === 'object' && typeof payload._evolcore_causation_ref === 'string'
1404
1467
  ? payload._evolcore_causation_ref
1405
1468
  : undefined;
@@ -1413,15 +1476,19 @@ export class AUNChannel {
1413
1476
  const topicName = (typeof payload === 'object' && payload !== null)
1414
1477
  ? this.sanitizeTopicName(payload.topicName)
1415
1478
  : undefined;
1416
- const messageId = msg.message_id ?? '';
1417
1479
  const causation = consumeAunCausation(causationRef || messageId, fromAid, this._aid || this.config.aid)
1418
1480
  ?? (causationRef ? consumeAunCausation(messageId, fromAid, this._aid || this.config.aid) : undefined);
1419
- const seq = msg.seq;
1481
+ const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
1482
+ // 终态错误只作为展示信号,不能进入 Agent,也不能被 observer 再次传播。
1483
+ if (inboundType === 'error') {
1484
+ this.acknowledgeImmediately(messageId, seq);
1485
+ logger.info(`${this.logPrefix()} P2P dropped terminal error: from=${fromAid || 'unknown'} mid=${messageId}`);
1486
+ return;
1487
+ }
1420
1488
  // Observer forward (inbound):在所有过滤之前转发原始明文 payload。
1421
1489
  // forwardInbound 内部排除 self-echo 与 from-owner。
1422
1490
  // 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
1423
1491
  // (即便日后 from-owner 排除规则调整,也不会泄漏)。
1424
- const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
1425
1492
  if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
1426
1493
  this.forwardInbound(msg);
1427
1494
  }
@@ -1437,10 +1504,13 @@ export class AUNChannel {
1437
1504
  const msgEncrypted = !!env.encrypted;
1438
1505
  if (!msgEncrypted)
1439
1506
  this.plaintextRecv++;
1440
- // Detect @mentions
1441
- const mentions = [];
1442
- if (this._aid && text.includes(`@${this._aid}`)) {
1443
- mentions.push(this._aid);
1507
+ // Preserve structured mentions for the shared inbound message model.
1508
+ // Text scanning remains only a rendering fallback and is not promoted to
1509
+ // a structured @all decision.
1510
+ if (mentions.length === 0 && this._aid && text.includes(`@${this._aid}`)) {
1511
+ // Private channels historically exposed a self mention even when the
1512
+ // sender omitted payload.mentions; keep that compatibility signal.
1513
+ mentions.push({ aid: this._aid });
1444
1514
  }
1445
1515
  // Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
1446
1516
  const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid);
@@ -1462,8 +1532,10 @@ export class AUNChannel {
1462
1532
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1463
1533
  return;
1464
1534
  }
1465
- // menu.* 协议:自定义消息快速路径,需要原始 payload JSON 传递给 bridge
1466
- if (p2pPayloadType.startsWith('menu.')) {
1535
+ // menu.* 与联系人本人状态查询:需要原始 payload JSON 传递给 bridge
1536
+ if (p2pPayloadType.startsWith('menu.')
1537
+ || p2pPayloadType === AUNChannel.CONTACT_REQUEST_QUERY_TYPE
1538
+ || p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
1467
1539
  this.acknowledgeImmediately(messageId, seq);
1468
1540
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1469
1541
  this.dispatchMessage({
@@ -1474,6 +1546,8 @@ export class AUNChannel {
1474
1546
  peerType: peerIdentity.type,
1475
1547
  encrypted: msgEncrypted,
1476
1548
  protectedHeaders,
1549
+ mentions,
1550
+ mentionAids: mentionEntryAids(mentions),
1477
1551
  replyContext: this.buildGroupReplyContext(threadId, fromAid, msgEncrypted, messageId),
1478
1552
  msgType: logDescriptor.msgType,
1479
1553
  payloadType: logDescriptor.payloadType,
@@ -1502,6 +1576,8 @@ export class AUNChannel {
1502
1576
  peerName: displayName || undefined,
1503
1577
  peerType: peerIdentity.type,
1504
1578
  encrypted: msgEncrypted,
1579
+ mentions,
1580
+ mentionAids: mentionEntryAids(mentions),
1505
1581
  msgType: logDescriptor.msgType,
1506
1582
  payloadType: logDescriptor.payloadType,
1507
1583
  payloadSummary: logDescriptor.payloadSummary,
@@ -1561,28 +1637,38 @@ export class AUNChannel {
1561
1637
  if (!data || typeof data !== 'object')
1562
1638
  return;
1563
1639
  const msg = data;
1640
+ // Deduplicate at the adapter boundary, before mention/slash routing.
1641
+ const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
1642
+ const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
1643
+ if (!this.claimInboundMessage('group', messageId, seq))
1644
+ return;
1564
1645
  // SDK 0.5.* 移除了顶层 from/sender_aid/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1565
- // message_id / seq / payload / same_* / dispatch_mode 等仍是顶层独立字段,不在 envelope 内。
1646
+ // message_id / seq / payload / same_* / mention_mode 等仍是顶层独立字段,不在 envelope 内。
1566
1647
  const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1567
1648
  const protectedHeaders = verifiedProtectedHeaders(msg, env);
1568
1649
  const groupId = env.group_id ?? '';
1569
1650
  const senderAid = env.from ?? '';
1570
1651
  const payload = msg.payload ?? '';
1652
+ const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId, seq);
1653
+ if (!payloadMentionEntries)
1654
+ return;
1655
+ const payloadMentionAids = mentionEntryAids(payloadMentionEntries);
1571
1656
  const logDescriptor = classifyAunPayloadForLog(payload);
1572
1657
  const text = this.extractTextPayload(payload, groupId, senderAid);
1573
1658
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1574
1659
  const topicName = (typeof payload === 'object' && payload !== null)
1575
1660
  ? this.sanitizeTopicName(payload.topicName)
1576
1661
  : undefined;
1577
- const messageId = msg.message_id ?? '';
1578
- const seq = msg.seq;
1662
+ const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
1663
+ // 终态错误只作为展示信号,不能进入 Agent,也不能被 observer 再次传播。
1664
+ if (inboundType === 'error') {
1665
+ this.acknowledgeImmediately(messageId, seq);
1666
+ logger.info(`${this.logPrefix()} Group dropped terminal error: group=${groupId || 'unknown'} sender=${senderAid || 'unknown'} mid=${messageId}`);
1667
+ return;
1668
+ }
1579
1669
  // Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
1580
1670
  // forwardInbound 内部排除 self-echo 与 from-owner。
1581
1671
  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
1672
  logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
1587
1673
  if (!groupId || !senderAid) {
1588
1674
  this.acknowledgeImmediately(messageId, seq);
@@ -1654,6 +1740,10 @@ export class AUNChannel {
1654
1740
  chatType: 'group', messageId, seq, groupId,
1655
1741
  encrypted: menuEncrypted,
1656
1742
  protectedHeaders,
1743
+ mentions: payloadMentionEntries,
1744
+ mentionAids: payloadMentionAids,
1745
+ isMentioned: hasMentionAll(payloadMentionEntries)
1746
+ || (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
1657
1747
  replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName),
1658
1748
  msgType: logDescriptor.msgType,
1659
1749
  payloadType: logDescriptor.payloadType,
@@ -1673,17 +1763,17 @@ export class AUNChannel {
1673
1763
  const msgEncrypted = !!env.encrypted;
1674
1764
  if (!msgEncrypted)
1675
1765
  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');
1766
+ // mention_mode 是唯一的服务端群 @ 策略字段;缺失/非法时使用协议内置默认 disabled。
1767
+ const serverMentionMode = normalizeMentionMode(msg.mention_mode)
1768
+ ?? normalizeMentionMode(payload?.mention_mode)
1769
+ ?? 'disabled';
1770
+ const localMentionMode = normalizeMentionMode(this.mentionModeResolver
1771
+ ? await this.mentionModeResolver(groupId).catch(() => undefined)
1772
+ : undefined);
1773
+ const mentionMode = localMentionMode ?? serverMentionMode;
1774
+ const mentionedSelf = !!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid);
1775
+ // @all 仅认结构化 mentions(payload.mentions),不扫描正文,避免引述性 "@all" 误判。
1776
+ const mentionedAll = hasMentionAll(payloadMentionEntries);
1687
1777
  // Echo 机制优先于 mention 过滤:消息第一行包含 echo 时触发
1688
1778
  // 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
1689
1779
  const firstLineGroup = text.split('\n')[0] || '';
@@ -1718,16 +1808,17 @@ export class AUNChannel {
1718
1808
  }
1719
1809
  else {
1720
1810
  // 非 echo 消息:mention 标记(不再过滤,交给响应模式决定)
1721
- // 命令豁免 broadcast:slash 命令在任何 dispatchMode 下都强制走 mention 语义,
1811
+ // slash 命令在任何 mentionMode 下都强制走 mention-only 语义
1722
1812
  // 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
1723
- const enforceMention = dispatchMode === 'mention' || isCommandMsg;
1813
+ const enforceMention = mentionMode === 'mention-only' || isCommandMsg;
1724
1814
  const isMentioned = mentionedSelf || mentionedAll;
1725
1815
  // 过滤逻辑下移到响应层:这里只标记,不过滤
1726
1816
  // 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
1727
1817
  // TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
1728
1818
  if (enforceMention && !isMentioned && !isOwnHandoff) {
1729
1819
  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))})`);
1820
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId);
1821
+ 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
1822
  return;
1732
1823
  }
1733
1824
  }
@@ -1745,9 +1836,7 @@ export class AUNChannel {
1745
1836
  logger.debug(`${this.logPrefix()} Group dropped: empty text and no attachments (group=${groupId} sender=${senderAid} mid=${messageId})`);
1746
1837
  return;
1747
1838
  }
1748
- const mentions = mentionedAll
1749
- ? ['all']
1750
- : mentionedSelf && this._aid ? [this._aid] : [];
1839
+ const mentions = payloadMentionEntries;
1751
1840
  // Process attachments
1752
1841
  const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments);
1753
1842
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
@@ -1758,27 +1847,27 @@ export class AUNChannel {
1758
1847
  const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1759
1848
  const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
1760
1849
  const textMentionAll = this.hasExplicitMention(text, 'all');
1761
- const structMentionSelf = this._aid ? payloadMentions.includes(this._aid) : false;
1762
- const structMentionAll = payloadMentions.includes('all');
1850
+ const structMentionSelf = this._aid ? payloadMentionAids.includes(this._aid) : false;
1851
+ const structMentionAll = mentionedAll;
1763
1852
  const reason = isOwnHandoff
1764
1853
  ? 'handoff.self'
1765
1854
  : mentionedAll
1766
1855
  ? 'mention.all(struct)'
1767
1856
  : mentionedSelf
1768
1857
  ? (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))}`);
1858
+ : `${mentionMode}.no-mention`;
1859
+ 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
1860
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1772
1861
  if (payloadType === 'action_card_reply')
1773
1862
  return;
1774
1863
  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)}`);
1864
+ logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mentionMode=${mentionMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
1776
1865
  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
1866
  this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode, payloadType === 'event' ? 'notify' : 'send');
1778
1867
  // 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
1779
1868
  // 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
1780
1869
  const renderMentionAids = Array.from(new Set([
1781
- ...payloadMentions,
1870
+ ...payloadMentionAids,
1782
1871
  ...this.extractMentionAidsFromText(text),
1783
1872
  ]));
1784
1873
  this.dispatchMessage({
@@ -1801,7 +1890,7 @@ export class AUNChannel {
1801
1890
  source: isOwnHandoff ? 'handoff' : undefined,
1802
1891
  encrypted: msgEncrypted,
1803
1892
  replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName),
1804
- dispatchMode: serverDispatchMode,
1893
+ mentionMode: serverMentionMode,
1805
1894
  images: inboundImages.length > 0 ? inboundImages : undefined,
1806
1895
  msgType: logDescriptor.msgType,
1807
1896
  payloadType: logDescriptor.payloadType,
@@ -1810,17 +1899,9 @@ export class AUNChannel {
1810
1899
  });
1811
1900
  }
1812
1901
  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
- }
1902
+ // Inbound events are claimed in handleIncomingPrivateMessage / GroupMessage
1903
+ // before parsing. Keeping dedupe there prevents duplicate slash commands
1904
+ // from reaching routing and mention policy code.
1824
1905
  // Echo 机制:消息第一行包含 "echo"(不区分大小写)且原始内容 ≤10 字符时,直接回声
1825
1906
  // 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
1826
1907
  const firstLine = event.text.split('\n')[0] || '';
@@ -1852,7 +1933,6 @@ export class AUNChannel {
1852
1933
  }
1853
1934
  if (!this.messageHandler)
1854
1935
  return;
1855
- const mentionObjects = event.mentions?.map(aid => ({ userId: aid }));
1856
1936
  // Use caller-supplied replyContext (group path builds mentionUserIds);
1857
1937
  // fall back to simple threadId-only context for private messages
1858
1938
  let replyContext = event.replyContext;
@@ -1877,12 +1957,12 @@ export class AUNChannel {
1877
1957
  messageId: event.messageId,
1878
1958
  causation: event.causation,
1879
1959
  threadId: event.threadId,
1880
- mentions: mentionObjects,
1960
+ mentions: event.mentions,
1881
1961
  mentionAids: event.mentionAids,
1882
1962
  isMentioned: event.isMentioned, // ← 新增:传递 isMentioned 标记
1883
1963
  replyContext,
1884
1964
  source: event.source,
1885
- dispatchMode: event.dispatchMode,
1965
+ mentionMode: event.mentionMode,
1886
1966
  images: event.images,
1887
1967
  msgType: event.msgType,
1888
1968
  payloadType: event.payloadType,
@@ -2421,8 +2501,8 @@ export class AUNChannel {
2421
2501
  onMessage(handler) {
2422
2502
  this.messageHandler = handler;
2423
2503
  }
2424
- setDispatchModeResolver(resolver) {
2425
- this.dispatchModeResolver = resolver;
2504
+ setMentionModeResolver(resolver) {
2505
+ this.mentionModeResolver = resolver;
2426
2506
  }
2427
2507
  onRecall(handler) {
2428
2508
  this.recallHandler = handler;
@@ -2511,6 +2591,21 @@ export class AUNChannel {
2511
2591
  finalPayload.chatmode = context.metadata.chatmode;
2512
2592
  return finalPayload;
2513
2593
  }
2594
+ /**
2595
+ * Validate the AUN-specific structured field before it crosses the durable
2596
+ * send boundary. Return a copy when mentions are present so callers do not
2597
+ * retain an unvalidated array reference.
2598
+ */
2599
+ normalizeAunPayloadMentions(payload) {
2600
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
2601
+ return payload;
2602
+ if (!Object.prototype.hasOwnProperty.call(payload, 'mentions'))
2603
+ return payload;
2604
+ return {
2605
+ ...payload,
2606
+ mentions: normalizeAunMentionEntries(payload.mentions),
2607
+ };
2608
+ }
2514
2609
  rememberInvalidatedInteraction(interactionId, reason) {
2515
2610
  this.invalidatedInteractions.set(interactionId, reason);
2516
2611
  const timer = setTimeout(() => this.invalidatedInteractions.delete(interactionId), 24 * 60 * 60 * 1000);
@@ -2602,6 +2697,16 @@ export class AUNChannel {
2602
2697
  }
2603
2698
  }
2604
2699
  async sendAunPayload(channelId, payload, context, label) {
2700
+ let normalizedPayload;
2701
+ try {
2702
+ normalizedPayload = this.normalizeAunPayloadMentions(payload);
2703
+ }
2704
+ catch (error) {
2705
+ const detail = error instanceof Error ? error.message : String(error);
2706
+ const code = error?.code ?? 'INVALID_MENTION_SCHEMA';
2707
+ logger.error(`${this.logPrefix()} Dropped durable AUN payload: invalid payload.mentions (${detail}) code=${code} channel=${channelId}`);
2708
+ return { ok: false, discarded: true };
2709
+ }
2605
2710
  if (!this.client || !this.connected)
2606
2711
  return { ok: false };
2607
2712
  const isGroup = this.isGroupId(channelId);
@@ -2616,8 +2721,8 @@ export class AUNChannel {
2616
2721
  if (causationRef && causation)
2617
2722
  registerAunCausation(causationRef, this.config.aid, targetAid, causation);
2618
2723
  const payloadWithCausationRef = causationRef
2619
- ? { ...payload, _evolcore_causation_ref: causationRef }
2620
- : payload;
2724
+ ? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
2725
+ : normalizedPayload;
2621
2726
  // Truncate payload if too large (AUN SDK limit: 1MB)
2622
2727
  const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
2623
2728
  const params = { payload: truncatedPayload, encrypt };
@@ -2692,6 +2797,9 @@ export class AUNChannel {
2692
2797
  this.forwardOutbound(result);
2693
2798
  }
2694
2799
  async sendContentPayload(channelId, payload, opts) {
2800
+ // Validate the caller's raw mentions before any early-return policy (such
2801
+ // as a cancelled interaction) can hide a malformed wire payload.
2802
+ const validatedPayload = this.normalizeAunPayloadMentions(payload);
2695
2803
  const interactionId = opts.postSend?.type === 'register_interaction_card'
2696
2804
  ? opts.postSend.requestId
2697
2805
  : undefined;
@@ -2699,7 +2807,12 @@ export class AUNChannel {
2699
2807
  logger.info(`${this.logPrefix()} Dropped invalidated interaction before durable enqueue: request=${interactionId}`);
2700
2808
  return {};
2701
2809
  }
2702
- const finalPayload = this.applyReplyContextToPayload(payload, opts.context);
2810
+ // Validate before write-ahead enqueue. Invalid AUN payloads must not be
2811
+ // hidden in the outbox and emitted later after a reconnect.
2812
+ // Validate the caller's raw mentions before deep-cleaning: that helper
2813
+ // intentionally removes undefined array elements, which would otherwise
2814
+ // turn malformed `mentions: [undefined]` into an apparently valid `[]`.
2815
+ const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, opts.context));
2703
2816
  const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
2704
2817
  const entry = outbox.enqueue(this.config.aid, {
2705
2818
  channelId,
@@ -2859,9 +2972,27 @@ export class AUNChannel {
2859
2972
  }
2860
2973
  let finalText = text;
2861
2974
  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;
2975
+ if (this.isGroupId(channelId)) {
2976
+ const requestedMentions = Array.from(new Set((context?.mentionUserIds ?? [])
2977
+ .filter((id) => typeof id === 'string' && id.trim().length > 0)
2978
+ .map(id => id.trim())));
2979
+ // AUN's textual representation gives @all precedence over individual
2980
+ // targets, matching the cross-channel mention contract.
2981
+ const textMentionsAll = this.hasExplicitMention(finalText, 'all');
2982
+ const mentionTargets = requestedMentions.includes('all') || textMentionsAll
2983
+ ? ['all']
2984
+ : requestedMentions;
2985
+ if (mentionTargets.length > 0) {
2986
+ for (const target of mentionTargets.reverse()) {
2987
+ if (!this.hasExplicitMention(finalText, target)) {
2988
+ finalText = `@${target} ` + finalText;
2989
+ }
2990
+ }
2991
+ }
2992
+ else if (context?.peerId && !context.suppressPeerMention && !textMentionsAll) {
2993
+ if (!this.hasExplicitMention(finalText, context.peerId)) {
2994
+ finalText = `@${context.peerId} ` + finalText;
2995
+ }
2865
2996
  }
2866
2997
  }
2867
2998
  const operationId = typeof context?.metadata?.operationId === 'string'
@@ -2905,16 +3036,27 @@ export class AUNChannel {
2905
3036
  }
2906
3037
  /** Daemon-side transport for `ec msg send` running inside an agent task. */
2907
3038
  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
3039
  if (!args.to || this.isGroupId(args.to)) {
2912
3040
  return { ok: false, error: 'aun-msg-send only supports private AID targets', code: 'UNSUPPORTED_TARGET' };
2913
3041
  }
2914
3042
  if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
2915
3043
  return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
2916
3044
  }
2917
- const finalPayload = this.stripUndefinedDeep({ ...args.payload });
3045
+ let finalPayload;
3046
+ try {
3047
+ const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
3048
+ finalPayload = this.stripUndefinedDeep(validatedPayload);
3049
+ }
3050
+ catch (e) {
3051
+ return {
3052
+ ok: false,
3053
+ error: e?.message || String(e),
3054
+ code: e?.code,
3055
+ };
3056
+ }
3057
+ if (!this.connected || !this.client) {
3058
+ return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
3059
+ }
2918
3060
  const info = await this.fetchPeerInfo(args.to);
2919
3061
  const chatmode = info.type === 'human' ? 'interactive' : 'proactive';
2920
3062
  if (!finalPayload.chatmode)
@@ -3019,20 +3161,34 @@ export class AUNChannel {
3019
3161
  }
3020
3162
  /** Daemon-side transport for `ec group send` running inside an agent task. */
3021
3163
  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
3164
  if (!args.groupId || !this.isGroupId(args.groupId)) {
3026
3165
  return { ok: false, error: 'aun-msg-send requires a valid group target', code: 'UNSUPPORTED_TARGET' };
3027
3166
  }
3028
3167
  if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
3029
3168
  return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
3030
3169
  }
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
3170
  try {
3171
+ // Validate before deep-cleaning so undefined entries cannot be silently
3172
+ // removed from a malformed mentions array.
3173
+ const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
3174
+ const finalPayload = this.stripUndefinedDeep(validatedPayload);
3175
+ const payloadMentions = Object.prototype.hasOwnProperty.call(finalPayload, 'mentions')
3176
+ ? normalizeAunMentionEntries(finalPayload.mentions)
3177
+ : undefined;
3178
+ if (args.mentions !== undefined) {
3179
+ const normalizedMentions = normalizeAunMentionEntries(args.mentions);
3180
+ if (normalizedMentions.length > 0)
3181
+ finalPayload.mentions = normalizedMentions;
3182
+ else if (payloadMentions !== undefined)
3183
+ finalPayload.mentions = payloadMentions;
3184
+ }
3185
+ else if (payloadMentions !== undefined) {
3186
+ finalPayload.mentions = payloadMentions;
3187
+ }
3188
+ if (!this.connected || !this.client) {
3189
+ return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
3190
+ }
3191
+ const encrypt = args.encrypt ?? this.shouldEncrypt(args.groupId);
3036
3192
  const result = await this.callAndTrace('group.send', {
3037
3193
  group_id: args.groupId,
3038
3194
  payload: this.truncatePayloadIfNeeded(finalPayload, 'group-send-ipc'),
@@ -3154,7 +3310,7 @@ export class AUNChannel {
3154
3310
  const source = context?.metadata?.source ?? 'daemon';
3155
3311
  const payload = { type: 'text', text: finalText };
3156
3312
  if (this.isGroupId(channelId)) {
3157
- const extracted = this.extractMentionAidsFromText(finalText);
3313
+ const extracted = this.extractMentionEntriesFromText(finalText);
3158
3314
  if (extracted.length > 0)
3159
3315
  payload.mentions = extracted;
3160
3316
  }
@@ -3353,6 +3509,10 @@ export class AUNChannel {
3353
3509
  const context = entry.context;
3354
3510
  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
3511
  const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
3512
+ if (sent.discarded) {
3513
+ logger.warn(`${this.logPrefix()} Removed malformed durable payload from outbox: id=${entry.id} channel=${channelId}`);
3514
+ return { ok: true, discarded: true };
3515
+ }
3356
3516
  if (!sent.ok || !sent.messageId)
3357
3517
  return sent;
3358
3518
  const isGroup = this.isGroupId(channelId);
@@ -3415,13 +3575,14 @@ export class AUNChannel {
3415
3575
  * 存储键:group_id/peer_aid + sender_aid + context.type + context.id
3416
3576
  */
3417
3577
  async sendThought(channelId, taskId, payload, context) {
3418
- if (!this.connected || !this.client)
3419
- return;
3420
3578
  if (!taskId)
3421
3579
  return;
3580
+ const validatedPayload = this.normalizeAunPayloadMentions(payload);
3581
+ const finalPayload = this.normalizeAunPayloadMentions(this.stripUndefinedDeep(validatedPayload));
3582
+ if (!this.connected || !this.client)
3583
+ return;
3422
3584
  // 私聊 channelId = 对端 AID(不含 device_id)
3423
3585
  const targetId = channelId;
3424
- const finalPayload = this.stripUndefinedDeep(payload);
3425
3586
  const encrypt = context?.metadata?.encrypted != null
3426
3587
  ? !!(context.metadata.encrypted)
3427
3588
  : this.shouldEncrypt(targetId);
@@ -3491,6 +3652,10 @@ export class AUNChannel {
3491
3652
  }
3492
3653
  /** 发送结构化 AUN payload;transient 协议载荷不写本地 messages.jsonl。 */
3493
3654
  async sendStructured(channelId, payload, context) {
3655
+ // Validate before checking connection state so malformed producer payloads
3656
+ // are reported deterministically instead of being hidden by a disconnect.
3657
+ const validatedPayload = this.normalizeAunPayloadMentions(payload);
3658
+ const finalPayload = this.stripUndefinedDeep(validatedPayload);
3494
3659
  if (!this.connected || !this.client)
3495
3660
  return null;
3496
3661
  const isGroup = this.isGroupId(channelId);
@@ -3499,7 +3664,6 @@ export class AUNChannel {
3499
3664
  const encrypt = context?.metadata?.encrypted != null
3500
3665
  ? !!(context.metadata.encrypted)
3501
3666
  : this.shouldEncrypt(encryptTarget);
3502
- const finalPayload = this.stripUndefinedDeep({ ...payload });
3503
3667
  if (context?.threadId && !finalPayload.thread_id)
3504
3668
  finalPayload.thread_id = context.threadId;
3505
3669
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
@@ -4236,11 +4400,30 @@ export class AUNChannelPlugin {
4236
4400
  return;
4237
4401
  }
4238
4402
  case 'system.error': {
4239
- await channel.sendMessage(channelId, payload.text, replyCtx);
4403
+ const errorPayload = {
4404
+ type: 'error',
4405
+ ...taskBase(),
4406
+ subtype: payload.subtype,
4407
+ text: payload.text,
4408
+ message: payload.text,
4409
+ user_message: payload.text,
4410
+ recoverable: payload.recoverable === true,
4411
+ terminal: payload.recoverable !== true,
4412
+ };
4413
+ await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
4240
4414
  return;
4241
4415
  }
4242
4416
  case 'result.error': {
4243
- await channel.sendMessage(channelId, payload.text, replyCtx);
4417
+ const errorPayload = {
4418
+ type: 'error',
4419
+ ...taskBase(),
4420
+ reason: payload.reason,
4421
+ text: payload.text,
4422
+ message: payload.text,
4423
+ user_message: payload.text,
4424
+ terminal: true,
4425
+ };
4426
+ await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
4244
4427
  return;
4245
4428
  }
4246
4429
  case 'result.file': {
@@ -4438,15 +4621,12 @@ export class AUNChannelPlugin {
4438
4621
  });
4439
4622
  });
4440
4623
  }
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({
4624
+ if (typeof channel.setMentionModeResolver === 'function') {
4625
+ channel.setMentionModeResolver(async (channelId) => {
4626
+ return resolveEffective({
4446
4627
  self: aid,
4447
4628
  peerKey: formatPeerKey('aun', channelId),
4448
4629
  }, { cache: true }).mentionMode;
4449
- return mentionModeToDispatch(mentionMode);
4450
4630
  });
4451
4631
  }
4452
4632
  },