claude-flow 3.21.0 → 3.22.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.
- package/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +23 -2
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +56 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory-distill.d.ts +27 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory-distill.js +374 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +4 -2
- package/v3/@claude-flow/cli/dist/src/index.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/index.js +22 -1
- package/v3/@claude-flow/cli/dist/src/init/executor.js +37 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +19 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +175 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-signing.d.ts +30 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-signing.js +60 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.d.ts +16 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +89 -8
- package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.d.ts +53 -0
- package/v3/@claude-flow/cli/dist/src/init/memory-package-resolver.js +118 -0
- package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +18 -7
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +13 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agenticow-speculate-tools.js +9 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.d.ts +162 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +548 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.d.ts +9 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +4 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +115 -54
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +71 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +330 -2
- package/v3/@claude-flow/cli/dist/src/services/distill-tuning.d.ts +111 -0
- package/v3/@claude-flow/cli/dist/src/services/distill-tuning.js +510 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-distillation.d.ts +41 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-distillation.js +277 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +22 -1
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +117 -6
- package/v3/@claude-flow/cli/package.json +5 -2
|
@@ -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,7 +6,8 @@
|
|
|
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
|
|
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';
|
|
@@ -297,6 +298,26 @@ 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;
|
|
301
322
|
/**
|
|
302
323
|
* Local testgaps worker (fallback when headless unavailable)
|
|
@@ -6,7 +6,8 @@
|
|
|
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
|
|
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';
|
|
@@ -14,12 +15,18 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, unl
|
|
|
14
15
|
import { cpus } from 'os';
|
|
15
16
|
import { join } from 'path';
|
|
16
17
|
import { HeadlessWorkerExecutor, isHeadlessWorker, } from './headless-worker-executor.js';
|
|
18
|
+
// ADR-174 M3: the consolidate worker below drives the real DISTILL/CONSOLIDATE
|
|
19
|
+
// pass instead of writing a hardcoded { patternsConsolidated: 0 } stub. The
|
|
20
|
+
// service itself is owned/frozen elsewhere (memory-distillation.ts) — it is
|
|
21
|
+
// incremental (rowid cursor), non-destructive, transactional, and
|
|
22
|
+
// quick_check-gated, so it's safe to call unconditionally on every tick.
|
|
23
|
+
import { runDistillation, defaultMemoryDbPath } from './memory-distillation.js';
|
|
17
24
|
// Default worker configurations with improved intervals (P0 fix: map 5min -> 15min)
|
|
18
25
|
const DEFAULT_WORKERS = [
|
|
19
26
|
{ type: 'map', intervalMs: 15 * 60 * 1000, offsetMs: 0, priority: 'normal', description: 'Codebase mapping', enabled: true },
|
|
20
27
|
{ type: 'audit', intervalMs: 10 * 60 * 1000, offsetMs: 2 * 60 * 1000, priority: 'critical', description: 'Security analysis', enabled: true },
|
|
21
28
|
{ type: 'optimize', intervalMs: 15 * 60 * 1000, offsetMs: 4 * 60 * 1000, priority: 'high', description: 'Performance optimization', enabled: true },
|
|
22
|
-
{ type: 'consolidate', intervalMs: 30 * 60 * 1000, offsetMs: 6 * 60 * 1000, priority: 'low', description: 'Memory
|
|
29
|
+
{ type: 'consolidate', intervalMs: 30 * 60 * 1000, offsetMs: 6 * 60 * 1000, priority: 'low', description: 'Memory distillation (ADR-174)', enabled: true },
|
|
23
30
|
{ type: 'testgaps', intervalMs: 20 * 60 * 1000, offsetMs: 8 * 60 * 1000, priority: 'normal', description: 'Test coverage analysis', enabled: true },
|
|
24
31
|
{ type: 'predict', intervalMs: 10 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Predictive preloading', enabled: false },
|
|
25
32
|
{ type: 'document', intervalMs: 60 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Auto-documentation', enabled: false },
|
|
@@ -27,6 +34,27 @@ const DEFAULT_WORKERS = [
|
|
|
27
34
|
// Worker timeout — must exceed the longest per-worker headless timeout (15 min for audit/refactor).
|
|
28
35
|
// Previously 5 min, which caused orphan processes when daemon timeout fired before executor timeout (#1117).
|
|
29
36
|
const DEFAULT_WORKER_TIMEOUT_MS = 16 * 60 * 1000;
|
|
37
|
+
// ADR-174 M3: hard cap on rows the `consolidate` worker distills in a single
|
|
38
|
+
// tick. The distillation service is cursor-driven (per-namespace rowid) and
|
|
39
|
+
// batches in transactions of `batchSize`, so a capped `maxEntries` guarantees
|
|
40
|
+
// this worker returns well within DEFAULT_WORKER_TIMEOUT_MS even against a
|
|
41
|
+
// large backlog — the cursor just picks up where it left off next tick
|
|
42
|
+
// (every 30 min per DEFAULT_WORKERS), draining an arbitrarily large backlog
|
|
43
|
+
// over several ticks instead of blocking on one.
|
|
44
|
+
const CONSOLIDATE_MAX_ENTRIES_PER_TICK = 1000;
|
|
45
|
+
const CONSOLIDATE_BATCH_SIZE = 200;
|
|
46
|
+
// ADR-174 M5: platform-default distillation config, chosen by the M4 self-
|
|
47
|
+
// optimization grid-search (scripts/tune-distill.mjs) on the real ~7.9k-entry
|
|
48
|
+
// corpus. Winner: batchSize=200, dedupDistance=0.2 (held-out MRR@10 0.753 vs
|
|
49
|
+
// 0.749 baseline — measured on-par, not a large uplift; the payoff is the
|
|
50
|
+
// populated substrate + trainable model). Override per-run via `memory distill`.
|
|
51
|
+
const CONSOLIDATE_DEDUP_DISTANCE = 0.2;
|
|
52
|
+
// ADR-174 M3 opt-out: set to skip the real distillation pass entirely (e.g.
|
|
53
|
+
// constrained CI hosts, or a user who wants the daemon's other workers but
|
|
54
|
+
// not this one without touching persisted worker-enabled state). Mirrors the
|
|
55
|
+
// `--no-distill` flag on `daemon start` (see commands/daemon.ts), which sets
|
|
56
|
+
// this env var on the forked/foreground daemon process.
|
|
57
|
+
const NO_DISTILL_ENV = 'RUFLO_DAEMON_NO_DISTILL';
|
|
30
58
|
// #2356 — Self-terminating lifecycle defaults. A background daemon with no
|
|
31
59
|
// upper bound on its lifetime runs until the box reboots; in the field this
|
|
32
60
|
// leaked tens of thousands of headless `claude --print` sweeps over many days
|
|
@@ -1268,18 +1296,101 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
1268
1296
|
writeFileSync(optimizeFile, JSON.stringify(perf, null, 2));
|
|
1269
1297
|
return perf;
|
|
1270
1298
|
}
|
|
1299
|
+
/**
|
|
1300
|
+
* ADR-174 M3: memory consolidation — runs the real DISTILL/CONSOLIDATE pass
|
|
1301
|
+
* (memory-distillation.ts) against `.swarm/memory.db`, turning raw
|
|
1302
|
+
* `memory_entries` into `episodes` / `reasoning_patterns` /
|
|
1303
|
+
* `pattern_embeddings` / `causal_edges`. Previously this wrote a hardcoded
|
|
1304
|
+
* `{ patternsConsolidated: 0 }` stub and touched no database — the root
|
|
1305
|
+
* cause of the intelligence tables staying empty.
|
|
1306
|
+
*
|
|
1307
|
+
* Kept as `runConsolidateWorker` / worker type `'consolidate'` for
|
|
1308
|
+
* back-compat with existing `-w consolidate` scripts and docs.
|
|
1309
|
+
*
|
|
1310
|
+
* Safety:
|
|
1311
|
+
* - Bounded via CONSOLIDATE_MAX_ENTRIES_PER_TICK so a large backlog can
|
|
1312
|
+
* never approach DEFAULT_WORKER_TIMEOUT_MS — the incremental cursor in
|
|
1313
|
+
* runDistillation() drains a bounded slice per tick and picks up where
|
|
1314
|
+
* it left off on the next scheduled run.
|
|
1315
|
+
* - runDistillation() never throws (it catches internally and returns
|
|
1316
|
+
* `{ skipped }` / `{ corrupt: true }`), but this worker still wraps the
|
|
1317
|
+
* call defensively — a background worker must never crash the daemon.
|
|
1318
|
+
*/
|
|
1271
1319
|
async runConsolidateWorker() {
|
|
1272
|
-
// Memory consolidation - clean up old patterns
|
|
1273
1320
|
const consolidateFile = join(this.projectRoot, '.claude-flow', 'metrics', 'consolidation.json');
|
|
1274
1321
|
const metricsDir = join(this.projectRoot, '.claude-flow', 'metrics');
|
|
1275
1322
|
if (!existsSync(metricsDir)) {
|
|
1276
1323
|
mkdirSync(metricsDir, { recursive: true });
|
|
1277
1324
|
}
|
|
1325
|
+
// Opt-out: RUFLO_DAEMON_NO_DISTILL=1 (or `daemon start --no-distill`)
|
|
1326
|
+
// skips the real distillation pass entirely without touching persisted
|
|
1327
|
+
// worker-enabled state (see also: `daemon enable -w consolidate --disable`,
|
|
1328
|
+
// which disables the worker's schedule altogether).
|
|
1329
|
+
if (process.env[NO_DISTILL_ENV] === '1') {
|
|
1330
|
+
const disabledResult = {
|
|
1331
|
+
timestamp: new Date().toISOString(),
|
|
1332
|
+
distillationEnabled: false,
|
|
1333
|
+
note: `Distillation disabled via ${NO_DISTILL_ENV}=1 / --no-distill`,
|
|
1334
|
+
patternsConsolidated: 0,
|
|
1335
|
+
memoryCleaned: 0,
|
|
1336
|
+
duplicatesRemoved: 0,
|
|
1337
|
+
};
|
|
1338
|
+
writeFileSync(consolidateFile, JSON.stringify(disabledResult, null, 2));
|
|
1339
|
+
return disabledResult;
|
|
1340
|
+
}
|
|
1341
|
+
let report;
|
|
1342
|
+
try {
|
|
1343
|
+
report = await runDistillation({
|
|
1344
|
+
dbPath: defaultMemoryDbPath(this.projectRoot),
|
|
1345
|
+
maxEntries: CONSOLIDATE_MAX_ENTRIES_PER_TICK,
|
|
1346
|
+
batchSize: CONSOLIDATE_BATCH_SIZE,
|
|
1347
|
+
dedupDistance: CONSOLIDATE_DEDUP_DISTANCE,
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
catch (error) {
|
|
1351
|
+
// Defensive only — runDistillation() is internally try/catch'd and
|
|
1352
|
+
// should never reach here. A worker must never crash the daemon.
|
|
1353
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1354
|
+
this.log('warn', `Consolidate worker: distillation threw unexpectedly: ${message}`);
|
|
1355
|
+
const errorResult = {
|
|
1356
|
+
timestamp: new Date().toISOString(),
|
|
1357
|
+
distillationEnabled: true,
|
|
1358
|
+
error: message,
|
|
1359
|
+
patternsConsolidated: 0,
|
|
1360
|
+
memoryCleaned: 0,
|
|
1361
|
+
duplicatesRemoved: 0,
|
|
1362
|
+
};
|
|
1363
|
+
writeFileSync(consolidateFile, JSON.stringify(errorResult, null, 2));
|
|
1364
|
+
return errorResult;
|
|
1365
|
+
}
|
|
1366
|
+
if (report.corrupt) {
|
|
1367
|
+
this.log('warn', `Consolidate worker: memory DB reports corruption — ${report.skipped ?? 'skipped'}`);
|
|
1368
|
+
}
|
|
1369
|
+
else if (report.skipped) {
|
|
1370
|
+
this.log('info', `Consolidate worker: distillation skipped (${report.skipped})`);
|
|
1371
|
+
}
|
|
1278
1372
|
const result = {
|
|
1279
1373
|
timestamp: new Date().toISOString(),
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1374
|
+
distillationEnabled: true,
|
|
1375
|
+
// Mapping onto the pre-existing metrics shape (ADR-174 M3):
|
|
1376
|
+
patternsConsolidated: report.patterns,
|
|
1377
|
+
// rows drained from the incremental cursor this tick — distillation is
|
|
1378
|
+
// non-destructive (never mutates/deletes memory_entries), so "cleaned"
|
|
1379
|
+
// here means "processed into the intelligence tables", not removed.
|
|
1380
|
+
memoryCleaned: report.processed,
|
|
1381
|
+
// clustering collapses near-duplicate entries into a single pattern
|
|
1382
|
+
// rather than deleting them; this is the count that got merged away
|
|
1383
|
+
// instead of becoming their own distinct pattern.
|
|
1384
|
+
duplicatesRemoved: Math.max(0, report.processed - report.patterns),
|
|
1385
|
+
episodes: report.episodes,
|
|
1386
|
+
patternEmbeddings: report.patternEmbeddings,
|
|
1387
|
+
causalEdges: report.causalEdges,
|
|
1388
|
+
promoted: report.promoted,
|
|
1389
|
+
byProvenance: report.byProvenance,
|
|
1390
|
+
namespaces: report.namespaces,
|
|
1391
|
+
dryRun: report.dryRun,
|
|
1392
|
+
corrupt: report.corrupt ?? false,
|
|
1393
|
+
skipped: report.skipped,
|
|
1283
1394
|
};
|
|
1284
1395
|
writeFileSync(consolidateFile, JSON.stringify(result, null, 2));
|
|
1285
1396
|
return result;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -75,8 +75,10 @@
|
|
|
75
75
|
},
|
|
76
76
|
"files": [
|
|
77
77
|
"dist",
|
|
78
|
+
"!dist/**/*.map",
|
|
79
|
+
"!dist/**/*.tsbuildinfo",
|
|
78
80
|
"bin",
|
|
79
|
-
"scripts",
|
|
81
|
+
"scripts/postinstall.cjs",
|
|
80
82
|
".claude",
|
|
81
83
|
"plugins",
|
|
82
84
|
"README.md"
|
|
@@ -132,6 +134,7 @@
|
|
|
132
134
|
"agentic-flow": "^3.0.0-alpha.1",
|
|
133
135
|
"agenticow": "~0.2.4",
|
|
134
136
|
"metaharness": "~0.2.8",
|
|
137
|
+
"page-agent": "~1.11.0",
|
|
135
138
|
"ruvector": "^0.2.27"
|
|
136
139
|
},
|
|
137
140
|
"publishConfig": {
|