/** * Public facade and fallback coordinator for embedded-agent compaction. */ import { AsyncLocalStorage } from "node:async_hooks"; import { resolveAgentModelFallbackValues } from "../../config/model-input.js"; import { loadSessionEntryReadOnly } from "../../config/sessions/session-accessor.js"; import { projectPublicSessionEntry } from "../../config/sessions/session-entry-projection.js"; import { isAbortError } from "../../infra/abort-signal.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { withPluginRuntimeGenerationScope } from "../../plugins/runtime/generation-scope.js"; import { resolveSessionPinnedHarnessId } from "../../sessions/agent-harness-session-key.js"; import { AsyncWorkScope, captureAsyncWorkTracker, getAsyncWorkSignal, } from "../../shared/async-work-scope.js"; import { createDeferredCore } from "../../shared/deferred.js"; import { resolveUserPath } from "../../utils.js"; import { prepareSystemAgentRunAdmission } from "../admitted-run-context.js"; import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js"; import { resolveAgentDir, resolveAgentWorkspaceDir, resolveRunModelFallbacksOverride, resolveSessionAgentIds, } from "../agent-scope.js"; import { resolveCliBackendConfig } from "../cli-backends.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js"; import { coerceToFailoverError } from "../failover-error.js"; import { ensureSelectedAgentHarnessPlugin } from "../harness/runtime-plugin.js"; import { isFallbackSummaryError } from "../model-fallback-attempt.js"; import { resolveModelCandidateChain } from "../model-fallback-candidates.js"; import { runWithModelFallback } from "../model-fallback-runner.js"; import { acquireAgentRunPreparedModelRuntime } from "../prepared-model-runtime.js"; import { resolveProjectKey } from "../project-memory-scope.js"; import { resolveProviderIdForAuth } from "../provider-auth-aliases.js"; import { applyAgentRunSessionTargetIdentity, resolveAgentRunSessionTarget, } from "../run-session-target.js"; import { resolveSystemPromptRepoRoot } from "../system-prompt-params.js"; import type { CompactEmbeddedAgentSessionParams, CompactEmbeddedAgentSessionRuntimeParams, } from "./compact.types.js"; import { containsRealConversationMessages } from "./compaction-diagnostics.js"; import { buildBeforeCompactionHookMetrics, estimateTokensAfterCompaction, runAfterCompactionHooks, runBeforeCompactionHooks, runPostCompactionSideEffects, } from "./compaction-hooks.js"; import { resolveEmbeddedCompactionTarget } from "./compaction-runtime-context.js"; import { projectCodexHostTranscriptBytePreflightConfig, resolveCompactionRuntimeSelection, } from "./compaction-runtime-preparation.js"; import { resolveCompactionTimeoutMs } from "./compaction-safety-timeout.js"; import { prepareCompactionSessionAgent } from "./compaction-session-agent.js"; import type { PreparedCompactEmbeddedAgentSessionParams } from "./direct-compaction-preparation.js"; import { compactEmbeddedAgentSessionDirectOnce } from "./direct-compaction.js"; import { readCompactionAccountingRecorder } from "./run/compaction-accounting-bridge.js"; import { prepareEmbeddedSessionActiveProjectKeys } from "./session-prompt-state.js"; import { consumeTranscriptBytePreflightClaim } from "./transcript-byte-preflight-authority.js"; import type { EmbeddedAgentCompactResult } from "./types.js"; export type { CompactEmbeddedAgentSessionParams } from "./compact.types.js"; type CompactEmbeddedAgentSessionParamsWithSessionFile = CompactEmbeddedAgentSessionRuntimeParams & { sessionFile: string; }; function lockedHarnessCompactionFailure(runtime: string): EmbeddedAgentCompactResult { return { ok: false, compacted: false, reason: `Model selection is locked to native agent harness "${runtime}"; generic compaction is unavailable.`, failure: { reason: "model_selection_locked" }, }; } export async function compactNativeCliSession(params: { runtime: string | undefined; compactParams: CompactEmbeddedAgentSessionParamsWithSessionFile; runControlOperation?: (run: () => Promise) => Promise; }): Promise { const runtime = normalizeOptionalAgentRuntimeId(params.runtime); if (!runtime || params.compactParams.trigger !== "manual") { return undefined; } const backend = resolveCliBackendConfig(runtime, params.compactParams.config, { agentId: params.compactParams.agentId, }); if (!backend?.ownsNativeCompaction) { return undefined; } const manualCompaction = backend.manualCompaction; if (!manualCompaction) { return { ok: false, compacted: false, reason: `CLI backend "${runtime}" owns compaction but does not support manual compaction.`, }; } const cliSessionBinding = params.compactParams.cliSessionBinding; const cliSessionId = (cliSessionBinding?.sessionId ?? params.compactParams.cliSessionId)?.trim(); if (!cliSessionId) { return { ok: false, compacted: false, reason: `CLI backend "${runtime}" cannot manually compact without a resumable native session.`, }; } const { runCliAgent } = await import("../cli-runner.js"); const runId = `${params.compactParams.runId ?? params.compactParams.sessionId}:native-compact`; const sessionAgentId = resolveSessionAgentIds({ sessionKey: params.compactParams.sessionKey, config: params.compactParams.config, agentId: params.compactParams.agentId, }).sessionAgentId; const preparedRunAdmission = prepareSystemAgentRunAdmission( params.compactParams.config ?? {}, runId, sessionAgentId, "agents.native-compaction", ); try { const runControlOperation = async () => { await runCliAgent({ preparedRunAdmission, sessionId: params.compactParams.sessionId, sessionKey: params.compactParams.sessionKey, sessionFile: params.compactParams.sessionFile, agentId: params.compactParams.agentId, workspaceDir: params.compactParams.workspaceDir, cwd: params.compactParams.cwd, agentDir: params.compactParams.agentDir, config: params.compactParams.config, prompt: manualCompaction.buildPrompt(params.compactParams.customInstructions), provider: runtime, modelProvider: params.compactParams.provider, model: params.compactParams.model, thinkLevel: params.compactParams.thinkLevel, timeoutMs: resolveCompactionTimeoutMs(params.compactParams.config), runId, cliSessionId, ...(cliSessionBinding ? { cliSessionBinding } : {}), ...(cliSessionBinding?.authProfileId ? { authProfileId: cliSessionBinding.authProfileId } : params.compactParams.authProfileId ? { authProfileId: params.compactParams.authProfileId } : {}), ...(params.compactParams.sessionEntry ? { sessionEntry: params.compactParams.sessionEntry } : {}), contextWindow: params.compactParams.sessionEntry?.contextWindow, trigger: "manual", controlOperation: "compact", disableCliLiveSession: true, // Compaction rewrites the persisted session behind any idle SDK query. Retire that query // after the control turn so the next user turn reloads the compacted conversation. cleanupCliLiveSessionOnRunEnd: true, allowEmptyAssistantReplyAsSilent: true, abortSignal: params.compactParams.abortSignal, }); }; if (params.runControlOperation) { await params.runControlOperation(runControlOperation); } else { await runControlOperation(); } } catch (err) { const signal = params.compactParams.abortSignal; if (signal?.aborted && (isAbortError(err) || err === signal.reason)) { throw err; } return { ok: false, compacted: false, reason: `CLI backend "${runtime}" failed to compact its native session: ${formatErrorMessage(err)}`, }; } finally { preparedRunAdmission.close(); } return { ok: true, compacted: true, reason: `CLI backend "${runtime}" compacted its native session.`, }; } function hasExplicitCompactionModel(params: CompactEmbeddedAgentSessionParams): boolean { return Boolean(params.config?.agents?.defaults?.compaction?.model?.trim()); } function resolveCompactionFallbacksOverride( params: CompactEmbeddedAgentSessionParams, ): string[] | undefined { if (params.modelSelectionLocked) { return []; } return ( params.modelFallbacksOverride ?? resolveRunModelFallbacksOverride({ cfg: params.config, sessionKey: params.sessionKey, }) ); } function hasCompactionModelFallbackCandidates(params: CompactEmbeddedAgentSessionParams): boolean { const fallbacksOverride = resolveCompactionFallbacksOverride(params); const defaultFallbacks = resolveAgentModelFallbackValues(params.config?.agents?.defaults?.model); return (fallbacksOverride ?? defaultFallbacks).length > 0; } function classifyCompactionFallbackResult( result: EmbeddedAgentCompactResult, provider: string, model: string, ) { if (result.ok) { return null; } const reason = result.reason?.trim(); if (!reason) { return null; } const failureError = Object.assign(new Error(result.failure?.rawError ?? reason), { status: result.failure?.status, code: result.failure?.code, }); const failoverError = coerceToFailoverError(failureError, { provider, model }); return failoverError ? { error: failoverError } : null; } function fallbackFailureToCompactionResult(err: unknown): EmbeddedAgentCompactResult { const reason = isFallbackSummaryError(err) ? err.message : formatErrorMessage(err); return { ok: false, compacted: false, reason, }; } /** * Core compaction logic without lane queueing. * Use this when already inside a session/global lane to avoid deadlocks. */ export async function compactEmbeddedAgentSessionDirect( paramsInput: CompactEmbeddedAgentSessionRuntimeParams, ): Promise { const paramsBase = applyAgentRunSessionTargetIdentity(paramsInput); const memoryTranscript = readCompactionAccountingRecorder( paramsBase.contextEngineRuntimeContext, )?.memoryTranscript; memoryTranscript?.assertActive(); const runSessionTarget = memoryTranscript?.sessionTarget ?? (await resolveAgentRunSessionTarget({ ...paramsBase, missingSessionKey: "resolve-existing", })); const entry = loadSessionEntryReadOnly({ ...runSessionTarget, readConsistency: "latest" }); const lockedHarnessRuntime = resolveSessionPinnedHarnessId(entry); const transcriptBytePreflightClaim = consumeTranscriptBytePreflightClaim( paramsBase, runSessionTarget, lockedHarnessRuntime, ); const transcriptBytePreflightAuthority = transcriptBytePreflightClaim?.authority; const requestedParams: CompactEmbeddedAgentSessionParamsWithSessionFile = { ...paramsBase, config: projectCodexHostTranscriptBytePreflightConfig( paramsBase.config, Boolean(transcriptBytePreflightAuthority), ), sessionEntry: entry ? projectPublicSessionEntry(entry) : undefined, agentHarnessId: lockedHarnessRuntime ?? paramsBase.agentHarnessId, modelSelectionLocked: entry?.modelSelectionLocked ?? paramsBase.modelSelectionLocked, agentId: runSessionTarget.agentId, sessionId: runSessionTarget.sessionId, sessionKey: runSessionTarget.sessionKey, // SQLite resolves storage identity; the request still owns thread routing. sessionTarget: { agentId: runSessionTarget.agentId, sessionId: runSessionTarget.sessionId, sessionKey: runSessionTarget.sessionKey, storePath: runSessionTarget.storePath, ...(paramsBase.sessionTarget?.threadId !== undefined ? { threadId: paramsBase.sessionTarget.threadId } : {}), }, sessionFile: runSessionTarget.sessionKey, }; const requestedAgentIds = resolveSessionAgentIds({ sessionKey: requestedParams.sessionKey, config: requestedParams.config, agentId: requestedParams.agentId, }); const requestedAgentDir = requestedParams.agentDir ?? resolveAgentDir(requestedParams.config ?? {}, requestedAgentIds.sessionAgentId); const requestedWorkspaceDir = resolveUserPath(requestedParams.workspaceDir); const canonicalWorkspaceDir = resolveUserPath( resolveAgentWorkspaceDir(requestedParams.config ?? {}, requestedAgentIds.sessionAgentId), ); const runtimeSelection = resolveCompactionRuntimeSelection({ ...requestedParams, modelId: requestedParams.model, boundHarnessRuntime: requestedParams.agentHarnessId, preparedRuntimePlan: requestedParams.runtimePlan, }); // Native control operations reuse the backend's existing authenticated session. // Run them before generic model preparation so subscription-only CLI sessions do // not incorrectly require an OpenClaw model API credential. const nativeCliResult = await compactNativeCliSession({ runtime: runtimeSelection.selectedHarnessRuntime, compactParams: { ...requestedParams, agentDir: requestedAgentDir, workspaceDir: requestedWorkspaceDir, }, }); if (nativeCliResult) { return nativeCliResult; } if ( lockedHarnessRuntime && lockedHarnessRuntime !== "openclaw" && !transcriptBytePreflightAuthority ) { return lockedHarnessCompactionFailure(lockedHarnessRuntime); } const callerResult = createDeferredCore(); const trackOwner = captureAsyncWorkTracker(); const parentSignal = getAsyncWorkSignal(); const cancellationSignal = requestedParams.abortSignal && parentSignal ? AbortSignal.any([requestedParams.abortSignal, parentSignal]) : (requestedParams.abortSignal ?? parentSignal); const work = new AsyncWorkScope(); let context = work.run(() => AsyncLocalStorage.snapshot()); let releasePreparedRuntime: (() => Promise) | undefined; const runPreparedCompaction = async () => { const preparedModelRuntimeLease = await acquireAgentRunPreparedModelRuntime( { config: requestedParams.config ?? {}, agentId: requestedAgentIds.sessionAgentId, agentDir: requestedAgentDir, workspaceDir: requestedWorkspaceDir, preserveWorkspaceDirOnRefresh: requestedWorkspaceDir !== canonicalWorkspaceDir, ...(requestedParams.allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true } : {}), }, { abortSignal: requestedParams.abortSignal, deriveRuntimePluginSelections: ({ config: admittedConfig, metadataSnapshot }) => { const config = projectCodexHostTranscriptBytePreflightConfig( admittedConfig, Boolean(transcriptBytePreflightAuthority), ); const selected = resolveCompactionRuntimeSelection({ ...requestedParams, config, modelId: requestedParams.model, boundHarnessRuntime: requestedParams.agentHarnessId, preparedRuntimePlan: requestedParams.runtimePlan, manifestPlugins: metadataSnapshot, allowPluginNormalization: false, }); const pluginPlanCandidates = resolveModelCandidateChain({ cfg: config, agentId: requestedAgentIds.sessionAgentId, manifestPlugins: metadataSnapshot, allowPluginNormalization: false, provider: selected.provider, model: selected.modelId, requestedRouteResolution: "resolved", fallbacksOverride: transcriptBytePreflightAuthority ? [] : resolveCompactionFallbacksOverride({ ...requestedParams, config }), }); return [ { provider: selected.provider, modelId: selected.modelId, ...(selected.selectedHarnessRuntime ? { runtime: selected.selectedHarnessRuntime } : {}), agentId: requestedAgentIds.sessionAgentId, }, ...pluginPlanCandidates .filter( (candidate) => candidate.provider !== selected.provider || candidate.model !== selected.modelId, ) .map((candidate) => ({ provider: candidate.provider, modelId: candidate.model, runtime: selected.boundHarnessRuntime, agentId: requestedAgentIds.sessionAgentId, })), ]; }, }, ); releasePreparedRuntime = () => preparedModelRuntimeLease[Symbol.asyncDispose](); try { const preparedModelRuntimeOwnerSnapshot = preparedModelRuntimeLease.snapshot; const preparedConfig = projectCodexHostTranscriptBytePreflightConfig( preparedModelRuntimeOwnerSnapshot.config, Boolean(transcriptBytePreflightAuthority), ) ?? preparedModelRuntimeOwnerSnapshot.config; const preparedWorkspaceDir = preparedModelRuntimeOwnerSnapshot.workspaceDir ?? requestedWorkspaceDir; const repoRoot = resolveSystemPromptRepoRoot({ config: preparedConfig, workspaceDir: preparedWorkspaceDir, cwd: requestedParams.cwd, }) ?? null; const projectKey = repoRoot ? await resolveProjectKey(repoRoot) : null; const activeProjectKeys = prepareEmbeddedSessionActiveProjectKeys( requestedParams.sessionId, projectKey, ); const preparedModelRuntime = Object.freeze({ ...preparedModelRuntimeOwnerSnapshot, config: preparedConfig, repoRoot, projectKey, activeProjectKeys, }); // Fallback policy and every attempt consume the same generation as model/auth discovery. // A reload may have committed while session targeting was resolved above. const params: PreparedCompactEmbeddedAgentSessionParams = { ...requestedParams, config: preparedConfig, agentId: preparedModelRuntime.agentId ?? requestedAgentIds.sessionAgentId, agentDir: preparedModelRuntime.agentDir, workspaceDir: preparedWorkspaceDir, preparedModelRuntime, ...(transcriptBytePreflightClaim ? { transcriptBytePreflightAuthority: true as const, ...(transcriptBytePreflightClaim.withCompactionPersistence ? { transcriptByteCompactionPersistence: transcriptBytePreflightClaim.withCompactionPersistence, } : {}), } : {}), }; const compactPrepared = async () => { if ( transcriptBytePreflightAuthority || hasExplicitCompactionModel(params) || !hasCompactionModelFallbackCandidates(params) ) { return await compactEmbeddedAgentSessionDirectOnce(params); } const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({ config: params.config, provider: params.provider, modelId: params.model, authProfileId: params.authProfileId, modelSelectionLocked: params.modelSelectionLocked, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); const primaryProvider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER; const primaryModel = resolvedCompactionTarget.model ?? DEFAULT_MODEL; const requestedPrimaryProvider = params.provider?.trim() || DEFAULT_PROVIDER; const resolveAuthProvider = (provider: string) => resolveProviderIdForAuth(provider, { config: params.config, metadataSnapshot: preparedModelRuntime.metadataSnapshot, }); const primaryAuthProviders = new Set( [primaryProvider, requestedPrimaryProvider].map(resolveAuthProvider), ); const fallbacksOverride = resolveCompactionFallbacksOverride(params); const fallbackAgentId = resolveSessionAgentIds({ sessionKey: params.sandboxSessionKey ?? params.sessionKey, config: params.config, agentId: params.sandboxAgentId ?? params.agentId, }).sessionAgentId; const resolvedPrimaryCandidate = resolveModelCandidateChain({ cfg: params.config, agentId: fallbackAgentId, manifestPlugins: preparedModelRuntime.metadataSnapshot, provider: primaryProvider, model: primaryModel, requestedRouteResolution: "resolved", fallbacksOverride, })[0]; const fallbackSessionKey = params.sandboxSessionKey ?? params.sessionKey ?? params.sessionId; const fallbackResult = await runWithModelFallback({ cfg: params.config, manifestPlugins: preparedModelRuntime.metadataSnapshot, provider: primaryProvider, model: primaryModel, requestedRouteResolution: "resolved", runId: params.runId ?? params.sessionId, agentDir: params.agentDir, agentId: fallbackAgentId, sessionId: params.sessionId, sessionKey: fallbackSessionKey, userLockedAuthProfileId: params.authProfileIdSource === "user" ? params.authProfileId : undefined, abortSignal: params.abortSignal, prepareAgentHarnessRuntime: async ({ provider, model, agentHarnessRuntimeOverride }) => { await ensureSelectedAgentHarnessPlugin({ config: params.config, provider, modelId: model, agentId: fallbackAgentId, sessionKey: fallbackSessionKey, agentHarnessRuntimeOverride, workspaceDir: params.workspaceDir, pluginRegistry: preparedModelRuntime.pluginRegistry!, }); }, fallbacksOverride, classifyResult: ({ result, provider, model }) => classifyCompactionFallbackResult(result, provider, model), run: async (provider, model) => { const isPrimaryCandidate = provider === resolvedPrimaryCandidate?.provider && model === resolvedPrimaryCandidate.model; const preservesPrimaryAuth = isPrimaryCandidate || primaryAuthProviders.has(resolveAuthProvider(provider)); const authProfileId = preservesPrimaryAuth ? params.authProfileId : undefined; return await compactEmbeddedAgentSessionDirectOnce({ ...params, provider, model, requestedRouteResolution: isPrimaryCandidate ? undefined : "resolved", authProfileId, authProfileIdSource: preservesPrimaryAuth ? params.authProfileIdSource : undefined, // The primary attempt retains its already prepared atomic plan. An // actual fallback may change route/auth class and must rebuild it. runtimeAuthPlan: isPrimaryCandidate ? params.runtimeAuthPlan : undefined, runtimePlan: isPrimaryCandidate ? params.runtimePlan : undefined, }); }, }); return fallbackResult.result; }; return await withPluginRuntimeGenerationScope(preparedModelRuntime, () => { context = AsyncLocalStorage.snapshot(); return compactPrepared(); }); } catch (err) { return fallbackFailureToCompactionResult(err); } }; // Logical completion reports promptly; actual attempt work retains this generation. void trackOwner(async () => { const closeWork = () => context(() => work.beginClose(cancellationSignal?.reason)); cancellationSignal?.addEventListener("abort", closeWork, { once: true }); if (cancellationSignal?.aborted) { closeWork(); } try { callerResult.resolve(await work.track(runPreparedCompaction)); } catch (error) { callerResult.reject(error); } finally { try { await AsyncWorkScope.runWhenAllIdle( () => [work], () => context(() => work.drain()), ); } finally { try { await releasePreparedRuntime?.(); } finally { cancellationSignal?.removeEventListener("abort", closeWork); } } } }).catch(callerResult.reject); return await callerResult.promise; } export const testing = { compactNativeCliSession, containsRealConversationMessages, estimateTokensAfterCompaction, buildBeforeCompactionHookMetrics, prepareCompactionSessionAgent, runBeforeCompactionHooks, runAfterCompactionHooks, runPostCompactionSideEffects, } as const;