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.
- package/CHANGELOG.md +22 -0
- package/README.md +58 -9
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +379 -108
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +402 -135
- package/dist/agents/ecagent-runner.js +171 -61
- 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 +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +182 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +409 -88
- 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 +69 -115
- package/dist/cli/init.js +27 -15
- 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 +135 -17
- 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 +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- 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 +150 -2
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-service.js +6 -2
- package/dist/core/causation/aun-association.js +7 -4
- 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 +4 -0
- 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 +486 -68
- 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 +410 -54
- 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 +159 -1
- package/dist/core/permission/tool-policy.js +575 -21
- 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 +1 -1
- package/dist/index.js +253 -46
- 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/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/schemas/_meta.json +4 -2
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- 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 +6 -3
- package/skills/eclink/SKILL.md +2 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1731
|
+
apiRetryTimeoutMs() {
|
|
1555
1732
|
if (this.globalSettings.idleMonitor?.enabled === false)
|
|
1556
1733
|
return undefined;
|
|
1557
|
-
const configuredSeconds = this.globalSettings.idleMonitor?.
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
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 = !!
|
|
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
|
|
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() &&
|
|
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=${
|
|
2963
|
-
|
|
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(
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
-
|
|
3982
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
4558
|
-
|
|
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
|
-
|
|
4638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|