claude-memory-layer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/commands/memory-forget.md +42 -0
- package/.claude-plugin/commands/memory-history.md +34 -0
- package/.claude-plugin/commands/memory-import.md +56 -0
- package/.claude-plugin/commands/memory-list.md +37 -0
- package/.claude-plugin/commands/memory-search.md +36 -0
- package/.claude-plugin/commands/memory-stats.md +34 -0
- package/.claude-plugin/hooks.json +59 -0
- package/.claude-plugin/plugin.json +24 -0
- package/.history/package_20260201112328.json +45 -0
- package/.history/package_20260201113602.json +45 -0
- package/.history/package_20260201113713.json +45 -0
- package/.history/package_20260201114110.json +45 -0
- package/Memo.txt +558 -0
- package/README.md +520 -0
- package/context.md +636 -0
- package/dist/.claude-plugin/commands/memory-forget.md +42 -0
- package/dist/.claude-plugin/commands/memory-history.md +34 -0
- package/dist/.claude-plugin/commands/memory-import.md +56 -0
- package/dist/.claude-plugin/commands/memory-list.md +37 -0
- package/dist/.claude-plugin/commands/memory-search.md +36 -0
- package/dist/.claude-plugin/commands/memory-stats.md +34 -0
- package/dist/.claude-plugin/hooks.json +59 -0
- package/dist/.claude-plugin/plugin.json +24 -0
- package/dist/cli/index.js +3539 -0
- package/dist/cli/index.js.map +7 -0
- package/dist/core/index.js +4408 -0
- package/dist/core/index.js.map +7 -0
- package/dist/hooks/session-end.js +2971 -0
- package/dist/hooks/session-end.js.map +7 -0
- package/dist/hooks/session-start.js +2969 -0
- package/dist/hooks/session-start.js.map +7 -0
- package/dist/hooks/stop.js +3123 -0
- package/dist/hooks/stop.js.map +7 -0
- package/dist/hooks/user-prompt-submit.js +2960 -0
- package/dist/hooks/user-prompt-submit.js.map +7 -0
- package/dist/services/memory-service.js +2931 -0
- package/dist/services/memory-service.js.map +7 -0
- package/package.json +45 -0
- package/plan.md +1642 -0
- package/scripts/build.ts +102 -0
- package/spec.md +624 -0
- package/specs/citations-system/context.md +243 -0
- package/specs/citations-system/plan.md +495 -0
- package/specs/citations-system/spec.md +371 -0
- package/specs/endless-mode/context.md +305 -0
- package/specs/endless-mode/plan.md +620 -0
- package/specs/endless-mode/spec.md +455 -0
- package/specs/entity-edge-model/context.md +401 -0
- package/specs/entity-edge-model/plan.md +459 -0
- package/specs/entity-edge-model/spec.md +391 -0
- package/specs/evidence-aligner-v2/context.md +401 -0
- package/specs/evidence-aligner-v2/plan.md +303 -0
- package/specs/evidence-aligner-v2/spec.md +312 -0
- package/specs/mcp-desktop-integration/context.md +278 -0
- package/specs/mcp-desktop-integration/plan.md +550 -0
- package/specs/mcp-desktop-integration/spec.md +494 -0
- package/specs/post-tool-use-hook/context.md +319 -0
- package/specs/post-tool-use-hook/plan.md +469 -0
- package/specs/post-tool-use-hook/spec.md +364 -0
- package/specs/private-tags/context.md +288 -0
- package/specs/private-tags/plan.md +412 -0
- package/specs/private-tags/spec.md +345 -0
- package/specs/progressive-disclosure/context.md +346 -0
- package/specs/progressive-disclosure/plan.md +663 -0
- package/specs/progressive-disclosure/spec.md +415 -0
- package/specs/task-entity-system/context.md +297 -0
- package/specs/task-entity-system/plan.md +301 -0
- package/specs/task-entity-system/spec.md +314 -0
- package/specs/vector-outbox-v2/context.md +470 -0
- package/specs/vector-outbox-v2/plan.md +562 -0
- package/specs/vector-outbox-v2/spec.md +466 -0
- package/specs/web-viewer-ui/context.md +384 -0
- package/specs/web-viewer-ui/plan.md +797 -0
- package/specs/web-viewer-ui/spec.md +516 -0
- package/src/cli/index.ts +570 -0
- package/src/core/canonical-key.ts +186 -0
- package/src/core/citation-generator.ts +63 -0
- package/src/core/consolidated-store.ts +279 -0
- package/src/core/consolidation-worker.ts +384 -0
- package/src/core/context-formatter.ts +276 -0
- package/src/core/continuity-manager.ts +336 -0
- package/src/core/edge-repo.ts +324 -0
- package/src/core/embedder.ts +124 -0
- package/src/core/entity-repo.ts +342 -0
- package/src/core/event-store.ts +672 -0
- package/src/core/evidence-aligner.ts +635 -0
- package/src/core/graduation.ts +365 -0
- package/src/core/index.ts +32 -0
- package/src/core/matcher.ts +210 -0
- package/src/core/metadata-extractor.ts +203 -0
- package/src/core/privacy/filter.ts +179 -0
- package/src/core/privacy/index.ts +20 -0
- package/src/core/privacy/tag-parser.ts +145 -0
- package/src/core/progressive-retriever.ts +415 -0
- package/src/core/retriever.ts +235 -0
- package/src/core/task/blocker-resolver.ts +325 -0
- package/src/core/task/index.ts +9 -0
- package/src/core/task/task-matcher.ts +238 -0
- package/src/core/task/task-projector.ts +345 -0
- package/src/core/task/task-resolver.ts +414 -0
- package/src/core/types.ts +841 -0
- package/src/core/vector-outbox.ts +295 -0
- package/src/core/vector-store.ts +182 -0
- package/src/core/vector-worker.ts +488 -0
- package/src/core/working-set-store.ts +244 -0
- package/src/hooks/post-tool-use.ts +127 -0
- package/src/hooks/session-end.ts +78 -0
- package/src/hooks/session-start.ts +57 -0
- package/src/hooks/stop.ts +78 -0
- package/src/hooks/user-prompt-submit.ts +54 -0
- package/src/mcp/handlers.ts +212 -0
- package/src/mcp/index.ts +47 -0
- package/src/mcp/tools.ts +78 -0
- package/src/server/api/citations.ts +101 -0
- package/src/server/api/events.ts +101 -0
- package/src/server/api/index.ts +18 -0
- package/src/server/api/search.ts +98 -0
- package/src/server/api/sessions.ts +111 -0
- package/src/server/api/stats.ts +97 -0
- package/src/server/index.ts +91 -0
- package/src/services/memory-service.ts +626 -0
- package/src/services/session-history-importer.ts +367 -0
- package/tests/canonical-key.test.ts +101 -0
- package/tests/evidence-aligner.test.ts +152 -0
- package/tests/matcher.test.ts +112 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector Outbox V2 - Transactional Outbox Pattern
|
|
3
|
+
* AXIOMMIND Principle 6: DuckDB → outbox → LanceDB unidirectional flow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Database } from 'duckdb';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import type {
|
|
9
|
+
OutboxJob,
|
|
10
|
+
OutboxStatus,
|
|
11
|
+
OutboxItemKind,
|
|
12
|
+
VALID_OUTBOX_TRANSITIONS
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
export interface OutboxConfig {
|
|
16
|
+
embeddingVersion: string;
|
|
17
|
+
maxRetries: number;
|
|
18
|
+
stuckThresholdMs: number;
|
|
19
|
+
cleanupDays: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_CONFIG: OutboxConfig = {
|
|
23
|
+
embeddingVersion: 'v1',
|
|
24
|
+
maxRetries: 3,
|
|
25
|
+
stuckThresholdMs: 5 * 60 * 1000, // 5 minutes
|
|
26
|
+
cleanupDays: 7
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface OutboxMetrics {
|
|
30
|
+
pendingCount: number;
|
|
31
|
+
processingCount: number;
|
|
32
|
+
doneCount: number;
|
|
33
|
+
failedCount: number;
|
|
34
|
+
oldestPendingAge: number | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class VectorOutbox {
|
|
38
|
+
private config: OutboxConfig;
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
private db: Database,
|
|
42
|
+
config?: Partial<OutboxConfig>
|
|
43
|
+
) {
|
|
44
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Enqueue item for vectorization (idempotent)
|
|
49
|
+
*/
|
|
50
|
+
async enqueue(
|
|
51
|
+
itemKind: OutboxItemKind,
|
|
52
|
+
itemId: string,
|
|
53
|
+
embeddingVersion?: string
|
|
54
|
+
): Promise<string> {
|
|
55
|
+
const version = embeddingVersion ?? this.config.embeddingVersion;
|
|
56
|
+
const jobId = randomUUID();
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
|
|
59
|
+
await this.db.run(
|
|
60
|
+
`INSERT INTO vector_outbox (
|
|
61
|
+
job_id, item_kind, item_id, embedding_version, status, retry_count, created_at, updated_at
|
|
62
|
+
) VALUES (?, ?, ?, ?, 'pending', 0, ?, ?)
|
|
63
|
+
ON CONFLICT (item_kind, item_id, embedding_version) DO NOTHING`,
|
|
64
|
+
[jobId, itemKind, itemId, version, now, now]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return jobId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Claim pending jobs for processing
|
|
72
|
+
*/
|
|
73
|
+
async claimJobs(limit: number = 32): Promise<OutboxJob[]> {
|
|
74
|
+
const now = new Date().toISOString();
|
|
75
|
+
|
|
76
|
+
// Atomic claim using UPDATE RETURNING
|
|
77
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
78
|
+
`UPDATE vector_outbox
|
|
79
|
+
SET status = 'processing', updated_at = ?
|
|
80
|
+
WHERE job_id IN (
|
|
81
|
+
SELECT job_id FROM vector_outbox
|
|
82
|
+
WHERE status = 'pending'
|
|
83
|
+
ORDER BY created_at ASC
|
|
84
|
+
LIMIT ?
|
|
85
|
+
)
|
|
86
|
+
RETURNING *`,
|
|
87
|
+
[now, limit]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return rows.map(row => this.rowToJob(row));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Mark job as done
|
|
95
|
+
*/
|
|
96
|
+
async markDone(jobId: string): Promise<void> {
|
|
97
|
+
await this.db.run(
|
|
98
|
+
`UPDATE vector_outbox
|
|
99
|
+
SET status = 'done', updated_at = ?
|
|
100
|
+
WHERE job_id = ?`,
|
|
101
|
+
[new Date().toISOString(), jobId]
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Mark job as failed
|
|
107
|
+
*/
|
|
108
|
+
async markFailed(jobId: string, error: string): Promise<void> {
|
|
109
|
+
const now = new Date().toISOString();
|
|
110
|
+
|
|
111
|
+
// Check retry count
|
|
112
|
+
const rows = await this.db.all<Array<{ retry_count: number }>>(
|
|
113
|
+
`SELECT retry_count FROM vector_outbox WHERE job_id = ?`,
|
|
114
|
+
[jobId]
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (rows.length === 0) return;
|
|
118
|
+
|
|
119
|
+
const retryCount = rows[0].retry_count;
|
|
120
|
+
const newStatus: OutboxStatus = retryCount >= this.config.maxRetries - 1
|
|
121
|
+
? 'failed'
|
|
122
|
+
: 'pending'; // Will retry
|
|
123
|
+
|
|
124
|
+
await this.db.run(
|
|
125
|
+
`UPDATE vector_outbox
|
|
126
|
+
SET status = ?, error = ?, retry_count = retry_count + 1, updated_at = ?
|
|
127
|
+
WHERE job_id = ?`,
|
|
128
|
+
[newStatus, error, now, jobId]
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get job by ID
|
|
134
|
+
*/
|
|
135
|
+
async getJob(jobId: string): Promise<OutboxJob | null> {
|
|
136
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
137
|
+
`SELECT * FROM vector_outbox WHERE job_id = ?`,
|
|
138
|
+
[jobId]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (rows.length === 0) return null;
|
|
142
|
+
return this.rowToJob(rows[0]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get jobs by status
|
|
147
|
+
*/
|
|
148
|
+
async getJobsByStatus(status: OutboxStatus, limit: number = 100): Promise<OutboxJob[]> {
|
|
149
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
150
|
+
`SELECT * FROM vector_outbox
|
|
151
|
+
WHERE status = ?
|
|
152
|
+
ORDER BY created_at ASC
|
|
153
|
+
LIMIT ?`,
|
|
154
|
+
[status, limit]
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return rows.map(row => this.rowToJob(row));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Reconcile: recover stuck and retry failed jobs
|
|
162
|
+
*/
|
|
163
|
+
async reconcile(): Promise<{ recovered: number; retried: number }> {
|
|
164
|
+
const now = new Date();
|
|
165
|
+
const stuckThreshold = new Date(now.getTime() - this.config.stuckThresholdMs);
|
|
166
|
+
|
|
167
|
+
// Recover stuck processing jobs
|
|
168
|
+
const recoveredResult = await this.db.run(
|
|
169
|
+
`UPDATE vector_outbox
|
|
170
|
+
SET status = 'pending', updated_at = ?
|
|
171
|
+
WHERE status = 'processing'
|
|
172
|
+
AND updated_at < ?`,
|
|
173
|
+
[now.toISOString(), stuckThreshold.toISOString()]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Retry failed jobs that haven't exceeded max retries
|
|
177
|
+
const retriedResult = await this.db.run(
|
|
178
|
+
`UPDATE vector_outbox
|
|
179
|
+
SET status = 'pending', updated_at = ?
|
|
180
|
+
WHERE status = 'failed'
|
|
181
|
+
AND retry_count < ?`,
|
|
182
|
+
[now.toISOString(), this.config.maxRetries]
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Get counts (DuckDB doesn't return affected rows easily)
|
|
186
|
+
const recoveredRows = await this.db.all<Array<{ count: number }>>(
|
|
187
|
+
`SELECT COUNT(*) as count FROM vector_outbox
|
|
188
|
+
WHERE status = 'pending' AND updated_at = ?`,
|
|
189
|
+
[now.toISOString()]
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
recovered: 0, // Approximate
|
|
194
|
+
retried: 0 // Approximate
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Cleanup old done jobs
|
|
200
|
+
*/
|
|
201
|
+
async cleanup(): Promise<number> {
|
|
202
|
+
const threshold = new Date();
|
|
203
|
+
threshold.setDate(threshold.getDate() - this.config.cleanupDays);
|
|
204
|
+
|
|
205
|
+
await this.db.run(
|
|
206
|
+
`DELETE FROM vector_outbox
|
|
207
|
+
WHERE status = 'done'
|
|
208
|
+
AND updated_at < ?`,
|
|
209
|
+
[threshold.toISOString()]
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return 0; // DuckDB doesn't return affected rows easily
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get metrics
|
|
217
|
+
*/
|
|
218
|
+
async getMetrics(): Promise<OutboxMetrics> {
|
|
219
|
+
const statusCounts = await this.db.all<Array<{ status: string; count: number }>>(
|
|
220
|
+
`SELECT status, COUNT(*) as count
|
|
221
|
+
FROM vector_outbox
|
|
222
|
+
GROUP BY status`
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const oldestPending = await this.db.all<Array<{ created_at: string }>>(
|
|
226
|
+
`SELECT created_at FROM vector_outbox
|
|
227
|
+
WHERE status = 'pending'
|
|
228
|
+
ORDER BY created_at ASC
|
|
229
|
+
LIMIT 1`
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const metrics: OutboxMetrics = {
|
|
233
|
+
pendingCount: 0,
|
|
234
|
+
processingCount: 0,
|
|
235
|
+
doneCount: 0,
|
|
236
|
+
failedCount: 0,
|
|
237
|
+
oldestPendingAge: null
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
for (const row of statusCounts) {
|
|
241
|
+
switch (row.status) {
|
|
242
|
+
case 'pending':
|
|
243
|
+
metrics.pendingCount = Number(row.count);
|
|
244
|
+
break;
|
|
245
|
+
case 'processing':
|
|
246
|
+
metrics.processingCount = Number(row.count);
|
|
247
|
+
break;
|
|
248
|
+
case 'done':
|
|
249
|
+
metrics.doneCount = Number(row.count);
|
|
250
|
+
break;
|
|
251
|
+
case 'failed':
|
|
252
|
+
metrics.failedCount = Number(row.count);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (oldestPending.length > 0) {
|
|
258
|
+
const oldestDate = new Date(oldestPending[0].created_at);
|
|
259
|
+
metrics.oldestPendingAge = Date.now() - oldestDate.getTime();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return metrics;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Validate state transition
|
|
267
|
+
*/
|
|
268
|
+
isValidTransition(from: OutboxStatus, to: OutboxStatus): boolean {
|
|
269
|
+
const validTransitions = [
|
|
270
|
+
{ from: 'pending', to: 'processing' },
|
|
271
|
+
{ from: 'processing', to: 'done' },
|
|
272
|
+
{ from: 'processing', to: 'failed' },
|
|
273
|
+
{ from: 'failed', to: 'pending' }
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
return validTransitions.some(t => t.from === from && t.to === to);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Convert database row to OutboxJob
|
|
281
|
+
*/
|
|
282
|
+
private rowToJob(row: Record<string, unknown>): OutboxJob {
|
|
283
|
+
return {
|
|
284
|
+
jobId: row.job_id as string,
|
|
285
|
+
itemKind: row.item_kind as OutboxItemKind,
|
|
286
|
+
itemId: row.item_id as string,
|
|
287
|
+
embeddingVersion: row.embedding_version as string,
|
|
288
|
+
status: row.status as OutboxStatus,
|
|
289
|
+
retryCount: row.retry_count as number,
|
|
290
|
+
error: row.error as string | undefined,
|
|
291
|
+
createdAt: new Date(row.created_at as string),
|
|
292
|
+
updatedAt: new Date(row.updated_at as string)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LanceDB Vector Store for semantic search
|
|
3
|
+
* AXIOMMIND Principle 6: Vector store consistency (DuckDB → outbox → LanceDB unidirectional)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as lancedb from '@lancedb/lancedb';
|
|
7
|
+
import type { VectorRecord } from './types.js';
|
|
8
|
+
|
|
9
|
+
export interface SearchResult {
|
|
10
|
+
id: string;
|
|
11
|
+
eventId: string;
|
|
12
|
+
content: string;
|
|
13
|
+
score: number;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
eventType: string;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class VectorStore {
|
|
20
|
+
private db: lancedb.Connection | null = null;
|
|
21
|
+
private table: lancedb.Table | null = null;
|
|
22
|
+
private readonly tableName = 'conversations';
|
|
23
|
+
|
|
24
|
+
constructor(private dbPath: string) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize LanceDB connection
|
|
28
|
+
*/
|
|
29
|
+
async initialize(): Promise<void> {
|
|
30
|
+
if (this.db) return;
|
|
31
|
+
|
|
32
|
+
this.db = await lancedb.connect(this.dbPath);
|
|
33
|
+
|
|
34
|
+
// Try to open existing table
|
|
35
|
+
try {
|
|
36
|
+
const tables = await this.db.tableNames();
|
|
37
|
+
if (tables.includes(this.tableName)) {
|
|
38
|
+
this.table = await this.db.openTable(this.tableName);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Table doesn't exist yet, will be created on first insert
|
|
42
|
+
this.table = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Add or update vector record
|
|
48
|
+
*/
|
|
49
|
+
async upsert(record: VectorRecord): Promise<void> {
|
|
50
|
+
await this.initialize();
|
|
51
|
+
|
|
52
|
+
if (!this.db) {
|
|
53
|
+
throw new Error('Database not initialized');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = {
|
|
57
|
+
id: record.id,
|
|
58
|
+
eventId: record.eventId,
|
|
59
|
+
sessionId: record.sessionId,
|
|
60
|
+
eventType: record.eventType,
|
|
61
|
+
content: record.content,
|
|
62
|
+
vector: record.vector,
|
|
63
|
+
timestamp: record.timestamp,
|
|
64
|
+
metadata: JSON.stringify(record.metadata || {})
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (!this.table) {
|
|
68
|
+
// Create table with first record
|
|
69
|
+
this.table = await this.db.createTable(this.tableName, [data]);
|
|
70
|
+
} else {
|
|
71
|
+
await this.table.add([data]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Add multiple vector records in batch
|
|
77
|
+
*/
|
|
78
|
+
async upsertBatch(records: VectorRecord[]): Promise<void> {
|
|
79
|
+
if (records.length === 0) return;
|
|
80
|
+
|
|
81
|
+
await this.initialize();
|
|
82
|
+
|
|
83
|
+
if (!this.db) {
|
|
84
|
+
throw new Error('Database not initialized');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const data = records.map(record => ({
|
|
88
|
+
id: record.id,
|
|
89
|
+
eventId: record.eventId,
|
|
90
|
+
sessionId: record.sessionId,
|
|
91
|
+
eventType: record.eventType,
|
|
92
|
+
content: record.content,
|
|
93
|
+
vector: record.vector,
|
|
94
|
+
timestamp: record.timestamp,
|
|
95
|
+
metadata: JSON.stringify(record.metadata || {})
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
if (!this.table) {
|
|
99
|
+
this.table = await this.db.createTable(this.tableName, data);
|
|
100
|
+
} else {
|
|
101
|
+
await this.table.add(data);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Search for similar vectors
|
|
107
|
+
*/
|
|
108
|
+
async search(
|
|
109
|
+
queryVector: number[],
|
|
110
|
+
options: {
|
|
111
|
+
limit?: number;
|
|
112
|
+
minScore?: number;
|
|
113
|
+
sessionId?: string;
|
|
114
|
+
} = {}
|
|
115
|
+
): Promise<SearchResult[]> {
|
|
116
|
+
await this.initialize();
|
|
117
|
+
|
|
118
|
+
if (!this.table) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const { limit = 5, minScore = 0.7, sessionId } = options;
|
|
123
|
+
|
|
124
|
+
let query = this.table.search(queryVector).limit(limit * 2); // Get more for filtering
|
|
125
|
+
|
|
126
|
+
// Apply session filter if specified
|
|
127
|
+
if (sessionId) {
|
|
128
|
+
query = query.where(`sessionId = '${sessionId}'`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const results = await query.toArray();
|
|
132
|
+
|
|
133
|
+
return results
|
|
134
|
+
.filter(r => {
|
|
135
|
+
// Convert distance to score (assuming cosine distance)
|
|
136
|
+
const score = 1 - (r._distance || 0);
|
|
137
|
+
return score >= minScore;
|
|
138
|
+
})
|
|
139
|
+
.slice(0, limit)
|
|
140
|
+
.map(r => ({
|
|
141
|
+
id: r.id as string,
|
|
142
|
+
eventId: r.eventId as string,
|
|
143
|
+
content: r.content as string,
|
|
144
|
+
score: 1 - (r._distance || 0),
|
|
145
|
+
sessionId: r.sessionId as string,
|
|
146
|
+
eventType: r.eventType as string,
|
|
147
|
+
timestamp: r.timestamp as string
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Delete vector by event ID
|
|
153
|
+
*/
|
|
154
|
+
async delete(eventId: string): Promise<void> {
|
|
155
|
+
if (!this.table) return;
|
|
156
|
+
await this.table.delete(`eventId = '${eventId}'`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get total count of vectors
|
|
161
|
+
*/
|
|
162
|
+
async count(): Promise<number> {
|
|
163
|
+
if (!this.table) return 0;
|
|
164
|
+
const result = await this.table.countRows();
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if vector exists for event
|
|
170
|
+
*/
|
|
171
|
+
async exists(eventId: string): Promise<boolean> {
|
|
172
|
+
if (!this.table) return false;
|
|
173
|
+
|
|
174
|
+
const results = await this.table
|
|
175
|
+
.search([])
|
|
176
|
+
.where(`eventId = '${eventId}'`)
|
|
177
|
+
.limit(1)
|
|
178
|
+
.toArray();
|
|
179
|
+
|
|
180
|
+
return results.length > 0;
|
|
181
|
+
}
|
|
182
|
+
}
|