fix(classifier): survive a vanished starred repo in the README probe#54
Merged
Merged
Conversation
The scheduled "AI operational state" workflow (classifier plan --save-state) crashed with exit 10 when a starred repo was deleted. The operational state records a README path for such a repo, so the planner takes the GraphQL blobOid fast path, and raw @octokit/graphql throws a NOT_FOUND GraphqlResponseError that blobOid never caught — one vanished star aborted the whole per-repo sweep, stalling state reconciliation for every repo. It surfaced only in steady state: first discovery uses REST (404 -> null, resilient); a cached README path takes the unguarded GraphQL fast path. Handle a repository-level NOT_FOUND at the seam. blobOid now treats an unresolvable repository (deleted / renamed away / turned private) as an absent README (null), matching the REST path's existing 404 -> null. The GraphQL probe is also wrapped in the shared RetryCoordinator, so a transient fault (secondary rate limit, 5xx, GraphQL exec timeout) on any single probe retries with bounded backoff instead of crashing the batch, while auth/invalid errors still fail loudly. - isRepositoryNotFound matches errors[].type === 'NOT_FOUND' scoped to the `repository` field (BLOB_OID_QUERY has one resolvable node). - Optional chaining tolerates the rare empty 2xx envelope (undefined). - Tests: NOT_FOUND tolerated without retry, transient retry-then-succeed, exec-timeout retry, budget exhaustion fails loud, terminal error propagates, empty envelope falls through, non-repository NOT_FOUND is not swallowed. Regression gate confirmed to FAIL on the pre-fix code. Verified end-to-end against the real deleted repo (returns null) and a live repo (resolves normally). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLoMBRETP2VWBxT1whUNc9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The scheduled AI operational state workflow (
ai-state.yml→classifier plan --save-state) has been failing daily with exit 10:curtischoutw/claude-institutionis a starred repo that was deleted (itsnode_idno longer resolves). The operational state onstarledger-ai-staterecordsreadme_path: "README.md"for it, so the planner takes the GraphQLblobOidfast path. Raw@octokit/graphqlthrows aNOT_FOUNDGraphqlResponseErrorthatblobOidnever caught, so it propagated through the planner's per-repo sweep (planner.ts:205, no try/catch) tofatal()→ one vanished star aborted the entire daily state reconciliation for every repo.It surfaced only in steady state: first discovery uses REST (
404 → null, already resilient); once a README path is cached, the run takes the un-guarded GraphQL fast path.Fix (at the seam — covers both the
ai-stateplanand the executor'splan)blobOidtreats a repository-levelNOT_FOUND(deleted / renamed away / turned private) as an absent README (null), matching the REST path's existing404 → null.isRepositoryNotFoundmatcheserrors[].type === 'NOT_FOUND'scoped to therepositoryfield (BLOB_OID_QUERYhas a single resolvable node).RetryCoordinator, so a transient fault (secondary rate limit, 5xx, GraphQL exec timeout) on any single probe retries with bounded backoff instead of crashing the batch. Auth/invalid errors still fail loudly.provenance/meta-rebasedo not use theknownPathfast path, so their behaviour is unchanged.@octokit/graphqlyieldsundefined.Verification
NOT_FOUNDtolerated without retry, transient retry-then-succeed, GraphQL exec-timeout retry, budget-exhaustion fails loud, terminal error propagates, empty-envelope falls through to REST, non-repositoryNOT_FOUNDis not swallowed. The regression gate was confirmed to FAIL on the pre-fix code (realTypeError).null(no crash); a live repo resolves normally.format:check/lint/typecheckclean.Reviewed
Diff reviewed by two independent fresh-context critics (Fable 5 + Opus). Fable caught — and this PR fixes — a
data === nullregression that missed the empty-envelopeundefinedcase. One residual boundary was consciously accepted: a persistent per-repo transient failure (≥ retry budget) or a systemic outage still fails the run loudly and retries next schedule, rather than silently degrading every repo to metadata; planner-level circuit-breaking is left as a follow-up.🤖 Generated with Claude Code
https://claude.ai/code/session_01YLoMBRETP2VWBxT1whUNc9