@aeriondyseti/vector-memory-mcp 1.1.0-dev.2 → 1.1.0-dev.3

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 (92) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/config/index.d.ts +17 -10
  3. package/dist/src/config/index.d.ts.map +1 -1
  4. package/dist/src/config/index.js +25 -11
  5. package/dist/src/config/index.js.map +1 -1
  6. package/dist/src/db/conversation.repository.d.ts +26 -0
  7. package/dist/src/db/conversation.repository.d.ts.map +1 -0
  8. package/dist/src/db/conversation.repository.js +72 -0
  9. package/dist/src/db/conversation.repository.js.map +1 -0
  10. package/dist/src/db/conversation.schema.d.ts +4 -0
  11. package/dist/src/db/conversation.schema.d.ts.map +1 -0
  12. package/dist/src/db/conversation.schema.js +15 -0
  13. package/dist/src/db/conversation.schema.js.map +1 -0
  14. package/dist/src/db/lancedb-utils.d.ts +13 -3
  15. package/dist/src/db/lancedb-utils.d.ts.map +1 -1
  16. package/dist/src/db/lancedb-utils.js +36 -7
  17. package/dist/src/db/lancedb-utils.js.map +1 -1
  18. package/dist/src/db/memory.repository.js +7 -7
  19. package/dist/src/db/memory.repository.js.map +1 -1
  20. package/dist/src/http/server.d.ts.map +1 -1
  21. package/dist/src/http/server.js +26 -7
  22. package/dist/src/http/server.js.map +1 -1
  23. package/dist/src/index.js +7 -6
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/mcp/handlers.d.ts +1 -1
  26. package/dist/src/mcp/handlers.d.ts.map +1 -1
  27. package/dist/src/mcp/handlers.js +106 -117
  28. package/dist/src/mcp/handlers.js.map +1 -1
  29. package/dist/src/mcp/tools.d.ts.map +1 -1
  30. package/dist/src/mcp/tools.js +43 -14
  31. package/dist/src/mcp/tools.js.map +1 -1
  32. package/dist/src/services/conversation.service.d.ts +38 -0
  33. package/dist/src/services/conversation.service.d.ts.map +1 -0
  34. package/dist/src/services/conversation.service.js +252 -0
  35. package/dist/src/services/conversation.service.js.map +1 -0
  36. package/dist/src/services/memory.service.d.ts +7 -25
  37. package/dist/src/services/memory.service.d.ts.map +1 -1
  38. package/dist/src/services/memory.service.js +66 -80
  39. package/dist/src/services/memory.service.js.map +1 -1
  40. package/dist/src/services/parsers/claude-code.parser.d.ts +8 -0
  41. package/dist/src/services/parsers/claude-code.parser.d.ts.map +1 -0
  42. package/dist/src/services/parsers/claude-code.parser.js +191 -0
  43. package/dist/src/services/parsers/claude-code.parser.js.map +1 -0
  44. package/dist/src/services/parsers/types.d.ts +9 -0
  45. package/dist/src/services/parsers/types.d.ts.map +1 -0
  46. package/dist/src/services/parsers/types.js +2 -0
  47. package/dist/src/services/parsers/types.js.map +1 -0
  48. package/dist/src/types/conversation.d.ts +99 -0
  49. package/dist/src/types/conversation.d.ts.map +1 -0
  50. package/dist/src/types/conversation.js +2 -0
  51. package/dist/src/types/conversation.js.map +1 -0
  52. package/hooks/session-start.ts +60 -42
  53. package/package.json +1 -1
  54. package/src/config/index.ts +39 -21
  55. package/src/db/conversation.repository.ts +120 -0
  56. package/src/db/conversation.schema.ts +33 -0
  57. package/src/db/lancedb-utils.ts +35 -7
  58. package/src/db/memory.repository.ts +7 -7
  59. package/src/http/server.ts +31 -7
  60. package/src/index.ts +10 -11
  61. package/src/mcp/handlers.ts +121 -123
  62. package/src/mcp/tools.ts +44 -15
  63. package/src/services/conversation.service.ts +354 -0
  64. package/src/services/memory.service.ts +101 -105
  65. package/src/services/parsers/claude-code.parser.ts +242 -0
  66. package/src/services/parsers/types.ts +14 -0
  67. package/src/types/conversation.ts +108 -0
  68. package/dist/src/db/conversation-history.repository.d.ts +0 -24
  69. package/dist/src/db/conversation-history.repository.d.ts.map +0 -1
  70. package/dist/src/db/conversation-history.repository.js +0 -184
  71. package/dist/src/db/conversation-history.repository.js.map +0 -1
  72. package/dist/src/db/conversation-history.schema.d.ts +0 -10
  73. package/dist/src/db/conversation-history.schema.d.ts.map +0 -1
  74. package/dist/src/db/conversation-history.schema.js +0 -31
  75. package/dist/src/db/conversation-history.schema.js.map +0 -1
  76. package/dist/src/services/conversation-history.service.d.ts +0 -64
  77. package/dist/src/services/conversation-history.service.d.ts.map +0 -1
  78. package/dist/src/services/conversation-history.service.js +0 -244
  79. package/dist/src/services/conversation-history.service.js.map +0 -1
  80. package/dist/src/services/session-parser.d.ts +0 -59
  81. package/dist/src/services/session-parser.d.ts.map +0 -1
  82. package/dist/src/services/session-parser.js +0 -147
  83. package/dist/src/services/session-parser.js.map +0 -1
  84. package/dist/src/types/conversation-history.d.ts +0 -74
  85. package/dist/src/types/conversation-history.d.ts.map +0 -1
  86. package/dist/src/types/conversation-history.js +0 -2
  87. package/dist/src/types/conversation-history.js.map +0 -1
  88. package/src/db/conversation-history.repository.ts +0 -255
  89. package/src/db/conversation-history.schema.ts +0 -40
  90. package/src/services/conversation-history.service.ts +0 -320
  91. package/src/services/session-parser.ts +0 -232
  92. package/src/types/conversation-history.ts +0 -82
@@ -0,0 +1,354 @@
1
+ import { createHash } from "crypto";
2
+ import { readFile, writeFile, mkdir } from "fs/promises";
3
+ import { dirname, join } from "path";
4
+ import type { ConversationRepository } from "../db/conversation.repository.js";
5
+ import type {
6
+ ConversationChunk,
7
+ ConversationHybridRow,
8
+ HistoryFilters,
9
+ IndexedSession,
10
+ ParsedMessage,
11
+ SessionFileInfo,
12
+ } from "../types/conversation.js";
13
+ import type { ConversationHistoryConfig } from "../config/index.js";
14
+ import { resolveSessionLogPath } from "../config/index.js";
15
+ import type { EmbeddingsService } from "./embeddings.service.js";
16
+ import type { SessionLogParser } from "./parsers/types.js";
17
+ import { ClaudeCodeSessionParser } from "./parsers/claude-code.parser.js";
18
+
19
+ /**
20
+ * Generate a deterministic chunk ID from session ID and message indices.
21
+ */
22
+ function chunkId(
23
+ sessionId: string,
24
+ startIdx: number,
25
+ endIdx: number
26
+ ): string {
27
+ return createHash("sha256")
28
+ .update(`${sessionId}:${startIdx}:${endIdx}`)
29
+ .digest("hex")
30
+ .slice(0, 32);
31
+ }
32
+
33
+ /**
34
+ * Group parsed messages into embeddable chunks.
35
+ */
36
+ export function chunkMessages(
37
+ messages: ParsedMessage[],
38
+ maxChunkMessages: number,
39
+ overlap: number
40
+ ): ConversationChunk[] {
41
+ if (messages.length === 0) return [];
42
+
43
+ const chunks: ConversationChunk[] = [];
44
+ let startIdx = 0;
45
+
46
+ while (startIdx < messages.length) {
47
+ const endIdx = Math.min(startIdx + maxChunkMessages, messages.length);
48
+ const chunkMsgs = messages.slice(startIdx, endIdx);
49
+
50
+ const firstRole = chunkMsgs[0].role;
51
+ const role = chunkMsgs.every((m) => m.role === firstRole)
52
+ ? firstRole
53
+ : "mixed";
54
+
55
+ const content = chunkMsgs
56
+ .map(
57
+ (m) =>
58
+ `[${m.role} @ ${m.timestamp.toISOString()}]: ${m.content}`
59
+ )
60
+ .join("\n\n");
61
+
62
+ const firstMsg = chunkMsgs[0];
63
+ const lastMsg = chunkMsgs[chunkMsgs.length - 1];
64
+
65
+ chunks.push({
66
+ id: chunkId(
67
+ firstMsg.sessionId,
68
+ firstMsg.messageIndex,
69
+ lastMsg.messageIndex
70
+ ),
71
+ content,
72
+ sessionId: firstMsg.sessionId,
73
+ timestamp: firstMsg.timestamp,
74
+ endTimestamp: lastMsg.timestamp,
75
+ role,
76
+ messageIndexStart: firstMsg.messageIndex,
77
+ messageIndexEnd: lastMsg.messageIndex,
78
+ project: firstMsg.project,
79
+ metadata: {
80
+ session_id: firstMsg.sessionId,
81
+ timestamp: firstMsg.timestamp.toISOString(),
82
+ role,
83
+ message_index_start: firstMsg.messageIndex,
84
+ message_index_end: lastMsg.messageIndex,
85
+ project: firstMsg.project,
86
+ git_branch: firstMsg.gitBranch,
87
+ is_subagent: firstMsg.isSubagent,
88
+ agent_id: firstMsg.agentId,
89
+ },
90
+ });
91
+
92
+ // Advance by (chunkSize - overlap), but always advance at least 1
93
+ const advance = Math.max(1, endIdx - startIdx - overlap);
94
+ startIdx += advance;
95
+ }
96
+
97
+ return chunks;
98
+ }
99
+
100
+ /** Serializable index state format */
101
+ interface IndexStateEntry {
102
+ sessionId: string;
103
+ filePath: string;
104
+ project: string;
105
+ lastModified: number;
106
+ chunkCount: number;
107
+ messageCount: number;
108
+ indexedAt: string;
109
+ firstMessageAt: string;
110
+ lastMessageAt: string;
111
+ }
112
+
113
+ export class ConversationHistoryService {
114
+ private indexStatePath: string;
115
+ private indexStateCache: Map<string, IndexedSession> | null = null;
116
+
117
+ constructor(
118
+ private repository: ConversationRepository,
119
+ private embeddings: EmbeddingsService,
120
+ public readonly config: ConversationHistoryConfig,
121
+ private dbPath: string,
122
+ private parser: SessionLogParser = new ClaudeCodeSessionParser()
123
+ ) {
124
+ this.indexStatePath = join(
125
+ dirname(dbPath),
126
+ "conversation_index_state.json"
127
+ );
128
+ }
129
+
130
+ private async loadIndexState(): Promise<Map<string, IndexedSession>> {
131
+ if (this.indexStateCache) return this.indexStateCache;
132
+ try {
133
+ const raw = await readFile(this.indexStatePath, "utf-8");
134
+ const entries: IndexStateEntry[] = JSON.parse(raw);
135
+ const map = new Map<string, IndexedSession>();
136
+ for (const e of entries) {
137
+ map.set(e.sessionId, {
138
+ sessionId: e.sessionId,
139
+ filePath: e.filePath,
140
+ project: e.project,
141
+ lastModified: e.lastModified,
142
+ chunkCount: e.chunkCount,
143
+ messageCount: e.messageCount,
144
+ indexedAt: new Date(e.indexedAt),
145
+ firstMessageAt: new Date(e.firstMessageAt),
146
+ lastMessageAt: new Date(e.lastMessageAt),
147
+ });
148
+ }
149
+ this.indexStateCache = map;
150
+ return map;
151
+ } catch {
152
+ const map = new Map<string, IndexedSession>();
153
+ this.indexStateCache = map;
154
+ return map;
155
+ }
156
+ }
157
+
158
+ private async saveIndexState(state: Map<string, IndexedSession>): Promise<void> {
159
+ const entries: IndexStateEntry[] = [...state.values()].map((s) => ({
160
+ sessionId: s.sessionId,
161
+ filePath: s.filePath,
162
+ project: s.project,
163
+ lastModified: s.lastModified,
164
+ chunkCount: s.chunkCount,
165
+ messageCount: s.messageCount,
166
+ indexedAt: s.indexedAt.toISOString(),
167
+ firstMessageAt: s.firstMessageAt.toISOString(),
168
+ lastMessageAt: s.lastMessageAt.toISOString(),
169
+ }));
170
+ await mkdir(dirname(this.indexStatePath), { recursive: true });
171
+ await writeFile(this.indexStatePath, JSON.stringify(entries, null, 2));
172
+ this.indexStateCache = state;
173
+ }
174
+
175
+ async indexConversations(
176
+ path?: string,
177
+ since?: Date
178
+ ): Promise<{ indexed: number; skipped: number; errors: string[] }> {
179
+ if (!this.config.enabled) {
180
+ return {
181
+ indexed: 0,
182
+ skipped: 0,
183
+ errors: ["Conversation history indexing is not enabled"],
184
+ };
185
+ }
186
+
187
+ const logPath = path ?? resolveSessionLogPath(this.config);
188
+ if (!logPath) {
189
+ return {
190
+ indexed: 0,
191
+ skipped: 0,
192
+ errors: ["No session log path configured or detected"],
193
+ };
194
+ }
195
+
196
+ const sessionFiles = await this.parser.findSessionFiles(
197
+ logPath,
198
+ since,
199
+ this.config.indexSubagents
200
+ );
201
+ const indexState = await this.loadIndexState();
202
+
203
+ let indexed = 0;
204
+ let skipped = 0;
205
+ const errors: string[] = [];
206
+
207
+ for (const file of sessionFiles) {
208
+ const existing = indexState.get(file.sessionId);
209
+ if (existing && existing.lastModified >= file.lastModified.getTime()) {
210
+ skipped++;
211
+ continue;
212
+ }
213
+
214
+ try {
215
+ await this.indexSession(file, indexState);
216
+ indexed++;
217
+ } catch (err) {
218
+ errors.push(
219
+ `${file.sessionId}: ${err instanceof Error ? err.message : String(err)}`
220
+ );
221
+ }
222
+ }
223
+
224
+ await this.saveIndexState(indexState);
225
+ return { indexed, skipped, errors };
226
+ }
227
+
228
+ private async indexSession(
229
+ file: SessionFileInfo,
230
+ indexState: Map<string, IndexedSession>
231
+ ): Promise<void> {
232
+ const messages = await this.parser.parse(
233
+ file.filePath,
234
+ this.config.indexSubagents
235
+ );
236
+ if (messages.length === 0) {
237
+ // Still track it so we don't re-attempt
238
+ indexState.set(file.sessionId, {
239
+ sessionId: file.sessionId,
240
+ filePath: file.filePath,
241
+ project: file.project,
242
+ lastModified: file.lastModified.getTime(),
243
+ chunkCount: 0,
244
+ messageCount: 0,
245
+ indexedAt: new Date(),
246
+ firstMessageAt: file.lastModified,
247
+ lastMessageAt: file.lastModified,
248
+ });
249
+ return;
250
+ }
251
+
252
+ const chunks = chunkMessages(
253
+ messages,
254
+ this.config.maxChunkMessages,
255
+ this.config.chunkOverlap
256
+ );
257
+
258
+ // Delete existing chunks for re-indexing
259
+ await this.repository.deleteBySessionId(file.sessionId);
260
+
261
+ // Embed all chunks
262
+ const embeddings = await this.embeddings.embedBatch(
263
+ chunks.map((c) => c.content)
264
+ );
265
+
266
+ // Insert all chunks
267
+ const rows = chunks.map((chunk, i) => ({
268
+ id: chunk.id,
269
+ vector: embeddings[i],
270
+ content: chunk.content,
271
+ metadata: JSON.stringify(chunk.metadata),
272
+ created_at: chunk.timestamp.getTime(),
273
+ session_id: chunk.sessionId,
274
+ role: chunk.role,
275
+ message_index_start: chunk.messageIndexStart,
276
+ message_index_end: chunk.messageIndexEnd,
277
+ project: chunk.project,
278
+ }));
279
+
280
+ await this.repository.insertBatch(rows);
281
+
282
+ // Update index state
283
+ indexState.set(file.sessionId, {
284
+ sessionId: file.sessionId,
285
+ filePath: file.filePath,
286
+ project: file.project,
287
+ lastModified: file.lastModified.getTime(),
288
+ chunkCount: chunks.length,
289
+ messageCount: messages.length,
290
+ indexedAt: new Date(),
291
+ firstMessageAt: messages[0].timestamp,
292
+ lastMessageAt: messages[messages.length - 1].timestamp,
293
+ });
294
+ }
295
+
296
+ async reindexSession(
297
+ sessionId: string
298
+ ): Promise<{ success: boolean; chunkCount: number; error?: string }> {
299
+ if (!this.config.enabled) {
300
+ return {
301
+ success: false,
302
+ chunkCount: 0,
303
+ error: "Conversation history indexing is not enabled",
304
+ };
305
+ }
306
+
307
+ const indexState = await this.loadIndexState();
308
+ const existing = indexState.get(sessionId);
309
+ if (!existing) {
310
+ return {
311
+ success: false,
312
+ chunkCount: 0,
313
+ error: "Session not found in index state",
314
+ };
315
+ }
316
+
317
+ // Construct session info for re-indexing
318
+ const file: SessionFileInfo = {
319
+ filePath: existing.filePath,
320
+ sessionId,
321
+ project: existing.project,
322
+ lastModified: new Date(),
323
+ };
324
+
325
+ await this.indexSession(file, indexState);
326
+ await this.saveIndexState(indexState);
327
+
328
+ const updated = indexState.get(sessionId)!;
329
+ return { success: true, chunkCount: updated.chunkCount };
330
+ }
331
+
332
+ async listIndexedSessions(
333
+ limit: number = 20,
334
+ offset: number = 0
335
+ ): Promise<{ sessions: IndexedSession[]; total: number }> {
336
+ const indexState = await this.loadIndexState();
337
+ const sessions = [...indexState.values()].sort(
338
+ (a, b) => b.indexedAt.getTime() - a.indexedAt.getTime()
339
+ );
340
+ return {
341
+ sessions: sessions.slice(offset, offset + limit),
342
+ total: sessions.length,
343
+ };
344
+ }
345
+
346
+ async searchHistory(
347
+ query: string,
348
+ embedding: number[],
349
+ limit: number,
350
+ filters?: HistoryFilters
351
+ ): Promise<ConversationHybridRow[]> {
352
+ return this.repository.findHybrid(embedding, query, limit, filters);
353
+ }
354
+ }
@@ -1,10 +1,10 @@
1
1
  import { randomUUID } from "crypto";
2
- import type { Memory, SearchIntent, IntentProfile } from "../types/memory.js";
2
+ import type { Memory, SearchIntent, IntentProfile, HybridRow } from "../types/memory.js";
3
3
  import { isDeleted } from "../types/memory.js";
4
+ import type { SearchResult, SearchOptions } from "../types/conversation.js";
4
5
  import type { MemoryRepository } from "../db/memory.repository.js";
5
6
  import type { EmbeddingsService } from "./embeddings.service.js";
6
- import type { ConversationHistoryService } from "./conversation-history.service.js";
7
- import type { SearchResult, MemorySearchResult } from "../types/conversation-history.js";
7
+ import type { ConversationHistoryService } from "./conversation.service.js";
8
8
 
9
9
  const INTENT_PROFILES: Record<SearchIntent, IntentProfile> = {
10
10
  continuity: { weights: { relevance: 0.3, recency: 0.5, utility: 0.2 }, jitter: 0.02 },
@@ -17,29 +17,19 @@ const INTENT_PROFILES: Record<SearchIntent, IntentProfile> = {
17
17
  const sigmoid = (x: number): number => 1 / (1 + Math.exp(-x));
18
18
 
19
19
  export class MemoryService {
20
- private historyService: ConversationHistoryService | null = null;
21
- private historyWeight: number = 0.5;
20
+ private conversationService: ConversationHistoryService | null = null;
22
21
 
23
22
  constructor(
24
23
  private repository: MemoryRepository,
25
24
  private embeddings: EmbeddingsService
26
- ) { }
27
-
28
- /**
29
- * Optionally wire conversation history for unified search.
30
- * Called from index.ts when conversationHistory.enabled is true.
31
- */
32
- setConversationHistory(service: ConversationHistoryService, weight: number): void {
33
- this.historyService = service;
34
- this.historyWeight = weight;
25
+ ) {}
26
+
27
+ setConversationService(service: ConversationHistoryService): void {
28
+ this.conversationService = service;
35
29
  }
36
30
 
37
- /**
38
- * Access the conversation history service (if wired).
39
- * Used by MCP handlers for index/list/reindex tools.
40
- */
41
- getConversationHistory(): ConversationHistoryService | null {
42
- return this.historyService;
31
+ getConversationService(): ConversationHistoryService | null {
32
+ return this.conversationService;
43
33
  }
44
34
 
45
35
  async store(
@@ -144,103 +134,109 @@ export class MemoryService {
144
134
  return updatedMemory;
145
135
  }
146
136
 
147
- async search(
148
- query: string,
149
- intent: SearchIntent,
150
- limit: number = 10,
151
- includeDeleted: boolean = false
152
- ): Promise<Memory[]> {
153
- const queryEmbedding = await this.embeddings.embed(query);
154
- const fetchLimit = limit * 5; // Fetch more for re-ranking
155
-
156
- const candidates = await this.repository.findHybrid(queryEmbedding, query, fetchLimit);
157
- const profile = INTENT_PROFILES[intent];
158
- const now = new Date();
159
-
160
- const scored = candidates
161
- .filter((m) => includeDeleted || !isDeleted(m))
162
- .map((candidate) => {
163
- // Relevance: RRF score (already normalized ~0-1)
164
- const relevance = candidate.rrfScore;
165
-
166
- // Recency: exponential decay
167
- const lastAccessed = candidate.lastAccessed ?? candidate.createdAt;
168
- const hoursSinceAccess = Math.max(0, (now.getTime() - lastAccessed.getTime()) / (1000 * 60 * 60));
169
- const recency = Math.pow(0.995, hoursSinceAccess);
170
-
171
- // Utility: sigmoid of usefulness + log(accessCount)
172
- const utility = sigmoid((candidate.usefulness + Math.log(candidate.accessCount + 1)) / 5);
173
-
174
- // Weighted score
175
- const { weights, jitter } = profile;
176
- const score =
177
- weights.relevance * relevance +
178
- weights.recency * recency +
179
- weights.utility * utility;
180
-
181
- // Apply jitter
182
- const finalScore = score * (1 + (Math.random() * 2 - 1) * jitter);
183
-
184
- return { memory: candidate as Memory, finalScore };
185
- });
186
-
187
- // Sort by final score descending
188
- scored.sort((a, b) => b.finalScore - a.finalScore);
189
-
190
- // Return top N (read-only - no access tracking)
191
- return scored.slice(0, limit).map((s) => s.memory);
137
+ private computeMemoryScore(
138
+ candidate: HybridRow,
139
+ profile: IntentProfile,
140
+ now: Date
141
+ ): number {
142
+ const relevance = candidate.rrfScore;
143
+ const lastAccessed = candidate.lastAccessed ?? candidate.createdAt;
144
+ const hoursSinceAccess = Math.max(
145
+ 0,
146
+ (now.getTime() - lastAccessed.getTime()) / (1000 * 60 * 60)
147
+ );
148
+ const recency = Math.pow(0.995, hoursSinceAccess);
149
+ const utility = sigmoid(
150
+ (candidate.usefulness + Math.log(candidate.accessCount + 1)) / 5
151
+ );
152
+ const { weights, jitter } = profile;
153
+ const score =
154
+ weights.relevance * relevance +
155
+ weights.recency * recency +
156
+ weights.utility * utility;
157
+ return score * (1 + (Math.random() * 2 - 1) * jitter);
192
158
  }
193
159
 
194
- /**
195
- * Search across both memories and conversation history (if enabled).
196
- * Returns a merged, score-normalized list sorted by relevance.
197
- *
198
- * If no history service is configured, falls back to memory-only search
199
- * wrapped as MemorySearchResult[].
200
- */
201
- async searchUnified(
160
+ async search(
202
161
  query: string,
203
162
  intent: SearchIntent,
204
163
  limit: number = 10,
205
164
  includeDeleted: boolean = false,
165
+ options?: SearchOptions
206
166
  ): Promise<SearchResult[]> {
207
- if (!this.historyService) {
208
- const memories = await this.search(query, intent, limit, includeDeleted);
209
- return this.toMemoryResults(memories, limit);
210
- }
167
+ const queryEmbedding = await this.embeddings.embed(query);
168
+ const profile = INTENT_PROFILES[intent];
169
+ const now = new Date();
211
170
 
212
- // Run both searches in parallel
213
- const [memories, historyResults] = await Promise.all([
214
- this.search(query, intent, limit, includeDeleted),
215
- this.historyService.search(query, limit),
171
+ const hasConversationService = this.conversationService !== null;
172
+ const historyOnly = (options?.historyOnly ?? false) && hasConversationService;
173
+ const includeHistory =
174
+ (options?.includeHistory ?? true) && hasConversationService;
175
+ const historyWeight =
176
+ options?.historyWeight ??
177
+ this.conversationService?.config.historyWeight ??
178
+ 0.75;
179
+
180
+ // Run memory + history queries in parallel
181
+ const memoryPromise =
182
+ !historyOnly
183
+ ? this.repository
184
+ .findHybrid(queryEmbedding, query, limit * 5)
185
+ .then((candidates) =>
186
+ candidates
187
+ .filter((m) => includeDeleted || !isDeleted(m))
188
+ .map((candidate) => ({
189
+ id: candidate.id,
190
+ content: candidate.content,
191
+ metadata: candidate.metadata,
192
+ createdAt: candidate.createdAt,
193
+ updatedAt: candidate.updatedAt,
194
+ source: "memory" as const,
195
+ score: this.computeMemoryScore(candidate, profile, now),
196
+ supersededBy: candidate.supersededBy,
197
+ usefulness: candidate.usefulness,
198
+ accessCount: candidate.accessCount,
199
+ lastAccessed: candidate.lastAccessed,
200
+ }))
201
+ )
202
+ : Promise.resolve([] as SearchResult[]);
203
+
204
+ const historyPromise =
205
+ includeHistory || historyOnly
206
+ ? this.conversationService!
207
+ .searchHistory(
208
+ query,
209
+ queryEmbedding,
210
+ historyOnly ? limit * 5 : limit * 3,
211
+ options?.historyFilters
212
+ )
213
+ .then((historyRows) =>
214
+ historyRows.map((row) => ({
215
+ id: row.id,
216
+ content: row.content,
217
+ metadata: row.metadata,
218
+ createdAt: row.createdAt,
219
+ updatedAt: row.createdAt,
220
+ source: "conversation_history" as const,
221
+ score: row.rrfScore * historyWeight,
222
+ sessionId: (row.metadata?.session_id as string) ?? "",
223
+ role: (row.metadata?.role as string) ?? "unknown",
224
+ messageIndexStart: (row.metadata?.message_index_start as number) ?? 0,
225
+ messageIndexEnd: (row.metadata?.message_index_end as number) ?? 0,
226
+ }))
227
+ )
228
+ : Promise.resolve([] as SearchResult[]);
229
+
230
+ const [memoryResults, historyResults] = await Promise.all([
231
+ memoryPromise,
232
+ historyPromise,
216
233
  ]);
217
234
 
218
- const memoryResults = this.toMemoryResults(memories, limit);
219
-
220
- // Apply history weight to RRF scores
221
- const weightedHistory: SearchResult[] = historyResults.map((h) => ({
222
- ...h,
223
- score: h.score * this.historyWeight,
224
- }));
225
-
226
- // Merge, sort by score descending, take top N
227
- const merged = [...memoryResults, ...weightedHistory];
235
+ // Merge and sort by score descending
236
+ const merged = [...memoryResults, ...historyResults];
228
237
  merged.sort((a, b) => b.score - a.score);
229
- return merged.slice(0, limit);
230
- }
231
238
 
232
- /** Convert pre-sorted Memory[] to MemorySearchResult[] with positional scores. */
233
- private toMemoryResults(memories: Memory[], limit: number): MemorySearchResult[] {
234
- return memories.map((m, rank) => ({
235
- source: "memory" as const,
236
- id: m.id,
237
- content: m.content,
238
- metadata: m.metadata,
239
- score: (limit - rank) / limit,
240
- createdAt: m.createdAt,
241
- updatedAt: m.updatedAt,
242
- supersededBy: m.supersededBy,
243
- }));
239
+ return merged.slice(0, limit);
244
240
  }
245
241
 
246
242
  async trackAccess(ids: string[]): Promise<void> {