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.
- package/.claude/guidance/shipped/moflo-yaml-reference.md +5 -5
- package/.claude/skills/meditate/SKILL.md +2 -2
- package/bin/cli-hooks/statusline.js +12 -8
- package/bin/cli.js +1 -1
- package/bin/hooks.mjs +1 -1
- package/bin/lib/meditate.mjs +1 -0
- package/bin/lib/pii-scrub.mjs +2 -5
- package/dist/src/cli/commands/daemon.js +4 -3
- package/dist/src/cli/commands/doctor-checks-deep.js +2 -2
- package/dist/src/cli/commands/doctor-checks-swarm.js +122 -13
- package/dist/src/cli/commands/hooks.js +7 -23
- package/dist/src/cli/commands/init.js +1 -1
- package/dist/src/cli/commands/mcp.js +2 -1
- package/dist/src/cli/commands/session.js +1 -1
- package/dist/src/cli/commands/start.js +1 -1
- package/dist/src/cli/commands/status.js +1 -1
- package/dist/src/cli/commands/task.js +1 -1
- package/dist/src/cli/commands/update.js +12 -12
- package/dist/src/cli/guidance/analyzer.js +3 -3
- package/dist/src/cli/guidance/gates.js +1 -1
- package/dist/src/cli/guidance/hooks.js +1 -1
- package/dist/src/cli/guidance/meta-governance.js +1 -1
- package/dist/src/cli/hooks/index.js +1 -1
- package/dist/src/cli/hooks/reasoningbank/guidance-provider.js +1 -1
- package/dist/src/cli/hooks/workers/index.js +1 -1
- package/dist/src/cli/hooks/workers/session-hook.js +0 -40
- package/dist/src/cli/index.js +2 -2
- package/dist/src/cli/init/executor.js +36 -20
- package/dist/src/cli/init/mcp-generator.js +10 -8
- package/dist/src/cli/init/settings-generator.js +10 -7
- package/dist/src/cli/init/types.js +2 -2
- package/dist/src/cli/mcp-server.js +2 -1
- package/dist/src/cli/memory/bridge-loader.js +42 -0
- package/dist/src/cli/memory/embedding-model.js +157 -0
- package/dist/src/cli/memory/entries-read.js +380 -0
- package/dist/src/cli/memory/entries-shared.js +73 -0
- package/dist/src/cli/memory/entries-write.js +384 -0
- package/dist/src/cli/memory/hnsw-singleton.js +242 -0
- package/dist/src/cli/memory/init.js +367 -0
- package/dist/src/cli/memory/learnings-overview.js +156 -0
- package/dist/src/cli/memory/memory-initializer.js +37 -2257
- package/dist/src/cli/memory/quantization.js +221 -0
- package/dist/src/cli/memory/schema.js +382 -0
- package/dist/src/cli/memory/verify.js +178 -0
- package/dist/src/cli/movector/index.js +1 -1
- package/dist/src/cli/plugins/store/discovery.js +9 -9
- package/dist/src/cli/{transfer/ipfs/client.js → plugins/store/ipfs-client.js} +4 -1
- package/dist/src/cli/plugins/tests/demo-plugin-store.js +1 -1
- package/dist/src/cli/plugins/tests/standalone-test.js +1 -1
- package/dist/src/cli/runtime/headless.js +5 -4
- package/dist/src/cli/scripts/publish-registry.js +6 -6
- package/dist/src/cli/services/daemon-dashboard.js +108 -7
- package/dist/src/cli/services/daemon-readiness.js +1 -1
- package/dist/src/cli/services/daemon-service.js +1 -1
- package/dist/src/cli/services/env-compat.js +29 -0
- package/dist/src/cli/services/hook-block-hash.js +5 -6
- package/dist/src/cli/services/registry-api.js +1 -1
- package/dist/src/cli/shared/core/config/loader.js +19 -11
- package/dist/src/cli/shared/events/example-usage.js +2 -2
- package/dist/src/cli/shared/events/index.js +1 -1
- package/dist/src/cli/shared/index.js +1 -1
- package/dist/src/cli/shared/mcp/index.js +1 -1
- package/dist/src/cli/shared/mcp/server.js +3 -3
- package/dist/src/cli/shared/plugin-interface.js +1 -1
- package/dist/src/cli/shared/plugins/index.js +1 -1
- package/dist/src/cli/shared/plugins/official/index.js +1 -1
- package/dist/src/cli/shared/security/index.js +1 -1
- package/dist/src/cli/shared/services/v3-progress.service.js +40 -29
- package/dist/src/cli/shared/types.js +1 -1
- package/dist/src/cli/swarm/coordination/swarm-hub.js +1 -1
- package/dist/src/cli/update/index.js +1 -1
- package/dist/src/cli/update/rate-limiter.js +3 -2
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/dist/src/cli/commands/transfer-store.js +0 -428
- package/dist/src/cli/transfer/anonymization/index.js +0 -281
- package/dist/src/cli/transfer/deploy-seraphine.js +0 -205
- package/dist/src/cli/transfer/export.js +0 -113
- package/dist/src/cli/transfer/index.js +0 -31
- package/dist/src/cli/transfer/ipfs/upload.js +0 -411
- package/dist/src/cli/transfer/models/seraphine.js +0 -373
- package/dist/src/cli/transfer/serialization/cfp.js +0 -184
- package/dist/src/cli/transfer/storage/gcs.js +0 -242
- package/dist/src/cli/transfer/storage/index.js +0 -6
- package/dist/src/cli/transfer/store/discovery.js +0 -382
- package/dist/src/cli/transfer/store/download.js +0 -334
- package/dist/src/cli/transfer/store/index.js +0 -153
- package/dist/src/cli/transfer/store/publish.js +0 -294
- package/dist/src/cli/transfer/store/registry.js +0 -285
- package/dist/src/cli/transfer/store/search.js +0 -232
- package/dist/src/cli/transfer/store/tests/standalone-test.js +0 -190
- package/dist/src/cli/transfer/store/types.js +0 -6
- package/dist/src/cli/transfer/test-seraphine.js +0 -105
- package/dist/src/cli/transfer/tests/test-store.js +0 -214
- 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
|