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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .sisyphus/notepads/rebase-v146/decisions.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions .sisyphus/notepads/rebase-v146/issues.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions .sisyphus/notepads/rebase-v146/learnings.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file added packages/app/public/ort-wasm-simd-threaded.wasm
Binary file not shown.
Binary file added packages/app/public/silero_vad_legacy.onnx
Binary file not shown.
19 changes: 19 additions & 0 deletions packages/opencode/src/server/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) {
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/server/instance/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/server/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/session/index.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 - "

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -334,6 +343,7 @@ export namespace Session {

export interface Interface {
readonly create: (input?: {
id?: SessionID
parentID?: SessionID
title?: string
permission?: Permission.Ruleset
Expand Down Expand Up @@ -521,13 +531,15 @@ export namespace Session {
})

const create = Effect.fn("Session.create")(function* (input?: {
id?: SessionID
parentID?: SessionID
title?: string
permission?: Permission.Ruleset
workspaceID?: WorkspaceID
}) {
const directory = yield* InstanceState.directory
return yield* createNext({
id: input?.id,
parentID: input?.parentID,
directory,
title: input?.title,
Expand Down
13 changes: 12 additions & 1 deletion packages/opencode/src/session/projectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> | null } : T

function grab<T extends object, K1 extends keyof T, X>(
Expand Down Expand Up @@ -64,7 +70,12 @@ export function toPartialRow(info: DeepPartial<Session.Info>) {

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) => {
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/test/server/session-messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
17 changes: 17 additions & 0 deletions packages/opencode/test/session/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -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)
},
})
})
})
2 changes: 2 additions & 0 deletions packages/sdk/js/src/v2/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1653,6 +1653,7 @@ export class Session2 extends HeyApiClient {
parameters?: {
directory?: string
workspace?: string
id?: string
parentID?: string
title?: string
permission?: PermissionRuleset
Expand All @@ -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" },
Expand Down
12 changes: 12 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1888,6 +1888,13 @@ export type McpResource = {
client: string
}

export type DuplicateIdError = {
name: "DuplicateIDError"
data: {
id: string
}
}

export type TextPartInput = {
id?: string
type: "text"
Expand Down Expand Up @@ -3265,6 +3272,7 @@ export type SessionListResponse = SessionListResponses[keyof SessionListResponse

export type SessionCreateData = {
body?: {
id?: string
parentID?: string
title?: string
permission?: PermissionRuleset
Expand All @@ -3283,6 +3291,10 @@ export type SessionCreateErrors = {
* Bad request
*/
400: BadRequestError
/**
* Conflict
*/
409: DuplicateIdError
}

export type SessionCreateError = SessionCreateErrors[keyof SessionCreateErrors]
Expand Down
Loading