// Covers CLI-backed attempt execution and session-binding persistence. import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { SessionEntry } from "../../config/sessions.js"; import { clearSessionStoreCacheForTest } from "../../config/sessions/store.js"; import { appendSessionTranscriptMessage } from "../../config/sessions/transcript-append.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { captureEnv, setTestEnvValue } from "../../test-utils/env.js"; import { saveAuthProfileStore } from "../auth-profiles/store.js"; import type { EmbeddedAgentRunResult } from "../embedded-agent.js"; import { FailoverError } from "../failover-error.js"; import { persistCliTurnTranscript, runAgentAttempt as runAgentAttemptImpl, } from "./attempt-execution.js"; import { resolveClaudeCliProjectDirForWorkspace } from "./claude-cli-project-dir.js"; type RunAgentAttemptParams = Parameters[0]; const runAgentAttempt = ( params: Omit & Partial>, ) => runAgentAttemptImpl({ ...params, lifecycleGeneration: params.lifecycleGeneration ?? "test-generation", }); const runCliAgentMock = vi.hoisted(() => vi.fn()); const runEmbeddedAgentMock = vi.hoisted(() => vi.fn()); const providerAuthAliasMocks = vi.hoisted(() => ({ resolveProviderAuthAliasMap: vi.fn(() => ({})), resolveProviderIdForAuth: vi.fn( ( provider: string, params?: { metadataSnapshot?: { plugins?: readonly { providerAuthAliases?: Record }[]; }; }, ) => { const normalized = provider.trim().toLowerCase(); for (const plugin of params?.metadataSnapshot?.plugins ?? []) { const alias = plugin.providerAuthAliases?.[normalized]?.trim(); if (alias) { return alias.toLowerCase(); } } return ["codex-cli", "openai"].includes(normalized) ? "openai" : normalized; }, ), })); vi.mock("../cli-runner.js", () => ({ runCliAgent: runCliAgentMock, })); vi.mock("../model-selection.js", () => ({ isCliProvider: (provider: string) => provider.trim().toLowerCase() === "claude-cli" || provider.trim().toLowerCase() === "codex-cli" || provider.trim().toLowerCase() === "google-gemini-cli", normalizeProviderId: (provider: string) => provider.trim().toLowerCase(), })); vi.mock("../provider-auth-aliases.js", () => ({ resolveProviderAuthAliasMap: providerAuthAliasMocks.resolveProviderAuthAliasMap, resolveProviderIdForAuth: providerAuthAliasMocks.resolveProviderIdForAuth, })); vi.mock("../model-runtime-aliases.js", async () => { const actual = await vi.importActual( "../model-runtime-aliases.js", ); return { ...actual, resolveCliRuntimeExecutionProvider: ({ provider, cfg, modelId, }: { provider?: string; cfg?: OpenClawConfig; modelId?: string; }) => { const key = provider && modelId ? `${provider}/${modelId}` : undefined; // Runtime alias tests only need the model-level runtime override path; // keeping the mock narrow avoids loading provider catalogs here. const runtime = key ? cfg?.agents?.defaults?.models?.[key]?.agentRuntime?.id?.trim() : undefined; return runtime || provider; }, }; }); vi.mock("../embedded-agent.js", () => ({ runEmbeddedAgent: runEmbeddedAgentMock, })); function makeCliResult(text: string): EmbeddedAgentRunResult { return { payloads: [{ text }], meta: { durationMs: 5, finalAssistantVisibleText: text, agentMeta: { sessionId: "session-cli", provider: "claude-cli", model: "opus", usage: { input: 12, output: 4, cacheRead: 3, cacheWrite: 0, total: 19, }, }, executionTrace: { winnerProvider: "claude-cli", winnerModel: "opus", fallbackUsed: false, runner: "cli", }, }, }; } async function persistCliTranscriptEntry( params: Parameters[0], ): Promise { const result = await persistCliTurnTranscript(params); if (result.kind !== "persisted") { throw new Error("expected CLI transcript persistence to keep the current session"); } return result.sessionEntry; } async function readSessionMessages(sessionFile: string) { return (await readSessionFileJsonLines<{ type?: string; message?: unknown }>(sessionFile)) .filter((entry) => entry.type === "message") .map( (entry) => entry.message as { role?: string; content?: unknown; provider?: string; model?: string }, ); } async function readSessionFileEntries(sessionFile: string) { return await readSessionFileJsonLines<{ type?: string; id?: string; parentId?: string | null; cwd?: string; message?: { role?: string }; }>(sessionFile); } async function readSessionFileJsonLines(sessionFile: string): Promise { // Session transcripts are JSONL; tests preserve that format so parent/child // id ordering and append behavior are covered end-to-end. const raw = await fs.readFile(sessionFile, "utf-8"); const entries: T[] = []; for (const line of raw.split(/\r?\n/)) { if (line.length === 0) { continue; } entries.push(JSON.parse(line) as T); } return entries; } function requireRecord(value: unknown, label: string): Record { if (typeof value !== "object" || value === null) { throw new Error(`${label} was not an object`); } return value as Record; } function expectRecordFields(record: Record, fields: Record) { for (const [key, value] of Object.entries(fields)) { expect(record[key]).toEqual(value); } } function requireMockArg(mock: ReturnType, callIndex: number, label: string) { const arg = mock.mock.calls[callIndex]?.[0]; if (arg === undefined) { throw new Error(`Expected mock argument for ${label}`); } return requireRecord(arg, label); } function expectMockArgFields( mock: ReturnType, fields: Record, callIndex = 0, ) { expectRecordFields(requireMockArg(mock, callIndex, "mock call argument"), fields); } function firstRunCliAgentArg(callIndex = 0) { return requireMockArg(runCliAgentMock, callIndex, "run CLI agent argument"); } function firstEmbeddedAgentArg(callIndex = 0) { return requireMockArg(runEmbeddedAgentMock, callIndex, "embedded OpenClaw agent argument"); } describe("CLI attempt execution", () => { let tmpDir: string; let storePath: string; let homeEnvSnapshot: ReturnType | undefined; async function runOpenClawEmbeddedAttemptForTest(overrides?: { opts?: Partial; runId?: string; }) { const runId = overrides?.runId ?? "run-embedded-live-stream-gate"; const sessionKey = `agent:main:direct:${runId}`; const sessionEntry: SessionEntry = { sessionId: `session-${runId}`, updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", originalProvider: "openai", modelOverride: "gpt-5.4", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, `${runId}.jsonl`), workspaceDir: tmpDir, body: "stream gate", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId, opts: { message: "stream gate", ...overrides?.opts, } as RunAgentAttemptParams["opts"], runContext: {} as RunAgentAttemptParams["runContext"], spawnedBy: undefined, messageChannel: "telegram", skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionStore, storePath, sessionHasHistory: false, }); return firstEmbeddedAgentArg(); } beforeEach(async () => { homeEnvSnapshot = captureEnv(["HOME"]); tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-attempt-")); storePath = path.join(tmpDir, "sessions.json"); runCliAgentMock.mockReset(); runEmbeddedAgentMock.mockReset(); providerAuthAliasMocks.resolveProviderAuthAliasMap.mockClear(); providerAuthAliasMocks.resolveProviderIdForAuth.mockClear(); }); afterEach(async () => { vi.useRealTimers(); homeEnvSnapshot?.restore(); homeEnvSnapshot = undefined; await fs.rm(tmpDir, { recursive: true, force: true }); }); async function runClaudeCliAttempt(params: { sessionKey: string; sessionEntry: SessionEntry; sessionStore: Record; body: string; runId: string; cwd?: string; }) { await runAgentAttempt({ providerOverride: "claude-cli", originalProvider: "claude-cli", modelOverride: "opus", cfg: {} as OpenClawConfig, sessionEntry: params.sessionEntry, sessionId: params.sessionEntry.sessionId, sessionKey: params.sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, cwd: params.cwd, body: params.body, isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: params.runId, opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "claude-cli", sessionStore: params.sessionStore, storePath, sessionHasHistory: false, }); } async function writeClaudeCliAssistantTranscript(cliSessionId: string) { // Claude stores resumable sessions under a workspace-derived project dir, // so stale-session tests must create the same on-disk shape. const homeDir = path.join(tmpDir, `home-${cliSessionId}`); const projectsDir = resolveClaudeCliProjectDirForWorkspace({ workspaceDir: tmpDir, homeDir, }); setTestEnvValue("HOME", homeDir); await fs.mkdir(projectsDir, { recursive: true }); await fs.writeFile( path.join(projectsDir, `${cliSessionId}.jsonl`), `${JSON.stringify({ type: "assistant", message: { role: "assistant", content: [{ type: "text", text: "old reply" }] }, })}\n`, "utf-8", ); } function makeClaudeCliSessionEntry( openclawSessionId: string, cliSessionId: string, ): SessionEntry { return { sessionId: openclawSessionId, updatedAt: Date.now(), cliSessionBindings: { "claude-cli": { sessionId: cliSessionId, authProfileId: "anthropic:claude-cli", }, }, cliSessionIds: { "claude-cli": cliSessionId }, claudeCliSessionId: cliSessionId, }; } it("clears stale Claude CLI session IDs before a fresh retry after session expiration", async () => { const sessionKey = "agent:main:subagent:cli-expired"; const homeDir = path.join(tmpDir, "home"); const projectsDir = resolveClaudeCliProjectDirForWorkspace({ workspaceDir: tmpDir, homeDir, }); setTestEnvValue("HOME", homeDir); await fs.mkdir(projectsDir, { recursive: true }); await fs.writeFile( path.join(projectsDir, "stale-cli-session.jsonl"), `${JSON.stringify({ type: "assistant", message: { role: "assistant", content: [{ type: "text", text: "old reply" }] }, })}\n`, "utf-8", ); const sessionEntry: SessionEntry = { sessionId: "session-cli-123", updatedAt: Date.now(), cliSessionIds: { "claude-cli": "stale-cli-session" }, claudeCliSessionId: "stale-legacy-session", }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); // The retry hook must clear poisoned bindings before the fresh CLI attempt // runs, otherwise the runner would resume the same expired Claude session. runCliAgentMock.mockImplementationOnce(async (args: unknown) => { const retry = requireRecord(args, "run CLI agent argument").onBeforeFreshCliSessionRetry; expect(retry).toBeTypeOf("function"); await ( retry as (params: { provider: string; reason: "session_expired"; sessionId: string; }) => Promise )({ provider: "claude-cli", reason: "session_expired", sessionId: "stale-cli-session", }); expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.claudeCliSessionId).toBeUndefined(); return makeCliResult("hello from cli"); }); await runAgentAttempt({ providerOverride: "claude-cli", originalProvider: "claude-cli", modelOverride: "opus", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "retry this", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-cli-expired", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "claude-cli", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().cliSessionId).toBe("stale-cli-session"); expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.claudeCliSessionId).toBeUndefined(); const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< string, SessionEntry >; expect(persisted[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined(); expect(persisted[sessionKey]?.claudeCliSessionId).toBeUndefined(); }); it("clears reused Claude CLI session IDs after AbortError without retrying", async () => { const sessionKey = "agent:main:direct:cli-abort"; const cliSessionId = "abort-poisoned-session"; await writeClaudeCliAssistantTranscript(cliSessionId); const sessionEntry = makeClaudeCliSessionEntry("session-cli-abort", cliSessionId); const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); const abortError = Object.assign(new Error("aborted"), { name: "AbortError" }); runCliAgentMock.mockRejectedValueOnce(abortError); await expect( runClaudeCliAttempt({ sessionKey, sessionEntry, sessionStore, body: "resume after abort", runId: "run-cli-abort", }), ).rejects.toMatchObject({ name: "AbortError" }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().cliSessionId).toBe(cliSessionId); expect(sessionStore[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.claudeCliSessionId).toBeUndefined(); const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< string, SessionEntry >; expect(persisted[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined(); expect(persisted[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined(); expect(persisted[sessionKey]?.claudeCliSessionId).toBeUndefined(); }); it("clears reused Claude CLI session IDs before a fresh retry after timeout failover", async () => { const sessionKey = "agent:main:direct:cli-timeout"; const cliSessionId = "timeout-poisoned-session"; await writeClaudeCliAssistantTranscript(cliSessionId); const sessionEntry = makeClaudeCliSessionEntry("session-cli-timeout", cliSessionId); const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockImplementationOnce(async (args: unknown) => { const retry = requireRecord(args, "run CLI agent argument").onBeforeFreshCliSessionRetry; expect(retry).toBeTypeOf("function"); await ( retry as (params: { provider: string; reason: "timeout"; sessionId: string; }) => Promise )({ provider: "claude-cli", reason: "timeout", sessionId: cliSessionId, }); expect(sessionStore[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.claudeCliSessionId).toBeUndefined(); return makeCliResult("hello after timeout"); }); await runClaudeCliAttempt({ sessionKey, sessionEntry, sessionStore, body: "resume after timeout", runId: "run-cli-timeout", }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().cliSessionId).toBe(cliSessionId); expect(sessionStore[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.claudeCliSessionId).toBeUndefined(); }); it("does not install a stale-session clearing hook for storeless CLI attempts", async () => { const sessionKey = "agent:main:internal-storeless"; const cliSessionId = "storeless-stale-session"; await writeClaudeCliAssistantTranscript(cliSessionId); const sessionEntry = makeClaudeCliSessionEntry("session-storeless", cliSessionId); runCliAgentMock.mockResolvedValueOnce(makeCliResult("storeless ok")); // Storeless attempts cannot persist binding cleanup, so installing the hook // would only give callers a false sense that stale state was repaired. await runAgentAttempt({ providerOverride: "claude-cli", originalProvider: "claude-cli", modelOverride: "opus", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "storeless retry path", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-storeless-cli", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "claude-cli", sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().cliSessionId).toBe(cliSessionId); expect(firstRunCliAgentArg().onBeforeFreshCliSessionRetry).toBeUndefined(); }); it.each(["auth", "billing", "rate_limit"] as const)( "clears reused Claude CLI session IDs after %s failover without retrying", async (reason) => { const sessionKey = `agent:main:direct:cli-${reason}`; const cliSessionId = `${reason}-poisoned-session`; await writeClaudeCliAssistantTranscript(cliSessionId); const sessionEntry = makeClaudeCliSessionEntry(`session-cli-${reason}`, cliSessionId); const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockRejectedValueOnce( new FailoverError(`${reason} failed`, { reason, provider: "claude-cli", model: "opus", }), ); await expect( runClaudeCliAttempt({ sessionKey, sessionEntry, sessionStore, body: `resume after ${reason}`, runId: `run-cli-${reason}`, }), ).rejects.toMatchObject({ name: "FailoverError", reason }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().cliSessionId).toBe(cliSessionId); expect(sessionStore[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.claudeCliSessionId).toBeUndefined(); }, ); it("does not pass --resume when the stored Claude CLI transcript is missing", async () => { const sessionKey = "agent:main:direct:claude-missing-transcript"; const homeDir = path.join(tmpDir, "home"); setTestEnvValue("HOME", homeDir); const sessionEntry: SessionEntry = { sessionId: "openclaw-session-123", updatedAt: Date.now(), cliSessionBindings: { "claude-cli": { sessionId: "phantom-claude-session", authProfileId: "anthropic:claude-cli", }, }, cliSessionIds: { "claude-cli": "phantom-claude-session" }, claudeCliSessionId: "phantom-claude-session", }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("fresh cli response")); await runClaudeCliAttempt({ sessionKey, sessionEntry, sessionStore, body: "remember me", runId: "run-cli-missing-transcript", }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().cliSessionId).toBeUndefined(); expect(firstRunCliAgentArg().cliSessionBinding).toBeUndefined(); expect(sessionStore[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined(); expect(sessionStore[sessionKey]?.claudeCliSessionId).toBeUndefined(); const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< string, SessionEntry >; expect(persisted[sessionKey]?.cliSessionBindings?.["claude-cli"]).toBeUndefined(); expect(persisted[sessionKey]?.cliSessionIds?.["claude-cli"]).toBeUndefined(); expect(persisted[sessionKey]?.claudeCliSessionId).toBeUndefined(); }); it("keeps Claude CLI resume when the stored transcript has assistant content", async () => { const sessionKey = "agent:main:direct:claude-transcript-present"; const cliSessionId = "existing-claude-session"; const homeDir = path.join(tmpDir, "home"); const projectsDir = resolveClaudeCliProjectDirForWorkspace({ workspaceDir: tmpDir, homeDir, }); setTestEnvValue("HOME", homeDir); await fs.mkdir(projectsDir, { recursive: true }); await fs.writeFile( path.join(projectsDir, `${cliSessionId}.jsonl`), `${JSON.stringify({ type: "assistant", message: { role: "assistant", content: [{ type: "text", text: "previous reply" }], }, })}\n`, "utf-8", ); const sessionEntry: SessionEntry = { sessionId: "openclaw-session-456", updatedAt: Date.now(), cliSessionBindings: { "claude-cli": { sessionId: cliSessionId, authProfileId: "anthropic:claude-cli", }, }, cliSessionIds: { "claude-cli": cliSessionId }, claudeCliSessionId: cliSessionId, }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("resumed cli response")); await runClaudeCliAttempt({ sessionKey, sessionEntry, sessionStore, body: "continue", runId: "run-cli-transcript-present", }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().cliSessionId).toBe(cliSessionId); expect(firstRunCliAgentArg().cliSessionBinding).toEqual({ sessionId: cliSessionId, authProfileId: "anthropic:claude-cli", }); expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBe(cliSessionId); expect(sessionStore[sessionKey]?.claudeCliSessionId).toBe(cliSessionId); }); it("checks Claude CLI transcript content under the process cwd", async () => { const sessionKey = "agent:main:direct:claude-transcript-cwd-present"; const cliSessionId = "existing-claude-cwd-session"; const homeDir = path.join(tmpDir, "home"); const cwd = path.join(tmpDir, "task"); const projectsDir = resolveClaudeCliProjectDirForWorkspace({ workspaceDir: cwd, homeDir, }); setTestEnvValue("HOME", homeDir); await fs.mkdir(projectsDir, { recursive: true }); await fs.writeFile( path.join(projectsDir, `${cliSessionId}.jsonl`), `${JSON.stringify({ type: "assistant", message: { role: "assistant", content: [{ type: "text", text: "previous reply" }], }, })}\n`, "utf-8", ); const sessionEntry = makeClaudeCliSessionEntry("openclaw-session-cwd", cliSessionId); const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("resumed cli response")); await runClaudeCliAttempt({ sessionKey, sessionEntry, sessionStore, body: "continue from task cwd", runId: "run-cli-transcript-cwd-present", cwd, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().cliSessionId).toBe(cliSessionId); expect(firstRunCliAgentArg().cwd).toBe(cwd); expect(sessionStore[sessionKey]?.cliSessionIds?.["claude-cli"]).toBe(cliSessionId); }); it("passes session-bound OpenAI Codex auth profile to codex-cli aliases", async () => { const sessionKey = "agent:main:direct:codex-cli-auth-alias"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-codex", updatedAt: Date.now(), authProfileOverride: "openai:work", authProfileOverrideSource: "user", }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("codex cli response")); await runAgentAttempt({ providerOverride: "codex-cli", originalProvider: "codex-cli", modelOverride: "gpt-5.4", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-codex-cli-auth-alias", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().authProfileId).toBe("openai:work"); }); it("skips auto auth-profile resolution for CLI-owned transport", async () => { const sessionKey = "agent:main:direct:codex-cli-owned-transport"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-codex-owned", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; const cfg: OpenClawConfig = { agents: { defaults: { agentRuntime: { id: "codex" }, }, }, }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); await fs.writeFile(path.join(tmpDir, "auth-profiles.json"), "{", "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("codex cli response")); await runAgentAttempt({ providerOverride: "codex-cli", originalProvider: "codex-cli", modelOverride: "gpt-5.4", cfg, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-codex-cli-owned-transport-auth-skip", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai-codex", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().authProfileId).toBeUndefined(); }); it("selects a google-gemini-cli auth profile for canonical Google models routed through Gemini CLI", async () => { const sessionKey = "agent:main:direct:gemini-cli-auth-bridge"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-gemini", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); saveAuthProfileStore( { version: 1, profiles: { "google-gemini-cli:user@example.test": { type: "oauth", provider: "google-gemini-cli", access: "access-token", refresh: "refresh-token", expires: Date.now() + 3_600_000, email: "user@example.test", }, }, }, tmpDir, { filterExternalAuthProfiles: false, syncExternalCli: false }, ); runCliAgentMock.mockResolvedValueOnce(makeCliResult("gemini cli response")); await runAgentAttempt({ providerOverride: "google", originalProvider: "google", modelOverride: "gemini-3.1-pro-preview", cfg: { auth: { order: { "google-gemini-cli": ["google-gemini-cli:user@example.test"], }, }, agents: { defaults: { models: { "google/gemini-3.1-pro-preview": { agentRuntime: { id: "google-gemini-cli" }, }, }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-gemini-cli-auth-bridge", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "google", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().provider).toBe("google-gemini-cli"); expect(firstRunCliAgentArg().authProfileId).toBe("google-gemini-cli:user@example.test"); }); it("forwards pinned canonical Google API-key profiles to Google models routed through Gemini CLI", async () => { const sessionKey = "agent:main:direct:gemini-cli-google-api-key"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-gemini-api-key", updatedAt: Date.now(), authProfileOverride: "google:api-key", authProfileOverrideSource: "user", }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); saveAuthProfileStore( { version: 1, profiles: { "google:api-key": { type: "api_key", provider: "google", key: "gemini-api-key", }, }, }, tmpDir, { filterExternalAuthProfiles: false, syncExternalCli: false }, ); runCliAgentMock.mockResolvedValueOnce(makeCliResult("gemini cli api-key response")); await runAgentAttempt({ providerOverride: "google", originalProvider: "google", modelOverride: "gemini-3.1-pro-preview", cfg: { agents: { defaults: { models: { "google/gemini-3.1-pro-preview": { agentRuntime: { id: "google-gemini-cli" }, }, }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-gemini-cli-google-api-key", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "google", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().provider).toBe("google-gemini-cli"); expect(firstRunCliAgentArg().authProfileId).toBe("google:api-key"); }); it("forwards incompatible pinned profiles to Gemini CLI for fail-closed backend validation", async () => { const sessionKey = "agent:main:direct:gemini-cli-incompatible-auth"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-gemini-incompatible-auth", updatedAt: Date.now(), authProfileOverride: "vercel-ai-gateway:default", authProfileOverrideSource: "user", }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); saveAuthProfileStore( { version: 1, profiles: { "vercel-ai-gateway:default": { type: "api_key", provider: "vercel-ai-gateway", key: "vercel-key", }, }, }, tmpDir, { filterExternalAuthProfiles: false, syncExternalCli: false }, ); runCliAgentMock.mockResolvedValueOnce(makeCliResult("should fail in real backend")); await runAgentAttempt({ providerOverride: "google", originalProvider: "google", modelOverride: "gemini-3.1-pro-preview", cfg: { agents: { defaults: { models: { "google/gemini-3.1-pro-preview": { agentRuntime: { id: "google-gemini-cli" }, }, }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-gemini-cli-incompatible-auth", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "google", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().provider).toBe("google-gemini-cli"); expect(firstRunCliAgentArg().authProfileId).toBe("vercel-ai-gateway:default"); }); it("ignores stale auto-selected profiles when resolving Gemini CLI auth order", async () => { const sessionKey = "agent:main:direct:gemini-cli-stale-auto-auth"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-gemini-stale-auto-auth", updatedAt: Date.now(), authProfileOverride: "openai:work", authProfileOverrideSource: "auto", }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); saveAuthProfileStore( { version: 1, profiles: { "openai:work": { type: "oauth", provider: "openai", access: "openai-access", refresh: "openai-refresh", expires: Date.now() + 60_000, }, "google:api-key": { type: "api_key", provider: "google", key: "gemini-api-key", }, }, }, tmpDir, { filterExternalAuthProfiles: false, syncExternalCli: false }, ); runCliAgentMock.mockResolvedValueOnce(makeCliResult("gemini cli api-key response")); await runAgentAttempt({ providerOverride: "google", originalProvider: "google", modelOverride: "gemini-3.1-pro-preview", cfg: { auth: { order: { google: ["google:api-key"], }, }, agents: { defaults: { models: { "google/gemini-3.1-pro-preview": { agentRuntime: { id: "google-gemini-cli" }, }, }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-gemini-cli-stale-auto-auth", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "google", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().provider).toBe("google-gemini-cli"); expect(firstRunCliAgentArg().authProfileId).toBe("google:api-key"); }); it("selects canonical Google API-key auth order for Google models routed through Gemini CLI", async () => { const sessionKey = "agent:main:direct:gemini-cli-google-api-key-order"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-gemini-api-key-order", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); saveAuthProfileStore( { version: 1, profiles: { "google:api-key": { type: "api_key", provider: "google", key: "gemini-api-key", }, }, }, tmpDir, { filterExternalAuthProfiles: false, syncExternalCli: false }, ); runCliAgentMock.mockResolvedValueOnce(makeCliResult("gemini cli api-key response")); await runAgentAttempt({ providerOverride: "google", originalProvider: "google", modelOverride: "gemini-3.1-pro-preview", cfg: { auth: { order: { google: ["google:api-key"], }, }, agents: { defaults: { models: { "google/gemini-3.1-pro-preview": { agentRuntime: { id: "google-gemini-cli" }, }, }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-gemini-cli-google-api-key-order", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "google", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().provider).toBe("google-gemini-cli"); expect(firstRunCliAgentArg().authProfileId).toBe("google:api-key"); }); it("persists CLI replies into the session transcript", async () => { const sessionKey = "agent:main:subagent:cli-transcript"; const sessionFile = path.join(tmpDir, "session-cli-transcript.jsonl"); const sessionEntry: SessionEntry = { sessionId: "session-cli-transcript", sessionFile, updatedAt: 1, status: "running", startedAt: 2, }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile( storePath, JSON.stringify( { [sessionKey]: { ...sessionEntry, updatedAt: 5, status: "done", endedAt: 4, }, }, null, 2, ), "utf-8", ); clearSessionStoreCacheForTest(); const nowCalls: number[] = []; let nextNow = 10_000; const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => { nextNow += 1_000; nowCalls.push(nextNow); return nextNow; }); let updatedEntry: SessionEntry | undefined; try { updatedEntry = await persistCliTranscriptEntry({ body: "persist this", result: makeCliResult("hello from cli"), sessionId: sessionEntry.sessionId, sessionKey, sessionEntry, sessionStore, storePath, sessionAgentId: "main", sessionCwd: tmpDir, config: {}, }); } finally { nowSpy.mockRestore(); } const updatedSessionFile = updatedEntry?.sessionFile; if (!updatedSessionFile) { throw new Error("expected CLI transcript persistence to create a session file"); } expect(await fs.realpath(updatedSessionFile)).toBe(await fs.realpath(sessionFile)); const entries = await readSessionFileEntries(sessionFile); expectRecordFields(requireRecord(entries[0], "session entry"), { type: "session", id: sessionEntry.sessionId, cwd: tmpDir, }); expectRecordFields(requireRecord(entries[1], "user transcript entry"), { type: "message", parentId: null, }); expectRecordFields(requireRecord(entries[2], "assistant transcript entry"), { type: "message", parentId: entries[1]?.id, }); const messages = await readSessionMessages(sessionFile); expect(messages).toHaveLength(2); expectRecordFields(requireRecord(messages[0], "user message"), { role: "user", content: "persist this", }); expectRecordFields(requireRecord(messages[1], "assistant message"), { role: "assistant", api: "cli", provider: "claude-cli", model: "opus", content: [{ type: "text", text: "hello from cli" }], }); const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< string, SessionEntry >; expect(await fs.realpath(persisted[sessionKey]?.sessionFile ?? "")).toBe( await fs.realpath(sessionFile), ); expect(persisted[sessionKey]?.updatedAt).toBeGreaterThan(sessionEntry.updatedAt); expect(persisted[sessionKey]?.updatedAt).toBeLessThanOrEqual(nowCalls.at(-1) ?? 0); expect(sessionStore[sessionKey]?.updatedAt).toBe(persisted[sessionKey]?.updatedAt); }); it("does not append a CLI transcript after the session is deleted", async () => { const sessionKey = "agent:main:subagent:cli-transcript-deleted"; const staleSessionFile = path.join(tmpDir, "session-cli-stale.jsonl"); const staleEntry: SessionEntry = { sessionId: "session-cli-stale", sessionFile: staleSessionFile, updatedAt: 1, }; const sessionStore: Record = { [sessionKey]: staleEntry }; await fs.writeFile(storePath, JSON.stringify({}, null, 2), "utf-8"); clearSessionStoreCacheForTest(); const result = await persistCliTurnTranscript({ body: "late prompt", result: makeCliResult("late reply"), sessionId: staleEntry.sessionId, sessionKey, sessionEntry: staleEntry, sessionStore, storePath, sessionAgentId: "main", sessionCwd: tmpDir, config: {}, }); expect(result).toEqual({ kind: "session-rebound", sessionEntry: undefined }); await expect(fs.stat(staleSessionFile)).rejects.toMatchObject({ code: "ENOENT" }); const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< string, SessionEntry >; expect(persisted[sessionKey]).toBeUndefined(); }); it("embedded assistant gap-fill skips user mirror and dedupes identical assistant tails", async () => { const sessionKey = "agent:main:subagent:embedded-gap-fill"; const sessionEntry: SessionEntry = { sessionId: "session-embedded-gap-fill", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); const result = makeCliResult("already mirrored"); result.meta.executionTrace = { winnerProvider: "anthropic", winnerModel: "claude-opus-4-6", fallbackUsed: false, runner: "embedded", }; const updatedFirst = await persistCliTranscriptEntry({ body: "ignored for gap fill", transcriptBody: "also ignored", result, sessionId: sessionEntry.sessionId, sessionKey, sessionEntry, sessionStore, storePath, sessionAgentId: "main", sessionCwd: tmpDir, config: {}, embeddedAssistantGapFill: true, }); let messages = await readSessionMessages(updatedFirst?.sessionFile ?? ""); expect(messages).toHaveLength(1); expectRecordFields(requireRecord(messages[0], "assistant message"), { role: "assistant", content: [{ type: "text", text: "already mirrored" }], }); await persistCliTurnTranscript({ body: "still ignored", result, sessionId: sessionEntry.sessionId, sessionKey, sessionEntry: updatedFirst, sessionStore, storePath, sessionAgentId: "main", sessionCwd: tmpDir, config: {}, embeddedAssistantGapFill: true, }); messages = await readSessionMessages(updatedFirst?.sessionFile ?? ""); expect(messages).toHaveLength(1); }); it("embedded assistant gap-fill skips malformed transcript tail rows before deduping", async () => { const sessionKey = "agent:main:subagent:embedded-gap-fill-malformed-tail"; const sessionEntry: SessionEntry = { sessionId: "session-embedded-gap-fill-malformed-tail", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); const result = makeCliResult("already mirrored"); result.meta.executionTrace = { winnerProvider: "anthropic", winnerModel: "claude-opus-4-6", fallbackUsed: false, runner: "embedded", }; const updatedFirst = await persistCliTranscriptEntry({ body: "ignored for gap fill", result, sessionId: sessionEntry.sessionId, sessionKey, sessionEntry, sessionStore, storePath, sessionAgentId: "main", sessionCwd: tmpDir, config: {}, embeddedAssistantGapFill: true, }); const sessionFile = updatedFirst?.sessionFile; if (typeof sessionFile !== "string") { throw new Error("Expected CLI transcript session file."); } await fs.appendFile(sessionFile, "{truncated-json\n", "utf-8"); await persistCliTurnTranscript({ body: "still ignored", result, sessionId: sessionEntry.sessionId, sessionKey, sessionEntry: updatedFirst, sessionStore, storePath, sessionAgentId: "main", sessionCwd: tmpDir, config: {}, embeddedAssistantGapFill: true, }); const validEntries = (await fs.readFile(sessionFile, "utf-8")) .split(/\r?\n/) .flatMap((line) => { if (!line) { return []; } try { return [JSON.parse(line) as { type?: string; message?: { role?: string } }]; } catch { return []; } }); expect(validEntries.filter((entry) => entry.type === "message")).toHaveLength(1); }); it("embedded assistant gap-fill skips trailing openclaw.cache-ttl custom entries (regression for #83427)", async () => { const sessionKey = "agent:main:subagent:embedded-gap-fill-cache-ttl"; const sessionEntry: SessionEntry = { sessionId: "session-embedded-gap-fill-cache-ttl", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); const result = makeCliResult("canonical answer"); result.meta.executionTrace = { winnerProvider: "anthropic", winnerModel: "claude-haiku-4-5-20251001", fallbackUsed: false, runner: "embedded", }; const updatedFirst = await persistCliTranscriptEntry({ body: "ignored for gap fill", result, sessionId: sessionEntry.sessionId, sessionKey, sessionEntry, sessionStore, storePath, sessionAgentId: "main", sessionCwd: tmpDir, config: {}, embeddedAssistantGapFill: true, }); const sessionFile = updatedFirst?.sessionFile; if (typeof sessionFile !== "string") { throw new Error("Expected CLI transcript session file."); } await fs.appendFile( sessionFile, `${JSON.stringify({ type: "custom", customType: "openclaw.cache-ttl", timestamp: new Date().toISOString(), data: { provider: "anthropic", modelId: "claude-haiku-4-5-20251001", }, })}\n`, "utf-8", ); await persistCliTurnTranscript({ body: "still ignored", result, sessionId: sessionEntry.sessionId, sessionKey, sessionEntry: updatedFirst, sessionStore, storePath, sessionAgentId: "main", sessionCwd: tmpDir, config: {}, embeddedAssistantGapFill: true, }); const messages = await readSessionMessages(sessionFile); expect(messages).toHaveLength(1); expectRecordFields(requireRecord(messages[0], "assistant message"), { role: "assistant", content: [{ type: "text", text: "canonical answer" }], }); }); it("embedded assistant gap-fill appends repeated replies after a user tail", async () => { const sessionKey = "agent:main:subagent:embedded-repeated-reply"; const sessionEntry: SessionEntry = { sessionId: "session-embedded-repeated-reply", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); const result = makeCliResult("same answer"); result.meta.executionTrace = { winnerProvider: "anthropic", winnerModel: "claude-opus-4-6", fallbackUsed: false, runner: "embedded", }; const updatedFirst = await persistCliTranscriptEntry({ body: "ignored for gap fill", result, sessionId: sessionEntry.sessionId, sessionKey, sessionEntry, sessionStore, storePath, sessionAgentId: "main", sessionCwd: tmpDir, config: {}, embeddedAssistantGapFill: true, }); const sessionFile = updatedFirst?.sessionFile; if (typeof sessionFile !== "string") { throw new Error("Expected CLI transcript session file."); } expect(path.isAbsolute(sessionFile)).toBe(true); const persistedFirst = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record< string, SessionEntry >; expect(await fs.realpath(persistedFirst[sessionKey]?.sessionFile ?? "")).toBe( await fs.realpath(sessionFile), ); await appendSessionTranscriptMessage({ transcriptPath: sessionFile, sessionId: sessionEntry.sessionId, cwd: tmpDir, config: {}, message: { role: "user", content: "next prompt", timestamp: Date.now(), }, }); await persistCliTurnTranscript({ body: "still ignored", result, sessionId: sessionEntry.sessionId, sessionKey, sessionEntry: updatedFirst, sessionStore, storePath, sessionAgentId: "main", sessionCwd: tmpDir, config: {}, embeddedAssistantGapFill: true, }); const messages = await readSessionMessages(sessionFile); expect(messages).toHaveLength(3); expect(messages.map((message) => message.role)).toEqual(["assistant", "user", "assistant"]); expectRecordFields(requireRecord(messages[2], "deduped assistant message"), { content: [{ type: "text", text: "same answer" }], }); }); it("persists the transcript body instead of runtime-only CLI prompt context", async () => { const sessionKey = "agent:main:subagent:cli-transcript-clean"; const sessionEntry: SessionEntry = { sessionId: "session-cli-transcript-clean", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); const updatedEntry = await persistCliTranscriptEntry({ body: [ "<<>>", "secret runtime context", "<<>>", "", "visible ask", ].join("\n"), transcriptBody: "visible ask", result: makeCliResult("hello from cli"), sessionId: sessionEntry.sessionId, sessionKey, sessionEntry, sessionStore, storePath, sessionAgentId: "main", sessionCwd: tmpDir, config: {}, }); const messages = await readSessionMessages(updatedEntry?.sessionFile ?? ""); expectRecordFields(requireRecord(messages[0], "transcript user message"), { role: "user", content: "visible ask", }); }); it("forwards separate user trigger, channel, and provider context to CLI runs", async () => { const sessionKey = "agent:main:direct:claude-channel-context"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-channel", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("channel aware")); await runAgentAttempt({ providerOverride: "claude-cli", originalProvider: "claude-cli", modelOverride: "opus", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "route this", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-cli-channel-context", opts: { messageProvider: "discord-voice", } as Parameters[0]["opts"], runContext: { currentChannelId: "channel:voice-room", chatId: "voice-room", channelContext: { sender: { id: "sender-voice", unionId: "sender-union" }, chat: { id: "voice-room" }, }, senderId: "sender-voice", } as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: "discord", skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "claude-cli", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expectMockArgFields(runCliAgentMock, { trigger: "user", messageChannel: "discord", messageProvider: "discord-voice", currentChannelId: "channel:voice-room", chatId: "voice-room", channelContext: { sender: { id: "sender-voice", unionId: "sender-union" }, chat: { id: "voice-room" }, }, senderId: "sender-voice", }); }); it("forwards message-tool-only policy and requires explicit subagent targets", async () => { const sessionKey = "agent:main:subagent:claude-message-policy"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-cli-message-policy", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("sent")); await runAgentAttempt({ providerOverride: "claude-cli", originalProvider: "claude-cli", modelOverride: "opus", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "route this", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-cli-message-policy", opts: { sourceReplyDeliveryMode: "message_tool_only", } as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: "discord", skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "claude-cli", sessionStore, storePath, sessionHasHistory: false, }); expectMockArgFields(runCliAgentMock, { sourceReplyDeliveryMode: "message_tool_only", requireExplicitMessageTarget: true, }); }); it("does not pass auth-order profiles to CLI backends that do not stage them", async () => { const sessionKey = "agent:main:direct:claude-auth-order"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-claude-auth-order", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("ambient claude cli")); await runAgentAttempt({ providerOverride: "claude-cli", originalProvider: "claude-cli", modelOverride: "opus", cfg: { auth: { order: { "claude-cli": ["claude-cli:work"], }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "use ambient cli auth", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-claude-auth-order", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "claude-cli", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expect(firstRunCliAgentArg().authProfileId).toBeUndefined(); }); it("does not pass auth-order profiles to configured CLI runtimes that do not stage them", async () => { const sessionKey = "agent:main:direct:anthropic-claude-runtime-auth-order"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-anthropic-claude-runtime-auth-order", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); saveAuthProfileStore( { version: 1, profiles: { "anthropic:work": { type: "api_key", provider: "anthropic", key: "test-key", }, }, }, tmpDir, { filterExternalAuthProfiles: false, syncExternalCli: false }, ); runCliAgentMock.mockResolvedValueOnce(makeCliResult("configured claude cli")); await runAgentAttempt({ providerOverride: "anthropic", originalProvider: "anthropic", modelOverride: "claude-opus-4-7", cfg: { auth: { order: { anthropic: ["anthropic:work"], }, }, agents: { defaults: { models: { "anthropic/claude-opus-4-7": { agentRuntime: { id: "claude-cli" } }, }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "use ambient cli auth", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-configured-claude-auth-order", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "anthropic", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expectMockArgFields(runCliAgentMock, { provider: "claude-cli", model: "claude-opus-4-7", }); expect(firstRunCliAgentArg().authProfileId).toBeUndefined(); }); it("forwards runtime toolsAllow into CLI attempts so the CLI harness can fail closed", async () => { const sessionKey = "agent:main:direct:claude-tools-allow"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-cli-tools-allow", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("restricted cli")); await runAgentAttempt({ providerOverride: "claude-cli", originalProvider: "claude-cli", modelOverride: "opus", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "route this", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-cli-tools-allow", opts: { toolsAllow: ["read", "web_search"], } as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: "discord", skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "claude-cli", sessionStore, storePath, sessionHasHistory: false, }); expectMockArgFields(runCliAgentMock, { provider: "claude-cli", toolsAllow: ["read", "web_search"], }); }); it("stamps CLI prompts with current timestamp context", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2024-06-05T15:30:00Z")); const sessionKey = "agent:main:direct:claude-timestamp"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-cli-timestamp", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("timestamped cli")); await runAgentAttempt({ providerOverride: "claude-cli", originalProvider: "claude-cli", modelOverride: "opus", cfg: { agents: { defaults: { userTimezone: "UTC" } } } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "what time is it?", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-cli-timestamp", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: "discord", skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "claude-cli", sessionStore, storePath, sessionHasHistory: false, }); expectMockArgFields(runCliAgentMock, { prompt: "[Wed 2024-06-05 15:30 UTC] what time is it?", }); }); it("routes canonical Anthropic models through the configured Claude CLI runtime", async () => { const sessionKey = "agent:main:direct:canonical-claude-cli"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-canonical-cli", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("canonical cli")); await runAgentAttempt({ providerOverride: "anthropic", originalProvider: "anthropic", modelOverride: "claude-opus-4-7", cfg: { agents: { defaults: { models: { "anthropic/claude-opus-4-7": { agentRuntime: { id: "claude-cli" } }, }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "route this", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-canonical-claude-cli", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: "telegram", skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "anthropic", sessionStore, storePath, sessionHasHistory: false, }); expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); expectMockArgFields(runCliAgentMock, { provider: "claude-cli", model: "claude-opus-4-7", }); }); it("routes provider-qualified Anthropic shorthand through the configured Claude CLI runtime", async () => { const sessionKey = "agent:main:direct:shorthand-claude-cli"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-shorthand-cli", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("shorthand cli")); await runAgentAttempt({ providerOverride: "anthropic", originalProvider: "anthropic", modelOverride: "opus-4.7", cfg: { agents: { defaults: { models: { "anthropic/opus-4.7": { agentRuntime: { id: "claude-cli" } }, }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "route this", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-shorthand-claude-cli", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: "telegram", skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "anthropic", sessionStore, storePath, sessionHasHistory: false, }); expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); expectMockArgFields(runCliAgentMock, { provider: "claude-cli", model: "opus-4.7", }); }); it("routes canonical OpenAI models through the configured embedded Codex runtime", async () => { const sessionKey = "agent:main:direct:canonical-codex-cli"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-canonical-codex-cli", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "canonical codex embedded" }], meta: { durationMs: 5, finalAssistantVisibleText: "canonical codex embedded", executionTrace: { runner: "openclaw" }, }, }); await runAgentAttempt({ providerOverride: "openai", originalProvider: "openai", modelOverride: "gpt-5.4", cfg: { agents: { defaults: { models: { "openai/gpt-5.4": { agentRuntime: { id: "codex" } }, }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "route this", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-canonical-codex-cli", opts: {} as Parameters[0]["opts"], runContext: { chatId: "chat-embedded", channelContext: { sender: { id: "sender-embedded", unionId: "embedded-union" }, chat: { id: "chat-embedded" }, }, senderId: "sender-embedded", } as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: "telegram", skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionStore, storePath, sessionHasHistory: false, }); expect(runCliAgentMock).not.toHaveBeenCalled(); expectMockArgFields(runEmbeddedAgentMock, { provider: "openai", model: "gpt-5.4", chatId: "chat-embedded", channelContext: { sender: { id: "sender-embedded", unionId: "embedded-union" }, chat: { id: "chat-embedded" }, }, senderId: "sender-embedded", }); }); it("keeps live stream output for visible subagent lane runs", async () => { const embeddedArg = await runOpenClawEmbeddedAttemptForTest({ opts: { lane: "subagent" }, runId: "visible-subagent-stream", }); expect(embeddedArg.suppressLiveStreamOutput).toBe(false); }); it("suppresses live stream output for hidden internal runs", async () => { const embeddedArg = await runOpenClawEmbeddedAttemptForTest({ opts: { lane: "subagent", sessionEffects: "internal" }, runId: "internal-subagent-stream", }); expect(embeddedArg.suppressLiveStreamOutput).toBe(true); }); it("forwards selected auth profiles through metadata-scoped provider aliases", async () => { const sessionKey = "agent:main:direct:metadata-auth-alias"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-metadata-auth-alias", updatedAt: Date.now(), authProfileOverride: "openai:work", authProfileOverrideSource: "user", }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); saveAuthProfileStore( { version: 1, profiles: { "openai:work": { type: "oauth", provider: "openai", access: "access-token", refresh: "refresh-token", expires: Date.now() + 60_000, }, }, }, tmpDir, { filterExternalAuthProfiles: false, syncExternalCli: false }, ); runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "fixture", originalProvider: "fixture", modelOverride: "fixture-model", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "use selected auth", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-metadata-auth-alias", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "fixture", sessionStore, storePath, pluginsEnabled: true, metadataSnapshot: { plugins: [ { id: "alias-owner", origin: "global", providerAuthAliases: { fixture: "openai" }, }, ], } as never, sessionHasHistory: false, }); expectMockArgFields(runEmbeddedAgentMock, { provider: "fixture", model: "fixture-model", authProfileId: "openai:work", authProfileIdSource: "user", }); }); it("forwards user-pinned OpenAI API-key backup profiles to Codex harness runs", async () => { const { clearAgentHarnesses, registerAgentHarness } = await import("../harness/registry.js"); const sessionKey = "agent:main:direct:openai-chatgpt-api-key"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-openai-chatgpt-api-key", updatedAt: Date.now(), authProfileOverride: "openai:backup", authProfileOverrideSource: "user", }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); saveAuthProfileStore( { version: 1, profiles: { "openai:backup": { type: "api_key", provider: "openai", key: "sk-test", }, }, }, tmpDir, { filterExternalAuthProfiles: false, syncExternalCli: false }, ); clearAgentHarnesses(); registerAgentHarness({ id: "codex", label: "Codex", supports: () => ({ supported: true, priority: 100 }), runAttempt: vi.fn(), }); runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); try { await runAgentAttempt({ providerOverride: "openai", originalProvider: "openai", modelOverride: "gpt-5.4", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "use backup auth", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-openai-chatgpt-api-key-backup", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionStore, storePath, sessionHasHistory: false, }); } finally { clearAgentHarnesses(); } expectMockArgFields(runEmbeddedAgentMock, { provider: "openai", model: "gpt-5.4", authProfileId: "openai:backup", authProfileIdSource: "user", }); }); it("keeps one-shot model runs on the raw embedded provider path", async () => { const sessionKey = "agent:main:direct:model-run-raw"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-model-run-raw", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "anthropic", modelOverride: "claude-opus-4-7", originalProvider: "anthropic", cfg: { agents: { defaults: { agentRuntime: { id: "claude-cli" }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "raw prompt", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-model-run-raw", opts: { modelRun: true, promptMode: "none", messageProvider: "discord-voice", inputProvenance: { kind: "inter_session", sourceSessionKey: "agent:main:discord:source", sourceTool: "sessions_send", }, } as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: "discord", skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "anthropic", sessionStore, storePath, sessionHasHistory: true, }); expect(runCliAgentMock).not.toHaveBeenCalled(); expectMockArgFields(runEmbeddedAgentMock, { provider: "anthropic", model: "claude-opus-4-7", agentHarnessId: "openclaw", prompt: "raw prompt", messageChannel: "discord", messageProvider: "discord-voice", modelRun: true, promptMode: "none", disableTools: true, }); expect(firstEmbeddedAgentArg().prompt).not.toContain("[Inter-session message]"); }); it("forwards trusted elevated defaults to embedded agent runs", async () => { const sessionKey = "agent:main:telegram:direct:123"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-elevated-followup", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; const bashElevated = { enabled: true, allowed: true, defaultLevel: "on" as const, }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", originalProvider: "openai", modelOverride: "gpt-5.4", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "follow up after approved exec", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-elevated-followup", opts: { bashElevated, } as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: "telegram", skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionStore, storePath, sessionHasHistory: false, }); expectMockArgFields(runEmbeddedAgentMock, { provider: "openai", model: "gpt-5.4", bashElevated, }); }); it("forwards one-shot CLI cleanup to CLI providers", async () => { const sessionKey = "agent:main:direct:cleanup-claude-cli"; const sessionEntry: SessionEntry = { sessionId: "openclaw-session-cleanup-cli", updatedAt: Date.now(), }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); runCliAgentMock.mockResolvedValueOnce(makeCliResult("cleanup cli")); await runAgentAttempt({ providerOverride: "claude-cli", originalProvider: "claude-cli", modelOverride: "claude-opus-4-7", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey, sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "cleanup", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-cleanup-claude-cli", opts: { cleanupBundleMcpOnRunEnd: true, cleanupCliLiveSessionOnRunEnd: true, } as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "claude-cli", sessionStore, storePath, sessionHasHistory: false, }); expectMockArgFields(runCliAgentMock, { cleanupBundleMcpOnRunEnd: true, cleanupCliLiveSessionOnRunEnd: true, }); expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); }); describe("embedded attempt harness pinning", () => { let tmpDir: string; beforeEach(async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-embedded-attempt-")); runCliAgentMock.mockReset(); runEmbeddedAgentMock.mockReset(); }); afterEach(async () => { await fs.rm(tmpDir, { recursive: true, force: true }); }); it("does not store a session harness pin for default OpenAI Codex routing", async () => { const sessionEntry: SessionEntry = { sessionId: "legacy-session", updatedAt: Date.now(), }; runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", originalProvider: "openai", modelOverride: "gpt-5.4", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey: "agent:main:main", sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-legacy-runtime-pin", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionHasHistory: true, }); expectMockArgFields(runEmbeddedAgentMock, { agentHarnessId: undefined }); }); it("ignores stale session Codex harness pins on non-OpenAI model switches", async () => { const sessionEntry: SessionEntry = { sessionId: "mixed-provider-session", updatedAt: Date.now(), agentHarnessId: "codex", }; runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "minimax", originalProvider: "minimax", modelOverride: "minimax-m2.7", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey: "agent:main:main", sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "switch to minimax", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-mixed-provider-auto-runtime", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "minimax", sessionHasHistory: true, }); expectMockArgFields(runEmbeddedAgentMock, { agentHarnessId: undefined }); }); it("forwards runtime toolsAllow into embedded attempts", async () => { const sessionEntry: SessionEntry = { sessionId: "tools-allow-session", updatedAt: Date.now(), }; runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", originalProvider: "openai", modelOverride: "gpt-5.4", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey: "agent:main:main", sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "read only", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-tools-allow", opts: { toolsAllow: ["read", "web_search"], } as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionHasHistory: false, }); expectMockArgFields(runEmbeddedAgentMock, { toolsAllow: ["read", "web_search"] }); }); it("lets provider/model runtime policy choose Codex without storing a session harness pin", async () => { const sessionEntry: SessionEntry = { sessionId: "codex-history-session", updatedAt: Date.now(), }; runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "codex", originalProvider: "codex", modelOverride: "gpt-5.4", cfg: { models: { providers: { codex: { baseUrl: "https://api.openai.com/v1", agentRuntime: { id: "codex" }, models: [], }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey: "agent:main:main", sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-codex-no-runtime-pin", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "codex", sessionHasHistory: true, }); expectMockArgFields(runEmbeddedAgentMock, { agentHarnessId: undefined }); }); it("auto-forwards OpenAI Codex auth profiles to default Codex harness runs", async () => { const { clearAgentHarnesses, registerAgentHarness } = await import("../harness/registry.js"); const sessionEntry: SessionEntry = { sessionId: "codex-auth-session", updatedAt: Date.now(), }; saveAuthProfileStore( { version: 1, profiles: { "openai:work": { type: "oauth", provider: "openai", access: "access-token", refresh: "refresh-token", expires: Date.now() + 60_000, }, }, }, tmpDir, { filterExternalAuthProfiles: false, syncExternalCli: false }, ); runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); clearAgentHarnesses(); registerAgentHarness({ id: "codex", label: "Codex", supports: () => ({ supported: true, priority: 100 }), runAttempt: vi.fn(), }); try { await runAgentAttempt({ providerOverride: "openai", originalProvider: "openai", modelOverride: "gpt-5.4", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey: "agent:main:main", sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-codex-auto-auth-profile", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionHasHistory: true, }); } finally { clearAgentHarnesses(); } expectMockArgFields(runEmbeddedAgentMock, { agentHarnessId: undefined, authProfileId: "openai:work", authProfileIdSource: "auto", }); }); it("pins a fresh OpenAI session to the Codex harness by default", async () => { const sessionEntry: SessionEntry = { sessionId: "fresh-session", updatedAt: Date.now(), }; runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", originalProvider: "openai", modelOverride: "gpt-5.4", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey: "agent:main:main", sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "start", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-fresh-no-pin", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionHasHistory: false, }); expectMockArgFields(runEmbeddedAgentMock, { agentHarnessId: undefined }); }); it("ignores stale OpenAI sessions pinned to OpenClaw and relies on default Codex routing", async () => { const sessionEntry: SessionEntry = { sessionId: "stale-agent-session", updatedAt: Date.now(), agentHarnessId: "openclaw", }; runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", originalProvider: "openai", modelOverride: "gpt-5.4", cfg: {} as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey: "agent:main:main", sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-stale-openai-runtime-pin", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionHasHistory: true, }); expectMockArgFields(runEmbeddedAgentMock, { provider: "openai", agentHarnessId: undefined, }); }); it("routes explicit OpenAI native runs with legacy Codex OAuth through OpenClaw", async () => { const sessionEntry: SessionEntry = { sessionId: "explicit-agent-codex-oauth-session", updatedAt: Date.now(), authProfileOverride: "openai:work", authProfileOverrideSource: "user", }; runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", originalProvider: "openai", modelOverride: "gpt-5.4", cfg: { models: { providers: { openai: { baseUrl: "https://api.openai.com/v1", agentRuntime: { id: "openclaw" }, models: [], }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey: "agent:main:main", sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "continue", isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-openai-agent-codex-oauth", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionHasHistory: false, }); expectMockArgFields(runEmbeddedAgentMock, { provider: "openai", model: "gpt-5.4", agentHarnessId: "openclaw", agentHarnessRuntimeOverride: "openclaw", authProfileId: "openai:work", authProfileIdSource: "user", }); }); it("does not pass CLI runtime aliases as embedded harness ids for fallback providers", async () => { const sessionEntry: SessionEntry = { sessionId: "fallback-session", updatedAt: Date.now(), }; runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", originalProvider: "claude-cli", modelOverride: "gpt-5.4", cfg: { agents: { defaults: { agentRuntime: { id: "claude-cli" }, }, }, } as OpenClawConfig, sessionEntry, sessionId: sessionEntry.sessionId, sessionKey: "agent:main:main", sessionAgentId: "main", sessionFile: path.join(tmpDir, "session.jsonl"), workspaceDir: tmpDir, body: "fallback", isFallbackRetry: true, resolvedThinkLevel: "medium", timeoutMs: 1_000, runId: "run-openai-fallback-with-cli-runtime", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, messageChannel: undefined, skillsSnapshot: undefined, resolvedVerboseLevel: undefined, agentDir: tmpDir, onAgentEvent: vi.fn(), authProfileProvider: "openai", sessionHasHistory: false, }); expect(runCliAgentMock).not.toHaveBeenCalled(); expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); expect(firstEmbeddedAgentArg()).not.toHaveProperty("agentHarnessId", "claude-cli"); }); });