Compare commits

..

7 Commits

Author SHA1 Message Date
Kit Langton 5ac2e0505b refactor(core): remove unused small model selection 2026-08-05 14:04:47 -04:00
Kit Langton d10b652637 refactor(core): remove unused formatter methods (#40684) 2026-08-05 13:45:23 -04:00
Kit Langton b03ca0d4e2 refactor(core): remove unused bus sequences (#40681) 2026-08-05 13:41:38 -04:00
Kit Langton 25aaea3d31 refactor(core): remove unused shell tree killer (#40680) 2026-08-05 13:38:59 -04:00
Kit Langton cae7a139bc refactor(server): remove obsolete auth header helpers (#40682) 2026-08-05 13:37:29 -04:00
Kit Langton 5ea62ab05f refactor(core): remove orphaned git change operations (#40683) 2026-08-05 13:32:03 -04:00
Kit Langton faadc05c88 refactor(core): remove unused snapshot operations (#40687) 2026-08-05 13:01:48 -04:00
27 changed files with 62 additions and 925 deletions
+5 -50
View File
@@ -211,43 +211,11 @@ export type StreamItem = Schema.Schema.Type<typeof StreamItem>
// event-level `error` envelope, so accept all three shapes here.
// https://www.openresponses.org/specification
const OpenResponsesErrorPayload = Schema.Struct({
type: optionalNull(Schema.String),
code: optionalNull(Schema.String),
message: optionalNull(Schema.String),
param: optionalNull(Schema.String),
})
const WebSocketErrorHeader = Schema.Union([Schema.String, Schema.Number, Schema.Boolean])
export const WebSocketErrorEvent = Schema.StructWithRest(
Schema.Struct({
type: Schema.tag("error"),
status: Schema.optional(Schema.Number),
status_code: Schema.optional(Schema.Number),
code: optionalNull(Schema.String),
message: Schema.optional(Schema.String),
param: optionalNull(Schema.String),
error: optionalNull(OpenResponsesErrorPayload),
headers: Schema.optional(Schema.Record(Schema.String, WebSocketErrorHeader)),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
const decodeWebSocketErrorEvent = Schema.decodeUnknownEffect(WebSocketErrorEvent)
const decodeKnownErrorEvent = (event: Event) =>
decodeWebSocketErrorEvent({
...event,
status: typeof event.status === "number" ? event.status : undefined,
status_code: typeof event.status_code === "number" ? event.status_code : undefined,
headers: ProviderShared.isRecord(event.headers)
? Object.fromEntries(
Object.entries(event.headers).filter(
(entry): entry is [string, string | number | boolean] =>
typeof entry[1] === "string" || typeof entry[1] === "number" || typeof entry[1] === "boolean",
),
)
: undefined,
})
export const Event = Schema.StructWithRest(
Schema.Struct({
type: Schema.String,
@@ -272,9 +240,6 @@ export const Event = Schema.StructWithRest(
message: Schema.optional(Schema.String),
param: optionalNull(Schema.String),
error: optionalNull(OpenResponsesErrorPayload),
status: Schema.optional(Schema.Unknown),
status_code: Schema.optional(Schema.Unknown),
headers: Schema.optional(Schema.Unknown),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
@@ -667,9 +632,9 @@ export type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
const NO_EVENTS: StepResult["1"] = []
// `response.completed` / `response.incomplete` are clean finishes that emit a
// `finish` event; `response.failed` and `error` are hard failures. All four end
// the stream, so keep this set aligned with `step` and the protocol's terminal predicate.
const TERMINAL_TYPES = new Set(["error", "response.completed", "response.incomplete", "response.failed"])
// `finish` event; `response.failed` is a hard failure. All three end the stream,
// so keep this set aligned with `step` and the protocol's terminal predicate.
const TERMINAL_TYPES = new Set(["response.completed", "response.incomplete", "response.failed"])
export const terminal = (event: Event) => TERMINAL_TYPES.has(event.type)
const onOutputTextDelta = (state: ParserState, event: Event, id: string): StepResult => {
@@ -1004,16 +969,10 @@ const providerErrorMessage = (event: Event, fallback: string): string => {
const providerError = (state: ParserState, event: Event, fallback: string) => {
const code = event.code || event.error?.code || event.response?.error?.code || undefined
const message = providerErrorMessage(event, fallback)
const status =
typeof event.status === "number"
? event.status
: typeof event.status_code === "number"
? event.status_code
: undefined
return new AIError({
module: state.id,
method: "stream",
reason: classifyProviderFailure({ message, code, status }),
reason: classifyProviderFailure({ message, code }),
})
}
@@ -1056,11 +1015,7 @@ export const step = (state: ParserState, event: Event) => {
if (event.type === "response.completed" || event.type === "response.incomplete")
return Effect.succeed(onResponseFinish(state, event))
if (event.type === "response.failed") return providerError(state, event, `${state.name} response failed`)
if (event.type === "error")
return decodeKnownErrorEvent(event).pipe(
Effect.mapError(() => ProviderShared.eventError(state.id, `${state.name} returned a malformed error event`)),
Effect.flatMap(() => providerError(state, event, `${state.name} stream error`)),
)
if (event.type === "error") return providerError(state, event, `${state.name} stream error`)
return Effect.succeed<StepResult>([state, NO_EVENTS])
}
-1
View File
@@ -67,7 +67,6 @@ const SERVER_CODES = new Set([
"overloaded_error",
"server_error",
"server_is_overloaded",
"slow_down",
"serviceunavailableexception",
])
const INVALID_REQUEST_CODES = new Set(["invalid_prompt", "invalid_request_error", "validationexception"])
+9 -84
View File
@@ -29,45 +29,14 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/AI
const transportError = (
method: string,
message: string,
input: {
readonly url?: string
readonly kind?: string
readonly phase?: TransportReason["phase"]
readonly delivery?: TransportReason["delivery"]
} = {},
input: { readonly url?: string; readonly kind?: string } = {},
) =>
new AIError({
module: "WebSocketExecutor",
method,
reason: new TransportReason({
message,
url: input.url,
kind: input.kind,
phase: input.phase,
delivery: input.delivery,
}),
reason: new TransportReason({ message, url: input.url, kind: input.kind }),
})
const annotateTransportError = (
error: AIError,
input: { readonly phase: TransportReason["phase"]; readonly delivery: TransportReason["delivery"] },
) =>
error.reason._tag === "Transport"
? new AIError({
module: error.module,
method: error.method,
reason: new TransportReason({
message: error.reason.message,
kind: error.reason.kind,
url: error.reason.url,
http: error.reason.http,
phase: input.phase,
delivery: input.delivery,
recovery: error.reason.recovery,
}),
})
: error
const eventMessage = (event: Event) => {
if ("message" in event && typeof event.message === "string") return event.message
return event.type
@@ -87,8 +56,6 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => {
transportError("open", `WebSocket closed before opening (state ${ws.readyState})`, {
url: input.url,
kind: "open",
phase: "connect",
delivery: "not-sent",
}),
)
}
@@ -112,12 +79,7 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => {
cleanup()
resume(
Effect.fail(
transportError("open", `Failed to open WebSocket: ${eventMessage(event)}`, {
url: input.url,
kind: "open",
phase: "connect",
delivery: "not-sent",
}),
transportError("open", `Failed to open WebSocket: ${eventMessage(event)}`, { url: input.url, kind: "open" }),
),
)
}
@@ -128,8 +90,6 @@ const waitOpen = (ws: globalThis.WebSocket, input: WebSocketRequest) => {
transportError("open", `WebSocket closed before opening with code ${event.code}`, {
url: input.url,
kind: "open",
phase: "connect",
delivery: "not-sent",
}),
),
)
@@ -159,8 +119,6 @@ const webSocketUrl = (value: string) =>
transportError("prepare", error instanceof Error ? error.message : "Invalid WebSocket URL", {
url: value,
kind: "websocket",
phase: "prepare",
delivery: "not-sent",
}),
})
@@ -172,8 +130,6 @@ export const open = (input: WebSocketRequest) =>
transportError("open", error instanceof Error ? error.message : "Failed to construct WebSocket", {
url: input.url,
kind: "open",
phase: "connect",
delivery: "not-sent",
}),
}).pipe(Effect.flatMap((ws) => fromWebSocket(ws, input)))
@@ -194,11 +150,7 @@ export const fromWebSocket = (
Queue.failCauseUnsafe(
messages,
Cause.fail(
transportError("message", "Unsupported WebSocket message payload", {
url: input.url,
kind: "message",
phase: "receive",
}),
transportError("message", "Unsupported WebSocket message payload", { url: input.url, kind: "message" }),
),
)
}
@@ -206,23 +158,16 @@ export const fromWebSocket = (
Queue.failCauseUnsafe(
messages,
Cause.fail(
transportError("message", `WebSocket error: ${eventMessage(event)}`, {
url: input.url,
kind: "message",
phase: "receive",
}),
transportError("message", `WebSocket error: ${eventMessage(event)}`, { url: input.url, kind: "message" }),
),
)
}
const onClose = (event: CloseEvent) => {
if (event.code === 1000 || event.code === 1005) return Queue.endUnsafe(messages)
Queue.failCauseUnsafe(
messages,
Cause.fail(
transportError("message", `WebSocket closed with code ${event.code}`, {
url: input.url,
kind: "close",
phase: "close",
}),
transportError("message", `WebSocket closed with code ${event.code}`, { url: input.url, kind: "close" }),
),
)
}
@@ -244,8 +189,6 @@ export const fromWebSocket = (
transportError("sendText", error instanceof Error ? error.message : "Failed to send WebSocket message", {
url: input.url,
kind: "write",
phase: "send",
delivery: "not-sent",
}),
}),
messages: Stream.fromQueue(messages),
@@ -301,8 +244,6 @@ export const json = <Body, Message>(input: JsonInput<Body, Message>): JsonTransp
transportError("json", "WebSocket JSON transport requires WebSocketExecutor.Service", {
url: prepared.url,
kind: "websocket",
phase: "prepare",
delivery: "not-sent",
}),
)
}
@@ -310,27 +251,11 @@ export const json = <Body, Message>(input: JsonInput<Body, Message>): JsonTransp
return Stream.unwrap(
Effect.gen(function* () {
const connection = yield* Effect.acquireRelease(
webSocket
.open({ url: prepared.url, headers: prepared.headers })
.pipe(
Effect.mapError((error) => annotateTransportError(error, { phase: "connect", delivery: "not-sent" })),
),
webSocket.open({ url: prepared.url, headers: prepared.headers }),
(connection) => connection.close,
)
yield* connection.sendText(prepared.message)
let observed = false
return connection.messages.pipe(
Stream.map((message) => {
observed = true
return messageText(message, decoder)
}),
Stream.mapError((error) =>
annotateTransportError(error, {
phase: error.reason._tag === "Transport" && error.reason.phase === "close" ? "close" : "receive",
delivery: observed ? "accepted" : "ambiguous",
}),
),
)
return connection.messages.pipe(Stream.map((message) => messageText(message, decoder)))
}),
)
},
-7
View File
@@ -98,13 +98,6 @@ export class TransportReason extends Schema.Class<TransportReason>("AI.Error.Tra
kind: Schema.optional(Schema.String),
url: Schema.optional(Schema.String),
http: Schema.optional(HttpContext),
phase: Schema.optional(
Schema.Literals(["prepare", "queue", "connect", "send", "receive", "decode", "complete", "fallback", "close"]),
),
delivery: Schema.optional(Schema.Literals(["not-sent", "rejected", "ambiguous", "accepted"])),
recovery: Schema.optional(
Schema.Literals(["retry-connect", "retry-full", "rotate-and-retry-full", "fallback-http", "fail"]),
),
}) {}
export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOutputReason>(
+2 -2
View File
@@ -69,10 +69,10 @@ describe("provider error classification", () => {
test("classifies V1 overloaded provider codes", () => {
expect(
['{"code":"resource_exhausted"}', '{"code":"service_unavailable"}', '{"code":"slow_down"}'].map(
['{"code":"resource_exhausted"}', '{"code":"service_unavailable"}'].map(
(message) => classifyProviderFailure({ message })._tag,
),
).toEqual(["ProviderInternal", "ProviderInternal", "ProviderInternal"])
).toEqual(["ProviderInternal", "ProviderInternal"])
})
test("classifies transient client statuses as provider internal", () => {
@@ -11,7 +11,6 @@ import {
ToolCallPart,
ToolDefinition,
ToolResultPart,
TransportReason,
Usage,
} from "../../src"
import { Auth, LLMClient, RequestExecutor, WebSocketExecutor } from "../../src/route"
@@ -289,114 +288,6 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("terminates WebSocket control events without waiting for the socket to close", () =>
Effect.gen(function* () {
const events = [
{ type: "error", error: { code: "slow_down", message: "Try later" } },
{
type: "error",
status_code: 429,
message: "Rate limited",
headers: { "retry-after": 1, "x-request-id": "request", cached: false, invalid: [] },
},
{
type: "response.failed",
response: { error: { code: "server_error", message: "Unavailable" } },
},
{ type: "error", status: "not-a-status", message: "Malformed status" },
]
const errors = yield* Effect.forEach(events, (event) =>
LLMClient.generate(
LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responsesWebSocket(
"gpt-4.1-mini",
),
prompt: "Say hello.",
}),
).pipe(
Effect.provide(
LLMClient.layer.pipe(
Layer.provide(
Layer.mergeAll(
Layer.succeed(
RequestExecutor.Service,
RequestExecutor.Service.of({ execute: () => Effect.die("unexpected HTTP request") }),
),
Layer.succeed(
WebSocketExecutor.Service,
WebSocketExecutor.Service.of({
open: () =>
Effect.succeed({
sendText: () => Effect.void,
messages: Stream.make(ProviderShared.encodeJson(event)).pipe(Stream.concat(Stream.never)),
close: Effect.void,
}),
}),
),
),
),
),
),
Effect.flip,
),
)
expect(errors.map((error) => error.reason._tag)).toEqual([
"ProviderInternal",
"RateLimit",
"ProviderInternal",
"UnknownProvider",
])
}),
)
it.effect("marks post-send WebSocket failures with delivery state", () =>
Effect.gen(function* () {
const failure = new AIError({
module: "test",
method: "receive",
reason: new TransportReason({ message: "socket closed", phase: "close" }),
})
const streams = [
Stream.fail(failure),
Stream.make(ProviderShared.encodeJson({ type: "response.created" })).pipe(Stream.concat(Stream.fail(failure))),
]
const deps = Layer.mergeAll(
Layer.succeed(
RequestExecutor.Service,
RequestExecutor.Service.of({ execute: () => Effect.die("unexpected HTTP request") }),
),
Layer.succeed(
WebSocketExecutor.Service,
WebSocketExecutor.Service.of({
open: () =>
Effect.succeed({
sendText: () => Effect.void,
messages: streams.shift() ?? Stream.die("unexpected WebSocket open"),
close: Effect.void,
}),
}),
),
)
const model = OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responsesWebSocket(
"gpt-4.1-mini",
)
const errors = yield* Effect.forEach(["first", "second"], (prompt) =>
LLMClient.generate(LLM.request({ model, prompt })).pipe(
Effect.provide(LLMClient.layer.pipe(Layer.provide(deps))),
Effect.flip,
),
)
expect(errors.map((error) => error.reason)).toEqual([
expect.objectContaining({ _tag: "Transport", phase: "close", delivery: "ambiguous" }),
expect.objectContaining({ _tag: "Transport", phase: "close", delivery: "accepted" }),
])
}),
)
it.effect("fails immediately when WebSocket is already closed", () =>
Effect.gen(function* () {
const error = yield* WebSocketExecutor.fromWebSocket(
@@ -406,7 +297,6 @@ describe("OpenAI Responses route", () => {
).pipe(Effect.flip)
expect(error.message).toContain("closed before opening")
expect(error.reason).toMatchObject({ _tag: "Transport", phase: "connect", delivery: "not-sent" })
}),
)
-19
View File
@@ -11,7 +11,6 @@ import {
LanguageModel,
ModelID,
ProviderID,
TransportReason,
Usage,
} from "../src/schema"
import { ProviderShared } from "../src/protocols/shared"
@@ -109,21 +108,3 @@ test("AI errors expose the shared runtime tag", async () => {
await Effect.runPromise(Effect.fail(error).pipe(Effect.catchTag("AI.Error", () => Effect.succeed("caught")))),
).toBe("caught")
})
test("transport errors serialize execution facts", () => {
const reason = new TransportReason({
message: "connection closed",
phase: "receive",
delivery: "ambiguous",
recovery: "fail",
})
expect(Schema.encodeSync(TransportReason)(reason)).toEqual({
_tag: "Transport",
message: "connection closed",
phase: "receive",
delivery: "ambiguous",
recovery: "fail",
})
expect(Schema.decodeUnknownSync(TransportReason)(Schema.encodeSync(TransportReason)(reason))).toEqual(reason)
})
+1 -17
View File
@@ -3,7 +3,7 @@ export * as Bus from "./bus"
import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
import { Event } from "@opencode-ai/schema/event"
import type { EventLog } from "@opencode-ai/schema/event-log"
import { and, asc, eq, gt, inArray, lte, sql } from "drizzle-orm"
import { and, asc, eq, gt, lte, sql } from "drizzle-orm"
import { Database } from "./database/database"
import { EventSequenceTable, EventTable } from "./event/sql"
import { Location } from "./location"
@@ -134,8 +134,6 @@ export interface Interface {
readonly after?: number
readonly follow?: boolean
}) => Stream.Stream<LogItem>
/** Latest committed seq per aggregate. Aggregates without events are absent. */
readonly sequences: (aggregateIDs: ReadonlyArray<string>) => Effect.Effect<ReadonlyMap<string, Event.Seq>>
/** @deprecated Use `subscribe()` and consume the returned stream. */
readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe>
readonly project: <D extends Event.Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void>
@@ -657,19 +655,6 @@ export const layerWith = (options?: LayerOptions) =>
}),
)
const sequences = (aggregateIDs: ReadonlyArray<string>): Effect.Effect<ReadonlyMap<string, Event.Seq>> => {
if (aggregateIDs.length === 0) return Effect.succeed(new Map())
return db
.select({ aggregateID: EventSequenceTable.aggregate_id, seq: EventSequenceTable.seq })
.from(EventSequenceTable)
.where(inArray(EventSequenceTable.aggregate_id, Array.from(aggregateIDs)))
.all()
.pipe(
Effect.orDie,
Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Event.Seq.make(row.seq)]))),
)
}
const listen = (listener: Subscriber): Effect.Effect<Unsubscribe> =>
Effect.sync(() => {
listeners.push(listener)
@@ -691,7 +676,6 @@ export const layerWith = (options?: LayerOptions) =>
publish,
subscribe,
log,
sequences,
listen,
project,
replay,
-56
View File
@@ -52,7 +52,6 @@ export interface Interface extends State.Transformable<Draft> {
readonly all: () => Effect.Effect<Model.Info[]>
readonly available: () => Effect.Effect<Model.Info[]>
readonly default: () => Effect.Effect<Model.Info | undefined>
readonly small: (providerID: Provider.ID) => Effect.Effect<Model.Info | undefined>
}
}
@@ -206,59 +205,6 @@ const layer = Layer.effect(
return (yield* result.model.available())[0]
}),
small: Effect.fn("Catalog.model.small")(function* (providerID) {
const record = state.get().providers.get(providerID)
if (!record) return
const provider = record.provider
// TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments.
if (providerID === Provider.ID.azure || providerID === Provider.ID.make("azure-cognitive-services")) {
return
}
if (providerID === Provider.ID.opencode) {
const gpt5Nano = record.models.get(Model.ID.make("gpt-5-nano"))
if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider)
}
const candidates = pipe(
Array.fromIterable(record.models.values()),
Array.filter(
(model) =>
model.providerID === providerID &&
model.enabled &&
model.status === "active" &&
model.capabilities.input.some((item) => item.startsWith("text")) &&
model.capabilities.output.some((item) => item.startsWith("text")),
),
Array.map((model) => ({
model,
cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999,
age: (Date.now() - model.time.released) / (1000 * 60 * 60 * 24 * 30),
small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()),
})),
Array.filter((item) => item.cost > 0 && item.age <= 18),
)
const pick = (items: typeof candidates) => {
if (!Array.isReadonlyArrayNonEmpty(items)) return
const maxCost = Math.max(...items.map((item) => item.cost), 0.01)
const maxAge = Math.max(...items.map((item) => item.age), 0.01)
const selected = Array.min(
items,
Order.mapInput(
Order.Number,
(item: (typeof candidates)[number]) =>
(item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2,
),
)
return projectModel(selected.model, provider)
}
const small = candidates.filter((item) => item.small)
return pick(small.length > 0 ? small : candidates)
}),
},
}
@@ -266,6 +212,4 @@ const layer = Layer.effect(
}),
)
const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, Integration.node] })
+2 -28
View File
@@ -1,6 +1,6 @@
export * as Formatter from "./formatter"
import { Context, Effect, Layer, Schema } from "effect"
import { Context, Effect, Layer } from "effect"
import { ChildProcess } from "effect/unstable/process"
import path from "path"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
@@ -11,16 +11,7 @@ import { Config } from "./config"
import { Location } from "./location"
import { make, type Info } from "./formatter/builtins"
export const Status = Schema.Struct({
name: Schema.String,
extensions: Schema.Array(Schema.String),
enabled: Schema.Boolean,
}).annotate({ identifier: "FormatterStatus" })
export type Status = typeof Status.Type
export interface Interface {
readonly init: () => Effect.Effect<void>
readonly status: () => Effect.Effect<Status[]>
readonly file: (filepath: string) => Effect.Effect<boolean>
}
@@ -84,23 +75,6 @@ const layer = Layer.effect(
return result
})
const init = Effect.fn("Formatter.init")(function* () {
yield* load
})
const status = Effect.fn("Formatter.status")(function* () {
yield* load
return yield* Effect.forEach(formatters, (formatter) =>
command(formatter).pipe(
Effect.map((enabled) => ({
name: formatter.name,
extensions: [...formatter.extensions],
enabled: enabled !== false,
})),
),
)
})
const file = Effect.fn("Formatter.file")(function* (filepath: string) {
yield* load
const matching = formatters.filter((formatter) =>
@@ -143,7 +117,7 @@ const layer = Layer.effect(
return false
})
return Service.of({ init, status, file })
return Service.of({ file })
}),
)
+1 -224
View File
@@ -1,8 +1,7 @@
export * as Git from "./git"
import path from "path"
import { randomUUID } from "crypto"
import { Context, Effect, Layer, Schema, Stream } from "effect"
import { Context, Effect, Layer, Schema } from "effect"
import { ChildProcess } from "effect/unstable/process"
import { AbsolutePath, RelativePath } from "./schema"
import { FSUtil } from "@opencode-ai/util/fs-util"
@@ -36,9 +35,6 @@ const snapshotConfig = `[core]
threads = true
`
export const ChangeSet = Schema.String.pipe(Schema.brand("Git.ChangeSet"))
export type ChangeSet = typeof ChangeSet.Type
export const TreeID = Schema.String.pipe(Schema.brand("Git.TreeID"))
export type TreeID = typeof TreeID.Type
@@ -73,13 +69,6 @@ export class WorktreeError extends Schema.TaggedErrorClass<WorktreeError>()("Git
cause: Schema.optional(Schema.Defect()),
}) {}
export class PatchError extends Schema.TaggedErrorClass<PatchError>()("Git.PatchError", {
operation: Schema.Literals(["capture", "apply", "reset"]),
directory: AbsolutePath,
message: Schema.String,
cause: Schema.optional(Schema.Defect()),
}) {}
export interface Interface {
readonly repo: {
readonly discover: (input: AbsolutePath) => Effect.Effect<Repository | undefined>
@@ -116,20 +105,6 @@ export interface Interface {
) => Effect.Effect<void, OperationError>
readonly resetHard: (repository: Repository, revision: string) => Effect.Effect<void, OperationError>
}
readonly change: {
readonly capture: (input: { repository: Repository; path: AbsolutePath }) => Effect.Effect<ChangeSet, PatchError>
readonly apply: (input: {
repository: Repository
path: AbsolutePath
changes: ChangeSet
}) => Effect.Effect<void, PatchError>
readonly discard: (input: {
repository: Repository
path: AbsolutePath
index: "preserve" | "reset"
untracked: "preserve" | "remove"
}) => Effect.Effect<void, PatchError>
}
readonly worktree: {
readonly create: (input: {
repository: Repository
@@ -175,17 +150,10 @@ export interface Interface {
context?: number
paths?: readonly RelativePath[]
}) => Effect.Effect<readonly File.Diff[], OperationError>
readonly preview: (input: {
repository: Repository
current: TreeID
files: ReadonlyMap<RelativePath, TreeID>
context?: number
}) => Effect.Effect<readonly File.Diff[], OperationError>
readonly restore: (input: {
repository: Repository
files: ReadonlyMap<RelativePath, TreeID>
}) => Effect.Effect<void, OperationError>
readonly checkout: (input: { repository: Repository; tree: TreeID }) => Effect.Effect<void, OperationError>
}
}
@@ -657,58 +625,6 @@ const layer = Layer.effect(
return { mode: match[1], object: match[2] }
})
const preview = Effect.fn("Git.tree.preview")(
(input: {
repository: Repository
current: TreeID
files: ReadonlyMap<RelativePath, TreeID>
context?: number
}) =>
locked(
input.repository,
Effect.gen(function* () {
const index = path.join(input.repository.gitDirectory, `preview-${randomUUID()}.index`)
const env = { GIT_INDEX_FILE: index }
return yield* Effect.gen(function* () {
yield* repositoryOperation("diff", input.repository, ["read-tree", input.current], { env })
yield* Effect.forEach(
input.files,
([file, tree]) =>
Effect.gen(function* () {
const source = yield* entry(input.repository, tree, file)
if (!source) {
yield* repositoryOperation(
"diff",
input.repository,
["update-index", "--force-remove", "--", file],
{ env },
)
return
}
yield* repositoryOperation(
"diff",
input.repository,
["update-index", "--add", "--cacheinfo", source.mode, source.object, file],
{ env },
)
}),
{ discard: true },
)
const target = TreeID.make(
(yield* repositoryOperation("diff", input.repository, ["write-tree"], { env })).text.trim(),
)
return yield* treeDiff({
repository: input.repository,
from: input.current,
to: target,
context: input.context,
paths: Array.from(input.files.keys()),
})
}).pipe(Effect.ensuring(fs.remove(index).pipe(Effect.catch(() => Effect.void))))
}),
),
)
const restore = Effect.fn("Git.tree.restore")(
(input: { repository: Repository; files: ReadonlyMap<RelativePath, TreeID> }) =>
locked(
@@ -738,142 +654,6 @@ const layer = Layer.effect(
),
)
const checkoutTree = Effect.fn("Git.tree.checkout")((input: { repository: Repository; tree: TreeID }) =>
locked(
input.repository,
Effect.gen(function* () {
yield* repositoryOperation("restore", input.repository, ["read-tree", input.tree])
yield* repositoryOperation("restore", input.repository, ["checkout-index", "--all", "--force"])
}),
),
)
const capture = Effect.fn("Git.change.capture")(function* (input: { repository: Repository; path: AbsolutePath }) {
const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "."
const tracked = yield* execute(
input.repository.worktree,
proc,
)(["diff", "--binary", "HEAD", "--", scope]).pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
),
)
if (tracked.exitCode !== 0) {
return yield* new PatchError({
operation: "capture",
directory: input.path,
message: tracked.stderr.trim() || tracked.text.trim() || "Failed to capture tracked changes",
})
}
const untracked = yield* execute(
input.repository.worktree,
proc,
)(["ls-files", "--others", "--exclude-standard", "-z", "--", scope]).pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
),
)
if (untracked.exitCode !== 0) {
return yield* new PatchError({
operation: "capture",
directory: input.path,
message: untracked.stderr.trim() || untracked.text.trim() || "Failed to list untracked changes",
})
}
const created = yield* Effect.forEach(untracked.text.split("\0").filter(Boolean), (file) =>
execute(
input.repository.worktree,
proc,
)(["diff", "--binary", "--no-index", "--", "/dev/null", file]).pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }),
),
Effect.flatMap((result) =>
// git diff --no-index returns 1 when differences were found.
result.exitCode === 0 || result.exitCode === 1
? Effect.succeed(result.text)
: Effect.fail(
new PatchError({
operation: "capture",
directory: input.path,
message:
result.stderr.trim() || result.text.trim() || `Failed to capture untracked change: ${file}`,
}),
),
),
),
)
return ChangeSet.make([tracked.text, ...created].filter(Boolean).join("\n"))
})
const apply = Effect.fn("Git.change.apply")(function* (input: {
repository: Repository
path: AbsolutePath
changes: ChangeSet
}) {
const result = yield* proc
.run(
ChildProcess.make("git", ["apply", "-"], {
cwd: input.path,
extendEnv: true,
stdin: Stream.make(new TextEncoder().encode(input.changes)),
}),
)
.pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "apply", directory: input.path, message: cause.message, cause }),
),
)
if (result.exitCode === 0) return
return yield* new PatchError({
operation: "apply",
directory: input.path,
message:
result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || "Failed to apply changes",
})
})
const discard = Effect.fn("Git.change.discard")(function* (input: {
repository: Repository
path: AbsolutePath
index: "preserve" | "reset"
untracked: "preserve" | "remove"
}) {
const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "."
const restore = yield* execute(
input.repository.worktree,
proc,
)(input.index === "reset" ? ["checkout", "HEAD", "--", scope] : ["checkout", "--", scope]).pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }),
),
)
if (restore.exitCode !== 0) {
return yield* new PatchError({
operation: "reset",
directory: input.path,
message: restore.stderr.trim() || restore.text.trim() || "Failed to restore tracked changes",
})
}
if (input.untracked === "preserve") return
const clean = yield* execute(
input.repository.worktree,
proc,
)(["clean", "-fd", "--", scope]).pipe(
Effect.mapError(
(cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }),
),
)
if (clean.exitCode === 0) return
return yield* new PatchError({
operation: "reset",
directory: input.path,
message: clean.stderr.trim() || clean.text.trim() || "Failed to clean untracked changes",
})
})
const worktreeRun = Effect.fnUntraced(function* (
operation: "create" | "remove" | "list",
repository: Repository,
@@ -949,7 +729,6 @@ const layer = Layer.effect(
remote: { get: remote },
history: { head, branch, defaultRemoteBranch: remoteHead, rootCommits: roots },
sync: { fetchRemotes: fetch, fetchBranch, checkoutRemoteBranch: checkout, resetHard: reset },
change: { capture, apply, discard },
worktree: { create: worktreeCreate, remove: worktreeRemove, list: worktreeList },
index: { refresh, ignored },
tree: {
@@ -957,9 +736,7 @@ const layer = Layer.effect(
write: writeTree,
files: treeFiles,
diff: treeDiff,
preview,
restore,
checkout: checkoutTree,
},
})
}),
+1 -2
View File
@@ -18,9 +18,8 @@ export function isRetryable(error: AIError) {
switch (error.reason._tag) {
case "RateLimit":
case "ProviderInternal":
return true
case "Transport":
return error.reason.delivery === undefined || error.reason.delivery === "not-sent"
return true
case "InvalidProviderOutput":
return error.reason.classification === "incomplete-stream"
case "Authentication":
-34
View File
@@ -1,15 +1,12 @@
export * as ShellSelect from "./select"
import path from "path"
import { spawn, type ChildProcess } from "child_process"
import { readFile } from "fs/promises"
import { statSync } from "fs"
import { setTimeout } from "node:timers/promises"
import { Schema } from "effect"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { which } from "../util/which"
const SIGKILL_TIMEOUT_MS = 200
const META: Record<string, { deny?: boolean; login?: boolean; posix?: boolean; ps?: boolean }> = {
bash: { login: true, posix: true },
dash: { login: true, posix: true },
@@ -33,37 +30,6 @@ export const Options = Schema.Struct({
})
export type Options = typeof Options.Type
export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise<void> {
const pid = proc.pid
if (!pid || opts?.exited?.()) return
if (process.platform === "win32") {
await new Promise<void>((resolve) => {
const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
stdio: "ignore",
windowsHide: true,
})
killer.once("exit", () => resolve())
killer.once("error", () => resolve())
})
return
}
try {
process.kill(-pid, "SIGTERM")
await setTimeout(SIGKILL_TIMEOUT_MS)
if (!opts?.exited?.()) {
process.kill(-pid, "SIGKILL")
}
} catch {
proc.kill("SIGTERM")
await setTimeout(SIGKILL_TIMEOUT_MS)
if (!opts?.exited?.()) {
proc.kill("SIGKILL")
}
}
}
function stat(file: string) {
return statSync(file, { throwIfNoEntry: false }) ?? undefined
}
+6 -59
View File
@@ -16,7 +16,7 @@ import { Hash } from "@opencode-ai/util/hash"
export { ID }
export class Error extends Schema.TaggedErrorClass<Error>()("Snapshot.Error", {
operation: Schema.Literals(["capture", "files", "diff", "preview", "restore"]),
operation: Schema.Literals(["capture", "files", "diff", "restore"]),
message: Schema.String,
cause: Schema.optional(Schema.Defect()),
}) {}
@@ -36,10 +36,6 @@ export interface RestoreInput {
readonly files: ReadonlyMap<RelativePath, ID>
}
export interface PreviewInput extends RestoreInput {
readonly context?: number
}
export interface Interface {
/**
* Capture the current Location-scoped filesystem state as a content-addressed
@@ -60,25 +56,11 @@ export interface Interface {
*/
readonly diff: (input: DiffInput) => Effect.Effect<readonly File.Diff[], Error>
/**
* Preview the filesystem result of a selective restore without modifying the
* worktree. Each project-relative path maps to the tree it would be restored
* from.
*/
readonly preview: (input: PreviewInput) => Effect.Effect<readonly File.Diff[], Error>
/**
* Restore selected project-relative paths from their associated trees. A path
* absent from its selected tree is removed; paths outside the map are untouched.
*/
*/
readonly restore: (input: RestoreInput) => Effect.Effect<void, Error>
/**
* Replace the snapshot index with a captured tree and check out all its entries.
* Files absent from the tree remain untouched. Prefer selective `restore` when
* only known paths should change.
*/
readonly checkout: (snapshot: ID) => Effect.Effect<void, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Snapshot") {}
@@ -176,59 +158,26 @@ const layer = Layer.effect(
.pipe(Effect.mapError((cause) => failure("diff", cause)))
})
const plan = Effect.fnUntraced(function* (
operation: "preview" | "restore",
worktree: AbsolutePath,
input: RestoreInput,
) {
const plan = Effect.fnUntraced(function* (worktree: AbsolutePath, input: RestoreInput) {
const files = new Map<RelativePath, Git.TreeID>()
for (const [file, snapshot] of input.files) {
const absolute = path.resolve(worktree, file)
if (!FSUtil.contains(worktree, absolute))
return yield* new Error({ operation, message: `Path escapes the project: ${file}` })
return yield* new Error({ operation: "restore", message: `Path escapes the project: ${file}` })
files.set(file, Git.TreeID.make(snapshot))
}
return files
})
const preview = Effect.fn("Snapshot.preview")(function* (input: PreviewInput) {
if (!(yield* enabled())) return yield* new Error({ operation: "preview", message: "Snapshots are disabled" })
const repo = yield* repository.pipe(Effect.mapError((cause) => failure("preview", cause)))
const files = yield* plan("preview", repo.worktree, input)
const current = yield* git.tree
.capture({
repository: repo.snapshotRepository,
scopes: Array.from(files.keys()),
ignores: repo.source,
maximumUntrackedFileBytes: 2 * 1024 * 1024,
})
.pipe(Effect.mapError((cause) => failure("preview", cause)))
return yield* git.tree
.preview({
repository: repo.snapshotRepository,
current,
files,
context: input.context,
})
.pipe(Effect.mapError((cause) => failure("preview", cause)))
})
const restore = Effect.fn("Snapshot.restore")(function* (input: RestoreInput) {
if (!(yield* enabled())) return yield* new Error({ operation: "restore", message: "Snapshots are disabled" })
const repo = yield* repository.pipe(Effect.mapError((cause) => failure("restore", cause)))
yield* git.tree
.restore({ repository: repo.snapshotRepository, files: yield* plan("restore", repo.worktree, input) })
.restore({ repository: repo.snapshotRepository, files: yield* plan(repo.worktree, input) })
.pipe(Effect.mapError((cause) => failure("restore", cause)))
})
const checkout = Effect.fn("Snapshot.checkout")(function* (snapshot: ID) {
const repo = yield* repository.pipe(Effect.mapError((cause) => failure("restore", cause)))
yield* git.tree
.checkout({ repository: repo.snapshotRepository, tree: Git.TreeID.make(snapshot) })
.pipe(Effect.mapError((cause) => failure("restore", cause)))
})
return Service.of({ capture, files, diff, preview, restore, checkout })
return Service.of({ capture, files, diff, restore })
}).pipe(Effect.withSpan("Snapshot.boot")),
)
@@ -244,9 +193,7 @@ export const noopLayer = Layer.succeed(
capture: () => Effect.succeed(undefined),
files: () => Effect.succeed([]),
diff: () => Effect.succeed([]),
preview: () => Effect.succeed([]),
restore: () => Effect.void,
checkout: () => Effect.void,
}),
)
-20
View File
@@ -1298,24 +1298,4 @@ describe("Bus", () => {
}),
)
it.effect("sequences returns the latest committed seq per aggregate and omits unknown aggregates", () =>
Effect.gen(function* () {
const bus = yield* Bus.Service
const first = Session.ID.create()
const second = Session.ID.create()
yield* bus.publish(DurableMessage, durableData(first, "zero"))
yield* bus.publish(DurableMessage, durableData(first, "one"))
yield* bus.publish(DurableMessage, durableData(second, "zero"))
const sequences = yield* bus.sequences([first, second, Session.ID.create()])
expect(sequences).toEqual(
new Map([
[first, Event.Seq.make(1)],
[second, Event.Seq.make(0)],
]),
)
expect(yield* bus.sequences([])).toEqual(new Map())
}),
)
})
-43
View File
@@ -1,5 +1,4 @@
import { describe, expect } from "bun:test"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Fiber, Layer, Stream } from "effect"
import { TestClock } from "effect/testing"
import { Catalog } from "@opencode-ai/core/catalog"
@@ -292,46 +291,4 @@ describe("Catalog", () => {
})
}),
)
it.effect("small model prefers small keyword candidates before cost scoring", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = Provider.ID.make("test")
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, () => {})
catalog.model.update(providerID, Model.ID.make("cheap-large"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = [
{
input: Money.USDPerMillionTokens.make(1),
output: Money.USDPerMillionTokens.make(1),
cache: {
read: Money.USDPerMillionTokens.zero,
write: Money.USDPerMillionTokens.zero,
},
},
]
model.time.released = Date.now()
})
catalog.model.update(providerID, Model.ID.make("expensive-mini"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = [
{
input: Money.USDPerMillionTokens.make(10),
output: Money.USDPerMillionTokens.make(10),
cache: {
read: Money.USDPerMillionTokens.zero,
write: Money.USDPerMillionTokens.zero,
},
},
]
model.time.released = Date.now()
})
})
expect((yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini")
}),
)
})
+34 -57
View File
@@ -56,52 +56,22 @@ function withTemp<A, E, R>(body: (directory: string) => Effect.Effect<A, E, R>)
}
describe("Formatter", () => {
it.live("status() returns empty list when no formatters are configured", () =>
it.live("does not run formatters marked as disabled in config", () =>
withTemp((directory) =>
Formatter.Service.use((formatter) => formatter.status()).pipe(Effect.provide(formatterLayer(directory))),
),
)
it.live("status() returns built-in formatters when formatter is true", () =>
withTemp((directory) =>
Formatter.Service.use((formatter) =>
Effect.gen(function* () {
const statuses = yield* formatter.status()
const gofmt = statuses.find((item) => item.name === "gofmt")
expect(gofmt).toBeDefined()
expect(gofmt?.extensions).toContain(".go")
}),
).pipe(Effect.provide(formatterLayer(directory, true))),
),
)
it.live("status() keeps built-in formatters when config object is provided", () =>
withTemp((directory) =>
Formatter.Service.use((formatter) =>
Effect.gen(function* () {
const statuses = yield* formatter.status()
expect(statuses.find((item) => item.name === "gofmt")?.extensions).toContain(".go")
expect(statuses.find((item) => item.name === "mix")).toBeDefined()
}),
).pipe(Effect.provide(formatterLayer(directory, { gofmt: {} }))),
),
)
it.live("status() excludes formatters marked as disabled in config", () =>
withTemp((directory) =>
Formatter.Service.use((formatter) =>
Effect.gen(function* () {
const statuses = yield* formatter.status()
expect(statuses.find((item) => item.name === "gofmt")).toBeUndefined()
expect(statuses.find((item) => item.name === "mix")).toBeDefined()
}),
).pipe(Effect.provide(formatterLayer(directory, { gofmt: { disabled: true } }))),
),
)
it.live("service initializes without error", () =>
withTemp((directory) =>
Formatter.Service.use((formatter) => formatter.init()).pipe(Effect.provide(formatterLayer(directory))),
Effect.gen(function* () {
const file = path.join(directory, "test.disabled")
expect(yield* Formatter.Service.use((formatter) => formatter.file(file))).toBe(false)
}).pipe(
Effect.provide(
formatterLayer(directory, {
disabled: {
disabled: true,
command: [process.execPath, "-e", "process.exit(0)", "$FILE"],
extensions: [".disabled"],
},
}),
),
),
),
)
@@ -115,22 +85,29 @@ describe("Formatter", () => {
),
)
it.live("status() initializes formatter state per directory", () =>
Effect.acquireUseRelease(
Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
([off, on]) =>
it.live("loads formatter state per directory", () =>
withTemp((off) =>
withTemp((on) =>
Effect.gen(function* () {
const disabled = yield* Formatter.Service.use((formatter) => formatter.status()).pipe(
Effect.provide(formatterLayer(off.path, false)),
const offFile = path.join(off, "test.isolated")
const onFile = path.join(on, "test.isolated")
const disabled = yield* Formatter.Service.use((formatter) => formatter.file(offFile)).pipe(
Effect.provide(formatterLayer(off, false)),
)
const enabled = yield* Formatter.Service.use((formatter) => formatter.status()).pipe(
Effect.provide(formatterLayer(on.path, true)),
const enabled = yield* Formatter.Service.use((formatter) => formatter.file(onFile)).pipe(
Effect.provide(
formatterLayer(on, {
isolated: {
command: [process.execPath, "-e", "process.exit(0)", "$FILE"],
extensions: [".isolated"],
},
}),
),
)
expect(disabled).toEqual([])
expect(enabled.find((item) => item.name === "gofmt")).toBeDefined()
expect(disabled).toBe(false)
expect(enabled).toBe(true)
}),
(directories) =>
Effect.promise(() => Promise.all(directories.map((tmp) => tmp[Symbol.asyncDispose]())).then(() => undefined)),
),
),
)
-1
View File
@@ -30,7 +30,6 @@ const catalog = Layer.mock(Catalog.Service, {
all: () => Effect.die("unused"),
available: () => Effect.die("unused"),
default: () => Effect.die("unused"),
small: () => Effect.die("unused"),
},
})
const integrations = Layer.mock(Integration.Service, {
-3
View File
@@ -185,9 +185,6 @@ describe("Git trees", () => {
])
const files = new Map([[RelativePath.make("scope/tracked.txt"), before]])
const preview = yield* git.tree.preview({ repository, current: after, files, context: 1 })
expect(preview).toHaveLength(1)
expect(preview[0]?.file).toBe(RelativePath.make("scope/tracked.txt"))
yield* git.tree.restore({ repository, files })
expect(yield* read(path.join(root.path, "scope", "tracked.txt"))).toBe("one\n")
expect(yield* read(path.join(root.path, "scope", "added.txt"))).toBe("added\n")
@@ -484,31 +484,4 @@ describe("OpencodePlugin", () => {
}),
),
)
it.effect("prefers gpt-5-nano as the opencode small model", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = Provider.ID.opencode
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, () => {})
catalog.model.update(providerID, Model.ID.make("cheap-mini"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = [...cost(1, 1)]
model.time.released = Date.now()
})
catalog.model.update(providerID, Model.ID.make("gpt-5-nano"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = [...cost(10, 10)]
model.time.released = Date.now()
})
})
const selected = yield* catalog.model.small(providerID)
expect(selected?.id).toBe(Model.ID.make("gpt-5-nano"))
}),
)
})
-22
View File
@@ -110,26 +110,4 @@ describe("toSessionError", () => {
expect(eligible.map(SessionRunnerRetry.isRetryable)).toEqual([true, true, true])
expect(ineligible.map(SessionRunnerRetry.isRetryable)).toEqual([false, false, false, false, false, false, false])
})
test("retries transport failures only when delivery is absent or not sent", () => {
const retryable = [
llm(new TransportReason({ message: "http transport" })),
llm(new TransportReason({ message: "connect failed", delivery: "not-sent", phase: "connect" })),
]
const ineligible = [
llm(new TransportReason({ message: "send uncertain", delivery: "ambiguous", phase: "send" })),
llm(new TransportReason({ message: "response interrupted", delivery: "accepted", phase: "receive" })),
llm(
new TransportReason({
message: "continuation rejected",
delivery: "rejected",
recovery: "retry-full",
phase: "receive",
}),
),
]
expect(retryable.map(SessionRunnerRetry.isRetryable)).toEqual([true, true])
expect(ineligible.map(SessionRunnerRetry.isRetryable)).toEqual([false, false, false])
})
})
+1 -3
View File
@@ -41,17 +41,15 @@ describe("Session.log", () => {
it.effect("replays public session events and marks synced at the aggregate watermark", () =>
Effect.gen(function* () {
const session = yield* Session.Service
const bus = yield* Bus.Service
const created = yield* session.create({ location })
yield* session.rename({ sessionID: created.id, title: "session.renamed" })
const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id })))
const watermark = (yield* bus.sequences([created.id])).get(created.id)
// Session creation commits a non-public durable event, so the marker's
// seq covers more of the aggregate than the public events emitted.
expect(items.map((item) => item.type)).toEqual(["session.renamed", "log.synced"])
expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: watermark })
expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: Event.Seq.make(1) })
}),
)
@@ -102,7 +102,6 @@ const promptCatalog = Layer.mock(Catalog.Service, {
all: () => Effect.succeed([]),
available: () => Effect.succeed([]),
default: () => Effect.succeed(undefined),
small: () => Effect.succeed(undefined),
},
})
const runnerLayer = (llmClient: Layer.Layer<typeof LLMClient.Service>) =>
@@ -362,7 +362,6 @@ const promptCatalog = Layer.mock(Catalog.Service, {
all: () => Effect.succeed([]),
available: () => Effect.succeed([]),
default: () => Effect.succeed(undefined),
small: () => Effect.succeed(undefined),
},
})
const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [
-33
View File
@@ -117,9 +117,6 @@ describe("Snapshot", () => {
RelativePath.make("scope/tracked.txt"),
])
const plan = new Map([[RelativePath.make("scope/tracked.txt"), before]])
const preview = yield* snapshot.preview({ files: plan, context: 1 })
expect(preview).toHaveLength(1)
expect(preview[0]?.file).toBe(RelativePath.make("scope/tracked.txt"))
yield* snapshot.restore({ files: plan })
expect(yield* read(path.join(location, "tracked.txt"))).toBe("one\n")
expect(yield* read(path.join(location, "added.txt"))).toBe("added\n")
@@ -185,36 +182,6 @@ describe("Snapshot", () => {
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
testEffect(Layer.empty).live("checks out a legacy revert snapshot without removing unrelated files", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
Effect.gen(function* () {
const project = path.join(tmp.path, "project")
yield* Effect.promise(async () => {
await fs.mkdir(project)
await fs.writeFile(path.join(project, "tracked.txt"), "one\n")
await initGit(project)
})
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const before = yield* snapshot.capture()
expect(before).toBeDefined()
if (!before) return
yield* Effect.promise(async () => {
await fs.writeFile(path.join(project, "tracked.txt"), "two\n")
await fs.writeFile(path.join(project, "unrelated.txt"), "keep\n")
})
yield* snapshot.checkout(before)
expect(yield* read(path.join(project, "tracked.txt"))).toBe("one\n")
expect(yield* read(path.join(project, "unrelated.txt"))).toBe("keep\n")
}).pipe(Effect.provide(snapshotLayer(tmp.path, project)))
}),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
})
function snapshotLayer(data: string, directory: string) {
-17
View File
@@ -2,10 +2,6 @@ export * as ServerAuth from "./auth"
import { Context, Layer, Option, Redacted } from "effect"
export type Credentials = {
password?: string
}
export type DecodedCredentials = {
readonly username: string
readonly password: Redacted.Redacted
@@ -37,16 +33,3 @@ export function authorized(credentials: DecodedCredentials, config: Info) {
Redacted.value(credentials.password) === config.password.value
)
}
export function header(credentials?: Credentials) {
const password = credentials?.password
if (!password) return undefined
return `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
}
export function headers(credentials?: Credentials) {
const authorization = header(credentials)
if (!authorization) return undefined
return { Authorization: authorization }
}
-4
View File
@@ -7,7 +7,3 @@ test("accepts only the fixed opencode username", () => {
expect(ServerAuth.authorized({ username: "opencode", password: Redacted.make("secret") }, config)).toBe(true)
expect(ServerAuth.authorized({ username: "custom", password: Redacted.make("secret") }, config)).toBe(false)
})
test("encodes the fixed opencode username", () => {
expect(ServerAuth.header({ password: "secret" })).toBe(`Basic ${Buffer.from("opencode:secret").toString("base64")}`)
})