evolcore 0.0.11 → 0.0.13

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 (164) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/codex-managed-hook.mjs +80 -0
  3. package/bin/ec-safe-output.js +26 -7
  4. package/dist/agents/claude-runner.js +86 -7
  5. package/dist/agents/codex-app-server-client.js +174 -20
  6. package/dist/agents/codex-runner.js +375 -92
  7. package/dist/agents/ecagent-runner.js +39 -4
  8. package/dist/agents/gemini-runner.js +1 -0
  9. package/dist/aun/aid/client.js +11 -21
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/managed-operation.js +107 -0
  12. package/dist/aun/aid/store.js +13 -7
  13. package/dist/aun/group-fs-download.js +56 -0
  14. package/dist/aun/group-identity.js +1 -2
  15. package/dist/aun/msg/group.js +91 -44
  16. package/dist/aun/msg/index.js +1 -2
  17. package/dist/aun/msg/managed-operation.js +865 -0
  18. package/dist/aun/msg/mention-schema.js +20 -0
  19. package/dist/aun/msg/p2p.js +7 -0
  20. package/dist/aun/msg/upload.js +16 -1
  21. package/dist/aun/outbox.js +31 -2
  22. package/dist/aun/rpc/client.js +66 -0
  23. package/dist/aun/rpc/index.js +1 -2
  24. package/dist/aun/storage/{upload.js → client.js} +11 -2
  25. package/dist/aun/storage/index.js +1 -2
  26. package/dist/channels/aun.js +655 -240
  27. package/dist/channels/daemon.js +13 -0
  28. package/dist/cli/agent-command.js +56 -8
  29. package/dist/cli/agent.js +108 -31
  30. package/dist/cli/aun-commands.js +270 -69
  31. package/dist/cli/bench.js +12 -3
  32. package/dist/cli/cli-argv.js +10 -0
  33. package/dist/cli/config.js +9 -17
  34. package/dist/cli/ctl-command.js +1 -1
  35. package/dist/cli/daemon-commands.js +70 -55
  36. package/dist/cli/fs-command.js +79 -10
  37. package/dist/cli/handoff-command.js +3 -0
  38. package/dist/cli/index.js +30 -2
  39. package/dist/cli/init.js +77 -8
  40. package/dist/cli/managed-command-guard.js +41 -0
  41. package/dist/cli/model.js +11 -4
  42. package/dist/cli/queue-command.js +53 -1
  43. package/dist/cli/response.js +71 -0
  44. package/dist/cli/restart-monitor.js +7 -32
  45. package/dist/cli/trigger-command.js +11 -0
  46. package/dist/config/access-policy.js +1 -2
  47. package/dist/config/builtin-roles.js +9 -0
  48. package/dist/config/config-manager.js +21 -1
  49. package/dist/config/contact-book-store.js +11 -3
  50. package/dist/config/contact-request-service.js +161 -15
  51. package/dist/config/mention-mode.js +8 -24
  52. package/dist/config/peer-role-resolver.js +6 -2
  53. package/dist/config/role-migration-startup.js +53 -0
  54. package/dist/config/role-store.js +1 -1
  55. package/dist/core/auth/agent-delegation.js +1 -2
  56. package/dist/core/auth/auth-gateway.js +29 -1
  57. package/dist/core/auth/authorization-audit.js +15 -0
  58. package/dist/core/auth/operation-authorizer.js +31 -7
  59. package/dist/core/auth/operation-catalog.js +196 -25
  60. package/dist/core/bootstrap-messages.js +25 -0
  61. package/dist/core/bootstrap-service.js +64 -4
  62. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  63. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  64. package/dist/core/capability/skill-discovery.js +55 -0
  65. package/dist/core/channel-loader.js +1 -1
  66. package/dist/core/command/cli-intent-parser.js +9 -0
  67. package/dist/core/command/command-handler.js +650 -42
  68. package/dist/core/command/connect-menu.js +15 -0
  69. package/dist/core/command/group-menu.js +4 -4
  70. package/dist/core/command/menu-catalog.js +370 -0
  71. package/dist/core/command/menu-handler.js +230 -51
  72. package/dist/core/command/menu-protocol.js +94 -2
  73. package/dist/core/command/slash-gate.js +5 -2
  74. package/dist/core/command/slash-handler.js +99 -31
  75. package/dist/core/daemon-file-cache.js +2 -1
  76. package/dist/core/data-migration.js +9 -2
  77. package/dist/core/event-catalog.js +6 -0
  78. package/dist/core/evolagent.js +4 -2
  79. package/dist/core/handoff/dispatcher.js +5 -2
  80. package/dist/core/handoff/runtime.js +157 -22
  81. package/dist/core/handoff/store.js +29 -1
  82. package/dist/core/handoff/types.js +1 -0
  83. package/dist/core/interaction-router.js +10 -0
  84. package/dist/core/message/im-renderer.js +19 -0
  85. package/dist/core/message/logical-queue-bridge.js +1 -0
  86. package/dist/core/message/mention-schema.js +126 -0
  87. package/dist/core/message/message-bridge.js +298 -88
  88. package/dist/core/message/message-log.js +3 -0
  89. package/dist/core/message/message-queue.js +268 -39
  90. package/dist/core/message/message-utils.js +81 -4
  91. package/dist/core/message/response-engine.js +161 -77
  92. package/dist/core/message/stream-debouncer.js +10 -2
  93. package/dist/core/permission/approval-gateway.js +1 -0
  94. package/dist/core/permission/ec-command-parser.js +243 -18
  95. package/dist/core/permission/index.js +1 -1
  96. package/dist/core/permission/mode.js +15 -0
  97. package/dist/core/permission/readonly-shell-query.js +148 -8
  98. package/dist/core/permission/sandbox-runtime.js +43 -1
  99. package/dist/core/permission/tool-policy.js +322 -57
  100. package/dist/core/protected-paths.js +114 -4
  101. package/dist/core/session/session-manager.js +42 -4
  102. package/dist/core/session/session-mapper.js +20 -1
  103. package/dist/eck/detect.js +18 -1
  104. package/dist/eck/group-rules-sync.js +15 -44
  105. package/dist/eck/manifest-engine.js +39 -13
  106. package/dist/index.js +541 -180
  107. package/dist/ipc.js +232 -10
  108. package/dist/paths.js +3 -0
  109. package/dist/response-system/coordinator.js +6 -4
  110. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  111. package/dist/response-system/modes/single-session/index.js +14 -1
  112. package/dist/response-system/selector.js +6 -5
  113. package/dist/trigger/anomaly-store.js +4 -0
  114. package/dist/trigger/feedback.js +64 -11
  115. package/dist/trigger/manager.js +21 -1
  116. package/dist/trigger/parser.js +14 -2
  117. package/dist/trigger/scheduler.js +1 -0
  118. package/dist/trigger/script-executor.js +1 -0
  119. package/dist/trigger/validation.js +67 -6
  120. package/dist/utils/codex-app-server-registry.js +90 -0
  121. package/dist/utils/codex-cli.js +5 -1
  122. package/dist/utils/cross-platform.js +15 -0
  123. package/dist/utils/ecweb-supervisor.js +332 -0
  124. package/dist/utils/error-utils.js +10 -0
  125. package/dist/utils/npm-ops.js +5 -12
  126. package/dist/utils/tool-summary.js +11 -0
  127. package/kits/docs/channels/aun.md +1 -1
  128. package/kits/docs/context-assembly.md +2 -2
  129. package/kits/docs/evolcore/agent.md +11 -6
  130. package/kits/docs/evolcore/aid.md +14 -0
  131. package/kits/docs/evolcore/trigger.md +17 -1
  132. package/kits/docs/prompt-loading-architecture.md +1 -2
  133. package/kits/docs/venues/group.md +1 -1
  134. package/kits/eck_manifest.json +0 -12
  135. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  136. package/kits/rules/04-relation.md +1 -1
  137. package/kits/rules/05-venue.md +2 -2
  138. package/kits/schemas/role-config.schema.1.json +1 -0
  139. package/kits/schemas/single-session.schema.1.json +3 -3
  140. package/kits/schemas/single-session.schema.2.json +3 -3
  141. package/kits/templates/roles/admin.json +27 -0
  142. package/kits/templates/roles/member.json +34 -1
  143. package/kits/templates/roles/visitor.json +35 -3
  144. package/kits/templates/system-fragments/bootstrap.md +1 -1
  145. package/kits/templates/system-fragments/channel.md +1 -1
  146. package/kits/templates/system-fragments/session.md +1 -1
  147. package/package.json +2 -2
  148. package/dist/aun/msg/payload-type.js +0 -27
  149. package/dist/aun/rpc/caller.js +0 -42
  150. package/dist/aun/rpc/connection.js +0 -25
  151. package/dist/aun/storage/manage.js +0 -10
  152. package/dist/config/access-policy-domain.js +0 -18
  153. package/dist/config/config-batch-get.js +0 -11
  154. package/dist/config/owner-policy.js +0 -4
  155. package/dist/config/role-config-v4-startup.js +0 -32
  156. package/dist/config/role-config-v5-startup.js +0 -27
  157. package/dist/core/auth/trigger-authorization.js +0 -15
  158. package/dist/core/command/evol-menu-version-gate.js +0 -39
  159. package/dist/core/interaction-registration.js +0 -10
  160. package/dist/core/permission/execution-sandbox.js +0 -16
  161. package/dist/core/relation/peer-key.js +0 -1
  162. package/dist/core/session/session-key.js +0 -24
  163. package/dist/eck/baseagent-caps.js +0 -18
  164. package/dist/eck/rules-loader.js +0 -28
@@ -11,7 +11,7 @@ import { StreamIdleMonitor } from './stream-idle-monitor.js';
11
11
  import { logger } from '../../utils/logger.js';
12
12
  import { isHostChinese } from '../../utils/locale.js';
13
13
  import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
14
- import { isEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
14
+ import { isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
15
15
  import { summarizeToolInput } from '../../utils/tool-summary.js';
16
16
  import { getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
17
17
  import { renderKitSections } from '../../eck/kit-renderer.js';
@@ -22,7 +22,7 @@ import { createRootCausation, deriveCausation, normalizeCausation } from '../cau
22
22
  import { recordCausationLink, recordCausationSpan } from '../causation/audit.js';
23
23
  import { normalizeBaseagent } from '../../agents/baseagent.js';
24
24
  import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
25
- import { buildEnvelope } from './message-utils.js';
25
+ import { buildEnvelope, replyContextFromSession, withEnvelopeReplyContext } from './message-utils.js';
26
26
  import { isSystemOrServicePeer, resolveChatMode } from './peer-mode.js';
27
27
  import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
28
28
  import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
@@ -33,7 +33,6 @@ import { eligibleFallbackModels } from '../model/model-fallback.js';
33
33
  import { RetryScheduler } from './retry-scheduler.js';
34
34
  import { constrainRuntimePermissionMode, resolveRuntimeStringField } from '../role/runtime-policy.js';
35
35
  import { resolveEffective, resolveEffectiveFieldWithSource } from '../../config/config-manager.js';
36
- import { dispatchToMentionMode } from '../../config/mention-mode.js';
37
36
  import { authorizationConfigRevision, checkRoleAccess, getFirstStaticAgentOwner, listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, roleToSessionIdentity, } from '../../config/peer-role-resolver.js';
38
37
  import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../../stats/writer.js';
39
38
  import { normalizeUsage } from '../../stats/normalizer.js';
@@ -137,6 +136,7 @@ const RETRY_EXHAUSTED_MARK = Symbol('evolcore.retryExhausted');
137
136
  const RETRY_MADE_PROGRESS_MARK = Symbol('evolcore.retryMadeProgress');
138
137
  const RETRY_HEALTH_RECORDED_MARK = Symbol('evolcore.retryHealthRecorded');
139
138
  const MODEL_FALLBACK_EXHAUSTED_MARK = Symbol('evolcore.modelFallbackExhausted');
139
+ const RETRY_INPUT_ALREADY_SUBMITTED_MARK = Symbol('evolcore.retryInputAlreadySubmitted');
140
140
  function markRetryExhausted(error, retries) {
141
141
  if (error && typeof error === 'object') {
142
142
  error[RETRY_EXHAUSTED_MARK] = retries;
@@ -177,6 +177,21 @@ function getModelFallbackExhaustedMessage(error) {
177
177
  : undefined;
178
178
  return typeof message === 'string' ? message : undefined;
179
179
  }
180
+ function markRetryInputAlreadySubmitted(error) {
181
+ if (error && typeof error === 'object') {
182
+ error[RETRY_INPUT_ALREADY_SUBMITTED_MARK] = true;
183
+ // A terminal delivery guard, rather than any buffered activity, owns the
184
+ // final user-visible explanation.
185
+ delete error._errorAlreadySent;
186
+ }
187
+ }
188
+ function wasRetryInputAlreadySubmitted(error) {
189
+ return !!(error && typeof error === 'object' && error[RETRY_INPUT_ALREADY_SUBMITTED_MARK] === true);
190
+ }
191
+ function formatRetryInputAlreadySubmittedMessage(error) {
192
+ const reason = getErrorMessage(error, undefined, false) || 'Codex 暂时不可用';
193
+ return `⚠️ Codex 已接收本次消息,但本轮未能确认完成。为避免在会话中重复写入同一消息,系统未自动重试。请检查会话状态后重新发送。\n原因:${reason}`;
194
+ }
180
195
  function isNonErrorInterruptReason(reason) {
181
196
  return reason === 'new_message'
182
197
  || reason === 'stop'
@@ -493,7 +508,7 @@ export class ResponseEngine {
493
508
  return;
494
509
  const available = error.availableBaseagents.length ? error.availableBaseagents.join(', ') : '(none)';
495
510
  const text = `❌ 当前会话绑定的 baseagent 不可用: ${error.baseagent}\nAgent: ${error.evolagentName}\n可用: ${available}\n请使用 /baseagent 切换到可用后端。`;
496
- const sessionReplyContext = { ...(replyContext ?? {}), sessionId: session.id };
511
+ const sessionReplyContext = { ...replyContextFromSession(session, replyContext), sessionId: session.id };
497
512
  await channelInfo.adapter.send(buildEnvelope({
498
513
  taskId: `system-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`,
499
514
  sessionId: session.id,
@@ -656,7 +671,7 @@ export class ResponseEngine {
656
671
  agentName: agent.name,
657
672
  chatmode: this.resolveEffectiveChatmodeForSession(session, channelKey, session.channelId),
658
673
  replyContext: {
659
- ...(session.metadata?.replyContext ?? {}),
674
+ ...replyContextFromSession(session),
660
675
  sessionId: session.id,
661
676
  threadId: session.threadId || session.metadata?.replyContext?.threadId,
662
677
  },
@@ -727,7 +742,19 @@ export class ResponseEngine {
727
742
  const selfAid = runtime?.selfAid || session?.selfAID;
728
743
  if (!selfAid)
729
744
  return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'self agent not found' };
730
- return this.handoffRuntime.returnHandoff({
745
+ return this.returnAuthorizedHandoffResult({
746
+ sessionId: params.sessionId,
747
+ handoffId: params.handoffId,
748
+ content: params.content,
749
+ }, selfAid);
750
+ }
751
+ /** Execute a handoff return after the daemon IPC boundary has authorized the task delegation. */
752
+ async returnAuthorizedHandoffResult(params, selfAid) {
753
+ if (!this.handoffRuntime) {
754
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff runtime not configured' };
755
+ }
756
+ const runtime = this.activeTaskRuntimeContexts.get(params.sessionId);
757
+ return await this.handoffRuntime.returnHandoff({
731
758
  selfAid,
732
759
  currentSessionId: params.sessionId,
733
760
  handoffId: params.handoffId,
@@ -926,7 +953,7 @@ export class ResponseEngine {
926
953
  }
927
954
  agent.registerStream(streamKey, retryStream);
928
955
  return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
929
- undefined, turnLease);
956
+ undefined, turnLease, opts.permissionMode);
930
957
  }
931
958
  // Dropping the whole session hides the root cause and can replay side
932
959
  // effects in unattended trigger tasks. Surface an actionable failure
@@ -1152,6 +1179,14 @@ export class ResponseEngine {
1152
1179
  async processMessage(message) {
1153
1180
  const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
1154
1181
  const totalExecutionMs = this.totalExecutionLimitMs();
1182
+ if (message.handoffDelivery
1183
+ && this.handoffRuntime
1184
+ && this.handoffRuntime.isDiscardedDelivery(message)) {
1185
+ logger.info(`[ResponseEngine] Skip stale handoff delivery before session resolution:`
1186
+ + ` direction=${message.handoffDelivery.direction}`
1187
+ + ` handoff=${message.handoffDelivery.handoffId}`);
1188
+ return;
1189
+ }
1155
1190
  // 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
1156
1191
  // message.channel 现在存实例名(channelName),可直接用于精确路由
1157
1192
  const { session, absoluteProjectPath } = await this.resolveSession(message);
@@ -1180,7 +1215,7 @@ export class ResponseEngine {
1180
1215
  logger.warn(`[ResponseEngine] Trigger execution identity missing: trigger=${message.triggerMeta?.triggerId ?? '<none>'}`);
1181
1216
  const channelInfo = this.resolveChannelInfo(accessChannelKey);
1182
1217
  if (channelInfo) {
1183
- await channelInfo.adapter.send({
1218
+ await channelInfo.adapter.send(buildEnvelope({
1184
1219
  taskId: `trigger-identity-missing-${Date.now()}`,
1185
1220
  sessionId: session.id,
1186
1221
  channel: accessChannelKey,
@@ -1189,7 +1224,7 @@ export class ResponseEngine {
1189
1224
  chatmode: 'interactive',
1190
1225
  replyContext: message.replyContext,
1191
1226
  timestamp: Date.now(),
1192
- }, {
1227
+ }), {
1193
1228
  kind: 'system.error',
1194
1229
  text: 'Trigger 创建者身份缺失,已拒绝执行',
1195
1230
  subtype: 'trigger_identity_missing',
@@ -1211,7 +1246,7 @@ export class ResponseEngine {
1211
1246
  const channelInfo = this.resolveChannelInfo(channelKey);
1212
1247
  if (channelInfo) {
1213
1248
  try {
1214
- await channelInfo.adapter.send({
1249
+ await channelInfo.adapter.send(buildEnvelope({
1215
1250
  taskId: `access-denied-${Date.now()}`,
1216
1251
  sessionId: session.id,
1217
1252
  channel: channelKey,
@@ -1220,7 +1255,7 @@ export class ResponseEngine {
1220
1255
  chatmode: 'interactive',
1221
1256
  replyContext: message.replyContext,
1222
1257
  timestamp: Date.now(),
1223
- }, {
1258
+ }), {
1224
1259
  kind: 'system.error',
1225
1260
  text: '暂无权限访问本 agent,请联系 agent 管理员授权访问',
1226
1261
  subtype: 'access_denied',
@@ -1478,7 +1513,12 @@ export class ResponseEngine {
1478
1513
  }
1479
1514
  /** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
1480
1515
  async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, outputState = { middleOutputMode: 'all' }, timeoutControl) {
1481
- const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
1516
+ // The model must see the time at which the adapter accepted the input.
1517
+ // Runner start is recorded separately so queue delay never rewrites input
1518
+ // chronology after a historical replay or a long backlog.
1519
+ const inboundTimestamp = message.receivedAt ?? message.enqueuedAt ?? message.timestamp ?? Date.now();
1520
+ const runnerStartedAt = Date.now();
1521
+ const messageId = `${message.channel}_${message.channelId}_${inboundTimestamp}`;
1482
1522
  const channelKey = this.messageChannelKey(message, session);
1483
1523
  const channelInfo = this.resolveChannelInfo(channelKey);
1484
1524
  const taskAgentAid = message.selfAID || session.selfAID;
@@ -1705,7 +1745,7 @@ export class ResponseEngine {
1705
1745
  const taskReplyContext = () => {
1706
1746
  const base = this.getReplyContext(message);
1707
1747
  return {
1708
- ...(base ?? {}),
1748
+ ...replyContextFromSession(session, base),
1709
1749
  sessionId: session.id,
1710
1750
  threadId: session.threadId || base?.threadId,
1711
1751
  metadata: withTaskMetadata(base?.metadata),
@@ -1720,11 +1760,11 @@ export class ResponseEngine {
1720
1760
  messageId: message.messageId, peerId: message.peerId, content: message.content,
1721
1761
  peerType: message.peerType || session.metadata?.peerType,
1722
1762
  chatType: chatType, isMentioned: message.isMentioned,
1723
- mentionAids: message.mentionAids, source: message.source,
1763
+ mentions: message.mentions, mentionAids: message.mentionAids, source: message.source,
1724
1764
  },
1725
1765
  modeConfig: resolvedMode.context.modeConfig,
1726
1766
  state: modeState,
1727
- isSendCommand: (toolName, toolInput) => isEvolcoreSendCommandForSession(toolName, toolInput, session.channelId),
1767
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, session.selfAID || message.selfAID),
1728
1768
  logger,
1729
1769
  } : null;
1730
1770
  if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
@@ -1825,7 +1865,7 @@ export class ResponseEngine {
1825
1865
  // Kept outside the task body so a thrown terminal error can preserve the
1826
1866
  // work already performed in its trigger audit metadata.
1827
1867
  let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
1828
- let startTime = Date.now();
1868
+ let startTime = runnerStartedAt;
1829
1869
  let protocolReplayAttempts = 0;
1830
1870
  try {
1831
1871
  const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
@@ -1834,7 +1874,10 @@ export class ResponseEngine {
1834
1874
  msgId: messageId,
1835
1875
  sessionId: session.id,
1836
1876
  dir: 'inbound',
1837
- status: 'received'
1877
+ status: 'received',
1878
+ receivedAt: message.receivedAt ?? inboundTimestamp,
1879
+ enqueuedAt: message.enqueuedAt,
1880
+ processingStartedAt: runnerStartedAt,
1838
1881
  });
1839
1882
  this.eventBus.publish({
1840
1883
  type: 'message:received',
@@ -1843,7 +1886,10 @@ export class ResponseEngine {
1843
1886
  channelId: message.channelId,
1844
1887
  content: message.content,
1845
1888
  agentName: agentNameForStats,
1846
- timestamp: Date.now()
1889
+ timestamp: inboundTimestamp,
1890
+ receivedAt: message.receivedAt ?? inboundTimestamp,
1891
+ enqueuedAt: message.enqueuedAt,
1892
+ processingStartedAt: runnerStartedAt,
1847
1893
  });
1848
1894
  // ── 硬上限检查:超限直接返回提示,不调模型 ──
1849
1895
  {
@@ -1988,7 +2034,7 @@ export class ResponseEngine {
1988
2034
  opts.metadata = withTaskMetadata(opts.metadata);
1989
2035
  if (payload.kind.startsWith('status.'))
1990
2036
  this.touchAgentActivity(channelKey);
1991
- const enrichedEnvelope = { ...envelope, replyContext: opts };
2037
+ const enrichedEnvelope = withEnvelopeReplyContext(envelope, opts);
1992
2038
  snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'sent' });
1993
2039
  await adapter.send(enrichedEnvelope, payload);
1994
2040
  },
@@ -2013,7 +2059,7 @@ export class ResponseEngine {
2013
2059
  const capturedReplyContext = taskReplyContext();
2014
2060
  // 设置权限审批的消息发送回调(指向当前渠道)
2015
2061
  const permissionPrompt = async (text) => {
2016
- await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text, isFinal: false });
2062
+ await adapter.send(withEnvelopeReplyContext(envelope, capturedReplyContext), { kind: 'result.text', text, isFinal: false });
2017
2063
  };
2018
2064
  const authChatType = configChatType;
2019
2065
  const authConversationId = authChatType === 'group'
@@ -2027,15 +2073,16 @@ export class ResponseEngine {
2027
2073
  : undefined;
2028
2074
  const permissionAdapter = triggerOriginChannelInfo?.adapter ?? adapter;
2029
2075
  const permissionChannelId = authorizationIdentity?.actorId ?? capturedChannelId;
2030
- const permissionReplyContext = authorizationIdentity ? undefined : capturedReplyContext;
2076
+ const permissionReplyContext = authorizationIdentity
2077
+ ? { delivery: { chatType: 'private' } }
2078
+ : capturedReplyContext;
2031
2079
  let permissionTerminalFailure;
2032
2080
  const permissionPromptForOrigin = async (text) => {
2033
- await permissionAdapter.send({
2081
+ await permissionAdapter.send(withEnvelopeReplyContext({
2034
2082
  ...envelope,
2035
2083
  channel: authorizationIdentity?.originChannelKey ?? envelope.channel,
2036
2084
  channelId: permissionChannelId,
2037
- replyContext: permissionReplyContext,
2038
- }, { kind: 'result.text', text, isFinal: false });
2085
+ }, permissionReplyContext), { kind: 'result.text', text, isFinal: false });
2039
2086
  };
2040
2087
  agent.setSendPrompt(permissionPromptForOrigin);
2041
2088
  const owningAgentForAuth = owningAgentForTask;
@@ -2118,11 +2165,21 @@ export class ResponseEngine {
2118
2165
  },
2119
2166
  };
2120
2167
  })();
2168
+ let effectivePermissionMode = 'readonly';
2121
2169
  const recordExecutionAnomaly = triggerRunId
2122
2170
  ? (anomaly) => {
2123
- recordTriggerExecutionAnomaly(triggerRunId, anomaly);
2171
+ recordTriggerExecutionAnomaly(triggerRunId, {
2172
+ ...anomaly,
2173
+ agentAid: anomaly.agentAid ?? session.selfAID ?? message.selfAID,
2174
+ sessionId: anomaly.sessionId ?? session.id,
2175
+ permissionMode: anomaly.permissionMode ?? effectivePermissionMode,
2176
+ });
2124
2177
  }
2125
2178
  : undefined;
2179
+ const runModeConfig = resolvedMode?.mode.configureRun && modeProcessCtx
2180
+ ? resolvedMode.mode.configureRun(modeProcessCtx)
2181
+ : undefined;
2182
+ const pureSessionPolicyHook = runModeConfig?.policyHook;
2126
2183
  // 设置权限审批的交互上下文(支持交互卡片)
2127
2184
  agent.setPermissionContext?.(session.id, {
2128
2185
  sendPrompt: permissionPromptForOrigin,
@@ -2144,6 +2201,7 @@ export class ResponseEngine {
2144
2201
  approvalRouting,
2145
2202
  approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
2146
2203
  recordExecutionAnomaly,
2204
+ preToolUsePolicyHook: pureSessionPolicyHook,
2147
2205
  armApprovedDelegationCommand: this.agentDelegationRegistry
2148
2206
  ? (carrierToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
2149
2207
  carrierToken,
@@ -2170,7 +2228,7 @@ export class ResponseEngine {
2170
2228
  },
2171
2229
  },
2172
2230
  permissionStateChanged: async (permissionEvent) => {
2173
- const executionEnvelope = { ...envelope, replyContext: capturedReplyContext };
2231
+ const executionEnvelope = withEnvelopeReplyContext(envelope, capturedReplyContext);
2174
2232
  if (permissionEvent.state === 'waiting') {
2175
2233
  const payload = {
2176
2234
  kind: 'status.requires_action',
@@ -2184,12 +2242,11 @@ export class ResponseEngine {
2184
2242
  };
2185
2243
  await adapter.send(executionEnvelope, payload);
2186
2244
  if (permissionAdapter !== adapter || permissionChannelId !== capturedChannelId) {
2187
- await permissionAdapter.send({
2245
+ await permissionAdapter.send(withEnvelopeReplyContext({
2188
2246
  ...envelope,
2189
2247
  channel: authorizationIdentity?.originChannelKey ?? envelope.channel,
2190
2248
  channelId: permissionChannelId,
2191
- replyContext: permissionReplyContext,
2192
- }, payload);
2249
+ }, permissionReplyContext), payload);
2193
2250
  }
2194
2251
  return;
2195
2252
  }
@@ -2233,10 +2290,7 @@ export class ResponseEngine {
2233
2290
  // [迁移点2] policyHook 由 ProactiveMode.configureRun 提供(插件决策);
2234
2291
  // 引擎补充副作用:违规时注入提醒到模型上下文 + 行为探针。
2235
2292
  policyHook: (() => {
2236
- const runConfig = resolvedMode?.mode.configureRun && modeProcessCtx
2237
- ? resolvedMode.mode.configureRun(modeProcessCtx)
2238
- : undefined;
2239
- const pluginHook = runConfig?.policyHook;
2293
+ const pluginHook = pureSessionPolicyHook;
2240
2294
  if (!pluginHook)
2241
2295
  return undefined;
2242
2296
  return (toolName, toolInput) => {
@@ -2250,9 +2304,9 @@ export class ResponseEngine {
2250
2304
  agent.injectUserMessage?.(session.id, errorMsg);
2251
2305
  return result;
2252
2306
  }
2253
- // 首工具检查通过(firstToolDone false 翻成 true 且未拦截)
2254
- const stAfter = modeState.get('proactive');
2255
- if (wasFirstPending && stAfter?.firstToolDone) {
2307
+ // 这里只记录审批结果。firstToolDone 必须等真实 tool_use 事件再推进,
2308
+ // 避免伪造 managed hook IPC 提前解除门禁。
2309
+ if (wasFirstPending) {
2256
2310
  snapshot.set(session.id, taskId, { policyHook: { triggered: true, blocked: false, toolName } });
2257
2311
  }
2258
2312
  return undefined;
@@ -2290,8 +2344,6 @@ export class ResponseEngine {
2290
2344
  let handoffPromptRendered = false;
2291
2345
  let runtimeEnv;
2292
2346
  try {
2293
- // 动态构建运行时上下文提示
2294
- const contextParts = [];
2295
2347
  // 通道能力
2296
2348
  const supportsFileMarker = currentChannelType !== 'aun' && !isProactive && !!channelInfo.adapter.capabilities?.file;
2297
2349
  const capParts = [];
@@ -2301,14 +2353,11 @@ export class ResponseEngine {
2301
2353
  capParts.push('图片输出');
2302
2354
  if (!isProactive && channelInfo.adapter.capabilities?.file)
2303
2355
  capParts.push('文件发送');
2304
- // Personal layer
2356
+ // Personal state is exposed to the manifest as vars. The manifest owns
2357
+ // loading persona.md and working.md into the system prompt.
2305
2358
  const owningAgent = owningAgentForTask;
2306
2359
  const persona = owningAgent?.getPersona?.() || undefined;
2307
2360
  const working = owningAgent?.getWorkingMemory?.() || undefined;
2308
- if (persona)
2309
- contextParts.push(persona);
2310
- if (working)
2311
- contextParts.push(`[当前关注]\n${working}`);
2312
2361
  // 计算 peerKey:群聊固定按 groupId/channelId,私聊按发送者 peerId。
2313
2362
  // 这样单条和积压合并批次不会因队列状态不同而切换关系级配置。
2314
2363
  const normalizedBaseagent = normalizeBaseagent(agent.name);
@@ -2316,7 +2365,6 @@ export class ResponseEngine {
2316
2365
  // Trigger 只能降低本次调用权限,不能突破当前角色的权限上限。
2317
2366
  // 作为 per-call 入参随 modelOverride 传入 runQuery —— 与 model/effort 同构,
2318
2367
  // 不写 AgentRunner 实例字段,多对端/多会话并发共享同一 runner 实例时互不污染。
2319
- let effectivePermissionMode;
2320
2368
  const triggerPermissionModeOverride = message.triggerMeta?.permissionModeOverride;
2321
2369
  try {
2322
2370
  effectivePermissionMode = constrainRuntimePermissionMode({
@@ -2532,9 +2580,10 @@ export class ResponseEngine {
2532
2580
  channel: currentChannelType || null,
2533
2581
  venueUid: undefined,
2534
2582
  // 群 @ 处理模式 / 客户端类型 / 权限模式
2535
- // 优先 agent/relation mentionMode 配置,fallback 到服务器 dispatch_mode 缓存(协议词汇需翻译)。
2583
+ // 优先 agent/relation mentionMode 配置,fallback 到服务器 mention_mode 缓存。
2536
2584
  mentionMode: effectiveAgentConfig?.mentionMode
2537
- ?? dispatchToMentionMode(session.metadata?.dispatchMode ?? message.dispatchMode)
2585
+ ?? session.metadata?.mentionMode
2586
+ ?? message.mentionMode
2538
2587
  ?? undefined,
2539
2588
  clientType: message.clientType || undefined,
2540
2589
  permissionMode: effectivePermissionMode,
@@ -2583,17 +2632,13 @@ export class ResponseEngine {
2583
2632
  const sessionManifests = this.agentRegistry?.resolveByChannel(channelKey)?.config?.sessionManifests;
2584
2633
  const manifestFile = sessionManifests?.[sessionType] ?? 'eck_manifest.json';
2585
2634
  const kitContext = renderKitSections(kitCtx, manifestFile);
2586
- if (kitContext)
2587
- contextParts.push(kitContext);
2588
- effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
2635
+ effectiveSystemPrompt = [options?.systemPromptAppend, kitContext].filter(Boolean).join('\n') || undefined;
2589
2636
  // ── Stats: context_breakdown 旁路采集(各段估算 token 数,字符数/4 近似) ──
2590
2637
  try {
2591
2638
  const estTokens = (s) => s ? Math.ceil(s.length / 4) : 0;
2592
2639
  const cbModel = effectiveModel || agentModel || 'unknown';
2593
2640
  const cbMaxTokens = 200000; // 保守默认,后续可从 model-catalog 取
2594
2641
  const systemPromptTokens = estTokens(options?.systemPromptAppend);
2595
- const personaTokens = estTokens(persona);
2596
- const workingTokens = estTokens(working);
2597
2642
  const kitTokens = estTokens(kitContext);
2598
2643
  const totalEst = estTokens(effectiveSystemPrompt);
2599
2644
  insertContextBreakdown(resolveRoot(), {
@@ -2603,7 +2648,7 @@ export class ResponseEngine {
2603
2648
  turn_count: 0, // 按 ts 排序得轮次
2604
2649
  model: cbModel,
2605
2650
  max_tokens: cbMaxTokens,
2606
- system_prompt: systemPromptTokens + personaTokens + workingTokens,
2651
+ system_prompt: systemPromptTokens,
2607
2652
  system_tools: 0, // 工具 schema 不在此层,留 0(后续 runner 层补)
2608
2653
  mcp_tools: 0,
2609
2654
  custom_agents: 0,
@@ -2629,6 +2674,7 @@ export class ResponseEngine {
2629
2674
  encrypted: message.encrypted,
2630
2675
  content: message.content, timestamp: message.timestamp,
2631
2676
  images: message.images,
2677
+ mentions: message.mentions,
2632
2678
  mentionAids: message.mentionAids,
2633
2679
  }];
2634
2680
  const peerItems = (() => {
@@ -2758,6 +2804,7 @@ export class ResponseEngine {
2758
2804
  ? (agent.resolveModelId(model) ?? model)
2759
2805
  : model));
2760
2806
  const retryAttemptTimeoutMs = this.retryAttemptTimeoutMs();
2807
+ const retryInputIsAtMostOnce = agent.retryInputSemantics === 'at_most_once';
2761
2808
  let runAttempt = 1;
2762
2809
  const recordRetryHealthError = async (retryError) => {
2763
2810
  if (!policy.accumulateErrors(chatType, identityRole))
@@ -2794,14 +2841,17 @@ export class ResponseEngine {
2794
2841
  const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
2795
2842
  resetTimer(eventType, toolName);
2796
2843
  attemptTimeout?.reset();
2797
- }, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease);
2844
+ }, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode);
2798
2845
  streamResult = attemptTimeout
2799
2846
  ? await Promise.race([processAttempt, attemptTimeout.promise])
2800
2847
  : await processAttempt;
2801
2848
  if (!streamResult.isError) {
2802
2849
  const protocolDecision = this.turnCoordinator.evaluateCommit(turnLease, this.buildTurnCommitEvidence(session, streamResult));
2803
2850
  const protocolReason = protocolDecision.ok ? undefined : protocolDecision.reason;
2804
- const safeProtocolReplay = isSafeProtocolReplayCandidate(streamResult, protocolReason);
2851
+ // Codex app-server turn/start appends its input before runQuery
2852
+ // resolves. Replaying here would create a second user turn.
2853
+ const safeProtocolReplay = !retryInputIsAtMostOnce
2854
+ && isSafeProtocolReplayCandidate(streamResult, protocolReason);
2805
2855
  const protocolRetry = retryScheduler.afterProtocolFailure(safeProtocolReplay ? protocolReason : undefined, safeProtocolReplay);
2806
2856
  if (protocolRetry.kind === 'protocol_replay') {
2807
2857
  protocolReplayAttempts = protocolRetry.attempt;
@@ -2846,6 +2896,16 @@ export class ResponseEngine {
2846
2896
  permissionError._errorAlreadySent = true;
2847
2897
  throw permissionError;
2848
2898
  }
2899
+ const retryWouldReplaySubmittedInput = retryAttemptTimedOut
2900
+ || isRetryableError(retryError)
2901
+ || classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE;
2902
+ if (retryInputIsAtMostOnce && streamRegistered && retryWouldReplaySubmittedInput) {
2903
+ await recordRetryHealthError(retryError);
2904
+ markRetryInputAlreadySubmitted(retryError);
2905
+ logger.warn(`[ResponseEngine] Not replaying already-submitted input: agent=${agent.name} `
2906
+ + `session=${session.id} task=${taskId} attempt=${runAttempt}`);
2907
+ throw retryError;
2908
+ }
2849
2909
  if (classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE && !hasExplicitTriggerModel) {
2850
2910
  if (probingPrimaryModel && previousFallbackState) {
2851
2911
  const resumedFallback = previousFallbackState.activeFallback;
@@ -2939,6 +2999,7 @@ export class ResponseEngine {
2939
2999
  resetTimer,
2940
3000
  shouldSuppress,
2941
3001
  proactive,
3002
+ permissionMode: effectivePermissionMode,
2942
3003
  turnLease,
2943
3004
  });
2944
3005
  }
@@ -2980,6 +3041,7 @@ export class ResponseEngine {
2980
3041
  resetTimer,
2981
3042
  shouldSuppress,
2982
3043
  proactive,
3044
+ permissionMode: effectivePermissionMode,
2983
3045
  turnLease,
2984
3046
  });
2985
3047
  // 重试后仍然 prompt_too_long:显示友好提示
@@ -3051,7 +3113,7 @@ export class ResponseEngine {
3051
3113
  return;
3052
3114
  const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
3053
3115
  if (!isCurrentlyBackground) {
3054
- await adapter.send({ ...envelope, replyContext: capturedReplyContext }, payload);
3116
+ await adapter.send(withEnvelopeReplyContext(envelope, capturedReplyContext), payload);
3055
3117
  if (payload.kind === 'result.text') {
3056
3118
  logger.info(`[ResponseEngine] afterProcess sent: task=${taskId} text="${payload.text.slice(0, 60)}"`);
3057
3119
  }
@@ -3139,7 +3201,10 @@ export class ResponseEngine {
3139
3201
  // 发送文件
3140
3202
  logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
3141
3203
  try {
3142
- await targetInfo.adapter.send(buildEnvelope({ taskId, channel: targetInfo.adapter.channelName, channelId: targetChannelId, agentName: agentNameForStats, replyContext: capturedReplyContext }), { kind: 'result.file', filePath: resolvedPath });
3204
+ const targetReplyContext = isCrossChannel
3205
+ ? { delivery: { chatType: 'private' } }
3206
+ : capturedReplyContext;
3207
+ await targetInfo.adapter.send(buildEnvelope({ taskId, channel: targetInfo.adapter.channelName, channelId: targetChannelId, agentName: agentNameForStats, replyContext: targetReplyContext }), { kind: 'result.file', filePath: resolvedPath });
3143
3208
  this.eventBus.publish({ type: 'runner:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
3144
3209
  sent++;
3145
3210
  if (isCrossChannel) {
@@ -3662,6 +3727,7 @@ export class ResponseEngine {
3662
3727
  // processEventStream 已通过 renderer 发过错误时也跳过
3663
3728
  const retryExhaustedCount = getRetryExhaustedCount(error);
3664
3729
  const modelFallbackExhaustedMessage = getModelFallbackExhaustedMessage(error);
3730
+ const retryInputAlreadySubmitted = wasRetryInputAlreadySubmitted(error);
3665
3731
  if (isUserInterrupt) {
3666
3732
  logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
3667
3733
  }
@@ -3673,13 +3739,15 @@ export class ResponseEngine {
3673
3739
  const idleSec = getLastIdleSec?.() || 0;
3674
3740
  const userMessage = retryExhaustedCount
3675
3741
  ? formatRetryableErrorFinalMessage(error, retryExhaustedCount)
3676
- : modelFallbackExhaustedMessage
3677
- ? modelFallbackExhaustedMessage
3678
- : isTotalExecutionTimeout
3679
- ? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
3680
- : isTimeout
3681
- ? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
3682
- : getErrorMessage(error, undefined);
3742
+ : retryInputAlreadySubmitted
3743
+ ? formatRetryInputAlreadySubmittedMessage(error)
3744
+ : modelFallbackExhaustedMessage
3745
+ ? modelFallbackExhaustedMessage
3746
+ : isTotalExecutionTimeout
3747
+ ? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
3748
+ : isTimeout
3749
+ ? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
3750
+ : getErrorMessage(error, undefined);
3683
3751
  // 获取 session 用于话题回复(如果 resolveSession 已执行)
3684
3752
  let sendOpts;
3685
3753
  try {
@@ -3696,14 +3764,14 @@ export class ResponseEngine {
3696
3764
  const errorPayload = {
3697
3765
  kind: 'result.error',
3698
3766
  text: userMessage,
3699
- reason: retryExhaustedCount ? 'retry_exhausted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
3767
+ reason: retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
3700
3768
  };
3701
3769
  if (!isUserInterrupt && daemonTrigger && procStatus === 'error') {
3702
3770
  // DaemonChannel settles a trigger on the first terminal payload. Put
3703
3771
  // the complete failure metadata first so the audit retains tool
3704
3772
  // counts and context-recovery diagnostics even for thrown errors
3705
3773
  // (for example when compact itself fails).
3706
- await adapter.send({ ...envelope, replyContext: sendOpts }, {
3774
+ await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), {
3707
3775
  kind: 'status.error',
3708
3776
  metadata: {
3709
3777
  errorType: errType,
@@ -3716,17 +3784,17 @@ export class ResponseEngine {
3716
3784
  contextRecoveryAttempts: streamResult.contextRecoveryAttempts ?? 0,
3717
3785
  },
3718
3786
  }).catch(() => { });
3719
- await adapter.send({ ...envelope, replyContext: sendOpts }, errorPayload);
3787
+ await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), errorPayload);
3720
3788
  }
3721
3789
  else {
3722
- await adapter.send({ ...envelope, replyContext: sendOpts }, errorPayload);
3790
+ await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), errorPayload);
3723
3791
  }
3724
3792
  if (!isUserInterrupt && daemonTrigger && procStatus !== 'error') {
3725
- await adapter.send({ ...envelope, replyContext: sendOpts }, statusPayload).catch(() => { });
3793
+ await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), statusPayload).catch(() => { });
3726
3794
  }
3727
3795
  // Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
3728
3796
  if (isProactive && adapter.capabilities?.thought) {
3729
- await adapter.send({ ...envelope, replyContext: sendOpts }, {
3797
+ await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), {
3730
3798
  kind: 'activity.batch',
3731
3799
  items: [{
3732
3800
  kind: 'notice',
@@ -3916,12 +3984,10 @@ export class ResponseEngine {
3916
3984
  await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
3917
3985
  }
3918
3986
  }
3919
- // 群聊分发模式同步:aun.ts 从服务器信封解析的 dispatchMode 注入到 message,
3920
- // 此处写入 session.metadata,确保 ECK 上下文的 venue fragment 正确渲染 dispatch 变量。
3921
- // 仅当 message.dispatchMode 有值且与 session 记录不一致时更新。
3922
- if (message.chatType === 'group' && message.dispatchMode && session.metadata?.dispatchMode !== message.dispatchMode) {
3923
- logger.info(`[ResponseEngine] dispatchMode sync: sessionId=${session.id} ${session.metadata?.dispatchMode ?? 'none'} -> ${message.dispatchMode}`);
3924
- session.metadata = { ...(session.metadata || {}), dispatchMode: message.dispatchMode };
3987
+ // 同步服务端 mention_mode,供同一群会话的上下文和菜单展示使用。
3988
+ if (message.chatType === 'group' && message.mentionMode && session.metadata?.mentionMode !== message.mentionMode) {
3989
+ logger.info(`[ResponseEngine] mentionMode sync: sessionId=${session.id} ${session.metadata?.mentionMode ?? 'none'} -> ${message.mentionMode}`);
3990
+ session.metadata = { ...(session.metadata || {}), mentionMode: message.mentionMode };
3925
3991
  await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
3926
3992
  }
3927
3993
  // chatMode 策略由 agent/relation behavior 配置在处理阶段解析;此处不再写 session 级参数。
@@ -3956,7 +4022,7 @@ export class ResponseEngine {
3956
4022
  */
3957
4023
  async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive,
3958
4024
  /** [迁移点4/5] 响应模式插件 + 状态,用于调 onToolUse/onComplete 钩子 */
3959
- modeHooks, causation, turnLease) {
4025
+ modeHooks, causation, turnLease, permissionMode) {
3960
4026
  // Per-session agent name for stats bucketing
3961
4027
  const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
3962
4028
  const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
@@ -4042,6 +4108,11 @@ export class ResponseEngine {
4042
4108
  }
4043
4109
  }
4044
4110
  }
4111
+ else if (event.type === 'notice') {
4112
+ for (const callId of event.closedToolCallIds ?? []) {
4113
+ openToolUseIds.delete(callId);
4114
+ }
4115
+ }
4045
4116
  else if (event.type === 'complete') {
4046
4117
  assistantUuid = event.assistantUuid ?? assistantUuid;
4047
4118
  protocolIncompleteReason = event.protocolIncompleteReason ?? protocolIncompleteReason;
@@ -4278,6 +4349,9 @@ export class ResponseEngine {
4278
4349
  sessionId: session.id,
4279
4350
  toolName: event.name,
4280
4351
  input: event.input,
4352
+ ...(event.callId ? { callId: event.callId } : {}),
4353
+ ...(session.selfAID ? { agentAid: session.selfAID } : {}),
4354
+ ...(permissionMode ? { permissionMode } : {}),
4281
4355
  timestamp: Date.now(),
4282
4356
  causation,
4283
4357
  });
@@ -4293,6 +4367,7 @@ export class ResponseEngine {
4293
4367
  }
4294
4368
  // [迁移点4] 工具汇报提醒:由 ProactiveMode.onToolUse 实现
4295
4369
  if (modeHooks?.mode?.onToolUse) {
4370
+ const proactiveState = modeHooks.state.get('proactive');
4296
4371
  modeHooks.mode.onToolUse({
4297
4372
  session,
4298
4373
  state: modeHooks.state,
@@ -4300,7 +4375,7 @@ export class ResponseEngine {
4300
4375
  toolInput: event.input || {},
4301
4376
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
4302
4377
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
4303
- isSendCommand: (toolName, toolInput) => isEvolcoreSendCommandForSession(toolName, toolInput, session.channelId),
4378
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', session.selfAID),
4304
4379
  logger,
4305
4380
  });
4306
4381
  }
@@ -4326,6 +4401,9 @@ export class ResponseEngine {
4326
4401
  toolName: event.name,
4327
4402
  isError: event.isError,
4328
4403
  agentName: agentNameForStats,
4404
+ ...(event.callId ? { callId: event.callId } : {}),
4405
+ ...(session.selfAID ? { agentAid: session.selfAID } : {}),
4406
+ ...(permissionMode ? { permissionMode } : {}),
4329
4407
  timestamp: Date.now(),
4330
4408
  causation,
4331
4409
  });
@@ -4342,6 +4420,12 @@ export class ResponseEngine {
4342
4420
  renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc);
4343
4421
  }
4344
4422
  }
4423
+ // 非终态运行时告警走 status notify,不进入 activity/thought 历史。
4424
+ if (event.type === 'notice' && event.severity === 'warn') {
4425
+ await renderer.sendWarningStatus(event.message, event.subtype, {
4426
+ ...(event.missingItems !== undefined && { missingItems: event.missingItems }),
4427
+ });
4428
+ }
4345
4429
  // 运行时错误(Codex: turn.failed / item error)
4346
4430
  if (event.type === 'error') {
4347
4431
  logger.warn(`[ResponseEngine] error event: ${event.errorType}: ${event.error}`);