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,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
+ }