evolcore 0.0.20 → 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 (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -48,7 +48,17 @@ import { deriveSessionTitle, shouldAutoFillSessionTitle } from '../session/sessi
48
48
  import { SessionTurnCoordinator } from '../session/session-turn-coordinator.js';
49
49
  import { recordTriggerExecutionAnomaly } from '../../trigger/anomaly-store.js';
50
50
  import { classifyToolErrorCode } 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
  /** 响应模式协调器(插件化机制中枢)。内置模式在构造时注册。 */
@@ -988,7 +999,8 @@ export class ResponseEngine {
988
999
  const active = this.activeRenderers.get(sessionId);
989
1000
  if (!active || active.suppressActivities)
990
1001
  return;
991
- 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)}`));
992
1004
  }
993
1005
  async emitOperationalNotice(renderer, text, severity, subtype) {
994
1006
  if (await renderer.sendOperationalNoticeAsText(text))
@@ -1006,10 +1018,28 @@ export class ResponseEngine {
1006
1018
  return true;
1007
1019
  }
1008
1020
  async retryAfterContextRecovery(prompt, opts) {
1009
- 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;
1010
1022
  if (!session.agentSessionId || !canCompactAgent(agent)) {
1011
1023
  throw new Error('CONTEXT_COMPACT_FAILED');
1012
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;
1013
1043
  // text event 在进入 renderer 前已经剔除了上下文超限错误行。保留缓冲中的
1014
1044
  // 有效正文,由 sendOperationalNoticeAsText 先 flush,避免恢复过程丢失已有模型输出。
1015
1045
  await this.emitOperationalNotice(renderer, '上下文过长,正在压缩会话...', 'warn', 'compact-trigger');
@@ -1017,15 +1047,17 @@ export class ResponseEngine {
1017
1047
  this.announcedCompactStarts.add(session.id);
1018
1048
  let compacted;
1019
1049
  try {
1020
- compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
1050
+ compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath, recoveryModelOverride);
1021
1051
  }
1022
1052
  finally {
1023
1053
  this.announcedCompactStarts.delete(session.id);
1024
1054
  }
1025
- if (compacted) {
1055
+ const compactResult = compacted;
1056
+ if (compactResult === true || compactResult?.ok === true) {
1026
1057
  await this.emitOperationalNotice(renderer, '✅ 压缩完成,继续处理...', 'info', 'compact-retry');
1027
1058
  await renderer.flush();
1028
- 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);
1029
1061
  if (await this.cancelStaleRunnerStart(agent, session.id, streamKey, turnLease, turnLease.taskId)) {
1030
1062
  throw new Error('RUNNER_START_SUPERSEDED');
1031
1063
  }
@@ -1033,9 +1065,9 @@ export class ResponseEngine {
1033
1065
  return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
1034
1066
  undefined, turnLease, opts.permissionMode, opts.proactiveSelfAid);
1035
1067
  }
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.
1068
+ if (compactResult && typeof compactResult === 'object' && compactResult.message) {
1069
+ logger.warn(`[ResponseEngine] Context compact failed: ${compactResult.message}`);
1070
+ }
1039
1071
  throw new Error('CONTEXT_COMPACT_FAILED');
1040
1072
  }
1041
1073
  async recoverFromContextLimit(initialResult, opts) {
@@ -1075,6 +1107,61 @@ export class ResponseEngine {
1075
1107
  && message.source === 'trigger'
1076
1108
  && !!message.triggerMeta?.triggerId;
1077
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
+ }
1078
1165
  resolveTriggerExecutionIdentity(message, selfAid) {
1079
1166
  if (message.source !== 'trigger' || !message.triggerMeta?.triggerId)
1080
1167
  return undefined;
@@ -1242,7 +1329,7 @@ export class ResponseEngine {
1242
1329
  static COMMAND_PREFIXES = [
1243
1330
  '/new', '/pwd', '/help', '/status', '/restart',
1244
1331
  '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
1245
- '/stop', '/pause', '/resume', '/clear', '/compact', '/del', '/perm', '/file', '/check',
1332
+ '/stop', '/pause', '/resume', '/clear', '/compact', '/renew', '/del', '/perm', '/file', '/check',
1246
1333
  '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode',
1247
1334
  '/aid', '/upgrade', '/evolagent',
1248
1335
  ];
@@ -1255,6 +1342,39 @@ export class ResponseEngine {
1255
1342
  * 处理消息(主入口)
1256
1343
  */
1257
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) {
1258
1378
  const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
1259
1379
  const totalExecutionMs = this.totalExecutionLimitMs();
1260
1380
  if (message.handoffDelivery
@@ -1318,7 +1438,15 @@ export class ResponseEngine {
1318
1438
  // ── 角色访问控制检查:读取该用户角色的 allowAccess 配置,false 则拦截并回复权限不足 ──
1319
1439
  const userRole = session.identity?.role || 'none';
1320
1440
  const isInternalHandoff = message.source === 'handoff';
1321
- 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)) {
1322
1450
  logger.warn(`[ResponseEngine] Access denied: role=${userRole} peerKey=${message.channelId} session=${session.id}`);
1323
1451
  const channelKey = session.metadata?.channelKey || message.channel;
1324
1452
  const channelInfo = this.resolveChannelInfo(channelKey);
@@ -1539,6 +1667,55 @@ export class ResponseEngine {
1539
1667
  this.activeMonitors.delete(streamKey);
1540
1668
  }
1541
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
+ }
1542
1719
  /** 获取回复上下文(跟着任务走) */
1543
1720
  getReplyContext(message) {
1544
1721
  return message.replyContext;
@@ -1551,10 +1728,10 @@ export class ResponseEngine {
1551
1728
  ? configuredSeconds * 1000
1552
1729
  : undefined;
1553
1730
  }
1554
- retryAttemptTimeoutMs() {
1731
+ apiRetryTimeoutMs() {
1555
1732
  if (this.globalSettings.idleMonitor?.enabled === false)
1556
1733
  return undefined;
1557
- const configuredSeconds = this.globalSettings.idleMonitor?.retryAttemptTimeout
1734
+ const configuredSeconds = this.globalSettings.idleMonitor?.apiRetryTimeout
1558
1735
  ?? this.globalSettings.idleMonitor?.timeout
1559
1736
  ?? 120;
1560
1737
  return typeof configuredSeconds === 'number'
@@ -1633,7 +1810,9 @@ export class ResponseEngine {
1633
1810
  // 二次拦截:如果命令消息绕过 MessageBridge 的 handleCommand 泄漏到这里,
1634
1811
  // 静默丢弃而不是发送给 Agent(命令已在 MessageBridge 层处理过)
1635
1812
  const rawContent = message.content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
1636
- if (rawContent.startsWith('/') && this.isKnownCommand(rawContent)) {
1813
+ if (!message.executionPermissionOverride
1814
+ && rawContent.startsWith('/')
1815
+ && this.isKnownCommand(rawContent)) {
1637
1816
  logger.warn(`[ResponseEngine] Command leaked past MessageBridge, dropped: "${rawContent.substring(0, 40)}"`);
1638
1817
  this.publishTriggerExecutionFailure(message, 'trigger_command_not_supported');
1639
1818
  return;
@@ -1870,7 +2049,7 @@ export class ResponseEngine {
1870
2049
  },
1871
2050
  modeConfig: resolvedMode.context.modeConfig,
1872
2051
  state: modeState,
1873
- isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, session.selfAID || message.selfAID),
2052
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid),
1874
2053
  logger,
1875
2054
  } : null;
1876
2055
  if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
@@ -1975,6 +2154,17 @@ export class ResponseEngine {
1975
2154
  let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
1976
2155
  let startTime = runnerStartedAt;
1977
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;
1978
2168
  try {
1979
2169
  const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
1980
2170
  // 记录收到消息
@@ -2062,7 +2252,8 @@ export class ResponseEngine {
2062
2252
  const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
2063
2253
  const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
2064
2254
  const e2eeInfo = message.replyContext?.metadata?.encrypted != null ? ` encrypt=${message.replyContext.metadata.encrypted}` : '';
2065
- 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}`);
2066
2257
  // 构建 peer 标识(优先 peerName,退化到 peerId / channelId)
2067
2258
  const peerName = session.metadata?.peerName ?? message.peerName;
2068
2259
  const peerId = session.metadata?.peerId ?? message.peerId ?? message.channelId;
@@ -2106,7 +2297,6 @@ export class ResponseEngine {
2106
2297
  suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
2107
2298
  operationalNoticesAsText: !isProactive && middleOutputMode !== 'none',
2108
2299
  fileMarkerPattern: options?.fileMarkerPattern,
2109
- diagEnabled: this.globalSettings.debug?.flusherDiag,
2110
2300
  send: async (payload) => {
2111
2301
  if (turnLease && !this.turnCoordinator.canPublish(turnLease)) {
2112
2302
  snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'suppressed-stale-turn' });
@@ -2145,7 +2335,7 @@ export class ResponseEngine {
2145
2335
  this.touchAgentActivity(channelKey);
2146
2336
  const enrichedEnvelope = withEnvelopeReplyContext(envelope, opts);
2147
2337
  snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'sent' });
2148
- await adapter.send(enrichedEnvelope, payload);
2338
+ return await adapter.send(enrichedEnvelope, payload);
2149
2339
  },
2150
2340
  });
2151
2341
  this.activeRenderers.set(session.id, {
@@ -2154,11 +2344,6 @@ export class ResponseEngine {
2154
2344
  suppressActivities: shouldSuppress(),
2155
2345
  });
2156
2346
  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
2347
  if (isProactive) {
2163
2348
  logger.info(`[ResponseEngine] proactive mode: outputs via thought.put task=${taskId}`);
2164
2349
  }
@@ -2275,12 +2460,21 @@ export class ResponseEngine {
2275
2460
  };
2276
2461
  })();
2277
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);
2278
2467
  const recordExecutionAnomaly = triggerRunId
2279
2468
  ? (anomaly) => {
2280
2469
  recordTriggerExecutionAnomaly(triggerRunId, {
2281
2470
  ...anomaly,
2282
2471
  correlationId: anomaly.correlationId ?? anomaly.requestId,
2283
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,
2284
2478
  sessionId: anomaly.sessionId ?? session.id,
2285
2479
  permissionMode: anomaly.permissionMode ?? effectivePermissionMode,
2286
2480
  });
@@ -2291,7 +2485,7 @@ export class ResponseEngine {
2291
2485
  : undefined;
2292
2486
  const pureSessionPolicyHook = runModeConfig?.policyHook;
2293
2487
  // 设置权限审批的交互上下文(支持交互卡片)
2294
- agent.setPermissionContext?.(session.id, {
2488
+ const permissionContext = {
2295
2489
  sendPrompt: permissionPromptForOrigin,
2296
2490
  adapter: permissionAdapter,
2297
2491
  channelId: permissionChannelId,
@@ -2305,12 +2499,14 @@ export class ResponseEngine {
2305
2499
  chatmode: isProactive ? 'proactive' : 'interactive',
2306
2500
  role: peerRole,
2307
2501
  chatType: authChatType,
2308
- selfAid: session.selfAID || message.selfAID,
2502
+ selfAid,
2503
+ managedTempDir: sessionRuntimeDir,
2309
2504
  allowReadonlySourceDiagnostics: effectiveAgentConfig?.readonlySourceDiagnostics === true,
2310
2505
  peerKey: authPeerKey,
2311
2506
  causation: taskCausation,
2312
2507
  approvalRouting,
2313
2508
  approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
2509
+ permissionMode: effectivePermissionMode,
2314
2510
  recordExecutionAnomaly,
2315
2511
  pauseController: taskPauseController,
2316
2512
  preToolUsePolicyHook: pureSessionPolicyHook,
@@ -2323,6 +2519,7 @@ export class ResponseEngine {
2323
2519
  })
2324
2520
  : undefined,
2325
2521
  turn: {
2522
+ sessionId: turnLease.sessionId,
2326
2523
  taskId,
2327
2524
  turnId: turnLease.turnId,
2328
2525
  generation: turnLease.generation,
@@ -2424,7 +2621,9 @@ export class ResponseEngine {
2424
2621
  return undefined;
2425
2622
  };
2426
2623
  })(),
2427
- });
2624
+ };
2625
+ activePermissionContext = permissionContext;
2626
+ agent.setPermissionContext?.(session.id, permissionContext);
2428
2627
  // per-session 权限模式在 try 内、peerKey 解析后设置(见 resolvePermissionMode 调用)
2429
2628
  // 标记会话为处理中(实时持久化,重启后可恢复)
2430
2629
  this.sessionManager.markProcessing(session.id, taskId);
@@ -2474,21 +2673,68 @@ export class ResponseEngine {
2474
2673
  // 这样单条和积压合并批次不会因队列状态不同而切换关系级配置。
2475
2674
  const normalizedBaseagent = normalizeBaseagent(agent.name);
2476
2675
  // 设置 per-call 权限模式:只按当前角色定义解析(不读物理配置层或 session.metadata)。
2477
- // Trigger 只能降低本次调用权限,不能突破当前角色的权限上限。
2676
+ // 普通 Trigger override 只能降低权限;可信 /fa 或 scheduler fullaccess
2677
+ // 使用独立授权路径,不进入角色 authority ceiling。
2478
2678
  // 作为 per-call 入参随 modelOverride 传入 runQuery —— 与 model/effort 同构,
2479
2679
  // 不写 AgentRunner 实例字段,多对端/多会话并发共享同一 runner 实例时互不污染。
2480
2680
  const triggerPermissionModeOverride = message.triggerMeta?.permissionModeOverride;
2481
- try {
2482
- effectivePermissionMode = constrainRuntimePermissionMode({
2483
- selfAid: selfAid || undefined,
2484
- role: peerRole,
2485
- requestedValue: triggerPermissionModeOverride,
2486
- }).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';
2487
2719
  }
2488
- catch (e) {
2489
- logger.warn(`[ResponseEngine] permission mode resolution failed, using fallback: ${e instanceof Error ? e.message : String(e)}`);
2490
- 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
+ }
2491
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;
2492
2738
  // 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
2493
2739
  // 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
2494
2740
  // 多对端并发各自独立解析、各自传参,无共享状态可被污染。
@@ -2622,7 +2868,9 @@ export class ResponseEngine {
2622
2868
  }
2623
2869
  // permissionMode 随角色策略或 trigger override 传入;单 runner
2624
2870
  // 嵌入/测试路径没有 self/peer 作用域时,避免制造无配置来源的 override。
2625
- const shouldPassPermissionMode = !!message.triggerMeta?.permissionModeOverride || !!selfAid;
2871
+ const shouldPassPermissionMode = !!fullAccessAuthorization
2872
+ || !!message.triggerMeta?.permissionModeOverride
2873
+ || !!selfAid;
2626
2874
  if (shouldPassPermissionMode) {
2627
2875
  modelOverride = { ...(modelOverride || {}), permissionMode: effectivePermissionMode };
2628
2876
  }
@@ -2638,6 +2886,61 @@ export class ResponseEngine {
2638
2886
  sessionTitle: deriveSessionTitle(session.name, message.content, session.threadId),
2639
2887
  };
2640
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();
2641
2944
  const causationPath = inputCausation.trigger?.path ?? [];
2642
2945
  const originNode = causationPath[0];
2643
2946
  logger.info(`[ResponseEngine] execution context session=${session.id}`
@@ -2889,7 +3192,6 @@ export class ResponseEngine {
2889
3192
  // private child of the process-provided TMPDIR. Both paths are passed
2890
3193
  // explicitly so sandboxed helper commands never fall back to a shared
2891
3194
  // agent or project directory for transient state.
2892
- const sessionRuntimeDir = agent.getManagedTempDir?.(session.id) ?? ensureSessionRuntimeDir(session.id);
2893
3195
  const taskRuntimeContext = {
2894
3196
  taskId,
2895
3197
  sessionId: session.id,
@@ -2903,6 +3205,12 @@ export class ResponseEngine {
2903
3205
  peerName: peerName || undefined,
2904
3206
  peerType: message.peerType || session.metadata?.peerType || undefined,
2905
3207
  peerRole,
3208
+ ...(fullAccessAuthorization ? {
3209
+ processRole: fullAccessAuthorization.processRole,
3210
+ dataScope: fullAccessAuthorization.dataScope,
3211
+ authorizedBy: fullAccessAuthorization.authorizedBy,
3212
+ executionSource: fullAccessAuthorization.source,
3213
+ } : {}),
2906
3214
  threadId: session.threadId || undefined,
2907
3215
  sessionRuntimeDir,
2908
3216
  runtimeLockDir: ensureRuntimeLockDir(sessionRuntimeDir),
@@ -2911,15 +3219,6 @@ export class ResponseEngine {
2911
3219
  };
2912
3220
  this.activeTaskRuntimeContexts.set(session.id, taskRuntimeContext);
2913
3221
  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
3222
  if (this.agentDelegationRegistry && configActorId && selfAid && peerKey) {
2924
3223
  const delegationToken = this.agentDelegationRegistry.issue({
2925
3224
  sessionId: session.id,
@@ -2932,13 +3231,14 @@ export class ResponseEngine {
2932
3231
  selfAid,
2933
3232
  peerKey,
2934
3233
  issuedRole: peerRole,
3234
+ ...(fullAccessAuthorization ? { executionIdentity: fullAccessAuthorization } : {}),
2935
3235
  });
2936
3236
  runtimeEnv[AGENT_DELEGATION_TOKEN_ENV] = delegationToken;
2937
3237
  }
2938
3238
  const retryScheduler = new RetryScheduler(fallbackCandidates, modelOverride?.model || agentModel, model => (typeof agent.resolveModelId === 'function'
2939
3239
  ? (agent.resolveModelId(model) ?? model)
2940
3240
  : model));
2941
- const retryAttemptTimeoutMs = this.retryAttemptTimeoutMs();
3241
+ const apiRetryTimeoutMs = this.apiRetryTimeoutMs();
2942
3242
  const retryInputIsAtMostOnce = agent.retryInputSemantics === 'at_most_once';
2943
3243
  let runAttempt = 1;
2944
3244
  const recordRetryHealthError = async (retryError) => {
@@ -2956,11 +3256,45 @@ export class ResponseEngine {
2956
3256
  };
2957
3257
  while (true) {
2958
3258
  let streamRegistered = false;
2959
- const shouldTimeoutRetryAttempt = retryScheduler.shouldTimeoutAttempt() && retryAttemptTimeoutMs !== undefined;
3259
+ const shouldTimeoutRetryAttempt = retryScheduler.shouldTimeoutAttempt() && apiRetryTimeoutMs !== undefined;
2960
3260
  let attemptTimeout;
2961
3261
  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);
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);
2964
3298
  // The turn may be superseded while the runner is still creating its
2965
3299
  // backend query. Replay the interrupt after runQuery resolves, when
2966
3300
  // every runner is required to have an addressable cancellation handle.
@@ -2970,16 +3304,20 @@ export class ResponseEngine {
2970
3304
  agent.registerStream(streamKey, stream);
2971
3305
  streamRegistered = true;
2972
3306
  if (shouldTimeoutRetryAttempt) {
2973
- attemptTimeout = createRetryAttemptTimeout(retryAttemptTimeoutMs);
3307
+ attemptTimeout = createRetryAttemptTimeout(apiRetryTimeoutMs);
2974
3308
  resetTimer('retry_attempt');
2975
3309
  }
2976
3310
  const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
2977
3311
  resetTimer(eventType, toolName);
2978
3312
  attemptTimeout?.reset();
2979
- }, 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);
2980
3314
  streamResult = attemptTimeout
2981
3315
  ? await Promise.race([processAttempt, attemptTimeout.promise])
2982
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();
2983
3321
  if (!streamResult.isError) {
2984
3322
  const protocolDecision = this.turnCoordinator.evaluateCommit(turnLease, this.buildTurnCommitEvidence(session, streamResult));
2985
3323
  const protocolReason = protocolDecision.ok ? undefined : protocolDecision.reason;
@@ -3015,6 +3353,7 @@ export class ResponseEngine {
3015
3353
  break; // 成功,跳出重试循环
3016
3354
  }
3017
3355
  catch (retryError) {
3356
+ await refreshTopicBindingForLogicalRetry();
3018
3357
  const retryAttemptTimedOut = retryError instanceof RetryAttemptTimeoutError;
3019
3358
  if (retryAttemptTimedOut) {
3020
3359
  logger.warn(`[ResponseEngine] Retry attempt ${runAttempt} timed out after ${retryError.timeoutMs}ms without agent events; interrupting current stream`);
@@ -3134,9 +3473,12 @@ export class ResponseEngine {
3134
3473
  resetTimer,
3135
3474
  shouldSuppress,
3136
3475
  proactive,
3137
- proactiveSelfAid: session.selfAID || message.selfAID,
3476
+ proactiveSelfAid: selfAid,
3138
3477
  permissionMode: effectivePermissionMode,
3139
3478
  turnLease,
3479
+ beforeRunQuery: fullAccessAuthorization
3480
+ ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
3481
+ : undefined,
3140
3482
  });
3141
3483
  }
3142
3484
  else {
@@ -3177,9 +3519,12 @@ export class ResponseEngine {
3177
3519
  resetTimer,
3178
3520
  shouldSuppress,
3179
3521
  proactive,
3180
- proactiveSelfAid: session.selfAID || message.selfAID,
3522
+ proactiveSelfAid: selfAid,
3181
3523
  permissionMode: effectivePermissionMode,
3182
3524
  turnLease,
3525
+ beforeRunQuery: fullAccessAuthorization
3526
+ ? () => this.assertCurrentFullAccessExecutionAuthorization(fullAccessAuthorization, agent)
3527
+ : undefined,
3183
3528
  });
3184
3529
  // 重试后仍然 prompt_too_long:显示友好提示
3185
3530
  const retryStillTooLong = streamResult.isError && streamHitContextLimit(streamResult);
@@ -3757,6 +4102,8 @@ export class ResponseEngine {
3757
4102
  });
3758
4103
  }
3759
4104
  catch (error) {
4105
+ fullAccessExecutionFailed = true;
4106
+ fullAccessExecutionFailureReason = error instanceof Error ? error.message : String(error);
3760
4107
  const authoritativeTimeoutError = timeoutControl?.claim();
3761
4108
  if (authoritativeTimeoutError) {
3762
4109
  error = authoritativeTimeoutError;
@@ -3949,6 +4296,47 @@ export class ResponseEngine {
3949
4296
  }
3950
4297
  }
3951
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
+ }
3952
4340
  // 同一 session 的新任务可能已替换登记;旧任务结束时不得删掉新任务的 renderer。
3953
4341
  if (this.activeRenderers.get(session.id)?.taskId === taskId) {
3954
4342
  this.activeRenderers.delete(session.id);
@@ -3963,7 +4351,7 @@ export class ResponseEngine {
3963
4351
  snapshot.end(session.id, taskId);
3964
4352
  }
3965
4353
  }
3966
- async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer) {
4354
+ async runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, renderer, modelOverride, beforeCompact) {
3967
4355
  if (!session.agentSessionId || !canCompactAgent(agent)) {
3968
4356
  logger.debug(`[ResponseEngine] Auto compact skipped: session=${session.id} agentSessionId=${session.agentSessionId || 'none'} canCompact=${canCompactAgent(agent)} agent=${agent.name}`);
3969
4357
  return;
@@ -3978,13 +4366,20 @@ export class ResponseEngine {
3978
4366
  await renderer.flush();
3979
4367
  this.announcedCompactStarts.add(session.id);
3980
4368
  try {
3981
- const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
3982
- 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) {
3983
4377
  await this.emitOperationalNotice(renderer, '✅ 上下文压缩完成,继续处理...', 'info', 'auto-compact-complete');
3984
4378
  await renderer.flush();
3985
4379
  }
3986
4380
  else {
3987
- 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'}`);
3988
4383
  }
3989
4384
  }
3990
4385
  catch (err) {
@@ -4121,14 +4516,22 @@ export class ResponseEngine {
4121
4516
  const groupName = await adapter?.getGroupName?.(session.metadata.groupId).catch(() => undefined);
4122
4517
  if (groupName) {
4123
4518
  session.metadata.groupName = groupName;
4124
- 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
+ }
4125
4523
  }
4126
4524
  }
4127
4525
  // 同步服务端 mention_mode,供同一群会话的上下文和菜单展示使用。
4128
4526
  if (message.chatType === 'group' && message.mentionMode && session.metadata?.mentionMode !== message.mentionMode) {
4129
4527
  logger.info(`[ResponseEngine] mentionMode sync: sessionId=${session.id} ${session.metadata?.mentionMode ?? 'none'} -> ${message.mentionMode}`);
4130
4528
  session.metadata = { ...(session.metadata || {}), mentionMode: message.mentionMode };
4131
- 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
+ }
4132
4535
  }
4133
4536
  // chatMode 策略由 agent/relation behavior 配置在处理阶段解析;此处不再写 session 级参数。
4134
4537
  // replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
@@ -4166,6 +4569,11 @@ export class ResponseEngine {
4166
4569
  // Per-session agent name for stats bucketing
4167
4570
  const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
4168
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);
4169
4577
  let hasReceivedText = false;
4170
4578
  let hasProjectedCurrentReplyText = false;
4171
4579
  let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
@@ -4554,8 +4962,9 @@ export class ResponseEngine {
4554
4962
  input: event.input,
4555
4963
  ...(event.callId ? { callId: event.callId } : {}),
4556
4964
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4557
- agentAid: session.selfAID ?? 'unknown',
4558
- permissionMode: permissionMode ?? 'unknown',
4965
+ agentName: lifecycleAgentName,
4966
+ agentAid: lifecycleAgentAid ?? 'unknown',
4967
+ permissionMode: lifecyclePermissionMode,
4559
4968
  decision: 'pending',
4560
4969
  decisionSource: 'runner',
4561
4970
  executed: false,
@@ -4631,11 +5040,11 @@ export class ResponseEngine {
4631
5040
  toolName: event.name,
4632
5041
  isError: event.isError,
4633
5042
  ...(event.isError ? { errorCode: classifyToolErrorCode({ errorCode: event.errorCode, error: event.error, result: event.result }) } : {}),
4634
- agentName: agentNameForStats,
4635
5043
  ...(event.callId ? { callId: event.callId } : {}),
4636
5044
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4637
- agentAid: session.selfAID ?? 'unknown',
4638
- permissionMode: permissionMode ?? 'unknown',
5045
+ agentName: lifecycleAgentName,
5046
+ agentAid: lifecycleAgentAid ?? 'unknown',
5047
+ permissionMode: lifecyclePermissionMode,
4639
5048
  decision: event.isError ? 'error' : 'allow',
4640
5049
  decisionSource: 'runner',
4641
5050
  executed: true,
@@ -4793,7 +5202,12 @@ export class ResponseEngine {
4793
5202
  lastReplyText,
4794
5203
  updateSessionMeta: async (patch) => {
4795
5204
  session.metadata = { ...(session.metadata || {}), ...patch };
4796
- 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
+ }
4797
5211
  },
4798
5212
  logger,
4799
5213
  });
@@ -4865,11 +5279,15 @@ export class ResponseEngine {
4865
5279
  // and mark the error so outer catch won't send a duplicate message
4866
5280
  const hasErrorSuppressingContent = hasErrorResult || renderer.hasNonLifecycleContent();
4867
5281
  if (hasErrorSuppressingContent) {
5282
+ let errorOutputFlushed = false;
4868
5283
  try {
4869
5284
  await renderer.flush(true);
5285
+ errorOutputFlushed = true;
4870
5286
  }
4871
- catch { }
4872
- 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) {
4873
5291
  error._errorAlreadySent = true;
4874
5292
  }
4875
5293
  }