feat: Multi-Provider support#2277

merged34
main
t3code/provider-instance-registry
openedApr 21, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 9d ago

Core foundation for multi-providers with some UX improvements to nicely support multi-account codex or custom claude code instances for example


Note

Medium Risk Changes the core wiring for all git text-generation calls to resolve by provider instance id, so misconfiguration or missing registry entries could break commit/PR/title generation across providers.

Overview Switches git text-generation from kind-based routing (RoutingTextGenerationLive) to instance-based routing via a new TextGenerationLive that looks up modelSelection.instanceId in ProviderInstanceRegistry and delegates to that instance’s bound textGeneration implementation.

Refactors Codex/Claude/Cursor/OpenCode text-generation implementations into per-instance factories (make*TextGeneration(settings, env)), removes singleton *TextGenerationLive layers and provider-kind validation checks, and updates tests/integration harnesses to use ProviderDriverKind/ProviderInstanceId plus adapter-registry mocks and no-op provider event loggers. Also extends desktop persisted client settings to include providerModelPreferences.

Reviewed by Cursor Bugbot for commit 245ec796d1faad7eb4e454999f82c41824c9e35c. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add multi-provider instance support routing all provider interactions by ProviderInstanceId

  • Replaces the closed ProviderKind string union with open branded ProviderDriverKind and ProviderInstanceId types throughout contracts, server, and web, allowing multiple configured instances of the same driver (e.g. two Codex accounts).
  • Introduces ProviderDriver SPI with built-in implementations for Codex, Claude, Cursor, and OpenCode; each driver's create() produces a ProviderInstance with adapter, text-generation, and snapshot closures bound to per-instance config.
  • Adds ProviderInstanceRegistry and ProviderInstanceRegistryLive to manage live instances; ProviderRegistryLive is refactored to key snapshots and cache files by instanceId and react dynamically to instance add/remove/rebuild events.
  • ModelSelection wire format migrates from { provider, model } to { instanceId, model } with a decode-time transform that promotes legacy provider strings; OrchestrationSession gains an optional providerInstanceId.
  • Web settings UI gains AddProviderInstanceDialog and ProviderInstanceCard for creating and editing provider instances; ModelPickerContent, ModelPickerSidebar, and ChatComposer are refactored to select and display models per-instance.
  • Sensitive provider instance environment variables are stored in a secret store and redacted before returning settings to clients.
  • Adds DB migrations 27 and 28 adding nullable provider_instance_id columns and indexes to provider_session_runtime and projection_thread_sessions.
  • Risk: ModelSelection shape changes from provider to instanceId on the wire; any client or stored event that contains the old shape will be migrated at decode time, but callers constructing raw objects must switch to instanceId.

Macroscope summarized 245ec79.


Open in Devin Review
Synced from GitHub

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

Run ID: d4436d2e-a060-4b92-a429-f1085c437e1b

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/provider-instance-registry

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

CcursorBotreviewed · 1 inline threadApr 21, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CodexAdapter hardcoded instanceId check breaks custom instances
    • Removed the hardcoded instanceId === "codex" check and replaced it with an unconditional input.modelSelection check, matching the pattern already used in ClaudeAdapter.

Create PR

Or push these changes by commenting:

1@cursor push 84b0d74e0a
Preview (pingdotgg/t3code@84b0d74e0a)
diff
1diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts
2--- a/apps/server/src/provider/Layers/CodexAdapter.ts
3+++ b/apps/server/src/provider/Layers/CodexAdapter.ts
4@@ -1368,10 +1368,8 @@
5 ? { resumeCursor: input.resumeCursor }
6 : {}),
7 runtimeMode: input.runtimeMode,
8- ...(input.modelSelection?.instanceId === "codex"
9- ? { model: input.modelSelection.model }
10- : {}),
11- ...(input.modelSelection?.instanceId === "codex" &&
12+ ...(input.modelSelection ? { model: input.modelSelection.model } : {}),
13+ ...(input.modelSelection &&
14 getModelSelectionOptionValue(input.modelSelection, "fastMode") === true
15 ? { serviceTier: "fast" }
16 : {}),

You can send follow-ups to the cloud agent here.

……@@ -1350,23 +1359,11 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
13661363 threadId: input.threadId,
13671364 cwd: input.cwd ?? process.cwd(),
1368- binaryPath: codexSettings.binaryPath,
1369- ...(codexSettings.homePath ? { homePath: codexSettings.homePath } : {}),
CcursorBoton line 1369 (old)Apr 21, 2026

CodexAdapter hardcoded instanceId check breaks custom instances

High Severity

CodexAdapter still gates model and serviceTier on input.modelSelection?.instanceId === "codex", but after this PR, custom Codex instances carry instance IDs like codex_personal. The equivalent check in ClaudeAdapter was correctly updated to use input.modelSelection unconditionally (line 2834), but this one was missed. Custom Codex instances will silently ignore the user's selected model and never enable fast mode.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 7a466fe69801e159448cebfdaf1df5077cb538ce. Configure here.

apps/web/src/hooks/useWorkspaceCommandExecutor.tslines 140–144Outdated · 7a466feOpen thread
……@@ -0,0 +1,261 @@
141+ const document = useWorkspaceDocument();
142+ const focusedSurface = useFocusedWorkspaceSurface();
143+ const openWorkspaceTarget = useCommandPaletteStore((state) => state.openWorkspaceTarget);
144+ const closeFocusedWindow = useWorkspaceStore((state) => state.closeFocusedWindow);
MmacroscopeappBoton lines 140–144Apr 21, 2026

🟡 Medium hooks/useWorkspaceCommandExecutor.ts:140

useCommandPaletteStore is called with a selector that reads state.openWorkspaceTarget, but that property does not exist on the CommandPaletteStore interface — the store only exposes open, openIntent, setOpen, toggleOpen, openAddProject, and clearOpenIntent. Consequently, openWorkspaceTarget evaluates to undefined, and the calls on lines 192 and 198 throw TypeError: openWorkspaceTarget is not a function. Consider either adding openWorkspaceTarget to the store or replacing these calls with the correct store method.

✅ Resolved in fb6a2294f57212464a50e151e2192de87fc13cfa

……@@ -0,0 +1,174 @@
158+ const serverSettings = yield* ServerSettingsService;
159+ const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe(
160+ Effect.orElseSucceed(() => undefined),
161+ );
MmacroscopeappBoton lines 159–161Apr 21, 2026

🟡 Medium Layers/ProviderInstanceRegistryHydration.ts:159

Errors from serverSettings.getSettings during initialization are silently swallowed by Effect.orElseSucceed(() => undefined), so a corrupt settings file or other startup failure causes the registry to initialize with an empty config map and no log entry explaining why providers are missing. Consider logging the error before falling back, or removing the fallback so startup failures are visible.

No longer relevant as of b1bb7a2f2485a0c7a4ecfda938c517db67b9b98f

apps/server/src/provider/Layers/ProviderEventLoggers.tslines 68–77Outdated · 7a466feOpen thread
……@@ -0,0 +1,83 @@
74+ });
75+ const canonical = yield* makeEventNdjsonLogger(providerEventLogPath, {
76+ stream: "canonical",
77+ });
MmacroscopeappBoton lines 68–77Apr 21, 2026

🟢 Low Layers/ProviderEventLoggers.ts:68

Both native and canonical loggers are created with the same providerEventLogPath, so makeEventNdjsonLogger writes both streams to identical file paths (e.g., /logs/thread-abc.log) since only path.dirname(filePath) and threadSegment are used in the filename. This causes interleaved/corrupted output when both streams write concurrently. Consider including the stream type in the log filename to keep the two streams separate.

No longer relevant as of b1bb7a2f2485a0c7a4ecfda938c517db67b9b98f

apps/web/src/workspace/store.tslines 868–872Outdated · 7a466feOpen thread
……@@ -0,0 +1,1227 @@
869+ if (!document.rootNodeId || !document.nodesById[document.rootNodeId]) {
870+ return createEmptyWorkspaceDocument();
871+ }
872+
MmacroscopeappBoton lines 868–872Apr 21, 2026

🟢 Low workspace/store.ts:868

readInitialWorkspaceDocument passes user-controlled storage data to normalizePersistedWorkspaceDocument without validating that nodesById exists. When nodesById is missing, normalizePersistedWorkspaceDocument throws TypeError: Cannot read properties of undefined at document.nodesById[document.rootNodeId]. This crashes the app on startup if browser storage is corrupted.

isWorkspaceDocument only checks version and layoutEngine, so the guard passes despite missing required fields. Consider validating nodesById in isWorkspaceDocument, or add a defensive check in normalizePersistedWorkspaceDocument before accessing nodesById.

No longer relevant as of b1bb7a2f2485a0c7a4ecfda938c517db67b9b98f

MmacroscopeappBoton outdated line 2824Apr 21, 2026

🟢 Low Layers/ClaudeAdapter.ts:2834

The startSession function no longer validates that input.modelSelection.instanceId === "claudeAgent" before using modelSelection, while sendTurn still performs this check. This inconsistency means startSession will pass non-Claude model selections to resolveClaudeApiModelId, potentially sending invalid model IDs to the Claude API. Consider re-adding the instanceId validation in startSession to match sendTurn.

✅ Resolved in 4be50878d48aa84d0066e04dd20e259a9d999c45

Synced from GitHub

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

apps/web/src/composerDraftStore.tslines 449–451Outdated · 7a466feOpen thread
……@@ -379,37 +405,56 @@ interface ComposerDraftStoreState {
448+
449+function cloneModelSelectionByProvider(
450+ selections: Partial<Record<ProviderKind, ModelSelection>>,
451+): DeepMutable<Partial<Record<ProviderKind, ModelSelection>>> {
MmacroscopeappBoton lines 449–451Apr 23, 2026

🟢 Low src/composerDraftStore.ts:449

cloneModelSelectionByProvider is typed with Partial<Record<ProviderKind, ModelSelection>> but is called with Partial<Record<ProviderInstanceId, ModelSelection>> in partializeComposerDraftStoreState. The type mismatch causes a TypeScript error and potential runtime issues when iterating over provider keys that don't match the expected format. Change the signature to use ProviderInstanceId instead of ProviderKind.

No longer relevant as of fb6a2294f57212464a50e151e2192de87fc13cfa

apps/server/src/git/Layers/TextGenerationLive.tslines 53–60Outdated · 7a466feOpen thread
……@@ -0,0 +1,101 @@
57+ operation,
58+ detail: `No provider instance registered for id '${instanceId}'.`,
59+ }),
60+ ),
MmacroscopeappBoton lines 53–60Apr 23, 2026

🟡 Medium Layers/TextGenerationLive.ts:53

The resolveInstance function allows text generation calls through disabled provider instances. The module docstring states that disabled instances should surface a TextGenerationError, but the current check only verifies instance ? without checking instance.enabled.

No longer relevant as of b1bb7a2f2485a0c7a4ecfda938c517db67b9b98f

apps/web/src/modelSelection.tsline 275Outdated · 7a466feOpen thread
……@@ -169,62 +237,53 @@ export function resolveAppModelSelection(
212273 model: DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER.codex,
213274 };
214- const provider = resolveSelectableProvider(providers, selection.provider);
275+ const provider = resolveSelectableProvider(providers, selection.instanceId);
MmacroscopeappBoton line 275Apr 23, 2026

🟠 High src/modelSelection.ts:275

When selection.instanceId is a custom instance like "codex_work", resolveSelectableProvider returns the driver kind "codex", so the comparison provider === selection.instanceId at line 279 returns false. This causes selectedModel to become null and modelOptions to become undefined, silently discarding the user's saved model and options for any non-default instance. The comparison should use the original selection.instanceId value to determine if we're still on the same instance, not the resolved driver kind.

Also found in 1 other location(s)

apps/server/src/provider/Layers/ClaudeAdapter.ts:563

The change from input.modelSelection?.provider === &#34;claudeAgent&#34; to input.modelSelection?.instanceId === &#34;claudeAgent&#34; appears incorrect. Based on ClaudeAdapterShape which defines provider: &#34;claudeAgent&#34; as the discriminant, provider identifies the type of adapter while instanceId is a unique identifier for each instance (as seen in ClaudeDriver.create which accepts an arbitrary instanceId parameter). If a user creates a Claude instance with a custom instanceId like &#34;my-claude&#34;, this check will fail and the effort-related logic (both raw effort extraction and model selection) will not execute, causing prompt injection and effort configuration to silently break for renamed instances.

✅ Resolved in b1bb7a2f2485a0c7a4ecfda938c517db67b9b98f

CcursorBotreviewed · 2 inline threadsApr 23, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Hardcoded "codex" provider in unknown-driver error path
    • Replaced the hardcoded provider: "codex" with provider: threadInstanceId so the error correctly identifies the actual unknown driver slug.
  • ✅ Fixed: Instance-level comparison blocks custom instances of same driver
    • Added a resolveDriverKindForInstanceId helper that maps instance IDs to their underlying driver kind via ServerSettings.providerInstances, so custom instances resolve to their driver for the narrowing check and the provider-switch guard now compares driver kinds instead of instance IDs.

Create PR

Or push these changes by commenting:

1@cursor push b84bfe908a
Preview (pingdotgg/t3code@b84bfe908a)
diff
1diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
2--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
3+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
4@@ -170,6 +170,22 @@
5 ),
6 );
7
8+ const resolveDriverKindForInstanceId = (
9+ instanceId: string,
10+ ): Effect.Effect<ProviderKind | undefined> => {
11+ if (Schema.is(ProviderKind)(instanceId)) {
12+ return Effect.succeed(instanceId);
13+ }
14+ return serverSettingsService.getSettings.pipe(
15+ Effect.map((settings) => {
16+ const instances = settings.providerInstances as Record<string, { driver?: string }>;
17+ const driver = instances[instanceId]?.driver;
18+ return driver !== undefined && Schema.is(ProviderKind)(driver) ? driver : undefined;
19+ }),
20+ Effect.orElseSucceed(() => undefined),
21+ );
22+ };
23+
24 const threadModelSelections = new Map<string, ModelSelection>();
25
26 const appendProviderFailureActivity = (input: {
27@@ -277,29 +293,28 @@
28 ? thread.session.providerName
29 : undefined;
30 const requestedModelSelection = options?.modelSelection;
31- // The thread's model selection is keyed by `instanceId` (an open slug);
32- // for built-in drivers the default instance id equals the driver id, so
33- // narrow back to the closed `ProviderKind` here. Threads persisted
34- // against a fork/unknown driver are surfaced via a structured error
35- // rather than silently routed to the wrong provider.
36- const threadProviderCandidate: string = currentProvider ?? thread.modelSelection.instanceId;
37- if (!Schema.is(ProviderKind)(threadProviderCandidate)) {
38+ const threadInstanceId: string = currentProvider ?? thread.modelSelection.instanceId;
39+ const resolvedThreadDriver: ProviderKind | undefined =
40+ yield* resolveDriverKindForInstanceId(threadInstanceId);
41+ if (resolvedThreadDriver === undefined) {
42 return yield* new ProviderAdapterRequestError({
43- provider: "codex",
44+ provider: threadInstanceId,
45 method: "thread.turn.start",
46- detail: `Thread '${threadId}' references unknown provider driver '${threadProviderCandidate}'. The driver is not installed in this build (rolled-back / fork mismatch).`,
47+ detail: `Thread '${threadId}' references unknown provider driver '${threadInstanceId}'. The driver is not installed in this build (rolled-back / fork mismatch).`,
48 });
49 }
50- const threadProvider: ProviderKind = threadProviderCandidate;
51- if (
52- requestedModelSelection !== undefined &&
53- requestedModelSelection.instanceId !== threadProvider
54- ) {
55- return yield* new ProviderAdapterRequestError({
56- provider: threadProvider,
57- method: "thread.turn.start",
58- detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,
59- });
60+ const threadProvider: ProviderKind = resolvedThreadDriver;
61+ if (requestedModelSelection !== undefined) {
62+ const requestedDriver: ProviderKind | undefined = yield* resolveDriverKindForInstanceId(
63+ requestedModelSelection.instanceId,
64+ );
65+ if (requestedDriver !== threadProvider) {
66+ return yield* new ProviderAdapterRequestError({
67+ provider: threadProvider,
68+ method: "thread.turn.start",
69+ detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,
70+ });
71+ }
72 }
73 const preferredProvider: ProviderKind = threadProvider;
74 const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;

You can send follow-ups to the cloud agent here.

……@@ -277,15 +277,28 @@ const make = Effect.gen(function* () {
286299 provider: threadProvider,
287300 method: "thread.turn.start",
288- detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.provider}'.`,
301+ detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,
CcursorBoton line 301Apr 23, 2026

Instance-level comparison blocks custom instances of same driver

High Severity

The provider-switch guard compares requestedModelSelection.instanceId (an open ProviderInstanceId) against threadProvider (a closed ProviderKind). A thread created with a custom instance like "codex_personal" fails immediately at the Schema.is(ProviderKind) check on line 286, and a turn request targeting a custom instance of the same driver as the thread is rejected as a "provider switch." This effectively prevents any thread bound to a non-default provider instance from starting turns through the orchestration engine.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit fb6a2294f57212464a50e151e2192de87fc13cfa. Configure here.

……@@ -277,15 +277,28 @@ const make = Effect.gen(function* () {
288+ provider: "codex",
289+ method: "thread.turn.start",
290+ detail: `Thread '${threadId}' references unknown provider driver '${threadProviderCandidate}'. The driver is not installed in this build (rolled-back / fork mismatch).`,
291+ });
CcursorBoton line 291Apr 23, 2026

Hardcoded "codex" provider in unknown-driver error path

Medium Severity

When a thread references an unknown provider driver, the ProviderAdapterRequestError hardcodes provider: "codex". The error's message getter formats as "Provider adapter request failed (codex) for thread.turn.start: ...", falsely attributing the error to the codex provider regardless of which unknown driver was actually referenced. This misleads error tracking, logging, and any downstream error-handling logic that inspects the provider field.

Reviewed by Cursor Bugbot for commit fb6a2294f57212464a50e151e2192de87fc13cfa. Configure here.

packages/contracts/src/settings.tsline 195Outdated · fb6a229Open thread
……@@ -167,28 +192,11 @@ export const DEFAULT_UNIFIED_SETTINGS: UnifiedSettings = {
189- options: Schema.optionalKey(ProviderOptionSelections),
190- }),
191-]);
195+const ModelSelectionPatch = Schema.Struct({
MmacroscopeappBoton line 195Apr 23, 2026

🟡 Medium src/settings.ts:195

ModelSelectionPatch accepts instanceId but callers still sending provider will have that key silently ignored, leaving instanceId undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps provider to instanceId if present.

Also found in 1 other location(s)

apps/server/src/provider/Layers/ClaudeAdapter.ts:557

The condition input.modelSelection?.instanceId === &#34;claudeAgent&#34; will likely fail for most Claude instances. According to ClaudeDriver metadata, supportsMultipleInstances: true, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked provider === &#34;claudeAgent&#34; which is a constant discriminant on all Claude adapters (as defined in ClaudeAdapterShape). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing rawEffort to be null and claudeModel to be undefined for all other Claude instances.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file packages/contracts/src/settings.ts around line 195:
2
3`ModelSelectionPatch` accepts `instanceId` but callers still sending `provider` will have that key silently ignored, leaving `instanceId` undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps `provider` to `instanceId` if present.
4
5Evidence trail:
6packages/contracts/src/settings.ts:195-199 (ModelSelectionPatch schema - no `provider` field), packages/contracts/src/orchestration.ts:75-104 (ModelSelection has provider→instanceId transform), packages/contracts/src/orchestration.test.ts:641 (test confirms ModelSelection migration), packages/contracts/src/ws.ts:112 and packages/contracts/src/rpc.ts:163 (ServerSettingsPatch exposed via API)
7
8Also found in 1 other location(s):
9- apps/server/src/provider/Layers/ClaudeAdapter.ts:557 -- The condition `input.modelSelection?.instanceId === "claudeAgent"` will likely fail for most Claude instances. According to `ClaudeDriver` metadata, `supportsMultipleInstances: true`, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked `provider === "claudeAgent"` which is a constant discriminant on all Claude adapters (as defined in `ClaudeAdapterShape`). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing `rawEffort` to be `null` and `claudeModel` to be `undefined` for all other Claude instances.
……@@ -0,0 +1,297 @@
138+ }
139+ const hasConfig = Object.keys(config).length > 0;
140+
141+ const nextInstance: ProviderInstanceConfig = {
MmacroscopeappBoton line 141Apr 23, 2026

🟢 Low settings/AddProviderInstanceDialog.tsx:141

ProviderInstanceId.make(instanceId) at line 151 is invoked outside the try block that starts at line 156. If the brand constructor throws (e.g., due to validation rule drift between validateInstanceId and ProviderInstanceId.make), the error propagates unhandled rather than being caught and shown via the error toast at lines 164-169. Consider moving the call inside the try block so constructor failures surface in the UI.

Also found in 1 other location(s)

apps/web/src/components/settings/ProviderInstanceCard.tsx:166

Unsafe type assertion on liveProvider?.status may cause a runtime crash. If the server returns a status value outside the expected set (disabled, error, ready, warning), the assertion as ProviderStatusKey | undefined silently passes, then PROVIDER_STATUS_STYLES[statusKey] returns undefined, and line 231 crashes when accessing statusStyle.dot. The ?? fallback only triggers when the status is nullish, not when it's an unexpected string like &#34;pending&#34; or &#34;initializing&#34;.

No longer relevant as of 5be8c1260e6b9816f8bde76d05af42c043225070

apps/web/src/workspace/store.tslines 982–1000Outdated · fb6a229Open thread
……@@ -0,0 +1,1227 @@
997+ ? splitWindowWithSurface(current, targetWindowId, "y", nextSurface)
998+ : insertSurfaceIntoWindow(current, targetWindowId, nextSurface);
999+ set(setDocumentState(nextDocument));
1000+ },
MmacroscopeappBoton lines 982–1000Apr 23, 2026

🟡 Medium workspace/store.ts:982

openTerminalSurfaceForThread ignores the disposition parameter when matching terminal surfaces already exist. Unlike openThreadSurface which only looks for existing surfaces when disposition === "focus-or-tab", this function always focuses existing surfaces regardless of disposition. When a user requests "split-right" or "new-tab", they expect a new terminal to be created, not focus shifted to an existing one.

Consider adding a condition to only focus existing surfaces when disposition === "focus-or-tab", matching the behavior of openThreadSurface.

No longer relevant as of b1bb7a2f2485a0c7a4ecfda938c517db67b9b98f

Added 5 commits

  • Split provider kind into driver id and instance id8c1e6c6
  • Surface provider instances and buffer settings inputs client-sidec098276
  • Add workspace split bindings and draft persistence02ad01e
  • Introduce provider instance registry and unify settings parity7420471
  • Fix provider instance test fixtures after rebase0c868cb
apps/web/src/components/settings/ProviderInstanceCard.tsxlines 66–80Outdated · 0c868cbOpen thread
……@@ -0,0 +1,372 @@
77+ delete base[key];
78+ }
79+ return Object.keys(base).length > 0 ? base : undefined;
80+}
MmacroscopeappBoton lines 66–80Apr 27, 2026

🟠 High settings/ProviderInstanceCard.tsx:66

nextConfigBlobWithString stores the untrimmed value instead of trimmed, so whitespace surrounding API keys or base URLs is preserved in the config. This causes authentication failures or malformed URL errors at runtime when users paste values with leading/trailing spaces.

No longer relevant as of b1bb7a2f2485a0c7a4ecfda938c517db67b9b98f

Added 3 commits

  • Track provider instances through orchestration and sessionsd05e60c
  • Add continuation group support for provider instances624a086
  • Propagate provider instance environmentsb1bb7a2
CcursorBotreviewed · 2 inline threadsApr 28, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Disabled instance check missing in text generation routing
    • Added an instance.enabled check in resolveInstance so disabled provider instances now surface a TextGenerationError instead of silently delegating to the underlying text generation closure.
  • ✅ Fixed: Empty registry when initial settings read fails
    • Changed the fallback from an empty ProviderInstanceConfigMap to DEFAULT_SERVER_SETTINGS so that built-in driver default instances are always materialized even when the initial settings read fails.

Create PR

Or push these changes by commenting:

1@cursor push 1a12ea9124
Preview (pingdotgg/t3code@1a12ea9124)
diff
1diff --git a/apps/server/src/git/Layers/TextGenerationLive.ts b/apps/server/src/git/Layers/TextGenerationLive.ts
2--- a/apps/server/src/git/Layers/TextGenerationLive.ts
3+++ b/apps/server/src/git/Layers/TextGenerationLive.ts
4@@ -51,7 +51,14 @@
5 registry.getInstance(instanceId).pipe(
6 Effect.flatMap((instance) =>
7 instance
8- ? Effect.succeed(instance.textGeneration)
9+ ? instance.enabled
10+ ? Effect.succeed(instance.textGeneration)
11+ : Effect.fail(
12+ new TextGenerationError({
13+ operation,
14+ detail: `Provider instance '${instanceId}' is disabled.`,
15+ }),
16+ )
17 : Effect.fail(
18 new TextGenerationError({
19 operation,
20
21diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts
22--- a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts
23+++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts
24@@ -42,6 +42,7 @@
25 * @module provider/Layers/ProviderInstanceRegistryHydration
26 */
27 import {
28+ DEFAULT_SERVER_SETTINGS,
29 defaultInstanceIdForDriver,
30 type ProviderInstanceConfig,
31 type ProviderInstanceConfigMap,
32@@ -158,13 +159,10 @@
33 > = Layer.unwrap(
34 Effect.gen(function* () {
35 const serverSettings = yield* ServerSettingsService;
36- const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe(
37- Effect.orElseSucceed(() => undefined),
38+ const initialSettings: ServerSettings = yield* serverSettings.getSettings.pipe(
39+ Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS),
40 );
41- const initialConfigMap =
42- initialSettings === undefined
43- ? ({} as ProviderInstanceConfigMap)
44- : deriveProviderInstanceConfigMap(initialSettings);
45+ const initialConfigMap = deriveProviderInstanceConfigMap(initialSettings);
46
47 const mutableLayer = ProviderInstanceRegistryMutableLayer({
48 drivers: BUILT_IN_DRIVERS,

You can send follow-ups to the cloud agent here.

……@@ -0,0 +1,176 @@
172+ });
173+
174+ return SettingsWatcherLive.pipe(Layer.provideMerge(mutableLayer));
175+ }),
CcursorBoton line 175Apr 28, 2026

Empty registry when initial settings read fails

Medium Severity

On startup the hydration layer reads serverSettings.getSettings with orElseSucceed(() => undefined) and, on failure, seeds the registry with an empty ProviderInstanceConfigMap. If the very first read fails for any reason, no built-in driver default instance is materialized and the registry stays empty until the next streamChanges emission — which may never arrive if settings are not edited. Every consumer that resolves by instanceId (text generation, provider service, etc.) will fail with "No provider instance registered" until then.

Reviewed by Cursor Bugbot for commit b1bb7a2f2485a0c7a4ecfda938c517db67b9b98f. Configure here.

apps/server/src/git/Layers/TextGenerationLive.tsline 62Outdated · b1bb7a2Open thread
……@@ -0,0 +1,101 @@
59+ }),
60+ ),
61+ ),
62+ );
CcursorBoton line 62Apr 28, 2026

Disabled instance check missing in text generation routing

Low Severity

The module docstring states that "Unknown or disabled instances surface a TextGenerationError with the missing instanceId," but resolveInstance only checks whether the instance exists in the registry — not whether it is enabled. A user who has explicitly disabled a provider instance can still trigger text generation against it via thread/PR/branch flows that route through instanceId. Either the documented intent or the implementation needs to be aligned.

Reviewed by Cursor Bugbot for commit b1bb7a2f2485a0c7a4ecfda938c517db67b9b98f. Configure here.

apps/server/src/serverSettings.tsline 175Outdated · b1bb7a2Open thread
……@@ -116,22 +162,34 @@ const PROVIDER_ORDER: readonly ProviderKind[] = ["codex", "claudeAgent", "openco
172+ }
173+ }
174+
175+ if (
MmacroscopeappBoton line 175Apr 28, 2026

🟡 Medium src/serverSettings.ts:175

Custom provider instances with non-built-in drivers are incorrectly overridden with fallback providers. When selection.instanceId refers to a custom instance whose driver is not a built-in driver ID, the code bypasses the enabled check at line 167 (because isBuiltInDriverId(selectedDriver) is false), then fails the second check at line 176 (because selection.instanceId itself isn't a built-in driver ID), and falls through to the fallback logic at lines 182-193. This replaces the user's valid, enabled custom provider selection with a built-in fallback, even when their custom instance is properly configured and enabled.

Consider adding an explicit check for custom (non-built-in) instances that verifies instanceConfig?.enabled ?? true is true before falling through to fallback logic.

✅ Resolved in 0013b64081e821d254fe7cc4080b6946f5dbec7c

apps/server/src/provider/Layers/CursorProvider.tsfileOutdated · b1bb7a2Open thread

🟠 High

https://github.com/pingdotgg/t3code/blob/b1bb7a2f2485a0c7a4ecfda938c517db67b9b98f/apps/server/src/provider/Layers/CursorProvider.ts#L1159

discoverCursorModelsViaAcp was updated to accept an environment parameter, but the caller in checkCursorProviderStatus at line 1159 still invokes it with only cursorSettings. This causes custom environment variables passed to checkCursorProviderStatus (e.g., processEnv from CursorDriver) to be dropped during ACP model discovery, falling back to process.env instead. Consider passing environment as the second argument.

No longer relevant as of 0013b64081e821d254fe7cc4080b6946f5dbec7c

apps/web/src/components/settings/ProviderInstanceCard.tsxlines 295–301Outdated · b1bb7a2Open thread
……@@ -0,0 +1,748 @@
298+ .map((row) => ({ ...row, name: row.name.trim() }))
299+ .filter((row) => ENVIRONMENT_VARIABLE_NAME_PATTERN.test(row.name))
300+ .map(({ id: _id, ...row }) => row),
301+ );
MmacroscopeappBoton lines 295–301Apr 28, 2026

🟡 Medium settings/ProviderInstanceCard.tsx:295

In ProviderEnvironmentSection, when a user adds a new variable row and edits the value field before filling in the name, committing the value triggers publishRows. The filter ENVIRONMENT_VARIABLE_NAME_PATTERN.test(row.name) removes rows with empty names, so the row is dropped from props.onChange. The useEffect then syncs local state from props.environment, causing the entire row — including the user's typed value — to silently disappear. This is a data-loss bug: data the user entered is discarded without warning.

Consider keeping rows with empty names in local state but not publishing them upstream until the name is valid, or showing validation feedback instead of dropping the row.

✅ Resolved in 0013b64081e821d254fe7cc4080b6946f5dbec7c

……@@ -0,0 +1,358 @@
152+
153+ const nextInstance: ProviderInstanceConfig = {
154+ driver: driver as ProviderDriverId,
155+ enabled: true,
MmacroscopeappBoton line 155Apr 28, 2026

🟢 Low settings/AddProviderInstanceDialog.tsx:155

When saving a provider instance, line 157 spreads the raw accentColor state value instead of the normalized result from normalizeProviderAccentColor(accentColor). If the user enters a color with leading/trailing whitespace like " #2563eb ", the condition passes (the normalized value is truthy) but the spread uses the untrimmed original, persisting invalid data.

✅ Resolved in 0013b64081e821d254fe7cc4080b6946f5dbec7c

apps/web/src/components/settings/SettingsPanels.tsxlines 661–662Outdated · b1bb7a2Open thread
……@@ -678,147 +589,142 @@ export function GeneralSettingsPanel() {
659+ const legacyConfig = settings.providers[providerSettings.provider];
660+ const defaultLegacyConfig = DEFAULT_UNIFIED_SETTINGS.providers[providerSettings.provider];
661+ const effectiveInstance: ProviderInstanceConfig =
662+ explicitInstance ?? ({ driver, config: legacyConfig } satisfies ProviderInstanceConfig);
MmacroscopeappBoton lines 661–662Apr 28, 2026

🟠 High settings/SettingsPanels.tsx:661

When synthesizing a default instance from legacy config at line 661-662, legacyConfig.enabled is nested inside config but not hoisted to instance.enabled. ProviderInstanceCard reads instance.enabled ?? true, so disabled legacy providers incorrectly appear enabled in the UI. Consider spreading enabled from legacyConfig into the synthesized instance.

✅ Resolved in 0013b64081e821d254fe7cc4080b6946f5dbec7c

Added 3 commits

  • Use Claude initialization results for provider auth status5be8c12
  • Bind provider adapters to their instance ids4be5087
  • Improve provider error attribution for missing instancesed6d810

🟡 Medium

https://github.com/pingdotgg/t3code/blob/ed6d810dae42cfc57a8ece40d3d5a30b0f88e734/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts#L864

When processSessionStopRequested creates the stopped session (lines 864-876), it omits providerInstanceId while preserving providerName. This causes providerInstanceId to be lost on session stop, so subsequent code at lines 291-292 falls back to thread.modelSelection.instanceId instead of the previously-bound instance. Consider including providerInstanceId in the stopped session object.

✅ Resolved in 0013b64081e821d254fe7cc4080b6946f5dbec7c

Added 3 commits

  • Use Claude init capabilities for provider status157fc5b
  • Propagate provider instance enabled statef3a568a
  • Require provider instance IDs across runtime routing6dc8493
apps/server/src/provider/Layers/OpenCodeAdapter.tsline 1271Outdated · 6dc8493Open thread
……@@ -435,955 +438,937 @@ const stopOpenCodeContext = Effect.fn("stopOpenCodeContext")(function* (
1262- const respondToUserInput: OpenCodeAdapterShape["respondToUserInput"] = Effect.fn(
1263- "respondToUserInput",
1264- )(function* (threadId, requestId, answers) {
1271+ const stopSession: OpenCodeAdapterShape["stopSession"] = Effect.fn("stopSession")(
MmacroscopeappBoton line 1271Apr 28, 2026

🟡 Medium Layers/OpenCodeAdapter.ts:1271

stopSession unconditionally emits session.exited even when stopOpenCodeContext has already been called by a racing emitUnexpectedExit. When the race occurs, both functions emit the event because stopOpenCodeContext returns early when its getAndSet loses, but stopSession doesn't check the result. Consider checking the return value of stopOpenCodeContext (or checking context.stopped after it returns) to skip emitting when another caller already handled shutdown.

✅ Resolved in 0013b64081e821d254fe7cc4080b6946f5dbec7c

Added 4 commits

  • Refactor workspace to use provider instance registrya5fb47a
  • Bind OpenCode fallback model selection to instance ida50c26d
  • Allow provider switches after stopped sessions0bfdf12
  • Reject cross-driver provider changes after session stopfa6e0cc
apps/server/src/orchestration/Layers/ProviderCommandReactor.tslines 354–360Outdated · fa6e0ccOpen thread
……@@ -271,87 +286,164 @@ const make = Effect.gen(function* () {
357+ requestedModelSelection !== undefined &&
358+ requestedModelSelection.instanceId !== currentInstanceId
359+ ) {
360+ if (currentInfo.driverId !== desiredInfo.driverId) {
MmacroscopeappBoton lines 354–360Apr 29, 2026

🟢 Low Layers/ProviderCommandReactor.ts:354

At line 355, thread.session !== null is used to detect an active session, but this is wrong when thread.session.status === "stopped". In that case activeThreadSession is null (lines 297-299) but thread.session !== null remains true, causing the driver-switch check at lines 360-377 to incorrectly block users from starting fresh with a different provider. The condition should be activeThreadSession !== null.

diff
1- if (
2- thread.session !== null &&
3- requestedModelSelection !== undefined &&
4- requestedModelSelection.instanceId !== currentInstanceId
5- ) {
6+ if (
7+ activeThreadSession !== null &&
8+ requestedModelSelection !== undefined &&
9+ requestedModelSelection.instanceId !== currentInstanceId
10+ ) {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file apps/server/src/orchestration/Layers/ProviderCommandReactor.ts around lines 354-360:
2
3At line 355, `thread.session !== null` is used to detect an active session, but this is wrong when `thread.session.status === "stopped"`. In that case `activeThreadSession` is `null` (lines 297-299) but `thread.session !== null` remains true, causing the driver-switch check at lines 360-377 to incorrectly block users from starting fresh with a different provider. The condition should be `activeThreadSession !== null`.
4
5Evidence trail:
6apps/server/src/orchestration/Layers/ProviderCommandReactor.ts lines 297-299 (activeThreadSession definition uses status !== "stopped" check), lines 310-314 (currentInstanceId uses activeThreadSession !== null), lines 355-377 (driver-switch guard incorrectly uses thread.session !== null instead of activeThreadSession !== null)

Added 3 commits

  • Surface unavailable provider instances in registry95ec3b7
  • Track provider instance state across sessions and settings0013b64
  • Add provider instance IDs to web test configs703132c
apps/web/src/providerInstances.tslines 243–247Outdated · 703132cOpen thread
……@@ -0,0 +1,266 @@
244+ entries.find((entry) => entry.enabled && entry.isAvailable)?.instanceId ??
245+ requested?.instanceId ??
246+ instanceId
247+ );
MmacroscopeappBoton lines 243–247Apr 29, 2026

🟢 Low src/providerInstances.ts:243

resolveSelectableProviderInstance can return a disabled or unavailable instance when no enabled+available entries exist, contradicting its documented contract of returning "the first enabled instance as a fallback so downstream code can still send a turn." The fallback chain ?? requested?.instanceId ?? instanceId at lines 245–247 returns requested.instanceId (potentially disabled/unavailable) or the original instanceId (unknown state) when no enabled+available providers are found, allowing downstream code to attempt turns against non-functional providers.

✅ Resolved in 7a654d5f3730e9e966a92387944173e817e81df8

Added 2 commits

  • Rename provider driver IDs to driver kinds90fb94e
  • Fix provider picker test instance defaultsf206406
packages/contracts/src/providerRuntime.tslines 248–248Outdated · f206406Open thread
……@@ -245,7 +245,11 @@ const RuntimeErrorType = Schema.Literal("runtime.error");
246246const ProviderRuntimeEventBase = Schema.Struct({
247247 eventId: EventId,
248- provider: ProviderKind,
248+ provider: BuiltInDriverKind,
MmacroscopeappBoton lines 248–248Apr 29, 2026

🟡 Medium src/providerRuntime.ts:248

Using BuiltInDriverKind for the provider field rejects events from forked drivers (e.g., "ollama"). The comment above explicitly states BuiltInDriverKind is "not a validation gate" and the field must accept unknown driver kinds for forward/backward compatibility. Consider switching to ProviderDriverKind (the open branded slug) so unknown driver values pass through and can be handled at runtime.

Also found in 1 other location(s)

packages/contracts/src/provider.ts:35

The provider field uses BuiltInDriverKind, a closed literal union ([&#34;codex&#34;, &#34;claudeAgent&#34;, &#34;cursor&#34;, &#34;opencode&#34;]), but the design documentation in providerInstance.ts explicitly states that BuiltInDriverKind is "reference data for defaults, presentation, and migration shims — not a validation gate" and that "Forks and downgrades will encounter driver kinds outside this list and the system must still operate." Using BuiltInDriverKind directly in this schema makes it a validation gate, causing decode failures for sessions created with fork-added drivers (e.g., ollama). Consider using an open branded slug schema (like ProviderInstanceId) instead.

No longer relevant as of 41e667767d850d7b0307c120375788dcf2dd9c38

Added 2 commits

  • Refactor provider kinds to use instance IDs9a14e09
  • Refactor provider session routing in ProviderService41e6677
apps/server/src/provider/Layers/ProviderRegistry.tsline 526Outdated · 41e6677Open thread
……@@ -202,64 +316,226 @@ const ProviderRegistryLiveBase = Layer.effect(
241524 return {
242525 getProviders: Ref.get(providersRef),
243- refresh: (provider?: ProviderKind) =>
526+ refresh: (provider?: ProviderDriverKind) =>
MmacroscopeappBoton line 526Apr 29, 2026

🟡 Medium Layers/ProviderRegistry.ts:526

The orElseSucceed fallback in refresh (line 529) and refreshInstance (line 534) returns an empty array when the underlying refresh fails. This differs from the inner logic at lines 348-350 and 360-362, which returns the cached providersRef list when a source is not found. An actual network failure during refresh therefore flashes an empty provider list to the UI instead of preserving the last known good state.

✅ Resolved in b9c11e7aed9b75412ba29054d227a128f68bccc4

apps/server/src/provider/Layers/ProviderRegistry.tsline 520Outdated · 41e6677Open thread
……@@ -202,64 +316,226 @@ const ProviderRegistryLiveBase = Layer.effect(
517+ // `Stream.fromSubscription` builds a stream over the pre-acquired
518+ // subscription rather than subscribing on stream start, which is
519+ // what closes the race.
520+ yield* Stream.runForEach(Stream.fromSubscription(instanceChanges), () => syncLiveSources).pipe(
MmacroscopeappBoton line 520Apr 29, 2026

🟡 Medium Layers/ProviderRegistry.ts:520

The Stream.runForEach consumer forked at lines 520-522 has no error handling around the entire syncLiveSources effect. If instanceRegistry.listInstances or instanceRegistry.listUnavailable fail, the error propagates up and terminates the consumer fiber. Once the fiber dies, the aggregator silently stops reacting to instance registry changes, leaving the UI with stale provider state. Consider wrapping the syncLiveSources call in Effect.ignoreCause or Effect.catchAll so the subscription stays alive.

Also found in 1 other location(s)

apps/server/src/provider/Layers/ProviderService.ts:1153

At line 1153, Effect.catch(runStopAll(), ...) is used, but Effect.catch does not exist in the Effect module API. The Effect module exports catchAll, catchAllCause, catchTag, etc., but not a direct catch function (which would also conflict with JavaScript's reserved keyword). This will cause a runtime error TypeError: Effect.catch is not a function when the provider service finalizer runs during shutdown. The correct pattern would be runStopAll().pipe(Effect.catchAll((error) =&gt; Effect.logWarning(...))) or Effect.catchAllCause if catching the full cause.

✅ Resolved in b9c11e7aed9b75412ba29054d227a128f68bccc4

Added 2 commits

  • Remove legacy provider-kind adapter lookup7a654d5
  • Add provider adapter registry coverage01f2b68
CcursorBotreviewed · 1 inline threadApr 29, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Metric test asserts branded type against plain string attribute
    • Changed the test setup to use ProviderDriverKind.make() for both the initial value and the reassignment, so the recorded attribute uses a branded type consistent with the assertion and real production usage.

Create PR

Or push these changes by commenting:

1@cursor push c9731a0748
Preview (pingdotgg/t3code@c9731a0748)
diff
1diff --git a/apps/server/src/observability/Metrics.test.ts b/apps/server/src/observability/Metrics.test.ts
2--- a/apps/server/src/observability/Metrics.test.ts
3+++ b/apps/server/src/observability/Metrics.test.ts
4@@ -76,10 +76,10 @@
5 Effect.gen(function* () {
6 const counter = Metric.counter("with_metrics_lazy_total");
7 const timer = Metric.timer("with_metrics_lazy_duration");
8- let provider = "unknown";
9+ let provider: string = ProviderDriverKind.make("unknown");
10
11 yield* Effect.sync(() => {
12- provider = "codex";
13+ provider = ProviderDriverKind.make("codex");
14 }).pipe(
15 withMetrics({
16 counter,

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 01f2b68276a37669fed81422e58a5699af8d8fab. Configure here.

apps/server/src/observability/Metrics.test.tsline 97Outdated · 01f2b68Open thread
……@@ -93,15 +94,15 @@ describe("withMetrics", () => {
9495 assert.equal(
9596 hasMetricSnapshot(snapshots, "with_metrics_lazy_total", {
96- provider: "codex",
97+ provider: ProviderDriverKind.make("codex"),
CcursorBoton line 97Apr 29, 2026

Metric test asserts branded type against plain string attribute

Low Severity

The metric attributes closure on line 88 uses a plain let provider = "unknown" (line 79) that is reassigned to the plain string "codex" (line 82). The assertion then checks against ProviderDriverKind.make("codex"). This works at runtime because branded types are structurally just strings, but it creates a misleading inconsistency — the recorded attribute is a plain string while the assertion uses a branded type. If the intent is to verify that production code passes ProviderDriverKind values into metric attributes, the test setup (line 82) should also use ProviderDriverKind.make("codex") to mirror real usage; otherwise it's testing a coincidence rather than the intended contract.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 01f2b68276a37669fed81422e58a5699af8d8fab. Configure here.

Added 4 commits

  • Preserve provider registry cache on sync failuresb9c11e7
  • Add provider model preference management17ce24c
  • Merge main and resolve OpenCode provider testee63c7b
  • Refactor provider home handling into effectful registryc45a05a
apps/server/src/provider/Layers/OpenCodeAdapter.tslines 1149–1149Outdated · c45a05aOpen thread
……@@ -433,957 +444,990 @@ const stopOpenCodeContext = Effect.fn("stopOpenCodeContext")(function* (
1146+ (context.session.model
1147+ ? { instanceId: boundInstanceId, model: context.session.model }
1148+ : undefined);
1149+ if (modelSelection?.instanceId !== boundInstanceId) {
MmacroscopeappBoton lines 1149–1149Apr 29, 2026

🟢 Low Layers/OpenCodeAdapter.ts:1149

When modelSelection is undefined (no model from input.modelSelection and no context.session.model), the validation at line 1149 produces a confusing error message. modelSelection?.instanceId evaluates to undefined, which is not equal to boundInstanceId, triggering the error "OpenCode model selection is bound to instance 'undefined', expected '...'". The actual problem is that no model was specified at all, not an instance mismatch. A subsequent validation at line 1156-1163 would more accurately report "must use the 'provider/model' format" but is never reached due to the earlier check. Consider checking for modelSelection existence before validating instanceId, or adjusting the error message to distinguish "no model" from "wrong instance".

No longer relevant as of 245ec796d1faad7eb4e454999f82c41824c9e35c

Added 1 commit

  • Add provider instance registry wizard48c6bf9
apps/web/index.htmllines 100–100Outdated · 48c6bf9Open thread
……@@ -97,5 +97,6 @@
9797 </div>
9898 </div>
9999 <script type="module" src="/src/main.tsx"></script>
100+ <script src="https://ui.sh/ui-picker.js"></script>
MmacroscopeappBoton lines 100–100Apr 29, 2026

🔴 Critical web/index.html:100

The new <script src="https://ui.sh/ui-picker.js"> loads and executes arbitrary JavaScript from an untrusted external domain with no integrity check, granting it full access to the page's DOM, cookies, localStorage (including t3code:theme and any auth tokens), and the ability to exfiltrate data or modify the application. If this inclusion is intentional, document the security review and add Subresource Integrity; otherwise remove it.

No longer relevant as of 245ec796d1faad7eb4e454999f82c41824c9e35c

Added 2 commits

  • Tighten OpenCode instance validation375dbad
  • Remove UI picker script from web shell245ec79