feat(id): brand PartID through Drizzle and Zod schemas (#16966)

This commit is contained in:
Kit Langton
2026-03-11 19:40:50 -04:00
committed by GitHub
parent d26c6f80e1
commit 090f636354
21 changed files with 102 additions and 97 deletions

View File

@@ -11,8 +11,7 @@ const seed = async () => {
const { Instance } = await import("../src/project/instance") const { Instance } = await import("../src/project/instance")
const { InstanceBootstrap } = await import("../src/project/bootstrap") const { InstanceBootstrap } = await import("../src/project/bootstrap")
const { Session } = await import("../src/session") const { Session } = await import("../src/session")
const { Identifier } = await import("../src/id/id") const { MessageID, PartID } = await import("../src/session/schema")
const { MessageID } = await import("../src/session/schema")
const { Project } = await import("../src/project/project") const { Project } = await import("../src/project/project")
await Instance.provide({ await Instance.provide({
@@ -21,7 +20,7 @@ const seed = async () => {
fn: async () => { fn: async () => {
const session = await Session.create({ title }) const session = await Session.create({ title })
const messageID = MessageID.ascending() const messageID = MessageID.ascending()
const partID = Identifier.descending("part") const partID = PartID.ascending()
const message = { const message = {
id: messageID, id: messageID,
sessionID: session.id, sessionID: session.id,

View File

@@ -4,8 +4,7 @@ import { Agent } from "../../../agent/agent"
import { Provider } from "../../../provider/provider" import { Provider } from "../../../provider/provider"
import { Session } from "../../../session" import { Session } from "../../../session"
import type { MessageV2 } from "../../../session/message-v2" import type { MessageV2 } from "../../../session/message-v2"
import { Identifier } from "../../../id/id" import { MessageID, PartID } from "../../../session/schema"
import { MessageID } from "../../../session/schema"
import { ToolRegistry } from "../../../tool/registry" import { ToolRegistry } from "../../../tool/registry"
import { Instance } from "../../../project/instance" import { Instance } from "../../../project/instance"
import { PermissionNext } from "../../../permission/next" import { PermissionNext } from "../../../permission/next"
@@ -151,7 +150,7 @@ async function createToolContext(agent: Agent.Info) {
return { return {
sessionID: session.id, sessionID: session.id,
messageID, messageID,
callID: Identifier.ascending("part"), callID: PartID.ascending(),
agent: agent.name, agent: agent.name,
abort: new AbortController().signal, abort: new AbortController().signal,
messages: [], messages: [],

View File

@@ -23,8 +23,7 @@ import { Instance } from "@/project/instance"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { Session } from "../../session" import { Session } from "../../session"
import type { SessionID } from "../../session/schema" import type { SessionID } from "../../session/schema"
import { Identifier } from "../../id/id" import { MessageID, PartID } from "../../session/schema"
import { MessageID } from "../../session/schema"
import { Provider } from "../../provider/provider" import { Provider } from "../../provider/provider"
import { Bus } from "../../bus" import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
@@ -945,13 +944,13 @@ export const GithubRunCommand = cmd({
// agent is omitted - server will use default_agent from config or fall back to "build" // agent is omitted - server will use default_agent from config or fall back to "build"
parts: [ parts: [
{ {
id: Identifier.ascending("part"), id: PartID.ascending(),
type: "text", type: "text",
text: message, text: message,
}, },
...files.flatMap((f) => [ ...files.flatMap((f) => [
{ {
id: Identifier.ascending("part"), id: PartID.ascending(),
type: "file" as const, type: "file" as const,
mime: f.mime, mime: f.mime,
url: `data:${f.mime};base64,${f.content}`, url: `data:${f.mime};base64,${f.content}`,
@@ -999,7 +998,7 @@ export const GithubRunCommand = cmd({
tools: { "*": false }, // Disable all tools to force text response tools: { "*": false }, // Disable all tools to force text response
parts: [ parts: [
{ {
id: Identifier.ascending("part"), id: PartID.ascending(),
type: "text", type: "text",
text: "Summarize the actions (tool calls & reasoning) you did for the user in 1-2 sentences.", text: "Summarize the actions (tool calls & reasoning) you did for the user in 1-2 sentences.",
}, },

View File

@@ -1,7 +1,7 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2" import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
import { Session } from "../../session" import { Session } from "../../session"
import { SessionID, MessageID } from "../../session/schema" import { SessionID, MessageID, PartID } from "../../session/schema"
import { WorkspaceID } from "../../control-plane/schema" import { WorkspaceID } from "../../control-plane/schema"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
@@ -161,7 +161,11 @@ export const ImportCommand = cmd({
workspaceID: exportData.info.workspaceID ? WorkspaceID.make(exportData.info.workspaceID) : undefined, workspaceID: exportData.info.workspaceID ? WorkspaceID.make(exportData.info.workspaceID) : undefined,
projectID: Instance.project.id, projectID: Instance.project.id,
revert: exportData.info.revert revert: exportData.info.revert
? { ...exportData.info.revert, messageID: MessageID.make(exportData.info.revert.messageID) } ? {
...exportData.info.revert,
messageID: MessageID.make(exportData.info.revert.messageID),
partID: exportData.info.revert.partID ? PartID.make(exportData.info.revert.partID) : undefined,
}
: undefined, : undefined,
}) })
Database.use((db) => Database.use((db) =>
@@ -193,7 +197,7 @@ export const ImportCommand = cmd({
db db
.insert(PartTable) .insert(PartTable)
.values({ .values({
id: part.id, id: PartID.make(part.id),
message_id: MessageID.make(msg.info.id), message_id: MessageID.make(msg.info.id),
session_id: row.id, session_id: row.id,
data: partData, data: partData,

View File

@@ -9,8 +9,7 @@ import { EmptyBorder } from "@tui/component/border"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
import { useRoute } from "@tui/context/route" import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { Identifier } from "@/id/id" import { MessageID, PartID } from "@/session/schema"
import { MessageID } from "@/session/schema"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { useKeybind } from "@tui/context/keybind" import { useKeybind } from "@tui/context/keybind"
import { usePromptHistory, type PromptInfo } from "./history" import { usePromptHistory, type PromptInfo } from "./history"
@@ -625,7 +624,7 @@ export function Prompt(props: PromptProps) {
parts: nonTextParts parts: nonTextParts
.filter((x) => x.type === "file") .filter((x) => x.type === "file")
.map((x) => ({ .map((x) => ({
id: Identifier.ascending("part"), id: PartID.ascending(),
...x, ...x,
})), })),
}) })
@@ -640,12 +639,12 @@ export function Prompt(props: PromptProps) {
variant, variant,
parts: [ parts: [
{ {
id: Identifier.ascending("part"), id: PartID.ascending(),
type: "text", type: "text",
text: inputText, text: inputText,
}, },
...nonTextParts.map((x) => ({ ...nonTextParts.map((x) => ({
id: Identifier.ascending("part"), id: PartID.ascending(),
...x, ...x,
})), })),
], ],

View File

@@ -1,7 +1,7 @@
import { Hono } from "hono" import { Hono } from "hono"
import { stream } from "hono/streaming" import { stream } from "hono/streaming"
import { describeRoute, validator, resolver } from "hono-openapi" import { describeRoute, validator, resolver } from "hono-openapi"
import { SessionID, MessageID } from "@/session/schema" import { SessionID, MessageID, PartID } from "@/session/schema"
import z from "zod" import z from "zod"
import { Session } from "../../session" import { Session } from "../../session"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
@@ -677,7 +677,7 @@ export const SessionRoutes = lazy(() =>
z.object({ z.object({
sessionID: SessionID.zod, sessionID: SessionID.zod,
messageID: MessageID.zod, messageID: MessageID.zod,
partID: z.string(), partID: PartID.zod,
}), }),
), ),
async (c) => { async (c) => {
@@ -712,7 +712,7 @@ export const SessionRoutes = lazy(() =>
z.object({ z.object({
sessionID: SessionID.zod, sessionID: SessionID.zod,
messageID: MessageID.zod, messageID: MessageID.zod,
partID: z.string(), partID: PartID.zod,
}), }),
), ),
validator("json", MessageV2.Part), validator("json", MessageV2.Part),

View File

@@ -1,8 +1,7 @@
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { Session } from "." import { Session } from "."
import { Identifier } from "../id/id" import { SessionID, MessageID, PartID } from "./schema"
import { SessionID, MessageID } from "./schema"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { Provider } from "../provider/provider" import { Provider } from "../provider/provider"
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
@@ -256,7 +255,7 @@ When constructing the summary, try to stick to this template:
: part : part
await Session.updatePart({ await Session.updatePart({
...replayPart, ...replayPart,
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: replayMsg.id, messageID: replayMsg.id,
sessionID: input.sessionID, sessionID: input.sessionID,
}) })
@@ -276,7 +275,7 @@ When constructing the summary, try to stick to this template:
: "") + : "") +
"Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed." "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed."
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: continueMsg.id, messageID: continueMsg.id,
sessionID: input.sessionID, sessionID: input.sessionID,
type: "text", type: "text",
@@ -317,7 +316,7 @@ When constructing the summary, try to stick to this template:
}, },
}) })
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: msg.id, messageID: msg.id,
sessionID: msg.sessionID, sessionID: msg.sessionID,
type: "compaction", type: "compaction",

View File

@@ -7,7 +7,6 @@ import z from "zod"
import { type ProviderMetadata } from "ai" import { type ProviderMetadata } from "ai"
import { Config } from "../config/config" import { Config } from "../config/config"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { Identifier } from "../id/id"
import { Installation } from "../installation" import { Installation } from "../installation"
import { Database, NotFoundError, eq, and, or, gte, isNull, desc, like, inArray, lt } from "../storage/db" import { Database, NotFoundError, eq, and, or, gte, isNull, desc, like, inArray, lt } from "../storage/db"
@@ -25,7 +24,7 @@ import { Snapshot } from "@/snapshot"
import { WorkspaceContext } from "../control-plane/workspace-context" import { WorkspaceContext } from "../control-plane/workspace-context"
import { ProjectID } from "../project/schema" import { ProjectID } from "../project/schema"
import { WorkspaceID } from "../control-plane/schema" import { WorkspaceID } from "../control-plane/schema"
import { SessionID, MessageID } from "./schema" import { SessionID, MessageID, PartID } from "./schema"
import type { Provider } from "@/provider/provider" import type { Provider } from "@/provider/provider"
import { PermissionNext } from "@/permission/next" import { PermissionNext } from "@/permission/next"
@@ -152,7 +151,7 @@ export namespace Session {
revert: z revert: z
.object({ .object({
messageID: MessageID.zod, messageID: MessageID.zod,
partID: z.string().optional(), partID: PartID.zod.optional(),
snapshot: z.string().optional(), snapshot: z.string().optional(),
diff: z.string().optional(), diff: z.string().optional(),
}) })
@@ -269,7 +268,7 @@ export namespace Session {
for (const part of msg.parts) { for (const part of msg.parts) {
await updatePart({ await updatePart({
...part, ...part,
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: cloned.id, messageID: cloned.id,
sessionID: session.id, sessionID: session.id,
}) })
@@ -731,7 +730,7 @@ export namespace Session {
z.object({ z.object({
sessionID: SessionID.zod, sessionID: SessionID.zod,
messageID: MessageID.zod, messageID: MessageID.zod,
partID: Identifier.schema("part"), partID: PartID.zod,
}), }),
async (input) => { async (input) => {
Database.use((db) => { Database.use((db) => {
@@ -779,7 +778,7 @@ export namespace Session {
z.object({ z.object({
sessionID: SessionID.zod, sessionID: SessionID.zod,
messageID: MessageID.zod, messageID: MessageID.zod,
partID: z.string(), partID: PartID.zod,
field: z.string(), field: z.string(),
delta: z.string(), delta: z.string(),
}), }),

View File

@@ -1,5 +1,5 @@
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { SessionID, MessageID } from "./schema" import { SessionID, MessageID, PartID } from "./schema"
import z from "zod" import z from "zod"
import { NamedError } from "@opencode-ai/util/error" import { NamedError } from "@opencode-ai/util/error"
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai" import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
@@ -78,7 +78,7 @@ export namespace MessageV2 {
export type OutputFormat = z.infer<typeof Format> export type OutputFormat = z.infer<typeof Format>
const PartBase = z.object({ const PartBase = z.object({
id: z.string(), id: PartID.zod,
sessionID: SessionID.zod, sessionID: SessionID.zod,
messageID: MessageID.zod, messageID: MessageID.zod,
}) })
@@ -472,7 +472,7 @@ export namespace MessageV2 {
z.object({ z.object({
sessionID: SessionID.zod, sessionID: SessionID.zod,
messageID: MessageID.zod, messageID: MessageID.zod,
partID: z.string(), partID: PartID.zod,
field: z.string(), field: z.string(),
delta: z.string(), delta: z.string(),
}), }),
@@ -482,7 +482,7 @@ export namespace MessageV2 {
z.object({ z.object({
sessionID: SessionID.zod, sessionID: SessionID.zod,
messageID: MessageID.zod, messageID: MessageID.zod,
partID: z.string(), partID: PartID.zod,
}), }),
), ),
} }

View File

@@ -1,6 +1,5 @@
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { Identifier } from "@/id/id"
import { Session } from "." import { Session } from "."
import { Agent } from "@/agent/agent" import { Agent } from "@/agent/agent"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
@@ -15,6 +14,7 @@ import { Config } from "@/config/config"
import { SessionCompaction } from "./compaction" import { SessionCompaction } from "./compaction"
import { PermissionNext } from "@/permission/next" import { PermissionNext } from "@/permission/next"
import { Question } from "@/question" import { Question } from "@/question"
import { PartID } from "./schema"
import type { SessionID, MessageID } from "./schema" import type { SessionID, MessageID } from "./schema"
export namespace SessionProcessor { export namespace SessionProcessor {
@@ -65,7 +65,7 @@ export namespace SessionProcessor {
continue continue
} }
const reasoningPart = { const reasoningPart = {
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: input.assistantMessage.id, messageID: input.assistantMessage.id,
sessionID: input.assistantMessage.sessionID, sessionID: input.assistantMessage.sessionID,
type: "reasoning" as const, type: "reasoning" as const,
@@ -111,7 +111,7 @@ export namespace SessionProcessor {
case "tool-input-start": case "tool-input-start":
const part = await Session.updatePart({ const part = await Session.updatePart({
id: toolcalls[value.id]?.id ?? Identifier.ascending("part"), id: toolcalls[value.id]?.id ?? PartID.ascending(),
messageID: input.assistantMessage.id, messageID: input.assistantMessage.id,
sessionID: input.assistantMessage.sessionID, sessionID: input.assistantMessage.sessionID,
type: "tool", type: "tool",
@@ -234,7 +234,7 @@ export namespace SessionProcessor {
case "start-step": case "start-step":
snapshot = await Snapshot.track() snapshot = await Snapshot.track()
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: input.assistantMessage.id, messageID: input.assistantMessage.id,
sessionID: input.sessionID, sessionID: input.sessionID,
snapshot, snapshot,
@@ -252,7 +252,7 @@ export namespace SessionProcessor {
input.assistantMessage.cost += usage.cost input.assistantMessage.cost += usage.cost
input.assistantMessage.tokens = usage.tokens input.assistantMessage.tokens = usage.tokens
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
reason: value.finishReason, reason: value.finishReason,
snapshot: await Snapshot.track(), snapshot: await Snapshot.track(),
messageID: input.assistantMessage.id, messageID: input.assistantMessage.id,
@@ -266,7 +266,7 @@ export namespace SessionProcessor {
const patch = await Snapshot.patch(snapshot) const patch = await Snapshot.patch(snapshot)
if (patch.files.length) { if (patch.files.length) {
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: input.assistantMessage.id, messageID: input.assistantMessage.id,
sessionID: input.sessionID, sessionID: input.sessionID,
type: "patch", type: "patch",
@@ -290,7 +290,7 @@ export namespace SessionProcessor {
case "text-start": case "text-start":
currentText = { currentText = {
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: input.assistantMessage.id, messageID: input.assistantMessage.id,
sessionID: input.assistantMessage.sessionID, sessionID: input.assistantMessage.sessionID,
type: "text", type: "text",
@@ -389,7 +389,7 @@ export namespace SessionProcessor {
const patch = await Snapshot.patch(snapshot) const patch = await Snapshot.patch(snapshot)
if (patch.files.length) { if (patch.files.length) {
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: input.assistantMessage.id, messageID: input.assistantMessage.id,
sessionID: input.sessionID, sessionID: input.sessionID,
type: "patch", type: "patch",

View File

@@ -3,8 +3,7 @@ import os from "os"
import fs from "fs/promises" import fs from "fs/promises"
import z from "zod" import z from "zod"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Identifier } from "../id/id" import { SessionID, MessageID, PartID } from "./schema"
import { SessionID, MessageID } from "./schema"
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
import { Log } from "../util/log" import { Log } from "../util/log"
import { SessionRevert } from "./revert" import { SessionRevert } from "./revert"
@@ -380,7 +379,7 @@ export namespace SessionPrompt {
}, },
})) as MessageV2.Assistant })) as MessageV2.Assistant
let part = (await Session.updatePart({ let part = (await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: assistantMessage.id, messageID: assistantMessage.id,
sessionID: assistantMessage.sessionID, sessionID: assistantMessage.sessionID,
type: "tool", type: "tool",
@@ -449,7 +448,7 @@ export namespace SessionPrompt {
}) })
const attachments = result?.attachments?.map((attachment) => ({ const attachments = result?.attachments?.map((attachment) => ({
...attachment, ...attachment,
id: Identifier.ascending("part"), id: PartID.ascending(),
sessionID, sessionID,
messageID: assistantMessage.id, messageID: assistantMessage.id,
})) }))
@@ -515,7 +514,7 @@ export namespace SessionPrompt {
} }
await Session.updateMessage(summaryUserMsg) await Session.updateMessage(summaryUserMsg)
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: summaryUserMsg.id, messageID: summaryUserMsg.id,
sessionID, sessionID,
type: "text", type: "text",
@@ -814,7 +813,7 @@ export namespace SessionPrompt {
...result, ...result,
attachments: result.attachments?.map((attachment) => ({ attachments: result.attachments?.map((attachment) => ({
...attachment, ...attachment,
id: Identifier.ascending("part"), id: PartID.ascending(),
sessionID: ctx.sessionID, sessionID: ctx.sessionID,
messageID: input.processor.message.id, messageID: input.processor.message.id,
})), })),
@@ -917,7 +916,7 @@ export namespace SessionPrompt {
output: truncated.content, output: truncated.content,
attachments: attachments.map((attachment) => ({ attachments: attachments.map((attachment) => ({
...attachment, ...attachment,
id: Identifier.ascending("part"), id: PartID.ascending(),
sessionID: ctx.sessionID, sessionID: ctx.sessionID,
messageID: input.processor.message.id, messageID: input.processor.message.id,
})), })),
@@ -989,7 +988,7 @@ export namespace SessionPrompt {
type Draft<T> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never type Draft<T> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never
const assign = (part: Draft<MessageV2.Part>): MessageV2.Part => ({ const assign = (part: Draft<MessageV2.Part>): MessageV2.Part => ({
...part, ...part,
id: part.id ?? Identifier.ascending("part"), id: part.id ? PartID.make(part.id) : PartID.ascending(),
}) })
const parts = await Promise.all( const parts = await Promise.all(
@@ -1335,7 +1334,7 @@ export namespace SessionPrompt {
if (!Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE) { if (!Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE) {
if (input.agent.name === "plan") { if (input.agent.name === "plan") {
userMessage.parts.push({ userMessage.parts.push({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: userMessage.info.id, messageID: userMessage.info.id,
sessionID: userMessage.info.sessionID, sessionID: userMessage.info.sessionID,
type: "text", type: "text",
@@ -1346,7 +1345,7 @@ export namespace SessionPrompt {
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
if (wasPlan && input.agent.name === "build") { if (wasPlan && input.agent.name === "build") {
userMessage.parts.push({ userMessage.parts.push({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: userMessage.info.id, messageID: userMessage.info.id,
sessionID: userMessage.info.sessionID, sessionID: userMessage.info.sessionID,
type: "text", type: "text",
@@ -1366,7 +1365,7 @@ export namespace SessionPrompt {
const exists = await Filesystem.exists(plan) const exists = await Filesystem.exists(plan)
if (exists) { if (exists) {
const part = await Session.updatePart({ const part = await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: userMessage.info.id, messageID: userMessage.info.id,
sessionID: userMessage.info.sessionID, sessionID: userMessage.info.sessionID,
type: "text", type: "text",
@@ -1385,7 +1384,7 @@ export namespace SessionPrompt {
const exists = await Filesystem.exists(plan) const exists = await Filesystem.exists(plan)
if (!exists) await fs.mkdir(path.dirname(plan), { recursive: true }) if (!exists) await fs.mkdir(path.dirname(plan), { recursive: true })
const part = await Session.updatePart({ const part = await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: userMessage.info.id, messageID: userMessage.info.id,
sessionID: userMessage.info.sessionID, sessionID: userMessage.info.sessionID,
type: "text", type: "text",
@@ -1520,7 +1519,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
await Session.updateMessage(userMsg) await Session.updateMessage(userMsg)
const userPart: MessageV2.Part = { const userPart: MessageV2.Part = {
type: "text", type: "text",
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: userMsg.id, messageID: userMsg.id,
sessionID: input.sessionID, sessionID: input.sessionID,
text: "The following tool was executed by the user", text: "The following tool was executed by the user",
@@ -1555,7 +1554,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
await Session.updateMessage(msg) await Session.updateMessage(msg)
const part: MessageV2.Part = { const part: MessageV2.Part = {
type: "tool", type: "tool",
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: msg.id, messageID: msg.id,
sessionID: input.sessionID, sessionID: input.sessionID,
tool: "bash", tool: "bash",

View File

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

View File

@@ -27,3 +27,15 @@ export const MessageID = messageIdSchema.pipe(
zod: z.string().startsWith("msg").pipe(z.custom<MessageID>()), zod: z.string().startsWith("msg").pipe(z.custom<MessageID>()),
})), })),
) )
const partIdSchema = Schema.String.pipe(Schema.brand("PartId"))
export type PartID = typeof partIdSchema.Type
export const PartID = partIdSchema.pipe(
withStatics((schema: typeof partIdSchema) => ({
make: (id: string) => schema.makeUnsafe(id),
ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("part", id)),
zod: z.string().startsWith("prt").pipe(z.custom<PartID>()),
})),
)

View File

@@ -4,7 +4,7 @@ import type { MessageV2 } from "./message-v2"
import type { Snapshot } from "../snapshot" import type { Snapshot } from "../snapshot"
import type { PermissionNext } from "../permission/next" import type { PermissionNext } from "../permission/next"
import type { ProjectID } from "../project/schema" import type { ProjectID } from "../project/schema"
import type { SessionID, MessageID } from "./schema" import type { SessionID, MessageID, PartID } from "./schema"
import type { WorkspaceID } from "../control-plane/schema" import type { WorkspaceID } from "../control-plane/schema"
import { Timestamps } from "../storage/schema.sql" import { Timestamps } from "../storage/schema.sql"
@@ -30,7 +30,7 @@ export const SessionTable = sqliteTable(
summary_deletions: integer(), summary_deletions: integer(),
summary_files: integer(), summary_files: integer(),
summary_diffs: text({ mode: "json" }).$type<Snapshot.FileDiff[]>(), summary_diffs: text({ mode: "json" }).$type<Snapshot.FileDiff[]>(),
revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: string; snapshot?: string; diff?: string }>(), revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: PartID; snapshot?: string; diff?: string }>(),
permission: text({ mode: "json" }).$type<PermissionNext.Ruleset>(), permission: text({ mode: "json" }).$type<PermissionNext.Ruleset>(),
...Timestamps, ...Timestamps,
time_compacting: integer(), time_compacting: integer(),
@@ -60,7 +60,7 @@ export const MessageTable = sqliteTable(
export const PartTable = sqliteTable( export const PartTable = sqliteTable(
"part", "part",
{ {
id: text().primaryKey(), id: text().$type<PartID>().primaryKey(),
message_id: text() message_id: text()
.$type<MessageID>() .$type<MessageID>()
.notNull() .notNull()

View File

@@ -31,7 +31,7 @@ export const BatchTool = Tool.define("batch", async () => {
}, },
async execute(params, ctx) { async execute(params, ctx) {
const { Session } = await import("../session") const { Session } = await import("../session")
const { Identifier } = await import("../id/id") const { PartID } = await import("../session/schema")
const toolCalls = params.tool_calls.slice(0, 25) const toolCalls = params.tool_calls.slice(0, 25)
const discardedCalls = params.tool_calls.slice(25) const discardedCalls = params.tool_calls.slice(25)
@@ -42,7 +42,7 @@ export const BatchTool = Tool.define("batch", async () => {
const executeCall = async (call: (typeof toolCalls)[0]) => { const executeCall = async (call: (typeof toolCalls)[0]) => {
const callStartTime = Date.now() const callStartTime = Date.now()
const partID = Identifier.ascending("part") const partID = PartID.ascending()
try { try {
if (DISALLOWED.has(call.tool)) { if (DISALLOWED.has(call.tool)) {
@@ -79,7 +79,7 @@ export const BatchTool = Tool.define("batch", async () => {
const result = await tool.execute(validatedParams, { ...ctx, callID: partID }) const result = await tool.execute(validatedParams, { ...ctx, callID: partID })
const attachments = result.attachments?.map((attachment) => ({ const attachments = result.attachments?.map((attachment) => ({
...attachment, ...attachment,
id: Identifier.ascending("part"), id: PartID.ascending(),
sessionID: ctx.sessionID, sessionID: ctx.sessionID,
messageID: ctx.messageID, messageID: ctx.messageID,
})) }))
@@ -134,7 +134,7 @@ export const BatchTool = Tool.define("batch", async () => {
// Add discarded calls as errors // Add discarded calls as errors
const now = Date.now() const now = Date.now()
for (const call of discardedCalls) { for (const call of discardedCalls) {
const partID = Identifier.ascending("part") const partID = PartID.ascending()
await Session.updatePart({ await Session.updatePart({
id: partID, id: partID,
messageID: ctx.messageID, messageID: ctx.messageID,

View File

@@ -4,10 +4,9 @@ import { Tool } from "./tool"
import { Question } from "../question" import { Question } from "../question"
import { Session } from "../session" import { Session } from "../session"
import { MessageV2 } from "../session/message-v2" import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id"
import { Provider } from "../provider/provider" import { Provider } from "../provider/provider"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { type SessionID, MessageID } from "../session/schema" import { type SessionID, MessageID, PartID } from "../session/schema"
import EXIT_DESCRIPTION from "./plan-exit.txt" import EXIT_DESCRIPTION from "./plan-exit.txt"
async function getLastModel(sessionID: SessionID) { async function getLastModel(sessionID: SessionID) {
@@ -56,7 +55,7 @@ export const PlanExitTool = Tool.define("plan_exit", {
} }
await Session.updateMessage(userMsg) await Session.updateMessage(userMsg)
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: userMsg.id, messageID: userMsg.id,
sessionID: ctx.sessionID, sessionID: ctx.sessionID,
type: "text", type: "text",
@@ -114,7 +113,7 @@ export const PlanEnterTool = Tool.define("plan_enter", {
} }
await Session.updateMessage(userMsg) await Session.updateMessage(userMsg)
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: userMsg.id, messageID: userMsg.id,
sessionID: ctx.sessionID, sessionID: ctx.sessionID,
type: "text", type: "text",

View File

@@ -1,12 +1,12 @@
import { test, expect, describe } from "bun:test" import { test, expect, describe } from "bun:test"
import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github" import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github"
import type { MessageV2 } from "../../src/session/message-v2" import type { MessageV2 } from "../../src/session/message-v2"
import { SessionID, MessageID } from "../../src/session/schema" import { SessionID, MessageID, PartID } from "../../src/session/schema"
// Helper to create minimal valid parts // Helper to create minimal valid parts
function createTextPart(text: string): MessageV2.Part { function createTextPart(text: string): MessageV2.Part {
return { return {
id: "1", id: PartID.ascending(),
sessionID: SessionID.make("s"), sessionID: SessionID.make("s"),
messageID: MessageID.make("m"), messageID: MessageID.make("m"),
type: "text" as const, type: "text" as const,
@@ -16,7 +16,7 @@ function createTextPart(text: string): MessageV2.Part {
function createReasoningPart(text: string): MessageV2.Part { function createReasoningPart(text: string): MessageV2.Part {
return { return {
id: "1", id: PartID.ascending(),
sessionID: SessionID.make("s"), sessionID: SessionID.make("s"),
messageID: MessageID.make("m"), messageID: MessageID.make("m"),
type: "reasoning" as const, type: "reasoning" as const,
@@ -28,7 +28,7 @@ function createReasoningPart(text: string): MessageV2.Part {
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): MessageV2.Part { function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): MessageV2.Part {
if (status === "completed") { if (status === "completed") {
return { return {
id: "1", id: PartID.ascending(),
sessionID: SessionID.make("s"), sessionID: SessionID.make("s"),
messageID: MessageID.make("m"), messageID: MessageID.make("m"),
type: "tool" as const, type: "tool" as const,
@@ -45,7 +45,7 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn
} }
} }
return { return {
id: "1", id: PartID.ascending(),
sessionID: SessionID.make("s"), sessionID: SessionID.make("s"),
messageID: MessageID.make("m"), messageID: MessageID.make("m"),
type: "tool" as const, type: "tool" as const,
@@ -61,7 +61,7 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn
function createStepStartPart(): MessageV2.Part { function createStepStartPart(): MessageV2.Part {
return { return {
id: "1", id: PartID.ascending(),
sessionID: SessionID.make("s"), sessionID: SessionID.make("s"),
messageID: MessageID.make("m"), messageID: MessageID.make("m"),
type: "step-start" as const, type: "step-start" as const,
@@ -70,7 +70,7 @@ function createStepStartPart(): MessageV2.Part {
function createStepFinishPart(): MessageV2.Part { function createStepFinishPart(): MessageV2.Part {
return { return {
id: "1", id: PartID.ascending(),
sessionID: SessionID.make("s"), sessionID: SessionID.make("s"),
messageID: MessageID.make("m"), messageID: MessageID.make("m"),
type: "step-finish" as const, type: "step-finish" as const,

View File

@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import { APICallError } from "ai" import { APICallError } from "ai"
import { MessageV2 } from "../../src/session/message-v2" import { MessageV2 } from "../../src/session/message-v2"
import type { Provider } from "../../src/provider/provider" import type { Provider } from "../../src/provider/provider"
import { SessionID, MessageID } from "../../src/session/schema" import { SessionID, MessageID, PartID } from "../../src/session/schema"
const sessionID = SessionID.make("session") const sessionID = SessionID.make("session")
const model: Provider.Model = { const model: Provider.Model = {
@@ -98,7 +98,7 @@ function assistantInfo(
function basePart(messageID: string, id: string) { function basePart(messageID: string, id: string) {
return { return {
id, id: PartID.make(id),
sessionID, sessionID,
messageID: MessageID.make(messageID), messageID: MessageID.make(messageID),
} }

View File

@@ -6,8 +6,7 @@ import { SessionCompaction } from "../../src/session/compaction"
import { MessageV2 } from "../../src/session/message-v2" import { MessageV2 } from "../../src/session/message-v2"
import { Log } from "../../src/util/log" import { Log } from "../../src/util/log"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { Identifier } from "../../src/id/id" import { MessageID, PartID } from "../../src/session/schema"
import { MessageID } from "../../src/session/schema"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"
const projectRoot = path.join(__dirname, "../..") const projectRoot = path.join(__dirname, "../..")
@@ -40,7 +39,7 @@ describe("revert + compact workflow", () => {
// Add a text part to the user message // Add a text part to the user message
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: userMsg1.id, messageID: userMsg1.id,
sessionID, sessionID,
type: "text", type: "text",
@@ -77,7 +76,7 @@ describe("revert + compact workflow", () => {
// Add a text part to the assistant message // Add a text part to the assistant message
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: assistantMsg1.id, messageID: assistantMsg1.id,
sessionID, sessionID,
type: "text", type: "text",
@@ -100,7 +99,7 @@ describe("revert + compact workflow", () => {
}) })
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: userMsg2.id, messageID: userMsg2.id,
sessionID, sessionID,
type: "text", type: "text",
@@ -136,7 +135,7 @@ describe("revert + compact workflow", () => {
await Session.updateMessage(assistantMsg2) await Session.updateMessage(assistantMsg2)
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: assistantMsg2.id, messageID: assistantMsg2.id,
sessionID, sessionID,
type: "text", type: "text",
@@ -215,7 +214,7 @@ describe("revert + compact workflow", () => {
}) })
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: userMsg.id, messageID: userMsg.id,
sessionID, sessionID,
type: "text", type: "text",
@@ -250,7 +249,7 @@ describe("revert + compact workflow", () => {
await Session.updateMessage(assistantMsg) await Session.updateMessage(assistantMsg)
await Session.updatePart({ await Session.updatePart({
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID: assistantMsg.id, messageID: assistantMsg.id,
sessionID, sessionID,
type: "text", type: "text",

View File

@@ -5,8 +5,7 @@ import { Bus } from "../../src/bus"
import { Log } from "../../src/util/log" import { Log } from "../../src/util/log"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { MessageV2 } from "../../src/session/message-v2" import { MessageV2 } from "../../src/session/message-v2"
import { Identifier } from "../../src/id/id" import { MessageID, PartID } from "../../src/session/schema"
import { MessageID } from "../../src/session/schema"
const projectRoot = path.join(__dirname, "../..") const projectRoot = path.join(__dirname, "../..")
Log.init({ print: false }) Log.init({ print: false })
@@ -108,7 +107,7 @@ describe("step-finish token propagation via Bus event", () => {
} }
const partInput = { const partInput = {
id: Identifier.ascending("part"), id: PartID.ascending(),
messageID, messageID,
sessionID: session.id, sessionID: session.id,
type: "step-finish" as const, type: "step-finish" as const,

View File

@@ -11,7 +11,7 @@ import { ProjectTable } from "../../src/project/project.sql"
import { ProjectID } from "../../src/project/schema" import { ProjectID } from "../../src/project/schema"
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql"
import { SessionShareTable } from "../../src/share/share.sql" import { SessionShareTable } from "../../src/share/share.sql"
import { SessionID, MessageID } from "../../src/session/schema" import { SessionID, MessageID, PartID } from "../../src/session/schema"
// Test fixtures // Test fixtures
const fixtures = { const fixtures = {
@@ -259,7 +259,7 @@ describe("JSON to SQLite migration", () => {
const parts = db.select().from(PartTable).all() const parts = db.select().from(PartTable).all()
expect(parts.length).toBe(1) expect(parts.length).toBe(1)
expect(parts[0].id).toBe("prt_testabc123") expect(parts[0].id).toBe(PartID.make("prt_testabc123"))
}) })
test("migrates legacy parts without ids in body", async () => { test("migrates legacy parts without ids in body", async () => {
@@ -302,7 +302,7 @@ describe("JSON to SQLite migration", () => {
const parts = db.select().from(PartTable).all() const parts = db.select().from(PartTable).all()
expect(parts.length).toBe(1) expect(parts.length).toBe(1)
expect(parts[0].id).toBe("prt_testabc123") expect(parts[0].id).toBe(PartID.make("prt_testabc123"))
expect(parts[0].message_id).toBe(MessageID.make("msg_test789ghi")) expect(parts[0].message_id).toBe(MessageID.make("msg_test789ghi"))
expect(parts[0].session_id).toBe(SessionID.make("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("id")
@@ -374,7 +374,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite }) const db = drizzle({ client: sqlite })
const parts = db.select().from(PartTable).all() const parts = db.select().from(PartTable).all()
expect(parts.length).toBe(1) expect(parts.length).toBe(1)
expect(parts[0].id).toBe("prt_from_filename") // Uses filename, not JSON id expect(parts[0].id).toBe(PartID.make("prt_from_filename")) // Uses filename, not JSON id
expect(parts[0].message_id).toBe(MessageID.make("msg_realmsgid")) // Uses parent dir, not JSON messageID expect(parts[0].message_id).toBe(MessageID.make("msg_realmsgid")) // Uses parent dir, not JSON messageID
}) })