@psiclawops/hypermem 0.5.0 → 0.5.2

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 (163) hide show
  1. package/ARCHITECTURE.md +12 -3
  2. package/README.md +30 -6
  3. package/bin/hypermem-status.mjs +166 -0
  4. package/dist/background-indexer.d.ts +132 -0
  5. package/dist/background-indexer.d.ts.map +1 -0
  6. package/dist/background-indexer.js +1044 -0
  7. package/dist/cache.d.ts +110 -0
  8. package/dist/cache.d.ts.map +1 -0
  9. package/dist/cache.js +495 -0
  10. package/dist/compaction-fence.d.ts +89 -0
  11. package/dist/compaction-fence.d.ts.map +1 -0
  12. package/dist/compaction-fence.js +153 -0
  13. package/dist/compositor.d.ts +226 -0
  14. package/dist/compositor.d.ts.map +1 -0
  15. package/dist/compositor.js +2558 -0
  16. package/dist/content-type-classifier.d.ts +41 -0
  17. package/dist/content-type-classifier.d.ts.map +1 -0
  18. package/dist/content-type-classifier.js +181 -0
  19. package/dist/cross-agent.d.ts +62 -0
  20. package/dist/cross-agent.d.ts.map +1 -0
  21. package/dist/cross-agent.js +259 -0
  22. package/dist/db.d.ts +131 -0
  23. package/dist/db.d.ts.map +1 -0
  24. package/dist/db.js +402 -0
  25. package/dist/desired-state-store.d.ts +100 -0
  26. package/dist/desired-state-store.d.ts.map +1 -0
  27. package/dist/desired-state-store.js +222 -0
  28. package/dist/doc-chunk-store.d.ts +140 -0
  29. package/dist/doc-chunk-store.d.ts.map +1 -0
  30. package/dist/doc-chunk-store.js +391 -0
  31. package/dist/doc-chunker.d.ts +99 -0
  32. package/dist/doc-chunker.d.ts.map +1 -0
  33. package/dist/doc-chunker.js +324 -0
  34. package/dist/dreaming-promoter.d.ts +86 -0
  35. package/dist/dreaming-promoter.d.ts.map +1 -0
  36. package/dist/dreaming-promoter.js +381 -0
  37. package/dist/episode-store.d.ts +49 -0
  38. package/dist/episode-store.d.ts.map +1 -0
  39. package/dist/episode-store.js +135 -0
  40. package/dist/fact-store.d.ts +75 -0
  41. package/dist/fact-store.d.ts.map +1 -0
  42. package/dist/fact-store.js +236 -0
  43. package/dist/fleet-store.d.ts +144 -0
  44. package/dist/fleet-store.d.ts.map +1 -0
  45. package/dist/fleet-store.js +276 -0
  46. package/dist/fos-mod.d.ts +178 -0
  47. package/dist/fos-mod.d.ts.map +1 -0
  48. package/dist/fos-mod.js +416 -0
  49. package/dist/hybrid-retrieval.d.ts +64 -0
  50. package/dist/hybrid-retrieval.d.ts.map +1 -0
  51. package/dist/hybrid-retrieval.js +344 -0
  52. package/dist/image-eviction.d.ts +49 -0
  53. package/dist/image-eviction.d.ts.map +1 -0
  54. package/dist/image-eviction.js +251 -0
  55. package/dist/index.d.ts +650 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +1072 -0
  58. package/dist/keystone-scorer.d.ts +51 -0
  59. package/dist/keystone-scorer.d.ts.map +1 -0
  60. package/dist/keystone-scorer.js +52 -0
  61. package/dist/knowledge-graph.d.ts +110 -0
  62. package/dist/knowledge-graph.d.ts.map +1 -0
  63. package/dist/knowledge-graph.js +305 -0
  64. package/dist/knowledge-lint.d.ts +29 -0
  65. package/dist/knowledge-lint.d.ts.map +1 -0
  66. package/dist/knowledge-lint.js +116 -0
  67. package/dist/knowledge-store.d.ts +72 -0
  68. package/dist/knowledge-store.d.ts.map +1 -0
  69. package/dist/knowledge-store.js +247 -0
  70. package/dist/library-schema.d.ts +22 -0
  71. package/dist/library-schema.d.ts.map +1 -0
  72. package/dist/library-schema.js +1038 -0
  73. package/dist/message-store.d.ts +89 -0
  74. package/dist/message-store.d.ts.map +1 -0
  75. package/dist/message-store.js +323 -0
  76. package/dist/metrics-dashboard.d.ts +114 -0
  77. package/dist/metrics-dashboard.d.ts.map +1 -0
  78. package/dist/metrics-dashboard.js +260 -0
  79. package/dist/obsidian-exporter.d.ts +57 -0
  80. package/dist/obsidian-exporter.d.ts.map +1 -0
  81. package/dist/obsidian-exporter.js +274 -0
  82. package/dist/obsidian-watcher.d.ts +147 -0
  83. package/dist/obsidian-watcher.d.ts.map +1 -0
  84. package/dist/obsidian-watcher.js +403 -0
  85. package/dist/open-domain.d.ts +46 -0
  86. package/dist/open-domain.d.ts.map +1 -0
  87. package/dist/open-domain.js +125 -0
  88. package/dist/preference-store.d.ts +54 -0
  89. package/dist/preference-store.d.ts.map +1 -0
  90. package/dist/preference-store.js +109 -0
  91. package/dist/preservation-gate.d.ts +82 -0
  92. package/dist/preservation-gate.d.ts.map +1 -0
  93. package/dist/preservation-gate.js +150 -0
  94. package/dist/proactive-pass.d.ts +63 -0
  95. package/dist/proactive-pass.d.ts.map +1 -0
  96. package/dist/proactive-pass.js +239 -0
  97. package/dist/profiles.d.ts +44 -0
  98. package/dist/profiles.d.ts.map +1 -0
  99. package/dist/profiles.js +227 -0
  100. package/dist/provider-translator.d.ts +50 -0
  101. package/dist/provider-translator.d.ts.map +1 -0
  102. package/dist/provider-translator.js +403 -0
  103. package/dist/rate-limiter.d.ts +76 -0
  104. package/dist/rate-limiter.d.ts.map +1 -0
  105. package/dist/rate-limiter.js +179 -0
  106. package/dist/repair-tool-pairs.d.ts +38 -0
  107. package/dist/repair-tool-pairs.d.ts.map +1 -0
  108. package/dist/repair-tool-pairs.js +138 -0
  109. package/dist/retrieval-policy.d.ts +51 -0
  110. package/dist/retrieval-policy.d.ts.map +1 -0
  111. package/dist/retrieval-policy.js +77 -0
  112. package/dist/schema.d.ts +15 -0
  113. package/dist/schema.d.ts.map +1 -0
  114. package/dist/schema.js +229 -0
  115. package/dist/secret-scanner.d.ts +51 -0
  116. package/dist/secret-scanner.d.ts.map +1 -0
  117. package/dist/secret-scanner.js +248 -0
  118. package/dist/seed.d.ts +108 -0
  119. package/dist/seed.d.ts.map +1 -0
  120. package/dist/seed.js +177 -0
  121. package/dist/session-flusher.d.ts +53 -0
  122. package/dist/session-flusher.d.ts.map +1 -0
  123. package/dist/session-flusher.js +69 -0
  124. package/dist/session-topic-map.d.ts +41 -0
  125. package/dist/session-topic-map.d.ts.map +1 -0
  126. package/dist/session-topic-map.js +77 -0
  127. package/dist/spawn-context.d.ts +54 -0
  128. package/dist/spawn-context.d.ts.map +1 -0
  129. package/dist/spawn-context.js +159 -0
  130. package/dist/system-store.d.ts +73 -0
  131. package/dist/system-store.d.ts.map +1 -0
  132. package/dist/system-store.js +182 -0
  133. package/dist/temporal-store.d.ts +80 -0
  134. package/dist/temporal-store.d.ts.map +1 -0
  135. package/dist/temporal-store.js +149 -0
  136. package/dist/topic-detector.d.ts +35 -0
  137. package/dist/topic-detector.d.ts.map +1 -0
  138. package/dist/topic-detector.js +249 -0
  139. package/dist/topic-store.d.ts +45 -0
  140. package/dist/topic-store.d.ts.map +1 -0
  141. package/dist/topic-store.js +136 -0
  142. package/dist/topic-synthesizer.d.ts +51 -0
  143. package/dist/topic-synthesizer.d.ts.map +1 -0
  144. package/dist/topic-synthesizer.js +315 -0
  145. package/dist/trigger-registry.d.ts +63 -0
  146. package/dist/trigger-registry.d.ts.map +1 -0
  147. package/dist/trigger-registry.js +163 -0
  148. package/dist/types.d.ts +537 -0
  149. package/dist/types.d.ts.map +1 -0
  150. package/dist/types.js +9 -0
  151. package/dist/vector-store.d.ts +170 -0
  152. package/dist/vector-store.d.ts.map +1 -0
  153. package/dist/vector-store.js +677 -0
  154. package/dist/version.d.ts +34 -0
  155. package/dist/version.d.ts.map +1 -0
  156. package/dist/version.js +34 -0
  157. package/dist/wiki-page-emitter.d.ts +65 -0
  158. package/dist/wiki-page-emitter.d.ts.map +1 -0
  159. package/dist/wiki-page-emitter.js +258 -0
  160. package/dist/work-store.d.ts +112 -0
  161. package/dist/work-store.d.ts.map +1 -0
  162. package/dist/work-store.js +273 -0
  163. package/package.json +4 -1
@@ -0,0 +1,89 @@
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, RecentTurn } 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 recent messages scoped to a topic (P3.4, Option B).
57
+ * Returns messages matching the topic_id OR with topic_id IS NULL
58
+ * (legacy messages created before topic tracking was introduced).
59
+ * This is transition-safe: no legacy messages are silently dropped.
60
+ */
61
+ getRecentMessagesByTopic(conversationId: number, topicId: string, limit?: number): StoredMessage[];
62
+ /**
63
+ * Get messages across all conversations for an agent (cross-session query).
64
+ */
65
+ getAgentMessages(agentId: string, opts?: {
66
+ since?: string;
67
+ limit?: number;
68
+ excludeHeartbeats?: boolean;
69
+ }): StoredMessage[];
70
+ /**
71
+ * Full-text search across all messages for an agent.
72
+ */
73
+ searchMessages(agentId: string, query: string, limit?: number): StoredMessage[];
74
+ /**
75
+ * Get recent turns for a session, in chronological order, with tool calls stripped.
76
+ * Joins messages through conversations to find by session_key.
77
+ * Returns up to `n` turns (capped at 50).
78
+ */
79
+ getRecentTurns(sessionKey: string, n: number): RecentTurn[];
80
+ /**
81
+ * Get message count for a conversation.
82
+ */
83
+ getMessageCount(conversationId: number): number;
84
+ /**
85
+ * Infer channel type from session key format.
86
+ */
87
+ private inferChannelType;
88
+ }
89
+ //# 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,EAClB,UAAU,EACX,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;;;;;OAKG;IACH,wBAAwB,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,aAAa,EAAE;IAYtG;;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;;;;OAIG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,EAAE;IA+B3D;;OAEG;IACH,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM;IAS/C;;OAEG;IACH,OAAO,CAAC,gBAAgB;CASzB"}
@@ -0,0 +1,323 @@
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 recent messages scoped to a topic (P3.4, Option B).
205
+ * Returns messages matching the topic_id OR with topic_id IS NULL
206
+ * (legacy messages created before topic tracking was introduced).
207
+ * This is transition-safe: no legacy messages are silently dropped.
208
+ */
209
+ getRecentMessagesByTopic(conversationId, topicId, limit = 50) {
210
+ const rows = this.db.prepare(`
211
+ SELECT * FROM messages
212
+ WHERE conversation_id = ? AND (topic_id = ? OR topic_id IS NULL)
213
+ ORDER BY message_index DESC
214
+ LIMIT ?
215
+ `).all(conversationId, topicId, limit);
216
+ // Reverse to get chronological order
217
+ return rows.reverse().map(parseMessageRow);
218
+ }
219
+ /**
220
+ * Get messages across all conversations for an agent (cross-session query).
221
+ */
222
+ getAgentMessages(agentId, opts) {
223
+ let sql = 'SELECT * FROM messages WHERE agent_id = ?';
224
+ const params = [agentId];
225
+ if (opts?.since) {
226
+ sql += ' AND created_at > ?';
227
+ params.push(opts.since);
228
+ }
229
+ if (opts?.excludeHeartbeats) {
230
+ sql += ' AND is_heartbeat = 0';
231
+ }
232
+ sql += ' ORDER BY created_at DESC';
233
+ if (opts?.limit) {
234
+ sql += ' LIMIT ?';
235
+ params.push(opts.limit);
236
+ }
237
+ const rows = this.db.prepare(sql).all(...params);
238
+ return rows.map(parseMessageRow);
239
+ }
240
+ /**
241
+ * Full-text search across all messages for an agent.
242
+ */
243
+ searchMessages(agentId, query, limit = 20) {
244
+ // Per-agent DB contains only one agent's data, so agent_id filter is
245
+ // redundant and catastrophically slows FTS (forces full result scan +
246
+ // join before LIMIT). Omitted by design — see bench/data-access-bench.mjs.
247
+ // Two-phase query: FTS subquery runs first (fast LIMIT inside FTS),
248
+ // then join the small result set for metadata retrieval.
249
+ // Direct JOIN + WHERE MATCH + ORDER BY rank + LIMIT forces SQLite to
250
+ // materialize the full FTS join before applying LIMIT — catastrophic
251
+ // on large message DBs. See: specs/HYPERMEM_INCIDENT_HISTORY.md Incident 3.
252
+ const rows = this.db.prepare(`
253
+ WITH fts_matches AS (
254
+ SELECT rowid, rank FROM messages_fts WHERE messages_fts MATCH ? ORDER BY rank LIMIT ?
255
+ )
256
+ SELECT m.* FROM messages m JOIN fts_matches ON m.id = fts_matches.rowid ORDER BY fts_matches.rank
257
+ `).all(query, limit);
258
+ return rows.map(parseMessageRow);
259
+ }
260
+ /**
261
+ * Get recent turns for a session, in chronological order, with tool calls stripped.
262
+ * Joins messages through conversations to find by session_key.
263
+ * Returns up to `n` turns (capped at 50).
264
+ */
265
+ getRecentTurns(sessionKey, n) {
266
+ const limit = Math.min(n, 50);
267
+ try {
268
+ const rows = this.db.prepare(`
269
+ SELECT m.role, m.text_content, m.created_at, m.message_index
270
+ FROM messages m
271
+ JOIN conversations c ON m.conversation_id = c.id
272
+ WHERE c.session_key = ?
273
+ AND m.role IN ('user', 'assistant')
274
+ ORDER BY m.message_index DESC
275
+ LIMIT ?
276
+ `).all(sessionKey, limit);
277
+ // Reverse to chronological order
278
+ rows.reverse();
279
+ return rows.map(row => ({
280
+ role: row.role,
281
+ // text_content only — tool calls are stored separately and excluded here
282
+ content: row.text_content ?? '',
283
+ timestamp: row.created_at
284
+ ? new Date(row.created_at).getTime()
285
+ : Date.now(),
286
+ seq: row.message_index,
287
+ }));
288
+ }
289
+ catch (err) {
290
+ console.warn('[hypermem:message-store] getRecentTurns failed:', err.message);
291
+ return [];
292
+ }
293
+ }
294
+ /**
295
+ * Get message count for a conversation.
296
+ */
297
+ getMessageCount(conversationId) {
298
+ const row = this.db
299
+ .prepare('SELECT COUNT(*) AS count FROM messages WHERE conversation_id = ?')
300
+ .get(conversationId);
301
+ return row.count;
302
+ }
303
+ // ─── Helpers ─────────────────────────────────────────────────
304
+ /**
305
+ * Infer channel type from session key format.
306
+ */
307
+ inferChannelType(sessionKey) {
308
+ if (sessionKey.includes(':webchat:'))
309
+ return 'webchat';
310
+ if (sessionKey.includes(':discord:'))
311
+ return 'discord';
312
+ if (sessionKey.includes(':telegram:'))
313
+ return 'telegram';
314
+ if (sessionKey.includes(':signal:'))
315
+ return 'signal';
316
+ if (sessionKey.includes(':subagent:') || sessionKey.includes(':spawn:'))
317
+ return 'subagent';
318
+ if (sessionKey.includes(':heartbeat'))
319
+ return 'heartbeat';
320
+ return 'other';
321
+ }
322
+ }
323
+ //# sourceMappingURL=message-store.js.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * hypermem Metrics Dashboard
3
+ *
4
+ * Provides a unified surface for observing system health:
5
+ * - Memory counts (facts, pages, episodes, vectors)
6
+ * - Composition performance (avg assembly time, budget utilization)
7
+ * - Ingestion stats (indexer throughput, promotion rate)
8
+ * - Embedding stats (cache hit rate, Ollama availability)
9
+ *
10
+ * All queries are read-only and safe to call on hot DBs.
11
+ */
12
+ import type { DatabaseSync } from 'node:sqlite';
13
+ export interface FactMetrics {
14
+ /** Total facts indexed across all agents */
15
+ totalFacts: number;
16
+ /** Facts per agent breakdown */
17
+ byAgent: Record<string, number>;
18
+ /** Facts added in the last 24h */
19
+ recentFacts: number;
20
+ }
21
+ export interface WikiMetrics {
22
+ /** Total synthesized wiki pages (non-superseded) */
23
+ totalPages: number;
24
+ /** Pages per agent */
25
+ byAgent: Record<string, number>;
26
+ /** Pages synthesized in the last 24h */
27
+ recentPages: number;
28
+ /** Oldest page age in hours (staleness indicator) */
29
+ oldestPageAgeHours: number | null;
30
+ }
31
+ export interface EpisodeMetrics {
32
+ /** Total episodes stored */
33
+ totalEpisodes: number;
34
+ /** Episodes per agent */
35
+ byAgent: Record<string, number>;
36
+ /** Average episode significance score (0-1) */
37
+ avgSignificance: number | null;
38
+ }
39
+ export interface VectorMetrics {
40
+ /** Total vectors indexed */
41
+ totalVectors: number;
42
+ /** Breakdown by source table */
43
+ byTable: Record<string, number>;
44
+ /** Embedding cache hit rate (0-1) for this process lifetime */
45
+ cacheHitRate: number | null;
46
+ }
47
+ export interface CompositionMetrics {
48
+ /** Average assembly time in ms (from output_metrics table) */
49
+ avgAssemblyMs: number | null;
50
+ /** p95 assembly time in ms */
51
+ p95AssemblyMs: number | null;
52
+ /** Average output tokens per turn */
53
+ avgOutputTokens: number | null;
54
+ /** Average input tokens per turn (context size) */
55
+ avgInputTokens: number | null;
56
+ /** Number of turns recorded */
57
+ totalTurns: number;
58
+ /** Average cache read tokens (Anthropic prompt cache utilization) */
59
+ avgCacheReadTokens: number | null;
60
+ }
61
+ export interface IngestionMetrics {
62
+ /** Total messages processed by the background indexer */
63
+ totalMessagesIndexed: number;
64
+ /** Total facts extracted */
65
+ totalFactsExtracted: number;
66
+ /** Noise rejection rate (1 - facts/messages, approximate) */
67
+ noiseRejectionRate: number | null;
68
+ /** Total episodes created */
69
+ totalEpisodesCreated: number;
70
+ /** Total knowledge items promoted by dreaming promoter */
71
+ totalKnowledgePromoted: number;
72
+ }
73
+ export interface SystemHealth {
74
+ /** Whether the main DB is readable */
75
+ mainDbOk: boolean;
76
+ /** Whether the library DB is readable */
77
+ libraryDbOk: boolean;
78
+ /** Main DB schema version */
79
+ mainSchemaVersion: number | null;
80
+ /** Library DB schema version */
81
+ librarySchemaVersion: number | null;
82
+ /** hypermem package version */
83
+ packageVersion: string;
84
+ /** Cache connection status (if provided) */
85
+ cacheOk: boolean | null;
86
+ /** Timestamp of this snapshot */
87
+ snapshotAt: string;
88
+ }
89
+ export interface HyperMemMetrics {
90
+ facts: FactMetrics;
91
+ wiki: WikiMetrics;
92
+ episodes: EpisodeMetrics;
93
+ vectors: VectorMetrics;
94
+ composition: CompositionMetrics;
95
+ ingestion: IngestionMetrics;
96
+ health: SystemHealth;
97
+ }
98
+ export interface MetricsDashboardOptions {
99
+ /** Agent IDs to scope to. If omitted, returns fleet-wide metrics. */
100
+ agentIds?: string[];
101
+ /** Include per-agent breakdowns. Default: true */
102
+ includeBreakdowns?: boolean;
103
+ }
104
+ /**
105
+ * Collect all metrics in a single pass.
106
+ * Safe to call on live DBs — all queries are read-only.
107
+ */
108
+ export declare function collectMetrics(mainDb: DatabaseSync, libraryDb: DatabaseSync, opts?: MetricsDashboardOptions): Promise<HyperMemMetrics>;
109
+ /**
110
+ * Format metrics as a human-readable summary string.
111
+ * Suitable for logging or status replies.
112
+ */
113
+ export declare function formatMetricsSummary(m: HyperMemMetrics): string;
114
+ //# sourceMappingURL=metrics-dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics-dashboard.d.ts","sourceRoot":"","sources":["../src/metrics-dashboard.ts"],"names":[],"mappings":"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"}