fix(core): remove session import cycle (#37596)

Co-authored-by: James Long <17031+jlongster@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-07-18 13:24:47 +00:00
committed by GitHub
co-authored by James Long
parent deb5b144c3
commit 529d55b1c3
6 changed files with 22 additions and 29 deletions
+2 -4
View File
@@ -74,7 +74,7 @@ type MakeInput<
T extends Tag | undefined = undefined, T extends Tag | undefined = undefined,
> = NodeIdentity & { > = NodeIdentity & {
readonly layer: Implementation readonly layer: Implementation
readonly deps: (Items | (() => Items)) & CheckDependencies<Implementation, NoInfer<Items>> readonly deps: Items & CheckDependencies<Implementation, NoInfer<Items>>
readonly tag?: T readonly tag?: T
} }
@@ -90,9 +90,7 @@ export function make<
name: input.service !== undefined ? input.service.key : input.name, name: input.service !== undefined ? input.service.key : input.name,
service: input.service, service: input.service,
implementation: input.layer, implementation: input.layer,
get dependencies() { dependencies: input.deps,
return typeof input.deps === "function" ? input.deps() : input.deps
},
tag: input.tag, tag: input.tag,
} }
} }
+11 -7
View File
@@ -6,7 +6,8 @@ import { Permission } from "@opencode-ai/schema/permission"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { Location } from "./location" import { Location } from "./location"
import { AgentV2 } from "./agent" import { AgentV2 } from "./agent"
import { SessionV2 } from "./session" import { SessionErrors } from "./session/error"
import { SessionSchema } from "./session/schema"
import { SessionStore } from "./session/store" import { SessionStore } from "./session/store"
import { Wildcard } from "./util/wildcard" import { Wildcard } from "./util/wildcard"
import { PermissionSaved } from "./permission/saved" import { PermissionSaved } from "./permission/saved"
@@ -98,11 +99,11 @@ export function merge(...rulesets: Permission.Ruleset[]): Permission.Ruleset {
} }
export interface Interface { export interface Interface {
readonly ask: (input: AssertInput) => Effect.Effect<AskResult, SessionV2.NotFoundError> readonly ask: (input: AssertInput) => Effect.Effect<AskResult, SessionErrors.NotFoundError>
readonly assert: (input: AssertInput) => Effect.Effect<void, Error | SessionV2.NotFoundError> readonly assert: (input: AssertInput) => Effect.Effect<void, Error | SessionErrors.NotFoundError>
readonly reply: (input: ReplyInput) => Effect.Effect<void, NotFoundError> readonly reply: (input: ReplyInput) => Effect.Effect<void, NotFoundError>
readonly get: (id: ID) => Effect.Effect<Request | undefined> readonly get: (id: ID) => Effect.Effect<Request | undefined>
readonly forSession: (sessionID: SessionV2.ID) => Effect.Effect<ReadonlyArray<Request>> readonly forSession: (sessionID: SessionSchema.ID) => Effect.Effect<ReadonlyArray<Request>>
readonly list: () => Effect.Effect<ReadonlyArray<Request>> readonly list: () => Effect.Effect<ReadonlyArray<Request>>
} }
@@ -142,9 +143,12 @@ const layer = Layer.effect(
) )
}) })
const configured = Effect.fn("PermissionV2.configured")(function* (sessionID: SessionV2.ID, agentID?: AgentV2.ID) { const configured = Effect.fn("PermissionV2.configured")(function* (
sessionID: SessionSchema.ID,
agentID?: AgentV2.ID,
) {
const session = yield* sessions.get(sessionID) const session = yield* sessions.get(sessionID)
if (!session) return yield* new SessionV2.NotFoundError({ sessionID }) if (!session) return yield* new SessionErrors.NotFoundError({ sessionID })
const agent = yield* agents.resolve(agentID ?? session.agent) const agent = yield* agents.resolve(agentID ?? session.agent)
return agent?.permissions ?? missingAgentPermissions return agent?.permissions ?? missingAgentPermissions
}) })
@@ -301,7 +305,7 @@ const layer = Layer.effect(
return pending.get(id)?.request return pending.get(id)?.request
}) })
const forSession = Effect.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) { const forSession = Effect.fn("PermissionV2.forSession")(function* (sessionID: SessionSchema.ID) {
return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID) return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID)
}) })
+3 -8
View File
@@ -28,9 +28,9 @@ import { fromRow } from "./session/info"
import { SessionRunner } from "./session/runner/index" import { SessionRunner } from "./session/runner/index"
import { SessionStore } from "./session/store" import { SessionStore } from "./session/store"
import { SessionExecution } from "./session/execution" import { SessionExecution } from "./session/execution"
import { MessageDecodeError, NotFoundError } from "./session/error"
import { makeGlobalNode } from "./effect/app-node" import { makeGlobalNode } from "./effect/app-node"
import { LocationServiceMap } from "./location-service-map" import { LocationServiceMap } from "./location-service-map"
import { MessageDecodeError } from "./session/error"
import { SessionEvent } from "./session/event" import { SessionEvent } from "./session/event"
import { SessionPending } from "./session/pending" import { SessionPending } from "./session/pending"
import { SessionGenerate } from "./session/generate" import { SessionGenerate } from "./session/generate"
@@ -108,10 +108,6 @@ type ForkInput = {
messageID?: SessionMessage.ID messageID?: SessionMessage.ID
} }
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Session.NotFoundError", {
sessionID: SessionSchema.ID,
}) {}
export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()( export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()(
"Session.OperationUnavailableError", "Session.OperationUnavailableError",
{ {
@@ -119,7 +115,7 @@ export class OperationUnavailableError extends Schema.TaggedErrorClass<Operation
}, },
) {} ) {}
export { MessageDecodeError } from "./session/error" export { MessageDecodeError, NotFoundError }
export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictError>()("Session.PromptConflictError", { export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictError>()("Session.PromptConflictError", {
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
@@ -1033,8 +1029,7 @@ const SHELL_MAX_CAPTURE_BYTES = 1024 * 1024
export const node = makeGlobalNode({ export const node = makeGlobalNode({
service: Service, service: Service,
layer: layer.pipe(Layer.orDie), layer: layer.pipe(Layer.orDie),
// Defer the execution node across the Session/runner module cycle until the graph is compiled. deps: [
deps: () => [
Job.node, Job.node,
Database.node, Database.node,
EventV2.node, EventV2.node,
+6
View File
@@ -1,9 +1,15 @@
export * as SessionErrors from "./error"
import { Schema } from "effect" import { Schema } from "effect"
import { Agent } from "@opencode-ai/schema/agent" import { Agent } from "@opencode-ai/schema/agent"
import { SessionMessage } from "./message" import { SessionMessage } from "./message"
import { SessionSchema } from "./schema" import { SessionSchema } from "./schema"
import { SessionError } from "@opencode-ai/schema/session-error" import { SessionError } from "@opencode-ai/schema/session-error"
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Session.NotFoundError", {
sessionID: SessionSchema.ID,
}) {}
export class MessageDecodeError extends Schema.TaggedErrorClass<MessageDecodeError>()("Session.MessageDecodeError", { export class MessageDecodeError extends Schema.TaggedErrorClass<MessageDecodeError>()("Session.MessageDecodeError", {
sessionID: SessionSchema.ID, sessionID: SessionSchema.ID,
messageID: SessionMessage.ID, messageID: SessionMessage.ID,
@@ -32,7 +32,6 @@ const b = make({ service: B, layer: bLayer, deps: [a] })
const c = make({ service: C, layer: cLayer, deps: [a, b] }) const c = make({ service: C, layer: cLayer, deps: [a, b] })
const failing = make({ service: A, layer: failingA, deps: [] }) const failing = make({ service: A, layer: failingA, deps: [] })
const dependent = make({ service: B, layer: bLayer, deps: [failing] }) const dependent = make({ service: B, layer: bLayer, deps: [failing] })
make({ service: B, layer: bLayer, deps: () => [a] })
const inputA = LayerNode.unbound(A, tags.values.app) const inputA = LayerNode.unbound(A, tags.values.app)
const inputDependent = make({ service: B, layer: bLayer, deps: [inputA] }) const inputDependent = make({ service: B, layer: bLayer, deps: [inputA] })
@@ -47,9 +46,6 @@ make({ service: A, name: "a", layer: aLayer, deps: [] })
// @ts-expect-error B requires A // @ts-expect-error B requires A
make({ service: B, layer: bLayer, deps: [] }) make({ service: B, layer: bLayer, deps: [] })
// @ts-expect-error Lazy dependencies must still provide A
make({ service: B, layer: bLayer, deps: () => [] })
// @ts-expect-error C requires A and B // @ts-expect-error C requires A and B
make({ service: C, layer: cLayer, deps: [a] }) make({ service: C, layer: cLayer, deps: [a] })
@@ -37,12 +37,6 @@ describe("layer node", () => {
expect(await Effect.runPromise(program)).toBe("hello production") expect(await Effect.runPromise(program)).toBe("hello production")
}) })
test("resolves lazy dependencies when compiling", async () => {
const greeting = make({ service: Greeting, layer: greetingLayer, deps: () => [value] })
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(build(greeting)))
expect(await Effect.runPromise(program)).toBe("hello production")
})
test("exposes roots but hides transitive dependencies", () => { test("exposes roots but hides transitive dependencies", () => {
const layer = build(LayerNode.group([greeting])) const layer = build(LayerNode.group([greeting]))
const check: Layer.Layer<Greeting> = layer const check: Layer.Layer<Greeting> = layer