diff --git a/packages/opencode/src/cli/cmd/export.ts b/packages/opencode/src/cli/cmd/export.ts index 20c95bf87e9..4088b4818d2 100644 --- a/packages/opencode/src/cli/cmd/export.ts +++ b/packages/opencode/src/cli/cmd/export.ts @@ -1,5 +1,6 @@ import type { Argv } from "yargs" import { Session } from "../../session" +import { SessionID } from "../../session/schema" import { cmd } from "./cmd" import { bootstrap } from "../bootstrap" import { UI } from "../ui" @@ -17,7 +18,7 @@ export const ExportCommand = cmd({ }, handler: async (args) => { await bootstrap(process.cwd(), async () => { - let sessionID = args.sessionID + let sessionID = args.sessionID ? SessionID.make(args.sessionID) : undefined process.stderr.write(`Exporting session: ${sessionID ?? "latest"}\n`) if (!sessionID) { @@ -58,7 +59,7 @@ export const ExportCommand = cmd({ throw new UI.CancelledError() } - sessionID = selectedSession as string + sessionID = selectedSession prompts.outro("Exporting session...", { output: process.stderr, @@ -67,7 +68,7 @@ export const ExportCommand = cmd({ try { const sessionInfo = await Session.get(sessionID!) - const messages = await Session.messages({ sessionID: sessionID! }) + const messages = await Session.messages({ sessionID: sessionInfo.id }) const exportData = { info: sessionInfo, diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index a58151308c2..ad893ce4f3e 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -22,6 +22,7 @@ import { ModelsDev } from "../../provider/models" import { Instance } from "@/project/instance" import { bootstrap } from "../bootstrap" import { Session } from "../../session" +import type { SessionID } from "../../session/schema" import { Identifier } from "../../id/id" import { Provider } from "../../provider/provider" import { Bus } from "../../bus" @@ -481,7 +482,7 @@ export const GithubRunCommand = cmd({ let octoRest: Octokit let octoGraph: typeof graphql let gitConfig: string - let session: { id: string; title: string; version: string } + let session: { id: SessionID; title: string; version: string } let shareId: string | undefined let exitCode = 0 type PromptFiles = Awaited>["promptFiles"] diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index cfea5fdbb39..f06b8f39fe0 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -1,6 +1,7 @@ import type { Argv } from "yargs" import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2" import { Session } from "../../session" +import { SessionID } from "../../session/schema" import { cmd } from "./cmd" import { bootstrap } from "../bootstrap" import { Database } from "../../storage/db" @@ -152,7 +153,12 @@ export const ImportCommand = cmd({ return } - const row = Session.toRow({ ...exportData.info, projectID: Instance.project.id }) + const row = Session.toRow({ + ...exportData.info, + id: SessionID.make(exportData.info.id), + parentID: exportData.info.parentID ? SessionID.make(exportData.info.parentID) : undefined, + projectID: Instance.project.id, + }) Database.use((db) => db .insert(SessionTable) @@ -167,7 +173,7 @@ export const ImportCommand = cmd({ .insert(MessageTable) .values({ id: msg.info.id, - session_id: exportData.info.id, + session_id: row.id, time_created: msg.info.time?.created ?? Date.now(), data: msg.info, }) @@ -182,7 +188,7 @@ export const ImportCommand = cmd({ .values({ id: part.id, message_id: msg.info.id, - session_id: exportData.info.id, + session_id: row.id, data: part, }) .onConflictDoNothing() diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 84840392a67..8acd7480c94 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -1,6 +1,7 @@ import type { Argv } from "yargs" import { cmd } from "./cmd" import { Session } from "../../session" +import { SessionID } from "../../session/schema" import { bootstrap } from "../bootstrap" import { UI } from "../ui" import { Locale } from "../../util/locale" @@ -57,13 +58,14 @@ export const SessionDeleteCommand = cmd({ }, handler: async (args) => { await bootstrap(process.cwd(), async () => { + const sessionID = SessionID.make(args.sessionID) try { - await Session.get(args.sessionID) + await Session.get(sessionID) } catch { UI.error(`Session not found: ${args.sessionID}`) process.exit(1) } - await Session.remove(args.sessionID) + await Session.remove(sessionID) UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL) }) }, diff --git a/packages/opencode/src/cli/cmd/tui/event.ts b/packages/opencode/src/cli/cmd/tui/event.ts index 9466ae54f2d..b2e4b92c551 100644 --- a/packages/opencode/src/cli/cmd/tui/event.ts +++ b/packages/opencode/src/cli/cmd/tui/event.ts @@ -1,5 +1,6 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" +import { SessionID } from "@/session/schema" import z from "zod" export const TuiEvent = { @@ -42,7 +43,7 @@ export const TuiEvent = { SessionSelect: BusEvent.define( "tui.session.select", z.object({ - sessionID: z.string().regex(/^ses/).describe("Session ID to navigate to"), + sessionID: SessionID.zod.describe("Session ID to navigate to"), }), ), } diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index dce7ac8bbc3..3f9f25e1dc4 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -1,4 +1,5 @@ import { BusEvent } from "@/bus/bus-event" +import { SessionID } from "@/session/schema" import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" @@ -14,7 +15,7 @@ export namespace Command { "command.executed", z.object({ name: z.string(), - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, arguments: z.string(), messageID: Identifier.schema("message"), }), diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index f1cd43fdbe5..16a94ec54d8 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -1,5 +1,6 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" +import { SessionID } from "@/session/schema" import z from "zod" import { Log } from "../util/log" import { Identifier } from "../id/id" @@ -24,7 +25,7 @@ export namespace Permission { id: z.string(), type: z.string(), pattern: z.union([z.string(), z.array(z.string())]).optional(), - sessionID: z.string(), + sessionID: SessionID.zod, messageID: z.string(), callID: z.string().optional(), message: z.string(), @@ -43,7 +44,7 @@ export namespace Permission { Replied: BusEvent.define( "permission.replied", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, permissionID: z.string(), response: z.string(), }), diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index 4e47e43c8f6..6bfe0035ba8 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -2,6 +2,7 @@ import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { Config } from "@/config/config" import { Identifier } from "@/id/id" +import { SessionID } from "@/session/schema" import { Instance } from "@/project/instance" import { Database, eq } from "@/storage/db" import { PermissionTable } from "@/session/session.sql" @@ -69,7 +70,7 @@ export namespace PermissionNext { export const Request = z .object({ id: Identifier.schema("permission"), - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, permission: z.string(), patterns: z.string().array(), metadata: z.record(z.string(), z.any()), @@ -100,7 +101,7 @@ export namespace PermissionNext { Replied: BusEvent.define( "permission.replied", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, requestID: z.string(), reply: Reply, }), diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts index c93b74b9a40..ba1fc66506b 100644 --- a/packages/opencode/src/question/index.ts +++ b/packages/opencode/src/question/index.ts @@ -1,6 +1,7 @@ import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { Identifier } from "@/id/id" +import { SessionID } from "@/session/schema" import { Instance } from "@/project/instance" import { Log } from "@/util/log" import z from "zod" @@ -34,7 +35,7 @@ export namespace Question { export const Request = z .object({ id: Identifier.schema("question"), - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, questions: z.array(Info).describe("Questions to ask"), tool: z .object({ @@ -65,7 +66,7 @@ export namespace Question { Replied: BusEvent.define( "question.replied", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, requestID: z.string(), answers: z.array(Answer), }), @@ -73,7 +74,7 @@ export namespace Question { Rejected: BusEvent.define( "question.rejected", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, requestID: z.string(), }), ), @@ -95,7 +96,7 @@ export namespace Question { }) export async function ask(input: { - sessionID: string + sessionID: SessionID questions: Info[] tool?: { messageID: string; callID: string } }): Promise { diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index 12938aeaba0..8e465d7ec23 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -1,6 +1,7 @@ import { Hono } from "hono" import { stream } from "hono/streaming" import { describeRoute, validator, resolver } from "hono-openapi" +import { SessionID } from "@/session/schema" import z from "zod" import { Session } from "../../session" import { MessageV2 } from "../../session/message-v2" @@ -173,7 +174,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, }), ), async (c) => { @@ -258,7 +259,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, }), ), validator( @@ -309,7 +310,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, }), ), validator("json", Session.initialize.schema.omit({ sessionID: true })), @@ -372,7 +373,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, }), ), async (c) => { @@ -401,7 +402,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, }), ), async (c) => { @@ -502,7 +503,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, }), ), validator( @@ -561,7 +562,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, }), ), validator( @@ -605,7 +606,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, messageID: z.string().meta({ description: "Message ID" }), }), ), @@ -640,7 +641,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, messageID: z.string().meta({ description: "Message ID" }), }), ), @@ -674,7 +675,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, messageID: z.string().meta({ description: "Message ID" }), partID: z.string().meta({ description: "Part ID" }), }), @@ -709,7 +710,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, messageID: z.string().meta({ description: "Message ID" }), partID: z.string().meta({ description: "Part ID" }), }), @@ -753,7 +754,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, }), ), validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })), @@ -785,7 +786,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, }), ), validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })), @@ -825,7 +826,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, }), ), validator("json", SessionPrompt.CommandInput.omit({ sessionID: true })), @@ -857,7 +858,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, }), ), validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })), @@ -889,7 +890,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, }), ), validator("json", SessionRevert.RevertInput.omit({ sessionID: true })), @@ -924,7 +925,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, }), ), async (c) => { @@ -955,7 +956,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, permissionID: z.string(), }), ), diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 79884d641ea..d26c3bb6b9f 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -2,6 +2,7 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { Session } from "." import { Identifier } from "../id/id" +import { SessionID } from "./schema" import { Instance } from "../project/instance" import { Provider } from "../provider/provider" import { MessageV2 } from "./message-v2" @@ -22,7 +23,7 @@ export namespace SessionCompaction { Compacted: BusEvent.define( "session.compacted", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, }), ), } @@ -55,7 +56,7 @@ export namespace SessionCompaction { // goes backwards through parts until there are 40_000 tokens worth of tool // calls. then erases output of previous tool calls. idea is to throw away old // tool calls that are no longer relevant. - export async function prune(input: { sessionID: string }) { + export async function prune(input: { sessionID: SessionID }) { const config = await Config.get() if (config.compaction?.prune === false) return log.info("pruning") @@ -101,7 +102,7 @@ export namespace SessionCompaction { export async function process(input: { parentID: string messages: MessageV2.WithParts[] - sessionID: string + sessionID: SessionID abort: AbortSignal auto: boolean overflow?: boolean @@ -295,7 +296,7 @@ When constructing the summary, try to stick to this template: export const create = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, agent: z.string(), model: z.object({ providerID: z.string(), diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index ff499fe2e77..76b550d3098 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -24,6 +24,7 @@ import { Command } from "../command" import { Snapshot } from "@/snapshot" import { WorkspaceContext } from "../control-plane/workspace-context" import { ProjectID } from "../project/schema" +import { SessionID } from "./schema" import type { Provider } from "@/provider/provider" import { PermissionNext } from "@/permission/next" @@ -119,12 +120,12 @@ export namespace Session { export const Info = z .object({ - id: Identifier.schema("session"), + id: SessionID.zod, slug: z.string(), projectID: ProjectID.zod, workspaceID: z.string().optional(), directory: z.string(), - parentID: Identifier.schema("session").optional(), + parentID: SessionID.zod.optional(), summary: z .object({ additions: z.number(), @@ -201,14 +202,14 @@ export namespace Session { Diff: BusEvent.define( "session.diff", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, diff: Snapshot.FileDiff.array(), }), ), Error: BusEvent.define( "session.error", z.object({ - sessionID: z.string().optional(), + sessionID: SessionID.zod.optional(), error: MessageV2.Assistant.shape.error, }), ), @@ -217,7 +218,7 @@ export namespace Session { export const create = fn( z .object({ - parentID: Identifier.schema("session").optional(), + parentID: SessionID.zod.optional(), title: z.string().optional(), permission: Info.shape.permission, workspaceID: Identifier.schema("workspace").optional(), @@ -236,7 +237,7 @@ export namespace Session { export const fork = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, messageID: Identifier.schema("message").optional(), }), async (input) => { @@ -277,7 +278,7 @@ export namespace Session { }, ) - export const touch = fn(Identifier.schema("session"), async (sessionID) => { + export const touch = fn(SessionID.zod, async (sessionID) => { const now = Date.now() Database.use((db) => { const row = db @@ -293,15 +294,15 @@ export namespace Session { }) export async function createNext(input: { - id?: string + id?: SessionID title?: string - parentID?: string + parentID?: SessionID workspaceID?: string directory: string permission?: PermissionNext.Ruleset }) { const result: Info = { - id: Identifier.descending("session", input.id), + id: SessionID.descending(input.id), slug: Slug.create(), version: Installation.VERSION, projectID: Instance.project.id, @@ -342,13 +343,13 @@ export namespace Session { return path.join(base, [input.time.created, input.slug].join("-") + ".md") } - export const get = fn(Identifier.schema("session"), async (id) => { + export const get = fn(SessionID.zod, async (id) => { const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) if (!row) throw new NotFoundError({ message: `Session not found: ${id}` }) return fromRow(row) }) - export const share = fn(Identifier.schema("session"), async (id) => { + export const share = fn(SessionID.zod, async (id) => { const cfg = await Config.get() if (cfg.share === "disabled") { throw new Error("Sharing is disabled in configuration") @@ -364,7 +365,7 @@ export namespace Session { return share }) - export const unshare = fn(Identifier.schema("session"), async (id) => { + export const unshare = fn(SessionID.zod, async (id) => { // Use ShareNext to remove the share (same as share function uses ShareNext to create) const { ShareNext } = await import("@/share/share-next") await ShareNext.remove(id) @@ -378,7 +379,7 @@ export namespace Session { export const setTitle = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, title: z.string(), }), async (input) => { @@ -399,7 +400,7 @@ export namespace Session { export const setArchived = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, time: z.number().optional(), }), async (input) => { @@ -420,7 +421,7 @@ export namespace Session { export const setPermission = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, permission: PermissionNext.Ruleset, }), async (input) => { @@ -441,7 +442,7 @@ export namespace Session { export const setRevert = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, revert: Info.shape.revert, summary: Info.shape.summary, }), @@ -467,7 +468,7 @@ export namespace Session { }, ) - export const clearRevert = fn(Identifier.schema("session"), async (sessionID) => { + export const clearRevert = fn(SessionID.zod, async (sessionID) => { return Database.use((db) => { const row = db .update(SessionTable) @@ -487,7 +488,7 @@ export namespace Session { export const setSummary = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, summary: Info.shape.summary, }), async (input) => { @@ -511,7 +512,7 @@ export namespace Session { }, ) - export const diff = fn(Identifier.schema("session"), async (sessionID) => { + export const diff = fn(SessionID.zod, async (sessionID) => { try { return await Storage.read(["session_diff", sessionID]) } catch { @@ -521,7 +522,7 @@ export namespace Session { export const messages = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, limit: z.number().optional(), }), async (input) => { @@ -647,7 +648,7 @@ export namespace Session { } } - export const children = fn(Identifier.schema("session"), async (parentID) => { + export const children = fn(SessionID.zod, async (parentID) => { const project = Instance.project const rows = Database.use((db) => db @@ -659,7 +660,7 @@ export namespace Session { return rows.map(fromRow) }) - export const remove = fn(Identifier.schema("session"), async (sessionID) => { + export const remove = fn(SessionID.zod, async (sessionID) => { const project = Instance.project try { const session = await get(sessionID) @@ -705,7 +706,7 @@ export namespace Session { export const removeMessage = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, messageID: Identifier.schema("message"), }), async (input) => { @@ -727,7 +728,7 @@ export namespace Session { export const removePart = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, messageID: Identifier.schema("message"), partID: Identifier.schema("part"), }), @@ -775,7 +776,7 @@ export namespace Session { export const updatePartDelta = fn( z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, messageID: z.string(), partID: z.string(), field: z.string(), @@ -873,7 +874,7 @@ export namespace Session { export const initialize = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, modelID: z.string(), providerID: z.string(), messageID: Identifier.schema("message"), diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 5b4e7bdbc04..43d735e4bdb 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -1,4 +1,5 @@ import { BusEvent } from "@/bus/bus-event" +import { SessionID } from "./schema" import z from "zod" import { NamedError } from "@opencode-ai/util/error" import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai" @@ -79,7 +80,7 @@ export namespace MessageV2 { const PartBase = z.object({ id: z.string(), - sessionID: z.string(), + sessionID: SessionID.zod, messageID: z.string(), }) @@ -344,7 +345,7 @@ export namespace MessageV2 { const Base = z.object({ id: z.string(), - sessionID: z.string(), + sessionID: SessionID.zod, }) export const User = Base.extend({ @@ -457,7 +458,7 @@ export namespace MessageV2 { Removed: BusEvent.define( "message.removed", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, messageID: z.string(), }), ), @@ -470,7 +471,7 @@ export namespace MessageV2 { PartDelta: BusEvent.define( "message.part.delta", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, messageID: z.string(), partID: z.string(), field: z.string(), @@ -480,7 +481,7 @@ export namespace MessageV2 { PartRemoved: BusEvent.define( "message.part.removed", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, messageID: z.string(), partID: z.string(), }), @@ -728,7 +729,7 @@ export namespace MessageV2 { ) } - export const stream = fn(Identifier.schema("session"), async function* (sessionID) { + export const stream = fn(SessionID.zod, async function* (sessionID) { const size = 50 let offset = 0 while (true) { @@ -792,7 +793,7 @@ export namespace MessageV2 { export const get = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, messageID: Identifier.schema("message"), }), async (input): Promise => { diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts index 5c950d0e402..691057db207 100644 --- a/packages/opencode/src/session/message.ts +++ b/packages/opencode/src/session/message.ts @@ -1,4 +1,5 @@ import z from "zod" +import { SessionID } from "./schema" import { NamedError } from "@opencode-ai/util/error" export namespace Message { @@ -142,7 +143,7 @@ export namespace Message { error: z .discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema]) .optional(), - sessionID: z.string(), + sessionID: SessionID.zod, tool: z.record( z.string(), z diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 67edc0ecfe3..33c6c9e4270 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -15,6 +15,7 @@ import { Config } from "@/config/config" import { SessionCompaction } from "./compaction" import { PermissionNext } from "@/permission/next" import { Question } from "@/question" +import type { SessionID } from "./schema" export namespace SessionProcessor { const DOOM_LOOP_THRESHOLD = 3 @@ -25,7 +26,7 @@ export namespace SessionProcessor { export function create(input: { assistantMessage: MessageV2.Assistant - sessionID: string + sessionID: SessionID model: Provider.Model abort: AbortSignal }) { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 54adf1104a1..5fe9121486a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -4,6 +4,7 @@ import fs from "fs/promises" import z from "zod" import { Filesystem } from "../util/filesystem" import { Identifier } from "../id/id" +import { SessionID } from "./schema" import { MessageV2 } from "./message-v2" import { Log } from "../util/log" import { SessionRevert } from "./revert" @@ -84,13 +85,13 @@ export namespace SessionPrompt { }, ) - export function assertNotBusy(sessionID: string) { + export function assertNotBusy(sessionID: SessionID) { const match = state()[sessionID] if (match) throw new Session.BusyError(sessionID) } export const PromptInput = z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, messageID: Identifier.schema("message").optional(), model: z .object({ @@ -254,7 +255,7 @@ export namespace SessionPrompt { return s[sessionID].abort.signal } - export function cancel(sessionID: string) { + export function cancel(sessionID: SessionID) { log.info("cancel", { sessionID }) const s = state() const match = s[sessionID] @@ -269,7 +270,7 @@ export namespace SessionPrompt { } export const LoopInput = z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, resume_existing: z.boolean().optional(), }) export const loop = fn(LoopInput, async (input) => { @@ -731,7 +732,7 @@ export namespace SessionPrompt { throw new Error("Impossible") }) - async function lastModel(sessionID: string) { + async function lastModel(sessionID: SessionID) { for await (const item of MessageV2.stream(sessionID)) { if (item.info.role === "user" && item.info.model) return item.info.model } @@ -1467,7 +1468,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the } export const ShellInput = z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, agent: z.string(), model: z .object({ @@ -1719,7 +1720,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the export const CommandInput = z.object({ messageID: Identifier.schema("message").optional(), - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, agent: z.string().optional(), model: z.string().optional(), arguments: z.string(), diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index ef9c7e2aace..9b42ab350af 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -1,5 +1,6 @@ import z from "zod" import { Identifier } from "../id/id" +import { SessionID } from "./schema" import { Snapshot } from "../snapshot" import { MessageV2 } from "./message-v2" import { Session } from "." @@ -15,7 +16,7 @@ export namespace SessionRevert { const log = Log.create({ service: "session.revert" }) export const RevertInput = z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, messageID: Identifier.schema("message"), partID: Identifier.schema("part").optional(), }) @@ -79,7 +80,7 @@ export namespace SessionRevert { return session } - export async function unrevert(input: { sessionID: string }) { + export async function unrevert(input: { sessionID: SessionID }) { log.info("unreverting", input) SessionPrompt.assertNotBusy(input.sessionID) const session = await Session.get(input.sessionID) diff --git a/packages/opencode/src/session/schema.ts b/packages/opencode/src/session/schema.ts new file mode 100644 index 00000000000..5277f5e0b60 --- /dev/null +++ b/packages/opencode/src/session/schema.ts @@ -0,0 +1,17 @@ +import { Schema } from "effect" +import z from "zod" + +import { withStatics } from "@/util/schema" +import { Identifier } from "@/id/id" + +const sessionIdSchema = Schema.String.pipe(Schema.brand("SessionId")) + +export type SessionID = typeof sessionIdSchema.Type + +export const SessionID = sessionIdSchema.pipe( + withStatics((schema: typeof sessionIdSchema) => ({ + make: (id: string) => schema.makeUnsafe(id), + descending: (id?: string) => schema.makeUnsafe(Identifier.descending("session", id)), + zod: z.string().startsWith("ses").pipe(z.custom()), + })), +) diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index 0b62dbbd8c2..309b623b864 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -4,6 +4,7 @@ import type { MessageV2 } from "./message-v2" import type { Snapshot } from "../snapshot" import type { PermissionNext } from "../permission/next" import type { ProjectID } from "../project/schema" +import type { SessionID } from "./schema" import { Timestamps } from "../storage/schema.sql" type PartData = Omit @@ -12,13 +13,13 @@ type InfoData = Omit export const SessionTable = sqliteTable( "session", { - id: text().primaryKey(), + id: text().$type().primaryKey(), project_id: text() .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), workspace_id: text(), - parent_id: text(), + parent_id: text().$type(), slug: text().notNull(), directory: text().notNull(), title: text().notNull(), @@ -46,6 +47,7 @@ export const MessageTable = sqliteTable( { id: text().primaryKey(), session_id: text() + .$type() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), ...Timestamps, @@ -61,7 +63,7 @@ export const PartTable = sqliteTable( message_id: text() .notNull() .references(() => MessageTable.id, { onDelete: "cascade" }), - session_id: text().notNull(), + session_id: text().$type().notNull(), ...Timestamps, data: text({ mode: "json" }).notNull().$type(), }, @@ -72,6 +74,7 @@ export const TodoTable = sqliteTable( "todo", { session_id: text() + .$type() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), content: text().notNull(), diff --git a/packages/opencode/src/session/status.ts b/packages/opencode/src/session/status.ts index 1db03b5db0d..57e7939853a 100644 --- a/packages/opencode/src/session/status.ts +++ b/packages/opencode/src/session/status.ts @@ -1,6 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { Instance } from "@/project/instance" +import { SessionID } from "./schema" import z from "zod" export namespace SessionStatus { @@ -28,7 +29,7 @@ export namespace SessionStatus { Status: BusEvent.define( "session.status", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, status: Info, }), ), @@ -36,7 +37,7 @@ export namespace SessionStatus { Idle: BusEvent.define( "session.idle", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, }), ), } @@ -46,7 +47,7 @@ export namespace SessionStatus { return data }) - export function get(sessionID: string) { + export function get(sessionID: SessionID) { return ( state()[sessionID] ?? { type: "idle", @@ -58,7 +59,7 @@ export namespace SessionStatus { return state() } - export function set(sessionID: string, status: Info) { + export function set(sessionID: SessionID, status: Info) { Bus.publish(Event.Status, { sessionID, status, diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 349336ba788..f489c9a6288 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -4,6 +4,7 @@ import { Session } from "." import { MessageV2 } from "./message-v2" import { Identifier } from "@/id/id" +import { SessionID } from "./schema" import { Snapshot } from "@/snapshot" import { Storage } from "@/storage/storage" @@ -68,7 +69,7 @@ export namespace SessionSummary { export const summarize = fn( z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, messageID: z.string(), }), async (input) => { @@ -80,7 +81,7 @@ export namespace SessionSummary { }, ) - async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) { + async function summarizeSession(input: { sessionID: SessionID; messages: MessageV2.WithParts[] }) { const diffs = await computeDiff({ messages: input.messages }) await Session.setSummary({ sessionID: input.sessionID, @@ -113,7 +114,7 @@ export namespace SessionSummary { export const diff = fn( z.object({ - sessionID: Identifier.schema("session"), + sessionID: SessionID.zod, messageID: Identifier.schema("message").optional(), }), async (input) => { diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index ec2bcdda3c4..02ad0d3b337 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -1,5 +1,6 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" +import { SessionID } from "./schema" import z from "zod" import { Database, eq, asc } from "../storage/db" import { TodoTable } from "./session.sql" @@ -18,13 +19,13 @@ export namespace Todo { Updated: BusEvent.define( "todo.updated", z.object({ - sessionID: z.string(), + sessionID: SessionID.zod, todos: z.array(Info), }), ), } - export function update(input: { sessionID: string; todos: Info[] }) { + export function update(input: { sessionID: SessionID; todos: Info[] }) { Database.transaction((db) => { db.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run() if (input.todos.length === 0) return @@ -43,7 +44,7 @@ export namespace Todo { Bus.publish(Event.Updated, input) } - export function get(sessionID: string) { + export function get(sessionID: SessionID) { const rows = Database.use((db) => db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).orderBy(asc(TodoTable.position)).all(), ) diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 73fd21d9059..d1b09e4bf22 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -3,6 +3,7 @@ import { Account } from "@/account" import { Config } from "@/config/config" import { Provider } from "@/provider/provider" import { Session } from "@/session" +import type { SessionID } from "@/session/schema" import { MessageV2 } from "@/session/message-v2" import { Database, eq } from "@/storage/db" import { SessionShareTable } from "./share.sql" @@ -109,7 +110,7 @@ export namespace ShareNext { }) } - export async function create(sessionID: string) { + export async function create(sessionID: SessionID) { if (disabled) return { id: "", url: "", secret: "" } log.info("creating share", { sessionID }) const req = await request() @@ -140,7 +141,7 @@ export namespace ShareNext { return result } - function get(sessionID: string) { + function get(sessionID: SessionID) { const row = Database.use((db) => db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).get(), ) @@ -186,7 +187,7 @@ export namespace ShareNext { } const queue = new Map }>() - async function sync(sessionID: string, data: Data[]) { + async function sync(sessionID: SessionID, data: Data[]) { if (disabled) return const existing = queue.get(sessionID) if (existing) { @@ -225,7 +226,7 @@ export namespace ShareNext { queue.set(sessionID, { timeout, data: dataMap }) } - export async function remove(sessionID: string) { + export async function remove(sessionID: SessionID) { if (disabled) return log.info("removing share", { sessionID }) const share = get(sessionID) @@ -248,7 +249,7 @@ export namespace ShareNext { Database.use((db) => db.delete(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).run()) } - async function fullSync(sessionID: string) { + async function fullSync(sessionID: SessionID) { log.info("full sync", { sessionID }) const session = await Session.get(sessionID) const diffs = await Session.diff(sessionID) diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts index ff84dccec44..0c25cf3e95e 100644 --- a/packages/opencode/src/tool/plan.ts +++ b/packages/opencode/src/tool/plan.ts @@ -7,9 +7,10 @@ import { MessageV2 } from "../session/message-v2" import { Identifier } from "../id/id" import { Provider } from "../provider/provider" import { Instance } from "../project/instance" +import type { SessionID } from "../session/schema" import EXIT_DESCRIPTION from "./plan-exit.txt" -async function getLastModel(sessionID: string) { +async function getLastModel(sessionID: SessionID) { for await (const item of MessageV2.stream(sessionID)) { if (item.info.role === "user" && item.info.model) return item.info.model } diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 8c8cf827aba..2304a2e2bf3 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -2,6 +2,7 @@ import { Tool } from "./tool" import DESCRIPTION from "./task.txt" import z from "zod" import { Session } from "../session" +import { SessionID } from "../session/schema" import { MessageV2 } from "../session/message-v2" import { Identifier } from "../id/id" import { Agent } from "../agent/agent" @@ -65,7 +66,7 @@ export const TaskTool = Tool.define("task", async (ctx) => { const session = await iife(async () => { if (params.task_id) { - const found = await Session.get(params.task_id).catch(() => {}) + const found = await Session.get(SessionID.make(params.task_id)).catch(() => {}) if (found) return found } diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 0e78ba665cf..7f58520c56d 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -2,6 +2,7 @@ import z from "zod" import type { MessageV2 } from "../session/message-v2" import type { Agent } from "../agent/agent" import type { PermissionNext } from "../permission/next" +import type { SessionID } from "../session/schema" import { Truncate } from "./truncation" export namespace Tool { @@ -14,7 +15,7 @@ export namespace Tool { } export type Context = { - sessionID: string + sessionID: SessionID messageID: string agent: string abort: AbortSignal diff --git a/packages/opencode/test/cli/github-action.test.ts b/packages/opencode/test/cli/github-action.test.ts index cd64bb59ec8..c2bd293d79f 100644 --- a/packages/opencode/test/cli/github-action.test.ts +++ b/packages/opencode/test/cli/github-action.test.ts @@ -1,12 +1,13 @@ import { test, expect, describe } from "bun:test" import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github" import type { MessageV2 } from "../../src/session/message-v2" +import { SessionID } from "../../src/session/schema" // Helper to create minimal valid parts function createTextPart(text: string): MessageV2.Part { return { id: "1", - sessionID: "s", + sessionID: SessionID.make("s"), messageID: "m", type: "text" as const, text, @@ -16,7 +17,7 @@ function createTextPart(text: string): MessageV2.Part { function createReasoningPart(text: string): MessageV2.Part { return { id: "1", - sessionID: "s", + sessionID: SessionID.make("s"), messageID: "m", type: "reasoning" as const, text, @@ -28,7 +29,7 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn if (status === "completed") { return { id: "1", - sessionID: "s", + sessionID: SessionID.make("s"), messageID: "m", type: "tool" as const, callID: "c1", @@ -45,7 +46,7 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn } return { id: "1", - sessionID: "s", + sessionID: SessionID.make("s"), messageID: "m", type: "tool" as const, callID: "c1", @@ -61,7 +62,7 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn function createStepStartPart(): MessageV2.Part { return { id: "1", - sessionID: "s", + sessionID: SessionID.make("s"), messageID: "m", type: "step-start" as const, } @@ -70,7 +71,7 @@ function createStepStartPart(): MessageV2.Part { function createStepFinishPart(): MessageV2.Part { return { id: "1", - sessionID: "s", + sessionID: SessionID.make("s"), messageID: "m", type: "step-finish" as const, reason: "done", diff --git a/packages/opencode/test/memory/abort-leak.test.ts b/packages/opencode/test/memory/abort-leak.test.ts index b202c9127a8..68770bf17ac 100644 --- a/packages/opencode/test/memory/abort-leak.test.ts +++ b/packages/opencode/test/memory/abort-leak.test.ts @@ -2,11 +2,12 @@ import { describe, test, expect } from "bun:test" import path from "path" import { Instance } from "../../src/project/instance" import { WebFetchTool } from "../../src/tool/webfetch" +import { SessionID } from "../../src/session/schema" const projectRoot = path.join(__dirname, "../..") const ctx = { - sessionID: "test", + sessionID: SessionID.make("ses_test"), messageID: "", callID: "", agent: "build", diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index add3332048c..b66b40dc09d 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -3,6 +3,7 @@ import os from "os" import { PermissionNext } from "../../src/permission/next" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" +import { SessionID } from "../../src/session/schema" // fromConfig tests @@ -462,7 +463,7 @@ test("ask - resolves immediately when action is allow", async () => { directory: tmp.path, fn: async () => { const result = await PermissionNext.ask({ - sessionID: "session_test", + sessionID: SessionID.make("session_test"), permission: "bash", patterns: ["ls"], metadata: {}, @@ -481,7 +482,7 @@ test("ask - throws RejectedError when action is deny", async () => { fn: async () => { await expect( PermissionNext.ask({ - sessionID: "session_test", + sessionID: SessionID.make("session_test"), permission: "bash", patterns: ["rm -rf /"], metadata: {}, @@ -499,7 +500,7 @@ test("ask - returns pending promise when action is ask", async () => { directory: tmp.path, fn: async () => { const promise = PermissionNext.ask({ - sessionID: "session_test", + sessionID: SessionID.make("session_test"), permission: "bash", patterns: ["ls"], metadata: {}, @@ -522,7 +523,7 @@ test("reply - once resolves the pending ask", async () => { fn: async () => { const askPromise = PermissionNext.ask({ id: "permission_test1", - sessionID: "session_test", + sessionID: SessionID.make("session_test"), permission: "bash", patterns: ["ls"], metadata: {}, @@ -547,7 +548,7 @@ test("reply - reject throws RejectedError", async () => { fn: async () => { const askPromise = PermissionNext.ask({ id: "permission_test2", - sessionID: "session_test", + sessionID: SessionID.make("session_test"), permission: "bash", patterns: ["ls"], metadata: {}, @@ -572,7 +573,7 @@ test("reply - always persists approval and resolves", async () => { fn: async () => { const askPromise = PermissionNext.ask({ id: "permission_test3", - sessionID: "session_test", + sessionID: SessionID.make("session_test"), permission: "bash", patterns: ["ls"], metadata: {}, @@ -594,7 +595,7 @@ test("reply - always persists approval and resolves", async () => { fn: async () => { // Stored approval should allow without asking const result = await PermissionNext.ask({ - sessionID: "session_test2", + sessionID: SessionID.make("session_test2"), permission: "bash", patterns: ["ls"], metadata: {}, @@ -613,7 +614,7 @@ test("reply - reject cancels all pending for same session", async () => { fn: async () => { const askPromise1 = PermissionNext.ask({ id: "permission_test4a", - sessionID: "session_same", + sessionID: SessionID.make("session_same"), permission: "bash", patterns: ["ls"], metadata: {}, @@ -623,7 +624,7 @@ test("reply - reject cancels all pending for same session", async () => { const askPromise2 = PermissionNext.ask({ id: "permission_test4b", - sessionID: "session_same", + sessionID: SessionID.make("session_same"), permission: "edit", patterns: ["foo.ts"], metadata: {}, @@ -655,7 +656,7 @@ test("ask - checks all patterns and stops on first deny", async () => { fn: async () => { await expect( PermissionNext.ask({ - sessionID: "session_test", + sessionID: SessionID.make("session_test"), permission: "bash", patterns: ["echo hello", "rm -rf /"], metadata: {}, @@ -676,7 +677,7 @@ test("ask - allows all patterns when all match allow rules", async () => { directory: tmp.path, fn: async () => { const result = await PermissionNext.ask({ - sessionID: "session_test", + sessionID: SessionID.make("session_test"), permission: "bash", patterns: ["echo hello", "ls -la", "pwd"], metadata: {}, diff --git a/packages/opencode/test/question/question.test.ts b/packages/opencode/test/question/question.test.ts index cf24faa7d22..c1c68c74724 100644 --- a/packages/opencode/test/question/question.test.ts +++ b/packages/opencode/test/question/question.test.ts @@ -2,6 +2,7 @@ import { test, expect } from "bun:test" import { Question } from "../../src/question" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" +import { SessionID } from "../../src/session/schema" test("ask - returns pending promise", async () => { await using tmp = await tmpdir({ git: true }) @@ -9,7 +10,7 @@ test("ask - returns pending promise", async () => { directory: tmp.path, fn: async () => { const promise = Question.ask({ - sessionID: "ses_test", + sessionID: SessionID.make("ses_test"), questions: [ { question: "What would you like to do?", @@ -43,7 +44,7 @@ test("ask - adds to pending list", async () => { ] Question.ask({ - sessionID: "ses_test", + sessionID: SessionID.make("ses_test"), questions, }) @@ -73,7 +74,7 @@ test("reply - resolves the pending ask with answers", async () => { ] const askPromise = Question.ask({ - sessionID: "ses_test", + sessionID: SessionID.make("ses_test"), questions, }) @@ -97,7 +98,7 @@ test("reply - removes from pending list", async () => { directory: tmp.path, fn: async () => { Question.ask({ - sessionID: "ses_test", + sessionID: SessionID.make("ses_test"), questions: [ { question: "What would you like to do?", @@ -146,7 +147,7 @@ test("reject - throws RejectedError", async () => { directory: tmp.path, fn: async () => { const askPromise = Question.ask({ - sessionID: "ses_test", + sessionID: SessionID.make("ses_test"), questions: [ { question: "What would you like to do?", @@ -173,7 +174,7 @@ test("reject - removes from pending list", async () => { directory: tmp.path, fn: async () => { const askPromise = Question.ask({ - sessionID: "ses_test", + sessionID: SessionID.make("ses_test"), questions: [ { question: "What would you like to do?", @@ -236,7 +237,7 @@ test("ask - handles multiple questions", async () => { ] const askPromise = Question.ask({ - sessionID: "ses_test", + sessionID: SessionID.make("ses_test"), questions, }) @@ -261,7 +262,7 @@ test("list - returns all pending requests", async () => { directory: tmp.path, fn: async () => { Question.ask({ - sessionID: "ses_test1", + sessionID: SessionID.make("ses_test1"), questions: [ { question: "Question 1?", @@ -272,7 +273,7 @@ test("list - returns all pending requests", async () => { }) Question.ask({ - sessionID: "ses_test2", + sessionID: SessionID.make("ses_test2"), questions: [ { question: "Question 2?", diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index a89a00ebc05..871fd8cb57e 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -11,6 +11,7 @@ import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" import type { Agent } from "../../src/agent/agent" import type { MessageV2 } from "../../src/session/message-v2" +import { SessionID } from "../../src/session/schema" describe("session.llm.hasToolCalls", () => { test("returns false for empty messages array", () => { @@ -265,7 +266,7 @@ describe("session.llm.stream", () => { directory: tmp.path, fn: async () => { const resolved = await Provider.getModel(providerID, model.id) - const sessionID = "session-test-1" + const sessionID = SessionID.make("session-test-1") const agent = { name: "test", mode: "primary", @@ -395,7 +396,7 @@ describe("session.llm.stream", () => { directory: tmp.path, fn: async () => { const resolved = await Provider.getModel("openai", model.id) - const sessionID = "session-test-2" + const sessionID = SessionID.make("session-test-2") const agent = { name: "test", mode: "primary", @@ -517,7 +518,7 @@ describe("session.llm.stream", () => { directory: tmp.path, fn: async () => { const resolved = await Provider.getModel(providerID, model.id) - const sessionID = "session-test-3" + const sessionID = SessionID.make("session-test-3") const agent = { name: "test", mode: "primary", @@ -618,7 +619,7 @@ describe("session.llm.stream", () => { directory: tmp.path, fn: async () => { const resolved = await Provider.getModel(providerID, model.id) - const sessionID = "session-test-4" + const sessionID = SessionID.make("session-test-4") const agent = { name: "test", mode: "primary", diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 184bcd3efad..158d341f9eb 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -2,8 +2,9 @@ import { describe, expect, test } from "bun:test" import { APICallError } from "ai" import { MessageV2 } from "../../src/session/message-v2" import type { Provider } from "../../src/provider/provider" +import { SessionID } from "../../src/session/schema" -const sessionID = "session" +const sessionID = SessionID.make("session") const model: Provider.Model = { id: "test-model", providerID: "test", diff --git a/packages/opencode/test/session/structured-output.test.ts b/packages/opencode/test/session/structured-output.test.ts index 2be4257dc78..537df07af6d 100644 --- a/packages/opencode/test/session/structured-output.test.ts +++ b/packages/opencode/test/session/structured-output.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { MessageV2 } from "../../src/session/message-v2" import { SessionPrompt } from "../../src/session/prompt" +import { SessionID } from "../../src/session/schema" describe("structured-output.OutputFormat", () => { test("parses text format", () => { @@ -96,7 +97,7 @@ describe("structured-output.UserMessage", () => { test("user message accepts outputFormat", () => { const result = MessageV2.User.safeParse({ id: "test-id", - sessionID: "test-session", + sessionID: SessionID.descending(), role: "user", time: { created: Date.now() }, agent: "default", @@ -112,7 +113,7 @@ describe("structured-output.UserMessage", () => { test("user message works without outputFormat (optional)", () => { const result = MessageV2.User.safeParse({ id: "test-id", - sessionID: "test-session", + sessionID: SessionID.descending(), role: "user", time: { created: Date.now() }, agent: "default", @@ -125,7 +126,7 @@ describe("structured-output.UserMessage", () => { describe("structured-output.AssistantMessage", () => { const baseAssistantMessage = { id: "test-id", - sessionID: "test-session", + sessionID: SessionID.descending(), role: "assistant" as const, parentID: "parent-id", modelID: "claude-3", diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index 76dfb8b4502..f5f8cbd5cd0 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -11,6 +11,7 @@ import { ProjectTable } from "../../src/project/project.sql" import { ProjectID } from "../../src/project/schema" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql" import { SessionShareTable } from "../../src/share/share.sql" +import { SessionID } from "../../src/session/schema" // Test fixtures const fixtures = { @@ -220,7 +221,7 @@ describe("JSON to SQLite migration", () => { const db = drizzle({ client: sqlite }) const sessions = db.select().from(SessionTable).all() expect(sessions.length).toBe(1) - expect(sessions[0].id).toBe("ses_test456def") + expect(sessions[0].id).toBe(SessionID.make("ses_test456def")) expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc")) expect(sessions[0].slug).toBe("test-session") expect(sessions[0].title).toBe("Test Session Title") @@ -295,7 +296,7 @@ describe("JSON to SQLite migration", () => { const messages = db.select().from(MessageTable).all() expect(messages.length).toBe(1) expect(messages[0].id).toBe("msg_test789ghi") - expect(messages[0].session_id).toBe("ses_test456def") + expect(messages[0].session_id).toBe(SessionID.make("ses_test456def")) expect(messages[0].data).not.toHaveProperty("id") expect(messages[0].data).not.toHaveProperty("sessionID") @@ -303,7 +304,7 @@ describe("JSON to SQLite migration", () => { expect(parts.length).toBe(1) expect(parts[0].id).toBe("prt_testabc123") expect(parts[0].message_id).toBe("msg_test789ghi") - expect(parts[0].session_id).toBe("ses_test456def") + expect(parts[0].session_id).toBe(SessionID.make("ses_test456def")) expect(parts[0].data).not.toHaveProperty("id") expect(parts[0].data).not.toHaveProperty("messageID") expect(parts[0].data).not.toHaveProperty("sessionID") @@ -336,7 +337,7 @@ describe("JSON to SQLite migration", () => { const messages = db.select().from(MessageTable).all() expect(messages.length).toBe(1) expect(messages[0].id).toBe("msg_from_filename") // Uses filename, not JSON id - expect(messages[0].session_id).toBe("ses_test456def") + expect(messages[0].session_id).toBe(SessionID.make("ses_test456def")) }) test("uses paths for part id and messageID when JSON has different values", async () => { @@ -426,7 +427,7 @@ describe("JSON to SQLite migration", () => { const db = drizzle({ client: sqlite }) const sessions = db.select().from(SessionTable).all() expect(sessions.length).toBe(1) - expect(sessions[0].id).toBe("ses_migrated") + expect(sessions[0].id).toBe(SessionID.make("ses_migrated")) expect(sessions[0].project_id).toBe(ProjectID.make(gitBasedProjectID)) // Uses directory, not stale JSON }) @@ -458,7 +459,7 @@ describe("JSON to SQLite migration", () => { const db = drizzle({ client: sqlite }) const sessions = db.select().from(SessionTable).all() expect(sessions.length).toBe(1) - expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id + expect(sessions[0].id).toBe(SessionID.make("ses_from_filename")) // Uses filename, not JSON id expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc")) }) diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts index f81723fee09..008b4fa49c6 100644 --- a/packages/opencode/test/tool/apply_patch.test.ts +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -4,9 +4,10 @@ import * as fs from "fs/promises" import { ApplyPatchTool } from "../../src/tool/apply_patch" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" +import { SessionID } from "../../src/session/schema" const baseCtx = { - sessionID: "test", + sessionID: SessionID.make("ses_test"), messageID: "", callID: "", agent: "build", diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index ac93016927a..d4a6c7ccdb5 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -7,9 +7,10 @@ import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" import type { PermissionNext } from "../../src/permission/next" import { Truncate } from "../../src/tool/truncation" +import { SessionID } from "../../src/session/schema" const ctx = { - sessionID: "test", + sessionID: SessionID.make("ses_test"), messageID: "", callID: "", agent: "build", diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index bcf75da05cd..601995889ed 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -5,9 +5,10 @@ import { EditTool } from "../../src/tool/edit" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" import { FileTime } from "../../src/file/time" +import { SessionID } from "../../src/session/schema" const ctx = { - sessionID: "test-edit-session", + sessionID: SessionID.make("ses_test-edit-session"), messageID: "", callID: "", agent: "build", diff --git a/packages/opencode/test/tool/external-directory.test.ts b/packages/opencode/test/tool/external-directory.test.ts index a75f767b3b6..fe33d69151d 100644 --- a/packages/opencode/test/tool/external-directory.test.ts +++ b/packages/opencode/test/tool/external-directory.test.ts @@ -4,9 +4,10 @@ import type { Tool } from "../../src/tool/tool" import { Instance } from "../../src/project/instance" import { assertExternalDirectory } from "../../src/tool/external-directory" import type { PermissionNext } from "../../src/permission/next" +import { SessionID } from "../../src/session/schema" const baseCtx: Omit = { - sessionID: "test", + sessionID: SessionID.make("ses_test"), messageID: "", callID: "", agent: "build", diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index e774580df61..5823553722e 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -3,9 +3,10 @@ import path from "path" import { GrepTool } from "../../src/tool/grep" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" +import { SessionID } from "../../src/session/schema" const ctx = { - sessionID: "test", + sessionID: SessionID.make("ses_test"), messageID: "", callID: "", agent: "build", diff --git a/packages/opencode/test/tool/question.test.ts b/packages/opencode/test/tool/question.test.ts index 4a436186db6..69e211abdd5 100644 --- a/packages/opencode/test/tool/question.test.ts +++ b/packages/opencode/test/tool/question.test.ts @@ -2,9 +2,10 @@ import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test" import { z } from "zod" import { QuestionTool } from "../../src/tool/question" import * as QuestionModule from "../../src/question" +import { SessionID } from "../../src/session/schema" const ctx = { - sessionID: "test-session", + sessionID: SessionID.make("ses_test-session"), messageID: "test-message", callID: "test-call", agent: "test-agent", diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index b22fc3e7120..45f3c1b32e8 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -6,11 +6,12 @@ import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" import { PermissionNext } from "../../src/permission/next" import { Agent } from "../../src/agent/agent" +import { SessionID } from "../../src/session/schema" const FIXTURES_DIR = path.join(import.meta.dir, "fixtures") const ctx = { - sessionID: "test", + sessionID: SessionID.make("ses_test"), messageID: "", callID: "", agent: "build", diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index d5057ba9e7f..3db08414edb 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -6,9 +6,10 @@ import type { Tool } from "../../src/tool/tool" import { Instance } from "../../src/project/instance" import { SkillTool } from "../../src/tool/skill" import { tmpdir } from "../fixture/fixture" +import { SessionID } from "../../src/session/schema" const baseCtx: Omit = { - sessionID: "test", + sessionID: SessionID.make("ses_test"), messageID: "", callID: "", agent: "build", diff --git a/packages/opencode/test/tool/webfetch.test.ts b/packages/opencode/test/tool/webfetch.test.ts index 0214700fedc..769a61e1656 100644 --- a/packages/opencode/test/tool/webfetch.test.ts +++ b/packages/opencode/test/tool/webfetch.test.ts @@ -2,11 +2,12 @@ import { describe, expect, test } from "bun:test" import path from "path" import { Instance } from "../../src/project/instance" import { WebFetchTool } from "../../src/tool/webfetch" +import { SessionID } from "../../src/session/schema" const projectRoot = path.join(import.meta.dir, "../..") const ctx = { - sessionID: "test", + sessionID: SessionID.make("ses_test"), messageID: "message", callID: "", agent: "build", diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index 695d48ccbbc..62279057a99 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -4,9 +4,10 @@ import fs from "fs/promises" import { WriteTool } from "../../src/tool/write" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" +import { SessionID } from "../../src/session/schema" const ctx = { - sessionID: "test-write-session", + sessionID: SessionID.make("ses_test-write-session"), messageID: "", callID: "", agent: "build",