1
0
Эх сурвалжийг харах

refactor(core): separate out location node functionality and integrate into v2 (#34119)

James Long 1 сар өмнө
parent
commit
ecdfff5a42
100 өөрчлөгдсөн 1342 нэмэгдсэн , 865 устгасан
  1. 2 0
      packages/core/package.json
  2. 3 0
      packages/core/src/agent.ts
  3. 3 0
      packages/core/src/aisdk.ts
  4. 3 0
      packages/core/src/background-job.ts
  5. 3 0
      packages/core/src/catalog.ts
  6. 3 0
      packages/core/src/command.ts
  7. 7 0
      packages/core/src/config.ts
  8. 3 0
      packages/core/src/credential.ts
  9. 2 2
      packages/core/src/cross-spawn-spawner.ts
  10. 2 2
      packages/core/src/database/database.ts
  11. 6 6
      packages/core/src/effect/layer-node-platform.ts
  12. 0 248
      packages/core/src/effect/layer-node.ts
  13. 2 0
      packages/core/src/effect/layer-node/index.ts
  14. 118 0
      packages/core/src/effect/layer-node/layer-node-tree.ts
  15. 138 0
      packages/core/src/effect/layer-node/layer-node.ts
  16. 18 0
      packages/core/src/effect/node-build.ts
  17. 14 0
      packages/core/src/effect/node.ts
  18. 0 11
      packages/core/src/effect/scoped-node.ts
  19. 2 2
      packages/core/src/event.ts
  20. 3 0
      packages/core/src/file-mutation.ts
  21. 7 0
      packages/core/src/filesystem.ts
  22. 6 3
      packages/core/src/filesystem/search.ts
  23. 7 0
      packages/core/src/filesystem/watcher.ts
  24. 2 2
      packages/core/src/fs-util.ts
  25. 2 2
      packages/core/src/git.ts
  26. 2 2
      packages/core/src/global.ts
  27. 3 0
      packages/core/src/image.ts
  28. 7 0
      packages/core/src/instruction-context.ts
  29. 3 0
      packages/core/src/integration.ts
  30. 0 151
      packages/core/src/location-layer.ts
  31. 7 0
      packages/core/src/location-mutation.ts
  32. 18 0
      packages/core/src/location-service-map.ts
  33. 112 0
      packages/core/src/location-services.ts
  34. 11 0
      packages/core/src/location.ts
  35. 2 2
      packages/core/src/models-dev.ts
  36. 2 2
      packages/core/src/npm.ts
  37. 7 0
      packages/core/src/permission.ts
  38. 3 0
      packages/core/src/permission/saved.ts
  39. 16 0
      packages/core/src/plugin.ts
  40. 29 2
      packages/core/src/plugin/internal.ts
  41. 3 0
      packages/core/src/policy.ts
  42. 2 2
      packages/core/src/process.ts
  43. 2 2
      packages/core/src/project.ts
  44. 8 2
      packages/core/src/project/copy.ts
  45. 2 2
      packages/core/src/project/directories.ts
  46. 3 0
      packages/core/src/pty.ts
  47. 2 2
      packages/core/src/pty/ticket.ts
  48. 3 0
      packages/core/src/question.ts
  49. 7 0
      packages/core/src/reference.ts
  50. 3 0
      packages/core/src/reference/guidance.ts
  51. 7 0
      packages/core/src/repository-cache.ts
  52. 2 2
      packages/core/src/ripgrep.ts
  53. 2 2
      packages/core/src/ripgrep/binary.ts
  54. 22 15
      packages/core/src/session.ts
  55. 4 0
      packages/core/src/session/execution.ts
  56. 11 2
      packages/core/src/session/execution/local.ts
  57. 2 2
      packages/core/src/session/projector.ts
  58. 22 0
      packages/core/src/session/runner/llm.ts
  59. 3 0
      packages/core/src/session/runner/model.ts
  60. 3 0
      packages/core/src/session/store.ts
  61. 3 0
      packages/core/src/session/todo.ts
  62. 3 0
      packages/core/src/skill.ts
  63. 4 0
      packages/core/src/skill/discovery.ts
  64. 3 0
      packages/core/src/skill/guidance.ts
  65. 7 0
      packages/core/src/snapshot.ts
  66. 9 0
      packages/core/src/system-context/builtins.ts
  67. 3 0
      packages/core/src/system-context/registry.ts
  68. 5 0
      packages/core/src/tool-output-store.ts
  69. 3 0
      packages/core/src/tool/application-tools.ts
  70. 37 0
      packages/core/src/tool/builtins.ts
  71. 3 0
      packages/core/src/tool/read-filesystem.ts
  72. 13 0
      packages/core/src/tool/registry.ts
  73. 2 2
      packages/core/src/util/effect-flock.ts
  74. 138 0
      packages/core/test/effect/layer-node/layer-node-types.test.ts
  75. 226 0
      packages/core/test/effect/layer-node/layer-node.test.ts
  76. 119 0
      packages/core/test/effect/layer-node/node-build.test.ts
  77. 6 6
      packages/core/test/location-layer.test.ts
  78. 2 2
      packages/core/test/move-session.test.ts
  79. 2 2
      packages/core/test/permission.test.ts
  80. 2 2
      packages/core/test/session-create.test.ts
  81. 2 2
      packages/core/test/session-history.test.ts
  82. 2 2
      packages/core/test/session-projector.test.ts
  83. 2 2
      packages/core/test/session-prompt.test.ts
  84. 2 2
      packages/core/test/session-runner-recorded.test.ts
  85. 2 2
      packages/core/test/session-runner.test.ts
  86. 4 4
      packages/opencode/src/agent/agent.ts
  87. 3 3
      packages/opencode/src/cli/cmd/debug/file.ts
  88. 3 3
      packages/opencode/src/cli/cmd/debug/v2.ts
  89. 3 3
      packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts
  90. 5 5
      packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts
  91. 22 1
      packages/opencode/src/server/routes/instance/httpapi/server.ts
  92. 1 2
      packages/opencode/src/session/message-v2.ts
  93. 2 2
      packages/opencode/src/session/session.ts
  94. 4 4
      packages/opencode/src/session/system.ts
  95. 2 2
      packages/opencode/test/agent/agent.test.ts
  96. 2 2
      packages/opencode/test/agent/plugin-agent-regression.test.ts
  97. 0 19
      packages/opencode/test/effect/layer-node-tiers-types.test.ts
  98. 0 169
      packages/opencode/test/effect/layer-node-tiers.test.ts
  99. 0 66
      packages/opencode/test/effect/layer-node-types.test.ts
  100. 0 86
      packages/opencode/test/effect/layer-node.test.ts

+ 2 - 0
packages/core/package.json

@@ -16,6 +16,8 @@
     "opencode": "./bin/opencode"
   },
   "exports": {
+    "./effect/layer-node": "./src/effect/layer-node/index.ts",
+    "./effect/node": "./src/effect/node.ts",
     "./session/runner": "./src/session/runner/index.ts",
     "./system-context": "./src/system-context/index.ts",
     "./*": "./src/*.ts"

+ 3 - 0
packages/core/src/agent.ts

@@ -1,5 +1,6 @@
 export * as AgentV2 from "./agent"
 
+import { makeLocationNode } from "./effect/node"
 import { Array, Context, Effect, Layer, Types } from "effect"
 import { Agent } from "@opencode-ai/schema/agent"
 import { State } from "./state"
@@ -106,3 +107,5 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer
+
+export const node = makeLocationNode({ service: Service, layer, deps: [] })

+ 3 - 0
packages/core/src/aisdk.ts

@@ -1,5 +1,6 @@
 export * as AISDK from "./aisdk"
 
+import { makeLocationNode } from "./effect/node"
 import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
 import { ModelV2 } from "./model"
@@ -231,4 +232,6 @@ export const locationLayer = Layer.effect(
   }),
 )
 
+export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [] })
+
 export const defaultLayer = locationLayer

+ 3 - 0
packages/core/src/background-job.ts

@@ -2,6 +2,7 @@ export * as BackgroundJob from "./background-job"
 
 import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Scope, SynchronizedRef } from "effect"
 import { Identifier } from "./id/id"
+import { makeGlobalNode } from "./effect/node"
 
 export type Status = "running" | "completed" | "error" | "cancelled"
 
@@ -362,3 +363,5 @@ export const make = Effect.gen(function* () {
 export const layer = Layer.effect(Service, make)
 
 export const defaultLayer = layer
+
+export const node = makeGlobalNode({ service: Service, layer, deps: [] })

+ 3 - 0
packages/core/src/catalog.ts

@@ -1,5 +1,6 @@
 export * as Catalog from "./catalog"
 
+import { makeLocationNode } from "./effect/node"
 import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
 import { Catalog } from "@opencode-ai/schema/catalog"
 import { ModelV2 } from "./model"
@@ -296,3 +297,5 @@ export const locationLayer = layer.pipe(
   Layer.provideMerge(Integration.locationLayer),
   Layer.provideMerge(Policy.locationLayer),
 )
+
+export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Policy.node, Integration.node] })

+ 3 - 0
packages/core/src/command.ts

@@ -1,5 +1,6 @@
 export * as CommandV2 from "./command"
 
+import { makeLocationNode } from "./effect/node"
 import { Context, Effect, Layer, Types } from "effect"
 import { Command } from "@opencode-ai/schema/command"
 import { State } from "./state"
@@ -59,3 +60,5 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer
+
+export const node = makeLocationNode({ service: Service, layer, deps: [] })

+ 7 - 0
packages/core/src/config.ts

@@ -1,5 +1,6 @@
 export * as Config from "./config"
 
+import { makeLocationNode } from "./effect/node"
 import path from "path"
 import { type ParseError, parse } from "jsonc-parser"
 import { Context, Effect, Layer, Option, Schema } from "effect"
@@ -218,3 +219,9 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer.pipe(Layer.provideMerge(Policy.locationLayer))
+
+export const node = makeLocationNode({
+  service: Service,
+  layer,
+  deps: [FSUtil.node, Global.node, Location.node, Policy.node],
+})

+ 3 - 0
packages/core/src/credential.ts

@@ -5,6 +5,7 @@ import { Context, Effect, Layer, Schema } from "effect"
 import { Credential } from "@opencode-ai/schema/credential"
 import { Integration } from "@opencode-ai/schema/integration"
 import { Database } from "./database/database"
+import { makeGlobalNode } from "./effect/node"
 import { CredentialTable } from "./credential/sql"
 
 export const ID = Credential.ID
@@ -135,3 +136,5 @@ export const layer = Layer.effect(
 )
 
 export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
+
+export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })

+ 2 - 2
packages/core/src/cross-spawn-spawner.ts

@@ -24,7 +24,7 @@ import {
 import * as NodeChildProcess from "node:child_process"
 import { PassThrough } from "node:stream"
 import launch from "cross-spawn"
-import { LayerNode } from "./effect/layer-node"
+import { makeGlobalNode } from "./effect/node"
 import { filesystem, path } from "./effect/layer-node-platform"
 
 const toError = (err: unknown): Error => (err instanceof globalThis.Error ? err : new globalThis.Error(String(err)))
@@ -503,6 +503,6 @@ export const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSyste
 )
 
 export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
-export const node = LayerNode.make({ service: ChildProcessSpawner, layer, deps: [filesystem, path] })
+export const node = makeGlobalNode({ service: ChildProcessSpawner, layer, deps: [filesystem, path] })
 
 export * as CrossSpawnSpawner from "./cross-spawn-spawner"

+ 2 - 2
packages/core/src/database/database.ts

@@ -8,7 +8,7 @@ import { Flag } from "../flag/flag"
 import { isAbsolute, join } from "path"
 import { DatabaseMigration } from "./migration"
 import { InstallationChannel } from "../installation/version"
-import { LayerNode } from "../effect/layer-node"
+import { makeGlobalNode } from "../effect/node"
 
 const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
 type DatabaseShape = Effect.Success<typeof makeDatabase>
@@ -60,4 +60,4 @@ export const defaultLayer = Layer.unwrap(
   }),
 ).pipe(Layer.provide(Global.defaultLayer))
 
-export const node = LayerNode.make({ service: Service, layer: layerFromPath(path()), deps: [] })
+export const node = makeGlobalNode({ service: Service, layer: layerFromPath(path()), deps: [] })

+ 6 - 6
packages/core/src/effect/layer-node-platform.ts

@@ -3,16 +3,16 @@ import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
 import { FileSystem, Path } from "effect"
 import { FetchHttpClient } from "effect/unstable/http"
 import { HttpClient } from "effect/unstable/http"
-import { LayerNode } from "./layer-node"
+import { makeGlobalNode } from "./node"
 
-export const filesystem = LayerNode.make({ service: FileSystem.FileSystem, layer: NodeFileSystem.layer, deps: [] })
-export const path = LayerNode.make({ service: Path.Path, layer: NodePath.layer, deps: [] })
-export const httpClient = LayerNode.make({ service: HttpClient.HttpClient, layer: FetchHttpClient.layer, deps: [] })
-export const requestExecutor = LayerNode.make({
+export const filesystem = makeGlobalNode({ service: FileSystem.FileSystem, layer: NodeFileSystem.layer, deps: [] })
+export const path = makeGlobalNode({ service: Path.Path, layer: NodePath.layer, deps: [] })
+export const httpClient = makeGlobalNode({ service: HttpClient.HttpClient, layer: FetchHttpClient.layer, deps: [] })
+export const requestExecutor = makeGlobalNode({
   service: RequestExecutor.Service,
   layer: RequestExecutor.layer,
   deps: [httpClient],
 })
-export const llmClient = LayerNode.make({ service: LLMClient.Service, layer: LLMClient.layer, deps: [requestExecutor] })
+export const llmClient = makeGlobalNode({ service: LLMClient.Service, layer: LLMClient.layer, deps: [requestExecutor] })
 
 export * as LayerNodePlatform from "./layer-node-platform"

+ 0 - 248
packages/core/src/effect/layer-node.ts

@@ -1,248 +0,0 @@
-import { Brand, Context, Layer } from "effect"
-
-type RuntimeLayer = Layer.Layer<never, unknown, unknown>
-type AnyNode = Node<unknown, unknown, any>
-type NodeList<Item extends AnyNode = AnyNode> = readonly [] | readonly [Item, ...Item[]]
-type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown, any> ? A : never
-type Error<Item> = [Item] extends [never] ? never : Item extends Node<unknown, infer E, any> ? E : never
-type Missing<Required, Dependencies extends NodeList> = Exclude<Required, Output<Dependencies[number]>>
-type CheckDependencies<Implementation extends Layer.Any, Dependencies extends NodeList> = [
-  Missing<Layer.Services<Implementation>, Dependencies>,
-] extends [never]
-  ? unknown
-  : { readonly "Missing dependencies": Missing<Layer.Services<Implementation>, Dependencies> }
-declare const $OutputType: unique symbol
-declare const $ErrorType: unique symbol
-
-export type Tier<Name extends string = string> = Name & Brand.Brand<"LayerNode.Tier">
-
-const makeTier = Brand.nominal<Tier>()
-
-export type Node<A, E = never, T extends Tier | undefined = undefined> = {
-  readonly kind: "layer" | "group"
-  readonly name: string
-  readonly service?: Context.Service.Any
-  readonly implementation?: Layer.Any
-  readonly dependencies: readonly AnyNode[]
-  readonly tier?: T
-  readonly [$OutputType]?: () => A
-  readonly [$ErrorType]?: () => E
-}
-
-type NodeIdentity =
-  | { readonly service: Context.Service.Any; readonly name?: never }
-  | { readonly name: string; readonly service?: never }
-type DistributiveOmit<A, K extends PropertyKey> = A extends unknown ? Omit<A, K> : never
-
-type NodeInput<
-  Implementation extends Layer.Any,
-  Items extends NodeList,
-  T extends Tier | undefined = undefined,
-> = NodeIdentity & {
-  readonly layer: Implementation
-  readonly deps: Items & CheckDependencies<Implementation, NoInfer<Items>>
-  readonly tier?: T
-}
-
-export function make<
-  const Implementation extends Layer.Any,
-  const Items extends NodeList,
-  const T extends Tier | undefined = undefined,
->(
-  input: NodeInput<Implementation, Items, T>,
-): Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, T> {
-  return {
-    kind: "layer",
-    name: input.service !== undefined ? input.service.key : input.name,
-    service: input.service,
-    implementation: input.layer,
-    dependencies: input.deps,
-    tier: input.tier,
-  }
-}
-
-export function group<const Items extends NodeList>(
-  dependencies: Items,
-): Node<Output<Items[number]>, Error<Items[number]>> {
-  return { kind: "group", name: "group", dependencies }
-}
-
-type AllowedTierNames<Names extends readonly string[], Name extends Names[number]> = Names extends readonly [
-  infer Head extends string,
-  ...infer Tail extends readonly string[],
-]
-  ? Head extends Name
-    ? Head | Tail[number]
-    : AllowedTierNames<Tail, Name>
-  : never
-
-type NodeInTiers<Names extends string> = Node<unknown, unknown, Tier<Names>>
-
-export interface Tiers<Names extends readonly [string, ...string[]]> {
-  readonly names: Names
-  readonly values: { readonly [K in Names[number]]: Tier<K> }
-  readonly make: <Name extends Names[number]>(
-    name: Name,
-  ) => <
-    const Implementation extends Layer.Any,
-    const Items extends NodeList<NodeInTiers<AllowedTierNames<Names, Name>>>,
-  >(
-    input: DistributiveOmit<NodeInput<Implementation, Items, Tier<Name>>, "tier">,
-  ) => Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, Tier<Name>>
-}
-
-export function tiers<const Names extends readonly [string, ...string[]]>(names: Names): Tiers<Names> {
-  const values = Object.fromEntries(names.map((name) => [name, makeTier(name)])) as Tiers<Names>["values"]
-  return {
-    names,
-    values,
-    make: ((name: Names[number]) => (input: DistributiveOmit<NodeInput<Layer.Any, NodeList, Tier>, "tier">) =>
-      make({ ...input, tier: values[name] })) as Tiers<Names>["make"],
-  }
-}
-
-const defaultTiers = tiers(["untiered"])
-const untiered = defaultTiers.values.untiered
-
-export type Replacement = {
-  readonly source: Layer.Any
-  readonly replacement: Layer.Any
-}
-
-type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never]
-  ? unknown
-  : { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }
-
-export function replace<A, E, R, E2>(
-  source: Layer.Layer<A, E, R>,
-  replacement: Layer.Layer<NoInfer<A>, E2, never> & CheckReplacementErrors<E, NoInfer<E2>>,
-): Replacement {
-  return { source, replacement }
-}
-
-export function buildLayer<
-  A,
-  E,
-  const Names extends readonly [string, ...string[]] = readonly ["untiered"],
-  const Built extends Layer.Any = Layer.Layer<never, never, never>,
->(
-  node: Node<A, E, any>,
-  options?: {
-    readonly tiers?: Tiers<Names>
-    readonly buildTier?: (tier: Names[number], layers: readonly Layer.Any[]) => Built
-    readonly replacements?: readonly Replacement[]
-  },
-): Layer.Layer<A | Layer.Success<Built>, E | Layer.Error<Built>, never> {
-  const tiers = options?.tiers ?? (defaultTiers as unknown as Tiers<Names>)
-  const replacementMap = new Map(options?.replacements?.map((item) => [item.source, item.replacement]))
-  const plans = plan(node, tiers, replacementMap)
-  const layers: RuntimeLayer[] = tiers.names.map((name) => {
-    const tier = tiers.values[name as Names[number]]
-    const layers = plans.get(tier) ?? []
-    return (options?.buildTier?.(name, layers) ?? combine(layers)) as RuntimeLayer
-  })
-  if (layers.length === 0) return Layer.empty as never
-  return layers.slice(1).reduce((result, layer) => result.pipe(Layer.provideMerge(layer)), layers[0]) as never
-}
-
-export function combine(layers: readonly Layer.Any[]): RuntimeLayer {
-  return layers.reduce<RuntimeLayer>(
-    (result, layer) => (layer as RuntimeLayer).pipe(Layer.provideMerge(result)),
-    Layer.empty as RuntimeLayer,
-  )
-}
-
-function plan(
-  root: AnyNode,
-  tiers: Tiers<readonly [string, ...string[]]>,
-  replacements: ReadonlyMap<Layer.Any, Layer.Any>,
-) {
-  const indexes = new Map(tiers.names.map((name, index) => [tiers.values[name], index]))
-  const plans = new Map<Tier, Layer.Any[]>()
-  const activeImplementations = new Map<Tier, Map<string, AnyNode>>()
-  const serviceTiers = new Map<string, Tier>()
-  const visiting = new Set<AnyNode>()
-  const stack: AnyNode[] = []
-  const boundaryVisited = new Map<AnyNode, Set<Tier>>()
-  const boundaryServices = new Map<Tier, Map<string, AnyNode>>()
-
-  const validateBoundary = (node: AnyNode, origin: Tier) => {
-    const checked = boundaryVisited.get(node) ?? new Set<Tier>()
-    boundaryVisited.set(node, checked)
-    if (checked.has(origin)) return false
-    checked.add(origin)
-    const services = boundaryServices.get(origin) ?? new Map<string, AnyNode>()
-    boundaryServices.set(origin, services)
-    const key = node.name
-    const existing = services.get(key)
-    if (existing && existing !== node) {
-      throw new Error(`Tier ${origin} has conflicting implementations for ${key}`)
-    }
-    services.set(key, node)
-    return true
-  }
-
-  const visit = (node: AnyNode, currentTier?: Tier, origins: readonly Tier[] = []) => {
-    if (node.kind === "group") {
-      node.dependencies.forEach((dependency) => visit(dependency, currentTier, origins))
-      return
-    }
-
-    const tier = node.tier ?? untiered
-    if (!indexes.has(tier)) throw new Error(`Node ${node.name} is not in the tier configuration`)
-    const key = node.name
-    const serviceTier = serviceTiers.get(key)
-    if (serviceTier && serviceTier !== tier) {
-      throw new Error(`Service ${key} belongs to both tier ${serviceTier} and tier ${tier}`)
-    }
-    serviceTiers.set(key, tier)
-    const nextOrigins = [...origins]
-    if (currentTier) {
-      const current = indexes.get(currentTier)!
-      const required = indexes.get(tier)!
-      if (required < current) {
-        throw new Error(`Tier ${currentTier} cannot depend on lower tier ${tier}`)
-      }
-      if (required > current) nextOrigins.push(currentTier)
-    }
-    const unseenOrigins = nextOrigins.filter((origin) => validateBoundary(node, origin))
-
-    // A node may need to be emitted more than once because the final output is a
-    // flat list of layers applied with Layer.provideMerge. If another node for
-    // the same service was emitted afterward, this node is no longer the active
-    // implementation for subsequent consumers. Re-emitting restores the intended
-    // implementation ordering while Effect memoization avoids reacquiring the layer.
-    const implementations = activeImplementations.get(tier) ?? new Map<string, AnyNode>()
-    activeImplementations.set(tier, implementations)
-    if (implementations.get(key) === node && unseenOrigins.length === 0) return
-
-    if (visiting.has(node)) {
-      const start = stack.indexOf(node)
-      throw new Error(
-        `Cycle detected in layer graph: ${[...stack.slice(start), node].map((item) => item.name).join(" -> ")}`,
-      )
-    }
-
-    visiting.add(node)
-    stack.push(node)
-    try {
-      node.dependencies.forEach((dependency) => visit(dependency, tier, unseenOrigins))
-      const layers = plans.get(tier) ?? []
-      plans.set(tier, layers)
-      layers.push(replacements.get(node.implementation!) ?? node.implementation!)
-      implementations.set(key, node)
-    } finally {
-      stack.pop()
-      visiting.delete(node)
-    }
-  }
-
-  visit(root)
-  return plans
-}
-
-function requireTier(node: AnyNode, indexes: ReadonlyMap<Tier, number>) {
-  if (!node.tier || !indexes.has(node.tier)) throw new Error(`Node ${node.name} is not in the tier configuration`)
-}
-
-export * as LayerNode from "./layer-node"

+ 2 - 0
packages/core/src/effect/layer-node/index.ts

@@ -0,0 +1,2 @@
+export * from "./layer-node"
+export * as LayerNodeTree from "./layer-node-tree"

+ 118 - 0
packages/core/src/effect/layer-node/layer-node-tree.ts

@@ -0,0 +1,118 @@
+import { Layer } from "effect"
+import { LayerNode } from "./layer-node"
+
+type AnyNode = LayerNode.Node<unknown, unknown, any>
+type RuntimeLayer = Layer.Layer<never, unknown, unknown>
+
+export function hoist<A, E, T extends LayerNode.Tag>(
+  root: LayerNode.Node<A, E, any>,
+  tag: T,
+): {
+  readonly node: LayerNode.Node<A, E>
+  readonly hoisted: LayerNode.Node<unknown, E>
+} {
+  const visited = new Map<AnyNode, AnyNode>()
+  const hoisted = new Map<string, AnyNode>()
+  const visiting = new Set<AnyNode>()
+  const stack: AnyNode[] = []
+
+  const visit = (node: AnyNode): AnyNode => {
+    if (node.kind === "group") {
+      return { ...node, dependencies: node.dependencies.map(visit) }
+    }
+
+    const existingNode = visited.get(node)
+    if (existingNode) return existingNode
+
+    if (node.tag === tag) {
+      const existing = hoisted.get(node.name)
+      if (existing && existing !== node) {
+        throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
+      }
+      hoisted.set(node.name, node)
+      const empty = LayerNode.group([])
+      visited.set(node, empty)
+      return empty
+    }
+    if (node.kind === "unbound") {
+      return node
+    }
+
+    if (visiting.has(node)) {
+      const start = stack.indexOf(node)
+      throw new Error(
+        `Cycle detected in layer tree: ${[...stack.slice(start), node].map((item) => item.name).join(" -> ")}`,
+      )
+    }
+    visiting.add(node)
+    stack.push(node)
+    try {
+      const dependencies = node.dependencies.map(visit)
+      const clone = { ...node, dependencies }
+      visited.set(node, clone)
+      return clone
+    } finally {
+      stack.pop()
+      visiting.delete(node)
+    }
+  }
+
+  return {
+    node: visit(root) as LayerNode.Node<A, E>,
+    hoisted: LayerNode.group(Array.from(hoisted.values())) as LayerNode.Node<unknown, E>,
+  }
+}
+
+export function compile<A, E>(
+  root: LayerNode.Node<A, E, any>,
+  replacements?: ReadonlyMap<Layer.Any, Layer.Any>,
+): Layer.Layer<A, E> {
+  const cache = new Map<AnyNode, RuntimeLayer>()
+  const compileNode = (node: AnyNode): RuntimeLayer => {
+    if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`)
+    const cached = cache.get(node)
+    if (cached) return cached
+    const dependencies = node.dependencies.flatMap(flatten).map(compileNode)
+    const implementation = (replacements?.get(node.implementation!) ?? node.implementation!) as RuntimeLayer
+    const layer =
+      dependencies.length === 0
+        ? implementation
+        : implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]]))
+    cache.set(node, layer)
+    return layer
+  }
+  const layers = flatten(root).map((node) => compileNode(node))
+  const layer = layers.reduce<RuntimeLayer>((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty)
+  return layer as Layer.Layer<A, E>
+}
+
+export function bind<A, E, T extends LayerNode.Tag | undefined>(
+  root: LayerNode.Node<A, E, T>,
+  source: AnyNode,
+  replacement: AnyNode,
+): LayerNode.Node<A, E, T> {
+  if (source.kind !== "unbound") throw new Error(`Cannot bind non-unbound layer node: ${source.name}`)
+  if (source.name !== replacement.name) {
+    throw new Error(`Cannot bind ${source.name} to ${replacement.name}`)
+  }
+  if (source.tag !== replacement.tag) {
+    throw new Error(`Cannot bind ${source.name} across tags`)
+  }
+  const visited = new Map<AnyNode, AnyNode>()
+  const visit = (node: AnyNode): AnyNode => {
+    if (node === source) return replacement
+    const existing = visited.get(node)
+    if (existing) return existing
+    if (node.kind === "unbound") return node
+    const clone = { ...node, dependencies: node.dependencies.map(visit) }
+    visited.set(node, clone)
+    return clone
+  }
+  return visit(root) as LayerNode.Node<A, E, T>
+}
+
+function flatten(node: AnyNode): readonly AnyNode[] {
+  return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node]
+}
+
+export * as LayerNodeTree from "./layer-node-tree"

+ 138 - 0
packages/core/src/effect/layer-node/layer-node.ts

@@ -0,0 +1,138 @@
+import { Brand, Context, Layer } from "effect"
+
+type AnyNode = Node<unknown, unknown, any>
+type NodeList<Item extends AnyNode = AnyNode> = readonly [] | readonly [Item, ...Item[]]
+export type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown, any> ? A : never
+export type Error<Item> = [Item] extends [never] ? never : Item extends Node<unknown, infer E, any> ? E : never
+type NodeTag<Item> = [Item] extends [never] ? undefined : Item extends Node<unknown, unknown, infer T> ? T : never
+type Missing<Required, Dependencies extends NodeList> = Exclude<Required, Output<Dependencies[number]>>
+type CheckDependencies<Implementation extends Layer.Any, Dependencies extends NodeList> = [
+  Missing<Layer.Services<Implementation>, Dependencies>,
+] extends [never]
+  ? unknown
+  : { readonly "Missing dependencies": Missing<Layer.Services<Implementation>, Dependencies> }
+declare const $OutputType: unique symbol
+declare const $ErrorType: unique symbol
+
+export type Tag<Name extends string = string> = Name & Brand.Brand<"LayerNode.Tag">
+
+const makeTag = Brand.nominal<Tag>()
+
+export interface Node<A, E = never, T extends Tag | undefined = undefined> {
+  readonly kind: "layer" | "unbound" | "group"
+  readonly name: string
+  readonly service?: Context.Service.Any
+  readonly implementation?: Layer.Any
+  readonly dependencies: readonly AnyNode[]
+  readonly tag?: T
+  readonly [$OutputType]?: () => A
+  readonly [$ErrorType]?: () => E
+}
+
+type NodeIdentity =
+  | { readonly service: Context.Service.Any; readonly name?: never }
+  | { readonly name: string; readonly service?: never }
+type DistributiveOmit<A, K extends PropertyKey> = A extends unknown ? Omit<A, K> : never
+
+type MakeInput<
+  Implementation extends Layer.Any,
+  Items extends NodeList,
+  T extends Tag | undefined = undefined,
+> = NodeIdentity & {
+  readonly layer: Implementation
+  readonly deps: Items & CheckDependencies<Implementation, NoInfer<Items>>
+  readonly tag?: T
+}
+
+export function make<
+  const Implementation extends Layer.Any,
+  const Items extends NodeList,
+  const T extends Tag | undefined = undefined,
+>(
+  input: MakeInput<Implementation, Items, T>,
+): Node<
+  Layer.Success<Implementation>,
+  Layer.Error<Implementation> | Error<Items[number]>,
+  T
+> {
+  return {
+    kind: "layer",
+    name: input.service !== undefined ? input.service.key : input.name,
+    service: input.service,
+    implementation: input.layer,
+    dependencies: input.deps,
+    tag: input.tag,
+  }
+}
+
+export function unbound<R, Shape, const T extends Tag>(
+  service: Context.Key<R, Shape>,
+  tag: T,
+): Node<R, never, T> {
+  return {
+    kind: "unbound",
+    name: service.key,
+    service,
+    dependencies: [],
+    tag,
+  }
+}
+
+export function group<const Items extends readonly AnyNode[]>(
+  dependencies: Items,
+): Node<Output<Items[number]>, Error<Items[number]>, NodeTag<Items[number]>> {
+  return { kind: "group", name: "group", dependencies }
+}
+
+export type TagConfig = Readonly<Record<string, readonly string[]>>
+type TagNames<Config extends TagConfig> = keyof Config & string
+type NodeInTags<Names extends string> = Node<unknown, unknown, Tag<Names> | undefined>
+type CheckTags<Items extends NodeList, Names extends string> = [Exclude<Items[number], NodeInTags<Names>>] extends [
+  never,
+]
+  ? unknown
+  : { readonly "Invalid tag dependencies": Exclude<Items[number], NodeInTags<Names>> }
+
+export interface Tags<Config extends TagConfig> {
+  readonly values: { readonly [Name in TagNames<Config>]: Tag<Name> }
+  readonly make: <Name extends TagNames<Config>>(
+    name: Name,
+  ) => <const Implementation extends Layer.Any, const Items extends NodeList>(
+    input: DistributiveOmit<MakeInput<Implementation, Items, Tag<Name>>, "tag"> &
+      CheckTags<Items, Name | Extract<Config[Name][number], string>>,
+  ) => Node<
+    Layer.Success<Implementation>,
+    Layer.Error<Implementation> | Error<Items[number]>,
+    Tag<Name>
+  >
+}
+
+export function tags<
+  const Config extends { readonly [Name in keyof Config]: readonly (keyof Config & string)[] },
+>(config: Config): Tags<Config> {
+  const names = Object.keys(config) as TagNames<Config>[]
+  const values = Object.fromEntries(names.map((name) => [name, makeTag(name)])) as Tags<Config>["values"]
+  return {
+    values,
+    make: ((name: TagNames<Config>) => (input: DistributiveOmit<MakeInput<Layer.Any, NodeList, Tag>, "tag">) =>
+      make({ ...input, tag: values[name] })) as Tags<Config>["make"],
+  }
+}
+
+export type Replacement = {
+  readonly source: Layer.Any
+  readonly replacement: Layer.Any
+}
+
+type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never]
+  ? unknown
+  : { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }
+
+export function replace<A, E, R, E2>(
+  source: Layer.Layer<A, E, R>,
+  replacement: Layer.Layer<NoInfer<A>, E2, never> & CheckReplacementErrors<E, NoInfer<E2>>,
+): Replacement {
+  return { source, replacement }
+}
+
+export * as LayerNode from "./layer-node"

+ 18 - 0
packages/core/src/effect/node-build.ts

@@ -0,0 +1,18 @@
+import { Layer } from "effect"
+import { buildLocationServiceMap } from "../location-services"
+import { LocationServiceMap } from "../location-service-map"
+import { LayerNode, LayerNodeTree } from "./layer-node"
+import { makeGlobalNode } from "./node"
+
+export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) {
+  const replacementMap = new Map(replacements?.map((item) => [item.source, item.replacement]))
+
+  const locationMap = buildLocationServiceMap(replacementMap)
+  const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] })
+
+  const app = LayerNodeTree.bind(root, LocationServiceMap.node, locationMapNode)
+
+  return LayerNodeTree.compile(app, replacementMap)
+}
+
+export * as NodeBuild from "./node-build"

+ 14 - 0
packages/core/src/effect/node.ts

@@ -0,0 +1,14 @@
+import { LayerNode } from "./layer-node"
+
+export const tags = LayerNode.tags({
+  location: ["global"],
+  global: [],
+})
+
+export type GlobalNode<A, E = never> = LayerNode.Node<A, E, (typeof tags.values)["global"]>
+export type LocationNode<A, E = never> = LayerNode.Node<A, E, (typeof tags.values)["location"]>
+
+export const makeGlobalNode = tags.make("global")
+export const makeLocationNode = tags.make("location")
+
+export * as Node from "./node"

+ 0 - 11
packages/core/src/effect/scoped-node.ts

@@ -1,11 +0,0 @@
-import { LayerNode } from "./layer-node"
-
-export const tiers = LayerNode.tiers(["location", "global"])
-
-export type GlobalNode<A, E = never> = LayerNode.Node<A, E, (typeof tiers.values)["global"]>
-export type LocationNode<A, E = never> = LayerNode.Node<A, E, (typeof tiers.values)["location"]>
-
-export const makeGlobalNode = tiers.make("global")
-export const makeLocationNode = tiers.make("location")
-
-export * as ScopedNode from "./scoped-node"

+ 2 - 2
packages/core/src/event.ts

@@ -7,7 +7,7 @@ import { and, asc, eq, gt, inArray } from "drizzle-orm"
 import { Database } from "./database/database"
 import { EventSequenceTable, EventTable } from "./event/sql"
 import { Location } from "./location"
-import { LayerNode } from "./effect/layer-node"
+import { makeGlobalNode } from "./effect/node"
 import { isDeepStrictEqual } from "node:util"
 import { Durable } from "@opencode-ai/schema/durable-event-manifest"
 
@@ -635,6 +635,6 @@ export const layerWith = (options?: LayerOptions) =>
   )
 
 export const layer = layerWith()
-export const node = LayerNode.make({ service: Service, layer: layer, deps: [Database.node] })
+export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] })
 
 export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))

+ 3 - 0
packages/core/src/file-mutation.ts

@@ -1,5 +1,6 @@
 export * as FileMutation from "./file-mutation"
 
+import { makeLocationNode } from "./effect/node"
 import { Context, Effect, Layer, Schema } from "effect"
 import { dirname } from "path"
 import { KeyedMutex } from "./effect/keyed-mutex"
@@ -192,6 +193,8 @@ function sameBytes(left: Uint8Array, right: Uint8Array) {
 
 export const locationLayer = layer
 
+export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] })
+
 /**
  * Deferred until the corresponding V2 integrations exist.
  */

+ 7 - 0
packages/core/src/filesystem.ts

@@ -1,5 +1,6 @@
 export * as FileSystem from "./filesystem"
 
+import { makeLocationNode } from "./effect/node"
 import path from "path"
 import { Context, Effect, Layer, Schema } from "effect"
 import { FSUtil } from "./fs-util"
@@ -113,3 +114,9 @@ const baseLayer = Layer.effect(
 export const layer = baseLayer.pipe(Layer.provide(FileSystemSearch.locationLayer), Layer.provide(FSUtil.defaultLayer))
 
 export const locationLayer = layer
+
+export const node = makeLocationNode({
+  service: Service,
+  layer: baseLayer,
+  deps: [FSUtil.node, Location.node, FileSystemSearch.node],
+})

+ 6 - 3
packages/core/src/filesystem/search.ts

@@ -1,5 +1,6 @@
 export * as FileSystemSearch from "./search"
 
+import { makeLocationNode } from "../effect/node"
 import path from "path"
 import { Context, Effect, Layer, Scope } from "effect"
 import { Fff } from "#fff"
@@ -229,6 +230,8 @@ export const fffLayer = Layer.effect(
   }),
 )
 
-export const locationLayer = Layer.unwrap(
-  Effect.sync(() => (Flag.OPENCODE_DISABLE_FFF || !Fff.available() ? ripgrepLayer : fffLayer)),
-)
+const layer = Layer.unwrap(Effect.sync(() => (Flag.OPENCODE_DISABLE_FFF || !Fff.available() ? ripgrepLayer : fffLayer)))
+
+export const locationLayer = layer
+
+export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Location.node, Ripgrep.node] })

+ 7 - 0
packages/core/src/filesystem/watcher.ts

@@ -3,6 +3,7 @@ export * as Watcher from "./watcher"
 // @ts-ignore
 import { createWrapper } from "@parcel/watcher/wrapper"
 import type ParcelWatcher from "@parcel/watcher"
+import { makeLocationNode } from "../effect/node"
 import { Cause, Context, Effect, Layer } from "effect"
 import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher"
 import path from "path"
@@ -133,3 +134,9 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer), Layer.provide(Git.defaultLayer))
+
+export const node = makeLocationNode({
+  service: Service,
+  layer,
+  deps: [FSUtil.node, Location.node, Config.node, Git.node, EventV2.node],
+})

+ 2 - 2
packages/core/src/fs-util.ts

@@ -7,7 +7,7 @@ import { Context, Effect, FileSystem, Layer, Schema } from "effect"
 import type { PlatformError } from "effect/PlatformError"
 import { Glob } from "./util/glob"
 import { serviceUse } from "./effect/service-use"
-import { LayerNode } from "./effect/layer-node"
+import { makeGlobalNode } from "./effect/node"
 import { filesystem } from "./effect/layer-node-platform"
 
 export namespace FSUtil {
@@ -201,7 +201,7 @@ export namespace FSUtil {
   )
 
   export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer))
-  export const node = LayerNode.make({ service: Service, layer: layer, deps: [filesystem] })
+  export const node = makeGlobalNode({ service: Service, layer: layer, deps: [filesystem] })
 
   // Pure helpers that don't need Effect (path manipulation, sync operations)
   export function mimeType(p: string): string {

+ 2 - 2
packages/core/src/git.ts

@@ -7,7 +7,7 @@ import { ChildProcess } from "effect/unstable/process"
 import { AbsolutePath, RelativePath } from "./schema"
 import { FSUtil } from "./fs-util"
 import { AppProcess } from "./process"
-import { LayerNode } from "./effect/layer-node"
+import { makeGlobalNode } from "./effect/node"
 import { File } from "./file"
 import { KeyedMutex } from "./effect/keyed-mutex"
 
@@ -944,7 +944,7 @@ export const layer = Layer.effect(
 )
 
 export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(AppProcess.defaultLayer))
-export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node, AppProcess.node] })
+export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, AppProcess.node] })
 
 interface Result {
   readonly exitCode: number

+ 2 - 2
packages/core/src/global.ts

@@ -5,7 +5,7 @@ import os from "os"
 import { Context, Effect, Layer } from "effect"
 import { Flock } from "./util/flock"
 import { Flag } from "./flag/flag"
-import { LayerNode } from "./effect/layer-node"
+import { makeGlobalNode } from "./effect/node"
 
 const app = "opencode"
 const data = path.join(xdgData!, app)
@@ -77,7 +77,7 @@ export const layer = Layer.effect(
 )
 
 export const defaultLayer = layer
-export const node = LayerNode.make({ service: Service, layer: layer, deps: [] })
+export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] })
 
 export const layerWith = (input: Partial<Interface>) =>
   Layer.effect(

+ 3 - 0
packages/core/src/image.ts

@@ -1,5 +1,6 @@
 export * as Image from "./image"
 
+import { makeLocationNode } from "./effect/node"
 import { Context, Effect, Layer, Schema } from "effect"
 import { Config } from "./config"
 import { FileSystem } from "./filesystem"
@@ -76,3 +77,5 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer))
+
+export const node = makeLocationNode({ service: Service, layer, deps: [Config.node] })

+ 7 - 0
packages/core/src/instruction-context.ts

@@ -9,6 +9,7 @@ import { Location } from "./location"
 import { AbsolutePath } from "./schema"
 import { SystemContext } from "./system-context/index"
 import { SystemContextRegistry } from "./system-context/registry"
+import { makeLocationNode } from "./effect/node"
 
 class File extends Schema.Class<File>("InstructionContext.File")({
   path: AbsolutePath,
@@ -87,6 +88,12 @@ export const layer = Layer.effectDiscard(
   }),
 )
 
+export const node = makeLocationNode({
+  name: "instruction-context",
+  layer,
+  deps: [FSUtil.node, Global.node, Location.node, SystemContextRegistry.node],
+})
+
 function render(files: ReadonlyArray<File>) {
   return files.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n")
 }

+ 3 - 0
packages/core/src/integration.ts

@@ -1,5 +1,6 @@
 export * as Integration from "./integration"
 
+import { makeLocationNode } from "./effect/node"
 import {
   Cause,
   Clock,
@@ -515,3 +516,5 @@ export const locationLayer = Layer.effect(
     })
   }),
 )
+
+export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [Credential.node, EventV2.node] })

+ 0 - 151
packages/core/src/location-layer.ts

@@ -1,151 +0,0 @@
-import { Effect, Layer, LayerMap } from "effect"
-import { Location } from "./location"
-import { Policy } from "./policy"
-import { Config } from "./config"
-import { PluginV2 } from "./plugin"
-import { Catalog } from "./catalog"
-import { Integration } from "./integration"
-import { CommandV2 } from "./command"
-import { AgentV2 } from "./agent"
-import { PluginInternal } from "./plugin/internal"
-import { Project } from "./project"
-import { ProjectCopy } from "./project/copy"
-import { ProjectDirectories } from "./project/directories"
-import { EventV2 } from "./event"
-import { Credential } from "./credential"
-import { Npm } from "./npm"
-import { ModelsDev } from "./models-dev"
-import { FSUtil } from "./fs-util"
-import { Git } from "./git"
-import { Global } from "./global"
-import { Database } from "./database/database"
-import { PermissionV2 } from "./permission"
-import { PermissionSaved } from "./permission/saved"
-import { FileSystem } from "./filesystem"
-import { Ripgrep } from "./ripgrep"
-import { Watcher } from "./filesystem/watcher"
-import { LocationMutation } from "./location-mutation"
-import { FileMutation } from "./file-mutation"
-import { Reference } from "./reference"
-import { ReferenceGuidance } from "./reference/guidance"
-import { RepositoryCache } from "./repository-cache"
-import { Pty } from "./pty"
-import { SkillV2 } from "./skill"
-import { SkillGuidance } from "./skill/guidance"
-import { BuiltInTools } from "./tool/builtins"
-import { Image } from "./image"
-import { ToolRegistry } from "./tool/registry"
-import { ApplicationTools } from "./tool/application-tools"
-import { ToolOutputStore } from "./tool-output-store"
-import { AppProcess } from "./process"
-import { SessionStore } from "./session/store"
-import { SessionTodo } from "./session/todo"
-import { QuestionV2 } from "./question"
-import { LLMClient } from "@opencode-ai/llm"
-import { RequestExecutor } from "@opencode-ai/llm/route"
-import * as SessionRunnerLLM from "./session/runner/llm"
-import { SessionRunnerModel } from "./session/runner/model"
-import { SystemContextBuiltIns } from "./system-context/builtins"
-import { FetchHttpClient } from "effect/unstable/http"
-import { Snapshot } from "./snapshot"
-
-export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("@opencode/example/LocationServiceMap", {
-  lookup: (ref: Location.Ref) => {
-    const boot = Layer.effectDiscard(
-      Effect.logInfo("booting location services", { directory: ref.directory, workspaceID: ref.workspaceID }),
-    )
-    const location = Location.layer(ref)
-    const systemContext = SystemContextBuiltIns.locationLayer
-    const base = Layer.mergeAll(
-      location,
-      Policy.locationLayer,
-      Config.locationLayer,
-      Reference.locationLayer,
-      PluginV2.locationLayer,
-      Catalog.locationLayer,
-      Integration.locationLayer,
-      CommandV2.locationLayer,
-      AgentV2.locationLayer,
-      PluginInternal.locationLayer,
-      ProjectCopy.locationLayer,
-      FileSystem.locationLayer,
-      Watcher.locationLayer,
-      Pty.locationLayer,
-      SkillV2.locationLayer,
-      systemContext,
-      LocationMutation.locationLayer.pipe(Layer.orDie),
-    ).pipe(Layer.provideMerge(location))
-    const resources = ToolOutputStore.layer.pipe(Layer.provide(base))
-    const permissionsAndTools = ToolRegistry.layer.pipe(
-      Layer.provideMerge(PermissionV2.locationLayer),
-      Layer.provide(resources),
-      Layer.provide(base),
-    )
-    const services = Layer.mergeAll(base, resources, permissionsAndTools)
-    const image = Image.layer.pipe(Layer.provide(services))
-    const mutation = FileMutation.locationLayer.pipe(Layer.provide(services))
-    const skillGuidance = SkillGuidance.locationLayer.pipe(Layer.provide(services))
-    const referenceGuidance = ReferenceGuidance.locationLayer.pipe(Layer.provide(services))
-    const todos = SessionTodo.layer.pipe(Layer.provide(services))
-    const questions = QuestionV2.locationLayer.pipe(Layer.provide(services))
-    const builtInTools = BuiltInTools.locationLayer.pipe(
-      Layer.provide(services),
-      Layer.provide(mutation),
-      Layer.provide(resources),
-      Layer.provide(todos),
-      Layer.provide(questions),
-      Layer.provide(image),
-    )
-    const model = SessionRunnerModel.locationLayer.pipe(Layer.provide(services))
-    const snapshot = Snapshot.locationLayer.pipe(Layer.provide(services))
-    const runner = SessionRunnerLLM.defaultLayer.pipe(
-      Layer.provide(services),
-      Layer.provide(model),
-      Layer.provide(skillGuidance),
-      Layer.provide(referenceGuidance),
-      Layer.provide(snapshot),
-    )
-
-    // Kick off a background project copy refresh to update locations now that we
-    // have a location
-    const projectCopyRefresh = Layer.effectDiscard(ProjectCopy.refreshAfterBoot).pipe(Layer.provide(services))
-
-    return Layer.mergeAll(
-      boot,
-      services,
-      image,
-      mutation,
-      resources,
-      todos,
-      questions,
-      model,
-      snapshot,
-      runner,
-      builtInTools,
-      referenceGuidance,
-      projectCopyRefresh,
-    ).pipe(Layer.fresh)
-  },
-  idleTimeToLive: "60 minutes",
-  dependencies: [
-    Project.defaultLayer,
-    EventV2.defaultLayer,
-    Credential.defaultLayer,
-    Npm.defaultLayer,
-    ModelsDev.defaultLayer,
-    FSUtil.defaultLayer,
-    Git.defaultLayer,
-    AppProcess.defaultLayer,
-    Global.defaultLayer,
-    Ripgrep.defaultLayer,
-    Database.defaultLayer,
-    ProjectDirectories.defaultLayer,
-    SessionStore.layer.pipe(Layer.provide(Database.defaultLayer)),
-    PermissionSaved.defaultLayer,
-    RepositoryCache.defaultLayer,
-    LLMClient.layer.pipe(Layer.provide(RequestExecutor.defaultLayer)),
-    FetchHttpClient.layer,
-    ToolOutputStore.defaultCleanupLayer,
-    ApplicationTools.layer,
-  ],
-}) {}

+ 7 - 0
packages/core/src/location-mutation.ts

@@ -1,5 +1,6 @@
 export * as LocationMutation from "./location-mutation"
 
+import { makeLocationNode } from "./effect/node"
 import path from "path"
 import { Context, Effect, Layer, Schema } from "effect"
 import { FSUtil } from "./fs-util"
@@ -153,3 +154,9 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer
+
+export const node = makeLocationNode({
+  service: Service,
+  layer: layer.pipe(Layer.orDie),
+  deps: [FSUtil.node, Location.node],
+})

+ 18 - 0
packages/core/src/location-service-map.ts

@@ -0,0 +1,18 @@
+import { Context, Effect, Layer, LayerMap } from "effect"
+import { LayerNode } from "./effect/layer-node"
+import { Node } from "./effect/node"
+import { Location } from "./location"
+import type { LocationError, LocationServices } from "./location-services"
+
+export class Service extends Context.Service<
+  Service,
+  LayerMap.LayerMap<Location.Ref, LocationServices, LocationError>
+>()("@opencode/example/LocationServiceMap") {
+  static get(ref: Location.Ref) {
+    return Layer.unwrap(Effect.map(Service, (locations) => locations.get(ref)))
+  }
+}
+
+export const node = LayerNode.unbound(Service, Node.tags.values.global)
+
+export * as LocationServiceMap from "./location-service-map"

+ 112 - 0
packages/core/src/location-services.ts

@@ -0,0 +1,112 @@
+import { Effect, Layer, LayerMap } from "effect"
+import { AgentV2 } from "./agent"
+import { AISDK } from "./aisdk"
+import { Catalog } from "./catalog"
+import { CommandV2 } from "./command"
+import { Config } from "./config"
+import { LayerNode, LayerNodeTree } from "./effect/layer-node"
+import { Node } from "./effect/node"
+import { FileMutation } from "./file-mutation"
+import { FileSystem } from "./filesystem"
+import { FileSystemSearch } from "./filesystem/search"
+import { Watcher } from "./filesystem/watcher"
+import { Image } from "./image"
+import { Integration } from "./integration"
+import { Location } from "./location"
+import { LocationMutation } from "./location-mutation"
+import { LocationServiceMap } from "./location-service-map"
+import { PermissionV2 } from "./permission"
+import { PluginV2 } from "./plugin"
+import { PluginInternal } from "./plugin/internal"
+import { Policy } from "./policy"
+import { ProjectCopy } from "./project/copy"
+import { Pty } from "./pty"
+import { QuestionV2 } from "./question"
+import { Reference } from "./reference"
+import { ReferenceGuidance } from "./reference/guidance"
+import * as SessionRunnerLLM from "./session/runner/llm"
+import { SessionRunnerModel } from "./session/runner/model"
+import { SessionTodo } from "./session/todo"
+import { SkillV2 } from "./skill"
+import { SkillGuidance } from "./skill/guidance"
+import { Snapshot } from "./snapshot"
+import { SystemContextBuiltIns } from "./system-context/builtins"
+import { SystemContextRegistry } from "./system-context/registry"
+import { BuiltInTools } from "./tool/builtins"
+import { ReadToolFileSystem } from "./tool/read-filesystem"
+import { ToolRegistry } from "./tool/registry"
+import { ToolOutputStore } from "./tool-output-store"
+
+export { LocationServiceMap } from "./location-service-map"
+
+export const locationServices = LayerNode.group([
+  Location.node,
+  Policy.node,
+  Config.node,
+  AgentV2.node,
+  CommandV2.node,
+  Reference.node,
+  Integration.node,
+  Catalog.node,
+  AISDK.node,
+  PluginV2.node,
+  PluginInternal.node,
+  ProjectCopy.node,
+  ProjectCopy.refreshNode,
+  FileSystemSearch.node,
+  FileSystem.node,
+  Watcher.node,
+  Pty.node,
+  SkillV2.node,
+  SystemContextRegistry.node,
+  SystemContextBuiltIns.node,
+  LocationMutation.node,
+  FileMutation.node,
+  PermissionV2.node,
+  ToolOutputStore.node,
+  ToolRegistry.node,
+  ToolRegistry.toolsNode,
+  Image.node,
+  SkillGuidance.node,
+  ReferenceGuidance.node,
+  SessionTodo.node,
+  QuestionV2.node,
+  ReadToolFileSystem.node,
+  BuiltInTools.node,
+  SessionRunnerModel.node,
+  Snapshot.node,
+  SessionRunnerLLM.node,
+])
+
+export type LocationServices = LayerNode.Output<typeof locationServices>
+export type LocationError = LayerNode.Error<typeof locationServices>
+
+export function buildLocationServiceMap(
+  replacements?: ReadonlyMap<Layer.Any, Layer.Any>,
+): Layer.Layer<LocationServiceMap.Service> {
+  return Layer.effect(
+    LocationServiceMap.Service,
+    LayerMap.make(
+      (ref: Location.Ref) => {
+        const location = LayerNodeTree.hoist(
+          LayerNodeTree.bind(locationServices, Location.node, Location.boundNode(ref)),
+          Node.tags.values.global,
+        )
+        return LayerNodeTree.compile(location.node, replacements).pipe(
+          Layer.fresh,
+          Layer.tap(() =>
+            Effect.logInfo("booting location services", {
+              directory: ref.directory,
+              workspaceID: ref.workspaceID,
+            }),
+          ),
+          Layer.provide(LayerNodeTree.compile(location.hoisted, replacements)),
+        )
+      },
+      { idleTimeToLive: "60 minutes" },
+    ),
+  )
+}
+
+// This is temporary for backwards compatibility
+export const locationServiceMapLayer = buildLocationServiceMap()

+ 11 - 0
packages/core/src/location.ts

@@ -1,6 +1,8 @@
 import { Context, Effect, Layer } from "effect"
 import { Info, Ref, response } from "@opencode-ai/schema/location"
 import { Project } from "./project"
+import { LayerNode } from "./effect/layer-node"
+import { makeLocationNode, tags } from "./effect/node"
 
 export * as Location from "./location"
 
@@ -12,6 +14,8 @@ export interface Interface extends Info {
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/Location") {}
 
+export const node = LayerNode.unbound(Service, tags.values.location)
+
 export const layer = (ref: Ref) =>
   Layer.effect(
     Service,
@@ -26,3 +30,10 @@ export const layer = (ref: Ref) =>
       })
     }),
   )
+
+export const boundNode = (ref: Ref) =>
+  makeLocationNode({
+    service: Service,
+    layer: layer(ref),
+    deps: [Project.node],
+  })

+ 2 - 2
packages/core/src/models-dev.ts

@@ -9,7 +9,7 @@ import { Hash } from "./util/hash"
 import { FSUtil } from "./fs-util"
 import { InstallationChannel, InstallationVersion } from "./installation/version"
 import { EventV2 } from "./event"
-import { LayerNode } from "./effect/layer-node"
+import { makeGlobalNode } from "./effect/node"
 import { httpClient } from "./effect/layer-node-platform"
 
 export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
@@ -244,6 +244,6 @@ export const defaultLayer = layer.pipe(
   Layer.provide(FSUtil.defaultLayer),
   Layer.provide(EventV2.defaultLayer),
 )
-export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node, EventV2.node, httpClient] })
+export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, EventV2.node, httpClient] })
 
 export * as ModelsDev from "./models-dev"

+ 2 - 2
packages/core/src/npm.ts

@@ -7,7 +7,7 @@ import { NodeFileSystem } from "@effect/platform-node"
 import { FSUtil } from "./fs-util"
 import { Global } from "./global"
 import { EffectFlock } from "./util/effect-flock"
-import { LayerNode } from "./effect/layer-node"
+import { makeGlobalNode } from "./effect/node"
 import { filesystem } from "./effect/layer-node-platform"
 import { makeRuntime } from "./effect/runtime"
 import { NpmConfig } from "./npm-config"
@@ -253,7 +253,7 @@ export const defaultLayer = layer.pipe(
   Layer.provide(Global.layer),
   Layer.provide(NodeFileSystem.layer),
 )
-export const node = LayerNode.make({
+export const node = makeGlobalNode({
   service: Service,
   layer: layer,
   deps: [FSUtil.node, Global.node, filesystem, EffectFlock.node],

+ 7 - 0
packages/core/src/permission.ts

@@ -1,5 +1,6 @@
 export * as PermissionV2 from "./permission"
 
+import { makeLocationNode } from "./effect/node"
 import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect"
 import { Permission } from "@opencode-ai/schema/permission"
 import { EventV2 } from "./event"
@@ -300,3 +301,9 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer.pipe(Layer.provideMerge(AgentV2.locationLayer))
+
+export const node = makeLocationNode({
+  service: Service,
+  layer,
+  deps: [EventV2.node, Location.node, AgentV2.node, SessionStore.node, PermissionSaved.node],
+})

+ 3 - 0
packages/core/src/permission/saved.ts

@@ -3,6 +3,7 @@ export * as PermissionSaved from "./saved"
 import { eq } from "drizzle-orm"
 import { Context, Effect, Layer, Schema } from "effect"
 import { Database } from "../database/database"
+import { makeGlobalNode } from "../effect/node"
 import { ProjectV2 } from "../project"
 import { PermissionTable } from "./sql"
 import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
@@ -76,3 +77,5 @@ export const layer = Layer.effect(
 )
 
 export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
+
+export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })

+ 16 - 0
packages/core/src/plugin.ts

@@ -1,5 +1,6 @@
 export * as PluginV2 from "./plugin"
 
+import { makeLocationNode } from "./effect/node"
 import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect"
 import type { Plugin as PluginRuntime } from "@opencode-ai/plugin/v2/effect"
 import { Plugin } from "@opencode-ai/schema/plugin"
@@ -150,3 +151,18 @@ export const locationLayer = layer.pipe(
   Layer.provideMerge(Reference.locationLayer),
   Layer.provideMerge(SkillV2.locationLayer),
 )
+
+export const node = makeLocationNode({
+  service: Service,
+  layer,
+  deps: [
+    EventV2.node,
+    AgentV2.node,
+    AISDK.node,
+    Catalog.node,
+    CommandV2.node,
+    Integration.node,
+    Reference.node,
+    SkillV2.node,
+  ],
+})

+ 29 - 2
packages/core/src/plugin/internal.ts

@@ -1,5 +1,7 @@
 export * as PluginInternal from "./internal"
 
+import { makeLocationNode } from "../effect/node"
+import { httpClient } from "../effect/layer-node-platform"
 import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
 import { Effect, Layer, Scope } from "effect"
 import { AgentV2 } from "../agent"
@@ -57,7 +59,7 @@ export function define<R>(plugin: Plugin<R>) {
   return plugin
 }
 
-export const locationLayer = Layer.effectDiscard(
+const layer = Layer.effectDiscard(
   Effect.gen(function* () {
     const catalog = yield* Catalog.Service
     const commands = yield* CommandV2.Service
@@ -117,9 +119,34 @@ export const locationLayer = Layer.effectDiscard(
       yield* add(VariantPlugin.Plugin)
     }).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true }))
   }),
-).pipe(
+)
+
+export const locationLayer = layer.pipe(
   Layer.provideMerge(PluginV2.locationLayer),
   Layer.provideMerge(Config.locationLayer),
   Layer.provideMerge(FileSystem.locationLayer),
   Layer.provideMerge(FetchHttpClient.layer),
 )
+
+export const node = makeLocationNode({
+  name: "plugin-internal",
+  layer,
+  deps: [
+    Catalog.node,
+    CommandV2.node,
+    PluginV2.node,
+    Integration.node,
+    AgentV2.node,
+    Config.node,
+    Location.node,
+    ModelsDev.node,
+    Npm.node,
+    EventV2.node,
+    FSUtil.node,
+    FileSystem.node,
+    Global.node,
+    httpClient,
+    SkillV2.node,
+    Reference.node,
+  ],
+})

+ 3 - 0
packages/core/src/policy.ts

@@ -1,5 +1,6 @@
 export * as Policy from "./policy"
 
+import { makeLocationNode } from "./effect/node"
 import { Context, Effect as EffectRuntime, Layer, Schema } from "effect"
 import { Wildcard } from "./util/wildcard"
 import { Location } from "./location"
@@ -44,3 +45,5 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer
+
+export const node = makeLocationNode({ service: Service, layer, deps: [Location.node] })

+ 2 - 2
packages/core/src/process.ts

@@ -3,7 +3,7 @@ import type { PlatformError } from "effect/PlatformError"
 import { ChildProcess } from "effect/unstable/process"
 import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
 import { CrossSpawnSpawner } from "./cross-spawn-spawner"
-import { LayerNode } from "./effect/layer-node"
+import { makeGlobalNode } from "./effect/node"
 
 export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()("AppProcessError", {
   command: Schema.String,
@@ -238,6 +238,6 @@ export const layer = Layer.effect(
 )
 
 export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
-export const node = LayerNode.make({ service: Service, layer: layer, deps: [CrossSpawnSpawner.node] })
+export const node = makeGlobalNode({ service: Service, layer: layer, deps: [CrossSpawnSpawner.node] })
 
 export * as AppProcess from "./process"

+ 2 - 2
packages/core/src/project.ts

@@ -6,7 +6,7 @@ import path from "path"
 import { AbsolutePath } from "./schema"
 import { FSUtil } from "./fs-util"
 import { Git } from "./git"
-import { LayerNode } from "./effect/layer-node"
+import { makeGlobalNode } from "./effect/node"
 import { Hash } from "./util/hash"
 import { ProjectDirectories } from "./project/directories"
 import { ProjectSchema } from "./project/schema"
@@ -134,7 +134,7 @@ export const defaultLayer = layer.pipe(
   Layer.provide(Git.defaultLayer),
   Layer.provideMerge(ProjectDirectories.defaultLayer),
 )
-export const node = LayerNode.make({
+export const node = makeGlobalNode({
   service: Service,
   layer: layer,
   deps: [FSUtil.node, Git.node, ProjectDirectories.node],

+ 8 - 2
packages/core/src/project/copy.ts

@@ -5,7 +5,7 @@ import path from "path"
 import { AbsolutePath } from "../schema"
 import { FSUtil } from "../fs-util"
 import { Git } from "../git"
-import { LayerNode } from "../effect/layer-node"
+import { makeLocationNode } from "../effect/node"
 import { Project } from "../project"
 import { ProjectDirectories } from "./directories"
 import { makeGitWorktreeStrategy } from "./copy-strategies"
@@ -279,8 +279,14 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer
-export const node = LayerNode.make({
+export const node = makeLocationNode({
   service: Service,
   layer: layer,
   deps: [FSUtil.node, Git.node, ProjectDirectories.node, EventV2.node, Database.node],
 })
+
+export const refreshNode = makeLocationNode({
+  name: "project-copy-refresh",
+  layer: Layer.effectDiscard(refreshAfterBoot),
+  deps: [node, Location.node],
+})

+ 2 - 2
packages/core/src/project/directories.ts

@@ -3,7 +3,7 @@ export * as ProjectDirectories from "./directories"
 import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm"
 import { Context, Effect, Layer, Schema } from "effect"
 import { Database } from "../database/database"
-import { LayerNode } from "../effect/layer-node"
+import { makeGlobalNode } from "../effect/node"
 import { AbsolutePath, optional } from "../schema"
 import { ProjectSchema } from "./schema"
 import { ProjectDirectoryTable } from "./sql"
@@ -156,4 +156,4 @@ export const layer = Layer.effect(
 )
 
 export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
-export const node = LayerNode.make({ service: Service, layer: layer, deps: [Database.node] })
+export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] })

+ 3 - 0
packages/core/src/pty.ts

@@ -1,5 +1,6 @@
 export * as Pty from "./pty"
 
+import { makeLocationNode } from "./effect/node"
 import type { Disp, Proc } from "#pty"
 import { Context, Effect, Layer, Schema, Types } from "effect"
 import { Pty } from "@opencode-ai/schema/pty"
@@ -313,3 +314,5 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer))
+
+export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Location.node, Config.node] })

+ 2 - 2
packages/core/src/pty/ticket.ts

@@ -4,7 +4,7 @@ import { WorkspaceV2 } from "../workspace"
 import { PtyTicket } from "@opencode-ai/schema/pty-ticket"
 import { PtyID } from "./schema"
 import { Cache, Context, Duration, Effect, Layer } from "effect"
-import { LayerNode } from "../effect/layer-node"
+import { makeGlobalNode } from "../effect/node"
 
 const DEFAULT_TTL = Duration.seconds(60)
 const CAPACITY = 10_000
@@ -54,4 +54,4 @@ export const make = (ttl: Duration.Input = DEFAULT_TTL) =>
 export const layer = Layer.effect(Service, make())
 
 export const defaultLayer = layer
-export const node = LayerNode.make({ service: Service, layer: layer, deps: [] })
+export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] })

+ 3 - 0
packages/core/src/question.ts

@@ -1,5 +1,6 @@
 export * as QuestionV2 from "./question"
 
+import { makeLocationNode } from "./effect/node"
 import { Context, Deferred, Effect, Layer, Schema } from "effect"
 import { Question } from "@opencode-ai/schema/question"
 import { EventV2 } from "./event"
@@ -148,3 +149,5 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer
+
+export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })

+ 7 - 0
packages/core/src/reference.ts

@@ -1,5 +1,6 @@
 export * as Reference from "./reference"
 
+import { makeLocationNode } from "./effect/node"
 import { Context, Effect, Layer, Scope, Types } from "effect"
 import { Reference } from "@opencode-ai/schema/reference"
 import { Global } from "./global"
@@ -120,3 +121,9 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer
+
+export const node = makeLocationNode({
+  service: Service,
+  layer,
+  deps: [Global.node, EventV2.node, RepositoryCache.node],
+})

+ 3 - 0
packages/core/src/reference/guidance.ts

@@ -1,5 +1,6 @@
 export * as ReferenceGuidance from "./guidance"
 
+import { makeLocationNode } from "../effect/node"
 import { Context, Effect, Layer, Schema } from "effect"
 import { Reference } from "../reference"
 import { SystemContext } from "../system-context/index"
@@ -64,3 +65,5 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer
+
+export const node = makeLocationNode({ service: Service, layer, deps: [Reference.node] })

+ 7 - 0
packages/core/src/repository-cache.ts

@@ -5,6 +5,7 @@ import { Git } from "./git"
 import { Global } from "./global"
 import { Repository } from "./repository"
 import { AbsolutePath } from "./schema"
+import { makeGlobalNode } from "./effect/node"
 import { EffectFlock } from "./util/effect-flock"
 
 export type Result = {
@@ -229,6 +230,12 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
   Layer.provide(Global.defaultLayer),
 )
 
+export const node = makeGlobalNode({
+  service: Service,
+  layer,
+  deps: [EffectFlock.node, FSUtil.node, Git.node, Global.node],
+})
+
 function statusForRepository(input: { reuse: boolean; refresh?: boolean; branchMatches?: boolean }) {
   if (!input.reuse) return "cloned" as const
   if (input.branchMatches === false || input.refresh) return "refreshed" as const

+ 2 - 2
packages/core/src/ripgrep.ts

@@ -3,7 +3,7 @@ export * as Ripgrep from "./ripgrep"
 import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect"
 import { ChildProcess } from "effect/unstable/process"
 import { Entry, Match } from "@opencode-ai/schema/filesystem"
-import { LayerNode } from "./effect/layer-node"
+import { makeGlobalNode } from "./effect/node"
 import { AppProcess, collectStream, waitForAbort } from "./process"
 import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
 import { RipgrepBinary } from "./ripgrep/binary"
@@ -279,4 +279,4 @@ export const layer = Layer.effect(
 )
 
 export const defaultLayer = layer.pipe(Layer.provide(Layer.merge(RipgrepBinary.defaultLayer, AppProcess.defaultLayer)))
-export const node = LayerNode.make({ service: Service, layer: layer, deps: [RipgrepBinary.node, AppProcess.node] })
+export const node = makeGlobalNode({ service: Service, layer: layer, deps: [RipgrepBinary.node, AppProcess.node] })

+ 2 - 2
packages/core/src/ripgrep/binary.ts

@@ -4,7 +4,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/
 import { ChildProcess } from "effect/unstable/process"
 import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
 import { CrossSpawnSpawner } from "../cross-spawn-spawner"
-import { LayerNode } from "../effect/layer-node"
+import { makeGlobalNode } from "../effect/node"
 import { httpClient } from "../effect/layer-node-platform"
 import { FSUtil } from "../fs-util"
 import { Global } from "../global"
@@ -130,7 +130,7 @@ export namespace RipgrepBinary {
     Layer.provide(CrossSpawnSpawner.defaultLayer),
   )
 
-  export const node = LayerNode.make({
+  export const node = makeGlobalNode({
     service: Service,
     layer: layer,
     deps: [FSUtil.node, httpClient, CrossSpawnSpawner.node],

+ 22 - 15
packages/core/src/session.ts

@@ -27,6 +27,8 @@ import { fromRow } from "./session/info"
 import { SessionRunner } from "./session/runner/index"
 import { SessionStore } from "./session/store"
 import { SessionExecution } from "./session/execution"
+import { makeGlobalNode } from "./effect/node"
+import { LocationServiceMap } from "./location-service-map"
 import { MessageDecodeError } from "./session/error"
 import { SessionEvent } from "./session/event"
 import { SessionInput } from "./session/input"
@@ -179,19 +181,16 @@ export interface Interface {
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Session") {}
 
-export const layer = Layer.unwrap(
-  Effect.promise(() => import("./location-layer")).pipe(
-    Effect.map(({ LocationServiceMap }) =>
-      Layer.effect(
-        Service,
-        Effect.gen(function* () {
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
           const database = yield* Database.Service
           const db = database.db
           const events = yield* EventV2.Service
           const projects = yield* ProjectV2.Service
           const execution = yield* SessionExecution.Service
           const store = yield* SessionStore.Service
-          const locations = yield* LocationServiceMap
+          const locations = yield* LocationServiceMap.Service
           const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message)
           const isDurableSessionEvent = Schema.is(SessionEvent.Durable)
           const decode = (row: typeof SessionMessageTable.$inferSelect) =>
@@ -451,17 +450,11 @@ export const layer = Layer.unwrap(
             },
           })
 
-          return result
-        }),
-      ),
-    ),
-  ),
+    return result
+  }),
 )
 
 export const defaultLayer = layer.pipe(
-  Layer.provide(
-    Layer.unwrap(Effect.promise(() => import("./location-layer")).pipe(Effect.map((m) => m.LocationServiceMap.layer))),
-  ),
   Layer.provide(SessionStore.defaultLayer),
   Layer.provide(SessionProjector.defaultLayer),
   Layer.provide(EventV2.defaultLayer),
@@ -483,3 +476,17 @@ const resolvePrompt = (input: PromptInput.Prompt) =>
       }
     }),
   })
+
+export const node = makeGlobalNode({
+  service: Service,
+  layer: layer.pipe(Layer.orDie),
+  deps: [
+    Database.node,
+    EventV2.node,
+    ProjectV2.node,
+    SessionExecution.node,
+    SessionStore.node,
+    LocationServiceMap.node,
+    SessionProjector.node,
+  ],
+})

+ 4 - 0
packages/core/src/session/execution.ts

@@ -1,6 +1,8 @@
 export * as SessionExecution from "./execution"
 
 import { Context, Effect, Layer } from "effect"
+import { LayerNode } from "../effect/layer-node"
+import { Node } from "../effect/node"
 import { SessionRunner } from "./runner/index"
 import { SessionSchema } from "./schema"
 
@@ -18,6 +20,8 @@ export interface Interface {
 /** Routes execution from a Session ID to the runner owned by that Session's Location. */
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionExecution") {}
 
+export const node = LayerNode.unbound(Service, Node.tags.values.global)
+
 /** Low-level compatibility layer for callers that only need durable Session recording. */
 export const noopLayer = Layer.succeed(
   Service,

+ 11 - 2
packages/core/src/session/execution/local.ts

@@ -1,5 +1,6 @@
 import { Cause, Effect, Layer } from "effect"
-import { LocationServiceMap } from "../../location-layer"
+import { LocationServiceMap } from "../../location-service-map"
+import { makeGlobalNode } from "../../effect/node"
 import { SessionRunCoordinator } from "../run-coordinator"
 import { SessionRunner } from "../runner"
 import { SessionSchema } from "../schema"
@@ -11,7 +12,7 @@ export const layer = Layer.effect(
   SessionExecution.Service,
   Effect.gen(function* () {
     const store = yield* SessionStore.Service
-    const locations = yield* LocationServiceMap
+    const locations = yield* LocationServiceMap.Service
     const coordinator = yield* SessionRunCoordinator.make<SessionSchema.ID, SessionRunner.RunError>({
       drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) {
         const session = yield* store.get(sessionID)
@@ -37,3 +38,11 @@ export const layer = Layer.effect(
 )
 
 export const defaultLayer = layer.pipe(Layer.provide(SessionStore.defaultLayer))
+
+export const node = makeGlobalNode({
+  service: SessionExecution.Service,
+  layer,
+  deps: [SessionStore.node, LocationServiceMap.node],
+})
+
+export * as SessionExecutionLocal from "./local"

+ 2 - 2
packages/core/src/session/projector.ts

@@ -4,7 +4,7 @@ import { and, desc, eq, gt, or, sql } from "drizzle-orm"
 import { DateTime, Effect, Layer, Schema } from "effect"
 import { Database } from "../database/database"
 import { EventV2 } from "../event"
-import { LayerNode } from "../effect/layer-node"
+import { makeGlobalNode } from "../effect/node"
 import { SessionEvent } from "./event"
 import { SessionV1 } from "../v1/session"
 import { WorkspaceTable } from "../control-plane/workspace.sql"
@@ -456,4 +456,4 @@ export const layer = Layer.effectDiscard(
 )
 
 export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(Database.defaultLayer))
-export const node = LayerNode.make({ name: "session-projector", layer, deps: [EventV2.node, Database.node] })
+export const node = makeGlobalNode({ name: "session-projector", layer, deps: [EventV2.node, Database.node] })

+ 22 - 0
packages/core/src/session/runner/llm.ts

@@ -36,6 +36,8 @@ import { createLLMEventPublisher } from "./publish-llm-event"
 import { toLLMMessages } from "./to-llm-message"
 import { MAX_STEPS_PROMPT } from "./max-steps"
 import { Snapshot } from "../../snapshot"
+import { makeLocationNode } from "../../effect/node"
+import { llmClient } from "../../effect/layer-node-platform"
 
 /**
  * Runs one durable coding-agent Session until it settles.
@@ -404,3 +406,23 @@ export const layer = Layer.effect(
 )
 
 export const defaultLayer = layer
+
+export const node = makeLocationNode({
+  service: Service,
+  layer,
+  deps: [
+    EventV2.node,
+    llmClient,
+    AgentV2.node,
+    ToolRegistry.node,
+    SessionRunnerModel.node,
+    SessionStore.node,
+    Location.node,
+    SystemContextRegistry.node,
+    SkillGuidance.node,
+    ReferenceGuidance.node,
+    Config.node,
+    Snapshot.node,
+    Database.node,
+  ],
+})

+ 3 - 0
packages/core/src/session/runner/model.ts

@@ -1,5 +1,6 @@
 export * as SessionRunnerModel from "./model"
 
+import { makeLocationNode } from "../../effect/node"
 import { type Model } from "@opencode-ai/llm"
 import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
 import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat"
@@ -213,3 +214,5 @@ export const locationLayer = Layer.effect(
     })
   }),
 )
+
+export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [Catalog.node, Integration.node] })

+ 3 - 0
packages/core/src/session/store.ts

@@ -3,6 +3,7 @@ export * as SessionStore from "./store"
 import { eq } from "drizzle-orm"
 import { Context, Effect, Layer, Schema } from "effect"
 import { Database } from "../database/database"
+import { makeGlobalNode } from "../effect/node"
 import { SessionHistory } from "./history"
 import { MessageDecodeError } from "./error"
 import { SessionMessage } from "./message"
@@ -60,3 +61,5 @@ export const layer = Layer.effect(
 )
 
 export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
+
+export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })

+ 3 - 0
packages/core/src/session/todo.ts

@@ -4,6 +4,7 @@ import { asc, eq } from "drizzle-orm"
 import { Context, Effect, Layer } from "effect"
 import { SessionTodo } from "@opencode-ai/schema/session-todo"
 import { Database } from "../database/database"
+import { makeLocationNode } from "../effect/node"
 import { EventV2 } from "../event"
 import { SessionSchema } from "./schema"
 import { TodoTable } from "./sql"
@@ -75,3 +76,5 @@ export const layer = Layer.effect(
 )
 
 export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(Database.defaultLayer))
+
+export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Database.node] })

+ 3 - 0
packages/core/src/skill.ts

@@ -1,5 +1,6 @@
 export * as SkillV2 from "./skill"
 
+import { makeLocationNode } from "./effect/node"
 import path from "path"
 import { Context, Effect, Layer, Schema, Types } from "effect"
 import { Skill } from "@opencode-ai/schema/skill"
@@ -129,3 +130,5 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer.pipe(Layer.provide(SkillDiscovery.defaultLayer))
+
+export const node = makeLocationNode({ service: Service, layer, deps: [SkillDiscovery.node, FSUtil.node] })

+ 4 - 0
packages/core/src/skill/discovery.ts

@@ -5,6 +5,8 @@ import { Context, Effect, Layer, Schedule, Schema } from "effect"
 import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
 import { FSUtil } from "../fs-util"
 import { Global } from "../global"
+import { makeGlobalNode } from "../effect/node"
+import { httpClient } from "../effect/layer-node-platform"
 import { AbsolutePath } from "../schema"
 
 const skillConcurrency = 4
@@ -213,3 +215,5 @@ export const defaultLayer = layer.pipe(
   Layer.provide(FSUtil.defaultLayer),
   Layer.provide(Global.defaultLayer),
 )
+
+export const node = makeGlobalNode({ service: Service, layer, deps: [httpClient, FSUtil.node, Global.node] })

+ 3 - 0
packages/core/src/skill/guidance.ts

@@ -1,5 +1,6 @@
 export * as SkillGuidance from "./guidance"
 
+import { makeLocationNode } from "../effect/node"
 import { Context, Effect, Layer, Schema } from "effect"
 import { AgentV2 } from "../agent"
 import { PermissionV2 } from "../permission"
@@ -71,3 +72,5 @@ export const layer = Layer.effect(
 )
 
 export const locationLayer = layer
+
+export const node = makeLocationNode({ service: Service, layer, deps: [SkillV2.node] })

+ 7 - 0
packages/core/src/snapshot.ts

@@ -1,5 +1,6 @@
 export * as Snapshot from "./snapshot"
 
+import { makeLocationNode } from "./effect/node"
 import path from "path"
 import { Context, Effect, Layer, Schema } from "effect"
 import { Config } from "./config"
@@ -228,6 +229,12 @@ export const layer = Layer.effect(
 
 export const locationLayer = layer.pipe(Layer.provideMerge(Config.locationLayer))
 
+export const node = makeLocationNode({
+  service: Service,
+  layer,
+  deps: [Config.node, FSUtil.node, Git.node, Global.node, Location.node],
+})
+
 export const noopLayer = Layer.succeed(
   Service,
   Service.of({

+ 9 - 0
packages/core/src/system-context/builtins.ts

@@ -1,10 +1,13 @@
 export * as SystemContextBuiltIns from "./builtins"
 
+import { makeLocationNode } from "../effect/node"
 import { DateTime, Effect, Layer, Schema } from "effect"
 import { Location } from "../location"
 import { SystemContext } from "./index"
 import { InstructionContext } from "../instruction-context"
 import { SystemContextRegistry } from "./registry"
+import { FSUtil } from "../fs-util"
+import { Global } from "../global"
 
 const builtIns = Layer.effectDiscard(
   Effect.gen(function* () {
@@ -45,3 +48,9 @@ export const layer = Layer.mergeAll(builtIns, InstructionContext.layer).pipe(
 )
 
 export const locationLayer = layer
+
+export const node = makeLocationNode({
+  name: "system-context-builtins",
+  layer,
+  deps: [Location.node, SystemContextRegistry.node, InstructionContext.node, FSUtil.node, Global.node],
+})

+ 3 - 0
packages/core/src/system-context/registry.ts

@@ -2,6 +2,7 @@ export * as SystemContextRegistry from "./registry"
 
 import { Context, Effect, Layer, Ref, Scope } from "effect"
 import { SystemContext } from "./index"
+import { makeLocationNode } from "../effect/node"
 
 export interface Entry {
   readonly key: SystemContext.Key
@@ -44,3 +45,5 @@ export const layer = Layer.effect(
     })
   }),
 )
+
+export const node = makeLocationNode({ service: Service, layer, deps: [] })

+ 5 - 0
packages/core/src/tool-output-store.ts

@@ -5,6 +5,7 @@ import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effe
 import { Config } from "./config"
 import { FSUtil } from "./fs-util"
 import { Global } from "./global"
+import { makeGlobalNode, makeLocationNode } from "./effect/node"
 import { SessionSchema } from "./session/schema"
 import { Identifier } from "./util/identifier"
 import type { ToolOutput } from "@opencode-ai/llm"
@@ -193,6 +194,8 @@ export const layer = Layer.effect(
 
 export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.defaultLayer))
 
+export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node, Config.node] })
+
 /** Runs retention scanning once globally rather than once per active Location. */
 export const cleanupLayer = Layer.effectDiscard(
   Effect.gen(function* () {
@@ -202,3 +205,5 @@ export const cleanupLayer = Layer.effectDiscard(
 )
 
 export const defaultCleanupLayer = Layer.merge(defaultLayer, cleanupLayer.pipe(Layer.provide(defaultLayer)))
+
+export const cleanupNode = makeGlobalNode({ name: "tool-output-cleanup", layer: defaultCleanupLayer, deps: [] })

+ 3 - 0
packages/core/src/tool/application-tools.ts

@@ -3,6 +3,7 @@ export * as ApplicationTools from "./application-tools"
 import { Context, Effect, Layer, Scope } from "effect"
 import { State } from "../state"
 import { Tool } from "./tool"
+import { makeGlobalNode } from "../effect/node"
 
 type Data = {
   readonly entries: Map<string, Entry>
@@ -52,3 +53,5 @@ export const layer = Layer.effect(
     })
   }),
 )
+
+export const node = makeGlobalNode({ service: Service, layer, deps: [] })

+ 37 - 0
packages/core/src/tool/builtins.ts

@@ -1,5 +1,6 @@
 export * as BuiltInTools from "./builtins"
 
+import { makeLocationNode } from "../effect/node"
 import { Layer } from "effect"
 import { BashTool } from "./bash"
 import { ApplyPatchTool } from "./apply-patch"
@@ -14,6 +15,20 @@ import { TodoWriteTool } from "./todowrite"
 import { WebFetchTool } from "./webfetch"
 import { WebSearchTool } from "./websearch"
 import { WriteTool } from "./write"
+import { FSUtil } from "../fs-util"
+import { AppProcess } from "../process"
+import { Config } from "../config"
+import { Location } from "../location"
+import { LocationMutation } from "../location-mutation"
+import { FileMutation } from "../file-mutation"
+import { PermissionV2 } from "../permission"
+import { Ripgrep } from "../ripgrep"
+import { Image } from "../image"
+import { QuestionV2 } from "../question"
+import { SkillV2 } from "../skill"
+import { SessionTodo } from "../session/todo"
+import { ToolRegistry } from "./registry"
+import { httpClient } from "../effect/layer-node-platform"
 
 /**
  * Composes only the shipped Location-scoped built-in tool transforms.
@@ -42,3 +57,25 @@ export const locationLayer = Layer.mergeAll(
   WebSearchTool.layer.pipe(Layer.provide(WebSearchTool.defaultConfigLayer)),
   WriteTool.layer,
 )
+
+export const node = makeLocationNode({
+  name: "built-in-tools",
+  layer: locationLayer,
+  deps: [
+    ToolRegistry.toolsNode,
+    FSUtil.node,
+    AppProcess.node,
+    Config.node,
+    Location.node,
+    LocationMutation.node,
+    FileMutation.node,
+    PermissionV2.node,
+    Ripgrep.node,
+    Image.node,
+    QuestionV2.node,
+    SkillV2.node,
+    SessionTodo.node,
+    ReadToolFileSystem.node,
+    httpClient,
+  ],
+})

+ 3 - 0
packages/core/src/tool/read-filesystem.ts

@@ -5,6 +5,7 @@ import { pathToFileURL } from "url"
 import { Context, Effect, Layer, Option, Schema } from "effect"
 import { FileSystem } from "../filesystem"
 import { FSUtil } from "../fs-util"
+import { makeLocationNode } from "../effect/node"
 import { AbsolutePath, PositiveInt, RelativePath } from "../schema"
 
 export const MAX_READ_LINES = 2_000
@@ -361,3 +362,5 @@ export const layer = Layer.effect(
     })
   }),
 )
+
+export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] })

+ 13 - 0
packages/core/src/tool/registry.ts

@@ -11,6 +11,7 @@ import { Wildcard } from "../util/wildcard"
 import { ApplicationTools } from "./application-tools"
 import { definition, permission, settle, validateName, type AnyTool, type RegistrationError } from "./tool"
 import { Tools } from "./tools"
+import { makeLocationNode } from "../effect/node"
 
 export type ExecuteInput = {
   readonly sessionID: SessionSchema.ID
@@ -137,3 +138,15 @@ export const defaultLayer = layer.pipe(
   Layer.provide(ApplicationTools.layer),
   Layer.provide(ToolOutputStore.defaultLayer),
 )
+
+export const node = makeLocationNode({
+  service: Service,
+  layer,
+  deps: [ApplicationTools.node, ToolOutputStore.node],
+})
+
+export const toolsNode = makeLocationNode({
+  service: Tools.Service,
+  layer,
+  deps: [ApplicationTools.node, ToolOutputStore.node],
+})

+ 2 - 2
packages/core/src/util/effect-flock.ts

@@ -6,7 +6,7 @@ import type { FileSystem, Scope } from "effect"
 import type { PlatformError } from "effect/PlatformError"
 import { FSUtil } from "../fs-util"
 import { Global } from "../global"
-import { LayerNode } from "../effect/layer-node"
+import { makeGlobalNode } from "../effect/node"
 import { Hash } from "./hash"
 
 export namespace EffectFlock {
@@ -281,5 +281,5 @@ export namespace EffectFlock {
   )
 
   export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.layer))
-  export const node = LayerNode.make({ service: Service, layer: layer, deps: [Global.node, FSUtil.node] })
+  export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Global.node, FSUtil.node] })
 }

+ 138 - 0
packages/core/test/effect/layer-node/layer-node-types.test.ts

@@ -0,0 +1,138 @@
+import { test } from "bun:test"
+import { Context, Effect, Layer } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node"
+import { makeGlobalNode, makeLocationNode } from "@opencode-ai/core/effect/node"
+
+class A extends Context.Service<A, {}>()("test/LayerNodeA") {}
+class B extends Context.Service<B, {}>()("test/LayerNodeB") {}
+class C extends Context.Service<C, {}>()("test/LayerNodeC") {}
+class LayerError {
+  readonly _tag = "LayerError"
+}
+class OtherError {
+  readonly _tag = "OtherError"
+}
+
+const tags = LayerNode.tags({ app: [] })
+const make = tags.make("app")
+const build = <A, E>(root: LayerNode.Node<A, E, any>) =>
+  LayerNodeTree.compile(root) as Layer.Layer<A, E>
+const aLayer = Layer.succeed(A, A.of({}))
+const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
+const cLayer = Layer.effect(
+  C,
+  Effect.gen(function* () {
+    yield* A
+    yield* B
+    return C.of({})
+  }),
+)
+const failingA = Layer.effect(A, Effect.fail(new LayerError()))
+const a = make({ service: A, layer: aLayer, deps: [] })
+const b = make({ service: B, layer: bLayer, deps: [a] })
+const c = make({ service: C, layer: cLayer, deps: [a, b] })
+const failing = make({ service: A, layer: failingA, deps: [] })
+const dependent = make({ service: B, layer: bLayer, deps: [failing] })
+const inputA = LayerNode.unbound(A, tags.values.app)
+const inputDependent = make({ service: B, layer: bLayer, deps: [inputA] })
+
+make({ name: "manual-a", layer: aLayer, deps: [] })
+
+// @ts-expect-error A node must have a service or name
+make({ layer: aLayer, deps: [] })
+
+// @ts-expect-error Service and name are mutually exclusive
+make({ service: A, name: "a", layer: aLayer, deps: [] })
+
+// @ts-expect-error B requires A
+make({ service: B, layer: bLayer, deps: [] })
+
+// @ts-expect-error C requires A and B
+make({ service: C, layer: cLayer, deps: [a] })
+
+const closed = build(LayerNode.group([c]))
+const closedWithError = build(LayerNode.group([dependent]))
+const checkClosed: Layer.Layer<C, never, never> = closed
+const checkError: Layer.Layer<B, LayerError, never> = closedWithError
+void checkClosed
+void checkError
+
+LayerNode.replace(aLayer, Layer.succeed(A, A.of({})))
+
+// @ts-expect-error Replacement must provide A
+LayerNode.replace(aLayer, Layer.succeed(B, B.of({})))
+
+// @ts-expect-error Replacement cannot introduce a new error
+LayerNode.replace(aLayer, Layer.effect(A, Effect.fail(new OtherError())))
+
+// @ts-expect-error Replacement must be closed
+LayerNode.replace(bLayer, bLayer)
+
+class TagA extends Context.Service<TagA, {}>()("test/TagA") {}
+class TagB extends Context.Service<TagB, {}>()("test/TagB") {}
+class TagC extends Context.Service<TagC, {}>()("test/TagC") {}
+
+const scopedTags = LayerNode.tags({ request: ["global"], global: [] })
+const request = scopedTags.make("request")
+const global = scopedTags.make("global")
+const globalA = global({ service: TagA, layer: Layer.succeed(TagA, TagA.of({})), deps: [] })
+const requestA = request({ service: TagA, layer: Layer.succeed(TagA, TagA.of({})), deps: [] })
+const requestB = request({ service: TagB, layer: Layer.succeed(TagB, TagB.of({})), deps: [] })
+const tagBLayer = Layer.effect(TagB, Effect.as(TagA, TagB.of({})))
+const tagCLayer = Layer.effect(
+  TagC,
+  Effect.gen(function* () {
+    yield* TagA
+    yield* TagB
+    return TagC.of({})
+  }),
+)
+
+request({ service: TagB, layer: tagBLayer, deps: [globalA] })
+request({ service: TagC, layer: tagCLayer, deps: [globalA, requestB] })
+request({ service: TagC, layer: tagCLayer, deps: [LayerNode.group([globalA, requestB])] })
+
+// @ts-expect-error Tag configuration can only reference declared tags
+LayerNode.tags({ request: ["missing"], global: [] })
+
+// @ts-expect-error An unrelated dependency cannot satisfy TagA
+request({ service: TagB, layer: tagBLayer, deps: [requestB] })
+
+// @ts-expect-error Providing only TagA leaves TagB missing
+request({ service: TagC, layer: tagCLayer, deps: [globalA] })
+
+// @ts-expect-error Providing only TagB leaves TagA missing
+request({ service: TagC, layer: tagCLayer, deps: [requestB] })
+
+// @ts-expect-error Duplicate TagA providers still leave TagB missing
+request({ service: TagC, layer: tagCLayer, deps: [globalA, requestA] })
+
+// @ts-expect-error A group with only TagA still leaves TagB missing
+request({ service: TagC, layer: tagCLayer, deps: [LayerNode.group([globalA])] })
+
+// @ts-expect-error Global cannot depend on request
+global({ service: TagB, layer: tagBLayer, deps: [requestA] })
+
+// @ts-expect-error Groups preserve their child tags
+global({ service: TagB, layer: tagBLayer, deps: [LayerNode.group([requestA])] })
+
+class ScopedA extends Context.Service<ScopedA, {}>()("test/ScopedA") {}
+class ScopedB extends Context.Service<ScopedB, {}>()("test/ScopedB") {}
+
+const scopedA = Layer.succeed(ScopedA, ScopedA.of({}))
+const scopedB = Layer.effect(ScopedB, Effect.as(ScopedA, ScopedB.of({})))
+const globalScopedA = makeGlobalNode({ service: ScopedA, layer: scopedA, deps: [] })
+const locationScopedA = makeLocationNode({ service: ScopedA, layer: scopedA, deps: [] })
+
+makeGlobalNode({ service: ScopedB, layer: scopedB, deps: [globalScopedA] })
+makeLocationNode({ service: ScopedB, layer: scopedB, deps: [globalScopedA] })
+makeLocationNode({ service: ScopedB, layer: scopedB, deps: [locationScopedA] })
+
+// @ts-expect-error Global nodes cannot depend on location nodes
+makeGlobalNode({ service: ScopedB, layer: scopedB, deps: [locationScopedA] })
+
+// @ts-expect-error ScopedB requires ScopedA
+makeLocationNode({ service: ScopedB, layer: scopedB, deps: [] })
+
+test("type exploration compiles", () => {})

+ 226 - 0
packages/core/test/effect/layer-node/layer-node.test.ts

@@ -0,0 +1,226 @@
+import { describe, expect, test } from "bun:test"
+import { Context, Effect, Layer } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node"
+
+class Value extends Context.Service<Value, { readonly value: string }>()("test/LayerNodeValue") {}
+class Greeting extends Context.Service<Greeting, { readonly value: string }>()("test/LayerNodeGreeting") {}
+class Left extends Context.Service<Left, { readonly value: string }>()("test/LayerNodeLeft") {}
+class Right extends Context.Service<Right, { readonly value: string }>()("test/LayerNodeRight") {}
+class Database extends Context.Service<Database, { readonly name: string }>()("test/GraphDatabase") {}
+class Users extends Context.Service<Users, { readonly list: Effect.Effect<string[]> }>()("test/GraphUsers") {}
+class App extends Context.Service<App, { readonly run: Effect.Effect<string[]> }>()("test/GraphApp") {}
+
+const tags = LayerNode.tags({ app: [] })
+const make = tags.make("app")
+const build = <A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) =>
+  LayerNodeTree.compile(
+    root,
+    new Map(replacements?.map((item) => [item.source, item.replacement])),
+  ) as Layer.Layer<A, E>
+const valueLayer = Layer.succeed(Value, Value.of({ value: "production" }))
+const greetingLayer = Layer.effect(
+  Greeting,
+  Effect.map(Value, (value) => Greeting.of({ value: `hello ${value.value}` })),
+)
+const value = make({ service: Value, layer: valueLayer, deps: [] })
+const greeting = make({ service: Greeting, layer: greetingLayer, deps: [value] })
+
+describe("layer node", () => {
+  test("builds an untagged graph", async () => {
+    const value = LayerNode.make({ service: Value, layer: valueLayer, deps: [] })
+    const greeting = LayerNode.make({ service: Greeting, layer: greetingLayer, deps: [value] })
+    const program = Effect.map(Greeting, (item) => item.value).pipe(
+      Effect.provide(LayerNodeTree.compile(LayerNode.group([greeting]))),
+    )
+    expect(await Effect.runPromise(program)).toBe("hello production")
+  })
+
+  test("builds a dependency graph", async () => {
+    const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(build(LayerNode.group([greeting]))))
+    expect(await Effect.runPromise(program)).toBe("hello production")
+  })
+
+  test("exposes roots but hides transitive dependencies", () => {
+    const layer = build(LayerNode.group([greeting]))
+    const check: Layer.Layer<Greeting> = layer
+    void check
+  })
+
+  test("preserves branch-specific implementations across roots", async () => {
+    const firstValue = make({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "first" })), deps: [] })
+    const secondValue = make({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
+    const leftLayer = Layer.effect(
+      Left,
+      Effect.map(Value, (item) => Left.of({ value: item.value })),
+    )
+    const rightLayer = Layer.effect(
+      Right,
+      Effect.map(Value, (item) => Right.of({ value: item.value })),
+    )
+    const left = make({ service: Left, layer: leftLayer, deps: [firstValue] })
+    const right = make({ service: Right, layer: rightLayer, deps: [secondValue] })
+    const layer = build(LayerNode.group([left, right]))
+    const program = Effect.gen(function* () {
+      return [(yield* Left).value, (yield* Right).value]
+    }).pipe(Effect.provide(layer))
+    expect(await Effect.runPromise(program)).toEqual(["first", "second"])
+  })
+
+  test("requires unbound nodes to be bound before compilation", async () => {
+    const unbound = LayerNode.unbound(Value, tags.values.app)
+    const greeting = make({ service: Greeting, layer: greetingLayer, deps: [unbound] })
+    const tree = LayerNode.group([greeting])
+    expect(() => LayerNodeTree.compile(tree)).toThrow("Unbound layer node: test/LayerNodeValue")
+    const bound = LayerNodeTree.bind(tree, unbound, value)
+    const layer = LayerNodeTree.compile(bound) as Layer.Layer<Greeting>
+    const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(layer))
+    expect(await Effect.runPromise(program)).toBe("hello production")
+  })
+
+  test("replaces a layer by identity", async () => {
+    const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
+    const program = Effect.map(Greeting, (item) => item.value).pipe(
+      Effect.provide(build(LayerNode.group([greeting]), [LayerNode.replace(valueLayer, replacement)])),
+    )
+    expect(await Effect.runPromise(program)).toBe("hello simulation")
+  })
+
+  test("replaces every use of the same layer", async () => {
+    const leftLayer = Layer.effect(
+      Left,
+      Effect.map(Value, (item) => Left.of({ value: item.value })),
+    )
+    const rightLayer = Layer.effect(
+      Right,
+      Effect.map(Value, (item) => Right.of({ value: item.value })),
+    )
+    const left = make({ service: Left, layer: leftLayer, deps: [value] })
+    const right = make({ service: Right, layer: rightLayer, deps: [value] })
+    const replacement = Layer.succeed(Value, Value.of({ value: "replaced" }))
+    const layer = build(LayerNode.group([left, right]), [LayerNode.replace(valueLayer, replacement)])
+    const program = Effect.gen(function* () {
+      return [(yield* Left).value, (yield* Right).value]
+    }).pipe(Effect.provide(layer))
+    expect(await Effect.runPromise(program)).toEqual(["replaced", "replaced"])
+  })
+
+  test("does not acquire an unused replacement", async () => {
+    let acquisitions = 0
+    const other = Layer.succeed(Value, Value.of({ value: "other" }))
+    const replacement = Layer.effect(
+      Value,
+      Effect.sync(() => {
+        acquisitions++
+        return Value.of({ value: "replacement" })
+      }),
+    )
+    await Effect.runPromise(
+      Effect.map(Greeting, (item) => item.value).pipe(
+        Effect.provide(build(LayerNode.group([greeting]), [LayerNode.replace(other, replacement)])),
+      ),
+    )
+    expect(acquisitions).toBe(0)
+  })
+
+  test("hoists and compiles tagged graphs", async () => {
+    const tags = LayerNode.tags({ location: ["global"], global: [] })
+    const global = tags.make("global")
+    const location = tags.make("location")
+    const database = global({
+      service: Database,
+      layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
+      deps: [],
+    })
+    const users = location({
+      service: Users,
+      layer: Layer.effect(
+        Users,
+        Effect.gen(function* () {
+          const db = yield* Database
+          return Users.of({ list: Effect.succeed([db.name]) })
+        }),
+      ),
+      deps: [database],
+    })
+    const app = location({
+      service: App,
+      layer: Layer.effect(
+        App,
+        Effect.gen(function* () {
+          const service = yield* Users
+          return App.of({ run: service.list })
+        }),
+      ),
+      deps: [users],
+    })
+
+    const result = LayerNodeTree.hoist(LayerNode.group([app]), tags.values.global)
+    expect(result.node.dependencies[0]?.dependencies[0]?.dependencies[0]).toMatchObject({
+      kind: "group",
+      dependencies: [],
+    })
+    expect(result.hoisted.dependencies).toEqual([database])
+
+    const layer = LayerNodeTree.compile(result.node).pipe(
+      Layer.provide(LayerNodeTree.compile(result.hoisted)),
+    ) as unknown as Layer.Layer<App>
+    const program = Effect.gen(function* () {
+      return yield* (yield* App).run
+    }).pipe(Effect.provide(layer))
+
+    expect(await Effect.runPromise(program)).toEqual(["Alice"])
+  })
+
+  test("rejects conflicting hoisted implementations", () => {
+    const tags = LayerNode.tags({ location: ["global"], global: [] })
+    const global = tags.make("global")
+    const location = tags.make("location")
+    const first = global({
+      service: Database,
+      layer: Layer.succeed(Database, Database.of({ name: "first" })),
+      deps: [],
+    })
+    const second = global({
+      service: Database,
+      layer: Layer.succeed(Database, Database.of({ name: "second" })),
+      deps: [],
+    })
+    const left = location({
+      service: Users,
+      layer: Layer.effect(Users, Effect.as(Database, Users.of({ list: Effect.succeed([]) }))),
+      deps: [first],
+    })
+    const right = location({
+      service: App,
+      layer: Layer.effect(App, Effect.as(Database, App.of({ run: Effect.succeed([]) }))),
+      deps: [second],
+    })
+
+    expect(() => LayerNodeTree.hoist(LayerNode.group([left, right]), tags.values.global)).toThrow(
+      "Tag global has conflicting implementations for test/GraphDatabase",
+    )
+  })
+
+  test("treats dependency groups as transparent while hoisting", () => {
+    const tags = LayerNode.tags({ location: ["global"], global: [] })
+    const global = tags.make("global")
+    const location = tags.make("location")
+    const database = global({
+      service: Database,
+      layer: Layer.succeed(Database, Database.of({ name: "Alice" })),
+      deps: [],
+    })
+    const users = location({
+      service: Users,
+      layer: Layer.effect(Users, Effect.as(Database, Users.of({ list: Effect.succeed([]) }))),
+      deps: [LayerNode.group([database])],
+    })
+    const result = LayerNodeTree.hoist(LayerNode.group([users]), tags.values.global)
+
+    expect(result.node.dependencies[0]?.dependencies[0]?.dependencies[0]).toMatchObject({
+      kind: "group",
+      dependencies: [],
+    })
+  })
+})

+ 119 - 0
packages/core/test/effect/layer-node/node-build.test.ts

@@ -0,0 +1,119 @@
+import { describe, expect, test } from "bun:test"
+import { Context, Effect, Layer } from "effect"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { Node } from "@opencode-ai/core/effect/node"
+import { NodeBuild } from "@opencode-ai/core/effect/node-build"
+import { Location } from "@opencode-ai/core/location"
+import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
+import { Project } from "@opencode-ai/core/project"
+import { AbsolutePath } from "@opencode-ai/core/schema"
+import { tmpdir } from "../../fixture/tmpdir"
+
+class Value extends Context.Service<Value, { readonly value: string }>()("test/TagValue") {}
+class Result extends Context.Service<Result, { readonly value: string }>()("test/TagResult") {}
+class Left extends Context.Service<Left, { readonly value: string }>()("test/TagLeft") {}
+class Right extends Context.Service<Right, { readonly value: string }>()("test/TagRight") {}
+class Last extends Context.Service<Last, { readonly value: string }>()("test/TagLast") {}
+
+describe("node build", () => {
+  test("shares top-level project with location services", async () => {
+    await using tmp = await tmpdir()
+    let acquisitions = 0
+    const projectLayer = Layer.effect(
+      Project.Service,
+      Effect.sync(() => {
+        acquisitions++
+        return Project.Service.of({
+          directories: () => Effect.succeed([]),
+          resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }),
+          commit: () => Effect.void,
+        })
+      }),
+    )
+    const layer = NodeBuild.build(LayerNode.group([Project.node, LocationServiceMap.node]), [
+      LayerNode.replace(Project.layer, projectLayer),
+    ])
+    const ref = Location.Ref.make({ directory: AbsolutePath.make(tmp.path) })
+    const program = Effect.gen(function* () {
+      yield* Project.Service
+      const locations = yield* LocationServiceMap.Service
+      return yield* Location.Service.pipe(Effect.provide(locations.get(ref)))
+    }).pipe(Effect.provide(layer))
+
+    expect((await Effect.runPromise(program)).directory).toBe(ref.directory)
+    expect(acquisitions).toBe(1)
+  })
+
+  test("returns a composed application layer", async () => {
+    const value = Node.makeGlobalNode({
+      service: Value,
+      layer: Layer.succeed(Value, Value.of({ value: "value" })),
+      deps: [],
+    })
+    const result = Node.makeGlobalNode({
+      service: Result,
+      layer: Layer.effect(
+        Result,
+        Effect.gen(function* () {
+          return Result.of({ value: (yield* Value).value })
+        }),
+      ),
+      deps: [value],
+    })
+    const serviceLayer = NodeBuild.build(LayerNode.group([result]))
+    const program = Effect.gen(function* () {
+      return (yield* Result).value
+    }).pipe(Effect.provide(serviceLayer))
+
+    expect(await Effect.runPromise(program)).toBe("value")
+  })
+
+  test("rebinds same-tag providers without reacquiring them", async () => {
+    let firstAcquisitions = 0
+    const tags = LayerNode.tags({ global: [] })
+    const global = tags.make("global")
+    const first = global({
+      service: Value,
+      layer: Layer.effect(
+        Value,
+        Effect.sync(() => {
+          firstAcquisitions++
+          return Value.of({ value: "first" })
+        }),
+      ),
+      deps: [],
+    })
+    const second = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
+    const left = global({
+      service: Left,
+      layer: Layer.effect(
+        Left,
+        Effect.map(Value, (value) => Left.of({ value: value.value })),
+      ),
+      deps: [first],
+    })
+    const right = global({
+      service: Right,
+      layer: Layer.effect(
+        Right,
+        Effect.map(Value, (value) => Right.of({ value: value.value })),
+      ),
+      deps: [second],
+    })
+    const last = global({
+      service: Last,
+      layer: Layer.effect(
+        Last,
+        Effect.map(Value, (value) => Last.of({ value: value.value })),
+      ),
+      deps: [first],
+    })
+    const layer = NodeBuild.build(LayerNode.group([left, right, last])) as Layer.Layer<Left | Right | Last>
+    const values = Effect.gen(function* () {
+      return [(yield* Left).value, (yield* Right).value, (yield* Last).value]
+    }).pipe(Effect.provide(layer))
+
+    expect(await Effect.runPromise(values)).toEqual(["first", "second", "first"])
+    expect(firstAcquisitions).toBe(1)
+  })
+})

+ 6 - 6
packages/core/test/location-layer.test.ts

@@ -6,7 +6,7 @@ import { Tool } from "@opencode-ai/core/tool/tool"
 import { define } from "@opencode-ai/plugin/v2/effect"
 import { AgentV2 } from "@opencode-ai/core/agent"
 import { Catalog } from "@opencode-ai/core/catalog"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { Location } from "@opencode-ai/core/location"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { ModelV2 } from "@opencode-ai/core/model"
@@ -34,7 +34,7 @@ const applicationTools = ApplicationTools.layer
 const it = testEffect(
   Layer.merge(
     Layer.mergeAll(applicationTools, Database.defaultLayer, EventV2.defaultLayer),
-    LocationServiceMap.layer.pipe(
+    locationServiceMapLayer.pipe(
       Layer.provide(applicationTools),
       Layer.provide(
         Layer.mergeAll(
@@ -60,7 +60,7 @@ describe("LocationServiceMap", () => {
       Effect.flatMap((dir) =>
         Effect.scoped(
           Effect.gen(function* () {
-            const locations = yield* LocationServiceMap
+            const locations = yield* LocationServiceMap.Service
             const directory = AbsolutePath.make(dir.path)
             const constructed = Location.Ref.make({ directory })
             const decoded = Schema.decodeUnknownSync(Location.Ref)({ directory })
@@ -111,7 +111,7 @@ describe("LocationServiceMap", () => {
               }
             }).pipe(
               Effect.scoped,
-              Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
+              Effect.provide(LocationServiceMap.Service.get(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
             )
 
           const blockedState = yield* update(blocked.path)
@@ -191,7 +191,7 @@ describe("LocationServiceMap", () => {
                 location,
               }),
             ),
-          ).pipe(Effect.provide(LocationServiceMap.get(location)), Effect.flip)
+          ).pipe(Effect.provide(LocationServiceMap.Service.get(location)), Effect.flip)
 
           expect(failure).toMatchObject({
             _tag: "SessionRunnerModel.ModelUnavailableError",
@@ -231,7 +231,7 @@ describe("LocationServiceMap", () => {
           })
         }).pipe(
           Effect.scoped,
-          Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))),
+          Effect.provide(LocationServiceMap.Service.get(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))),
         ),
       ),
     ),

+ 2 - 2
packages/core/test/move-session.test.ts

@@ -14,7 +14,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { ProjectDirectories } from "@opencode-ai/core/project/directories"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { SessionV2 } from "@opencode-ai/core/session"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { SessionExecution } from "@opencode-ai/core/session/execution"
 import { SessionProjector } from "@opencode-ai/core/session/projector"
 import { SessionTable } from "@opencode-ai/core/session/sql"
@@ -29,7 +29,7 @@ const project = Project.layer.pipe(
   Layer.provide(ProjectDirectories.defaultLayer),
 )
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(LocationServiceMap.layer),
+  Layer.provide(locationServiceMapLayer),
   Layer.provide(Database.defaultLayer),
   Layer.provide(EventV2.defaultLayer),
   Layer.provide(project),

+ 2 - 2
packages/core/test/permission.test.ts

@@ -11,7 +11,7 @@ import { Project } from "@opencode-ai/core/project"
 import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { SessionV2 } from "@opencode-ai/core/session"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { SessionTable } from "@opencode-ai/core/session/sql"
 import { SessionExecution } from "@opencode-ai/core/session/execution"
 import { SessionStore } from "@opencode-ai/core/session/store"
@@ -24,7 +24,7 @@ const current = Layer.succeed(
   Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
 )
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(LocationServiceMap.layer),
+  Layer.provide(locationServiceMapLayer),
   Layer.provide(EventV2.defaultLayer),
   Layer.provide(Database.defaultLayer),
   Layer.provide(SessionStore.defaultLayer),

+ 2 - 2
packages/core/test/session-create.test.ts

@@ -13,7 +13,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { SessionV2 } from "@opencode-ai/core/session"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { Prompt } from "@opencode-ai/core/session/prompt"
 import { SessionProjector } from "@opencode-ai/core/session/projector"
@@ -35,7 +35,7 @@ const projects = Layer.succeed(
   }),
 )
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(LocationServiceMap.layer),
+  Layer.provide(locationServiceMapLayer),
   Layer.provide(EventV2.defaultLayer),
   Layer.provide(Database.defaultLayer),
   Layer.provide(SessionStore.defaultLayer),

+ 2 - 2
packages/core/test/session-history.test.ts

@@ -3,7 +3,7 @@ import { Effect, Layer, Schema } from "effect"
 import { Database } from "@opencode-ai/core/database/database"
 import { EventV2 } from "@opencode-ai/core/event"
 import { Location } from "@opencode-ai/core/location"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { ProjectV2 } from "@opencode-ai/core/project"
 import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { AbsolutePath } from "@opencode-ai/core/schema"
@@ -23,7 +23,7 @@ const projects = Layer.succeed(
   }),
 )
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(LocationServiceMap.layer),
+  Layer.provide(locationServiceMapLayer),
   Layer.provide(EventV2.defaultLayer),
   Layer.provide(Database.defaultLayer),
   Layer.provide(SessionStore.defaultLayer),

+ 2 - 2
packages/core/test/session-projector.test.ts

@@ -10,7 +10,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { SessionV2 } from "@opencode-ai/core/session"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { SessionEvent } from "@opencode-ai/core/session/event"
 import { SessionMessage } from "@opencode-ai/core/session/message"
 import { Prompt } from "@opencode-ai/core/session/prompt"
@@ -164,7 +164,7 @@ describe("SessionProjector", () => {
     }).pipe(
       Effect.provide(
         SessionV2.layer.pipe(
-          Layer.provide(LocationServiceMap.layer),
+          Layer.provide(locationServiceMapLayer),
           Layer.provide(EventV2.defaultLayer),
           Layer.provide(Database.defaultLayer),
           Layer.provide(Project.defaultLayer),

+ 2 - 2
packages/core/test/session-prompt.test.ts

@@ -9,7 +9,7 @@ import { Project } from "@opencode-ai/core/project"
 import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { SessionV2 } from "@opencode-ai/core/session"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { Prompt } from "@opencode-ai/core/session/prompt"
 import { SessionMessage } from "@opencode-ai/core/session/message"
 import { SessionProjector } from "@opencode-ai/core/session/projector"
@@ -42,7 +42,7 @@ const execution = Layer.succeed(
   }),
 )
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(LocationServiceMap.layer),
+  Layer.provide(locationServiceMapLayer),
   Layer.provide(EventV2.defaultLayer),
   Layer.provide(Database.defaultLayer),
   Layer.provide(SessionStore.defaultLayer),

+ 2 - 2
packages/core/test/session-runner-recorded.test.ts

@@ -12,7 +12,7 @@ import { Project } from "@opencode-ai/core/project"
 import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { SessionV2 } from "@opencode-ai/core/session"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { Snapshot } from "@opencode-ai/core/snapshot"
 import { Prompt } from "@opencode-ai/core/session/prompt"
 import { SessionProjector } from "@opencode-ai/core/session/projector"
@@ -103,7 +103,7 @@ const execution = Layer.effect(
   }),
 ).pipe(Layer.provide(runner))
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(LocationServiceMap.layer),
+  Layer.provide(locationServiceMapLayer),
   Layer.provide(EventV2.defaultLayer),
   Layer.provide(Database.defaultLayer),
   Layer.provide(SessionStore.defaultLayer),

+ 2 - 2
packages/core/test/session-runner.test.ts

@@ -19,7 +19,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { QuestionV2 } from "@opencode-ai/core/question"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { SessionV2 } from "@opencode-ai/core/session"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { Snapshot } from "@opencode-ai/core/snapshot"
 import { ContextSnapshotDecodeError } from "@opencode-ai/core/session/error"
 import { SessionEvent } from "@opencode-ai/core/session/event"
@@ -262,7 +262,7 @@ const execution = Layer.effect(
   }),
 ).pipe(Layer.provide(runner))
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(LocationServiceMap.layer),
+  Layer.provide(locationServiceMapLayer),
   Layer.provide(EventV2.defaultLayer),
   Layer.provide(Database.defaultLayer),
   Layer.provide(SessionStore.defaultLayer),

+ 4 - 4
packages/opencode/src/agent/agent.ts

@@ -27,7 +27,7 @@ import * as OtelTracer from "@effect/opentelemetry/Tracer"
 import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ModelV2 } from "@opencode-ai/core/model"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { Reference } from "@opencode-ai/core/reference"
 import { Location } from "@opencode-ai/core/location"
 import { PluginV2 } from "@opencode-ai/core/plugin"
@@ -93,7 +93,7 @@ export const layer = Layer.effect(
     const plugin = yield* Plugin.Service
     const skill = yield* Skill.Service
     const provider = yield* Provider.Service
-    const locations = yield* LocationServiceMap
+    const locations = yield* LocationServiceMap.Service
 
     const state = yield* InstanceState.make<State>(
       Effect.fn("Agent.state")(function* (ctx) {
@@ -444,10 +444,10 @@ export const defaultLayer = layer.pipe(
   Layer.provide(Auth.defaultLayer),
   Layer.provide(Config.defaultLayer),
   Layer.provide(Skill.defaultLayer),
-  Layer.provide(LocationServiceMap.layer),
+  Layer.provide(locationServiceMapLayer),
 )
 
-const locationServiceMapNode = LayerNode.make({ service: Service, layer: LocationServiceMap.layer, deps: [] })
+const locationServiceMapNode = LayerNode.make({ service: LocationServiceMap.Service, layer: locationServiceMapLayer, deps: [] })
 
 export const node = LayerNode.make({
   service: Service,

+ 3 - 3
packages/opencode/src/cli/cmd/debug/file.ts

@@ -1,7 +1,7 @@
 import { EOL } from "os"
 import { Effect } from "effect"
 import { FileSystem } from "@opencode-ai/core/filesystem"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { Location } from "@opencode-ai/core/location"
 import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
 import { effectCmd } from "../../effect-cmd"
@@ -9,8 +9,8 @@ import { cmd } from "../cmd"
 
 const filesystem = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
   effect.pipe(
-    Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(process.cwd()) }))),
-    Effect.provide(LocationServiceMap.layer),
+    Effect.provide(LocationServiceMap.Service.get(Location.Ref.make({ directory: AbsolutePath.make(process.cwd()) }))),
+    Effect.provide(locationServiceMapLayer),
   )
 
 const FileSearchCommand = effectCmd({

+ 3 - 3
packages/opencode/src/cli/cmd/debug/v2.ts

@@ -1,7 +1,7 @@
 import { EOL } from "os"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { Location } from "@opencode-ai/core/location"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { effectCmd } from "../../effect-cmd"
@@ -31,12 +31,12 @@ export const V2Command = effectCmd({
     }).pipe(
       Effect.withSpan("Cli.debug.v2"),
       Effect.provide(
-        LocationServiceMap.get(
+        LocationServiceMap.Service.get(
           Location.Ref.make({
             directory: AbsolutePath.make(process.cwd()),
           }),
         ),
       ),
-      Effect.provide(LocationServiceMap.layer),
+      Effect.provide(locationServiceMapLayer),
     ),
 })

+ 3 - 3
packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts

@@ -1,6 +1,6 @@
 import * as InstanceState from "@/effect/instance-state"
 import { FileSystem } from "@opencode-ai/core/filesystem"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { Ripgrep } from "@opencode-ai/core/ripgrep"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { Location } from "@opencode-ai/core/location"
@@ -14,7 +14,7 @@ import { InstanceHttpApi } from "../api"
 export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handlers) =>
   Effect.gen(function* () {
     const ripgrep = yield* Ripgrep.Service
-    const locations = yield* LocationServiceMap
+    const locations = yield* LocationServiceMap.Service
 
     const filesystem = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
       return yield* effect.pipe(
@@ -136,4 +136,4 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
       .handle("content", content)
       .handle("status", status)
   }),
-).pipe(Layer.provide(LocationServiceMap.layer))
+).pipe(Layer.provide(locationServiceMapLayer))

+ 5 - 5
packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts

@@ -6,7 +6,7 @@ import { Pty } from "@opencode-ai/core/pty"
 import { PtyProtocol } from "@opencode-ai/core/pty/protocol"
 import { PtyID } from "@opencode-ai/core/pty/schema"
 import { PtyTicket } from "@opencode-ai/core/pty/ticket"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { Location } from "@opencode-ai/core/location"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { Shell } from "@opencode-ai/core/shell"
@@ -43,7 +43,7 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler
     const tickets = yield* PtyTicket.Service
     const cors = yield* CorsConfig
     const plugin = yield* Plugin.Service
-    const locations = yield* LocationServiceMap
+    const locations = yield* LocationServiceMap.Service
     const unregister = registerDisposer((directory) =>
       Effect.runPromise(locations.invalidate(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
     )
@@ -158,13 +158,13 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler
       .handle("remove", remove)
       .handle("connectToken", connectToken)
   }),
-).pipe(Layer.provide(LocationServiceMap.layer))
+).pipe(Layer.provide(locationServiceMapLayer))
 
 export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-connect", (handlers) =>
   Effect.gen(function* () {
     const tickets = yield* PtyTicket.Service
     const cors = yield* CorsConfig
-    const locations = yield* LocationServiceMap
+    const locations = yield* LocationServiceMap.Service
     const unregister = registerDisposer((directory) =>
       Effect.runPromise(locations.invalidate(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
     )
@@ -270,4 +270,4 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne
       }),
     )
   }),
-).pipe(Layer.provide(LocationServiceMap.layer))
+).pipe(Layer.provide(locationServiceMapLayer))

+ 22 - 1
packages/opencode/src/server/routes/instance/httpapi/server.ts

@@ -52,15 +52,19 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
 import { MoveSession } from "@opencode-ai/core/control-plane/move-session"
 import { Database } from "@opencode-ai/core/database/database"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { LayerNodeTree } from "@opencode-ai/core/effect/layer-node"
 import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
 import { EventV2 } from "@opencode-ai/core/event"
 import { ModelsDev } from "@opencode-ai/core/models-dev"
 import { Npm } from "@opencode-ai/core/npm"
+import { PermissionSaved } from "@opencode-ai/core/permission/saved"
 import { ProjectV2 } from "@opencode-ai/core/project"
 import { ProjectCopy } from "@opencode-ai/core/project/copy"
 import { PtyTicket } from "@opencode-ai/core/pty/ticket"
 import { Ripgrep } from "@opencode-ai/core/ripgrep"
 import { SessionProjector } from "@opencode-ai/core/session/projector"
+import { SessionV2 } from "@opencode-ai/core/session"
+import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local"
 import { lazy } from "@/util/lazy"
 import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@opencode-ai/server/cors"
 import { serveUIEffect } from "@/server/shared/ui"
@@ -95,6 +99,10 @@ import { sessionHandlers } from "./handlers/session"
 import { syncHandlers } from "./handlers/sync"
 import { tuiHandlers } from "./handlers/tui"
 import { handlers } from "@opencode-ai/server/handlers"
+import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
+import { layer as locationLayer } from "@opencode-ai/server/location"
+import { sessionLocationLayer } from "@opencode-ai/server/middleware/session-location"
+import { PtyEnvironment } from "@opencode-ai/server/pty-environment"
 import { schemaErrorLayer as v2SchemaErrorLayer } from "@opencode-ai/server/middleware/schema-error"
 import { workspaceHandlers } from "./handlers/workspace"
 import { instanceContextLayer } from "./middleware/instance-context"
@@ -221,6 +229,7 @@ const app = LayerNode.group([
   Discovery.node,
   Question.node,
   Permission.node,
+  PermissionSaved.node,
   Todo.node,
   Session.node,
   SessionProjector.node,
@@ -279,9 +288,21 @@ export function createRoutes(
       MoveSession.defaultLayer,
       HttpServer.layerServices,
     ]),
-    Layer.provide(LayerNode.buildLayer(app)),
     Layer.provide(Layer.succeed(CorsConfig)(corsOptions)),
     Layer.provideMerge(Observability.layer),
+
+    Layer.provide(sessionLocationLayer),
+    Layer.provide(locationLayer),
+    Layer.provide(PtyEnvironment.layer),
+    Layer.provide(
+      SessionV2.defaultLayer.pipe(
+        Layer.provide(SessionExecutionLocal.defaultLayer),
+        Layer.provide(locationServiceMapLayer),
+      ),
+    ),
+    Layer.provide(locationServiceMapLayer),
+
+    Layer.provide(LayerNodeTree.compile(app)),
   )
 }
 

+ 1 - 2
packages/opencode/src/session/message-v2.ts

@@ -36,8 +36,6 @@ import type { SystemError } from "bun"
 import type { Provider } from "@/provider/provider"
 import { Effect, Schema } from "effect"
 
-export const node = LayerNode.group([Database.node])
-
 /** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
 interface FetchDecompressionError extends Error {
   code: "ZlibError"
@@ -733,3 +731,4 @@ export function fromError(
 }
 
 export * as MessageV2 from "./message-v2"
+export const node = LayerNode.group([Database.node])

+ 2 - 2
packages/opencode/src/session/session.ts

@@ -13,7 +13,7 @@ import { makeRuntime } from "@opencode-ai/core/effect/runtime"
 import { EventV2Bridge } from "@/event-v2-bridge"
 import { SessionV2 } from "@opencode-ai/core/session"
 import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
 
 import { NotFoundError } from "@/storage/storage"
 import { eq } from "drizzle-orm"
@@ -947,7 +947,7 @@ export const defaultLayer = layer.pipe(
   Layer.provide(
     SessionV2.defaultLayer.pipe(
       Layer.provide(SessionExecutionLocal.defaultLayer),
-      Layer.provide(LocationServiceMap.layer),
+      Layer.provide(locationServiceMapLayer),
     ),
   ),
   Layer.provide(RuntimeFlags.defaultLayer),

+ 4 - 4
packages/opencode/src/session/system.ts

@@ -18,7 +18,7 @@ import { Permission } from "@/permission"
 import { Skill } from "@/skill"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { Location } from "@opencode-ai/core/location"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { Reference } from "@opencode-ai/core/reference"
 import { MCP } from "@/mcp"
 import { PermissionV1 } from "@opencode-ai/core/v1/permission"
@@ -52,7 +52,7 @@ export const layer = Layer.effect(
   Effect.gen(function* () {
     const skill = yield* Skill.Service
     const mcp = yield* MCP.Service
-    const locations = yield* LocationServiceMap
+    const locations = yield* LocationServiceMap.Service
 
     return Service.of({
       environment: Effect.fn("SystemPrompt.environment")(function* (model: Provider.Model) {
@@ -131,10 +131,10 @@ export const layer = Layer.effect(
 export const defaultLayer = layer.pipe(
   Layer.provide(Skill.defaultLayer),
   Layer.provide(MCP.defaultLayer),
-  Layer.provide(LocationServiceMap.layer),
+  Layer.provide(locationServiceMapLayer),
 )
 
-const locationServiceMapNode = LayerNode.make({ service: Service, layer: LocationServiceMap.layer, deps: [] })
+const locationServiceMapNode = LayerNode.make({ service: LocationServiceMap.Service, layer: locationServiceMapLayer, deps: [] })
 
 export const node = LayerNode.make({
   service: Service,

+ 2 - 2
packages/opencode/test/agent/agent.test.ts

@@ -14,7 +14,7 @@ import { Plugin } from "../../src/plugin"
 import { Provider } from "../../src/provider/provider"
 import { Skill } from "../../src/skill"
 import { Truncate } from "../../src/tool/truncate"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
 
 const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
   Agent.layer.pipe(
@@ -23,7 +23,7 @@ const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
     Layer.provide(Auth.defaultLayer),
     Layer.provide(Config.defaultLayer),
     Layer.provide(Skill.defaultLayer),
-    Layer.provide(LocationServiceMap.layer),
+    Layer.provide(locationServiceMapLayer),
     Layer.provide(RuntimeFlags.layer(flags)),
   )
 

+ 2 - 2
packages/opencode/test/agent/plugin-agent-regression.test.ts

@@ -1,6 +1,6 @@
 import { expect } from "bun:test"
 import { FSUtil } from "@opencode-ai/core/fs-util"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
 import { Effect, Layer } from "effect"
 import { FetchHttpClient } from "effect/unstable/http"
 import path from "path"
@@ -44,7 +44,7 @@ const agentLayer = Agent.layer.pipe(
   Layer.provide(SkillTest.empty),
   Layer.provide(provider.layer),
   Layer.provide(pluginLayer),
-  Layer.provide(LocationServiceMap.layer),
+  Layer.provide(locationServiceMapLayer),
   Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
 )
 

+ 0 - 19
packages/opencode/test/effect/layer-node-tiers-types.test.ts

@@ -1,19 +0,0 @@
-import { test } from "bun:test"
-import { Context, Effect, Layer } from "effect"
-import { LayerNode } from "@opencode-ai/core/effect/layer-node"
-
-class A extends Context.Service<A, {}>()("test/TierA") {}
-class B extends Context.Service<B, {}>()("test/TierB") {}
-
-const tiers = LayerNode.tiers(["request", "global"])
-const request = tiers.make("request")
-const global = tiers.make("global")
-const globalA = global({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })
-const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
-
-request({ service: B, layer: bLayer, deps: [globalA] })
-
-// @ts-expect-error Global cannot depend on request
-global({ service: B, layer: bLayer, deps: [request({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })] })
-
-test("type exploration compiles", () => {})

+ 0 - 169
packages/opencode/test/effect/layer-node-tiers.test.ts

@@ -1,169 +0,0 @@
-import { expect, test } from "bun:test"
-import { Context, Effect, Layer } from "effect"
-import { LayerNode } from "@opencode-ai/core/effect/layer-node"
-
-class Value extends Context.Service<Value, { readonly value: string }>()("test/TierValue") {}
-class Result extends Context.Service<Result, { readonly value: string }>()("test/TierResult") {}
-class Left extends Context.Service<Left, { readonly value: string }>()("test/TierLeft") {}
-class Right extends Context.Service<Right, { readonly value: string }>()("test/TierRight") {}
-class Last extends Context.Service<Last, { readonly value: string }>()("test/TierLast") {}
-
-test("builds tiers with a custom builder", async () => {
-  let locationBuilds = 0
-  const tiers = LayerNode.tiers(["location", "global"])
-  const global = tiers.make("global")
-  const location = tiers.make("location")
-  const value = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "value" })), deps: [] })
-  const result = location({
-    service: Result,
-    layer: Layer.effect(
-      Result,
-      Effect.gen(function* () {
-        return Result.of({ value: (yield* Value).value })
-      }),
-    ),
-    deps: [value],
-  })
-  const layer = LayerNode.buildLayer(LayerNode.group([result]), {
-    tiers,
-    buildTier: (tier, layers) => {
-      if (tier !== "location") return LayerNode.combine(layers)
-      locationBuilds++
-      return LayerNode.combine(layers).pipe(Layer.fresh)
-    },
-  })
-  const program = Effect.gen(function* () {
-    return (yield* Result).value
-  }).pipe(Effect.provide(layer))
-
-  expect(await Effect.runPromise(program)).toBe("value")
-  expect(locationBuilds).toBe(1)
-})
-
-test("rejects conflicting higher-tier service implementations", () => {
-  const tiers = LayerNode.tiers(["location", "global"])
-  const global = tiers.make("global")
-  const location = tiers.make("location")
-  const first = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "first" })), deps: [] })
-  const second = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
-  const left = location({
-    service: Left,
-    layer: Layer.effect(Left, Effect.as(Value, Left.of({ value: "left" }))),
-    deps: [first],
-  })
-  const right = location({
-    service: Right,
-    layer: Layer.effect(Right, Effect.as(Value, Right.of({ value: "right" }))),
-    deps: [second],
-  })
-
-  expect(() => LayerNode.buildLayer(LayerNode.group([left, right]), { tiers })).toThrow(
-    "conflicting implementations for test/TierValue",
-  )
-})
-
-test("validates tier dependencies through groups", () => {
-  const tiers = LayerNode.tiers(["location", "global"])
-  const global = tiers.make("global")
-  const location = tiers.make("location")
-  const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
-  const invalid = global({
-    service: Result,
-    layer: Layer.effect(
-      Result,
-      Effect.map(Value, (value) => Result.of({ value: value.value })),
-    ),
-    deps: [LayerNode.group([local])],
-  })
-
-  expect(() => LayerNode.buildLayer(invalid, { tiers })).toThrow("Tier global cannot depend on lower tier location")
-})
-
-test("validates shared groups in each consumer tier", () => {
-  const tiers = LayerNode.tiers(["location", "global"])
-  const global = tiers.make("global")
-  const location = tiers.make("location")
-  const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
-  const shared = LayerNode.group([local])
-  const valid = location({
-    service: Left,
-    layer: Layer.effect(
-      Left,
-      Effect.map(Value, (value) => Left.of({ value: value.value })),
-    ),
-    deps: [shared],
-  })
-  const invalid = global({
-    service: Result,
-    layer: Layer.effect(
-      Result,
-      Effect.map(Value, (value) => Result.of({ value: value.value })),
-    ),
-    deps: [shared],
-  })
-
-  expect(() => LayerNode.buildLayer(LayerNode.group([valid, invalid]), { tiers })).toThrow(
-    "Tier global cannot depend on lower tier location",
-  )
-})
-
-test("rejects a service assigned to multiple tiers", () => {
-  const tiers = LayerNode.tiers(["location", "global"])
-  const global = tiers.make("global")
-  const location = tiers.make("location")
-  const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
-  const shared = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "global" })), deps: [] })
-
-  expect(() => LayerNode.buildLayer(LayerNode.group([local, shared]), { tiers })).toThrow(
-    "Service test/TierValue belongs to both tier location and tier global",
-  )
-})
-
-test("rebinds same-tier providers without reacquiring them", async () => {
-  let firstAcquisitions = 0
-  const tiers = LayerNode.tiers(["global"])
-  const global = tiers.make("global")
-  const first = global({
-    service: Value,
-    layer: Layer.effect(
-      Value,
-      Effect.sync(() => {
-        firstAcquisitions++
-        return Value.of({ value: "first" })
-      }),
-    ),
-    deps: [],
-  })
-  const second = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
-  const left = global({
-    service: Left,
-    layer: Layer.effect(
-      Left,
-      Effect.map(Value, (value) => Left.of({ value: value.value })),
-    ),
-    deps: [first],
-  })
-  const right = global({
-    service: Right,
-    layer: Layer.effect(
-      Right,
-      Effect.map(Value, (value) => Right.of({ value: value.value })),
-    ),
-    deps: [second],
-  })
-  const last = global({
-    service: Last,
-    layer: Layer.effect(
-      Last,
-      Effect.map(Value, (value) => Last.of({ value: value.value })),
-    ),
-    deps: [first],
-  })
-  const layer = LayerNode.buildLayer(LayerNode.group([left, right, last]), { tiers })
-  const values = Effect.gen(function* () {
-    return [(yield* Left).value, (yield* Right).value, (yield* Last).value]
-  }).pipe(Effect.provide(layer))
-
-  expect(await Effect.runPromise(values)).toEqual(["first", "second", "first"])
-  expect(firstAcquisitions).toBe(1)
-})

+ 0 - 66
packages/opencode/test/effect/layer-node-types.test.ts

@@ -1,66 +0,0 @@
-import { test } from "bun:test"
-import { Context, Effect, Layer } from "effect"
-import { LayerNode } from "@opencode-ai/core/effect/layer-node"
-
-class A extends Context.Service<A, {}>()("test/LayerNodeA") {}
-class B extends Context.Service<B, {}>()("test/LayerNodeB") {}
-class C extends Context.Service<C, {}>()("test/LayerNodeC") {}
-class LayerError {
-  readonly _tag = "LayerError"
-}
-class OtherError {
-  readonly _tag = "OtherError"
-}
-
-const tiers = LayerNode.tiers(["app"])
-const make = tiers.make("app")
-const aLayer = Layer.succeed(A, A.of({}))
-const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
-const cLayer = Layer.effect(
-  C,
-  Effect.gen(function* () {
-    yield* A
-    yield* B
-    return C.of({})
-  }),
-)
-const failingA = Layer.effect(A, Effect.fail(new LayerError()))
-const a = make({ service: A, layer: aLayer, deps: [] })
-const b = make({ service: B, layer: bLayer, deps: [a] })
-const c = make({ service: C, layer: cLayer, deps: [a, b] })
-const failing = make({ service: A, layer: failingA, deps: [] })
-const dependent = make({ service: B, layer: bLayer, deps: [failing] })
-
-make({ name: "manual-a", layer: aLayer, deps: [] })
-
-// @ts-expect-error A node must have a service or name
-make({ layer: aLayer, deps: [] })
-
-// @ts-expect-error Service and name are mutually exclusive
-make({ service: A, name: "a", layer: aLayer, deps: [] })
-
-// @ts-expect-error B requires A
-make({ service: B, layer: bLayer, deps: [] })
-
-// @ts-expect-error C requires A and B
-make({ service: C, layer: cLayer, deps: [a] })
-
-const closed = LayerNode.buildLayer(c, { tiers })
-const closedWithError = LayerNode.buildLayer(dependent, { tiers })
-const checkClosed: Layer.Layer<C, never, never> = closed
-const checkError: Layer.Layer<B, LayerError, never> = closedWithError
-void checkClosed
-void checkError
-
-LayerNode.replace(aLayer, Layer.succeed(A, A.of({})))
-
-// @ts-expect-error Replacement must provide A
-LayerNode.replace(aLayer, Layer.succeed(B, B.of({})))
-
-// @ts-expect-error Replacement cannot introduce a new error
-LayerNode.replace(aLayer, Layer.effect(A, Effect.fail(new OtherError())))
-
-// @ts-expect-error Replacement must be closed
-LayerNode.replace(bLayer, bLayer)
-
-test("type exploration compiles", () => {})

+ 0 - 86
packages/opencode/test/effect/layer-node.test.ts

@@ -1,86 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { Context, Effect, Layer } from "effect"
-import { LayerNode } from "@opencode-ai/core/effect/layer-node"
-
-class Value extends Context.Service<Value, { readonly value: string }>()("test/LayerNodeValue") {}
-class Greeting extends Context.Service<Greeting, { readonly value: string }>()("test/LayerNodeGreeting") {}
-class Left extends Context.Service<Left, { readonly value: string }>()("test/LayerNodeLeft") {}
-class Right extends Context.Service<Right, { readonly value: string }>()("test/LayerNodeRight") {}
-
-const tiers = LayerNode.tiers(["app"])
-const make = tiers.make("app")
-const valueLayer = Layer.succeed(Value, Value.of({ value: "production" }))
-const greetingLayer = Layer.effect(
-  Greeting,
-  Effect.map(Value, (value) => Greeting.of({ value: `hello ${value.value}` })),
-)
-const value = make({ service: Value, layer: valueLayer, deps: [] })
-const greeting = make({ service: Greeting, layer: greetingLayer, deps: [value] })
-
-describe("layer node", () => {
-  test("builds an untiered graph", async () => {
-    const value = LayerNode.make({ service: Value, layer: valueLayer, deps: [] })
-    const greeting = LayerNode.make({ service: Greeting, layer: greetingLayer, deps: [value] })
-    const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(LayerNode.buildLayer(greeting)))
-    expect(await Effect.runPromise(program)).toBe("hello production")
-  })
-
-  test("builds a dependency graph", async () => {
-    const program = Effect.map(Greeting, (item) => item.value).pipe(
-      Effect.provide(LayerNode.buildLayer(greeting, { tiers })),
-    )
-    expect(await Effect.runPromise(program)).toBe("hello production")
-  })
-
-  test("replaces a layer by identity", async () => {
-    const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
-    const program = Effect.map(Greeting, (item) => item.value).pipe(
-      Effect.provide(
-        LayerNode.buildLayer(greeting, { tiers, replacements: [LayerNode.replace(valueLayer, replacement)] }),
-      ),
-    )
-    expect(await Effect.runPromise(program)).toBe("hello simulation")
-  })
-
-  test("replaces every use of the same layer", async () => {
-    const leftLayer = Layer.effect(
-      Left,
-      Effect.map(Value, (item) => Left.of({ value: item.value })),
-    )
-    const rightLayer = Layer.effect(
-      Right,
-      Effect.map(Value, (item) => Right.of({ value: item.value })),
-    )
-    const left = make({ service: Left, layer: leftLayer, deps: [value] })
-    const right = make({ service: Right, layer: rightLayer, deps: [value] })
-    const replacement = Layer.succeed(Value, Value.of({ value: "replaced" }))
-    const layer = LayerNode.buildLayer(LayerNode.group([left, right]), {
-      tiers,
-      replacements: [LayerNode.replace(valueLayer, replacement)],
-    })
-    const program = Effect.gen(function* () {
-      return [(yield* Left).value, (yield* Right).value]
-    }).pipe(Effect.provide(layer))
-    expect(await Effect.runPromise(program)).toEqual(["replaced", "replaced"])
-  })
-
-  test("does not acquire an unused replacement", async () => {
-    let acquisitions = 0
-    const other = Layer.succeed(Value, Value.of({ value: "other" }))
-    const replacement = Layer.effect(
-      Value,
-      Effect.sync(() => {
-        acquisitions++
-        return Value.of({ value: "replacement" })
-      }),
-    )
-    await Effect.runPromise(
-      Effect.map(Greeting, (item) => item.value).pipe(
-        Effect.provide(
-          LayerNode.buildLayer(greeting, { tiers, replacements: [LayerNode.replace(other, replacement)] }),
-        ),
-      ),
-    )
-    expect(acquisitions).toBe(0)
-  })
-})

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно