Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion plugins/codex/scripts/lib/codex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -559,9 +559,12 @@ function applyTurnNotification(state, message) {
async function captureTurn(client, threadId, startRequest, options = {}) {
const state = createTurnCaptureState(threadId, options);
const previousHandler = client.notificationHandler;
let startResponded = false;

client.setNotificationHandler((message) => {
if (!state.turnId) {
// A turn/start response may omit turn.id. Buffer only until the start
// request settles, not until a turn ID happens to become available.
if (!startResponded) {
state.bufferedNotifications.push(message);
return;
}
Expand Down Expand Up @@ -598,6 +601,7 @@ async function captureTurn(client, threadId, startRequest, options = {}) {
}
}
state.bufferedNotifications.length = 0;
startResponded = true;

if (response.turn?.status && response.turn.status !== "inProgress") {
completeTurn(state, response.turn);
Expand Down
22 changes: 20 additions & 2 deletions tests/fake-codex-fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,15 @@ rl.on("line", (line) => {
prompt
};
saveState(state);
send({ id: message.id, result: { turn: buildTurn(turnId) } });
if (BEHAVIOR === "missing-turn-id-with-early-event") {
// Simulate a notification sent before the turn/start response.
send({ method: "turn/started", params: { threadId: thread.id, turn: buildTurn(turnId) } });
}
if (BEHAVIOR === "missing-turn-id" || BEHAVIOR === "missing-turn-id-with-early-event") {
send({ id: message.id, result: { turn: { status: "inProgress", items: [] } } });
} else {
send({ id: message.id, result: { turn: buildTurn(turnId) } });
}

const payload = message.params.outputSchema && message.params.outputSchema.properties && message.params.outputSchema.properties.verdict
? structuredReviewPayload(prompt)
Expand Down Expand Up @@ -585,7 +593,17 @@ rl.on("line", (line) => {
}
];

if (BEHAVIOR === "interruptible-slow-task") {
if (BEHAVIOR === "missing-turn-id" || BEHAVIOR === "missing-turn-id-with-early-event") {
// The remaining notifications arrive after turn/start resolves.
setTimeout(() => {
for (const entry of items) {
if (entry.completed) {
send({ method: "item/completed", params: { threadId: thread.id, turnId, item: entry.completed } });
}
}
send({ method: "turn/completed", params: { threadId: thread.id, turn: buildTurn(turnId, "completed") } });
}, 60);
} else if (BEHAVIOR === "interruptible-slow-task") {
send({ method: "turn/started", params: { threadId: thread.id, turn: buildTurn(turnId) } });
const timer = setTimeout(() => {
if (!interruptibleTurns.has(turnId)) {
Expand Down
37 changes: 37 additions & 0 deletions tests/missing-turn-id.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";

import { buildEnv, installFakeCodex } from "./fake-codex-fixture.mjs";
import { initGitRepo, makeTempDir, run } from "./helpers.mjs";

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const SCRIPT = path.join(ROOT, "plugins", "codex", "scripts", "codex-companion.mjs");

for (const behavior of ["missing-turn-id", "missing-turn-id-with-early-event"]) {
test(`task finishes when turn/start omits turn.id (${behavior})`, () => {
const repo = makeTempDir();
const binDir = makeTempDir();
installFakeCodex(binDir, behavior);
initGitRepo(repo);
fs.writeFileSync(path.join(repo, "README.md"), "fixture\n");
run("git", ["add", "README.md"], { cwd: repo });
run("git", ["commit", "-m", "init"], { cwd: repo });

// Bound the regression: the old notification gate would wait forever.
const result = spawnSync(process.execPath, [SCRIPT, "task", "verify progress"], {
cwd: repo,
env: buildEnv(binDir),
encoding: "utf8",
timeout: 8000
});

assert.equal(result.status, 0, result.stderr || String(result.error));
assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n");
const state = JSON.parse(fs.readFileSync(path.join(binDir, "fake-codex-state.json"), "utf8"));
assert.ok(state.lastTurnStart.turnId);
});
}