Initial import: live state on api.qbirr.com (server v0.6.3)

This commit is contained in:
2026-05-26 16:06:29 +02:00
commit 7ba4cb4a31
38 changed files with 5242 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
/**
* 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];
}