Fix #1298: github capture: a foreign Link during all_visible enumeration aborts the whole tick instead of one repo - #1302
Fix #1298: github capture: a foreign Link during all_visible enumeration aborts the whole tick instead of one repo#1302philcunliffe wants to merge 4 commits into
Conversation
…ead tick `captureRepos` called `resolveRepos` outside the per-repo try/catch, so a failure while enumerating the `all_visible` inventory (a foreign `Link` header now throws `github_foreign_origin`) escaped the whole function and took the tick with it. Catch it, record it in `errors` alongside per-repo failures, and log `github.inventory_resolve_failed` with the error kind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…diagnosis `hyp github backfill owner/repo` reports "none of [...] are in the active repository inventory" whenever the selection comes back empty. Since an enumeration failure now degrades to zero repos plus a recorded error instead of throwing, that branch became reachable for a cause it does not describe, printing a false diagnosis directly under the real one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 1 - head
|
The new early return reported `pending: false` unconditionally. That is not merely declining to claim backlog, it clears it: on master an enumeration throw propagated past `tick()`'s assignment in source.js, so `backlogPending` survived the failed tick, while now the tick takes the success path and assigns false. A tick that exhausts its 400-request budget and saves continuations, followed by one whose `listViewerRepos()` fails transiently, therefore drops from the 15-minute backlog cadence to a full 24-hour poll interval with work still saved on disk. The tick captured nothing, so the honest answer is on the durable cursors: report the continuations they still hold. An error alone remains not backlog (the fresh-cursor case still reports false). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 1 fixed the false "none of [...] are in the active repository inventory" line under a real inventory error, but the same run still fell through to the success tail and printed "run 'hyp graph project'", telling the reader to project a table the tick never touched. Guard it with the same exact condition (`repos === 0` can only coexist with an error when the inventory itself failed, since an empty selection never enters the per-repo loop). Cover both diagnoses with the first tests the github commands have had, and make `hasSavedWork`'s doc honest: the durable cursors are the only evidence available on this path, and they are approximate on both sides (they carry work a failed repository left behind, and cannot see repos a budget-exhausted rotation never reached), which is still better than a flat `false` that would discard a real continuation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 2 (final) - head
|
| state | cursors.repos |
hasSavedWork |
right answer? |
|---|---|---|---|
| first-ever run | {} from readCursors's fallback (cursors.js:43), and repos is a non-optional Record in CursorState (types.d.ts:248), so Object.values cannot throw inside the catch |
false |
yes: nothing on disk, nothing to lose |
| exhausted but complete | capture.js:310 delete cursor.work on the comments-phase boundary, so a drained repo keeps only since/etag/pull_numbers |
false |
yes |
| partially consumed | a repo interrupted mid-phase keeps work (set at capture.js:210, persisted by tick.js's finally) |
true |
yes: this is the continuation the 15-minute cadence exists for |
| cursor entry created but untouched | capture.js:143 seeds {} before capture, work absent |
false |
yes, no false positive from the seed |
The flag is read before any mutation on that path (the early return precedes rotateTo), so it reports pre-tick durable state, which is exactly the quantity a tick that captured nothing should be reporting.
Where it does not agree with pending_repos. The JSDoc claimed it is "the same set the budget-exhausted log line counts as pending_repos" (capture.js:735 vs capture.js:187). That claim is false in both directions, and the divergence is the substance of the MEDIUM below. I rewrote the doc rather than the code; see the reasoning there.
Finding 2 (commands.js guard) - correct, and the condition is exact. result.repos === 0 means the filtered selection was empty, so the per-repo loop never ran and a non-empty errors can only be the inventory failure. It was, however, incomplete: see LOW below.
Finding 3 (LOW, fixed in cdf51d2a) - the same false diagnosis, one line further down
hypaware-core/plugins-workspace/github/src/commands.js:64
Round 1 stopped the "none of [...] are in the active repository inventory" line, but the same run still falls through to the success tail:
github backfill: 0 event(s) across 0 repo(s)
! (inventory): GitHub continuation URL refused: it does not address the configured API base ...
run 'hyp graph project' to project github_events into the graph
Before this PR a bad token or a foreign-origin proxy threw out of captureRepos and produced one clear stderr line at commands.js:67. Now the reader is handed a next step that projects a table this run never touched. Exit code was already 1, so this is diagnosis quality, same class as round 1's finding 2 and reachable by the same input, which is why I finished it rather than leaving it.
Fixed with the same exact condition:
if (result.repos === 0 && result.errors.length > 0) return 1New tests, and they are load-bearing. test/plugins/github-commands.test.js is the first test coverage the github commands have had at all (round 1's commands.js fix landed untested, which is how this one survived). Two cases: a failed enumeration prints the real cause and neither bad line; a genuinely absent repository still gets the inventory diagnosis, so the guards cannot silently over-suppress. Verified not ok 1 / ok 2 against commands.js at both origin/master and 99284fcd, and ok 1 / ok 2 with the fix.
Finding 4 (MEDIUM, accepted, not fixed) - hasSavedWork also counts failure residue
hypaware-core/plugins-workspace/github/src/capture.js:114
hasSavedWork counts any cursor holding work, which includes work a repository left behind by failing. The per-repo catch at capture.js:162 deliberately refuses to count that ("treating an error as pending pins a daily source at the 15-minute backlog cadence for as long as one repository keeps failing", LLP 0360#cadence). So: a token that lost scope for one private repo throws every tick and never clears its work, and while all_visible enumeration is also failing, nextCaptureDelay clamps a 24h source to BACKLOG_RETRY_MS for as long as both persist. It is symmetric in the other direction too: a rotation that exhausted its budget exactly at a repository boundary leaves un-visited repos with no work at all (capture.js:182 sets pending from visited < repos.length, which no cursor records), so hasSavedWork can under-report real backlog.
I am not fixing it, deliberately, and this is the round's judgment call:
- An exact answer is not on disk. Budget residue and failure residue are the same bytes. Distinguishing them means a new cursor field, which CLAUDE.md rules out for a bug fix ("do not invent columns, config keys, or schema fields"). The alternative, a
pending: undefined"this tick learned nothing" contract sosource.jskeeps its previous flag (which is exactly what master's throw path did), ripples through the shared return type intick.js,commands.js, andsource.jsat the final review round of a +34/-5 fix. - Both residual errors are bounded and non-destructive. The worst case is a mis-timed retry: one extra failing
listViewerReposcall per 15 minutes against an already-broken endpoint, or one delayed backlog resume. Neither loses rows or corrupts a cursor, and both self-heal the moment enumeration succeeds, because the success path recomputespendingfrom the real signal. - It is still strictly better than the alternative it replaced. A flat
falsediscards a real continuation; this at worst mis-times one. - The pathological combination also needs enumeration to be permanently broken, which is the condition
hyp statussurfaces vialastErroranyway.
What I did change is the claim: capture.js's JSDoc now says the cursors are the only evidence this path has and are approximate on both sides, instead of asserting an equivalence with pending_repos that does not hold. A future reader meets the trade-off where they meet the code.
Also checked, no action
tick.js:28still awaitsruntime.observedRepos.list()outside any guard, so the defaultsession_reposinventory keeps the whole-tick failure shape github capture: a foreign Link during all_visible enumeration aborts the whole tick instead of one repo #1298 describes. Unchanged from round 1's read of it: it is a local cache read, not a network call, and github capture: a foreign Link during all_visible enumeration aborts the whole tick instead of one repo #1298's acceptance condition nameslistViewerRepos()specifically. Out of scope, recorded so the next reader does not mistake it for covered.- Exit codes are unchanged by both guards: an inventory failure was exit 1 through the old
catchand is exit 1 now, viaerrors.length > 0.runGithubSynchas no next-step line and needed no equivalent. errKindremains a faithful extraction (null, primitive, and kind-less errors all yield{}).- No cursor damage: the early return precedes
rotateTo,next_repois untouched,writeCursorsstill runs intick.js'sfinally. npm test5990 pass / 0 fail (was 5988 before my two tests),npm run typecheckclean, both re-run after the fix. No em dashes, no semicolons, no new dependencies.- Independent
code-reviewpass at high effort raised exactly the three items above (MEDIUMhasSavedWorkresidue, LOW next-step line,tick.jsobservation) and found no null-deref, missingawait, or off-by-one. It surfaced no fourth issue.
Triage at head
|
Ship risk:
|
What neutral was doingRung Why it cannot proceedThe merge queue is evicting PRs for Evidence, two consecutive merge groups, both evicted:
For comparison, the same jobs on ordinary
So the suite normally finishes in 1 to 2 minutes and merge groups succeeded as recently This is repo-wide infrastructure, not a defect in this pull request. The change itself What it needs from youA call on the merge-queue CI budget. The options, as neutral sees them:
Neutral has deliberately not re-enqueued anything: with the cap unchanged, every How to unstickReply with a comment on this PR (or push to the branch); neutral monitors this thread and |
Feature or issue
GitHub capture is built so one bad repository never kills a whole tick, but
captureReposresolved its inventory outside that per-repotry/catch. Withinventory = "all_visible", enumeration reaches the network throughclient.listViewerRepos(), and since the origin pin landed a foreignLinkheader there throwsgithub_foreign_origin. The throw escapedcaptureReposentirely, so no repository was captured that tick and it surfaced only as a whole-tickgithub.poll_tick_failed.Solution
resolveReposcall incaptureRepos, record the failure in the sameerrorsarray per-repo failures use, and loggithub.inventory_resolve_failedwith the inventory mode anderror_kind; the tick returns normally with zero repos andpending: false, matching LLP 0360#cadence (a failure retries on the ordinary cadence, it is not bounded backlog).hypErrorKindextraction became oneerrKind(err)helper, shared with the per-repo error log.test/plugins/github-capture.test.js: enumeration throwsgithub_foreign_origin, and the tick completes with the error recorded and attributable. It fails on master and passes here; fullnpm test(5987 pass) andnpm run typecheckare green.Code: +34 / -5 lines
Fixes #1298