diff --git a/extensions/codex/media-understanding-provider.test.ts b/extensions/codex/media-understanding-provider.test.ts index cee6663dd284..864c73df93b9 100644 --- a/extensions/codex/media-understanding-provider.test.ts +++ b/extensions/codex/media-understanding-provider.test.ts @@ -12,6 +12,7 @@ const sharedClientMocks = vi.hoisted(() => ({ vi.mock("./src/app-server/shared-client.js", () => ({ createIsolatedCodexAppServerClient: sharedClientMocks.createIsolatedCodexAppServerClient, + retireSharedCodexAppServerClientIfCurrent: () => undefined, })); function codexModel(inputModalities: string[] = ["text", "image"]) { diff --git a/extensions/codex/media-understanding-provider.ts b/extensions/codex/media-understanding-provider.ts index c1ce4b2d930c..2d53b9df52dd 100644 --- a/extensions/codex/media-understanding-provider.ts +++ b/extensions/codex/media-understanding-provider.ts @@ -76,6 +76,7 @@ async function describeCodexImages( const { text } = await runBoundedCodexAppServerTurn({ config: req.cfg, model: { mode: "required", id: model }, + modelProvider: "openai", profile: req.profile, timeoutMs: req.timeoutMs, signal: req.signal, @@ -121,6 +122,7 @@ async function extractCodexStructured( const { text } = await runBoundedCodexAppServerTurn({ config: req.cfg, model: { mode: "required", id: model }, + modelProvider: "openai", profile: req.profile, timeoutMs: req.timeoutMs, signal: req.signal, diff --git a/extensions/codex/src/app-server/thread-lifecycle.test.ts b/extensions/codex/src/app-server/thread-lifecycle.test.ts index 635e247c87c7..1f336fa80bfa 100644 --- a/extensions/codex/src/app-server/thread-lifecycle.test.ts +++ b/extensions/codex/src/app-server/thread-lifecycle.test.ts @@ -1048,7 +1048,7 @@ describe("Codex app-server native code mode config", () => { expect(instructions).toContain("## Skill Workshop"); expect(instructions).toContain("Durable reusable skill/playbook/workflow work"); expect(instructions).toContain("`skill_workshop`"); - expect(instructions).toContain("Generated = pending proposal"); + expect(instructions).toContain("Other generated work = pending proposal"); expect(instructions).toContain("only explicit user ask"); }); @@ -1083,7 +1083,7 @@ describe("Codex app-server native code mode config", () => { }); expect(instructions).toContain("For progress, set `final=false`."); - expect(instructions).toContain("set `final=true`"); + expect(instructions).toContain("Set `final=true`, or omit it,"); }); it("keeps durable dynamic tool fingerprints scoped to loading mode", () => { diff --git a/extensions/codex/src/app-server/thread-lifecycle.user-mcp-servers.test.ts b/extensions/codex/src/app-server/thread-lifecycle.user-mcp-servers.test.ts index 079e4492a053..4f3ce67ae6cc 100644 --- a/extensions/codex/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +++ b/extensions/codex/src/app-server/thread-lifecycle.user-mcp-servers.test.ts @@ -197,6 +197,7 @@ describe("startOrResumeThread — user mcp.servers projection (regression: #8081 it("projects only Codex user MCP servers scoped to the current agent", async () => { const sessionFile = path.join(tempDir, "session.jsonl"); + registerCodexTestSessionIdentity(sessionFile, "session-1", "agent:atlas:session-1"); const workspaceDir = path.join(tempDir, "workspace"); const request = vi.fn(async (method: string, _params: unknown) => { if (method === "thread/start") { @@ -207,28 +208,33 @@ describe("startOrResumeThread — user mcp.servers projection (regression: #8081 await startOrResumeThread({ client: { request } as never, - params: createParams(sessionFile, workspaceDir, { - mcp: { - servers: { - atlas: { - transport: "streamable-http", - url: "https://atlas.example.com/mcp", - codex: { - agents: ["atlas"], - defaultToolsApprovalMode: "approve", + params: { + ...createParams(sessionFile, workspaceDir, { + mcp: { + servers: { + atlas: { + transport: "streamable-http", + url: "https://atlas.example.com/mcp", + codex: { + agents: ["atlas"], + defaultToolsApprovalMode: "approve", + }, }, - }, - apolo: { - transport: "streamable-http", - url: "https://apolo.example.com/mcp", - codex: { - agents: ["apolo"], - defaultToolsApprovalMode: "approve", + apolo: { + transport: "streamable-http", + url: "https://apolo.example.com/mcp", + codex: { + agents: ["apolo"], + defaultToolsApprovalMode: "approve", + }, }, }, }, - }, - } as unknown as EmbeddedRunAttemptParams["config"]), + } as unknown as EmbeddedRunAttemptParams["config"]), + // Explicit multi-agent ownership (#114388): the session key owner must + // match the explicit agentId below. + sessionKey: "agent:atlas:session-1", + }, agentId: "atlas", cwd: workspaceDir, dynamicTools: [], diff --git a/extensions/codex/src/app-server/transcript-mirror.test.ts b/extensions/codex/src/app-server/transcript-mirror.test.ts index 9155b92a580a..205f37b2ff2a 100644 --- a/extensions/codex/src/app-server/transcript-mirror.test.ts +++ b/extensions/codex/src/app-server/transcript-mirror.test.ts @@ -4,7 +4,7 @@ import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime"; +import { embeddedAgentLog, type AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime"; import { initializeGlobalHookRunner, resetGlobalHookRunner, @@ -24,6 +24,7 @@ import { buildCodexUserPromptMessage, codexTranscriptMirrorRuntime, importCodexThreadHistoryToTranscript, + mirrorPromptAtTurnStartBestEffort, projectBoundedCodexThreadHistory, } from "./transcript-mirror.js"; import { attachCodexMirrorIdentity } from "./upstream-prompt-provenance.js"; @@ -1275,8 +1276,10 @@ describe("mirrorCodexAppServerTranscript", () => { expect(await readMirrorMessages(target)).toEqual([]); }); - it("leaves the assistant unowned when transcript persistence fails", async () => { + it("skips transcript mirrors for sessionless embedded runs", async () => { const root = await makeRoot("openclaw-codex-transcript-failure-"); + const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined); + const markRuntimePersistencePending = vi.fn(); const assistantMessage = attachCodexMirrorIdentity( makeAgentAssistantMessage({ content: [{ type: "text", text: "needs fallback persistence" }], @@ -1285,14 +1288,31 @@ describe("mirrorCodexAppServerTranscript", () => { "turn-1:assistant", ); + const params = { + prompt: "sessionless prompt", + runId: "probe-setup-inference-sessionless", + sessionId: "session-1", + userTurnTranscriptRecorder: { + markRuntimePersistencePending, + resolveMessage: async () => undefined, + }, + } as unknown as Parameters[0]["params"]; + + await mirrorPromptAtTurnStartBestEffort({ + params, + sessionKey: "agent:main:setup-inference:incognito-session-1", + notifyUserMessagePersisted: () => undefined, + cwd: root, + threadId: "thread-1", + turnId: "turn-1", + upstreamUserText: "sessionless prompt", + }); const mirrorOutcome = await mirrorTranscriptBestEffort({ - params: { - sessionId: "session-1", - suppressNextUserMessagePersistence: true, - } as unknown as Parameters[0]["params"], + params, result: { messagesSnapshot: [assistantMessage], } as Parameters[0]["result"], + sessionKey: "agent:main:setup-inference:incognito-session-1", notifyUserMessagePersisted: () => undefined, cwd: root, threadId: "thread-1", @@ -1300,6 +1320,66 @@ describe("mirrorCodexAppServerTranscript", () => { }); expect(mirrorOutcome).toEqual({ assistantTranscriptOwned: false, mirroredMessages: [] }); + expect(markRuntimePersistencePending).not.toHaveBeenCalled(); + expect(warn).not.toHaveBeenCalled(); + }); + + it("renders normal-session mirror failures in structured warnings", async () => { + const root = await makeRoot("openclaw-codex-transcript-failure-"); + const blockedParent = path.join(root, "not-a-directory"); + await fs.writeFile(blockedParent, "blocked"); + const storePath = path.join(blockedParent, "openclaw-agent.sqlite"); + const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined); + warn.mockClear(); + const runId = "run-1"; + const sessionId = "session-1"; + const params = { + prompt: "persist me", + runId, + sessionId, + sessionTarget: { storePath }, + } as unknown as Parameters[0]["params"]; + + await mirrorPromptAtTurnStartBestEffort({ + params, + sessionKey: "agent:main:session-1", + notifyUserMessagePersisted: () => undefined, + cwd: storePath, + threadId: "thread-1", + turnId: "turn-1", + upstreamUserText: "persist me", + }); + + expect(warn).toHaveBeenCalledWith("failed to mirror codex app-server prompt at turn start", { + error: expect.any(String), + runId, + sessionId, + }); + const warning = warn.mock.calls.at(-1)?.[1] as { error?: string } | undefined; + expect(warning?.error).not.toBe(""); + + warn.mockClear(); + await mirrorTranscriptBestEffort({ + params, + result: { + messagesSnapshot: [ + makeAgentAssistantMessage({ + content: [{ type: "text", text: "persist me too" }], + timestamp: Date.now(), + }), + ], + } as Parameters[0]["result"], + sessionKey: "agent:main:session-1", + notifyUserMessagePersisted: () => undefined, + cwd: root, + threadId: "thread-1", + turnId: "turn-1", + }); + expect(warn).toHaveBeenCalledWith("failed to mirror codex app-server transcript", { + error: expect.any(String), + runId, + sessionId, + }); }); it("does not attest a stale idempotency hit with the same mirror identity", async () => { diff --git a/extensions/codex/src/app-server/transcript-mirror.ts b/extensions/codex/src/app-server/transcript-mirror.ts index 10f97ccf072a..61222deacad5 100644 --- a/extensions/codex/src/app-server/transcript-mirror.ts +++ b/extensions/codex/src/app-server/transcript-mirror.ts @@ -112,6 +112,9 @@ async function mirrorBestEffort(params: { terminalAnchor?: TranscriptEntryAnchor; mirroredMessages: MirroredAgentMessage[]; }> { + if (!params.params.sessionTarget) { + return { assistantTranscriptOwned: false, mirroredMessages: [] }; + } try { const messages = await resolveFinalCodexMirrorMessages({ params: params.params, @@ -182,7 +185,11 @@ async function mirrorBestEffort(params: { mirroredMessages, }; } catch (error) { - embeddedAgentLog.warn("failed to mirror codex app-server transcript", { error }); + embeddedAgentLog.warn("failed to mirror codex app-server transcript", { + error: formatErrorMessage(error), + runId: params.params.runId, + sessionId: params.params.sessionId, + }); return { assistantTranscriptOwned: false, mirroredMessages: [] }; } } @@ -249,7 +256,7 @@ export async function mirrorPromptAtTurnStartBestEffort(params: { turnId: string; upstreamUserText: string; }): Promise { - if (params.params.suppressNextUserMessagePersistence) { + if (params.params.suppressNextUserMessagePersistence || !params.params.sessionTarget) { return; } try { @@ -281,7 +288,11 @@ export async function mirrorPromptAtTurnStartBestEffort(params: { params.params.userTurnTranscriptRecorder?.markRuntimePersistencePending(mirrorPromise); await mirrorPromise; } catch (error) { - embeddedAgentLog.warn("failed to mirror codex app-server prompt at turn start", { error }); + embeddedAgentLog.warn("failed to mirror codex app-server prompt at turn start", { + error: formatErrorMessage(error), + runId: params.params.runId, + sessionId: params.params.sessionId, + }); } }