feat: inject conversation history into session work model
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 0s

- Add session_messages table and migration for storing conversation turns
- Add getSessionHistory() helper to load recent history with token budget
- Pass conversation history to executeWork() and executeWorkStreaming()
- Persist user/assistant exchanges after completed requests
- Rejected/failed requests do not pollute history

Fixes #39

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

View File

@@ -0,0 +1,13 @@
-- Migration: Session conversation history (#38/#39)
-- Stores user/assistant message pairs for context injection into the work model.
CREATE TABLE IF NOT EXISTS session_messages (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id),
role TEXT NOT NULL,
content TEXT NOT NULL,
token_count INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_session_messages_session_id ON session_messages(session_id);

View File

@@ -1,4 +1,5 @@
import { drizzle } from "drizzle-orm/node-postgres";
import { eq, asc } from "drizzle-orm";
import pg from "pg";
import * as schema from "./schema";
@@ -14,3 +15,46 @@ export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });
export * from "./schema";
// ── Session history helper ──────────────────────────────────────────────────
/**
* Load the most recent conversation history for a session, capped by turn
* count and approximate token budget.
*
* @param sessionId Session to load history for
* @param maxTurns Maximum number of messages to return (default 8)
* @param maxTokens Approximate token budget — stops including older messages
* once cumulative token_count exceeds this (default 4000)
* @returns Array of { role, content } objects in chronological order
*/
export async function getSessionHistory(
sessionId: string,
maxTurns = 8,
maxTokens = 4000,
): Promise<Array<{ role: "user" | "assistant"; content: string }>> {
const rows = await db
.select({
role: schema.sessionMessages.role,
content: schema.sessionMessages.content,
tokenCount: schema.sessionMessages.tokenCount,
})
.from(schema.sessionMessages)
.where(eq(schema.sessionMessages.sessionId, sessionId))
.orderBy(asc(schema.sessionMessages.id));
// Take the most recent messages that fit within budget
const result: Array<{ role: "user" | "assistant"; content: string }> = [];
let totalTokens = 0;
// Walk from newest to oldest, then reverse
for (let i = rows.length - 1; i >= 0 && result.length < maxTurns; i--) {
const row = rows[i]!;
const tokens = row.tokenCount ?? Math.ceil(row.content.length / 4);
if (totalTokens + tokens > maxTokens && result.length > 0) break;
totalTokens += tokens;
result.push({ role: row.role as "user" | "assistant", content: row.content });
}
return result.reverse();
}

View File

@@ -13,3 +13,4 @@ export * from "./nostr-trust-vouches";
export * from "./relay-accounts";
export * from "./relay-event-queue";
export * from "./job-debates";
export * from "./session-messages";

View File

@@ -0,0 +1,18 @@
import { pgTable, text, timestamp, integer, serial } from "drizzle-orm/pg-core";
import { sessions } from "./sessions";
// ── session_messages ────────────────────────────────────────────────────────
// Stores conversation history for context injection into the work model.
export const sessionMessages = pgTable("session_messages", {
id: serial("id").primaryKey(),
sessionId: text("session_id")
.notNull()
.references(() => sessions.id),
role: text("role").$type<"user" | "assistant">().notNull(),
content: text("content").notNull(),
tokenCount: integer("token_count"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
export type SessionMessage = typeof sessionMessages.$inferSelect;