mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-30 09:21:11 +00:00
* fix: allow preflight compaction to reenter session locks * chore: refresh proof checks * test(agents): cover compaction lock reentry Replace the source-text guard with behavior-level coverage through the compaction harness. Co-authored-by: Plexus Technology <plexusadmin@plxsai.localdomain> * fix(agents): scope compaction lock reentry Reuse only the matching logical transcript writer lock and keep unrelated same-process compaction serialized. Co-authored-by: Plexus Technology <plexusadmin@plxsai.localdomain> * test(agents): preserve lock runner generics Co-authored-by: Plexus Technology <plexusadmin@plxsai.localdomain> * test(agents): preserve generic lock wrapper type Co-authored-by: Plexus Technology <plexusadmin@plxsai.localdomain> --------- Co-authored-by: Plexus Technology <plexusadmin@plxsai.localdomain> Co-authored-by: Peter Steinberger <steipete@gmail.com>
1921 lines
76 KiB
TypeScript
1921 lines
76 KiB
TypeScript
/**
|
|
* Implements embedded-agent transcript compaction and runtime handoff.
|
|
*/
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import { isAcpRuntimeSpawnAvailable } from "../../acp/runtime/availability.js";
|
|
import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
|
import { resolveAgentModelFallbackValues } from "../../config/model-input.js";
|
|
import { parseSqliteSessionFileMarker } from "../../config/sessions/sqlite-marker.js";
|
|
import { acquireOwnedSessionTranscriptWriteLock } from "../../config/sessions/transcript-write-context.js";
|
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
|
import {
|
|
createFileBackedCompactionCheckpointStore,
|
|
readSessionLeafStateFromTranscriptAsync,
|
|
resolveCompactionCheckpointTranscriptPosition,
|
|
resolveSessionCompactionCheckpointReason,
|
|
type CapturedCompactionCheckpointSnapshot,
|
|
} from "../../gateway/session-compaction-checkpoints.js";
|
|
import {
|
|
formatActiveNodeContextLabel,
|
|
getActiveNodeContext,
|
|
} from "../../infra/active-node-context.js";
|
|
import { resolveDiagnosticModelContentCapturePolicy } from "../../infra/diagnostic-llm-content.js";
|
|
import {
|
|
createDiagnosticTraceContext,
|
|
freezeDiagnosticTraceContext,
|
|
getActiveDiagnosticTraceContext,
|
|
} from "../../infra/diagnostic-trace-context.js";
|
|
import { formatErrorMessage } from "../../infra/errors.js";
|
|
import { getMachineDisplayName } from "../../infra/machine-name.js";
|
|
import { resolveRuntimeOsLabel } from "../../infra/os-summary.js";
|
|
import { generateSecureToken } from "../../infra/secure-random.js";
|
|
import { listRegisteredPluginAgentPromptGuidance } from "../../plugins/command-registry-state.js";
|
|
import { getCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js";
|
|
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
|
import { extractModelCompat } from "../../plugins/provider-model-compat.js";
|
|
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
|
|
import {
|
|
prepareProviderRuntimeAuth,
|
|
transformProviderSystemPrompt,
|
|
} from "../../plugins/provider-runtime.js";
|
|
import {
|
|
isCronSessionKey,
|
|
isSubagentSessionKey,
|
|
parseAgentSessionKey,
|
|
} from "../../routing/session-key.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 { 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 { createBundleMcpToolRuntime } from "../agent-bundle-mcp-tools.js";
|
|
import {
|
|
consumeCompactionSafeguardCancelReason,
|
|
setCompactionSafeguardCancelReason,
|
|
} from "../agent-hooks/compaction-safeguard-runtime.js";
|
|
import { createPreparedEmbeddedAgentSettingsManager } from "../agent-project-settings.js";
|
|
import { isDefaultAgentRuntimeId, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
|
|
import {
|
|
resolveAgentDir,
|
|
resolveRunModelFallbacksOverride,
|
|
resolveSessionAgentIds,
|
|
} from "../agent-scope.js";
|
|
import {
|
|
applyAgentAutoCompactionGuard,
|
|
applyAgentCompactionSettingsFromConfig,
|
|
isSilentOverflowProneModel,
|
|
} from "../agent-settings.js";
|
|
import { createOpenClawCodingTools, resolveProcessToolScopeKey } from "../agent-tools.js";
|
|
import { listActiveProcessSessionReferences } from "../bash-process-references.js";
|
|
import {
|
|
makeBootstrapWarn,
|
|
resolveBootstrapContextForRun,
|
|
resolveContextInjectionMode,
|
|
} from "../bootstrap-files.js";
|
|
import {
|
|
listChannelSupportedActions,
|
|
resolveChannelMessageToolHints,
|
|
resolveChannelReactionGuidance,
|
|
} from "../channel-tools.js";
|
|
import {
|
|
hasMeaningfulConversationContent,
|
|
isRealConversationMessage,
|
|
} from "../compaction-real-conversation.js";
|
|
import { resolveContextWindowInfo } from "../context-window-guard.js";
|
|
import { resolveConversationCapabilityProfile } from "../conversation-capability-profile.js";
|
|
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
|
|
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
|
|
import { resolveOpenClawReferencePaths } from "../docs-path.js";
|
|
import { ensureSessionHeader } from "../embedded-agent-helpers.js";
|
|
import { pickFallbackThinkingLevel } from "../embedded-agent-helpers.js";
|
|
import { coerceToFailoverError, describeFailoverError } from "../failover-error.js";
|
|
import { resolveAgentHarnessPolicy } from "../harness/policy.js";
|
|
import { ensureSelectedAgentHarnessPlugin } from "../harness/runtime-plugin.js";
|
|
import {
|
|
selectAgentHarness,
|
|
selectAgentHarnessForPreparedModelProviders,
|
|
} from "../harness/selection.js";
|
|
import { resolveHeartbeatPromptForSystemPrompt } from "../heartbeat-system-prompt.js";
|
|
import {
|
|
applyAuthHeaderOverride,
|
|
applyLocalNoAuthHeaderOverride,
|
|
ensureAuthProfileStore,
|
|
ensureAuthProfileStoreWithoutExternalProfiles,
|
|
MissingProviderAuthError,
|
|
resolveModelAuthMode,
|
|
} from "../model-auth.js";
|
|
import {
|
|
isFallbackSummaryError,
|
|
resolveModelCandidateChain,
|
|
runWithModelFallback,
|
|
} from "../model-fallback.js";
|
|
import { supportsModelTools } from "../model-tool-support.js";
|
|
import { ensureOpenClawModelsJson } from "../models-config.js";
|
|
import { isOpenAIProvider } from "../openai-routing.js";
|
|
import { resolveAgentPromptSurfaceForSessionKey } from "../prompt-surface.js";
|
|
import { applyPreparedRuntimeAuthToModel } from "../provider-request-config.js";
|
|
import {
|
|
protectPreparedProviderRuntimeAuth,
|
|
unwrapSecretSentinelsForProviderEgress,
|
|
} from "../provider-secret-egress.js";
|
|
import { registerProviderStreamForModel } from "../provider-stream.js";
|
|
import {
|
|
applyAgentRunSessionTargetIdentity,
|
|
resolveAgentRunSessionTarget,
|
|
} from "../run-session-target.js";
|
|
import { collectRuntimeChannelCapabilities } from "../runtime-capabilities.js";
|
|
import { buildAgentRuntimePlan } from "../runtime-plan/build.js";
|
|
import {
|
|
providerUsesCredentialScopedModelMetadata,
|
|
resolveReusableRuntimeModelAuth,
|
|
} from "../runtime-plan/credential-scoped-model.js";
|
|
import { materializePreparedRuntimeModel } from "../runtime-plan/materialize-model.js";
|
|
import {
|
|
prepareAgentRuntimeAuth,
|
|
type PreparedAgentRuntimeAuthAttempt,
|
|
} from "../runtime-plan/prepare-auth.js";
|
|
import {
|
|
resolvePreparedRuntimeAuthAttempts,
|
|
resolvePreparedRuntimeModelAuth,
|
|
} from "../runtime-plan/resolve-auth.js";
|
|
import type { AgentRuntimeAuthPlan, AgentRuntimePlan } from "../runtime-plan/types.js";
|
|
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
|
|
import type { AgentMessage } from "../runtime/index.js";
|
|
import { resolveSandboxContext } from "../sandbox.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,
|
|
resolveSessionLockMaxHoldFromTimeout,
|
|
resolveSessionWriteLockOptions,
|
|
} from "../session-write-lock.js";
|
|
import { createAgentSession, estimateTokens, SessionManager } from "../sessions/index.js";
|
|
import { detectRuntimeShell } from "../shell-utils.js";
|
|
import { resolveCandidateThinkingLevel } from "../thinking-runtime.js";
|
|
import {
|
|
filterProviderNormalizableTools,
|
|
filterRuntimeCompatibleTools,
|
|
} from "../tool-schema-projection.js";
|
|
import { logRuntimeToolSchemaQuarantine } from "../tool-schema-quarantine.js";
|
|
import {
|
|
classifyCompactionReason,
|
|
formatUnknownCompactionReasonDetail,
|
|
resolveCompactionFailureReason,
|
|
} from "./compact-reasons.js";
|
|
import type {
|
|
CompactEmbeddedAgentSessionParams,
|
|
CompactEmbeddedAgentSessionRuntimeParams,
|
|
CompactionMessageMetrics,
|
|
} from "./compact.types.js";
|
|
import { dedupeDuplicateUserMessagesForCompaction } from "./compaction-duplicate-user-messages.js";
|
|
import { buildCompactionHarnessModelProvider } from "./compaction-harness-model-provider.js";
|
|
import {
|
|
asCompactionHookRunner,
|
|
buildBeforeCompactionHookMetrics,
|
|
estimateTokensAfterCompaction,
|
|
runAfterCompactionHooks,
|
|
runBeforeCompactionHooks,
|
|
runPostCompactionSideEffects,
|
|
} from "./compaction-hooks.js";
|
|
import {
|
|
resolveCompactionHarnessRuntime,
|
|
resolveEmbeddedCompactionTarget,
|
|
} from "./compaction-runtime-context.js";
|
|
import {
|
|
compactWithSafetyTimeout,
|
|
resolveCompactionTimeoutMs,
|
|
} from "./compaction-safety-timeout.js";
|
|
import { prepareCompactionSessionAgent } from "./compaction-session-agent.js";
|
|
import {
|
|
type CompactionTranscriptRotation,
|
|
rotateTranscriptAfterCompaction,
|
|
shouldRotateCompactionTranscript,
|
|
} from "./compaction-successor-transcript.js";
|
|
import { applyFinalEffectiveToolPolicy } from "./effective-tool-policy.js";
|
|
import { buildEmbeddedExtensionFactories } from "./extensions.js";
|
|
import { getHistoryLimitFromSessionKey, limitHistoryTurns } from "./history.js";
|
|
import { log } from "./logger.js";
|
|
import { hardenManualCompactionBoundary } from "./manual-compaction-boundary.js";
|
|
import { buildEmbeddedMessageActionDiscoveryInput } from "./message-action-discovery-input.js";
|
|
import { readAgentModelContextTokens } from "./model-context-tokens.js";
|
|
import { resolveModelAsync } from "./model.js";
|
|
import { sanitizeSessionHistory, validateReplayTurns } from "./replay-history.js";
|
|
import { createEmbeddedAgentResourceLoader } from "./resource-loader.js";
|
|
import { wrapStreamFnWithDiagnosticModelCallEvents } from "./run/attempt.model-diagnostic-events.js";
|
|
import { resolveAttemptSpawnWorkspaceDir } from "./run/attempt.thread-helpers.js";
|
|
import { buildEmbeddedSandboxInfo, resolveEmbeddedSandboxInfoExecPolicy } from "./sandbox-info.js";
|
|
import {
|
|
mapSandboxSkillEntriesForPrompt,
|
|
mapSandboxSkillUsagePaths,
|
|
resolveSandboxSkillRuntimeInputs,
|
|
} from "./sandbox-skills.js";
|
|
import { prewarmSessionFile, trackSessionManagerAccess } from "./session-manager-cache.js";
|
|
import { applySystemPromptToSession, buildEmbeddedSystemPrompt } from "./system-prompt.js";
|
|
import {
|
|
collectAllowedToolNames,
|
|
collectRegisteredToolNames,
|
|
toSessionToolAllowlist,
|
|
} from "./tool-name-allowlist.js";
|
|
import { splitSdkTools } from "./tool-split.js";
|
|
import { readTranscriptFileState } from "./transcript-file-state.js";
|
|
import type { EmbeddedAgentCompactResult } from "./types.js";
|
|
import {
|
|
mapThinkingLevel,
|
|
mapThinkingLevelForProvider,
|
|
normalizeContextTokenBudget,
|
|
} from "./utils.js";
|
|
import { flushPendingToolResultsAfterIdle } from "./wait-for-idle-before-flush.js";
|
|
export type { CompactEmbeddedAgentSessionParams } from "./compact.types.js";
|
|
|
|
const compactionCheckpointStore = createFileBackedCompactionCheckpointStore();
|
|
type CompactEmbeddedAgentSessionParamsWithSessionFile = CompactEmbeddedAgentSessionRuntimeParams & {
|
|
sessionFile: string;
|
|
};
|
|
|
|
function hasRealConversationContent(
|
|
msg: AgentMessage,
|
|
messages: AgentMessage[],
|
|
index: number,
|
|
): boolean {
|
|
return isRealConversationMessage(msg, messages, index);
|
|
}
|
|
|
|
function createCompactionDiagId(): string {
|
|
return `cmp-${Date.now().toString(36)}-${generateSecureToken(4)}`;
|
|
}
|
|
|
|
function resolveCompactionProviderStream(params: {
|
|
effectiveModel: ProviderRuntimeModel;
|
|
config?: OpenClawConfig;
|
|
agentDir: string;
|
|
effectiveWorkspace: string;
|
|
}) {
|
|
return registerProviderStreamForModel({
|
|
model: params.effectiveModel,
|
|
cfg: params.config,
|
|
agentDir: params.agentDir,
|
|
workspaceDir: params.effectiveWorkspace,
|
|
});
|
|
}
|
|
|
|
function normalizeObservedTokenCount(value: unknown): number | undefined {
|
|
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
? Math.floor(value)
|
|
: undefined;
|
|
}
|
|
|
|
function getMessageTextChars(msg: AgentMessage): number {
|
|
const content = (msg as { content?: unknown }).content;
|
|
if (typeof content === "string") {
|
|
return content.length;
|
|
}
|
|
if (!Array.isArray(content)) {
|
|
return 0;
|
|
}
|
|
let total = 0;
|
|
for (const block of content) {
|
|
if (!block || typeof block !== "object") {
|
|
continue;
|
|
}
|
|
const text = (block as { text?: unknown }).text;
|
|
if (typeof text === "string") {
|
|
total += text.length;
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
|
|
function resolveMessageToolLabel(msg: AgentMessage): string | undefined {
|
|
const candidate =
|
|
(msg as { toolName?: unknown }).toolName ??
|
|
(msg as { name?: unknown }).name ??
|
|
(msg as { tool?: unknown }).tool;
|
|
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate : undefined;
|
|
}
|
|
|
|
function summarizeCompactionMessages(messages: AgentMessage[]): CompactionMessageMetrics {
|
|
let historyTextChars = 0;
|
|
let toolResultChars = 0;
|
|
const contributors: Array<{ role: string; chars: number; tool?: string }> = [];
|
|
let estTokens = 0;
|
|
let tokenEstimationFailed = false;
|
|
|
|
for (const msg of messages) {
|
|
const role = typeof msg.role === "string" ? msg.role : "unknown";
|
|
const chars = getMessageTextChars(msg);
|
|
historyTextChars += chars;
|
|
if (role === "toolResult") {
|
|
toolResultChars += chars;
|
|
}
|
|
contributors.push({ role, chars, tool: resolveMessageToolLabel(msg) });
|
|
if (!tokenEstimationFailed) {
|
|
try {
|
|
estTokens += estimateTokens(msg);
|
|
} catch {
|
|
tokenEstimationFailed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
messages: messages.length,
|
|
historyTextChars,
|
|
toolResultChars,
|
|
estTokens: tokenEstimationFailed ? undefined : estTokens,
|
|
contributors: selectTopContributors(contributors),
|
|
};
|
|
}
|
|
|
|
function selectTopContributors(
|
|
contributors: CompactionMessageMetrics["contributors"],
|
|
): CompactionMessageMetrics["contributors"] {
|
|
const selected: CompactionMessageMetrics["contributors"] = [];
|
|
for (const contributor of contributors) {
|
|
let insertAt = selected.length;
|
|
for (let index = 0; index < selected.length; index += 1) {
|
|
const selectedContributor = selected.at(index);
|
|
if (selectedContributor && contributor.chars > selectedContributor.chars) {
|
|
insertAt = index;
|
|
break;
|
|
}
|
|
}
|
|
if (insertAt < 3) {
|
|
selected.splice(insertAt, 0, contributor);
|
|
if (selected.length > 3) {
|
|
selected.pop();
|
|
}
|
|
} else if (selected.length < 3) {
|
|
selected.push(contributor);
|
|
}
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
function containsRealConversationMessages(messages: AgentMessage[]): boolean {
|
|
return messages.some((message, index, allMessages) =>
|
|
hasRealConversationContent(message, allMessages, index),
|
|
);
|
|
}
|
|
|
|
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<EmbeddedAgentCompactResult> {
|
|
const paramsBase = applyAgentRunSessionTargetIdentity(paramsInput);
|
|
const lockedHarnessRuntime = normalizeOptionalAgentRuntimeId(paramsBase.agentHarnessId);
|
|
if (paramsBase.modelSelectionLocked === true && lockedHarnessRuntime !== "openclaw") {
|
|
return {
|
|
ok: false,
|
|
compacted: false,
|
|
reason: lockedHarnessRuntime
|
|
? `Model selection is locked to native agent harness "${lockedHarnessRuntime}"; generic compaction is unavailable.`
|
|
: "Model selection is locked but the persisted agent harness is unavailable.",
|
|
failure: { reason: "model_selection_locked" },
|
|
};
|
|
}
|
|
const runSessionTarget = await resolveAgentRunSessionTarget(paramsBase);
|
|
const params: CompactEmbeddedAgentSessionParamsWithSessionFile = {
|
|
...paramsBase,
|
|
agentId: paramsBase.agentId ?? runSessionTarget.agentId,
|
|
sessionId: runSessionTarget.sessionId,
|
|
sessionKey: paramsBase.sessionKey ?? runSessionTarget.sessionKey,
|
|
sessionFile: runSessionTarget.sessionFile,
|
|
};
|
|
if (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 fallbacksOverride = resolveCompactionFallbacksOverride(params);
|
|
const resolvedPrimaryCandidate = resolveModelCandidateChain({
|
|
cfg: params.config,
|
|
provider: primaryProvider,
|
|
model: primaryModel,
|
|
fallbacksOverride,
|
|
})[0];
|
|
const fallbackAgentId = resolveSessionAgentIds({
|
|
sessionKey: params.sandboxSessionKey ?? params.sessionKey,
|
|
config: params.config,
|
|
agentId: params.agentId,
|
|
}).sessionAgentId;
|
|
const fallbackSessionKey = params.sandboxSessionKey ?? params.sessionKey ?? params.sessionId;
|
|
try {
|
|
const fallbackResult = await runWithModelFallback<EmbeddedAgentCompactResult>({
|
|
cfg: params.config,
|
|
provider: primaryProvider,
|
|
model: primaryModel,
|
|
runId: params.runId ?? params.sessionId,
|
|
agentDir: params.agentDir,
|
|
agentId: fallbackAgentId,
|
|
sessionId: params.sessionId,
|
|
sessionKey: fallbackSessionKey,
|
|
abortSignal: params.abortSignal,
|
|
prepareAgentHarnessRuntime: async ({ provider, model, agentHarnessRuntimeOverride }) => {
|
|
await ensureSelectedAgentHarnessPlugin({
|
|
config: params.config,
|
|
provider,
|
|
modelId: model,
|
|
agentId: fallbackAgentId,
|
|
sessionKey: fallbackSessionKey,
|
|
agentHarnessRuntimeOverride,
|
|
workspaceDir: params.workspaceDir,
|
|
});
|
|
},
|
|
fallbacksOverride,
|
|
classifyResult: ({ result, provider, model }) =>
|
|
classifyCompactionFallbackResult(result, provider, model),
|
|
run: async (provider, model) => {
|
|
const isPrimaryCandidate =
|
|
provider === resolvedPrimaryCandidate?.provider &&
|
|
model === resolvedPrimaryCandidate.model;
|
|
const preservesPrimaryAuth =
|
|
isPrimaryCandidate ||
|
|
provider === primaryProvider ||
|
|
provider === requestedPrimaryProvider;
|
|
const authProfileId = preservesPrimaryAuth ? params.authProfileId : undefined;
|
|
const candidateThinkLevel = resolveCandidateThinkingLevel({
|
|
cfg: params.config,
|
|
provider,
|
|
modelId: model,
|
|
level: params.thinkLevel,
|
|
agentId: fallbackAgentId,
|
|
sessionKey: fallbackSessionKey,
|
|
agentRuntime: params.agentHarnessId,
|
|
});
|
|
return await compactEmbeddedAgentSessionDirectOnce({
|
|
...params,
|
|
provider,
|
|
model,
|
|
authProfileId,
|
|
authProfileIdSource: preservesPrimaryAuth ? params.authProfileIdSource : undefined,
|
|
thinkLevel: candidateThinkLevel,
|
|
// 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;
|
|
} catch (err) {
|
|
return fallbackFailureToCompactionResult(err);
|
|
}
|
|
}
|
|
|
|
async function compactEmbeddedAgentSessionDirectOnce(
|
|
params: CompactEmbeddedAgentSessionParamsWithSessionFile,
|
|
): Promise<EmbeddedAgentCompactResult> {
|
|
const startedAt = Date.now();
|
|
const diagId = params.diagId?.trim() || createCompactionDiagId();
|
|
const trigger = params.trigger ?? "manual";
|
|
const attempt = params.attempt ?? 1;
|
|
const maxAttempts = params.maxAttempts ?? 1;
|
|
const runId = params.runId ?? params.sessionId;
|
|
// Parent compaction model-call spans to the active run/harness trace when one
|
|
// exists, otherwise start a fresh root. Compaction emits no intermediate span
|
|
// of its own (unlike the run lifecycle, which backs its run trace with a
|
|
// run.started span), so a child trace here would orphan the model call under a
|
|
// phantom parent. The :compaction: runId/callId already distinguishes the span.
|
|
const compactionModelCallTrace = freezeDiagnosticTraceContext(
|
|
getActiveDiagnosticTraceContext() ?? createDiagnosticTraceContext(),
|
|
);
|
|
const diagnosticCompactionRunId = `${runId}:compaction:${diagId}`;
|
|
let diagnosticModelCallSeq = 0;
|
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
|
ensureRuntimePluginsLoaded({
|
|
config: params.config,
|
|
workspaceDir: resolvedWorkspace,
|
|
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
|
|
});
|
|
const earlyAgentIds = resolveSessionAgentIds({
|
|
sessionKey: params.sessionKey,
|
|
config: params.config,
|
|
agentId: params.agentId,
|
|
});
|
|
const agentDir =
|
|
params.agentDir ?? resolveAgentDir(params.config ?? {}, earlyAgentIds.sessionAgentId);
|
|
const runtimePolicySessionKey = params.sandboxSessionKey ?? params.sessionKey;
|
|
const runtimePolicyAgentId =
|
|
params.sandboxSessionKey && parseAgentSessionKey(params.sandboxSessionKey)
|
|
? undefined
|
|
: params.agentId;
|
|
const policyCompactionTarget = resolveEmbeddedCompactionTarget({
|
|
config: params.config,
|
|
provider: params.provider,
|
|
modelId: params.model,
|
|
authProfileId: params.authProfileId,
|
|
modelSelectionLocked: params.modelSelectionLocked,
|
|
defaultProvider: DEFAULT_PROVIDER,
|
|
defaultModel: DEFAULT_MODEL,
|
|
});
|
|
const policyProvider = policyCompactionTarget.provider ?? DEFAULT_PROVIDER;
|
|
const policyModelId = policyCompactionTarget.model ?? DEFAULT_MODEL;
|
|
const configuredHarnessPolicy = resolveAgentHarnessPolicy({
|
|
provider: policyProvider,
|
|
modelId: policyModelId,
|
|
config: params.config,
|
|
agentId: runtimePolicyAgentId,
|
|
sessionKey: runtimePolicySessionKey,
|
|
});
|
|
const configuredHarnessRuntime =
|
|
configuredHarnessPolicy.runtimeSource &&
|
|
configuredHarnessPolicy.runtimeSource !== "implicit" &&
|
|
!isDefaultAgentRuntimeId(configuredHarnessPolicy.runtime)
|
|
? configuredHarnessPolicy.runtime
|
|
: undefined;
|
|
const boundHarnessRuntime = normalizeOptionalAgentRuntimeId(params.agentHarnessId);
|
|
const selectedHarnessRuntime = resolveCompactionHarnessRuntime({
|
|
boundHarnessRuntime,
|
|
preparedRuntimePlan: params.runtimePlan,
|
|
configuredHarnessRuntime,
|
|
provider: policyProvider,
|
|
modelId: policyModelId,
|
|
});
|
|
const selectedHarnessRuntimeOverride = boundHarnessRuntime ? undefined : selectedHarnessRuntime;
|
|
const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
|
|
config: params.config,
|
|
provider: params.provider,
|
|
modelId: params.model,
|
|
authProfileId: params.authProfileId,
|
|
harnessRuntime: selectedHarnessRuntime,
|
|
modelSelectionLocked: params.modelSelectionLocked,
|
|
defaultProvider: DEFAULT_PROVIDER,
|
|
defaultModel: DEFAULT_MODEL,
|
|
});
|
|
// Keep the configured provider for harness policy, while auth/model loading below can
|
|
// route OpenAI compaction through Codex OAuth when that runtime owns the session credentials.
|
|
const provider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER;
|
|
const runtimeProvider = resolvedCompactionTarget.runtimeProvider ?? provider;
|
|
const contextConfigProvider = resolvedCompactionTarget.contextProvider ?? provider;
|
|
const modelId = resolvedCompactionTarget.model ?? DEFAULT_MODEL;
|
|
const providedRuntimeAuthPlan = params.runtimeAuthPlan ?? params.runtimePlan?.auth;
|
|
const {
|
|
plan: reusableRuntimeAuthPlan,
|
|
authProfileId,
|
|
modelAuth: initialModelAuth,
|
|
} = resolveReusableRuntimeModelAuth({
|
|
plan: providedRuntimeAuthPlan,
|
|
provider,
|
|
modelId,
|
|
authProfileId: resolvedCompactionTarget.authProfileId,
|
|
});
|
|
// Ensure the policy-selected harness plugin so selection can pick implicit codex.
|
|
await ensureSelectedAgentHarnessPlugin({
|
|
config: params.config,
|
|
provider,
|
|
modelId,
|
|
agentId: runtimePolicyAgentId,
|
|
sessionKey: runtimePolicySessionKey,
|
|
agentHarnessId: boundHarnessRuntime,
|
|
agentHarnessRuntimeOverride: selectedHarnessRuntimeOverride,
|
|
workspaceDir: resolvedWorkspace,
|
|
});
|
|
let thinkLevel: ThinkLevel = params.thinkLevel ?? "off";
|
|
const attemptedThinking = new Set<ThinkLevel>();
|
|
const fail = (reason: string, err?: unknown): EmbeddedAgentCompactResult => {
|
|
const failureReason = classifyCompactionReason(reason);
|
|
const failure = err ? describeFailoverError(err) : undefined;
|
|
const detail =
|
|
failureReason === "unknown" ? formatUnknownCompactionReasonDetail(reason) : undefined;
|
|
const detailSuffix = detail ? ` detail=${detail}` : "";
|
|
log.warn(
|
|
`[compaction-diag] end runId=${runId} sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
|
`diagId=${diagId} trigger=${trigger} provider=${provider}/${modelId} ` +
|
|
`attempt=${attempt} maxAttempts=${maxAttempts} outcome=failed reason=${failureReason}${detailSuffix} ` +
|
|
`durationMs=${Date.now() - startedAt}`,
|
|
);
|
|
return {
|
|
ok: false,
|
|
compacted: false,
|
|
reason,
|
|
failure: failure
|
|
? {
|
|
reason: failure.reason,
|
|
status: failure.status,
|
|
code: failure.code,
|
|
rawError: failure.rawError ?? failure.message,
|
|
}
|
|
: undefined,
|
|
};
|
|
};
|
|
await ensureOpenClawModelsJson(params.config, agentDir, {
|
|
workspaceDir: resolvedWorkspace,
|
|
});
|
|
const { model, error, authStorage, modelRegistry } = await resolveModelAsync(
|
|
runtimeProvider,
|
|
modelId,
|
|
agentDir,
|
|
params.config,
|
|
initialModelAuth,
|
|
);
|
|
if (!model) {
|
|
const reason = error ?? `Unknown model: ${runtimeProvider}/${modelId}`;
|
|
return fail(reason);
|
|
}
|
|
const runtimeAuthProfileStore = isOpenAIProvider(provider)
|
|
? ensureAuthProfileStore(agentDir, {
|
|
externalCliProviderIds: ["openai"],
|
|
allowKeychainPrompt: false,
|
|
})
|
|
: ensureAuthProfileStoreWithoutExternalProfiles(agentDir, {
|
|
allowKeychainPrompt: false,
|
|
});
|
|
const providerUsesProfileScopedModelMetadata = providerUsesCredentialScopedModelMetadata({
|
|
provider: runtimeProvider,
|
|
modelId,
|
|
config: params.config,
|
|
agentDir,
|
|
workspaceDir: resolvedWorkspace,
|
|
});
|
|
// Overrides stay unset when no bound/planned/explicit harness resolved so auth-aware
|
|
// selection can pick the credential-owning harness (codex for ChatGPT OAuth); native
|
|
// transcript compaction stays gated on selectedHarnessRuntime.
|
|
const selectHarnessForPreparedAttempts = (attempts: readonly PreparedAgentRuntimeAuthAttempt[]) =>
|
|
selectAgentHarnessForPreparedModelProviders({
|
|
provider,
|
|
modelId,
|
|
modelProviders: attempts.map((authAttempt) =>
|
|
buildCompactionHarnessModelProvider({
|
|
model,
|
|
plan: authAttempt.plan,
|
|
attempt: authAttempt,
|
|
}),
|
|
),
|
|
config: params.config,
|
|
agentId: runtimePolicyAgentId,
|
|
sessionKey: runtimePolicySessionKey,
|
|
agentHarnessId: boundHarnessRuntime,
|
|
agentHarnessRuntimeOverride: selectedHarnessRuntimeOverride,
|
|
});
|
|
const initialHarness = reusableRuntimeAuthPlan
|
|
? undefined
|
|
: selectAgentHarness({
|
|
provider,
|
|
modelId,
|
|
modelProvider: buildCompactionHarnessModelProvider({ model }),
|
|
config: params.config,
|
|
agentId: runtimePolicyAgentId,
|
|
sessionKey: runtimePolicySessionKey,
|
|
agentHarnessId: boundHarnessRuntime,
|
|
agentHarnessRuntimeOverride: selectedHarnessRuntimeOverride,
|
|
});
|
|
const prepareRuntimeAuth = (harness: ReturnType<typeof selectAgentHarness>) =>
|
|
prepareAgentRuntimeAuth({
|
|
provider,
|
|
modelId,
|
|
modelApi: model.api,
|
|
modelBaseUrl: model.baseUrl,
|
|
config: params.config,
|
|
env: process.env,
|
|
agentDir,
|
|
workspaceDir: resolvedWorkspace,
|
|
authProfileStore: runtimeAuthProfileStore,
|
|
sessionAuthProfileId: authProfileId,
|
|
sessionAuthProfileSource: params.authProfileIdSource,
|
|
harnessId: harness.id,
|
|
harnessRuntime: harness.id,
|
|
harnessAuthBootstrap: harness.authBootstrap,
|
|
});
|
|
let runtimeAuthPreparation = reusableRuntimeAuthPlan
|
|
? {
|
|
plan: reusableRuntimeAuthPlan,
|
|
attempts: [{ kind: "implicit" as const, plan: reusableRuntimeAuthPlan }],
|
|
}
|
|
: prepareRuntimeAuth(initialHarness!);
|
|
let selectedPreparedHarness = selectHarnessForPreparedAttempts(runtimeAuthPreparation.attempts);
|
|
if (!reusableRuntimeAuthPlan && selectedPreparedHarness.id !== initialHarness?.id) {
|
|
runtimeAuthPreparation = prepareRuntimeAuth(selectedPreparedHarness);
|
|
const confirmedHarness = selectHarnessForPreparedAttempts(runtimeAuthPreparation.attempts);
|
|
if (confirmedHarness.id !== selectedPreparedHarness.id) {
|
|
throw new Error(
|
|
`Prepared compaction auth routes did not converge on one agent harness for ${provider}/${modelId}.`,
|
|
);
|
|
}
|
|
selectedPreparedHarness = confirmedHarness;
|
|
}
|
|
const preparedHarnessRuntime = selectedPreparedHarness.id;
|
|
const resolvePreparedModel = ({
|
|
config,
|
|
authProfileId: profileId,
|
|
authProfileMode: resolvedAuthProfileMode,
|
|
}: Parameters<
|
|
Parameters<typeof materializePreparedRuntimeModel<ProviderRuntimeModel>>[0]["resolveModel"]
|
|
>[0]) =>
|
|
resolveModelAsync(runtimeProvider, modelId, agentDir, config, {
|
|
authStorage,
|
|
modelRegistry,
|
|
skipAgentDiscovery: true,
|
|
allowBundledStaticCatalogFallback: true,
|
|
preferBundledStaticCatalogTransport: true,
|
|
workspaceDir: resolvedWorkspace,
|
|
authProfileId: profileId,
|
|
authProfileMode: resolvedAuthProfileMode,
|
|
});
|
|
const materializeAuthAttemptModel = async (materializeParams: {
|
|
plan: AgentRuntimeAuthPlan;
|
|
model: ProviderRuntimeModel;
|
|
forceResolve?: boolean;
|
|
}): Promise<ProviderRuntimeModel> =>
|
|
(await materializePreparedRuntimeModel<ProviderRuntimeModel>({
|
|
plan: materializeParams.plan,
|
|
provider,
|
|
modelId,
|
|
config: params.config,
|
|
model: materializeParams.model,
|
|
forceResolve: materializeParams.forceResolve,
|
|
resolveModel: resolvePreparedModel,
|
|
})) ?? materializeParams.model;
|
|
const resolveRuntimeAuthAttempt = () =>
|
|
resolvePreparedRuntimeAuthAttempts({
|
|
attempts: runtimeAuthPreparation.attempts,
|
|
store: runtimeAuthProfileStore,
|
|
modelId,
|
|
model,
|
|
materializeModel: materializeAuthAttemptModel,
|
|
forceCredentialScopedDirectModelResolve: providerUsesProfileScopedModelMetadata,
|
|
resolveAuth: async ({ attempt: preparedAttempt, model: attemptModel }) =>
|
|
await resolvePreparedRuntimeModelAuth({
|
|
plan: preparedAttempt.plan,
|
|
model: attemptModel,
|
|
cfg: params.config,
|
|
store: runtimeAuthProfileStore,
|
|
agentDir,
|
|
workspaceDir: resolvedWorkspace,
|
|
...(preparedAttempt.allowAuthProfileFallback !== undefined
|
|
? { allowAuthProfileFallback: preparedAttempt.allowAuthProfileFallback }
|
|
: {}),
|
|
secretSentinels: true,
|
|
}),
|
|
errorMessage: `Prepared compaction auth attempts could not be resolved for ${provider}/${modelId}.`,
|
|
});
|
|
let resolvedAuthAttempt: Awaited<ReturnType<typeof resolveRuntimeAuthAttempt>>;
|
|
try {
|
|
resolvedAuthAttempt = await resolveRuntimeAuthAttempt();
|
|
} catch (err) {
|
|
return fail(formatErrorMessage(err), err);
|
|
}
|
|
let runtimeModel = resolvedAuthAttempt.model;
|
|
const apiKeyInfo = resolvedAuthAttempt.auth;
|
|
const resolvedRuntimeAuthPlan = resolvedAuthAttempt.plan;
|
|
let hasRuntimeAuthExchange = false;
|
|
try {
|
|
if (!apiKeyInfo.apiKey) {
|
|
if (apiKeyInfo.mode !== "aws-sdk") {
|
|
throw new MissingProviderAuthError(runtimeModel.provider, apiKeyInfo);
|
|
}
|
|
} else {
|
|
const preparedAuth = protectPreparedProviderRuntimeAuth({
|
|
provider: runtimeModel.provider,
|
|
preparedAuth: await prepareProviderRuntimeAuth({
|
|
provider: runtimeModel.provider,
|
|
config: params.config,
|
|
workspaceDir: resolvedWorkspace,
|
|
env: process.env,
|
|
context: {
|
|
config: params.config,
|
|
agentDir,
|
|
workspaceDir: resolvedWorkspace,
|
|
env: process.env,
|
|
provider: runtimeModel.provider,
|
|
modelId,
|
|
model: runtimeModel,
|
|
apiKey: unwrapSecretSentinelsForProviderEgress(
|
|
apiKeyInfo.apiKey,
|
|
"provider runtime auth exchange",
|
|
),
|
|
authMode: apiKeyInfo.mode,
|
|
profileId: apiKeyInfo.profileId,
|
|
},
|
|
}),
|
|
});
|
|
runtimeModel = applyPreparedRuntimeAuthToModel(runtimeModel, preparedAuth);
|
|
const runtimeApiKey = preparedAuth?.apiKey ?? apiKeyInfo.apiKey;
|
|
hasRuntimeAuthExchange = Boolean(preparedAuth?.apiKey);
|
|
if (!runtimeApiKey) {
|
|
throw new Error(`Provider "${runtimeModel.provider}" runtime auth returned no apiKey.`);
|
|
}
|
|
authStorage.setRuntimeApiKey(runtimeModel.provider, runtimeApiKey);
|
|
}
|
|
} catch (err) {
|
|
const reason = formatErrorMessage(err);
|
|
return fail(reason, err);
|
|
}
|
|
|
|
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
|
const sandboxSessionKey =
|
|
params.sandboxSessionKey?.trim() || params.sessionKey?.trim() || params.sessionId;
|
|
const sandbox = await resolveSandboxContext({
|
|
config: params.config,
|
|
execOverrides: params.execOverrides,
|
|
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 compaction runs; omit cwd or use the agent workspace as cwd",
|
|
);
|
|
}
|
|
const effectiveCwd = sandbox?.enabled ? effectiveWorkspace : (requestedCwd ?? effectiveWorkspace);
|
|
await fs.mkdir(effectiveWorkspace, { recursive: true });
|
|
const isSqliteSessionTranscript = Boolean(parseSqliteSessionFileMarker(params.sessionFile));
|
|
if (!isSqliteSessionTranscript) {
|
|
await ensureSessionHeader({
|
|
sessionFile: params.sessionFile,
|
|
sessionId: params.sessionId,
|
|
cwd: effectiveCwd,
|
|
});
|
|
}
|
|
const { sessionAgentId: effectiveSkillAgentId } = earlyAgentIds;
|
|
|
|
let restoreSkillEnv: (() => void) | undefined;
|
|
let compactionSessionManager: unknown = null;
|
|
let checkpointSnapshot: CapturedCompactionCheckpointSnapshot | null = null;
|
|
let checkpointSnapshotRetained = false;
|
|
try {
|
|
const {
|
|
skillsEligibility,
|
|
skillsPromptWorkspaceDir: effectiveSkillsPromptWorkspace,
|
|
skillsSnapshot: skillsSnapshotForRun,
|
|
skillsWorkspaceDir: effectiveSkillsWorkspace,
|
|
workspaceOnly: loadSkillsWorkspaceOnly,
|
|
} = resolveSandboxSkillRuntimeInputs({
|
|
sandbox,
|
|
effectiveWorkspace,
|
|
skillsSnapshot: params.skillsSnapshot,
|
|
});
|
|
const { shouldLoadSkillEntries, skillEntries } = resolveEmbeddedRunSkillEntries({
|
|
workspaceDir: effectiveSkillsWorkspace,
|
|
config: params.config,
|
|
agentId: effectiveSkillAgentId,
|
|
eligibility: skillsEligibility,
|
|
skillsSnapshot: skillsSnapshotForRun,
|
|
workspaceOnly: loadSkillsWorkspaceOnly,
|
|
});
|
|
restoreSkillEnv = skillsSnapshotForRun
|
|
? applySkillEnvOverridesFromSnapshot({
|
|
snapshot: skillsSnapshotForRun,
|
|
config: params.config,
|
|
})
|
|
: applySkillEnvOverrides({
|
|
skills: skillEntries ?? [],
|
|
config: params.config,
|
|
});
|
|
const promptSkillEntries = mapSandboxSkillEntriesForPrompt({
|
|
entries: shouldLoadSkillEntries ? skillEntries : undefined,
|
|
skillsWorkspaceDir: effectiveSkillsWorkspace,
|
|
skillsPromptWorkspaceDir: effectiveSkillsPromptWorkspace,
|
|
});
|
|
const skillUsagePaths = mapSandboxSkillUsagePaths({
|
|
paths: sandbox?.skillUsagePaths,
|
|
skillsWorkspaceDir: effectiveSkillsWorkspace,
|
|
skillsPromptWorkspaceDir: effectiveSkillsPromptWorkspace,
|
|
});
|
|
const skillsPrompt = resolveSkillsPromptForRun({
|
|
skillsSnapshot: skillsSnapshotForRun,
|
|
entries: promptSkillEntries,
|
|
config: params.config,
|
|
workspaceDir: effectiveSkillsPromptWorkspace,
|
|
agentId: effectiveSkillAgentId,
|
|
eligibility: skillsEligibility,
|
|
});
|
|
|
|
const sessionLabel = params.sessionKey ?? params.sessionId;
|
|
const resolvedMessageProvider = params.messageChannel ?? params.messageProvider;
|
|
const contextInjectionMode = resolveContextInjectionMode(params.config, effectiveSkillAgentId);
|
|
const { contextFiles } =
|
|
contextInjectionMode === "never"
|
|
? { contextFiles: [] }
|
|
: await resolveBootstrapContextForRun({
|
|
workspaceDir: effectiveWorkspace,
|
|
config: params.config,
|
|
sessionKey: params.sessionKey,
|
|
sessionId: params.sessionId,
|
|
agentId: effectiveSkillAgentId,
|
|
warn: makeBootstrapWarn({
|
|
sessionLabel,
|
|
warn: (message) => log.warn(message),
|
|
}),
|
|
});
|
|
// Apply contextTokens cap to model so session runtime's auto-compaction
|
|
// threshold uses the effective limit, not the native context window.
|
|
const runtimeModelWithContext = runtimeModel as ProviderRuntimeModel;
|
|
const ctxInfo = resolveContextWindowInfo({
|
|
cfg: params.config,
|
|
provider: contextConfigProvider,
|
|
modelId,
|
|
modelContextTokens: readAgentModelContextTokens(runtimeModel),
|
|
modelContextWindow: runtimeModelWithContext.contextWindow,
|
|
defaultTokens: DEFAULT_CONTEXT_TOKENS,
|
|
});
|
|
const resolvedContextTokenBudget =
|
|
normalizeContextTokenBudget(ctxInfo.tokens) ?? DEFAULT_CONTEXT_TOKENS;
|
|
const requestedContextTokenBudget =
|
|
normalizeContextTokenBudget(params.contextTokenBudget) ??
|
|
normalizeContextTokenBudget(params.tokenBudget);
|
|
const contextTokenBudget = Math.min(
|
|
requestedContextTokenBudget ?? resolvedContextTokenBudget,
|
|
resolvedContextTokenBudget,
|
|
);
|
|
const effectiveModel = applyAuthHeaderOverride(
|
|
applyLocalNoAuthHeaderOverride(
|
|
contextTokenBudget < (runtimeModelWithContext.contextWindow ?? Infinity)
|
|
? { ...runtimeModelWithContext, contextWindow: contextTokenBudget }
|
|
: runtimeModelWithContext,
|
|
apiKeyInfo,
|
|
),
|
|
// Skip header injection when runtime auth exchange produced a
|
|
// different credential — the SDK reads the exchanged token from
|
|
// authStorage automatically.
|
|
hasRuntimeAuthExchange ? null : apiKeyInfo,
|
|
params.config,
|
|
);
|
|
const reuseFullRuntimePlan = params.runtimePlan?.auth === resolvedRuntimeAuthPlan;
|
|
const preparedRuntimePlan =
|
|
(reuseFullRuntimePlan ? params.runtimePlan : undefined) ??
|
|
buildAgentRuntimePlan({
|
|
provider,
|
|
modelId,
|
|
model: effectiveModel,
|
|
modelApi: effectiveModel.api,
|
|
harnessId: preparedHarnessRuntime,
|
|
harnessRuntime: preparedHarnessRuntime,
|
|
authProfileMode: resolvedRuntimeAuthPlan.selectedAuthMode,
|
|
sessionAuthProfileId: resolvedRuntimeAuthPlan.forwardedAuthProfileId,
|
|
sessionAuthProfileSource: resolvedRuntimeAuthPlan.forwardedAuthProfileSource,
|
|
sessionAuthProfileCandidateIds: resolvedRuntimeAuthPlan.forwardedAuthProfileCandidateIds,
|
|
modelRoute: resolvedRuntimeAuthPlan.modelRoute,
|
|
config: params.config,
|
|
workspaceDir: effectiveWorkspace,
|
|
agentDir,
|
|
agentId: effectiveSkillAgentId,
|
|
thinkingLevel: mapThinkingLevelForProvider(thinkLevel),
|
|
});
|
|
const runtimePlan = reuseFullRuntimePlan
|
|
? preparedRuntimePlan
|
|
: { ...preparedRuntimePlan, auth: resolvedRuntimeAuthPlan };
|
|
|
|
const runAbortController = new AbortController();
|
|
const spawnWorkspaceDir =
|
|
effectiveCwd !== effectiveWorkspace
|
|
? resolvedWorkspace
|
|
: resolveAttemptSpawnWorkspaceDir({
|
|
sandbox,
|
|
resolvedWorkspace,
|
|
});
|
|
const runtimeCapabilityProfile = resolveConversationCapabilityProfile({
|
|
config: params.config,
|
|
sessionKey: sandboxSessionKey,
|
|
runSessionKey:
|
|
params.sessionKey && params.sessionKey !== sandboxSessionKey
|
|
? params.sessionKey
|
|
: undefined,
|
|
sessionId: params.sessionId,
|
|
runId: params.runId,
|
|
agentDir,
|
|
agentAccountId: params.agentAccountId,
|
|
messageProvider: resolvedMessageProvider,
|
|
chatType: params.chatType,
|
|
groupId: params.groupId,
|
|
groupChannel: params.groupChannel,
|
|
groupSpace: params.groupSpace,
|
|
spawnedBy: params.spawnedBy,
|
|
senderId: params.senderId,
|
|
senderName: params.senderName,
|
|
senderUsername: params.senderUsername,
|
|
senderE164: params.senderE164,
|
|
senderIsOwner: params.senderIsOwner,
|
|
modelProvider: effectiveModel.provider,
|
|
modelId,
|
|
modelApi: effectiveModel.api,
|
|
modelContextWindowTokens: contextTokenBudget,
|
|
workspaceDir: effectiveWorkspace,
|
|
cwd: effectiveCwd,
|
|
spawnWorkspaceDir,
|
|
skillsSnapshot: skillsSnapshotForRun,
|
|
sandboxToolPolicy: sandbox?.tools,
|
|
});
|
|
const toolsEnabled = supportsModelTools(effectiveModel);
|
|
const toolsRaw = toolsEnabled
|
|
? createOpenClawCodingTools({
|
|
exec: {
|
|
...params.execOverrides,
|
|
config: params.config,
|
|
elevated: params.bashElevated,
|
|
},
|
|
sandbox,
|
|
messageProvider: resolvedMessageProvider,
|
|
clientCaps: params.clientCaps,
|
|
chatType: params.chatType,
|
|
agentAccountId: params.agentAccountId,
|
|
sessionKey: sandboxSessionKey,
|
|
runSessionKey:
|
|
params.sessionKey && params.sessionKey !== sandboxSessionKey
|
|
? params.sessionKey
|
|
: undefined,
|
|
sessionId: params.sessionId,
|
|
runId: params.runId,
|
|
oneShotCliRun: params.oneShotCliRun,
|
|
groupId: params.groupId,
|
|
groupChannel: params.groupChannel,
|
|
groupSpace: params.groupSpace,
|
|
spawnedBy: params.spawnedBy,
|
|
senderId: params.senderId,
|
|
senderName: params.senderName,
|
|
senderUsername: params.senderUsername,
|
|
senderE164: params.senderE164,
|
|
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
|
|
agentDir,
|
|
cwd: effectiveCwd,
|
|
workspaceDir: effectiveWorkspace,
|
|
spawnWorkspaceDir,
|
|
config: params.config,
|
|
abortSignal: runAbortController.signal,
|
|
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
|
modelProvider: effectiveModel.provider,
|
|
modelId,
|
|
modelHasVision: effectiveModel.input?.includes("image") ?? false,
|
|
modelCompat: extractModelCompat(effectiveModel),
|
|
modelApi: effectiveModel.api,
|
|
modelContextWindowTokens: contextTokenBudget,
|
|
skillsSnapshot: skillsSnapshotForRun,
|
|
skillUsagePaths,
|
|
conversationCapabilityProfile: runtimeCapabilityProfile,
|
|
modelAuthMode: resolveModelAuthMode(effectiveModel.provider, params.config, undefined, {
|
|
workspaceDir: effectiveWorkspace,
|
|
}),
|
|
})
|
|
: [];
|
|
const runtimePlanModelContext = {
|
|
workspaceDir: effectiveWorkspace,
|
|
modelApi: effectiveModel.api,
|
|
model: effectiveModel,
|
|
};
|
|
const normalizableToolProjection = filterProviderNormalizableTools(
|
|
toolsEnabled ? toolsRaw : [],
|
|
);
|
|
logRuntimeToolSchemaQuarantine({
|
|
diagnostics: normalizableToolProjection.diagnostics,
|
|
tools: toolsEnabled ? toolsRaw : [],
|
|
runId,
|
|
agentId: effectiveSkillAgentId,
|
|
sessionKey: params.sessionKey,
|
|
sessionId: params.sessionId,
|
|
});
|
|
const tools = runtimePlan.tools.normalize(
|
|
[...normalizableToolProjection.tools],
|
|
runtimePlanModelContext,
|
|
);
|
|
const bundleMcpRuntime = toolsEnabled
|
|
? await createBundleMcpToolRuntime({
|
|
workspaceDir: effectiveWorkspace,
|
|
cfg: params.config,
|
|
reservedToolNames: tools.map((tool) => tool.name),
|
|
})
|
|
: undefined;
|
|
const bundleLspRuntime = toolsEnabled
|
|
? await createBundleLspToolRuntime({
|
|
workspaceDir: effectiveWorkspace,
|
|
cfg: params.config,
|
|
reservedToolNames: [
|
|
...tools.map((tool) => tool.name),
|
|
...(bundleMcpRuntime?.tools.map((tool) => tool.name) ?? []),
|
|
],
|
|
})
|
|
: undefined;
|
|
const filteredBundledTools = applyFinalEffectiveToolPolicy({
|
|
bundledTools: [...(bundleMcpRuntime?.tools ?? []), ...(bundleLspRuntime?.tools ?? [])],
|
|
config: params.config,
|
|
// The same profile constructed the core tool set above, so core and
|
|
// bundled tools cannot disagree about policy inputs (agentId included:
|
|
// both resolve it from the session key inside the profile).
|
|
conversationCapabilityProfile: runtimeCapabilityProfile,
|
|
warn: (message) => log.warn(message),
|
|
});
|
|
const normalizableBundledToolProjection = filterProviderNormalizableTools(filteredBundledTools);
|
|
if (normalizableBundledToolProjection.diagnostics.length > 0) {
|
|
logRuntimeToolSchemaQuarantine({
|
|
diagnostics: normalizableBundledToolProjection.diagnostics,
|
|
tools: filteredBundledTools,
|
|
runId,
|
|
agentId: effectiveSkillAgentId,
|
|
sessionKey: params.sessionKey,
|
|
sessionId: params.sessionId,
|
|
});
|
|
}
|
|
const normalizedBundledTools =
|
|
filteredBundledTools.length > 0
|
|
? runtimePlan.tools.normalize(
|
|
[...normalizableBundledToolProjection.tools],
|
|
runtimePlanModelContext,
|
|
)
|
|
: filteredBundledTools;
|
|
const projectedEffectiveTools = [...tools, ...normalizedBundledTools];
|
|
const toolSchemaProjection = filterRuntimeCompatibleTools(projectedEffectiveTools);
|
|
logRuntimeToolSchemaQuarantine({
|
|
diagnostics: toolSchemaProjection.diagnostics,
|
|
tools: projectedEffectiveTools,
|
|
runId,
|
|
agentId: effectiveSkillAgentId,
|
|
sessionKey: params.sessionKey,
|
|
sessionId: params.sessionId,
|
|
});
|
|
const effectiveTools = [...toolSchemaProjection.tools];
|
|
const allowedToolNames = collectAllowedToolNames({ tools: effectiveTools });
|
|
runtimePlan.tools.logDiagnostics(effectiveTools, runtimePlanModelContext);
|
|
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 { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
|
|
sessionKey: params.sessionKey,
|
|
config: params.config,
|
|
agentId: params.agentId,
|
|
});
|
|
// 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,
|
|
}),
|
|
)
|
|
: undefined;
|
|
const messageToolHints = runtimeChannel
|
|
? resolveChannelMessageToolHints({
|
|
cfg: params.config,
|
|
channel: runtimeChannel,
|
|
accountId: params.agentAccountId,
|
|
})
|
|
: undefined;
|
|
|
|
const runtimeInfo = {
|
|
agentId: sessionAgentId,
|
|
sessionKey: params.sessionKey,
|
|
host: machineName,
|
|
os: resolveRuntimeOsLabel(),
|
|
arch: os.arch(),
|
|
node: process.version,
|
|
model: `${provider}/${modelId}`,
|
|
shell: detectRuntimeShell(),
|
|
channel: runtimeChannel,
|
|
chatType: params.chatType,
|
|
capabilities: runtimeCapabilities,
|
|
channelActions,
|
|
activeProcessSessions: listActiveProcessSessionReferences({
|
|
scopeKey: resolveProcessToolScopeKey({
|
|
sessionKey: sandboxSessionKey,
|
|
agentId: sessionAgentId,
|
|
}),
|
|
}),
|
|
activeNode: formatActiveNodeContextLabel(getActiveNodeContext()),
|
|
};
|
|
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(provider, {
|
|
config: params.config,
|
|
workspaceDir: effectiveWorkspace,
|
|
env: process.env,
|
|
modelId,
|
|
modelApi: effectiveModel.api,
|
|
model: effectiveModel,
|
|
});
|
|
const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone);
|
|
const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat);
|
|
const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat);
|
|
const promptSurface = resolveAgentPromptSurfaceForSessionKey(params.sessionKey);
|
|
const promptMode =
|
|
isSubagentSessionKey(params.sessionKey) || isCronSessionKey(params.sessionKey)
|
|
? "minimal"
|
|
: "full";
|
|
const nativeCommandGuidanceLines = listRegisteredPluginAgentPromptGuidance({
|
|
surface: promptSurface,
|
|
});
|
|
const openClawReferences = await resolveOpenClawReferencePaths({
|
|
workspaceDir: effectiveWorkspace,
|
|
argv1: process.argv[1],
|
|
cwd: effectiveCwd,
|
|
moduleUrl: import.meta.url,
|
|
});
|
|
const promptContributionContext: Parameters<
|
|
AgentRuntimePlan["prompt"]["resolveSystemPromptContribution"]
|
|
>[0] = {
|
|
config: params.config,
|
|
agentDir,
|
|
workspaceDir: effectiveWorkspace,
|
|
provider,
|
|
modelId,
|
|
promptMode,
|
|
runtimeChannel,
|
|
runtimeCapabilities,
|
|
agentId: sessionAgentId,
|
|
};
|
|
const promptContribution =
|
|
runtimePlan.prompt.resolveSystemPromptContribution(promptContributionContext);
|
|
const buildSystemPromptText = (defaultThinkLevel: ThinkLevel) => {
|
|
const builtSystemPrompt = buildEmbeddedSystemPrompt({
|
|
config: params.config,
|
|
agentId: sessionAgentId,
|
|
workspaceDir: effectiveWorkspace,
|
|
defaultThinkLevel,
|
|
reasoningLevel: params.reasoningLevel ?? "off",
|
|
extraSystemPrompt: params.extraSystemPrompt,
|
|
ownerNumbers: params.ownerNumbers,
|
|
reasoningTagHint,
|
|
heartbeatPrompt: resolveHeartbeatPromptForSystemPrompt({
|
|
config: params.config,
|
|
agentId: sessionAgentId,
|
|
defaultAgentId,
|
|
}),
|
|
skillsPrompt,
|
|
docsPath: openClawReferences.docsPath ?? undefined,
|
|
sourcePath: openClawReferences.sourcePath ?? undefined,
|
|
promptMode,
|
|
promptSurface,
|
|
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
|
acpEnabled: isAcpRuntimeSpawnAvailable({
|
|
config: params.config,
|
|
sandboxed: sandboxInfo?.enabled === true,
|
|
}),
|
|
runtimeInfo,
|
|
reactionGuidance,
|
|
messageToolHints,
|
|
sandboxInfo,
|
|
tools: effectiveTools,
|
|
userTimezone,
|
|
userTime,
|
|
userTimeFormat,
|
|
contextFiles,
|
|
promptContribution,
|
|
nativeCommandGuidanceLines,
|
|
});
|
|
return transformProviderSystemPrompt({
|
|
provider,
|
|
config: params.config,
|
|
workspaceDir: effectiveWorkspace,
|
|
context: {
|
|
config: params.config,
|
|
agentDir,
|
|
workspaceDir: effectiveWorkspace,
|
|
provider,
|
|
modelId,
|
|
promptMode,
|
|
runtimeChannel,
|
|
runtimeCapabilities,
|
|
agentId: sessionAgentId,
|
|
systemPrompt: builtSystemPrompt,
|
|
},
|
|
});
|
|
};
|
|
|
|
const compactionTimeoutMs = resolveCompactionTimeoutMs(params.config);
|
|
const sessionLock =
|
|
(await acquireOwnedSessionTranscriptWriteLock({
|
|
sessionFile: params.sessionFile,
|
|
sessionKey: params.sessionKey,
|
|
})) ??
|
|
(await acquireSessionWriteLock({
|
|
sessionFile: params.sessionFile,
|
|
...resolveSessionWriteLockOptions(params.config, {
|
|
maxHoldMsFallback: resolveSessionLockMaxHoldFromTimeout({
|
|
timeoutMs: compactionTimeoutMs,
|
|
}),
|
|
}),
|
|
}));
|
|
try {
|
|
if (!isSqliteSessionTranscript) {
|
|
await repairSessionFileIfNeeded({
|
|
sessionFile: params.sessionFile,
|
|
debug: (message) => log.debug(message),
|
|
warn: (message) => log.warn(message),
|
|
});
|
|
await prewarmSessionFile(params.sessionFile);
|
|
}
|
|
const transcriptPolicy = runtimePlan.transcript.resolvePolicy(runtimePlanModelContext);
|
|
const sessionManager = guardSessionManager(SessionManager.open(params.sessionFile), {
|
|
agentId: sessionAgentId,
|
|
sessionKey: params.sessionKey,
|
|
config: params.config,
|
|
contextWindowTokens: contextTokenBudget,
|
|
allowSyntheticToolResults: transcriptPolicy.allowSyntheticToolResults,
|
|
missingToolResultText:
|
|
effectiveModel.api === "openai-responses" ||
|
|
effectiveModel.api === "azure-openai-responses" ||
|
|
effectiveModel.api === "openai-chatgpt-responses"
|
|
? "aborted"
|
|
: undefined,
|
|
allowedToolNames,
|
|
});
|
|
checkpointSnapshot = await compactionCheckpointStore.captureSnapshot({
|
|
sessionManager,
|
|
sessionFile: params.sessionFile,
|
|
});
|
|
compactionSessionManager = sessionManager;
|
|
trackSessionManagerAccess(params.sessionFile);
|
|
const settingsManager = createPreparedEmbeddedAgentSettingsManager({
|
|
cwd: effectiveCwd,
|
|
agentDir,
|
|
cfg: params.config,
|
|
pluginMetadataSnapshot: getCurrentPluginMetadataSnapshot({
|
|
config: params.config,
|
|
env: process.env,
|
|
workspaceDir: effectiveWorkspace,
|
|
}),
|
|
contextTokenBudget,
|
|
});
|
|
// 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,
|
|
modelId,
|
|
model: effectiveModel,
|
|
});
|
|
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. effectiveModel.baseUrl matches the surrounding scope so
|
|
// auth-profile-injected baseUrls reach the endpoint-class detector.
|
|
applyAgentCompactionSettingsFromConfig({
|
|
settingsManager,
|
|
cfg: params.config,
|
|
contextTokenBudget,
|
|
});
|
|
// contextEngineInfo is intentionally omitted: this guard runs inside the
|
|
// compaction LLM session, which is not the user-facing agent session and
|
|
// has no associated context engine.
|
|
applyAgentAutoCompactionGuard({
|
|
settingsManager,
|
|
silentOverflowProneProvider: isSilentOverflowProneModel({
|
|
provider,
|
|
modelId,
|
|
baseUrl: effectiveModel.baseUrl ?? undefined,
|
|
}),
|
|
});
|
|
|
|
const { customTools } = splitSdkTools({
|
|
tools: effectiveTools,
|
|
sandboxEnabled: Boolean(sandbox?.enabled),
|
|
toolHookContext: {
|
|
agentId: sessionAgentId,
|
|
config: params.config,
|
|
cwd: effectiveCwd,
|
|
sessionKey: sandboxSessionKey,
|
|
sessionId: params.sessionId,
|
|
runId: params.runId,
|
|
channelId: params.currentChannelId,
|
|
},
|
|
});
|
|
// The session runtime treats `tools` as a name allowlist during session creation. Pass the
|
|
// exact OpenClaw-managed registrations so custom tools survive startup.
|
|
const sessionToolAllowlist = toSessionToolAllowlist(collectRegisteredToolNames(customTools));
|
|
|
|
const providerStreamFn = resolveCompactionProviderStream({
|
|
effectiveModel,
|
|
config: params.config,
|
|
agentDir,
|
|
effectiveWorkspace,
|
|
});
|
|
while (true) {
|
|
// Rebuild the compaction session on retry so provider wrappers, payload
|
|
// shaping, and the embedded system prompt all reflect the fallback level.
|
|
attemptedThinking.add(thinkLevel);
|
|
const systemPromptText = buildSystemPromptText(thinkLevel);
|
|
let session: Awaited<ReturnType<typeof createAgentSession>>["session"] | undefined;
|
|
try {
|
|
const createdSession = await createAgentSession({
|
|
cwd: effectiveCwd,
|
|
agentDir,
|
|
authStorage,
|
|
modelRegistry,
|
|
model: effectiveModel,
|
|
thinkingLevel: mapThinkingLevel(thinkLevel),
|
|
tools: sessionToolAllowlist,
|
|
customTools,
|
|
sessionManager,
|
|
settingsManager,
|
|
resourceLoader,
|
|
});
|
|
session = createdSession.session;
|
|
session.setActiveToolsByName(sessionToolAllowlist);
|
|
applySystemPromptToSession(session, systemPromptText);
|
|
// Compaction builds the same embedded system prompt, so it must flow
|
|
// through the same transport/payload shaping stack as normal turns.
|
|
await prepareCompactionSessionAgent({
|
|
session,
|
|
providerStreamFn,
|
|
sessionId: params.sessionId,
|
|
signal: runAbortController.signal,
|
|
effectiveModel,
|
|
resolvedApiKey: hasRuntimeAuthExchange ? undefined : apiKeyInfo?.apiKey,
|
|
authStorage,
|
|
config: params.config,
|
|
provider,
|
|
modelId,
|
|
thinkLevel,
|
|
sessionAgentId,
|
|
effectiveWorkspace,
|
|
agentDir,
|
|
runtimePlan,
|
|
sessionKey: sandboxSessionKey,
|
|
sandboxToolPolicy: sandbox?.tools,
|
|
messageProvider: resolvedMessageProvider,
|
|
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,
|
|
});
|
|
session.agent.streamFn = wrapStreamFnWithDiagnosticModelCallEvents(
|
|
session.agent.streamFn,
|
|
{
|
|
runId: diagnosticCompactionRunId,
|
|
...(params.sessionKey && { sessionKey: params.sessionKey }),
|
|
sessionId: params.sessionId,
|
|
provider,
|
|
model: modelId,
|
|
api: effectiveModel.api,
|
|
transport: session.agent.transport,
|
|
contextTokenBudget,
|
|
trace: compactionModelCallTrace,
|
|
contentCapture: resolveDiagnosticModelContentCapturePolicy(params.config),
|
|
nextCallId: () =>
|
|
`${diagnosticCompactionRunId}:model:${(diagnosticModelCallSeq += 1)}`,
|
|
},
|
|
);
|
|
|
|
const prior = await sanitizeSessionHistory({
|
|
messages: session.messages,
|
|
modelApi: effectiveModel.api,
|
|
modelId,
|
|
provider,
|
|
allowedToolNames,
|
|
config: params.config,
|
|
workspaceDir: effectiveWorkspace,
|
|
env: process.env,
|
|
model: effectiveModel,
|
|
sessionManager,
|
|
sessionId: params.sessionId,
|
|
policy: transcriptPolicy,
|
|
preserveLatestAssistantThinking: false,
|
|
});
|
|
const validated = await validateReplayTurns({
|
|
messages: prior,
|
|
modelApi: effectiveModel.api,
|
|
modelId,
|
|
provider,
|
|
config: params.config,
|
|
workspaceDir: effectiveWorkspace,
|
|
env: process.env,
|
|
model: effectiveModel,
|
|
sessionId: params.sessionId,
|
|
policy: transcriptPolicy,
|
|
});
|
|
const dedupedValidated = dedupeDuplicateUserMessagesForCompaction(validated);
|
|
// Apply validated transcript to the live session even when no history limit is configured,
|
|
// so compaction and hook metrics are based on the same message set.
|
|
session.agent.state.messages = dedupedValidated;
|
|
// "Original" compaction metrics should describe the validated transcript that enters
|
|
// limiting/compaction, not the raw on-disk session snapshot.
|
|
const originalMessages = session.messages.slice();
|
|
const truncated = limitHistoryTurns(
|
|
session.messages,
|
|
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",
|
|
...(effectiveModel.api === "openai-responses" ||
|
|
effectiveModel.api === "azure-openai-responses" ||
|
|
effectiveModel.api === "openai-chatgpt-responses"
|
|
? { missingToolResultText: "aborted" }
|
|
: {}),
|
|
})
|
|
: truncated;
|
|
if (limited.length > 0) {
|
|
session.agent.state.messages = limited;
|
|
}
|
|
const hookRunner = asCompactionHookRunner(getGlobalHookRunner());
|
|
const observedTokenCount = normalizeObservedTokenCount(params.currentTokenCount);
|
|
const beforeHookMetrics = buildBeforeCompactionHookMetrics({
|
|
originalMessages,
|
|
currentMessages: session.messages,
|
|
observedTokenCount,
|
|
estimateTokensFn: estimateTokens,
|
|
});
|
|
const { hookSessionKey, missingSessionKey } = await runBeforeCompactionHooks({
|
|
hookRunner,
|
|
sessionId: params.sessionId,
|
|
sessionKey: params.sessionKey,
|
|
sessionAgentId,
|
|
workspaceDir: effectiveWorkspace,
|
|
messageProvider: resolvedMessageProvider,
|
|
metrics: beforeHookMetrics,
|
|
onHookMessages: params.onCompactionHookMessages,
|
|
});
|
|
const { messageCountOriginal } = beforeHookMetrics;
|
|
const diagEnabled = log.isEnabled("debug");
|
|
const preMetrics = diagEnabled
|
|
? summarizeCompactionMessages(session.messages)
|
|
: undefined;
|
|
if (diagEnabled && preMetrics) {
|
|
log.debug(
|
|
`[compaction-diag] start runId=${runId} sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
|
`diagId=${diagId} trigger=${trigger} provider=${provider}/${modelId} ` +
|
|
`attempt=${attempt} maxAttempts=${maxAttempts} ` +
|
|
`pre.messages=${preMetrics.messages} pre.historyTextChars=${preMetrics.historyTextChars} ` +
|
|
`pre.toolResultChars=${preMetrics.toolResultChars} pre.estTokens=${preMetrics.estTokens ?? "unknown"}`,
|
|
);
|
|
log.debug(
|
|
`[compaction-diag] contributors diagId=${diagId} top=${JSON.stringify(preMetrics.contributors)}`,
|
|
);
|
|
}
|
|
|
|
if (!containsRealConversationMessages(session.messages)) {
|
|
log.info(
|
|
`[compaction] skipping — no real conversation messages (sessionKey=${params.sessionKey ?? params.sessionId})`,
|
|
);
|
|
return {
|
|
ok: true,
|
|
compacted: false,
|
|
reason: "no real conversation messages",
|
|
};
|
|
}
|
|
|
|
const compactStartedAt = Date.now();
|
|
// Measure compactedCount from the original pre-limiting transcript so compaction
|
|
// lifecycle metrics represent total reduction through the compaction pipeline.
|
|
const messageCountCompactionInput = messageCountOriginal;
|
|
// Estimate full session tokens BEFORE compaction (including system prompt,
|
|
// bootstrap context, workspace files, and all history). This is needed for
|
|
// a correct sanity check — result.tokensBefore only covers the summarizable
|
|
// history subset, not the full session.
|
|
let fullSessionTokensBefore = 0;
|
|
try {
|
|
fullSessionTokensBefore = limited.reduce((sum, msg) => sum + estimateTokens(msg), 0);
|
|
} catch {
|
|
// If token estimation throws on a malformed message, fall back to 0 so
|
|
// the sanity check below becomes a no-op instead of crashing compaction.
|
|
}
|
|
const activeSession = session;
|
|
const result = await compactWithSafetyTimeout(
|
|
() => {
|
|
setCompactionSafeguardCancelReason(compactionSessionManager, undefined);
|
|
return activeSession.compact(params.customInstructions);
|
|
},
|
|
compactionTimeoutMs,
|
|
{
|
|
abortSignal: params.abortSignal,
|
|
onCancel: () => {
|
|
activeSession.abortCompaction();
|
|
},
|
|
},
|
|
);
|
|
let effectiveFirstKeptEntryId = result.firstKeptEntryId;
|
|
let postCompactionLeafId =
|
|
typeof sessionManager.getLeafId === "function"
|
|
? (sessionManager.getLeafId() ?? undefined)
|
|
: undefined;
|
|
let transcriptRotationSessionManager: Parameters<
|
|
typeof rotateTranscriptAfterCompaction
|
|
>[0]["sessionManager"] = sessionManager;
|
|
if (params.trigger === "manual" && !isSqliteSessionTranscript) {
|
|
try {
|
|
const hardenedBoundary = await hardenManualCompactionBoundary({
|
|
sessionFile: params.sessionFile,
|
|
preserveRecentTail:
|
|
typeof params.config?.agents?.defaults?.compaction?.keepRecentTokens === "number",
|
|
});
|
|
if (hardenedBoundary.applied) {
|
|
effectiveFirstKeptEntryId =
|
|
hardenedBoundary.firstKeptEntryId ?? effectiveFirstKeptEntryId;
|
|
postCompactionLeafId = hardenedBoundary.leafId ?? postCompactionLeafId;
|
|
session.agent.state.messages = hardenedBoundary.messages;
|
|
transcriptRotationSessionManager = await readTranscriptFileState(
|
|
params.sessionFile,
|
|
);
|
|
}
|
|
} catch (err) {
|
|
log.warn("[compaction] failed to harden manual compaction boundary", {
|
|
errorMessage: formatErrorMessage(err),
|
|
});
|
|
}
|
|
}
|
|
// Estimate tokens after compaction by summing token estimates for remaining messages
|
|
const tokensAfter = estimateTokensAfterCompaction({
|
|
messagesAfter: session.messages,
|
|
observedTokenCount,
|
|
fullSessionTokensBefore,
|
|
estimateTokensFn: estimateTokens,
|
|
});
|
|
const messageCountAfter = session.messages.length;
|
|
const compactedCount = Math.max(0, messageCountCompactionInput - messageCountAfter);
|
|
let transcriptRotation: CompactionTranscriptRotation = { rotated: false };
|
|
if (shouldRotateCompactionTranscript(params.config) && !isSqliteSessionTranscript) {
|
|
try {
|
|
transcriptRotation = await rotateTranscriptAfterCompaction({
|
|
sessionManager: transcriptRotationSessionManager,
|
|
sessionFile: params.sessionFile,
|
|
});
|
|
} catch (err) {
|
|
log.warn("[compaction] post-compaction transcript rotation failed", {
|
|
errorMessage: formatErrorMessage(err),
|
|
errorStack: err instanceof Error ? err.stack : undefined,
|
|
});
|
|
}
|
|
}
|
|
const activeSessionId = transcriptRotation.sessionId ?? params.sessionId;
|
|
const activeSessionFile = transcriptRotation.sessionFile ?? params.sessionFile;
|
|
const activePostLeafId = transcriptRotation.leafId ?? postCompactionLeafId;
|
|
if (transcriptRotation.rotated) {
|
|
log.info(
|
|
`[compaction] rotated active transcript after compaction ` +
|
|
`(sessionKey=${params.sessionKey ?? params.sessionId})`,
|
|
);
|
|
}
|
|
await runPostCompactionSideEffects({
|
|
config: params.config,
|
|
sessionKey: params.sessionKey,
|
|
sessionId: activeSessionId,
|
|
agentId: sessionAgentId,
|
|
sessionFile: activeSessionFile,
|
|
});
|
|
if (params.config && params.sessionKey && checkpointSnapshot) {
|
|
try {
|
|
const transcriptState =
|
|
await readSessionLeafStateFromTranscriptAsync(activeSessionFile);
|
|
const checkpointPosition = resolveCompactionCheckpointTranscriptPosition({
|
|
preferredLeafId: activePostLeafId,
|
|
transcriptState,
|
|
});
|
|
const storedCheckpoint = await compactionCheckpointStore.persistCheckpoint({
|
|
cfg: params.config,
|
|
sessionKey: params.sessionKey,
|
|
sessionId: activeSessionId,
|
|
reason: resolveSessionCompactionCheckpointReason({
|
|
trigger: params.trigger,
|
|
}),
|
|
snapshot: checkpointSnapshot,
|
|
summary: result.summary,
|
|
firstKeptEntryId: effectiveFirstKeptEntryId,
|
|
tokensBefore: observedTokenCount ?? result.tokensBefore,
|
|
tokensAfter,
|
|
postSessionFile: activeSessionFile,
|
|
postLeafId: checkpointPosition.leafId,
|
|
postEntryId: checkpointPosition.entryId,
|
|
createdAt: compactStartedAt,
|
|
});
|
|
checkpointSnapshotRetained = storedCheckpoint !== null;
|
|
} catch (err) {
|
|
log.warn("failed to persist compaction checkpoint", {
|
|
errorMessage: formatErrorMessage(err),
|
|
});
|
|
}
|
|
}
|
|
const postMetrics = diagEnabled
|
|
? summarizeCompactionMessages(session.messages)
|
|
: undefined;
|
|
if (diagEnabled && preMetrics && postMetrics) {
|
|
log.debug(
|
|
`[compaction-diag] end runId=${runId} sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
|
`diagId=${diagId} trigger=${trigger} provider=${provider}/${modelId} ` +
|
|
`attempt=${attempt} maxAttempts=${maxAttempts} outcome=compacted reason=none ` +
|
|
`durationMs=${Date.now() - compactStartedAt} retrying=false ` +
|
|
`post.messages=${postMetrics.messages} post.historyTextChars=${postMetrics.historyTextChars} ` +
|
|
`post.toolResultChars=${postMetrics.toolResultChars} post.estTokens=${postMetrics.estTokens ?? "unknown"} ` +
|
|
`delta.messages=${postMetrics.messages - preMetrics.messages} ` +
|
|
`delta.historyTextChars=${postMetrics.historyTextChars - preMetrics.historyTextChars} ` +
|
|
`delta.toolResultChars=${postMetrics.toolResultChars - preMetrics.toolResultChars} ` +
|
|
`delta.estTokens=${typeof preMetrics.estTokens === "number" && typeof postMetrics.estTokens === "number" ? postMetrics.estTokens - preMetrics.estTokens : "unknown"}`,
|
|
);
|
|
}
|
|
await runAfterCompactionHooks({
|
|
hookRunner,
|
|
sessionId: activeSessionId,
|
|
sessionAgentId,
|
|
hookSessionKey,
|
|
missingSessionKey,
|
|
workspaceDir: effectiveWorkspace,
|
|
messageProvider: resolvedMessageProvider,
|
|
messageCountAfter,
|
|
tokensAfter,
|
|
compactedCount,
|
|
sessionFile: activeSessionFile,
|
|
...(activeSessionId !== params.sessionId
|
|
? { previousSessionId: params.sessionId }
|
|
: {}),
|
|
summaryLength: typeof result.summary === "string" ? result.summary.length : undefined,
|
|
tokensBefore: result.tokensBefore,
|
|
firstKeptEntryId: effectiveFirstKeptEntryId,
|
|
onHookMessages: params.onCompactionHookMessages,
|
|
});
|
|
return {
|
|
ok: true,
|
|
compacted: true,
|
|
result: {
|
|
summary: result.summary,
|
|
firstKeptEntryId: effectiveFirstKeptEntryId,
|
|
tokensBefore: observedTokenCount ?? result.tokensBefore,
|
|
tokensAfter,
|
|
details: result.details,
|
|
sessionId: transcriptRotation.sessionId,
|
|
sessionFile: transcriptRotation.sessionFile,
|
|
},
|
|
};
|
|
} catch (err) {
|
|
const fallbackThinking = pickFallbackThinkingLevel({
|
|
message: formatErrorMessage(err),
|
|
attempted: attemptedThinking,
|
|
});
|
|
if (fallbackThinking) {
|
|
// Near-term provider fix: when compaction hits a reasoning-mandatory
|
|
// endpoint with `off`, retry once with `minimal` instead of surfacing
|
|
// a user-visible failure.
|
|
log.warn(
|
|
`[compaction] request rejected for ${provider}/${modelId}; retrying with ${fallbackThinking}`,
|
|
);
|
|
thinkLevel = fallbackThinking;
|
|
continue;
|
|
}
|
|
throw err;
|
|
} finally {
|
|
try {
|
|
await flushPendingToolResultsAfterIdle({
|
|
agent: session?.agent,
|
|
sessionManager,
|
|
});
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
try {
|
|
session?.dispose();
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
try {
|
|
await bundleMcpRuntime?.dispose();
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
try {
|
|
await bundleLspRuntime?.dispose();
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
await sessionLock.release();
|
|
}
|
|
} catch (err) {
|
|
const reason = resolveCompactionFailureReason({
|
|
reason: formatErrorMessage(err),
|
|
safeguardCancelReason: consumeCompactionSafeguardCancelReason(compactionSessionManager),
|
|
});
|
|
return fail(reason, err);
|
|
} finally {
|
|
if (!checkpointSnapshotRetained) {
|
|
await compactionCheckpointStore.cleanupSnapshot(checkpointSnapshot);
|
|
}
|
|
restoreSkillEnv?.();
|
|
}
|
|
}
|
|
|
|
export const testing = {
|
|
hasRealConversationContent,
|
|
hasMeaningfulConversationContent,
|
|
containsRealConversationMessages,
|
|
estimateTokensAfterCompaction,
|
|
buildBeforeCompactionHookMetrics,
|
|
hardenManualCompactionBoundary,
|
|
resolveCompactionProviderStream,
|
|
prepareCompactionSessionAgent,
|
|
runBeforeCompactionHooks,
|
|
runAfterCompactionHooks,
|
|
runPostCompactionSideEffects,
|
|
} as const;
|
|
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|