mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
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:
committed by
GitHub
parent
d8ff371780
commit
f383064b29
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user