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:
@@ -31,6 +31,7 @@ describe('compactHistory', () => {
|
||||
thresholdPct: 80,
|
||||
keepTurns: 2, // keeps last 4 messages
|
||||
summaryMaxTokens: 1024,
|
||||
importanceThreshold: 1,
|
||||
};
|
||||
|
||||
it('returns no-op when messages count is at or below keepTurns threshold', async () => {
|
||||
@@ -100,6 +101,7 @@ describe('compactHistory', () => {
|
||||
expect(DEFAULT_COMPACTION_CONFIG.thresholdPct).toBe(80);
|
||||
expect(DEFAULT_COMPACTION_CONFIG.keepTurns).toBe(4);
|
||||
expect(DEFAULT_COMPACTION_CONFIG.summaryMaxTokens).toBe(1024);
|
||||
expect(DEFAULT_COMPACTION_CONFIG.importanceThreshold).toBe(1);
|
||||
});
|
||||
|
||||
it('shifts leading assistant messages from toKeep into toCompact to ensure user-first', async () => {
|
||||
@@ -120,4 +122,28 @@ describe('compactHistory', () => {
|
||||
expect(result.messages[1].role).toBe('user');
|
||||
expect(result.messages[1].content).toBe('Message 6');
|
||||
});
|
||||
|
||||
it('preserves high-importance older turns instead of compacting them', async () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'user', content: 'hello' },
|
||||
{ role: 'assistant', content: 'hi' },
|
||||
{ role: 'user', content: 'I prefer concise responses and markdown tables.' },
|
||||
{ role: 'assistant', content: 'noted' },
|
||||
{ role: 'user', content: 'Message 4' },
|
||||
{ role: 'assistant', content: 'Message 5' },
|
||||
{ role: 'user', content: 'Message 6' },
|
||||
{ role: 'assistant', content: 'Message 7' },
|
||||
];
|
||||
|
||||
const orchestrator = makeMockOrchestrator();
|
||||
const result = await compactHistory({
|
||||
messages,
|
||||
orchestrator,
|
||||
config: { ...config, importanceThreshold: 0.45 },
|
||||
});
|
||||
|
||||
expect(result.messages.some(msg => typeof msg.content === 'string' && msg.content.includes('I prefer concise responses'))).toBe(true);
|
||||
expect(result.messages.some(msg => typeof msg.content === 'string' && msg.content.includes('[Summary of earlier conversation]'))).toBe(true);
|
||||
expect(result.messages.length).toBeGreaterThan(5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MemoryStore } from '../memory/store.js';
|
||||
import { COMPACTION_SYSTEM_PROMPT, MEMORY_EXTRACTION_PROMPT } from '../backends/native/prompts.js';
|
||||
import { estimateMessageTokens } from './tokens.js';
|
||||
import { getMessageText } from '../models/media.js';
|
||||
import { selectImportantMessages } from './weighting.js';
|
||||
|
||||
export interface CompactionConfig {
|
||||
/** Percentage of context window that triggers compaction (default: 80). */
|
||||
@@ -12,6 +13,8 @@ export interface CompactionConfig {
|
||||
keepTurns: number;
|
||||
/** Maximum tokens for the compaction summary response. */
|
||||
summaryMaxTokens: number;
|
||||
/** Preserve messages at or above this importance score from compaction. */
|
||||
importanceThreshold: number;
|
||||
}
|
||||
|
||||
export interface CompactionResult {
|
||||
@@ -29,6 +32,7 @@ export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
|
||||
thresholdPct: 80,
|
||||
keepTurns: 4,
|
||||
summaryMaxTokens: 1024,
|
||||
importanceThreshold: 1,
|
||||
};
|
||||
|
||||
export async function compactHistory(opts: {
|
||||
@@ -56,10 +60,34 @@ export async function compactHistory(opts: {
|
||||
// Ensure toKeep starts with a user message to avoid assistant→assistant
|
||||
// after the compaction summary (which has role 'assistant').
|
||||
while (toKeep.length > 0 && toKeep[0].role === 'assistant') {
|
||||
toCompact.push(toKeep.shift()!);
|
||||
const shifted = toKeep.shift();
|
||||
if (!shifted) {
|
||||
break;
|
||||
}
|
||||
toCompact.push(shifted);
|
||||
}
|
||||
|
||||
const formattedConversation = toCompact.map((msg) => `${msg.role}: ${getMessageText(msg)}`).join('\n\n');
|
||||
const preservedImportant = selectImportantMessages(toCompact, {
|
||||
threshold: config.importanceThreshold,
|
||||
maxMessages: Math.max(1, config.keepTurns),
|
||||
});
|
||||
|
||||
const preservedSet = new Set(preservedImportant.map(item => item.index));
|
||||
const toSummarize = toCompact.filter((_, index) => !preservedSet.has(index));
|
||||
|
||||
const formattedConversation = toSummarize.map((msg) => `${msg.role}: ${getMessageText(msg)}`).join('\n\n');
|
||||
|
||||
const preservedMessages = preservedImportant.map(item => item.message);
|
||||
|
||||
if (formattedConversation.trim().length === 0) {
|
||||
const compactedMessages = [...preservedMessages, ...toKeep];
|
||||
return {
|
||||
messages: compactedMessages,
|
||||
compactedCount: messages.length - compactedMessages.length,
|
||||
tokensBefore: estimateMessageTokens(messages),
|
||||
tokensAfter: estimateMessageTokens(compactedMessages),
|
||||
};
|
||||
}
|
||||
|
||||
const tier = orchestrator.getDelegationTier('compaction');
|
||||
|
||||
@@ -99,9 +127,9 @@ export async function compactHistory(opts: {
|
||||
}
|
||||
|
||||
return {
|
||||
messages: [summaryMessage, ...toKeep],
|
||||
compactedCount: toCompact.length,
|
||||
messages: [...preservedMessages, summaryMessage, ...toKeep],
|
||||
compactedCount: toSummarize.length,
|
||||
tokensBefore: estimateMessageTokens(messages),
|
||||
tokensAfter: estimateMessageTokens([summaryMessage, ...toKeep]),
|
||||
tokensAfter: estimateMessageTokens([...preservedMessages, summaryMessage, ...toKeep]),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user