feat: implement model persistence with per-session overrides

- Add session_config SQLite table for per-session settings
- Update routing to support session override → agent config → global default resolution chain
- Upgrade WebChat SessionBridge from NativeAgent to AgentOrchestrator
- Add /model, /local, /cloud commands to Telegram adapter
- Add /model command to WebChat gateway handlers
- Clear session overrides on /reset command
- Pass memoryStore and config through to SessionBridge
- Add comprehensive tests for all new functionality

Fixes model persistence bug where TUI model changes didn't affect WebChat/Telegram sessions. Now:
- TUI /model sets global default (persists across restarts, affects all new sessions)
- WebChat/Telegram /model sets session override (only that conversation, cleared on /reset)
- WebChat sessions gain AgentOrchestrator features (delegation, compaction, memory)
This commit is contained in:
William Valentin
2026-02-11 21:51:38 -08:00
parent b0092c8284
commit a8a2c59313
12 changed files with 1175 additions and 46 deletions
+33
View File
@@ -8,6 +8,9 @@ export interface Session {
getHistory(): Message[];
clear(): void;
replaceHistory(messages: Message[]): void;
getConfig(key: string): string | undefined;
setConfig(key: string, value: string): void;
deleteConfig(key: string): void;
}
export class ManagedSession implements Session {
@@ -64,6 +67,18 @@ export class ManagedSession implements Session {
setHistory(messages: Message[]): void {
this.history = [...messages];
}
getConfig(key: string): string | undefined {
return this.store.getSessionConfig(this.id, key);
}
setConfig(key: string, value: string): void {
this.store.setSessionConfig(this.id, key, value);
}
deleteConfig(key: string): void {
this.store.deleteSessionConfig(this.id, key);
}
}
export class SessionManager {
@@ -129,4 +144,22 @@ export class SessionManager {
this.sessions.delete(id);
}
}
/** Get a session config value. */
getSessionConfig(frontend: string, userId: string, key: string): string | undefined {
const session = this.getSession(frontend, userId);
return session.getConfig(key);
}
/** Set a session config value. */
setSessionConfig(frontend: string, userId: string, key: string, value: string): void {
const session = this.getSession(frontend, userId);
session.setConfig(key, value);
}
/** Delete a session config value. */
deleteSessionConfig(frontend: string, userId: string, key: string): void {
const session = this.getSession(frontend, userId);
session.deleteConfig(key);
}
}
+59 -4
View File
@@ -36,6 +36,13 @@ export class SessionStore {
code_used TEXT NOT NULL,
PRIMARY KEY (channel, sender_id)
);
CREATE TABLE IF NOT EXISTS session_config (
session_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (session_id, key)
);
CREATE INDEX IF NOT EXISTS idx_session_config_session ON session_config(session_id);
`);
}
@@ -78,8 +85,11 @@ export class SessionStore {
}
clearSession(sessionId: string): void {
const stmt = this.db.prepare('DELETE FROM messages WHERE session_id = ?');
stmt.run(sessionId);
const transaction = this.db.transaction(() => {
this.db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
this.db.prepare('DELETE FROM session_config WHERE session_id = ?').run(sessionId);
});
transaction();
}
listSessions(): string[] {
@@ -98,10 +108,12 @@ export class SessionStore {
if (stale.length === 0) {return [];}
const deleteStmt = this.db.prepare('DELETE FROM messages WHERE session_id = ?');
const deleteMessages = this.db.prepare('DELETE FROM messages WHERE session_id = ?');
const deleteConfig = this.db.prepare('DELETE FROM session_config WHERE session_id = ?');
const transaction = this.db.transaction(() => {
for (const { session_id } of stale) {
deleteStmt.run(session_id);
deleteMessages.run(session_id);
deleteConfig.run(session_id);
}
});
transaction();
@@ -136,6 +148,49 @@ export class SessionStore {
};
}
/** Get a single config value for a session. */
getSessionConfig(sessionId: string, key: string): string | undefined {
const stmt = this.db.prepare(
'SELECT value FROM session_config WHERE session_id = ? AND key = ?',
);
const row = stmt.get(sessionId, key) as { value: string } | undefined;
return row?.value;
}
/** Get all config values for a session. */
getAllSessionConfig(sessionId: string): Record<string, string> {
const stmt = this.db.prepare(
'SELECT key, value FROM session_config WHERE session_id = ?',
);
const rows = stmt.all(sessionId) as Array<{ key: string; value: string }>;
const result: Record<string, string> = {};
for (const row of rows) {
result[row.key] = row.value;
}
return result;
}
/** Set a config value for a session. Upserts (INSERT OR REPLACE). */
setSessionConfig(sessionId: string, key: string, value: string): void {
this.db.prepare(
'INSERT OR REPLACE INTO session_config (session_id, key, value) VALUES (?, ?, ?)',
).run(sessionId, key, value);
}
/** Delete a config value for a session. */
deleteSessionConfig(sessionId: string, key: string): void {
this.db.prepare(
'DELETE FROM session_config WHERE session_id = ? AND key = ?',
).run(sessionId, key);
}
/** Delete all config for a session (used when session is cleared/pruned). */
clearSessionConfig(sessionId: string): void {
this.db.prepare(
'DELETE FROM session_config WHERE session_id = ?',
).run(sessionId);
}
close(): void {
this.db.close();
}