File size: 7,144 Bytes
6111b2b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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 = {
  enabled: true,
  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;
}