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
@@ -1,20 +1,26 @@
1
1
  import { loadAgent } from '../../config-store.js';
2
2
  import { initializationBlocksTasks } from '../../cli/agent-create.js';
3
3
  import { createHash, randomBytes } from 'crypto';
4
+ import fs from 'fs';
5
+ import path from 'path';
4
6
  import { agentStorageMenuService, isAgentStorageMenuRequest } from '../command/agent-storage-menu.js';
5
7
  import { logger } from '../../utils/logger.js';
6
8
  import { StreamDebouncer } from './stream-debouncer.js';
7
9
  import { appendMessageLog, appendMessageLogStrict, buildInboundEntry, isTransientProtocolMessage } from './message-log.js';
8
10
  import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from './message-utils.js';
9
11
  import { chatDirPath } from '../session/session-fs-store.js';
10
- import { readTopicLable } from '../session/session-mapper.js';
12
+ import { readTopicLabel, readTopicStatus } from '../session/session-mapper.js';
11
13
  import { formatChannelKey, tryParseChannelKey } from '../channel-loader.js';
12
14
  import { agentDir, resolvePaths } from '../../paths.js';
13
15
  import { listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, } from '../../config/peer-role-resolver.js';
14
16
  import { resolveContactView, resolvePeerDisplayLabel, resolvePrincipal } from '../../config/contact-book.js';
15
17
  import { findContactRequestNote, expirePendingContactRequests, reviewContactRequest, submitContactRequest, contactRequestSubmissionProtocolResult, queryOwnContactRequestStatus, } from '../../config/contact-request-service.js';
16
18
  import { isValidAid } from '../../aun/aid/validation.js';
17
- import { PeerIdentityCache } from '../relation/peer-identity.js';
19
+ import { PeerIdentityCache, formatPeerKey } from '../relation/peer-identity.js';
20
+ import { resolveEffective } from '../../config/config-manager.js';
21
+ import { createDecisionInferenceProvider } from '../inference/text-inference.js';
22
+ import { AutoMentionGate } from './auto-mention-gate.js';
23
+ import { syncGroupRulesContext } from '../../eck/group-rules-sync.js';
18
24
  import { handlePendingDingtalkContactBindMessage, registerPendingDingtalkContactBind } from '../../channels/dingtalk.js';
19
25
  import { handlePendingFeishuContactBindMessage, registerPendingFeishuContactBind } from '../../channels/feishu.js';
20
26
  import { handlePendingQQBotContactBindMessage, registerPendingQQBotContactBind } from '../../channels/qqbot.js';
@@ -30,9 +36,9 @@ import { recordCausationSpan } from '../causation/audit.js';
30
36
  import { renderActionAsText } from '../interaction-router.js';
31
37
  import { planApprovalRoute } from '../permission/approval-gateway.js';
32
38
  import { guardGroupSlashCommand, parseFullAccessCommand } from '../command/slash-gate.js';
33
- import { isFullAccessEnabled } from '../../config-store.js';
34
39
  import { createAgentPolicyAuthorization } from '../role/runtime-policy.js';
35
40
  import { auditFullAccessEvent } from '../auth/authorization-audit.js';
41
+ import { DIAGNOSTIC_AGENT_AID, DiagnosticSessionRouter, parseDiagnosticEvent, isDiagnosticSessionActive } from '../command/diag-service.js';
36
42
  import { consumeAdmissionReplyBudget, clearAdmissionReplyBudgets, evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission, } from './message-admission.js';
37
43
  const CONTACT_REJECTION_NOTICE = '[rejection] Not accepting messages from you.';
38
44
  const REJECTABLE_PEER_TYPES = new Set(['ai', 'bot', 'agent', 'service']);
@@ -104,6 +110,79 @@ export class MessageBridge {
104
110
  cmdHandler;
105
111
  eventBus;
106
112
  processOwnersProvider;
113
+ autoMentionGate = new AutoMentionGate({
114
+ resolveInference: item => {
115
+ const config = resolveEffective({
116
+ self: item.selfAid,
117
+ peerKey: formatPeerKey('aun', item.groupId),
118
+ }, { cache: true, expand: true });
119
+ const model = config.decision_model?.model?.trim();
120
+ const provider = createDecisionInferenceProvider(config.decision_model);
121
+ return model && provider ? { model, provider } : undefined;
122
+ },
123
+ resolveResources: async (item) => {
124
+ const config = resolveEffective({
125
+ self: item.selfAid,
126
+ peerKey: formatPeerKey('aun', item.groupId),
127
+ }, { cache: true, expand: true });
128
+ const resources = {};
129
+ if (config.groupRules?.mode !== 'ignore') {
130
+ try {
131
+ const synced = await syncGroupRulesContext({ selfAid: item.selfAid, groupId: item.groupId, channel: 'aun' });
132
+ if (synced.groupRulesPath && fs.existsSync(synced.groupRulesPath)) {
133
+ resources.groupRules = fs.readFileSync(synced.groupRulesPath, 'utf8');
134
+ }
135
+ }
136
+ catch (error) {
137
+ logger.debug(`[MessageBridge] Decision context group rules unavailable: ${String(error)}`);
138
+ }
139
+ }
140
+ const personalPath = path.join(agentDir(item.selfAid), 'personal', 'personal.md');
141
+ if (fs.existsSync(personalPath))
142
+ resources.personal = fs.readFileSync(personalPath, 'utf8');
143
+ if (item.projectPath) {
144
+ const files = [];
145
+ let current = path.resolve(item.projectPath);
146
+ while (true) {
147
+ const candidate = path.join(current, 'AGENTS.md');
148
+ if (fs.existsSync(candidate))
149
+ files.unshift(candidate);
150
+ const parent = path.dirname(current);
151
+ if (parent === current)
152
+ break;
153
+ current = parent;
154
+ }
155
+ if (files.length > 0) {
156
+ resources.projectInstructions = files.map(file => (`# ${path.relative(item.projectPath, file) || 'AGENTS.md'}\n${fs.readFileSync(file, 'utf8')}`)).join('\n\n');
157
+ }
158
+ }
159
+ return resources;
160
+ },
161
+ resolveQuietWindowMs: item => resolveEffective({
162
+ self: item.selfAid,
163
+ peerKey: formatPeerKey('aun', item.groupId),
164
+ }, { cache: true, expand: true }).autoMentionQuietWindowMs,
165
+ resolveMessageState: item => {
166
+ if (!item.messageId)
167
+ return 'missing';
168
+ if (item.sessionId) {
169
+ const state = this.messageQueue.getMessageState?.(item.sessionId, item.messageId, item.selfAid)?.state ?? 'missing';
170
+ if (state !== 'missing')
171
+ return state;
172
+ }
173
+ return this.messageQueue.getMessageStateByMessageId?.(item.messageId, item.selfAid, {
174
+ channelId: item.groupId,
175
+ groupId: item.groupId,
176
+ threadId: item.threadId || '',
177
+ })?.state ?? 'missing';
178
+ },
179
+ resolveProcessing: item => {
180
+ if (!item.sessionId)
181
+ return !!item.mainSessionBusy;
182
+ return (this.messageQueue.isProcessing?.(item.sessionId) ?? false)
183
+ || (this.messageQueue.getQueueLength?.(item.sessionId) ?? 0) > 0;
184
+ },
185
+ });
107
186
  debouncers = new Map();
108
187
  defaultDebounce;
109
188
  /** Serializes session selection/renewal with inbound log admission per route. */
@@ -115,6 +194,7 @@ export class MessageBridge {
115
194
  menuTokenStore = new MenuTokenStore();
116
195
  handoffRuntime;
117
196
  sessionRenewService;
197
+ diagnosticRouter;
118
198
  aidStatsCollector;
119
199
  contactBindRuntimeChecker;
120
200
  interactionRouter;
@@ -129,6 +209,13 @@ export class MessageBridge {
129
209
  this.processOwnersProvider = processOwnersProvider;
130
210
  this.defaultDebounce = defaultDebounce ?? 0;
131
211
  this.sessionRenewService = new SessionRenewService(sessionManager, processor, { eventBus });
212
+ this.diagnosticRouter = new DiagnosticSessionRouter(sessionManager, async (sessionId) => {
213
+ // Revoke in the router first, then clear pending work before awaiting the
214
+ // runner interruption. Delayed debounce and recovery are fenced below.
215
+ this.messageQueue.clearBySession(sessionId);
216
+ this.messageQueue.markActiveTerminal(sessionId, 'stop');
217
+ await this.processor.interruptSession(sessionId, 'stop');
218
+ });
132
219
  }
133
220
  /** Inject EvolAgentRegistry so owner lookups/writes route to agent.json for agent-owned channels. */
134
221
  setAgentRegistry(registry) {
@@ -520,6 +607,12 @@ export class MessageBridge {
520
607
  }
521
608
  return true;
522
609
  }
610
+ async sendAdmissionMenuResponse(context, adapter, channel, msg, response) {
611
+ if (!consumeAdmissionReplyBudget(context))
612
+ return false;
613
+ await this.sendMenuResponse(adapter, channel, msg, response);
614
+ return true;
615
+ }
523
616
  /** Controls handled before the normal slash handler still need structured group feedback. */
524
617
  async sendSlashControlReply(channel, msg, sendReply, payload, adapter = this.processor.getChannelInfo?.(channel)?.adapter) {
525
618
  if (msg.chatType === 'group' && adapter?.send) {
@@ -757,6 +850,12 @@ export class MessageBridge {
757
850
  const conversationId = chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
758
851
  const selfAid = msg.selfAID || owningAgent?.aid || parsedChannelKey?.selfAID;
759
852
  const resolvedChannelType = msg.channelType || parsedChannelKey?.type || effectiveChannelType;
853
+ const menuControl = resolvedChannelType === 'aun'
854
+ ? parseMenuControl(content)
855
+ : { isMenu: false };
856
+ const isPrivateMenuTokenRequest = menuControl.isMenu
857
+ && menuControl.type === 'menu.token.request'
858
+ && chatType === 'private';
760
859
  const admissionRoute = [
761
860
  resolvedChannelType,
762
861
  selfAid || '',
@@ -793,7 +892,8 @@ export class MessageBridge {
793
892
  if (chatType === 'private' && !peerType) {
794
893
  peerType = PeerIdentityCache.get('aun', primaryId, agentDir(selfAid))?.type;
795
894
  }
796
- if (chatType === 'private'
895
+ if (!menuControl.isMenu
896
+ && chatType === 'private'
797
897
  && REJECTABLE_PEER_TYPES.has(String(peerType || '').trim().toLowerCase())) {
798
898
  await this.sendAdmissionControlReply(admissionPreflight.context, msg, sendReply, CONTACT_REJECTION_NOTICE);
799
899
  }
@@ -831,11 +931,23 @@ export class MessageBridge {
831
931
  return;
832
932
  }
833
933
  const admissionDecision = evaluateOrdinaryInboundAdmission(admissionPreflight.context);
834
- if (!admissionDecision.allow) {
934
+ if (!admissionDecision.allow && !isPrivateMenuTokenRequest) {
835
935
  if (chatType === 'private') {
836
- const notice = this.admissionDeniedNotice(admissionDecision.context, resolvedChannelType);
837
- if (notice) {
838
- await this.sendAdmissionControlReply(admissionDecision.context, msg, sendReply, notice);
936
+ if (menuControl.isMenu) {
937
+ if (hasValidMenuId(menuControl)) {
938
+ const header = { id: menuControl.id, ...(menuControl.name?.trim() ? { name: menuControl.name } : {}) };
939
+ await this.sendAdmissionMenuResponse(admissionDecision.context, adapter, channelName, msg, menuFailure(header, {
940
+ code: 'ROLE_ACCESS_DENIED',
941
+ message: this.admissionDeniedNotice(admissionDecision.context, resolvedChannelType)
942
+ ?? '当前不接受陌生人消息。',
943
+ }));
944
+ }
945
+ }
946
+ else {
947
+ const notice = this.admissionDeniedNotice(admissionDecision.context, resolvedChannelType);
948
+ if (notice) {
949
+ await this.sendAdmissionControlReply(admissionDecision.context, msg, sendReply, notice);
950
+ }
839
951
  }
840
952
  }
841
953
  logger.info(`[MessageBridge] Inbound admission denied before business routing: self=${selfAid} actor=${actorId ?? '<none>'} channel=${channelKey} reason=${admissionDecision.reason}`);
@@ -844,9 +956,6 @@ export class MessageBridge {
844
956
  // Menu is a protocol control plane. Its parsing and response path must
845
957
  // remain available throughout the agent lifecycle; lifecycle gates
846
958
  // ordinary model execution, not protocol handling.
847
- const menuControl = resolvedChannelType === 'aun'
848
- ? parseMenuControl(content)
849
- : { isMenu: false };
850
959
  let menuResponseWarning;
851
960
  if (menuControl.isMenu) {
852
961
  // Menu is an AUN private control plane. Group messages must never
@@ -971,6 +1080,37 @@ export class MessageBridge {
971
1080
  await this.sendAccessDenied(adapter, channelName, msg, sendReply);
972
1081
  return;
973
1082
  }
1083
+ // Diagnostic invitations carry their own execution boundary. Resolve
1084
+ // that boundary before ordinary command/session routing so a new case
1085
+ // cannot inherit an older Codex thread.
1086
+ let diagnosticSession;
1087
+ if (selfAid === DIAGNOSTIC_AGENT_AID && resolvedChannelType === 'aun'
1088
+ && chatType === 'private' && msg.source !== 'handoff') {
1089
+ try {
1090
+ const diagnosticEvent = parseDiagnosticEvent(content);
1091
+ if (diagnosticEvent) {
1092
+ releaseAdmission = await this.acquireAdmission(admissionRoute);
1093
+ const routed = await this.diagnosticRouter.route({
1094
+ event: diagnosticEvent, message: msg, channel: channelName,
1095
+ channelType: resolvedChannelType, selfAid,
1096
+ chatDir: chatDirPath(resolvePaths().sessionsDir, resolvedChannelType, msg.channelId, selfAid, channelName),
1097
+ projectPath: owningAgent?.projectPath ?? this.defaultProjectPath,
1098
+ baseagent: owningAgent?.baseagent,
1099
+ });
1100
+ if ('notice' in routed) {
1101
+ await sendReply(msg.channelId, routed.notice, msg.replyContext);
1102
+ return;
1103
+ }
1104
+ diagnosticSession = routed.session;
1105
+ diagnosticSession.identity = identity;
1106
+ }
1107
+ }
1108
+ catch (error) {
1109
+ logger.warn(`[MessageBridge] Diagnostic admission failed: ${error instanceof Error ? error.message : 'unknown error'}`);
1110
+ await sendReply(msg.channelId, '诊断会话隔离未完成,本次未执行诊断。请检查诊断端日志后重试。', msg.replyContext);
1111
+ return;
1112
+ }
1113
+ }
974
1114
  // /fa is an execution prefix, not a session-mode switch or a normal
975
1115
  // quick command. Only this trusted bridge path can construct the
976
1116
  // internal per-call authorization carried into ResponseEngine.
@@ -1003,10 +1143,6 @@ export class MessageBridge {
1003
1143
  await reject('用法:/fa <提示词> 或 /fullaccess <提示词>');
1004
1144
  return;
1005
1145
  }
1006
- if (!isFullAccessEnabled()) {
1007
- await reject('fullaccess 功能未启用,请先设置 daemon.json.fullaccess.enabled=true');
1008
- return;
1009
- }
1010
1146
  if (!authSubject.isDaemonOwner || !authSubject.principalId) {
1011
1147
  await reject('仅可信 daemon-owner 可以使用 fullaccess');
1012
1148
  return;
@@ -1073,6 +1209,27 @@ export class MessageBridge {
1073
1209
  // 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
1074
1210
  const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
1075
1211
  const cmdContent = contentForCmd || content;
1212
+ let autoMode = false;
1213
+ if (resolvedChannelType === 'aun' && chatType === 'group' && selfAid && msg.source !== 'handoff') {
1214
+ try {
1215
+ const config = resolveEffective({ self: selfAid, peerKey: formatPeerKey('aun', msg.groupId || msg.channelId) }, { cache: true, expand: true });
1216
+ autoMode = config.mentionMode === 'auto';
1217
+ }
1218
+ catch (error) {
1219
+ logger.warn(`[MessageBridge] Auto mention configuration unavailable: ${String(error)}`);
1220
+ }
1221
+ }
1222
+ if (autoMode && !msg.isMentioned && /^\//.test(cmdContent)) {
1223
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', msg.channelId, selfAid || '', channelKey);
1224
+ appendMessageLog(chatDir, buildInboundEntry({
1225
+ from: msg.peerId, to: selfAid || '', chatType: 'group', groupId: msg.groupId,
1226
+ threadId: msg.threadId, msgId: msg.messageId, content: msg.messageLogContent ?? content,
1227
+ timestamp: receivedAt, msgType: 'command', groupAttention: 'command', groupDelivery: 'filtered',
1228
+ }));
1229
+ if (msg.messageId)
1230
+ adapter?.acknowledge?.(msg.messageId).catch(() => { });
1231
+ return;
1232
+ }
1076
1233
  if (msg.source !== 'handoff' && !executionPermissionOverride) {
1077
1234
  const groupGuard = guardGroupSlashCommand(cmdContent, chatType);
1078
1235
  if (groupGuard?.kind === 'command.error') {
@@ -1154,6 +1311,72 @@ export class MessageBridge {
1154
1311
  logger.info(`[MessageBridge] Thread creation denied (silent drop): channel=${channelName} channelId=${msg.channelId} thread=${msg.threadId} sender=${msg.peerId}`);
1155
1312
  return;
1156
1313
  }
1314
+ let autoResult;
1315
+ if (autoMode && selfAid && msg.groupId && !executionPermissionOverride
1316
+ && !isCmd && !/\[EvolCore\.(receive|reply|agent)\]/.test(content)) {
1317
+ const attentionItem = {
1318
+ value: msg, selfAid, groupId: msg.groupId, threadId: msg.threadId || '', channelKey,
1319
+ sessionId: msg.replyContext?.sessionId
1320
+ ?? this.sessionManager.getActiveSessionSync?.(channelName, msg.channelId, resolvedChannelType, selfAid)?.id,
1321
+ projectPath: owningAgent?.projectPath ?? this.defaultProjectPath,
1322
+ messageId: msg.messageId, sender: msg.peerId, role: identity.role,
1323
+ mentions: (msg.mentions ?? []).flatMap(mention => {
1324
+ if (typeof mention.aid === 'string')
1325
+ return [mention.aid];
1326
+ return mention.scope === 'all' ? ['all'] : [];
1327
+ }),
1328
+ content, receivedAt,
1329
+ mainSessionBusy: (() => {
1330
+ try {
1331
+ const active = this.sessionManager.getActiveSessionSync?.(channelName, msg.channelId, resolvedChannelType, selfAid);
1332
+ return !!active && ((this.messageQueue.isProcessing?.(active.id) ?? false)
1333
+ || (this.messageQueue.getQueueLength?.(active.id) ?? 0) > 0);
1334
+ }
1335
+ catch {
1336
+ return false;
1337
+ }
1338
+ })(),
1339
+ };
1340
+ if (msg.isMentioned) {
1341
+ autoResult = {
1342
+ decision: 'interrupt', source: 'mention', leader: true, batch: [attentionItem], background: [],
1343
+ current: [{ source: 'current', message_id: msg.messageId ?? null, sender: msg.peerId,
1344
+ role: identity.role, mentions: attentionItem.mentions,
1345
+ timestamp: new Date(receivedAt).toISOString(), content }],
1346
+ };
1347
+ }
1348
+ else {
1349
+ if (msg.messageId)
1350
+ adapter?.acknowledge?.(msg.messageId).catch(() => { });
1351
+ autoResult = await this.autoMentionGate.offer(attentionItem);
1352
+ if (autoResult.decision === 'ignore') {
1353
+ try {
1354
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', msg.channelId, selfAid, channelKey);
1355
+ appendMessageLogStrict(chatDir, buildInboundEntry({
1356
+ from: msg.peerId, to: selfAid, chatType: 'group', groupId: msg.groupId,
1357
+ threadId: msg.threadId, msgId: msg.messageId, content: msg.messageLogContent ?? content,
1358
+ permMode: identity.role, timestamp: receivedAt, receivedAt: msg.receivedAt,
1359
+ gatewaySeq: msg.gatewaySeq, encrypt: msg.encrypted, peerName: msg.peerName,
1360
+ peerType: msg.peerType, msgType: msg.msgType, payloadType: msg.payloadType,
1361
+ payloadSummary: msg.payloadSummary,
1362
+ groupAttention: autoResult.source === 'fallback' ? 'auto-fallback' : 'auto-ignore',
1363
+ groupDelivery: 'filtered',
1364
+ attentionDecision: { ...autoResult.details, decision: 'ignore', source: autoResult.source },
1365
+ }), true);
1366
+ return;
1367
+ }
1368
+ catch (error) {
1369
+ logger.warn(`[MessageBridge] Auto ignore history append failed, responding: ${String(error)}`);
1370
+ autoResult = { ...autoResult, decision: 'interrupt', source: 'fallback', leader: true, batch: [attentionItem] };
1371
+ }
1372
+ }
1373
+ if (!autoResult.leader)
1374
+ return;
1375
+ }
1376
+ }
1377
+ const attentionDecision = autoResult
1378
+ ? { ...autoResult.details, decision: autoResult.decision, source: autoResult.source }
1379
+ : undefined;
1157
1380
  const metadata = {};
1158
1381
  // 话题会话创建时写入 replyContext(用于 threadId 路由);主会话不写(避免群聊覆盖)
1159
1382
  if (msg.threadId && msg.replyContext)
@@ -1186,9 +1409,10 @@ export class MessageBridge {
1186
1409
  // transaction. Without this per-route single-flight, a second message
1187
1410
  // can observe the just-renewed empty session before the first message
1188
1411
  // has been appended and create another renewed session.
1189
- releaseAdmission = await this.acquireAdmission(admissionRoute);
1412
+ if (!releaseAdmission)
1413
+ releaseAdmission = await this.acquireAdmission(admissionRoute);
1190
1414
  const hadMainSession = msg.threadId ? true : this.sessionManager.hasMainSession(channelName, msg.channelId, msg.channelType || effectiveChannelType, msg.selfAID || selfAid);
1191
- let session = await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, this.extractTopicName(msg), msg.peerId, chatType, owningAgent?.baseagent, msg.selfAID, msg.channelType || effectiveChannelType, msg.peerType, identity, readTopicLable(msg) ?? readTopicLable(msg.replyContext?.metadata));
1415
+ let session = diagnosticSession ?? await this.sessionManager.getOrCreateSession(channelName, msg.channelId, effectiveProjectPath, msg.threadId, Object.keys(metadata).length ? metadata : undefined, this.extractTopicName(msg), msg.peerId, chatType, owningAgent?.baseagent, msg.selfAID, msg.channelType || effectiveChannelType, msg.peerType, identity, readTopicLabel(msg) ?? readTopicLabel(msg.replyContext?.metadata), readTopicStatus(msg) ?? readTopicStatus(msg.replyContext?.metadata));
1192
1416
  if (session.threadId && typeof this.messageQueue.reserveSessionIngress === 'function') {
1193
1417
  releaseSessionIngress = this.messageQueue.reserveSessionIngress(session.id);
1194
1418
  }
@@ -1201,7 +1425,7 @@ export class MessageBridge {
1201
1425
  const inboundCausation = restoredCausation
1202
1426
  ? deriveCausation(restoredCausation)
1203
1427
  : createRootCausation();
1204
- const renewResult = await this.sessionRenewService.resolve({
1428
+ const renewResult = session.metadata?.diagnostic ? { session } : await this.sessionRenewService.resolve({
1205
1429
  session,
1206
1430
  channelName,
1207
1431
  channelType: msg.channelType || effectiveChannelType,
@@ -1218,6 +1442,32 @@ export class MessageBridge {
1218
1442
  causation: inboundCausation,
1219
1443
  });
1220
1444
  session = renewResult.session;
1445
+ if ('renewed' in renewResult && renewResult.renewed) {
1446
+ const notice = '已自动切换到新会话';
1447
+ try {
1448
+ if (adapter?.send) {
1449
+ await adapter.send(buildEnvelope({
1450
+ taskId: `session-renew-${randomBytes(5).toString('hex')}`,
1451
+ sessionId: session.id,
1452
+ channel: channelName,
1453
+ channelId: msg.channelId,
1454
+ agentName: owningAgent?.name ?? selfAid ?? '<unknown>',
1455
+ chatmode: 'interactive',
1456
+ replyContext: msg.replyContext,
1457
+ }), {
1458
+ kind: 'system.notice',
1459
+ text: notice,
1460
+ subtype: 'session.auto-renewed',
1461
+ });
1462
+ }
1463
+ else {
1464
+ await sendReply(msg.channelId, notice, msg.replyContext);
1465
+ }
1466
+ }
1467
+ catch (error) {
1468
+ logger.warn(`[MessageBridge] Failed to send automatic session-renew notice: ${error instanceof Error ? error.message : String(error)}`);
1469
+ }
1470
+ }
1221
1471
  // 4. 群聊发送者标注由消息渲染层(message-renderer)逐条承担,不再在此硬编码前缀,
1222
1472
  // 消息日志因此保存干净原文。policy.messagePrefix 暂保留(未来清理)。
1223
1473
  // 5. 构造完整消息(channel 字段存实例名,用于 session 精确匹配)
@@ -1244,15 +1494,45 @@ export class MessageBridge {
1244
1494
  mentions: msg.mentions, mentionAids: msg.mentionAids, threadId: msg.threadId,
1245
1495
  isMentioned: msg.isMentioned,
1246
1496
  topicName: this.extractTopicName(msg),
1247
- lable: session.lable,
1497
+ label: session.label,
1498
+ topicStatus: session.topicStatus,
1248
1499
  replyContext: msg.replyContext,
1249
1500
  source: msg.source,
1250
1501
  ...(executionPermissionOverride ? { executionPermissionOverride } : {}),
1251
1502
  boundSessionId: session.id,
1252
1503
  resolvedIdentity: identity,
1253
1504
  mentionMode: msg.mentionMode,
1505
+ ...(autoResult && autoResult.decision !== 'ignore' ? { attentionPolicy: autoResult.decision } : {}),
1506
+ ...(autoResult ? { autoMentionBatch: true } : {}),
1254
1507
  causation: inboundCausation,
1255
1508
  };
1509
+ if (autoResult && autoResult.batch.length > 1) {
1510
+ fullMessage.content = autoResult.batch.map(item => item.content).join('\n');
1511
+ fullMessage.sourceMessageIds = autoResult.batch.flatMap(item => item.messageId ? [item.messageId] : []);
1512
+ fullMessage.items = autoResult.batch.map(item => ({
1513
+ peerId: item.sender, peerName: item.value.peerName, peerType: item.value.peerType,
1514
+ peerRole: item.role, content: item.content,
1515
+ mentions: item.value.mentions, mentionAids: item.value.mentionAids,
1516
+ images: item.value.images, timestamp: item.receivedAt,
1517
+ receivedAt: item.value.receivedAt, gatewaySeq: item.value.gatewaySeq,
1518
+ encrypted: item.value.encrypted,
1519
+ sameDevice: item.value.sameDevice, sameNetwork: item.value.sameNetwork,
1520
+ sameEgressIp: item.value.sameEgressIp,
1521
+ }));
1522
+ fullMessage.images = autoResult.batch.flatMap(item => item.value.images ?? []);
1523
+ fullMessage.mentions = autoResult.batch.flatMap(item => item.value.mentions ?? []);
1524
+ fullMessage.timestamp = autoResult.batch[0].receivedAt;
1525
+ fullMessage.receivedAt = autoResult.batch[0].receivedAt;
1526
+ fullMessage.encrypted = autoResult.batch.some(item => item.value.encrypted === true) ? true : fullMessage.encrypted;
1527
+ if (fullMessage.encrypted && fullMessage.replyContext) {
1528
+ fullMessage.replyContext = {
1529
+ ...fullMessage.replyContext,
1530
+ metadata: { ...fullMessage.replyContext.metadata, encrypted: true },
1531
+ };
1532
+ }
1533
+ if (!autoResult.batch.every(item => item.role === identity.role))
1534
+ fullMessage.batchRole = 'visitor';
1535
+ }
1256
1536
  recordCausationSpan(inboundCausation, 'message.inbound', {
1257
1537
  status: 'completed',
1258
1538
  refs: { messageId: msg.messageId, sessionId: session.id },
@@ -1285,6 +1565,11 @@ export class MessageBridge {
1285
1565
  msgType: msg.msgType,
1286
1566
  payloadType: msg.payloadType,
1287
1567
  payloadSummary: msg.payloadSummary,
1568
+ groupAttention: autoResult?.source === 'mention' ? 'auto-mention'
1569
+ : autoResult?.source === 'fallback' ? 'auto-fallback'
1570
+ : autoResult?.decision === 'defer' ? 'auto-defer'
1571
+ : autoResult ? 'auto-interrupt' : undefined,
1572
+ attentionDecision,
1288
1573
  }) };
1289
1574
  })();
1290
1575
  let handoffCandidate = false;
@@ -1317,6 +1602,23 @@ export class MessageBridge {
1317
1602
  }
1318
1603
  if (!handoffCandidate)
1319
1604
  appendMessageLog(inboundEntry.chatDir, inboundEntry.entry);
1605
+ if (autoResult && autoResult.batch.length > 1) {
1606
+ for (const item of autoResult.batch.slice(0, -1)) {
1607
+ const member = item.value;
1608
+ appendMessageLog(inboundEntry.chatDir, buildInboundEntry({
1609
+ from: item.sender, to: selfAid || '', sessionId: session.id,
1610
+ threadId: session.threadId || null, chatType: 'group', groupId: msg.groupId,
1611
+ msgId: item.messageId, content: member.messageLogContent ?? item.content,
1612
+ permMode: item.role, timestamp: item.receivedAt,
1613
+ receivedAt: member.receivedAt, gatewaySeq: member.gatewaySeq,
1614
+ encrypt: member.encrypted, peerName: member.peerName, peerType: member.peerType,
1615
+ msgType: member.msgType, payloadType: member.payloadType,
1616
+ payloadSummary: member.payloadSummary,
1617
+ groupAttention: autoResult?.decision === 'defer' ? 'auto-defer' : 'auto-interrupt',
1618
+ attentionDecision,
1619
+ }));
1620
+ }
1621
+ }
1320
1622
  // The authoritative session choice is now paired with its first
1321
1623
  // conversation record. Release before waiting for queue completion so
1322
1624
  // later group messages can still exercise the normal interrupt/FIFO
@@ -1335,16 +1637,24 @@ export class MessageBridge {
1335
1637
  adapter?.acknowledge?.(fullMessage.messageId).catch(() => { });
1336
1638
  }
1337
1639
  const isInterrupt = chatType !== 'group' && !handoffCandidate;
1640
+ const attentionPolicy = fullMessage.attentionPolicy;
1641
+ const shouldInterrupt = isInterrupt || attentionPolicy === 'interrupt';
1338
1642
  const enqueueAgentName = owningAgent?.name ?? '<unknown>';
1339
1643
  const selfAID = session.selfAID;
1340
1644
  const doEnqueue = async (m) => {
1645
+ // A cancellation can arrive while this message is being debounced.
1646
+ if (!isDiagnosticSessionActive(session, this.sessionManager.getChatDir(session)))
1647
+ return;
1341
1648
  const enqueue = handoffCandidate ? this.messageQueue.enqueuePersisted.bind(this.messageQueue) : this.messageQueue.enqueue.bind(this.messageQueue);
1342
1649
  try {
1343
1650
  return await enqueue(session.id, m, session.projectPath, {
1344
- interruptible: isInterrupt,
1345
- interruptSamePeer: !isInterrupt, // 群聊:同人连发且队列无他人时打断
1651
+ interruptible: shouldInterrupt,
1652
+ attentionPolicy,
1653
+ interruptSamePeer: !shouldInterrupt && !attentionPolicy, // 旧群聊路径保留同人连发打断
1654
+ priority: attentionPolicy === 'interrupt',
1346
1655
  agentName: enqueueAgentName,
1347
- role: !isInterrupt ? (session.identity?.role ?? 'none') : undefined,
1656
+ role: chatType === 'group' ? (session.identity?.role ?? 'none')
1657
+ : (!shouldInterrupt ? (session.identity?.role ?? 'none') : undefined),
1348
1658
  sessionKeyField: session.sessionKey,
1349
1659
  selfAID, // ← 传递 selfAID,用于队列隔离
1350
1660
  onPersisted: releaseIngress,
@@ -1704,7 +2014,7 @@ export class MessageBridge {
1704
2014
  ...(msg.peerDeviceId ? { toDeviceId: msg.peerDeviceId } : {}),
1705
2015
  metadata: {
1706
2016
  ...(msg.replyContext?.metadata ?? {}),
1707
- ...evolMenuResponseTransportMetadata(msg.protectedHeaders?.sdk_version),
2017
+ ...evolMenuResponseTransportMetadata(msg.protectedHeaders?.sdk_version, msg.protectedHeaders?.menu_volatile_v1),
1708
2018
  ...(isAgentStorageMenuRequest(request) ? { encrypted: true } : {}),
1709
2019
  },
1710
2020
  };
@@ -345,6 +345,7 @@ export function buildInboundEntry(opts) {
345
345
  payloadSummary: opts.payloadSummary,
346
346
  groupAttention: opts.groupAttention,
347
347
  groupDelivery: opts.groupDelivery,
348
+ attentionDecision: opts.attentionDecision,
348
349
  content: opts.content,
349
350
  replyTo: opts.replyTo ?? null,
350
351
  agent: null,