@aeriondyseti/vector-memory-mcp 2.4.0 → 2.4.4-dev.1
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.
- package/package.json +1 -1
- package/server/core/connection.ts +1 -1
- package/server/core/conversation.repository.ts +113 -16
- package/server/core/conversation.service.ts +19 -19
- package/server/core/conversation.ts +7 -5
- package/server/core/embeddings.service.ts +6 -0
- package/server/core/memory.repository.ts +49 -14
- package/server/core/memory.service.ts +47 -42
- package/server/core/memory.ts +40 -1
- package/server/core/migration.service.ts +3 -3
- package/server/core/migrations.ts +17 -35
- package/server/core/parsers/claude-code.parser.ts +3 -3
- package/server/core/parsers/types.ts +1 -1
- package/server/core/sqlite-utils.ts +67 -2
- package/server/index.ts +10 -10
- package/server/transports/http/mcp-transport.ts +5 -5
- package/server/transports/http/server.ts +7 -6
- package/server/transports/mcp/handlers.ts +47 -23
- package/server/transports/mcp/server.ts +5 -5
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import { existsSync, mkdirSync } from "fs";
|
|
3
3
|
import { dirname } from "path";
|
|
4
|
-
import { removeVec0Tables, runMigrations } from "./migrations
|
|
4
|
+
import { removeVec0Tables, runMigrations } from "./migrations";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Open (or create) a SQLite database at the given path
|
|
@@ -2,15 +2,15 @@ import type { Database } from "bun:sqlite";
|
|
|
2
2
|
import type {
|
|
3
3
|
ConversationHybridRow,
|
|
4
4
|
HistoryFilters,
|
|
5
|
-
} from "./conversation
|
|
5
|
+
} from "./conversation";
|
|
6
6
|
import {
|
|
7
7
|
serializeVector,
|
|
8
8
|
safeParseJsonObject,
|
|
9
9
|
sanitizeFtsQuery,
|
|
10
|
-
|
|
10
|
+
hybridRRFWithSignals,
|
|
11
11
|
topByRRF,
|
|
12
12
|
knnSearch,
|
|
13
|
-
} from "./sqlite-utils
|
|
13
|
+
} from "./sqlite-utils";
|
|
14
14
|
|
|
15
15
|
export class ConversationRepository {
|
|
16
16
|
constructor(private db: Database) {}
|
|
@@ -105,13 +105,102 @@ export class ConversationRepository {
|
|
|
105
105
|
tx();
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
async replaceSession(
|
|
109
|
+
sessionId: string,
|
|
110
|
+
rows: Array<{
|
|
111
|
+
id: string;
|
|
112
|
+
vector: number[];
|
|
113
|
+
content: string;
|
|
114
|
+
metadata: string;
|
|
115
|
+
created_at: number;
|
|
116
|
+
session_id: string;
|
|
117
|
+
role: string;
|
|
118
|
+
message_index_start: number;
|
|
119
|
+
message_index_end: number;
|
|
120
|
+
project: string;
|
|
121
|
+
}>
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
const insertMain = this.db.prepare(
|
|
124
|
+
`INSERT OR REPLACE INTO conversation_history
|
|
125
|
+
(id, content, metadata, created_at, session_id, role, message_index_start, message_index_end, project)
|
|
126
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
127
|
+
);
|
|
128
|
+
const deleteVec = this.db.prepare(
|
|
129
|
+
`DELETE FROM conversation_history_vec WHERE id = ?`
|
|
130
|
+
);
|
|
131
|
+
const insertVec = this.db.prepare(
|
|
132
|
+
`INSERT INTO conversation_history_vec (id, vector) VALUES (?, ?)`
|
|
133
|
+
);
|
|
134
|
+
const deleteFts = this.db.prepare(
|
|
135
|
+
`DELETE FROM conversation_history_fts WHERE id = ?`
|
|
136
|
+
);
|
|
137
|
+
const insertFts = this.db.prepare(
|
|
138
|
+
`INSERT INTO conversation_history_fts (id, content) VALUES (?, ?)`
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const tx = this.db.transaction(() => {
|
|
142
|
+
// Delete old chunks first
|
|
143
|
+
const idRows = this.db
|
|
144
|
+
.prepare(`SELECT id FROM conversation_history WHERE session_id = ?`)
|
|
145
|
+
.all(sessionId) as Array<{ id: string }>;
|
|
146
|
+
|
|
147
|
+
if (idRows.length > 0) {
|
|
148
|
+
const ids = idRows.map((r) => r.id);
|
|
149
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
150
|
+
this.db
|
|
151
|
+
.prepare(
|
|
152
|
+
`DELETE FROM conversation_history_vec WHERE id IN (${placeholders})`
|
|
153
|
+
)
|
|
154
|
+
.run(...ids);
|
|
155
|
+
this.db
|
|
156
|
+
.prepare(
|
|
157
|
+
`DELETE FROM conversation_history_fts WHERE id IN (${placeholders})`
|
|
158
|
+
)
|
|
159
|
+
.run(...ids);
|
|
160
|
+
this.db
|
|
161
|
+
.prepare(`DELETE FROM conversation_history WHERE session_id = ?`)
|
|
162
|
+
.run(sessionId);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Insert new chunks
|
|
166
|
+
for (const row of rows) {
|
|
167
|
+
insertMain.run(
|
|
168
|
+
row.id,
|
|
169
|
+
row.content,
|
|
170
|
+
row.metadata,
|
|
171
|
+
row.created_at,
|
|
172
|
+
row.session_id,
|
|
173
|
+
row.role,
|
|
174
|
+
row.message_index_start,
|
|
175
|
+
row.message_index_end,
|
|
176
|
+
row.project
|
|
177
|
+
);
|
|
178
|
+
deleteVec.run(row.id);
|
|
179
|
+
insertVec.run(row.id, serializeVector(row.vector));
|
|
180
|
+
deleteFts.run(row.id);
|
|
181
|
+
insertFts.run(row.id, row.content);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
tx();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Hybrid search combining vector KNN and FTS5, fused with Reciprocal Rank Fusion.
|
|
190
|
+
*
|
|
191
|
+
* NOTE: Filters (session, role, project, date) are applied AFTER candidate selection
|
|
192
|
+
* and RRF scoring, not pushed into the KNN/FTS queries. This is an intentional
|
|
193
|
+
* performance tradeoff — KNN is brute-force JS-side (no SQL pre-filter possible),
|
|
194
|
+
* and filtering post-RRF avoids duplicating filter logic across both retrieval paths.
|
|
195
|
+
* The consequence is that filtered queries may return fewer than `limit` results.
|
|
196
|
+
*/
|
|
108
197
|
async findHybrid(
|
|
109
198
|
embedding: number[],
|
|
110
199
|
query: string,
|
|
111
200
|
limit: number,
|
|
112
201
|
filters?: HistoryFilters
|
|
113
202
|
): Promise<ConversationHybridRow[]> {
|
|
114
|
-
const candidateCount = limit *
|
|
203
|
+
const candidateCount = limit * 5;
|
|
115
204
|
|
|
116
205
|
// Vector KNN search (brute-force cosine similarity in JS)
|
|
117
206
|
const vecResults = knnSearch(this.db, "conversation_history_vec", embedding, candidateCount);
|
|
@@ -127,8 +216,10 @@ export class ConversationRepository {
|
|
|
127
216
|
)
|
|
128
217
|
.all(ftsQuery, candidateCount) as Array<{ id: string }>;
|
|
129
218
|
|
|
130
|
-
// Compute RRF scores
|
|
131
|
-
const
|
|
219
|
+
// Compute RRF scores with search signals for confidence scoring
|
|
220
|
+
const signalsMap = hybridRRFWithSignals(vecResults, ftsResults);
|
|
221
|
+
const rrfScores = new Map<string, number>();
|
|
222
|
+
for (const [id, s] of signalsMap) rrfScores.set(id, s.rrfScore);
|
|
132
223
|
const topIds = topByRRF(rrfScores, limit);
|
|
133
224
|
|
|
134
225
|
if (topIds.length === 0) return [];
|
|
@@ -185,17 +276,23 @@ export class ConversationRepository {
|
|
|
185
276
|
project: string;
|
|
186
277
|
}>;
|
|
187
278
|
|
|
188
|
-
// Build a lookup for ordering by RRF score
|
|
189
|
-
const scoreMap = new Map(topIds.map((id) => [id, rrfScores.get(id)!]));
|
|
190
|
-
|
|
191
279
|
return fullRows
|
|
192
|
-
.map((row) =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
280
|
+
.map((row) => {
|
|
281
|
+
const signals = signalsMap.get(row.id)!;
|
|
282
|
+
return {
|
|
283
|
+
id: row.id,
|
|
284
|
+
content: row.content,
|
|
285
|
+
metadata: safeParseJsonObject(row.metadata),
|
|
286
|
+
createdAt: new Date(row.created_at),
|
|
287
|
+
rrfScore: signals.rrfScore,
|
|
288
|
+
signals: {
|
|
289
|
+
cosineSimilarity: signals.cosineSimilarity,
|
|
290
|
+
ftsMatch: signals.ftsMatch,
|
|
291
|
+
knnRank: signals.knnRank,
|
|
292
|
+
ftsRank: signals.ftsRank,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
})
|
|
199
296
|
.sort((a, b) => b.rrfScore - a.rrfScore);
|
|
200
297
|
}
|
|
201
298
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
2
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
3
3
|
import { dirname, join } from "path";
|
|
4
|
-
import type { ConversationRepository } from "./conversation.repository
|
|
4
|
+
import type { ConversationRepository } from "./conversation.repository";
|
|
5
5
|
import type {
|
|
6
6
|
ConversationChunk,
|
|
7
7
|
ConversationHybridRow,
|
|
@@ -10,12 +10,12 @@ import type {
|
|
|
10
10
|
ParsedMessage,
|
|
11
11
|
SessionFileInfo,
|
|
12
12
|
SessionIndexDetail,
|
|
13
|
-
} from "./conversation
|
|
14
|
-
import type { ConversationHistoryConfig } from "../config/index
|
|
15
|
-
import { resolveSessionLogPath } from "../config/index
|
|
16
|
-
import type { EmbeddingsService } from "./embeddings.service
|
|
17
|
-
import type { SessionLogParser } from "./parsers/types
|
|
18
|
-
import { ClaudeCodeSessionParser } from "./parsers/claude-code.parser
|
|
13
|
+
} from "./conversation";
|
|
14
|
+
import type { ConversationHistoryConfig } from "../config/index";
|
|
15
|
+
import { resolveSessionLogPath } from "../config/index";
|
|
16
|
+
import type { EmbeddingsService } from "./embeddings.service";
|
|
17
|
+
import type { SessionLogParser } from "./parsers/types";
|
|
18
|
+
import { ClaudeCodeSessionParser } from "./parsers/claude-code.parser";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Generate a deterministic chunk ID from session ID and message indices.
|
|
@@ -78,12 +78,7 @@ export function chunkMessages(
|
|
|
78
78
|
messageIndexEnd: lastMsg.messageIndex,
|
|
79
79
|
project: firstMsg.project,
|
|
80
80
|
metadata: {
|
|
81
|
-
session_id: firstMsg.sessionId,
|
|
82
81
|
timestamp: firstMsg.timestamp.toISOString(),
|
|
83
|
-
role,
|
|
84
|
-
message_index_start: firstMsg.messageIndex,
|
|
85
|
-
message_index_end: lastMsg.messageIndex,
|
|
86
|
-
project: firstMsg.project,
|
|
87
82
|
git_branch: firstMsg.gitBranch,
|
|
88
83
|
is_subagent: firstMsg.isSubagent,
|
|
89
84
|
agent_id: firstMsg.agentId,
|
|
@@ -273,20 +268,24 @@ export class ConversationHistoryService {
|
|
|
273
268
|
this.config.chunkOverlap
|
|
274
269
|
);
|
|
275
270
|
|
|
276
|
-
//
|
|
277
|
-
await this.repository.deleteBySessionId(file.sessionId);
|
|
278
|
-
|
|
279
|
-
// Embed all chunks
|
|
271
|
+
// Embed all chunks FIRST (pure computation, no DB side effects)
|
|
280
272
|
const embeddings = await this.embeddings.embedBatch(
|
|
281
273
|
chunks.map((c) => c.content)
|
|
282
274
|
);
|
|
283
275
|
|
|
284
|
-
//
|
|
276
|
+
// Build rows
|
|
285
277
|
const rows = chunks.map((chunk, i) => ({
|
|
286
278
|
id: chunk.id,
|
|
287
279
|
vector: embeddings[i],
|
|
288
280
|
content: chunk.content,
|
|
289
|
-
metadata: JSON.stringify(
|
|
281
|
+
metadata: JSON.stringify({
|
|
282
|
+
...chunk.metadata,
|
|
283
|
+
session_id: chunk.sessionId,
|
|
284
|
+
role: chunk.role,
|
|
285
|
+
message_index_start: chunk.messageIndexStart,
|
|
286
|
+
message_index_end: chunk.messageIndexEnd,
|
|
287
|
+
project: chunk.project,
|
|
288
|
+
}),
|
|
290
289
|
created_at: chunk.timestamp.getTime(),
|
|
291
290
|
session_id: chunk.sessionId,
|
|
292
291
|
role: chunk.role,
|
|
@@ -295,7 +294,8 @@ export class ConversationHistoryService {
|
|
|
295
294
|
project: chunk.project,
|
|
296
295
|
}));
|
|
297
296
|
|
|
298
|
-
|
|
297
|
+
// Atomically replace old chunks with new ones
|
|
298
|
+
await this.repository.replaceSession(file.sessionId, rows);
|
|
299
299
|
|
|
300
300
|
// Update index state
|
|
301
301
|
const session: IndexedSession = {
|
|
@@ -14,12 +14,7 @@ export interface ParsedMessage {
|
|
|
14
14
|
|
|
15
15
|
/** Metadata stored per conversation chunk in the database */
|
|
16
16
|
export interface ConversationChunkMetadata {
|
|
17
|
-
session_id: string;
|
|
18
17
|
timestamp: string;
|
|
19
|
-
role: string;
|
|
20
|
-
message_index_start: number;
|
|
21
|
-
message_index_end: number;
|
|
22
|
-
project: string;
|
|
23
18
|
git_branch?: string;
|
|
24
19
|
is_subagent: boolean;
|
|
25
20
|
agent_id?: string;
|
|
@@ -52,6 +47,8 @@ export interface IndexedSession {
|
|
|
52
47
|
lastMessageAt: Date;
|
|
53
48
|
}
|
|
54
49
|
|
|
50
|
+
import type { SearchSignals } from "./memory";
|
|
51
|
+
|
|
55
52
|
/** Raw row from conversation_history table with RRF score */
|
|
56
53
|
export interface ConversationHybridRow {
|
|
57
54
|
id: string;
|
|
@@ -59,6 +56,7 @@ export interface ConversationHybridRow {
|
|
|
59
56
|
metadata: Record<string, unknown>;
|
|
60
57
|
createdAt: Date;
|
|
61
58
|
rrfScore: number;
|
|
59
|
+
signals: SearchSignals;
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
/** Unified search result with source provenance */
|
|
@@ -70,6 +68,8 @@ export interface SearchResult {
|
|
|
70
68
|
updatedAt: Date;
|
|
71
69
|
source: "memory" | "conversation_history";
|
|
72
70
|
score: number;
|
|
71
|
+
/** Absolute relevance confidence (0.0-1.0). Based on cosine similarity + retrieval agreement. */
|
|
72
|
+
confidence: number;
|
|
73
73
|
// Memory-specific fields
|
|
74
74
|
supersededBy: string | null;
|
|
75
75
|
usefulness?: number;
|
|
@@ -114,6 +114,8 @@ export interface HistoryFilters {
|
|
|
114
114
|
|
|
115
115
|
/** Options for the integrated search across both sources */
|
|
116
116
|
export interface SearchOptions {
|
|
117
|
+
limit?: number;
|
|
118
|
+
includeDeleted?: boolean;
|
|
117
119
|
includeHistory?: boolean;
|
|
118
120
|
historyOnly?: boolean;
|
|
119
121
|
historyWeight?: number;
|
|
@@ -108,6 +108,12 @@ export class EmbeddingsService {
|
|
|
108
108
|
|
|
109
109
|
private meanPool(data: Float32Array, mask: number[], seqLen: number): number[] {
|
|
110
110
|
const dim = this._dimension;
|
|
111
|
+
const expectedLen = seqLen * dim;
|
|
112
|
+
if (data.length < expectedLen) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`ONNX output size ${data.length} < expected ${expectedLen} (seqLen=${seqLen}, dim=${dim}). Model/dimension mismatch?`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
111
117
|
const pooled = new Array(dim).fill(0);
|
|
112
118
|
let maskSum = 0;
|
|
113
119
|
for (let t = 0; t < seqLen; t++) {
|
|
@@ -4,15 +4,17 @@ import {
|
|
|
4
4
|
deserializeVector,
|
|
5
5
|
safeParseJsonObject,
|
|
6
6
|
sanitizeFtsQuery,
|
|
7
|
-
|
|
7
|
+
hybridRRFWithSignals,
|
|
8
8
|
topByRRF,
|
|
9
9
|
knnSearch,
|
|
10
|
-
|
|
10
|
+
batchedQuery,
|
|
11
|
+
SQLITE_BATCH_SIZE,
|
|
12
|
+
} from "./sqlite-utils";
|
|
11
13
|
import {
|
|
12
14
|
type Memory,
|
|
13
15
|
type HybridRow,
|
|
14
16
|
DELETED_TOMBSTONE,
|
|
15
|
-
} from "./memory
|
|
17
|
+
} from "./memory";
|
|
16
18
|
|
|
17
19
|
export class MemoryRepository {
|
|
18
20
|
constructor(private db: Database) {}
|
|
@@ -144,14 +146,16 @@ export class MemoryRepository {
|
|
|
144
146
|
async findByIds(ids: string[]): Promise<Memory[]> {
|
|
145
147
|
if (ids.length === 0) return [];
|
|
146
148
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
return batchedQuery(this.db, ids, (batch) => {
|
|
150
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
151
|
+
const rows = this.db
|
|
152
|
+
.prepare(`SELECT * FROM memories WHERE id IN (${placeholders})`)
|
|
153
|
+
.all(...batch) as Array<Record<string, unknown>>;
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
return rows.map((row) => {
|
|
156
|
+
const embedding = this.getEmbedding(row.id as string);
|
|
157
|
+
return this.rowToMemory(row, embedding);
|
|
158
|
+
});
|
|
155
159
|
});
|
|
156
160
|
}
|
|
157
161
|
|
|
@@ -165,6 +169,28 @@ export class MemoryRepository {
|
|
|
165
169
|
return result.changes > 0;
|
|
166
170
|
}
|
|
167
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Increment access_count and update last_accessed for multiple memories in batch.
|
|
174
|
+
* Uses batched IN clauses to stay within SQLite parameter limits.
|
|
175
|
+
*/
|
|
176
|
+
bulkUpdateAccess(ids: string[], now: Date): void {
|
|
177
|
+
if (ids.length === 0) return;
|
|
178
|
+
const ts = now.getTime();
|
|
179
|
+
|
|
180
|
+
const runBatch = (batch: string[]) => {
|
|
181
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
182
|
+
this.db
|
|
183
|
+
.prepare(
|
|
184
|
+
`UPDATE memories SET access_count = access_count + 1, last_accessed = ? WHERE id IN (${placeholders})`
|
|
185
|
+
)
|
|
186
|
+
.run(ts, ...batch);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < ids.length; i += SQLITE_BATCH_SIZE) {
|
|
190
|
+
runBatch(ids.slice(i, i + SQLITE_BATCH_SIZE));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
168
194
|
/**
|
|
169
195
|
* Hybrid search combining vector KNN and FTS5, fused with Reciprocal Rank Fusion.
|
|
170
196
|
*/
|
|
@@ -173,7 +199,7 @@ export class MemoryRepository {
|
|
|
173
199
|
query: string,
|
|
174
200
|
limit: number,
|
|
175
201
|
): Promise<HybridRow[]> {
|
|
176
|
-
const candidateLimit = limit *
|
|
202
|
+
const candidateLimit = limit * 5;
|
|
177
203
|
|
|
178
204
|
// Vector KNN search (brute-force cosine similarity in JS)
|
|
179
205
|
const vectorResults = knnSearch(this.db, "memories_vec", embedding, candidateLimit);
|
|
@@ -188,8 +214,10 @@ export class MemoryRepository {
|
|
|
188
214
|
.all(ftsQuery, candidateLimit) as Array<{ id: string }>)
|
|
189
215
|
: [];
|
|
190
216
|
|
|
191
|
-
// Compute RRF scores
|
|
192
|
-
const
|
|
217
|
+
// Compute RRF scores with search signals for confidence scoring
|
|
218
|
+
const signalsMap = hybridRRFWithSignals(vectorResults, ftsResults);
|
|
219
|
+
const rrfScores = new Map<string, number>();
|
|
220
|
+
for (const [id, s] of signalsMap) rrfScores.set(id, s.rrfScore);
|
|
193
221
|
const topIds = topByRRF(rrfScores, limit);
|
|
194
222
|
|
|
195
223
|
if (topIds.length === 0) return [];
|
|
@@ -216,9 +244,16 @@ export class MemoryRepository {
|
|
|
216
244
|
|
|
217
245
|
const memEmbedding = this.getEmbedding(id);
|
|
218
246
|
const memory = this.rowToMemory(row, memEmbedding);
|
|
247
|
+
const signals = signalsMap.get(id)!;
|
|
219
248
|
results.push({
|
|
220
249
|
...memory,
|
|
221
|
-
rrfScore:
|
|
250
|
+
rrfScore: signals.rrfScore,
|
|
251
|
+
signals: {
|
|
252
|
+
cosineSimilarity: signals.cosineSimilarity,
|
|
253
|
+
ftsMatch: signals.ftsMatch,
|
|
254
|
+
knnRank: signals.knnRank,
|
|
255
|
+
ftsRank: signals.ftsRank,
|
|
256
|
+
},
|
|
222
257
|
});
|
|
223
258
|
}
|
|
224
259
|
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { randomUUID, createHash } from "crypto";
|
|
2
|
-
import type { Memory, SearchIntent, IntentProfile, HybridRow } from "./memory
|
|
3
|
-
import { isDeleted } from "./memory
|
|
4
|
-
import type { SearchResult, SearchOptions } from "./conversation
|
|
5
|
-
import type { MemoryRepository } from "./memory.repository
|
|
6
|
-
import type { EmbeddingsService } from "./embeddings.service
|
|
7
|
-
import type { ConversationHistoryService } from "./conversation.service
|
|
8
|
-
|
|
2
|
+
import type { Memory, SearchIntent, IntentProfile, HybridRow } from "./memory";
|
|
3
|
+
import { isDeleted, computeConfidence } from "./memory";
|
|
4
|
+
import type { SearchResult, SearchOptions } from "./conversation";
|
|
5
|
+
import type { MemoryRepository } from "./memory.repository";
|
|
6
|
+
import type { EmbeddingsService } from "./embeddings.service";
|
|
7
|
+
import type { ConversationHistoryService } from "./conversation.service";
|
|
8
|
+
|
|
9
|
+
// Jitter values halved from original (0.02/0.05/0.15) because RRF_K=10 produces
|
|
10
|
+
// ~6x more score spread than K=60, amplifying jitter's disruption effect.
|
|
9
11
|
const INTENT_PROFILES: Record<SearchIntent, IntentProfile> = {
|
|
10
|
-
continuity: { weights: { relevance: 0.3, recency: 0.5, utility: 0.2 }, jitter: 0.
|
|
11
|
-
fact_check: { weights: { relevance: 0.6, recency: 0.1, utility: 0.3 }, jitter: 0.
|
|
12
|
-
frequent: { weights: { relevance: 0.2, recency: 0.2, utility: 0.6 }, jitter: 0.
|
|
13
|
-
associative: { weights: { relevance: 0.7, recency: 0.1, utility: 0.2 }, jitter: 0.
|
|
14
|
-
explore: { weights: { relevance: 0.4, recency: 0.3, utility: 0.3 }, jitter: 0.
|
|
12
|
+
continuity: { weights: { relevance: 0.3, recency: 0.5, utility: 0.2 }, jitter: 0.01 },
|
|
13
|
+
fact_check: { weights: { relevance: 0.6, recency: 0.1, utility: 0.3 }, jitter: 0.01 },
|
|
14
|
+
frequent: { weights: { relevance: 0.2, recency: 0.2, utility: 0.6 }, jitter: 0.01 },
|
|
15
|
+
associative: { weights: { relevance: 0.7, recency: 0.1, utility: 0.2 }, jitter: 0.025 },
|
|
16
|
+
explore: { weights: { relevance: 0.4, recency: 0.3, utility: 0.3 }, jitter: 0.08 },
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
const sigmoid = (x: number): number => 1 / (1 + Math.exp(-x));
|
|
@@ -87,19 +89,10 @@ export class MemoryService {
|
|
|
87
89
|
async getMultiple(ids: string[]): Promise<Memory[]> {
|
|
88
90
|
if (ids.length === 0) return [];
|
|
89
91
|
const memories = await this.repository.findByIds(ids);
|
|
90
|
-
// Track access in bulk
|
|
91
92
|
const now = new Date();
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
this.repository.upsert({
|
|
96
|
-
...m,
|
|
97
|
-
accessCount: m.accessCount + 1,
|
|
98
|
-
lastAccessed: now,
|
|
99
|
-
})
|
|
100
|
-
)
|
|
101
|
-
);
|
|
102
|
-
return live;
|
|
93
|
+
const liveIds = memories.filter((m) => !isDeleted(m)).map((m) => m.id);
|
|
94
|
+
this.repository.bulkUpdateAccess(liveIds, now);
|
|
95
|
+
return memories.filter((m) => !isDeleted(m));
|
|
103
96
|
}
|
|
104
97
|
|
|
105
98
|
async delete(id: string): Promise<boolean> {
|
|
@@ -186,10 +179,10 @@ export class MemoryService {
|
|
|
186
179
|
async search(
|
|
187
180
|
query: string,
|
|
188
181
|
intent: SearchIntent,
|
|
189
|
-
limit: number = 10,
|
|
190
|
-
includeDeleted: boolean = false,
|
|
191
182
|
options?: SearchOptions
|
|
192
183
|
): Promise<SearchResult[]> {
|
|
184
|
+
const limit = options?.limit ?? 10;
|
|
185
|
+
const includeDeleted = options?.includeDeleted ?? false;
|
|
193
186
|
const queryEmbedding = await this.embeddings.embed(query);
|
|
194
187
|
const profile = INTENT_PROFILES[intent];
|
|
195
188
|
const now = new Date();
|
|
@@ -223,6 +216,7 @@ export class MemoryService {
|
|
|
223
216
|
updatedAt: candidate.updatedAt,
|
|
224
217
|
source: "memory" as const,
|
|
225
218
|
score: this.computeMemoryScore(candidate, profile, now),
|
|
219
|
+
confidence: computeConfidence(candidate.signals),
|
|
226
220
|
supersededBy: candidate.supersededBy,
|
|
227
221
|
usefulness: candidate.usefulness,
|
|
228
222
|
accessCount: candidate.accessCount,
|
|
@@ -249,6 +243,7 @@ export class MemoryService {
|
|
|
249
243
|
updatedAt: row.createdAt,
|
|
250
244
|
source: "conversation_history" as const,
|
|
251
245
|
score: row.rrfScore * historyWeight,
|
|
246
|
+
confidence: computeConfidence(row.signals),
|
|
252
247
|
supersededBy: null,
|
|
253
248
|
sessionId: (row.metadata?.session_id as string) ?? "",
|
|
254
249
|
role: (row.metadata?.role as string) ?? "unknown",
|
|
@@ -272,19 +267,7 @@ export class MemoryService {
|
|
|
272
267
|
|
|
273
268
|
async trackAccess(ids: string[]): Promise<void> {
|
|
274
269
|
if (ids.length === 0) return;
|
|
275
|
-
|
|
276
|
-
const now = new Date();
|
|
277
|
-
await Promise.all(
|
|
278
|
-
memories
|
|
279
|
-
.filter((m) => !isDeleted(m))
|
|
280
|
-
.map((m) =>
|
|
281
|
-
this.repository.upsert({
|
|
282
|
-
...m,
|
|
283
|
-
accessCount: m.accessCount + 1,
|
|
284
|
-
lastAccessed: now,
|
|
285
|
-
})
|
|
286
|
-
)
|
|
287
|
-
);
|
|
270
|
+
this.repository.bulkUpdateAccess(ids, new Date());
|
|
288
271
|
}
|
|
289
272
|
|
|
290
273
|
private static readonly UUID_ZERO =
|
|
@@ -292,8 +275,16 @@ export class MemoryService {
|
|
|
292
275
|
|
|
293
276
|
private static waypointId(project?: string): string {
|
|
294
277
|
if (!project?.length) return MemoryService.UUID_ZERO;
|
|
295
|
-
const
|
|
296
|
-
|
|
278
|
+
const normalized = project.trim().toLowerCase();
|
|
279
|
+
const hex = createHash("sha256").update(`waypoint:${normalized}`).digest("hex");
|
|
280
|
+
return `wp:${hex.slice(0, 32)}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Legacy UUID-formatted waypoint ID for migration fallback reads. */
|
|
284
|
+
private static legacyWaypointId(project?: string): string | null {
|
|
285
|
+
if (!project?.length) return null; // UUID_ZERO is still current for no-project
|
|
286
|
+
const normalized = project.trim().toLowerCase();
|
|
287
|
+
const hex = createHash("sha256").update(`waypoint:${normalized}`).digest("hex");
|
|
297
288
|
return [
|
|
298
289
|
hex.slice(0, 8),
|
|
299
290
|
hex.slice(8, 12),
|
|
@@ -386,6 +377,20 @@ ${list(args.memory_ids)}`;
|
|
|
386
377
|
}
|
|
387
378
|
|
|
388
379
|
async getLatestWaypoint(project?: string): Promise<Memory | null> {
|
|
389
|
-
|
|
380
|
+
const waypoint = await this.get(MemoryService.waypointId(project));
|
|
381
|
+
if (waypoint) return waypoint;
|
|
382
|
+
|
|
383
|
+
// Fallback: try legacy UUID-formatted waypoint ID and migrate on read
|
|
384
|
+
const legacyId = MemoryService.legacyWaypointId(project);
|
|
385
|
+
if (!legacyId) return null;
|
|
386
|
+
|
|
387
|
+
const legacy = await this.repository.findById(legacyId);
|
|
388
|
+
if (!legacy) return null;
|
|
389
|
+
|
|
390
|
+
// Migrate: write under new ID, delete old
|
|
391
|
+
const newId = MemoryService.waypointId(project);
|
|
392
|
+
await this.repository.upsert({ ...legacy, id: newId });
|
|
393
|
+
await this.repository.markDeleted(legacyId);
|
|
394
|
+
return { ...legacy, id: newId };
|
|
390
395
|
}
|
|
391
396
|
}
|
package/server/core/memory.ts
CHANGED
|
@@ -38,7 +38,46 @@ export interface IntentProfile {
|
|
|
38
38
|
jitter: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** Signals preserved from the hybrid search pipeline for confidence scoring. */
|
|
42
|
+
export interface SearchSignals {
|
|
43
|
+
cosineSimilarity: number | null;
|
|
44
|
+
ftsMatch: boolean;
|
|
45
|
+
knnRank: number | null;
|
|
46
|
+
ftsRank: number | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
/** Augments any entity type with an RRF score from hybrid search. */
|
|
42
|
-
export type WithRrfScore<T> = T & { rrfScore: number };
|
|
50
|
+
export type WithRrfScore<T> = T & { rrfScore: number; signals: SearchSignals };
|
|
43
51
|
|
|
44
52
|
export type HybridRow = WithRrfScore<Memory>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compute absolute confidence (0-1) from search signals.
|
|
56
|
+
*
|
|
57
|
+
* Based primarily on cosine similarity (the strongest absolute signal)
|
|
58
|
+
* mapped through a sigmoid with an agreement bonus for dual-path matches.
|
|
59
|
+
* The midpoint and steepness are calibrated for all-MiniLM-L6-v2 embeddings.
|
|
60
|
+
*/
|
|
61
|
+
// Calibrated against all-MiniLM-L6-v2: noise ceiling ~0.25, weak-relevant floor ~0.30
|
|
62
|
+
const CONFIDENCE_STEEPNESS = 14;
|
|
63
|
+
const CONFIDENCE_MIDPOINT = 0.35;
|
|
64
|
+
const CONFIDENCE_AGREEMENT_BONUS = 0.08;
|
|
65
|
+
|
|
66
|
+
export function computeConfidence(signals: SearchSignals): number {
|
|
67
|
+
const sim = signals.cosineSimilarity;
|
|
68
|
+
|
|
69
|
+
if (sim === null) {
|
|
70
|
+
// FTS-only result — keyword match but no semantic confirmation
|
|
71
|
+
return signals.ftsMatch ? 0.40 : 0.0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Shifted sigmoid: maps cosine similarity to interpretable confidence
|
|
75
|
+
let confidence = 1 / (1 + Math.exp(-CONFIDENCE_STEEPNESS * (sim - CONFIDENCE_MIDPOINT)));
|
|
76
|
+
|
|
77
|
+
// Dual-path agreement bonus: found by both KNN and FTS
|
|
78
|
+
if (signals.ftsMatch) {
|
|
79
|
+
confidence = Math.min(1.0, confidence + CONFIDENCE_AGREEMENT_BONUS);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return confidence;
|
|
83
|
+
}
|
|
@@ -3,9 +3,9 @@ import { createHash } from "crypto";
|
|
|
3
3
|
import { existsSync, statSync, readdirSync } from "fs";
|
|
4
4
|
import { resolve, dirname } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { serializeVector } from "./sqlite-utils
|
|
7
|
-
import type { MemoryRepository } from "./memory.repository
|
|
8
|
-
import type { EmbeddingsService } from "./embeddings.service
|
|
6
|
+
import { serializeVector } from "./sqlite-utils";
|
|
7
|
+
import type { MemoryRepository } from "./memory.repository";
|
|
8
|
+
import type { EmbeddingsService } from "./embeddings.service";
|
|
9
9
|
|
|
10
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
-
import type { EmbeddingsService } from "./embeddings.service
|
|
3
|
-
import { serializeVector } from "./sqlite-utils
|
|
2
|
+
import type { EmbeddingsService } from "./embeddings.service";
|
|
3
|
+
import { serializeVector } from "./sqlite-utils";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Pre-migration step: remove vec0 virtual table entries from sqlite_master
|
|
@@ -127,41 +127,23 @@ export async function backfillVectors(
|
|
|
127
127
|
db: Database,
|
|
128
128
|
embeddings: EmbeddingsService,
|
|
129
129
|
): Promise<void> {
|
|
130
|
-
//
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
.get();
|
|
134
|
-
const memoriesExist = db.prepare("SELECT 1 FROM memories LIMIT 1").get();
|
|
135
|
-
const convosExist = db.prepare("SELECT 1 FROM conversation_history LIMIT 1").get();
|
|
130
|
+
// Quick gap check: if no rows are missing vectors, skip the expensive backfill
|
|
131
|
+
const hasMemories = db.prepare("SELECT 1 FROM memories LIMIT 1").get();
|
|
132
|
+
const hasConvos = db.prepare("SELECT 1 FROM conversation_history LIMIT 1").get();
|
|
136
133
|
|
|
137
|
-
|
|
138
|
-
// Only run the expensive LEFT JOIN when there's reason to suspect gaps.
|
|
139
|
-
const convoSentinel = db
|
|
140
|
-
.prepare("SELECT 1 FROM conversation_history_vec LIMIT 1")
|
|
141
|
-
.get();
|
|
142
|
-
const mayNeedMemoryBackfill = memoriesExist && !sentinel;
|
|
143
|
-
const mayNeedConvoBackfill = convosExist && !convoSentinel;
|
|
134
|
+
if (!hasMemories && !hasConvos) return;
|
|
144
135
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
).get();
|
|
157
|
-
if (!convoGap) return;
|
|
158
|
-
} else if (!gap && !convosExist) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
return; // No data at all
|
|
163
|
-
}
|
|
164
|
-
}
|
|
136
|
+
const memoryGap = hasMemories && db.prepare(
|
|
137
|
+
`SELECT 1 FROM memories m LEFT JOIN memories_vec v ON m.id = v.id
|
|
138
|
+
WHERE v.id IS NULL OR length(v.vector) = 0 LIMIT 1`,
|
|
139
|
+
).get();
|
|
140
|
+
|
|
141
|
+
const convoGap = hasConvos && db.prepare(
|
|
142
|
+
`SELECT 1 FROM conversation_history c LEFT JOIN conversation_history_vec v ON c.id = v.id
|
|
143
|
+
WHERE v.id IS NULL OR length(v.vector) = 0 LIMIT 1`,
|
|
144
|
+
).get();
|
|
145
|
+
|
|
146
|
+
if (!memoryGap && !convoGap) return;
|
|
165
147
|
|
|
166
148
|
// ── Memories ──────────────────────────────────────────────────────
|
|
167
149
|
const missingMemories = db
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from "fs/promises";
|
|
2
2
|
import { basename, dirname, join } from "path";
|
|
3
|
-
import type { ParsedMessage, SessionFileInfo } from "../conversation
|
|
4
|
-
import type { SessionLogParser } from "./types
|
|
3
|
+
import type { ParsedMessage, SessionFileInfo } from "../conversation";
|
|
4
|
+
import type { SessionLogParser } from "./types";
|
|
5
5
|
|
|
6
6
|
// UUID pattern for session IDs
|
|
7
7
|
const UUID_PATTERN =
|
|
@@ -45,7 +45,7 @@ export class ClaudeCodeSessionParser implements SessionLogParser {
|
|
|
45
45
|
const fileName = basename(filePath, ".jsonl");
|
|
46
46
|
const parentDir = basename(dirname(filePath));
|
|
47
47
|
// Check if this is inside a subagents directory
|
|
48
|
-
const isSubagentFile =
|
|
48
|
+
const isSubagentFile = /[/\\]subagents[/\\]/.test(filePath);
|
|
49
49
|
|
|
50
50
|
// For subagent files, project dir is 3 levels up: <project>/<session>/subagents/<file>
|
|
51
51
|
// For main files, project dir is direct parent
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
2
|
|
|
3
|
-
/** RRF constant
|
|
4
|
-
export const RRF_K =
|
|
3
|
+
/** RRF constant — lower K gives sharper top-rank discrimination in the 1/(K+rank) formula */
|
|
4
|
+
export const RRF_K = 10;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Maximum parameters per SQLite query to stay within SQLITE_MAX_VARIABLE_NUMBER.
|
|
8
|
+
*/
|
|
9
|
+
export const SQLITE_BATCH_SIZE = 100;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute a query in batches when the number of parameters exceeds SQLITE_BATCH_SIZE.
|
|
13
|
+
* Splits the ids array and concatenates results.
|
|
14
|
+
*/
|
|
15
|
+
export function batchedQuery<T>(
|
|
16
|
+
db: Database,
|
|
17
|
+
ids: string[],
|
|
18
|
+
queryFn: (batch: string[]) => T[]
|
|
19
|
+
): T[] {
|
|
20
|
+
if (ids.length <= SQLITE_BATCH_SIZE) return queryFn(ids);
|
|
21
|
+
const results: T[] = [];
|
|
22
|
+
for (let i = 0; i < ids.length; i += SQLITE_BATCH_SIZE) {
|
|
23
|
+
results.push(...queryFn(ids.slice(i, i + SQLITE_BATCH_SIZE)));
|
|
24
|
+
}
|
|
25
|
+
return results;
|
|
26
|
+
}
|
|
5
27
|
|
|
6
28
|
/**
|
|
7
29
|
* Serialize a number[] embedding to raw float32 bytes for BLOB storage.
|
|
@@ -94,6 +116,49 @@ export function hybridRRF(
|
|
|
94
116
|
return scores;
|
|
95
117
|
}
|
|
96
118
|
|
|
119
|
+
import type { SearchSignals } from "./memory";
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Compute hybrid RRF scores while preserving per-result search signals
|
|
123
|
+
* (cosine similarity, FTS match, rank positions) for confidence scoring.
|
|
124
|
+
*/
|
|
125
|
+
export function hybridRRFWithSignals(
|
|
126
|
+
vectorResults: Array<{ id: string; distance: number }>,
|
|
127
|
+
ftsResults: Array<{ id: string }>,
|
|
128
|
+
k: number = RRF_K
|
|
129
|
+
): Map<string, SearchSignals & { rrfScore: number }> {
|
|
130
|
+
const knnMap = new Map<string, { similarity: number; rank: number }>();
|
|
131
|
+
vectorResults.forEach((r, i) => {
|
|
132
|
+
knnMap.set(r.id, { similarity: 1 - r.distance, rank: i + 1 });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const ftsMap = new Map<string, number>();
|
|
136
|
+
ftsResults.forEach((r, i) => {
|
|
137
|
+
ftsMap.set(r.id, i + 1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const allIds = new Set([...knnMap.keys(), ...ftsMap.keys()]);
|
|
141
|
+
const results = new Map<string, SearchSignals & { rrfScore: number }>();
|
|
142
|
+
|
|
143
|
+
for (const id of allIds) {
|
|
144
|
+
const knn = knnMap.get(id);
|
|
145
|
+
const ftsRank = ftsMap.get(id) ?? null;
|
|
146
|
+
let rrfScore = 0;
|
|
147
|
+
if (knn) rrfScore += 1 / (k + knn.rank);
|
|
148
|
+
if (ftsRank !== null) rrfScore += 1 / (k + ftsRank);
|
|
149
|
+
|
|
150
|
+
results.set(id, {
|
|
151
|
+
rrfScore,
|
|
152
|
+
cosineSimilarity: knn?.similarity ?? null,
|
|
153
|
+
ftsMatch: ftsRank !== null,
|
|
154
|
+
knnRank: knn?.rank ?? null,
|
|
155
|
+
ftsRank,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
|
|
97
162
|
/**
|
|
98
163
|
* Sort ids by RRF score descending and return top N.
|
|
99
164
|
*/
|
package/server/index.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { loadConfig, parseCliArgs } from "./config/index
|
|
4
|
-
import { connectToDatabase } from "./core/connection
|
|
5
|
-
import { backfillVectors } from "./core/migrations
|
|
6
|
-
import { MemoryRepository } from "./core/memory.repository
|
|
7
|
-
import { ConversationRepository } from "./core/conversation.repository
|
|
8
|
-
import { EmbeddingsService } from "./core/embeddings.service
|
|
9
|
-
import { MemoryService } from "./core/memory.service
|
|
10
|
-
import { ConversationHistoryService } from "./core/conversation.service
|
|
11
|
-
import { startServer } from "./transports/mcp/server
|
|
12
|
-
import { startHttpServer } from "./transports/http/server
|
|
3
|
+
import { loadConfig, parseCliArgs } from "./config/index";
|
|
4
|
+
import { connectToDatabase } from "./core/connection";
|
|
5
|
+
import { backfillVectors } from "./core/migrations";
|
|
6
|
+
import { MemoryRepository } from "./core/memory.repository";
|
|
7
|
+
import { ConversationRepository } from "./core/conversation.repository";
|
|
8
|
+
import { EmbeddingsService } from "./core/embeddings.service";
|
|
9
|
+
import { MemoryService } from "./core/memory.service";
|
|
10
|
+
import { ConversationHistoryService } from "./core/conversation.service";
|
|
11
|
+
import { startServer } from "./transports/mcp/server";
|
|
12
|
+
import { startHttpServer } from "./transports/http/server";
|
|
13
13
|
|
|
14
14
|
async function main(): Promise<void> {
|
|
15
15
|
const args = process.argv.slice(2);
|
|
@@ -21,11 +21,11 @@ import {
|
|
|
21
21
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
22
22
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
23
23
|
|
|
24
|
-
import { tools } from "../mcp/tools
|
|
25
|
-
import { handleToolCall } from "../mcp/handlers
|
|
26
|
-
import { SERVER_INSTRUCTIONS } from "../mcp/server
|
|
27
|
-
import { VERSION } from "../../config/index
|
|
28
|
-
import type { MemoryService } from "../../core/memory.service
|
|
24
|
+
import { tools } from "../mcp/tools";
|
|
25
|
+
import { handleToolCall } from "../mcp/handlers";
|
|
26
|
+
import { SERVER_INSTRUCTIONS } from "../mcp/server";
|
|
27
|
+
import { VERSION } from "../../config/index";
|
|
28
|
+
import type { MemoryService } from "../../core/memory.service";
|
|
29
29
|
|
|
30
30
|
interface Session {
|
|
31
31
|
server: Server;
|
|
@@ -3,11 +3,11 @@ import { cors } from "hono/cors";
|
|
|
3
3
|
import { createServer } from "net";
|
|
4
4
|
import { writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
|
-
import type { MemoryService } from "../../core/memory.service
|
|
7
|
-
import type { Config } from "../../config/index
|
|
8
|
-
import { isDeleted } from "../../core/memory
|
|
9
|
-
import { createMcpRoutes } from "./mcp-transport
|
|
10
|
-
import type { Memory, SearchIntent } from "../../core/memory
|
|
6
|
+
import type { MemoryService } from "../../core/memory.service";
|
|
7
|
+
import type { Config } from "../../config/index";
|
|
8
|
+
import { isDeleted } from "../../core/memory";
|
|
9
|
+
import { createMcpRoutes } from "./mcp-transport";
|
|
10
|
+
import type { Memory, SearchIntent } from "../../core/memory";
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -139,7 +139,7 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
139
139
|
return c.json({ error: "Missing or invalid 'query' field" }, 400);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
const results = await memoryService.search(query, intent, limit);
|
|
142
|
+
const results = await memoryService.search(query, intent, { limit });
|
|
143
143
|
|
|
144
144
|
return c.json({
|
|
145
145
|
results: results.map((r) => ({
|
|
@@ -147,6 +147,7 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
147
147
|
content: r.content,
|
|
148
148
|
metadata: r.metadata,
|
|
149
149
|
source: r.source,
|
|
150
|
+
confidence: r.confidence,
|
|
150
151
|
createdAt: r.createdAt.toISOString(),
|
|
151
152
|
})),
|
|
152
153
|
count: results.length,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import type { MemoryService } from "../../core/memory.service
|
|
3
|
-
import type { ConversationHistoryService } from "../../core/conversation.service
|
|
4
|
-
import type { SearchIntent } from "../../core/memory
|
|
5
|
-
import type { HistoryFilters, SearchResult } from "../../core/conversation
|
|
6
|
-
import { DEBUG } from "../../config/index
|
|
2
|
+
import type { MemoryService } from "../../core/memory.service";
|
|
3
|
+
import type { ConversationHistoryService } from "../../core/conversation.service";
|
|
4
|
+
import type { SearchIntent } from "../../core/memory";
|
|
5
|
+
import type { HistoryFilters, SearchResult } from "../../core/conversation";
|
|
6
|
+
import { DEBUG } from "../../config/index";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Safely coerce a tool argument to an array. Handles the case where the MCP
|
|
@@ -51,6 +51,14 @@ function parseDate(value: unknown, fieldName: string): Date | undefined {
|
|
|
51
51
|
return date;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function requireString(args: Record<string, unknown> | undefined, field: string): string {
|
|
55
|
+
const value = args?.[field];
|
|
56
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
57
|
+
throw new Error(`${field} is required`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
export async function handleStoreMemories(
|
|
55
63
|
args: Record<string, unknown> | undefined,
|
|
56
64
|
service: MemoryService
|
|
@@ -189,7 +197,9 @@ export async function handleSearchMemories(
|
|
|
189
197
|
return errorResult(errorText(e));
|
|
190
198
|
}
|
|
191
199
|
|
|
192
|
-
const results = await service.search(query, intent,
|
|
200
|
+
const results = await service.search(query, intent, {
|
|
201
|
+
limit,
|
|
202
|
+
includeDeleted,
|
|
193
203
|
includeHistory,
|
|
194
204
|
historyOnly,
|
|
195
205
|
historyFilters,
|
|
@@ -202,19 +212,7 @@ export async function handleSearchMemories(
|
|
|
202
212
|
};
|
|
203
213
|
}
|
|
204
214
|
|
|
205
|
-
const formatted = results.map((r
|
|
206
|
-
let result = `[${r.source}] ID: ${r.id}\nContent: ${r.content}`;
|
|
207
|
-
if (r.metadata && Object.keys(r.metadata).length > 0) {
|
|
208
|
-
result += `\nMetadata: ${JSON.stringify(r.metadata)}`;
|
|
209
|
-
}
|
|
210
|
-
if (r.source === "memory" && includeDeleted && r.supersededBy) {
|
|
211
|
-
result += `\n[DELETED]`;
|
|
212
|
-
}
|
|
213
|
-
if (r.source === "conversation_history" && r.sessionId) {
|
|
214
|
-
result += `\nSession: ${r.sessionId}`;
|
|
215
|
-
}
|
|
216
|
-
return result;
|
|
217
|
-
});
|
|
215
|
+
const formatted = results.map((r) => formatSearchResult(r, includeDeleted));
|
|
218
216
|
|
|
219
217
|
return {
|
|
220
218
|
content: [{ type: "text", text: formatted.join("\n\n---\n\n") }],
|
|
@@ -241,6 +239,20 @@ function formatMemoryDetail(
|
|
|
241
239
|
return result;
|
|
242
240
|
}
|
|
243
241
|
|
|
242
|
+
function formatSearchResult(r: SearchResult, includeDeleted: boolean): string {
|
|
243
|
+
let result = `[${r.source}] ID: ${r.id}\nConfidence: ${r.confidence.toFixed(2)}\nContent: ${r.content}`;
|
|
244
|
+
if (r.metadata && Object.keys(r.metadata).length > 0) {
|
|
245
|
+
result += `\nMetadata: ${JSON.stringify(r.metadata)}`;
|
|
246
|
+
}
|
|
247
|
+
if (r.source === "memory" && includeDeleted && r.supersededBy) {
|
|
248
|
+
result += `\n[DELETED]`;
|
|
249
|
+
}
|
|
250
|
+
if (r.source === "conversation_history" && r.sessionId) {
|
|
251
|
+
result += `\nSession: ${r.sessionId}`;
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
244
256
|
export async function handleGetMemories(
|
|
245
257
|
args: Record<string, unknown> | undefined,
|
|
246
258
|
service: MemoryService
|
|
@@ -267,8 +279,11 @@ export async function handleReportMemoryUsefulness(
|
|
|
267
279
|
args: Record<string, unknown> | undefined,
|
|
268
280
|
service: MemoryService
|
|
269
281
|
): Promise<CallToolResult> {
|
|
270
|
-
const memoryId = args
|
|
271
|
-
const useful = args?.useful
|
|
282
|
+
const memoryId = requireString(args, "memory_id");
|
|
283
|
+
const useful = args?.useful;
|
|
284
|
+
if (typeof useful !== "boolean") {
|
|
285
|
+
return errorResult("useful is required and must be a boolean");
|
|
286
|
+
}
|
|
272
287
|
|
|
273
288
|
const memory = await service.vote(memoryId, useful ? 1 : -1);
|
|
274
289
|
|
|
@@ -290,10 +305,19 @@ export async function handleSetWaypoint(
|
|
|
290
305
|
args: Record<string, unknown> | undefined,
|
|
291
306
|
service: MemoryService
|
|
292
307
|
): Promise<CallToolResult> {
|
|
308
|
+
let project: string;
|
|
309
|
+
let summary: string;
|
|
310
|
+
try {
|
|
311
|
+
project = requireString(args, "project");
|
|
312
|
+
summary = requireString(args, "summary");
|
|
313
|
+
} catch (e) {
|
|
314
|
+
return errorResult(errorText(e));
|
|
315
|
+
}
|
|
316
|
+
|
|
293
317
|
const memory = await service.setWaypoint({
|
|
294
|
-
project
|
|
318
|
+
project,
|
|
295
319
|
branch: args?.branch as string | undefined,
|
|
296
|
-
summary
|
|
320
|
+
summary,
|
|
297
321
|
completed: (args?.completed as string[] | undefined) ?? [],
|
|
298
322
|
in_progress_blocked: (args?.in_progress_blocked as string[] | undefined) ?? [],
|
|
299
323
|
key_decisions: (args?.key_decisions as string[] | undefined) ?? [],
|
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
ListResourcesRequestSchema,
|
|
7
7
|
ReadResourceRequestSchema,
|
|
8
8
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import { resources, readResource } from "./resources
|
|
9
|
+
import { resources, readResource } from "./resources";
|
|
10
10
|
|
|
11
|
-
import { tools } from "./tools
|
|
12
|
-
import { handleToolCall } from "./handlers
|
|
13
|
-
import type { MemoryService } from "../../core/memory.service
|
|
14
|
-
import { VERSION } from "../../config/index
|
|
11
|
+
import { tools } from "./tools";
|
|
12
|
+
import { handleToolCall } from "./handlers";
|
|
13
|
+
import type { MemoryService } from "../../core/memory.service";
|
|
14
|
+
import { VERSION } from "../../config/index";
|
|
15
15
|
|
|
16
16
|
export const SERVER_INSTRUCTIONS = `This server is the user's canonical memory system. It provides persistent, semantic vector memory that survives across conversations and sessions.
|
|
17
17
|
|