Files
openclaw/src/agents/embedded-agent-runner/run/attempt.ts
T
Peter Steinberger 77f1359612 refactor: extract media and ACP core packages (#88534)
* refactor: extract media and acp core packages

* refactor: remove relocated media and acp sources

* build: wire new core packages into dependency checks

* test: alias new core packages in vitest

* build: keep media sniffer runtime dependency

* docs: refresh plugin sdk api baseline

* fix: keep normalized proposal queries non-empty

* test: keep channel timer tests isolated

* fix: keep rebased plugin checks green

* fix: preserve sms numeric allowlist entries

* test: harden exec foreground timeout failure

* test: remove duplicate skill workshop assertion

* fix: remove channel config lint suppression

* test: refresh lint suppression allowlist
2026-05-31 11:30:33 +01:00

4930 lines
200 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import { MAX_IMAGE_BYTES } from "@openclaw/media-core/constants";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { isAcpRuntimeSpawnAvailable } from "../../../acp/runtime/availability.js";
import { buildHierarchyReinforcementMessage } from "../../../auto-reply/handoff-summarizer.js";
import { filterHeartbeatTranscriptArtifacts } from "../../../auto-reply/heartbeat-filter.js";
import { getRuntimeConfig } from "../../../config/config.js";
import { resolveStorePath } from "../../../config/sessions/paths.js";
import {
loadSessionStore,
runQuotaSuspensionMaintenance,
updateSessionStoreEntry,
} from "../../../config/sessions/store.js";
import {
bindOwnedSessionTranscriptWrites,
withOwnedSessionTranscriptWrites,
} from "../../../config/sessions/transcript-write-context.js";
import {
assertContextEngineHostSupport,
OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST,
} from "../../../context-engine/host-compat.js";
import { resolveContextEngineOwnerPluginId } from "../../../context-engine/registry.js";
import type { AssembleResult } from "../../../context-engine/types.js";
import { emitTrustedDiagnosticEvent } from "../../../infra/diagnostic-events.js";
import { resolveDiagnosticModelContentCapturePolicy } from "../../../infra/diagnostic-llm-content.js";
import {
createChildDiagnosticTraceContext,
createDiagnosticTraceContextFromActiveScope,
freezeDiagnosticTraceContext,
} from "../../../infra/diagnostic-trace-context.js";
import { isEmbeddedMode } from "../../../infra/embedded-mode.js";
import { formatErrorMessage } from "../../../infra/errors.js";
import { resolveHeartbeatSummaryForAgent } from "../../../infra/heartbeat-summary.js";
import { getMachineDisplayName } from "../../../infra/machine-name.js";
import { createCodexNativeWebSearchWrapper } from "../../../llm/providers/stream-wrappers/openai.js";
import type { AssistantMessage } from "../../../llm/types.js";
import { listRegisteredPluginAgentPromptGuidance } from "../../../plugins/command-registry-state.js";
import { getCurrentPluginMetadataSnapshot } from "../../../plugins/current-plugin-metadata-snapshot.js";
import { buildAgentHookContextChannelFields } from "../../../plugins/hook-agent-context.js";
import { resolveBlockMessage } from "../../../plugins/hook-decision-types.js";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import type { PluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.types.js";
import {
resolveProviderRuntimePluginHandle,
type ProviderRuntimePluginHandle,
} from "../../../plugins/provider-hook-runtime.js";
import {
extractModelCompat,
resolveToolCallArgumentsEncoding,
} from "../../../plugins/provider-model-compat.js";
import {
resolveProviderSystemPromptContribution,
resolveProviderTextTransforms,
transformProviderSystemPrompt,
} from "../../../plugins/provider-runtime.js";
import { getPluginToolMeta } from "../../../plugins/tools.js";
import { isSubagentSessionKey } from "../../../routing/session-key.js";
import { annotateInterSessionPromptText } from "../../../sessions/input-provenance.js";
import { resolveSkillsPromptForRun } from "../../../skills/loading/workspace.js";
import { resolveEmbeddedRunSkillEntries } from "../../../skills/runtime/embedded-run-entries.js";
import {
applySkillEnvOverrides,
applySkillEnvOverridesFromSnapshot,
} from "../../../skills/runtime/env-overrides.js";
import {
buildTrajectoryArtifacts,
buildTrajectoryRunMetadata,
} from "../../../trajectory/metadata.js";
import {
createTrajectoryRuntimeRecorder,
toTrajectoryToolDefinitions,
} from "../../../trajectory/runtime.js";
import { resolveUserPath } from "../../../utils.js";
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
import { createBundleLspToolRuntime } from "../../agent-bundle-lsp-runtime.js";
import {
getOrCreateSessionMcpRuntime,
materializeBundleMcpToolsForRun,
} from "../../agent-bundle-mcp-tools.js";
import { createPreparedEmbeddedAgentSettingsManager } from "../../agent-project-settings.js";
import { resolveAgentDir, resolveSessionAgentIds } from "../../agent-scope.js";
import {
applyAgentAutoCompactionGuard,
applyAgentCompactionSettingsFromConfig,
isSilentOverflowProneModel,
resolveEffectiveCompactionMode,
} from "../../agent-settings.js";
import {
createClientToolNameConflictError,
findClientToolNameConflicts,
toClientToolDefinitions,
} from "../../agent-tool-definition-adapter.js";
import {
createOpenClawCodingTools,
resolveProcessToolScopeKey,
resolveToolLoopDetectionConfig,
} from "../../agent-tools.js";
import {
resolveEffectiveToolPolicy,
resolveGroupToolPolicy,
resolveInheritedToolPolicyForSession,
resolveSubagentToolPolicyForSession,
} from "../../agent-tools.policy.js";
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
import { listActiveProcessSessionReferences } from "../../bash-process-references.js";
import {
analyzeBootstrapBudget,
buildBootstrapPromptWarning,
buildBootstrapPromptWarningNotice,
buildBootstrapTruncationReportMeta,
buildBootstrapInjectionStats,
} from "../../bootstrap-budget.js";
import {
FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE,
buildBootstrapContextForFiles,
hasCompletedBootstrapTurn,
isWorkspaceBootstrapPending,
makeBootstrapWarn,
resolveBootstrapFilesForRun,
resolveContextInjectionMode,
} from "../../bootstrap-files.js";
import { createCacheTrace } from "../../cache-trace.js";
import {
listChannelSupportedActions,
resolveChannelMessageToolHints,
resolveChannelReactionGuidance,
} from "../../channel-tools.js";
import {
addClientToolsToCodeModeCatalog,
applyCodeModeCatalog,
CODE_MODE_EXEC_TOOL_NAME,
CODE_MODE_WAIT_TOOL_NAME,
createCodeModeTools,
resolveCodeModeConfig,
} from "../../code-mode.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../defaults.js";
import { resolveOpenClawReferencePaths } from "../../docs-path.js";
import {
isCloudCodeAssistFormatError,
resolveBootstrapMaxChars,
resolveBootstrapPromptTruncationWarningMode,
resolveBootstrapTotalMaxChars,
} from "../../embedded-agent-helpers.js";
import { countActiveToolExecutions } from "../../embedded-agent-subscribe.handlers.tools.js";
import { subscribeEmbeddedAgentSession } from "../../embedded-agent-subscribe.js";
import { isTimeoutError } from "../../failover-error.js";
import { runAgentEndSideEffects } from "../../harness/agent-end-side-effects.js";
import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js";
import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
import { filterLocalModelLeanTools, isLocalModelLeanEnabled } from "../../local-model-lean.js";
import { resolveModelAuthMode } from "../../model-auth.js";
import { resolveDefaultModelForAgent } from "../../model-selection.js";
import { supportsModelTools } from "../../model-tool-support.js";
import { wrapStreamFnTextTransforms } from "../../plugin-text-transforms.js";
import { resolveAgentPromptSurfaceForSessionKey } from "../../prompt-surface.js";
import { describeProviderRequestRoutingSummary } from "../../provider-attribution.js";
import { registerProviderStreamForModel } from "../../provider-stream.js";
import { runAgentCleanupStep } from "../../run-cleanup-timeout.js";
import { collectRuntimeChannelCapabilities } from "../../runtime-capabilities.js";
import {
logAgentRuntimeToolDiagnostics,
normalizeAgentRuntimeTools,
} from "../../runtime-plan/tools.js";
import type { AgentMessage } from "../../runtime/index.js";
import { resolveSandboxContext } from "../../sandbox.js";
import { resolveSandboxRuntimeStatus } from "../../sandbox/runtime-status.js";
import { repairSessionFileIfNeeded } from "../../session-file-repair.js";
import { guardSessionManager } from "../../session-tool-result-guard-wrapper.js";
import { sanitizeToolUseResultPairing } from "../../session-transcript-repair.js";
import { acquireSessionWriteLock } from "../../session-write-lock.js";
import { createAgentSession, SessionManager } from "../../sessions/index.js";
import { detectRuntimeShell } from "../../shell-utils.js";
import { buildActiveSubagentSystemPromptAddition } from "../../subagent-active-context.js";
import {
isSubagentEnvelopeSession,
resolveSubagentCapabilityStore,
} from "../../subagent-capabilities.js";
import { ensureSystemPromptCacheBoundary } from "../../system-prompt-cache-boundary.js";
import { buildSystemPromptParams } from "../../system-prompt-params.js";
import { buildSystemPromptReport } from "../../system-prompt-report.js";
import {
appendModelIdentitySystemPrompt,
buildModelIdentityPromptLine,
} from "../../system-prompt.js";
import { resolveAgentTimeoutMs } from "../../timeout.js";
import {
buildEmptyExplicitToolAllowlistError,
collectExplicitToolAllowlistSources,
} from "../../tool-allowlist-guard.js";
import { filterRuntimeCompatibleTools } from "../../tool-schema-projection.js";
import { logRuntimeToolSchemaQuarantine } from "../../tool-schema-quarantine.js";
import {
addClientToolsToToolSearchCatalog,
applyToolSearchCatalog,
clearToolSearchCatalog,
createToolSearchCatalogRef,
projectToolSearchTargetTranscriptMessages,
resolveToolSearchConfig,
type ToolSearchCatalogRef,
type ToolSearchCatalogToolExecutor,
type ToolSearchTargetTranscriptProjection,
} from "../../tool-search.js";
import { shouldAllowProviderOwnedThinkingReplay } from "../../transcript-policy.js";
import { normalizeUsage, type NormalizedUsage } from "../../usage.js";
import { DEFAULT_BOOTSTRAP_FILENAME, type WorkspaceBootstrapFile } from "../../workspace.js";
import { isRunnerAbortError } from "../abort.js";
import { isCacheTtlEligibleProvider, readLastCacheTtlTimestamp } from "../cache-ttl.js";
import { resolveCompactionTimeoutMs } from "../compaction-safety-timeout.js";
import { runContextEngineMaintenance } from "../context-engine-maintenance.js";
import { applyFinalEffectiveToolPolicy } from "../effective-tool-policy.js";
import { buildEmbeddedExtensionFactories } from "../extensions.js";
import {
applyExtraParamsToAgent,
resolveAgentTransportOverride,
resolveExplicitSettingsTransport,
resolveExtraParams,
resolvePreparedExtraParams,
} from "../extra-params.js";
import { prepareGooglePromptCacheStreamFn } from "../google-prompt-cache.js";
import { getHistoryLimitFromSessionKey, limitHistoryTurns } from "../history.js";
import { log } from "../logger.js";
import { buildEmbeddedMessageActionDiscoveryInput } from "../message-action-discovery-input.js";
import {
collectPromptCacheToolNames,
beginPromptCacheObservation,
completePromptCacheObservation,
type PromptCacheBreak,
type PromptCacheChange,
} from "../prompt-cache-observability.js";
import { resolveCacheRetention } from "../prompt-cache-retention.js";
import {
normalizeAssistantReplayContent,
sanitizeSessionHistory,
validateReplayTurns,
} from "../replay-history.js";
import { observeReplayMetadata, replayMetadataFromState } from "../replay-state.js";
import { createEmbeddedAgentResourceLoader } from "../resource-loader.js";
import {
clearActiveEmbeddedRun,
type EmbeddedAgentQueueHandle,
markActiveEmbeddedRunAbandoned,
setActiveEmbeddedRun,
updateActiveEmbeddedRunSessionFile,
updateActiveEmbeddedRunSnapshot,
} from "../runs.js";
import { buildEmbeddedSandboxInfo, resolveEmbeddedSandboxInfoExecPolicy } from "../sandbox-info.js";
import { prewarmSessionFile, trackSessionManagerAccess } from "../session-manager-cache.js";
import { prepareSessionManagerForRun } from "../session-manager-init.js";
import {
describeEmbeddedAgentStreamStrategy,
resetEmbeddedAgentBaseStreamFnCacheForTest,
resolveEmbeddedAgentApiKey,
resolveEmbeddedAgentBaseStreamFn,
resolveEmbeddedAgentStreamFn,
} from "../stream-resolution.js";
import { applySystemPromptToSession } from "../system-prompt.js";
import {
dropReasoningFromHistory,
dropThinkingBlocks,
wrapAnthropicStreamWithRecovery,
} from "../thinking.js";
import {
collectCoreBuiltinToolNames,
collectRegisteredToolNames,
AGENT_RESERVED_TOOL_NAMES,
toSessionToolAllowlist,
} from "../tool-name-allowlist.js";
import {
installContextEngineLoopHook,
installToolResultContextGuard,
} from "../tool-result-context-guard.js";
import {
resolveLiveToolResultMaxChars,
truncateOversizedToolResultsInMessages,
truncateOversizedToolResultsInSessionManager,
} from "../tool-result-truncation.js";
import { splitSdkTools } from "../tool-split.js";
import { mapThinkingLevel } from "../utils.js";
import { flushPendingToolResultsAfterIdle } from "../wait-for-idle-before-flush.js";
import { abortable as abortableWithSignal } from "./abortable.js";
import { createEmbeddedAgentSessionWithResourceLoader } from "./attempt-session.js";
import {
applyEmbeddedAttemptToolsAllow,
mergeForcedEmbeddedAttemptToolsAllow,
resolveEmbeddedAttemptToolConstructionPlan,
shouldCreateBundleLspRuntimeForAttempt,
shouldCreateBundleMcpRuntimeForAttempt,
} from "./attempt-tool-construction-plan.js";
import {
resolveAttemptTrajectoryTerminal,
resolveTerminalAssistantTexts,
} from "./attempt-trajectory-status.js";
import {
isPrimaryBootstrapRun,
remapInjectedContextFilesToWorkspace,
} from "./attempt.bootstrap-context.js";
export { buildContextEnginePromptCacheInfo } from "./attempt.context-engine-helpers.js";
import {
rotateTranscriptAfterCompaction,
shouldRotateCompactionTranscript,
} from "../compaction-successor-transcript.js";
import { resolveAttemptWorkspaceBootstrapRouting } from "./attempt-bootstrap-routing.js";
import { configureEmbeddedAttemptHttpRuntime } from "./attempt-http-runtime.js";
import {
createEmbeddedRunStageTracker,
formatEmbeddedRunStageSummary,
shouldWarnEmbeddedRunStageSummary,
} from "./attempt-stage-timing.js";
import { buildAttemptSystemPrompt } from "./attempt-system-prompt.js";
import {
assembleAttemptContextEngine,
buildLoopPromptCacheInfo,
buildContextEnginePromptCacheInfo,
findCurrentAttemptAssistantMessage,
finalizeAttemptContextEngineTurn,
resolvePromptCacheTouchTimestamp,
resolveAttemptBootstrapContext,
runAttemptContextEngineBootstrap,
} from "./attempt.context-engine-helpers.js";
import {
installModelPromptTransform,
installRuntimeContextMessageForPrompt,
normalizeMessagesForCurrentPromptBoundary,
normalizeMessagesForLlmBoundary,
} from "./attempt.llm-boundary.js";
import {
diagnosticErrorCategory,
wrapStreamFnWithDiagnosticModelCallEvents,
} from "./attempt.model-diagnostic-events.js";
import {
buildAfterTurnRuntimeContext,
buildAfterTurnRuntimeContextFromUsage,
prependSystemPromptAddition,
resolveAttemptFsWorkspaceOnly,
resolveAttemptMediaTaskSystemPromptAddition,
resolvePromptBuildHookResult,
resolvePromptModeForSession,
resolvePromptSubmissionSkipReason,
shouldWarnOnOrphanedUserRepair,
shouldInjectHeartbeatPrompt,
} from "./attempt.prompt-helpers.js";
import { steerActiveSessionWithOptionalDeliveryWait } from "./attempt.queue-message.js";
import {
resolveAttemptStreamAuthProfileId,
resolveAttemptToolPolicyMessageProvider,
resolveEmbeddedAttemptSessionWriteLockOptions,
resolveUnknownToolGuardThreshold,
shouldRunLlmOutputHooksForAttempt,
} from "./attempt.run-decisions.js";
import {
acquireEmbeddedAttemptSessionFileOwner,
EmbeddedAttemptSessionTakeoverError,
type EmbeddedAttemptSessionFileOwner,
createEmbeddedAttemptSessionLockController,
installPromptSubmissionLockRelease,
} from "./attempt.session-lock.js";
import {
createYieldAbortedResponse,
persistSessionsYieldContextMessage,
queueSessionsYieldInterruptMessage,
stripSessionsYieldArtifacts,
waitForSessionsYieldAbortSettle,
} from "./attempt.sessions-yield.js";
import { wrapStreamFnHandleSensitiveStopReason } from "./attempt.stop-reason-recovery.js";
import {
buildEmbeddedSubscriptionParams,
cleanupEmbeddedAttemptResources,
} from "./attempt.subscription-cleanup.js";
import {
appendAttemptCacheTtlIfNeeded,
composeSystemPromptWithHookContext,
resolveAttemptSpawnWorkspaceDir,
shouldPersistCompletedBootstrapTurn,
} from "./attempt.thread-helpers.js";
import {
shouldRepairMalformedToolCallArguments,
wrapStreamFnDecodeXaiToolCallArguments,
wrapStreamFnRepairMalformedToolCallArguments,
} from "./attempt.tool-call-argument-repair.js";
import {
sanitizeOpenAIResponsesReplayForStream,
sanitizeReplayToolCallIdsForStream,
shouldApplyReplayToolCallIdSanitizer,
wrapStreamFnPromoteStandaloneTextToolCalls,
wrapStreamFnSanitizeMalformedToolCalls,
wrapStreamFnTrimToolCallNames,
} from "./attempt.tool-call-normalization.js";
import { buildEmbeddedAttemptToolRunContext } from "./attempt.tool-run-context.js";
import {
buildToolSearchRunPlan,
TOOL_SEARCH_CONTROL_ALLOWLIST_NAMES,
} from "./attempt.tool-search-run-plan.js";
import { resolveAttemptTranscriptPolicy } from "./attempt.transcript-policy.js";
import { waitForCompactionRetryWithAggregateTimeout } from "./compaction-retry-aggregate-timeout.js";
import {
resolveRunTimeoutDuringCompaction,
selectCompactionTimeoutSnapshot,
shouldFlagCompactionTimeout,
} from "./compaction-timeout.js";
import { resolveFinalAssistantVisibleText } from "./helpers.js";
import {
installHistoryImagePruneContextTransform,
pruneProcessedHistoryImages,
} from "./history-image-prune.js";
import { detectAndLoadPromptImages } from "./images.js";
import {
buildAttemptReplayMetadata,
resolveSilentToolResultReplyPayload,
shouldTreatEmptyAssistantReplyAsSilent,
} from "./incomplete-turn.js";
import { resolveLlmIdleTimeoutMs, streamWithIdleTimeout } from "./llm-idle-timeout.js";
import { resolveMessageMergeStrategy } from "./message-merge-strategy.js";
import { installMessageToolOnlyTerminalHook } from "./message-tool-terminal.js";
import { wrapStreamFnWithMessageTransform } from "./message-transform-stream-wrapper.js";
import {
MID_TURN_PRECHECK_ERROR_MESSAGE,
isMidTurnPrecheckSignal,
type MidTurnPrecheckRequest,
} from "./midturn-precheck.js";
import {
PREEMPTIVE_OVERFLOW_ERROR_TEXT,
buildPrePromptContextBudgetStatus,
formatPrePromptPrecheckLog,
shouldPreemptivelyCompactBeforePrompt,
} from "./preemptive-compaction.js";
import {
buildCurrentInboundPrompt,
buildRuntimeContextCustomMessage,
resolveRuntimeContextPromptParts,
} from "./runtime-context-prompt.js";
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
export {
appendAttemptCacheTtlIfNeeded,
composeSystemPromptWithHookContext,
resolveAttemptSpawnWorkspaceDir,
} from "./attempt.thread-helpers.js";
export {
buildAfterTurnRuntimeContext,
buildAfterTurnRuntimeContextFromUsage,
mergeOrphanedTrailingUserPrompt,
prependSystemPromptAddition,
resolveAttemptFsWorkspaceOnly,
resolveAttemptMediaTaskSystemPromptAddition,
resolvePromptBuildHookResult,
resolvePromptModeForSession,
shouldWarnOnOrphanedUserRepair,
shouldInjectHeartbeatPrompt,
} from "./attempt.prompt-helpers.js";
export {
persistSessionsYieldContextMessage,
queueSessionsYieldInterruptMessage,
stripSessionsYieldArtifacts,
} from "./attempt.sessions-yield.js";
export {
decodeHtmlEntitiesInObject,
wrapStreamFnRepairMalformedToolCallArguments,
} from "./attempt.tool-call-argument-repair.js";
export {
wrapStreamFnPromoteStandaloneTextToolCalls,
wrapStreamFnSanitizeMalformedToolCalls,
wrapStreamFnTrimToolCallNames,
} from "./attempt.tool-call-normalization.js";
export {
resetEmbeddedAgentBaseStreamFnCacheForTest,
resolveEmbeddedAgentBaseStreamFn,
resolveEmbeddedAgentStreamFn,
};
const MAX_BTW_SNAPSHOT_MESSAGES = 100;
const PROMPT_TOOL_RESULT_AGGREGATE_CAP_MULTIPLIER = 4;
function summarizeMessagePayload(msg: AgentMessage): { textChars: number; imageBlocks: number } {
const content = (msg as { content?: unknown }).content;
if (typeof content === "string") {
return { textChars: content.length, imageBlocks: 0 };
}
if (!Array.isArray(content)) {
return { textChars: 0, imageBlocks: 0 };
}
let textChars = 0;
let imageBlocks = 0;
for (const block of content) {
if (!block || typeof block !== "object") {
continue;
}
const typedBlock = block as { type?: unknown; text?: unknown };
if (typedBlock.type === "image") {
imageBlocks++;
continue;
}
if (typeof typedBlock.text === "string") {
textChars += typedBlock.text.length;
}
}
return { textChars, imageBlocks };
}
function summarizeSessionContext(messages: AgentMessage[]): {
roleCounts: string;
totalTextChars: number;
totalImageBlocks: number;
maxMessageTextChars: number;
} {
const roleCounts = new Map<string, number>();
let totalTextChars = 0;
let totalImageBlocks = 0;
let maxMessageTextChars = 0;
for (const msg of messages) {
const role = typeof msg.role === "string" ? msg.role : "unknown";
roleCounts.set(role, (roleCounts.get(role) ?? 0) + 1);
const payload = summarizeMessagePayload(msg);
totalTextChars += payload.textChars;
totalImageBlocks += payload.imageBlocks;
if (payload.textChars > maxMessageTextChars) {
maxMessageTextChars = payload.textChars;
}
}
return {
roleCounts:
[...roleCounts.entries()]
.toSorted((a, b) => a[0].localeCompare(b[0]))
.map(([role, count]) => `${role}:${count}`)
.join(",") || "none",
totalTextChars,
totalImageBlocks,
maxMessageTextChars,
};
}
function cloneHookMessages(messages: AgentMessage[]): AgentMessage[] {
return messages.map((message) => structuredClone(message));
}
function sessionMessagesContainIdempotencyKey(
messages: AgentMessage[],
idempotencyKey: string,
): boolean {
return messages.some(
(message) =>
typeof (message as { idempotencyKey?: unknown }).idempotencyKey === "string" &&
(message as { idempotencyKey?: unknown }).idempotencyKey === idempotencyKey,
);
}
function flushSessionManagerFile(sessionManager: ReturnType<typeof guardSessionManager>): void {
(sessionManager as unknown as { rewriteFile?: () => void }).rewriteFile?.();
}
function shouldPreservePromptErrorAfterCleanupError(params: {
promptError: unknown;
cleanupError: unknown;
}): boolean {
return (
Boolean(params.promptError) &&
params.cleanupError instanceof EmbeddedAttemptSessionTakeoverError
);
}
class EmbeddedAttemptPromptErrorWithCleanupTakeoverError extends Error {
readonly promptError: unknown;
readonly cleanupError: EmbeddedAttemptSessionTakeoverError;
constructor(params: { promptError: unknown; cleanupError: EmbeddedAttemptSessionTakeoverError }) {
super(formatErrorMessage(params.promptError), { cause: params.cleanupError });
this.name = "EmbeddedAttemptSessionTakeoverError";
this.promptError = params.promptError;
this.cleanupError = params.cleanupError;
}
}
function hasVisiblePendingToolMediaReply(
reply: { mediaUrls?: string[]; audioAsVoice?: boolean } | null | undefined,
): boolean {
return Boolean(
reply &&
((reply.mediaUrls ?? []).some((url) => url.trim().length > 0) || reply.audioAsVoice === true),
);
}
function isMidTurnPrecheckAssistantError(message: AgentMessage | undefined): boolean {
if (!message || message.role !== "assistant") {
return false;
}
const record = message as unknown as { stopReason?: unknown; errorMessage?: unknown };
return record.stopReason === "error" && record.errorMessage === MID_TURN_PRECHECK_ERROR_MESSAGE;
}
function removeTrailingMidTurnPrecheckAssistantError(params: {
activeSession: { agent: { state: { messages: AgentMessage[] } } };
sessionManager: ReturnType<typeof guardSessionManager>;
}): void {
const messages = params.activeSession.agent.state.messages;
if (isMidTurnPrecheckAssistantError(messages.at(-1))) {
params.activeSession.agent.state.messages = messages.slice(0, -1);
}
const mutableSessionManager = params.sessionManager as unknown as {
fileEntries?: Array<{
type?: string;
id?: string;
parentId?: string | null;
message?: AgentMessage;
}>;
byId?: Map<string, unknown>;
leafId?: string | null;
rewriteFile?: () => void;
};
const lastEntry = mutableSessionManager.fileEntries?.at(-1);
if (lastEntry?.type !== "message" || !isMidTurnPrecheckAssistantError(lastEntry.message)) {
if (isMidTurnPrecheckAssistantError(params.activeSession.agent.state.messages.at(-1))) {
log.warn(
"[context-overflow-midturn-precheck] removed synthetic assistant error from active session but could not locate matching persisted SessionManager entry",
);
}
return;
}
if (typeof mutableSessionManager.rewriteFile !== "function") {
log.warn(
"[context-overflow-midturn-precheck] removed synthetic assistant error from active session but SessionManager rewrite hook is unavailable",
);
return;
}
mutableSessionManager.fileEntries?.pop();
if (lastEntry.id) {
mutableSessionManager.byId?.delete(lastEntry.id);
}
mutableSessionManager.leafId = lastEntry.parentId ?? null;
mutableSessionManager.rewriteFile();
}
function collectAttemptExplicitToolAllowlistSources(params: {
config?: EmbeddedRunAttemptParams["config"];
sessionKey?: string;
sandboxSessionKey?: string;
agentId?: string;
modelProvider?: string;
modelId?: string;
messageProvider?: string;
agentAccountId?: string | null;
groupId?: string | null;
groupChannel?: string | null;
groupSpace?: string | null;
spawnedBy?: string | null;
senderId?: string | null;
senderName?: string | null;
senderUsername?: string | null;
senderE164?: string | null;
sandboxToolPolicy?: { allow?: string[]; deny?: string[] };
toolsAllow?: string[];
}) {
const { agentId, globalPolicy, globalProviderPolicy, agentPolicy, agentProviderPolicy } =
resolveEffectiveToolPolicy({
config: params.config,
sessionKey: params.sessionKey,
agentId: params.agentId,
modelProvider: params.modelProvider,
modelId: params.modelId,
});
const groupPolicy = resolveGroupToolPolicy({
config: params.config,
sessionKey: params.sessionKey,
spawnedBy: params.spawnedBy,
messageProvider: params.messageProvider,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
accountId: params.agentAccountId,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
});
const subagentStore = resolveSubagentCapabilityStore(params.sandboxSessionKey, {
cfg: params.config,
});
const subagentPolicy =
params.sandboxSessionKey &&
isSubagentEnvelopeSession(params.sandboxSessionKey, {
cfg: params.config,
store: subagentStore,
})
? resolveSubagentToolPolicyForSession(params.config, params.sandboxSessionKey, {
store: subagentStore,
})
: undefined;
const inheritedToolPolicy = resolveInheritedToolPolicyForSession(
params.config,
params.sandboxSessionKey,
{
store: subagentStore,
},
);
return collectExplicitToolAllowlistSources([
{ label: "tools.allow", allow: globalPolicy?.allow },
{ label: "tools.byProvider.allow", allow: globalProviderPolicy?.allow },
{
label: agentId ? `agents.${agentId}.tools.allow` : "agent tools.allow",
allow: agentPolicy?.allow,
},
{
label: agentId ? `agents.${agentId}.tools.byProvider.allow` : "agent tools.byProvider.allow",
allow: agentProviderPolicy?.allow,
},
{ label: "group tools.allow", allow: groupPolicy?.allow },
{ label: "sandbox tools.allow", allow: params.sandboxToolPolicy?.allow },
{ label: "subagent tools.allow", allow: subagentPolicy?.allow },
{ label: "inherited tools.allow", allow: inheritedToolPolicy?.allow },
{ label: "runtime toolsAllow", allow: params.toolsAllow, enforceWhenToolsDisabled: true },
]);
}
export async function runEmbeddedAttempt(
params: EmbeddedRunAttemptParams,
): Promise<EmbeddedRunAttemptResult> {
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
const runAbortController = new AbortController();
configureEmbeddedAttemptHttpRuntime({ timeoutMs: params.timeoutMs });
log.debug(
`embedded run start: runId=${params.runId} sessionId=${params.sessionId} provider=${params.provider} model=${params.modelId} thinking=${params.thinkLevel} messageChannel=${params.messageChannel ?? params.messageProvider ?? "unknown"}`,
);
const prepStages = createEmbeddedRunStageTracker();
const emitPrepStageSummary = (phase: string) => {
const summary = prepStages.snapshot();
const shouldWarn = shouldWarnEmbeddedRunStageSummary(summary);
if (!shouldWarn && !log.isEnabled("trace")) {
return;
}
const message = formatEmbeddedRunStageSummary(
`[trace:embedded-run] prep stages: runId=${params.runId} sessionId=${params.sessionId} phase=${phase}`,
summary,
);
if (shouldWarn) {
log.warn(message);
} else {
log.trace(message);
}
};
const emitCorePluginToolStageSummary = (
phase: string,
summary: ReturnType<typeof prepStages.snapshot>,
) => {
if (summary.stages.length === 0) {
return;
}
const shouldWarn = shouldWarnEmbeddedRunStageSummary(summary, {
totalThresholdMs: 5_000,
stageThresholdMs: 2_000,
});
if (!shouldWarn && !log.isEnabled("trace")) {
return;
}
const message = formatEmbeddedRunStageSummary(
`[trace:embedded-run] core-plugin-tool stages: runId=${params.runId} sessionId=${params.sessionId} phase=${phase}`,
summary,
);
if (shouldWarn) {
log.warn(message);
} else {
log.trace(message);
}
};
await fs.mkdir(resolvedWorkspace, { recursive: true });
const sandboxSessionKey =
params.sandboxSessionKey?.trim() || params.sessionKey?.trim() || params.sessionId;
const sandbox = await resolveSandboxContext({
config: params.config,
sessionKey: sandboxSessionKey,
workspaceDir: resolvedWorkspace,
});
const effectiveWorkspace = sandbox?.enabled
? sandbox.workspaceAccess === "rw"
? resolvedWorkspace
: sandbox.workspaceDir
: resolvedWorkspace;
const requestedCwd = params.cwd ? resolveUserPath(params.cwd) : undefined;
if (sandbox?.enabled && requestedCwd && requestedCwd !== resolvedWorkspace) {
throw new Error(
"cwd override is not supported for sandboxed embedded agent runs; omit cwd or use the agent workspace as cwd",
);
}
const effectiveCwd = sandbox?.enabled ? effectiveWorkspace : (requestedCwd ?? effectiveWorkspace);
await fs.mkdir(effectiveWorkspace, { recursive: true });
let currentPluginMetadataSnapshotResolved = false;
let currentPluginMetadataSnapshot: PluginMetadataSnapshot | undefined;
const getCurrentAttemptPluginMetadataSnapshot = () => {
if (!currentPluginMetadataSnapshotResolved) {
currentPluginMetadataSnapshot = getCurrentPluginMetadataSnapshot({
config: params.config,
env: process.env,
workspaceDir: effectiveWorkspace,
});
currentPluginMetadataSnapshotResolved = true;
}
return currentPluginMetadataSnapshot;
};
let providerRuntimeHandle: ProviderRuntimePluginHandle | undefined;
const getProviderRuntimeHandle = () => {
if (providerRuntimeHandle?.plugin) {
return providerRuntimeHandle;
}
const resolvedHandle = resolveProviderRuntimePluginHandle({
provider: params.provider,
modelId: params.modelId,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
});
if (resolvedHandle.plugin) {
providerRuntimeHandle = resolvedHandle;
}
return resolvedHandle;
};
const { sessionAgentId } = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
const effectiveFsWorkspaceOnly = resolveAttemptFsWorkspaceOnly({
config: params.config,
sessionAgentId,
});
prepStages.mark("workspace-sandbox");
let restoreSkillEnv: (() => void) | undefined;
let aborted = Boolean(params.abortSignal?.aborted);
let externalAbort = false;
let timedOut = false;
let idleTimedOut = false;
let timedOutDuringCompaction = false;
let timedOutDuringToolExecution = false;
let promptError: unknown = null;
let emitDiagnosticRunCompleted:
| ((
outcome: "completed" | "aborted" | "blocked" | "error",
err?: unknown,
extra?: { blockedBy?: string },
) => void)
| undefined;
let beforeAgentRunBlocked = false;
let beforeAgentRunBlockedBy: string | undefined;
// Releases the eager session lock if post-prompt code exits before cleanup.
let releaseRetainedSessionLock: (() => Promise<void>) | undefined;
let retainedSessionFileOwner: EmbeddedAttemptSessionFileOwner | undefined;
let bundleMcpRuntime: Awaited<ReturnType<typeof materializeBundleMcpToolsForRun>> | undefined;
let bundleLspRuntime: Awaited<ReturnType<typeof createBundleLspToolRuntime>> | undefined;
let toolSearchCatalogRef: ToolSearchCatalogRef | undefined;
let toolSearchCatalogApplied = false;
const sessionCleanupOwnsEmbeddedResources = false;
let abortActiveSessionForExternalSignal: (() => Promise<void>) | undefined;
let abortRunForExternalSignal: ((isTimeout?: boolean, reason?: unknown) => void) | undefined;
let isCompactionPendingForExternalSignal: (() => boolean) | undefined;
let isCompactionInFlightForExternalSignal: (() => boolean) | undefined;
let removeExternalAbortSignalListener: (() => void) | undefined;
const createAttemptAbortError = (signal: AbortSignal): Error => {
if (signal.reason instanceof Error) {
return signal.reason;
}
const err = new Error("request aborted", { cause: signal.reason });
err.name = "AbortError";
return err;
};
const getAbortReason = (signal: AbortSignal): unknown =>
"reason" in signal ? (signal as { reason?: unknown }).reason : undefined;
const makeTimeoutAbortReason = (): Error => {
const err = new Error("request timed out");
err.name = "TimeoutError";
return err;
};
const cleanupEmbeddedPrepResourcesAfterEarlyExit = async () => {
if (toolSearchCatalogApplied) {
clearToolSearchCatalog({
sessionId: params.sessionId,
sessionKey: sandboxSessionKey,
agentId: sessionAgentId,
runId: params.runId,
catalogRef: toolSearchCatalogRef,
});
toolSearchCatalogApplied = false;
}
try {
await bundleMcpRuntime?.dispose();
} catch {
/* best-effort */
} finally {
bundleMcpRuntime = undefined;
}
try {
await bundleLspRuntime?.dispose();
} catch {
/* best-effort */
} finally {
bundleLspRuntime = undefined;
}
};
const onExternalAbortSignal = () => {
const signal = params.abortSignal;
if (!signal) {
return;
}
externalAbort = true;
const reason = getAbortReason(signal);
const timeout = reason ? isTimeoutError(reason) : false;
if (
shouldFlagCompactionTimeout({
isTimeout: timeout,
isCompactionPendingOrRetrying: isCompactionPendingForExternalSignal?.() ?? false,
isCompactionInFlight: isCompactionInFlightForExternalSignal?.() ?? false,
})
) {
timedOutDuringCompaction = true;
}
if (abortRunForExternalSignal) {
abortRunForExternalSignal(timeout, reason);
return;
}
aborted = true;
if (timeout) {
timedOut = true;
if (!timedOutDuringCompaction && countActiveToolExecutions(params.runId) > 0) {
timedOutDuringToolExecution = true;
}
}
promptError = createAttemptAbortError(signal);
if (!runAbortController.signal.aborted) {
runAbortController.abort(timeout ? (reason ?? makeTimeoutAbortReason()) : reason);
}
void abortActiveSessionForExternalSignal?.();
};
const armExternalAbortSignal = () => {
const signal = params.abortSignal;
if (!signal || removeExternalAbortSignalListener) {
return;
}
if (signal.aborted) {
onExternalAbortSignal();
return;
}
signal.addEventListener("abort", onExternalAbortSignal, { once: true });
removeExternalAbortSignalListener = () => {
signal.removeEventListener("abort", onExternalAbortSignal);
removeExternalAbortSignalListener = undefined;
};
};
const throwIfAttemptAbortSignalFiredAfterPrepCleanup = async () => {
if (params.abortSignal?.aborted === true) {
const abortError = createAttemptAbortError(params.abortSignal);
aborted = true;
externalAbort = true;
promptError = abortError;
await cleanupEmbeddedPrepResourcesAfterEarlyExit();
throw abortError;
}
};
try {
const skillsSnapshotForRun =
sandbox?.enabled && sandbox.workspaceAccess !== "rw" ? undefined : params.skillsSnapshot;
const { shouldLoadSkillEntries, skillEntries } = resolveEmbeddedRunSkillEntries({
workspaceDir: effectiveWorkspace,
config: params.config,
agentId: sessionAgentId,
skillsSnapshot: skillsSnapshotForRun,
});
restoreSkillEnv = skillsSnapshotForRun
? applySkillEnvOverridesFromSnapshot({
snapshot: skillsSnapshotForRun,
config: params.config,
})
: applySkillEnvOverrides({
skills: skillEntries ?? [],
config: params.config,
});
const skillsPrompt = resolveSkillsPromptForRun({
skillsSnapshot: skillsSnapshotForRun,
entries: shouldLoadSkillEntries ? skillEntries : undefined,
config: params.config,
workspaceDir: effectiveWorkspace,
agentId: sessionAgentId,
});
prepStages.mark("skills");
const sessionLabel = params.sessionKey ?? params.sessionId;
const contextInjectionMode = resolveContextInjectionMode(params.config, sessionAgentId);
const isRawModelRun = params.modelRun === true || params.promptMode === "none";
if (isRawModelRun && log.isEnabled("debug")) {
log.debug(
`raw model run enabled: modelRun=${params.modelRun === true} promptMode=${params.promptMode ?? "unset"}`,
);
}
const activeContextEngine = isRawModelRun ? undefined : params.contextEngine;
if (activeContextEngine && activeContextEngine.info.id !== "legacy") {
assertContextEngineHostSupport({
contextEngine: activeContextEngine,
operation: "agent-run",
host: OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST,
});
}
const resolveActiveContextEnginePluginId = () =>
resolveContextEngineOwnerPluginId(activeContextEngine);
const agentDir = params.agentDir ?? resolveAgentDir(params.config ?? {}, sessionAgentId);
const diagnosticTrace = freezeDiagnosticTraceContext(
createDiagnosticTraceContextFromActiveScope(),
);
const runTrace = freezeDiagnosticTraceContext(
createChildDiagnosticTraceContext(diagnosticTrace),
);
const diagnosticRunBase = {
runId: params.runId,
...(params.sessionKey && { sessionKey: params.sessionKey }),
...(params.sessionId && { sessionId: params.sessionId }),
provider: params.provider,
model: params.modelId,
trigger: params.trigger,
...((params.messageChannel ?? params.messageProvider)
? { channel: params.messageChannel ?? params.messageProvider }
: {}),
trace: runTrace,
};
emitTrustedDiagnosticEvent({
type: "run.started",
...diagnosticRunBase,
});
const diagnosticRunStartedAt = Date.now();
let diagnosticRunCompleted = false;
emitDiagnosticRunCompleted = (outcome, err, extra) => {
if (diagnosticRunCompleted) {
return;
}
diagnosticRunCompleted = true;
emitTrustedDiagnosticEvent({
type: "run.completed",
...diagnosticRunBase,
durationMs: Date.now() - diagnosticRunStartedAt,
outcome,
...(extra?.blockedBy ? { blockedBy: extra.blockedBy } : {}),
...(err && outcome !== "blocked" ? { errorCategory: diagnosticErrorCategory(err) } : {}),
});
};
const corePluginToolStages = createEmbeddedRunStageTracker();
const toolsAllowWithForcedRuntimeTools = mergeForcedEmbeddedAttemptToolsAllow(
params.toolsAllow,
{
forceMessageTool:
params.forceMessageTool === true ||
params.sourceReplyDeliveryMode === "message_tool_only",
},
);
const toolConstructionPlan = resolveEmbeddedAttemptToolConstructionPlan({
disableTools: params.disableTools,
isRawModelRun,
toolsAllow: toolsAllowWithForcedRuntimeTools,
});
const toolsEnabled = supportsModelTools(params.model);
const codeModeConfig = resolveCodeModeConfig(params.config, sessionAgentId);
const codeModeControlsEnabledForRun =
toolsEnabled &&
params.disableTools !== true &&
!isRawModelRun &&
params.toolsAllow?.length !== 0 &&
codeModeConfig.enabled;
const toolSearchControlsEnabledForRun =
toolsEnabled &&
params.disableTools !== true &&
!isRawModelRun &&
params.toolsAllow?.length !== 0 &&
!codeModeControlsEnabledForRun &&
resolveToolSearchConfig(params.config).enabled;
const effectiveToolsAllow =
toolSearchControlsEnabledForRun && toolsAllowWithForcedRuntimeTools
? [
...new Set([
...toolsAllowWithForcedRuntimeTools,
...TOOL_SEARCH_CONTROL_ALLOWLIST_NAMES,
]),
]
: toolsAllowWithForcedRuntimeTools;
const shouldConstructTools =
toolConstructionPlan.constructTools ||
toolSearchControlsEnabledForRun ||
codeModeControlsEnabledForRun;
let toolSearchCatalogExecutor: ToolSearchCatalogToolExecutor | undefined;
toolSearchCatalogRef =
toolSearchControlsEnabledForRun || codeModeControlsEnabledForRun
? createToolSearchCatalogRef()
: undefined;
const toolSearchTargetTranscriptProjections: ToolSearchTargetTranscriptProjection[] = [];
const toolsRaw = !shouldConstructTools
? []
: (() => {
const allTools = createOpenClawCodingTools({
agentId: sessionAgentId,
...buildEmbeddedAttemptToolRunContext({ ...params, trace: runTrace }),
exec: {
...params.execOverrides,
config: params.config,
elevated: params.bashElevated,
},
sandbox,
messageProvider: resolveAttemptToolPolicyMessageProvider(params),
agentAccountId: params.agentAccountId,
messageTo: params.messageTo,
messageThreadId: params.messageThreadId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
memberRoleIds: params.memberRoleIds,
spawnedBy: params.spawnedBy,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
senderIsOwner: params.senderIsOwner,
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
sessionKey: sandboxSessionKey,
// When sandboxSessionKey differs from the real run session key (e.g. Telegram
// direct peer key vs agent:main:main), pass the live key so session_status
// "current" resolves to the active run session, not the stale sandbox key.
runSessionKey:
params.sessionKey && params.sessionKey !== sandboxSessionKey
? params.sessionKey
: undefined,
sessionId: params.sessionId,
runId: params.runId,
toolSearchCatalogRef,
agentDir,
cwd: effectiveCwd,
workspaceDir: effectiveWorkspace,
// Runtime cwd can point at a task repo while bootstrap/persona files stay in the
// agent workspace. Spawned subagents inherit the real agent workspace, not task cwd.
spawnWorkspaceDir:
effectiveCwd !== effectiveWorkspace
? resolvedWorkspace
: resolveAttemptSpawnWorkspaceDir({
sandbox,
resolvedWorkspace,
}),
config: params.config,
abortSignal: runAbortController.signal,
modelProvider: params.provider,
modelId: params.modelId,
modelCompat: extractModelCompat(params.model),
modelApi: params.model.api,
modelContextWindowTokens: params.model.contextWindow,
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, undefined, {
workspaceDir: effectiveWorkspace,
}),
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
includeCoreTools: toolConstructionPlan.includeCoreTools,
includeToolSearchControls: toolSearchControlsEnabledForRun,
toolSearchCatalogExecutor: (toolParams) => {
if (!toolSearchCatalogExecutor) {
throw new Error("Tool Search catalog executor is unavailable for this run.");
}
return toolSearchCatalogExecutor(toolParams);
},
toolConstructionPlan: toolConstructionPlan.codingToolConstructionPlan,
replyToMode: params.replyToMode,
hasRepliedRef: params.hasRepliedRef,
modelHasVision: params.model.input?.includes("image") ?? false,
requireExplicitMessageTarget:
params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
inboundEventKind: params.currentInboundEventKind,
disableMessageTool: params.disableMessageTool,
forceMessageTool: params.forceMessageTool,
enableHeartbeatTool: params.enableHeartbeatTool,
forceHeartbeatTool: params.forceHeartbeatTool,
authProfileStore: params.authProfileStore,
recordToolPrepStage: (name) => corePluginToolStages.mark(name),
onToolOutcome: params.onToolOutcome,
skillsSnapshot: skillsSnapshotForRun,
onYield: (message) => {
yieldDetected = true;
yieldMessage = message;
queueYieldInterruptForSession?.();
runAbortController.abort("sessions_yield");
abortSessionForYield?.();
},
});
corePluginToolStages.mark("attempt:create-openclaw-coding-tools");
const filteredTools = applyEmbeddedAttemptToolsAllow(allTools, effectiveToolsAllow, {
toolMeta: (tool) => getPluginToolMeta(tool),
});
corePluginToolStages.mark("attempt:tools-allow");
return filteredTools;
})();
prepStages.mark("core-plugin-tools");
emitCorePluginToolStageSummary("core-plugin-tools", corePluginToolStages.snapshot());
const bootstrapHasFileAccess = toolsEnabled && toolsRaw.some((tool) => tool.name === "read");
const bootstrapWarn = makeBootstrapWarn({
sessionLabel,
workspaceDir: resolvedWorkspace,
warn: (message) => log.warn(message),
});
let completedBootstrapTurn: boolean | undefined;
const hasCompletedBootstrapTurnForAttempt = async (sessionFile: string) => {
completedBootstrapTurn ??= await hasCompletedBootstrapTurn(sessionFile);
return completedBootstrapTurn;
};
const resolveBootstrapRouting = (bootstrapFiles?: readonly WorkspaceBootstrapFile[]) =>
resolveAttemptWorkspaceBootstrapRouting({
isWorkspaceBootstrapPending,
bootstrapFiles,
bootstrapContextRunKind: params.bootstrapContextRunKind,
trigger: params.trigger,
sessionKey: params.sessionKey,
isPrimaryRun: isPrimaryBootstrapRun(params.sessionKey),
isCanonicalWorkspace: params.isCanonicalWorkspace,
effectiveWorkspace,
resolvedWorkspace,
hasBootstrapFileAccess: bootstrapHasFileAccess,
});
const shouldProbeContinuationSkip =
!isRawModelRun &&
contextInjectionMode === "continuation-skip" &&
(params.bootstrapContextRunKind ?? "default") !== "heartbeat" &&
(await hasCompletedBootstrapTurnForAttempt(params.sessionFile));
let preloadedBootstrapFiles: WorkspaceBootstrapFile[] | undefined;
let bootstrapRouting =
shouldProbeContinuationSkip || isRawModelRun || contextInjectionMode === "never"
? await resolveBootstrapRouting()
: undefined;
if (
!isRawModelRun &&
contextInjectionMode !== "never" &&
(bootstrapRouting === undefined || bootstrapRouting.bootstrapMode === "full")
) {
preloadedBootstrapFiles = await resolveBootstrapFilesForRun({
workspaceDir: resolvedWorkspace,
config: params.config,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: sessionAgentId,
warn: bootstrapWarn,
contextMode: params.bootstrapContextMode,
runKind: params.bootstrapContextRunKind,
});
bootstrapRouting = await resolveBootstrapRouting(preloadedBootstrapFiles);
}
bootstrapRouting ??= await resolveBootstrapRouting(preloadedBootstrapFiles);
const bootstrapMode = bootstrapRouting.bootstrapMode;
const {
bootstrapFiles: hookAdjustedBootstrapFiles,
contextFiles: resolvedContextFiles,
shouldRecordCompletedBootstrapTurn,
} = await resolveAttemptBootstrapContext({
// modelRun is a provider probe, not an agent turn. Keep AGENTS/BOOTSTRAP
// context out even when the gateway is exercising the embedded runtime.
contextInjectionMode: isRawModelRun ? "never" : contextInjectionMode,
bootstrapContextMode: params.bootstrapContextMode,
bootstrapContextRunKind: params.bootstrapContextRunKind ?? "default",
bootstrapMode,
sessionFile: params.sessionFile,
hasCompletedBootstrapTurn: hasCompletedBootstrapTurnForAttempt,
resolveBootstrapContextForRun: async () => {
const bootstrapFiles =
preloadedBootstrapFiles ??
(await resolveBootstrapFilesForRun({
workspaceDir: resolvedWorkspace,
config: params.config,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: sessionAgentId,
warn: bootstrapWarn,
contextMode: params.bootstrapContextMode,
runKind: params.bootstrapContextRunKind,
}));
return {
bootstrapFiles,
contextFiles: buildBootstrapContextForFiles(bootstrapFiles, {
config: params.config,
agentId: sessionAgentId,
warn: bootstrapWarn,
}),
};
},
});
prepStages.mark("bootstrap-context");
const remappedContextFiles = remapInjectedContextFilesToWorkspace({
files: resolvedContextFiles,
sourceWorkspaceDir: resolvedWorkspace,
targetWorkspaceDir: effectiveWorkspace,
});
const contextFiles = bootstrapRouting.includeBootstrapInSystemContext
? remappedContextFiles
: remappedContextFiles.filter((file) => !/(^|[\\/])BOOTSTRAP\.md$/iu.test(file.path.trim()));
const bootstrapFilesForInjectionStats = bootstrapRouting.includeBootstrapInSystemContext
? hookAdjustedBootstrapFiles
: hookAdjustedBootstrapFiles.filter((file) => file.name !== DEFAULT_BOOTSTRAP_FILENAME);
const bootstrapMaxChars = resolveBootstrapMaxChars(params.config, sessionAgentId);
const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.config, sessionAgentId);
const bootstrapAnalysis = analyzeBootstrapBudget({
files: buildBootstrapInjectionStats({
bootstrapFiles: bootstrapFilesForInjectionStats,
injectedFiles: contextFiles,
}),
bootstrapMaxChars,
bootstrapTotalMaxChars,
});
const bootstrapPromptWarningMode = resolveBootstrapPromptTruncationWarningMode(params.config);
const bootstrapPromptWarning = buildBootstrapPromptWarning({
analysis: bootstrapAnalysis,
mode: bootstrapPromptWarningMode,
seenSignatures: params.bootstrapPromptWarningSignaturesSeen,
previousSignature: params.bootstrapPromptWarningSignature,
});
const workspaceNotes: string[] = [];
if (
hookAdjustedBootstrapFiles.some(
(file) => file.name === DEFAULT_BOOTSTRAP_FILENAME && !file.missing,
)
) {
workspaceNotes.push("Reminder: commit your changes in this workspace after edits.");
}
if (isEmbeddedMode()) {
workspaceNotes.push(
"Running in local embedded mode (no gateway). Most tools work locally. Gateway-dependent tools (canvas, nodes, cron, message, sessions_send, sessions_spawn, gateway) are unavailable. Subagent kill/steer require a gateway. Do not attempt to read gateway-specific files such as sessions.json, gateway.log, or gateway.pid.",
);
}
const { defaultAgentId } = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
// Track sessions_yield tool invocation (callback pattern, like clientToolCallDetected)
let yieldDetected = false;
let yieldMessage: string | null = null;
// Late-binding reference so onYield can abort the session (declared after tool creation)
let abortSessionForYield: (() => void) | null = null;
let queueYieldInterruptForSession: (() => void) | null = null;
let yieldAbortSettled: Promise<void> | null = null;
const runtimePlanModelContext = {
workspaceDir: effectiveWorkspace,
modelApi: params.model.api,
model: params.model,
};
const tools = normalizeAgentRuntimeTools({
runtimePlan: params.runtimePlan,
tools: toolsEnabled ? toolsRaw : [],
provider: params.provider,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
modelId: params.modelId,
modelApi: params.model.api,
model: params.model,
runtimeHandle: getProviderRuntimeHandle(),
});
const clientTools = toolsEnabled && !isRawModelRun ? params.clientTools : undefined;
const bundleMcpEnabled = shouldCreateBundleMcpRuntimeForAttempt({
toolsEnabled,
disableTools: params.disableTools || isRawModelRun,
toolsAllow: params.toolsAllow,
});
const bundleMcpSessionRuntime = bundleMcpEnabled
? await getOrCreateSessionMcpRuntime({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
workspaceDir: effectiveWorkspace,
cfg: params.config,
})
: undefined;
const bundleMcpRuntime = bundleMcpSessionRuntime
? await materializeBundleMcpToolsForRun({
runtime: bundleMcpSessionRuntime,
reservedToolNames: [
...tools.map((tool) => tool.name),
...(clientTools?.map((tool) => tool.function.name) ?? []),
],
})
: undefined;
const bundleLspEnabled = shouldCreateBundleLspRuntimeForAttempt({
toolsEnabled,
disableTools: params.disableTools || isRawModelRun,
toolsAllow: params.toolsAllow,
});
const bundleLspRuntime = bundleLspEnabled
? await createBundleLspToolRuntime({
workspaceDir: effectiveWorkspace,
cfg: params.config,
reservedToolNames: [
...tools.map((tool) => tool.name),
...(clientTools?.map((tool) => tool.function.name) ?? []),
...(bundleMcpRuntime?.tools.map((tool) => tool.name) ?? []),
],
})
: undefined;
const allowedBundledTools = applyEmbeddedAttemptToolsAllow(
[...(bundleMcpRuntime?.tools ?? []), ...(bundleLspRuntime?.tools ?? [])],
effectiveToolsAllow,
{
toolMeta: (tool) => getPluginToolMeta(tool),
},
);
const filteredBundledTools = applyFinalEffectiveToolPolicy({
bundledTools: allowedBundledTools,
config: params.config,
sandboxToolPolicy: sandbox?.tools,
sessionKey: sandboxSessionKey,
agentId: sessionAgentId,
modelProvider: params.provider,
modelId: params.modelId,
messageProvider: resolveAttemptToolPolicyMessageProvider(params),
agentAccountId: params.agentAccountId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
spawnedBy: params.spawnedBy,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
warn: (message) => log.warn(message),
});
const normalizedBundledTools =
filteredBundledTools.length > 0
? normalizeAgentRuntimeTools({
runtimePlan: params.runtimePlan,
tools: filteredBundledTools,
provider: params.provider,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
modelId: params.modelId,
modelApi: params.model.api,
model: params.model,
runtimeHandle: getProviderRuntimeHandle(),
})
: filteredBundledTools;
const projectedUncompactedEffectiveTools = filterLocalModelLeanTools({
tools: [...tools, ...normalizedBundledTools],
config: params.config,
agentId: sessionAgentId,
});
const uncompactedToolSchemaProjection = filterRuntimeCompatibleTools(
projectedUncompactedEffectiveTools,
);
logRuntimeToolSchemaQuarantine({
diagnostics: uncompactedToolSchemaProjection.diagnostics,
tools: projectedUncompactedEffectiveTools,
runId: params.runId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
});
const uncompactedEffectiveTools = [...uncompactedToolSchemaProjection.tools];
let effectiveTools = uncompactedEffectiveTools;
const catalogToolHookContext = {
agentId: sessionAgentId,
config: params.config,
cwd: effectiveCwd,
sessionKey: sandboxSessionKey,
sessionId: params.sessionId,
runId: params.runId,
channelId: params.currentChannelId,
trace: runTrace,
loopDetection: resolveToolLoopDetectionConfig({
cfg: params.config,
agentId: sessionAgentId,
}),
onToolOutcome: params.onToolOutcome,
};
const codeModeTools = codeModeControlsEnabledForRun
? createCodeModeTools({
config: params.config,
runtimeConfig: params.config,
agentId: sessionAgentId,
sessionKey: sandboxSessionKey,
sessionId: params.sessionId,
runId: params.runId,
catalogRef: toolSearchCatalogRef,
abortSignal: runAbortController.signal,
executeTool: (toolParams) => {
if (!toolSearchCatalogExecutor) {
throw new Error("Code Mode catalog executor is unavailable for this run.");
}
return toolSearchCatalogExecutor(toolParams);
},
})
: [];
const toolSearch = codeModeControlsEnabledForRun
? applyCodeModeCatalog({
tools: [...codeModeTools, ...effectiveTools],
config: params.config,
sessionId: params.sessionId,
sessionKey: sandboxSessionKey,
agentId: sessionAgentId,
runId: params.runId,
catalogRef: toolSearchCatalogRef,
toolHookContext: catalogToolHookContext,
})
: applyToolSearchCatalog({
tools: effectiveTools,
config: params.config,
sessionId: params.sessionId,
sessionKey: sandboxSessionKey,
agentId: sessionAgentId,
runId: params.runId,
catalogRef: toolSearchCatalogRef,
toolHookContext: catalogToolHookContext,
});
const projectedToolSearchTools = filterLocalModelLeanTools({
tools: toolSearch.tools,
config: params.config,
agentId: sessionAgentId,
});
const toolSearchSchemaProjection = filterRuntimeCompatibleTools(projectedToolSearchTools);
logRuntimeToolSchemaQuarantine({
diagnostics: toolSearchSchemaProjection.diagnostics,
tools: projectedToolSearchTools,
runId: params.runId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
});
effectiveTools = [...toolSearchSchemaProjection.tools];
if (toolSearch.compacted && !toolSearch.catalogReused) {
prepStages.mark(codeModeControlsEnabledForRun ? "code-mode" : "tool-search");
log.info(
codeModeControlsEnabledForRun
? `code-mode: cataloged ${toolSearch.catalogToolCount} tools behind exec/wait`
: `tool-search: cataloged ${toolSearch.catalogToolCount} tools behind compact prompt surface`,
);
}
prepStages.mark("bundle-tools");
const explicitToolAllowlistSources = collectAttemptExplicitToolAllowlistSources({
config: params.config,
sessionKey: params.sessionKey,
sandboxSessionKey,
agentId: sessionAgentId,
modelProvider: params.provider,
modelId: params.modelId,
messageProvider: resolveAttemptToolPolicyMessageProvider(params),
agentAccountId: params.agentAccountId,
groupId: params.groupId,
groupChannel: params.groupChannel,
groupSpace: params.groupSpace,
spawnedBy: params.spawnedBy,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164,
sandboxToolPolicy: sandbox?.tools,
toolsAllow: params.toolsAllow,
});
const toolSearchRunPlan = buildToolSearchRunPlan({
visibleTools: effectiveTools,
uncompactedTools: uncompactedEffectiveTools,
clientTools,
catalogRegistered: toolSearch.catalogRegistered,
catalogToolCount: toolSearch.catalogToolCount,
controlsEnabled: toolSearchControlsEnabledForRun || codeModeControlsEnabledForRun,
controlNames: codeModeControlsEnabledForRun
? [CODE_MODE_EXEC_TOOL_NAME, CODE_MODE_WAIT_TOOL_NAME]
: undefined,
explicitAllowlistSources: explicitToolAllowlistSources,
});
const allowedToolNames = toolSearchRunPlan.visibleAllowedToolNames;
const replayAllowedToolNames = toolSearchRunPlan.replayAllowedToolNames;
const emptyExplicitToolAllowlistError = buildEmptyExplicitToolAllowlistError({
sources: explicitToolAllowlistSources,
callableToolNames: toolSearchRunPlan.emptyAllowlistCallableNames,
toolsEnabled,
disableTools: params.disableTools,
});
logAgentRuntimeToolDiagnostics({
runtimePlan: params.runtimePlan,
tools: effectiveTools,
provider: params.provider,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
modelId: params.modelId,
modelApi: params.model.api,
model: params.model,
runtimeHandle: getProviderRuntimeHandle(),
});
const machineName = await getMachineDisplayName();
const runtimeChannel = normalizeMessageChannel(params.messageChannel ?? params.messageProvider);
const runtimeCapabilities = collectRuntimeChannelCapabilities({
cfg: params.config,
channel: runtimeChannel,
accountId: params.agentAccountId,
});
const reactionGuidance =
runtimeChannel && params.config
? resolveChannelReactionGuidance({
cfg: params.config,
channel: runtimeChannel,
accountId: params.agentAccountId,
})
: undefined;
const sandboxInfoExecPolicy = resolveEmbeddedSandboxInfoExecPolicy({
config: params.config,
agentId: sessionAgentId,
sessionKey: params.sessionKey,
sandboxAvailable: sandbox?.enabled === true,
execOverrides: params.execOverrides,
});
const sandboxInfo = buildEmbeddedSandboxInfo(
sandbox,
params.bashElevated,
sandboxInfoExecPolicy,
);
const reasoningTagHint = isReasoningTagProvider(params.provider, {
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
modelId: params.modelId,
modelApi: params.model.api,
model: params.model,
runtimeHandle: getProviderRuntimeHandle(),
});
// Resolve channel-specific message actions for system prompt
const channelActions = runtimeChannel
? listChannelSupportedActions(
buildEmbeddedMessageActionDiscoveryInput({
cfg: params.config,
channel: runtimeChannel,
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
accountId: params.agentAccountId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: sessionAgentId,
senderId: params.senderId,
senderIsOwner: params.senderIsOwner,
}),
)
: undefined;
const messageToolHints = runtimeChannel
? resolveChannelMessageToolHints({
cfg: params.config,
channel: runtimeChannel,
accountId: params.agentAccountId,
})
: undefined;
const defaultModelRef = resolveDefaultModelForAgent({
cfg: params.config ?? {},
agentId: sessionAgentId,
});
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
const activeProcessSessions = listActiveProcessSessionReferences({
scopeKey: resolveProcessToolScopeKey({
sessionKey: sandboxSessionKey,
agentId: sessionAgentId,
}),
});
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
config: params.config,
agentId: sessionAgentId,
workspaceDir: effectiveWorkspace,
cwd: effectiveCwd,
runtime: {
host: machineName,
os: `${os.type()} ${os.release()}`,
arch: os.arch(),
node: process.version,
model: `${params.provider}/${params.modelId}`,
defaultModel: defaultModelLabel,
shell: detectRuntimeShell(),
channel: runtimeChannel,
capabilities: runtimeCapabilities,
channelActions,
activeProcessSessions,
},
});
const isDefaultAgent = sessionAgentId === defaultAgentId;
const promptMode =
params.promptMode ??
(isRawModelRun ? "none" : resolvePromptModeForSession(params.sessionKey));
const promptSurface = resolveAgentPromptSurfaceForSessionKey(params.sessionKey);
// When toolsAllow is set, use minimal prompt and strip skills catalog
const effectivePromptMode = params.toolsAllow?.length ? ("minimal" as const) : promptMode;
const effectiveSkillsPrompt = params.toolsAllow?.length ? undefined : skillsPrompt;
const openClawReferences = await resolveOpenClawReferencePaths({
workspaceDir: effectiveWorkspace,
argv1: process.argv[1],
cwd: effectiveCwd,
moduleUrl: import.meta.url,
});
const heartbeatPrompt = shouldInjectHeartbeatPrompt({
config: params.config,
agentId: sessionAgentId,
defaultAgentId,
isDefaultAgent,
trigger: params.trigger,
})
? resolveHeartbeatPromptForSystemPrompt({
config: params.config,
agentId: sessionAgentId,
defaultAgentId,
})
: undefined;
const promptContributionContext = {
config: params.config,
agentDir: params.agentDir,
workspaceDir: effectiveWorkspace,
provider: params.provider,
modelId: params.modelId,
promptMode: effectivePromptMode,
runtimeChannel,
runtimeCapabilities,
agentId: sessionAgentId,
trigger: params.trigger,
};
const promptContribution =
params.runtimePlan?.prompt.resolveSystemPromptContribution(promptContributionContext) ??
resolveProviderSystemPromptContribution({
provider: params.provider,
config: params.config,
workspaceDir: effectiveWorkspace,
runtimeHandle: getProviderRuntimeHandle(),
context: promptContributionContext,
});
const bootstrapTruncationNotice = buildBootstrapPromptWarningNotice(
bootstrapPromptWarning.lines,
);
const attemptSystemPrompt = buildAttemptSystemPrompt({
isRawModelRun,
transformProviderSystemPrompt: (transformParams) =>
transformProviderSystemPrompt({
...transformParams,
runtimeHandle: getProviderRuntimeHandle(),
}),
embeddedSystemPrompt: {
config: params.config,
agentId: sessionAgentId,
workspaceDir: effectiveWorkspace,
defaultThinkLevel: params.thinkLevel,
reasoningLevel: params.reasoningLevel ?? "off",
extraSystemPrompt: params.extraSystemPrompt,
ownerNumbers: params.ownerNumbers,
reasoningTagHint,
heartbeatPrompt,
skillsPrompt: effectiveSkillsPrompt,
docsPath: openClawReferences.docsPath ?? undefined,
sourcePath: openClawReferences.sourcePath ?? undefined,
workspaceNotes: workspaceNotes?.length ? workspaceNotes : undefined,
reactionGuidance,
promptMode: effectivePromptMode,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
silentReplyPromptMode: params.silentReplyPromptMode,
acpEnabled: isAcpRuntimeSpawnAvailable({
config: params.config,
sandboxed: sandboxInfo?.enabled === true,
}),
promptSurface,
nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance({
surface: promptSurface,
}),
runtimeInfo,
messageToolHints,
sandboxInfo,
tools: effectiveTools,
userTimezone,
userTime,
userTimeFormat,
contextFiles,
bootstrapMode,
bootstrapTruncationNotice,
includeMemorySection: !activeContextEngine || activeContextEngine.info.id === "legacy",
promptContribution,
},
providerTransform: {
provider: params.provider,
config: params.config,
workspaceDir: effectiveWorkspace,
context: {
config: params.config,
agentDir: params.agentDir,
workspaceDir: effectiveWorkspace,
provider: params.provider,
modelId: params.modelId,
promptMode: effectivePromptMode,
runtimeChannel,
runtimeCapabilities,
agentId: sessionAgentId,
},
},
});
const appendPrompt = attemptSystemPrompt.systemPrompt;
const systemPromptReport = buildSystemPromptReport({
source: "run",
generatedAt: Date.now(),
sessionId: params.sessionId,
sessionKey: params.sessionKey,
provider: params.provider,
model: params.modelId,
workspaceDir: effectiveWorkspace,
bootstrapMaxChars,
bootstrapTotalMaxChars,
bootstrapTruncation: buildBootstrapTruncationReportMeta({
analysis: bootstrapAnalysis,
warningMode: bootstrapPromptWarningMode,
warning: bootstrapPromptWarning,
}),
sandbox: (() => {
const runtime = resolveSandboxRuntimeStatus({
cfg: params.config,
sessionKey: sandboxSessionKey,
});
return { mode: runtime.mode, sandboxed: runtime.sandboxed };
})(),
systemPrompt: appendPrompt,
bootstrapFiles: hookAdjustedBootstrapFiles,
injectedFiles: contextFiles,
skillsPrompt,
tools: effectiveTools,
});
let systemPromptText = attemptSystemPrompt.systemPrompt;
prepStages.mark("system-prompt");
const compactionTimeoutMs = resolveCompactionTimeoutMs(params.config);
const sessionWriteLockOptions = resolveEmbeddedAttemptSessionWriteLockOptions({
config: params.config,
compactionTimeoutMs,
});
await throwIfAttemptAbortSignalFiredAfterPrepCleanup();
retainedSessionFileOwner = await acquireEmbeddedAttemptSessionFileOwner({
sessionFile: params.sessionFile,
timeoutMs: sessionWriteLockOptions.maxHoldMs,
signal: params.abortSignal,
});
const sessionLockController = await createEmbeddedAttemptSessionLockController({
acquireSessionWriteLock,
lockOptions: {
sessionFile: params.sessionFile,
...sessionWriteLockOptions,
},
});
releaseRetainedSessionLock = () => sessionLockController.dispose();
armExternalAbortSignal();
let sessionManager: ReturnType<typeof guardSessionManager> | undefined;
let session: Awaited<ReturnType<typeof createAgentSession>>["session"] | undefined;
let removeToolResultContextGuard: (() => void) | undefined;
let trajectoryRecorder: ReturnType<typeof createTrajectoryRuntimeRecorder> | null = null;
let trajectoryEndRecorded = false;
let buildAbortSettlePromise: () => Promise<void> | null = () => null;
let cleanupYieldAborted = false;
try {
await repairSessionFileIfNeeded({
sessionFile: params.sessionFile,
debug: (message) => log.debug(message),
warn: (message) => log.warn(message),
});
const hadSessionFile = await fs
.stat(params.sessionFile)
.then(() => true)
.catch(() => false);
const transcriptPolicy = resolveAttemptTranscriptPolicy({
runtimePlan: params.runtimePlan,
runtimePlanModelContext,
provider: params.provider,
modelId: params.modelId,
config: params.config,
env: process.env,
});
await prewarmSessionFile(params.sessionFile);
const preparedUserTurnMessage = await params.userTurnTranscriptRecorder?.resolveMessage();
sessionManager = guardSessionManager(SessionManager.open(params.sessionFile), {
agentId: sessionAgentId,
sessionKey: params.sessionKey,
config: params.config,
contextWindowTokens: params.contextTokenBudget,
inputProvenance: params.inputProvenance,
preparedUserTurnMessage,
allowSyntheticToolResults: transcriptPolicy.allowSyntheticToolResults,
missingToolResultText:
params.model.api === "openai-responses" ||
params.model.api === "azure-openai-responses" ||
params.model.api === "openai-chatgpt-responses"
? "aborted"
: undefined,
allowedToolNames: replayAllowedToolNames,
suppressNextUserMessagePersistence: params.suppressNextUserMessagePersistence,
suppressTranscriptOnlyAssistantPersistence:
params.suppressTranscriptOnlyAssistantPersistence,
suppressAssistantErrorPersistence: params.suppressAssistantErrorPersistence,
onMessagePersisted: () => {
sessionLockController.refreshAfterOwnedSessionWrite();
},
onUserMessagePersisted: (message) => {
params.onUserMessagePersisted?.(message);
},
onAssistantErrorMessagePersisted: (message) => {
params.onAssistantErrorMessagePersisted?.(message);
},
});
trackSessionManagerAccess(params.sessionFile);
await runAttemptContextEngineBootstrap({
hadSessionFile,
contextEngine: activeContextEngine,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
sessionFile: params.sessionFile,
sessionManager,
runtimeContext: buildAfterTurnRuntimeContext({
attempt: params,
workspaceDir: effectiveWorkspace,
cwd: effectiveCwd,
agentDir,
tokenBudget: params.contextTokenBudget,
activeAgentId: sessionAgentId,
contextEnginePluginId: resolveActiveContextEnginePluginId(),
}),
runMaintenance: async (contextParams) =>
await runContextEngineMaintenance({
contextEngine: contextParams.contextEngine as never,
sessionId: contextParams.sessionId,
sessionKey: contextParams.sessionKey,
sessionFile: contextParams.sessionFile,
reason: contextParams.reason,
sessionManager: contextParams.sessionManager as never,
runtimeContext: contextParams.runtimeContext,
config: params.config,
agentId: sessionAgentId,
}),
warn: (message) => log.warn(message),
});
await prepareSessionManagerForRun({
sessionManager,
sessionFile: params.sessionFile,
hadSessionFile,
sessionId: params.sessionId,
cwd: effectiveCwd,
});
const settingsManager = createPreparedEmbeddedAgentSettingsManager({
cwd: effectiveCwd,
agentDir,
cfg: params.config,
pluginMetadataSnapshot: getCurrentAttemptPluginMetadataSnapshot(),
contextTokenBudget: params.contextTokenBudget,
});
const autoCompactionGuardArgs = {
settingsManager,
contextEngineInfo: activeContextEngine?.info,
compactionMode: resolveEffectiveCompactionMode(params.config),
silentOverflowProneProvider: isSilentOverflowProneModel({
provider: params.provider,
modelId: params.modelId,
baseUrl: params.model.baseUrl ?? undefined,
}),
};
applyAgentAutoCompactionGuard(autoCompactionGuardArgs);
// Sets compaction/pruning runtime state and returns extension factories
// that must be passed to the resource loader for the safeguard to be active.
const extensionFactories = buildEmbeddedExtensionFactories({
cfg: params.config,
sessionManager,
provider: params.provider,
modelId: params.modelId,
model: params.model,
});
const resourceLoader = createEmbeddedAgentResourceLoader({
cwd: effectiveCwd,
agentDir,
settingsManager,
extensionFactories,
});
await resourceLoader.reload();
// DefaultResourceLoader.reload() rehydrates settings from disk and can drop OpenClaw
// compaction overrides applied in createPreparedEmbeddedAgentSettingsManager — same
// rehydration also restores OpenClaw runtime's auto-compaction (openclaw#75799), so re-apply
// both guards.
applyAgentCompactionSettingsFromConfig({
settingsManager,
cfg: params.config,
contextTokenBudget: params.contextTokenBudget,
});
applyAgentAutoCompactionGuard(autoCompactionGuardArgs);
prepStages.mark("session-resource-loader");
// Get hook runner early so it's available when creating tools
const hookRunner = getGlobalHookRunner();
const { customTools } = splitSdkTools({
tools: effectiveTools,
sandboxEnabled: Boolean(sandbox?.enabled),
toolHookContext: catalogToolHookContext,
});
// Add client tools (OpenResponses hosted tools) to customTools.
// Reserve slots synchronously at tool execution entry, before async
// before_tool_call hooks run, so parallel client-tool batches preserve
// assistant source order even when later hooks finish first.
const clientToolCallSlots: Array<{
toolCallId: string;
name: string;
params?: Record<string, unknown>;
completed: boolean;
}> = [];
const clientToolCallSlotIndexes = new Map<string, number>();
const reserveClientToolCallSlot = (toolCallId: string, toolName: string) => {
if (clientToolCallSlotIndexes.has(toolCallId)) {
return;
}
clientToolCallSlotIndexes.set(toolCallId, clientToolCallSlots.length);
clientToolCallSlots.push({
toolCallId,
name: toolName,
completed: false,
});
};
const clientToolLoopDetection = resolveToolLoopDetectionConfig({
cfg: params.config,
agentId: sessionAgentId,
});
// Exact raw names of every tool registered for this run, including
// bundled/plugin tools. Used as the raw-name set for the trusted local
// media passthrough gate: a normalized alias is not sufficient — the
// emitted tool name must match an exact registration of this run.
const builtinToolNames = new Set(
uncompactedEffectiveTools.flatMap((tool) => {
const name = (tool.name ?? "").trim();
return name ? [name] : [];
}),
);
// Admission-time conflict check only against non-plugin core tools, to
// preserve prior behavior where client tools may coexist with unrelated
// plugin tool names. MEDIA passthrough is still gated by the raw-name
// set above, so a client tool that normalize-collides with a plugin
// tool cannot inherit the plugin's local-media trust.
const coreBuiltinToolNames = collectCoreBuiltinToolNames(uncompactedEffectiveTools, {
isPluginTool: (tool) =>
Boolean(getPluginToolMeta(tool as Parameters<typeof getPluginToolMeta>[0])),
});
const clientToolNameConflicts = findClientToolNameConflicts({
tools: clientTools ?? [],
existingToolNames: [...coreBuiltinToolNames, ...AGENT_RESERVED_TOOL_NAMES],
});
if (clientToolNameConflicts.length > 0) {
throw createClientToolNameConflictError(clientToolNameConflicts);
}
let clientToolDefs = clientTools
? toClientToolDefinitions(
clientTools,
{
reserve: reserveClientToolCallSlot,
complete: (toolCallId, toolName, toolParams) => {
reserveClientToolCallSlot(toolCallId, toolName);
const slotIndex = clientToolCallSlotIndexes.get(toolCallId);
if (slotIndex === undefined) {
return;
}
const slot = clientToolCallSlots[slotIndex];
if (!slot) {
return;
}
slot.name = toolName;
slot.params = toolParams;
slot.completed = true;
},
discard: (toolCallId) => {
const slotIndex = clientToolCallSlotIndexes.get(toolCallId);
if (slotIndex === undefined) {
return;
}
const slot = clientToolCallSlots[slotIndex];
if (slot) {
slot.completed = false;
slot.params = undefined;
}
},
},
{
agentId: sessionAgentId,
sessionKey: sandboxSessionKey,
config: params.config,
sessionId: params.sessionId,
runId: params.runId,
loopDetection: clientToolLoopDetection,
onToolOutcome: params.onToolOutcome,
},
)
: [];
const clientToolSearch = codeModeControlsEnabledForRun
? addClientToolsToCodeModeCatalog({
tools: clientToolDefs,
config: params.config,
sessionId: params.sessionId,
sessionKey: sandboxSessionKey,
agentId: sessionAgentId,
runId: params.runId,
catalogRef: toolSearchCatalogRef,
})
: addClientToolsToToolSearchCatalog({
tools: clientToolDefs,
config: params.config,
sessionId: params.sessionId,
sessionKey: sandboxSessionKey,
agentId: sessionAgentId,
runId: params.runId,
catalogRef: toolSearchCatalogRef,
});
clientToolDefs = clientToolSearch.tools;
if (clientToolSearch.compacted) {
log.info(
codeModeControlsEnabledForRun
? `code-mode: cataloged ${clientToolSearch.catalogToolCount} client tools behind exec/wait`
: `tool-search: cataloged ${clientToolSearch.catalogToolCount} client tools behind compact prompt surface`,
);
}
const allCustomTools = [...customTools, ...clientToolDefs];
// The session runtime treats `tools` as a name allowlist during session creation. Pass the
// exact OpenClaw-managed registrations so custom tools survive startup and
// client-provided names do not broaden the prompt/runtime boundary.
const sessionToolAllowlist = toSessionToolAllowlist(
collectRegisteredToolNames(allCustomTools),
);
const createdSession = await createEmbeddedAgentSessionWithResourceLoader<
Awaited<ReturnType<typeof createAgentSession>>
>({
createAgentSession: async (options) =>
await createAgentSession(options as unknown as Parameters<typeof createAgentSession>[0]),
options: {
cwd: effectiveCwd,
agentDir,
authStorage: params.authStorage,
modelRegistry: params.modelRegistry,
model: params.model,
thinkingLevel: mapThinkingLevel(params.thinkLevel),
tools: sessionToolAllowlist,
customTools: allCustomTools,
sessionManager,
settingsManager,
resourceLoader,
withSessionWriteLock: (operation) =>
sessionLockController.withSessionWriteLock(operation),
},
});
session = createdSession.session;
if (!session) {
throw new Error("Embedded agent session missing");
}
session.setActiveToolsByName(sessionToolAllowlist);
const activeSession = session;
const setActiveSessionSystemPrompt = (nextSystemPrompt: string) => {
systemPromptText = nextSystemPrompt;
applySystemPromptToSession(activeSession, nextSystemPrompt);
};
setActiveSessionSystemPrompt(systemPromptText);
installMessageToolOnlyTerminalHook({
agent: activeSession.agent,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
});
prepStages.mark("agent-session");
if (isRawModelRun) {
// Raw model probes should measure exactly the requested prompt against
// the selected provider/model. Reset clears restored transcript state
// and queues; the empty system prompt prevents the runtime from rebuilding the
// normal OpenClaw agent/tool prompt when `session.prompt()` starts.
activeSession.agent.reset();
setActiveSessionSystemPrompt("");
}
if (typeof activeSession.agent.convertToLlm === "function") {
const baseConvertToLlm = activeSession.agent.convertToLlm.bind(activeSession.agent);
activeSession.agent.convertToLlm = async (messages) =>
await baseConvertToLlm(normalizeMessagesForLlmBoundary(messages));
}
let prePromptMessageCount = activeSession.messages.length;
let contextEngineAfterTurnCheckpoint: number | null = null;
let unwindowedContextEngineMessagesForPrecheck: AgentMessage[] | undefined;
let contextEnginePromptAuthority: NonNullable<AssembleResult["promptAuthority"]> =
"assembled";
const inFlightPromptSettlePromises = new Set<Promise<void>>();
const inFlightAbortSettlePromises = new Set<Promise<void>>();
const trackSettlePromise = (
promises: Set<Promise<void>>,
promise: Promise<void>,
): Promise<void> => {
promises.add(promise);
void promise.then(
() => {
promises.delete(promise);
},
() => {
promises.delete(promise);
},
);
return promise;
};
const trackPromptSettlePromise = (promise: Promise<void>): Promise<void> =>
trackSettlePromise(inFlightPromptSettlePromises, promise);
const trackAbortSettlePromise = (promise: Promise<void>): Promise<void> =>
trackSettlePromise(inFlightAbortSettlePromises, promise);
const abortActiveSession = (): Promise<void> =>
trackAbortSettlePromise(Promise.resolve(activeSession.abort()));
abortActiveSessionForExternalSignal = abortActiveSession;
buildAbortSettlePromise = (): Promise<void> | null => {
const promises = [...inFlightPromptSettlePromises, ...inFlightAbortSettlePromises];
if (promises.length === 0) {
return null;
}
return Promise.allSettled(promises).then(() => undefined);
};
abortSessionForYield = () => {
yieldAbortSettled = abortActiveSession();
};
queueYieldInterruptForSession = () => {
queueSessionsYieldInterruptMessage(activeSession);
};
const contextTokenBudgetForGuard = Math.max(
1,
Math.floor(
params.contextTokenBudget ??
params.model.contextWindow ??
params.model.maxTokens ??
DEFAULT_CONTEXT_TOKENS,
),
);
const toolResultMaxCharsForGuard = resolveLiveToolResultMaxChars({
contextWindowTokens: contextTokenBudgetForGuard,
cfg: params.config,
agentId: sessionAgentId,
});
const midTurnPrecheckEnabled =
params.config?.agents?.defaults?.compaction?.midTurnPrecheck?.enabled === true;
let pendingMidTurnPrecheckRequest: MidTurnPrecheckRequest | null = null;
const onMidTurnPrecheck = (request: MidTurnPrecheckRequest) => {
pendingMidTurnPrecheckRequest = request;
};
const midTurnPrecheckOptions = midTurnPrecheckEnabled
? {
midTurnPrecheck: {
enabled: true,
contextTokenBudget: contextTokenBudgetForGuard,
reserveTokens: () => settingsManager.getCompactionReserveTokens(),
toolResultMaxChars: toolResultMaxCharsForGuard,
getSystemPrompt: () => systemPromptText,
getPrePromptMessageCount: () => prePromptMessageCount,
onMidTurnPrecheck,
},
}
: {};
if (activeContextEngine?.info.ownsCompaction === true) {
const removeContextEngineLoopHook = installContextEngineLoopHook({
agent: activeSession.agent,
contextEngine: activeContextEngine,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
sessionFile: params.sessionFile,
tokenBudget: params.contextTokenBudget,
modelId: params.modelId,
getPrePromptMessageCount: () => prePromptMessageCount,
onAfterTurnCheckpoint: (messageCount) => {
contextEngineAfterTurnCheckpoint = messageCount;
},
getRuntimeContext: ({ messages, prePromptMessageCount: loopPrePromptMessageCount }) =>
buildAfterTurnRuntimeContext({
attempt: params,
workspaceDir: effectiveWorkspace,
cwd: effectiveCwd,
agentDir,
tokenBudget: params.contextTokenBudget,
promptCache:
promptCache ??
buildLoopPromptCacheInfo({
messagesSnapshot: messages,
prePromptMessageCount: loopPrePromptMessageCount,
retention: effectivePromptCacheRetention,
fallbackLastCacheTouchAt: readLastCacheTtlTimestamp(sessionManager, {
provider: params.provider,
modelId: params.modelId,
}),
}),
}),
});
const removeGuard = installToolResultContextGuard({
agent: activeSession.agent,
contextWindowTokens: contextTokenBudgetForGuard,
...midTurnPrecheckOptions,
});
removeToolResultContextGuard = () => {
removeGuard();
removeContextEngineLoopHook();
};
} else {
removeToolResultContextGuard = installToolResultContextGuard({
agent: activeSession.agent,
contextWindowTokens: contextTokenBudgetForGuard,
...midTurnPrecheckOptions,
});
}
const removeLoopContextGuard = removeToolResultContextGuard;
const removeHistoryImagePruneContextTransform = installHistoryImagePruneContextTransform(
activeSession.agent,
);
removeToolResultContextGuard = () => {
removeHistoryImagePruneContextTransform();
removeLoopContextGuard?.();
};
const cacheTrace = createCacheTrace({
cfg: params.config,
env: process.env,
runId: params.runId,
sessionId: activeSession.sessionId,
sessionKey: params.sessionKey,
provider: params.provider,
modelId: params.modelId,
modelApi: params.model.api,
workspaceDir: params.workspaceDir,
});
const anthropicPayloadLogger = createAnthropicPayloadLogger({
env: process.env,
runId: params.runId,
sessionId: activeSession.sessionId,
sessionKey: params.sessionKey,
provider: params.provider,
modelId: params.modelId,
modelApi: params.model.api,
workspaceDir: params.workspaceDir,
});
trajectoryRecorder = createTrajectoryRuntimeRecorder({
cfg: params.config,
env: process.env,
runId: params.runId,
sessionId: activeSession.sessionId,
sessionKey: params.sessionKey,
sessionFile: params.sessionFile,
provider: params.provider,
modelId: params.modelId,
modelApi: params.model.api,
workspaceDir: params.workspaceDir,
});
trajectoryRecorder?.recordEvent("session.started", {
trigger: params.trigger,
sessionFile: params.sessionFile,
workspaceDir: effectiveWorkspace,
agentId: sessionAgentId,
messageProvider: params.messageProvider,
messageChannel: params.messageChannel,
localModelLean: isLocalModelLeanEnabled({
config: params.config,
agentId: sessionAgentId,
}),
toolCount: effectiveTools.length,
clientToolCount: clientToolDefs.length,
});
trajectoryRecorder?.recordEvent(
"trace.metadata",
buildTrajectoryRunMetadata({
env: process.env,
config: params.config,
workspaceDir: effectiveWorkspace,
sessionFile: params.sessionFile,
sessionKey: params.sessionKey,
agentId: sessionAgentId,
trigger: params.trigger,
messageProvider: params.messageProvider,
messageChannel: params.messageChannel,
provider: params.provider,
modelId: params.modelId,
modelApi: params.model.api,
timeoutMs: params.timeoutMs,
fastMode: params.fastMode,
thinkLevel: params.thinkLevel,
reasoningLevel: params.reasoningLevel,
toolResultFormat: params.toolResultFormat,
disableTools: params.disableTools,
toolsAllow: params.toolsAllow,
skillsSnapshot: params.skillsSnapshot,
systemPromptReport,
}),
);
// Rebuild each turn from the session's original stream base so prior-turn
// wrappers do not pin us to stale provider/API transport behavior.
const defaultSessionStreamFn = resolveEmbeddedAgentBaseStreamFn({
session: activeSession,
});
const resolvedTransport = resolveExplicitSettingsTransport({
settingsManager,
sessionTransport: activeSession.agent.transport,
});
const streamExtraParamsOverride = {
...params.streamParams,
fastMode: params.fastMode,
};
const preparedRuntimeExtraParams = params.runtimePlan?.transport.resolveExtraParams({
extraParamsOverride: streamExtraParamsOverride,
thinkingLevel: params.thinkLevel,
agentId: sessionAgentId,
workspaceDir: effectiveWorkspace,
model: params.model,
resolvedTransport,
});
const resolvedExtraParams = resolveExtraParams({
cfg: params.config,
provider: params.provider,
modelId: params.modelId,
agentId: sessionAgentId,
});
const effectiveExtraParams =
preparedRuntimeExtraParams ??
resolvePreparedExtraParams({
cfg: params.config,
provider: params.provider,
modelId: params.modelId,
extraParamsOverride: streamExtraParamsOverride,
thinkingLevel: params.thinkLevel,
agentId: sessionAgentId,
agentDir,
workspaceDir: effectiveWorkspace,
resolvedExtraParams,
model: params.model,
resolvedTransport,
});
const providerStreamFn = registerProviderStreamForModel({
model: params.model,
cfg: params.config,
agentDir,
workspaceDir: effectiveWorkspace,
});
const streamStrategy = describeEmbeddedAgentStreamStrategy({
currentStreamFn: defaultSessionStreamFn,
providerStreamFn,
model: params.model,
resolvedApiKey: params.resolvedApiKey,
});
activeSession.agent.streamFn = resolveEmbeddedAgentStreamFn({
currentStreamFn: defaultSessionStreamFn,
providerStreamFn,
sessionId: params.sessionId,
promptCacheKey: params.promptCacheKey,
signal: runAbortController.signal,
model: params.model,
resolvedApiKey: params.resolvedApiKey,
authProfileId: resolveAttemptStreamAuthProfileId(params),
authStorage: params.authStorage,
});
const providerTextTransforms = resolveProviderTextTransforms({
provider: params.provider,
config: params.config,
workspaceDir: effectiveWorkspace,
runtimeHandle: getProviderRuntimeHandle(),
});
if (providerTextTransforms) {
activeSession.agent.streamFn = wrapStreamFnTextTransforms({
streamFn: activeSession.agent.streamFn,
input: providerTextTransforms.input,
output: providerTextTransforms.output,
transformSystemPrompt: false,
});
}
applyExtraParamsToAgent(
activeSession.agent,
params.config,
params.provider,
params.modelId,
streamExtraParamsOverride,
params.thinkLevel,
sessionAgentId,
effectiveWorkspace,
params.model,
agentDir,
resolvedTransport,
{ preparedExtraParams: effectiveExtraParams },
);
if (codeModeControlsEnabledForRun) {
activeSession.agent.streamFn = createCodexNativeWebSearchWrapper(
activeSession.agent.streamFn,
{
config: params.config,
agentDir,
codeModeToolSurfaceEnabled: true,
},
);
}
const effectivePromptCacheRetention = resolveCacheRetention(
effectiveExtraParams,
params.provider,
params.model.api,
params.modelId,
);
const agentTransportOverride = resolveAgentTransportOverride({
settingsManager,
effectiveExtraParams,
});
const effectiveAgentTransport = agentTransportOverride ?? activeSession.agent.transport;
if (agentTransportOverride && activeSession.agent.transport !== agentTransportOverride) {
const previousTransport = activeSession.agent.transport;
log.debug(
`embedded agent transport override: ${previousTransport} -> ${agentTransportOverride} ` +
`(${params.provider}/${params.modelId})`,
);
}
prepStages.mark("stream-setup");
emitPrepStageSummary("stream-ready");
const cacheObservabilityEnabled = Boolean(cacheTrace) || log.isEnabled("debug");
const promptCacheToolNames = collectPromptCacheToolNames(
allCustomTools as Array<{ name?: string }>,
);
let promptCacheChangesForTurn: PromptCacheChange[] | null = null;
if (cacheTrace) {
cacheTrace.recordStage("session:loaded", {
messages: activeSession.messages,
system: systemPromptText,
note: "after session create",
});
activeSession.agent.streamFn = cacheTrace.wrapStreamFn(activeSession.agent.streamFn);
}
// Anthropic Claude endpoints can reject replayed `thinking` blocks on
// any follow-up provider call, including tool continuations. Sanitize
// outbound messages where policy allows rewriting; otherwise preserve
// latest thinking and let the recovery wrapper retry once without it.
if (transcriptPolicy.dropThinkingBlocks || transcriptPolicy.dropReasoningFromHistory) {
activeSession.agent.streamFn = wrapStreamFnWithMessageTransform(
activeSession.agent.streamFn,
(messages) => {
const reasoningSanitized = transcriptPolicy.dropReasoningFromHistory
? dropReasoningFromHistory(messages)
: messages;
return transcriptPolicy.dropThinkingBlocks
? dropThinkingBlocks(reasoningSanitized)
: reasoningSanitized;
},
);
}
if (
transcriptPolicy.preserveSignatures ||
transcriptPolicy.dropThinkingBlocks ||
transcriptPolicy.dropReasoningFromHistory
) {
activeSession.agent.streamFn = wrapAnthropicStreamWithRecovery(
activeSession.agent.streamFn,
{
id: activeSession.sessionId,
},
);
}
// Mistral (and other strict providers) reject tool call IDs that don't match their
// format requirements (e.g. [a-zA-Z0-9]{9}). sanitizeSessionHistory only processes
// historical messages at attempt start, but the agent loop's internal tool call →
// tool result cycles bypass that path. Wrap streamFn so every outbound request
// sees sanitized tool call IDs.
const isOpenAIResponsesApi =
params.model.api === "openai-responses" ||
params.model.api === "azure-openai-responses" ||
params.model.api === "openai-chatgpt-responses";
const replayToolCallIdSanitizerDecision = {
sanitizeToolCallIds: transcriptPolicy.sanitizeToolCallIds,
toolCallIdMode: transcriptPolicy.toolCallIdMode,
isOpenAIResponsesApi,
};
if (shouldApplyReplayToolCallIdSanitizer(replayToolCallIdSanitizerDecision)) {
const mode = replayToolCallIdSanitizerDecision.toolCallIdMode;
activeSession.agent.streamFn = wrapStreamFnWithMessageTransform(
activeSession.agent.streamFn,
(messages, model) =>
sanitizeReplayToolCallIdsForStream({
messages,
mode,
allowedToolNames: replayAllowedToolNames,
preserveNativeAnthropicToolUseIds: transcriptPolicy.preserveNativeAnthropicToolUseIds,
preserveReplaySafeThinkingToolCallIds: shouldAllowProviderOwnedThinkingReplay({
modelApi: (model as { api?: unknown })?.api as string | null | undefined,
provider: params.provider,
policy: transcriptPolicy,
}),
repairToolUseResultPairing: transcriptPolicy.repairToolUseResultPairing,
}),
);
}
if (isOpenAIResponsesApi) {
activeSession.agent.streamFn = wrapStreamFnWithMessageTransform(
activeSession.agent.streamFn,
(messages) => sanitizeOpenAIResponsesReplayForStream(messages),
);
}
const innerStreamFn = activeSession.agent.streamFn;
activeSession.agent.streamFn = (model, context, options) => {
const signal = runAbortController.signal as AbortSignal & { reason?: unknown };
if (yieldDetected && signal.aborted && signal.reason === "sessions_yield") {
return createYieldAbortedResponse(model) as unknown as Awaited<
ReturnType<typeof innerStreamFn>
>;
}
return innerStreamFn(model, context, options);
};
// Some models emit tool names with surrounding whitespace (e.g. " read ").
// agent runtime dispatches tool calls with exact string matching, so normalize
// names on the live response stream before tool execution.
activeSession.agent.streamFn = wrapStreamFnSanitizeMalformedToolCalls(
activeSession.agent.streamFn,
allowedToolNames,
transcriptPolicy,
params.provider,
);
activeSession.agent.streamFn = wrapStreamFnPromoteStandaloneTextToolCalls(
activeSession.agent.streamFn,
allowedToolNames,
);
activeSession.agent.streamFn = wrapStreamFnTrimToolCallNames(
activeSession.agent.streamFn,
allowedToolNames,
{
unknownToolThreshold: resolveUnknownToolGuardThreshold(clientToolLoopDetection),
},
);
if (
shouldRepairMalformedToolCallArguments({
provider: params.provider,
modelApi: params.model.api,
})
) {
activeSession.agent.streamFn = wrapStreamFnRepairMalformedToolCallArguments(
activeSession.agent.streamFn,
);
}
if (resolveToolCallArgumentsEncoding(params.model) === "html-entities") {
activeSession.agent.streamFn = wrapStreamFnDecodeXaiToolCallArguments(
activeSession.agent.streamFn,
);
}
if (anthropicPayloadLogger) {
activeSession.agent.streamFn = anthropicPayloadLogger.wrapStreamFn(
activeSession.agent.streamFn,
);
}
// Anthropic-compatible providers can add new stop reasons before shared model runtime maps them.
// Recover the known "sensitive" stop reason here so a model refusal does not
// bubble out as an uncaught runner error and stall channel polling.
activeSession.agent.streamFn = wrapStreamFnHandleSensitiveStopReason(
activeSession.agent.streamFn,
);
// Wrap stream with idle timeout detection.
//
// Prefer the caller's explicit `runTimeoutOverrideMs` when provided —
// it carries the "this run was launched with a deliberate per-run
// timeout" signal without losing it when the value numerically equals
// `agents.defaults.timeoutSeconds`. Fall back to the value-equality
// heuristic for callers that haven't been migrated to plumb the flag.
const configuredRunTimeoutMs = resolveAgentTimeoutMs({
cfg: params.config,
});
const resolvedRunTimeoutMs =
params.runTimeoutOverrideMs ??
(params.timeoutMs !== configuredRunTimeoutMs ? params.timeoutMs : undefined);
const idleTimeoutMs = resolveLlmIdleTimeoutMs({
cfg: params.config,
trigger: params.trigger,
runTimeoutMs: resolvedRunTimeoutMs,
modelRequestTimeoutMs: (params.model as { requestTimeoutMs?: number }).requestTimeoutMs,
model: params.model as { baseUrl?: string },
});
if (idleTimeoutMs > 0) {
activeSession.agent.streamFn = streamWithIdleTimeout(
activeSession.agent.streamFn,
idleTimeoutMs,
(error) => idleTimeoutTrigger?.(error),
);
}
let diagnosticModelCallSeq = 0;
activeSession.agent.streamFn = wrapStreamFnWithDiagnosticModelCallEvents(
activeSession.agent.streamFn,
{
runId: params.runId,
...(params.sessionKey && { sessionKey: params.sessionKey }),
...(params.sessionId && { sessionId: params.sessionId }),
provider: params.provider,
model: params.modelId,
api: params.model.api,
transport: effectiveAgentTransport,
...(params.contextWindowInfo?.tokens
? { contextTokenBudget: params.contextWindowInfo.tokens }
: {}),
...(params.contextWindowInfo?.source
? { contextWindowSource: params.contextWindowInfo.source }
: {}),
...(params.contextWindowInfo?.referenceTokens
? { contextWindowReferenceTokens: params.contextWindowInfo.referenceTokens }
: {}),
trace: runTrace,
contentCapture: resolveDiagnosticModelContentCapturePolicy(params.config),
nextCallId: () => `${params.runId}:model:${(diagnosticModelCallSeq += 1)}`,
onStarted: () => {
params.onExecutionPhase?.({
phase: "model_call_started",
provider: params.provider,
model: params.modelId,
firstModelCallStarted: true,
});
},
},
);
try {
if (isRawModelRun) {
activeSession.agent.reset();
setActiveSessionSystemPrompt("");
cacheTrace?.recordStage("session:raw-model-run", {
messages: activeSession.messages,
system: systemPromptText,
});
} else {
const prior = await sanitizeSessionHistory({
messages: activeSession.messages,
modelApi: params.model.api,
modelId: params.modelId,
provider: params.provider,
allowedToolNames: replayAllowedToolNames,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
model: params.model,
sessionManager,
sessionId: params.sessionId,
policy: transcriptPolicy,
});
cacheTrace?.recordStage("session:sanitized", { messages: prior });
const validated = await validateReplayTurns({
messages: prior,
modelApi: params.model.api,
modelId: params.modelId,
provider: params.provider,
config: params.config,
workspaceDir: effectiveWorkspace,
env: process.env,
model: params.model,
sessionId: params.sessionId,
policy: transcriptPolicy,
});
if (params.sessionKey && !isRawModelRun) {
const storePath = resolveStorePath(params.config?.session?.store, {
agentId: sessionAgentId,
});
await runQuotaSuspensionMaintenance({ storePath });
const store = loadSessionStore(storePath, { skipCache: true });
const sessionEntry = store[params.sessionKey];
const suspension = sessionEntry?.quotaSuspension;
if (suspension?.state === "resuming") {
const subagents = Object.values(store)
.filter((s) => s.spawnedBy === sessionEntry.sessionId)
.map((s) => ({
sessionId: s.sessionId,
role: s.subagentRole,
lastStatus: s.status,
}));
const handoffMsg = buildHierarchyReinforcementMessage({
summary: suspension.summary ?? "No recovery briefing was captured.",
activeSubagents: subagents,
});
validated.push(handoffMsg);
await updateSessionStoreEntry({
storePath,
sessionKey: params.sessionKey,
skipMaintenance: true,
takeCacheOwnership: true,
update: async (entry) => {
if (entry.quotaSuspension?.state !== "resuming") {
return null;
}
return {
quotaSuspension: { ...entry.quotaSuspension, state: "active" },
};
},
});
}
}
if (params.sessionKey && params.config && !isRawModelRun) {
const activeSubagentPromptAddition = buildActiveSubagentSystemPromptAddition({
cfg: params.config,
controllerSessionKey: params.sessionKey,
hasSessionsYield: effectiveTools.some((tool) => tool.name === "sessions_yield"),
});
if (activeSubagentPromptAddition) {
setActiveSessionSystemPrompt(
prependSystemPromptAddition({
systemPrompt: systemPromptText,
systemPromptAddition: activeSubagentPromptAddition,
}),
);
}
}
const heartbeatSummary =
params.config && sessionAgentId
? resolveHeartbeatSummaryForAgent(params.config, sessionAgentId)
: undefined;
const heartbeatFiltered = filterHeartbeatTranscriptArtifacts(
validated,
heartbeatSummary?.ackMaxChars,
heartbeatSummary?.prompt,
);
const truncated = limitHistoryTurns(
heartbeatFiltered,
getHistoryLimitFromSessionKey(params.sessionKey, params.config),
);
// Re-run tool_use/tool_result pairing repair after truncation, since
// limitHistoryTurns can orphan tool_result blocks by removing the
// assistant message that contained the matching tool_use.
const limited = transcriptPolicy.repairToolUseResultPairing
? sanitizeToolUseResultPairing(truncated, {
erroredAssistantResultPolicy: "drop",
...(isOpenAIResponsesApi ? { missingToolResultText: "aborted" } : {}),
})
: truncated;
cacheTrace?.recordStage("session:limited", { messages: limited });
if (limited.length > 0 || prior.length > 0) {
activeSession.agent.state.messages = limited;
}
}
if (activeContextEngine) {
try {
// Snapshot before assemble: the assemble contract does not require
// the input array to be treated immutably, so an engine that windows
// history in place would otherwise leave the precheck reading
// already-windowed messages instead of the true pre-assembly state.
const preassemblyContextEngineMessagesForPrecheck = activeSession.messages.slice();
const assembled = await assembleAttemptContextEngine({
contextEngine: activeContextEngine,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
messages: activeSession.messages,
tokenBudget: params.contextTokenBudget,
availableTools: new Set(effectiveTools.map((tool) => tool.name)),
citationsMode: params.config?.memory?.citations,
modelId: params.modelId,
...(params.prompt !== undefined ? { prompt: params.prompt } : {}),
});
if (!assembled) {
throw new Error("context engine assemble returned no result");
}
if (assembled.messages !== activeSession.messages) {
activeSession.agent.state.messages = assembled.messages;
}
contextEnginePromptAuthority = assembled.promptAuthority ?? "assembled";
if (contextEnginePromptAuthority === "preassembly_may_overflow") {
unwindowedContextEngineMessagesForPrecheck =
preassemblyContextEngineMessagesForPrecheck;
}
if (assembled.systemPromptAddition) {
setActiveSessionSystemPrompt(
prependSystemPromptAddition({
systemPrompt: systemPromptText,
systemPromptAddition: assembled.systemPromptAddition,
}),
);
log.debug(
`context engine: prepended system prompt addition (${assembled.systemPromptAddition.length} chars)`,
);
}
} catch (assembleErr) {
log.warn(
`context engine assemble failed, using pipeline messages: ${String(assembleErr)}`,
);
}
}
} catch (err) {
await flushPendingToolResultsAfterIdle({
agent: activeSession?.agent,
sessionManager,
// PERF: If the run was aborted during the setup,
// skip the idle wait and flush pending results synchronously so we can
// immediately dispose the session without orphaning tool calls.
...(params.abortSignal?.aborted ? { timeoutMs: 0 } : {}),
});
activeSession.dispose();
throw err;
}
let yieldAborted = false;
const getAbortReason = (signal: AbortSignal): unknown =>
"reason" in signal ? (signal as { reason?: unknown }).reason : undefined;
const makeTimeoutAbortReason = (): Error => {
const err = new Error("request timed out");
err.name = "TimeoutError";
return err;
};
const abortCompaction = () => {
if (!activeSession.isCompacting) {
return;
}
try {
activeSession.abortCompaction();
} catch (err) {
if (!isProbeSession) {
log.warn(
`embedded run abortCompaction failed: runId=${params.runId} sessionId=${params.sessionId} err=${String(err)}`,
);
}
}
};
const abortRun = (isTimeout = false, reason?: unknown) => {
aborted = true;
if (isTimeout) {
timedOut = true;
if (!timedOutDuringCompaction && countActiveToolExecutions(params.runId) > 0) {
timedOutDuringToolExecution = true;
}
}
if (isTimeout) {
runAbortController.abort(reason ?? makeTimeoutAbortReason());
} else {
runAbortController.abort(reason);
}
abortCompaction();
void abortActiveSession();
if (isTimeout && queueHandleForAbandonment) {
markActiveEmbeddedRunAbandoned({
sessionId: params.sessionId,
handle: queueHandleForAbandonment,
sessionKey: params.sessionKey,
sessionFile: params.sessionFile,
reason: "timeout",
});
void sessionLockController.releaseHeldLockForAbort().catch((err) => {
log.warn(
`failed to release session lock on timeout abort: runId=${params.runId} ${String(err)}`,
);
});
}
};
abortRunForExternalSignal = abortRun;
const idleTimeoutTrigger: ((error: Error) => void) | undefined = (error) => {
idleTimedOut = true;
abortRun(true, error);
};
const abortable = <T>(promise: Promise<T>): Promise<T> =>
abortableWithSignal(runAbortController.signal, promise);
const ownedTranscriptWriteContext = {
sessionFile: params.sessionFile,
sessionKey: params.sessionKey,
withSessionWriteLock: <T>(
operation: () => Promise<T> | T,
options?: { publishOwnedWrite?: boolean },
) => sessionLockController.withSessionWriteLock(operation, options),
};
const promptActiveSession = (
prompt: string,
options?: Parameters<typeof activeSession.prompt>[1],
): Promise<void> =>
withOwnedSessionTranscriptWrites(ownedTranscriptWriteContext, async () =>
abortable(trackPromptSettlePromise(activeSession.prompt(prompt, options))),
);
const onBlockReply = params.onBlockReply
? bindOwnedSessionTranscriptWrites(ownedTranscriptWriteContext, params.onBlockReply)
: undefined;
const onBlockReplyFlush = params.onBlockReplyFlush
? bindOwnedSessionTranscriptWrites(ownedTranscriptWriteContext, params.onBlockReplyFlush)
: undefined;
const subscription = subscribeEmbeddedAgentSession(
buildEmbeddedSubscriptionParams({
session: activeSession,
runId: params.runId,
messageChannel: runtimeChannel,
initialReplayState: params.initialReplayState,
hookRunner: getGlobalHookRunner() ?? undefined,
verboseLevel: params.verboseLevel,
reasoningMode: params.reasoningLevel ?? "off",
thinkingLevel: params.thinkLevel,
toolResultFormat: params.toolResultFormat,
shouldEmitToolResult: params.shouldEmitToolResult,
shouldEmitToolOutput: params.shouldEmitToolOutput,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
onToolResult: params.onToolResult,
onReasoningStream: params.onReasoningStream,
onReasoningEnd: params.onReasoningEnd,
onBlockReply,
onBlockReplyFlush,
blockReplyBreak: params.blockReplyBreak,
blockReplyChunking: params.blockReplyChunking,
onPartialReply: params.onPartialReply,
onAssistantMessageStart: params.onAssistantMessageStart,
onExecutionPhase: params.onExecutionPhase,
onAgentEvent: params.onAgentEvent,
terminalLifecyclePhase: params.deferTerminalLifecycleEnd ? "finishing" : "end",
onBeforeLifecycleTerminal: () => {
// Clear embedded-run activity before emitting terminal lifecycle events so
// post-completion cleanup does not observe a logically finished run as active.
clearActiveEmbeddedRun(
params.sessionId,
queueHandle,
params.sessionKey,
params.sessionFile,
);
},
enforceFinalTag: params.enforceFinalTag,
silentExpected: params.silentExpected,
config: params.config,
sessionKey: sandboxSessionKey,
sessionId: params.sessionId,
agentId: sessionAgentId,
builtinToolNames,
internalEvents: params.internalEvents,
}),
);
const {
assistantTexts,
toolMetas,
getAcceptedSessionSpawns,
runToolLifecycle,
unsubscribe,
waitForCompactionRetry,
isCompactionInFlight,
getItemLifecycle,
getMessagingToolSentTexts,
getMessagingToolSentMediaUrls,
getMessagingToolSentTargets,
getMessagingToolSourceReplyPayloads,
getHeartbeatToolResponse,
getPendingToolMediaReply,
getVisibleBlockReplyCount,
getSuccessfulCronAdds,
getReplayState,
didSendViaMessagingTool,
didSendDeterministicApprovalPrompt,
getLastToolError,
setTerminalLifecycleMeta,
getUsageTotals,
getCompactionCount,
getLastCompactionTokensAfter,
} = subscription;
isCompactionPendingForExternalSignal = subscription.isCompacting;
isCompactionInFlightForExternalSignal = () => activeSession.isCompacting;
toolSearchCatalogExecutor = async (toolParams) => {
try {
const result = await runToolLifecycle({
toolName: toolParams.toolName,
toolCallId: toolParams.toolCallId,
args: toolParams.input,
execute: async () =>
await toolParams.tool.execute(
toolParams.toolCallId,
toolParams.input,
toolParams.signal ?? runAbortController.signal,
toolParams.onUpdate,
undefined as never,
),
});
toolSearchTargetTranscriptProjections.push({
parentToolCallId: toolParams.parentToolCallId,
toolCallId: toolParams.toolCallId,
toolName: toolParams.toolName,
input: toolParams.input,
result,
timestamp: Date.now(),
});
return result;
} catch (error) {
const message = formatErrorMessage(error);
toolSearchTargetTranscriptProjections.push({
parentToolCallId: toolParams.parentToolCallId,
toolCallId: toolParams.toolCallId,
toolName: toolParams.toolName,
input: toolParams.input,
result: {
content: [{ type: "text", text: message }],
details: { status: "error", error: message },
},
isError: true,
timestamp: Date.now(),
});
throw error;
}
};
const abortActiveRunExternally = () => {
externalAbort = true;
abortRun();
};
const queueHandle: EmbeddedAgentQueueHandle & {
kind: "embedded";
cancel: (reason?: "user_abort" | "restart" | "superseded") => void;
} = {
kind: "embedded",
queueMessage: async (text: string, options) => {
if (options?.steeringMode) {
activeSession.agent.steeringMode = options.steeringMode;
}
await steerActiveSessionWithOptionalDeliveryWait(activeSession, text, options);
},
isStreaming: () => activeSession.isStreaming,
isCompacting: () => subscription.isCompacting(),
supportsTranscriptCommitWait: true,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
cancel: abortActiveRunExternally,
abort: abortActiveRunExternally,
};
let lastAssistant: AssistantMessage | undefined;
let currentAttemptAssistant: EmbeddedRunAttemptResult["currentAttemptAssistant"];
let attemptUsage: NormalizedUsage | undefined;
let cacheBreak: PromptCacheBreak | null = null;
let promptCache: EmbeddedRunAttemptResult["promptCache"];
let lastCallUsage: NormalizedUsage | undefined;
let contextBudgetStatus: EmbeddedRunAttemptResult["contextBudgetStatus"];
let compactionOccurredThisAttempt = false;
let finalPromptText: string | undefined;
if (params.replyOperation) {
params.replyOperation.attachBackend(queueHandle);
}
const queueHandleForAbandonment: EmbeddedAgentQueueHandle | undefined = queueHandle;
setActiveEmbeddedRun(params.sessionId, queueHandle, params.sessionKey, params.sessionFile);
let abortWarnTimer: NodeJS.Timeout | undefined;
const isProbeSession = params.sessionId?.startsWith("probe-") ?? false;
const compactionTimeoutMs = resolveCompactionTimeoutMs(params.config);
let abortTimer: NodeJS.Timeout | undefined;
let compactionGraceUsed = false;
const scheduleAbortTimer = (delayMs: number, reason: "initial" | "compaction-grace") => {
abortTimer = setTimeout(
() => {
const timeoutAction = resolveRunTimeoutDuringCompaction({
isCompactionPendingOrRetrying: subscription.isCompacting(),
isCompactionInFlight: activeSession.isCompacting,
graceAlreadyUsed: compactionGraceUsed,
});
if (timeoutAction === "extend") {
compactionGraceUsed = true;
if (!isProbeSession) {
log.warn(
`embedded run timeout reached during compaction; extending deadline: ` +
`runId=${params.runId} sessionId=${params.sessionId} extraMs=${compactionTimeoutMs}`,
);
}
scheduleAbortTimer(compactionTimeoutMs, "compaction-grace");
return;
}
if (!isProbeSession) {
log.warn(
reason === "compaction-grace"
? `embedded run timeout after compaction grace: runId=${params.runId} sessionId=${params.sessionId} timeoutMs=${params.timeoutMs} compactionGraceMs=${compactionTimeoutMs}`
: `embedded run timeout: runId=${params.runId} sessionId=${params.sessionId} timeoutMs=${params.timeoutMs}`,
);
}
if (
shouldFlagCompactionTimeout({
isTimeout: true,
isCompactionPendingOrRetrying: subscription.isCompacting(),
isCompactionInFlight: activeSession.isCompacting,
})
) {
timedOutDuringCompaction = true;
}
abortRun(true);
if (!abortWarnTimer) {
abortWarnTimer = setTimeout(() => {
if (!activeSession.isStreaming) {
return;
}
if (!isProbeSession) {
log.warn(
`embedded run abort still streaming: runId=${params.runId} sessionId=${params.sessionId}`,
);
}
}, 10_000);
}
},
Math.max(1, delayMs),
);
};
scheduleAbortTimer(params.timeoutMs, "initial");
let messagesSnapshot: AgentMessage[] = [];
let sessionIdUsed = activeSession.sessionId;
let sessionFileUsed: string | undefined = params.sessionFile;
const onAbort = () => {
externalAbort = true;
const reason = params.abortSignal ? getAbortReason(params.abortSignal) : undefined;
const timeout = reason ? isTimeoutError(reason) : false;
if (
shouldFlagCompactionTimeout({
isTimeout: timeout,
isCompactionPendingOrRetrying: subscription.isCompacting(),
isCompactionInFlight: activeSession.isCompacting,
})
) {
timedOutDuringCompaction = true;
}
abortRun(timeout, reason);
};
if (params.abortSignal) {
if (params.abortSignal.aborted) {
onAbort();
} else {
params.abortSignal.addEventListener("abort", onAbort, {
once: true,
});
}
}
// Hook runner was already obtained earlier before tool creation
const hookAgentId = sessionAgentId;
const activeSessionManager = sessionManager;
let preflightRecovery: EmbeddedRunAttemptResult["preflightRecovery"];
let promptErrorSource: EmbeddedRunAttemptResult["promptErrorSource"] = null;
const handleMidTurnPrecheckRequest = (request: MidTurnPrecheckRequest) => {
const logMidTurnPrecheck = (route: string, extra?: string) => {
log.warn(
`[context-overflow-midturn-precheck] sessionKey=${params.sessionKey ?? params.sessionId} ` +
`provider=${params.provider}/${params.modelId} route=${route} ` +
`estimatedPromptTokens=${request.estimatedPromptTokens} ` +
`promptBudgetBeforeReserve=${request.promptBudgetBeforeReserve} ` +
`overflowTokens=${request.overflowTokens} ` +
`toolResultReducibleChars=${request.toolResultReducibleChars} ` +
`effectiveReserveTokens=${request.effectiveReserveTokens} ` +
`prePromptMessageCount=${prePromptMessageCount} ` +
(extra ? `${extra} ` : "") +
`sessionFile=${params.sessionFile}`,
);
};
if (request.route === "truncate_tool_results_only") {
const contextTokenBudget = params.contextTokenBudget ?? DEFAULT_CONTEXT_TOKENS;
const toolResultMaxChars = resolveLiveToolResultMaxChars({
contextWindowTokens: contextTokenBudget,
cfg: params.config,
agentId: sessionAgentId,
});
const truncationResult = truncateOversizedToolResultsInSessionManager({
sessionManager: activeSessionManager,
contextWindowTokens: contextTokenBudget,
maxCharsOverride: toolResultMaxChars,
sessionFile: params.sessionFile,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: sessionAgentId,
});
if (truncationResult.truncated) {
preflightRecovery = {
route: "truncate_tool_results_only",
source: "mid-turn",
handled: true,
truncatedCount: truncationResult.truncatedCount,
};
const sessionContext = activeSessionManager.buildSessionContext();
activeSession.agent.state.messages = sessionContext.messages;
logMidTurnPrecheck(
request.route,
`handled=true truncatedCount=${truncationResult.truncatedCount}`,
);
} else {
preflightRecovery = { route: "compact_only", source: "mid-turn" };
promptError = new Error(PREEMPTIVE_OVERFLOW_ERROR_TEXT);
promptErrorSource = "precheck";
logMidTurnPrecheck(
"compact_only",
`truncateFallbackReason=${truncationResult.reason ?? "unknown"}`,
);
}
} else {
preflightRecovery = { route: request.route, source: "mid-turn" };
promptError = new Error(PREEMPTIVE_OVERFLOW_ERROR_TEXT);
promptErrorSource = "precheck";
logMidTurnPrecheck(request.route);
}
};
let skipPromptSubmission = false;
try {
const promptStartedAt = Date.now();
if (emptyExplicitToolAllowlistError) {
promptError = emptyExplicitToolAllowlistError;
promptErrorSource = "precheck";
skipPromptSubmission = true;
log.warn(`[tools] ${emptyExplicitToolAllowlistError.message}`);
}
// Run before_prompt_build hooks to allow plugins to inject prompt context.
// Legacy compatibility: before_agent_start is also checked for context fields.
let effectivePrompt = params.prompt;
const hookCtx = {
runId: params.runId,
trace: freezeDiagnosticTraceContext(diagnosticTrace),
agentId: hookAgentId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
workspaceDir: params.workspaceDir,
modelProviderId: params.model.provider,
modelId: params.model.id,
trigger: params.trigger,
...buildAgentHookContextChannelFields(params),
};
const promptBuildMessages =
pruneProcessedHistoryImages(activeSession.messages) ?? activeSession.messages;
const hookResult = isRawModelRun
? undefined
: await resolvePromptBuildHookResult({
config: params.config ?? getRuntimeConfig(),
prompt: params.prompt,
messages: promptBuildMessages,
hookCtx,
hookRunner,
beforeAgentStartResult: params.beforeAgentStartResult,
});
const promptBeforePromptBuildHooks = effectivePrompt;
const promptBuildPrependContext = hookResult?.prependContext;
const promptBuildAppendContext = hookResult?.appendContext;
const hasPromptBuildContext =
Boolean(promptBuildPrependContext?.trim()) || Boolean(promptBuildAppendContext?.trim());
{
if (hookResult?.prependContext) {
effectivePrompt = `${hookResult.prependContext}\n\n${effectivePrompt}`;
log.debug(
`hooks: prepended context to prompt (${hookResult.prependContext.length} chars)`,
);
}
if (hookResult?.appendContext) {
effectivePrompt = `${effectivePrompt}\n\n${hookResult.appendContext}`;
log.debug(
`hooks: appended context to prompt (${hookResult.appendContext.length} chars)`,
);
}
const legacySystemPrompt = normalizeOptionalString(hookResult?.systemPrompt) ?? "";
if (legacySystemPrompt) {
setActiveSessionSystemPrompt(legacySystemPrompt);
log.debug(`hooks: applied systemPrompt (${legacySystemPrompt.length} chars)`);
}
const prependedOrAppendedSystemPrompt = composeSystemPromptWithHookContext({
baseSystemPrompt: systemPromptText,
prependSystemContext: hookResult?.prependSystemContext,
appendSystemContext: hookResult?.appendSystemContext,
});
if (prependedOrAppendedSystemPrompt) {
const prependSystemLen = hookResult?.prependSystemContext?.trim().length ?? 0;
const appendSystemLen = hookResult?.appendSystemContext?.trim().length ?? 0;
setActiveSessionSystemPrompt(prependedOrAppendedSystemPrompt);
log.debug(
`hooks: applied prependSystemContext/appendSystemContext (${prependSystemLen}+${appendSystemLen} chars)`,
);
}
const mediaTaskSystemPromptAddition = resolveAttemptMediaTaskSystemPromptAddition({
sessionKey: params.sessionKey,
trigger: params.trigger,
});
if (mediaTaskSystemPromptAddition) {
setActiveSessionSystemPrompt(
prependSystemPromptAddition({
systemPrompt: ensureSystemPromptCacheBoundary(systemPromptText),
systemPromptAddition: mediaTaskSystemPromptAddition,
}),
);
}
}
// The model identity line is appended below; for a marker-free hook systemPrompt
// override ensure the cache boundary first so the identity lands in the dynamic
// suffix, not the cached prefix — otherwise an idle turn's prefix (O + identity)
// diverges from an active media turn's prefix (O) and breaks prompt caching. Skip
// empty prompts (raw/gateway runs) and turns with no identity line, which need none.
const modelAwareSystemPrompt = appendModelIdentitySystemPrompt({
systemPrompt:
buildModelIdentityPromptLine(runtimeInfo.model) && systemPromptText.trim().length > 0
? ensureSystemPromptCacheBoundary(systemPromptText)
: systemPromptText,
model: runtimeInfo.model,
});
if (modelAwareSystemPrompt !== systemPromptText) {
setActiveSessionSystemPrompt(modelAwareSystemPrompt);
}
if (cacheObservabilityEnabled) {
const cacheObservation = beginPromptCacheObservation({
sessionId: params.sessionId,
promptCacheKey: params.promptCacheKey,
sessionKey: params.sessionKey,
provider: params.provider,
modelId: params.modelId,
modelApi: params.model.api,
cacheRetention: effectivePromptCacheRetention,
streamStrategy,
transport: effectiveAgentTransport,
systemPrompt: systemPromptText,
toolNames: promptCacheToolNames,
});
promptCacheChangesForTurn = cacheObservation.changes;
cacheTrace?.recordStage("cache:state", {
options: {
snapshot: cacheObservation.snapshot,
previousCacheRead: cacheObservation.previousCacheRead ?? undefined,
changes:
cacheObservation.changes?.map((change) => ({
code: change.code,
detail: change.detail,
})) ?? undefined,
},
});
}
const routingSummary = describeProviderRequestRoutingSummary({
provider: params.provider,
api: params.model.api,
baseUrl: params.model.baseUrl,
capability: "llm",
transport: "stream",
});
log.debug(
`embedded run prompt start: runId=${params.runId} sessionId=${params.sessionId} ` +
routingSummary,
);
const effectiveTranscriptPrompt =
params.transcriptPrompt === undefined ? undefined : params.transcriptPrompt;
let transcriptPromptForRuntimeSplit = effectiveTranscriptPrompt;
let promptForRuntimeContextSplit = promptBeforePromptBuildHooks;
// Repair orphaned trailing user messages so new prompts don't violate role ordering.
const leafEntry = isRawModelRun ? null : sessionManager.getLeafEntry();
if (leafEntry?.type === "message" && leafEntry.message.role === "user") {
const messageMergeStrategy = resolveMessageMergeStrategy();
const orphanPromptMerge = messageMergeStrategy.mergeOrphanedTrailingUserPrompt({
prompt: effectivePrompt,
trigger: params.trigger,
leafMessage: leafEntry.message,
});
const runtimePromptMerge = messageMergeStrategy.mergeOrphanedTrailingUserPrompt({
prompt: promptForRuntimeContextSplit,
trigger: params.trigger,
leafMessage: leafEntry.message,
});
const transcriptPromptMerge =
effectiveTranscriptPrompt === undefined
? undefined
: messageMergeStrategy.mergeOrphanedTrailingUserPrompt({
prompt: effectiveTranscriptPrompt,
trigger: params.trigger,
leafMessage: leafEntry.message,
});
effectivePrompt = orphanPromptMerge.prompt;
promptForRuntimeContextSplit = runtimePromptMerge.prompt;
if (transcriptPromptMerge) {
transcriptPromptForRuntimeSplit = transcriptPromptMerge.prompt;
}
if (orphanPromptMerge.removeLeaf) {
if (leafEntry.parentId) {
sessionManager.branch(leafEntry.parentId);
} else {
sessionManager.resetLeaf();
}
const sessionContext = sessionManager.buildSessionContext();
activeSession.agent.state.messages = sessionContext.messages;
}
const orphanRepairMessage =
`${
orphanPromptMerge.removeLeaf
? orphanPromptMerge.merged
? "Merged and removed"
: "Removed already-queued"
: "Preserved"
} orphaned user message` +
(orphanPromptMerge.removeLeaf
? " to prevent consecutive user turns. "
: " without removing the active session leaf. ") +
`runId=${params.runId} sessionId=${params.sessionId} trigger=${params.trigger}`;
if (shouldWarnOnOrphanedUserRepair(params.trigger)) {
log.warn(orphanRepairMessage);
} else {
log.debug(orphanRepairMessage);
}
}
const promptForModelBeforeRuntimeContextSplit = effectivePrompt;
if (!isRawModelRun) {
promptForRuntimeContextSplit = annotateInterSessionPromptText(
promptForRuntimeContextSplit,
params.inputProvenance,
);
}
const transcriptLeafId =
(sessionManager.getLeafEntry() as { id?: string } | null | undefined)?.id ?? null;
const heartbeatSummary =
params.config && sessionAgentId
? resolveHeartbeatSummaryForAgent(params.config, sessionAgentId)
: undefined;
try {
const filteredMessages = filterHeartbeatTranscriptArtifacts(
activeSession.messages,
heartbeatSummary?.ackMaxChars,
heartbeatSummary?.prompt,
);
if (filteredMessages.length < activeSession.messages.length) {
activeSession.agent.state.messages = filteredMessages;
}
prePromptMessageCount = activeSession.messages.length;
const contextTokenBudget = params.contextTokenBudget ?? DEFAULT_CONTEXT_TOKENS;
const promptToolResultMaxChars = resolveLiveToolResultMaxChars({
contextWindowTokens: contextTokenBudget,
cfg: params.config,
agentId: sessionAgentId,
});
let promptHistoryMessages = activeSession.messages;
const promptToolResultTruncation = truncateOversizedToolResultsInMessages(
activeSession.messages,
contextTokenBudget,
promptToolResultMaxChars,
promptToolResultMaxChars * PROMPT_TOOL_RESULT_AGGREGATE_CAP_MULTIPLIER,
);
if (promptToolResultTruncation.truncatedCount > 0) {
promptHistoryMessages = promptToolResultTruncation.messages;
log.info(
`[tool-result-truncation] Truncated ${promptToolResultTruncation.truncatedCount} ` +
`tool result(s) for prompt history ` +
`(maxChars=${promptToolResultMaxChars} ` +
`aggregateBudgetChars=${
promptToolResultMaxChars * PROMPT_TOOL_RESULT_AGGREGATE_CAP_MULTIPLIER
}) ` +
`sessionKey=${params.sessionKey ?? params.sessionId ?? "unknown"}`,
);
}
const promptSubmission = resolveRuntimeContextPromptParts({
effectivePrompt: promptForRuntimeContextSplit,
transcriptPrompt: transcriptPromptForRuntimeSplit,
modelPrompt: hasPromptBuildContext
? promptForModelBeforeRuntimeContextSplit
: undefined,
emptyTranscriptMode: params.suppressNextUserMessagePersistence
? "model-prompt"
: "runtime-event",
});
const promptForSession = buildCurrentInboundPrompt({
context: params.currentInboundContext,
prompt: promptSubmission.prompt,
});
const promptForModel = buildCurrentInboundPrompt({
context: params.currentInboundContext,
prompt: promptSubmission.modelPrompt ?? promptSubmission.prompt,
});
const runtimeSystemContext = promptSubmission.runtimeSystemContext?.trim();
if (promptSubmission.runtimeOnly && runtimeSystemContext) {
const runtimeSystemPrompt = composeSystemPromptWithHookContext({
baseSystemPrompt: systemPromptText,
appendSystemContext: runtimeSystemContext,
});
if (runtimeSystemPrompt) {
setActiveSessionSystemPrompt(runtimeSystemPrompt);
}
}
const runtimeContextForHook = promptSubmission.runtimeOnly
? undefined
: promptSubmission.runtimeContext?.trim();
const runtimeContextMessageForCurrentTurn =
buildRuntimeContextCustomMessage(runtimeContextForHook);
const messagesForCurrentPrompt = runtimeContextMessageForCurrentTurn
? [...promptHistoryMessages, runtimeContextMessageForCurrentTurn]
: promptHistoryMessages;
const hookMessagesForCurrentPrompt = normalizeMessagesForCurrentPromptBoundary({
messages: messagesForCurrentPrompt,
prompt: promptForModel,
});
if (systemPromptReport) {
systemPromptReport.currentTurn = {
...(params.currentInboundEventKind ? { kind: params.currentInboundEventKind } : {}),
promptChars: promptForModel.length,
runtimeContextChars: promptSubmission.runtimeOnly
? (runtimeSystemContext?.length ?? 0)
: (runtimeContextForHook?.length ?? 0),
};
}
const systemPromptForHook = systemPromptText;
const persistBlockedBeforeAgentRun = async (block: {
message: string;
pluginId: string;
}): Promise<boolean> => {
const idempotencyKey = `hook-block:before_agent_run:user:${params.runId}`;
if (sessionMessagesContainIdempotencyKey(activeSession.messages, idempotencyKey)) {
return true;
}
const nowMs = Date.now();
const redactedUserMessage = {
role: "user" as const,
content: [{ type: "text" as const, text: block.message }],
timestamp: nowMs,
idempotencyKey,
__openclaw: {
beforeAgentRunBlocked: {
blockedBy: block.pluginId,
blockedAt: nowMs,
},
},
};
try {
activeSessionManager.appendMessage(
redactedUserMessage as Parameters<typeof activeSessionManager.appendMessage>[0],
);
flushSessionManagerFile(activeSessionManager);
activeSession.agent.state.messages =
activeSessionManager.buildSessionContext().messages;
return true;
} catch (err) {
log.warn(
`before_agent_run block: failed to persist redacted user message: ${
(err as Error)?.message ?? String(err)
}`,
);
return false;
}
};
if (hookRunner?.hasHooks("before_agent_run")) {
const beforeRunMessages = cloneHookMessages(hookMessagesForCurrentPrompt);
let beforeRunResult:
| Awaited<ReturnType<NonNullable<typeof hookRunner>["runBeforeAgentRun"]>>
| undefined;
try {
beforeRunResult = await hookRunner.runBeforeAgentRun(
{
prompt: promptForModel,
systemPrompt: systemPromptForHook,
messages: beforeRunMessages,
channelId: hookCtx.channelId,
accountId: params.agentAccountId ?? undefined,
senderId: params.senderId ?? undefined,
senderIsOwner: params.senderIsOwner ?? undefined,
},
hookCtx,
);
} catch {
log.warn("before_agent_run hook failed; blocking request");
beforeAgentRunBlocked = true;
beforeAgentRunBlockedBy = "before_agent_run";
await persistBlockedBeforeAgentRun({
message: resolveBlockMessage(
{ outcome: "block", reason: "before_agent_run hook failed" },
{ blockedBy: "before_agent_run" },
),
pluginId: "before_agent_run",
});
promptError = new Error(
resolveBlockMessage(
{ outcome: "block", reason: "before_agent_run hook failed" },
{ blockedBy: "before_agent_run" },
),
);
promptErrorSource = "hook:before_agent_run";
skipPromptSubmission = true;
}
const beforeRunDecision = beforeRunResult?.decision;
const beforeRunPluginId = beforeRunResult?.pluginId ?? "unknown";
if (beforeRunDecision?.outcome === "block") {
beforeAgentRunBlocked = true;
beforeAgentRunBlockedBy = beforeRunPluginId;
const blockReplacementMsg = resolveBlockMessage(beforeRunDecision, {
blockedBy: beforeRunPluginId,
});
log.warn(`before_agent_run hook blocked by ${beforeRunPluginId}`);
await persistBlockedBeforeAgentRun({
message: blockReplacementMsg,
pluginId: beforeRunPluginId,
});
promptError = new Error(blockReplacementMsg);
promptErrorSource = "hook:before_agent_run";
skipPromptSubmission = true;
}
}
if (!skipPromptSubmission) {
const googlePromptCacheStreamFn = await prepareGooglePromptCacheStreamFn({
apiKey: await resolveEmbeddedAgentApiKey({
provider: params.provider,
resolvedApiKey: params.resolvedApiKey,
authStorage: params.authStorage,
}),
extraParams: effectiveExtraParams,
model: params.model,
modelId: params.modelId,
provider: params.provider,
sessionManager: {
appendCustomEntry: async (customType, data) => {
await sessionLockController.withSessionWriteLock(() => {
activeSessionManager.appendCustomEntry(customType, data);
});
},
getEntries: () => activeSessionManager.getEntries(),
},
signal: runAbortController.signal,
streamFn: activeSession.agent.streamFn,
systemPrompt: systemPromptText,
});
if (googlePromptCacheStreamFn) {
activeSession.agent.streamFn = googlePromptCacheStreamFn;
}
installPromptSubmissionLockRelease({
session: activeSession,
waitForSessionEvents: (sessionToDrain) =>
sessionLockController.waitForSessionEvents(sessionToDrain),
releaseForPrompt: () => sessionLockController.releaseForPrompt(),
reacquireAfterPrompt: () => sessionLockController.reacquireAfterPrompt(),
sessionKey: params.sessionKey,
sessionFile: params.sessionFile,
withSessionWriteLock: (run) => sessionLockController.withSessionWriteLock(run),
});
}
// Detect and load images referenced in the visible prompt for vision-capable models.
// Images are prompt-local only.
const imageResult = skipPromptSubmission
? {
images: [],
detectedRefs: [],
loadedCount: 0,
skippedCount: 0,
}
: await detectAndLoadPromptImages({
prompt: promptSubmission.prompt,
workspaceDir: effectiveWorkspace,
model: params.model,
existingImages: params.images,
imageOrder: params.imageOrder,
maxBytes: MAX_IMAGE_BYTES,
maxDimensionPx: resolveImageSanitizationLimits(params.config).maxDimensionPx,
workspaceOnly: effectiveFsWorkspaceOnly,
// Enforce sandbox path restrictions when sandbox is enabled
sandbox:
sandbox?.enabled && sandbox?.fsBridge
? { root: sandbox.workspaceDir, bridge: sandbox.fsBridge }
: undefined,
});
if (!skipPromptSubmission) {
cacheTrace?.recordStage("prompt:before", {
prompt: promptForModel,
messages: activeSession.messages,
});
cacheTrace?.recordStage("prompt:images", {
prompt: promptForModel,
messages: activeSession.messages,
note: `images: prompt=${imageResult.images.length}`,
});
const trajectoryProviderVisibleTools = toTrajectoryToolDefinitions(effectiveTools);
trajectoryRecorder?.recordEvent("context.compiled", {
systemPrompt: systemPromptForHook,
prompt: promptForModel,
messages: activeSession.messages,
tools: toTrajectoryToolDefinitions(
toolSearch.compacted ? uncompactedEffectiveTools : effectiveTools,
),
...(toolSearch.compacted
? { providerVisibleTools: trajectoryProviderVisibleTools }
: {}),
imagesCount: imageResult.images.length,
streamStrategy,
transport: effectiveAgentTransport,
transcriptLeafId,
});
}
const promptSkipReason = skipPromptSubmission
? null
: resolvePromptSubmissionSkipReason({
prompt: promptForModel,
messages: activeSession.messages,
runtimeOnly: promptSubmission.runtimeOnly,
imageCount: imageResult.images.length,
});
if (promptSkipReason) {
skipPromptSubmission = true;
const skipContext =
`runId=${params.runId} sessionId=${params.sessionId} trigger=${params.trigger} ` +
`provider=${params.provider}/${params.modelId}`;
if (promptSkipReason === "blank_user_prompt") {
log.warn(`embedded run prompt skipped: blank user prompt ${skipContext}`);
} else {
log.info(`embedded run prompt skipped: empty prompt/history/images ${skipContext}`);
}
trajectoryRecorder?.recordEvent("prompt.skipped", {
reason: promptSkipReason,
prompt: promptForModel,
messages: activeSession.messages,
imagesCount: imageResult.images.length,
});
}
const msgCount = activeSession.messages.length;
const systemLen = systemPromptText?.length ?? 0;
const promptLen = effectivePrompt.length;
const sessionSummary = summarizeSessionContext(activeSession.messages);
const reserveTokens = settingsManager.getCompactionReserveTokens();
emitTrustedDiagnosticEvent({
type: "context.assembled",
runId: params.runId,
...(params.sessionKey && { sessionKey: params.sessionKey }),
...(params.sessionId && { sessionId: params.sessionId }),
provider: params.provider,
model: params.modelId,
...((params.messageChannel ?? params.messageProvider)
? { channel: params.messageChannel ?? params.messageProvider }
: {}),
trigger: params.trigger,
messageCount: msgCount,
historyTextChars: sessionSummary.totalTextChars,
historyImageBlocks: sessionSummary.totalImageBlocks,
maxMessageTextChars: sessionSummary.maxMessageTextChars,
systemPromptChars: systemLen,
promptChars: promptLen,
promptImages: imageResult.images.length,
contextTokenBudget,
reserveTokens,
trace: freezeDiagnosticTraceContext(createChildDiagnosticTraceContext(runTrace)),
});
params.onExecutionPhase?.({
phase: "context_assembled",
provider: params.provider,
model: params.modelId,
});
// Diagnostic: log context sizes before prompt to help debug early overflow errors.
if (log.isEnabled("debug")) {
log.debug(
`[context-diag] pre-prompt: sessionKey=${params.sessionKey ?? params.sessionId} ` +
`messages=${msgCount} roleCounts=${sessionSummary.roleCounts} ` +
`historyTextChars=${sessionSummary.totalTextChars} ` +
`maxMessageTextChars=${sessionSummary.maxMessageTextChars} ` +
`historyImageBlocks=${sessionSummary.totalImageBlocks} ` +
`systemPromptChars=${systemLen} promptChars=${promptLen} ` +
`promptImages=${imageResult.images.length} ` +
`provider=${params.provider}/${params.modelId} sessionFile=${params.sessionFile}`,
);
}
if (!skipPromptSubmission && !isRawModelRun && hookRunner?.hasHooks("llm_input")) {
hookRunner
.runLlmInput(
{
runId: params.runId,
sessionId: params.sessionId,
provider: params.provider,
model: params.modelId,
systemPrompt: systemPromptForHook,
prompt: promptForModel,
historyMessages: cloneHookMessages(hookMessagesForCurrentPrompt),
imagesCount: imageResult.images.length,
tools,
},
{
runId: params.runId,
trace: freezeDiagnosticTraceContext(diagnosticTrace),
agentId: hookAgentId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
workspaceDir: params.workspaceDir,
trigger: params.trigger,
...buildAgentHookContextChannelFields(params),
},
)
.catch((err) => {
log.warn(`llm_input hook failed: ${String(err)}`);
});
}
const preemptiveCompaction = skipPromptSubmission
? null
: shouldPreemptivelyCompactBeforePrompt({
messages: messagesForCurrentPrompt,
...(contextEnginePromptAuthority === "preassembly_may_overflow"
? { unwindowedMessages: unwindowedContextEngineMessagesForPrecheck }
: {}),
systemPrompt: systemPromptForHook,
prompt: promptForModel,
contextTokenBudget,
reserveTokens,
toolResultMaxChars: promptToolResultMaxChars,
});
if (preemptiveCompaction) {
contextBudgetStatus = buildPrePromptContextBudgetStatus({
result: preemptiveCompaction,
provider: params.provider,
modelId: params.modelId,
messageCount: activeSession.messages.length,
contextTokenBudget,
reserveTokens,
...(params.sessionId ? { sessionId: params.sessionId } : {}),
...(contextEnginePromptAuthority === "preassembly_may_overflow" &&
unwindowedContextEngineMessagesForPrecheck
? { unwindowedMessageCount: unwindowedContextEngineMessagesForPrecheck.length }
: {}),
});
log.debug(
formatPrePromptPrecheckLog({
result: preemptiveCompaction,
provider: params.provider,
modelId: params.modelId,
messageCount: activeSession.messages.length,
contextTokenBudget,
reserveTokens,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.sessionId ? { sessionId: params.sessionId } : {}),
...(contextEnginePromptAuthority === "preassembly_may_overflow" &&
unwindowedContextEngineMessagesForPrecheck
? { unwindowedMessageCount: unwindowedContextEngineMessagesForPrecheck.length }
: {}),
...(params.sessionFile ? { sessionFile: params.sessionFile } : {}),
}),
);
}
if (preemptiveCompaction?.route === "truncate_tool_results_only") {
const toolResultMaxChars = resolveLiveToolResultMaxChars({
contextWindowTokens: contextTokenBudget,
cfg: params.config,
agentId: sessionAgentId,
});
const truncationResult = truncateOversizedToolResultsInSessionManager({
sessionManager,
contextWindowTokens: contextTokenBudget,
maxCharsOverride: toolResultMaxChars,
sessionFile: params.sessionFile,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: sessionAgentId,
});
if (truncationResult.truncated) {
preflightRecovery = {
route: "truncate_tool_results_only",
handled: true,
truncatedCount: truncationResult.truncatedCount,
};
log.info(
`[context-overflow-precheck] early tool-result truncation succeeded for ` +
`${params.provider}/${params.modelId} route=${preemptiveCompaction.route} ` +
`truncatedCount=${truncationResult.truncatedCount} ` +
`estimatedPromptTokens=${preemptiveCompaction.estimatedPromptTokens} ` +
`promptBudgetBeforeReserve=${preemptiveCompaction.promptBudgetBeforeReserve} ` +
`overflowTokens=${preemptiveCompaction.overflowTokens} ` +
`toolResultReducibleChars=${preemptiveCompaction.toolResultReducibleChars} ` +
`effectiveReserveTokens=${preemptiveCompaction.effectiveReserveTokens} ` +
`sessionFile=${params.sessionFile}`,
);
skipPromptSubmission = true;
}
if (!skipPromptSubmission) {
log.warn(
`[context-overflow-precheck] early tool-result truncation did not help for ` +
`${params.provider}/${params.modelId}; falling back to compaction ` +
`reason=${truncationResult.reason ?? "unknown"} sessionFile=${params.sessionFile}`,
);
preflightRecovery = { route: "compact_only" };
promptError = new Error(PREEMPTIVE_OVERFLOW_ERROR_TEXT);
promptErrorSource = "precheck";
skipPromptSubmission = true;
}
}
if (preemptiveCompaction?.shouldCompact) {
preflightRecovery =
preemptiveCompaction.route === "compact_then_truncate"
? { route: "compact_then_truncate" }
: { route: "compact_only" };
promptError = new Error(PREEMPTIVE_OVERFLOW_ERROR_TEXT);
promptErrorSource = "precheck";
log.warn(
`[context-overflow-precheck] sessionKey=${params.sessionKey ?? params.sessionId} ` +
`provider=${params.provider}/${params.modelId} ` +
`route=${preemptiveCompaction.route} ` +
`estimatedPromptTokens=${preemptiveCompaction.estimatedPromptTokens} ` +
`promptBudgetBeforeReserve=${preemptiveCompaction.promptBudgetBeforeReserve} ` +
`overflowTokens=${preemptiveCompaction.overflowTokens} ` +
`toolResultReducibleChars=${preemptiveCompaction.toolResultReducibleChars} ` +
`reserveTokens=${reserveTokens} ` +
`effectiveReserveTokens=${preemptiveCompaction.effectiveReserveTokens} ` +
`sessionFile=${params.sessionFile}`,
);
skipPromptSubmission = true;
}
if (!skipPromptSubmission) {
const normalizedReplayMessages = normalizeAssistantReplayContent(
activeSession.messages,
);
if (normalizedReplayMessages !== activeSession.messages) {
activeSession.agent.state.messages = normalizedReplayMessages;
}
const installProviderPromptHistoryTransform = (): (() => void) => {
const baseStreamFn = activeSession.agent.streamFn;
const providerPromptStreamFn = wrapStreamFnWithMessageTransform(
baseStreamFn,
(messages) => {
const providerPromptHistoryTruncation = truncateOversizedToolResultsInMessages(
messages,
contextTokenBudget,
promptToolResultMaxChars,
promptToolResultMaxChars * PROMPT_TOOL_RESULT_AGGREGATE_CAP_MULTIPLIER,
);
return providerPromptHistoryTruncation.truncatedCount > 0
? providerPromptHistoryTruncation.messages
: messages;
},
);
activeSession.agent.streamFn = providerPromptStreamFn;
return () => {
if (activeSession.agent.streamFn === providerPromptStreamFn) {
activeSession.agent.streamFn = baseStreamFn;
}
};
};
finalPromptText = promptForSession;
trajectoryRecorder?.recordEvent("prompt.submitted", {
prompt: promptForModel,
systemPrompt: systemPromptForHook,
messages: activeSession.messages,
imagesCount: imageResult.images.length,
});
const btwSnapshotMessages = normalizedReplayMessages.slice(-MAX_BTW_SNAPSHOT_MESSAGES);
updateActiveEmbeddedRunSnapshot(params.sessionId, {
transcriptLeafId,
messages: btwSnapshotMessages,
inFlightPrompt: promptForSession,
});
let captureCurrentPromptForModel = false;
const cleanupModelPromptTransform = installModelPromptTransform({
session: activeSession,
transcriptPrompt: promptForSession,
modelPrompt: promptForModel,
prependContext: promptBuildPrependContext,
appendContext: promptBuildAppendContext,
shouldCapturePrompt: () => captureCurrentPromptForModel,
});
const armModelPromptTransform = (submitted: boolean) => {
if (submitted) {
captureCurrentPromptForModel = true;
}
};
const cleanupProviderPromptHistoryTransform = installProviderPromptHistoryTransform();
try {
if (promptSubmission.runtimeOnly) {
await promptActiveSession(promptForSession, {
preflightResult: armModelPromptTransform,
});
} else {
const cleanupRuntimeContextMessage = installRuntimeContextMessageForPrompt({
session: activeSession,
message: runtimeContextMessageForCurrentTurn,
});
try {
// Only pass images option if there are actually images to pass
// This avoids potential issues with models that don't expect the images parameter
if (imageResult.images.length > 0) {
await promptActiveSession(promptForSession, {
images: imageResult.images,
preflightResult: armModelPromptTransform,
});
} else {
await promptActiveSession(promptForSession, {
preflightResult: armModelPromptTransform,
});
}
} finally {
cleanupRuntimeContextMessage();
}
}
} finally {
cleanupProviderPromptHistoryTransform();
cleanupModelPromptTransform();
}
}
} catch (err) {
yieldAborted =
yieldDetected &&
isRunnerAbortError(err) &&
err instanceof Error &&
err.cause === "sessions_yield";
cleanupYieldAborted = yieldAborted;
if (yieldAborted) {
aborted = false;
await waitForSessionsYieldAbortSettle({
settlePromise: yieldAbortSettled,
runId: params.runId,
sessionId: params.sessionId,
});
await sessionLockController.releaseHeldLockForAbort();
await sessionLockController.waitForSessionEvents(activeSession);
await sessionLockController.withSessionWriteLock(async () => {
stripSessionsYieldArtifacts(activeSession);
if (yieldMessage) {
await persistSessionsYieldContextMessage(activeSession, yieldMessage);
}
});
} else if (isMidTurnPrecheckSignal(err)) {
await sessionLockController.waitForSessionEvents(activeSession);
await sessionLockController.withSessionWriteLock(() => {
handleMidTurnPrecheckRequest(err.request);
});
} else {
promptError = err;
promptErrorSource = "prompt";
}
} finally {
log.debug(
`embedded run prompt end: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - promptStartedAt}`,
);
}
if (pendingMidTurnPrecheckRequest) {
const request = pendingMidTurnPrecheckRequest;
pendingMidTurnPrecheckRequest = null;
await sessionLockController.waitForSessionEvents(activeSession);
await sessionLockController.withSessionWriteLock(() => {
removeTrailingMidTurnPrecheckAssistantError({
activeSession,
sessionManager: activeSessionManager,
});
if (!preflightRecovery && promptErrorSource !== "precheck") {
promptError = null;
promptErrorSource = null;
handleMidTurnPrecheckRequest(request);
}
});
}
await sessionLockController.waitForSessionEvents(activeSession);
await sessionLockController.releaseForPrompt();
// Capture snapshot before compaction wait so we have complete messages if timeout occurs
// Check compaction state before and after to avoid race condition where compaction starts during capture
// Use session state (not subscription) for snapshot decisions - need instantaneous compaction status
const wasCompactingBefore = activeSession.isCompacting;
const snapshot = activeSession.messages.slice();
const wasCompactingAfter = activeSession.isCompacting;
// Only trust snapshot if compaction wasn't running before or after capture
const preCompactionSnapshot = wasCompactingBefore || wasCompactingAfter ? null : snapshot;
const preCompactionSessionId = activeSession.sessionId;
const COMPACTION_RETRY_AGGREGATE_TIMEOUT_MS = 60_000;
try {
// Flush buffered block replies before waiting for compaction so the
// user receives the assistant response immediately. Without this,
// coalesced/buffered blocks stay in the pipeline until compaction
// finishes — which can take minutes on large contexts (#35074).
if (onBlockReplyFlush) {
await onBlockReplyFlush();
}
// Skip compaction wait when yield aborted the run — the signal is
// already tripped and abortable() would immediately reject.
const compactionRetryWait = yieldAborted
? { timedOut: false }
: await waitForCompactionRetryWithAggregateTimeout({
waitForCompactionRetry,
abortable,
aggregateTimeoutMs: COMPACTION_RETRY_AGGREGATE_TIMEOUT_MS,
isCompactionStillInFlight: isCompactionInFlight,
});
if (compactionRetryWait.timedOut) {
timedOutDuringCompaction = true;
if (!isProbeSession) {
log.warn(
`compaction retry aggregate timeout (${COMPACTION_RETRY_AGGREGATE_TIMEOUT_MS}ms): ` +
`proceeding with pre-compaction state runId=${params.runId} sessionId=${params.sessionId}`,
);
}
}
} catch (err) {
if (isRunnerAbortError(err)) {
if (!promptError) {
promptError = err;
promptErrorSource = "compaction";
}
if (!isProbeSession) {
log.debug(
`compaction wait aborted: runId=${params.runId} sessionId=${params.sessionId}`,
);
}
} else {
throw err;
}
}
await sessionLockController.waitForSessionEvents(activeSession);
await sessionLockController.withSessionWriteLock(async () => {
// Check if ANY compaction occurred during the entire attempt (prompt + retry).
// Using a cumulative count (> 0) instead of a delta check avoids missing
// compactions that complete during activeSession.prompt() before the delta
// baseline is sampled.
compactionOccurredThisAttempt = getCompactionCount() > 0;
// Append cache-TTL timestamp AFTER prompt + compaction retry completes.
// Previously this was before the prompt, which caused a custom entry to be
// inserted between compaction and the next prompt — breaking the
// prepareCompaction() guard that checks the last entry type, leading to
// double-compaction. See: https://github.com/openclaw/openclaw/issues/9282
// Skip when timed out during compaction — session state may be inconsistent.
// Also skip when compaction ran this attempt — appending a custom entry
// after compaction would break the guard again. See: #28491
appendAttemptCacheTtlIfNeeded({
sessionManager: activeSessionManager,
timedOutDuringCompaction,
compactionOccurredThisAttempt,
config: params.config,
provider: params.provider,
modelId: params.modelId,
modelApi: params.model.api,
isCacheTtlEligibleProvider,
});
// If timeout occurred during compaction, use pre-compaction snapshot when available
// (compaction restructures messages but does not add user/assistant turns).
const snapshotSelection = selectCompactionTimeoutSnapshot({
timedOutDuringCompaction,
preCompactionSnapshot,
preCompactionSessionId,
currentSnapshot: activeSession.messages.slice(),
currentSessionId: activeSession.sessionId,
});
if (timedOutDuringCompaction) {
if (!isProbeSession) {
log.warn(
`using ${snapshotSelection.source} snapshot: timed out during compaction runId=${params.runId} sessionId=${params.sessionId}`,
);
}
}
messagesSnapshot = projectToolSearchTargetTranscriptMessages(
snapshotSelection.messagesSnapshot,
toolSearchTargetTranscriptProjections,
);
sessionIdUsed = snapshotSelection.sessionIdUsed;
lastAssistant = messagesSnapshot
.slice()
.toReversed()
.find((message): message is AssistantMessage => message.role === "assistant");
currentAttemptAssistant = findCurrentAttemptAssistantMessage({
messagesSnapshot,
prePromptMessageCount,
});
attemptUsage = getUsageTotals();
cacheBreak = cacheObservabilityEnabled
? completePromptCacheObservation({
sessionId: params.sessionId,
promptCacheKey: params.promptCacheKey,
sessionKey: params.sessionKey,
usage: attemptUsage,
})
: null;
lastCallUsage = normalizeUsage(currentAttemptAssistant?.usage);
const promptCacheObservation =
cacheObservabilityEnabled &&
(cacheBreak || promptCacheChangesForTurn || typeof attemptUsage?.cacheRead === "number")
? {
broke: Boolean(cacheBreak),
...(typeof cacheBreak?.previousCacheRead === "number"
? { previousCacheRead: cacheBreak.previousCacheRead }
: {}),
...(typeof cacheBreak?.cacheRead === "number"
? { cacheRead: cacheBreak.cacheRead }
: typeof attemptUsage?.cacheRead === "number"
? { cacheRead: attemptUsage.cacheRead }
: {}),
changes: cacheBreak?.changes ?? promptCacheChangesForTurn,
}
: undefined;
const fallbackLastCacheTouchAt = readLastCacheTtlTimestamp(activeSessionManager, {
provider: params.provider,
modelId: params.modelId,
});
promptCache = buildContextEnginePromptCacheInfo({
retention: effectivePromptCacheRetention,
lastCallUsage,
observation: promptCacheObservation,
lastCacheTouchAt: resolvePromptCacheTouchTimestamp({
lastCallUsage,
assistantTimestamp: currentAttemptAssistant?.timestamp,
fallbackLastCacheTouchAt,
}),
});
if (promptError && promptErrorSource === "prompt" && !compactionOccurredThisAttempt) {
try {
activeSessionManager.appendCustomEntry("openclaw:prompt-error", {
timestamp: Date.now(),
runId: params.runId,
sessionId: params.sessionId,
provider: params.provider,
model: params.modelId,
api: params.model.api,
error: formatErrorMessage(promptError),
});
} catch (entryErr) {
log.warn(`failed to persist prompt error entry: ${String(entryErr)}`);
}
}
});
// Let the active context engine run its post-turn lifecycle. These hooks
// may call runtime LLM capabilities, so only their transcript rewrite
// helper reacquires the session write lock.
if (activeContextEngine) {
const afterTurnRuntimeContext = buildAfterTurnRuntimeContextFromUsage({
attempt: params,
workspaceDir: effectiveWorkspace,
agentDir,
tokenBudget: params.contextTokenBudget,
lastCallUsage,
promptCache,
activeAgentId: sessionAgentId,
contextEnginePluginId: resolveActiveContextEnginePluginId(),
});
await finalizeAttemptContextEngineTurn({
contextEngine: activeContextEngine,
promptError: Boolean(promptError),
aborted,
yieldAborted,
sessionIdUsed,
sessionKey: params.sessionKey,
sessionFile: params.sessionFile,
messagesSnapshot,
prePromptMessageCount: contextEngineAfterTurnCheckpoint ?? prePromptMessageCount,
tokenBudget: params.contextTokenBudget,
runtimeContext: afterTurnRuntimeContext,
runMaintenance: async (contextParams) =>
await runContextEngineMaintenance({
contextEngine: contextParams.contextEngine as never,
sessionId: contextParams.sessionId,
sessionKey: contextParams.sessionKey,
sessionFile: contextParams.sessionFile,
reason: contextParams.reason,
sessionManager: contextParams.sessionManager as never,
withSessionManagerRewriteLock: async (operation) =>
await sessionLockController.withSessionWriteLock(operation),
runtimeContext: contextParams.runtimeContext,
config: params.config,
agentId: sessionAgentId,
}),
sessionManager: activeSessionManager,
config: params.config,
warn: (message) => log.warn(message),
});
}
await sessionLockController.waitForSessionEvents(activeSession);
await sessionLockController.withSessionWriteLock(async () => {
if (
shouldPersistCompletedBootstrapTurn({
shouldRecordCompletedBootstrapTurn,
promptError,
aborted,
timedOutDuringCompaction,
compactionOccurredThisAttempt,
})
) {
try {
activeSessionManager.appendCustomEntry(FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE, {
timestamp: Date.now(),
runId: params.runId,
sessionId: params.sessionId,
});
} catch (entryErr) {
log.warn(`failed to persist bootstrap completion entry: ${String(entryErr)}`);
}
}
if (
compactionOccurredThisAttempt &&
!promptError &&
!aborted &&
!timedOut &&
!idleTimedOut &&
!timedOutDuringCompaction &&
shouldRotateCompactionTranscript(params.config)
) {
try {
const rotation = await rotateTranscriptAfterCompaction({
sessionManager: activeSessionManager,
sessionFile: params.sessionFile,
});
if (rotation.rotated) {
sessionIdUsed = rotation.sessionId ?? sessionIdUsed;
sessionFileUsed = rotation.sessionFile ?? sessionFileUsed;
updateActiveEmbeddedRunSessionFile(params.sessionId, sessionFileUsed);
log.info(
`[compaction] rotated active transcript after automatic compaction ` +
`(sessionKey=${params.sessionKey ?? params.sessionId})`,
);
}
} catch (err) {
log.warn("[compaction] automatic transcript rotation failed", {
errorMessage: formatErrorMessage(err),
});
}
}
});
cacheTrace?.recordStage("session:after", {
messages: messagesSnapshot,
note: timedOutDuringCompaction
? "compaction timeout"
: promptError
? "prompt error"
: undefined,
});
anthropicPayloadLogger?.recordUsage(messagesSnapshot, promptError);
runAgentEndSideEffects({
event: {
messages: messagesSnapshot,
success: !aborted && !promptError,
error: promptError ? formatErrorMessage(promptError) : undefined,
durationMs: Date.now() - promptStartedAt,
},
ctx: {
runId: params.runId,
trace: freezeDiagnosticTraceContext(diagnosticTrace),
agentId: hookAgentId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
workspaceDir: params.workspaceDir,
trigger: params.trigger,
...(params.config ? { config: params.config } : {}),
...buildAgentHookContextChannelFields(params),
},
hookRunner,
});
} finally {
clearTimeout(abortTimer);
if (abortWarnTimer) {
clearTimeout(abortWarnTimer);
}
if (!isProbeSession && (aborted || timedOut) && !timedOutDuringCompaction) {
log.debug(
`run cleanup: runId=${params.runId} sessionId=${params.sessionId} aborted=${aborted} timedOut=${timedOut}`,
);
}
try {
unsubscribe();
} catch (err) {
// unsubscribe() should never throw; if it does, it indicates a serious bug.
// Log at error level to ensure visibility, but don't rethrow in finally block
// as it would mask any exception from the try block above.
log.error(
`CRITICAL: unsubscribe failed, possible resource leak: runId=${params.runId} ${String(err)}`,
);
}
if (params.replyOperation) {
params.replyOperation.detachBackend(queueHandle);
}
clearActiveEmbeddedRun(
params.sessionId,
queueHandle,
params.sessionKey,
params.sessionFile,
);
params.abortSignal?.removeEventListener?.("abort", onAbort);
}
const toolMetasNormalized = toolMetas
.filter(
(entry): entry is { toolName: string; meta?: string } =>
typeof entry.toolName === "string" && entry.toolName.trim().length > 0,
)
.map((entry) => ({ toolName: entry.toolName, meta: entry.meta }));
if (cacheObservabilityEnabled) {
const cacheBreakForLog = cacheBreak as PromptCacheBreak | null;
if (cacheBreakForLog) {
const changeSummary =
cacheBreakForLog.changes
?.map((change) => `${change.code}(${change.detail})`)
.join(", ") ?? "no tracked cache input change";
log.warn(
`[prompt-cache] cache read dropped ${cacheBreakForLog.previousCacheRead} -> ${cacheBreakForLog.cacheRead} ` +
`for ${params.provider}/${params.modelId} via ${streamStrategy}; ${changeSummary}`,
);
cacheTrace?.recordStage("cache:result", {
options: {
previousCacheRead: cacheBreakForLog.previousCacheRead,
cacheRead: cacheBreakForLog.cacheRead,
changes:
cacheBreakForLog.changes?.map((change) => ({
code: change.code,
detail: change.detail,
})) ?? undefined,
},
});
} else if (cacheTrace && promptCacheChangesForTurn) {
cacheTrace.recordStage("cache:result", {
note: "state changed without a cache-read break",
options: {
cacheRead: attemptUsage?.cacheRead ?? 0,
changes: promptCacheChangesForTurn.map((change) => ({
code: change.code,
detail: change.detail,
})),
},
});
} else if (cacheTrace) {
cacheTrace.recordStage("cache:result", {
note: "stable cache inputs",
options: {
cacheRead: attemptUsage?.cacheRead ?? 0,
},
});
}
}
if (
hookRunner?.hasHooks("llm_output") &&
shouldRunLlmOutputHooksForAttempt({ promptErrorSource })
) {
hookRunner
.runLlmOutput(
{
runId: params.runId,
sessionId: params.sessionId,
provider: params.provider,
model: params.modelId,
...(params.contextWindowInfo?.tokens
? { contextTokenBudget: params.contextWindowInfo.tokens }
: {}),
...(params.contextWindowInfo?.source
? { contextWindowSource: params.contextWindowInfo.source }
: {}),
...(params.contextWindowInfo?.referenceTokens
? { contextWindowReferenceTokens: params.contextWindowInfo.referenceTokens }
: {}),
resolvedRef:
params.runtimePlan?.observability.resolvedRef ??
`${params.provider}/${params.modelId}`,
...(params.runtimePlan?.observability.harnessId
? { harnessId: params.runtimePlan.observability.harnessId }
: {}),
assistantTexts,
lastAssistant,
usage: attemptUsage,
},
{
runId: params.runId,
trace: freezeDiagnosticTraceContext(diagnosticTrace),
agentId: hookAgentId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
workspaceDir: params.workspaceDir,
trigger: params.trigger,
...(params.contextWindowInfo?.tokens
? { contextTokenBudget: params.contextWindowInfo.tokens }
: {}),
...(params.contextWindowInfo?.source
? { contextWindowSource: params.contextWindowInfo.source }
: {}),
...(params.contextWindowInfo?.referenceTokens
? { contextWindowReferenceTokens: params.contextWindowInfo.referenceTokens }
: {}),
...buildAgentHookContextChannelFields(params),
},
)
.catch((err) => {
log.warn(`llm_output hook failed: ${String(err)}`);
});
}
const acceptedSessionSpawns = getAcceptedSessionSpawns();
const observedReplayMetadata = buildAttemptReplayMetadata({
toolMetas: toolMetasNormalized,
didSendViaMessagingTool: didSendViaMessagingTool(),
messagingToolSentTexts: getMessagingToolSentTexts(),
messagingToolSentMediaUrls: getMessagingToolSentMediaUrls(),
acceptedSessionSpawns,
successfulCronAdds: getSuccessfulCronAdds(),
});
const pendingToolMediaReply = getPendingToolMediaReply();
const replayMetadata = replayMetadataFromState(
observeReplayMetadata(getReplayState(), observedReplayMetadata),
);
const completedClientToolCalls = clientToolCallSlots.flatMap((slot) =>
slot.completed && slot.params
? [
{
name: slot.name,
params: slot.params,
},
]
: [],
);
const completedClientToolCallsForAttempt =
completedClientToolCalls.length > 0 ? completedClientToolCalls : undefined;
const didSendDeterministicApprovalPromptNow = didSendDeterministicApprovalPrompt();
const lastToolError = getLastToolError?.();
const heartbeatToolResponse = getHeartbeatToolResponse();
const messagingToolSourceReplyPayloads = getMessagingToolSourceReplyPayloads();
const pendingToolMediaPayloadCount = hasVisiblePendingToolMediaReply(pendingToolMediaReply)
? 1
: 0;
const visibleBlockReplyCount = getVisibleBlockReplyCount();
const silentToolResultReplyPayload = resolveSilentToolResultReplyPayload({
isCronTrigger: params.trigger === "cron",
payloadCount: pendingToolMediaPayloadCount,
aborted,
timedOut,
attempt: {
clientToolCalls: completedClientToolCallsForAttempt,
yieldDetected,
didSendDeterministicApprovalPrompt: didSendDeterministicApprovalPromptNow,
lastToolError,
messagesSnapshot,
toolMetas: toolMetasNormalized,
},
});
const synthesizedPayloadCount =
visibleBlockReplyCount +
pendingToolMediaPayloadCount +
messagingToolSourceReplyPayloads.length +
(silentToolResultReplyPayload ? 1 : 0);
const emptyAssistantReplyIsSilent = shouldTreatEmptyAssistantReplyAsSilent({
allowEmptyAssistantReplyAsSilent: params.allowEmptyAssistantReplyAsSilent,
payloadCount: 0,
aborted,
timedOut,
attempt: {
assistantTexts,
clientToolCalls: completedClientToolCallsForAttempt,
currentAttemptAssistant,
yieldDetected,
didSendDeterministicApprovalPrompt: didSendDeterministicApprovalPromptNow,
didSendViaMessagingTool: didSendViaMessagingTool(),
messagingToolSentTexts: getMessagingToolSentTexts(),
messagingToolSentMediaUrls: getMessagingToolSentMediaUrls(),
messagingToolSentTargets: getMessagingToolSentTargets(),
acceptedSessionSpawns,
lastToolError,
lastAssistant,
itemLifecycle: getItemLifecycle(),
toolMetas: toolMetasNormalized,
replayMetadata,
promptErrorSource,
timedOutDuringCompaction,
},
});
const terminalAssistantTexts = resolveTerminalAssistantTexts({
assistantTexts,
lastAssistantStopReason: lastAssistant?.stopReason,
lastAssistantVisibleText: resolveFinalAssistantVisibleText(lastAssistant),
});
const attemptTrajectoryTerminal = resolveAttemptTrajectoryTerminal({
promptError,
aborted,
externalAbort,
timedOut,
assistantTexts: terminalAssistantTexts,
toolMetas: toolMetasNormalized,
didSendViaMessagingTool: didSendViaMessagingTool(),
didSendDeterministicApprovalPrompt: didSendDeterministicApprovalPromptNow,
messagingToolSentTexts: getMessagingToolSentTexts(),
messagingToolSentMediaUrls: getMessagingToolSentMediaUrls(),
messagingToolSentTargets: getMessagingToolSentTargets(),
successfulCronAdds: getSuccessfulCronAdds(),
synthesizedPayloadCount,
acceptedSessionSpawns,
heartbeatToolResponse,
clientToolCalls: completedClientToolCalls,
yieldDetected,
lastToolError,
silentExpected: params.silentExpected,
emptyAssistantReplyIsSilent,
lastAssistantStopReason: lastAssistant?.stopReason,
});
trajectoryRecorder?.recordEvent("model.completed", {
aborted,
externalAbort,
timedOut,
idleTimedOut,
timedOutDuringCompaction,
timedOutDuringToolExecution,
promptError: promptError ? formatErrorMessage(promptError) : undefined,
promptErrorSource,
terminalError: attemptTrajectoryTerminal.terminalError,
usage: attemptUsage,
promptCache,
compactionCount: getCompactionCount(),
assistantTexts,
finalPromptText,
messagesSnapshot,
});
trajectoryRecorder?.recordEvent(
"trace.artifacts",
buildTrajectoryArtifacts({
status: attemptTrajectoryTerminal.status,
aborted,
externalAbort,
timedOut,
idleTimedOut,
timedOutDuringCompaction,
timedOutDuringToolExecution,
promptError: promptError ? formatErrorMessage(promptError) : undefined,
promptErrorSource,
terminalError: attemptTrajectoryTerminal.terminalError,
usage: attemptUsage,
promptCache,
compactionCount: getCompactionCount(),
assistantTexts,
finalPromptText,
itemLifecycle: getItemLifecycle(),
toolMetas: toolMetasNormalized,
didSendViaMessagingTool: didSendViaMessagingTool(),
successfulCronAdds: getSuccessfulCronAdds(),
messagingToolSentTexts: getMessagingToolSentTexts(),
messagingToolSentMediaUrls: getMessagingToolSentMediaUrls(),
messagingToolSentTargets: getMessagingToolSentTargets(),
lastToolError,
}),
);
trajectoryRecorder?.recordEvent("session.ended", {
status: attemptTrajectoryTerminal.status,
aborted,
externalAbort,
timedOut,
idleTimedOut,
timedOutDuringCompaction,
timedOutDuringToolExecution,
promptError: promptError ? formatErrorMessage(promptError) : undefined,
terminalError: attemptTrajectoryTerminal.terminalError,
});
trajectoryEndRecorded = true;
return {
replayMetadata,
itemLifecycle: getItemLifecycle(),
setTerminalLifecycleMeta,
aborted,
externalAbort,
timedOut,
idleTimedOut,
timedOutDuringCompaction,
timedOutDuringToolExecution,
promptError,
promptErrorSource,
preflightRecovery,
sessionIdUsed,
sessionFileUsed,
diagnosticTrace,
bootstrapPromptWarningSignaturesSeen: bootstrapPromptWarning.warningSignaturesSeen,
bootstrapPromptWarningSignature: bootstrapPromptWarning.signature,
systemPromptReport,
finalPromptText,
messagesSnapshot,
assistantTexts,
toolMetas: toolMetasNormalized,
acceptedSessionSpawns,
lastAssistant,
currentAttemptAssistant,
lastToolError,
didSendViaMessagingTool: didSendViaMessagingTool(),
didSendDeterministicApprovalPrompt: didSendDeterministicApprovalPromptNow,
messagingToolSentTexts: getMessagingToolSentTexts(),
messagingToolSentMediaUrls: getMessagingToolSentMediaUrls(),
messagingToolSentTargets: getMessagingToolSentTargets(),
messagingToolSourceReplyPayloads,
heartbeatToolResponse,
toolMediaUrls: pendingToolMediaReply?.mediaUrls,
toolAudioAsVoice: pendingToolMediaReply?.audioAsVoice,
toolTrustedLocalMedia: pendingToolMediaReply?.trustedLocalMedia,
successfulCronAdds: getSuccessfulCronAdds(),
cloudCodeAssistFormatError: Boolean(
lastAssistant?.errorMessage && isCloudCodeAssistFormatError(lastAssistant.errorMessage),
),
attemptUsage,
promptCache,
contextBudgetStatus,
compactionCount: getCompactionCount(),
compactionTokensAfter: getLastCompactionTokensAfter(),
// Client tool calls detected (OpenResponses hosted tools).
// Stay `undefined` (not `[]`) when none were detected so downstream
// truthiness predicates keep working without a `.length` check.
clientToolCalls: completedClientToolCalls.length > 0 ? completedClientToolCalls : undefined,
yieldDetected: yieldDetected || undefined,
};
} finally {
if (trajectoryRecorder && !trajectoryEndRecorded) {
trajectoryRecorder.recordEvent("session.ended", {
status: promptError ? "error" : aborted || timedOut ? "interrupted" : "cleanup",
aborted,
externalAbort,
timedOut,
idleTimedOut,
timedOutDuringCompaction,
timedOutDuringToolExecution,
promptError: promptError ? formatErrorMessage(promptError) : undefined,
});
}
await runAgentCleanupStep({
runId: params.runId,
sessionId: params.sessionId,
step: "openclaw-trajectory-flush",
log,
getTimeoutDetails: () => trajectoryRecorder?.describeFlushState(),
cleanup: async () => {
await trajectoryRecorder?.flush();
},
});
// Always tear down the session (and release the lock) before we leave this attempt.
//
// BUGFIX: Wait for the agent to be truly idle before flushing pending tool results.
// agent runtime's auto-retry resolves waitForRetry() on assistant message receipt,
// *before* tool execution completes in the retried agent loop. Without this wait,
// flushPendingToolResults() fires while tools are still executing, inserting
// synthetic "missing tool result" errors and causing silent agent failures.
// See: https://github.com/openclaw/openclaw/issues/8643
let cleanupError: unknown;
try {
clearToolSearchCatalog({
sessionId: params.sessionId,
sessionKey: sandboxSessionKey,
agentId: sessionAgentId,
runId: params.runId,
catalogRef: toolSearchCatalogRef,
});
const cleanupAborted =
Boolean(params.abortSignal?.aborted) ||
aborted ||
timedOut ||
idleTimedOut ||
timedOutDuringCompaction;
const cleanupAbortLike = cleanupAborted || cleanupYieldAborted;
const cleanupSessionLock = await sessionLockController.acquireForCleanup({ session });
await cleanupEmbeddedAttemptResources({
removeToolResultContextGuard,
flushPendingToolResultsAfterIdle,
session,
sessionManager,
bundleMcpRuntime,
bundleLspRuntime,
sessionLock: cleanupSessionLock,
// PERF: If the run was aborted (user stop, timeout, sessions_yield, etc.),
// skip the idle wait and flush pending results synchronously so we can
// release the session lock ASAP.
aborted: cleanupAbortLike,
abortSettlePromise: cleanupAborted ? buildAbortSettlePromise() : null,
skipSessionFlush: sessionLockController.hasSessionTakeover(),
runId: params.runId,
sessionId: params.sessionId,
});
} catch (err) {
cleanupError = err;
}
const synthesizedCleanupTakeoverError =
!cleanupError && promptError && sessionLockController.hasSessionTakeover()
? new EmbeddedAttemptSessionTakeoverError(params.sessionFile)
: undefined;
const cleanupFailure = cleanupError ?? synthesizedCleanupTakeoverError;
const shouldPreservePromptError = shouldPreservePromptErrorAfterCleanupError({
promptError,
cleanupError: cleanupFailure,
});
emitDiagnosticRunCompleted?.(
cleanupFailure
? "error"
: beforeAgentRunBlocked
? "blocked"
: promptError
? "error"
: aborted || timedOut || idleTimedOut || timedOutDuringCompaction
? "aborted"
: "completed",
shouldPreservePromptError ? promptError : (cleanupFailure ?? promptError),
beforeAgentRunBlocked
? { blockedBy: beforeAgentRunBlockedBy ?? "before_agent_run" }
: undefined,
);
if (cleanupFailure) {
if (shouldPreservePromptError) {
log.warn(
`embedded attempt cleanup detected session takeover after prompt failure; preserving prompt error: ` +
`runId=${params.runId} sessionId=${params.sessionId} ` +
`promptError=${formatErrorMessage(promptError)} cleanupError=${formatErrorMessage(cleanupFailure)}`,
);
await Promise.reject(
new EmbeddedAttemptPromptErrorWithCleanupTakeoverError({
promptError,
cleanupError: cleanupFailure as EmbeddedAttemptSessionTakeoverError,
}),
);
} else {
await Promise.reject(cleanupFailure);
}
}
}
} finally {
removeExternalAbortSignalListener?.();
if (!sessionCleanupOwnsEmbeddedResources) {
try {
await cleanupEmbeddedPrepResourcesAfterEarlyExit();
} catch (cleanupErr) {
log.warn(
`failed to clean up embedded prep resources after early attempt exit: runId=${params.runId} ${String(cleanupErr)}`,
);
}
}
try {
await releaseRetainedSessionLock?.();
} catch (releaseErr) {
log.error(
`failed to release retained session lock on attempt teardown: runId=${params.runId} ${String(releaseErr)}`,
);
}
retainedSessionFileOwner?.release();
retainedSessionFileOwner = undefined;
emitDiagnosticRunCompleted?.(
aborted ? "aborted" : "error",
promptError ?? new Error("run exited before diagnostic completion"),
);
restoreSkillEnv?.();
}
}