Proposal
Why
The server has two independent, hand-maintained dependency-injection graphs for
Effect services, and nothing keeps them in sync. A service can be fully and
correctly wired into one and silently absent from the other — TypeScript
typechecks each graph independently and cannot see the gap.
Graph A — legacy, hand-rolled, no compile-time dependency check.
packages/opencode/src/effect/app-runtime.ts builds AppLayer via
Layer.mergeAll(...) over ~40 X.defaultLayer exports, where each
defaultLayer is manually written as
layer.pipe(Layer.provide(Dep1.defaultLayer), Layer.provide(Dep2.defaultLayer), ...).
packages/opencode/src/effect/bootstrap-runtime.ts (BootstrapLayer) and
packages/opencode/src/session/prompt.ts (SessionPrompt.defaultLayer) follow
the same style. Nothing checks that a .pipe(Layer.provide(...)) chain
actually lists every dependency the wrapped layer needs — a missing entry is a
silent gap, not a compile error.
Graph B — LayerNode, compile-time-checked.
packages/core/src/effect/layer-node.ts defines LayerNode.make(layer, deps)
and LayerNode.group(nodes). Its CheckDependencies type produces an actual
TypeScript error ({"Missing dependencies": ...}) when a node's declared
deps don't cover what the wrapped layer requires. This is the graph that
matters at runtime: packages/opencode/src/server/routes/instance/httpapi/server.ts
builds the real HTTP API server — the one every TUI session and opencode run
invocation actually talks to — from LayerNode.group([...]) /
LayerNode.buildLayer(app), not from AppLayer.
Because the two graphs are separately maintained, a service correctly added to
Graph A gives zero signal about Graph B, and vice versa. This has caused three
independent incidents:
- 2026-08-02 (today).
AutoMode.Service
(packages/opencode/src/auto-mode/service.ts) was wired into AppLayer
and, after a first fix attempt, into SessionPrompt.defaultLayer — but was
never added to the LayerNode.group([...]) node list in
server/routes/instance/httpapi/server.ts. Since that list is what
actually serves TUI/opencode run sessions, every prompt crashed at
runtime with Service not found: @opencode/AutoMode, even though
bun run typecheck passed cleanly on both attempts. Fixed by adding
AutoMode.node to server.ts's node list.
- Side finding:
SessionPrompt.defaultLayer's .pipe(Layer.provide(...))
chain was sitting at exactly 20 arguments — TypeScript's pipe()
overload ceiling. A 21st argument produces a hard arity error
(TS2554: Expected 0-20 arguments, but got 21) and the whole chain's
inferred R degrades to unknown, breaking a dozen unrelated files that
structurally depend on that type. The workaround (folding the new layer
into an existing Layer.mergeAll(...) argument instead of adding a new
.pipe() slot) works but is a trap for the next person who doesn't know
the ceiling exists.
- 2026-07-19 (
openspec/changes/intake-20260718-203617-8301aa/.skein/agent-notes.md):
identical failure mode for PatternDetection —
"PatternDetection.node missing from LayerNode.make at prompt.ts:1779" →
"typecheck error + service-not-found in tests."
openspec/changes/retire-auto-reply/ (open, unstarted): documents the
inverse failure mode. auto-reply, automation/automation-features,
pattern-detection, and scheduler all export layer/defaultLayer but
were never given a LayerNode node and never added to server.ts at
all — so instead of crashing, they are silently inert. opencode auto-reply --enable reports success and does nothing, which that proposal calls out
as "worse than not having it. It cost real debugging time to establish
that it is inert."
Same root cause, three incidents, two opposite symptoms (silent no-op vs.
runtime crash) depending on which side of the split a service lands on. This
is a systemic gap, not a one-off mistake, and it will keep recurring for every
new service until the graphs are unified or guarded.
What Changes
- Audit every service that exports a legacy
layer/defaultLayer pair and
determine whether it also exports and registers a LayerNode .node in
server/routes/instance/httpapi/server.ts's node list. Produce a complete
mismatch list in both directions.
- Decide and document the canonical mechanism going forward.
LayerNode has
compile-time dependency checking; the legacy .pipe(Layer.provide(...))
style does not and has a silent arity ceiling. Recommendation: migrate
app-runtime.ts's AppLayer and bootstrap-runtime.ts's BootstrapLayer
onto LayerNode, retiring the hand-rolled defaultLayer composition
pattern where it duplicates what a .node already expresses.
- If a full migration is judged too large for one change, land a regression
guard instead (e.g. a script/CI check that fails when a service has a
defaultLayer export but no corresponding .node registered in
server.ts, or vice versa) so this class of bug is caught before merge
rather than at runtime.
- Flag
session/prompt.ts's SessionPrompt.defaultLayer specifically — it is
at the .pipe() argument ceiling today and the next added dependency will
hit the same trap unless the chain is restructured (e.g. via LayerNode,
or by pre-emptively grouping more entries into Layer.mergeAll(...)).
- Cross-reference
openspec/changes/retire-auto-reply/ — its Phase 1 audit
(auto-reply, automation-features, pattern-detection, scheduler) is the same
"who's actually registered in the graph" question from the opposite
direction, and should reuse this change's audit method rather than
duplicating it.
Non-Goals
- Not rewriting every service's DI wiring in one change — this is audit,
canonicalization decision, and (if in scope) a regression guard, not a
wholesale rewrite.
- Not deleting the auto-reply/automation/pattern-detection/scheduler code —
that is retire-auto-reply's job; this change only supplies the audit
method and flags the shared root cause.
Impact
- Affected:
packages/opencode/src/effect/app-runtime.ts,
packages/opencode/src/effect/bootstrap-runtime.ts,
packages/opencode/src/session/prompt.ts,
packages/opencode/src/server/routes/instance/httpapi/server.ts,
packages/core/src/effect/layer-node.ts, and every service module that
exports defaultLayer/node.
- No user-facing behavior change is intended beyond eliminating a class of
runtime crash / silent-dead-service bug.
Disposition (2026-09-18)
Archived — superseded/obsolete. Unified upstream (451876b): one LayerNode graph in app-runtime.ts and server.ts already includes the fork nodes. The fork's dead re-created defaultLayer graph was deleted 2026-09-18 under retire-legacy-compat-shims.
Tasks
Phase 1: Audit the split
Phase 2: Canonicalize
Phase 3: Fix the .pipe() arity trap
Phase 4: Reconcile with retire-auto-reply
Phase 5: Verification
Unchecked items above: see Disposition in proposal.md (2026-09-18).
Related
Proposal
Why
The server has two independent, hand-maintained dependency-injection graphs for
Effect services, and nothing keeps them in sync. A service can be fully and
correctly wired into one and silently absent from the other — TypeScript
typechecks each graph independently and cannot see the gap.
Graph A — legacy, hand-rolled, no compile-time dependency check.
packages/opencode/src/effect/app-runtime.tsbuildsAppLayerviaLayer.mergeAll(...)over ~40X.defaultLayerexports, where eachdefaultLayeris manually written aslayer.pipe(Layer.provide(Dep1.defaultLayer), Layer.provide(Dep2.defaultLayer), ...).packages/opencode/src/effect/bootstrap-runtime.ts(BootstrapLayer) andpackages/opencode/src/session/prompt.ts(SessionPrompt.defaultLayer) followthe same style. Nothing checks that a
.pipe(Layer.provide(...))chainactually lists every dependency the wrapped layer needs — a missing entry is a
silent gap, not a compile error.
Graph B —
LayerNode, compile-time-checked.packages/core/src/effect/layer-node.tsdefinesLayerNode.make(layer, deps)and
LayerNode.group(nodes). ItsCheckDependenciestype produces an actualTypeScript error (
{"Missing dependencies": ...}) when a node's declareddepsdon't cover what the wrapped layer requires. This is the graph thatmatters at runtime:
packages/opencode/src/server/routes/instance/httpapi/server.tsbuilds the real HTTP API server — the one every TUI session and
opencode runinvocation actually talks to — from
LayerNode.group([...])/LayerNode.buildLayer(app), not fromAppLayer.Because the two graphs are separately maintained, a service correctly added to
Graph A gives zero signal about Graph B, and vice versa. This has caused three
independent incidents:
AutoMode.Service(
packages/opencode/src/auto-mode/service.ts) was wired intoAppLayerand, after a first fix attempt, into
SessionPrompt.defaultLayer— but wasnever added to the
LayerNode.group([...])node list inserver/routes/instance/httpapi/server.ts. Since that list is whatactually serves TUI/
opencode runsessions, every prompt crashed atruntime with
Service not found: @opencode/AutoMode, even thoughbun run typecheckpassed cleanly on both attempts. Fixed by addingAutoMode.nodetoserver.ts's node list.SessionPrompt.defaultLayer's.pipe(Layer.provide(...))chain was sitting at exactly 20 arguments — TypeScript's
pipe()overload ceiling. A 21st argument produces a hard arity error
(
TS2554: Expected 0-20 arguments, but got 21) and the whole chain'sinferred
Rdegrades tounknown, breaking a dozen unrelated files thatstructurally depend on that type. The workaround (folding the new layer
into an existing
Layer.mergeAll(...)argument instead of adding a new.pipe()slot) works but is a trap for the next person who doesn't knowthe ceiling exists.
openspec/changes/intake-20260718-203617-8301aa/.skein/agent-notes.md):identical failure mode for
PatternDetection—"
PatternDetection.nodemissing fromLayerNode.makeat prompt.ts:1779" →"typecheck error + service-not-found in tests."
openspec/changes/retire-auto-reply/(open, unstarted): documents theinverse failure mode.
auto-reply,automation/automation-features,pattern-detection, andschedulerall exportlayer/defaultLayerbutwere never given a
LayerNodenodeand never added toserver.tsatall — so instead of crashing, they are silently inert.
opencode auto-reply --enablereports success and does nothing, which that proposal calls outas "worse than not having it. It cost real debugging time to establish
that it is inert."
Same root cause, three incidents, two opposite symptoms (silent no-op vs.
runtime crash) depending on which side of the split a service lands on. This
is a systemic gap, not a one-off mistake, and it will keep recurring for every
new service until the graphs are unified or guarded.
What Changes
layer/defaultLayerpair anddetermine whether it also exports and registers a
LayerNode.nodeinserver/routes/instance/httpapi/server.ts's node list. Produce a completemismatch list in both directions.
LayerNodehascompile-time dependency checking; the legacy
.pipe(Layer.provide(...))style does not and has a silent arity ceiling. Recommendation: migrate
app-runtime.ts'sAppLayerandbootstrap-runtime.ts'sBootstrapLayeronto
LayerNode, retiring the hand-rolleddefaultLayercompositionpattern where it duplicates what a
.nodealready expresses.guard instead (e.g. a script/CI check that fails when a service has a
defaultLayerexport but no corresponding.noderegistered inserver.ts, or vice versa) so this class of bug is caught before mergerather than at runtime.
session/prompt.ts'sSessionPrompt.defaultLayerspecifically — it isat the
.pipe()argument ceiling today and the next added dependency willhit the same trap unless the chain is restructured (e.g. via
LayerNode,or by pre-emptively grouping more entries into
Layer.mergeAll(...)).openspec/changes/retire-auto-reply/— its Phase 1 audit(auto-reply, automation-features, pattern-detection, scheduler) is the same
"who's actually registered in the graph" question from the opposite
direction, and should reuse this change's audit method rather than
duplicating it.
Non-Goals
canonicalization decision, and (if in scope) a regression guard, not a
wholesale rewrite.
that is
retire-auto-reply's job; this change only supplies the auditmethod and flags the shared root cause.
Impact
packages/opencode/src/effect/app-runtime.ts,packages/opencode/src/effect/bootstrap-runtime.ts,packages/opencode/src/session/prompt.ts,packages/opencode/src/server/routes/instance/httpapi/server.ts,packages/core/src/effect/layer-node.ts, and every service module thatexports
defaultLayer/node.runtime crash / silent-dead-service bug.
Disposition (2026-09-18)
Archived — superseded/obsolete. Unified upstream (451876b): one LayerNode graph in app-runtime.ts and server.ts already includes the fork nodes. The fork's dead re-created defaultLayer graph was deleted 2026-09-18 under retire-legacy-compat-shims.
Tasks
Phase 1: Audit the split
defaultLayer(the legacylayer.pipe(Layer.provide(...))style) acrosspackages/opencode/srcand
packages/core/src. -grep -rln "export const defaultLayer" packages/opencode/src packages/core/src- Validation: complete file list recorded in.skein/agent-notes.md.node(aLayerNode.make(...)/LayerNode.group(...)call) and whether thatnodeis actually present in theLayerNode.group([...])list inpackages/opencode/src/server/routes/instance/httpapi/server.ts. - Validation: table of{service, has defaultLayer, has node, node registered in server.ts}recorded in.skein/agent-notes.md.defaultLayerexists but thenodeis missingor unregistered — these are live crash risks (the
AutoMode/PatternDetectionfailure mode: reachable viaAppLayer/direct calls,absent from the graph that actually serves requests). - Validation: list of mismatches, each with the call site(s) that would
trigger a runtime
Service not foundif exercised.nodeexists but is only reachable throughLayerNode.group([...])in server.ts and never throughAppLayer/BootstrapLayer— confirm whether anything outside the HTTP server path(CLI-only commands,
bootstrap-runtime.tsconsumers) needs that serviceand would break. - Validation: list of any such gaps, or explicit note that none exist.
Phase 2: Canonicalize
LayerNode,since
CheckDependenciesgives compile-time missing-dependency errorsthat the legacy
.pipe(Layer.provide(...))style cannot). Record thedecision and rationale in
design.md.app-runtime.ts'sAppLayerandbootstrap-runtime.ts'sBootstrapLayerontoLayerNode.group(...)/LayerNode.buildLayer(...), reusing each service's existing.nodeexport instead of re-deriving dependencies by hand. - Validation:
bun run typecheckandbun testgreen inpackages/opencodeafter the port;opencode runand TUI sessionsmoke-tested manually per
[[run]]-style verification (seeAGENTS.md/ project conventions for how this repo verifies CLIchanges).
instead — a script (invoked from CI or
bun run typecheck) that failswhen a service exporting
defaultLayerhas no corresponding.noderegistered in
server.ts's node list, and vice versa. - Validation: guard fails on a deliberately-reintroduced version oftoday's bug (temporarily remove
AutoMode.nodefrom server.ts andconfirm the guard catches it), then passes with the fix restored.
Phase 3: Fix the
.pipe()arity trap.pipe(Layer.provide(...), ...)chain in the codebase (app-runtime.ts,bootstrap-runtime.ts,session/prompt.ts, and any others found inPhase 1). Flag any chain at or near TypeScript's
pipe()overloadceiling (20 arguments) as fragile. - Validation: list of chains with their current argument counts.
SessionPrompt.defaultLayer(
packages/opencode/src/session/prompt.ts) so it is not sitting at theceiling — either by migrating it to
LayerNode(if 2.2 is in scope) orby pre-emptively grouping more of its dependencies into the existing
Layer.mergeAll(...)argument so future additions don't require a newtop-level
.pipe()slot. - Validation:bun run typecheckpasses; adding a throwaway extraLayer.provide(SomeExisting.defaultLayer)no longer producesTS2554.Phase 4: Reconcile with retire-auto-reply
openspec/changes/retire-auto-reply/Phase 1 (its own audit ofauto-reply/automation-features/pattern-detection/scheduler) lands,
cross-check its findings against this change's Phase 1 table — both are
answering "is this service actually registered in the graph that
matters" and should agree. - Validation: no contradictions between the two audits; any found are
resolved and noted in both changes.
Phase 5: Verification
bun run typecheck(root, all packages) andbun test packages/opencode --timeout 60000green.exercises tool permission checks (confirms the actual HTTP API graph,
not just
AppLayer, is exercised). - Validation: noService not founderrors in server logs.Related