claude-memory-layer 1.0.10 → 1.0.12
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/AGENTS.md +60 -0
- package/README.md +166 -2
- package/bootstrap-kb/decisions/decisions.md +244 -0
- package/bootstrap-kb/glossary/glossary.md +46 -0
- package/bootstrap-kb/modules/.claude-plugin.md +22 -0
- package/bootstrap-kb/modules/agents.md.md +15 -0
- package/bootstrap-kb/modules/claude.md.md +15 -0
- package/bootstrap-kb/modules/context.md.md +15 -0
- package/bootstrap-kb/modules/docs.md +18 -0
- package/bootstrap-kb/modules/handoff.md.md +15 -0
- package/bootstrap-kb/modules/package-lock.json.md +15 -0
- package/bootstrap-kb/modules/package.json.md +15 -0
- package/bootstrap-kb/modules/plan.md.md +15 -0
- package/bootstrap-kb/modules/readme.md.md +15 -0
- package/bootstrap-kb/modules/scripts.md +26 -0
- package/bootstrap-kb/modules/spec.md.md +15 -0
- package/bootstrap-kb/modules/specs.md +20 -0
- package/bootstrap-kb/modules/src.md +51 -0
- package/bootstrap-kb/modules/tests.md +42 -0
- package/bootstrap-kb/modules/tsconfig.json.md +15 -0
- package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
- package/bootstrap-kb/overview/overview.md +40 -0
- package/bootstrap-kb/sources/manifest.json +950 -0
- package/bootstrap-kb/sources/manifest.md +227 -0
- package/bootstrap-kb/timeline/timeline.md +57 -0
- package/d.sh +3 -0
- package/deploy.sh +3 -0
- package/dist/cli/index.js +3577 -389
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1383 -138
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1917 -214
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1813 -231
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1802 -205
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1909 -248
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1861 -206
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +2341 -217
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +2350 -226
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1805 -206
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +1447 -55
- package/dist/ui/index.html +318 -147
- package/dist/ui/style.css +892 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -0
- package/docs/OPERATIONS.md +18 -0
- package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
- package/memory/_index.md +405 -0
- package/memory/default/uncategorized/2026-02-25.md +4839 -0
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
- package/memory/specs/citations-system/2026-02-25.md +1121 -0
- package/memory/specs/endless-mode/2026-02-25.md +1392 -0
- package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
- package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
- package/memory/specs/private-tags/2026-02-25.md +1057 -0
- package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
- package/memory/specs/task-entity-system/2026-02-25.md +924 -0
- package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
- package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
- package/package.json +9 -2
- package/scripts/build.ts +6 -0
- package/scripts/fix-sync-gap.js +32 -0
- package/scripts/heartbeat-memory-orchestrator.sh +28 -0
- package/scripts/report-sync-gap.js +26 -0
- package/scripts/review-queue-auto-resolve.js +21 -0
- package/scripts/sync-gap-auto-heal.sh +17 -0
- package/specs/20260207-dashboard-upgrade/context.md +38 -0
- package/specs/20260207-dashboard-upgrade/spec.md +96 -0
- package/src/cli/index.ts +391 -60
- package/src/core/consolidated-store.ts +63 -1
- package/src/core/consolidation-worker.ts +115 -6
- package/src/core/event-store.ts +14 -0
- package/src/core/index.ts +1 -0
- package/src/core/ingest-interceptor.ts +80 -0
- package/src/core/markdown-mirror.ts +70 -0
- package/src/core/md-mirror.ts +92 -0
- package/src/core/mongo-sync-config.ts +165 -0
- package/src/core/mongo-sync-worker.ts +381 -0
- package/src/core/retriever.ts +540 -150
- package/src/core/sqlite-event-store.ts +794 -7
- package/src/core/sqlite-wrapper.ts +8 -0
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/turn-state.ts +159 -0
- package/src/core/types.ts +51 -8
- package/src/core/vector-store.ts +21 -3
- package/src/hooks/post-tool-use.ts +68 -23
- package/src/hooks/session-end.ts +8 -3
- package/src/hooks/stop.ts +96 -25
- package/src/hooks/user-prompt-submit.ts +44 -5
- package/src/server/api/chat.ts +244 -0
- package/src/server/api/citations.ts +3 -3
- package/src/server/api/events.ts +30 -5
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +9 -1
- package/src/server/api/projects.ts +74 -0
- package/src/server/api/search.ts +3 -3
- package/src/server/api/sessions.ts +3 -3
- package/src/server/api/stats.ts +89 -8
- package/src/server/api/turns.ts +143 -0
- package/src/server/api/utils.ts +46 -0
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +508 -71
- package/src/services/session-history-importer.ts +215 -51
- package/src/ui/app.js +1447 -55
- package/src/ui/index.html +318 -147
- package/src/ui/style.css +892 -0
- package/tests/bootstrap-organizer.test.ts +111 -0
- package/tests/consolidation-worker.test.ts +75 -0
- package/tests/ingest-interceptor.test.ts +38 -0
- package/tests/markdown-mirror.test.ts +85 -0
- package/tests/md-mirror.test.ts +50 -0
- package/tests/retriever-fallback-chain.test.ts +223 -0
- package/tests/retriever-strategy-scope.test.ts +97 -0
- package/tests/retriever.memu-adoption.test.ts +122 -0
- package/tests/sqlite-event-store-replication.test.ts +92 -0
- package/.claude/settings.local.json +0 -27
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260201112328.json +0 -45
- package/.history/package_20260201113602.json +0 -45
- package/.history/package_20260201113713.json +0 -45
- package/.history/package_20260201114110.json +0 -45
- package/.history/package_20260201114632.json +0 -46
- package/.history/package_20260201133143.json +0 -45
- package/.history/package_20260201134319.json +0 -45
- package/.history/package_20260201134326.json +0 -45
- package/.history/package_20260201134334.json +0 -45
- package/.history/package_20260201134912.json +0 -45
- package/.history/package_20260201142928.json +0 -46
- package/.history/package_20260201192048.json +0 -47
- package/.history/package_20260202114053.json +0 -49
- package/.history/package_20260202121115.json +0 -49
- package/test_access.js +0 -49
package/src/core/retriever.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Memory Retriever - Unified retrieval interface
|
|
3
|
-
* Combines vector search,
|
|
3
|
+
* Combines vector search, keyword search, scoped filtering, and matching
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { EventStore } from './event-store.js';
|
|
@@ -10,7 +10,19 @@ import { Matcher } from './matcher.js';
|
|
|
10
10
|
import { SharedStore } from './shared-store.js';
|
|
11
11
|
import { SharedVectorStore } from './shared-vector-store.js';
|
|
12
12
|
import { GraduationPipeline } from './graduation.js';
|
|
13
|
-
import type { MemoryEvent, MatchResult,
|
|
13
|
+
import type { MemoryEvent, MatchResult, SharedTroubleshootingEntry } from './types.js';
|
|
14
|
+
|
|
15
|
+
export interface RetrievalScope {
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
eventTypes?: MemoryEvent['eventType'][];
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
canonicalKeyPrefix?: string;
|
|
20
|
+
sessionIdPrefix?: string;
|
|
21
|
+
contentIncludes?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type RetrievalStrategy = 'auto' | 'fast' | 'deep';
|
|
25
|
+
export type ProjectScopeMode = 'strict' | 'prefer' | 'global';
|
|
14
26
|
|
|
15
27
|
export interface RetrievalOptions {
|
|
16
28
|
topK: number;
|
|
@@ -18,6 +30,28 @@ export interface RetrievalOptions {
|
|
|
18
30
|
sessionId?: string;
|
|
19
31
|
maxTokens: number;
|
|
20
32
|
includeSessionContext: boolean;
|
|
33
|
+
scope?: RetrievalScope;
|
|
34
|
+
strategy?: RetrievalStrategy;
|
|
35
|
+
rerankWithKeyword?: boolean;
|
|
36
|
+
rerankWeights?: {
|
|
37
|
+
semantic?: number;
|
|
38
|
+
lexical?: number;
|
|
39
|
+
recency?: number;
|
|
40
|
+
};
|
|
41
|
+
decayPolicy?: {
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
windowDays?: number;
|
|
44
|
+
maxPenalty?: number;
|
|
45
|
+
};
|
|
46
|
+
intentRewrite?: boolean;
|
|
47
|
+
graphHop?: {
|
|
48
|
+
enabled?: boolean;
|
|
49
|
+
maxHops?: number;
|
|
50
|
+
hopPenalty?: number;
|
|
51
|
+
};
|
|
52
|
+
projectScopeMode?: ProjectScopeMode;
|
|
53
|
+
projectHash?: string;
|
|
54
|
+
allowedProjectHashes?: string[];
|
|
21
55
|
}
|
|
22
56
|
|
|
23
57
|
export interface RetrievalResult {
|
|
@@ -25,6 +59,21 @@ export interface RetrievalResult {
|
|
|
25
59
|
matchResult: MatchResult;
|
|
26
60
|
totalTokens: number;
|
|
27
61
|
context: string;
|
|
62
|
+
fallbackTrace?: string[];
|
|
63
|
+
selectedDebug?: Array<{
|
|
64
|
+
eventId: string;
|
|
65
|
+
score: number;
|
|
66
|
+
semanticScore?: number;
|
|
67
|
+
lexicalScore?: number;
|
|
68
|
+
recencyScore?: number;
|
|
69
|
+
}>;
|
|
70
|
+
candidateDebug?: Array<{
|
|
71
|
+
eventId: string;
|
|
72
|
+
score: number;
|
|
73
|
+
semanticScore?: number;
|
|
74
|
+
lexicalScore?: number;
|
|
75
|
+
recencyScore?: number;
|
|
76
|
+
}>;
|
|
28
77
|
}
|
|
29
78
|
|
|
30
79
|
export interface MemoryWithContext {
|
|
@@ -46,7 +95,20 @@ const DEFAULT_OPTIONS: RetrievalOptions = {
|
|
|
46
95
|
topK: 5,
|
|
47
96
|
minScore: 0.7,
|
|
48
97
|
maxTokens: 2000,
|
|
49
|
-
includeSessionContext: true
|
|
98
|
+
includeSessionContext: true,
|
|
99
|
+
strategy: 'auto',
|
|
100
|
+
rerankWithKeyword: true,
|
|
101
|
+
decayPolicy: {
|
|
102
|
+
enabled: true,
|
|
103
|
+
windowDays: 30,
|
|
104
|
+
maxPenalty: 0.15
|
|
105
|
+
},
|
|
106
|
+
graphHop: {
|
|
107
|
+
enabled: true,
|
|
108
|
+
maxHops: 1,
|
|
109
|
+
hopPenalty: 0.08
|
|
110
|
+
},
|
|
111
|
+
projectScopeMode: 'global'
|
|
50
112
|
};
|
|
51
113
|
|
|
52
114
|
export interface SharedStoreOptions {
|
|
@@ -54,14 +116,19 @@ export interface SharedStoreOptions {
|
|
|
54
116
|
sharedVectorStore?: SharedVectorStore;
|
|
55
117
|
}
|
|
56
118
|
|
|
119
|
+
type EventStoreLike = EventStore & {
|
|
120
|
+
keywordSearch?: (query: string, limit?: number) => Promise<Array<{ event: MemoryEvent; rank: number }>>;
|
|
121
|
+
};
|
|
122
|
+
|
|
57
123
|
export class Retriever {
|
|
58
|
-
private readonly eventStore:
|
|
124
|
+
private readonly eventStore: EventStoreLike;
|
|
59
125
|
private readonly vectorStore: VectorStore;
|
|
60
126
|
private readonly embedder: Embedder;
|
|
61
127
|
private readonly matcher: Matcher;
|
|
62
128
|
private sharedStore?: SharedStore;
|
|
63
129
|
private sharedVectorStore?: SharedVectorStore;
|
|
64
130
|
private graduation?: GraduationPipeline;
|
|
131
|
+
private queryRewriter?: (query: string) => Promise<string | null>;
|
|
65
132
|
|
|
66
133
|
constructor(
|
|
67
134
|
eventStore: EventStore,
|
|
@@ -70,7 +137,7 @@ export class Retriever {
|
|
|
70
137
|
matcher: Matcher,
|
|
71
138
|
sharedOptions?: SharedStoreOptions
|
|
72
139
|
) {
|
|
73
|
-
this.eventStore = eventStore;
|
|
140
|
+
this.eventStore = eventStore as EventStoreLike;
|
|
74
141
|
this.vectorStore = vectorStore;
|
|
75
142
|
this.embedder = embedder;
|
|
76
143
|
this.matcher = matcher;
|
|
@@ -78,106 +145,152 @@ export class Retriever {
|
|
|
78
145
|
this.sharedVectorStore = sharedOptions?.sharedVectorStore;
|
|
79
146
|
}
|
|
80
147
|
|
|
81
|
-
/**
|
|
82
|
-
* Set graduation pipeline for access tracking
|
|
83
|
-
*/
|
|
84
148
|
setGraduationPipeline(graduation: GraduationPipeline): void {
|
|
85
149
|
this.graduation = graduation;
|
|
86
150
|
}
|
|
87
151
|
|
|
88
|
-
/**
|
|
89
|
-
* Set shared stores after construction
|
|
90
|
-
*/
|
|
91
152
|
setSharedStores(sharedStore: SharedStore, sharedVectorStore: SharedVectorStore): void {
|
|
92
153
|
this.sharedStore = sharedStore;
|
|
93
154
|
this.sharedVectorStore = sharedVectorStore;
|
|
94
155
|
}
|
|
95
156
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
157
|
+
setQueryRewriter(rewriter: (query: string) => Promise<string | null>): void {
|
|
158
|
+
this.queryRewriter = rewriter;
|
|
159
|
+
}
|
|
160
|
+
|
|
99
161
|
async retrieve(
|
|
100
162
|
query: string,
|
|
101
163
|
options: Partial<RetrievalOptions> = {}
|
|
102
164
|
): Promise<RetrievalResult> {
|
|
103
165
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
166
|
+
const sessionFilter = opts.scope?.sessionId ?? opts.sessionId;
|
|
167
|
+
const fallbackTrace: string[] = [];
|
|
104
168
|
|
|
105
|
-
|
|
106
|
-
const queryEmbedding = await this.embedder.embed(query);
|
|
169
|
+
const fallbackEnabled = (opts.strategy ?? 'auto') === 'auto';
|
|
107
170
|
|
|
108
|
-
//
|
|
109
|
-
const
|
|
110
|
-
|
|
171
|
+
// Stage 1: primary retrieval
|
|
172
|
+
const primaryStrategy: RetrievalStrategy = opts.strategy === 'auto' ? 'fast' : (opts.strategy || 'fast');
|
|
173
|
+
let current = await this.runStage(query, {
|
|
174
|
+
strategy: primaryStrategy,
|
|
175
|
+
topK: opts.topK,
|
|
111
176
|
minScore: opts.minScore,
|
|
112
|
-
sessionId:
|
|
177
|
+
sessionId: sessionFilter,
|
|
178
|
+
scope: opts.scope,
|
|
179
|
+
rerankWithKeyword: opts.rerankWithKeyword !== false,
|
|
180
|
+
rerankWeights: opts.rerankWeights,
|
|
181
|
+
decayPolicy: opts.decayPolicy,
|
|
182
|
+
intentRewrite: opts.intentRewrite === true,
|
|
183
|
+
graphHop: opts.graphHop,
|
|
184
|
+
projectScopeMode: opts.projectScopeMode,
|
|
185
|
+
projectHash: opts.projectHash,
|
|
186
|
+
allowedProjectHashes: opts.allowedProjectHashes
|
|
113
187
|
});
|
|
188
|
+
fallbackTrace.push(`stage:primary:${primaryStrategy}`);
|
|
189
|
+
|
|
190
|
+
// Stage 2: deep fallback
|
|
191
|
+
if (fallbackEnabled && this.shouldFallback(current.matchResult, current.results) && primaryStrategy !== 'deep') {
|
|
192
|
+
current = await this.runStage(query, {
|
|
193
|
+
strategy: 'deep',
|
|
194
|
+
topK: opts.topK,
|
|
195
|
+
minScore: opts.minScore,
|
|
196
|
+
sessionId: sessionFilter,
|
|
197
|
+
scope: opts.scope,
|
|
198
|
+
rerankWithKeyword: opts.rerankWithKeyword !== false,
|
|
199
|
+
rerankWeights: opts.rerankWeights,
|
|
200
|
+
decayPolicy: opts.decayPolicy,
|
|
201
|
+
graphHop: opts.graphHop,
|
|
202
|
+
projectScopeMode: opts.projectScopeMode,
|
|
203
|
+
projectHash: opts.projectHash,
|
|
204
|
+
allowedProjectHashes: opts.allowedProjectHashes
|
|
205
|
+
});
|
|
206
|
+
fallbackTrace.push('fallback:deep');
|
|
207
|
+
}
|
|
114
208
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
209
|
+
// Stage 3: scope-expanded deep fallback
|
|
210
|
+
if (fallbackEnabled && this.shouldFallback(current.matchResult, current.results)) {
|
|
211
|
+
current = await this.runStage(query, {
|
|
212
|
+
strategy: 'deep',
|
|
213
|
+
topK: opts.topK,
|
|
214
|
+
minScore: Math.max(0.5, opts.minScore - 0.15),
|
|
215
|
+
sessionId: undefined,
|
|
216
|
+
scope: undefined,
|
|
217
|
+
rerankWithKeyword: true,
|
|
218
|
+
rerankWeights: opts.rerankWeights,
|
|
219
|
+
decayPolicy: opts.decayPolicy,
|
|
220
|
+
graphHop: opts.graphHop,
|
|
221
|
+
projectScopeMode: opts.projectScopeMode,
|
|
222
|
+
projectHash: opts.projectHash,
|
|
223
|
+
allowedProjectHashes: opts.allowedProjectHashes
|
|
224
|
+
});
|
|
225
|
+
fallbackTrace.push('fallback:scope-expanded');
|
|
226
|
+
}
|
|
120
227
|
|
|
121
|
-
//
|
|
122
|
-
|
|
228
|
+
// Stage 4: summary fallback
|
|
229
|
+
if (fallbackEnabled && this.shouldFallback(current.matchResult, current.results)) {
|
|
230
|
+
const summary = await this.buildSummaryFallback(query, opts.topK);
|
|
231
|
+
current = {
|
|
232
|
+
results: summary,
|
|
233
|
+
candidateResults: summary,
|
|
234
|
+
matchResult: this.matcher.matchSearchResults(summary, () => 0)
|
|
235
|
+
};
|
|
236
|
+
fallbackTrace.push('fallback:summary');
|
|
237
|
+
}
|
|
123
238
|
|
|
124
|
-
|
|
239
|
+
const memories = await this.enrichResults(current.results.slice(0, opts.topK), opts as RetrievalOptions);
|
|
125
240
|
const context = this.buildContext(memories, opts.maxTokens);
|
|
126
241
|
|
|
127
242
|
return {
|
|
128
243
|
memories,
|
|
129
|
-
matchResult,
|
|
244
|
+
matchResult: current.matchResult,
|
|
130
245
|
totalTokens: this.estimateTokens(context),
|
|
131
|
-
context
|
|
246
|
+
context,
|
|
247
|
+
fallbackTrace,
|
|
248
|
+
selectedDebug: current.results.slice(0, opts.topK).map((r: SearchResult & { semanticScore?: number; lexicalScore?: number; recencyScore?: number }) => ({
|
|
249
|
+
eventId: r.eventId,
|
|
250
|
+
score: r.score,
|
|
251
|
+
semanticScore: r.semanticScore,
|
|
252
|
+
lexicalScore: r.lexicalScore,
|
|
253
|
+
recencyScore: r.recencyScore,
|
|
254
|
+
})),
|
|
255
|
+
candidateDebug: (current.candidateResults || []).slice(0, Math.max(opts.topK * 3, 20)).map((r: SearchResult & { semanticScore?: number; lexicalScore?: number; recencyScore?: number }) => ({
|
|
256
|
+
eventId: r.eventId,
|
|
257
|
+
score: r.score,
|
|
258
|
+
semanticScore: r.semanticScore,
|
|
259
|
+
lexicalScore: r.lexicalScore,
|
|
260
|
+
recencyScore: r.recencyScore,
|
|
261
|
+
}))
|
|
132
262
|
};
|
|
133
263
|
}
|
|
134
264
|
|
|
135
|
-
/**
|
|
136
|
-
* Retrieve with unified search (project + shared)
|
|
137
|
-
*/
|
|
138
265
|
async retrieveUnified(
|
|
139
266
|
query: string,
|
|
140
267
|
options: Partial<UnifiedRetrievalOptions> = {}
|
|
141
268
|
): Promise<UnifiedRetrievalResult> {
|
|
142
|
-
// Get project-local results first
|
|
143
269
|
const projectResult = await this.retrieve(query, options);
|
|
144
270
|
|
|
145
|
-
// If shared search is not requested or stores not available, return project results only
|
|
146
271
|
if (!options.includeShared || !this.sharedStore || !this.sharedVectorStore) {
|
|
147
272
|
return projectResult;
|
|
148
273
|
}
|
|
149
274
|
|
|
150
275
|
try {
|
|
151
|
-
// Generate query embedding (reuse if possible)
|
|
152
276
|
const queryEmbedding = await this.embedder.embed(query);
|
|
277
|
+
const sharedVectorResults = await this.sharedVectorStore.search(queryEmbedding.vector, {
|
|
278
|
+
limit: options.topK || 5,
|
|
279
|
+
minScore: options.minScore || 0.7,
|
|
280
|
+
excludeProjectHash: options.projectHash
|
|
281
|
+
});
|
|
153
282
|
|
|
154
|
-
// Vector search in shared store
|
|
155
|
-
const sharedVectorResults = await this.sharedVectorStore.search(
|
|
156
|
-
queryEmbedding.vector,
|
|
157
|
-
{
|
|
158
|
-
limit: options.topK || 5,
|
|
159
|
-
minScore: options.minScore || 0.7,
|
|
160
|
-
excludeProjectHash: options.projectHash
|
|
161
|
-
}
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
// Get full entries from shared store
|
|
165
283
|
const sharedMemories: SharedTroubleshootingEntry[] = [];
|
|
166
284
|
for (const result of sharedVectorResults) {
|
|
167
285
|
const entry = await this.sharedStore.get(result.entryId);
|
|
168
|
-
if (entry)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// Record usage for ranking
|
|
173
|
-
await this.sharedStore.recordUsage(entry.entryId);
|
|
174
|
-
}
|
|
286
|
+
if (!entry) continue;
|
|
287
|
+
if (!options.projectHash || entry.sourceProjectHash !== options.projectHash) {
|
|
288
|
+
sharedMemories.push(entry);
|
|
289
|
+
await this.sharedStore.recordUsage(entry.entryId);
|
|
175
290
|
}
|
|
176
291
|
}
|
|
177
292
|
|
|
178
|
-
// Build unified context
|
|
179
293
|
const unifiedContext = this.buildUnifiedContext(projectResult, sharedMemories);
|
|
180
|
-
|
|
181
294
|
return {
|
|
182
295
|
...projectResult,
|
|
183
296
|
context: unifiedContext,
|
|
@@ -185,74 +298,351 @@ export class Retriever {
|
|
|
185
298
|
sharedMemories
|
|
186
299
|
};
|
|
187
300
|
} catch (error) {
|
|
188
|
-
// If shared search fails, return project results only
|
|
189
301
|
console.error('Shared search failed:', error);
|
|
190
302
|
return projectResult;
|
|
191
303
|
}
|
|
192
304
|
}
|
|
193
305
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
306
|
+
private async runStage(
|
|
307
|
+
query: string,
|
|
308
|
+
input: {
|
|
309
|
+
strategy: RetrievalStrategy;
|
|
310
|
+
topK: number;
|
|
311
|
+
minScore: number;
|
|
312
|
+
sessionId?: string;
|
|
313
|
+
scope?: RetrievalScope;
|
|
314
|
+
rerankWithKeyword: boolean;
|
|
315
|
+
rerankWeights?: {
|
|
316
|
+
semantic?: number;
|
|
317
|
+
lexical?: number;
|
|
318
|
+
recency?: number;
|
|
319
|
+
};
|
|
320
|
+
decayPolicy?: {
|
|
321
|
+
enabled?: boolean;
|
|
322
|
+
windowDays?: number;
|
|
323
|
+
maxPenalty?: number;
|
|
324
|
+
};
|
|
325
|
+
intentRewrite?: boolean;
|
|
326
|
+
graphHop?: {
|
|
327
|
+
enabled?: boolean;
|
|
328
|
+
maxHops?: number;
|
|
329
|
+
hopPenalty?: number;
|
|
330
|
+
};
|
|
331
|
+
projectScopeMode?: ProjectScopeMode;
|
|
332
|
+
projectHash?: string;
|
|
333
|
+
allowedProjectHashes?: string[];
|
|
334
|
+
}
|
|
335
|
+
): Promise<{ results: SearchResult[]; matchResult: MatchResult }> {
|
|
336
|
+
let initialResults = await this.searchByStrategy(query, {
|
|
337
|
+
strategy: input.strategy,
|
|
338
|
+
topK: input.topK,
|
|
339
|
+
minScore: input.minScore,
|
|
340
|
+
sessionId: input.sessionId
|
|
341
|
+
});
|
|
202
342
|
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
343
|
+
if (input.intentRewrite && input.strategy === 'deep' && this.queryRewriter) {
|
|
344
|
+
const rewritten = (await this.queryRewriter(query))?.trim();
|
|
345
|
+
if (rewritten && rewritten !== query) {
|
|
346
|
+
const rewrittenResults = await this.searchByStrategy(rewritten, {
|
|
347
|
+
strategy: 'deep',
|
|
348
|
+
topK: input.topK,
|
|
349
|
+
minScore: Math.max(0.5, input.minScore - 0.1),
|
|
350
|
+
sessionId: input.sessionId
|
|
351
|
+
});
|
|
352
|
+
initialResults = this.mergeResults(initialResults, rewrittenResults, input.topK * 3);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const expandedResults = input.graphHop?.enabled === false
|
|
357
|
+
? initialResults
|
|
358
|
+
: await this.expandGraphHops(initialResults, {
|
|
359
|
+
maxHops: Math.max(1, input.graphHop?.maxHops ?? 1),
|
|
360
|
+
hopPenalty: Math.max(0, input.graphHop?.hopPenalty ?? 0.08),
|
|
361
|
+
limit: input.topK * 4,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const rerankedResults = input.rerankWithKeyword
|
|
365
|
+
? this.rerankByKeywordOverlap(expandedResults, query, input.rerankWeights, input.decayPolicy)
|
|
366
|
+
: expandedResults;
|
|
367
|
+
|
|
368
|
+
const filtered = await this.applyScopeFilters(rerankedResults, {
|
|
369
|
+
scope: input.scope,
|
|
370
|
+
projectScopeMode: input.projectScopeMode,
|
|
371
|
+
projectHash: input.projectHash,
|
|
372
|
+
allowedProjectHashes: input.allowedProjectHashes
|
|
373
|
+
});
|
|
374
|
+
const top = filtered.slice(0, input.topK);
|
|
375
|
+
const matchResult = this.matcher.matchSearchResults(top, () => 0);
|
|
376
|
+
|
|
377
|
+
return { results: top, candidateResults: filtered, matchResult };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private mergeResults(primary: SearchResult[], secondary: SearchResult[], limit: number): SearchResult[] {
|
|
381
|
+
const byId = new Map<string, SearchResult>();
|
|
382
|
+
for (const row of primary) byId.set(row.eventId, row);
|
|
383
|
+
for (const row of secondary) {
|
|
384
|
+
const prev = byId.get(row.eventId);
|
|
385
|
+
if (!prev || row.score > prev.score) {
|
|
386
|
+
byId.set(row.eventId, row);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return [...byId.values()].sort((a, b) => b.score - a.score).slice(0, limit);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async expandGraphHops(
|
|
393
|
+
seeds: SearchResult[],
|
|
394
|
+
opts: { maxHops: number; hopPenalty: number; limit: number }
|
|
395
|
+
): Promise<SearchResult[]> {
|
|
396
|
+
const byId = new Map<string, SearchResult>();
|
|
397
|
+
for (const s of seeds) byId.set(s.eventId, s);
|
|
398
|
+
|
|
399
|
+
let frontier = seeds.map((s) => ({ row: s, hop: 0 }));
|
|
400
|
+
|
|
401
|
+
for (let hop = 1; hop <= opts.maxHops; hop += 1) {
|
|
402
|
+
const next: Array<{ row: SearchResult; hop: number }> = [];
|
|
403
|
+
|
|
404
|
+
for (const f of frontier) {
|
|
405
|
+
const ev = await this.eventStore.getEvent(f.row.eventId);
|
|
406
|
+
if (!ev) continue;
|
|
407
|
+
const rel = ((ev.metadata as Record<string, unknown> | undefined)?.relatedEventIds ?? []) as unknown;
|
|
408
|
+
const relatedIds = Array.isArray(rel)
|
|
409
|
+
? rel.filter((x): x is string => typeof x === 'string')
|
|
410
|
+
: [];
|
|
411
|
+
|
|
412
|
+
for (const rid of relatedIds) {
|
|
413
|
+
if (byId.has(rid)) continue;
|
|
414
|
+
const target = await this.eventStore.getEvent(rid);
|
|
415
|
+
if (!target) continue;
|
|
416
|
+
|
|
417
|
+
const score = Math.max(0, f.row.score - opts.hopPenalty * hop);
|
|
418
|
+
const row: SearchResult = {
|
|
419
|
+
id: `hop-${hop}-${rid}`,
|
|
420
|
+
eventId: target.id,
|
|
421
|
+
content: target.content,
|
|
422
|
+
score,
|
|
423
|
+
sessionId: target.sessionId,
|
|
424
|
+
eventType: target.eventType,
|
|
425
|
+
timestamp: target.timestamp.toISOString(),
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
byId.set(row.eventId, row);
|
|
429
|
+
next.push({ row, hop });
|
|
430
|
+
if (byId.size >= opts.limit) break;
|
|
209
431
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
432
|
+
if (byId.size >= opts.limit) break;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
frontier = next;
|
|
436
|
+
if (frontier.length === 0 || byId.size >= opts.limit) break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return [...byId.values()].sort((a, b) => b.score - a.score).slice(0, opts.limit);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private shouldFallback(matchResult: MatchResult, results: SearchResult[]): boolean {
|
|
443
|
+
if (results.length === 0) return true;
|
|
444
|
+
if (matchResult.confidence === 'none') return true;
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private async buildSummaryFallback(query: string, topK: number): Promise<SearchResult[]> {
|
|
449
|
+
const recent = await this.eventStore.getRecentEvents(Math.max(topK * 6, 20));
|
|
450
|
+
const q = this.tokenize(query);
|
|
451
|
+
|
|
452
|
+
const ranked = recent
|
|
453
|
+
.map((e) => ({ e, overlap: this.keywordOverlap(q, this.tokenize(e.content)) }))
|
|
454
|
+
.filter((r) => r.overlap > 0)
|
|
455
|
+
.sort((a, b) => b.overlap - a.overlap)
|
|
456
|
+
.slice(0, topK)
|
|
457
|
+
.map((row, idx) => ({
|
|
458
|
+
id: `summary-${row.e.id}`,
|
|
459
|
+
eventId: row.e.id,
|
|
460
|
+
content: row.e.content,
|
|
461
|
+
score: Math.max(0.25, 0.6 - idx * 0.05),
|
|
462
|
+
sessionId: row.e.sessionId,
|
|
463
|
+
eventType: row.e.eventType,
|
|
464
|
+
timestamp: row.e.timestamp.toISOString()
|
|
465
|
+
}));
|
|
466
|
+
|
|
467
|
+
return ranked;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private async searchByStrategy(
|
|
471
|
+
query: string,
|
|
472
|
+
input: { strategy: RetrievalStrategy; topK: number; minScore: number; sessionId?: string }
|
|
473
|
+
): Promise<SearchResult[]> {
|
|
474
|
+
const strategy = input.strategy === 'auto' ? 'deep' : input.strategy;
|
|
475
|
+
|
|
476
|
+
if (strategy === 'fast') {
|
|
477
|
+
const keyword = await this.searchByKeyword(query, {
|
|
478
|
+
limit: Math.max(5, input.topK * 3),
|
|
479
|
+
sessionId: input.sessionId
|
|
480
|
+
});
|
|
481
|
+
return keyword;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const queryEmbedding = await this.embedder.embed(query);
|
|
485
|
+
return this.vectorStore.search(queryEmbedding.vector, {
|
|
486
|
+
limit: Math.max(5, input.topK * 3),
|
|
487
|
+
minScore: input.minScore,
|
|
488
|
+
sessionId: input.sessionId
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private async searchByKeyword(
|
|
493
|
+
query: string,
|
|
494
|
+
input: { limit: number; sessionId?: string }
|
|
495
|
+
): Promise<SearchResult[]> {
|
|
496
|
+
if (this.eventStore.keywordSearch) {
|
|
497
|
+
const rows = await this.eventStore.keywordSearch(query, input.limit);
|
|
498
|
+
const filtered = input.sessionId ? rows.filter((r) => r.event.sessionId === input.sessionId) : rows;
|
|
499
|
+
return filtered.map((row, idx) => ({
|
|
500
|
+
id: `kw-${row.event.id}`,
|
|
501
|
+
eventId: row.event.id,
|
|
502
|
+
content: row.event.content,
|
|
503
|
+
score: Math.max(0.4, 1 - idx * 0.04),
|
|
504
|
+
sessionId: row.event.sessionId,
|
|
505
|
+
eventType: row.event.eventType,
|
|
506
|
+
timestamp: row.event.timestamp.toISOString()
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const recent = await this.eventStore.getRecentEvents(input.limit * 4);
|
|
511
|
+
const tokens = this.tokenize(query);
|
|
512
|
+
const filtered = recent
|
|
513
|
+
.filter((e) => (input.sessionId ? e.sessionId === input.sessionId : true))
|
|
514
|
+
.map((e) => ({ e, overlap: this.keywordOverlap(tokens, this.tokenize(e.content)) }))
|
|
515
|
+
.filter((r) => r.overlap > 0)
|
|
516
|
+
.sort((a, b) => b.overlap - a.overlap)
|
|
517
|
+
.slice(0, input.limit);
|
|
518
|
+
|
|
519
|
+
return filtered.map((row, idx) => ({
|
|
520
|
+
id: `kw-fallback-${row.e.id}`,
|
|
521
|
+
eventId: row.e.id,
|
|
522
|
+
content: row.e.content,
|
|
523
|
+
score: Math.max(0.3, 0.9 - idx * 0.05),
|
|
524
|
+
sessionId: row.e.sessionId,
|
|
525
|
+
eventType: row.e.eventType,
|
|
526
|
+
timestamp: row.e.timestamp.toISOString()
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private rerankByKeywordOverlap(
|
|
531
|
+
results: SearchResult[],
|
|
532
|
+
query: string,
|
|
533
|
+
weights?: { semantic?: number; lexical?: number; recency?: number },
|
|
534
|
+
decayPolicy?: { enabled?: boolean; windowDays?: number; maxPenalty?: number }
|
|
535
|
+
): SearchResult[] {
|
|
536
|
+
const q = this.tokenize(query);
|
|
537
|
+
const now = Date.now();
|
|
538
|
+
|
|
539
|
+
const sw = Math.max(0, weights?.semantic ?? 0.7);
|
|
540
|
+
const lw = Math.max(0, weights?.lexical ?? 0.2);
|
|
541
|
+
const rw = Math.max(0, weights?.recency ?? 0.1);
|
|
542
|
+
const total = sw + lw + rw || 1;
|
|
543
|
+
|
|
544
|
+
const decayEnabled = decayPolicy?.enabled !== false;
|
|
545
|
+
const decayWindow = Math.max(1, decayPolicy?.windowDays ?? 30);
|
|
546
|
+
const decayMaxPenalty = Math.max(0, decayPolicy?.maxPenalty ?? 0.15);
|
|
547
|
+
|
|
548
|
+
return [...results]
|
|
549
|
+
.map((r) => {
|
|
550
|
+
const overlap = this.keywordOverlap(q, this.tokenize(r.content));
|
|
551
|
+
const recencyDays = Math.max(0, (now - new Date(r.timestamp).getTime()) / (1000 * 60 * 60 * 24));
|
|
552
|
+
const recency = Math.max(0, 1 - recencyDays / decayWindow);
|
|
553
|
+
let blended = (r.score * sw + overlap * lw + recency * rw) / total;
|
|
554
|
+
|
|
555
|
+
if (decayEnabled && recencyDays > decayWindow && overlap < 0.5) {
|
|
556
|
+
const ageFactor = Math.min(1, (recencyDays - decayWindow) / decayWindow);
|
|
557
|
+
blended -= decayMaxPenalty * ageFactor;
|
|
214
558
|
}
|
|
215
|
-
|
|
559
|
+
|
|
560
|
+
return { ...r, score: Math.max(0, blended), semanticScore: r.score, lexicalScore: overlap, recencyScore: recency };
|
|
561
|
+
})
|
|
562
|
+
.sort((a, b) => b.score - a.score);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private async applyScopeFilters(
|
|
566
|
+
results: SearchResult[],
|
|
567
|
+
options?: {
|
|
568
|
+
scope?: RetrievalScope;
|
|
569
|
+
projectScopeMode?: ProjectScopeMode;
|
|
570
|
+
projectHash?: string;
|
|
571
|
+
allowedProjectHashes?: string[];
|
|
572
|
+
}
|
|
573
|
+
): Promise<SearchResult[]> {
|
|
574
|
+
const scope = options?.scope;
|
|
575
|
+
const projectScopeMode = options?.projectScopeMode ?? 'global';
|
|
576
|
+
const allowedProjectHashes = new Set(
|
|
577
|
+
[options?.projectHash, ...(options?.allowedProjectHashes || [])].filter(
|
|
578
|
+
(value): value is string => typeof value === 'string' && value.length > 0
|
|
579
|
+
)
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
if (!scope && projectScopeMode === 'global') return results;
|
|
583
|
+
|
|
584
|
+
const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
|
|
585
|
+
const filtered: Array<{ result: SearchResult; projectHash?: string }> = [];
|
|
586
|
+
|
|
587
|
+
for (const result of results) {
|
|
588
|
+
if (scope?.sessionId && result.sessionId !== scope.sessionId) continue;
|
|
589
|
+
if (scope?.sessionIdPrefix && !result.sessionId.startsWith(scope.sessionIdPrefix)) continue;
|
|
590
|
+
if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result.eventType as MemoryEvent['eventType'])) continue;
|
|
591
|
+
|
|
592
|
+
const event = await this.eventStore.getEvent(result.eventId);
|
|
593
|
+
if (!event) continue;
|
|
594
|
+
|
|
595
|
+
if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix)) continue;
|
|
596
|
+
if (normalizedIncludes.length > 0) {
|
|
597
|
+
const lc = event.content.toLowerCase();
|
|
598
|
+
if (!normalizedIncludes.some((needle) => lc.includes(needle))) continue;
|
|
216
599
|
}
|
|
600
|
+
if (scope?.metadata && !this.matchesMetadataScope(event.metadata, scope.metadata)) continue;
|
|
601
|
+
|
|
602
|
+
const projectHash = this.extractProjectHash(event.metadata);
|
|
603
|
+
filtered.push({ result, projectHash });
|
|
217
604
|
}
|
|
218
605
|
|
|
219
|
-
|
|
606
|
+
if (projectScopeMode === 'global' || allowedProjectHashes.size === 0) {
|
|
607
|
+
return filtered.map((x) => x.result);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const projectMatched = filtered.filter((x) => x.projectHash && allowedProjectHashes.has(x.projectHash));
|
|
611
|
+
|
|
612
|
+
if (projectScopeMode === 'strict') {
|
|
613
|
+
return projectMatched.map((x) => x.result);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return (projectMatched.length > 0 ? projectMatched : filtered).map((x) => x.result);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private extractProjectHash(metadata: Record<string, unknown> | undefined): string | undefined {
|
|
620
|
+
if (!metadata || typeof metadata !== 'object') return undefined;
|
|
621
|
+
const scope = metadata.scope;
|
|
622
|
+
if (!scope || typeof scope !== 'object') return undefined;
|
|
623
|
+
const project = (scope as Record<string, unknown>).project;
|
|
624
|
+
if (!project || typeof project !== 'object') return undefined;
|
|
625
|
+
const hash = (project as Record<string, unknown>).hash;
|
|
626
|
+
return typeof hash === 'string' && hash.length > 0 ? hash : undefined;
|
|
220
627
|
}
|
|
221
628
|
|
|
222
|
-
/**
|
|
223
|
-
* Retrieve memories from a specific session
|
|
224
|
-
*/
|
|
225
629
|
async retrieveFromSession(sessionId: string): Promise<MemoryEvent[]> {
|
|
226
630
|
return this.eventStore.getSessionEvents(sessionId);
|
|
227
631
|
}
|
|
228
632
|
|
|
229
|
-
/**
|
|
230
|
-
* Get recent memories across all sessions
|
|
231
|
-
*/
|
|
232
633
|
async retrieveRecent(limit: number = 100): Promise<MemoryEvent[]> {
|
|
233
634
|
return this.eventStore.getRecentEvents(limit);
|
|
234
635
|
}
|
|
235
636
|
|
|
236
|
-
|
|
237
|
-
* Enrich search results with full event data
|
|
238
|
-
*/
|
|
239
|
-
private async enrichResults(
|
|
240
|
-
results: SearchResult[],
|
|
241
|
-
options: RetrievalOptions
|
|
242
|
-
): Promise<MemoryWithContext[]> {
|
|
637
|
+
private async enrichResults(results: SearchResult[], options: RetrievalOptions): Promise<MemoryWithContext[]> {
|
|
243
638
|
const memories: MemoryWithContext[] = [];
|
|
244
639
|
|
|
245
640
|
for (const result of results) {
|
|
246
641
|
const event = await this.eventStore.getEvent(result.eventId);
|
|
247
642
|
if (!event) continue;
|
|
248
643
|
|
|
249
|
-
// Record access for graduation scoring (keep this for graduation logic)
|
|
250
644
|
if (this.graduation) {
|
|
251
|
-
this.graduation.recordAccess(
|
|
252
|
-
event.id,
|
|
253
|
-
options.sessionId || 'unknown',
|
|
254
|
-
result.score
|
|
255
|
-
);
|
|
645
|
+
this.graduation.recordAccess(event.id, options.sessionId || 'unknown', result.score);
|
|
256
646
|
}
|
|
257
647
|
|
|
258
648
|
let sessionContext: string | undefined;
|
|
@@ -260,37 +650,20 @@ export class Retriever {
|
|
|
260
650
|
sessionContext = await this.getSessionContext(event.sessionId, event.id);
|
|
261
651
|
}
|
|
262
652
|
|
|
263
|
-
memories.push({
|
|
264
|
-
event,
|
|
265
|
-
score: result.score,
|
|
266
|
-
sessionContext
|
|
267
|
-
});
|
|
653
|
+
memories.push({ event, score: result.score, sessionContext });
|
|
268
654
|
}
|
|
269
655
|
|
|
270
|
-
// Note: Access count is NOT incremented here anymore.
|
|
271
|
-
// It should be incremented only when memories are actually used in prompts.
|
|
272
|
-
|
|
273
656
|
return memories;
|
|
274
657
|
}
|
|
275
658
|
|
|
276
|
-
|
|
277
|
-
* Get surrounding context from the same session
|
|
278
|
-
*/
|
|
279
|
-
private async getSessionContext(
|
|
280
|
-
sessionId: string,
|
|
281
|
-
eventId: string
|
|
282
|
-
): Promise<string | undefined> {
|
|
659
|
+
private async getSessionContext(sessionId: string, eventId: string): Promise<string | undefined> {
|
|
283
660
|
const sessionEvents = await this.eventStore.getSessionEvents(sessionId);
|
|
284
|
-
|
|
285
|
-
// Find the event index
|
|
286
661
|
const eventIndex = sessionEvents.findIndex(e => e.id === eventId);
|
|
287
662
|
if (eventIndex === -1) return undefined;
|
|
288
663
|
|
|
289
|
-
// Get 1 event before and after for context
|
|
290
664
|
const start = Math.max(0, eventIndex - 1);
|
|
291
665
|
const end = Math.min(sessionEvents.length, eventIndex + 2);
|
|
292
666
|
const contextEvents = sessionEvents.slice(start, end);
|
|
293
|
-
|
|
294
667
|
if (contextEvents.length <= 1) return undefined;
|
|
295
668
|
|
|
296
669
|
return contextEvents
|
|
@@ -299,9 +672,23 @@ export class Retriever {
|
|
|
299
672
|
.join('\n');
|
|
300
673
|
}
|
|
301
674
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
675
|
+
private buildUnifiedContext(projectResult: RetrievalResult, sharedMemories: SharedTroubleshootingEntry[]): string {
|
|
676
|
+
let context = projectResult.context;
|
|
677
|
+
if (sharedMemories.length === 0) return context;
|
|
678
|
+
|
|
679
|
+
context += '\n\n## Cross-Project Knowledge\n\n';
|
|
680
|
+
for (const memory of sharedMemories.slice(0, 3)) {
|
|
681
|
+
context += `### ${memory.title}\n`;
|
|
682
|
+
if (memory.symptoms.length > 0) context += `**Symptoms:** ${memory.symptoms.join(', ')}\n`;
|
|
683
|
+
context += `**Root Cause:** ${memory.rootCause}\n`;
|
|
684
|
+
context += `**Solution:** ${memory.solution}\n`;
|
|
685
|
+
if (memory.technologies && memory.technologies.length > 0) context += `**Technologies:** ${memory.technologies.join(', ')}\n`;
|
|
686
|
+
context += `_Confidence: ${(memory.confidence * 100).toFixed(0)}%_\n\n`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return context;
|
|
690
|
+
}
|
|
691
|
+
|
|
305
692
|
private buildContext(memories: MemoryWithContext[], maxTokens: number): string {
|
|
306
693
|
const parts: string[] = [];
|
|
307
694
|
let currentTokens = 0;
|
|
@@ -309,59 +696,62 @@ export class Retriever {
|
|
|
309
696
|
for (const memory of memories) {
|
|
310
697
|
const memoryText = this.formatMemory(memory);
|
|
311
698
|
const memoryTokens = this.estimateTokens(memoryText);
|
|
312
|
-
|
|
313
|
-
if (currentTokens + memoryTokens > maxTokens) {
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
|
|
699
|
+
if (currentTokens + memoryTokens > maxTokens) break;
|
|
317
700
|
parts.push(memoryText);
|
|
318
701
|
currentTokens += memoryTokens;
|
|
319
702
|
}
|
|
320
703
|
|
|
321
|
-
if (parts.length === 0)
|
|
322
|
-
return '';
|
|
323
|
-
}
|
|
324
|
-
|
|
704
|
+
if (parts.length === 0) return '';
|
|
325
705
|
return `## Relevant Memories\n\n${parts.join('\n\n---\n\n')}`;
|
|
326
706
|
}
|
|
327
707
|
|
|
328
|
-
/**
|
|
329
|
-
* Format a single memory for context
|
|
330
|
-
*/
|
|
331
708
|
private formatMemory(memory: MemoryWithContext): string {
|
|
332
709
|
const { event, score, sessionContext } = memory;
|
|
333
710
|
const date = event.timestamp.toISOString().split('T')[0];
|
|
334
711
|
|
|
335
712
|
let text = `**${event.eventType}** (${date}, score: ${score.toFixed(2)})\n${event.content}`;
|
|
713
|
+
if (sessionContext) text += `\n\n_Context:_ ${sessionContext}`;
|
|
714
|
+
return text;
|
|
715
|
+
}
|
|
336
716
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
717
|
+
private matchesMetadataScope(
|
|
718
|
+
metadata: Record<string, unknown> | undefined,
|
|
719
|
+
expected: Record<string, unknown>
|
|
720
|
+
): boolean {
|
|
721
|
+
if (!metadata) return false;
|
|
340
722
|
|
|
341
|
-
return
|
|
723
|
+
return Object.entries(expected).every(([path, value]) => {
|
|
724
|
+
const actual = path.split('.').reduce<unknown>((acc, key) => {
|
|
725
|
+
if (typeof acc !== 'object' || acc === null) return undefined;
|
|
726
|
+
return (acc as Record<string, unknown>)[key];
|
|
727
|
+
}, metadata);
|
|
728
|
+
|
|
729
|
+
return actual === value;
|
|
730
|
+
});
|
|
342
731
|
}
|
|
343
732
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
733
|
+
private tokenize(text: string): string[] {
|
|
734
|
+
return text
|
|
735
|
+
.toLowerCase()
|
|
736
|
+
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
|
|
737
|
+
.split(/\s+/)
|
|
738
|
+
.filter((t) => t.length >= 2)
|
|
739
|
+
.slice(0, 64);
|
|
350
740
|
}
|
|
351
741
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
742
|
+
private keywordOverlap(a: string[], b: string[]): number {
|
|
743
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
744
|
+
const bs = new Set(b);
|
|
745
|
+
let hit = 0;
|
|
746
|
+
for (const t of a) if (bs.has(t)) hit += 1;
|
|
747
|
+
return hit / a.length;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private estimateTokens(text: string): number {
|
|
751
|
+
return Math.ceil(text.length / 4);
|
|
359
752
|
}
|
|
360
753
|
}
|
|
361
754
|
|
|
362
|
-
/**
|
|
363
|
-
* Create a retriever with default components
|
|
364
|
-
*/
|
|
365
755
|
export function createRetriever(
|
|
366
756
|
eventStore: EventStore,
|
|
367
757
|
vectorStore: VectorStore,
|