evolcore 0.0.19 → 0.0.21

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 (136) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +60 -9
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/baseagent.js +10 -6
  5. package/dist/agents/claude-runner.js +383 -111
  6. package/dist/agents/codex-app-server-client.js +10 -2
  7. package/dist/agents/codex-runner.js +405 -137
  8. package/dist/agents/ecagent-runner.js +174 -63
  9. package/dist/agents/gemini-runner.js +130 -30
  10. package/dist/agents/request-identity.js +25 -0
  11. package/dist/agents/runner-types.js +19 -0
  12. package/dist/aun/aid/agentmd.js +66 -2
  13. package/dist/aun/aid/identity.js +4 -1
  14. package/dist/aun/aid/index.js +1 -1
  15. package/dist/aun/msg/group.js +86 -9
  16. package/dist/aun/msg/history.js +213 -36
  17. package/dist/aun/msg/managed-operation.js +58 -9
  18. package/dist/aun/msg/p2p.js +26 -11
  19. package/dist/aun/outbox.js +295 -68
  20. package/dist/aun/service-proxy.js +43 -25
  21. package/dist/channels/aun.js +1004 -273
  22. package/dist/channels/daemon.js +6 -1
  23. package/dist/cli/agent-command.js +4 -3
  24. package/dist/cli/agent.js +66 -56
  25. package/dist/cli/aun-commands.js +177 -42
  26. package/dist/cli/command-log.js +10 -11
  27. package/dist/cli/contact.js +1 -0
  28. package/dist/cli/daemon-commands.js +81 -121
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/init.js +76 -24
  31. package/dist/cli/restart-monitor.js +3 -3
  32. package/dist/cli/task-context.js +46 -0
  33. package/dist/cli/trigger-command.js +1 -1
  34. package/dist/cli/watch-logs.js +10 -3
  35. package/dist/config/builtin-roles.js +1 -0
  36. package/dist/config/config-field-policy.js +16 -5
  37. package/dist/config/config-manager.js +226 -24
  38. package/dist/config/config-operation-service.js +1 -2
  39. package/dist/config/contact-operation-service.js +32 -1
  40. package/dist/config/contact-request-service.js +44 -0
  41. package/dist/config/daemon-services.js +186 -0
  42. package/dist/config/gateway-config.js +29 -16
  43. package/dist/config/lifecycle.js +16 -5
  44. package/dist/config/role-service.js +54 -3
  45. package/dist/config/schema-migration.js +550 -0
  46. package/dist/config-store.js +161 -12
  47. package/dist/core/agent-application-service.js +279 -0
  48. package/dist/core/audit/log-integrity.js +102 -0
  49. package/dist/core/auth/agent-delegation.js +31 -1
  50. package/dist/core/auth/auth-gateway.js +33 -4
  51. package/dist/core/auth/authorization-audit.js +155 -4
  52. package/dist/core/auth/operation-authorizer.js +41 -1
  53. package/dist/core/auth/operation-catalog.js +9 -1
  54. package/dist/core/bootstrap-messages.js +2 -2
  55. package/dist/core/bootstrap-service.js +27 -38
  56. package/dist/core/causation/aun-association.js +7 -4
  57. package/dist/core/channel-loader.js +0 -2
  58. package/dist/core/command/agent-control.js +56 -16
  59. package/dist/core/command/command-handler.js +290 -44
  60. package/dist/core/command/connect-menu.js +3 -4
  61. package/dist/core/command/group-menu.js +5 -7
  62. package/dist/core/command/menu-handler.js +279 -80
  63. package/dist/core/command/role-menu.js +21 -11
  64. package/dist/core/command/slash-gate.js +85 -18
  65. package/dist/core/command/slash-handler.js +350 -32
  66. package/dist/core/event-catalog.js +5 -0
  67. package/dist/core/evolagent.js +9 -4
  68. package/dist/core/handoff/dispatcher.js +4 -0
  69. package/dist/core/handoff/runtime.js +10 -0
  70. package/dist/core/handoff/store.js +32 -9
  71. package/dist/core/inference/text-inference.js +7 -15
  72. package/dist/core/message/im-renderer.js +83 -84
  73. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  74. package/dist/core/message/message-bridge.js +124 -10
  75. package/dist/core/message/message-log.js +14 -7
  76. package/dist/core/message/message-queue.js +206 -16
  77. package/dist/core/message/message-utils.js +12 -5
  78. package/dist/core/message/response-engine.js +495 -72
  79. package/dist/core/message/send-receipt.js +1 -0
  80. package/dist/core/message/stream-debouncer.js +9 -2
  81. package/dist/core/model/model-catalog.js +23 -15
  82. package/dist/core/model/model-diagnostics.js +28 -10
  83. package/dist/core/permission/approval-gateway.js +180 -6
  84. package/dist/core/permission/ec-command-parser.js +465 -56
  85. package/dist/core/permission/mode.js +18 -3
  86. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  87. package/dist/core/permission/readonly-shell-query.js +263 -9
  88. package/dist/core/permission/sandbox-runtime.js +205 -13
  89. package/dist/core/permission/tool-policy.js +673 -62
  90. package/dist/core/relation/peer-identity.js +18 -0
  91. package/dist/core/session/session-fs-store.js +154 -5
  92. package/dist/core/session/session-manager.js +299 -30
  93. package/dist/core/session/session-renew.js +19 -12
  94. package/dist/core/session/session-turn-coordinator.js +11 -4
  95. package/dist/eck/kit-renderer.js +18 -9
  96. package/dist/index.js +283 -65
  97. package/dist/ipc.js +374 -24
  98. package/dist/paths.js +64 -7
  99. package/dist/response-system/context-builder.js +1 -7
  100. package/dist/trigger/anomaly-store.js +1 -0
  101. package/dist/trigger/feedback.js +56 -5
  102. package/dist/trigger/history.js +79 -4
  103. package/dist/trigger/legacy-session-history.js +2 -2
  104. package/dist/trigger/parser.js +3 -2
  105. package/dist/trigger/validation.js +6 -1
  106. package/dist/utils/atomic-write.js +27 -0
  107. package/dist/utils/ecweb-utils.js +16 -2
  108. package/dist/utils/error-utils.js +4 -1
  109. package/dist/utils/logger.js +21 -2
  110. package/dist/utils/process-tree-stats.js +24 -4
  111. package/dist/utils/process-tree-worker.js +31 -0
  112. package/dist/utils/project-path.js +1 -2
  113. package/dist/utils/stats.js +52 -18
  114. package/dist/utils/welcome.js +2 -2
  115. package/kits/docs/INDEX.md +1 -1
  116. package/kits/docs/evolcore/INDEX.md +1 -1
  117. package/kits/docs/evolcore/contact.md +7 -1
  118. package/kits/docs/evolcore/msg.md +16 -0
  119. package/kits/rules/01-overview.md +1 -1
  120. package/kits/rules/03-identity.md +1 -1
  121. package/kits/rules/05-venue.md +1 -1
  122. package/kits/schemas/_meta.json +10 -5
  123. package/kits/schemas/agent-config.schema.10.json +2 -1
  124. package/kits/schemas/agent-config.schema.11.json +421 -0
  125. package/kits/schemas/daemon.schema.5.json +135 -0
  126. package/kits/schemas/daemon.schema.6.json +131 -0
  127. package/kits/schemas/defaults.schema.5.json +119 -0
  128. package/kits/schemas/migrations/README.md +3 -1
  129. package/kits/schemas/relation-config.schema.8.json +13 -0
  130. package/kits/schemas/role-config.schema.1.json +1 -2
  131. package/kits/schemas/single-session.schema.3.json +32 -0
  132. package/kits/templates/roles/admin.json +1 -0
  133. package/kits/templates/roles/member.json +1 -0
  134. package/kits/templates/roles/visitor.json +1 -0
  135. package/package.json +7 -3
  136. package/skills/eclink/SKILL.md +2 -0
@@ -33,6 +33,7 @@ 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 { resolveAgentLifecycle } from '../../config/lifecycle.js';
36
37
  import { authorizationConfigRevision, checkRoleAccess, getFirstStaticAgentOwner, listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, roleToSessionIdentity, } from '../../config/peer-role-resolver.js';
37
38
  import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../../stats/writer.js';
38
39
  import { normalizeUsage } from '../../stats/normalizer.js';
@@ -47,7 +48,17 @@ import { deriveSessionTitle, shouldAutoFillSessionTitle } from '../session/sessi
47
48
  import { SessionTurnCoordinator } from '../session/session-turn-coordinator.js';
48
49
  import { recordTriggerExecutionAnomaly } from '../../trigger/anomaly-store.js';
49
50
  import { classifyToolErrorCode } from '../permission/tool-error-code.js';
51
+ import { normalizeExecutionPermissionMode } from '../permission/mode.js';
50
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
+ }
51
62
  export class PauseController {
52
63
  state = 'running';
53
64
  waiters = new Set();
@@ -504,6 +515,7 @@ export class ResponseEngine {
504
515
  /** sessionId → 尚未收到 runner:task-notification 的子任务。 */
505
516
  activeRunnerTasks = new Map();
506
517
  triggerTerminalMessages = new WeakSet();
518
+ fullAccessTerminalMessages = new WeakSet();
507
519
  agentDelegationRegistry;
508
520
  turnCoordinator;
509
521
  /** 响应模式协调器(插件化机制中枢)。内置模式在构造时注册。 */
@@ -987,7 +999,8 @@ export class ResponseEngine {
987
999
  const active = this.activeRenderers.get(sessionId);
988
1000
  if (!active || active.suppressActivities)
989
1001
  return;
990
- void this.emitOperationalNotice(active.renderer, '\u23f3 会话压缩中...', 'info', 'compact-start');
1002
+ void this.emitOperationalNotice(active.renderer, '\u23f3 会话压缩中...', 'info', 'compact-start')
1003
+ .catch(error => logger.warn(`[ResponseEngine] compact-start notice send failed: ${error instanceof Error ? error.message : String(error)}`));
991
1004
  }
992
1005
  async emitOperationalNotice(renderer, text, severity, subtype) {
993
1006
  if (await renderer.sendOperationalNoticeAsText(text))
@@ -1005,10 +1018,28 @@ export class ResponseEngine {
1005
1018
  return true;
1006
1019
  }
1007
1020
  async retryAfterContextRecovery(prompt, opts) {
1008
- const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, } = opts;
1021
+ const { streamKey, renderer, agent, session, absoluteProjectPath, effectiveSystemPrompt, modelOverride, runtimeEnv, resetTimer, shouldSuppress, proactive, turnLease, beforeRunQuery, } = opts;
1009
1022
  if (!session.agentSessionId || !canCompactAgent(agent)) {
1010
1023
  throw new Error('CONTEXT_COMPACT_FAILED');
1011
1024
  }
1025
+ // A topic may have discovered and activated its first backend earlier in
1026
+ // this logical turn. The Session event is authoritative at that point,
1027
+ // while the original override can still describe the pre-run UNBOUND
1028
+ // snapshot. Compact and retry must address the accepted backend under the
1029
+ // same complete TurnLease or a runner could create a second backend.
1030
+ const recoveryModelOverride = session.threadId
1031
+ ? {
1032
+ ...(modelOverride || {}),
1033
+ backend: { kind: 'topic', agentSessionId: session.agentSessionId },
1034
+ turn: {
1035
+ sessionId: turnLease.sessionId,
1036
+ taskId: turnLease.taskId,
1037
+ turnId: turnLease.turnId,
1038
+ generation: turnLease.generation,
1039
+ inputId: turnLease.inputId,
1040
+ },
1041
+ }
1042
+ : modelOverride;
1012
1043
  // text event 在进入 renderer 前已经剔除了上下文超限错误行。保留缓冲中的
1013
1044
  // 有效正文,由 sendOperationalNoticeAsText 先 flush,避免恢复过程丢失已有模型输出。
1014
1045
  await this.emitOperationalNotice(renderer, '上下文过长,正在压缩会话...', 'warn', 'compact-trigger');
@@ -1016,15 +1047,17 @@ export class ResponseEngine {
1016
1047
  this.announcedCompactStarts.add(session.id);
1017
1048
  let compacted;
1018
1049
  try {
1019
- compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
1050
+ compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath, recoveryModelOverride);
1020
1051
  }
1021
1052
  finally {
1022
1053
  this.announcedCompactStarts.delete(session.id);
1023
1054
  }
1024
- if (compacted) {
1055
+ const compactResult = compacted;
1056
+ if (compactResult === true || compactResult?.ok === true) {
1025
1057
  await this.emitOperationalNotice(renderer, '✅ 压缩完成,继续处理...', 'info', 'compact-retry');
1026
1058
  await renderer.flush();
1027
- const retryStream = await agent.runQuery(session.id, prompt, absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
1059
+ beforeRunQuery?.();
1060
+ const retryStream = await agent.runQuery(session.id, prompt, absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, recoveryModelOverride, runtimeEnv);
1028
1061
  if (await this.cancelStaleRunnerStart(agent, session.id, streamKey, turnLease, turnLease.taskId)) {
1029
1062
  throw new Error('RUNNER_START_SUPERSEDED');
1030
1063
  }
@@ -1032,9 +1065,9 @@ export class ResponseEngine {
1032
1065
  return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
1033
1066
  undefined, turnLease, opts.permissionMode, opts.proactiveSelfAid);
1034
1067
  }
1035
- // Dropping the whole session hides the root cause and can replay side
1036
- // effects in unattended trigger tasks. Surface an actionable failure
1037
- // instead; callers may explicitly start a fresh task if that is desired.
1068
+ if (compactResult && typeof compactResult === 'object' && compactResult.message) {
1069
+ logger.warn(`[ResponseEngine] Context compact failed: ${compactResult.message}`);
1070
+ }
1038
1071
  throw new Error('CONTEXT_COMPACT_FAILED');
1039
1072
  }
1040
1073
  async recoverFromContextLimit(initialResult, opts) {
@@ -1074,6 +1107,61 @@ export class ResponseEngine {
1074
1107
  && message.source === 'trigger'
1075
1108
  && !!message.triggerMeta?.triggerId;
1076
1109
  }
1110
+ /**
1111
+ * A fullaccess Trigger may run with no current relation access for its
1112
+ * origin actor, but only after the daemon gate and persisted owner
1113
+ * provenance still validate. This is intentionally stricter than merely
1114
+ * checking the message's requested permission mode.
1115
+ */
1116
+ isCurrentFullAccessTrigger(message) {
1117
+ const trigger = message.triggerMeta;
1118
+ if (message.source !== 'trigger'
1119
+ || trigger?.permissionModeOverride !== 'fullaccess'
1120
+ || typeof trigger.authorizedBy !== 'string' || !trigger.authorizedBy
1121
+ || typeof trigger.triggerId !== 'string' || !trigger.triggerId
1122
+ || typeof trigger.runId !== 'string' || !trigger.runId
1123
+ || typeof trigger.attemptId !== 'string' || !trigger.attemptId) {
1124
+ return false;
1125
+ }
1126
+ try {
1127
+ return isFullAccessEnabled()
1128
+ && new Set(loadDaemonConfig().owners ?? []).has(trigger.authorizedBy);
1129
+ }
1130
+ catch {
1131
+ return false;
1132
+ }
1133
+ }
1134
+ /**
1135
+ * The authorization envelope is immutable for one logical run, but the
1136
+ * daemon gate, approving owner, and runner profile are live policy. Check
1137
+ * them immediately before every Runner invocation, including retries.
1138
+ */
1139
+ assertCurrentFullAccessExecutionAuthorization(authorization, agent) {
1140
+ if (authorization.permissionMode !== 'fullaccess'
1141
+ || authorization.processRole !== 'fullaccess-run'
1142
+ || authorization.dataScope !== 'daemon'
1143
+ || typeof authorization.authorizedBy !== 'string'
1144
+ || !authorization.authorizedBy) {
1145
+ throw new Error('fullaccess execution authorization is missing or malformed');
1146
+ }
1147
+ if (authorization.source === 'trigger'
1148
+ && (!authorization.triggerId || !authorization.runId || !authorization.attemptId)) {
1149
+ throw new Error('fullaccess Trigger authorization lacks scheduler identity or approval provenance');
1150
+ }
1151
+ if (authorization.source !== 'fullaccess-command' && authorization.source !== 'trigger') {
1152
+ throw new Error('fullaccess execution authorization has an unsupported source');
1153
+ }
1154
+ if (!isFullAccessEnabled()) {
1155
+ throw new Error('fullaccess execution is disabled by daemon.json');
1156
+ }
1157
+ const currentOwners = new Set(loadDaemonConfig().owners ?? []);
1158
+ if (!currentOwners.has(authorization.authorizedBy)) {
1159
+ throw new Error('fullaccess execution authorization is no longer owned');
1160
+ }
1161
+ if (agent.capabilities?.fullaccess !== true) {
1162
+ throw new Error(`baseagent ${agent.name} does not provide a complete fullaccess execution profile`);
1163
+ }
1164
+ }
1077
1165
  resolveTriggerExecutionIdentity(message, selfAid) {
1078
1166
  if (message.source !== 'trigger' || !message.triggerMeta?.triggerId)
1079
1167
  return undefined;
@@ -1241,7 +1329,7 @@ export class ResponseEngine {
1241
1329
  static COMMAND_PREFIXES = [
1242
1330
  '/new', '/pwd', '/help', '/status', '/restart',
1243
1331
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
1244
- '/stop', '/pause', '/resume', '/clear', '/compact', '/del', '/perm', '/file', '/check',
1332
+ '/stop', '/pause', '/resume', '/clear', '/compact', '/renew', '/del', '/perm', '/file', '/check',
1245
1333
  '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode',
1246
1334
  '/aid', '/upgrade', '/evolagent',
1247
1335
  ];
@@ -1254,6 +1342,39 @@ export class ResponseEngine {
1254
1342
  * 处理消息(主入口)
1255
1343
  */
1256
1344
  async processMessage(message) {
1345
+ const fallbackFullAccessAudit = this.resolveFullAccessAuditCandidate(message);
1346
+ let outerFailureReason;
1347
+ try {
1348
+ await this.processMessageWithMonitors(message);
1349
+ }
1350
+ catch (error) {
1351
+ outerFailureReason = error instanceof Error ? error.message : String(error);
1352
+ throw error;
1353
+ }
1354
+ finally {
1355
+ if (fallbackFullAccessAudit && !this.fullAccessTerminalMessages.has(message)) {
1356
+ auditFullAccessEvent({
1357
+ event: fallbackFullAccessAudit.authorization.source === 'trigger'
1358
+ ? 'fullaccess.trigger.execution.ended'
1359
+ : 'fullaccess.execution.ended',
1360
+ source: fallbackFullAccessAudit.authorization.source,
1361
+ actorId: fallbackFullAccessAudit.authorization.authorizedBy,
1362
+ processRole: fallbackFullAccessAudit.processRole,
1363
+ agentAid: message.selfAID,
1364
+ messageId: message.messageId,
1365
+ triggerId: fallbackFullAccessAudit.authorization.triggerId,
1366
+ runId: fallbackFullAccessAudit.authorization.runId,
1367
+ attemptId: fallbackFullAccessAudit.authorization.attemptId,
1368
+ result: 'blocked',
1369
+ reason: outerFailureReason ?? 'fullaccess execution stopped before permission setup',
1370
+ executed: false,
1371
+ executionState: 'blocked',
1372
+ });
1373
+ this.fullAccessTerminalMessages.add(message);
1374
+ }
1375
+ }
1376
+ }
1377
+ async processMessageWithMonitors(message) {
1257
1378
  const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
1258
1379
  const totalExecutionMs = this.totalExecutionLimitMs();
1259
1380
  if (message.handoffDelivery
@@ -1317,7 +1438,15 @@ export class ResponseEngine {
1317
1438
  // ── 角色访问控制检查:读取该用户角色的 allowAccess 配置,false 则拦截并回复权限不足 ──
1318
1439
  const userRole = session.identity?.role || 'none';
1319
1440
  const isInternalHandoff = message.source === 'handoff';
1320
- if (!isInternalHandoff && !checkRoleAccess(userRole, selfAidForAccess)) {
1441
+ const fullAccessAuditCandidate = this.resolveFullAccessAuditCandidate(message);
1442
+ const isCurrentFullAccessCommand = fullAccessAuditCandidate?.authorization.source === 'fullaccess-command'
1443
+ && fullAccessAuditCandidate.processRole === 'fullaccess-run';
1444
+ const isCurrentFullAccessTrigger = fullAccessAuditCandidate?.authorization.source === 'trigger'
1445
+ && fullAccessAuditCandidate.processRole === 'fullaccess-run';
1446
+ if (!isInternalHandoff
1447
+ && !isCurrentFullAccessCommand
1448
+ && !isCurrentFullAccessTrigger
1449
+ && !checkRoleAccess(userRole, selfAidForAccess)) {
1321
1450
  logger.warn(`[ResponseEngine] Access denied: role=${userRole} peerKey=${message.channelId} session=${session.id}`);
1322
1451
  const channelKey = session.metadata?.channelKey || message.channel;
1323
1452
  const channelInfo = this.resolveChannelInfo(channelKey);
@@ -1538,6 +1667,55 @@ export class ResponseEngine {
1538
1667
  this.activeMonitors.delete(streamKey);
1539
1668
  }
1540
1669
  }
1670
+ resolveFullAccessAuditCandidate(message) {
1671
+ const commandAuthorization = message.executionPermissionOverride;
1672
+ if (commandAuthorization?.permissionMode === 'fullaccess'
1673
+ && commandAuthorization.processRole === 'fullaccess-run'
1674
+ && commandAuthorization.dataScope === 'daemon'
1675
+ && commandAuthorization.source === 'fullaccess-command'
1676
+ && typeof commandAuthorization.authorizedBy === 'string'
1677
+ && commandAuthorization.authorizedBy.length > 0) {
1678
+ let currentOwner = false;
1679
+ try {
1680
+ currentOwner = isFullAccessEnabled()
1681
+ && new Set(loadDaemonConfig().owners ?? []).has(commandAuthorization.authorizedBy);
1682
+ }
1683
+ catch {
1684
+ // Runtime validation will fail closed; the audit must still retain the
1685
+ // attempted authorization without falsely labeling it active.
1686
+ }
1687
+ return {
1688
+ authorization: commandAuthorization,
1689
+ processRole: currentOwner ? 'fullaccess-run' : 'none',
1690
+ };
1691
+ }
1692
+ const trigger = message.triggerMeta;
1693
+ if (message.source === 'trigger'
1694
+ && trigger?.permissionModeOverride === 'fullaccess'
1695
+ && typeof trigger.authorizedBy === 'string' && trigger.authorizedBy.length > 0
1696
+ && typeof trigger.triggerId === 'string' && trigger.triggerId.length > 0
1697
+ && typeof trigger.runId === 'string' && trigger.runId.length > 0
1698
+ && typeof trigger.attemptId === 'string' && trigger.attemptId.length > 0) {
1699
+ const current = this.isCurrentFullAccessTrigger(message);
1700
+ return {
1701
+ authorization: {
1702
+ permissionMode: 'fullaccess',
1703
+ processRole: 'fullaccess-run',
1704
+ dataScope: 'daemon',
1705
+ source: 'trigger',
1706
+ authorizedBy: trigger.authorizedBy,
1707
+ triggerId: trigger.triggerId,
1708
+ runId: trigger.runId,
1709
+ attemptId: trigger.attemptId,
1710
+ },
1711
+ // Keep attempted authorization details for audit correlation, but do
1712
+ // not claim an active fullaccess identity when the process gate or
1713
+ // approval provenance has since become invalid.
1714
+ processRole: current ? 'fullaccess-run' : 'none',
1715
+ };
1716
+ }
1717
+ return undefined;
1718
+ }
1541
1719
  /** 获取回复上下文(跟着任务走) */
1542
1720
  getReplyContext(message) {
1543
1721
  return message.replyContext;
@@ -1550,10 +1728,10 @@ export class ResponseEngine {
1550
1728
  ? configuredSeconds * 1000
1551
1729
  : undefined;
1552
1730
  }
1553
- retryAttemptTimeoutMs() {
1731
+ apiRetryTimeoutMs() {
1554
1732
  if (this.globalSettings.idleMonitor?.enabled === false)
1555
1733
  return undefined;
1556
- const configuredSeconds = this.globalSettings.idleMonitor?.retryAttemptTimeout
1734
+ const configuredSeconds = this.globalSettings.idleMonitor?.apiRetryTimeout
1557
1735
  ?? this.globalSettings.idleMonitor?.timeout
1558
1736
  ?? 120;
1559
1737
  return typeof configuredSeconds === 'number'
@@ -1606,7 +1784,9 @@ export class ResponseEngine {
1606
1784
  const owningAgentForTask = this.agentRegistry?.resolveByChannel(channelKey)
1607
1785
  ?? (taskAgentAid ? this.agentRegistry?.get(taskAgentAid) : null);
1608
1786
  const runnerSelfAid = owningAgentForTask?.aid || session.selfAID || message.selfAID;
1609
- const lifecycle = owningAgentForTask?.config?.lifecycle;
1787
+ const lifecycle = owningAgentForTask
1788
+ ? resolveAgentLifecycle(owningAgentForTask.config)
1789
+ : undefined;
1610
1790
  const isActive = lifecycle === 'active';
1611
1791
  const isBootstrapping = lifecycle === 'bootstrapping';
1612
1792
  // Per-method agent name for stats bucketing (agent.name or '<unknown>')
@@ -1617,10 +1797,12 @@ export class ResponseEngine {
1617
1797
  return;
1618
1798
  }
1619
1799
  // Agent-owned turns are runnable only in the two explicit execution
1620
- // states. An untransitioned `created` agent or a malformed/missing
1621
- // lifecycle must not fall through to a base runner's default prompt.
1800
+ // states. An untransitioned `created` agent or malformed lifecycle must
1801
+ // not fall through to a base runner's default prompt. Missing lifecycle
1802
+ // is normalized to active for legacy configurations.
1622
1803
  if (owningAgentForTask && !isActive && !isBootstrapping) {
1623
- const blockedLifecycle = lifecycle ?? 'missing';
1804
+ const blockedLifecycle = lifecycle
1805
+ ?? `invalid (${String(owningAgentForTask.config.lifecycle)})`;
1624
1806
  logger.error(`[ResponseEngine] Agent lifecycle is not runnable: agent=${owningAgentForTask.aid} lifecycle=${blockedLifecycle}`);
1625
1807
  this.publishTriggerExecutionFailure(message, `agent_lifecycle_not_runnable:${blockedLifecycle}`);
1626
1808
  return;
@@ -1628,7 +1810,9 @@ export class ResponseEngine {
1628
1810
  // 二次拦截:如果命令消息绕过 MessageBridge 的 handleCommand 泄漏到这里,
1629
1811
  // 静默丢弃而不是发送给 Agent(命令已在 MessageBridge 层处理过)
1630
1812
  const rawContent = message.content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
1631
- if (rawContent.startsWith('/') && this.isKnownCommand(rawContent)) {
1813
+ if (!message.executionPermissionOverride
1814
+ && rawContent.startsWith('/')
1815
+ && this.isKnownCommand(rawContent)) {
1632
1816
  logger.warn(`[ResponseEngine] Command leaked past MessageBridge, dropped: "${rawContent.substring(0, 40)}"`);
1633
1817
  this.publishTriggerExecutionFailure(message, 'trigger_command_not_supported');
1634
1818
  return;
@@ -1865,7 +2049,7 @@ export class ResponseEngine {
1865
2049
  },
1866
2050
  modeConfig: resolvedMode.context.modeConfig,
1867
2051
  state: modeState,
1868
- isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, session.selfAID || message.selfAID),
2052
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid),
1869
2053
  logger,
1870
2054
  } : null;
1871
2055
  if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
@@ -1970,6 +2154,17 @@ export class ResponseEngine {
1970
2154
  let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
1971
2155
  let startTime = runnerStartedAt;
1972
2156
  let protocolReplayAttempts = 0;
2157
+ // These values span the outer task try/catch/finally so fullaccess audit
2158
+ // completion and context cleanup also cover setup failures.
2159
+ let fullAccessAuthorization;
2160
+ let fullAccessAuditStartedAt;
2161
+ let fullAccessAuditBaseagent;
2162
+ let fullAccessAuditModel;
2163
+ let fullAccessAuditProcessRole = 'none';
2164
+ let fullAccessRunnerInvoked = false;
2165
+ let fullAccessExecutionFailed = false;
2166
+ let fullAccessExecutionFailureReason;
2167
+ let activePermissionContext;
1973
2168
  try {
1974
2169
  const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
1975
2170
  // 记录收到消息
@@ -2057,7 +2252,8 @@ export class ResponseEngine {
2057
2252
  const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
2058
2253
  const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
2059
2254
  const e2eeInfo = message.replyContext?.metadata?.encrypted != null ? ` encrypt=${message.replyContext.metadata.encrypted}` : '';
2060
- logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}${e2eeInfo}`);
2255
+ const contentPreview = formatInboundMessageLogText(message.content);
2256
+ logger.info(`[${message.channel}] ${message.channelId}: ${contentPreview}${imageInfo}${modeInfo}${e2eeInfo}`);
2061
2257
  // 构建 peer 标识(优先 peerName,退化到 peerId / channelId)
2062
2258
  const peerName = session.metadata?.peerName ?? message.peerName;
2063
2259
  const peerId = session.metadata?.peerId ?? message.peerId ?? message.channelId;
@@ -2101,7 +2297,6 @@ export class ResponseEngine {
2101
2297
  suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
2102
2298
  operationalNoticesAsText: !isProactive && middleOutputMode !== 'none',
2103
2299
  fileMarkerPattern: options?.fileMarkerPattern,
2104
- diagEnabled: this.globalSettings.debug?.flusherDiag,
2105
2300
  send: async (payload) => {
2106
2301
  if (turnLease && !this.turnCoordinator.canPublish(turnLease)) {
2107
2302
  snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'suppressed-stale-turn' });
@@ -2140,7 +2335,7 @@ export class ResponseEngine {
2140
2335
  this.touchAgentActivity(channelKey);
2141
2336
  const enrichedEnvelope = withEnvelopeReplyContext(envelope, opts);
2142
2337
  snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'sent' });
2143
- await adapter.send(enrichedEnvelope, payload);
2338
+ return await adapter.send(enrichedEnvelope, payload);
2144
2339
  },
2145
2340
  });
2146
2341
  this.activeRenderers.set(session.id, {
@@ -2149,11 +2344,6 @@ export class ResponseEngine {
2149
2344
  suppressActivities: shouldSuppress(),
2150
2345
  });
2151
2346
  renderer.addLifecycle('started');
2152
- // 预压缩与任务过程中的自动压缩共用同一投影规则:interactive 的 text/all
2153
- // 均以普通非最终文本显示运行提示;none 不显示。其它 activity 在 all 下仍保持结构化。
2154
- // 它仍发生在真正的 prompt 执行之前。
2155
- await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer);
2156
- startTime = Date.now();
2157
2347
  if (isProactive) {
2158
2348
  logger.info(`[ResponseEngine] proactive mode: outputs via thought.put task=${taskId}`);
2159
2349
  }
@@ -2270,12 +2460,21 @@ export class ResponseEngine {
2270
2460
  };
2271
2461
  })();
2272
2462
  let effectivePermissionMode = 'readonly';
2463
+ // Allocate/register the session-owned root before any runner preflight
2464
+ // context is installed. Codex may already own a stronger directory;
2465
+ // other runners receive a private child of the process TMPDIR.
2466
+ const sessionRuntimeDir = agent.getManagedTempDir?.(session.id) ?? ensureSessionRuntimeDir(session.id);
2273
2467
  const recordExecutionAnomaly = triggerRunId
2274
2468
  ? (anomaly) => {
2275
2469
  recordTriggerExecutionAnomaly(triggerRunId, {
2276
2470
  ...anomaly,
2277
2471
  correlationId: anomaly.correlationId ?? anomaly.requestId,
2278
2472
  agentAid: anomaly.agentAid ?? session.selfAID ?? message.selfAID,
2473
+ agentName: anomaly.agentName
2474
+ ?? (agentNameForStats !== '<unknown>' ? agentNameForStats : undefined)
2475
+ ?? anomaly.agentAid
2476
+ ?? session.selfAID
2477
+ ?? message.selfAID,
2279
2478
  sessionId: anomaly.sessionId ?? session.id,
2280
2479
  permissionMode: anomaly.permissionMode ?? effectivePermissionMode,
2281
2480
  });
@@ -2286,7 +2485,7 @@ export class ResponseEngine {
2286
2485
  : undefined;
2287
2486
  const pureSessionPolicyHook = runModeConfig?.policyHook;
2288
2487
  // 设置权限审批的交互上下文(支持交互卡片)
2289
- agent.setPermissionContext?.(session.id, {
2488
+ const permissionContext = {
2290
2489
  sendPrompt: permissionPromptForOrigin,
2291
2490
  adapter: permissionAdapter,
2292
2491
  channelId: permissionChannelId,
@@ -2300,12 +2499,14 @@ export class ResponseEngine {
2300
2499
  chatmode: isProactive ? 'proactive' : 'interactive',
2301
2500
  role: peerRole,
2302
2501
  chatType: authChatType,
2303
- selfAid: session.selfAID || message.selfAID,
2502
+ selfAid,
2503
+ managedTempDir: sessionRuntimeDir,
2304
2504
  allowReadonlySourceDiagnostics: effectiveAgentConfig?.readonlySourceDiagnostics === true,
2305
2505
  peerKey: authPeerKey,
2306
2506
  causation: taskCausation,
2307
2507
  approvalRouting,
2308
2508
  approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
2509
+ permissionMode: effectivePermissionMode,
2309
2510
  recordExecutionAnomaly,
2310
2511
  pauseController: taskPauseController,
2311
2512
  preToolUsePolicyHook: pureSessionPolicyHook,
@@ -2318,6 +2519,7 @@ export class ResponseEngine {
2318
2519
  })
2319
2520
  : undefined,
2320
2521
  turn: {
2522
+ sessionId: turnLease.sessionId,
2321
2523
  taskId,
2322
2524
  turnId: turnLease.turnId,
2323
2525
  generation: turnLease.generation,
@@ -2419,7 +2621,9 @@ export class ResponseEngine {
2419
2621
  return undefined;
2420
2622
  };
2421
2623
  })(),
2422
- });
2624
+ };
2625
+ activePermissionContext = permissionContext;
2626
+ agent.setPermissionContext?.(session.id, permissionContext);
2423
2627
  // per-session 权限模式在 try 内、peerKey 解析后设置(见 resolvePermissionMode 调用)
2424
2628
  // 标记会话为处理中(实时持久化,重启后可恢复)
2425
2629
  this.sessionManager.markProcessing(session.id, taskId);
@@ -2469,21 +2673,68 @@ export class ResponseEngine {
2469
2673
  // 这样单条和积压合并批次不会因队列状态不同而切换关系级配置。
2470
2674
  const normalizedBaseagent = normalizeBaseagent(agent.name);
2471
2675
  // 设置 per-call 权限模式:只按当前角色定义解析(不读物理配置层或 session.metadata)。
2472
- // Trigger 只能降低本次调用权限,不能突破当前角色的权限上限。
2676
+ // 普通 Trigger override 只能降低权限;可信 /fa 或 scheduler fullaccess
2677
+ // 使用独立授权路径,不进入角色 authority ceiling。
2473
2678
  // 作为 per-call 入参随 modelOverride 传入 runQuery —— 与 model/effort 同构,
2474
2679
  // 不写 AgentRunner 实例字段,多对端/多会话并发共享同一 runner 实例时互不污染。
2475
2680
  const triggerPermissionModeOverride = message.triggerMeta?.permissionModeOverride;
2476
- try {
2477
- effectivePermissionMode = constrainRuntimePermissionMode({
2478
- selfAid: selfAid || undefined,
2479
- role: peerRole,
2480
- requestedValue: triggerPermissionModeOverride,
2481
- }).effectiveValue;
2681
+ const commandFullAccess = message.executionPermissionOverride;
2682
+ const triggerFullAccess = triggerPermissionModeOverride === 'fullaccess';
2683
+ if (commandFullAccess || triggerFullAccess) {
2684
+ if (commandFullAccess) {
2685
+ if (commandFullAccess.permissionMode !== 'fullaccess'
2686
+ || commandFullAccess.processRole !== 'fullaccess-run'
2687
+ || commandFullAccess.dataScope !== 'daemon'
2688
+ || commandFullAccess.source !== 'fullaccess-command'
2689
+ || !commandFullAccess.authorizedBy) {
2690
+ throw new Error('fullaccess command authorization is missing or malformed');
2691
+ }
2692
+ fullAccessAuthorization = commandFullAccess;
2693
+ }
2694
+ else {
2695
+ if (message.source !== 'trigger'
2696
+ || !message.triggerMeta?.triggerId
2697
+ || !message.triggerMeta.runId
2698
+ || !message.triggerMeta.attemptId
2699
+ || !message.triggerMeta.authorizedBy) {
2700
+ throw new Error('fullaccess Trigger override lacks trusted scheduler identity or approval provenance');
2701
+ }
2702
+ const currentOwners = new Set(loadDaemonConfig().owners ?? []);
2703
+ if (!currentOwners.has(message.triggerMeta.authorizedBy)) {
2704
+ throw new Error('fullaccess Trigger approval is no longer owned');
2705
+ }
2706
+ fullAccessAuthorization = {
2707
+ permissionMode: 'fullaccess',
2708
+ processRole: 'fullaccess-run',
2709
+ dataScope: 'daemon',
2710
+ source: 'trigger',
2711
+ triggerId: message.triggerMeta.triggerId,
2712
+ runId: message.triggerMeta.runId,
2713
+ attemptId: message.triggerMeta.attemptId,
2714
+ authorizedBy: message.triggerMeta.authorizedBy,
2715
+ };
2716
+ }
2717
+ this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
2718
+ effectivePermissionMode = 'fullaccess';
2482
2719
  }
2483
- catch (e) {
2484
- logger.warn(`[ResponseEngine] permission mode resolution failed, using fallback: ${e instanceof Error ? e.message : String(e)}`);
2485
- effectivePermissionMode = 'readonly';
2720
+ else {
2721
+ try {
2722
+ effectivePermissionMode = constrainRuntimePermissionMode({
2723
+ selfAid: selfAid || undefined,
2724
+ role: peerRole,
2725
+ requestedValue: triggerPermissionModeOverride,
2726
+ }).effectiveValue;
2727
+ }
2728
+ catch (e) {
2729
+ logger.warn(`[ResponseEngine] permission mode resolution failed, using fallback: ${e instanceof Error ? e.message : String(e)}`);
2730
+ effectivePermissionMode = 'readonly';
2731
+ }
2486
2732
  }
2733
+ // The mode is resolved after the initial context is assembled. Keep
2734
+ // the runner-held object live so preflight, approvals, and audit
2735
+ // records all observe the same normalized value.
2736
+ permissionContext.permissionMode = effectivePermissionMode;
2737
+ permissionContext.executionPermission = fullAccessAuthorization;
2487
2738
  // 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
2488
2739
  // 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
2489
2740
  // 多对端并发各自独立解析、各自传参,无共享状态可被污染。
@@ -2617,7 +2868,9 @@ export class ResponseEngine {
2617
2868
  }
2618
2869
  // permissionMode 随角色策略或 trigger override 传入;单 runner
2619
2870
  // 嵌入/测试路径没有 self/peer 作用域时,避免制造无配置来源的 override。
2620
- const shouldPassPermissionMode = !!message.triggerMeta?.permissionModeOverride || !!selfAid;
2871
+ const shouldPassPermissionMode = !!fullAccessAuthorization
2872
+ || !!message.triggerMeta?.permissionModeOverride
2873
+ || !!selfAid;
2621
2874
  if (shouldPassPermissionMode) {
2622
2875
  modelOverride = { ...(modelOverride || {}), permissionMode: effectivePermissionMode };
2623
2876
  }
@@ -2633,6 +2886,61 @@ export class ResponseEngine {
2633
2886
  sessionTitle: deriveSessionTitle(session.name, message.content, session.threadId),
2634
2887
  };
2635
2888
  }
2889
+ // Capture the authoritative topic binding after begin() has durably
2890
+ // published this logical run's TurnLease. A topic UNBOUND value is
2891
+ // explicit and must never fall back to runner-local cache. This must
2892
+ // happen before task-start auto compact because compact is a backend
2893
+ // management action just like the subsequent runQuery.
2894
+ let runAgentSessionId = session.agentSessionId;
2895
+ if (session.threadId
2896
+ && typeof this.sessionManager.getSessionById === 'function') {
2897
+ const latestBackendSession = await this.sessionManager.getSessionById(session.id);
2898
+ if (!latestBackendSession || latestBackendSession.threadId !== session.threadId) {
2899
+ throw new Error('topic session changed before backend run started');
2900
+ }
2901
+ runAgentSessionId = latestBackendSession.agentSessionId;
2902
+ session.agentSessionId = runAgentSessionId;
2903
+ session.metadata = latestBackendSession.metadata;
2904
+ }
2905
+ modelOverride = {
2906
+ ...(modelOverride || {}),
2907
+ ...(session.threadId
2908
+ ? { backend: { kind: 'topic', agentSessionId: runAgentSessionId ?? null } }
2909
+ : {}),
2910
+ turn: {
2911
+ sessionId: turnLease.sessionId,
2912
+ taskId,
2913
+ turnId: turnLease.turnId,
2914
+ generation: turnLease.generation,
2915
+ inputId: turnLease.inputId,
2916
+ },
2917
+ };
2918
+ const refreshTopicBindingForLogicalRetry = async () => {
2919
+ if (!session.threadId || typeof this.sessionManager.getSessionById !== 'function')
2920
+ return;
2921
+ const latest = await this.sessionManager.getSessionById(session.id);
2922
+ if (!latest || latest.threadId !== session.threadId) {
2923
+ throw new Error('topic session changed during backend run');
2924
+ }
2925
+ const latestAgentSessionId = latest.agentSessionId;
2926
+ session.agentSessionId = latestAgentSessionId;
2927
+ session.metadata = latest.metadata;
2928
+ if (latestAgentSessionId === runAgentSessionId)
2929
+ return;
2930
+ logger.info(`[ResponseEngine] Refreshing authoritative topic backend for logical retry: `
2931
+ + `session=${session.id} task=${taskId} backend=${latestAgentSessionId ?? 'none'}`);
2932
+ runAgentSessionId = latestAgentSessionId;
2933
+ modelOverride = {
2934
+ ...(modelOverride || {}),
2935
+ backend: { kind: 'topic', agentSessionId: runAgentSessionId ?? null },
2936
+ };
2937
+ };
2938
+ // 预压缩必须使用与本轮 runQuery 相同的有效模型/强度覆盖。
2939
+ // 模型解析完成后再执行,避免关系级模型存在时回落到 runner 默认模型。
2940
+ await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer, modelOverride, fullAccessAuthorization
2941
+ ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
2942
+ : undefined);
2943
+ startTime = Date.now();
2636
2944
  const causationPath = inputCausation.trigger?.path ?? [];
2637
2945
  const originNode = causationPath[0];
2638
2946
  logger.info(`[ResponseEngine] execution context session=${session.id}`
@@ -2884,7 +3192,6 @@ export class ResponseEngine {
2884
3192
  // private child of the process-provided TMPDIR. Both paths are passed
2885
3193
  // explicitly so sandboxed helper commands never fall back to a shared
2886
3194
  // agent or project directory for transient state.
2887
- const sessionRuntimeDir = agent.getManagedTempDir?.(session.id) ?? ensureSessionRuntimeDir(session.id);
2888
3195
  const taskRuntimeContext = {
2889
3196
  taskId,
2890
3197
  sessionId: session.id,
@@ -2898,6 +3205,12 @@ export class ResponseEngine {
2898
3205
  peerName: peerName || undefined,
2899
3206
  peerType: message.peerType || session.metadata?.peerType || undefined,
2900
3207
  peerRole,
3208
+ ...(fullAccessAuthorization ? {
3209
+ processRole: fullAccessAuthorization.processRole,
3210
+ dataScope: fullAccessAuthorization.dataScope,
3211
+ authorizedBy: fullAccessAuthorization.authorizedBy,
3212
+ executionSource: fullAccessAuthorization.source,
3213
+ } : {}),
2901
3214
  threadId: session.threadId || undefined,
2902
3215
  sessionRuntimeDir,
2903
3216
  runtimeLockDir: ensureRuntimeLockDir(sessionRuntimeDir),
@@ -2906,15 +3219,6 @@ export class ResponseEngine {
2906
3219
  };
2907
3220
  this.activeTaskRuntimeContexts.set(session.id, taskRuntimeContext);
2908
3221
  runtimeEnv = buildTaskRuntimeEnv(taskRuntimeContext);
2909
- modelOverride = {
2910
- ...(modelOverride || {}),
2911
- turn: {
2912
- taskId,
2913
- turnId: turnLease.turnId,
2914
- generation: turnLease.generation,
2915
- inputId: turnLease.inputId,
2916
- },
2917
- };
2918
3222
  if (this.agentDelegationRegistry && configActorId && selfAid && peerKey) {
2919
3223
  const delegationToken = this.agentDelegationRegistry.issue({
2920
3224
  sessionId: session.id,
@@ -2927,13 +3231,14 @@ export class ResponseEngine {
2927
3231
  selfAid,
2928
3232
  peerKey,
2929
3233
  issuedRole: peerRole,
3234
+ ...(fullAccessAuthorization ? { executionIdentity: fullAccessAuthorization } : {}),
2930
3235
  });
2931
3236
  runtimeEnv[AGENT_DELEGATION_TOKEN_ENV] = delegationToken;
2932
3237
  }
2933
3238
  const retryScheduler = new RetryScheduler(fallbackCandidates, modelOverride?.model || agentModel, model => (typeof agent.resolveModelId === 'function'
2934
3239
  ? (agent.resolveModelId(model) ?? model)
2935
3240
  : model));
2936
- const retryAttemptTimeoutMs = this.retryAttemptTimeoutMs();
3241
+ const apiRetryTimeoutMs = this.apiRetryTimeoutMs();
2937
3242
  const retryInputIsAtMostOnce = agent.retryInputSemantics === 'at_most_once';
2938
3243
  let runAttempt = 1;
2939
3244
  const recordRetryHealthError = async (retryError) => {
@@ -2951,11 +3256,45 @@ export class ResponseEngine {
2951
3256
  };
2952
3257
  while (true) {
2953
3258
  let streamRegistered = false;
2954
- const shouldTimeoutRetryAttempt = retryScheduler.shouldTimeoutAttempt() && retryAttemptTimeoutMs !== undefined;
3259
+ const shouldTimeoutRetryAttempt = retryScheduler.shouldTimeoutAttempt() && apiRetryTimeoutMs !== undefined;
2955
3260
  let attemptTimeout;
2956
3261
  try {
2957
- logger.info(`[ResponseEngine] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${runAttempt} apiRetries=${retryScheduler.apiRetryAttemptsMade} agentSessionId=${session.agentSessionId ?? 'none'}`);
2958
- const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
3262
+ 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'}`);
3263
+ if (fullAccessAuthorization) {
3264
+ this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
3265
+ if (fullAccessAuditStartedAt === undefined) {
3266
+ fullAccessAuditProcessRole = 'fullaccess-run';
3267
+ fullAccessAuditStartedAt = Date.now();
3268
+ fullAccessAuditBaseagent = normalizedBaseagent.canonical;
3269
+ fullAccessAuditModel = modelOverride?.model || agentModel;
3270
+ auditFullAccessEvent({
3271
+ event: fullAccessAuthorization.source === 'trigger'
3272
+ ? 'fullaccess.trigger.execution.started'
3273
+ : 'fullaccess.execution.started',
3274
+ source: fullAccessAuthorization.source,
3275
+ actorId: fullAccessAuthorization.authorizedBy,
3276
+ processRole: fullAccessAuditProcessRole,
3277
+ agentAid: selfAid,
3278
+ agentName: agentNameForStats,
3279
+ sessionId: session.id,
3280
+ taskId,
3281
+ messageId: message.messageId,
3282
+ triggerId: fullAccessAuthorization.triggerId,
3283
+ runId: fullAccessAuthorization.runId,
3284
+ attemptId: fullAccessAuthorization.attemptId,
3285
+ dataScope: fullAccessAuthorization.dataScope,
3286
+ authorizedBy: fullAccessAuthorization.authorizedBy,
3287
+ baseagent: fullAccessAuditBaseagent,
3288
+ model: fullAccessAuditModel,
3289
+ });
3290
+ // Audit is synchronous today, but it is an observable boundary.
3291
+ // Revalidate once more so an administrative revocation observed
3292
+ // there cannot be followed by a privileged Runner call.
3293
+ this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent);
3294
+ }
3295
+ fullAccessRunnerInvoked = true;
3296
+ }
3297
+ const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
2959
3298
  // The turn may be superseded while the runner is still creating its
2960
3299
  // backend query. Replay the interrupt after runQuery resolves, when
2961
3300
  // every runner is required to have an addressable cancellation handle.
@@ -2965,16 +3304,20 @@ export class ResponseEngine {
2965
3304
  agent.registerStream(streamKey, stream);
2966
3305
  streamRegistered = true;
2967
3306
  if (shouldTimeoutRetryAttempt) {
2968
- attemptTimeout = createRetryAttemptTimeout(retryAttemptTimeoutMs);
3307
+ attemptTimeout = createRetryAttemptTimeout(apiRetryTimeoutMs);
2969
3308
  resetTimer('retry_attempt');
2970
3309
  }
2971
3310
  const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
2972
3311
  resetTimer(eventType, toolName);
2973
3312
  attemptTimeout?.reset();
2974
- }, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, session.selfAID || message.selfAID);
3313
+ }, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid);
2975
3314
  streamResult = attemptTimeout
2976
3315
  ? await Promise.race([processAttempt, attemptTimeout.promise])
2977
3316
  : await processAttempt;
3317
+ // Activation can complete either before runQuery returns (Codex)
3318
+ // or while its event stream is consumed (Claude/Gemini/Ecagent).
3319
+ // Always reconcile from strict latest before any logical retry.
3320
+ await refreshTopicBindingForLogicalRetry();
2978
3321
  if (!streamResult.isError) {
2979
3322
  const protocolDecision = this.turnCoordinator.evaluateCommit(turnLease, this.buildTurnCommitEvidence(session, streamResult));
2980
3323
  const protocolReason = protocolDecision.ok ? undefined : protocolDecision.reason;
@@ -3010,6 +3353,7 @@ export class ResponseEngine {
3010
3353
  break; // 成功,跳出重试循环
3011
3354
  }
3012
3355
  catch (retryError) {
3356
+ await refreshTopicBindingForLogicalRetry();
3013
3357
  const retryAttemptTimedOut = retryError instanceof RetryAttemptTimeoutError;
3014
3358
  if (retryAttemptTimedOut) {
3015
3359
  logger.warn(`[ResponseEngine] Retry attempt ${runAttempt} timed out after ${retryError.timeoutMs}ms without agent events; interrupting current stream`);
@@ -3129,9 +3473,12 @@ export class ResponseEngine {
3129
3473
  resetTimer,
3130
3474
  shouldSuppress,
3131
3475
  proactive,
3132
- proactiveSelfAid: session.selfAID || message.selfAID,
3476
+ proactiveSelfAid: selfAid,
3133
3477
  permissionMode: effectivePermissionMode,
3134
3478
  turnLease,
3479
+ beforeRunQuery: fullAccessAuthorization
3480
+ ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
3481
+ : undefined,
3135
3482
  });
3136
3483
  }
3137
3484
  else {
@@ -3172,9 +3519,12 @@ export class ResponseEngine {
3172
3519
  resetTimer,
3173
3520
  shouldSuppress,
3174
3521
  proactive,
3175
- proactiveSelfAid: session.selfAID || message.selfAID,
3522
+ proactiveSelfAid: selfAid,
3176
3523
  permissionMode: effectivePermissionMode,
3177
3524
  turnLease,
3525
+ beforeRunQuery: fullAccessAuthorization
3526
+ ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
3527
+ : undefined,
3178
3528
  });
3179
3529
  // 重试后仍然 prompt_too_long:显示友好提示
3180
3530
  const retryStillTooLong = streamResult.isError && streamHitContextLimit(streamResult);
@@ -3752,6 +4102,8 @@ export class ResponseEngine {
3752
4102
  });
3753
4103
  }
3754
4104
  catch (error) {
4105
+ fullAccessExecutionFailed = true;
4106
+ fullAccessExecutionFailureReason = error instanceof Error ? error.message : String(error);
3755
4107
  const authoritativeTimeoutError = timeoutControl?.claim();
3756
4108
  if (authoritativeTimeoutError) {
3757
4109
  error = authoritativeTimeoutError;
@@ -3944,6 +4296,47 @@ export class ResponseEngine {
3944
4296
  }
3945
4297
  }
3946
4298
  finally {
4299
+ if (fullAccessAuthorization) {
4300
+ const startedAt = fullAccessAuditStartedAt;
4301
+ const executed = fullAccessRunnerInvoked;
4302
+ const interrupted = isExpectedTerminalInterrupt(streamResult.terminalReason);
4303
+ const failed = fullAccessExecutionFailed || streamResult.isError;
4304
+ auditFullAccessEvent({
4305
+ event: fullAccessAuthorization.source === 'trigger'
4306
+ ? 'fullaccess.trigger.execution.ended'
4307
+ : 'fullaccess.execution.ended',
4308
+ source: fullAccessAuthorization.source,
4309
+ actorId: fullAccessAuthorization.authorizedBy,
4310
+ processRole: fullAccessAuditProcessRole,
4311
+ agentAid: selfAid,
4312
+ agentName: agentNameForStats,
4313
+ sessionId: session.id,
4314
+ taskId,
4315
+ messageId: message.messageId,
4316
+ triggerId: fullAccessAuthorization.triggerId,
4317
+ runId: fullAccessAuthorization.runId,
4318
+ attemptId: fullAccessAuthorization.attemptId,
4319
+ dataScope: fullAccessAuthorization.dataScope,
4320
+ authorizedBy: fullAccessAuthorization.authorizedBy,
4321
+ baseagent: fullAccessAuditBaseagent,
4322
+ model: fullAccessAuditModel,
4323
+ result: !executed ? 'blocked' : failed ? 'failed' : interrupted ? 'interrupted' : 'completed',
4324
+ reason: fullAccessExecutionFailureReason
4325
+ ?? streamResult.errors?.join('; ')
4326
+ ?? (!executed ? 'fullaccess execution stopped before Runner invocation' : undefined),
4327
+ executed,
4328
+ executionState: !executed ? 'blocked' : failed ? 'failed' : 'completed',
4329
+ durationMs: executed && startedAt !== undefined ? Date.now() - startedAt : undefined,
4330
+ });
4331
+ this.fullAccessTerminalMessages.add(message);
4332
+ }
4333
+ // The authorization belongs to this logical run only. Clear the live
4334
+ // runner context as well as relying on the Message object to be dropped,
4335
+ // so late callbacks cannot observe a completed fullaccess grant.
4336
+ const completedPermissionContext = activePermissionContext;
4337
+ if (completedPermissionContext && completedPermissionContext.executionPermission === fullAccessAuthorization) {
4338
+ delete completedPermissionContext.executionPermission;
4339
+ }
3947
4340
  // 同一 session 的新任务可能已替换登记;旧任务结束时不得删掉新任务的 renderer。
3948
4341
  if (this.activeRenderers.get(session.id)?.taskId === taskId) {
3949
4342
  this.activeRenderers.delete(session.id);
@@ -3958,7 +4351,7 @@ export class ResponseEngine {
3958
4351
  snapshot.end(session.id, taskId);
3959
4352
  }
3960
4353
  }
3961
- async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer) {
4354
+ async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer, modelOverride, beforeCompact) {
3962
4355
  if (!session.agentSessionId || !canCompactAgent(agent)) {
3963
4356
  logger.debug(`[ResponseEngine] Auto compact skipped: session=${session.id} agentSessionId=${session.agentSessionId || 'none'} canCompact=${canCompactAgent(agent)} agent=${agent.name}`);
3964
4357
  return;
@@ -3973,13 +4366,20 @@ export class ResponseEngine {
3973
4366
  await renderer.flush();
3974
4367
  this.announcedCompactStarts.add(session.id);
3975
4368
  try {
3976
- const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
3977
- if (compacted) {
4369
+ // Auto compact is a privileged backend-management operation for a
4370
+ // fullaccess run. Revalidate immediately before invoking the runner so a
4371
+ // daemon gate/owner revocation observed after task setup cannot still
4372
+ // cause host-level runner activity.
4373
+ beforeCompact?.();
4374
+ const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath, modelOverride);
4375
+ const compactResult = compacted;
4376
+ if (compactResult === true || compactResult?.ok === true) {
3978
4377
  await this.emitOperationalNotice(renderer, '✅ 上下文压缩完成,继续处理...', 'info', 'auto-compact-complete');
3979
4378
  await renderer.flush();
3980
4379
  }
3981
4380
  else {
3982
- logger.warn(`[ResponseEngine] Auto compact at task.start returned false (session=${session.id})`);
4381
+ const compactFailure = compactResult;
4382
+ 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'}`);
3983
4383
  }
3984
4384
  }
3985
4385
  catch (err) {
@@ -4116,14 +4516,22 @@ export class ResponseEngine {
4116
4516
  const groupName = await adapter?.getGroupName?.(session.metadata.groupId).catch(() => undefined);
4117
4517
  if (groupName) {
4118
4518
  session.metadata.groupName = groupName;
4119
- await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
4519
+ await this.sessionManager.patchSessionMetadata?.(session.id, { groupName });
4520
+ if (typeof this.sessionManager.patchSessionMetadata !== 'function') {
4521
+ await this.sessionManager.updateSession(session.id, { metadata: { groupName } });
4522
+ }
4120
4523
  }
4121
4524
  }
4122
4525
  // 同步服务端 mention_mode,供同一群会话的上下文和菜单展示使用。
4123
4526
  if (message.chatType === 'group' && message.mentionMode && session.metadata?.mentionMode !== message.mentionMode) {
4124
4527
  logger.info(`[ResponseEngine] mentionMode sync: sessionId=${session.id} ${session.metadata?.mentionMode ?? 'none'} -> ${message.mentionMode}`);
4125
4528
  session.metadata = { ...(session.metadata || {}), mentionMode: message.mentionMode };
4126
- await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
4529
+ if (typeof this.sessionManager.patchSessionMetadata === 'function') {
4530
+ await this.sessionManager.patchSessionMetadata(session.id, { mentionMode: message.mentionMode });
4531
+ }
4532
+ else {
4533
+ await this.sessionManager.updateSession(session.id, { metadata: { mentionMode: message.mentionMode } });
4534
+ }
4127
4535
  }
4128
4536
  // chatMode 策略由 agent/relation behavior 配置在处理阶段解析;此处不再写 session 级参数。
4129
4537
  // replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
@@ -4161,6 +4569,11 @@ export class ResponseEngine {
4161
4569
  // Per-session agent name for stats bucketing
4162
4570
  const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
4163
4571
  const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
4572
+ const lifecycleAgentAid = proactiveSelfAid || session.selfAID;
4573
+ const lifecycleAgentName = agentNameForStats !== '<unknown>'
4574
+ ? agentNameForStats
4575
+ : lifecycleAgentAid || '<unknown>';
4576
+ const lifecyclePermissionMode = normalizeExecutionPermissionMode(permissionMode);
4164
4577
  let hasReceivedText = false;
4165
4578
  let hasProjectedCurrentReplyText = false;
4166
4579
  let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
@@ -4549,8 +4962,9 @@ export class ResponseEngine {
4549
4962
  input: event.input,
4550
4963
  ...(event.callId ? { callId: event.callId } : {}),
4551
4964
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4552
- agentAid: session.selfAID ?? 'unknown',
4553
- permissionMode: permissionMode ?? 'unknown',
4965
+ agentName: lifecycleAgentName,
4966
+ agentAid: lifecycleAgentAid ?? 'unknown',
4967
+ permissionMode: lifecyclePermissionMode,
4554
4968
  decision: 'pending',
4555
4969
  decisionSource: 'runner',
4556
4970
  executed: false,
@@ -4626,11 +5040,11 @@ export class ResponseEngine {
4626
5040
  toolName: event.name,
4627
5041
  isError: event.isError,
4628
5042
  ...(event.isError ? { errorCode: classifyToolErrorCode({ errorCode: event.errorCode, error: event.error, result: event.result }) } : {}),
4629
- agentName: agentNameForStats,
4630
5043
  ...(event.callId ? { callId: event.callId } : {}),
4631
5044
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4632
- agentAid: session.selfAID ?? 'unknown',
4633
- permissionMode: permissionMode ?? 'unknown',
5045
+ agentName: lifecycleAgentName,
5046
+ agentAid: lifecycleAgentAid ?? 'unknown',
5047
+ permissionMode: lifecyclePermissionMode,
4634
5048
  decision: event.isError ? 'error' : 'allow',
4635
5049
  decisionSource: 'runner',
4636
5050
  executed: true,
@@ -4788,7 +5202,12 @@ export class ResponseEngine {
4788
5202
  lastReplyText,
4789
5203
  updateSessionMeta: async (patch) => {
4790
5204
  session.metadata = { ...(session.metadata || {}), ...patch };
4791
- await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
5205
+ if (typeof this.sessionManager.patchSessionMetadata === 'function') {
5206
+ await this.sessionManager.patchSessionMetadata(session.id, patch);
5207
+ }
5208
+ else {
5209
+ await this.sessionManager.updateSession(session.id, { metadata: patch });
5210
+ }
4792
5211
  },
4793
5212
  logger,
4794
5213
  });
@@ -4860,11 +5279,15 @@ export class ResponseEngine {
4860
5279
  // and mark the error so outer catch won't send a duplicate message
4861
5280
  const hasErrorSuppressingContent = hasErrorResult || renderer.hasNonLifecycleContent();
4862
5281
  if (hasErrorSuppressingContent) {
5282
+ let errorOutputFlushed = false;
4863
5283
  try {
4864
5284
  await renderer.flush(true);
5285
+ errorOutputFlushed = true;
4865
5286
  }
4866
- catch { }
4867
- if (error instanceof Error) {
5287
+ catch (flushError) {
5288
+ logger.warn(`[ResponseEngine] Failed to flush error output: ${flushError instanceof Error ? flushError.message : String(flushError)}`);
5289
+ }
5290
+ if (errorOutputFlushed && error instanceof Error) {
4868
5291
  error._errorAlreadySent = true;
4869
5292
  }
4870
5293
  }