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
+1 -1
View File
@@ -120,7 +120,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
const gateway = createGateway({
config, sessionManager, modelRouter, systemPrompt, toolRegistry, toolExecutor,
channelRegistry, pairingManager, lifecycle,
channelRegistry, pairingManager, lifecycle, memoryStore,
getChannelAgents: () => channelAgents,
});
+55 -1
View File
@@ -58,9 +58,18 @@ export function createMessageRouter(deps: {
if (!entry) {
const session = deps.sessionManager.getSession(channel, senderId);
// Read per-session model tier override (persisted in SQLite)
const sessionTierOverride = session.getConfig('modelTier') as ModelTier | undefined;
// Resolution chain: metadata (cron) → session override → agent config → global default
const effectiveTier = tierFromMetadata
?? sessionTierOverride
?? agentConfig?.modelTier
?? deps.config.agents.primary_tier
?? 'default';
// Use agent config overrides where available, falling back to global config
const effectiveSystemPrompt = agentConfig?.systemPrompt ?? deps.systemPrompt;
const effectiveTier = tierFromMetadata ?? agentConfig?.modelTier ?? deps.config.agents.primary_tier ?? 'default';
const effectiveProvider = deps.config.models.default.provider;
const delegationConfig: DelegationConfig = {
@@ -166,6 +175,46 @@ export function createMessageRouter(deps: {
if (msg.metadata?.isCommand) {
if (msg.metadata.command === 'reset') {
agent.reset();
// Clear per-session config overrides
const session = deps.sessionManager.getSession(msg.channel, msg.senderId);
session.deleteConfig('modelTier');
return;
}
if (msg.metadata.command === 'model') {
const modelArg = msg.metadata.commandArgs as string | undefined;
const session = deps.sessionManager.getSession(msg.channel, msg.senderId);
if (!modelArg) {
// Show current model tier
const currentTier = agent.getModelTier();
const sessionOverride = session.getConfig('modelTier');
const available = deps.modelRouter.getAvailableTiers();
const labels = deps.modelRouter.getAllLabels();
const lines = [`Active tier: ${currentTier}${sessionOverride ? ' (session override)' : ''}`];
for (const tier of available) {
const label = labels[tier] ?? 'unknown';
const marker = tier === currentTier ? ' ←' : '';
lines.push(` ${tier}: ${label}${marker}`);
}
await reply({ text: lines.join('\n'), replyTo: msg.id });
return;
}
// Validate tier
const validTiers = deps.modelRouter.getAvailableTiers();
if (!validTiers.includes(modelArg as ModelTier)) {
await reply({ text: `Model tier not available: ${modelArg}`, replyTo: msg.id });
return;
}
// Persist to session config
session.setConfig('modelTier', modelArg);
// Update the orchestrator's agent tier
agent.setModelTier(modelArg as ModelTier);
const label = deps.modelRouter.getLabel(modelArg as ModelTier);
await reply({ text: `Switched to model: ${modelArg} (${label})`, replyTo: msg.id });
return;
}
if (msg.metadata.command === 'compact') {
@@ -215,8 +264,13 @@ export function createMessageRouter(deps: {
try {
// Determine if the active model supports native audio input
let effectiveTier: string = deps.config.agents.primary_tier ?? 'default';
const session = deps.sessionManager.getSession(msg.channel, msg.senderId);
const sessionTierOverride = session.getConfig('modelTier');
if (msg.metadata?.modelTier) {
effectiveTier = msg.metadata.modelTier as string;
} else if (sessionTierOverride) {
effectiveTier = sessionTierOverride;
} else if (deps.agentRouter && deps.agentConfigRegistry) {
const agentName = deps.agentRouter.resolve(msg.channel, msg.senderId);
if (agentName) {
+3
View File
@@ -13,6 +13,7 @@ import { SkillRegistry, SkillInstaller, loadAllSkills } from '../skills/index.js
import { assembleSystemPrompt } from '../prompt/index.js';
import { resolve } from 'path';
import { homedir } from 'os';
import type { MemoryStore } from '../memory/store.js';
// ── Skills ──────────────────────────────────────────────────────
@@ -121,6 +122,7 @@ export interface GatewayDeps {
pairingManager?: PairingManager;
lifecycle: Lifecycle;
getChannelAgents: () => Map<string, { orchestrator: AgentOrchestrator; collector: OutboundAttachmentCollector }> | null;
memoryStore?: MemoryStore;
}
export function createGateway(deps: GatewayDeps): GatewayServer {
@@ -144,6 +146,7 @@ export function createGateway(deps: GatewayDeps): GatewayServer {
config,
channelRegistry,
pairingManager,
memoryStore: deps.memoryStore,
restart: async () => {
console.log('Restart requested via gateway');
await lifecycle.shutdown();