feat: retry on provider errors using --continue#3
Conversation
- Add src/retry.ts with isRetryableError() for transport-signal-only patterns - Add build retry loop in check() with up to 3 retries and exponential backoff - Add continueRun() command to resume previous builds using --continue - Extract handlePostBuild() helper shared between check() and continueRun() - Default skip_permissions to false (security fix) - Fix double writeState call in check() - Remove accidentally committed artifacts from git history Addresses PR review feedback: narrowed retry scope, removed unused deps, fixed security default, cleaned committed artifacts.
There was a problem hiding this comment.
4 issues found across 9 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tsconfig.json">
<violation number="1" location="tsconfig.json:11">
P2: Excluding test files from tsconfig.json removes TypeScript type-checking for tests with no alternative pipeline</violation>
</file>
<file name="src/retry.ts">
<violation number="1" location="src/retry.ts:28">
P2: Retryability classification prioritizes retryable patterns before non-retryable build errors, causing false-positive retries on mixed-error logs.</violation>
</file>
<file name="src/index.ts">
<violation number="1" location="src/index.ts:351">
P1: `continueRun` uses the latest GitHub release instead of the previously interrupted release from persisted state, causing version/env/prompt mismatch when a new release was published since the interrupted run.</violation>
<violation number="2" location="src/index.ts:379">
P1: Non-retryable integration failures no longer trigger desktop notifications, creating an observability regression. Before the refactor, `check()` and `verifyBuild()` unconditionally notified on any error. Now `runOpenCodeWithBuildRetry()` and `verifyBuild()` only notify when `isBuildRetryableError()` returns true, so non-retryable errors bubble up silently.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| } | ||
| } | ||
|
|
||
| async function runOpenCodeWithBuildRetry( |
There was a problem hiding this comment.
P1: Non-retryable integration failures no longer trigger desktop notifications, creating an observability regression. Before the refactor, check() and verifyBuild() unconditionally notified on any error. Now runOpenCodeWithBuildRetry() and verifyBuild() only notify when isBuildRetryableError() returns true, so non-retryable errors bubble up silently.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/index.ts, line 379:
<comment>Non-retryable integration failures no longer trigger desktop notifications, creating an observability regression. Before the refactor, `check()` and `verifyBuild()` unconditionally notified on any error. Now `runOpenCodeWithBuildRetry()` and `verifyBuild()` only notify when `isBuildRetryableError()` returns true, so non-retryable errors bubble up silently.</comment>
<file context>
@@ -315,74 +339,90 @@ async function check(cfg: Cfg, force: boolean) {
+ }
+}
+
+async function runOpenCodeWithBuildRetry(
+ cfg: Cfg,
+ prompt: string,
</file context>
| } | ||
| } | ||
|
|
||
| async function continueRun(cfg: Cfg) { |
There was a problem hiding this comment.
P1: continueRun uses the latest GitHub release instead of the previously interrupted release from persisted state, causing version/env/prompt mismatch when a new release was published since the interrupted run.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/index.ts, line 351:
<comment>`continueRun` uses the latest GitHub release instead of the previously interrupted release from persisted state, causing version/env/prompt mismatch when a new release was published since the interrupted run.</comment>
<file context>
@@ -315,74 +339,90 @@ async function check(cfg: Cfg, force: boolean) {
}
}
+async function continueRun(cfg: Cfg) {
+ const release = await latest(cfg);
+ const prev = await readState(cfg);
</file context>
| @@ -7,5 +7,6 @@ | |||
| "noEmit": true, | |||
There was a problem hiding this comment.
P2: Excluding test files from tsconfig.json removes TypeScript type-checking for tests with no alternative pipeline
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tsconfig.json, line 11:
<comment>Excluding test files from tsconfig.json removes TypeScript type-checking for tests with no alternative pipeline</comment>
<file context>
@@ -7,5 +7,6 @@
},
- "include": ["src/**/*.ts"]
+ "include": ["src/**/*.ts"],
+ "exclude": ["src/**/*.test.ts"]
}
</file context>
| "request aborted", | ||
| "operation aborted", | ||
| ]; | ||
| if (textPatterns.some((p) => content.includes(p))) return true; |
There was a problem hiding this comment.
P2: Retryability classification prioritizes retryable patterns before non-retryable build errors, causing false-positive retries on mixed-error logs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/retry.ts, line 28:
<comment>Retryability classification prioritizes retryable patterns before non-retryable build errors, causing false-positive retries on mixed-error logs.</comment>
<file context>
@@ -0,0 +1,63 @@
+ "request aborted",
+ "operation aborted",
+ ];
+ if (textPatterns.some((p) => content.includes(p))) return true;
+
+ const httpStatusRegex = /(?:http|status|error|response)[\s:]+429\b/;
</file context>
There was a problem hiding this comment.
Pull request overview
Adds automatic retry/resume behavior for transient OpenCode/provider failures by detecting retryable log patterns and re-running opencode run with --continue, plus a new continue command to resume prior sessions.
Changes:
- Introduces
src/retry.ts(log slicing + retryable-error detection) and a Bun test for it. - Refactors
check()to run OpenCode with provider-retry + build/verify retry loops, and addscontinuecommand + shared post-build handler. - Updates config/docs/examples for retry settings and adjusts ignore/tsconfig behavior.
Reviewed changes
Copilot reviewed 6 out of 9 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Excludes *.test.ts from typechecking. |
| src/retry.ts | Implements retryable error detection + log slicing helpers. |
| src/retry.test.ts | Adds Bun tests for isRetryableError(). |
| src/index.ts | Adds retry loops, continue command, post-build helper, and new config keys. |
| README.md | Documents provider-retry behavior and config snippet updates. |
| package.json | Adds empty dependencies object (no functional runtime change). |
| config.example.json | Updates example config with retry settings. |
| bun.lock | Removes committed Bun lockfile. |
| .gitignore | Adds ignores for .omo/, bun.lock, and opencode dirs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import fs from "node:fs/promises"; | ||
| import { readFileSync } from "node:fs"; | ||
| import path from "node:path"; | ||
| import os from "node:os"; |
| import { spawn } from "node:child_process"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { isRetryableError, readLogSlice, fileSize } from "./retry"; | ||
|
|
| async function continueRun(cfg: Cfg) { | ||
| const release = await latest(cfg); | ||
| const prev = await readState(cfg); | ||
| if (!prev.tag) throw new Error("No previous build state found. Run `orw check` first."); | ||
|
|
| } catch (err) { | ||
| lastErr = err; | ||
| if (!await isBuildRetryableError(cfg, err)) { | ||
| throw err; | ||
| } | ||
| if (attempt >= cfg.build_retry_attempts) break; | ||
| out(`Build attempt ${attempt}/${cfg.build_retry_attempts} did not produce verified artifacts, retrying opencode (waiting ${cfg.build_retry_delay_ms / 1000}s)...`); | ||
| await sleep(cfg.build_retry_delay_ms); | ||
| } |
| } catch (err) { | ||
| await notify( | ||
| "OpenCode integration failed", | ||
| `${release.tag_name} did not produce verified artifacts. See ${log}.`, | ||
| ); | ||
| if (await isBuildRetryableError(cfg, err)) { | ||
| await notify( | ||
| "OpenCode integration failed", | ||
| `${release.tag_name} did not produce verified artifacts. See ${log}.`, | ||
| ); | ||
| } | ||
| throw err; |
| lastErr = err; | ||
| const afterSize = await fileSize(log); | ||
| const attemptLog = readLogSlice(log, beforeSize); | ||
| if (!isRetryableError(err, attemptLog)) { |
| if (textPatterns.some((p) => content.includes(p))) return true; | ||
|
|
||
| const httpStatusRegex = /(?:http|status|error|response)[\s:]+429\b/; | ||
| const httpServerErrorRegex = /(?:http|status|error|response)[\s:]+50[23]\b/; | ||
| if (httpStatusRegex.test(content) || httpServerErrorRegex.test(content)) return true; | ||
|
|
||
| const nonRetryableBuildPatterns = [ | ||
| "build failed", | ||
| "compilation failed", | ||
| "typescript error", | ||
| "error ts", | ||
| "expected a string starting with", | ||
| ]; | ||
| const hasBuildError = nonRetryableBuildPatterns.some((p) => content.includes(p)); | ||
| if (hasBuildError) return false; | ||
|
|
||
| return false; |
| ### Retry on provider errors | ||
|
|
||
| When `opencode run` fails with a retryable provider error (e.g. "Provider returned error", rate limits, 5xx responses), ORW automatically retries using `opencode run --continue` to resume the last session. | ||
|
|
||
| - `retry_attempts`: max number of attempts including the first one (default `3`) | ||
| - `retry_delay_ms`: delay between retries in milliseconds (default `10000`) | ||
|
|
| function helpText() { | ||
| return `OpenCode Release Watch\n\nUsage:\n orw [--config <path>] [command] [options]\n orw --help\n\nCommands:\n init Create orw.config.json in the current directory\n preview Print the integration prompt for the latest release\n check Build the latest release if needed; default command\n status Print the last successful build/install state\n install-ready Install the last verified artifacts\n install-when-closed Wait for OpenCode to quit, then install\n launchd install Install the macOS launchd scheduler\n launchd uninstall Remove the macOS launchd scheduler\n\nOptions:\n -c, --config <path> Use a specific config file\n --force Rebuild even if the latest release was processed\n --wait-for-opencode With install-ready, wait until OpenCode quits\n -h, --help Show this help\n`; | ||
| return `OpenCode Release Watch\n\nUsage:\n orw [--config <path>] [command] [options]\n orw --help\n\nCommands:\n init Create orw.config.json in the current directory\n preview Print the integration prompt for the latest release\n check Build the latest release if needed; default command\n status Print the last successful build/install state\n install-ready Install the last verified artifacts\n install-when-closed Wait for OpenCode to quit, then install\n launchd install Install the macOS launchd scheduler\n launchd uninstall Remove the macOS launchd scheduler\n continue Resume the last interrupted opencode session\n\nOptions:\n -c, --config <path> Use a specific config file\n --force Rebuild even if the latest release was processed\n --wait-for-opencode With install-ready, wait until OpenCode quits\n -h, --help Show this help\n\nConfig (orw.config.json):\n retry_attempts Number of retry attempts on provider error (default: 3)\n retry_delay_ms Delay between retries in milliseconds (default: 10000)\n`; | ||
| } |
| "install_desktop": false, | ||
| "notify_timeout": 120 | ||
| "notify_timeout": 120, | ||
| "retry_attempts": 3, | ||
| "retry_delay_ms": 10000 |
| at: new Date().toISOString(), | ||
| }; | ||
| } catch (err) { | ||
| await notify( | ||
| "OpenCode integration failed", | ||
| `${release.tag_name} did not produce verified artifacts. See ${log}.`, | ||
| ); | ||
| if (await isBuildRetryableError(cfg, err)) { | ||
| await notify( | ||
| "OpenCode integration failed", | ||
| `${release.tag_name} did not produce verified artifacts. See ${log}.`, | ||
| ); | ||
| } | ||
| throw err; | ||
| } |
There was a problem hiding this comment.
Missing failure notifications for non-ENOENT verification errors
The original verifyBuild always sent an "OpenCode integration failed" notification on any catch. The new code only notifies when isBuildRetryableError returns true, which is limited to the ENOENT case (CLI binary missing). A version-mismatch failure ("Built CLI reported version X, expected Y") returns false from isBuildRetryableError, so no notification is sent and the error propagates silently through runOpenCodeWithBuildRetry (which immediately rethrows on false) all the way to the top-level error handler — the user only sees stderr output, no macOS notification. The same gap applies to non-retryable opencode run failures: the old check() had an explicit notify() in its catch around run(); the new runOpenCodeWithRetry has no notification path at all. For users relying on launchd / background scheduling, these failures will go unnoticed.
| let lastErr: unknown; | ||
| let logOffset = 0; | ||
|
|
||
| for (let attempt = 1; attempt <= cfg.retry_attempts; attempt++) { | ||
| const isRetry = attempt > 1; | ||
| if (isRetry) { | ||
| out(`Retry attempt ${attempt}/${cfg.retry_attempts} after provider error (waiting ${cfg.retry_delay_ms / 1000}s)...`); | ||
| await note(log, `\n--- Retry attempt ${attempt}/${cfg.retry_attempts} ---\n`); | ||
| await sleep(cfg.retry_delay_ms); | ||
| } | ||
|
|
||
| const beforeSize = await fileSize(log); | ||
| const args = [cfg.opencode_bin, "run"]; | ||
| if (cfg.skip_permissions) args.push("--dangerously-skip-permissions"); | ||
| if (isRetry || forceContinue) args.push("--continue"); | ||
| args.push("--agent", cfg.agent, "--model", cfg.model, prompt); | ||
|
|
||
| try { | ||
| await run(args, { | ||
| cwd: cfg.work_repo, | ||
| log, | ||
| env: envWithFlag, | ||
| }); | ||
| return; | ||
| } catch (err) { | ||
| lastErr = err; | ||
| const afterSize = await fileSize(log); | ||
| const attemptLog = readLogSlice(log, beforeSize); |
There was a problem hiding this comment.
Unused
logOffset and afterSize variables
logOffset is declared but never read or incremented; the per-attempt log slicing is handled correctly via beforeSize / readLogSlice. afterSize is computed but also never consumed. Both are dead code and can be removed.
| let lastErr: unknown; | |
| let logOffset = 0; | |
| for (let attempt = 1; attempt <= cfg.retry_attempts; attempt++) { | |
| const isRetry = attempt > 1; | |
| if (isRetry) { | |
| out(`Retry attempt ${attempt}/${cfg.retry_attempts} after provider error (waiting ${cfg.retry_delay_ms / 1000}s)...`); | |
| await note(log, `\n--- Retry attempt ${attempt}/${cfg.retry_attempts} ---\n`); | |
| await sleep(cfg.retry_delay_ms); | |
| } | |
| const beforeSize = await fileSize(log); | |
| const args = [cfg.opencode_bin, "run"]; | |
| if (cfg.skip_permissions) args.push("--dangerously-skip-permissions"); | |
| if (isRetry || forceContinue) args.push("--continue"); | |
| args.push("--agent", cfg.agent, "--model", cfg.model, prompt); | |
| try { | |
| await run(args, { | |
| cwd: cfg.work_repo, | |
| log, | |
| env: envWithFlag, | |
| }); | |
| return; | |
| } catch (err) { | |
| lastErr = err; | |
| const afterSize = await fileSize(log); | |
| const attemptLog = readLogSlice(log, beforeSize); | |
| let lastErr: unknown; | |
| for (let attempt = 1; attempt <= cfg.retry_attempts; attempt++) { | |
| const isRetry = attempt > 1; | |
| if (isRetry) { | |
| out(`Retry attempt ${attempt}/${cfg.retry_attempts} after provider error (waiting ${cfg.retry_delay_ms / 1000}s)...`); | |
| await note(log, `\n--- Retry attempt ${attempt}/${cfg.retry_attempts} ---\n`); | |
| await sleep(cfg.retry_delay_ms); | |
| } | |
| const beforeSize = await fileSize(log); | |
| const args = [cfg.opencode_bin, "run"]; | |
| if (cfg.skip_permissions) args.push("--dangerously-skip-permissions"); | |
| if (isRetry || forceContinue) args.push("--continue"); | |
| args.push("--agent", cfg.agent, "--model", cfg.model, prompt); | |
| try { | |
| await run(args, { | |
| cwd: cfg.work_repo, | |
| log, | |
| env: envWithFlag, | |
| }); | |
| return; | |
| } catch (err) { | |
| lastErr = err; | |
| const attemptLog = readLogSlice(log, beforeSize); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| import fs from "node:fs/promises"; | ||
| import { readFileSync } from "node:fs"; | ||
| import path from "node:path"; |
There was a problem hiding this comment.
readFileSync was likely imported when readLogSlice was still implemented inline in this file. After the extraction to src/retry.ts, it is no longer used here and can be removed.
| import fs from "node:fs/promises"; | |
| import { readFileSync } from "node:fs"; | |
| import path from "node:path"; | |
| import fs from "node:fs/promises"; | |
| import path from "node:path"; |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| async function isBuildRetryableError(cfg: Cfg, err: unknown): Promise<boolean> { | ||
| if (!(err instanceof Error)) return false; | ||
| const msg = err.message.toLowerCase(); | ||
| const code = "code" in err ? err.code : undefined; | ||
|
|
||
| if (code === "ENOENT") { | ||
| const cli = cliPath(cfg); | ||
| return !(await exists(cli)); | ||
| } | ||
|
|
||
| const buildErrorPatterns = [ | ||
| "expected a string starting with", | ||
| "build failed", | ||
| "compilation failed", | ||
| "typescript error", | ||
| "error ts", | ||
| "bun run build", | ||
| ]; | ||
| return buildErrorPatterns.some((p) => msg.includes(p)); | ||
| } |
There was a problem hiding this comment.
isBuildRetryableError build-pattern branch matches dead paths
The function checks err.message.toLowerCase() against patterns like "build failed", "compilation failed", "typescript error", etc. In practice, every error thrown by run() has the format "${cmd[0]} exited with ${code}" (e.g. "opencode exited with 1"), and the version-mismatch error thrown by verifyBuild reads "Built CLI reported version X, expected Y". Neither ever contains the listed patterns, so the buildErrorPatterns.some(...) return is unreachable dead code. Only the code === "ENOENT" branch ever fires.
| async function continueRun(cfg: Cfg) { | ||
| const release = await latest(cfg); | ||
| const prev = await readState(cfg); | ||
| if (!prev.tag) throw new Error("No previous build state found. Run `orw check` first."); | ||
|
|
||
| const free = await hold(cfg, true); | ||
| try { | ||
| const log = prev.log ?? path.join( | ||
| logDir(cfg), | ||
| `${stamp()}-${release.tag_name.replaceAll("/", "-")}-continue.log`, | ||
| ); | ||
| await fs.mkdir(path.dirname(log), { recursive: true }); | ||
| await note(log, `\n--- Resuming: continue run for ${release.tag_name} ---\n`); | ||
|
|
||
| const env = releaseEnv(release); | ||
| const sources = resolveSources(cfg); | ||
| const prompt = await render(cfg, sources, release); | ||
|
|
||
| out(`Resuming last opencode session for ${release.tag_name}...`); | ||
| const next = await runOpenCodeWithBuildRetry(cfg, prompt, env, log, release, true); | ||
| await writeState(cfg, next); | ||
| await handlePostBuild(cfg, next, release.tag_name); | ||
| out(`Continue completed for ${release.tag_name}`); | ||
| } finally { | ||
| await free(); | ||
| } | ||
| } |
There was a problem hiding this comment.
continueRun does not validate that the stored tag matches the latest release
continueRun fetches the latest release but only checks that prev.tag is non-empty — it never verifies prev.tag === release.tag_name. If a new release has been published since the original failed run, the command will append a --continue session targeting the old opencode state while passing environment variables (and a prompt) from the newer release, producing a mismatch between what the session resumes and what it is being asked to build.
| const httpServerErrorRegex = /(?:http|status|error|response)[\s:]+50[23]\b/; | ||
| if (httpStatusRegex.test(content) || httpServerErrorRegex.test(content)) return true; |
There was a problem hiding this comment.
HTTP 500 detection gap in
httpServerErrorRegex
httpServerErrorRegex covers only 50[23] (502/503). A bare "HTTP 500" or "status 500" log line (without the "internal server error" suffix matched by the text pattern on line 22) will not be detected as retryable. Provider APIs frequently surface generic 500s, and those would silently fall through the retry check.
Summary
Automatically retry failed OpenCode builds when the failure is likely transient (provider/rate-limit errors), using
--continueto resume the build instead of starting over.Changes
src/retry.tswithisRetryableError()— detects retryable errors from build log (rate limit, provider errors, transport aborts)check()— up to 3 retries with exponential backoffcontinueRun()command — resumes a previous build using--continuehandlePostBuild()helper shared betweencheck()andcontinueRun()for install hint outputskip_permissionstofalse(security fix from PR review)writeStatecall incheck()Review Addressed
All feedback from the previous PR review has been incorporated:
skip_permissionsdefaults tofalseisRetryableErrornarrowed to transport-signal-only abort patternswriteStatecall removedNeed help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Automatically retries transient provider errors and resumes OpenCode builds using --continue, reducing failed runs and avoiding starting over. Adds a continue command and configurable retry settings.
New Features
opencode runon provider/rate-limit/network errors using--continue(per-attempt log slicing).continuecommand to resume the last session.retry_attempts,retry_delay_ms,build_retry_attempts,build_retry_delay_ms(defaults: 3, 10000, 3, 30000).handlePostBuild(); updated help text and README.isRetryableError().Bug Fixes
skip_permissionstofalse.writeStatecall incheck()..omo/,bun.lock,opencode,opencode-shallow; removed committedbun.lock.package.json; exclude tests intsconfig.Written for commit 0624f5b. Summary will update on new commits.
Greptile Summary
This PR adds automatic retry logic for transient provider errors during OpenCode builds, a new
continuecommand to manually resume an interrupted session, and refactors post-build handling into a sharedhandlePostBuildhelper. It also cleans up accidentally committed artifacts from git history and fixes theskip_permissionsdefault tofalse.runOpenCodeWithRetryloop (up toretry_attempts, default 3) that retries on provider/network errors detected via log scanning, and an outerrunOpenCodeWithBuildRetryloop (up tobuild_retry_attempts, default 3) that retries when artifact verification finds the CLI binary missing.check()emitted for all errors;verifyBuildnow only notifies on ENOENT, so non-retryable failures such as version mismatches silently skip the macOS notification.src/retry.tsis well-tested, but two unused variables (logOffset,afterSize) and one unused import (readFileSync) are left over insrc/index.ts.Confidence Score: 3/5
The retry and continue machinery works correctly for the happy path, but the refactoring accidentally dropped failure notifications for non-ENOENT verification errors — a regression that affects the operator experience under launchd/background scheduling.
The notification regression in verifyBuild means that deterministic failures such as a version mismatch between the built CLI and the expected release tag will no longer surface as macOS notifications; only the error message printed to stderr remains. For users running orw as a background launchd job this makes silent build failures much harder to detect. Additionally, src/index.ts has a dead-variable pair and an unused import that suggests the refactor was not fully cleaned up. The new src/retry.ts module and its tests are solid and well-scoped.
src/index.ts — the notification regression and dead-code cleanup both live here; src/retry.ts is clean and needs no further attention.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant CLI as orw CLI participant OWBR as runOpenCodeWithBuildRetry participant OWRR as runOpenCodeWithRetry participant OC as opencode run participant VB as verifyBuild CLI->>OWBR: check() / continueRun() loop build_retry_attempts (default 3) OWBR->>OWRR: runOpenCodeWithRetry(forceContinue) loop retry_attempts (default 3) OWRR->>OC: opencode run [--continue] --agent --model alt success OC-->>OWRR: exit 0 OWRR-->>OWBR: return else retryable provider error (rate limit, 5xx, network) OC-->>OWRR: exit non-0 Note over OWRR: isRetryableError checks log slice OWRR->>OWRR: sleep(retry_delay_ms), retry with --continue else non-retryable error OC-->>OWRR: exit non-0 OWRR-->>OWBR: throw end end OWBR->>VB: verifyBuild() alt artifacts verified VB-->>OWBR: State OWBR-->>CLI: State CLI->>CLI: writeState + handlePostBuild else ENOENT (CLI missing) VB-->>OWBR: throw (+ notify) OWBR->>OWBR: sleep(build_retry_delay_ms), retry else non-retryable (version mismatch) VB-->>OWBR: throw (no notify) OWBR-->>CLI: throw immediately end end%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant CLI as orw CLI participant OWBR as runOpenCodeWithBuildRetry participant OWRR as runOpenCodeWithRetry participant OC as opencode run participant VB as verifyBuild CLI->>OWBR: check() / continueRun() loop build_retry_attempts (default 3) OWBR->>OWRR: runOpenCodeWithRetry(forceContinue) loop retry_attempts (default 3) OWRR->>OC: opencode run [--continue] --agent --model alt success OC-->>OWRR: exit 0 OWRR-->>OWBR: return else retryable provider error (rate limit, 5xx, network) OC-->>OWRR: exit non-0 Note over OWRR: isRetryableError checks log slice OWRR->>OWRR: sleep(retry_delay_ms), retry with --continue else non-retryable error OC-->>OWRR: exit non-0 OWRR-->>OWBR: throw end end OWBR->>VB: verifyBuild() alt artifacts verified VB-->>OWBR: State OWBR-->>CLI: State CLI->>CLI: writeState + handlePostBuild else ENOENT (CLI missing) VB-->>OWBR: throw (+ notify) OWBR->>OWBR: sleep(build_retry_delay_ms), retry else non-retryable (version mismatch) VB-->>OWBR: throw (no notify) OWBR-->>CLI: throw immediately end endReviews (1): Last reviewed commit: "feat: retry on provider errors using --c..." | Re-trigger Greptile