evolcore 0.0.20 → 0.0.22

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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. package/dist/config/aun-gateway-config.js +0 -2
@@ -37,7 +37,7 @@ import { resolveAgentLifecycle } from '../../config/lifecycle.js';
37
37
  import { authorizationConfigRevision, checkRoleAccess, getFirstStaticAgentOwner, listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, roleToSessionIdentity, } from '../../config/peer-role-resolver.js';
38
38
  import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../../stats/writer.js';
39
39
  import { normalizeUsage } from '../../stats/normalizer.js';
40
- import { resolvePrices } from '../../stats/price-resolver.js';
40
+ import { resolvePrices, roundCostForOutput } from '../../stats/price-resolver.js';
41
41
  import { getBudgetStatus } from '../../stats/budget.js';
42
42
  import { formatUsageSubjectKey, getRoleBudgetStatus } from '../../stats/role-budget.js';
43
43
  import { snapshot } from './response-snapshot.js';
@@ -47,8 +47,18 @@ import { registerBuiltinModes } from '../../response-system/modes/index.js';
47
47
  import { deriveSessionTitle, shouldAutoFillSessionTitle } from '../session/session-title.js';
48
48
  import { SessionTurnCoordinator } from '../session/session-turn-coordinator.js';
49
49
  import { recordTriggerExecutionAnomaly } from '../../trigger/anomaly-store.js';
50
- import { classifyToolErrorCode } from '../permission/tool-error-code.js';
50
+ import { classifyToolErrorCode, normalizeToolErrorCode } from '../permission/tool-error-code.js';
51
+ import { normalizeExecutionPermissionMode } from '../permission/mode.js';
51
52
  import { buildToolLifecycleEventKey } from '../audit/event-key.js';
53
+ import { isFullAccessEnabled, loadDaemonConfig } from '../../config-store.js';
54
+ import { auditFullAccessEvent } from '../auth/authorization-audit.js';
55
+ const INBOUND_MESSAGE_LOG_TEXT_MAX_LENGTH = 240;
56
+ export function formatInboundMessageLogText(text) {
57
+ const singleLine = text.replace(/\s+/gu, ' ').trim();
58
+ if (singleLine.length <= INBOUND_MESSAGE_LOG_TEXT_MAX_LENGTH)
59
+ return singleLine;
60
+ return singleLine.slice(0, INBOUND_MESSAGE_LOG_TEXT_MAX_LENGTH - 3).trimEnd() + '...';
61
+ }
52
62
  export class PauseController {
53
63
  state = 'running';
54
64
  waiters = new Set();
@@ -505,6 +515,7 @@ export class ResponseEngine {
505
515
  /** sessionId → 尚未收到 runner:task-notification 的子任务。 */
506
516
  activeRunnerTasks = new Map();
507
517
  triggerTerminalMessages = new WeakSet();
518
+ fullAccessTerminalMessages = new WeakSet();
508
519
  agentDelegationRegistry;
509
520
  turnCoordinator;
510
521
  /** 响应模式协调器(插件化机制中枢)。内置模式在构造时注册。 */
@@ -626,20 +637,44 @@ export class ResponseEngine {
626
637
  // 监听中断事件,标记被中断的 session
627
638
  this.eventBus.subscribe('task:interrupted', (event) => {
628
639
  if ('sessionId' in event && event.sessionId) {
640
+ const eventIdentity = event;
641
+ const eventTaskId = eventIdentity.taskId;
642
+ const activeTask = this.activeTaskSpans.get(event.sessionId);
643
+ // A delayed interruption from an older task must never invalidate a
644
+ // newer task that has already claimed the same session.
645
+ if (eventTaskId && activeTask && activeTask.taskId !== eventTaskId)
646
+ return;
629
647
  const reason = (event.reason || 'new_message');
630
- this.turnCoordinator.invalidate(event.sessionId, reason);
648
+ const currentTurn = this.turnCoordinator.invalidate(event.sessionId, reason, { taskId: eventTaskId, generation: eventIdentity.generation });
649
+ if ((eventTaskId || eventIdentity.generation !== undefined) && !currentTurn)
650
+ return;
631
651
  this.interruptedSessions.set(event.sessionId, reason);
632
652
  this.cancelRetryDelays(event.sessionId);
633
- this.agentDelegationRegistry?.revokeSession(event.sessionId);
653
+ // The event can be delayed relative to a newer task on the same
654
+ // session. Revoke only when the producer supplied the matching task;
655
+ // the interrupt path below performs the authoritative task-scoped
656
+ // revocation for legacy events without a taskId.
657
+ const interruptedTaskId = eventTaskId;
658
+ if (interruptedTaskId && activeTask?.taskId === interruptedTaskId) {
659
+ this.agentDelegationRegistry?.revokeTask(event.sessionId, interruptedTaskId);
660
+ }
634
661
  }
635
662
  });
636
663
  this.eventBus.subscribe('task:completed', event => {
637
- if ('sessionId' in event && event.sessionId)
638
- this.activeTaskSpans.delete(event.sessionId);
664
+ if ('sessionId' in event && event.sessionId) {
665
+ const taskId = event.taskId;
666
+ const active = this.activeTaskSpans.get(event.sessionId);
667
+ if (!taskId || active?.taskId === taskId)
668
+ this.activeTaskSpans.delete(event.sessionId);
669
+ }
639
670
  });
640
671
  this.eventBus.subscribe('task:error', event => {
641
- if ('sessionId' in event && event.sessionId)
642
- this.activeTaskSpans.delete(event.sessionId);
672
+ if ('sessionId' in event && event.sessionId) {
673
+ const taskId = event.taskId;
674
+ const active = this.activeTaskSpans.get(event.sessionId);
675
+ if (!taskId || active?.taskId === taskId)
676
+ this.activeTaskSpans.delete(event.sessionId);
677
+ }
643
678
  });
644
679
  // 初始化响应模式协调器,注册内置模式(interactive/proactive)
645
680
  const registry = new ResponseModeRegistry();
@@ -988,7 +1023,8 @@ export class ResponseEngine {
988
1023
  const active = this.activeRenderers.get(sessionId);
989
1024
  if (!active || active.suppressActivities)
990
1025
  return;
991
- void this.emitOperationalNotice(active.renderer, '\u23f3 会话压缩中...', 'info', 'compact-start');
1026
+ void this.emitOperationalNotice(active.renderer, '\u23f3 会话压缩中...', 'info', 'compact-start')
1027
+ .catch(error => logger.warn(`[ResponseEngine] compact-start notice send failed: ${error instanceof Error ? error.message : String(error)}`));
992
1028
  }
993
1029
  async emitOperationalNotice(renderer, text, severity, subtype) {
994
1030
  if (await renderer.sendOperationalNoticeAsText(text))
@@ -1006,10 +1042,28 @@ export class ResponseEngine {
1006
1042
  return true;
1007
1043
  }
1008
1044
  async retryAfterContextRecovery(prompt, opts) {
1009
- const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, } = opts;
1045
+ const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, beforeRunQuery, } = opts;
1010
1046
  if (!session.agentSessionId || !canCompactAgent(agent)) {
1011
1047
  throw new Error('CONTEXT_COMPACT_FAILED');
1012
1048
  }
1049
+ // A topic may have discovered and activated its first backend earlier in
1050
+ // this logical turn. The Session event is authoritative at that point,
1051
+ // while the original override can still describe the pre-run UNBOUND
1052
+ // snapshot. Compact and retry must address the accepted backend under the
1053
+ // same complete TurnLease or a runner could create a second backend.
1054
+ const recoveryModelOverride = session.threadId
1055
+ ? {
1056
+ ...(modelOverride || {}),
1057
+ backend: { kind: 'topic', agentSessionId: session.agentSessionId },
1058
+ turn: {
1059
+ sessionId: turnLease.sessionId,
1060
+ taskId: turnLease.taskId,
1061
+ turnId: turnLease.turnId,
1062
+ generation: turnLease.generation,
1063
+ inputId: turnLease.inputId,
1064
+ },
1065
+ }
1066
+ : modelOverride;
1013
1067
  // text event 在进入 renderer 前已经剔除了上下文超限错误行。保留缓冲中的
1014
1068
  // 有效正文,由 sendOperationalNoticeAsText 先 flush,避免恢复过程丢失已有模型输出。
1015
1069
  await this.emitOperationalNotice(renderer, '上下文过长,正在压缩会话...', 'warn', 'compact-trigger');
@@ -1017,15 +1071,17 @@ export class ResponseEngine {
1017
1071
  this.announcedCompactStarts.add(session.id);
1018
1072
  let compacted;
1019
1073
  try {
1020
- compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
1074
+ compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath, recoveryModelOverride);
1021
1075
  }
1022
1076
  finally {
1023
1077
  this.announcedCompactStarts.delete(session.id);
1024
1078
  }
1025
- if (compacted) {
1079
+ const compactResult = compacted;
1080
+ if (compactResult === true || compactResult?.ok === true) {
1026
1081
  await this.emitOperationalNotice(renderer, '✅ 压缩完成,继续处理...', 'info', 'compact-retry');
1027
1082
  await renderer.flush();
1028
- const retryStream = await agent.runQuery(session.id, prompt, absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
1083
+ beforeRunQuery?.();
1084
+ const retryStream = await agent.runQuery(session.id, prompt, absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, recoveryModelOverride, runtimeEnv);
1029
1085
  if (await this.cancelStaleRunnerStart(agent, session.id, streamKey, turnLease, turnLease.taskId)) {
1030
1086
  throw new Error('RUNNER_START_SUPERSEDED');
1031
1087
  }
@@ -1033,9 +1089,9 @@ export class ResponseEngine {
1033
1089
  return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
1034
1090
  undefined, turnLease, opts.permissionMode, opts.proactiveSelfAid);
1035
1091
  }
1036
- // Dropping the whole session hides the root cause and can replay side
1037
- // effects in unattended trigger tasks. Surface an actionable failure
1038
- // instead; callers may explicitly start a fresh task if that is desired.
1092
+ if (compactResult && typeof compactResult === 'object' && compactResult.message) {
1093
+ logger.warn(`[ResponseEngine] Context compact failed: ${compactResult.message}`);
1094
+ }
1039
1095
  throw new Error('CONTEXT_COMPACT_FAILED');
1040
1096
  }
1041
1097
  async recoverFromContextLimit(initialResult, opts) {
@@ -1075,6 +1131,61 @@ export class ResponseEngine {
1075
1131
  && message.source === 'trigger'
1076
1132
  && !!message.triggerMeta?.triggerId;
1077
1133
  }
1134
+ /**
1135
+ * A fullaccess Trigger may run with no current relation access for its
1136
+ * origin actor, but only after the daemon gate and persisted owner
1137
+ * provenance still validate. This is intentionally stricter than merely
1138
+ * checking the message's requested permission mode.
1139
+ */
1140
+ isCurrentFullAccessTrigger(message) {
1141
+ const trigger = message.triggerMeta;
1142
+ if (message.source !== 'trigger'
1143
+ || trigger?.permissionModeOverride !== 'fullaccess'
1144
+ || typeof trigger.authorizedBy !== 'string' || !trigger.authorizedBy
1145
+ || typeof trigger.triggerId !== 'string' || !trigger.triggerId
1146
+ || typeof trigger.runId !== 'string' || !trigger.runId
1147
+ || typeof trigger.attemptId !== 'string' || !trigger.attemptId) {
1148
+ return false;
1149
+ }
1150
+ try {
1151
+ return isFullAccessEnabled()
1152
+ && new Set(loadDaemonConfig().owners ?? []).has(trigger.authorizedBy);
1153
+ }
1154
+ catch {
1155
+ return false;
1156
+ }
1157
+ }
1158
+ /**
1159
+ * The authorization envelope is immutable for one logical run, but the
1160
+ * daemon gate, approving owner, and runner profile are live policy. Check
1161
+ * them immediately before every Runner invocation, including retries.
1162
+ */
1163
+ assertCurrentFullAccessExecutionAuthorization(authorization, agent) {
1164
+ if (authorization.permissionMode !== 'fullaccess'
1165
+ || authorization.processRole !== 'fullaccess-run'
1166
+ || authorization.dataScope !== 'daemon'
1167
+ || typeof authorization.authorizedBy !== 'string'
1168
+ || !authorization.authorizedBy) {
1169
+ throw new Error('fullaccess execution authorization is missing or malformed');
1170
+ }
1171
+ if (authorization.source === 'trigger'
1172
+ && (!authorization.triggerId || !authorization.runId || !authorization.attemptId)) {
1173
+ throw new Error('fullaccess Trigger authorization lacks scheduler identity or approval provenance');
1174
+ }
1175
+ if (authorization.source !== 'fullaccess-command' && authorization.source !== 'trigger') {
1176
+ throw new Error('fullaccess execution authorization has an unsupported source');
1177
+ }
1178
+ if (!isFullAccessEnabled()) {
1179
+ throw new Error('fullaccess execution is disabled by daemon.json');
1180
+ }
1181
+ const currentOwners = new Set(loadDaemonConfig().owners ?? []);
1182
+ if (!currentOwners.has(authorization.authorizedBy)) {
1183
+ throw new Error('fullaccess execution authorization is no longer owned');
1184
+ }
1185
+ if (agent.capabilities?.fullaccess !== true) {
1186
+ throw new Error(`baseagent ${agent.name} does not provide a complete fullaccess execution profile`);
1187
+ }
1188
+ }
1078
1189
  resolveTriggerExecutionIdentity(message, selfAid) {
1079
1190
  if (message.source !== 'trigger' || !message.triggerMeta?.triggerId)
1080
1191
  return undefined;
@@ -1187,7 +1298,7 @@ export class ResponseEngine {
1187
1298
  causation: opts.causation ?? message.causation,
1188
1299
  });
1189
1300
  }
1190
- publishTriggerExecutionSkipped(message, reason, causation) {
1301
+ publishTriggerExecutionSkipped(message, reason, causation, interruption) {
1191
1302
  const terminal = this.claimTriggerTerminal(message);
1192
1303
  if (!terminal)
1193
1304
  return;
@@ -1200,6 +1311,7 @@ export class ResponseEngine {
1200
1311
  attemptId: trigger.attemptId,
1201
1312
  originTriggerId: trigger.triggerId,
1202
1313
  reason,
1314
+ ...(interruption ?? {}),
1203
1315
  targetChannel: message.channel,
1204
1316
  targetChannelId: message.channelId,
1205
1317
  fireTime: trigger.fireTime,
@@ -1207,16 +1319,25 @@ export class ResponseEngine {
1207
1319
  });
1208
1320
  }
1209
1321
  /** Close the internal daemon conversation before returning from an interrupted Trigger turn. */
1210
- async publishTriggerExecutionInterrupted(message, adapter, envelope, reason, causation) {
1322
+ async publishTriggerExecutionInterrupted(message, adapter, envelope, reason, causation, generation) {
1323
+ const interruption = reason === 'stale_generation'
1324
+ ? {
1325
+ reasonCode: 'stale_generation',
1326
+ decisionSource: 'infrastructure',
1327
+ executionState: 'interrupted',
1328
+ ...(generation?.expected !== undefined ? { generation: generation.expected } : {}),
1329
+ ...(generation?.current !== undefined ? { currentGeneration: generation.current } : {}),
1330
+ }
1331
+ : undefined;
1211
1332
  if (this.isTrustedDaemonTrigger(message)) {
1212
1333
  await adapter.send(envelope, {
1213
1334
  kind: 'status.interrupted',
1214
- metadata: { reason },
1335
+ metadata: { reason, ...(interruption ?? {}) },
1215
1336
  }).catch(error => {
1216
1337
  logger.warn(`[ResponseEngine] Failed to close interrupted Trigger run=${message.triggerMeta?.runId ?? '<unknown>'}: ${error instanceof Error ? error.message : String(error)}`);
1217
1338
  });
1218
1339
  }
1219
- this.publishTriggerExecutionSkipped(message, reason, causation);
1340
+ this.publishTriggerExecutionSkipped(message, reason, causation, interruption);
1220
1341
  }
1221
1342
  publishTriggerExecutionCompleted(message, messageId, durationMs, causation) {
1222
1343
  const terminal = this.claimTriggerTerminal(message);
@@ -1242,7 +1363,7 @@ export class ResponseEngine {
1242
1363
  static COMMAND_PREFIXES = [
1243
1364
  '/new', '/pwd', '/help', '/status', '/restart',
1244
1365
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
1245
- '/stop', '/pause', '/resume', '/clear', '/compact', '/del', '/perm', '/file', '/check',
1366
+ '/stop', '/pause', '/resume', '/clear', '/compact', '/renew', '/del', '/perm', '/file', '/check',
1246
1367
  '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode',
1247
1368
  '/aid', '/upgrade', '/evolagent',
1248
1369
  ];
@@ -1255,6 +1376,39 @@ export class ResponseEngine {
1255
1376
  * 处理消息(主入口)
1256
1377
  */
1257
1378
  async processMessage(message) {
1379
+ const fallbackFullAccessAudit = this.resolveFullAccessAuditCandidate(message);
1380
+ let outerFailureReason;
1381
+ try {
1382
+ await this.processMessageWithMonitors(message);
1383
+ }
1384
+ catch (error) {
1385
+ outerFailureReason = error instanceof Error ? error.message : String(error);
1386
+ throw error;
1387
+ }
1388
+ finally {
1389
+ if (fallbackFullAccessAudit && !this.fullAccessTerminalMessages.has(message)) {
1390
+ auditFullAccessEvent({
1391
+ event: fallbackFullAccessAudit.authorization.source === 'trigger'
1392
+ ? 'fullaccess.trigger.execution.ended'
1393
+ : 'fullaccess.execution.ended',
1394
+ source: fallbackFullAccessAudit.authorization.source,
1395
+ actorId: fallbackFullAccessAudit.authorization.authorizedBy,
1396
+ processRole: fallbackFullAccessAudit.processRole,
1397
+ agentAid: message.selfAID,
1398
+ messageId: message.messageId,
1399
+ triggerId: fallbackFullAccessAudit.authorization.triggerId,
1400
+ runId: fallbackFullAccessAudit.authorization.runId,
1401
+ attemptId: fallbackFullAccessAudit.authorization.attemptId,
1402
+ result: 'blocked',
1403
+ reason: outerFailureReason ?? 'fullaccess execution stopped before permission setup',
1404
+ executed: false,
1405
+ executionState: 'blocked',
1406
+ });
1407
+ this.fullAccessTerminalMessages.add(message);
1408
+ }
1409
+ }
1410
+ }
1411
+ async processMessageWithMonitors(message) {
1258
1412
  const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
1259
1413
  const totalExecutionMs = this.totalExecutionLimitMs();
1260
1414
  if (message.handoffDelivery
@@ -1318,7 +1472,15 @@ export class ResponseEngine {
1318
1472
  // ── 角色访问控制检查:读取该用户角色的 allowAccess 配置,false 则拦截并回复权限不足 ──
1319
1473
  const userRole = session.identity?.role || 'none';
1320
1474
  const isInternalHandoff = message.source === 'handoff';
1321
- if (!isInternalHandoff && !checkRoleAccess(userRole, selfAidForAccess)) {
1475
+ const fullAccessAuditCandidate = this.resolveFullAccessAuditCandidate(message);
1476
+ const isCurrentFullAccessCommand = fullAccessAuditCandidate?.authorization.source === 'fullaccess-command'
1477
+ && fullAccessAuditCandidate.processRole === 'fullaccess-run';
1478
+ const isCurrentFullAccessTrigger = fullAccessAuditCandidate?.authorization.source === 'trigger'
1479
+ && fullAccessAuditCandidate.processRole === 'fullaccess-run';
1480
+ if (!isInternalHandoff
1481
+ && !isCurrentFullAccessCommand
1482
+ && !isCurrentFullAccessTrigger
1483
+ && !checkRoleAccess(userRole, selfAidForAccess)) {
1322
1484
  logger.warn(`[ResponseEngine] Access denied: role=${userRole} peerKey=${message.channelId} session=${session.id}`);
1323
1485
  const channelKey = session.metadata?.channelKey || message.channel;
1324
1486
  const channelInfo = this.resolveChannelInfo(channelKey);
@@ -1539,6 +1701,55 @@ export class ResponseEngine {
1539
1701
  this.activeMonitors.delete(streamKey);
1540
1702
  }
1541
1703
  }
1704
+ resolveFullAccessAuditCandidate(message) {
1705
+ const commandAuthorization = message.executionPermissionOverride;
1706
+ if (commandAuthorization?.permissionMode === 'fullaccess'
1707
+ && commandAuthorization.processRole === 'fullaccess-run'
1708
+ && commandAuthorization.dataScope === 'daemon'
1709
+ && commandAuthorization.source === 'fullaccess-command'
1710
+ && typeof commandAuthorization.authorizedBy === 'string'
1711
+ && commandAuthorization.authorizedBy.length > 0) {
1712
+ let currentOwner = false;
1713
+ try {
1714
+ currentOwner = isFullAccessEnabled()
1715
+ && new Set(loadDaemonConfig().owners ?? []).has(commandAuthorization.authorizedBy);
1716
+ }
1717
+ catch {
1718
+ // Runtime validation will fail closed; the audit must still retain the
1719
+ // attempted authorization without falsely labeling it active.
1720
+ }
1721
+ return {
1722
+ authorization: commandAuthorization,
1723
+ processRole: currentOwner ? 'fullaccess-run' : 'none',
1724
+ };
1725
+ }
1726
+ const trigger = message.triggerMeta;
1727
+ if (message.source === 'trigger'
1728
+ && trigger?.permissionModeOverride === 'fullaccess'
1729
+ && typeof trigger.authorizedBy === 'string' && trigger.authorizedBy.length > 0
1730
+ && typeof trigger.triggerId === 'string' && trigger.triggerId.length > 0
1731
+ && typeof trigger.runId === 'string' && trigger.runId.length > 0
1732
+ && typeof trigger.attemptId === 'string' && trigger.attemptId.length > 0) {
1733
+ const current = this.isCurrentFullAccessTrigger(message);
1734
+ return {
1735
+ authorization: {
1736
+ permissionMode: 'fullaccess',
1737
+ processRole: 'fullaccess-run',
1738
+ dataScope: 'daemon',
1739
+ source: 'trigger',
1740
+ authorizedBy: trigger.authorizedBy,
1741
+ triggerId: trigger.triggerId,
1742
+ runId: trigger.runId,
1743
+ attemptId: trigger.attemptId,
1744
+ },
1745
+ // Keep attempted authorization details for audit correlation, but do
1746
+ // not claim an active fullaccess identity when the process gate or
1747
+ // approval provenance has since become invalid.
1748
+ processRole: current ? 'fullaccess-run' : 'none',
1749
+ };
1750
+ }
1751
+ return undefined;
1752
+ }
1542
1753
  /** 获取回复上下文(跟着任务走) */
1543
1754
  getReplyContext(message) {
1544
1755
  return message.replyContext;
@@ -1551,10 +1762,10 @@ export class ResponseEngine {
1551
1762
  ? configuredSeconds * 1000
1552
1763
  : undefined;
1553
1764
  }
1554
- retryAttemptTimeoutMs() {
1765
+ apiRetryTimeoutMs() {
1555
1766
  if (this.globalSettings.idleMonitor?.enabled === false)
1556
1767
  return undefined;
1557
- const configuredSeconds = this.globalSettings.idleMonitor?.retryAttemptTimeout
1768
+ const configuredSeconds = this.globalSettings.idleMonitor?.apiRetryTimeout
1558
1769
  ?? this.globalSettings.idleMonitor?.timeout
1559
1770
  ?? 120;
1560
1771
  return typeof configuredSeconds === 'number'
@@ -1633,7 +1844,9 @@ export class ResponseEngine {
1633
1844
  // 二次拦截:如果命令消息绕过 MessageBridge 的 handleCommand 泄漏到这里,
1634
1845
  // 静默丢弃而不是发送给 Agent(命令已在 MessageBridge 层处理过)
1635
1846
  const rawContent = message.content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
1636
- if (rawContent.startsWith('/') && this.isKnownCommand(rawContent)) {
1847
+ if (!message.executionPermissionOverride
1848
+ && rawContent.startsWith('/')
1849
+ && this.isKnownCommand(rawContent)) {
1637
1850
  logger.warn(`[ResponseEngine] Command leaked past MessageBridge, dropped: "${rawContent.substring(0, 40)}"`);
1638
1851
  this.publishTriggerExecutionFailure(message, 'trigger_command_not_supported');
1639
1852
  return;
@@ -1870,7 +2083,7 @@ export class ResponseEngine {
1870
2083
  },
1871
2084
  modeConfig: resolvedMode.context.modeConfig,
1872
2085
  state: modeState,
1873
- isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, session.selfAID || message.selfAID),
2086
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid),
1874
2087
  logger,
1875
2088
  } : null;
1876
2089
  if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
@@ -1951,6 +2164,8 @@ export class ResponseEngine {
1951
2164
  this.eventBus.publish({
1952
2165
  type: 'task:error',
1953
2166
  sessionId: session.id,
2167
+ taskId,
2168
+ generation: turnLease?.generation,
1954
2169
  error: timeoutError.message,
1955
2170
  errorType,
1956
2171
  agentName: agentNameForStats,
@@ -1975,6 +2190,17 @@ export class ResponseEngine {
1975
2190
  let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
1976
2191
  let startTime = runnerStartedAt;
1977
2192
  let protocolReplayAttempts = 0;
2193
+ // These values span the outer task try/catch/finally so fullaccess audit
2194
+ // completion and context cleanup also cover setup failures.
2195
+ let fullAccessAuthorization;
2196
+ let fullAccessAuditStartedAt;
2197
+ let fullAccessAuditBaseagent;
2198
+ let fullAccessAuditModel;
2199
+ let fullAccessAuditProcessRole = 'none';
2200
+ let fullAccessRunnerInvoked = false;
2201
+ let fullAccessExecutionFailed = false;
2202
+ let fullAccessExecutionFailureReason;
2203
+ let activePermissionContext;
1978
2204
  try {
1979
2205
  const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
1980
2206
  // 记录收到消息
@@ -2062,9 +2288,13 @@ export class ResponseEngine {
2062
2288
  const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
2063
2289
  const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
2064
2290
  const e2eeInfo = message.replyContext?.metadata?.encrypted != null ? ` encrypt=${message.replyContext.metadata.encrypted}` : '';
2065
- logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}${e2eeInfo}`);
2291
+ const contentPreview = formatInboundMessageLogText(message.content);
2292
+ logger.info(`[${message.channel}] ${message.channelId}: ${contentPreview}${imageInfo}${modeInfo}${e2eeInfo}`);
2066
2293
  // 构建 peer 标识(优先 peerName,退化到 peerId / channelId)
2067
- const peerName = session.metadata?.peerName ?? message.peerName;
2294
+ // Group sessions are shared by multiple senders. The current inbound
2295
+ // message is authoritative; session metadata is only a fallback for
2296
+ // legacy paths that do not carry a per-message display name.
2297
+ const peerName = message.peerName ?? session.metadata?.peerName;
2068
2298
  const peerId = session.metadata?.peerId ?? message.peerId ?? message.channelId;
2069
2299
  const peerShort = peerId ? peerId.split('.')[0].split(':')[0] : '?';
2070
2300
  const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
@@ -2075,7 +2305,16 @@ export class ResponseEngine {
2075
2305
  turnLease = await this.turnCoordinator.begin(session, taskId);
2076
2306
  // 记录开始处理
2077
2307
  const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
2078
- this.eventBus.publish({ type: 'task:started', sessionId: session.id, agentName: agentNameForStats, encrypt: taskEncrypt, chatmode, causation: taskCausation });
2308
+ this.eventBus.publish({
2309
+ type: 'task:started',
2310
+ sessionId: session.id,
2311
+ taskId,
2312
+ generation: turnLease.generation,
2313
+ agentName: agentNameForStats,
2314
+ encrypt: taskEncrypt,
2315
+ chatmode,
2316
+ causation: taskCausation,
2317
+ });
2079
2318
  this.touchAgentActivity(channelKey);
2080
2319
  // Upgrade the channel acknowledgement at task start, before compaction and
2081
2320
  // runner setup, so processing feedback remains visible for the whole run.
@@ -2101,12 +2340,12 @@ export class ResponseEngine {
2101
2340
  adapter,
2102
2341
  envelope,
2103
2342
  agentAid: session.selfAID,
2343
+ permissionMode: () => effectivePermissionMode,
2104
2344
  flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
2105
2345
  suppressActivityItems: isProactive ? false : middleOutputMode !== 'all',
2106
2346
  suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
2107
2347
  operationalNoticesAsText: !isProactive && middleOutputMode !== 'none',
2108
2348
  fileMarkerPattern: options?.fileMarkerPattern,
2109
- diagEnabled: this.globalSettings.debug?.flusherDiag,
2110
2349
  send: async (payload) => {
2111
2350
  if (turnLease && !this.turnCoordinator.canPublish(turnLease)) {
2112
2351
  snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'suppressed-stale-turn' });
@@ -2145,7 +2384,7 @@ export class ResponseEngine {
2145
2384
  this.touchAgentActivity(channelKey);
2146
2385
  const enrichedEnvelope = withEnvelopeReplyContext(envelope, opts);
2147
2386
  snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'sent' });
2148
- await adapter.send(enrichedEnvelope, payload);
2387
+ return await adapter.send(enrichedEnvelope, payload);
2149
2388
  },
2150
2389
  });
2151
2390
  this.activeRenderers.set(session.id, {
@@ -2154,11 +2393,6 @@ export class ResponseEngine {
2154
2393
  suppressActivities: shouldSuppress(),
2155
2394
  });
2156
2395
  renderer.addLifecycle('started');
2157
- // 预压缩与任务过程中的自动压缩共用同一投影规则:interactive 的 text/all
2158
- // 均以普通非最终文本显示运行提示;none 不显示。其它 activity 在 all 下仍保持结构化。
2159
- // 它仍发生在真正的 prompt 执行之前。
2160
- await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer);
2161
- startTime = Date.now();
2162
2396
  if (isProactive) {
2163
2397
  logger.info(`[ResponseEngine] proactive mode: outputs via thought.put task=${taskId}`);
2164
2398
  }
@@ -2275,12 +2509,21 @@ export class ResponseEngine {
2275
2509
  };
2276
2510
  })();
2277
2511
  let effectivePermissionMode = 'readonly';
2512
+ // Allocate/register the session-owned root before any runner preflight
2513
+ // context is installed. Codex may already own a stronger directory;
2514
+ // other runners receive a private child of the process TMPDIR.
2515
+ const sessionRuntimeDir = agent.getManagedTempDir?.(session.id) ?? ensureSessionRuntimeDir(session.id);
2278
2516
  const recordExecutionAnomaly = triggerRunId
2279
2517
  ? (anomaly) => {
2280
2518
  recordTriggerExecutionAnomaly(triggerRunId, {
2281
2519
  ...anomaly,
2282
2520
  correlationId: anomaly.correlationId ?? anomaly.requestId,
2283
2521
  agentAid: anomaly.agentAid ?? session.selfAID ?? message.selfAID,
2522
+ agentName: anomaly.agentName
2523
+ ?? (agentNameForStats !== '<unknown>' ? agentNameForStats : undefined)
2524
+ ?? anomaly.agentAid
2525
+ ?? session.selfAID
2526
+ ?? message.selfAID,
2284
2527
  sessionId: anomaly.sessionId ?? session.id,
2285
2528
  permissionMode: anomaly.permissionMode ?? effectivePermissionMode,
2286
2529
  });
@@ -2291,7 +2534,7 @@ export class ResponseEngine {
2291
2534
  : undefined;
2292
2535
  const pureSessionPolicyHook = runModeConfig?.policyHook;
2293
2536
  // 设置权限审批的交互上下文(支持交互卡片)
2294
- agent.setPermissionContext?.(session.id, {
2537
+ const permissionContext = {
2295
2538
  sendPrompt: permissionPromptForOrigin,
2296
2539
  adapter: permissionAdapter,
2297
2540
  channelId: permissionChannelId,
@@ -2305,12 +2548,14 @@ export class ResponseEngine {
2305
2548
  chatmode: isProactive ? 'proactive' : 'interactive',
2306
2549
  role: peerRole,
2307
2550
  chatType: authChatType,
2308
- selfAid: session.selfAID || message.selfAID,
2551
+ selfAid,
2552
+ managedTempDir: sessionRuntimeDir,
2309
2553
  allowReadonlySourceDiagnostics: effectiveAgentConfig?.readonlySourceDiagnostics === true,
2310
2554
  peerKey: authPeerKey,
2311
2555
  causation: taskCausation,
2312
2556
  approvalRouting,
2313
2557
  approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
2558
+ permissionMode: effectivePermissionMode,
2314
2559
  recordExecutionAnomaly,
2315
2560
  pauseController: taskPauseController,
2316
2561
  preToolUsePolicyHook: pureSessionPolicyHook,
@@ -2323,6 +2568,7 @@ export class ResponseEngine {
2323
2568
  })
2324
2569
  : undefined,
2325
2570
  turn: {
2571
+ sessionId: turnLease.sessionId,
2326
2572
  taskId,
2327
2573
  turnId: turnLease.turnId,
2328
2574
  generation: turnLease.generation,
@@ -2424,7 +2670,9 @@ export class ResponseEngine {
2424
2670
  return undefined;
2425
2671
  };
2426
2672
  })(),
2427
- });
2673
+ };
2674
+ activePermissionContext = permissionContext;
2675
+ agent.setPermissionContext?.(session.id, permissionContext);
2428
2676
  // per-session 权限模式在 try 内、peerKey 解析后设置(见 resolvePermissionMode 调用)
2429
2677
  // 标记会话为处理中(实时持久化,重启后可恢复)
2430
2678
  this.sessionManager.markProcessing(session.id, taskId);
@@ -2474,21 +2722,68 @@ export class ResponseEngine {
2474
2722
  // 这样单条和积压合并批次不会因队列状态不同而切换关系级配置。
2475
2723
  const normalizedBaseagent = normalizeBaseagent(agent.name);
2476
2724
  // 设置 per-call 权限模式:只按当前角色定义解析(不读物理配置层或 session.metadata)。
2477
- // Trigger 只能降低本次调用权限,不能突破当前角色的权限上限。
2725
+ // 普通 Trigger override 只能降低权限;可信 /fa 或 scheduler fullaccess
2726
+ // 使用独立授权路径,不进入角色 authority ceiling。
2478
2727
  // 作为 per-call 入参随 modelOverride 传入 runQuery —— 与 model/effort 同构,
2479
2728
  // 不写 AgentRunner 实例字段,多对端/多会话并发共享同一 runner 实例时互不污染。
2480
2729
  const triggerPermissionModeOverride = message.triggerMeta?.permissionModeOverride;
2481
- try {
2482
- effectivePermissionMode = constrainRuntimePermissionMode({
2483
- selfAid: selfAid || undefined,
2484
- role: peerRole,
2485
- requestedValue: triggerPermissionModeOverride,
2486
- }).effectiveValue;
2730
+ const commandFullAccess = message.executionPermissionOverride;
2731
+ const triggerFullAccess = triggerPermissionModeOverride === 'fullaccess';
2732
+ if (commandFullAccess || triggerFullAccess) {
2733
+ if (commandFullAccess) {
2734
+ if (commandFullAccess.permissionMode !== 'fullaccess'
2735
+ || commandFullAccess.processRole !== 'fullaccess-run'
2736
+ || commandFullAccess.dataScope !== 'daemon'
2737
+ || commandFullAccess.source !== 'fullaccess-command'
2738
+ || !commandFullAccess.authorizedBy) {
2739
+ throw new Error('fullaccess command authorization is missing or malformed');
2740
+ }
2741
+ fullAccessAuthorization = commandFullAccess;
2742
+ }
2743
+ else {
2744
+ if (message.source !== 'trigger'
2745
+ || !message.triggerMeta?.triggerId
2746
+ || !message.triggerMeta.runId
2747
+ || !message.triggerMeta.attemptId
2748
+ || !message.triggerMeta.authorizedBy) {
2749
+ throw new Error('fullaccess Trigger override lacks trusted scheduler identity or approval provenance');
2750
+ }
2751
+ const currentOwners = new Set(loadDaemonConfig().owners ?? []);
2752
+ if (!currentOwners.has(message.triggerMeta.authorizedBy)) {
2753
+ throw new Error('fullaccess Trigger approval is no longer owned');
2754
+ }
2755
+ fullAccessAuthorization = {
2756
+ permissionMode: 'fullaccess',
2757
+ processRole: 'fullaccess-run',
2758
+ dataScope: 'daemon',
2759
+ source: 'trigger',
2760
+ triggerId: message.triggerMeta.triggerId,
2761
+ runId: message.triggerMeta.runId,
2762
+ attemptId: message.triggerMeta.attemptId,
2763
+ authorizedBy: message.triggerMeta.authorizedBy,
2764
+ };
2765
+ }
2766
+ this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
2767
+ effectivePermissionMode = 'fullaccess';
2487
2768
  }
2488
- catch (e) {
2489
- logger.warn(`[ResponseEngine] permission mode resolution failed, using fallback: ${e instanceof Error ? e.message : String(e)}`);
2490
- effectivePermissionMode = 'readonly';
2769
+ else {
2770
+ try {
2771
+ effectivePermissionMode = constrainRuntimePermissionMode({
2772
+ selfAid: selfAid || undefined,
2773
+ role: peerRole,
2774
+ requestedValue: triggerPermissionModeOverride,
2775
+ }).effectiveValue;
2776
+ }
2777
+ catch (e) {
2778
+ logger.warn(`[ResponseEngine] permission mode resolution failed, using fallback: ${e instanceof Error ? e.message : String(e)}`);
2779
+ effectivePermissionMode = 'readonly';
2780
+ }
2491
2781
  }
2782
+ // The mode is resolved after the initial context is assembled. Keep
2783
+ // the runner-held object live so preflight, approvals, and audit
2784
+ // records all observe the same normalized value.
2785
+ permissionContext.permissionMode = effectivePermissionMode;
2786
+ permissionContext.executionPermission = fullAccessAuthorization;
2492
2787
  // 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
2493
2788
  // 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
2494
2789
  // 多对端并发各自独立解析、各自传参,无共享状态可被污染。
@@ -2622,7 +2917,9 @@ export class ResponseEngine {
2622
2917
  }
2623
2918
  // permissionMode 随角色策略或 trigger override 传入;单 runner
2624
2919
  // 嵌入/测试路径没有 self/peer 作用域时,避免制造无配置来源的 override。
2625
- const shouldPassPermissionMode = !!message.triggerMeta?.permissionModeOverride || !!selfAid;
2920
+ const shouldPassPermissionMode = !!fullAccessAuthorization
2921
+ || !!message.triggerMeta?.permissionModeOverride
2922
+ || !!selfAid;
2626
2923
  if (shouldPassPermissionMode) {
2627
2924
  modelOverride = { ...(modelOverride || {}), permissionMode: effectivePermissionMode };
2628
2925
  }
@@ -2638,6 +2935,61 @@ export class ResponseEngine {
2638
2935
  sessionTitle: deriveSessionTitle(session.name, message.content, session.threadId),
2639
2936
  };
2640
2937
  }
2938
+ // Capture the authoritative topic binding after begin() has durably
2939
+ // published this logical run's TurnLease. A topic UNBOUND value is
2940
+ // explicit and must never fall back to runner-local cache. This must
2941
+ // happen before task-start auto compact because compact is a backend
2942
+ // management action just like the subsequent runQuery.
2943
+ let runAgentSessionId = session.agentSessionId;
2944
+ if (session.threadId
2945
+ && typeof this.sessionManager.getSessionById === 'function') {
2946
+ const latestBackendSession = await this.sessionManager.getSessionById(session.id);
2947
+ if (!latestBackendSession || latestBackendSession.threadId !== session.threadId) {
2948
+ throw new Error('topic session changed before backend run started');
2949
+ }
2950
+ runAgentSessionId = latestBackendSession.agentSessionId;
2951
+ session.agentSessionId = runAgentSessionId;
2952
+ session.metadata = latestBackendSession.metadata;
2953
+ }
2954
+ modelOverride = {
2955
+ ...(modelOverride || {}),
2956
+ ...(session.threadId
2957
+ ? { backend: { kind: 'topic', agentSessionId: runAgentSessionId ?? null } }
2958
+ : {}),
2959
+ turn: {
2960
+ sessionId: turnLease.sessionId,
2961
+ taskId,
2962
+ turnId: turnLease.turnId,
2963
+ generation: turnLease.generation,
2964
+ inputId: turnLease.inputId,
2965
+ },
2966
+ };
2967
+ const refreshTopicBindingForLogicalRetry = async () => {
2968
+ if (!session.threadId || typeof this.sessionManager.getSessionById !== 'function')
2969
+ return;
2970
+ const latest = await this.sessionManager.getSessionById(session.id);
2971
+ if (!latest || latest.threadId !== session.threadId) {
2972
+ throw new Error('topic session changed during backend run');
2973
+ }
2974
+ const latestAgentSessionId = latest.agentSessionId;
2975
+ session.agentSessionId = latestAgentSessionId;
2976
+ session.metadata = latest.metadata;
2977
+ if (latestAgentSessionId === runAgentSessionId)
2978
+ return;
2979
+ logger.info(`[ResponseEngine] Refreshing authoritative topic backend for logical retry: `
2980
+ + `session=${session.id} task=${taskId} backend=${latestAgentSessionId ?? 'none'}`);
2981
+ runAgentSessionId = latestAgentSessionId;
2982
+ modelOverride = {
2983
+ ...(modelOverride || {}),
2984
+ backend: { kind: 'topic', agentSessionId: runAgentSessionId ?? null },
2985
+ };
2986
+ };
2987
+ // 预压缩必须使用与本轮 runQuery 相同的有效模型/强度覆盖。
2988
+ // 模型解析完成后再执行,避免关系级模型存在时回落到 runner 默认模型。
2989
+ await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer, modelOverride, fullAccessAuthorization
2990
+ ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
2991
+ : undefined);
2992
+ startTime = Date.now();
2641
2993
  const causationPath = inputCausation.trigger?.path ?? [];
2642
2994
  const originNode = causationPath[0];
2643
2995
  logger.info(`[ResponseEngine] execution context session=${session.id}`
@@ -2805,7 +3157,7 @@ export class ResponseEngine {
2805
3157
  }];
2806
3158
  const peerItems = (() => {
2807
3159
  if (message.handoffDelivery && this.handoffRuntime) {
2808
- const items = this.handoffRuntime.buildPromptItems(message);
3160
+ const items = this.handoffRuntime.buildPromptItems(message, peerRole);
2809
3161
  if (items.length > 0) {
2810
3162
  v2HandoffIds = Array.from(new Set(items.flatMap(item => (item.handoff?.handoffIds?.length
2811
3163
  ? item.handoff.handoffIds
@@ -2889,7 +3241,6 @@ export class ResponseEngine {
2889
3241
  // private child of the process-provided TMPDIR. Both paths are passed
2890
3242
  // explicitly so sandboxed helper commands never fall back to a shared
2891
3243
  // agent or project directory for transient state.
2892
- const sessionRuntimeDir = agent.getManagedTempDir?.(session.id) ?? ensureSessionRuntimeDir(session.id);
2893
3244
  const taskRuntimeContext = {
2894
3245
  taskId,
2895
3246
  sessionId: session.id,
@@ -2903,6 +3254,14 @@ export class ResponseEngine {
2903
3254
  peerName: peerName || undefined,
2904
3255
  peerType: message.peerType || session.metadata?.peerType || undefined,
2905
3256
  peerRole,
3257
+ permissionMode: effectivePermissionMode,
3258
+ ...(fullAccessAuthorization ? {
3259
+ processRole: fullAccessAuthorization.processRole,
3260
+ dataScope: fullAccessAuthorization.dataScope,
3261
+ authorizedBy: fullAccessAuthorization.authorizedBy,
3262
+ executionSource: fullAccessAuthorization.source,
3263
+ } : {}),
3264
+ daemonRuntimeEpoch: this.agentDelegationRegistry?.getRuntimeEpoch(),
2906
3265
  threadId: session.threadId || undefined,
2907
3266
  sessionRuntimeDir,
2908
3267
  runtimeLockDir: ensureRuntimeLockDir(sessionRuntimeDir),
@@ -2911,15 +3270,6 @@ export class ResponseEngine {
2911
3270
  };
2912
3271
  this.activeTaskRuntimeContexts.set(session.id, taskRuntimeContext);
2913
3272
  runtimeEnv = buildTaskRuntimeEnv(taskRuntimeContext);
2914
- modelOverride = {
2915
- ...(modelOverride || {}),
2916
- turn: {
2917
- taskId,
2918
- turnId: turnLease.turnId,
2919
- generation: turnLease.generation,
2920
- inputId: turnLease.inputId,
2921
- },
2922
- };
2923
3273
  if (this.agentDelegationRegistry && configActorId && selfAid && peerKey) {
2924
3274
  const delegationToken = this.agentDelegationRegistry.issue({
2925
3275
  sessionId: session.id,
@@ -2932,13 +3282,14 @@ export class ResponseEngine {
2932
3282
  selfAid,
2933
3283
  peerKey,
2934
3284
  issuedRole: peerRole,
3285
+ ...(fullAccessAuthorization ? { executionIdentity: fullAccessAuthorization } : {}),
2935
3286
  });
2936
3287
  runtimeEnv[AGENT_DELEGATION_TOKEN_ENV] = delegationToken;
2937
3288
  }
2938
3289
  const retryScheduler = new RetryScheduler(fallbackCandidates, modelOverride?.model || agentModel, model => (typeof agent.resolveModelId === 'function'
2939
3290
  ? (agent.resolveModelId(model) ?? model)
2940
3291
  : model));
2941
- const retryAttemptTimeoutMs = this.retryAttemptTimeoutMs();
3292
+ const apiRetryTimeoutMs = this.apiRetryTimeoutMs();
2942
3293
  const retryInputIsAtMostOnce = agent.retryInputSemantics === 'at_most_once';
2943
3294
  let runAttempt = 1;
2944
3295
  const recordRetryHealthError = async (retryError) => {
@@ -2956,11 +3307,45 @@ export class ResponseEngine {
2956
3307
  };
2957
3308
  while (true) {
2958
3309
  let streamRegistered = false;
2959
- const shouldTimeoutRetryAttempt = retryScheduler.shouldTimeoutAttempt() && retryAttemptTimeoutMs !== undefined;
3310
+ const shouldTimeoutRetryAttempt = retryScheduler.shouldTimeoutAttempt() && apiRetryTimeoutMs !== undefined;
2960
3311
  let attemptTimeout;
2961
3312
  try {
2962
- logger.info(`[ResponseEngine] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${runAttempt} apiRetries=${retryScheduler.apiRetryAttemptsMade} agentSessionId=${session.agentSessionId ?? 'none'}`);
2963
- const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
3313
+ logger.info(`[ResponseEngine] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${runAttempt} apiRetries=${retryScheduler.apiRetryAttemptsMade} agentSessionId=${runAgentSessionId ?? 'none'} topicBinding=${session.threadId ? 'authoritative' : 'legacy'}`);
3314
+ if (fullAccessAuthorization) {
3315
+ this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
3316
+ if (fullAccessAuditStartedAt === undefined) {
3317
+ fullAccessAuditProcessRole = 'fullaccess-run';
3318
+ fullAccessAuditStartedAt = Date.now();
3319
+ fullAccessAuditBaseagent = normalizedBaseagent.canonical;
3320
+ fullAccessAuditModel = modelOverride?.model || agentModel;
3321
+ auditFullAccessEvent({
3322
+ event: fullAccessAuthorization.source === 'trigger'
3323
+ ? 'fullaccess.trigger.execution.started'
3324
+ : 'fullaccess.execution.started',
3325
+ source: fullAccessAuthorization.source,
3326
+ actorId: fullAccessAuthorization.authorizedBy,
3327
+ processRole: fullAccessAuditProcessRole,
3328
+ agentAid: selfAid,
3329
+ agentName: agentNameForStats,
3330
+ sessionId: session.id,
3331
+ taskId,
3332
+ messageId: message.messageId,
3333
+ triggerId: fullAccessAuthorization.triggerId,
3334
+ runId: fullAccessAuthorization.runId,
3335
+ attemptId: fullAccessAuthorization.attemptId,
3336
+ dataScope: fullAccessAuthorization.dataScope,
3337
+ authorizedBy: fullAccessAuthorization.authorizedBy,
3338
+ baseagent: fullAccessAuditBaseagent,
3339
+ model: fullAccessAuditModel,
3340
+ });
3341
+ // Audit is synchronous today, but it is an observable boundary.
3342
+ // Revalidate once more so an administrative revocation observed
3343
+ // there cannot be followed by a privileged Runner call.
3344
+ this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
3345
+ }
3346
+ fullAccessRunnerInvoked = true;
3347
+ }
3348
+ const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
2964
3349
  // The turn may be superseded while the runner is still creating its
2965
3350
  // backend query. Replay the interrupt after runQuery resolves, when
2966
3351
  // every runner is required to have an addressable cancellation handle.
@@ -2970,16 +3355,20 @@ export class ResponseEngine {
2970
3355
  agent.registerStream(streamKey, stream);
2971
3356
  streamRegistered = true;
2972
3357
  if (shouldTimeoutRetryAttempt) {
2973
- attemptTimeout = createRetryAttemptTimeout(retryAttemptTimeoutMs);
3358
+ attemptTimeout = createRetryAttemptTimeout(apiRetryTimeoutMs);
2974
3359
  resetTimer('retry_attempt');
2975
3360
  }
2976
3361
  const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
2977
3362
  resetTimer(eventType, toolName);
2978
3363
  attemptTimeout?.reset();
2979
- }, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, session.selfAID || message.selfAID);
3364
+ }, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid);
2980
3365
  streamResult = attemptTimeout
2981
3366
  ? await Promise.race([processAttempt, attemptTimeout.promise])
2982
3367
  : await processAttempt;
3368
+ // Activation can complete either before runQuery returns (Codex)
3369
+ // or while its event stream is consumed (Claude/Gemini/Ecagent).
3370
+ // Always reconcile from strict latest before any logical retry.
3371
+ await refreshTopicBindingForLogicalRetry();
2983
3372
  if (!streamResult.isError) {
2984
3373
  const protocolDecision = this.turnCoordinator.evaluateCommit(turnLease, this.buildTurnCommitEvidence(session, streamResult));
2985
3374
  const protocolReason = protocolDecision.ok ? undefined : protocolDecision.reason;
@@ -3015,6 +3404,7 @@ export class ResponseEngine {
3015
3404
  break; // 成功,跳出重试循环
3016
3405
  }
3017
3406
  catch (retryError) {
3407
+ await refreshTopicBindingForLogicalRetry();
3018
3408
  const retryAttemptTimedOut = retryError instanceof RetryAttemptTimeoutError;
3019
3409
  if (retryAttemptTimedOut) {
3020
3410
  logger.warn(`[ResponseEngine] Retry attempt ${runAttempt} timed out after ${retryError.timeoutMs}ms without agent events; interrupting current stream`);
@@ -3134,9 +3524,12 @@ export class ResponseEngine {
3134
3524
  resetTimer,
3135
3525
  shouldSuppress,
3136
3526
  proactive,
3137
- proactiveSelfAid: session.selfAID || message.selfAID,
3527
+ proactiveSelfAid: selfAid,
3138
3528
  permissionMode: effectivePermissionMode,
3139
3529
  turnLease,
3530
+ beforeRunQuery: fullAccessAuthorization
3531
+ ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
3532
+ : undefined,
3140
3533
  });
3141
3534
  }
3142
3535
  else {
@@ -3177,9 +3570,12 @@ export class ResponseEngine {
3177
3570
  resetTimer,
3178
3571
  shouldSuppress,
3179
3572
  proactive,
3180
- proactiveSelfAid: session.selfAID || message.selfAID,
3573
+ proactiveSelfAid: selfAid,
3181
3574
  permissionMode: effectivePermissionMode,
3182
3575
  turnLease,
3576
+ beforeRunQuery: fullAccessAuthorization
3577
+ ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
3578
+ : undefined,
3183
3579
  });
3184
3580
  // 重试后仍然 prompt_too_long:显示友好提示
3185
3581
  const retryStillTooLong = streamResult.isError && streamHitContextLimit(streamResult);
@@ -3202,7 +3598,10 @@ export class ResponseEngine {
3202
3598
  }
3203
3599
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
3204
3600
  logger.info(`[ResponseEngine] Stale turn stopped before publish: session=${session.id} task=${taskId}`);
3205
- await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation);
3601
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation, {
3602
+ expected: turnLease?.generation,
3603
+ current: this.turnCoordinator.current(session).generation,
3604
+ });
3206
3605
  return;
3207
3606
  }
3208
3607
  if (!commitDecision.ok) {
@@ -3214,7 +3613,10 @@ export class ResponseEngine {
3214
3613
  }
3215
3614
  this.agentDelegationRegistry?.revokeTask(session.id, taskId);
3216
3615
  logger.info(`[ResponseEngine] Stale turn stopped before protocol rejection: session=${session.id} task=${taskId}`);
3217
- await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation);
3616
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, 'stale_generation', taskCausation, {
3617
+ expected: turnLease?.generation,
3618
+ current: this.turnCoordinator.current(session).generation,
3619
+ });
3218
3620
  return;
3219
3621
  }
3220
3622
  const reason = streamResult.protocolIncompleteReason || commitDecision.reason;
@@ -3416,7 +3818,10 @@ export class ResponseEngine {
3416
3818
  if (!commitDecision.ok) {
3417
3819
  logger.info(`[ResponseEngine] Turn commit rejected after flush: session=${session.id} task=${taskId} reason=${commitDecision.reason}`);
3418
3820
  if (commitDecision.reason === 'stale_generation') {
3419
- await this.publishTriggerExecutionInterrupted(message, adapter, envelope, commitDecision.reason, taskCausation);
3821
+ await this.publishTriggerExecutionInterrupted(message, adapter, envelope, commitDecision.reason, taskCausation, {
3822
+ expected: turnLease?.generation,
3823
+ current: this.turnCoordinator.current(session).generation,
3824
+ });
3420
3825
  }
3421
3826
  else {
3422
3827
  this.publishTriggerExecutionFailure(message, `turn_commit_rejected:${commitDecision.reason}`, {
@@ -3503,6 +3908,8 @@ export class ResponseEngine {
3503
3908
  this.eventBus.publish({
3504
3909
  type: 'task:error',
3505
3910
  sessionId: session.id,
3911
+ taskId,
3912
+ generation: turnLease?.generation,
3506
3913
  error: errorSummary,
3507
3914
  errorType,
3508
3915
  agentName: agentNameForStats,
@@ -3620,12 +4027,18 @@ export class ResponseEngine {
3620
4027
  cache_read_tokens: sum.cache_read_tokens,
3621
4028
  cache_creation_tokens: sum.cache_creation_tokens,
3622
4029
  // 顶层 cost_usd/cost_cny 保持向后兼容 = 网关实际价
3623
- cost_usd: sum.cost_gateway_usd,
3624
- cost_cny: sum.cost_gateway_cny,
4030
+ cost_usd: roundCostForOutput(sum.cost_gateway_usd),
4031
+ cost_cny: roundCostForOutput(sum.cost_gateway_cny),
3625
4032
  call_count: sum.calls,
3626
4033
  cost: {
3627
- official: { usd: sum.cost_official_usd, cny: sum.cost_official_cny },
3628
- gateway: { usd: sum.cost_gateway_usd, cny: sum.cost_gateway_cny },
4034
+ official: {
4035
+ usd: roundCostForOutput(sum.cost_official_usd),
4036
+ cny: roundCostForOutput(sum.cost_official_cny),
4037
+ },
4038
+ gateway: {
4039
+ usd: roundCostForOutput(sum.cost_gateway_usd),
4040
+ cny: roundCostForOutput(sum.cost_gateway_cny),
4041
+ },
3629
4042
  },
3630
4043
  };
3631
4044
  }
@@ -3643,10 +4056,13 @@ export class ResponseEngine {
3643
4056
  }
3644
4057
  else {
3645
4058
  // cost 同时给原价(official)与网关实际价(gateway);顶层 cost_usd/cost_cny 保持向后兼容 = 网关价。
3646
- const gatewayUsd = turnCost.gateway?.usd ?? turnCost.official?.usd ?? 0;
3647
- const gatewayCny = turnCost.gateway?.cny ?? turnCost.official?.cny ?? 0;
4059
+ const gatewayUsd = roundCostForOutput(turnCost.gateway?.usd ?? turnCost.official?.usd ?? 0);
4060
+ const gatewayCny = roundCostForOutput(turnCost.gateway?.cny ?? turnCost.official?.cny ?? 0);
3648
4061
  const turnCostBlock = {
3649
- official: { usd: turnCost.official?.usd ?? 0, cny: turnCost.official?.cny ?? 0 },
4062
+ official: {
4063
+ usd: roundCostForOutput(turnCost.official?.usd ?? 0),
4064
+ cny: roundCostForOutput(turnCost.official?.cny ?? 0),
4065
+ },
3650
4066
  gateway: { usd: gatewayUsd, cny: gatewayCny },
3651
4067
  };
3652
4068
  // 最后一次访问:本轮可能有多次大模型调用(numTurns>1),整轮的 turnCostBlock 不等于
@@ -3661,10 +4077,13 @@ export class ResponseEngine {
3661
4077
  model: lastModel, turns: 1,
3662
4078
  });
3663
4079
  const lp = resolvePrices(resolveRoot(), lastEvent, agent.getGatewayPricing?.());
3664
- const lpGwUsd = lp.gateway?.usd ?? lp.official?.usd ?? 0;
3665
- const lpGwCny = lp.gateway?.cny ?? lp.official?.cny ?? 0;
4080
+ const lpGwUsd = roundCostForOutput(lp.gateway?.usd ?? lp.official?.usd ?? 0);
4081
+ const lpGwCny = roundCostForOutput(lp.gateway?.cny ?? lp.official?.cny ?? 0);
3666
4082
  lastModelCall = { ...lastModelCall, cost: {
3667
- official: { usd: lp.official?.usd ?? 0, cny: lp.official?.cny ?? 0 },
4083
+ official: {
4084
+ usd: roundCostForOutput(lp.official?.usd ?? 0),
4085
+ cny: roundCostForOutput(lp.official?.cny ?? 0),
4086
+ },
3668
4087
  gateway: { usd: lpGwUsd, cny: lpGwCny },
3669
4088
  } };
3670
4089
  }
@@ -3712,6 +4131,8 @@ export class ResponseEngine {
3712
4131
  this.eventBus.publish({
3713
4132
  type: 'task:completed',
3714
4133
  sessionId: session.id,
4134
+ taskId,
4135
+ generation: turnLease.generation,
3715
4136
  channel: message.channel,
3716
4137
  channelId: message.channelId,
3717
4138
  terminalReason: streamResult.terminalReason,
@@ -3757,6 +4178,8 @@ export class ResponseEngine {
3757
4178
  });
3758
4179
  }
3759
4180
  catch (error) {
4181
+ fullAccessExecutionFailed = true;
4182
+ fullAccessExecutionFailureReason = error instanceof Error ? error.message : String(error);
3760
4183
  const authoritativeTimeoutError = timeoutControl?.claim();
3761
4184
  if (authoritativeTimeoutError) {
3762
4185
  error = authoritativeTimeoutError;
@@ -3841,6 +4264,8 @@ export class ResponseEngine {
3841
4264
  this.eventBus.publish({
3842
4265
  type: 'task:error',
3843
4266
  sessionId: session.id,
4267
+ taskId,
4268
+ generation: turnLease?.generation,
3844
4269
  error: errorMsg,
3845
4270
  errorType,
3846
4271
  agentName: agentNameForStats,
@@ -3861,14 +4286,15 @@ export class ResponseEngine {
3861
4286
  }
3862
4287
  // 发送用户友好的错误消息
3863
4288
  // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送错误提示
3864
- // processEventStream 已通过 renderer 发过错误时也跳过
4289
+ // 普通渠道可跳过 renderer 已发送的重复错误;daemon trigger 仍必须
4290
+ // 发送结构化终态,否则 DaemonChannel 会一直等到 watchdog。
3865
4291
  const retryExhaustedCount = getRetryExhaustedCount(error);
3866
4292
  const modelFallbackExhaustedMessage = getModelFallbackExhaustedMessage(error);
3867
4293
  const retryInputAlreadySubmitted = wasRetryInputAlreadySubmitted(error);
3868
4294
  if (isUserInterrupt) {
3869
4295
  logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
3870
4296
  }
3871
- else if (error?._errorAlreadySent && !retryExhaustedCount && !isTimeout && !isTotalExecutionTimeout) {
4297
+ else if (error?._errorAlreadySent && !daemonTrigger && !retryExhaustedCount && !isTimeout && !isTotalExecutionTimeout) {
3872
4298
  logger.info(`[ResponseEngine] Error already sent via renderer, skip sending duplicate message`);
3873
4299
  }
3874
4300
  else {
@@ -3949,6 +4375,47 @@ export class ResponseEngine {
3949
4375
  }
3950
4376
  }
3951
4377
  finally {
4378
+ if (fullAccessAuthorization) {
4379
+ const startedAt = fullAccessAuditStartedAt;
4380
+ const executed = fullAccessRunnerInvoked;
4381
+ const interrupted = isExpectedTerminalInterrupt(streamResult.terminalReason);
4382
+ const failed = fullAccessExecutionFailed || streamResult.isError;
4383
+ auditFullAccessEvent({
4384
+ event: fullAccessAuthorization.source === 'trigger'
4385
+ ? 'fullaccess.trigger.execution.ended'
4386
+ : 'fullaccess.execution.ended',
4387
+ source: fullAccessAuthorization.source,
4388
+ actorId: fullAccessAuthorization.authorizedBy,
4389
+ processRole: fullAccessAuditProcessRole,
4390
+ agentAid: selfAid,
4391
+ agentName: agentNameForStats,
4392
+ sessionId: session.id,
4393
+ taskId,
4394
+ messageId: message.messageId,
4395
+ triggerId: fullAccessAuthorization.triggerId,
4396
+ runId: fullAccessAuthorization.runId,
4397
+ attemptId: fullAccessAuthorization.attemptId,
4398
+ dataScope: fullAccessAuthorization.dataScope,
4399
+ authorizedBy: fullAccessAuthorization.authorizedBy,
4400
+ baseagent: fullAccessAuditBaseagent,
4401
+ model: fullAccessAuditModel,
4402
+ result: !executed ? 'blocked' : failed ? 'failed' : interrupted ? 'interrupted' : 'completed',
4403
+ reason: fullAccessExecutionFailureReason
4404
+ ?? streamResult.errors?.join('; ')
4405
+ ?? (!executed ? 'fullaccess execution stopped before Runner invocation' : undefined),
4406
+ executed,
4407
+ executionState: !executed ? 'blocked' : failed ? 'failed' : 'completed',
4408
+ durationMs: executed && startedAt !== undefined ? Date.now() - startedAt : undefined,
4409
+ });
4410
+ this.fullAccessTerminalMessages.add(message);
4411
+ }
4412
+ // The authorization belongs to this logical run only. Clear the live
4413
+ // runner context as well as relying on the Message object to be dropped,
4414
+ // so late callbacks cannot observe a completed fullaccess grant.
4415
+ const completedPermissionContext = activePermissionContext;
4416
+ if (completedPermissionContext && completedPermissionContext.executionPermission === fullAccessAuthorization) {
4417
+ delete completedPermissionContext.executionPermission;
4418
+ }
3952
4419
  // 同一 session 的新任务可能已替换登记;旧任务结束时不得删掉新任务的 renderer。
3953
4420
  if (this.activeRenderers.get(session.id)?.taskId === taskId) {
3954
4421
  this.activeRenderers.delete(session.id);
@@ -3963,7 +4430,7 @@ export class ResponseEngine {
3963
4430
  snapshot.end(session.id, taskId);
3964
4431
  }
3965
4432
  }
3966
- async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer) {
4433
+ async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer, modelOverride, beforeCompact) {
3967
4434
  if (!session.agentSessionId || !canCompactAgent(agent)) {
3968
4435
  logger.debug(`[ResponseEngine] Auto compact skipped: session=${session.id} agentSessionId=${session.agentSessionId || 'none'} canCompact=${canCompactAgent(agent)} agent=${agent.name}`);
3969
4436
  return;
@@ -3978,13 +4445,20 @@ export class ResponseEngine {
3978
4445
  await renderer.flush();
3979
4446
  this.announcedCompactStarts.add(session.id);
3980
4447
  try {
3981
- const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
3982
- if (compacted) {
4448
+ // Auto compact is a privileged backend-management operation for a
4449
+ // fullaccess run. Revalidate immediately before invoking the runner so a
4450
+ // daemon gate/owner revocation observed after task setup cannot still
4451
+ // cause host-level runner activity.
4452
+ beforeCompact?.();
4453
+ const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath, modelOverride);
4454
+ const compactResult = compacted;
4455
+ if (compactResult === true || compactResult?.ok === true) {
3983
4456
  await this.emitOperationalNotice(renderer, '✅ 上下文压缩完成,继续处理...', 'info', 'auto-compact-complete');
3984
4457
  await renderer.flush();
3985
4458
  }
3986
4459
  else {
3987
- logger.warn(`[ResponseEngine] Auto compact at task.start returned false (session=${session.id})`);
4460
+ const compactFailure = compactResult;
4461
+ logger.warn(`[ResponseEngine] Auto compact at task.start failed: session=${session.id} code=${compactFailure?.code ?? 'sdk_error'} durationMs=${compactFailure?.durationMs ?? 0} message=${compactFailure?.message ?? 'compact failed'}`);
3988
4462
  }
3989
4463
  }
3990
4464
  catch (err) {
@@ -4121,14 +4595,22 @@ export class ResponseEngine {
4121
4595
  const groupName = await adapter?.getGroupName?.(session.metadata.groupId).catch(() => undefined);
4122
4596
  if (groupName) {
4123
4597
  session.metadata.groupName = groupName;
4124
- await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
4598
+ await this.sessionManager.patchSessionMetadata?.(session.id, { groupName });
4599
+ if (typeof this.sessionManager.patchSessionMetadata !== 'function') {
4600
+ await this.sessionManager.updateSession(session.id, { metadata: { groupName } });
4601
+ }
4125
4602
  }
4126
4603
  }
4127
4604
  // 同步服务端 mention_mode,供同一群会话的上下文和菜单展示使用。
4128
4605
  if (message.chatType === 'group' && message.mentionMode && session.metadata?.mentionMode !== message.mentionMode) {
4129
4606
  logger.info(`[ResponseEngine] mentionMode sync: sessionId=${session.id} ${session.metadata?.mentionMode ?? 'none'} -> ${message.mentionMode}`);
4130
4607
  session.metadata = { ...(session.metadata || {}), mentionMode: message.mentionMode };
4131
- await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
4608
+ if (typeof this.sessionManager.patchSessionMetadata === 'function') {
4609
+ await this.sessionManager.patchSessionMetadata(session.id, { mentionMode: message.mentionMode });
4610
+ }
4611
+ else {
4612
+ await this.sessionManager.updateSession(session.id, { metadata: { mentionMode: message.mentionMode } });
4613
+ }
4132
4614
  }
4133
4615
  // chatMode 策略由 agent/relation behavior 配置在处理阶段解析;此处不再写 session 级参数。
4134
4616
  // replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
@@ -4166,6 +4648,11 @@ export class ResponseEngine {
4166
4648
  // Per-session agent name for stats bucketing
4167
4649
  const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
4168
4650
  const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
4651
+ const lifecycleAgentAid = proactiveSelfAid || session.selfAID;
4652
+ const lifecycleAgentName = agentNameForStats !== '<unknown>'
4653
+ ? agentNameForStats
4654
+ : lifecycleAgentAid || '<unknown>';
4655
+ const lifecyclePermissionMode = normalizeExecutionPermissionMode(permissionMode);
4169
4656
  let hasReceivedText = false;
4170
4657
  let hasProjectedCurrentReplyText = false;
4171
4658
  let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
@@ -4225,11 +4712,17 @@ export class ResponseEngine {
4225
4712
  if (event.type === 'complete') {
4226
4713
  event = normalizeCompleteAgentEvent(event);
4227
4714
  }
4228
- if (event.type === 'tool_result' && event.isError && !event.errorCode) {
4229
- event = {
4230
- ...event,
4231
- errorCode: classifyToolErrorCode({ error: event.error, result: event.result }),
4232
- };
4715
+ if (event.type === 'tool_result' && event.isError) {
4716
+ const classificationMissing = !normalizeToolErrorCode(event.errorCode)
4717
+ && event.decisionSource !== 'policy'
4718
+ && event.decisionSource !== 'approval';
4719
+ const errorCode = classifyToolErrorCode({
4720
+ errorCode: event.errorCode,
4721
+ decisionSource: event.decisionSource,
4722
+ });
4723
+ if (event.errorCode !== errorCode) {
4724
+ event = { ...event, errorCode, ...(classificationMissing ? { classificationMissing: true } : {}) };
4725
+ }
4233
4726
  }
4234
4727
  // 每收到事件重置空闲超时
4235
4728
  const toolName = event.type === 'tool_use' ? event.name : undefined;
@@ -4332,7 +4825,13 @@ export class ResponseEngine {
4332
4825
  eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
4333
4826
  }
4334
4827
  else if (event.type === 'tool_result') {
4335
- eventDetail = ` tool=${event.name} ok=${!event.isError}`;
4828
+ const decision = event.decision ?? (event.isError ? 'error' : 'allow');
4829
+ const executed = event.executed ?? !event.isError;
4830
+ const executionState = event.executionState
4831
+ ?? (decision === 'deny' ? 'blocked' : event.isError ? 'failed' : 'completed');
4832
+ eventDetail = ` tool=${event.name} ok=${decision === 'allow' && executed}`
4833
+ + ` decision=${decision} executed=${executed} executionState=${executionState}`
4834
+ + (event.policyCode ? ` policy=${event.policyCode}` : '');
4336
4835
  }
4337
4836
  const frameworkEvents = new Set(['session_id', 'state_changed', 'status']);
4338
4837
  if (frameworkEvents.has(event.type)) {
@@ -4554,8 +5053,9 @@ export class ResponseEngine {
4554
5053
  input: event.input,
4555
5054
  ...(event.callId ? { callId: event.callId } : {}),
4556
5055
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4557
- agentAid: session.selfAID ?? 'unknown',
4558
- permissionMode: permissionMode ?? 'unknown',
5056
+ agentName: lifecycleAgentName,
5057
+ agentAid: lifecycleAgentAid ?? 'unknown',
5058
+ permissionMode: lifecyclePermissionMode,
4559
5059
  decision: 'pending',
4560
5060
  decisionSource: 'runner',
4561
5061
  executed: false,
@@ -4597,6 +5097,16 @@ export class ResponseEngine {
4597
5097
  toolName: event.name,
4598
5098
  toolInput: event.input || {},
4599
5099
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
5100
+ recordReminder: reminder => {
5101
+ this.eventBus.publish({
5102
+ type: 'runner:proactive-reminder',
5103
+ sessionId: session.id,
5104
+ ...reminder,
5105
+ injection: 'requested',
5106
+ timestamp: Date.now(),
5107
+ causation,
5108
+ });
5109
+ },
4600
5110
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
4601
5111
  isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
4602
5112
  logger,
@@ -4622,6 +5132,7 @@ export class ResponseEngine {
4622
5132
  sessionId: session.id,
4623
5133
  callId: event.callId,
4624
5134
  correlationId: event.correlationId,
5135
+ requestId: event.requestId,
4625
5136
  });
4626
5137
  this.eventBus.publish({
4627
5138
  type: 'tool:result',
@@ -4630,16 +5141,29 @@ export class ResponseEngine {
4630
5141
  sessionId: session.id,
4631
5142
  toolName: event.name,
4632
5143
  isError: event.isError,
4633
- ...(event.isError ? { errorCode: classifyToolErrorCode({ errorCode: event.errorCode, error: event.error, result: event.result }) } : {}),
4634
- agentName: agentNameForStats,
5144
+ ...(event.isError ? {
5145
+ errorCode: classifyToolErrorCode({
5146
+ errorCode: event.errorCode,
5147
+ decisionSource: event.decisionSource,
5148
+ }),
5149
+ ...((event.classificationMissing || (!normalizeToolErrorCode(event.errorCode)
5150
+ && event.decisionSource !== 'policy'
5151
+ && event.decisionSource !== 'approval')) ? { classificationMissing: true } : {}),
5152
+ } : {}),
4635
5153
  ...(event.callId ? { callId: event.callId } : {}),
4636
5154
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4637
- agentAid: session.selfAID ?? 'unknown',
4638
- permissionMode: permissionMode ?? 'unknown',
4639
- decision: event.isError ? 'error' : 'allow',
4640
- decisionSource: 'runner',
4641
- executed: true,
4642
- executionState: event.isError ? 'failed' : 'completed',
5155
+ ...(event.requestId ? { requestId: event.requestId } : {}),
5156
+ agentName: lifecycleAgentName,
5157
+ agentAid: lifecycleAgentAid ?? 'unknown',
5158
+ permissionMode: lifecyclePermissionMode,
5159
+ decision: event.decision ?? (event.isError ? 'error' : 'allow'),
5160
+ decisionSource: event.decisionSource ?? 'runner',
5161
+ ...(event.policyCode ? { policyCode: event.policyCode } : {}),
5162
+ ...(event.reason ? { reason: event.reason } : {}),
5163
+ executed: event.executed ?? (event.decision === 'deny' ? false : true),
5164
+ executionState: event.executionState ?? (event.decision === 'deny'
5165
+ ? 'blocked'
5166
+ : event.isError ? 'failed' : 'completed'),
4643
5167
  timestamp: Date.now(),
4644
5168
  causation,
4645
5169
  });
@@ -4686,7 +5210,23 @@ export class ResponseEngine {
4686
5210
  result: event.result,
4687
5211
  isError: event.isError,
4688
5212
  error: event.error,
5213
+ decision: event.decision,
5214
+ decisionSource: event.decisionSource,
5215
+ executed: event.executed,
5216
+ executionState: event.executionState,
5217
+ policyCode: event.policyCode,
5218
+ requestId: event.requestId,
4689
5219
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
5220
+ recordReminder: reminder => {
5221
+ this.eventBus.publish({
5222
+ type: 'runner:proactive-reminder',
5223
+ sessionId: session.id,
5224
+ ...reminder,
5225
+ injection: 'requested',
5226
+ timestamp: Date.now(),
5227
+ causation,
5228
+ });
5229
+ },
4690
5230
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
4691
5231
  isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
4692
5232
  logger,
@@ -4793,7 +5333,12 @@ export class ResponseEngine {
4793
5333
  lastReplyText,
4794
5334
  updateSessionMeta: async (patch) => {
4795
5335
  session.metadata = { ...(session.metadata || {}), ...patch };
4796
- await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
5336
+ if (typeof this.sessionManager.patchSessionMetadata === 'function') {
5337
+ await this.sessionManager.patchSessionMetadata(session.id, patch);
5338
+ }
5339
+ else {
5340
+ await this.sessionManager.updateSession(session.id, { metadata: patch });
5341
+ }
4797
5342
  },
4798
5343
  logger,
4799
5344
  });
@@ -4865,11 +5410,15 @@ export class ResponseEngine {
4865
5410
  // and mark the error so outer catch won't send a duplicate message
4866
5411
  const hasErrorSuppressingContent = hasErrorResult || renderer.hasNonLifecycleContent();
4867
5412
  if (hasErrorSuppressingContent) {
5413
+ let errorOutputFlushed = false;
4868
5414
  try {
4869
5415
  await renderer.flush(true);
5416
+ errorOutputFlushed = true;
4870
5417
  }
4871
- catch { }
4872
- if (error instanceof Error) {
5418
+ catch (flushError) {
5419
+ logger.warn(`[ResponseEngine] Failed to flush error output: ${flushError instanceof Error ? flushError.message : String(flushError)}`);
5420
+ }
5421
+ if (errorOutputFlushed && error instanceof Error) {
4873
5422
  error._errorAlreadySent = true;
4874
5423
  }
4875
5424
  }