44 lines
1.9 KiB
TypeScript
44 lines
1.9 KiB
TypeScript
/**
|
|
* Server-side conversation memory. Persists each run's user goal + final
|
|
* assistant answer per session, and recalls the recent turns so a run has
|
|
* context even when the plugin sends little/no history. Independent of the
|
|
* plugin's own carry-forward.
|
|
*/
|
|
|
|
import { getDb } from '../db/pool.js';
|
|
import { newId } from '../lib/crypto.js';
|
|
import type { RunContextMessage } from './types.js';
|
|
|
|
const MAX_RECALL = 12; // turns to prepend
|
|
const MAX_CONTENT = 4000; // clip very long turns
|
|
|
|
export function saveTurn(session_id: string, role: 'user' | 'assistant', content: string, user_id?: string): void {
|
|
if (!session_id || !content) return;
|
|
getDb().prepare(
|
|
'INSERT INTO conversation_turns (id, session_id, user_id, role, content) VALUES (?,?,?,?,?)',
|
|
).run(newId(), session_id, user_id ?? null, role, content.slice(0, MAX_CONTENT));
|
|
}
|
|
|
|
export function recallTurns(session_id: string, limit = MAX_RECALL): RunContextMessage[] {
|
|
if (!session_id) return [];
|
|
const rows = getDb().prepare(
|
|
'SELECT role, content FROM conversation_turns WHERE session_id = ? ORDER BY created_at DESC LIMIT ?',
|
|
).all(session_id, limit) as { role: string; content: string }[];
|
|
return rows
|
|
.reverse()
|
|
.map((r) => ({ role: r.role === 'assistant' ? 'assistant' : 'user', content: r.content }) as RunContextMessage);
|
|
}
|
|
|
|
/**
|
|
* Merge recalled memory with the context the plugin sent, de-duplicating
|
|
* so we don't double-feed turns the plugin already included. Memory is
|
|
* prepended (older), then the plugin's context (newer).
|
|
*/
|
|
export function mergeContext(session_id: string, pluginContext: RunContextMessage[]): RunContextMessage[] {
|
|
const recalled = recallTurns(session_id);
|
|
if (recalled.length === 0) return pluginContext;
|
|
const seen = new Set(pluginContext.map((m) => `${m.role}:${m.content.slice(0, 80)}`));
|
|
const fromMemory = recalled.filter((m) => !seen.has(`${m.role}:${m.content.slice(0, 80)}`));
|
|
return [...fromMemory, ...pluginContext];
|
|
}
|