Skip to content
Merged
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
37 changes: 37 additions & 0 deletions apps/hook/server/codex-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
32 changes: 30 additions & 2 deletions apps/hook/server/codex-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /<proposed_plan>([\s\S]*?)<\/proposed_plan>/gi;

// --- Rollout File Discovery ---
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion apps/hook/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] };
}
Expand Down
4 changes: 4 additions & 0 deletions apps/skills/plannotator-last/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading