Drivers, instances, registries, and multi-instance routing
T3 Code separates an open driver kind from configured instance identity, discovery snapshots, durable thread bindings, native sessions, and continuation compatibility so several accounts of one harness can coexist.
What this chapter resolves
- Name every provider identity and use only providerInstanceId as the normal routing key.
- Derive explicit-versus-legacy settings precedence, enabled state, secret materialization, and unavailable shadows.
- Follow instance construction, child-scope replacement, snapshot aggregation, binding lookup, adoption, and resume.
- Explain what configuration reload and recovery do not guarantee.
“Which provider owns this thread?” sounds like one lookup. In the implementation it is an identity chain:
T3 thread → persisted provider-instance id → current ProviderInstance
→ captured adapter → native session / resume cursor
The driver kind travels beside that chain as an implementation and consistency label. A native account may be visible in a discovery snapshot. Neither replaces the instance id as the normal route.
This separation is what lets codex_personal and codex_work use the same Codex
driver with different homes, environment, credentials, processes, snapshots, and
adapter state.
The identity ladder
* Splits the historical "provider kind" concept into two:
*
* - `ProviderDriverKind` is the implementation kind selector (e.g. codex,
* claudeAgent, a fork's `ollama`, …). It picks which driver package
* handles the protocol, the probe, the adapter, and text generation.
*
* - `ProviderInstanceId` is the routing key (a user-defined slug).
* Threads, sessions, runtime events, and persisted bindings reference
* instance ids — never driver kinds — so a user can configure multiple
* instances of the same driver (e.g. `codex_personal` + `codex_work`),
* each with independent driver-specific configuration.
*
* Forward/backward compatibility invariant
* ----------------------------------------
* `ProviderDriverKind` is intentionally an **open** branded slug, not a closed
* literal union. The server hosts forks, ships in PRs that add drivers, and
* users frequently roll between branches and forks. Any of those paths can
* leave `ServerSettings`, persisted thread state, or session bindings
* referencing a driver that the currently-running build does not know about.
*
* The rule: parsing any of those payloads must always succeed, and the
* runtime is responsible for marking the unknown driver/instance as
* "unavailable" rather than crashing. Built-in drivers shipped by the core
* product happens to register in a given build is not part of the contract
* layer. Driver availability is discovered through the runtime registry.
*
* Driver-specific configuration is similarly opaque at the contracts layer:
* drivers live in (or will be extracted to) their own packages and own their
* config schemas. The contracts package only knows the envelope.| Identity | Example | Owner and role | Routing key? |
|---|---|---|---|
| Driver kind | codex | open branded slug selecting implementation, config decoder, probe, adapter, and text generation | No; it verifies implementation and names the default legacy instance |
| Instance id | codex_work | user-defined slug naming one materialized configuration and its scoped closures | Yes |
| Discovery snapshot | models, auth state, health | UI-facing observation for one instance; cached and refreshed independently | No; its instanceId field points back to the route |
| Native account | provider email or login | provider-reported observation; may affect access and models | No |
| T3 thread id | thread_… | durable product conversation/work item | Entry to the persisted binding lookup |
| Native session id | app-server, SDK, ACP, or ses_… | adapter/provider-owned continuation identity | No; wrapped in an opaque resume cursor |
| Continuation key | codex:instance:codex_work | driver-defined compatibility group used when deciding whether a session may continue across instance changes | No; a compatibility predicate |
ProviderDriverKind is intentionally not a closed five-literal union. The shipped
build registers Codex, Claude, Cursor, Grok, and OpenCode, but settings and persisted
state can outlive a build, move between forks, or refer to a driver that is absent
after rollback. Schema decoding accepts any valid slug; the runtime makes absence
visible.
Drivers are factories; instances are scoped records
* `ProviderDriver` is a record, not a Context.Service. The thing it produces
* (`ProviderInstance`) is also a record — three captured closures
* (`snapshot`, `adapter`, `textGeneration`), an id, and a driver kind. There
* are intentionally no per-driver Context tags because tags are
* singleton-per-runtime and we need many instances of the same driver.
*
* The only Effect service involved is `ProviderInstanceRegistry`, which
* owns the live `Map<InstanceId, ProviderInstance>` and is itself a
* singleton.
*
* Driver factories are functions of `(typed config, env)` where:
* - `typed config` is decoded once by the registry via `configSchema`,
* so drivers never deal with raw `unknown`.
* - `env` flows through Effect's R channel. Each driver declares the
* subset of infrastructure services it needs (FileSystem,
* ChildProcessSpawner, …) on its `create` return type; the registry
* layer's R is the union of those, and the runtime layer satisfies it./**
* One materialized provider instance. Held by the registry, looked up by
* `instanceId`, torn down by closing the scope it was created in.
*
* The three "shape" fields are captured closures owned by this instance —
* stopping one instance cannot affect another, and starting a second
* instance of the same driver does not reach into the first instance's
* state.
*/
export interface ProviderInstance {
readonly instanceId: ProviderInstanceId;
readonly driverKind: ProviderDriverKind;
readonly continuationIdentity: ProviderContinuationIdentity;
readonly displayName: string | undefined;
readonly accentColor?: string | undefined;
readonly enabled: boolean;
readonly snapshot: ServerProviderShape;
readonly adapter: ProviderAdapterShape<ProviderAdapterError>;
readonly textGeneration: TextGeneration.TextGeneration["Service"];
}
export interface ProviderContinuationIdentity {
readonly driverKind: ProviderDriverKind;
readonly continuationKey: string;
}
export function defaultProviderContinuationIdentity(input: {
readonly driverKind: ProviderDriverKind;
readonly instanceId: ProviderInstanceId;
}): ProviderContinuationIdentity {
return {
driverKind: input.driverKind,
continuationKey: `${input.driverKind}:instance:${input.instanceId}`,
};
}
/**
* Inputs the registry passes to a driver's `create` function.
*
* `config` is the typed payload — already decoded by the registry through
* `driver.configSchema`. Drivers never decode their own raw envelope.
*/
export interface ProviderDriverCreateInput<Config> {
readonly instanceId: ProviderInstanceId;
readonly displayName: string | undefined;
readonly accentColor?: string | undefined;
readonly environment: ProviderInstanceEnvironment;
readonly enabled: boolean;
readonly config: Config;
}
/**
* Driver SPI — registered as a plain value, not a Layer.
*
* `Config` is whatever the driver decoded from
* `ProviderInstanceConfig.config`. `R` is the union of infrastructure
* services the driver depends on; the registry layer aggregates `R` across
* all registered drivers and the runtime supplies them.
*
* `create` is responsible for *all* per-instance state — process handles,
* pubsub topics, refs, file watchers — and must release them when its
* scope closes. Two calls to `create` with different `instanceId` /
* `config` MUST yield instances with no shared mutable state.A ProviderDriver contributes:
- an open
driverKindand static presentation metadata; - a schema for its opaque config envelope and a typed default;
- a scoped
createfunction.
The registry decodes config once, then passes the typed value, instance id,
presentation overrides, effective enabled flag, and materialized environment into
create. The result is a ProviderInstance containing three captured closures:
discovery snapshot, provider adapter, and text generation.
This record-based SPI avoids the singleton trap. Two Context tags named “Codex” would still describe one service per Effect environment; two ordinary instance records can each own different Refs, PubSubs, child processes, file watchers, and finalizers.
Settings hydrate an explicit map over legacy defaults
T3 Code is midway through a provider-instance migration. Settings contain both:
- legacy typed blobs under
settings.providers.<kind>; - a new
providerInstancesrecord whose keys are instance ids and whose values carry driver, opaque config, generic environment, enabled state, and optional presentation.
Hydration begins with the explicit map. For each built-in driver it computes the
default instance id—literally the driver slug. Only if that key is absent does it
synthesize an instance from the legacy blob. An explicit providerInstances.codex
entry therefore wins over providers.codex; a custom codex_work exists alongside
either default.
Enabled state has its own precedence. Explicit false in the instance envelope or
decoded driver config always disables. Otherwise the envelope value wins, then the
driver config value, then the default is enabled.
Zoom with the controls, +/−, or Ctrl/⌘ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.
Text equivalent
A ServerSettings change enters the hydration watcher. Explicit providerInstances are copied first; for each built-in with no explicit default-id entry, its legacy providers blob is synthesized. For each changed entry the instance registry closes the previous child scope before constructing a replacement. A registered driver with valid config creates a live instance. An unknown driver, invalid config, or create error creates an unavailable shadow. Unchanged live instances keep the same object and scope. The registry sets its live-entry Ref and then its unavailable-shadow Ref; these are two writes, not an atomic map swap. It publishes one change notification only if entry identity/order or unavailable state changed. ProviderRegistry re-pulls discovery snapshot sources and ProviderService re-pulls adapter streams after that tick. Persisted thread bindings remain in SQLite; a later routed operation can adopt or resume through the current adapter, or fail if no live route or cursor exists.
apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts:65–134 ↗apps/server/src/provider/Layers/ProviderInstanceRegistryLive.ts:118–214 ↗apps/server/src/provider/Layers/ProviderInstanceRegistryLive.ts:228–320 ↗apps/server/src/provider/Layers/ProviderAdapterRegistry.ts:36–100 ↗apps/server/src/provider/Layers/ProviderRegistry.ts:511–693 ↗apps/server/src/provider/Layers/ProviderService.ts:411–544 ↗Unavailable shadows preserve configuration truth
Three construction failures become UI-visible shadows instead of server-boot failures:
- the driver slug is valid but not registered in this build;
- the registered driver’s config decoder rejects the opaque value;
- driver creation returns
ProviderDriverError.
The shadow is disabled, not installed, in error status, has unknown auth, carries
the exact unavailable reason, and appears in discovery aggregation. It is excluded
from live adapter lookup and listInstances, so a caller cannot accidentally route
a turn to a presentation-only object.
This is rollback/fork tolerance: “I understand the saved shape but cannot run it” is represented as product state, not a corrupt settings file.
Environment values are materialized, not sandboxed
Every instance can define generic environment entries. Sensitive entries are
persisted in the server secret store under a name derived from instance id and
variable name. Client-facing settings replace their value with an empty string and
set valueRedacted: true; server-internal settings materialize the secret before
driver construction. A redacted patch preserves the existing secret.
At process construction, the materialized instance environment overlays inherited
process.env. An intentional empty string overrides the inherited value. This is
ordinary child-process configuration—not isolation from the parent environment.
Discovery and routing are two registries
ProviderRegistry aggregates snapshots for presentation: availability, models,
authentication, health, maintenance state, and per-instance refresh streams. It
can hydrate a correlated cache at boot and retain selected prior model metadata
while a fresh probe is partial.
ProviderAdapterRegistry resolves live behavior. It is a stateless facade over
the instance registry: getByInstance, getInstanceInfo, listInstances, and a
change stream. Snapshot presence is not sufficient to route; a shadow is visible
to the first registry and absent from the second.
These surfaces all key by instance id, but their correlation checks live at different
boundaries. ProviderRegistry validates that a discovery snapshot’s driver and
instance match its source. ProviderService performs the corresponding check for hot
runtime events. ProviderAdapterRegistry is the exact live lookup/list/change facade;
it does not perform either payload-correlation check.
The durable route is recovered lazily
For an operation such as send, interrupt, approval, structured input, or rollback,
ProviderService performs this decision:
- load the persisted binding for the T3 thread;
- require its instance id and resolve that exact current adapter;
- if the adapter already has the thread session, route immediately;
- otherwise, when recovery is allowed, invoke the recovery helper; it rechecks the adapter and can list, adopt, and upsert an existing session;
- if that recheck finds no listed session, require an opaque resume cursor;
- restore persisted cwd/model/runtime mode, prepare MCP, and call
startSessionwith that cursor; - validate the returned driver and update the durable binding.
stopSession deliberately does not recover a stopped process merely to stop it.
Starting a new session also stops stale live sessions for the same T3 thread on
other current instances.
Zoom with the controls, +/−, or Ctrl/⌘ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.
Text equivalent
Given a thread operation, ProviderService reads its persisted binding. With no binding it returns validation failure. It requires the binding's provider-instance id and resolves that exact live adapter; an absent or unavailable configured instance fails instead of falling back to another instance of the same driver. If the first adapter.hasSession check is true, resolveRoutableSession returns a direct active route without listSessions, adoption, or a binding write. If no session is active and the operation disallows recovery, it continues as inactive only for stop semantics. When recovery is allowed, recoverSessionForThread rechecks the adapter; if listSessions yields the thread, that helper adopts the returned session and refreshes the binding. Otherwise it requires a resume cursor, restores persisted cwd, model selection and runtime mode, prepares the MCP session, and invokes adapter.startSession. Success validates driver identity and upserts the binding; native failure propagates.
Default continuation identity includes both driver and instance. Codex and Claude can override it with home/layout-derived groups, so a same-driver instance switch is compatible only when both driver kind and continuation key match. A shared native login by itself is not enough evidence.
Route a conceptual provider fleet
Use the lab to compare a legacy default, an explicit same-driver work instance, an unknown-driver shadow, a removed instance with an old binding, and a rebuilt instance with or without resume state. The animation separates settings materialization from live lookup and lazy recovery.
Which configured runtime receives the thread?
Walk from authored settings to one exact route. Discovery visibility, live behavior, and resume eligibility are different checks.
Legacy Codex default; stage 1 of 6: Settings. Routable default instance.
Settings
providers.codex exists; providerInstances.codex is absent.
providers.codex exists; providerInstances.codex is absent.
Synthesize the default instance id `codex` from the legacy typed blob.
Registered Codex driver decodes config and creates one scoped live instance.
UI snapshot and routable adapter both use instanceId `codex`.
Thread binding names `codex`; driver kind verifies `codex`.
Exact adapter lookup succeeds; an active session routes immediately.
Static fleet-routing matrix
| Scenario | Hydration / construction | Binding / operation | Outcome and secret boundary |
|---|---|---|---|
| Legacy Codex default | Synthesize the default instance id `codex` from the legacy typed blob. Registered Codex driver decodes config and creates one scoped live instance. | Thread binding names `codex`; driver kind verifies `codex`. Exact adapter lookup succeeds; an active session routes immediately. | Routable default instance Only marked sensitive environment values are split/redacted. |
| Explicit work account | Keep `codex_work` exactly as authored. It does not overwrite the default `codex` key. The same driver factory creates independent adapter, snapshot, and text-generation closures. | A work thread persists `providerInstanceId: codex_work`. Lookup cannot fall back to `codex`; it routes the work instance only. | Two same-driver instances stay isolated Sensitive per-instance environment can point each process at different homes or credentials. |
| Explicit default overrides legacy | The explicit `providerInstances.codex` envelope wins; no legacy copy is synthesized for that id. Decode the explicit opaque config and compute enabled state; nested false dominates. | Existing default bindings still name `codex`, now resolving the replacement configuration. If the object changed, the old scope closes first; later work may need cursor recovery. | Explicit configuration wins, without live-session handoff A redacted sensitive patch preserves the prior secret rather than writing an empty value. |
| Unknown fork driver | The open driver slug parses successfully and remains in the effective map. No registered driver exists, so construction yields an unavailable shadow. | A persisted binding can still name `research`, but no live adapter is listed. getByInstance fails; the service does not silently choose another account or driver. | Visible configuration, intentionally unroutable Unknown opaque config is preserved; the contracts layer does not attempt driver-specific decoding. |
| Changed instance with cursor | The same instance id now carries a different entry. Close the old child scope before creating the replacement instance and stream. | SQLite still maps the thread to codex_work and retains opaque resume data. No active replacement session: an eligible operation calls startSession with saved cursor/mode/cwd/model. | Lazy resume attempt; not zero-downtime migration Changed sensitive values are materialized before the new driver scope is created. |
| Removed instance binding | codex_work disappears from the effective map. The old child scope closes and no replacement is created. | The durable thread binding is not automatically reassigned or deleted. Exact route lookup fails; another Codex instance cannot claim the thread implicitly. | Durable binding stranded until explicit repair/configuration Removing a sensitive environment entry also removes its named secret through settings logic. |
| Live instance, no resume state | No settings change is required. The current instance and adapter are live. | The route is exact but its resume cursor is null/absent. Recovery-capable operations return validation failure instead of starting an unrelated fresh conversation. | Routable instance, unrecoverable conversation Credentials may be healthy and still cannot reconstruct missing provider continuation state. |
| External OpenCode password | The explicit config is decoded by the OpenCode driver. No local server scope is owned for an external URL; the client uses supplied Basic auth. | Threads still route by instance id, never by server URL or password. The adapter calls the external server for that configured instance. | Routable external instance with a plaintext-settings exception serverPassword is documented and tested as plaintext in settings; password UI styling is not encryption. |
Evidence and remaining seams
Registry tests construct two Codex instances and verify distinct adapter,
text-generation, and snapshot closures. They cover nested enabled-state precedence
and boot-safe unknown-driver shadows. Settings tests verify that marked sensitive
environment values do not enter settings.json, while redacted updates preserve
the stored secret. ProviderService tests exercise persisted routing, resume,
stale-session cleanup, event correlation, and input validation.
The inspected tests do not inject a hard crash into every instruction of child-scope
replacement and subsequent lazy recovery. They also do not enforce
supportsMultipleInstances: false, because the production registry does not yet
implement that documented branch.