Skip to content

feat: retry on provider errors using --continue#3

Open
jc01rho wants to merge 1 commit into
cortexkit:mainfrom
jc01rho:feat/retry-continue-upstream
Open

feat: retry on provider errors using --continue#3
jc01rho wants to merge 1 commit into
cortexkit:mainfrom
jc01rho:feat/retry-continue-upstream

Conversation

@jc01rho

@jc01rho jc01rho commented Jun 21, 2026

Copy link
Copy Markdown

Summary

Automatically retry failed OpenCode builds when the failure is likely transient (provider/rate-limit errors), using --continue to resume the build instead of starting over.

Changes

  • Add src/retry.ts with isRetryableError() — detects retryable errors from build log (rate limit, provider errors, transport aborts)
  • Add build retry loop in check() — up to 3 retries with exponential backoff
  • Add continueRun() command — resumes a previous build using --continue
  • Extract handlePostBuild() helper shared between check() and continueRun() for install hint output
  • Default skip_permissions to false (security fix from PR review)
  • Fix double writeState call in check()
  • Clean up git history — remove accidentally committed artifacts (bun.lock, opencode submodule, .omo session files)

Review Addressed

All feedback from the previous PR review has been incorporated:

  • Removed committed artifacts from git history
  • Removed unused runtime dependencies from package.json
  • skip_permissions defaults to false
  • isRetryableError narrowed to transport-signal-only abort patterns
  • Double writeState call removed

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with 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

    • Retries opencode run on provider/rate-limit/network errors using --continue (per-attempt log slicing).
    • New continue command to resume the last session.
    • Build-level retry when artifacts aren’t verified; waits between attempts.
    • Config keys: retry_attempts, retry_delay_ms, build_retry_attempts, build_retry_delay_ms (defaults: 3, 10000, 3, 30000).
    • Extracted handlePostBuild(); updated help text and README.
    • Tests added for isRetryableError().
  • Bug Fixes

    • Default skip_permissions to false.
    • Removed double writeState call in check().
    • Clean repo: ignore .omo/, bun.lock, opencode, opencode-shallow; removed committed bun.lock.
    • Removed unused runtime deps from package.json; exclude tests in tsconfig.

Written for commit 0624f5b. Summary will update on new commits.

Review in cubic

Greptile Summary

This PR adds automatic retry logic for transient provider errors during OpenCode builds, a new continue command to manually resume an interrupted session, and refactors post-build handling into a shared handlePostBuild helper. It also cleans up accidentally committed artifacts from git history and fixes the skip_permissions default to false.

  • Two nested retry loops are introduced: an inner runOpenCodeWithRetry loop (up to retry_attempts, default 3) that retries on provider/network errors detected via log scanning, and an outer runOpenCodeWithBuildRetry loop (up to build_retry_attempts, default 3) that retries when artifact verification finds the CLI binary missing.
  • The refactoring removes the failure notification that the original check() emitted for all errors; verifyBuild now only notifies on ENOENT, so non-retryable failures such as version mismatches silently skip the macOS notification.
  • src/retry.ts is well-tested, but two unused variables (logOffset, afterSize) and one unused import (readFileSync) are left over in src/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

Filename Overview
src/index.ts Core orchestration file — adds retry loops, continueRun command, and handlePostBuild helper. Contains a notification regression (non-ENOENT verification failures no longer notify), two unused variables (logOffset, afterSize), an unused import (readFileSync), and a continueRun version-mismatch gap.
src/retry.ts New module with isRetryableError, readLogSlice, and fileSize helpers. Logic is well-scoped; minor gap where bare "HTTP 500" (without "internal server error" suffix) is not matched by httpServerErrorRegex.
src/retry.test.ts New test file with good coverage of isRetryableError edge cases including bare-abort rejection, HTTP status context requirements, and per-attempt log-slicing isolation.
.gitignore Adds entries to ignore previously committed artifacts: .omo/, bun.lock, opencode, and opencode-shallow directories.
package.json Adds an empty "dependencies": {} object — harmless but unnecessary boilerplate.

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
Loading
%%{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
    end
Loading

Reviews (1): Last reviewed commit: "feat: retry on provider errors using --c..." | Re-trigger Greptile

Greptile also left 6 inline comments on this PR.

- 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.
Copilot AI review requested due to automatic review settings June 21, 2026 09:16

@cubic-dev-ai cubic-dev-ai 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.

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

Comment thread src/index.ts
}
}

async function runOpenCodeWithBuildRetry(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread src/index.ts
}
}

async function continueRun(cfg: Cfg) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread tsconfig.json
@@ -7,5 +7,6 @@
"noEmit": true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread src/retry.ts
"request aborted",
"operation aborted",
];
if (textPatterns.some((p) => content.includes(p))) return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Copilot AI 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.

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 adds continue command + 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.

Comment thread src/index.ts
Comment on lines 3 to 6
import fs from "node:fs/promises";
import { readFileSync } from "node:fs";
import path from "node:path";
import os from "node:os";
Comment thread src/index.ts
Comment on lines 8 to 11
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { isRetryableError, readLogSlice, fileSize } from "./retry";

Comment thread src/index.ts
Comment on lines +351 to +355
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.");

Comment thread src/index.ts
Comment on lines +392 to +400
} 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);
}
Comment thread src/index.ts
Comment on lines 450 to 457
} 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;
Comment thread src/index.ts
Comment on lines +494 to +497
lastErr = err;
const afterSize = await fileSize(log);
const attemptLog = readLogSlice(log, beforeSize);
if (!isRetryableError(err, attemptLog)) {
Comment thread src/retry.ts
Comment on lines +28 to +44
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;
Comment thread README.md
Comment on lines +103 to +109
### 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`)

Comment thread src/index.ts
Comment on lines 189 to 191
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`;
}
Comment thread config.example.json
Comment on lines 15 to +18
"install_desktop": false,
"notify_timeout": 120
"notify_timeout": 120,
"retry_attempts": 3,
"retry_delay_ms": 10000
Comment thread src/index.ts
Comment on lines 448 to 458
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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.

Comment thread src/index.ts
Comment on lines +469 to +496
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Suggested change
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!

Comment thread src/index.ts
Comment on lines 3 to 5
import fs from "node:fs/promises";
import { readFileSync } from "node:fs";
import path from "node:path";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unused readFileSync import

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.

Suggested change
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!

Comment thread src/index.ts
Comment on lines +405 to +424
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));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Comment thread src/index.ts
Comment on lines +351 to +377
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();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Comment thread src/retry.ts
Comment on lines +31 to +32
const httpServerErrorRegex = /(?:http|status|error|response)[\s:]+50[23]\b/;
if (httpStatusRegex.test(content) || httpServerErrorRegex.test(content)) return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

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.

2 participants