Skip to content

CLI: one-shot 'relayfile read' does not retry 429 workspace_busy (Retry-After is parsed but unused) - #527

Closed
agent-relay-code[bot] wants to merge 4 commits into
mainfrom
relayflow/relayfile-garden-relayfile-2-9119454f-d1cf0e5e
Closed

agent-relay-code[bot] wants to merge 4 commits into
mainfrom
relayflow/relayfile-garden-relayfile-2-9119454f-d1cf0e5e

Conversation

@agent-relay-code

@agent-relay-code agent-relay-code Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

PR summary

What changed

  • Added bounded retries for transient 429 and 503 responses to the one-shot tree, read, export, and status GET commands.
  • Reused the CLI's existing polite polling implementation for Retry-After, exponential fallback, jitter, rate limiting, and context-aware waits, with three attempts and a 60-second maximum delay.
  • Added --no-retry to each affected command for callers that require immediate failure.
  • Routed retry notices to stderr so file, JSON, and export payloads on stdout remain clean.
  • Documented the retry behavior and opt-out flag.

Tests

  • Added deterministic regression coverage for 429 workspace_busy recovery, 503, exhaustion, non-retryable errors, delay clamping, cancellation, stdout/stderr separation, and read --no-retry request count.
  • go test ./cmd/relayfile-cli -count=1
  • scripts/check-contract-surface.sh
  • git diff --check

Checks

Relayflow ran this repository's checks (.relayflow/check.sh) and they passed.

What ran (.relayflow/check.sh)
#!/bin/sh
set -e

# Match CI's actions/setup-go step when this repair environment does not
# already provide Go. Keep the downloaded toolchain local to .relayflow.
if ! command -v go >/dev/null 2>&1; then
  GO_VERSION=1.22.12
  GO_ROOT="$PWD/.relayflow/tools/go-$GO_VERSION"
  if [ ! -x "$GO_ROOT/bin/go" ]; then
    mkdir -p "$PWD/.relayflow/tools"
    archive="$PWD/.relayflow/tools/go-$GO_VERSION.linux-amd64.tar.gz"
    curl -fsSL "https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz" -o "$archive"
    extract_dir="$PWD/.relayflow/tools/go-$GO_VERSION.extract"
    rm -rf "$extract_dir"
    mkdir -p "$extract_dir"
    tar -xzf "$archive" -C "$extract_dir"
    mv "$extract_dir/go" "$GO_ROOT"
    rmdir "$extract_dir"
  fi
  PATH="$GO_ROOT/bin:$PATH"
  export PATH
fi

# Match the toolchain-independent setup performed by the pull-request jobs.
npm ci
(
  cd packages/sdk/typescript
  npm ci
)

# GitHub Actions pins Node 22 and Bun 1.3.14 for the SDK checks.
npm install --no-save --no-package-lock node@22 bun@1.3.14
PATH="$PWD/node_modules/.bin:$PATH"
export PATH

# Run generated-source checks and builds before the test suites.
npm run codegen --workspace=@relayfile/client
git diff --exit-code -- packages/client/src/generated/control-plane.ts
npm run build --workspace=packages/core
npm run build --workspace=packages/sdk/typescript
npm run build --workspace=@relayfile/client

mkdir -p bin
go build -o bin/relayfile ./cmd/relayfile
go build -o bin/relayfile-mount ./cmd/relayfile-mount
go build -o bin/relayfile-cli ./cmd/relayfile-cli

go test ./...

(
  cd packages/sdk/typescript
  npx tsc --noEmit
  npm run test
)
npm run test:bundle:bun --workspace=packages/sdk/typescript

npm run typecheck --workspace=@relayfile/client
npm run test --workspace=@relayfile/client

CI=true npx tsx scripts/e2e.ts --ci
npm run test:release

./scripts/check-contract-surface.sh
node --test scripts/validate-mount-qualification-workflow.test.mjs
node scripts/validate-mount-qualification-workflow.mjs
./scripts/check-publish-workflow.sh
./scripts/check-publish-workflow.sh .github/workflows/publish-python.yml
./scripts/test-check-publish-workflow.sh

if [ -d packages/server ] && [ -f packages/server/tsconfig.json ]; then
  if [ -f packages/server/package-lock.json ]; then
    (cd packages/server && npm ci)
  fi
  (cd packages/server && npx tsc --noEmit)
fi

# Provider-backed evals require OPENROUTER_API_KEY, so they are not run here.
# Publish and mount-qualification jobs require GitHub release/artifact services,
# mutate release versions, or deploy packages, so their remote-only steps are omitted.

Fixes #526


Note

Low Risk
CLI-only resilience for read paths; default behavior adds waits on transient errors but preserves prior failure modes via --no-retry.

Overview
Adds automatic bounded retries for transient 429/503 responses on one-shot workspace GET flows (tree, read, export, status, and pull’s provider discovery via sync status).

A new oneShotGETRetryPolicy wraps workspace JSON/byte reads with up to three attempts, honoring Retry-After, exponential backoff with jitter (capped at 60s), and human-readable wait lines on stderr so stdout payloads stay clean. --no-retry on each affected command restores immediate failure for scripts that need it.

Command handlers and the published CLI spec/docs are updated to document the flag and default retry behavior; regression tests cover workspace_busy, delay clamping, cancellation, stderr separation, and --no-retry.

Reviewed by Cursor Bugbot for commit 2b1bd9b. Bugbot is set up for automated code reviews on this repo. Configure here.


Agent Relay sessions

  • claude session 03e53226 · contributor · ran gh pr commands · last active 2026-10-05

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cea10c96-eddf-4a35-8719-ab13670720cc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread cmd/relayfile-cli/one_shot_retry.go Outdated
}
var apiErr *apiError
_ = errors.As(lastErr, &apiErr)
return pollResult{err: lastErr, httpStatus: apiErr.StatusCode, retryAfter: apiErr.RetryAfter}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Busy workspace retries too early

For a 429 workspace_busy response, politePoll can shorten the advertised Retry-After with negative jitter. The next request can hit the still-busy workspace and exhaust the retry budget.

Learn more

The one-shot retry passes the parsed server delay to politePoll. That poller clamps the delay and then applies positive or negative jitter. Negative jitter sends a request before a busy workspace's advertised recovery time, even though the retry is intended to respect the server's hint.

Example: A workspace answers 429 with Retry-After: 7. The poller's 20% jitter can schedule the next request after 5.6 seconds instead of at least seven seconds. Repeated early retries can use all three attempts before the workspace is ready.

Recommended fix: Treat Retry-After as a minimum wait when scheduling one-shot retries. Apply jitter only as extra time, or take the maximum of the server delay and the jittered client backoff, while retaining the intended upper bound.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread cmd/relayfile-cli/one_shot_retry.go Outdated
}
var apiErr *apiError
_ = errors.As(lastErr, &apiErr)
return pollResult{err: lastErr, httpStatus: apiErr.StatusCode, retryAfter: apiErr.RetryAfter}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Service retries ignore recovery time

For a 503 with Retry-After, politePoll ignores the server delay and retries after its one-second fallback. All three attempts can finish before the service's advertised recovery time.

Learn more

The one-shot policy retries both 429 and 503 errors. It forwards the parsed delay to politePoll, but that loop applies retryAfter only when httpStatus == 429. A 503 always takes exponential backoff regardless of its header.

Example: A server responds 503 with Retry-After: 30. The CLI retries at roughly one and then three seconds, returns the final 503, and never waits for the advertised 30-second recovery.

Recommended fix: Extend the wait calculation for one-shot requests to honor valid Retry-After on 503 as well as 429, preserving the bounded delay and retry count.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusTooManyRequests && apiErr.Code == "workspace_busy" {
return "workspace busy"
}
return "service unavailable"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Rate limits mislabeled as outages

When a 429 rate_limited response triggers a retry, oneShotRetryLabel prints “service unavailable.” Users see an outage notice instead of the rate limit that blocked their read.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cda02fb. Configure here.

Comment thread cmd/relayfile-cli/one_shot_retry.go Outdated
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-10-03T23-26-12-419Z-HEAD-provider
Mode: provider
Git SHA: b6bd785

Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0

Human Review Cases

No reviewable human-review cases captured Relayfile output.

@khaliqgant

Copy link
Copy Markdown
Member

Closing as redundant: #526 was already fixed and closed by #528 (triage 2026-10-05).

@khaliqgant khaliqgant closed this Oct 5, 2026
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.

CLI: one-shot 'relayfile read' does not retry 429 workspace_busy (Retry-After is parsed but unused)

1 participant