feat(orchestrator): Add OpenCode 2 provider support - #5251
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
d6420ac to
6c8f5a0
Compare
There was a problem hiding this comment.
Effect service conventions review of the new OpenCode 2 service modules. Two findings; everything else (namespace effect/* imports, TextGeneration["Service"] usage, Context.Reference + layer in ProviderInteractionModeReflections.ts, scoped layer wiring) looks consistent with the conventions.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
Effect service conventions review of the new OpenCode 2 service modules. Six findings, all in newly added service code (runtime service module, reaper service, text-generation implementation). The adapter/driver wiring, ProviderInteractionModeReflections, and the client-runtime/mobile changes look consistent with the conventions.
Posted via Macroscope — Effect Service Conventions
6c8f5a0 to
4b51274
Compare
There was a problem hiding this comment.
One convention finding on the new OpenCode 2 service module. Everything flagged in earlier runs (inline Context.Service interfaces, structured OpenCode2RuntimeError/SpawnedProcessReaperError attributes, no raw server output or credential text in detail, retry classification on a structural category) is addressed in this head.
Posted via Macroscope — Effect Service Conventions
4b51274 to
d21ef22
Compare
d21ef22 to
187dc69
Compare
There was a problem hiding this comment.
One finding on the new OpenCode 2 provider probe: the probe wrapper copies its cause's message into a detail field and builds the caller-visible status message from it.
Posted via Macroscope — Effect Service Conventions
| environment: resolvedEnvironment, | ||
| }).pipe( | ||
| Effect.mapError( | ||
| (cause) => new OpenCode2ProbeError({ cause, detail: openCodeRuntimeErrorDetail(cause) }), |
There was a problem hiding this comment.
loadOpenCode2Inventory already fails with the structured OpenCode2RuntimeError, and here it is wrapped in OpenCode2ProbeError whose detail is just openCodeRuntimeErrorDetail(cause) — i.e. a copy of cause.message. That detail is then what formatOpenCode2ProbeError turns into the provider status text, and the same function immediately unwraps input.cause.cause again to recover the structural category, so the wrapper carries no context the cause did not already have.
Consider passing the structured error through instead of wrapping it (drop the Effect.mapError here and let fallback/formatOpenCode2ProbeError read category plus message off the OpenCode2RuntimeError directly, keeping the Error-message branch in normalizedErrorMessage for the non-runtime causes such as the Unable to determine OpenCode 2 version failure). That removes the detail-copies-cause.message hop without changing the messages the Settings UI shows.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
One finding on the new OpenCode 2 provider probe: the probe wrapper copies its cause's message into a detail field and builds the caller-visible status message from it.
Posted via Macroscope — Effect Service Conventions
| environment: resolvedEnvironment, | ||
| }).pipe( | ||
| Effect.mapError( | ||
| (cause) => new OpenCode2ProbeError({ cause, detail: openCodeRuntimeErrorDetail(cause) }), |
There was a problem hiding this comment.
loadOpenCode2Inventory already fails with the structured OpenCode2RuntimeError, and here it is wrapped in OpenCode2ProbeError whose detail is just openCodeRuntimeErrorDetail(cause) — i.e. a copy of cause.message. That detail is then what formatOpenCode2ProbeError turns into the provider status text, and the same function immediately unwraps input.cause.cause again to recover the structural category, so the wrapper carries no context the cause did not already have.
Consider passing the structured error through instead of wrapping it (drop the Effect.mapError here and let fallback/formatOpenCode2ProbeError read category plus message off the OpenCode2RuntimeError directly, keeping the Error-message branch in normalizedErrorMessage for the non-runtime causes such as the Unable to determine OpenCode 2 version failure). That removes the detail-copies-cause.message hop without changing the messages the Settings UI shows.
Posted via Macroscope — Effect Service Conventions
| this.advance(); | ||
| return data; | ||
| } | ||
| if (frame?.type === "sdk.error" && frame.operation === operation) { |
There was a problem hiding this comment.
🟡 Medium Adapters/OpenCode2AdapterV2.testkit.ts:219
In OpenCode2ReplayController.response, the sdk.error branch throws immediately without waiting for entry.afterMs, even though afterMs is honored for successful sdk.response frames and sdk.event frames. A transcript that records a delayed SDK failure therefore rejects instantly instead of after the specified delay, so timeout/race/recovery behavior is not reproduced. Consider awaiting entry.afterMs before throwing, matching the sdk.response branch.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts around line 219:
In `OpenCode2ReplayController.response`, the `sdk.error` branch throws immediately without waiting for `entry.afterMs`, even though `afterMs` is honored for successful `sdk.response` frames and `sdk.event` frames. A transcript that records a delayed SDK failure therefore rejects instantly instead of after the specified delay, so timeout/race/recovery behavior is not reproduced. Consider awaiting `entry.afterMs` before throwing, matching the `sdk.response` branch.
| } | ||
| } | ||
| } | ||
| if (entry?.type === "runtime_exit") { |
There was a problem hiding this comment.
🟡 Medium Adapters/OpenCode2AdapterV2.testkit.ts:272
When a runtime_exit entry with status: "success" is reached, the events() async generator consumes it for only the one iterator that claims the cursor, then advance()s past it and returns. Every other concurrent events() subscriber wakes up, sees the cursor already past the exit entry, skips the entry?.type === "runtime_exit" branch, and then awaits this.changed(signal) forever because no further advance() can occur. This contradicts the controller's own support for multiple concurrent event subscribers and causes them to hang instead of observing stream completion. Consider treating a successful runtime_exit as a broadcast terminal marker that all waiting subscribers observe (and return from) without advancing the cursor, so every subscriber completes.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts around line 272:
When a `runtime_exit` entry with `status: "success"` is reached, the `events()` async generator consumes it for only the one iterator that claims the cursor, then `advance()`s past it and returns. Every other concurrent `events()` subscriber wakes up, sees the cursor already past the exit entry, skips the `entry?.type === "runtime_exit"` branch, and then awaits `this.changed(signal)` forever because no further `advance()` can occur. This contradicts the controller's own support for multiple concurrent event subscribers and causes them to hang instead of observing stream completion. Consider treating a successful `runtime_exit` as a broadcast terminal marker that all waiting subscribers observe (and return from) without advancing the cursor, so every subscriber completes.
| const startupOutputRef = yield* Ref.make<{ | ||
| readonly output: string | null; | ||
| readonly failureCategory: OpenCode2RuntimeErrorCategory | null; | ||
| }>({ output: "", failureCategory: null }); |
There was a problem hiding this comment.
🟠 High provider/opencode2Runtime.ts:412
absorb keeps only the last 16,384 characters of startup output and requires both the URL and password banners to be present in that single suffix window. If more than 16 KiB of output arrives between the two banner lines, the first credential is evicted from the buffer before the second one arrives, so parseOpenCode2Startup never sees both at once, readyDeferred never completes, and a successfully started server is reported as startup-timeout. Track the URL and password independently instead of requiring both to coexist in the bounded buffer.
- const startupOutputRef = yield* Ref.make<{
- readonly output: string | null;
- readonly failureCategory: OpenCode2RuntimeErrorCategory | null;
- }>({ output: "", failureCategory: null });
+ const startupStateRef = yield* Ref.make<{
+ readonly url: string | null;
+ readonly password: string | null;
+ readonly failureCategory: OpenCode2RuntimeErrorCategory | null;
+ }>({ url: null, password: null, failureCategory: null });🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/opencode2Runtime.ts around lines 412-415:
`absorb` keeps only the last 16,384 characters of startup output and requires both the URL and password banners to be present in that single suffix window. If more than 16 KiB of output arrives between the two banner lines, the first credential is evicted from the buffer before the second one arrives, so `parseOpenCode2Startup` never sees both at once, `readyDeferred` never completes, and a successfully started server is reported as `startup-timeout`. Track the URL and password independently instead of requiring both to coexist in the bounded buffer.
| return new RegExp(`^${expression}$`).test(value); | ||
| } | ||
|
|
||
| function openCode2SessionPermissionMatches( |
There was a problem hiding this comment.
🟠 High Adapters/OpenCode2AdapterV2.ts:1206
openCode2SessionPermissionMatches grants a request whenever the action and resources match any permission in the shared sessionPermissions array, without checking which session the permission was granted for. Because acceptForSession inserts into that single shared array while events for multiple parent/child session IDs are handled, approving a resource for one subagent session also auto-approves matching requests from sibling or parent sessions, breaking the session-scoped permission boundary. Store and match the granting sessionID alongside each permission so a match requires the same session.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts around line 1206:
`openCode2SessionPermissionMatches` grants a request whenever the action and resources match any permission in the shared `sessionPermissions` array, without checking which session the permission was granted for. Because `acceptForSession` inserts into that single shared array while events for multiple parent/child session IDs are handled, approving a resource for one subagent session also auto-approves matching requests from sibling or parent sessions, breaking the session-scoped permission boundary. Store and match the granting `sessionID` alongside each permission so a match requires the same session.
| ); | ||
| } | ||
|
|
||
| function rememberOpenCode2SessionPermission( |
There was a problem hiding this comment.
🟡 Medium Adapters/OpenCode2AdapterV2.ts:1232
rememberOpenCode2SessionPermission stores an empty resources array when both permission.save and permission.resources are empty. openCode2SessionPermissionMatches can never match an empty remembered list, so an acceptForSession grant on a resource-less permission is never persisted — every subsequent equivalent request prompts again. Consider normalizing empty resources to ["*"] (matching the rest of the code) before storing the remembered permission.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts around line 1232:
`rememberOpenCode2SessionPermission` stores an empty `resources` array when both `permission.save` and `permission.resources` are empty. `openCode2SessionPermissionMatches` can never match an empty remembered list, so an `acceptForSession` grant on a resource-less permission is never persisted — every subsequent equivalent request prompts again. Consider normalizing empty `resources` to `["*"]` (matching the rest of the code) before storing the remembered permission.
| ); | ||
| } | ||
|
|
||
| function isOpenCodeAllowAllPolicy( |
There was a problem hiding this comment.
🟠 High Adapters/OpenCode2AdapterV2.ts:1220
isOpenCodeAllowAllPolicy checks whether the initial turn's policy allows everything, and that decision is cached for the entire provider session. If the session starts in full-access mode and a later turn switches to a stricter approval policy, the allow-all configuration set at openSession time is never revoked, so commands continue executing with full access and permission prompts are not restored until the session is reopened. This silently ignores the user's tightened security policy.
The function evaluates runtimePolicy for only the first turn and the result is treated as immutable for the session. Consider re-evaluating the policy on each turn (or revoking the native allow-all configuration when the policy becomes stricter) so mid-session policy changes take effect.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts around line 1220:
`isOpenCodeAllowAllPolicy` checks whether the *initial* turn's policy allows everything, and that decision is cached for the entire provider session. If the session starts in full-access mode and a later turn switches to a stricter approval policy, the allow-all configuration set at `openSession` time is never revoked, so commands continue executing with full access and permission prompts are not restored until the session is reopened. This silently ignores the user's tightened security policy.
The function evaluates `runtimePolicy` for only the first turn and the result is treated as immutable for the session. Consider re-evaluating the policy on each turn (or revoking the native allow-all configuration when the policy becomes stricter) so mid-session policy changes take effect.
| parts: new Map(), | ||
| toolIdsByCallId: new Map(), | ||
| providerTurn, | ||
| nextItemOrdinal: 2, |
There was a problem hiding this comment.
🟡 Medium Adapters/OpenCode2AdapterV2.ts:2613
createChildTurn hardcodes nextItemOrdinal to 2 for every provider-native child turn, so when a second turn is created on the same child thread its items start at ordinal 2 — the same ordinals already used by the first turn. Projections and clients order the thread timeline by authoritative item ordinal, so the items from the two turns are interleaved by ordinal ID rather than displayed chronologically. The root-turn path avoids this by deriving the starting ordinal from providerTurn.ordinal; the child path should do the same (or otherwise carry forward the thread-wide counter).
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts around line 2613:
`createChildTurn` hardcodes `nextItemOrdinal` to `2` for every provider-native child turn, so when a second turn is created on the same child thread its items start at ordinal `2` — the same ordinals already used by the first turn. Projections and clients order the thread timeline by authoritative item ordinal, so the items from the two turns are interleaved by ordinal ID rather than displayed chronologically. The root-turn path avoids this by deriving the starting ordinal from `providerTurn.ordinal`; the child path should do the same (or otherwise carry forward the thread-wide counter).
| } | ||
|
|
||
| const ready = readyExit.value; | ||
| if (Option.isNone(ready)) { |
There was a problem hiding this comment.
🟠 High provider/opencode2Runtime.ts:503
The startup-timeout path at Option.isNone(ready) returns an error without terminating the spawned process — it only interrupts exitFiber. The SIGTERM/SIGKILL cleanup is registered as a finalizer on the caller's runtimeScope, so if the caller catches this failure and keeps that scope alive (e.g. to retry), the timed-out server process and its stdout/stderr fibers keep running until the entire scope eventually closes. The process should be terminated and untracked before returning the timeout error, or acquired in a child scope that is closed on startup failure.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/opencode2Runtime.ts around line 503:
The startup-timeout path at `Option.isNone(ready)` returns an error without terminating the spawned process — it only interrupts `exitFiber`. The `SIGTERM`/`SIGKILL` cleanup is registered as a finalizer on the caller's `runtimeScope`, so if the caller catches this failure and keeps that scope alive (e.g. to retry), the timed-out server process and its stdout/stderr fibers keep running until the entire scope eventually closes. The process should be terminated and untracked before returning the timeout error, or acquired in a child scope that is closed on startup failure.
| export function togglePendingUserInputOptionSelection( | ||
| question: ThreadUserInputQuestion, | ||
| draft: PendingUserInputDraftAnswer | undefined, | ||
| optionLabel: string, | ||
| ): PendingUserInputDraftAnswer { | ||
| if (question.multiSelect) { | ||
| const selectedOptionLabels = normalizeSelectedOptionLabels(draft?.selectedOptionLabels); | ||
| const nextSelectedOptionLabels = selectedOptionLabels.includes(optionLabel) | ||
| ? selectedOptionLabels.filter((label) => label !== optionLabel) | ||
| : [...selectedOptionLabels, optionLabel]; | ||
| return { | ||
| customAnswer: "", | ||
| ...(nextSelectedOptionLabels.length > 0 | ||
| ? { selectedOptionLabels: nextSelectedOptionLabels } | ||
| : {}), | ||
| }; | ||
| } | ||
|
|
||
| return { customAnswer: "", selectedOptionLabels: [optionLabel] }; |
There was a problem hiding this comment.
🟡 Medium lib/threadActivity.ts:680
togglePendingUserInputOptionSelection cannot deselect a multi-select option whose label has leading or trailing whitespace. The first toggle stores the raw optionLabel, but the next toggle normalizes the stored draft to a trimmed label before comparing it with the raw optionLabel, so selectedOptionLabels.includes(optionLabel) is always false and the option is added again instead of removed. Normalize optionLabel before comparing and storing so the label is consistent across toggles.
| export function togglePendingUserInputOptionSelection( | |
| question: ThreadUserInputQuestion, | |
| draft: PendingUserInputDraftAnswer | undefined, | |
| optionLabel: string, | |
| ): PendingUserInputDraftAnswer { | |
| if (question.multiSelect) { | |
| const selectedOptionLabels = normalizeSelectedOptionLabels(draft?.selectedOptionLabels); | |
| const nextSelectedOptionLabels = selectedOptionLabels.includes(optionLabel) | |
| ? selectedOptionLabels.filter((label) => label !== optionLabel) | |
| : [...selectedOptionLabels, optionLabel]; | |
| return { | |
| customAnswer: "", | |
| ...(nextSelectedOptionLabels.length > 0 | |
| ? { selectedOptionLabels: nextSelectedOptionLabels } | |
| : {}), | |
| }; | |
| } | |
| return { customAnswer: "", selectedOptionLabels: [optionLabel] }; | |
| export function togglePendingUserInputOptionSelection( | |
| question: ThreadUserInputQuestion, | |
| draft: PendingUserInputDraftAnswer | undefined, | |
| optionLabel: string, | |
| ): PendingUserInputDraftAnswer { | |
| const normalizedOptionLabel = optionLabel.trim(); | |
| if (question.multiSelect) { | |
| const selectedOptionLabels = normalizeSelectedOptionLabels(draft?.selectedOptionLabels); | |
| const nextSelectedOptionLabels = selectedOptionLabels.includes(normalizedOptionLabel) | |
| ? selectedOptionLabels.filter((label) => label !== normalizedOptionLabel) | |
| : [...selectedOptionLabels, normalizedOptionLabel]; | |
| return { | |
| customAnswer: "", | |
| ...(nextSelectedOptionLabels.length > 0 | |
| ? { selectedOptionLabels: nextSelectedOptionLabels } | |
| : {}), | |
| }; | |
| } | |
| return { customAnswer: "", selectedOptionLabels: [normalizedOptionLabel] }; |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/lib/threadActivity.ts around lines 680-698:
`togglePendingUserInputOptionSelection` cannot deselect a multi-select option whose label has leading or trailing whitespace. The first toggle stores the raw `optionLabel`, but the next toggle normalizes the stored draft to a trimmed label before comparing it with the raw `optionLabel`, so `selectedOptionLabels.includes(optionLabel)` is always `false` and the option is added again instead of removed. Normalize `optionLabel` before comparing and storing so the label is consistent across toggles.
| this.transcript = transcript; | ||
| } | ||
|
|
||
| async expectOutbound(actual: unknown): Promise<void> { |
There was a problem hiding this comment.
🟡 Medium Adapters/OpenCode2AdapterV2.testkit.ts:163
expectOutbound does not call this.throwFailure() before reading the entry at line 172, so when a prior mismatch has poisoned the controller via fail, a later outbound call whose frame matches the current cursor still advances the cursor and resolves successfully. By contrast, response and events both call this.throwFailure() at the top of their loops, so they correctly surface the stored failure. This lets callers proceed after a deterministic replay failure that should have propagated. Consider calling this.throwFailure() at the start of expectOutbound, before the while loop.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts around line 163:
`expectOutbound` does not call `this.throwFailure()` before reading the entry at line 172, so when a prior mismatch has poisoned the controller via `fail`, a later outbound call whose frame matches the current cursor still advances the cursor and resolves successfully. By contrast, `response` and `events` both call `this.throwFailure()` at the top of their loops, so they correctly surface the stored failure. This lets callers proceed after a deterministic replay failure that should have propagated. Consider calling `this.throwFailure()` at the start of `expectOutbound`, before the `while` loop.
Summary
provider, using the
@opencode-ai/cli@nextruntime and v2 SDK surface.provider settings, model selection, reasoning variants, Build/Plan mode,
permissions, questions, Queue, Stop, background work, subagents, lineage,
replay, web, desktop, and mobile.
observed a complete, stable inventory.
Bun, pnpm, and Vite Plus installations of
@opencode-ai/cli@next.This draft targets
t3code/codex-turn-mapping.Problem and Fix
opencode2driver, runtime, adapter, provider layer, and text-generation backend using@opencode-ai/sdk-next/v2. OpenCode 1.x remains unchanged and both providers can be configured together.latestchannel, while OpenCode 2 ships onnext; blindly updating an npm package also cannot guarantee that it owns the configured executable.nextdist-tag with a channel-isolated cache key. Offer one-click@opencode-ai/cli@nextupdates only for recognized npm, Bun, pnpm, and Vite Plus paths; keep custom paths and externally managed servers manual-only.Defensive Fixes
opencode2.exeas a placeholder that requires postinstall replacement, and Bun blocks dependency lifecycle scripts unless the package is trusted.--trustand pnpm's package-scoped build approval, and preserve the default script-running behavior for npm and Vite Plus.UI Changes
Before this change, T3 Code had no OpenCode 2 provider entry. After the change,
OpenCode 2 appears as a distinct Preview provider in Settings and the model
picker, with its own icon, provider instances, reasoning selector, Build/Plan
mapping, Queue controls, Stop states, provider-native child lineage, and the
shared one-click update action when its executable belongs to a recognized
package manager. Its icons use OpenCode's blue
devtreatment because OpenCode2 is distributed on OpenCode's development release track.
Baseline settlement:
Queue:
Subagents and background work:
Direct Stop and recovery:
The linked guide contains copyable prompts and expected UI outcomes for desktop,
web, and mobile. Packaged desktop verification covered provider setup, the
settled authenticated catalog, Queue to Steer, and multi-item Queue editing and
reordering. The Stop and nested-child rows are backed by the automated and
headless coverage below; their packaged UI prompts remain in the guide for the
draft review pass.
Startup Performance
Measured with
opencode2 v0.0.0-next-16694using isolated, test-owned processgroups, ephemeral ports, and empty temporary working directories:
630ms maximum.
maximum after banner readiness.
connected integrations. Every inventory accepted by the production 500ms floor
and matching-snapshot rule matched the inventory observed at 5.5 seconds; the
latest acceptance was 649ms after banner readiness.
1.779s median. Every check reported ready with all 108 models.
These measurements did not read T3 userdata or interact with a running desktop
application.
Validation
vp check: pass, with pre-existing warnings onlyvp run typecheck: pass across all 15 typecheck tasksvp run build:desktop: passvp run test: pass across all 14 test packages; server result was 2,157passed with 16 environment-gated skips
node scripts/release-smoke.ts: passcoverage: 52 tests pass across four files
with three real-binary adapter cases gated; seven selected replay fixtures
also pass
including multiselect forms on web and mobile, replay concurrency, Stop
targeting, stale refresh rejection, and reaper failure handling
directories under the parent workspace
tmp/opencode2advanced from next-16691to next-16694 with
bun add -g --trust @opencode-ai/cli@next; the resultingpackage bin is the platform ELF executable, not the shell placeholder
to Steer; packaged command logs confirm durable edit and reorder receipts
foreground-child Stop, settled-parent child Stop, direct-child Stop, sibling
exclusion, background shell Stop, recovery ordering, and nested depth-2 Stop
OPENCODE_CONFIG_CONTENT={"experimental":{"subagent_depth":2}}on a privatetest server; it does not change global OpenCode configuration
Known Limitations
default instance; users initially install
@opencode-ai/cli@next,authenticate with
opencode2 auth connect, and add an OpenCode 2 providerinstance explicitly. Later updates are one click for recognized
package-managed paths; custom paths remain manual.
experimental background subagents themselves. T3-owned servers receive the
required environment flag from provider settings.
Stop topology is therefore harness-only unless a server opts into depth 2.
Model: GPT-5 Codex, with Grok 4.5, GPT-5.6 Sol, and Terra review passes
Harness: T3 Code
Note
Add OpenCode 2 as a built-in provider with full orchestration and UI support
opencode2provider driver with its own settings schema (OpenCode2Settings), runtime (OpenCode2RuntimeLive), orchestration adapter (OpenCode2AdapterV2), and text generation implementation (makeOpenCode2TextGeneration).SpawnedProcessReaperservice with a sidecar process that ensures managed OpenCode 2 server subprocesses are cleaned up on parent exit, on both POSIX and Windows.hasInterruptibleProviderNativeBackgroundWork, Stop commands can target provider-native threads without a run ID, and the composer surfaces a secondary Stop button when background work is active.next) and per-channel version caching;parseSemveris fixed to preserve full prerelease identifiers containing hyphens (e.g.0.0.0-next-16339).OpenCode2Iconto the icon registry and surfaces OpenCode 2 in the model picker, settings UI, and mobile composer with distinct blue-themed visuals.OpenCode2RuntimeLiveandSpawnedProcessReaper; any misconfiguration in the sidecar respawn logic could affect process cleanup for all managed providers.📊 Macroscope summarized 6c8f5a0. 121 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
(Automatic summaries will resume when PR exits draft mode or review begins).🗂️ Filtered Issues
No issues evaluated.