From da67802bafa564fe85796c496607d06b38ec3be6 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 17 Jun 2026 06:50:35 +0200 Subject: [PATCH] fix(codex): respect lifecycle mismatch rotations --- .../auth-profile-runtime-contract.test.ts | 24 +++++++- ...enclaw-owned-tool-runtime-contract.test.ts | 2 +- .../run-attempt.context-engine.test.ts | 19 +++++++ .../run-attempt.native-hook-relay.test.ts | 25 ++++++++- .../run-attempt.turn-watches.test.ts | 21 ++++++- .../codex/src/app-server/thread-lifecycle.ts | 56 +++++++++---------- .../openclaw-owned-tool-runtime-contract.ts | 19 +++---- 7 files changed, 122 insertions(+), 44 deletions(-) diff --git a/extensions/codex/src/app-server/auth-profile-runtime-contract.test.ts b/extensions/codex/src/app-server/auth-profile-runtime-contract.test.ts index 46eee5112923..f74380972406 100644 --- a/extensions/codex/src/app-server/auth-profile-runtime-contract.test.ts +++ b/extensions/codex/src/app-server/auth-profile-runtime-contract.test.ts @@ -10,7 +10,10 @@ import { AUTH_PROFILE_RUNTIME_CONTRACT } from "openclaw/plugin-sdk/agent-runtime import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { CodexAppServerClientFactory } from "./client-factory.js"; import { runCodexAppServerAttempt as runCodexAppServerAttemptImpl } from "./run-attempt.js"; -import { readCodexAppServerBinding, writeCodexAppServerBinding } from "./session-binding.js"; +import { + readCodexAppServerBinding, + writeCodexAppServerBinding as writeRawCodexAppServerBinding, +} from "./session-binding.js"; import { createCodexTestModel } from "./test-support.js"; let codexAppServerClientFactoryForTest: CodexAppServerClientFactory | undefined; @@ -58,6 +61,25 @@ function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAtt } as EmbeddedRunAttemptParams; } +const DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT = JSON.stringify({ + "features.standalone_web_search": false, + web_search: "disabled", +}); + +function writeCodexAppServerBinding( + ...args: Parameters +) { + const [sessionFile, binding, lookup] = args; + return writeRawCodexAppServerBinding( + sessionFile, + { + webSearchThreadConfigFingerprint: DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT, + ...binding, + }, + lookup, + ); +} + function threadStartResult(threadId = "thread-auth-contract") { return { thread: { diff --git a/extensions/codex/src/app-server/openclaw-owned-tool-runtime-contract.test.ts b/extensions/codex/src/app-server/openclaw-owned-tool-runtime-contract.test.ts index 0c0ba8cc62b6..9df679278df8 100644 --- a/extensions/codex/src/app-server/openclaw-owned-tool-runtime-contract.test.ts +++ b/extensions/codex/src/app-server/openclaw-owned-tool-runtime-contract.test.ts @@ -161,7 +161,7 @@ describe("OpenClaw-owned tool runtime contract — Codex app-server adapter", () expectRecordFields(eventRecord, { toolName: "exec", toolCallId: "call-middleware", - args: { command: "status" }, + args: mergedParams, }); expectRecordFields(requireRecord(eventRecord.result, "tool_result middleware result"), { content: [{ type: "text", text: "raw output" }], diff --git a/extensions/codex/src/app-server/run-attempt.context-engine.test.ts b/extensions/codex/src/app-server/run-attempt.context-engine.test.ts index 6a09d9e80b99..97b838740bc1 100644 --- a/extensions/codex/src/app-server/run-attempt.context-engine.test.ts +++ b/extensions/codex/src/app-server/run-attempt.context-engine.test.ts @@ -66,6 +66,25 @@ function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAtt } as EmbeddedRunAttemptParams; } +const DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT = JSON.stringify({ + "features.standalone_web_search": false, + web_search: "disabled", +}); + +function writeCodexAppServerBinding( + ...args: Parameters +) { + const [sessionFile, binding, lookup] = args; + return writeRawCodexAppServerBinding( + sessionFile, + { + webSearchThreadConfigFingerprint: DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT, + ...binding, + }, + lookup, + ); +} + function assistantMessage(text: string, timestamp: number): AgentMessage { return { role: "assistant", diff --git a/extensions/codex/src/app-server/run-attempt.native-hook-relay.test.ts b/extensions/codex/src/app-server/run-attempt.native-hook-relay.test.ts index 2397d6b53689..8e1e1739fd0f 100644 --- a/extensions/codex/src/app-server/run-attempt.native-hook-relay.test.ts +++ b/extensions/codex/src/app-server/run-attempt.native-hook-relay.test.ts @@ -18,10 +18,32 @@ import { tempDir, } from "./run-attempt-test-harness.js"; import { testing } from "./run-attempt.js"; -import { readCodexAppServerBinding, writeCodexAppServerBinding } from "./session-binding.js"; +import { + readCodexAppServerBinding, + writeCodexAppServerBinding as writeRawCodexAppServerBinding, +} from "./session-binding.js"; setupRunAttemptTestHooks(); +const DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT = JSON.stringify({ + "features.standalone_web_search": false, + web_search: "disabled", +}); + +function writeCodexAppServerBinding( + ...args: Parameters +) { + const [sessionFile, binding, lookup] = args; + return writeRawCodexAppServerBinding( + sessionFile, + { + webSearchThreadConfigFingerprint: DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT, + ...binding, + }, + lookup, + ); +} + describe("runCodexAppServerAttempt native hook relay", () => { it("registers native hook relay config for an enabled Codex turn and cleans it up", async () => { const sessionFile = path.join(tempDir, "session.jsonl"); @@ -609,6 +631,7 @@ describe("runCodexAppServerAttempt native hook relay", () => { cwd: workspaceDir, model: "gpt-5.4-codex", modelProvider: "openai", + dynamicToolsFingerprint: "[]", nativeHookRelayGeneration: "generation-from-failed-resume", }); const harness = createStartedThreadHarness(async (method) => { diff --git a/extensions/codex/src/app-server/run-attempt.turn-watches.test.ts b/extensions/codex/src/app-server/run-attempt.turn-watches.test.ts index 3560064ec74c..f331057d4762 100644 --- a/extensions/codex/src/app-server/run-attempt.turn-watches.test.ts +++ b/extensions/codex/src/app-server/run-attempt.turn-watches.test.ts @@ -38,11 +38,30 @@ import { testing } from "./run-attempt.js"; import { readCodexAppServerBinding, resolveCodexAppServerBindingPath, - writeCodexAppServerBinding, + writeCodexAppServerBinding as writeRawCodexAppServerBinding, } from "./session-binding.js"; setupRunAttemptTestHooks(); +const DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT = JSON.stringify({ + "features.standalone_web_search": false, + web_search: "disabled", +}); + +function writeCodexAppServerBinding( + ...args: Parameters +) { + const [sessionFile, binding, lookup] = args; + return writeRawCodexAppServerBinding( + sessionFile, + { + webSearchThreadConfigFingerprint: DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT, + ...binding, + }, + lookup, + ); +} + const tinyPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="; diff --git a/extensions/codex/src/app-server/thread-lifecycle.ts b/extensions/codex/src/app-server/thread-lifecycle.ts index ac333c989a97..3ad3217c66fa 100644 --- a/extensions/codex/src/app-server/thread-lifecycle.ts +++ b/extensions/codex/src/app-server/thread-lifecycle.ts @@ -426,20 +426,6 @@ export async function startOrResumeThread(params: { } binding = undefined; } - if ( - binding?.threadId && - params.nativeCodeModeEnabled === false && - !persistentWebSearchRestriction - ) { - embeddedAgentLog.debug( - "codex app-server native tool surface disabled for turn; starting transient thread", - { - threadId: binding.threadId, - }, - ); - preserveExistingBinding = true; - binding = undefined; - } if (binding?.threadId && (binding.contextEngine || contextEngineBinding)) { if ( !contextEngineBinding || @@ -589,12 +575,25 @@ export async function startOrResumeThread(params: { ); await clearCodexAppServerBinding(params.params.sessionFile); } + } else if ( + params.nativeCodeModeEnabled === false && + !persistentWebSearchRestriction && + binding.dynamicToolsFingerprint !== EMPTY_DYNAMIC_TOOLS_FINGERPRINT + ) { + embeddedAgentLog.debug( + "codex app-server native tool surface disabled for turn; starting transient thread", + { + threadId: binding.threadId, + }, + ); + preserveExistingBinding = true; } else { + const resumeBinding = binding; try { - const authProfileId = params.params.authProfileId ?? binding.authProfileId; + const authProfileId = params.params.authProfileId ?? resumeBinding.authProfileId; const finalConfigPatch = params.buildFinalConfigPatch?.({ action: "resume", - binding, + binding: resumeBinding, }) ?? { configPatch: params.finalConfigPatch, nativeHookRelayGeneration: params.nativeHookRelayGeneration, @@ -606,7 +605,7 @@ export async function startOrResumeThread(params: { ); const resumeParams = lifecycleTiming.measureSync("thread-resume-params", () => buildThreadResumeParams(params.params, { - threadId: binding.threadId, + threadId: resumeBinding.threadId, authProfileId, model: startModelSelection.model, modelProvider: startModelProvider, @@ -634,7 +633,7 @@ export async function startOrResumeThread(params: { const nextMcpServersFingerprint = params.mcpServersFingerprintEvaluated === true ? params.mcpServersFingerprint - : binding.mcpServersFingerprint; + : resumeBinding.mcpServersFingerprint; await lifecycleTiming.measure("thread-resume-write-binding", () => writeCodexAppServerBinding( params.params.sessionFile, @@ -650,13 +649,14 @@ export async function startOrResumeThread(params: { userMcpServersFingerprint, mcpServersFingerprint: nextMcpServersFingerprint, nativeHookRelayGeneration: - finalConfigPatch.nativeHookRelayGeneration ?? binding.nativeHookRelayGeneration, - pluginAppsFingerprint: binding.pluginAppsFingerprint, - pluginAppsInputFingerprint: binding.pluginAppsInputFingerprint, - pluginAppPolicyContext: binding.pluginAppPolicyContext, + finalConfigPatch.nativeHookRelayGeneration ?? + resumeBinding.nativeHookRelayGeneration, + pluginAppsFingerprint: resumeBinding.pluginAppsFingerprint, + pluginAppsInputFingerprint: resumeBinding.pluginAppsInputFingerprint, + pluginAppPolicyContext: resumeBinding.pluginAppPolicyContext, contextEngine: contextEngineBinding, environmentSelectionFingerprint, - createdAt: binding.createdAt, + createdAt: resumeBinding.createdAt, }, { authProfileStore: params.params.authProfileStore, @@ -686,7 +686,7 @@ export async function startOrResumeThread(params: { }); const activeTurnIds = readActiveCodexTurnIds(response.thread); return { - ...binding, + ...resumeBinding, threadId: response.thread.id, cwd: params.cwd, authProfileId: boundAuthProfileId, @@ -698,10 +698,10 @@ export async function startOrResumeThread(params: { userMcpServersFingerprint, mcpServersFingerprint: nextMcpServersFingerprint, nativeHookRelayGeneration: - finalConfigPatch.nativeHookRelayGeneration ?? binding.nativeHookRelayGeneration, - pluginAppsFingerprint: binding.pluginAppsFingerprint, - pluginAppsInputFingerprint: binding.pluginAppsInputFingerprint, - pluginAppPolicyContext: binding.pluginAppPolicyContext, + finalConfigPatch.nativeHookRelayGeneration ?? resumeBinding.nativeHookRelayGeneration, + pluginAppsFingerprint: resumeBinding.pluginAppsFingerprint, + pluginAppsInputFingerprint: resumeBinding.pluginAppsInputFingerprint, + pluginAppPolicyContext: resumeBinding.pluginAppPolicyContext, contextEngine: contextEngineBinding, environmentSelectionFingerprint, lifecycle: { diff --git a/src/plugin-sdk/test-helpers/agents/openclaw-owned-tool-runtime-contract.ts b/src/plugin-sdk/test-helpers/agents/openclaw-owned-tool-runtime-contract.ts index 0778c85a161e..e7083cbded8b 100644 --- a/src/plugin-sdk/test-helpers/agents/openclaw-owned-tool-runtime-contract.ts +++ b/src/plugin-sdk/test-helpers/agents/openclaw-owned-tool-runtime-contract.ts @@ -4,10 +4,7 @@ import { resetAdjustedParamsByToolCallIdForTests } from "../../../agents/agent-t import type { AgentToolResult } from "../../../agents/runtime/index.js"; import { setToolTerminalPresentation } from "../../../agents/tool-terminal-presentation.js"; import type { AnyAgentTool } from "../../../agents/tools/common.js"; -import type { - CodexAppServerExtensionFactory, - CodexAppServerToolResultEvent, -} from "../../../plugins/codex-app-server-extension-types.js"; +import type { AgentToolResultMiddlewareEvent } from "../../../plugins/agent-tool-result-middleware-types.js"; import { initializeGlobalHookRunner, resetGlobalHookRunner, @@ -90,20 +87,18 @@ export function installOpenClawOwnedToolHooks(params?: { * Pair with `installOpenClawOwnedToolHooks()` when a test asserts before/after hook behavior. */ export function installCodexToolResultMiddleware( - handler: (event: CodexAppServerToolResultEvent) => AgentToolResult, + handler: (event: AgentToolResultMiddlewareEvent) => AgentToolResult, ) { - const middleware = vi.fn(async (event: CodexAppServerToolResultEvent) => ({ + const middleware = vi.fn(async (event: AgentToolResultMiddlewareEvent) => ({ result: handler(event), })); const registry = createEmptyPluginRegistry(); - const factory: CodexAppServerExtensionFactory = async (codex) => { - codex.on("tool_result", middleware); - }; - registry.codexAppServerExtensionFactories.push({ + registry.agentToolResultMiddlewares.push({ pluginId: "runtime-contract", pluginName: "Runtime Contract", - rawFactory: factory, - factory, + rawHandler: middleware, + handler: middleware, + runtimes: ["codex"], source: "test", }); setActivePluginRegistry(registry);