moflo 4.10.24 → 4.10.25

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 (95) hide show
  1. package/.claude/guidance/shipped/moflo-yaml-reference.md +5 -5
  2. package/.claude/skills/meditate/SKILL.md +2 -2
  3. package/bin/cli-hooks/statusline.js +12 -8
  4. package/bin/cli.js +1 -1
  5. package/bin/hooks.mjs +1 -1
  6. package/bin/lib/meditate.mjs +1 -0
  7. package/bin/lib/pii-scrub.mjs +2 -5
  8. package/dist/src/cli/commands/daemon.js +4 -3
  9. package/dist/src/cli/commands/doctor-checks-deep.js +2 -2
  10. package/dist/src/cli/commands/doctor-checks-swarm.js +122 -13
  11. package/dist/src/cli/commands/hooks.js +7 -23
  12. package/dist/src/cli/commands/init.js +1 -1
  13. package/dist/src/cli/commands/mcp.js +2 -1
  14. package/dist/src/cli/commands/session.js +1 -1
  15. package/dist/src/cli/commands/start.js +1 -1
  16. package/dist/src/cli/commands/status.js +1 -1
  17. package/dist/src/cli/commands/task.js +1 -1
  18. package/dist/src/cli/commands/update.js +12 -12
  19. package/dist/src/cli/guidance/analyzer.js +3 -3
  20. package/dist/src/cli/guidance/gates.js +1 -1
  21. package/dist/src/cli/guidance/hooks.js +1 -1
  22. package/dist/src/cli/guidance/meta-governance.js +1 -1
  23. package/dist/src/cli/hooks/index.js +1 -1
  24. package/dist/src/cli/hooks/reasoningbank/guidance-provider.js +1 -1
  25. package/dist/src/cli/hooks/workers/index.js +1 -1
  26. package/dist/src/cli/hooks/workers/session-hook.js +0 -40
  27. package/dist/src/cli/index.js +2 -2
  28. package/dist/src/cli/init/executor.js +36 -20
  29. package/dist/src/cli/init/mcp-generator.js +10 -8
  30. package/dist/src/cli/init/settings-generator.js +10 -7
  31. package/dist/src/cli/init/types.js +2 -2
  32. package/dist/src/cli/mcp-server.js +2 -1
  33. package/dist/src/cli/memory/bridge-loader.js +42 -0
  34. package/dist/src/cli/memory/embedding-model.js +157 -0
  35. package/dist/src/cli/memory/entries-read.js +380 -0
  36. package/dist/src/cli/memory/entries-shared.js +73 -0
  37. package/dist/src/cli/memory/entries-write.js +384 -0
  38. package/dist/src/cli/memory/hnsw-singleton.js +242 -0
  39. package/dist/src/cli/memory/init.js +367 -0
  40. package/dist/src/cli/memory/learnings-overview.js +156 -0
  41. package/dist/src/cli/memory/memory-initializer.js +37 -2257
  42. package/dist/src/cli/memory/quantization.js +221 -0
  43. package/dist/src/cli/memory/schema.js +382 -0
  44. package/dist/src/cli/memory/verify.js +178 -0
  45. package/dist/src/cli/movector/index.js +1 -1
  46. package/dist/src/cli/plugins/store/discovery.js +9 -9
  47. package/dist/src/cli/{transfer/ipfs/client.js → plugins/store/ipfs-client.js} +4 -1
  48. package/dist/src/cli/plugins/tests/demo-plugin-store.js +1 -1
  49. package/dist/src/cli/plugins/tests/standalone-test.js +1 -1
  50. package/dist/src/cli/runtime/headless.js +5 -4
  51. package/dist/src/cli/scripts/publish-registry.js +6 -6
  52. package/dist/src/cli/services/daemon-dashboard.js +108 -7
  53. package/dist/src/cli/services/daemon-readiness.js +1 -1
  54. package/dist/src/cli/services/daemon-service.js +1 -1
  55. package/dist/src/cli/services/env-compat.js +29 -0
  56. package/dist/src/cli/services/hook-block-hash.js +5 -6
  57. package/dist/src/cli/services/registry-api.js +1 -1
  58. package/dist/src/cli/shared/core/config/loader.js +19 -11
  59. package/dist/src/cli/shared/events/example-usage.js +2 -2
  60. package/dist/src/cli/shared/events/index.js +1 -1
  61. package/dist/src/cli/shared/index.js +1 -1
  62. package/dist/src/cli/shared/mcp/index.js +1 -1
  63. package/dist/src/cli/shared/mcp/server.js +3 -3
  64. package/dist/src/cli/shared/plugin-interface.js +1 -1
  65. package/dist/src/cli/shared/plugins/index.js +1 -1
  66. package/dist/src/cli/shared/plugins/official/index.js +1 -1
  67. package/dist/src/cli/shared/security/index.js +1 -1
  68. package/dist/src/cli/shared/services/v3-progress.service.js +40 -29
  69. package/dist/src/cli/shared/types.js +1 -1
  70. package/dist/src/cli/swarm/coordination/swarm-hub.js +1 -1
  71. package/dist/src/cli/update/index.js +1 -1
  72. package/dist/src/cli/update/rate-limiter.js +3 -2
  73. package/dist/src/cli/version.js +1 -1
  74. package/package.json +2 -2
  75. package/dist/src/cli/commands/transfer-store.js +0 -428
  76. package/dist/src/cli/transfer/anonymization/index.js +0 -281
  77. package/dist/src/cli/transfer/deploy-seraphine.js +0 -205
  78. package/dist/src/cli/transfer/export.js +0 -113
  79. package/dist/src/cli/transfer/index.js +0 -31
  80. package/dist/src/cli/transfer/ipfs/upload.js +0 -411
  81. package/dist/src/cli/transfer/models/seraphine.js +0 -373
  82. package/dist/src/cli/transfer/serialization/cfp.js +0 -184
  83. package/dist/src/cli/transfer/storage/gcs.js +0 -242
  84. package/dist/src/cli/transfer/storage/index.js +0 -6
  85. package/dist/src/cli/transfer/store/discovery.js +0 -382
  86. package/dist/src/cli/transfer/store/download.js +0 -334
  87. package/dist/src/cli/transfer/store/index.js +0 -153
  88. package/dist/src/cli/transfer/store/publish.js +0 -294
  89. package/dist/src/cli/transfer/store/registry.js +0 -285
  90. package/dist/src/cli/transfer/store/search.js +0 -232
  91. package/dist/src/cli/transfer/store/tests/standalone-test.js +0 -190
  92. package/dist/src/cli/transfer/store/types.js +0 -6
  93. package/dist/src/cli/transfer/test-seraphine.js +0 -105
  94. package/dist/src/cli/transfer/tests/test-store.js +0 -214
  95. package/dist/src/cli/transfer/types.js +0 -6
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Memory database initialization, legacy migration, and temporal decay.
3
+ *
4
+ * Extracted from `memory-initializer.ts` (#1203 decomposition). Builds a
5
+ * fresh V3 database via the unified `openDaemonDatabase` factory, detects and
6
+ * reports legacy installs, verifies initialization state, and applies
7
+ * confidence decay to stale patterns.
8
+ *
9
+ * @module memory/init
10
+ */
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { errorDetail } from '../shared/utils/error-detail.js';
14
+ import { MOFLO_DIR, legacyMemoryDbPath, memoryDbPath, } from '../services/moflo-paths.js';
15
+ import { openDaemonDatabase } from './daemon-backend.js';
16
+ import { getBridge } from './bridge-loader.js';
17
+ import { MEMORY_SCHEMA_V3, getInitialMetadata } from './schema.js';
18
+ /**
19
+ * Check for legacy database installations and migrate if needed
20
+ */
21
+ export async function checkAndMigrateLegacy(options) {
22
+ const { dbPath, verbose = false } = options;
23
+ // Check for legacy locations. `.swarm/memory.db` is the pre-#727 layout
24
+ // primarily handled by the launcher's copy-verify-delete migration; this
25
+ // probe still catches consumers whose launcher migration deferred. The
26
+ // bare `memory.db` and `.claude/memory.db`/`data/memory.db` entries are
27
+ // older still.
28
+ const legacyPaths = [
29
+ legacyMemoryDbPath(process.cwd()),
30
+ path.join(process.cwd(), MOFLO_DIR, 'memory.db'),
31
+ path.join(process.cwd(), 'memory.db'),
32
+ path.join(process.cwd(), '.claude', 'memory.db'),
33
+ path.join(process.cwd(), 'data', 'memory.db'),
34
+ ];
35
+ for (const legacyPath of legacyPaths) {
36
+ if (fs.existsSync(legacyPath) && legacyPath !== dbPath) {
37
+ try {
38
+ const legacyDb = openDaemonDatabase(legacyPath);
39
+ // Check if it has data
40
+ const countResult = legacyDb.exec('SELECT COUNT(*) FROM memory_entries');
41
+ const count = countResult[0]?.values[0]?.[0] || 0;
42
+ // Get version if available
43
+ let version = 'unknown';
44
+ try {
45
+ const versionResult = legacyDb.exec("SELECT value FROM metadata WHERE key='schema_version'");
46
+ version = versionResult[0]?.values[0]?.[0] || 'unknown';
47
+ }
48
+ catch { /* no metadata table */ }
49
+ legacyDb.close();
50
+ if (count > 0) {
51
+ return {
52
+ needsMigration: true,
53
+ legacyVersion: version,
54
+ legacyEntries: count
55
+ };
56
+ }
57
+ }
58
+ catch {
59
+ // Not a valid SQLite database, skip
60
+ }
61
+ }
62
+ }
63
+ return { needsMigration: false };
64
+ }
65
+ /**
66
+ * ADR-053: Activate ControllerRegistry so AgentDB v3 controllers
67
+ * (ReasoningBank, SkillLibrary, ExplainableRecall, etc.) are instantiated.
68
+ *
69
+ * Uses the memory-bridge's getControllerRegistry() which lazily creates
70
+ * a singleton ControllerRegistry and initializes it with the given dbPath.
71
+ * After this call, all enabled controllers are ready for immediate use.
72
+ *
73
+ * Failures are isolated: if the memory module or its native deps are not installed,
74
+ * this returns an empty result without throwing.
75
+ */
76
+ async function activateControllerRegistry(dbPath, verbose) {
77
+ const startTime = performance.now();
78
+ const activated = [];
79
+ const failed = [];
80
+ try {
81
+ const bridge = await getBridge();
82
+ if (!bridge) {
83
+ return { activated, failed, initTimeMs: performance.now() - startTime };
84
+ }
85
+ const registry = await bridge.getControllerRegistry(dbPath);
86
+ if (!registry) {
87
+ return { activated, failed, initTimeMs: performance.now() - startTime };
88
+ }
89
+ // Collect controller status from the registry
90
+ if (typeof registry.listControllers === 'function') {
91
+ const controllers = registry.listControllers();
92
+ for (const ctrl of controllers) {
93
+ if (ctrl.enabled) {
94
+ activated.push(ctrl.name);
95
+ }
96
+ else {
97
+ failed.push(ctrl.name);
98
+ }
99
+ }
100
+ }
101
+ if (verbose && activated.length > 0) {
102
+ console.log(`ControllerRegistry: ${activated.length} controllers activated`);
103
+ }
104
+ }
105
+ catch {
106
+ // ControllerRegistry activation is best-effort
107
+ }
108
+ return { activated, failed, initTimeMs: performance.now() - startTime };
109
+ }
110
+ /**
111
+ * Cross-platform safe unlink with a short EBUSY/EPERM retry loop. Windows
112
+ * holds file handles briefly after process exit (and the consumer's
113
+ * background daemon may still own the moflo.db handle when `memory init
114
+ * --force` runs); on POSIX the first attempt always succeeds. Total wait
115
+ * is bounded at ~750ms so we never paper over a real long-held handle.
116
+ *
117
+ * Used by `initializeMemoryDatabase` to ride out the daemon's brief handle
118
+ * release race after `flo healer --kill-zombies`. See #1098 for the smoke
119
+ * harness incident that drove this — the daemon's WAL+SHM file handles
120
+ * weren't released by Windows for ~200ms after SIGKILL.
121
+ */
122
+ function unlinkWithRetry(filePath) {
123
+ const delays = [0, 50, 100, 100, 100, 100, 100, 100, 50, 50]; // ~750ms total
124
+ let lastErr = null;
125
+ for (let attempt = 0; attempt < delays.length; attempt++) {
126
+ if (delays[attempt] > 0)
127
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delays[attempt]);
128
+ try {
129
+ fs.unlinkSync(filePath);
130
+ return;
131
+ }
132
+ catch (err) {
133
+ lastErr = err;
134
+ const code = err?.code;
135
+ // Only retry on the handle-release race codes. ENOENT means we've
136
+ // already won (or the file was never there); EACCES could be a real
137
+ // permission issue worth surfacing immediately.
138
+ if (code === 'ENOENT')
139
+ return;
140
+ if (code !== 'EBUSY' && code !== 'EPERM')
141
+ throw err;
142
+ }
143
+ }
144
+ throw lastErr;
145
+ }
146
+ /**
147
+ * Initialize the memory database via the unified node:sqlite factory.
148
+ */
149
+ export async function initializeMemoryDatabase(options) {
150
+ const { backend = 'hybrid', dbPath: customPath, force = false, verbose = false, migrate = true } = options;
151
+ const dbPath = customPath || memoryDbPath(process.cwd());
152
+ const dbDir = path.dirname(dbPath);
153
+ try {
154
+ // Create directory if needed
155
+ if (!fs.existsSync(dbDir)) {
156
+ fs.mkdirSync(dbDir, { recursive: true });
157
+ }
158
+ // Check for legacy installations
159
+ if (migrate) {
160
+ const legacyCheck = await checkAndMigrateLegacy({ dbPath, verbose });
161
+ if (legacyCheck.needsMigration && verbose) {
162
+ console.log(`Found legacy database (v${legacyCheck.legacyVersion}) with ${legacyCheck.legacyEntries} entries`);
163
+ }
164
+ }
165
+ // Check existing database
166
+ if (fs.existsSync(dbPath) && !force) {
167
+ return {
168
+ success: false,
169
+ backend,
170
+ dbPath,
171
+ schemaVersion: '3.0.0',
172
+ tablesCreated: [],
173
+ indexesCreated: [],
174
+ features: {
175
+ vectorEmbeddings: false,
176
+ patternLearning: false,
177
+ temporalDecay: false,
178
+ hnswIndexing: false,
179
+ migrationTracking: false
180
+ },
181
+ error: 'Database already exists. Use --force to reinitialize.'
182
+ };
183
+ }
184
+ // Force a clean slate so the new file gets fresh WAL state too.
185
+ if (fs.existsSync(dbPath) && force) {
186
+ // Windows EBUSY guard (#1098): the consumer's background daemon
187
+ // (spawned by session-start during `npm install`) holds an open
188
+ // file handle on moflo.db. `unlinkSync` would otherwise throw EBUSY
189
+ // immediately — and the OS-level handle release race only resolves
190
+ // after the daemon process actually exits, so a tight retry loop
191
+ // can't outwait it. Stop the daemon first (graceful SIGTERM +
192
+ // 1s grace + SIGKILL via `killBackgroundDaemon`), THEN retry the
193
+ // unlink to ride out any residual handle-release lag.
194
+ const projectRoot = path.dirname(path.dirname(dbPath));
195
+ const { killBackgroundDaemon } = await import('../commands/daemon.js');
196
+ try {
197
+ await killBackgroundDaemon(projectRoot);
198
+ }
199
+ catch { /* best-effort; the retry below still gives us a budget */ }
200
+ unlinkWithRetry(dbPath);
201
+ // Also drop any sidecar WAL files so the next open doesn't replay
202
+ // stale uncommitted transactions from the previous DB.
203
+ for (const suffix of ['-wal', '-shm']) {
204
+ const sidecar = dbPath + suffix;
205
+ if (fs.existsSync(sidecar)) {
206
+ try {
207
+ unlinkWithRetry(sidecar);
208
+ }
209
+ catch { /* best-effort */ }
210
+ }
211
+ }
212
+ }
213
+ const db = openDaemonDatabase(dbPath);
214
+ try {
215
+ // Execute schema
216
+ db.run(MEMORY_SCHEMA_V3);
217
+ // Insert initial metadata
218
+ db.run(getInitialMetadata(backend));
219
+ }
220
+ finally {
221
+ db.close();
222
+ }
223
+ // Also create schema file for reference
224
+ const schemaPath = path.join(dbDir, 'schema.sql');
225
+ fs.writeFileSync(schemaPath, MEMORY_SCHEMA_V3 + '\n' + getInitialMetadata(backend));
226
+ // ADR-053: Activate ControllerRegistry so controllers (ReasoningBank,
227
+ // SkillLibrary, ExplainableRecall, etc.) are instantiated during init
228
+ const controllerResult = await activateControllerRegistry(dbPath, verbose);
229
+ return {
230
+ success: true,
231
+ backend,
232
+ dbPath,
233
+ schemaVersion: '3.0.0',
234
+ tablesCreated: [
235
+ 'memory_entries',
236
+ 'patterns',
237
+ 'pattern_history',
238
+ 'trajectories',
239
+ 'trajectory_steps',
240
+ 'migration_state',
241
+ 'sessions',
242
+ 'vector_indexes',
243
+ 'metadata'
244
+ ],
245
+ indexesCreated: [
246
+ 'idx_memory_namespace',
247
+ 'idx_memory_key',
248
+ 'idx_memory_type',
249
+ 'idx_memory_status',
250
+ 'idx_memory_created',
251
+ 'idx_memory_accessed',
252
+ 'idx_memory_owner',
253
+ 'idx_patterns_type',
254
+ 'idx_patterns_confidence',
255
+ 'idx_patterns_status',
256
+ 'idx_patterns_last_matched',
257
+ 'idx_pattern_history_pattern',
258
+ 'idx_steps_trajectory'
259
+ ],
260
+ features: {
261
+ vectorEmbeddings: true,
262
+ patternLearning: true,
263
+ temporalDecay: true,
264
+ hnswIndexing: true,
265
+ migrationTracking: true
266
+ },
267
+ controllers: controllerResult,
268
+ };
269
+ }
270
+ catch (error) {
271
+ return {
272
+ success: false,
273
+ backend,
274
+ dbPath,
275
+ schemaVersion: '3.0.0',
276
+ tablesCreated: [],
277
+ indexesCreated: [],
278
+ features: {
279
+ vectorEmbeddings: false,
280
+ patternLearning: false,
281
+ temporalDecay: false,
282
+ hnswIndexing: false,
283
+ migrationTracking: false
284
+ },
285
+ error: errorDetail(error)
286
+ };
287
+ }
288
+ }
289
+ /**
290
+ * Check if memory database is properly initialized
291
+ */
292
+ export async function checkMemoryInitialization(dbPath) {
293
+ const path_ = dbPath || memoryDbPath(process.cwd());
294
+ if (!fs.existsSync(path_)) {
295
+ return { initialized: false };
296
+ }
297
+ try {
298
+ const db = openDaemonDatabase(path_);
299
+ // Check for metadata table
300
+ const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table'");
301
+ const tableNames = tables[0]?.values?.map(v => v[0]) || [];
302
+ // Get version
303
+ let version = 'unknown';
304
+ let backend = 'unknown';
305
+ try {
306
+ const versionResult = db.exec("SELECT value FROM metadata WHERE key='schema_version'");
307
+ version = versionResult[0]?.values[0]?.[0] || 'unknown';
308
+ const backendResult = db.exec("SELECT value FROM metadata WHERE key='backend'");
309
+ backend = backendResult[0]?.values[0]?.[0] || 'unknown';
310
+ }
311
+ catch {
312
+ // Metadata table might not exist
313
+ }
314
+ db.close();
315
+ return {
316
+ initialized: true,
317
+ version,
318
+ backend,
319
+ features: {
320
+ vectorEmbeddings: tableNames.includes('vector_indexes'),
321
+ patternLearning: tableNames.includes('patterns'),
322
+ temporalDecay: tableNames.includes('pattern_history')
323
+ },
324
+ tables: tableNames
325
+ };
326
+ }
327
+ catch {
328
+ // Could not read database
329
+ return { initialized: false };
330
+ }
331
+ }
332
+ /**
333
+ * Apply temporal decay to patterns
334
+ * Reduces confidence of patterns that haven't been used recently
335
+ */
336
+ export async function applyTemporalDecay(dbPath) {
337
+ const path_ = dbPath || memoryDbPath(process.cwd());
338
+ try {
339
+ const db = openDaemonDatabase(path_);
340
+ // Apply decay: confidence *= exp(-decay_rate * days_since_last_use)
341
+ const now = Date.now();
342
+ const decayQuery = `
343
+ UPDATE patterns
344
+ SET
345
+ confidence = confidence * (1.0 - decay_rate * ((? - COALESCE(last_matched_at, created_at)) / 86400000.0)),
346
+ updated_at = ?
347
+ WHERE status = 'active'
348
+ AND confidence > 0.1
349
+ AND (? - COALESCE(last_matched_at, created_at)) > 86400000
350
+ `;
351
+ db.run(decayQuery, [now, now, now]);
352
+ const changes = db.getRowsModified();
353
+ db.close();
354
+ return {
355
+ success: true,
356
+ patternsDecayed: changes
357
+ };
358
+ }
359
+ catch (error) {
360
+ return {
361
+ success: false,
362
+ patternsDecayed: 0,
363
+ error: errorDetail(error)
364
+ };
365
+ }
366
+ }
367
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Learnings-namespace overview for the Luminarium "Learnings" panel (#1203).
3
+ *
4
+ * Surfaces what the learning loop (auto-meditate's background distill + manual
5
+ * /meditate + ad-hoc memory_store) has actually accumulated: a bounded list of
6
+ * recent learnings with content, a per-day growth series, and a provenance
7
+ * tally derived from the write-time `source:<origin>` tag.
8
+ *
9
+ * Read-only. Opens the DB directly via the unified `openDaemonDatabase`
10
+ * factory (same pattern as `getNamespaceCounts`) — the dashboard route that
11
+ * calls this runs inside the daemon, so direct access is correct and there is
12
+ * no daemon round-trip. The `total` is an authoritative COUNT (NOT the length
13
+ * of the capped recent list) so the panel never under-reports — the #1149
14
+ * "memory_stats lies" guard applies here too.
15
+ *
16
+ * @module memory/learnings-overview
17
+ */
18
+ import * as fs from 'fs';
19
+ import { memoryDbPath } from '../services/moflo-paths.js';
20
+ import { openDaemonDatabase } from './daemon-backend.js';
21
+ /** The namespace this overview is scoped to. */
22
+ const LEARNINGS_NAMESPACE = 'learnings';
23
+ /** Default number of most-recent learnings returned with their bodies. */
24
+ const DEFAULT_RECENT_LIMIT = 20;
25
+ /** Default per-learning body cap (chars) — bounds the browser payload. */
26
+ const DEFAULT_BODY_CAP = 600;
27
+ /** Cap on the number of daily growth buckets returned (most-recent N). */
28
+ const MAX_GROWTH_BUCKETS = 30;
29
+ /** Provenance bucket key for learnings written before write-time tagging. */
30
+ export const LEGACY_PROVENANCE = 'unknown';
31
+ /** Parse the `source:<origin>` provenance tag out of a tags JSON array. */
32
+ function parseSourceTag(tagsJson) {
33
+ if (!tagsJson)
34
+ return null;
35
+ try {
36
+ const tags = JSON.parse(tagsJson);
37
+ if (!Array.isArray(tags))
38
+ return null;
39
+ for (const t of tags) {
40
+ if (typeof t === 'string' && t.startsWith('source:')) {
41
+ const v = t.slice('source:'.length).trim();
42
+ if (v)
43
+ return v;
44
+ }
45
+ }
46
+ }
47
+ catch {
48
+ // Malformed tags JSON — treat as untagged.
49
+ }
50
+ return null;
51
+ }
52
+ /** First non-empty, trimmed line of a body (for the headline). */
53
+ function firstLineOf(content) {
54
+ for (const line of content.split('\n')) {
55
+ const t = line.trim();
56
+ if (t)
57
+ return t;
58
+ }
59
+ return '';
60
+ }
61
+ /** A finite positive integer, or the default (guards NaN / float / ≤0 inputs). */
62
+ function toPositiveInt(v, def) {
63
+ return typeof v === 'number' && Number.isFinite(v) && v >= 1 ? Math.floor(v) : def;
64
+ }
65
+ /** UTC `YYYY-MM-DD` for a ms timestamp, or null if not a finite timestamp. */
66
+ function utcDay(ms) {
67
+ if (!Number.isFinite(ms) || ms <= 0)
68
+ return null;
69
+ const iso = new Date(ms).toISOString();
70
+ return iso.slice(0, 10);
71
+ }
72
+ const EMPTY = {
73
+ total: 0,
74
+ recent: [],
75
+ provenance: {},
76
+ growth: [],
77
+ addedLast7d: 0,
78
+ addedLast30d: 0,
79
+ };
80
+ /**
81
+ * Build the learnings overview. Returns an empty shape only when the DB file
82
+ * doesn't exist yet (genuine "no learnings"); DB read errors propagate so the
83
+ * dashboard route surfaces a 500 rather than a misleading empty panel.
84
+ */
85
+ export async function getLearningsOverview(options) {
86
+ // recentLimit is interpolated into the SQL LIMIT clause — accept only a
87
+ // finite positive integer so a stray float/NaN/≤0 can't produce bad SQL.
88
+ const recentLimit = toPositiveInt(options?.recentLimit, DEFAULT_RECENT_LIMIT);
89
+ const bodyCap = toPositiveInt(options?.bodyCap, DEFAULT_BODY_CAP);
90
+ const resolvedPath = options?.dbPath || memoryDbPath(process.cwd());
91
+ if (!fs.existsSync(resolvedPath)) {
92
+ return { ...EMPTY };
93
+ }
94
+ const db = openDaemonDatabase(resolvedPath);
95
+ try {
96
+ // One full-namespace scan over the small tags + created_at columns yields
97
+ // BOTH the authoritative total (row count — NOT the capped recent-list
98
+ // length, the #1149 guard) and the provenance tally + growth buckets. Kept
99
+ // in JS so tag parsing needs no SQLite JSON extension. The WHERE clause
100
+ // matches getNamespaceCounts, so this count agrees with memory_stats.
101
+ const allRes = db.exec("SELECT tags, created_at FROM memory_entries WHERE status = 'active' AND namespace = 'learnings'");
102
+ const allRows = allRes[0]?.values ?? [];
103
+ const total = allRows.length;
104
+ const provenance = {};
105
+ const dayCounts = new Map();
106
+ const now = Date.now();
107
+ const cutoff7d = now - 7 * 86_400_000;
108
+ const cutoff30d = now - 30 * 86_400_000;
109
+ let addedLast7d = 0;
110
+ let addedLast30d = 0;
111
+ for (const row of allRows) {
112
+ const [tagsJson, createdAtRaw] = row;
113
+ const source = parseSourceTag(tagsJson) ?? LEGACY_PROVENANCE;
114
+ provenance[source] = (provenance[source] ?? 0) + 1;
115
+ const createdAt = Number(createdAtRaw);
116
+ const day = utcDay(createdAt);
117
+ if (day)
118
+ dayCounts.set(day, (dayCounts.get(day) ?? 0) + 1);
119
+ if (Number.isFinite(createdAt)) {
120
+ if (createdAt >= cutoff7d)
121
+ addedLast7d++;
122
+ if (createdAt >= cutoff30d)
123
+ addedLast30d++;
124
+ }
125
+ }
126
+ // Most-recent MAX_GROWTH_BUCKETS days, oldest→newest (bounds payload).
127
+ const growth = [...dayCounts.entries()]
128
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
129
+ .slice(-MAX_GROWTH_BUCKETS)
130
+ .map(([date, count]) => ({ date, count }));
131
+ // 3) Recent learnings with capped bodies, newest first.
132
+ const recentRes = db.exec("SELECT key, content, tags, created_at, updated_at FROM memory_entries " +
133
+ "WHERE status = 'active' AND namespace = 'learnings' " +
134
+ `ORDER BY updated_at DESC LIMIT ${recentLimit}`);
135
+ const recent = [];
136
+ for (const row of recentRes[0]?.values ?? []) {
137
+ const [key, contentRaw, tagsJson, createdAtRaw, updatedAtRaw] = row;
138
+ const content = contentRaw ?? '';
139
+ const truncated = content.length > bodyCap;
140
+ recent.push({
141
+ key: String(key),
142
+ firstLine: firstLineOf(content).slice(0, 200),
143
+ body: truncated ? content.slice(0, bodyCap) : content,
144
+ truncated,
145
+ source: parseSourceTag(tagsJson),
146
+ createdAt: Number(createdAtRaw) || 0,
147
+ updatedAt: Number(updatedAtRaw) || 0,
148
+ });
149
+ }
150
+ return { total, recent, provenance, growth, addedLast7d, addedLast30d };
151
+ }
152
+ finally {
153
+ db.close();
154
+ }
155
+ }
156
+ //# sourceMappingURL=learnings-overview.js.map