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