@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
package/dist/seed.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * HyperMem Workspace Seeder
3
+ *
4
+ * Reads ACA workspace files, chunks them by logical section, and indexes
5
+ * into HyperMem for demand-loaded retrieval (ACA offload).
6
+ *
7
+ * Usage:
8
+ * const seeder = new WorkspaceSeeder(hypermem);
9
+ * const result = await seeder.seedWorkspace('/path/to/workspace', { agentId: 'forge' });
10
+ *
11
+ * Idempotent: skips files whose source hash hasn't changed since last index.
12
+ * Atomic: each file's chunks are swapped in a single transaction.
13
+ *
14
+ * Files seeded (from ACA_COLLECTIONS):
15
+ * POLICY.md → governance/policy (shared-fleet)
16
+ * CHARTER.md → governance/charter (per-tier)
17
+ * COMMS.md → governance/comms (shared-fleet)
18
+ * AGENTS.md → operations/agents (per-tier)
19
+ * TOOLS.md → operations/tools (per-agent)
20
+ * SOUL.md → identity/soul (per-agent)
21
+ * JOB.md → identity/job (per-agent)
22
+ * MOTIVATIONS.md → identity/motivations(per-agent)
23
+ * MEMORY.md → memory/decisions (per-agent)
24
+ * memory/*.md → memory/daily (per-agent)
25
+ */
26
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
27
+ import * as path from 'node:path';
28
+ import { chunkFile, inferCollection, ACA_COLLECTIONS, hashContent } from './doc-chunker.js';
29
+ import { DocChunkStore } from './doc-chunk-store.js';
30
+ // ─── Seeder ──────────────────────────────────────────────────────
31
+ export class WorkspaceSeeder {
32
+ db;
33
+ chunkStore;
34
+ constructor(db) {
35
+ this.db = db;
36
+ this.chunkStore = new DocChunkStore(db);
37
+ }
38
+ /**
39
+ * Seed all ACA files from a workspace directory.
40
+ */
41
+ async seedWorkspace(workspaceDir, opts = {}) {
42
+ const result = {
43
+ files: [],
44
+ totalInserted: 0,
45
+ totalDeleted: 0,
46
+ skipped: 0,
47
+ reindexed: 0,
48
+ errors: [],
49
+ };
50
+ const filesToProcess = this.discoverFiles(workspaceDir, opts);
51
+ for (const { filePath, collectionDef } of filesToProcess) {
52
+ // Skip if collection filter provided
53
+ if (opts.collections && !opts.collections.includes(collectionDef.collection)) {
54
+ continue;
55
+ }
56
+ try {
57
+ const chunks = chunkFile(filePath, {
58
+ collection: collectionDef.collection,
59
+ scope: collectionDef.scope,
60
+ tier: collectionDef.scope === 'per-tier' ? (opts.tier ?? 'all') : undefined,
61
+ agentId: collectionDef.scope === 'per-agent' ? opts.agentId : undefined,
62
+ });
63
+ if (chunks.length === 0)
64
+ continue;
65
+ // Force re-index if requested
66
+ if (opts.force) {
67
+ this.chunkStore.deleteSource(filePath, collectionDef.collection);
68
+ }
69
+ const indexResult = this.chunkStore.indexChunks(chunks);
70
+ result.files.push({ filePath, collection: collectionDef.collection, result: indexResult });
71
+ result.totalInserted += indexResult.inserted;
72
+ result.totalDeleted += indexResult.deleted;
73
+ if (indexResult.skipped)
74
+ result.skipped++;
75
+ if (indexResult.reindexed)
76
+ result.reindexed++;
77
+ }
78
+ catch (err) {
79
+ result.errors.push({
80
+ filePath,
81
+ error: err instanceof Error ? err.message : String(err),
82
+ });
83
+ }
84
+ }
85
+ return result;
86
+ }
87
+ /**
88
+ * Seed a single file explicitly.
89
+ */
90
+ seedFile(filePath, collection, opts = {}) {
91
+ const collectionDef = Object.values(ACA_COLLECTIONS).find(d => d.collection === collection)
92
+ ?? { collection, scope: 'per-agent', description: '' };
93
+ if (opts.force) {
94
+ this.chunkStore.deleteSource(filePath, collection);
95
+ }
96
+ const chunks = chunkFile(filePath, {
97
+ collection,
98
+ scope: collectionDef.scope,
99
+ tier: collectionDef.scope === 'per-tier' ? (opts.tier ?? 'all') : undefined,
100
+ agentId: collectionDef.scope === 'per-agent' ? opts.agentId : undefined,
101
+ });
102
+ const indexResult = this.chunkStore.indexChunks(chunks);
103
+ return { filePath, collection, result: indexResult };
104
+ }
105
+ /**
106
+ * Check which workspace files need re-indexing.
107
+ */
108
+ checkStaleness(workspaceDir, opts = {}) {
109
+ const filesToProcess = this.discoverFiles(workspaceDir, opts);
110
+ return filesToProcess.map(({ filePath, collectionDef }) => {
111
+ try {
112
+ const content = readFileSync(filePath, 'utf-8');
113
+ const hash = hashContent(content);
114
+ const needsReindex = this.chunkStore.needsReindex(filePath, collectionDef.collection, hash);
115
+ return { filePath, collection: collectionDef.collection, needsReindex };
116
+ }
117
+ catch {
118
+ return { filePath, collection: collectionDef.collection, needsReindex: true };
119
+ }
120
+ });
121
+ }
122
+ /**
123
+ * Get stats about what's currently indexed.
124
+ */
125
+ getIndexStats() {
126
+ return this.chunkStore.getStats();
127
+ }
128
+ /**
129
+ * Query indexed chunks by collection.
130
+ */
131
+ queryChunks(collection, opts = {}) {
132
+ return this.chunkStore.queryChunks({ collection, ...opts });
133
+ }
134
+ // ─── Private helpers ─────────────────────────────────────────
135
+ discoverFiles(workspaceDir, opts) {
136
+ const files = [];
137
+ // Known ACA files in workspace root
138
+ for (const fileName of Object.keys(ACA_COLLECTIONS)) {
139
+ const filePath = path.join(workspaceDir, fileName);
140
+ if (!existsSync(filePath))
141
+ continue;
142
+ const collectionDef = inferCollection(fileName, opts.agentId);
143
+ if (!collectionDef)
144
+ continue;
145
+ files.push({ filePath, collectionDef });
146
+ }
147
+ // Daily memory files
148
+ if (opts.includeDailyMemory !== false) {
149
+ const memoryDir = path.join(workspaceDir, 'memory');
150
+ if (existsSync(memoryDir)) {
151
+ const memFiles = readdirSync(memoryDir)
152
+ .filter(f => /^\d{4}-\d{2}-\d{2}\.md$/.test(f))
153
+ .sort()
154
+ .reverse(); // Most recent first
155
+ const limit = opts.dailyMemoryLimit ?? 30;
156
+ for (const memFile of memFiles.slice(0, limit)) {
157
+ const filePath = path.join(memoryDir, memFile);
158
+ const collectionDef = inferCollection(memFile, opts.agentId);
159
+ if (collectionDef) {
160
+ files.push({ filePath, collectionDef });
161
+ }
162
+ }
163
+ }
164
+ }
165
+ return files;
166
+ }
167
+ }
168
+ // ─── Standalone seed function ────────────────────────────────────
169
+ /**
170
+ * Seed a workspace directly from a DatabaseSync instance.
171
+ * Convenience wrapper for use in the hook handler and CLI.
172
+ */
173
+ export async function seedWorkspace(db, workspaceDir, opts = {}) {
174
+ const seeder = new WorkspaceSeeder(db);
175
+ return seeder.seedWorkspace(workspaceDir, opts);
176
+ }
177
+ //# sourceMappingURL=seed.js.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * HyperMem System Registry Store
3
+ *
4
+ * Server config, service states, operational flags.
5
+ * Lives in the central library DB.
6
+ * The source of truth for "what's running and what state is it in."
7
+ */
8
+ import type { DatabaseSync } from 'node:sqlite';
9
+ export interface SystemState {
10
+ id: number;
11
+ category: string;
12
+ key: string;
13
+ value: unknown;
14
+ updatedAt: string;
15
+ updatedBy: string | null;
16
+ ttl: string | null;
17
+ }
18
+ export interface SystemEvent {
19
+ id: number;
20
+ category: string;
21
+ key: string;
22
+ eventType: string;
23
+ oldValue: unknown;
24
+ newValue: unknown;
25
+ agentId: string | null;
26
+ createdAt: string;
27
+ metadata: Record<string, unknown> | null;
28
+ }
29
+ export declare class SystemStore {
30
+ private readonly db;
31
+ constructor(db: DatabaseSync);
32
+ /**
33
+ * Set a system state value. Records a change event if the value changed.
34
+ */
35
+ set(category: string, key: string, value: unknown, opts?: {
36
+ updatedBy?: string;
37
+ ttl?: string;
38
+ }): SystemState;
39
+ /**
40
+ * Get a system state value.
41
+ */
42
+ get(category: string, key: string): SystemState | null;
43
+ /**
44
+ * Get all state in a category.
45
+ */
46
+ getCategory(category: string): SystemState[];
47
+ /**
48
+ * Delete a system state entry.
49
+ */
50
+ delete(category: string, key: string, agentId?: string): boolean;
51
+ /**
52
+ * Record an event without changing state (e.g., restart, error, warning).
53
+ */
54
+ recordEvent(category: string, key: string, eventType: string, opts?: {
55
+ agentId?: string;
56
+ metadata?: Record<string, unknown>;
57
+ }): void;
58
+ /**
59
+ * Get recent events for a category/key.
60
+ */
61
+ getEvents(opts?: {
62
+ category?: string;
63
+ key?: string;
64
+ eventType?: string;
65
+ since?: string;
66
+ limit?: number;
67
+ }): SystemEvent[];
68
+ /**
69
+ * Prune expired TTL entries.
70
+ */
71
+ pruneExpired(): number;
72
+ }
73
+ //# sourceMappingURL=system-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"system-store.d.ts","sourceRoot":"","sources":["../src/system-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1C;AAqCD,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C;;OAEG;IACH,GAAG,CACD,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,OAAO,EACd,IAAI,CAAC,EAAE;QACL,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,GACA,WAAW;IA8Bd;;OAEG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAsBtD;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE;IAW5C;;OAEG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IAmBhE;;OAEG;IACH,WAAW,CACT,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,GACA,IAAI;IAcP;;OAEG;IACH,SAAS,CAAC,IAAI,CAAC,EAAE;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,WAAW,EAAE;IA4BjB;;OAEG;IACH,YAAY,IAAI,MAAM;CAQvB"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * HyperMem System Registry Store
3
+ *
4
+ * Server config, service states, operational flags.
5
+ * Lives in the central library DB.
6
+ * The source of truth for "what's running and what state is it in."
7
+ */
8
+ function nowIso() {
9
+ return new Date().toISOString();
10
+ }
11
+ function parseStateRow(row) {
12
+ let value;
13
+ try {
14
+ value = JSON.parse(row.value);
15
+ }
16
+ catch {
17
+ value = row.value;
18
+ }
19
+ return {
20
+ id: row.id,
21
+ category: row.category,
22
+ key: row.key,
23
+ value,
24
+ updatedAt: row.updated_at,
25
+ updatedBy: row.updated_by || null,
26
+ ttl: row.ttl || null,
27
+ };
28
+ }
29
+ function parseEventRow(row) {
30
+ let oldValue;
31
+ let newValue;
32
+ let metadata = null;
33
+ try {
34
+ oldValue = row.old_value ? JSON.parse(row.old_value) : null;
35
+ }
36
+ catch {
37
+ oldValue = row.old_value;
38
+ }
39
+ try {
40
+ newValue = row.new_value ? JSON.parse(row.new_value) : null;
41
+ }
42
+ catch {
43
+ newValue = row.new_value;
44
+ }
45
+ try {
46
+ metadata = row.metadata ? JSON.parse(row.metadata) : null;
47
+ }
48
+ catch { /* ignore */ }
49
+ return {
50
+ id: row.id,
51
+ category: row.category,
52
+ key: row.key,
53
+ eventType: row.event_type,
54
+ oldValue,
55
+ newValue,
56
+ agentId: row.agent_id || null,
57
+ createdAt: row.created_at,
58
+ metadata,
59
+ };
60
+ }
61
+ export class SystemStore {
62
+ db;
63
+ constructor(db) {
64
+ this.db = db;
65
+ }
66
+ /**
67
+ * Set a system state value. Records a change event if the value changed.
68
+ */
69
+ set(category, key, value, opts) {
70
+ const now = nowIso();
71
+ const valueStr = JSON.stringify(value);
72
+ // Get old value for change tracking
73
+ const old = this.db.prepare('SELECT value FROM system_state WHERE category = ? AND key = ?').get(category, key);
74
+ this.db.prepare(`
75
+ INSERT INTO system_state (category, key, value, updated_at, updated_by, ttl)
76
+ VALUES (?, ?, ?, ?, ?, ?)
77
+ ON CONFLICT(category, key) DO UPDATE SET
78
+ value = excluded.value,
79
+ updated_at = excluded.updated_at,
80
+ updated_by = excluded.updated_by,
81
+ ttl = COALESCE(excluded.ttl, ttl)
82
+ `).run(category, key, valueStr, now, opts?.updatedBy || null, opts?.ttl || null);
83
+ // Record change event if value actually changed
84
+ if (!old || old.value !== valueStr) {
85
+ this.db.prepare(`
86
+ INSERT INTO system_events (category, key, event_type, old_value, new_value, agent_id, created_at)
87
+ VALUES (?, ?, 'changed', ?, ?, ?, ?)
88
+ `).run(category, key, old?.value || null, valueStr, opts?.updatedBy || null, now);
89
+ }
90
+ return this.get(category, key);
91
+ }
92
+ /**
93
+ * Get a system state value.
94
+ */
95
+ get(category, key) {
96
+ const row = this.db.prepare('SELECT * FROM system_state WHERE category = ? AND key = ?').get(category, key);
97
+ if (!row)
98
+ return null;
99
+ // Check TTL
100
+ const state = parseStateRow(row);
101
+ if (state.ttl) {
102
+ const ttlDate = new Date(state.ttl);
103
+ if (ttlDate < new Date()) {
104
+ // Expired — delete and return null
105
+ this.db.prepare('DELETE FROM system_state WHERE category = ? AND key = ?')
106
+ .run(category, key);
107
+ return null;
108
+ }
109
+ }
110
+ return state;
111
+ }
112
+ /**
113
+ * Get all state in a category.
114
+ */
115
+ getCategory(category) {
116
+ const rows = this.db.prepare('SELECT * FROM system_state WHERE category = ? ORDER BY key').all(category);
117
+ return rows.map(parseStateRow).filter(s => {
118
+ if (s.ttl && new Date(s.ttl) < new Date())
119
+ return false;
120
+ return true;
121
+ });
122
+ }
123
+ /**
124
+ * Delete a system state entry.
125
+ */
126
+ delete(category, key, agentId) {
127
+ const old = this.db.prepare('SELECT value FROM system_state WHERE category = ? AND key = ?').get(category, key);
128
+ const result = this.db.prepare('DELETE FROM system_state WHERE category = ? AND key = ?').run(category, key);
129
+ if (old && result.changes > 0) {
130
+ this.db.prepare(`
131
+ INSERT INTO system_events (category, key, event_type, old_value, agent_id, created_at)
132
+ VALUES (?, ?, 'deleted', ?, ?, ?)
133
+ `).run(category, key, old.value, agentId || null, nowIso());
134
+ }
135
+ return result.changes > 0;
136
+ }
137
+ /**
138
+ * Record an event without changing state (e.g., restart, error, warning).
139
+ */
140
+ recordEvent(category, key, eventType, opts) {
141
+ this.db.prepare(`
142
+ INSERT INTO system_events (category, key, event_type, agent_id, created_at, metadata)
143
+ VALUES (?, ?, ?, ?, ?, ?)
144
+ `).run(category, key, eventType, opts?.agentId || null, nowIso(), opts?.metadata ? JSON.stringify(opts.metadata) : null);
145
+ }
146
+ /**
147
+ * Get recent events for a category/key.
148
+ */
149
+ getEvents(opts) {
150
+ let sql = 'SELECT * FROM system_events WHERE 1=1';
151
+ const params = [];
152
+ if (opts?.category) {
153
+ sql += ' AND category = ?';
154
+ params.push(opts.category);
155
+ }
156
+ if (opts?.key) {
157
+ sql += ' AND key = ?';
158
+ params.push(opts.key);
159
+ }
160
+ if (opts?.eventType) {
161
+ sql += ' AND event_type = ?';
162
+ params.push(opts.eventType);
163
+ }
164
+ if (opts?.since) {
165
+ sql += ' AND created_at > ?';
166
+ params.push(opts.since);
167
+ }
168
+ sql += ' ORDER BY created_at DESC LIMIT ?';
169
+ params.push(opts?.limit || 50);
170
+ const rows = this.db.prepare(sql).all(...params);
171
+ return rows.map(parseEventRow);
172
+ }
173
+ /**
174
+ * Prune expired TTL entries.
175
+ */
176
+ pruneExpired() {
177
+ const now = nowIso();
178
+ const result = this.db.prepare("DELETE FROM system_state WHERE ttl IS NOT NULL AND ttl < ?").run(now);
179
+ return result.changes;
180
+ }
181
+ }
182
+ //# sourceMappingURL=system-store.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * HyperMem Topic Store
3
+ *
4
+ * Cross-session topic tracking. Topics are conversation threads that
5
+ * can span multiple sessions and channels.
6
+ * Lives in the central library DB.
7
+ */
8
+ import type { DatabaseSync } from 'node:sqlite';
9
+ import type { Topic, TopicStatus } from './types.js';
10
+ export declare class TopicStore {
11
+ private readonly db;
12
+ constructor(db: DatabaseSync);
13
+ /**
14
+ * Create a new topic.
15
+ */
16
+ create(agentId: string, name: string, description?: string, visibility?: string): Topic;
17
+ /**
18
+ * Touch a topic — update activity tracking.
19
+ */
20
+ touch(topicId: number, sessionKey: string, messagesDelta?: number): void;
21
+ /**
22
+ * Get active topics for an agent.
23
+ */
24
+ getActive(agentId: string, limit?: number): Topic[];
25
+ /**
26
+ * Get all topics for an agent.
27
+ */
28
+ getAll(agentId: string, opts?: {
29
+ status?: TopicStatus;
30
+ limit?: number;
31
+ }): Topic[];
32
+ /**
33
+ * Find topics matching a query.
34
+ */
35
+ search(agentId: string, query: string, limit?: number): Topic[];
36
+ /**
37
+ * Mark dormant topics (no activity for dormantAfterHours).
38
+ */
39
+ markDormant(agentId: string, dormantAfterHours?: number): number;
40
+ /**
41
+ * Close dormant topics.
42
+ */
43
+ closeDormant(agentId: string, closedAfterDays?: number): number;
44
+ }
45
+ //# sourceMappingURL=topic-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topic-store.d.ts","sourceRoot":"","sources":["../src/topic-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAqBrD,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK;IAwBvF;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,GAAE,MAAU,GAAG,IAAI;IAY3E;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,KAAK,EAAE;IAWvD;;OAEG;IACH,MAAM,CACJ,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAC9C,KAAK,EAAE;IAoBV;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,KAAK,EAAE;IAanE;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,iBAAiB,GAAE,MAAW,GAAG,MAAM;IAYpE;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,GAAE,MAAU,GAAG,MAAM;CAWnE"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * HyperMem Topic Store
3
+ *
4
+ * Cross-session topic tracking. Topics are conversation threads that
5
+ * can span multiple sessions and channels.
6
+ * Lives in the central library DB.
7
+ */
8
+ function nowIso() {
9
+ return new Date().toISOString();
10
+ }
11
+ function parseTopicRow(row) {
12
+ return {
13
+ id: row.id,
14
+ agentId: row.agent_id,
15
+ name: row.name,
16
+ description: row.description || null,
17
+ status: row.status,
18
+ visibility: row.visibility || 'org',
19
+ lastSessionKey: row.last_session_key || null,
20
+ messageCount: row.message_count,
21
+ createdAt: row.created_at,
22
+ updatedAt: row.updated_at,
23
+ };
24
+ }
25
+ export class TopicStore {
26
+ db;
27
+ constructor(db) {
28
+ this.db = db;
29
+ }
30
+ /**
31
+ * Create a new topic.
32
+ */
33
+ create(agentId, name, description, visibility) {
34
+ const now = nowIso();
35
+ const result = this.db.prepare(`
36
+ INSERT INTO topics (agent_id, name, description, status, visibility, message_count, created_at, updated_at)
37
+ VALUES (?, ?, ?, 'active', ?, 0, ?, ?)
38
+ `).run(agentId, name, description || null, visibility || 'org', now, now);
39
+ const id = Number(result.lastInsertRowid);
40
+ return {
41
+ id,
42
+ agentId,
43
+ name,
44
+ description: description || null,
45
+ status: 'active',
46
+ visibility: visibility || 'org',
47
+ lastSessionKey: null,
48
+ messageCount: 0,
49
+ createdAt: now,
50
+ updatedAt: now,
51
+ };
52
+ }
53
+ /**
54
+ * Touch a topic — update activity tracking.
55
+ */
56
+ touch(topicId, sessionKey, messagesDelta = 1) {
57
+ const now = nowIso();
58
+ this.db.prepare(`
59
+ UPDATE topics
60
+ SET last_session_key = ?,
61
+ message_count = message_count + ?,
62
+ status = 'active',
63
+ updated_at = ?
64
+ WHERE id = ?
65
+ `).run(sessionKey, messagesDelta, now, topicId);
66
+ }
67
+ /**
68
+ * Get active topics for an agent.
69
+ */
70
+ getActive(agentId, limit = 20) {
71
+ const rows = this.db.prepare(`
72
+ SELECT * FROM topics
73
+ WHERE agent_id = ? AND status = 'active'
74
+ ORDER BY updated_at DESC
75
+ LIMIT ?
76
+ `).all(agentId, limit);
77
+ return rows.map(parseTopicRow);
78
+ }
79
+ /**
80
+ * Get all topics for an agent.
81
+ */
82
+ getAll(agentId, opts) {
83
+ let sql = 'SELECT * FROM topics WHERE agent_id = ?';
84
+ const params = [agentId];
85
+ if (opts?.status) {
86
+ sql += ' AND status = ?';
87
+ params.push(opts.status);
88
+ }
89
+ sql += ' ORDER BY updated_at DESC';
90
+ if (opts?.limit) {
91
+ sql += ' LIMIT ?';
92
+ params.push(opts.limit);
93
+ }
94
+ const rows = this.db.prepare(sql).all(...params);
95
+ return rows.map(parseTopicRow);
96
+ }
97
+ /**
98
+ * Find topics matching a query.
99
+ */
100
+ search(agentId, query, limit = 10) {
101
+ const rows = this.db.prepare(`
102
+ SELECT * FROM topics
103
+ WHERE agent_id = ?
104
+ AND (name LIKE ? OR description LIKE ?)
105
+ AND status != 'closed'
106
+ ORDER BY updated_at DESC
107
+ LIMIT ?
108
+ `).all(agentId, `%${query}%`, `%${query}%`, limit);
109
+ return rows.map(parseTopicRow);
110
+ }
111
+ /**
112
+ * Mark dormant topics (no activity for dormantAfterHours).
113
+ */
114
+ markDormant(agentId, dormantAfterHours = 24) {
115
+ const cutoff = new Date(Date.now() - dormantAfterHours * 60 * 60 * 1000).toISOString();
116
+ const result = this.db.prepare(`
117
+ UPDATE topics
118
+ SET status = 'dormant', updated_at = ?
119
+ WHERE agent_id = ? AND status = 'active' AND updated_at < ?
120
+ `).run(nowIso(), agentId, cutoff);
121
+ return result.changes;
122
+ }
123
+ /**
124
+ * Close dormant topics.
125
+ */
126
+ closeDormant(agentId, closedAfterDays = 7) {
127
+ const cutoff = new Date(Date.now() - closedAfterDays * 24 * 60 * 60 * 1000).toISOString();
128
+ const result = this.db.prepare(`
129
+ UPDATE topics
130
+ SET status = 'closed', updated_at = ?
131
+ WHERE agent_id = ? AND status = 'dormant' AND updated_at < ?
132
+ `).run(nowIso(), agentId, cutoff);
133
+ return result.changes;
134
+ }
135
+ }
136
+ //# sourceMappingURL=topic-store.js.map