Three additional invariant tests addressing the gaps surfaced by Cubic and the post-implementation review:
- idle path must not dispatch when state ownership changes during the idleSettleMs window
- verification-failure path must treat incrementIteration failure as a loud failure, not a success
- reset strategy must surface session.create rejections as session_creation_rejected even when the SDK throws instead of returning an error envelope
Split the verification-failure restart into clearVerificationState (clears\nthe verification flags so we cleanly transition back to the main loop)\nfollowed by injectContinuationPrompt, with incrementIteration only on\nsuccessful injection. On rejection: clear the loop state and emit a loud\nwarning toast. Mirrors the dispatch-before-commit contract enforced for\nthe idle and session.error paths.
Reorder the session.idle and session.error retry paths so the durable\niteration counter and the progress toast advance only when continueIteration\nreturns dispatched. On dispatch_rejected or session_creation_rejected,\nclear the loop state and emit a loud failure toast instead of silently\nlogging while the loop appears to make progress.\n\nAdds an explicit settle-window state check so a session.deleted firing\nduring the idleSettleMs sleep no longer feeds dispatch against a cleared\nloop. Keeps idleSettleMs intact for the original idle-settle race.
Replace silent returns in continueIteration with a discriminated union\n(dispatched | session_creation_rejected | dispatch_rejected). Wraps\ninjectContinuationPrompt in try/catch so reset-strategy createIterationSession\nreturning null and promptAsync rejections both surface as typed failures\nthe caller can react to.
Lock the contract that durable iteration state and visible UI must only\nadvance when the continuation dispatch is semantically accepted. Adds 4\npermanent invariant tests covering the idle, session.error retry, and\nverification-failure orchestration paths, plus the reset-strategy\nsilent-null path.
Extract isCmuxCompatEnvironment to shared module and fix
interactive_bash to resolve cmux executables with the __tmux-compat
prefix, matching all other tmux command paths in the codebase.
After SIGTERM/SIGKILL escalation, the stdout stream may not close immediately on all platforms. The unconditional await on outputPromise could then hang indefinitely, defeating the bounded process lifetime guarantee. Race outputPromise against a short follow-up timeout to ensure getOpenCodeVersion always returns within a bounded time.
Refs #3766
Only clear recentAnyIdles when the stored marker matches the synthetic idle timestamp for the same session, preventing accidental clobbering of newer idle markers. Add a regression test to verify other sessions keep their dedup state during this bypass path.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
When the binary resolved as 'opencode' on PATH is the OpenCode Desktop GUI (not the CLI), it does not respond to --version with prompt exit. proc.exited then waits forever, freezing the installer at 'Checking OpenCode installation'.
Fix: race proc.exited against OPENCODE_VERSION_CHECK_TIMEOUT_MS=1500. On timeout, proc.kill() and treat the binary as failed so the next candidate is tried. Success requires both timedExitCode === 0 and proc.exitCode === 0.
Fixes#3766
- Add /exceeded.*quota/i and /usage\s*quota/i to classifyErrorType quota block
- Align /usage.?quota/i -> /usage\s*quota/i in RETRYABLE_ERROR_PATTERNS for consistency
- Strengthen auto-retry-signal test assertion
- Add classifyErrorType assertion to Volcano Engine regression test
Ensures Volcano Engine errors are both retryable AND logged as
errorType: quota_exceeded.
Volcano Engine sends quota exceeded errors with the words in reverse
order: 'You have exceeded the 5-hour usage quota'. The existing
patterns required 'quota' to precede 'exceeded', so they never matched.
- Add /exceeded.*quota/i and /usage.?quota/i to RETRYABLE_ERROR_PATTERNS
- Add exceeded.*quota and usage\s*quota to AUTO_RETRY_PATTERNS
- Add regression tests for both detection paths
Fixes: runtime-fallback not triggering on Volcano Engine quota errors
The deep and artistry category fallback chains had no opencode-go provider
entries, causing them to fall through to the ultimate fallback
(opencode/gpt-5-nano) which is not available to opencode-go users.
Add opencode-go entries:
- deep: deepseek-v4-pro -> kimi-k2.6 -> glm-5.1
- artistry: kimi-k2.6 -> glm-5.1
Fixes#3924
The same output.trim() bug fixed in PR #3909 for doctor exists in the
installer's opencode-binary.ts. Without this fix, `bunx oh-my-opencode
install` would store polluted Electron stdout (e.g., `00:24:25.202 >
app starting { version: '1.14.33', packaged: true }`) as the OpenCode
version in config, breaking downstream version-dependent logic.
- Extract extractSemverFromOutput to src/shared/extract-semver.ts
(precedent: spawn-with-windows-hide is in shared because used by
both doctor and installer)
- src/cli/doctor/checks/system-binary.ts now imports from shared and
re-exports for backward compat
- src/cli/config-manager/opencode-binary.ts uses the shared helper
with `?? output.trim()` fallback to preserve legacy behavior on
non-semver-shaped successful outputs (e.g., custom builds)
- Add 4 installer regression tests covering: clean semver, polluted
Electron stdout (regression for #3765 installer caller), fallback
for non-semver, null when no binary on PATH
Refs #3765