fix(agents): preserve admitted CLI session context (#130475)

* fix(agents): preserve admitted CLI session context

* fix(agents): snapshot CLI session after admission

* test(agents): reuse prepared CLI fixture
This commit is contained in:
Peter Steinberger
2026-08-26 18:15:35 -07:00
committed by GitHub
parent d8ff371780
commit f383064b29
5 changed files with 245 additions and 156 deletions
@@ -1,27 +1,16 @@
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import type { PreparedAgentRunAdmission } from "../../agents/admitted-run-context.js";
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
import type { BootstrapContextRunKind } from "../../agents/bootstrap-mode.js";
import type { RunCliAgentParams } from "../../agents/cli-runner/types.js";
import {
getCliSessionBinding,
shouldClearFailedCliSessionBinding,
} from "../../agents/cli-session.js";
import type { RunEmbeddedAgentParams } from "../../agents/embedded-agent-runner/run/params.js";
import type { FastModeAutoProgressState } from "../../agents/fast-mode.js";
import type { ContextEngineLogicalTurnLease } from "../../agents/harness/context-engine-logical-turn.js";
import { withLocalSessionPlacementTurnAdmission } from "../../agents/session-placement-admission.js";
import { normalizeChatType } from "../../channels/chat-type.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
getGeneratedMediaTaskIdsForSessionKey,
hasNewGeneratedMediaTaskForSessionKey,
} from "../../tasks/task-status-access.js";
import type { ThinkLevel } from "../thinking.js";
import {
createAgentLifecycleTerminalBackstop,
type AgentLifecycleTerminalBackstop,
} from "./agent-lifecycle-terminal.js";
import { createAgentLifecycleTerminalBackstop } from "./agent-lifecycle-terminal.js";
import { resolveRunAuthProfile } from "./agent-runner-auth-profile.js";
import {
clearCliSessionBindingForRun,
@@ -31,58 +20,19 @@ import {
runCliAgentWithLifecycle,
} from "./agent-runner-cli-dispatch.js";
import { buildCommandOutputFromToolResultEvent } from "./agent-runner-command-output.js";
import type { AgentTurnParams } from "./agent-runner-execution.types.js";
import type { createAgentTurnPresentation } from "./agent-runner-presentation.js";
import type { AgentTurnTimingTracker } from "./agent-runner-turn-timing.js";
import type { AgentFallbackCandidateCommonParams } from "./agent-runner-fallback-cycle.types.js";
import { shouldBridgeCliPreambleEvents } from "./get-reply.types.js";
import { hasInboundAudio } from "./inbound-media.js";
import { resolveOriginMessageProvider } from "./origin-routing.js";
import type { FollowupRun } from "./queue.js";
import { resolveReplyOperationTerminationFields } from "./reply-operation-abort.js";
import { resolveFollowupRunToolAuthorityFingerprint } from "./reply-tool-authority.js";
type CliPresentation = Pick<
ReturnType<typeof createAgentTurnPresentation>,
| "blockReplyHandler"
| "classifyStreamingPartial"
| "sanitizeStreamingText"
| "startPresentationWhileTyping"
>;
export async function runCliFallbackCandidate(params: {
preparedRunAdmission: PreparedAgentRunAdmission;
turn: AgentTurnParams;
candidateRun: FollowupRun["run"];
runtimeConfig: OpenClawConfig;
provider: string;
model: string;
cliExecutionProvider: string;
candidateThinkLevel?: ThinkLevel;
candidateFastMode: Pick<RunCliAgentParams, "fastMode" | "fastModeAutoOnSeconds">;
runId: string;
lifecycleGeneration: string;
runAbortSignal?: AbortSignal;
runLane: RunCliAgentParams["lane"];
isFinalFallbackAttempt?: boolean;
suppressQueuedUserPersistenceForCandidate: boolean;
userTurnTranscriptRecorder: RunCliAgentParams["userTurnTranscriptRecorder"];
contextEngineLogicalTurnLease: ContextEngineLogicalTurnLease;
onContextEngineTurnCandidate: RunCliAgentParams["onContextEngineTurnCandidate"];
notifyUserMessagePersisted: () => void;
fastModeStartedAtMs: number;
fastModeAutoProgressState: FastModeAutoProgressState;
bootstrapContextRunKind: BootstrapContextRunKind;
bootstrapPromptWarningSignaturesSeen: string[];
currentTurnImages: Awaited<
ReturnType<typeof import("./current-turn-images.js").resolveCurrentTurnImages>
>;
signalExecutionPhaseForTyping: NonNullable<RunEmbeddedAgentParams["onExecutionPhase"]>;
notifyAgentRunStart: () => void;
preserveProgressCallbackStartOrder: boolean;
presentation: CliPresentation;
timing: AgentTurnTimingTracker;
onLifecycleBackstop: (backstop: AgentLifecycleTerminalBackstop) => void;
}): Promise<{
export async function runCliFallbackCandidate(
params: AgentFallbackCandidateCommonParams & {
cliExecutionProvider: string;
lifecycleGeneration: string;
},
): Promise<{
result: Awaited<ReturnType<typeof runCliAgentWithLifecycle>>;
bootstrapPromptWarningSignaturesSeen: string[];
}> {
@@ -103,10 +53,6 @@ export async function runCliFallbackCandidate(params: {
storePath: turn.storePath,
}
: undefined;
const cliSessionBinding = getCliSessionBinding(
turn.getActiveSessionEntry(),
params.cliExecutionProvider,
);
const cliLifecycleStartedAt = Date.now();
const lifecycleBackstop = createAgentLifecycleTerminalBackstop({
runId: params.runId,
@@ -120,7 +66,6 @@ export async function runCliFallbackCandidate(params: {
const authProfile = resolveRunAuthProfile(params.candidateRun, params.cliExecutionProvider, {
config: params.runtimeConfig,
});
let droppedCliSessionReplacement = false;
const hookMessageProvider = resolveOriginMessageProvider({
originatingChannel: turn.followupRun.originatingChannel,
provider: turn.sessionCtx.Provider,
@@ -186,10 +131,13 @@ export async function runCliFallbackCandidate(params: {
runId: params.runId,
},
async () => {
// Admission may wait behind another turn that starts detached media.
// Snapshot only after this turn owns the session placement.
// Placement admission may wait behind an older turn. Snapshot placement,
// permission, and native resume identity only after this turn owns it.
const sessionEntry = turn.getActiveSessionEntry();
const cliSessionBinding = getCliSessionBinding(sessionEntry, params.cliExecutionProvider);
const mediaTaskIdsBefore = getGeneratedMediaTaskIdsForSessionKey(turn.sessionKey);
return await runCliAgentWithLifecycle({
let droppedCliSessionReplacement = false;
const candidateResult = await runCliAgentWithLifecycle({
runId: params.runId,
lifecycleGeneration: params.lifecycleGeneration,
provider: params.cliExecutionProvider,
@@ -349,6 +297,7 @@ export async function runCliFallbackCandidate(params: {
sessionId: turn.followupRun.run.sessionId,
sessionKey: turn.sessionKey,
sessionTarget,
sessionEntry,
chatType:
normalizeChatType(turn.followupRun.originatingChatType) ??
normalizeChatType(turn.sessionCtx.ChatType) ??
@@ -381,7 +330,7 @@ export async function runCliFallbackCandidate(params: {
modelHasVision,
modelContextWindow: selectedModelEntry?.contextWindow,
modelContextTokens: selectedModelEntry?.contextTokens,
contextWindow: turn.getActiveSessionEntry()?.contextWindow,
contextWindow: sessionEntry?.contextWindow,
provider: params.cliExecutionProvider,
execOverrides: turn.followupRun.run.execOverrides,
bashElevated: turn.followupRun.run.bashElevated,
@@ -454,19 +403,20 @@ export async function runCliFallbackCandidate(params: {
replyOperation: turn.replyOperation,
},
});
if (droppedCliSessionReplacement) {
await clearCliSessionBindingForRun({
provider: params.cliExecutionProvider,
expectedSessionId: cliSessionBinding?.sessionId,
sessionKey: turn.sessionKey,
sessionStore: turn.activeSessionStore,
storePath: turn.storePath,
activeSessionEntry: turn.getActiveSessionEntry(),
});
}
return candidateResult;
},
),
);
if (droppedCliSessionReplacement) {
await clearCliSessionBindingForRun({
provider: params.cliExecutionProvider,
expectedSessionId: cliSessionBinding?.sessionId,
sessionKey: turn.sessionKey,
sessionStore: turn.activeSessionStore,
storePath: turn.storePath,
activeSessionEntry: turn.getActiveSessionEntry(),
});
}
return {
result,
bootstrapPromptWarningSignaturesSeen: resolveBootstrapWarningSignaturesSeen(
@@ -1,16 +1,10 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { PreparedAgentRunAdmission } from "../../agents/admitted-run-context.js";
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
import type { BootstrapContextRunKind } from "../../agents/bootstrap-mode.js";
import type { RunEmbeddedAgentInternalParams } from "../../agents/embedded-agent-runner/run/internal-params.js";
import type { RunEmbeddedAgentParams } from "../../agents/embedded-agent-runner/run/params.js";
import { runEmbeddedAgent } from "../../agents/embedded-agent.js";
import type { FastModeAutoProgressState } from "../../agents/fast-mode.js";
import type { ContextEngineLogicalTurnLease } from "../../agents/harness/context-engine-logical-turn.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
import { resolveOpenAIRuntimeProvider } from "../../agents/openai-routing.js";
import { resolveGroupSessionKey } from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
isTrustedMessageActionTurnIngress,
mintMessageActionTurnCapability,
@@ -23,21 +17,14 @@ import {
resolveMessageChannel,
} from "../../utils/message-channel.js";
import type { PartialReplyPayload } from "../get-reply-options.types.js";
import type { ThinkLevel } from "../thinking.js";
import type { ReplyPayload } from "../types.js";
import {
createAgentLifecycleTerminalBackstop,
type AgentLifecycleTerminalBackstop,
} from "./agent-lifecycle-terminal.js";
import { createAgentLifecycleTerminalBackstop } from "./agent-lifecycle-terminal.js";
import {
createAgentRunEventHandler,
type MessageToolDeliveryState,
} from "./agent-runner-event-handler.js";
import type { AgentTurnParams } from "./agent-runner-execution.types.js";
import type { createAgentTurnPresentation } from "./agent-runner-presentation.js";
import type { AgentTurnTimingTracker } from "./agent-runner-turn-timing.js";
import type { AgentFallbackCandidateCommonParams } from "./agent-runner-fallback-cycle.types.js";
import { buildEmbeddedRunExecutionParams } from "./agent-runner-utils.js";
import type { FollowupRun } from "./queue.js";
import { resolveReplyOperationTerminationFields } from "./reply-operation-abort.js";
import { markReplyOperationGlobalLaneWaitProgress } from "./reply-run-registry.js";
import { resolveFollowupRunToolAuthorityFingerprint } from "./reply-tool-authority.js";
@@ -46,60 +33,21 @@ import {
readSourceReplyDeliveryRuntime,
} from "./source-reply-delivery-runtime.js";
type EmbeddedPresentation = Pick<
ReturnType<typeof createAgentTurnPresentation>,
| "classifyStreamingPartial"
| "sanitizeStreamingText"
| "normalizeStreamingText"
| "startPresentationWhileTyping"
| "blockReplyHandler"
>;
export async function runEmbeddedFallbackCandidate(params: {
preparedRunAdmission: PreparedAgentRunAdmission;
turn: AgentTurnParams;
effectiveRun: FollowupRun["run"];
candidateRun: FollowupRun["run"];
runtimeConfig: OpenClawConfig;
provider: string;
model: string;
sessionRuntimeOverride?: string;
candidateThinkLevel?: ThinkLevel;
candidateFastMode: Pick<RunEmbeddedAgentParams, "fastMode" | "fastModeAutoOnSeconds">;
runLane: RunEmbeddedAgentParams["lane"];
runId: string;
getLifecycleGeneration: () => string;
onLifecycleGeneration: (generation: string) => void;
runAbortSignal?: AbortSignal;
allowTransientCooldownProbe?: boolean;
isFinalFallbackAttempt?: boolean;
suppressQueuedUserPersistenceForCandidate: boolean;
suppressAssistantErrorPersistenceForCandidate: boolean;
onAssistantErrorMessagePersisted: () => void;
userTurnTranscriptRecorder: NonNullable<AgentTurnParams["opts"]>["userTurnTranscriptRecorder"];
contextEngineLogicalTurnLease: ContextEngineLogicalTurnLease;
onContextEngineTurnCandidate: RunEmbeddedAgentParams["onContextEngineTurnCandidate"];
notifyUserMessagePersisted: () => void;
fastModeStartedAtMs: number;
fastModeAutoProgressState: FastModeAutoProgressState;
bootstrapContextRunKind: BootstrapContextRunKind;
bootstrapPromptWarningSignaturesSeen: string[];
currentTurnImages: Awaited<
ReturnType<typeof import("./current-turn-images.js").resolveCurrentTurnImages>
>;
signalExecutionPhaseForTyping: NonNullable<
Parameters<typeof runEmbeddedAgent>[0]["onExecutionPhase"]
>;
notifyAgentRunStart: () => void;
notifyUserAboutCompaction: boolean;
messageToolDeliveryState: MessageToolDeliveryState;
preserveProgressCallbackStartOrder: boolean;
githubPublicationAvailable: boolean;
presentation: EmbeddedPresentation;
timing: AgentTurnTimingTracker;
onLifecycleBackstop: (backstop: AgentLifecycleTerminalBackstop) => void;
onCompactionCount: (count: number) => void;
}): Promise<{
export async function runEmbeddedFallbackCandidate(
params: AgentFallbackCandidateCommonParams & {
effectiveRun: AgentFallbackCandidateCommonParams["candidateRun"];
sessionRuntimeOverride?: string;
getLifecycleGeneration: () => string;
onLifecycleGeneration: (generation: string) => void;
allowTransientCooldownProbe?: boolean;
suppressAssistantErrorPersistenceForCandidate: boolean;
onAssistantErrorMessagePersisted: () => void;
notifyUserAboutCompaction: boolean;
messageToolDeliveryState: MessageToolDeliveryState;
githubPublicationAvailable: boolean;
onCompactionCount: (count: number) => void;
},
): Promise<{
result: Awaited<ReturnType<typeof runEmbeddedAgent>>;
bootstrapPromptWarningSignaturesSeen: string[];
}> {
@@ -1,5 +1,9 @@
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildPreparedCliRunContext } from "../../agents/cli-runner.test-helpers.js";
import { executeDeps } from "../../agents/cli-runner/execute-deps.js";
import { executePreparedCliRun } from "../../agents/cli-runner/execute.js";
import { prepareCliPromptImagePayload } from "../../agents/cli-runner/helpers.js";
import { buildCliMcpGrantContext } from "../../agents/cli-runner/mcp-grant-context.js";
import type { RunCliAgentParams } from "../../agents/cli-runner/types.js";
import { detectAndLoadPromptImages } from "../../agents/embedded-agent-runner/run/images.js";
import { FailoverError } from "../../agents/failover-error.js";
@@ -27,6 +31,137 @@ const state = setupAgentRunnerExecutionTestState();
afterEach(resetGeneratedMediaTaskActivityForTests);
describe("executeAgentTurn: CLI session routing", () => {
it("carries the admitted session permission and placement into the CLI grant", async () => {
state.isCliProviderMock.mockReturnValue(true);
state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({
result: await params.run(
"claude-cli",
"claude-sonnet-4-6",
initialFallbackAttemptOptions(params),
),
provider: "claude-cli",
model: "claude-sonnet-4-6",
attempts: [],
}));
let sessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: 1,
permissionMode: "guarded",
sessionRoot: "/workspace/old",
execHost: "gateway",
cliSessionBindings: {
"claude-cli": { sessionId: "old-native-session", forceReuse: true },
},
};
const admittedSessionEntry: SessionEntry = {
sessionId: "session",
updatedAt: 2,
permissionMode: "read-only",
sessionRoot: "/workspace/project",
execHost: "node",
execNode: "node-a",
execCwd: "/workspace/project/task",
cliSessionBindings: {
"claude-cli": { sessionId: "new-native-session", forceReuse: true },
},
};
state.runCliAgentMock.mockResolvedValueOnce({ payloads: [{ text: "done" }], meta: {} });
const followupRun = createFollowupRun();
followupRun.run.provider = "claude-cli";
followupRun.run.model = "claude-sonnet-4-6";
const restoreAdmission = installSessionPlacementAdmissionProvider({
executeLocalTurn: async (_claim, runLocal) => {
sessionEntry = admittedSessionEntry;
return await runLocal();
},
executeTurn: async (_claim, _params, runLocal) => await runLocal(),
});
try {
const executeAgentTurn = await getExecuteAgentTurnForTest();
const result = await executeAgentTurn({
...createMinimalRunAgentTurnParams({ followupRun }),
getActiveSessionEntry: () => sessionEntry,
});
expect(result.kind).toBe("success");
const run = requireMockCall(
state.runCliAgentMock,
0,
"CLI run params",
)[0] as RunCliAgentParams;
expect(run.sessionEntry).toBe(admittedSessionEntry);
expect(run.sessionEntry?.sessionRoot).toBe("/workspace/project");
expect(run.cliSessionId).toBe("new-native-session");
expect(run.cliSessionBinding).toMatchObject({
sessionId: "new-native-session",
forceReuse: true,
});
const observedCliSessionId = run.cliSessionBinding?.sessionId ?? run.cliSessionId;
expect(observedCliSessionId).toBe("new-native-session");
if (!observedCliSessionId) {
throw new Error("expected admitted CLI session binding");
}
expect(
buildCliMcpGrantContext({
run,
config: run.config ?? {},
requireExplicitMessageTarget: false,
agentId: "main",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
}).execSession,
).toMatchObject({
permissionMode: "read-only",
execHost: "node",
execNode: "node-a",
});
const nodeInvoke = vi.fn<typeof executeDeps.invokeNodeClaudeCliRun>(async (request) => {
expect(request.nodeId).toBe("node-a");
expect(request.argv).toContain("new-native-session");
expect(request.argv).not.toContain("old-native-session");
return {
ok: true,
payloadJSON: JSON.stringify({ exitCode: 0, stderrTail: "", truncated: false }),
};
});
const restoreNodeInvoke = executeDeps.invokeNodeClaudeCliRun;
const backend = {
command: "claude",
args: ["-p"],
resumeArgs: ["--resume", "{sessionId}"],
output: "text" as const,
input: "stdin" as const,
serialize: true,
};
const prepared = buildPreparedCliRunContext({
provider: "claude-cli",
model: run.model,
runId: run.runId,
workspaceDir: run.workspaceDir,
config: run.config,
backend,
});
prepared.params = {
...run,
admittedRunContext: prepared.params.admittedRunContext,
skillsSnapshot: undefined,
};
prepared.cwd = run.cwd;
prepared.reusableCliSession = { mode: "reuse", sessionId: observedCliSessionId };
executeDeps.invokeNodeClaudeCliRun = nodeInvoke;
try {
await executePreparedCliRun(prepared, observedCliSessionId);
} finally {
executeDeps.invokeNodeClaudeCliRun = restoreNodeInvoke;
}
expect(nodeInvoke).toHaveBeenCalledOnce();
} finally {
restoreAdmission();
}
});
it("carries prepared model and thread context facts into CLI execution", async () => {
state.isCliProviderMock.mockReturnValue(true);
state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({
@@ -561,14 +696,30 @@ describe("executeAgentTurn: CLI session routing", () => {
},
} as unknown as SessionEntry;
const activeSessionStore = { main: sessionEntry };
const result = await executeAgentTurn({
...createMinimalRunAgentTurnParams({ followupRun }),
activeSessionStore,
getActiveSessionEntry: () => sessionEntry,
let cleanupObservedBeforePlacementRelease = false;
const restoreAdmission = installSessionPlacementAdmissionProvider({
executeLocalTurn: async (_claim, runLocal) => {
const resultLocal = await runLocal();
expect(activeSessionStore.main.cliSessionBindings?.["codex-cli"]).toBeUndefined();
cleanupObservedBeforePlacementRelease = true;
return resultLocal;
},
executeTurn: async (_claim, _params, runLocal) => await runLocal(),
});
let result: Awaited<ReturnType<typeof executeAgentTurn>>;
try {
result = await executeAgentTurn({
...createMinimalRunAgentTurnParams({ followupRun }),
activeSessionStore,
getActiveSessionEntry: () => sessionEntry,
});
} finally {
restoreAdmission();
}
expect(result.kind).toBe("success");
expect(cleanupObservedBeforePlacementRelease).toBe(true);
expectMockCallArgFields(state.runCliAgentMock, 0, "CLI run params", {
currentInboundEventKind: "room_event",
cliSessionId: "existing-cli-session",
@@ -16,7 +16,10 @@ import { runCliFallbackCandidate } from "./agent-runner-cli-candidate.js";
import { runEmbeddedFallbackCandidate } from "./agent-runner-embedded-candidate.js";
import type { MessageToolDeliveryState } from "./agent-runner-event-handler.js";
import type { EmbeddedAgentRunResult } from "./agent-runner-execution.types.js";
import type { AgentFallbackCycleParams } from "./agent-runner-fallback-cycle.types.js";
import type {
AgentFallbackCandidateCommonParams,
AgentFallbackCycleParams,
} from "./agent-runner-fallback-cycle.types.js";
import { emitModelFallbackStepLifecycle } from "./agent-runner-model-fallback-lifecycle.js";
import {
resolveModelFallbackOptions,
@@ -240,6 +243,7 @@ export async function runAgentFallbackCandidates(params: AgentFallbackCycleParam
candidateFastMode,
runId: params.runId,
runAbortSignal: params.runAbortSignal,
runLane,
isFinalFallbackAttempt: runOptions?.isFinalFallbackAttempt,
suppressQueuedUserPersistenceForCandidate:
(turn.followupRun.run.suppressNextUserMessagePersistence ?? false) ||
@@ -263,13 +267,12 @@ export async function runAgentFallbackCandidates(params: AgentFallbackCycleParam
onLifecycleBackstop: (backstop: AgentLifecycleTerminalBackstop) => {
params.state.pendingLifecycleTerminal = { provider, model, backstop };
},
};
} satisfies AgentFallbackCandidateCommonParams;
if (runtime.useCliExecution) {
const candidate = await runCliFallbackCandidate({
...common,
cliExecutionProvider: runtime.cliExecutionProvider,
lifecycleGeneration: params.state.lifecycleGeneration,
runLane,
});
params.state.bootstrapPromptWarningSignaturesSeen =
candidate.bootstrapPromptWarningSignaturesSeen;
@@ -277,7 +280,6 @@ export async function runAgentFallbackCandidates(params: AgentFallbackCycleParam
}
const candidate = await runEmbeddedFallbackCandidate({
...common,
runLane,
githubPublicationAvailable: await (githubPublicationAvailability ??=
turn.sessionKey && params.effectiveRun.agentId
? prepareGitHubPublicationAvailability({
@@ -1,7 +1,11 @@
import type { PreparedAgentRunAdmission } from "../../agents/admitted-run-context.js";
import type { BootstrapContextRunKind } from "../../agents/bootstrap-mode.js";
import type { RunEmbeddedAgentParams } from "../../agents/embedded-agent-runner/run/params.js";
import type { FastModeAutoProgressState } from "../../agents/fast-mode.js";
import type { ContextEngineLogicalTurnLease } from "../../agents/harness/context-engine-logical-turn.js";
import type { SessionEntry } from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { ThinkLevel } from "../thinking.js";
import type { AgentLifecycleTerminalBackstop } from "./agent-lifecycle-terminal.js";
import type {
AgentTurnInternalResult,
@@ -13,6 +17,40 @@ import type { createAgentTurnPresentation } from "./agent-runner-presentation.js
import type { AgentTurnTimingTracker } from "./agent-runner-turn-timing.js";
import type { FollowupRun } from "./queue.js";
/** Inputs prepared once per fallback candidate and consumed by either runtime adapter. */
export type AgentFallbackCandidateCommonParams = {
preparedRunAdmission: PreparedAgentRunAdmission;
turn: AgentTurnParams;
candidateRun: FollowupRun["run"];
runtimeConfig: OpenClawConfig;
provider: string;
model: string;
candidateThinkLevel?: ThinkLevel;
candidateFastMode: Pick<RunEmbeddedAgentParams, "fastMode" | "fastModeAutoOnSeconds">;
runId: string;
runAbortSignal?: AbortSignal;
runLane: RunEmbeddedAgentParams["lane"];
isFinalFallbackAttempt?: boolean;
suppressQueuedUserPersistenceForCandidate: boolean;
userTurnTranscriptRecorder: RunEmbeddedAgentParams["userTurnTranscriptRecorder"];
contextEngineLogicalTurnLease: ContextEngineLogicalTurnLease;
onContextEngineTurnCandidate: RunEmbeddedAgentParams["onContextEngineTurnCandidate"];
notifyUserMessagePersisted: () => void;
fastModeStartedAtMs: number;
fastModeAutoProgressState: FastModeAutoProgressState;
bootstrapContextRunKind: BootstrapContextRunKind;
bootstrapPromptWarningSignaturesSeen: string[];
currentTurnImages: Awaited<
ReturnType<typeof import("./current-turn-images.js").resolveCurrentTurnImages>
>;
signalExecutionPhaseForTyping: NonNullable<RunEmbeddedAgentParams["onExecutionPhase"]>;
notifyAgentRunStart: () => void;
preserveProgressCallbackStartOrder: boolean;
presentation: ReturnType<typeof createAgentTurnPresentation>;
timing: AgentTurnTimingTracker;
onLifecycleBackstop: (backstop: AgentLifecycleTerminalBackstop) => void;
};
export type AgentFallbackCycleState = {
lifecycleGeneration: string;
autoCompactionCount: number;