feat(id): brand WorkspaceID through Drizzle and Zod schemas (#16964)

This commit is contained in:
Kit Langton
2026-03-11 19:30:17 -04:00
committed by GitHub
parent f1c3a44190
commit 16a6d6feba
49 changed files with 205 additions and 157 deletions

View File

@@ -5,6 +5,7 @@ import { Provider } from "../../../provider/provider"
import { Session } from "../../../session"
import type { MessageV2 } from "../../../session/message-v2"
import { Identifier } from "../../../id/id"
import { MessageID } from "../../../session/schema"
import { ToolRegistry } from "../../../tool/registry"
import { Instance } from "../../../project/instance"
import { PermissionNext } from "../../../permission/next"
@@ -113,7 +114,7 @@ function parseToolParams(input?: string) {
async function createToolContext(agent: Agent.Info) {
const session = await Session.create({ title: `Debug tool run (${agent.name})` })
const messageID = Identifier.ascending("message")
const messageID = MessageID.ascending()
const model = agent.model ?? (await Provider.defaultModel())
const now = Date.now()
const message: MessageV2.Assistant = {

View File

@@ -24,6 +24,7 @@ import { bootstrap } from "../bootstrap"
import { Session } from "../../session"
import type { SessionID } from "../../session/schema"
import { Identifier } from "../../id/id"
import { MessageID } from "../../session/schema"
import { Provider } from "../../provider/provider"
import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2"
@@ -935,7 +936,7 @@ export const GithubRunCommand = cmd({
const result = await SessionPrompt.prompt({
sessionID: session.id,
messageID: Identifier.ascending("message"),
messageID: MessageID.ascending(),
variant,
model: {
providerID,
@@ -989,7 +990,7 @@ export const GithubRunCommand = cmd({
console.log("Requesting summary from agent...")
const summary = await SessionPrompt.prompt({
sessionID: session.id,
messageID: Identifier.ascending("message"),
messageID: MessageID.ascending(),
variant,
model: {
providerID,

View File

@@ -1,7 +1,8 @@
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 { SessionID, MessageID } from "../../session/schema"
import { WorkspaceID } from "../../control-plane/schema"
import { cmd } from "./cmd"
import { bootstrap } from "../bootstrap"
import { Database } from "../../storage/db"
@@ -157,7 +158,11 @@ export const ImportCommand = cmd({
...exportData.info,
id: SessionID.make(exportData.info.id),
parentID: exportData.info.parentID ? SessionID.make(exportData.info.parentID) : undefined,
workspaceID: exportData.info.workspaceID ? WorkspaceID.make(exportData.info.workspaceID) : undefined,
projectID: Instance.project.id,
revert: exportData.info.revert
? { ...exportData.info.revert, messageID: MessageID.make(exportData.info.revert.messageID) }
: undefined,
})
Database.use((db) =>
db
@@ -168,28 +173,30 @@ export const ImportCommand = cmd({
)
for (const msg of exportData.messages) {
const { id: _mid, sessionID: _msid, ...msgData } = msg.info
Database.use((db) =>
db
.insert(MessageTable)
.values({
id: msg.info.id,
id: MessageID.make(msg.info.id),
session_id: row.id,
time_created: msg.info.time?.created ?? Date.now(),
data: msg.info,
data: msgData,
})
.onConflictDoNothing()
.run(),
)
for (const part of msg.parts) {
const { id: _pid, sessionID: _psid, messageID: _pmid, ...partData } = part
Database.use((db) =>
db
.insert(PartTable)
.values({
id: part.id,
message_id: msg.info.id,
message_id: MessageID.make(msg.info.id),
session_id: row.id,
data: part,
data: partData,
})
.onConflictDoNothing()
.run(),

View File

@@ -10,6 +10,7 @@ import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync"
import { Identifier } from "@/id/id"
import { MessageID } from "@/session/schema"
import { createStore, produce } from "solid-js/store"
import { useKeybind } from "@tui/context/keybind"
import { usePromptHistory, type PromptInfo } from "./history"
@@ -561,7 +562,7 @@ export function Prompt(props: PromptProps) {
sessionID = res.data.id
}
const messageID = Identifier.ascending("message")
const messageID = MessageID.ascending()
let inputText = store.prompt.input
// Expand pasted text inline before submitting

View File

@@ -1,5 +1,5 @@
import { BusEvent } from "@/bus/bus-event"
import { SessionID } from "@/session/schema"
import { SessionID, MessageID } from "@/session/schema"
import z from "zod"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
@@ -17,7 +17,7 @@ export namespace Command {
name: z.string(),
sessionID: SessionID.zod,
arguments: z.string(),
messageID: Identifier.schema("message"),
messageID: MessageID.zod,
}),
),
}

View File

@@ -0,0 +1,17 @@
import { Schema } from "effect"
import z from "zod"
import { withStatics } from "@/util/schema"
import { Identifier } from "@/id/id"
const workspaceIdSchema = Schema.String.pipe(Schema.brand("WorkspaceId"))
export type WorkspaceID = typeof workspaceIdSchema.Type
export const WorkspaceID = workspaceIdSchema.pipe(
withStatics((schema: typeof workspaceIdSchema) => ({
make: (id: string) => schema.makeUnsafe(id),
ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("workspace", id)),
zod: z.string().startsWith("wrk").pipe(z.custom<WorkspaceID>()),
})),
)

View File

@@ -1,9 +1,9 @@
import z from "zod"
import { Identifier } from "@/id/id"
import { ProjectID } from "@/project/schema"
import { WorkspaceID } from "./schema"
export const WorkspaceInfo = z.object({
id: Identifier.schema("workspace"),
id: WorkspaceID.zod,
type: z.string(),
branch: z.string().nullable(),
name: z.string().nullable(),

View File

@@ -1,13 +1,14 @@
import { Context } from "../util/context"
import type { WorkspaceID } from "./schema"
interface Context {
workspaceID?: string
workspaceID?: WorkspaceID
}
const context = Context.create<Context>("workspace")
export const WorkspaceContext = {
async provide<R>(input: { workspaceID?: string; fn: () => R }): Promise<R> {
async provide<R>(input: { workspaceID?: WorkspaceID; fn: () => R }): Promise<R> {
return context.provide({ workspaceID: input.workspaceID }, async () => {
return input.fn()
})

View File

@@ -4,6 +4,7 @@ import { InstanceBootstrap } from "../../project/bootstrap"
import { SessionRoutes } from "../../server/routes/session"
import { WorkspaceServerRoutes } from "./routes"
import { WorkspaceContext } from "../workspace-context"
import { WorkspaceID } from "../schema"
export namespace WorkspaceServer {
export function App() {
@@ -20,9 +21,9 @@ export namespace WorkspaceServer {
return new Hono()
.use(async (c, next) => {
const workspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
const rawWorkspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
const raw = c.req.query("directory") || c.req.header("x-opencode-directory")
if (workspaceID == null) {
if (rawWorkspaceID == null) {
throw new Error("workspaceID parameter is required")
}
if (raw == null) {
@@ -38,7 +39,7 @@ export namespace WorkspaceServer {
})()
return WorkspaceContext.provide({
workspaceID,
workspaceID: WorkspaceID.make(rawWorkspaceID),
async fn() {
return Instance.provide({
directory,

View File

@@ -1,9 +1,10 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "../project/project.sql"
import type { ProjectID } from "../project/schema"
import type { WorkspaceID } from "./schema"
export const WorkspaceTable = sqliteTable("workspace", {
id: text().primaryKey(),
id: text().$type<WorkspaceID>().primaryKey(),
type: text().notNull(),
branch: text(),
name: text(),

View File

@@ -1,5 +1,4 @@
import z from "zod"
import { Identifier } from "@/id/id"
import { fn } from "@/util/fn"
import { Database, eq } from "@/storage/db"
import { Project } from "@/project/project"
@@ -10,6 +9,7 @@ import { ProjectID } from "@/project/schema"
import { WorkspaceTable } from "./workspace.sql"
import { getAdaptor } from "./adaptors"
import { WorkspaceInfo } from "./types"
import { WorkspaceID } from "./schema"
import { parseSSE } from "./sse"
export namespace Workspace {
@@ -46,7 +46,7 @@ export namespace Workspace {
}
const CreateInput = z.object({
id: Identifier.schema("workspace").optional(),
id: WorkspaceID.zod.optional(),
type: Info.shape.type,
branch: Info.shape.branch,
projectID: ProjectID.zod,
@@ -54,7 +54,7 @@ export namespace Workspace {
})
export const create = fn(CreateInput, async (input) => {
const id = Identifier.ascending("workspace", input.id)
const id = WorkspaceID.ascending(input.id)
const adaptor = await getAdaptor(input.type)
const config = await adaptor.configure({ ...input, id, name: null, directory: null })
@@ -94,13 +94,13 @@ export namespace Workspace {
return rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
}
export const get = fn(Identifier.schema("workspace"), async (id) => {
export const get = fn(WorkspaceID.zod, async (id) => {
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
if (!row) return
return fromRow(row)
})
export const remove = fn(Identifier.schema("workspace"), async (id) => {
export const remove = fn(WorkspaceID.zod, async (id) => {
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
if (row) {
const info = fromRow(row)

View File

@@ -1,6 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { SessionID } from "@/session/schema"
import { SessionID, MessageID } from "@/session/schema"
import z from "zod"
import { Log } from "../util/log"
import { Identifier } from "../id/id"
@@ -26,7 +26,7 @@ export namespace Permission {
type: z.string(),
pattern: z.union([z.string(), z.array(z.string())]).optional(),
sessionID: SessionID.zod,
messageID: z.string(),
messageID: MessageID.zod,
callID: z.string().optional(),
message: z.string(),
metadata: z.record(z.string(), z.any()),

View File

@@ -2,7 +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 { SessionID, MessageID } from "@/session/schema"
import { Instance } from "@/project/instance"
import { Database, eq } from "@/storage/db"
import { PermissionTable } from "@/session/session.sql"
@@ -77,7 +77,7 @@ export namespace PermissionNext {
always: z.string().array(),
tool: z
.object({
messageID: z.string(),
messageID: MessageID.zod,
callID: z.string(),
})
.optional(),

View File

@@ -1,7 +1,7 @@
import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event"
import { Identifier } from "@/id/id"
import { SessionID } from "@/session/schema"
import { SessionID, MessageID } from "@/session/schema"
import { Instance } from "@/project/instance"
import { Log } from "@/util/log"
import z from "zod"
@@ -39,7 +39,7 @@ export namespace Question {
questions: z.array(Info).describe("Questions to ask"),
tool: z
.object({
messageID: z.string(),
messageID: MessageID.zod,
callID: z.string(),
})
.optional(),
@@ -98,7 +98,7 @@ export namespace Question {
export async function ask(input: {
sessionID: SessionID
questions: Info[]
tool?: { messageID: string; callID: string }
tool?: { messageID: MessageID; callID: string }
}): Promise<Answer[]> {
const s = await state()
const id = Identifier.ascending("question")

View File

@@ -1,7 +1,7 @@
import { Hono } from "hono"
import { stream } from "hono/streaming"
import { describeRoute, validator, resolver } from "hono-openapi"
import { SessionID } from "@/session/schema"
import { SessionID, MessageID } from "@/session/schema"
import z from "zod"
import { Session } from "../../session"
import { MessageV2 } from "../../session/message-v2"
@@ -607,7 +607,7 @@ export const SessionRoutes = lazy(() =>
"param",
z.object({
sessionID: SessionID.zod,
messageID: z.string().meta({ description: "Message ID" }),
messageID: MessageID.zod,
}),
),
async (c) => {
@@ -642,7 +642,7 @@ export const SessionRoutes = lazy(() =>
"param",
z.object({
sessionID: SessionID.zod,
messageID: z.string().meta({ description: "Message ID" }),
messageID: MessageID.zod,
}),
),
async (c) => {
@@ -676,8 +676,8 @@ export const SessionRoutes = lazy(() =>
"param",
z.object({
sessionID: SessionID.zod,
messageID: z.string().meta({ description: "Message ID" }),
partID: z.string().meta({ description: "Part ID" }),
messageID: MessageID.zod,
partID: z.string(),
}),
),
async (c) => {
@@ -711,8 +711,8 @@ export const SessionRoutes = lazy(() =>
"param",
z.object({
sessionID: SessionID.zod,
messageID: z.string().meta({ description: "Message ID" }),
partID: z.string().meta({ description: "Part ID" }),
messageID: MessageID.zod,
partID: z.string(),
}),
),
validator("json", MessageV2.Part),

View File

@@ -22,6 +22,7 @@ import { Flag } from "../flag/flag"
import { Command } from "../command"
import { Global } from "../global"
import { WorkspaceContext } from "../control-plane/workspace-context"
import { WorkspaceID } from "../control-plane/schema"
import { WorkspaceRouterMiddleware } from "../control-plane/workspace-router-middleware"
import { ProjectRoutes } from "./routes/project"
import { SessionRoutes } from "./routes/session"
@@ -190,7 +191,7 @@ export namespace Server {
)
.use(async (c, next) => {
if (c.req.path === "/log") return next()
const workspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
const rawWorkspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
const directory = Filesystem.resolve(
(() => {
@@ -203,7 +204,7 @@ export namespace Server {
)
return WorkspaceContext.provide({
workspaceID,
workspaceID: rawWorkspaceID ? WorkspaceID.make(rawWorkspaceID) : undefined,
async fn() {
return Instance.provide({
directory,

View File

@@ -2,7 +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 { SessionID, MessageID } from "./schema"
import { Instance } from "../project/instance"
import { Provider } from "../provider/provider"
import { MessageV2 } from "./message-v2"
@@ -100,7 +100,7 @@ export namespace SessionCompaction {
}
export async function process(input: {
parentID: string
parentID: MessageID
messages: MessageV2.WithParts[]
sessionID: SessionID
abort: AbortSignal
@@ -134,7 +134,7 @@ export namespace SessionCompaction {
? await Provider.getModel(agent.model.providerID, agent.model.modelID)
: await Provider.getModel(userMessage.model.providerID, userMessage.model.modelID)
const msg = (await Session.updateMessage({
id: Identifier.ascending("message"),
id: MessageID.ascending(),
role: "assistant",
parentID: input.parentID,
sessionID: input.sessionID,
@@ -237,7 +237,7 @@ When constructing the summary, try to stick to this template:
if (replay) {
const original = replay.info as MessageV2.User
const replayMsg = await Session.updateMessage({
id: Identifier.ascending("message"),
id: MessageID.ascending(),
role: "user",
sessionID: input.sessionID,
time: { created: Date.now() },
@@ -263,7 +263,7 @@ When constructing the summary, try to stick to this template:
}
} else {
const continueMsg = await Session.updateMessage({
id: Identifier.ascending("message"),
id: MessageID.ascending(),
role: "user",
sessionID: input.sessionID,
time: { created: Date.now() },
@@ -307,7 +307,7 @@ When constructing the summary, try to stick to this template:
}),
async (input) => {
const msg = await Session.updateMessage({
id: Identifier.ascending("message"),
id: MessageID.ascending(),
role: "user",
model: input.model,
sessionID: input.sessionID,

View File

@@ -24,7 +24,8 @@ 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 { WorkspaceID } from "../control-plane/schema"
import { SessionID, MessageID } from "./schema"
import type { Provider } from "@/provider/provider"
import { PermissionNext } from "@/permission/next"
@@ -123,7 +124,7 @@ export namespace Session {
id: SessionID.zod,
slug: z.string(),
projectID: ProjectID.zod,
workspaceID: z.string().optional(),
workspaceID: WorkspaceID.zod.optional(),
directory: z.string(),
parentID: SessionID.zod.optional(),
summary: z
@@ -150,7 +151,7 @@ export namespace Session {
permission: PermissionNext.Ruleset.optional(),
revert: z
.object({
messageID: z.string(),
messageID: MessageID.zod,
partID: z.string().optional(),
snapshot: z.string().optional(),
diff: z.string().optional(),
@@ -221,7 +222,7 @@ export namespace Session {
parentID: SessionID.zod.optional(),
title: z.string().optional(),
permission: Info.shape.permission,
workspaceID: Identifier.schema("workspace").optional(),
workspaceID: WorkspaceID.zod.optional(),
})
.optional(),
async (input) => {
@@ -238,7 +239,7 @@ export namespace Session {
export const fork = fn(
z.object({
sessionID: SessionID.zod,
messageID: Identifier.schema("message").optional(),
messageID: MessageID.zod.optional(),
}),
async (input) => {
const original = await get(input.sessionID)
@@ -250,11 +251,11 @@ export namespace Session {
title,
})
const msgs = await messages({ sessionID: input.sessionID })
const idMap = new Map<string, string>()
const idMap = new Map<string, MessageID>()
for (const msg of msgs) {
if (input.messageID && msg.info.id >= input.messageID) break
const newID = Identifier.ascending("message")
const newID = MessageID.ascending()
idMap.set(msg.info.id, newID)
const parentID = msg.info.role === "assistant" && msg.info.parentID ? idMap.get(msg.info.parentID) : undefined
@@ -297,7 +298,7 @@ export namespace Session {
id?: SessionID
title?: string
parentID?: SessionID
workspaceID?: string
workspaceID?: WorkspaceID
directory: string
permission?: PermissionNext.Ruleset
}) {
@@ -538,7 +539,7 @@ export namespace Session {
export function* list(input?: {
directory?: string
workspaceID?: string
workspaceID?: WorkspaceID
roots?: boolean
start?: number
search?: string
@@ -707,7 +708,7 @@ export namespace Session {
export const removeMessage = fn(
z.object({
sessionID: SessionID.zod,
messageID: Identifier.schema("message"),
messageID: MessageID.zod,
}),
async (input) => {
// CASCADE delete handles parts automatically
@@ -729,7 +730,7 @@ export namespace Session {
export const removePart = fn(
z.object({
sessionID: SessionID.zod,
messageID: Identifier.schema("message"),
messageID: MessageID.zod,
partID: Identifier.schema("part"),
}),
async (input) => {
@@ -777,7 +778,7 @@ export namespace Session {
export const updatePartDelta = fn(
z.object({
sessionID: SessionID.zod,
messageID: z.string(),
messageID: MessageID.zod,
partID: z.string(),
field: z.string(),
delta: z.string(),
@@ -877,7 +878,7 @@ export namespace Session {
sessionID: SessionID.zod,
modelID: z.string(),
providerID: z.string(),
messageID: Identifier.schema("message"),
messageID: MessageID.zod,
}),
async (input) => {
await SessionPrompt.command({

View File

@@ -1,9 +1,8 @@
import { BusEvent } from "@/bus/bus-event"
import { SessionID } from "./schema"
import { SessionID, MessageID } from "./schema"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
import { Identifier } from "../id/id"
import { LSP } from "../lsp"
import { Snapshot } from "@/snapshot"
import { fn } from "@/util/fn"
@@ -81,7 +80,7 @@ export namespace MessageV2 {
const PartBase = z.object({
id: z.string(),
sessionID: SessionID.zod,
messageID: z.string(),
messageID: MessageID.zod,
})
export const SnapshotPart = PartBase.extend({
@@ -344,7 +343,7 @@ export namespace MessageV2 {
export type ToolPart = z.infer<typeof ToolPart>
const Base = z.object({
id: z.string(),
id: MessageID.zod,
sessionID: SessionID.zod,
})
@@ -411,7 +410,7 @@ export namespace MessageV2 {
APIError.Schema,
])
.optional(),
parentID: z.string(),
parentID: MessageID.zod,
modelID: z.string(),
providerID: z.string(),
/**
@@ -459,7 +458,7 @@ export namespace MessageV2 {
"message.removed",
z.object({
sessionID: SessionID.zod,
messageID: z.string(),
messageID: MessageID.zod,
}),
),
PartUpdated: BusEvent.define(
@@ -472,7 +471,7 @@ export namespace MessageV2 {
"message.part.delta",
z.object({
sessionID: SessionID.zod,
messageID: z.string(),
messageID: MessageID.zod,
partID: z.string(),
field: z.string(),
delta: z.string(),
@@ -482,7 +481,7 @@ export namespace MessageV2 {
"message.part.removed",
z.object({
sessionID: SessionID.zod,
messageID: z.string(),
messageID: MessageID.zod,
partID: z.string(),
}),
),
@@ -699,7 +698,7 @@ export namespace MessageV2 {
// media (images, PDFs) in tool results
if (media.length > 0) {
result.push({
id: Identifier.ascending("message"),
id: MessageID.ascending(),
role: "user",
parts: [
{
@@ -782,7 +781,7 @@ export namespace MessageV2 {
}
})
export const parts = fn(Identifier.schema("message"), async (message_id) => {
export const parts = fn(MessageID.zod, async (message_id) => {
const rows = Database.use((db) =>
db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(),
)
@@ -794,7 +793,7 @@ export namespace MessageV2 {
export const get = fn(
z.object({
sessionID: SessionID.zod,
messageID: Identifier.schema("message"),
messageID: MessageID.zod,
}),
async (input): Promise<WithParts> => {
const row = Database.use((db) => db.select().from(MessageTable).where(eq(MessageTable.id, input.messageID)).get())

View File

@@ -15,7 +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"
import type { SessionID, MessageID } from "./schema"
export namespace SessionProcessor {
const DOOM_LOOP_THRESHOLD = 3

View File

@@ -4,7 +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 { SessionID, MessageID } from "./schema"
import { MessageV2 } from "./message-v2"
import { Log } from "../util/log"
import { SessionRevert } from "./revert"
@@ -92,7 +92,7 @@ export namespace SessionPrompt {
export const PromptInput = z.object({
sessionID: SessionID.zod,
messageID: Identifier.schema("message").optional(),
messageID: MessageID.zod.optional(),
model: z
.object({
providerID: z.string(),
@@ -355,7 +355,7 @@ export namespace SessionPrompt {
const taskTool = await TaskTool.init()
const taskModel = task.model ? await Provider.getModel(task.model.providerID, task.model.modelID) : model
const assistantMessage = (await Session.updateMessage({
id: Identifier.ascending("message"),
id: MessageID.ascending(),
role: "assistant",
parentID: lastUser.id,
sessionID,
@@ -504,7 +504,7 @@ export namespace SessionPrompt {
// If we create assistant messages w/ out user ones following mid loop thinking signatures
// will be missing and it can cause errors for models like gemini for example
const summaryUserMsg: MessageV2.User = {
id: Identifier.ascending("message"),
id: MessageID.ascending(),
sessionID,
role: "user",
time: {
@@ -568,7 +568,7 @@ export namespace SessionPrompt {
const processor = SessionProcessor.create({
assistantMessage: (await Session.updateMessage({
id: Identifier.ascending("message"),
id: MessageID.ascending(),
parentID: lastUser.id,
role: "assistant",
mode: agent.name,
@@ -971,7 +971,7 @@ export namespace SessionPrompt {
const variant = input.variant ?? (agent.variant && full?.variants?.[agent.variant] ? agent.variant : undefined)
const info: MessageV2.Info = {
id: input.messageID ?? Identifier.ascending("message"),
id: input.messageID ?? MessageID.ascending(),
role: "user",
sessionID: input.sessionID,
time: {
@@ -1505,7 +1505,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const agent = await Agent.get(input.agent)
const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))
const userMsg: MessageV2.User = {
id: Identifier.ascending("message"),
id: MessageID.ascending(),
sessionID: input.sessionID,
time: {
created: Date.now(),
@@ -1529,7 +1529,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
await Session.updatePart(userPart)
const msg: MessageV2.Assistant = {
id: Identifier.ascending("message"),
id: MessageID.ascending(),
sessionID: input.sessionID,
parentID: userMsg.id,
mode: input.agent,
@@ -1719,7 +1719,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(),
messageID: MessageID.zod.optional(),
sessionID: SessionID.zod,
agent: z.string().optional(),
model: z.string().optional(),

View File

@@ -1,6 +1,6 @@
import z from "zod"
import { Identifier } from "../id/id"
import { SessionID } from "./schema"
import { SessionID, MessageID } from "./schema"
import { Snapshot } from "../snapshot"
import { MessageV2 } from "./message-v2"
import { Session } from "."
@@ -17,7 +17,7 @@ export namespace SessionRevert {
export const RevertInput = z.object({
sessionID: SessionID.zod,
messageID: Identifier.schema("message"),
messageID: MessageID.zod,
partID: Identifier.schema("part").optional(),
})
export type RevertInput = z.infer<typeof RevertInput>

View File

@@ -15,3 +15,15 @@ export const SessionID = sessionIdSchema.pipe(
zod: z.string().startsWith("ses").pipe(z.custom<SessionID>()),
})),
)
const messageIdSchema = Schema.String.pipe(Schema.brand("MessageId"))
export type MessageID = typeof messageIdSchema.Type
export const MessageID = messageIdSchema.pipe(
withStatics((schema: typeof messageIdSchema) => ({
make: (id: string) => schema.makeUnsafe(id),
ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("message", id)),
zod: z.string().startsWith("msg").pipe(z.custom<MessageID>()),
})),
)

View File

@@ -4,7 +4,8 @@ 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 type { SessionID, MessageID } from "./schema"
import type { WorkspaceID } from "../control-plane/schema"
import { Timestamps } from "../storage/schema.sql"
type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
@@ -18,7 +19,7 @@ export const SessionTable = sqliteTable(
.$type<ProjectID>()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
workspace_id: text(),
workspace_id: text().$type<WorkspaceID>(),
parent_id: text().$type<SessionID>(),
slug: text().notNull(),
directory: text().notNull(),
@@ -29,7 +30,7 @@ export const SessionTable = sqliteTable(
summary_deletions: integer(),
summary_files: integer(),
summary_diffs: text({ mode: "json" }).$type<Snapshot.FileDiff[]>(),
revert: text({ mode: "json" }).$type<{ messageID: string; partID?: string; snapshot?: string; diff?: string }>(),
revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: string; snapshot?: string; diff?: string }>(),
permission: text({ mode: "json" }).$type<PermissionNext.Ruleset>(),
...Timestamps,
time_compacting: integer(),
@@ -45,7 +46,7 @@ export const SessionTable = sqliteTable(
export const MessageTable = sqliteTable(
"message",
{
id: text().primaryKey(),
id: text().$type<MessageID>().primaryKey(),
session_id: text()
.$type<SessionID>()
.notNull()
@@ -61,6 +62,7 @@ export const PartTable = sqliteTable(
{
id: text().primaryKey(),
message_id: text()
.$type<MessageID>()
.notNull()
.references(() => MessageTable.id, { onDelete: "cascade" }),
session_id: text().$type<SessionID>().notNull(),

View File

@@ -4,7 +4,7 @@ import { Session } from "."
import { MessageV2 } from "./message-v2"
import { Identifier } from "@/id/id"
import { SessionID } from "./schema"
import { SessionID, MessageID } from "./schema"
import { Snapshot } from "@/snapshot"
import { Storage } from "@/storage/storage"
@@ -70,7 +70,7 @@ export namespace SessionSummary {
export const summarize = fn(
z.object({
sessionID: SessionID.zod,
messageID: z.string(),
messageID: MessageID.zod,
}),
async (input) => {
const all = await Session.messages({ sessionID: input.sessionID })
@@ -115,7 +115,7 @@ export namespace SessionSummary {
export const diff = fn(
z.object({
sessionID: SessionID.zod,
messageID: Identifier.schema("message").optional(),
messageID: MessageID.zod.optional(),
}),
async (input) => {
const diffs = await Storage.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID]).catch(() => [])

View File

@@ -7,7 +7,7 @@ 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 { type SessionID, MessageID } from "../session/schema"
import EXIT_DESCRIPTION from "./plan-exit.txt"
async function getLastModel(sessionID: SessionID) {
@@ -45,7 +45,7 @@ export const PlanExitTool = Tool.define("plan_exit", {
const model = await getLastModel(ctx.sessionID)
const userMsg: MessageV2.User = {
id: Identifier.ascending("message"),
id: MessageID.ascending(),
sessionID: ctx.sessionID,
role: "user",
time: {
@@ -103,7 +103,7 @@ export const PlanEnterTool = Tool.define("plan_enter", {
const model = await getLastModel(ctx.sessionID)
const userMsg: MessageV2.User = {
id: Identifier.ascending("message"),
id: MessageID.ascending(),
sessionID: ctx.sessionID,
role: "user",
time: {

View File

@@ -2,7 +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 { SessionID, MessageID } from "../session/schema"
import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id"
import { Agent } from "../agent/agent"
@@ -117,7 +117,7 @@ export const TaskTool = Tool.define("task", async (ctx) => {
},
})
const messageID = Identifier.ascending("message")
const messageID = MessageID.ascending()
function cancel() {
SessionPrompt.cancel(session.id)

View File

@@ -2,7 +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 type { SessionID, MessageID } from "../session/schema"
import { Truncate } from "./truncation"
export namespace Tool {
@@ -16,7 +16,7 @@ export namespace Tool {
export type Context<M extends Metadata = Metadata> = {
sessionID: SessionID
messageID: string
messageID: MessageID
agent: string
abort: AbortSignal
callID?: string