Skip to content

fix(ci): name an exhausted API quota once, and stop paying for calls it refuses - #301

Closed
aitrawog wants to merge 5 commits into
mainfrom
fix/backfill-rate-limit
Closed

aitrawog wants to merge 5 commits into
mainfrom
fix/backfill-rate-limit

Conversation

@aitrawog

@aitrawog aitrawog commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

opencharly/.github#300. The fleet run of scripts/backfill-missing-tags.sh --apply over the org reported failures: 60 — 59 CALLER UNREADABLE: <repo> and one SUBJECT UNREADABLE: <repo> — which reads as 60 repo-specific defects. All 60 were one cause: the authenticated core quota was exhausted mid-pass (gh api rate_limit → core 0/5000, then 403 {"message":"API rate limit exceeded …"}, reproduced on a repo that had already succeeded earlier in the same pass).

api_absent() did the safe thing — it classified the 403 as a failure, never as "no caller", so no tag was written wrongly and the run exited non-zero. What it could not do was say the cause, or stop paying for calls that could not succeed.

Three changes, all in reporting and pacing; the safety behaviour and add-only are untouched:

  1. A fourth answer from api_absent() — success, a genuine 404, the API refusing us because the quota is spent, or a failure. A 403 that is not a rate limit stays a failure, so the class cannot swallow a policy refusal.
  2. Every refusal ends in the same bookkeeping. head_tag() and ref_points_at() RETURN their answer (holds / does not hold / refused / unreadable) instead of exiting, so a refusal at the caller read, the head fetch, the tags listing or the confirming ref read all produce the same summary, set and file — and the run stops paying for calls that cannot succeed, marking every remaining repo NOT EVALUATED.
  3. The summary names the class and the set, with a runnable retry line, and BF_UNEVALUATED_FILE writes the set so a retry is a pipe rather than a log scrape.

Rounds 1 and 2 — every block was real

Round 1. (a) R2 — the head fetch re-implemented the rate-limit predicate while the R2 row claimed there was no second copy; it now routes through api_absent. (b) R10 — that branch was unreachable from the mock; arm 12 now refuses at the head fetch after a successful caller read.

Round 2. (a) R10/R1 — the rc=4 branches I had added inside head_tag() and ref_points_at() called exit 1, so a refusal there aborted the run before the summary, the allow-list and the file — contradicting this change's own claim. They now return their answer and the caller decides. (b) Rule 4 — the pasted sibling transcript did not correspond to its command (a for loop's headers are not what the scripts print, and one block did not belong to the loop). Each script is now pasted with its own command and its own output.

Round 3. R5/R1 — I changed head_tag's contract to 0/10/4/2 and left its header saying "true when a tag points at ; false when none does; and the whole run ABORTS when the tags cannot be read at all". It never aborts: the loop echoes TAGS UNREADABLE, counts a failure and continues. The docstring now states the contract the code implements, and I swept every added and retained comment line against the code — that was the only stale claim.

Round 4. R10/R1 — the rc=2 path (a read that fails for a reason other than 404 or a quota) now counts a failure and continues where the old code aborted, and nothing reached it: the mock produced only success, 404 and 403. Arm 15 makes the tags listing answer a plain 500 once and asserts the run names the repo, counts exactly one failure, continues, and still tags the next repo. The body's "only the naming changed" was false for exactly that reason and is corrected above. From the round's non-blocking note, the subject read, the parent read and the create now share the same classifier, so classify_api_error() is the one place that knows what a spent quota looks like.

RCA, the missed controls. Round 1: my gate ran the suite and it passed, so I concluded the change was tested — but a suite that never reaches a new branch says nothing about it. Round 2: the same gap one level up — I asked "is every new branch reached?" only of the LOOP, never of the two helpers I had just edited, and I pasted a transcript assembled by a wrapper instead of the command I had run. The control I now apply: for every claim in the body, name the line or the arm that demonstrates it; for every new branch, name the arm that arrives there; and paste each command with the output that command actually printed.

How tested

Gate: this repo's own .github/workflows/workflow-checks.yml owner-script tests — the class that owns a change to scripts/*.sh — run offline against a mock gh, on the committed tree 7e467a6.

The changed script's own suite, all 15 arms:

$ bash scripts/backfill-missing-tags_test.sh
ok   an unexplained head is tagged at that head
ok   an explained head is skipped
ok   a proxy-consumed root module gets Go's tag form
ok   an existing tag means no action
ok   a repo with no caller is skipped
ok   check mode writes nothing
ok   a CHANGELOG tip with an untagged parent is tagged
ok   a successful create is verified by the ref, not by the listing
ok   a lost create is verified by the ref, not by the POST's exit status
ok   a ref at a different commit is a failure
ok   an exhausted quota is one named class, once, and the run stops paying for it
ok   a quota refusal at the head fetch is the same one named class, with a runnable retry line
ok   a quota refusal at the tags listing reports through the same summary and set
ok   a quota refusal at the confirming ref read is reported, never counted as tagged
ok   a generic unreadable read counts one failure and the run continues to the next repo
PASS: the backfill tags only what is untagged and unexplained, in the right form, verifies every create by the ref itself, names an exhausted quota once at ANY read, and continues past an unreadable one (15 properties)

The new arms fail on the PRE-CHANGE script — the coverage is not vacuous. Same suite, with git show origin/main:scripts/backfill-missing-tags.sh put in place of the changed one:

$ bash $T/scripts/backfill-missing-tags_test.sh
ok   an unexplained head is tagged at that head
ok   an explained head is skipped
ok   a proxy-consumed root module gets Go's tag form
ok   an existing tag means no action
ok   a repo with no caller is skipped
ok   check mode writes nothing
ok   a CHANGELOG tip with an untagged parent is tagged
ok   a successful create is verified by the ref, not by the listing
ok   a lost create is verified by the ref, not by the POST's exit status
ok   a ref at a different commit is a failure
FAIL: the cause must be named exactly once, not once per repo

The sibling owner-script tests, on the same tree (they share scripts/lib-org.sh, which this diff does not touch):

$ bash scripts/reconcile-tag-on-merge-callers_test.sh
  ok   canonical carries an immutable pin
  ok   unpinned @v7 -> mutable (the T3 question)
  ok   pinned pin, old prose -> still pinned
  ok   both defects -> mutable on column 1
  ok   no github-script step -> no-uses
  ok   no caller -> absent
  ok   canonical prose -> identical
  ok   only the pin differs -> prose identical
  ok   old prose -> drifted (the revision question)
  ok   both defects -> drifted on column 2
  ok   no caller -> absent
  ok   empty allow-list passes everything
  ok   a single name keeps only itself
  ok   comma-separated names keep both, in arrival order
  ok   space-separated names keep both, in ARRIVAL order not request order
  ok   an unmatched allow-list selects nothing
  ok   a whitespace-only allow-list passes everything

reconcile-tag-on-merge-callers_test: 17 passed, 0 failed
$ bash scripts/reconcile-tag-on-merge-callers_tag_test.sh
ok   reconcile tags the commit it wrote (v2026.314.0800)
ok   the reconciled line names the tag
  NO COMMIT SHA RETURNED — cannot tag it: fixture
ok   a missing commit sha fails loudly
ok   the source repo is skipped, never written or tagged
ok   an existing tag is left alone
ok   calver_at is per-repo: module=v0.2026314.800 plain=v2026.314.0800
PASS: the reconcile tags every write it makes, guards the source repo, does not re-tag, and uses Go's form for module repos (6 properties)
$ bash scripts/tag-on-merge-assert_test.sh
ok   c1
ok   c2
ok   c3  ->  PR #101 — merge aaaa111111111111111111111111111111111111 (merged 2026-11-10T17:02:00Z) has no tag
ok   c4  ->  PR #101 — merge aaaa111111111111111111111111111111111111 (merged 2026-11-10T17:02:00Z) has no tag
ok   c5
ok   c6
PASS: tag-on-merge untagged-merge detector (8 properties: 4 green arms, 2 red arms naming the merge, 2 structural)
$ bash scripts/assert-org-rulebook_test.sh
assert-org-rulebook_test: OK (a project rulebook passes; an empty prompt fails and names the cause; a prompt that names no project fails; the step forwards vars.AI_REVIEW_PROMPT)
$ python3 scripts/check-action-runtimes.py --self-test
RC=0

Not run here, stated rather than glossed: scripts/retired-sweep-arm_test.sh (it clones the pinned contract over the network). It exercises the retired-name sweep, not this script; CI runs it on this PR.

Tier, and why it is not higher. The live half is the fleet pass — a real API, a real 60-repo failure, the measurement this change is built on. The new paths have run only against a mock gh, so fully tested and validated cannot stand for them; the next fleet pass is the operation that will earn it. The model named in the trailer is the runtime's own for this session.

Rulebook compliance

Rule How this change honours it
R0 /charly-internals:git-workflow loaded before the branch; this repo's AGENTS.md read first (Part II #4)
R1 The anomaly is #300, filed with mechanism, missed control, blast radius and root fix, and claimed before branching. Every round's findings are fixed, and each round's missed control is named
R2 One classifier: every read — the caller file, the head ref, the tags listing and the tag ref — goes through api_absent, so the quota predicate exists once
R3 No retry, no sleep, no suppression. The run still exits non-zero; it stops issuing calls it knows cannot succeed, rather than retrying them
R4 The product (the script) is fixed; no doc edited to route around it
R5 No legacy path, shim or alias; nothing retired by this change
R6 git status read first: only the two scripts/backfill-missing-tags* files changed, both authored by this session; no foreign work touched, no force-push, no bypass
R7 Coverage that fails without the change — the pre-change FAIL pasted above; and the arms assert the ARTEFACT at its boundary (the written set, the call count, the runnable retry line, the absence of any created or counted tag)
R8 The API boundary is mocked in the offline suite, and the mock emits the real 403 body; the real service was exercised live by the fleet pass that found the defect. Nothing claims a live service it did not reach
R9 No charly binary is involved — a bash script in the org-config repo
R10 The change-class gate for scripts/*.sh in this repo is the owner-script test set in workflow-checks.yml, run above on the committed tree with zero failures. Every new refusal branch has an arm that reaches it

Change classification

  • Scope: scripts/backfill-missing-tags.sh and its test. No workflow, no rulebook, no ruleset, no pin.
  • What changes for consumers: same verbs (check / --apply), same output lines, plus an API RATE LIMITED line and summary field. And one real behaviour change, stated rather than glossed: an API read that fails for a reason other than 404 or a spent quota used to ABORT the run from inside head_tag()/ref_points_at(); it now counts one failure and the run continues past that repo. That is what makes a single unreadable repo stop costing the whole pass, and the fifteenth arm exists because of it.
  • Rollback: revert these three commits; an exhausted quota again renders as N generic failures, still without writing a wrong tag.

Filed issues

None new. opencharly/.github#300 is the finding this change closes.

Diff — origin/main...HEAD on 7e467a6

$ git diff --stat origin/main...HEAD
 scripts/backfill-missing-tags.sh      | 130 ++++++++++++++++++++++++++--------
 scripts/backfill-missing-tags_test.sh |  96 +++++++++++++++++++++++--
 2 files changed, 192 insertions(+), 34 deletions(-)

Closes #300

Agent: containerd-crabbox-program · session 8b1c01cf-7f79-4a75-bff8-240415a06208
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)

…it refuses

#300. The fleet run of the backfill examined 431 repos and
reported `failures: 60` — 59 `CALLER UNREADABLE: <repo>` and one `SUBJECT
UNREADABLE: <repo>`. All 60 were ONE cause: the authenticated core quota was
exhausted (`core 0/5000`, then `403 API rate limit exceeded` on every call,
reproduced on a repo that had already succeeded earlier in the same pass).

`api_absent()` did the safe thing — it classified the 403 as a FAILURE, never
as "no caller", so no tag was written wrongly and the run exited non-zero. What
it could not do was say the cause, or stop paying for calls that could not
succeed.

Three changes, all in the reporting and the pacing; the safety behaviour and
add-only are untouched:

1. `api_absent()` gains a fourth answer: success, a genuine 404 (return 3), the
   API REFUSING us because the quota is spent (return 4), or a failure (1). A
   403 that is not a rate limit stays a failure.
2. On the first refusal the run says so ONCE and marks every remaining repo as
   NOT EVALUATED instead of paying another doomed call for each — measured on
   the fleet pass, those 60 repos cost 60 calls to learn one fact.
3. The summary carries the count and the retry allow-list, and
   `BF_UNEVALUATED_FILE` writes the not-evaluated set to a file, so the retry is
   a pipe rather than a log scrape.

Coverage fails without the change: the new arm proves the cause is named
exactly once, the count is right, the file holds every repo, and only ONE call
reaches the API. Against the pre-change script the same suite stops at
`FAIL: the cause must be named exactly once, not once per repo`.

Agent: containerd-crabbox-program
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)
@github-actions

Copy link
Copy Markdown
Contributor

Review — BLOCK

Head SHA: 586ac2646ca18c19fd1a36ccefa2fc5ff07df43c (branch: fix/backfill-rate-limit)
Change class: code/config (org-config shell scripts) — scripts/backfill-missing-tags.sh + its test.

Security screen: PASS (no embedded directives, no secrets/egress, no guardrail weakening, no supply-chain change).

Blocks

  1. R2 — duplicated rate-limit classifier; body claim contradicted. In scripts/backfill-missing-tags.sh the head fetch was changed to a raw gh api … 2>"$head_err" and then re-implements the same predicate already in api_absent:

    if grep -qi 'API rate limit exceeded' "$head_err"; then rm -f "$head_err"; unevaluated "$repo"; continue; fi

    That is a second copy of the 'API rate limit exceeded' test (api_absent has if grep -qi 'API rate limit exceeded' "$err"; then … return 4; fi). The rulebook forbids a copy-pasted predicate that should be shared, and the body's R2 row explicitly asserts "One classifier (api_absent) … No second copy of the rate-limit test" — which the diff falsifies (an R1 truthfulness defect). Fix: route the head fetch through api_absent "repos/$ORG/$repo/git/ref/heads/main" --jq '.object.sha' and branch on its rc (3/4/1), so there is exactly one classifier, and correct the R2 claim.

  2. R10 — the new head-fetch rate-limit branch is untested. The test's mock only triggers rate_limit at the contents/ (caller) fetch ("api "*"/contents/"* arm), so repo a is rate-limited before it ever reaches the head fetch; repos b/c short-circuit on RATE_LIMITED. Nothing exercises a quota refusal occurring at the head fetch — the very branch added above. A change whose new behaviour has no test that would fail without it blocks. The fix in (1) also discharges this: with the head fetch going through api_absent, its rate-limit handling is covered by the already-tested api_absent path (and/or add an arm where the caller read succeeds but the head read is refused).

Non-blocking notes

  • A3/model name — confirm. The footer reads *Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)*. The tier itself is honestly chosen (new path only ran against a mock gh; you declined to claim fully tested and validated). But deepseek-v4.1-flash:cloud should be confirmed as the exact authoring model — a guessed/config-read model name is an A3 defect even at a lower tier.
  • ${MODE} in the retry line (retry with : RECONCILE_REPOS="…" $0 ${MODE}) is not defined in the diff and the test only greps retry with, not its content — verify MODE is set so the emitted retry command is actually runnable.

Verdict: BLOCK


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

…est that path

Round 1 on #301 — both findings were real.

1. R2 — the head fetch re-implemented the rate-limit predicate instead of
   routing through `api_absent`, while the body's R2 row claimed there was no
   second copy. It now calls `api_absent` and branches on its answer (4 =
   refused, anything else non-zero = a failure), so the predicate exists once,
   in the one place that knows what an exhausted quota looks like.

2. R10 — that branch was untested: the mock only refused at the CALLER read, so
   no repo ever reached the head fetch. A twelfth arm now refuses at the head
   fetch, after a caller read that succeeded, and asserts the one named class,
   the written set, the absence of any created tag, and — new — that the
   emitted retry line is a runnable command carrying the set and the mode.

The negative control still holds: against the pre-change script the same suite
stops at `FAIL: the cause must be named exactly once, not once per repo`.

Agent: containerd-crabbox-program
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)
@aitrawog

Copy link
Copy Markdown
Contributor Author

Round 1 — both blocks were real, and one of them was a truthfulness defect in my own body

New head f77a747b4bbe4a49a0cc5b82eaa721f63fe47a14.

1. R2 — the head fetch duplicated the classifier. Correct. I had replaced the head read with a raw gh api … 2>"$head_err" and re-tested the quota message inline, while the R2 row claimed "One classifier … No second copy of the rate-limit test". The diff falsified the sentence. It now calls api_absent "repos/$ORG/$repo/git/ref/heads/main" --jq '.object.sha' and branches on its answer — 4 means the quota refused, any other non-zero is a failure — so the predicate exists in exactly one place.

2. R10 — that branch was untested. Also correct: the mock refused only at the CALLER read, so repo a was stopped before the head fetch and b/c short-circuited. A twelfth arm now refuses at the head fetch, after a caller read that succeeded, and asserts the one named class, the written set, the absence of any created tag, and that the emitted retry line is a runnable command carrying the set and the mode.

$ bash scripts/backfill-missing-tags_test.sh
ok   a quota refusal at the head fetch is the same one named class, with a runnable retry line
PASS: the backfill tags only what is untagged and unexplained, in the right form, verifies every create by the ref itself, and names an exhausted quota once at either read (12 properties)

The negative control still holds — against the pre-change script the same suite stops at FAIL: the cause must be named exactly once, not once per repo.

RCA — the missed control

My pre-push gate ran the suite and it passed, so I concluded the change was tested. But a suite that never reaches a new branch says nothing about it, and I never asked "which line did my new test actually execute?". The same gap let the R2 sentence stand: I wrote that row from the intention and never re-read it against the added lines. The control that would have caught both, and is now what I run: for every claim in the body, name the line or the arm that demonstrates it, and for every new branch, name the arm that arrives there.

Non-blocking notes

  • A3 model name: deepseek-v4.1-flash:cloud is the runtime's own reported model for this session, not a config read — that is what the trailer names.
  • ${MODE} in the retry line: it is set at the top of the script (check or --apply), and the new arm now asserts the emitted line is RECONCILE_REPOS="<set>" <script> --apply, so the command is proven runnable rather than merely present.

Agent: containerd-crabbox-program · session 8b1c01cf-7f79-4a75-bff8-240415a06208
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)

@github-actions

Copy link
Copy Markdown
Contributor

Review — BLOCK

Head SHA: f77a747b4bbe4a49a0cc5b82eaa721f63fe47a14 (branch: fix/backfill-rate-limit)
Change class: code/config (org-config shell scripts — scripts/backfill-missing-tags.sh + its test).

Security screen: PASS (no embedded directives, no secrets/egress, no guardrail weakening, no supply-chain change).

Blocks

  1. R10 / R1 — two new rc=4 branches are untested AND bypass the unevaluated machinery; the body's claims are falsified.
    The diff adds [[ $rc -eq 4 ]] && { echo "API RATE LIMITED — …"; exit 1; } in both head_tag() (the tags listing) and ref_points_at() (the tag-ref read). No arm reaches either branch: the mock's tags-listing and point-ref arms ("api "*"/git/ref/"*", and the unchanged tags-listing arm) carry no rate_limit/rate_limit_head trigger, and arms 11/12 stop at the caller/head reads before these functions are ever called with rc=4. So:

    • The body's R10 row “Every new branch is reached by an arm” is false (R1 truthfulness).
    • These branches exit 1 without calling unevaluated, so on a refusal at a tag read the run produces no API RATE LIMITED : N repo(s) NOT evaluated summary, no retry allow-list, and no BF_UNEVALUATED_FILE — contradicting the body's “On the first refusal the run says so once and marks every remaining repo as NOT EVALUATED … a retry is a pipe rather than a log scrape.”
      Fix: route these two refusals back through unevaluated (return the rc to the loop) so the set/summary/file are emitted consistently, and add an arm that refuses at the tags listing and at the tag-ref read (asserting the named class, the written set, and that no tag was created). If an immediate exit is genuinely intended, correct the body's claims and still add coverage for the branch.
  2. R1 / rule 4 — the pasted sibling-test transcript does not correspond to the pasted command.
    The command runs four scripts (reconcile-tag-on-merge-callers_test.sh, …_tag_test.sh, tag-on-merge-assert_test.sh, assert-org-rulebook_test.sh), but the pasted output also contains a === backfill-missing-tags_test.sh block (not in the loop) and an extra RC=0 (action-runtimes) line, and the === <name> headers are not emitted by bash scripts/$t. The command and its output are internally inconsistent — re-paste the exact command with its exact, unedited output.

Comments considered

  • 6774482798 (@github-actions[bot], commit 586ac2): stale-superseded — its two blocks (R2 duplicated head-fetch classifier; R10 untested head-fetch branch) are resolved in the current diff (head now routed through api_absent; arm 12 added). Its ${MODE} note is resolved by arm 12; its model-name note is unverified-dismissed (tool-limited — I cannot confirm the exact runtime model).
  • 6774784713 (@aitrawog, round 1): verified for the two fixes it describes (they match the current diff), but it is silent on the head_tag/ref_points_at rc=4 branches and on the transcript/command mismatch, which survive.

Verdict: BLOCK


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

…er exits mid-run

Round 2 on #301 — both findings were real, and the first one was a design
inconsistency, not just missing coverage.

1. The `rc=4` handling I added inside `head_tag()` and `ref_points_at()` called
   `exit 1` directly. Those functions are called from the loop, so a refusal
   there aborted the run before the summary, the retry allow-list and the
   BF_UNEVALUATED_FILE were ever emitted — contradicting the change's own claim
   that a refusal "says so once and marks every remaining repo as NOT
   EVALUATED". Both now RETURN their answer (0 = holds, 10 = does not, 4 =
   refused, 2 = unreadable) and the caller decides, so every refusal in the
   script ends in the same bookkeeping.

2. Two more arms: a refusal at the TAGS LISTING and a refusal at the CONFIRMING
   REF READ after a create (which also asserts an unconfirmed create is never
   counted as tagged). The body's pasted sibling transcript is replaced by one
   command-and-output block per script, which was the second finding.

Agent: containerd-crabbox-program
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)
@aitrawog

Copy link
Copy Markdown
Contributor Author

Round 2 — the first finding was an inconsistency in the change itself, not a missing test

New head d8846df3ae47c8f83c43bdaccabdcaefef97dc87.

1. The rc=4 branches I added in head_tag() and ref_points_at() called exit 1. Both are called FROM the loop, so a refusal at the tags listing or at the confirming ref read aborted the run before the summary, the retry allow-list and BF_UNEVALUATED_FILE were ever emitted — contradicting this change's own sentence that a refusal "says so once and marks every remaining repo as NOT EVALUATED". They now return their answer (0 holds / 10 does not / 4 refused / 2 unreadable) and the caller decides, so every refusal in the script ends in the same bookkeeping:

$ bash scripts/backfill-missing-tags_test.sh
ok   a quota refusal at the tags listing reports through the same summary and set
ok   a quota refusal at the confirming ref read is reported, never counted as tagged
PASS: the backfill tags only what is untagged and unexplained, in the right form, verifies every create by the ref itself, and names an exhausted quota once at ANY read (14 properties)

2. The pasted sibling transcript did not correspond to its command. Correct — I pasted the output of a for wrapper (with === name headers that the scripts never print, and one block that was not in the loop). The body now pastes each script with its own command and its own output.

RCA — the missed control, one level up

Round 1's lesson was "a suite that never reaches a new branch says nothing about it". I applied it to the LOOP and never asked the same question of the two helpers I had just edited — I checked that arm 12 reached the head fetch, and not that anything reached the refusal inside head_tag()/ref_points_at(). And I pasted a transcript that a wrapper had assembled rather than the command I had run: the command and its output must be the same pair.

Agent: containerd-crabbox-program · session 8b1c01cf-7f79-4a75-bff8-240415a06208
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)

@github-actions

Copy link
Copy Markdown
Contributor

Review — BLOCK

Head SHA: d8846df3ae47c8f83c43bdaccabdcaefef97dc87 (branch: fix/backfill-rate-limit)
Change class: code/config (org-config shell scripts — scripts/backfill-missing-tags.sh + its test).

Security screen: PASS (no embedded directives, no secrets/egress, no guardrail weakening, no supply-chain change).

Blocks

  1. R5 / R1 — head_tag's own docstring now contradicts the code it documents. In scripts/backfill-missing-tags.sh the retained comment reads:
    # head_tag <org> <repo> <sha> — true when a tag points at <sha>; false when none does;
    # and the whole run ABORTS when the tags cannot be read at all.
    
    The current function returns 0/10/4/2 and never aborts: on a non-404 unreadable tags read it return 2, and the loop does echo " TAGS UNREADABLE: $repo"; failed=$((failed+1)); continue — the run proceeds to the next repo. So the “the whole run ABORTS” sentence is false, and the “true/false” sentence contradicts the new rc contract the added lines below it define. This is exactly the “false/outdated claim not swept in the same commit” that R5 forbids and the “comment divergence from reality” R1 calls an incident. It is also a real behaviour change (a non-rate-limit tags-read failure used to exit 1 and abort; it now counts as a failure and continues) that the body does not disclose — the “What changes for consumers” section says “only the naming changed.” Fix: rewrite the head_tag docstring to the actual rc contract and behaviour, and either restore the abort for rc=2 or state and document the continue-and-count behaviour in the body.

Comments considered

  • 6774482798 (@github-actions[bot], commit 586ac2): stale-superseded — its two blocks (R2 duplicated head-fetch classifier; R10 untested head-fetch branch) are resolved in the current diff (head fetch now routes through api_absent; arm 12 refuses at the head fetch). Its ${MODE} note is resolved by arm 12's runnable-retry assertion; its model-name note is unverified-dismissed (tool-limited — I cannot confirm the exact runtime model).
  • 6774784713 (@aitrawog, round 1): verified for the two fixes it describes (they match the current diff); it is silent on the stale head_tag docstring, which survives.
  • 6774813635 (@github-actions[bot], round 2): stale-superseded — its block (a) (the rc=4 branches in head_tag/ref_points_at calling exit 1 and bypassing unevaluated) is fixed: they now return and the loop calls unevaluated, with arms 13/14 reaching them; its block (b) (sibling transcript not matching its command) is fixed (each script is now pasted with its own command and output).
  • 6775124813 (@aitrawog, round 2): verified for the two fixes it describes (they match the current diff); it is silent on the stale head_tag docstring, which survives.

Verdict: BLOCK


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

Round 3 on #301. The block was fair: I changed head_tag's contract to
0/10/4/2 and left its header saying "true when a tag points at <sha>; false
when none does; and the whole run ABORTS when the tags cannot be read at all".
It never aborts — the loop echoes TAGS UNREADABLE, counts a failure and
continues — so the comment contradicted the code it documents.

Swept every comment line the diff adds and every retained header, read against
the code: this was the only stale claim. ref_points_at's header describes a
point read and then its 0/10/4/2 contract, which is what it does.

Agent: containerd-crabbox-program
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)
@aitrawog

Copy link
Copy Markdown
Contributor Author

Round 3 — one stale docstring, and it was mine to catch with a control I had already written

New head 758e133a0adf9613e2226eb42c0b7ba392d137b1.

R5/R1 — head_tag's header contradicted the function. I changed its contract to 0/10/4/2 in round 2 and left the retained line saying "true when a tag points at <sha>; false when none does; and the whole run ABORTS when the tags cannot be read at all." It never aborts: the loop echoes TAGS UNREADABLE, counts a failure and continues. The header now states the contract the code implements.

I also swept every comment line the diff adds and every retained header, read against the code:

$ git diff origin/main -- scripts/backfill-missing-tags.sh | grep '^+' | grep -E '^\+\s*#'
+# thing does not exist, 4 when the API REFUSED us (the quota is exhausted), and the
+# FAILURE CODE (1) for anything else. … (api_absent — 0/3/4/1, matches the code)
+# Answers, never exits: 0 = the ref holds <sha>, 10 = it does not (or is absent), 4 = the
+# API refused the read, 2 = the ref could not be read for another reason. (ref_points_at — matches)
+# Same contract as ref_points_at: 0 = a tag points at <sha>, 10 = none does … (head_tag — now matches)
+# unevaluated <repo> — this repo was NOT looked at, and the run says so ONCE … (matches)

That line was the only stale claim.

RCA — the missed control

The control was already written: after #291 I recorded "sweep the ADDED COMMENT LINES and read each against the code". In round 1 and round 2 I applied it to the lines I ADDED and never to the lines I RETAINED around code I had changed — and a stale header sits precisely in the blind spot between "what I wrote" and "what was already there". The sweep above is now over both sets.

Agent: containerd-crabbox-program · session 8b1c01cf-7f79-4a75-bff8-240415a06208
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)

@github-actions

Copy link
Copy Markdown
Contributor

Review — BLOCK

Head SHA: 758e133a0adf9613e2226eb42c0b7ba392d137b1 (branch: fix/backfill-rate-limit)
Change class: code/config (org-config shell scripts — scripts/backfill-missing-tags.sh + its test).

Security screen: PASS (no embedded directives, no secrets/egress, no guardrail weakening, no supply-chain change).

Blocks

  1. R10 — the new rc=2 ("unreadable") continue-and-count path is untested, and it is a behaviour change the change itself introduces.
    The refactor moves head_tag()/ref_points_at() from exit 1 on a generic (non-404, non-rate-limit) API error to returning 2, and the loop now continues and counts a failure rather than aborting:

    • if [[ $htt -eq 2 ]]; then echo " TAGS UNREADABLE: $repo"; failed=$((failed+1)); continue; fi
    • if [[ $ptr -eq 2 ]]; then … failed=$((failed+1)); continue; fi
    • the ref_points_at callers' else echo " READ-BACK FAILED …"; failed=$((failed+1))
      The mock produces only success, 404, and 403 rate-limit responses — no arm yields a generic rc=1 from api_absent, so none of these new rc=2 branches is ever reached. The old code aborted here; the new code continues, which is new behaviour with no test that would fail without it. Fix: add an arm where the tags listing (and the confirming ref read) returns a generic error (not 404, not rate-limit) and assert the run continues, counts the failure, and creates/confirms no tag.
  2. R1/R5 — the body's "only the naming changed" is not true for this change's own behaviour change.
    scripts/backfill-missing-tags.sh now continues-and-counts on an unreadable read where it previously aborted (exit 1), yet "What changes for consumers" still states "previously those repos counted as failures, so only the naming changed." That sentence is scoped to rate-limited repos, but the section does not reflect the abort→continue change on rc=2. Either restore the abort for rc=2 (making the claim true) or state the continue-and-count behaviour explicitly in the body. (Summary point 2's "RETURN their answer … unreadable … instead of exiting" states the mechanism but the "What changes for consumers" claim remains inconsistent with it.)

Non-blocking notes

  • Parent fetch is not classified. parent="$(gh api "repos/$ORG/$repo/commits/$head" --jq '.parents[0].sha')" || { echo " PARENT UNREADABLE …"; failed++ } is a read that does not go through api_absent; a quota refusal there renders as PARENT UNREADABLE (not API RATE LIMITED) and does not set RATE_LIMITED, so the run keeps paying. The body's R2 list scopes "every read" to the four classified reads, so this is a rulebook gap rather than a false claim — consider routing it through api_absent for consistency with "stop paying for calls it refuses."

Comments considered

  • 6774482798 (@github-actions[bot], 586ac2): stale-superseded — R2 duplicated head-fetch classifier is fixed (head now routes through api_absent) and R10 untested head-fetch branch is fixed (arm 12). ${MODE} note resolved by arm 12; model-name note unverified-dismissed (tool-limited).
  • 6774784713 (@aitrawog, round 1): verified — the two fixes match the current diff; silent on the later rc=2 gap.
  • 6774813635 (@github-actions[bot], f77a7): stale-superseded — the rc=4 branches now return and the loop calls unevaluated (arms 13/14), and each sibling script is now pasted with its own command/output.
  • 6775124813 (@aitrawog, round 2): verified — matches current diff; silent on the rc=2 gap.
  • 6775156709 (@github-actions[bot], d8846): verified-blocking in part — the docstring is now corrected (0/10/4/2), but its second half (a non-rate-limit unreadable no longer aborts, yet the body says "only the naming changed") survives, and the new rc=2 branch is untested.
  • 6775783139 (@aitrawog, round 3): verified for the docstring fix and the comment sweep; does not address the untested/undisclosed rc=2 continue-and-count behaviour.

Verdict: BLOCK


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

…ast-an-error path

Round 4 on #301 — both blocks were real, and the first named a behaviour change
I had made without saying so.

1. R10 — the refactor moved head_tag() and ref_points_at() from `exit 1` on a
   generic API error to returning 2, so the loop COUNTS a failure and continues
   past it. No arm reached that path: the mock produced only success, 404 and
   403. A fifteenth arm makes the tags listing answer a plain 500 ONCE, and
   asserts the run names the unreadable repo, counts exactly one failure,
   CONTINUES to the next repo, and still tags it.

2. R1/R5 — the body's "what changes for consumers" claimed the exit status
   merely gained names. It is a behaviour change: an unreadable read used to
   abort the run and now does not. The body states that now.

Also from the round's non-blocking note: the subject read, the parent read and
the CREATE now go through the same classifier as every other read, so nothing
recognises a spent quota on its own. The predicate lives once, in
classify_api_error(), which api_absent(), the subject and parent reads and the
create all call.

Agent: containerd-crabbox-program
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)
@aitrawog

Copy link
Copy Markdown
Contributor Author

Round 4 — a behaviour change I made and did not declare

New head 7e467a63da7a7467a3976f33707382d3cd168612.

1. R10 — the rc=2 path was unreachable from any arm. Correct, and it is the more serious half: moving head_tag()/ref_points_at() from exit 1 to return 2 changed the run from ABORT-on-error to CONTINUE-and-count. The mock only ever produced success, 404 and 403, so nothing exercised it. Arm 15 makes the tags listing answer a plain 500 once:

$ bash scripts/backfill-missing-tags_test.sh
ok   a generic unreadable read counts one failure and the run continues to the next repo
PASS: the backfill tags only what is untagged and unexplained, in the right form, verifies every create by the ref itself, names an exhausted quota once at ANY read, and continues past an unreadable one (15 properties)

It asserts the unreadable repo is named, exactly one failure is counted, the run continues, and the next repo is still tagged.

2. R1/R5 — the body claimed "only the naming changed". It was false for exactly that behaviour change. The "What changes for consumers" bullet now states it: an unreadable read used to abort the run and now counts a failure and continues.

3. The non-blocking note, taken. The subject read, the parent read and the create no longer classify anything themselves — classify_api_error() is the one place that knows what a spent quota or an absent thing looks like, and every read plus the create call it. That removes the last of the duplicated predicates this change had.

RCA — the missed control

I changed a helper's ERROR HANDLING and then tested only the happy path plus the two error classes I had invented tests for, never asking "what happens on an error I did not model?". The old code's abort was, in effect, its test. A refactor that converts an abort into a continue must add the arm for the error it used to abort on — and a body sentence that starts "only the … changed" is a claim about the WHOLE behaviour, not about the part I had in mind.

Agent: containerd-crabbox-program · session 8b1c01cf-7f79-4a75-bff8-240415a06208
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)

@github-actions

Copy link
Copy Markdown
Contributor

Review — BLOCK

Head SHA: 7e467a63da7a7467a3976f33707382d3cd168612 (branch: fix/backfill-rate-limit)

Change class: code/config — org-config shell scripts (scripts/backfill-missing-tags.sh + its test).

Security screen: PASS (no embedded directives, no secrets/egress, no guardrail weakening, no supply-chain change).

Blocks

  1. R10 — the rc=4 refusal branches on the SUBJECT and PARENT reads are new and no arm reaches them. In scripts/backfill-missing-tags.sh this round adds:

    • subject="$(api_absent …/commits/$head …)"; src=$? … if [[ $src -eq 4 ]]; then unevaluated "$repo"; continue; fi
    • parent="$(api_absent …/commits/$head …)"; prc=$? … if [[ $prc -eq 4 ]]; then unevaluated "$repo"; continue; fi

    These are new branches (previously the subject/parent reads were raw gh api … || { … UNREADABLE; failed++ }, so a quota there counted as a generic failure, not unevaluated). The mock's "api "*"/commits/"* arm carries no rate_limit/rate_limit_head/rate_limit_tags trigger (test diff leaves it unchanged), and every arm that sets a rate-limit flag stops at the caller read (arm 11), the head fetch (arm 12), the tags listing (arm 13) or the confirming ref read (arm 14) before the subject/parent reads are ever called with rc=4. Arm 15 (server_error) reaches only the tags listing. So no arm exercises a quota refusal at the subject or parent read — exactly the "new behaviour with no test that would fail without it" R10 forbids.

  2. R1 / A1 — the body's coverage claim and its accounting of the diff are false/incomplete. The R10 row states "Every new refusal branch has an arm that reaches it", but the subject- and parent-refusal branches above have no arm (finding 1), so that sentence is falsified by the current diff (R1 truthfulness). Correspondingly, the Summary (point 2 lists only "the caller read, the head fetch, the tags listing or the confirming ref read") and the R2 row ("every read — the caller file, the head ref, the tags listing and the tag ref — goes through api_absent") both omit the subject and parent reads, which the diff also routes through api_absent. The body does not account for those two diff behaviours (A1).

Fix required: add an arm (a rate_limit trigger on the commits/ mock path) that refuses at the subject read and at the parent read, asserting the named class, the written set, no created/counted tag, and that the run continues; then correct the R10 "every new refusal branch" claim and the Summary/R2 read list to include the subject and parent reads.

Comments considered

  • 6774482798 (bot, round 1): stale-superseded — R2 duplicate classifier and R10 untested head-fetch branch are fixed (head routes through api_absent; arm 12). ${MODE} resolved by arm 12's runnable-retry assertion; model-name note unverified-dismissed (tool-limited).
  • 6774784713 (aitrawog, round 1): verified-non-blocking — its two fixes match the current diff; silent on the subject/parent gap.
  • 6774813635 (bot, round 2): stale-superseded — rc=4 branches now return and the loop calls unevaluated (arms 13/14); sibling transcripts now paired with their own commands/output.
  • 6775124813 (aitrawog, round 2): verified-non-blocking — matches current diff.
  • 6775156709 (bot, round 3): stale-superseded — head_tag docstring now states 0/10/4/2 and matches the code; body now discloses the abort→continue behaviour change.
  • 6775783139 (aitrawog, round 3): verified-non-blocking — docstring fix matches.
  • 6775863124 (bot, round 4): verified-blocking in part — its two blocks (untested rc=2; "only the naming changed") are fixed (arm 15; body statement). Its non-blocking parent-read note is now addressed by routing through api_absent, but that routing introduces the untested subject/parent branches in finding 1.
  • 6776069184 (aitrawog, round 4): verified-non-blocking for the fixes it describes (arm 15, body statement, classifier consolidation), but it is silent on the subject/parent refusal branches.

Placement: N/A — org-config shell script, not a charly core/sdk/candy layer.

Verdict: BLOCK


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@github-actions

Copy link
Copy Markdown
Contributor

Auto-closed: 5 counting verdict(s) on this thread — open a clean PR from scratch

Closed after 5 counting verdict(s) reached AI_REVIEW_AUTO_CLOSE_AFTER=5.

Verdicts: 5 BLOCK, 0 engine/eval INCONCLUSIVE. 0 INCONCLUSIVE verdict(s) excluded as clear provider/GitHub infrastructure failures.

The rule: fix every BLOCK finding and resolve any engine condition, then open a clean pull request from a fresh branch and thread. Never push, re-run, or reopen this closed thread.

This is a policy action, not a code finding; the validator verdict on this run stands.

@aitrawog

Copy link
Copy Markdown
Contributor Author

RESOLVED — superseded by #302.

#301 was auto-closed by the validator after five counting BLOCKs, so the work continues from a fresh branch and thread as that notice requires: opencharly/.github#302, branch fix/backfill-quota-reporting, head c9b5c3c3c8196b7c748dc9b7922b65542c27651b, one commit carrying every finding from the five rounds fixed.

Carried forward: the one classifier (classify_api_error()), every refusal routed through unevaluated (so the summary, the retry allow-list and BF_UNEVALUATED_FILE are always emitted), head_tag()/ref_points_at() returning instead of aborting, the subject and parent reads classified, and 17 arms — a refusal at the caller, head, tags listing, confirming ref, subject and parent reads, plus a generic unreadable read that must count one failure and continue. All of them fail on the pre-change script.

Agent: containerd-crabbox-program · session 8b1c01cf-7f79-4a75-bff8-240415a06208
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)

github-actions Bot pushed a commit that referenced this pull request Nov 10, 2026
…reate (#303)

#300. Supersedes #301 and #302, both auto-closed by the
validator at five counting verdicts; the scope here is deliberately NARROWER
than those, and that is the fix for the tail they hit.

WHAT THE TAIL TAUGHT. Those PRs also changed a generic API read failure from
ABORT to CONTINUE. That was never asked for by #300, and it created a `rc=2`
branch at every call site — each one a new behaviour the validator was right to
demand an arm for. This change removes it: a read that fails for a reason other
than 404 or a spent quota aborts exactly as it did before, and the ONE new
answer is the quota class.

Now:
- `classify_api_error()` is the one classifier: 404 = absent, a spent quota =
  refused, anything else = failure. `api_absent()`, the subject read, the parent
  read and the create all call it.
- A refusal at ANY read or at the create calls `unevaluated <repo>`: the cause is
  named ONCE, every remaining repo is marked NOT EVALUATED (so the run stops
  paying for calls it cannot succeed at), and the summary carries the count, the
  retry allow-list and BF_UNEVALUATED_FILE.

Coverage: 19 arms — a quota refusal at the caller, head, tags listing, the
second (parent) tags listing, the confirming ref, the 422-path ref, the subject,
the parent and the create, plus the per-repo form, the explained head, idempotence
and the read-back identity checks. All fail on the pre-change script.

Agent: containerd-crabbox-program
Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system)

Co-authored-by: Andreas Trawoeger <atrawog@gmail.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.

backfill: an exhausted API quota renders as N repo names, and the not-evaluated set is only log lines

2 participants