diff --git a/apps/hook/server/codex-session.test.ts b/apps/hook/server/codex-session.test.ts index c9d749953..3884446db 100644 --- a/apps/hook/server/codex-session.test.ts +++ b/apps/hook/server/codex-session.test.ts @@ -310,6 +310,43 @@ describe("getLastCodexMessage", () => { expect(result).not.toBeNull(); expect(result!.text).toBe("Valid message"); }); + + test("can ignore assistant messages from the active Codex turn", () => { + const previousTurnId = "turn-previous"; + const activeTurnId = "turn-active"; + const path = writeTempRollout( + buildRollout( + sessionMeta(), + turnStarted(previousTurnId), + userMessage("Explain the thing"), + assistantMessage("Substantive final answer"), + turnCompleted(previousTurnId), + turnStarted(activeTurnId), + userMessage("[$plannotator-last]"), + assistantMessage("I’ll open Plannotator on my last response.") + ) + ); + + const result = getLastCodexMessage(path, { beforeActiveTurn: true }); + expect(result).not.toBeNull(); + expect(result!.text).toBe("Substantive final answer"); + }); + + test("keeps default latest-message behavior inside an active turn", () => { + const turnId = "turn-active"; + const path = writeTempRollout( + buildRollout( + sessionMeta(), + assistantMessage("Previous answer"), + turnStarted(turnId), + assistantMessage("Current status update") + ) + ); + + const result = getLastCodexMessage(path); + expect(result).not.toBeNull(); + expect(result!.text).toBe("Current status update"); + }); }); describe("getLatestCodexPlan", () => { diff --git a/apps/hook/server/codex-session.ts b/apps/hook/server/codex-session.ts index a3e625d31..585e5f2d5 100644 --- a/apps/hook/server/codex-session.ts +++ b/apps/hook/server/codex-session.ts @@ -48,12 +48,17 @@ export interface CodexPlanResult { source: CodexPlanSource; } +export interface GetLastCodexMessageOptions { + beforeActiveTurn?: boolean; +} + export interface GetLatestCodexPlanOptions { turnId?: string; stopHookActive?: boolean; } const TURN_START_TYPES = new Set(["task_started", "turn_started"]); +const TURN_COMPLETE_TYPES = new Set(["task_complete", "turn_completed"]); const PROPOSED_PLAN_RE = /([\s\S]*?)<\/proposed_plan>/gi; // --- Rollout File Discovery --- @@ -200,6 +205,24 @@ function findTurnStartIndex(entries: RolloutEntry[], turnId?: string): number { return lastTurnContext === -1 ? 0 : lastTurnContext; } +function findActiveTurnStartIndex(entries: RolloutEntry[]): number { + const latestTurnStart = findLastIndex( + entries, + (entry) => + entry.type === "event_msg" && + TURN_START_TYPES.has(entry.payload?.type || "") + ); + if (latestTurnStart === -1) return -1; + + const latestTurnComplete = findLastIndex( + entries, + (entry) => + entry.type === "event_msg" && + TURN_COMPLETE_TYPES.has(entry.payload?.type || "") + ); + return latestTurnStart > latestTurnComplete ? latestTurnStart : -1; +} + function isHookPromptMessage(entry: RolloutEntry): boolean { if (entry.type !== "response_item") return false; if (entry.payload?.type !== "message") return false; @@ -295,12 +318,17 @@ function pickLatestPreferredPlan( * Extracts output_text blocks from payload.content. */ export function getLastCodexMessage( - rolloutPath: string + rolloutPath: string, + options: GetLastCodexMessageOptions = {} ): { text: string } | null { const entries = parseRolloutEntries(rolloutPath); + const activeTurnStart = options.beforeActiveTurn + ? findActiveTurnStartIndex(entries) + : -1; + const endIndex = activeTurnStart === -1 ? entries.length - 1 : activeTurnStart - 1; // Walk backward - for (let i = entries.length - 1; i >= 0; i--) { + for (let i = endIndex; i >= 0; i--) { const entry = entries[i]; if (entry.type !== "response_item") continue; if (entry.payload?.type !== "message") continue; diff --git a/apps/hook/server/index.ts b/apps/hook/server/index.ts index 9287d2fee..13709a53e 100644 --- a/apps/hook/server/index.ts +++ b/apps/hook/server/index.ts @@ -771,7 +771,7 @@ if (args[0] === "sessions") { if (process.env.PLANNOTATOR_DEBUG) { console.error(`[DEBUG] Rollout: ${rolloutPath}`); } - const msg = getLastCodexMessage(rolloutPath); + const msg = getLastCodexMessage(rolloutPath, { beforeActiveTurn: true }); if (msg) { lastMessage = { messageId: codexThreadId, text: msg.text, lineNumbers: [] }; } diff --git a/apps/skills/plannotator-last/SKILL.md b/apps/skills/plannotator-last/SKILL.md index b37ed76ef..9df2b32e5 100644 --- a/apps/skills/plannotator-last/SKILL.md +++ b/apps/skills/plannotator-last/SKILL.md @@ -7,6 +7,10 @@ description: Open Plannotator on the latest rendered assistant message and use t Use this skill when the user wants to annotate the latest assistant response in Plannotator. +Do not send a commentary/status message before running the command. The command +targets the latest rendered assistant response, so a preamble can mistakenly become the +thing being annotated. + Run: ```bash