claude-flow 3.21.1 → 3.23.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 (40) hide show
  1. package/.claude/helpers/.helpers-version +1 -0
  2. package/.claude/helpers/auto-memory-hook.mjs +59 -274
  3. package/.claude/helpers/helpers.manifest.json +12 -0
  4. package/.claude/helpers/hook-handler.cjs +132 -112
  5. package/.claude/helpers/intelligence.cjs +992 -196
  6. package/package.json +1 -1
  7. package/v3/@claude-flow/cli/dist/src/commands/daemon.js +23 -2
  8. package/v3/@claude-flow/cli/dist/src/commands/memory-backup.d.ts +11 -0
  9. package/v3/@claude-flow/cli/dist/src/commands/memory-backup.js +46 -0
  10. package/v3/@claude-flow/cli/dist/src/commands/memory-distill.d.ts +27 -0
  11. package/v3/@claude-flow/cli/dist/src/commands/memory-distill.js +374 -0
  12. package/v3/@claude-flow/cli/dist/src/commands/memory.js +5 -2
  13. package/v3/@claude-flow/cli/dist/src/index.d.ts +1 -1
  14. package/v3/@claude-flow/cli/dist/src/index.js +22 -1
  15. package/v3/@claude-flow/cli/dist/src/init/executor.js +20 -0
  16. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +19 -0
  17. package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +175 -0
  18. package/v3/@claude-flow/cli/dist/src/init/helper-signing.d.ts +30 -0
  19. package/v3/@claude-flow/cli/dist/src/init/helper-signing.js +60 -0
  20. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.d.ts +16 -0
  21. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +63 -6
  22. package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +18 -7
  23. package/v3/@claude-flow/cli/dist/src/mcp-client.js +13 -0
  24. package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +9 -2
  25. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.d.ts +162 -0
  26. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +548 -0
  27. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.d.ts +9 -1
  28. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +4 -1
  29. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +67 -47
  30. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +71 -0
  31. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +330 -2
  32. package/v3/@claude-flow/cli/dist/src/services/distill-tuning.d.ts +111 -0
  33. package/v3/@claude-flow/cli/dist/src/services/distill-tuning.js +510 -0
  34. package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +24 -0
  35. package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +94 -0
  36. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.d.ts +41 -0
  37. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.js +277 -0
  38. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +31 -2
  39. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +159 -6
  40. package/v3/@claude-flow/cli/package.json +5 -2
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Vector-memory DB backup.
3
+ *
4
+ * Snapshots `.swarm/memory.db` (the sqlite store holding memory_entries +
5
+ * embeddings + the distilled reasoning_patterns) to a timestamped file using
6
+ * better-sqlite3's ONLINE backup API — a consistent, WAL-safe copy that does not
7
+ * block or corrupt a concurrently-written DB (unlike a naive file copy of a
8
+ * WAL-mode DB). Rotates to keep the last N snapshots and, optionally, uploads
9
+ * offsite to Google Cloud Storage.
10
+ *
11
+ * Used by `memory backup` (manual) and the daemon's nightly `backup` worker.
12
+ * Best-effort + non-destructive: it only reads the source DB and writes new
13
+ * files; it never mutates or deletes the live memory DB.
14
+ */
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ export function defaultMemoryDbPath(cwd = process.cwd()) {
18
+ return path.join(cwd, '.swarm', 'memory.db');
19
+ }
20
+ /** ISO timestamp safe for filenames (no ':' or '.'). */
21
+ function fileStamp(ms) {
22
+ return new Date(ms).toISOString().replace(/[:.]/g, '-');
23
+ }
24
+ export async function backupMemoryDb(opts = {}) {
25
+ const dbPath = opts.dbPath ?? defaultMemoryDbPath();
26
+ if (!dbPath || !fs.existsSync(dbPath))
27
+ return { backedUp: false, skipped: 'no-db' };
28
+ let Database;
29
+ try {
30
+ const mod = 'better-sqlite3';
31
+ Database = (await import(mod)).default;
32
+ }
33
+ catch {
34
+ return { backedUp: false, skipped: 'better-sqlite3 unavailable' };
35
+ }
36
+ const destDir = opts.destDir ?? path.join(path.dirname(dbPath), 'backups');
37
+ try {
38
+ fs.mkdirSync(destDir, { recursive: true });
39
+ }
40
+ catch { /* */ }
41
+ const destPath = path.join(destDir, `memory-${fileStamp(opts.timestamp ?? Date.now())}.db`);
42
+ // WAL-safe online backup: read-only source, consistent snapshot to destPath.
43
+ let db;
44
+ try {
45
+ db = new Database(dbPath, { readonly: true });
46
+ await db.backup(destPath);
47
+ db.close();
48
+ }
49
+ catch (e) {
50
+ try {
51
+ db?.close();
52
+ }
53
+ catch { /* */ }
54
+ return { backedUp: false, skipped: `backup failed: ${e?.message ?? e}` };
55
+ }
56
+ let sizeBytes = 0;
57
+ try {
58
+ sizeBytes = fs.statSync(destPath).size;
59
+ }
60
+ catch { /* */ }
61
+ // Rotation — ISO-stamped names sort chronologically, so keep the tail.
62
+ const keep = typeof opts.keep === 'number' && opts.keep > 0 ? opts.keep : 7;
63
+ const rotatedAway = [];
64
+ try {
65
+ const snaps = fs.readdirSync(destDir).filter(f => /^memory-.*\.db$/.test(f)).sort();
66
+ while (snaps.length > keep) {
67
+ const old = snaps.shift();
68
+ try {
69
+ fs.rmSync(path.join(destDir, old), { force: true });
70
+ rotatedAway.push(old);
71
+ }
72
+ catch { /* */ }
73
+ }
74
+ }
75
+ catch { /* */ }
76
+ // Optional offsite to GCS (best-effort; local backup already succeeded).
77
+ let gcsUri;
78
+ if (opts.gcs) {
79
+ try {
80
+ const { execFileSync } = await import('child_process');
81
+ const dest = opts.gcs.replace(/\/+$/, '') + '/' + path.basename(destPath);
82
+ execFileSync('gcloud', ['storage', 'cp', destPath, dest], { stdio: ['ignore', 'ignore', 'inherit'] });
83
+ gcsUri = dest;
84
+ }
85
+ catch { /* offsite failed — local snapshot stands */ }
86
+ }
87
+ if (opts.verbose) {
88
+ console.log(`memory DB backed up → ${destPath} (${Math.round(sizeBytes / 1024)} KB)` +
89
+ (rotatedAway.length ? `, rotated ${rotatedAway.length} old` : '') +
90
+ (gcsUri ? `, offsite ${gcsUri}` : ''));
91
+ }
92
+ return { backedUp: true, path: destPath, sizeBytes, rotatedAway, gcsUri };
93
+ }
94
+ //# sourceMappingURL=memory-backup.js.map
@@ -0,0 +1,41 @@
1
+ export type DistillProvenance = 'oracle:test-exec' | 'judge:fable' | 'proxy:structural';
2
+ export interface DistillOptions {
3
+ dbPath: string;
4
+ /** Restrict to these namespaces (default: all namespaces with embeddings). */
5
+ namespaces?: string[];
6
+ /** Rows per transaction (default 200). */
7
+ batchSize?: number;
8
+ /** Hard cap on rows processed this invocation (default: unbounded within a run). */
9
+ maxEntries?: number;
10
+ /** Cosine distance below which two entries collapse into one pattern (default 0.2, the ADR-174 M4-tuned platform default: ~37% fewer patterns, retrieval-neutral). */
11
+ dedupDistance?: number;
12
+ /** Report counts, write nothing (default false). */
13
+ dryRun?: boolean;
14
+ /** Judge tier. Only 'structural' ($0) is implemented here; 'fable' is reserved (ADR-172). */
15
+ judge?: 'structural' | 'fable';
16
+ /** Ignore the cursor and re-scan from this rowid (default: cursor-driven). */
17
+ sinceRowid?: number;
18
+ verbose?: boolean;
19
+ }
20
+ export interface DistillReport {
21
+ processed: number;
22
+ episodes: number;
23
+ patterns: number;
24
+ patternEmbeddings: number;
25
+ causalEdges: number;
26
+ promoted: number;
27
+ byProvenance: Record<string, number>;
28
+ namespaces: string[];
29
+ dryRun: boolean;
30
+ spendUsd: number;
31
+ corrupt?: boolean;
32
+ skipped?: string;
33
+ }
34
+ /**
35
+ * Distill accumulated memory into the structured intelligence tables.
36
+ * Incremental, $0, transactional, provenance-tagged.
37
+ */
38
+ export declare function runDistillation(options: DistillOptions): Promise<DistillReport>;
39
+ /** Where the memory DB lives, for daemon/CLI callers. */
40
+ export declare function defaultMemoryDbPath(cwd?: string): string;
41
+ //# sourceMappingURL=memory-distillation.d.ts.map
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Memory Distillation Service (ADR-174)
3
+ *
4
+ * Turns raw `memory_entries` (what ruflo has been RECORDING for thousands of
5
+ * commits) into the structured intelligence substrate it has never populated:
6
+ * `episodes` → `reasoning_patterns` (+ `pattern_embeddings`) → `causal_edges`.
7
+ * This is the DISTILL/CONSOLIDATE half of the RETRIEVE→JUDGE→DISTILL→CONSOLIDATE
8
+ * pipeline; RETRIEVE (embeddings) was already populated, the rest was empty
9
+ * because the daemon's `consolidate` worker was a stub (see ADR-174 root cause).
10
+ *
11
+ * Design invariants (load-bearing):
12
+ * - $0 by default: reuses the deterministic structural extractor
13
+ * (structured-distill.ts) and the embeddings ALREADY on each row — no LLM
14
+ * call, no new embedding work, unless a caller explicitly opts into a judge.
15
+ * - Incremental: a `distill_state` cursor (per namespace, by monotonic rowid)
16
+ * means a run never rescans processed rows.
17
+ * - Non-destructive: NEVER mutates or deletes `memory_entries`; only inserts
18
+ * into the (previously empty) target tables. Writes are per-batch
19
+ * transactions — a failure rolls back the batch and advances no cursor.
20
+ * - Safe on a recovered DB: quick_check gate before any write; skips (not
21
+ * throws) on corruption; better-sqlite3 optional (silent no-op if absent).
22
+ * - Provenance discipline (ADR-171): `feedback` outcomes are execution-tier
23
+ * ground truth; everything else is proxy. Proxy-tier patterns are written
24
+ * but never `promoted` — visible for audit, excluded from promoted recall.
25
+ */
26
+ import * as fs from 'fs';
27
+ import * as path from 'path';
28
+ import { distillTrajectoryContent, serialiseDistilled } from '../memory/structured-distill.js';
29
+ function emptyReport(dryRun, extra = {}) {
30
+ return {
31
+ processed: 0, episodes: 0, patterns: 0, patternEmbeddings: 0, causalEdges: 0,
32
+ promoted: 0, byProvenance: {}, namespaces: [], dryRun, spendUsd: 0, ...extra,
33
+ };
34
+ }
35
+ function parseEmbedding(raw) {
36
+ if (typeof raw !== 'string' || !raw)
37
+ return null;
38
+ try {
39
+ const v = JSON.parse(raw);
40
+ return Array.isArray(v) ? v : null;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function cosine(a, b) {
47
+ const len = Math.min(a.length, b.length);
48
+ let dot = 0, na = 0, nb = 0;
49
+ for (let i = 0; i < len; i++) {
50
+ dot += a[i] * b[i];
51
+ na += a[i] * a[i];
52
+ nb += b[i] * b[i];
53
+ }
54
+ const mag = Math.sqrt(na * nb);
55
+ return mag === 0 ? 0 : dot / mag;
56
+ }
57
+ /**
58
+ * Best-effort success/reward extraction for a `feedback` outcome record. These
59
+ * are recorded post-edit outcomes — execution-observed ground truth (ADR-171
60
+ * execution tier), not a proxy guess.
61
+ */
62
+ function judgeFeedback(content) {
63
+ try {
64
+ const o = JSON.parse(content);
65
+ if (typeof o.success === 'boolean')
66
+ return { success: o.success, reward: o.success ? 1 : 0 };
67
+ if (typeof o.outcome === 'string') {
68
+ const ok = /success|pass|ok|true/i.test(o.outcome);
69
+ return { success: ok, reward: ok ? 1 : 0 };
70
+ }
71
+ if (typeof o.error === 'string' && o.error)
72
+ return { success: false, reward: 0 };
73
+ }
74
+ catch { /* not JSON — treat as neutral */ }
75
+ return { success: true, reward: 0.5 };
76
+ }
77
+ /**
78
+ * Distill accumulated memory into the structured intelligence tables.
79
+ * Incremental, $0, transactional, provenance-tagged.
80
+ */
81
+ export async function runDistillation(options) {
82
+ const { dbPath, namespaces, batchSize = 200, maxEntries, dedupDistance = 0.2, dryRun = false, judge = 'structural', sinceRowid, verbose = false, } = options;
83
+ if (judge === 'fable') {
84
+ // ADR-172 cost gate: the LLM judge path is not enabled in this $0 service.
85
+ return emptyReport(dryRun, { skipped: 'judge:fable requires the cost-bounded advisor path (ADR-172), not enabled here' });
86
+ }
87
+ if (!dbPath || !fs.existsSync(dbPath))
88
+ return emptyReport(dryRun, { skipped: 'no-db' });
89
+ let Database;
90
+ try {
91
+ const mod = 'better-sqlite3';
92
+ Database = (await import(mod)).default;
93
+ }
94
+ catch {
95
+ return emptyReport(dryRun, { skipped: 'better-sqlite3 unavailable' });
96
+ }
97
+ const report = emptyReport(dryRun);
98
+ let db;
99
+ try {
100
+ db = new Database(dbPath, { timeout: 3000 });
101
+ const tableExists = (name) => (db.prepare("SELECT COUNT(*) AS c FROM sqlite_master WHERE type='table' AND name=?").get(name)?.c ?? 0) > 0;
102
+ if (!tableExists('memory_entries')) {
103
+ db.close();
104
+ return { ...report, skipped: 'no memory_entries' };
105
+ }
106
+ for (const t of ['episodes', 'reasoning_patterns', 'pattern_embeddings', 'causal_edges']) {
107
+ if (!tableExists(t)) {
108
+ db.close();
109
+ return { ...report, skipped: `target table ${t} missing (agentdb schema not initialised)` };
110
+ }
111
+ }
112
+ // Safety gate: never distill into a structurally-corrupt DB.
113
+ const qc = db.prepare('PRAGMA quick_check(1)').get();
114
+ if (qc && String(Object.values(qc)[0] ?? '').toLowerCase() !== 'ok') {
115
+ db.close();
116
+ return { ...report, corrupt: true, skipped: 'memory DB reports corruption — run recoverMemoryDatabase first' };
117
+ }
118
+ // M0: incremental cursor table (per namespace, by monotonic rowid).
119
+ db.exec(`CREATE TABLE IF NOT EXISTS distill_state (
120
+ namespace TEXT PRIMARY KEY,
121
+ last_rowid INTEGER NOT NULL DEFAULT 0,
122
+ last_run_at INTEGER
123
+ )`);
124
+ // Which namespaces to process.
125
+ const nsAll = db.prepare("SELECT DISTINCT COALESCE(namespace,'default') AS ns FROM memory_entries WHERE embedding IS NOT NULL").all().map(r => r.ns);
126
+ const nsList = namespaces && namespaces.length ? nsAll.filter(n => namespaces.includes(n)) : nsAll;
127
+ report.namespaces = nsList;
128
+ const getCursor = db.prepare('SELECT last_rowid FROM distill_state WHERE namespace = ?');
129
+ const setCursor = db.prepare('INSERT INTO distill_state (namespace, last_rowid, last_run_at) VALUES (?, ?, ?) ' +
130
+ 'ON CONFLICT(namespace) DO UPDATE SET last_rowid = excluded.last_rowid, last_run_at = excluded.last_run_at');
131
+ const insEpisode = db.prepare('INSERT INTO episodes (session_id, task, input, output, reward, success, tags, metadata) VALUES (?,?,?,?,?,?,?,?)');
132
+ const insPattern = db.prepare('INSERT INTO reasoning_patterns (task_type, approach, success_rate, uses, avg_reward, tags, metadata) VALUES (?,?,?,?,?,?,?)');
133
+ const insPatEmb = db.prepare('INSERT OR REPLACE INTO pattern_embeddings (pattern_id, embedding) VALUES (?, ?)');
134
+ const insEdge = db.prepare('INSERT INTO causal_edges (from_memory_id, from_memory_type, to_memory_id, to_memory_type, similarity, confidence, mechanism, metadata) VALUES (?,?,?,?,?,?,?,?)');
135
+ let remaining = typeof maxEntries === 'number' ? maxEntries : Infinity;
136
+ for (const ns of nsList) {
137
+ if (remaining <= 0)
138
+ break;
139
+ let cursor = typeof sinceRowid === 'number' ? sinceRowid : (getCursor.get(ns)?.last_rowid ?? 0);
140
+ // Process this namespace in bounded batches until drained or cap hit.
141
+ // eslint-disable-next-line no-constant-condition
142
+ while (true) {
143
+ if (remaining <= 0)
144
+ break;
145
+ const lim = Math.min(batchSize, remaining);
146
+ const rows = db.prepare("SELECT rowid AS rowid, id, COALESCE(namespace,'default') AS namespace, content, embedding " +
147
+ 'FROM memory_entries WHERE embedding IS NOT NULL AND COALESCE(namespace,' + "'default'" + ') = ? AND rowid > ? ' +
148
+ 'ORDER BY rowid LIMIT ?').all(ns, cursor, lim);
149
+ if (rows.length === 0)
150
+ break;
151
+ const src = rows.map(r => ({
152
+ rowid: r.rowid, id: r.id, namespace: r.namespace,
153
+ content: String(r.content ?? ''), embedding: parseEmbedding(r.embedding),
154
+ }));
155
+ // Greedy cosine clustering within the batch (bounded n → O(n²) is fine).
156
+ const clusters = [];
157
+ for (const row of src) {
158
+ let placed = false;
159
+ if (row.embedding) {
160
+ for (const cl of clusters) {
161
+ if (cl.rep.embedding && cosine(row.embedding, cl.rep.embedding) >= 1 - dedupDistance) {
162
+ cl.members.push(row);
163
+ if (ns === 'feedback' && judgeFeedback(row.content).success)
164
+ cl.successCount++;
165
+ placed = true;
166
+ break;
167
+ }
168
+ }
169
+ }
170
+ if (!placed) {
171
+ const success = ns === 'feedback' ? judgeFeedback(row.content).success : false;
172
+ clusters.push({
173
+ rep: row, members: [row], successCount: success ? 1 : 0,
174
+ provenance: ns === 'feedback' ? 'oracle:test-exec' : 'proxy:structural',
175
+ });
176
+ }
177
+ }
178
+ const maxRowid = Math.max(...src.map(r => r.rowid), cursor);
179
+ // Embedding-coverage invariant: a cluster is only written if it has an
180
+ // embeddable representative — reselect to any member with a valid
181
+ // (parseable) vector; skip the cluster if none (unretrievable anyway).
182
+ // Guarantees every reasoning_pattern has exactly one pattern_embedding.
183
+ const embVecOf = (cl) => cl.rep.embedding ?? cl.members.find(m => m.embedding)?.embedding ?? null;
184
+ if (!dryRun) {
185
+ const commit = db.transaction(() => {
186
+ let prevEpId = null;
187
+ for (const cl of clusters) {
188
+ const embVec = embVecOf(cl);
189
+ if (!embVec)
190
+ continue; // no embeddable member → skip (keeps 1:1)
191
+ const distilled = distillTrajectoryContent(cl.rep.content);
192
+ const taskType = distilled.labels[0] ?? ns;
193
+ const approach = distilled.summary || serialiseDistilled(distilled).slice(0, 200);
194
+ const uses = cl.members.length;
195
+ const successRate = cl.provenance === 'oracle:test-exec' ? cl.successCount / uses : 0;
196
+ const avgReward = successRate; // structural: reward == success fraction
197
+ const promoted = cl.provenance === 'oracle:test-exec'; // proxy NEVER promotes (ADR-171)
198
+ const provMeta = {
199
+ provenance: cl.provenance, provenance_tier: cl.provenance, promoted,
200
+ sourceIds: cl.members.map(m => m.id).slice(0, 25),
201
+ paths: distilled.paths.slice(0, 10), namespace: ns, distilledBy: 'structural',
202
+ };
203
+ const epInfo = insEpisode.run(`distill:${ns}`, approach, cl.rep.content.slice(0, 2000), '', avgReward, promoted && cl.successCount > 0 ? 1 : 0, JSON.stringify(distilled.labels), JSON.stringify(provMeta));
204
+ report.episodes++;
205
+ const patInfo = insPattern.run(taskType, approach, successRate, uses, avgReward, JSON.stringify(distilled.labels), JSON.stringify(provMeta));
206
+ const patternId = Number(patInfo.lastInsertRowid);
207
+ report.patterns++;
208
+ report.byProvenance[cl.provenance] = (report.byProvenance[cl.provenance] ?? 0) + 1;
209
+ if (promoted)
210
+ report.promoted++;
211
+ // pattern_embeddings: reuse the representative's existing 384-dim
212
+ // vector as a Float32 BLOB — $0, no re-embedding. Guaranteed 1:1.
213
+ insPatEmb.run(patternId, Buffer.from(Float32Array.from(embVec).buffer));
214
+ report.patternEmbeddings++;
215
+ // WEAK relational edge — NOT causal proof. Links to the actual
216
+ // previous episode (not epId-1, which wrongly assumed consecutive
217
+ // rowids). Explicitly typed co-occurrence / proxy-tier / non-
218
+ // promoted: may rank retrieval, must NOT justify autonomous action
219
+ // (ADR-174 edge contract).
220
+ const epId = Number(epInfo.lastInsertRowid);
221
+ if (prevEpId !== null) {
222
+ insEdge.run(prevEpId, 'episode', epId, 'episode', 0, 0.3, 'co-occurrence', JSON.stringify({
223
+ edge_type: 'cooccurrence',
224
+ provenance_tier: 'proxy:structural',
225
+ confidence: 0.3,
226
+ promoted: false,
227
+ namespace: ns,
228
+ note: 'weak co-occurrence; may rank retrieval, must not justify autonomous action',
229
+ }));
230
+ report.causalEdges++;
231
+ }
232
+ prevEpId = epId;
233
+ }
234
+ setCursor.run(ns, maxRowid, Date.now());
235
+ });
236
+ commit();
237
+ }
238
+ else {
239
+ // dry-run accounting only — mirror the embeddable-cluster skip.
240
+ for (const cl of clusters) {
241
+ if (!embVecOf(cl))
242
+ continue;
243
+ report.patterns++;
244
+ report.episodes++;
245
+ report.byProvenance[cl.provenance] = (report.byProvenance[cl.provenance] ?? 0) + 1;
246
+ if (cl.provenance === 'oracle:test-exec')
247
+ report.promoted++;
248
+ }
249
+ }
250
+ report.processed += rows.length;
251
+ remaining -= rows.length;
252
+ cursor = maxRowid;
253
+ if (rows.length < lim)
254
+ break; // namespace drained
255
+ }
256
+ }
257
+ db.close();
258
+ if (verbose) {
259
+ console.log(`distilled ${report.processed} entries → ${report.patterns} patterns ` +
260
+ `(${report.promoted} promoted), ${report.episodes} episodes, ${report.causalEdges} edges` +
261
+ (dryRun ? ' [dry-run]' : ''));
262
+ }
263
+ }
264
+ catch (e) {
265
+ try {
266
+ db?.close();
267
+ }
268
+ catch { /* */ }
269
+ return { ...report, skipped: `error: ${e?.message ?? e}` };
270
+ }
271
+ return report;
272
+ }
273
+ /** Where the memory DB lives, for daemon/CLI callers. */
274
+ export function defaultMemoryDbPath(cwd = process.cwd()) {
275
+ return path.join(cwd, '.swarm', 'memory.db');
276
+ }
277
+ //# sourceMappingURL=memory-distillation.js.map
@@ -6,12 +6,13 @@
6
6
  * - map: Codebase mapping (5 min interval)
7
7
  * - audit: Security analysis (10 min interval)
8
8
  * - optimize: Performance optimization (15 min interval)
9
- * - consolidate: Memory consolidation (30 min interval)
9
+ * - consolidate: Memory distillation memory_entries -> episodes/reasoning_patterns/
10
+ * causal_edges (30 min interval, ADR-174; RUFLO_DAEMON_NO_DISTILL=1 / --no-distill to skip)
10
11
  * - testgaps: Test coverage analysis (20 min interval)
11
12
  */
12
13
  import { EventEmitter } from 'events';
13
14
  import { HeadlessWorkerExecutor } from './headless-worker-executor.js';
14
- export type WorkerType = 'ultralearn' | 'optimize' | 'consolidate' | 'predict' | 'audit' | 'map' | 'preload' | 'deepdive' | 'document' | 'refactor' | 'benchmark' | 'testgaps';
15
+ export type WorkerType = 'ultralearn' | 'optimize' | 'consolidate' | 'predict' | 'audit' | 'map' | 'preload' | 'deepdive' | 'document' | 'refactor' | 'benchmark' | 'testgaps' | 'backup';
15
16
  interface WorkerConfig {
16
17
  type: WorkerType;
17
18
  intervalMs: number;
@@ -297,7 +298,35 @@ export declare class WorkerDaemon extends EventEmitter {
297
298
  * Local optimize worker (fallback when headless unavailable)
298
299
  */
299
300
  private runOptimizeWorkerLocal;
301
+ /**
302
+ * ADR-174 M3: memory consolidation — runs the real DISTILL/CONSOLIDATE pass
303
+ * (memory-distillation.ts) against `.swarm/memory.db`, turning raw
304
+ * `memory_entries` into `episodes` / `reasoning_patterns` /
305
+ * `pattern_embeddings` / `causal_edges`. Previously this wrote a hardcoded
306
+ * `{ patternsConsolidated: 0 }` stub and touched no database — the root
307
+ * cause of the intelligence tables staying empty.
308
+ *
309
+ * Kept as `runConsolidateWorker` / worker type `'consolidate'` for
310
+ * back-compat with existing `-w consolidate` scripts and docs.
311
+ *
312
+ * Safety:
313
+ * - Bounded via CONSOLIDATE_MAX_ENTRIES_PER_TICK so a large backlog can
314
+ * never approach DEFAULT_WORKER_TIMEOUT_MS — the incremental cursor in
315
+ * runDistillation() drains a bounded slice per tick and picks up where
316
+ * it left off on the next scheduled run.
317
+ * - runDistillation() never throws (it catches internally and returns
318
+ * `{ skipped }` / `{ corrupt: true }`), but this worker still wraps the
319
+ * call defensively — a background worker must never crash the daemon.
320
+ */
300
321
  private runConsolidateWorker;
322
+ /**
323
+ * Nightly memory-DB backup worker (24h interval). Takes a WAL-safe, consistent
324
+ * snapshot of .swarm/memory.db with rotation (keep last N). Never throws — a
325
+ * worker must not crash the daemon; a skip/error is written to the metrics
326
+ * file. Opt-out by omitting `backup` from `-w`; offsite GCS is opt-in via
327
+ * RUFLO_BACKUP_GCS (a gs:// prefix), retention via RUFLO_BACKUP_KEEP.
328
+ */
329
+ private runBackupWorker;
301
330
  /**
302
331
  * Local testgaps worker (fallback when headless unavailable)
303
332
  */