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.
Files changed (127) hide show
  1. package/.claude-plugin/commands/memory-forget.md +42 -0
  2. package/.claude-plugin/commands/memory-history.md +34 -0
  3. package/.claude-plugin/commands/memory-import.md +56 -0
  4. package/.claude-plugin/commands/memory-list.md +37 -0
  5. package/.claude-plugin/commands/memory-search.md +36 -0
  6. package/.claude-plugin/commands/memory-stats.md +34 -0
  7. package/.claude-plugin/hooks.json +59 -0
  8. package/.claude-plugin/plugin.json +24 -0
  9. package/.history/package_20260201112328.json +45 -0
  10. package/.history/package_20260201113602.json +45 -0
  11. package/.history/package_20260201113713.json +45 -0
  12. package/.history/package_20260201114110.json +45 -0
  13. package/Memo.txt +558 -0
  14. package/README.md +520 -0
  15. package/context.md +636 -0
  16. package/dist/.claude-plugin/commands/memory-forget.md +42 -0
  17. package/dist/.claude-plugin/commands/memory-history.md +34 -0
  18. package/dist/.claude-plugin/commands/memory-import.md +56 -0
  19. package/dist/.claude-plugin/commands/memory-list.md +37 -0
  20. package/dist/.claude-plugin/commands/memory-search.md +36 -0
  21. package/dist/.claude-plugin/commands/memory-stats.md +34 -0
  22. package/dist/.claude-plugin/hooks.json +59 -0
  23. package/dist/.claude-plugin/plugin.json +24 -0
  24. package/dist/cli/index.js +3539 -0
  25. package/dist/cli/index.js.map +7 -0
  26. package/dist/core/index.js +4408 -0
  27. package/dist/core/index.js.map +7 -0
  28. package/dist/hooks/session-end.js +2971 -0
  29. package/dist/hooks/session-end.js.map +7 -0
  30. package/dist/hooks/session-start.js +2969 -0
  31. package/dist/hooks/session-start.js.map +7 -0
  32. package/dist/hooks/stop.js +3123 -0
  33. package/dist/hooks/stop.js.map +7 -0
  34. package/dist/hooks/user-prompt-submit.js +2960 -0
  35. package/dist/hooks/user-prompt-submit.js.map +7 -0
  36. package/dist/services/memory-service.js +2931 -0
  37. package/dist/services/memory-service.js.map +7 -0
  38. package/package.json +45 -0
  39. package/plan.md +1642 -0
  40. package/scripts/build.ts +102 -0
  41. package/spec.md +624 -0
  42. package/specs/citations-system/context.md +243 -0
  43. package/specs/citations-system/plan.md +495 -0
  44. package/specs/citations-system/spec.md +371 -0
  45. package/specs/endless-mode/context.md +305 -0
  46. package/specs/endless-mode/plan.md +620 -0
  47. package/specs/endless-mode/spec.md +455 -0
  48. package/specs/entity-edge-model/context.md +401 -0
  49. package/specs/entity-edge-model/plan.md +459 -0
  50. package/specs/entity-edge-model/spec.md +391 -0
  51. package/specs/evidence-aligner-v2/context.md +401 -0
  52. package/specs/evidence-aligner-v2/plan.md +303 -0
  53. package/specs/evidence-aligner-v2/spec.md +312 -0
  54. package/specs/mcp-desktop-integration/context.md +278 -0
  55. package/specs/mcp-desktop-integration/plan.md +550 -0
  56. package/specs/mcp-desktop-integration/spec.md +494 -0
  57. package/specs/post-tool-use-hook/context.md +319 -0
  58. package/specs/post-tool-use-hook/plan.md +469 -0
  59. package/specs/post-tool-use-hook/spec.md +364 -0
  60. package/specs/private-tags/context.md +288 -0
  61. package/specs/private-tags/plan.md +412 -0
  62. package/specs/private-tags/spec.md +345 -0
  63. package/specs/progressive-disclosure/context.md +346 -0
  64. package/specs/progressive-disclosure/plan.md +663 -0
  65. package/specs/progressive-disclosure/spec.md +415 -0
  66. package/specs/task-entity-system/context.md +297 -0
  67. package/specs/task-entity-system/plan.md +301 -0
  68. package/specs/task-entity-system/spec.md +314 -0
  69. package/specs/vector-outbox-v2/context.md +470 -0
  70. package/specs/vector-outbox-v2/plan.md +562 -0
  71. package/specs/vector-outbox-v2/spec.md +466 -0
  72. package/specs/web-viewer-ui/context.md +384 -0
  73. package/specs/web-viewer-ui/plan.md +797 -0
  74. package/specs/web-viewer-ui/spec.md +516 -0
  75. package/src/cli/index.ts +570 -0
  76. package/src/core/canonical-key.ts +186 -0
  77. package/src/core/citation-generator.ts +63 -0
  78. package/src/core/consolidated-store.ts +279 -0
  79. package/src/core/consolidation-worker.ts +384 -0
  80. package/src/core/context-formatter.ts +276 -0
  81. package/src/core/continuity-manager.ts +336 -0
  82. package/src/core/edge-repo.ts +324 -0
  83. package/src/core/embedder.ts +124 -0
  84. package/src/core/entity-repo.ts +342 -0
  85. package/src/core/event-store.ts +672 -0
  86. package/src/core/evidence-aligner.ts +635 -0
  87. package/src/core/graduation.ts +365 -0
  88. package/src/core/index.ts +32 -0
  89. package/src/core/matcher.ts +210 -0
  90. package/src/core/metadata-extractor.ts +203 -0
  91. package/src/core/privacy/filter.ts +179 -0
  92. package/src/core/privacy/index.ts +20 -0
  93. package/src/core/privacy/tag-parser.ts +145 -0
  94. package/src/core/progressive-retriever.ts +415 -0
  95. package/src/core/retriever.ts +235 -0
  96. package/src/core/task/blocker-resolver.ts +325 -0
  97. package/src/core/task/index.ts +9 -0
  98. package/src/core/task/task-matcher.ts +238 -0
  99. package/src/core/task/task-projector.ts +345 -0
  100. package/src/core/task/task-resolver.ts +414 -0
  101. package/src/core/types.ts +841 -0
  102. package/src/core/vector-outbox.ts +295 -0
  103. package/src/core/vector-store.ts +182 -0
  104. package/src/core/vector-worker.ts +488 -0
  105. package/src/core/working-set-store.ts +244 -0
  106. package/src/hooks/post-tool-use.ts +127 -0
  107. package/src/hooks/session-end.ts +78 -0
  108. package/src/hooks/session-start.ts +57 -0
  109. package/src/hooks/stop.ts +78 -0
  110. package/src/hooks/user-prompt-submit.ts +54 -0
  111. package/src/mcp/handlers.ts +212 -0
  112. package/src/mcp/index.ts +47 -0
  113. package/src/mcp/tools.ts +78 -0
  114. package/src/server/api/citations.ts +101 -0
  115. package/src/server/api/events.ts +101 -0
  116. package/src/server/api/index.ts +18 -0
  117. package/src/server/api/search.ts +98 -0
  118. package/src/server/api/sessions.ts +111 -0
  119. package/src/server/api/stats.ts +97 -0
  120. package/src/server/index.ts +91 -0
  121. package/src/services/memory-service.ts +626 -0
  122. package/src/services/session-history-importer.ts +367 -0
  123. package/tests/canonical-key.test.ts +101 -0
  124. package/tests/evidence-aligner.test.ts +152 -0
  125. package/tests/matcher.test.ts +112 -0
  126. package/tsconfig.json +24 -0
  127. 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
+ }