feat: add session_messages table for conversation history
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s

Add a session_messages table that stores the full user/assistant
conversation history within a session. Each session request now
persists both the user message and assistant response atomically
alongside the session_request and balance update.

- New schema: session_messages (id, session_id, role, content,
  session_request_id, created_at) with index on session_id
- New migration: 0008_session_messages.sql
- New endpoint: GET /sessions/:id/messages (macaroon-authed)
- Messages inserted transactionally during POST /sessions/:id/request

Fixes #37

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-03-22 21:51:55 -04:00
parent 4c747aa331
commit b4aa672c58
4 changed files with 105 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
import { Router, type Request, type Response } from "express";
import { randomBytes, randomUUID, createHash } from "crypto";
import { db, sessions, sessionRequests, type Session } from "@workspace/db";
import { db, sessions, sessionRequests, sessionMessages, type Session } from "@workspace/db";
import { eq, and } from "drizzle-orm";
import { lnbitsService } from "../lib/lnbits.js";
import { sessionsLimiter } from "../lib/rate-limiter.js";
@@ -251,6 +251,42 @@ router.get("/sessions/:id", async (req: Request, res: Response) => {
}
});
// ── GET /sessions/:id/messages ────────────────────────────────────────────────
router.get("/sessions/:id/messages", async (req: Request, res: Response) => {
const id = req.params.id as string;
const macaroon = extractMacaroon(req);
try {
const session = await getSessionById(id);
if (!session) { res.status(404).json({ error: "Session not found" }); return; }
if (!macaroon || macaroon !== session.macaroon) {
res.status(401).json({ error: "Invalid or missing macaroon" });
return;
}
const rows = await db
.select()
.from(sessionMessages)
.where(eq(sessionMessages.sessionId, id))
.orderBy(sessionMessages.id);
res.json({
sessionId: id,
messages: rows.map((m) => ({
id: m.id,
role: m.role,
content: m.content,
sessionRequestId: m.sessionRequestId,
createdAt: m.createdAt.toISOString(),
})),
});
} catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : "Failed to fetch messages" });
}
});
// ── POST /sessions/:id/request ────────────────────────────────────────────────
router.post("/sessions/:id/request", async (req: Request, res: Response) => {
@@ -424,7 +460,7 @@ router.post("/sessions/:id/request", async (req: Request, res: Response) => {
const newSessionState = newBalance < MIN_BALANCE_SATS ? "paused" : "active";
const expiresAt = new Date(Date.now() + EXPIRY_MS);
// Persist session request + update session balance atomically
// Persist session request + messages + update session balance atomically
await db.transaction(async (tx) => {
await tx.insert(sessionRequests).values({
id: requestId,
@@ -443,6 +479,24 @@ router.post("/sessions/:id/request", async (req: Request, res: Response) => {
btcPriceUsd,
});
// Store conversation messages
await tx.insert(sessionMessages).values({
sessionId: id,
role: "user",
content: requestText,
sessionRequestId: requestId,
});
const assistantContent = result ?? reason ?? errorMessage ?? "";
if (assistantContent) {
await tx.insert(sessionMessages).values({
sessionId: id,
role: "assistant",
content: assistantContent,
sessionRequestId: requestId,
});
}
await tx
.update(sessions)
.set({