Twinned from https://github.com/cfsh/ninefold/issues/628 by tools/beads/import_github.py's reconcile pass.
A gh call that FAILS and an issue that genuinely has NO comments produce the same value, so a rate-limited poll tick reports the same thing as a quiet one: nothing new.
2026-08-06 ~11:52 UTC, a routine tick on this box. The digest printed:
``` ════ ISSUES CLAIMED BY THIS SESSION · LAST 3 COMMENTS ════ ════ IDLE ════ ```
Header, no content. My claimed issue #601 has a CLAIM comment on it and had since 06:53, so that section had been three lines every previous tick.
``` $ gh api rate_limit --jq .resources core: remaining 4998 graphql: remaining 0 ```
The GraphQL budget was exhausted; REST was untouched at 4998/5000. gh issue view / gh pr view spend GraphQL, so every issue read in the sweep failed while the tool carried on.
⚠️ Note what did NOT go wrong, because the alarming reading is the first one and it is not right. mine was still non-empty — the label/claim sweep that populates it had already succeeded — so WRAP_UP was correctly suppressed and the stop-condition predicate held. I initially read the empty section as "#601 has been closed or unclaimed" and had to check via REST (gh api repos/cfsh/ninefold/issues/601, which had budget) to establish it is open, labelled active+session:spawn-box, CLAIM intact. This is a missed-feedback defect, not a false-wrap-up one.
Two or fallbacks in tools/nflib/gh.py, each individually reasonable:
```python def issue_view(self, number, fields): return self.json(["issue", "view", ...]) or {}
def issue_comments(self, number): return self.issue_view(number, ["comments"]).get("comments", []) or [] ```
A failed call → {} → []. poll.py's emit_claimed then does (gh.issue_comments(n) or [])[-3:] and prints nothing. Same output as an issue nobody has commented on. Nothing is raised, nothing is logged, exit code is 0.
CLAUDE.md is explicit that this is a primary feedback channel:
> ⚠️ Issues carry conversations, not just titles. Aedan drops new work and design feedback as comments on *existing* open issues. Poll them.
and the poll loop's own instruction for every tick is "advance the issues in your chunk and read their latest comments." On a rate-limited tick that read silently returns nothing and the session concludes nothing arrived. A comment from Aedan landing in that window is invisible and stays invisible — the next tick's fingerprint may have moved on, and nothing re-reads it.
17,47 is two full digests per session per hour, and CHANGED fires on *any* session's tracker activity, so quiet sessions rarely get the cheap path.So the budget is expected to run out, repeatedly, at the busiest times — which are exactly the times feedback is most likely to be arriving.
The root is that json() or {} erases the difference between "asked and got nothing" and "could not ask". Options that occur to me, in rough order of cost:
nflib.gh boundary, and have poll.py print something like ⚠ could not read comments for #601 (gh failed) instead of silence. A tick that could not read is not a quiet tick and should not be reported as one.rate_limit before a full digest and say so up front — one cheap REST call. A digest that runs at 200 GraphQL remaining is going to be partly blind.Related: #618 — same failure *direction* (silent degradation reported as "nothing to do"), different input (a missing tracked-prs.txt). Both are the #389 shape the tooling tier already guards against elsewhere; emit_idle's PREDICATE UNEVALUABLE: no sid given is the pattern being asked for here.
None.
No comments.