evolcore 0.0.25 → 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 (304) hide show
  1. package/CHANGELOG.md +632 -565
  2. package/LICENSE +21 -21
  3. package/README.md +455 -455
  4. package/bin/codex-managed-acl.ps1 +203 -160
  5. package/bin/codex-managed-hook.mjs +208 -208
  6. package/bin/ec-safe-output.js +327 -327
  7. package/bin/ec.js +40 -29
  8. package/bin/install-codex-managed-hooks.mjs +582 -555
  9. package/dist/agents/baseagent-availability.js +68 -0
  10. package/dist/agents/baseagent.js +96 -46
  11. package/dist/agents/claude-runner.js +172 -33
  12. package/dist/agents/codex/app-server-client.js +76 -31
  13. package/dist/agents/codex/catalog.js +1 -1
  14. package/dist/agents/codex/diagnostics.js +34 -1
  15. package/dist/agents/codex-runner.js +434 -446
  16. package/dist/agents/ecagent-runner.js +174 -34
  17. package/dist/agents/gemini-runner.js +8 -7
  18. package/dist/agents/runner-types.js +75 -19
  19. package/dist/aun/aid/agentmd.js +151 -36
  20. package/dist/aun/aid/client.js +1 -1
  21. package/dist/aun/aid/control-aid.js +180 -1
  22. package/dist/aun/aid/identity.js +4 -4
  23. package/dist/aun/aid/index.js +1 -1
  24. package/dist/aun/aid/store.js +17 -13
  25. package/dist/aun/group/history.js +303 -0
  26. package/dist/aun/group/identity.js +43 -0
  27. package/dist/aun/group/index.js +5 -0
  28. package/dist/aun/{msg/group.js → group/operations.js} +162 -39
  29. package/dist/aun/{menu-client.js → menu/client.js} +85 -6
  30. package/dist/aun/menu/index.js +2 -0
  31. package/dist/aun/menu/transport.js +49 -0
  32. package/dist/aun/msg/index.js +2 -1
  33. package/dist/aun/msg/managed-operation.js +85 -19
  34. package/dist/aun/msg/mention-schema.js +51 -0
  35. package/dist/aun/msg/p2p.js +19 -9
  36. package/dist/aun/msg/upload.js +43 -80
  37. package/dist/aun/outbox.js +29 -7
  38. package/dist/aun/rpc/client.js +1 -0
  39. package/dist/aun/storage/client.js +39 -50
  40. package/dist/channels/aun.js +842 -318
  41. package/dist/channels/daemon.js +10 -6
  42. package/dist/cli/agent-command.js +212 -89
  43. package/dist/cli/agent-create.js +626 -11
  44. package/dist/cli/agent.js +125 -13
  45. package/dist/cli/aun-commands.js +487 -337
  46. package/dist/cli/bench.js +20 -20
  47. package/dist/cli/config.js +55 -33
  48. package/dist/cli/contact.js +72 -15
  49. package/dist/cli/ctl-command.js +24 -24
  50. package/dist/cli/daemon-commands.js +140 -63
  51. package/dist/cli/diag.js +31 -0
  52. package/dist/cli/fs-command.js +70 -70
  53. package/dist/cli/handoff-command.js +46 -14
  54. package/dist/cli/index.js +91 -79
  55. package/dist/cli/init.js +34 -1
  56. package/dist/cli/link-rules.js +21 -21
  57. package/dist/cli/managed-command-guard.js +5 -1
  58. package/dist/cli/menu-client.js +51 -32
  59. package/dist/cli/model.js +3 -3
  60. package/dist/cli/net-check.js +22 -22
  61. package/dist/cli/queue-command.js +31 -31
  62. package/dist/cli/response.js +32 -32
  63. package/dist/cli/restart-monitor.js +103 -42
  64. package/dist/cli/stats.js +51 -51
  65. package/dist/cli/task-command.js +59 -0
  66. package/dist/cli/task-context.js +97 -49
  67. package/dist/cli/trigger-command.js +74 -64
  68. package/dist/config/builtin-roles.js +11 -0
  69. package/dist/config/config-field-policy.js +11 -0
  70. package/dist/config/config-manager.js +32 -4
  71. package/dist/config/config-operation-service.js +160 -0
  72. package/dist/config/contact-book-store.js +1 -1
  73. package/dist/config/contact-book.js +1 -1
  74. package/dist/config/contact-operation-service.js +97 -8
  75. package/dist/config/peer-role-resolver.js +1 -1
  76. package/dist/config/resolved-config-op.js +30 -7
  77. package/dist/config-store.js +1 -1
  78. package/dist/core/agent-application-service.js +7 -1
  79. package/dist/core/audit/event-key.js +17 -2
  80. package/dist/core/audit/log-integrity.js +34 -8
  81. package/dist/core/auth/agent-delegation.js +48 -14
  82. package/dist/core/auth/auth-gateway.js +2 -0
  83. package/dist/core/auth/authorization-audit.js +53 -49
  84. package/dist/core/auth/operation-authorizer.js +90 -7
  85. package/dist/core/auth/operation-catalog.js +145 -4
  86. package/dist/core/auth/remote-access-store.js +155 -10
  87. package/dist/core/bootstrap-messages.js +66 -7
  88. package/dist/core/bootstrap-service.js +111 -20
  89. package/dist/core/command/agent-control.js +88 -7
  90. package/dist/core/command/agent-storage-menu.js +443 -0
  91. package/dist/core/command/cli-intent-parser.js +19 -0
  92. package/dist/core/command/command-handler.js +159 -65
  93. package/dist/core/command/data-root-resolver.js +273 -0
  94. package/dist/core/command/diag-service.js +118 -0
  95. package/dist/core/command/group-menu.js +7 -2
  96. package/dist/core/command/menu-catalog.js +46 -7
  97. package/dist/core/command/menu-handler.js +392 -100
  98. package/dist/core/command/menu-protocol.js +45 -6
  99. package/dist/core/command/menu-token-store.js +41 -8
  100. package/dist/core/command/role-menu.js +2 -2
  101. package/dist/core/command/slash-gate.js +34 -4
  102. package/dist/core/command/slash-handler.js +123 -43
  103. package/dist/core/event-catalog.js +4 -0
  104. package/dist/core/evolagent-registry.js +28 -2
  105. package/dist/core/group/invite-service.js +924 -0
  106. package/dist/core/handoff/runtime.js +205 -27
  107. package/dist/core/handoff/store.js +193 -4
  108. package/dist/core/interaction-router.js +22 -8
  109. package/dist/core/message/im-renderer.js +146 -34
  110. package/dist/core/message/message-bridge.js +163 -20
  111. package/dist/core/message/message-log.js +5 -3
  112. package/dist/core/message/message-queue.js +99 -41
  113. package/dist/core/message/message-utils.js +1 -0
  114. package/dist/core/message/response-engine.js +755 -144
  115. package/dist/core/model/catalog-service.js +79 -49
  116. package/dist/core/model/context-window-suffix.js +33 -0
  117. package/dist/core/model/model-catalog.js +1 -1
  118. package/dist/core/model/model-permission.js +12 -3
  119. package/dist/core/permission/approval-gateway.js +36 -3
  120. package/dist/core/permission/ec-command-parser.js +112 -41
  121. package/dist/core/permission/protected-paths.js +134 -16
  122. package/dist/core/permission/sandbox-runtime.js +108 -17
  123. package/dist/core/permission/tool-error-code.js +15 -0
  124. package/dist/core/permission/tool-policy.js +9 -8
  125. package/dist/core/relation/peer-identity.js +57 -6
  126. package/dist/core/relation/shared-data.js +106 -0
  127. package/dist/core/role/runtime-policy.js +64 -0
  128. package/dist/core/runtime-lock.js +10 -26
  129. package/dist/core/session/runner-task-history.js +170 -0
  130. package/dist/core/session/session-manager.js +34 -6
  131. package/dist/core/session/session-mapper.js +41 -0
  132. package/dist/core/session/session-turn-coordinator.js +37 -7
  133. package/dist/core/session/session-turns.js +48 -7
  134. package/dist/core/task/task-recovery-service.js +176 -0
  135. package/dist/core/task/task-resume-operation.js +32 -0
  136. package/dist/eck/group-rules-sync.js +5 -3
  137. package/dist/eck/kit-renderer.js +1 -0
  138. package/dist/index.js +588 -217
  139. package/dist/ipc.js +193 -15
  140. package/dist/paths.js +17 -4
  141. package/dist/product.js +2 -0
  142. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  143. package/dist/stats/billing.js +41 -15
  144. package/dist/stats/db.js +159 -159
  145. package/dist/stats/eck-vars.js +9 -9
  146. package/dist/stats/price-resolver.js +28 -10
  147. package/dist/stats/query.js +139 -139
  148. package/dist/stats/role-budget.js +6 -6
  149. package/dist/stats/writer.js +51 -51
  150. package/dist/trigger/event-source.js +19 -0
  151. package/dist/trigger/feedback.js +233 -40
  152. package/dist/trigger/parser.js +55 -5
  153. package/dist/trigger/patch.js +9 -0
  154. package/dist/trigger/scheduler.js +30 -4
  155. package/dist/trigger/validation.js +20 -3
  156. package/dist/utils/cross-platform.js +30 -1
  157. package/dist/utils/ecweb-utils.js +1 -0
  158. package/dist/utils/error-dict.json +160 -160
  159. package/dist/utils/error-utils.js +61 -2
  160. package/dist/utils/evolcore-version.js +129 -0
  161. package/dist/utils/logger.js +26 -14
  162. package/dist/utils/macos-autostart.js +22 -22
  163. package/dist/utils/model-prices.jsonl +23 -23
  164. package/dist/utils/npm-ops.js +176 -35
  165. package/dist/utils/tool-summary.js +59 -3
  166. package/dist/utils/welcome.js +196 -23
  167. package/ecagent/LICENSE +21 -21
  168. package/ecagent/NOTICE +11 -11
  169. package/ecagent/README.md +61 -61
  170. package/ecagent/dist/harness/agent-harness.js +3 -0
  171. package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
  172. package/ecagent/dist/harness/compaction/compaction.js +83 -83
  173. package/ecagent/dist/harness/messages.js +7 -7
  174. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  175. package/kits/docs/GUIDE.md +20 -20
  176. package/kits/docs/INDEX.md +60 -66
  177. package/kits/docs/aun/CHEATSHEET.md +19 -19
  178. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
  179. package/kits/docs/channels/aun.md +69 -56
  180. package/kits/docs/channels/feishu.md +56 -56
  181. package/kits/docs/context-assembly.md +379 -367
  182. package/kits/docs/eck_templates/GUIDE.template.md +22 -22
  183. package/kits/docs/eck_templates/INDEX.template.md +28 -28
  184. package/kits/docs/eck_templates/path-registry.template.md +33 -33
  185. package/kits/docs/eck_templates/runtime.template.md +19 -19
  186. package/kits/docs/evolcore/INDEX.md +76 -71
  187. package/kits/docs/evolcore/agent-create.md +202 -165
  188. package/kits/docs/evolcore/agent.md +99 -67
  189. package/kits/docs/evolcore/aid.md +68 -60
  190. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
  191. package/kits/docs/evolcore/config.md +241 -195
  192. package/kits/docs/evolcore/contact.md +75 -68
  193. package/kits/docs/evolcore/ctl.md +46 -46
  194. package/kits/docs/evolcore/event.md +216 -216
  195. package/kits/docs/evolcore/fs-architecture.md +1215 -1215
  196. package/kits/docs/evolcore/fs.md +110 -110
  197. package/kits/docs/evolcore/group-collaboration.md +244 -244
  198. package/kits/docs/evolcore/group-fs.md +17 -17
  199. package/kits/docs/evolcore/group-rules.md +251 -251
  200. package/kits/docs/evolcore/group.md +267 -152
  201. package/kits/docs/evolcore/menu-client.md +58 -34
  202. package/kits/docs/evolcore/model.md +50 -50
  203. package/kits/docs/evolcore/msg.md +169 -165
  204. package/kits/docs/evolcore/response.md +75 -75
  205. package/kits/docs/evolcore/rpc.md +37 -37
  206. package/kits/docs/evolcore/self-summary.md +29 -29
  207. package/kits/docs/evolcore/stats.md +83 -83
  208. package/kits/docs/evolcore/storage.md +53 -50
  209. package/kits/docs/evolcore/task.md +14 -0
  210. package/kits/docs/evolcore/trigger.md +623 -575
  211. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
  212. package/kits/docs/identity/PATH_OPS.md +16 -16
  213. package/kits/docs/identity/ROLE_DETAIL.md +25 -25
  214. package/kits/docs/identity/identity-tools.md +26 -26
  215. package/kits/docs/path-registry.md +62 -62
  216. package/kits/docs/prompt-loading-architecture.md +269 -269
  217. package/kits/docs/venues/aun-group.md +45 -45
  218. package/kits/docs/venues/aun-private.md +10 -10
  219. package/kits/docs/venues/client-desktop.md +10 -10
  220. package/kits/docs/venues/client-mobile.md +10 -10
  221. package/kits/docs/venues/feishu-group.md +13 -13
  222. package/kits/docs/venues/feishu-private.md +9 -9
  223. package/kits/docs/venues/group.md +42 -25
  224. package/kits/docs/venues/private.md +10 -10
  225. package/kits/eck_manifest.auxiliary.json +43 -43
  226. package/kits/eck_manifest.json +217 -190
  227. package/kits/eck_message_manifest.json +74 -74
  228. package/kits/migrations/README-role-config-v4.md +32 -32
  229. package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
  230. package/kits/migrations/migrate-role-config-v4.mjs +614 -614
  231. package/kits/migrations/migrate-role-config-v5.mjs +365 -365
  232. package/kits/migrations/rename-config-file.mjs +99 -99
  233. package/kits/rules/01-overview.md +166 -162
  234. package/kits/rules/02-navigation.md +96 -96
  235. package/kits/rules/03-identity.md +36 -36
  236. package/kits/rules/04-relation.md +76 -75
  237. package/kits/rules/05-venue.md +60 -60
  238. package/kits/rules/06-channel.md +65 -63
  239. package/kits/schemas/_meta.json +53 -53
  240. package/kits/schemas/agent-config.schema.1.json +177 -177
  241. package/kits/schemas/agent-config.schema.10.json +395 -395
  242. package/kits/schemas/agent-config.schema.11.json +421 -421
  243. package/kits/schemas/agent-config.schema.12.json +451 -427
  244. package/kits/schemas/agent-config.schema.2.json +239 -239
  245. package/kits/schemas/agent-config.schema.3.json +119 -119
  246. package/kits/schemas/agent-config.schema.4.json +208 -208
  247. package/kits/schemas/agent-config.schema.5.json +326 -326
  248. package/kits/schemas/agent-config.schema.6.json +322 -322
  249. package/kits/schemas/agent-config.schema.7.json +303 -303
  250. package/kits/schemas/agent-config.schema.8.json +304 -304
  251. package/kits/schemas/agent-config.schema.9.json +364 -364
  252. package/kits/schemas/contact-book.schema.1.json +36 -36
  253. package/kits/schemas/contact-book.schema.2.json +43 -43
  254. package/kits/schemas/contact-book.schema.3.json +67 -67
  255. package/kits/schemas/daemon.schema.1.json +91 -91
  256. package/kits/schemas/daemon.schema.2.json +101 -101
  257. package/kits/schemas/daemon.schema.3.json +123 -123
  258. package/kits/schemas/daemon.schema.4.json +136 -136
  259. package/kits/schemas/daemon.schema.5.json +135 -135
  260. package/kits/schemas/daemon.schema.6.json +131 -131
  261. package/kits/schemas/defaults.schema.1.json +81 -81
  262. package/kits/schemas/defaults.schema.2.json +85 -85
  263. package/kits/schemas/defaults.schema.3.json +73 -73
  264. package/kits/schemas/defaults.schema.4.json +107 -107
  265. package/kits/schemas/defaults.schema.5.json +119 -119
  266. package/kits/schemas/menu-exec-schema-commands.md +208 -208
  267. package/kits/schemas/migrations/README.md +30 -30
  268. package/kits/schemas/relation-config.schema.1.json +158 -158
  269. package/kits/schemas/relation-config.schema.2.json +73 -73
  270. package/kits/schemas/relation-config.schema.3.json +50 -50
  271. package/kits/schemas/relation-config.schema.4.json +47 -47
  272. package/kits/schemas/relation-config.schema.5.json +47 -47
  273. package/kits/schemas/relation-config.schema.6.json +46 -46
  274. package/kits/schemas/relation-config.schema.7.json +59 -59
  275. package/kits/schemas/relation-config.schema.8.json +103 -103
  276. package/kits/schemas/role-config.schema.1.json +215 -201
  277. package/kits/schemas/role-registry.schema.1.json +35 -35
  278. package/kits/schemas/single-session.schema.1.json +31 -31
  279. package/kits/schemas/single-session.schema.2.json +57 -57
  280. package/kits/schemas/single-session.schema.3.json +32 -32
  281. package/kits/templates/bootstrap-welcome.md +17 -17
  282. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
  283. package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
  284. package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
  285. package/kits/templates/message-fragments/inject-default.md +2 -2
  286. package/kits/templates/message-fragments/item.md +2 -2
  287. package/kits/templates/roles/admin.json +92 -92
  288. package/kits/templates/roles/member.json +101 -96
  289. package/kits/templates/roles/owner.json +9 -9
  290. package/kits/templates/roles/visitor.json +90 -85
  291. package/kits/templates/system-fragments/baseagent.md +14 -14
  292. package/kits/templates/system-fragments/bootstrap.md +53 -34
  293. package/kits/templates/system-fragments/channel.md +28 -64
  294. package/kits/templates/system-fragments/commands.md +36 -28
  295. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  296. package/kits/templates/system-fragments/identity.md +13 -13
  297. package/kits/templates/system-fragments/relation.md +19 -19
  298. package/kits/templates/system-fragments/reply.md +56 -0
  299. package/kits/templates/system-fragments/session.md +59 -59
  300. package/kits/templates/system-fragments/venue.md +31 -31
  301. package/package.json +104 -98
  302. package/dist/aun/group-identity.js +0 -9
  303. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  304. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -1,9 +1,11 @@
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';
5
6
  import path from 'path';
6
7
  import os from 'os';
8
+ import { fileURLToPath, pathToFileURL } from 'url';
7
9
  import { logger, localTimestamp } from '../utils/logger.js';
8
10
  import { LogWriter } from '../utils/log-writer.js';
9
11
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
@@ -14,6 +16,7 @@ import { appendAidEvent } from '../utils/instance-registry.js';
14
16
  import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, findMessageLogOperationMessageId, hasMessageLogOperation } from '../core/message/message-log.js';
15
17
  import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
16
18
  import { chatDirPath } from '../core/session/session-fs-store.js';
19
+ import { readTopicLable } from '../core/session/session-mapper.js';
17
20
  import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
18
21
  import { appendAidLifecycle } from '../aun/aid/identity.js';
19
22
  import { enableFullGroupPullPagination, enableReliableMessageDelivery, getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
@@ -25,18 +28,20 @@ import { isManagementRole } from '../config/builtin-roles.js';
25
28
  import { normalizeMentionMode } from '../config/mention-mode.js';
26
29
  import { getProcessStartTime } from '../utils/process-introspect.js';
27
30
  import * as outbox from '../aun/outbox.js';
28
- import { guessMime, formatSize } from '../utils/media-cache.js';
31
+ import { formatSize } from '../utils/media-cache.js';
29
32
  import { formatPeerKey, PeerIdentityCache } from '../core/relation/peer-identity.js';
30
33
  import { isHClassPath } from '../core/permission/protected-paths.js';
31
34
  import { consumeAunCausation, registerAunCausation } from '../core/causation/aun-association.js';
32
35
  import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
33
36
  import { recordCausationSpan } from '../core/causation/audit.js';
34
- 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';
35
39
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
40
+ import { ManagedMenuClient, menuSenderDeviceId, menuTransportParams, } from '../aun/menu/index.js';
36
41
  import { toolInputForDisplay } from '../utils/tool-summary.js';
37
- 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';
38
43
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
39
- import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
44
+ import { aunOutboundAttention, normalizeAunMentionEntries, parseAunAttention, withAunAttention, } from '../aun/msg/mention-schema.js';
40
45
  import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
41
46
  import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
42
47
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
@@ -49,9 +54,148 @@ const OBSERVER_GROUP_MEMBERS_TTL_MS = 60_000;
49
54
  const OBSERVER_GROUP_MEMBERS_PAGE_SIZE = 200;
50
55
  const OBSERVER_GROUP_MEMBERS_MAX_PAGES = 100;
51
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
+ }
52
151
  export function formatAunSendLogText(text) {
53
152
  return text.replace(/\s+/gu, ' ').trim().slice(0, AUN_SEND_LOG_TEXT_MAX_LENGTH);
54
153
  }
154
+ /**
155
+ * Echo trace 行的墙钟前缀。
156
+ *
157
+ * 格式必须保持 `HH:MM:SS.mmm` 且毫秒后紧跟空格——Evol App 的
158
+ * `evol/lib/utils/echo_utils.dart` 用 `^(\d{2}):(\d{2}):(\d{2})\.(\d{3})\s` 匹配这一段,
159
+ * 在毫秒和空格之间插任何东西(比如时区偏移)都会让旧版 App 静默失配、不再显示 RTT。
160
+ * 时区信息改由行尾的 {@link echoTraceOriginLabel} 提供,前缀一字不动,两端发版顺序因此解耦。
161
+ */
162
+ export function echoTraceClock(now = Date.now()) {
163
+ const d = new Date(now);
164
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
165
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
166
+ }
167
+ /**
168
+ * Echo trace 行尾的权威时间标记:`epoch=<ms> tz=<±HH:MM>`。
169
+ *
170
+ * 前缀的墙钟没有日期也没有时区,消费方只能按自己的本地时区解析,两端 TZ 不一致时算出的
171
+ * RTT 会整小时偏移(UTC 对 Asia/Shanghai 就是 +28800000ms),跨午夜还会得到负值。
172
+ * `epoch` 是绝对值,消费方应当用它相减算延迟;`tz` 仅供人工核对墙钟,不参与计算。
173
+ */
174
+ export function echoTraceOriginLabel(now = Date.now()) {
175
+ const d = new Date(now);
176
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
177
+ const offsetMinutes = -d.getTimezoneOffset();
178
+ const sign = offsetMinutes >= 0 ? '+' : '-';
179
+ const absOffset = Math.abs(offsetMinutes);
180
+ return ` epoch=${now} tz=${sign}${pad(Math.floor(absOffset / 60))}:${pad(absOffset % 60)}`;
181
+ }
182
+ /**
183
+ * 入站腿延迟(对端/网关发出 → 本进程收到)。
184
+ *
185
+ * `created_at` 由对端或网关打戳,因此结果含对端时钟漂移,只能当估算值——标签用
186
+ * `inbound_est` 而非 `inbound` 就是为了让消费方知道这点。负值说明对端时钟快于本机,
187
+ * 照实输出而不夹到 0:那本身就是有用的时钟偏移信号。
188
+ */
189
+ export function echoInboundLegLabel(createdAt, receivedAt) {
190
+ if (typeof createdAt !== 'number' || !Number.isFinite(createdAt) || createdAt <= 0)
191
+ return '';
192
+ // 网关历史上有按秒打戳的实现,按量级归一到毫秒。
193
+ const createdMs = createdAt < 1e12 ? createdAt * 1000 : createdAt;
194
+ const delta = receivedAt - createdMs;
195
+ if (!Number.isFinite(delta))
196
+ return '';
197
+ return ` inbound_est=${Math.round(delta)}ms`;
198
+ }
55
199
  function attachmentDownloadHost(url) {
56
200
  try {
57
201
  return new URL(url).host || '<unknown>';
@@ -201,7 +345,7 @@ export function buildAunFilePayload(params) {
201
345
  payload.name = params.filename;
202
346
  payload.content_type = params.contentType;
203
347
  }
204
- return payload;
348
+ return withAunAttention(payload, params.context?.attention);
205
349
  }
206
350
  const AUN_PERMANENT_ERROR_CODES = new Set([
207
351
  400, 401, 403, 404,
@@ -431,6 +575,8 @@ export function aunOptsToInbound(opts, channel, channelType) {
431
575
  content: opts.content,
432
576
  chatType: opts.chatType || 'private',
433
577
  peerId: opts.peerId || '',
578
+ peerDeviceId: opts.peerDeviceId,
579
+ peerSlotId: opts.peerSlotId,
434
580
  peerName: opts.peerName,
435
581
  peerType: opts.peerType,
436
582
  sameDevice: opts.sameDevice,
@@ -473,22 +619,8 @@ function verifiedProtectedHeaders(msg, env) {
473
619
  }
474
620
  return undefined;
475
621
  }
476
- function redactMenuTokensForTrace(value, seen = new WeakSet()) {
477
- if (!value || typeof value !== 'object')
478
- return value;
479
- if (seen.has(value))
480
- return '[Circular]';
481
- seen.add(value);
482
- if (Array.isArray(value))
483
- return value.map(item => redactMenuTokensForTrace(item, seen));
484
- const redacted = {};
485
- for (const [key, item] of Object.entries(value)) {
486
- const normalizedKey = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toLowerCase();
487
- redacted[key] = /^(?:menu|ecweb)_token$/.test(normalizedKey)
488
- ? '[REDACTED]'
489
- : redactMenuTokensForTrace(item, seen);
490
- }
491
- return redacted;
622
+ export function redactMenuTokensForTrace(value) {
623
+ return sanitizeMenuLogValue(value);
492
624
  }
493
625
  function payloadTypeForObserver(value) {
494
626
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -502,9 +634,26 @@ function payloadTypeForObserver(value) {
502
634
  const type = payload.type;
503
635
  return typeof type === 'string' ? type : undefined;
504
636
  }
637
+ function errorCodeForLog(error) {
638
+ if (!error || typeof error !== 'object')
639
+ return '';
640
+ const value = error;
641
+ return typeof value.code === 'string' || typeof value.code === 'number' ? String(value.code) : '';
642
+ }
505
643
  export class AUNChannel {
506
644
  config;
645
+ managedMenuClient = new ManagedMenuClient(params => {
646
+ if (!this.connected || !this.client)
647
+ return Promise.reject(new Error('AUN channel is not connected'));
648
+ return this.callAndTrace('message.send', params);
649
+ });
650
+ requestRemoteMenu(options) {
651
+ if (options.fromAid !== (this._aid || this.config.aid))
652
+ throw new Error('Menu sender does not match channel identity');
653
+ return this.managedMenuClient.request(options);
654
+ }
507
655
  client = null;
656
+ localSenderInstallation;
508
657
  store = null;
509
658
  /** 实际连接的网关 URL(来自 authenticate() 返回值 / connection.state 事件),替代旧 (client as any)._gatewayUrl。 */
510
659
  gatewayUrl = '';
@@ -515,6 +664,10 @@ export class AUNChannel {
515
664
  traceWriter = null;
516
665
  eventBus = null;
517
666
  queuedHandler = null;
667
+ groupInviteHandler;
668
+ groupInviteConnectedHandler;
669
+ groupInviteDeliveryGate;
670
+ groupInviteDelivered;
518
671
  pendingEchoMessages = new Map();
519
672
  isEchoSending = false;
520
673
  agentDir;
@@ -562,46 +715,89 @@ export class AUNChannel {
562
715
  * 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + daemon.log 失败日志)。
563
716
  * 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
564
717
  */
565
- callClient(method, params) {
566
- return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)));
718
+ callClient(method, params, lane = 'normal', onStart) {
719
+ params = menuTransportParams(method, params);
720
+ return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)), lane, onStart);
567
721
  }
568
- withOutboundSendGate(method, run) {
569
- if (!AUNChannel.OUTBOUND_SEND_METHODS.has(method))
722
+ withOutboundSendGate(method, run, lane = 'normal', onStart) {
723
+ if (!AUNChannel.OUTBOUND_SEND_METHODS.has(method)) {
724
+ onStart?.();
570
725
  return run();
726
+ }
727
+ const queue = lane === 'observer' ? this.observerSendQueue : this.outboundSendQueue;
728
+ if (lane === 'observer' && queue.length >= AUNChannel.OBSERVER_SEND_QUEUE_MAX) {
729
+ this.noteObserverQueueDrop();
730
+ const error = Object.assign(new Error('observer send queue is full'), {
731
+ code: 'AUN_OBSERVER_QUEUE_FULL',
732
+ });
733
+ return Promise.reject(error);
734
+ }
571
735
  return new Promise((resolve, reject) => {
572
- this.outboundSendQueue.push({
736
+ queue.push({
573
737
  run: run,
574
738
  resolve: resolve,
575
739
  reject,
740
+ onStart,
576
741
  });
577
742
  this.pumpOutboundSendGate();
578
743
  });
579
744
  }
580
745
  pumpOutboundSendGate() {
581
- while (this.outboundSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY
582
- && this.outboundSendQueue.length > 0) {
583
- const job = this.outboundSendQueue.shift();
746
+ while (this.outboundSendActive + this.observerSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY && this.outboundSendQueue.length > 0) {
747
+ this.startOutboundSendJob(this.outboundSendQueue.shift(), 'normal');
748
+ }
749
+ while (this.outboundSendActive + this.observerSendActive < AUNChannel.OUTBOUND_SEND_CONCURRENCY
750
+ && this.observerSendActive < AUNChannel.OBSERVER_SEND_CONCURRENCY
751
+ && this.outboundSendQueue.length === 0
752
+ && this.observerSendQueue.length > 0) {
753
+ this.startOutboundSendJob(this.observerSendQueue.shift(), 'observer');
754
+ }
755
+ }
756
+ clearQueuedObserverSends(reason) {
757
+ if (this.observerSendQueue.length === 0)
758
+ return;
759
+ const error = Object.assign(new Error(`observer send discarded while ${reason}`), {
760
+ code: 'AUN_OBSERVER_QUEUE_DROPPED',
761
+ });
762
+ const queued = this.observerSendQueue.splice(0);
763
+ for (const job of queued)
764
+ job.reject(error);
765
+ this.noteObserverQueueDrop(queued.length);
766
+ }
767
+ startOutboundSendJob(job, lane) {
768
+ if (lane === 'observer')
769
+ this.observerSendActive++;
770
+ else
584
771
  this.outboundSendActive++;
585
- Promise.resolve()
586
- .then(job.run)
587
- .then(job.resolve, job.reject)
588
- .finally(() => {
772
+ Promise.resolve()
773
+ .then(() => {
774
+ job.onStart?.();
775
+ return job.run();
776
+ })
777
+ .then(job.resolve, job.reject)
778
+ .finally(() => {
779
+ if (lane === 'observer')
780
+ this.observerSendActive--;
781
+ else
589
782
  this.outboundSendActive--;
590
- this.pumpOutboundSendGate();
591
- });
592
- }
783
+ this.pumpOutboundSendGate();
784
+ });
593
785
  }
594
786
  async callAndTrace(method, params, opts) {
595
787
  this.trace('OUT', method, params);
596
- // RPC 往返计时:区分「网关慢」与「本地队列堵塞」。message.send/group.send 的
597
- // duration 若普遍逼近 SDK 的 rpc 超时(默认 10s),说明发送管线已饱和或链路抖动,
598
- // 是 /evolhelp 等命令「回复慢/无回复」的直接可观测信号。
788
+ // 计时同时覆盖排队和 SDK 往返;callStartedAt 让日志能区分「本地队列堵塞」
789
+ // 与「网关慢」。两者对 /evolhelp 等命令的回复延迟都有直接影响。
599
790
  const rpcStart = Date.now();
600
791
  // SLOW_RPC_WARN_MS 需明显低于 SDK 默认 10s 超时,以便在真正超时前提前告警。
601
792
  const SLOW_RPC_WARN_MS = 3000;
793
+ let callStartedAt = rpcStart;
602
794
  try {
603
- const result = await this.callClient(method, params);
795
+ const result = await this.callClient(method, params, opts?.lane ?? 'normal', () => {
796
+ callStartedAt = Date.now();
797
+ });
604
798
  const durationMs = Date.now() - rpcStart;
799
+ const rpcDurationMs = Date.now() - callStartedAt;
800
+ const queueWaitMs = Math.max(0, callStartedAt - rpcStart);
605
801
  if (!opts?.silentOk) {
606
802
  const r = result;
607
803
  const snap = r && typeof r === 'object'
@@ -609,25 +805,66 @@ export class AUNChannel {
609
805
  : { duration_ms: durationMs };
610
806
  this.trace('OUT', `${method}.ok`, snap);
611
807
  }
612
- if (durationMs >= SLOW_RPC_WARN_MS) {
613
- logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms (发送管线可能饱和或链路抖动)`);
614
- }
808
+ if (durationMs >= SLOW_RPC_WARN_MS)
809
+ this.logRpcWarning('slow', method, params, durationMs, queueWaitMs, rpcDurationMs);
615
810
  return result;
616
811
  }
617
812
  catch (e) {
618
813
  const durationMs = Date.now() - rpcStart;
814
+ const queueWaitMs = Math.max(0, callStartedAt - rpcStart);
815
+ const rpcDurationMs = Math.max(0, Date.now() - callStartedAt);
619
816
  this.trace('OUT', `${method}.error`, {
620
817
  error: e?.message ?? String(e),
621
818
  code: e?.code,
622
819
  name: e?.name,
623
820
  duration_ms: durationMs,
624
821
  });
625
- if (!opts?.silentError) {
626
- logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
822
+ if (!opts?.silentError && e?.code !== 'AUN_OBSERVER_QUEUE_FULL' && e?.code !== 'AUN_OBSERVER_QUEUE_DROPPED') {
823
+ this.logRpcWarning('failed', method, params, durationMs, queueWaitMs, rpcDurationMs, e);
627
824
  }
628
825
  throw e;
629
826
  }
630
827
  }
828
+ logRpcWarning(kind, method, params, durationMs, queueWaitMs, rpcDurationMs, error) {
829
+ const payloadType = payloadTypeForObserver(params) ?? 'unknown';
830
+ const code = kind === 'failed' ? errorCodeForLog(error) : '';
831
+ const key = `${kind}|${method}|${payloadType}`;
832
+ const now = Date.now();
833
+ const state = this.rpcWarningStates.get(key) ?? { lastWarnAt: 0, suppressed: 0, maxDurationMs: 0 };
834
+ state.maxDurationMs = Math.max(state.maxDurationMs, durationMs);
835
+ if (state.lastWarnAt > 0 && now - state.lastWarnAt < AUNChannel.RPC_WARN_INTERVAL_MS) {
836
+ state.suppressed++;
837
+ this.rpcWarningStates.set(key, state);
838
+ return;
839
+ }
840
+ const suppressed = state.suppressed;
841
+ const maxDurationMs = state.maxDurationMs;
842
+ state.lastWarnAt = now;
843
+ state.suppressed = 0;
844
+ state.maxDurationMs = 0;
845
+ this.rpcWarningStates.set(key, state);
846
+ const summary = suppressed > 0 ? ` suppressed=${suppressed} max_duration=${maxDurationMs}ms` : '';
847
+ if (kind === 'failed') {
848
+ const name = error instanceof Error ? error.name : '';
849
+ const message = error instanceof Error ? error.message : String(error ?? '');
850
+ const errorLabel = `${name}${code ? `(${code})` : ''} ${message}`.trim();
851
+ logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${errorLabel} queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${summary}`);
852
+ return;
853
+ }
854
+ logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${summary} (发送管线可能饱和或链路抖动)`);
855
+ }
856
+ observerQueueDropCount = 0;
857
+ observerQueueDropLastWarnAt = 0;
858
+ noteObserverQueueDrop(count = 1) {
859
+ this.observerQueueDropCount += count;
860
+ const now = Date.now();
861
+ if (now - this.observerQueueDropLastWarnAt < AUNChannel.RPC_WARN_INTERVAL_MS)
862
+ return;
863
+ const dropped = this.observerQueueDropCount;
864
+ this.observerQueueDropCount = 0;
865
+ this.observerQueueDropLastWarnAt = now;
866
+ logger.warn(`${this.logPrefix()} observer send queue full: dropped=${dropped} (observer telemetry is being sampled while the gateway is unavailable)`);
867
+ }
631
868
  /** Resolve the authenticated transport route for an ordinary outbound send.
632
869
  * The channel id is an address only; it is intentionally never used to infer
633
870
  * whether the address is a group.
@@ -689,11 +926,17 @@ export class AUNChannel {
689
926
  return;
690
927
  }
691
928
  const context = { delivery };
692
- if (threadId)
929
+ if (typeof threadId === 'string')
693
930
  context.threadId = threadId;
694
931
  if (operatorId && delivery.chatType === 'group')
695
932
  context.peerId = operatorId;
696
- void this.sendMessage(channelId, text, context).catch((err) => {
933
+ const payload = {
934
+ type: 'notice',
935
+ ...this.buildTaskPayloadBase({}, context),
936
+ text,
937
+ severity: 'info',
938
+ };
939
+ void this.sendReliableStructured(channelId, payload, context, text).catch((err) => {
697
940
  logger.error(`${this.logPrefix()} Failed to send card action error:`, err);
698
941
  });
699
942
  }
@@ -713,6 +956,12 @@ export class AUNChannel {
713
956
  : typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
714
957
  const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
715
958
  const cardDelivery = cardInfo?.delivery ?? delivery;
959
+ const savedTopic = this.ownedCardTopics.get(cardMsgId);
960
+ // Keep the topic after the actionable mapping expires, including for
961
+ // older clients that omit thread_id in their callbacks.
962
+ const threadId = cardInfo?.replyContext?.threadId
963
+ ?? (savedTopic?.channelId === channelId ? savedTopic?.threadId : undefined)
964
+ ?? (typeof obj.thread_id === 'string' ? obj.thread_id : undefined);
716
965
  if (cardInfo) {
717
966
  // compose 按钮由新版客户端在卡片内采集输入;旧版客户端可继续从主编辑器
718
967
  // 采集。两者最终都回传 action_card_reply{behavior:"compose", text}。
@@ -728,10 +977,6 @@ export class AUNChannel {
728
977
  const actionValue = isComposeReply ? '_custom_input'
729
978
  : typeof obj.value === 'string' ? obj.value
730
979
  : typeof obj.action_value === 'string' ? obj.action_value : text;
731
- // Prefer the trusted context persisted with the card. A callback's
732
- // thread_id is client-controlled and may be absent or stale.
733
- const threadId = cardInfo?.replyContext?.threadId
734
- ?? (typeof obj.thread_id === 'string' ? obj.thread_id : undefined);
735
980
  // 卡片点击者身份:只信认证信封(senderAid 参数,由调用方从 msg.from / msg.sender_aid 提取)。
736
981
  // payload 自报字段(from / sender_aid / user_id)不可信,可被客户端伪造,不读取。
737
982
  // 两类卡片共用:CommandCard → 伪入站消息的 peerId,ActionInteraction → operatorId。
@@ -774,9 +1019,11 @@ export class AUNChannel {
774
1019
  peerName: typeof obj.label === 'string' ? obj.label : typeof obj.action_label === 'string' ? obj.action_label : undefined,
775
1020
  messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
776
1021
  threadId,
777
- replyContext: cardInfo?.replyContext ?? ((threadId || cardDelivery)
778
- ? { ...(threadId ? { threadId } : {}), ...(cardDelivery ? { delivery: cardDelivery } : {}) }
779
- : undefined),
1022
+ replyContext: {
1023
+ ...(cardInfo.replyContext ?? {}),
1024
+ ...(typeof threadId === 'string' ? { threadId } : {}),
1025
+ ...(cardDelivery ? { delivery: cardDelivery } : {}),
1026
+ },
780
1027
  source: 'card-trigger',
781
1028
  })).catch((err) => {
782
1029
  logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
@@ -887,9 +1134,9 @@ export class AUNChannel {
887
1134
  }
888
1135
  }
889
1136
  else if (this.ownedCardMsgIds.has(cardMsgId)) {
890
- // 本 agent 发出的卡片,但 entry 已过期(24h TTL)
1137
+ // 本 agent 发出的卡片,但 entry 已过期或被清理。
891
1138
  logger.debug(`${this.logPrefix()} action_card_reply expired: cardMsgId=${cardMsgId}`);
892
- this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', undefined, undefined, delivery);
1139
+ this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', senderAid, threadId, delivery);
893
1140
  }
894
1141
  else {
895
1142
  // 非本 agent 发出的卡片(broadcast 模式下其他 agent 的卡)→ 静默忽略
@@ -1136,14 +1383,14 @@ export class AUNChannel {
1136
1383
  return sanitized;
1137
1384
  return undefined;
1138
1385
  }
1139
- buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId) {
1386
+ buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, lable) {
1140
1387
  if (!groupId)
1141
1388
  throw new Error('buildGroupReplyContext requires groupId');
1142
1389
  const replyContext = {
1143
1390
  metadata: { encrypted, chatmode },
1144
1391
  delivery: { chatType: 'group', groupId },
1145
1392
  };
1146
- if (threadId)
1393
+ if (typeof threadId === 'string')
1147
1394
  replyContext.threadId = threadId;
1148
1395
  replyContext.peerId = senderAid;
1149
1396
  if (messageId)
@@ -1154,20 +1401,24 @@ export class AUNChannel {
1154
1401
  replyContext.metadata = {};
1155
1402
  replyContext.metadata.topicName = topicName;
1156
1403
  }
1404
+ if (lable && threadId)
1405
+ replyContext.metadata.lable = lable;
1157
1406
  return replyContext;
1158
1407
  }
1159
- buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId) {
1408
+ buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId, lable) {
1160
1409
  const replyContext = {
1161
1410
  metadata: { encrypted, chatmode, refMessageId },
1162
1411
  delivery: { chatType: 'private' },
1163
1412
  peerId: senderAid,
1164
1413
  };
1165
- if (threadId)
1414
+ if (typeof threadId === 'string')
1166
1415
  replyContext.threadId = threadId;
1167
1416
  if (messageId)
1168
1417
  replyContext.replyToMessageId = messageId;
1169
1418
  if (topicName && threadId)
1170
1419
  replyContext.metadata.topicName = topicName;
1420
+ if (lable && threadId)
1421
+ replyContext.metadata.lable = lable;
1171
1422
  return replyContext;
1172
1423
  }
1173
1424
  claimInboundMessage(kind, messageId, seq) {
@@ -1284,6 +1535,7 @@ export class AUNChannel {
1284
1535
  invalidInboundEnvelopeLastLogAt = 0;
1285
1536
  groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
1286
1537
  peerInfoCache = new Map();
1538
+ peerInfoRevision = 0;
1287
1539
  messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
1288
1540
  sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
1289
1541
  peerE2ee = new Map();
@@ -1295,6 +1547,7 @@ export class AUNChannel {
1295
1547
  cardMessageIdMap = new Map();
1296
1548
  /** 本 agent 曾发出过的卡片 msgId(只增不删,用于区分"过期失效"vs"他人发的卡") */
1297
1549
  ownedCardMsgIds = new Set();
1550
+ ownedCardTopics = new Map();
1298
1551
  /** requestId → 已投递的卡片 message_id 集合(撤权/超时时撤回) */
1299
1552
  interactionCardMessageIds = new Map();
1300
1553
  /** 撤权可能先于 outbox 卡片投递完成;迟到卡片注册后立即撤回。 */
@@ -1304,7 +1557,7 @@ export class AUNChannel {
1304
1557
  groupSlashRequireMentionsResolver;
1305
1558
  static PROACTIVE_ALLOW_TYPES = new Set([
1306
1559
  'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
1307
- 'merge', 'link', 'location', 'personal_card',
1560
+ 'merge', 'link', 'location', 'personal_card', 'contact.request.response',
1308
1561
  ]);
1309
1562
  /** 观察者插话请求类型(owner → agent.AID)。详见 docs/observer-insert-design.md。 */
1310
1563
  static INJECT_REQUEST_TYPE = 'observer.inject';
@@ -1344,7 +1597,7 @@ export class AUNChannel {
1344
1597
  aidState;
1345
1598
  aidStatsCollector;
1346
1599
  outboxInFlight = new Set();
1347
- /** Shared gateway-facing send limit for replies, activity, observer and thought messages. */
1600
+ /** Shared total gateway-facing send capacity; normal traffic has priority over observer telemetry. */
1348
1601
  static OUTBOUND_SEND_METHODS = new Set([
1349
1602
  'message.send',
1350
1603
  'group.send',
@@ -1352,8 +1605,14 @@ export class AUNChannel {
1352
1605
  'group.thought.put',
1353
1606
  ]);
1354
1607
  static OUTBOUND_SEND_CONCURRENCY = 4;
1608
+ static OBSERVER_SEND_CONCURRENCY = 1;
1609
+ static OBSERVER_SEND_QUEUE_MAX = 256;
1610
+ static RPC_WARN_INTERVAL_MS = 60_000;
1355
1611
  outboundSendActive = 0;
1356
1612
  outboundSendQueue = [];
1613
+ observerSendActive = 0;
1614
+ observerSendQueue = [];
1615
+ rpcWarningStates = new Map();
1357
1616
  constructor(config) {
1358
1617
  this.config = config;
1359
1618
  this.agentDir = agentDirPath(config.aid);
@@ -1391,6 +1650,44 @@ export class AUNChannel {
1391
1650
  getAid() {
1392
1651
  return this._aid ?? this.config.aid;
1393
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
+ }
1394
1691
  /** 是否已连接(client 存在且认证完成)。 */
1395
1692
  isConnected() {
1396
1693
  return this.connected && this.client !== null;
@@ -1425,6 +1722,27 @@ export class AUNChannel {
1425
1722
  this.initInProgress = false;
1426
1723
  }
1427
1724
  }
1725
+ /** Optional local compatibility code is never required by a clean checkout or npm install. */
1726
+ async loadLocalSenderInstallation(client) {
1727
+ this.localSenderInstallation = undefined;
1728
+ const currentFile = fileURLToPath(import.meta.url);
1729
+ const modulePath = path.join(path.dirname(currentFile), '..', 'aun', `sender-installation${path.extname(currentFile)}`);
1730
+ if (!fs.existsSync(modulePath))
1731
+ return;
1732
+ try {
1733
+ // Use a runtime URL so TypeScript can build when the local file is absent.
1734
+ const extension = await import(pathToFileURL(modulePath).href);
1735
+ if (typeof extension.enableAunSenderInstallation !== 'function'
1736
+ || typeof extension.aunSenderInstallation !== 'function') {
1737
+ throw new Error('Invalid local sender installation module');
1738
+ }
1739
+ extension.enableAunSenderInstallation(client);
1740
+ this.localSenderInstallation = extension;
1741
+ }
1742
+ catch (error) {
1743
+ logger.warn(`${this.logPrefix()} Optional local sender installation disabled: ${error instanceof Error ? error.message : String(error)}`);
1744
+ }
1745
+ }
1428
1746
  async _initClientInner() {
1429
1747
  this.observerGroupMembers.clear();
1430
1748
  for (const groupId of new Set([
@@ -1454,6 +1772,7 @@ export class AUNChannel {
1454
1772
  this.store = null;
1455
1773
  }
1456
1774
  this.connected = false;
1775
+ this.clearQueuedObserverSends('client reinitialization');
1457
1776
  this.lastConnectionStateSignature = '';
1458
1777
  const aunPath = this.config.keystorePath || resolveRoot();
1459
1778
  const aidName = this.config.aid;
@@ -1473,11 +1792,39 @@ export class AUNChannel {
1473
1792
  const client = await loadClient(store, aidName);
1474
1793
  enableFullGroupPullPagination(client);
1475
1794
  enableReliableMessageDelivery(client);
1795
+ await this.loadLocalSenderInstallation(client);
1476
1796
  this.client = client;
1477
1797
  // A reconnect replaces the SDK client. Late events from the retired
1478
1798
  // instance must not mutate the new instance's health or message state.
1479
1799
  const isCurrentClient = () => this.client === client;
1480
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
+ });
1481
1828
  client.on('message.received', async (data) => {
1482
1829
  if (!isCurrentClient())
1483
1830
  return;
@@ -1523,6 +1870,9 @@ export class AUNChannel {
1523
1870
  const inviteIds = Array.isArray(record.invite_ids) ? record.invite_ids : [];
1524
1871
  this.trace('IN', 'group.invite_received', data);
1525
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
+ });
1526
1876
  });
1527
1877
  }
1528
1878
  // 0.5.10/0.5.11 local lifecycle events summarize SDK delivery and Pull
@@ -1562,18 +1912,7 @@ export class AUNChannel {
1562
1912
  client.on('message.recalled', (data) => {
1563
1913
  if (!isCurrentClient())
1564
1914
  return;
1565
- this.trace('IN', 'message.recalled', data);
1566
- if (data && typeof data === 'object') {
1567
- const ids = data.message_ids;
1568
- if (Array.isArray(ids)) {
1569
- for (const id of ids) {
1570
- if (typeof id === 'string') {
1571
- logger.info(`${this.logPrefix()} Message recalled: ${id}`);
1572
- this.recallHandler?.(id, this._aid || this.config.aid);
1573
- }
1574
- }
1575
- }
1576
- }
1915
+ this.handleMessageRecalled(data, client, isCurrentClient);
1577
1916
  });
1578
1917
  client.on('message.undecryptable', (data) => {
1579
1918
  if (!isCurrentClient())
@@ -1659,13 +1998,37 @@ export class AUNChannel {
1659
1998
  this._aid = this.client.aid ?? undefined;
1660
1999
  const deviceId = this.client._device_id ?? '';
1661
2000
  this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
1662
- // pureIdentity(控制 AID):无 agent.md,跳过自身 agent.md 拉取,省一次 404
1663
- 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
+ }
1664
2022
  if (this._selfName && this.aidStatsCollector)
1665
2023
  this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
1666
2024
  this.connected = true;
1667
2025
  this.connectedAt = Date.now();
1668
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
+ }
1669
2032
  // AUN seq_tracker is transport-scoped, while this channel's durable
1670
2033
  // dedup log is application-scoped. A diagnostic consumer can advance
1671
2034
  // the former without the application having seen those messages. On a
@@ -1723,7 +2086,7 @@ export class AUNChannel {
1723
2086
  if (hasMessageLogOperation(chatDir, operationId)
1724
2087
  || hasMessageLogOperation(chatDir, postBootstrapWelcomeOperationId(aidName)))
1725
2088
  continue;
1726
- const prepared = outbox.findByDedupeKey(aidName, operationId);
2089
+ const prepared = outbox.findByDedupeKey(aidName, operationId, { includeTerminal: true });
1727
2090
  if (prepared && prepared.channelId === owner && isDeliveryTarget(prepared.delivery) && sameDeliveryTarget(prepared.delivery, delivery))
1728
2091
  continue;
1729
2092
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
@@ -1759,7 +2122,7 @@ export class AUNChannel {
1759
2122
  }
1760
2123
  async sendPostBootstrapWelcome() {
1761
2124
  if (!await this.preparePostBootstrapWelcome())
1762
- return false;
2125
+ throw new Error('Failed to persist post-bootstrap welcome');
1763
2126
  return this.reconcilePostBootstrapWelcome();
1764
2127
  }
1765
2128
  async hasPendingPostBootstrapWelcome() {
@@ -1777,6 +2140,8 @@ export class AUNChannel {
1777
2140
  const pull = client?._pullV2;
1778
2141
  if (typeof repair !== 'function' || typeof pull !== 'function')
1779
2142
  return;
2143
+ if (this.inboundP2PCursorRepairTimer)
2144
+ clearTimeout(this.inboundP2PCursorRepairTimer);
1780
2145
  const ns = `p2p:${this.config.aid}`;
1781
2146
  let appSeq = 0;
1782
2147
  for (const [key, value] of this.inboundSeenMessages) {
@@ -1789,7 +2154,8 @@ export class AUNChannel {
1789
2154
  }
1790
2155
  if (appSeq <= 0)
1791
2156
  return;
1792
- setTimeout(() => {
2157
+ this.inboundP2PCursorRepairTimer = setTimeout(() => {
2158
+ this.inboundP2PCursorRepairTimer = null;
1793
2159
  if (!isCurrentClient() || !this.connected)
1794
2160
  return;
1795
2161
  const transportSeq = Number(tracker.getContiguousSeq?.(ns) ?? 0);
@@ -1807,6 +2173,24 @@ export class AUNChannel {
1807
2173
  }
1808
2174
  }, 250);
1809
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
+ }
1810
2194
  pendingPostBootstrapWelcomeOwners() {
1811
2195
  return pendingPostBootstrapWelcomeOwners(this.config.aid);
1812
2196
  }
@@ -1822,25 +2206,35 @@ export class AUNChannel {
1822
2206
  const configuredAid = this.config.aid;
1823
2207
  const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
1824
2208
  const entries = findPostBootstrapWelcomeOutbox(aid);
1825
- if (entries.length === 0)
1826
- return false;
1827
2209
  const agentConfig = loadAgent(aid);
2210
+ const owners = agentConfig?.owners ?? [];
2211
+ if (entries.length === 0)
2212
+ return postBootstrapWelcomeStatus(aid, owners);
1828
2213
  if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
1829
2214
  logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
1830
- return true;
2215
+ return postBootstrapWelcomeStatus(aid, owners);
1831
2216
  }
1832
2217
  if (!this.connected || !this.client)
1833
- return true;
2218
+ return postBootstrapWelcomeStatus(aid, owners);
1834
2219
  for (const entry of entries) {
1835
2220
  if (!outbox.isDeliveryForChannel(entry.delivery, entry.channelId))
1836
2221
  continue;
1837
- const sent = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
1838
- if (sent.status === 'sent')
1839
- this.removeDeliveredOutboxEntry(entry);
1840
- else if (sent.status === 'permanent')
1841
- 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
+ }
1842
2236
  }
1843
- return true;
2237
+ return postBootstrapWelcomeStatus(aid, owners);
1844
2238
  }
1845
2239
  // ── Event handlers ──────────────────────────────────────────
1846
2240
  /**
@@ -2115,6 +2509,15 @@ export class AUNChannel {
2115
2509
  return;
2116
2510
  const protectedHeaders = verifiedProtectedHeaders(msg, env);
2117
2511
  const payload = msg.payload ?? '';
2512
+ if (this.managedMenuClient.accept(fromAid, payload, !!env.encrypted)) {
2513
+ this.acknowledgeImmediately(messageId, seq);
2514
+ return;
2515
+ }
2516
+ if (payloadTypeForObserver(msg) === 'observer.forward') {
2517
+ this.acknowledgeImmediately(messageId, seq);
2518
+ logger.debug(`${this.logPrefix()} P2P dropped observer.forward control envelope: mid=${messageId}`);
2519
+ return;
2520
+ }
2118
2521
  const mentions = this.parsePayloadMentionsOrReject(payload, 'p2p.inbound', messageId, seq);
2119
2522
  if (!mentions)
2120
2523
  return;
@@ -2174,7 +2577,7 @@ export class AUNChannel {
2174
2577
  const chatId = fromAid;
2175
2578
  // 解析对端身份(30天缓存)
2176
2579
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
2177
- 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());
2178
2581
  const shortAid = this.getShortAid(fromAid);
2179
2582
  const displayName = peerIdentity.name || shortAid;
2180
2583
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
@@ -2191,19 +2594,33 @@ export class AUNChannel {
2191
2594
  if (p2pPayloadType.startsWith('menu.')
2192
2595
  || p2pPayloadType === AUNChannel.CONTACT_REQUEST_QUERY_TYPE
2193
2596
  || p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
2597
+ const installation = p2pPayloadType.startsWith('menu.')
2598
+ ? this.localSenderInstallation?.aunSenderInstallation(msg) : undefined;
2599
+ const peerDeviceId = p2pPayloadType.startsWith('menu.')
2600
+ ? menuSenderDeviceId(msg.sender_device_id) : undefined;
2194
2601
  this.acknowledgeImmediately(messageId, seq);
2195
2602
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
2196
2603
  this.dispatchMessage({
2197
2604
  channelId: chatId, userId: fromAid,
2198
2605
  text: JSON.stringify(payload),
2199
2606
  chatType: 'private', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq,
2607
+ peerDeviceId,
2608
+ peerSlotId: installation?.slotId,
2609
+ threadId,
2200
2610
  peerName: displayName || undefined,
2201
2611
  peerType: peerIdentity.type,
2202
2612
  encrypted: msgEncrypted,
2203
2613
  protectedHeaders,
2204
2614
  mentions,
2205
2615
  mentionAids: mentionEntryAids(mentions),
2206
- 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
+ },
2207
2624
  msgType: logDescriptor.msgType,
2208
2625
  payloadType: logDescriptor.payloadType,
2209
2626
  payloadSummary: logDescriptor.payloadSummary,
@@ -2228,6 +2645,7 @@ export class AUNChannel {
2228
2645
  channelId: chatId, userId: fromAid,
2229
2646
  text: JSON.stringify(payload),
2230
2647
  chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
2648
+ threadId,
2231
2649
  peerName: displayName || undefined,
2232
2650
  peerType: peerIdentity.type,
2233
2651
  encrypted: msgEncrypted,
@@ -2252,7 +2670,7 @@ export class AUNChannel {
2252
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 });
2253
2671
  const isSystemP2P = p2pPayloadType === 'event';
2254
2672
  this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
2255
- 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));
2256
2674
  this.dispatchMessage({
2257
2675
  channelId: chatId,
2258
2676
  userId: fromAid,
@@ -2260,6 +2678,7 @@ export class AUNChannel {
2260
2678
  chatType: 'private',
2261
2679
  messageId,
2262
2680
  receivedAt,
2681
+ createdAt: typeof msg.created_at === 'number' ? msg.created_at : undefined,
2263
2682
  gatewaySeq: seq,
2264
2683
  causation,
2265
2684
  seq,
@@ -2297,6 +2716,12 @@ export class AUNChannel {
2297
2716
  const groupId = env.group_id ?? '';
2298
2717
  const senderAid = env.from ?? '';
2299
2718
  const payload = msg.payload ?? '';
2719
+ const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
2720
+ if (inboundType === 'observer.forward') {
2721
+ this.acknowledgeImmediately(messageId, seq);
2722
+ logger.debug(`${this.logPrefix()} Group dropped observer.forward control envelope: mid=${messageId}`);
2723
+ return;
2724
+ }
2300
2725
  const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId, seq);
2301
2726
  if (!payloadMentionEntries)
2302
2727
  return;
@@ -2307,7 +2732,6 @@ export class AUNChannel {
2307
2732
  const topicName = (typeof payload === 'object' && payload !== null)
2308
2733
  ? this.sanitizeTopicName(payload.topicName)
2309
2734
  : undefined;
2310
- const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
2311
2735
  // 终态错误只作为展示信号,不能进入 Agent,也不能被 observer 再次传播。
2312
2736
  if (inboundType === 'error') {
2313
2737
  this.acknowledgeImmediately(messageId, seq);
@@ -2358,7 +2782,7 @@ export class AUNChannel {
2358
2782
  peerName: displayName,
2359
2783
  peerType: peerInfo?.type || 'unknown',
2360
2784
  seq,
2361
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId),
2785
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId, readTopicLable(payload)),
2362
2786
  createdAt,
2363
2787
  });
2364
2788
  return;
@@ -2377,27 +2801,10 @@ export class AUNChannel {
2377
2801
  {
2378
2802
  const payloadObj = (payload && typeof payload === 'object') ? payload : null;
2379
2803
  const payloadType = typeof payloadObj?.type === 'string' ? payloadObj.type : '';
2380
- // menu.* 协议:自定义消息快速路径
2804
+ // Menu control requests are private-only, including token issuance.
2381
2805
  if (payloadType.startsWith('menu.')) {
2382
- const menuEncrypted = !!env.encrypted;
2383
2806
  this.acknowledgeImmediately(messageId, seq);
2384
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId, receivedAt, seq);
2385
- this.dispatchMessage({
2386
- channelId: groupId, userId: senderAid,
2387
- text: JSON.stringify(payload),
2388
- chatType: 'group', messageId, seq, receivedAt, receivedAtMono, gatewaySeq: seq, groupId,
2389
- encrypted: menuEncrypted,
2390
- protectedHeaders,
2391
- mentions: payloadMentionEntries,
2392
- mentionAids: payloadMentionAids,
2393
- isMentioned: hasMentionAll(payloadMentionEntries)
2394
- || (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
2395
- replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName, groupId),
2396
- msgType: logDescriptor.msgType,
2397
- payloadType: logDescriptor.payloadType,
2398
- payloadSummary: logDescriptor.payloadSummary,
2399
- messageLogContent: logDescriptor.content,
2400
- });
2807
+ logger.debug(`${this.logPrefix()} Group Menu dropped: code=MENU_PRIVATE_ONLY mid=${messageId}`);
2401
2808
  return;
2402
2809
  }
2403
2810
  if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
@@ -2437,17 +2844,15 @@ export class AUNChannel {
2437
2844
  if (isEchoMsg) {
2438
2845
  // 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
2439
2846
  // >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
2440
- const echoTs = () => {
2441
- const d = new Date();
2442
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
2443
- };
2847
+ const echoCreatedAt = data.created_at;
2848
+ const echoReceiveTs = Date.now();
2444
2849
  let echoText = text;
2445
- echoText += `\n${echoTs()} [EvolCore.receive] from=${senderAid} mid=${messageId} chat=group self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
2850
+ echoText += `\n${echoTraceClock(echoReceiveTs)} [EvolCore.receive] from=${senderAid} mid=${messageId} chat=group self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((echoReceiveTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoInboundLegLabel(echoCreatedAt, receivedAt)}${echoTraceOriginLabel(echoReceiveTs)}`;
2446
2851
  const msgChatmodeEcho = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
2447
2852
  this.pendingEchoMessages.set(messageId, {
2448
2853
  text: echoText,
2449
2854
  channelId: groupId,
2450
- context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined, groupId),
2855
+ context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId, readTopicLable(payload)),
2451
2856
  receiveTs: Date.now(),
2452
2857
  });
2453
2858
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
@@ -2469,8 +2874,20 @@ export class AUNChannel {
2469
2874
  // 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
2470
2875
  // TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
2471
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
+ }
2472
2890
  this.acknowledgeImmediately(messageId, seq);
2473
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
2474
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))})`);
2475
2892
  return;
2476
2893
  }
@@ -2493,7 +2910,7 @@ export class AUNChannel {
2493
2910
  // Process attachments
2494
2911
  const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments, { chatType: 'group', groupId });
2495
2912
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
2496
- 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());
2497
2914
  const shortAid = this.getShortAid(senderAid);
2498
2915
  const displayName = peerIdentity.name || shortAid;
2499
2916
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
@@ -2536,6 +2953,7 @@ export class AUNChannel {
2536
2953
  chatType: 'group',
2537
2954
  messageId,
2538
2955
  receivedAt,
2956
+ createdAt: typeof msg.created_at === 'number' ? msg.created_at : undefined,
2539
2957
  gatewaySeq: seq,
2540
2958
  seq,
2541
2959
  threadId,
@@ -2544,7 +2962,7 @@ export class AUNChannel {
2544
2962
  isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
2545
2963
  source: isOwnHandoff ? 'handoff' : undefined,
2546
2964
  encrypted: msgEncrypted,
2547
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId),
2965
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId, readTopicLable(payload)),
2548
2966
  mentionMode: serverMentionMode,
2549
2967
  images: inboundImages.length > 0 ? inboundImages : undefined,
2550
2968
  msgType: logDescriptor.msgType,
@@ -2564,6 +2982,7 @@ export class AUNChannel {
2564
2982
  && authenticatedDelivery.chatType === 'group'
2565
2983
  && suppliedDelivery.groupId === authenticatedDelivery.groupId;
2566
2984
  const delivery = suppliedDelivery && suppliedMatches ? suppliedDelivery : authenticatedDelivery;
2985
+ const threadId = event.threadId ?? event.replyContext?.threadId;
2567
2986
  if (suppliedDelivery && delivery !== suppliedDelivery) {
2568
2987
  logger.warn(`${this.logPrefix()} Ignoring mismatched inbound reply route: channel=${event.channelId} chatType=${event.chatType}`);
2569
2988
  }
@@ -2585,17 +3004,15 @@ export class AUNChannel {
2585
3004
  }
2586
3005
  // 长 echo(>10 字符):存 pending,继续交给 agent 处理
2587
3006
  if (/echo/i.test(firstLine) && firstLine.trim().length > 10 && !hasEvolCoreTracePrivate) {
2588
- const echoTs = () => {
2589
- const d = new Date();
2590
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
2591
- };
3007
+ const echoReceiveTs = Date.now();
2592
3008
  let echoText = event.text;
2593
- echoText += `\n${echoTs()} [EvolCore.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
3009
+ echoText += `\n${echoTraceClock(echoReceiveTs)} [EvolCore.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((echoReceiveTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoInboundLegLabel(event.createdAt, event.receivedAt ?? echoReceiveTs)}${echoTraceOriginLabel(echoReceiveTs)}`;
2594
3010
  this.pendingEchoMessages.set(event.messageId, {
2595
3011
  text: echoText,
2596
3012
  channelId: event.channelId,
2597
3013
  context: {
2598
3014
  ...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
3015
+ ...(typeof threadId === 'string' ? { threadId } : {}),
2599
3016
  delivery,
2600
3017
  },
2601
3018
  receiveTs: Date.now(),
@@ -2610,7 +3027,7 @@ export class AUNChannel {
2610
3027
  const replyContext = {
2611
3028
  ...(event.replyContext ?? {}),
2612
3029
  delivery,
2613
- ...(event.threadId && !event.replyContext?.threadId ? { threadId: event.threadId } : {}),
3030
+ ...(typeof threadId === 'string' ? { threadId } : {}),
2614
3031
  };
2615
3032
  this.messageHandler({
2616
3033
  channelId: event.channelId || '',
@@ -2622,6 +3039,8 @@ export class AUNChannel {
2622
3039
  peerId: event.userId || event.channelId || '',
2623
3040
  peerName: event.peerName,
2624
3041
  peerType: event.peerType,
3042
+ peerDeviceId: event.peerDeviceId,
3043
+ peerSlotId: event.peerSlotId,
2625
3044
  sameDevice: event.sameDevice,
2626
3045
  sameNetwork: event.sameNetwork,
2627
3046
  sameEgressIp: event.sameEgressIp,
@@ -2632,7 +3051,7 @@ export class AUNChannel {
2632
3051
  receivedAtMono: event.receivedAtMono,
2633
3052
  gatewaySeq: event.gatewaySeq ?? event.seq,
2634
3053
  causation: event.causation,
2635
- threadId: event.threadId,
3054
+ threadId,
2636
3055
  mentions: event.mentions,
2637
3056
  mentionAids: event.mentionAids,
2638
3057
  isMentioned: event.isMentioned, // ← 新增:传递 isMentioned 标记
@@ -2648,10 +3067,10 @@ export class AUNChannel {
2648
3067
  logger.error(`${this.logPrefix()} Message handler error:`, err);
2649
3068
  });
2650
3069
  }
2651
- appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq) {
3070
+ appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq, groupAttention, groupDelivery) {
2652
3071
  try {
2653
3072
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
2654
- appendMessageLog(chatDir, buildInboundEntry({
3073
+ const entry = buildInboundEntry({
2655
3074
  from: fromAid,
2656
3075
  to: this.config.aid,
2657
3076
  threadId: threadId || null,
@@ -2666,10 +3085,18 @@ export class AUNChannel {
2666
3085
  timestamp: receivedAt,
2667
3086
  receivedAt,
2668
3087
  gatewaySeq,
2669
- }));
3088
+ groupAttention,
3089
+ groupDelivery,
3090
+ });
3091
+ if (groupDelivery === 'filtered')
3092
+ appendMessageLogStrict(chatDir, entry, true);
3093
+ else
3094
+ appendMessageLog(chatDir, entry);
2670
3095
  }
2671
3096
  catch (e) {
2672
- logger.debug(`${this.logPrefix()} appendInboundPayloadJsonl failed: ${e}`);
3097
+ logger.warn(`${this.logPrefix()} appendInboundPayloadJsonl failed: ${e}`);
3098
+ if (groupDelivery === 'filtered')
3099
+ throw e;
2673
3100
  }
2674
3101
  }
2675
3102
  // ── 观察者模式(Observer Mode) ──────────────────────────────
@@ -2797,7 +3224,10 @@ export class AUNChannel {
2797
3224
  forwardInbound(data) {
2798
3225
  if (!this.connected || !this.client)
2799
3226
  return Promise.resolve();
2800
- if (payloadTypeForObserver(data)?.startsWith('menu.'))
3227
+ const inboundType = payloadTypeForObserver(data);
3228
+ // Observer envelopes are control-plane telemetry. Mirroring one again
3229
+ // would amplify every activity event across observer-connected agents.
3230
+ if (inboundType === 'observer.forward' || inboundType?.startsWith('menu.'))
2801
3231
  return Promise.resolve();
2802
3232
  const { observable, owners } = this.getObserverConfig();
2803
3233
  if (!observable || owners.length === 0)
@@ -2833,7 +3263,8 @@ export class AUNChannel {
2833
3263
  forwardOutbound(result) {
2834
3264
  if (!this.connected || !this.client)
2835
3265
  return Promise.resolve();
2836
- if (payloadTypeForObserver(result)?.startsWith('menu.'))
3266
+ const outboundType = payloadTypeForObserver(result);
3267
+ if (outboundType === 'observer.forward' || outboundType?.startsWith('menu.'))
2837
3268
  return Promise.resolve();
2838
3269
  const { observable, owners } = this.getObserverConfig();
2839
3270
  if (!observable || owners.length === 0)
@@ -2874,8 +3305,8 @@ export class AUNChannel {
2874
3305
  };
2875
3306
  const encrypt = this.shouldEncrypt('observer.forward');
2876
3307
  for (const ownerAid of owners) {
2877
- this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt })
2878
- .catch(e => logger.warn(`${this.logPrefix()} observer.forward send to ${ownerAid} failed: ${e}`));
3308
+ this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt }, { lane: 'observer' })
3309
+ .catch(e => logger.debug(`${this.logPrefix()} observer.forward send to ${ownerAid} failed: ${e}`));
2879
3310
  }
2880
3311
  }
2881
3312
  // ── 观察者插话(Observer Insert,v0.3 待用上下文提示) ──────────────
@@ -2978,14 +3409,12 @@ export class AUNChannel {
2978
3409
  }
2979
3410
  }
2980
3411
  handleEcho(event) {
2981
- const ts = () => {
2982
- const d = new Date();
2983
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
2984
- };
3412
+ const receiveTs = Date.now();
2985
3413
  // 在收到的消息文本末尾追加 trace 行(只追加,不修改原文)
2986
3414
  let echoText = event.text;
2987
- echoText += `\n${ts()} [EvolCore.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
2988
- echoText += `\n${ts()} [EvolCore.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
3415
+ echoText += `\n${echoTraceClock(receiveTs)} [EvolCore.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((receiveTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoInboundLegLabel(event.createdAt, receiveTs)}${echoTraceOriginLabel(receiveTs)}`;
3416
+ const replyTs = Date.now();
3417
+ echoText += `\n${echoTraceClock(replyTs)} [EvolCore.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((replyTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoTraceOriginLabel(replyTs)}`;
2989
3418
  if (Buffer.byteLength(echoText, 'utf-8') > 4096) {
2990
3419
  echoText += `\n[TRUNCATED]`;
2991
3420
  }
@@ -2995,7 +3424,7 @@ export class AUNChannel {
2995
3424
  ? { chatType: 'group', groupId: event.channelId }
2996
3425
  : { chatType: 'private' };
2997
3426
  const context = {
2998
- threadId: event.threadId,
3427
+ threadId: event.threadId ?? event.replyContext?.threadId,
2999
3428
  metadata: event.replyContext?.metadata,
3000
3429
  delivery: authenticatedDelivery,
3001
3430
  };
@@ -3098,6 +3527,7 @@ export class AUNChannel {
3098
3527
  else if (state === 'disconnected' || state === 'reconnecting') {
3099
3528
  const wasConnected = this.connected;
3100
3529
  this.connected = false;
3530
+ this.clearQueuedObserverSends(state);
3101
3531
  // The nine-state SDK emits standby -> retry_backoff -> reconnecting for
3102
3532
  // one reconnect cycle. Count the connected-to-down edge once instead of
3103
3533
  // inflating the metric for every intermediate state.
@@ -3281,6 +3711,7 @@ export class AUNChannel {
3281
3711
  this.client = null;
3282
3712
  }
3283
3713
  this.connected = false;
3714
+ this.clearQueuedObserverSends(`takeover_${reason}`);
3284
3715
  const delaySec = Math.round(delayMs / 1000);
3285
3716
  logger.info(`${this.logPrefix()} Scheduling TS-layer reconnect (${reason}) in ${delaySec}s`);
3286
3717
  this.trace('OUT', 'reconnect.scheduled', { reason, delayMs, generation: gen });
@@ -3444,7 +3875,9 @@ export class AUNChannel {
3444
3875
  return Buffer.byteLength(logText, 'utf-8');
3445
3876
  }
3446
3877
  applyReplyContextToPayload(payload, context) {
3447
- const finalPayload = this.stripUndefinedDeep({ ...payload });
3878
+ const finalPayload = this.stripUndefinedDeep(withAunAttention({ ...payload }, context?.attention));
3879
+ if (finalPayload.thread_id === '')
3880
+ delete finalPayload.thread_id;
3448
3881
  if (context?.threadId && !finalPayload.thread_id)
3449
3882
  finalPayload.thread_id = context.threadId;
3450
3883
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
@@ -3456,13 +3889,14 @@ export class AUNChannel {
3456
3889
  return finalPayload;
3457
3890
  }
3458
3891
  /**
3459
- * Validate the AUN-specific structured field before it crosses the durable
3892
+ * Validate AUN-specific structured fields before crossing the durable
3460
3893
  * send boundary. Return a copy when mentions are present so callers do not
3461
3894
  * retain an unvalidated array reference.
3462
3895
  */
3463
3896
  normalizeAunPayloadMentions(payload) {
3464
3897
  if (!payload || typeof payload !== 'object' || Array.isArray(payload))
3465
3898
  return payload;
3899
+ payload = withAunAttention(payload);
3466
3900
  if (!Object.prototype.hasOwnProperty.call(payload, 'mentions'))
3467
3901
  return payload;
3468
3902
  return {
@@ -3498,6 +3932,9 @@ export class AUNChannel {
3498
3932
  if (!current || typeof current !== 'object')
3499
3933
  continue;
3500
3934
  for (const key of Object.keys(current)) {
3935
+ // Topic routing must survive content truncation and fallback.
3936
+ if (current === limited && key === 'thread_id')
3937
+ continue;
3501
3938
  const value = current[key];
3502
3939
  if (typeof value === 'string') {
3503
3940
  leaves.push({ parent: current, key, value, bytes: Buffer.byteLength(value, 'utf8') });
@@ -3544,6 +3981,8 @@ export class AUNChannel {
3544
3981
  };
3545
3982
  if (typeof payload.chatmode === 'string')
3546
3983
  fallback.chatmode = truncateUtf8ToBytes(payload.chatmode, 32);
3984
+ if (typeof payload.thread_id === 'string')
3985
+ fallback.thread_id = payload.thread_id;
3547
3986
  logger.info(`${this.logPrefix()} Thought payload reduced to fallback (${Buffer.byteLength(JSON.stringify(fallback), 'utf8')} bytes)`);
3548
3987
  return fallback;
3549
3988
  }
@@ -3602,15 +4041,23 @@ export class AUNChannel {
3602
4041
  registerCardPostSend(messageId, action, channelId, delivery, replyContext) {
3603
4042
  if (action.type !== 'register_interaction_card')
3604
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;
3605
4049
  this.cardMessageIdMap.set(messageId, {
3606
4050
  requestId: action.requestId,
3607
4051
  isCommandCard: action.isCommandCard,
3608
4052
  initiatorAid: action.initiatorAid,
3609
4053
  channelId,
3610
4054
  delivery: delivery ?? action.delivery,
3611
- replyContext,
4055
+ replyContext: callbackContext,
3612
4056
  });
3613
4057
  this.ownedCardMsgIds.add(messageId);
4058
+ if (typeof replyContext?.threadId === 'string') {
4059
+ this.ownedCardTopics.set(messageId, { channelId, threadId: replyContext.threadId });
4060
+ }
3614
4061
  let messageIds = this.interactionCardMessageIds.get(action.requestId);
3615
4062
  if (!messageIds) {
3616
4063
  messageIds = new Set();
@@ -3629,7 +4076,10 @@ export class AUNChannel {
3629
4076
  this.interactionCardMessageIds.delete(action.requestId);
3630
4077
  }, mapTtl);
3631
4078
  mapTimer.unref?.();
3632
- const ownedTimer = setTimeout(() => this.ownedCardMsgIds.delete(messageId), 24 * 60 * 60 * 1000);
4079
+ const ownedTimer = setTimeout(() => {
4080
+ this.ownedCardMsgIds.delete(messageId);
4081
+ this.ownedCardTopics.delete(messageId);
4082
+ }, 24 * 60 * 60 * 1000);
3633
4083
  ownedTimer.unref?.();
3634
4084
  const invalidationReason = this.invalidatedInteractions.get(action.requestId);
3635
4085
  if (invalidationReason) {
@@ -3642,6 +4092,8 @@ export class AUNChannel {
3642
4092
  return;
3643
4093
  if (entry.postSend.type === 'register_interaction_card') {
3644
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);
3645
4097
  }
3646
4098
  }
3647
4099
  async sendAunPayload(channelId, payload, context, label) {
@@ -3673,9 +4125,20 @@ export class AUNChannel {
3673
4125
  const payloadWithCausationRef = causationRef
3674
4126
  ? { ...normalizedPayload, _evolcore_causation_ref: causationRef }
3675
4127
  : normalizedPayload;
4128
+ if (payloadWithCausationRef.thread_id === '')
4129
+ delete payloadWithCausationRef.thread_id;
3676
4130
  // Truncate payload if too large (AUN SDK limit: 1MB)
3677
4131
  const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
3678
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
+ }
3679
4142
  if (isGroup)
3680
4143
  params.group_id = delivery.groupId;
3681
4144
  else
@@ -3696,7 +4159,7 @@ export class AUNChannel {
3696
4159
  return await callOnce(params, false);
3697
4160
  }
3698
4161
  catch (e) {
3699
- if (encrypt && e instanceof E2EEError) {
4162
+ if (encrypt && e instanceof E2EEError && !isMenuPayload) {
3700
4163
  this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
3701
4164
  logger.warn(`${this.logPrefix()} E2EE ${label} send failed to ${channelId}, retrying plaintext: ${e}`);
3702
4165
  const fallbackParams = { ...params, encrypt: false };
@@ -3749,7 +4212,8 @@ export class AUNChannel {
3749
4212
  ...classifyAunPayloadForLog(payload), msgId: messageId, encrypt, context, isGroup, source,
3750
4213
  transport: this.sendReceiptFromResult(result),
3751
4214
  });
3752
- this.forwardOutbound(result);
4215
+ if (typeof payload.type !== 'string' || !payload.type.startsWith('menu.'))
4216
+ this.forwardOutbound(result);
3753
4217
  }
3754
4218
  async sendContentPayload(channelId, payload, opts) {
3755
4219
  const delivery = this.requireDelivery(channelId, opts.context);
@@ -3792,6 +4256,7 @@ export class AUNChannel {
3792
4256
  queue: opts.queue,
3793
4257
  channelId,
3794
4258
  delivery,
4259
+ dedupeKey: opts.dedupeKey,
3795
4260
  type: 'payload',
3796
4261
  contentKind: opts.contentKind,
3797
4262
  payload: finalPayload,
@@ -3863,7 +4328,7 @@ export class AUNChannel {
3863
4328
  base.initiator = context.peerId;
3864
4329
  if (context?.replyToMessageId)
3865
4330
  base.ref_message_id = context.replyToMessageId;
3866
- return base;
4331
+ return withAunAttention(base, context?.attention);
3867
4332
  }
3868
4333
  activityLogText(raw) {
3869
4334
  // 兼容两种入参:原始 ThoughtItem(顶层字段),或已构建的 activity payload(字段收在 .item 里)
@@ -3889,68 +4354,27 @@ export class AUNChannel {
3889
4354
  /**
3890
4355
  * Truncate payload if it exceeds AUN protocol limits.
3891
4356
  * AUN SDK enforces MAX_WS_PAYLOAD_SIZE = 1_000_000 bytes (1MB) for the entire JSON-RPC message.
3892
- * We use a conservative 800KB limit for the payload itself to leave room for RPC envelope.
4357
+ * We use a conservative 512KB limit for the payload itself to leave room for
4358
+ * the encrypted AUN envelope and RPC framing.
3893
4359
  */
3894
4360
  truncatePayloadIfNeeded(payload, label) {
3895
- const MAX_PAYLOAD_SIZE = 800 * 1024; // 800KB conservative limit
3896
- const MAX_FIELD_SIZE = 256 * 1024; // 256KB per field
3897
- // First check total size
3898
- const payloadJson = JSON.stringify(payload);
3899
- const totalSize = Buffer.byteLength(payloadJson, 'utf-8');
3900
- if (totalSize <= MAX_PAYLOAD_SIZE) {
4361
+ const result = truncateAunPayloadIfNeeded(payload);
4362
+ if (result.originalSize <= AUN_MAX_PAYLOAD_SIZE) {
3901
4363
  return payload; // No truncation needed
3902
4364
  }
3903
- logger.warn(`${this.logPrefix()} Payload too large (${formatSize(totalSize)}), truncating... label=${label}`);
3904
- // Deep clone to avoid mutating original
3905
- const truncated = JSON.parse(payloadJson);
3906
- // Truncate large string fields recursively
3907
- const truncateObject = (obj, path) => {
3908
- if (!obj || typeof obj !== 'object')
3909
- return false;
3910
- let changed = false;
3911
- for (const key of Object.keys(obj)) {
3912
- const value = obj[key];
3913
- const fieldPath = path ? `${path}.${key}` : key;
3914
- if (typeof value === 'string') {
3915
- const fieldSize = Buffer.byteLength(value, 'utf-8');
3916
- if (fieldSize > MAX_FIELD_SIZE) {
3917
- const truncatedValue = value.substring(0, MAX_FIELD_SIZE / 4); // Rough estimate, UTF-8 safe
3918
- const actualTruncatedSize = Buffer.byteLength(truncatedValue, 'utf-8');
3919
- obj[key] = truncatedValue + `\n\n[截断:原始输出 ${formatSize(fieldSize)},仅显示前 ${formatSize(actualTruncatedSize)}]`;
3920
- obj[`${key}_truncated`] = true;
3921
- logger.info(`${this.logPrefix()} Truncated field ${fieldPath}: ${formatSize(fieldSize)} → ${formatSize(actualTruncatedSize)}`);
3922
- changed = true;
3923
- }
3924
- }
3925
- else if (Array.isArray(value)) {
3926
- for (let i = 0; i < value.length; i++) {
3927
- if (truncateObject(value[i], `${fieldPath}[${i}]`)) {
3928
- changed = true;
3929
- }
3930
- }
3931
- }
3932
- else if (typeof value === 'object' && value !== null) {
3933
- if (truncateObject(value, fieldPath)) {
3934
- changed = true;
3935
- }
3936
- }
3937
- }
3938
- return changed;
3939
- };
3940
- truncateObject(truncated, '');
3941
- // Check size again after truncation
3942
- const newJson = JSON.stringify(truncated);
3943
- const newSize = Buffer.byteLength(newJson, 'utf-8');
3944
- if (newSize > MAX_PAYLOAD_SIZE) {
3945
- // Still too large - this shouldn't happen with 256KB field limit, but log it
3946
- 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`);
3947
4371
  return {
3948
4372
  type: 'text',
3949
4373
  text: '消息内容过大,无法完整发送,请缩小输出后重试',
3950
4374
  };
3951
4375
  }
3952
- logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(totalSize)} → ${formatSize(newSize)}`);
3953
- return truncated;
4376
+ logger.info(`${this.logPrefix()} Payload truncated: ${formatSize(result.originalSize)} → ${formatSize(result.finalSize)}`);
4377
+ return result.payload;
3954
4378
  }
3955
4379
  buildActivityPayload(envelope, context, item) {
3956
4380
  const rawItem = item && typeof item === 'object' && !Array.isArray(item)
@@ -3962,11 +4386,11 @@ export class AUNChannel {
3962
4386
  const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
3963
4387
  ? { ...rawItem, arguments: displayArguments }
3964
4388
  : rawItem;
3965
- return {
4389
+ return withAunAttention({
3966
4390
  ...this.buildTaskPayloadBase(envelope, context),
3967
4391
  type: 'activity',
3968
4392
  item: this.stripUndefinedDeep(activityItem),
3969
- };
4393
+ });
3970
4394
  }
3971
4395
  async sendReliableStructured(channelId, payload, context, logText) {
3972
4396
  const result = await this.sendContentPayload(channelId, payload, {
@@ -3981,11 +4405,14 @@ export class AUNChannel {
3981
4405
  throw Object.assign(new Error(result.error ?? 'AUN structured send failed'), {
3982
4406
  code: result.code ?? 'AUN_SEND_FAILED',
3983
4407
  outboxId: result.outboxId,
4408
+ permanent: true,
4409
+ status: 'failed',
3984
4410
  });
3985
4411
  }
3986
4412
  return result;
3987
4413
  }
3988
4414
  async sendMessage(channelId, text, context) {
4415
+ parseAunAttention(context?.attention);
3989
4416
  if (!text?.trim()) {
3990
4417
  logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
3991
4418
  return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
@@ -3994,7 +4421,7 @@ export class AUNChannel {
3994
4421
  const routedContext = this.withDelivery(context, delivery);
3995
4422
  // 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
3996
4423
  if (!this.isEchoSending) {
3997
- await this.flushPendingEcho(channelId, delivery);
4424
+ await this.flushPendingEcho(channelId, delivery, context?.threadId);
3998
4425
  }
3999
4426
  let finalText = text;
4000
4427
  this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
@@ -4102,6 +4529,7 @@ export class AUNChannel {
4102
4529
  if (entry.deliveryReceipt?.messageId) {
4103
4530
  return { status: 'sent', messageId: entry.deliveryReceipt.messageId };
4104
4531
  }
4532
+ outbox.recordRetry(this.config.aid, entry, result);
4105
4533
  return {
4106
4534
  status: 'queued',
4107
4535
  outboxId: entry.id,
@@ -4121,6 +4549,8 @@ export class AUNChannel {
4121
4549
  try {
4122
4550
  const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
4123
4551
  finalPayload = this.stripUndefinedDeep(validatedPayload);
4552
+ if (finalPayload.thread_id === '')
4553
+ delete finalPayload.thread_id;
4124
4554
  }
4125
4555
  catch (e) {
4126
4556
  return {
@@ -4222,11 +4652,7 @@ export class AUNChannel {
4222
4652
  code: 'H_CLASS_PROTECTED',
4223
4653
  });
4224
4654
  }
4225
- const connection = {
4226
- call: (method, params) => this.callAndTrace(method, params),
4227
- close: async () => { },
4228
- };
4229
- const built = await uploadFileAndBuildPayload(connection, this.config.aid, file.filePath, {
4655
+ const built = await uploadFileAndBuildPayload(this.client, this.config.aid, file.filePath, {
4230
4656
  as: file.as,
4231
4657
  contentType: file.contentType,
4232
4658
  text: file.text,
@@ -4247,6 +4673,8 @@ export class AUNChannel {
4247
4673
  // removed from a malformed mentions array.
4248
4674
  const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
4249
4675
  const finalPayload = this.stripUndefinedDeep(validatedPayload);
4676
+ if (finalPayload.thread_id === '')
4677
+ delete finalPayload.thread_id;
4250
4678
  const payloadMentions = Object.prototype.hasOwnProperty.call(finalPayload, 'mentions')
4251
4679
  ? normalizeAunMentionEntries(finalPayload.mentions)
4252
4680
  : undefined;
@@ -4321,20 +4749,20 @@ export class AUNChannel {
4321
4749
  return { ok: false, error: e?.message || String(e), code: e?.code };
4322
4750
  }
4323
4751
  }
4324
- async flushPendingEcho(channelId, delivery) {
4752
+ async flushPendingEcho(channelId, delivery, threadId) {
4325
4753
  // 查找该 channelId 是否有 pending echo(长 echo 等待 agent 首次回复)
4326
4754
  for (const [key, echo] of this.pendingEchoMessages) {
4327
- if (echo.channelId === channelId) {
4755
+ if (echo.channelId === channelId
4756
+ && (echo.context?.threadId ?? '') === (threadId ?? '')
4757
+ && echo.context?.delivery?.chatType === delivery.chatType) {
4328
4758
  this.pendingEchoMessages.delete(key);
4329
4759
  logger.info(`${this.logPrefix()} [Echo] flushPendingEcho triggered: key=${key} channelId=${channelId} pendingCount=${this.pendingEchoMessages.size}`);
4330
- const ts = () => {
4331
- const d = new Date();
4332
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
4333
- };
4334
- const agentDuration = Date.now() - echo.receiveTs;
4760
+ const agentDoneTs = Date.now();
4761
+ const agentDuration = agentDoneTs - echo.receiveTs;
4335
4762
  let echoText = echo.text;
4336
- echoText += `\n${ts()} [EvolCore.agent] duration=${agentDuration}ms`;
4337
- echoText += `\n${ts()} [EvolCore.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
4763
+ echoText += `\n${echoTraceClock(agentDoneTs)} [EvolCore.agent] duration=${agentDuration}ms${echoTraceOriginLabel(agentDoneTs)}`;
4764
+ const replyTs = Date.now();
4765
+ echoText += `\n${echoTraceClock(replyTs)} [EvolCore.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((replyTs - this.connectedAt) / 1000) + 's' : 'unknown'}${echoTraceOriginLabel(replyTs)}`;
4338
4766
  if (Buffer.byteLength(echoText, 'utf-8') > 4096) {
4339
4767
  echoText += `\n[TRUNCATED]`;
4340
4768
  }
@@ -4401,10 +4829,16 @@ export class AUNChannel {
4401
4829
  return { status: 'retry', error: 'post-bootstrap welcome is waiting for active lifecycle', code: 'BOOTSTRAP_NOT_ACTIVE' };
4402
4830
  }
4403
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
+ }
4404
4838
  }
4405
4839
  // 从 context.metadata.source 读取 source,默认为 'daemon'
4406
4840
  const source = context?.metadata?.source ?? 'daemon';
4407
- const payload = { type: 'text', text: finalText };
4841
+ const payload = withAunAttention({ type: 'text', text: finalText }, context?.attention);
4408
4842
  if (delivery.chatType === 'group') {
4409
4843
  const extracted = this.extractMentionEntriesFromText(finalText);
4410
4844
  if (extracted.length > 0)
@@ -4418,6 +4852,8 @@ export class AUNChannel {
4418
4852
  payload.task_id = context.metadata.taskId;
4419
4853
  if (context?.metadata?.chatmode)
4420
4854
  payload.chatmode = context.metadata.chatmode;
4855
+ if (context?.metadata?.forkAnchor)
4856
+ payload.fork_anchor = context.metadata.forkAnchor;
4421
4857
  const causation = normalizeCausation(context?.metadata?.causation);
4422
4858
  const causationRef = delivery.chatType !== 'group' && causation
4423
4859
  ? `caus_${crypto.randomUUID().replace(/-/g, '')}`
@@ -4608,6 +5044,7 @@ export class AUNChannel {
4608
5044
  queue: entry.queue,
4609
5045
  };
4610
5046
  const receipt = {
5047
+ acceptedAt: Date.now(),
4611
5048
  messageId,
4612
5049
  encrypt,
4613
5050
  transport: this.sendReceiptFromResult(result),
@@ -4630,6 +5067,15 @@ export class AUNChannel {
4630
5067
  logger.info(`${this.logPrefix()} Discarded invalidated interaction from durable outbox: request=${interactionId} entry=${entry.id}`);
4631
5068
  return { ok: true, status: 'sent' };
4632
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
+ }
4633
5079
  const interactionExpiresAt = entry.postSend?.type === 'register_interaction_card'
4634
5080
  ? entry.postSend.expiresAt
4635
5081
  : undefined;
@@ -4777,6 +5223,10 @@ export class AUNChannel {
4777
5223
  const payloadWithChatmode = context?.metadata?.chatmode && !finalPayload.chatmode
4778
5224
  ? { ...finalPayload, chatmode: context.metadata.chatmode }
4779
5225
  : finalPayload;
5226
+ if (payloadWithChatmode.thread_id === '')
5227
+ delete payloadWithChatmode.thread_id;
5228
+ if (context?.threadId && !payloadWithChatmode.thread_id)
5229
+ payloadWithChatmode.thread_id = context.threadId;
4780
5230
  const params = {
4781
5231
  context: { type: 'task', id: taskId },
4782
5232
  payload: this.limitThoughtPayload(payloadWithChatmode),
@@ -4857,7 +5307,7 @@ export class AUNChannel {
4857
5307
  // Validate before checking connection state so malformed producer payloads
4858
5308
  // are reported deterministically instead of being hidden by a disconnect.
4859
5309
  const validatedPayload = this.normalizeAunPayloadMentions(payload);
4860
- const finalPayload = this.stripUndefinedDeep(validatedPayload);
5310
+ const finalPayload = this.stripUndefinedDeep(withAunAttention(validatedPayload, context?.attention));
4861
5311
  if (!this.connected || !this.client) {
4862
5312
  return {
4863
5313
  ok: false,
@@ -4872,12 +5322,19 @@ export class AUNChannel {
4872
5322
  const encrypt = context?.metadata?.encrypted != null
4873
5323
  ? !!(context.metadata.encrypted)
4874
5324
  : this.shouldEncrypt(encryptTarget);
5325
+ if (finalPayload.thread_id === '')
5326
+ delete finalPayload.thread_id;
4875
5327
  if (context?.threadId && !finalPayload.thread_id)
4876
5328
  finalPayload.thread_id = context.threadId;
4877
5329
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
4878
5330
  finalPayload.ref_message_id = context.replyToMessageId;
4879
5331
  const isMenuPayload = typeof finalPayload.type === 'string' && finalPayload.type.startsWith('menu.');
4880
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
+ }
4881
5338
  const rawHeaders = context?.metadata?.protectedHeaders;
4882
5339
  if (encrypt && rawHeaders && typeof rawHeaders === 'object' && !Array.isArray(rawHeaders)) {
4883
5340
  const protectedHeaders = {};
@@ -4943,21 +5400,23 @@ export class AUNChannel {
4943
5400
  });
4944
5401
  }
4945
5402
  async sendFile(channelId, filePath, context) {
5403
+ parseAunAttention(context?.attention);
4946
5404
  const delivery = this.requireDelivery(channelId, context);
4947
5405
  context = this.withDelivery(context, delivery);
5406
+ const isFetch = typeof context?.metadata?.correlationId === 'string' && !!context.metadata.correlationId;
4948
5407
  const absPath = path.resolve(filePath);
4949
5408
  if (!fs.existsSync(absPath)) {
4950
5409
  logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
4951
- return;
5410
+ return { status: 'failed', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
4952
5411
  }
4953
5412
  const stat = fs.statSync(absPath);
4954
5413
  if (stat.size === 0) {
4955
5414
  logger.warn(`${this.logPrefix()} sendFile: file is empty`);
4956
- return;
5415
+ return { status: 'failed', error: 'file is empty', code: 'EMPTY_FILE' };
4957
5416
  }
4958
5417
  if (stat.size > 10 * 1024 * 1024) {
4959
5418
  logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
4960
- return;
5419
+ return { status: 'failed', error: `file too large (max ${formatSize(MAX_AUN_ATTACHMENT_SIZE)})`, code: 'FILE_TOO_LARGE' };
4961
5420
  }
4962
5421
  // Write-ahead: persist to outbox
4963
5422
  const entry = outbox.enqueue(this.config.aid, {
@@ -4975,17 +5434,47 @@ export class AUNChannel {
4975
5434
  if (!this.reconnectTimer && !this.client) {
4976
5435
  this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendFile failed: ${e}`));
4977
5436
  }
4978
- 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' };
4979
5443
  }
4980
5444
  const result = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), { status: 'retry', error: 'outbox send is already in flight' });
4981
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
+ }
4982
5452
  this.removeDeliveredOutboxEntry(entry);
5453
+ return { status: 'sent', messageId };
4983
5454
  }
4984
- 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.
4985
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
+ };
4986
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
+ };
4987
5475
  }
4988
5476
  async sendImage(channelId, data, mimeType, alt, context) {
5477
+ parseAunAttention(context?.attention);
4989
5478
  const delivery = this.requireDelivery(channelId, context);
4990
5479
  context = this.withDelivery(context, delivery);
4991
5480
  if (!Buffer.isBuffer(data)) {
@@ -5055,11 +5544,7 @@ export class AUNChannel {
5055
5544
  }
5056
5545
  const digest = crypto.createHash('sha256').update(data).digest('hex').slice(0, 12);
5057
5546
  const filename = `image-${digest}.${extension}`;
5058
- const connection = {
5059
- call: (method, params) => this.callAndTrace(method, params),
5060
- close: async () => { },
5061
- };
5062
- 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 });
5063
5548
  const imagePayload = { ...built.payload };
5064
5549
  setIfDefined(imagePayload, 'alt', alt);
5065
5550
  return imagePayload;
@@ -5146,57 +5631,14 @@ export class AUNChannel {
5146
5631
  logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
5147
5632
  return { status: 'permanent', error: 'file no longer exists', code: 'FILE_NOT_FOUND' };
5148
5633
  }
5149
- const filename = path.basename(absPath);
5150
- const fileData = fs.readFileSync(absPath);
5151
- const stat = fs.statSync(absPath);
5152
- const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
5153
- const contentType = guessMime(filename);
5154
- const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
5155
5634
  try {
5156
- if (stat.size <= 64 * 1024) {
5157
- await this.callAndTrace('storage.put_object', {
5158
- object_key: objectKey,
5159
- content: fileData.toString('base64'),
5160
- content_type: contentType,
5161
- is_private: false,
5162
- overwrite: true,
5163
- });
5164
- }
5165
- else {
5166
- const session = await this.callAndTrace('storage.create_upload_session', {
5167
- object_key: objectKey,
5168
- size_bytes: stat.size,
5169
- content_type: contentType,
5170
- });
5171
- const uploadUrl = session.upload_url;
5172
- if (!uploadUrl)
5173
- throw new Error('No upload_url in session response');
5174
- this.trace('OUT', 'http.put.upload_url', { object_key: objectKey, size: stat.size });
5175
- const uploadResp = await fetch(uploadUrl, { method: 'PUT', body: fileData });
5176
- this.trace('OUT', uploadResp.ok ? 'http.put.upload_url.ok' : 'http.put.upload_url.error', { status: uploadResp.status });
5177
- if (!uploadResp.ok)
5178
- throw new Error(`HTTP upload failed: ${uploadResp.status}`);
5179
- await this.callAndTrace('storage.complete_upload', {
5180
- object_key: objectKey,
5181
- sha256,
5182
- content_type: contentType,
5183
- is_private: false,
5184
- size_bytes: stat.size,
5185
- });
5186
- }
5187
- const attachment = {
5188
- owner_aid: this._aid || '',
5189
- object_key: objectKey,
5190
- filename,
5191
- size_bytes: stat.size,
5192
- sha256,
5193
- content_type: contentType,
5194
- };
5635
+ const { attachment } = await uploadFileAndBuildPayload(this.client, this.config.aid, absPath, { as: 'file' });
5636
+ const { filename, size_bytes: size, content_type: contentType } = attachment;
5195
5637
  const filePayload = buildAunFilePayload({
5196
5638
  filename,
5197
- size: stat.size,
5639
+ size,
5198
5640
  contentType,
5199
- attachment,
5641
+ attachment: { ...attachment },
5200
5642
  context,
5201
5643
  });
5202
5644
  const fileTargetAid = channelId;
@@ -5282,7 +5724,7 @@ export class AUNChannel {
5282
5724
  logger.error(`${this.logPrefix()} AUN accepted the durable file but its receipt could not be persisted: id=${entry.id} mid=${sentMid} error=${detail}`);
5283
5725
  return { status: 'permanent', error: detail, code: 'OUTBOX_RECEIPT_CHECKPOINT_FAILED' };
5284
5726
  }
5285
- logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
5727
+ logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(size)}) → ${channelId}`);
5286
5728
  const fileText = filePayload.text;
5287
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 }) });
5288
5730
  const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
@@ -5401,11 +5843,12 @@ export class AUNChannel {
5401
5843
  // corrected Owner route was delivered.
5402
5844
  if (entry.channelId !== owner || entry.delivery?.chatType !== 'private') {
5403
5845
  delete repaired.deliveryReceipt;
5846
+ delete repaired.deliveryResult;
5847
+ delete repaired.terminal;
5848
+ delete repaired.lastError;
5849
+ delete repaired.lastErrorCode;
5850
+ delete repaired.attempts;
5404
5851
  }
5405
- delete repaired.terminal;
5406
- delete repaired.lastError;
5407
- delete repaired.lastErrorCode;
5408
- delete repaired.attempts;
5409
5852
  if (outbox.replaceIfRouteMatches(aid, entry, repaired) !== 'replaced')
5410
5853
  continue;
5411
5854
  logger.warn(`${this.logPrefix()} Repaired bootstrap outbox route to private Owner delivery: entry=${entry.id} owner=${owner}`);
@@ -5445,17 +5888,21 @@ export class AUNChannel {
5445
5888
  notice: 'notice',
5446
5889
  requires_action: 'requires_action',
5447
5890
  };
5448
- const statusPayload = {
5891
+ const statusPayload = withAunAttention({
5449
5892
  type: 'task.status',
5450
5893
  status: stateMap[status] ?? status,
5451
5894
  task_id: taskId,
5452
5895
  session_id: sessionId,
5453
5896
  severity,
5454
- terminal: ['done', 'interrupted', 'error', 'timeout'].includes(status),
5455
- };
5897
+ });
5456
5898
  const metadataInput = extraMeta ? { ...extraMeta } : undefined;
5457
5899
  if (metadataInput)
5458
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;
5459
5906
  const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
5460
5907
  if (metadata && Object.keys(metadata).length > 0)
5461
5908
  statusPayload.metadata = metadata;
@@ -5508,10 +5955,6 @@ export class AUNChannel {
5508
5955
  // returns until the channel reconnects.
5509
5956
  const delivery = this.requireDelivery(channelId, context);
5510
5957
  context = this.withDelivery(context, delivery);
5511
- if (!this.connected || !this.client) {
5512
- logger.debug(`${this.logPrefix()} Custom payload skipped while disconnected: channel=${channelId}`);
5513
- return;
5514
- }
5515
5958
  // SDK 0.3.0 E2EE requires payload to be an object
5516
5959
  let payloadObj;
5517
5960
  try {
@@ -5522,9 +5965,20 @@ export class AUNChannel {
5522
5965
  catch {
5523
5966
  payloadObj = { text: payload };
5524
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
+ }
5525
5978
  await this.sendStructuredOrThrow(channelId, payloadObj, context);
5526
5979
  }
5527
5980
  async disconnect() {
5981
+ this.managedMenuClient.close();
5528
5982
  this.intentionalDisconnect = true;
5529
5983
  if (this.reconnectTimer) {
5530
5984
  clearTimeout(this.reconnectTimer);
@@ -5549,6 +6003,7 @@ export class AUNChannel {
5549
6003
  this.store = null;
5550
6004
  }
5551
6005
  this.connected = false;
6006
+ this.clearQueuedObserverSends('intentional disconnect');
5552
6007
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
5553
6008
  appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
5554
6009
  this.setAidStatus('disabled');
@@ -5599,15 +6054,15 @@ export class AUNChannel {
5599
6054
  };
5600
6055
  }
5601
6056
  /** 读取本地 agent.md 中的 name(用于身份上下文展示),若本地不存在则尝试远程拉取 */
5602
- loadSelfName(aid) {
6057
+ loadSelfName(aid, aunPath) {
5603
6058
  const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
5604
- const name = resolveAgentDisplayName(aidName);
6059
+ const name = resolveAgentDisplayName(aidName, { aunPath });
5605
6060
  if (!name)
5606
- void this.fetchAndCacheSelfName(aidName);
6061
+ void this.fetchAndCacheSelfName(aidName, aunPath);
5607
6062
  return name;
5608
6063
  }
5609
- async fetchAndCacheSelfName(aidName) {
5610
- const name = await refreshAgentDisplayName(aidName);
6064
+ async fetchAndCacheSelfName(aidName, aunPath) {
6065
+ const name = await refreshAgentDisplayName(aidName, { aunPath });
5611
6066
  if (name) {
5612
6067
  this._selfName = name;
5613
6068
  if (this.aidStatsCollector)
@@ -5624,13 +6079,20 @@ export class AUNChannel {
5624
6079
  if (!this.client)
5625
6080
  return { type: null };
5626
6081
  try {
6082
+ const client = this.client;
6083
+ const revision = this.peerInfoRevision;
5627
6084
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
5628
- const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false);
5629
- const type = identity.type === 'human' ? 'human' : 'ai';
6085
+ const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false, this.config.keystorePath || resolveRoot());
6086
+ const type = identity.type === 'human' ? 'human' : 'agent';
5630
6087
  const name = identity.name || undefined;
5631
6088
  const info = { type, name };
5632
- this.peerInfoCache.set(aid, info);
5633
- 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
+ }
5634
6096
  return info;
5635
6097
  }
5636
6098
  catch (e) {
@@ -5652,13 +6114,20 @@ export class AUNChannel {
5652
6114
  if (!this.store)
5653
6115
  throw new Error('not connected');
5654
6116
  const { agentmdPut } = await import('../aun/aid/agentmd.js');
5655
- 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
+ });
5656
6122
  }
5657
6123
  async downloadAgentMd(aid) {
5658
6124
  if (!this.store)
5659
6125
  throw new Error('not connected');
5660
6126
  const { agentmdSync } = await import('../aun/aid/agentmd.js');
5661
- 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
+ });
5662
6131
  return result.content ?? '';
5663
6132
  }
5664
6133
  /**
@@ -5757,7 +6226,7 @@ export class AUNChannelPlugin {
5757
6226
  const dedupeOperationId = payload.kind === 'result.text' && payload.isFinal === false
5758
6227
  ? undefined
5759
6228
  : operationId;
5760
- const replyCtx = outboundCausation
6229
+ let replyCtx = outboundCausation
5761
6230
  ? {
5762
6231
  ...(envelope.replyContext ?? {}),
5763
6232
  delivery,
@@ -5766,6 +6235,13 @@ export class AUNChannelPlugin {
5766
6235
  : (envelope.replyContext || envelope.delivery)
5767
6236
  ? { ...(envelope.replyContext ?? {}), delivery }
5768
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 };
5769
6245
  if (outboundCausation) {
5770
6246
  recordCausationSpan(outboundCausation, 'message.outbound', {
5771
6247
  status: 'started',
@@ -5777,12 +6253,34 @@ export class AUNChannelPlugin {
5777
6253
  case 'result.text':
5778
6254
  case 'command.result':
5779
6255
  case 'command.error': {
6256
+ // Group command feedback is control traffic, not model input for
6257
+ // the other agents in the conversation.
6258
+ if (delivery.chatType === 'group'
6259
+ && (payload.kind === 'command.result' || payload.kind === 'command.error')) {
6260
+ const isError = payload.kind === 'command.error';
6261
+ const feedback = {
6262
+ type: isError ? 'error' : 'notice',
6263
+ ...taskBase(),
6264
+ subtype: payload.kind,
6265
+ text: payload.text,
6266
+ ...(isError
6267
+ ? { message: payload.text, user_message: payload.text, recoverable: true }
6268
+ : { severity: 'info' }),
6269
+ };
6270
+ for (const key of ['format', 'reason', 'reasonCode', 'nextStep']) {
6271
+ setIfDefined(feedback, key, payload[key]);
6272
+ }
6273
+ await channel.sendReliableStructured(channelId, feedback, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
6274
+ return;
6275
+ }
5780
6276
  const sendCtx = {
5781
6277
  ...(replyCtx ?? {}),
5782
6278
  metadata: {
5783
6279
  ...Object.fromEntries(Object.entries(replyCtx?.metadata ?? {})
5784
6280
  .filter(([key]) => key !== 'operationId')),
5785
6281
  ...(dedupeOperationId ? { operationId: dedupeOperationId } : {}),
6282
+ ...(payload.kind === 'result.text' && payload.metadata?.forkAnchor
6283
+ ? { forkAnchor: payload.metadata.forkAnchor } : {}),
5786
6284
  },
5787
6285
  };
5788
6286
  if (payload.kind === 'result.text' && payload.isFinal)
@@ -5810,7 +6308,7 @@ export class AUNChannelPlugin {
5810
6308
  messages: [{
5811
6309
  messageId: result.messageId,
5812
6310
  partIndex: 0,
5813
- ...(sendCtx.threadId ? { threadId: sendCtx.threadId } : {}),
6311
+ ...(typeof sendCtx.threadId === 'string' ? { threadId: sendCtx.threadId } : {}),
5814
6312
  }],
5815
6313
  };
5816
6314
  }
@@ -5846,14 +6344,13 @@ export class AUNChannelPlugin {
5846
6344
  }
5847
6345
  case 'system.error': {
5848
6346
  const errorPayload = {
5849
- type: 'error',
6347
+ type: payload.recoverable === true ? 'notice' : 'error',
5850
6348
  ...taskBase(),
5851
6349
  subtype: payload.subtype,
5852
6350
  text: payload.text,
5853
- message: payload.text,
5854
- user_message: payload.text,
5855
- recoverable: payload.recoverable === true,
5856
- terminal: payload.recoverable !== true,
6351
+ ...(payload.recoverable === true
6352
+ ? { severity: 'warn' }
6353
+ : { message: payload.text, user_message: payload.text, recoverable: false }),
5857
6354
  };
5858
6355
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
5859
6356
  return;
@@ -5866,7 +6363,6 @@ export class AUNChannelPlugin {
5866
6363
  text: payload.text,
5867
6364
  message: payload.text,
5868
6365
  user_message: payload.text,
5869
- terminal: true,
5870
6366
  };
5871
6367
  await channel.sendReliableStructured(channelId, errorPayload, { ...(replyCtx ?? {}), suppressPeerMention: true }, payload.text);
5872
6368
  return;
@@ -5875,8 +6371,27 @@ export class AUNChannelPlugin {
5875
6371
  const fileCtx = payload.correlationId
5876
6372
  ? { ...(replyCtx ?? {}), metadata: { ...(replyCtx?.metadata ?? {}), correlationId: payload.correlationId } }
5877
6373
  : replyCtx;
5878
- await channel.sendFile(channelId, payload.filePath, fileCtx);
5879
- 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
+ };
5880
6395
  }
5881
6396
  case 'result.image': {
5882
6397
  await channel.sendImage(channelId, payload.data, payload.mimeType, payload.alt, replyCtx);
@@ -6020,13 +6535,13 @@ export class AUNChannelPlugin {
6020
6535
  };
6021
6536
  if (req.kind.kind === 'action') {
6022
6537
  const action = req.kind;
6023
- const aunCard = {
6538
+ const aunCard = withAunAttention({
6024
6539
  type: 'action_card',
6025
6540
  title: action.title,
6026
6541
  actions: action.buttons.map(btn => ({
6027
6542
  label: btn.label, value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
6028
6543
  })),
6029
- };
6544
+ });
6030
6545
  // AUN action_card 以 compose behavior 声明自由文本动作。新版客户端
6031
6546
  // 在卡片内采集并确认;旧版客户端可继续从主编辑器采集。两者都结构化
6032
6547
  // 回传 action_card_reply{behavior:"compose"},Core 不拦截普通消息队列。
@@ -6044,6 +6559,7 @@ export class AUNChannelPlugin {
6044
6559
  const result = await channel.sendContentPayload(channelId, aunCard, {
6045
6560
  contentKind: 'card',
6046
6561
  context: replyCtx,
6562
+ dedupeKey: req.id.startsWith('gir-') ? operationId : undefined,
6047
6563
  logText: action.title ? `[card] ${action.title}` : '[card]',
6048
6564
  postSend: {
6049
6565
  type: 'register_interaction_card',
@@ -6058,7 +6574,7 @@ export class AUNChannelPlugin {
6058
6574
  }
6059
6575
  else if (req.kind.kind === 'command-card') {
6060
6576
  const card = req.kind;
6061
- const aunCard = {
6577
+ const aunCard = withAunAttention({
6062
6578
  type: 'action_card',
6063
6579
  title: card.title,
6064
6580
  actions: card.buttons.map(btn => {
@@ -6071,7 +6587,7 @@ export class AUNChannelPlugin {
6071
6587
  setIfDefined(action, 'disabled', btn.disabled);
6072
6588
  return action;
6073
6589
  }),
6074
- };
6590
+ });
6075
6591
  if (card.body) {
6076
6592
  aunCard.text = card.body;
6077
6593
  setIfDefined(aunCard, 'format', card.bodyFormat);
@@ -6162,6 +6678,14 @@ export class AUNChannelPlugin {
6162
6678
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
6163
6679
  handler(aunOptsToInbound(opts, adapter.channelName, channelType));
6164
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
+ });
6165
6689
  },
6166
6690
  registerHooks(hookCtx) {
6167
6691
  channel.setEventBus(hookCtx.eventBus);