@psiclawops/hypermem 0.1.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 (94) hide show
  1. package/ARCHITECTURE.md +296 -0
  2. package/LICENSE +190 -0
  3. package/README.md +243 -0
  4. package/dist/background-indexer.d.ts +117 -0
  5. package/dist/background-indexer.d.ts.map +1 -0
  6. package/dist/background-indexer.js +732 -0
  7. package/dist/compaction-fence.d.ts +89 -0
  8. package/dist/compaction-fence.d.ts.map +1 -0
  9. package/dist/compaction-fence.js +153 -0
  10. package/dist/compositor.d.ts +139 -0
  11. package/dist/compositor.d.ts.map +1 -0
  12. package/dist/compositor.js +1109 -0
  13. package/dist/cross-agent.d.ts +57 -0
  14. package/dist/cross-agent.d.ts.map +1 -0
  15. package/dist/cross-agent.js +254 -0
  16. package/dist/db.d.ts +131 -0
  17. package/dist/db.d.ts.map +1 -0
  18. package/dist/db.js +398 -0
  19. package/dist/desired-state-store.d.ts +100 -0
  20. package/dist/desired-state-store.d.ts.map +1 -0
  21. package/dist/desired-state-store.js +212 -0
  22. package/dist/doc-chunk-store.d.ts +115 -0
  23. package/dist/doc-chunk-store.d.ts.map +1 -0
  24. package/dist/doc-chunk-store.js +278 -0
  25. package/dist/doc-chunker.d.ts +99 -0
  26. package/dist/doc-chunker.d.ts.map +1 -0
  27. package/dist/doc-chunker.js +324 -0
  28. package/dist/episode-store.d.ts +48 -0
  29. package/dist/episode-store.d.ts.map +1 -0
  30. package/dist/episode-store.js +135 -0
  31. package/dist/fact-store.d.ts +57 -0
  32. package/dist/fact-store.d.ts.map +1 -0
  33. package/dist/fact-store.js +175 -0
  34. package/dist/fleet-store.d.ts +144 -0
  35. package/dist/fleet-store.d.ts.map +1 -0
  36. package/dist/fleet-store.js +276 -0
  37. package/dist/hybrid-retrieval.d.ts +60 -0
  38. package/dist/hybrid-retrieval.d.ts.map +1 -0
  39. package/dist/hybrid-retrieval.js +340 -0
  40. package/dist/index.d.ts +611 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +1042 -0
  43. package/dist/knowledge-graph.d.ts +110 -0
  44. package/dist/knowledge-graph.d.ts.map +1 -0
  45. package/dist/knowledge-graph.js +305 -0
  46. package/dist/knowledge-store.d.ts +72 -0
  47. package/dist/knowledge-store.d.ts.map +1 -0
  48. package/dist/knowledge-store.js +241 -0
  49. package/dist/library-schema.d.ts +22 -0
  50. package/dist/library-schema.d.ts.map +1 -0
  51. package/dist/library-schema.js +717 -0
  52. package/dist/message-store.d.ts +76 -0
  53. package/dist/message-store.d.ts.map +1 -0
  54. package/dist/message-store.js +273 -0
  55. package/dist/preference-store.d.ts +54 -0
  56. package/dist/preference-store.d.ts.map +1 -0
  57. package/dist/preference-store.js +109 -0
  58. package/dist/preservation-gate.d.ts +82 -0
  59. package/dist/preservation-gate.d.ts.map +1 -0
  60. package/dist/preservation-gate.js +150 -0
  61. package/dist/provider-translator.d.ts +40 -0
  62. package/dist/provider-translator.d.ts.map +1 -0
  63. package/dist/provider-translator.js +349 -0
  64. package/dist/rate-limiter.d.ts +76 -0
  65. package/dist/rate-limiter.d.ts.map +1 -0
  66. package/dist/rate-limiter.js +179 -0
  67. package/dist/redis.d.ts +188 -0
  68. package/dist/redis.d.ts.map +1 -0
  69. package/dist/redis.js +534 -0
  70. package/dist/schema.d.ts +15 -0
  71. package/dist/schema.d.ts.map +1 -0
  72. package/dist/schema.js +203 -0
  73. package/dist/secret-scanner.d.ts +51 -0
  74. package/dist/secret-scanner.d.ts.map +1 -0
  75. package/dist/secret-scanner.js +248 -0
  76. package/dist/seed.d.ts +108 -0
  77. package/dist/seed.d.ts.map +1 -0
  78. package/dist/seed.js +177 -0
  79. package/dist/system-store.d.ts +73 -0
  80. package/dist/system-store.d.ts.map +1 -0
  81. package/dist/system-store.js +182 -0
  82. package/dist/topic-store.d.ts +45 -0
  83. package/dist/topic-store.d.ts.map +1 -0
  84. package/dist/topic-store.js +136 -0
  85. package/dist/types.d.ts +329 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +9 -0
  88. package/dist/vector-store.d.ts +132 -0
  89. package/dist/vector-store.d.ts.map +1 -0
  90. package/dist/vector-store.js +498 -0
  91. package/dist/work-store.d.ts +112 -0
  92. package/dist/work-store.d.ts.map +1 -0
  93. package/dist/work-store.js +273 -0
  94. package/package.json +57 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * HyperMem Message Store
3
+ *
4
+ * CRUD operations for conversations and messages in SQLite.
5
+ * All messages are stored in provider-neutral format.
6
+ * This is the write-through layer: Redis → here.
7
+ */
8
+ import type { DatabaseSync } from 'node:sqlite';
9
+ import type { NeutralMessage, StoredMessage, Conversation, ChannelType, ConversationStatus } from './types.js';
10
+ export declare class MessageStore {
11
+ private readonly db;
12
+ constructor(db: DatabaseSync);
13
+ /**
14
+ * Get or create a conversation for a session.
15
+ */
16
+ getOrCreateConversation(agentId: string, sessionKey: string, opts?: {
17
+ channelType?: ChannelType;
18
+ channelId?: string;
19
+ provider?: string;
20
+ model?: string;
21
+ }): Conversation;
22
+ /**
23
+ * Get a conversation by session key.
24
+ */
25
+ getConversation(sessionKey: string): Conversation | null;
26
+ /**
27
+ * Get all conversations for an agent, optionally filtered.
28
+ */
29
+ getConversations(agentId: string, opts?: {
30
+ status?: ConversationStatus;
31
+ channelType?: ChannelType;
32
+ limit?: number;
33
+ }): Conversation[];
34
+ /**
35
+ * Update conversation metadata.
36
+ */
37
+ updateConversation(conversationId: number, updates: {
38
+ provider?: string;
39
+ model?: string;
40
+ status?: ConversationStatus;
41
+ endedAt?: string;
42
+ }): void;
43
+ /**
44
+ * Record a message to the database.
45
+ * Returns the stored message with its assigned ID.
46
+ */
47
+ recordMessage(conversationId: number, agentId: string, message: NeutralMessage, opts?: {
48
+ tokenCount?: number;
49
+ isHeartbeat?: boolean;
50
+ }): StoredMessage;
51
+ /**
52
+ * Get recent messages for a conversation.
53
+ */
54
+ getRecentMessages(conversationId: number, limit?: number): StoredMessage[];
55
+ /**
56
+ * Get messages across all conversations for an agent (cross-session query).
57
+ */
58
+ getAgentMessages(agentId: string, opts?: {
59
+ since?: string;
60
+ limit?: number;
61
+ excludeHeartbeats?: boolean;
62
+ }): StoredMessage[];
63
+ /**
64
+ * Full-text search across all messages for an agent.
65
+ */
66
+ searchMessages(agentId: string, query: string, limit?: number): StoredMessage[];
67
+ /**
68
+ * Get message count for a conversation.
69
+ */
70
+ getMessageCount(conversationId: number): number;
71
+ /**
72
+ * Infer channel type from session key format.
73
+ */
74
+ private inferChannelType;
75
+ }
76
+ //# sourceMappingURL=message-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-store.d.ts","sourceRoot":"","sources":["../src/message-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,YAAY,EACZ,WAAW,EACX,kBAAkB,EACnB,MAAM,YAAY,CAAC;AA8CpB,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAI7C;;OAEG;IACH,uBAAuB,CACrB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE;QACL,WAAW,CAAC,EAAE,WAAW,CAAC;QAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GACA,YAAY;IAgDf;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAQxD;;OAEG;IACH,gBAAgB,CACd,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,MAAM,CAAC,EAAE,kBAAkB,CAAC;QAC5B,WAAW,CAAC,EAAE,WAAW,CAAC;QAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GACA,YAAY,EAAE;IAwBjB;;OAEG;IACH,kBAAkB,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE;QAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,kBAAkB,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,GAAG,IAAI;IA2BR;;;OAGG;IACH,aAAa,CACX,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,cAAc,EACvB,IAAI,CAAC,EAAE;QACL,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB,GACA,aAAa;IA+DhB;;OAEG;IACH,iBAAiB,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,aAAa,EAAE;IAY9E;;OAEG;IACH,gBAAgB,CACd,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,GACA,aAAa,EAAE;IAuBlB;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,aAAa,EAAE;IAmBnF;;OAEG;IACH,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM;IAS/C;;OAEG;IACH,OAAO,CAAC,gBAAgB;CASzB"}
@@ -0,0 +1,273 @@
1
+ /**
2
+ * HyperMem Message Store
3
+ *
4
+ * CRUD operations for conversations and messages in SQLite.
5
+ * All messages are stored in provider-neutral format.
6
+ * This is the write-through layer: Redis → here.
7
+ */
8
+ function nowIso() {
9
+ return new Date().toISOString();
10
+ }
11
+ /**
12
+ * Parse a stored message row from SQLite into a StoredMessage object.
13
+ */
14
+ function parseMessageRow(row) {
15
+ return {
16
+ id: row.id,
17
+ conversationId: row.conversation_id,
18
+ agentId: row.agent_id,
19
+ role: row.role,
20
+ textContent: row.text_content || null,
21
+ toolCalls: row.tool_calls ? JSON.parse(row.tool_calls) : null,
22
+ toolResults: row.tool_results ? JSON.parse(row.tool_results) : null,
23
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
24
+ messageIndex: row.message_index,
25
+ tokenCount: row.token_count || null,
26
+ isHeartbeat: row.is_heartbeat === 1,
27
+ createdAt: row.created_at,
28
+ };
29
+ }
30
+ function parseConversationRow(row) {
31
+ return {
32
+ id: row.id,
33
+ sessionKey: row.session_key,
34
+ sessionId: row.session_id || null,
35
+ agentId: row.agent_id,
36
+ channelType: row.channel_type,
37
+ channelId: row.channel_id || null,
38
+ provider: row.provider || null,
39
+ model: row.model || null,
40
+ status: row.status,
41
+ messageCount: row.message_count,
42
+ tokenCountIn: row.token_count_in,
43
+ tokenCountOut: row.token_count_out,
44
+ createdAt: row.created_at,
45
+ updatedAt: row.updated_at,
46
+ endedAt: row.ended_at || null,
47
+ };
48
+ }
49
+ export class MessageStore {
50
+ db;
51
+ constructor(db) {
52
+ this.db = db;
53
+ }
54
+ // ─── Conversation Operations ─────────────────────────────────
55
+ /**
56
+ * Get or create a conversation for a session.
57
+ */
58
+ getOrCreateConversation(agentId, sessionKey, opts) {
59
+ const existing = this.db
60
+ .prepare('SELECT * FROM conversations WHERE session_key = ?')
61
+ .get(sessionKey);
62
+ if (existing) {
63
+ return parseConversationRow(existing);
64
+ }
65
+ const now = nowIso();
66
+ const channelType = opts?.channelType || this.inferChannelType(sessionKey);
67
+ const result = this.db.prepare(`
68
+ INSERT INTO conversations (session_key, agent_id, channel_type, channel_id, provider, model, created_at, updated_at)
69
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
70
+ `).run(sessionKey, agentId, channelType, opts?.channelId || null, opts?.provider || null, opts?.model || null, now, now);
71
+ // node:sqlite returns { changes, lastInsertRowid }
72
+ const id = result.lastInsertRowid;
73
+ return {
74
+ id,
75
+ sessionKey,
76
+ sessionId: null,
77
+ agentId,
78
+ channelType,
79
+ channelId: opts?.channelId || null,
80
+ provider: opts?.provider || null,
81
+ model: opts?.model || null,
82
+ status: 'active',
83
+ messageCount: 0,
84
+ tokenCountIn: 0,
85
+ tokenCountOut: 0,
86
+ createdAt: now,
87
+ updatedAt: now,
88
+ endedAt: null,
89
+ };
90
+ }
91
+ /**
92
+ * Get a conversation by session key.
93
+ */
94
+ getConversation(sessionKey) {
95
+ const row = this.db
96
+ .prepare('SELECT * FROM conversations WHERE session_key = ?')
97
+ .get(sessionKey);
98
+ return row ? parseConversationRow(row) : null;
99
+ }
100
+ /**
101
+ * Get all conversations for an agent, optionally filtered.
102
+ */
103
+ getConversations(agentId, opts) {
104
+ let sql = 'SELECT * FROM conversations WHERE agent_id = ?';
105
+ const params = [agentId];
106
+ if (opts?.status) {
107
+ sql += ' AND status = ?';
108
+ params.push(opts.status);
109
+ }
110
+ if (opts?.channelType) {
111
+ sql += ' AND channel_type = ?';
112
+ params.push(opts.channelType);
113
+ }
114
+ sql += ' ORDER BY updated_at DESC';
115
+ if (opts?.limit) {
116
+ sql += ' LIMIT ?';
117
+ params.push(opts.limit);
118
+ }
119
+ const rows = this.db.prepare(sql).all(...params);
120
+ return rows.map(parseConversationRow);
121
+ }
122
+ /**
123
+ * Update conversation metadata.
124
+ */
125
+ updateConversation(conversationId, updates) {
126
+ const sets = ['updated_at = ?'];
127
+ const params = [nowIso()];
128
+ if (updates.provider !== undefined) {
129
+ sets.push('provider = ?');
130
+ params.push(updates.provider);
131
+ }
132
+ if (updates.model !== undefined) {
133
+ sets.push('model = ?');
134
+ params.push(updates.model);
135
+ }
136
+ if (updates.status !== undefined) {
137
+ sets.push('status = ?');
138
+ params.push(updates.status);
139
+ }
140
+ if (updates.endedAt !== undefined) {
141
+ sets.push('ended_at = ?');
142
+ params.push(updates.endedAt);
143
+ }
144
+ params.push(conversationId);
145
+ this.db.prepare(`UPDATE conversations SET ${sets.join(', ')} WHERE id = ?`).run(...params);
146
+ }
147
+ // ─── Message Operations ──────────────────────────────────────
148
+ /**
149
+ * Record a message to the database.
150
+ * Returns the stored message with its assigned ID.
151
+ */
152
+ recordMessage(conversationId, agentId, message, opts) {
153
+ const now = nowIso();
154
+ // Get next message index
155
+ const lastRow = this.db
156
+ .prepare('SELECT MAX(message_index) AS max_idx FROM messages WHERE conversation_id = ?')
157
+ .get(conversationId);
158
+ const messageIndex = (lastRow?.max_idx ?? -1) + 1;
159
+ const result = this.db.prepare(`
160
+ INSERT INTO messages (conversation_id, agent_id, role, text_content, tool_calls, tool_results, metadata, token_count, message_index, is_heartbeat, created_at)
161
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
162
+ `).run(conversationId, agentId, message.role, message.textContent, message.toolCalls ? JSON.stringify(message.toolCalls) : null, message.toolResults ? JSON.stringify(message.toolResults) : null, message.metadata ? JSON.stringify(message.metadata) : null, opts?.tokenCount || null, messageIndex, opts?.isHeartbeat ? 1 : 0, now);
163
+ const id = result.lastInsertRowid;
164
+ // Update conversation counters
165
+ const tokenDelta = opts?.tokenCount || 0;
166
+ const isOutput = message.role === 'assistant';
167
+ this.db.prepare(`
168
+ UPDATE conversations
169
+ SET message_count = message_count + 1,
170
+ token_count_in = token_count_in + ?,
171
+ token_count_out = token_count_out + ?,
172
+ updated_at = ?
173
+ WHERE id = ?
174
+ `).run(isOutput ? 0 : tokenDelta, isOutput ? tokenDelta : 0, now, conversationId);
175
+ return {
176
+ id,
177
+ conversationId,
178
+ agentId,
179
+ role: message.role,
180
+ textContent: message.textContent,
181
+ toolCalls: message.toolCalls,
182
+ toolResults: message.toolResults,
183
+ metadata: message.metadata,
184
+ messageIndex,
185
+ tokenCount: opts?.tokenCount || null,
186
+ isHeartbeat: opts?.isHeartbeat || false,
187
+ createdAt: now,
188
+ };
189
+ }
190
+ /**
191
+ * Get recent messages for a conversation.
192
+ */
193
+ getRecentMessages(conversationId, limit = 50) {
194
+ const rows = this.db.prepare(`
195
+ SELECT * FROM messages
196
+ WHERE conversation_id = ?
197
+ ORDER BY message_index DESC
198
+ LIMIT ?
199
+ `).all(conversationId, limit);
200
+ // Reverse to get chronological order
201
+ return rows.reverse().map(parseMessageRow);
202
+ }
203
+ /**
204
+ * Get messages across all conversations for an agent (cross-session query).
205
+ */
206
+ getAgentMessages(agentId, opts) {
207
+ let sql = 'SELECT * FROM messages WHERE agent_id = ?';
208
+ const params = [agentId];
209
+ if (opts?.since) {
210
+ sql += ' AND created_at > ?';
211
+ params.push(opts.since);
212
+ }
213
+ if (opts?.excludeHeartbeats) {
214
+ sql += ' AND is_heartbeat = 0';
215
+ }
216
+ sql += ' ORDER BY created_at DESC';
217
+ if (opts?.limit) {
218
+ sql += ' LIMIT ?';
219
+ params.push(opts.limit);
220
+ }
221
+ const rows = this.db.prepare(sql).all(...params);
222
+ return rows.map(parseMessageRow);
223
+ }
224
+ /**
225
+ * Full-text search across all messages for an agent.
226
+ */
227
+ searchMessages(agentId, query, limit = 20) {
228
+ // Per-agent DB contains only one agent's data, so agent_id filter is
229
+ // redundant and catastrophically slows FTS (forces full result scan +
230
+ // join before LIMIT). Omitted by design — see bench/data-access-bench.mjs.
231
+ // Two-phase query: FTS subquery runs first (fast LIMIT inside FTS),
232
+ // then join the small result set for metadata retrieval.
233
+ // Direct JOIN + WHERE MATCH + ORDER BY rank + LIMIT forces SQLite to
234
+ // materialize the full FTS join before applying LIMIT — catastrophic
235
+ // on large message DBs. See: specs/HYPERMEM_INCIDENT_HISTORY.md Incident 3.
236
+ const rows = this.db.prepare(`
237
+ WITH fts_matches AS (
238
+ SELECT rowid, rank FROM messages_fts WHERE messages_fts MATCH ? ORDER BY rank LIMIT ?
239
+ )
240
+ SELECT m.* FROM messages m JOIN fts_matches ON m.id = fts_matches.rowid ORDER BY fts_matches.rank
241
+ `).all(query, limit);
242
+ return rows.map(parseMessageRow);
243
+ }
244
+ /**
245
+ * Get message count for a conversation.
246
+ */
247
+ getMessageCount(conversationId) {
248
+ const row = this.db
249
+ .prepare('SELECT COUNT(*) AS count FROM messages WHERE conversation_id = ?')
250
+ .get(conversationId);
251
+ return row.count;
252
+ }
253
+ // ─── Helpers ─────────────────────────────────────────────────
254
+ /**
255
+ * Infer channel type from session key format.
256
+ */
257
+ inferChannelType(sessionKey) {
258
+ if (sessionKey.includes(':webchat:'))
259
+ return 'webchat';
260
+ if (sessionKey.includes(':discord:'))
261
+ return 'discord';
262
+ if (sessionKey.includes(':telegram:'))
263
+ return 'telegram';
264
+ if (sessionKey.includes(':signal:'))
265
+ return 'signal';
266
+ if (sessionKey.includes(':subagent:') || sessionKey.includes(':spawn:'))
267
+ return 'subagent';
268
+ if (sessionKey.includes(':heartbeat'))
269
+ return 'heartbeat';
270
+ return 'other';
271
+ }
272
+ }
273
+ //# sourceMappingURL=message-store.js.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * HyperMem Preference Store
3
+ *
4
+ * Behavioral patterns observed about people, systems, and workflows.
5
+ * Lives in the central library DB.
6
+ * "ragesaq prefers architectural stability" is a preference, not a fact.
7
+ */
8
+ import type { DatabaseSync } from 'node:sqlite';
9
+ export interface Preference {
10
+ id: number;
11
+ subject: string;
12
+ domain: string;
13
+ key: string;
14
+ value: string;
15
+ agentId: string;
16
+ confidence: number;
17
+ visibility: string;
18
+ sourceType: string;
19
+ sourceRef: string | null;
20
+ createdAt: string;
21
+ updatedAt: string;
22
+ }
23
+ export declare class PreferenceStore {
24
+ private readonly db;
25
+ constructor(db: DatabaseSync);
26
+ /**
27
+ * Set or update a preference. Upserts on (subject, domain, key).
28
+ */
29
+ set(subject: string, key: string, value: string, opts?: {
30
+ domain?: string;
31
+ agentId?: string;
32
+ confidence?: number;
33
+ visibility?: string;
34
+ sourceType?: string;
35
+ sourceRef?: string;
36
+ }): Preference;
37
+ /**
38
+ * Get a specific preference.
39
+ */
40
+ get(subject: string, key: string, domain?: string): Preference | null;
41
+ /**
42
+ * Get all preferences for a subject.
43
+ */
44
+ getForSubject(subject: string, domain?: string): Preference[];
45
+ /**
46
+ * Search preferences by value content.
47
+ */
48
+ search(query: string, subject?: string): Preference[];
49
+ /**
50
+ * Delete a preference.
51
+ */
52
+ delete(subject: string, key: string, domain?: string): boolean;
53
+ }
54
+ //# sourceMappingURL=preference-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preference-store.d.ts","sourceRoot":"","sources":["../src/preference-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAmBD,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C;;OAEG;IACH,GAAG,CACD,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;QACL,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GACA,UAAU;IA+Cb;;OAEG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,MAAkB,GAAG,UAAU,GAAG,IAAI;IAQhF;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,EAAE;IAe7D;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,UAAU,EAAE;IAerD;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,MAAkB,GAAG,OAAO;CAO1E"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * HyperMem Preference Store
3
+ *
4
+ * Behavioral patterns observed about people, systems, and workflows.
5
+ * Lives in the central library DB.
6
+ * "ragesaq prefers architectural stability" is a preference, not a fact.
7
+ */
8
+ function nowIso() {
9
+ return new Date().toISOString();
10
+ }
11
+ function parseRow(row) {
12
+ return {
13
+ id: row.id,
14
+ subject: row.subject,
15
+ domain: row.domain,
16
+ key: row.key,
17
+ value: row.value,
18
+ agentId: row.agent_id,
19
+ confidence: row.confidence,
20
+ visibility: row.visibility || 'fleet',
21
+ sourceType: row.source_type || 'observation',
22
+ sourceRef: row.source_ref || null,
23
+ createdAt: row.created_at,
24
+ updatedAt: row.updated_at,
25
+ };
26
+ }
27
+ export class PreferenceStore {
28
+ db;
29
+ constructor(db) {
30
+ this.db = db;
31
+ }
32
+ /**
33
+ * Set or update a preference. Upserts on (subject, domain, key).
34
+ */
35
+ set(subject, key, value, opts) {
36
+ const now = nowIso();
37
+ const domain = opts?.domain || 'general';
38
+ const result = this.db.prepare(`
39
+ INSERT INTO preferences (subject, domain, key, value, agent_id, confidence,
40
+ visibility, source_type, source_ref, created_at, updated_at)
41
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
42
+ ON CONFLICT(subject, domain, key) DO UPDATE SET
43
+ value = excluded.value,
44
+ confidence = excluded.confidence,
45
+ agent_id = excluded.agent_id,
46
+ source_type = excluded.source_type,
47
+ source_ref = excluded.source_ref,
48
+ updated_at = excluded.updated_at
49
+ `).run(subject, domain, key, value, opts?.agentId || 'system', opts?.confidence || 1.0, opts?.visibility || 'fleet', opts?.sourceType || 'observation', opts?.sourceRef || null, now, now);
50
+ const id = Number(result.lastInsertRowid);
51
+ return {
52
+ id,
53
+ subject,
54
+ domain,
55
+ key,
56
+ value,
57
+ agentId: opts?.agentId || 'system',
58
+ confidence: opts?.confidence || 1.0,
59
+ visibility: opts?.visibility || 'fleet',
60
+ sourceType: opts?.sourceType || 'observation',
61
+ sourceRef: opts?.sourceRef || null,
62
+ createdAt: now,
63
+ updatedAt: now,
64
+ };
65
+ }
66
+ /**
67
+ * Get a specific preference.
68
+ */
69
+ get(subject, key, domain = 'general') {
70
+ const row = this.db.prepare('SELECT * FROM preferences WHERE subject = ? AND domain = ? AND key = ?').get(subject, domain, key);
71
+ return row ? parseRow(row) : null;
72
+ }
73
+ /**
74
+ * Get all preferences for a subject.
75
+ */
76
+ getForSubject(subject, domain) {
77
+ let sql = 'SELECT * FROM preferences WHERE subject = ?';
78
+ const params = [subject];
79
+ if (domain) {
80
+ sql += ' AND domain = ?';
81
+ params.push(domain);
82
+ }
83
+ sql += ' ORDER BY domain, key';
84
+ const rows = this.db.prepare(sql).all(...params);
85
+ return rows.map(parseRow);
86
+ }
87
+ /**
88
+ * Search preferences by value content.
89
+ */
90
+ search(query, subject) {
91
+ let sql = 'SELECT * FROM preferences WHERE (value LIKE ? OR key LIKE ?)';
92
+ const params = [`%${query}%`, `%${query}%`];
93
+ if (subject) {
94
+ sql += ' AND subject = ?';
95
+ params.push(subject);
96
+ }
97
+ sql += ' ORDER BY confidence DESC LIMIT 20';
98
+ const rows = this.db.prepare(sql).all(...params);
99
+ return rows.map(parseRow);
100
+ }
101
+ /**
102
+ * Delete a preference.
103
+ */
104
+ delete(subject, key, domain = 'general') {
105
+ const result = this.db.prepare('DELETE FROM preferences WHERE subject = ? AND domain = ? AND key = ?').run(subject, domain, key);
106
+ return result.changes > 0;
107
+ }
108
+ }
109
+ //# sourceMappingURL=preference-store.js.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * HyperMem Preservation Gate
3
+ *
4
+ * Verifies that a proposed compaction summary preserves the semantic
5
+ * content of its source messages by measuring geometric fidelity in
6
+ * embedding space.
7
+ *
8
+ * Before a summary replaces raw messages, it must pass two checks:
9
+ *
10
+ * 1. Centroid Alignment — the summary embedding must be close to the
11
+ * centroid of the source message embeddings (cos similarity).
12
+ *
13
+ * 2. Source Coverage — the summary must have positive cosine similarity
14
+ * with each individual source message (averaged).
15
+ *
16
+ * If the combined preservation score falls below the threshold, the
17
+ * summary is rejected. The caller should fall back to extractive
18
+ * compaction (concatenation/selection) rather than accepting a
19
+ * semantically drifted summary.
20
+ *
21
+ * This prevents the silent failure mode where a confident summarizer
22
+ * produces fluent text that has drifted away from the original meaning
23
+ * in vector space — making it unretrievable by the very system that
24
+ * will later search for it.
25
+ *
26
+ * Inspired by the Nomic-space preservation gate in openclaw-memory-libravdb
27
+ * (mathematics-v2.md §5.3), adapted for our Ollama + sqlite-vec stack.
28
+ */
29
+ import { type EmbeddingConfig } from './vector-store.js';
30
+ export interface PreservationResult {
31
+ /** Cosine similarity between summary and source centroid */
32
+ alignment: number;
33
+ /** Average positive cosine similarity between summary and each source */
34
+ coverage: number;
35
+ /** Combined score: (alignment + coverage) / 2, clamped to [0, 1] */
36
+ score: number;
37
+ /** Whether the summary passed the preservation gate */
38
+ passed: boolean;
39
+ /** The threshold used for the gate */
40
+ threshold: number;
41
+ }
42
+ export interface PreservationConfig {
43
+ /**
44
+ * Minimum combined preservation score for a summary to be accepted.
45
+ * Default: 0.65 (same as libravdb's shipped default).
46
+ *
47
+ * At 0.65, the summary must be meaningfully close to the source cluster.
48
+ * Lower values accept more drift; higher values are stricter.
49
+ * Range: [0, 1].
50
+ */
51
+ threshold: number;
52
+ /** Embedding config for Ollama calls (used by async path only) */
53
+ embedding?: Partial<EmbeddingConfig>;
54
+ }
55
+ /**
56
+ * Verify that a summary preserves its source content in embedding space.
57
+ *
58
+ * SYNCHRONOUS PATH — for when you already have pre-computed embeddings
59
+ * (e.g., from the background indexer or vector store cache).
60
+ *
61
+ * This is the preferred path: no network calls, no async, deterministic.
62
+ *
63
+ * @param summaryEmbedding - The embedding of the proposed summary
64
+ * @param sourceEmbeddings - Embeddings of the source messages being replaced
65
+ * @param config - Preservation threshold config
66
+ */
67
+ export declare function verifyPreservationFromVectors(summaryEmbedding: Float32Array, sourceEmbeddings: Float32Array[], config?: Partial<PreservationConfig>): PreservationResult;
68
+ /**
69
+ * Verify that a summary preserves its source content in embedding space.
70
+ *
71
+ * ASYNC PATH — generates embeddings via Ollama on demand.
72
+ * Use when pre-computed embeddings aren't available.
73
+ *
74
+ * This makes N+1 embedding calls (1 for summary, N for sources if not cached).
75
+ * For batch compaction, prefer pre-computing embeddings and using the sync path.
76
+ *
77
+ * @param summaryText - The proposed summary text
78
+ * @param sourceTexts - The source message texts being replaced
79
+ * @param config - Preservation threshold and embedding config
80
+ */
81
+ export declare function verifyPreservation(summaryText: string, sourceTexts: string[], config?: Partial<PreservationConfig>): Promise<PreservationResult>;
82
+ //# sourceMappingURL=preservation-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preservation-gate.d.ts","sourceRoot":"","sources":["../src/preservation-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAI7E,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,MAAM,EAAE,OAAO,CAAC;IAChB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;;;;OAOG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;CACtC;AA4DD;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAC3C,gBAAgB,EAAE,YAAY,EAC9B,gBAAgB,EAAE,YAAY,EAAE,EAChC,MAAM,GAAE,OAAO,CAAC,kBAAkB,CAAM,GACvC,kBAAkB,CAmCpB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EAAE,EACrB,MAAM,GAAE,OAAO,CAAC,kBAAkB,CAAM,GACvC,OAAO,CAAC,kBAAkB,CAAC,CAqB7B"}