evolcore 0.0.12 → 0.0.14
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 +37 -0
- package/bin/codex-managed-hook.mjs +4 -1
- package/bin/ec-safe-output.js +26 -7
- package/bin/install-codex-managed-hooks.mjs +201 -0
- package/dist/agents/claude-runner.js +105 -11
- package/dist/agents/codex-app-server-client.js +176 -7
- package/dist/agents/codex-runner.js +248 -84
- package/dist/agents/ecagent-runner.js +21 -2
- package/dist/agents/gemini-runner.js +9 -4
- package/dist/aun/aid/control-aid.js +36 -13
- package/dist/aun/aid/store.js +13 -7
- package/dist/aun/group-fs-download.js +56 -0
- package/dist/aun/group-identity.js +1 -2
- package/dist/aun/msg/group.js +11 -33
- package/dist/aun/msg/index.js +1 -2
- package/dist/aun/msg/managed-operation.js +176 -55
- package/dist/aun/msg/upload.js +16 -1
- package/dist/aun/outbox.js +31 -2
- package/dist/aun/rpc/client.js +66 -0
- package/dist/aun/rpc/index.js +1 -2
- package/dist/aun/storage/{upload.js → client.js} +11 -2
- package/dist/aun/storage/index.js +1 -2
- package/dist/channels/aun.js +514 -150
- package/dist/channels/daemon.js +2 -0
- package/dist/cli/agent.js +9 -96
- package/dist/cli/aun-commands.js +1 -1
- package/dist/cli/cli-argv.js +10 -0
- package/dist/cli/config.js +9 -17
- package/dist/cli/ctl-command.js +1 -1
- package/dist/cli/daemon-commands.js +39 -24
- package/dist/cli/fs-command.js +65 -19
- package/dist/cli/init.js +77 -8
- package/dist/cli/queue-command.js +42 -0
- package/dist/cli/restart-monitor.js +3 -14
- package/dist/cli/task-context.js +172 -0
- package/dist/cli/trigger-command.js +7 -0
- package/dist/config/access-policy.js +1 -2
- package/dist/config/builtin-roles.js +7 -0
- package/dist/config/config-manager.js +27 -3
- package/dist/config/contact-book-store.js +7 -2
- package/dist/config/peer-role-resolver.js +6 -2
- package/dist/config/role-migration-startup.js +53 -0
- package/dist/config/role-store.js +1 -1
- package/dist/core/auth/agent-delegation.js +1 -1
- package/dist/core/auth/auth-gateway.js +30 -1
- package/dist/core/auth/authorization-audit.js +47 -0
- package/dist/core/auth/operation-authorizer.js +1 -0
- package/dist/core/auth/operation-catalog.js +10 -1
- package/dist/core/bootstrap-messages.js +9 -2
- package/dist/core/bootstrap-service.js +8 -1
- package/dist/core/command/command-handler.js +201 -51
- package/dist/core/command/menu-catalog.js +4 -0
- package/dist/core/command/menu-handler.js +85 -18
- package/dist/core/command/menu-protocol.js +23 -2
- package/dist/core/command/slash-handler.js +58 -10
- package/dist/core/event-catalog.js +8 -0
- package/dist/core/handoff/runtime.js +12 -1
- package/dist/core/handoff/store.js +5 -1
- package/dist/core/interaction-router.js +10 -0
- package/dist/core/message/im-renderer.js +34 -1
- package/dist/core/message/message-bridge.js +68 -13
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-queue.js +267 -39
- package/dist/core/message/message-utils.js +81 -4
- package/dist/core/message/response-engine.js +242 -48
- package/dist/core/message/stream-debouncer.js +10 -2
- package/dist/core/permission/approval-gateway.js +1 -0
- package/dist/core/permission/ec-command-parser.js +649 -18
- package/dist/core/permission/index.js +1 -1
- package/dist/core/permission/mode.js +15 -0
- package/dist/core/permission/readonly-shell-query.js +148 -8
- package/dist/core/permission/sandbox-runtime.js +21 -0
- package/dist/core/permission/tool-policy.js +266 -56
- package/dist/core/protected-paths.js +107 -2
- package/dist/core/runtime-lock.js +101 -0
- package/dist/core/session/session-manager.js +21 -2
- package/dist/core/session/session-mapper.js +20 -1
- package/dist/eck/detect.js +18 -1
- package/dist/eck/group-rules-sync.js +15 -44
- package/dist/index.js +191 -89
- package/dist/ipc.js +16 -3
- package/dist/paths.js +3 -0
- package/dist/response-system/engines/v1/proactive-flow.js +92 -8
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +64 -11
- package/dist/trigger/history.js +42 -7
- package/dist/trigger/manager.js +21 -1
- package/dist/trigger/parser.js +14 -2
- package/dist/trigger/scheduler.js +1 -0
- package/dist/trigger/validation.js +67 -6
- package/dist/utils/ecweb-supervisor.js +332 -0
- package/dist/utils/error-utils.js +17 -0
- package/dist/utils/logger.js +37 -4
- package/kits/docs/evolcore/trigger.md +17 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/templates/roles/admin.json +7 -0
- package/kits/templates/roles/member.json +8 -1
- package/kits/templates/roles/visitor.json +2 -0
- package/package.json +1 -1
- package/dist/aun/msg/payload-type.js +0 -27
- package/dist/aun/rpc/caller.js +0 -42
- package/dist/aun/rpc/connection.js +0 -25
- package/dist/aun/storage/manage.js +0 -10
- package/dist/config/access-policy-domain.js +0 -18
- package/dist/config/config-batch-get.js +0 -11
- package/dist/config/owner-policy.js +0 -4
- package/dist/config/role-config-v4-startup.js +0 -32
- package/dist/config/role-config-v5-startup.js +0 -27
- package/dist/core/auth/trigger-authorization.js +0 -15
- package/dist/core/interaction-registration.js +0 -10
- package/dist/core/permission/execution-sandbox.js +0 -16
- package/dist/core/relation/peer-key.js +0 -1
- package/dist/core/session/session-key.js +0 -24
- package/dist/eck/baseagent-caps.js +0 -18
- package/dist/eck/rules-loader.js +0 -28
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForContextWindow, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken } from '../../agents/runner-types.js';
|
|
6
|
-
import { buildTaskRuntimeEnv } from '../../cli/task-context.js';
|
|
6
|
+
import { buildTaskRuntimeEnv, ensureRuntimeLockDir, ensureSessionRuntimeDir } from '../../cli/task-context.js';
|
|
7
7
|
import { IMRenderer } from './im-renderer.js';
|
|
8
8
|
import { createSendFileMarkerPattern } from './file-markers.js';
|
|
9
9
|
import { createTextInferenceProvider } from '../inference/text-inference.js';
|
|
@@ -22,7 +22,7 @@ import { createRootCausation, deriveCausation, normalizeCausation } from '../cau
|
|
|
22
22
|
import { recordCausationLink, recordCausationSpan } from '../causation/audit.js';
|
|
23
23
|
import { normalizeBaseagent } from '../../agents/baseagent.js';
|
|
24
24
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
25
|
-
import { buildEnvelope } from './message-utils.js';
|
|
25
|
+
import { buildEnvelope, replyContextFromSession, withEnvelopeReplyContext } from './message-utils.js';
|
|
26
26
|
import { isSystemOrServicePeer, resolveChatMode } from './peer-mode.js';
|
|
27
27
|
import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
|
|
28
28
|
import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
|
|
@@ -136,6 +136,7 @@ const RETRY_EXHAUSTED_MARK = Symbol('evolcore.retryExhausted');
|
|
|
136
136
|
const RETRY_MADE_PROGRESS_MARK = Symbol('evolcore.retryMadeProgress');
|
|
137
137
|
const RETRY_HEALTH_RECORDED_MARK = Symbol('evolcore.retryHealthRecorded');
|
|
138
138
|
const MODEL_FALLBACK_EXHAUSTED_MARK = Symbol('evolcore.modelFallbackExhausted');
|
|
139
|
+
const RETRY_INPUT_ALREADY_SUBMITTED_MARK = Symbol('evolcore.retryInputAlreadySubmitted');
|
|
139
140
|
function markRetryExhausted(error, retries) {
|
|
140
141
|
if (error && typeof error === 'object') {
|
|
141
142
|
error[RETRY_EXHAUSTED_MARK] = retries;
|
|
@@ -176,6 +177,21 @@ function getModelFallbackExhaustedMessage(error) {
|
|
|
176
177
|
: undefined;
|
|
177
178
|
return typeof message === 'string' ? message : undefined;
|
|
178
179
|
}
|
|
180
|
+
function markRetryInputAlreadySubmitted(error) {
|
|
181
|
+
if (error && typeof error === 'object') {
|
|
182
|
+
error[RETRY_INPUT_ALREADY_SUBMITTED_MARK] = true;
|
|
183
|
+
// A terminal delivery guard, rather than any buffered activity, owns the
|
|
184
|
+
// final user-visible explanation.
|
|
185
|
+
delete error._errorAlreadySent;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function wasRetryInputAlreadySubmitted(error) {
|
|
189
|
+
return !!(error && typeof error === 'object' && error[RETRY_INPUT_ALREADY_SUBMITTED_MARK] === true);
|
|
190
|
+
}
|
|
191
|
+
function formatRetryInputAlreadySubmittedMessage(error) {
|
|
192
|
+
const reason = getErrorMessage(error, undefined, false) || 'Codex 暂时不可用';
|
|
193
|
+
return `⚠️ Codex 已接收本次消息,但本轮未能确认完成。为避免在会话中重复写入同一消息,系统未自动重试。请检查会话状态后重新发送。\n原因:${reason}`;
|
|
194
|
+
}
|
|
179
195
|
function isNonErrorInterruptReason(reason) {
|
|
180
196
|
return reason === 'new_message'
|
|
181
197
|
|| reason === 'stop'
|
|
@@ -492,7 +508,7 @@ export class ResponseEngine {
|
|
|
492
508
|
return;
|
|
493
509
|
const available = error.availableBaseagents.length ? error.availableBaseagents.join(', ') : '(none)';
|
|
494
510
|
const text = `❌ 当前会话绑定的 baseagent 不可用: ${error.baseagent}\nAgent: ${error.evolagentName}\n可用: ${available}\n请使用 /baseagent 切换到可用后端。`;
|
|
495
|
-
const sessionReplyContext = { ...(replyContext
|
|
511
|
+
const sessionReplyContext = { ...replyContextFromSession(session, replyContext), sessionId: session.id };
|
|
496
512
|
await channelInfo.adapter.send(buildEnvelope({
|
|
497
513
|
taskId: `system-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`,
|
|
498
514
|
sessionId: session.id,
|
|
@@ -655,7 +671,7 @@ export class ResponseEngine {
|
|
|
655
671
|
agentName: agent.name,
|
|
656
672
|
chatmode: this.resolveEffectiveChatmodeForSession(session, channelKey, session.channelId),
|
|
657
673
|
replyContext: {
|
|
658
|
-
...(session
|
|
674
|
+
...replyContextFromSession(session),
|
|
659
675
|
sessionId: session.id,
|
|
660
676
|
threadId: session.threadId || session.metadata?.replyContext?.threadId,
|
|
661
677
|
},
|
|
@@ -937,7 +953,7 @@ export class ResponseEngine {
|
|
|
937
953
|
}
|
|
938
954
|
agent.registerStream(streamKey, retryStream);
|
|
939
955
|
return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
|
|
940
|
-
undefined, turnLease);
|
|
956
|
+
undefined, turnLease, opts.permissionMode, opts.proactiveSelfAid);
|
|
941
957
|
}
|
|
942
958
|
// Dropping the whole session hides the root cause and can replay side
|
|
943
959
|
// effects in unattended trigger tasks. Surface an actionable failure
|
|
@@ -1199,7 +1215,7 @@ export class ResponseEngine {
|
|
|
1199
1215
|
logger.warn(`[ResponseEngine] Trigger execution identity missing: trigger=${message.triggerMeta?.triggerId ?? '<none>'}`);
|
|
1200
1216
|
const channelInfo = this.resolveChannelInfo(accessChannelKey);
|
|
1201
1217
|
if (channelInfo) {
|
|
1202
|
-
await channelInfo.adapter.send({
|
|
1218
|
+
await channelInfo.adapter.send(buildEnvelope({
|
|
1203
1219
|
taskId: `trigger-identity-missing-${Date.now()}`,
|
|
1204
1220
|
sessionId: session.id,
|
|
1205
1221
|
channel: accessChannelKey,
|
|
@@ -1208,7 +1224,7 @@ export class ResponseEngine {
|
|
|
1208
1224
|
chatmode: 'interactive',
|
|
1209
1225
|
replyContext: message.replyContext,
|
|
1210
1226
|
timestamp: Date.now(),
|
|
1211
|
-
}, {
|
|
1227
|
+
}), {
|
|
1212
1228
|
kind: 'system.error',
|
|
1213
1229
|
text: 'Trigger 创建者身份缺失,已拒绝执行',
|
|
1214
1230
|
subtype: 'trigger_identity_missing',
|
|
@@ -1230,7 +1246,7 @@ export class ResponseEngine {
|
|
|
1230
1246
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
1231
1247
|
if (channelInfo) {
|
|
1232
1248
|
try {
|
|
1233
|
-
await channelInfo.adapter.send({
|
|
1249
|
+
await channelInfo.adapter.send(buildEnvelope({
|
|
1234
1250
|
taskId: `access-denied-${Date.now()}`,
|
|
1235
1251
|
sessionId: session.id,
|
|
1236
1252
|
channel: channelKey,
|
|
@@ -1239,7 +1255,7 @@ export class ResponseEngine {
|
|
|
1239
1255
|
chatmode: 'interactive',
|
|
1240
1256
|
replyContext: message.replyContext,
|
|
1241
1257
|
timestamp: Date.now(),
|
|
1242
|
-
}, {
|
|
1258
|
+
}), {
|
|
1243
1259
|
kind: 'system.error',
|
|
1244
1260
|
text: '暂无权限访问本 agent,请联系 agent 管理员授权访问',
|
|
1245
1261
|
subtype: 'access_denied',
|
|
@@ -1497,7 +1513,12 @@ export class ResponseEngine {
|
|
|
1497
1513
|
}
|
|
1498
1514
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
1499
1515
|
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, outputState = { middleOutputMode: 'all' }, timeoutControl) {
|
|
1500
|
-
|
|
1516
|
+
// The model must see the time at which the adapter accepted the input.
|
|
1517
|
+
// Runner start is recorded separately so queue delay never rewrites input
|
|
1518
|
+
// chronology after a historical replay or a long backlog.
|
|
1519
|
+
const inboundTimestamp = message.receivedAt ?? message.enqueuedAt ?? message.timestamp ?? Date.now();
|
|
1520
|
+
const runnerStartedAt = Date.now();
|
|
1521
|
+
const messageId = `${message.channel}_${message.channelId}_${inboundTimestamp}`;
|
|
1501
1522
|
const channelKey = this.messageChannelKey(message, session);
|
|
1502
1523
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
1503
1524
|
const taskAgentAid = message.selfAID || session.selfAID;
|
|
@@ -1724,7 +1745,7 @@ export class ResponseEngine {
|
|
|
1724
1745
|
const taskReplyContext = () => {
|
|
1725
1746
|
const base = this.getReplyContext(message);
|
|
1726
1747
|
return {
|
|
1727
|
-
...(base
|
|
1748
|
+
...replyContextFromSession(session, base),
|
|
1728
1749
|
sessionId: session.id,
|
|
1729
1750
|
threadId: session.threadId || base?.threadId,
|
|
1730
1751
|
metadata: withTaskMetadata(base?.metadata),
|
|
@@ -1844,7 +1865,7 @@ export class ResponseEngine {
|
|
|
1844
1865
|
// Kept outside the task body so a thrown terminal error can preserve the
|
|
1845
1866
|
// work already performed in its trigger audit metadata.
|
|
1846
1867
|
let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
1847
|
-
let startTime =
|
|
1868
|
+
let startTime = runnerStartedAt;
|
|
1848
1869
|
let protocolReplayAttempts = 0;
|
|
1849
1870
|
try {
|
|
1850
1871
|
const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
@@ -1853,7 +1874,10 @@ export class ResponseEngine {
|
|
|
1853
1874
|
msgId: messageId,
|
|
1854
1875
|
sessionId: session.id,
|
|
1855
1876
|
dir: 'inbound',
|
|
1856
|
-
status: 'received'
|
|
1877
|
+
status: 'received',
|
|
1878
|
+
receivedAt: message.receivedAt ?? inboundTimestamp,
|
|
1879
|
+
enqueuedAt: message.enqueuedAt,
|
|
1880
|
+
processingStartedAt: runnerStartedAt,
|
|
1857
1881
|
});
|
|
1858
1882
|
this.eventBus.publish({
|
|
1859
1883
|
type: 'message:received',
|
|
@@ -1862,7 +1886,10 @@ export class ResponseEngine {
|
|
|
1862
1886
|
channelId: message.channelId,
|
|
1863
1887
|
content: message.content,
|
|
1864
1888
|
agentName: agentNameForStats,
|
|
1865
|
-
timestamp:
|
|
1889
|
+
timestamp: inboundTimestamp,
|
|
1890
|
+
receivedAt: message.receivedAt ?? inboundTimestamp,
|
|
1891
|
+
enqueuedAt: message.enqueuedAt,
|
|
1892
|
+
processingStartedAt: runnerStartedAt,
|
|
1866
1893
|
});
|
|
1867
1894
|
// ── 硬上限检查:超限直接返回提示,不调模型 ──
|
|
1868
1895
|
{
|
|
@@ -1965,6 +1992,7 @@ export class ResponseEngine {
|
|
|
1965
1992
|
const renderer = new IMRenderer({
|
|
1966
1993
|
adapter,
|
|
1967
1994
|
envelope,
|
|
1995
|
+
agentAid: session.selfAID,
|
|
1968
1996
|
flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
|
|
1969
1997
|
suppressActivityItems: isProactive ? false : middleOutputMode !== 'all',
|
|
1970
1998
|
suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
|
|
@@ -2007,7 +2035,7 @@ export class ResponseEngine {
|
|
|
2007
2035
|
opts.metadata = withTaskMetadata(opts.metadata);
|
|
2008
2036
|
if (payload.kind.startsWith('status.'))
|
|
2009
2037
|
this.touchAgentActivity(channelKey);
|
|
2010
|
-
const enrichedEnvelope =
|
|
2038
|
+
const enrichedEnvelope = withEnvelopeReplyContext(envelope, opts);
|
|
2011
2039
|
snapshot.pushOutbound(session.id, taskId, { kind: payload.kind, decision: 'sent' });
|
|
2012
2040
|
await adapter.send(enrichedEnvelope, payload);
|
|
2013
2041
|
},
|
|
@@ -2032,7 +2060,7 @@ export class ResponseEngine {
|
|
|
2032
2060
|
const capturedReplyContext = taskReplyContext();
|
|
2033
2061
|
// 设置权限审批的消息发送回调(指向当前渠道)
|
|
2034
2062
|
const permissionPrompt = async (text) => {
|
|
2035
|
-
await adapter.send(
|
|
2063
|
+
await adapter.send(withEnvelopeReplyContext(envelope, capturedReplyContext), { kind: 'result.text', text, isFinal: false });
|
|
2036
2064
|
};
|
|
2037
2065
|
const authChatType = configChatType;
|
|
2038
2066
|
const authConversationId = authChatType === 'group'
|
|
@@ -2046,15 +2074,16 @@ export class ResponseEngine {
|
|
|
2046
2074
|
: undefined;
|
|
2047
2075
|
const permissionAdapter = triggerOriginChannelInfo?.adapter ?? adapter;
|
|
2048
2076
|
const permissionChannelId = authorizationIdentity?.actorId ?? capturedChannelId;
|
|
2049
|
-
const permissionReplyContext = authorizationIdentity
|
|
2077
|
+
const permissionReplyContext = authorizationIdentity
|
|
2078
|
+
? { delivery: { chatType: 'private' } }
|
|
2079
|
+
: capturedReplyContext;
|
|
2050
2080
|
let permissionTerminalFailure;
|
|
2051
2081
|
const permissionPromptForOrigin = async (text) => {
|
|
2052
|
-
await permissionAdapter.send({
|
|
2082
|
+
await permissionAdapter.send(withEnvelopeReplyContext({
|
|
2053
2083
|
...envelope,
|
|
2054
2084
|
channel: authorizationIdentity?.originChannelKey ?? envelope.channel,
|
|
2055
2085
|
channelId: permissionChannelId,
|
|
2056
|
-
|
|
2057
|
-
}, { kind: 'result.text', text, isFinal: false });
|
|
2086
|
+
}, permissionReplyContext), { kind: 'result.text', text, isFinal: false });
|
|
2058
2087
|
};
|
|
2059
2088
|
agent.setSendPrompt(permissionPromptForOrigin);
|
|
2060
2089
|
const owningAgentForAuth = owningAgentForTask;
|
|
@@ -2142,6 +2171,7 @@ export class ResponseEngine {
|
|
|
2142
2171
|
? (anomaly) => {
|
|
2143
2172
|
recordTriggerExecutionAnomaly(triggerRunId, {
|
|
2144
2173
|
...anomaly,
|
|
2174
|
+
correlationId: anomaly.correlationId ?? anomaly.requestId,
|
|
2145
2175
|
agentAid: anomaly.agentAid ?? session.selfAID ?? message.selfAID,
|
|
2146
2176
|
sessionId: anomaly.sessionId ?? session.id,
|
|
2147
2177
|
permissionMode: anomaly.permissionMode ?? effectivePermissionMode,
|
|
@@ -2200,7 +2230,7 @@ export class ResponseEngine {
|
|
|
2200
2230
|
},
|
|
2201
2231
|
},
|
|
2202
2232
|
permissionStateChanged: async (permissionEvent) => {
|
|
2203
|
-
const executionEnvelope =
|
|
2233
|
+
const executionEnvelope = withEnvelopeReplyContext(envelope, capturedReplyContext);
|
|
2204
2234
|
if (permissionEvent.state === 'waiting') {
|
|
2205
2235
|
const payload = {
|
|
2206
2236
|
kind: 'status.requires_action',
|
|
@@ -2214,12 +2244,11 @@ export class ResponseEngine {
|
|
|
2214
2244
|
};
|
|
2215
2245
|
await adapter.send(executionEnvelope, payload);
|
|
2216
2246
|
if (permissionAdapter !== adapter || permissionChannelId !== capturedChannelId) {
|
|
2217
|
-
await permissionAdapter.send({
|
|
2247
|
+
await permissionAdapter.send(withEnvelopeReplyContext({
|
|
2218
2248
|
...envelope,
|
|
2219
2249
|
channel: authorizationIdentity?.originChannelKey ?? envelope.channel,
|
|
2220
2250
|
channelId: permissionChannelId,
|
|
2221
|
-
|
|
2222
|
-
}, payload);
|
|
2251
|
+
}, permissionReplyContext), payload);
|
|
2223
2252
|
}
|
|
2224
2253
|
return;
|
|
2225
2254
|
}
|
|
@@ -2731,6 +2760,12 @@ export class ResponseEngine {
|
|
|
2731
2760
|
this.handoffRuntime.completeTargetContext(selfAid, handoffId);
|
|
2732
2761
|
}
|
|
2733
2762
|
}
|
|
2763
|
+
// The runner may already own a stronger per-session directory (Codex
|
|
2764
|
+
// registers one before constructing its thread). Other runners use a
|
|
2765
|
+
// private child of the process-provided TMPDIR. Both paths are passed
|
|
2766
|
+
// explicitly so sandboxed helper commands never fall back to a shared
|
|
2767
|
+
// agent or project directory for transient state.
|
|
2768
|
+
const sessionRuntimeDir = agent.getManagedTempDir?.(session.id) ?? ensureSessionRuntimeDir(session.id);
|
|
2734
2769
|
const taskRuntimeContext = {
|
|
2735
2770
|
taskId,
|
|
2736
2771
|
sessionId: session.id,
|
|
@@ -2744,6 +2779,8 @@ export class ResponseEngine {
|
|
|
2744
2779
|
peerType: message.peerType || session.metadata?.peerType || undefined,
|
|
2745
2780
|
peerRole,
|
|
2746
2781
|
threadId: session.threadId || undefined,
|
|
2782
|
+
sessionRuntimeDir,
|
|
2783
|
+
runtimeLockDir: ensureRuntimeLockDir(sessionRuntimeDir),
|
|
2747
2784
|
handoffIds: v2HandoffIds.length > 0 && v2HandoffDirection === 'target' ? v2HandoffIds : undefined,
|
|
2748
2785
|
causation: taskCausation,
|
|
2749
2786
|
};
|
|
@@ -2777,6 +2814,7 @@ export class ResponseEngine {
|
|
|
2777
2814
|
? (agent.resolveModelId(model) ?? model)
|
|
2778
2815
|
: model));
|
|
2779
2816
|
const retryAttemptTimeoutMs = this.retryAttemptTimeoutMs();
|
|
2817
|
+
const retryInputIsAtMostOnce = agent.retryInputSemantics === 'at_most_once';
|
|
2780
2818
|
let runAttempt = 1;
|
|
2781
2819
|
const recordRetryHealthError = async (retryError) => {
|
|
2782
2820
|
if (!policy.accumulateErrors(chatType, identityRole))
|
|
@@ -2813,14 +2851,17 @@ export class ResponseEngine {
|
|
|
2813
2851
|
const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
|
|
2814
2852
|
resetTimer(eventType, toolName);
|
|
2815
2853
|
attemptTimeout?.reset();
|
|
2816
|
-
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease);
|
|
2854
|
+
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, session.selfAID || message.selfAID);
|
|
2817
2855
|
streamResult = attemptTimeout
|
|
2818
2856
|
? await Promise.race([processAttempt, attemptTimeout.promise])
|
|
2819
2857
|
: await processAttempt;
|
|
2820
2858
|
if (!streamResult.isError) {
|
|
2821
2859
|
const protocolDecision = this.turnCoordinator.evaluateCommit(turnLease, this.buildTurnCommitEvidence(session, streamResult));
|
|
2822
2860
|
const protocolReason = protocolDecision.ok ? undefined : protocolDecision.reason;
|
|
2823
|
-
|
|
2861
|
+
// Codex app-server turn/start appends its input before runQuery
|
|
2862
|
+
// resolves. Replaying here would create a second user turn.
|
|
2863
|
+
const safeProtocolReplay = !retryInputIsAtMostOnce
|
|
2864
|
+
&& isSafeProtocolReplayCandidate(streamResult, protocolReason);
|
|
2824
2865
|
const protocolRetry = retryScheduler.afterProtocolFailure(safeProtocolReplay ? protocolReason : undefined, safeProtocolReplay);
|
|
2825
2866
|
if (protocolRetry.kind === 'protocol_replay') {
|
|
2826
2867
|
protocolReplayAttempts = protocolRetry.attempt;
|
|
@@ -2865,6 +2906,16 @@ export class ResponseEngine {
|
|
|
2865
2906
|
permissionError._errorAlreadySent = true;
|
|
2866
2907
|
throw permissionError;
|
|
2867
2908
|
}
|
|
2909
|
+
const retryWouldReplaySubmittedInput = retryAttemptTimedOut
|
|
2910
|
+
|| isRetryableError(retryError)
|
|
2911
|
+
|| classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE;
|
|
2912
|
+
if (retryInputIsAtMostOnce && streamRegistered && retryWouldReplaySubmittedInput) {
|
|
2913
|
+
await recordRetryHealthError(retryError);
|
|
2914
|
+
markRetryInputAlreadySubmitted(retryError);
|
|
2915
|
+
logger.warn(`[ResponseEngine] Not replaying already-submitted input: agent=${agent.name} `
|
|
2916
|
+
+ `session=${session.id} task=${taskId} attempt=${runAttempt}`);
|
|
2917
|
+
throw retryError;
|
|
2918
|
+
}
|
|
2868
2919
|
if (classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE && !hasExplicitTriggerModel) {
|
|
2869
2920
|
if (probingPrimaryModel && previousFallbackState) {
|
|
2870
2921
|
const resumedFallback = previousFallbackState.activeFallback;
|
|
@@ -2958,6 +3009,8 @@ export class ResponseEngine {
|
|
|
2958
3009
|
resetTimer,
|
|
2959
3010
|
shouldSuppress,
|
|
2960
3011
|
proactive,
|
|
3012
|
+
proactiveSelfAid: session.selfAID || message.selfAID,
|
|
3013
|
+
permissionMode: effectivePermissionMode,
|
|
2961
3014
|
turnLease,
|
|
2962
3015
|
});
|
|
2963
3016
|
}
|
|
@@ -2999,6 +3052,8 @@ export class ResponseEngine {
|
|
|
2999
3052
|
resetTimer,
|
|
3000
3053
|
shouldSuppress,
|
|
3001
3054
|
proactive,
|
|
3055
|
+
proactiveSelfAid: session.selfAID || message.selfAID,
|
|
3056
|
+
permissionMode: effectivePermissionMode,
|
|
3002
3057
|
turnLease,
|
|
3003
3058
|
});
|
|
3004
3059
|
// 重试后仍然 prompt_too_long:显示友好提示
|
|
@@ -3070,7 +3125,7 @@ export class ResponseEngine {
|
|
|
3070
3125
|
return;
|
|
3071
3126
|
const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
3072
3127
|
if (!isCurrentlyBackground) {
|
|
3073
|
-
await adapter.send(
|
|
3128
|
+
await adapter.send(withEnvelopeReplyContext(envelope, capturedReplyContext), payload);
|
|
3074
3129
|
if (payload.kind === 'result.text') {
|
|
3075
3130
|
logger.info(`[ResponseEngine] afterProcess sent: task=${taskId} text="${payload.text.slice(0, 60)}"`);
|
|
3076
3131
|
}
|
|
@@ -3158,7 +3213,10 @@ export class ResponseEngine {
|
|
|
3158
3213
|
// 发送文件
|
|
3159
3214
|
logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
|
|
3160
3215
|
try {
|
|
3161
|
-
|
|
3216
|
+
const targetReplyContext = isCrossChannel
|
|
3217
|
+
? { delivery: { chatType: 'private' } }
|
|
3218
|
+
: capturedReplyContext;
|
|
3219
|
+
await targetInfo.adapter.send(buildEnvelope({ taskId, channel: targetInfo.adapter.channelName, channelId: targetChannelId, agentName: agentNameForStats, replyContext: targetReplyContext }), { kind: 'result.file', filePath: resolvedPath });
|
|
3162
3220
|
this.eventBus.publish({ type: 'runner:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
|
|
3163
3221
|
sent++;
|
|
3164
3222
|
if (isCrossChannel) {
|
|
@@ -3681,6 +3739,7 @@ export class ResponseEngine {
|
|
|
3681
3739
|
// processEventStream 已通过 renderer 发过错误时也跳过
|
|
3682
3740
|
const retryExhaustedCount = getRetryExhaustedCount(error);
|
|
3683
3741
|
const modelFallbackExhaustedMessage = getModelFallbackExhaustedMessage(error);
|
|
3742
|
+
const retryInputAlreadySubmitted = wasRetryInputAlreadySubmitted(error);
|
|
3684
3743
|
if (isUserInterrupt) {
|
|
3685
3744
|
logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
|
|
3686
3745
|
}
|
|
@@ -3692,13 +3751,15 @@ export class ResponseEngine {
|
|
|
3692
3751
|
const idleSec = getLastIdleSec?.() || 0;
|
|
3693
3752
|
const userMessage = retryExhaustedCount
|
|
3694
3753
|
? formatRetryableErrorFinalMessage(error, retryExhaustedCount)
|
|
3695
|
-
:
|
|
3696
|
-
?
|
|
3697
|
-
:
|
|
3698
|
-
?
|
|
3699
|
-
:
|
|
3700
|
-
?
|
|
3701
|
-
:
|
|
3754
|
+
: retryInputAlreadySubmitted
|
|
3755
|
+
? formatRetryInputAlreadySubmittedMessage(error)
|
|
3756
|
+
: modelFallbackExhaustedMessage
|
|
3757
|
+
? modelFallbackExhaustedMessage
|
|
3758
|
+
: isTotalExecutionTimeout
|
|
3759
|
+
? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
|
|
3760
|
+
: isTimeout
|
|
3761
|
+
? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
|
|
3762
|
+
: getErrorMessage(error, undefined);
|
|
3702
3763
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
3703
3764
|
let sendOpts;
|
|
3704
3765
|
try {
|
|
@@ -3715,14 +3776,14 @@ export class ResponseEngine {
|
|
|
3715
3776
|
const errorPayload = {
|
|
3716
3777
|
kind: 'result.error',
|
|
3717
3778
|
text: userMessage,
|
|
3718
|
-
reason: retryExhaustedCount ? 'retry_exhausted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
|
|
3779
|
+
reason: retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
|
|
3719
3780
|
};
|
|
3720
3781
|
if (!isUserInterrupt && daemonTrigger && procStatus === 'error') {
|
|
3721
3782
|
// DaemonChannel settles a trigger on the first terminal payload. Put
|
|
3722
3783
|
// the complete failure metadata first so the audit retains tool
|
|
3723
3784
|
// counts and context-recovery diagnostics even for thrown errors
|
|
3724
3785
|
// (for example when compact itself fails).
|
|
3725
|
-
await adapter.send(
|
|
3786
|
+
await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), {
|
|
3726
3787
|
kind: 'status.error',
|
|
3727
3788
|
metadata: {
|
|
3728
3789
|
errorType: errType,
|
|
@@ -3735,17 +3796,17 @@ export class ResponseEngine {
|
|
|
3735
3796
|
contextRecoveryAttempts: streamResult.contextRecoveryAttempts ?? 0,
|
|
3736
3797
|
},
|
|
3737
3798
|
}).catch(() => { });
|
|
3738
|
-
await adapter.send(
|
|
3799
|
+
await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), errorPayload);
|
|
3739
3800
|
}
|
|
3740
3801
|
else {
|
|
3741
|
-
await adapter.send(
|
|
3802
|
+
await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), errorPayload);
|
|
3742
3803
|
}
|
|
3743
3804
|
if (!isUserInterrupt && daemonTrigger && procStatus !== 'error') {
|
|
3744
|
-
await adapter.send(
|
|
3805
|
+
await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), statusPayload).catch(() => { });
|
|
3745
3806
|
}
|
|
3746
3807
|
// Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
|
|
3747
3808
|
if (isProactive && adapter.capabilities?.thought) {
|
|
3748
|
-
await adapter.send(
|
|
3809
|
+
await adapter.send(withEnvelopeReplyContext(envelope, sendOpts), {
|
|
3749
3810
|
kind: 'activity.batch',
|
|
3750
3811
|
items: [{
|
|
3751
3812
|
kind: 'notice',
|
|
@@ -3973,7 +4034,7 @@ export class ResponseEngine {
|
|
|
3973
4034
|
*/
|
|
3974
4035
|
async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive,
|
|
3975
4036
|
/** [迁移点4/5] 响应模式插件 + 状态,用于调 onToolUse/onComplete 钩子 */
|
|
3976
|
-
modeHooks, causation, turnLease) {
|
|
4037
|
+
modeHooks, causation, turnLease, permissionMode, proactiveSelfAid) {
|
|
3977
4038
|
// Per-session agent name for stats bucketing
|
|
3978
4039
|
const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
|
|
3979
4040
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
|
|
@@ -3989,6 +4050,7 @@ export class ResponseEngine {
|
|
|
3989
4050
|
let toolUseCount = 0;
|
|
3990
4051
|
const openToolUseIds = new Set();
|
|
3991
4052
|
const anonymousToolUseIds = new Map();
|
|
4053
|
+
const anonymousOpenIdByCorrelation = new Map();
|
|
3992
4054
|
const finalizeResult = () => ({
|
|
3993
4055
|
...completeResult,
|
|
3994
4056
|
hasReceivedText,
|
|
@@ -4004,7 +4066,28 @@ export class ResponseEngine {
|
|
|
4004
4066
|
let lastReplyText = '';
|
|
4005
4067
|
// callId → description 映射,用于 tool_result 回显描述
|
|
4006
4068
|
const toolDescByCallId = new Map();
|
|
4069
|
+
const toolInputByCallId = new Map();
|
|
4070
|
+
const toolInputByCorrelationId = new Map();
|
|
4071
|
+
const anonymousToolInputsByName = new Map();
|
|
4007
4072
|
const ctlQueueReadCallIds = new Set();
|
|
4073
|
+
const removePendingToolInput = (pending) => {
|
|
4074
|
+
for (const [callId, value] of toolInputByCallId) {
|
|
4075
|
+
if (value === pending)
|
|
4076
|
+
toolInputByCallId.delete(callId);
|
|
4077
|
+
}
|
|
4078
|
+
for (const [correlationId, value] of toolInputByCorrelationId) {
|
|
4079
|
+
if (value === pending)
|
|
4080
|
+
toolInputByCorrelationId.delete(correlationId);
|
|
4081
|
+
}
|
|
4082
|
+
const anonymous = anonymousToolInputsByName.get(pending.name);
|
|
4083
|
+
if (anonymous) {
|
|
4084
|
+
const remaining = anonymous.filter(value => value !== pending);
|
|
4085
|
+
if (remaining.length > 0)
|
|
4086
|
+
anonymousToolInputsByName.set(pending.name, remaining);
|
|
4087
|
+
else
|
|
4088
|
+
anonymousToolInputsByName.delete(pending.name);
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4008
4091
|
try {
|
|
4009
4092
|
for await (let event of stream) {
|
|
4010
4093
|
if (turnLease && !this.turnCoordinator.isCurrent(turnLease)) {
|
|
@@ -4036,16 +4119,44 @@ export class ResponseEngine {
|
|
|
4036
4119
|
else {
|
|
4037
4120
|
const syntheticId = `__anonymous_tool_${toolUseCount}`;
|
|
4038
4121
|
openToolUseIds.add(syntheticId);
|
|
4122
|
+
if (event.correlationId)
|
|
4123
|
+
anonymousOpenIdByCorrelation.set(event.correlationId, syntheticId);
|
|
4039
4124
|
const pending = anonymousToolUseIds.get(event.name) ?? [];
|
|
4040
4125
|
pending.push(syntheticId);
|
|
4041
4126
|
anonymousToolUseIds.set(event.name, pending);
|
|
4042
4127
|
}
|
|
4043
4128
|
}
|
|
4044
4129
|
else if (event.type === 'tool_result') {
|
|
4045
|
-
|
|
4130
|
+
// A result carrying both identifiers may be stale or replayed. Do not
|
|
4131
|
+
// close the active call until both identifiers resolve to the same
|
|
4132
|
+
// pending tool invocation; otherwise a mismatched result can make the
|
|
4133
|
+
// protocol look complete even though the real call is still open.
|
|
4134
|
+
const hasBothResultIdentifiers = !!event.callId && !!event.correlationId;
|
|
4135
|
+
const byCallId = event.callId ? toolInputByCallId.get(event.callId) : undefined;
|
|
4136
|
+
const byCorrelationId = event.correlationId
|
|
4137
|
+
? toolInputByCorrelationId.get(event.correlationId)
|
|
4138
|
+
: undefined;
|
|
4139
|
+
const identifiersMatch = !hasBothResultIdentifiers
|
|
4140
|
+
|| (!!byCallId && !!byCorrelationId && byCallId === byCorrelationId);
|
|
4141
|
+
if (event.callId && identifiersMatch) {
|
|
4046
4142
|
openToolUseIds.delete(event.callId);
|
|
4047
4143
|
}
|
|
4048
|
-
else {
|
|
4144
|
+
else if (!event.callId && event.correlationId && byCorrelationId) {
|
|
4145
|
+
const syntheticId = anonymousOpenIdByCorrelation.get(event.correlationId);
|
|
4146
|
+
if (syntheticId) {
|
|
4147
|
+
openToolUseIds.delete(syntheticId);
|
|
4148
|
+
anonymousOpenIdByCorrelation.delete(event.correlationId);
|
|
4149
|
+
}
|
|
4150
|
+
else {
|
|
4151
|
+
for (const [callId, pending] of toolInputByCallId) {
|
|
4152
|
+
if (pending === byCorrelationId) {
|
|
4153
|
+
openToolUseIds.delete(callId);
|
|
4154
|
+
break;
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
else if (!event.callId) {
|
|
4049
4160
|
const names = event.name ? [event.name] : [...anonymousToolUseIds.keys()];
|
|
4050
4161
|
for (const name of names) {
|
|
4051
4162
|
const pending = anonymousToolUseIds.get(name);
|
|
@@ -4059,6 +4170,11 @@ export class ResponseEngine {
|
|
|
4059
4170
|
}
|
|
4060
4171
|
}
|
|
4061
4172
|
}
|
|
4173
|
+
else if (event.type === 'notice') {
|
|
4174
|
+
for (const callId of event.closedToolCallIds ?? []) {
|
|
4175
|
+
openToolUseIds.delete(callId);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4062
4178
|
else if (event.type === 'complete') {
|
|
4063
4179
|
assistantUuid = event.assistantUuid ?? assistantUuid;
|
|
4064
4180
|
protocolIncompleteReason = event.protocolIncompleteReason ?? protocolIncompleteReason;
|
|
@@ -4295,14 +4411,33 @@ export class ResponseEngine {
|
|
|
4295
4411
|
sessionId: session.id,
|
|
4296
4412
|
toolName: event.name,
|
|
4297
4413
|
input: event.input,
|
|
4414
|
+
...(event.callId ? { callId: event.callId } : {}),
|
|
4415
|
+
...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
|
|
4416
|
+
...(session.selfAID ? { agentAid: session.selfAID } : {}),
|
|
4417
|
+
...(permissionMode ? { permissionMode } : {}),
|
|
4298
4418
|
timestamp: Date.now(),
|
|
4299
4419
|
causation,
|
|
4300
4420
|
});
|
|
4421
|
+
const desc = summarizeToolInput(event.name, event.input || {});
|
|
4422
|
+
const pendingInput = { name: event.name, input: event.input || {} };
|
|
4423
|
+
if (event.callId)
|
|
4424
|
+
toolInputByCallId.set(event.callId, pendingInput);
|
|
4425
|
+
if (event.correlationId)
|
|
4426
|
+
toolInputByCorrelationId.set(event.correlationId, pendingInput);
|
|
4427
|
+
else if (event.callId) {
|
|
4428
|
+
// Renderer/event-log normalization uses callId as the fallback
|
|
4429
|
+
// correlation. Keep the same alias here so a result carrying both
|
|
4430
|
+
// fields with that fallback still matches the original call.
|
|
4431
|
+
toolInputByCorrelationId.set(event.callId, pendingInput);
|
|
4432
|
+
}
|
|
4433
|
+
if (!event.callId && !event.correlationId) {
|
|
4434
|
+
const pending = anonymousToolInputsByName.get(event.name) ?? [];
|
|
4435
|
+
pending.push(pendingInput);
|
|
4436
|
+
anonymousToolInputsByName.set(event.name, pending);
|
|
4437
|
+
}
|
|
4301
4438
|
if (!shouldSuppress()) {
|
|
4302
|
-
|
|
4303
|
-
if (event.callId) {
|
|
4439
|
+
if (event.callId)
|
|
4304
4440
|
toolDescByCallId.set(event.callId, desc);
|
|
4305
|
-
}
|
|
4306
4441
|
renderer.addToolCall(event.name, event.input, event.callId, desc, event.turn, event.outputTokens);
|
|
4307
4442
|
}
|
|
4308
4443
|
if (event.callId && isCtlQueueReadCommand(event.name, event.input || {})) {
|
|
@@ -4318,7 +4453,7 @@ export class ResponseEngine {
|
|
|
4318
4453
|
toolInput: event.input || {},
|
|
4319
4454
|
injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
|
|
4320
4455
|
getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
|
|
4321
|
-
isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', session.selfAID),
|
|
4456
|
+
isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
|
|
4322
4457
|
logger,
|
|
4323
4458
|
});
|
|
4324
4459
|
}
|
|
@@ -4344,9 +4479,62 @@ export class ResponseEngine {
|
|
|
4344
4479
|
toolName: event.name,
|
|
4345
4480
|
isError: event.isError,
|
|
4346
4481
|
agentName: agentNameForStats,
|
|
4482
|
+
...(event.callId ? { callId: event.callId } : {}),
|
|
4483
|
+
...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
|
|
4484
|
+
...(session.selfAID ? { agentAid: session.selfAID } : {}),
|
|
4485
|
+
...(permissionMode ? { permissionMode } : {}),
|
|
4347
4486
|
timestamp: Date.now(),
|
|
4348
4487
|
causation,
|
|
4349
4488
|
});
|
|
4489
|
+
// A proactive send gate advances only after the matching command
|
|
4490
|
+
// completed successfully and returned an explicit send receipt.
|
|
4491
|
+
let original;
|
|
4492
|
+
const byCallId = event.callId ? toolInputByCallId.get(event.callId) : undefined;
|
|
4493
|
+
const byCorrelationId = event.correlationId
|
|
4494
|
+
? toolInputByCorrelationId.get(event.correlationId)
|
|
4495
|
+
: undefined;
|
|
4496
|
+
// Both identifiers must describe the same call. This avoids
|
|
4497
|
+
// accepting a stale/replayed correlation as the result of a new
|
|
4498
|
+
// call, or vice versa.
|
|
4499
|
+
const hasBothResultIdentifiers = !!event.callId && !!event.correlationId;
|
|
4500
|
+
if (hasBothResultIdentifiers
|
|
4501
|
+
&& (!byCallId || !byCorrelationId || byCallId !== byCorrelationId)) {
|
|
4502
|
+
logger.warn(`[ResponseEngine] Ignoring mismatched tool result identifiers: session=${session.id}`);
|
|
4503
|
+
}
|
|
4504
|
+
else if (!hasBothResultIdentifiers) {
|
|
4505
|
+
original = byCallId ?? byCorrelationId;
|
|
4506
|
+
}
|
|
4507
|
+
else {
|
|
4508
|
+
original = byCallId;
|
|
4509
|
+
}
|
|
4510
|
+
if (!original && !event.callId && !event.correlationId) {
|
|
4511
|
+
const pending = anonymousToolInputsByName.get(event.name);
|
|
4512
|
+
// Without either identifier, FIFO is unsafe when same-named calls
|
|
4513
|
+
// overlap or complete out of order. Leave the gate unchanged until
|
|
4514
|
+
// a unique pending call can be matched.
|
|
4515
|
+
if (pending?.length === 1) {
|
|
4516
|
+
original = pending[0];
|
|
4517
|
+
anonymousToolInputsByName.delete(event.name);
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
if (original)
|
|
4521
|
+
removePendingToolInput(original);
|
|
4522
|
+
if (original && modeHooks?.mode?.onToolResult) {
|
|
4523
|
+
const proactiveState = modeHooks.state.get('proactive');
|
|
4524
|
+
await modeHooks.mode.onToolResult({
|
|
4525
|
+
session,
|
|
4526
|
+
state: modeHooks.state,
|
|
4527
|
+
toolName: original.name,
|
|
4528
|
+
toolInput: original.input,
|
|
4529
|
+
result: event.result,
|
|
4530
|
+
isError: event.isError,
|
|
4531
|
+
error: event.error,
|
|
4532
|
+
injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
|
|
4533
|
+
getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
|
|
4534
|
+
isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
|
|
4535
|
+
logger,
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4350
4538
|
// 从 tool_use 阶段缓存的描述中回溯
|
|
4351
4539
|
const cachedDesc = event.callId ? toolDescByCallId.get(event.callId) : undefined;
|
|
4352
4540
|
if (event.isError && !shouldSuppress()) {
|
|
@@ -4360,6 +4548,12 @@ export class ResponseEngine {
|
|
|
4360
4548
|
renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc);
|
|
4361
4549
|
}
|
|
4362
4550
|
}
|
|
4551
|
+
// 非终态运行时告警走 status notify,不进入 activity/thought 历史。
|
|
4552
|
+
if (event.type === 'notice' && event.severity === 'warn') {
|
|
4553
|
+
await renderer.sendWarningStatus(event.message, event.subtype, {
|
|
4554
|
+
...(event.missingItems !== undefined && { missingItems: event.missingItems }),
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4363
4557
|
// 运行时错误(Codex: turn.failed / item error)
|
|
4364
4558
|
if (event.type === 'error') {
|
|
4365
4559
|
logger.warn(`[ResponseEngine] error event: ${event.errorType}: ${event.error}`);
|