This commit is contained in:
James Long 2026-05-17 15:53:11 -04:00
parent 4f455f1869
commit 1c453bd4e1
3 changed files with 207 additions and 88 deletions

View file

@ -4,43 +4,42 @@ Generated for `createSimulatedRoutes` in `packages/opencode/src/server/routes/in
## Notation
- `→ X` means "yields `X.Service` from its `Effect.gen` body"
- `→ X` means "yields `X.Service` from its `Effect.gen` body at layer init (a true `RIn` of `.layer`)"
- `(lazy)` means "uses `InstanceState.context` or similar at call time, not at layer construction"
- `(opt)` means "uses `Effect.serviceOption(X)` — not strictly required"
- `(internal)` means "satisfied internally by the `.layer` itself via `Layer.provide(...)`, NOT a residual requirement"
## Service → Dependencies
## Service → Dependencies (current, post-rebase)
```
─── External / Platform ─────────────────────────────────────────
NodePath (no app deps) provides Path.Path
HttpClient (FetchHttp) (no app deps) provides HttpClient.HttpClient
FetchHttpClient (no app deps) provides HttpClient.HttpClient
HttpServer.layerServices (no app deps)
ChildProcessSpawner (from SimulationSpawner) (no app deps)
─── Leaf services (no app deps) ─────────────────────────────────
Global (no app deps)
Env (no app deps)
Bus (lazy via InstanceState)
SyncEvent (lazy via InstanceState)
AccountRepo (uses Database directly)
PtyTicket (Cache only)
Truncate → AppFileSystem (+ Config opt)
Env (no app deps — uses InstanceState.make, no Service yields)
Bus (no app deps — uses InstanceState.make, no Service yields)
SyncEvent → RuntimeFlags, Bus (NEW — previously listed as leaf/lazy)
AccountRepo (no app deps — pure DB closures)
PtyTicket (no app deps — Cache only)
Truncate → AppFileSystem
─── Middleware/route layers (no app service deps) ───────────────
errorLayer
compressionLayer → HttpServerRequest (builtin)
corsVaryFix
fenceLayer → HttpServerRequest (builtin)
runtime
simulationShareNextLayer provides ShareNext (Layer.succeed override)
─── Simulation overrides ────────────────────────────────────────
simulationBoundary:
SimulationFileSystem provides AppFileSystem + FileSystem.FileSystem(noop)
SimulationSpawner provides ChildProcessSpawner
SimulationNetwork provides SimulationNetwork.Service + HttpClient
(httpClientLayer → SimulationNetwork)
SimulationFileSystem provides AppFileSystem (does NOT provide FileSystem.FileSystem;
tier0 also merges FileSystem.layerNoop({}) for that tag)
SimulationSpawner provides ChildProcessSpawner (Layer.succeed; no deps)
SimulationNetwork provides SimulationNetwork.Service + HttpClient.HttpClient
(httpClientLayer composed inside `layer(options)`)
SimulationGit → AppFileSystem (overrides Git tag)
SimulationProvider → Simulation (overrides Provider tag)
Simulation → AppFileSystem, SimulationNetwork
@ -48,31 +47,34 @@ Simulation → AppFileSystem, SimulationNetwork
─── Core services ───────────────────────────────────────────────
EffectFlock → Global, AppFileSystem
Auth → AppFileSystem
McpAuth → AppFileSystem
Account → AccountRepo, HttpClient
Npm → AppFileSystem, Global, FileSystem.FileSystem, EffectFlock
Config → AppFileSystem, Auth, Account, Env, Npm
Permission → Bus
Plugin → Bus, Config
Plugin → Bus, Config, RuntimeFlags (NEW: RuntimeFlags)
Discovery → AppFileSystem, Path, HttpClient
Skill → Discovery, Config, Bus, AppFileSystem, Global
Skill → Discovery, Config, Bus, AppFileSystem, Global,
RuntimeFlags (NEW: RuntimeFlags)
SystemPrompt → Skill
─── File / git ──────────────────────────────────────────────────
Ripgrep → AppFileSystem, HttpClient, ChildProcessSpawner
File → AppFileSystem, Ripgrep, Git, Scope
FileWatcher → Config, Git
Format → Config, ChildProcessSpawner
Snapshot → AppFileSystem, ChildProcessSpawner, Config
Format → Config, AppProcess, RuntimeFlags (CHANGED: was ChildProcessSpawner; now AppProcess + RuntimeFlags)
Snapshot → AppFileSystem, AppProcess, Config (CHANGED: was ChildProcessSpawner)
Storage → AppFileSystem, Git
Vcs → Git, Bus, Scope
Worktree → Scope, AppFileSystem, Path, ChildProcessSpawner,
Git, Project, InstanceStore
Project → AppFileSystem, Path, ChildProcessSpawner, Bus
Worktree → Scope, AppFileSystem, Path, AppProcess,
Git, Project, InstanceStore (CHANGED: AppProcess instead of ChildProcessSpawner)
Project → AppFileSystem, Path, ChildProcessSpawner,
Bus, RuntimeFlags (NEW: RuntimeFlags)
─── Provider / LSP / MCP ────────────────────────────────────────
ModelsDev → AppFileSystem, HttpClient
ProviderAuth → Auth, Plugin
LSP → Config
LSP → Config, RuntimeFlags (NEW: RuntimeFlags)
McpAuth → AppFileSystem
MCP → ChildProcessSpawner, McpAuth, Bus, Config
@ -80,98 +82,146 @@ MCP → ChildProcessSpawner, McpAuth, Bus, Config
Todo → Bus
Question → Bus
SessionStatus → Bus
SessionRunState → SessionStatus
Instruction → Config, AppFileSystem, Global, HttpClient
SessionRunState → BackgroundJob, SessionStatus (NEW: BackgroundJob)
Instruction → Config, AppFileSystem, Global, HttpClient,
RuntimeFlags (NEW: RuntimeFlags)
Session → Bus, Storage, SyncEvent
Session → BackgroundJob, Bus, Storage, SyncEvent,
RuntimeFlags (NEW: BackgroundJob, RuntimeFlags)
SessionSummary → Session, Snapshot, Storage, Bus
SessionRevert → Session, Snapshot, Storage, Bus,
SessionSummary, SessionRunState, SyncEvent
LLM → Auth, Config, Provider, Plugin, Permission
LLM → Auth, Config, Provider, Plugin, RuntimeFlags (CHANGED: Permission satisfied internally;
(Permission satisfied internally via .layer) RuntimeFlags is new)
Agent → Config, Auth, Plugin, Skill, Provider
Agent → Config, Auth, Plugin, Skill, Provider,
RuntimeFlags (NEW: RuntimeFlags)
Command → Config, MCP, Skill
SessionProcessor → Session, Config, Bus, Snapshot, Agent, LLM,
Permission, Plugin, SessionSummary, Scope,
SessionStatus
Permission, Plugin, SessionSummary,
SessionStatus, Image, EventV2Bridge,
RuntimeFlags, Scope (NEW: Image, EventV2Bridge, RuntimeFlags)
SessionCompaction → Bus, Config, Session, Agent, Plugin,
SessionProcessor, Provider
SessionProcessor, Provider, EventV2Bridge,
RuntimeFlags (NEW: EventV2Bridge, RuntimeFlags)
SessionPrompt → Bus, SessionStatus, Session, Agent, Provider,
SessionProcessor, SessionCompaction, Plugin,
Command, Config, Permission, AppFileSystem,
MCP, LSP, ToolRegistry, Truncate,
ChildProcessSpawner, Scope, Instruction,
SessionRunState, SessionRevert,
SessionSummary, SystemPrompt, LLM
SessionSummary, SystemPrompt, LLM,
Image, Reference, EventV2Bridge,
RuntimeFlags (NEW: Image, Reference, EventV2Bridge, RuntimeFlags)
ToolRegistry → Config, Plugin, Agent, Skill, Truncate (+ many
tool deps: Question, Todo, Session, Provider,
Git, LSP, Instruction, AppFileSystem, Bus,
HttpClient, ChildProcessSpawner, Ripgrep,
Format, Truncate)
ToolRegistry → Config, Plugin, Question, Todo, Agent, Skill,
Session, SessionStatus, BackgroundJob,
Provider, Git, Reference, LSP, Instruction,
AppFileSystem, Bus, HttpClient,
ChildProcessSpawner, Ripgrep, Format,
Truncate, RuntimeFlags (NEW: BackgroundJob, Reference, RuntimeFlags)
─── Share / Workspace ───────────────────────────────────────────
ShareNext (provided by simulationShareNextLayer in sim)
SessionShare → Config, Session, ShareNext, Scope, SyncEvent
SessionShare → Config, Session, ShareNext, Scope, SyncEvent,
RuntimeFlags (NEW: RuntimeFlags)
Workspace → Auth, Session, SessionPrompt, HttpClient,
SyncEvent, Vcs
(also uses InstanceStore/Bootstrap via
Effect.provide inside runInWorkspace)
SyncEvent, Vcs, AppFileSystem, RuntimeFlags (NEW: AppFileSystem (explicit), RuntimeFlags)
─── Misc ────────────────────────────────────────────────────────
Installation → HttpClient, ChildProcessSpawner
Installation → HttpClient, AppProcess (CHANGED: AppProcess instead of ChildProcessSpawner)
Pty → Config, Bus, Plugin
─── Instance lifecycle ──────────────────────────────────────────
InstanceBootstrap → Config, File, FileWatcher, Format, LSP, Plugin,
Project, ShareNext, Snapshot, Vcs
Project, Reference, ShareNext, Snapshot, Vcs (NEW: Reference)
InstanceStore → Project, InstanceBootstrap, Scope
Observability (no app deps; provides Logger + tracer)
```
## NEW dependencies introduced since previous graph
The rebase pulled in several new cross-cutting services that need to be
satisfied somewhere in tier0/tier1 of the simulated chain. They are NOT yet
listed in the `Tier*Services` unions or merged into any tier in `server.ts`:
```
RuntimeFlags.Service — yielded by ~17 services (Plugin, Skill, Project,
Session, SyncEvent, Format, LSP, Instruction,
Agent, LLM, SessionShare, SessionProcessor,
SessionCompaction, ToolRegistry, SessionPrompt,
Workspace, SessionRunState).
Source: `@/effect/runtime-flags`.
AppProcess.Service — yielded by Installation, Format, Snapshot,
Worktree (and prod Git, but sim uses SimulationGit).
Source: presumed `@opencode-ai/core/app-process`
or similar — needs `AppProcess.defaultLayer`.
BackgroundJob.Service — yielded by Session, SessionRunState, ToolRegistry.
Needs `BackgroundJob.defaultLayer`.
Image.Service — yielded by SessionProcessor, SessionPrompt.
EventV2Bridge.Service — yielded by SessionProcessor, SessionCompaction,
SessionPrompt. Source: `@/event-v2-bridge` (already
imported in server.ts but never added to a tier).
Reference.Service — yielded by ToolRegistry, SessionPrompt,
InstanceBootstrap.
```
## Dependency Tiers (topological order)
Roughly, build order from leaves to roots:
```
Tier 0 (no deps):
Global, Env, NodePath, AccountRepo, PtyTicket, Bus (lazy),
SyncEvent (lazy), AppFileSystem (sim), ChildProcessSpawner (sim),
HttpClient (sim), SimulationNetwork, errorLayer, compressionLayer,
corsVaryFix, fenceLayer, runtime, simulationShareNextLayer
Global, Env, NodePath, AccountRepo, PtyTicket, Bus, SyncEvent,
AppFileSystem (sim), ChildProcessSpawner (sim), HttpClient (sim),
SimulationNetwork, FileSystem.layerNoop, simulationShareNextLayer
+ (NEW REQUIRED) RuntimeFlags, AppProcess, BackgroundJob, Image,
EventV2Bridge, Reference
(SyncEvent now depends on RuntimeFlags + Bus, so it's actually tier 1.)
Tier 1:
Auth, Truncate, EffectFlock, Permission, Todo, Question,
SessionStatus, McpAuth, Discovery, SimulationGit, Simulation,
Ripgrep, Storage, Vcs
SessionStatus, McpAuth, Discovery, SimulationGit, Ripgrep, Account,
SyncEvent (needs RuntimeFlags + Bus)
Tier 2:
Account, Npm, LSP, Skill, SessionRunState, ModelsDev, File,
Project, Worktree (needs InstanceStore — cycle hint), MCP,
Installation, Pty, SystemPrompt
Npm, ModelsDev, Project, Installation, Storage, Vcs, SessionRunState
Tier 3:
Config, FileWatcher, Format, Snapshot, ProviderAuth, Session,
SessionShare, SessionSummary
Config, File, Simulation, Session
Tier 4:
Plugin, Agent, Command, LLM, SessionRevert,
Plugin, FileWatcher, Format, Snapshot, LSP, MCP, Skill, Instruction,
SimulationProvider (= Provider)
Tier 5:
SessionProcessor, SessionCompaction, Workspace
Pty, ProviderAuth, SessionSummary, Agent, Command, LLM, SystemPrompt
Tier 6:
SessionPrompt, ToolRegistry
SessionRevert, SessionProcessor, SessionShare
Tier 7:
InstanceBootstrap (needs many of the above)
SessionCompaction, ToolRegistry
Tier 8:
InstanceStore (needs InstanceBootstrap + Project)
SessionPrompt
Tier 9:
Workspace, InstanceBootstrap
Tier 10:
InstanceStore
Tier 11:
Worktree
```
## Potential cycles / hazards
@ -180,8 +230,7 @@ Tier 8:
Worktree → InstanceStore → InstanceBootstrap → Project → (back to Worktree?)
- InstanceBootstrap requires Project (yes)
- Project does NOT require Worktree directly
- Worktree requires InstanceStore — only used inside its methods,
but yielded at layer init, so it's a true requirement
- Worktree requires InstanceStore at layer init
→ Worktree must be built AFTER InstanceStore.
SimulationProvider provides Provider tag, depends on Simulation.
@ -189,26 +238,63 @@ SimulationProvider provides Provider tag, depends on Simulation.
SimulationProvider in this layer chain.
SimulationGit provides Git tag, used by File, FileWatcher,
Storage, Vcs, Worktree, ToolRegistry tools, Project (no — Project
uses ChildProcessSpawner instead).
Storage, Vcs, Worktree, ToolRegistry tools.
LLM.layer pipes Layer.provide(Permission.defaultLayer) internally.
That means LLM brings its own Permission — beware double-providing
Permission (sim chain also provides Permission.layer separately).
So LLM's residual requirements no longer include Permission, BUT the
sim chain still provides Permission.layer separately (correct — used by
SessionProcessor, SessionPrompt directly).
```
## Why the simulated chain fails with stepwise `provideMerge`
## Why the simulated chain fails today
`Layer.provideMerge(self, that)` builds `that` independently and uses it to satisfy `self`'s reqs. `that` cannot see services already provided inside `self`. So when you write:
```ts
withCoreAppServices // contains Config.layer, Plugin.layer, ...
.pipe(Layer.provideMerge(Npm.layer)) // Npm needs EffectFlock
The current `server.ts` `Tier0Services` union lists:
```
AppFileSystem, FileSystem.FileSystem, ChildProcessSpawner, HttpClient,
SimulationNetwork, Path, Global, Env, Bus, AccountRepo, ShareNext,
SyncEvent, PtyTicket
```
`Npm.layer` is built in isolation and its `EffectFlock` requirement must be filled from OUTSIDE the entire pipe — not from inside `withCoreAppServices`. Same for every other "consumer added later" combination.
But the actual `Layer.mergeAll(...)` body at tier0 has residual requirements
beyond that union. The TS error says
`Layer<..., never, Service | Service>` — those two unresolved `Service`s
are members of the NEW dependencies table above.
Production avoids this because every service is added via `Layer.provide([... defaultLayer, defaultLayer, ...])` — flat mergeAll of self-contained layers, all visible to each other.
The most likely culprits, in order of leakage:
1. **`SyncEvent.layer`** now yields `RuntimeFlags` and `Bus`. `Bus` is in tier0
already, but `RuntimeFlags` is not provided anywhere in `createSimulatedRoutes`.
So `SyncEvent` leaks `RuntimeFlags` into tier0's `RIn`.
2. Downstream tiers also leak `RuntimeFlags`, `AppProcess`, `BackgroundJob`,
`Image`, `EventV2Bridge`, `Reference` — these cascade through every higher
tier as `Service | Service | ...` in the error type.
## Fix strategy
The minimal change to make tier0 type-check:
1. Add `RuntimeFlags.defaultLayer` to tier0 and `RuntimeFlags.Service` to
`Tier0Services`. This satisfies `SyncEvent`'s new dep and unblocks every
service that yields `RuntimeFlags`.
2. Add `AppProcess.defaultLayer` (or a simulated equivalent) to tier0 and
`AppProcess.Service` to `Tier0Services`. Needed by `Installation`, `Format`,
`Snapshot`, `Worktree`.
3. Add `BackgroundJob.defaultLayer` to tier0 and `BackgroundJob.Service` to
`Tier0Services`. Needed by `Session`, `SessionRunState`, `ToolRegistry`.
4. Add `Image.defaultLayer` to tier0 (or wherever it fits) and `Image.Service`
to `Tier0Services`. Needed by `SessionProcessor`, `SessionPrompt`.
5. Add `EventV2Bridge.defaultLayer` to tier0 and `EventV2Bridge.Service` to
`Tier0Services`. (Note: `EventV2Bridge` is already imported in `server.ts`
for the production routes but is not in any simulated tier.)
6. Add `Reference.defaultLayer` to a tier that satisfies its deps (it's a leaf
wrt the listed graph above) and `Reference.Service` to the corresponding
tier union. Needed by `ToolRegistry`, `SessionPrompt`, `InstanceBootstrap`.
Production routes (`createProductionRoutes`) already include
`RuntimeFlags.defaultLayer` and `EventV2Bridge.defaultLayer` in the flat
`Layer.provide([...])` list — they were simply never carried over to the
simulated chain after the rebase.
## See also

View file

@ -165,6 +165,10 @@ export const SimulateCommand = cmd({
[OPENCODE_RUN_ID]: ensureRunID(),
PWD: simulatedDirectory,
[simulatedCwdEnv]: simulatedDirectory,
// Simulated filesystem lives in InMemoryFs — SQLite needs to be in-memory
// too, since real fs writes to `/opencode/.local/share/opencode/...` will
// fail (the directory only exists in the sim FS).
OPENCODE_DB: ":memory:",
}),
})
worker.onerror = (e) => {

View file

@ -1,6 +1,11 @@
import { Config as EffectConfig, Context, Effect, Layer, FileSystem, Path } from "effect"
import { ChildProcessSpawner } from "effect/unstable/process"
import { NodePath } from "@effect/platform-node"
import { AppProcess } from "@opencode-ai/core/process"
import { EventV2 } from "@opencode-ai/core/event"
import { BackgroundJob } from "@/background/job"
import { Image } from "@/image/image"
import { Reference } from "@/reference/reference"
import { Git } from "@/git"
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi"
import {
@ -318,8 +323,10 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
| Bus.Service
| AccountRepo.Service
| ShareNext.Service
| SyncEvent.Service
| PtyTicket.Service
| RuntimeFlags.Service
| BackgroundJob.Service
| EventV2.Service
const tier0: Layer.Layer<Tier0Services, never, never> = Layer.mergeAll(
SimulationFileSystem.layer({
@ -341,8 +348,10 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
Bus.layer,
AccountRepo.layer,
simulationShareNextLayer,
SyncEvent.layer,
PtyTicket.layer,
RuntimeFlags.defaultLayer,
BackgroundJob.layer,
EventV2.layer,
)
// ─── Tier 1: services depending only on tier 0 ──────────────────────────
@ -358,6 +367,8 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
// Discovery → AppFileSystem, Path, HttpClient
// Ripgrep → AppFileSystem, HttpClient, ChildProcessSpawner
// Account → AccountRepo, HttpClient
// SyncEvent → RuntimeFlags, Bus
// AppProcess → ChildProcessSpawner
const tier1: Layer.Layer<
| Tier0Services
| Git.Service
@ -371,7 +382,9 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
| SessionStatus.Service
| Discovery.Service
| Ripgrep.Service
| Account.Service,
| Account.Service
| SyncEvent.Service
| AppProcess.Service,
never,
never
> = Layer.mergeAll(
@ -387,6 +400,8 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
Discovery.layer,
Ripgrep.layer,
Account.layer,
SyncEvent.layer,
AppProcess.layer,
).pipe(Layer.provideMerge(tier0))
type Tier1Services =
@ -403,15 +418,18 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
| Discovery.Service
| Ripgrep.Service
| Account.Service
| SyncEvent.Service
| AppProcess.Service
// ─── Tier 2: services depending only on tier 0 + tier 1 ─────────────────
// Npm → AppFileSystem, Global, FileSystem, EffectFlock
// ModelsDev → AppFileSystem, HttpClient
// Project → AppFileSystem, Path, ChildProcessSpawner, Bus
// Installation → HttpClient, ChildProcessSpawner
// Project → AppFileSystem, Path, ChildProcessSpawner, Bus, RuntimeFlags
// Installation → HttpClient, AppProcess
// Storage → AppFileSystem, Git
// Vcs → Git, Bus
// SessionRunState → SessionStatus
// SessionRunState → BackgroundJob, SessionStatus
// EventV2Bridge → EventV2, Bus, SyncEvent
const tier2: Layer.Layer<
| Tier1Services
| Npm.Service
@ -420,7 +438,8 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
| Installation.Service
| Storage.Service
| Vcs.Service
| SessionRunState.Service,
| SessionRunState.Service
| EventV2Bridge.Service,
never,
never
> = Layer.mergeAll(
@ -431,6 +450,7 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
Storage.layer,
Vcs.layer,
SessionRunState.layer,
EventV2Bridge.layer,
).pipe(Layer.provideMerge(tier1))
type Tier2Services =
@ -442,6 +462,7 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
| Storage.Service
| Vcs.Service
| SessionRunState.Service
| EventV2Bridge.Service
// ─── Tier 3: services depending only on tier 0-2 ────────────────────────
// Config → AppFileSystem, Auth, Account, Env, Npm
@ -457,15 +478,17 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
type Tier3Services = Tier2Services | Config.Service | File.Service | Simulation.Service | Session.Service
// ─── Tier 4: services depending on tier 0-3 (mostly Config) ─────────────
// Plugin → Bus, Config
// Plugin → Bus, Config, RuntimeFlags
// FileWatcher → Config, Git
// Format → Config, ChildProcessSpawner
// Snapshot → AppFileSystem, ChildProcessSpawner, Config
// LSP → Config
// Format → Config, AppProcess, RuntimeFlags
// Snapshot → AppFileSystem, AppProcess, Config
// LSP → Config, RuntimeFlags
// MCP → ChildProcessSpawner, McpAuth, Bus, Config
// Skill → Discovery, Config, Bus, AppFileSystem, Global
// Instruction → Config, AppFileSystem, Global, HttpClient
// Skill → Discovery, Config, Bus, AppFileSystem, Global, RuntimeFlags
// Instruction → Config, AppFileSystem, Global, HttpClient, RuntimeFlags
// SimulationProvider → Simulation (provides Provider tag)
// Image → Config
// Reference → Config, AppFileSystem, Git, RuntimeFlags
const tier4: Layer.Layer<
| Tier3Services
| Plugin.Service
@ -476,7 +499,9 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
| MCP.Service
| Skill.Service
| Instruction.Service
| Provider.Service,
| Provider.Service
| Image.Service
| Reference.Service,
never,
never
> = Layer.mergeAll(
@ -489,6 +514,8 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
Skill.layer,
Instruction.layer,
SimulationProvider.layer,
Image.layer,
Reference.layer,
).pipe(Layer.provideMerge(tier3))
type Tier4Services =
@ -502,6 +529,8 @@ export function createSimulatedRoutes(corsOptions?: CorsOptions): ReturnType<typ
| Skill.Service
| Instruction.Service
| Provider.Service
| Image.Service
| Reference.Service
// ─── Tier 5: services depending on tier 0-4 ─────────────────────────────
// Pty → Config, Bus, Plugin