[BUG] Issue sweep applies lifecycle deadlines unevenly — sweep.ts pages by position through a list it reorders
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
Note: this is a bug in this repo's own automation (scripts/sweep.ts), not in the Claude Code CLI. The version / platform / terminal fields below are required by the template but don't apply here — I've filled them in as accurately as I can.
What's Wrong?
scripts/sweep.ts misses a portion of eligible issues on every run, and reports success anyway.
The effect users see. When an issue gets the stale label, the lifecycle workflow posts:
This issue has been automatically marked as stale due to inactivity. This issue will be closed automatically if there's no activity within 14 days.
That message promises a fixed deadline. In practice the deadline is inconsistent. Two issues that both went quiet on the same day can be labelled a week or more apart, depending only on where they happened to sit in the list during a given run. Nothing about the issue itself causes the difference.
The cause. Both loops walk the list by position while changing that same list.
markStale() — line 54 — counts up through pages:
for (let page = 1; page <= 10; page++) {
const issues = await githubRequest<any[]>(
`/repos/${owner}/${repo}/issues?state=open&sort=updated&direction=asc&per_page=100&page=${page}`
);
The list is sorted by updated, ascending. On line 82 the loop adds a label — and on GitHub, labelling an issue updates its updated_at. The issue therefore jumps from the front of an ascending list to the back, and everything behind it shifts forward one position.
After the first 100 issues are labelled, the list has shifted by 100. The request for page=2 asks for positions 101–200, but a different set of issues occupies those positions now. The 100 that shifted forward are never examined on that run.
closeExpired() — line 100 — has the same shape. Line 145 closes the issue, and since the query filters on state=open, closing removes it from the result set entirely. The list shifts by exactly the number just closed, and the next page request overshoots by that amount.
Why it hasn't surfaced. There's no error. The final line reports what was done, never what was skipped:
Done: 47 labeled stale, 12 closed
The workflow goes green. Skipped issues are usually picked up on a later run, so the queue does drain — it just drains unevenly, and the timing users are promised isn't the timing they get.
Small illustration
Nine eligible issues, pages of three (the real code uses 100 — same behaviour):
A B C D E F G H I
Page 1 returns A B C. All three get labelled, so all three move to the back:
D E F G H I A B C
Page 2 asks for positions 4, 5, 6 — which now hold G H I. D E F sit untouched at the front, and the loop has already moved past them.
What Should Happen?
Every issue meeting the staleness rules should be labelled in a single pass, and every issue past its lifecycle timeout should be closed in a single pass, so that the 14-day deadline stated in the warning comment is the deadline that's actually enforced.
Error Messages/Logs
None — that's part of the problem. The run exits 0 and the workflow reports success.
Steps to Reproduce
- On a repo with more than 100 issues eligible for the
stalelabel, run the sweep for real:
````
bun run scripts/sweep.ts
- Note the count on the final
Done:line. - Run it again immediately.
- The second run labels a further batch of issues that were already eligible during the first run and should have been handled then. Nothing changed between the two runs except the shifting of the list.
--dry-run does not reproduce this. In dry-run mode nothing is labelled or closed, so the list never shifts and the paging works correctly. The bug only appears on real runs, which is likely why it hasn't been caught.
Is this a regression?
I don't know
Claude Code Version
N/A — scripts/sweep.ts in this repository, not the CLI
Platform
Other — GitHub Actions (.github/workflows/sweep.yml)
Operating System
Other Linux — ubuntu-latest runner
Terminal/Shell
Non-interactive/CI environment
Additional Information
This repo has already fixed this exact class of bug once. Commit baf38dd — "Fix lock-closed-issues workflow: use search API instead of offset pagination" (#69470) — hit the same wall in .github/workflows/lock-closed-issues.yml.
The approach used there applies directly here: rather than counting up through pages, request the first page repeatedly. Because handled issues have moved out of the way, the first page keeps refilling with genuine work until it comes back empty. That version also:
- keeps a
Setof issue numbers already processed, so a failed write can't cause an endless loop - caps the number handled per run (
MAX_PER_RUN) - sleeps between writes to stay under secondary rate limits
Both markStale() and closeExpired() need the change.
Happy to open a PR following the lock-closed-issues.yml pattern .
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗