fix(codex): keep Stop plan review in the current turn - #1169
Conversation
|
Deep review done, including end-to-end reproduction from a worktree with an isolated data dir. The store itself is careful work: fail-open is real (missing file, corrupt JSON, empty session_id all fall through to a fresh review, verified), the config resolver matches house convention with garbage values disabling the feature, hashing makes path-shaped session ids safe, and 7-day pruning bounds growth. But the risk lives at the two call sites, not in the store, and that is where this needs changes. Blocking
Fix before merge regardless of direction: plan-decision-store.ts contains a literal NUL byte (the separator at line 44 is a raw 0x00, not an escape). Verified consequences: git shows the file as Binary (the PR diff literally says Bin 0 -> 4151 bytes, so nobody can review it as a diff), grep needs -a, and rg does not list the file's own definitions. Use \u0000 in the template literal. Should-fix, if the store direction survives: label replayed feedback (e.g. "Previously requested changes (not re-reviewed): ..."); Claude-path denials are recorded with user feedback but never read (write-only prose on disk); a malformed record on the Codex path currently becomes a fabricated "Plan changes requested" denial rather than being treated as absent (validate decision === approved|denied); the 10 new tests only cover the store in isolation, with nothing on approve-then-resubmit, deny-then-resubmit, corrupt-store-reopens, or session boundaries (I hand-verified several of those; they should be CI regression tests); the env-variables reference page is missing the new knob and AGENTS.md does not say it is hook-runtimes-only (OpenCode and Pi are unaffected); the normalizer duplicates codex-session's normalizePlan and the comment claiming it mirrors version-history dedup is inaccurate (saveToHistory compares exact strings). Honest bottom line: the annoyance in #1075 is real, but the Claude half needs an occurrence/freshness bound plus a visible replay signal before auto-answering a human gate, and the Codex half is better fixed at collectPlanCandidates' turn filtering than by replaying denials. Happy to talk through the occurrence-binding design if useful, and to re-review quickly on push. |
Reuse only fresh approvals for the active Claude ExitPlanMode occurrence; never replay Codex or denial decisions. Filter Codex proposal fallbacks to the requested turn. Refs backnotprop#1169
|
Pushed the review fixes. Claude reuse is visible and bound to one active submission occurrence. Codex now filters stale plans by turn. Checks are passing. |
|
Re-verified f2c5bab in depth, including 10 independent probes and end-to-end runs through the real hook binary. This redesign is exactly right and all three blockers are resolved:
Two small asks before merge:
Benign residuals, no action needed: a turn boundary without an id inherits the previous activeTurnId (extra review, safe direction); concurrent hook writes can lose a record (extra review, verified with 8 parallel writers, no temp leaks); future record-shape changes invalidate old records via the strict two-key check (safe, worth a comment). |
Reuse only fresh approvals for the active Claude ExitPlanMode occurrence; never replay Codex or denial decisions. Filter Codex proposal fallbacks to the requested turn. Refs backnotprop#1169
f2c5bab to
07dbdc0
Compare
|
Rebased onto current main and resolved the environment reference conflict. Added debug-only breadcrumbs for unsafe Codex Stop skips. Local rollout markers and Claude occurrence timing were also checked. |
|
Verified the final push directly: the PLANNOTATOR_DEBUG breadcrumb covers both skip shapes (missing Stop payload turn_id, missing id-carrying rollout marker) with distinct messages on stderr and a clean exit, exactly as asked, and the rebase onto current main resolved cleanly. All 208 hook tests pass on the head. This closes out both asks from the re-review; merge-ready from my side once the post-rebase CI finishes. |
|
Thanks for fixing this! This looks like it addresses both cases from my issue without skipping reviews for genuinely new plans. |
|
Thanks for confirming both cases, that closes the loop on the report. This is queued to merge. |
Reuse only fresh approvals for the active Claude ExitPlanMode occurrence; never replay Codex or denial decisions. Filter Codex proposal fallbacks to the requested turn. Refs backnotprop#1169
07dbdc0 to
2e0078d
Compare
|
@backnotprop rebased onto current main (7682628) — the only conflict was the Just pinging because you mentioned it was queued to merge, but it didn't happen after 3 days. |
|
@backnotprop reminder for this and the other #1154 fix, both ready |
Reuse only fresh approvals for the active Claude ExitPlanMode occurrence; never replay Codex or denial decisions. Filter Codex proposal fallbacks to the requested turn. Refs backnotprop#1169
77b0944 to
e9e7f3e
Compare
|
CI passing again, ready for you @backnotprop |
|
Following up here after my comment on #1075. I have walked the user-visible sequences again and here is where I land. The Codex Stop fix I understand and want. The transcript scraper is our logic, and re-surfacing an already-decided plan across a turn boundary is our bug. The turn-identity boundary and the fail-closed handling look like the right shape. The ExitPlanMode half I am going to pass on. The sequences that produce a duplicate review on Claude are: the model re-submitting the same plan (which your fix deliberately still reviews, and I agree with that call), the harness re-asking for the same tool call, or duplicate hook registration in local config. In all of those Plannotator is doing its job by asking. A decision store in our hook would be compensating for harness behavior in the wrong layer, and it is most of this diff. If there is a real-world Claude sequence I am missing, the video repro I asked for on the issue is the best way to show me. If you trim this PR to the Codex changes I will review it for merge. Sorry for the runaround, the Aug 1 "queued to merge" came before I had pulled the two cases apart. AI-assisted (Claude) under maintainer direction. |
e9e7f3e to
404397b
Compare
|
Trimmed this to the Codex-only Stop fix requested in the 2026-08-25 review. The Claude occurrence-bound approval reuse, its config, storage, transcript handling, tests, and docs are withdrawn from this PR. The Claude half of #1075 remains open for a separate contribution if a real-world reproduction supports it. The new head is one commit on current Fresh local gates:
|
Summary
Addresses the Codex Stop half of #1075.
The Codex
Stophook can scrape the latest<proposed_plan>from the rollout after a later turn that proposed no plan. That resurfaces a plan from an earlier turn and opens another review.This change keeps plan candidates inside the turn identified by the current Stop payload.
Approach
turn_idwith a matching id-carrying rollout marker before inspecting plan candidates.<proposed_plan>blocks only while the active marker matches the Stop turn.Planitems.PLANNOTATOR_DEBUGbreadcrumb for missing Stop identity or a missing rollout marker.There is no decision store or replay on this path.
Validation
bun test apps/hook/server/codex-session.test.ts: 26 passbun test apps/hook/server: 231 passbun run typecheck: passbun run build:review: passbun run build:hook: passTests cover missing and blank Stop turn IDs, missing rollout markers, debug-only diagnostics, later-turn stale plan blocks, turn contexts without IDs, and existing Stop re-entry behavior.
Compatibility
No public API, endpoint, config, or payload changes. Claude
ExitPlanModebehavior is unchanged.