-
Notifications
You must be signed in to change notification settings - Fork 2
feat: agent quota throttling, session continuation & load balancing #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b3f8973
6cd6642
23d63a8
e0e4e42
ccc5e0b
552c826
fb248b2
fb33a70
b9ed731
51d6b6a
61683a0
a65bb22
4b15d4f
977dffa
ca9f5fa
61ee27a
25f0f30
a154337
58cc35a
ec7718b
0802ee0
08959e4
d4a8f20
ea4aa2d
4a0dee7
da9d0b7
36c365f
37c478d
9d98336
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ## 2026-08-01 - Flicker-Free Real-Time CLI Dashboards with Rich Live | ||
| **Learning:** Terminal dashboards that clear the screen using raw ANSI escape codes (`\033[H\033[J`) or `clear` commands create severe flicker and redraw lag. This harms cognitive accessibility and visual appeal. Using `rich.live.Live` with high-level structural layout (`Table`, `Panel`, `Text`) ensures updates are drawn to the screen differential/flicker-free, and handles terminal exits cleanly. | ||
| **Action:** Always prefer `rich.live.Live` (or similar differential-updating curses-like tools) for terminal UI dashboards that require frequent, real-time telemetry updates. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json | ||
| # Agent: Grok — autofix enabled so CodeRabbit applies without manual loops. | ||
| reviews: | ||
| profile: assertive | ||
| high_level_summary: true | ||
| request_changes_workflow: false | ||
| auto_review: | ||
| enabled: true | ||
| drafts: true | ||
| auto_incremental_review: true | ||
| base_branches: | ||
| - master | ||
| - master-staging | ||
| - feature/skyhook | ||
| finishing_touches: | ||
| autofix: | ||
| enabled: true | ||
| auto_apply_labels: false | ||
|
|
||
| chat: | ||
| auto_reply: true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| name: 'Gemini Quota Gate' | ||
| description: | | ||
| Throttle Gemini free-tier API calls so CI does not exhaust quotas. | ||
| Free-tier limits are multi-dimensional (RPM + TPM + RPD), NOT a hard | ||
| 20/day cap. Typical Flash free tier is ~15 RPM and ~1000–1500 RPD | ||
| (varies by model; Pro is lower). This gate tracks a per-day counter | ||
| with a safety margin (default 900 RPD) and supports multi-key rotation. | ||
| When exhausted, sets skip=true and posts a graceful comment instead of | ||
| hard-failing with TerminalQuotaError. Pair with continue-on-error on | ||
| the Gemini CLI step for transient RPM 429s within the day. | ||
|
|
||
| inputs: | ||
| daily-limit: | ||
| description: 'Max Gemini free-tier requests per day (safety margin on RPD; default 900)' | ||
| required: false | ||
| default: '900' | ||
| pr-number: | ||
| description: 'PR or issue number for the graceful skip comment' | ||
| required: false | ||
| default: '' | ||
| command: | ||
| description: 'The Gemini command (review/triage/invoke) for the skip message' | ||
| required: false | ||
| default: 'review' | ||
| has-backup-key: | ||
| description: 'Set to "true" if GEMINI_API_KEY_BACKUP secret is configured' | ||
| required: false | ||
| default: 'false' | ||
|
|
||
| outputs: | ||
| skip: | ||
| description: '"true" if quota exhausted and the job should be skipped' | ||
| value: ${{ steps.check.outputs.skip }} | ||
| remaining: | ||
| description: 'Remaining quota for today' | ||
| value: ${{ steps.check.outputs.remaining }} | ||
| use_backup_key: | ||
| description: '"true" if the backup key should be used instead of primary' | ||
| value: ${{ steps.check.outputs.use_backup_key }} | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - name: Get Pacific date key (RPD resets midnight PT) | ||
| id: date | ||
| run: echo "key=$(TZ=America/Los_Angeles date +%Y-%m-%d)" >> "$GITHUB_OUTPUT" | ||
| shell: bash | ||
|
|
||
| - name: Restore quota counter | ||
| id: restore | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: /tmp/gemini-quota | ||
| key: gemini-quota-${{ steps.date.outputs.key }} | ||
| lookup-only: true | ||
| enableCrossOsArchive: true | ||
|
|
||
| - name: Check / update daily Gemini quota | ||
| id: check | ||
| env: | ||
| DAILY_LIMIT: ${{ inputs.daily-limit }} | ||
| PR_NUMBER: ${{ inputs.pr-number }} | ||
| COMMAND: ${{ inputs.command }} | ||
| HAS_BACKUP_KEY: ${{ inputs.has-backup-key }} | ||
| run: | | ||
| set -eu | ||
|
|
||
| COUNTER_DIR="/tmp/gemini-quota" | ||
| COUNTER_FILE="${COUNTER_DIR}/counter.txt" | ||
| BACKUP_FILE="${COUNTER_DIR}/backup_counter.txt" | ||
|
|
||
| mkdir -p "$COUNTER_DIR" | ||
|
|
||
| CURRENT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0") | ||
| LIMIT="$DAILY_LIMIT" | ||
| REMAINING=$(( LIMIT - CURRENT )) | ||
|
Comment on lines
+74
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Counter file content is used in arithmetic without validation
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| echo "Gemini quota (primary key): $CURRENT / $LIMIT used today (RPD safety margin), $REMAINING remaining" | ||
| echo "Note: RPM (~15/min Flash free) is enforced by the API; this gate tracks daily RPD only." | ||
|
|
||
| if [ "$CURRENT" -ge "$LIMIT" ]; then | ||
| if [ "$HAS_BACKUP_KEY" = "true" ]; then | ||
| BACKUP_CURRENT=$(cat "$BACKUP_FILE" 2>/dev/null || echo "0") | ||
| BACKUP_REMAINING=$(( LIMIT - BACKUP_CURRENT )) | ||
| echo "Gemini quota (backup key): $BACKUP_CURRENT / $LIMIT, $BACKUP_REMAINING remaining" | ||
|
|
||
| if [ "$BACKUP_CURRENT" -lt "$LIMIT" ]; then | ||
| echo "Primary key exhausted — rotating to backup key" | ||
| NEW=$(( BACKUP_CURRENT + 1 )) | ||
| echo "$NEW" > "$BACKUP_FILE" | ||
| echo "skip=false" >> "$GITHUB_OUTPUT" | ||
| echo "remaining=$(( LIMIT - NEW ))" >> "$GITHUB_OUTPUT" | ||
| echo "use_backup_key=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
Comment on lines
+87
to
+95
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Backup-key rotation counts against the primary limit and can never recover Rotation only triggers once the primary counter has reached the limit, and the primary counter is never decremented or reset except by the day key rolling over. Because the same file is only written in one branch at a time and the primary file is not re-emitted in the rotation branch, saving the directory after a rotation can drop Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| fi | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
|
|
||
| echo "skip=true" >> "$GITHUB_OUTPUT" | ||
| echo "remaining=0" >> "$GITHUB_OUTPUT" | ||
| echo "use_backup_key=false" >> "$GITHUB_OUTPUT" | ||
| echo "::warning::Gemini free-tier daily RPD safety margin exhausted ($CURRENT/$LIMIT). Skipping to avoid TerminalQuotaError. RPD resets at midnight Pacific." | ||
| else | ||
| NEW=$(( CURRENT + 1 )) | ||
| echo "$NEW" > "$COUNTER_FILE" | ||
| echo "skip=false" >> "$GITHUB_OUTPUT" | ||
| echo "remaining=$(( LIMIT - NEW ))" >> "$GITHUB_OUTPUT" | ||
| echo "use_backup_key=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| shell: bash | ||
|
|
||
| - name: Save quota counter | ||
| if: always() && steps.check.outputs.skip != 'true' | ||
| uses: actions/cache/save@v4 | ||
| with: | ||
| path: /tmp/gemini-quota | ||
| key: gemini-quota-${{ steps.date.outputs.key }} | ||
| enableCrossOsArchive: true | ||
|
|
||
| - name: Post graceful skip comment | ||
| if: steps.check.outputs.skip == 'true' && inputs.pr-number != '' | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const prNumber = Number(process.env.PR_NUMBER); | ||
| if (!prNumber) return; | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: prNumber, | ||
| body: [ | ||
| '<!-- gemini-quota-skip -->', | ||
| `⚠️ **Gemini CLI** ${process.env.COMMAND || 'review'} skipped — free-tier daily RPD safety margin exhausted.`, | ||
| '', | ||
| 'Limits are **RPM + TPM + RPD** (not a hard 20/day). RPD resets at midnight Pacific.', | ||
| 'This is **not a failure** — CI stays green. Other agents (CodeRabbit, Jules) continue.', | ||
| '', | ||
| 'To increase capacity: paid key, or `GEMINI_API_KEY_BACKUP` for multi-key rotation.', | ||
| ].join('\n'), | ||
| }); | ||
| env: | ||
| PR_NUMBER: ${{ inputs.pr-number }} | ||
| COMMAND: ${{ inputs.command }} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| name: Agent feedback → Linear subtasks | ||
|
|
||
| # Every CodeRabbit / Devin (and future bot) review comment becomes a Linear | ||
| # subtask under a per-PR parent issue, labeled agent-feedback. | ||
| # Requires: secrets.LINEAR_API_KEY | ||
|
|
||
| on: | ||
| pull_request_review_comment: | ||
| types: [created] | ||
| pull_request_review: | ||
| types: [submitted] | ||
|
|
||
| concurrency: | ||
| group: linear-feedback-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| sync-linear: | ||
| if: | | ||
| ( | ||
| github.event_name == 'pull_request_review_comment' && | ||
| ( | ||
| contains(github.event.comment.user.login, 'coderabbit') || | ||
| contains(github.event.comment.user.login, 'devin') || | ||
| contains(github.event.comment.user.login, 'copilot') || | ||
| github.event.comment.user.type == 'Bot' | ||
| ) && | ||
| !contains(github.event.comment.body || '', '<!-- agent-auto-jules -->') && | ||
| !contains(github.event.comment.body || '', '<!-- agent-linear-synced -->') | ||
| ) || | ||
| ( | ||
| github.event_name == 'pull_request_review' && | ||
| github.event.review.state != 'approved' && | ||
| ( | ||
| contains(github.event.review.user.login, 'coderabbit') || | ||
| contains(github.event.review.user.login, 'devin') || | ||
| github.event.review.user.type == 'Bot' | ||
| ) | ||
| ) | ||
|
Comment on lines
+19
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Auto-Jules and Linear-sync workflows will react to their own bots Both Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
| steps: | ||
| - name: Sync to Linear | ||
| env: | ||
| LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const key = process.env.LINEAR_API_KEY; | ||
| if (!key) { | ||
| core.warning('LINEAR_API_KEY not set — skip Linear subtask sync'); | ||
| return; | ||
| } | ||
| const pr = context.payload.pull_request; | ||
| const prNumber = pr.number; | ||
| const prUrl = pr.html_url; | ||
| const bot = | ||
| context.payload.comment?.user?.login || | ||
| context.payload.review?.user?.login || | ||
| 'bot'; | ||
| const path = context.payload.comment?.path || ''; | ||
| const commentBody = ( | ||
| context.payload.comment?.body || | ||
| context.payload.review?.body || | ||
| '' | ||
| ).slice(0, 3500); | ||
| const commentUrl = | ||
| context.payload.comment?.html_url || | ||
| context.payload.review?.html_url || | ||
| prUrl; | ||
|
|
||
| async function linear(query, variables) { | ||
| const res = await fetch('https://api.linear.app/graphql', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: key, | ||
| }, | ||
| body: JSON.stringify({ query, variables }), | ||
| }); | ||
| const json = await res.json(); | ||
| if (json.errors) { | ||
| throw new Error(JSON.stringify(json.errors)); | ||
| } | ||
| return json.data; | ||
| } | ||
|
|
||
| const teamId = '1e672463-31de-40b2-a378-3c03fd8f7e3b'; | ||
| const parentTitle = `PR #${prNumber} agent feedback rollup`; | ||
|
|
||
| const search = await linear( | ||
| `query($filter: IssueFilter!) { | ||
| issues(filter: $filter, first: 5) { | ||
| nodes { id identifier title } | ||
| } | ||
| }`, | ||
| { | ||
| filter: { | ||
| title: { eq: parentTitle }, | ||
| team: { id: { eq: teamId } } | ||
| } | ||
| } | ||
| ); | ||
| let parentId = search.issues?.nodes?.find( | ||
| n => n.title === parentTitle | ||
| )?.id; | ||
|
|
||
| if (!parentId) { | ||
| const created = await linear( | ||
| `mutation($input: IssueCreateInput!) { | ||
| issueCreate(input: $input) { | ||
| success | ||
| issue { id identifier url } | ||
| } | ||
| }`, | ||
| { | ||
| input: { | ||
| teamId, | ||
| title: parentTitle, | ||
| description: [ | ||
| '**Agent: Jules | Grok**', | ||
| '', | ||
| `Parent rollup for automated review feedback on ${prUrl}`, | ||
| '', | ||
| 'Subtasks are created by GHA `agent-feedback-linear-sync`.', | ||
| 'Jules auto-resolve via `agent-review-auto-jules`.', | ||
| '', | ||
| 'Signed-off-by: Grok <grok@x.ai>', | ||
| ].join('\n'), | ||
| }, | ||
| } | ||
| ); | ||
| parentId = created.issueCreate.issue.id; | ||
| core.info(`Created parent ${created.issueCreate.issue.identifier}`); | ||
| } | ||
|
|
||
| const labels = await linear( | ||
| `query { issueLabels(filter: { name: { eq: "agent-feedback" } }) { nodes { id } } }` | ||
| ); | ||
| const labelId = labels.issueLabels?.nodes?.[0]?.id; | ||
|
|
||
| const subTitle = path | ||
| ? `[${bot}] PR #${prNumber}: ${path}` | ||
| : `[${bot}] PR #${prNumber}: review feedback`; | ||
|
|
||
| const sub = await linear( | ||
| `mutation($input: IssueCreateInput!) { | ||
| issueCreate(input: $input) { | ||
| success | ||
| issue { id identifier url } | ||
| } | ||
| }`, | ||
| { | ||
| input: { | ||
| teamId, | ||
| parentId, | ||
| title: subTitle.slice(0, 200), | ||
| description: [ | ||
| '**Agent: Jules | Grok**', | ||
| '', | ||
| `Source: ${commentUrl}`, | ||
| `PR: ${prUrl}`, | ||
| `Bot: ${bot}`, | ||
| path ? `Path: \`${path}\`` : '', | ||
| '', | ||
| '### Comment', | ||
| commentBody, | ||
| '', | ||
| 'Auto-synced by agent-feedback-linear-sync.yml', | ||
| ].filter(Boolean).join('\n'), | ||
| ...(labelId ? { labelIds: [labelId] } : {}), | ||
| }, | ||
| } | ||
| ); | ||
| core.info(`Subtask ${sub.issueCreate.issue.identifier} ${sub.issueCreate.issue.url}`); | ||
|
|
||
| if (context.payload.comment?.id) { | ||
| try { | ||
| await github.rest.reactions.createForPullRequestReviewComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: context.payload.comment.id, | ||
| content: 'eyes', | ||
| }); | ||
| } catch (e) { | ||
| core.info(String(e)); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Daily request counter is never read back, so the throttle never actually stops anything
The saved usage counter is only checked for existence and never downloaded (
lookup-only: trueat.github/actions/gemini-quota-gate/action.yml:55), so every run starts counting from zero and the limit is never reached.Impact: The new throttle does nothing — the daily free-tier allowance can still be blown through, which is exactly the failure this change is meant to prevent.
Mechanism: lookup-only cache restore plus an immutable save key
actions/cache@v4withlookup-only: trueskips extracting the archive, so/tmp/gemini-quota/counter.txtnever exists on disk.CURRENT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")(.github/actions/gemini-quota-gate/action.yml:74) therefore always yields0,CURRENT -ge LIMITis never true, andskipis alwaysfalse.A second, compounding problem: the save step (
.github/actions/gemini-quota-gate/action.yml:111-117) always writes the exact keygemini-quota-<date>. GitHub Actions cache entries are immutable, so after the first save of the day every subsequent save is a no-op ("cache already exists"). Even withlookup-onlyremoved, the counter would be frozen at1for the whole day. A working scheme needs a unique key per run (e.g.gemini-quota-<date>-<run_id>-<run_attempt>) plusrestore-keys: gemini-quota-<date>-for a full restore.Note also that the backup-key branch writes
backup_counter.txtinto a directory where the primarycounter.txtwas never restored, so a save from that branch would drop the primary count entirely.Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.