You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Review commands should persist their Codex threads (write rollouts) like task runs do #529
Review runs (/codex:review, /codex:adversarial-review) start their app-server threads with ephemeral: true (hardcoded in the review path), while task/rescue runs pass persistThread: true. Ephemeral threads never write a rollout to ~/.codex/sessions.
Why this is a problem
The resume hint is broken for reviews./codex:status prints codex resume <session-id> for review jobs, but the thread is memory-only, so that ID goes dead as soon as the broker restarts. The printed hint promises something the implementation can't deliver.
Review usage is invisible locally. Token usage lives inside the rollout events; with no rollout, review runs never appear in ~/.codex/sessions, so local accounting tools that read the sessions tree (e.g. ccusage codex) see rescue usage but not review usage.
Two run kinds, two silent behaviors. Users have no reason to expect that a review and a rescue differ in persistence; the asymmetry is surprising and undocumented.
Request
Make review runs persist their threads unconditionally, exactly like task runs — not a flag, not a config option. Uniform behavior means the /codex:status resume hint is truthful for every job kind, and local usage accounting is complete for all plugin activity.
The plumbing already exists: persistThread in scripts/lib/codex.mjs (the thread-start call maps it to ephemeral); the review path just hardcodes ephemeral: true instead of using it.
+1, and two findings from patching this locally on 1.0.6 that the proposed approach would still leave broken:
1. Persisting the thread is necessary but not sufficient for codex resume <id>. With persistThread: true added to the adversarial-review call, the rollout is written to ~/.codex/sessions as expected. But codex exec resume <id> "<follow-up>" then fails with:
thread/resume failed: thread <id> already has an active writer (code -32600)
because the plugin's shared broker still holds the thread. So the codex resume <session-id> hint that /codex:status prints stays unusable while the broker is alive, even after the persistence fix.
2. The plugin itself has no way to continue a review.task --resume / --resume-last go through findLatestResumableTaskJob, which only matches jobClass === "task", so the last review is never a candidate. Continuing through the broker avoids the writer conflict above, so this is the natural path. Locally I changed that predicate to also accept adversarial reviews:
With both changes, node scripts/codex-companion.mjs task --resume "<counter-argument>" resumes the adversarial review with its full context (verified: it answered with its own verdict and the exact line it had flagged). This is the use case that motivated it: sending the reviewer a counter-argument finding by finding and letting it defend, adjust, or withdraw each one.
Suggestion for the fix: besides persisting review threads, either let task --resume-last pick up review jobs, or add an explicit --resume <threadId> (see #230 / #700), and make /codex:status print a resume command that works while the broker is running (the companion's, not codex resume). Native reviews (runAppServerReview) would need the same treatment; I only patched the adversarial path.
Correction to my previous comment: widening the findLatestResumableTaskJob predicate is the wrong fix. task-resume-candidate uses the same function, so after an adversarial review /codex:rescue offers to "continue" the review thread, which is read-only (cf. #740) — "apply the top fix" then silently can't write. And each resumed review is recorded as a task job, so the next --resume falls back into the review thread again.
What works cleanly (tested locally on 1.0.6):
keep findLatestResumableTaskJob exactly as upstream (tasks only), so --resume / /codex:rescue never pick up a review;
add a separate findLatestResumableReviewJob (jobClass === "review" && kind === "adversarial-review") and an explicit task --resume-review flag that resumes it through the broker, read-only, rejecting --write / --resume / --fresh;
record the resumed run as jobClass: "review", kind: "adversarial-review", so repeated --resume-review calls follow the same chain and task-resume-candidate still returns the last real task.
Verified: after a task T and an adversarial review R, --resume-review resumes R, a following --resume resumes T (not R), and task-resume-candidate returns T.
Activity
dinhnguyenminhhoang commentedon Jul 26, 2026
Hi, I’d like to work on this issue.
My proposed approach is:
Could you confirm that this approach is acceptable before I start?
Jerotheking commentedon Sep 24, 2026
+1, and two findings from patching this locally on 1.0.6 that the proposed approach would still leave broken:
1. Persisting the thread is necessary but not sufficient for
codex resume <id>. WithpersistThread: trueadded to the adversarial-review call, the rollout is written to~/.codex/sessionsas expected. Butcodex exec resume <id> "<follow-up>"then fails with:because the plugin's shared broker still holds the thread. So the
codex resume <session-id>hint that/codex:statusprints stays unusable while the broker is alive, even after the persistence fix.2. The plugin itself has no way to continue a review.
task --resume/--resume-lastgo throughfindLatestResumableTaskJob, which only matchesjobClass === "task", so the last review is never a candidate. Continuing through the broker avoids the writer conflict above, so this is the natural path. Locally I changed that predicate to also accept adversarial reviews:With both changes,
node scripts/codex-companion.mjs task --resume "<counter-argument>"resumes the adversarial review with its full context (verified: it answered with its own verdict and the exact line it had flagged). This is the use case that motivated it: sending the reviewer a counter-argument finding by finding and letting it defend, adjust, or withdraw each one.Suggestion for the fix: besides persisting review threads, either let
task --resume-lastpick up review jobs, or add an explicit--resume <threadId>(see #230 / #700), and make/codex:statusprint a resume command that works while the broker is running (the companion's, notcodex resume). Native reviews (runAppServerReview) would need the same treatment; I only patched the adversarial path.Jerotheking commentedon Sep 24, 2026
Correction to my previous comment: widening the
findLatestResumableTaskJobpredicate is the wrong fix.task-resume-candidateuses the same function, so after an adversarial review/codex:rescueoffers to "continue" the review thread, which is read-only (cf. #740) — "apply the top fix" then silently can't write. And each resumed review is recorded as ataskjob, so the next--resumefalls back into the review thread again.What works cleanly (tested locally on 1.0.6):
findLatestResumableTaskJobexactly as upstream (tasks only), so--resume//codex:rescuenever pick up a review;findLatestResumableReviewJob(jobClass === "review" && kind === "adversarial-review") and an explicittask --resume-reviewflag that resumes it through the broker, read-only, rejecting--write/--resume/--fresh;jobClass: "review",kind: "adversarial-review", so repeated--resume-reviewcalls follow the same chain andtask-resume-candidatestill returns the last real task.Verified: after a task T and an adversarial review R,
--resume-reviewresumes R, a following--resumeresumes T (not R), andtask-resume-candidatereturns T.fix(review): persist review threads under a review name (openai#529)