[BUG] Issue sweep applies lifecycle deadlines unevenly — sweep.ts pages by position through a list it reorders

Status Open
Maintainer reply None cached
Activity 1 comment · opened Aug 21, 2026

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

  1. On a repo with more than 100 issues eligible for the stale label, run the sweep for real:

``
bun run scripts/sweep.ts
``

  1. Note the count on the final Done: line.
  2. Run it again immediately.
  3. 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 Set of 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 .

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗