fix(aic-guardrail): widen fallback search window and add in-loop rate-limit backoff#43447
Conversation
…heck - Increase MAX_RUNS_TO_SEARCH from 5 to 30 in restore_aic_usage_cache_fallback.cjs so pull_request branches reliably find the aic-usage-cache artifact (AC2). - Add RATE_LIMIT_RECHECK_INTERVAL=10 constant and in-loop rate-limit re-check in check_daily_aic_workflow_guardrail.cjs: after every 10 consumed API operations (5 cache-miss runs at ESTIMATED_API_OPERATIONS_PER_RUN=2), re-check remaining quota and break early if at or below RATE_LIMIT_RESERVE. This catches concurrent activations collectively exhausting the shared token budget in real-time, not just at the upfront snapshot (AC3, AC4). - Add test verifying the in-loop backoff stops inspection after exactly 5 runs when a mid-loop rate-limit snapshot returns remaining below the reserve." Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
|
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. |
|
✅ Test Quality Sentinel completed test quality analysis. |
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
There was a problem hiding this comment.
Pull request overview
This PR updates the AIC (AI Credits) guardrail and its supporting cache-fallback behavior to reduce GitHub API rate-limit exhaustion under high concurrency by (1) searching further back for cache artifacts and (2) periodically re-checking live core rate-limit headroom inside the inspection loop and stopping early when the shared reserve is at risk.
Changes:
- Widen
restore_aic_usage_cache_fallback.cjsartifact fallback search window from 5 → 30 recent runs to improve cache hit rate, especially forpull_requestbranches. - Add in-loop rate-limit re-checking to
check_daily_aic_workflow_guardrail.cjs(every 10 estimated cache-miss API ops) to stop inspection whenremaining <= RATE_LIMIT_RESERVE. - Add a new unit test covering early loop termination triggered by the in-loop rate-limit re-check.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/data/action_pins.json | Large JSON reformatting change (appears non-functional); flagged as out-of-scope vs PR description. |
| pkg/actionpins/data/action_pins.json | Large JSON reformatting change (appears non-functional); flagged as out-of-scope vs PR description. |
| actions/setup/js/restore_aic_usage_cache_fallback.cjs | Increases fallback search window for locating the aic-usage-cache artifact. |
| actions/setup/js/check_daily_aic_workflow_guardrail.cjs | Adds periodic in-loop core rate-limit snapshot checks and early exit when headroom is exhausted. |
| actions/setup/js/check_daily_aic_workflow_guardrail.test.cjs | Adds test asserting the inspection loop stops after 5 cache-miss runs when mid-loop rate limit drops below reserve. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 3/5 changed files
- Comments generated: 2
- Review effort level: Low
| { | ||
| "entries": { | ||
| "actions-ecosystem/action-add-labels@v1.1.3": { | ||
| "repo": "actions-ecosystem/action-add-labels", | ||
| "version": "v1.1.3", | ||
| "sha": "c96b68fec76a0987cd93957189e9abd0b9a72ff1", | ||
| "inputs": { | ||
| "github_token": { | ||
| "description": "A GitHub token.", | ||
| "default": "${{ github.token }}" | ||
| }, | ||
| "labels": { | ||
| "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.", | ||
| "required": true | ||
| }, | ||
| "number": { | ||
| "description": "The number of the issue or pull request." | ||
| }, | ||
| "repo": { | ||
| "description": "The owner and repository name. e.g.) Codertocat/Hello-World", | ||
| "default": "${{ github.repository }}" | ||
| } | ||
| }, | ||
| "action_description": "Add labels to an issue or a pull request." | ||
| }, | ||
| "actions/cache/restore@v6.1.0": { | ||
| "repo": "actions/cache/restore", | ||
| "version": "v6.1.0", | ||
| "sha": "55cc8345863c7cc4c66a329aec7e433d2d1c52a9" | ||
| }, | ||
| "actions/cache/save@v6.1.0": { | ||
| "repo": "actions/cache/save", | ||
| "version": "v6.1.0", | ||
| "sha": "55cc8345863c7cc4c66a329aec7e433d2d1c52a9" | ||
| }, | ||
| "actions/cache@v6.1.0": { | ||
| "repo": "actions/cache", | ||
| "version": "v6.1.0", | ||
| "sha": "55cc8345863c7cc4c66a329aec7e433d2d1c52a9" | ||
| }, | ||
| "actions/checkout@v7.0.0": { | ||
| "repo": "actions/checkout", | ||
| "version": "v7.0.0", | ||
| "sha": "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" | ||
| }, | ||
| "actions/create-github-app-token@v3.2.0": { | ||
| "repo": "actions/create-github-app-token", | ||
| "version": "v3.2.0", | ||
| "sha": "bcd2ba49218906704ab6c1aa796996da409d3eb1" | ||
| }, | ||
| "actions/download-artifact@v8.0.1": { | ||
| "repo": "actions/download-artifact", | ||
| "version": "v8.0.1", | ||
| "sha": "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" | ||
| }, | ||
| "actions/github-script@v9.0.0": { | ||
| "repo": "actions/github-script", | ||
| "version": "v9.0.0", | ||
| "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" | ||
| }, | ||
| "actions/setup-dotnet@v5.4.0": { | ||
| "repo": "actions/setup-dotnet", | ||
| "version": "v5.4.0", | ||
| "sha": "26b0ec14cb23fa6904739307f278c14f94c95bf1" | ||
| }, | ||
| "actions/setup-go@v6.5.0": { | ||
| "repo": "actions/setup-go", | ||
| "version": "v6.5.0", | ||
| "sha": "924ae3a1cded613372ab5595356fb5720e22ba16" | ||
| }, | ||
| "actions/setup-java@v5.4.0": { | ||
| "repo": "actions/setup-java", | ||
| "version": "v5.4.0", | ||
| "sha": "1bcf9fb12cf4aa7d266a90ae39939e61372fe520" | ||
| }, | ||
| "actions/setup-node@v6.4.0": { | ||
| "repo": "actions/setup-node", | ||
| "version": "v6.4.0", | ||
| "sha": "48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e" | ||
| }, | ||
| "actions/setup-python@v6.3.0": { | ||
| "repo": "actions/setup-python", | ||
| "version": "v6.3.0", | ||
| "sha": "ece7cb06caefa5fff74198d8649806c4678c61a1" | ||
| }, | ||
| "actions/upload-artifact@v7.0.1": { | ||
| "repo": "actions/upload-artifact", | ||
| "version": "v7.0.1", | ||
| "sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" | ||
| }, | ||
| "anchore/sbom-action@v0.24.0": { | ||
| "repo": "anchore/sbom-action", | ||
| "version": "v0.24.0", | ||
| "sha": "e22c389904149dbc22b58101806040fa8d37a610" | ||
| }, | ||
| "astral-sh/setup-uv@v8.2.0": { | ||
| "repo": "astral-sh/setup-uv", | ||
| "version": "v8.2.0", | ||
| "sha": "fac544c07dec837d0ccb6301d7b5580bf5edae39" | ||
| }, | ||
| "denoland/setup-deno@v2.0.4": { | ||
| "repo": "denoland/setup-deno", | ||
| "version": "v2.0.4", | ||
| "sha": "667a34cdef165d8d2b2e98dde39547c9daac7282" | ||
| }, | ||
| "docker/build-push-action@v7.2.0": { | ||
| "repo": "docker/build-push-action", | ||
| "version": "v7.2.0", | ||
| "sha": "f9f3042f7e2789586610d6e8b85c8f03e5195baf", | ||
| "released_at": "2026-05-21T15:23:58Z" | ||
| }, | ||
| "docker/login-action@v4.2.0": { | ||
| "repo": "docker/login-action", | ||
| "version": "v4.2.0", | ||
| "sha": "650006c6eb7dba73a995cc03b0b2d7f5ca915bee", | ||
| "released_at": "2026-05-22T11:55:00Z" | ||
| }, | ||
| "docker/metadata-action@v6.1.0": { | ||
| "repo": "docker/metadata-action", | ||
| "version": "v6.1.0", | ||
| "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" | ||
| }, | ||
| "docker/setup-buildx-action@v4.1.0": { | ||
| "repo": "docker/setup-buildx-action", | ||
| "version": "v4.1.0", | ||
| "sha": "d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5" | ||
| }, | ||
| "erlef/setup-beam@v1.24.0": { | ||
| "repo": "erlef/setup-beam", | ||
| "version": "v1.24.0", | ||
| "sha": "fc68ffb90438ef2936bbb3251622353b3dcb2f93" | ||
| }, | ||
| "github/codeql-action/upload-sarif@v4.36.2": { | ||
| "repo": "github/codeql-action/upload-sarif", | ||
| "version": "v4.36.2", | ||
| "sha": "8aad20d150bbac5944a9f9d289da16a4b0d87c1e" | ||
| }, | ||
| "github/stale-repos@v9.0.15": { | ||
| "repo": "github/stale-repos", | ||
| "version": "v9.0.15", | ||
| "sha": "a0db8bfe2318ed6ddbd01e95d7633803f99f77e9" | ||
| }, | ||
| "haskell-actions/setup@v2.11.0": { | ||
| "repo": "haskell-actions/setup", | ||
| "version": "v2.11.0", | ||
| "sha": "cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553" | ||
| }, | ||
| "oven-sh/setup-bun@v2.2.0": { | ||
| "repo": "oven-sh/setup-bun", | ||
| "version": "v2.2.0", | ||
| "sha": "0c5077e51419868618aeaa5fe8019c62421857d6" | ||
| }, | ||
| "ruby/setup-ruby@v1.314.0": { | ||
| "repo": "ruby/setup-ruby", | ||
| "version": "v1.314.0", | ||
| "sha": "9eb537ca036ebaed86729dcb9309076e4c5c3b74" | ||
| }, | ||
| "safedep/pmg@v1": { | ||
| "repo": "safedep/pmg", | ||
| "version": "v1", | ||
| "sha": "46cc70db535107183c9e752bb55d1d5c5f1a9290" | ||
| }, | ||
| "super-linter/super-linter@v8.7.0": { | ||
| "repo": "super-linter/super-linter", | ||
| "version": "v8.7.0", | ||
| "sha": "4ce20838b8ab83717e78138c5b3a1407148e0918" | ||
| "entries": { | ||
| "actions-ecosystem/action-add-labels@v1.1.3": { | ||
| "repo": "actions-ecosystem/action-add-labels", | ||
| "version": "v1.1.3", |
| { | ||
| "entries": { | ||
| "actions-ecosystem/action-add-labels@v1.1.3": { | ||
| "repo": "actions-ecosystem/action-add-labels", | ||
| "version": "v1.1.3", | ||
| "sha": "c96b68fec76a0987cd93957189e9abd0b9a72ff1", | ||
| "inputs": { | ||
| "github_token": { | ||
| "description": "A GitHub token.", | ||
| "default": "${{ github.token }}" | ||
| }, | ||
| "labels": { | ||
| "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.", | ||
| "required": true | ||
| }, | ||
| "number": { | ||
| "description": "The number of the issue or pull request." | ||
| }, | ||
| "repo": { | ||
| "description": "The owner and repository name. e.g.) Codertocat/Hello-World", | ||
| "default": "${{ github.repository }}" | ||
| } | ||
| }, | ||
| "action_description": "Add labels to an issue or a pull request." | ||
| }, | ||
| "actions/cache/restore@v6.1.0": { | ||
| "repo": "actions/cache/restore", | ||
| "version": "v6.1.0", | ||
| "sha": "55cc8345863c7cc4c66a329aec7e433d2d1c52a9" | ||
| }, | ||
| "actions/cache/save@v6.1.0": { | ||
| "repo": "actions/cache/save", | ||
| "version": "v6.1.0", | ||
| "sha": "55cc8345863c7cc4c66a329aec7e433d2d1c52a9" | ||
| }, | ||
| "actions/cache@v6.1.0": { | ||
| "repo": "actions/cache", | ||
| "version": "v6.1.0", | ||
| "sha": "55cc8345863c7cc4c66a329aec7e433d2d1c52a9" | ||
| }, | ||
| "actions/checkout@v7.0.0": { | ||
| "repo": "actions/checkout", | ||
| "version": "v7.0.0", | ||
| "sha": "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" | ||
| }, | ||
| "actions/create-github-app-token@v3.2.0": { | ||
| "repo": "actions/create-github-app-token", | ||
| "version": "v3.2.0", | ||
| "sha": "bcd2ba49218906704ab6c1aa796996da409d3eb1" | ||
| }, | ||
| "actions/download-artifact@v8.0.1": { | ||
| "repo": "actions/download-artifact", | ||
| "version": "v8.0.1", | ||
| "sha": "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c" | ||
| }, | ||
| "actions/github-script@v9.0.0": { | ||
| "repo": "actions/github-script", | ||
| "version": "v9.0.0", | ||
| "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" | ||
| }, | ||
| "actions/setup-dotnet@v5.4.0": { | ||
| "repo": "actions/setup-dotnet", | ||
| "version": "v5.4.0", | ||
| "sha": "26b0ec14cb23fa6904739307f278c14f94c95bf1" | ||
| }, | ||
| "actions/setup-go@v6.5.0": { | ||
| "repo": "actions/setup-go", | ||
| "version": "v6.5.0", | ||
| "sha": "924ae3a1cded613372ab5595356fb5720e22ba16" | ||
| }, | ||
| "actions/setup-java@v5.4.0": { | ||
| "repo": "actions/setup-java", | ||
| "version": "v5.4.0", | ||
| "sha": "1bcf9fb12cf4aa7d266a90ae39939e61372fe520" | ||
| }, | ||
| "actions/setup-node@v6.4.0": { | ||
| "repo": "actions/setup-node", | ||
| "version": "v6.4.0", | ||
| "sha": "48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e" | ||
| }, | ||
| "actions/setup-python@v6.3.0": { | ||
| "repo": "actions/setup-python", | ||
| "version": "v6.3.0", | ||
| "sha": "ece7cb06caefa5fff74198d8649806c4678c61a1" | ||
| }, | ||
| "actions/upload-artifact@v7.0.1": { | ||
| "repo": "actions/upload-artifact", | ||
| "version": "v7.0.1", | ||
| "sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" | ||
| }, | ||
| "anchore/sbom-action@v0.24.0": { | ||
| "repo": "anchore/sbom-action", | ||
| "version": "v0.24.0", | ||
| "sha": "e22c389904149dbc22b58101806040fa8d37a610" | ||
| }, | ||
| "astral-sh/setup-uv@v8.2.0": { | ||
| "repo": "astral-sh/setup-uv", | ||
| "version": "v8.2.0", | ||
| "sha": "fac544c07dec837d0ccb6301d7b5580bf5edae39" | ||
| }, | ||
| "denoland/setup-deno@v2.0.4": { | ||
| "repo": "denoland/setup-deno", | ||
| "version": "v2.0.4", | ||
| "sha": "667a34cdef165d8d2b2e98dde39547c9daac7282" | ||
| }, | ||
| "docker/build-push-action@v7.2.0": { | ||
| "repo": "docker/build-push-action", | ||
| "version": "v7.2.0", | ||
| "sha": "f9f3042f7e2789586610d6e8b85c8f03e5195baf", | ||
| "released_at": "2026-05-21T15:23:58Z" | ||
| }, | ||
| "docker/login-action@v4.2.0": { | ||
| "repo": "docker/login-action", | ||
| "version": "v4.2.0", | ||
| "sha": "650006c6eb7dba73a995cc03b0b2d7f5ca915bee", | ||
| "released_at": "2026-05-22T11:55:00Z" | ||
| }, | ||
| "docker/metadata-action@v6.1.0": { | ||
| "repo": "docker/metadata-action", | ||
| "version": "v6.1.0", | ||
| "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" | ||
| }, | ||
| "docker/setup-buildx-action@v4.1.0": { | ||
| "repo": "docker/setup-buildx-action", | ||
| "version": "v4.1.0", | ||
| "sha": "d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5" | ||
| }, | ||
| "erlef/setup-beam@v1.24.0": { | ||
| "repo": "erlef/setup-beam", | ||
| "version": "v1.24.0", | ||
| "sha": "fc68ffb90438ef2936bbb3251622353b3dcb2f93" | ||
| }, | ||
| "github/codeql-action/upload-sarif@v4.36.2": { | ||
| "repo": "github/codeql-action/upload-sarif", | ||
| "version": "v4.36.2", | ||
| "sha": "8aad20d150bbac5944a9f9d289da16a4b0d87c1e" | ||
| }, | ||
| "github/stale-repos@v9.0.15": { | ||
| "repo": "github/stale-repos", | ||
| "version": "v9.0.15", | ||
| "sha": "a0db8bfe2318ed6ddbd01e95d7633803f99f77e9" | ||
| }, | ||
| "haskell-actions/setup@v2.11.0": { | ||
| "repo": "haskell-actions/setup", | ||
| "version": "v2.11.0", | ||
| "sha": "cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553" | ||
| }, | ||
| "oven-sh/setup-bun@v2.2.0": { | ||
| "repo": "oven-sh/setup-bun", | ||
| "version": "v2.2.0", | ||
| "sha": "0c5077e51419868618aeaa5fe8019c62421857d6" | ||
| }, | ||
| "ruby/setup-ruby@v1.314.0": { | ||
| "repo": "ruby/setup-ruby", | ||
| "version": "v1.314.0", | ||
| "sha": "9eb537ca036ebaed86729dcb9309076e4c5c3b74" | ||
| }, | ||
| "safedep/pmg@v1": { | ||
| "repo": "safedep/pmg", | ||
| "version": "v1", | ||
| "sha": "46cc70db535107183c9e752bb55d1d5c5f1a9290" | ||
| }, | ||
| "super-linter/super-linter@v8.7.0": { | ||
| "repo": "super-linter/super-linter", | ||
| "version": "v8.7.0", | ||
| "sha": "4ce20838b8ab83717e78138c5b3a1407148e0918" | ||
| "entries": { | ||
| "actions-ecosystem/action-add-labels@v1.1.3": { | ||
| "repo": "actions-ecosystem/action-add-labels", | ||
| "version": "v1.1.3", |
|
@copilot merge main and recompile |
…der cache fallback window Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🏗️ Design Decision Gate — ADR RequiredThis PR makes significant changes to core business logic (>100 new lines in 📄 Draft ADR committed:
📋 What to do next
Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision. ❓ Why ADRs MatterADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you. 📋 Michael Nygard ADR Format ReferenceAn ADR must contain these four sections to be considered complete:
All ADRs are stored in
|
🧪 Test Quality Sentinel Report✅ Test Quality Score: 90/100 — Excellent
📊 Metrics (1 test)
i️ Flagged Tests (1 — inflation only)
The only concern is test inflation (100 new test lines / 35 new production lines = 2.86:1) — understandable given the extensive mock scaffolding required for an integration-style vitest test, but worth noting. No test was added for the Verdict
Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "awmgmcpg"See Network Configuration for more information.
|
Review SummaryGood, targeted fix for a real production incident. Applied 5 inline suggestions — none are blocking, but a few are worth addressing:
@copilot please address the review comments above.
|
There was a problem hiding this comment.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 97.3 AIC · ⌖ 7.79 AIC · ⊞ 6.6K
Comment /matt to run again
| // concurrent activations start making calls simultaneously. Re-checking every | ||
| // RATE_LIMIT_RECHECK_INTERVAL consumed operations (1 re-check per ~5 cache-miss runs) | ||
| // lets each job detect budget exhaustion and stop before the reserve is fully drained. | ||
| if (apiCallsInLoop > 0 && apiCallsInLoop % RATE_LIMIT_RECHECK_INTERVAL === 0) { |
There was a problem hiding this comment.
[/diagnosing-bugs] The in-loop re-check only fires at exact multiples of RATE_LIMIT_RECHECK_INTERVAL (10 ops). If the loop finishes with fewer ops (e.g. only 3–4 cache-miss runs), the re-check never fires for that execution, leaving a concurrent budget-exhaustion window undetected.
💡 Suggestion
Document this known limitation in the constant's JSDoc, or add a post-loop check:
// After the for...of loop:
if (apiCallsInLoop > 0 && apiCallsInLoop % RATE_LIMIT_RECHECK_INTERVAL !== 0) {
const finalRL = await getCoreRateLimitSnapshot(githubClient);
if (finalRL.remaining <= RATE_LIMIT_RESERVE) {
logDailyGuardrail('Post-loop rate limit check: headroom exhausted', { remaining: finalRL.remaining });
}
}Alternatively, accepting this as a known trade-off is fine — just note it explicitly in the JSDoc so future maintainers don't wonder why short runs skip the check.
@copilot please address this.
| }); | ||
| } else { | ||
| // Cache miss: fetch AIC from the run's usage artifact. | ||
| apiCallsInLoop += ESTIMATED_API_OPERATIONS_PER_RUN; |
There was a problem hiding this comment.
[/diagnosing-bugs] apiCallsInLoop is incremented after getRunAIC is awaited on line 597. This means if getRunAIC throws an exception, the catch block on line 622 swallows the error and the ops counter is never incremented — so a run that fails mid-artifact-fetch is invisible to the rate-limit accounting.
💡 Suggestion
Increment before the async call to ensure every cache-miss attempt is counted regardless of whether it succeeds or fails:
} else {
// Cache miss: fetch AIC from the run's usage artifact.
apiCallsInLoop += ESTIMATED_API_OPERATIONS_PER_RUN; // increment first
runAIC = await module.exports.getRunAIC(artifactClient, run.id, token, owner, repo);
}This prevents the re-check threshold from being delayed in error-heavy runs where all artifact fetches throw.
@copilot please address this.
| rateLimitGetCallCount++; | ||
| // First call (start snapshot): plenty of budget. | ||
| // Subsequent calls (in-loop re-check, end snapshot): below reserve. | ||
| const remaining = rateLimitGetCallCount === 1 ? 5000 : 50; |
There was a problem hiding this comment.
[/tdd] The mock's rate-limit collapse is binary: call 1 returns 5000, all subsequent calls return 50. This conflates the in-loop re-check and the post-loop end snapshot into the same low-remaining scenario. The test doesn't verify what happens if the in-loop re-check returns above the reserve (e.g. 200) but the final end snapshot is below — the loop should continue but reporting should still capture the final state.
💡 Suggested additional test case
Add a sibling test where the mid-loop re-check returns a "safe" value (e.g. 500) and the loop runs to completion, confirming no early break occurs:
it('main() continues loop when in-loop re-check finds sufficient headroom', async () => {
// all rate-limit calls return safely above reserve
// assert getRunAIC called for all 15 runs
});This completes the specification of the in-loop branch's both paths (break vs. continue).
@copilot please address this.
|
|
||
| // 5 cache-miss runs are processed (10 consumed API ops) before iteration 6 triggers the | ||
| // in-loop re-check, finds remaining=50 <= RATE_LIMIT_RESERVE=100, and breaks the loop. | ||
| expect(getRunAICSpy).toHaveBeenCalledTimes(5); |
There was a problem hiding this comment.
[/tdd] The assertion expect(getRunAICSpy).toHaveBeenCalledTimes(5) is tightly coupled to the magic number 5, which is derived from RATE_LIMIT_RECHECK_INTERVAL / ESTIMATED_API_OPERATIONS_PER_RUN. If either constant changes, this test will silently become wrong without a clear signal.
💡 Suggestion
Reference the constants explicitly in the assertion comment or compute the expected call count from the imported constants:
// RATE_LIMIT_RECHECK_INTERVAL=10 ops / ESTIMATED_API_OPERATIONS_PER_RUN=2 = 5 runs before first re-check fires
const expectedRunsBeforeRecheck = RATE_LIMIT_RECHECK_INTERVAL / ESTIMATED_API_OPERATIONS_PER_RUN;
expect(getRunAICSpy).toHaveBeenCalledTimes(expectedRunsBeforeRecheck);This makes the test self-documenting and resilient to constant changes.
@copilot please address this.
| * A larger window improves hit rate on `pull_request` branches where `actions/cache` is | ||
| * branch-scoped and frequently misses, requiring the artifact fallback to look further back. | ||
| */ | ||
| const MAX_RUNS_TO_SEARCH = 30; |
There was a problem hiding this comment.
[/diagnosing-bugs] MAX_RUNS_TO_SEARCH = 30 widens the fallback search window but also widens the window for the listWorkflowRuns API call. The PR notes "30 covers the same single listWorkflowRuns page at no additional cost" — this is only true if the workflow has ≥30 completed runs on a single page (100 per page). On a lightly-used workflow or a brand-new repo, this assumption holds fine. But the 6× wider window also means the listArtifacts calls are spread over up to 30 runs rather than 5, which could increase the rate-limit fan-out when the cache is cold.
💡 Suggestion
The PR description says the artifact fallback is the only warm path for PR branches. If that's the goal, the constant might benefit from a more explicit name and JSDoc that clarifies the trade-off:
/**
* Maximum number of recent workflow runs to scan for a usable cache artifact.
* On pull_request branches, actions/cache is branch-scoped and will never hit
* a run from another PR, so this artifact fallback is the only warm path.
*
* Trade-off: a larger value improves cold-cache hit rate but increases
* listArtifacts fan-out when no artifact is found (e.g. completely cold run).
* 30 stays within a single listWorkflowRuns page (100 per page) and is the
* observed sufficient window for active repositories.
*/
const MAX_RUNS_TO_SEARCH = 30;Also consider whether the fallback should cap its listArtifacts calls using the same rate-limit guard applied in the guardrail file.
@copilot please address this.
| process.env.GITHUB_EVENT_NAME = "pull_request"; | ||
|
|
||
| try { | ||
| await expect(exports.main()).resolves.toBeUndefined(); |
There was a problem hiding this comment.
[/tdd] The test name describes the re-check mechanic precisely, but the test doesn't assert that truncatedByRateLimit is reflected in the output (e.g. daily_ai_credits_exceeded or some output/log that signals "inspection was rate-limited"). The truncatedByRateLimit = true branch should have a visible observable effect for downstream consumers.
💡 Suggestion
Check if the code sets any output or log that signals truncation and add an assertion:
// If truncatedByRateLimit affects any output, assert it:
expect(coreOutputs['daily_aic_guardrail_truncated_by_rate_limit']).toBe('true');
// or check the warning log emitted when truncation occursIf no such output exists, consider adding one to make truncation observable to downstream workflow steps.
@copilot please address this.
|
@copilot please run the
|
Done — merged main and recompiled. All pre-PR validation checks passed. |
There was a problem hiding this comment.
Review: fix(aic-guardrail) — widen fallback search window and in-loop rate-limit backoff
The core fix in check_daily_aic_workflow_guardrail.cjs is sound: the in-loop getCoreRateLimitSnapshot re-check addresses the concurrent-activation staleness problem cleanly, and the new test accurately exercises the early-exit path. The implementation is correct.
I've surfaced a few non-blocking issues and one reliability concern worth addressing before merge:
🔴 Reliability concern (non-blocking but worth addressing)
restore_aic_usage_cache_fallback.cjs: Widening MAX_RUNS_TO_SEARCH from 5→30 increases the worst-case listWorkflowRunArtifacts fan-out from 5 to 30 calls per activation (one per run until a usable artifact is found). The PR body identifies this exact call class as causing 99.5% of rate-limit saturation. The fix in the guardrail file won't protect against the fallback script's fan-out. See inline comment.
🟡 Non-blocking suggestions
apiCallsInLoopincremented before the API call: conservative accounting but worth a clarifying note or a post-call placement (inline comment on line 596).- Re-check overhead not budgeted:
computeMaxInspectableRunsdoesn't account for the extrarateLimit.get()calls from in-loop re-checks (inline comment on line 574). mockCoremissingsetFailed: the new test mock is incomplete; a future logic change that triggerssetFailedwill produce a crypticTypeErrorrather than a meaningful test failure (inline comment on line 629).
Overall the in-loop backoff (AC3/AC4) and the RATE_LIMIT_RECHECK_INTERVAL constant are well-designed. The fallback search window concern (AC2) needs attention before this ships.
🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 139.5 AIC · ⌖ 7.03 AIC · ⊞ 4.9K
Comments that could not be inline-anchored
actions/setup/js/restore_aic_usage_cache_fallback.cjs:135
listWorkflowRunArtifacts fan-out increases 6×
The change to MAX_RUNS_TO_SEARCH = 30 means the per_page on listWorkflowRuns fetches 30 runs in one call — that part is free. However, the inner loop at line 138 calls listWorkflowRunArtifacts once per run until it finds a usable artifact. Worst-case, that is 30 separate API calls instead of 5.
The PR body describes the O(N×M) fan-out of listArtifacts in check_daily_aic_workflow_guardrail.cjs as the core rate-limit proble…
actions/setup/js/check_daily_aic_workflow_guardrail.cjs:596
apiCallsInLoop incremented before the API call on error
The counter is incremented at line 596, then getRunAIC is called at line 597. If getRunAIC throws (caught at line 622), the budget counter has already been advanced for a failed call — the next re-check will fire one interval earlier than expected.
This is conservative (safe) accounting, but it means a burst of failing artifact lookups advances the re-check counter without consuming real rate-limit budget. If the intent is to t…
actions/setup/js/check_daily_aic_workflow_guardrail.cjs:574
In-loop re-check itself consumes 1 API call per interval
The getCoreRateLimitSnapshot at this line calls githubClient.rest.rateLimit.get() which itself costs 1 API call. With RATE_LIMIT_RECHECK_INTERVAL = 10 and ESTIMATED_API_OPERATIONS_PER_RUN = 2, a re-check fires every 5 cache-miss runs. On a 100-run inspection window that would add ~10 extra rate-limit calls on top of the 200 artifact calls.
The computeMaxInspectableRuns budget doesn't account for these re-check calls (only …
actions/setup/js/check_daily_aic_workflow_guardrail.cjs:573
Re-check fires at the start of the iteration, using last iteration's count
The guard condition apiCallsInLoop > 0 && apiCallsInLoop % RATE_LIMIT_RECHECK_INTERVAL === 0 is evaluated at the top of the loop. Since apiCallsInLoop is incremented only on cache misses inside the else branch (line 596), a mix of cache hits and misses will cause the re-check to fire less frequently than RATE_LIMIT_RECHECK_INTERVAL / ESTIMATED_API_OPERATIONS_PER_RUN runs (because cache hits advance the ite…
actions/setup/js/check_daily_aic_workflow_guardrail.test.cjs:629
Test mockCore is missing setFailed
The test mock at line 629 defines setOutput, info, warning, and summary, but does not define setFailed. If a future change alters the loop logic so that the guardrail threshold is exceeded before the rate-limit re-check fires, main() will call core.setFailed(...)) and the test will throw TypeError: core.setFailed is not a function, producing a confusing failure message.
Add setFailed: () => {} (or a spy) to the mock to make future b…
|
|
|
🎉 This pull request is included in a new release. Release: |
Under high run frequency with a cold cache, N concurrent activations each calling
listArtifactsper prior run produces an O(N×M) fan-out against the sharedGITHUB_TOKENcore budget — in production this accounted for 99.5% of all/actions/runs/:run_id/artifactscalls in one hour, starving unrelated critical steps (e.g. attestation persistence) with HTTP 403.Changes
restore_aic_usage_cache_fallback.cjs— wider cross-branch artifact search (AC2)MAX_RUNS_TO_SEARCH: 5 → 30pull_requestbranches never hit a per-branchactions/cacheentry from another PR, making the artifact fallback their only warm-cache path. The previous 5-run window missed on active repos; 30 covers the same singlelistWorkflowRunspage at no additional cost.check_daily_aic_workflow_guardrail.cjs— in-loop rate-limit re-check (AC3, AC4)RATE_LIMIT_RECHECK_INTERVAL = 10: after every 10 API operations consumed by cache misses (ESTIMATED_API_OPERATIONS_PER_RUN = 2→ fires every 5 cache-miss runs), callsgetCoreRateLimitSnapshotand breaks early ifremaining ≤ RATE_LIMIT_RESERVE.computeMaxInspectableRuns) is computed once from a stale snapshot; concurrent activations each see the same snapshot and collectively overspend it. The in-loop re-check reads the live shared quota and stops as soon as it's actually at risk.Test coverage
getRunAICis called exactly 5 times before the loop stops.