evolcore 0.0.26 → 0.0.27
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 +25 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +63 -26
- package/dist/agents/codex-runner.js +316 -411
- package/dist/agents/ecagent-runner.js +170 -25
- package/dist/agents/gemini-runner.js +6 -0
- package/dist/agents/runner-types.js +50 -2
- package/dist/aun/aid/agentmd.js +73 -31
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +180 -1
- package/dist/aun/aid/identity.js +3 -3
- package/dist/aun/group/history.js +303 -0
- package/dist/aun/group/identity.js +43 -0
- package/dist/aun/group/index.js +5 -0
- package/dist/aun/{msg/group.js → group/operations.js} +161 -38
- package/dist/aun/{menu-client.js → menu/client.js} +12 -7
- package/dist/aun/menu/index.js +2 -0
- package/dist/aun/menu/transport.js +49 -0
- package/dist/aun/msg/index.js +2 -1
- package/dist/aun/msg/managed-operation.js +84 -18
- package/dist/aun/msg/mention-schema.js +51 -0
- package/dist/aun/msg/p2p.js +13 -8
- package/dist/aun/msg/upload.js +43 -88
- package/dist/aun/outbox.js +29 -7
- package/dist/aun/rpc/client.js +1 -0
- package/dist/aun/storage/client.js +39 -50
- package/dist/channels/aun.js +522 -233
- package/dist/channels/daemon.js +10 -6
- package/dist/cli/agent-command.js +108 -6
- package/dist/cli/agent-create.js +515 -2
- package/dist/cli/agent.js +109 -8
- package/dist/cli/aun-commands.js +191 -42
- package/dist/cli/config.js +8 -5
- package/dist/cli/contact.js +2 -2
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +35 -23
- package/dist/cli/diag.js +2 -2
- package/dist/cli/fs-command.js +3 -3
- package/dist/cli/handoff-command.js +6 -6
- package/dist/cli/index.js +8 -2
- package/dist/cli/init.js +34 -1
- package/dist/cli/managed-command-guard.js +2 -1
- package/dist/cli/menu-client.js +9 -5
- package/dist/cli/model.js +3 -3
- package/dist/cli/queue-command.js +2 -2
- package/dist/cli/response.js +2 -2
- package/dist/cli/restart-monitor.js +16 -2
- package/dist/cli/task-command.js +59 -0
- package/dist/cli/task-context.js +97 -49
- package/dist/cli/trigger-command.js +20 -10
- package/dist/config/builtin-roles.js +10 -0
- package/dist/config/config-field-policy.js +11 -0
- package/dist/config/config-manager.js +32 -4
- package/dist/config/config-operation-service.js +20 -5
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/contact-book.js +1 -1
- package/dist/config/contact-operation-service.js +1 -1
- package/dist/config/peer-role-resolver.js +1 -1
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config-store.js +1 -1
- package/dist/core/agent-application-service.js +5 -0
- package/dist/core/audit/event-key.js +17 -2
- package/dist/core/audit/log-integrity.js +34 -8
- package/dist/core/auth/agent-delegation.js +48 -14
- package/dist/core/auth/auth-gateway.js +2 -0
- package/dist/core/auth/authorization-audit.js +34 -38
- package/dist/core/auth/operation-authorizer.js +67 -5
- package/dist/core/auth/operation-catalog.js +110 -0
- package/dist/core/auth/remote-access-store.js +115 -9
- package/dist/core/bootstrap-messages.js +66 -7
- package/dist/core/bootstrap-service.js +110 -19
- package/dist/core/command/agent-control.js +88 -7
- package/dist/core/command/agent-storage-menu.js +443 -0
- package/dist/core/command/cli-intent-parser.js +19 -0
- package/dist/core/command/command-handler.js +76 -39
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/group-menu.js +7 -2
- package/dist/core/command/menu-catalog.js +34 -4
- package/dist/core/command/menu-handler.js +348 -77
- package/dist/core/command/menu-protocol.js +40 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +37 -22
- package/dist/core/event-catalog.js +4 -0
- package/dist/core/evolagent-registry.js +28 -2
- package/dist/core/group/invite-service.js +924 -0
- package/dist/core/interaction-router.js +22 -8
- package/dist/core/message/im-renderer.js +76 -15
- package/dist/core/message/message-bridge.js +94 -11
- package/dist/core/message/message-log.js +5 -3
- package/dist/core/message/message-queue.js +73 -41
- package/dist/core/message/response-engine.js +709 -117
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +61 -29
- package/dist/core/permission/protected-paths.js +134 -16
- package/dist/core/permission/sandbox-runtime.js +72 -3
- package/dist/core/permission/tool-error-code.js +15 -0
- package/dist/core/permission/tool-policy.js +7 -7
- package/dist/core/relation/peer-identity.js +57 -6
- package/dist/core/relation/shared-data.js +106 -0
- package/dist/core/role/runtime-policy.js +64 -0
- package/dist/core/runtime-lock.js +10 -26
- package/dist/core/session/runner-task-history.js +170 -0
- package/dist/core/session/session-manager.js +34 -6
- package/dist/core/session/session-mapper.js +41 -0
- package/dist/core/session/session-turn-coordinator.js +37 -7
- package/dist/core/session/session-turns.js +48 -7
- package/dist/core/task/task-recovery-service.js +176 -0
- package/dist/core/task/task-resume-operation.js +32 -0
- package/dist/eck/group-rules-sync.js +5 -3
- package/dist/eck/kit-renderer.js +1 -0
- package/dist/index.js +308 -54
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +2 -0
- package/dist/trigger/event-source.js +19 -0
- package/dist/trigger/feedback.js +233 -40
- package/dist/trigger/parser.js +55 -5
- package/dist/trigger/patch.js +9 -0
- package/dist/trigger/scheduler.js +30 -4
- package/dist/trigger/validation.js +20 -3
- package/dist/utils/ecweb-utils.js +1 -0
- package/dist/utils/error-utils.js +40 -2
- package/dist/utils/logger.js +23 -13
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- package/ecagent/dist/harness/agent-harness.js +3 -0
- package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
- package/kits/docs/INDEX.md +0 -6
- package/kits/docs/channels/aun.md +15 -2
- package/kits/docs/channels/feishu.md +1 -1
- package/kits/docs/context-assembly.md +29 -17
- package/kits/docs/evolcore/INDEX.md +8 -3
- package/kits/docs/evolcore/agent-create.md +39 -7
- package/kits/docs/evolcore/agent.md +33 -1
- package/kits/docs/evolcore/aid.md +9 -1
- package/kits/docs/evolcore/config.md +40 -1
- package/kits/docs/evolcore/group-collaboration.md +1 -1
- package/kits/docs/evolcore/group.md +117 -2
- package/kits/docs/evolcore/menu-client.md +16 -0
- package/kits/docs/evolcore/msg.md +3 -0
- package/kits/docs/evolcore/storage.md +3 -0
- package/kits/docs/evolcore/task.md +14 -0
- package/kits/docs/evolcore/trigger.md +51 -3
- package/kits/docs/venues/group.md +5 -0
- package/kits/eck_manifest.json +27 -0
- package/kits/rules/01-overview.md +11 -7
- package/kits/rules/04-relation.md +2 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +6 -4
- package/kits/schemas/agent-config.schema.12.json +25 -1
- package/kits/schemas/daemon.schema.6.json +2 -2
- package/kits/schemas/defaults.schema.5.json +1 -1
- package/kits/schemas/relation-config.schema.1.json +1 -1
- package/kits/schemas/relation-config.schema.5.json +1 -1
- package/kits/schemas/role-config.schema.1.json +14 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/baseagent.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +26 -9
- package/kits/templates/system-fragments/channel.md +8 -44
- package/kits/templates/system-fragments/commands.md +10 -2
- package/kits/templates/system-fragments/group-unread-history.md +7 -0
- package/kits/templates/system-fragments/reply.md +56 -0
- package/package.json +7 -3
- package/dist/aun/group-identity.js +0 -9
- /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
- /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { initializationBlocksTasks } from '../../cli/agent-create.js';
|
|
1
2
|
import path from 'path';
|
|
2
3
|
import fs from 'fs';
|
|
3
4
|
import os from 'os';
|
|
4
5
|
import crypto from 'crypto';
|
|
5
|
-
import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken } from '../../agents/runner-types.js';
|
|
6
|
+
import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken, formatAsyncQuestionAnswer, parseAsyncUserQuestions, } from '../../agents/runner-types.js';
|
|
7
|
+
import { loadedBaseagentIds } from '../../agents/baseagent-availability.js';
|
|
8
|
+
import { evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission } from './message-admission.js';
|
|
9
|
+
import { authorizeAccess, authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
6
10
|
import { buildTaskRuntimeEnv, ensureRuntimeLockDir, ensureSessionRuntimeDir } from '../../cli/task-context.js';
|
|
7
11
|
import { IMRenderer } from './im-renderer.js';
|
|
8
12
|
import { createSendFileMarkerPattern } from './file-markers.js';
|
|
@@ -10,11 +14,11 @@ import { createTextInferenceProvider } from '../inference/text-inference.js';
|
|
|
10
14
|
import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
11
15
|
import { logger } from '../../utils/logger.js';
|
|
12
16
|
import { isHostChinese } from '../../utils/locale.js';
|
|
13
|
-
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
|
|
17
|
+
import { ExecutionTimeoutError, OrphanedTargetSessionError, isExecutionFailure, getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
|
|
14
18
|
import { readEvolcoreRuntimeFlavor, tagErrorTextWithEvolcoreVersion } from '../../utils/evolcore-version.js';
|
|
15
19
|
import { describeProactiveSendCommandFailure, isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
|
|
16
|
-
import {
|
|
17
|
-
import { agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
20
|
+
import { summarizeToolInputForDisplay } from '../../utils/tool-summary.js';
|
|
21
|
+
import { agentDataDir, agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
18
22
|
import { renderKitSections } from '../../eck/kit-renderer.js';
|
|
19
23
|
import { renderMessageBody } from '../../eck/message-renderer.js';
|
|
20
24
|
import { syncGroupRulesContext } from '../../eck/group-rules-sync.js';
|
|
@@ -22,8 +26,9 @@ import { consumeHints, hintsToSubMessages, composeHintFallback } from './pending
|
|
|
22
26
|
import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
|
|
23
27
|
import { recordCausationLink, recordCausationSpan } from '../causation/audit.js';
|
|
24
28
|
import { normalizeBaseagent } from '../../agents/baseagent.js';
|
|
29
|
+
import { renderActionAsText } from '../interaction-router.js';
|
|
25
30
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
26
|
-
import { buildEnvelope, replyContextFromSession, withEnvelopeReplyContext } from './message-utils.js';
|
|
31
|
+
import { buildEnvelope, isInteractionSendAccepted, replyContextFromSession, sendInteractionPayload, withEnvelopeReplyContext } from './message-utils.js';
|
|
27
32
|
import { isSystemOrServicePeer, resolveChatMode } from './peer-mode.js';
|
|
28
33
|
import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
|
|
29
34
|
import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
|
|
@@ -52,7 +57,290 @@ import { classifyToolErrorCode, normalizeToolErrorCode } from '../permission/too
|
|
|
52
57
|
import { normalizeExecutionPermissionMode } from '../permission/mode.js';
|
|
53
58
|
import { buildToolLifecycleEventKey } from '../audit/event-key.js';
|
|
54
59
|
import { isFullAccessEnabled, loadDaemonConfig } from '../../config-store.js';
|
|
60
|
+
import { assertAgentPolicyAuthorization } from '../role/runtime-policy.js';
|
|
55
61
|
import { auditFullAccessEvent } from '../auth/authorization-audit.js';
|
|
62
|
+
import { TaskRecoveryService, isTaskRecoveryCurrent } from '../task/task-recovery-service.js';
|
|
63
|
+
const QUESTION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
64
|
+
/** Pending async questions outlive the originating turn, but never change its backend binding. */
|
|
65
|
+
export class AsyncQuestionService {
|
|
66
|
+
deps;
|
|
67
|
+
registered = new Map();
|
|
68
|
+
deliveries = new Map();
|
|
69
|
+
constructor(deps) {
|
|
70
|
+
this.deps = deps;
|
|
71
|
+
}
|
|
72
|
+
directory(selfAid) {
|
|
73
|
+
return this.deps.directory?.(selfAid) ?? path.join(agentDataDir(selfAid), 'async-questions');
|
|
74
|
+
}
|
|
75
|
+
file(selfAid, id) {
|
|
76
|
+
if (!/^aq-[a-f0-9]{40}$/.test(id))
|
|
77
|
+
throw new Error('Invalid async question id');
|
|
78
|
+
return path.join(this.directory(selfAid), `${id}.json`);
|
|
79
|
+
}
|
|
80
|
+
save(record) {
|
|
81
|
+
fs.mkdirSync(this.directory(record.selfAid), { recursive: true, mode: 0o700 });
|
|
82
|
+
const destination = this.file(record.selfAid, record.id);
|
|
83
|
+
const temporary = `${destination}.tmp`;
|
|
84
|
+
const fd = fs.openSync(temporary, 'w', 0o600);
|
|
85
|
+
try {
|
|
86
|
+
fs.writeFileSync(fd, JSON.stringify(record));
|
|
87
|
+
fs.fsyncSync(fd);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
fs.closeSync(fd);
|
|
91
|
+
}
|
|
92
|
+
// Replace atomically, without unlinking the previous durable state first.
|
|
93
|
+
fs.renameSync(temporary, destination);
|
|
94
|
+
}
|
|
95
|
+
read(selfAid, id) {
|
|
96
|
+
try {
|
|
97
|
+
const value = JSON.parse(fs.readFileSync(this.file(selfAid, id), 'utf8'));
|
|
98
|
+
if (value.version !== 1 || value.selfAid !== selfAid || value.id !== id
|
|
99
|
+
|| !value.sessionId || !value.providerThreadId || !value.itemId
|
|
100
|
+
|| !value.origin?.peerId || !value.channelKey || !Number.isFinite(value.expiresAt))
|
|
101
|
+
return undefined;
|
|
102
|
+
parseAsyncUserQuestions([value.question]);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error.code !== 'ENOENT')
|
|
107
|
+
throw error;
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async present(session, request, message, adapter, envelope) {
|
|
112
|
+
if (!session.selfAID || !message.peerId)
|
|
113
|
+
throw new Error('Async question requires an authenticated route');
|
|
114
|
+
const questions = parseAsyncUserQuestions(request.questions);
|
|
115
|
+
const records = [];
|
|
116
|
+
for (const [index, question] of questions.entries()) {
|
|
117
|
+
const hash = crypto.createHash('sha256').update(JSON.stringify([
|
|
118
|
+
session.selfAID, session.id, request.providerThreadId, request.itemId, index,
|
|
119
|
+
])).digest('hex').slice(0, 40);
|
|
120
|
+
const id = `aq-${hash}`;
|
|
121
|
+
const existing = this.read(session.selfAID, id);
|
|
122
|
+
if (existing && existing.status !== 'pending')
|
|
123
|
+
continue;
|
|
124
|
+
const replyContext = {
|
|
125
|
+
delivery: envelope.delivery ?? envelope.replyContext?.delivery,
|
|
126
|
+
threadId: session.threadId || undefined,
|
|
127
|
+
metadata: {
|
|
128
|
+
encrypted: message.encrypted ?? envelope.replyContext?.metadata?.encrypted,
|
|
129
|
+
chatmode: envelope.chatmode,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const record = existing ?? {
|
|
133
|
+
version: 1, id, sessionId: session.id, selfAid: session.selfAID,
|
|
134
|
+
providerThreadId: request.providerThreadId, providerTurnId: request.providerTurnId,
|
|
135
|
+
itemId: request.itemId, question,
|
|
136
|
+
origin: {
|
|
137
|
+
channel: message.channel, channelType: session.channelType,
|
|
138
|
+
channelId: session.channelId, peerId: message.peerId, peerType: message.peerType,
|
|
139
|
+
chatType: session.chatType === 'group' ? 'group' : 'private',
|
|
140
|
+
threadId: session.threadId || undefined, selfAID: session.selfAID,
|
|
141
|
+
},
|
|
142
|
+
envelope: { ...envelope, sessionId: session.id, replyContext, delivery: replyContext.delivery },
|
|
143
|
+
channelKey: adapter.channelKey, createdAt: Date.now(), expiresAt: Date.now() + QUESTION_TTL_MS,
|
|
144
|
+
status: 'pending',
|
|
145
|
+
};
|
|
146
|
+
if (!existing)
|
|
147
|
+
this.save(record);
|
|
148
|
+
records.push(record);
|
|
149
|
+
}
|
|
150
|
+
// Persist the complete batch before dispatch; one failed card must not lose the other questions.
|
|
151
|
+
const errors = [];
|
|
152
|
+
for (const record of records) {
|
|
153
|
+
try {
|
|
154
|
+
await this.deliver(record, adapter);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
errors.push(error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (errors.length)
|
|
161
|
+
throw new AggregateError(errors, 'Async question delivery failed; pending questions retained');
|
|
162
|
+
}
|
|
163
|
+
async restore(adapter, selfAid) {
|
|
164
|
+
let names;
|
|
165
|
+
try {
|
|
166
|
+
names = fs.readdirSync(this.directory(selfAid));
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error.code === 'ENOENT')
|
|
170
|
+
return;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
for (const name of names) {
|
|
174
|
+
if (!/^aq-[a-f0-9]{40}\.json$/.test(name))
|
|
175
|
+
continue;
|
|
176
|
+
try {
|
|
177
|
+
const record = this.read(selfAid, name.slice(0, -5));
|
|
178
|
+
if (!record || record.channelKey !== adapter.channelKey)
|
|
179
|
+
continue;
|
|
180
|
+
if (this.registered.get(record.id) === adapter)
|
|
181
|
+
continue;
|
|
182
|
+
if (record.expiresAt <= Date.now()) {
|
|
183
|
+
fs.unlinkSync(this.file(selfAid, record.id));
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (record.status === 'submitting') {
|
|
187
|
+
// A crash may have happened after Codex accepted the steer. Never replay it blindly.
|
|
188
|
+
record.status = 'uncertain';
|
|
189
|
+
record.reason = 'restart_during_submission';
|
|
190
|
+
this.save(record);
|
|
191
|
+
}
|
|
192
|
+
if (record.status === 'pending')
|
|
193
|
+
await this.deliver(record, adapter);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
logger.warn(`[AsyncQuestions] restore failed: id=${name} error=${String(error)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async validSession(record) {
|
|
201
|
+
const session = await this.deps.getSession(record.sessionId);
|
|
202
|
+
if (!session || session.baseagent !== 'codex' || session.selfAID !== record.selfAid
|
|
203
|
+
|| session.agentSessionId !== record.providerThreadId
|
|
204
|
+
|| session.channel !== record.origin.channel
|
|
205
|
+
|| session.channelType !== record.origin.channelType
|
|
206
|
+
|| session.channelId !== record.origin.channelId
|
|
207
|
+
|| (session.threadId || '') !== (record.origin.threadId || '')
|
|
208
|
+
|| !(await this.deps.authorize(record, session)))
|
|
209
|
+
return undefined;
|
|
210
|
+
return session;
|
|
211
|
+
}
|
|
212
|
+
/** Recheck persisted answers after queue delays/restarts, immediately before a provider run. */
|
|
213
|
+
async validateQueuedAnswer(session, answer) {
|
|
214
|
+
if (!session.selfAID)
|
|
215
|
+
return false;
|
|
216
|
+
const record = this.read(session.selfAID, answer.requestId);
|
|
217
|
+
return !!record && record.sessionId === session.id
|
|
218
|
+
&& ['submitting', 'answered', 'uncertain'].includes(record.status)
|
|
219
|
+
&& record.providerThreadId === answer.providerThreadId
|
|
220
|
+
&& record.itemId === answer.questionItemId && record.question.title === answer.question
|
|
221
|
+
&& record.answer === answer.answer && !!(await this.validSession(record));
|
|
222
|
+
}
|
|
223
|
+
async deliver(record, adapter) {
|
|
224
|
+
if (this.registered.get(record.id) === adapter)
|
|
225
|
+
return;
|
|
226
|
+
// A channel reload replaces its callback/card registry. Rebind pending questions to the new adapter.
|
|
227
|
+
if (this.registered.has(record.id))
|
|
228
|
+
await this.deps.router.cancel(record.id, 'daemon_restart');
|
|
229
|
+
const previous = this.deliveries.get(record.id);
|
|
230
|
+
if (previous)
|
|
231
|
+
return previous;
|
|
232
|
+
const delivery = this.deliverOnce(record, adapter);
|
|
233
|
+
this.deliveries.set(record.id, delivery);
|
|
234
|
+
try {
|
|
235
|
+
await delivery;
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
this.deliveries.delete(record.id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async deliverOnce(record, adapter) {
|
|
242
|
+
if (record.expiresAt <= Date.now() || !(await this.validSession(record))) {
|
|
243
|
+
record.status = 'cancelled';
|
|
244
|
+
record.reason = 'expired_or_binding_changed';
|
|
245
|
+
this.save(record);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const options = record.question.options ?? [];
|
|
249
|
+
const interaction = {
|
|
250
|
+
type: 'interaction', id: record.id, sessionId: record.sessionId, channelId: record.origin.channelId,
|
|
251
|
+
initiatorId: record.origin.peerId, blocking: false, expiresAt: record.expiresAt,
|
|
252
|
+
kind: {
|
|
253
|
+
kind: 'action', title: '请补充信息', body: record.question.title, bodyFormat: 'plain',
|
|
254
|
+
buttons: options.map((label, index) => ({ key: `opt-${index}`, label })),
|
|
255
|
+
allowCustomInput: true,
|
|
256
|
+
},
|
|
257
|
+
fallback: {
|
|
258
|
+
command: 'ask',
|
|
259
|
+
buttonArgMap: Object.fromEntries(options.map((_, index) => [`opt-${index}`, `${record.id} ${index + 1}`])),
|
|
260
|
+
acceptFreeText: true, freeTextHint: `回复 /ask ${record.id} <回答>`,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
// Register before dispatch: a fast reply must not race card delivery.
|
|
264
|
+
this.registered.set(record.id, adapter);
|
|
265
|
+
this.deps.router.register(record.id, record.sessionId, async (action, values) => {
|
|
266
|
+
const current = this.read(record.selfAid, record.id);
|
|
267
|
+
if (!current || current.status !== 'pending' || current.expiresAt <= Date.now()) {
|
|
268
|
+
await this.deps.router.cancel(record.id, 'expired_or_resolved');
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
const answer = this.answerText(current.question, action, values);
|
|
272
|
+
if (!answer)
|
|
273
|
+
return false;
|
|
274
|
+
const session = await this.validSession(current);
|
|
275
|
+
if (!session) {
|
|
276
|
+
await this.deps.router.cancel(record.id, 'binding_or_authorization_changed');
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
current.answer = answer;
|
|
280
|
+
current.status = 'submitting';
|
|
281
|
+
this.save(current);
|
|
282
|
+
try {
|
|
283
|
+
await this.deps.submit(current, session, {
|
|
284
|
+
requestId: current.id, providerThreadId: current.providerThreadId,
|
|
285
|
+
questionItemId: current.itemId, question: current.question.title, answer,
|
|
286
|
+
});
|
|
287
|
+
current.status = 'answered';
|
|
288
|
+
this.save(current);
|
|
289
|
+
this.registered.delete(record.id);
|
|
290
|
+
logger.info(`[AsyncQuestions] answer accepted: session=${record.sessionId} request=${record.id}`);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
current.status = 'uncertain';
|
|
295
|
+
current.reason = 'submission_failed';
|
|
296
|
+
this.save(current);
|
|
297
|
+
logger.warn(`[AsyncQuestions] answer submission failed: request=${record.id} error=${String(error)}`);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}, {
|
|
301
|
+
initiatorId: record.origin.peerId, initiatorChannelKey: adapter.channelKey,
|
|
302
|
+
blocking: false, retainOnRejected: true, fallbackCommand: 'ask', timeoutMs: record.expiresAt - Date.now(),
|
|
303
|
+
onCancel: async (reason) => {
|
|
304
|
+
this.registered.delete(record.id);
|
|
305
|
+
if (reason === 'daemon_restart')
|
|
306
|
+
return;
|
|
307
|
+
const current = this.read(record.selfAid, record.id);
|
|
308
|
+
if (current?.status === 'pending') {
|
|
309
|
+
current.status = 'cancelled';
|
|
310
|
+
current.reason = reason;
|
|
311
|
+
this.save(current);
|
|
312
|
+
}
|
|
313
|
+
await adapter.invalidateInteraction?.(record.id, reason);
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
try {
|
|
317
|
+
const receipt = await sendInteractionPayload(adapter, {
|
|
318
|
+
...record.envelope, operationId: `${record.id}-${Date.now()}`,
|
|
319
|
+
}, interaction, renderActionAsText(interaction), record.envelope.replyContext);
|
|
320
|
+
if (!isInteractionSendAccepted(receipt))
|
|
321
|
+
throw new Error('Async question card was not accepted for delivery');
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
// Keep the durable pending record for a later channel reconnect/daemon restore.
|
|
325
|
+
await this.deps.router.cancel(record.id, 'daemon_restart');
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
answerText(question, action, values) {
|
|
330
|
+
if (action === 'cancel' || action === 'dismiss' || values?.behavior === 'dismiss')
|
|
331
|
+
return undefined;
|
|
332
|
+
let answer;
|
|
333
|
+
if (action === '_custom_input')
|
|
334
|
+
answer = typeof values?.custom_text === 'string' ? values.custom_text.trim() : undefined;
|
|
335
|
+
else if (/^opt-\d+$/.test(action))
|
|
336
|
+
answer = question.options?.[Number(action.slice(4))];
|
|
337
|
+
else if (question.options?.length && /^\d+$/.test(action))
|
|
338
|
+
answer = question.options[Number(action) - 1];
|
|
339
|
+
else
|
|
340
|
+
answer = action.trim();
|
|
341
|
+
return answer && answer.length <= 65_536 ? answer : undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
56
344
|
const INBOUND_MESSAGE_LOG_TEXT_MAX_LENGTH = 240;
|
|
57
345
|
export function formatInboundMessageLogText(text) {
|
|
58
346
|
const singleLine = text.replace(/\s+/gu, ' ').trim();
|
|
@@ -519,12 +807,15 @@ export class ResponseEngine {
|
|
|
519
807
|
primaryRunnerKey;
|
|
520
808
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
521
809
|
timeoutErrors = new Map();
|
|
810
|
+
activeTimeoutHandlers = new Map();
|
|
522
811
|
/** sessionId → 已成功的模型降级状态(进程重启清零) */
|
|
523
812
|
modelFallbackMap = new Map();
|
|
524
813
|
/** sessionId → interruptible task-level API retry delays. */
|
|
525
814
|
retryDelayWaiters = new Map();
|
|
526
815
|
interactionRouter;
|
|
816
|
+
asyncQuestions;
|
|
527
817
|
messageQueue;
|
|
818
|
+
taskRecoveryService;
|
|
528
819
|
handoffRuntime;
|
|
529
820
|
/** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
|
|
530
821
|
activeMonitors = new Map();
|
|
@@ -598,10 +889,7 @@ export class ResponseEngine {
|
|
|
598
889
|
return createTextInferenceProvider(baseagent, config);
|
|
599
890
|
}
|
|
600
891
|
getAvailableBaseagentsForOwner(evolName) {
|
|
601
|
-
|
|
602
|
-
return [...this.agentMap.keys()]
|
|
603
|
-
.filter(key => key.startsWith(prefix))
|
|
604
|
-
.map(key => key.slice(prefix.length));
|
|
892
|
+
return loadedBaseagentIds(this.agentMap.keys(), evolName);
|
|
605
893
|
}
|
|
606
894
|
async sendBaseagentMismatch(channelKey, channelId, session, error, replyContext) {
|
|
607
895
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
@@ -742,6 +1030,12 @@ export class ResponseEngine {
|
|
|
742
1030
|
}
|
|
743
1031
|
setInteractionRouter(router) {
|
|
744
1032
|
this.interactionRouter = router;
|
|
1033
|
+
this.asyncQuestions = new AsyncQuestionService({
|
|
1034
|
+
router,
|
|
1035
|
+
getSession: id => this.sessionManager.getSessionById(id),
|
|
1036
|
+
authorize: async (record, session) => this.authorizeAsyncQuestion(record, session),
|
|
1037
|
+
submit: (record, session, answer) => this.submitAsyncQuestionAnswer(record, session, answer),
|
|
1038
|
+
});
|
|
745
1039
|
// 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
|
|
746
1040
|
// 避免把「正在等用户点按钮」误判为「任务卡死」而中断任务。
|
|
747
1041
|
router.setWaitHooks({
|
|
@@ -752,9 +1046,118 @@ export class ResponseEngine {
|
|
|
752
1046
|
this.activeMonitors.get(sessionId)?.resume();
|
|
753
1047
|
},
|
|
754
1048
|
});
|
|
1049
|
+
for (const { adapter } of this.channels.values())
|
|
1050
|
+
this.restoreAsyncQuestions(adapter);
|
|
755
1051
|
}
|
|
756
1052
|
setMessageQueue(queue) {
|
|
757
1053
|
this.messageQueue = queue;
|
|
1054
|
+
this.taskRecoveryService = new TaskRecoveryService({
|
|
1055
|
+
sessions: this.sessionManager,
|
|
1056
|
+
queue,
|
|
1057
|
+
getRunner: (session) => this.getAgent(session.metadata?.channelKey || session.channel, session.baseagent, session.selfAID),
|
|
1058
|
+
resumePause: (sessionId) => this.resumeSession(sessionId),
|
|
1059
|
+
hasChannel: (session) => !!this.resolveChannelInfo(session.metadata?.channelKey || session.channel),
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
async recoverTask(request) {
|
|
1063
|
+
if (!this.taskRecoveryService) {
|
|
1064
|
+
return { ok: false, status: 'not_recoverable', sessionId: request.sessionId, message: '消息队列尚未初始化' };
|
|
1065
|
+
}
|
|
1066
|
+
return this.taskRecoveryService.resume(request);
|
|
1067
|
+
}
|
|
1068
|
+
authorizeAsyncQuestion(record, session) {
|
|
1069
|
+
const origin = record.origin;
|
|
1070
|
+
const owner = this.agentRegistry?.resolveByChannel(origin.channel);
|
|
1071
|
+
if (owner?.aid !== record.selfAid)
|
|
1072
|
+
return false;
|
|
1073
|
+
const chatType = origin.chatType === 'group' ? 'group' : 'private';
|
|
1074
|
+
const channelType = origin.channelType || record.channelKey.split('#')[0];
|
|
1075
|
+
const preflight = evaluateInboundAdmissionPreflight({
|
|
1076
|
+
selfAid: record.selfAid, channelKey: record.channelKey, channelType, chatType, actorId: origin.peerId,
|
|
1077
|
+
});
|
|
1078
|
+
if (!preflight.allow || !evaluateOrdinaryInboundAdmission(preflight.context).allow)
|
|
1079
|
+
return false;
|
|
1080
|
+
const identity = this.inferDirectSessionIdentity({ ...origin, content: '' }, owner);
|
|
1081
|
+
if (!identity || identity.role === 'none')
|
|
1082
|
+
return false;
|
|
1083
|
+
return authorizeAccess(buildAuthSubject({
|
|
1084
|
+
selfAid: record.selfAid, channel: record.channelKey, channelType, chatType,
|
|
1085
|
+
actorId: origin.peerId, channelId: session.channelId, conversationId: session.channelId, identity,
|
|
1086
|
+
})).allow;
|
|
1087
|
+
}
|
|
1088
|
+
async submitAsyncQuestionAnswer(record, session, answer) {
|
|
1089
|
+
const runner = this.getAgent(record.origin.channel, 'codex', record.selfAid);
|
|
1090
|
+
if (!runner.steerAsyncQuestionAnswer)
|
|
1091
|
+
throw new Error('Codex async input is unavailable');
|
|
1092
|
+
if (await runner.steerAsyncQuestionAnswer(session.id, answer) === 'accepted')
|
|
1093
|
+
return;
|
|
1094
|
+
if (!this.messageQueue)
|
|
1095
|
+
throw new Error('Async question resume queue is unavailable');
|
|
1096
|
+
// The queue owns idle-thread restoration and a fresh task/permission context.
|
|
1097
|
+
// Keep the exact Session/backend identity; never create a new topic for an old card.
|
|
1098
|
+
await this.messageQueue.enqueuePersisted(session.id, {
|
|
1099
|
+
...record.origin, content: formatAsyncQuestionAnswer(answer),
|
|
1100
|
+
asyncQuestionAnswer: answer, boundSessionId: session.id, baseagent: 'codex',
|
|
1101
|
+
source: 'card-trigger', messageId: `async-answer-${record.id}`,
|
|
1102
|
+
timestamp: Date.now(), receivedAt: Date.now(),
|
|
1103
|
+
encrypted: record.envelope.replyContext?.metadata?.encrypted,
|
|
1104
|
+
replyContext: record.envelope.replyContext,
|
|
1105
|
+
}, session.projectPath, {
|
|
1106
|
+
interruptible: false, interruptSamePeer: false,
|
|
1107
|
+
agentName: this.agentRegistry?.resolveByChannel(record.origin.channel)?.name,
|
|
1108
|
+
selfAID: record.selfAid, sessionKeyField: session.sessionKey,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
/** Read the exact attempt's lifecycle without touching any newer task. */
|
|
1112
|
+
async inspectTargetExecution(sessionId, attemptId, selfAid) {
|
|
1113
|
+
const session = await this.sessionManager.getSessionById(sessionId);
|
|
1114
|
+
if (!session || session.deletedAt || session.selfAID !== selfAid)
|
|
1115
|
+
return { state: 'missing' };
|
|
1116
|
+
const state = this.turnCoordinator.current(session);
|
|
1117
|
+
const terminal = state.lastTerminal;
|
|
1118
|
+
const matches = (context) => (context?.triggerAttemptId ?? context?.recovery?.attemptId) === attemptId;
|
|
1119
|
+
if (terminal && matches(terminal.recoveryContext)) {
|
|
1120
|
+
return { state: 'terminal', taskId: terminal.taskId, generation: terminal.generation,
|
|
1121
|
+
outcome: terminal.status === 'committed' ? 'completed'
|
|
1122
|
+
: ['cancelled', 'superseded', 'interrupted'].includes(terminal.status) ? 'skipped' : 'failed',
|
|
1123
|
+
reason: terminal.reason };
|
|
1124
|
+
}
|
|
1125
|
+
const queued = this.messageQueue?.getMessageState(sessionId, attemptId, selfAid);
|
|
1126
|
+
if (queued?.state === 'queued')
|
|
1127
|
+
return { state: 'live' };
|
|
1128
|
+
const active = state.active;
|
|
1129
|
+
if (active && matches(active.recoveryContext)) {
|
|
1130
|
+
const identity = { taskId: active.taskId, generation: active.generation };
|
|
1131
|
+
if (active.status === 'waiting_interaction' || active.status === 'interrupting'
|
|
1132
|
+
|| active.interactions.some(item => !item.settlement)
|
|
1133
|
+
|| this.isPauseRequested(sessionId) || this.retryDelayWaiters.has(sessionId))
|
|
1134
|
+
return { state: 'live', ...identity };
|
|
1135
|
+
const runner = this.getAgent(session.metadata?.channelKey || session.channel, session.baseagent, selfAid);
|
|
1136
|
+
return { state: runner.hasActiveStream(sessionId) ? 'live' : 'missing', ...identity,
|
|
1137
|
+
hasTerminalOwner: this.activeTimeoutHandlers.has(sessionId) };
|
|
1138
|
+
}
|
|
1139
|
+
// Never adopt or interrupt a newer task to clean up an older attempt.
|
|
1140
|
+
if (active) {
|
|
1141
|
+
return { state: 'terminal', outcome: 'skipped', reason: 'stale_generation',
|
|
1142
|
+
taskId: active.taskId, generation: active.generation };
|
|
1143
|
+
}
|
|
1144
|
+
if (queued?.state === 'active') {
|
|
1145
|
+
return { state: 'missing', hasTerminalOwner: this.activeTimeoutHandlers.has(sessionId) };
|
|
1146
|
+
}
|
|
1147
|
+
if (queued?.state === 'missing' && (active || terminal)) {
|
|
1148
|
+
const current = active ?? terminal;
|
|
1149
|
+
return { state: 'terminal', outcome: 'skipped', reason: 'stale_generation',
|
|
1150
|
+
taskId: current.taskId, generation: current.generation };
|
|
1151
|
+
}
|
|
1152
|
+
return { state: 'missing' };
|
|
1153
|
+
}
|
|
1154
|
+
/** Outer execution deadlines use the same terminal owner as runner idle timeouts. */
|
|
1155
|
+
async timeoutSession(sessionId, error) {
|
|
1156
|
+
const timeout = this.activeTimeoutHandlers.get(sessionId);
|
|
1157
|
+
if (timeout)
|
|
1158
|
+
await timeout(error);
|
|
1159
|
+
else
|
|
1160
|
+
await this.interruptSession(sessionId, error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout');
|
|
758
1161
|
}
|
|
759
1162
|
async interruptSession(sessionId, reason) {
|
|
760
1163
|
this.clearPauseSession(sessionId);
|
|
@@ -808,7 +1211,7 @@ export class ResponseEngine {
|
|
|
808
1211
|
// TimeoutController owns timeout delivery (including user-facing timeout
|
|
809
1212
|
// details) and already guarantees a single terminal status. Sending it
|
|
810
1213
|
// here as well would race that barrier and duplicate status.timeout.
|
|
811
|
-
if (!interruptedTaskId || effectiveReason === 'timeout')
|
|
1214
|
+
if (!interruptedTaskId || effectiveReason === 'timeout' || effectiveReason === 'execution_error')
|
|
812
1215
|
return Promise.resolve();
|
|
813
1216
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
814
1217
|
if (!channelInfo)
|
|
@@ -930,6 +1333,8 @@ export class ResponseEngine {
|
|
|
930
1333
|
agentRegistry;
|
|
931
1334
|
setAgentRegistry(registry) {
|
|
932
1335
|
this.agentRegistry = registry;
|
|
1336
|
+
for (const { adapter } of this.channels.values())
|
|
1337
|
+
this.restoreAsyncQuestions(adapter);
|
|
933
1338
|
}
|
|
934
1339
|
/** 更新 EvolAgent.lastActivity —— 每次发出 status.* 事件(含 progress)时调用 */
|
|
935
1340
|
touchAgentActivity(channelKey) {
|
|
@@ -1026,6 +1431,15 @@ export class ResponseEngine {
|
|
|
1026
1431
|
if (!this.channelTypeMap.has(type)) {
|
|
1027
1432
|
this.channelTypeMap.set(type, adapter.channelName);
|
|
1028
1433
|
}
|
|
1434
|
+
this.restoreAsyncQuestions(adapter);
|
|
1435
|
+
}
|
|
1436
|
+
restoreAsyncQuestions(adapter) {
|
|
1437
|
+
const selfAid = this.agentRegistry?.resolveByChannel(adapter.channelKey)?.aid;
|
|
1438
|
+
if (selfAid && this.asyncQuestions) {
|
|
1439
|
+
void this.asyncQuestions.restore(adapter, selfAid).catch(error => {
|
|
1440
|
+
logger.warn(`[AsyncQuestions] channel restore failed: ${String(error)}`);
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1029
1443
|
}
|
|
1030
1444
|
/**
|
|
1031
1445
|
* 注销渠道适配器(热重载断开渠道时调用,避免遗留死实例)。
|
|
@@ -1219,7 +1633,10 @@ export class ResponseEngine {
|
|
|
1219
1633
|
&& (!authorization.triggerId || !authorization.runId || !authorization.attemptId)) {
|
|
1220
1634
|
throw new Error('fullaccess Trigger authorization lacks scheduler identity or approval provenance');
|
|
1221
1635
|
}
|
|
1222
|
-
if (authorization.source
|
|
1636
|
+
if (authorization.source === 'agent-policy') {
|
|
1637
|
+
assertAgentPolicyAuthorization(authorization);
|
|
1638
|
+
}
|
|
1639
|
+
else if (authorization.source !== 'fullaccess-command' && authorization.source !== 'trigger') {
|
|
1223
1640
|
throw new Error('fullaccess execution authorization has an unsupported source');
|
|
1224
1641
|
}
|
|
1225
1642
|
if (!isFullAccessEnabled()) {
|
|
@@ -1241,6 +1658,8 @@ export class ResponseEngine {
|
|
|
1241
1658
|
const originChannelKey = message.triggerMeta.originChannelKey;
|
|
1242
1659
|
if (!selfAid || !actorId || !channelType || !originChannelKey)
|
|
1243
1660
|
return undefined;
|
|
1661
|
+
const chatType = message.triggerMeta.originChatType ?? 'private';
|
|
1662
|
+
const conversationId = message.triggerMeta.originConversationId ?? actorId;
|
|
1244
1663
|
const control = message.triggerMeta.originControl === true;
|
|
1245
1664
|
let actor;
|
|
1246
1665
|
let resolvedRole = 'none';
|
|
@@ -1249,9 +1668,9 @@ export class ResponseEngine {
|
|
|
1249
1668
|
selfAid,
|
|
1250
1669
|
channelKey: originChannelKey,
|
|
1251
1670
|
channelType,
|
|
1252
|
-
chatType
|
|
1671
|
+
chatType,
|
|
1253
1672
|
actorId,
|
|
1254
|
-
conversationId
|
|
1673
|
+
conversationId,
|
|
1255
1674
|
});
|
|
1256
1675
|
actor = detail.actor;
|
|
1257
1676
|
resolvedRole = detail.effectiveRole || 'none';
|
|
@@ -1271,7 +1690,7 @@ export class ResponseEngine {
|
|
|
1271
1690
|
channelType,
|
|
1272
1691
|
originChannelKey,
|
|
1273
1692
|
control,
|
|
1274
|
-
peerKey: formatPeerKey(channelType, actorId),
|
|
1693
|
+
peerKey: formatPeerKey(channelType, chatType === 'group' ? conversationId : actorId),
|
|
1275
1694
|
};
|
|
1276
1695
|
}
|
|
1277
1696
|
resolveHandoffOriginExecutionIdentity(message, selfAid) {
|
|
@@ -1437,7 +1856,8 @@ export class ResponseEngine {
|
|
|
1437
1856
|
auditFullAccessEvent({
|
|
1438
1857
|
event: fallbackFullAccessAudit.authorization.source === 'trigger'
|
|
1439
1858
|
? 'fullaccess.trigger.execution.ended'
|
|
1440
|
-
:
|
|
1859
|
+
: fallbackFullAccessAudit.authorization.source === 'agent-policy'
|
|
1860
|
+
? 'fullaccess.agent-policy.execution.ended' : 'fullaccess.execution.ended',
|
|
1441
1861
|
source: fallbackFullAccessAudit.authorization.source,
|
|
1442
1862
|
actorId: fallbackFullAccessAudit.authorization.authorizedBy,
|
|
1443
1863
|
processRole: fallbackFullAccessAudit.processRole,
|
|
@@ -1458,6 +1878,15 @@ export class ResponseEngine {
|
|
|
1458
1878
|
async processMessageWithMonitors(message) {
|
|
1459
1879
|
const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
|
|
1460
1880
|
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
1881
|
+
if (message.taskRecovery) {
|
|
1882
|
+
const current = message.boundSessionId
|
|
1883
|
+
? await this.sessionManager.getSessionById(message.boundSessionId)
|
|
1884
|
+
: undefined;
|
|
1885
|
+
if (!current || current.selfAID !== message.selfAID || !isTaskRecoveryCurrent(current, message.taskRecovery)) {
|
|
1886
|
+
this.publishTriggerExecutionSkipped(message, 'recovery_stale');
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1461
1890
|
if (message.handoffDelivery
|
|
1462
1891
|
&& this.handoffRuntime
|
|
1463
1892
|
&& this.handoffRuntime.isDiscardedDelivery(message)) {
|
|
@@ -1520,7 +1949,8 @@ export class ResponseEngine {
|
|
|
1520
1949
|
const userRole = session.identity?.role || 'none';
|
|
1521
1950
|
const isInternalHandoff = message.source === 'handoff';
|
|
1522
1951
|
const fullAccessAuditCandidate = this.resolveFullAccessAuditCandidate(message);
|
|
1523
|
-
const isCurrentFullAccessCommand = fullAccessAuditCandidate?.authorization.source === 'fullaccess-command'
|
|
1952
|
+
const isCurrentFullAccessCommand = (fullAccessAuditCandidate?.authorization.source === 'fullaccess-command'
|
|
1953
|
+
|| fullAccessAuditCandidate?.authorization.source === 'agent-policy')
|
|
1524
1954
|
&& fullAccessAuditCandidate.processRole === 'fullaccess-run';
|
|
1525
1955
|
const isCurrentFullAccessTrigger = fullAccessAuditCandidate?.authorization.source === 'trigger'
|
|
1526
1956
|
&& fullAccessAuditCandidate.processRole === 'fullaccess-run';
|
|
@@ -1609,11 +2039,17 @@ export class ResponseEngine {
|
|
|
1609
2039
|
claimedByInternal: false,
|
|
1610
2040
|
terminalDelivered: false,
|
|
1611
2041
|
claim: () => {
|
|
1612
|
-
if (!timeoutControl.error || this.interruptedSessions.get(session.id)
|
|
2042
|
+
if (!timeoutControl.error || !['timeout', 'execution_error'].includes(this.interruptedSessions.get(session.id) ?? ''))
|
|
1613
2043
|
return undefined;
|
|
1614
2044
|
timeoutControl.claimedByInternal = true;
|
|
1615
2045
|
return timeoutControl.error;
|
|
1616
2046
|
},
|
|
2047
|
+
claimTerminal: () => {
|
|
2048
|
+
if (timeoutControl.terminalDelivered)
|
|
2049
|
+
return false;
|
|
2050
|
+
timeoutControl.terminalDelivered = true;
|
|
2051
|
+
return true;
|
|
2052
|
+
},
|
|
1617
2053
|
};
|
|
1618
2054
|
const rejectAfterInterruptBarrier = (error) => {
|
|
1619
2055
|
if (timeoutTriggered)
|
|
@@ -1622,9 +2058,10 @@ export class ResponseEngine {
|
|
|
1622
2058
|
timeoutControl.error = error;
|
|
1623
2059
|
this.timeoutErrors.set(session.id, error);
|
|
1624
2060
|
this.turnCoordinator.hydrate(session);
|
|
1625
|
-
|
|
1626
|
-
this.
|
|
1627
|
-
|
|
2061
|
+
const reason = error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout';
|
|
2062
|
+
this.turnCoordinator.invalidate(session.id, reason);
|
|
2063
|
+
this.interruptedSessions.set(session.id, reason);
|
|
2064
|
+
const barrier = this.interruptSession(session.id, reason)
|
|
1628
2065
|
.catch(interruptError => {
|
|
1629
2066
|
logger.debug(`[ResponseEngine] Timeout interrupt barrier failed: ${interruptError instanceof Error ? interruptError.message : String(interruptError)}`);
|
|
1630
2067
|
});
|
|
@@ -1635,6 +2072,11 @@ export class ResponseEngine {
|
|
|
1635
2072
|
});
|
|
1636
2073
|
};
|
|
1637
2074
|
const triggerAttemptId = message.replyContext?.metadata?.triggerAttemptId;
|
|
2075
|
+
const externalTimeout = async (error) => {
|
|
2076
|
+
rejectAfterInterruptBarrier(error);
|
|
2077
|
+
await timeoutControl.barrier;
|
|
2078
|
+
};
|
|
2079
|
+
this.activeTimeoutHandlers.set(session.id, externalTimeout);
|
|
1638
2080
|
const resetTimer = (eventType, toolName) => {
|
|
1639
2081
|
monitor?.recordEvent(eventType || 'unknown', toolName);
|
|
1640
2082
|
if (typeof triggerAttemptId === 'string') {
|
|
@@ -1714,11 +2156,9 @@ export class ResponseEngine {
|
|
|
1714
2156
|
await Promise.race(processingPromises);
|
|
1715
2157
|
}
|
|
1716
2158
|
catch (error) {
|
|
1717
|
-
if (error instanceof Error && (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
|
|
2159
|
+
if (error instanceof Error && (isExecutionFailure(error) || error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
|
|
1718
2160
|
await timeoutControl.barrier;
|
|
1719
|
-
|
|
1720
|
-
await timeoutControl.deliverUnhandled?.(error);
|
|
1721
|
-
}
|
|
2161
|
+
await timeoutControl.deliverUnhandled?.(error);
|
|
1722
2162
|
this.timeoutErrors.delete(session.id);
|
|
1723
2163
|
}
|
|
1724
2164
|
// 超时错误:kill 级别已发送诊断信息,无需再发
|
|
@@ -1759,6 +2199,8 @@ export class ResponseEngine {
|
|
|
1759
2199
|
if (totalExecutionTimer)
|
|
1760
2200
|
clearTimeout(totalExecutionTimer);
|
|
1761
2201
|
this.activeMonitors.delete(streamKey);
|
|
2202
|
+
if (this.activeTimeoutHandlers.get(session.id) === externalTimeout)
|
|
2203
|
+
this.activeTimeoutHandlers.delete(session.id);
|
|
1762
2204
|
}
|
|
1763
2205
|
}
|
|
1764
2206
|
resolveFullAccessAuditCandidate(message) {
|
|
@@ -1766,15 +2208,18 @@ export class ResponseEngine {
|
|
|
1766
2208
|
if (commandAuthorization?.permissionMode === 'fullaccess'
|
|
1767
2209
|
&& commandAuthorization.processRole === 'fullaccess-run'
|
|
1768
2210
|
&& commandAuthorization.dataScope === 'daemon'
|
|
1769
|
-
&& commandAuthorization.source === 'fullaccess-command'
|
|
2211
|
+
&& (commandAuthorization.source === 'fullaccess-command' || commandAuthorization.source === 'agent-policy')
|
|
1770
2212
|
&& typeof commandAuthorization.authorizedBy === 'string'
|
|
1771
2213
|
&& commandAuthorization.authorizedBy.length > 0) {
|
|
1772
2214
|
let currentOwner = false;
|
|
1773
2215
|
try {
|
|
1774
2216
|
currentOwner = isFullAccessEnabled()
|
|
1775
2217
|
&& new Set(loadDaemonConfig().owners ?? []).has(commandAuthorization.authorizedBy);
|
|
2218
|
+
if (commandAuthorization.source === 'agent-policy')
|
|
2219
|
+
assertAgentPolicyAuthorization(commandAuthorization);
|
|
1776
2220
|
}
|
|
1777
2221
|
catch {
|
|
2222
|
+
currentOwner = false;
|
|
1778
2223
|
// Runtime validation will fail closed; the audit must still retain the
|
|
1779
2224
|
// attempted authorization without falsely labeling it active.
|
|
1780
2225
|
}
|
|
@@ -1894,7 +2339,7 @@ export class ResponseEngine {
|
|
|
1894
2339
|
// states. An untransitioned `created` agent or malformed lifecycle must
|
|
1895
2340
|
// not fall through to a base runner's default prompt. Missing lifecycle
|
|
1896
2341
|
// is normalized to active for legacy configurations.
|
|
1897
|
-
if (owningAgentForTask && !isActive && !isBootstrapping) {
|
|
2342
|
+
if (owningAgentForTask && ((!isActive && !isBootstrapping) || initializationBlocksTasks(owningAgentForTask.config))) {
|
|
1898
2343
|
const blockedLifecycle = lifecycle
|
|
1899
2344
|
?? `invalid (${String(owningAgentForTask.config.lifecycle)})`;
|
|
1900
2345
|
logger.error(`[ResponseEngine] Agent lifecycle is not runnable: agent=${owningAgentForTask.aid} lifecycle=${blockedLifecycle}`);
|
|
@@ -1991,7 +2436,7 @@ export class ResponseEngine {
|
|
|
1991
2436
|
?? this.resolveHandoffOriginExecutionIdentity(message, selfAid);
|
|
1992
2437
|
const configChannelType = authorizationIdentity?.channelType ?? currentChannelType;
|
|
1993
2438
|
const configChatType = authorizationIdentity
|
|
1994
|
-
? (message.handoffDelivery?.originAuthorization?.chatType ?? 'private')
|
|
2439
|
+
? (message.triggerMeta?.originChatType ?? message.handoffDelivery?.originAuthorization?.chatType ?? 'private')
|
|
1995
2440
|
: (chatType === 'group' ? 'group' : 'private');
|
|
1996
2441
|
// A group channel ID identifies the conversation, not the authenticated
|
|
1997
2442
|
// operator. Keep the historical private-chat fallback (where channelId
|
|
@@ -2179,25 +2624,30 @@ export class ResponseEngine {
|
|
|
2179
2624
|
});
|
|
2180
2625
|
if (timeoutControl) {
|
|
2181
2626
|
timeoutControl.deliverUnhandled = async (timeoutError) => {
|
|
2182
|
-
if (timeoutControl.
|
|
2627
|
+
if (!timeoutControl.claimTerminal())
|
|
2183
2628
|
return;
|
|
2184
|
-
timeoutControl.terminalDelivered = true;
|
|
2185
2629
|
const isTotalExecutionTimeout = timeoutError.message === 'TOTAL_EXECUTION_TIMEOUT';
|
|
2186
|
-
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
2187
|
-
const statusPayload = {
|
|
2630
|
+
const totalExecutionMs = timeoutError instanceof ExecutionTimeoutError ? timeoutError.timeoutMs : this.totalExecutionLimitMs();
|
|
2631
|
+
const statusPayload = timeoutError instanceof OrphanedTargetSessionError ? {
|
|
2632
|
+
kind: 'status.error',
|
|
2633
|
+
metadata: { errorType: timeoutError.reason, message: '任务执行状态丢失,已结束本次执行。' },
|
|
2634
|
+
} : {
|
|
2188
2635
|
kind: 'status.timeout',
|
|
2189
2636
|
metadata: isTotalExecutionTimeout
|
|
2190
|
-
? (totalExecutionMs === undefined ? {} : { totalExecutionMs })
|
|
2637
|
+
? { ...(totalExecutionMs === undefined ? {} : { totalExecutionMs }),
|
|
2638
|
+
...(timeoutError instanceof ExecutionTimeoutError ? { reason: timeoutError.reason } : {}) }
|
|
2191
2639
|
: { idleSec: getLastIdleSec?.() || undefined },
|
|
2192
2640
|
};
|
|
2193
2641
|
const idleSec = getLastIdleSec?.() || 0;
|
|
2194
|
-
const userMessage =
|
|
2195
|
-
?
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
:
|
|
2642
|
+
const userMessage = timeoutError instanceof OrphanedTargetSessionError
|
|
2643
|
+
? '⚠️ 任务执行状态丢失,已结束本次执行。'
|
|
2644
|
+
: isTotalExecutionTimeout
|
|
2645
|
+
? (totalExecutionMs === undefined
|
|
2646
|
+
? '⚠️ 任务超过总执行时限,已自动中断'
|
|
2647
|
+
: `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
|
|
2648
|
+
: idleSec > 0
|
|
2649
|
+
? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断`
|
|
2650
|
+
: '⚠️ 任务超时,已自动中断';
|
|
2201
2651
|
agent.cleanupStream(streamKey);
|
|
2202
2652
|
this.sessionManager.clearProcessingIfTask(session.id, taskId);
|
|
2203
2653
|
if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
|
|
@@ -2211,13 +2661,13 @@ export class ResponseEngine {
|
|
|
2211
2661
|
await adapter.send(envelope, {
|
|
2212
2662
|
kind: 'result.error',
|
|
2213
2663
|
text: userMessage,
|
|
2214
|
-
reason: isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
|
|
2664
|
+
reason: isExecutionFailure(timeoutError) ? timeoutError.reason : isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
|
|
2215
2665
|
}).catch(() => { });
|
|
2216
2666
|
if (daemonTrigger) {
|
|
2217
2667
|
await adapter.send(envelope, statusPayload).catch(() => { });
|
|
2218
2668
|
}
|
|
2219
2669
|
this.touchAgentActivity(channelKey);
|
|
2220
|
-
this.publishTriggerExecutionFailure(message, timeoutError.message, {
|
|
2670
|
+
this.publishTriggerExecutionFailure(message, isExecutionFailure(timeoutError) ? timeoutError.reason : timeoutError.message, {
|
|
2221
2671
|
messageId,
|
|
2222
2672
|
causation: taskCausation,
|
|
2223
2673
|
});
|
|
@@ -2225,9 +2675,14 @@ export class ResponseEngine {
|
|
|
2225
2675
|
this.eventBus.publish({
|
|
2226
2676
|
type: 'task:error',
|
|
2227
2677
|
sessionId: session.id,
|
|
2678
|
+
agentAid: runnerSelfAid,
|
|
2679
|
+
peerId: configActorId,
|
|
2680
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
2681
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
2228
2682
|
taskId,
|
|
2229
2683
|
generation: turnLease?.generation,
|
|
2230
2684
|
error: timeoutError.message,
|
|
2685
|
+
terminalReason: isExecutionFailure(timeoutError) ? timeoutError.reason : undefined,
|
|
2231
2686
|
errorType,
|
|
2232
2687
|
agentName: agentNameForStats,
|
|
2233
2688
|
causation: taskCausation,
|
|
@@ -2356,14 +2811,28 @@ export class ResponseEngine {
|
|
|
2356
2811
|
// message is authoritative; session metadata is only a fallback for
|
|
2357
2812
|
// legacy paths that do not carry a per-message display name.
|
|
2358
2813
|
const peerName = message.peerName ?? session.metadata?.peerName;
|
|
2359
|
-
const peerId =
|
|
2814
|
+
const peerId = message.peerId ?? session.metadata?.peerId ?? message.channelId;
|
|
2360
2815
|
const peerShort = peerId ? peerId.split('.')[0].split(':')[0] : '?';
|
|
2361
2816
|
const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
|
|
2362
2817
|
logger.info(`[ResponseEngine] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} chatMode=${effectiveChatMode} baseagent=${session.baseagent} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
2363
2818
|
// Establish the durable turn before exposing any processing state. This
|
|
2364
2819
|
// lets an immediate /stop or replacement message close the same task and
|
|
2365
2820
|
// prevents a BusinessTrip reaction from becoming orphaned during setup.
|
|
2366
|
-
|
|
2821
|
+
if (message.taskRecovery && !isTaskRecoveryCurrent({
|
|
2822
|
+
...session, metadata: { ...session.metadata, turnState: this.turnCoordinator.current(session) },
|
|
2823
|
+
}, message.taskRecovery)) {
|
|
2824
|
+
this.publishTriggerExecutionSkipped(message, 'recovery_stale');
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
turnLease = await this.turnCoordinator.begin(session, taskId, undefined, configActorId ? {
|
|
2828
|
+
peerId: configActorId,
|
|
2829
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
2830
|
+
baseagent: session.baseagent,
|
|
2831
|
+
agentSessionId: session.agentSessionId,
|
|
2832
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
2833
|
+
recovery: message.taskRecovery?.origin,
|
|
2834
|
+
triggerAttemptId: message.triggerMeta?.attemptId,
|
|
2835
|
+
} : undefined);
|
|
2367
2836
|
// 记录开始处理
|
|
2368
2837
|
const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
|
|
2369
2838
|
this.eventBus.publish({
|
|
@@ -2440,7 +2909,10 @@ export class ResponseEngine {
|
|
|
2440
2909
|
if (payload.kind === 'result.text' && payload.isFinal) {
|
|
2441
2910
|
opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
|
|
2442
2911
|
}
|
|
2443
|
-
opts.metadata = withTaskMetadata(
|
|
2912
|
+
opts.metadata = withTaskMetadata({
|
|
2913
|
+
...(opts.metadata ?? {}),
|
|
2914
|
+
...(payload.kind === 'result.text' ? payload.metadata : {}),
|
|
2915
|
+
});
|
|
2444
2916
|
// status.notice is a diagnostic observation, not execution activity.
|
|
2445
2917
|
if (payload.kind.startsWith('status.') && payload.kind !== 'status.notice') {
|
|
2446
2918
|
this.touchAgentActivity(channelKey);
|
|
@@ -2585,10 +3057,10 @@ export class ResponseEngine {
|
|
|
2585
3057
|
};
|
|
2586
3058
|
})();
|
|
2587
3059
|
let effectivePermissionMode = 'readonly';
|
|
2588
|
-
// Allocate
|
|
2589
|
-
// context is installed.
|
|
2590
|
-
//
|
|
2591
|
-
const sessionRuntimeDir =
|
|
3060
|
+
// Allocate the stable session-owned root before any runner preflight
|
|
3061
|
+
// context is installed. Every runner for this EvolCore session receives
|
|
3062
|
+
// the same directory, independent of daemon or runner lifetimes.
|
|
3063
|
+
const sessionRuntimeDir = ensureSessionRuntimeDir(session.id);
|
|
2592
3064
|
const recordExecutionAnomaly = triggerRunId
|
|
2593
3065
|
? (anomaly) => {
|
|
2594
3066
|
recordTriggerExecutionAnomaly(triggerRunId, {
|
|
@@ -2636,8 +3108,8 @@ export class ResponseEngine {
|
|
|
2636
3108
|
pauseController: taskPauseController,
|
|
2637
3109
|
preToolUsePolicyHook: pureSessionPolicyHook,
|
|
2638
3110
|
armApprovedDelegationCommand: this.agentDelegationRegistry
|
|
2639
|
-
? (
|
|
2640
|
-
|
|
3111
|
+
? (sessionToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
|
|
3112
|
+
sessionToken,
|
|
2641
3113
|
sessionId: session.id,
|
|
2642
3114
|
taskId,
|
|
2643
3115
|
commandHash,
|
|
@@ -2812,10 +3284,19 @@ export class ResponseEngine {
|
|
|
2812
3284
|
if (commandFullAccess.permissionMode !== 'fullaccess'
|
|
2813
3285
|
|| commandFullAccess.processRole !== 'fullaccess-run'
|
|
2814
3286
|
|| commandFullAccess.dataScope !== 'daemon'
|
|
2815
|
-
|| commandFullAccess.source !== 'fullaccess-command'
|
|
3287
|
+
|| (commandFullAccess.source !== 'fullaccess-command' && commandFullAccess.source !== 'agent-policy')
|
|
2816
3288
|
|| !commandFullAccess.authorizedBy) {
|
|
2817
3289
|
throw new Error('fullaccess command authorization is missing or malformed');
|
|
2818
3290
|
}
|
|
3291
|
+
if (commandFullAccess.source === 'agent-policy'
|
|
3292
|
+
&& (commandFullAccess.agentAid !== runnerSelfAid
|
|
3293
|
+
|| configChatType !== 'private'
|
|
3294
|
+
|| (message.source !== undefined && message.source !== 'user')
|
|
3295
|
+
|| !hasTrustedPrincipal(authenticatedActor)
|
|
3296
|
+
|| authenticatedActor.principalId !== commandFullAccess.authorizedBy
|
|
3297
|
+
|| authenticatedActor.operatorId !== commandFullAccess.policyActor?.operatorId)) {
|
|
3298
|
+
throw new Error('Agent fullaccess policy authorization is missing its Agent identity');
|
|
3299
|
+
}
|
|
2819
3300
|
fullAccessAuthorization = commandFullAccess;
|
|
2820
3301
|
}
|
|
2821
3302
|
else {
|
|
@@ -3030,18 +3511,25 @@ export class ResponseEngine {
|
|
|
3030
3511
|
// happen before task-start auto compact because compact is a backend
|
|
3031
3512
|
// management action just like the subsequent runQuery.
|
|
3032
3513
|
let runAgentSessionId = session.agentSessionId;
|
|
3033
|
-
if (session.threadId
|
|
3514
|
+
if ((session.threadId || message.taskRecovery)
|
|
3034
3515
|
&& typeof this.sessionManager.getSessionById === 'function') {
|
|
3035
3516
|
const latestBackendSession = await this.sessionManager.getSessionById(session.id);
|
|
3036
|
-
if (!latestBackendSession || latestBackendSession.threadId !== session.threadId) {
|
|
3517
|
+
if (!latestBackendSession || latestBackendSession.deletedAt || latestBackendSession.threadId !== session.threadId) {
|
|
3037
3518
|
throw new Error('topic session changed before backend run started');
|
|
3038
3519
|
}
|
|
3520
|
+
if (message.taskRecovery && (latestBackendSession.agentSessionId !== message.taskRecovery.agentSessionId
|
|
3521
|
+
|| latestBackendSession.baseagent !== message.taskRecovery.baseagent)) {
|
|
3522
|
+
throw new Error('recovery backend changed before execution');
|
|
3523
|
+
}
|
|
3039
3524
|
runAgentSessionId = latestBackendSession.agentSessionId;
|
|
3040
3525
|
session.agentSessionId = runAgentSessionId;
|
|
3041
3526
|
session.metadata = latestBackendSession.metadata;
|
|
3042
3527
|
}
|
|
3043
3528
|
modelOverride = {
|
|
3044
3529
|
...(modelOverride || {}),
|
|
3530
|
+
...(message.taskRecovery
|
|
3531
|
+
? { recovery: { agentSessionId: message.taskRecovery.agentSessionId } }
|
|
3532
|
+
: {}),
|
|
3045
3533
|
...(session.threadId
|
|
3046
3534
|
? { backend: { kind: 'topic', agentSessionId: runAgentSessionId ?? null } }
|
|
3047
3535
|
: {}),
|
|
@@ -3054,13 +3542,17 @@ export class ResponseEngine {
|
|
|
3054
3542
|
},
|
|
3055
3543
|
};
|
|
3056
3544
|
const refreshTopicBindingForLogicalRetry = async () => {
|
|
3057
|
-
if (!session.threadId || typeof this.sessionManager.getSessionById !== 'function')
|
|
3545
|
+
if ((!session.threadId && !message.taskRecovery) || typeof this.sessionManager.getSessionById !== 'function')
|
|
3058
3546
|
return;
|
|
3059
3547
|
const latest = await this.sessionManager.getSessionById(session.id);
|
|
3060
3548
|
if (!latest || latest.threadId !== session.threadId) {
|
|
3061
3549
|
throw new Error('topic session changed during backend run');
|
|
3062
3550
|
}
|
|
3063
3551
|
const latestAgentSessionId = latest.agentSessionId;
|
|
3552
|
+
if (message.taskRecovery && (latestAgentSessionId !== message.taskRecovery.agentSessionId
|
|
3553
|
+
|| latest.baseagent !== message.taskRecovery.baseagent)) {
|
|
3554
|
+
throw new Error('recovery backend changed during execution');
|
|
3555
|
+
}
|
|
3064
3556
|
session.agentSessionId = latestAgentSessionId;
|
|
3065
3557
|
session.metadata = latest.metadata;
|
|
3066
3558
|
if (latestAgentSessionId === runAgentSessionId)
|
|
@@ -3097,6 +3589,26 @@ export class ResponseEngine {
|
|
|
3097
3589
|
groupId: session.metadata?.groupId || message.channelId,
|
|
3098
3590
|
channel: currentChannelType || message.channel,
|
|
3099
3591
|
});
|
|
3592
|
+
const historyGroupId = session.metadata?.groupId || message.channelId;
|
|
3593
|
+
const canUseGroupHistory = (operation) => {
|
|
3594
|
+
if (currentChannelType !== 'aun' || session.chatType !== 'group' || !selfAid || !historyGroupId)
|
|
3595
|
+
return false;
|
|
3596
|
+
const historySubject = buildAuthSubject({
|
|
3597
|
+
selfAid, actorId: configActorId, channel: channelKey, channelType: configChannelType,
|
|
3598
|
+
chatType: configChatType, channelId: message.channelId,
|
|
3599
|
+
conversationId: configChatType === 'group' ? historyGroupId : configActorId,
|
|
3600
|
+
permissionMode: effectivePermissionMode,
|
|
3601
|
+
});
|
|
3602
|
+
return authorizeOperation({
|
|
3603
|
+
source: 'agent-tool', subject: historySubject, audit: false,
|
|
3604
|
+
intent: {
|
|
3605
|
+
operation, scope: ['owner', 'admin'].includes(historySubject.role) ? 'agent' : 'relation',
|
|
3606
|
+
source: 'agent-tool',
|
|
3607
|
+
args: { self: selfAid, targetId: historyGroupId, peer: historyGroupId,
|
|
3608
|
+
peerKey: formatPeerKey('aun', historyGroupId) },
|
|
3609
|
+
},
|
|
3610
|
+
}).allow;
|
|
3611
|
+
};
|
|
3100
3612
|
// Kit renderer: 组装上下文
|
|
3101
3613
|
const pkgRoot = getPackageRoot();
|
|
3102
3614
|
const kitCtx = {
|
|
@@ -3128,11 +3640,12 @@ export class ResponseEngine {
|
|
|
3128
3640
|
peerKey,
|
|
3129
3641
|
peerName: peerName || undefined,
|
|
3130
3642
|
peerRole,
|
|
3131
|
-
|
|
3643
|
+
// Display missing identity explicitly without changing the mode resolver's legacy fallback.
|
|
3644
|
+
peerType: peerType || 'unknown',
|
|
3132
3645
|
sameDevice: message.sameDevice ?? false,
|
|
3133
3646
|
sameNetwork: message.sameNetwork ?? false,
|
|
3134
3647
|
sameEgressIp: message.sameEgressIp ?? false,
|
|
3135
|
-
groupId: session.
|
|
3648
|
+
groupId: session.chatType === 'group' ? historyGroupId : undefined,
|
|
3136
3649
|
groupName: session.metadata?.groupName || undefined,
|
|
3137
3650
|
// 信封展示用:有群名则「名<ID>」,否则纯 ID。规避模板引擎无 not/else 的限制。
|
|
3138
3651
|
groupLabel: session.metadata?.groupId
|
|
@@ -3147,6 +3660,7 @@ export class ResponseEngine {
|
|
|
3147
3660
|
?? session.metadata?.mentionMode
|
|
3148
3661
|
?? message.mentionMode
|
|
3149
3662
|
?? undefined,
|
|
3663
|
+
groupHistoryAvailable: canUseGroupHistory('ec.group.history'),
|
|
3150
3664
|
clientType: message.clientType || undefined,
|
|
3151
3665
|
permissionMode: effectivePermissionMode,
|
|
3152
3666
|
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
@@ -3343,11 +3857,9 @@ export class ResponseEngine {
|
|
|
3343
3857
|
}
|
|
3344
3858
|
}
|
|
3345
3859
|
}
|
|
3346
|
-
//
|
|
3347
|
-
//
|
|
3348
|
-
//
|
|
3349
|
-
// explicitly so sandboxed helper commands never fall back to a shared
|
|
3350
|
-
// agent or project directory for transient state.
|
|
3860
|
+
// Pass the stable session directory explicitly so sandboxed helper
|
|
3861
|
+
// commands never fall back to a process, agent, or project directory
|
|
3862
|
+
// for transient state.
|
|
3351
3863
|
const taskRuntimeContext = {
|
|
3352
3864
|
taskId,
|
|
3353
3865
|
sessionId: session.id,
|
|
@@ -3430,7 +3942,8 @@ export class ResponseEngine {
|
|
|
3430
3942
|
auditFullAccessEvent({
|
|
3431
3943
|
event: fullAccessAuthorization.source === 'trigger'
|
|
3432
3944
|
? 'fullaccess.trigger.execution.started'
|
|
3433
|
-
:
|
|
3945
|
+
: fullAccessAuthorization.source === 'agent-policy'
|
|
3946
|
+
? 'fullaccess.agent-policy.execution.started' : 'fullaccess.execution.started',
|
|
3434
3947
|
source: fullAccessAuthorization.source,
|
|
3435
3948
|
actorId: fullAccessAuthorization.authorizedBy,
|
|
3436
3949
|
processRole: fullAccessAuditProcessRole,
|
|
@@ -3454,6 +3967,23 @@ export class ResponseEngine {
|
|
|
3454
3967
|
}
|
|
3455
3968
|
fullAccessRunnerInvoked = true;
|
|
3456
3969
|
}
|
|
3970
|
+
if (message.asyncQuestionAnswer) {
|
|
3971
|
+
if (session.baseagent !== 'codex' || runAgentSessionId !== message.asyncQuestionAnswer.providerThreadId) {
|
|
3972
|
+
throw new Error('Async question backend changed before queued resume');
|
|
3973
|
+
}
|
|
3974
|
+
if (!(await this.asyncQuestions?.validateQueuedAnswer(session, message.asyncQuestionAnswer))) {
|
|
3975
|
+
throw new Error('Async question authorization or persisted answer changed before queued resume');
|
|
3976
|
+
}
|
|
3977
|
+
// An active queue item restored after submission resumes the unfinished work.
|
|
3978
|
+
// Its native answer is already in Codex history and must not be appended twice.
|
|
3979
|
+
if (!message.restartResume?.submitted) {
|
|
3980
|
+
modelOverride = { ...modelOverride, asyncQuestionAnswer: message.asyncQuestionAnswer };
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
if (session.baseagent === 'codex') {
|
|
3984
|
+
modelOverride = { ...modelOverride,
|
|
3985
|
+
enableRequestUserInput: effectiveAgentConfig?.baseagents?.codex?.enableRequestUserInput ?? true };
|
|
3986
|
+
}
|
|
3457
3987
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
|
|
3458
3988
|
// The turn may be superseded while the runner is still creating its
|
|
3459
3989
|
// backend query. Replay the interrupt after runQuery resolves, when
|
|
@@ -3470,7 +4000,23 @@ export class ResponseEngine {
|
|
|
3470
4000
|
const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
|
|
3471
4001
|
resetTimer(eventType, toolName);
|
|
3472
4002
|
attemptTimeout?.reset();
|
|
3473
|
-
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid)
|
|
4003
|
+
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid, async (request) => {
|
|
4004
|
+
if (!this.asyncQuestions)
|
|
4005
|
+
throw new Error('Async question service is unavailable');
|
|
4006
|
+
if (permissionContext.approvalInteractionPolicy === 'deny') {
|
|
4007
|
+
logger.info(`[AsyncQuestions] no interactive recipient for unattended task: session=${session.id}`);
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
await renderer.flush(false);
|
|
4011
|
+
try {
|
|
4012
|
+
await this.asyncQuestions.present(session, request, message, adapter, envelope);
|
|
4013
|
+
}
|
|
4014
|
+
catch (error) {
|
|
4015
|
+
// Card dispatch is non-blocking business input. Keep consuming the running turn;
|
|
4016
|
+
// pending records can be delivered after reconnect/restart.
|
|
4017
|
+
logger.warn(`[AsyncQuestions] presentation failed: session=${session.id} error=${String(error)}`);
|
|
4018
|
+
}
|
|
4019
|
+
});
|
|
3474
4020
|
streamResult = attemptTimeout
|
|
3475
4021
|
? await Promise.race([processAttempt, attemptTimeout.promise])
|
|
3476
4022
|
: await processAttempt;
|
|
@@ -3889,7 +4435,7 @@ export class ResponseEngine {
|
|
|
3889
4435
|
// `hasReceivedText` 是任务级计数。工具调用前的文字已经显示,并不表示最后一轮
|
|
3890
4436
|
// (通常来自 complete.result)的文字也已显示;用本轮投影标记避免终态丢失。
|
|
3891
4437
|
if (shouldSuppress() || !streamResult.hasProjectedCurrentReplyText) {
|
|
3892
|
-
renderer.addText(visibleFinalReplyText);
|
|
4438
|
+
await renderer.addText(visibleFinalReplyText, undefined, undefined, streamResult.assistantUuid);
|
|
3893
4439
|
}
|
|
3894
4440
|
}
|
|
3895
4441
|
// 先清理流和处理中状态(保证即使 flush 卡住,session 也不会永久处于"处理中")
|
|
@@ -3908,7 +4454,7 @@ export class ResponseEngine {
|
|
|
3908
4454
|
const suffix = showHint
|
|
3909
4455
|
? `\n\n---\n⚠️ [降级模型: ${usedFallback.model} | 可告诉我"帮我检查可用模型"来诊断]`
|
|
3910
4456
|
: `\n\n---\n⚠️ [降级模型: ${usedFallback.model}]`;
|
|
3911
|
-
renderer.addText(suffix);
|
|
4457
|
+
await renderer.addText(suffix, undefined, undefined, streamResult.assistantUuid);
|
|
3912
4458
|
if (showHint && curFbState) {
|
|
3913
4459
|
curFbState.hintShown = true;
|
|
3914
4460
|
this.modelFallbackMap.set(session.id, curFbState);
|
|
@@ -3921,7 +4467,14 @@ export class ResponseEngine {
|
|
|
3921
4467
|
}
|
|
3922
4468
|
else {
|
|
3923
4469
|
// Flush 剩余内容(文件标记已在 flush 时自动移除)
|
|
3924
|
-
|
|
4470
|
+
// A final text flush can also contain failed output. Preserve the
|
|
4471
|
+
// outcome so AUN does not label that text as normal completion.
|
|
4472
|
+
if (streamResult.isError && !isExpectedTerminalInterrupt(streamResult.terminalReason)) {
|
|
4473
|
+
await renderer.flush(true, 'failed');
|
|
4474
|
+
}
|
|
4475
|
+
else {
|
|
4476
|
+
await renderer.flush(!isExpectedTerminalInterrupt(streamResult.terminalReason));
|
|
4477
|
+
}
|
|
3925
4478
|
}
|
|
3926
4479
|
const terminalInterrupt = isExpectedTerminalInterrupt(streamResult.terminalReason);
|
|
3927
4480
|
if (!streamResult.isError && !terminalInterrupt) {
|
|
@@ -3944,7 +4497,8 @@ export class ResponseEngine {
|
|
|
3944
4497
|
}
|
|
3945
4498
|
}
|
|
3946
4499
|
else {
|
|
3947
|
-
await this.turnCoordinator.finish(session, turnLease, terminalInterrupt ? 'cancelled' : 'failed',
|
|
4500
|
+
await this.turnCoordinator.finish(session, turnLease, terminalInterrupt ? 'cancelled' : 'failed', permissionTerminalFailure ? 'permission_denied'
|
|
4501
|
+
: streamResult.protocolIncompleteReason || streamResult.terminalReason || streamResult.subtype);
|
|
3948
4502
|
}
|
|
3949
4503
|
// 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
|
|
3950
4504
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
@@ -3978,7 +4532,7 @@ export class ResponseEngine {
|
|
|
3978
4532
|
// DaemonChannel settles a trigger when it receives a terminal
|
|
3979
4533
|
// payload. Send the metadata-bearing status first so failed runs do
|
|
3980
4534
|
// not lose their tool counts and recovery diagnostics.
|
|
3981
|
-
if (message
|
|
4535
|
+
if (this.isTrustedDaemonTrigger(message)) {
|
|
3982
4536
|
await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
|
|
3983
4537
|
await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
|
|
3984
4538
|
}
|
|
@@ -4019,6 +4573,10 @@ export class ResponseEngine {
|
|
|
4019
4573
|
this.eventBus.publish({
|
|
4020
4574
|
type: 'task:error',
|
|
4021
4575
|
sessionId: session.id,
|
|
4576
|
+
agentAid: runnerSelfAid,
|
|
4577
|
+
peerId: configActorId,
|
|
4578
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
4579
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
4022
4580
|
taskId,
|
|
4023
4581
|
generation: turnLease?.generation,
|
|
4024
4582
|
error: errorSummary,
|
|
@@ -4297,7 +4855,8 @@ export class ResponseEngine {
|
|
|
4297
4855
|
await timeoutControl?.barrier;
|
|
4298
4856
|
}
|
|
4299
4857
|
const handlingTimeout = error instanceof Error
|
|
4300
|
-
&& (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT');
|
|
4858
|
+
&& (isExecutionFailure(error) || error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT');
|
|
4859
|
+
const ownsTimeoutTerminal = !handlingTimeout || !timeoutControl || timeoutControl.claimTerminal();
|
|
4301
4860
|
if (handlingTimeout) {
|
|
4302
4861
|
this.timeoutErrors.delete(session.id);
|
|
4303
4862
|
}
|
|
@@ -4319,11 +4878,12 @@ export class ResponseEngine {
|
|
|
4319
4878
|
const errType = classifyError(error);
|
|
4320
4879
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
4321
4880
|
const isTotalExecutionTimeout = error instanceof Error && error.message === 'TOTAL_EXECUTION_TIMEOUT';
|
|
4322
|
-
const isTimeout = error instanceof Error && error.message === 'SDK_TIMEOUT';
|
|
4881
|
+
const isTimeout = error instanceof Error && (error.message === 'SDK_TIMEOUT' || error instanceof ExecutionTimeoutError);
|
|
4323
4882
|
const canPublishTerminal = !turnLease || this.turnCoordinator.canPublish(turnLease);
|
|
4324
4883
|
const staleSupersededTurn = !!turnLease
|
|
4325
4884
|
&& !canPublishTerminal
|
|
4326
4885
|
&& interruptReason !== 'timeout'
|
|
4886
|
+
&& interruptReason !== 'execution_error'
|
|
4327
4887
|
&& !isTimeout
|
|
4328
4888
|
&& !isTotalExecutionTimeout;
|
|
4329
4889
|
const isUserInterrupt = isNonErrorInterruptReason(interruptReason) || staleSupersededTurn;
|
|
@@ -4331,7 +4891,7 @@ export class ResponseEngine {
|
|
|
4331
4891
|
if (isUserInterrupt) {
|
|
4332
4892
|
await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'stale_turn', taskCausation);
|
|
4333
4893
|
}
|
|
4334
|
-
else {
|
|
4894
|
+
else if (ownsTimeoutTerminal) {
|
|
4335
4895
|
this.publishTriggerExecutionFailure(message, errorMsg, {
|
|
4336
4896
|
messageId,
|
|
4337
4897
|
causation: taskCausation,
|
|
@@ -4340,24 +4900,27 @@ export class ResponseEngine {
|
|
|
4340
4900
|
if (leaseWasCurrent && turnLease) {
|
|
4341
4901
|
await this.turnCoordinator.finish(session, turnLease, 'failed', error instanceof Error ? error.message : String(error));
|
|
4342
4902
|
}
|
|
4343
|
-
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
4903
|
+
const totalExecutionMs = error instanceof ExecutionTimeoutError ? error.timeoutMs : this.totalExecutionLimitMs();
|
|
4344
4904
|
const procStatus = errType === ErrorType.SDK_TIMEOUT || isTotalExecutionTimeout ? 'timeout'
|
|
4345
4905
|
: errType === ErrorType.STREAM_ERROR ? 'interrupted'
|
|
4346
4906
|
: 'error';
|
|
4347
4907
|
const daemonTrigger = this.isTrustedDaemonTrigger(message);
|
|
4348
|
-
const statusPayload =
|
|
4349
|
-
? { kind: 'status.
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4908
|
+
const statusPayload = error instanceof OrphanedTargetSessionError
|
|
4909
|
+
? { kind: 'status.error', metadata: { errorType: error.reason, message: '任务执行状态丢失,已结束本次执行。' } }
|
|
4910
|
+
: procStatus === 'timeout'
|
|
4911
|
+
? { kind: 'status.timeout', metadata: isTotalExecutionTimeout
|
|
4912
|
+
? { ...(totalExecutionMs === undefined ? {} : { totalExecutionMs }),
|
|
4913
|
+
...(error instanceof ExecutionTimeoutError ? { reason: error.reason } : {}) }
|
|
4914
|
+
: { idleSec: getLastIdleSec?.() || undefined } }
|
|
4915
|
+
: procStatus === 'interrupted'
|
|
4916
|
+
? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
|
|
4917
|
+
: { kind: 'status.error' };
|
|
4355
4918
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
4356
|
-
if (!isUserInterrupt && !daemonTrigger) {
|
|
4919
|
+
if (!isUserInterrupt && ownsTimeoutTerminal && !daemonTrigger) {
|
|
4357
4920
|
await adapter.send(envelope, statusPayload).catch(() => { });
|
|
4358
4921
|
this.touchAgentActivity(channelKey);
|
|
4359
4922
|
}
|
|
4360
|
-
if (!isUserInterrupt && daemonTrigger)
|
|
4923
|
+
if (!isUserInterrupt && ownsTimeoutTerminal && daemonTrigger)
|
|
4361
4924
|
this.touchAgentActivity(channelKey);
|
|
4362
4925
|
// 用户主动中断时降级日志;其余仍按 error 记录
|
|
4363
4926
|
if (isUserInterrupt) {
|
|
@@ -4371,13 +4934,18 @@ export class ResponseEngine {
|
|
|
4371
4934
|
// (message-queue / slash-handler)已发过 task:interrupted(它填充了
|
|
4372
4935
|
// interruptedSessions,isUserInterrupt 才会为真),stats 侧已据此收尾任务。
|
|
4373
4936
|
// 此处不再发任何事件——发 task:error 会误归类,重发 task:interrupted 会重复记账。
|
|
4374
|
-
if (!isUserInterrupt) {
|
|
4937
|
+
if (!isUserInterrupt && ownsTimeoutTerminal) {
|
|
4375
4938
|
this.eventBus.publish({
|
|
4376
4939
|
type: 'task:error',
|
|
4377
4940
|
sessionId: session.id,
|
|
4941
|
+
agentAid: runnerSelfAid,
|
|
4942
|
+
peerId: configActorId,
|
|
4943
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
4944
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
4378
4945
|
taskId,
|
|
4379
4946
|
generation: turnLease?.generation,
|
|
4380
4947
|
error: errorMsg,
|
|
4948
|
+
terminalReason: isExecutionFailure(error) ? error.reason : undefined,
|
|
4381
4949
|
errorType,
|
|
4382
4950
|
agentName: agentNameForStats,
|
|
4383
4951
|
causation: taskCausation,
|
|
@@ -4385,14 +4953,19 @@ export class ResponseEngine {
|
|
|
4385
4953
|
recordCausationSpan(taskCausation, 'task.run', { status: 'failed', refs: { taskId, sessionId: session.id, messageId }, reason: errorType });
|
|
4386
4954
|
}
|
|
4387
4955
|
// 记录处理失败
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4956
|
+
if (ownsTimeoutTerminal) {
|
|
4957
|
+
logger.message({
|
|
4958
|
+
msgId: messageId,
|
|
4959
|
+
sessionId: session.id,
|
|
4960
|
+
dir: 'inbound',
|
|
4961
|
+
status: isUserInterrupt ? 'interrupted' : 'failed',
|
|
4962
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4963
|
+
});
|
|
4964
|
+
}
|
|
4965
|
+
else {
|
|
4966
|
+
logger.info(`[ResponseEngine] Duplicate timeout terminal suppressed: session=${session.id} task=${taskId}`);
|
|
4967
|
+
}
|
|
4968
|
+
if (error instanceof Error && !isUserInterrupt && ownsTimeoutTerminal) {
|
|
4396
4969
|
logger.error(`[${message.channel}] Error stack:`, error.stack);
|
|
4397
4970
|
}
|
|
4398
4971
|
// 发送用户友好的错误消息
|
|
@@ -4402,7 +4975,11 @@ export class ResponseEngine {
|
|
|
4402
4975
|
const retryExhaustedCount = getRetryExhaustedCount(error);
|
|
4403
4976
|
const modelFallbackExhaustedMessage = getModelFallbackExhaustedMessage(error);
|
|
4404
4977
|
const retryInputAlreadySubmitted = wasRetryInputAlreadySubmitted(error);
|
|
4405
|
-
if (
|
|
4978
|
+
if (!ownsTimeoutTerminal) {
|
|
4979
|
+
// The monitor path already owns delivery for this timeout. Cleanup and
|
|
4980
|
+
// durable turn finalization above still run, but no terminal is emitted twice.
|
|
4981
|
+
}
|
|
4982
|
+
else if (isUserInterrupt) {
|
|
4406
4983
|
logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
|
|
4407
4984
|
}
|
|
4408
4985
|
else if (error?._errorAlreadySent && !daemonTrigger && !retryExhaustedCount && !isTimeout && !isTotalExecutionTimeout) {
|
|
@@ -4411,19 +4988,21 @@ export class ResponseEngine {
|
|
|
4411
4988
|
else {
|
|
4412
4989
|
// SDK_TIMEOUT:status.timeout 已发结构化状态,此处再补一条用户可见的错误文本(result.error)
|
|
4413
4990
|
const idleSec = getLastIdleSec?.() || 0;
|
|
4414
|
-
const userMessage =
|
|
4415
|
-
?
|
|
4416
|
-
:
|
|
4417
|
-
?
|
|
4418
|
-
:
|
|
4419
|
-
?
|
|
4420
|
-
:
|
|
4421
|
-
?
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
:
|
|
4991
|
+
const userMessage = error instanceof OrphanedTargetSessionError
|
|
4992
|
+
? '⚠️ 任务执行状态丢失,已结束本次执行。'
|
|
4993
|
+
: retryExhaustedCount
|
|
4994
|
+
? formatRetryableErrorFinalMessage(error, retryExhaustedCount)
|
|
4995
|
+
: retryInputAlreadySubmitted
|
|
4996
|
+
? formatRetryInputAlreadySubmittedMessage(error)
|
|
4997
|
+
: modelFallbackExhaustedMessage
|
|
4998
|
+
? modelFallbackExhaustedMessage
|
|
4999
|
+
: isTotalExecutionTimeout
|
|
5000
|
+
? (totalExecutionMs === undefined
|
|
5001
|
+
? '⚠️ 任务超过总执行时限,已自动中断'
|
|
5002
|
+
: `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
|
|
5003
|
+
: isTimeout
|
|
5004
|
+
? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
|
|
5005
|
+
: getErrorMessage(error, undefined);
|
|
4427
5006
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
4428
5007
|
let sendOpts;
|
|
4429
5008
|
try {
|
|
@@ -4440,7 +5019,7 @@ export class ResponseEngine {
|
|
|
4440
5019
|
const errorPayload = {
|
|
4441
5020
|
kind: 'result.error',
|
|
4442
5021
|
text: userMessage,
|
|
4443
|
-
reason: retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
|
|
5022
|
+
reason: isExecutionFailure(error) ? error.reason : retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
|
|
4444
5023
|
};
|
|
4445
5024
|
if (!isUserInterrupt && daemonTrigger && procStatus === 'error') {
|
|
4446
5025
|
// DaemonChannel settles a trigger on the first terminal payload. Put
|
|
@@ -4483,9 +5062,6 @@ export class ResponseEngine {
|
|
|
4483
5062
|
}).catch(() => { });
|
|
4484
5063
|
}
|
|
4485
5064
|
}
|
|
4486
|
-
if (handlingTimeout && timeoutControl) {
|
|
4487
|
-
timeoutControl.terminalDelivered = true;
|
|
4488
|
-
}
|
|
4489
5065
|
}
|
|
4490
5066
|
finally {
|
|
4491
5067
|
if (fullAccessAuthorization) {
|
|
@@ -4496,7 +5072,8 @@ export class ResponseEngine {
|
|
|
4496
5072
|
auditFullAccessEvent({
|
|
4497
5073
|
event: fullAccessAuthorization.source === 'trigger'
|
|
4498
5074
|
? 'fullaccess.trigger.execution.ended'
|
|
4499
|
-
:
|
|
5075
|
+
: fullAccessAuthorization.source === 'agent-policy'
|
|
5076
|
+
? 'fullaccess.agent-policy.execution.ended' : 'fullaccess.execution.ended',
|
|
4500
5077
|
source: fullAccessAuthorization.source,
|
|
4501
5078
|
actorId: fullAccessAuthorization.authorizedBy,
|
|
4502
5079
|
processRole: fullAccessAuditProcessRole,
|
|
@@ -4694,7 +5271,7 @@ export class ResponseEngine {
|
|
|
4694
5271
|
logger.warn(`[ResponseEngine] Bound session ${boundSessionId} not found, falling back to latest`);
|
|
4695
5272
|
}
|
|
4696
5273
|
}
|
|
4697
|
-
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, message.topicName, message.peerId, message.chatType, resolvedBaseagent, message.selfAID, message.channelType, message.peerType, resolvedIdentity);
|
|
5274
|
+
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, message.topicName, message.peerId, message.chatType, resolvedBaseagent, message.selfAID, message.channelType, message.peerType, resolvedIdentity, message.lable);
|
|
4698
5275
|
this.ensureSessionBaseagent(session, resolvedBaseagent);
|
|
4699
5276
|
// 群名解析:群会话首次取群显示名(group.get),缓存到 metadata,供信封渲染。
|
|
4700
5277
|
// 渠道私有方法 getGroupName 自带进程缓存 + 容错;取不到不阻塞(groupName 保持空,模板回退 groupId)。
|
|
@@ -4752,7 +5329,7 @@ export class ResponseEngine {
|
|
|
4752
5329
|
*/
|
|
4753
5330
|
async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive,
|
|
4754
5331
|
/** [迁移点4/5] 响应模式插件 + 状态,用于调 onToolUse/onComplete 钩子 */
|
|
4755
|
-
modeHooks, causation, turnLease, permissionMode, proactiveSelfAid) {
|
|
5332
|
+
modeHooks, causation, turnLease, permissionMode, proactiveSelfAid, onAsyncQuestion) {
|
|
4756
5333
|
// Per-session agent name for stats bucketing
|
|
4757
5334
|
const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
|
|
4758
5335
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
|
|
@@ -4839,6 +5416,15 @@ export class ResponseEngine {
|
|
|
4839
5416
|
// It must never be projected as progress, activity, text, or transcript.
|
|
4840
5417
|
if (event.type === 'heartbeat')
|
|
4841
5418
|
continue;
|
|
5419
|
+
if (event.type === 'user_question') {
|
|
5420
|
+
if (!onAsyncQuestion)
|
|
5421
|
+
throw new Error('Async question has no delivery context');
|
|
5422
|
+
await onAsyncQuestion(event.request);
|
|
5423
|
+
madeProgress = true;
|
|
5424
|
+
// A structured question is user-visible progress without being a text reply.
|
|
5425
|
+
textCount++;
|
|
5426
|
+
continue;
|
|
5427
|
+
}
|
|
4842
5428
|
if (event.type === 'input_accepted') {
|
|
4843
5429
|
inputAccepted = !turnLease || event.inputId === turnLease.inputId;
|
|
4844
5430
|
logger.debug(`[ResponseEngine] Input accepted: session=${session.id} input=${event.inputId}`);
|
|
@@ -4934,7 +5520,7 @@ export class ResponseEngine {
|
|
|
4934
5520
|
|| (typeof input?.prompt === 'string' ? input.prompt.slice(0, 80) : '')
|
|
4935
5521
|
|| (typeof input?.query === 'string' ? input.query.slice(0, 80) : '')
|
|
4936
5522
|
|| '';
|
|
4937
|
-
eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
|
|
5523
|
+
eventDetail = ` tool=${event.name}${desc ? ` desc="${String(desc).replace(/\s+/g, ' ')}"` : ''}`;
|
|
4938
5524
|
}
|
|
4939
5525
|
else if (event.type === 'tool_result') {
|
|
4940
5526
|
const decision = event.decision ?? (event.isError ? 'error' : 'allow');
|
|
@@ -4954,6 +5540,10 @@ export class ResponseEngine {
|
|
|
4954
5540
|
}
|
|
4955
5541
|
// IMRenderer 旁路:proactive 模式逐事件投影为 thought(fire-and-forget)
|
|
4956
5542
|
renderer.emit(event);
|
|
5543
|
+
if (event.type === 'text_start') {
|
|
5544
|
+
await renderer.sendTextStart(event.contentIndex, event.turn);
|
|
5545
|
+
continue;
|
|
5546
|
+
}
|
|
4957
5547
|
// session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
|
|
4958
5548
|
if (event.type === 'session_id') {
|
|
4959
5549
|
logger.debug(`[ResponseEngine] Session ID updated: ${event.sessionId} for session: ${session.id}`);
|
|
@@ -5115,7 +5705,7 @@ export class ResponseEngine {
|
|
|
5115
5705
|
this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: visibleText, isFinal: false });
|
|
5116
5706
|
}
|
|
5117
5707
|
if (visibleText && !shouldSuppress()) {
|
|
5118
|
-
renderer.addText(visibleText, event.outputTokens, event.turn);
|
|
5708
|
+
await renderer.addText(visibleText, event.outputTokens, event.turn, event.assistantUuid);
|
|
5119
5709
|
hasProjectedCurrentReplyText = true;
|
|
5120
5710
|
}
|
|
5121
5711
|
}
|
|
@@ -5175,7 +5765,7 @@ export class ResponseEngine {
|
|
|
5175
5765
|
timestamp: Date.now(),
|
|
5176
5766
|
causation,
|
|
5177
5767
|
});
|
|
5178
|
-
const desc =
|
|
5768
|
+
const desc = summarizeToolInputForDisplay(event.name, event.input || {});
|
|
5179
5769
|
const pendingInput = { name: event.name, input: event.input || {} };
|
|
5180
5770
|
if (event.callId)
|
|
5181
5771
|
toolInputByCallId.set(event.callId, pendingInput);
|
|
@@ -5351,7 +5941,7 @@ export class ResponseEngine {
|
|
|
5351
5941
|
let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
|
|
5352
5942
|
// 移除 XML 风格的错误标签
|
|
5353
5943
|
errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
|
|
5354
|
-
renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, event.durationMs, cachedDesc);
|
|
5944
|
+
renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, event.durationMs, cachedDesc, event);
|
|
5355
5945
|
}
|
|
5356
5946
|
else if (!event.isError && !shouldSuppress()) {
|
|
5357
5947
|
renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc);
|
|
@@ -5452,7 +6042,7 @@ export class ResponseEngine {
|
|
|
5452
6042
|
// User interrupt (AbortError) is expected, log at info level
|
|
5453
6043
|
const catchInterruptReason = this.interruptedSessions.get(session.id);
|
|
5454
6044
|
const catchIsUserInterrupt = isNonErrorInterruptReason(catchInterruptReason);
|
|
5455
|
-
if (catchInterruptReason === 'timeout') {
|
|
6045
|
+
if (catchInterruptReason === 'timeout' || catchInterruptReason === 'execution_error') {
|
|
5456
6046
|
logger.info('[ResponseEngine] Stream closed by timeout barrier');
|
|
5457
6047
|
throw this.timeoutErrors.get(session.id) ?? new Error('SDK_TIMEOUT');
|
|
5458
6048
|
}
|
|
@@ -5490,7 +6080,9 @@ export class ResponseEngine {
|
|
|
5490
6080
|
if (hasErrorSuppressingContent) {
|
|
5491
6081
|
let errorOutputFlushed = false;
|
|
5492
6082
|
try {
|
|
5493
|
-
|
|
6083
|
+
// The outer retry/terminal handler has not decided the outcome yet.
|
|
6084
|
+
// Pending stream output must not announce successful completion.
|
|
6085
|
+
await renderer.flush(false);
|
|
5494
6086
|
errorOutputFlushed = true;
|
|
5495
6087
|
}
|
|
5496
6088
|
catch (flushError) {
|