How has effect usage evolved in opencode?
Tell me about usage of effect in this codebase. How has it advanced or not over time, and pros and cons you thing there’ll be over time.
##Short version
Effect has moved from an incremental utility into a major architectural foundation of OpenCode. It now underpins:
- service definitions and dependency injection
- application and location-scoped runtimes
- typed errors and domain schemas
- concurrency, cancellation, streams, and resource lifecycles
- HTTP APIs and generated clients
- database adapters
- plugin contracts
- much of the test infrastructure
But the migration is not complete or uniform. Promise-based code, legacy v1 domains, Effect bridges, and two overlapping application structures remain. I would characterize the current codebase as deeply Effect-based, but still in an active consolidation phase.
A repository-wide search finds Effect imports in well over 1,000 TypeScript source/test files, although that includes generated code and tests.
##How Effect is being used
###1. Services and dependency injection
Most important domains expose an interface whose operations return Effect.Effect, paired with a Context.Service:
export interface Interface {
readonly run: (...) => Effect.Effect<void, RunError>
}
export class Service extends Context.Service<Service, Interface>()(
"@opencode/v2/SessionRunner",
) {}
See packages/core/src/session/runner/index.ts.
This pattern appears throughout sessions, tools, filesystem, configuration, permissions, plugins, projects, credentials, LLM execution, and server code. Dependencies are obtained from the Effect environment rather than imported as mutable singletons or passed through every function.
The operation types can carry:
- success type
- expected error type
- required services/environment
For example, Session operations distinguish things such as NotFoundError, MessageDecodeError, and unavailable operations in packages/core/src/session.ts.
###2. A custom typed Layer graph
OpenCode has gone beyond using ordinary Layer.merge calls. It has built LayerNode, a typed application dependency graph that:
- records explicit node dependencies
- checks for missing dependencies at compile time
- detects cycles
- supports implementation replacement for tests and alternate hosts
- groups services into application tiers
- hoists global services out of location-scoped graphs
See packages/core/src/effect/layer-node.ts and the global/location tags in packages/core/src/effect/app-node.ts.
One major use is constructing per-location service sets:
export const locationServices = LayerNode.group([
Location.node,
Policy.node,
Config.node,
AgentV2.node,
// ...
SessionRunnerLLM.node,
])
Those sets are placed in an Effect LayerMap with an idle TTL in packages/core/src/location-services.ts. This is a fairly sophisticated use of Effect: a workspace/location gets an isolated, scoped collection of services, while process-wide services can be shared.
The older/main opencode application runtime similarly assembles a large graph into a ManagedRuntime in packages/opencode/src/effect/app-runtime.ts.
###3. Structured concurrency and lifecycle management
Effect is particularly useful in the agent/session runtime, where OpenCode has long-lived work, interruption, races, and cleanup.
packages/opencode/src/effect/runner.ts uses:
FiberDeferredSynchronizedRefScope- interruption causes
- finalizers
to coordinate foreground session runs and shell work. This is exactly the kind of stateful concurrent workflow where plain Promise and AbortController code tends to become difficult to reason about.
The embedded SDK is scoped as well. packages/sdk-next/src/opencode.ts uses Effect.acquireRelease so closing the owning scope disposes the in-memory web handler and related resources.
###4. Effect Schema as the domain-contract system
Effect Schema is now pervasive. packages/schema defines reusable domain types for:
- sessions and messages
- projects and workspaces
- permissions and questions
- credentials and integrations
- PTYs
- events
- filesystem values
- branded identifiers
For example, packages/schema/src/session.ts and packages/schema/src/event.ts.
These schemas serve several purposes at once:
- runtime decoding and validation
- TypeScript type inference
- transformations and defaults
- branded IDs
- tagged errors
- HTTP/OpenAPI descriptions
- generated client types
That is more substantial than merely replacing Zod syntax. Schema is becoming the shared language between storage, core domain code, HTTP contracts, and client generation.
###5. HTTP API and generated clients
The newer server stack is built with Effect’s HTTP API modules. packages/server/src/routes.ts composes an HttpApiBuilder layer with:
- handlers
- authorization
- schema-error middleware
- location resolution
- application services
The protocol is described with HttpApiEndpoint, HttpApiGroup, and schemas under packages/protocol/src.
The generated Effect client then maps transport/schema failures into client errors and exposes Effect and Stream operations. See packages/client/src/generated-effect/client.ts.
Importantly, the client package keeps Effect optional:
"exports": {
".": "./src/index.ts",
"./effect": "./src/effect.ts"
},
"peerDependenciesMeta": {
"effect": {
"optional": true
}
}
See packages/client/package.json. That gives consumers a zero-Effect Promise client at the package root and an Effect-native API through /effect.
###6. Plugins are moving toward Effect-native contracts
The v2 plugin API models plugins as scoped effects:
export interface Plugin<R = Scope.Scope> {
readonly id: string
readonly effect: (context: PluginContext) =>
Effect.Effect<void, never, R>
}
See packages/plugin/src/v2/effect/plugin.ts.
Registration APIs return scoped registrations, and integration callbacks can expose typed Effect operations. This makes plugin installation/removal and hot reload fit Effect’s lifecycle model rather than relying only on informal cleanup callbacks.
###7. Testing
Effect-specific test helpers provide:
- explicit test and live Layers
TestClockTestConsole- scoped fixtures
- service replacements
- shared or isolated Layer memoization
- cause reporting
See packages/opencode/test/lib/effect.ts.
This is one of the strongest practical benefits of the migration: tests can substitute an implementation in the dependency graph instead of monkey-patching modules or recreating large integration environments.
##How it has advanced over time
The history looks less like “Effect was added once” and more like several increasingly ambitious phases.
###Early/incremental phase
There was already some Effect-based account and utility code by early March 2026. The repository still relied heavily on async facades, Promise APIs, and other schema systems. A March 13 commit explicitly introduced an Effect-to-Zod bridge, suggesting the initial approach was coexistence rather than immediate replacement.
###Service migration: late March through April
The concentrated migration becomes clear in late March:
- MCP lifecycle migrated to Effect
- the event bus moved to Effect
PubSuband scoped state - services began being yielded directly rather than accessed through Promise facades
- provider, auth, files, instructions, session state, tools, and other domains moved into Effect services
Representative historical commits include:
effectify Bus service: migrate to Effect PubSub + InstanceState- service migrations recorded in the repository’s March–April commit history
This was the point where Effect became architecture rather than an isolated implementation tool.
###Schema migration: April into May
The next major front was making Effect Schema canonical:
- configuration schemas
- permissions
- provider/model values
- message and tool-state DTOs
- snapshot and LSP values
- runtime validators
The history contains a large sequence of narrowly scoped Schema migration commits in April. That approach is generally safer than one giant rewrite and explains why legacy v1 and compatibility layers are still visible.
###Tests and runtime boundaries: May
The migration then spread into:
- Effect test runners
- filesystem and process services
- HTTP API tests
- runtime flags
- server tests and shared in-process runtimes
This is significant because a framework migration is not really mature while tests still bypass the same runtime and dependency model used by production.
###Typed application topology: June and afterward
A major architectural step was the custom typed Layer graph:
- June 9:
feat(opencode): add typed application layer graph - June 25:
refactor(core): support tiered layer nodes - June 27: location-node integration
- June 29 onward: test conversion and replacement refinements
- July 3: replacement rewriting during layer hoisting
That progression shows real advancement. The team encountered limitations in manually wiring a large Layer graph and built project-specific tooling around them.
###Effect 4 adoption
The repository is currently on Effect 4.0.0-beta.83, including matching platform, SQL, and OpenTelemetry packages; see package.json. The history shows repeated upgrades through beta 37, 42, 46, 65, 74, and 83.
So the codebase did not merely adopt stable Effect 3 and stop. It has been tracking Effect 4 beta APIs closely.
##Where it has not advanced completely
###1. It is still a mixed Effect/Promise codebase
There are many legitimate Promise boundaries, particularly around third-party SDK callbacks, UI code, authentication plugins, WebSockets, and process APIs. But there are also still substantial Promise-oriented subsystems in packages/opencode/src.
packages/opencode/src/effect/bridge.ts explicitly exists to cross between Effect fibers, Promise callbacks, and an older AsyncLocalStorage workspace context.
That bridge is well considered, but its existence demonstrates that Effect is not yet the only execution model.
###2. Legacy and new architectures coexist
The source tree has:
v1schema/config/session domainsV2servicespackages/opencodeimplementations- newer
packages/core,packages/server,packages/protocol,packages/client, andpackages/sdk-nextboundaries - temporary compatibility layers
For example, packages/core/src/location-services.ts still exports a location service map layer explicitly marked as temporary for backward compatibility.
This duplication makes the current tree appear more complicated than the intended final architecture.
###3. Typed errors are used unevenly
Some domains have precise tagged error channels. Others use:
unknown- defects through
Effect.die/orDie - broad unions
- APIs with no typed error despite wrapping operations that may fail operationally
That is normal during migration, but it means one of Effect’s main promises—accurate failure channels—is only partially realized.
###4. The application needs custom infrastructure around Effect Layers
LayerNode is useful, but it is also evidence that raw Effect Layer composition was not sufficient or ergonomic enough for this application’s topology.
The project has had to implement and maintain:
- dependency checking
- cycle detection
- replacement rewriting
- tier tags
- location/global hoisting
- graph compilation
That is not necessarily a failure, but it is additional framework code contributors need to understand.
###5. It depends on beta and unstable APIs
The current version is beta, while HTTP APIs are imported from paths such as:
effect/unstable/httpeffect/unstable/httpapieffect/unstable/encoding
The repository also carries a local patch for Effect’s SSE/OpenAPI schema handling in patches/effect@4.0.0-beta.83.patch.
This is the clearest concrete sign that the adoption is advanced but not settled.
##Likely long-term advantages
###Better cancellation and resource safety
Agent runtimes combine model streams, tool calls, subprocesses, sockets, HTTP connections, and background work. Fibers and scopes provide one coherent interruption and cleanup model. This should reduce:
- orphaned subprocesses
- leaked subscriptions
- forgotten event listeners
- races between cancellation and completion
- ad hoc
try/finallycleanup
###Stronger domain and transport consistency
Using Effect Schema for domain values, HTTP contracts, OpenAPI, and generated clients reduces the number of independently maintained representations.
The newer architecture is capable of deriving:
- runtime decoders
- domain types
- HTTP contracts
- OpenAPI descriptions
- Promise clients
- Effect clients
from closely related sources.
###Improved test isolation
Explicit service environments and replaceable nodes make it easier to test difficult behaviors such as:
- alternate filesystems
- fake LLM transports
- deterministic clocks
- permission policies
- location-specific services
- scoped cleanup
- server/client integration without a real network listener
###Better observability
Named Effect.fn operations, structured causes, and @effect/opentelemetry provide a path to coherent tracing across model requests, tools, storage, HTTP, and background fibers.
###A better fit for plugin lifecycles
Scoped plugin effects can acquire registrations, event subscriptions, or resources and release all of them reliably during removal or reload. That is much safer than requiring each plugin to manually track every disposer.
##Likely long-term costs and risks
###A high contributor learning curve
A contributor now needs to understand not only TypeScript and OpenCode’s domain, but also:
- Effect’s success/error/environment type parameters
- generators and pipelines
- Context services
- Layers and memoization
- Scope and finalizers
- Fibers, causes, streams, and interruption
- OpenCode’s custom
LayerNodeabstraction
This can make small changes feel disproportionately difficult.
###Type-level complexity and slower feedback
Large service graphs and schema transformations can produce difficult compiler errors and may increase typechecking cost. Custom dependency validation in LayerNode improves correctness but adds another level of conditional types.
###Migration seams can become permanent
Promise bridges and compatibility facades are valuable during migration, but if both styles remain indefinitely, developers must understand two models and subtle context propagation rules.
The comment and implementation in effect/bridge.ts show how easy it is to lose workspace or logging context when crossing those boundaries.
###Upstream churn
Following Effect 4 betas and unstable HTTP APIs means frequent migration work. The repeated beta upgrades and the local Effect patch indicate an ongoing maintenance burden.
This risk should decrease when Effect 4 and the relevant HTTP APIs stabilize, but until then OpenCode is effectively an early adopter.
###Public API coupling
An Effect-native plugin or client API exposes Effect types directly. Consumers must align on a compatible Effect version, understand scopes, and avoid duplicate-version problems.
The optional /effect client export is a good mitigation. Plugin v2 is more coupled because Effect is part of the plugin contract itself.
###Framework-shaped architecture
Effect makes certain designs very attractive: every domain becomes a service, every dependency a Layer, and every operation an Effect. That can improve consistency, but it can also produce ceremony for pure or simple code that would be clearer as an ordinary function.
The best long-term outcome will come from keeping Effect at effectful/concurrent/resource boundaries while allowing ordinary TypeScript for pure transformations and UI-local logic.
##Overall assessment
I think the adoption is delivering real value rather than being merely stylistic. OpenCode is exactly the kind of program that benefits from structured concurrency, scopes, streams, typed failures, and replaceable services.
The main concern is not whether Effect is useful here; it plainly is. The concern is how much migration and framework surface remains in motion simultaneously:
- Effect 4 beta
- unstable HTTP APIs
- a custom application graph
- old and new service trees
- Promise/Effect bridges
- schema and SDK generation changes
If those converge into one stable architecture, the long-term result should be substantially safer and more testable. If compatibility layers and duplicate structures remain, the codebase risks retaining most of Effect’s conceptual cost while only partially realizing its simplification benefits.
END OF POST