Skip to content

Commit b337411

Browse files
committed
fix(codex): rotate relay generation on fresh thread fallback
1 parent 668590b commit b337411

4 files changed

Lines changed: 112 additions & 169 deletions

File tree

extensions/codex/src/app-server/attempt-startup.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,9 @@ export async function startCodexAttemptThread(params: {
8484
effectiveWorkspace: string;
8585
dynamicTools: CodexDynamicToolSpec[];
8686
developerInstructions: string | undefined;
87-
finalConfigPatch: Parameters<typeof startOrResumeThread>[0]["finalConfigPatch"];
88-
nativeHookRelayGeneration: string | undefined;
87+
finalConfigPatch?: Parameters<typeof startOrResumeThread>[0]["finalConfigPatch"];
88+
buildFinalConfigPatch?: Parameters<typeof startOrResumeThread>[0]["buildFinalConfigPatch"];
89+
nativeHookRelayGeneration?: string;
8990
bundleMcpThreadConfig: CodexBundleMcpThreadConfig;
9091
nativeToolSurfaceEnabled: boolean;
9192
sandboxExecServerEnabled: boolean;
@@ -254,6 +255,7 @@ export async function startCodexAttemptThread(params: {
254255
developerInstructions: params.developerInstructions,
255256
config: threadConfig,
256257
finalConfigPatch: params.finalConfigPatch,
258+
buildFinalConfigPatch: params.buildFinalConfigPatch,
257259
nativeHookRelayGeneration: params.nativeHookRelayGeneration,
258260
nativeCodeModeEnabled: params.nativeToolSurfaceEnabled,
259261
nativeCodeModeOnlyEnabled: params.appServer.codeModeOnly,

extensions/codex/src/app-server/run-attempt.native-hook-relay.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,59 @@ describe("runCodexAppServerAttempt native hook relay", () => {
585585
testing.flushPendingCodexNativeHookRelayUnregistersForTests();
586586
});
587587

588+
it("rotates native hook relay generations when resume fails over to a fresh thread", async () => {
589+
const sessionFile = path.join(tempDir, "session.jsonl");
590+
const workspaceDir = path.join(tempDir, "workspace");
591+
await writeCodexAppServerBinding(sessionFile, {
592+
threadId: "thread-existing",
593+
cwd: workspaceDir,
594+
model: "gpt-5.4-codex",
595+
modelProvider: "openai",
596+
nativeHookRelayGeneration: "generation-from-failed-resume",
597+
});
598+
const harness = createStartedThreadHarness(async (method) => {
599+
if (method === "thread/resume") {
600+
throw new Error("resume failed");
601+
}
602+
return undefined;
603+
});
604+
605+
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir), {
606+
nativeHookRelay: {
607+
enabled: true,
608+
events: ["pre_tool_use"],
609+
},
610+
});
611+
await harness.waitForMethod("turn/start");
612+
613+
const startRequest = harness.requests.find((request) => request.method === "thread/start");
614+
const relayId = extractRelayIdFromThreadRequest(startRequest?.params);
615+
const currentGeneration = extractGenerationFromThreadRequest(startRequest?.params);
616+
expect(currentGeneration).not.toBe("generation-from-failed-resume");
617+
await expect(
618+
invokeNativeHookRelay({
619+
provider: "codex",
620+
relayId,
621+
generation: "generation-from-failed-resume",
622+
event: "pre_tool_use",
623+
requireGeneration: true,
624+
rawPayload: {
625+
hook_event_name: "PreToolUse",
626+
tool_name: "Bash",
627+
tool_use_id: "failed-resume-stale-tool",
628+
tool_input: { command: "pwd" },
629+
},
630+
}),
631+
).rejects.toThrow("native hook relay bridge stale registration");
632+
633+
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
634+
await run;
635+
expect((await readCodexAppServerBinding(sessionFile))?.nativeHookRelayGeneration).toBe(
636+
currentGeneration,
637+
);
638+
testing.flushPendingCodexNativeHookRelayUnregistersForTests();
639+
});
640+
588641
it("builds deterministic opaque Codex native hook relay ids", () => {
589642
const relayId = testing.buildCodexNativeHookRelayId({
590643
agentId: "dev-codex",

extensions/codex/src/app-server/run-attempt.ts

Lines changed: 26 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ import {
113113
isCodexAppServerApprovalPolicyAllowedByRequirements,
114114
isCodexSandboxExecServerEnabled,
115115
readCodexPluginConfig,
116-
resolveCodexPluginsPolicy,
117116
resolveCodexComputerUseConfig,
118117
resolveCodexAppServerRuntimeOptions,
119118
shouldAutoApproveCodexAppServerApprovals,
@@ -127,7 +126,6 @@ import {
127126
import {
128127
buildDynamicTools,
129128
createCodexDynamicToolBuildStageTracker,
130-
disableCodexPluginThreadConfig,
131129
filterCodexDynamicToolsForAllowlist,
132130
formatCodexDynamicToolBuildStageSummary,
133131
includeForcedCodexDynamicToolAllow,
@@ -181,11 +179,6 @@ import {
181179
} from "./native-hook-relay.js";
182180
import { registerCodexNativeSubagentMonitor } from "./native-subagent-monitor.js";
183181
import { describeCodexNotificationCorrelation } from "./notification-correlation.js";
184-
import { buildCodexPluginAppCacheKey } from "./plugin-app-cache-key.js";
185-
import {
186-
buildCodexPluginThreadConfigInputFingerprint,
187-
shouldBuildCodexPluginThreadConfig,
188-
} from "./plugin-thread-config.js";
189182
import { isCodexAppServerProfilerEnabled } from "./profiler-flag.js";
190183
import {
191184
assertCodexTurnStartResponse,
@@ -214,7 +207,6 @@ import {
214207
buildTurnCollaborationMode,
215208
buildTurnStartParams,
216209
codexDynamicToolsFingerprint,
217-
resolveCodexNativeHookRelayBindingReuse,
218210
type CodexAppServerThreadLifecycleBinding,
219211
type CodexContextEngineThreadBootstrapProjection,
220212
} from "./thread-lifecycle.js";
@@ -802,58 +794,16 @@ export async function runCodexAppServerAttempt(
802794
timeoutMs: params.timeoutMs,
803795
timeoutFloorMs: options.startupTimeoutFloorMs,
804796
});
805-
const nativeHookRelayPluginThreadConfigRequired =
806-
!nativeToolSurfaceEnabled || shouldBuildCodexPluginThreadConfig(pluginConfig);
807-
const nativeHookRelayPluginThreadConfigPluginConfig = nativeToolSurfaceEnabled
808-
? pluginConfig
809-
: disableCodexPluginThreadConfig(pluginConfig);
810-
const nativeHookRelayPluginAppCacheKey = nativeHookRelayPluginThreadConfigRequired
811-
? buildCodexPluginAppCacheKey({
812-
appServer,
813-
agentDir,
814-
authProfileId: startupAuthProfileId,
815-
accountId: startupAuthAccountCacheKey,
816-
envApiKeyFingerprint: startupEnvApiKeyCacheKey,
817-
})
818-
: undefined;
819-
const nativeHookRelayResolvedPluginPolicy = nativeHookRelayPluginThreadConfigRequired
820-
? resolveCodexPluginsPolicy(nativeHookRelayPluginThreadConfigPluginConfig)
821-
: undefined;
822-
const nativeHookRelayBindingReuse = resolveCodexNativeHookRelayBindingReuse({
823-
binding: startupBinding,
824-
attemptParams: buildActiveRunAttemptParams(),
825-
agentId: sessionAgentId,
826-
dynamicTools: toolBridge.specs,
827-
nativeCodeModeEnabled: nativeToolSurfaceEnabled,
828-
userMcpServersEnabled: nativeToolSurfaceEnabled,
829-
mcpServersFingerprint: bundleMcpThreadConfig.fingerprint,
830-
mcpServersFingerprintEvaluated: bundleMcpThreadConfig.evaluated,
831-
environmentSelection: undefined,
832-
contextEngineProjection,
833-
pluginThreadConfig: nativeHookRelayPluginThreadConfigRequired
834-
? {
835-
enabled: true,
836-
inputFingerprint: buildCodexPluginThreadConfigInputFingerprint({
837-
pluginConfig: nativeHookRelayPluginThreadConfigPluginConfig,
838-
appCacheKey: nativeHookRelayPluginAppCacheKey!,
839-
}),
840-
enabledPluginConfigKeys: nativeHookRelayResolvedPluginPolicy?.pluginPolicies
841-
.filter((plugin) => plugin.enabled)
842-
.map((plugin) => plugin.configKey)
843-
.toSorted(),
844-
}
845-
: undefined,
846-
});
847-
try {
848-
emitCodexAppServerEvent(params, {
849-
stream: "codex_app_server.lifecycle",
850-
data: { phase: "startup" },
851-
});
797+
const buildNativeHookRelayFinalConfigPatch = (
798+
decision: { action: "resume"; binding: CodexAppServerThreadBinding } | { action: "start" },
799+
) => {
800+
nativeHookRelay?.unregister();
852801
nativeHookRelay = createCodexNativeHookRelay({
853802
options: options.nativeHookRelay,
854-
generation: nativeHookRelayBindingReuse.generation,
803+
generation:
804+
decision.action === "resume" ? decision.binding.nativeHookRelayGeneration : undefined,
855805
generationMismatchGraceMs:
856-
nativeHookRelayBindingReuse.canReuseBinding && !nativeHookRelayBindingReuse.generation
806+
decision.action === "resume" && !decision.binding.nativeHookRelayGeneration
857807
? CODEX_NATIVE_HOOK_RELAY_TTL_GRACE_MS
858808
: undefined,
859809
events: nativeHookRelayEvents,
@@ -868,15 +818,24 @@ export async function runCodexAppServerAttempt(
868818
turnStartTimeoutMs: params.timeoutMs,
869819
signal: runAbortController.signal,
870820
});
871-
const nativeHookRelayConfig = nativeHookRelay
872-
? buildCodexNativeHookRelayConfig({
873-
relay: nativeHookRelay,
874-
events: nativeHookRelayEvents,
875-
hookTimeoutSec: options.nativeHookRelay?.hookTimeoutSec,
876-
})
877-
: options.nativeHookRelay?.enabled === false
878-
? buildCodexNativeHookRelayDisabledConfig()
879-
: undefined;
821+
return {
822+
configPatch: nativeHookRelay
823+
? buildCodexNativeHookRelayConfig({
824+
relay: nativeHookRelay,
825+
events: nativeHookRelayEvents,
826+
hookTimeoutSec: options.nativeHookRelay?.hookTimeoutSec,
827+
})
828+
: options.nativeHookRelay?.enabled === false
829+
? buildCodexNativeHookRelayDisabledConfig()
830+
: undefined,
831+
nativeHookRelayGeneration: nativeHookRelay?.generation,
832+
};
833+
};
834+
try {
835+
emitCodexAppServerEvent(params, {
836+
stream: "codex_app_server.lifecycle",
837+
data: { phase: "startup" },
838+
});
880839
const startupResult = await startCodexAttemptThread({
881840
attemptClientFactory,
882841
appServer,
@@ -892,8 +851,7 @@ export async function runCodexAppServerAttempt(
892851
effectiveWorkspace,
893852
dynamicTools: toolBridge.specs,
894853
developerInstructions: promptBuild.developerInstructions,
895-
finalConfigPatch: nativeHookRelayConfig,
896-
nativeHookRelayGeneration: nativeHookRelay?.generation,
854+
buildFinalConfigPatch: buildNativeHookRelayFinalConfigPatch,
897855
bundleMcpThreadConfig,
898856
nativeToolSurfaceEnabled,
899857
sandboxExecServerEnabled,

0 commit comments

Comments
 (0)