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
+63
View File
@@ -82,6 +82,45 @@ describe('MemoryStore', () => {
});
});
describe('category APIs', () => {
it('reads and writes category namespaces', () => {
store.writeCategory('user', 'facts', 'User lives in Berlin', 'replace');
expect(store.readCategory('user', 'facts')).toBe('User lives in Berlin');
});
it('supports append and replace modes in category writes', () => {
store.writeCategory('user', 'preferences', 'Prefers short answers', 'replace');
store.writeCategory('user', 'preferences', 'Likes numbered lists', 'append');
expect(store.readCategory('user', 'preferences')).toContain('Prefers short answers');
expect(store.readCategory('user', 'preferences')).toContain('Likes numbered lists');
store.writeCategory('user', 'preferences', 'Only this remains', 'replace');
const content = store.readCategory('user', 'preferences');
expect(content).toContain('Only this remains');
expect(content).not.toContain('Prefers short answers');
});
it('lists only categories that exist under a base namespace', () => {
store.writeCategory('user', 'facts', 'Fact', 'replace');
store.writeCategory('user', 'projects', 'Project', 'replace');
store.writeCategory('global', 'decisions', 'Decision', 'replace');
expect(store.listCategories('user')).toEqual(['facts', 'projects']);
expect(store.listCategories('global')).toEqual(['decisions']);
expect(store.listCategories('sessions/abc')).toEqual([]);
});
it('reads all existing categories under a base namespace', () => {
store.writeCategory('user', 'facts', 'Fact content', 'replace');
store.writeCategory('user', 'decisions', 'Decision content', 'replace');
expect(store.readAllCategories('user')).toEqual({
facts: 'Fact content',
decisions: 'Decision content',
});
});
});
describe('search', () => {
beforeEach(() => {
store.write('notes', 'The quick brown fox jumps over the lazy dog\nAnother line of text\nFox sightings are common here', 'replace');
@@ -123,6 +162,24 @@ describe('MemoryStore', () => {
const results = store.search('xyznonexistent');
expect(results).toEqual([]);
});
it('supports filtering by category', () => {
store.writeCategory('user', 'facts', 'fox factual statement', 'replace');
store.writeCategory('user', 'preferences', 'prefers fox metaphors', 'replace');
const factOnly = store.search('fox', { categories: ['facts'] });
expect(factOnly.length).toBeGreaterThan(0);
expect(factOnly.every(result => result.namespace.endsWith('/facts'))).toBe(true);
});
it('supports filtering by base namespace prefix', () => {
store.writeCategory('user', 'facts', 'fox in user facts', 'replace');
store.writeCategory('global', 'facts', 'fox in global facts', 'replace');
const userOnly = store.search('fox', { baseNamespacePrefix: 'user/' });
expect(userOnly.length).toBeGreaterThan(0);
expect(userOnly.every(result => result.namespace.startsWith('user/'))).toBe(true);
});
});
describe('listNamespaces', () => {
@@ -166,6 +223,8 @@ describe('MemoryStore', () => {
it('includes user and global memory under headings', () => {
store.write('user', 'User prefers concise answers', 'replace');
store.write('global', 'System-wide knowledge base', 'replace');
store.writeCategory('user', 'facts', 'User timezone is UTC', 'replace');
store.writeCategory('global', 'decisions', 'Adopt pnpm workspace', 'replace');
const context = store.getContextForPrompt();
@@ -174,6 +233,10 @@ describe('MemoryStore', () => {
expect(context).toContain('User prefers concise answers');
expect(context).toContain('Global Memory');
expect(context).toContain('System-wide knowledge base');
expect(context).toContain('User Facts');
expect(context).toContain('User timezone is UTC');
expect(context).toContain('Global Decisions');
expect(context).toContain('Adopt pnpm workspace');
});
it('truncates content to stay within maxContextTokens', () => {