diff --git a/AGENTS.md b/AGENTS.md index 44d08ae955eb..dbf8ace12de1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,103 +1,144 @@ -- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`. -- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. -- The default branch in this repo is `dev`. -- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs. -- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility. +# OpenCode AGENTS.md -## Style Guide +## Ground Rules + +- Default branch is `dev`, not `main`. +- Package manager: Bun 1.3.13 (exact). Pre-push hook enforces version match. +- PRs require conventional commit titles (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`) + linked issue (`Fixes #N`). Docs/refactor/feat PRs skip the issue requirement. +- Use `--no-verify` only when CI codegen (`chore: generate`) or user explicitly requests it. +- Run `./script/generate.ts` after changing API routes or SDK surfaces; CI auto-generates on push to `dev`. -### General Principles +## Monorepo Map -- Keep things in one function unless composable or reusable -- Avoid `try`/`catch` where possible -- Avoid using the `any` type -- Use Bun APIs when possible, like `Bun.file()` -- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity -- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream -- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module. +``` +packages/opencode Core server, CLI, TUI (SolidJS + opentui) +packages/app Web UI (SolidJS, Vite) +packages/desktop Tauri desktop app (wraps packages/app) +packages/desktop-electron Electron desktop app +packages/ui Shared UI components +packages/plugin @opencode-ai/plugin +packages/sdk SDK (JS) +packages/console Console app +packages/function Backend functions +packages/identity Auth/identity +packages/enterprise Enterprise features +``` -Reduce total variable count by inlining when a value is only used once. +Dependencies: `@opencode-ai/sdk`, `@opencode-ai/ui`, `@opencode-ai/shared`, `@opencode-ai/plugin` are `workspace:*`. -```ts -// Good -const journal = await Bun.file(path.join(dir, "journal.json")).json() +## Dev Commands -// Bad -const journalPath = path.join(dir, "journal.json") -const journal = await Bun.file(journalPath).json() +```bash +bun install # Install deps (exact versions via bunfig.toml) +bun dev # Run TUI against packages/opencode dir +bun dev # Run TUI against different dir +bun dev serve # Headless API server (port 4096) +bun dev web # Server + proxy web UI (hits production app.opencode.ai) +bun lint # oxlint (type-aware) +bun typecheck # turbo typecheck across all packages ``` -### Destructuring +For local UI dev, run server + app separately: +```bash +# Server (from root or packages/opencode) +bun dev serve +# App (from packages/app) +bun dev -- --port 4444 +# Open http://localhost:4444 +``` -Avoid unnecessary destructuring. Use dot notation to preserve context. +## Testing -```ts -// Good -obj.a -obj.b +**NEVER run tests from repo root.** Guard exists in `bunfig.toml` and `package.json`. -// Bad -const { a, b } = obj -``` +```bash +# Unit tests +bun test # packages/opencode: bun test --timeout 30000 +bun test:unit # packages/app: bun test --preload ./happydom.ts ./src +bun test:unit:watch # packages/app: watch mode -### Variables +# E2E (packages/app, uses Playwright chromium) +bun test:e2e:local # local e2e run -Prefer `const` over `let`. Use ternaries or early returns instead of reassignment. +# CI +bun turbo test:ci # runs all test:ci tasks +``` -```ts -// Good -const foo = condition ? 1 : 2 +Test fixtures (`packages/opencode/test/fixture/fixture.ts`): +- `tmpdir({ git, config, init, dispose })` – creates temp dir, auto-cleans via `await using` +- `testEffect(layers)` – for Effect-based tests; use `it.live()` for real I/O, `it.effect()` for simulated clock +- `provideTmpdirInstance(cb)` – creates temp dir, binds as active Instance, runs Effect, cleans up -// Bad -let foo -if (condition) foo = 1 -else foo = 2 +## Type Checking + +```bash +bun typecheck # from root: turbo typecheck (all packages) +bun typecheck # from packages/opencode: tsgo --noEmit +bun typecheck # from packages/app: tsgo -b ``` -### Control Flow +Never use `tsc` directly. Each package uses `tsgo` (TypeScript native preview) or `--noEmit`. + +## Style Guide -Avoid `else` statements. Prefer early returns. +### General +- Keep logic in one function unless composable/reusable. +- Avoid `try`/`catch`; prefer `.catch(...)`. +- Avoid `any`; prefer precise types. +- Use Bun APIs when available (`Bun.file()`, `Bun.write()`, etc.). +- Prefer `const` over `let`; ternaries over reassignment; early returns over `else`. +- Avoid unnecessary destructuring — use dot notation to preserve context. +- Inline variables used only once. -```ts -// Good -function foo() { - if (condition) return 1 - return 2 -} +### Effect Framework (packages/opencode) +- Use `Effect.gen(function* () { ... })` for composition. +- Use `Effect.fn("Domain.method")` for named/traced effects, `Effect.fnUntraced` for internal. +- No `export namespace Foo {}` — use flat top-level exports + `export * as Foo from "."` at bottom. +- Multi-sibling directories (e.g., `src/session/`, `src/config/`): no barrel `index.ts` — import specific files. +- Use `makeRuntime` for services, `InstanceState` (via `ScopedCache`) for per-directory state. +- `Effect.forkIn(scope)` not `Effect.fork`/`Effect.forkDaemon` (Effect v4 beta). +- Prefer Effect services (`FileSystem`, `HttpClient`, `ChildProcessSpawner`, `Path`, `Clock`) over raw platform APIs. -// Bad -function foo() { - if (condition) return 1 - else return 2 -} -``` +### Database +- Drizzle schema in `src/**/*.sql.ts`. Snake_case tables/columns. +- Migrations: `bun run db generate --name ` (from `packages/opencode`). +- Output: `migration/_/migration.sql`. -### Schema Definitions (Drizzle) +### SolidJS (packages/app, packages/opencode TUI) +- Prefer `createStore` over multiple `createSignal` calls. -Use snake_case for field names so column names don't need to be redefined as strings. +### Desktop Packages +- **Tauri** (`packages/desktop`): Never call `invoke` manually; use generated bindings from `src/bindings.ts`. +- **Electron** (`packages/desktop-electron`): Renderer calls `window.api` only; main process registers IPC in `src/main/ipc.ts`. -```ts -// Good -const table = sqliteTable("session", { - id: text().primaryKey(), - project_id: text().notNull(), - created_at: integer().notNull(), -}) +## Pre-push Hook -// Bad -const table = sqliteTable("session", { - id: text("id").primaryKey(), - projectID: text("project_id").notNull(), - createdAt: integer("created_at").notNull(), -}) -``` +Runs `bun typecheck`. If it fails, push is rejected. Fix type errors locally before pushing. -## Testing +Lint is NOT in the hook — run `bun lint` manually or let CI catch it. -- Avoid mocks as much as possible -- Test actual implementation, do not duplicate logic into tests -- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`. +## CI (GitHub Actions) -## Type Checking +- **test**: Unit (linux + windows, `bun turbo test:ci`) + E2E (Playwright chromium in packages/app) +- **typecheck**: `bun typecheck` +- **generate**: Runs `./script/generate.ts` on push to dev, commits result +- **pr-standards**: Enforces conventional commit titles and linked issues for PRs +- **publish**: Full build pipeline (CLI + Tauri + Electron + npm + Docker) + +## Code Generation + +After changing API routes, server endpoints, or SDK types: +```bash +./script/generate.ts +``` +This regenerates the JS SDK and related files. CI auto-runs this on push to dev. + +## Debugging + +```bash +bun dev spawn # Debug-friendly TUI (server in separate process) +bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts serve --port 4096 +opencode attach http://localhost:4096 # Attach TUI to debugged server +``` -- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly. +For app debugging: NEVER restart the app or server process from within the debugging session. diff --git a/infra/app.ts b/infra/app.ts index bb627f51ec51..7d344e939dfb 100644 --- a/infra/app.ts +++ b/infra/app.ts @@ -41,7 +41,7 @@ export const api = new sst.cloudflare.Worker("Api", { args.migrations = { // Note: when releasing the next tag, make sure all stages use tag v2 oldTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1", - newTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1", + newTag: "v2", //newSqliteClasses: ["SyncServer"], } }, diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts index 448760ae1aaa..92968caa8352 100755 --- a/packages/opencode/script/schema.ts +++ b/packages/opencode/script/schema.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun import { z } from "zod" -import { Config } from "../src/config" +import { Config } from "../src/config/config" import { TuiConfig } from "../src/cli/cmd/tui/config/tui" function generate(schema: z.ZodType) { diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 24bcb9c2d698..8fdd12035c54 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -43,7 +43,7 @@ import { Agent as AgentModule } from "../agent/agent" import { AppRuntime } from "@/effect/app-runtime" import { Installation } from "@/installation" import { MessageV2 } from "@/session/message-v2" -import { Config } from "@/config" +import { Config } from "@/config/config" import { ConfigMCP } from "@/config/mcp" import { Todo } from "@/session/todo" import { Result, Schema } from "effect" diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index 523b03737445..4957e4de411d 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -91,7 +91,6 @@ export class ACPSessionManager { setModel(sessionId: string, model: ACPSessionState["model"]) { const session = this.get(sessionId) session.model = model - this.sessions.set(sessionId, session) return session } @@ -103,14 +102,12 @@ export class ACPSessionManager { setVariant(sessionId: string, variant?: string) { const session = this.get(sessionId) session.variant = variant - this.sessions.set(sessionId, session) return session } setMode(sessionId: string, modeId: string) { const session = this.get(sessionId) session.modeId = modeId - this.sessions.set(sessionId, session) return session } } diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 355718b6bf39..cb3ddbf5575b 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -1,4 +1,4 @@ -import { Config } from "../config" +import { Config } from "../config/config" import z from "zod" import { Provider } from "../provider" import { ModelID, ProviderID } from "../provider/schema" diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 5b4b5120f864..4b835a719961 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -3,6 +3,7 @@ import { Effect, Layer, Record, Result, Schema, Context } from "effect" import { zod } from "@/util/effect-zod" import { Global } from "../global" import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Log } from "@/util" export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" @@ -47,6 +48,8 @@ export interface Interface { readonly remove: (key: string) => Effect.Effect } +const log = Log.create({ service: "auth" }) + export class Service extends Context.Service()("@opencode/Auth") {} export const layer = Layer.effect( @@ -59,7 +62,9 @@ export const layer = Layer.effect( if (process.env.OPENCODE_AUTH_CONTENT) { try { return JSON.parse(process.env.OPENCODE_AUTH_CONTENT) - } catch (err) {} + } catch (err) { + log.warn("failed to parse OPENCODE_AUTH_CONTENT", { error: err }) + } } const data = (yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => ({})))) as Record diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts index 10b6d5c9e2b0..b48319b49466 100644 --- a/packages/opencode/src/cli/cmd/debug/agent.ts +++ b/packages/opencode/src/cli/cmd/debug/agent.ts @@ -3,7 +3,7 @@ import { basename } from "path" import { Effect } from "effect" import { Agent } from "../../../agent/agent" import { Provider } from "../../../provider" -import { Session } from "../../../session" +import { Session } from "../../../session/session" import type { MessageV2 } from "../../../session/message-v2" import { MessageID, PartID } from "../../../session/schema" import { ToolRegistry } from "../../../tool" diff --git a/packages/opencode/src/cli/cmd/debug/config.ts b/packages/opencode/src/cli/cmd/debug/config.ts index b1f1c25e9ccc..59e29c4a3856 100644 --- a/packages/opencode/src/cli/cmd/debug/config.ts +++ b/packages/opencode/src/cli/cmd/debug/config.ts @@ -1,5 +1,5 @@ import { EOL } from "os" -import { Config } from "../../../config" +import { Config } from "../../../config/config" import { AppRuntime } from "@/effect/app-runtime" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" diff --git a/packages/opencode/src/cli/cmd/export.ts b/packages/opencode/src/cli/cmd/export.ts index 06b361c6d5e6..ddab1bd99e35 100644 --- a/packages/opencode/src/cli/cmd/export.ts +++ b/packages/opencode/src/cli/cmd/export.ts @@ -1,5 +1,5 @@ import type { Argv } from "yargs" -import { Session } from "../../session" +import { Session } from "../../session/session" import { MessageV2 } from "../../session/message-v2" import { SessionID } from "../../session/schema" import { cmd } from "./cmd" diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index fe8e233dd176..1ae52124286c 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -22,7 +22,7 @@ import { ModelsDev } from "../../provider" import { Instance } from "@/project/instance" import { bootstrap } from "../bootstrap" import { SessionShare } from "@/share" -import { Session } from "../../session" +import { Session } from "../../session/session" import type { SessionID } from "../../session/schema" import { MessageID, PartID } from "../../session/schema" import { Provider } from "../../provider" diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index 26256a770fd4..e518a3dcdbed 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -1,6 +1,6 @@ import type { Argv } from "yargs" import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2" -import { Session } from "../../session" +import { Session } from "../../session/session" import { MessageV2 } from "../../session/message-v2" import { cmd } from "./cmd" import { bootstrap } from "../bootstrap" diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index a5751ce83667..e2a617222dc2 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -7,7 +7,7 @@ import { UI } from "../ui" import { MCP } from "../../mcp" import { McpAuth } from "../../mcp/auth" import { McpOAuthProvider } from "../../mcp/oauth-provider" -import { Config } from "../../config" +import { Config } from "../../config/config" import { ConfigMCP } from "../../config/mcp" import { Instance } from "../../project/instance" import { Installation } from "../../installation" diff --git a/packages/opencode/src/cli/cmd/plug.ts b/packages/opencode/src/cli/cmd/plug.ts index 9dfda16d6458..42d06ff47fa0 100644 --- a/packages/opencode/src/cli/cmd/plug.ts +++ b/packages/opencode/src/cli/cmd/plug.ts @@ -1,7 +1,7 @@ import { intro, log, outro, spinner } from "@clack/prompts" import type { Argv } from "yargs" -import { ConfigPaths } from "../../config" +import { ConfigPaths } from "../../config/paths" import { Global } from "../../global" import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install" import { resolvePluginTarget } from "../../plugin/shared" diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index e2eb0b65a343..1ed99de939e3 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -7,7 +7,7 @@ import { ModelsDev } from "../../provider" import { map, pipe, sortBy, values } from "remeda" import path from "path" import os from "os" -import { Config } from "../../config" +import { Config } from "../../config/config" import { Global } from "../../global" import { Plugin } from "../../plugin" import { Instance } from "../../project/instance" diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 8537a74d4510..a5171a0e8857 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -1,6 +1,6 @@ import type { Argv } from "yargs" import { cmd } from "./cmd" -import { Session } from "../../session" +import { Session } from "../../session/session" import { SessionID } from "../../session/schema" import { bootstrap } from "../bootstrap" import { UI } from "../ui" diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index 34af56ad7a90..199dfb241eae 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -1,6 +1,6 @@ import type { Argv } from "yargs" import { cmd } from "./cmd" -import { Session } from "../../session" +import { Session } from "../../session/session" import { bootstrap } from "../bootstrap" import { Database } from "../../storage" import { SessionTable } from "../../session/session.sql" diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 30a597b91ebb..62f8f3875923 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -49,7 +49,7 @@ import { DialogAlert } from "./ui/dialog-alert" import { DialogConfirm } from "./ui/dialog-confirm" import { ToastProvider, useToast } from "./ui/toast" import { ExitProvider, useExit } from "./context/exit" -import { Session as SessionApi } from "@/session" +import { Session as SessionApi } from "@/session/session" import { TuiEvent } from "./event" import { KVProvider, useKV } from "./context/kv" import { Provider } from "@/provider" diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index a16c98a9f49d..90874245cc10 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -143,7 +143,9 @@ export async function restoreWorkspaceSession(input: { try { await input.sync.bootstrap({ fatal: false }) - } catch (e) {} + } catch (e) { + log.warn("workspace bootstrap failed during restore", { error: errorData(e) }) + } await Promise.all([input.project.workspace.sync(), input.sync.session.sync(input.sessionID)]).catch((err) => { log.error("session restore refresh failed", { diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 8cec99c615fd..43cc1ed1c852 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -5,7 +5,7 @@ import { Instance } from "@/project/instance" import { InstanceBootstrap } from "@/project/bootstrap" import { Rpc } from "@/util" import { upgrade } from "@/cli/upgrade" -import { Config } from "@/config" +import { Config } from "@/config/config" import { GlobalBus } from "@/bus/global" import { Flag } from "@/flag/flag" import { writeHeapSnapshot } from "node:v8" diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts index a489ea14c5b3..14eeef4d54d3 100644 --- a/packages/opencode/src/cli/network.ts +++ b/packages/opencode/src/cli/network.ts @@ -1,5 +1,5 @@ import type { Argv, InferredOptionTypes } from "yargs" -import { Config } from "../config" +import { Config } from "../config/config" import { AppRuntime } from "@/effect/app-runtime" const options = { diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index a3e3f3013deb..4a8e371d91e4 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -1,5 +1,5 @@ import { Bus } from "@/bus" -import { Config } from "@/config" +import { Config } from "@/config/config" import { AppRuntime } from "@/effect/app-runtime" import { Flag } from "@/flag/flag" import { Installation } from "@/installation" diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 478a12f66465..f3f4c3de8d74 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -5,7 +5,7 @@ import type { InstanceContext } from "@/project/instance" import { SessionID, MessageID } from "@/session/schema" import { Effect, Layer, Context, Schema } from "effect" import z from "zod" -import { Config } from "../config" +import { Config } from "../config/config" import { MCP } from "../mcp" import { Skill } from "../skill" import PROMPT_INITIALIZE from "./template/initialize.txt" diff --git a/packages/opencode/src/config/agent.ts b/packages/opencode/src/config/agent.ts index 2978916b570d..1ad602b8faa9 100644 --- a/packages/opencode/src/config/agent.ts +++ b/packages/opencode/src/config/agent.ts @@ -119,7 +119,7 @@ export async function load(dir: string) { const message = ConfigMarkdown.FrontmatterError.isInstance(err) ? err.data.message : `Failed to parse agent ${item}` - const { Session } = await import("@/session") + const { Session } = await import("@/session/session") void Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load agent", { agent: item, err }) return undefined @@ -156,7 +156,7 @@ export async function loadMode(dir: string) { const message = ConfigMarkdown.FrontmatterError.isInstance(err) ? err.data.message : `Failed to parse mode ${item}` - const { Session } = await import("@/session") + const { Session } = await import("@/session/session") void Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load mode", { mode: item, err }) return undefined diff --git a/packages/opencode/src/config/command.ts b/packages/opencode/src/config/command.ts index 3e0adccc303b..f4d39d2ad072 100644 --- a/packages/opencode/src/config/command.ts +++ b/packages/opencode/src/config/command.ts @@ -36,7 +36,7 @@ export async function load(dir: string) { const message = ConfigMarkdown.FrontmatterError.isInstance(err) ? err.data.message : `Failed to parse command ${item}` - const { Session } = await import("@/session") + const { Session } = await import("@/session/session") void Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load command", { command: item, err }) return undefined diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f1ceb1b4ed39..683e4df45c25 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -789,3 +789,5 @@ export const defaultLayer = layer.pipe( Layer.provide(Account.defaultLayer), Layer.provide(Npm.defaultLayer), ) + +export * as Config from "./config" diff --git a/packages/opencode/src/config/index.ts b/packages/opencode/src/config/index.ts deleted file mode 100644 index a05c29d25ce9..000000000000 --- a/packages/opencode/src/config/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export * as Config from "./config" -export * as ConfigAgent from "./agent" -export * as ConfigCommand from "./command" -export * as ConfigError from "./error" -export * as ConfigFormatter from "./formatter" -export * as ConfigLSP from "./lsp" -export * as ConfigVariable from "./variable" -export { ConfigManaged } from "./managed" -export * as ConfigMarkdown from "./markdown" -export * as ConfigMCP from "./mcp" -export { ConfigModelID } from "./model-id" -export * as ConfigParse from "./parse" -export * as ConfigPermission from "./permission" -export * as ConfigPaths from "./paths" -export * as ConfigProvider from "./provider" -export * as ConfigSkills from "./skills" diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 107f2d9903e6..c60cd32edc51 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -18,7 +18,7 @@ import { getAdaptor } from "./adaptors" import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types" import { WorkspaceID } from "./schema" import { parseSSE } from "./sse" -import { Session } from "@/session" +import { Session } from "@/session/session" import { SessionTable } from "@/session/session.sql" import { SessionID } from "@/session/schema" import { errorData } from "@/util/error" diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index d68e00a323b0..0deb3326aebc 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -6,7 +6,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Bus } from "@/bus" import { Auth } from "@/auth" import { Account } from "@/account/account" -import { Config } from "@/config" +import { Config } from "@/config/config" import { Git } from "@/git" import { Ripgrep } from "@/file/ripgrep" import { File } from "@/file" @@ -22,7 +22,7 @@ import { Discovery } from "@/skill/discovery" import { Question } from "@/question" import { Permission } from "@/permission" import { Todo } from "@/session/todo" -import { Session } from "@/session" +import { Session } from "@/session/session" import { SessionStatus } from "@/session/status" import { SessionRunState } from "@/session/run-state" import { SessionProcessor } from "@/session/processor" diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts index 37698c43a5d6..b86f43874bb5 100644 --- a/packages/opencode/src/effect/bootstrap-runtime.ts +++ b/packages/opencode/src/effect/bootstrap-runtime.ts @@ -9,7 +9,7 @@ import { File } from "@/file" import { Vcs } from "@/project" import { Snapshot } from "@/snapshot" import { Bus } from "@/bus" -import { Config } from "@/config" +import { Config } from "@/config/config" import * as Observability from "./observability" import { memoMap } from "./memo-map" diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 0ac98b9c2d8a..1c54a6d76d1e 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -12,7 +12,7 @@ import { Flag } from "@/flag/flag" import { Git } from "@/git" import { Instance } from "@/project/instance" import { lazy } from "@/util/lazy" -import { Config } from "../config" +import { Config } from "../config/config" import { FileIgnore } from "./ignore" import { Protected } from "./protected" import { Log } from "../util" diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 53a2c10119b1..ca298d03db2b 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -5,7 +5,7 @@ import { InstanceState } from "@/effect" import path from "path" import { mergeDeep } from "remeda" import z from "zod" -import { Config } from "../config" +import { Config } from "../config/config" import { Log } from "../util" import * as Formatter from "./formatter" diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index 27bac598fb75..a0de026f98ec 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -2,9 +2,11 @@ import fs from "fs/promises" import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" import path from "path" import os from "os" -import { Filesystem } from "../util" +import { Filesystem, Log } from "../util" import { Flock } from "@opencode-ai/shared/util/flock" +const log = Log.create({ service: "global" }) + const app = "opencode" const data = path.join(xdgData!, app) @@ -51,7 +53,9 @@ if (version !== CACHE_VERSION) { }), ), ) - } catch {} + } catch (e) { + log.warn("cache directory cleanup failed", { error: e }) + } await Filesystem.write(path.join(Path.cache, "version"), CACHE_VERSION) } diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index 7741ff60e530..6fc2905134a5 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -6,7 +6,7 @@ import path from "path" import { pathToFileURL, fileURLToPath } from "url" import * as LSPServer from "./server" import z from "zod" -import { Config } from "../config" +import { Config } from "../config/config" import { Flag } from "@/flag/flag" import { Process } from "../util" import { spawn as lspspawn } from "./launch" diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 3c6816c5b702..46bc40adafe0 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -9,7 +9,7 @@ import { type Tool as MCPToolDef, ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js" -import { Config } from "../config" +import { Config } from "../config/config" import { ConfigMCP } from "../config/mcp" import { Log } from "../util" import { NamedError } from "@opencode-ai/shared/util/error" @@ -521,7 +521,9 @@ export const layer = Layer.effect( for (const dpid of pids) { try { process.kill(dpid, "SIGTERM") - } catch {} + } catch (e) { + log.debug("process.kill SIGTERM failed", { error: e, pid: dpid }) + } } } yield* Effect.tryPromise(() => client.close()).pipe(Effect.ignore) diff --git a/packages/opencode/src/node.ts b/packages/opencode/src/node.ts index 1cb30d8082fa..2975f7a7b91e 100644 --- a/packages/opencode/src/node.ts +++ b/packages/opencode/src/node.ts @@ -1,4 +1,4 @@ -export { Config } from "./config" +export { Config } from "./config/config" export { Server } from "./server/server" export { bootstrap } from "./cli/bootstrap" export { Log } from "./util" diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index dd2a784694df..c2dcfacd1e1d 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -5,13 +5,13 @@ import type { PluginModule, WorkspaceAdaptor as PluginWorkspaceAdaptor, } from "@opencode-ai/plugin" -import { Config } from "../config" +import { Config } from "../config/config" import { Bus } from "../bus" import { Log } from "../util" import { createOpencodeClient } from "@opencode-ai/sdk" import { Flag } from "../flag/flag" import { CodexAuthPlugin } from "./codex" -import { Session } from "../session" +import { Session } from "../session/session" import { NamedError } from "@opencode-ai/shared/util/error" import { CopilotAuthPlugin } from "./github-copilot/copilot" import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" diff --git a/packages/opencode/src/plugin/loader.ts b/packages/opencode/src/plugin/loader.ts index e61612561bcd..d7fd9927752a 100644 --- a/packages/opencode/src/plugin/loader.ts +++ b/packages/opencode/src/plugin/loader.ts @@ -11,206 +11,206 @@ import { import { ConfigPlugin } from "@/config/plugin" import { InstallationVersion } from "@/installation/version" -export namespace PluginLoader { - // A normalized plugin declaration derived from config before any filesystem or npm work happens. - export type Plan = { - spec: string - options: ConfigPlugin.Options | undefined - deprecated: boolean - } +// A normalized plugin declaration derived from config before any filesystem or npm work happens. +export type Plan = { + spec: string + options: ConfigPlugin.Options | undefined + deprecated: boolean +} - // A plugin that has been resolved to a concrete target and entrypoint on disk. - export type Resolved = Plan & { - source: PluginSource - target: string - entry: string - pkg?: PluginPackage - } +// A plugin that has been resolved to a concrete target and entrypoint on disk. +export type Resolved = Plan & { + source: PluginSource + target: string + entry: string + pkg?: PluginPackage +} - // A plugin target we could inspect, but which does not expose the requested kind of entrypoint. - export type Missing = Plan & { - source: PluginSource - target: string - pkg?: PluginPackage - message: string - } +// A plugin target we could inspect, but which does not expose the requested kind of entrypoint. +export type Missing = Plan & { + source: PluginSource + target: string + pkg?: PluginPackage + message: string +} - // A resolved plugin whose module has been imported successfully. - export type Loaded = Resolved & { - mod: Record - } +// A resolved plugin whose module has been imported successfully. +export type Loaded = Resolved & { + mod: Record +} - type Candidate = { origin: ConfigPlugin.Origin; plan: Plan } - type Report = { - // Called before each attempt so callers can log initial load attempts and retries uniformly. - start?: (candidate: Candidate, retry: boolean) => void - // Called when the package exists but does not provide the requested entrypoint. - missing?: (candidate: Candidate, retry: boolean, message: string, resolved: Missing) => void - // Called for operational failures such as install, compatibility, or dynamic import errors. - error?: ( - candidate: Candidate, - retry: boolean, - stage: "install" | "entry" | "compatibility" | "load", - error: unknown, - resolved?: Resolved, - ) => void - } +type Candidate = { origin: ConfigPlugin.Origin; plan: Plan } +type Report = { + // Called before each attempt so callers can log initial load attempts and retries uniformly. + start?: (candidate: Candidate, retry: boolean) => void + // Called when the package exists but does not provide the requested entrypoint. + missing?: (candidate: Candidate, retry: boolean, message: string, resolved: Missing) => void + // Called for operational failures such as install, compatibility, or dynamic import errors. + error?: ( + candidate: Candidate, + retry: boolean, + stage: "install" | "entry" | "compatibility" | "load", + error: unknown, + resolved?: Resolved, + ) => void +} - // Normalize a config item into the loader's internal representation. - function plan(item: ConfigPlugin.Spec): Plan { - const spec = ConfigPlugin.pluginSpecifier(item) - return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) } - } +// Normalize a config item into the loader's internal representation. +function plan(item: ConfigPlugin.Spec): Plan { + const spec = ConfigPlugin.pluginSpecifier(item) + return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) } +} - // Resolve a configured plugin into a concrete entrypoint that can later be imported. - // - // The stages here intentionally separate install/target resolution, entrypoint detection, - // and compatibility checks so callers can report the exact reason a plugin was skipped. - export async function resolve( - plan: Plan, - kind: PluginKind, - ): Promise< - | { ok: true; value: Resolved } - | { ok: false; stage: "missing"; value: Missing } - | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown } - > { - // First make sure the plugin exists locally, installing npm plugins on demand. - let target = "" - try { - target = await resolvePluginTarget(plan.spec) - } catch (error) { - return { ok: false, stage: "install", error } +// Resolve a configured plugin into a concrete entrypoint that can later be imported. +// +// The stages here intentionally separate install/target resolution, entrypoint detection, +// and compatibility checks so callers can report the exact reason a plugin was skipped. +export async function resolve( + plan: Plan, + kind: PluginKind, +): Promise< + | { ok: true; value: Resolved } + | { ok: false; stage: "missing"; value: Missing } + | { ok: false; stage: "install" | "entry" | "compatibility"; error: unknown } +> { + // First make sure the plugin exists locally, installing npm plugins on demand. + let target = "" + try { + target = await resolvePluginTarget(plan.spec) + } catch (error) { + return { ok: false, stage: "install", error } + } + if (!target) return { ok: false, stage: "install", error: new Error(`Plugin ${plan.spec} target is empty`) } + + // Then inspect the target for the requested server/tui entrypoint. + let base + try { + base = await createPluginEntry(plan.spec, target, kind) + } catch (error) { + return { ok: false, stage: "entry", error } + } + if (!base.entry) + return { + ok: false, + stage: "missing", + value: { + ...plan, + source: base.source, + target: base.target, + pkg: base.pkg, + message: `Plugin ${plan.spec} does not expose a ${kind} entrypoint`, + }, } - if (!target) return { ok: false, stage: "install", error: new Error(`Plugin ${plan.spec} target is empty`) } - // Then inspect the target for the requested server/tui entrypoint. - let base + // npm plugins can declare which opencode versions they support; file plugins are treated + // as local development code and skip this compatibility gate. + if (base.source === "npm") { try { - base = await createPluginEntry(plan.spec, target, kind) + await checkPluginCompatibility(base.target, InstallationVersion, base.pkg) } catch (error) { - return { ok: false, stage: "entry", error } - } - if (!base.entry) - return { - ok: false, - stage: "missing", - value: { - ...plan, - source: base.source, - target: base.target, - pkg: base.pkg, - message: `Plugin ${plan.spec} does not expose a ${kind} entrypoint`, - }, - } - - // npm plugins can declare which opencode versions they support; file plugins are treated - // as local development code and skip this compatibility gate. - if (base.source === "npm") { - try { - await checkPluginCompatibility(base.target, InstallationVersion, base.pkg) - } catch (error) { - return { ok: false, stage: "compatibility", error } - } + return { ok: false, stage: "compatibility", error } } - return { ok: true, value: { ...plan, source: base.source, target: base.target, entry: base.entry, pkg: base.pkg } } } + return { ok: true, value: { ...plan, source: base.source, target: base.target, entry: base.entry, pkg: base.pkg } } +} - // Import the resolved module only after all earlier validation has succeeded. - export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> { - let mod - try { - mod = await import(row.entry) - } catch (error) { - return { ok: false, error } - } - if (!mod) return { ok: false, error: new Error(`Plugin ${row.spec} module is empty`) } - return { ok: true, value: { ...row, mod } } +// Import the resolved module only after all earlier validation has succeeded. +export async function load(row: Resolved): Promise<{ ok: true; value: Loaded } | { ok: false; error: unknown }> { + let mod + try { + mod = await import(row.entry) + } catch (error) { + return { ok: false, error } } + if (!mod) return { ok: false, error: new Error(`Plugin ${row.spec} module is empty`) } + return { ok: true, value: { ...row, mod } } +} - // Run one candidate through the full pipeline: resolve, optionally surface a missing entry, - // import the module, and finally let the caller transform the loaded plugin into any result type. - async function attempt( - candidate: Candidate, - kind: PluginKind, - retry: boolean, - finish: ((load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise) | undefined, - missing: ((value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise) | undefined, - report: Report | undefined, - ): Promise { - const plan = candidate.plan - - // Deprecated plugin packages are silently ignored because they are now built in. - if (plan.deprecated) return - - report?.start?.(candidate, retry) - - const resolved = await resolve(plan, kind) - if (!resolved.ok) { - if (resolved.stage === "missing") { - // Missing entrypoints are handled separately so callers can still inspect package metadata, - // for example to load theme files from a tui plugin package that has no code entrypoint. - if (missing) { - const value = await missing(resolved.value, candidate.origin, retry) - if (value !== undefined) return value - } - report?.missing?.(candidate, retry, resolved.value.message, resolved.value) - return +// Run one candidate through the full pipeline: resolve, optionally surface a missing entry, +// import the module, and finally let the caller transform the loaded plugin into any result type. +async function attempt( + candidate: Candidate, + kind: PluginKind, + retry: boolean, + finish: ((load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise) | undefined, + missing: ((value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise) | undefined, + report: Report | undefined, +): Promise { + const plan = candidate.plan + + // Deprecated plugin packages are silently ignored because they are now built in. + if (plan.deprecated) return + + report?.start?.(candidate, retry) + + const resolved = await resolve(plan, kind) + if (!resolved.ok) { + if (resolved.stage === "missing") { + // Missing entrypoints are handled separately so callers can still inspect package metadata, + // for example to load theme files from a tui plugin package that has no code entrypoint. + if (missing) { + const value = await missing(resolved.value, candidate.origin, retry) + if (value !== undefined) return value } - report?.error?.(candidate, retry, resolved.stage, resolved.error) + report?.missing?.(candidate, retry, resolved.value.message, resolved.value) return } - - const loaded = await load(resolved.value) - if (!loaded.ok) { - report?.error?.(candidate, retry, "load", loaded.error, resolved.value) - return - } - - // The default behavior is to return the successfully loaded plugin as-is, but callers can - // provide a finisher to adapt the result into a more specific runtime shape. - if (!finish) return loaded.value as R - return finish(loaded.value, candidate.origin, retry) + report?.error?.(candidate, retry, resolved.stage, resolved.error) + return } - type Input = { - items: ConfigPlugin.Origin[] - kind: PluginKind - wait?: () => Promise - finish?: (load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise - missing?: (value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise - report?: Report + const loaded = await load(resolved.value) + if (!loaded.ok) { + report?.error?.(candidate, retry, "load", loaded.error, resolved.value) + return } - // Resolve and load all configured plugins in parallel. - // - // If `wait` is provided, file-based plugins that initially failed are retried once after the - // caller finishes preparing dependencies. This supports local plugins that depend on an install - // step happening elsewhere before their entrypoint becomes loadable. - export async function loadExternal(input: Input): Promise { - const candidates = input.items.map((origin) => ({ origin, plan: plan(origin.spec) })) - const list: Array> = [] - for (const candidate of candidates) { - list.push(attempt(candidate, input.kind, false, input.finish, input.missing, input.report)) - } - const out = await Promise.all(list) - if (input.wait) { - let deps: Promise | undefined - for (let i = 0; i < candidates.length; i++) { - if (out[i] !== undefined) continue - - // Only local file plugins are retried. npm plugins already attempted installation during - // the first pass, while file plugins may need the caller's dependency preparation to finish. - const candidate = candidates[i] - if (!candidate || pluginSource(candidate.plan.spec) !== "file") continue - deps ??= input.wait() - await deps - out[i] = await attempt(candidate, input.kind, true, input.finish, input.missing, input.report) - } - } + // The default behavior is to return the successfully loaded plugin as-is, but callers can + // provide a finisher to adapt the result into a more specific runtime shape. + if (!finish) return loaded.value as R + return finish(loaded.value, candidate.origin, retry) +} + +type Input = { + items: ConfigPlugin.Origin[] + kind: PluginKind + wait?: () => Promise + finish?: (load: Loaded, origin: ConfigPlugin.Origin, retry: boolean) => Promise + missing?: (value: Missing, origin: ConfigPlugin.Origin, retry: boolean) => Promise + report?: Report +} - // Drop skipped/failed entries while preserving the successful result order. - const ready: R[] = [] - for (const item of out) if (item !== undefined) ready.push(item) - return ready +// Resolve and load all configured plugins in parallel. +// +// If `wait` is provided, file-based plugins that initially failed are retried once after the +// caller finishes preparing dependencies. This supports local plugins that depend on an install +// step happening elsewhere before their entrypoint becomes loadable. +export async function loadExternal(input: Input): Promise { + const candidates = input.items.map((origin) => ({ origin, plan: plan(origin.spec) })) + const list: Array> = [] + for (const candidate of candidates) { + list.push(attempt(candidate, input.kind, false, input.finish, input.missing, input.report)) + } + const out = await Promise.all(list) + if (input.wait) { + let deps: Promise | undefined + for (let i = 0; i < candidates.length; i++) { + if (out[i] !== undefined) continue + + // Only local file plugins are retried. npm plugins already attempted installation during + // the first pass, while file plugins may need the caller's dependency preparation to finish. + const candidate = candidates[i] + if (!candidate || pluginSource(candidate.plan.spec) !== "file") continue + deps ??= input.wait() + await deps + out[i] = await attempt(candidate, input.kind, true, input.finish, input.missing, input.report) + } } + + // Drop skipped/failed entries while preserving the successful result order. + const ready: R[] = [] + for (const item of out) if (item !== undefined) ready.push(item) + return ready } + +export * as PluginLoader from "./loader" diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index a7c071a9f80b..9d4ac422adb0 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -12,7 +12,7 @@ import { Log } from "@/util" import { FileWatcher } from "@/file/watcher" import { ShareNext } from "@/share" import * as Effect from "effect/Effect" -import { Config } from "@/config" +import { Config } from "@/config/config" export const InstanceBootstrap = Effect.gen(function* () { Log.Default.info("bootstrapping", { directory: Instance.directory }) diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index a4f629caf3bb..001e5b0e2603 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -1,8 +1,11 @@ import { APICallError } from "ai" import { STATUS_CODES } from "http" import { iife } from "@/util/iife" +import { Log } from "@/util" import type { ProviderID } from "./schema" +const log = Log.create({ service: "provider.error" }) + // Adapted from overflow detection patterns in: // https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts const OVERFLOW_PATTERNS = [ @@ -68,7 +71,9 @@ function message(providerID: ProviderID, e: APICallError) { if (errMsg && typeof errMsg === "string") { return `${msg}: ${errMsg}` } - } catch {} + } catch (e) { + log.debug("JSON.parse of responseBody failed", { error: e }) + } // If responseBody is HTML (e.g. from a gateway or proxy error page), // provide a human-readable message instead of dumping raw markup diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 0fe53e6e47f0..fc8a4f723cfd 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1,6 +1,6 @@ import os from "os" import fuzzysort from "fuzzysort" -import { Config } from "../config" +import { Config } from "../config/config" import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda" import { NoSuchModelError, type Provider as SDK } from "ai" import { Log } from "../util" diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index 604fa77fbb8a..d18d0f4e5244 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -122,11 +122,15 @@ export const layer = Layer.effect( function teardown(session: Active) { try { session.process.kill() - } catch {} + } catch (e) { + log.debug("process.kill failed", { error: e }) + } for (const [sub, ws] of session.subscribers.entries()) { try { if (sock(ws) === sub) ws.close() - } catch {} + } catch (e) { + log.debug("ws.close failed", { error: e }) + } } session.subscribers.clear() } diff --git a/packages/opencode/src/server/mdns.ts b/packages/opencode/src/server/mdns.ts index 580456754d5a..cf75f099b55f 100644 --- a/packages/opencode/src/server/mdns.ts +++ b/packages/opencode/src/server/mdns.ts @@ -36,7 +36,9 @@ export function publish(port: number, domain?: string) { if (bonjour) { try { bonjour.destroy() - } catch {} + } catch (err) { + log.warn("bonjour.destroy() failed", { error: err }) + } } bonjour = undefined currentPort = undefined diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index b67d15f55045..6d76dda7cb1f 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -1,7 +1,7 @@ import { Provider } from "../provider" import { NamedError } from "@opencode-ai/shared/util/error" import { NotFoundError } from "../storage" -import { Session } from "../session" +import { Session } from "../session/session" import type { ContentfulStatusCode } from "hono/utils/http-status" import type { ErrorHandler, MiddlewareHandler } from "hono" import { HTTPException } from "hono/http-exception" diff --git a/packages/opencode/src/server/projectors.ts b/packages/opencode/src/server/projectors.ts index 18c273d587bf..288b013b9f2c 100644 --- a/packages/opencode/src/server/projectors.ts +++ b/packages/opencode/src/server/projectors.ts @@ -1,6 +1,6 @@ import sessionProjectors from "../session/projectors" import { SyncEvent } from "@/sync" -import { Session } from "@/session" +import { Session } from "@/session/session" import { SessionTable } from "@/session/session.sql" import { Database, eq } from "@/storage" diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts index a1199a46911a..9c2ed537e711 100644 --- a/packages/opencode/src/server/routes/global.ts +++ b/packages/opencode/src/server/routes/global.ts @@ -13,7 +13,7 @@ import { Installation } from "@/installation" import { InstallationVersion } from "@/installation/version" import { Log } from "../../util" import { lazy } from "../../util/lazy" -import { Config } from "../../config" +import { Config } from "../../config/config" import { errors } from "../error" const log = Log.create({ service: "server" }) diff --git a/packages/opencode/src/server/routes/instance/config.ts b/packages/opencode/src/server/routes/instance/config.ts index 88e5feef9d19..412aaf169ef9 100644 --- a/packages/opencode/src/server/routes/instance/config.ts +++ b/packages/opencode/src/server/routes/instance/config.ts @@ -1,7 +1,7 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" -import { Config } from "@/config" +import { Config } from "@/config/config" import { Provider } from "@/provider" import { errors } from "../../error" import { lazy } from "@/util/lazy" diff --git a/packages/opencode/src/server/routes/instance/experimental.ts b/packages/opencode/src/server/routes/instance/experimental.ts index f13003cb4e59..ae185f655535 100644 --- a/packages/opencode/src/server/routes/instance/experimental.ts +++ b/packages/opencode/src/server/routes/instance/experimental.ts @@ -8,8 +8,8 @@ import { Worktree } from "@/worktree" import { Instance } from "@/project/instance" import { Project } from "@/project" import { MCP } from "@/mcp" -import { Session } from "@/session" -import { Config } from "@/config" +import { Session } from "@/session/session" +import { Config } from "@/config/config" import { ConsoleState } from "@/config/console-state" import { Account } from "@/account/account" import { AccountID, OrgID } from "@/account/schema" diff --git a/packages/opencode/src/server/routes/instance/httpapi/config.ts b/packages/opencode/src/server/routes/instance/httpapi/config.ts index fcdf6d1a33af..5de81878e455 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/config.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/config.ts @@ -1,4 +1,4 @@ -import { Config } from "@/config" +import { Config } from "@/config/config" import { Provider } from "@/provider" import { Effect, Layer } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" diff --git a/packages/opencode/src/server/routes/instance/httpapi/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/provider.ts index dd1a21d2b0d3..bcdf76059749 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/provider.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/provider.ts @@ -1,5 +1,5 @@ import { ProviderAuth } from "@/provider" -import { Config } from "@/config" +import { Config } from "@/config/config" import { ModelsDev } from "@/provider" import { Provider } from "@/provider" import { ProviderID } from "@/provider/schema" diff --git a/packages/opencode/src/server/routes/instance/provider.ts b/packages/opencode/src/server/routes/instance/provider.ts index 617980e39ca1..effa18cfda3a 100644 --- a/packages/opencode/src/server/routes/instance/provider.ts +++ b/packages/opencode/src/server/routes/instance/provider.ts @@ -1,7 +1,7 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" -import { Config } from "@/config" +import { Config } from "@/config/config" import { Provider } from "@/provider" import { ModelsDev } from "@/provider" import { ProviderAuth } from "@/provider" diff --git a/packages/opencode/src/server/routes/instance/session.ts b/packages/opencode/src/server/routes/instance/session.ts index 4f4f8ed86e7f..5cbce4c031a6 100644 --- a/packages/opencode/src/server/routes/instance/session.ts +++ b/packages/opencode/src/server/routes/instance/session.ts @@ -3,7 +3,7 @@ import { stream } from "hono/streaming" import { describeRoute, validator, resolver } from "hono-openapi" import { SessionID, MessageID, PartID } from "@/session/schema" import z from "zod" -import { Session } from "@/session" +import { Session } from "@/session/session" import { MessageV2 } from "@/session/message-v2" import { SessionPrompt } from "@/session/prompt" import { SessionRunState } from "@/session/run-state" diff --git a/packages/opencode/src/server/routes/instance/tui.ts b/packages/opencode/src/server/routes/instance/tui.ts index 932cf509eb73..8e7d0b70d219 100644 --- a/packages/opencode/src/server/routes/instance/tui.ts +++ b/packages/opencode/src/server/routes/instance/tui.ts @@ -3,7 +3,7 @@ import { describeRoute, validator, resolver } from "hono-openapi" import { Schema } from "effect" import z from "zod" import { Bus } from "@/bus" -import { Session } from "@/session" +import { Session } from "@/session/session" import type { SessionID } from "@/session/schema" import { TuiEvent } from "@/cli/cmd/tui/event" import { zodObject } from "@/util/effect-zod" diff --git a/packages/opencode/src/server/workspace.ts b/packages/opencode/src/server/workspace.ts index d30a117d6a4a..35bcbb32f95c 100644 --- a/packages/opencode/src/server/workspace.ts +++ b/packages/opencode/src/server/workspace.ts @@ -7,7 +7,7 @@ import { Workspace } from "@/control-plane/workspace" import { Flag } from "@/flag/flag" import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" -import { Session } from "@/session" +import { Session } from "@/session/session" import { SessionID } from "@/session/schema" import { AppRuntime } from "@/effect/app-runtime" import { Effect } from "effect" diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index dc126e6837d5..8a48299cb48b 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -10,7 +10,7 @@ import { Log } from "../util" import { SessionProcessor } from "./processor" import { Agent } from "@/agent/agent" import { Plugin } from "@/plugin" -import { Config } from "@/config" +import { Config } from "@/config/config" import { NotFoundError } from "@/storage" import { ModelID, ProviderID } from "@/provider/schema" import { Effect, Layer, Context, Schema } from "effect" diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts deleted file mode 100644 index 1b79fd01a483..000000000000 --- a/packages/opencode/src/session/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as Session from "./session" diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 122644c1fd89..a6e6af279683 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -2,7 +2,7 @@ import os from "os" import path from "path" import { Effect, Layer, Context } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" -import { Config } from "@/config" +import { Config } from "@/config/config" import { InstanceState } from "@/effect" import { Flag } from "@/flag/flag" import { AppFileSystem } from "@opencode-ai/shared/filesystem" diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index b72f873de01d..e0f9b5dd274a 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -6,7 +6,7 @@ import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, json import { mergeDeep, pipe } from "remeda" import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" import { ProviderTransform } from "@/provider" -import { Config } from "@/config" +import { Config } from "@/config/config" import { Instance } from "@/project/instance" import type { Agent } from "@/agent/agent" import type { MessageV2 } from "./message-v2" @@ -255,8 +255,8 @@ const live: Layer.Layer< metadata: typeof result === "object" ? result?.metadata : undefined, title: typeof result === "object" ? result?.title : undefined, } - } catch (e: any) { - return { result: "", error: e.message ?? String(e) } + } catch (e: unknown) { + return { result: "", error: e instanceof Error ? e.message : String(e) } } } diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index d04645b7360c..f0e542341ac2 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -20,6 +20,9 @@ import { zod, ZodOverride } from "@/util/effect-zod" import { NonNegativeInt, withStatics } from "@/util/schema" import { namedSchemaError } from "@/util/named-schema-error" import { EffectLogger } from "@/effect" +import { Log } from "@/util" + +const log = Log.create({ service: "message-v2" }) /** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */ interface FetchDecompressionError extends Error { @@ -1188,7 +1191,9 @@ export function fromError( }, ).toObject() } - } catch {} + } catch (e) { + log.debug("failed to parse stream error", { error: e }) + } return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject() } } diff --git a/packages/opencode/src/session/overflow.ts b/packages/opencode/src/session/overflow.ts index 477b5815b2f3..c74ce5bddf8f 100644 --- a/packages/opencode/src/session/overflow.ts +++ b/packages/opencode/src/session/overflow.ts @@ -1,4 +1,4 @@ -import type { Config } from "@/config" +import type { Config } from "@/config/config" import type { Provider } from "@/provider" import { ProviderTransform } from "@/provider" import type { MessageV2 } from "./message-v2" diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 21f9329c6fce..c961f4caf41f 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -2,7 +2,7 @@ import { Cause, Deferred, Effect, Layer, Context, Scope } from "effect" import * as Stream from "effect/Stream" import { Agent } from "@/agent/agent" import { Bus } from "@/bus" -import { Config } from "@/config" +import { Config } from "@/config/config" import { Permission } from "@/permission" import { Plugin } from "@/plugin" import { Snapshot } from "@/snapshot" diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 3d07a96ecdc5..e04caccfebc1 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -31,7 +31,7 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import * as Stream from "effect/Stream" import { Command } from "../command" import { pathToFileURL, fileURLToPath } from "url" -import { ConfigMarkdown } from "../config" +import { ConfigMarkdown } from "../config/markdown" import { SessionSummary } from "./summary" import { NamedError } from "@opencode-ai/shared/util/error" import { SessionProcessor } from "./processor" diff --git a/packages/opencode/src/session/run-state.ts b/packages/opencode/src/session/run-state.ts index 7a106f8a4ca4..aa8a45f6ac7f 100644 --- a/packages/opencode/src/session/run-state.ts +++ b/packages/opencode/src/session/run-state.ts @@ -54,7 +54,11 @@ export const layer = Layer.effect( if (existing) return existing const next = Runner.make(data.scope, { onIdle: Effect.gen(function* () { - data.runners.delete(sessionID) + yield* Effect.sync(() => { + const r = data.runners.get(sessionID) + data.runners.delete(sessionID) + return r + }) yield* status.set(sessionID, { type: "idle" }) }), onBusy: status.set(sessionID, { type: "busy" }), diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index f4fe3bf8bd73..2d549e5c3979 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -506,6 +506,7 @@ export const layer: Layer.Layer = SyncEvent.remove(sessionID) }) } catch (e) { + log.warn("session remove failed", { error: e }) log.error(e) } }) @@ -849,3 +850,5 @@ export function* listGlobal(input?: { yield { ...fromRow(row), project } } } + +export * as Session from "./session" diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts index 63b76707858d..8ed15bdb008c 100644 --- a/packages/opencode/src/share/session.ts +++ b/packages/opencode/src/share/session.ts @@ -1,8 +1,8 @@ -import { Session } from "@/session" +import { Session } from "@/session/session" import { SessionID } from "@/session/schema" import { SyncEvent } from "@/sync" import { Effect, Layer, Scope, Context } from "effect" -import { Config } from "../config" +import { Config } from "../config/config" import { Flag } from "../flag/flag" import * as ShareNext from "./share-next" diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index f26a085c222c..8ddfb8b99bc0 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -6,11 +6,11 @@ import { Bus } from "@/bus" import { InstanceState } from "@/effect" import { Provider } from "@/provider" import { ModelID, ProviderID } from "@/provider/schema" -import { Session } from "@/session" +import { Session } from "@/session/session" import { MessageV2 } from "@/session/message-v2" import type { SessionID } from "@/session/schema" import { Database, eq } from "@/storage" -import { Config } from "@/config" +import { Config } from "@/config/config" import { Log } from "@/util" import { SessionShareTable } from "./share.sql" diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index dd5cc4e5d5b3..d42df095dc62 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -11,8 +11,8 @@ import { Flag } from "@/flag/flag" import { Global } from "@/global" import { Permission } from "@/permission" import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Config } from "../config" -import { ConfigMarkdown } from "../config" +import { Config } from "../config/config" +import { ConfigMarkdown } from "../config/markdown" import { Glob } from "@opencode-ai/shared/util/glob" import { Log } from "../util" import { Discovery } from "./discovery" @@ -81,7 +81,7 @@ const add = Effect.fnUntraced(function* (state: State, match: string, bus: Bus.I const message = ConfigMarkdown.FrontmatterError.isInstance(err) ? err.data.message : `Failed to parse skill ${match}` - const { Session } = yield* Effect.promise(() => import("@/session")) + const { Session } = yield* Effect.promise(() => import("@/session/session")) yield* bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) log.error("failed to load skill", { skill: match, err }) return undefined diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index ddc4cb29eac1..550e22f327a3 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -7,7 +7,7 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { InstanceState } from "@/effect" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { Hash } from "@opencode-ai/shared/util/hash" -import { Config } from "../config" +import { Config } from "../config/config" import { Global } from "../global" import { Log } from "../util" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index cfff5a0a30b5..93462e2227aa 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -33,9 +33,24 @@ function convertToLineEnding(text: string, ending: "\n" | "\r\n"): string { } const locks = new Map() +const evictionTimers = new Map>() + +function scheduleEviction(resolvedFilePath: string) { + const timer = setTimeout(() => { + locks.delete(resolvedFilePath) + evictionTimers.delete(resolvedFilePath) + }, 60_000) + evictionTimers.set(resolvedFilePath, timer) +} function lock(filePath: string) { const resolvedFilePath = AppFileSystem.resolve(filePath) + const existingTimer = evictionTimers.get(resolvedFilePath) + if (existingTimer) { + clearTimeout(existingTimer) + evictionTimers.delete(resolvedFilePath) + } + const hit = locks.get(resolvedFilePath) if (hit) return hit @@ -166,6 +181,7 @@ export const EditTool = Tool.define( ) }).pipe(Effect.orDie), ) + scheduleEviction(AppFileSystem.resolve(filePath)) let additions = 0 let deletions = 0 diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts index 8e2f11360eba..a451338d9911 100644 --- a/packages/opencode/src/tool/plan.ts +++ b/packages/opencode/src/tool/plan.ts @@ -2,7 +2,7 @@ import path from "path" import { Effect, Schema } from "effect" import * as Tool from "./tool" import { Question } from "../question" -import { Session } from "../session" +import { Session } from "../session/session" import { MessageV2 } from "../session/message-v2" import { Provider } from "../provider" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 539ad632020c..5ac0d24aa2c9 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -1,5 +1,5 @@ import { PlanExitTool } from "./plan" -import { Session } from "../session" +import { Session } from "../session/session" import { QuestionTool } from "./question" import { BashTool } from "./bash" import { EditTool } from "./edit" @@ -13,7 +13,7 @@ import { WriteTool } from "./write" import { InvalidTool } from "./invalid" import { SkillTool } from "./skill" import * as Tool from "./tool" -import { Config } from "../config" +import { Config } from "../config/config" import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin" import { Schema } from "effect" import z from "zod" diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 5cb0dc6a8361..af081e3f59b0 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -1,11 +1,11 @@ import * as Tool from "./tool" import DESCRIPTION from "./task.txt" -import { Session } from "../session" +import { Session } from "../session/session" import { SessionID, MessageID } from "../session/schema" import { MessageV2 } from "../session/message-v2" import { Agent } from "../agent/agent" import type { SessionPrompt } from "../session/prompt" -import { Config } from "../config" +import { Config } from "../config/config" import { Effect, Schema } from "effect" export interface TaskPromptOps { diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index e0d846858ee6..40cc7af7f096 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -4,7 +4,7 @@ import path from "path" import type { Agent } from "../agent/agent" import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { evaluate } from "@/permission/evaluate" -import { Config } from "../config" +import { Config } from "../config/config" import { Identifier } from "../id/id" import { Log } from "../util" import { ToolID } from "./schema" diff --git a/packages/opencode/src/util/error.ts b/packages/opencode/src/util/error.ts index fbda2dc50e02..c53ec4d3f990 100644 --- a/packages/opencode/src/util/error.ts +++ b/packages/opencode/src/util/error.ts @@ -1,4 +1,7 @@ import { isRecord } from "./record" +import { Log } from "@/util" + +const log = Log.create({ service: "util.error" }) export function errorFormat(error: unknown): string { if (error instanceof Error) { @@ -8,7 +11,8 @@ export function errorFormat(error: unknown): string { if (typeof error === "object" && error !== null) { try { return JSON.stringify(error, null, 2) - } catch { + } catch (e) { + log.debug("JSON.stringify failed", { error: e }) return "Unexpected error (unserializable)" } } diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 6c4d455224e9..97d56680eddd 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -5,6 +5,9 @@ import { dirname, join, relative, resolve as pathResolve, win32 } from "path" import { Readable } from "stream" import { pipeline } from "stream/promises" import { Glob } from "@opencode-ai/shared/util/glob" +import { Log } from "@/util" + +const log = Log.create({ service: "filesystem" }) // Fast sync version for metadata checks export async function exists(p: string): Promise { @@ -14,7 +17,8 @@ export async function exists(p: string): Promise { export async function isDir(p: string): Promise { try { return statSync(p).isDirectory() - } catch { + } catch (e) { + log.debug("statSync failed", { error: e, path: p }) return false } } @@ -115,7 +119,8 @@ export function normalizePath(p: string): string { const resolved = win32.normalize(win32.resolve(windowsPath(p))) try { return realpathSync.native(resolved) - } catch { + } catch (e) { + log.debug("realpathSync.native failed", { error: e, path: resolved }) return resolved } } @@ -231,8 +236,8 @@ export async function globUp(pattern: string, start: string, stop?: string) { dot: true, }) result.push(...matches) - } catch { - // Skip invalid glob patterns + } catch (e) { + log.debug("glob scan failed", { error: e, pattern, cwd: current }) } if (stop === current) break const parent = dirname(current) diff --git a/packages/opencode/src/util/lock.ts b/packages/opencode/src/util/lock.ts index 3f8e609378a3..72ace0a1b071 100644 --- a/packages/opencode/src/util/lock.ts +++ b/packages/opencode/src/util/lock.ts @@ -1,3 +1,11 @@ +export const DEFAULT_TIMEOUT_MS = 30000 + +function timeoutAfter(ms: number): Promise { + return new Promise((_, reject) => + setTimeout(() => reject(new Error(`Lock acquisition timeout after ${ms}ms`)), ms), + ) +} + const locks = new Map< string, { @@ -24,73 +32,88 @@ function process(key: string) { const lock = locks.get(key) if (!lock || lock.writer || lock.readers > 0) return - // Prioritize writers to prevent starvation if (lock.waitingWriters.length > 0) { const nextWriter = lock.waitingWriters.shift()! nextWriter() return } - // Wake up all waiting readers while (lock.waitingReaders.length > 0) { const nextReader = lock.waitingReaders.shift()! nextReader() } - // Clean up empty locks if (lock.readers === 0 && !lock.writer && lock.waitingReaders.length === 0 && lock.waitingWriters.length === 0) { locks.delete(key) } } -export async function read(key: string): Promise { +export async function read(key: string, timeoutMs?: number): Promise { + const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS const lock = get(key) - return new Promise((resolve) => { - if (!lock.writer && lock.waitingWriters.length === 0) { + const dispose = () => { + lock.readers-- + process(key) + } + + let tryAcquire: (() => void) | undefined + + const acquisition = new Promise((resolve) => { + tryAcquire = () => { lock.readers++ - resolve({ - [Symbol.dispose]: () => { - lock.readers-- - process(key) - }, - }) + resolve({ [Symbol.dispose]: dispose }) + } + + if (!lock.writer && lock.waitingWriters.length === 0) { + tryAcquire() } else { - lock.waitingReaders.push(() => { - lock.readers++ - resolve({ - [Symbol.dispose]: () => { - lock.readers-- - process(key) - }, - }) - }) + lock.waitingReaders.push(tryAcquire) } }) + + try { + return await Promise.race([acquisition, timeoutAfter(effectiveTimeout)]) + } catch (err) { + if (tryAcquire) { + const idx = lock.waitingReaders.indexOf(tryAcquire) + if (idx !== -1) lock.waitingReaders.splice(idx, 1) + } + throw err + } } -export async function write(key: string): Promise { +export async function write(key: string, timeoutMs?: number): Promise { + const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS const lock = get(key) - return new Promise((resolve) => { - if (!lock.writer && lock.readers === 0) { + const dispose = () => { + lock.writer = false + process(key) + } + + let tryAcquire: (() => void) | undefined + + const acquisition = new Promise((resolve) => { + tryAcquire = () => { lock.writer = true - resolve({ - [Symbol.dispose]: () => { - lock.writer = false - process(key) - }, - }) + resolve({ [Symbol.dispose]: dispose }) + } + + if (!lock.writer && lock.readers === 0) { + tryAcquire() } else { - lock.waitingWriters.push(() => { - lock.writer = true - resolve({ - [Symbol.dispose]: () => { - lock.writer = false - process(key) - }, - }) - }) + lock.waitingWriters.push(tryAcquire) } }) + + try { + return await Promise.race([acquisition, timeoutAfter(effectiveTimeout)]) + } catch (err) { + if (tryAcquire) { + const idx = lock.waitingWriters.indexOf(tryAcquire) + if (idx !== -1) lock.waitingWriters.splice(idx, 1) + } + throw err + } } diff --git a/packages/opencode/src/v2/session-event.ts b/packages/opencode/src/v2/session-event.ts index f922becf3af0..9f4fa961d989 100644 --- a/packages/opencode/src/v2/session-event.ts +++ b/packages/opencode/src/v2/session-event.ts @@ -3,456 +3,456 @@ import { withStatics } from "@/util/schema" import * as DateTime from "effect/DateTime" import { Schema } from "effect" -export namespace SessionEvent { - export const ID = Schema.String.pipe( - Schema.brand("Session.Event.ID"), - withStatics((s) => ({ - create: () => s.make(Identifier.create("evt", "ascending")), - })), - ) - export type ID = Schema.Schema.Type - type Stamp = Schema.Schema.Type - type BaseInput = { - id?: ID - metadata?: Record - timestamp?: Stamp +export const ID = Schema.String.pipe( + Schema.brand("Session.Event.ID"), + withStatics((s) => ({ + create: () => s.make(Identifier.create("evt", "ascending")), + })), +) +export type ID = Schema.Schema.Type +type Stamp = Schema.Schema.Type +type BaseInput = { + id?: ID + metadata?: Record + timestamp?: Stamp +} + +const Base = { + id: ID, + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + timestamp: Schema.DateTimeUtc, +} + +export class Source extends Schema.Class("Session.Event.Source")({ + start: Schema.Number, + end: Schema.Number, + text: Schema.String, +}) {} + +export class FileAttachment extends Schema.Class("Session.Event.FileAttachment")({ + uri: Schema.String, + mime: Schema.String, + name: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + source: Source.pipe(Schema.optional), +}) { + static create(input: FileAttachment) { + return new FileAttachment({ + uri: input.uri, + mime: input.mime, + name: input.name, + description: input.description, + source: input.source, + }) } +} - const Base = { - id: ID, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - timestamp: Schema.DateTimeUtc, +export class AgentAttachment extends Schema.Class("Session.Event.AgentAttachment")({ + name: Schema.String, + source: Source.pipe(Schema.optional), +}) {} + +export class RetryError extends Schema.Class("Session.Event.Retry.Error")({ + message: Schema.String, + statusCode: Schema.Number.pipe(Schema.optional), + isRetryable: Schema.Boolean, + responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + responseBody: Schema.String.pipe(Schema.optional), + metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), +}) {} + +export class Prompt extends Schema.Class("Session.Event.Prompt")({ + ...Base, + type: Schema.Literal("prompt"), + text: Schema.String, + files: Schema.Array(FileAttachment).pipe(Schema.optional), + agents: Schema.Array(AgentAttachment).pipe(Schema.optional), +}) { + static create(input: BaseInput & { text: string; files?: FileAttachment[]; agents?: AgentAttachment[] }) { + return new Prompt({ + id: input.id ?? ID.create(), + type: "prompt", + timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), + metadata: input.metadata, + text: input.text, + files: input.files, + agents: input.agents, + }) } +} - export class Source extends Schema.Class("Session.Event.Source")({ - start: Schema.Number, - end: Schema.Number, - text: Schema.String, - }) {} +export class Synthetic extends Schema.Class("Session.Event.Synthetic")({ + ...Base, + type: Schema.Literal("synthetic"), + text: Schema.String, +}) { + static create(input: BaseInput & { text: string }) { + return new Synthetic({ + id: input.id ?? ID.create(), + type: "synthetic", + timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), + metadata: input.metadata, + text: input.text, + }) + } +} - export class FileAttachment extends Schema.Class("Session.Event.FileAttachment")({ - uri: Schema.String, - mime: Schema.String, - name: Schema.String.pipe(Schema.optional), - description: Schema.String.pipe(Schema.optional), - source: Source.pipe(Schema.optional), +export namespace Step { + export class Started extends Schema.Class("Session.Event.Step.Started")({ + ...Base, + type: Schema.Literal("step.started"), + model: Schema.Struct({ + id: Schema.String, + providerID: Schema.String, + variant: Schema.String.pipe(Schema.optional), + }), }) { - static create(input: FileAttachment) { - return new FileAttachment({ - uri: input.uri, - mime: input.mime, - name: input.name, - description: input.description, - source: input.source, + static create(input: BaseInput & { model: { id: string; providerID: string; variant?: string } }) { + return new Started({ + id: input.id ?? ID.create(), + type: "step.started", + timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), + metadata: input.metadata, + model: input.model, }) } } - export class AgentAttachment extends Schema.Class("Session.Event.AgentAttachment")({ - name: Schema.String, - source: Source.pipe(Schema.optional), - }) {} + export class Ended extends Schema.Class("Session.Event.Step.Ended")({ + ...Base, + type: Schema.Literal("step.ended"), + reason: Schema.String, + cost: Schema.Number, + tokens: Schema.Struct({ + input: Schema.Number, + output: Schema.Number, + reasoning: Schema.Number, + cache: Schema.Struct({ + read: Schema.Number, + write: Schema.Number, + }), + }), + }) { + static create(input: BaseInput & { reason: string; cost: number; tokens: Ended["tokens"] }) { + return new Ended({ + id: input.id ?? ID.create(), + type: "step.ended", + timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), + metadata: input.metadata, + reason: input.reason, + cost: input.cost, + tokens: input.tokens, + }) + } + } +} - export class RetryError extends Schema.Class("Session.Event.Retry.Error")({ - message: Schema.String, - statusCode: Schema.Number.pipe(Schema.optional), - isRetryable: Schema.Boolean, - responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), - responseBody: Schema.String.pipe(Schema.optional), - metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), - }) {} +export namespace Text { + export class Started extends Schema.Class("Session.Event.Text.Started")({ + ...Base, + type: Schema.Literal("text.started"), + }) { + static create(input: BaseInput = {}) { + return new Started({ + id: input.id ?? ID.create(), + type: "text.started", + timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), + metadata: input.metadata, + }) + } + } - export class Prompt extends Schema.Class("Session.Event.Prompt")({ + export class Delta extends Schema.Class("Session.Event.Text.Delta")({ ...Base, - type: Schema.Literal("prompt"), - text: Schema.String, - files: Schema.Array(FileAttachment).pipe(Schema.optional), - agents: Schema.Array(AgentAttachment).pipe(Schema.optional), + type: Schema.Literal("text.delta"), + delta: Schema.String, }) { - static create(input: BaseInput & { text: string; files?: FileAttachment[]; agents?: AgentAttachment[] }) { - return new Prompt({ + static create(input: BaseInput & { delta: string }) { + return new Delta({ id: input.id ?? ID.create(), - type: "prompt", + type: "text.delta", timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), metadata: input.metadata, - text: input.text, - files: input.files, - agents: input.agents, + delta: input.delta, }) } } - export class Synthetic extends Schema.Class("Session.Event.Synthetic")({ + export class Ended extends Schema.Class("Session.Event.Text.Ended")({ ...Base, - type: Schema.Literal("synthetic"), + type: Schema.Literal("text.ended"), text: Schema.String, }) { static create(input: BaseInput & { text: string }) { - return new Synthetic({ + return new Ended({ id: input.id ?? ID.create(), - type: "synthetic", + type: "text.ended", timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), metadata: input.metadata, text: input.text, }) } } +} - export namespace Step { - export class Started extends Schema.Class("Session.Event.Step.Started")({ - ...Base, - type: Schema.Literal("step.started"), - model: Schema.Struct({ - id: Schema.String, - providerID: Schema.String, - variant: Schema.String.pipe(Schema.optional), - }), - }) { - static create(input: BaseInput & { model: { id: string; providerID: string; variant?: string } }) { - return new Started({ - id: input.id ?? ID.create(), - type: "step.started", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - model: input.model, - }) - } - } - - export class Ended extends Schema.Class("Session.Event.Step.Ended")({ - ...Base, - type: Schema.Literal("step.ended"), - reason: Schema.String, - cost: Schema.Number, - tokens: Schema.Struct({ - input: Schema.Number, - output: Schema.Number, - reasoning: Schema.Number, - cache: Schema.Struct({ - read: Schema.Number, - write: Schema.Number, - }), - }), - }) { - static create(input: BaseInput & { reason: string; cost: number; tokens: Ended["tokens"] }) { - return new Ended({ - id: input.id ?? ID.create(), - type: "step.ended", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - reason: input.reason, - cost: input.cost, - tokens: input.tokens, - }) - } +export namespace Reasoning { + export class Started extends Schema.Class("Session.Event.Reasoning.Started")({ + ...Base, + type: Schema.Literal("reasoning.started"), + }) { + static create(input: BaseInput = {}) { + return new Started({ + id: input.id ?? ID.create(), + type: "reasoning.started", + timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), + metadata: input.metadata, + }) } } - export namespace Text { - export class Started extends Schema.Class("Session.Event.Text.Started")({ - ...Base, - type: Schema.Literal("text.started"), - }) { - static create(input: BaseInput = {}) { - return new Started({ - id: input.id ?? ID.create(), - type: "text.started", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - }) - } - } - - export class Delta extends Schema.Class("Session.Event.Text.Delta")({ - ...Base, - type: Schema.Literal("text.delta"), - delta: Schema.String, - }) { - static create(input: BaseInput & { delta: string }) { - return new Delta({ - id: input.id ?? ID.create(), - type: "text.delta", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - delta: input.delta, - }) - } + export class Delta extends Schema.Class("Session.Event.Reasoning.Delta")({ + ...Base, + type: Schema.Literal("reasoning.delta"), + delta: Schema.String, + }) { + static create(input: BaseInput & { delta: string }) { + return new Delta({ + id: input.id ?? ID.create(), + type: "reasoning.delta", + timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), + metadata: input.metadata, + delta: input.delta, + }) } + } - export class Ended extends Schema.Class("Session.Event.Text.Ended")({ - ...Base, - type: Schema.Literal("text.ended"), - text: Schema.String, - }) { - static create(input: BaseInput & { text: string }) { - return new Ended({ - id: input.id ?? ID.create(), - type: "text.ended", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - text: input.text, - }) - } + export class Ended extends Schema.Class("Session.Event.Reasoning.Ended")({ + ...Base, + type: Schema.Literal("reasoning.ended"), + text: Schema.String, + }) { + static create(input: BaseInput & { text: string }) { + return new Ended({ + id: input.id ?? ID.create(), + type: "reasoning.ended", + timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), + metadata: input.metadata, + text: input.text, + }) } } +} - export namespace Reasoning { - export class Started extends Schema.Class("Session.Event.Reasoning.Started")({ +export namespace Tool { + export namespace Input { + export class Started extends Schema.Class("Session.Event.Tool.Input.Started")({ ...Base, - type: Schema.Literal("reasoning.started"), + callID: Schema.String, + name: Schema.String, + type: Schema.Literal("tool.input.started"), }) { - static create(input: BaseInput = {}) { + static create(input: BaseInput & { callID: string; name: string }) { return new Started({ id: input.id ?? ID.create(), - type: "reasoning.started", + type: "tool.input.started", timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), metadata: input.metadata, + callID: input.callID, + name: input.name, }) } } - export class Delta extends Schema.Class("Session.Event.Reasoning.Delta")({ + export class Delta extends Schema.Class("Session.Event.Tool.Input.Delta")({ ...Base, - type: Schema.Literal("reasoning.delta"), + callID: Schema.String, + type: Schema.Literal("tool.input.delta"), delta: Schema.String, }) { - static create(input: BaseInput & { delta: string }) { + static create(input: BaseInput & { callID: string; delta: string }) { return new Delta({ id: input.id ?? ID.create(), - type: "reasoning.delta", + type: "tool.input.delta", timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), metadata: input.metadata, + callID: input.callID, delta: input.delta, }) } } - export class Ended extends Schema.Class("Session.Event.Reasoning.Ended")({ + export class Ended extends Schema.Class("Session.Event.Tool.Input.Ended")({ ...Base, - type: Schema.Literal("reasoning.ended"), + callID: Schema.String, + type: Schema.Literal("tool.input.ended"), text: Schema.String, }) { - static create(input: BaseInput & { text: string }) { + static create(input: BaseInput & { callID: string; text: string }) { return new Ended({ id: input.id ?? ID.create(), - type: "reasoning.ended", + type: "tool.input.ended", timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), metadata: input.metadata, + callID: input.callID, text: input.text, }) } } } - export namespace Tool { - export namespace Input { - export class Started extends Schema.Class("Session.Event.Tool.Input.Started")({ - ...Base, - callID: Schema.String, - name: Schema.String, - type: Schema.Literal("tool.input.started"), - }) { - static create(input: BaseInput & { callID: string; name: string }) { - return new Started({ - id: input.id ?? ID.create(), - type: "tool.input.started", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - name: input.name, - }) - } - } - - export class Delta extends Schema.Class("Session.Event.Tool.Input.Delta")({ - ...Base, - callID: Schema.String, - type: Schema.Literal("tool.input.delta"), - delta: Schema.String, - }) { - static create(input: BaseInput & { callID: string; delta: string }) { - return new Delta({ - id: input.id ?? ID.create(), - type: "tool.input.delta", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - delta: input.delta, - }) - } - } - - export class Ended extends Schema.Class("Session.Event.Tool.Input.Ended")({ - ...Base, - callID: Schema.String, - type: Schema.Literal("tool.input.ended"), - text: Schema.String, - }) { - static create(input: BaseInput & { callID: string; text: string }) { - return new Ended({ - id: input.id ?? ID.create(), - type: "tool.input.ended", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - text: input.text, - }) - } - } - } - - export class Called extends Schema.Class("Session.Event.Tool.Called")({ - ...Base, - type: Schema.Literal("tool.called"), - callID: Schema.String, - tool: Schema.String, - input: Schema.Record(Schema.String, Schema.Unknown), - provider: Schema.Struct({ - executed: Schema.Boolean, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - }), - }) { - static create( - input: BaseInput & { - callID: string - tool: string - input: Record - provider: Called["provider"] - }, - ) { - return new Called({ - id: input.id ?? ID.create(), - type: "tool.called", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - tool: input.tool, - input: input.input, - provider: input.provider, - }) - } - } - - export class Success extends Schema.Class("Session.Event.Tool.Success")({ - ...Base, - type: Schema.Literal("tool.success"), - callID: Schema.String, - title: Schema.String, - output: Schema.String.pipe(Schema.optional), - attachments: Schema.Array(FileAttachment).pipe(Schema.optional), - provider: Schema.Struct({ - executed: Schema.Boolean, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - }), - }) { - static create( - input: BaseInput & { - callID: string - title: string - output?: string - attachments?: FileAttachment[] - provider: Success["provider"] - }, - ) { - return new Success({ - id: input.id ?? ID.create(), - type: "tool.success", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - title: input.title, - output: input.output, - attachments: input.attachments, - provider: input.provider, - }) - } - } - - export class Error extends Schema.Class("Session.Event.Tool.Error")({ - ...Base, - type: Schema.Literal("tool.error"), - callID: Schema.String, - error: Schema.String, - provider: Schema.Struct({ - executed: Schema.Boolean, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - }), - }) { - static create(input: BaseInput & { callID: string; error: string; provider: Error["provider"] }) { - return new Error({ - id: input.id ?? ID.create(), - type: "tool.error", - timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), - metadata: input.metadata, - callID: input.callID, - error: input.error, - provider: input.provider, - }) - } + export class Called extends Schema.Class("Session.Event.Tool.Called")({ + ...Base, + type: Schema.Literal("tool.called"), + callID: Schema.String, + tool: Schema.String, + input: Schema.Record(Schema.String, Schema.Unknown), + provider: Schema.Struct({ + executed: Schema.Boolean, + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + }), + }) { + static create( + input: BaseInput & { + callID: string + tool: string + input: Record + provider: Called["provider"] + }, + ) { + return new Called({ + id: input.id ?? ID.create(), + type: "tool.called", + timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), + metadata: input.metadata, + callID: input.callID, + tool: input.tool, + input: input.input, + provider: input.provider, + }) } } - export class Retried extends Schema.Class("Session.Event.Retried")({ + export class Success extends Schema.Class("Session.Event.Tool.Success")({ ...Base, - type: Schema.Literal("retried"), - attempt: Schema.Number, - error: RetryError, + type: Schema.Literal("tool.success"), + callID: Schema.String, + title: Schema.String, + output: Schema.String.pipe(Schema.optional), + attachments: Schema.Array(FileAttachment).pipe(Schema.optional), + provider: Schema.Struct({ + executed: Schema.Boolean, + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + }), }) { - static create(input: BaseInput & { attempt: number; error: RetryError }) { - return new Retried({ + static create( + input: BaseInput & { + callID: string + title: string + output?: string + attachments?: FileAttachment[] + provider: Success["provider"] + }, + ) { + return new Success({ id: input.id ?? ID.create(), - type: "retried", + type: "tool.success", timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), metadata: input.metadata, - attempt: input.attempt, - error: input.error, + callID: input.callID, + title: input.title, + output: input.output, + attachments: input.attachments, + provider: input.provider, }) } } - export class Compacted extends Schema.Class("Session.Event.Compated")({ + export class Error extends Schema.Class("Session.Event.Tool.Error")({ ...Base, - type: Schema.Literal("compacted"), - auto: Schema.Boolean, - overflow: Schema.Boolean.pipe(Schema.optional), + type: Schema.Literal("tool.error"), + callID: Schema.String, + error: Schema.String, + provider: Schema.Struct({ + executed: Schema.Boolean, + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + }), }) { - static create(input: BaseInput & { auto: boolean; overflow?: boolean }) { - return new Compacted({ + static create(input: BaseInput & { callID: string; error: string; provider: Error["provider"] }) { + return new Error({ id: input.id ?? ID.create(), - type: "compacted", + type: "tool.error", timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), metadata: input.metadata, - auto: input.auto, - overflow: input.overflow, + callID: input.callID, + error: input.error, + provider: input.provider, }) } } +} + +export class Retried extends Schema.Class("Session.Event.Retried")({ + ...Base, + type: Schema.Literal("retried"), + attempt: Schema.Number, + error: RetryError, +}) { + static create(input: BaseInput & { attempt: number; error: RetryError }) { + return new Retried({ + id: input.id ?? ID.create(), + type: "retried", + timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), + metadata: input.metadata, + attempt: input.attempt, + error: input.error, + }) + } +} - export const Event = Schema.Union( - [ - Prompt, - Synthetic, - Step.Started, - Step.Ended, - Text.Started, - Text.Delta, - Text.Ended, - Tool.Input.Started, - Tool.Input.Delta, - Tool.Input.Ended, - Tool.Called, - Tool.Success, - Tool.Error, - Reasoning.Started, - Reasoning.Delta, - Reasoning.Ended, - Retried, - Compacted, - ], - { - mode: "oneOf", - }, - ).pipe(Schema.toTaggedUnion("type")) - export type Event = Schema.Schema.Type - export type Type = Event["type"] +export class Compacted extends Schema.Class("Session.Event.Compated")({ + ...Base, + type: Schema.Literal("compacted"), + auto: Schema.Boolean, + overflow: Schema.Boolean.pipe(Schema.optional), +}) { + static create(input: BaseInput & { auto: boolean; overflow?: boolean }) { + return new Compacted({ + id: input.id ?? ID.create(), + type: "compacted", + timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()), + metadata: input.metadata, + auto: input.auto, + overflow: input.overflow, + }) + } } + +export const Event = Schema.Union( + [ + Prompt, + Synthetic, + Step.Started, + Step.Ended, + Text.Started, + Text.Delta, + Text.Ended, + Tool.Input.Started, + Tool.Input.Delta, + Tool.Input.Ended, + Tool.Called, + Tool.Success, + Tool.Error, + Reasoning.Started, + Reasoning.Delta, + Reasoning.Ended, + Retried, + Compacted, + ], + { + mode: "oneOf", + }, +).pipe(Schema.toTaggedUnion("type")) +export type Event = Schema.Schema.Type +export type Type = Event["type"] + +export * as SessionEvent from "./session-event" diff --git a/packages/opencode/src/v2/session.ts b/packages/opencode/src/v2/session.ts index 79a69161200c..2bac11f4fe3a 100644 --- a/packages/opencode/src/v2/session.ts +++ b/packages/opencode/src/v2/session.ts @@ -1,7 +1,7 @@ import { Context, Layer, Schema, Effect } from "effect" import { SessionEntry } from "./session-entry" import { Struct } from "effect" -import { Session } from "@/session" +import { Session } from "@/session/session" import { SessionID } from "@/session/schema" export const ID = SessionID diff --git a/packages/opencode/test/config/agent-color.test.ts b/packages/opencode/test/config/agent-color.test.ts index bfa948619bb1..390d0ccff9e9 100644 --- a/packages/opencode/test/config/agent-color.test.ts +++ b/packages/opencode/test/config/agent-color.test.ts @@ -3,7 +3,7 @@ import { Effect } from "effect" import path from "path" import { provideInstance, tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" -import { Config } from "../../src/config" +import { Config } from "../../src/config/config" import { Agent as AgentSvc } from "../../src/agent/agent" import { Color } from "../../src/util" import { AppRuntime } from "../../src/effect/app-runtime" diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 361ac0b5df74..110b33857190 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1,7 +1,8 @@ import { test, expect, describe, mock, afterEach, beforeEach } from "bun:test" import { Effect, Layer, Option } from "effect" import { NodeFileSystem, NodePath } from "@effect/platform-node" -import { Config, ConfigManaged } from "../../src/config" +import { Config } from "../../src/config/config" +import { ConfigManaged } from "../../src/config/managed" import { ConfigParse } from "../../src/config/parse" import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" diff --git a/packages/opencode/test/config/markdown.test.ts b/packages/opencode/test/config/markdown.test.ts index b807850c3923..b72bb48b53bc 100644 --- a/packages/opencode/test/config/markdown.test.ts +++ b/packages/opencode/test/config/markdown.test.ts @@ -1,5 +1,5 @@ import { expect, test, describe } from "bun:test" -import { ConfigMarkdown } from "../../src/config" +import * as ConfigMarkdown from "../../src/config/markdown" describe("ConfigMarkdown: normal template", () => { const template = `This is a @valid/path/to/a/file and it should also match at diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index c7b6d4a50494..91ec03fd7f92 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -4,7 +4,7 @@ import fs from "fs/promises" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { TuiConfig } from "../../src/cli/cmd/tui/config/tui" -import { Config } from "../../src/config" +import { Config } from "../../src/config/config" import { Global } from "../../src/global" import { Filesystem } from "../../src/util" import { AppRuntime } from "../../src/effect/app-runtime" diff --git a/packages/opencode/test/file/watcher.test.ts b/packages/opencode/test/file/watcher.test.ts index 0c2355008306..0c8968d94b05 100644 --- a/packages/opencode/test/file/watcher.test.ts +++ b/packages/opencode/test/file/watcher.test.ts @@ -5,7 +5,7 @@ import path from "path" import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect" import { tmpdir } from "../fixture/fixture" import { Bus } from "../../src/bus" -import { Config } from "../../src/config" +import { Config } from "../../src/config/config" import { FileWatcher } from "../../src/file/watcher" import { Git } from "../../src/git" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts index fd7f5e380876..79705435472c 100644 --- a/packages/opencode/test/fixture/fixture.ts +++ b/packages/opencode/test/fixture/fixture.ts @@ -6,7 +6,7 @@ import { Effect, Context } from "effect" import type * as PlatformError from "effect/PlatformError" import type * as Scope from "effect/Scope" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import type { Config } from "../../src/config" +import type { Config } from "../../src/config/config" import { InstanceRef } from "../../src/effect/instance-ref" import { Instance } from "../../src/project/instance" import { TestLLMServer } from "../lib/llm-server" diff --git a/packages/opencode/test/permission-task.test.ts b/packages/opencode/test/permission-task.test.ts index 3c53314b6aff..d415d23ebccb 100644 --- a/packages/opencode/test/permission-task.test.ts +++ b/packages/opencode/test/permission-task.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, test, expect } from "bun:test" import { Permission } from "../src/permission" -import { Config } from "../src/config" +import { Config } from "../src/config/config" import { Instance } from "../src/project/instance" import { tmpdir } from "./fixture/fixture" import { AppRuntime } from "../src/effect/app-runtime" diff --git a/packages/opencode/test/server/global-session-list.test.ts b/packages/opencode/test/server/global-session-list.test.ts index 03b1a0346aee..faf40f2642b6 100644 --- a/packages/opencode/test/server/global-session-list.test.ts +++ b/packages/opencode/test/server/global-session-list.test.ts @@ -3,7 +3,7 @@ import { Effect } from "effect" import z from "zod" import { Instance } from "../../src/project/instance" import { Project } from "../../src/project" -import { Session as SessionNs } from "../../src/session" +import { Session as SessionNs } from "../../src/session/session" import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/server/session-actions.test.ts b/packages/opencode/test/server/session-actions.test.ts index 4be2344aab88..bca8cb67fa89 100644 --- a/packages/opencode/test/server/session-actions.test.ts +++ b/packages/opencode/test/server/session-actions.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, mock, test } from "bun:test" import { Effect } from "effect" import { Instance } from "../../src/project/instance" import { Server } from "../../src/server/server" -import { Session as SessionNs } from "../../src/session" +import { Session as SessionNs } from "../../src/session/session" import type { SessionID } from "../../src/session/schema" import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 602d0f204930..3e551f1931d1 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, test } from "bun:test" import { Effect } from "effect" import { Instance } from "../../src/project/instance" -import { Session as SessionNs } from "../../src/session" +import { Session as SessionNs } from "../../src/session/session" import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/server/session-messages.test.ts b/packages/opencode/test/server/session-messages.test.ts index 23e8b5014535..7ff3c6b8117d 100644 --- a/packages/opencode/test/server/session-messages.test.ts +++ b/packages/opencode/test/server/session-messages.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test" import { Effect } from "effect" import { Instance } from "../../src/project/instance" import { Server } from "../../src/server/server" -import { Session as SessionNs } from "../../src/session" +import { Session as SessionNs } from "../../src/session/session" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { Log } from "../../src/util" diff --git a/packages/opencode/test/server/session-select.test.ts b/packages/opencode/test/server/session-select.test.ts index 21e07f88a07f..5e4e673b800e 100644 --- a/packages/opencode/test/server/session-select.test.ts +++ b/packages/opencode/test/server/session-select.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test" import { Effect } from "effect" -import { Session as SessionNs } from "../../src/session" +import { Session as SessionNs } from "../../src/session/session" import type { SessionID } from "../../src/session/schema" import { Log } from "../../src/util" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 4fe9c1551136..63d85750dcf1 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -4,7 +4,7 @@ import { Cause, Effect, Exit, Layer, ManagedRuntime } from "effect" import * as Stream from "effect/Stream" import z from "zod" import { Bus } from "../../src/bus" -import { Config } from "../../src/config" +import { Config } from "../../src/config/config" import { Agent } from "../../src/agent/agent" import { LLM } from "../../src/session/llm" import { SessionCompaction } from "../../src/session/compaction" @@ -14,7 +14,7 @@ import { Log } from "../../src/util" import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" import { provideTmpdirInstance, tmpdir } from "../fixture/fixture" -import { Session as SessionNs } from "../../src/session" +import { Session as SessionNs } from "../../src/session/session" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" diff --git a/packages/opencode/test/session/messages-pagination.test.ts b/packages/opencode/test/session/messages-pagination.test.ts index df2d18b9f123..f170f98795ec 100644 --- a/packages/opencode/test/session/messages-pagination.test.ts +++ b/packages/opencode/test/session/messages-pagination.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" import path from "path" import { Instance } from "../../src/project/instance" -import { Session as SessionNs } from "../../src/session" +import { Session as SessionNs } from "../../src/session/session" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { ModelID, ProviderID } from "../../src/provider/schema" diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index 74ce913077d3..60d666e5ded7 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -5,12 +5,12 @@ import path from "path" import type { Agent } from "../../src/agent/agent" import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" -import { Config } from "../../src/config" +import { Config } from "../../src/config/config" import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" import { Provider } from "../../src/provider" import { ModelID, ProviderID } from "../../src/provider/schema" -import { Session } from "../../src/session" +import { Session } from "../../src/session/session" import { LLM } from "../../src/session/llm" import { MessageV2 } from "../../src/session/message-v2" import { SessionProcessor } from "../../src/session/processor" diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 911cb4415553..d9a15df630fc 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -8,7 +8,7 @@ import { NamedError } from "@opencode-ai/shared/util/error" import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Command } from "../../src/command" -import { Config } from "../../src/config" +import { Config } from "../../src/config/config" import { LSP } from "../../src/lsp" import { MCP } from "../../src/mcp" import { Permission } from "../../src/permission" @@ -18,7 +18,7 @@ import { Env } from "../../src/env" import { ModelID, ProviderID } from "../../src/provider/schema" import { Question } from "../../src/question" import { Todo } from "../../src/session/todo" -import { Session } from "../../src/session" +import { Session } from "../../src/session/session" import { LLM } from "../../src/session/llm" import { MessageV2 } from "../../src/session/message-v2" import { AppFileSystem } from "@opencode-ai/shared/filesystem" diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts index f28fb94c0be5..6a11b5870b1f 100644 --- a/packages/opencode/test/session/revert-compact.test.ts +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -2,7 +2,7 @@ import { describe, expect } from "bun:test" import fs from "fs/promises" import path from "path" import { Effect, Layer } from "effect" -import { Session } from "../../src/session" +import { Session } from "../../src/session/session" import { ModelID, ProviderID } from "../../src/provider/schema" import { SessionRevert } from "../../src/session/revert" import { MessageV2 } from "../../src/session/message-v2" diff --git a/packages/opencode/test/session/schema-decoding.test.ts b/packages/opencode/test/session/schema-decoding.test.ts index 5894b2615414..c40594df0219 100644 --- a/packages/opencode/test/session/schema-decoding.test.ts +++ b/packages/opencode/test/session/schema-decoding.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import { Schema } from "effect" -import { Session } from "../../src/session" +import { Session } from "../../src/session/session" import { SessionPrompt } from "../../src/session/prompt" import { SessionRevert } from "../../src/session/revert" import { SessionStatus } from "../../src/session/status" diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index d4a1d711d849..c530d83b9846 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import path from "path" -import { Session as SessionNs } from "../../src/session" +import { Session as SessionNs } from "../../src/session/session" import { Bus } from "../../src/bus" import { Log } from "../../src/util" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 651754733909..dce6968f2735 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -16,7 +16,7 @@ import { Effect, Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" import fs from "fs/promises" import path from "path" -import { Session } from "../../src/session" +import { Session } from "../../src/session/session" import { LLM } from "../../src/session/llm" import { SessionPrompt } from "../../src/session/prompt" import { SessionRevert } from "../../src/session/revert" @@ -32,7 +32,7 @@ import { NodeFileSystem } from "@effect/platform-node" import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Command } from "../../src/command" -import { Config } from "../../src/config" +import { Config } from "../../src/config/config" import { LSP } from "../../src/lsp" import { MCP } from "../../src/mcp" import { Permission } from "../../src/permission" diff --git a/packages/opencode/test/session/structured-output-integration.test.ts b/packages/opencode/test/session/structured-output-integration.test.ts index fb8d42f0772b..299286513847 100644 --- a/packages/opencode/test/session/structured-output-integration.test.ts +++ b/packages/opencode/test/session/structured-output-integration.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import path from "path" import { Effect, Layer } from "effect" -import { Session } from "../../src/session" +import { Session } from "../../src/session/session" import { SessionPrompt } from "../../src/session/prompt" import { Log } from "../../src/util" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index e217300d0946..6a8d17fedc64 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -8,9 +8,9 @@ import { Account } from "../../src/account/account" import { AccountRepo } from "../../src/account/repo" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Bus } from "../../src/bus" -import { Config } from "../../src/config" +import { Config } from "../../src/config/config" import { Provider } from "../../src/provider" -import { Session } from "../../src/session" +import { Session } from "../../src/session/session" import type { SessionID } from "../../src/session/schema" import { ShareNext } from "../../src/share" import { SessionShareTable } from "../../src/share/share.sql" diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index b94dd5208655..e05bfb9de3ca 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -1,10 +1,10 @@ import { afterEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Agent } from "../../src/agent/agent" -import { Config } from "../../src/config" +import { Config } from "../../src/config/config" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Instance } from "../../src/project/instance" -import { Session } from "../../src/session" +import { Session } from "../../src/session/session" import { MessageV2 } from "../../src/session/message-v2" import type { SessionPrompt } from "../../src/session/prompt" import { MessageID, PartID } from "../../src/session/schema" diff --git a/packages/opencode/test/tool/truncation.test.ts b/packages/opencode/test/tool/truncation.test.ts index 369ad2d58181..5fb0cf03d2ac 100644 --- a/packages/opencode/test/tool/truncation.test.ts +++ b/packages/opencode/test/tool/truncation.test.ts @@ -2,7 +2,7 @@ import { describe, test, expect } from "bun:test" import { NodeFileSystem } from "@effect/platform-node" import { Effect, FileSystem, Layer } from "effect" import { Truncate } from "../../src/tool" -import { Config } from "../../src/config" +import { Config } from "../../src/config/config" import { Identifier } from "../../src/id/id" import { Process } from "../../src/util" import { Filesystem } from "../../src/util" diff --git a/packages/opencode/test/workspace/workspace-restore.test.ts b/packages/opencode/test/workspace/workspace-restore.test.ts index ad6ac2c5fd85..26e9dbefa9e8 100644 --- a/packages/opencode/test/workspace/workspace-restore.test.ts +++ b/packages/opencode/test/workspace/workspace-restore.test.ts @@ -9,7 +9,7 @@ import { AppRuntime } from "../../src/effect/app-runtime" import { Flag } from "../../src/flag/flag" import { ModelID, ProviderID } from "../../src/provider/schema" import { Instance } from "../../src/project/instance" -import { Session as SessionNs } from "../../src/session" +import { Session as SessionNs } from "../../src/session/session" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { Database, asc, eq } from "../../src/storage"