evolcore 0.0.26 → 0.0.28
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 +36 -0
- package/bin/codex-managed-acl.ps1 +43 -0
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/baseagent.js +10 -5
- package/dist/agents/claude-runner.js +123 -22
- package/dist/agents/codex/app-server-client.js +70 -26
- package/dist/agents/codex/catalog.js +7 -1
- package/dist/agents/codex-runner.js +363 -421
- 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} +13 -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 +544 -239
- 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 +111 -162
- package/dist/cli/diag.js +3 -3
- 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 +13 -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 +81 -19
- 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 +105 -40
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/diag-service.js +292 -27
- 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 +364 -96
- package/dist/core/command/menu-protocol.js +43 -6
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-handler.js +61 -37
- 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 +94 -16
- package/dist/core/message/message-bridge.js +140 -13
- 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 +729 -118
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +76 -36
- 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 +8 -8
- 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 +38 -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 +322 -207
- package/dist/ipc.js +141 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +3 -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/atomic-write.js +98 -0
- package/dist/utils/cross-platform.js +87 -16
- 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/npm-ops.js +119 -4
- package/dist/utils/package-version-info.js +244 -0
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +174 -1
- package/dist/utils/windows-shell-trust.js +37 -0
- 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 +23 -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,13 @@
|
|
|
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 { isDiagnosticSessionActive } from '../command/diag-service.js';
|
|
9
|
+
import { evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission } from './message-admission.js';
|
|
10
|
+
import { authorizeAccess, authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
6
11
|
import { buildTaskRuntimeEnv, ensureRuntimeLockDir, ensureSessionRuntimeDir } from '../../cli/task-context.js';
|
|
7
12
|
import { IMRenderer } from './im-renderer.js';
|
|
8
13
|
import { createSendFileMarkerPattern } from './file-markers.js';
|
|
@@ -10,11 +15,11 @@ import { createTextInferenceProvider } from '../inference/text-inference.js';
|
|
|
10
15
|
import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
11
16
|
import { logger } from '../../utils/logger.js';
|
|
12
17
|
import { isHostChinese } from '../../utils/locale.js';
|
|
13
|
-
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
|
|
18
|
+
import { ExecutionTimeoutError, OrphanedTargetSessionError, isExecutionFailure, getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
|
|
14
19
|
import { readEvolcoreRuntimeFlavor, tagErrorTextWithEvolcoreVersion } from '../../utils/evolcore-version.js';
|
|
15
20
|
import { describeProactiveSendCommandFailure, isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
|
|
16
|
-
import {
|
|
17
|
-
import { agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
21
|
+
import { summarizeToolInputForDisplay } from '../../utils/tool-summary.js';
|
|
22
|
+
import { agentDataDir, agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
18
23
|
import { renderKitSections } from '../../eck/kit-renderer.js';
|
|
19
24
|
import { renderMessageBody } from '../../eck/message-renderer.js';
|
|
20
25
|
import { syncGroupRulesContext } from '../../eck/group-rules-sync.js';
|
|
@@ -22,8 +27,9 @@ import { consumeHints, hintsToSubMessages, composeHintFallback } from './pending
|
|
|
22
27
|
import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
|
|
23
28
|
import { recordCausationLink, recordCausationSpan } from '../causation/audit.js';
|
|
24
29
|
import { normalizeBaseagent } from '../../agents/baseagent.js';
|
|
30
|
+
import { renderActionAsText } from '../interaction-router.js';
|
|
25
31
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
26
|
-
import { buildEnvelope, replyContextFromSession, withEnvelopeReplyContext } from './message-utils.js';
|
|
32
|
+
import { buildEnvelope, isInteractionSendAccepted, replyContextFromSession, sendInteractionPayload, withEnvelopeReplyContext } from './message-utils.js';
|
|
27
33
|
import { isSystemOrServicePeer, resolveChatMode } from './peer-mode.js';
|
|
28
34
|
import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
|
|
29
35
|
import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
|
|
@@ -52,7 +58,290 @@ import { classifyToolErrorCode, normalizeToolErrorCode } from '../permission/too
|
|
|
52
58
|
import { normalizeExecutionPermissionMode } from '../permission/mode.js';
|
|
53
59
|
import { buildToolLifecycleEventKey } from '../audit/event-key.js';
|
|
54
60
|
import { isFullAccessEnabled, loadDaemonConfig } from '../../config-store.js';
|
|
61
|
+
import { assertAgentPolicyAuthorization } from '../role/runtime-policy.js';
|
|
55
62
|
import { auditFullAccessEvent } from '../auth/authorization-audit.js';
|
|
63
|
+
import { TaskRecoveryService, isTaskRecoveryCurrent } from '../task/task-recovery-service.js';
|
|
64
|
+
const QUESTION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
65
|
+
/** Pending async questions outlive the originating turn, but never change its backend binding. */
|
|
66
|
+
export class AsyncQuestionService {
|
|
67
|
+
deps;
|
|
68
|
+
registered = new Map();
|
|
69
|
+
deliveries = new Map();
|
|
70
|
+
constructor(deps) {
|
|
71
|
+
this.deps = deps;
|
|
72
|
+
}
|
|
73
|
+
directory(selfAid) {
|
|
74
|
+
return this.deps.directory?.(selfAid) ?? path.join(agentDataDir(selfAid), 'async-questions');
|
|
75
|
+
}
|
|
76
|
+
file(selfAid, id) {
|
|
77
|
+
if (!/^aq-[a-f0-9]{40}$/.test(id))
|
|
78
|
+
throw new Error('Invalid async question id');
|
|
79
|
+
return path.join(this.directory(selfAid), `${id}.json`);
|
|
80
|
+
}
|
|
81
|
+
save(record) {
|
|
82
|
+
fs.mkdirSync(this.directory(record.selfAid), { recursive: true, mode: 0o700 });
|
|
83
|
+
const destination = this.file(record.selfAid, record.id);
|
|
84
|
+
const temporary = `${destination}.tmp`;
|
|
85
|
+
const fd = fs.openSync(temporary, 'w', 0o600);
|
|
86
|
+
try {
|
|
87
|
+
fs.writeFileSync(fd, JSON.stringify(record));
|
|
88
|
+
fs.fsyncSync(fd);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
fs.closeSync(fd);
|
|
92
|
+
}
|
|
93
|
+
// Replace atomically, without unlinking the previous durable state first.
|
|
94
|
+
fs.renameSync(temporary, destination);
|
|
95
|
+
}
|
|
96
|
+
read(selfAid, id) {
|
|
97
|
+
try {
|
|
98
|
+
const value = JSON.parse(fs.readFileSync(this.file(selfAid, id), 'utf8'));
|
|
99
|
+
if (value.version !== 1 || value.selfAid !== selfAid || value.id !== id
|
|
100
|
+
|| !value.sessionId || !value.providerThreadId || !value.itemId
|
|
101
|
+
|| !value.origin?.peerId || !value.channelKey || !Number.isFinite(value.expiresAt))
|
|
102
|
+
return undefined;
|
|
103
|
+
parseAsyncUserQuestions([value.question]);
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error.code !== 'ENOENT')
|
|
108
|
+
throw error;
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async present(session, request, message, adapter, envelope) {
|
|
113
|
+
if (!session.selfAID || !message.peerId)
|
|
114
|
+
throw new Error('Async question requires an authenticated route');
|
|
115
|
+
const questions = parseAsyncUserQuestions(request.questions);
|
|
116
|
+
const records = [];
|
|
117
|
+
for (const [index, question] of questions.entries()) {
|
|
118
|
+
const hash = crypto.createHash('sha256').update(JSON.stringify([
|
|
119
|
+
session.selfAID, session.id, request.providerThreadId, request.itemId, index,
|
|
120
|
+
])).digest('hex').slice(0, 40);
|
|
121
|
+
const id = `aq-${hash}`;
|
|
122
|
+
const existing = this.read(session.selfAID, id);
|
|
123
|
+
if (existing && existing.status !== 'pending')
|
|
124
|
+
continue;
|
|
125
|
+
const replyContext = {
|
|
126
|
+
delivery: envelope.delivery ?? envelope.replyContext?.delivery,
|
|
127
|
+
threadId: session.threadId || undefined,
|
|
128
|
+
metadata: {
|
|
129
|
+
encrypted: message.encrypted ?? envelope.replyContext?.metadata?.encrypted,
|
|
130
|
+
chatmode: envelope.chatmode,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const record = existing ?? {
|
|
134
|
+
version: 1, id, sessionId: session.id, selfAid: session.selfAID,
|
|
135
|
+
providerThreadId: request.providerThreadId, providerTurnId: request.providerTurnId,
|
|
136
|
+
itemId: request.itemId, question,
|
|
137
|
+
origin: {
|
|
138
|
+
channel: message.channel, channelType: session.channelType,
|
|
139
|
+
channelId: session.channelId, peerId: message.peerId, peerType: message.peerType,
|
|
140
|
+
chatType: session.chatType === 'group' ? 'group' : 'private',
|
|
141
|
+
threadId: session.threadId || undefined, selfAID: session.selfAID,
|
|
142
|
+
},
|
|
143
|
+
envelope: { ...envelope, sessionId: session.id, replyContext, delivery: replyContext.delivery },
|
|
144
|
+
channelKey: adapter.channelKey, createdAt: Date.now(), expiresAt: Date.now() + QUESTION_TTL_MS,
|
|
145
|
+
status: 'pending',
|
|
146
|
+
};
|
|
147
|
+
if (!existing)
|
|
148
|
+
this.save(record);
|
|
149
|
+
records.push(record);
|
|
150
|
+
}
|
|
151
|
+
// Persist the complete batch before dispatch; one failed card must not lose the other questions.
|
|
152
|
+
const errors = [];
|
|
153
|
+
for (const record of records) {
|
|
154
|
+
try {
|
|
155
|
+
await this.deliver(record, adapter);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
errors.push(error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (errors.length)
|
|
162
|
+
throw new AggregateError(errors, 'Async question delivery failed; pending questions retained');
|
|
163
|
+
}
|
|
164
|
+
async restore(adapter, selfAid) {
|
|
165
|
+
let names;
|
|
166
|
+
try {
|
|
167
|
+
names = fs.readdirSync(this.directory(selfAid));
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error.code === 'ENOENT')
|
|
171
|
+
return;
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
for (const name of names) {
|
|
175
|
+
if (!/^aq-[a-f0-9]{40}\.json$/.test(name))
|
|
176
|
+
continue;
|
|
177
|
+
try {
|
|
178
|
+
const record = this.read(selfAid, name.slice(0, -5));
|
|
179
|
+
if (!record || record.channelKey !== adapter.channelKey)
|
|
180
|
+
continue;
|
|
181
|
+
if (this.registered.get(record.id) === adapter)
|
|
182
|
+
continue;
|
|
183
|
+
if (record.expiresAt <= Date.now()) {
|
|
184
|
+
fs.unlinkSync(this.file(selfAid, record.id));
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (record.status === 'submitting') {
|
|
188
|
+
// A crash may have happened after Codex accepted the steer. Never replay it blindly.
|
|
189
|
+
record.status = 'uncertain';
|
|
190
|
+
record.reason = 'restart_during_submission';
|
|
191
|
+
this.save(record);
|
|
192
|
+
}
|
|
193
|
+
if (record.status === 'pending')
|
|
194
|
+
await this.deliver(record, adapter);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
logger.warn(`[AsyncQuestions] restore failed: id=${name} error=${String(error)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async validSession(record) {
|
|
202
|
+
const session = await this.deps.getSession(record.sessionId);
|
|
203
|
+
if (!session || session.baseagent !== 'codex' || session.selfAID !== record.selfAid
|
|
204
|
+
|| session.agentSessionId !== record.providerThreadId
|
|
205
|
+
|| session.channel !== record.origin.channel
|
|
206
|
+
|| session.channelType !== record.origin.channelType
|
|
207
|
+
|| session.channelId !== record.origin.channelId
|
|
208
|
+
|| (session.threadId || '') !== (record.origin.threadId || '')
|
|
209
|
+
|| !(await this.deps.authorize(record, session)))
|
|
210
|
+
return undefined;
|
|
211
|
+
return session;
|
|
212
|
+
}
|
|
213
|
+
/** Recheck persisted answers after queue delays/restarts, immediately before a provider run. */
|
|
214
|
+
async validateQueuedAnswer(session, answer) {
|
|
215
|
+
if (!session.selfAID)
|
|
216
|
+
return false;
|
|
217
|
+
const record = this.read(session.selfAID, answer.requestId);
|
|
218
|
+
return !!record && record.sessionId === session.id
|
|
219
|
+
&& ['submitting', 'answered', 'uncertain'].includes(record.status)
|
|
220
|
+
&& record.providerThreadId === answer.providerThreadId
|
|
221
|
+
&& record.itemId === answer.questionItemId && record.question.title === answer.question
|
|
222
|
+
&& record.answer === answer.answer && !!(await this.validSession(record));
|
|
223
|
+
}
|
|
224
|
+
async deliver(record, adapter) {
|
|
225
|
+
if (this.registered.get(record.id) === adapter)
|
|
226
|
+
return;
|
|
227
|
+
// A channel reload replaces its callback/card registry. Rebind pending questions to the new adapter.
|
|
228
|
+
if (this.registered.has(record.id))
|
|
229
|
+
await this.deps.router.cancel(record.id, 'daemon_restart');
|
|
230
|
+
const previous = this.deliveries.get(record.id);
|
|
231
|
+
if (previous)
|
|
232
|
+
return previous;
|
|
233
|
+
const delivery = this.deliverOnce(record, adapter);
|
|
234
|
+
this.deliveries.set(record.id, delivery);
|
|
235
|
+
try {
|
|
236
|
+
await delivery;
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
this.deliveries.delete(record.id);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async deliverOnce(record, adapter) {
|
|
243
|
+
if (record.expiresAt <= Date.now() || !(await this.validSession(record))) {
|
|
244
|
+
record.status = 'cancelled';
|
|
245
|
+
record.reason = 'expired_or_binding_changed';
|
|
246
|
+
this.save(record);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const options = record.question.options ?? [];
|
|
250
|
+
const interaction = {
|
|
251
|
+
type: 'interaction', id: record.id, sessionId: record.sessionId, channelId: record.origin.channelId,
|
|
252
|
+
initiatorId: record.origin.peerId, blocking: false, expiresAt: record.expiresAt,
|
|
253
|
+
kind: {
|
|
254
|
+
kind: 'action', title: '请补充信息', body: record.question.title, bodyFormat: 'plain',
|
|
255
|
+
buttons: options.map((label, index) => ({ key: `opt-${index}`, label })),
|
|
256
|
+
allowCustomInput: true,
|
|
257
|
+
},
|
|
258
|
+
fallback: {
|
|
259
|
+
command: 'ask',
|
|
260
|
+
buttonArgMap: Object.fromEntries(options.map((_, index) => [`opt-${index}`, `${record.id} ${index + 1}`])),
|
|
261
|
+
acceptFreeText: true, freeTextHint: `回复 /ask ${record.id} <回答>`,
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
// Register before dispatch: a fast reply must not race card delivery.
|
|
265
|
+
this.registered.set(record.id, adapter);
|
|
266
|
+
this.deps.router.register(record.id, record.sessionId, async (action, values) => {
|
|
267
|
+
const current = this.read(record.selfAid, record.id);
|
|
268
|
+
if (!current || current.status !== 'pending' || current.expiresAt <= Date.now()) {
|
|
269
|
+
await this.deps.router.cancel(record.id, 'expired_or_resolved');
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const answer = this.answerText(current.question, action, values);
|
|
273
|
+
if (!answer)
|
|
274
|
+
return false;
|
|
275
|
+
const session = await this.validSession(current);
|
|
276
|
+
if (!session) {
|
|
277
|
+
await this.deps.router.cancel(record.id, 'binding_or_authorization_changed');
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
current.answer = answer;
|
|
281
|
+
current.status = 'submitting';
|
|
282
|
+
this.save(current);
|
|
283
|
+
try {
|
|
284
|
+
await this.deps.submit(current, session, {
|
|
285
|
+
requestId: current.id, providerThreadId: current.providerThreadId,
|
|
286
|
+
questionItemId: current.itemId, question: current.question.title, answer,
|
|
287
|
+
});
|
|
288
|
+
current.status = 'answered';
|
|
289
|
+
this.save(current);
|
|
290
|
+
this.registered.delete(record.id);
|
|
291
|
+
logger.info(`[AsyncQuestions] answer accepted: session=${record.sessionId} request=${record.id}`);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
current.status = 'uncertain';
|
|
296
|
+
current.reason = 'submission_failed';
|
|
297
|
+
this.save(current);
|
|
298
|
+
logger.warn(`[AsyncQuestions] answer submission failed: request=${record.id} error=${String(error)}`);
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}, {
|
|
302
|
+
initiatorId: record.origin.peerId, initiatorChannelKey: adapter.channelKey,
|
|
303
|
+
blocking: false, retainOnRejected: true, fallbackCommand: 'ask', timeoutMs: record.expiresAt - Date.now(),
|
|
304
|
+
onCancel: async (reason) => {
|
|
305
|
+
this.registered.delete(record.id);
|
|
306
|
+
if (reason === 'daemon_restart')
|
|
307
|
+
return;
|
|
308
|
+
const current = this.read(record.selfAid, record.id);
|
|
309
|
+
if (current?.status === 'pending') {
|
|
310
|
+
current.status = 'cancelled';
|
|
311
|
+
current.reason = reason;
|
|
312
|
+
this.save(current);
|
|
313
|
+
}
|
|
314
|
+
await adapter.invalidateInteraction?.(record.id, reason);
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
try {
|
|
318
|
+
const receipt = await sendInteractionPayload(adapter, {
|
|
319
|
+
...record.envelope, operationId: `${record.id}-${Date.now()}`,
|
|
320
|
+
}, interaction, renderActionAsText(interaction), record.envelope.replyContext);
|
|
321
|
+
if (!isInteractionSendAccepted(receipt))
|
|
322
|
+
throw new Error('Async question card was not accepted for delivery');
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
// Keep the durable pending record for a later channel reconnect/daemon restore.
|
|
326
|
+
await this.deps.router.cancel(record.id, 'daemon_restart');
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
answerText(question, action, values) {
|
|
331
|
+
if (action === 'cancel' || action === 'dismiss' || values?.behavior === 'dismiss')
|
|
332
|
+
return undefined;
|
|
333
|
+
let answer;
|
|
334
|
+
if (action === '_custom_input')
|
|
335
|
+
answer = typeof values?.custom_text === 'string' ? values.custom_text.trim() : undefined;
|
|
336
|
+
else if (/^opt-\d+$/.test(action))
|
|
337
|
+
answer = question.options?.[Number(action.slice(4))];
|
|
338
|
+
else if (question.options?.length && /^\d+$/.test(action))
|
|
339
|
+
answer = question.options[Number(action) - 1];
|
|
340
|
+
else
|
|
341
|
+
answer = action.trim();
|
|
342
|
+
return answer && answer.length <= 65_536 ? answer : undefined;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
56
345
|
const INBOUND_MESSAGE_LOG_TEXT_MAX_LENGTH = 240;
|
|
57
346
|
export function formatInboundMessageLogText(text) {
|
|
58
347
|
const singleLine = text.replace(/\s+/gu, ' ').trim();
|
|
@@ -519,12 +808,15 @@ export class ResponseEngine {
|
|
|
519
808
|
primaryRunnerKey;
|
|
520
809
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
521
810
|
timeoutErrors = new Map();
|
|
811
|
+
activeTimeoutHandlers = new Map();
|
|
522
812
|
/** sessionId → 已成功的模型降级状态(进程重启清零) */
|
|
523
813
|
modelFallbackMap = new Map();
|
|
524
814
|
/** sessionId → interruptible task-level API retry delays. */
|
|
525
815
|
retryDelayWaiters = new Map();
|
|
526
816
|
interactionRouter;
|
|
817
|
+
asyncQuestions;
|
|
527
818
|
messageQueue;
|
|
819
|
+
taskRecoveryService;
|
|
528
820
|
handoffRuntime;
|
|
529
821
|
/** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
|
|
530
822
|
activeMonitors = new Map();
|
|
@@ -598,10 +890,7 @@ export class ResponseEngine {
|
|
|
598
890
|
return createTextInferenceProvider(baseagent, config);
|
|
599
891
|
}
|
|
600
892
|
getAvailableBaseagentsForOwner(evolName) {
|
|
601
|
-
|
|
602
|
-
return [...this.agentMap.keys()]
|
|
603
|
-
.filter(key => key.startsWith(prefix))
|
|
604
|
-
.map(key => key.slice(prefix.length));
|
|
893
|
+
return loadedBaseagentIds(this.agentMap.keys(), evolName);
|
|
605
894
|
}
|
|
606
895
|
async sendBaseagentMismatch(channelKey, channelId, session, error, replyContext) {
|
|
607
896
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
@@ -742,6 +1031,12 @@ export class ResponseEngine {
|
|
|
742
1031
|
}
|
|
743
1032
|
setInteractionRouter(router) {
|
|
744
1033
|
this.interactionRouter = router;
|
|
1034
|
+
this.asyncQuestions = new AsyncQuestionService({
|
|
1035
|
+
router,
|
|
1036
|
+
getSession: id => this.sessionManager.getSessionById(id),
|
|
1037
|
+
authorize: async (record, session) => this.authorizeAsyncQuestion(record, session),
|
|
1038
|
+
submit: (record, session, answer) => this.submitAsyncQuestionAnswer(record, session, answer),
|
|
1039
|
+
});
|
|
745
1040
|
// 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
|
|
746
1041
|
// 避免把「正在等用户点按钮」误判为「任务卡死」而中断任务。
|
|
747
1042
|
router.setWaitHooks({
|
|
@@ -752,9 +1047,118 @@ export class ResponseEngine {
|
|
|
752
1047
|
this.activeMonitors.get(sessionId)?.resume();
|
|
753
1048
|
},
|
|
754
1049
|
});
|
|
1050
|
+
for (const { adapter } of this.channels.values())
|
|
1051
|
+
this.restoreAsyncQuestions(adapter);
|
|
755
1052
|
}
|
|
756
1053
|
setMessageQueue(queue) {
|
|
757
1054
|
this.messageQueue = queue;
|
|
1055
|
+
this.taskRecoveryService = new TaskRecoveryService({
|
|
1056
|
+
sessions: this.sessionManager,
|
|
1057
|
+
queue,
|
|
1058
|
+
getRunner: (session) => this.getAgent(session.metadata?.channelKey || session.channel, session.baseagent, session.selfAID),
|
|
1059
|
+
resumePause: (sessionId) => this.resumeSession(sessionId),
|
|
1060
|
+
hasChannel: (session) => !!this.resolveChannelInfo(session.metadata?.channelKey || session.channel),
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
async recoverTask(request) {
|
|
1064
|
+
if (!this.taskRecoveryService) {
|
|
1065
|
+
return { ok: false, status: 'not_recoverable', sessionId: request.sessionId, message: '消息队列尚未初始化' };
|
|
1066
|
+
}
|
|
1067
|
+
return this.taskRecoveryService.resume(request);
|
|
1068
|
+
}
|
|
1069
|
+
authorizeAsyncQuestion(record, session) {
|
|
1070
|
+
const origin = record.origin;
|
|
1071
|
+
const owner = this.agentRegistry?.resolveByChannel(origin.channel);
|
|
1072
|
+
if (owner?.aid !== record.selfAid)
|
|
1073
|
+
return false;
|
|
1074
|
+
const chatType = origin.chatType === 'group' ? 'group' : 'private';
|
|
1075
|
+
const channelType = origin.channelType || record.channelKey.split('#')[0];
|
|
1076
|
+
const preflight = evaluateInboundAdmissionPreflight({
|
|
1077
|
+
selfAid: record.selfAid, channelKey: record.channelKey, channelType, chatType, actorId: origin.peerId,
|
|
1078
|
+
});
|
|
1079
|
+
if (!preflight.allow || !evaluateOrdinaryInboundAdmission(preflight.context).allow)
|
|
1080
|
+
return false;
|
|
1081
|
+
const identity = this.inferDirectSessionIdentity({ ...origin, content: '' }, owner);
|
|
1082
|
+
if (!identity || identity.role === 'none')
|
|
1083
|
+
return false;
|
|
1084
|
+
return authorizeAccess(buildAuthSubject({
|
|
1085
|
+
selfAid: record.selfAid, channel: record.channelKey, channelType, chatType,
|
|
1086
|
+
actorId: origin.peerId, channelId: session.channelId, conversationId: session.channelId, identity,
|
|
1087
|
+
})).allow;
|
|
1088
|
+
}
|
|
1089
|
+
async submitAsyncQuestionAnswer(record, session, answer) {
|
|
1090
|
+
const runner = this.getAgent(record.origin.channel, 'codex', record.selfAid);
|
|
1091
|
+
if (!runner.steerAsyncQuestionAnswer)
|
|
1092
|
+
throw new Error('Codex async input is unavailable');
|
|
1093
|
+
if (await runner.steerAsyncQuestionAnswer(session.id, answer) === 'accepted')
|
|
1094
|
+
return;
|
|
1095
|
+
if (!this.messageQueue)
|
|
1096
|
+
throw new Error('Async question resume queue is unavailable');
|
|
1097
|
+
// The queue owns idle-thread restoration and a fresh task/permission context.
|
|
1098
|
+
// Keep the exact Session/backend identity; never create a new topic for an old card.
|
|
1099
|
+
await this.messageQueue.enqueuePersisted(session.id, {
|
|
1100
|
+
...record.origin, content: formatAsyncQuestionAnswer(answer),
|
|
1101
|
+
asyncQuestionAnswer: answer, boundSessionId: session.id, baseagent: 'codex',
|
|
1102
|
+
source: 'card-trigger', messageId: `async-answer-${record.id}`,
|
|
1103
|
+
timestamp: Date.now(), receivedAt: Date.now(),
|
|
1104
|
+
encrypted: record.envelope.replyContext?.metadata?.encrypted,
|
|
1105
|
+
replyContext: record.envelope.replyContext,
|
|
1106
|
+
}, session.projectPath, {
|
|
1107
|
+
interruptible: false, interruptSamePeer: false,
|
|
1108
|
+
agentName: this.agentRegistry?.resolveByChannel(record.origin.channel)?.name,
|
|
1109
|
+
selfAID: record.selfAid, sessionKeyField: session.sessionKey,
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
/** Read the exact attempt's lifecycle without touching any newer task. */
|
|
1113
|
+
async inspectTargetExecution(sessionId, attemptId, selfAid) {
|
|
1114
|
+
const session = await this.sessionManager.getSessionById(sessionId);
|
|
1115
|
+
if (!session || session.deletedAt || session.selfAID !== selfAid)
|
|
1116
|
+
return { state: 'missing' };
|
|
1117
|
+
const state = this.turnCoordinator.current(session);
|
|
1118
|
+
const terminal = state.lastTerminal;
|
|
1119
|
+
const matches = (context) => (context?.triggerAttemptId ?? context?.recovery?.attemptId) === attemptId;
|
|
1120
|
+
if (terminal && matches(terminal.recoveryContext)) {
|
|
1121
|
+
return { state: 'terminal', taskId: terminal.taskId, generation: terminal.generation,
|
|
1122
|
+
outcome: terminal.status === 'committed' ? 'completed'
|
|
1123
|
+
: ['cancelled', 'superseded', 'interrupted'].includes(terminal.status) ? 'skipped' : 'failed',
|
|
1124
|
+
reason: terminal.reason };
|
|
1125
|
+
}
|
|
1126
|
+
const queued = this.messageQueue?.getMessageState(sessionId, attemptId, selfAid);
|
|
1127
|
+
if (queued?.state === 'queued')
|
|
1128
|
+
return { state: 'live' };
|
|
1129
|
+
const active = state.active;
|
|
1130
|
+
if (active && matches(active.recoveryContext)) {
|
|
1131
|
+
const identity = { taskId: active.taskId, generation: active.generation };
|
|
1132
|
+
if (active.status === 'waiting_interaction' || active.status === 'interrupting'
|
|
1133
|
+
|| active.interactions.some(item => !item.settlement)
|
|
1134
|
+
|| this.isPauseRequested(sessionId) || this.retryDelayWaiters.has(sessionId))
|
|
1135
|
+
return { state: 'live', ...identity };
|
|
1136
|
+
const runner = this.getAgent(session.metadata?.channelKey || session.channel, session.baseagent, selfAid);
|
|
1137
|
+
return { state: runner.hasActiveStream(sessionId) ? 'live' : 'missing', ...identity,
|
|
1138
|
+
hasTerminalOwner: this.activeTimeoutHandlers.has(sessionId) };
|
|
1139
|
+
}
|
|
1140
|
+
// Never adopt or interrupt a newer task to clean up an older attempt.
|
|
1141
|
+
if (active) {
|
|
1142
|
+
return { state: 'terminal', outcome: 'skipped', reason: 'stale_generation',
|
|
1143
|
+
taskId: active.taskId, generation: active.generation };
|
|
1144
|
+
}
|
|
1145
|
+
if (queued?.state === 'active') {
|
|
1146
|
+
return { state: 'missing', hasTerminalOwner: this.activeTimeoutHandlers.has(sessionId) };
|
|
1147
|
+
}
|
|
1148
|
+
if (queued?.state === 'missing' && (active || terminal)) {
|
|
1149
|
+
const current = active ?? terminal;
|
|
1150
|
+
return { state: 'terminal', outcome: 'skipped', reason: 'stale_generation',
|
|
1151
|
+
taskId: current.taskId, generation: current.generation };
|
|
1152
|
+
}
|
|
1153
|
+
return { state: 'missing' };
|
|
1154
|
+
}
|
|
1155
|
+
/** Outer execution deadlines use the same terminal owner as runner idle timeouts. */
|
|
1156
|
+
async timeoutSession(sessionId, error) {
|
|
1157
|
+
const timeout = this.activeTimeoutHandlers.get(sessionId);
|
|
1158
|
+
if (timeout)
|
|
1159
|
+
await timeout(error);
|
|
1160
|
+
else
|
|
1161
|
+
await this.interruptSession(sessionId, error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout');
|
|
758
1162
|
}
|
|
759
1163
|
async interruptSession(sessionId, reason) {
|
|
760
1164
|
this.clearPauseSession(sessionId);
|
|
@@ -808,7 +1212,7 @@ export class ResponseEngine {
|
|
|
808
1212
|
// TimeoutController owns timeout delivery (including user-facing timeout
|
|
809
1213
|
// details) and already guarantees a single terminal status. Sending it
|
|
810
1214
|
// here as well would race that barrier and duplicate status.timeout.
|
|
811
|
-
if (!interruptedTaskId || effectiveReason === 'timeout')
|
|
1215
|
+
if (!interruptedTaskId || effectiveReason === 'timeout' || effectiveReason === 'execution_error')
|
|
812
1216
|
return Promise.resolve();
|
|
813
1217
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
814
1218
|
if (!channelInfo)
|
|
@@ -930,6 +1334,8 @@ export class ResponseEngine {
|
|
|
930
1334
|
agentRegistry;
|
|
931
1335
|
setAgentRegistry(registry) {
|
|
932
1336
|
this.agentRegistry = registry;
|
|
1337
|
+
for (const { adapter } of this.channels.values())
|
|
1338
|
+
this.restoreAsyncQuestions(adapter);
|
|
933
1339
|
}
|
|
934
1340
|
/** 更新 EvolAgent.lastActivity —— 每次发出 status.* 事件(含 progress)时调用 */
|
|
935
1341
|
touchAgentActivity(channelKey) {
|
|
@@ -1026,6 +1432,15 @@ export class ResponseEngine {
|
|
|
1026
1432
|
if (!this.channelTypeMap.has(type)) {
|
|
1027
1433
|
this.channelTypeMap.set(type, adapter.channelName);
|
|
1028
1434
|
}
|
|
1435
|
+
this.restoreAsyncQuestions(adapter);
|
|
1436
|
+
}
|
|
1437
|
+
restoreAsyncQuestions(adapter) {
|
|
1438
|
+
const selfAid = this.agentRegistry?.resolveByChannel(adapter.channelKey)?.aid;
|
|
1439
|
+
if (selfAid && this.asyncQuestions) {
|
|
1440
|
+
void this.asyncQuestions.restore(adapter, selfAid).catch(error => {
|
|
1441
|
+
logger.warn(`[AsyncQuestions] channel restore failed: ${String(error)}`);
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1029
1444
|
}
|
|
1030
1445
|
/**
|
|
1031
1446
|
* 注销渠道适配器(热重载断开渠道时调用,避免遗留死实例)。
|
|
@@ -1219,7 +1634,10 @@ export class ResponseEngine {
|
|
|
1219
1634
|
&& (!authorization.triggerId || !authorization.runId || !authorization.attemptId)) {
|
|
1220
1635
|
throw new Error('fullaccess Trigger authorization lacks scheduler identity or approval provenance');
|
|
1221
1636
|
}
|
|
1222
|
-
if (authorization.source
|
|
1637
|
+
if (authorization.source === 'agent-policy') {
|
|
1638
|
+
assertAgentPolicyAuthorization(authorization);
|
|
1639
|
+
}
|
|
1640
|
+
else if (authorization.source !== 'fullaccess-command' && authorization.source !== 'trigger') {
|
|
1223
1641
|
throw new Error('fullaccess execution authorization has an unsupported source');
|
|
1224
1642
|
}
|
|
1225
1643
|
if (!isFullAccessEnabled()) {
|
|
@@ -1241,6 +1659,8 @@ export class ResponseEngine {
|
|
|
1241
1659
|
const originChannelKey = message.triggerMeta.originChannelKey;
|
|
1242
1660
|
if (!selfAid || !actorId || !channelType || !originChannelKey)
|
|
1243
1661
|
return undefined;
|
|
1662
|
+
const chatType = message.triggerMeta.originChatType ?? 'private';
|
|
1663
|
+
const conversationId = message.triggerMeta.originConversationId ?? actorId;
|
|
1244
1664
|
const control = message.triggerMeta.originControl === true;
|
|
1245
1665
|
let actor;
|
|
1246
1666
|
let resolvedRole = 'none';
|
|
@@ -1249,9 +1669,9 @@ export class ResponseEngine {
|
|
|
1249
1669
|
selfAid,
|
|
1250
1670
|
channelKey: originChannelKey,
|
|
1251
1671
|
channelType,
|
|
1252
|
-
chatType
|
|
1672
|
+
chatType,
|
|
1253
1673
|
actorId,
|
|
1254
|
-
conversationId
|
|
1674
|
+
conversationId,
|
|
1255
1675
|
});
|
|
1256
1676
|
actor = detail.actor;
|
|
1257
1677
|
resolvedRole = detail.effectiveRole || 'none';
|
|
@@ -1271,7 +1691,7 @@ export class ResponseEngine {
|
|
|
1271
1691
|
channelType,
|
|
1272
1692
|
originChannelKey,
|
|
1273
1693
|
control,
|
|
1274
|
-
peerKey: formatPeerKey(channelType, actorId),
|
|
1694
|
+
peerKey: formatPeerKey(channelType, chatType === 'group' ? conversationId : actorId),
|
|
1275
1695
|
};
|
|
1276
1696
|
}
|
|
1277
1697
|
resolveHandoffOriginExecutionIdentity(message, selfAid) {
|
|
@@ -1437,7 +1857,8 @@ export class ResponseEngine {
|
|
|
1437
1857
|
auditFullAccessEvent({
|
|
1438
1858
|
event: fallbackFullAccessAudit.authorization.source === 'trigger'
|
|
1439
1859
|
? 'fullaccess.trigger.execution.ended'
|
|
1440
|
-
:
|
|
1860
|
+
: fallbackFullAccessAudit.authorization.source === 'agent-policy'
|
|
1861
|
+
? 'fullaccess.agent-policy.execution.ended' : 'fullaccess.execution.ended',
|
|
1441
1862
|
source: fallbackFullAccessAudit.authorization.source,
|
|
1442
1863
|
actorId: fallbackFullAccessAudit.authorization.authorizedBy,
|
|
1443
1864
|
processRole: fallbackFullAccessAudit.processRole,
|
|
@@ -1458,6 +1879,9 @@ export class ResponseEngine {
|
|
|
1458
1879
|
async processMessageWithMonitors(message) {
|
|
1459
1880
|
const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
|
|
1460
1881
|
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
1882
|
+
// A discarded handoff must be dropped before any bound-session lookup.
|
|
1883
|
+
// Restored origin deliveries may point at a session that has already been
|
|
1884
|
+
// replaced; resolving it first would reactivate the stale session path.
|
|
1461
1885
|
if (message.handoffDelivery
|
|
1462
1886
|
&& this.handoffRuntime
|
|
1463
1887
|
&& this.handoffRuntime.isDiscardedDelivery(message)) {
|
|
@@ -1466,9 +1890,32 @@ export class ResponseEngine {
|
|
|
1466
1890
|
+ ` handoff=${message.handoffDelivery.handoffId}`);
|
|
1467
1891
|
return;
|
|
1468
1892
|
}
|
|
1893
|
+
// Includes persisted queues, recovery triggers and delayed owner replies.
|
|
1894
|
+
// A closed diagnostic must not activate its old main session again.
|
|
1895
|
+
const diagnosticBoundId = message.boundSessionId ?? message.triggerMeta?.boundSessionId;
|
|
1896
|
+
if (diagnosticBoundId) {
|
|
1897
|
+
const bound = await this.sessionManager.getSessionById(diagnosticBoundId);
|
|
1898
|
+
if (bound && (bound.metadata?.diagnostic || bound.metadata?.diagnosticStatus === 'cancelled')
|
|
1899
|
+
&& !isDiagnosticSessionActive(bound, this.sessionManager.getChatDir(bound))) {
|
|
1900
|
+
logger.info(`[ResponseEngine] Skip closed diagnostic session ${bound.id}`);
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (message.taskRecovery) {
|
|
1905
|
+
const current = message.boundSessionId
|
|
1906
|
+
? await this.sessionManager.getSessionById(message.boundSessionId)
|
|
1907
|
+
: undefined;
|
|
1908
|
+
if (!current || current.selfAID !== message.selfAID || !isTaskRecoveryCurrent(current, message.taskRecovery)) {
|
|
1909
|
+
this.publishTriggerExecutionSkipped(message, 'recovery_stale');
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1469
1913
|
// 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
|
|
1470
1914
|
// message.channel 现在存实例名(channelName),可直接用于精确路由
|
|
1471
1915
|
const { session, absoluteProjectPath } = await this.resolveSession(message);
|
|
1916
|
+
if ((session.metadata?.diagnostic || session.metadata?.diagnosticStatus === 'cancelled')
|
|
1917
|
+
&& !isDiagnosticSessionActive(session, this.sessionManager.getChatDir(session)))
|
|
1918
|
+
return;
|
|
1472
1919
|
const accessChannelKey = this.messageChannelKey(message, session);
|
|
1473
1920
|
const accessChannelInfo = this.resolveChannelInfo(accessChannelKey);
|
|
1474
1921
|
const accessAdapter = accessChannelInfo?.adapter;
|
|
@@ -1520,7 +1967,8 @@ export class ResponseEngine {
|
|
|
1520
1967
|
const userRole = session.identity?.role || 'none';
|
|
1521
1968
|
const isInternalHandoff = message.source === 'handoff';
|
|
1522
1969
|
const fullAccessAuditCandidate = this.resolveFullAccessAuditCandidate(message);
|
|
1523
|
-
const isCurrentFullAccessCommand = fullAccessAuditCandidate?.authorization.source === 'fullaccess-command'
|
|
1970
|
+
const isCurrentFullAccessCommand = (fullAccessAuditCandidate?.authorization.source === 'fullaccess-command'
|
|
1971
|
+
|| fullAccessAuditCandidate?.authorization.source === 'agent-policy')
|
|
1524
1972
|
&& fullAccessAuditCandidate.processRole === 'fullaccess-run';
|
|
1525
1973
|
const isCurrentFullAccessTrigger = fullAccessAuditCandidate?.authorization.source === 'trigger'
|
|
1526
1974
|
&& fullAccessAuditCandidate.processRole === 'fullaccess-run';
|
|
@@ -1609,11 +2057,17 @@ export class ResponseEngine {
|
|
|
1609
2057
|
claimedByInternal: false,
|
|
1610
2058
|
terminalDelivered: false,
|
|
1611
2059
|
claim: () => {
|
|
1612
|
-
if (!timeoutControl.error || this.interruptedSessions.get(session.id)
|
|
2060
|
+
if (!timeoutControl.error || !['timeout', 'execution_error'].includes(this.interruptedSessions.get(session.id) ?? ''))
|
|
1613
2061
|
return undefined;
|
|
1614
2062
|
timeoutControl.claimedByInternal = true;
|
|
1615
2063
|
return timeoutControl.error;
|
|
1616
2064
|
},
|
|
2065
|
+
claimTerminal: () => {
|
|
2066
|
+
if (timeoutControl.terminalDelivered)
|
|
2067
|
+
return false;
|
|
2068
|
+
timeoutControl.terminalDelivered = true;
|
|
2069
|
+
return true;
|
|
2070
|
+
},
|
|
1617
2071
|
};
|
|
1618
2072
|
const rejectAfterInterruptBarrier = (error) => {
|
|
1619
2073
|
if (timeoutTriggered)
|
|
@@ -1622,9 +2076,10 @@ export class ResponseEngine {
|
|
|
1622
2076
|
timeoutControl.error = error;
|
|
1623
2077
|
this.timeoutErrors.set(session.id, error);
|
|
1624
2078
|
this.turnCoordinator.hydrate(session);
|
|
1625
|
-
|
|
1626
|
-
this.
|
|
1627
|
-
|
|
2079
|
+
const reason = error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout';
|
|
2080
|
+
this.turnCoordinator.invalidate(session.id, reason);
|
|
2081
|
+
this.interruptedSessions.set(session.id, reason);
|
|
2082
|
+
const barrier = this.interruptSession(session.id, reason)
|
|
1628
2083
|
.catch(interruptError => {
|
|
1629
2084
|
logger.debug(`[ResponseEngine] Timeout interrupt barrier failed: ${interruptError instanceof Error ? interruptError.message : String(interruptError)}`);
|
|
1630
2085
|
});
|
|
@@ -1635,6 +2090,11 @@ export class ResponseEngine {
|
|
|
1635
2090
|
});
|
|
1636
2091
|
};
|
|
1637
2092
|
const triggerAttemptId = message.replyContext?.metadata?.triggerAttemptId;
|
|
2093
|
+
const externalTimeout = async (error) => {
|
|
2094
|
+
rejectAfterInterruptBarrier(error);
|
|
2095
|
+
await timeoutControl.barrier;
|
|
2096
|
+
};
|
|
2097
|
+
this.activeTimeoutHandlers.set(session.id, externalTimeout);
|
|
1638
2098
|
const resetTimer = (eventType, toolName) => {
|
|
1639
2099
|
monitor?.recordEvent(eventType || 'unknown', toolName);
|
|
1640
2100
|
if (typeof triggerAttemptId === 'string') {
|
|
@@ -1714,11 +2174,9 @@ export class ResponseEngine {
|
|
|
1714
2174
|
await Promise.race(processingPromises);
|
|
1715
2175
|
}
|
|
1716
2176
|
catch (error) {
|
|
1717
|
-
if (error instanceof Error && (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
|
|
2177
|
+
if (error instanceof Error && (isExecutionFailure(error) || error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
|
|
1718
2178
|
await timeoutControl.barrier;
|
|
1719
|
-
|
|
1720
|
-
await timeoutControl.deliverUnhandled?.(error);
|
|
1721
|
-
}
|
|
2179
|
+
await timeoutControl.deliverUnhandled?.(error);
|
|
1722
2180
|
this.timeoutErrors.delete(session.id);
|
|
1723
2181
|
}
|
|
1724
2182
|
// 超时错误:kill 级别已发送诊断信息,无需再发
|
|
@@ -1759,6 +2217,8 @@ export class ResponseEngine {
|
|
|
1759
2217
|
if (totalExecutionTimer)
|
|
1760
2218
|
clearTimeout(totalExecutionTimer);
|
|
1761
2219
|
this.activeMonitors.delete(streamKey);
|
|
2220
|
+
if (this.activeTimeoutHandlers.get(session.id) === externalTimeout)
|
|
2221
|
+
this.activeTimeoutHandlers.delete(session.id);
|
|
1762
2222
|
}
|
|
1763
2223
|
}
|
|
1764
2224
|
resolveFullAccessAuditCandidate(message) {
|
|
@@ -1766,15 +2226,18 @@ export class ResponseEngine {
|
|
|
1766
2226
|
if (commandAuthorization?.permissionMode === 'fullaccess'
|
|
1767
2227
|
&& commandAuthorization.processRole === 'fullaccess-run'
|
|
1768
2228
|
&& commandAuthorization.dataScope === 'daemon'
|
|
1769
|
-
&& commandAuthorization.source === 'fullaccess-command'
|
|
2229
|
+
&& (commandAuthorization.source === 'fullaccess-command' || commandAuthorization.source === 'agent-policy')
|
|
1770
2230
|
&& typeof commandAuthorization.authorizedBy === 'string'
|
|
1771
2231
|
&& commandAuthorization.authorizedBy.length > 0) {
|
|
1772
2232
|
let currentOwner = false;
|
|
1773
2233
|
try {
|
|
1774
2234
|
currentOwner = isFullAccessEnabled()
|
|
1775
2235
|
&& new Set(loadDaemonConfig().owners ?? []).has(commandAuthorization.authorizedBy);
|
|
2236
|
+
if (commandAuthorization.source === 'agent-policy')
|
|
2237
|
+
assertAgentPolicyAuthorization(commandAuthorization);
|
|
1776
2238
|
}
|
|
1777
2239
|
catch {
|
|
2240
|
+
currentOwner = false;
|
|
1778
2241
|
// Runtime validation will fail closed; the audit must still retain the
|
|
1779
2242
|
// attempted authorization without falsely labeling it active.
|
|
1780
2243
|
}
|
|
@@ -1894,7 +2357,7 @@ export class ResponseEngine {
|
|
|
1894
2357
|
// states. An untransitioned `created` agent or malformed lifecycle must
|
|
1895
2358
|
// not fall through to a base runner's default prompt. Missing lifecycle
|
|
1896
2359
|
// is normalized to active for legacy configurations.
|
|
1897
|
-
if (owningAgentForTask && !isActive && !isBootstrapping) {
|
|
2360
|
+
if (owningAgentForTask && ((!isActive && !isBootstrapping) || initializationBlocksTasks(owningAgentForTask.config))) {
|
|
1898
2361
|
const blockedLifecycle = lifecycle
|
|
1899
2362
|
?? `invalid (${String(owningAgentForTask.config.lifecycle)})`;
|
|
1900
2363
|
logger.error(`[ResponseEngine] Agent lifecycle is not runnable: agent=${owningAgentForTask.aid} lifecycle=${blockedLifecycle}`);
|
|
@@ -1991,7 +2454,7 @@ export class ResponseEngine {
|
|
|
1991
2454
|
?? this.resolveHandoffOriginExecutionIdentity(message, selfAid);
|
|
1992
2455
|
const configChannelType = authorizationIdentity?.channelType ?? currentChannelType;
|
|
1993
2456
|
const configChatType = authorizationIdentity
|
|
1994
|
-
? (message.handoffDelivery?.originAuthorization?.chatType ?? 'private')
|
|
2457
|
+
? (message.triggerMeta?.originChatType ?? message.handoffDelivery?.originAuthorization?.chatType ?? 'private')
|
|
1995
2458
|
: (chatType === 'group' ? 'group' : 'private');
|
|
1996
2459
|
// A group channel ID identifies the conversation, not the authenticated
|
|
1997
2460
|
// operator. Keep the historical private-chat fallback (where channelId
|
|
@@ -2179,25 +2642,30 @@ export class ResponseEngine {
|
|
|
2179
2642
|
});
|
|
2180
2643
|
if (timeoutControl) {
|
|
2181
2644
|
timeoutControl.deliverUnhandled = async (timeoutError) => {
|
|
2182
|
-
if (timeoutControl.
|
|
2645
|
+
if (!timeoutControl.claimTerminal())
|
|
2183
2646
|
return;
|
|
2184
|
-
timeoutControl.terminalDelivered = true;
|
|
2185
2647
|
const isTotalExecutionTimeout = timeoutError.message === 'TOTAL_EXECUTION_TIMEOUT';
|
|
2186
|
-
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
2187
|
-
const statusPayload = {
|
|
2648
|
+
const totalExecutionMs = timeoutError instanceof ExecutionTimeoutError ? timeoutError.timeoutMs : this.totalExecutionLimitMs();
|
|
2649
|
+
const statusPayload = timeoutError instanceof OrphanedTargetSessionError ? {
|
|
2650
|
+
kind: 'status.error',
|
|
2651
|
+
metadata: { errorType: timeoutError.reason, message: '任务执行状态丢失,已结束本次执行。' },
|
|
2652
|
+
} : {
|
|
2188
2653
|
kind: 'status.timeout',
|
|
2189
2654
|
metadata: isTotalExecutionTimeout
|
|
2190
|
-
? (totalExecutionMs === undefined ? {} : { totalExecutionMs })
|
|
2655
|
+
? { ...(totalExecutionMs === undefined ? {} : { totalExecutionMs }),
|
|
2656
|
+
...(timeoutError instanceof ExecutionTimeoutError ? { reason: timeoutError.reason } : {}) }
|
|
2191
2657
|
: { idleSec: getLastIdleSec?.() || undefined },
|
|
2192
2658
|
};
|
|
2193
2659
|
const idleSec = getLastIdleSec?.() || 0;
|
|
2194
|
-
const userMessage =
|
|
2195
|
-
?
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
:
|
|
2660
|
+
const userMessage = timeoutError instanceof OrphanedTargetSessionError
|
|
2661
|
+
? '⚠️ 任务执行状态丢失,已结束本次执行。'
|
|
2662
|
+
: isTotalExecutionTimeout
|
|
2663
|
+
? (totalExecutionMs === undefined
|
|
2664
|
+
? '⚠️ 任务超过总执行时限,已自动中断'
|
|
2665
|
+
: `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
|
|
2666
|
+
: idleSec > 0
|
|
2667
|
+
? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断`
|
|
2668
|
+
: '⚠️ 任务超时,已自动中断';
|
|
2201
2669
|
agent.cleanupStream(streamKey);
|
|
2202
2670
|
this.sessionManager.clearProcessingIfTask(session.id, taskId);
|
|
2203
2671
|
if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
|
|
@@ -2211,13 +2679,13 @@ export class ResponseEngine {
|
|
|
2211
2679
|
await adapter.send(envelope, {
|
|
2212
2680
|
kind: 'result.error',
|
|
2213
2681
|
text: userMessage,
|
|
2214
|
-
reason: isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
|
|
2682
|
+
reason: isExecutionFailure(timeoutError) ? timeoutError.reason : isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
|
|
2215
2683
|
}).catch(() => { });
|
|
2216
2684
|
if (daemonTrigger) {
|
|
2217
2685
|
await adapter.send(envelope, statusPayload).catch(() => { });
|
|
2218
2686
|
}
|
|
2219
2687
|
this.touchAgentActivity(channelKey);
|
|
2220
|
-
this.publishTriggerExecutionFailure(message, timeoutError.message, {
|
|
2688
|
+
this.publishTriggerExecutionFailure(message, isExecutionFailure(timeoutError) ? timeoutError.reason : timeoutError.message, {
|
|
2221
2689
|
messageId,
|
|
2222
2690
|
causation: taskCausation,
|
|
2223
2691
|
});
|
|
@@ -2225,9 +2693,14 @@ export class ResponseEngine {
|
|
|
2225
2693
|
this.eventBus.publish({
|
|
2226
2694
|
type: 'task:error',
|
|
2227
2695
|
sessionId: session.id,
|
|
2696
|
+
agentAid: runnerSelfAid,
|
|
2697
|
+
peerId: configActorId,
|
|
2698
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
2699
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
2228
2700
|
taskId,
|
|
2229
2701
|
generation: turnLease?.generation,
|
|
2230
2702
|
error: timeoutError.message,
|
|
2703
|
+
terminalReason: isExecutionFailure(timeoutError) ? timeoutError.reason : undefined,
|
|
2231
2704
|
errorType,
|
|
2232
2705
|
agentName: agentNameForStats,
|
|
2233
2706
|
causation: taskCausation,
|
|
@@ -2356,14 +2829,28 @@ export class ResponseEngine {
|
|
|
2356
2829
|
// message is authoritative; session metadata is only a fallback for
|
|
2357
2830
|
// legacy paths that do not carry a per-message display name.
|
|
2358
2831
|
const peerName = message.peerName ?? session.metadata?.peerName;
|
|
2359
|
-
const peerId =
|
|
2832
|
+
const peerId = message.peerId ?? session.metadata?.peerId ?? message.channelId;
|
|
2360
2833
|
const peerShort = peerId ? peerId.split('.')[0].split(':')[0] : '?';
|
|
2361
2834
|
const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
|
|
2362
2835
|
logger.info(`[ResponseEngine] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} chatMode=${effectiveChatMode} baseagent=${session.baseagent} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
2363
2836
|
// Establish the durable turn before exposing any processing state. This
|
|
2364
2837
|
// lets an immediate /stop or replacement message close the same task and
|
|
2365
2838
|
// prevents a BusinessTrip reaction from becoming orphaned during setup.
|
|
2366
|
-
|
|
2839
|
+
if (message.taskRecovery && !isTaskRecoveryCurrent({
|
|
2840
|
+
...session, metadata: { ...session.metadata, turnState: this.turnCoordinator.current(session) },
|
|
2841
|
+
}, message.taskRecovery)) {
|
|
2842
|
+
this.publishTriggerExecutionSkipped(message, 'recovery_stale');
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
turnLease = await this.turnCoordinator.begin(session, taskId, undefined, configActorId ? {
|
|
2846
|
+
peerId: configActorId,
|
|
2847
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
2848
|
+
baseagent: session.baseagent,
|
|
2849
|
+
agentSessionId: session.agentSessionId,
|
|
2850
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
2851
|
+
recovery: message.taskRecovery?.origin,
|
|
2852
|
+
triggerAttemptId: message.triggerMeta?.attemptId,
|
|
2853
|
+
} : undefined);
|
|
2367
2854
|
// 记录开始处理
|
|
2368
2855
|
const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
|
|
2369
2856
|
this.eventBus.publish({
|
|
@@ -2405,6 +2892,7 @@ export class ResponseEngine {
|
|
|
2405
2892
|
flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
|
|
2406
2893
|
suppressActivityItems: isProactive ? false : middleOutputMode !== 'all',
|
|
2407
2894
|
suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
|
|
2895
|
+
deferTextUntilBoundary: (message.channelType ?? session.channelType) === 'aun' && session.chatType === 'private',
|
|
2408
2896
|
operationalNoticesAsText: !isProactive && middleOutputMode !== 'none',
|
|
2409
2897
|
fileMarkerPattern: options?.fileMarkerPattern,
|
|
2410
2898
|
send: async (payload) => {
|
|
@@ -2440,7 +2928,10 @@ export class ResponseEngine {
|
|
|
2440
2928
|
if (payload.kind === 'result.text' && payload.isFinal) {
|
|
2441
2929
|
opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
|
|
2442
2930
|
}
|
|
2443
|
-
opts.metadata = withTaskMetadata(
|
|
2931
|
+
opts.metadata = withTaskMetadata({
|
|
2932
|
+
...(opts.metadata ?? {}),
|
|
2933
|
+
...(payload.kind === 'result.text' ? payload.metadata : {}),
|
|
2934
|
+
});
|
|
2444
2935
|
// status.notice is a diagnostic observation, not execution activity.
|
|
2445
2936
|
if (payload.kind.startsWith('status.') && payload.kind !== 'status.notice') {
|
|
2446
2937
|
this.touchAgentActivity(channelKey);
|
|
@@ -2585,10 +3076,10 @@ export class ResponseEngine {
|
|
|
2585
3076
|
};
|
|
2586
3077
|
})();
|
|
2587
3078
|
let effectivePermissionMode = 'readonly';
|
|
2588
|
-
// Allocate
|
|
2589
|
-
// context is installed.
|
|
2590
|
-
//
|
|
2591
|
-
const sessionRuntimeDir =
|
|
3079
|
+
// Allocate the stable session-owned root before any runner preflight
|
|
3080
|
+
// context is installed. Every runner for this EvolCore session receives
|
|
3081
|
+
// the same directory, independent of daemon or runner lifetimes.
|
|
3082
|
+
const sessionRuntimeDir = ensureSessionRuntimeDir(session.id);
|
|
2592
3083
|
const recordExecutionAnomaly = triggerRunId
|
|
2593
3084
|
? (anomaly) => {
|
|
2594
3085
|
recordTriggerExecutionAnomaly(triggerRunId, {
|
|
@@ -2636,8 +3127,8 @@ export class ResponseEngine {
|
|
|
2636
3127
|
pauseController: taskPauseController,
|
|
2637
3128
|
preToolUsePolicyHook: pureSessionPolicyHook,
|
|
2638
3129
|
armApprovedDelegationCommand: this.agentDelegationRegistry
|
|
2639
|
-
? (
|
|
2640
|
-
|
|
3130
|
+
? (sessionToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
|
|
3131
|
+
sessionToken,
|
|
2641
3132
|
sessionId: session.id,
|
|
2642
3133
|
taskId,
|
|
2643
3134
|
commandHash,
|
|
@@ -2812,10 +3303,19 @@ export class ResponseEngine {
|
|
|
2812
3303
|
if (commandFullAccess.permissionMode !== 'fullaccess'
|
|
2813
3304
|
|| commandFullAccess.processRole !== 'fullaccess-run'
|
|
2814
3305
|
|| commandFullAccess.dataScope !== 'daemon'
|
|
2815
|
-
|| commandFullAccess.source !== 'fullaccess-command'
|
|
3306
|
+
|| (commandFullAccess.source !== 'fullaccess-command' && commandFullAccess.source !== 'agent-policy')
|
|
2816
3307
|
|| !commandFullAccess.authorizedBy) {
|
|
2817
3308
|
throw new Error('fullaccess command authorization is missing or malformed');
|
|
2818
3309
|
}
|
|
3310
|
+
if (commandFullAccess.source === 'agent-policy'
|
|
3311
|
+
&& (commandFullAccess.agentAid !== runnerSelfAid
|
|
3312
|
+
|| configChatType !== 'private'
|
|
3313
|
+
|| (message.source !== undefined && message.source !== 'user')
|
|
3314
|
+
|| !hasTrustedPrincipal(authenticatedActor)
|
|
3315
|
+
|| authenticatedActor.principalId !== commandFullAccess.authorizedBy
|
|
3316
|
+
|| authenticatedActor.operatorId !== commandFullAccess.policyActor?.operatorId)) {
|
|
3317
|
+
throw new Error('Agent fullaccess policy authorization is missing its Agent identity');
|
|
3318
|
+
}
|
|
2819
3319
|
fullAccessAuthorization = commandFullAccess;
|
|
2820
3320
|
}
|
|
2821
3321
|
else {
|
|
@@ -3030,18 +3530,25 @@ export class ResponseEngine {
|
|
|
3030
3530
|
// happen before task-start auto compact because compact is a backend
|
|
3031
3531
|
// management action just like the subsequent runQuery.
|
|
3032
3532
|
let runAgentSessionId = session.agentSessionId;
|
|
3033
|
-
if (session.threadId
|
|
3533
|
+
if ((session.threadId || message.taskRecovery)
|
|
3034
3534
|
&& typeof this.sessionManager.getSessionById === 'function') {
|
|
3035
3535
|
const latestBackendSession = await this.sessionManager.getSessionById(session.id);
|
|
3036
|
-
if (!latestBackendSession || latestBackendSession.threadId !== session.threadId) {
|
|
3536
|
+
if (!latestBackendSession || latestBackendSession.deletedAt || latestBackendSession.threadId !== session.threadId) {
|
|
3037
3537
|
throw new Error('topic session changed before backend run started');
|
|
3038
3538
|
}
|
|
3539
|
+
if (message.taskRecovery && (latestBackendSession.agentSessionId !== message.taskRecovery.agentSessionId
|
|
3540
|
+
|| latestBackendSession.baseagent !== message.taskRecovery.baseagent)) {
|
|
3541
|
+
throw new Error('recovery backend changed before execution');
|
|
3542
|
+
}
|
|
3039
3543
|
runAgentSessionId = latestBackendSession.agentSessionId;
|
|
3040
3544
|
session.agentSessionId = runAgentSessionId;
|
|
3041
3545
|
session.metadata = latestBackendSession.metadata;
|
|
3042
3546
|
}
|
|
3043
3547
|
modelOverride = {
|
|
3044
3548
|
...(modelOverride || {}),
|
|
3549
|
+
...(message.taskRecovery
|
|
3550
|
+
? { recovery: { agentSessionId: message.taskRecovery.agentSessionId } }
|
|
3551
|
+
: {}),
|
|
3045
3552
|
...(session.threadId
|
|
3046
3553
|
? { backend: { kind: 'topic', agentSessionId: runAgentSessionId ?? null } }
|
|
3047
3554
|
: {}),
|
|
@@ -3054,13 +3561,17 @@ export class ResponseEngine {
|
|
|
3054
3561
|
},
|
|
3055
3562
|
};
|
|
3056
3563
|
const refreshTopicBindingForLogicalRetry = async () => {
|
|
3057
|
-
if (!session.threadId || typeof this.sessionManager.getSessionById !== 'function')
|
|
3564
|
+
if ((!session.threadId && !message.taskRecovery) || typeof this.sessionManager.getSessionById !== 'function')
|
|
3058
3565
|
return;
|
|
3059
3566
|
const latest = await this.sessionManager.getSessionById(session.id);
|
|
3060
3567
|
if (!latest || latest.threadId !== session.threadId) {
|
|
3061
3568
|
throw new Error('topic session changed during backend run');
|
|
3062
3569
|
}
|
|
3063
3570
|
const latestAgentSessionId = latest.agentSessionId;
|
|
3571
|
+
if (message.taskRecovery && (latestAgentSessionId !== message.taskRecovery.agentSessionId
|
|
3572
|
+
|| latest.baseagent !== message.taskRecovery.baseagent)) {
|
|
3573
|
+
throw new Error('recovery backend changed during execution');
|
|
3574
|
+
}
|
|
3064
3575
|
session.agentSessionId = latestAgentSessionId;
|
|
3065
3576
|
session.metadata = latest.metadata;
|
|
3066
3577
|
if (latestAgentSessionId === runAgentSessionId)
|
|
@@ -3097,6 +3608,26 @@ export class ResponseEngine {
|
|
|
3097
3608
|
groupId: session.metadata?.groupId || message.channelId,
|
|
3098
3609
|
channel: currentChannelType || message.channel,
|
|
3099
3610
|
});
|
|
3611
|
+
const historyGroupId = session.metadata?.groupId || message.channelId;
|
|
3612
|
+
const canUseGroupHistory = (operation) => {
|
|
3613
|
+
if (currentChannelType !== 'aun' || session.chatType !== 'group' || !selfAid || !historyGroupId)
|
|
3614
|
+
return false;
|
|
3615
|
+
const historySubject = buildAuthSubject({
|
|
3616
|
+
selfAid, actorId: configActorId, channel: channelKey, channelType: configChannelType,
|
|
3617
|
+
chatType: configChatType, channelId: message.channelId,
|
|
3618
|
+
conversationId: configChatType === 'group' ? historyGroupId : configActorId,
|
|
3619
|
+
permissionMode: effectivePermissionMode,
|
|
3620
|
+
});
|
|
3621
|
+
return authorizeOperation({
|
|
3622
|
+
source: 'agent-tool', subject: historySubject, audit: false,
|
|
3623
|
+
intent: {
|
|
3624
|
+
operation, scope: ['owner', 'admin'].includes(historySubject.role) ? 'agent' : 'relation',
|
|
3625
|
+
source: 'agent-tool',
|
|
3626
|
+
args: { self: selfAid, targetId: historyGroupId, peer: historyGroupId,
|
|
3627
|
+
peerKey: formatPeerKey('aun', historyGroupId) },
|
|
3628
|
+
},
|
|
3629
|
+
}).allow;
|
|
3630
|
+
};
|
|
3100
3631
|
// Kit renderer: 组装上下文
|
|
3101
3632
|
const pkgRoot = getPackageRoot();
|
|
3102
3633
|
const kitCtx = {
|
|
@@ -3128,11 +3659,12 @@ export class ResponseEngine {
|
|
|
3128
3659
|
peerKey,
|
|
3129
3660
|
peerName: peerName || undefined,
|
|
3130
3661
|
peerRole,
|
|
3131
|
-
|
|
3662
|
+
// Display missing identity explicitly without changing the mode resolver's legacy fallback.
|
|
3663
|
+
peerType: peerType || 'unknown',
|
|
3132
3664
|
sameDevice: message.sameDevice ?? false,
|
|
3133
3665
|
sameNetwork: message.sameNetwork ?? false,
|
|
3134
3666
|
sameEgressIp: message.sameEgressIp ?? false,
|
|
3135
|
-
groupId: session.
|
|
3667
|
+
groupId: session.chatType === 'group' ? historyGroupId : undefined,
|
|
3136
3668
|
groupName: session.metadata?.groupName || undefined,
|
|
3137
3669
|
// 信封展示用:有群名则「名<ID>」,否则纯 ID。规避模板引擎无 not/else 的限制。
|
|
3138
3670
|
groupLabel: session.metadata?.groupId
|
|
@@ -3147,6 +3679,7 @@ export class ResponseEngine {
|
|
|
3147
3679
|
?? session.metadata?.mentionMode
|
|
3148
3680
|
?? message.mentionMode
|
|
3149
3681
|
?? undefined,
|
|
3682
|
+
groupHistoryAvailable: canUseGroupHistory('ec.group.history'),
|
|
3150
3683
|
clientType: message.clientType || undefined,
|
|
3151
3684
|
permissionMode: effectivePermissionMode,
|
|
3152
3685
|
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
@@ -3343,11 +3876,9 @@ export class ResponseEngine {
|
|
|
3343
3876
|
}
|
|
3344
3877
|
}
|
|
3345
3878
|
}
|
|
3346
|
-
//
|
|
3347
|
-
//
|
|
3348
|
-
//
|
|
3349
|
-
// explicitly so sandboxed helper commands never fall back to a shared
|
|
3350
|
-
// agent or project directory for transient state.
|
|
3879
|
+
// Pass the stable session directory explicitly so sandboxed helper
|
|
3880
|
+
// commands never fall back to a process, agent, or project directory
|
|
3881
|
+
// for transient state.
|
|
3351
3882
|
const taskRuntimeContext = {
|
|
3352
3883
|
taskId,
|
|
3353
3884
|
sessionId: session.id,
|
|
@@ -3430,7 +3961,8 @@ export class ResponseEngine {
|
|
|
3430
3961
|
auditFullAccessEvent({
|
|
3431
3962
|
event: fullAccessAuthorization.source === 'trigger'
|
|
3432
3963
|
? 'fullaccess.trigger.execution.started'
|
|
3433
|
-
:
|
|
3964
|
+
: fullAccessAuthorization.source === 'agent-policy'
|
|
3965
|
+
? 'fullaccess.agent-policy.execution.started' : 'fullaccess.execution.started',
|
|
3434
3966
|
source: fullAccessAuthorization.source,
|
|
3435
3967
|
actorId: fullAccessAuthorization.authorizedBy,
|
|
3436
3968
|
processRole: fullAccessAuditProcessRole,
|
|
@@ -3454,6 +3986,23 @@ export class ResponseEngine {
|
|
|
3454
3986
|
}
|
|
3455
3987
|
fullAccessRunnerInvoked = true;
|
|
3456
3988
|
}
|
|
3989
|
+
if (message.asyncQuestionAnswer) {
|
|
3990
|
+
if (session.baseagent !== 'codex' || runAgentSessionId !== message.asyncQuestionAnswer.providerThreadId) {
|
|
3991
|
+
throw new Error('Async question backend changed before queued resume');
|
|
3992
|
+
}
|
|
3993
|
+
if (!(await this.asyncQuestions?.validateQueuedAnswer(session, message.asyncQuestionAnswer))) {
|
|
3994
|
+
throw new Error('Async question authorization or persisted answer changed before queued resume');
|
|
3995
|
+
}
|
|
3996
|
+
// An active queue item restored after submission resumes the unfinished work.
|
|
3997
|
+
// Its native answer is already in Codex history and must not be appended twice.
|
|
3998
|
+
if (!message.restartResume?.submitted) {
|
|
3999
|
+
modelOverride = { ...modelOverride, asyncQuestionAnswer: message.asyncQuestionAnswer };
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
if (session.baseagent === 'codex') {
|
|
4003
|
+
modelOverride = { ...modelOverride,
|
|
4004
|
+
enableRequestUserInput: effectiveAgentConfig?.baseagents?.codex?.enableRequestUserInput ?? true };
|
|
4005
|
+
}
|
|
3457
4006
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
|
|
3458
4007
|
// The turn may be superseded while the runner is still creating its
|
|
3459
4008
|
// backend query. Replay the interrupt after runQuery resolves, when
|
|
@@ -3470,7 +4019,23 @@ export class ResponseEngine {
|
|
|
3470
4019
|
const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
|
|
3471
4020
|
resetTimer(eventType, toolName);
|
|
3472
4021
|
attemptTimeout?.reset();
|
|
3473
|
-
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid)
|
|
4022
|
+
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid, async (request) => {
|
|
4023
|
+
if (!this.asyncQuestions)
|
|
4024
|
+
throw new Error('Async question service is unavailable');
|
|
4025
|
+
if (permissionContext.approvalInteractionPolicy === 'deny') {
|
|
4026
|
+
logger.info(`[AsyncQuestions] no interactive recipient for unattended task: session=${session.id}`);
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
await renderer.flush(false);
|
|
4030
|
+
try {
|
|
4031
|
+
await this.asyncQuestions.present(session, request, message, adapter, envelope);
|
|
4032
|
+
}
|
|
4033
|
+
catch (error) {
|
|
4034
|
+
// Card dispatch is non-blocking business input. Keep consuming the running turn;
|
|
4035
|
+
// pending records can be delivered after reconnect/restart.
|
|
4036
|
+
logger.warn(`[AsyncQuestions] presentation failed: session=${session.id} error=${String(error)}`);
|
|
4037
|
+
}
|
|
4038
|
+
});
|
|
3474
4039
|
streamResult = attemptTimeout
|
|
3475
4040
|
? await Promise.race([processAttempt, attemptTimeout.promise])
|
|
3476
4041
|
: await processAttempt;
|
|
@@ -3889,7 +4454,7 @@ export class ResponseEngine {
|
|
|
3889
4454
|
// `hasReceivedText` 是任务级计数。工具调用前的文字已经显示,并不表示最后一轮
|
|
3890
4455
|
// (通常来自 complete.result)的文字也已显示;用本轮投影标记避免终态丢失。
|
|
3891
4456
|
if (shouldSuppress() || !streamResult.hasProjectedCurrentReplyText) {
|
|
3892
|
-
renderer.addText(visibleFinalReplyText);
|
|
4457
|
+
await renderer.addText(visibleFinalReplyText, undefined, undefined, streamResult.assistantUuid);
|
|
3893
4458
|
}
|
|
3894
4459
|
}
|
|
3895
4460
|
// 先清理流和处理中状态(保证即使 flush 卡住,session 也不会永久处于"处理中")
|
|
@@ -3908,7 +4473,7 @@ export class ResponseEngine {
|
|
|
3908
4473
|
const suffix = showHint
|
|
3909
4474
|
? `\n\n---\n⚠️ [降级模型: ${usedFallback.model} | 可告诉我"帮我检查可用模型"来诊断]`
|
|
3910
4475
|
: `\n\n---\n⚠️ [降级模型: ${usedFallback.model}]`;
|
|
3911
|
-
renderer.addText(suffix);
|
|
4476
|
+
await renderer.addText(suffix, undefined, undefined, streamResult.assistantUuid);
|
|
3912
4477
|
if (showHint && curFbState) {
|
|
3913
4478
|
curFbState.hintShown = true;
|
|
3914
4479
|
this.modelFallbackMap.set(session.id, curFbState);
|
|
@@ -3921,7 +4486,14 @@ export class ResponseEngine {
|
|
|
3921
4486
|
}
|
|
3922
4487
|
else {
|
|
3923
4488
|
// Flush 剩余内容(文件标记已在 flush 时自动移除)
|
|
3924
|
-
|
|
4489
|
+
// A final text flush can also contain failed output. Preserve the
|
|
4490
|
+
// outcome so AUN does not label that text as normal completion.
|
|
4491
|
+
if (streamResult.isError && !isExpectedTerminalInterrupt(streamResult.terminalReason)) {
|
|
4492
|
+
await renderer.flush(true, 'failed');
|
|
4493
|
+
}
|
|
4494
|
+
else {
|
|
4495
|
+
await renderer.flush(!isExpectedTerminalInterrupt(streamResult.terminalReason));
|
|
4496
|
+
}
|
|
3925
4497
|
}
|
|
3926
4498
|
const terminalInterrupt = isExpectedTerminalInterrupt(streamResult.terminalReason);
|
|
3927
4499
|
if (!streamResult.isError && !terminalInterrupt) {
|
|
@@ -3944,7 +4516,8 @@ export class ResponseEngine {
|
|
|
3944
4516
|
}
|
|
3945
4517
|
}
|
|
3946
4518
|
else {
|
|
3947
|
-
await this.turnCoordinator.finish(session, turnLease, terminalInterrupt ? 'cancelled' : 'failed',
|
|
4519
|
+
await this.turnCoordinator.finish(session, turnLease, terminalInterrupt ? 'cancelled' : 'failed', permissionTerminalFailure ? 'permission_denied'
|
|
4520
|
+
: streamResult.protocolIncompleteReason || streamResult.terminalReason || streamResult.subtype);
|
|
3948
4521
|
}
|
|
3949
4522
|
// 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
|
|
3950
4523
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
@@ -3978,7 +4551,7 @@ export class ResponseEngine {
|
|
|
3978
4551
|
// DaemonChannel settles a trigger when it receives a terminal
|
|
3979
4552
|
// payload. Send the metadata-bearing status first so failed runs do
|
|
3980
4553
|
// not lose their tool counts and recovery diagnostics.
|
|
3981
|
-
if (message
|
|
4554
|
+
if (this.isTrustedDaemonTrigger(message)) {
|
|
3982
4555
|
await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
|
|
3983
4556
|
await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
|
|
3984
4557
|
}
|
|
@@ -4019,6 +4592,10 @@ export class ResponseEngine {
|
|
|
4019
4592
|
this.eventBus.publish({
|
|
4020
4593
|
type: 'task:error',
|
|
4021
4594
|
sessionId: session.id,
|
|
4595
|
+
agentAid: runnerSelfAid,
|
|
4596
|
+
peerId: configActorId,
|
|
4597
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
4598
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
4022
4599
|
taskId,
|
|
4023
4600
|
generation: turnLease?.generation,
|
|
4024
4601
|
error: errorSummary,
|
|
@@ -4297,7 +4874,8 @@ export class ResponseEngine {
|
|
|
4297
4874
|
await timeoutControl?.barrier;
|
|
4298
4875
|
}
|
|
4299
4876
|
const handlingTimeout = error instanceof Error
|
|
4300
|
-
&& (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT');
|
|
4877
|
+
&& (isExecutionFailure(error) || error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT');
|
|
4878
|
+
const ownsTimeoutTerminal = !handlingTimeout || !timeoutControl || timeoutControl.claimTerminal();
|
|
4301
4879
|
if (handlingTimeout) {
|
|
4302
4880
|
this.timeoutErrors.delete(session.id);
|
|
4303
4881
|
}
|
|
@@ -4319,11 +4897,12 @@ export class ResponseEngine {
|
|
|
4319
4897
|
const errType = classifyError(error);
|
|
4320
4898
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
4321
4899
|
const isTotalExecutionTimeout = error instanceof Error && error.message === 'TOTAL_EXECUTION_TIMEOUT';
|
|
4322
|
-
const isTimeout = error instanceof Error && error.message === 'SDK_TIMEOUT';
|
|
4900
|
+
const isTimeout = error instanceof Error && (error.message === 'SDK_TIMEOUT' || error instanceof ExecutionTimeoutError);
|
|
4323
4901
|
const canPublishTerminal = !turnLease || this.turnCoordinator.canPublish(turnLease);
|
|
4324
4902
|
const staleSupersededTurn = !!turnLease
|
|
4325
4903
|
&& !canPublishTerminal
|
|
4326
4904
|
&& interruptReason !== 'timeout'
|
|
4905
|
+
&& interruptReason !== 'execution_error'
|
|
4327
4906
|
&& !isTimeout
|
|
4328
4907
|
&& !isTotalExecutionTimeout;
|
|
4329
4908
|
const isUserInterrupt = isNonErrorInterruptReason(interruptReason) || staleSupersededTurn;
|
|
@@ -4331,7 +4910,7 @@ export class ResponseEngine {
|
|
|
4331
4910
|
if (isUserInterrupt) {
|
|
4332
4911
|
await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'stale_turn', taskCausation);
|
|
4333
4912
|
}
|
|
4334
|
-
else {
|
|
4913
|
+
else if (ownsTimeoutTerminal) {
|
|
4335
4914
|
this.publishTriggerExecutionFailure(message, errorMsg, {
|
|
4336
4915
|
messageId,
|
|
4337
4916
|
causation: taskCausation,
|
|
@@ -4340,24 +4919,27 @@ export class ResponseEngine {
|
|
|
4340
4919
|
if (leaseWasCurrent && turnLease) {
|
|
4341
4920
|
await this.turnCoordinator.finish(session, turnLease, 'failed', error instanceof Error ? error.message : String(error));
|
|
4342
4921
|
}
|
|
4343
|
-
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
4922
|
+
const totalExecutionMs = error instanceof ExecutionTimeoutError ? error.timeoutMs : this.totalExecutionLimitMs();
|
|
4344
4923
|
const procStatus = errType === ErrorType.SDK_TIMEOUT || isTotalExecutionTimeout ? 'timeout'
|
|
4345
4924
|
: errType === ErrorType.STREAM_ERROR ? 'interrupted'
|
|
4346
4925
|
: 'error';
|
|
4347
4926
|
const daemonTrigger = this.isTrustedDaemonTrigger(message);
|
|
4348
|
-
const statusPayload =
|
|
4349
|
-
? { kind: 'status.
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4927
|
+
const statusPayload = error instanceof OrphanedTargetSessionError
|
|
4928
|
+
? { kind: 'status.error', metadata: { errorType: error.reason, message: '任务执行状态丢失,已结束本次执行。' } }
|
|
4929
|
+
: procStatus === 'timeout'
|
|
4930
|
+
? { kind: 'status.timeout', metadata: isTotalExecutionTimeout
|
|
4931
|
+
? { ...(totalExecutionMs === undefined ? {} : { totalExecutionMs }),
|
|
4932
|
+
...(error instanceof ExecutionTimeoutError ? { reason: error.reason } : {}) }
|
|
4933
|
+
: { idleSec: getLastIdleSec?.() || undefined } }
|
|
4934
|
+
: procStatus === 'interrupted'
|
|
4935
|
+
? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
|
|
4936
|
+
: { kind: 'status.error' };
|
|
4355
4937
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
4356
|
-
if (!isUserInterrupt && !daemonTrigger) {
|
|
4938
|
+
if (!isUserInterrupt && ownsTimeoutTerminal && !daemonTrigger) {
|
|
4357
4939
|
await adapter.send(envelope, statusPayload).catch(() => { });
|
|
4358
4940
|
this.touchAgentActivity(channelKey);
|
|
4359
4941
|
}
|
|
4360
|
-
if (!isUserInterrupt && daemonTrigger)
|
|
4942
|
+
if (!isUserInterrupt && ownsTimeoutTerminal && daemonTrigger)
|
|
4361
4943
|
this.touchAgentActivity(channelKey);
|
|
4362
4944
|
// 用户主动中断时降级日志;其余仍按 error 记录
|
|
4363
4945
|
if (isUserInterrupt) {
|
|
@@ -4371,13 +4953,18 @@ export class ResponseEngine {
|
|
|
4371
4953
|
// (message-queue / slash-handler)已发过 task:interrupted(它填充了
|
|
4372
4954
|
// interruptedSessions,isUserInterrupt 才会为真),stats 侧已据此收尾任务。
|
|
4373
4955
|
// 此处不再发任何事件——发 task:error 会误归类,重发 task:interrupted 会重复记账。
|
|
4374
|
-
if (!isUserInterrupt) {
|
|
4956
|
+
if (!isUserInterrupt && ownsTimeoutTerminal) {
|
|
4375
4957
|
this.eventBus.publish({
|
|
4376
4958
|
type: 'task:error',
|
|
4377
4959
|
sessionId: session.id,
|
|
4960
|
+
agentAid: runnerSelfAid,
|
|
4961
|
+
peerId: configActorId,
|
|
4962
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
4963
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
4378
4964
|
taskId,
|
|
4379
4965
|
generation: turnLease?.generation,
|
|
4380
4966
|
error: errorMsg,
|
|
4967
|
+
terminalReason: isExecutionFailure(error) ? error.reason : undefined,
|
|
4381
4968
|
errorType,
|
|
4382
4969
|
agentName: agentNameForStats,
|
|
4383
4970
|
causation: taskCausation,
|
|
@@ -4385,14 +4972,19 @@ export class ResponseEngine {
|
|
|
4385
4972
|
recordCausationSpan(taskCausation, 'task.run', { status: 'failed', refs: { taskId, sessionId: session.id, messageId }, reason: errorType });
|
|
4386
4973
|
}
|
|
4387
4974
|
// 记录处理失败
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4975
|
+
if (ownsTimeoutTerminal) {
|
|
4976
|
+
logger.message({
|
|
4977
|
+
msgId: messageId,
|
|
4978
|
+
sessionId: session.id,
|
|
4979
|
+
dir: 'inbound',
|
|
4980
|
+
status: isUserInterrupt ? 'interrupted' : 'failed',
|
|
4981
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4982
|
+
});
|
|
4983
|
+
}
|
|
4984
|
+
else {
|
|
4985
|
+
logger.info(`[ResponseEngine] Duplicate timeout terminal suppressed: session=${session.id} task=${taskId}`);
|
|
4986
|
+
}
|
|
4987
|
+
if (error instanceof Error && !isUserInterrupt && ownsTimeoutTerminal) {
|
|
4396
4988
|
logger.error(`[${message.channel}] Error stack:`, error.stack);
|
|
4397
4989
|
}
|
|
4398
4990
|
// 发送用户友好的错误消息
|
|
@@ -4402,7 +4994,11 @@ export class ResponseEngine {
|
|
|
4402
4994
|
const retryExhaustedCount = getRetryExhaustedCount(error);
|
|
4403
4995
|
const modelFallbackExhaustedMessage = getModelFallbackExhaustedMessage(error);
|
|
4404
4996
|
const retryInputAlreadySubmitted = wasRetryInputAlreadySubmitted(error);
|
|
4405
|
-
if (
|
|
4997
|
+
if (!ownsTimeoutTerminal) {
|
|
4998
|
+
// The monitor path already owns delivery for this timeout. Cleanup and
|
|
4999
|
+
// durable turn finalization above still run, but no terminal is emitted twice.
|
|
5000
|
+
}
|
|
5001
|
+
else if (isUserInterrupt) {
|
|
4406
5002
|
logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
|
|
4407
5003
|
}
|
|
4408
5004
|
else if (error?._errorAlreadySent && !daemonTrigger && !retryExhaustedCount && !isTimeout && !isTotalExecutionTimeout) {
|
|
@@ -4411,19 +5007,21 @@ export class ResponseEngine {
|
|
|
4411
5007
|
else {
|
|
4412
5008
|
// SDK_TIMEOUT:status.timeout 已发结构化状态,此处再补一条用户可见的错误文本(result.error)
|
|
4413
5009
|
const idleSec = getLastIdleSec?.() || 0;
|
|
4414
|
-
const userMessage =
|
|
4415
|
-
?
|
|
4416
|
-
:
|
|
4417
|
-
?
|
|
4418
|
-
:
|
|
4419
|
-
?
|
|
4420
|
-
:
|
|
4421
|
-
?
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
:
|
|
5010
|
+
const userMessage = error instanceof OrphanedTargetSessionError
|
|
5011
|
+
? '⚠️ 任务执行状态丢失,已结束本次执行。'
|
|
5012
|
+
: retryExhaustedCount
|
|
5013
|
+
? formatRetryableErrorFinalMessage(error, retryExhaustedCount)
|
|
5014
|
+
: retryInputAlreadySubmitted
|
|
5015
|
+
? formatRetryInputAlreadySubmittedMessage(error)
|
|
5016
|
+
: modelFallbackExhaustedMessage
|
|
5017
|
+
? modelFallbackExhaustedMessage
|
|
5018
|
+
: isTotalExecutionTimeout
|
|
5019
|
+
? (totalExecutionMs === undefined
|
|
5020
|
+
? '⚠️ 任务超过总执行时限,已自动中断'
|
|
5021
|
+
: `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
|
|
5022
|
+
: isTimeout
|
|
5023
|
+
? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
|
|
5024
|
+
: getErrorMessage(error, undefined);
|
|
4427
5025
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
4428
5026
|
let sendOpts;
|
|
4429
5027
|
try {
|
|
@@ -4440,7 +5038,7 @@ export class ResponseEngine {
|
|
|
4440
5038
|
const errorPayload = {
|
|
4441
5039
|
kind: 'result.error',
|
|
4442
5040
|
text: userMessage,
|
|
4443
|
-
reason: retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
|
|
5041
|
+
reason: isExecutionFailure(error) ? error.reason : retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
|
|
4444
5042
|
};
|
|
4445
5043
|
if (!isUserInterrupt && daemonTrigger && procStatus === 'error') {
|
|
4446
5044
|
// DaemonChannel settles a trigger on the first terminal payload. Put
|
|
@@ -4483,9 +5081,6 @@ export class ResponseEngine {
|
|
|
4483
5081
|
}).catch(() => { });
|
|
4484
5082
|
}
|
|
4485
5083
|
}
|
|
4486
|
-
if (handlingTimeout && timeoutControl) {
|
|
4487
|
-
timeoutControl.terminalDelivered = true;
|
|
4488
|
-
}
|
|
4489
5084
|
}
|
|
4490
5085
|
finally {
|
|
4491
5086
|
if (fullAccessAuthorization) {
|
|
@@ -4496,7 +5091,8 @@ export class ResponseEngine {
|
|
|
4496
5091
|
auditFullAccessEvent({
|
|
4497
5092
|
event: fullAccessAuthorization.source === 'trigger'
|
|
4498
5093
|
? 'fullaccess.trigger.execution.ended'
|
|
4499
|
-
:
|
|
5094
|
+
: fullAccessAuthorization.source === 'agent-policy'
|
|
5095
|
+
? 'fullaccess.agent-policy.execution.ended' : 'fullaccess.execution.ended',
|
|
4500
5096
|
source: fullAccessAuthorization.source,
|
|
4501
5097
|
actorId: fullAccessAuthorization.authorizedBy,
|
|
4502
5098
|
processRole: fullAccessAuditProcessRole,
|
|
@@ -4668,7 +5264,7 @@ export class ResponseEngine {
|
|
|
4668
5264
|
// valid mapped owner/admin to role=none at the execution boundary.
|
|
4669
5265
|
if (resolvedIdentity)
|
|
4670
5266
|
bound.identity = resolvedIdentity;
|
|
4671
|
-
if (bound.threadId) {
|
|
5267
|
+
if (bound.threadId || bound.metadata?.diagnostic) {
|
|
4672
5268
|
const absoluteProjectPath = path.isAbsolute(bound.projectPath)
|
|
4673
5269
|
? bound.projectPath : path.resolve(process.cwd(), bound.projectPath);
|
|
4674
5270
|
return { session: bound, absoluteProjectPath };
|
|
@@ -4694,7 +5290,7 @@ export class ResponseEngine {
|
|
|
4694
5290
|
logger.warn(`[ResponseEngine] Bound session ${boundSessionId} not found, falling back to latest`);
|
|
4695
5291
|
}
|
|
4696
5292
|
}
|
|
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);
|
|
5293
|
+
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
5294
|
this.ensureSessionBaseagent(session, resolvedBaseagent);
|
|
4699
5295
|
// 群名解析:群会话首次取群显示名(group.get),缓存到 metadata,供信封渲染。
|
|
4700
5296
|
// 渠道私有方法 getGroupName 自带进程缓存 + 容错;取不到不阻塞(groupName 保持空,模板回退 groupId)。
|
|
@@ -4752,7 +5348,7 @@ export class ResponseEngine {
|
|
|
4752
5348
|
*/
|
|
4753
5349
|
async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive,
|
|
4754
5350
|
/** [迁移点4/5] 响应模式插件 + 状态,用于调 onToolUse/onComplete 钩子 */
|
|
4755
|
-
modeHooks, causation, turnLease, permissionMode, proactiveSelfAid) {
|
|
5351
|
+
modeHooks, causation, turnLease, permissionMode, proactiveSelfAid, onAsyncQuestion) {
|
|
4756
5352
|
// Per-session agent name for stats bucketing
|
|
4757
5353
|
const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
|
|
4758
5354
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
|
|
@@ -4839,6 +5435,15 @@ export class ResponseEngine {
|
|
|
4839
5435
|
// It must never be projected as progress, activity, text, or transcript.
|
|
4840
5436
|
if (event.type === 'heartbeat')
|
|
4841
5437
|
continue;
|
|
5438
|
+
if (event.type === 'user_question') {
|
|
5439
|
+
if (!onAsyncQuestion)
|
|
5440
|
+
throw new Error('Async question has no delivery context');
|
|
5441
|
+
await onAsyncQuestion(event.request);
|
|
5442
|
+
madeProgress = true;
|
|
5443
|
+
// A structured question is user-visible progress without being a text reply.
|
|
5444
|
+
textCount++;
|
|
5445
|
+
continue;
|
|
5446
|
+
}
|
|
4842
5447
|
if (event.type === 'input_accepted') {
|
|
4843
5448
|
inputAccepted = !turnLease || event.inputId === turnLease.inputId;
|
|
4844
5449
|
logger.debug(`[ResponseEngine] Input accepted: session=${session.id} input=${event.inputId}`);
|
|
@@ -4934,7 +5539,7 @@ export class ResponseEngine {
|
|
|
4934
5539
|
|| (typeof input?.prompt === 'string' ? input.prompt.slice(0, 80) : '')
|
|
4935
5540
|
|| (typeof input?.query === 'string' ? input.query.slice(0, 80) : '')
|
|
4936
5541
|
|| '';
|
|
4937
|
-
eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
|
|
5542
|
+
eventDetail = ` tool=${event.name}${desc ? ` desc="${String(desc).replace(/\s+/g, ' ')}"` : ''}`;
|
|
4938
5543
|
}
|
|
4939
5544
|
else if (event.type === 'tool_result') {
|
|
4940
5545
|
const decision = event.decision ?? (event.isError ? 'error' : 'allow');
|
|
@@ -4954,6 +5559,10 @@ export class ResponseEngine {
|
|
|
4954
5559
|
}
|
|
4955
5560
|
// IMRenderer 旁路:proactive 模式逐事件投影为 thought(fire-and-forget)
|
|
4956
5561
|
renderer.emit(event);
|
|
5562
|
+
if (event.type === 'text_start') {
|
|
5563
|
+
await renderer.sendTextStart(event.contentIndex, event.turn);
|
|
5564
|
+
continue;
|
|
5565
|
+
}
|
|
4957
5566
|
// session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
|
|
4958
5567
|
if (event.type === 'session_id') {
|
|
4959
5568
|
logger.debug(`[ResponseEngine] Session ID updated: ${event.sessionId} for session: ${session.id}`);
|
|
@@ -5115,7 +5724,7 @@ export class ResponseEngine {
|
|
|
5115
5724
|
this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: visibleText, isFinal: false });
|
|
5116
5725
|
}
|
|
5117
5726
|
if (visibleText && !shouldSuppress()) {
|
|
5118
|
-
renderer.addText(visibleText, event.outputTokens, event.turn);
|
|
5727
|
+
await renderer.addText(visibleText, event.outputTokens, event.turn, event.assistantUuid, event.phase);
|
|
5119
5728
|
hasProjectedCurrentReplyText = true;
|
|
5120
5729
|
}
|
|
5121
5730
|
}
|
|
@@ -5175,7 +5784,7 @@ export class ResponseEngine {
|
|
|
5175
5784
|
timestamp: Date.now(),
|
|
5176
5785
|
causation,
|
|
5177
5786
|
});
|
|
5178
|
-
const desc =
|
|
5787
|
+
const desc = summarizeToolInputForDisplay(event.name, event.input || {});
|
|
5179
5788
|
const pendingInput = { name: event.name, input: event.input || {} };
|
|
5180
5789
|
if (event.callId)
|
|
5181
5790
|
toolInputByCallId.set(event.callId, pendingInput);
|
|
@@ -5351,7 +5960,7 @@ export class ResponseEngine {
|
|
|
5351
5960
|
let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
|
|
5352
5961
|
// 移除 XML 风格的错误标签
|
|
5353
5962
|
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);
|
|
5963
|
+
renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, event.durationMs, cachedDesc, event);
|
|
5355
5964
|
}
|
|
5356
5965
|
else if (!event.isError && !shouldSuppress()) {
|
|
5357
5966
|
renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc);
|
|
@@ -5452,7 +6061,7 @@ export class ResponseEngine {
|
|
|
5452
6061
|
// User interrupt (AbortError) is expected, log at info level
|
|
5453
6062
|
const catchInterruptReason = this.interruptedSessions.get(session.id);
|
|
5454
6063
|
const catchIsUserInterrupt = isNonErrorInterruptReason(catchInterruptReason);
|
|
5455
|
-
if (catchInterruptReason === 'timeout') {
|
|
6064
|
+
if (catchInterruptReason === 'timeout' || catchInterruptReason === 'execution_error') {
|
|
5456
6065
|
logger.info('[ResponseEngine] Stream closed by timeout barrier');
|
|
5457
6066
|
throw this.timeoutErrors.get(session.id) ?? new Error('SDK_TIMEOUT');
|
|
5458
6067
|
}
|
|
@@ -5490,7 +6099,9 @@ export class ResponseEngine {
|
|
|
5490
6099
|
if (hasErrorSuppressingContent) {
|
|
5491
6100
|
let errorOutputFlushed = false;
|
|
5492
6101
|
try {
|
|
5493
|
-
|
|
6102
|
+
// The outer retry/terminal handler has not decided the outcome yet.
|
|
6103
|
+
// Pending stream output must not announce successful completion.
|
|
6104
|
+
await renderer.flush(false);
|
|
5494
6105
|
errorOutputFlushed = true;
|
|
5495
6106
|
}
|
|
5496
6107
|
catch (flushError) {
|