Skip to content

fix(sweep): unstarve markStale via search API; snapshot listings before mutating#75938

Open
Hem1700 wants to merge 4 commits into
anthropics:mainfrom
Hem1700:fix/sweep-starvation
Open

fix(sweep): unstarve markStale via search API; snapshot listings before mutating#75938
Hem1700 wants to merge 4 commits into
anthropics:mainfrom
Hem1700:fix/sweep-starvation

Conversation

@Hem1700

@Hem1700 Hem1700 commented Jul 9, 2026

Copy link
Copy Markdown

Problem

markStale currently labels nothing. It walks /repos/{owner}/{repo}/issues?state=open&sort=updated&direction=asc with a fixed budget of 10×100, but that listing interleaves the issues it can label with items it can only skip — and the skips are permanent residents of the oldest-updated-first window, so they accumulate at the front until no candidate is reachable.

Replaying the script's exact ten listing calls against this repo (2026-07-08), the full 1,000-item window contained:

category count leaves the window?
open pull requests 469 never (PRs aren't swept)
issues with ≥10 👍 (STALE_UPVOTE_THRESHOLD) 434 never
already stale/autoclose 114 only when closed
assigned 53 rarely
locked 2 never
labelable candidates 0

The newest item in the window was updated 2026-05-07 — seven weeks short of the 14-day cutoff. Meanwhile is:issue is:open updated:<2026-06-25 -label:stale -label:autoclose no:assignee matches 3,002 issues the sweep should be nudging but can never reach. Since markStale feeds closeExpired (stale → 14 d → close), the lifecycle pipeline is stalled for new issues, and the window only grows more clogged as more PRs and popular issues age into it.

Two smaller defects in the same pattern:

  • Mutating while offset-paginating. Labeling an issue (plus the nudge comment issue-lifecycle-comment.yml posts in response) bumps updated_at, reordering the ascending-sorted listing mid-walk; in closeExpired, closing an issue removes it from the labels=-filtered listing. Either way, each mutation on page N shifts the remaining pages left, so the page=N+1 fetch skips one issue per mutation. Same bug class Fix lock-closed-issues workflow: use search API instead of offset pagination #69470 fixed in lock-closed-issues.yml.
  • closeExpired's 10-page cap is too small: label:stale alone currently lists ~1,490 open issues (15 pages), so the oldest ~500 are never even examined.

Fix

  • markStale asks the search API for exactly the labelable set (is:issue is:open updated:<cutoff -label:stale -label:autoclose no:assignee, oldest-updated first) — the same approach as Fix lock-closed-issues workflow: use search API instead of offset pagination #69470. PRs, upvoted, assigned, and already-labeled issues no longer occupy the window. A client-side predicate re-checks every condition (the search index can lag) plus the two search can't express: the exact cutoff time (search's updated: is date-granular) and the 👍-specific threshold (search's reactions: counts all reaction kinds).
  • Per-run write cap (250) with 1 s pacing, matching lock-closed-issues.yml, so the backlog drains across scheduled runs without tripping secondary rate limits. Search fetches are paced (2 s) and retried with backoff on 403/429 — search's secondary limit is burst-sensitive.
  • closeExpired snapshots the full listing before closing anything (up to 30 pages), so closes can't shift pages underneath the walk, and the current stale backlog fits.
  • Decision logic lives in scripts/sweep-lib.ts with unit tests (bun test scripts/sweep-lib.test.ts, no network), following the issue-lifecycle.ts shared-module pattern.

The per-issue close logic (events → labeledAt, human-comment safety net) is deliberately untouched: #75541 is improving events handling there, and this PR stays out of its way — the two compose.

Verification

  • bun test scripts/sweep-lib.test.ts — 18 pass, no network.
  • The measurement above is the old markStale's exact walk (same endpoint, same filters, same page budget): deterministically 0 labels per run.
  • Live read-only --dry-run of the new code against this repo immediately surfaces real candidates and stops at the per-run cap:
=== marking stale (14d inactive) ===
#67586: would label stale (26d inactive) — [BUG] Claude Code installer places `claude.exe` in bin/ on Linux, install script does not create working symlink
#67728: would label stale (26d inactive) — [BUG] + [RFC] Tasks run forever: 7-layer architectural root cause + permanent fix proposal (PALMS)
#61927: would label stale (26d inactive) — [BUG] "Pull request status couldn't be checked" banner persistently appears on every session in worktree branches without an associated PR
...
(250 candidates — stopped at the per-run cap)

The closeExpired phase of the same dry-run walks the snapshot listing and surfaces real work as well (#57930: would close (stale, 14d old), #23012: skipping (human activity after stale label), …). The run also happened to exercise the retry path end-to-end: the token had a hot secondary rate limit, and the search fetches backed off and completed instead of killing the run.

With the cap at 250 and the sweep scheduled twice daily, the ~3,000-issue backlog drains in about six days, then the sweep returns to a steady trickle.

Hem1700 and others added 4 commits July 8, 2026 18:07
…t pagination

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re mutating

markStale walked the oldest-updated-first /issues listing with a fixed
10-page budget, but that window is now fully occupied by permanent
non-candidates (open PRs, >=10-upvote issues, already-stale, assigned),
so every run visited 1000 items and labeled zero. Query the search API
for exactly the labelable set instead, with a per-run write cap.

closeExpired offset-paginated a listing it was shrinking with every
close (skipping one issue per close at each page boundary) and its
stale listing outgrew the 10-page cap; snapshot the listing first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant