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,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector Worker - Single-Writer Pattern Implementation
|
|
3
|
+
* AXIOMMIND Principle 6: DuckDB → outbox → LanceDB unidirectional flow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventStore } from './event-store.js';
|
|
7
|
+
import { VectorStore } from './vector-store.js';
|
|
8
|
+
import { Embedder } from './embedder.js';
|
|
9
|
+
import type { OutboxItem, VectorRecord } from './types.js';
|
|
10
|
+
|
|
11
|
+
export interface WorkerConfig {
|
|
12
|
+
batchSize: number;
|
|
13
|
+
pollIntervalMs: number;
|
|
14
|
+
maxRetries: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_CONFIG: WorkerConfig = {
|
|
18
|
+
batchSize: 32,
|
|
19
|
+
pollIntervalMs: 1000,
|
|
20
|
+
maxRetries: 3
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class VectorWorker {
|
|
24
|
+
private readonly eventStore: EventStore;
|
|
25
|
+
private readonly vectorStore: VectorStore;
|
|
26
|
+
private readonly embedder: Embedder;
|
|
27
|
+
private readonly config: WorkerConfig;
|
|
28
|
+
private running = false;
|
|
29
|
+
private pollTimeout: NodeJS.Timeout | null = null;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
eventStore: EventStore,
|
|
33
|
+
vectorStore: VectorStore,
|
|
34
|
+
embedder: Embedder,
|
|
35
|
+
config: Partial<WorkerConfig> = {}
|
|
36
|
+
) {
|
|
37
|
+
this.eventStore = eventStore;
|
|
38
|
+
this.vectorStore = vectorStore;
|
|
39
|
+
this.embedder = embedder;
|
|
40
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Start the worker polling loop
|
|
45
|
+
*/
|
|
46
|
+
start(): void {
|
|
47
|
+
if (this.running) return;
|
|
48
|
+
this.running = true;
|
|
49
|
+
this.poll();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Stop the worker
|
|
54
|
+
*/
|
|
55
|
+
stop(): void {
|
|
56
|
+
this.running = false;
|
|
57
|
+
if (this.pollTimeout) {
|
|
58
|
+
clearTimeout(this.pollTimeout);
|
|
59
|
+
this.pollTimeout = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Process a single batch of outbox items
|
|
65
|
+
*/
|
|
66
|
+
async processBatch(): Promise<number> {
|
|
67
|
+
const items = await this.eventStore.getPendingOutboxItems(this.config.batchSize);
|
|
68
|
+
|
|
69
|
+
if (items.length === 0) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const successful: string[] = [];
|
|
74
|
+
const failed: string[] = [];
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Generate embeddings for all items
|
|
78
|
+
const embeddings = await this.embedder.embedBatch(items.map(i => i.content));
|
|
79
|
+
|
|
80
|
+
// Prepare vector records
|
|
81
|
+
const records: VectorRecord[] = [];
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < items.length; i++) {
|
|
84
|
+
const item = items[i];
|
|
85
|
+
const embedding = embeddings[i];
|
|
86
|
+
|
|
87
|
+
// Get event details
|
|
88
|
+
const event = await this.eventStore.getEvent(item.eventId);
|
|
89
|
+
if (!event) {
|
|
90
|
+
failed.push(item.id);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
records.push({
|
|
95
|
+
id: `vec_${item.id}`,
|
|
96
|
+
eventId: item.eventId,
|
|
97
|
+
sessionId: event.sessionId,
|
|
98
|
+
eventType: event.eventType,
|
|
99
|
+
content: item.content,
|
|
100
|
+
vector: embedding.vector,
|
|
101
|
+
timestamp: event.timestamp.toISOString(),
|
|
102
|
+
metadata: event.metadata
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
successful.push(item.id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Batch insert to vector store
|
|
109
|
+
if (records.length > 0) {
|
|
110
|
+
await this.vectorStore.upsertBatch(records);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Mark successful items as done
|
|
114
|
+
if (successful.length > 0) {
|
|
115
|
+
await this.eventStore.completeOutboxItems(successful);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Mark failed items
|
|
119
|
+
if (failed.length > 0) {
|
|
120
|
+
await this.eventStore.failOutboxItems(failed, 'Event not found');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return successful.length;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
// Mark all items as failed
|
|
126
|
+
const allIds = items.map(i => i.id);
|
|
127
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
128
|
+
await this.eventStore.failOutboxItems(allIds, errorMessage);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Poll for new items
|
|
135
|
+
*/
|
|
136
|
+
private async poll(): Promise<void> {
|
|
137
|
+
if (!this.running) return;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await this.processBatch();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Vector worker error:', error);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Schedule next poll
|
|
146
|
+
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Process all pending items (blocking)
|
|
151
|
+
*/
|
|
152
|
+
async processAll(): Promise<number> {
|
|
153
|
+
let totalProcessed = 0;
|
|
154
|
+
let processed: number;
|
|
155
|
+
|
|
156
|
+
do {
|
|
157
|
+
processed = await this.processBatch();
|
|
158
|
+
totalProcessed += processed;
|
|
159
|
+
} while (processed > 0);
|
|
160
|
+
|
|
161
|
+
return totalProcessed;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if worker is running
|
|
166
|
+
*/
|
|
167
|
+
isRunning(): boolean {
|
|
168
|
+
return this.running;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create and start a vector worker
|
|
174
|
+
*/
|
|
175
|
+
export function createVectorWorker(
|
|
176
|
+
eventStore: EventStore,
|
|
177
|
+
vectorStore: VectorStore,
|
|
178
|
+
embedder: Embedder,
|
|
179
|
+
config?: Partial<WorkerConfig>
|
|
180
|
+
): VectorWorker {
|
|
181
|
+
const worker = new VectorWorker(eventStore, vectorStore, embedder, config);
|
|
182
|
+
return worker;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ============================================================
|
|
186
|
+
// Vector Worker V2 - Extended for Task Entity System
|
|
187
|
+
// ============================================================
|
|
188
|
+
|
|
189
|
+
import { Database } from 'duckdb';
|
|
190
|
+
import { VectorOutbox } from './vector-outbox.js';
|
|
191
|
+
import type { OutboxJob, OutboxItemKind } from './types.js';
|
|
192
|
+
|
|
193
|
+
export interface WorkerConfigV2 {
|
|
194
|
+
batchSize: number;
|
|
195
|
+
pollIntervalMs: number;
|
|
196
|
+
maxRetries: number;
|
|
197
|
+
embeddingVersion: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const DEFAULT_CONFIG_V2: WorkerConfigV2 = {
|
|
201
|
+
batchSize: 32,
|
|
202
|
+
pollIntervalMs: 1000,
|
|
203
|
+
maxRetries: 3,
|
|
204
|
+
embeddingVersion: 'v1'
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Content provider interface for different item kinds
|
|
209
|
+
*/
|
|
210
|
+
export interface ContentProvider {
|
|
211
|
+
getContent(itemKind: OutboxItemKind, itemId: string): Promise<{
|
|
212
|
+
content: string;
|
|
213
|
+
metadata: Record<string, unknown>;
|
|
214
|
+
} | null>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Default content provider using database
|
|
219
|
+
*/
|
|
220
|
+
export class DefaultContentProvider implements ContentProvider {
|
|
221
|
+
constructor(private db: Database) {}
|
|
222
|
+
|
|
223
|
+
async getContent(itemKind: OutboxItemKind, itemId: string): Promise<{
|
|
224
|
+
content: string;
|
|
225
|
+
metadata: Record<string, unknown>;
|
|
226
|
+
} | null> {
|
|
227
|
+
switch (itemKind) {
|
|
228
|
+
case 'entry':
|
|
229
|
+
return this.getEntryContent(itemId);
|
|
230
|
+
case 'task_title':
|
|
231
|
+
return this.getTaskTitleContent(itemId);
|
|
232
|
+
case 'event':
|
|
233
|
+
return this.getEventContent(itemId);
|
|
234
|
+
default:
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async getEntryContent(entryId: string): Promise<{
|
|
240
|
+
content: string;
|
|
241
|
+
metadata: Record<string, unknown>;
|
|
242
|
+
} | null> {
|
|
243
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
244
|
+
`SELECT title, content_json, entry_type FROM entries WHERE entry_id = ?`,
|
|
245
|
+
[entryId]
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (rows.length === 0) return null;
|
|
249
|
+
|
|
250
|
+
const row = rows[0];
|
|
251
|
+
const contentJson = typeof row.content_json === 'string'
|
|
252
|
+
? JSON.parse(row.content_json)
|
|
253
|
+
: row.content_json;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
content: `${row.title}\n${JSON.stringify(contentJson)}`,
|
|
257
|
+
metadata: {
|
|
258
|
+
itemKind: 'entry',
|
|
259
|
+
entryType: row.entry_type
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async getTaskTitleContent(taskId: string): Promise<{
|
|
265
|
+
content: string;
|
|
266
|
+
metadata: Record<string, unknown>;
|
|
267
|
+
} | null> {
|
|
268
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
269
|
+
`SELECT title, search_text, current_json FROM entities
|
|
270
|
+
WHERE entity_id = ? AND entity_type = 'task'`,
|
|
271
|
+
[taskId]
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (rows.length === 0) return null;
|
|
275
|
+
|
|
276
|
+
const row = rows[0];
|
|
277
|
+
return {
|
|
278
|
+
content: row.search_text as string || row.title as string,
|
|
279
|
+
metadata: {
|
|
280
|
+
itemKind: 'task_title',
|
|
281
|
+
entityType: 'task'
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private async getEventContent(eventId: string): Promise<{
|
|
287
|
+
content: string;
|
|
288
|
+
metadata: Record<string, unknown>;
|
|
289
|
+
} | null> {
|
|
290
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
291
|
+
`SELECT content, event_type, session_id FROM events WHERE id = ?`,
|
|
292
|
+
[eventId]
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (rows.length === 0) return null;
|
|
296
|
+
|
|
297
|
+
const row = rows[0];
|
|
298
|
+
return {
|
|
299
|
+
content: row.content as string,
|
|
300
|
+
metadata: {
|
|
301
|
+
itemKind: 'event',
|
|
302
|
+
eventType: row.event_type,
|
|
303
|
+
sessionId: row.session_id
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Vector Worker V2 - Supports multiple item kinds
|
|
311
|
+
*/
|
|
312
|
+
export class VectorWorkerV2 {
|
|
313
|
+
private readonly outbox: VectorOutbox;
|
|
314
|
+
private readonly vectorStore: VectorStore;
|
|
315
|
+
private readonly embedder: Embedder;
|
|
316
|
+
private readonly contentProvider: ContentProvider;
|
|
317
|
+
private readonly config: WorkerConfigV2;
|
|
318
|
+
private running = false;
|
|
319
|
+
private pollTimeout: NodeJS.Timeout | null = null;
|
|
320
|
+
|
|
321
|
+
constructor(
|
|
322
|
+
db: Database,
|
|
323
|
+
vectorStore: VectorStore,
|
|
324
|
+
embedder: Embedder,
|
|
325
|
+
config: Partial<WorkerConfigV2> = {},
|
|
326
|
+
contentProvider?: ContentProvider
|
|
327
|
+
) {
|
|
328
|
+
this.outbox = new VectorOutbox(db, {
|
|
329
|
+
embeddingVersion: config.embeddingVersion ?? DEFAULT_CONFIG_V2.embeddingVersion,
|
|
330
|
+
maxRetries: config.maxRetries ?? DEFAULT_CONFIG_V2.maxRetries
|
|
331
|
+
});
|
|
332
|
+
this.vectorStore = vectorStore;
|
|
333
|
+
this.embedder = embedder;
|
|
334
|
+
this.config = { ...DEFAULT_CONFIG_V2, ...config };
|
|
335
|
+
this.contentProvider = contentProvider ?? new DefaultContentProvider(db);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Start the worker polling loop
|
|
340
|
+
*/
|
|
341
|
+
start(): void {
|
|
342
|
+
if (this.running) return;
|
|
343
|
+
this.running = true;
|
|
344
|
+
this.poll();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Stop the worker
|
|
349
|
+
*/
|
|
350
|
+
stop(): void {
|
|
351
|
+
this.running = false;
|
|
352
|
+
if (this.pollTimeout) {
|
|
353
|
+
clearTimeout(this.pollTimeout);
|
|
354
|
+
this.pollTimeout = null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Process a single batch of outbox jobs
|
|
360
|
+
*/
|
|
361
|
+
async processBatch(): Promise<number> {
|
|
362
|
+
const jobs = await this.outbox.claimJobs(this.config.batchSize);
|
|
363
|
+
|
|
364
|
+
if (jobs.length === 0) {
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let successCount = 0;
|
|
369
|
+
|
|
370
|
+
for (const job of jobs) {
|
|
371
|
+
try {
|
|
372
|
+
await this.processJob(job);
|
|
373
|
+
await this.outbox.markDone(job.jobId);
|
|
374
|
+
successCount++;
|
|
375
|
+
} catch (error) {
|
|
376
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
377
|
+
await this.outbox.markFailed(job.jobId, errorMessage);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return successCount;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Process a single job
|
|
386
|
+
*/
|
|
387
|
+
private async processJob(job: OutboxJob): Promise<void> {
|
|
388
|
+
// Get content
|
|
389
|
+
const contentData = await this.contentProvider.getContent(job.itemKind, job.itemId);
|
|
390
|
+
|
|
391
|
+
if (!contentData) {
|
|
392
|
+
// Item not found, mark as done (skip)
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Generate embedding
|
|
397
|
+
const embedding = await this.embedder.embed(contentData.content);
|
|
398
|
+
|
|
399
|
+
// Upsert to vector store
|
|
400
|
+
const record: VectorRecord = {
|
|
401
|
+
id: `${job.itemKind}_${job.itemId}_${job.embeddingVersion}`,
|
|
402
|
+
eventId: job.itemKind === 'event' ? job.itemId : '',
|
|
403
|
+
sessionId: (contentData.metadata.sessionId as string) ?? '',
|
|
404
|
+
eventType: (contentData.metadata.eventType as string) ?? job.itemKind,
|
|
405
|
+
content: contentData.content,
|
|
406
|
+
vector: embedding.vector,
|
|
407
|
+
timestamp: new Date().toISOString(),
|
|
408
|
+
metadata: {
|
|
409
|
+
...contentData.metadata,
|
|
410
|
+
embeddingVersion: job.embeddingVersion
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Use idempotent upsert (delete + add)
|
|
415
|
+
await this.vectorStore.upsertBatch([record]);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Poll for new jobs
|
|
420
|
+
*/
|
|
421
|
+
private async poll(): Promise<void> {
|
|
422
|
+
if (!this.running) return;
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
await this.processBatch();
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.error('Vector worker V2 error:', error);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Schedule next poll
|
|
431
|
+
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Process all pending jobs (blocking)
|
|
436
|
+
*/
|
|
437
|
+
async processAll(): Promise<number> {
|
|
438
|
+
let totalProcessed = 0;
|
|
439
|
+
let processed: number;
|
|
440
|
+
|
|
441
|
+
do {
|
|
442
|
+
processed = await this.processBatch();
|
|
443
|
+
totalProcessed += processed;
|
|
444
|
+
} while (processed > 0);
|
|
445
|
+
|
|
446
|
+
return totalProcessed;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Run reconciliation
|
|
451
|
+
*/
|
|
452
|
+
async reconcile(): Promise<{ recovered: number; retried: number }> {
|
|
453
|
+
return this.outbox.reconcile();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Get metrics
|
|
458
|
+
*/
|
|
459
|
+
async getMetrics() {
|
|
460
|
+
return this.outbox.getMetrics();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Check if worker is running
|
|
465
|
+
*/
|
|
466
|
+
isRunning(): boolean {
|
|
467
|
+
return this.running;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Get the outbox instance for direct access
|
|
472
|
+
*/
|
|
473
|
+
getOutbox(): VectorOutbox {
|
|
474
|
+
return this.outbox;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Create a Vector Worker V2 instance
|
|
480
|
+
*/
|
|
481
|
+
export function createVectorWorkerV2(
|
|
482
|
+
db: Database,
|
|
483
|
+
vectorStore: VectorStore,
|
|
484
|
+
embedder: Embedder,
|
|
485
|
+
config?: Partial<WorkerConfigV2>
|
|
486
|
+
): VectorWorkerV2 {
|
|
487
|
+
return new VectorWorkerV2(db, vectorStore, embedder, config);
|
|
488
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working Set Store
|
|
3
|
+
* Manages the active memory window for Endless Mode
|
|
4
|
+
* Biomimetic: Simulates human working memory (7±2 items, 15-30s duration)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { Database } from 'duckdb';
|
|
9
|
+
import type {
|
|
10
|
+
MemoryEvent,
|
|
11
|
+
EndlessModeConfig,
|
|
12
|
+
WorkingSet,
|
|
13
|
+
WorkingSetItem
|
|
14
|
+
} from './types.js';
|
|
15
|
+
import { EventStore } from './event-store.js';
|
|
16
|
+
|
|
17
|
+
export class WorkingSetStore {
|
|
18
|
+
constructor(
|
|
19
|
+
private eventStore: EventStore,
|
|
20
|
+
private config: EndlessModeConfig
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
private get db(): Database {
|
|
24
|
+
return this.eventStore.getDatabase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Add an event to the working set
|
|
29
|
+
*/
|
|
30
|
+
async add(eventId: string, relevanceScore: number = 1.0, topics?: string[]): Promise<void> {
|
|
31
|
+
const expiresAt = new Date(
|
|
32
|
+
Date.now() + this.config.workingSet.timeWindowHours * 60 * 60 * 1000
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
await this.db.run(
|
|
36
|
+
`INSERT OR REPLACE INTO working_set (id, event_id, added_at, relevance_score, topics, expires_at)
|
|
37
|
+
VALUES (?, ?, CURRENT_TIMESTAMP, ?, ?, ?)`,
|
|
38
|
+
[
|
|
39
|
+
randomUUID(),
|
|
40
|
+
eventId,
|
|
41
|
+
relevanceScore,
|
|
42
|
+
JSON.stringify(topics || []),
|
|
43
|
+
expiresAt.toISOString()
|
|
44
|
+
]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Enforce size limit
|
|
48
|
+
await this.enforceLimit();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the current working set
|
|
53
|
+
*/
|
|
54
|
+
async get(): Promise<WorkingSet> {
|
|
55
|
+
// Clean up expired items first
|
|
56
|
+
await this.cleanup();
|
|
57
|
+
|
|
58
|
+
// Get working set items with their events
|
|
59
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
60
|
+
`SELECT ws.*, e.*
|
|
61
|
+
FROM working_set ws
|
|
62
|
+
JOIN events e ON ws.event_id = e.id
|
|
63
|
+
ORDER BY ws.relevance_score DESC, ws.added_at DESC
|
|
64
|
+
LIMIT ?`,
|
|
65
|
+
[this.config.workingSet.maxEvents]
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const events: MemoryEvent[] = rows.map(row => ({
|
|
69
|
+
id: row.id as string,
|
|
70
|
+
eventType: row.event_type as 'user_prompt' | 'agent_response' | 'session_summary' | 'tool_observation',
|
|
71
|
+
sessionId: row.session_id as string,
|
|
72
|
+
timestamp: new Date(row.timestamp as string),
|
|
73
|
+
content: row.content as string,
|
|
74
|
+
canonicalKey: row.canonical_key as string,
|
|
75
|
+
dedupeKey: row.dedupe_key as string,
|
|
76
|
+
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
recentEvents: events,
|
|
81
|
+
lastActivity: events.length > 0 ? events[0].timestamp : new Date(),
|
|
82
|
+
continuityScore: await this.calculateContinuityScore()
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get working set items (metadata only)
|
|
88
|
+
*/
|
|
89
|
+
async getItems(): Promise<WorkingSetItem[]> {
|
|
90
|
+
const rows = await this.db.all<Array<Record<string, unknown>>>(
|
|
91
|
+
`SELECT * FROM working_set ORDER BY relevance_score DESC, added_at DESC`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return rows.map(row => ({
|
|
95
|
+
id: row.id as string,
|
|
96
|
+
eventId: row.event_id as string,
|
|
97
|
+
addedAt: new Date(row.added_at as string),
|
|
98
|
+
relevanceScore: row.relevance_score as number,
|
|
99
|
+
topics: row.topics ? JSON.parse(row.topics as string) : undefined,
|
|
100
|
+
expiresAt: new Date(row.expires_at as string)
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Update relevance score for an event
|
|
106
|
+
*/
|
|
107
|
+
async updateRelevance(eventId: string, score: number): Promise<void> {
|
|
108
|
+
await this.db.run(
|
|
109
|
+
`UPDATE working_set SET relevance_score = ? WHERE event_id = ?`,
|
|
110
|
+
[score, eventId]
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Prune specific events from working set (after consolidation)
|
|
116
|
+
*/
|
|
117
|
+
async prune(eventIds: string[]): Promise<void> {
|
|
118
|
+
if (eventIds.length === 0) return;
|
|
119
|
+
|
|
120
|
+
const placeholders = eventIds.map(() => '?').join(',');
|
|
121
|
+
await this.db.run(
|
|
122
|
+
`DELETE FROM working_set WHERE event_id IN (${placeholders})`,
|
|
123
|
+
eventIds
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get the count of items in working set
|
|
129
|
+
*/
|
|
130
|
+
async count(): Promise<number> {
|
|
131
|
+
const result = await this.db.all<Array<{ count: number }>>(
|
|
132
|
+
`SELECT COUNT(*) as count FROM working_set`
|
|
133
|
+
);
|
|
134
|
+
return result[0]?.count || 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Clear the entire working set
|
|
139
|
+
*/
|
|
140
|
+
async clear(): Promise<void> {
|
|
141
|
+
await this.db.run(`DELETE FROM working_set`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if an event is in the working set
|
|
146
|
+
*/
|
|
147
|
+
async contains(eventId: string): Promise<boolean> {
|
|
148
|
+
const result = await this.db.all<Array<{ count: number }>>(
|
|
149
|
+
`SELECT COUNT(*) as count FROM working_set WHERE event_id = ?`,
|
|
150
|
+
[eventId]
|
|
151
|
+
);
|
|
152
|
+
return (result[0]?.count || 0) > 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Refresh expiration for an event (rehears al - keep relevant items longer)
|
|
157
|
+
*/
|
|
158
|
+
async refresh(eventId: string): Promise<void> {
|
|
159
|
+
const newExpiresAt = new Date(
|
|
160
|
+
Date.now() + this.config.workingSet.timeWindowHours * 60 * 60 * 1000
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
await this.db.run(
|
|
164
|
+
`UPDATE working_set SET expires_at = ? WHERE event_id = ?`,
|
|
165
|
+
[newExpiresAt.toISOString(), eventId]
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Clean up expired items
|
|
171
|
+
*/
|
|
172
|
+
private async cleanup(): Promise<void> {
|
|
173
|
+
await this.db.run(
|
|
174
|
+
`DELETE FROM working_set WHERE expires_at < datetime('now')`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Enforce the maximum size limit
|
|
180
|
+
* Removes lowest relevance items when over limit
|
|
181
|
+
*/
|
|
182
|
+
private async enforceLimit(): Promise<void> {
|
|
183
|
+
const maxEvents = this.config.workingSet.maxEvents;
|
|
184
|
+
|
|
185
|
+
// Get IDs to keep (highest relevance, most recent)
|
|
186
|
+
const keepIds = await this.db.all<Array<{ id: string }>>(
|
|
187
|
+
`SELECT id FROM working_set
|
|
188
|
+
ORDER BY relevance_score DESC, added_at DESC
|
|
189
|
+
LIMIT ?`,
|
|
190
|
+
[maxEvents]
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (keepIds.length === 0) return;
|
|
194
|
+
|
|
195
|
+
const keepIdList = keepIds.map(r => r.id);
|
|
196
|
+
const placeholders = keepIdList.map(() => '?').join(',');
|
|
197
|
+
|
|
198
|
+
// Delete everything not in the keep list
|
|
199
|
+
await this.db.run(
|
|
200
|
+
`DELETE FROM working_set WHERE id NOT IN (${placeholders})`,
|
|
201
|
+
keepIdList
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Calculate continuity score based on recent context transitions
|
|
207
|
+
*/
|
|
208
|
+
private async calculateContinuityScore(): Promise<number> {
|
|
209
|
+
const result = await this.db.all<Array<{ avg_score: number | null }>>(
|
|
210
|
+
`SELECT AVG(continuity_score) as avg_score
|
|
211
|
+
FROM continuity_log
|
|
212
|
+
WHERE created_at > datetime('now', '-1 hour')`
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return result[0]?.avg_score ?? 0.5;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get topics from current working set for context matching
|
|
220
|
+
*/
|
|
221
|
+
async getActiveTopics(): Promise<string[]> {
|
|
222
|
+
const rows = await this.db.all<Array<{ topics: string }>>(
|
|
223
|
+
`SELECT topics FROM working_set WHERE topics IS NOT NULL`
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const allTopics = new Set<string>();
|
|
227
|
+
for (const row of rows) {
|
|
228
|
+
const topics = JSON.parse(row.topics) as string[];
|
|
229
|
+
topics.forEach(t => allTopics.add(t));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return Array.from(allTopics);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create a Working Set Store instance
|
|
238
|
+
*/
|
|
239
|
+
export function createWorkingSetStore(
|
|
240
|
+
eventStore: EventStore,
|
|
241
|
+
config: EndlessModeConfig
|
|
242
|
+
): WorkingSetStore {
|
|
243
|
+
return new WorkingSetStore(eventStore, config);
|
|
244
|
+
}
|