evolcore 0.0.25 → 0.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +632 -565
- package/LICENSE +21 -21
- package/README.md +455 -455
- package/bin/codex-managed-acl.ps1 +203 -160
- package/bin/codex-managed-hook.mjs +208 -208
- package/bin/ec-safe-output.js +327 -327
- package/bin/ec.js +40 -29
- package/bin/install-codex-managed-hooks.mjs +582 -555
- package/dist/agents/baseagent-availability.js +68 -0
- package/dist/agents/baseagent.js +96 -46
- package/dist/agents/claude-runner.js +172 -33
- package/dist/agents/codex/app-server-client.js +76 -31
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +34 -1
- package/dist/agents/codex-runner.js +434 -446
- package/dist/agents/ecagent-runner.js +174 -34
- package/dist/agents/gemini-runner.js +8 -7
- package/dist/agents/runner-types.js +75 -19
- package/dist/aun/aid/agentmd.js +151 -36
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/control-aid.js +180 -1
- package/dist/aun/aid/identity.js +4 -4
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +17 -13
- 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} +162 -39
- package/dist/aun/{menu-client.js → menu/client.js} +85 -6
- 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 +85 -19
- package/dist/aun/msg/mention-schema.js +51 -0
- package/dist/aun/msg/p2p.js +19 -9
- package/dist/aun/msg/upload.js +43 -80
- 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 +842 -318
- package/dist/channels/daemon.js +10 -6
- package/dist/cli/agent-command.js +212 -89
- package/dist/cli/agent-create.js +626 -11
- package/dist/cli/agent.js +125 -13
- package/dist/cli/aun-commands.js +487 -337
- package/dist/cli/bench.js +20 -20
- package/dist/cli/config.js +55 -33
- package/dist/cli/contact.js +72 -15
- package/dist/cli/ctl-command.js +24 -24
- package/dist/cli/daemon-commands.js +140 -63
- package/dist/cli/diag.js +31 -0
- package/dist/cli/fs-command.js +70 -70
- package/dist/cli/handoff-command.js +46 -14
- package/dist/cli/index.js +91 -79
- package/dist/cli/init.js +34 -1
- package/dist/cli/link-rules.js +21 -21
- package/dist/cli/managed-command-guard.js +5 -1
- package/dist/cli/menu-client.js +51 -32
- package/dist/cli/model.js +3 -3
- package/dist/cli/net-check.js +22 -22
- package/dist/cli/queue-command.js +31 -31
- package/dist/cli/response.js +32 -32
- package/dist/cli/restart-monitor.js +103 -42
- package/dist/cli/stats.js +51 -51
- package/dist/cli/task-command.js +59 -0
- package/dist/cli/task-context.js +97 -49
- package/dist/cli/trigger-command.js +74 -64
- package/dist/config/builtin-roles.js +11 -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 +160 -0
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/contact-book.js +1 -1
- package/dist/config/contact-operation-service.js +97 -8
- package/dist/config/peer-role-resolver.js +1 -1
- package/dist/config/resolved-config-op.js +30 -7
- package/dist/config-store.js +1 -1
- package/dist/core/agent-application-service.js +7 -1
- 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 +53 -49
- package/dist/core/auth/operation-authorizer.js +90 -7
- package/dist/core/auth/operation-catalog.js +145 -4
- package/dist/core/auth/remote-access-store.js +155 -10
- package/dist/core/bootstrap-messages.js +66 -7
- package/dist/core/bootstrap-service.js +111 -20
- 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 +159 -65
- package/dist/core/command/data-root-resolver.js +273 -0
- package/dist/core/command/diag-service.js +118 -0
- package/dist/core/command/group-menu.js +7 -2
- package/dist/core/command/menu-catalog.js +46 -7
- package/dist/core/command/menu-handler.js +392 -100
- package/dist/core/command/menu-protocol.js +45 -6
- package/dist/core/command/menu-token-store.js +41 -8
- package/dist/core/command/role-menu.js +2 -2
- package/dist/core/command/slash-gate.js +34 -4
- package/dist/core/command/slash-handler.js +123 -43
- 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/handoff/runtime.js +205 -27
- package/dist/core/handoff/store.js +193 -4
- package/dist/core/interaction-router.js +22 -8
- package/dist/core/message/im-renderer.js +146 -34
- package/dist/core/message/message-bridge.js +163 -20
- package/dist/core/message/message-log.js +5 -3
- package/dist/core/message/message-queue.js +99 -41
- package/dist/core/message/message-utils.js +1 -0
- package/dist/core/message/response-engine.js +755 -144
- package/dist/core/model/catalog-service.js +79 -49
- package/dist/core/model/context-window-suffix.js +33 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-permission.js +12 -3
- package/dist/core/permission/approval-gateway.js +36 -3
- package/dist/core/permission/ec-command-parser.js +112 -41
- package/dist/core/permission/protected-paths.js +134 -16
- package/dist/core/permission/sandbox-runtime.js +108 -17
- package/dist/core/permission/tool-error-code.js +15 -0
- package/dist/core/permission/tool-policy.js +9 -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 +34 -6
- package/dist/core/session/session-mapper.js +41 -0
- package/dist/core/session/session-turn-coordinator.js +37 -7
- package/dist/core/session/session-turns.js +48 -7
- package/dist/core/task/task-recovery-service.js +176 -0
- package/dist/core/task/task-resume-operation.js +32 -0
- package/dist/eck/group-rules-sync.js +5 -3
- package/dist/eck/kit-renderer.js +1 -0
- package/dist/index.js +588 -217
- package/dist/ipc.js +193 -15
- package/dist/paths.js +17 -4
- package/dist/product.js +2 -0
- package/dist/response-system/engines/v1/proactive-flow.js +59 -2
- package/dist/stats/billing.js +41 -15
- package/dist/stats/db.js +159 -159
- package/dist/stats/eck-vars.js +9 -9
- package/dist/stats/price-resolver.js +28 -10
- package/dist/stats/query.js +139 -139
- package/dist/stats/role-budget.js +6 -6
- package/dist/stats/writer.js +51 -51
- 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/cross-platform.js +30 -1
- package/dist/utils/ecweb-utils.js +1 -0
- package/dist/utils/error-dict.json +160 -160
- package/dist/utils/error-utils.js +61 -2
- package/dist/utils/evolcore-version.js +129 -0
- package/dist/utils/logger.js +26 -14
- package/dist/utils/macos-autostart.js +22 -22
- package/dist/utils/model-prices.jsonl +23 -23
- package/dist/utils/npm-ops.js +176 -35
- package/dist/utils/tool-summary.js +59 -3
- package/dist/utils/welcome.js +196 -23
- package/ecagent/LICENSE +21 -21
- package/ecagent/NOTICE +11 -11
- package/ecagent/README.md +61 -61
- package/ecagent/dist/harness/agent-harness.js +3 -0
- package/ecagent/dist/harness/compaction/branch-summarization.js +30 -30
- package/ecagent/dist/harness/compaction/compaction.js +83 -83
- package/ecagent/dist/harness/messages.js +7 -7
- package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
- package/kits/docs/GUIDE.md +20 -20
- package/kits/docs/INDEX.md +60 -66
- package/kits/docs/aun/CHEATSHEET.md +19 -19
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -15
- package/kits/docs/channels/aun.md +69 -56
- package/kits/docs/channels/feishu.md +56 -56
- package/kits/docs/context-assembly.md +379 -367
- package/kits/docs/eck_templates/GUIDE.template.md +22 -22
- package/kits/docs/eck_templates/INDEX.template.md +28 -28
- package/kits/docs/eck_templates/path-registry.template.md +33 -33
- package/kits/docs/eck_templates/runtime.template.md +19 -19
- package/kits/docs/evolcore/INDEX.md +76 -71
- package/kits/docs/evolcore/agent-create.md +202 -165
- package/kits/docs/evolcore/agent.md +99 -67
- package/kits/docs/evolcore/aid.md +68 -60
- package/kits/docs/evolcore/codex-windows-sandbox.md +146 -146
- package/kits/docs/evolcore/config.md +241 -195
- package/kits/docs/evolcore/contact.md +75 -68
- package/kits/docs/evolcore/ctl.md +46 -46
- package/kits/docs/evolcore/event.md +216 -216
- package/kits/docs/evolcore/fs-architecture.md +1215 -1215
- package/kits/docs/evolcore/fs.md +110 -110
- package/kits/docs/evolcore/group-collaboration.md +244 -244
- package/kits/docs/evolcore/group-fs.md +17 -17
- package/kits/docs/evolcore/group-rules.md +251 -251
- package/kits/docs/evolcore/group.md +267 -152
- package/kits/docs/evolcore/menu-client.md +58 -34
- package/kits/docs/evolcore/model.md +50 -50
- package/kits/docs/evolcore/msg.md +169 -165
- package/kits/docs/evolcore/response.md +75 -75
- package/kits/docs/evolcore/rpc.md +37 -37
- package/kits/docs/evolcore/self-summary.md +29 -29
- package/kits/docs/evolcore/stats.md +83 -83
- package/kits/docs/evolcore/storage.md +53 -50
- package/kits/docs/evolcore/task.md +14 -0
- package/kits/docs/evolcore/trigger.md +623 -575
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -26
- package/kits/docs/identity/PATH_OPS.md +16 -16
- package/kits/docs/identity/ROLE_DETAIL.md +25 -25
- package/kits/docs/identity/identity-tools.md +26 -26
- package/kits/docs/path-registry.md +62 -62
- package/kits/docs/prompt-loading-architecture.md +269 -269
- package/kits/docs/venues/aun-group.md +45 -45
- package/kits/docs/venues/aun-private.md +10 -10
- package/kits/docs/venues/client-desktop.md +10 -10
- package/kits/docs/venues/client-mobile.md +10 -10
- package/kits/docs/venues/feishu-group.md +13 -13
- package/kits/docs/venues/feishu-private.md +9 -9
- package/kits/docs/venues/group.md +42 -25
- package/kits/docs/venues/private.md +10 -10
- package/kits/eck_manifest.auxiliary.json +43 -43
- package/kits/eck_manifest.json +217 -190
- package/kits/eck_message_manifest.json +74 -74
- package/kits/migrations/README-role-config-v4.md +32 -32
- package/kits/migrations/migrate-contact-book-v2.mjs +748 -748
- package/kits/migrations/migrate-role-config-v4.mjs +614 -614
- package/kits/migrations/migrate-role-config-v5.mjs +365 -365
- package/kits/migrations/rename-config-file.mjs +99 -99
- package/kits/rules/01-overview.md +166 -162
- package/kits/rules/02-navigation.md +96 -96
- package/kits/rules/03-identity.md +36 -36
- package/kits/rules/04-relation.md +76 -75
- package/kits/rules/05-venue.md +60 -60
- package/kits/rules/06-channel.md +65 -63
- package/kits/schemas/_meta.json +53 -53
- package/kits/schemas/agent-config.schema.1.json +177 -177
- package/kits/schemas/agent-config.schema.10.json +395 -395
- package/kits/schemas/agent-config.schema.11.json +421 -421
- package/kits/schemas/agent-config.schema.12.json +451 -427
- package/kits/schemas/agent-config.schema.2.json +239 -239
- package/kits/schemas/agent-config.schema.3.json +119 -119
- package/kits/schemas/agent-config.schema.4.json +208 -208
- package/kits/schemas/agent-config.schema.5.json +326 -326
- package/kits/schemas/agent-config.schema.6.json +322 -322
- package/kits/schemas/agent-config.schema.7.json +303 -303
- package/kits/schemas/agent-config.schema.8.json +304 -304
- package/kits/schemas/agent-config.schema.9.json +364 -364
- package/kits/schemas/contact-book.schema.1.json +36 -36
- package/kits/schemas/contact-book.schema.2.json +43 -43
- package/kits/schemas/contact-book.schema.3.json +67 -67
- package/kits/schemas/daemon.schema.1.json +91 -91
- package/kits/schemas/daemon.schema.2.json +101 -101
- package/kits/schemas/daemon.schema.3.json +123 -123
- package/kits/schemas/daemon.schema.4.json +136 -136
- package/kits/schemas/daemon.schema.5.json +135 -135
- package/kits/schemas/daemon.schema.6.json +131 -131
- package/kits/schemas/defaults.schema.1.json +81 -81
- package/kits/schemas/defaults.schema.2.json +85 -85
- package/kits/schemas/defaults.schema.3.json +73 -73
- package/kits/schemas/defaults.schema.4.json +107 -107
- package/kits/schemas/defaults.schema.5.json +119 -119
- package/kits/schemas/menu-exec-schema-commands.md +208 -208
- package/kits/schemas/migrations/README.md +30 -30
- package/kits/schemas/relation-config.schema.1.json +158 -158
- package/kits/schemas/relation-config.schema.2.json +73 -73
- package/kits/schemas/relation-config.schema.3.json +50 -50
- package/kits/schemas/relation-config.schema.4.json +47 -47
- package/kits/schemas/relation-config.schema.5.json +47 -47
- package/kits/schemas/relation-config.schema.6.json +46 -46
- package/kits/schemas/relation-config.schema.7.json +59 -59
- package/kits/schemas/relation-config.schema.8.json +103 -103
- package/kits/schemas/role-config.schema.1.json +215 -201
- package/kits/schemas/role-registry.schema.1.json +35 -35
- package/kits/schemas/single-session.schema.1.json +31 -31
- package/kits/schemas/single-session.schema.2.json +57 -57
- package/kits/schemas/single-session.schema.3.json +32 -32
- package/kits/templates/bootstrap-welcome.md +17 -17
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -9
- package/kits/templates/message-fragments/handoff-request-to-target.md +19 -19
- package/kits/templates/message-fragments/handoff-response-to-origin.md +9 -9
- package/kits/templates/message-fragments/inject-default.md +2 -2
- package/kits/templates/message-fragments/item.md +2 -2
- package/kits/templates/roles/admin.json +92 -92
- package/kits/templates/roles/member.json +101 -96
- package/kits/templates/roles/owner.json +9 -9
- package/kits/templates/roles/visitor.json +90 -85
- package/kits/templates/system-fragments/baseagent.md +14 -14
- package/kits/templates/system-fragments/bootstrap.md +53 -34
- package/kits/templates/system-fragments/channel.md +28 -64
- package/kits/templates/system-fragments/commands.md +36 -28
- package/kits/templates/system-fragments/group-unread-history.md +7 -0
- package/kits/templates/system-fragments/identity.md +13 -13
- package/kits/templates/system-fragments/relation.md +19 -19
- package/kits/templates/system-fragments/reply.md +56 -0
- package/kits/templates/system-fragments/session.md +59 -59
- package/kits/templates/system-fragments/venue.md +31 -31
- package/package.json +104 -98
- package/dist/aun/group-identity.js +0 -9
- /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
- /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { initializationBlocksTasks } from '../../cli/agent-create.js';
|
|
1
2
|
import path from 'path';
|
|
2
3
|
import fs from 'fs';
|
|
3
4
|
import os from 'os';
|
|
4
5
|
import crypto from 'crypto';
|
|
5
|
-
import { BaseagentRunnerUnavailableError, hasCompact,
|
|
6
|
+
import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken, formatAsyncQuestionAnswer, parseAsyncUserQuestions, } from '../../agents/runner-types.js';
|
|
7
|
+
import { loadedBaseagentIds } from '../../agents/baseagent-availability.js';
|
|
8
|
+
import { evaluateInboundAdmissionPreflight, evaluateOrdinaryInboundAdmission } from './message-admission.js';
|
|
9
|
+
import { authorizeAccess, authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
6
10
|
import { buildTaskRuntimeEnv, ensureRuntimeLockDir, ensureSessionRuntimeDir } from '../../cli/task-context.js';
|
|
7
11
|
import { IMRenderer } from './im-renderer.js';
|
|
8
12
|
import { createSendFileMarkerPattern } from './file-markers.js';
|
|
@@ -10,10 +14,11 @@ import { createTextInferenceProvider } from '../inference/text-inference.js';
|
|
|
10
14
|
import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
11
15
|
import { logger } from '../../utils/logger.js';
|
|
12
16
|
import { isHostChinese } from '../../utils/locale.js';
|
|
13
|
-
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
+
import { ExecutionTimeoutError, OrphanedTargetSessionError, isExecutionFailure, getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
|
|
18
|
+
import { readEvolcoreRuntimeFlavor, tagErrorTextWithEvolcoreVersion } from '../../utils/evolcore-version.js';
|
|
19
|
+
import { describeProactiveSendCommandFailure, isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
|
|
20
|
+
import { summarizeToolInputForDisplay } from '../../utils/tool-summary.js';
|
|
21
|
+
import { agentDataDir, agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
17
22
|
import { renderKitSections } from '../../eck/kit-renderer.js';
|
|
18
23
|
import { renderMessageBody } from '../../eck/message-renderer.js';
|
|
19
24
|
import { syncGroupRulesContext } from '../../eck/group-rules-sync.js';
|
|
@@ -21,8 +26,9 @@ import { consumeHints, hintsToSubMessages, composeHintFallback } from './pending
|
|
|
21
26
|
import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
|
|
22
27
|
import { recordCausationLink, recordCausationSpan } from '../causation/audit.js';
|
|
23
28
|
import { normalizeBaseagent } from '../../agents/baseagent.js';
|
|
29
|
+
import { renderActionAsText } from '../interaction-router.js';
|
|
24
30
|
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
25
|
-
import { buildEnvelope, replyContextFromSession, withEnvelopeReplyContext } from './message-utils.js';
|
|
31
|
+
import { buildEnvelope, isInteractionSendAccepted, replyContextFromSession, sendInteractionPayload, withEnvelopeReplyContext } from './message-utils.js';
|
|
26
32
|
import { isSystemOrServicePeer, resolveChatMode } from './peer-mode.js';
|
|
27
33
|
import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
|
|
28
34
|
import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
|
|
@@ -51,7 +57,290 @@ import { classifyToolErrorCode, normalizeToolErrorCode } from '../permission/too
|
|
|
51
57
|
import { normalizeExecutionPermissionMode } from '../permission/mode.js';
|
|
52
58
|
import { buildToolLifecycleEventKey } from '../audit/event-key.js';
|
|
53
59
|
import { isFullAccessEnabled, loadDaemonConfig } from '../../config-store.js';
|
|
60
|
+
import { assertAgentPolicyAuthorization } from '../role/runtime-policy.js';
|
|
54
61
|
import { auditFullAccessEvent } from '../auth/authorization-audit.js';
|
|
62
|
+
import { TaskRecoveryService, isTaskRecoveryCurrent } from '../task/task-recovery-service.js';
|
|
63
|
+
const QUESTION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
64
|
+
/** Pending async questions outlive the originating turn, but never change its backend binding. */
|
|
65
|
+
export class AsyncQuestionService {
|
|
66
|
+
deps;
|
|
67
|
+
registered = new Map();
|
|
68
|
+
deliveries = new Map();
|
|
69
|
+
constructor(deps) {
|
|
70
|
+
this.deps = deps;
|
|
71
|
+
}
|
|
72
|
+
directory(selfAid) {
|
|
73
|
+
return this.deps.directory?.(selfAid) ?? path.join(agentDataDir(selfAid), 'async-questions');
|
|
74
|
+
}
|
|
75
|
+
file(selfAid, id) {
|
|
76
|
+
if (!/^aq-[a-f0-9]{40}$/.test(id))
|
|
77
|
+
throw new Error('Invalid async question id');
|
|
78
|
+
return path.join(this.directory(selfAid), `${id}.json`);
|
|
79
|
+
}
|
|
80
|
+
save(record) {
|
|
81
|
+
fs.mkdirSync(this.directory(record.selfAid), { recursive: true, mode: 0o700 });
|
|
82
|
+
const destination = this.file(record.selfAid, record.id);
|
|
83
|
+
const temporary = `${destination}.tmp`;
|
|
84
|
+
const fd = fs.openSync(temporary, 'w', 0o600);
|
|
85
|
+
try {
|
|
86
|
+
fs.writeFileSync(fd, JSON.stringify(record));
|
|
87
|
+
fs.fsyncSync(fd);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
fs.closeSync(fd);
|
|
91
|
+
}
|
|
92
|
+
// Replace atomically, without unlinking the previous durable state first.
|
|
93
|
+
fs.renameSync(temporary, destination);
|
|
94
|
+
}
|
|
95
|
+
read(selfAid, id) {
|
|
96
|
+
try {
|
|
97
|
+
const value = JSON.parse(fs.readFileSync(this.file(selfAid, id), 'utf8'));
|
|
98
|
+
if (value.version !== 1 || value.selfAid !== selfAid || value.id !== id
|
|
99
|
+
|| !value.sessionId || !value.providerThreadId || !value.itemId
|
|
100
|
+
|| !value.origin?.peerId || !value.channelKey || !Number.isFinite(value.expiresAt))
|
|
101
|
+
return undefined;
|
|
102
|
+
parseAsyncUserQuestions([value.question]);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error.code !== 'ENOENT')
|
|
107
|
+
throw error;
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async present(session, request, message, adapter, envelope) {
|
|
112
|
+
if (!session.selfAID || !message.peerId)
|
|
113
|
+
throw new Error('Async question requires an authenticated route');
|
|
114
|
+
const questions = parseAsyncUserQuestions(request.questions);
|
|
115
|
+
const records = [];
|
|
116
|
+
for (const [index, question] of questions.entries()) {
|
|
117
|
+
const hash = crypto.createHash('sha256').update(JSON.stringify([
|
|
118
|
+
session.selfAID, session.id, request.providerThreadId, request.itemId, index,
|
|
119
|
+
])).digest('hex').slice(0, 40);
|
|
120
|
+
const id = `aq-${hash}`;
|
|
121
|
+
const existing = this.read(session.selfAID, id);
|
|
122
|
+
if (existing && existing.status !== 'pending')
|
|
123
|
+
continue;
|
|
124
|
+
const replyContext = {
|
|
125
|
+
delivery: envelope.delivery ?? envelope.replyContext?.delivery,
|
|
126
|
+
threadId: session.threadId || undefined,
|
|
127
|
+
metadata: {
|
|
128
|
+
encrypted: message.encrypted ?? envelope.replyContext?.metadata?.encrypted,
|
|
129
|
+
chatmode: envelope.chatmode,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const record = existing ?? {
|
|
133
|
+
version: 1, id, sessionId: session.id, selfAid: session.selfAID,
|
|
134
|
+
providerThreadId: request.providerThreadId, providerTurnId: request.providerTurnId,
|
|
135
|
+
itemId: request.itemId, question,
|
|
136
|
+
origin: {
|
|
137
|
+
channel: message.channel, channelType: session.channelType,
|
|
138
|
+
channelId: session.channelId, peerId: message.peerId, peerType: message.peerType,
|
|
139
|
+
chatType: session.chatType === 'group' ? 'group' : 'private',
|
|
140
|
+
threadId: session.threadId || undefined, selfAID: session.selfAID,
|
|
141
|
+
},
|
|
142
|
+
envelope: { ...envelope, sessionId: session.id, replyContext, delivery: replyContext.delivery },
|
|
143
|
+
channelKey: adapter.channelKey, createdAt: Date.now(), expiresAt: Date.now() + QUESTION_TTL_MS,
|
|
144
|
+
status: 'pending',
|
|
145
|
+
};
|
|
146
|
+
if (!existing)
|
|
147
|
+
this.save(record);
|
|
148
|
+
records.push(record);
|
|
149
|
+
}
|
|
150
|
+
// Persist the complete batch before dispatch; one failed card must not lose the other questions.
|
|
151
|
+
const errors = [];
|
|
152
|
+
for (const record of records) {
|
|
153
|
+
try {
|
|
154
|
+
await this.deliver(record, adapter);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
errors.push(error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (errors.length)
|
|
161
|
+
throw new AggregateError(errors, 'Async question delivery failed; pending questions retained');
|
|
162
|
+
}
|
|
163
|
+
async restore(adapter, selfAid) {
|
|
164
|
+
let names;
|
|
165
|
+
try {
|
|
166
|
+
names = fs.readdirSync(this.directory(selfAid));
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error.code === 'ENOENT')
|
|
170
|
+
return;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
for (const name of names) {
|
|
174
|
+
if (!/^aq-[a-f0-9]{40}\.json$/.test(name))
|
|
175
|
+
continue;
|
|
176
|
+
try {
|
|
177
|
+
const record = this.read(selfAid, name.slice(0, -5));
|
|
178
|
+
if (!record || record.channelKey !== adapter.channelKey)
|
|
179
|
+
continue;
|
|
180
|
+
if (this.registered.get(record.id) === adapter)
|
|
181
|
+
continue;
|
|
182
|
+
if (record.expiresAt <= Date.now()) {
|
|
183
|
+
fs.unlinkSync(this.file(selfAid, record.id));
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (record.status === 'submitting') {
|
|
187
|
+
// A crash may have happened after Codex accepted the steer. Never replay it blindly.
|
|
188
|
+
record.status = 'uncertain';
|
|
189
|
+
record.reason = 'restart_during_submission';
|
|
190
|
+
this.save(record);
|
|
191
|
+
}
|
|
192
|
+
if (record.status === 'pending')
|
|
193
|
+
await this.deliver(record, adapter);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
logger.warn(`[AsyncQuestions] restore failed: id=${name} error=${String(error)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async validSession(record) {
|
|
201
|
+
const session = await this.deps.getSession(record.sessionId);
|
|
202
|
+
if (!session || session.baseagent !== 'codex' || session.selfAID !== record.selfAid
|
|
203
|
+
|| session.agentSessionId !== record.providerThreadId
|
|
204
|
+
|| session.channel !== record.origin.channel
|
|
205
|
+
|| session.channelType !== record.origin.channelType
|
|
206
|
+
|| session.channelId !== record.origin.channelId
|
|
207
|
+
|| (session.threadId || '') !== (record.origin.threadId || '')
|
|
208
|
+
|| !(await this.deps.authorize(record, session)))
|
|
209
|
+
return undefined;
|
|
210
|
+
return session;
|
|
211
|
+
}
|
|
212
|
+
/** Recheck persisted answers after queue delays/restarts, immediately before a provider run. */
|
|
213
|
+
async validateQueuedAnswer(session, answer) {
|
|
214
|
+
if (!session.selfAID)
|
|
215
|
+
return false;
|
|
216
|
+
const record = this.read(session.selfAID, answer.requestId);
|
|
217
|
+
return !!record && record.sessionId === session.id
|
|
218
|
+
&& ['submitting', 'answered', 'uncertain'].includes(record.status)
|
|
219
|
+
&& record.providerThreadId === answer.providerThreadId
|
|
220
|
+
&& record.itemId === answer.questionItemId && record.question.title === answer.question
|
|
221
|
+
&& record.answer === answer.answer && !!(await this.validSession(record));
|
|
222
|
+
}
|
|
223
|
+
async deliver(record, adapter) {
|
|
224
|
+
if (this.registered.get(record.id) === adapter)
|
|
225
|
+
return;
|
|
226
|
+
// A channel reload replaces its callback/card registry. Rebind pending questions to the new adapter.
|
|
227
|
+
if (this.registered.has(record.id))
|
|
228
|
+
await this.deps.router.cancel(record.id, 'daemon_restart');
|
|
229
|
+
const previous = this.deliveries.get(record.id);
|
|
230
|
+
if (previous)
|
|
231
|
+
return previous;
|
|
232
|
+
const delivery = this.deliverOnce(record, adapter);
|
|
233
|
+
this.deliveries.set(record.id, delivery);
|
|
234
|
+
try {
|
|
235
|
+
await delivery;
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
this.deliveries.delete(record.id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async deliverOnce(record, adapter) {
|
|
242
|
+
if (record.expiresAt <= Date.now() || !(await this.validSession(record))) {
|
|
243
|
+
record.status = 'cancelled';
|
|
244
|
+
record.reason = 'expired_or_binding_changed';
|
|
245
|
+
this.save(record);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const options = record.question.options ?? [];
|
|
249
|
+
const interaction = {
|
|
250
|
+
type: 'interaction', id: record.id, sessionId: record.sessionId, channelId: record.origin.channelId,
|
|
251
|
+
initiatorId: record.origin.peerId, blocking: false, expiresAt: record.expiresAt,
|
|
252
|
+
kind: {
|
|
253
|
+
kind: 'action', title: '请补充信息', body: record.question.title, bodyFormat: 'plain',
|
|
254
|
+
buttons: options.map((label, index) => ({ key: `opt-${index}`, label })),
|
|
255
|
+
allowCustomInput: true,
|
|
256
|
+
},
|
|
257
|
+
fallback: {
|
|
258
|
+
command: 'ask',
|
|
259
|
+
buttonArgMap: Object.fromEntries(options.map((_, index) => [`opt-${index}`, `${record.id} ${index + 1}`])),
|
|
260
|
+
acceptFreeText: true, freeTextHint: `回复 /ask ${record.id} <回答>`,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
// Register before dispatch: a fast reply must not race card delivery.
|
|
264
|
+
this.registered.set(record.id, adapter);
|
|
265
|
+
this.deps.router.register(record.id, record.sessionId, async (action, values) => {
|
|
266
|
+
const current = this.read(record.selfAid, record.id);
|
|
267
|
+
if (!current || current.status !== 'pending' || current.expiresAt <= Date.now()) {
|
|
268
|
+
await this.deps.router.cancel(record.id, 'expired_or_resolved');
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
const answer = this.answerText(current.question, action, values);
|
|
272
|
+
if (!answer)
|
|
273
|
+
return false;
|
|
274
|
+
const session = await this.validSession(current);
|
|
275
|
+
if (!session) {
|
|
276
|
+
await this.deps.router.cancel(record.id, 'binding_or_authorization_changed');
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
current.answer = answer;
|
|
280
|
+
current.status = 'submitting';
|
|
281
|
+
this.save(current);
|
|
282
|
+
try {
|
|
283
|
+
await this.deps.submit(current, session, {
|
|
284
|
+
requestId: current.id, providerThreadId: current.providerThreadId,
|
|
285
|
+
questionItemId: current.itemId, question: current.question.title, answer,
|
|
286
|
+
});
|
|
287
|
+
current.status = 'answered';
|
|
288
|
+
this.save(current);
|
|
289
|
+
this.registered.delete(record.id);
|
|
290
|
+
logger.info(`[AsyncQuestions] answer accepted: session=${record.sessionId} request=${record.id}`);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
current.status = 'uncertain';
|
|
295
|
+
current.reason = 'submission_failed';
|
|
296
|
+
this.save(current);
|
|
297
|
+
logger.warn(`[AsyncQuestions] answer submission failed: request=${record.id} error=${String(error)}`);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}, {
|
|
301
|
+
initiatorId: record.origin.peerId, initiatorChannelKey: adapter.channelKey,
|
|
302
|
+
blocking: false, retainOnRejected: true, fallbackCommand: 'ask', timeoutMs: record.expiresAt - Date.now(),
|
|
303
|
+
onCancel: async (reason) => {
|
|
304
|
+
this.registered.delete(record.id);
|
|
305
|
+
if (reason === 'daemon_restart')
|
|
306
|
+
return;
|
|
307
|
+
const current = this.read(record.selfAid, record.id);
|
|
308
|
+
if (current?.status === 'pending') {
|
|
309
|
+
current.status = 'cancelled';
|
|
310
|
+
current.reason = reason;
|
|
311
|
+
this.save(current);
|
|
312
|
+
}
|
|
313
|
+
await adapter.invalidateInteraction?.(record.id, reason);
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
try {
|
|
317
|
+
const receipt = await sendInteractionPayload(adapter, {
|
|
318
|
+
...record.envelope, operationId: `${record.id}-${Date.now()}`,
|
|
319
|
+
}, interaction, renderActionAsText(interaction), record.envelope.replyContext);
|
|
320
|
+
if (!isInteractionSendAccepted(receipt))
|
|
321
|
+
throw new Error('Async question card was not accepted for delivery');
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
// Keep the durable pending record for a later channel reconnect/daemon restore.
|
|
325
|
+
await this.deps.router.cancel(record.id, 'daemon_restart');
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
answerText(question, action, values) {
|
|
330
|
+
if (action === 'cancel' || action === 'dismiss' || values?.behavior === 'dismiss')
|
|
331
|
+
return undefined;
|
|
332
|
+
let answer;
|
|
333
|
+
if (action === '_custom_input')
|
|
334
|
+
answer = typeof values?.custom_text === 'string' ? values.custom_text.trim() : undefined;
|
|
335
|
+
else if (/^opt-\d+$/.test(action))
|
|
336
|
+
answer = question.options?.[Number(action.slice(4))];
|
|
337
|
+
else if (question.options?.length && /^\d+$/.test(action))
|
|
338
|
+
answer = question.options[Number(action) - 1];
|
|
339
|
+
else
|
|
340
|
+
answer = action.trim();
|
|
341
|
+
return answer && answer.length <= 65_536 ? answer : undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
55
344
|
const INBOUND_MESSAGE_LOG_TEXT_MAX_LENGTH = 240;
|
|
56
345
|
export function formatInboundMessageLogText(text) {
|
|
57
346
|
const singleLine = text.replace(/\s+/gu, ' ').trim();
|
|
@@ -485,9 +774,6 @@ export async function resolveGroupRulesForTurn(opts, sync = syncGroupRulesContex
|
|
|
485
774
|
}
|
|
486
775
|
return sync({ selfAid: opts.selfAid, groupId: opts.groupId, channel: opts.channel });
|
|
487
776
|
}
|
|
488
|
-
function autoCompactTokensFromMaxTokens(maxTokens) {
|
|
489
|
-
return autoCompactWindowForContextWindow(maxTokens);
|
|
490
|
-
}
|
|
491
777
|
/**
|
|
492
778
|
* 构造 OutboundEnvelope —— 出站三件套的信封部分。
|
|
493
779
|
*
|
|
@@ -521,12 +807,15 @@ export class ResponseEngine {
|
|
|
521
807
|
primaryRunnerKey;
|
|
522
808
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
523
809
|
timeoutErrors = new Map();
|
|
810
|
+
activeTimeoutHandlers = new Map();
|
|
524
811
|
/** sessionId → 已成功的模型降级状态(进程重启清零) */
|
|
525
812
|
modelFallbackMap = new Map();
|
|
526
813
|
/** sessionId → interruptible task-level API retry delays. */
|
|
527
814
|
retryDelayWaiters = new Map();
|
|
528
815
|
interactionRouter;
|
|
816
|
+
asyncQuestions;
|
|
529
817
|
messageQueue;
|
|
818
|
+
taskRecoveryService;
|
|
530
819
|
handoffRuntime;
|
|
531
820
|
/** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
|
|
532
821
|
activeMonitors = new Map();
|
|
@@ -600,10 +889,7 @@ export class ResponseEngine {
|
|
|
600
889
|
return createTextInferenceProvider(baseagent, config);
|
|
601
890
|
}
|
|
602
891
|
getAvailableBaseagentsForOwner(evolName) {
|
|
603
|
-
|
|
604
|
-
return [...this.agentMap.keys()]
|
|
605
|
-
.filter(key => key.startsWith(prefix))
|
|
606
|
-
.map(key => key.slice(prefix.length));
|
|
892
|
+
return loadedBaseagentIds(this.agentMap.keys(), evolName);
|
|
607
893
|
}
|
|
608
894
|
async sendBaseagentMismatch(channelKey, channelId, session, error, replyContext) {
|
|
609
895
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
@@ -744,6 +1030,12 @@ export class ResponseEngine {
|
|
|
744
1030
|
}
|
|
745
1031
|
setInteractionRouter(router) {
|
|
746
1032
|
this.interactionRouter = router;
|
|
1033
|
+
this.asyncQuestions = new AsyncQuestionService({
|
|
1034
|
+
router,
|
|
1035
|
+
getSession: id => this.sessionManager.getSessionById(id),
|
|
1036
|
+
authorize: async (record, session) => this.authorizeAsyncQuestion(record, session),
|
|
1037
|
+
submit: (record, session, answer) => this.submitAsyncQuestionAnswer(record, session, answer),
|
|
1038
|
+
});
|
|
747
1039
|
// 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
|
|
748
1040
|
// 避免把「正在等用户点按钮」误判为「任务卡死」而中断任务。
|
|
749
1041
|
router.setWaitHooks({
|
|
@@ -754,9 +1046,118 @@ export class ResponseEngine {
|
|
|
754
1046
|
this.activeMonitors.get(sessionId)?.resume();
|
|
755
1047
|
},
|
|
756
1048
|
});
|
|
1049
|
+
for (const { adapter } of this.channels.values())
|
|
1050
|
+
this.restoreAsyncQuestions(adapter);
|
|
757
1051
|
}
|
|
758
1052
|
setMessageQueue(queue) {
|
|
759
1053
|
this.messageQueue = queue;
|
|
1054
|
+
this.taskRecoveryService = new TaskRecoveryService({
|
|
1055
|
+
sessions: this.sessionManager,
|
|
1056
|
+
queue,
|
|
1057
|
+
getRunner: (session) => this.getAgent(session.metadata?.channelKey || session.channel, session.baseagent, session.selfAID),
|
|
1058
|
+
resumePause: (sessionId) => this.resumeSession(sessionId),
|
|
1059
|
+
hasChannel: (session) => !!this.resolveChannelInfo(session.metadata?.channelKey || session.channel),
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
async recoverTask(request) {
|
|
1063
|
+
if (!this.taskRecoveryService) {
|
|
1064
|
+
return { ok: false, status: 'not_recoverable', sessionId: request.sessionId, message: '消息队列尚未初始化' };
|
|
1065
|
+
}
|
|
1066
|
+
return this.taskRecoveryService.resume(request);
|
|
1067
|
+
}
|
|
1068
|
+
authorizeAsyncQuestion(record, session) {
|
|
1069
|
+
const origin = record.origin;
|
|
1070
|
+
const owner = this.agentRegistry?.resolveByChannel(origin.channel);
|
|
1071
|
+
if (owner?.aid !== record.selfAid)
|
|
1072
|
+
return false;
|
|
1073
|
+
const chatType = origin.chatType === 'group' ? 'group' : 'private';
|
|
1074
|
+
const channelType = origin.channelType || record.channelKey.split('#')[0];
|
|
1075
|
+
const preflight = evaluateInboundAdmissionPreflight({
|
|
1076
|
+
selfAid: record.selfAid, channelKey: record.channelKey, channelType, chatType, actorId: origin.peerId,
|
|
1077
|
+
});
|
|
1078
|
+
if (!preflight.allow || !evaluateOrdinaryInboundAdmission(preflight.context).allow)
|
|
1079
|
+
return false;
|
|
1080
|
+
const identity = this.inferDirectSessionIdentity({ ...origin, content: '' }, owner);
|
|
1081
|
+
if (!identity || identity.role === 'none')
|
|
1082
|
+
return false;
|
|
1083
|
+
return authorizeAccess(buildAuthSubject({
|
|
1084
|
+
selfAid: record.selfAid, channel: record.channelKey, channelType, chatType,
|
|
1085
|
+
actorId: origin.peerId, channelId: session.channelId, conversationId: session.channelId, identity,
|
|
1086
|
+
})).allow;
|
|
1087
|
+
}
|
|
1088
|
+
async submitAsyncQuestionAnswer(record, session, answer) {
|
|
1089
|
+
const runner = this.getAgent(record.origin.channel, 'codex', record.selfAid);
|
|
1090
|
+
if (!runner.steerAsyncQuestionAnswer)
|
|
1091
|
+
throw new Error('Codex async input is unavailable');
|
|
1092
|
+
if (await runner.steerAsyncQuestionAnswer(session.id, answer) === 'accepted')
|
|
1093
|
+
return;
|
|
1094
|
+
if (!this.messageQueue)
|
|
1095
|
+
throw new Error('Async question resume queue is unavailable');
|
|
1096
|
+
// The queue owns idle-thread restoration and a fresh task/permission context.
|
|
1097
|
+
// Keep the exact Session/backend identity; never create a new topic for an old card.
|
|
1098
|
+
await this.messageQueue.enqueuePersisted(session.id, {
|
|
1099
|
+
...record.origin, content: formatAsyncQuestionAnswer(answer),
|
|
1100
|
+
asyncQuestionAnswer: answer, boundSessionId: session.id, baseagent: 'codex',
|
|
1101
|
+
source: 'card-trigger', messageId: `async-answer-${record.id}`,
|
|
1102
|
+
timestamp: Date.now(), receivedAt: Date.now(),
|
|
1103
|
+
encrypted: record.envelope.replyContext?.metadata?.encrypted,
|
|
1104
|
+
replyContext: record.envelope.replyContext,
|
|
1105
|
+
}, session.projectPath, {
|
|
1106
|
+
interruptible: false, interruptSamePeer: false,
|
|
1107
|
+
agentName: this.agentRegistry?.resolveByChannel(record.origin.channel)?.name,
|
|
1108
|
+
selfAID: record.selfAid, sessionKeyField: session.sessionKey,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
/** Read the exact attempt's lifecycle without touching any newer task. */
|
|
1112
|
+
async inspectTargetExecution(sessionId, attemptId, selfAid) {
|
|
1113
|
+
const session = await this.sessionManager.getSessionById(sessionId);
|
|
1114
|
+
if (!session || session.deletedAt || session.selfAID !== selfAid)
|
|
1115
|
+
return { state: 'missing' };
|
|
1116
|
+
const state = this.turnCoordinator.current(session);
|
|
1117
|
+
const terminal = state.lastTerminal;
|
|
1118
|
+
const matches = (context) => (context?.triggerAttemptId ?? context?.recovery?.attemptId) === attemptId;
|
|
1119
|
+
if (terminal && matches(terminal.recoveryContext)) {
|
|
1120
|
+
return { state: 'terminal', taskId: terminal.taskId, generation: terminal.generation,
|
|
1121
|
+
outcome: terminal.status === 'committed' ? 'completed'
|
|
1122
|
+
: ['cancelled', 'superseded', 'interrupted'].includes(terminal.status) ? 'skipped' : 'failed',
|
|
1123
|
+
reason: terminal.reason };
|
|
1124
|
+
}
|
|
1125
|
+
const queued = this.messageQueue?.getMessageState(sessionId, attemptId, selfAid);
|
|
1126
|
+
if (queued?.state === 'queued')
|
|
1127
|
+
return { state: 'live' };
|
|
1128
|
+
const active = state.active;
|
|
1129
|
+
if (active && matches(active.recoveryContext)) {
|
|
1130
|
+
const identity = { taskId: active.taskId, generation: active.generation };
|
|
1131
|
+
if (active.status === 'waiting_interaction' || active.status === 'interrupting'
|
|
1132
|
+
|| active.interactions.some(item => !item.settlement)
|
|
1133
|
+
|| this.isPauseRequested(sessionId) || this.retryDelayWaiters.has(sessionId))
|
|
1134
|
+
return { state: 'live', ...identity };
|
|
1135
|
+
const runner = this.getAgent(session.metadata?.channelKey || session.channel, session.baseagent, selfAid);
|
|
1136
|
+
return { state: runner.hasActiveStream(sessionId) ? 'live' : 'missing', ...identity,
|
|
1137
|
+
hasTerminalOwner: this.activeTimeoutHandlers.has(sessionId) };
|
|
1138
|
+
}
|
|
1139
|
+
// Never adopt or interrupt a newer task to clean up an older attempt.
|
|
1140
|
+
if (active) {
|
|
1141
|
+
return { state: 'terminal', outcome: 'skipped', reason: 'stale_generation',
|
|
1142
|
+
taskId: active.taskId, generation: active.generation };
|
|
1143
|
+
}
|
|
1144
|
+
if (queued?.state === 'active') {
|
|
1145
|
+
return { state: 'missing', hasTerminalOwner: this.activeTimeoutHandlers.has(sessionId) };
|
|
1146
|
+
}
|
|
1147
|
+
if (queued?.state === 'missing' && (active || terminal)) {
|
|
1148
|
+
const current = active ?? terminal;
|
|
1149
|
+
return { state: 'terminal', outcome: 'skipped', reason: 'stale_generation',
|
|
1150
|
+
taskId: current.taskId, generation: current.generation };
|
|
1151
|
+
}
|
|
1152
|
+
return { state: 'missing' };
|
|
1153
|
+
}
|
|
1154
|
+
/** Outer execution deadlines use the same terminal owner as runner idle timeouts. */
|
|
1155
|
+
async timeoutSession(sessionId, error) {
|
|
1156
|
+
const timeout = this.activeTimeoutHandlers.get(sessionId);
|
|
1157
|
+
if (timeout)
|
|
1158
|
+
await timeout(error);
|
|
1159
|
+
else
|
|
1160
|
+
await this.interruptSession(sessionId, error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout');
|
|
760
1161
|
}
|
|
761
1162
|
async interruptSession(sessionId, reason) {
|
|
762
1163
|
this.clearPauseSession(sessionId);
|
|
@@ -810,7 +1211,7 @@ export class ResponseEngine {
|
|
|
810
1211
|
// TimeoutController owns timeout delivery (including user-facing timeout
|
|
811
1212
|
// details) and already guarantees a single terminal status. Sending it
|
|
812
1213
|
// here as well would race that barrier and duplicate status.timeout.
|
|
813
|
-
if (!interruptedTaskId || effectiveReason === 'timeout')
|
|
1214
|
+
if (!interruptedTaskId || effectiveReason === 'timeout' || effectiveReason === 'execution_error')
|
|
814
1215
|
return Promise.resolve();
|
|
815
1216
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
816
1217
|
if (!channelInfo)
|
|
@@ -915,9 +1316,25 @@ export class ResponseEngine {
|
|
|
915
1316
|
causation: runtime?.causation,
|
|
916
1317
|
});
|
|
917
1318
|
}
|
|
1319
|
+
async recoverAuthorizedHandoffResult(params, selfAid) {
|
|
1320
|
+
if (!this.handoffRuntime) {
|
|
1321
|
+
return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff runtime not configured' };
|
|
1322
|
+
}
|
|
1323
|
+
const runtime = this.activeTaskRuntimeContexts.get(params.sessionId);
|
|
1324
|
+
return this.handoffRuntime.recoverReturnHandoff({
|
|
1325
|
+
selfAid,
|
|
1326
|
+
recoverySessionId: params.sessionId,
|
|
1327
|
+
handoffId: params.handoffId,
|
|
1328
|
+
content: params.content,
|
|
1329
|
+
actorId: params.actorId,
|
|
1330
|
+
causation: runtime?.causation,
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
918
1333
|
agentRegistry;
|
|
919
1334
|
setAgentRegistry(registry) {
|
|
920
1335
|
this.agentRegistry = registry;
|
|
1336
|
+
for (const { adapter } of this.channels.values())
|
|
1337
|
+
this.restoreAsyncQuestions(adapter);
|
|
921
1338
|
}
|
|
922
1339
|
/** 更新 EvolAgent.lastActivity —— 每次发出 status.* 事件(含 progress)时调用 */
|
|
923
1340
|
touchAgentActivity(channelKey) {
|
|
@@ -1014,6 +1431,15 @@ export class ResponseEngine {
|
|
|
1014
1431
|
if (!this.channelTypeMap.has(type)) {
|
|
1015
1432
|
this.channelTypeMap.set(type, adapter.channelName);
|
|
1016
1433
|
}
|
|
1434
|
+
this.restoreAsyncQuestions(adapter);
|
|
1435
|
+
}
|
|
1436
|
+
restoreAsyncQuestions(adapter) {
|
|
1437
|
+
const selfAid = this.agentRegistry?.resolveByChannel(adapter.channelKey)?.aid;
|
|
1438
|
+
if (selfAid && this.asyncQuestions) {
|
|
1439
|
+
void this.asyncQuestions.restore(adapter, selfAid).catch(error => {
|
|
1440
|
+
logger.warn(`[AsyncQuestions] channel restore failed: ${String(error)}`);
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1017
1443
|
}
|
|
1018
1444
|
/**
|
|
1019
1445
|
* 注销渠道适配器(热重载断开渠道时调用,避免遗留死实例)。
|
|
@@ -1207,7 +1633,10 @@ export class ResponseEngine {
|
|
|
1207
1633
|
&& (!authorization.triggerId || !authorization.runId || !authorization.attemptId)) {
|
|
1208
1634
|
throw new Error('fullaccess Trigger authorization lacks scheduler identity or approval provenance');
|
|
1209
1635
|
}
|
|
1210
|
-
if (authorization.source
|
|
1636
|
+
if (authorization.source === 'agent-policy') {
|
|
1637
|
+
assertAgentPolicyAuthorization(authorization);
|
|
1638
|
+
}
|
|
1639
|
+
else if (authorization.source !== 'fullaccess-command' && authorization.source !== 'trigger') {
|
|
1211
1640
|
throw new Error('fullaccess execution authorization has an unsupported source');
|
|
1212
1641
|
}
|
|
1213
1642
|
if (!isFullAccessEnabled()) {
|
|
@@ -1229,6 +1658,8 @@ export class ResponseEngine {
|
|
|
1229
1658
|
const originChannelKey = message.triggerMeta.originChannelKey;
|
|
1230
1659
|
if (!selfAid || !actorId || !channelType || !originChannelKey)
|
|
1231
1660
|
return undefined;
|
|
1661
|
+
const chatType = message.triggerMeta.originChatType ?? 'private';
|
|
1662
|
+
const conversationId = message.triggerMeta.originConversationId ?? actorId;
|
|
1232
1663
|
const control = message.triggerMeta.originControl === true;
|
|
1233
1664
|
let actor;
|
|
1234
1665
|
let resolvedRole = 'none';
|
|
@@ -1237,9 +1668,9 @@ export class ResponseEngine {
|
|
|
1237
1668
|
selfAid,
|
|
1238
1669
|
channelKey: originChannelKey,
|
|
1239
1670
|
channelType,
|
|
1240
|
-
chatType
|
|
1671
|
+
chatType,
|
|
1241
1672
|
actorId,
|
|
1242
|
-
conversationId
|
|
1673
|
+
conversationId,
|
|
1243
1674
|
});
|
|
1244
1675
|
actor = detail.actor;
|
|
1245
1676
|
resolvedRole = detail.effectiveRole || 'none';
|
|
@@ -1259,7 +1690,7 @@ export class ResponseEngine {
|
|
|
1259
1690
|
channelType,
|
|
1260
1691
|
originChannelKey,
|
|
1261
1692
|
control,
|
|
1262
|
-
peerKey: formatPeerKey(channelType, actorId),
|
|
1693
|
+
peerKey: formatPeerKey(channelType, chatType === 'group' ? conversationId : actorId),
|
|
1263
1694
|
};
|
|
1264
1695
|
}
|
|
1265
1696
|
resolveHandoffOriginExecutionIdentity(message, selfAid) {
|
|
@@ -1425,7 +1856,8 @@ export class ResponseEngine {
|
|
|
1425
1856
|
auditFullAccessEvent({
|
|
1426
1857
|
event: fallbackFullAccessAudit.authorization.source === 'trigger'
|
|
1427
1858
|
? 'fullaccess.trigger.execution.ended'
|
|
1428
|
-
:
|
|
1859
|
+
: fallbackFullAccessAudit.authorization.source === 'agent-policy'
|
|
1860
|
+
? 'fullaccess.agent-policy.execution.ended' : 'fullaccess.execution.ended',
|
|
1429
1861
|
source: fallbackFullAccessAudit.authorization.source,
|
|
1430
1862
|
actorId: fallbackFullAccessAudit.authorization.authorizedBy,
|
|
1431
1863
|
processRole: fallbackFullAccessAudit.processRole,
|
|
@@ -1446,6 +1878,15 @@ export class ResponseEngine {
|
|
|
1446
1878
|
async processMessageWithMonitors(message) {
|
|
1447
1879
|
const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
|
|
1448
1880
|
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
1881
|
+
if (message.taskRecovery) {
|
|
1882
|
+
const current = message.boundSessionId
|
|
1883
|
+
? await this.sessionManager.getSessionById(message.boundSessionId)
|
|
1884
|
+
: undefined;
|
|
1885
|
+
if (!current || current.selfAID !== message.selfAID || !isTaskRecoveryCurrent(current, message.taskRecovery)) {
|
|
1886
|
+
this.publishTriggerExecutionSkipped(message, 'recovery_stale');
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1449
1890
|
if (message.handoffDelivery
|
|
1450
1891
|
&& this.handoffRuntime
|
|
1451
1892
|
&& this.handoffRuntime.isDiscardedDelivery(message)) {
|
|
@@ -1508,7 +1949,8 @@ export class ResponseEngine {
|
|
|
1508
1949
|
const userRole = session.identity?.role || 'none';
|
|
1509
1950
|
const isInternalHandoff = message.source === 'handoff';
|
|
1510
1951
|
const fullAccessAuditCandidate = this.resolveFullAccessAuditCandidate(message);
|
|
1511
|
-
const isCurrentFullAccessCommand = fullAccessAuditCandidate?.authorization.source === 'fullaccess-command'
|
|
1952
|
+
const isCurrentFullAccessCommand = (fullAccessAuditCandidate?.authorization.source === 'fullaccess-command'
|
|
1953
|
+
|| fullAccessAuditCandidate?.authorization.source === 'agent-policy')
|
|
1512
1954
|
&& fullAccessAuditCandidate.processRole === 'fullaccess-run';
|
|
1513
1955
|
const isCurrentFullAccessTrigger = fullAccessAuditCandidate?.authorization.source === 'trigger'
|
|
1514
1956
|
&& fullAccessAuditCandidate.processRole === 'fullaccess-run';
|
|
@@ -1597,11 +2039,17 @@ export class ResponseEngine {
|
|
|
1597
2039
|
claimedByInternal: false,
|
|
1598
2040
|
terminalDelivered: false,
|
|
1599
2041
|
claim: () => {
|
|
1600
|
-
if (!timeoutControl.error || this.interruptedSessions.get(session.id)
|
|
2042
|
+
if (!timeoutControl.error || !['timeout', 'execution_error'].includes(this.interruptedSessions.get(session.id) ?? ''))
|
|
1601
2043
|
return undefined;
|
|
1602
2044
|
timeoutControl.claimedByInternal = true;
|
|
1603
2045
|
return timeoutControl.error;
|
|
1604
2046
|
},
|
|
2047
|
+
claimTerminal: () => {
|
|
2048
|
+
if (timeoutControl.terminalDelivered)
|
|
2049
|
+
return false;
|
|
2050
|
+
timeoutControl.terminalDelivered = true;
|
|
2051
|
+
return true;
|
|
2052
|
+
},
|
|
1605
2053
|
};
|
|
1606
2054
|
const rejectAfterInterruptBarrier = (error) => {
|
|
1607
2055
|
if (timeoutTriggered)
|
|
@@ -1610,9 +2058,10 @@ export class ResponseEngine {
|
|
|
1610
2058
|
timeoutControl.error = error;
|
|
1611
2059
|
this.timeoutErrors.set(session.id, error);
|
|
1612
2060
|
this.turnCoordinator.hydrate(session);
|
|
1613
|
-
|
|
1614
|
-
this.
|
|
1615
|
-
|
|
2061
|
+
const reason = error instanceof OrphanedTargetSessionError ? 'execution_error' : 'timeout';
|
|
2062
|
+
this.turnCoordinator.invalidate(session.id, reason);
|
|
2063
|
+
this.interruptedSessions.set(session.id, reason);
|
|
2064
|
+
const barrier = this.interruptSession(session.id, reason)
|
|
1616
2065
|
.catch(interruptError => {
|
|
1617
2066
|
logger.debug(`[ResponseEngine] Timeout interrupt barrier failed: ${interruptError instanceof Error ? interruptError.message : String(interruptError)}`);
|
|
1618
2067
|
});
|
|
@@ -1623,6 +2072,11 @@ export class ResponseEngine {
|
|
|
1623
2072
|
});
|
|
1624
2073
|
};
|
|
1625
2074
|
const triggerAttemptId = message.replyContext?.metadata?.triggerAttemptId;
|
|
2075
|
+
const externalTimeout = async (error) => {
|
|
2076
|
+
rejectAfterInterruptBarrier(error);
|
|
2077
|
+
await timeoutControl.barrier;
|
|
2078
|
+
};
|
|
2079
|
+
this.activeTimeoutHandlers.set(session.id, externalTimeout);
|
|
1626
2080
|
const resetTimer = (eventType, toolName) => {
|
|
1627
2081
|
monitor?.recordEvent(eventType || 'unknown', toolName);
|
|
1628
2082
|
if (typeof triggerAttemptId === 'string') {
|
|
@@ -1702,11 +2156,9 @@ export class ResponseEngine {
|
|
|
1702
2156
|
await Promise.race(processingPromises);
|
|
1703
2157
|
}
|
|
1704
2158
|
catch (error) {
|
|
1705
|
-
if (error instanceof Error && (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
|
|
2159
|
+
if (error instanceof Error && (isExecutionFailure(error) || error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
|
|
1706
2160
|
await timeoutControl.barrier;
|
|
1707
|
-
|
|
1708
|
-
await timeoutControl.deliverUnhandled?.(error);
|
|
1709
|
-
}
|
|
2161
|
+
await timeoutControl.deliverUnhandled?.(error);
|
|
1710
2162
|
this.timeoutErrors.delete(session.id);
|
|
1711
2163
|
}
|
|
1712
2164
|
// 超时错误:kill 级别已发送诊断信息,无需再发
|
|
@@ -1747,6 +2199,8 @@ export class ResponseEngine {
|
|
|
1747
2199
|
if (totalExecutionTimer)
|
|
1748
2200
|
clearTimeout(totalExecutionTimer);
|
|
1749
2201
|
this.activeMonitors.delete(streamKey);
|
|
2202
|
+
if (this.activeTimeoutHandlers.get(session.id) === externalTimeout)
|
|
2203
|
+
this.activeTimeoutHandlers.delete(session.id);
|
|
1750
2204
|
}
|
|
1751
2205
|
}
|
|
1752
2206
|
resolveFullAccessAuditCandidate(message) {
|
|
@@ -1754,15 +2208,18 @@ export class ResponseEngine {
|
|
|
1754
2208
|
if (commandAuthorization?.permissionMode === 'fullaccess'
|
|
1755
2209
|
&& commandAuthorization.processRole === 'fullaccess-run'
|
|
1756
2210
|
&& commandAuthorization.dataScope === 'daemon'
|
|
1757
|
-
&& commandAuthorization.source === 'fullaccess-command'
|
|
2211
|
+
&& (commandAuthorization.source === 'fullaccess-command' || commandAuthorization.source === 'agent-policy')
|
|
1758
2212
|
&& typeof commandAuthorization.authorizedBy === 'string'
|
|
1759
2213
|
&& commandAuthorization.authorizedBy.length > 0) {
|
|
1760
2214
|
let currentOwner = false;
|
|
1761
2215
|
try {
|
|
1762
2216
|
currentOwner = isFullAccessEnabled()
|
|
1763
2217
|
&& new Set(loadDaemonConfig().owners ?? []).has(commandAuthorization.authorizedBy);
|
|
2218
|
+
if (commandAuthorization.source === 'agent-policy')
|
|
2219
|
+
assertAgentPolicyAuthorization(commandAuthorization);
|
|
1764
2220
|
}
|
|
1765
2221
|
catch {
|
|
2222
|
+
currentOwner = false;
|
|
1766
2223
|
// Runtime validation will fail closed; the audit must still retain the
|
|
1767
2224
|
// attempted authorization without falsely labeling it active.
|
|
1768
2225
|
}
|
|
@@ -1882,7 +2339,7 @@ export class ResponseEngine {
|
|
|
1882
2339
|
// states. An untransitioned `created` agent or malformed lifecycle must
|
|
1883
2340
|
// not fall through to a base runner's default prompt. Missing lifecycle
|
|
1884
2341
|
// is normalized to active for legacy configurations.
|
|
1885
|
-
if (owningAgentForTask && !isActive && !isBootstrapping) {
|
|
2342
|
+
if (owningAgentForTask && ((!isActive && !isBootstrapping) || initializationBlocksTasks(owningAgentForTask.config))) {
|
|
1886
2343
|
const blockedLifecycle = lifecycle
|
|
1887
2344
|
?? `invalid (${String(owningAgentForTask.config.lifecycle)})`;
|
|
1888
2345
|
logger.error(`[ResponseEngine] Agent lifecycle is not runnable: agent=${owningAgentForTask.aid} lifecycle=${blockedLifecycle}`);
|
|
@@ -1979,7 +2436,7 @@ export class ResponseEngine {
|
|
|
1979
2436
|
?? this.resolveHandoffOriginExecutionIdentity(message, selfAid);
|
|
1980
2437
|
const configChannelType = authorizationIdentity?.channelType ?? currentChannelType;
|
|
1981
2438
|
const configChatType = authorizationIdentity
|
|
1982
|
-
? (message.handoffDelivery?.originAuthorization?.chatType ?? 'private')
|
|
2439
|
+
? (message.triggerMeta?.originChatType ?? message.handoffDelivery?.originAuthorization?.chatType ?? 'private')
|
|
1983
2440
|
: (chatType === 'group' ? 'group' : 'private');
|
|
1984
2441
|
// A group channel ID identifies the conversation, not the authenticated
|
|
1985
2442
|
// operator. Keep the historical private-chat fallback (where channelId
|
|
@@ -2132,6 +2589,7 @@ export class ResponseEngine {
|
|
|
2132
2589
|
modeConfig: resolvedMode.context.modeConfig,
|
|
2133
2590
|
state: modeState,
|
|
2134
2591
|
isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid),
|
|
2592
|
+
describeSendCommandFailure: describeProactiveSendCommandFailure,
|
|
2135
2593
|
logger,
|
|
2136
2594
|
} : null;
|
|
2137
2595
|
if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
|
|
@@ -2166,25 +2624,30 @@ export class ResponseEngine {
|
|
|
2166
2624
|
});
|
|
2167
2625
|
if (timeoutControl) {
|
|
2168
2626
|
timeoutControl.deliverUnhandled = async (timeoutError) => {
|
|
2169
|
-
if (timeoutControl.
|
|
2627
|
+
if (!timeoutControl.claimTerminal())
|
|
2170
2628
|
return;
|
|
2171
|
-
timeoutControl.terminalDelivered = true;
|
|
2172
2629
|
const isTotalExecutionTimeout = timeoutError.message === 'TOTAL_EXECUTION_TIMEOUT';
|
|
2173
|
-
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
2174
|
-
const statusPayload = {
|
|
2630
|
+
const totalExecutionMs = timeoutError instanceof ExecutionTimeoutError ? timeoutError.timeoutMs : this.totalExecutionLimitMs();
|
|
2631
|
+
const statusPayload = timeoutError instanceof OrphanedTargetSessionError ? {
|
|
2632
|
+
kind: 'status.error',
|
|
2633
|
+
metadata: { errorType: timeoutError.reason, message: '任务执行状态丢失,已结束本次执行。' },
|
|
2634
|
+
} : {
|
|
2175
2635
|
kind: 'status.timeout',
|
|
2176
2636
|
metadata: isTotalExecutionTimeout
|
|
2177
|
-
? (totalExecutionMs === undefined ? {} : { totalExecutionMs })
|
|
2637
|
+
? { ...(totalExecutionMs === undefined ? {} : { totalExecutionMs }),
|
|
2638
|
+
...(timeoutError instanceof ExecutionTimeoutError ? { reason: timeoutError.reason } : {}) }
|
|
2178
2639
|
: { idleSec: getLastIdleSec?.() || undefined },
|
|
2179
2640
|
};
|
|
2180
2641
|
const idleSec = getLastIdleSec?.() || 0;
|
|
2181
|
-
const userMessage =
|
|
2182
|
-
?
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
:
|
|
2642
|
+
const userMessage = timeoutError instanceof OrphanedTargetSessionError
|
|
2643
|
+
? '⚠️ 任务执行状态丢失,已结束本次执行。'
|
|
2644
|
+
: isTotalExecutionTimeout
|
|
2645
|
+
? (totalExecutionMs === undefined
|
|
2646
|
+
? '⚠️ 任务超过总执行时限,已自动中断'
|
|
2647
|
+
: `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
|
|
2648
|
+
: idleSec > 0
|
|
2649
|
+
? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断`
|
|
2650
|
+
: '⚠️ 任务超时,已自动中断';
|
|
2188
2651
|
agent.cleanupStream(streamKey);
|
|
2189
2652
|
this.sessionManager.clearProcessingIfTask(session.id, taskId);
|
|
2190
2653
|
if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
|
|
@@ -2198,13 +2661,13 @@ export class ResponseEngine {
|
|
|
2198
2661
|
await adapter.send(envelope, {
|
|
2199
2662
|
kind: 'result.error',
|
|
2200
2663
|
text: userMessage,
|
|
2201
|
-
reason: isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
|
|
2664
|
+
reason: isExecutionFailure(timeoutError) ? timeoutError.reason : isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
|
|
2202
2665
|
}).catch(() => { });
|
|
2203
2666
|
if (daemonTrigger) {
|
|
2204
2667
|
await adapter.send(envelope, statusPayload).catch(() => { });
|
|
2205
2668
|
}
|
|
2206
2669
|
this.touchAgentActivity(channelKey);
|
|
2207
|
-
this.publishTriggerExecutionFailure(message, timeoutError.message, {
|
|
2670
|
+
this.publishTriggerExecutionFailure(message, isExecutionFailure(timeoutError) ? timeoutError.reason : timeoutError.message, {
|
|
2208
2671
|
messageId,
|
|
2209
2672
|
causation: taskCausation,
|
|
2210
2673
|
});
|
|
@@ -2212,9 +2675,14 @@ export class ResponseEngine {
|
|
|
2212
2675
|
this.eventBus.publish({
|
|
2213
2676
|
type: 'task:error',
|
|
2214
2677
|
sessionId: session.id,
|
|
2678
|
+
agentAid: runnerSelfAid,
|
|
2679
|
+
peerId: configActorId,
|
|
2680
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
2681
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
2215
2682
|
taskId,
|
|
2216
2683
|
generation: turnLease?.generation,
|
|
2217
2684
|
error: timeoutError.message,
|
|
2685
|
+
terminalReason: isExecutionFailure(timeoutError) ? timeoutError.reason : undefined,
|
|
2218
2686
|
errorType,
|
|
2219
2687
|
agentName: agentNameForStats,
|
|
2220
2688
|
causation: taskCausation,
|
|
@@ -2343,14 +2811,28 @@ export class ResponseEngine {
|
|
|
2343
2811
|
// message is authoritative; session metadata is only a fallback for
|
|
2344
2812
|
// legacy paths that do not carry a per-message display name.
|
|
2345
2813
|
const peerName = message.peerName ?? session.metadata?.peerName;
|
|
2346
|
-
const peerId =
|
|
2814
|
+
const peerId = message.peerId ?? session.metadata?.peerId ?? message.channelId;
|
|
2347
2815
|
const peerShort = peerId ? peerId.split('.')[0].split(':')[0] : '?';
|
|
2348
2816
|
const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
|
|
2349
2817
|
logger.info(`[ResponseEngine] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} chatMode=${effectiveChatMode} baseagent=${session.baseagent} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
2350
2818
|
// Establish the durable turn before exposing any processing state. This
|
|
2351
2819
|
// lets an immediate /stop or replacement message close the same task and
|
|
2352
2820
|
// prevents a BusinessTrip reaction from becoming orphaned during setup.
|
|
2353
|
-
|
|
2821
|
+
if (message.taskRecovery && !isTaskRecoveryCurrent({
|
|
2822
|
+
...session, metadata: { ...session.metadata, turnState: this.turnCoordinator.current(session) },
|
|
2823
|
+
}, message.taskRecovery)) {
|
|
2824
|
+
this.publishTriggerExecutionSkipped(message, 'recovery_stale');
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
turnLease = await this.turnCoordinator.begin(session, taskId, undefined, configActorId ? {
|
|
2828
|
+
peerId: configActorId,
|
|
2829
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
2830
|
+
baseagent: session.baseagent,
|
|
2831
|
+
agentSessionId: session.agentSessionId,
|
|
2832
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
2833
|
+
recovery: message.taskRecovery?.origin,
|
|
2834
|
+
triggerAttemptId: message.triggerMeta?.attemptId,
|
|
2835
|
+
} : undefined);
|
|
2354
2836
|
// 记录开始处理
|
|
2355
2837
|
const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
|
|
2356
2838
|
this.eventBus.publish({
|
|
@@ -2427,7 +2909,10 @@ export class ResponseEngine {
|
|
|
2427
2909
|
if (payload.kind === 'result.text' && payload.isFinal) {
|
|
2428
2910
|
opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
|
|
2429
2911
|
}
|
|
2430
|
-
opts.metadata = withTaskMetadata(
|
|
2912
|
+
opts.metadata = withTaskMetadata({
|
|
2913
|
+
...(opts.metadata ?? {}),
|
|
2914
|
+
...(payload.kind === 'result.text' ? payload.metadata : {}),
|
|
2915
|
+
});
|
|
2431
2916
|
// status.notice is a diagnostic observation, not execution activity.
|
|
2432
2917
|
if (payload.kind.startsWith('status.') && payload.kind !== 'status.notice') {
|
|
2433
2918
|
this.touchAgentActivity(channelKey);
|
|
@@ -2572,10 +3057,10 @@ export class ResponseEngine {
|
|
|
2572
3057
|
};
|
|
2573
3058
|
})();
|
|
2574
3059
|
let effectivePermissionMode = 'readonly';
|
|
2575
|
-
// Allocate
|
|
2576
|
-
// context is installed.
|
|
2577
|
-
//
|
|
2578
|
-
const sessionRuntimeDir =
|
|
3060
|
+
// Allocate the stable session-owned root before any runner preflight
|
|
3061
|
+
// context is installed. Every runner for this EvolCore session receives
|
|
3062
|
+
// the same directory, independent of daemon or runner lifetimes.
|
|
3063
|
+
const sessionRuntimeDir = ensureSessionRuntimeDir(session.id);
|
|
2579
3064
|
const recordExecutionAnomaly = triggerRunId
|
|
2580
3065
|
? (anomaly) => {
|
|
2581
3066
|
recordTriggerExecutionAnomaly(triggerRunId, {
|
|
@@ -2623,8 +3108,8 @@ export class ResponseEngine {
|
|
|
2623
3108
|
pauseController: taskPauseController,
|
|
2624
3109
|
preToolUsePolicyHook: pureSessionPolicyHook,
|
|
2625
3110
|
armApprovedDelegationCommand: this.agentDelegationRegistry
|
|
2626
|
-
? (
|
|
2627
|
-
|
|
3111
|
+
? (sessionToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
|
|
3112
|
+
sessionToken,
|
|
2628
3113
|
sessionId: session.id,
|
|
2629
3114
|
taskId,
|
|
2630
3115
|
commandHash,
|
|
@@ -2764,6 +3249,7 @@ export class ResponseEngine {
|
|
|
2764
3249
|
let previousFallbackState;
|
|
2765
3250
|
let agentModel;
|
|
2766
3251
|
let v2HandoffIds = [];
|
|
3252
|
+
let v2HandoffResultRefs = [];
|
|
2767
3253
|
let v2HandoffDirection;
|
|
2768
3254
|
let handoffPromptRendered = false;
|
|
2769
3255
|
let runtimeEnv;
|
|
@@ -2798,10 +3284,19 @@ export class ResponseEngine {
|
|
|
2798
3284
|
if (commandFullAccess.permissionMode !== 'fullaccess'
|
|
2799
3285
|
|| commandFullAccess.processRole !== 'fullaccess-run'
|
|
2800
3286
|
|| commandFullAccess.dataScope !== 'daemon'
|
|
2801
|
-
|| commandFullAccess.source !== 'fullaccess-command'
|
|
3287
|
+
|| (commandFullAccess.source !== 'fullaccess-command' && commandFullAccess.source !== 'agent-policy')
|
|
2802
3288
|
|| !commandFullAccess.authorizedBy) {
|
|
2803
3289
|
throw new Error('fullaccess command authorization is missing or malformed');
|
|
2804
3290
|
}
|
|
3291
|
+
if (commandFullAccess.source === 'agent-policy'
|
|
3292
|
+
&& (commandFullAccess.agentAid !== runnerSelfAid
|
|
3293
|
+
|| configChatType !== 'private'
|
|
3294
|
+
|| (message.source !== undefined && message.source !== 'user')
|
|
3295
|
+
|| !hasTrustedPrincipal(authenticatedActor)
|
|
3296
|
+
|| authenticatedActor.principalId !== commandFullAccess.authorizedBy
|
|
3297
|
+
|| authenticatedActor.operatorId !== commandFullAccess.policyActor?.operatorId)) {
|
|
3298
|
+
throw new Error('Agent fullaccess policy authorization is missing its Agent identity');
|
|
3299
|
+
}
|
|
2805
3300
|
fullAccessAuthorization = commandFullAccess;
|
|
2806
3301
|
}
|
|
2807
3302
|
else {
|
|
@@ -3016,18 +3511,25 @@ export class ResponseEngine {
|
|
|
3016
3511
|
// happen before task-start auto compact because compact is a backend
|
|
3017
3512
|
// management action just like the subsequent runQuery.
|
|
3018
3513
|
let runAgentSessionId = session.agentSessionId;
|
|
3019
|
-
if (session.threadId
|
|
3514
|
+
if ((session.threadId || message.taskRecovery)
|
|
3020
3515
|
&& typeof this.sessionManager.getSessionById === 'function') {
|
|
3021
3516
|
const latestBackendSession = await this.sessionManager.getSessionById(session.id);
|
|
3022
|
-
if (!latestBackendSession || latestBackendSession.threadId !== session.threadId) {
|
|
3517
|
+
if (!latestBackendSession || latestBackendSession.deletedAt || latestBackendSession.threadId !== session.threadId) {
|
|
3023
3518
|
throw new Error('topic session changed before backend run started');
|
|
3024
3519
|
}
|
|
3520
|
+
if (message.taskRecovery && (latestBackendSession.agentSessionId !== message.taskRecovery.agentSessionId
|
|
3521
|
+
|| latestBackendSession.baseagent !== message.taskRecovery.baseagent)) {
|
|
3522
|
+
throw new Error('recovery backend changed before execution');
|
|
3523
|
+
}
|
|
3025
3524
|
runAgentSessionId = latestBackendSession.agentSessionId;
|
|
3026
3525
|
session.agentSessionId = runAgentSessionId;
|
|
3027
3526
|
session.metadata = latestBackendSession.metadata;
|
|
3028
3527
|
}
|
|
3029
3528
|
modelOverride = {
|
|
3030
3529
|
...(modelOverride || {}),
|
|
3530
|
+
...(message.taskRecovery
|
|
3531
|
+
? { recovery: { agentSessionId: message.taskRecovery.agentSessionId } }
|
|
3532
|
+
: {}),
|
|
3031
3533
|
...(session.threadId
|
|
3032
3534
|
? { backend: { kind: 'topic', agentSessionId: runAgentSessionId ?? null } }
|
|
3033
3535
|
: {}),
|
|
@@ -3040,13 +3542,17 @@ export class ResponseEngine {
|
|
|
3040
3542
|
},
|
|
3041
3543
|
};
|
|
3042
3544
|
const refreshTopicBindingForLogicalRetry = async () => {
|
|
3043
|
-
if (!session.threadId || typeof this.sessionManager.getSessionById !== 'function')
|
|
3545
|
+
if ((!session.threadId && !message.taskRecovery) || typeof this.sessionManager.getSessionById !== 'function')
|
|
3044
3546
|
return;
|
|
3045
3547
|
const latest = await this.sessionManager.getSessionById(session.id);
|
|
3046
3548
|
if (!latest || latest.threadId !== session.threadId) {
|
|
3047
3549
|
throw new Error('topic session changed during backend run');
|
|
3048
3550
|
}
|
|
3049
3551
|
const latestAgentSessionId = latest.agentSessionId;
|
|
3552
|
+
if (message.taskRecovery && (latestAgentSessionId !== message.taskRecovery.agentSessionId
|
|
3553
|
+
|| latest.baseagent !== message.taskRecovery.baseagent)) {
|
|
3554
|
+
throw new Error('recovery backend changed during execution');
|
|
3555
|
+
}
|
|
3050
3556
|
session.agentSessionId = latestAgentSessionId;
|
|
3051
3557
|
session.metadata = latest.metadata;
|
|
3052
3558
|
if (latestAgentSessionId === runAgentSessionId)
|
|
@@ -3083,6 +3589,26 @@ export class ResponseEngine {
|
|
|
3083
3589
|
groupId: session.metadata?.groupId || message.channelId,
|
|
3084
3590
|
channel: currentChannelType || message.channel,
|
|
3085
3591
|
});
|
|
3592
|
+
const historyGroupId = session.metadata?.groupId || message.channelId;
|
|
3593
|
+
const canUseGroupHistory = (operation) => {
|
|
3594
|
+
if (currentChannelType !== 'aun' || session.chatType !== 'group' || !selfAid || !historyGroupId)
|
|
3595
|
+
return false;
|
|
3596
|
+
const historySubject = buildAuthSubject({
|
|
3597
|
+
selfAid, actorId: configActorId, channel: channelKey, channelType: configChannelType,
|
|
3598
|
+
chatType: configChatType, channelId: message.channelId,
|
|
3599
|
+
conversationId: configChatType === 'group' ? historyGroupId : configActorId,
|
|
3600
|
+
permissionMode: effectivePermissionMode,
|
|
3601
|
+
});
|
|
3602
|
+
return authorizeOperation({
|
|
3603
|
+
source: 'agent-tool', subject: historySubject, audit: false,
|
|
3604
|
+
intent: {
|
|
3605
|
+
operation, scope: ['owner', 'admin'].includes(historySubject.role) ? 'agent' : 'relation',
|
|
3606
|
+
source: 'agent-tool',
|
|
3607
|
+
args: { self: selfAid, targetId: historyGroupId, peer: historyGroupId,
|
|
3608
|
+
peerKey: formatPeerKey('aun', historyGroupId) },
|
|
3609
|
+
},
|
|
3610
|
+
}).allow;
|
|
3611
|
+
};
|
|
3086
3612
|
// Kit renderer: 组装上下文
|
|
3087
3613
|
const pkgRoot = getPackageRoot();
|
|
3088
3614
|
const kitCtx = {
|
|
@@ -3098,7 +3624,7 @@ export class ResponseEngine {
|
|
|
3098
3624
|
KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
|
|
3099
3625
|
KITS_MESSAGE_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'message-fragments'),
|
|
3100
3626
|
// evolcore 运行模式:dev=源码仓库 | install=全局安装包
|
|
3101
|
-
evolcoreMode:
|
|
3627
|
+
evolcoreMode: readEvolcoreRuntimeFlavor(),
|
|
3102
3628
|
// 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
|
|
3103
3629
|
PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
|
|
3104
3630
|
RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
|
|
@@ -3114,11 +3640,12 @@ export class ResponseEngine {
|
|
|
3114
3640
|
peerKey,
|
|
3115
3641
|
peerName: peerName || undefined,
|
|
3116
3642
|
peerRole,
|
|
3117
|
-
|
|
3643
|
+
// Display missing identity explicitly without changing the mode resolver's legacy fallback.
|
|
3644
|
+
peerType: peerType || 'unknown',
|
|
3118
3645
|
sameDevice: message.sameDevice ?? false,
|
|
3119
3646
|
sameNetwork: message.sameNetwork ?? false,
|
|
3120
3647
|
sameEgressIp: message.sameEgressIp ?? false,
|
|
3121
|
-
groupId: session.
|
|
3648
|
+
groupId: session.chatType === 'group' ? historyGroupId : undefined,
|
|
3122
3649
|
groupName: session.metadata?.groupName || undefined,
|
|
3123
3650
|
// 信封展示用:有群名则「名<ID>」,否则纯 ID。规避模板引擎无 not/else 的限制。
|
|
3124
3651
|
groupLabel: session.metadata?.groupId
|
|
@@ -3133,6 +3660,7 @@ export class ResponseEngine {
|
|
|
3133
3660
|
?? session.metadata?.mentionMode
|
|
3134
3661
|
?? message.mentionMode
|
|
3135
3662
|
?? undefined,
|
|
3663
|
+
groupHistoryAvailable: canUseGroupHistory('ec.group.history'),
|
|
3136
3664
|
clientType: message.clientType || undefined,
|
|
3137
3665
|
permissionMode: effectivePermissionMode,
|
|
3138
3666
|
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
@@ -3241,6 +3769,7 @@ export class ResponseEngine {
|
|
|
3241
3769
|
v2HandoffIds = Array.from(new Set(items.flatMap(item => (item.handoff?.handoffIds?.length
|
|
3242
3770
|
? item.handoff.handoffIds
|
|
3243
3771
|
: (item.handoff?.handoffId ? [item.handoff.handoffId] : [])))));
|
|
3772
|
+
v2HandoffResultRefs = items.flatMap(item => item.handoff?.resultRefs ?? []);
|
|
3244
3773
|
v2HandoffDirection = message.handoffDelivery.direction;
|
|
3245
3774
|
return items;
|
|
3246
3775
|
}
|
|
@@ -3314,18 +3843,23 @@ export class ResponseEngine {
|
|
|
3314
3843
|
return;
|
|
3315
3844
|
}
|
|
3316
3845
|
if (v2HandoffIds.length > 0 && handoffPromptRendered && this.handoffRuntime && selfAid) {
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
this.handoffRuntime.completeOriginContext(selfAid, handoffId);
|
|
3320
|
-
|
|
3321
|
-
|
|
3846
|
+
if (v2HandoffDirection === 'origin' && v2HandoffResultRefs.length > 0) {
|
|
3847
|
+
for (const ref of v2HandoffResultRefs) {
|
|
3848
|
+
this.handoffRuntime.completeOriginContext(selfAid, ref.handoffId, ref.resultId);
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
else {
|
|
3852
|
+
for (const handoffId of v2HandoffIds) {
|
|
3853
|
+
if (v2HandoffDirection === 'origin')
|
|
3854
|
+
this.handoffRuntime.completeOriginContext(selfAid, handoffId);
|
|
3855
|
+
else if (v2HandoffDirection === 'target')
|
|
3856
|
+
this.handoffRuntime.completeTargetContext(selfAid, handoffId);
|
|
3857
|
+
}
|
|
3322
3858
|
}
|
|
3323
3859
|
}
|
|
3324
|
-
//
|
|
3325
|
-
//
|
|
3326
|
-
//
|
|
3327
|
-
// explicitly so sandboxed helper commands never fall back to a shared
|
|
3328
|
-
// agent or project directory for transient state.
|
|
3860
|
+
// Pass the stable session directory explicitly so sandboxed helper
|
|
3861
|
+
// commands never fall back to a process, agent, or project directory
|
|
3862
|
+
// for transient state.
|
|
3329
3863
|
const taskRuntimeContext = {
|
|
3330
3864
|
taskId,
|
|
3331
3865
|
sessionId: session.id,
|
|
@@ -3408,7 +3942,8 @@ export class ResponseEngine {
|
|
|
3408
3942
|
auditFullAccessEvent({
|
|
3409
3943
|
event: fullAccessAuthorization.source === 'trigger'
|
|
3410
3944
|
? 'fullaccess.trigger.execution.started'
|
|
3411
|
-
:
|
|
3945
|
+
: fullAccessAuthorization.source === 'agent-policy'
|
|
3946
|
+
? 'fullaccess.agent-policy.execution.started' : 'fullaccess.execution.started',
|
|
3412
3947
|
source: fullAccessAuthorization.source,
|
|
3413
3948
|
actorId: fullAccessAuthorization.authorizedBy,
|
|
3414
3949
|
processRole: fullAccessAuditProcessRole,
|
|
@@ -3432,6 +3967,23 @@ export class ResponseEngine {
|
|
|
3432
3967
|
}
|
|
3433
3968
|
fullAccessRunnerInvoked = true;
|
|
3434
3969
|
}
|
|
3970
|
+
if (message.asyncQuestionAnswer) {
|
|
3971
|
+
if (session.baseagent !== 'codex' || runAgentSessionId !== message.asyncQuestionAnswer.providerThreadId) {
|
|
3972
|
+
throw new Error('Async question backend changed before queued resume');
|
|
3973
|
+
}
|
|
3974
|
+
if (!(await this.asyncQuestions?.validateQueuedAnswer(session, message.asyncQuestionAnswer))) {
|
|
3975
|
+
throw new Error('Async question authorization or persisted answer changed before queued resume');
|
|
3976
|
+
}
|
|
3977
|
+
// An active queue item restored after submission resumes the unfinished work.
|
|
3978
|
+
// Its native answer is already in Codex history and must not be appended twice.
|
|
3979
|
+
if (!message.restartResume?.submitted) {
|
|
3980
|
+
modelOverride = { ...modelOverride, asyncQuestionAnswer: message.asyncQuestionAnswer };
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
if (session.baseagent === 'codex') {
|
|
3984
|
+
modelOverride = { ...modelOverride,
|
|
3985
|
+
enableRequestUserInput: effectiveAgentConfig?.baseagents?.codex?.enableRequestUserInput ?? true };
|
|
3986
|
+
}
|
|
3435
3987
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, runAgentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride, runtimeEnv);
|
|
3436
3988
|
// The turn may be superseded while the runner is still creating its
|
|
3437
3989
|
// backend query. Replay the interrupt after runQuery resolves, when
|
|
@@ -3448,7 +4000,23 @@ export class ResponseEngine {
|
|
|
3448
4000
|
const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
|
|
3449
4001
|
resetTimer(eventType, toolName);
|
|
3450
4002
|
attemptTimeout?.reset();
|
|
3451
|
-
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid)
|
|
4003
|
+
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, selfAid, async (request) => {
|
|
4004
|
+
if (!this.asyncQuestions)
|
|
4005
|
+
throw new Error('Async question service is unavailable');
|
|
4006
|
+
if (permissionContext.approvalInteractionPolicy === 'deny') {
|
|
4007
|
+
logger.info(`[AsyncQuestions] no interactive recipient for unattended task: session=${session.id}`);
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
await renderer.flush(false);
|
|
4011
|
+
try {
|
|
4012
|
+
await this.asyncQuestions.present(session, request, message, adapter, envelope);
|
|
4013
|
+
}
|
|
4014
|
+
catch (error) {
|
|
4015
|
+
// Card dispatch is non-blocking business input. Keep consuming the running turn;
|
|
4016
|
+
// pending records can be delivered after reconnect/restart.
|
|
4017
|
+
logger.warn(`[AsyncQuestions] presentation failed: session=${session.id} error=${String(error)}`);
|
|
4018
|
+
}
|
|
4019
|
+
});
|
|
3452
4020
|
streamResult = attemptTimeout
|
|
3453
4021
|
? await Promise.race([processAttempt, attemptTimeout.promise])
|
|
3454
4022
|
: await processAttempt;
|
|
@@ -3867,7 +4435,7 @@ export class ResponseEngine {
|
|
|
3867
4435
|
// `hasReceivedText` 是任务级计数。工具调用前的文字已经显示,并不表示最后一轮
|
|
3868
4436
|
// (通常来自 complete.result)的文字也已显示;用本轮投影标记避免终态丢失。
|
|
3869
4437
|
if (shouldSuppress() || !streamResult.hasProjectedCurrentReplyText) {
|
|
3870
|
-
renderer.addText(visibleFinalReplyText);
|
|
4438
|
+
await renderer.addText(visibleFinalReplyText, undefined, undefined, streamResult.assistantUuid);
|
|
3871
4439
|
}
|
|
3872
4440
|
}
|
|
3873
4441
|
// 先清理流和处理中状态(保证即使 flush 卡住,session 也不会永久处于"处理中")
|
|
@@ -3886,7 +4454,7 @@ export class ResponseEngine {
|
|
|
3886
4454
|
const suffix = showHint
|
|
3887
4455
|
? `\n\n---\n⚠️ [降级模型: ${usedFallback.model} | 可告诉我"帮我检查可用模型"来诊断]`
|
|
3888
4456
|
: `\n\n---\n⚠️ [降级模型: ${usedFallback.model}]`;
|
|
3889
|
-
renderer.addText(suffix);
|
|
4457
|
+
await renderer.addText(suffix, undefined, undefined, streamResult.assistantUuid);
|
|
3890
4458
|
if (showHint && curFbState) {
|
|
3891
4459
|
curFbState.hintShown = true;
|
|
3892
4460
|
this.modelFallbackMap.set(session.id, curFbState);
|
|
@@ -3899,7 +4467,14 @@ export class ResponseEngine {
|
|
|
3899
4467
|
}
|
|
3900
4468
|
else {
|
|
3901
4469
|
// Flush 剩余内容(文件标记已在 flush 时自动移除)
|
|
3902
|
-
|
|
4470
|
+
// A final text flush can also contain failed output. Preserve the
|
|
4471
|
+
// outcome so AUN does not label that text as normal completion.
|
|
4472
|
+
if (streamResult.isError && !isExpectedTerminalInterrupt(streamResult.terminalReason)) {
|
|
4473
|
+
await renderer.flush(true, 'failed');
|
|
4474
|
+
}
|
|
4475
|
+
else {
|
|
4476
|
+
await renderer.flush(!isExpectedTerminalInterrupt(streamResult.terminalReason));
|
|
4477
|
+
}
|
|
3903
4478
|
}
|
|
3904
4479
|
const terminalInterrupt = isExpectedTerminalInterrupt(streamResult.terminalReason);
|
|
3905
4480
|
if (!streamResult.isError && !terminalInterrupt) {
|
|
@@ -3922,7 +4497,8 @@ export class ResponseEngine {
|
|
|
3922
4497
|
}
|
|
3923
4498
|
}
|
|
3924
4499
|
else {
|
|
3925
|
-
await this.turnCoordinator.finish(session, turnLease, terminalInterrupt ? 'cancelled' : 'failed',
|
|
4500
|
+
await this.turnCoordinator.finish(session, turnLease, terminalInterrupt ? 'cancelled' : 'failed', permissionTerminalFailure ? 'permission_denied'
|
|
4501
|
+
: streamResult.protocolIncompleteReason || streamResult.terminalReason || streamResult.subtype);
|
|
3926
4502
|
}
|
|
3927
4503
|
// 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
|
|
3928
4504
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
@@ -3956,7 +4532,7 @@ export class ResponseEngine {
|
|
|
3956
4532
|
// DaemonChannel settles a trigger when it receives a terminal
|
|
3957
4533
|
// payload. Send the metadata-bearing status first so failed runs do
|
|
3958
4534
|
// not lose their tool counts and recovery diagnostics.
|
|
3959
|
-
if (message
|
|
4535
|
+
if (this.isTrustedDaemonTrigger(message)) {
|
|
3960
4536
|
await adapter.send(envelope, { kind: 'status.error', metadata: terminalMetadata }).catch(() => { });
|
|
3961
4537
|
await adapter.send(envelope, { kind: 'result.error', text: userErrorSummary, reason: rawSubtype }).catch(() => { });
|
|
3962
4538
|
}
|
|
@@ -3997,6 +4573,10 @@ export class ResponseEngine {
|
|
|
3997
4573
|
this.eventBus.publish({
|
|
3998
4574
|
type: 'task:error',
|
|
3999
4575
|
sessionId: session.id,
|
|
4576
|
+
agentAid: runnerSelfAid,
|
|
4577
|
+
peerId: configActorId,
|
|
4578
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
4579
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
4000
4580
|
taskId,
|
|
4001
4581
|
generation: turnLease?.generation,
|
|
4002
4582
|
error: errorSummary,
|
|
@@ -4275,7 +4855,8 @@ export class ResponseEngine {
|
|
|
4275
4855
|
await timeoutControl?.barrier;
|
|
4276
4856
|
}
|
|
4277
4857
|
const handlingTimeout = error instanceof Error
|
|
4278
|
-
&& (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT');
|
|
4858
|
+
&& (isExecutionFailure(error) || error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT');
|
|
4859
|
+
const ownsTimeoutTerminal = !handlingTimeout || !timeoutControl || timeoutControl.claimTerminal();
|
|
4279
4860
|
if (handlingTimeout) {
|
|
4280
4861
|
this.timeoutErrors.delete(session.id);
|
|
4281
4862
|
}
|
|
@@ -4297,11 +4878,12 @@ export class ResponseEngine {
|
|
|
4297
4878
|
const errType = classifyError(error);
|
|
4298
4879
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
4299
4880
|
const isTotalExecutionTimeout = error instanceof Error && error.message === 'TOTAL_EXECUTION_TIMEOUT';
|
|
4300
|
-
const isTimeout = error instanceof Error && error.message === 'SDK_TIMEOUT';
|
|
4881
|
+
const isTimeout = error instanceof Error && (error.message === 'SDK_TIMEOUT' || error instanceof ExecutionTimeoutError);
|
|
4301
4882
|
const canPublishTerminal = !turnLease || this.turnCoordinator.canPublish(turnLease);
|
|
4302
4883
|
const staleSupersededTurn = !!turnLease
|
|
4303
4884
|
&& !canPublishTerminal
|
|
4304
4885
|
&& interruptReason !== 'timeout'
|
|
4886
|
+
&& interruptReason !== 'execution_error'
|
|
4305
4887
|
&& !isTimeout
|
|
4306
4888
|
&& !isTotalExecutionTimeout;
|
|
4307
4889
|
const isUserInterrupt = isNonErrorInterruptReason(interruptReason) || staleSupersededTurn;
|
|
@@ -4309,7 +4891,7 @@ export class ResponseEngine {
|
|
|
4309
4891
|
if (isUserInterrupt) {
|
|
4310
4892
|
await this.publishTriggerExecutionInterrupted(message, adapter, envelope, interruptReason ?? 'stale_turn', taskCausation);
|
|
4311
4893
|
}
|
|
4312
|
-
else {
|
|
4894
|
+
else if (ownsTimeoutTerminal) {
|
|
4313
4895
|
this.publishTriggerExecutionFailure(message, errorMsg, {
|
|
4314
4896
|
messageId,
|
|
4315
4897
|
causation: taskCausation,
|
|
@@ -4318,24 +4900,27 @@ export class ResponseEngine {
|
|
|
4318
4900
|
if (leaseWasCurrent && turnLease) {
|
|
4319
4901
|
await this.turnCoordinator.finish(session, turnLease, 'failed', error instanceof Error ? error.message : String(error));
|
|
4320
4902
|
}
|
|
4321
|
-
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
4903
|
+
const totalExecutionMs = error instanceof ExecutionTimeoutError ? error.timeoutMs : this.totalExecutionLimitMs();
|
|
4322
4904
|
const procStatus = errType === ErrorType.SDK_TIMEOUT || isTotalExecutionTimeout ? 'timeout'
|
|
4323
4905
|
: errType === ErrorType.STREAM_ERROR ? 'interrupted'
|
|
4324
4906
|
: 'error';
|
|
4325
4907
|
const daemonTrigger = this.isTrustedDaemonTrigger(message);
|
|
4326
|
-
const statusPayload =
|
|
4327
|
-
? { kind: 'status.
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4908
|
+
const statusPayload = error instanceof OrphanedTargetSessionError
|
|
4909
|
+
? { kind: 'status.error', metadata: { errorType: error.reason, message: '任务执行状态丢失,已结束本次执行。' } }
|
|
4910
|
+
: procStatus === 'timeout'
|
|
4911
|
+
? { kind: 'status.timeout', metadata: isTotalExecutionTimeout
|
|
4912
|
+
? { ...(totalExecutionMs === undefined ? {} : { totalExecutionMs }),
|
|
4913
|
+
...(error instanceof ExecutionTimeoutError ? { reason: error.reason } : {}) }
|
|
4914
|
+
: { idleSec: getLastIdleSec?.() || undefined } }
|
|
4915
|
+
: procStatus === 'interrupted'
|
|
4916
|
+
? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
|
|
4917
|
+
: { kind: 'status.error' };
|
|
4333
4918
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
4334
|
-
if (!isUserInterrupt && !daemonTrigger) {
|
|
4919
|
+
if (!isUserInterrupt && ownsTimeoutTerminal && !daemonTrigger) {
|
|
4335
4920
|
await adapter.send(envelope, statusPayload).catch(() => { });
|
|
4336
4921
|
this.touchAgentActivity(channelKey);
|
|
4337
4922
|
}
|
|
4338
|
-
if (!isUserInterrupt && daemonTrigger)
|
|
4923
|
+
if (!isUserInterrupt && ownsTimeoutTerminal && daemonTrigger)
|
|
4339
4924
|
this.touchAgentActivity(channelKey);
|
|
4340
4925
|
// 用户主动中断时降级日志;其余仍按 error 记录
|
|
4341
4926
|
if (isUserInterrupt) {
|
|
@@ -4349,13 +4934,18 @@ export class ResponseEngine {
|
|
|
4349
4934
|
// (message-queue / slash-handler)已发过 task:interrupted(它填充了
|
|
4350
4935
|
// interruptedSessions,isUserInterrupt 才会为真),stats 侧已据此收尾任务。
|
|
4351
4936
|
// 此处不再发任何事件——发 task:error 会误归类,重发 task:interrupted 会重复记账。
|
|
4352
|
-
if (!isUserInterrupt) {
|
|
4937
|
+
if (!isUserInterrupt && ownsTimeoutTerminal) {
|
|
4353
4938
|
this.eventBus.publish({
|
|
4354
4939
|
type: 'task:error',
|
|
4355
4940
|
sessionId: session.id,
|
|
4941
|
+
agentAid: runnerSelfAid,
|
|
4942
|
+
peerId: configActorId,
|
|
4943
|
+
encrypted: message.encrypted ?? message.replyContext?.metadata?.encrypted,
|
|
4944
|
+
originTriggerId: message.triggerMeta?.triggerId,
|
|
4356
4945
|
taskId,
|
|
4357
4946
|
generation: turnLease?.generation,
|
|
4358
4947
|
error: errorMsg,
|
|
4948
|
+
terminalReason: isExecutionFailure(error) ? error.reason : undefined,
|
|
4359
4949
|
errorType,
|
|
4360
4950
|
agentName: agentNameForStats,
|
|
4361
4951
|
causation: taskCausation,
|
|
@@ -4363,14 +4953,19 @@ export class ResponseEngine {
|
|
|
4363
4953
|
recordCausationSpan(taskCausation, 'task.run', { status: 'failed', refs: { taskId, sessionId: session.id, messageId }, reason: errorType });
|
|
4364
4954
|
}
|
|
4365
4955
|
// 记录处理失败
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4956
|
+
if (ownsTimeoutTerminal) {
|
|
4957
|
+
logger.message({
|
|
4958
|
+
msgId: messageId,
|
|
4959
|
+
sessionId: session.id,
|
|
4960
|
+
dir: 'inbound',
|
|
4961
|
+
status: isUserInterrupt ? 'interrupted' : 'failed',
|
|
4962
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4963
|
+
});
|
|
4964
|
+
}
|
|
4965
|
+
else {
|
|
4966
|
+
logger.info(`[ResponseEngine] Duplicate timeout terminal suppressed: session=${session.id} task=${taskId}`);
|
|
4967
|
+
}
|
|
4968
|
+
if (error instanceof Error && !isUserInterrupt && ownsTimeoutTerminal) {
|
|
4374
4969
|
logger.error(`[${message.channel}] Error stack:`, error.stack);
|
|
4375
4970
|
}
|
|
4376
4971
|
// 发送用户友好的错误消息
|
|
@@ -4380,7 +4975,11 @@ export class ResponseEngine {
|
|
|
4380
4975
|
const retryExhaustedCount = getRetryExhaustedCount(error);
|
|
4381
4976
|
const modelFallbackExhaustedMessage = getModelFallbackExhaustedMessage(error);
|
|
4382
4977
|
const retryInputAlreadySubmitted = wasRetryInputAlreadySubmitted(error);
|
|
4383
|
-
if (
|
|
4978
|
+
if (!ownsTimeoutTerminal) {
|
|
4979
|
+
// The monitor path already owns delivery for this timeout. Cleanup and
|
|
4980
|
+
// durable turn finalization above still run, but no terminal is emitted twice.
|
|
4981
|
+
}
|
|
4982
|
+
else if (isUserInterrupt) {
|
|
4384
4983
|
logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
|
|
4385
4984
|
}
|
|
4386
4985
|
else if (error?._errorAlreadySent && !daemonTrigger && !retryExhaustedCount && !isTimeout && !isTotalExecutionTimeout) {
|
|
@@ -4389,19 +4988,21 @@ export class ResponseEngine {
|
|
|
4389
4988
|
else {
|
|
4390
4989
|
// SDK_TIMEOUT:status.timeout 已发结构化状态,此处再补一条用户可见的错误文本(result.error)
|
|
4391
4990
|
const idleSec = getLastIdleSec?.() || 0;
|
|
4392
|
-
const userMessage =
|
|
4393
|
-
?
|
|
4394
|
-
:
|
|
4395
|
-
?
|
|
4396
|
-
:
|
|
4397
|
-
?
|
|
4398
|
-
:
|
|
4399
|
-
?
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
:
|
|
4991
|
+
const userMessage = error instanceof OrphanedTargetSessionError
|
|
4992
|
+
? '⚠️ 任务执行状态丢失,已结束本次执行。'
|
|
4993
|
+
: retryExhaustedCount
|
|
4994
|
+
? formatRetryableErrorFinalMessage(error, retryExhaustedCount)
|
|
4995
|
+
: retryInputAlreadySubmitted
|
|
4996
|
+
? formatRetryInputAlreadySubmittedMessage(error)
|
|
4997
|
+
: modelFallbackExhaustedMessage
|
|
4998
|
+
? modelFallbackExhaustedMessage
|
|
4999
|
+
: isTotalExecutionTimeout
|
|
5000
|
+
? (totalExecutionMs === undefined
|
|
5001
|
+
? '⚠️ 任务超过总执行时限,已自动中断'
|
|
5002
|
+
: `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
|
|
5003
|
+
: isTimeout
|
|
5004
|
+
? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
|
|
5005
|
+
: getErrorMessage(error, undefined);
|
|
4405
5006
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
4406
5007
|
let sendOpts;
|
|
4407
5008
|
try {
|
|
@@ -4418,7 +5019,7 @@ export class ResponseEngine {
|
|
|
4418
5019
|
const errorPayload = {
|
|
4419
5020
|
kind: 'result.error',
|
|
4420
5021
|
text: userMessage,
|
|
4421
|
-
reason: retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
|
|
5022
|
+
reason: isExecutionFailure(error) ? error.reason : retryExhaustedCount ? 'retry_exhausted' : retryInputAlreadySubmitted ? 'input_already_submitted' : modelFallbackExhaustedMessage ? 'model_unavailable' : isTotalExecutionTimeout ? 'total_execution_timeout' : isTimeout ? 'timeout' : errType,
|
|
4422
5023
|
};
|
|
4423
5024
|
if (!isUserInterrupt && daemonTrigger && procStatus === 'error') {
|
|
4424
5025
|
// DaemonChannel settles a trigger on the first terminal payload. Put
|
|
@@ -4452,16 +5053,15 @@ export class ResponseEngine {
|
|
|
4452
5053
|
kind: 'activity.batch',
|
|
4453
5054
|
items: [{
|
|
4454
5055
|
kind: 'notice',
|
|
4455
|
-
|
|
5056
|
+
// activity.batch 载荷混合正常内容与错误,顶层出站包裹不整体打标;
|
|
5057
|
+
// 这里的 notice 是 proactive 下用户能看到的错误副本,单独打标。
|
|
5058
|
+
text: tagErrorTextWithEvolcoreVersion(userMessage),
|
|
4456
5059
|
severity: 'warn',
|
|
4457
5060
|
subtype: 'task-error',
|
|
4458
5061
|
}],
|
|
4459
5062
|
}).catch(() => { });
|
|
4460
5063
|
}
|
|
4461
5064
|
}
|
|
4462
|
-
if (handlingTimeout && timeoutControl) {
|
|
4463
|
-
timeoutControl.terminalDelivered = true;
|
|
4464
|
-
}
|
|
4465
5065
|
}
|
|
4466
5066
|
finally {
|
|
4467
5067
|
if (fullAccessAuthorization) {
|
|
@@ -4472,7 +5072,8 @@ export class ResponseEngine {
|
|
|
4472
5072
|
auditFullAccessEvent({
|
|
4473
5073
|
event: fullAccessAuthorization.source === 'trigger'
|
|
4474
5074
|
? 'fullaccess.trigger.execution.ended'
|
|
4475
|
-
:
|
|
5075
|
+
: fullAccessAuthorization.source === 'agent-policy'
|
|
5076
|
+
? 'fullaccess.agent-policy.execution.ended' : 'fullaccess.execution.ended',
|
|
4476
5077
|
source: fullAccessAuthorization.source,
|
|
4477
5078
|
actorId: fullAccessAuthorization.authorizedBy,
|
|
4478
5079
|
processRole: fullAccessAuditProcessRole,
|
|
@@ -4567,12 +5168,12 @@ export class ResponseEngine {
|
|
|
4567
5168
|
return undefined;
|
|
4568
5169
|
const shouldClose = !writerDb;
|
|
4569
5170
|
try {
|
|
4570
|
-
const row = db.prepare(`SELECT model, input_tokens, cache_creation_tokens, cache_read_tokens,
|
|
4571
|
-
context_tokens
|
|
4572
|
-
FROM model_calls
|
|
4573
|
-
WHERE session_id = ?
|
|
4574
|
-
AND agent_session_id = ?
|
|
4575
|
-
ORDER BY ts DESC, call_index DESC
|
|
5171
|
+
const row = db.prepare(`SELECT model, input_tokens, cache_creation_tokens, cache_read_tokens,
|
|
5172
|
+
context_tokens
|
|
5173
|
+
FROM model_calls
|
|
5174
|
+
WHERE session_id = ?
|
|
5175
|
+
AND agent_session_id = ?
|
|
5176
|
+
ORDER BY ts DESC, call_index DESC
|
|
4576
5177
|
LIMIT 1`).get(sessionId, agentSessionId);
|
|
4577
5178
|
if (!row)
|
|
4578
5179
|
return undefined;
|
|
@@ -4590,16 +5191,11 @@ export class ResponseEngine {
|
|
|
4590
5191
|
}
|
|
4591
5192
|
if (totalTokens <= 0)
|
|
4592
5193
|
return undefined;
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
// Historic rows may contain the old fixed 350k threshold. Never let a
|
|
4599
|
-
// persisted value exceed the context window that accompanied it.
|
|
4600
|
-
const autoCompactTokens = inferredAutoCompactTokens
|
|
4601
|
-
? Math.min(candidateAutoCompactTokens, inferredAutoCompactTokens)
|
|
4602
|
-
: candidateAutoCompactTokens;
|
|
5194
|
+
// EvolCore 的自动压缩阈值固定为 650k,与模型上下文窗口无关:
|
|
5195
|
+
// 大窗口模型(1M)到 650k 由 EvolCore 压缩;小窗口模型(200k/128k)
|
|
5196
|
+
// 到不了该阈值,交给 base agent 自身的压缩机制处理。
|
|
5197
|
+
// 历史行里可能存着旧的 350k 阈值,这里一律忽略,只认当前常量。
|
|
5198
|
+
const autoCompactTokens = autoCompactWindowForModel(model);
|
|
4603
5199
|
return { totalTokens, autoCompactTokens };
|
|
4604
5200
|
}
|
|
4605
5201
|
finally {
|
|
@@ -4675,7 +5271,7 @@ export class ResponseEngine {
|
|
|
4675
5271
|
logger.warn(`[ResponseEngine] Bound session ${boundSessionId} not found, falling back to latest`);
|
|
4676
5272
|
}
|
|
4677
5273
|
}
|
|
4678
|
-
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, message.topicName, message.peerId, message.chatType, resolvedBaseagent, message.selfAID, message.channelType, message.peerType, resolvedIdentity);
|
|
5274
|
+
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, message.topicName, message.peerId, message.chatType, resolvedBaseagent, message.selfAID, message.channelType, message.peerType, resolvedIdentity, message.lable);
|
|
4679
5275
|
this.ensureSessionBaseagent(session, resolvedBaseagent);
|
|
4680
5276
|
// 群名解析:群会话首次取群显示名(group.get),缓存到 metadata,供信封渲染。
|
|
4681
5277
|
// 渠道私有方法 getGroupName 自带进程缓存 + 容错;取不到不阻塞(groupName 保持空,模板回退 groupId)。
|
|
@@ -4733,7 +5329,7 @@ export class ResponseEngine {
|
|
|
4733
5329
|
*/
|
|
4734
5330
|
async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive,
|
|
4735
5331
|
/** [迁移点4/5] 响应模式插件 + 状态,用于调 onToolUse/onComplete 钩子 */
|
|
4736
|
-
modeHooks, causation, turnLease, permissionMode, proactiveSelfAid) {
|
|
5332
|
+
modeHooks, causation, turnLease, permissionMode, proactiveSelfAid, onAsyncQuestion) {
|
|
4737
5333
|
// Per-session agent name for stats bucketing
|
|
4738
5334
|
const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
|
|
4739
5335
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
|
|
@@ -4820,6 +5416,15 @@ export class ResponseEngine {
|
|
|
4820
5416
|
// It must never be projected as progress, activity, text, or transcript.
|
|
4821
5417
|
if (event.type === 'heartbeat')
|
|
4822
5418
|
continue;
|
|
5419
|
+
if (event.type === 'user_question') {
|
|
5420
|
+
if (!onAsyncQuestion)
|
|
5421
|
+
throw new Error('Async question has no delivery context');
|
|
5422
|
+
await onAsyncQuestion(event.request);
|
|
5423
|
+
madeProgress = true;
|
|
5424
|
+
// A structured question is user-visible progress without being a text reply.
|
|
5425
|
+
textCount++;
|
|
5426
|
+
continue;
|
|
5427
|
+
}
|
|
4823
5428
|
if (event.type === 'input_accepted') {
|
|
4824
5429
|
inputAccepted = !turnLease || event.inputId === turnLease.inputId;
|
|
4825
5430
|
logger.debug(`[ResponseEngine] Input accepted: session=${session.id} input=${event.inputId}`);
|
|
@@ -4915,7 +5520,7 @@ export class ResponseEngine {
|
|
|
4915
5520
|
|| (typeof input?.prompt === 'string' ? input.prompt.slice(0, 80) : '')
|
|
4916
5521
|
|| (typeof input?.query === 'string' ? input.query.slice(0, 80) : '')
|
|
4917
5522
|
|| '';
|
|
4918
|
-
eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
|
|
5523
|
+
eventDetail = ` tool=${event.name}${desc ? ` desc="${String(desc).replace(/\s+/g, ' ')}"` : ''}`;
|
|
4919
5524
|
}
|
|
4920
5525
|
else if (event.type === 'tool_result') {
|
|
4921
5526
|
const decision = event.decision ?? (event.isError ? 'error' : 'allow');
|
|
@@ -4935,6 +5540,10 @@ export class ResponseEngine {
|
|
|
4935
5540
|
}
|
|
4936
5541
|
// IMRenderer 旁路:proactive 模式逐事件投影为 thought(fire-and-forget)
|
|
4937
5542
|
renderer.emit(event);
|
|
5543
|
+
if (event.type === 'text_start') {
|
|
5544
|
+
await renderer.sendTextStart(event.contentIndex, event.turn);
|
|
5545
|
+
continue;
|
|
5546
|
+
}
|
|
4938
5547
|
// session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
|
|
4939
5548
|
if (event.type === 'session_id') {
|
|
4940
5549
|
logger.debug(`[ResponseEngine] Session ID updated: ${event.sessionId} for session: ${session.id}`);
|
|
@@ -5096,7 +5705,7 @@ export class ResponseEngine {
|
|
|
5096
5705
|
this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: visibleText, isFinal: false });
|
|
5097
5706
|
}
|
|
5098
5707
|
if (visibleText && !shouldSuppress()) {
|
|
5099
|
-
renderer.addText(visibleText, event.outputTokens, event.turn);
|
|
5708
|
+
await renderer.addText(visibleText, event.outputTokens, event.turn, event.assistantUuid);
|
|
5100
5709
|
hasProjectedCurrentReplyText = true;
|
|
5101
5710
|
}
|
|
5102
5711
|
}
|
|
@@ -5156,7 +5765,7 @@ export class ResponseEngine {
|
|
|
5156
5765
|
timestamp: Date.now(),
|
|
5157
5766
|
causation,
|
|
5158
5767
|
});
|
|
5159
|
-
const desc =
|
|
5768
|
+
const desc = summarizeToolInputForDisplay(event.name, event.input || {});
|
|
5160
5769
|
const pendingInput = { name: event.name, input: event.input || {} };
|
|
5161
5770
|
if (event.callId)
|
|
5162
5771
|
toolInputByCallId.set(event.callId, pendingInput);
|
|
@@ -5332,7 +5941,7 @@ export class ResponseEngine {
|
|
|
5332
5941
|
let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
|
|
5333
5942
|
// 移除 XML 风格的错误标签
|
|
5334
5943
|
errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
|
|
5335
|
-
renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, event.durationMs, cachedDesc);
|
|
5944
|
+
renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, event.durationMs, cachedDesc, event);
|
|
5336
5945
|
}
|
|
5337
5946
|
else if (!event.isError && !shouldSuppress()) {
|
|
5338
5947
|
renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, event.durationMs, cachedDesc);
|
|
@@ -5433,7 +6042,7 @@ export class ResponseEngine {
|
|
|
5433
6042
|
// User interrupt (AbortError) is expected, log at info level
|
|
5434
6043
|
const catchInterruptReason = this.interruptedSessions.get(session.id);
|
|
5435
6044
|
const catchIsUserInterrupt = isNonErrorInterruptReason(catchInterruptReason);
|
|
5436
|
-
if (catchInterruptReason === 'timeout') {
|
|
6045
|
+
if (catchInterruptReason === 'timeout' || catchInterruptReason === 'execution_error') {
|
|
5437
6046
|
logger.info('[ResponseEngine] Stream closed by timeout barrier');
|
|
5438
6047
|
throw this.timeoutErrors.get(session.id) ?? new Error('SDK_TIMEOUT');
|
|
5439
6048
|
}
|
|
@@ -5471,7 +6080,9 @@ export class ResponseEngine {
|
|
|
5471
6080
|
if (hasErrorSuppressingContent) {
|
|
5472
6081
|
let errorOutputFlushed = false;
|
|
5473
6082
|
try {
|
|
5474
|
-
|
|
6083
|
+
// The outer retry/terminal handler has not decided the outcome yet.
|
|
6084
|
+
// Pending stream output must not announce successful completion.
|
|
6085
|
+
await renderer.flush(false);
|
|
5475
6086
|
errorOutputFlushed = true;
|
|
5476
6087
|
}
|
|
5477
6088
|
catch (flushError) {
|