diff --git a/.sisyphus/notepads/rebase-v146/decisions.md b/.sisyphus/notepads/rebase-v146/decisions.md new file mode 100644 index 000000000000..e6f8c870a065 --- /dev/null +++ b/.sisyphus/notepads/rebase-v146/decisions.md @@ -0,0 +1 @@ +- Kept upstream Effect-native `session/index.ts`/middleware shapes, added `DuplicateIDError` support via shared import paths and `CreateInput.id`, and squashed the resolution into `pyyluxzz` so the workspace no longer reports conflicts. diff --git a/.sisyphus/notepads/rebase-v146/issues.md b/.sisyphus/notepads/rebase-v146/issues.md new file mode 100644 index 000000000000..5578253377e2 --- /dev/null +++ b/.sisyphus/notepads/rebase-v146/issues.md @@ -0,0 +1 @@ +- `bun typecheck` initially failed because this workspace had no installed dependencies (`tsgo` missing); running `bun install` in the workspace restored the toolchain. diff --git a/.sisyphus/notepads/rebase-v146/learnings.md b/.sisyphus/notepads/rebase-v146/learnings.md new file mode 100644 index 000000000000..2f9160ed2791 --- /dev/null +++ b/.sisyphus/notepads/rebase-v146/learnings.md @@ -0,0 +1 @@ +- Branch 8 resolves by keeping upstream route migration in `server/instance/session.ts` and deleting `server/routes/session.ts`; only the create route needs `...errors(400, 409)` ported. diff --git a/packages/app/public/ort-wasm-simd-threaded.wasm b/packages/app/public/ort-wasm-simd-threaded.wasm new file mode 100644 index 000000000000..f21ee10a4c63 Binary files /dev/null and b/packages/app/public/ort-wasm-simd-threaded.wasm differ diff --git a/packages/app/public/silero_vad_legacy.onnx b/packages/app/public/silero_vad_legacy.onnx new file mode 100644 index 000000000000..e6db48d6e2a0 Binary files /dev/null and b/packages/app/public/silero_vad_legacy.onnx differ diff --git a/packages/opencode/src/server/error.ts b/packages/opencode/src/server/error.ts index cc5fa961877a..9907bd70adc3 100644 --- a/packages/opencode/src/server/error.ts +++ b/packages/opencode/src/server/error.ts @@ -29,6 +29,25 @@ export const ERRORS = { }, }, }, + 409: { + description: "Conflict", + content: { + "application/json": { + schema: resolver( + z + .object({ + name: z.literal("DuplicateIDError"), + data: z.object({ + id: z.string(), + }), + }) + .meta({ + ref: "DuplicateIDError", + }), + ), + }, + }, + }, } as const export function errors(...codes: number[]) { diff --git a/packages/opencode/src/server/instance/session.ts b/packages/opencode/src/server/instance/session.ts index a011c32f9b2d..e791c87b70e5 100644 --- a/packages/opencode/src/server/instance/session.ts +++ b/packages/opencode/src/server/instance/session.ts @@ -198,7 +198,7 @@ export const SessionRoutes = lazy(() => description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.", operationId: "session.create", responses: { - ...errors(400), + ...errors(400, 409), 200: { description: "Successfully created session", content: { diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index 6e916518669c..ff1e3f863ced 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -23,6 +23,7 @@ export const ErrorMiddleware: ErrorHandler = (err, c) => { else if (err instanceof Provider.ModelNotFoundError) status = 400 else if (err.name === "ProviderAuthValidationFailed") status = 400 else if (err.name.startsWith("Worktree")) status = 400 + else if (err.name === "DuplicateIDError") status = 409 else status = 500 return c.json(err.toObject(), { status }) } diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index d8ab812349c7..531d96725711 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -1,4 +1,5 @@ import { Slug } from "@opencode-ai/shared/util/slug" +import { NamedError } from "@opencode-ai/shared/util/error" import path from "path" import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" @@ -32,6 +33,13 @@ import { Effect, Layer, Option, Context } from "effect" export namespace Session { const log = Log.create({ service: "session" }) + export const DuplicateIDError = NamedError.create( + "DuplicateIDError", + z.object({ + id: z.string(), + }), + ) + const parentTitlePrefix = "New session - " const childTitlePrefix = "Child session - " @@ -179,6 +187,7 @@ export namespace Session { export const CreateInput = z .object({ + id: SessionID.zod.optional(), parentID: SessionID.zod.optional(), title: z.string().optional(), permission: Info.shape.permission, @@ -334,6 +343,7 @@ export namespace Session { export interface Interface { readonly create: (input?: { + id?: SessionID parentID?: SessionID title?: string permission?: Permission.Ruleset @@ -521,6 +531,7 @@ export namespace Session { }) const create = Effect.fn("Session.create")(function* (input?: { + id?: SessionID parentID?: SessionID title?: string permission?: Permission.Ruleset @@ -528,6 +539,7 @@ export namespace Session { }) { const directory = yield* InstanceState.directory return yield* createNext({ + id: input?.id, parentID: input?.parentID, directory, title: input?.title, diff --git a/packages/opencode/src/session/projectors.ts b/packages/opencode/src/session/projectors.ts index 460f0a41c596..6f347c529a3b 100644 --- a/packages/opencode/src/session/projectors.ts +++ b/packages/opencode/src/session/projectors.ts @@ -15,6 +15,12 @@ function foreign(err: unknown) { return "message" in err && typeof err.message === "string" && err.message.includes("FOREIGN KEY constraint failed") } +function duplicate(err: unknown, table: string) { + if (typeof err !== "object" || err === null) return false + if (!("message" in err) || typeof err.message !== "string") return false + return err.message.includes(`UNIQUE constraint failed: ${table}.id`) +} + export type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial | null } : T function grab( @@ -64,7 +70,12 @@ export function toPartialRow(info: DeepPartial) { export default [ SyncEvent.project(Session.Event.Created, (db, data) => { - db.insert(SessionTable).values(Session.toRow(data.info)).run() + try { + db.insert(SessionTable).values(Session.toRow(data.info)).run() + } catch (err) { + if (duplicate(err, "session")) throw new Session.DuplicateIDError({ id: data.info.id }) + throw err + } }), SyncEvent.project(Session.Event.Updated, (db, data) => { diff --git a/packages/opencode/test/server/session-messages.test.ts b/packages/opencode/test/server/session-messages.test.ts index 24ee6a1b436f..e9a7212b05dc 100644 --- a/packages/opencode/test/server/session-messages.test.ts +++ b/packages/opencode/test/server/session-messages.test.ts @@ -166,6 +166,7 @@ describe("session messages endpoint", () => { }) }) + describe("session.prompt_async error handling", () => { test("prompt_async route has error handler for detached prompt call", async () => { const src = await Bun.file(new URL("../../src/server/instance/session.ts", import.meta.url)).text() diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index 15132a270125..3f3d0f0138ec 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -8,6 +8,7 @@ import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { AppRuntime } from "../../src/effect/app-runtime" import { tmpdir } from "../fixture/fixture" +import { SyncEvent } from "../../src/sync" const projectRoot = path.join(__dirname, "../..") Log.init({ print: false }) @@ -179,3 +180,19 @@ describe("Session", () => { expect(missing).toBe(true) }) }) + +describe("DuplicateIDError", () => { + test("projector throws on duplicate session ID", async () => { + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await create({}) + expect(() => { + SyncEvent.run(SessionNs.Event.Created, { sessionID: session.id, info: session as any }) + }).toThrow() + await remove(session.id) + }, + }) + }) +}) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index d7bf43f506f8..6b2f76e5105b 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -1653,6 +1653,7 @@ export class Session2 extends HeyApiClient { parameters?: { directory?: string workspace?: string + id?: string parentID?: string title?: string permission?: PermissionRuleset @@ -1667,6 +1668,7 @@ export class Session2 extends HeyApiClient { args: [ { in: "query", key: "directory" }, { in: "query", key: "workspace" }, + { in: "body", key: "id" }, { in: "body", key: "parentID" }, { in: "body", key: "title" }, { in: "body", key: "permission" }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 24c1d53bf749..e0f85a319862 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1888,6 +1888,13 @@ export type McpResource = { client: string } +export type DuplicateIdError = { + name: "DuplicateIDError" + data: { + id: string + } +} + export type TextPartInput = { id?: string type: "text" @@ -3265,6 +3272,7 @@ export type SessionListResponse = SessionListResponses[keyof SessionListResponse export type SessionCreateData = { body?: { + id?: string parentID?: string title?: string permission?: PermissionRuleset @@ -3283,6 +3291,10 @@ export type SessionCreateErrors = { * Bad request */ 400: BadRequestError + /** + * Conflict + */ + 409: DuplicateIdError } export type SessionCreateError = SessionCreateErrors[keyof SessionCreateErrors]