Compare commits

...

12 Commits

Author SHA1 Message Date
Kit Langton
77feb08c2e fix: use proper ID constructors in structured-output tests
SessionID.make("test-session") and MessageID.make("test-id") bypass
zod validation but safeParse still checks startsWith prefixes at
runtime. Use the proper constructors that generate valid prefixed IDs.
2026-03-10 23:15:41 -04:00
Kit Langton
022665c587 fix: brand sessionID in structured-output tests 2026-03-10 22:22:33 -04:00
Kit Langton
00197a8566 feat: add SessionID.descending() constructor 2026-03-10 22:17:38 -04:00
Kit Langton
24720d1f29 fix: type createNext input.id as SessionID 2026-03-10 22:16:38 -04:00
Kit Langton
c5bdbe18b4 fix: clean up boundary mappings in import/export commands
- import.ts: inline SessionID.make() in toRow spread, use row.id for session_id
- export.ts: brand sessionID at CLI arg boundary, remove redundant as string cast
2026-03-10 22:13:34 -04:00
Kit Langton
cfa809bf33 fix(export): inline SessionID.make, remove redundant intermediate variable 2026-03-10 22:11:10 -04:00
Kit Langton
3dbffd548b fix: restore .describe() on TUI session select event 2026-03-10 22:07:38 -04:00
Kit Langton
901135543f feat(session): brand SessionID through Drizzle and Zod schemas
Same pattern as ProjectID: add branded SessionID type with
SessionID.zod helper, type Drizzle columns with $type<SessionID>(),
and replace all z.string()/Identifier.schema("session") usages.
Brand flows through z.infer automatically, mapped at boundaries.
2026-03-10 22:04:13 -04:00
Kit Langton
c3c8aa78b9 fix: use z.infer<typeof Info> instead of z.infer<typeof WorkspaceInfo> 2026-03-10 21:35:03 -04:00
Kit Langton
deecd7ea89 fix: address PR feedback
- Remove unnecessary WorkspaceInfoType alias import
- Use input.projectID instead of config.projectID in workspace create
- Compare against ProjectID.global instead of String coercion in test
2026-03-10 21:33:22 -04:00
Kit Langton
ca116111f4 fix(import): remove redundant project_id override in toRow call 2026-03-10 21:29:34 -04:00
Kit Langton
e9f973ebc2 feat(project): brand ProjectID through Drizzle and Zod schemas
Introduce Effect branded ProjectID type into Drizzle table columns
and Zod schemas so the brand flows through z.infer automatically.
Adds ProjectID.zod helper to bridge Effect brands into Zod,
eliminating manual Omit & intersect type overrides.
2026-03-10 21:27:02 -04:00
54 changed files with 302 additions and 201 deletions

View File

@@ -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,

View File

@@ -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<ReturnType<typeof getUserPrompt>>["promptFiles"]

View File

@@ -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"
@@ -86,7 +87,7 @@ export const ImportCommand = cmd({
await bootstrap(process.cwd(), async () => {
let exportData:
| {
info: Session.Info
info: SDKSession
messages: Array<{
info: Message
parts: Part[]
@@ -152,7 +153,12 @@ export const ImportCommand = cmd({
return
}
const row = { ...Session.toRow(exportData.info), project_id: 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()

View File

@@ -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)
})
},

View File

@@ -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"),
}),
),
}

View File

@@ -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"),
}),

View File

@@ -1,5 +1,6 @@
import z from "zod"
import { Identifier } from "@/id/id"
import { ProjectID } from "@/project/schema"
export const WorkspaceInfo = z.object({
id: Identifier.schema("workspace"),
@@ -8,7 +9,7 @@ export const WorkspaceInfo = z.object({
name: z.string().nullable(),
directory: z.string().nullable(),
extra: z.unknown().nullable(),
projectID: z.string(),
projectID: ProjectID.zod,
})
export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>

View File

@@ -1,5 +1,6 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "../project/project.sql"
import type { ProjectID } from "../project/schema"
export const WorkspaceTable = sqliteTable("workspace", {
id: text().primaryKey(),
@@ -9,6 +10,7 @@ export const WorkspaceTable = sqliteTable("workspace", {
directory: text(),
extra: text({ mode: "json" }),
project_id: text()
.$type<ProjectID>()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
})

View File

@@ -6,6 +6,7 @@ import { Project } from "@/project/project"
import { BusEvent } from "@/bus/bus-event"
import { GlobalBus } from "@/bus/global"
import { Log } from "@/util/log"
import { ProjectID } from "@/project/schema"
import { WorkspaceTable } from "./workspace.sql"
import { getAdaptor } from "./adaptors"
import { WorkspaceInfo } from "./types"
@@ -48,7 +49,7 @@ export namespace Workspace {
id: Identifier.schema("workspace").optional(),
type: Info.shape.type,
branch: Info.shape.branch,
projectID: Info.shape.projectID,
projectID: ProjectID.zod,
extra: Info.shape.extra,
})

View File

@@ -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(),
}),

View File

@@ -2,11 +2,13 @@ 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"
import { fn } from "@/util/fn"
import { Log } from "@/util/log"
import { ProjectID } from "@/project/schema"
import { Wildcard } from "@/util/wildcard"
import os from "os"
import z from "zod"
@@ -68,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()),
@@ -90,7 +92,7 @@ export namespace PermissionNext {
export type Reply = z.infer<typeof Reply>
export const Approval = z.object({
projectID: z.string(),
projectID: ProjectID.zod,
patterns: z.string().array(),
})
@@ -99,7 +101,7 @@ export namespace PermissionNext {
Replied: BusEvent.define(
"permission.replied",
z.object({
sessionID: z.string(),
sessionID: SessionID.zod,
requestID: z.string(),
reply: Reply,
}),

View File

@@ -1,8 +1,9 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
import { Timestamps } from "../storage/schema.sql"
import type { ProjectID } from "./schema"
export const ProjectTable = sqliteTable("project", {
id: text().primaryKey(),
id: text().$type<ProjectID>().primaryKey(),
worktree: text().notNull(),
vcs: text(),
name: text(),

View File

@@ -15,6 +15,7 @@ import { existsSync } from "fs"
import { git } from "../util/git"
import { Glob } from "../util/glob"
import { which } from "../util/which"
import { ProjectID } from "./schema"
export namespace Project {
const log = Log.create({ service: "project" })
@@ -33,7 +34,7 @@ export namespace Project {
export const Info = z
.object({
id: z.string(),
id: ProjectID.zod,
worktree: z.string(),
vcs: z.literal("git").optional(),
name: z.string().optional(),
@@ -73,7 +74,7 @@ export namespace Project {
? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined }
: undefined
return {
id: row.id,
id: ProjectID.make(row.id),
worktree: row.worktree,
vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined,
name: row.name ?? undefined,
@@ -91,6 +92,7 @@ export namespace Project {
function readCachedId(dir: string) {
return Filesystem.readText(path.join(dir, "opencode"))
.then((x) => x.trim())
.then(ProjectID.make)
.catch(() => undefined)
}
@@ -111,7 +113,7 @@ export namespace Project {
if (!gitBinary) {
return {
id: id ?? "global",
id: id ?? ProjectID.global,
worktree: sandbox,
sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
@@ -130,7 +132,7 @@ export namespace Project {
if (!worktree) {
return {
id: id ?? "global",
id: id ?? ProjectID.global,
worktree: sandbox,
sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
@@ -160,14 +162,14 @@ export namespace Project {
if (!roots) {
return {
id: "global",
id: ProjectID.global,
worktree: sandbox,
sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
id = roots[0]
id = roots[0] ? ProjectID.make(roots[0]) : undefined
if (id) {
await Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined)
}
@@ -175,7 +177,7 @@ export namespace Project {
if (!id) {
return {
id: "global",
id: ProjectID.global,
worktree: sandbox,
sandbox,
vcs: "git",
@@ -208,7 +210,7 @@ export namespace Project {
}
return {
id: "global",
id: ProjectID.global,
worktree: "/",
sandbox: "/",
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
@@ -228,7 +230,7 @@ export namespace Project {
updated: Date.now(),
},
}
if (data.id !== "global") {
if (data.id !== ProjectID.global) {
await migrateFromGlobal(data.id, data.worktree)
}
return fresh
@@ -308,12 +310,12 @@ export namespace Project {
return
}
async function migrateFromGlobal(id: string, worktree: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get())
async function migrateFromGlobal(id: ProjectID, worktree: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, ProjectID.global)).get())
if (!row) return
const sessions = Database.use((db) =>
db.select().from(SessionTable).where(eq(SessionTable.project_id, "global")).all(),
db.select().from(SessionTable).where(eq(SessionTable.project_id, ProjectID.global)).all(),
)
if (sessions.length === 0) return
@@ -323,14 +325,14 @@ export namespace Project {
// Skip sessions that belong to a different directory
if (row.directory && row.directory !== worktree) return
log.info("migrating session", { sessionID: row.id, from: "global", to: id })
log.info("migrating session", { sessionID: row.id, from: ProjectID.global, to: id })
Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run())
}).catch((error) => {
log.error("failed to migrate sessions from global to project", { error, projectId: id })
})
}
export function setInitialized(id: string) {
export function setInitialized(id: ProjectID) {
Database.use((db) =>
db
.update(ProjectTable)
@@ -352,7 +354,7 @@ export namespace Project {
)
}
export function get(id: string): Info | undefined {
export function get(id: ProjectID): Info | undefined {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) return undefined
return fromRow(row)
@@ -375,12 +377,13 @@ export namespace Project {
export const update = fn(
z.object({
projectID: z.string(),
projectID: ProjectID.zod,
name: z.string().optional(),
icon: Info.shape.icon.optional(),
commands: Info.shape.commands.optional(),
}),
async (input) => {
const id = ProjectID.make(input.projectID)
const result = Database.use((db) =>
db
.update(ProjectTable)
@@ -391,7 +394,7 @@ export namespace Project {
commands: input.commands,
time_updated: Date.now(),
})
.where(eq(ProjectTable.id, input.projectID))
.where(eq(ProjectTable.id, id))
.returning()
.get(),
)
@@ -407,7 +410,7 @@ export namespace Project {
},
)
export async function sandboxes(id: string) {
export async function sandboxes(id: ProjectID) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) return []
const data = fromRow(row)
@@ -419,7 +422,7 @@ export namespace Project {
return valid
}
export async function addSandbox(id: string, directory: string) {
export async function addSandbox(id: ProjectID, directory: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) throw new Error(`Project not found: ${id}`)
const sandboxes = [...row.sandboxes]
@@ -443,7 +446,7 @@ export namespace Project {
return data
}
export async function removeSandbox(id: string, directory: string) {
export async function removeSandbox(id: ProjectID, directory: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) throw new Error(`Project not found: ${id}`)
const sandboxes = row.sandboxes.filter((s) => s !== directory)

View File

@@ -0,0 +1,16 @@
import { Schema } from "effect"
import z from "zod"
import { withStatics } from "@/util/schema"
const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectId"))
export type ProjectID = typeof projectIdSchema.Type
export const ProjectID = projectIdSchema.pipe(
withStatics((schema: typeof projectIdSchema) => ({
global: schema.makeUnsafe("global"),
make: (id: string) => schema.makeUnsafe(id),
zod: z.string().pipe(z.custom<ProjectID>()),
})),
)

View File

@@ -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<Answer[]> {

View File

@@ -4,6 +4,7 @@ import { resolver } from "hono-openapi"
import { Instance } from "../../project/instance"
import { Project } from "../../project/project"
import z from "zod"
import { ProjectID } from "../../project/schema"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
import { InstanceBootstrap } from "../../project/bootstrap"
@@ -105,7 +106,7 @@ export const ProjectRoutes = lazy(() =>
...errors(400, 404),
},
}),
validator("param", z.object({ projectID: z.string() })),
validator("param", z.object({ projectID: ProjectID.zod })),
validator("json", Project.update.schema.omit({ projectID: true })),
async (c) => {
const projectID = c.req.valid("param").projectID

View File

@@ -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(),
}),
),

View File

@@ -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(),

View File

@@ -23,6 +23,8 @@ import { fn } from "@/util/fn"
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"
@@ -118,12 +120,12 @@ export namespace Session {
export const Info = z
.object({
id: Identifier.schema("session"),
id: SessionID.zod,
slug: z.string(),
projectID: 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(),
@@ -162,7 +164,7 @@ export namespace Session {
export const ProjectInfo = z
.object({
id: z.string(),
id: ProjectID.zod,
name: z.string().optional(),
worktree: z.string(),
})
@@ -200,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,
}),
),
@@ -216,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(),
@@ -235,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) => {
@@ -276,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
@@ -292,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,
@@ -341,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")
@@ -363,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)
@@ -377,7 +379,7 @@ export namespace Session {
export const setTitle = fn(
z.object({
sessionID: Identifier.schema("session"),
sessionID: SessionID.zod,
title: z.string(),
}),
async (input) => {
@@ -398,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) => {
@@ -419,7 +421,7 @@ export namespace Session {
export const setPermission = fn(
z.object({
sessionID: Identifier.schema("session"),
sessionID: SessionID.zod,
permission: PermissionNext.Ruleset,
}),
async (input) => {
@@ -440,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,
}),
@@ -466,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)
@@ -486,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) => {
@@ -510,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<Snapshot.FileDiff[]>(["session_diff", sessionID])
} catch {
@@ -520,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) => {
@@ -646,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
@@ -658,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)
@@ -704,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) => {
@@ -726,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"),
}),
@@ -774,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(),
@@ -872,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"),

View File

@@ -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<WithParts> => {

View File

@@ -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

View File

@@ -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
}) {

View File

@@ -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) => {
@@ -726,7 +727,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
}
@@ -1462,7 +1463,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({
@@ -1713,7 +1714,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(),

View File

@@ -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)

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 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<SessionID>()),
})),
)

View File

@@ -3,6 +3,8 @@ import { ProjectTable } from "../project/project.sql"
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<MessageV2.Part, "id" | "sessionID" | "messageID">
@@ -11,12 +13,13 @@ type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
export const SessionTable = sqliteTable(
"session",
{
id: text().primaryKey(),
id: text().$type<SessionID>().primaryKey(),
project_id: text()
.$type<ProjectID>()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
workspace_id: text(),
parent_id: text(),
parent_id: text().$type<SessionID>(),
slug: text().notNull(),
directory: text().notNull(),
title: text().notNull(),
@@ -44,6 +47,7 @@ export const MessageTable = sqliteTable(
{
id: text().primaryKey(),
session_id: text()
.$type<SessionID>()
.notNull()
.references(() => SessionTable.id, { onDelete: "cascade" }),
...Timestamps,
@@ -59,7 +63,7 @@ export const PartTable = sqliteTable(
message_id: text()
.notNull()
.references(() => MessageTable.id, { onDelete: "cascade" }),
session_id: text().notNull(),
session_id: text().$type<SessionID>().notNull(),
...Timestamps,
data: text({ mode: "json" }).notNull().$type<PartData>(),
},
@@ -70,6 +74,7 @@ export const TodoTable = sqliteTable(
"todo",
{
session_id: text()
.$type<SessionID>()
.notNull()
.references(() => SessionTable.id, { onDelete: "cascade" }),
content: text().notNull(),

View File

@@ -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,

View File

@@ -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) => {

View File

@@ -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(),
)

View File

@@ -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<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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<M extends Metadata = Metadata> = {
sessionID: string
sessionID: SessionID
messageID: string
agent: string
abort: AbortSignal

View File

@@ -8,6 +8,7 @@ import { InstanceBootstrap } from "../project/bootstrap"
import { Project } from "../project/project"
import { Database, eq } from "../storage/db"
import { ProjectTable } from "../project/project.sql"
import type { ProjectID } from "../project/schema"
import { fn } from "../util/fn"
import { Log } from "../util/log"
import { Process } from "../util/process"
@@ -310,7 +311,7 @@ export namespace Worktree {
return false
}
async function runStartScripts(directory: string, input: { projectID: string; extra?: string }) {
async function runStartScripts(directory: string, input: { projectID: ProjectID; extra?: string }) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get())
const project = row ? Project.fromRow(row) : undefined
const startup = project?.commands?.start?.trim() ?? ""
@@ -322,7 +323,7 @@ export namespace Worktree {
return true
}
function queueStartScripts(directory: string, input: { projectID: string; extra?: string }) {
function queueStartScripts(directory: string, input: { projectID: ProjectID; extra?: string }) {
setTimeout(() => {
const start = async () => {
await runStartScripts(directory, input)

View File

@@ -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",

View File

@@ -8,6 +8,7 @@ import path from "path"
import fs from "fs/promises"
import { pathToFileURL } from "url"
import { Global } from "../../src/global"
import { ProjectID } from "../../src/project/schema"
import { Filesystem } from "../../src/util/filesystem"
// Get managed config directory from environment (set in preload.ts)
@@ -44,7 +45,7 @@ async function check(map: (dir: string) => string) {
const cfg = await Config.get()
expect(cfg.snapshot).toBe(true)
expect(Instance.directory).toBe(Filesystem.resolve(tmp.path))
expect(Instance.project.id).not.toBe("global")
expect(Instance.project.id).not.toBe(ProjectID.global)
},
})
} finally {

View File

@@ -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",

View File

@@ -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: {},

View File

@@ -6,6 +6,7 @@ import path from "path"
import { tmpdir } from "../fixture/fixture"
import { Filesystem } from "../../src/util/filesystem"
import { GlobalBus } from "../../src/bus/global"
import { ProjectID } from "../../src/project/schema"
Log.init({ print: false })
@@ -74,7 +75,7 @@ describe("Project.fromDirectory", () => {
const { project } = await p.fromDirectory(tmp.path)
expect(project).toBeDefined()
expect(project.id).toBe("global")
expect(project.id).toBe(ProjectID.global)
expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path)
@@ -90,7 +91,7 @@ describe("Project.fromDirectory", () => {
const { project } = await p.fromDirectory(tmp.path)
expect(project).toBeDefined()
expect(project.id).not.toBe("global")
expect(project.id).not.toBe(ProjectID.global)
expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path)
@@ -107,7 +108,7 @@ describe("Project.fromDirectory", () => {
await withMode("rev-list-fail", async () => {
const { project } = await p.fromDirectory(tmp.path)
expect(project.vcs).toBe("git")
expect(project.id).toBe("global")
expect(project.id).toBe(ProjectID.global)
expect(project.worktree).toBe(tmp.path)
})
})
@@ -301,7 +302,7 @@ describe("Project.update", () => {
await expect(
Project.update({
projectID: "nonexistent-project-id",
projectID: ProjectID.make("nonexistent-project-id"),
name: "Should Fail",
}),
).rejects.toThrow("Project not found: nonexistent-project-id")

View File

@@ -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?",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -8,8 +8,10 @@ import { readFileSync, readdirSync } from "fs"
import { JsonMigration } from "../../src/storage/json-migration"
import { Global } from "../../src/global"
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 = {
@@ -123,7 +125,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_test123abc")
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
expect(projects[0].worktree).toBe("/test/path")
expect(projects[0].name).toBe("Test Project")
expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
@@ -148,7 +150,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_filename") // Uses filename, not JSON id
expect(projects[0].id).toBe(ProjectID.make("proj_filename")) // Uses filename, not JSON id
})
test("migrates project with commands", async () => {
@@ -169,7 +171,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_with_commands")
expect(projects[0].id).toBe(ProjectID.make("proj_with_commands"))
expect(projects[0].commands).toEqual({ start: "npm run dev" })
})
@@ -190,7 +192,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_no_commands")
expect(projects[0].id).toBe(ProjectID.make("proj_no_commands"))
expect(projects[0].commands).toBeNull()
})
@@ -219,8 +221,8 @@ 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].project_id).toBe("proj_test123abc")
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")
expect(sessions[0].summary_additions).toBe(10)
@@ -294,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")
@@ -302,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")
@@ -335,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 () => {
@@ -425,8 +427,8 @@ 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].project_id).toBe(gitBasedProjectID) // Uses directory, not stale JSON
expect(sessions[0].id).toBe(SessionID.make("ses_migrated"))
expect(sessions[0].project_id).toBe(ProjectID.make(gitBasedProjectID)) // Uses directory, not stale JSON
})
test("uses filename for session id when JSON has different value", async () => {
@@ -457,8 +459,8 @@ 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].project_id).toBe("proj_test123abc")
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"))
})
test("is idempotent (running twice doesn't duplicate)", async () => {
@@ -643,7 +645,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_test123abc")
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
})
test("skips invalid todo entries while preserving source positions", async () => {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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<Tool.Context, "ask"> = {
sessionID: "test",
sessionID: SessionID.make("ses_test"),
messageID: "",
callID: "",
agent: "build",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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<Tool.Context, "ask"> = {
sessionID: "test",
sessionID: SessionID.make("ses_test"),
messageID: "",
callID: "",
agent: "build",

View File

@@ -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",

View File

@@ -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",