Skip to content

Commit 3527c36

Browse files
committed
fix(codex): fail closed interrupted native hook cleanup
1 parent 4b2176a commit 3527c36

3 files changed

Lines changed: 50 additions & 1 deletion

File tree

extensions/codex/src/app-server/event-projector.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ export class CodexAppServerEventProjector {
176176
private readonly options: CodexAppServerEventProjectorOptions = {},
177177
) {}
178178

179+
getCompletedTurnStatus(): CodexTurn["status"] | undefined {
180+
return this.completedTurn?.status;
181+
}
182+
179183
async handleNotification(notification: CodexServerNotification): Promise<void> {
180184
const params = isJsonObject(notification.params) ? notification.params : undefined;
181185
if (!params) {

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8233,6 +8233,46 @@ describe("runCodexAppServerAttempt", () => {
82338233
expect(nativeHookRelayTesting.getNativeHookRelayRegistrationForTests(relayId)).toBeUndefined();
82348234
});
82358235

8236+
it("cleans up native hook relay state when Codex completes the turn as interrupted", async () => {
8237+
const harness = createStartedThreadHarness();
8238+
const run = runCodexAppServerAttempt(
8239+
createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")),
8240+
{ nativeHookRelay: { enabled: true }, turnTerminalIdleTimeoutMs: 60_000 },
8241+
);
8242+
8243+
await harness.waitForMethod("turn/start");
8244+
const startRequest = harness.requests.find((request) => request.method === "thread/start");
8245+
const relayId = extractRelayIdFromThreadRequest(startRequest?.params);
8246+
await harness.notify({
8247+
method: "turn/completed",
8248+
params: {
8249+
threadId: "thread-1",
8250+
turnId: "turn-1",
8251+
turn: { id: "turn-1", status: "interrupted", items: [] },
8252+
},
8253+
});
8254+
8255+
const result = await run;
8256+
expect(result.aborted).toBe(false);
8257+
expect(result.timedOut).toBe(false);
8258+
expect(result.promptError).toBeNull();
8259+
expect(nativeHookRelayTesting.getNativeHookRelayRegistrationForTests(relayId)).toBeUndefined();
8260+
await expect(
8261+
invokeNativeHookRelay({
8262+
provider: "codex",
8263+
relayId,
8264+
event: "pre_tool_use",
8265+
rawPayload: {
8266+
hook_event_name: "PreToolUse",
8267+
tool_name: "Bash",
8268+
tool_input: { command: "pnpm test" },
8269+
},
8270+
}),
8271+
).rejects.toThrow("native hook relay not found");
8272+
testing.flushPendingCodexNativeHookRelayUnregistersForTests();
8273+
expect(nativeHookRelayTesting.getNativeHookRelayRegistrationForTests(relayId)).toBeUndefined();
8274+
});
8275+
82368276
it("keeps upstream cancellation aborted when Codex completes the turn as interrupted", async () => {
82378277
const harness = createStartedThreadHarness();
82388278
const abortController = new AbortController();

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3284,8 +3284,13 @@ export async function runCodexAppServerAttempt(
32843284
},
32853285
ctx: hookContext,
32863286
});
3287+
const completedTurnStatus = activeProjector.getCompletedTurnStatus();
32873288
shouldDelayNativeHookRelayUnregister =
3288-
!timedOut && !runAbortController.signal.aborted && !finalAborted && !finalPromptError;
3289+
completedTurnStatus === "completed" &&
3290+
!timedOut &&
3291+
!runAbortController.signal.aborted &&
3292+
!finalAborted &&
3293+
!finalPromptError;
32893294
return {
32903295
...result,
32913296
timedOut,

0 commit comments

Comments
 (0)