evolcore 0.0.27 → 0.0.29-test.0

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 (195) hide show
  1. package/CHANGELOG.md +38 -15
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +29 -14
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +13 -2
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex/diagnostics.js +4 -3
  9. package/dist/agents/codex-runner.js +444 -106
  10. package/dist/agents/gemini-runner.js +35 -17
  11. package/dist/aun/group/history.js +130 -90
  12. package/dist/aun/group/operations.js +1 -3
  13. package/dist/aun/menu/client.js +1 -0
  14. package/dist/aun/menu/transport.js +5 -3
  15. package/dist/aun/msg/managed-operation.js +70 -0
  16. package/dist/aun/msg/p2p.js +1 -3
  17. package/dist/aun/msg/upload.js +2 -2
  18. package/dist/aun/outbox.js +1 -37
  19. package/dist/channels/aun.js +262 -86
  20. package/dist/channels/daemon.js +18 -1
  21. package/dist/channels/feishu.js +1 -2
  22. package/dist/cli/agent-command.js +23 -3
  23. package/dist/cli/agent-create.js +68 -2
  24. package/dist/cli/agent.js +12 -2
  25. package/dist/cli/aun-commands.js +87 -13
  26. package/dist/cli/cli-argv.js +26 -0
  27. package/dist/cli/daemon-commands.js +116 -149
  28. package/dist/cli/data-command.js +4 -4
  29. package/dist/cli/diag.js +1 -1
  30. package/dist/cli/handoff-command.js +2 -0
  31. package/dist/cli/index.js +18 -1
  32. package/dist/cli/managed-command-guard.js +10 -0
  33. package/dist/cli/memory-command.js +441 -0
  34. package/dist/cli/menu-client.js +4 -0
  35. package/dist/cli/restart-monitor.js +127 -23
  36. package/dist/cli/skill-command.js +41 -0
  37. package/dist/cli/task-context.js +2 -23
  38. package/dist/cli/trigger-command.js +9 -1
  39. package/dist/cli/watch-msg-diag.js +209 -0
  40. package/dist/config/config-field-policy.js +52 -3
  41. package/dist/config/config-manager.js +17 -4
  42. package/dist/config/contact-book-store.js +2 -2
  43. package/dist/config/mention-mode.js +7 -2
  44. package/dist/config/merge.js +28 -9
  45. package/dist/config/resolved-config-op.js +1 -1
  46. package/dist/config/runtime-env.js +137 -0
  47. package/dist/config/schema-registry.js +11 -1
  48. package/dist/config-store.js +10 -10
  49. package/dist/core/audit/event-key.js +3 -1
  50. package/dist/core/audit/log-integrity.js +4 -0
  51. package/dist/core/auth/agent-delegation.js +2 -1
  52. package/dist/core/auth/auth-gateway.js +13 -6
  53. package/dist/core/auth/authorization-audit.js +54 -19
  54. package/dist/core/auth/operation-authorizer.js +17 -0
  55. package/dist/core/auth/operation-catalog.js +9 -0
  56. package/dist/core/causation/context.js +5 -1
  57. package/dist/core/command/agent-control.js +1 -2
  58. package/dist/core/command/command-handler.js +80 -24
  59. package/dist/core/command/diag-service.js +292 -27
  60. package/dist/core/command/memory-slash.js +270 -0
  61. package/dist/core/command/menu-catalog.js +1 -0
  62. package/dist/core/command/menu-handler.js +331 -79
  63. package/dist/core/command/menu-protocol.js +45 -4
  64. package/dist/core/command/slash-gate.js +10 -10
  65. package/dist/core/command/slash-handler.js +230 -79
  66. package/dist/core/data-migration.js +282 -505
  67. package/dist/core/event-catalog.js +26 -0
  68. package/dist/core/evolagent-registry.js +6 -0
  69. package/dist/core/evolagent.js +0 -10
  70. package/dist/core/handoff/store.js +1 -20
  71. package/dist/core/inference/text-inference.js +57 -1
  72. package/dist/core/memory/consolidation-context.js +505 -0
  73. package/dist/core/memory/consolidation-ops-records.js +124 -0
  74. package/dist/core/memory/consolidation-trigger.js +394 -0
  75. package/dist/core/memory/index-manager.js +308 -0
  76. package/dist/core/memory/launch-consolidation.js +395 -0
  77. package/dist/core/memory/memory-admin.js +234 -0
  78. package/dist/core/memory/memory-document.js +100 -0
  79. package/dist/core/memory/memory-session-policy.js +41 -0
  80. package/dist/core/memory/memory-signature.js +257 -0
  81. package/dist/core/memory/memory-writer.js +432 -0
  82. package/dist/core/memory/staleness.js +117 -0
  83. package/dist/core/message/auto-mention-gate.js +454 -0
  84. package/dist/core/message/im-renderer.js +90 -20
  85. package/dist/core/message/message-bridge.js +333 -23
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/message-queue.js +114 -27
  88. package/dist/core/message/pending-hints.js +329 -161
  89. package/dist/core/message/response-engine.js +596 -90
  90. package/dist/core/permission/ec-command-parser.js +1044 -87
  91. package/dist/core/permission/protected-paths.js +376 -126
  92. package/dist/core/permission/sandbox-runtime.js +40 -12
  93. package/dist/core/permission/tool-error-code.js +96 -13
  94. package/dist/core/permission/tool-policy.js +277 -24
  95. package/dist/core/role/runtime-policy.js +3 -7
  96. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  97. package/dist/core/session/session-fs-store.js +0 -76
  98. package/dist/core/session/session-idle-monitor.js +355 -0
  99. package/dist/core/session/session-manager.js +482 -17
  100. package/dist/core/session/session-mapper.js +33 -16
  101. package/dist/core/session/session-renew.js +217 -155
  102. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  103. package/dist/core/skill/global-watcher.js +282 -0
  104. package/dist/core/skill/index-generator.js +84 -0
  105. package/dist/core/skill/startup.js +73 -0
  106. package/dist/core/skill/switch-resolver.js +115 -0
  107. package/dist/core/task/task-activity-store.js +219 -0
  108. package/dist/eck/kit-renderer.js +30 -3
  109. package/dist/eck/manifest-engine.js +26 -4
  110. package/dist/index.js +722 -171
  111. package/dist/ipc.js +564 -2
  112. package/dist/paths.js +30 -9
  113. package/dist/product.js +1 -0
  114. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  115. package/dist/response-system/modes/single-session/index.js +3 -0
  116. package/dist/trigger/feedback.js +1 -0
  117. package/dist/trigger/manager.js +0 -10
  118. package/dist/trigger/scheduler.js +11 -0
  119. package/dist/trigger/validation.js +22 -0
  120. package/dist/utils/atomic-write.js +98 -0
  121. package/dist/utils/avatar-upload.js +59 -18
  122. package/dist/utils/cross-platform.js +87 -16
  123. package/dist/utils/error-dict.json +6 -3
  124. package/dist/utils/error-utils.js +27 -0
  125. package/dist/utils/logger.js +32 -10
  126. package/dist/utils/media-cache.js +84 -0
  127. package/dist/utils/msg-diag.js +97 -0
  128. package/dist/utils/npm-ops.js +119 -4
  129. package/dist/utils/package-version-info.js +244 -0
  130. package/dist/utils/welcome.js +37 -8
  131. package/dist/utils/windows-shell-trust.js +37 -0
  132. package/kits/docs/AUTHORING.md +314 -0
  133. package/kits/docs/GUIDE.md +2 -0
  134. package/kits/docs/INDEX.md +2 -0
  135. package/kits/docs/aun/CHEATSHEET.md +4 -0
  136. package/kits/docs/aun/SDK.md +60 -0
  137. package/kits/docs/channels/aun.md +8 -0
  138. package/kits/docs/context-assembly.md +10 -6
  139. package/kits/docs/evolcore/INDEX.md +17 -3
  140. package/kits/docs/evolcore/agent-create.md +24 -0
  141. package/kits/docs/evolcore/agent.md +5 -2
  142. package/kits/docs/evolcore/config.md +1 -1
  143. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  144. package/kits/docs/evolcore/group.md +34 -18
  145. package/kits/docs/evolcore/memory-architecture.md +78 -0
  146. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  147. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  148. package/kits/docs/evolcore/memory-tags.md +63 -0
  149. package/kits/docs/evolcore/memory.md +110 -0
  150. package/kits/docs/evolcore/menu-client.md +27 -11
  151. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  152. package/kits/docs/evolcore/menu-protocol.md +213 -0
  153. package/kits/docs/evolcore/msg.md +2 -0
  154. package/kits/docs/evolcore/trigger.md +7 -5
  155. package/kits/docs/path-registry.md +6 -8
  156. package/kits/docs/prompt-loading-architecture.md +5 -3
  157. package/kits/docs/venues/group.md +5 -4
  158. package/kits/eck_manifest.json +294 -88
  159. package/kits/eck_manifest.memory.json +257 -0
  160. package/kits/rules/03-identity.md +8 -8
  161. package/kits/rules/05-venue.md +4 -2
  162. package/kits/rules/06-channel.md +1 -1
  163. package/kits/schemas/_meta.json +276 -47
  164. package/kits/schemas/agent-config.schema.12.json +38 -2
  165. package/kits/schemas/agent-config.schema.13.json +591 -0
  166. package/kits/schemas/daemon.schema.6.json +9 -9
  167. package/kits/schemas/defaults.schema.5.json +29 -0
  168. package/kits/schemas/relation-config.schema.8.json +14 -1
  169. package/kits/schemas/relation-config.schema.9.json +129 -0
  170. package/kits/schemas/role-config.schema.1.json +3 -3
  171. package/kits/skills/.switches.json +3 -0
  172. package/kits/skills/ec-skill/SKILL.md +217 -0
  173. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  174. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  175. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  176. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  177. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  178. package/kits/templates/decision/auto-mention.md +25 -0
  179. package/kits/templates/decision/session-renew.md +22 -0
  180. package/kits/templates/memory-consolidation/forget.md +104 -0
  181. package/kits/templates/memory-consolidation/generalize.md +76 -0
  182. package/kits/templates/memory-consolidation/notes.md +133 -0
  183. package/kits/templates/memory-consolidation/profile.md +58 -0
  184. package/kits/templates/memory-consolidation/promote.md +72 -0
  185. package/kits/templates/memory-consolidation/workflow.md +75 -0
  186. package/kits/templates/system-fragments/bootstrap.md +14 -5
  187. package/kits/templates/system-fragments/commands.md +1 -0
  188. package/kits/templates/system-fragments/group-history.md +6 -0
  189. package/kits/templates/system-fragments/identity.md +0 -3
  190. package/kits/templates/system-fragments/memory.md +79 -0
  191. package/kits/templates/system-fragments/reply.md +1 -0
  192. package/kits/templates/system-fragments/venue.md +1 -1
  193. package/package.json +4 -6
  194. package/dist/trigger/legacy-session-history.js +0 -19
  195. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -11,13 +11,13 @@ import { LogWriter } from '../utils/log-writer.js';
11
11
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
12
12
  import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
13
13
  import { resolvePaths, agentDir as agentDirPath, resolveRoot, channelStatePath } from '../paths.js';
14
- import { saveToUploads, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
14
+ import { saveToUploads, saveRelationAttachment, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
15
15
  import { appendAidEvent } from '../utils/instance-registry.js';
16
16
  import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, findMessageLogOperationMessageId, hasMessageLogOperation } from '../core/message/message-log.js';
17
17
  import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
18
18
  import { chatDirPath } from '../core/session/session-fs-store.js';
19
- import { readTopicLable } from '../core/session/session-mapper.js';
20
- import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
19
+ import { readTopicLabel, readTopicStatus } from '../core/session/session-mapper.js';
20
+ import { appendHintAddDetailed, appendHintRemoveDetailed, appendHintPromoteDetailed, parseInjectRequest, peekHints, withHintTargetLock } from '../core/message/pending-hints.js';
21
21
  import { appendAidLifecycle } from '../aun/aid/identity.js';
22
22
  import { enableFullGroupPullPagination, enableReliableMessageDelivery, getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
23
23
  import { MAX_AUN_ATTACHMENT_SIZE, uploadBufferAndBuildPayload, uploadFileAndBuildPayload } from '../aun/msg/upload.js';
@@ -25,7 +25,7 @@ import { loadAgent } from '../config-store.js';
25
25
  import { resolveAgentLifecycle } from '../config/lifecycle.js';
26
26
  import { resolveEffective } from '../config/config-manager.js';
27
27
  import { isManagementRole } from '../config/builtin-roles.js';
28
- import { normalizeMentionMode } from '../config/mention-mode.js';
28
+ import { normalizeAunMentionMode, normalizeMentionMode } from '../config/mention-mode.js';
29
29
  import { getProcessStartTime } from '../utils/process-introspect.js';
30
30
  import * as outbox from '../aun/outbox.js';
31
31
  import { formatSize } from '../utils/media-cache.js';
@@ -715,8 +715,9 @@ export class AUNChannel {
715
715
  * 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + daemon.log 失败日志)。
716
716
  * 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
717
717
  */
718
- callClient(method, params, lane = 'normal', onStart) {
718
+ callClient(method, params, lane = 'normal', onStart, onPrepared) {
719
719
  params = menuTransportParams(method, params);
720
+ onPrepared?.(params);
720
721
  return this.withOutboundSendGate(method, () => (this.client.call(method, params).then(value => value)), lane, onStart);
721
722
  }
722
723
  withOutboundSendGate(method, run, lane = 'normal', onStart) {
@@ -784,7 +785,6 @@ export class AUNChannel {
784
785
  });
785
786
  }
786
787
  async callAndTrace(method, params, opts) {
787
- this.trace('OUT', method, params);
788
788
  // 计时同时覆盖排队和 SDK 往返;callStartedAt 让日志能区分「本地队列堵塞」
789
789
  // 与「网关慢」。两者对 /evolhelp 等命令的回复延迟都有直接影响。
790
790
  const rpcStart = Date.now();
@@ -794,6 +794,11 @@ export class AUNChannel {
794
794
  try {
795
795
  const result = await this.callClient(method, params, opts?.lane ?? 'normal', () => {
796
796
  callStartedAt = Date.now();
797
+ }, preparedParams => {
798
+ // Apply Menu compatibility exactly once, then log the same parameters
799
+ // passed to the SDK (TTL and device targeting may both be removed).
800
+ params = preparedParams;
801
+ this.trace('OUT', method, params);
797
802
  });
798
803
  const durationMs = Date.now() - rpcStart;
799
804
  const rpcDurationMs = Date.now() - callStartedAt;
@@ -844,14 +849,17 @@ export class AUNChannel {
844
849
  state.maxDurationMs = 0;
845
850
  this.rpcWarningStates.set(key, state);
846
851
  const summary = suppressed > 0 ? ` suppressed=${suppressed} max_duration=${maxDurationMs}ms` : '';
852
+ const menuTransport = method === 'message.send' && payloadType.startsWith('menu.')
853
+ ? ` ttl=${params.ttl ?? 'default'} to_device_id=${JSON.stringify(params.to_device_id ?? null)}`
854
+ : '';
847
855
  if (kind === 'failed') {
848
856
  const name = error instanceof Error ? error.name : '';
849
857
  const message = error instanceof Error ? error.message : String(error ?? '');
850
858
  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}`);
859
+ logger.warn(`${this.logPrefix()} rpc ${method} failed after ${durationMs}ms: ${errorLabel} queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${menuTransport}${summary}`);
852
860
  return;
853
861
  }
854
- logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${summary} (发送管线可能饱和或链路抖动)`);
862
+ logger.warn(`${this.logPrefix()} rpc ${method} SLOW: duration=${durationMs}ms queue_wait=${queueWaitMs}ms rpc=${rpcDurationMs}ms payload=${payloadType}${menuTransport}${summary} (发送管线可能饱和或链路抖动)`);
855
863
  }
856
864
  observerQueueDropCount = 0;
857
865
  observerQueueDropLastWarnAt = 0;
@@ -1330,7 +1338,10 @@ export class AUNChannel {
1330
1338
  // Keep the command contract consistent for mixed friendly/canonical @
1331
1339
  // tokens: selector labels are removed above, then known AID-shaped tokens
1332
1340
  // anywhere in the command are removed by the existing helper.
1333
- return this.stripAllMentions(commandText);
1341
+ const stripped = this.stripAllMentions(commandText);
1342
+ // Keep the removed /mentionmode spelling compatible at the channel
1343
+ // boundary while the command handler uses /mention internally.
1344
+ return stripped.replace(/^\/mentionmode(?=\s|$)/, '/mention');
1334
1345
  }
1335
1346
  parsePayloadMentionsOrReject(payload, context, messageId, seq) {
1336
1347
  const raw = payload && typeof payload === 'object' && !Array.isArray(payload)
@@ -1383,7 +1394,7 @@ export class AUNChannel {
1383
1394
  return sanitized;
1384
1395
  return undefined;
1385
1396
  }
1386
- buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, lable) {
1397
+ buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId, label, topicStatus) {
1387
1398
  if (!groupId)
1388
1399
  throw new Error('buildGroupReplyContext requires groupId');
1389
1400
  const replyContext = {
@@ -1401,11 +1412,13 @@ export class AUNChannel {
1401
1412
  replyContext.metadata = {};
1402
1413
  replyContext.metadata.topicName = topicName;
1403
1414
  }
1404
- if (lable && threadId)
1405
- replyContext.metadata.lable = lable;
1415
+ if (label && threadId)
1416
+ replyContext.metadata.label = label;
1417
+ if (topicStatus && threadId)
1418
+ replyContext.metadata.topicStatus = topicStatus;
1406
1419
  return replyContext;
1407
1420
  }
1408
- buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId, lable) {
1421
+ buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId, label, topicStatus) {
1409
1422
  const replyContext = {
1410
1423
  metadata: { encrypted, chatmode, refMessageId },
1411
1424
  delivery: { chatType: 'private' },
@@ -1417,8 +1430,10 @@ export class AUNChannel {
1417
1430
  replyContext.replyToMessageId = messageId;
1418
1431
  if (topicName && threadId)
1419
1432
  replyContext.metadata.topicName = topicName;
1420
- if (lable && threadId)
1421
- replyContext.metadata.lable = lable;
1433
+ if (label && threadId)
1434
+ replyContext.metadata.label = label;
1435
+ if (topicStatus && threadId)
1436
+ replyContext.metadata.topicStatus = topicStatus;
1422
1437
  return replyContext;
1423
1438
  }
1424
1439
  claimInboundMessage(kind, messageId, seq) {
@@ -1537,7 +1552,7 @@ export class AUNChannel {
1537
1552
  peerInfoCache = new Map();
1538
1553
  peerInfoRevision = 0;
1539
1554
  messageSeqMap = new Map(); // messageId → seq (for ack/diagnostics)
1540
- sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
1555
+ sentCount = new Map(); // "channelId\0threadId" → 本轮已发消息计数(seqInTurn/turnMessageCount 用)
1541
1556
  peerE2ee = new Map();
1542
1557
  static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
1543
1558
  plaintextRecv = 0;
@@ -1834,6 +1849,18 @@ export class AUNChannel {
1834
1849
  const kind = (data && typeof data === 'object') ? data.kind ?? '' : '';
1835
1850
  const keys = (data && typeof data === 'object') ? Object.keys(data).join(',') : typeof data;
1836
1851
  logger.debug(`${this.logPrefix()}[DIAG] message.received: kind=${kind} keys=${keys}`);
1852
+ // 最早入站打点:SDK message.received 回调触发瞬间
1853
+ if (data && typeof data === 'object') {
1854
+ const d = data;
1855
+ const payloadType = d.payload?.type ?? d.envelope?.type ?? '';
1856
+ if (payloadType === 'text') {
1857
+ const msgId = d.message_id ?? d.id ?? '';
1858
+ const from = d.from ?? d.envelope?.from ?? '';
1859
+ const to = d.to ?? d.envelope?.to ?? this.config.aid ?? '';
1860
+ const text = d.payload?.text ?? '';
1861
+ import('../utils/msg-diag.js').then(m => m.diagLog('sdk_received', { aid: this.config.aid, from, to, msgId, text })).catch(() => { });
1862
+ }
1863
+ }
1837
1864
  await this.handleIncomingPrivateMessage(data);
1838
1865
  });
1839
1866
  // pureIdentity(控制 AID):协议层不接群消息,不注册 group 创建监听
@@ -2256,24 +2283,39 @@ export class AUNChannel {
2256
2283
  }
2257
2284
  const fileParts = [];
2258
2285
  for (const att of rawAttachments) {
2259
- const filePath = await this.downloadAttachment(att, channelId, delivery);
2260
- if (!filePath)
2286
+ const attachmentBuffer = await this.downloadAttachment(att, channelId, delivery);
2287
+ if (!attachmentBuffer)
2261
2288
  continue;
2262
2289
  const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
2263
2290
  let img = null;
2264
2291
  try {
2265
- const { readFileSync } = await import('node:fs');
2266
- img = await bufferToInboundImage(readFileSync(filePath), {
2292
+ img = await bufferToInboundImage(attachmentBuffer, {
2267
2293
  contentType: att.content_type, mimeType: att.mime_type, filename: name,
2268
2294
  });
2269
2295
  }
2270
2296
  catch { /* read failed, treat as non-image file */ }
2271
2297
  if (img) {
2298
+ try {
2299
+ const attachment = saveRelationAttachment(attachmentBuffer, this.config.aid, formatPeerKey('aun', channelId), img.mimeType);
2300
+ img = { ...img, attachmentId: attachment.attachmentId, localPath: attachment.filePath };
2301
+ }
2302
+ catch (error) {
2303
+ logger.warn(`${this.logPrefix()} Failed to persist relation attachment for ${name}: ${error instanceof Error ? error.message : String(error)}`);
2304
+ }
2272
2305
  images.push(img);
2273
2306
  // 图片已注入视觉通道,不追加 [文件: …] 文本行
2274
2307
  }
2275
2308
  else {
2276
- fileParts.push(`[文件: ${name} → ${filePath}]`);
2309
+ const projectPath = this.projectPathProvider
2310
+ ? await this.projectPathProvider(channelId)
2311
+ : process.cwd();
2312
+ try {
2313
+ const saved = saveToUploads(attachmentBuffer, name, projectPath);
2314
+ fileParts.push(`[文件: ${name} → ${saved.filePath}]`);
2315
+ }
2316
+ catch (error) {
2317
+ logger.warn(`${this.logPrefix()} saveToUploads failed for ${name}: ${error}`);
2318
+ }
2277
2319
  }
2278
2320
  }
2279
2321
  const parts = [];
@@ -2384,18 +2426,7 @@ export class AUNChannel {
2384
2426
  return null;
2385
2427
  }
2386
2428
  }
2387
- const projectPath = this.projectPathProvider
2388
- ? await this.projectPathProvider(channelId)
2389
- : process.cwd();
2390
- try {
2391
- const result = saveToUploads(buffer, filename, projectPath);
2392
- logger.info(`${this.logPrefix()} Saved attachment: ${result.filePath} (${result.size} bytes)`);
2393
- return result.filePath;
2394
- }
2395
- catch (e) {
2396
- logger.warn(`${this.logPrefix()} saveToUploads failed for ${filename}: ${e}`);
2397
- return null;
2398
- }
2429
+ return buffer;
2399
2430
  }
2400
2431
  extractAttachmentUrlToken(url) {
2401
2432
  try {
@@ -2547,9 +2578,13 @@ export class AUNChannel {
2547
2578
  // forwardInbound 内部排除 self-echo 与 from-owner。
2548
2579
  // 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
2549
2580
  // (即便日后 from-owner 排除规则调整,也不会泄漏)。
2550
- if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
2581
+ if (typeof inboundType !== 'string' || !inboundType.startsWith('observer.inject')) {
2551
2582
  void this.forwardInbound(msg);
2552
2583
  }
2584
+ if (inboundType === 'observer.inject.status' || inboundType === 'observer.inject.ack') {
2585
+ this.acknowledgeImmediately(messageId, seq);
2586
+ return;
2587
+ }
2553
2588
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
2554
2589
  // 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
2555
2590
  const msgChatId = typeof payload === 'object' && payload !== null && payload.chat_id;
@@ -2585,6 +2620,9 @@ export class AUNChannel {
2585
2620
  ? payload.type
2586
2621
  : '';
2587
2622
  logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${p2pPayloadType} chatId=${chatId} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
2623
+ if (p2pPayloadType === 'text') {
2624
+ 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(() => { });
2625
+ }
2588
2626
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
2589
2627
  if (p2pPayloadType === 'action_card_reply') {
2590
2628
  this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
@@ -2632,7 +2670,9 @@ export class AUNChannel {
2632
2670
  // observer 插话(v0.3):只落盘到 pending-hints,不进 Agent、不回 owner。详见 docs/observer-insert-design.md。
2633
2671
  if (p2pPayloadType === AUNChannel.INJECT_REQUEST_TYPE) {
2634
2672
  this.acknowledgeImmediately(messageId, seq);
2635
- this.handleObserverInject(fromAid, payload, displayName, peerIdentity.type);
2673
+ void Promise.resolve(this.handleObserverInject(fromAid, payload, displayName, peerIdentity.type)).catch(error => {
2674
+ logger.error(`${this.logPrefix()} observer.inject failed: ${error instanceof Error ? error.message : String(error)}`);
2675
+ });
2636
2676
  return;
2637
2677
  }
2638
2678
  // bind.request:Evol App 绑定请求。绕过白名单透传原始 payload JSON 给 bridge,
@@ -2670,7 +2710,7 @@ export class AUNChannel {
2670
2710
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
2671
2711
  const isSystemP2P = p2pPayloadType === 'event';
2672
2712
  this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
2673
- const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId, readTopicLable(payload));
2713
+ const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId, readTopicLabel(payload), readTopicStatus(payload));
2674
2714
  this.dispatchMessage({
2675
2715
  channelId: chatId,
2676
2716
  userId: fromAid,
@@ -2717,9 +2757,9 @@ export class AUNChannel {
2717
2757
  const senderAid = env.from ?? '';
2718
2758
  const payload = msg.payload ?? '';
2719
2759
  const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
2720
- if (inboundType === 'observer.forward') {
2760
+ if (inboundType === 'observer.forward' || (typeof inboundType === 'string' && inboundType.startsWith('observer.inject'))) {
2721
2761
  this.acknowledgeImmediately(messageId, seq);
2722
- logger.debug(`${this.logPrefix()} Group dropped observer.forward control envelope: mid=${messageId}`);
2762
+ logger.debug(`${this.logPrefix()} Group dropped observer control envelope: mid=${messageId}`);
2723
2763
  return;
2724
2764
  }
2725
2765
  const payloadMentionEntries = this.parsePayloadMentionsOrReject(payload, 'group.inbound', messageId, seq);
@@ -2782,7 +2822,7 @@ export class AUNChannel {
2782
2822
  peerName: displayName,
2783
2823
  peerType: peerInfo?.type || 'unknown',
2784
2824
  seq,
2785
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId, readTopicLable(payload)),
2825
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId, readTopicLabel(payload), readTopicStatus(payload)),
2786
2826
  createdAt,
2787
2827
  });
2788
2828
  return;
@@ -2819,8 +2859,8 @@ export class AUNChannel {
2819
2859
  if (!msgEncrypted)
2820
2860
  this.plaintextRecv++;
2821
2861
  // mention_mode 是唯一的服务端群 @ 策略字段;缺失/非法时使用协议内置默认 disabled。
2822
- const serverMentionMode = normalizeMentionMode(msg.mention_mode)
2823
- ?? normalizeMentionMode(payload?.mention_mode)
2862
+ const serverMentionMode = normalizeAunMentionMode(msg.mention_mode)
2863
+ ?? normalizeAunMentionMode(payload?.mention_mode)
2824
2864
  ?? 'disabled';
2825
2865
  const localMentionMode = normalizeMentionMode(this.mentionModeResolver
2826
2866
  ? await this.mentionModeResolver(groupId).catch(() => undefined)
@@ -2838,6 +2878,13 @@ export class AUNChannel {
2838
2878
  // 命令判定:剥离所有 @ 后看是否 / 开头(多 @ 场景如 @a @b /status 也能正确识别)。
2839
2879
  // echo 消息走独立的 trace 流程,不参与命令语义判定。
2840
2880
  const isCommandMsg = !isEchoMsg && slashCommandText.startsWith('/');
2881
+ const commandMentionAids = payloadMentionAids.length > 0
2882
+ ? payloadMentionAids
2883
+ : this.extractMentionAidsFromText(text);
2884
+ const commandTargetsAnotherAgent = isCommandMsg
2885
+ && commandMentionAids.length > 0
2886
+ && !commandMentionAids.includes(this._aid ?? '')
2887
+ && !mentionedAll;
2841
2888
  const groupSlashRequireMentions = this.groupSlashRequireMentionsResolver
2842
2889
  ? await this.groupSlashRequireMentionsResolver().catch(() => undefined) ?? true
2843
2890
  : true;
@@ -2852,7 +2899,7 @@ export class AUNChannel {
2852
2899
  this.pendingEchoMessages.set(messageId, {
2853
2900
  text: echoText,
2854
2901
  channelId: groupId,
2855
- context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId, readTopicLable(payload)),
2902
+ context: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmodeEcho, topicName, groupId, readTopicLabel(payload), readTopicStatus(payload)),
2856
2903
  receiveTs: Date.now(),
2857
2904
  });
2858
2905
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
@@ -2868,15 +2915,15 @@ export class AUNChannel {
2868
2915
  // slash 命令默认要求结构化 @;Agent 可通过
2869
2916
  // groupSlashRequireMentions=false 选择仅按正文命令格式处理。
2870
2917
  const enforceMention = (mentionMode === 'mention-only' && !isCommandMsg)
2871
- || (isCommandMsg && groupSlashRequireMentions);
2918
+ || (isCommandMsg && (mentionMode === 'auto' || groupSlashRequireMentions));
2872
2919
  const isMentioned = mentionedSelf || mentionedAll;
2873
2920
  // 过滤逻辑下移到响应层:这里只标记,不过滤
2874
2921
  // 但为了保持现有行为兼容(避免大量未 @ 消息涌入),暂时保留过滤
2875
2922
  // TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
2876
- if (enforceMention && !isMentioned && !isOwnHandoff) {
2923
+ if ((commandTargetsAnotherAgent || (enforceMention && !isMentioned)) && !isOwnHandoff) {
2877
2924
  try {
2878
2925
  if (text.trim() || this.collectAllAttachments(payload).length) {
2879
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq, 'unmentioned', 'filtered');
2926
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq, isCommandMsg ? 'command' : 'unmentioned', 'filtered');
2880
2927
  }
2881
2928
  }
2882
2929
  catch (error) {
@@ -2888,7 +2935,8 @@ export class AUNChannel {
2888
2935
  return;
2889
2936
  }
2890
2937
  this.acknowledgeImmediately(messageId, seq);
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))})`);
2938
+ const dropReason = commandTargetsAnotherAgent ? 'command-targeted-other-agent' : 'unmentioned';
2939
+ logger.info(`${this.logPrefix()} Group dropped: ${dropReason} (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} slashRequireMentions=${groupSlashRequireMentions} isCommand=${isCommandMsg} commandMentionAids=${JSON.stringify(commandMentionAids)} textPreview=${JSON.stringify(text.slice(0, 80))})`);
2892
2940
  return;
2893
2941
  }
2894
2942
  }
@@ -2962,7 +3010,7 @@ export class AUNChannel {
2962
3010
  isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
2963
3011
  source: isOwnHandoff ? 'handoff' : undefined,
2964
3012
  encrypted: msgEncrypted,
2965
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId, readTopicLable(payload)),
3013
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId, readTopicLabel(payload), readTopicStatus(payload)),
2966
3014
  mentionMode: serverMentionMode,
2967
3015
  images: inboundImages.length > 0 ? inboundImages : undefined,
2968
3016
  msgType: logDescriptor.msgType,
@@ -3069,7 +3117,7 @@ export class AUNChannel {
3069
3117
  }
3070
3118
  appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq, groupAttention, groupDelivery) {
3071
3119
  try {
3072
- const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
3120
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid, this.config.channelName);
3073
3121
  const entry = buildInboundEntry({
3074
3122
  from: fromAid,
3075
3123
  to: this.config.aid,
@@ -3107,6 +3155,7 @@ export class AUNChannel {
3107
3155
  // observable / owners 不在此处缓存——由 daemon 注入 resolver,从 EvolAgent 的
3108
3156
  // in-memory merged config(启动/重启/热重载时统一更新的唯一缓存)读取,避免重复缓存。
3109
3157
  observerConfigResolver;
3158
+ observerHintRuntime;
3110
3159
  observerGroupMembers = new Map();
3111
3160
  observerGroupMemberFetches = new Map();
3112
3161
  observerGroupMembershipEpoch = new Map();
@@ -3114,6 +3163,15 @@ export class AUNChannel {
3114
3163
  setObserverConfigResolver(fn) {
3115
3164
  this.observerConfigResolver = fn;
3116
3165
  }
3166
+ setObserverHintRuntime(runtime) {
3167
+ this.observerHintRuntime = runtime;
3168
+ }
3169
+ notifyObserverHintConsumed(hint, target, meta) {
3170
+ this.emitInjectStatus(hint.ownerAid, hint.id, target, 'consumed', 0, meta?.action ?? (hint.promoteRequestId ? 'promote' : 'add'), meta?.requestId ?? hint.promoteRequestId);
3171
+ }
3172
+ notifyObserverHintFailed(hint, target, failure) {
3173
+ this.emitInjectStatus(hint.ownerAid, hint.id, target, 'failed', 0, failure.action ?? (hint.promoteRequestId ? 'promote' : 'add'), failure.requestId ?? hint.promoteRequestId, failure);
3174
+ }
3117
3175
  /** 读取 observable 开关 + owners;未接入 daemon 时没有 owning Agent,保持关闭。 */
3118
3176
  getObserverConfig() {
3119
3177
  return this.observerConfigResolver?.() ?? { observable: false, owners: [] };
@@ -3227,7 +3285,7 @@ export class AUNChannel {
3227
3285
  const inboundType = payloadTypeForObserver(data);
3228
3286
  // Observer envelopes are control-plane telemetry. Mirroring one again
3229
3287
  // would amplify every activity event across observer-connected agents.
3230
- if (inboundType === 'observer.forward' || inboundType?.startsWith('menu.'))
3288
+ if (inboundType === 'observer.forward' || inboundType?.startsWith('observer.inject') || inboundType?.startsWith('menu.'))
3231
3289
  return Promise.resolve();
3232
3290
  const { observable, owners } = this.getObserverConfig();
3233
3291
  if (!observable || owners.length === 0)
@@ -3264,7 +3322,7 @@ export class AUNChannel {
3264
3322
  if (!this.connected || !this.client)
3265
3323
  return Promise.resolve();
3266
3324
  const outboundType = payloadTypeForObserver(result);
3267
- if (outboundType === 'observer.forward' || outboundType?.startsWith('menu.'))
3325
+ if (outboundType === 'observer.forward' || outboundType?.startsWith('observer.inject') || outboundType?.startsWith('menu.'))
3268
3326
  return Promise.resolve();
3269
3327
  const { observable, owners } = this.getObserverConfig();
3270
3328
  if (!observable || owners.length === 0)
@@ -3309,13 +3367,7 @@ export class AUNChannel {
3309
3367
  .catch(e => logger.debug(`${this.logPrefix()} observer.forward send to ${ownerAid} failed: ${e}`));
3310
3368
  }
3311
3369
  }
3312
- // ── 观察者插话(Observer Insert,v0.3 待用上下文提示) ──────────────
3313
- //
3314
- // owner 经 message.send 给 agent 自身 AID 发 observer.inject(payload 为对象)。
3315
- // 鉴权 from∈owners 后,把提示【只落盘】到 agent↔对端 会话的 pending-hints.jsonl
3316
- // (不 dispatch、不跑 LLM、不回 owner);下一条对端消息到达时由 message-processor
3317
- // 回放消费、注入渲染层。action=add 加提示 / remove 撤销。
3318
- // 详见 docs/observer-insert-design.md 第一部分。
3370
+ // ── 观察者插话 ──────────────
3319
3371
  /** 回 observer.inject.ack 给 owner;加密使用 daemon 的全局 AUN 默认设置。 */
3320
3372
  emitInjectAck(ownerAid, injectId, data, error) {
3321
3373
  if (!this.connected || !this.client)
@@ -3330,12 +3382,51 @@ export class AUNChannel {
3330
3382
  this.callAndTrace('message.send', { to: ownerAid, payload: ackPayload, encrypt: this.shouldEncrypt(ownerAid) })
3331
3383
  .catch(e => logger.warn(`${this.logPrefix()} observer.inject.ack send to ${ownerAid} failed: ${e}`));
3332
3384
  }
3333
- /** 处理 observer.inject:鉴权 + 校验 + 只落盘到 pending-hints(不触发处理、不回 owner)。 */
3334
- handleObserverInject(fromAid, payload, displayName, peerType) {
3385
+ emitInjectStatus(ownerAid, hintId, target, status, attempt = 0, action, requestId, failure) {
3386
+ if (!this.getObserverConfig().owners.includes(ownerAid))
3387
+ return;
3388
+ const payload = {
3389
+ type: 'observer.inject.status', id: hintId,
3390
+ target: { channel_id: target.channelId, chat_type: target.chatType,
3391
+ ...(target.threadId ? { thread_id: target.threadId } : {}) },
3392
+ data: {
3393
+ status,
3394
+ hint_id: hintId,
3395
+ ...(action ? { action } : {}),
3396
+ ...(requestId ? { request_id: requestId } : {}),
3397
+ ...(status === 'consumed' ? { consumed_at: Date.now() } : {}),
3398
+ ...(failure ? { error: { code: failure.code, message: failure.message } } : {}),
3399
+ },
3400
+ };
3401
+ const delivery = this.connected && this.client
3402
+ ? this.callAndTrace('message.send', { to: ownerAid, payload, encrypt: this.shouldEncrypt(ownerAid) })
3403
+ : Promise.reject(new Error('AUN is disconnected'));
3404
+ void delivery.catch(error => {
3405
+ if (attempt < 3) {
3406
+ const timer = setTimeout(() => this.emitInjectStatus(ownerAid, hintId, target, status, attempt + 1, action, requestId, failure), [250, 1000, 5000][attempt]);
3407
+ timer.unref?.();
3408
+ }
3409
+ else
3410
+ logger.warn(`${this.logPrefix()} observer.inject.status delivery failed owner=${ownerAid} hint=${hintId} status=${status}: ${error}`);
3411
+ });
3412
+ }
3413
+ async handleObserverInject(fromAid, payload, displayName, peerType) {
3335
3414
  void peerType;
3336
3415
  const { owners } = this.getObserverConfig();
3337
3416
  const ts = Date.now();
3338
3417
  const req = parseInjectRequest(payload, fromAid, owners, ts);
3418
+ const requestAction = req.kind === 'reject' ? req.action : req.kind;
3419
+ const requestHintId = req.kind === 'add' ? req.id : 'targetId' in req ? req.targetId : undefined;
3420
+ const requestTarget = 'channelId' in req && req.channelId
3421
+ ? { channel_id: req.channelId, chat_type: req.chatType ?? 'private', ...(req.threadId ? { thread_id: req.threadId } : {}) }
3422
+ : undefined;
3423
+ const ackData = (status, hintId) => ({
3424
+ status,
3425
+ action: requestAction,
3426
+ ...(hintId || requestHintId ? { hint_id: hintId || requestHintId } : {}),
3427
+ ...(req.injectId ? { request_id: req.injectId } : {}),
3428
+ ...(requestTarget ? { target: requestTarget } : {}),
3429
+ });
3339
3430
  if (req.kind === 'reject') {
3340
3431
  logger.warn(`${this.logPrefix()} observer.inject rejected: ${this.getShortAid(fromAid)} ${req.code}`);
3341
3432
  this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.action }, { code: req.code, message: req.message });
@@ -3343,24 +3434,67 @@ export class AUNChannel {
3343
3434
  }
3344
3435
  const selfAID = this.config.aid;
3345
3436
  const sessionsDir = resolvePaths().sessionsDir;
3346
- let ok;
3347
- if (req.kind === 'remove') {
3348
- ok = appendHintRemove(sessionsDir, 'aun', req.channelId, selfAID, { targetId: req.targetId, threadId: req.threadId, ts });
3349
- }
3350
- else {
3351
- ok = appendHintAdd(sessionsDir, 'aun', req.channelId, selfAID, { id: req.id, text: req.text, threadId: req.threadId, ownerAid: req.ownerAid, ts });
3437
+ const channelKey = this.config.channelName ?? 'aun';
3438
+ try {
3439
+ await withHintTargetLock(sessionsDir, 'aun', req.channelId, selfAID, req.chatType, req.threadId, async () => {
3440
+ const immediate = (req.kind === 'add' && req.mode === 'interrupt') || req.kind === 'promote';
3441
+ const target = { channelKey, channelId: req.channelId, chatType: req.chatType, threadId: req.threadId, selfAid: selfAID };
3442
+ if (immediate && (!this.observerHintRuntime || !await this.observerHintRuntime.hasSession(target))) {
3443
+ this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.kind }, { code: 'SESSION_NOT_FOUND', message: '目标 Agent session 不存在' });
3444
+ return;
3445
+ }
3446
+ const result = req.kind === 'add'
3447
+ ? appendHintAddDetailed(sessionsDir, 'aun', req.channelId, selfAID, {
3448
+ id: req.id, text: req.text, mode: req.mode, chatType: req.chatType,
3449
+ threadId: req.threadId, ownerAid: req.ownerAid, ts,
3450
+ }, channelKey)
3451
+ : req.kind === 'remove'
3452
+ ? appendHintRemoveDetailed(sessionsDir, 'aun', req.channelId, selfAID, {
3453
+ targetId: req.targetId, chatType: req.chatType, threadId: req.threadId, ts,
3454
+ }, channelKey)
3455
+ : appendHintPromoteDetailed(sessionsDir, 'aun', req.channelId, selfAID, {
3456
+ targetId: req.targetId, chatType: req.chatType, threadId: req.threadId, requestId: req.injectId, ts,
3457
+ }, channelKey);
3458
+ if (!result.ok) {
3459
+ this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.kind }, { code: result.code ?? 'STORE_FAILED', message: '提示状态操作失败' });
3460
+ return;
3461
+ }
3462
+ const hintId = req.kind === 'add' ? req.id : req.targetId;
3463
+ logger.info(`${this.logPrefix()} observer.inject ${req.kind} stored: from=${this.getShortAid(fromAid)}(${displayName}) target=${req.channelId} chatType=${req.chatType} thread=${req.threadId ?? 'main'} hint=${hintId ?? 'all'}`);
3464
+ this.emitInjectAck(fromAid, req.injectId, ackData('accepted', hintId));
3465
+ if (req.kind === 'remove') {
3466
+ for (const hint of result.removedHints ?? [])
3467
+ this.emitInjectStatus(hint.ownerAid, hint.id, target, 'removed', 0, 'remove', req.injectId);
3468
+ }
3469
+ else if (req.kind === 'add') {
3470
+ this.emitInjectStatus(req.ownerAid, req.id, target, result.state ?? 'pending', 0, 'add', req.injectId || req.id);
3471
+ }
3472
+ else {
3473
+ const stored = peekHints(sessionsDir, 'aun', req.channelId, selfAID, req.threadId, req.chatType, undefined, channelKey)
3474
+ .find(item => item.id === req.targetId);
3475
+ if (stored)
3476
+ this.emitInjectStatus(stored.ownerAid, req.targetId, target, 'pending', 0, 'promote', stored.promoteRequestId || req.injectId);
3477
+ }
3478
+ if (immediate && result.state === 'pending') {
3479
+ try {
3480
+ const scheduled = await this.observerHintRuntime.schedule({ ...target, hintId: hintId, requestId: req.injectId });
3481
+ if (scheduled === 'SESSION_NOT_FOUND')
3482
+ logger.warn(`${this.logPrefix()} observer hint session vanished before scheduling hint=${hintId}`);
3483
+ }
3484
+ catch (error) {
3485
+ logger.warn(`${this.logPrefix()} observer hint scheduling failed hint=${hintId}: ${error}`);
3486
+ }
3487
+ }
3488
+ const watchText = req.kind === 'add' ? req.text
3489
+ : req.kind === 'promote' ? `[立即执行提示] id=${req.targetId}`
3490
+ : `[撤销提示]${req.targetId ? ` id=${req.targetId}` : '(全部)'}`;
3491
+ this.recordInjectWatch('in', fromAid, req.channelId, req.chatType, `inject-${req.injectId || ts}`, watchText, req.threadId, channelKey);
3492
+ }, channelKey);
3352
3493
  }
3353
- if (!ok) {
3354
- this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.kind }, { code: 'STORE_FAILED', message: '提示落盘失败' });
3355
- return;
3494
+ catch (error) {
3495
+ logger.warn(`${this.logPrefix()} observer.inject failed before acceptance: ${error instanceof Error ? error.message : String(error)}`);
3496
+ this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.kind }, { code: 'STORE_FAILED', message: '提示状态操作失败' });
3356
3497
  }
3357
- logger.info(`${this.logPrefix()} observer.inject ${req.kind} stored: from=${this.getShortAid(fromAid)}(${displayName}) target=${req.channelId} chatType=${req.chatType} thread=${req.threadId ?? 'main'}${req.kind === 'add' ? ` textLen=${req.text.length}` : `${req.targetId ? ` targetId=${req.targetId}` : ' (clear-all)'}`}`);
3358
- this.emitInjectAck(fromAid, req.injectId, { status: 'accepted', action: req.kind });
3359
- // 记录到 watch(被观察的 agent↔对端 会话),带 owner-inject 标记区分对端真实消息。
3360
- // v0.3:只记"提示已添加/已撤销",不触发处理、不产生 agent→owner 回应。
3361
- const watchText = req.kind === 'remove' ? `[撤销提示]${req.targetId ? ` id=${req.targetId}` : '(全部)'}` : req.text;
3362
- const synthId = `inject-${req.injectId || ts}`;
3363
- this.recordInjectWatch('in', fromAid, req.channelId, req.chatType, synthId, watchText, req.threadId);
3364
3498
  }
3365
3499
  /**
3366
3500
  * 把 observer 插话 / 对插话的回应记录到 watch(被观察的 agent↔对端 会话),
@@ -3369,11 +3503,11 @@ export class AUNChannel {
3369
3503
  * @param dir 'in'=owner→agent 插话;'out'=agent→owner 对插话的回应
3370
3504
  * @param peerChannelId 被观察会话的对端(agent↔对端),日志落点 = sessions/aun/<self>/<peerChannelId>/
3371
3505
  */
3372
- recordInjectWatch(dir, ownerAid, peerChannelId, chatType, msgId, text, threadId) {
3506
+ recordInjectWatch(dir, ownerAid, peerChannelId, chatType, msgId, text, threadId, channelKey) {
3373
3507
  try {
3374
3508
  const selfAID = this.config.aid;
3375
3509
  const isGroup = chatType === 'group';
3376
- const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID);
3510
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID, channelKey);
3377
3511
  const entry = dir === 'in'
3378
3512
  ? buildInboundEntry({
3379
3513
  from: ownerAid, to: selfAID, chatType,
@@ -4136,6 +4270,9 @@ export class AUNChannel {
4136
4270
  if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
4137
4271
  params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
4138
4272
  }
4273
+ if (isMenuPayload && context?.metadata?.menuClientVolatileV1 === true) {
4274
+ params.menu_client_volatile_v1 = true;
4275
+ }
4139
4276
  if (isMenuPayload && encrypt && context?.metadata?.protectedHeaders) {
4140
4277
  params.protected_headers = context.metadata.protectedHeaders;
4141
4278
  }
@@ -4380,17 +4517,24 @@ export class AUNChannel {
4380
4517
  const rawItem = item && typeof item === 'object' && !Array.isArray(item)
4381
4518
  ? { ...item }
4382
4519
  : { kind: 'unknown', text: String(item ?? '') };
4520
+ // Lift evolCommand out of the item and into the payload's ext dict.
4521
+ const evolCommand = typeof rawItem.evolCommand === 'string' ? rawItem.evolCommand : undefined;
4522
+ if (evolCommand)
4523
+ delete rawItem.evolCommand;
4383
4524
  const displayArguments = rawItem.kind === 'tool_call'
4384
4525
  ? toolInputForDisplay(rawItem.name, rawItem.arguments)
4385
4526
  : undefined;
4386
4527
  const activityItem = rawItem.kind === 'tool_call' && displayArguments !== rawItem.arguments
4387
4528
  ? { ...rawItem, arguments: displayArguments }
4388
4529
  : rawItem;
4389
- return withAunAttention({
4530
+ const base = {
4390
4531
  ...this.buildTaskPayloadBase(envelope, context),
4391
4532
  type: 'activity',
4392
4533
  item: this.stripUndefinedDeep(activityItem),
4393
- });
4534
+ };
4535
+ if (evolCommand)
4536
+ base.ext = { evolCommand };
4537
+ return withAunAttention(base);
4394
4538
  }
4395
4539
  async sendReliableStructured(channelId, payload, context, logText) {
4396
4540
  const result = await this.sendContentPayload(channelId, payload, {
@@ -4418,13 +4562,17 @@ export class AUNChannel {
4418
4562
  return { status: 'failed', error: 'message text is empty', code: 'EMPTY_MESSAGE' };
4419
4563
  }
4420
4564
  const delivery = this.requireDelivery(channelId, context);
4421
- const routedContext = this.withDelivery(context, delivery);
4565
+ let routedContext = this.withDelivery(context, delivery);
4422
4566
  // 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
4423
4567
  if (!this.isEchoSending) {
4424
4568
  await this.flushPendingEcho(channelId, delivery, context?.threadId);
4425
4569
  }
4426
4570
  let finalText = text;
4427
- this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
4571
+ const turnKey = `${channelId}\0${context?.threadId ?? ''}`;
4572
+ const seqInTurn = (this.sentCount.get(turnKey) || 0) + 1;
4573
+ this.sentCount.set(turnKey, seqInTurn);
4574
+ // Carry seq into context so deliverTextEntry can embed it in ext.
4575
+ routedContext = { ...routedContext, ext: { ...routedContext.ext, seqInTurn } };
4428
4576
  if (delivery.chatType === 'group') {
4429
4577
  const requestedMentions = Array.from(new Set((context?.mentionUserIds ?? [])
4430
4578
  .filter((id) => typeof id === 'string' && id.trim().length > 0)
@@ -4838,7 +4986,23 @@ export class AUNChannel {
4838
4986
  }
4839
4987
  // 从 context.metadata.source 读取 source,默认为 'daemon'
4840
4988
  const source = context?.metadata?.source ?? 'daemon';
4989
+ // Merge ext hints from context into the wire payload.
4990
+ // isFinal: written by response-engine; turnMessageCount: total messages this turn (only on final).
4991
+ let ext = context?.ext
4992
+ ? { ...context.ext }
4993
+ : undefined;
4994
+ if (ext?.isFinal) {
4995
+ const turnKey = `${channelId}\0${context?.threadId ?? ''}`;
4996
+ const total = this.sentCount.get(turnKey) ?? 1;
4997
+ ext = { ...ext, turnMessageCount: total };
4998
+ }
4841
4999
  const payload = withAunAttention({ type: 'text', text: finalText }, context?.attention);
5000
+ if (ext && Object.keys(ext).length > 0)
5001
+ payload.ext = ext;
5002
+ // sentAt 在每次投递尝试时盖戳(outbox 重发取重发时刻)。服务端会用自己的时间覆盖
5003
+ // 顶层 timestamp,两者有毫秒级差异,sentAt 只代表本端时钟。
5004
+ if (context?.debug)
5005
+ payload.debug = { ...context.debug, sentAt: localTimestamp() };
4842
5006
  if (delivery.chatType === 'group') {
4843
5007
  const extracted = this.extractMentionEntriesFromText(finalText);
4844
5008
  if (extracted.length > 0)
@@ -4864,6 +5028,7 @@ export class AUNChannel {
4864
5028
  }
4865
5029
  // 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
4866
5030
  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}`);
5031
+ 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(() => { });
4867
5032
  const isGroup = delivery.chatType === 'group';
4868
5033
  const targetAid = channelId;
4869
5034
  if (entry.deliveryReceipt) {
@@ -5335,6 +5500,9 @@ export class AUNChannel {
5335
5500
  if (isMenuPayload && typeof context?.metadata?.menuClientSdkVersion === 'string') {
5336
5501
  params.menu_client_sdk_version = context.metadata.menuClientSdkVersion;
5337
5502
  }
5503
+ if (isMenuPayload && context?.metadata?.menuClientVolatileV1 === true) {
5504
+ params.menu_client_volatile_v1 = true;
5505
+ }
5338
5506
  const rawHeaders = context?.metadata?.protectedHeaders;
5339
5507
  if (encrypt && rawHeaders && typeof rawHeaders === 'object' && !Array.isArray(rawHeaders)) {
5340
5508
  const protectedHeaders = {};
@@ -5414,8 +5582,8 @@ export class AUNChannel {
5414
5582
  logger.warn(`${this.logPrefix()} sendFile: file is empty`);
5415
5583
  return { status: 'failed', error: 'file is empty', code: 'EMPTY_FILE' };
5416
5584
  }
5417
- if (stat.size > 10 * 1024 * 1024) {
5418
- logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
5585
+ if (stat.size > MAX_AUN_ATTACHMENT_SIZE) {
5586
+ logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max ${formatSize(MAX_AUN_ATTACHMENT_SIZE)})`);
5419
5587
  return { status: 'failed', error: `file too large (max ${formatSize(MAX_AUN_ATTACHMENT_SIZE)})`, code: 'FILE_TOO_LARGE' };
5420
5588
  }
5421
5589
  // Write-ahead: persist to outbox
@@ -5869,8 +6037,13 @@ export class AUNChannel {
5869
6037
  return;
5870
6038
  }
5871
6039
  context = this.withDelivery(context, delivery);
5872
- if (status === 'start')
5873
- this.sentCount.delete(channelId); // 新任务开始,重置计数
6040
+ if (status === 'start') {
6041
+ // 新任务开始,重置所有以该 channelId 开头的计数(不区分 threadId)
6042
+ for (const key of this.sentCount.keys()) {
6043
+ if (key.startsWith(`${channelId}\0`))
6044
+ this.sentCount.delete(key);
6045
+ }
6046
+ }
5874
6047
  if (!this.client || !this.connected)
5875
6048
  return;
5876
6049
  const requestedSeverity = extraMeta?.severity;
@@ -6239,6 +6412,8 @@ export class AUNChannelPlugin {
6239
6412
  if (replyCtx) {
6240
6413
  replyCtx = { ...replyCtx };
6241
6414
  delete replyCtx.attention;
6415
+ // Do not inherit title from a previous payload's reply context.
6416
+ delete replyCtx.title;
6242
6417
  }
6243
6418
  if (attention !== undefined)
6244
6419
  replyCtx = { ...(replyCtx ?? {}), attention };
@@ -6283,8 +6458,9 @@ export class AUNChannelPlugin {
6283
6458
  ? { forkAnchor: payload.metadata.forkAnchor } : {}),
6284
6459
  },
6285
6460
  };
6286
- if (payload.kind === 'result.text' && payload.isFinal)
6461
+ if (payload.kind === 'result.text' && payload.isFinal && delivery.chatType === 'private') {
6287
6462
  sendCtx.title = '✅ 最终回复:';
6463
+ }
6288
6464
  let result;
6289
6465
  try {
6290
6466
  result = await channel.sendMessage(channelId, payload.text, sendCtx);