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.
- package/CHANGELOG.md +43 -1
- package/README.md +60 -9
- package/bin/install-codex-managed-hooks.mjs +61 -0
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +383 -111
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +405 -137
- package/dist/agents/ecagent-runner.js +174 -63
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +66 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +86 -9
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +26 -11
- package/dist/aun/outbox.js +295 -68
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +1004 -273
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +81 -121
- package/dist/cli/index.js +1 -0
- package/dist/cli/init.js +76 -24
- package/dist/cli/restart-monitor.js +3 -3
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +226 -24
- package/dist/config/config-operation-service.js +1 -2
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +29 -16
- package/dist/config/lifecycle.js +16 -5
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +161 -12
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +155 -4
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +2 -2
- package/dist/core/bootstrap-service.js +27 -38
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/channel-loader.js +0 -2
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +279 -80
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +9 -4
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +495 -72
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +465 -56
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +205 -13
- package/dist/core/permission/tool-policy.js +673 -62
- package/dist/core/relation/peer-identity.js +18 -0
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +18 -9
- package/dist/index.js +283 -65
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +21 -2
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/stats.js +52 -18
- package/dist/utils/welcome.js +2 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/rules/01-overview.md +1 -1
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/schemas/_meta.json +10 -5
- package/kits/schemas/agent-config.schema.10.json +2 -1
- package/kits/schemas/agent-config.schema.11.json +421 -0
- package/kits/schemas/daemon.schema.5.json +135 -0
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +119 -0
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +7 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1731
|
+
apiRetryTimeoutMs() {
|
|
1554
1732
|
if (this.globalSettings.idleMonitor?.enabled === false)
|
|
1555
1733
|
return undefined;
|
|
1556
|
-
const configuredSeconds = this.globalSettings.idleMonitor?.
|
|
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
|
|
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
|
|
1621
|
-
//
|
|
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
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
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
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
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 = !!
|
|
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
|
|
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() &&
|
|
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=${
|
|
2958
|
-
|
|
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(
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
-
|
|
3977
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
4553
|
-
|
|
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
|
-
|
|
4633
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|