evolcore 0.0.35 → 0.0.37

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 (224) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +177 -85
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +115 -36
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
@@ -3,6 +3,7 @@ import { sanitizeMenuLogValue } from '../core/command/menu-protocol.js';
3
3
  import crypto from 'crypto';
4
4
  import { performance } from 'node:perf_hooks';
5
5
  import fs from 'fs';
6
+ import { ownerResolver } from '../core/ownership/owner-resolver.js';
6
7
  import path from 'path';
7
8
  import os from 'os';
8
9
  import { logger, localTimestamp } from '../utils/logger.js';
@@ -10,9 +11,10 @@ import { LogWriter } from '../utils/log-writer.js';
10
11
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
11
12
  import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
12
13
  import { resolvePaths, agentDir as agentDirPath, resolveRoot, channelStatePath } from '../paths.js';
14
+ import { getAunRequestRegistry, MAX_AUN_VOLATILE_TTL_SECONDS } from '../core/message/aun-request-registry.js';
13
15
  import { saveToUploads, saveRelationAttachment, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
14
16
  import { appendAidEvent } from '../utils/instance-registry.js';
15
- import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, findMessageLogOperationMessageId, hasMessageLogOperation } from '../core/message/message-log.js';
17
+ import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, findLatestMessageLogOperation, findMessageLogOperationMessageId, hasMessageLogOperation } from '../core/message/message-log.js';
16
18
  import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
17
19
  import { chatDirPath } from '../core/session/session-fs-store.js';
18
20
  import { readTopicLabel, readTopicStatus } from '../core/session/session-mapper.js';
@@ -21,6 +23,7 @@ import { appendAidLifecycle } from '../aun/aid/identity.js';
21
23
  import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
22
24
  import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
23
25
  import { loadAgent } from '../config-store.js';
26
+ import { configuredOwnerList } from '../config/owner-config.js';
24
27
  import { resolveAgentLifecycle } from '../config/lifecycle.js';
25
28
  import { resolveEffective } from '../config/config-manager.js';
26
29
  import { isManagementRole } from '../config/builtin-roles.js';
@@ -42,6 +45,7 @@ import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOu
42
45
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
43
46
  import { aunOutboundAttention, normalizeAunMentionEntries, parseAunAttention, withAunAttention, } from '../aun/msg/mention-schema.js';
44
47
  import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
48
+ import { isTopicThreadId } from '../core/thread/topic-id.js';
45
49
  import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
46
50
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
47
51
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
@@ -331,7 +335,7 @@ export function buildAunFilePayload(params) {
331
335
  text: `📎 ${params.filename} (${formatSize(params.size)})`,
332
336
  attachments: [params.attachment],
333
337
  };
334
- if (params.context?.threadId)
338
+ if (isTopicThreadId(params.context?.threadId))
335
339
  payload.thread_id = params.context.threadId;
336
340
  if (params.context?.replyToMessageId)
337
341
  payload.ref_message_id = params.context.replyToMessageId;
@@ -469,6 +473,12 @@ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
469
473
  const retryByName = /(?:timeout|connection|rate.?limit|temporar(?:y|ily)|unavailable|network|socket)/i.test(details.name ?? '');
470
474
  const relayTargetMissing = text.includes('relay_target_not_found') || text.includes('relay target not found');
471
475
  const rpcHandlerTimeout = numericCode === -32004 && /rpc handler timeout/i.test(text);
476
+ // -32013 is otherwise permanent, but "client signature cert unavailable" is
477
+ // the gateway failing to resolve the signer's certificate while the local
478
+ // cert is valid and auth succeeded. It clears server-side, so keep the
479
+ // durable entry and retry until TTL (same idea as the SDK's transient
480
+ // "certificate not loaded").
481
+ const signatureCertUnavailable = numericCode === -32013 && /signature cert(?:ificate)? unavailable/i.test(text);
472
482
  const groupStateCode = numericCode === -33002 || numericCode === -33003;
473
483
  const groupClosed = groupStateCode && /\b(?:closed|dissolved|disbanded)\b|解散|已关闭/i.test(text);
474
484
  const groupSuspended = groupStateCode && /\bsuspend(?:ed)?\b|暂停/i.test(text);
@@ -497,7 +507,7 @@ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
497
507
  const unconfirmedDelivery = missingReceipt
498
508
  && (acceptedDispatch || ambiguousAccepted || ambiguousNoReceipt || successfulHttpWithoutReceipt);
499
509
  let status;
500
- if ((AUN_PERMANENT_ERROR_CODES.has(numericCode ?? Number.NaN) && !rpcHandlerTimeout)
510
+ if ((AUN_PERMANENT_ERROR_CODES.has(numericCode ?? Number.NaN) && !rpcHandlerTimeout && !signatureCertUnavailable)
501
511
  || permanentByText
502
512
  || AUN_PERMANENT_STRING_CODES.has(stringCode)
503
513
  || rejectedDispatch
@@ -515,6 +525,7 @@ export function classifyAunSendFailure(value, fallback = 'AUN send failed') {
515
525
  else if (AUN_RETRYABLE_ERROR_CODES.has(numericCode ?? Number.NaN)
516
526
  || transientHttpStatus
517
527
  || rpcHandlerTimeout
528
+ || signatureCertUnavailable
518
529
  || AUN_RETRYABLE_STRING_CODES.has(stringCode)
519
530
  || retryByName
520
531
  || relayTargetMissing
@@ -680,7 +691,7 @@ export class AUNChannel {
680
691
  traceWriter = null;
681
692
  eventBus = null;
682
693
  queuedHandler = null;
683
- /** `topic-renamed` 消费器,由 runtime 注入(见 setThreadRenameHandler)。 */
694
+ /** `topic.renamed` 事件消费器,由 runtime 注入(见 setThreadRenameHandler)。 */
684
695
  threadRenameHandler = null;
685
696
  groupInviteHandler;
686
697
  groupInviteConnectedHandler;
@@ -1412,19 +1423,33 @@ export class AUNChannel {
1412
1423
  return undefined;
1413
1424
  }
1414
1425
  /**
1415
- * 处理对端 `topic-renamed` 通知:校验字段后交给注入的改名消费器落索引,
1426
+ * 处理对端 `event/topic.renamed` 通知:校验字段后交给注入的改名消费器落索引,
1416
1427
  * 成功后发 `thread:renamed` 让前端刷新。全程不进入 Agent 消息队列。
1417
1428
  * 通知是尽力而为的,任何失败只记日志,不影响后续消息处理。
1418
1429
  */
1419
1430
  applyInboundTopicRename(params) {
1420
1431
  const payload = params.payload && typeof params.payload === 'object' ? params.payload : null;
1421
- const threadId = typeof payload?.threadId === 'string' ? payload.threadId : undefined;
1422
- const name = this.sanitizeTopicName(payload?.topicName);
1423
- const nameUpdatedAt = typeof payload?.nameUpdatedAt === 'number' && Number.isFinite(payload.nameUpdatedAt)
1424
- ? payload.nameUpdatedAt : undefined;
1432
+ const data = payload?.data && typeof payload.data === 'object'
1433
+ ? payload.data : undefined;
1434
+ // 未知 schema 只忽略业务应用,不能按聊天文本处理。
1435
+ if (data?.schema_version !== 1) {
1436
+ logger.debug(`${this.logPrefix()} topic.renamed ignored: schema_version=${String(data?.schema_version)}`);
1437
+ return;
1438
+ }
1439
+ if (payload?.occurred_at !== undefined && data?.name_updated_at !== undefined
1440
+ && payload.occurred_at !== data.name_updated_at) {
1441
+ logger.debug(`${this.logPrefix()} topic.renamed ignored: occurred_at does not match name_updated_at`);
1442
+ return;
1443
+ }
1444
+ const threadId = typeof payload?.thread_id === 'string' ? payload.thread_id.trim() || undefined : undefined;
1445
+ const name = this.sanitizeTopicName(data?.name);
1446
+ const nameUpdatedAt = typeof data?.name_updated_at === 'number' && Number.isFinite(data.name_updated_at)
1447
+ ? data.name_updated_at : undefined;
1425
1448
  const applier = this.threadRenameHandler;
1449
+ // 主话题与话题的改名是同一套逻辑,区别只在 id 形态:主话题 id(`main:<a>:<b>`)
1450
+ // 由 applyRemoteRename 落到主会话上,不进话题索引。
1426
1451
  if (!applier || !threadId || !name || nameUpdatedAt === undefined || !params.peer) {
1427
- logger.debug(`${this.logPrefix()} topic-renamed ignored: peer=${params.peer || '<none>'} thread=${threadId || '<none>'} hasName=${!!name}`);
1452
+ logger.debug(`${this.logPrefix()} topic.renamed ignored: peer=${params.peer || '<none>'} thread=${threadId || '<none>'} hasName=${!!name}`);
1428
1453
  return;
1429
1454
  }
1430
1455
  void applier({
@@ -1432,7 +1457,7 @@ export class AUNChannel {
1432
1457
  threadId, name, nameUpdatedAt,
1433
1458
  }).then(result => {
1434
1459
  if (!result.applied) {
1435
- logger.debug(`${this.logPrefix()} topic-renamed not applied (${result.reason ?? 'unknown'}): thread=${threadId}`);
1460
+ logger.debug(`${this.logPrefix()} topic.renamed not applied (${result.reason ?? 'unknown'}): thread=${threadId}`);
1436
1461
  return;
1437
1462
  }
1438
1463
  this.eventBus?.publish?.({
@@ -1444,9 +1469,9 @@ export class AUNChannel {
1444
1469
  nameUpdatedAt,
1445
1470
  by: params.senderAid ?? '',
1446
1471
  });
1447
- logger.info(`${this.logPrefix()} topic-renamed applied: thread=${threadId} name=${JSON.stringify(name)}`);
1472
+ logger.info(`${this.logPrefix()} topic.renamed applied: thread=${threadId} name=${JSON.stringify(name)}`);
1448
1473
  }).catch(error => {
1449
- logger.warn(`${this.logPrefix()} topic-renamed failed: ${error instanceof Error ? error.message : String(error)}`);
1474
+ logger.warn(`${this.logPrefix()} topic.renamed failed: ${error instanceof Error ? error.message : String(error)}`);
1450
1475
  });
1451
1476
  }
1452
1477
  buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, label, topicStatus) {
@@ -1793,6 +1818,7 @@ export class AUNChannel {
1793
1818
  this.observerGroupMemberFetches.clear();
1794
1819
  // Clean up existing client if any
1795
1820
  if (this.client) {
1821
+ ownerResolver.detachClient(this.config.aid, this.client);
1796
1822
  this.trace('OUT', 'client.close', { reason: 'initClient' });
1797
1823
  try {
1798
1824
  await this.client.close();
@@ -1830,6 +1856,15 @@ export class AUNChannel {
1830
1856
  this.store = store;
1831
1857
  const client = await loadClient(store, aidName);
1832
1858
  this.client = client;
1859
+ const localConfig = loadAgent(aidName);
1860
+ ownerResolver.setAgentConfiguredOwners(aidName, configuredOwnerList(localConfig));
1861
+ ownerResolver.attachClient(aidName, client);
1862
+ try {
1863
+ await this.config.onClientReady?.(client);
1864
+ }
1865
+ catch (error) {
1866
+ logger.warn(`${this.logPrefix()} client-ready hook failed: ${error instanceof Error ? error.message : String(error)}`);
1867
+ }
1833
1868
  // A reconnect replaces the SDK client. Late events from the retired
1834
1869
  // instance must not mutate the new instance's health or message state.
1835
1870
  const isCurrentClient = () => this.client === client;
@@ -2099,12 +2134,13 @@ export class AUNChannel {
2099
2134
  logger.warn(`${this.logPrefix()} agent config not found for ${aidName}, skipping welcome message`);
2100
2135
  return false;
2101
2136
  }
2102
- const owners = Array.from(new Set(agentConfig.owners ?? []));
2137
+ await ownerResolver.refresh(aidName);
2138
+ const owners = ownerResolver.owners(aidName);
2103
2139
  if (owners.length === 0) {
2104
2140
  logger.info(`${this.logPrefix()} No owner configured, skipping post-bootstrap welcome message`);
2105
2141
  return false;
2106
2142
  }
2107
- this.repairBootstrapRoutes();
2143
+ await this.repairBootstrapRoutes();
2108
2144
  // Bootstrap welcomes target the configured personal Owner AID. They are
2109
2145
  // not replies to an inbound group message, so their route is always
2110
2146
  // private and must not be inferred with group.get_info.
@@ -2156,7 +2192,7 @@ export class AUNChannel {
2156
2192
  return this.reconcilePostBootstrapWelcome();
2157
2193
  }
2158
2194
  async hasPendingPostBootstrapWelcome() {
2159
- this.repairBootstrapRoutes();
2195
+ await this.repairBootstrapRoutes();
2160
2196
  const aid = this.config.aid.replace(/^@/, '');
2161
2197
  return hasPendingPostBootstrapWelcomeOutbox(aid);
2162
2198
  }
@@ -2189,12 +2225,13 @@ export class AUNChannel {
2189
2225
  * already-active agent from manufacturing another welcome message.
2190
2226
  */
2191
2227
  async reconcilePostBootstrapWelcome() {
2192
- this.repairBootstrapRoutes();
2228
+ await this.repairBootstrapRoutes();
2193
2229
  const configuredAid = this.config.aid;
2194
2230
  const aid = configuredAid.startsWith('@') ? configuredAid.slice(1) : configuredAid;
2195
2231
  const entries = findPostBootstrapWelcomeOutbox(aid);
2196
2232
  const agentConfig = loadAgent(aid);
2197
- const owners = agentConfig?.owners ?? [];
2233
+ await ownerResolver.refresh(aid);
2234
+ const owners = ownerResolver.owners(aid);
2198
2235
  if (entries.length === 0)
2199
2236
  return postBootstrapWelcomeStatus(aid, owners);
2200
2237
  if (!agentConfig || resolveAgentLifecycle(agentConfig) !== 'active') {
@@ -2499,6 +2536,18 @@ export class AUNChannel {
2499
2536
  return;
2500
2537
  const protectedHeaders = verifiedProtectedHeaders(msg, env);
2501
2538
  const payload = msg.payload ?? '';
2539
+ const responseRecord = getAunRequestRegistry().resolveIncoming({
2540
+ selfAid: this._aid || this.config.aid,
2541
+ fromAid,
2542
+ messageId,
2543
+ payload,
2544
+ });
2545
+ if (responseRecord) {
2546
+ logger.info(`${this.logPrefix()} AUN request response matched: request_id=${responseRecord.requestId} mid=${messageId}`);
2547
+ const responseLog = classifyAunPayloadForLog(payload);
2548
+ this.appendInboundPayloadJsonl(fromAid, fromAid, messageId, !!env.encrypted, false, responseLog, undefined, receivedAt, seq);
2549
+ return;
2550
+ }
2502
2551
  if (this.managedMenuClient.accept(fromAid, payload, !!env.encrypted)) {
2503
2552
  return;
2504
2553
  }
@@ -2577,8 +2626,8 @@ export class AUNChannel {
2577
2626
  if (p2pPayloadType === 'text') {
2578
2627
  import('../utils/msg-diag.js').then(m => m.diagLog('aun_in', { aid: this.config.aid, from: fromAid, to: this.config.aid, msgId: messageId, text })).catch(() => { });
2579
2628
  }
2580
- // topic-renamed:对端话题改名通知。只更新本端话题索引,不进 Agent 队列。
2581
- if (p2pPayloadType === 'topic-renamed') {
2629
+ // event.topic.renamed:对端话题改名通知。只更新本端话题索引,不进 Agent 队列。
2630
+ if (p2pPayloadType === 'event' && payload?.event === 'topic.renamed') {
2582
2631
  this.applyInboundTopicRename({
2583
2632
  payload, selfAid: this.config.aid, peer: fromAid, peerKind: 'private',
2584
2633
  senderAid: fromAid,
@@ -2657,7 +2706,7 @@ export class AUNChannel {
2657
2706
  // payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
2658
2707
  if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
2659
2708
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
2660
- logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
2709
+ logger.info(`${this.logPrefix()} P2P retained in generic inbound event stream (not Agent chat): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
2661
2710
  return;
2662
2711
  }
2663
2712
  const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
@@ -2711,13 +2760,23 @@ export class AUNChannel {
2711
2760
  const groupId = env.group_id ?? '';
2712
2761
  const senderAid = env.from ?? '';
2713
2762
  const payload = msg.payload ?? '';
2763
+ const responseRecord = getAunRequestRegistry().resolveIncoming({
2764
+ selfAid: this._aid || this.config.aid,
2765
+ fromAid: senderAid,
2766
+ messageId,
2767
+ payload,
2768
+ });
2769
+ if (responseRecord) {
2770
+ logger.info(`${this.logPrefix()} AUN group request response matched: request_id=${responseRecord.requestId} mid=${messageId}`);
2771
+ return;
2772
+ }
2714
2773
  const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
2715
2774
  if (inboundType === 'observer.forward' || (typeof inboundType === 'string' && inboundType.startsWith('observer.inject'))) {
2716
2775
  logger.debug(`${this.logPrefix()} Group dropped observer control envelope: mid=${messageId}`);
2717
2776
  return;
2718
2777
  }
2719
- // topic-renamed:群内话题改名通知。只更新本端索引,不进 Agent 队列。
2720
- if (inboundType === 'topic-renamed') {
2778
+ // event.topic.renamed:群内话题改名通知。只更新本端索引,不进 Agent 队列。
2779
+ if (inboundType === 'event' && payload?.event === 'topic.renamed') {
2721
2780
  this.applyInboundTopicRename({
2722
2781
  payload, selfAid: this.config.aid, peer: groupId || senderAid, peerKind: 'group',
2723
2782
  senderAid,
@@ -2806,7 +2865,7 @@ export class AUNChannel {
2806
2865
  }
2807
2866
  if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
2808
2867
  this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId, receivedAt, seq);
2809
- logger.info(`${this.logPrefix()} Group dropped (type deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
2868
+ logger.info(`${this.logPrefix()} Group payload retained in generic inbound event stream (not Agent chat): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
2810
2869
  return;
2811
2870
  }
2812
2871
  }
@@ -3271,7 +3330,7 @@ export class AUNChannel {
3271
3330
  * result 为 SDK message.send / group.send 的 SendResult(已 attach envelope + payload)。
3272
3331
  * 若对端本身是 owner,排除该 owner(不把"回复 A"转发给 A 自己)。
3273
3332
  */
3274
- forwardOutbound(result) {
3333
+ forwardOutbound(result, outboundDebug) {
3275
3334
  if (!this.connected || !this.client)
3276
3335
  return Promise.resolve();
3277
3336
  const outboundType = payloadTypeForObserver(result);
@@ -3292,13 +3351,13 @@ export class AUNChannel {
3292
3351
  if (candidateOwners.length === 0)
3293
3352
  return Promise.resolve();
3294
3353
  if (typeof env.group_id !== 'string' || !env.group_id.trim()) {
3295
- this.emitForward('outbound', result, candidateOwners);
3354
+ this.emitForward('outbound', result, candidateOwners, outboundDebug);
3296
3355
  return Promise.resolve();
3297
3356
  }
3298
3357
  return this.observerRecipients(candidateOwners, env)
3299
3358
  .then(recipientOwners => {
3300
3359
  if (recipientOwners.length > 0)
3301
- this.emitForward('outbound', result, recipientOwners);
3360
+ this.emitForward('outbound', result, recipientOwners, outboundDebug);
3302
3361
  })
3303
3362
  .catch(error => logger.debug(`${this.logPrefix()} observer outbound recipient lookup failed: ${error}`));
3304
3363
  }
@@ -3306,14 +3365,17 @@ export class AUNChannel {
3306
3365
  * 实际投递 observer.forward 给每个 owner,使用 daemon 的全局 AUN 默认加密设置。
3307
3366
  * original 为 SDK 给到的原始对象(入站回调 data / 出站 SendResult),整体透传,
3308
3367
  * 不挑字段、不改字段——SDK 加任何字段都会自动一并转发给 owner。
3368
+ * outboundDebug 是出站时的 payload.debug,SDK SendResult 里没有,需要从调用方透传。
3309
3369
  */
3310
- emitForward(direction, original, owners) {
3370
+ emitForward(direction, original, owners, outboundDebug) {
3311
3371
  const forwardPayload = {
3312
3372
  type: 'observer.forward',
3313
3373
  direction,
3314
3374
  agent_aid: this.config.aid,
3315
3375
  original,
3316
3376
  };
3377
+ if (outboundDebug)
3378
+ forwardPayload.debug = outboundDebug;
3317
3379
  const encrypt = this.shouldEncrypt('observer.forward');
3318
3380
  for (const ownerAid of owners) {
3319
3381
  this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt }, { lane: 'observer' })
@@ -3341,7 +3403,7 @@ export class AUNChannel {
3341
3403
  const payload = {
3342
3404
  type: 'observer.inject.status', id: hintId,
3343
3405
  target: { channel_id: target.channelId, chat_type: target.chatType,
3344
- ...(target.threadId ? { thread_id: target.threadId } : {}) },
3406
+ ...(isTopicThreadId(target.threadId) ? { thread_id: target.threadId } : {}) },
3345
3407
  data: {
3346
3408
  status,
3347
3409
  hint_id: hintId,
@@ -3371,7 +3433,7 @@ export class AUNChannel {
3371
3433
  const requestAction = req.kind === 'reject' ? req.action : req.kind;
3372
3434
  const requestHintId = req.kind === 'add' ? req.id : 'targetId' in req ? req.targetId : undefined;
3373
3435
  const requestTarget = 'channelId' in req && req.channelId
3374
- ? { channel_id: req.channelId, chat_type: req.chatType ?? 'private', ...(req.threadId ? { thread_id: req.threadId } : {}) }
3436
+ ? { channel_id: req.channelId, chat_type: req.chatType ?? 'private', ...(isTopicThreadId(req.threadId) ? { thread_id: req.threadId } : {}) }
3375
3437
  : undefined;
3376
3438
  const ackData = (status, hintId) => ({
3377
3439
  status,
@@ -3793,6 +3855,7 @@ export class AUNChannel {
3793
3855
  }
3794
3856
  // Force close SDK client,中断它内部的重连循环
3795
3857
  if (this.client) {
3858
+ ownerResolver.detachClient(this.config.aid, this.client);
3796
3859
  this.trace('OUT', 'client.close', { reason: `takeover_${reason}` });
3797
3860
  this.client.close().catch(() => { });
3798
3861
  this.client = null;
@@ -3817,6 +3880,7 @@ export class AUNChannel {
3817
3880
  if (gen !== this.reconnectGeneration) {
3818
3881
  logger.info(`${this.logPrefix()} TS-layer reconnect (${reason}) succeeded but generation stale, closing stale client`);
3819
3882
  if (this.client) {
3883
+ ownerResolver.detachClient(this.config.aid, this.client);
3820
3884
  this.client.close().catch(() => { });
3821
3885
  this.client = null;
3822
3886
  }
@@ -3835,7 +3899,7 @@ export class AUNChannel {
3835
3899
  }
3836
3900
  // ── Public API (same interface as before) ───────────────────
3837
3901
  /**
3838
- * 对端发来的话题改名通知(`topic-renamed`)。由 runtime 注入,交给
3902
+ * 对端发来的话题改名通知(`event/topic.renamed`)。由 runtime 注入,交给
3839
3903
  * ThreadService.applyRemoteRename 落本地索引;不进入 Agent 消息队列。
3840
3904
  */
3841
3905
  setThreadRenameHandler(handler) {
@@ -3972,7 +4036,8 @@ export class AUNChannel {
3972
4036
  const finalPayload = this.stripUndefinedDeep(withAunAttention({ ...payload }, context?.attention));
3973
4037
  if (finalPayload.thread_id === '')
3974
4038
  delete finalPayload.thread_id;
3975
- if (context?.threadId && !finalPayload.thread_id)
4039
+ // 主话题 id(main:<a>:<b>)是本端身份,不上线;主话题的线上形态就是不带该字段。
4040
+ if (isTopicThreadId(context?.threadId) && !finalPayload.thread_id)
3976
4041
  finalPayload.thread_id = context.threadId;
3977
4042
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
3978
4043
  finalPayload.ref_message_id = context.replyToMessageId;
@@ -4125,6 +4190,79 @@ export class AUNChannel {
4125
4190
  await this.recallInteractionCard(messageId, channelId, card?.delivery);
4126
4191
  }
4127
4192
  }
4193
+ restoreInteraction(envelope, interaction, receipt) {
4194
+ if (interaction.kind.kind !== 'action')
4195
+ return undefined;
4196
+ const channelId = envelope.channelId;
4197
+ const delivery = this.requireDelivery(channelId, envelope.replyContext, envelope.delivery);
4198
+ const operationId = envelope.operationId ?? envelope.taskId;
4199
+ const entries = outbox.findInteractionCards(this.config.aid, interaction.id).filter(entry => entry.channelId === channelId && isDeliveryTarget(entry.delivery)
4200
+ && sameDeliveryTarget(entry.delivery, delivery));
4201
+ const messageIds = new Set();
4202
+ if (receipt?.status === 'sent' && receipt.operationId === operationId) {
4203
+ for (const message of receipt.messages) {
4204
+ if (message.messageId?.trim())
4205
+ messageIds.add(message.messageId);
4206
+ }
4207
+ if (receipt.messageId?.trim())
4208
+ messageIds.add(receipt.messageId);
4209
+ }
4210
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
4211
+ const loggedMessageId = findMessageLogOperationMessageId(chatDir, operationId)
4212
+ ?? findLatestMessageLogOperation(chatDir, `${interaction.id}-`)?.messageId;
4213
+ if (loggedMessageId)
4214
+ messageIds.add(loggedMessageId);
4215
+ for (const entry of entries) {
4216
+ const messageId = entry.deliveryReceipt?.messageId ?? entry.deliveryResult?.messageId;
4217
+ if (messageId?.trim())
4218
+ messageIds.add(messageId);
4219
+ }
4220
+ if (messageIds.size > 0) {
4221
+ for (const messageId of messageIds) {
4222
+ this.registerCardPostSend(messageId, {
4223
+ type: 'register_interaction_card', requestId: interaction.id,
4224
+ isCommandCard: false, initiatorAid: interaction.initiatorId,
4225
+ expiresAt: interaction.expiresAt, delivery,
4226
+ }, channelId, delivery, envelope.replyContext);
4227
+ }
4228
+ return sentReceipt(envelope, [...messageIds], envelope.replyContext?.threadId);
4229
+ }
4230
+ const pendingEntry = entries.find(entry => !entry.terminal && !entry.deliveryResult
4231
+ && !entry.deliveryReceipt);
4232
+ if (pendingEntry) {
4233
+ const state = outbox.deliveryState(this.config.aid, pendingEntry.id);
4234
+ if (state.status === 'queued') {
4235
+ return {
4236
+ status: 'queued', operationId, messages: [], outboxId: pendingEntry.id,
4237
+ };
4238
+ }
4239
+ if (state.status === 'failed') {
4240
+ return {
4241
+ status: 'failed', operationId, messages: [], outboxId: pendingEntry.id,
4242
+ error: state.error, code: state.code,
4243
+ };
4244
+ }
4245
+ }
4246
+ const failedEntry = entries.find(entry => entry.terminal);
4247
+ if (failedEntry?.terminal) {
4248
+ return {
4249
+ status: 'failed', operationId, messages: [], outboxId: failedEntry.id,
4250
+ error: failedEntry.terminal.error, code: failedEntry.terminal.code,
4251
+ };
4252
+ }
4253
+ if (receipt?.status === 'queued') {
4254
+ const state = outbox.deliveryState(this.config.aid, receipt.outboxId);
4255
+ if (state.status === 'queued')
4256
+ return receipt;
4257
+ if (state.status === 'failed') {
4258
+ return {
4259
+ status: 'failed', operationId, messages: [], outboxId: receipt.outboxId,
4260
+ error: state.error, code: state.code,
4261
+ };
4262
+ }
4263
+ }
4264
+ return undefined;
4265
+ }
4128
4266
  forgetInteractionCard(messageId, interactionId) {
4129
4267
  this.cardMessageIdMap.delete(messageId);
4130
4268
  const ids = this.interactionCardMessageIds.get(interactionId);
@@ -4225,7 +4363,7 @@ export class AUNChannel {
4225
4363
  const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
4226
4364
  const params = { payload: truncatedPayload, encrypt };
4227
4365
  const isMenuPayload = typeof truncatedPayload.type === 'string' && truncatedPayload.type.startsWith('menu.');
4228
- if (!isGroup && truncatedPayload.type === 'menu.response')
4366
+ if (!isGroup && ['menu.response', 'menu.event'].includes(truncatedPayload.type))
4229
4367
  params.to_device_id = context?.toDeviceId;
4230
4368
  if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
4231
4369
  params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
@@ -4419,7 +4557,7 @@ export class AUNChannel {
4419
4557
  base.agent_name = envelope.agentName;
4420
4558
  if (envelope.chatmode)
4421
4559
  base.chatmode = envelope.chatmode;
4422
- if (context?.threadId)
4560
+ if (isTopicThreadId(context?.threadId))
4423
4561
  base.thread_id = context.threadId;
4424
4562
  if (context?.peerId)
4425
4563
  base.initiator = context.peerId;
@@ -4670,11 +4808,17 @@ export class AUNChannel {
4670
4808
  return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
4671
4809
  }
4672
4810
  let finalPayload;
4811
+ const preserveRawPayload = args.rawPayload === true;
4673
4812
  try {
4674
- const validatedPayload = this.normalizeAunPayloadMentions(args.payload);
4813
+ const validatedPayload = preserveRawPayload ? { ...args.payload } : this.normalizeAunPayloadMentions(args.payload);
4675
4814
  finalPayload = this.stripUndefinedDeep(validatedPayload);
4676
- if (finalPayload.thread_id === '')
4815
+ if (!preserveRawPayload && finalPayload.thread_id === '')
4677
4816
  delete finalPayload.thread_id;
4817
+ // Ability call 省略 sent_at 时由 transport 在实际发送前补当前毫秒时间戳,Caller 无需自行取时间
4818
+ if (preserveRawPayload && finalPayload.type === 'ability' && finalPayload.role === 'call'
4819
+ && finalPayload.sent_at === undefined) {
4820
+ finalPayload.sent_at = Date.now();
4821
+ }
4678
4822
  }
4679
4823
  catch (e) {
4680
4824
  return {
@@ -4686,38 +4830,60 @@ export class AUNChannel {
4686
4830
  if (!this.connected || !this.client) {
4687
4831
  return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
4688
4832
  }
4689
- const info = await this.fetchPeerInfo(args.to);
4690
- // 与 p2p 发送路径共用同一判定:非人类 → proactive;人类需 owner/admin 才 interactive。
4691
- const { chatmodeForPeerIdentity } = await import('../core/message/peer-mode.js');
4692
- let peerRole = null;
4693
- try {
4694
- const { resolvePeerRoleDetail } = await import('../config/peer-role-resolver.js');
4695
- peerRole = resolvePeerRoleDetail({
4696
- selfAid: this.config.aid,
4697
- channelKey: `aun#${this.config.aid}#main`,
4698
- channelType: 'aun',
4699
- chatType: 'private',
4700
- actorId: args.to,
4701
- conversationId: args.to,
4702
- peerType: info.type ?? undefined,
4703
- }).effectiveRole;
4704
- }
4705
- catch { /* 角色解析失败按未知处理 */ }
4706
- const chatmode = chatmodeForPeerIdentity({ isMachine: info.type !== 'human', type: info.type ?? undefined }, peerRole);
4707
- if (!finalPayload.chatmode)
4833
+ if (args.ttl !== undefined && (!Number.isInteger(args.ttl) || args.ttl < 1 || args.ttl > MAX_AUN_VOLATILE_TTL_SECONDS)) {
4834
+ return {
4835
+ ok: false,
4836
+ error: `volatile AUN ttl must be an integer from 1 through ${MAX_AUN_VOLATILE_TTL_SECONDS} seconds`,
4837
+ code: 'INVALID_REQUEST_TTL',
4838
+ };
4839
+ }
4840
+ if (preserveRawPayload && jsonUtf8Size(finalPayload) > AUN_MAX_PAYLOAD_SIZE) {
4841
+ return {
4842
+ ok: false,
4843
+ error: `raw payload exceeds the ${AUN_MAX_PAYLOAD_SIZE} byte AUN payload limit`,
4844
+ code: 'PAYLOAD_TOO_LARGE',
4845
+ };
4846
+ }
4847
+ // Raw payload delivery (ability calls) is an application-level envelope:
4848
+ // peer metadata is neither needed nor safe to inject into it.
4849
+ const info = preserveRawPayload ? undefined : await this.fetchPeerInfo(args.to);
4850
+ let chatmode;
4851
+ if (info) {
4852
+ // 与 p2p 发送路径共用同一判定:非人类 → proactive;人类需 owner/admin 才 interactive。
4853
+ const { chatmodeForPeerIdentity } = await import('../core/message/peer-mode.js');
4854
+ let peerRole = null;
4855
+ try {
4856
+ const { resolvePeerRoleDetail } = await import('../config/peer-role-resolver.js');
4857
+ peerRole = resolvePeerRoleDetail({
4858
+ selfAid: this.config.aid,
4859
+ channelKey: `aun#${this.config.aid}#main`,
4860
+ channelType: 'aun',
4861
+ chatType: 'private',
4862
+ actorId: args.to,
4863
+ conversationId: args.to,
4864
+ peerType: info.type ?? undefined,
4865
+ }).effectiveRole;
4866
+ }
4867
+ catch { /* 角色解析失败按未知处理 */ }
4868
+ chatmode = chatmodeForPeerIdentity({ isMachine: info.type !== 'human', type: info.type ?? undefined }, peerRole);
4869
+ }
4870
+ if (chatmode && !finalPayload.chatmode)
4708
4871
  finalPayload.chatmode = chatmode;
4709
- const parentCausation = normalizeCausation(args.causation);
4872
+ const parentCausation = preserveRawPayload ? undefined : normalizeCausation(args.causation);
4710
4873
  const outboundCausation = parentCausation ? deriveCausation(parentCausation) : undefined;
4711
4874
  const causationRef = outboundCausation ? `caus_${crypto.randomUUID().replace(/-/g, '')}` : undefined;
4712
4875
  if (causationRef && outboundCausation) {
4713
4876
  finalPayload._evolcore_causation_ref = causationRef;
4714
4877
  registerAunCausation(causationRef, this.config.aid, args.to, outboundCausation);
4715
4878
  }
4879
+ if (args.debug)
4880
+ finalPayload.debug = { ...args.debug, sentAt: localTimestamp() };
4716
4881
  const encrypt = args.encrypt ?? this.shouldEncrypt(args.to);
4717
4882
  const sendParams = {
4718
4883
  to: args.to,
4719
- payload: this.truncatePayloadIfNeeded(finalPayload, 'msg-send-ipc'),
4884
+ payload: preserveRawPayload ? finalPayload : this.truncatePayloadIfNeeded(finalPayload, 'msg-send-ipc'),
4720
4885
  encrypt,
4886
+ ...(args.ttl !== undefined ? { ttl: args.ttl } : {}),
4721
4887
  };
4722
4888
  try {
4723
4889
  const result = await this.callAndTrace('message.send', sendParams);
@@ -4733,7 +4899,7 @@ export class AUNChannel {
4733
4899
  refs: { messageId, sessionId: args.log?.sessionId },
4734
4900
  });
4735
4901
  }
4736
- this.forwardOutbound(result);
4902
+ this.forwardOutbound(result, args.debug);
4737
4903
  let logWritten = false;
4738
4904
  if (args.log?.content !== undefined) {
4739
4905
  const classified = classifyAunPayloadForLog(finalPayload);
@@ -4831,6 +4997,8 @@ export class AUNChannel {
4831
4997
  if (!this.connected || !this.client) {
4832
4998
  return { ok: false, error: 'AUN channel is not connected', code: 'AUN_NOT_CONNECTED' };
4833
4999
  }
5000
+ if (args.debug)
5001
+ finalPayload.debug = { ...args.debug, sentAt: localTimestamp() };
4834
5002
  const encrypt = args.encrypt ?? this.shouldEncrypt(args.groupId);
4835
5003
  const result = await this.callAndTrace('group.send', {
4836
5004
  group_id: args.groupId,
@@ -4842,7 +5010,7 @@ export class AUNChannel {
4842
5010
  return { ok: false, error: `group.send returned no message_id: ${JSON.stringify(result)}`, code: 'MISSING_MESSAGE_ID' };
4843
5011
  }
4844
5012
  this.logAunSendAccepted('group.send', args.groupId, messageId, encrypt, result);
4845
- this.forwardOutbound(result);
5013
+ this.forwardOutbound(result, args.debug);
4846
5014
  let logWritten = false;
4847
5015
  if (args.log?.content !== undefined) {
4848
5016
  const classified = classifyAunPayloadForLog(finalPayload);
@@ -5000,7 +5168,7 @@ export class AUNChannel {
5000
5168
  if (extracted.length > 0)
5001
5169
  payload.mentions = extracted;
5002
5170
  }
5003
- if (context?.threadId)
5171
+ if (isTopicThreadId(context?.threadId))
5004
5172
  payload.thread_id = context.threadId;
5005
5173
  if (context?.replyToMessageId)
5006
5174
  payload.ref_message_id = context.replyToMessageId;
@@ -5021,6 +5189,10 @@ export class AUNChannel {
5021
5189
  // 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
5022
5190
  logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${finalText.length}`);
5023
5191
  import('../utils/msg-diag.js').then(m => m.diagLog('aun_out', { aid: this.config.aid, from: this.config.aid, to: channelId, msgId: payload.task_id ?? '', taskId: payload.task_id, text: finalText })).catch(() => { });
5192
+ // aun_out 是发送前打点;send_ok 只在网关返回 message_id 后记录,两者差值即发送失败次数
5193
+ const diagSendOk = (mid) => {
5194
+ import('../utils/msg-diag.js').then(m => m.diagLog('send_ok', { aid: this.config.aid, from: this.config.aid, to: channelId, msgId: mid, taskId: payload.task_id, text: finalText })).catch(() => { });
5195
+ };
5024
5196
  const isGroup = delivery.chatType === 'group';
5025
5197
  const targetAid = channelId;
5026
5198
  if (entry.deliveryReceipt) {
@@ -5073,7 +5245,7 @@ export class AUNChannel {
5073
5245
  transport: this.sendReceiptFromResult(result),
5074
5246
  });
5075
5247
  // Observer forward: outbound (group) — 原样转发 SDK SendResult(含 envelope + payload)
5076
- this.forwardOutbound(result);
5248
+ this.forwardOutbound(result, payload.debug);
5077
5249
  }
5078
5250
  }
5079
5251
  else {
@@ -5106,6 +5278,8 @@ export class AUNChannel {
5106
5278
  this.forwardOutbound(result);
5107
5279
  }
5108
5280
  }
5281
+ if (acceptedMessageId)
5282
+ diagSendOk(acceptedMessageId);
5109
5283
  return sentOutboxResult(acceptedMessageId);
5110
5284
  }
5111
5285
  catch (e) {
@@ -5170,6 +5344,8 @@ export class AUNChannel {
5170
5344
  });
5171
5345
  this.forwardOutbound(result);
5172
5346
  }
5347
+ if (acceptedMessageId)
5348
+ diagSendOk(acceptedMessageId);
5173
5349
  return sentOutboxResult(acceptedMessageId);
5174
5350
  }
5175
5351
  catch (e2) {
@@ -5326,7 +5502,8 @@ export class AUNChannel {
5326
5502
  from: selfAID,
5327
5503
  to: channelId,
5328
5504
  sessionId: context?.sessionId,
5329
- threadId: context?.threadId || null,
5505
+ // 日志用本地话题身份(含主话题规范 id),线上报文仍不带主话题字段。
5506
+ threadId: context?.localTopicId ?? (isTopicThreadId(context?.threadId) ? context.threadId : null),
5330
5507
  chatType: isGroup ? 'group' : 'private',
5331
5508
  groupId: isGroup ? channelId : null,
5332
5509
  msgId,
@@ -5382,7 +5559,7 @@ export class AUNChannel {
5382
5559
  : finalPayload;
5383
5560
  if (payloadWithChatmode.thread_id === '')
5384
5561
  delete payloadWithChatmode.thread_id;
5385
- if (context?.threadId && !payloadWithChatmode.thread_id)
5562
+ if (isTopicThreadId(context?.threadId) && !payloadWithChatmode.thread_id)
5386
5563
  payloadWithChatmode.thread_id = context.threadId;
5387
5564
  // Same diagnostic dictionary as text payloads (sessionId/agentSessionId/model).
5388
5565
  if (context?.debug)
@@ -5484,13 +5661,14 @@ export class AUNChannel {
5484
5661
  : this.shouldEncrypt(encryptTarget);
5485
5662
  if (finalPayload.thread_id === '')
5486
5663
  delete finalPayload.thread_id;
5487
- if (context?.threadId && !finalPayload.thread_id)
5664
+ // 主话题 id(main:<a>:<b>)是本端身份,不上线;主话题的线上形态就是不带该字段。
5665
+ if (isTopicThreadId(context?.threadId) && !finalPayload.thread_id)
5488
5666
  finalPayload.thread_id = context.threadId;
5489
5667
  if (context?.replyToMessageId && !finalPayload.ref_message_id)
5490
5668
  finalPayload.ref_message_id = context.replyToMessageId;
5491
5669
  const isMenuPayload = typeof finalPayload.type === 'string' && finalPayload.type.startsWith('menu.');
5492
5670
  const params = { payload: finalPayload, encrypt };
5493
- if (!isGroup && finalPayload.type === 'menu.response')
5671
+ if (!isGroup && ['menu.response', 'menu.event'].includes(finalPayload.type))
5494
5672
  params.to_device_id = context?.toDeviceId;
5495
5673
  if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
5496
5674
  params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
@@ -5932,7 +6110,7 @@ export class AUNChannel {
5932
6110
  async drainOutbox() {
5933
6111
  if (!this.connected || !this.client)
5934
6112
  return;
5935
- this.repairBootstrapRoutes();
6113
+ await this.repairBootstrapRoutes();
5936
6114
  const drainQueue = async (queue) => {
5937
6115
  if (!outbox.hasPending(this.config.aid, queue))
5938
6116
  return;
@@ -5975,9 +6153,10 @@ export class AUNChannel {
5975
6153
  await drainQueue('activity');
5976
6154
  }
5977
6155
  /** Repair bootstrap outbox entries to the configured personal Owner route. */
5978
- repairBootstrapRoutes() {
6156
+ async repairBootstrapRoutes() {
5979
6157
  const aid = this.config.aid.replace(/^@/, '');
5980
- const owners = Array.from(new Set(loadAgent(aid)?.owners ?? []));
6158
+ await ownerResolver.refresh(aid);
6159
+ const owners = ownerResolver.owners(aid);
5981
6160
  const entries = [
5982
6161
  ...outbox.findByDedupePrefix(aid, bootstrapInitialMessageOperationPrefix(aid), { includeTerminal: true }),
5983
6162
  ...findPostBootstrapWelcomeOutbox(aid, { includeTerminal: true }),
@@ -6074,7 +6253,7 @@ export class AUNChannel {
6074
6253
  const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
6075
6254
  if (metadata && Object.keys(metadata).length > 0)
6076
6255
  statusPayload.metadata = metadata;
6077
- if (context?.threadId)
6256
+ if (isTopicThreadId(context?.threadId))
6078
6257
  statusPayload.thread_id = context.threadId;
6079
6258
  if (context?.peerId)
6080
6259
  statusPayload.initiator = context.peerId;
@@ -6149,6 +6328,8 @@ export class AUNChannel {
6149
6328
  await this.sendStructuredOrThrow(channelId, payloadObj, context);
6150
6329
  }
6151
6330
  async disconnect() {
6331
+ if (this.client)
6332
+ ownerResolver.detachClient(this.config.aid, this.client);
6152
6333
  this.managedMenuClient.close();
6153
6334
  this.intentionalDisconnect = true;
6154
6335
  if (this.reconnectTimer) {
@@ -6717,6 +6898,7 @@ export class AUNChannelPlugin {
6717
6898
  return suppressedReceipt(envelope, 'interaction_not_sent');
6718
6899
  };
6719
6900
  if (req.kind.kind === 'action') {
6901
+ const isAsyncQuestion = /^aq-[a-f0-9]{40}$/.test(req.id);
6720
6902
  const action = req.kind;
6721
6903
  const aunCard = withAunAttention({
6722
6904
  type: 'action_card',
@@ -6737,12 +6919,16 @@ export class AUNChannelPlugin {
6737
6919
  }
6738
6920
  if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
6739
6921
  aunCard.initiator = req.initiatorId;
6740
- if (replyCtx?.threadId)
6922
+ if (isTopicThreadId(replyCtx?.threadId))
6741
6923
  aunCard.thread_id = replyCtx.threadId;
6924
+ const cardReplyContext = isAsyncQuestion
6925
+ ? { ...replyCtx, metadata: { ...replyCtx?.metadata, operationId } }
6926
+ : replyCtx;
6742
6927
  const result = await channel.sendContentPayload(channelId, aunCard, {
6743
6928
  contentKind: 'card',
6744
- context: replyCtx,
6745
- dedupeKey: req.id.startsWith('gir-') ? operationId : undefined,
6929
+ context: cardReplyContext,
6930
+ dedupeKey: req.id.startsWith('gir-') || isAsyncQuestion ? operationId : undefined,
6931
+ ttl: isAsyncQuestion ? Math.max(0, cardExpiresAt - Date.now()) : undefined,
6746
6932
  logText: action.title ? `[card] ${action.title}` : '[card]',
6747
6933
  postSend: {
6748
6934
  type: 'register_interaction_card',
@@ -6777,7 +6963,7 @@ export class AUNChannelPlugin {
6777
6963
  }
6778
6964
  if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
6779
6965
  aunCard.initiator = req.initiatorId;
6780
- if (replyCtx?.threadId)
6966
+ if (isTopicThreadId(replyCtx?.threadId))
6781
6967
  aunCard.thread_id = replyCtx.threadId;
6782
6968
  const result = await channel.sendContentPayload(channelId, aunCard, {
6783
6969
  contentKind: 'card',
@@ -6831,6 +7017,7 @@ export class AUNChannelPlugin {
6831
7017
  acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); },
6832
7018
  onInteraction: (cb) => { channel.interactionCallback = cb; },
6833
7019
  invalidateInteraction: (interactionId, reason) => channel.invalidateInteraction(interactionId, reason),
7020
+ restoreInteraction: (envelope, interaction, receipt) => channel.restoreInteraction(envelope, interaction, receipt),
6834
7021
  uploadAgentMd: (content) => channel.uploadAgentMd(content),
6835
7022
  downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
6836
7023
  getGroupName: (groupId) => channel.getGroupName(groupId),