Skip to content

Commit 9873cdb

Browse files
authored
Fix Codex annotate-last message selection (#740)
1 parent 1b1cefb commit 9873cdb

4 files changed

Lines changed: 72 additions & 3 deletions

File tree

apps/hook/server/codex-session.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,43 @@ describe("getLastCodexMessage", () => {
310310
expect(result).not.toBeNull();
311311
expect(result!.text).toBe("Valid message");
312312
});
313+
314+
test("can ignore assistant messages from the active Codex turn", () => {
315+
const previousTurnId = "turn-previous";
316+
const activeTurnId = "turn-active";
317+
const path = writeTempRollout(
318+
buildRollout(
319+
sessionMeta(),
320+
turnStarted(previousTurnId),
321+
userMessage("Explain the thing"),
322+
assistantMessage("Substantive final answer"),
323+
turnCompleted(previousTurnId),
324+
turnStarted(activeTurnId),
325+
userMessage("[$plannotator-last]"),
326+
assistantMessage("I’ll open Plannotator on my last response.")
327+
)
328+
);
329+
330+
const result = getLastCodexMessage(path, { beforeActiveTurn: true });
331+
expect(result).not.toBeNull();
332+
expect(result!.text).toBe("Substantive final answer");
333+
});
334+
335+
test("keeps default latest-message behavior inside an active turn", () => {
336+
const turnId = "turn-active";
337+
const path = writeTempRollout(
338+
buildRollout(
339+
sessionMeta(),
340+
assistantMessage("Previous answer"),
341+
turnStarted(turnId),
342+
assistantMessage("Current status update")
343+
)
344+
);
345+
346+
const result = getLastCodexMessage(path);
347+
expect(result).not.toBeNull();
348+
expect(result!.text).toBe("Current status update");
349+
});
313350
});
314351

315352
describe("getLatestCodexPlan", () => {

apps/hook/server/codex-session.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,17 @@ export interface CodexPlanResult {
4848
source: CodexPlanSource;
4949
}
5050

51+
export interface GetLastCodexMessageOptions {
52+
beforeActiveTurn?: boolean;
53+
}
54+
5155
export interface GetLatestCodexPlanOptions {
5256
turnId?: string;
5357
stopHookActive?: boolean;
5458
}
5559

5660
const TURN_START_TYPES = new Set(["task_started", "turn_started"]);
61+
const TURN_COMPLETE_TYPES = new Set(["task_complete", "turn_completed"]);
5762
const PROPOSED_PLAN_RE = /<proposed_plan>([\s\S]*?)<\/proposed_plan>/gi;
5863

5964
// --- Rollout File Discovery ---
@@ -200,6 +205,24 @@ function findTurnStartIndex(entries: RolloutEntry[], turnId?: string): number {
200205
return lastTurnContext === -1 ? 0 : lastTurnContext;
201206
}
202207

208+
function findActiveTurnStartIndex(entries: RolloutEntry[]): number {
209+
const latestTurnStart = findLastIndex(
210+
entries,
211+
(entry) =>
212+
entry.type === "event_msg" &&
213+
TURN_START_TYPES.has(entry.payload?.type || "")
214+
);
215+
if (latestTurnStart === -1) return -1;
216+
217+
const latestTurnComplete = findLastIndex(
218+
entries,
219+
(entry) =>
220+
entry.type === "event_msg" &&
221+
TURN_COMPLETE_TYPES.has(entry.payload?.type || "")
222+
);
223+
return latestTurnStart > latestTurnComplete ? latestTurnStart : -1;
224+
}
225+
203226
function isHookPromptMessage(entry: RolloutEntry): boolean {
204227
if (entry.type !== "response_item") return false;
205228
if (entry.payload?.type !== "message") return false;
@@ -295,12 +318,17 @@ function pickLatestPreferredPlan(
295318
* Extracts output_text blocks from payload.content.
296319
*/
297320
export function getLastCodexMessage(
298-
rolloutPath: string
321+
rolloutPath: string,
322+
options: GetLastCodexMessageOptions = {}
299323
): { text: string } | null {
300324
const entries = parseRolloutEntries(rolloutPath);
325+
const activeTurnStart = options.beforeActiveTurn
326+
? findActiveTurnStartIndex(entries)
327+
: -1;
328+
const endIndex = activeTurnStart === -1 ? entries.length - 1 : activeTurnStart - 1;
301329

302330
// Walk backward
303-
for (let i = entries.length - 1; i >= 0; i--) {
331+
for (let i = endIndex; i >= 0; i--) {
304332
const entry = entries[i];
305333
if (entry.type !== "response_item") continue;
306334
if (entry.payload?.type !== "message") continue;

apps/hook/server/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ if (args[0] === "sessions") {
771771
if (process.env.PLANNOTATOR_DEBUG) {
772772
console.error(`[DEBUG] Rollout: ${rolloutPath}`);
773773
}
774-
const msg = getLastCodexMessage(rolloutPath);
774+
const msg = getLastCodexMessage(rolloutPath, { beforeActiveTurn: true });
775775
if (msg) {
776776
lastMessage = { messageId: codexThreadId, text: msg.text, lineNumbers: [] };
777777
}

apps/skills/plannotator-last/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ description: Open Plannotator on the latest rendered assistant message and use t
77

88
Use this skill when the user wants to annotate the latest assistant response in Plannotator.
99

10+
Do not send a commentary/status message before running the command. The command
11+
targets the latest rendered assistant response, so a preamble can mistakenly become the
12+
thing being annotated.
13+
1014
Run:
1115

1216
```bash

0 commit comments

Comments
 (0)