om-memory-system 4.23.0-next.144 → 4.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/adapters/opencode/user-prompt.js +6 -1
  2. package/dist/config-template.d.ts +6 -0
  3. package/dist/config-template.js +385 -0
  4. package/dist/config.d.ts +27 -1
  5. package/dist/config.js +29 -382
  6. package/dist/core/retrieval.js +1 -1
  7. package/dist/services/chat/chat-answer.d.ts +56 -0
  8. package/dist/services/chat/chat-answer.js +88 -0
  9. package/dist/services/chat/chat-plan.d.ts +60 -0
  10. package/dist/services/chat/chat-plan.js +92 -0
  11. package/dist/services/chat/chat-retrieve.d.ts +36 -0
  12. package/dist/services/chat/chat-retrieve.js +82 -0
  13. package/dist/services/chat/chat-routes.d.ts +12 -0
  14. package/dist/services/chat/chat-routes.js +145 -0
  15. package/dist/services/chat/chat-runtime.d.ts +21 -0
  16. package/dist/services/chat/chat-runtime.js +104 -0
  17. package/dist/services/chat/chat-service.d.ts +42 -0
  18. package/dist/services/chat/chat-service.js +130 -0
  19. package/dist/services/chat/chat-status.d.ts +16 -0
  20. package/dist/services/chat/chat-status.js +23 -0
  21. package/dist/services/chat/chat-store.d.ts +60 -0
  22. package/dist/services/chat/chat-store.js +352 -0
  23. package/dist/services/chat/chat-types.d.ts +59 -0
  24. package/dist/services/chat/chat-types.js +8 -0
  25. package/dist/services/client.d.ts +1 -0
  26. package/dist/services/client.js +38 -5
  27. package/dist/services/context.d.ts +3 -1
  28. package/dist/services/context.js +4 -2
  29. package/dist/services/global-config-writer.js +61 -7
  30. package/dist/services/privacy.js +2 -2
  31. package/dist/services/request-timeouts.d.ts +5 -0
  32. package/dist/services/request-timeouts.js +8 -0
  33. package/dist/services/reranker-test.d.ts +1 -0
  34. package/dist/services/reranker-test.js +94 -0
  35. package/dist/services/reranker.d.ts +45 -0
  36. package/dist/services/reranker.js +133 -0
  37. package/dist/services/retrieval-rerank.d.ts +43 -0
  38. package/dist/services/retrieval-rerank.js +42 -0
  39. package/dist/services/settings-snapshot.js +26 -4
  40. package/dist/services/turso/exact-scan.d.ts +6 -0
  41. package/dist/services/turso/exact-scan.js +23 -0
  42. package/dist/services/turso/keyword-search.d.ts +35 -0
  43. package/dist/services/turso/keyword-search.js +153 -0
  44. package/dist/services/turso/rank-fusion.d.ts +34 -0
  45. package/dist/services/turso/rank-fusion.js +45 -0
  46. package/dist/services/turso/tag-match.d.ts +7 -0
  47. package/dist/services/turso/tag-match.js +29 -0
  48. package/dist/services/turso/types.d.ts +2 -0
  49. package/dist/services/turso/vector-search.d.ts +3 -3
  50. package/dist/services/turso/vector-search.js +23 -39
  51. package/dist/services/user-profile/profile-context.d.ts +6 -1
  52. package/dist/services/user-profile/profile-context.js +43 -7
  53. package/dist/services/user-profile/profile-relevance.d.ts +37 -0
  54. package/dist/services/user-profile/profile-relevance.js +75 -0
  55. package/dist/services/web-route-modules.d.ts +8 -0
  56. package/dist/services/web-route-modules.js +15 -0
  57. package/dist/services/web-server.js +8 -4
  58. package/dist/utils/keyword-boost.d.ts +17 -0
  59. package/dist/utils/keyword-boost.js +32 -0
  60. package/dist/utils/memory-limits.d.ts +7 -8
  61. package/dist/utils/memory-limits.js +10 -4
  62. package/dist/utils/reranker-settings.d.ts +13 -0
  63. package/dist/utils/reranker-settings.js +48 -0
  64. package/dist/utils/threshold-default.d.ts +12 -0
  65. package/dist/utils/threshold-default.js +22 -0
  66. package/dist/web/assets/index-DWk9C9Sm.css +2 -0
  67. package/dist/web/assets/index-rHcTKCkC.js +126 -0
  68. package/dist/web/index.html +2 -2
  69. package/package.json +1 -1
  70. package/dist/web/assets/index-7385lY75.css +0 -2
  71. package/dist/web/assets/index-C9y78EH_.js +0 -127
@@ -0,0 +1,16 @@
1
+ export interface ChatSettings {
2
+ memoryProvider?: string;
3
+ memoryModel?: string;
4
+ memoryApiUrl?: string;
5
+ memoryApiKey?: string;
6
+ }
7
+ export interface ChatStatus {
8
+ ready: boolean;
9
+ /** Names of the settings still needed. Never their values. */
10
+ missing: string[];
11
+ provider: string;
12
+ /** The host that receives questions and memory text, so the page can name it. */
13
+ apiHost: string | null;
14
+ }
15
+ /** Whether chat can call the external memory model. Mirrors `buildMemoryProviderConfig`. */
16
+ export declare function chatStatus(config: ChatSettings): ChatStatus;
@@ -0,0 +1,23 @@
1
+ import { isPlaceholderApiKey } from "../ai/api-key-placeholder.js";
2
+ /** Whether chat can call the external memory model. Mirrors `buildMemoryProviderConfig`. */
3
+ export function chatStatus(config) {
4
+ const provider = config.memoryProvider ?? "openai-chat";
5
+ const presetEndpoint = provider === "orcarouter";
6
+ const missing = [];
7
+ if (!config.memoryModel && !presetEndpoint)
8
+ missing.push("memoryModel");
9
+ if (!config.memoryApiUrl && !presetEndpoint)
10
+ missing.push("memoryApiUrl");
11
+ if (!config.memoryApiKey || isPlaceholderApiKey(config.memoryApiKey)) {
12
+ missing.push("memoryApiKey");
13
+ }
14
+ return { ready: missing.length === 0, missing, provider, apiHost: hostOf(config.memoryApiUrl) };
15
+ }
16
+ function hostOf(url) {
17
+ try {
18
+ return url ? new URL(url).host : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
@@ -0,0 +1,60 @@
1
+ import type { TursoDb } from "../turso/turso-db.js";
2
+ import { type ChatConversation, type ChatFolder, type ChatMessage, type ChatNotice, type ChatPlanRecord, type ChatRole, type ChatSource } from "./chat-types.js";
3
+ /** Bump when the tables change, and add the step to `migrate`. */
4
+ export declare const CHAT_SCHEMA_VERSION = 1;
5
+ export declare const FOLDER_NAME_MAX = 80;
6
+ export declare const BULK_DELETE_MAX = 500;
7
+ type GetConnection = (dbPath: string) => Promise<TursoDb>;
8
+ /**
9
+ * Conversations, messages, and folders in `chats.db`. One instance per file;
10
+ * every write checks the schema version so a file from a newer OMMS stays
11
+ * untouched.
12
+ */
13
+ export declare class ChatStore {
14
+ private readonly dbPath;
15
+ private readonly connect;
16
+ private readOnly;
17
+ private lastStamp;
18
+ constructor(dbPath: string, connect?: GetConnection);
19
+ private db;
20
+ private now;
21
+ init(): Promise<void>;
22
+ isReadOnly(): boolean;
23
+ private writable;
24
+ listConversations(): Promise<ChatConversation[]>;
25
+ getConversation(id: string): Promise<{
26
+ conversation: ChatConversation;
27
+ messages: ChatMessage[];
28
+ } | null>;
29
+ createConversation(input: {
30
+ title: string;
31
+ scopeTag: string | null;
32
+ folderId?: string | null;
33
+ }): Promise<ChatConversation>;
34
+ private updateConversation;
35
+ renameConversation(id: string, title: string): Promise<void>;
36
+ setConversationScope(id: string, scopeTag: string | null): Promise<void>;
37
+ moveConversation(id: string, folderId: string | null): Promise<void>;
38
+ deleteConversation(id: string): Promise<void>;
39
+ /** Deletes every id or none: an unknown id fails the whole request. */
40
+ bulkDeleteConversations(ids: string[]): Promise<number>;
41
+ appendMessage(conversationId: string, input: {
42
+ role: ChatRole;
43
+ content: string;
44
+ sources?: ChatSource[];
45
+ plan?: ChatPlanRecord | null;
46
+ model?: string | null;
47
+ errorCode?: string | null;
48
+ notice?: ChatNotice | null;
49
+ }): Promise<ChatMessage>;
50
+ deleteMessage(id: string): Promise<void>;
51
+ countMessages(): Promise<number>;
52
+ listFolders(): Promise<ChatFolder[]>;
53
+ private requireFolder;
54
+ private assertNameFree;
55
+ createFolder(name: string): Promise<ChatFolder>;
56
+ renameFolder(id: string, name: string): Promise<void>;
57
+ /** Deletes the folder and every conversation in it. Returns how many conversations went. */
58
+ deleteFolder(id: string): Promise<number>;
59
+ }
60
+ export {};
@@ -0,0 +1,352 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { tursoConnectionManager } from "../turso/connection-manager.js";
3
+ import { ChatStoreError, } from "./chat-types.js";
4
+ /** Bump when the tables change, and add the step to `migrate`. */
5
+ export const CHAT_SCHEMA_VERSION = 1;
6
+ export const FOLDER_NAME_MAX = 80;
7
+ export const BULK_DELETE_MAX = 500;
8
+ // The file is copied between computers by hand (ADR-032), so it keeps the
9
+ // default rollback journal: committed data lives in chats.db alone.
10
+ const SCHEMA = [
11
+ `CREATE TABLE IF NOT EXISTS chat_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)`,
12
+ `CREATE TABLE IF NOT EXISTS chat_folders (
13
+ id TEXT PRIMARY KEY,
14
+ name TEXT NOT NULL,
15
+ name_key TEXT NOT NULL UNIQUE,
16
+ created_at INTEGER NOT NULL,
17
+ updated_at INTEGER NOT NULL
18
+ )`,
19
+ `CREATE TABLE IF NOT EXISTS chat_conversations (
20
+ id TEXT PRIMARY KEY,
21
+ folder_id TEXT REFERENCES chat_folders(id) ON DELETE CASCADE,
22
+ title TEXT NOT NULL,
23
+ scope_tag TEXT,
24
+ created_at INTEGER NOT NULL,
25
+ updated_at INTEGER NOT NULL
26
+ )`,
27
+ `CREATE TABLE IF NOT EXISTS chat_messages (
28
+ id TEXT PRIMARY KEY,
29
+ conversation_id TEXT NOT NULL REFERENCES chat_conversations(id) ON DELETE CASCADE,
30
+ role TEXT NOT NULL,
31
+ content TEXT NOT NULL,
32
+ sources_json TEXT,
33
+ plan_json TEXT,
34
+ model TEXT,
35
+ error_code TEXT,
36
+ notice TEXT,
37
+ created_at INTEGER NOT NULL
38
+ )`,
39
+ `CREATE INDEX IF NOT EXISTS chat_messages_conversation ON chat_messages(conversation_id, created_at)`,
40
+ `CREATE INDEX IF NOT EXISTS chat_conversations_folder ON chat_conversations(folder_id)`,
41
+ ];
42
+ // Only these fields are written, so a source never carries a folder path or an email.
43
+ const SOURCE_FIELDS = [
44
+ "label",
45
+ "kind",
46
+ "id",
47
+ "text",
48
+ "projectName",
49
+ "projectTag",
50
+ "createdAt",
51
+ "tags",
52
+ "score",
53
+ "section",
54
+ "category",
55
+ "evidence",
56
+ ];
57
+ function snapshot(source) {
58
+ const out = {};
59
+ for (const field of SOURCE_FIELDS) {
60
+ if (source[field] !== undefined)
61
+ out[field] = source[field];
62
+ }
63
+ return out;
64
+ }
65
+ function parseJson(value, fallback) {
66
+ if (typeof value !== "string" || !value)
67
+ return fallback;
68
+ try {
69
+ return JSON.parse(value);
70
+ }
71
+ catch {
72
+ return fallback;
73
+ }
74
+ }
75
+ function toConversation(row) {
76
+ return {
77
+ id: String(row.id),
78
+ folderId: row.folder_id == null ? null : String(row.folder_id),
79
+ title: String(row.title),
80
+ scopeTag: row.scope_tag == null ? null : String(row.scope_tag),
81
+ createdAt: Number(row.created_at),
82
+ updatedAt: Number(row.updated_at),
83
+ };
84
+ }
85
+ function toMessage(row) {
86
+ return {
87
+ id: String(row.id),
88
+ conversationId: String(row.conversation_id),
89
+ role: String(row.role),
90
+ content: String(row.content),
91
+ sources: parseJson(row.sources_json, []),
92
+ plan: parseJson(row.plan_json, null),
93
+ model: row.model == null ? null : String(row.model),
94
+ errorCode: row.error_code == null ? null : String(row.error_code),
95
+ notice: row.notice == null ? null : String(row.notice),
96
+ createdAt: Number(row.created_at),
97
+ };
98
+ }
99
+ function folderName(name) {
100
+ const trimmed = name.trim();
101
+ if (!trimmed || trimmed.length > FOLDER_NAME_MAX) {
102
+ throw new ChatStoreError(`Folder names have 1 to ${FOLDER_NAME_MAX} characters`, "invalid");
103
+ }
104
+ return { name: trimmed, key: trimmed.toLowerCase() };
105
+ }
106
+ /**
107
+ * Conversations, messages, and folders in `chats.db`. One instance per file;
108
+ * every write checks the schema version so a file from a newer OMMS stays
109
+ * untouched.
110
+ */
111
+ export class ChatStore {
112
+ dbPath;
113
+ connect;
114
+ readOnly = false;
115
+ // Strictly increasing timestamps keep "newest first" stable within one millisecond.
116
+ lastStamp = 0;
117
+ constructor(dbPath, connect = (path) => tursoConnectionManager.getConnection(path)) {
118
+ this.dbPath = dbPath;
119
+ this.connect = connect;
120
+ }
121
+ db() {
122
+ return this.connect(this.dbPath);
123
+ }
124
+ now() {
125
+ this.lastStamp = Math.max(Date.now(), this.lastStamp + 1);
126
+ return this.lastStamp;
127
+ }
128
+ async init() {
129
+ const db = await this.db();
130
+ await db.execute(SCHEMA[0]);
131
+ const row = await db.get("SELECT value FROM chat_meta WHERE key = 'schema_version'");
132
+ const version = row ? Number(row.value) : 0;
133
+ if (version > CHAT_SCHEMA_VERSION) {
134
+ this.readOnly = true;
135
+ return;
136
+ }
137
+ this.readOnly = false;
138
+ await db.batch(SCHEMA.slice(1).map((sql) => ({ sql })));
139
+ if (version < CHAT_SCHEMA_VERSION) {
140
+ await db.run("INSERT INTO chat_meta (key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", [String(CHAT_SCHEMA_VERSION)]);
141
+ }
142
+ }
143
+ isReadOnly() {
144
+ return this.readOnly;
145
+ }
146
+ writable() {
147
+ if (this.readOnly) {
148
+ throw new ChatStoreError("chats.db is from a newer OMMS and is read-only", "read_only");
149
+ }
150
+ }
151
+ async listConversations() {
152
+ const db = await this.db();
153
+ const rows = await db.all("SELECT * FROM chat_conversations ORDER BY updated_at DESC, created_at DESC");
154
+ return rows.map(toConversation);
155
+ }
156
+ async getConversation(id) {
157
+ const db = await this.db();
158
+ const row = await db.get("SELECT * FROM chat_conversations WHERE id = ?", [id]);
159
+ if (!row)
160
+ return null;
161
+ const messages = await db.all("SELECT * FROM chat_messages WHERE conversation_id = ? ORDER BY created_at, rowid", [id]);
162
+ return { conversation: toConversation(row), messages: messages.map(toMessage) };
163
+ }
164
+ async createConversation(input) {
165
+ this.writable();
166
+ const db = await this.db();
167
+ if (input.folderId)
168
+ await this.requireFolder(input.folderId);
169
+ const stamp = this.now();
170
+ const conversation = {
171
+ id: randomUUID(),
172
+ folderId: input.folderId ?? null,
173
+ title: input.title.trim() || "New chat",
174
+ scopeTag: input.scopeTag,
175
+ createdAt: stamp,
176
+ updatedAt: stamp,
177
+ };
178
+ await db.run("INSERT INTO chat_conversations (id, folder_id, title, scope_tag, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", [
179
+ conversation.id,
180
+ conversation.folderId,
181
+ conversation.title,
182
+ conversation.scopeTag,
183
+ stamp,
184
+ stamp,
185
+ ]);
186
+ return conversation;
187
+ }
188
+ async updateConversation(id, set, args) {
189
+ this.writable();
190
+ const db = await this.db();
191
+ const changed = await db.run(`UPDATE chat_conversations SET ${set}, updated_at = ? WHERE id = ?`, [...args, this.now(), id]);
192
+ if (changed === 0)
193
+ throw new ChatStoreError("Conversation not found", "not_found");
194
+ }
195
+ async renameConversation(id, title) {
196
+ const trimmed = title.trim();
197
+ if (!trimmed)
198
+ throw new ChatStoreError("A title is required", "invalid");
199
+ await this.updateConversation(id, "title = ?", [trimmed.slice(0, 200)]);
200
+ }
201
+ async setConversationScope(id, scopeTag) {
202
+ await this.updateConversation(id, "scope_tag = ?", [scopeTag]);
203
+ }
204
+ async moveConversation(id, folderId) {
205
+ if (folderId)
206
+ await this.requireFolder(folderId);
207
+ await this.updateConversation(id, "folder_id = ?", [folderId]);
208
+ }
209
+ async deleteConversation(id) {
210
+ this.writable();
211
+ const db = await this.db();
212
+ const changed = await db.run("DELETE FROM chat_conversations WHERE id = ?", [id]);
213
+ if (changed === 0)
214
+ throw new ChatStoreError("Conversation not found", "not_found");
215
+ }
216
+ /** Deletes every id or none: an unknown id fails the whole request. */
217
+ async bulkDeleteConversations(ids) {
218
+ this.writable();
219
+ const unique = [...new Set(ids)];
220
+ if (unique.length === 0 || unique.length > BULK_DELETE_MAX) {
221
+ throw new ChatStoreError(`Select 1 to ${BULK_DELETE_MAX} conversations`, "invalid");
222
+ }
223
+ const db = await this.db();
224
+ const marks = unique.map(() => "?").join(",");
225
+ return db.transaction("write", async (tx) => {
226
+ const found = await tx.execute({
227
+ sql: `SELECT COUNT(*) AS n FROM chat_conversations WHERE id IN (${marks})`,
228
+ args: unique,
229
+ });
230
+ if (Number(found.rows[0]?.n) !== unique.length) {
231
+ throw new ChatStoreError("Conversation not found", "not_found");
232
+ }
233
+ const result = await tx.execute({
234
+ sql: `DELETE FROM chat_conversations WHERE id IN (${marks})`,
235
+ args: unique,
236
+ });
237
+ return Number(result.rowsAffected);
238
+ });
239
+ }
240
+ async appendMessage(conversationId, input) {
241
+ this.writable();
242
+ const db = await this.db();
243
+ const stamp = this.now();
244
+ const sources = (input.sources ?? []).map(snapshot);
245
+ const message = {
246
+ id: randomUUID(),
247
+ conversationId,
248
+ role: input.role,
249
+ content: input.content,
250
+ sources,
251
+ plan: input.plan ?? null,
252
+ model: input.model ?? null,
253
+ errorCode: input.errorCode ?? null,
254
+ notice: input.notice ?? null,
255
+ createdAt: stamp,
256
+ };
257
+ await db.batch([
258
+ {
259
+ sql: "INSERT INTO chat_messages (id, conversation_id, role, content, sources_json, plan_json, model, error_code, notice, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
260
+ args: [
261
+ message.id,
262
+ conversationId,
263
+ message.role,
264
+ message.content,
265
+ sources.length ? JSON.stringify(sources) : null,
266
+ message.plan ? JSON.stringify(message.plan) : null,
267
+ message.model,
268
+ message.errorCode,
269
+ message.notice,
270
+ stamp,
271
+ ],
272
+ },
273
+ {
274
+ sql: "UPDATE chat_conversations SET updated_at = ? WHERE id = ?",
275
+ args: [stamp, conversationId],
276
+ },
277
+ ]);
278
+ return message;
279
+ }
280
+ async deleteMessage(id) {
281
+ this.writable();
282
+ const db = await this.db();
283
+ await db.run("DELETE FROM chat_messages WHERE id = ?", [id]);
284
+ }
285
+ async countMessages() {
286
+ const db = await this.db();
287
+ const row = await db.get("SELECT COUNT(*) AS n FROM chat_messages");
288
+ return Number(row?.n ?? 0);
289
+ }
290
+ async listFolders() {
291
+ const db = await this.db();
292
+ const rows = await db.all(`SELECT f.*, (SELECT COUNT(*) FROM chat_conversations c WHERE c.folder_id = f.id) AS n
293
+ FROM chat_folders f ORDER BY f.name_key`);
294
+ return rows.map((row) => ({
295
+ id: String(row.id),
296
+ name: String(row.name),
297
+ createdAt: Number(row.created_at),
298
+ updatedAt: Number(row.updated_at),
299
+ conversationCount: Number(row.n),
300
+ }));
301
+ }
302
+ async requireFolder(id) {
303
+ const db = await this.db();
304
+ if (!(await db.get("SELECT id FROM chat_folders WHERE id = ?", [id]))) {
305
+ throw new ChatStoreError("Folder not found", "not_found");
306
+ }
307
+ }
308
+ async assertNameFree(key, exceptId) {
309
+ const db = await this.db();
310
+ const clash = await db.get("SELECT id FROM chat_folders WHERE name_key = ? AND id != ?", [
311
+ key,
312
+ exceptId ?? "",
313
+ ]);
314
+ if (clash)
315
+ throw new ChatStoreError("A folder with this name exists", "conflict");
316
+ }
317
+ async createFolder(name) {
318
+ this.writable();
319
+ const clean = folderName(name);
320
+ await this.assertNameFree(clean.key);
321
+ const db = await this.db();
322
+ const stamp = this.now();
323
+ const id = randomUUID();
324
+ await db.run("INSERT INTO chat_folders (id, name, name_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", [id, clean.name, clean.key, stamp, stamp]);
325
+ return { id, name: clean.name, createdAt: stamp, updatedAt: stamp, conversationCount: 0 };
326
+ }
327
+ async renameFolder(id, name) {
328
+ this.writable();
329
+ const clean = folderName(name);
330
+ await this.assertNameFree(clean.key, id);
331
+ const db = await this.db();
332
+ const changed = await db.run("UPDATE chat_folders SET name = ?, name_key = ?, updated_at = ? WHERE id = ?", [clean.name, clean.key, this.now(), id]);
333
+ if (changed === 0)
334
+ throw new ChatStoreError("Folder not found", "not_found");
335
+ }
336
+ /** Deletes the folder and every conversation in it. Returns how many conversations went. */
337
+ async deleteFolder(id) {
338
+ this.writable();
339
+ const db = await this.db();
340
+ return db.transaction("write", async (tx) => {
341
+ const count = await tx.execute({
342
+ sql: "SELECT COUNT(*) AS n FROM chat_conversations WHERE folder_id = ?",
343
+ args: [id],
344
+ });
345
+ const result = await tx.execute({ sql: "DELETE FROM chat_folders WHERE id = ?", args: [id] });
346
+ if (Number(result.rowsAffected) === 0) {
347
+ throw new ChatStoreError("Folder not found", "not_found");
348
+ }
349
+ return Number(count.rows[0]?.n ?? 0);
350
+ });
351
+ }
352
+ }
@@ -0,0 +1,59 @@
1
+ /** Shared shapes for the web chat: stored conversations, sources, and the search plan. */
2
+ export type ChatRole = "user" | "assistant";
3
+ /** A source as it was sent to the model, saved with the answer so it still opens later. */
4
+ export interface ChatSource {
5
+ /** `S1`, `S2`, … for memories; `P1`, `P2`, … for profile items. */
6
+ label: string;
7
+ kind: "memory" | "profile";
8
+ id: string;
9
+ text: string;
10
+ projectName?: string;
11
+ projectTag?: string;
12
+ createdAt?: number;
13
+ tags?: string[];
14
+ score?: number;
15
+ /** Profile sources only. */
16
+ section?: "preferences" | "patterns" | "workflows";
17
+ category?: string;
18
+ evidence?: string[];
19
+ }
20
+ export interface ChatPlanRecord {
21
+ queries: string[];
22
+ tags: string[];
23
+ profileCategories: string[];
24
+ /** True when the plan call failed or returned nothing usable. */
25
+ skipped: boolean;
26
+ }
27
+ /** Shown with an assistant message that is not a normal cited answer. */
28
+ export type ChatNotice = "no_sources" | "insufficient" | "uncited";
29
+ export interface ChatMessage {
30
+ id: string;
31
+ conversationId: string;
32
+ role: ChatRole;
33
+ content: string;
34
+ sources: ChatSource[];
35
+ plan: ChatPlanRecord | null;
36
+ model: string | null;
37
+ errorCode: string | null;
38
+ notice: ChatNotice | null;
39
+ createdAt: number;
40
+ }
41
+ export interface ChatConversation {
42
+ id: string;
43
+ folderId: string | null;
44
+ title: string;
45
+ scopeTag: string | null;
46
+ createdAt: number;
47
+ updatedAt: number;
48
+ }
49
+ export interface ChatFolder {
50
+ id: string;
51
+ name: string;
52
+ createdAt: number;
53
+ updatedAt: number;
54
+ conversationCount: number;
55
+ }
56
+ export declare class ChatStoreError extends Error {
57
+ readonly code: "not_found" | "conflict" | "invalid" | "read_only";
58
+ constructor(message: string, code: "not_found" | "conflict" | "invalid" | "read_only");
59
+ }
@@ -0,0 +1,8 @@
1
+ /** Shared shapes for the web chat: stored conversations, sources, and the search plan. */
2
+ export class ChatStoreError extends Error {
3
+ code;
4
+ constructor(message, code) {
5
+ super(message);
6
+ this.code = code;
7
+ }
8
+ }
@@ -22,6 +22,7 @@ export declare class LocalMemoryClient {
22
22
  results: import("./turso/types.js").SearchResult[];
23
23
  total: number;
24
24
  timing: number;
25
+ queryVector: Float32Array<ArrayBufferLike>;
25
26
  error?: undefined;
26
27
  } | {
27
28
  success: false;
@@ -5,6 +5,8 @@ import { tursoConnectionManager } from "./turso/connection-manager.js";
5
5
  import { ensureTursoReady } from "./turso/ready.js";
6
6
  import { formatTagsForEmbedding } from "./turso/vector-utils.js";
7
7
  import { extractScopeFromContainerTag, resolveMemoryScope, } from "./memory-scope.js";
8
+ import { rerankSettingsFrom } from "./reranker.js";
9
+ import { rerankedSearch } from "./retrieval-rerank.js";
8
10
  import { CONFIG } from "../config.js";
9
11
  import { log } from "./logger.js";
10
12
  function safeToISOString(timestamp) {
@@ -86,24 +88,55 @@ export class LocalMemoryClient {
86
88
  this.reset();
87
89
  }
88
90
  async searchMemories(query, containerTag, scope = "project") {
89
- // Capture the result count at entry: initialize, embedding, and shard
90
- // reads all await, and a config edit during them must not change this
91
- // search's limit.
91
+ // Capture the result count and the reranker settings at entry: initialize,
92
+ // embedding, and shard reads all await, and a config edit during them must
93
+ // not change this search's limit or reranker target.
92
94
  const maxMemories = CONFIG.maxMemories;
95
+ const keywordBoost = CONFIG.keywordBoost;
96
+ const reranker = rerankSettingsFrom(CONFIG);
97
+ // The reranker path below always uses the boost: it needs a full pool on the similarity scale.
98
+ const keywordMode = CONFIG.keywordMode;
99
+ if (CONFIG.rerankerEnabled && !reranker) {
100
+ // On but incomplete: search must keep working, so treat it as off.
101
+ log("Reranker enabled without a URL or model; searching without it", {
102
+ code: "reranker_not_configured",
103
+ });
104
+ }
93
105
  try {
94
106
  await this.initialize();
95
107
  const queryVector = await embeddingService.embedWithTimeout(query, { task: "query" });
96
108
  const resolved = resolveScopeValue(scope, containerTag);
97
109
  const shards = (await Promise.all(resolved.map((ref) => tursoShardManager.getAllShards(ref.scope, ref.hash)))).flat();
98
110
  if (shards.length === 0) {
99
- return { success: true, results: [], total: 0, timing: 0 };
111
+ return { success: true, results: [], total: 0, timing: 0, queryVector };
112
+ }
113
+ let results;
114
+ let warnings;
115
+ if (reranker) {
116
+ // One shard read with no threshold feeds both the reranker and the
117
+ // fallback path (retrieval-rerank.ts, design decision 7).
118
+ ({ results, warnings } = await rerankedSearch({
119
+ shards,
120
+ queryVector,
121
+ containerTag: scope === "all-projects" ? "" : containerTag,
122
+ queryText: query,
123
+ maxMemories,
124
+ similarityThreshold: CONFIG.similarityThreshold,
125
+ }, reranker, {
126
+ search: (shardList, vector, tag, limit, threshold, queryText) => tursoVectorSearch.searchAcrossShards(shardList, vector, tag, limit, threshold, queryText, keywordBoost),
127
+ log: (message, meta) => log(message, meta),
128
+ }));
129
+ }
130
+ else {
131
+ ({ results, warnings } = await tursoVectorSearch.searchAcrossShards(shards, queryVector, scope === "all-projects" ? "" : containerTag, maxMemories, CONFIG.similarityThreshold, query, keywordBoost, keywordMode));
100
132
  }
101
- const { results, warnings } = await tursoVectorSearch.searchAcrossShards(shards, queryVector, scope === "all-projects" ? "" : containerTag, maxMemories, CONFIG.similarityThreshold, query);
102
133
  return {
103
134
  success: true,
104
135
  results,
105
136
  total: results.length,
106
137
  timing: 0,
138
+ // The embedded prompt, so callers (the profile step) need not embed it again.
139
+ queryVector,
107
140
  ...(warnings.length > 0 ? { warnings } : {}),
108
141
  };
109
142
  }
@@ -15,5 +15,7 @@ interface MemoriesResponseMinimal {
15
15
  * order, and the first oversized memory is shortened with a visible marker.
16
16
  * Hosts emit several sections in one request by passing `maxBytes` residuals.
17
17
  */
18
- export declare function formatContextForPrompt(userId: string | null, projectMemories: MemoriesResponseMinimal, budget?: ContextBudgetOptions): Promise<string>;
18
+ export declare function formatContextForPrompt(userId: string | null, projectMemories: MemoriesResponseMinimal, budget?: ContextBudgetOptions,
19
+ /** The embedded prompt, when there is one. Lets the profile block follow the prompt. */
20
+ queryVector?: ArrayLike<number>): Promise<string>;
19
21
  export {};
@@ -13,10 +13,12 @@ const HEADER_TEXT = "The following block is reference context injected from the
13
13
  * order, and the first oversized memory is shortened with a visible marker.
14
14
  * Hosts emit several sections in one request by passing `maxBytes` residuals.
15
15
  */
16
- export async function formatContextForPrompt(userId, projectMemories, budget) {
16
+ export async function formatContextForPrompt(userId, projectMemories, budget,
17
+ /** The embedded prompt, when there is one. Lets the profile block follow the prompt. */
18
+ queryVector) {
17
19
  const blocks = [];
18
20
  if (CONFIG.injectProfile && userId) {
19
- const profileContext = await getUserProfileContext(userId);
21
+ const profileContext = await getUserProfileContext(userId, queryVector);
20
22
  if (profileContext) {
21
23
  blocks.push({
22
24
  prefix: "<user_profile>\n",