diff --git a/.changeset/directory-page-heading.md b/.changeset/directory-page-heading.md deleted file mode 100644 index 686e6c191e7..00000000000 --- a/.changeset/directory-page-heading.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@opencode/core": patch ---- - -Correct directory page headings when the read offset is zero. diff --git a/packages/core/package.json b/packages/core/package.json index 7197ffbd570..18f0aae695a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -66,12 +66,6 @@ "node": "./src/shell/parser-wasm.node.ts", "default": "./src/shell/parser-wasm.bun.ts" }, - "#process-lock-ffi": { - "workerd": "./src/util/process-lock-ffi.workerd.ts", - "bun": "./src/util/process-lock-ffi.bun.ts", - "node": "./src/util/process-lock-ffi.node.ts", - "default": "./src/util/process-lock-ffi.bun.ts" - }, "#v1-migration": { "types": "./src/database/v1-migration.bun.ts", "bun": "./src/database/v1-migration.bun.ts", diff --git a/packages/core/script/build.ts b/packages/core/script/build.ts index 8d06b50ef5d..0416d583d2b 100644 --- a/packages/core/script/build.ts +++ b/packages/core/script/build.ts @@ -26,7 +26,6 @@ const result = await Bun.build({ "#fff", "#photon-wasm", "#shell-parser-wasm", - "#process-lock-ffi", "#v1-migration", ], splitting: true, diff --git a/packages/core/src/file-access.ts b/packages/core/src/file-access.ts index 880bb3b60b7..bae484d5d79 100644 --- a/packages/core/src/file-access.ts +++ b/packages/core/src/file-access.ts @@ -30,12 +30,6 @@ export interface ExternalDirectoryAuthorization { readonly save: string } -export const externalDirectoryPermission = (input: ExternalDirectoryAuthorization) => ({ - action: input.action, - resources: [input.resource], - save: [input.save], -}) - export interface Target { readonly absolute: AbsolutePath /** Location-relative for internal paths, absolute for external paths. */ diff --git a/packages/core/src/location-mutation.ts b/packages/core/src/location-mutation.ts deleted file mode 100644 index 8390377cc8e..00000000000 --- a/packages/core/src/location-mutation.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** @deprecated Use FileAccess for path resolution and authorization. */ -export { FileAccess as LocationMutation } from "./file-access.js" -export * from "./file-access.js" diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 095811cc39a..893c4d215b7 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -8,7 +8,6 @@ import { LocationServiceMap } from "./location-service-map.js" export { LocationServiceMap } from "./location-service-map.js" export type LocationServices = Instance.Services -export type LocationError = Instance.Error export function buildLocationServiceMap( replacements: LayerNode.Replacements = [], diff --git a/packages/core/src/session/error.ts b/packages/core/src/session/error.ts index f4295989fc6..60d6bada1b3 100644 --- a/packages/core/src/session/error.ts +++ b/packages/core/src/session/error.ts @@ -50,15 +50,6 @@ export class StepFailedError extends Schema.TaggedError()("Sess } } -export class UserInterruptedError extends Schema.TaggedError()( - "Session.UserInterruptedError", - {}, -) { - override get message() { - return "Session interrupted by user" - } -} - export class PromptConflictError extends Schema.TaggedError()("Session.PromptConflictError", { sessionID: SessionSchema.ID, messageID: SessionMessage.ID, diff --git a/packages/core/src/session/execution.ts b/packages/core/src/session/execution.ts index 557065f74e0..eeed241ae14 100644 --- a/packages/core/src/session/execution.ts +++ b/packages/core/src/session/execution.ts @@ -12,7 +12,6 @@ import { SessionRunner } from "./runner/index.js" import { SessionSchema } from "./schema.js" import { SessionStore } from "./store.js" import { toSessionError } from "./to-session-error.js" -import { UserInterruptedError } from "./error.js" import { SessionInbox } from "./inbox.js" export interface Interface { @@ -51,9 +50,7 @@ type InterruptReason = "user" | "shutdown" | "inactivity" export function terminal(exit: Exit.Exit, reason?: InterruptReason) { if (Exit.isSuccess(exit)) return { type: "succeeded" as const } if (Cause.hasInterrupts(exit.cause)) return { type: "interrupted" as const, reason: reason ?? "shutdown" } - const failure = Cause.squash(exit.cause) - if (failure instanceof UserInterruptedError) return { type: "interrupted" as const, reason: "user" as const } - return { type: "failed" as const, error: toSessionError(failure) } + return { type: "failed" as const, error: toSessionError(Cause.squash(exit.cause)) } } /** Process-local execution: drains run in this process using the selected instance. */ diff --git a/packages/core/src/session/runner/index.ts b/packages/core/src/session/runner/index.ts index 9b98190311c..644983587d0 100644 --- a/packages/core/src/session/runner/index.ts +++ b/packages/core/src/session/runner/index.ts @@ -4,7 +4,7 @@ import type { AIError } from "@opencode/ai" import { Context, Data, Effect } from "effect" import { SessionSchema } from "../schema.js" import type { Promotable } from "../inbox.js" -import type { AgentNotFoundError, MessageDecodeError, StepFailedError, UserInterruptedError } from "../error.js" +import type { AgentNotFoundError, MessageDecodeError, StepFailedError } from "../error.js" import { SessionRunnerModel } from "./model.js" import type { Instructions } from "../../instructions/index.js" @@ -14,7 +14,6 @@ export type RunError = | MessageDecodeError | AgentNotFoundError | StepFailedError - | UserInterruptedError | Instructions.InitializationBlocked export type Continuation = { readonly step: number } diff --git a/packages/core/src/session/runner/retry.ts b/packages/core/src/session/runner/retry.ts index fa2337bbe9c..473ba2094f1 100644 --- a/packages/core/src/session/runner/retry.ts +++ b/packages/core/src/session/runner/retry.ts @@ -1,6 +1,6 @@ export * as SessionRunnerRetry from "./retry.js" -import { AIError, isContextOverflowFailure } from "@opencode/ai" +import { AIError } from "@opencode/ai" import { Agent } from "@opencode/schema/agent" import { Model } from "@opencode/schema/model" import { SessionError } from "@opencode/schema/session-error" @@ -10,7 +10,6 @@ import type { PluginHooks } from "../../plugin/hooks.js" import { SessionEvent } from "../event.js" import { SessionMessage } from "../message.js" import { SessionSchema } from "../schema.js" -import { toSessionError } from "../to-session-error.js" interface Input { readonly cause: AIError @@ -119,24 +118,6 @@ export const policy = (sessionID: SessionSchema.ID) => }) }) -/** - * Retries one auxiliary request's transient failures under a shared `policy` allowance, letting the - * session retry hook adjust each decision. Context overflow is never transient: callers recover it. - */ -export const transient = - (decide: Effect.Success>, input: Pick) => - (effect: Effect.Effect) => - Effect.retry(effect, { - while: (cause) => - Effect.gen(function* () { - if (isContextOverflowFailure(cause)) return false - const decision = yield* decide({ ...input, cause, error: toSessionError(cause), retry: isRetryable(cause) }) - if (!decision.retry) return false - yield* Effect.sleep(decision.delay) - return true - }), - }) - export const make = (bus: Bus.Interface, sessionID: SessionSchema.ID) => Effect.gen(function* () { const decide = yield* policy(sessionID) diff --git a/packages/core/src/session/to-session-error.ts b/packages/core/src/session/to-session-error.ts index 5a1cf37e7ce..6fd2859fa35 100644 --- a/packages/core/src/session/to-session-error.ts +++ b/packages/core/src/session/to-session-error.ts @@ -3,7 +3,7 @@ import { Tool } from "@opencode/schema/tool" import { SessionError } from "@opencode/schema/session-error" import { Permission } from "../permission.js" import { Integration } from "../integration.js" -import { AgentNotFoundError, StepFailedError, UserInterruptedError } from "./error.js" +import { AgentNotFoundError, StepFailedError } from "./error.js" import { SessionRunnerModel } from "./runner/model.js" export function toSessionError(cause: unknown): SessionError.Error { @@ -51,7 +51,6 @@ export function toSessionError(cause: unknown): SessionError.Error { if (cause instanceof SessionRunnerModel.UnsupportedCompactionError) return { type: "provider.unsupported-operation", message: cause.message } if (cause instanceof AgentNotFoundError) return { type: "unknown", message: cause.message } - if (cause instanceof UserInterruptedError) return { type: "aborted", message: cause.message } if ( cause instanceof SessionRunnerModel.ModelNotSelectedError || cause instanceof SessionRunnerModel.ModelUnavailableError || diff --git a/packages/core/src/util/array.ts b/packages/core/src/util/array.ts deleted file mode 100644 index 1fb8ac69ecd..00000000000 --- a/packages/core/src/util/array.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function findLast( - items: readonly T[], - predicate: (item: T, index: number, items: readonly T[]) => boolean, -): T | undefined { - for (let i = items.length - 1; i >= 0; i -= 1) { - const item = items[i] - if (predicate(item, i, items)) return item - } - return undefined -} diff --git a/packages/core/src/util/process-lock-ffi.bun.ts b/packages/core/src/util/process-lock-ffi.bun.ts deleted file mode 100644 index 3544935e9d6..00000000000 --- a/packages/core/src/util/process-lock-ffi.bun.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { dlopen, read, type Pointer } from "bun:ffi" -import { existsSync } from "node:fs" - -export type LockResult = - | { readonly acquired: true } - | { readonly acquired: false; readonly held: true } - | { readonly acquired: false; readonly held: false; readonly code: number } - -const LOCK_EX = 2 -const LOCK_NB = 4 -const DARWIN_EWOULDBLOCK = 35 -const LINUX_EWOULDBLOCK = 11 - -export function lockDarwin(fd: number): LockResult { - const library = dlopen("/usr/lib/libSystem.B.dylib", { - flock: { args: ["i32", "i32"], returns: "i32" }, - __error: { args: [], returns: "ptr" }, - }) - try { - const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB) - const code = result === 0 ? 0 : errorCode(library.symbols.__error()) - if (result === 0) return { acquired: true } - if (code === DARWIN_EWOULDBLOCK) return { acquired: false, held: true } - return { acquired: false, held: false, code } - } finally { - library.close() - } -} - -export function lockLinux(fd: number): LockResult { - const musl = `/lib/libc.musl-${process.arch === "arm64" ? "aarch64" : "x86_64"}.so.1` - const library = dlopen(existsSync(musl) ? musl : "libc.so.6", { - flock: { args: ["i32", "i32"], returns: "i32" }, - __errno_location: { args: [], returns: "ptr" }, - }) - try { - const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB) - const code = result === 0 ? 0 : errorCode(library.symbols.__errno_location()) - if (result === 0) return { acquired: true } - if (code === LINUX_EWOULDBLOCK) return { acquired: false, held: true } - return { acquired: false, held: false, code } - } finally { - library.close() - } -} - -function errorCode(pointer: Pointer | bigint | null) { - if (pointer === null) throw new Error("Failed to read process lock error code") - return read.i32(pointer, 0) -} diff --git a/packages/core/src/util/process-lock-ffi.node.ts b/packages/core/src/util/process-lock-ffi.node.ts deleted file mode 100644 index 5416bec635d..00000000000 --- a/packages/core/src/util/process-lock-ffi.node.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { dlopen, getInt32 } from "node:ffi" - -export type LockResult = - | { readonly acquired: true } - | { readonly acquired: false; readonly held: true } - | { readonly acquired: false; readonly held: false; readonly code: number } - -const LOCK_EX = 2 -const LOCK_NB = 4 -const DARWIN_EWOULDBLOCK = 35 -const LINUX_EWOULDBLOCK = 11 - -export function lockDarwin(fd: number): LockResult { - const library = dlopen("/usr/lib/libSystem.B.dylib", { - flock: { arguments: ["int32", "int32"], return: "int32" }, - __error: { arguments: [], return: "pointer" }, - }) - try { - const result = library.functions.flock(fd, LOCK_EX | LOCK_NB) - const code = result === 0 ? 0 : getInt32(library.functions.__error(), 0) - if (result === 0) return { acquired: true } - if (code === DARWIN_EWOULDBLOCK) return { acquired: false, held: true } - return { acquired: false, held: false, code } - } finally { - library.lib.close() - } -} - -export function lockLinux(fd: number): LockResult { - const library = dlopen("libc.so.6", { - flock: { arguments: ["int32", "int32"], return: "int32" }, - __errno_location: { arguments: [], return: "pointer" }, - }) - try { - const result = library.functions.flock(fd, LOCK_EX | LOCK_NB) - const code = result === 0 ? 0 : getInt32(library.functions.__errno_location(), 0) - if (result === 0) return { acquired: true } - if (code === LINUX_EWOULDBLOCK) return { acquired: false, held: true } - return { acquired: false, held: false, code } - } finally { - library.lib.close() - } -} diff --git a/packages/core/src/util/process-lock-ffi.workerd.ts b/packages/core/src/util/process-lock-ffi.workerd.ts deleted file mode 100644 index 1549ef42c72..00000000000 --- a/packages/core/src/util/process-lock-ffi.workerd.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type LockResult = - | { readonly acquired: true } - | { readonly acquired: false; readonly held: true } - | { readonly acquired: false; readonly held: false; readonly code: number } - -// workerd has no FFI and no cross-process file locking; a Durable Object is -// already single-threaded per instance, so nothing on this runtime should -// reach these. -const unavailable = (_fd: number): LockResult => { - throw new Error("Process locks are unavailable on the workerd runtime") -} - -export const lockDarwin = unavailable -export const lockLinux = unavailable diff --git a/packages/core/src/util/process-lock.ts b/packages/core/src/util/process-lock.ts deleted file mode 100644 index 7c9a05e8856..00000000000 --- a/packages/core/src/util/process-lock.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { lockDarwin, lockLinux, type LockResult } from "#process-lock-ffi" -import { closeSync, mkdirSync, openSync } from "node:fs" -import { connect, createServer, type Server, type Socket } from "node:net" -import path from "node:path" -import { Effect, Schema } from "effect" -import { Hash } from "@opencode/util/hash" - -export namespace ProcessLock { - export class HeldError extends Schema.TaggedError()("ProcessLockHeldError", { - file: Schema.String, - }) { - override get message() { - return `Process lock is already held: ${this.file}` - } - } - - export class SystemError extends Schema.TaggedError()("ProcessLockSystemError", { - file: Schema.String, - operation: Schema.Literals(["open", "acquire"]), - code: Schema.String, - }) { - override get message() { - return `Process lock ${this.operation} failed for ${this.file}: ${this.code}` - } - } - - export type LockError = HeldError | SystemError - - const acquirePosix = Effect.fnUntraced(function* (file: string) { - const fd = yield* Effect.try({ - try: () => { - mkdirSync(path.dirname(file), { recursive: true }) - return openSync(file, "a+", 0o600) - }, - catch: (cause) => - new SystemError({ - file, - operation: "open", - code: cause instanceof Error ? cause.message : String(cause), - }), - }) - const result = yield* Effect.try({ - try: () => lock(fd), - catch: (cause) => - new SystemError({ - file, - operation: "acquire", - code: cause instanceof Error ? cause.message : String(cause), - }), - }).pipe( - Effect.tapError(() => - Effect.sync(() => { - closeSync(fd) - }), - ), - ) - if (result.acquired) return fd - closeSync(fd) - return yield* result.held - ? new HeldError({ file }) - : new SystemError({ file, operation: "acquire", code: String(result.code) }) - }) - - export const acquire = Effect.fn("ProcessLock.acquire")(function* (file: string) { - if (process.platform === "win32") { - yield* Effect.acquireRelease(acquireWindows(file), closeWindows) - return - } - yield* Effect.acquireRelease(acquirePosix(file), (fd) => - Effect.sync(() => { - closeSync(fd) - }), - ) - }) -} - -function lock(fd: number): LockResult { - if (process.platform === "darwin") return lockDarwin(fd) - if (process.platform === "linux") return lockLinux(fd) - throw new Error(`Unsupported process lock platform: ${process.platform}`) -} - -function acquireWindows(file: string) { - return Effect.callback((resume) => { - const server = createServer() - let probe: Socket | undefined - const pipe = `\\\\.\\pipe\\opencode-process-lock-${Hash.sha256(path.resolve(file).toLowerCase())}` - const onError = (cause: NodeJS.ErrnoException) => { - server.off("listening", onListening) - probe = connect(pipe) - const onProbeError = () => { - probe?.off("connect", onConnect) - resume( - Effect.fail( - new ProcessLock.SystemError({ - file, - operation: "acquire", - code: cause.code ?? cause.message, - }), - ), - ) - } - const onConnect = () => { - probe?.off("error", onProbeError) - probe?.destroy() - resume(Effect.fail(new ProcessLock.HeldError({ file }))) - } - probe.once("connect", onConnect) - probe.once("error", onProbeError) - } - const onListening = () => { - server.off("error", onError) - resume(Effect.succeed(server)) - } - server.once("error", onError) - server.once("listening", onListening) - server.on("connection", (socket) => socket.destroy()) - server.listen(pipe) - return Effect.sync(() => { - probe?.destroy() - server.close() - }) - }) -} - -function closeWindows(server: Server) { - return Effect.callback((resume) => { - if (!server.listening) return resume(Effect.void) - server.close((error) => resume(error ? Effect.die(error) : Effect.void)) - }) -} diff --git a/packages/core/test/fixture/process-lock-worker.ts b/packages/core/test/fixture/process-lock-worker.ts deleted file mode 100644 index 2f8a063b8fd..00000000000 --- a/packages/core/test/fixture/process-lock-worker.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ProcessLock } from "@opencode/core/util/process-lock" -import { Effect, Schema } from "effect" -import fs from "node:fs/promises" - -const input = Schema.decodeUnknownSync( - Schema.fromJsonString(Schema.Struct({ file: Schema.String, ready: Schema.String })), -)(process.argv[2]) - -await Effect.runPromise( - Effect.scoped( - Effect.gen(function* () { - yield* ProcessLock.acquire(input.file) - yield* Effect.promise(() => fs.writeFile(input.ready, String(process.pid))) - return yield* Effect.never - }), - ), -) diff --git a/packages/core/test/session-execution.test.ts b/packages/core/test/session-execution.test.ts index 79887e1a338..063c5fedfbc 100644 --- a/packages/core/test/session-execution.test.ts +++ b/packages/core/test/session-execution.test.ts @@ -15,7 +15,6 @@ import { AbsolutePath } from "@opencode/core/schema" import { Session } from "@opencode/core/session" import { SessionExecution } from "@opencode/core/session/execution" import { SessionRestart } from "@opencode/core/session/execution/restart" -import { UserInterruptedError } from "@opencode/core/session/error" import { SessionEvent } from "@opencode/core/session/event" import { SessionInbox } from "@opencode/core/session/inbox" import { SessionMessage } from "@opencode/core/session/message" @@ -50,10 +49,6 @@ describe("SessionExecution lifecycle", () => { const interrupted = Effect.runSyncExit(Effect.interrupt) expect(SessionExecution.terminal(interrupted)).toEqual({ type: "interrupted", reason: "shutdown" }) expect(SessionExecution.terminal(interrupted, "user")).toEqual({ type: "interrupted", reason: "user" }) - expect(SessionExecution.terminal(Exit.fail(new UserInterruptedError()))).toEqual({ - type: "interrupted", - reason: "user", - }) }) it.effect("the sweep only lists claimed top-level Sessions", () => diff --git a/packages/core/test/util/process-lock.test.ts b/packages/core/test/util/process-lock.test.ts deleted file mode 100644 index eb52b53537d..00000000000 --- a/packages/core/test/util/process-lock.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { expect } from "bun:test" -import { ProcessLock } from "@opencode/core/util/process-lock" -import { Effect } from "effect" -import fs from "node:fs/promises" -import os from "node:os" -import path from "node:path" -import { it } from "../lib/effect" - -const worker = path.join(import.meta.dir, "../fixture/process-lock-worker.ts") - -it.live( - "releases ownership when the scope closes", - Effect.gen(function* () { - const root = yield* temp("opencode-process-lock-") - const file = path.join(root, "service.lock") - yield* Effect.scoped(ProcessLock.acquire(file)) - yield* Effect.scoped(ProcessLock.acquire(file)) - }), -) - -it.live( - "releases ownership when the process dies", - Effect.gen(function* () { - const root = yield* temp("opencode-process-lock-death-") - const file = path.join(root, "service.lock") - const ready = path.join(root, "ready") - const child = yield* Effect.acquireRelease( - Effect.sync(() => - Bun.spawn([process.execPath, worker, JSON.stringify({ file, ready })], { - stdout: "ignore", - stderr: "pipe", - }), - ), - (child) => - Effect.promise(async () => { - kill(child) - await child.exited - }), - ) - yield* Effect.promise(async () => { - for (let attempt = 0; attempt < 100 && !(await Bun.file(ready).exists()); attempt++) await Bun.sleep(20) - }) - expect(yield* Effect.promise(() => Bun.file(ready).exists())).toBe(true) - - const error = yield* Effect.scoped(ProcessLock.acquire(file)).pipe(Effect.flip) - expect(error._tag).toBe("ProcessLockHeldError") - - if (process.platform !== "win32") { - process.kill(child.pid, "SIGSTOP") - const paused = yield* Effect.scoped(ProcessLock.acquire(file)).pipe(Effect.flip) - expect(paused._tag).toBe("ProcessLockHeldError") - process.kill(child.pid, "SIGCONT") - } - - kill(child) - yield* Effect.promise(() => child.exited) - yield* Effect.scoped(ProcessLock.acquire(file)) - }), -) - -function temp(prefix: string) { - return Effect.acquireRelease( - Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), prefix))), - (root) => Effect.promise(() => fs.rm(root, { recursive: true, force: true })), - ) -} - -function kill(child: Bun.Subprocess) { - if (process.platform === "win32") return child.kill() - return child.kill("SIGKILL") -} diff --git a/packages/server/src/workerd.ts b/packages/server/src/workerd.ts index 949ee3fd4d0..5ea075bc88e 100644 --- a/packages/server/src/workerd.ts +++ b/packages/server/src/workerd.ts @@ -23,8 +23,8 @@ import type { ServerOptions } from "./options" * * - Database runs on the injected `DurableObjectStorage` SQLite. * - Watcher and fff are disabled through their existing option flags; pty, fff, - * shell-parser, photon, and process-lock native modules resolve to inert - * stubs under the `workerd` bundle condition. + * shell-parser, and photon native modules resolve to inert stubs under the + * `workerd` bundle condition. * - Bare locations use a typed no-execution-plane process spawner; FileSystem, * FileSystemSearch, and Pty fail with a clear defect until a remote sandbox * backs them; Snapshot and Vcs degrade to no-op results.