Skip to content

feat(orchestrator): Add OpenCode 2 provider support - #5251

Draft
mwolson wants to merge 1 commit into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:feat/opencode2-provider
Draft

feat(orchestrator): Add OpenCode 2 provider support#5251
mwolson wants to merge 1 commit into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:feat/opencode2-provider

Conversation

@mwolson

@mwolson mwolson commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add OpenCode 2.x as a preview provider alongside the existing OpenCode 1.x
    provider, using the @opencode-ai/cli@next runtime and v2 SDK surface.
  • Cover the provider end to end across server orchestration, text generation,
    provider settings, model selection, reasoning variants, Build/Plan mode,
    permissions, questions, Queue, Stop, background work, subagents, lineage,
    replay, web, desktop, and mobile.
  • Keep incomplete startup catalogs private until a bounded settlement check has
    observed a complete, stable inventory.
  • Add release-channel-aware advisories and one-click updates for recognized npm,
    Bun, pnpm, and Vite Plus installations of @opencode-ai/cli@next.

This draft targets t3code/codex-turn-mapping.

Problem and Fix

Problem and Why it Happened Fix
OpenCode 2 changed its startup banner, mandatory server authentication, HTTP routes, SDK, and event vocabulary. Treating it as another binary path for the existing OpenCode driver would conflate incompatible protocols. Add a separate opencode2 driver, 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.
The ready banner can arrive before plugins and authenticated model integrations finish loading, so the first non-empty inventory can be incomplete. Read models, agents, and integrations concurrently; observe stable snapshots behind a 500ms minimum window; require a model-bearing connected integration when available; publish the replacement snapshot atomically.
Provider-native background work and subagents outlive ordinary turns and can be stopped from a settled parent or projected child thread. Generic run-only cancellation cannot identify those targets safely. Project native lineage and shell ownership, preserve post-settlement waiting and wake behavior, and route Stop to the exact active child or shell while excluding siblings.
OpenCode 2 moved question prompts to forms, including multiselect fields, exposes reasoning as model variants, and owns Build/Plan as native agents. Translate form events through the existing question surface with truthful single- and multi-answer shapes on web and mobile, validate variants against the settled catalog, and map the shared interaction-mode toggle to native Build/Plan agents while retaining custom-agent selection.
Generic provider advisories query npm's stable latest channel, while OpenCode 2 ships on next; blindly updating an npm package also cannot guarantee that it owns the configured executable. Resolve advisories from the npm next dist-tag with a channel-isolated cache key. Offer one-click @opencode-ai/cli@next updates only for recognized npm, Bun, pnpm, and Vite Plus paths; keep custom paths and externally managed servers manual-only.
The provider must remain truthful on every client while its first inventory check is pending and while Queue or Stop changes durable orchestration state. Add provider metadata, icons, settings, instance-aware model picker behavior, mobile composer support, Queue controls, Stop presentation, and replay-backed projections across shared client state.

Defensive Fixes

Risk Fix
A failed, interrupted, or timed-out spawned server could leave child processes behind, including descendants that outlive their wrapper. Register scoped process-group cleanup atomically with spawn, keep output drained, detect parent replacement, verify the whole process group during bounded escalation, and terminate abandoned or control-channel-broken reaper sidecars.
An unknown reasoning variant is accepted by the server but can silently swallow the next prompt. Clamp explicit variants against the settled catalog and fall back to the server default when the selection cannot be validated.
The npm package ships opencode2.exe as a placeholder that requires postinstall replacement, and Bun blocks dependency lifecycle scripts unless the package is trusted. Mark the package as lifecycle-dependent, use Bun's --trust and pnpm's package-scoped build approval, and preserve the default script-running behavior for npm and Vite Plus.
OpenCode's prerelease identifiers contain a second hyphen, which the shared semver normalizer previously truncated before comparison. Preserve the complete prerelease suffix so development builds such as next-16691 and next-16694 produce the correct advisory state.
Late, replayed, or out-of-order native events could duplicate wakes, reflect stale interaction mode, revive stopped work, or collide across threads. Claim delayed replay events before waiting, scope reflection receipts by thread, retain terminal ownership state, and cover delayed completion, recovery ordering, sibling isolation, and retired sessions.
Startup and SDK diagnostics can contain credentials or conflate an HTTP 404 with a missing executable. Use schema-backed errors with bounded operations and safe categories; retain package, authentication, and network guidance without copying raw causes or the minted server password into Settings or logs.

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 dev treatment because OpenCode
2 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-16694 using isolated, test-owned process
groups, ephemeral ports, and empty temporary working directories:

  • 20 cold starts reached server-banner readiness in 577ms median, 625ms p95, and
    630ms maximum.
  • The complete usable catalog appeared 522ms median, 537ms p95, and 556ms
    maximum after banner readiness.
  • All 20 runs converged to the same 108-model, 7-agent catalog with two
    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.
  • 10 full source-level provider checks completed in 1.722s to 1.859s, with a
    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 only
  • vp run typecheck: pass across all 15 typecheck tasks
  • vp run build:desktop: pass
  • vp run test: pass across all 14 test packages; server result was 2,157
    passed with 16 environment-gated skips
  • node scripts/release-smoke.ts: pass
  • Focused orchestration Stop coverage: 242 tests pass
  • Changed-surface regression run: 505 tests pass with 10 environment-gated skips
  • Focused provider-maintenance, OpenCode 2 driver/provider, and shared semver
    coverage: 52 tests pass across four files
  • Review-repair regression coverage: 150 focused tests pass across eight files
    with three real-binary adapter cases gated; seven selected replay fixtures
    also pass
  • Round B review-repair coverage: 143 focused tests pass across 13 files,
    including multiselect forms on web and mobile, replay concurrency, Stop
    targeting, stale refresh rejection, and reaper failure handling
  • Affected server typecheck: pass with only pre-existing Effect suggestions
  • Isolated Bun-managed spawned-server runtime: pass with fresh HOME and XDG
    directories under the parent workspace tmp/
  • Real Bun 1.3.14 isolated-global update: opencode2 advanced from next-16691
    to next-16694 with bun add -g --trust @opencode-ai/cli@next; the resulting
    package bin is the platform ELF executable, not the shell placeholder
  • Real OpenCode 2 Queue packs pass for multi-item controls and Queue promotion
    to Steer; packaged command logs confirm durable edit and reorder receipts
  • Real OpenCode 2 Stop coverage passes for direct Stop, queued recovery,
    foreground-child Stop, settled-parent child Stop, direct-child Stop, sibling
    exclusion, background shell Stop, recovery ordering, and nested depth-2 Stop
  • Nested depth-2 verification uses only inline
    OPENCODE_CONFIG_CONTENT={"experimental":{"subagent_depth":2}} on a private
    test server; it does not change global OpenCode configuration
  • Published manual UI guide and live scenario matrix

Known Limitations

  • OpenCode 2 is still an upstream preview line. This driver intentionally has no
    default instance; users initially install @opencode-ai/cli@next,
    authenticate with opencode2 auth connect, and add an OpenCode 2 provider
    instance explicitly. Later updates are one click for recognized
    package-managed paths; custom paths remain manual.
  • External OpenCode 2 servers must supply their own server password and enable
    experimental background subagents themselves. T3-owned servers receive the
    required environment flag from provider settings.
  • OpenCode currently defaults provider-native subagent depth to 1. The nested
    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

  • Introduces an opencode2 provider driver with its own settings schema (OpenCode2Settings), runtime (OpenCode2RuntimeLive), orchestration adapter (OpenCode2AdapterV2), and text generation implementation (makeOpenCode2TextGeneration).
  • Adds a SpawnedProcessReaper service with a sidecar process that ensures managed OpenCode 2 server subprocesses are cleaned up on parent exit, on both POSIX and Windows.
  • Extends the orchestrator to handle provider-native background work: threads now expose 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.
  • Provider maintenance resolution supports npm dist-tag (next) and per-channel version caching; parseSemver is fixed to preserve full prerelease identifiers containing hyphens (e.g. 0.0.0-next-16339).
  • Adds an OpenCode2Icon to the icon registry and surfaces OpenCode 2 in the model picker, settings UI, and mobile composer with distinct blue-themed visuals.
  • Wires replay test fixtures and integration tests covering background subagents, child stop/recovery, compaction, shell terminals, thread deletion, and permissions.
  • Risk: the production server layer now unconditionally starts OpenCode2RuntimeLive and SpawnedProcessReaper; 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.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ccc60674-ded2-47fe-b380-15390551af4e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 3, 2026
@mwolson
mwolson force-pushed the feat/opencode2-provider branch from d6420ac to 6c8f5a0 Compare August 3, 2026 00:21

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread apps/server/src/textGeneration/OpenCode2TextGeneration.ts Outdated
Comment thread apps/server/src/provider/SpawnedProcessReaper.ts Outdated
Comment thread apps/server/src/textGeneration/OpenCode2TextGeneration.ts
Comment thread apps/server/src/orchestration-v2/testkit/fixtures/shared.ts Outdated
Comment thread apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.testkit.ts Outdated
Comment thread apps/server/src/orchestration-v2/ProviderInteractionModeReflectionService.ts Outdated
Comment thread apps/server/src/provider/opencode2Runtime.ts
Comment thread apps/server/src/provider/opencode2Runtime.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread apps/server/src/provider/opencode2Runtime.ts Outdated
Comment thread apps/server/src/textGeneration/OpenCode2TextGeneration.ts
Comment thread apps/server/src/provider/opencode2Runtime.ts Outdated
Comment thread apps/server/src/provider/SpawnedProcessReaper.ts Outdated
Comment thread apps/server/src/provider/SpawnedProcessReaper.ts Outdated
Comment thread apps/server/src/provider/opencode2Runtime.ts
@mwolson
mwolson force-pushed the feat/opencode2-provider branch from 6c8f5a0 to 4b51274 Compare August 3, 2026 12:24

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread apps/server/src/provider/opencode2Runtime.ts Outdated
Comment thread apps/server/src/provider/SpawnedProcessReaper.ts
Comment thread packages/contracts/src/orchestrationV2.ts
Comment thread apps/server/src/orchestration-v2/ProviderInteractionModeReflectionService.ts Outdated
Comment thread apps/server/src/provider/makeManagedServerProvider.ts
Comment thread apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts
Comment thread packages/client-runtime/src/state/threadExecution.ts
Comment thread apps/server/src/provider/SpawnedProcessReaper.ts
Comment thread apps/server/src/provider/SpawnedProcessReaper.ts Outdated
@mwolson
mwolson force-pushed the feat/opencode2-provider branch from 4b51274 to d21ef22 Compare August 3, 2026 14:08
@mwolson
mwolson force-pushed the feat/opencode2-provider branch from d21ef22 to 187dc69 Compare August 3, 2026 14:10

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment on lines +412 to +415
const startupOutputRef = yield* Ref.make<{
readonly output: string | null;
readonly failureCategory: OpenCode2RuntimeErrorCategory | null;
}>({ output: "", failureCategory: null });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment on lines +680 to +698
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] };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant