evolcore 0.0.26 → 0.0.28

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 (177) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/baseagent.js +10 -5
  5. package/dist/agents/claude-runner.js +123 -22
  6. package/dist/agents/codex/app-server-client.js +70 -26
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex-runner.js +363 -421
  9. package/dist/agents/ecagent-runner.js +170 -25
  10. package/dist/agents/gemini-runner.js +6 -0
  11. package/dist/agents/runner-types.js +50 -2
  12. package/dist/aun/aid/agentmd.js +73 -31
  13. package/dist/aun/aid/client.js +1 -1
  14. package/dist/aun/aid/control-aid.js +180 -1
  15. package/dist/aun/aid/identity.js +3 -3
  16. package/dist/aun/group/history.js +303 -0
  17. package/dist/aun/group/identity.js +43 -0
  18. package/dist/aun/group/index.js +5 -0
  19. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  20. package/dist/aun/{menu-client.js → menu/client.js} +13 -7
  21. package/dist/aun/menu/index.js +2 -0
  22. package/dist/aun/menu/transport.js +49 -0
  23. package/dist/aun/msg/index.js +2 -1
  24. package/dist/aun/msg/managed-operation.js +84 -18
  25. package/dist/aun/msg/mention-schema.js +51 -0
  26. package/dist/aun/msg/p2p.js +13 -8
  27. package/dist/aun/msg/upload.js +43 -88
  28. package/dist/aun/outbox.js +29 -7
  29. package/dist/aun/rpc/client.js +1 -0
  30. package/dist/aun/storage/client.js +39 -50
  31. package/dist/channels/aun.js +544 -239
  32. package/dist/channels/daemon.js +10 -6
  33. package/dist/cli/agent-command.js +108 -6
  34. package/dist/cli/agent-create.js +515 -2
  35. package/dist/cli/agent.js +109 -8
  36. package/dist/cli/aun-commands.js +191 -42
  37. package/dist/cli/config.js +8 -5
  38. package/dist/cli/contact.js +2 -2
  39. package/dist/cli/ctl-command.js +2 -2
  40. package/dist/cli/daemon-commands.js +111 -162
  41. package/dist/cli/diag.js +3 -3
  42. package/dist/cli/fs-command.js +3 -3
  43. package/dist/cli/handoff-command.js +6 -6
  44. package/dist/cli/index.js +8 -2
  45. package/dist/cli/init.js +34 -1
  46. package/dist/cli/managed-command-guard.js +2 -1
  47. package/dist/cli/menu-client.js +13 -5
  48. package/dist/cli/model.js +3 -3
  49. package/dist/cli/queue-command.js +2 -2
  50. package/dist/cli/response.js +2 -2
  51. package/dist/cli/restart-monitor.js +81 -19
  52. package/dist/cli/task-command.js +59 -0
  53. package/dist/cli/task-context.js +97 -49
  54. package/dist/cli/trigger-command.js +20 -10
  55. package/dist/config/builtin-roles.js +10 -0
  56. package/dist/config/config-field-policy.js +11 -0
  57. package/dist/config/config-manager.js +32 -4
  58. package/dist/config/config-operation-service.js +20 -5
  59. package/dist/config/contact-book-store.js +1 -1
  60. package/dist/config/contact-book.js +1 -1
  61. package/dist/config/contact-operation-service.js +1 -1
  62. package/dist/config/peer-role-resolver.js +1 -1
  63. package/dist/config/resolved-config-op.js +3 -0
  64. package/dist/config-store.js +1 -1
  65. package/dist/core/agent-application-service.js +5 -0
  66. package/dist/core/audit/event-key.js +17 -2
  67. package/dist/core/audit/log-integrity.js +34 -8
  68. package/dist/core/auth/agent-delegation.js +48 -14
  69. package/dist/core/auth/auth-gateway.js +2 -0
  70. package/dist/core/auth/authorization-audit.js +34 -38
  71. package/dist/core/auth/operation-authorizer.js +67 -5
  72. package/dist/core/auth/operation-catalog.js +110 -0
  73. package/dist/core/auth/remote-access-store.js +115 -9
  74. package/dist/core/bootstrap-messages.js +66 -7
  75. package/dist/core/bootstrap-service.js +110 -19
  76. package/dist/core/command/agent-control.js +88 -7
  77. package/dist/core/command/agent-storage-menu.js +443 -0
  78. package/dist/core/command/cli-intent-parser.js +19 -0
  79. package/dist/core/command/command-handler.js +105 -40
  80. package/dist/core/command/data-root-resolver.js +273 -0
  81. package/dist/core/command/diag-service.js +292 -27
  82. package/dist/core/command/group-menu.js +7 -2
  83. package/dist/core/command/menu-catalog.js +34 -4
  84. package/dist/core/command/menu-handler.js +364 -96
  85. package/dist/core/command/menu-protocol.js +43 -6
  86. package/dist/core/command/role-menu.js +2 -2
  87. package/dist/core/command/slash-handler.js +61 -37
  88. package/dist/core/event-catalog.js +4 -0
  89. package/dist/core/evolagent-registry.js +28 -2
  90. package/dist/core/group/invite-service.js +924 -0
  91. package/dist/core/interaction-router.js +22 -8
  92. package/dist/core/message/im-renderer.js +94 -16
  93. package/dist/core/message/message-bridge.js +140 -13
  94. package/dist/core/message/message-log.js +5 -3
  95. package/dist/core/message/message-queue.js +73 -41
  96. package/dist/core/message/response-engine.js +729 -118
  97. package/dist/core/permission/approval-gateway.js +36 -3
  98. package/dist/core/permission/ec-command-parser.js +76 -36
  99. package/dist/core/permission/protected-paths.js +134 -16
  100. package/dist/core/permission/sandbox-runtime.js +72 -3
  101. package/dist/core/permission/tool-error-code.js +15 -0
  102. package/dist/core/permission/tool-policy.js +8 -8
  103. package/dist/core/relation/peer-identity.js +57 -6
  104. package/dist/core/relation/shared-data.js +106 -0
  105. package/dist/core/role/runtime-policy.js +64 -0
  106. package/dist/core/runtime-lock.js +10 -26
  107. package/dist/core/session/runner-task-history.js +170 -0
  108. package/dist/core/session/session-manager.js +38 -6
  109. package/dist/core/session/session-mapper.js +41 -0
  110. package/dist/core/session/session-turn-coordinator.js +37 -7
  111. package/dist/core/session/session-turns.js +48 -7
  112. package/dist/core/task/task-recovery-service.js +176 -0
  113. package/dist/core/task/task-resume-operation.js +32 -0
  114. package/dist/eck/group-rules-sync.js +5 -3
  115. package/dist/eck/kit-renderer.js +1 -0
  116. package/dist/index.js +322 -207
  117. package/dist/ipc.js +141 -15
  118. package/dist/paths.js +17 -4
  119. package/dist/product.js +3 -0
  120. package/dist/trigger/event-source.js +19 -0
  121. package/dist/trigger/feedback.js +233 -40
  122. package/dist/trigger/parser.js +55 -5
  123. package/dist/trigger/patch.js +9 -0
  124. package/dist/trigger/scheduler.js +30 -4
  125. package/dist/trigger/validation.js +20 -3
  126. package/dist/utils/atomic-write.js +98 -0
  127. package/dist/utils/cross-platform.js +87 -16
  128. package/dist/utils/ecweb-utils.js +1 -0
  129. package/dist/utils/error-utils.js +40 -2
  130. package/dist/utils/logger.js +23 -13
  131. package/dist/utils/npm-ops.js +119 -4
  132. package/dist/utils/package-version-info.js +244 -0
  133. package/dist/utils/tool-summary.js +59 -3
  134. package/dist/utils/welcome.js +174 -1
  135. package/dist/utils/windows-shell-trust.js +37 -0
  136. package/ecagent/dist/harness/agent-harness.js +3 -0
  137. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  138. package/kits/docs/INDEX.md +0 -6
  139. package/kits/docs/channels/aun.md +23 -2
  140. package/kits/docs/channels/feishu.md +1 -1
  141. package/kits/docs/context-assembly.md +29 -17
  142. package/kits/docs/evolcore/INDEX.md +8 -3
  143. package/kits/docs/evolcore/agent-create.md +39 -7
  144. package/kits/docs/evolcore/agent.md +33 -1
  145. package/kits/docs/evolcore/aid.md +9 -1
  146. package/kits/docs/evolcore/config.md +40 -1
  147. package/kits/docs/evolcore/group-collaboration.md +1 -1
  148. package/kits/docs/evolcore/group.md +117 -2
  149. package/kits/docs/evolcore/menu-client.md +16 -0
  150. package/kits/docs/evolcore/msg.md +3 -0
  151. package/kits/docs/evolcore/storage.md +3 -0
  152. package/kits/docs/evolcore/task.md +14 -0
  153. package/kits/docs/evolcore/trigger.md +51 -3
  154. package/kits/docs/venues/group.md +5 -0
  155. package/kits/eck_manifest.json +27 -0
  156. package/kits/rules/01-overview.md +11 -7
  157. package/kits/rules/04-relation.md +2 -1
  158. package/kits/rules/05-venue.md +1 -1
  159. package/kits/rules/06-channel.md +6 -4
  160. package/kits/schemas/agent-config.schema.12.json +25 -1
  161. package/kits/schemas/daemon.schema.6.json +2 -2
  162. package/kits/schemas/defaults.schema.5.json +1 -1
  163. package/kits/schemas/relation-config.schema.1.json +1 -1
  164. package/kits/schemas/relation-config.schema.5.json +1 -1
  165. package/kits/schemas/role-config.schema.1.json +14 -0
  166. package/kits/templates/roles/member.json +5 -0
  167. package/kits/templates/roles/visitor.json +5 -0
  168. package/kits/templates/system-fragments/baseagent.md +1 -1
  169. package/kits/templates/system-fragments/bootstrap.md +26 -9
  170. package/kits/templates/system-fragments/channel.md +8 -44
  171. package/kits/templates/system-fragments/commands.md +10 -2
  172. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  173. package/kits/templates/system-fragments/reply.md +56 -0
  174. package/package.json +7 -3
  175. package/dist/aun/group-identity.js +0 -9
  176. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  177. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,4 +1,5 @@
1
- import { E2EEError } from '@agentunion/fastaun';
1
+ import { E2EEError, validateAIDFormat } from '@agentunion/fastaun';
2
+ import { sanitizeMenuLogValue } from '../core/command/menu-protocol.js';
2
3
  import crypto from 'crypto';
3
4
  import { performance } from 'node:perf_hooks';
4
5
  import fs from 'fs';
@@ -15,6 +16,7 @@ import { appendAidEvent } from '../utils/instance-registry.js';
15
16
  import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, findMessageLogOperationMessageId, hasMessageLogOperation } from '../core/message/message-log.js';
16
17
  import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
17
18
  import { chatDirPath } from '../core/session/session-fs-store.js';
19
+ import { readTopicLable } from '../core/session/session-mapper.js';
18
20
  import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
19
21
  import { appendAidLifecycle } from '../aun/aid/identity.js';
20
22
  import { enableFullGroupPullPagination, enableReliableMessageDelivery, getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
@@ -26,19 +28,20 @@ import { isManagementRole } from '../config/builtin-roles.js';
26
28
  import { normalizeMentionMode } from '../config/mention-mode.js';
27
29
  import { getProcessStartTime } from '../utils/process-introspect.js';
28
30
  import * as outbox from '../aun/outbox.js';
29
- import { guessMime, formatSize } from '../utils/media-cache.js';
31
+ import { formatSize } from '../utils/media-cache.js';
30
32
  import { formatPeerKey, PeerIdentityCache } from '../core/relation/peer-identity.js';
31
33
  import { isHClassPath } from '../core/permission/protected-paths.js';
32
34
  import { consumeAunCausation, registerAunCausation } from '../core/causation/aun-association.js';
33
35
  import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
34
36
  import { recordCausationSpan } from '../core/causation/audit.js';
35
- import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
37
+ import { invalidateAgentDisplayName, refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
38
+ import { buildControlAgentName, ensureControlAgentMd } from '../aun/aid/control-aid.js';
36
39
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
37
- import { ManagedMenuClient } from '../aun/menu-client.js';
40
+ import { ManagedMenuClient, menuSenderDeviceId, menuTransportParams, } from '../aun/menu/index.js';
38
41
  import { toolInputForDisplay } from '../utils/tool-summary.js';
39
- import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, pendingPostBootstrapWelcomeOwners, isPostBootstrapWelcomeOperationId, findPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
42
+ import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, pendingPostBootstrapWelcomeOwners, isPostBootstrapWelcomeOperationId, findPostBootstrapWelcomeOutbox, bootstrapInitialMessageOperationPrefix, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, postBootstrapWelcomeStatus, } from '../core/bootstrap-messages.js';
40
43
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
41
- import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
44
+ import { aunOutboundAttention, normalizeAunMentionEntries, parseAunAttention, withAunAttention, } from '../aun/msg/mention-schema.js';
42
45
  import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
43
46
  import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
44
47
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
@@ -51,6 +54,100 @@ const OBSERVER_GROUP_MEMBERS_TTL_MS = 60_000;
51
54
  const OBSERVER_GROUP_MEMBERS_PAGE_SIZE = 200;
52
55
  const OBSERVER_GROUP_MEMBERS_MAX_PAGES = 100;
53
56
  const AUN_SEND_LOG_TEXT_MAX_LENGTH = 60;
57
+ const AUN_MAX_PAYLOAD_SIZE = 512 * 1024;
58
+ const AUN_MAX_FIELD_SIZE = 256 * 1024;
59
+ const AUN_MIN_FIELD_SIZE = 1024;
60
+ function jsonUtf8Size(value) {
61
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
62
+ }
63
+ function truncatedString(original, originalSize, maxJsonBytes) {
64
+ const fallback = '[输出已截断]';
65
+ if (jsonUtf8Size(fallback) > maxJsonBytes)
66
+ return fallback;
67
+ const candidate = (prefix) => (`${prefix}\n\n[截断:原始输出 ${formatSize(originalSize)},仅显示前 ${formatSize(Buffer.byteLength(prefix, 'utf8'))}]`);
68
+ let low = 0;
69
+ let high = original.length;
70
+ while (low < high) {
71
+ const middle = Math.ceil((low + high) / 2);
72
+ if (jsonUtf8Size(candidate(original.slice(0, middle))) <= maxJsonBytes)
73
+ low = middle;
74
+ else
75
+ high = middle - 1;
76
+ }
77
+ return candidate(original.slice(0, low));
78
+ }
79
+ function collectAunPayloadStrings(value, path, entries) {
80
+ if (!value || typeof value !== 'object')
81
+ return;
82
+ for (const key of Object.keys(value)) {
83
+ const child = value[key];
84
+ const childPath = path ? `${path}.${key}` : key;
85
+ if (typeof child === 'string') {
86
+ entries.push({
87
+ parent: value,
88
+ key,
89
+ path: childPath,
90
+ original: child,
91
+ originalSize: Buffer.byteLength(child, 'utf8'),
92
+ });
93
+ }
94
+ else if (child && typeof child === 'object') {
95
+ collectAunPayloadStrings(child, childPath, entries);
96
+ }
97
+ }
98
+ }
99
+ /** Enforce the AUN payload budget using final JSON/UTF-8 size, including escaping. */
100
+ export function truncateAunPayloadIfNeeded(payload, maxPayloadBytes = AUN_MAX_PAYLOAD_SIZE, maxFieldBytes = AUN_MAX_FIELD_SIZE) {
101
+ const originalSize = jsonUtf8Size(payload);
102
+ if (originalSize <= maxPayloadBytes) {
103
+ return { payload, originalSize, finalSize: originalSize, truncatedFields: [], dropped: false };
104
+ }
105
+ const truncated = JSON.parse(JSON.stringify(payload));
106
+ const entries = [];
107
+ collectAunPayloadStrings(truncated, '', entries);
108
+ const changed = new Map();
109
+ const applyLimit = (entry, targetBytes) => {
110
+ const current = entry.parent[entry.key];
111
+ const currentSize = typeof current === 'string' ? jsonUtf8Size(current) : 0;
112
+ const next = truncatedString(entry.original, entry.originalSize, targetBytes);
113
+ const nextSize = jsonUtf8Size(next);
114
+ if (typeof current !== 'string' || nextSize >= currentSize)
115
+ return false;
116
+ entry.parent[entry.key] = next;
117
+ entry.parent[`${String(entry.key)}_truncated`] = true;
118
+ changed.set(entry.path, { originalSize: entry.originalSize, finalSize: nextSize });
119
+ return true;
120
+ };
121
+ // First cap any individual string by its serialized contribution. This
122
+ // catches escaped JSON/log output whose raw string is deceptively small.
123
+ for (const entry of entries) {
124
+ if (jsonUtf8Size(entry.parent[entry.key]) > maxFieldBytes) {
125
+ applyLimit(entry, Math.floor(maxFieldBytes / 4));
126
+ }
127
+ }
128
+ let finalSize = jsonUtf8Size(truncated);
129
+ let guard = 0;
130
+ while (finalSize > maxPayloadBytes && guard++ < entries.length * 16 + 16) {
131
+ const candidates = entries
132
+ .map(entry => ({ entry, size: jsonUtf8Size(entry.parent[entry.key]) }))
133
+ .filter(item => item.size > jsonUtf8Size('[输出已截断]') + AUN_MIN_FIELD_SIZE)
134
+ .sort((a, b) => b.size - a.size);
135
+ const largest = candidates[0];
136
+ if (!largest)
137
+ break;
138
+ if (!applyLimit(largest.entry, Math.max(AUN_MIN_FIELD_SIZE, Math.floor(largest.size / 2))))
139
+ break;
140
+ finalSize = jsonUtf8Size(truncated);
141
+ }
142
+ const truncatedFields = [...changed.entries()].map(([path, sizes]) => ({ path, ...sizes }));
143
+ return {
144
+ payload: truncated,
145
+ originalSize,
146
+ finalSize,
147
+ truncatedFields,
148
+ dropped: finalSize > maxPayloadBytes,
149
+ };
150
+ }
54
151
  export function formatAunSendLogText(text) {
55
152
  return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
56
153
  }
@@ -248,7 +345,7 @@ export function buildAunFilePayload(params) {
248
345
  payload.name = params.filename;
249
346
  payload.content_type = params.contentType;
250
347
  }
251
- return payload;
348
+ return withAunAttention(payload, params.context?.attention);
252
349
  }
253
350
  const AUN_PERMANENT_ERROR_CODES = new Set([
254
351
  400, 401, 403, 404,
@@ -522,22 +619,8 @@ function verifiedProtectedHeaders(msg, env) {
522
619
  }
523
620
  return undefined;
524
621
  }
525
- function redactMenuTokensForTrace(value, seen = new WeakSet()) {
526
- if (!value || typeof value !== 'object')
527
- return value;
528
- if (seen.has(value))
529
- return '[Circular]';
530
- seen.add(value);
531
- if (Array.isArray(value))
532
- return value.map(item => redactMenuTokensForTrace(item, seen));
533
- const redacted = {};
534
- for (const [key, item] of Object.entries(value)) {
535
- const normalizedKey = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toLowerCase();
536
- redacted[key] = /^(?:menu|ecweb)_token$/.test(normalizedKey)
537
- ? '[REDACTED]'
538
- : redactMenuTokensForTrace(item, seen);
539
- }
540
- return redacted;
622
+ export function redactMenuTokensForTrace(value) {
623
+ return sanitizeMenuLogValue(value);
541
624
  }
542
625
  function payloadTypeForObserver(value) {
543
626
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -581,6 +664,10 @@ export class AUNChannel {
581
664
  traceWriter = null;
582
665
  eventBus = null;
583
666
  queuedHandler = null;
667
+ groupInviteHandler;
668
+ groupInviteConnectedHandler;
669
+ groupInviteDeliveryGate;
670
+ groupInviteDelivered;
584
671
  pendingEchoMessages = new Map();
585
672
  isEchoSending = false;
586
673
  agentDir;
@@ -628,7 +715,9 @@ export class AUNChannel {
628
715
  * 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + daemon.log 失败日志)。
629
716
  * 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
630
717
  */
631
- callClient(method, params, lane = 'normal', onStart) {
718
+ callClient(method, params, lane = 'normal', onStart, onPrepared) {
719
+ params = menuTransportParams(method, params);
720
+ onPrepared?.(params);
632
721
  return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)), lane, onStart);
633
722
  }
634
723
  withOutboundSendGate(method, run, lane = 'normal', onStart) {
@@ -696,7 +785,6 @@ export class AUNChannel {
696
785
  });
697
786
  }
698
787
  async callAndTrace(method, params, opts) {
699
- this.trace('OUT', method, params);
700
788
  // 计时同时覆盖排队和 SDK 往返;callStartedAt 让日志能区分「本地队列堵塞」
701
789
  // 与「网关慢」。两者对 /evolhelp 等命令的回复延迟都有直接影响。
702
790
  const rpcStart = Date.now();
@@ -706,6 +794,11 @@ export class AUNChannel {
706
794
  try {
707
795
  const result = await this.callClient(method, params, opts?.lane ?? 'normal', () => {
708
796
  callStartedAt = Date.now();
797
+ }, preparedParams => {
798
+ // Apply Menu compatibility exactly once, then log the same parameters
799
+ // passed to the SDK (TTL and device targeting may both be removed).
800
+ params = preparedParams;
801
+ this.trace('OUT', method, params);
709
802
  });
710
803
  const durationMs = Date.now() - rpcStart;
711
804
  const rpcDurationMs = Date.now() - callStartedAt;
@@ -756,14 +849,17 @@ export class AUNChannel {
756
849
  state.maxDurationMs = 0;
757
850
  this.rpcWarningStates.set(key, state);
758
851
  const summary = suppressed > 0 ? ` suppressed=${suppressed} max_duration=${maxDurationMs}ms` : '';
852
+ const menuTransport = method === 'message.send' && payloadType.startsWith('menu.')
853
+ ? ` ttl=${params.ttl ?? 'default'} to_device_id=${JSON.stringify(params.to_device_id ?? null)}`
854
+ : '';
759
855
  if (kind === 'failed') {
760
856
  const name = error instanceof Error ? error.name : '';
761
857
  const message = error instanceof Error ? error.message : String(error ?? '');
762
858
  const errorLabel = `${name}${code ? `(${code})` : ''} ${message}`.trim();
763
- logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${errorLabel} queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${summary}`);
859
+ logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${errorLabel} queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${menuTransport}${summary}`);
764
860
  return;
765
861
  }
766
- logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${summary} (发送管线可能饱和或链路抖动)`);
862
+ logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${menuTransport}${summary} (发送管线可能饱和或链路抖动)`);
767
863
  }
768
864
  observerQueueDropCount = 0;
769
865
  observerQueueDropLastWarnAt = 0;
@@ -838,7 +934,7 @@ export class AUNChannel {
838
934
  return;
839
935
  }
840
936
  const context = { delivery };
841
- if (threadId)
937
+ if (typeof threadId === 'string')
842
938
  context.threadId = threadId;
843
939
  if (operatorId && delivery.chatType === 'group')
844
940
  context.peerId = operatorId;
@@ -933,7 +1029,7 @@ export class AUNChannel {
933
1029
  threadId,
934
1030
  replyContext: {
935
1031
  ...(cardInfo.replyContext ?? {}),
936
- ...(threadId ? { threadId } : {}),
1032
+ ...(typeof threadId === 'string' ? { threadId } : {}),
937
1033
  ...(cardDelivery ? { delivery: cardDelivery } : {}),
938
1034
  },
939
1035
  source: 'card-trigger',
@@ -1295,14 +1391,14 @@ export class AUNChannel {
1295
1391
  return sanitized;
1296
1392
  return undefined;
1297
1393
  }
1298
- buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId) {
1394
+ buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, lable) {
1299
1395
  if (!groupId)
1300
1396
  throw new Error('buildGroupReplyContext requires groupId');
1301
1397
  const replyContext = {
1302
1398
  metadata: { encrypted, chatmode },
1303
1399
  delivery: { chatType: 'group', groupId },
1304
1400
  };
1305
- if (threadId)
1401
+ if (typeof threadId === 'string')
1306
1402
  replyContext.threadId = threadId;
1307
1403
  replyContext.peerId = senderAid;
1308
1404
  if (messageId)
@@ -1313,20 +1409,24 @@ export class AUNChannel {
1313
1409
  replyContext.metadata = {};
1314
1410
  replyContext.metadata.topicName = topicName;
1315
1411
  }
1412
+ if (lable && threadId)
1413
+ replyContext.metadata.lable = lable;
1316
1414
  return replyContext;
1317
1415
  }
1318
- buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId) {
1416
+ buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId, lable) {
1319
1417
  const replyContext = {
1320
1418
  metadata: { encrypted, chatmode, refMessageId },
1321
1419
  delivery: { chatType: 'private' },
1322
1420
  peerId: senderAid,
1323
1421
  };
1324
- if (threadId)
1422
+ if (typeof threadId === 'string')
1325
1423
  replyContext.threadId = threadId;
1326
1424
  if (messageId)
1327
1425
  replyContext.replyToMessageId = messageId;
1328
1426
  if (topicName && threadId)
1329
1427
  replyContext.metadata.topicName = topicName;
1428
+ if (lable && threadId)
1429
+ replyContext.metadata.lable = lable;
1330
1430
  return replyContext;
1331
1431
  }
1332
1432
  claimInboundMessage(kind, messageId, seq) {
@@ -1443,6 +1543,7 @@ export class AUNChannel {
1443
1543
  invalidInboundEnvelopeLastLogAt = 0;
1444
1544
  groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
1445
1545
  peerInfoCache = new Map();
1546
+ peerInfoRevision = 0;
1446
1547
  messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
1447
1548
  sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
1448
1549
  peerE2ee = new Map();
@@ -1557,6 +1658,44 @@ export class AUNChannel {
1557
1658
  getAid() {
1558
1659
  return this._aid ?? this.config.aid;
1559
1660
  }
1661
+ setGroupInviteReviewHandlers(handlers) {
1662
+ if (this.config.pureIdentity && handlers)
1663
+ return;
1664
+ this.groupInviteHandler = handlers?.onInvite;
1665
+ this.groupInviteConnectedHandler = handlers?.onConnected;
1666
+ this.groupInviteDeliveryGate = handlers?.canDeliver;
1667
+ this.groupInviteDelivered = handlers?.onDelivered;
1668
+ }
1669
+ async getGroupInviteSummary(groupId) {
1670
+ if (!this.client || !this.connected)
1671
+ return {};
1672
+ try {
1673
+ const result = await this.callAndTrace('group.get_info', { group_id: groupId });
1674
+ const group = result?.group ?? result;
1675
+ return {
1676
+ ...(typeof group?.name === 'string' ? { name: group.name.slice(0, 120) } : {}),
1677
+ ...(typeof group?.description === 'string' ? { description: group.description.slice(0, 240) } : {}),
1678
+ };
1679
+ }
1680
+ catch {
1681
+ return {};
1682
+ }
1683
+ }
1684
+ getGroupInviteDeliveryState(outboxId) {
1685
+ return outbox.deliveryState(this.config.aid, outboxId);
1686
+ }
1687
+ async invalidateGroupInviteCard(interactionId, ownerAid, messageIds) {
1688
+ const known = new Set(this.interactionCardMessageIds.get(interactionId) ?? []);
1689
+ await this.invalidateInteraction(interactionId, 'group_invite_review');
1690
+ // After a daemon restart only the review store knows the delivered cards.
1691
+ for (const id of messageIds) {
1692
+ if (!known.has(id))
1693
+ await this.recallInteractionCard(id, ownerAid, { chatType: 'private' });
1694
+ }
1695
+ }
1696
+ refreshGroupInviteMembership(groupId) {
1697
+ this.invalidateObserverGroupMembers(groupId);
1698
+ }
1560
1699
  /** 是否已连接(client 存在且认证完成)。 */
1561
1700
  isConnected() {
1562
1701
  return this.connected && this.client !== null;
@@ -1667,6 +1806,33 @@ export class AUNChannel {
1667
1806
  // instance must not mutate the new instance's health or message state.
1668
1807
  const isCurrentClient = () => this.client === client;
1669
1808
  // Register event handlers before connecting
1809
+ client.on('agentmd.changed', (data) => {
1810
+ if (!isCurrentClient() || this.intentionalDisconnect || typeof data !== 'string')
1811
+ return;
1812
+ let aid;
1813
+ try {
1814
+ aid = validateAIDFormat(data);
1815
+ }
1816
+ catch {
1817
+ return;
1818
+ }
1819
+ this.peerInfoRevision++;
1820
+ this.peerInfoCache.delete(aid);
1821
+ this.groupNameCache.delete(aid);
1822
+ invalidateAgentDisplayName(aid);
1823
+ PeerIdentityCache.invalidate('aun', aid, path.join(resolvePaths().agentsDir, this.config.aid));
1824
+ this.trace('IN', 'agentmd.changed', { aid });
1825
+ // Peer snapshots refresh lazily on the next lookup. The self name is
1826
+ // held separately by this channel, so refresh it with the existing API.
1827
+ if (aid === (this._aid || this.config.aid)) {
1828
+ void refreshAgentDisplayName(aid).then(name => {
1829
+ if (!name || !isCurrentClient() || this.intentionalDisconnect)
1830
+ return;
1831
+ this._selfName = name;
1832
+ this.aidStatsCollector?.setSelfName(this.config.aid, name);
1833
+ });
1834
+ }
1835
+ });
1670
1836
  client.on('message.received', async (data) => {
1671
1837
  if (!isCurrentClient())
1672
1838
  return;
@@ -1712,6 +1878,9 @@ export class AUNChannel {
1712
1878
  const inviteIds = Array.isArray(record.invite_ids) ? record.invite_ids : [];
1713
1879
  this.trace('IN', 'group.invite_received', data);
1714
1880
  logger.info(`${this.logPrefix()} Group invitation received: group=${groupId} pending=${inviteIds.length || 1}`);
1881
+ void Promise.resolve(this.groupInviteHandler?.(data)).catch(error => {
1882
+ logger.warn(`${this.logPrefix()} Group invitation review dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
1883
+ });
1715
1884
  });
1716
1885
  }
1717
1886
  // 0.5.10/0.5.11 local lifecycle events summarize SDK delivery and Pull
@@ -1751,18 +1920,7 @@ export class AUNChannel {
1751
1920
  client.on('message.recalled', (data) => {
1752
1921
  if (!isCurrentClient())
1753
1922
  return;
1754
- this.trace('IN', 'message.recalled', data);
1755
- if (data && typeof data === 'object') {
1756
- const ids = data.message_ids;
1757
- if (Array.isArray(ids)) {
1758
- for (const id of ids) {
1759
- if (typeof id === 'string') {
1760
- logger.info(`${this.logPrefix()} Message recalled: ${id}`);
1761
- this.recallHandler?.(id, this._aid || this.config.aid);
1762
- }
1763
- }
1764
- }
1765
- }
1923
+ this.handleMessageRecalled(data, client, isCurrentClient);
1766
1924
  });
1767
1925
  client.on('message.undecryptable', (data) => {
1768
1926
  if (!isCurrentClient())
@@ -1848,13 +2006,37 @@ export class AUNChannel {
1848
2006
  this._aid = this.client.aid ?? undefined;
1849
2007
  const deviceId = this.client._device_id ?? '';
1850
2008
  this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
1851
- // pureIdentity(控制 AID):无 agent.md,跳过自身 agent.md 拉取,省一次 404
1852
- this._selfName = this.config.pureIdentity ? undefined : this.loadSelfName(aidName);
2009
+ // 控制 AID 也有标准 host agent.md。发布在后台执行,短暂的 Name
2010
+ // Service/Gateway 故障不能把控制通道判定为连接失败。
2011
+ if (this.config.pureIdentity) {
2012
+ this._selfName = buildControlAgentName(aidName);
2013
+ void ensureControlAgentMd(aidName, {
2014
+ store: this.store ?? undefined,
2015
+ aunPath,
2016
+ cacheRoot: resolveRoot(),
2017
+ })
2018
+ .then(result => {
2019
+ this._selfName = result.name;
2020
+ if (this.aidStatsCollector)
2021
+ this.aidStatsCollector.setSelfName(this.config.aid, result.name);
2022
+ if (result.published)
2023
+ logger.info(`${this.logPrefix()} 控制 AID agent.md 已发布: ${result.name}`);
2024
+ })
2025
+ .catch(error => logger.warn(`${this.logPrefix()} 控制 AID agent.md 补全失败(后台重试): ${error instanceof Error ? error.message : String(error)}`));
2026
+ }
2027
+ else {
2028
+ this._selfName = this.loadSelfName(aidName, aunPath);
2029
+ }
1853
2030
  if (this._selfName && this.aidStatsCollector)
1854
2031
  this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
1855
2032
  this.connected = true;
1856
2033
  this.connectedAt = Date.now();
1857
2034
  this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
2035
+ if (!this.config.pureIdentity) {
2036
+ void Promise.resolve(this.groupInviteConnectedHandler?.()).catch(error => {
2037
+ logger.warn(`${this.logPrefix()} Group invitation recovery failed: ${error instanceof Error ? error.message : String(error)}`);
2038
+ });
2039
+ }
1858
2040
  // AUN seq_tracker is transport-scoped, while this channel's durable
1859
2041
  // dedup log is application-scoped. A diagnostic consumer can advance
1860
2042
  // the former without the application having seen those messages. On a
@@ -1912,7 +2094,7 @@ export class AUNChannel {
1912
2094
  if (hasMessageLogOperation(chatDir, operationId)
1913
2095
  || hasMessageLogOperation(chatDir, postBootstrapWelcomeOperationId(aidName)))
1914
2096
  continue;
1915
- const prepared = outbox.findByDedupeKey(aidName, operationId);
2097
+ const prepared = outbox.findByDedupeKey(aidName, operationId, { includeTerminal: true });
1916
2098
  if (prepared && prepared.channelId === owner && isDeliveryTarget(prepared.delivery) && sameDeliveryTarget(prepared.delivery, delivery))
1917
2099
  continue;
1918
2100
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
@@ -1948,7 +2130,7 @@ export class AUNChannel {
1948
2130
  }
1949
2131
  async sendPostBootstrapWelcome() {
1950
2132
  if (!await this.preparePostBootstrapWelcome())
1951
- return false;
2133
+ throw new Error('Failed to persist post-bootstrap welcome');
1952
2134
  return this.reconcilePostBootstrapWelcome();
1953
2135
  }
1954
2136
  async hasPendingPostBootstrapWelcome() {
@@ -1966,6 +2148,8 @@ export class AUNChannel {
1966
2148
  const pull = client?._pullV2;
1967
2149
  if (typeof repair !== 'function' || typeof pull !== 'function')
1968
2150
  return;
2151
+ if (this.inboundP2PCursorRepairTimer)
2152
+ clearTimeout(this.inboundP2PCursorRepairTimer);
1969
2153
  const ns = `p2p:${this.config.aid}`;
1970
2154
  let appSeq = 0;
1971
2155
  for (const [key, value] of this.inboundSeenMessages) {
@@ -1978,7 +2162,8 @@ export class AUNChannel {
1978
2162
  }
1979
2163
  if (appSeq <= 0)
1980
2164
  return;
1981
- setTimeout(() => {
2165
+ this.inboundP2PCursorRepairTimer = setTimeout(() => {
2166
+ this.inboundP2PCursorRepairTimer = null;
1982
2167
  if (!isCurrentClient() || !this.connected)
1983
2168
  return;
1984
2169
  const transportSeq = Number(tracker.getContiguousSeq?.(ns) ?? 0);
@@ -1996,6 +2181,24 @@ export class AUNChannel {
1996
2181
  }
1997
2182
  }, 250);
1998
2183
  }
2184
+ inboundP2PCursorRepairTimer = null;
2185
+ handleMessageRecalled(data, client, isCurrentClient) {
2186
+ this.trace('IN', 'message.recalled', data);
2187
+ if (data && typeof data === 'object') {
2188
+ const ids = data.message_ids;
2189
+ if (Array.isArray(ids)) {
2190
+ for (const id of ids) {
2191
+ if (typeof id === 'string') {
2192
+ logger.info(`${this.logPrefix()} Message recalled: ${id}`);
2193
+ this.recallHandler?.(id, this._aid || this.config.aid);
2194
+ }
2195
+ }
2196
+ }
2197
+ }
2198
+ // A recall tombstone may arrive at a high transport sequence and advance
2199
+ // the SDK cursor past messages that were never delivered to the app.
2200
+ this.scheduleInboundP2PCursorRepair(client, isCurrentClient);
2201
+ }
1999
2202
  pendingPostBootstrapWelcomeOwners() {
2000
2203
  return pendingPostBootstrapWelcomeOwners(this.config.aid);
2001
2204
  }
@@ -2011,25 +2214,35 @@ export class AUNChannel {
2011
2214
  const configuredAid = this.config.aid;
2012
2215
  const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
2013
2216
  const entries = findPostBootstrapWelcomeOutbox(aid);
2014
- if (entries.length === 0)
2015
- return false;
2016
2217
  const agentConfig = loadAgent(aid);
2218
+ const owners = agentConfig?.owners ?? [];
2219
+ if (entries.length === 0)
2220
+ return postBootstrapWelcomeStatus(aid, owners);
2017
2221
  if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
2018
2222
  logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
2019
- return true;
2223
+ return postBootstrapWelcomeStatus(aid, owners);
2020
2224
  }
2021
2225
  if (!this.connected || !this.client)
2022
- return true;
2226
+ return postBootstrapWelcomeStatus(aid, owners);
2023
2227
  for (const entry of entries) {
2024
2228
  if (!outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
2025
2229
  continue;
2026
- const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
2027
- if (sent.status === 'sent')
2028
- this.removeDeliveredOutboxEntry(entry);
2029
- else if (sent.status === 'permanent')
2030
- this.markPermanentOutboxFailure(entry, sent);
2230
+ try {
2231
+ const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
2232
+ if (sent.status === 'sent')
2233
+ this.removeDeliveredOutboxEntry(entry);
2234
+ else if (sent.status === 'permanent')
2235
+ this.markPermanentOutboxFailure(entry, sent);
2236
+ else
2237
+ outbox.recordRetry(this.config.aid, entry, sent);
2238
+ }
2239
+ catch (error) {
2240
+ // Lifecycle has already committed. Preserve its success and the
2241
+ // durable operation even when local post-send bookkeeping fails.
2242
+ logger.warn(`${this.logPrefix()} Welcome reconciliation failed: entry=${entry.id} error=${error}`);
2243
+ }
2031
2244
  }
2032
- return true;
2245
+ return postBootstrapWelcomeStatus(aid, owners);
2033
2246
  }
2034
2247
  // ── Event handlers ──────────────────────────────────────────
2035
2248
  /**
@@ -2372,7 +2585,7 @@ export class AUNChannel {
2372
2585
  const chatId = fromAid;
2373
2586
  // 解析对端身份(30天缓存)
2374
2587
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
2375
- const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false);
2588
+ const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
2376
2589
  const shortAid = this.getShortAid(fromAid);
2377
2590
  const displayName = peerIdentity.name || shortAid;
2378
2591
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
@@ -2391,13 +2604,15 @@ export class AUNChannel {
2391
2604
  || p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
2392
2605
  const installation = p2pPayloadType.startsWith('menu.')
2393
2606
  ? this.localSenderInstallation?.aunSenderInstallation(msg) : undefined;
2607
+ const peerDeviceId = p2pPayloadType.startsWith('menu.')
2608
+ ? menuSenderDeviceId(msg.sender_device_id) : undefined;
2394
2609
  this.acknowledgeImmediately(messageId, seq);
2395
2610
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
2396
2611
  this.dispatchMessage({
2397
2612
  channelId: chatId, userId: fromAid,
2398
2613
  text: JSON.stringify(payload),
2399
2614
  chatType: 'private', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq,
2400
- peerDeviceId: installation?.deviceId,
2615
+ peerDeviceId,
2401
2616
  peerSlotId: installation?.slotId,
2402
2617
  threadId,
2403
2618
  peerName: displayName || undefined,
@@ -2406,7 +2621,14 @@ export class AUNChannel {
2406
2621
  protectedHeaders,
2407
2622
  mentions,
2408
2623
  mentionAids: mentionEntryAids(mentions),
2409
- replyContext: this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
2624
+ replyContext: {
2625
+ ...this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
2626
+ metadata: {
2627
+ ...this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId).metadata,
2628
+ ...(protectedHeaders?.sdk_version ? { menuClientSdkVersion: protectedHeaders.sdk_version } : {}),
2629
+ },
2630
+ ...(peerDeviceId ? { toDeviceId: peerDeviceId } : {}),
2631
+ },
2410
2632
  msgType: logDescriptor.msgType,
2411
2633
  payloadType: logDescriptor.payloadType,
2412
2634
  payloadSummary: logDescriptor.payloadSummary,
@@ -2456,7 +2678,7 @@ export class AUNChannel {
2456
2678
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
2457
2679
  const isSystemP2P = p2pPayloadType === 'event';
2458
2680
  this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
2459
- const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId);
2681
+ const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId, readTopicLable(payload));
2460
2682
  this.dispatchMessage({
2461
2683
  channelId: chatId,
2462
2684
  userId: fromAid,
@@ -2568,7 +2790,7 @@ export class AUNChannel {
2568
2790
  peerName: displayName,
2569
2791
  peerType: peerInfo?.type || 'unknown',
2570
2792
  seq,
2571
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId),
2793
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId, readTopicLable(payload)),
2572
2794
  createdAt,
2573
2795
  });
2574
2796
  return;
@@ -2638,7 +2860,7 @@ export class AUNChannel {
2638
2860
  this.pendingEchoMessages.set(messageId, {
2639
2861
  text: echoText,
2640
2862
  channelId: groupId,
2641
- context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId),
2863
+ context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId, readTopicLable(payload)),
2642
2864
  receiveTs: Date.now(),
2643
2865
  });
2644
2866
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
@@ -2660,8 +2882,20 @@ export class AUNChannel {
2660
2882
  // 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
2661
2883
  // TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
2662
2884
  if (enforceMention && !isMentioned && !isOwnHandoff) {
2885
+ try {
2886
+ if (text.trim() || this.collectAllAttachments(payload).length) {
2887
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq, 'unmentioned', 'filtered');
2888
+ }
2889
+ }
2890
+ catch (error) {
2891
+ // A failed local append must not suppress a subsequent SDK redelivery.
2892
+ const namespace = this.config.aid + ':' + (this.config.channelName ?? 'aun') + ':aun:group';
2893
+ this.inboundSeenMessages.delete(JSON.stringify([namespace, messageId]));
2894
+ this.rewriteInboundDedupe();
2895
+ logger.warn(`${this.logPrefix()} Deferred group message was not recorded; awaiting redelivery: ${error}`);
2896
+ return;
2897
+ }
2663
2898
  this.acknowledgeImmediately(messageId, seq);
2664
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
2665
2899
  logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
2666
2900
  return;
2667
2901
  }
@@ -2684,7 +2918,7 @@ export class AUNChannel {
2684
2918
  // Process attachments
2685
2919
  const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments, { chatType: 'group', groupId });
2686
2920
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
2687
- const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
2921
+ const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
2688
2922
  const shortAid = this.getShortAid(senderAid);
2689
2923
  const displayName = peerIdentity.name || shortAid;
2690
2924
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
@@ -2736,7 +2970,7 @@ export class AUNChannel {
2736
2970
  isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
2737
2971
  source: isOwnHandoff ? 'handoff' : undefined,
2738
2972
  encrypted: msgEncrypted,
2739
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId),
2973
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId, readTopicLable(payload)),
2740
2974
  mentionMode: serverMentionMode,
2741
2975
  images: inboundImages.length > 0 ? inboundImages : undefined,
2742
2976
  msgType: logDescriptor.msgType,
@@ -2786,7 +3020,7 @@ export class AUNChannel {
2786
3020
  channelId: event.channelId,
2787
3021
  context: {
2788
3022
  ...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
2789
- ...(threadId ? { threadId } : {}),
3023
+ ...(typeof threadId === 'string' ? { threadId } : {}),
2790
3024
  delivery,
2791
3025
  },
2792
3026
  receiveTs: Date.now(),
@@ -2801,7 +3035,7 @@ export class AUNChannel {
2801
3035
  const replyContext = {
2802
3036
  ...(event.replyContext ?? {}),
2803
3037
  delivery,
2804
- ...(threadId ? { threadId } : {}),
3038
+ ...(typeof threadId === 'string' ? { threadId } : {}),
2805
3039
  };
2806
3040
  this.messageHandler({
2807
3041
  channelId: event.channelId || '',
@@ -2841,10 +3075,10 @@ export class AUNChannel {
2841
3075
  logger.error(`${this.logPrefix()} Message handler error:`, err);
2842
3076
  });
2843
3077
  }
2844
- appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq) {
3078
+ appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq, groupAttention, groupDelivery) {
2845
3079
  try {
2846
3080
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
2847
- appendMessageLog(chatDir, buildInboundEntry({
3081
+ const entry = buildInboundEntry({
2848
3082
  from: fromAid,
2849
3083
  to: this.config.aid,
2850
3084
  threadId: threadId || null,
@@ -2859,10 +3093,18 @@ export class AUNChannel {
2859
3093
  timestamp: receivedAt,
2860
3094
  receivedAt,
2861
3095
  gatewaySeq,
2862
- }));
3096
+ groupAttention,
3097
+ groupDelivery,
3098
+ });
3099
+ if (groupDelivery === 'filtered')
3100
+ appendMessageLogStrict(chatDir, entry, true);
3101
+ else
3102
+ appendMessageLog(chatDir, entry);
2863
3103
  }
2864
3104
  catch (e) {
2865
- logger.debug(`${this.logPrefix()} appendInboundPayloadJsonl failed: ${e}`);
3105
+ logger.warn(`${this.logPrefix()} appendInboundPayloadJsonl failed: ${e}`);
3106
+ if (groupDelivery === 'filtered')
3107
+ throw e;
2866
3108
  }
2867
3109
  }
2868
3110
  // ── 观察者模式(Observer Mode) ──────────────────────────────
@@ -3641,7 +3883,9 @@ export class AUNChannel {
3641
3883
  return Buffer.byteLength(logText, 'utf-8');
3642
3884
  }
3643
3885
  applyReplyContextToPayload(payload, context) {
3644
- const finalPayload = this.stripUndefinedDeep({ ...payload });
3886
+ const finalPayload = this.stripUndefinedDeep(withAunAttention({ ...payload }, context?.attention));
3887
+ if (finalPayload.thread_id === '')
3888
+ delete finalPayload.thread_id;
3645
3889
  if (context?.threadId && !finalPayload.thread_id)
3646
3890
  finalPayload.thread_id = context.threadId;
3647
3891
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
@@ -3653,13 +3897,14 @@ export class AUNChannel {
3653
3897
  return finalPayload;
3654
3898
  }
3655
3899
  /**
3656
- * Validate the AUN-specific structured field before it crosses the durable
3900
+ * Validate AUN-specific structured fields before crossing the durable
3657
3901
  * send boundary. Return a copy when mentions are present so callers do not
3658
3902
  * retain an unvalidated array reference.
3659
3903
  */
3660
3904
  normalizeAunPayloadMentions(payload) {
3661
3905
  if (!payload || typeof payload !== 'object' || Array.isArray(payload))
3662
3906
  return payload;
3907
+ payload = withAunAttention(payload);
3663
3908
  if (!Object.prototype.hasOwnProperty.call(payload, 'mentions'))
3664
3909
  return payload;
3665
3910
  return {
@@ -3804,16 +4049,21 @@ export class AUNChannel {
3804
4049
  registerCardPostSend(messageId, action, channelId, delivery, replyContext) {
3805
4050
  if (action.type !== 'register_interaction_card')
3806
4051
  return;
4052
+ // Card replies start a new input; an outbound attention marker must not
4053
+ // become the default for every message produced after clicking the card.
4054
+ const callbackContext = replyContext ? { ...replyContext } : undefined;
4055
+ if (callbackContext)
4056
+ delete callbackContext.attention;
3807
4057
  this.cardMessageIdMap.set(messageId, {
3808
4058
  requestId: action.requestId,
3809
4059
  isCommandCard: action.isCommandCard,
3810
4060
  initiatorAid: action.initiatorAid,
3811
4061
  channelId,
3812
4062
  delivery: delivery ?? action.delivery,
3813
- replyContext,
4063
+ replyContext: callbackContext,
3814
4064
  });
3815
4065
  this.ownedCardMsgIds.add(messageId);
3816
- if (replyContext?.threadId) {
4066
+ if (typeof replyContext?.threadId === 'string') {
3817
4067
  this.ownedCardTopics.set(messageId, { channelId, threadId: replyContext.threadId });
3818
4068
  }
3819
4069
  let messageIds = this.interactionCardMessageIds.get(action.requestId);
@@ -3850,6 +4100,8 @@ export class AUNChannel {
3850
4100
  return;
3851
4101
  if (entry.postSend.type === 'register_interaction_card') {
3852
4102
  this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery, entry.context);
4103
+ if (entry.postSend.requestId.startsWith('gir-'))
4104
+ this.groupInviteDelivered?.(entry.postSend.requestId, messageId);
3853
4105
  }
3854
4106
  }
3855
4107
  async sendAunPayload(channelId, payload, context, label) {
@@ -3881,9 +4133,20 @@ export class AUNChannel {
3881
4133
  const payloadWithCausationRef = causationRef
3882
4134
  ? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
3883
4135
  : normalizedPayload;
4136
+ if (payloadWithCausationRef.thread_id === '')
4137
+ delete payloadWithCausationRef.thread_id;
3884
4138
  // Truncate payload if too large (AUN SDK limit: 1MB)
3885
4139
  const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
3886
4140
  const params = { payload: truncatedPayload, encrypt };
4141
+ const isMenuPayload = typeof truncatedPayload.type === 'string' && truncatedPayload.type.startsWith('menu.');
4142
+ if (!isGroup && truncatedPayload.type === 'menu.response')
4143
+ params.to_device_id = context?.toDeviceId;
4144
+ if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
4145
+ params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
4146
+ }
4147
+ if (isMenuPayload && encrypt && context?.metadata?.protectedHeaders) {
4148
+ params.protected_headers = context.metadata.protectedHeaders;
4149
+ }
3887
4150
  if (isGroup)
3888
4151
  params.group_id = delivery.groupId;
3889
4152
  else
@@ -3904,7 +4167,7 @@ export class AUNChannel {
3904
4167
  return await callOnce(params, false);
3905
4168
  }
3906
4169
  catch (e) {
3907
- if (encrypt && e instanceof E2EEError) {
4170
+ if (encrypt && e instanceof E2EEError && !isMenuPayload) {
3908
4171
  this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
3909
4172
  logger.warn(`${this.logPrefix()} E2EE ${label} send failed to ${channelId}, retrying plaintext: ${e}`);
3910
4173
  const fallbackParams = { ...params, encrypt: false };
@@ -3957,7 +4220,8 @@ export class AUNChannel {
3957
4220
  ...classifyAunPayloadForLog(payload), msgId: messageId, encrypt, context, isGroup, source,
3958
4221
  transport: this.sendReceiptFromResult(result),
3959
4222
  });
3960
- this.forwardOutbound(result);
4223
+ if (typeof payload.type !== 'string' || !payload.type.startsWith('menu.'))
4224
+ this.forwardOutbound(result);
3961
4225
  }
3962
4226
  async sendContentPayload(channelId, payload, opts) {
3963
4227
  const delivery = this.requireDelivery(channelId, opts.context);
@@ -4000,6 +4264,7 @@ export class AUNChannel {
4000
4264
  queue: opts.queue,
4001
4265
  channelId,
4002
4266
  delivery,
4267
+ dedupeKey: opts.dedupeKey,
4003
4268
  type: 'payload',
4004
4269
  contentKind: opts.contentKind,
4005
4270
  payload: finalPayload,
@@ -4071,7 +4336,7 @@ export class AUNChannel {
4071
4336
  base.initiator = context.peerId;
4072
4337
  if (context?.replyToMessageId)
4073
4338
  base.ref_message_id = context.replyToMessageId;
4074
- return base;
4339
+ return withAunAttention(base, context?.attention);
4075
4340
  }
4076
4341
  activityLogText(raw) {
4077
4342
  // 兼容两种入参:原始 ThoughtItem(顶层字段),或已构建的 activity payload(字段收在 .item 里)
@@ -4101,65 +4366,23 @@ export class AUNChannel {
4101
4366
  * the encrypted AUN envelope and RPC framing.
4102
4367
  */
4103
4368
  truncatePayloadIfNeeded(payload, label) {
4104
- const MAX_PAYLOAD_SIZE = 512 * 1024; // 512KB conservative limit
4105
- const MAX_FIELD_SIZE = 256 * 1024; // 256KB per field
4106
- // First check total size
4107
- const payloadJson = JSON.stringify(payload);
4108
- const totalSize = Buffer.byteLength(payloadJson, 'utf-8');
4109
- if (totalSize <= MAX_PAYLOAD_SIZE) {
4369
+ const result = truncateAunPayloadIfNeeded(payload);
4370
+ if (result.originalSize <= AUN_MAX_PAYLOAD_SIZE) {
4110
4371
  return payload; // No truncation needed
4111
4372
  }
4112
- logger.warn(`${this.logPrefix()} Payload too large (${formatSize(totalSize)}), truncating... label=${label}`);
4113
- // Deep clone to avoid mutating original
4114
- const truncated = JSON.parse(payloadJson);
4115
- // Truncate large string fields recursively
4116
- const truncateObject = (obj, path) => {
4117
- if (!obj || typeof obj !== 'object')
4118
- return false;
4119
- let changed = false;
4120
- for (const key of Object.keys(obj)) {
4121
- const value = obj[key];
4122
- const fieldPath = path ? `${path}.${key}` : key;
4123
- if (typeof value === 'string') {
4124
- const fieldSize = Buffer.byteLength(value, 'utf-8');
4125
- if (fieldSize > MAX_FIELD_SIZE) {
4126
- const truncatedValue = value.substring(0, MAX_FIELD_SIZE / 4); // Rough estimate, UTF-8 safe
4127
- const actualTruncatedSize = Buffer.byteLength(truncatedValue, 'utf-8');
4128
- obj[key] = truncatedValue + `\n\n[截断:原始输出 ${formatSize(fieldSize)},仅显示前 ${formatSize(actualTruncatedSize)}]`;
4129
- obj[`${key}_truncated`] = true;
4130
- logger.info(`${this.logPrefix()} Truncated field ${fieldPath}: ${formatSize(fieldSize)} → ${formatSize(actualTruncatedSize)}`);
4131
- changed = true;
4132
- }
4133
- }
4134
- else if (Array.isArray(value)) {
4135
- for (let i = 0; i < value.length; i++) {
4136
- if (truncateObject(value[i], `${fieldPath}[${i}]`)) {
4137
- changed = true;
4138
- }
4139
- }
4140
- }
4141
- else if (typeof value === 'object' && value !== null) {
4142
- if (truncateObject(value, fieldPath)) {
4143
- changed = true;
4144
- }
4145
- }
4146
- }
4147
- return changed;
4148
- };
4149
- truncateObject(truncated, '');
4150
- // Check size again after truncation
4151
- const newJson = JSON.stringify(truncated);
4152
- const newSize = Buffer.byteLength(newJson, 'utf-8');
4153
- if (newSize > MAX_PAYLOAD_SIZE) {
4154
- // Still too large - this shouldn't happen with 256KB field limit, but log it
4155
- logger.error(`${this.logPrefix()} Payload still too large after truncation: ${formatSize(newSize)}, dropping payload`);
4373
+ logger.warn(`${this.logPrefix()} Payload too large (${formatSize(result.originalSize)}), truncating... label=${label}`);
4374
+ for (const field of result.truncatedFields) {
4375
+ logger.info(`${this.logPrefix()} Truncated field ${field.path}: ${formatSize(field.originalSize)} → ${formatSize(field.finalSize)}`);
4376
+ }
4377
+ if (result.dropped) {
4378
+ logger.error(`${this.logPrefix()} Payload still too large after truncation: ${formatSize(result.finalSize)}, dropping payload`);
4156
4379
  return {
4157
4380
  type: 'text',
4158
4381
  text: '消息内容过大,无法完整发送,请缩小输出后重试',
4159
4382
  };
4160
4383
  }
4161
- logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(totalSize)} → ${formatSize(newSize)}`);
4162
- return truncated;
4384
+ logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(result.originalSize)} → ${formatSize(result.finalSize)}`);
4385
+ return result.payload;
4163
4386
  }
4164
4387
  buildActivityPayload(envelope, context, item) {
4165
4388
  const rawItem = item && typeof item === 'object' && !Array.isArray(item)
@@ -4171,11 +4394,11 @@ export class AUNChannel {
4171
4394
  const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
4172
4395
  ? { ...rawItem, arguments: displayArguments }
4173
4396
  : rawItem;
4174
- return {
4397
+ return withAunAttention({
4175
4398
  ...this.buildTaskPayloadBase(envelope, context),
4176
4399
  type: 'activity',
4177
4400
  item: this.stripUndefinedDeep(activityItem),
4178
- };
4401
+ });
4179
4402
  }
4180
4403
  async sendReliableStructured(channelId, payload, context, logText) {
4181
4404
  const result = await this.sendContentPayload(channelId, payload, {
@@ -4197,6 +4420,7 @@ export class AUNChannel {
4197
4420
  return result;
4198
4421
  }
4199
4422
  async sendMessage(channelId, text, context) {
4423
+ parseAunAttention(context?.attention);
4200
4424
  if (!text?.trim()) {
4201
4425
  logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
4202
4426
  return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
@@ -4313,6 +4537,7 @@ export class AUNChannel {
4313
4537
  if (entry.deliveryReceipt?.messageId) {
4314
4538
  return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
4315
4539
  }
4540
+ outbox.recordRetry(this.config.aid, entry, result);
4316
4541
  return {
4317
4542
  status: 'queued',
4318
4543
  outboxId: entry.id,
@@ -4332,6 +4557,8 @@ export class AUNChannel {
4332
4557
  try {
4333
4558
  const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
4334
4559
  finalPayload = this.stripUndefinedDeep(validatedPayload);
4560
+ if (finalPayload.thread_id === '')
4561
+ delete finalPayload.thread_id;
4335
4562
  }
4336
4563
  catch (e) {
4337
4564
  return {
@@ -4433,11 +4660,7 @@ export class AUNChannel {
4433
4660
  code: 'H_CLASS_PROTECTED',
4434
4661
  });
4435
4662
  }
4436
- const connection = {
4437
- call: (method, params) => this.callAndTrace(method, params),
4438
- close: async () => { },
4439
- };
4440
- const built = await uploadFileAndBuildPayload(connection, this.config.aid, file.filePath, {
4663
+ const built = await uploadFileAndBuildPayload(this.client, this.config.aid, file.filePath, {
4441
4664
  as: file.as,
4442
4665
  contentType: file.contentType,
4443
4666
  text: file.text,
@@ -4458,6 +4681,8 @@ export class AUNChannel {
4458
4681
  // removed from a malformed mentions array.
4459
4682
  const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
4460
4683
  const finalPayload = this.stripUndefinedDeep(validatedPayload);
4684
+ if (finalPayload.thread_id === '')
4685
+ delete finalPayload.thread_id;
4461
4686
  const payloadMentions = Object.prototype.hasOwnProperty.call(finalPayload, 'mentions')
4462
4687
  ? normalizeAunMentionEntries(finalPayload.mentions)
4463
4688
  : undefined;
@@ -4612,10 +4837,21 @@ export class AUNChannel {
4612
4837
  return { status: 'retry', error: 'post-bootstrap welcome is waiting for active lifecycle', code: 'BOOTSTRAP_NOT_ACTIVE' };
4613
4838
  }
4614
4839
  }
4840
+ if (operationId.startsWith(bootstrapInitialMessageOperationPrefix(this.config.aid)) && !entry.deliveryReceipt) {
4841
+ const config = loadAgent(this.config.aid);
4842
+ if (config && resolveAgentLifecycle(config) === 'active') {
4843
+ return { status: 'permanent', code: 'BOOTSTRAP_SUPERSEDED', error: 'Bootstrap is complete; initial prompt cancelled' };
4844
+ }
4845
+ }
4615
4846
  }
4616
4847
  // 从 context.metadata.source 读取 source,默认为 'daemon'
4617
4848
  const source = context?.metadata?.source ?? 'daemon';
4618
- const payload = { type: 'text', text: finalText };
4849
+ // Put the label in the visible body without requiring client title support.
4850
+ // Derive it from the durable context without mutating the stored text.
4851
+ const title = delivery.chatType === 'private' ? context?.title?.trim() : undefined;
4852
+ const wireText = title && finalText.split(/\r?\n/, 1)[0].trim() !== title
4853
+ ? `${title}\n\n${finalText}` : finalText;
4854
+ const payload = withAunAttention({ type: 'text', text: wireText }, context?.attention);
4619
4855
  if (delivery.chatType === 'group') {
4620
4856
  const extracted = this.extractMentionEntriesFromText(finalText);
4621
4857
  if (extracted.length > 0)
@@ -4629,6 +4865,8 @@ export class AUNChannel {
4629
4865
  payload.task_id = context.metadata.taskId;
4630
4866
  if (context?.metadata?.chatmode)
4631
4867
  payload.chatmode = context.metadata.chatmode;
4868
+ if (context?.metadata?.forkAnchor)
4869
+ payload.fork_anchor = context.metadata.forkAnchor;
4632
4870
  const causation = normalizeCausation(context?.metadata?.causation);
4633
4871
  const causationRef = delivery.chatType !== 'group' && causation
4634
4872
  ? `caus_${crypto.randomUUID().replace(/-/g, '')}`
@@ -4638,7 +4876,7 @@ export class AUNChannel {
4638
4876
  registerAunCausation(causationRef, this.config.aid, channelId, causation);
4639
4877
  }
4640
4878
  // 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
4641
- 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}`);
4879
+ 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=${wireText.length}`);
4642
4880
  const isGroup = delivery.chatType === 'group';
4643
4881
  const targetAid = channelId;
4644
4882
  if (entry.deliveryReceipt) {
@@ -4819,6 +5057,7 @@ export class AUNChannel {
4819
5057
  queue: entry.queue,
4820
5058
  };
4821
5059
  const receipt = {
5060
+ acceptedAt: Date.now(),
4822
5061
  messageId,
4823
5062
  encrypt,
4824
5063
  transport: this.sendReceiptFromResult(result),
@@ -4841,6 +5080,15 @@ export class AUNChannel {
4841
5080
  logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
4842
5081
  return { ok: true, status: 'sent' };
4843
5082
  }
5083
+ if (interactionId?.startsWith('gir-')) {
5084
+ const result = this.groupInviteDeliveryGate?.(interactionId, entry.channelId) ?? 'stale';
5085
+ if (result === 'defer')
5086
+ return { ok: false, status: 'retry', error: 'group invitation callback is not ready', code: 'INVITE_REVIEW_NOT_READY' };
5087
+ if (result !== 'allow') {
5088
+ logger.info(`${this.logPrefix()} Discarded stale group invitation card: request=${interactionId} entry=${entry.id}`);
5089
+ return { ok: true, status: 'sent' };
5090
+ }
5091
+ }
4844
5092
  const interactionExpiresAt = entry.postSend?.type === 'register_interaction_card'
4845
5093
  ? entry.postSend.expiresAt
4846
5094
  : undefined;
@@ -4988,6 +5236,8 @@ export class AUNChannel {
4988
5236
  const payloadWithChatmode = context?.metadata?.chatmode && !finalPayload.chatmode
4989
5237
  ? { ...finalPayload, chatmode: context.metadata.chatmode }
4990
5238
  : finalPayload;
5239
+ if (payloadWithChatmode.thread_id === '')
5240
+ delete payloadWithChatmode.thread_id;
4991
5241
  if (context?.threadId && !payloadWithChatmode.thread_id)
4992
5242
  payloadWithChatmode.thread_id = context.threadId;
4993
5243
  const params = {
@@ -5070,7 +5320,7 @@ export class AUNChannel {
5070
5320
  // Validate before checking connection state so malformed producer payloads
5071
5321
  // are reported deterministically instead of being hidden by a disconnect.
5072
5322
  const validatedPayload = this.normalizeAunPayloadMentions(payload);
5073
- const finalPayload = this.stripUndefinedDeep(validatedPayload);
5323
+ const finalPayload = this.stripUndefinedDeep(withAunAttention(validatedPayload, context?.attention));
5074
5324
  if (!this.connected || !this.client) {
5075
5325
  return {
5076
5326
  ok: false,
@@ -5085,12 +5335,19 @@ export class AUNChannel {
5085
5335
  const encrypt = context?.metadata?.encrypted != null
5086
5336
  ? !!(context.metadata.encrypted)
5087
5337
  : this.shouldEncrypt(encryptTarget);
5338
+ if (finalPayload.thread_id === '')
5339
+ delete finalPayload.thread_id;
5088
5340
  if (context?.threadId && !finalPayload.thread_id)
5089
5341
  finalPayload.thread_id = context.threadId;
5090
5342
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
5091
5343
  finalPayload.ref_message_id = context.replyToMessageId;
5092
5344
  const isMenuPayload = typeof finalPayload.type === 'string' && finalPayload.type.startsWith('menu.');
5093
5345
  const params = { payload: finalPayload, encrypt };
5346
+ if (!isGroup && finalPayload.type === 'menu.response')
5347
+ params.to_device_id = context?.toDeviceId;
5348
+ if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
5349
+ params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
5350
+ }
5094
5351
  const rawHeaders = context?.metadata?.protectedHeaders;
5095
5352
  if (encrypt && rawHeaders && typeof rawHeaders === 'object' && !Array.isArray(rawHeaders)) {
5096
5353
  const protectedHeaders = {};
@@ -5156,21 +5413,23 @@ export class AUNChannel {
5156
5413
  });
5157
5414
  }
5158
5415
  async sendFile(channelId, filePath, context) {
5416
+ parseAunAttention(context?.attention);
5159
5417
  const delivery = this.requireDelivery(channelId, context);
5160
5418
  context = this.withDelivery(context, delivery);
5419
+ const isFetch = typeof context?.metadata?.correlationId === 'string' && !!context.metadata.correlationId;
5161
5420
  const absPath = path.resolve(filePath);
5162
5421
  if (!fs.existsSync(absPath)) {
5163
5422
  logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
5164
- return;
5423
+ return { status: 'failed', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
5165
5424
  }
5166
5425
  const stat = fs.statSync(absPath);
5167
5426
  if (stat.size === 0) {
5168
5427
  logger.warn(`${this.logPrefix()} sendFile: file is empty`);
5169
- return;
5428
+ return { status: 'failed', error: 'file is empty', code: 'EMPTY_FILE' };
5170
5429
  }
5171
5430
  if (stat.size > 10 * 1024 * 1024) {
5172
5431
  logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
5173
- return;
5432
+ return { status: 'failed', error: `file too large (max ${formatSize(MAX_AUN_ATTACHMENT_SIZE)})`, code: 'FILE_TOO_LARGE' };
5174
5433
  }
5175
5434
  // Write-ahead: persist to outbox
5176
5435
  const entry = outbox.enqueue(this.config.aid, {
@@ -5188,17 +5447,47 @@ export class AUNChannel {
5188
5447
  if (!this.reconnectTimer && !this.client) {
5189
5448
  this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendFile failed: ${e}`));
5190
5449
  }
5191
- return;
5450
+ const error = 'AUN channel is not connected';
5451
+ if (isFetch) {
5452
+ this.markPermanentOutboxFailure(entry, { error, code: 'AUN_NOT_CONNECTED' });
5453
+ return { status: 'failed', outboxId: entry.id, error, code: 'AUN_NOT_CONNECTED' };
5454
+ }
5455
+ return { status: 'queued', outboxId: entry.id, error, code: 'AUN_NOT_CONNECTED' };
5192
5456
  }
5193
5457
  const result = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
5194
5458
  if (result.status === 'sent') {
5459
+ const messageId = result.messageId ?? entry.deliveryReceipt?.messageId;
5460
+ if (!messageId) {
5461
+ const error = 'file send returned no message_id';
5462
+ this.markPermanentOutboxFailure(entry, { error, code: 'MISSING_MESSAGE_ID' });
5463
+ return { status: 'failed', outboxId: entry.id, error, code: 'MISSING_MESSAGE_ID' };
5464
+ }
5195
5465
  this.removeDeliveredOutboxEntry(entry);
5466
+ return { status: 'sent', messageId };
5196
5467
  }
5197
- else if (result.status === 'permanent') {
5468
+ // Remote acceptance is authoritative even if local bookkeeping needs retry.
5469
+ if (entry.deliveryReceipt?.messageId)
5470
+ return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
5471
+ if (result.status === 'permanent' || isFetch) {
5472
+ // A correlated fetch must resolve promptly. Retrying it after reporting
5473
+ // failure could deliver an unsolicited file for an already closed request.
5198
5474
  this.markPermanentOutboxFailure(entry, result);
5475
+ return {
5476
+ status: 'failed',
5477
+ outboxId: entry.id,
5478
+ error: result.error ?? 'permanent file send failure',
5479
+ ...(result.code !== undefined ? { code: result.code } : {}),
5480
+ };
5199
5481
  }
5482
+ return {
5483
+ status: 'queued',
5484
+ outboxId: entry.id,
5485
+ ...(result.error !== undefined ? { error: result.error } : {}),
5486
+ ...(result.code !== undefined ? { code: result.code } : {}),
5487
+ };
5200
5488
  }
5201
5489
  async sendImage(channelId, data, mimeType, alt, context) {
5490
+ parseAunAttention(context?.attention);
5202
5491
  const delivery = this.requireDelivery(channelId, context);
5203
5492
  context = this.withDelivery(context, delivery);
5204
5493
  if (!Buffer.isBuffer(data)) {
@@ -5268,11 +5557,7 @@ export class AUNChannel {
5268
5557
  }
5269
5558
  const digest = crypto.createHash('sha256').update(data).digest('hex').slice(0, 12);
5270
5559
  const filename = `image-${digest}.${extension}`;
5271
- const connection = {
5272
- call: (method, params) => this.callAndTrace(method, params),
5273
- close: async () => { },
5274
- };
5275
- const built = await uploadBufferAndBuildPayload(connection, this.config.aid, data, filename, { as: 'image', contentType });
5560
+ const built = await uploadBufferAndBuildPayload(this.client, this.config.aid, data, filename, { as: 'image', contentType });
5276
5561
  const imagePayload = { ...built.payload };
5277
5562
  setIfDefined(imagePayload, 'alt', alt);
5278
5563
  return imagePayload;
@@ -5359,57 +5644,14 @@ export class AUNChannel {
5359
5644
  logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
5360
5645
  return { status: 'permanent', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
5361
5646
  }
5362
- const filename = path.basename(absPath);
5363
- const fileData = fs.readFileSync(absPath);
5364
- const stat = fs.statSync(absPath);
5365
- const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
5366
- const contentType = guessMime(filename);
5367
- const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
5368
5647
  try {
5369
- if (stat.size <= 64 * 1024) {
5370
- await this.callAndTrace('storage.put_object', {
5371
- object_key: objectKey,
5372
- content: fileData.toString('base64'),
5373
- content_type: contentType,
5374
- is_private: false,
5375
- overwrite: true,
5376
- });
5377
- }
5378
- else {
5379
- const session = await this.callAndTrace('storage.create_upload_session', {
5380
- object_key: objectKey,
5381
- size_bytes: stat.size,
5382
- content_type: contentType,
5383
- });
5384
- const uploadUrl = session.upload_url;
5385
- if (!uploadUrl)
5386
- throw new Error('No upload_url in session response');
5387
- this.trace('OUT', 'http.put.upload_url', { object_key: objectKey, size: stat.size });
5388
- const uploadResp = await fetch(uploadUrl, { method: 'PUT', body: fileData });
5389
- this.trace('OUT', uploadResp.ok ? 'http.put.upload_url.ok' : 'http.put.upload_url.error', { status: uploadResp.status });
5390
- if (!uploadResp.ok)
5391
- throw new Error(`HTTP upload failed: ${uploadResp.status}`);
5392
- await this.callAndTrace('storage.complete_upload', {
5393
- object_key: objectKey,
5394
- sha256,
5395
- content_type: contentType,
5396
- is_private: false,
5397
- size_bytes: stat.size,
5398
- });
5399
- }
5400
- const attachment = {
5401
- owner_aid: this._aid || '',
5402
- object_key: objectKey,
5403
- filename,
5404
- size_bytes: stat.size,
5405
- sha256,
5406
- content_type: contentType,
5407
- };
5648
+ const { attachment } = await uploadFileAndBuildPayload(this.client, this.config.aid, absPath, { as: 'file' });
5649
+ const { filename, size_bytes: size, content_type: contentType } = attachment;
5408
5650
  const filePayload = buildAunFilePayload({
5409
5651
  filename,
5410
- size: stat.size,
5652
+ size,
5411
5653
  contentType,
5412
- attachment,
5654
+ attachment: { ...attachment },
5413
5655
  context,
5414
5656
  });
5415
5657
  const fileTargetAid = channelId;
@@ -5495,7 +5737,7 @@ export class AUNChannel {
5495
5737
  logger.error(`${this.logPrefix()} AUN accepted the durable file but its receipt could not be persisted: id=${entry.id} mid=${sentMid} error=${detail}`);
5496
5738
  return { status: 'permanent', error: detail, code: 'OUTBOX_RECEIPT_CHECKPOINT_FAILED' };
5497
5739
  }
5498
- logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
5740
+ logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(size)}) → ${channelId}`);
5499
5741
  const fileText = filePayload.text;
5500
5742
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: sentMid, kind: 'file', len: fileText.length, ...(isGroup && { groupId: channelId }) });
5501
5743
  const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
@@ -5614,11 +5856,12 @@ export class AUNChannel {
5614
5856
  // corrected Owner route was delivered.
5615
5857
  if (entry.channelId !== owner || entry.delivery?.chatType !== 'private') {
5616
5858
  delete repaired.deliveryReceipt;
5859
+ delete repaired.deliveryResult;
5860
+ delete repaired.terminal;
5861
+ delete repaired.lastError;
5862
+ delete repaired.lastErrorCode;
5863
+ delete repaired.attempts;
5617
5864
  }
5618
- delete repaired.terminal;
5619
- delete repaired.lastError;
5620
- delete repaired.lastErrorCode;
5621
- delete repaired.attempts;
5622
5865
  if (outbox.replaceIfRouteMatches(aid, entry, repaired) !== 'replaced')
5623
5866
  continue;
5624
5867
  logger.warn(`${this.logPrefix()} Repaired bootstrap outbox route to private Owner delivery: entry=${entry.id} owner=${owner}`);
@@ -5658,17 +5901,21 @@ export class AUNChannel {
5658
5901
  notice: 'notice',
5659
5902
  requires_action: 'requires_action',
5660
5903
  };
5661
- const statusPayload = {
5904
+ const statusPayload = withAunAttention({
5662
5905
  type: 'task.status',
5663
5906
  status: stateMap[status] ?? status,
5664
5907
  task_id: taskId,
5665
5908
  session_id: sessionId,
5666
5909
  severity,
5667
- terminal: ['done', 'interrupted', 'error', 'timeout'].includes(status),
5668
- };
5910
+ });
5669
5911
  const metadataInput = extraMeta ? { ...extraMeta } : undefined;
5670
5912
  if (metadataInput)
5671
5913
  delete metadataInput.severity;
5914
+ const statusKind = typeof extraMeta?.kind === 'string' ? extraMeta.kind : undefined;
5915
+ if (statusKind)
5916
+ statusPayload.kind = statusKind;
5917
+ if (metadataInput)
5918
+ delete metadataInput.kind;
5672
5919
  const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
5673
5920
  if (metadata && Object.keys(metadata).length > 0)
5674
5921
  statusPayload.metadata = metadata;
@@ -5721,10 +5968,6 @@ export class AUNChannel {
5721
5968
  // returns until the channel reconnects.
5722
5969
  const delivery = this.requireDelivery(channelId, context);
5723
5970
  context = this.withDelivery(context, delivery);
5724
- if (!this.connected || !this.client) {
5725
- logger.debug(`${this.logPrefix()} Custom payload skipped while disconnected: channel=${channelId}`);
5726
- return;
5727
- }
5728
5971
  // SDK 0.3.0 E2EE requires payload to be an object
5729
5972
  let payloadObj;
5730
5973
  try {
@@ -5735,6 +5978,16 @@ export class AUNChannel {
5735
5978
  catch {
5736
5979
  payloadObj = { text: payload };
5737
5980
  }
5981
+ // File fetch was already acknowledged. Its terminal error must survive a
5982
+ // disconnect so the pending App request is not silently abandoned.
5983
+ if (payloadObj.type === 'menu.response' && payloadObj.name === 'file' && payloadObj.error) {
5984
+ await this.sendReliableStructured(channelId, payloadObj, context);
5985
+ return;
5986
+ }
5987
+ if (!this.connected || !this.client) {
5988
+ logger.debug(`${this.logPrefix()} Custom payload skipped while disconnected: channel=${channelId}`);
5989
+ return;
5990
+ }
5738
5991
  await this.sendStructuredOrThrow(channelId, payloadObj, context);
5739
5992
  }
5740
5993
  async disconnect() {
@@ -5814,15 +6067,15 @@ export class AUNChannel {
5814
6067
  };
5815
6068
  }
5816
6069
  /** 读取本地 agent.md 中的 name(用于身份上下文展示),若本地不存在则尝试远程拉取 */
5817
- loadSelfName(aid) {
6070
+ loadSelfName(aid, aunPath) {
5818
6071
  const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
5819
- const name = resolveAgentDisplayName(aidName);
6072
+ const name = resolveAgentDisplayName(aidName, { aunPath });
5820
6073
  if (!name)
5821
- void this.fetchAndCacheSelfName(aidName);
6074
+ void this.fetchAndCacheSelfName(aidName, aunPath);
5822
6075
  return name;
5823
6076
  }
5824
- async fetchAndCacheSelfName(aidName) {
5825
- const name = await refreshAgentDisplayName(aidName);
6077
+ async fetchAndCacheSelfName(aidName, aunPath) {
6078
+ const name = await refreshAgentDisplayName(aidName, { aunPath });
5826
6079
  if (name) {
5827
6080
  this._selfName = name;
5828
6081
  if (this.aidStatsCollector)
@@ -5839,13 +6092,20 @@ export class AUNChannel {
5839
6092
  if (!this.client)
5840
6093
  return { type: null };
5841
6094
  try {
6095
+ const client = this.client;
6096
+ const revision = this.peerInfoRevision;
5842
6097
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
5843
- const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false);
6098
+ const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
5844
6099
  const type = identity.type === 'human' ? 'human' : 'agent';
5845
6100
  const name = identity.name || undefined;
5846
6101
  const info = { type, name };
5847
- this.peerInfoCache.set(aid, info);
5848
- setTimeout(() => this.peerInfoCache.delete(aid), 30 * 60 * 1000);
6102
+ if (identity.lastCheckedAt !== 0 && this.client === client && this.peerInfoRevision === revision) {
6103
+ this.peerInfoCache.set(aid, info);
6104
+ setTimeout(() => {
6105
+ if (this.peerInfoCache.get(aid) === info)
6106
+ this.peerInfoCache.delete(aid);
6107
+ }, 30 * 60 * 1000);
6108
+ }
5849
6109
  return info;
5850
6110
  }
5851
6111
  catch (e) {
@@ -5867,13 +6127,20 @@ export class AUNChannel {
5867
6127
  if (!this.store)
5868
6128
  throw new Error('not connected');
5869
6129
  const { agentmdPut } = await import('../aun/aid/agentmd.js');
5870
- await agentmdPut(content, { aid: this.config.aid, store: this.store });
6130
+ await agentmdPut(content, {
6131
+ aid: this.config.aid,
6132
+ store: this.store,
6133
+ aunPath: this.config.keystorePath || resolveRoot(),
6134
+ });
5871
6135
  }
5872
6136
  async downloadAgentMd(aid) {
5873
6137
  if (!this.store)
5874
6138
  throw new Error('not connected');
5875
6139
  const { agentmdSync } = await import('../aun/aid/agentmd.js');
5876
- const result = await agentmdSync(aid, { store: this.store ?? undefined });
6140
+ const result = await agentmdSync(aid, {
6141
+ store: this.store ?? undefined,
6142
+ aunPath: this.config.keystorePath || resolveRoot(),
6143
+ });
5877
6144
  return result.content ?? '';
5878
6145
  }
5879
6146
  /**
@@ -5972,7 +6239,7 @@ export class AUNChannelPlugin {
5972
6239
  const dedupeOperationId = payload.kind === 'result.text' && payload.isFinal === false
5973
6240
  ? undefined
5974
6241
  : operationId;
5975
- const replyCtx = outboundCausation
6242
+ let replyCtx = outboundCausation
5976
6243
  ? {
5977
6244
  ...(envelope.replyContext ?? {}),
5978
6245
  delivery,
@@ -5981,6 +6248,15 @@ export class AUNChannelPlugin {
5981
6248
  : (envelope.replyContext || envelope.delivery)
5982
6249
  ? { ...(envelope.replyContext ?? {}), delivery }
5983
6250
  : undefined;
6251
+ const attention = aunOutboundAttention(payload);
6252
+ if (replyCtx) {
6253
+ replyCtx = { ...replyCtx };
6254
+ delete replyCtx.attention;
6255
+ // Assign titles per payload below, never inherit a final-reply title.
6256
+ delete replyCtx.title;
6257
+ }
6258
+ if (attention !== undefined)
6259
+ replyCtx = { ...(replyCtx ?? {}), attention };
5984
6260
  if (outboundCausation) {
5985
6261
  recordCausationSpan(outboundCausation, 'message.outbound', {
5986
6262
  status: 'started',
@@ -6003,7 +6279,7 @@ export class AUNChannelPlugin {
6003
6279
  subtype: payload.kind,
6004
6280
  text: payload.text,
6005
6281
  ...(isError
6006
- ? { message: payload.text, user_message: payload.text, recoverable: true, terminal: false }
6282
+ ? { message: payload.text, user_message: payload.text, recoverable: true }
6007
6283
  : { severity: 'info' }),
6008
6284
  };
6009
6285
  for (const key of ['format', 'reason', 'reasonCode', 'nextStep']) {
@@ -6018,10 +6294,13 @@ export class AUNChannelPlugin {
6018
6294
  ...Object.fromEntries(Object.entries(replyCtx?.metadata ?? {})
6019
6295
  .filter(([key]) => key !== 'operationId')),
6020
6296
  ...(dedupeOperationId ? { operationId: dedupeOperationId } : {}),
6297
+ ...(payload.kind === 'result.text' && payload.metadata?.forkAnchor
6298
+ ? { forkAnchor: payload.metadata.forkAnchor } : {}),
6021
6299
  },
6022
6300
  };
6023
- if (payload.kind === 'result.text' && payload.isFinal)
6301
+ if (payload.kind === 'result.text' && payload.isFinal && delivery.chatType === 'private') {
6024
6302
  sendCtx.title = '✅ 最终回复:';
6303
+ }
6025
6304
  let result;
6026
6305
  try {
6027
6306
  result = await channel.sendMessage(channelId, payload.text, sendCtx);
@@ -6045,7 +6324,7 @@ export class AUNChannelPlugin {
6045
6324
  messages: [{
6046
6325
  messageId: result.messageId,
6047
6326
  partIndex: 0,
6048
- ...(sendCtx.threadId ? { threadId: sendCtx.threadId } : {}),
6327
+ ...(typeof sendCtx.threadId === 'string' ? { threadId: sendCtx.threadId } : {}),
6049
6328
  }],
6050
6329
  };
6051
6330
  }
@@ -6081,14 +6360,13 @@ export class AUNChannelPlugin {
6081
6360
  }
6082
6361
  case 'system.error': {
6083
6362
  const errorPayload = {
6084
- type: 'error',
6363
+ type: payload.recoverable === true ? 'notice' : 'error',
6085
6364
  ...taskBase(),
6086
6365
  subtype: payload.subtype,
6087
6366
  text: payload.text,
6088
- message: payload.text,
6089
- user_message: payload.text,
6090
- recoverable: payload.recoverable === true,
6091
- terminal: payload.recoverable !== true,
6367
+ ...(payload.recoverable === true
6368
+ ? { severity: 'warn' }
6369
+ : { message: payload.text, user_message: payload.text, recoverable: false }),
6092
6370
  };
6093
6371
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6094
6372
  return;
@@ -6101,7 +6379,6 @@ export class AUNChannelPlugin {
6101
6379
  text: payload.text,
6102
6380
  message: payload.text,
6103
6381
  user_message: payload.text,
6104
- terminal: true,
6105
6382
  };
6106
6383
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6107
6384
  return;
@@ -6110,8 +6387,27 @@ export class AUNChannelPlugin {
6110
6387
  const fileCtx = payload.correlationId
6111
6388
  ? { ...(replyCtx ?? {}), metadata: { ...(replyCtx?.metadata ?? {}), correlationId: payload.correlationId } }
6112
6389
  : replyCtx;
6113
- await channel.sendFile(channelId, payload.filePath, fileCtx);
6114
- return;
6390
+ const result = await channel.sendFile(channelId, payload.filePath, fileCtx);
6391
+ if (result.status === 'sent')
6392
+ return sentReceipt(envelope, [result.messageId], fileCtx?.threadId);
6393
+ if (result.status === 'queued') {
6394
+ return {
6395
+ status: 'queued',
6396
+ operationId,
6397
+ messages: [],
6398
+ outboxId: result.outboxId,
6399
+ ...(result.error !== undefined ? { error: result.error } : {}),
6400
+ ...(result.code !== undefined ? { code: result.code } : {}),
6401
+ };
6402
+ }
6403
+ return {
6404
+ status: 'failed',
6405
+ operationId,
6406
+ messages: [],
6407
+ ...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
6408
+ error: result.error,
6409
+ ...(result.code !== undefined ? { code: result.code } : {}),
6410
+ };
6115
6411
  }
6116
6412
  case 'result.image': {
6117
6413
  await channel.sendImage(channelId, payload.data, payload.mimeType, payload.alt, replyCtx);
@@ -6255,13 +6551,13 @@ export class AUNChannelPlugin {
6255
6551
  };
6256
6552
  if (req.kind.kind === 'action') {
6257
6553
  const action = req.kind;
6258
- const aunCard = {
6554
+ const aunCard = withAunAttention({
6259
6555
  type: 'action_card',
6260
6556
  title: action.title,
6261
6557
  actions: action.buttons.map(btn => ({
6262
6558
  label: btn.label, value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
6263
6559
  })),
6264
- };
6560
+ });
6265
6561
  // AUN action_card 以 compose behavior 声明自由文本动作。新版客户端
6266
6562
  // 在卡片内采集并确认;旧版客户端可继续从主编辑器采集。两者都结构化
6267
6563
  // 回传 action_card_reply{behavior:"compose"},Core 不拦截普通消息队列。
@@ -6279,6 +6575,7 @@ export class AUNChannelPlugin {
6279
6575
  const result = await channel.sendContentPayload(channelId, aunCard, {
6280
6576
  contentKind: 'card',
6281
6577
  context: replyCtx,
6578
+ dedupeKey: req.id.startsWith('gir-') ? operationId : undefined,
6282
6579
  logText: action.title ? `[card] ${action.title}` : '[card]',
6283
6580
  postSend: {
6284
6581
  type: 'register_interaction_card',
@@ -6293,7 +6590,7 @@ export class AUNChannelPlugin {
6293
6590
  }
6294
6591
  else if (req.kind.kind === 'command-card') {
6295
6592
  const card = req.kind;
6296
- const aunCard = {
6593
+ const aunCard = withAunAttention({
6297
6594
  type: 'action_card',
6298
6595
  title: card.title,
6299
6596
  actions: card.buttons.map(btn => {
@@ -6306,7 +6603,7 @@ export class AUNChannelPlugin {
6306
6603
  setIfDefined(action, 'disabled', btn.disabled);
6307
6604
  return action;
6308
6605
  }),
6309
- };
6606
+ });
6310
6607
  if (card.body) {
6311
6608
  aunCard.text = card.body;
6312
6609
  setIfDefined(aunCard, 'format', card.bodyFormat);
@@ -6397,6 +6694,14 @@ export class AUNChannelPlugin {
6397
6694
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
6398
6695
  handler(aunOptsToInbound(opts, adapter.channelName, channelType));
6399
6696
  }), async (channelId, text, replyContext) => { await channel.sendMessage(channelId, text, replyContext); }, adapter, channelType);
6697
+ bridge.registerGroupInviteReviewChannel({
6698
+ selfAid: aid,
6699
+ channelName: adapter.channelName,
6700
+ channelKey: adapter.channelKey,
6701
+ agentName: ctx.agentName,
6702
+ channel,
6703
+ adapter,
6704
+ });
6400
6705
  },
6401
6706
  registerHooks(hookCtx) {
6402
6707
  channel.setEventBus(hookCtx.eventBus);