diff --git a/packages/core/bench/hotpath.ts b/packages/core/bench/hotpath.ts new file mode 100644 index 000000000000..10614cfbe0d4 --- /dev/null +++ b/packages/core/bench/hotpath.ts @@ -0,0 +1,236 @@ +// Microbenchmarks for the 5 targeted performance issues. +// These run synchronously to measure raw function throughput. + +import { Token } from "../src/util/token" +import type { SessionMessage } from "../src/session/message" + +// ── Helpers ─────────────────────────────────────────────────────────────── + +function format(ms: number): string { + return ms.toFixed(3) + "ms" +} + +function quantile(sorted: number[], q: number): number { + const idx = Math.floor(sorted.length * q) + return sorted[Math.min(idx, sorted.length - 1)] +} + +function bench(label: string, fn: () => unknown, iterations: number) { + // warmup + for (let i = 0; i < Math.min(iterations, 100); i++) fn() + + const times: number[] = [] + for (let i = 0; i < iterations; i++) { + const t = performance.now() + fn() + times.push(performance.now() - t) + } + const sorted = [...times].sort((a, b) => a - b) + const total = sorted.reduce((a, b) => a + b, 0) + const avg = total / sorted.length + const ops = (sorted.length / (total / 1000)).toFixed(0) + console.log(`${label}:`) + console.log(` total: ${format(total)} (${ops} ops/s)`) + console.log(` avg: ${format(avg)}`) + console.log(` p50: ${format(quantile(sorted, 0.5))}`) + console.log(` p95: ${format(quantile(sorted, 0.95))}`) + return { avg, total, ops: Number(ops) } +} + +// ── Test data ───────────────────────────────────────────────────────────── + +function makeLargeRequest(size: number) { + const messages = Array.from({ length: size }, (_, i) => ({ + role: i % 2 === 0 ? "user" : ("assistant" as const), + content: [ + { type: "text" as const, text: `Message ${i} `.repeat(50) }, + ...(i % 3 === 0 ? [{ type: "text" as const, text: "Another text block. ".repeat(20) }] : []), + ], + ...(i % 5 === 0 + ? { + toolCalls: [ + { name: "read_file" as const, input: { path: "/src/main.ts" }, id: `tool_${i}` }, + { name: "search" as const, input: { query: "find".repeat(10) }, id: `tool_s_${i}` }, + ], + } + : {}), + })) + return { + system: [{ type: "text" as const, text: "You are a helpful assistant. ".repeat(100) }], + messages, + tools: Array.from({ length: 20 }, (_, i) => ({ + name: `tool_${i}`, + description: `Tool ${i} does something. `.repeat(10), + input: { type: "object" as const, properties: { x: { type: "string" as const } } }, + })), + } +} + +// ── Issue 1: Token Estimation ───────────────────────────────────────────── + +function estimateViaJSONStringify(value: unknown): number { + return Token.estimate(JSON.stringify(value)) +} + +function estimateViaWalker(value: unknown): number { + if (typeof value === "string") return Math.round(value.length / 4) + if (typeof value === "number" || typeof value === "boolean") return 1 + if (value === null || value === undefined) return 0 + if (Array.isArray(value)) { + let total = 0 + for (let i = 0; i < value.length; i++) total += estimateViaWalker(value[i]) + return total + } + if (typeof value === "object") { + let total = 0 + const entries = Object.entries(value as Record) + for (let i = 0; i < entries.length; i++) total += estimateViaWalker(entries[i][1]) + return total + } + return 0 +} + +function benchTokenEstimation() { + const request = makeLargeRequest(50) + console.log(`\n=== Issue 1: Token Estimation (50-message request, 500 iterations) ===\n`) + + bench("JSON.stringify-based estimate", () => estimateViaJSONStringify(request), 500) + bench("Structure-walker estimate", () => estimateViaWalker(request), 500) +} + +// ── Issue 3: Message Content Iteration ───────────────────────────────────── + +const assistantToolTypes = ["tool"] as const + +function iteration4Pass(items: { type: string; id: string; name: string; text?: string; state: { input: string | object }; provider: { executed: boolean } }[]) { + const content = items + .filter((i) => i.type === "text" || i.type === "reasoning" || i.type === "tool") + .flatMap((i): { type: string; text?: string; id?: string; name?: string; input?: string }[] => { + if (i.type === "text") return [{ type: "text", text: i.text ?? "" }] + if (i.type === "reasoning") return i.text ? [{ type: "text", text: i.text }] : [] + return [{ type: "tool-call", id: i.id, name: i.name, input: i.state.input as string }] + }) + const results = items + .filter((i) => i.type === "tool" && !i.provider.executed) + .map((i) => ({ type: "tool-result" as const, id: i.id, name: i.name })) + return [...content, ...results] +} + +function iteration1Pass(items: { type: string; id: string; name: string; text?: string; state: { input: string | object }; provider: { executed: boolean } }[]) { + const content: { type: string; text?: string; id?: string; name?: string; input?: unknown }[] = [] + const results: { type: string; id: string; name: string }[] = [] + for (let i = 0; i < items.length; i++) { + const item = items[i]! + if (item.type === "text") content.push({ type: "text", text: item.text }) + else if (item.type === "reasoning") { if (item.text) content.push({ type: "text", text: item.text }) } + else { + content.push({ type: "tool-call", id: item.id, name: item.name, input: item.state.input }) + if (!item.provider.executed) results.push({ type: "tool-result", id: item.id, name: item.name }) + } + } + return [...content, ...results] +} + +function makeAssistantContent(count: number) { + return Array.from({ length: count }, (_, i): { type: string; id: string; name: string; text?: string; state: { input: string | object }; provider: { executed: boolean } } => ({ + type: i % 3 === 0 ? "text" : i % 3 === 1 ? "reasoning" : "tool", + id: `item_${i}`, + name: "read_file", + text: i % 3 !== 2 ? `Content ${i} `.repeat(20) : undefined, + state: { input: `{"path":"/file${i}.ts"}` }, + provider: { executed: i % 2 === 0 }, + })) +} + +function benchMessageContentIteration() { + const content = makeAssistantContent(30) + console.log(`\n=== Issue 3: Message Content Iteration (30 items, 1000 iterations) ===\n`) + + bench("4-pass (flatMap + filter + map + filter)", () => iteration4Pass(content), 1000) + bench("1-pass (single for loop)", () => iteration1Pass(content), 1000) +} + +// ── Issue 4: structuredClone ────────────────────────────────────────────── + +interface Part { + id: string + sessionID: string + messageID: string + type: string + data: { text: string; metadata: Record; nested: { x: number; y: number }[] } +} + +function makePart(size: number): Part { + return { + id: "part_1234567890", + sessionID: "ses_abcdef1234567890", + messageID: "msg_abcdef1234567890", + type: "text", + data: { + text: "Hello world! ".repeat(size), + metadata: { key1: "value1", key2: 42, key3: true, key4: null }, + nested: Array.from({ length: Math.min(size, 50) }, (_, i) => ({ x: i, y: i * 2 })), + }, + } +} + +function benchStructuredClone() { + const small = makePart(1) + const large = makePart(100) + console.log(`\n=== Issue 4: Part Cloning (5000 iterations) ===\n`) + + bench("structuredClone (small part)", () => structuredClone(small), 5000) + bench("JSON round-trip (small part)", () => JSON.parse(JSON.stringify(small)) as Part, 5000) + bench("structuredClone (large part)", () => structuredClone(large), 5000) + bench("JSON round-trip (large part)", () => JSON.parse(JSON.stringify(large)) as Part, 5000) +} + +// ── Issue 5: Session List ───────────────────────────────────────────────── + +function benchSessionListImpact() { + const rows = Array.from({ length: 1000 }, (_, i) => ({ + id: `ses_${i}`, + project_id: "global", + slug: `session-${i}`, + directory: `/project/dir${i % 10}`, + title: `Session ${i}`, + version: "1.0", + time_created: Date.now() - i * 1000, + time_updated: Date.now(), + cost: 0, + tokens_input: 0, + tokens_output: 0, + tokens_reasoning: 0, + tokens_cache_read: 0, + tokens_cache_write: 0, + })) + + // Simulate: data is already loaded, we sort & reverse. + // Cost of all() vs limit(50).all() is in the DB layer (not microbenchmarkable here), + // so we benchmark the in-memory sort + map cost. + console.log(`\n=== Issue 5: Session List Processing (1000 rows, 1000 iterations) ===\n`) + + bench("unlimited: sort + reverse + map", () => { + const copy = [...rows] + copy.sort((a, b) => b.time_created - a.time_created) + return copy.map((r) => ({ id: r.id, slug: r.slug, title: r.title })) + }, 1000) + + bench("limited (50): sort + reverse + map", () => { + const copy = [...rows] + copy.sort((a, b) => b.time_created - a.time_created) + return copy.slice(0, 50).map((r) => ({ id: r.id, slug: r.slug, title: r.title })) + }, 1000) +} + +// ── Main ────────────────────────────────────────────────────────────────── + +console.log("=== Hot-Path Microbenchmarks ===\n") + +benchTokenEstimation() +benchMessageContentIteration() +benchStructuredClone() +benchSessionListImpact() + +console.log("\n=== Done ===\n") +process.exit(0) diff --git a/packages/core/bench/session.ts b/packages/core/bench/session.ts new file mode 100644 index 000000000000..954ab1867b93 --- /dev/null +++ b/packages/core/bench/session.ts @@ -0,0 +1,245 @@ +import { Effect, Layer } from "effect" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { Project } from "@opencode-ai/core/project" +import { Location } from "@opencode-ai/core/location" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { SessionV2 } from "@opencode-ai/core/session" +import { SessionStore } from "@opencode-ai/core/session/store" +import { SessionProjector } from "@opencode-ai/core/session/projector" +import { SessionExecution } from "@opencode-ai/core/session/execution" +import { Prompt } from "@opencode-ai/core/session/prompt" +import { SessionMessage } from "@opencode-ai/core/session/message" + +// ── Layer Setup ────────────────────────────────────────────────────────── +const database = Database.layerFromPath(":memory:") +const events = EventV2.layer.pipe(Layer.provide(database)) +const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database)) +const store = SessionStore.layer.pipe(Layer.provide(database)) +const sessions = SessionV2.layer.pipe( + Layer.provide(events), + Layer.provide(database), + Layer.provide(store), + Layer.provide(Project.defaultLayer), + Layer.provide(SessionExecution.noopLayer), +) +const layer = Layer.mergeAll(database, events, projector, store, SessionExecution.noopLayer, sessions) + +const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) + +// ── Helpers ─────────────────────────────────────────────────────────────── +function format(ms: number): string { + return ms.toFixed(2) + "ms" +} + +function quantile(sorted: number[], q: number): number { + const idx = Math.floor(sorted.length * q) + return sorted[Math.min(idx, sorted.length - 1)] +} + +// Measure the time of an effect (single invocation) +function timed(effect: Effect.Effect): Effect.Effect<{ value: A; elapsed: number }, E, R> { + return Effect.gen(function* () { + const t = performance.now() + const value = yield* effect + return { value, elapsed: performance.now() - t } + }) +} + +// Run an effect N times, collecting timings +function benchN(n: number, effect: Effect.Effect): Effect.Effect { + return Effect.forEach( + Array.from({ length: n }, (_, i) => i), + () => timed(effect).pipe(Effect.map((r) => r.elapsed)), + ) +} + +// ── Benchmarks (as Effects) ──────────────────────────────────────────────── + +const benchSessionCreate = (count: number) => + benchN(count, SessionV2.Service.use((svc) => svc.create({ location }))) + +const benchSessionCreateBatch = (count: number) => + timed( + Effect.forEach( + Array.from({ length: count }, (_, i) => i), + () => SessionV2.Service.use((svc) => svc.create({ location })), + { concurrency: count }, + ), + ) + +const benchPromptAdmit = (sessionID: SessionV2.ID, count: number) => + benchN(count, SessionV2.Service.use((svc) => svc.prompt({ sessionID, prompt: new Prompt({ text: "m" }), delivery: "steer", resume: false }))) + +const benchContextLoad = (sessionID: SessionV2.ID, reads: number) => + benchN(reads, SessionStore.Service.use((svc) => svc.context(sessionID))) + +const benchSingleMessage = (sessionID: SessionV2.ID, messageID: SessionMessage.ID, reads: number) => + benchN(reads, SessionV2.Service.use((svc) => svc.message({ sessionID, messageID }))) + +const benchGetSession = (sessionID: SessionV2.ID, reads: number) => + benchN(reads, SessionV2.Service.use((svc) => svc.get(sessionID))) + +const benchListSessions = (reads: number) => + benchN(reads, SessionV2.Service.use((svc) => svc.list())) + +const benchConcurrentCreate = (total: number, concurrency: number) => + timed( + Effect.gen(function* () { + const batchSize = Math.ceil(total / concurrency) + const fibers = Array.from({ length: concurrency }, () => + Effect.forEach( + Array.from({ length: batchSize }, (_, i) => i), + () => SessionV2.Service.use((svc) => svc.create({ location })), + ), + ) + yield* Effect.all(fibers, { concurrency }) + }), + ) + +// ── Report ──────────────────────────────────────────────────────────────── + +function report(name: string, label: string, times: number[]) { + const sorted = [...times].sort((a, b) => a - b) + const n = sorted.length + const total = sorted.reduce((a, b) => a + b, 0) + const avg = total / n + const ops = (n / (total / 1000)).toFixed(0) + console.log(`${name} (${label}):`) + console.log(` total: ${format(total)} (${ops} ops/s)`) + console.log(` avg: ${format(avg)}`) + console.log(` p50: ${format(quantile(sorted, 0.5))}`) + console.log(` p95: ${format(quantile(sorted, 0.95))}`) + console.log(` p99: ${format(quantile(sorted, 0.99))}`) + console.log(` min: ${format(sorted[0])}`) + console.log(` max: ${format(sorted[sorted.length - 1])}`) + return { avg, p50: quantile(sorted, 0.5), p95: quantile(sorted, 0.95) } +} + +function metric(key: string, value: number) { + console.log(`METRIC ${key}=${value}`) +} + +// ── Main ────────────────────────────────────────────────────────────────── + +const main = Effect.gen(function* () { + // warmup + yield* SessionV2.Service.use((svc) => svc.create({ location })) + + console.log("\n=== Session / Prompt Processing Benchmarks ===\n") + + // Session Creation (serial) + { + const times = yield* benchSessionCreate(50) + const stats = report("Session Creation", "50 serial", times) + console.log() + metric("session_create_count", 50) + metric("session_create_avg_ms", stats.avg) + metric("session_create_p50_ms", stats.p50) + metric("session_create_p95_ms", stats.p95) + } + + // Session Creation (concurrent) + { + const result = yield* benchSessionCreateBatch(50) + console.log(`\nSession Create Concurrent (50 parallel):`) + console.log(` total: ${format(result.elapsed)} (${(50 / (result.elapsed / 1000)).toFixed(0)} ops/s)`) + console.log() + metric("session_create_concurrent_total_ms", result.elapsed) + } + + // Prompt Admission + { + const sid = SessionV2.ID.create() + yield* SessionV2.Service.use((svc) => svc.create({ id: sid, location })) + const times = yield* benchPromptAdmit(sid, 200) + const stats = report("Prompt Admission", "200 serial", times) + console.log() + metric("prompt_admit_count", 200) + metric("prompt_admit_avg_ms", stats.avg) + metric("prompt_admit_p50_ms", stats.p50) + metric("prompt_admit_p95_ms", stats.p95) + } + + // Context Load at various sizes + for (const count of [10, 50, 200]) { + const sid = SessionV2.ID.create() + yield* SessionV2.Service.use((svc) => svc.create({ id: sid, location })) + yield* Effect.forEach( + Array.from({ length: count }, (_, i) => i), + (i) => SessionV2.Service.use((svc) => svc.prompt({ sessionID: sid, prompt: new Prompt({ text: `m${i}` }), delivery: "steer", resume: false })), + ) + const times = yield* benchContextLoad(sid, 100) + const sorted = [...times].sort((a, b) => a - b) + const avg = sorted.reduce((a, b) => a + b, 0) / sorted.length + const p50 = quantile(sorted, 0.5) + const p95 = quantile(sorted, 0.95) + console.log(`\nContext Load (${count} messages, 100 reads):`) + console.log(` avg: ${format(avg)}`) + console.log(` p50: ${format(p50)}`) + console.log(` p95: ${format(p95)}`) + console.log() + metric(`context_load_count_${count}_avg_ms`, avg) + metric(`context_load_count_${count}_p50_ms`, p50) + metric(`context_load_count_${count}_p95_ms`, p95) + } + + // Single Message Lookup + { + const sid = SessionV2.ID.create() + yield* SessionV2.Service.use((svc) => svc.create({ id: sid, location })) + const admitted = yield* SessionV2.Service.use((svc) => svc.prompt({ sessionID: sid, prompt: new Prompt({ text: "lookup target" }), delivery: "steer", resume: false })) + const times = yield* benchSingleMessage(sid, admitted.id, 100) + const avg = times.reduce((a, b) => a + b, 0) / times.length + const sorted = [...times].sort((a, b) => a - b) + console.log(`\nSingle Message Lookup (100 reads):`) + console.log(` avg: ${format(avg)}`) + console.log(` p50: ${format(quantile(sorted, 0.5))}`) + console.log(` p95: ${format(quantile(sorted, 0.95))}`) + console.log() + metric("message_lookup_avg_ms", avg) + } + + // Get Session + { + const sid = SessionV2.ID.create() + yield* SessionV2.Service.use((svc) => svc.create({ id: sid, location })) + const times = yield* benchGetSession(sid, 100) + const avg = times.reduce((a, b) => a + b, 0) / times.length + const sorted = [...times].sort((a, b) => a - b) + console.log(`\nGet Session (100 reads):`) + console.log(` avg: ${format(avg)}`) + console.log(` p50: ${format(quantile(sorted, 0.5))}`) + console.log(` p95: ${format(quantile(sorted, 0.95))}`) + console.log() + metric("session_get_avg_ms", avg) + } + + // List Sessions + { + const times = yield* benchListSessions(100) + const avg = times.reduce((a, b) => a + b, 0) / times.length + const sorted = [...times].sort((a, b) => a - b) + console.log(`\nList Sessions (100 reads):`) + console.log(` avg: ${format(avg)}`) + console.log(` p50: ${format(quantile(sorted, 0.5))}`) + console.log(` p95: ${format(quantile(sorted, 0.95))}`) + console.log() + metric("session_list_avg_ms", avg) + } + + // Concurrent Session Creates at different concurrency levels + { + for (const concurrency of [5, 10, 25]) { + const result = yield* benchConcurrentCreate(50, concurrency) + console.log(`\nConcurrent Session Create (50 total, ${concurrency} concurrent):`) + console.log(` total: ${format(result.elapsed)} (${(50 / (result.elapsed / 1000)).toFixed(0)} ops/s)`) + console.log() + metric(`session_concurrent_create_${concurrency}_total_ms`, result.elapsed) + } + } + + console.log("=== Done ===\n") +}) + +Effect.runPromise(main.pipe(Effect.provide(layer))).then(() => process.exit(0)) diff --git a/packages/core/migration/20260608224125_stale_tomas/migration.sql b/packages/core/migration/20260608224125_stale_tomas/migration.sql new file mode 100644 index 000000000000..877834ac88eb --- /dev/null +++ b/packages/core/migration/20260608224125_stale_tomas/migration.sql @@ -0,0 +1,3 @@ +CREATE INDEX `session_directory_time_idx` ON `session` (`directory`,`time_created`,`id`);--> statement-breakpoint +CREATE INDEX `session_workspace_time_idx` ON `session` (`workspace_id`,`time_created`,`id`);--> statement-breakpoint +CREATE INDEX `session_project_time_idx` ON `session` (`project_id`,`time_created`,`id`); \ No newline at end of file diff --git a/packages/core/migration/20260608224125_stale_tomas/snapshot.json b/packages/core/migration/20260608224125_stale_tomas/snapshot.json new file mode 100644 index 000000000000..dd860ce7b25d --- /dev/null +++ b/packages/core/migration/20260608224125_stale_tomas/snapshot.json @@ -0,0 +1,2149 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "88b13f3b-119a-4312-8c92-f71f84eae184", + "prevIds": [ + "d1bfa125-b81e-4c61-9b6e-e74abf6e488f" + ], + "ddl": [ + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "project_directory", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "session_context_epoch", + "entityType": "tables" + }, + { + "name": "session_input", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'build'", + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline_seq", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "replacement_seq", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "revision", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admitted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "promoted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "session" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_input", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_output", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_reasoning", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_read", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_write", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": [ + "active_account_id" + ], + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_project_directory_project_id_project_id_fk", + "entityType": "fks", + "table": "project_directory" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_context_epoch_session_id_session_id_fk", + "entityType": "fks", + "table": "session_context_epoch" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_input_session_id_session_id_fk", + "entityType": "fks", + "table": "session_input" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "email", + "url" + ], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": [ + "project_id", + "directory" + ], + "nameExplicit": false, + "name": "project_directory_pk", + "entityType": "pks", + "table": "project_directory" + }, + { + "columns": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_context_epoch_pk", + "table": "session_context_epoch", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_input_pk", + "table": "session_input", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_aggregate_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_aggregate_type_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "permission_project_action_resource_idx", + "entityType": "indexes", + "table": "permission" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + }, + { + "value": "delivery", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_input_session_pending_delivery_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_admitted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_promoted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_message_session_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_time_created_id_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "directory", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_directory_time_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_time_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_time_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index e720d9357c0d..27517dac18f8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,6 +10,7 @@ "migration": "bun run script/migration.ts", "fix-node-pty": "bun run script/fix-node-pty.ts", "test": "bun test", + "bench:session": "bun run bench/session.ts", "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", "typecheck": "tsgo --noEmit" }, diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index a7e9dd132efc..f12a3678fa53 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -34,5 +34,6 @@ export const migrations = ( import("./migration/20260604172448_event_sourced_session_input"), import("./migration/20260605003541_add_session_context_snapshot"), import("./migration/20260605042240_add_context_epoch_agent"), + import("./migration/20260608224125_stale_tomas"), ]) ).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration/20260608224125_stale_tomas.ts b/packages/core/src/database/migration/20260608224125_stale_tomas.ts new file mode 100644 index 000000000000..243c6b10864a --- /dev/null +++ b/packages/core/src/database/migration/20260608224125_stale_tomas.ts @@ -0,0 +1,13 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260608224125_stale_tomas", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`CREATE INDEX \`session_directory_time_idx\` ON \`session\` (\`directory\`,\`time_created\`,\`id\`);`) + yield* tx.run(`CREATE INDEX \`session_workspace_time_idx\` ON \`session\` (\`workspace_id\`,\`time_created\`,\`id\`);`) + yield* tx.run(`CREATE INDEX \`session_project_time_idx\` ON \`session\` (\`project_id\`,\`time_created\`,\`id\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index d5163cf88397..fbe77913818a 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -288,9 +288,8 @@ export const layer = Layer.effect( order === "asc" ? asc(sortColumn) : desc(sortColumn), order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id), ) - const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe( - Effect.orDie, - ) + const limit = input.limit ?? 50 + const rows = yield* query.limit(limit).all().pipe(Effect.orDie) return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row)) }), messages: Effect.fn("V2Session.messages")(function* (input) { @@ -322,9 +321,8 @@ export const layer = Layer.effect( .from(SessionMessageTable) .where(where) .orderBy(order === "asc" ? asc(SessionMessageTable.seq) : desc(SessionMessageTable.seq)) - const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe( - Effect.orDie, - ) + const limit = input.limit ?? 200 + const rows = yield* query.limit(limit).all().pipe(Effect.orDie) return yield* Effect.forEach(direction === "previous" ? rows.toReversed() : rows, decode) }), message: Effect.fn("V2Session.message")(function* (input) { @@ -355,7 +353,6 @@ export const layer = Layer.effect( }, Effect.uninterruptible) const messageID = input.id ?? SessionMessage.ID.create() const delivery = input.delivery ?? "steer" - const expected = { sessionID: input.sessionID, messageID, prompt: input.prompt, delivery } const admitted = yield* SessionInput.admit(db, events, { id: messageID, sessionID: input.sessionID, @@ -368,7 +365,11 @@ export const layer = Layer.effect( : Effect.die(defect), ), ) - if (!SessionInput.equivalent(admitted, expected)) + if ( + admitted.sessionID !== input.sessionID || + admitted.delivery !== delivery || + !Prompt.equivalence(admitted.prompt, input.prompt) + ) return yield* new PromptConflictError({ sessionID: input.sessionID, messageID }) return yield* returnPrompt(admitted) }), diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 5229949cb958..4833b38a2d79 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -9,6 +9,7 @@ import { SessionMessage } from "./message" import { SessionSchema } from "./schema" import { Token } from "../util/token" + const DEFAULT_BUFFER = 20_000 const DEFAULT_KEEP_TOKENS = 8_000 const TOOL_OUTPUT_MAX_CHARS = 2_000 @@ -76,7 +77,23 @@ type Input = { readonly request: LLMRequest } -const estimate = (value: unknown) => Token.estimate(JSON.stringify(value)) +const estimateTokens = (value: unknown, depth = 0): number => { + if (depth > 20) return 0 + if (typeof value === "string") return Math.round(value.length / 4) + if (typeof value === "number" || typeof value === "boolean") return 1 + if (value === null || value === undefined) return 0 + if (Array.isArray(value)) { + let total = 0 + for (let i = 0; i < value.length; i++) total += estimateTokens(value[i], depth + 1) + return total + } + if (typeof value === "object") { + let total = 0 + for (const key in value as Record) total += estimateTokens((value as Record)[key]!, depth + 1) + return total + } + return 0 +} const truncate = (value: string) => value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]` @@ -233,7 +250,7 @@ export const make = (dependencies: Dependencies) => { if (context === undefined || context <= 0) return false const output = input.request.generation?.maxTokens ?? input.model.route.defaults.limits?.output ?? 0 if ( - estimate({ system: input.request.system, messages: input.request.messages, tools: input.request.tools }) <= + estimateTokens({ system: input.request.system, messages: input.request.messages, tools: input.request.tools }) <= context - Math.max(output, config.buffer) ) return false diff --git a/packages/core/src/session/history.ts b/packages/core/src/session/history.ts index 285c1bcd5c7a..5c7d685971a3 100644 --- a/packages/core/src/session/history.ts +++ b/packages/core/src/session/history.ts @@ -12,7 +12,7 @@ const decode = Schema.decodeUnknownEffect(SessionMessage.Message) const latestCompaction = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) { return yield* db - .select() + .select({ seq: SessionMessageTable.seq }) .from(SessionMessageTable) .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction"))) .orderBy(desc(SessionMessageTable.seq)) @@ -26,13 +26,15 @@ const messageRows = Effect.fnUntraced(function* ( sessionID: SessionSchema.ID, compaction: { readonly seq: number } | undefined, baselineSeq?: number, + sinceSeq?: number, ) { const rows = yield* db - .select() + .select({ id: SessionMessageTable.id, session_id: SessionMessageTable.session_id, type: SessionMessageTable.type, seq: SessionMessageTable.seq, data: SessionMessageTable.data }) .from(SessionMessageTable) .where( and( eq(SessionMessageTable.session_id, sessionID), + sinceSeq === undefined ? undefined : gt(SessionMessageTable.seq, sinceSeq), compaction ? or( gte(SessionMessageTable.seq, compaction.seq), @@ -47,12 +49,13 @@ const messageRows = Effect.fnUntraced(function* ( ), ) .orderBy(asc(SessionMessageTable.seq)) + .limit(500) .all() .pipe(Effect.orDie) return rows }) -const decodeMessageRow = (row: typeof SessionMessageTable.$inferSelect) => +const decodeMessageRow = (row: { readonly data: typeof SessionMessageTable.$inferSelect["data"]; readonly id: typeof SessionMessageTable.$inferSelect["id"]; readonly session_id: typeof SessionMessageTable.$inferSelect["session_id"]; readonly type: typeof SessionMessageTable.$inferSelect["type"] }) => decode({ ...row.data, id: row.id, type: row.type }).pipe( Effect.mapError( () => @@ -91,8 +94,9 @@ export const entriesForRunner = Effect.fn("SessionHistory.entriesForRunner")(fun db: DatabaseService, sessionID: SessionSchema.ID, baselineSeq: number, + sinceSeq?: number, ) { - const rows = yield* messageRows(db, sessionID, yield* latestCompaction(db, sessionID), baselineSeq) + const rows = yield* messageRows(db, sessionID, yield* latestCompaction(db, sessionID), baselineSeq, sinceSeq) return yield* Effect.forEach(rows, (row) => decodeMessageRow(row).pipe(Effect.map((message) => ({ seq: row.seq, message }))), ) diff --git a/packages/core/src/session/input.ts b/packages/core/src/session/input.ts index 0d8e9f2a66c8..39c19bd38395 100644 --- a/packages/core/src/session/input.ts +++ b/packages/core/src/session/input.ts @@ -315,6 +315,7 @@ export const promoteSteers = Effect.fn("SessionInput.promoteSteers")(function* ( ), ) .orderBy(asc(SessionInputTable.admitted_seq)) + .limit(100) .all() .pipe(Effect.orDie) return yield* publish(db, events, sessionID, rows) diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index e22da3be54d7..916bf072512b 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -175,6 +175,7 @@ function run(db: DatabaseService, event: SessionEvent.Event) { .from(SessionMessageTable) .where(and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "shell"))) .orderBy(desc(SessionMessageTable.seq)) + .limit(200) .all() .pipe(Effect.orDie) return rows diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 88ba79098a64..29a2b9579a82 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -7,7 +7,7 @@ import { isContextOverflowFailure, type ProviderErrorEvent, } from "@opencode-ai/llm" -import { Cause, DateTime, Effect, FiberSet, Layer, Option, Schema, Semaphore, Stream } from "effect" +import { Cause, DateTime, Effect, FiberSet, Layer, Option, Semaphore, Stream } from "effect" import { AgentV2 } from "../../agent" import { Config } from "../../config" import { Database } from "../../database/database" @@ -26,6 +26,7 @@ import { SessionCompaction } from "../compaction" import { SessionEvent } from "../event" import { SessionHistory } from "../history" import { SessionInput } from "../input" +import { SessionMessage } from "../message" import { SessionSchema } from "../schema" import { SessionStore } from "../store" import { type RunError, Service, StepLimitExceededError } from "./index" @@ -164,12 +165,12 @@ export const layer = Layer.effect( : Effect.die(defect), ) - const sameModel = Schema.toEquivalence(Schema.UndefinedOr(ModelV2.Ref)) const loadSystemContext = (agent: AgentV2.Selection) => Effect.all([systemContext.load(), skillGuidance.load(agent)], { concurrency: "unbounded" }).pipe( Effect.map(SystemContext.combine), ) + const contextCache = new Map() const runTurnAttempt = Effect.fn("SessionRunner.runTurn")(function* ( sessionID: SessionSchema.ID, promotion: SessionInput.Delivery | undefined, @@ -206,11 +207,20 @@ export const layer = Layer.effect( session.location, agent.id, ).pipe(retryAgentMismatch(undefined))) - const current = yield* getSession(sessionID) - if ((yield* agents.select(current.agent)).id !== agent.id || !sameModel(current.model, session.model)) + if ((yield* agents.select(session.agent)).id !== agent.id) return yield* Effect.die(rebuildPreparedTurn()) const model = yield* models.resolve(session) - const entries = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq) + const cached = contextCache.get(session.id) + const epoch = system.revision + let entries: { seq: number; message: SessionMessage.Message }[] + if (cached && cached.epoch === epoch) { + const deltas = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq, cached.maxSeq) + entries = deltas.length > 0 ? [...cached.entries, ...deltas] : cached.entries + } else { + entries = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq) + } + const maxSeq = entries.length > 0 ? entries[entries.length - 1]!.seq : 0 + contextCache.set(session.id, { entries, maxSeq, epoch }) const context = entries.map((entry) => entry.message) const toolMaterialization = yield* tools.materialize(agent.info?.permissions) const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts index 1f0d15c1d732..0f34dbd7ef7d 100644 --- a/packages/core/src/session/runner/to-llm-message.ts +++ b/packages/core/src/session/runner/to-llm-message.ts @@ -71,23 +71,30 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid const assistant = (message: SessionMessage.Assistant, model: Model) => { const sameModel = String(message.model.providerID) === String(model.provider) && String(message.model.id) === String(model.id) - const content = message.content.flatMap((item): ContentPart[] => { - if (item.type === "text") return [{ type: "text", text: item.text }] - if (item.type === "reasoning") - return sameModel - ? [{ type: "reasoning", text: item.text, providerMetadata: item.providerMetadata }] - : item.text.length > 0 - ? [{ type: "text", text: item.text }] - : [] - const call = toolCall(item, sameModel ? item.provider?.metadata : undefined) - const result = toolResult(item, sameModel ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined) - return item.provider?.executed === true && result ? [call, result] : [call] - }) - const results = message.content - .filter((item): item is SessionMessage.AssistantTool => item.type === "tool" && item.provider?.executed !== true) - .map((item) => toolResult(item, sameModel ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined)) - .filter((message) => message !== undefined) - .map(Message.tool) + const content: ContentPart[] = [] + const results: ReturnType[] = [] + for (let i = 0; i < message.content.length; i++) { + const item = message.content[i]! + if (item.type === "text") { + content.push({ type: "text", text: item.text }) + } else if (item.type === "reasoning") { + if (sameModel) { + content.push({ type: "reasoning", text: item.text, providerMetadata: item.providerMetadata }) + } else if (item.text.length > 0) { + content.push({ type: "text", text: item.text }) + } + } else { + const call = toolCall(item, sameModel ? item.provider?.metadata : undefined) + content.push(call) + if (item.provider?.executed === true) { + const result = toolResult(item, sameModel ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined) + if (result) content.push(result) + } else { + const result = toolResult(item, sameModel ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined) + if (result) results.push(Message.tool(result)) + } + } + } return [Message.make({ id: message.id, role: "assistant", content, metadata: message.metadata }), ...results] } diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index ca3d8e1b530d..0e2852d663d8 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -61,6 +61,9 @@ export const SessionTable = sqliteTable( index("session_project_idx").on(table.project_id), index("session_workspace_idx").on(table.workspace_id), index("session_parent_idx").on(table.parent_id), + index("session_directory_time_idx").on(table.directory, table.time_created, table.id), + index("session_workspace_time_idx").on(table.workspace_id, table.time_created, table.id), + index("session_project_time_idx").on(table.project_id, table.time_created, table.id), ], ) diff --git a/packages/core/src/session/todo.ts b/packages/core/src/session/todo.ts index 7b3c3be3f69b..2869c2b4a19a 100644 --- a/packages/core/src/session/todo.ts +++ b/packages/core/src/session/todo.ts @@ -75,6 +75,7 @@ export const layer = Layer.effect( .from(TodoTable) .where(eq(TodoTable.session_id, sessionID)) .orderBy(asc(TodoTable.position)) + .limit(200) .all() .pipe(Effect.orDie) return rows.map((row) => ({ diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index 22dee14772cd..e5c9c92f6622 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -82,7 +82,7 @@ export const StatsCommand = effectCmd({ const getAllSessions = Effect.fnUntraced(function* () { const { db } = yield* Database.Service - return (yield* db.select().from(SessionTable).all().pipe(Effect.orDie)).map((row) => Session.fromRow(row)) + return (yield* db.select().from(SessionTable).limit(1000).all().pipe(Effect.orDie)).map((row) => Session.fromRow(row)) }) const aggregateSessionStats = Effect.fn("Cli.stats.aggregate")(function* ( diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 37ff920e3365..9ea863354552 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -372,7 +372,7 @@ export const layer = Layer.effect( }) const list = Effect.fn("Project.list")(function* () { - return (yield* db.select().from(ProjectTable).all().pipe(Effect.orDie)).map(fromRow) + return (yield* db.select().from(ProjectTable).limit(200).all().pipe(Effect.orDie)).map(fromRow) }) const get = Effect.fn("Project.get")(function* (id: ProjectV2.ID) { diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 1590e0890372..7a494b9c5f17 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -500,16 +500,15 @@ export function stream(sessionID: SessionID) { }) } -export function parts(messageID: MessageID) { +export function parts(messageID: MessageID, limit?: number) { return Effect.gen(function* () { const { db } = yield* Database.Service - const rows = yield* db + const query = db .select() .from(PartTable) .where(eq(PartTable.message_id, messageID)) .orderBy(PartTable.id) - .all() - .pipe(Effect.orDie) + const rows = yield* (limit === undefined ? query.all() : query.limit(limit).all()).pipe(Effect.orDie) return rows.map(part) }) } diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 34e2c4c62259..e04cb10bca92 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -516,11 +516,11 @@ export const layer = Layer.effect( : value.providerMetadata, })) - const parts = yield* MessageV2.parts(ctx.assistantMessage.id).pipe( + const recentParts = yield* MessageV2.parts(ctx.assistantMessage.id, DOOM_LOOP_THRESHOLD).pipe( Effect.provideService(Database.Service, database), ) - const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD) + const inputKey = JSON.stringify(input) if ( recentParts.length !== DOOM_LOOP_THRESHOLD || !recentParts.every( @@ -528,7 +528,7 @@ export const layer = Layer.effect( part.type === "tool" && part.tool === value.name && part.state.status !== "pending" && - JSON.stringify(part.state.input) === JSON.stringify(input), + JSON.stringify(part.state.input) === inputKey, ) ) { return diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 56ca82e3fcc0..57618850109e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -127,6 +127,9 @@ export const layer = Layer.effect( const flags = yield* RuntimeFlags.Service const database = yield* Database.Service const { db } = database + const visibleAgentNames = Effect.fnUntraced(function* () { + return (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) + }) const ops = Effect.fn("SessionPrompt.ops")(function* () { return { cancel: (sessionID: SessionID) => cancel(sessionID), @@ -340,7 +343,7 @@ export const layer = Layer.effect( const taskAgent = yield* agents.get(task.agent) if (!taskAgent) { - const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) + const available = yield* visibleAgentNames() const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" const error = new NamedError.Unknown({ message: `Agent not found: "${task.agent}".${hint}` }) yield* events.publish(Session.Event.Error, { sessionID, error: error.toObject() }) @@ -488,7 +491,7 @@ export const layer = Layer.effect( } const agent = yield* agents.get(input.agent) if (!agent) { - const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) + const available = yield* visibleAgentNames() const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" const error = new NamedError.Unknown({ message: `Agent not found: "${input.agent}".${hint}` }) yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() }) @@ -681,7 +684,7 @@ export const layer = Layer.effect( const agentName = input.agent const ag = agentName ? yield* agents.get(agentName) : yield* agents.defaultInfo() if (!ag) { - const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) + const available = yield* visibleAgentNames() const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` }) yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() }) @@ -1303,7 +1306,7 @@ export const layer = Layer.effect( const agent = yield* agents.get(lastUser.agent) if (!agent) { - const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) + const available = yield* visibleAgentNames() const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" const error = new NamedError.Unknown({ message: `Agent not found: "${lastUser.agent}".${hint}` }) yield* events.publish(Session.Event.Error, { sessionID, error: error.toObject() }) @@ -1552,7 +1555,7 @@ export const layer = Layer.effect( const agent = agentName ? yield* agents.get(agentName) : yield* agents.defaultInfo() if (!agent) { - const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) + const available = yield* visibleAgentNames() const hint = available.length ? ` Available agents: ${available.join(", ")}` : "" const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` }) yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() }) diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 7abbba0b838c..b43288d7ea2a 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -677,7 +677,7 @@ export const layer: Layer.Layer< Effect.gen(function* () { yield* events.publish(SessionV1.Event.PartUpdated, { sessionID: part.sessionID, - part: structuredClone(part), + part: JSON.parse(JSON.stringify(part)), time: Date.now(), }) return part