server: reconcile ai_sessions persistence to Chunk 9 AISession shape

Reshape the ai_sessions table (migration 003) and mapping/routes to the
full AISession contract: title, kind, notes, created_at, last_message_at;
drop legacy role/model/settings. toAISession now maps the new columns and
routes create/patch/messages handlers use the new shape.
This commit is contained in:
Schwitzz
2026-10-29 15:03:32 -05:00
parent fc8eaa86c3
commit a1b5587fcb
15 changed files with 181 additions and 60 deletions
+12 -7
View File
@@ -1,6 +1,5 @@
import type {
AISession,
AISessionRole,
AuditLogEntry,
Board,
Book,
@@ -11,6 +10,7 @@ import type {
PlotBeat,
Project,
ProjectMeta,
SessionKind,
User,
WorldItem,
} from "@pubscript/core";
@@ -98,12 +98,14 @@ export interface PlotBeatRow {
export interface AISessionRow {
id: string;
project_id: string;
role: string;
model: string;
settings: string;
title: string;
kind: string;
messages: string;
suggestions: string;
notes: string;
created_at: string;
updated_at: string;
last_message_at: string | null;
}
// ---- Row -> core types ----
@@ -235,11 +237,14 @@ export function toAISession(r: AISessionRow): AISession {
return {
id: r.id,
projectId: r.project_id,
role: r.role as AISessionRole,
model: r.model,
settings: JSON.parse(r.settings),
title: r.title,
kind: r.kind as SessionKind,
messages: JSON.parse(r.messages),
suggestions: JSON.parse(r.suggestions),
notes: JSON.parse(r.notes),
createdAt: r.created_at,
updatedAt: r.updated_at,
lastMessageAt: r.last_message_at,
};
}
+24
View File
@@ -0,0 +1,24 @@
-- Chunk #9: reshape ai_sessions to the full AISession shape.
-- Adds title, kind, notes, created_at, last_message_at; drops role, model, settings.
CREATE TABLE ai_sessions_new (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
title TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL DEFAULT 'session',
messages TEXT NOT NULL DEFAULT '[]',
suggestions TEXT NOT NULL DEFAULT '[]',
notes TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
last_message_at TEXT
);
INSERT INTO ai_sessions_new (id, project_id, title, kind, messages, suggestions, notes, created_at, updated_at, last_message_at)
SELECT id, project_id, '', 'session', messages, suggestions, '[]', updated_at, updated_at, NULL
FROM ai_sessions;
DROP TABLE ai_sessions;
ALTER TABLE ai_sessions_new RENAME TO ai_sessions;
CREATE INDEX IF NOT EXISTS idx_ai_sessions_project ON ai_sessions(project_id);
+35 -17
View File
@@ -92,11 +92,14 @@ const beatCreate = z.object({
});
const aiSessionCreate = z.object({
id: z.string().optional(),
role: z.string().min(1),
model: z.string().min(1),
settings: z.record(z.unknown()).default({}),
messages: z.array(z.any()).optional(),
suggestions: z.array(z.any()).optional(),
title: z.string().default(""),
kind: z.enum(["session", "agent"]).default("session"),
messages: z.array(z.any()).default([]),
suggestions: z.array(z.any()).default([]),
notes: z.array(z.any()).default([]),
createdAt: z.string().optional(),
updatedAt: z.string().optional(),
lastMessageAt: z.string().nullable().optional(),
});
const aiMessageAppend = z.object({ role: z.string(), content: z.string() });
const aiSuggestionAdd = z.object({ kind: z.string(), text: z.string() });
@@ -565,14 +568,15 @@ export function registerRoutes(app: FastifyInstance): void {
const sid = body.id ?? ulid();
const t = now();
db.prepare(
`INSERT INTO ai_sessions (id, project_id, role, model, settings, messages, suggestions, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`INSERT INTO ai_sessions (id, project_id, title, kind, messages, suggestions, notes, created_at, updated_at, last_message_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
project_id = excluded.project_id, role = excluded.role, model = excluded.model,
settings = excluded.settings, messages = excluded.messages, suggestions = excluded.suggestions,
updated_at = excluded.updated_at`,
).run(sid, id, body.role, body.model, JSON.stringify(body.settings),
JSON.stringify(body.messages ?? []), JSON.stringify(body.suggestions ?? []), t);
title = excluded.title, kind = excluded.kind,
messages = excluded.messages, suggestions = excluded.suggestions, notes = excluded.notes,
updated_at = excluded.updated_at, last_message_at = excluded.last_message_at`,
).run(sid, id, body.title, body.kind, JSON.stringify(body.messages),
JSON.stringify(body.suggestions), JSON.stringify(body.notes),
body.createdAt ?? t, t, body.lastMessageAt ?? null);
const row = db.prepare("SELECT * FROM ai_sessions WHERE id = ?").get(sid) as AISessionRow;
return { session: toAISession(row) };
});
@@ -590,11 +594,25 @@ export function registerRoutes(app: FastifyInstance): void {
const row = db.prepare("SELECT * FROM ai_sessions WHERE id = ?").get(id) as AISessionRow | undefined;
if (!row) throw app.httpErrors.create(404, { error: "not_found" });
requireProjectAccess(app, req, row.project_id, true);
const body = z.object({ settings: z.record(z.unknown()).optional(), model: z.string().optional() }).parse(req.body);
const settings = body.settings ? JSON.stringify(body.settings) : row.settings;
const model = body.model ?? row.model;
const body = z.object({
title: z.string().optional(),
kind: z.enum(["session", "agent"]).optional(),
messages: z.array(z.any()).optional(),
suggestions: z.array(z.any()).optional(),
notes: z.array(z.any()).optional(),
lastMessageAt: z.string().nullable().optional(),
}).parse(req.body);
const t = now();
db.prepare("UPDATE ai_sessions SET settings = ?, model = ?, updated_at = ? WHERE id = ?").run(settings, model, t, id);
db.prepare(
`UPDATE ai_sessions SET title = ?, kind = ?, messages = ?, suggestions = ?, notes = ?, last_message_at = ?, updated_at = ? WHERE id = ?`
).run(
body.title ?? row.title, body.kind ?? row.kind,
JSON.stringify(body.messages ?? JSON.parse(row.messages)),
JSON.stringify(body.suggestions ?? JSON.parse(row.suggestions)),
JSON.stringify(body.notes ?? JSON.parse(row.notes)),
body.lastMessageAt ?? row.last_message_at,
t, id,
);
const updated = db.prepare("SELECT * FROM ai_sessions WHERE id = ?").get(id) as AISessionRow;
return { session: toAISession(updated) };
});
@@ -617,7 +635,7 @@ export function registerRoutes(app: FastifyInstance): void {
const messages = JSON.parse(row.messages) as any[];
messages.push({ role, content, at: now() });
const t = now();
db.prepare("UPDATE ai_sessions SET messages = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(messages), t, id);
db.prepare("UPDATE ai_sessions SET messages = ?, last_message_at = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(messages), t, t, id);
const updated = db.prepare("SELECT * FROM ai_sessions WHERE id = ?").get(id) as AISessionRow;
return { session: toAISession(updated) };
});