feat(session): persist model tier overrides per session

Store per-session config in SQLite and route /model and /reset through command fast-paths so channel sessions keep independent model selection across reconnects and restarts.
This commit is contained in:
William Valentin
2026-02-13 01:04:26 -08:00
parent 3472a0b926
commit 9f81c01603
35 changed files with 1438 additions and 144 deletions
+75 -5
View File
@@ -1,6 +1,7 @@
import Database from 'better-sqlite3';
import type { Message } from '../models/types.js';
import type { PairingStore, ApprovedSender } from '../channels/pairing.js';
import type { HistoryMetadata } from './indexer.js';
/** Parse a duration string like '30d', '7d', '12h' to milliseconds. Returns null if invalid or '0'. */
export function parseDuration(s: string): number | null {
@@ -44,13 +45,18 @@ export class SessionStore {
);
CREATE INDEX IF NOT EXISTS idx_session_config_session ON session_config(session_id);
`);
const messageColumns = this.db.prepare('PRAGMA table_info(messages)').all() as Array<{ name: string }>;
if (!messageColumns.some(column => column.name === 'metadata')) {
this.db.exec('ALTER TABLE messages ADD COLUMN metadata TEXT');
}
}
addMessage(sessionId: string, message: Message): void {
addMessage(sessionId: string, message: Message, metadata?: HistoryMetadata): void {
const stmt = this.db.prepare(
'INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)',
'INSERT INTO messages (session_id, role, content, metadata) VALUES (?, ?, ?, ?)',
);
stmt.run(sessionId, message.role, message.content);
stmt.run(sessionId, message.role, message.content, metadata ? JSON.stringify(metadata) : null);
}
getMessages(sessionId: string): Message[] {
@@ -75,10 +81,10 @@ export class SessionStore {
this.db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
// Re-insert in order
const insert = this.db.prepare(
'INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)',
'INSERT INTO messages (session_id, role, content, metadata) VALUES (?, ?, ?, ?)',
);
for (const msg of messages) {
insert.run(sessionId, msg.role, msg.content);
insert.run(sessionId, msg.role, msg.content, null);
}
});
transaction();
@@ -194,4 +200,68 @@ export class SessionStore {
close(): void {
this.db.close();
}
getMessagesWithMetadata(sessionId: string): Array<{
id: number;
sessionId: string;
role: 'user' | 'assistant';
content: string;
createdAt: number;
metadata: HistoryMetadata | null;
}> {
const stmt = this.db.prepare(
'SELECT id, session_id, role, content, created_at, metadata FROM messages WHERE session_id = ? ORDER BY id ASC',
);
const rows = stmt.all(sessionId) as Array<{
id: number;
session_id: string;
role: string;
content: string;
created_at: number;
metadata: string | null;
}>;
return rows.map(row => ({
id: row.id,
sessionId: row.session_id,
role: row.role as 'user' | 'assistant',
content: row.content,
createdAt: row.created_at,
metadata: row.metadata ? JSON.parse(row.metadata) as HistoryMetadata : null,
}));
}
getAllMessagesWithMetadata(): Array<{
id: number;
sessionId: string;
role: 'user' | 'assistant';
content: string;
createdAt: number;
metadata: HistoryMetadata | null;
}> {
const stmt = this.db.prepare(
'SELECT id, session_id, role, content, created_at, metadata FROM messages ORDER BY id ASC',
);
const rows = stmt.all() as Array<{
id: number;
session_id: string;
role: string;
content: string;
created_at: number;
metadata: string | null;
}>;
return rows.map(row => ({
id: row.id,
sessionId: row.session_id,
role: row.role as 'user' | 'assistant',
content: row.content,
createdAt: row.created_at,
metadata: row.metadata ? JSON.parse(row.metadata) as HistoryMetadata : null,
}));
}
updateMessageMetadata(messageId: number, metadata: HistoryMetadata): void {
this.db.prepare('UPDATE messages SET metadata = ? WHERE id = ?').run(JSON.stringify(metadata), messageId);
}
}