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
+5 -2
View File
@@ -62,10 +62,13 @@ describe('TelegramAdapter', () => {
// .use() for auth middleware
expect(mockUse).toHaveBeenCalledTimes(1);
// .command() for /start and /reset
expect(mockCommand).toHaveBeenCalledTimes(2);
// .command() for /start, /reset, /model, /local, /cloud
expect(mockCommand).toHaveBeenCalledTimes(5);
expect(mockCommand.mock.calls[0][0]).toBe('start');
expect(mockCommand.mock.calls[1][0]).toBe('reset');
expect(mockCommand.mock.calls[2][0]).toBe('model');
expect(mockCommand.mock.calls[3][0]).toBe('local');
expect(mockCommand.mock.calls[4][0]).toBe('cloud');
// .on('message:text', ...) for text handler
expect(mockOn).toHaveBeenCalledWith('message:text', expect.any(Function));
// .start() to begin long polling
+46
View File
@@ -163,6 +163,52 @@ export class TelegramAdapter implements ChannelAdapter {
await ctx.reply('Conversation reset.');
});
this.bot.command('model', async (ctx) => {
if (!this.messageHandler) return;
const args = ctx.message?.text?.replace(/^\/model\s*/, '').trim() ?? '';
this.messageHandler({
id: String(ctx.message?.message_id ?? Date.now()),
channel: 'telegram',
senderId: String(ctx.chat.id),
senderName: ctx.from?.first_name,
text: `/model ${args}`.trim(),
timestamp: Date.now(),
metadata: {
isCommand: true,
command: 'model',
commandArgs: args || undefined,
},
});
});
this.bot.command('local', async (ctx) => {
if (!this.messageHandler) return;
this.messageHandler({
id: String(ctx.message?.message_id ?? Date.now()),
channel: 'telegram',
senderId: String(ctx.chat.id),
senderName: ctx.from?.first_name,
text: '/model local',
timestamp: Date.now(),
metadata: { isCommand: true, command: 'model', commandArgs: 'local' },
});
});
this.bot.command('cloud', async (ctx) => {
if (!this.messageHandler) return;
this.messageHandler({
id: String(ctx.message?.message_id ?? Date.now()),
channel: 'telegram',
senderId: String(ctx.chat.id),
senderName: ctx.from?.first_name,
text: '/model default',
timestamp: Date.now(),
metadata: { isCommand: true, command: 'model', commandArgs: 'default' },
});
});
// ── Text message handler ──
this.bot.on('message:text', async (ctx) => {