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

@jc01rhojc01rho 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 CodesmithAutofix 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

FilenameOverview
src/index.tsCore 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.tsNew 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.tsNew test file with good coverage of isRetryableError edge cases including bare-abort rejection, HTTP status context requirements, and per-attempt log-slicing isolation.
.gitignoreAdds entries to ignore previously committed artifacts: .omo/, bun.lock, opencode, and opencode-shallow directories.
package.jsonAdds 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.
CopilotAI review requested due to automatic review settings June 21, 2026 09:16

@cubic-dev-aicubic-dev-aiBot 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 threadsrc/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 threadsrc/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 threadtsconfig.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 threadsrc/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>

CopilotAI 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
FileDescription
tsconfig.jsonExcludes *.test.ts from typechecking.
src/retry.tsImplements retryable error detection + log slicing helpers.
src/retry.test.tsAdds Bun tests for isRetryableError().
src/index.tsAdds retry loops, continue command, post-build helper, and new config keys.
README.mdDocuments provider-retry behavior and config snippet updates.
package.jsonAdds empty dependencies object (no functional runtime change).
config.example.jsonUpdates example config with retry settings.
bun.lockRemoves committed Bun lockfile.
.gitignoreAdds ignores for .omo/, bun.lock, and opencode dirs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/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 threadsrc/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 threadsrc/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 threadsrc/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 threadsrc/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 threadsrc/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 threadsrc/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 threadREADME.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 threadsrc/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 threadconfig.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 threadsrc/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.

P1Missing 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 threadsrc/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.

P2Unused 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
letlastErr: unknown;
letlogOffset=0;
for(letattempt=1;attempt<=cfg.retry_attempts;attempt++){
constisRetry=attempt>1;
if(isRetry){
out(`Retry attempt ${attempt}/${cfg.retry_attempts} after provider error (waiting ${cfg.retry_delay_ms/1000}s)...`);
awaitnote(log,`\n--- Retry attempt ${attempt}/${cfg.retry_attempts} ---\n`);
awaitsleep(cfg.retry_delay_ms);
}
constbeforeSize=awaitfileSize(log);
constargs=[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{
awaitrun(args,{
cwd: cfg.work_repo,
log,
env: envWithFlag,
});
return;
}catch(err){
lastErr=err;
constafterSize=awaitfileSize(log);
constattemptLog=readLogSlice(log,beforeSize);
letlastErr: unknown;
for(letattempt=1;attempt<=cfg.retry_attempts;attempt++){
constisRetry=attempt>1;
if(isRetry){
out(`Retry attempt ${attempt}/${cfg.retry_attempts} after provider error (waiting ${cfg.retry_delay_ms/1000}s)...`);
awaitnote(log,`\n--- Retry attempt ${attempt}/${cfg.retry_attempts} ---\n`);
awaitsleep(cfg.retry_delay_ms);
}
constbeforeSize=awaitfileSize(log);
constargs=[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{
awaitrun(args,{
cwd: cfg.work_repo,
log,
env: envWithFlag,
});
return;
}catch(err){
lastErr=err;
constattemptLog=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 threadsrc/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.

P2Unused 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
importfsfrom"node:fs/promises";
import{readFileSync}from"node:fs";
importpathfrom"node:path";
importfsfrom"node:fs/promises";
importpathfrom"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 threadsrc/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.

P2isBuildRetryableError 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 threadsrc/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.

P2continueRun 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 threadsrc/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.

P2HTTP 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 freeto 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

@jc01rho