evolcore 0.0.10 → 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 (131) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +3 -3
  3. package/bin/codex-managed-hook.mjs +80 -0
  4. package/dist/agents/baseagent.js +4 -0
  5. package/dist/agents/claude-runner.js +158 -44
  6. package/dist/agents/codex-app-server-client.js +153 -23
  7. package/dist/agents/codex-runner.js +331 -43
  8. package/dist/agents/ecagent-runner.js +53 -6
  9. package/dist/agents/gemini-runner.js +1 -0
  10. package/dist/agents/request-identity.js +55 -0
  11. package/dist/aun/aid/client.js +11 -21
  12. package/dist/aun/aid/managed-operation.js +107 -0
  13. package/dist/aun/msg/group.js +81 -12
  14. package/dist/aun/msg/managed-operation.js +804 -0
  15. package/dist/aun/msg/mention-schema.js +20 -0
  16. package/dist/aun/msg/p2p.js +7 -0
  17. package/dist/aun/outbox.js +28 -31
  18. package/dist/channels/aun.js +418 -235
  19. package/dist/channels/daemon.js +13 -0
  20. package/dist/cli/agent-command.js +72 -17
  21. package/dist/cli/agent.js +249 -22
  22. package/dist/cli/aun-commands.js +270 -69
  23. package/dist/cli/bench.js +12 -3
  24. package/dist/cli/daemon-commands.js +52 -33
  25. package/dist/cli/fs-command.js +60 -0
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +106 -63
  28. package/dist/cli/init-cancel.js +208 -0
  29. package/dist/cli/init-channel.js +343 -195
  30. package/dist/cli/init.js +21 -9
  31. package/dist/cli/managed-command-guard.js +41 -0
  32. package/dist/cli/model.js +11 -4
  33. package/dist/cli/queue-command.js +11 -1
  34. package/dist/cli/response.js +71 -0
  35. package/dist/cli/restart-monitor.js +4 -18
  36. package/dist/cli/trigger-command.js +4 -0
  37. package/dist/config/builtin-roles.js +5 -0
  38. package/dist/config/contact-book-store.js +11 -3
  39. package/dist/config/contact-request-service.js +161 -15
  40. package/dist/config/gateway-config.js +26 -10
  41. package/dist/config/mention-mode.js +8 -24
  42. package/dist/core/agent-reload-coordinator.js +53 -0
  43. package/dist/core/auth/agent-delegation.js +0 -1
  44. package/dist/core/auth/operation-authorizer.js +62 -154
  45. package/dist/core/auth/operation-catalog.js +264 -22
  46. package/dist/core/bootstrap-messages.js +68 -0
  47. package/dist/core/bootstrap-service.js +144 -10
  48. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  49. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  50. package/dist/core/capability/skill-discovery.js +55 -0
  51. package/dist/core/channel-loader.js +24 -7
  52. package/dist/core/command/agent-control.js +14 -11
  53. package/dist/core/command/cli-intent-parser.js +9 -0
  54. package/dist/core/command/command-handler.js +467 -6
  55. package/dist/core/command/connect-menu.js +15 -0
  56. package/dist/core/command/group-menu.js +4 -4
  57. package/dist/core/command/menu-catalog.js +366 -0
  58. package/dist/core/command/menu-handler.js +213 -110
  59. package/dist/core/command/menu-protocol.js +71 -0
  60. package/dist/core/command/slash-gate.js +5 -2
  61. package/dist/core/command/slash-handler.js +45 -25
  62. package/dist/core/daemon-file-cache.js +2 -1
  63. package/dist/core/data-migration.js +9 -2
  64. package/dist/core/evolagent-registry.js +125 -35
  65. package/dist/core/evolagent.js +12 -5
  66. package/dist/core/handoff/dispatcher.js +5 -2
  67. package/dist/core/handoff/runtime.js +145 -21
  68. package/dist/core/handoff/store.js +24 -0
  69. package/dist/core/handoff/types.js +1 -0
  70. package/dist/core/inference/text-inference.js +38 -4
  71. package/dist/core/message/logical-queue-bridge.js +1 -0
  72. package/dist/core/message/mention-schema.js +126 -0
  73. package/dist/core/message/message-bridge.js +237 -79
  74. package/dist/core/message/message-log.js +23 -0
  75. package/dist/core/message/message-queue.js +20 -4
  76. package/dist/core/message/response-engine.js +54 -36
  77. package/dist/core/model/model-catalog.js +143 -24
  78. package/dist/core/model/model-diagnostics.js +28 -10
  79. package/dist/core/permission/ec-command-parser.js +156 -10
  80. package/dist/core/permission/index.js +1 -0
  81. package/dist/core/permission/readonly-shell-query.js +532 -0
  82. package/dist/core/permission/sandbox-runtime.js +22 -1
  83. package/dist/core/permission/shell-environment.js +46 -0
  84. package/dist/core/permission/tool-policy.js +292 -111
  85. package/dist/core/protected-paths.js +17 -9
  86. package/dist/core/runner-reload-transaction.js +57 -0
  87. package/dist/core/session/session-manager.js +21 -2
  88. package/dist/eck/manifest-engine.js +39 -13
  89. package/dist/index.js +644 -180
  90. package/dist/ipc.js +246 -19
  91. package/dist/response-system/coordinator.js +6 -4
  92. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  93. package/dist/response-system/modes/single-session/index.js +14 -1
  94. package/dist/response-system/selector.js +6 -5
  95. package/dist/trigger/anomaly-store.js +4 -0
  96. package/dist/trigger/script-executor.js +1 -0
  97. package/dist/utils/aid-bind.js +3 -8
  98. package/dist/utils/codex-app-server-registry.js +90 -0
  99. package/dist/utils/codex-cli.js +5 -1
  100. package/dist/utils/cross-platform.js +15 -0
  101. package/dist/utils/log-writer.js +6 -10
  102. package/dist/utils/logger.js +5 -5
  103. package/dist/utils/npm-ops.js +5 -12
  104. package/dist/utils/tool-summary.js +11 -0
  105. package/kits/docs/channels/aun.md +1 -1
  106. package/kits/docs/context-assembly.md +2 -2
  107. package/kits/docs/evolcore/agent.md +11 -6
  108. package/kits/docs/evolcore/aid.md +14 -0
  109. package/kits/docs/evolcore/msg.md +13 -0
  110. package/kits/docs/prompt-loading-architecture.md +1 -2
  111. package/kits/docs/venues/group.md +1 -1
  112. package/kits/eck_manifest.json +0 -12
  113. package/kits/rules/01-overview.md +9 -0
  114. package/kits/rules/04-relation.md +1 -1
  115. package/kits/rules/05-venue.md +2 -2
  116. package/kits/schemas/agent-config.schema.3.json +1 -1
  117. package/kits/schemas/agent-config.schema.4.json +1 -1
  118. package/kits/schemas/relation-config.schema.2.json +1 -1
  119. package/kits/schemas/role-config.schema.1.json +1 -1
  120. package/kits/schemas/single-session.schema.1.json +3 -3
  121. package/kits/schemas/single-session.schema.2.json +3 -3
  122. package/kits/templates/roles/admin.json +27 -0
  123. package/kits/templates/roles/member.json +44 -0
  124. package/kits/templates/roles/visitor.json +40 -2
  125. package/kits/templates/system-fragments/bootstrap.md +12 -6
  126. package/kits/templates/system-fragments/channel.md +6 -0
  127. package/kits/templates/system-fragments/session.md +2 -0
  128. package/package.json +3 -2
  129. package/skills/eclink/SKILL.md +15 -3
  130. package/skills/eclink/agents/openai.yaml +3 -3
  131. package/dist/core/command/evol-menu-version-gate.js +0 -39
@@ -7,20 +7,21 @@ 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, agentMdPath as agentMdPathFn, agentDir as agentDirPath, resolveRoot } from '../paths.js';
10
+ import { resolvePaths, agentDir as agentDirPath, resolveRoot } 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
- import { appendMessageLog, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog } from '../core/message/message-log.js';
13
+ import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, hasMessageLogOperation } from '../core/message/message-log.js';
14
14
  import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
15
15
  import { chatDirPath } from '../core/session/session-fs-store.js';
16
16
  import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
17
17
  import { appendAidLifecycle } from '../aun/aid/identity.js';
18
18
  import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
19
19
  import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
20
- import { loadAgent, saveAgent } from '../config-store.js';
20
+ import { loadAgent } from '../config-store.js';
21
+ import { normalizeAgentLifecycle } from '../config/lifecycle.js';
21
22
  import { resolveEffective } from '../config/config-manager.js';
22
23
  import { isManagementRole } from '../config/builtin-roles.js';
23
- import { mentionModeToDispatch } from '../config/mention-mode.js';
24
+ import { normalizeMentionMode } from '../config/mention-mode.js';
24
25
  import { getProcessStartTime } from '../utils/process-introspect.js';
25
26
  import * as outbox from '../aun/outbox.js';
26
27
  import { guessMime, formatSize } from '../utils/media-cache.js';
@@ -33,6 +34,9 @@ import { recordCausationSpan } from '../core/causation/audit.js';
33
34
  import { isExplicitGroupId } from '../aun/group-identity.js';
34
35
  import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
35
36
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.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';
36
40
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
37
41
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
38
42
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
@@ -137,7 +141,7 @@ export function aunOptsToInbound(opts, channel, channelType) {
137
141
  replyContext: opts.replyContext,
138
142
  source: opts.source,
139
143
  images: opts.images,
140
- dispatchMode: opts.dispatchMode,
144
+ mentionMode: opts.mentionMode,
141
145
  };
142
146
  }
143
147
  function verifiedProtectedHeaders(msg, env) {
@@ -200,7 +204,6 @@ export class AUNChannel {
200
204
  connected = false;
201
205
  traceWriter = null;
202
206
  eventBus = null;
203
- ownerBoundHandler = null;
204
207
  queuedHandler = null;
205
208
  pendingEchoMessages = new Map();
206
209
  isEchoSending = false;
@@ -697,24 +700,20 @@ export class AUNChannel {
697
700
  .replace(/[ \t]+/g, ' ')
698
701
  .trim();
699
702
  }
700
- extractMentionAids(mentions) {
701
- const aids = [];
702
- for (const m of mentions) {
703
- if (typeof m === 'string')
704
- aids.push(m);
705
- else if (m && typeof m === 'object' && typeof m.aid === 'string')
706
- 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);
707
709
  }
708
- return aids;
709
- }
710
- hasMentionAll(mentions) {
711
- for (const m of mentions) {
712
- if (m === 'all')
713
- return true;
714
- if (m && typeof m === 'object' && m.scope === 'all')
715
- 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;
716
716
  }
717
- return false;
718
717
  }
719
718
  /** 从正文提取 @aid(AID 格式:至少三段域名),用于出站填充 payload.mentions */
720
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;
@@ -730,6 +729,19 @@ export class AUNChannel {
730
729
  }
731
730
  return out;
732
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
+ }
733
745
  /** Sanitize a raw topicName from untrusted payload: trim, strip control chars, enforce length. */
734
746
  sanitizeTopicName(raw) {
735
747
  if (typeof raw !== 'string')
@@ -754,6 +766,31 @@ export class AUNChannel {
754
766
  }
755
767
  return replyContext;
756
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
+ }
757
794
  acknowledgeImmediately(messageId, _seq) {
758
795
  // SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
759
796
  // as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
@@ -770,10 +807,16 @@ export class AUNChannel {
770
807
  _aid;
771
808
  _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
772
809
  _chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
773
- 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();
774
817
  groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
775
818
  peerInfoCache = new Map();
776
- messageSeqMap = new Map(); // messageId → seq (for ack)
819
+ messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
777
820
  sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
778
821
  peerE2ee = new Map();
779
822
  static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
@@ -788,7 +831,7 @@ export class AUNChannel {
788
831
  interactionCardMessageIds = new Map();
789
832
  /** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
790
833
  invalidatedInteractions = new Map();
791
- dispatchModeResolver;
834
+ mentionModeResolver;
792
835
  static PROACTIVE_ALLOW_TYPES = new Set([
793
836
  'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
794
837
  'merge', 'link', 'location', 'personal_card',
@@ -797,6 +840,10 @@ export class AUNChannel {
797
840
  static INJECT_REQUEST_TYPE = 'observer.inject';
798
841
  /** 绑定请求类型(Evol App → 控制 AID):快速路径,绕过白名单透传原始 payload JSON 给 bridge。 */
799
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';
800
847
  // Reconnect state
801
848
  // SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
802
849
  // 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
@@ -833,9 +880,9 @@ export class AUNChannel {
833
880
  baseName: 'aun',
834
881
  logDir: resolvePaths().logs,
835
882
  rotation: 'hourly',
836
- retention: { hours: 12 },
883
+ retention: { hours: 24 },
837
884
  });
838
- logger.info(`${this.logPrefix()} Trace logging enabled (hourly rotation, 12h retention): ${this.traceWriter.activePath()}`);
885
+ logger.info(`${this.logPrefix()} Trace logging enabled (hourly rotation, 24h retention): ${this.traceWriter.activePath()}`);
839
886
  }
840
887
  this.aidState = {
841
888
  aid: config.aid,
@@ -1083,11 +1130,6 @@ export class AUNChannel {
1083
1130
  logger.info(`${this.logPrefix()} Connected as ${this._aid}`);
1084
1131
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
1085
1132
  appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
1086
- // Send welcome message to owner after first connection
1087
- // pureIdentity(控制 AID):跳过 evolagent onboarding(根除 warn 噪声 + 永不 agentmdPut)
1088
- if (!this.config.pureIdentity) {
1089
- await this.sendWelcomeMessage();
1090
- }
1091
1133
  }
1092
1134
  catch (e) {
1093
1135
  this.trace('OUT', 'client.connect.error', { error: String(e) });
@@ -1097,122 +1139,89 @@ export class AUNChannel {
1097
1139
  throw e;
1098
1140
  }
1099
1141
  }
1100
- async sendWelcomeMessage() {
1142
+ async preparePostBootstrapWelcome() {
1101
1143
  try {
1102
1144
  const aid = this.config.aid;
1103
1145
  const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
1104
- // Read initialized + owners from per-agent config.json
1105
- // (config.json 是 owner 的真相来源——auto-bind 后会更新这里,但 this.config 是
1106
- // channel 启动时的快照,不会自动同步)
1107
1146
  const agentConfig = loadAgent(aidName);
1108
1147
  if (!agentConfig) {
1109
1148
  logger.warn(`${this.logPrefix()} agent config not found for ${aidName}, skipping welcome message`);
1110
- return;
1111
- }
1112
- if (agentConfig.initialized === true) {
1113
- logger.info(`${this.logPrefix()} Agent already initialized, skipping welcome message`);
1114
- return;
1149
+ return false;
1115
1150
  }
1116
1151
  const owner = getFirstStaticAgentOwner(aidName);
1117
1152
  if (!owner) {
1118
- logger.info(`${this.logPrefix()} No owner configured, skipping welcome message (will retry after auto-bind)`);
1119
- return;
1153
+ logger.info(`${this.logPrefix()} No owner configured, skipping post-bootstrap welcome message`);
1154
+ return false;
1120
1155
  }
1121
- const agentMdLocalPath = agentMdPathFn(aidName);
1122
- const existingAgentMd = fs.existsSync(agentMdLocalPath) ? fs.readFileSync(agentMdLocalPath, 'utf-8') : '';
1123
- const existingFrontmatterMatch = existingAgentMd.match(/^---\n([\s\S]*?)\n---/);
1124
- const existingFrontmatter = existingFrontmatterMatch?.[1] ?? '';
1125
- // Fetch owner's agent.md to derive name and validate type
1126
- const ownerInfo = await this.fetchPeerInfo(owner);
1127
- if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
1128
- logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human). Consider using a human AID as owner.`);
1156
+ const operationId = postBootstrapWelcomeOperationId(aidName);
1157
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
1158
+ if (hasMessageLogOperation(chatDir, operationId) || outbox.findByDedupeKey(aidName, operationId)) {
1159
+ return true;
1129
1160
  }
1130
- // Name: prefer existing agent.md name if user has customized it,
1131
- // otherwise generate "{ownerName}的Evol助手 ({aidLabel})" for disambiguation
1132
1161
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
1133
- const ownerDisplayName = (ownerInfo.name || ownerAidClean.split('.')[0]).slice(0, 12);
1134
- const currentNameMatch = existingFrontmatter.match(/^name:\s*"?([^"\n]+)/m);
1135
- const currentName = currentNameMatch?.[1]?.trim().replace(/"$/, '');
1136
- const aidLabel = aidName.split('.')[0];
1137
- let agentDisplayName;
1138
- if (currentName && currentName !== aidLabel) {
1139
- agentDisplayName = currentName;
1140
- }
1141
- else {
1142
- agentDisplayName = `${ownerDisplayName}的Evol助手 (${aidLabel})`;
1143
- }
1144
- // Preserve user-provided description (from `agent new --description`), fallback to default
1145
- const currentDescMatch = existingFrontmatter.match(/^description:\s*"?([^"\n]*)/m);
1146
- const currentDesc = currentDescMatch?.[1]?.trim().replace(/"$/, '');
1147
- const agentDescription = currentDesc
1148
- ? currentDesc
1149
- : 'EvolCore AI Agent Gateway - 连接 Claude/Codex 到消息通道';
1150
- // Generate new agent.md (no `initialized` frontmatter — that's now in config.json)
1151
- const newAgentMd = `---
1152
- aid: "${aid}"
1153
- name: "${agentDisplayName}"
1154
- type: "codeagent"
1155
- version: "1.0.0"
1156
- description: "${agentDescription}"
1157
- tags:
1158
- - evolcore
1159
- - ai-agent
1160
- - gateway
1161
- ---
1162
-
1163
- # ${agentDisplayName}
1164
-
1165
- EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1166
- `;
1167
- // Write locally and publish to AUN network (auto-sign)
1168
- try {
1169
- const { agentmdPut } = await import('../aun/aid/agentmd.js');
1170
- await agentmdPut(newAgentMd, { aid: aidName, store: this.store });
1171
- logger.info(`${this.logPrefix()} Published agent.md to AUN network`);
1172
- }
1173
- catch (e) {
1174
- logger.warn(`${this.logPrefix()} Failed to publish agent.md: ${e}`);
1175
- }
1176
- // Send welcome message
1177
- const { generateWelcomeMessage } = await import('../utils/welcome.js');
1178
- const welcomeText = generateWelcomeMessage({
1179
- channelType: 'aun',
1180
- agentName: agentDisplayName,
1181
- ownerName: ownerDisplayName,
1182
- includeBindingNote: true,
1183
- });
1184
- // First contact with Owner races against Owner's async cert fetch from
1185
- // gateway PKI; a 3s pause lets the cert propagate. persist_required asks
1186
- // the gateway to durably store the message so Owner can recover it via
1187
- // pull if the initial E2EE push still arrives before the cert resolves.
1188
- await new Promise(resolve => setTimeout(resolve, 3000));
1189
- if (!this.client) {
1190
- logger.warn(`${this.logPrefix()} Client disconnected before welcome message could be sent`);
1191
- return;
1192
- }
1193
- await this.callAndTrace('message.send', {
1194
- to: owner,
1195
- payload: { type: 'text', text: welcomeText },
1196
- encrypt: this.shouldEncrypt(owner),
1197
- persist_required: true,
1198
- });
1199
- logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
1200
- // Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
1162
+ let ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
1201
1163
  try {
1202
- const fresh = loadAgent(aidName);
1203
- if (fresh) {
1204
- fresh.initialized = true;
1205
- saveAgent(fresh);
1206
- logger.info(`${this.logPrefix()} Marked ${aidName} as initialized in config.json`);
1164
+ const ownerInfo = await this.fetchPeerInfo(owner);
1165
+ ownerDisplayName = (ownerInfo.name || ownerDisplayName).slice(0, 12);
1166
+ if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
1167
+ logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human).`);
1207
1168
  }
1208
1169
  }
1209
1170
  catch (e) {
1210
- logger.warn(`${this.logPrefix()} Failed to update initialized flag in config.json: ${e}`);
1171
+ logger.warn(`${this.logPrefix()} Failed to resolve owner display name; using AID label: ${e}`);
1211
1172
  }
1173
+ preparePostBootstrapWelcomeOutbox(aidName, owner, ownerDisplayName);
1174
+ logger.info(`${this.logPrefix()} Post-bootstrap welcome durably prepared for owner: ${owner}`);
1175
+ return true;
1212
1176
  }
1213
1177
  catch (e) {
1214
- logger.warn(`${this.logPrefix()} Failed to send welcome message: ${e}`);
1178
+ logger.warn(`${this.logPrefix()} Failed to prepare post-bootstrap welcome message: ${e}`);
1179
+ return false;
1180
+ }
1181
+ }
1182
+ async sendPostBootstrapWelcome() {
1183
+ if (!await this.preparePostBootstrapWelcome())
1184
+ return false;
1185
+ return this.reconcilePostBootstrapWelcome();
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
+ }
1195
+ /**
1196
+ * Resume delivery of an already prepared completion welcome.
1197
+ *
1198
+ * Unlike sendPostBootstrapWelcome(), this method never creates a new
1199
+ * operation. That distinction keeps repeated `ec agent ready` calls on an
1200
+ * already-active agent from manufacturing another welcome message.
1201
+ */
1202
+ async reconcilePostBootstrapWelcome() {
1203
+ const configuredAid = this.config.aid;
1204
+ const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
1205
+ const operationId = postBootstrapWelcomeOperationId(aid);
1206
+ const entry = outbox.findByDedupeKey(aid, operationId);
1207
+ if (!entry) {
1208
+ const owner = getFirstStaticAgentOwner(aid);
1209
+ if (!owner)
1210
+ return false;
1211
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
1212
+ return hasMessageLogOperation(chatDir, operationId);
1213
+ }
1214
+ const agentConfig = loadAgent(aid);
1215
+ if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active') {
1216
+ logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
1217
+ return true;
1215
1218
  }
1219
+ if (!this.connected || !this.client)
1220
+ return true;
1221
+ const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
1222
+ if (sent)
1223
+ outbox.remove(aid, entry.id);
1224
+ return true;
1216
1225
  }
1217
1226
  // ── Event handlers ──────────────────────────────────────────
1218
1227
  /**
@@ -1439,12 +1448,21 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1439
1448
  if (!data || typeof data !== 'object')
1440
1449
  return;
1441
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;
1442
1457
  // SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1443
1458
  // message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
1444
1459
  const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1445
1460
  const protectedHeaders = verifiedProtectedHeaders(msg, env);
1446
1461
  const fromAid = env.from ?? '';
1447
1462
  const payload = msg.payload ?? '';
1463
+ const mentions = this.parsePayloadMentionsOrReject(payload, 'p2p.inbound', messageId, seq);
1464
+ if (!mentions)
1465
+ return;
1448
1466
  const causationRef = payload && typeof payload === 'object' && typeof payload._evolcore_causation_ref === 'string'
1449
1467
  ? payload._evolcore_causation_ref
1450
1468
  : undefined;
@@ -1458,15 +1476,19 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1458
1476
  const topicName = (typeof payload === 'object' && payload !== null)
1459
1477
  ? this.sanitizeTopicName(payload.topicName)
1460
1478
  : undefined;
1461
- const messageId = msg.message_id ?? '';
1462
1479
  const causation = consumeAunCausation(causationRef || messageId, fromAid, this._aid || this.config.aid)
1463
1480
  ?? (causationRef ? consumeAunCausation(messageId, fromAid, this._aid || this.config.aid) : undefined);
1464
- 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
+ }
1465
1488
  // Observer forward (inbound):在所有过滤之前转发原始明文 payload。
1466
1489
  // forwardInbound 内部排除 self-echo 与 from-owner。
1467
1490
  // 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
1468
1491
  // (即便日后 from-owner 排除规则调整,也不会泄漏)。
1469
- const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
1470
1492
  if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
1471
1493
  this.forwardInbound(msg);
1472
1494
  }
@@ -1482,10 +1504,13 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1482
1504
  const msgEncrypted = !!env.encrypted;
1483
1505
  if (!msgEncrypted)
1484
1506
  this.plaintextRecv++;
1485
- // Detect @mentions
1486
- const mentions = [];
1487
- if (this._aid && text.includes(`@${this._aid}`)) {
1488
- 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 });
1489
1514
  }
1490
1515
  // Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
1491
1516
  const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid);
@@ -1507,8 +1532,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1507
1532
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1508
1533
  return;
1509
1534
  }
1510
- // menu.* 协议:自定义消息快速路径,需要原始 payload JSON 传递给 bridge
1511
- 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) {
1512
1539
  this.acknowledgeImmediately(messageId, seq);
1513
1540
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1514
1541
  this.dispatchMessage({
@@ -1519,6 +1546,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1519
1546
  peerType: peerIdentity.type,
1520
1547
  encrypted: msgEncrypted,
1521
1548
  protectedHeaders,
1549
+ mentions,
1550
+ mentionAids: mentionEntryAids(mentions),
1522
1551
  replyContext: this.buildGroupReplyContext(threadId, fromAid, msgEncrypted, messageId),
1523
1552
  msgType: logDescriptor.msgType,
1524
1553
  payloadType: logDescriptor.payloadType,
@@ -1547,6 +1576,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1547
1576
  peerName: displayName || undefined,
1548
1577
  peerType: peerIdentity.type,
1549
1578
  encrypted: msgEncrypted,
1579
+ mentions,
1580
+ mentionAids: mentionEntryAids(mentions),
1550
1581
  msgType: logDescriptor.msgType,
1551
1582
  payloadType: logDescriptor.payloadType,
1552
1583
  payloadSummary: logDescriptor.payloadSummary,
@@ -1606,28 +1637,38 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1606
1637
  if (!data || typeof data !== 'object')
1607
1638
  return;
1608
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;
1609
1645
  // SDK 0.5.* 移除了顶层 from/sender_aid/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1610
- // message_id / seq / payload / same_* / dispatch_mode 等仍是顶层独立字段,不在 envelope 内。
1646
+ // message_id / seq / payload / same_* / mention_mode 等仍是顶层独立字段,不在 envelope 内。
1611
1647
  const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1612
1648
  const protectedHeaders = verifiedProtectedHeaders(msg, env);
1613
1649
  const groupId = env.group_id ?? '';
1614
1650
  const senderAid = env.from ?? '';
1615
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);
1616
1656
  const logDescriptor = classifyAunPayloadForLog(payload);
1617
1657
  const text = this.extractTextPayload(payload, groupId, senderAid);
1618
1658
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1619
1659
  const topicName = (typeof payload === 'object' && payload !== null)
1620
1660
  ? this.sanitizeTopicName(payload.topicName)
1621
1661
  : undefined;
1622
- const messageId = msg.message_id ?? '';
1623
- 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
+ }
1624
1669
  // Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
1625
1670
  // forwardInbound 内部排除 self-echo 与 from-owner。
1626
1671
  this.forwardInbound(msg);
1627
- // Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
1628
- const payloadMentions = Array.isArray(payload?.mentions)
1629
- ? payload.mentions.filter((m) => typeof m === 'string')
1630
- : [];
1631
1672
  logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
1632
1673
  if (!groupId || !senderAid) {
1633
1674
  this.acknowledgeImmediately(messageId, seq);
@@ -1699,6 +1740,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1699
1740
  chatType: 'group', messageId, seq, groupId,
1700
1741
  encrypted: menuEncrypted,
1701
1742
  protectedHeaders,
1743
+ mentions: payloadMentionEntries,
1744
+ mentionAids: payloadMentionAids,
1745
+ isMentioned: hasMentionAll(payloadMentionEntries)
1746
+ || (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
1702
1747
  replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName),
1703
1748
  msgType: logDescriptor.msgType,
1704
1749
  payloadType: logDescriptor.payloadType,
@@ -1718,17 +1763,17 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1718
1763
  const msgEncrypted = !!env.encrypted;
1719
1764
  if (!msgEncrypted)
1720
1765
  this.plaintextRecv++;
1721
- // dispatch_mode: 本地设置优先,fallback 到服务器参数
1722
- const serverDispatchMode = msg.dispatch_mode ?? payload?.dispatch_mode ?? 'mention';
1723
- const localDispatchMode = this.dispatchModeResolver
1724
- ? await this.dispatchModeResolver(groupId).catch(() => undefined)
1725
- : undefined;
1726
- const dispatchMode = localDispatchMode || serverDispatchMode;
1727
- const mentionedSelf = this._aid
1728
- ? (this.hasExplicitMention(text, this._aid) || payloadMentions.includes(this._aid))
1729
- : false;
1730
- // @all 仅认结构化 mentions(payload.mentions),不扫描正文 — 避免引述性 "@all" 误判
1731
- 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);
1732
1777
  // Echo 机制优先于 mention 过滤:消息第一行包含 echo 时触发
1733
1778
  // 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
1734
1779
  const firstLineGroup = text.split('\n')[0] || '';
@@ -1763,16 +1808,17 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1763
1808
  }
1764
1809
  else {
1765
1810
  // 非 echo 消息:mention 标记(不再过滤,交给响应模式决定)
1766
- // 命令豁免 broadcast:slash 命令在任何 dispatchMode 下都强制走 mention 语义,
1811
+ // slash 命令在任何 mentionMode 下都强制走 mention-only 语义
1767
1812
  // 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
1768
- const enforceMention = dispatchMode === 'mention' || isCommandMsg;
1813
+ const enforceMention = mentionMode === 'mention-only' || isCommandMsg;
1769
1814
  const isMentioned = mentionedSelf || mentionedAll;
1770
1815
  // 过滤逻辑下移到响应层:这里只标记,不过滤
1771
1816
  // 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
1772
1817
  // TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
1773
1818
  if (enforceMention && !isMentioned && !isOwnHandoff) {
1774
1819
  this.acknowledgeImmediately(messageId, seq);
1775
- 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))})`);
1776
1822
  return;
1777
1823
  }
1778
1824
  }
@@ -1790,9 +1836,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1790
1836
  logger.debug(`${this.logPrefix()} Group dropped: empty text and no attachments (group=${groupId} sender=${senderAid} mid=${messageId})`);
1791
1837
  return;
1792
1838
  }
1793
- const mentions = mentionedAll
1794
- ? ['all']
1795
- : mentionedSelf && this._aid ? [this._aid] : [];
1839
+ const mentions = payloadMentionEntries;
1796
1840
  // Process attachments
1797
1841
  const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments);
1798
1842
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
@@ -1803,27 +1847,27 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1803
1847
  const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1804
1848
  const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
1805
1849
  const textMentionAll = this.hasExplicitMention(text, 'all');
1806
- const structMentionSelf = this._aid ? payloadMentions.includes(this._aid) : false;
1807
- const structMentionAll = payloadMentions.includes('all');
1850
+ const structMentionSelf = this._aid ? payloadMentionAids.includes(this._aid) : false;
1851
+ const structMentionAll = mentionedAll;
1808
1852
  const reason = isOwnHandoff
1809
1853
  ? 'handoff.self'
1810
1854
  : mentionedAll
1811
1855
  ? 'mention.all(struct)'
1812
1856
  : mentionedSelf
1813
1857
  ? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
1814
- : `${dispatchMode}.no-mention`;
1815
- 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))}`);
1816
1860
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1817
1861
  if (payloadType === 'action_card_reply')
1818
1862
  return;
1819
1863
  const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1820
- 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)}`);
1821
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 });
1822
1866
  this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode, payloadType === 'event' ? 'notify' : 'send');
1823
1867
  // 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
1824
1868
  // 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
1825
1869
  const renderMentionAids = Array.from(new Set([
1826
- ...payloadMentions,
1870
+ ...payloadMentionAids,
1827
1871
  ...this.extractMentionAidsFromText(text),
1828
1872
  ]));
1829
1873
  this.dispatchMessage({
@@ -1846,7 +1890,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1846
1890
  source: isOwnHandoff ? 'handoff' : undefined,
1847
1891
  encrypted: msgEncrypted,
1848
1892
  replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName),
1849
- dispatchMode: serverDispatchMode,
1893
+ mentionMode: serverMentionMode,
1850
1894
  images: inboundImages.length > 0 ? inboundImages : undefined,
1851
1895
  msgType: logDescriptor.msgType,
1852
1896
  payloadType: logDescriptor.payloadType,
@@ -1855,17 +1899,9 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1855
1899
  });
1856
1900
  }
1857
1901
  dispatchMessage(event) {
1858
- // Dedup
1859
- if (event.messageId) {
1860
- if (this.seenMessages.has(event.messageId))
1861
- return;
1862
- this.seenMessages.set(event.messageId, Date.now());
1863
- setTimeout(() => this.seenMessages.delete(event.messageId), 5 * 60 * 1000);
1864
- // Track seq for acknowledge
1865
- if (event.seq != null) {
1866
- this.messageSeqMap.set(event.messageId, event.seq);
1867
- }
1868
- }
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.
1869
1905
  // Echo 机制:消息第一行包含 "echo"(不区分大小写)且原始内容 ≤10 字符时,直接回声
1870
1906
  // 包含 [EvolCore.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
1871
1907
  const firstLine = event.text.split('\n')[0] || '';
@@ -1897,7 +1933,6 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1897
1933
  }
1898
1934
  if (!this.messageHandler)
1899
1935
  return;
1900
- const mentionObjects = event.mentions?.map(aid => ({ userId: aid }));
1901
1936
  // Use caller-supplied replyContext (group path builds mentionUserIds);
1902
1937
  // fall back to simple threadId-only context for private messages
1903
1938
  let replyContext = event.replyContext;
@@ -1922,12 +1957,12 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1922
1957
  messageId: event.messageId,
1923
1958
  causation: event.causation,
1924
1959
  threadId: event.threadId,
1925
- mentions: mentionObjects,
1960
+ mentions: event.mentions,
1926
1961
  mentionAids: event.mentionAids,
1927
1962
  isMentioned: event.isMentioned, // ← 新增:传递 isMentioned 标记
1928
1963
  replyContext,
1929
1964
  source: event.source,
1930
- dispatchMode: event.dispatchMode,
1965
+ mentionMode: event.mentionMode,
1931
1966
  images: event.images,
1932
1967
  msgType: event.msgType,
1933
1968
  payloadType: event.payloadType,
@@ -2445,31 +2480,12 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2445
2480
  // ── Public API (same interface as before) ───────────────────
2446
2481
  setEventBus(bus) {
2447
2482
  // 重新订阅前先解掉旧的——避免 reload/重连后 listener 累积
2448
- if (this.eventBus && this.ownerBoundHandler && typeof this.eventBus.unsubscribe === 'function') {
2449
- this.eventBus.unsubscribe('channel:owner-bound', this.ownerBoundHandler);
2450
- }
2451
2483
  if (this.eventBus && this.queuedHandler && typeof this.eventBus.unsubscribe === 'function') {
2452
2484
  this.eventBus.unsubscribe('task:queued', this.queuedHandler);
2453
2485
  }
2454
- this.ownerBoundHandler = null;
2455
2486
  this.queuedHandler = null;
2456
2487
  this.eventBus = bus;
2457
2488
  if (bus && typeof bus.subscribe === 'function') {
2458
- const handler = (event) => {
2459
- if (event.channelName !== this.config.channelName)
2460
- return;
2461
- // sendWelcomeMessage 内部读 config.json 中最新的 owners[0],并幂等检查 initialized
2462
- // 自身做 client 健康检查后再发
2463
- if (!this.client) {
2464
- logger.info(`${this.logPrefix()} owner-bound event received but client not connected; skip welcome retry`);
2465
- return;
2466
- }
2467
- this.sendWelcomeMessage().catch(e => {
2468
- logger.warn(`${this.logPrefix()} owner-bound welcome retry failed: ${e}`);
2469
- });
2470
- };
2471
- bus.subscribe('channel:owner-bound', handler);
2472
- this.ownerBoundHandler = handler;
2473
2489
  const queuedHandler = (event) => {
2474
2490
  if (event.channel !== this.config.channelName)
2475
2491
  return;
@@ -2485,8 +2501,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2485
2501
  onMessage(handler) {
2486
2502
  this.messageHandler = handler;
2487
2503
  }
2488
- setDispatchModeResolver(resolver) {
2489
- this.dispatchModeResolver = resolver;
2504
+ setMentionModeResolver(resolver) {
2505
+ this.mentionModeResolver = resolver;
2490
2506
  }
2491
2507
  onRecall(handler) {
2492
2508
  this.recallHandler = handler;
@@ -2575,6 +2591,21 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2575
2591
  finalPayload.chatmode = context.metadata.chatmode;
2576
2592
  return finalPayload;
2577
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
+ }
2578
2609
  rememberInvalidatedInteraction(interactionId, reason) {
2579
2610
  this.invalidatedInteractions.set(interactionId, reason);
2580
2611
  const timer = setTimeout(() => this.invalidatedInteractions.delete(interactionId), 24 * 60 * 60 * 1000);
@@ -2666,6 +2697,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2666
2697
  }
2667
2698
  }
2668
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
+ }
2669
2710
  if (!this.client || !this.connected)
2670
2711
  return { ok: false };
2671
2712
  const isGroup = this.isGroupId(channelId);
@@ -2680,8 +2721,8 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2680
2721
  if (causationRef && causation)
2681
2722
  registerAunCausation(causationRef, this.config.aid, targetAid, causation);
2682
2723
  const payloadWithCausationRef = causationRef
2683
- ? { ...payload, _evolcore_causation_ref: causationRef }
2684
- : payload;
2724
+ ? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
2725
+ : normalizedPayload;
2685
2726
  // Truncate payload if too large (AUN SDK limit: 1MB)
2686
2727
  const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
2687
2728
  const params = { payload: truncatedPayload, encrypt };
@@ -2756,6 +2797,9 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2756
2797
  this.forwardOutbound(result);
2757
2798
  }
2758
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);
2759
2803
  const interactionId = opts.postSend?.type === 'register_interaction_card'
2760
2804
  ? opts.postSend.requestId
2761
2805
  : undefined;
@@ -2763,7 +2807,12 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2763
2807
  logger.info(`${this.logPrefix()} Dropped invalidated interaction before durable enqueue: request=${interactionId}`);
2764
2808
  return {};
2765
2809
  }
2766
- 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));
2767
2816
  const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
2768
2817
  const entry = outbox.enqueue(this.config.aid, {
2769
2818
  channelId,
@@ -2923,17 +2972,48 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2923
2972
  }
2924
2973
  let finalText = text;
2925
2974
  this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
2926
- if (this.isGroupId(channelId) && context?.peerId) {
2927
- if (!finalText.includes(`@${context.peerId}`)) {
2928
- 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
+ }
2929
2996
  }
2930
2997
  }
2998
+ const operationId = typeof context?.metadata?.operationId === 'string'
2999
+ ? context.metadata.operationId
3000
+ : undefined;
3001
+ if (operationId) {
3002
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
3003
+ if (hasMessageLogOperation(chatDir, operationId))
3004
+ return;
3005
+ }
2931
3006
  // Write-ahead: persist to outbox before attempting send
2932
3007
  const entry = outbox.enqueue(this.config.aid, {
2933
3008
  channelId,
3009
+ dedupeKey: operationId,
3010
+ critical: context?.metadata?.criticalDelivery === true,
2934
3011
  type: 'text',
2935
3012
  text: finalText,
2936
3013
  context,
3014
+ ttl: typeof context?.metadata?.outboxTtl === 'number'
3015
+ ? context.metadata.outboxTtl
3016
+ : undefined,
2937
3017
  });
2938
3018
  logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
2939
3019
  // 积压深度告警:outbox 待发条目累积说明发送速度跟不上,回复将出现明显延迟。
@@ -2956,16 +3036,27 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2956
3036
  }
2957
3037
  /** Daemon-side transport for `ec msg send` running inside an agent task. */
2958
3038
  async sendDaemonMsg(args) {
2959
- if (!this.connected || !this.client) {
2960
- return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
2961
- }
2962
3039
  if (!args.to || this.isGroupId(args.to)) {
2963
3040
  return { ok: false, error: 'aun-msg-send only supports private AID targets', code: 'UNSUPPORTED_TARGET' };
2964
3041
  }
2965
3042
  if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
2966
3043
  return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
2967
3044
  }
2968
- 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
+ }
2969
3060
  const info = await this.fetchPeerInfo(args.to);
2970
3061
  const chatmode = info.type === 'human' ? 'interactive' : 'proactive';
2971
3062
  if (!finalPayload.chatmode)
@@ -3070,20 +3161,34 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3070
3161
  }
3071
3162
  /** Daemon-side transport for `ec group send` running inside an agent task. */
3072
3163
  async sendDaemonGroupMsg(args) {
3073
- if (!this.connected || !this.client) {
3074
- return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
3075
- }
3076
3164
  if (!args.groupId || !this.isGroupId(args.groupId)) {
3077
3165
  return { ok: false, error: 'aun-msg-send requires a valid group target', code: 'UNSUPPORTED_TARGET' };
3078
3166
  }
3079
3167
  if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
3080
3168
  return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
3081
3169
  }
3082
- const finalPayload = this.stripUndefinedDeep({ ...args.payload });
3083
- if (args.mentions?.length)
3084
- finalPayload.mentions = args.mentions;
3085
- const encrypt = args.encrypt ?? this.shouldEncrypt(args.groupId);
3086
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);
3087
3192
  const result = await this.callAndTrace('group.send', {
3088
3193
  group_id: args.groupId,
3089
3194
  payload: this.truncatePayloadIfNeeded(finalPayload, 'group-send-ipc'),
@@ -3187,11 +3292,25 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3187
3292
  const channelId = entry.channelId;
3188
3293
  const finalText = entry.text;
3189
3294
  const context = entry.context;
3295
+ const operationId = entry.dedupeKey
3296
+ || (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
3297
+ if (operationId) {
3298
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
3299
+ if (hasMessageLogOperation(chatDir, operationId)) {
3300
+ logger.info(`${this.logPrefix()} Durable operation already logged; skipping duplicate send: ${operationId}`);
3301
+ return true;
3302
+ }
3303
+ if (operationId === postBootstrapWelcomeOperationId(this.config.aid)) {
3304
+ const agentConfig = loadAgent(this.config.aid);
3305
+ if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active')
3306
+ return false;
3307
+ }
3308
+ }
3190
3309
  // 从 context.metadata.source 读取 source,默认为 'daemon'
3191
3310
  const source = context?.metadata?.source ?? 'daemon';
3192
3311
  const payload = { type: 'text', text: finalText };
3193
3312
  if (this.isGroupId(channelId)) {
3194
- const extracted = this.extractMentionAidsFromText(finalText);
3313
+ const extracted = this.extractMentionEntriesFromText(finalText);
3195
3314
  if (extracted.length > 0)
3196
3315
  payload.mentions = extracted;
3197
3316
  }
@@ -3215,11 +3334,25 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3215
3334
  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}`);
3216
3335
  const isGroup = this.isGroupId(channelId);
3217
3336
  const targetAid = channelId;
3337
+ if (operationId && entry.deliveryReceipt) {
3338
+ this.appendOutboundJsonl(channelId, {
3339
+ ...classifyAunPayloadForLog(payload),
3340
+ msgId: entry.deliveryReceipt.messageId,
3341
+ encrypt: entry.deliveryReceipt.encrypt,
3342
+ context,
3343
+ isGroup,
3344
+ source,
3345
+ transport: entry.deliveryReceipt.transport,
3346
+ });
3347
+ return true;
3348
+ }
3218
3349
  const encryptTarget = isGroup ? channelId : targetAid;
3219
3350
  const encrypt = context?.metadata?.encrypted != null
3220
3351
  ? !!(context.metadata.encrypted)
3221
3352
  : this.shouldEncrypt(encryptTarget);
3222
3353
  const params = { payload, encrypt };
3354
+ if (context?.metadata?.persistRequired === true)
3355
+ params.persist_required = true;
3223
3356
  try {
3224
3357
  if (isGroup) {
3225
3358
  params.group_id = channelId;
@@ -3237,6 +3370,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3237
3370
  }
3238
3371
  else {
3239
3372
  this.logAunSendAccepted('group.send', channelId, mid, encrypt, result, finalText);
3373
+ this.checkpointTextDelivery(entry, mid, encrypt, result);
3240
3374
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
3241
3375
  const statsContext = await this.groupStatsContext(channelId);
3242
3376
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send', statsContext);
@@ -3257,6 +3391,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3257
3391
  }
3258
3392
  else {
3259
3393
  this.logAunSendAccepted('message.send', this.peerLabel(targetAid), result.message_id, encrypt, result, finalText);
3394
+ this.checkpointTextDelivery(entry, result.message_id, encrypt, result);
3260
3395
  const causation = normalizeCausation(context?.metadata?.causation);
3261
3396
  if (causation) {
3262
3397
  registerAunCausation(result.message_id, this.config.aid, targetAid, causation);
@@ -3292,6 +3427,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3292
3427
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
3293
3428
  return false;
3294
3429
  }
3430
+ this.checkpointTextDelivery(entry, mid, false, result);
3295
3431
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
3296
3432
  const statsContext = await this.groupStatsContext(channelId);
3297
3433
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, false, context?.metadata?.chatmode, 'send', statsContext);
@@ -3310,6 +3446,7 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3310
3446
  logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
3311
3447
  return false;
3312
3448
  }
3449
+ this.checkpointTextDelivery(entry, mid, false, result);
3313
3450
  const causation = normalizeCausation(context?.metadata?.causation);
3314
3451
  if (causation) {
3315
3452
  registerAunCausation(mid, this.config.aid, targetAid, causation);
@@ -3341,6 +3478,18 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3341
3478
  }
3342
3479
  }
3343
3480
  }
3481
+ checkpointTextDelivery(entry, messageId, encrypt, result) {
3482
+ if (!entry.dedupeKey)
3483
+ return;
3484
+ entry.deliveryReceipt = {
3485
+ messageId,
3486
+ encrypt,
3487
+ transport: this.sendReceiptFromResult(result),
3488
+ };
3489
+ if (!outbox.replace(this.config.aid, entry)) {
3490
+ logger.warn(`${this.logPrefix()} Failed to checkpoint durable operation receipt: ${entry.dedupeKey}`);
3491
+ }
3492
+ }
3344
3493
  async deliverPayloadEntry(entry) {
3345
3494
  const interactionId = entry.postSend?.type === 'register_interaction_card'
3346
3495
  ? entry.postSend.requestId
@@ -3360,6 +3509,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3360
3509
  const context = entry.context;
3361
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}`);
3362
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
+ }
3363
3516
  if (!sent.ok || !sent.messageId)
3364
3517
  return sent;
3365
3518
  const isGroup = this.isGroupId(channelId);
@@ -3371,13 +3524,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3371
3524
  }
3372
3525
  /** 有效会话正文写入 messages.jsonl(message.send/group.send 成功后调用)。 */
3373
3526
  appendOutboundJsonl(channelId, descriptor) {
3527
+ const operationId = typeof descriptor.context?.metadata?.operationId === 'string'
3528
+ ? descriptor.context.metadata.operationId
3529
+ : undefined;
3374
3530
  try {
3375
3531
  const { content, msgType, payloadType, payloadSummary, msgId, encrypt, transport, context, isGroup, source = 'daemon' } = descriptor;
3376
3532
  const sessionsDir = resolvePaths().sessionsDir;
3377
3533
  const selfAID = this.config.aid;
3378
3534
  const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfAID);
3379
3535
  const chatmode = context?.metadata?.chatmode;
3380
- appendMessageLog(chatDir, buildOutboundEntry({
3536
+ const entry = buildOutboundEntry({
3381
3537
  from: selfAID,
3382
3538
  to: channelId,
3383
3539
  sessionId: context?.sessionId,
@@ -3397,9 +3553,16 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3397
3553
  payloadType,
3398
3554
  payloadSummary,
3399
3555
  source,
3400
- }));
3556
+ operationId,
3557
+ });
3558
+ if (operationId)
3559
+ appendMessageLogStrict(chatDir, entry);
3560
+ else
3561
+ appendMessageLog(chatDir, entry);
3401
3562
  }
3402
3563
  catch (e) {
3564
+ if (operationId)
3565
+ throw e;
3403
3566
  logger.debug(`${this.logPrefix()} appendOutboundJsonl failed: ${e}`);
3404
3567
  }
3405
3568
  }
@@ -3412,13 +3575,14 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3412
3575
  * 存储键:group_id/peer_aid + sender_aid + context.type + context.id
3413
3576
  */
3414
3577
  async sendThought(channelId, taskId, payload, context) {
3415
- if (!this.connected || !this.client)
3416
- return;
3417
3578
  if (!taskId)
3418
3579
  return;
3580
+ const validatedPayload = this.normalizeAunPayloadMentions(payload);
3581
+ const finalPayload = this.normalizeAunPayloadMentions(this.stripUndefinedDeep(validatedPayload));
3582
+ if (!this.connected || !this.client)
3583
+ return;
3419
3584
  // 私聊 channelId = 对端 AID(不含 device_id)
3420
3585
  const targetId = channelId;
3421
- const finalPayload = this.stripUndefinedDeep(payload);
3422
3586
  const encrypt = context?.metadata?.encrypted != null
3423
3587
  ? !!(context.metadata.encrypted)
3424
3588
  : this.shouldEncrypt(targetId);
@@ -3488,6 +3652,10 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3488
3652
  }
3489
3653
  /** 发送结构化 AUN payload;transient 协议载荷不写本地 messages.jsonl。 */
3490
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);
3491
3659
  if (!this.connected || !this.client)
3492
3660
  return null;
3493
3661
  const isGroup = this.isGroupId(channelId);
@@ -3496,7 +3664,6 @@ EvolCore AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
3496
3664
  const encrypt = context?.metadata?.encrypted != null
3497
3665
  ? !!(context.metadata.encrypted)
3498
3666
  : this.shouldEncrypt(encryptTarget);
3499
- const finalPayload = this.stripUndefinedDeep({ ...payload });
3500
3667
  if (context?.threadId && !finalPayload.thread_id)
3501
3668
  finalPayload.thread_id = context.threadId;
3502
3669
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
@@ -4233,11 +4400,30 @@ export class AUNChannelPlugin {
4233
4400
  return;
4234
4401
  }
4235
4402
  case 'system.error': {
4236
- 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);
4237
4414
  return;
4238
4415
  }
4239
4416
  case 'result.error': {
4240
- 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);
4241
4427
  return;
4242
4428
  }
4243
4429
  case 'result.file': {
@@ -4435,15 +4621,12 @@ export class AUNChannelPlugin {
4435
4621
  });
4436
4622
  });
4437
4623
  }
4438
- if (typeof channel.setDispatchModeResolver === 'function') {
4439
- channel.setDispatchModeResolver(async (channelId) => {
4440
- // 配置层用 mentionMode(mention-only/disabled),翻译成 AUN 协议
4441
- // dispatch_mode(mention/broadcast);未设值返回 undefined,回退服务端下发值。
4442
- const mentionMode = resolveEffective({
4624
+ if (typeof channel.setMentionModeResolver === 'function') {
4625
+ channel.setMentionModeResolver(async (channelId) => {
4626
+ return resolveEffective({
4443
4627
  self: aid,
4444
4628
  peerKey: formatPeerKey('aun', channelId),
4445
4629
  }, { cache: true }).mentionMode;
4446
- return mentionModeToDispatch(mentionMode);
4447
4630
  });
4448
4631
  }
4449
4632
  },