Spaces:
Runtime error
Runtime error
| import { getSettings } from "@/lib/db/settings"; | |
| import type { MemoryConfig } from "./types"; | |
| export interface MemorySettings { | |
| enabled: boolean; | |
| maxTokens: number; | |
| retentionDays: number; | |
| strategy: "recent" | "semantic" | "hybrid"; | |
| skillsEnabled: boolean; | |
| // Plan 21 — D9: new embedding / vector store fields | |
| embeddingSource: "remote" | "static" | "transformers" | "auto"; | |
| embeddingProviderModel: string | null; | |
| transformersEnabled: boolean; | |
| staticEnabled: boolean; | |
| rerankEnabled: boolean; | |
| rerankProviderModel: string | null; | |
| vectorStore: "sqlite-vec" | "qdrant" | "auto"; | |
| } | |
| export const DEFAULT_MEMORY_SETTINGS: MemorySettings = { | |
| // Off by default: enabling memory injects up to `maxTokens` (~2k) of retrieved | |
| // context into every chat request, which is billed — a surprising cost for new | |
| // installs and for clients that manage their own context. Opt in explicitly via | |
| // Settings → Memory (the UI warns about the token cost). Per-request opt-out is | |
| // also available via the `x-omniroute-no-memory` header. See | |
| // _tasks/PRD-2026-06-19-no-memory-header.md. | |
| enabled: false, | |
| maxTokens: 2000, | |
| retentionDays: 30, | |
| strategy: "hybrid", | |
| skillsEnabled: true, | |
| // Plan 21 — D9 defaults | |
| embeddingSource: "auto", | |
| embeddingProviderModel: null, | |
| transformersEnabled: false, | |
| staticEnabled: false, | |
| rerankEnabled: false, | |
| rerankProviderModel: null, | |
| vectorStore: "auto", | |
| }; | |
| let cachedMemorySettings: MemorySettings | null = null; | |
| function toBoolean(value: unknown, fallback: boolean): boolean { | |
| return typeof value === "boolean" ? value : fallback; | |
| } | |
| function clampInteger(value: unknown, fallback: number, min: number, max: number): number { | |
| if (typeof value !== "number" || !Number.isFinite(value)) return fallback; | |
| return Math.min(Math.max(Math.round(value), min), max); | |
| } | |
| function normalizeStrategy(value: unknown): MemorySettings["strategy"] { | |
| return value === "recent" || value === "semantic" || value === "hybrid" | |
| ? value | |
| : DEFAULT_MEMORY_SETTINGS.strategy; | |
| } | |
| function normalizeEmbeddingSource(value: unknown): MemorySettings["embeddingSource"] { | |
| return value === "remote" || value === "static" || value === "transformers" || value === "auto" | |
| ? value | |
| : DEFAULT_MEMORY_SETTINGS.embeddingSource; | |
| } | |
| function normalizeVectorStore(value: unknown): MemorySettings["vectorStore"] { | |
| return value === "sqlite-vec" || value === "qdrant" || value === "auto" | |
| ? value | |
| : DEFAULT_MEMORY_SETTINGS.vectorStore; | |
| } | |
| function normalizeNullableString(value: unknown, fallback: string | null): string | null { | |
| if (value === null || value === undefined) return fallback; | |
| return typeof value === "string" && value.length > 0 ? value : fallback; | |
| } | |
| export function normalizeMemorySettings(rawSettings: Record<string, unknown> = {}): MemorySettings { | |
| return { | |
| enabled: toBoolean(rawSettings.memoryEnabled, DEFAULT_MEMORY_SETTINGS.enabled), | |
| maxTokens: clampInteger( | |
| rawSettings.memoryMaxTokens, | |
| DEFAULT_MEMORY_SETTINGS.maxTokens, | |
| 0, | |
| 16000 | |
| ), | |
| retentionDays: clampInteger( | |
| rawSettings.memoryRetentionDays, | |
| DEFAULT_MEMORY_SETTINGS.retentionDays, | |
| 1, | |
| 365 | |
| ), | |
| strategy: normalizeStrategy(rawSettings.memoryStrategy), | |
| skillsEnabled: toBoolean(rawSettings.skillsEnabled, DEFAULT_MEMORY_SETTINGS.skillsEnabled), | |
| // Plan 21 — D9 new fields | |
| embeddingSource: normalizeEmbeddingSource(rawSettings.memoryEmbeddingSource), | |
| embeddingProviderModel: normalizeNullableString( | |
| rawSettings.memoryEmbeddingProviderModel, | |
| DEFAULT_MEMORY_SETTINGS.embeddingProviderModel | |
| ), | |
| transformersEnabled: toBoolean( | |
| rawSettings.memoryTransformersEnabled, | |
| DEFAULT_MEMORY_SETTINGS.transformersEnabled | |
| ), | |
| staticEnabled: toBoolean(rawSettings.memoryStaticEnabled, DEFAULT_MEMORY_SETTINGS.staticEnabled), | |
| rerankEnabled: toBoolean(rawSettings.memoryRerankEnabled, DEFAULT_MEMORY_SETTINGS.rerankEnabled), | |
| rerankProviderModel: normalizeNullableString( | |
| rawSettings.memoryRerankProviderModel, | |
| DEFAULT_MEMORY_SETTINGS.rerankProviderModel | |
| ), | |
| vectorStore: normalizeVectorStore(rawSettings.memoryVectorStore), | |
| }; | |
| } | |
| export function toMemorySettingsUpdates( | |
| settings: Partial<MemorySettings> | |
| ): Record<string, unknown> { | |
| const updates: Record<string, unknown> = {}; | |
| if (settings.enabled !== undefined) updates.memoryEnabled = settings.enabled; | |
| if (settings.maxTokens !== undefined) updates.memoryMaxTokens = settings.maxTokens; | |
| if (settings.retentionDays !== undefined) updates.memoryRetentionDays = settings.retentionDays; | |
| if (settings.strategy !== undefined) updates.memoryStrategy = settings.strategy; | |
| if (settings.skillsEnabled !== undefined) updates.skillsEnabled = settings.skillsEnabled; | |
| // Plan 21 — D9 new fields | |
| if (settings.embeddingSource !== undefined) | |
| updates.memoryEmbeddingSource = settings.embeddingSource; | |
| if (settings.embeddingProviderModel !== undefined) | |
| updates.memoryEmbeddingProviderModel = settings.embeddingProviderModel; | |
| if (settings.transformersEnabled !== undefined) | |
| updates.memoryTransformersEnabled = settings.transformersEnabled; | |
| if (settings.staticEnabled !== undefined) updates.memoryStaticEnabled = settings.staticEnabled; | |
| if (settings.rerankEnabled !== undefined) updates.memoryRerankEnabled = settings.rerankEnabled; | |
| if (settings.rerankProviderModel !== undefined) | |
| updates.memoryRerankProviderModel = settings.rerankProviderModel; | |
| if (settings.vectorStore !== undefined) updates.memoryVectorStore = settings.vectorStore; | |
| return updates; | |
| } | |
| export function toMemoryRetrievalConfig( | |
| settings: MemorySettings, | |
| extra: { query?: string } = {} | |
| ): Partial<MemoryConfig> & { query?: string } { | |
| const enabled = settings.enabled && settings.maxTokens > 0; | |
| const config: Partial<MemoryConfig> & { query?: string } = { | |
| enabled, | |
| maxTokens: enabled ? settings.maxTokens : 0, | |
| retrievalStrategy: settings.strategy === "recent" ? "exact" : settings.strategy, | |
| autoSummarize: false, | |
| persistAcrossModels: false, | |
| retentionDays: settings.retentionDays, | |
| scope: "apiKey", | |
| }; | |
| // Plan 21 FAIL #1 fix: forward the last user message as `query` so that | |
| // semantic / hybrid strategies actually exercise the vector store in the | |
| // chat hot path (chatCore.ts), not only in the Playground. | |
| // | |
| // BUT only for query-driven strategies. The "recent" strategy (mapped to the | |
| // internal "exact" path) is recency-based: it must return the most recent | |
| // memories regardless of the current prompt. Forwarding a query there makes | |
| // retrieveMemories apply relevance filtering (score > 0), which silently | |
| // drops recent memories whose text doesn't overlap the prompt — breaking the | |
| // "recent" contract. So skip query forwarding when strategy === "recent". | |
| if (extra.query && extra.query.trim().length > 0 && settings.strategy !== "recent") { | |
| config.query = extra.query.trim(); | |
| } | |
| return config; | |
| } | |
| export async function getMemorySettings(): Promise<MemorySettings> { | |
| if (cachedMemorySettings !== null) { | |
| return cachedMemorySettings; | |
| } | |
| const settings = (await getSettings()) as Record<string, unknown>; | |
| cachedMemorySettings = normalizeMemorySettings(settings); | |
| return cachedMemorySettings; | |
| } | |
| export function invalidateMemorySettingsCache(): void { | |
| cachedMemorySettings = null; | |
| } | |