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:
+116
-14
@@ -1,5 +1,6 @@
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
||||
import { join, relative, dirname } from 'path';
|
||||
import { MEMORY_CATEGORIES, categoryNamespace, isMemoryCategory, type MemoryCategory } from './categories.js';
|
||||
|
||||
/**
|
||||
* Configuration for the MemoryStore.
|
||||
@@ -11,6 +12,11 @@ export interface MemoryStoreConfig {
|
||||
maxContextTokens: number;
|
||||
}
|
||||
|
||||
export interface PromptMemorySection {
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single search result from scanning memory files.
|
||||
*/
|
||||
@@ -25,6 +31,11 @@ export interface SearchResult {
|
||||
context: string;
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
categories?: MemoryCategory[];
|
||||
baseNamespacePrefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages persistent markdown memory files on disk.
|
||||
*
|
||||
@@ -94,14 +105,72 @@ export class MemoryStore {
|
||||
this._dirtyNamespaces.add(namespace);
|
||||
}
|
||||
|
||||
/** Read content for a category under a base namespace. */
|
||||
readCategory(baseNamespace: string, category: MemoryCategory): string {
|
||||
return this.read(categoryNamespace(baseNamespace, category));
|
||||
}
|
||||
|
||||
/** Write content for a category under a base namespace. */
|
||||
writeCategory(baseNamespace: string, category: MemoryCategory, content: string, mode: 'append' | 'replace'): void {
|
||||
this.write(categoryNamespace(baseNamespace, category), content, mode);
|
||||
}
|
||||
|
||||
/** List categories that currently exist under a base namespace. */
|
||||
listCategories(baseNamespace: string): MemoryCategory[] {
|
||||
const categorySet = new Set<MemoryCategory>();
|
||||
const prefix = `${baseNamespace}/`;
|
||||
|
||||
for (const namespace of this.listNamespaces()) {
|
||||
if (!namespace.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const suffix = namespace.slice(prefix.length);
|
||||
if (suffix.includes('/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isMemoryCategory(suffix)) {
|
||||
categorySet.add(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
return MEMORY_CATEGORIES.filter(category => categorySet.has(category));
|
||||
}
|
||||
|
||||
/** Read all category files under a base namespace. */
|
||||
readAllCategories(baseNamespace: string): Partial<Record<MemoryCategory, string>> {
|
||||
const result: Partial<Record<MemoryCategory, string>> = {};
|
||||
|
||||
for (const category of this.listCategories(baseNamespace)) {
|
||||
const content = this.readCategory(baseNamespace, category);
|
||||
if (content.length > 0) {
|
||||
result[category] = content;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search across all memory files for a keyword or phrase.
|
||||
* Performs case-insensitive line-by-line matching.
|
||||
* Returns matching lines with 1 line of context above and below.
|
||||
*/
|
||||
search(query: string): SearchResult[] {
|
||||
search(query: string, opts?: SearchOptions): SearchResult[] {
|
||||
const results: SearchResult[] = [];
|
||||
const namespaces = this.listNamespaces();
|
||||
const namespaces = this.listNamespaces().filter((namespace) => {
|
||||
if (opts?.baseNamespacePrefix && !namespace.startsWith(opts.baseNamespacePrefix)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts?.categories && opts.categories.length > 0) {
|
||||
const suffix = namespace.split('/').pop() ?? '';
|
||||
return isMemoryCategory(suffix) && opts.categories.includes(suffix);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
for (const namespace of namespaces) {
|
||||
@@ -178,23 +247,13 @@ export class MemoryStore {
|
||||
* (estimated at 4 characters per token).
|
||||
*/
|
||||
getContextForPrompt(): string {
|
||||
const userMemory = this.read('user');
|
||||
const globalMemory = this.read('global');
|
||||
const sections = this.getPromptSections().map((section) => `## ${section.title}\n\n${section.content}`);
|
||||
|
||||
// Nothing to inject
|
||||
if (userMemory.length === 0 && globalMemory.length === 0) {
|
||||
if (sections.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const sections: string[] = [];
|
||||
|
||||
if (userMemory.length > 0) {
|
||||
sections.push(`## User Memory\n\n${userMemory}`);
|
||||
}
|
||||
if (globalMemory.length > 0) {
|
||||
sections.push(`## Global Memory\n\n${globalMemory}`);
|
||||
}
|
||||
|
||||
const full = sections.join('\n\n');
|
||||
|
||||
// Truncate to fit within the token budget (estimate: 4 chars ≈ 1 token)
|
||||
@@ -205,6 +264,45 @@ export class MemoryStore {
|
||||
return full.slice(0, maxChars);
|
||||
}
|
||||
|
||||
/** Build memory sections used by prompt injectors. */
|
||||
getPromptSections(): PromptMemorySection[] {
|
||||
const userMemory = this.read('user');
|
||||
const globalMemory = this.read('global');
|
||||
const userCategoryMemory = this.readAllCategories('user');
|
||||
const globalCategoryMemory = this.readAllCategories('global');
|
||||
|
||||
const sections: PromptMemorySection[] = [];
|
||||
|
||||
if (userMemory.length > 0) {
|
||||
sections.push({ title: 'User Memory', content: userMemory });
|
||||
}
|
||||
if (globalMemory.length > 0) {
|
||||
sections.push({ title: 'Global Memory', content: globalMemory });
|
||||
}
|
||||
|
||||
for (const category of MEMORY_CATEGORIES) {
|
||||
const content = userCategoryMemory[category];
|
||||
if (content) {
|
||||
sections.push({
|
||||
title: `User ${this._categoryLabel(category)}`,
|
||||
content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const category of MEMORY_CATEGORIES) {
|
||||
const content = globalCategoryMemory[category];
|
||||
if (content) {
|
||||
sections.push({
|
||||
title: `Global ${this._categoryLabel(category)}`,
|
||||
content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -236,4 +334,8 @@ export class MemoryStore {
|
||||
|
||||
return namespaces;
|
||||
}
|
||||
|
||||
private _categoryLabel(category: MemoryCategory): string {
|
||||
return `${category.charAt(0).toUpperCase()}${category.slice(1)}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user