Compare commits

..
Author SHA1 Message Date
hermes 04b15a2c27 fix(agents): wire XAI_API_KEY through reusable workflow (#122)
ci / lint (push) Successful in 14s
Declare workflow_call secrets, diagnostics, explicit caller template.
2026-07-30 15:47:35 +02:00
hermes 9dc1c203cb fix(agents): declare workflow_call secrets + diagnose empty XAI_API_KEY
ci / lint (pull_request) Successful in 14s
Cross-owner reusable calls left XAI_API_KEY empty in the runner while
OLLAMA_CLOUD_API_KEY worked. Declare secrets on workflow_call, accept
alternate secret names, log key lengths (not values), and ship an
ai-agent.yml caller template with an explicit secrets map.
2026-07-30 16:47:13 +03:00
hermes 58db2996ad feat(agents): automatic Ollama Cloud → xAI fallback (#121)
ci / lint (push) Successful in 14s
Primary stays ollama-cloud; on quota/provider failure run-agent fails over once to agents.json fallback (xAI).
2026-07-30 15:28:44 +02:00
hermes 0ddde87b40 feat(agents): automatic model fallback when Ollama Cloud is exhausted
ci / lint (pull_request) Successful in 13s
Keep ollama-cloud/* as primary. Each agent may declare fallback in
agents.json (xai-oc/grok-*). On quota/auth/provider failure, run-agent
switches once to the fallback and continues retries.

- agents.json: fallback fields for pm/junior/senior/qa/intern
- route.sh: emit fallback to GITHUB_OUTPUT
- agent.yml: pass FALLBACK env into run-agent
- install-opencode.sh: register primary+fallback models in provider maps
- run-agent.sh: failover on quota/credit/429/capacity-class errors
2026-07-30 16:28:30 +03:00
hermes bbdb200f32 fix(agents): route pm/junior/senior/qa to xAI while Ollama Cloud is exhausted
ci / lint (pull_request) Successful in 13s
Ollama Cloud usage is at 100% so ollama-cloud/* agents fail. Move
pm, junior, senior, qa to xai-oc (grok-4.5 / grok-4.3). leave intern on
local ollama/ornith:35b; lead/ops already on xAI.
2026-07-30 16:25:46 +03:00
ffaerber d7d54546a3 Merge pull request 'Allow hermes user to trigger AI agent workflow' (#120) from ai/issue-218-allow-hermes into main
ci / lint (push) Successful in 13s
Reviewed-on: #120
2026-07-30 14:07:49 +02:00
hermes 38b36b2304 feat: allow hermes user to trigger AI agent workflow
ci / lint (pull_request) Successful in 14s
Add 'hermes' to the trusted issue creators and comment authors in the
workflow gate so the Hermes Agent can create issues and comment with
@mentions to trigger the AI agent pipeline.
2026-07-30 15:06:05 +03:00
ffaerber e040247097 Merge pull request 'fix(agents): rename xai provider key to xai-oc to avoid opencode catalog collision' (#119) from hotfix/xai-provider-name-collision into main
ci / lint (push) Successful in 14s
2026-07-30 13:08:37 +02:00
6 changed files with 92 additions and 15 deletions
+32 -3
View File
@@ -4,6 +4,31 @@ name: agent
# The gate + steps run in the caller's event context (github.event.* / github.repository are the caller's). # The gate + steps run in the caller's event context (github.event.* / github.repository are the caller's).
on: on:
workflow_call: workflow_call:
# Explicit secret contract so callers can map secrets by name (more reliable than
# secrets: inherit alone on some Gitea versions / cross-owner reusable workflows).
secrets:
GITEA_TOKEN:
required: true
OLLAMA_URL:
required: false
OLLAMA_CLOUD_API_KEY:
required: false
XAI_API_KEY:
required: false
TOKEN_PM:
required: false
TOKEN_SENIOR:
required: false
TOKEN_JUNIOR:
required: false
TOKEN_LEAD:
required: false
TOKEN_QA:
required: false
TOKEN_OPS:
required: false
TOKEN_INTERN:
required: false
# Pinned opencode version — used to install it and to key the CI cache below. # Pinned opencode version — used to install it and to key the CI cache below.
env: env:
@@ -25,9 +50,10 @@ jobs:
# Trusted author only, and only when a known agent is mentioned. This gate is the main # Trusted author only, and only when a known agent is mentioned. This gate is the main
# defense against malicious-issue prompt injection — do not loosen it. # defense against malicious-issue prompt injection — do not loosen it.
if: > if: >
(github.event.comment == null && github.event.issue.user.login == 'ffaerber') || (github.event.comment == null && (github.event.issue.user.login == 'ffaerber' || github.event.issue.user.login == 'hermes')) ||
(github.event.comment != null && (github.event.comment != null &&
(github.event.comment.user.login == 'ffaerber' || (github.event.comment.user.login == 'ffaerber' ||
github.event.comment.user.login == 'hermes' ||
github.event.comment.user.login == 'pm' || github.event.comment.user.login == 'pm' ||
github.event.comment.user.login == 'junior' || github.event.comment.user.login == 'junior' ||
github.event.comment.user.login == 'senior' || github.event.comment.user.login == 'senior' ||
@@ -149,7 +175,9 @@ jobs:
SCRIPTS: ${{ runner.temp }}/agents-scripts SCRIPTS: ${{ runner.temp }}/agents-scripts
OLLAMA_URL: ${{ secrets.OLLAMA_URL }} OLLAMA_URL: ${{ secrets.OLLAMA_URL }}
OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }} OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }} # Accept common alternate names — empty XAI_API_KEY has bitten us when the
# secret was stored under a slightly different key on the caller repo.
XAI_API_KEY: ${{ secrets.XAI_API_KEY || secrets.XAI_KEY || secrets.GROK_API_KEY || secrets.XAI_TOKEN }}
NAME: ${{ steps.prep.outputs.name }} NAME: ${{ steps.prep.outputs.name }}
SKILLS: ${{ steps.prep.outputs.skills }} # JSON array of skills this agent may load SKILLS: ${{ steps.prep.outputs.skills }} # JSON array of skills this agent may load
run: bash "$SCRIPTS/install-opencode.sh" run: bash "$SCRIPTS/install-opencode.sh"
@@ -212,7 +240,7 @@ jobs:
id: run id: run
env: env:
SCRIPTS: ${{ runner.temp }}/agents-scripts SCRIPTS: ${{ runner.temp }}/agents-scripts
XAI_API_KEY: ${{ secrets.XAI_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY || secrets.XAI_KEY || secrets.GROK_API_KEY || secrets.XAI_TOKEN }}
# SELF_TOKEN = the RUNNING agent's OWN token (TOKEN_PM for @pm, TOKEN_OPS for @ops, …). # SELF_TOKEN = the RUNNING agent's OWN token (TOKEN_PM for @pm, TOKEN_OPS for @ops, …).
# Only this agent's token is placed in its process env, so no agent can act as another. # Only this agent's token is placed in its process env, so no agent can act as another.
# Powers the gitea-api / gitea-admin skills — each agent calls Gitea as itself. Every # Powers the gitea-api / gitea-admin skills — each agent calls Gitea as itself. Every
@@ -221,6 +249,7 @@ jobs:
SELF_TOKEN: ${{ steps.prep.outputs.name == 'pm' && secrets.TOKEN_PM || steps.prep.outputs.name == 'junior' && secrets.TOKEN_JUNIOR || steps.prep.outputs.name == 'senior' && secrets.TOKEN_SENIOR || steps.prep.outputs.name == 'lead' && secrets.TOKEN_LEAD || steps.prep.outputs.name == 'qa' && secrets.TOKEN_QA || steps.prep.outputs.name == 'ops' && secrets.TOKEN_OPS || steps.prep.outputs.name == 'intern' && secrets.TOKEN_INTERN || '' }} SELF_TOKEN: ${{ steps.prep.outputs.name == 'pm' && secrets.TOKEN_PM || steps.prep.outputs.name == 'junior' && secrets.TOKEN_JUNIOR || steps.prep.outputs.name == 'senior' && secrets.TOKEN_SENIOR || steps.prep.outputs.name == 'lead' && secrets.TOKEN_LEAD || steps.prep.outputs.name == 'qa' && secrets.TOKEN_QA || steps.prep.outputs.name == 'ops' && secrets.TOKEN_OPS || steps.prep.outputs.name == 'intern' && secrets.TOKEN_INTERN || '' }}
NAME: ${{ steps.prep.outputs.name }} NAME: ${{ steps.prep.outputs.name }}
MODEL: ${{ steps.prep.outputs.model }} MODEL: ${{ steps.prep.outputs.model }}
FALLBACK: ${{ steps.prep.outputs.fallback }}
VISION: ${{ steps.prep.outputs.vision }} VISION: ${{ steps.prep.outputs.vision }}
MODE: ${{ steps.prep.outputs.mode }} MODE: ${{ steps.prep.outputs.mode }}
WORKMODE: ${{ steps.prep.outputs.workmode }} # build | discuss (devs consulted in-thread) WORKMODE: ${{ steps.prep.outputs.workmode }} # build | discuss (devs consulted in-thread)
+14 -1
View File
@@ -12,4 +12,17 @@ on:
jobs: jobs:
agent: agent:
uses: gitea/agents/.gitea/workflows/agent.yml@main uses: gitea/agents/.gitea/workflows/agent.yml@main
secrets: inherit # Explicit secret map (plus inherit) so XAI_API_KEY / OLLAMA_* always reach the reusable
# workflow. secrets: inherit alone has left XAI_API_KEY empty on some Gitea cross-owner calls.
secrets:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
OLLAMA_URL: ${{ secrets.OLLAMA_URL }}
OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
TOKEN_PM: ${{ secrets.TOKEN_PM }}
TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }}
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
TOKEN_QA: ${{ secrets.TOKEN_QA }}
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
TOKEN_INTERN: ${{ secrets.TOKEN_INTERN }}
+10 -5
View File
@@ -1,28 +1,31 @@
{ {
"pm": { "pm": {
"model": "ollama-cloud/minimax-m3:cloud", "model": "ollama-cloud/minimax-m3:cloud",
"fallback": "xai-oc/grok-4.5",
"vision": true, "vision": true,
"mode": "comment", "mode": "comment",
"skills": [ "skills": [
"gitea-api" "gitea-api"
], ],
"desc": "Product manager & orchestrator — plans and picks the dev, hands each finished PR to @qa for review, and reports back to the issue creator (in autopilot it merges approved PRs itself). Works from the issue thread only — comments only, never edits files, never reads the PR diff." "desc": "Product manager & orchestrator — plans and picks the dev, hands each finished PR to @qa for review, and reports back to the issue creator (in autopilot it merges approved PRs itself). Works from the issue thread only — comments only, never edits files, never reads the PR diff. Falls back to xAI grok-4.5 if Ollama Cloud is unavailable/quota-exhausted."
}, },
"junior": { "junior": {
"model": "ollama-cloud/kimi-k2.7-code:cloud", "model": "ollama-cloud/kimi-k2.7-code:cloud",
"fallback": "xai-oc/grok-4.3",
"vision": false, "vision": false,
"mode": "pr", "mode": "pr",
"skills": [], "skills": [],
"desc": "Junior dev — small, low-risk changes (mostly YAML/compose/config). Text-only, cannot read images. Defers complex or image tasks to @senior or @lead." "desc": "Junior dev — small, low-risk changes (mostly YAML/compose/config). Text-only, cannot read images. Defers complex or image tasks to @senior or @lead. Falls back to xAI grok-4.3 if Ollama Cloud fails."
}, },
"senior": { "senior": {
"model": "ollama-cloud/glm-5.2:cloud", "model": "ollama-cloud/glm-5.2:cloud",
"fallback": "xai-oc/grok-4.5",
"vision": false, "vision": false,
"mode": "pr", "mode": "pr",
"skills": [ "skills": [
"gitea-api" "gitea-api"
], ],
"desc": "Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only)." "desc": "Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only). Falls back to xAI grok-4.5 if Ollama Cloud fails."
}, },
"lead": { "lead": {
"model": "xai-oc/grok-4.5", "model": "xai-oc/grok-4.5",
@@ -35,12 +38,13 @@
}, },
"qa": { "qa": {
"model": "ollama-cloud/minimax-m3:cloud", "model": "ollama-cloud/minimax-m3:cloud",
"fallback": "xai-oc/grok-4.5",
"vision": true, "vision": true,
"mode": "comment", "mode": "comment",
"skills": [ "skills": [
"gitea-api" "gitea-api"
], ],
"desc": "QA / reviewer — reviews PRs: reads the diff, drives a headless browser (Playwright) to verify behavior, posts specific recommendations on the PR and the pass/fail verdict on the issue. Never edits code, never merges." "desc": "QA / reviewer — reviews PRs: reads the diff, drives a headless browser (Playwright) to verify behavior, posts specific recommendations on the PR and the pass/fail verdict on the issue. Never edits code, never merges. Falls back to xAI grok-4.5 if Ollama Cloud fails."
}, },
"ops": { "ops": {
"model": "xai-oc/grok-4.5", "model": "xai-oc/grok-4.5",
@@ -53,9 +57,10 @@
}, },
"intern": { "intern": {
"model": "ollama/ornith:35b", "model": "ollama/ornith:35b",
"fallback": "xai-oc/grok-4.3",
"vision": false, "vision": false,
"mode": "pr", "mode": "pr",
"skills": [], "skills": [],
"desc": "Intern — very basic tasks only, routed to the local Ollama model (ornith:35b). Text-only, cannot read images. Escalates anything non-trivial to @junior, @senior or @lead." "desc": "Intern — very basic tasks only, routed to the local Ollama model (ornith:35b). Text-only, cannot read images. Escalates anything non-trivial to @junior, @senior or @lead. Falls back to xAI grok-4.3 if local Ollama is down."
} }
} }
+13 -3
View File
@@ -5,6 +5,14 @@
# NAME SKILLS GITHUB_PATH HOME # NAME SKILLS GITHUB_PATH HOME
set -eu set -eu
# Non-secret diagnostics — prove which provider keys reached the runner (length only).
echo "provider key lengths: OLLAMA_URL=${#OLLAMA_URL} OLLAMA_CLOUD_API_KEY=${#OLLAMA_CLOUD_API_KEY} XAI_API_KEY=${#XAI_API_KEY}"
if [ -z "${XAI_API_KEY:-}" ]; then
echo "WARNING: XAI_API_KEY is empty in this job. xai-oc fallback will fail."
echo "Fix: set Actions secret XAI_API_KEY on the CALLER repo (e.g. ffaerber/homelab),"
echo "not only on gitea/agents. Name must be exact: XAI_API_KEY"
fi
# PIN the opencode version: an unpinned `latest` means a breaking release (CLI flags, or the # PIN the opencode version: an unpinned `latest` means a breaking release (CLI flags, or the
# --format json event schema that build-activity-log.sh parses) breaks every agent in every repo # --format json event schema that build-activity-log.sh parses) breaks every agent in every repo
# at once. Bump deliberately by changing this default (or set OPENCODE_VERSION in the step env). # at once. Bump deliberately by changing this default (or set OPENCODE_VERSION in the step env).
@@ -54,9 +62,11 @@ PERM=$(jq -nc --argjson s "$SKILLS" '
# 1.17.13 — renaming the key to `xai-oc` avoids the collision entirely. See issue #118. # 1.17.13 — renaming the key to `xai-oc` avoids the collision entirely. See issue #118.
# See issue #31. # See issue #31.
AGENTS_JSON="${SCRIPTS:-$(dirname -- "$0")}/agents.json" AGENTS_JSON="${SCRIPTS:-$(dirname -- "$0")}/agents.json"
CLOUD_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama-cloud/")) | sub("^ollama-cloud/";"")] | map({(.):{}}) | add // {}' "$AGENTS_JSON") # Include primary `.model` AND optional `.fallback` so failover models are always registered
LOCAL_MODELS=$(jq -r '[.[] | .model | select(startswith("ollama/")) | sub("^ollama/";"")] | map({(.):{}}) | add // {"ornith:35b":{}}' "$AGENTS_JSON") # in opencode provider maps (issue: Ollama Cloud quota → xAI).
XAI_MODELS=$(jq -r '[.[] | .model | select(startswith("xai-oc/")) | sub("^xai-oc/";"")] | map({(.):{}}) | add // {}' "$AGENTS_JSON") CLOUD_MODELS=$(jq -r '[.[] | (.model, .fallback) | select(type=="string" and startswith("ollama-cloud/")) | sub("^ollama-cloud/";"")] | unique | map({(.):{}}) | add // {}' "$AGENTS_JSON")
LOCAL_MODELS=$(jq -r '[.[] | (.model, .fallback) | select(type=="string" and startswith("ollama/")) | sub("^ollama/";"")] | unique | map({(.):{}}) | add // {"ornith:35b":{}}' "$AGENTS_JSON")
XAI_MODELS=$(jq -r '[.[] | (.model, .fallback) | select(type=="string" and startswith("xai-oc/")) | sub("^xai-oc/";"")] | unique | map({(.):{}}) | add // {}' "$AGENTS_JSON")
jq -n --argjson mcp "$MCP" --argjson perm "$PERM" --argjson cloud "$CLOUD_MODELS" --argjson local "$LOCAL_MODELS" --argjson xai "$XAI_MODELS" --arg url "$OLLAMA_URL" --arg ckey "$OLLAMA_CLOUD_API_KEY" --arg xkey "$XAI_API_KEY" '{ jq -n --argjson mcp "$MCP" --argjson perm "$PERM" --argjson cloud "$CLOUD_MODELS" --argjson local "$LOCAL_MODELS" --argjson xai "$XAI_MODELS" --arg url "$OLLAMA_URL" --arg ckey "$OLLAMA_CLOUD_API_KEY" --arg xkey "$XAI_API_KEY" '{
provider: { provider: {
ollama: {npm:"@ai-sdk/openai-compatible", options:{baseURL:($url+"/v1")}, models:$local}, ollama: {npm:"@ai-sdk/openai-compatible", options:{baseURL:($url+"/v1")}, models:$local},
+5 -3
View File
@@ -43,12 +43,14 @@ if [ -z "$name" ]; then
# Not an agent task (e.g. the gate's contains() matched "@internal"). Skip GRACEFULLY: emit # Not an agent task (e.g. the gate's contains() matched "@internal"). Skip GRACEFULLY: emit
# mode=skip so every later step no-ops — a red run for a non-agent comment is just noise. # mode=skip so every later step no-ops — a red run for a non-agent comment is just noise.
echo "no known agent mentioned (word-boundary) — skipping run" echo "no known agent mentioned (word-boundary) — skipping run"
{ echo "name=none"; echo "model=none"; echo "vision=false"; echo "mode=skip"; echo "skills=[]"; { echo "name=none"; echo "model=none"; echo "fallback="; echo "vision=false"; echo "mode=skip"; echo "skills=[]";
echo "branch=main"; echo "new=false"; echo "autopilot=false"; echo "issnum=$NUM"; } >> "$GITHUB_OUTPUT" echo "branch=main"; echo "new=false"; echo "autopilot=false"; echo "issnum=$NUM"; } >> "$GITHUB_OUTPUT"
exit 0 exit 0
fi fi
fi fi
model=$(jq -r --arg a "$name" '.[$a].model' /tmp/agents.json) model=$(jq -r --arg a "$name" '.[$a].model' /tmp/agents.json)
# Optional provider failover when primary is quota/network-dead (e.g. Ollama Cloud → xAI).
fallback=$(jq -r --arg a "$name" '.[$a].fallback // empty' /tmp/agents.json)
vision=$(jq -r --arg a "$name" '.[$a].vision' /tmp/agents.json) vision=$(jq -r --arg a "$name" '.[$a].vision' /tmp/agents.json)
mode=$(jq -r --arg a "$name" '.[$a].mode' /tmp/agents.json) mode=$(jq -r --arg a "$name" '.[$a].mode' /tmp/agents.json)
# Compact JSON array of the skills this agent may load (scopes permission.skill in install-opencode.sh). # Compact JSON array of the skills this agent may load (scopes permission.skill in install-opencode.sh).
@@ -70,8 +72,8 @@ if [ "$mode" = "pr" ] && [ -z "$IS_PR" ]; then
workmode=discuss workmode=discuss
fi fi
fi fi
echo "Routing to @$name (model=$model vision=$vision mode=$mode workmode=$workmode skills=$skills)" echo "Routing to @$name (model=$model fallback=${fallback:-none} vision=$vision mode=$mode workmode=$workmode skills=$skills)"
{ echo "name=$name"; echo "model=$model"; echo "vision=$vision"; echo "mode=$mode"; echo "workmode=$workmode"; echo "skills=$skills"; } >> "$GITHUB_OUTPUT" { echo "name=$name"; echo "model=$model"; echo "fallback=$fallback"; echo "vision=$vision"; echo "mode=$mode"; echo "workmode=$workmode"; echo "skills=$skills"; } >> "$GITHUB_OUTPUT"
# Act as the agent's own Gitea user when its token is set; else the built-in bot. # Act as the agent's own Gitea user when its token is set; else the built-in bot.
case "$name" in case "$name" in
+18
View File
@@ -186,6 +186,16 @@ rc=1
# instance-wide. 20 min is far above any legitimate attempt. timeout SIGTERMs, then SIGKILLs 30s # instance-wide. 20 min is far above any legitimate attempt. timeout SIGTERMs, then SIGKILLs 30s
# later. rc=124 (timed out) is NOT retried — a hung backend stays hung; fail fast, free the runner. # later. rc=124 (timed out) is NOT retried — a hung backend stays hung; fail fast, free the runner.
AGENT_TIMEOUT="${AGENT_TIMEOUT:-1200}" AGENT_TIMEOUT="${AGENT_TIMEOUT:-1200}"
# Provider failover: when primary model dies on quota/auth/provider errors, switch once to
# FALLBACK (from agents.json) and continue the retry loop. Transient rate-limits still back off
# on the current model first.
FALLBACK_MODEL="${FALLBACK:-}"
fallback_used=0
is_failover_error() {
# Ollama Cloud exhausted / provider hard-fail — switch to fallback rather than thrash.
grep -qiE 'overloaded|429|529|rate.?limit|timeout|ETIMEDOUT|ECONNRESET|EAI_AGAIN|quota|credit|balance|usage.?limit|limit.?exceed|402|403|401|insufficient|out of credits|payment.?required|model_not_found|not found|Unavailable|capacity|ENOTFOUND|ECONNREFUSED' \
/tmp/events.jsonl /tmp/agent_err.log 2>/dev/null
}
for attempt in 1 2 3; do for attempt in 1 2 3; do
echo "opencode attempt $attempt/3 for @$NAME ($MODEL, timeout ${AGENT_TIMEOUT}s)" echo "opencode attempt $attempt/3 for @$NAME ($MODEL, timeout ${AGENT_TIMEOUT}s)"
rc=0 rc=0
@@ -196,6 +206,14 @@ for attempt in 1 2 3; do
echo "--- stderr (trace) ---"; cat /tmp/agent_err.log echo "--- stderr (trace) ---"; cat /tmp/agent_err.log
[ $rc -eq 0 ] && break [ $rc -eq 0 ] && break
if [ $rc -eq 124 ]; then echo "attempt timed out after ${AGENT_TIMEOUT}s — backend hung, not retrying"; break; fi if [ $rc -eq 124 ]; then echo "attempt timed out after ${AGENT_TIMEOUT}s — backend hung, not retrying"; break; fi
if [ $fallback_used -eq 0 ] && [ -n "$FALLBACK_MODEL" ] && [ "$FALLBACK_MODEL" != "$MODEL" ] && is_failover_error; then
echo "primary model failed — failing over to fallback: $FALLBACK_MODEL"
MODEL="$FALLBACK_MODEL"
fallback_used=1
# short pause then use next attempt slot on the fallback provider
sleep 2
continue
fi
if grep -qiE 'overloaded|429|529|rate.?limit|timeout|ETIMEDOUT|ECONNRESET|EAI_AGAIN' /tmp/events.jsonl /tmp/agent_err.log; then if grep -qiE 'overloaded|429|529|rate.?limit|timeout|ETIMEDOUT|ECONNRESET|EAI_AGAIN' /tmp/events.jsonl /tmp/agent_err.log; then
echo "transient error — backing off $((attempt*20))s"; sleep $((attempt * 20)); continue echo "transient error — backing off $((attempt*20))s"; sleep $((attempt * 20)); continue
fi fi