Phase 2a: Drizzle + PGlite database foundation

Add Drizzle schema for all four tables (projects, workspaces,
agent_snapshots, agent_timeline_rows), PGlite bootstrap lifecycle,
migration harness, and real contract tests proving query shapes work.

No runtime authority changes — Session, AgentManager, and existing
storage call sites are untouched. CLI cleanup removes stale projection
fields from two fetchAgentTimeline call sites.

Audit-verified: correctness (all acceptance criteria met), boundary
(no runtime files touched), complexity (minor flags resolved).
This commit is contained in:
Mohamed Boudra
2026-03-25 20:39:07 +07:00
parent acda34fc70
commit 214f85a150
12 changed files with 1898 additions and 2 deletions
+1077
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -193,7 +193,6 @@ export async function resolveStructuredResponseMessage(options: {
try {
const timeline = await options.client.fetchAgentTimeline(options.agentId, {
direction: "tail",
projection: "projected",
limit: 200,
});
for (let index = timeline.entries.length - 1; index >= 0; index -= 1) {
-1
View File
@@ -11,7 +11,6 @@ export async function fetchProjectedTimelineItems(
const timeline = await input.client.fetchAgentTimeline(input.agentId, {
direction: "tail",
limit: 0,
projection: "projected",
});
return timeline.entries.map((entry) => entry.item);
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./packages/server/src/server/db/schema.ts",
out: "./packages/server/src/server/db/migrations",
dialect: "postgresql",
strict: true,
verbose: true,
});
+3
View File
@@ -64,6 +64,7 @@
"@ai-sdk/openai": "2.0.52",
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
"@deepgram/sdk": "^3.4.0",
"@electric-sql/pglite": "^0.4.1",
"@getpaseo/highlight": "0.1.33",
"@getpaseo/relay": "0.1.33",
"@modelcontextprotocol/sdk": "^1.20.1",
@@ -73,6 +74,7 @@
"ai": "5.0.78",
"ajv": "^8.17.1",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.45.1",
"express": "^4.18.2",
"express-basic-auth": "^1.2.1",
"fast-uri": "^3.1.0",
@@ -96,6 +98,7 @@
},
"devDependencies": {
"@playwright/test": "^1.56.1",
"drizzle-kit": "^0.31.10",
"@types/express": "^4.17.20",
"@types/node": "^20.9.0",
"@types/qrcode": "^1.5.6",
@@ -0,0 +1,11 @@
import { fileURLToPath } from "node:url";
import type { PgliteDatabase } from "drizzle-orm/pglite";
import { migrate } from "drizzle-orm/pglite/migrator";
const migrationsFolder = fileURLToPath(new URL("./migrations", import.meta.url));
export async function runPaseoDbMigrations(db: PgliteDatabase<typeof import("./schema.js").paseoDbSchema>): Promise<void> {
await migrate(db, { migrationsFolder });
}
@@ -0,0 +1,56 @@
CREATE TABLE "agent_snapshots" (
"agent_id" text PRIMARY KEY NOT NULL,
"provider" text NOT NULL,
"workspace_id" text,
"cwd" text NOT NULL,
"created_at" text NOT NULL,
"updated_at" text NOT NULL,
"last_activity_at" text,
"last_user_message_at" text,
"title" text,
"labels" jsonb NOT NULL,
"last_status" text NOT NULL,
"last_mode_id" text,
"config" jsonb,
"runtime_info" jsonb,
"persistence" jsonb,
"requires_attention" boolean NOT NULL,
"attention_reason" text,
"attention_timestamp" text,
"internal" boolean NOT NULL,
"archived_at" text
);
--> statement-breakpoint
CREATE TABLE "agent_timeline_rows" (
"agent_id" text NOT NULL,
"seq" integer NOT NULL,
"committed_at" text NOT NULL,
"item" jsonb NOT NULL,
"item_kind" text,
CONSTRAINT "agent_timeline_rows_pk" PRIMARY KEY("agent_id","seq")
);
--> statement-breakpoint
CREATE TABLE "projects" (
"project_id" text PRIMARY KEY NOT NULL,
"root_path" text NOT NULL,
"kind" text NOT NULL,
"display_name" text NOT NULL,
"created_at" text NOT NULL,
"updated_at" text NOT NULL,
"archived_at" text
);
--> statement-breakpoint
CREATE TABLE "workspaces" (
"workspace_id" text PRIMARY KEY NOT NULL,
"project_id" text NOT NULL,
"cwd" text NOT NULL,
"kind" text NOT NULL,
"display_name" text NOT NULL,
"created_at" text NOT NULL,
"updated_at" text NOT NULL,
"archived_at" text
);
--> statement-breakpoint
ALTER TABLE "agent_snapshots" ADD CONSTRAINT "agent_snapshots_workspace_id_workspaces_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("workspace_id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE "workspaces" ADD CONSTRAINT "workspaces_project_id_projects_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("project_id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint
CREATE INDEX "workspaces_project_id_idx" ON "workspaces" USING btree ("project_id");
@@ -0,0 +1,363 @@
{
"id": "5940c904-fd8b-4276-953d-acc382bf3fb8",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.agent_snapshots": {
"name": "agent_snapshots",
"schema": "",
"columns": {
"agent_id": {
"name": "agent_id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"cwd": {
"name": "cwd",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_activity_at": {
"name": "last_activity_at",
"type": "text",
"primaryKey": false,
"notNull": false
},
"last_user_message_at": {
"name": "last_user_message_at",
"type": "text",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false
},
"labels": {
"name": "labels",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"last_status": {
"name": "last_status",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_mode_id": {
"name": "last_mode_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"config": {
"name": "config",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"runtime_info": {
"name": "runtime_info",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"persistence": {
"name": "persistence",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"requires_attention": {
"name": "requires_attention",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"attention_reason": {
"name": "attention_reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"attention_timestamp": {
"name": "attention_timestamp",
"type": "text",
"primaryKey": false,
"notNull": false
},
"internal": {
"name": "internal",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"archived_at": {
"name": "archived_at",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"agent_snapshots_workspace_id_workspaces_workspace_id_fk": {
"name": "agent_snapshots_workspace_id_workspaces_workspace_id_fk",
"tableFrom": "agent_snapshots",
"tableTo": "workspaces",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"workspace_id"
],
"onDelete": "set null",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_timeline_rows": {
"name": "agent_timeline_rows",
"schema": "",
"columns": {
"agent_id": {
"name": "agent_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"seq": {
"name": "seq",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"committed_at": {
"name": "committed_at",
"type": "text",
"primaryKey": false,
"notNull": true
},
"item": {
"name": "item",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"item_kind": {
"name": "item_kind",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"agent_timeline_rows_pk": {
"name": "agent_timeline_rows_pk",
"columns": [
"agent_id",
"seq"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.projects": {
"name": "projects",
"schema": "",
"columns": {
"project_id": {
"name": "project_id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"root_path": {
"name": "root_path",
"type": "text",
"primaryKey": false,
"notNull": true
},
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true
},
"display_name": {
"name": "display_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true
},
"archived_at": {
"name": "archived_at",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspaces": {
"name": "workspaces",
"schema": "",
"columns": {
"workspace_id": {
"name": "workspace_id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"project_id": {
"name": "project_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"cwd": {
"name": "cwd",
"type": "text",
"primaryKey": false,
"notNull": true
},
"kind": {
"name": "kind",
"type": "text",
"primaryKey": false,
"notNull": true
},
"display_name": {
"name": "display_name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true
},
"archived_at": {
"name": "archived_at",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"workspaces_project_id_idx": {
"name": "workspaces_project_id_idx",
"columns": [
{
"expression": "project_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"workspaces_project_id_projects_project_id_fk": {
"name": "workspaces_project_id_projects_project_id_fk",
"tableFrom": "workspaces",
"tableTo": "projects",
"columnsFrom": [
"project_id"
],
"columnsTo": [
"project_id"
],
"onDelete": "restrict",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1774405361702,
"tag": "0000_youthful_lady_mastermind",
"breakpoints": true
}
]
}
@@ -0,0 +1,264 @@
import os from "node:os";
import path from "node:path";
import { mkdtempSync, rmSync } from "node:fs";
import { and, asc, desc, eq, gt, lt, sql } from "drizzle-orm";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import type { AgentTimelineItem } from "../agent/agent-sdk-types.js";
import { openPaseoDatabase } from "./pglite-database.js";
import { runPaseoDbMigrations } from "./migrations.js";
import {
agentSnapshots,
agentTimelineRows,
projects,
workspaces,
} from "./schema.js";
function createTimestamp(day: number): string {
return `2026-03-${String(day).padStart(2, "0")}T00:00:00.000Z`;
}
function createTimelineItem(type: AgentTimelineItem["type"], suffix: string): AgentTimelineItem {
if (type === "user_message") {
return { type, text: `user-${suffix}`, messageId: `msg-${suffix}` };
}
if (type === "assistant_message" || type === "reasoning") {
return { type, text: `${type}-${suffix}` };
}
return { type: "error", message: `error-${suffix}` };
}
describe("PGlite database contract", () => {
let tmpDir: string;
let dataDir: string;
beforeEach(() => {
tmpDir = mkdtempSync(path.join(os.tmpdir(), "paseo-db-"));
dataDir = path.join(tmpDir, "db");
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
test("creates, migrates, closes, and reopens a persistent database", async () => {
const database = await openPaseoDatabase(dataDir);
await database.db.insert(projects).values({
projectId: "project-1",
rootPath: "/tmp/project-1",
kind: "git",
displayName: "Project One",
createdAt: createTimestamp(1),
updatedAt: createTimestamp(1),
archivedAt: null,
});
await database.close();
const reopened = await openPaseoDatabase(dataDir);
const rows = await reopened.db.select().from(projects);
expect(rows).toEqual([
{
projectId: "project-1",
rootPath: "/tmp/project-1",
kind: "git",
displayName: "Project One",
createdAt: createTimestamp(1),
updatedAt: createTimestamp(1),
archivedAt: null,
},
]);
await reopened.close();
});
test("supports project and workspace linkage plus archive field updates", async () => {
const database = await openPaseoDatabase(dataDir);
await database.db.insert(projects).values({
projectId: "project-1",
rootPath: "/tmp/project-1",
kind: "git",
displayName: "Project One",
createdAt: createTimestamp(1),
updatedAt: createTimestamp(1),
archivedAt: null,
});
await database.db.insert(workspaces).values({
workspaceId: "workspace-1",
projectId: "project-1",
cwd: "/tmp/project-1",
kind: "local_checkout",
displayName: "main",
createdAt: createTimestamp(1),
updatedAt: createTimestamp(1),
archivedAt: null,
});
await database.db
.update(workspaces)
.set({ archivedAt: createTimestamp(2), updatedAt: createTimestamp(2) })
.where(eq(workspaces.workspaceId, "workspace-1"));
const linkedRows = await database.db
.select({
projectId: projects.projectId,
workspaceId: workspaces.workspaceId,
workspaceArchivedAt: workspaces.archivedAt,
})
.from(workspaces)
.innerJoin(projects, eq(workspaces.projectId, projects.projectId));
expect(linkedRows).toEqual([
{
projectId: "project-1",
workspaceId: "workspace-1",
workspaceArchivedAt: createTimestamp(2),
},
]);
await database.close();
});
test("supports snapshot insert, get, and update with the current persisted metadata shape", async () => {
const database = await openPaseoDatabase(dataDir);
await database.db.insert(agentSnapshots).values({
agentId: "agent-1",
provider: "codex",
workspaceId: null,
cwd: "/tmp/project-1",
createdAt: createTimestamp(1),
updatedAt: createTimestamp(1),
lastActivityAt: createTimestamp(1),
lastUserMessageAt: null,
title: "Agent One",
labels: { surface: "workspace" },
lastStatus: "idle",
lastModeId: "plan",
config: { model: "gpt-5.1", modeId: "plan" },
runtimeInfo: { provider: "codex", sessionId: "session-1" },
persistence: { provider: "codex", sessionId: "session-1" },
requiresAttention: false,
attentionReason: null,
attentionTimestamp: null,
internal: false,
archivedAt: null,
});
await database.db
.update(agentSnapshots)
.set({
updatedAt: createTimestamp(2),
lastStatus: "running",
title: "Agent One Updated",
archivedAt: createTimestamp(3),
})
.where(eq(agentSnapshots.agentId, "agent-1"));
const rows = await database.db
.select()
.from(agentSnapshots)
.where(eq(agentSnapshots.agentId, "agent-1"));
expect(rows).toEqual([
{
agentId: "agent-1",
provider: "codex",
workspaceId: null,
cwd: "/tmp/project-1",
createdAt: createTimestamp(1),
updatedAt: createTimestamp(2),
lastActivityAt: createTimestamp(1),
lastUserMessageAt: null,
title: "Agent One Updated",
labels: { surface: "workspace" },
lastStatus: "running",
lastModeId: "plan",
config: { model: "gpt-5.1", modeId: "plan" },
runtimeInfo: { provider: "codex", sessionId: "session-1" },
persistence: { provider: "codex", sessionId: "session-1" },
requiresAttention: false,
attentionReason: null,
attentionTimestamp: null,
internal: false,
archivedAt: createTimestamp(3),
},
]);
await database.close();
});
test("supports timeline append and tail, after-seq, before-seq access patterns in committed order", async () => {
const database = await openPaseoDatabase(dataDir);
const rows = [1, 2, 3, 4].map((seq) => ({
agentId: "agent-1",
seq,
committedAt: createTimestamp(seq),
item: createTimelineItem(seq === 1 ? "user_message" : "assistant_message", String(seq)),
itemKind: seq === 1 ? "user_message" : "assistant_message",
}));
await database.db.insert(agentTimelineRows).values(rows);
const tailRows = await database.db
.select()
.from(agentTimelineRows)
.where(eq(agentTimelineRows.agentId, "agent-1"))
.orderBy(desc(agentTimelineRows.seq))
.limit(2);
expect(tailRows.map((row) => row.seq).reverse()).toEqual([3, 4]);
const afterRows = await database.db
.select()
.from(agentTimelineRows)
.where(and(eq(agentTimelineRows.agentId, "agent-1"), gt(agentTimelineRows.seq, 2)))
.orderBy(asc(agentTimelineRows.seq));
expect(afterRows.map((row) => row.seq)).toEqual([3, 4]);
const beforeRows = await database.db
.select()
.from(agentTimelineRows)
.where(and(eq(agentTimelineRows.agentId, "agent-1"), lt(agentTimelineRows.seq, 4)))
.orderBy(desc(agentTimelineRows.seq))
.limit(2);
expect(beforeRows.map((row) => row.seq).reverse()).toEqual([2, 3]);
await database.close();
});
test("enforces per-agent seq uniqueness and reruns migrations without drift", async () => {
const database = await openPaseoDatabase(dataDir);
await database.db.insert(agentTimelineRows).values({
agentId: "agent-1",
seq: 1,
committedAt: createTimestamp(1),
item: createTimelineItem("assistant_message", "1"),
itemKind: "assistant_message",
});
await expect(
database.db.insert(agentTimelineRows).values({
agentId: "agent-1",
seq: 1,
committedAt: createTimestamp(2),
item: createTimelineItem("assistant_message", "duplicate"),
itemKind: "assistant_message",
}),
).rejects.toThrow();
await runPaseoDbMigrations(database.db);
const migrationRows = await database.db.execute(
sql`select * from drizzle.__drizzle_migrations order by created_at`,
);
expect(migrationRows.rows).toHaveLength(1);
await database.close();
});
});
@@ -0,0 +1,24 @@
import { PGlite } from "@electric-sql/pglite";
import { drizzle } from "drizzle-orm/pglite";
import { runPaseoDbMigrations } from "./migrations.js";
import { paseoDbSchema } from "./schema.js";
export interface PaseoDatabaseHandle {
client: PGlite;
db: ReturnType<typeof drizzle<typeof paseoDbSchema>>;
close(): Promise<void>;
}
export async function openPaseoDatabase(dataDir: string): Promise<PaseoDatabaseHandle> {
const client = new PGlite(dataDir);
const db = drizzle(client, { schema: paseoDbSchema });
await runPaseoDbMigrations(db);
return {
client,
db,
async close(): Promise<void> {
await client.close();
},
};
}
+78
View File
@@ -0,0 +1,78 @@
import { boolean, index, integer, jsonb, pgTable, primaryKey, text } from "drizzle-orm/pg-core";
import type { AgentPersistenceHandle, AgentRuntimeInfo, AgentTimelineItem } from "../agent/agent-sdk-types.js";
import type { StoredAgentRecord } from "../agent/agent-storage.js";
export const projects = pgTable("projects", {
projectId: text("project_id").primaryKey(),
rootPath: text("root_path").notNull(),
kind: text("kind").notNull(),
displayName: text("display_name").notNull(),
createdAt: text("created_at").notNull(),
updatedAt: text("updated_at").notNull(),
archivedAt: text("archived_at"),
});
export const workspaces = pgTable(
"workspaces",
{
workspaceId: text("workspace_id").primaryKey(),
projectId: text("project_id")
.notNull()
.references(() => projects.projectId, { onDelete: "restrict", onUpdate: "cascade" }),
cwd: text("cwd").notNull(),
kind: text("kind").notNull(),
displayName: text("display_name").notNull(),
createdAt: text("created_at").notNull(),
updatedAt: text("updated_at").notNull(),
archivedAt: text("archived_at"),
},
(table) => [index("workspaces_project_id_idx").on(table.projectId)],
);
export const agentSnapshots = pgTable("agent_snapshots", {
agentId: text("agent_id").primaryKey(),
provider: text("provider").notNull(),
workspaceId: text("workspace_id").references(() => workspaces.workspaceId, {
onDelete: "set null",
onUpdate: "cascade",
}),
cwd: text("cwd").notNull(),
createdAt: text("created_at").notNull(),
updatedAt: text("updated_at").notNull(),
lastActivityAt: text("last_activity_at"),
lastUserMessageAt: text("last_user_message_at"),
title: text("title"),
labels: jsonb("labels").$type<StoredAgentRecord["labels"]>().notNull(),
lastStatus: text("last_status").notNull(),
lastModeId: text("last_mode_id"),
config: jsonb("config").$type<StoredAgentRecord["config"]>(),
runtimeInfo: jsonb("runtime_info").$type<AgentRuntimeInfo>(),
persistence: jsonb("persistence").$type<AgentPersistenceHandle>(),
requiresAttention: boolean("requires_attention").notNull(),
attentionReason: text("attention_reason"),
attentionTimestamp: text("attention_timestamp"),
internal: boolean("internal").notNull(),
archivedAt: text("archived_at"),
});
export const agentTimelineRows = pgTable(
"agent_timeline_rows",
{
agentId: text("agent_id").notNull(),
seq: integer("seq").notNull(),
committedAt: text("committed_at").notNull(),
item: jsonb("item").$type<AgentTimelineItem>().notNull(),
itemKind: text("item_kind"),
},
(table) => [
primaryKey({ columns: [table.agentId, table.seq], name: "agent_timeline_rows_pk" }),
],
);
export const paseoDbSchema = {
projects,
workspaces,
agentSnapshots,
agentTimelineRows,
};