fix(review): rollout poll never gives up while the run is alive (Bugbot finding) · Entire

fix(review): rollout poll never gives up while the run is alive (Bugbot finding)

2503535→main·

peyton-alt·1w ago·2 files·+55 added/-9 removed

waitForRollout returned '' after ~30s of polling even with the review still running, permanently exiting the tailer — a rollout materialising late lost live tokens for the rest of a minutes-long run (the stdout per-turn fallback still delivered final counts, but no live movement). The poll now continues until the file appears or stop fires, debug- logging once past the expected-quickly window (the signature of a codex release changing the rollout layout) and backing off to a slower cadence so a missing file costs one cheap glob every ~2.4s.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

Sessions

01KX0YVCTWCESSW3AW8H673NGEView transcript

[?
test(review): pin that codex $name skills survive the legacy repair + native invocationClaude Code·37 steps](/content/gh/entireio/cli/session/99f0d90a-6e31-4c60-ba62-aa2dccd29033#timeline-01KX0YVCTWCESSW3AW8H673NGE/index.html)

Changes

2

141 unmodified lines

142
143
144
145
146
147
148
145
146
147
148
149
150
151
152
150
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
157
172
173
174
160
161
162
175
176
177

141 unmodified lines

}
}

// waitForRollout polls for the rollout file matching threadID, returning its
// path or "" if stop fires or the attempts are exhausted. Exhaustion is
// debug-logged: it is the likely failure mode if a codex release changes the
// rollout layout, and it would otherwise silently disable live tokens.
// waitForRollout polls for the rollout file matching threadID until it
// appears or stop fires — never giving up while the review is running, since
// a rollout that materialises late (slow codex startup, unusual layout
timing) should still get live tokens for the rest of the run. After the
// expected-quickly window it debug-logs once (the likely signature of a
// codex release changing the rollout layout, which would otherwise silently
// disable live tokens) and backs off to a slower poll.
func waitForRollout(ctx context.Context, sessionDir, threadID string, stop <-chan struct{}) string {
for range rolloutPollAttempts {
return pollForRollout(ctx, sessionDir, threadID, stop, rolloutPollAttempts, rolloutPollInterval)
}
}

func pollForRollout(ctx context.Context, sessionDir, threadID string, stop <-chan struct{}, window int, interval time.Duration) string {
for attempt := 0; ; attempt++ {
if path := findRolloutBySessionID(sessionDir, threadID); path != "" {
return path
}
wait := interval
if attempt >= window {
if attempt == window {
logging.Debug(ctx, "codex token tail: rollout file still missing; continuing to poll",
            slog.String("session_dir", sessionDir), slog.String("thread_id", threadID))
}
wait = interval * 8 // ~2.4s at production cadence — cheap for a minutes-long run
}
select {
case <-stop:
return ""
case <-time.After(rolloutPollInterval):
case <-time.After(wait):
}
}
logging.Debug(ctx, "codex token tail: rollout file never appeared",
    slog.String("session_dir", sessionDir), slog.String("thread_id", threadID))
return ""
}

// parseRolloutTokenCount extracts cumulative input/output token totals from one

Mcmd/entire/cli/agent/codex/review_tokens.go+21/-9

1
2
3
4
5
6
7
380 unmodified lines

388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423

package codex

import (
    "context"
    "io"
    "os"
    "path/filepath"
380 unmodified lines

t.Fatal("tailRolloutTokens did not return promptly after stop with no rollout file")
}

// TestPollForRollout_KeepsLookingPastTheWindow pins that the poll never
// gives up while stop is open: a rollout that materialises after the
// expected-quickly window must still be found (previously the poll returned
// "" after ~30s and live tokens were lost for the rest of the run).
func TestPollForRollout_KeepsLookingPastTheWindow(t *testing.T) {
    // Cannot t.Parallel — uses t.Setenv.
    dir := t.TempDir()
    t.Setenv("ENTIRE_TEST_CODEX_SESSION_DIR", dir)
    rollout := filepath.Join(dir, "rollout-2026-06-03T08-57-39-"+tailTestThreadID+".jsonl")

stop := make(chan struct{})
    defer close(stop)
    got := make(chan string, 1)
    go func() {
        got <- pollForRollout(context.Background(), dir, tailTestThreadID, stop, 3, 10*time.Millisecond)
    }()

// Create the file well after the 3-attempt window has elapsed.
    time.Sleep(200 * time.Millisecond)
    if err := os.WriteFile(rollout, []byte(tokenLine(1, 1)), 0o644); err != nil {
        t.Fatal(err)
    }

select {
    case path := <-got:
        if path != rollout {
            t.Fatalf("pollForRollout = %q, want %q (gave up instead of continuing past the window)", path, rollout)
        }
    case <-time.After(5 * time.Second):
        t.Fatal("pollForRollout did not find the late rollout")
    }
}