evolcore 0.0.26 → 0.0.27

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 (169) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/claude-runner.js +123 -22
  5. package/dist/agents/codex/app-server-client.js +63 -26
  6. package/dist/agents/codex-runner.js +316 -411
  7. package/dist/agents/ecagent-runner.js +170 -25
  8. package/dist/agents/gemini-runner.js +6 -0
  9. package/dist/agents/runner-types.js +50 -2
  10. package/dist/aun/aid/agentmd.js +73 -31
  11. package/dist/aun/aid/client.js +1 -1
  12. package/dist/aun/aid/control-aid.js +180 -1
  13. package/dist/aun/aid/identity.js +3 -3
  14. package/dist/aun/group/history.js +303 -0
  15. package/dist/aun/group/identity.js +43 -0
  16. package/dist/aun/group/index.js +5 -0
  17. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  18. package/dist/aun/{menu-client.js → menu/client.js} +12 -7
  19. package/dist/aun/menu/index.js +2 -0
  20. package/dist/aun/menu/transport.js +49 -0
  21. package/dist/aun/msg/index.js +2 -1
  22. package/dist/aun/msg/managed-operation.js +84 -18
  23. package/dist/aun/msg/mention-schema.js +51 -0
  24. package/dist/aun/msg/p2p.js +13 -8
  25. package/dist/aun/msg/upload.js +43 -88
  26. package/dist/aun/outbox.js +29 -7
  27. package/dist/aun/rpc/client.js +1 -0
  28. package/dist/aun/storage/client.js +39 -50
  29. package/dist/channels/aun.js +522 -233
  30. package/dist/channels/daemon.js +10 -6
  31. package/dist/cli/agent-command.js +108 -6
  32. package/dist/cli/agent-create.js +515 -2
  33. package/dist/cli/agent.js +109 -8
  34. package/dist/cli/aun-commands.js +191 -42
  35. package/dist/cli/config.js +8 -5
  36. package/dist/cli/contact.js +2 -2
  37. package/dist/cli/ctl-command.js +2 -2
  38. package/dist/cli/daemon-commands.js +35 -23
  39. package/dist/cli/diag.js +2 -2
  40. package/dist/cli/fs-command.js +3 -3
  41. package/dist/cli/handoff-command.js +6 -6
  42. package/dist/cli/index.js +8 -2
  43. package/dist/cli/init.js +34 -1
  44. package/dist/cli/managed-command-guard.js +2 -1
  45. package/dist/cli/menu-client.js +9 -5
  46. package/dist/cli/model.js +3 -3
  47. package/dist/cli/queue-command.js +2 -2
  48. package/dist/cli/response.js +2 -2
  49. package/dist/cli/restart-monitor.js +16 -2
  50. package/dist/cli/task-command.js +59 -0
  51. package/dist/cli/task-context.js +97 -49
  52. package/dist/cli/trigger-command.js +20 -10
  53. package/dist/config/builtin-roles.js +10 -0
  54. package/dist/config/config-field-policy.js +11 -0
  55. package/dist/config/config-manager.js +32 -4
  56. package/dist/config/config-operation-service.js +20 -5
  57. package/dist/config/contact-book-store.js +1 -1
  58. package/dist/config/contact-book.js +1 -1
  59. package/dist/config/contact-operation-service.js +1 -1
  60. package/dist/config/peer-role-resolver.js +1 -1
  61. package/dist/config/resolved-config-op.js +3 -0
  62. package/dist/config-store.js +1 -1
  63. package/dist/core/agent-application-service.js +5 -0
  64. package/dist/core/audit/event-key.js +17 -2
  65. package/dist/core/audit/log-integrity.js +34 -8
  66. package/dist/core/auth/agent-delegation.js +48 -14
  67. package/dist/core/auth/auth-gateway.js +2 -0
  68. package/dist/core/auth/authorization-audit.js +34 -38
  69. package/dist/core/auth/operation-authorizer.js +67 -5
  70. package/dist/core/auth/operation-catalog.js +110 -0
  71. package/dist/core/auth/remote-access-store.js +115 -9
  72. package/dist/core/bootstrap-messages.js +66 -7
  73. package/dist/core/bootstrap-service.js +110 -19
  74. package/dist/core/command/agent-control.js +88 -7
  75. package/dist/core/command/agent-storage-menu.js +443 -0
  76. package/dist/core/command/cli-intent-parser.js +19 -0
  77. package/dist/core/command/command-handler.js +76 -39
  78. package/dist/core/command/data-root-resolver.js +273 -0
  79. package/dist/core/command/group-menu.js +7 -2
  80. package/dist/core/command/menu-catalog.js +34 -4
  81. package/dist/core/command/menu-handler.js +348 -77
  82. package/dist/core/command/menu-protocol.js +40 -6
  83. package/dist/core/command/role-menu.js +2 -2
  84. package/dist/core/command/slash-handler.js +37 -22
  85. package/dist/core/event-catalog.js +4 -0
  86. package/dist/core/evolagent-registry.js +28 -2
  87. package/dist/core/group/invite-service.js +924 -0
  88. package/dist/core/interaction-router.js +22 -8
  89. package/dist/core/message/im-renderer.js +76 -15
  90. package/dist/core/message/message-bridge.js +94 -11
  91. package/dist/core/message/message-log.js +5 -3
  92. package/dist/core/message/message-queue.js +73 -41
  93. package/dist/core/message/response-engine.js +709 -117
  94. package/dist/core/permission/approval-gateway.js +36 -3
  95. package/dist/core/permission/ec-command-parser.js +61 -29
  96. package/dist/core/permission/protected-paths.js +134 -16
  97. package/dist/core/permission/sandbox-runtime.js +72 -3
  98. package/dist/core/permission/tool-error-code.js +15 -0
  99. package/dist/core/permission/tool-policy.js +7 -7
  100. package/dist/core/relation/peer-identity.js +57 -6
  101. package/dist/core/relation/shared-data.js +106 -0
  102. package/dist/core/role/runtime-policy.js +64 -0
  103. package/dist/core/runtime-lock.js +10 -26
  104. package/dist/core/session/runner-task-history.js +170 -0
  105. package/dist/core/session/session-manager.js +34 -6
  106. package/dist/core/session/session-mapper.js +41 -0
  107. package/dist/core/session/session-turn-coordinator.js +37 -7
  108. package/dist/core/session/session-turns.js +48 -7
  109. package/dist/core/task/task-recovery-service.js +176 -0
  110. package/dist/core/task/task-resume-operation.js +32 -0
  111. package/dist/eck/group-rules-sync.js +5 -3
  112. package/dist/eck/kit-renderer.js +1 -0
  113. package/dist/index.js +308 -54
  114. package/dist/ipc.js +141 -15
  115. package/dist/paths.js +17 -4
  116. package/dist/product.js +2 -0
  117. package/dist/trigger/event-source.js +19 -0
  118. package/dist/trigger/feedback.js +233 -40
  119. package/dist/trigger/parser.js +55 -5
  120. package/dist/trigger/patch.js +9 -0
  121. package/dist/trigger/scheduler.js +30 -4
  122. package/dist/trigger/validation.js +20 -3
  123. package/dist/utils/ecweb-utils.js +1 -0
  124. package/dist/utils/error-utils.js +40 -2
  125. package/dist/utils/logger.js +23 -13
  126. package/dist/utils/tool-summary.js +59 -3
  127. package/dist/utils/welcome.js +174 -1
  128. package/ecagent/dist/harness/agent-harness.js +3 -0
  129. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  130. package/kits/docs/INDEX.md +0 -6
  131. package/kits/docs/channels/aun.md +15 -2
  132. package/kits/docs/channels/feishu.md +1 -1
  133. package/kits/docs/context-assembly.md +29 -17
  134. package/kits/docs/evolcore/INDEX.md +8 -3
  135. package/kits/docs/evolcore/agent-create.md +39 -7
  136. package/kits/docs/evolcore/agent.md +33 -1
  137. package/kits/docs/evolcore/aid.md +9 -1
  138. package/kits/docs/evolcore/config.md +40 -1
  139. package/kits/docs/evolcore/group-collaboration.md +1 -1
  140. package/kits/docs/evolcore/group.md +117 -2
  141. package/kits/docs/evolcore/menu-client.md +16 -0
  142. package/kits/docs/evolcore/msg.md +3 -0
  143. package/kits/docs/evolcore/storage.md +3 -0
  144. package/kits/docs/evolcore/task.md +14 -0
  145. package/kits/docs/evolcore/trigger.md +51 -3
  146. package/kits/docs/venues/group.md +5 -0
  147. package/kits/eck_manifest.json +27 -0
  148. package/kits/rules/01-overview.md +11 -7
  149. package/kits/rules/04-relation.md +2 -1
  150. package/kits/rules/05-venue.md +1 -1
  151. package/kits/rules/06-channel.md +6 -4
  152. package/kits/schemas/agent-config.schema.12.json +25 -1
  153. package/kits/schemas/daemon.schema.6.json +2 -2
  154. package/kits/schemas/defaults.schema.5.json +1 -1
  155. package/kits/schemas/relation-config.schema.1.json +1 -1
  156. package/kits/schemas/relation-config.schema.5.json +1 -1
  157. package/kits/schemas/role-config.schema.1.json +14 -0
  158. package/kits/templates/roles/member.json +5 -0
  159. package/kits/templates/roles/visitor.json +5 -0
  160. package/kits/templates/system-fragments/baseagent.md +1 -1
  161. package/kits/templates/system-fragments/bootstrap.md +26 -9
  162. package/kits/templates/system-fragments/channel.md +8 -44
  163. package/kits/templates/system-fragments/commands.md +10 -2
  164. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  165. package/kits/templates/system-fragments/reply.md +56 -0
  166. package/package.json +7 -3
  167. package/dist/aun/group-identity.js +0 -9
  168. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  169. /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;
@@ -629,6 +716,7 @@ export class AUNChannel {
629
716
  * 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
630
717
  */
631
718
  callClient(method, params, lane = 'normal', onStart) {
719
+ params = menuTransportParams(method, params);
632
720
  return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)), lane, onStart);
633
721
  }
634
722
  withOutboundSendGate(method, run, lane = 'normal', onStart) {
@@ -838,7 +926,7 @@ export class AUNChannel {
838
926
  return;
839
927
  }
840
928
  const context = { delivery };
841
- if (threadId)
929
+ if (typeof threadId === 'string')
842
930
  context.threadId = threadId;
843
931
  if (operatorId && delivery.chatType === 'group')
844
932
  context.peerId = operatorId;
@@ -933,7 +1021,7 @@ export class AUNChannel {
933
1021
  threadId,
934
1022
  replyContext: {
935
1023
  ...(cardInfo.replyContext ?? {}),
936
- ...(threadId ? { threadId } : {}),
1024
+ ...(typeof threadId === 'string' ? { threadId } : {}),
937
1025
  ...(cardDelivery ? { delivery: cardDelivery } : {}),
938
1026
  },
939
1027
  source: 'card-trigger',
@@ -1295,14 +1383,14 @@ export class AUNChannel {
1295
1383
  return sanitized;
1296
1384
  return undefined;
1297
1385
  }
1298
- buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId) {
1386
+ buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, lable) {
1299
1387
  if (!groupId)
1300
1388
  throw new Error('buildGroupReplyContext requires groupId');
1301
1389
  const replyContext = {
1302
1390
  metadata: { encrypted, chatmode },
1303
1391
  delivery: { chatType: 'group', groupId },
1304
1392
  };
1305
- if (threadId)
1393
+ if (typeof threadId === 'string')
1306
1394
  replyContext.threadId = threadId;
1307
1395
  replyContext.peerId = senderAid;
1308
1396
  if (messageId)
@@ -1313,20 +1401,24 @@ export class AUNChannel {
1313
1401
  replyContext.metadata = {};
1314
1402
  replyContext.metadata.topicName = topicName;
1315
1403
  }
1404
+ if (lable && threadId)
1405
+ replyContext.metadata.lable = lable;
1316
1406
  return replyContext;
1317
1407
  }
1318
- buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId) {
1408
+ buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId, lable) {
1319
1409
  const replyContext = {
1320
1410
  metadata: { encrypted, chatmode, refMessageId },
1321
1411
  delivery: { chatType: 'private' },
1322
1412
  peerId: senderAid,
1323
1413
  };
1324
- if (threadId)
1414
+ if (typeof threadId === 'string')
1325
1415
  replyContext.threadId = threadId;
1326
1416
  if (messageId)
1327
1417
  replyContext.replyToMessageId = messageId;
1328
1418
  if (topicName && threadId)
1329
1419
  replyContext.metadata.topicName = topicName;
1420
+ if (lable && threadId)
1421
+ replyContext.metadata.lable = lable;
1330
1422
  return replyContext;
1331
1423
  }
1332
1424
  claimInboundMessage(kind, messageId, seq) {
@@ -1443,6 +1535,7 @@ export class AUNChannel {
1443
1535
  invalidInboundEnvelopeLastLogAt = 0;
1444
1536
  groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
1445
1537
  peerInfoCache = new Map();
1538
+ peerInfoRevision = 0;
1446
1539
  messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
1447
1540
  sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
1448
1541
  peerE2ee = new Map();
@@ -1557,6 +1650,44 @@ export class AUNChannel {
1557
1650
  getAid() {
1558
1651
  return this._aid ?? this.config.aid;
1559
1652
  }
1653
+ setGroupInviteReviewHandlers(handlers) {
1654
+ if (this.config.pureIdentity && handlers)
1655
+ return;
1656
+ this.groupInviteHandler = handlers?.onInvite;
1657
+ this.groupInviteConnectedHandler = handlers?.onConnected;
1658
+ this.groupInviteDeliveryGate = handlers?.canDeliver;
1659
+ this.groupInviteDelivered = handlers?.onDelivered;
1660
+ }
1661
+ async getGroupInviteSummary(groupId) {
1662
+ if (!this.client || !this.connected)
1663
+ return {};
1664
+ try {
1665
+ const result = await this.callAndTrace('group.get_info', { group_id: groupId });
1666
+ const group = result?.group ?? result;
1667
+ return {
1668
+ ...(typeof group?.name === 'string' ? { name: group.name.slice(0, 120) } : {}),
1669
+ ...(typeof group?.description === 'string' ? { description: group.description.slice(0, 240) } : {}),
1670
+ };
1671
+ }
1672
+ catch {
1673
+ return {};
1674
+ }
1675
+ }
1676
+ getGroupInviteDeliveryState(outboxId) {
1677
+ return outbox.deliveryState(this.config.aid, outboxId);
1678
+ }
1679
+ async invalidateGroupInviteCard(interactionId, ownerAid, messageIds) {
1680
+ const known = new Set(this.interactionCardMessageIds.get(interactionId) ?? []);
1681
+ await this.invalidateInteraction(interactionId, 'group_invite_review');
1682
+ // After a daemon restart only the review store knows the delivered cards.
1683
+ for (const id of messageIds) {
1684
+ if (!known.has(id))
1685
+ await this.recallInteractionCard(id, ownerAid, { chatType: 'private' });
1686
+ }
1687
+ }
1688
+ refreshGroupInviteMembership(groupId) {
1689
+ this.invalidateObserverGroupMembers(groupId);
1690
+ }
1560
1691
  /** 是否已连接(client 存在且认证完成)。 */
1561
1692
  isConnected() {
1562
1693
  return this.connected && this.client !== null;
@@ -1667,6 +1798,33 @@ export class AUNChannel {
1667
1798
  // instance must not mutate the new instance's health or message state.
1668
1799
  const isCurrentClient = () => this.client === client;
1669
1800
  // Register event handlers before connecting
1801
+ client.on('agentmd.changed', (data) => {
1802
+ if (!isCurrentClient() || this.intentionalDisconnect || typeof data !== 'string')
1803
+ return;
1804
+ let aid;
1805
+ try {
1806
+ aid = validateAIDFormat(data);
1807
+ }
1808
+ catch {
1809
+ return;
1810
+ }
1811
+ this.peerInfoRevision++;
1812
+ this.peerInfoCache.delete(aid);
1813
+ this.groupNameCache.delete(aid);
1814
+ invalidateAgentDisplayName(aid);
1815
+ PeerIdentityCache.invalidate('aun', aid, path.join(resolvePaths().agentsDir, this.config.aid));
1816
+ this.trace('IN', 'agentmd.changed', { aid });
1817
+ // Peer snapshots refresh lazily on the next lookup. The self name is
1818
+ // held separately by this channel, so refresh it with the existing API.
1819
+ if (aid === (this._aid || this.config.aid)) {
1820
+ void refreshAgentDisplayName(aid).then(name => {
1821
+ if (!name || !isCurrentClient() || this.intentionalDisconnect)
1822
+ return;
1823
+ this._selfName = name;
1824
+ this.aidStatsCollector?.setSelfName(this.config.aid, name);
1825
+ });
1826
+ }
1827
+ });
1670
1828
  client.on('message.received', async (data) => {
1671
1829
  if (!isCurrentClient())
1672
1830
  return;
@@ -1712,6 +1870,9 @@ export class AUNChannel {
1712
1870
  const inviteIds = Array.isArray(record.invite_ids) ? record.invite_ids : [];
1713
1871
  this.trace('IN', 'group.invite_received', data);
1714
1872
  logger.info(`${this.logPrefix()} Group invitation received: group=${groupId} pending=${inviteIds.length || 1}`);
1873
+ void Promise.resolve(this.groupInviteHandler?.(data)).catch(error => {
1874
+ logger.warn(`${this.logPrefix()} Group invitation review dispatch failed: ${error instanceof Error ? error.message : String(error)}`);
1875
+ });
1715
1876
  });
1716
1877
  }
1717
1878
  // 0.5.10/0.5.11 local lifecycle events summarize SDK delivery and Pull
@@ -1751,18 +1912,7 @@ export class AUNChannel {
1751
1912
  client.on('message.recalled', (data) => {
1752
1913
  if (!isCurrentClient())
1753
1914
  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
- }
1915
+ this.handleMessageRecalled(data, client, isCurrentClient);
1766
1916
  });
1767
1917
  client.on('message.undecryptable', (data) => {
1768
1918
  if (!isCurrentClient())
@@ -1848,13 +1998,37 @@ export class AUNChannel {
1848
1998
  this._aid = this.client.aid ?? undefined;
1849
1999
  const deviceId = this.client._device_id ?? '';
1850
2000
  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);
2001
+ // 控制 AID 也有标准 host agent.md。发布在后台执行,短暂的 Name
2002
+ // Service/Gateway 故障不能把控制通道判定为连接失败。
2003
+ if (this.config.pureIdentity) {
2004
+ this._selfName = buildControlAgentName(aidName);
2005
+ void ensureControlAgentMd(aidName, {
2006
+ store: this.store ?? undefined,
2007
+ aunPath,
2008
+ cacheRoot: resolveRoot(),
2009
+ })
2010
+ .then(result => {
2011
+ this._selfName = result.name;
2012
+ if (this.aidStatsCollector)
2013
+ this.aidStatsCollector.setSelfName(this.config.aid, result.name);
2014
+ if (result.published)
2015
+ logger.info(`${this.logPrefix()} 控制 AID agent.md 已发布: ${result.name}`);
2016
+ })
2017
+ .catch(error => logger.warn(`${this.logPrefix()} 控制 AID agent.md 补全失败(后台重试): ${error instanceof Error ? error.message : String(error)}`));
2018
+ }
2019
+ else {
2020
+ this._selfName = this.loadSelfName(aidName, aunPath);
2021
+ }
1853
2022
  if (this._selfName && this.aidStatsCollector)
1854
2023
  this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
1855
2024
  this.connected = true;
1856
2025
  this.connectedAt = Date.now();
1857
2026
  this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
2027
+ if (!this.config.pureIdentity) {
2028
+ void Promise.resolve(this.groupInviteConnectedHandler?.()).catch(error => {
2029
+ logger.warn(`${this.logPrefix()} Group invitation recovery failed: ${error instanceof Error ? error.message : String(error)}`);
2030
+ });
2031
+ }
1858
2032
  // AUN seq_tracker is transport-scoped, while this channel's durable
1859
2033
  // dedup log is application-scoped. A diagnostic consumer can advance
1860
2034
  // the former without the application having seen those messages. On a
@@ -1912,7 +2086,7 @@ export class AUNChannel {
1912
2086
  if (hasMessageLogOperation(chatDir, operationId)
1913
2087
  || hasMessageLogOperation(chatDir, postBootstrapWelcomeOperationId(aidName)))
1914
2088
  continue;
1915
- const prepared = outbox.findByDedupeKey(aidName, operationId);
2089
+ const prepared = outbox.findByDedupeKey(aidName, operationId, { includeTerminal: true });
1916
2090
  if (prepared && prepared.channelId === owner && isDeliveryTarget(prepared.delivery) && sameDeliveryTarget(prepared.delivery, delivery))
1917
2091
  continue;
1918
2092
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
@@ -1948,7 +2122,7 @@ export class AUNChannel {
1948
2122
  }
1949
2123
  async sendPostBootstrapWelcome() {
1950
2124
  if (!await this.preparePostBootstrapWelcome())
1951
- return false;
2125
+ throw new Error('Failed to persist post-bootstrap welcome');
1952
2126
  return this.reconcilePostBootstrapWelcome();
1953
2127
  }
1954
2128
  async hasPendingPostBootstrapWelcome() {
@@ -1966,6 +2140,8 @@ export class AUNChannel {
1966
2140
  const pull = client?._pullV2;
1967
2141
  if (typeof repair !== 'function' || typeof pull !== 'function')
1968
2142
  return;
2143
+ if (this.inboundP2PCursorRepairTimer)
2144
+ clearTimeout(this.inboundP2PCursorRepairTimer);
1969
2145
  const ns = `p2p:${this.config.aid}`;
1970
2146
  let appSeq = 0;
1971
2147
  for (const [key, value] of this.inboundSeenMessages) {
@@ -1978,7 +2154,8 @@ export class AUNChannel {
1978
2154
  }
1979
2155
  if (appSeq <= 0)
1980
2156
  return;
1981
- setTimeout(() => {
2157
+ this.inboundP2PCursorRepairTimer = setTimeout(() => {
2158
+ this.inboundP2PCursorRepairTimer = null;
1982
2159
  if (!isCurrentClient() || !this.connected)
1983
2160
  return;
1984
2161
  const transportSeq = Number(tracker.getContiguousSeq?.(ns) ?? 0);
@@ -1996,6 +2173,24 @@ export class AUNChannel {
1996
2173
  }
1997
2174
  }, 250);
1998
2175
  }
2176
+ inboundP2PCursorRepairTimer = null;
2177
+ handleMessageRecalled(data, client, isCurrentClient) {
2178
+ this.trace('IN', 'message.recalled', data);
2179
+ if (data && typeof data === 'object') {
2180
+ const ids = data.message_ids;
2181
+ if (Array.isArray(ids)) {
2182
+ for (const id of ids) {
2183
+ if (typeof id === 'string') {
2184
+ logger.info(`${this.logPrefix()} Message recalled: ${id}`);
2185
+ this.recallHandler?.(id, this._aid || this.config.aid);
2186
+ }
2187
+ }
2188
+ }
2189
+ }
2190
+ // A recall tombstone may arrive at a high transport sequence and advance
2191
+ // the SDK cursor past messages that were never delivered to the app.
2192
+ this.scheduleInboundP2PCursorRepair(client, isCurrentClient);
2193
+ }
1999
2194
  pendingPostBootstrapWelcomeOwners() {
2000
2195
  return pendingPostBootstrapWelcomeOwners(this.config.aid);
2001
2196
  }
@@ -2011,25 +2206,35 @@ export class AUNChannel {
2011
2206
  const configuredAid = this.config.aid;
2012
2207
  const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
2013
2208
  const entries = findPostBootstrapWelcomeOutbox(aid);
2014
- if (entries.length === 0)
2015
- return false;
2016
2209
  const agentConfig = loadAgent(aid);
2210
+ const owners = agentConfig?.owners ?? [];
2211
+ if (entries.length === 0)
2212
+ return postBootstrapWelcomeStatus(aid, owners);
2017
2213
  if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
2018
2214
  logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
2019
- return true;
2215
+ return postBootstrapWelcomeStatus(aid, owners);
2020
2216
  }
2021
2217
  if (!this.connected || !this.client)
2022
- return true;
2218
+ return postBootstrapWelcomeStatus(aid, owners);
2023
2219
  for (const entry of entries) {
2024
2220
  if (!outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
2025
2221
  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);
2222
+ try {
2223
+ const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
2224
+ if (sent.status === 'sent')
2225
+ this.removeDeliveredOutboxEntry(entry);
2226
+ else if (sent.status === 'permanent')
2227
+ this.markPermanentOutboxFailure(entry, sent);
2228
+ else
2229
+ outbox.recordRetry(this.config.aid, entry, sent);
2230
+ }
2231
+ catch (error) {
2232
+ // Lifecycle has already committed. Preserve its success and the
2233
+ // durable operation even when local post-send bookkeeping fails.
2234
+ logger.warn(`${this.logPrefix()} Welcome reconciliation failed: entry=${entry.id} error=${error}`);
2235
+ }
2031
2236
  }
2032
- return true;
2237
+ return postBootstrapWelcomeStatus(aid, owners);
2033
2238
  }
2034
2239
  // ── Event handlers ──────────────────────────────────────────
2035
2240
  /**
@@ -2372,7 +2577,7 @@ export class AUNChannel {
2372
2577
  const chatId = fromAid;
2373
2578
  // 解析对端身份(30天缓存)
2374
2579
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
2375
- const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false);
2580
+ const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
2376
2581
  const shortAid = this.getShortAid(fromAid);
2377
2582
  const displayName = peerIdentity.name || shortAid;
2378
2583
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
@@ -2391,13 +2596,15 @@ export class AUNChannel {
2391
2596
  || p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
2392
2597
  const installation = p2pPayloadType.startsWith('menu.')
2393
2598
  ? this.localSenderInstallation?.aunSenderInstallation(msg) : undefined;
2599
+ const peerDeviceId = p2pPayloadType.startsWith('menu.')
2600
+ ? menuSenderDeviceId(msg.sender_device_id) : undefined;
2394
2601
  this.acknowledgeImmediately(messageId, seq);
2395
2602
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
2396
2603
  this.dispatchMessage({
2397
2604
  channelId: chatId, userId: fromAid,
2398
2605
  text: JSON.stringify(payload),
2399
2606
  chatType: 'private', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq,
2400
- peerDeviceId: installation?.deviceId,
2607
+ peerDeviceId,
2401
2608
  peerSlotId: installation?.slotId,
2402
2609
  threadId,
2403
2610
  peerName: displayName || undefined,
@@ -2406,7 +2613,14 @@ export class AUNChannel {
2406
2613
  protectedHeaders,
2407
2614
  mentions,
2408
2615
  mentionAids: mentionEntryAids(mentions),
2409
- replyContext: this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
2616
+ replyContext: {
2617
+ ...this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
2618
+ metadata: {
2619
+ ...this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId).metadata,
2620
+ ...(protectedHeaders?.sdk_version ? { menuClientSdkVersion: protectedHeaders.sdk_version } : {}),
2621
+ },
2622
+ ...(peerDeviceId ? { toDeviceId: peerDeviceId } : {}),
2623
+ },
2410
2624
  msgType: logDescriptor.msgType,
2411
2625
  payloadType: logDescriptor.payloadType,
2412
2626
  payloadSummary: logDescriptor.payloadSummary,
@@ -2456,7 +2670,7 @@ export class AUNChannel {
2456
2670
  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
2671
  const isSystemP2P = p2pPayloadType === 'event';
2458
2672
  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);
2673
+ const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId, readTopicLable(payload));
2460
2674
  this.dispatchMessage({
2461
2675
  channelId: chatId,
2462
2676
  userId: fromAid,
@@ -2568,7 +2782,7 @@ export class AUNChannel {
2568
2782
  peerName: displayName,
2569
2783
  peerType: peerInfo?.type || 'unknown',
2570
2784
  seq,
2571
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId),
2785
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId, readTopicLable(payload)),
2572
2786
  createdAt,
2573
2787
  });
2574
2788
  return;
@@ -2638,7 +2852,7 @@ export class AUNChannel {
2638
2852
  this.pendingEchoMessages.set(messageId, {
2639
2853
  text: echoText,
2640
2854
  channelId: groupId,
2641
- context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId),
2855
+ context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId, readTopicLable(payload)),
2642
2856
  receiveTs: Date.now(),
2643
2857
  });
2644
2858
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
@@ -2660,8 +2874,20 @@ export class AUNChannel {
2660
2874
  // 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
2661
2875
  // TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
2662
2876
  if (enforceMention && !isMentioned && !isOwnHandoff) {
2877
+ try {
2878
+ if (text.trim() || this.collectAllAttachments(payload).length) {
2879
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq, 'unmentioned', 'filtered');
2880
+ }
2881
+ }
2882
+ catch (error) {
2883
+ // A failed local append must not suppress a subsequent SDK redelivery.
2884
+ const namespace = this.config.aid + ':' + (this.config.channelName ?? 'aun') + ':aun:group';
2885
+ this.inboundSeenMessages.delete(JSON.stringify([namespace, messageId]));
2886
+ this.rewriteInboundDedupe();
2887
+ logger.warn(`${this.logPrefix()} Deferred group message was not recorded; awaiting redelivery: ${error}`);
2888
+ return;
2889
+ }
2663
2890
  this.acknowledgeImmediately(messageId, seq);
2664
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
2665
2891
  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
2892
  return;
2667
2893
  }
@@ -2684,7 +2910,7 @@ export class AUNChannel {
2684
2910
  // Process attachments
2685
2911
  const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments, { chatType: 'group', groupId });
2686
2912
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
2687
- const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
2913
+ const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
2688
2914
  const shortAid = this.getShortAid(senderAid);
2689
2915
  const displayName = peerIdentity.name || shortAid;
2690
2916
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
@@ -2736,7 +2962,7 @@ export class AUNChannel {
2736
2962
  isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
2737
2963
  source: isOwnHandoff ? 'handoff' : undefined,
2738
2964
  encrypted: msgEncrypted,
2739
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId),
2965
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId, readTopicLable(payload)),
2740
2966
  mentionMode: serverMentionMode,
2741
2967
  images: inboundImages.length > 0 ? inboundImages : undefined,
2742
2968
  msgType: logDescriptor.msgType,
@@ -2786,7 +3012,7 @@ export class AUNChannel {
2786
3012
  channelId: event.channelId,
2787
3013
  context: {
2788
3014
  ...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
2789
- ...(threadId ? { threadId } : {}),
3015
+ ...(typeof threadId === 'string' ? { threadId } : {}),
2790
3016
  delivery,
2791
3017
  },
2792
3018
  receiveTs: Date.now(),
@@ -2801,7 +3027,7 @@ export class AUNChannel {
2801
3027
  const replyContext = {
2802
3028
  ...(event.replyContext ?? {}),
2803
3029
  delivery,
2804
- ...(threadId ? { threadId } : {}),
3030
+ ...(typeof threadId === 'string' ? { threadId } : {}),
2805
3031
  };
2806
3032
  this.messageHandler({
2807
3033
  channelId: event.channelId || '',
@@ -2841,10 +3067,10 @@ export class AUNChannel {
2841
3067
  logger.error(`${this.logPrefix()} Message handler error:`, err);
2842
3068
  });
2843
3069
  }
2844
- appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq) {
3070
+ appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq, groupAttention, groupDelivery) {
2845
3071
  try {
2846
3072
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
2847
- appendMessageLog(chatDir, buildInboundEntry({
3073
+ const entry = buildInboundEntry({
2848
3074
  from: fromAid,
2849
3075
  to: this.config.aid,
2850
3076
  threadId: threadId || null,
@@ -2859,10 +3085,18 @@ export class AUNChannel {
2859
3085
  timestamp: receivedAt,
2860
3086
  receivedAt,
2861
3087
  gatewaySeq,
2862
- }));
3088
+ groupAttention,
3089
+ groupDelivery,
3090
+ });
3091
+ if (groupDelivery === 'filtered')
3092
+ appendMessageLogStrict(chatDir, entry, true);
3093
+ else
3094
+ appendMessageLog(chatDir, entry);
2863
3095
  }
2864
3096
  catch (e) {
2865
- logger.debug(`${this.logPrefix()} appendInboundPayloadJsonl failed: ${e}`);
3097
+ logger.warn(`${this.logPrefix()} appendInboundPayloadJsonl failed: ${e}`);
3098
+ if (groupDelivery === 'filtered')
3099
+ throw e;
2866
3100
  }
2867
3101
  }
2868
3102
  // ── 观察者模式(Observer Mode) ──────────────────────────────
@@ -3641,7 +3875,9 @@ export class AUNChannel {
3641
3875
  return Buffer.byteLength(logText, 'utf-8');
3642
3876
  }
3643
3877
  applyReplyContextToPayload(payload, context) {
3644
- const finalPayload = this.stripUndefinedDeep({ ...payload });
3878
+ const finalPayload = this.stripUndefinedDeep(withAunAttention({ ...payload }, context?.attention));
3879
+ if (finalPayload.thread_id === '')
3880
+ delete finalPayload.thread_id;
3645
3881
  if (context?.threadId && !finalPayload.thread_id)
3646
3882
  finalPayload.thread_id = context.threadId;
3647
3883
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
@@ -3653,13 +3889,14 @@ export class AUNChannel {
3653
3889
  return finalPayload;
3654
3890
  }
3655
3891
  /**
3656
- * Validate the AUN-specific structured field before it crosses the durable
3892
+ * Validate AUN-specific structured fields before crossing the durable
3657
3893
  * send boundary. Return a copy when mentions are present so callers do not
3658
3894
  * retain an unvalidated array reference.
3659
3895
  */
3660
3896
  normalizeAunPayloadMentions(payload) {
3661
3897
  if (!payload || typeof payload !== 'object' || Array.isArray(payload))
3662
3898
  return payload;
3899
+ payload = withAunAttention(payload);
3663
3900
  if (!Object.prototype.hasOwnProperty.call(payload, 'mentions'))
3664
3901
  return payload;
3665
3902
  return {
@@ -3804,16 +4041,21 @@ export class AUNChannel {
3804
4041
  registerCardPostSend(messageId, action, channelId, delivery, replyContext) {
3805
4042
  if (action.type !== 'register_interaction_card')
3806
4043
  return;
4044
+ // Card replies start a new input; an outbound attention marker must not
4045
+ // become the default for every message produced after clicking the card.
4046
+ const callbackContext = replyContext ? { ...replyContext } : undefined;
4047
+ if (callbackContext)
4048
+ delete callbackContext.attention;
3807
4049
  this.cardMessageIdMap.set(messageId, {
3808
4050
  requestId: action.requestId,
3809
4051
  isCommandCard: action.isCommandCard,
3810
4052
  initiatorAid: action.initiatorAid,
3811
4053
  channelId,
3812
4054
  delivery: delivery ?? action.delivery,
3813
- replyContext,
4055
+ replyContext: callbackContext,
3814
4056
  });
3815
4057
  this.ownedCardMsgIds.add(messageId);
3816
- if (replyContext?.threadId) {
4058
+ if (typeof replyContext?.threadId === 'string') {
3817
4059
  this.ownedCardTopics.set(messageId, { channelId, threadId: replyContext.threadId });
3818
4060
  }
3819
4061
  let messageIds = this.interactionCardMessageIds.get(action.requestId);
@@ -3850,6 +4092,8 @@ export class AUNChannel {
3850
4092
  return;
3851
4093
  if (entry.postSend.type === 'register_interaction_card') {
3852
4094
  this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery, entry.context);
4095
+ if (entry.postSend.requestId.startsWith('gir-'))
4096
+ this.groupInviteDelivered?.(entry.postSend.requestId, messageId);
3853
4097
  }
3854
4098
  }
3855
4099
  async sendAunPayload(channelId, payload, context, label) {
@@ -3881,9 +4125,20 @@ export class AUNChannel {
3881
4125
  const payloadWithCausationRef = causationRef
3882
4126
  ? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
3883
4127
  : normalizedPayload;
4128
+ if (payloadWithCausationRef.thread_id === '')
4129
+ delete payloadWithCausationRef.thread_id;
3884
4130
  // Truncate payload if too large (AUN SDK limit: 1MB)
3885
4131
  const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
3886
4132
  const params = { payload: truncatedPayload, encrypt };
4133
+ const isMenuPayload = typeof truncatedPayload.type === 'string' && truncatedPayload.type.startsWith('menu.');
4134
+ if (!isGroup && truncatedPayload.type === 'menu.response')
4135
+ params.to_device_id = context?.toDeviceId;
4136
+ if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
4137
+ params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
4138
+ }
4139
+ if (isMenuPayload && encrypt && context?.metadata?.protectedHeaders) {
4140
+ params.protected_headers = context.metadata.protectedHeaders;
4141
+ }
3887
4142
  if (isGroup)
3888
4143
  params.group_id = delivery.groupId;
3889
4144
  else
@@ -3904,7 +4159,7 @@ export class AUNChannel {
3904
4159
  return await callOnce(params, false);
3905
4160
  }
3906
4161
  catch (e) {
3907
- if (encrypt && e instanceof E2EEError) {
4162
+ if (encrypt && e instanceof E2EEError && !isMenuPayload) {
3908
4163
  this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
3909
4164
  logger.warn(`${this.logPrefix()} E2EE ${label} send failed to ${channelId}, retrying plaintext: ${e}`);
3910
4165
  const fallbackParams = { ...params, encrypt: false };
@@ -3957,7 +4212,8 @@ export class AUNChannel {
3957
4212
  ...classifyAunPayloadForLog(payload), msgId: messageId, encrypt, context, isGroup, source,
3958
4213
  transport: this.sendReceiptFromResult(result),
3959
4214
  });
3960
- this.forwardOutbound(result);
4215
+ if (typeof payload.type !== 'string' || !payload.type.startsWith('menu.'))
4216
+ this.forwardOutbound(result);
3961
4217
  }
3962
4218
  async sendContentPayload(channelId, payload, opts) {
3963
4219
  const delivery = this.requireDelivery(channelId, opts.context);
@@ -4000,6 +4256,7 @@ export class AUNChannel {
4000
4256
  queue: opts.queue,
4001
4257
  channelId,
4002
4258
  delivery,
4259
+ dedupeKey: opts.dedupeKey,
4003
4260
  type: 'payload',
4004
4261
  contentKind: opts.contentKind,
4005
4262
  payload: finalPayload,
@@ -4071,7 +4328,7 @@ export class AUNChannel {
4071
4328
  base.initiator = context.peerId;
4072
4329
  if (context?.replyToMessageId)
4073
4330
  base.ref_message_id = context.replyToMessageId;
4074
- return base;
4331
+ return withAunAttention(base, context?.attention);
4075
4332
  }
4076
4333
  activityLogText(raw) {
4077
4334
  // 兼容两种入参:原始 ThoughtItem(顶层字段),或已构建的 activity payload(字段收在 .item 里)
@@ -4101,65 +4358,23 @@ export class AUNChannel {
4101
4358
  * the encrypted AUN envelope and RPC framing.
4102
4359
  */
4103
4360
  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) {
4361
+ const result = truncateAunPayloadIfNeeded(payload);
4362
+ if (result.originalSize <= AUN_MAX_PAYLOAD_SIZE) {
4110
4363
  return payload; // No truncation needed
4111
4364
  }
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`);
4365
+ logger.warn(`${this.logPrefix()} Payload too large (${formatSize(result.originalSize)}), truncating... label=${label}`);
4366
+ for (const field of result.truncatedFields) {
4367
+ logger.info(`${this.logPrefix()} Truncated field ${field.path}: ${formatSize(field.originalSize)} → ${formatSize(field.finalSize)}`);
4368
+ }
4369
+ if (result.dropped) {
4370
+ logger.error(`${this.logPrefix()} Payload still too large after truncation: ${formatSize(result.finalSize)}, dropping payload`);
4156
4371
  return {
4157
4372
  type: 'text',
4158
4373
  text: '消息内容过大,无法完整发送,请缩小输出后重试',
4159
4374
  };
4160
4375
  }
4161
- logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(totalSize)} → ${formatSize(newSize)}`);
4162
- return truncated;
4376
+ logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(result.originalSize)} → ${formatSize(result.finalSize)}`);
4377
+ return result.payload;
4163
4378
  }
4164
4379
  buildActivityPayload(envelope, context, item) {
4165
4380
  const rawItem = item && typeof item === 'object' && !Array.isArray(item)
@@ -4171,11 +4386,11 @@ export class AUNChannel {
4171
4386
  const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
4172
4387
  ? { ...rawItem, arguments: displayArguments }
4173
4388
  : rawItem;
4174
- return {
4389
+ return withAunAttention({
4175
4390
  ...this.buildTaskPayloadBase(envelope, context),
4176
4391
  type: 'activity',
4177
4392
  item: this.stripUndefinedDeep(activityItem),
4178
- };
4393
+ });
4179
4394
  }
4180
4395
  async sendReliableStructured(channelId, payload, context, logText) {
4181
4396
  const result = await this.sendContentPayload(channelId, payload, {
@@ -4197,6 +4412,7 @@ export class AUNChannel {
4197
4412
  return result;
4198
4413
  }
4199
4414
  async sendMessage(channelId, text, context) {
4415
+ parseAunAttention(context?.attention);
4200
4416
  if (!text?.trim()) {
4201
4417
  logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
4202
4418
  return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
@@ -4313,6 +4529,7 @@ export class AUNChannel {
4313
4529
  if (entry.deliveryReceipt?.messageId) {
4314
4530
  return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
4315
4531
  }
4532
+ outbox.recordRetry(this.config.aid, entry, result);
4316
4533
  return {
4317
4534
  status: 'queued',
4318
4535
  outboxId: entry.id,
@@ -4332,6 +4549,8 @@ export class AUNChannel {
4332
4549
  try {
4333
4550
  const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
4334
4551
  finalPayload = this.stripUndefinedDeep(validatedPayload);
4552
+ if (finalPayload.thread_id === '')
4553
+ delete finalPayload.thread_id;
4335
4554
  }
4336
4555
  catch (e) {
4337
4556
  return {
@@ -4433,11 +4652,7 @@ export class AUNChannel {
4433
4652
  code: 'H_CLASS_PROTECTED',
4434
4653
  });
4435
4654
  }
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, {
4655
+ const built = await uploadFileAndBuildPayload(this.client, this.config.aid, file.filePath, {
4441
4656
  as: file.as,
4442
4657
  contentType: file.contentType,
4443
4658
  text: file.text,
@@ -4458,6 +4673,8 @@ export class AUNChannel {
4458
4673
  // removed from a malformed mentions array.
4459
4674
  const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
4460
4675
  const finalPayload = this.stripUndefinedDeep(validatedPayload);
4676
+ if (finalPayload.thread_id === '')
4677
+ delete finalPayload.thread_id;
4461
4678
  const payloadMentions = Object.prototype.hasOwnProperty.call(finalPayload, 'mentions')
4462
4679
  ? normalizeAunMentionEntries(finalPayload.mentions)
4463
4680
  : undefined;
@@ -4612,10 +4829,16 @@ export class AUNChannel {
4612
4829
  return { status: 'retry', error: 'post-bootstrap welcome is waiting for active lifecycle', code: 'BOOTSTRAP_NOT_ACTIVE' };
4613
4830
  }
4614
4831
  }
4832
+ if (operationId.startsWith(bootstrapInitialMessageOperationPrefix(this.config.aid)) && !entry.deliveryReceipt) {
4833
+ const config = loadAgent(this.config.aid);
4834
+ if (config && resolveAgentLifecycle(config) === 'active') {
4835
+ return { status: 'permanent', code: 'BOOTSTRAP_SUPERSEDED', error: 'Bootstrap is complete; initial prompt cancelled' };
4836
+ }
4837
+ }
4615
4838
  }
4616
4839
  // 从 context.metadata.source 读取 source,默认为 'daemon'
4617
4840
  const source = context?.metadata?.source ?? 'daemon';
4618
- const payload = { type: 'text', text: finalText };
4841
+ const payload = withAunAttention({ type: 'text', text: finalText }, context?.attention);
4619
4842
  if (delivery.chatType === 'group') {
4620
4843
  const extracted = this.extractMentionEntriesFromText(finalText);
4621
4844
  if (extracted.length > 0)
@@ -4629,6 +4852,8 @@ export class AUNChannel {
4629
4852
  payload.task_id = context.metadata.taskId;
4630
4853
  if (context?.metadata?.chatmode)
4631
4854
  payload.chatmode = context.metadata.chatmode;
4855
+ if (context?.metadata?.forkAnchor)
4856
+ payload.fork_anchor = context.metadata.forkAnchor;
4632
4857
  const causation = normalizeCausation(context?.metadata?.causation);
4633
4858
  const causationRef = delivery.chatType !== 'group' && causation
4634
4859
  ? `caus_${crypto.randomUUID().replace(/-/g, '')}`
@@ -4819,6 +5044,7 @@ export class AUNChannel {
4819
5044
  queue: entry.queue,
4820
5045
  };
4821
5046
  const receipt = {
5047
+ acceptedAt: Date.now(),
4822
5048
  messageId,
4823
5049
  encrypt,
4824
5050
  transport: this.sendReceiptFromResult(result),
@@ -4841,6 +5067,15 @@ export class AUNChannel {
4841
5067
  logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
4842
5068
  return { ok: true, status: 'sent' };
4843
5069
  }
5070
+ if (interactionId?.startsWith('gir-')) {
5071
+ const result = this.groupInviteDeliveryGate?.(interactionId, entry.channelId) ?? 'stale';
5072
+ if (result === 'defer')
5073
+ return { ok: false, status: 'retry', error: 'group invitation callback is not ready', code: 'INVITE_REVIEW_NOT_READY' };
5074
+ if (result !== 'allow') {
5075
+ logger.info(`${this.logPrefix()} Discarded stale group invitation card: request=${interactionId} entry=${entry.id}`);
5076
+ return { ok: true, status: 'sent' };
5077
+ }
5078
+ }
4844
5079
  const interactionExpiresAt = entry.postSend?.type === 'register_interaction_card'
4845
5080
  ? entry.postSend.expiresAt
4846
5081
  : undefined;
@@ -4988,6 +5223,8 @@ export class AUNChannel {
4988
5223
  const payloadWithChatmode = context?.metadata?.chatmode && !finalPayload.chatmode
4989
5224
  ? { ...finalPayload, chatmode: context.metadata.chatmode }
4990
5225
  : finalPayload;
5226
+ if (payloadWithChatmode.thread_id === '')
5227
+ delete payloadWithChatmode.thread_id;
4991
5228
  if (context?.threadId && !payloadWithChatmode.thread_id)
4992
5229
  payloadWithChatmode.thread_id = context.threadId;
4993
5230
  const params = {
@@ -5070,7 +5307,7 @@ export class AUNChannel {
5070
5307
  // Validate before checking connection state so malformed producer payloads
5071
5308
  // are reported deterministically instead of being hidden by a disconnect.
5072
5309
  const validatedPayload = this.normalizeAunPayloadMentions(payload);
5073
- const finalPayload = this.stripUndefinedDeep(validatedPayload);
5310
+ const finalPayload = this.stripUndefinedDeep(withAunAttention(validatedPayload, context?.attention));
5074
5311
  if (!this.connected || !this.client) {
5075
5312
  return {
5076
5313
  ok: false,
@@ -5085,12 +5322,19 @@ export class AUNChannel {
5085
5322
  const encrypt = context?.metadata?.encrypted != null
5086
5323
  ? !!(context.metadata.encrypted)
5087
5324
  : this.shouldEncrypt(encryptTarget);
5325
+ if (finalPayload.thread_id === '')
5326
+ delete finalPayload.thread_id;
5088
5327
  if (context?.threadId && !finalPayload.thread_id)
5089
5328
  finalPayload.thread_id = context.threadId;
5090
5329
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
5091
5330
  finalPayload.ref_message_id = context.replyToMessageId;
5092
5331
  const isMenuPayload = typeof finalPayload.type === 'string' && finalPayload.type.startsWith('menu.');
5093
5332
  const params = { payload: finalPayload, encrypt };
5333
+ if (!isGroup && finalPayload.type === 'menu.response')
5334
+ params.to_device_id = context?.toDeviceId;
5335
+ if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
5336
+ params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
5337
+ }
5094
5338
  const rawHeaders = context?.metadata?.protectedHeaders;
5095
5339
  if (encrypt && rawHeaders && typeof rawHeaders === 'object' && !Array.isArray(rawHeaders)) {
5096
5340
  const protectedHeaders = {};
@@ -5156,21 +5400,23 @@ export class AUNChannel {
5156
5400
  });
5157
5401
  }
5158
5402
  async sendFile(channelId, filePath, context) {
5403
+ parseAunAttention(context?.attention);
5159
5404
  const delivery = this.requireDelivery(channelId, context);
5160
5405
  context = this.withDelivery(context, delivery);
5406
+ const isFetch = typeof context?.metadata?.correlationId === 'string' && !!context.metadata.correlationId;
5161
5407
  const absPath = path.resolve(filePath);
5162
5408
  if (!fs.existsSync(absPath)) {
5163
5409
  logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
5164
- return;
5410
+ return { status: 'failed', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
5165
5411
  }
5166
5412
  const stat = fs.statSync(absPath);
5167
5413
  if (stat.size === 0) {
5168
5414
  logger.warn(`${this.logPrefix()} sendFile: file is empty`);
5169
- return;
5415
+ return { status: 'failed', error: 'file is empty', code: 'EMPTY_FILE' };
5170
5416
  }
5171
5417
  if (stat.size > 10 * 1024 * 1024) {
5172
5418
  logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
5173
- return;
5419
+ return { status: 'failed', error: `file too large (max ${formatSize(MAX_AUN_ATTACHMENT_SIZE)})`, code: 'FILE_TOO_LARGE' };
5174
5420
  }
5175
5421
  // Write-ahead: persist to outbox
5176
5422
  const entry = outbox.enqueue(this.config.aid, {
@@ -5188,17 +5434,47 @@ export class AUNChannel {
5188
5434
  if (!this.reconnectTimer && !this.client) {
5189
5435
  this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendFile failed: ${e}`));
5190
5436
  }
5191
- return;
5437
+ const error = 'AUN channel is not connected';
5438
+ if (isFetch) {
5439
+ this.markPermanentOutboxFailure(entry, { error, code: 'AUN_NOT_CONNECTED' });
5440
+ return { status: 'failed', outboxId: entry.id, error, code: 'AUN_NOT_CONNECTED' };
5441
+ }
5442
+ return { status: 'queued', outboxId: entry.id, error, code: 'AUN_NOT_CONNECTED' };
5192
5443
  }
5193
5444
  const result = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
5194
5445
  if (result.status === 'sent') {
5446
+ const messageId = result.messageId ?? entry.deliveryReceipt?.messageId;
5447
+ if (!messageId) {
5448
+ const error = 'file send returned no message_id';
5449
+ this.markPermanentOutboxFailure(entry, { error, code: 'MISSING_MESSAGE_ID' });
5450
+ return { status: 'failed', outboxId: entry.id, error, code: 'MISSING_MESSAGE_ID' };
5451
+ }
5195
5452
  this.removeDeliveredOutboxEntry(entry);
5453
+ return { status: 'sent', messageId };
5196
5454
  }
5197
- else if (result.status === 'permanent') {
5455
+ // Remote acceptance is authoritative even if local bookkeeping needs retry.
5456
+ if (entry.deliveryReceipt?.messageId)
5457
+ return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
5458
+ if (result.status === 'permanent' || isFetch) {
5459
+ // A correlated fetch must resolve promptly. Retrying it after reporting
5460
+ // failure could deliver an unsolicited file for an already closed request.
5198
5461
  this.markPermanentOutboxFailure(entry, result);
5462
+ return {
5463
+ status: 'failed',
5464
+ outboxId: entry.id,
5465
+ error: result.error ?? 'permanent file send failure',
5466
+ ...(result.code !== undefined ? { code: result.code } : {}),
5467
+ };
5199
5468
  }
5469
+ return {
5470
+ status: 'queued',
5471
+ outboxId: entry.id,
5472
+ ...(result.error !== undefined ? { error: result.error } : {}),
5473
+ ...(result.code !== undefined ? { code: result.code } : {}),
5474
+ };
5200
5475
  }
5201
5476
  async sendImage(channelId, data, mimeType, alt, context) {
5477
+ parseAunAttention(context?.attention);
5202
5478
  const delivery = this.requireDelivery(channelId, context);
5203
5479
  context = this.withDelivery(context, delivery);
5204
5480
  if (!Buffer.isBuffer(data)) {
@@ -5268,11 +5544,7 @@ export class AUNChannel {
5268
5544
  }
5269
5545
  const digest = crypto.createHash('sha256').update(data).digest('hex').slice(0, 12);
5270
5546
  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 });
5547
+ const built = await uploadBufferAndBuildPayload(this.client, this.config.aid, data, filename, { as: 'image', contentType });
5276
5548
  const imagePayload = { ...built.payload };
5277
5549
  setIfDefined(imagePayload, 'alt', alt);
5278
5550
  return imagePayload;
@@ -5359,57 +5631,14 @@ export class AUNChannel {
5359
5631
  logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
5360
5632
  return { status: 'permanent', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
5361
5633
  }
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
5634
  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
- };
5635
+ const { attachment } = await uploadFileAndBuildPayload(this.client, this.config.aid, absPath, { as: 'file' });
5636
+ const { filename, size_bytes: size, content_type: contentType } = attachment;
5408
5637
  const filePayload = buildAunFilePayload({
5409
5638
  filename,
5410
- size: stat.size,
5639
+ size,
5411
5640
  contentType,
5412
- attachment,
5641
+ attachment: { ...attachment },
5413
5642
  context,
5414
5643
  });
5415
5644
  const fileTargetAid = channelId;
@@ -5495,7 +5724,7 @@ export class AUNChannel {
5495
5724
  logger.error(`${this.logPrefix()} AUN accepted the durable file but its receipt could not be persisted: id=${entry.id} mid=${sentMid} error=${detail}`);
5496
5725
  return { status: 'permanent', error: detail, code: 'OUTBOX_RECEIPT_CHECKPOINT_FAILED' };
5497
5726
  }
5498
- logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
5727
+ logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(size)}) → ${channelId}`);
5499
5728
  const fileText = filePayload.text;
5500
5729
  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
5730
  const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
@@ -5614,11 +5843,12 @@ export class AUNChannel {
5614
5843
  // corrected Owner route was delivered.
5615
5844
  if (entry.channelId !== owner || entry.delivery?.chatType !== 'private') {
5616
5845
  delete repaired.deliveryReceipt;
5846
+ delete repaired.deliveryResult;
5847
+ delete repaired.terminal;
5848
+ delete repaired.lastError;
5849
+ delete repaired.lastErrorCode;
5850
+ delete repaired.attempts;
5617
5851
  }
5618
- delete repaired.terminal;
5619
- delete repaired.lastError;
5620
- delete repaired.lastErrorCode;
5621
- delete repaired.attempts;
5622
5852
  if (outbox.replaceIfRouteMatches(aid, entry, repaired) !== 'replaced')
5623
5853
  continue;
5624
5854
  logger.warn(`${this.logPrefix()} Repaired bootstrap outbox route to private Owner delivery: entry=${entry.id} owner=${owner}`);
@@ -5658,17 +5888,21 @@ export class AUNChannel {
5658
5888
  notice: 'notice',
5659
5889
  requires_action: 'requires_action',
5660
5890
  };
5661
- const statusPayload = {
5891
+ const statusPayload = withAunAttention({
5662
5892
  type: 'task.status',
5663
5893
  status: stateMap[status] ?? status,
5664
5894
  task_id: taskId,
5665
5895
  session_id: sessionId,
5666
5896
  severity,
5667
- terminal: ['done', 'interrupted', 'error', 'timeout'].includes(status),
5668
- };
5897
+ });
5669
5898
  const metadataInput = extraMeta ? { ...extraMeta } : undefined;
5670
5899
  if (metadataInput)
5671
5900
  delete metadataInput.severity;
5901
+ const statusKind = typeof extraMeta?.kind === 'string' ? extraMeta.kind : undefined;
5902
+ if (statusKind)
5903
+ statusPayload.kind = statusKind;
5904
+ if (metadataInput)
5905
+ delete metadataInput.kind;
5672
5906
  const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
5673
5907
  if (metadata && Object.keys(metadata).length > 0)
5674
5908
  statusPayload.metadata = metadata;
@@ -5721,10 +5955,6 @@ export class AUNChannel {
5721
5955
  // returns until the channel reconnects.
5722
5956
  const delivery = this.requireDelivery(channelId, context);
5723
5957
  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
5958
  // SDK 0.3.0 E2EE requires payload to be an object
5729
5959
  let payloadObj;
5730
5960
  try {
@@ -5735,6 +5965,16 @@ export class AUNChannel {
5735
5965
  catch {
5736
5966
  payloadObj = { text: payload };
5737
5967
  }
5968
+ // File fetch was already acknowledged. Its terminal error must survive a
5969
+ // disconnect so the pending App request is not silently abandoned.
5970
+ if (payloadObj.type === 'menu.response' && payloadObj.name === 'file' && payloadObj.error) {
5971
+ await this.sendReliableStructured(channelId, payloadObj, context);
5972
+ return;
5973
+ }
5974
+ if (!this.connected || !this.client) {
5975
+ logger.debug(`${this.logPrefix()} Custom payload skipped while disconnected: channel=${channelId}`);
5976
+ return;
5977
+ }
5738
5978
  await this.sendStructuredOrThrow(channelId, payloadObj, context);
5739
5979
  }
5740
5980
  async disconnect() {
@@ -5814,15 +6054,15 @@ export class AUNChannel {
5814
6054
  };
5815
6055
  }
5816
6056
  /** 读取本地 agent.md 中的 name(用于身份上下文展示),若本地不存在则尝试远程拉取 */
5817
- loadSelfName(aid) {
6057
+ loadSelfName(aid, aunPath) {
5818
6058
  const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
5819
- const name = resolveAgentDisplayName(aidName);
6059
+ const name = resolveAgentDisplayName(aidName, { aunPath });
5820
6060
  if (!name)
5821
- void this.fetchAndCacheSelfName(aidName);
6061
+ void this.fetchAndCacheSelfName(aidName, aunPath);
5822
6062
  return name;
5823
6063
  }
5824
- async fetchAndCacheSelfName(aidName) {
5825
- const name = await refreshAgentDisplayName(aidName);
6064
+ async fetchAndCacheSelfName(aidName, aunPath) {
6065
+ const name = await refreshAgentDisplayName(aidName, { aunPath });
5826
6066
  if (name) {
5827
6067
  this._selfName = name;
5828
6068
  if (this.aidStatsCollector)
@@ -5839,13 +6079,20 @@ export class AUNChannel {
5839
6079
  if (!this.client)
5840
6080
  return { type: null };
5841
6081
  try {
6082
+ const client = this.client;
6083
+ const revision = this.peerInfoRevision;
5842
6084
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
5843
- const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false);
6085
+ const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
5844
6086
  const type = identity.type === 'human' ? 'human' : 'agent';
5845
6087
  const name = identity.name || undefined;
5846
6088
  const info = { type, name };
5847
- this.peerInfoCache.set(aid, info);
5848
- setTimeout(() => this.peerInfoCache.delete(aid), 30 * 60 * 1000);
6089
+ if (identity.lastCheckedAt !== 0 && this.client === client && this.peerInfoRevision === revision) {
6090
+ this.peerInfoCache.set(aid, info);
6091
+ setTimeout(() => {
6092
+ if (this.peerInfoCache.get(aid) === info)
6093
+ this.peerInfoCache.delete(aid);
6094
+ }, 30 * 60 * 1000);
6095
+ }
5849
6096
  return info;
5850
6097
  }
5851
6098
  catch (e) {
@@ -5867,13 +6114,20 @@ export class AUNChannel {
5867
6114
  if (!this.store)
5868
6115
  throw new Error('not connected');
5869
6116
  const { agentmdPut } = await import('../aun/aid/agentmd.js');
5870
- await agentmdPut(content, { aid: this.config.aid, store: this.store });
6117
+ await agentmdPut(content, {
6118
+ aid: this.config.aid,
6119
+ store: this.store,
6120
+ aunPath: this.config.keystorePath || resolveRoot(),
6121
+ });
5871
6122
  }
5872
6123
  async downloadAgentMd(aid) {
5873
6124
  if (!this.store)
5874
6125
  throw new Error('not connected');
5875
6126
  const { agentmdSync } = await import('../aun/aid/agentmd.js');
5876
- const result = await agentmdSync(aid, { store: this.store ?? undefined });
6127
+ const result = await agentmdSync(aid, {
6128
+ store: this.store ?? undefined,
6129
+ aunPath: this.config.keystorePath || resolveRoot(),
6130
+ });
5877
6131
  return result.content ?? '';
5878
6132
  }
5879
6133
  /**
@@ -5972,7 +6226,7 @@ export class AUNChannelPlugin {
5972
6226
  const dedupeOperationId = payload.kind === 'result.text' && payload.isFinal === false
5973
6227
  ? undefined
5974
6228
  : operationId;
5975
- const replyCtx = outboundCausation
6229
+ let replyCtx = outboundCausation
5976
6230
  ? {
5977
6231
  ...(envelope.replyContext ?? {}),
5978
6232
  delivery,
@@ -5981,6 +6235,13 @@ export class AUNChannelPlugin {
5981
6235
  : (envelope.replyContext || envelope.delivery)
5982
6236
  ? { ...(envelope.replyContext ?? {}), delivery }
5983
6237
  : undefined;
6238
+ const attention = aunOutboundAttention(payload);
6239
+ if (replyCtx) {
6240
+ replyCtx = { ...replyCtx };
6241
+ delete replyCtx.attention;
6242
+ }
6243
+ if (attention !== undefined)
6244
+ replyCtx = { ...(replyCtx ?? {}), attention };
5984
6245
  if (outboundCausation) {
5985
6246
  recordCausationSpan(outboundCausation, 'message.outbound', {
5986
6247
  status: 'started',
@@ -6003,7 +6264,7 @@ export class AUNChannelPlugin {
6003
6264
  subtype: payload.kind,
6004
6265
  text: payload.text,
6005
6266
  ...(isError
6006
- ? { message: payload.text, user_message: payload.text, recoverable: true, terminal: false }
6267
+ ? { message: payload.text, user_message: payload.text, recoverable: true }
6007
6268
  : { severity: 'info' }),
6008
6269
  };
6009
6270
  for (const key of ['format', 'reason', 'reasonCode', 'nextStep']) {
@@ -6018,6 +6279,8 @@ export class AUNChannelPlugin {
6018
6279
  ...Object.fromEntries(Object.entries(replyCtx?.metadata ?? {})
6019
6280
  .filter(([key]) => key !== 'operationId')),
6020
6281
  ...(dedupeOperationId ? { operationId: dedupeOperationId } : {}),
6282
+ ...(payload.kind === 'result.text' && payload.metadata?.forkAnchor
6283
+ ? { forkAnchor: payload.metadata.forkAnchor } : {}),
6021
6284
  },
6022
6285
  };
6023
6286
  if (payload.kind === 'result.text' && payload.isFinal)
@@ -6045,7 +6308,7 @@ export class AUNChannelPlugin {
6045
6308
  messages: [{
6046
6309
  messageId: result.messageId,
6047
6310
  partIndex: 0,
6048
- ...(sendCtx.threadId ? { threadId: sendCtx.threadId } : {}),
6311
+ ...(typeof sendCtx.threadId === 'string' ? { threadId: sendCtx.threadId } : {}),
6049
6312
  }],
6050
6313
  };
6051
6314
  }
@@ -6081,14 +6344,13 @@ export class AUNChannelPlugin {
6081
6344
  }
6082
6345
  case 'system.error': {
6083
6346
  const errorPayload = {
6084
- type: 'error',
6347
+ type: payload.recoverable === true ? 'notice' : 'error',
6085
6348
  ...taskBase(),
6086
6349
  subtype: payload.subtype,
6087
6350
  text: payload.text,
6088
- message: payload.text,
6089
- user_message: payload.text,
6090
- recoverable: payload.recoverable === true,
6091
- terminal: payload.recoverable !== true,
6351
+ ...(payload.recoverable === true
6352
+ ? { severity: 'warn' }
6353
+ : { message: payload.text, user_message: payload.text, recoverable: false }),
6092
6354
  };
6093
6355
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6094
6356
  return;
@@ -6101,7 +6363,6 @@ export class AUNChannelPlugin {
6101
6363
  text: payload.text,
6102
6364
  message: payload.text,
6103
6365
  user_message: payload.text,
6104
- terminal: true,
6105
6366
  };
6106
6367
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6107
6368
  return;
@@ -6110,8 +6371,27 @@ export class AUNChannelPlugin {
6110
6371
  const fileCtx = payload.correlationId
6111
6372
  ? { ...(replyCtx ?? {}), metadata: { ...(replyCtx?.metadata ?? {}), correlationId: payload.correlationId } }
6112
6373
  : replyCtx;
6113
- await channel.sendFile(channelId, payload.filePath, fileCtx);
6114
- return;
6374
+ const result = await channel.sendFile(channelId, payload.filePath, fileCtx);
6375
+ if (result.status === 'sent')
6376
+ return sentReceipt(envelope, [result.messageId], fileCtx?.threadId);
6377
+ if (result.status === 'queued') {
6378
+ return {
6379
+ status: 'queued',
6380
+ operationId,
6381
+ messages: [],
6382
+ outboxId: result.outboxId,
6383
+ ...(result.error !== undefined ? { error: result.error } : {}),
6384
+ ...(result.code !== undefined ? { code: result.code } : {}),
6385
+ };
6386
+ }
6387
+ return {
6388
+ status: 'failed',
6389
+ operationId,
6390
+ messages: [],
6391
+ ...(result.outboxId !== undefined ? { outboxId: result.outboxId } : {}),
6392
+ error: result.error,
6393
+ ...(result.code !== undefined ? { code: result.code } : {}),
6394
+ };
6115
6395
  }
6116
6396
  case 'result.image': {
6117
6397
  await channel.sendImage(channelId, payload.data, payload.mimeType, payload.alt, replyCtx);
@@ -6255,13 +6535,13 @@ export class AUNChannelPlugin {
6255
6535
  };
6256
6536
  if (req.kind.kind === 'action') {
6257
6537
  const action = req.kind;
6258
- const aunCard = {
6538
+ const aunCard = withAunAttention({
6259
6539
  type: 'action_card',
6260
6540
  title: action.title,
6261
6541
  actions: action.buttons.map(btn => ({
6262
6542
  label: btn.label, value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
6263
6543
  })),
6264
- };
6544
+ });
6265
6545
  // AUN action_card 以 compose behavior 声明自由文本动作。新版客户端
6266
6546
  // 在卡片内采集并确认;旧版客户端可继续从主编辑器采集。两者都结构化
6267
6547
  // 回传 action_card_reply{behavior:"compose"},Core 不拦截普通消息队列。
@@ -6279,6 +6559,7 @@ export class AUNChannelPlugin {
6279
6559
  const result = await channel.sendContentPayload(channelId, aunCard, {
6280
6560
  contentKind: 'card',
6281
6561
  context: replyCtx,
6562
+ dedupeKey: req.id.startsWith('gir-') ? operationId : undefined,
6282
6563
  logText: action.title ? `[card] ${action.title}` : '[card]',
6283
6564
  postSend: {
6284
6565
  type: 'register_interaction_card',
@@ -6293,7 +6574,7 @@ export class AUNChannelPlugin {
6293
6574
  }
6294
6575
  else if (req.kind.kind === 'command-card') {
6295
6576
  const card = req.kind;
6296
- const aunCard = {
6577
+ const aunCard = withAunAttention({
6297
6578
  type: 'action_card',
6298
6579
  title: card.title,
6299
6580
  actions: card.buttons.map(btn => {
@@ -6306,7 +6587,7 @@ export class AUNChannelPlugin {
6306
6587
  setIfDefined(action, 'disabled', btn.disabled);
6307
6588
  return action;
6308
6589
  }),
6309
- };
6590
+ });
6310
6591
  if (card.body) {
6311
6592
  aunCard.text = card.body;
6312
6593
  setIfDefined(aunCard, 'format', card.bodyFormat);
@@ -6397,6 +6678,14 @@ export class AUNChannelPlugin {
6397
6678
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
6398
6679
  handler(aunOptsToInbound(opts, adapter.channelName, channelType));
6399
6680
  }), async (channelId, text, replyContext) => { await channel.sendMessage(channelId, text, replyContext); }, adapter, channelType);
6681
+ bridge.registerGroupInviteReviewChannel({
6682
+ selfAid: aid,
6683
+ channelName: adapter.channelName,
6684
+ channelKey: adapter.channelKey,
6685
+ agentName: ctx.agentName,
6686
+ channel,
6687
+ adapter,
6688
+ });
6400
6689
  },
6401
6690
  registerHooks(hookCtx) {
6402
6691
  channel.setEventBus(hookCtx.eventBus);