mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-27 11:42:52 +00:00
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:
Generated
+1077
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user