Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 51 additions & 45 deletions packages/client/src/promise/generated/client.ts

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions packages/client/src/promise/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2531,6 +2531,14 @@ export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly m
export const isUnauthorizedError = (value: unknown): value is UnauthorizedError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError"

export type LocationNotFoundError = {
readonly _tag: "LocationNotFoundError"
readonly location: { readonly directory: string }
readonly message: string
}
export const isLocationNotFoundError = (value: unknown): value is LocationNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "LocationNotFoundError"

export type ServiceUnavailableError = {
readonly _tag: "ServiceUnavailableError"
readonly message: string
Expand Down
18 changes: 14 additions & 4 deletions packages/core/src/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const nodes = [
export const graph = LayerNode.group(nodes)

export type Services = LayerNode.Output<typeof graph>
export type Error = Layer.Error<ReturnType<typeof layer>>
export type Error = FileSystem.DirectoryNotFoundError

export interface Options {
// Plugins this instance is born with; empty and absent are equivalent.
Expand Down Expand Up @@ -145,7 +145,7 @@ const vanillaReplacements: LayerNode.Replacements = [
]

// One instance is one compiled, fresh copy of the graph standing on a directory.
export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Services> {
export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Services, Error> {
const startedAt = performance.now()
// Ordered: vanilla defaults, then caller replacements (which win over the
// defaults), then instance bindings (which win over everything).
Expand All @@ -157,8 +157,18 @@ export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Ser
]

return LayerNode.compile(graph, { replacements, shared: Node.tags.values.global }).pipe(
// Instance boot failures are defects; provided operations retain their typed errors.
Layer.orDie,
// A missing directory is expected; other instance boot failures remain defects.
Layer.catchCause(
(cause): Layer.Layer<Services, Error> =>
Layer.unwrap(
Effect.failCause(cause).pipe(
Effect.catch(
(error): Effect.Effect<never, Error> =>
error instanceof FileSystem.DirectoryNotFoundError ? Effect.fail(error) : Effect.die(error),
),
),
),
),
Layer.tap((context) => Effect.addFinalizer(() => Context.get(context, LocationLifecycle.Service).shutdown)),
Layer.tap(() =>
Effect.logInfo("location services booted", {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/instance/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { Context, type Effect } from "effect"
import type { Session } from "@opencode/schema/session"
import { Node } from "@opencode/util/effect/app-node"
import { LayerNode } from "@opencode/util/effect/layer-node"
import type { Services } from "../instance.js"
import type { Services, Error } from "../instance.js"

/** Selects Session capabilities; implementations own caching and lifetime. */
export interface Interface {
readonly provide: (
session: Session.Info,
) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Services>>
) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | Error, Exclude<R, Services>>
}

export class Service extends Context.Service<Service, Interface>()("@opencode/Instance") {}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/location-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function buildLocationServiceMap(
Effect.gen(function* () {
const owner = yield* Effect.scope
const builds = MutableHashMap.empty<Location.Ref, { close?: Effect.Effect<void> }>()
const inner: LayerMap.LayerMap<Location.Ref, LocationServices> = yield* LayerMap.make(
const inner: LayerMap.LayerMap<Location.Ref, LocationServices, Instance.Error> = yield* LayerMap.make(
(ref: Location.Ref) => {
const build: { close?: Effect.Effect<void> } = {}
MutableHashMap.set(builds, ref, build)
Expand Down
20 changes: 16 additions & 4 deletions packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { and, desc, eq } from "drizzle-orm"
import { Project } from "./project.js"
import { Model } from "@opencode/schema/model"
import { Location } from "./location.js"
import type { FileSystem } from "./filesystem.js"
import { SessionMessage } from "./session/message.js"
import { PromptInput } from "@opencode/schema/prompt-input"
import { Bus } from "./bus.js"
Expand Down Expand Up @@ -145,7 +146,10 @@ export interface Interface {
readonly from?: SessionMessage.ID
readonly to?: SessionMessage.ID
readonly context?: number
}) => Effect.Effect<readonly FileDiff.Info[], NotFoundError | MessageNotFoundError | TurnRangeError | Snapshot.Error>
}) => Effect.Effect<
readonly FileDiff.Info[],
NotFoundError | MessageNotFoundError | TurnRangeError | Snapshot.Error | FileSystem.DirectoryNotFoundError
>
/**
* Durable admitted session work not yet visible in projected history,
* ordered by admission. Includes unpromoted user and synthetic inputs and
Expand Down Expand Up @@ -195,7 +199,10 @@ export interface Interface {
agents?: PromptInput.Prompt["agents"]
skills?: PromptInput.Prompt["skills"]
delivery?: SessionInbox.Delivery
}) => Effect.Effect<void, NotFoundError | Command.NotFoundError | Command.ExecutionError>
}) => Effect.Effect<
void,
NotFoundError | Command.NotFoundError | Command.ExecutionError | FileSystem.DirectoryNotFoundError
>
readonly shell: (
input: Parameters<Session.Handle["shell"]>[0] & { sessionID: SessionSchema.ID },
) => ReturnType<Session.Handle["shell"]>
Expand All @@ -218,8 +225,13 @@ export interface Interface {
sessionID: SessionSchema.ID
messageID: SessionMessage.ID
files?: boolean
}) => Effect.Effect<SessionSchema.Revert, NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error>
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error>
}) => Effect.Effect<
SessionSchema.Revert,
NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error | FileSystem.DirectoryNotFoundError
>
readonly clear: (
sessionID: SessionSchema.ID,
) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error | FileSystem.DirectoryNotFoundError>
readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError>
}
}
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/session/generate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * as SessionGenerate from "./generate.js"

import type { FileSystem } from "../filesystem.js"
import { LLMClient, Message, type AIError } from "@opencode/ai"
import { Effect } from "effect"
import { Database } from "../database/database.js"
Expand All @@ -14,7 +15,12 @@ import { SessionModelRequest } from "./model-request.js"
import type { SessionRunnerModel } from "./runner/model.js"
import type { SessionSchema } from "./schema.js"

export type Error = AgentNotFoundError | Instructions.InitializationBlocked | SessionRunnerModel.Error | AIError
export type Error =
| AgentNotFoundError
| Instructions.InitializationBlocked
| SessionRunnerModel.Error
| AIError
| FileSystem.DirectoryNotFoundError

/** Generates text from current Session context without mutating the Session. */
export const generate = Effect.fn("SessionGenerate.generate")(function* (input: {
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/session/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ const layer = Layer.effect(
yield* locations.contextEffect(destination.location).pipe(
Effect.scoped,
Effect.catchCause((cause) => {
if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause)
if (Cause.hasInterruptsOnly(cause))
return Effect.failCause(Cause.fromReasons<never>(cause.reasons.filter(Cause.isInterruptReason)))
return Effect.logWarning("session move destination unavailable", { directory, cause }).pipe(
Effect.andThen(Effect.fail(new DestinationUnavailableError({ directory }))),
)
Expand All @@ -106,7 +107,11 @@ const layer = Layer.effect(
return yield* SessionRunner.Service.pipe(
instances.provide(session),
Effect.as(false),
Effect.catchCause((cause) => (Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.succeed(true))),
Effect.catchCause((cause) =>
Cause.hasInterrupts(cause)
? Effect.failCause(Cause.fromReasons<never>(cause.reasons.filter(Cause.isInterruptReason)))
: Effect.succeed(true),
),
)
})

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/session/runner/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * as SessionRunner from "./index.js"

import type { FileSystem } from "../../filesystem.js"
import type { AIError } from "@opencode/ai"
import { Context, Data, Effect } from "effect"
import { SessionSchema } from "../schema.js"
Expand All @@ -15,6 +16,7 @@ export type RunError =
| AgentNotFoundError
| StepFailedError
| Instructions.InitializationBlocked
| FileSystem.DirectoryNotFoundError

export type Continuation = { readonly step: number }

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Slug } from "./util/slug.js"
import { Bus } from "./bus.js"
import { Database } from "./database/database.js"
import { Location } from "./location.js"
import type { FileSystem } from "./filesystem.js"
import { LocationServiceMap } from "./location-service-map.js"
import { Project } from "./project.js"
import { Worktree } from "@opencode/schema/worktree"
Expand Down Expand Up @@ -67,6 +68,7 @@ export class StrategyUnavailableError extends Schema.TaggedError<StrategyUnavail
) {}

export type Error =
| FileSystem.DirectoryNotFoundError
| Project.NotFoundError
| SourceDirectoryNotFoundError
| DestinationExistsError
Expand Down
3 changes: 2 additions & 1 deletion packages/core/test/fixture/prompt-location.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FileSystem } from "@opencode/core/filesystem"
import { Bus } from "@opencode/core/bus"
import { Image } from "@opencode/core/image"
import { LocationServiceMap } from "@opencode/core/location-service-map"
Expand All @@ -24,7 +25,7 @@ export const promptLocationNode = makeGlobalNode({
Bus.node.replace(Layer.succeed(Bus.Service, bus)),
Plugin.node.replace(Layer.mock(Plugin.Service, { awaitActivation: Effect.void })),
],
}) as Layer.Layer<LocationServices>,
}) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
)
}),
),
Expand Down
3 changes: 2 additions & 1 deletion packages/core/test/location-activity.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FileSystem } from "@opencode/core/filesystem"
import { describe, expect } from "bun:test"
import { Context, Deferred, Duration, Effect, Fiber, Layer, LayerMap, RcMap, Schema } from "effect"
import { TestClock } from "effect/testing"
Expand Down Expand Up @@ -65,7 +66,7 @@ const locations = Layer.effect(
Layer.provideMerge(Form.layer),
Layer.provide(Layer.succeed(Bus.Service, bus)),
Layer.fresh,
) as unknown as Layer.Layer<LocationServices>,
) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
{ idleTimeToLive: Duration.infinity },
)
return {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/test/location-layer.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FileSystem } from "@opencode/core/filesystem"
import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
Expand Down Expand Up @@ -67,7 +68,7 @@ const activityLocations = Layer.effect(
workspaceID: ref.workspaceID,
project: { id: Project.ID.global, directory: ref.directory, canonical: ref.directory },
}),
) as unknown as Layer.Layer<LocationServices>,
) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
{ idleTimeToLive: Duration.infinity },
),
)
Expand Down
3 changes: 2 additions & 1 deletion packages/core/test/session-compact.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FileSystem } from "@opencode/core/filesystem"
import { describe, expect } from "bun:test"
import { LLMClient, LLMEvent, LanguageModel, type LLMRequest } from "@opencode/ai"
import { OpenAIChat } from "@opencode/ai/protocols"
Expand Down Expand Up @@ -58,7 +59,7 @@ const locations = Layer.effect(
Layer.provide(client),
Layer.provide(config),
Layer.provide(models),
) as unknown as Layer.Layer<LocationServices>,
) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
),
)
const it = testEffect(
Expand Down
3 changes: 2 additions & 1 deletion packages/core/test/session-execution.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FileSystem } from "@opencode/core/filesystem"
import { describe, expect, test } from "bun:test"
import { AIError, TransportError } from "@opencode/ai"
import { Database } from "@opencode/core/database/database"
Expand Down Expand Up @@ -1371,7 +1372,7 @@ function buildExecution(
() =>
// The local execution test only needs the Session runner from the Location graph.
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion
runner as unknown as Layer.Layer<LocationServices>,
runner as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
),
)
return yield* Layer.buildWithScope(
Expand Down
9 changes: 7 additions & 2 deletions packages/core/test/session-move.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FileSystem } from "@opencode/core/filesystem"
import { describe, expect } from "bun:test"
import path from "path"
import { chmod, mkdir, readdir, rm } from "fs/promises"
Expand Down Expand Up @@ -58,7 +59,7 @@ const itWithActiveExecution = testEffect(
replacements: [Project.node.replace(globalProjectNode), offlineModels],
}),
Layer.succeed(SessionRunner.Service, { drain: () => Effect.never }),
) as unknown as Layer.Layer<LocationServices>,
) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
),
),
),
Expand All @@ -68,7 +69,11 @@ const itWithActiveExecution = testEffect(
const unavailableLocations = Layer.effect(
LocationServiceMap.Service,
LayerMap.make(
() => Layer.effectDiscard(Effect.fail(new Error("broken location"))) as unknown as Layer.Layer<LocationServices>,
() =>
Layer.effectDiscard(Effect.die(new Error("broken location"))) as unknown as Layer.Layer<
LocationServices,
FileSystem.DirectoryNotFoundError
>,
),
)
const itWithUnavailableDestination = testEffect(
Expand Down
3 changes: 2 additions & 1 deletion packages/core/test/session-prompt.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect } from "bun:test"
import type { FileSystem } from "@opencode/core/filesystem"
import { DateTime, Effect, Fiber, Layer, LayerMap, Schema, Stream } from "effect"
import path from "path"
import { pathToFileURL } from "url"
Expand Down Expand Up @@ -83,7 +84,7 @@ const locations = makeGlobalNode({
restore: () => Effect.void,
}),
Layer.mock(Plugin.Service, { awaitActivation: Effect.void }),
).pipe(Layer.fresh) as unknown as Layer.Layer<LocationServices>,
).pipe(Layer.fresh) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
)
}),
),
Expand Down
3 changes: 2 additions & 1 deletion packages/core/test/session-skill.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FileSystem } from "@opencode/core/filesystem"
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Layer, LayerMap } from "effect"
Expand Down Expand Up @@ -49,7 +50,7 @@ const locations = makeGlobalNode({
list: () => Effect.succeed([info]),
}),
Layer.mock(Plugin.Service, { awaitActivation: Effect.void }),
) as unknown as Layer.Layer<LocationServices>,
) as unknown as Layer.Layer<LocationServices, FileSystem.DirectoryNotFoundError>,
),
),
deps: [],
Expand Down
5 changes: 3 additions & 2 deletions packages/protocol/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InvalidRequestError, SessionNotFoundError } from "./errors.js"
import { LocationNotFoundError, InvalidRequestError, SessionNotFoundError } from "./errors.js"
import { makeDefaultApi } from "./api.js"
import type { Api } from "./api.js"
import type { Context } from "effect"
Expand All @@ -7,11 +7,12 @@ import type { EventGroup } from "./groups/event.js"

class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware>()(
"@opencode/client/LocationMiddleware",
{ error: [LocationNotFoundError] },
) {}

class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocationMiddleware>()(
"@opencode/client/SessionLocationMiddleware",
{ error: [InvalidRequestError, SessionNotFoundError] },
{ error: [InvalidRequestError, SessionNotFoundError, LocationNotFoundError] },
) {}

type ClientApiShape = Api<
Expand Down
7 changes: 7 additions & 0 deletions packages/protocol/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Schema } from "effect"
import { Skill } from "@opencode/schema/skill"
import { Location } from "@opencode/schema/location"

export class LocationNotFoundError extends Schema.TaggedError<LocationNotFoundError>()(
"LocationNotFoundError",
{ location: Location.PublicRef, message: Schema.String },
{ httpApiStatus: 404 },
) {}

export class InvalidRequestError extends Schema.TaggedError<InvalidRequestError>()(
"InvalidRequestError",
Expand Down
4 changes: 2 additions & 2 deletions packages/protocol/src/groups/generate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Model } from "@opencode/schema/model"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { InvalidRequestError, ServiceUnavailableError } from "../errors.js"
import { LocationNotFoundError, InvalidRequestError, ServiceUnavailableError } from "../errors.js"

export const GenerateGroup = HttpApiGroup.make("server.generate")
.add(
Expand All @@ -13,7 +13,7 @@ export const GenerateGroup = HttpApiGroup.make("server.generate")
success: Schema.Struct({
data: Schema.Struct({ text: Schema.String }),
}).annotate({ identifier: "GenerateTextResponse" }),
error: [InvalidRequestError, ServiceUnavailableError],
error: [InvalidRequestError, ServiceUnavailableError, LocationNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "experimental.generate.text",
Expand Down
4 changes: 2 additions & 2 deletions packages/protocol/src/groups/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Project } from "@opencode/schema/project"
import { Session } from "@opencode/schema/session"
import { Context, Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { PermissionNotFoundError, SessionNotFoundError } from "../errors.js"
import { LocationNotFoundError, PermissionNotFoundError, SessionNotFoundError } from "../errors.js"
import { LocationQuery, locationQueryOpenApi } from "./location.js"

export const makePermissionGroup = <
Expand Down Expand Up @@ -89,7 +89,7 @@ export const makePermissionGroup = <
HttpApiEndpoint.get("session.permission.list", "/api/session/:sessionID/permission", {
params: { sessionID: Session.ID },
success: Schema.Struct({ data: Schema.Array(Permission.Request) }),
error: SessionNotFoundError,
error: [SessionNotFoundError, LocationNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.permission.list",
Expand Down
Loading
Loading