claude-flow 3.32.8 → 3.32.9
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/helpers/statusline.cjs +55 -1
- package/.claude-plugin/hooks/hooks.json +3 -1
- package/package.json +9 -3
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +267 -20
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +43 -1
- package/v3/@claude-flow/cli/dist/src/funnel/message-transport.d.ts +11 -4
- package/v3/@claude-flow/cli/dist/src/funnel/message-transport.js +11 -4
- package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.d.ts +9 -0
- package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js +13 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +32 -5
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +72 -0
- package/v3/@claude-flow/cli/package.json +4 -2
|
@@ -633,12 +633,55 @@ function compareVersions(a, b) {
|
|
|
633
633
|
return 0;
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
+
// #2742: when CWD is a linked git worktree, it has no node_modules of its
|
|
637
|
+
// own (worktrees don't get their own `npm install`), so every CWD-relative
|
|
638
|
+
// probe in getPkgVersion() misses and the version silently falls back to
|
|
639
|
+
// the baked-in default — even though the main repo's install a few
|
|
640
|
+
// directories away is perfectly resolvable. A linked worktree's `.git` is
|
|
641
|
+
// a plain FILE (not a directory) containing `gitdir: <main>/.git/worktrees/
|
|
642
|
+
// <name>`; walk up from CWD to find it, parse the pointer, and strip the
|
|
643
|
+
// trailing `.git/worktrees/<name>` segment to recover the main repo root.
|
|
644
|
+
// Pure fs — no `git rev-parse` spawn (statusline renders are latency-
|
|
645
|
+
// sensitive; this doc comment's neighbors are explicit about avoiding
|
|
646
|
+
// spawns in the render path).
|
|
647
|
+
function resolveWorktreeMainRoot() {
|
|
648
|
+
try {
|
|
649
|
+
let dir = CWD;
|
|
650
|
+
for (;;) {
|
|
651
|
+
const dotGit = path.join(dir, '.git');
|
|
652
|
+
if (fs.existsSync(dotGit)) {
|
|
653
|
+
if (fs.statSync(dotGit).isFile()) {
|
|
654
|
+
const contents = fs.readFileSync(dotGit, 'utf-8');
|
|
655
|
+
const m = contents.match(/^gitdir:\s*(.+)$/m);
|
|
656
|
+
const wtGitDir = m && m[1].trim();
|
|
657
|
+
if (wtGitDir) {
|
|
658
|
+
// Git writes this pointer with forward slashes even on Windows
|
|
659
|
+
// (a git-for-windows convention for its own internal files) —
|
|
660
|
+
// path.sep (backslash on win32) never matches, so normalize
|
|
661
|
+
// before searching rather than building an OS-specific marker.
|
|
662
|
+
const normalized = wtGitDir.replace(/\\/g, '/');
|
|
663
|
+
const marker = '/.git/worktrees/';
|
|
664
|
+
const idx = normalized.lastIndexOf(marker);
|
|
665
|
+
if (idx > 0) return normalized.slice(0, idx);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return null; // a real (non-worktree) .git dir — nothing to resolve
|
|
669
|
+
}
|
|
670
|
+
const parent = path.dirname(dir);
|
|
671
|
+
if (parent === dir) return null; // reached filesystem root
|
|
672
|
+
dir = parent;
|
|
673
|
+
}
|
|
674
|
+
} catch {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
636
679
|
function getPkgVersion() {
|
|
637
680
|
// Baked in at generation time from the real running CLI's own resolved
|
|
638
681
|
// version (see generateStatuslineScript()'s doc comment) — correct even
|
|
639
682
|
// when this renders via a pure npx invocation with no local install for
|
|
640
683
|
// the candidate scan below to find.
|
|
641
|
-
let ver = "3.
|
|
684
|
+
let ver = "3.32.8";
|
|
642
685
|
try {
|
|
643
686
|
const home = os.homedir();
|
|
644
687
|
const pkgPaths = [
|
|
@@ -647,6 +690,17 @@ function getPkgVersion() {
|
|
|
647
690
|
path.join(CWD, 'node_modules', 'ruflo', 'package.json'),
|
|
648
691
|
path.join(CWD, 'v3', '@claude-flow', 'cli', 'package.json'),
|
|
649
692
|
];
|
|
693
|
+
// #2742: CWD is a linked git worktree with no node_modules of its own —
|
|
694
|
+
// probe the main repo's install too, so a worktree session shows the
|
|
695
|
+
// same version a main-repo session would.
|
|
696
|
+
const worktreeMainRoot = resolveWorktreeMainRoot();
|
|
697
|
+
if (worktreeMainRoot) {
|
|
698
|
+
pkgPaths.push(
|
|
699
|
+
path.join(worktreeMainRoot, 'node_modules', '@claude-flow', 'cli', 'package.json'),
|
|
700
|
+
path.join(worktreeMainRoot, 'node_modules', 'ruflo', 'package.json'),
|
|
701
|
+
path.join(worktreeMainRoot, 'v3', '@claude-flow', 'cli', 'package.json'),
|
|
702
|
+
);
|
|
703
|
+
}
|
|
650
704
|
// #2221: global installs (npm i -g ruflo) live outside CWD/node_modules, so the
|
|
651
705
|
// probes above all miss and the version falls back to the hard-coded default.
|
|
652
706
|
// Derive the global node_modules dir from the running node binary (no npm spawn —
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "#1921 — hook commands invoke scripts/ruflo-hook.sh (resilient shim): prefers a locally-installed `ruflo`/`claude-flow` binary, falls back to `npx --prefer-offline`, and always exits 0 so a CLI/install failure (e.g. arborist `Invalid Version` on npm 10.8.x) never surfaces an error in Claude Code or blocks a turn. The trailing `|| true` guards the case where $CLAUDE_PLUGIN_ROOT is unset (older Claude Code) — the hook then no-ops silently. DO NOT revert to a bare `npx <pkg>@alpha hooks …` per fire.",
|
|
3
3
|
"_platform": "posix",
|
|
4
|
-
"_platform_note": "#2132 — This hooks.json uses /bin/bash, POSIX pipelines (jq, xargs, tr), and .sh scripts. It is intentionally POSIX-only (Mac/Linux)
|
|
4
|
+
"_platform_note": "#2132 — This hooks.json uses /bin/bash, POSIX pipelines (jq, xargs, tr), and .sh scripts. It is intentionally POSIX-only (Mac/Linux) today and known-broken on native Windows (#2721 shape). The previous claim here that `ruflo init` overrides these entries with node-based equivalents on Windows was never actually implemented and is incorrect — Claude Code merges plugin-declared hooks additively with any init-generated settings.json, it does not replace them. This package (separate from plugins/ruflo-core, which got the #2721 fix) still needs its own Windows-compatible rewrite; see _legacy_unaudited_shim below.",
|
|
5
|
+
"_legacy_unaudited_shim": true,
|
|
6
|
+
"_legacy_unaudited_shim_note": "#2721 fixed plugins/ruflo-core and plugins/ruflo-cost-tracker (marketplace-listed in .claude-plugin/marketplace.json) by rewiring hooks.json to a `node -e` bootstrap around scripts/ruflo-hook.cjs. This older, separately-published \"claude-flow\" plugin package (not in the ruflo marketplace list) has its own, larger jq/xargs-based hook set and its own scripts/ruflo-hook.cjs that hooks.json never references — same underlying bug shape, NOT fixed as part of #2721. Needs its own audited pass before this flag can be removed.",
|
|
5
7
|
"hooks": {
|
|
6
8
|
"PreToolUse": [
|
|
7
9
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.32.
|
|
3
|
+
"version": "3.32.9",
|
|
4
4
|
"description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -78,11 +78,12 @@
|
|
|
78
78
|
"@ruvector/router-linux-x64-gnu": "^0.1.30",
|
|
79
79
|
"@ruvector/sona": "^0.1.5",
|
|
80
80
|
"agentdb": "^3.0.0-alpha.17",
|
|
81
|
-
"agentic-flow": "^2.0.14"
|
|
81
|
+
"agentic-flow": "^2.0.14",
|
|
82
|
+
"better-sqlite3": "12.9.0"
|
|
82
83
|
},
|
|
83
84
|
"overrides": {
|
|
84
85
|
"ruvector": "^0.2.27",
|
|
85
|
-
"better-sqlite3": "
|
|
86
|
+
"better-sqlite3": "12.9.0",
|
|
86
87
|
"js-yaml": ">=4.3.0",
|
|
87
88
|
"hono": ">=4.12.25",
|
|
88
89
|
"@ruvector/rvf-wasm": "0.1.5",
|
|
@@ -131,6 +132,11 @@
|
|
|
131
132
|
"typescript": "^5.0.0",
|
|
132
133
|
"vitest": "^3.2.6"
|
|
133
134
|
},
|
|
135
|
+
"pnpm": {
|
|
136
|
+
"overrides": {
|
|
137
|
+
"better-sqlite3": "12.9.0"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
134
140
|
"engines": {
|
|
135
141
|
"node": ">=20.0.0"
|
|
136
142
|
},
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Created with ruv.io
|
|
6
6
|
*/
|
|
7
7
|
import { output } from '../output.js';
|
|
8
|
-
import { existsSync, readFileSync, statSync } from 'fs';
|
|
8
|
+
import { existsSync, readFileSync, statSync, openSync, readSync, closeSync } from 'fs';
|
|
9
9
|
import { join, dirname } from 'path';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { createHash } from 'crypto';
|
|
@@ -216,6 +216,16 @@ async function checkDaemonStatus() {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
// Check memory database
|
|
219
|
+
//
|
|
220
|
+
// #2737: renamed the reported row from "Memory Database" to "Memory Database
|
|
221
|
+
// Presence" — this check is existsSync()+statSync() ONLY, it never opens the
|
|
222
|
+
// file or queries it, so it PASSES for any file that exists and can be
|
|
223
|
+
// stat'd, corrupt or not. It was the ONLY memory probe in the default
|
|
224
|
+
// `allChecks` run (the real integrity checks were --component memory only),
|
|
225
|
+
// so a corrupt .swarm/memory.db could sail through a bare `doctor` run as
|
|
226
|
+
// "healthy". The name change makes the check's actual scope honest; the
|
|
227
|
+
// behavior below is unchanged. See checkMemoryStructuralIntegrity below for
|
|
228
|
+
// the new default-run check that actually opens the file.
|
|
219
229
|
async function checkMemoryDatabase() {
|
|
220
230
|
// Authoritative path comes from `getMemoryRoot()` (honors
|
|
221
231
|
// `CLAUDE_FLOW_MEMORY_PATH`, claude-flow.config.json's `memory.persistPath`,
|
|
@@ -238,14 +248,14 @@ async function checkMemoryDatabase() {
|
|
|
238
248
|
try {
|
|
239
249
|
const stats = statSync(dbPath);
|
|
240
250
|
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
241
|
-
return { name: 'Memory Database', status: 'pass', message: `${dbPath} (${sizeMB} MB)` };
|
|
251
|
+
return { name: 'Memory Database Presence', status: 'pass', message: `${dbPath} (${sizeMB} MB) — existence/size only, not a health check (see Memory Structural Integrity)` };
|
|
242
252
|
}
|
|
243
253
|
catch {
|
|
244
|
-
return { name: 'Memory Database', status: 'warn', message: `${dbPath} (unable to stat)` };
|
|
254
|
+
return { name: 'Memory Database Presence', status: 'warn', message: `${dbPath} (unable to stat)` };
|
|
245
255
|
}
|
|
246
256
|
}
|
|
247
257
|
}
|
|
248
|
-
return { name: 'Memory Database', status: 'warn', message: 'Not initialized', fix: 'claude-flow memory configure --backend hybrid' };
|
|
258
|
+
return { name: 'Memory Database Presence', status: 'warn', message: 'Not initialized', fix: 'claude-flow memory configure --backend hybrid' };
|
|
249
259
|
}
|
|
250
260
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
251
261
|
// #2677 — memory doctor: functional checks (stuinfla)
|
|
@@ -305,46 +315,282 @@ async function tryOpenSqlJs(dbPath) {
|
|
|
305
315
|
return null;
|
|
306
316
|
}
|
|
307
317
|
}
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
318
|
+
// ── #2737 shared helpers: encryption-at-rest carve-out ─────────────────────
|
|
319
|
+
//
|
|
320
|
+
// "file is not a database" / "malformed" from sql.js or better-sqlite3 is
|
|
321
|
+
// EXACTLY the signal a legitimately RFE1-encrypted-at-rest memory.db also
|
|
322
|
+
// produces when opened without decrypting (ADR-096). Before either the new
|
|
323
|
+
// default-run check or the strengthened checkMemoryIntegrity below concludes
|
|
324
|
+
// "malformed = fail", they sniff for the RFE1 magic using the SAME detector
|
|
325
|
+
// checkEncryptionAtRest uses (isEncryptedBlob), so the two checks can never
|
|
326
|
+
// disagree about what counts as "encrypted".
|
|
327
|
+
/** Read just the first `len` bytes of a file without loading the whole file
|
|
328
|
+
* into memory — a multi-GB memory.db shouldn't get fully buffered just to
|
|
329
|
+
* check a 4-byte magic. Returns fewer bytes (or empty) when the file is
|
|
330
|
+
* shorter than `len`; isEncryptedBlob() correctly treats a too-short blob
|
|
331
|
+
* as "not encrypted", so callers don't need to special-case that. */
|
|
332
|
+
function readHeaderBytes(path, len) {
|
|
333
|
+
const fd = openSync(path, 'r');
|
|
334
|
+
try {
|
|
335
|
+
const buf = Buffer.alloc(len);
|
|
336
|
+
const bytesRead = readSync(fd, buf, 0, len, 0);
|
|
337
|
+
return buf.subarray(0, bytesRead);
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
closeSync(fd);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// RFE1 wire format is magic(4) + iv(12) + ciphertext(N) + tag(16); the
|
|
344
|
+
// shortest possible blob is 32 bytes. 64 gives headroom without importing
|
|
345
|
+
// vault.ts's private MIN_BLOB_LEN constant.
|
|
346
|
+
const ENCRYPTION_SNIFF_LEN = 64;
|
|
347
|
+
/** True iff `dbPath` is a legitimately RFE1-encrypted-at-rest file. A read
|
|
348
|
+
* failure (permissions, races) is treated as "not encrypted" — callers
|
|
349
|
+
* still hit their own open/query error handling right after, so nothing
|
|
350
|
+
* gets silently swallowed. */
|
|
351
|
+
function isMemoryDbEncryptedAtRest(dbPath) {
|
|
352
|
+
try {
|
|
353
|
+
return isEncryptedBlob(readHeaderBytes(dbPath, ENCRYPTION_SNIFF_LEN));
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// #2737 part 1 — bare `doctor` (no --component flag) never actually opened
|
|
360
|
+
// memory.db: checkMemoryDatabase above is existsSync()+statSync() only, so
|
|
361
|
+
// it PASSES on any file that exists and can be stat'd, corrupt or not, and
|
|
362
|
+
// it was the ONLY memory probe `allChecks` ran. The real integrity checks
|
|
363
|
+
// (checkMemoryIntegrity/Content/EmbeddingCoverage below) were, and remain,
|
|
364
|
+
// --component memory only.
|
|
365
|
+
//
|
|
366
|
+
// This is a NEW, cheaper, native check added to the default `allChecks` run
|
|
367
|
+
// (not a replacement for the deep trio):
|
|
368
|
+
// - better-sqlite3 (native) instead of sql.js: it's WAL-aware because
|
|
369
|
+
// SQLite itself resolves any sibling -wal file next to the main image;
|
|
370
|
+
// sql.js is WAL-blind — tryOpenSqlJs() above only readFileSync()s the
|
|
371
|
+
// main file and hands the raw bytes to the WASM build, so any
|
|
372
|
+
// committed-but-not-checkpointed rows sitting in a -wal file are
|
|
373
|
+
// invisible to it.
|
|
374
|
+
// - PRAGMA quick_check, not integrity_check: per sqlite.org, quick_check
|
|
375
|
+
// "is like integrity_check except that it does not verify UNIQUE
|
|
376
|
+
// constraints and does not verify that index content matches table
|
|
377
|
+
// content" — bounded enough to run unconditionally on every bare
|
|
378
|
+
// `doctor` call. The full integrity_check (with those extra
|
|
379
|
+
// cross-checks) is what the strengthened checkMemoryIntegrity below
|
|
380
|
+
// runs when better-sqlite3 is available.
|
|
381
|
+
// Explicitly labeled "structural-only" in both the row name and every
|
|
382
|
+
// message so nobody mistakes this cheap default-run probe for the deeper
|
|
383
|
+
// --component memory one.
|
|
384
|
+
async function checkMemoryStructuralIntegrity() {
|
|
385
|
+
const NAME = 'Memory Structural Integrity (quick_check)';
|
|
386
|
+
const dbPath = await resolveMemoryDbPath();
|
|
387
|
+
if (!dbPath) {
|
|
388
|
+
// Not-yet-initialized project — Memory Database Presence above already
|
|
389
|
+
// surfaces this; "UNKNOWN is never PASS" still applies, so this stays a
|
|
390
|
+
// warn rather than a silent skip, but doesn't pile on a second red for
|
|
391
|
+
// the same root cause.
|
|
392
|
+
return {
|
|
393
|
+
name: NAME,
|
|
394
|
+
status: 'warn',
|
|
395
|
+
message: 'no memory.db found (see Memory Database Presence above) — structural-only check skipped',
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
// Encryption-at-rest carve-out (#2737 part 3) — see helpers above.
|
|
399
|
+
if (isMemoryDbEncryptedAtRest(dbPath)) {
|
|
400
|
+
return {
|
|
401
|
+
name: NAME,
|
|
402
|
+
status: 'warn',
|
|
403
|
+
message: `${dbPath} — RFE1-encrypted at rest; structural-only check can't run without decrypting (expected, not corruption — see Encryption at Rest)`,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
let Database;
|
|
407
|
+
try {
|
|
408
|
+
Database = (await import('better-sqlite3')).default;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return {
|
|
412
|
+
name: NAME,
|
|
413
|
+
status: 'warn',
|
|
414
|
+
message: `${dbPath} — better-sqlite3 not installed; structural-only check skipped (optional native module)`,
|
|
415
|
+
fix: 'npm install better-sqlite3 (enables WAL-aware structural checks on every `doctor` run)',
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
let db;
|
|
419
|
+
try {
|
|
420
|
+
db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
const msg = e.message || String(e);
|
|
424
|
+
// Encryption already ruled out above — an unencrypted file
|
|
425
|
+
// better-sqlite3 can't even open is definitive corruption.
|
|
426
|
+
// #2737 part 3: fail, not warn.
|
|
427
|
+
return {
|
|
428
|
+
name: NAME,
|
|
429
|
+
status: 'fail',
|
|
430
|
+
message: `${dbPath} — better-sqlite3 failed to open: ${msg} (unencrypted; definitively malformed) [structural-only]`,
|
|
431
|
+
fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
const rows = db.pragma('quick_check');
|
|
436
|
+
const values = rows.map((r) => String(Object.values(r)[0]));
|
|
437
|
+
if (values.length === 1 && values[0] === 'ok') {
|
|
438
|
+
return {
|
|
439
|
+
name: NAME,
|
|
440
|
+
status: 'pass',
|
|
441
|
+
message: `${dbPath} — PRAGMA quick_check: ok [structural-only, native + WAL-aware]`,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
name: NAME,
|
|
446
|
+
status: 'fail',
|
|
447
|
+
message: `${dbPath} — PRAGMA quick_check: ${values.slice(0, 3).join('; ')}${values.length > 3 ? ` (+${values.length - 3} more)` : ''} [structural-only]`,
|
|
448
|
+
fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
catch (e) {
|
|
452
|
+
const msg = e.message || String(e);
|
|
453
|
+
// Encryption ruled out above; DB opened but the pragma itself threw —
|
|
454
|
+
// still definitive, unencrypted breakage. Fail, not warn.
|
|
455
|
+
return {
|
|
456
|
+
name: NAME,
|
|
457
|
+
status: 'fail',
|
|
458
|
+
message: `${dbPath} — quick_check probe threw: ${msg} (unencrypted) [structural-only]`,
|
|
459
|
+
fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
finally {
|
|
463
|
+
try {
|
|
464
|
+
db.close();
|
|
465
|
+
}
|
|
466
|
+
catch { /* best-effort */ }
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Check 1 (--component memory, deep path) — #2737 part 2 strengthens this:
|
|
470
|
+
// prefer native better-sqlite3 PRAGMA integrity_check (adds index↔table
|
|
471
|
+
// cross-checking + UNIQUE verification that quick_check above deliberately
|
|
472
|
+
// skips, and is WAL-aware — see checkMemoryStructuralIntegrity's comment)
|
|
473
|
+
// when better-sqlite3 is available, falling back to the original sql.js
|
|
474
|
+
// probe when it's not. The sql.js fallback is main-image-only (WAL-blind);
|
|
475
|
+
// its message says so explicitly so operators know its limits.
|
|
476
|
+
//
|
|
477
|
+
// #2737 part 3: with the encryption carve-out applied up front on BOTH
|
|
478
|
+
// paths, a "file is not a database" / "malformed" result is now definitive,
|
|
479
|
+
// unencrypted corruption in every branch below — fail, not warn (the
|
|
480
|
+
// previous version treated this as an ambiguous warn because it couldn't
|
|
481
|
+
// tell corruption from encryption; that ambiguity is what the carve-out
|
|
482
|
+
// resolves).
|
|
313
483
|
async function checkMemoryIntegrity() {
|
|
314
484
|
const dbPath = await resolveMemoryDbPath();
|
|
315
485
|
if (!dbPath)
|
|
316
|
-
return { name: 'Memory Integrity', status: 'warn', message: 'no memory.db found (see Memory Database check above)' };
|
|
486
|
+
return { name: 'Memory Integrity', status: 'warn', message: 'no memory.db found (see Memory Database Presence check above)' };
|
|
487
|
+
if (isMemoryDbEncryptedAtRest(dbPath)) {
|
|
488
|
+
return {
|
|
489
|
+
name: 'Memory Integrity',
|
|
490
|
+
status: 'warn',
|
|
491
|
+
message: `${dbPath} — RFE1-encrypted at rest; integrity check can't run without decrypting (expected, not corruption — see Encryption at Rest)`,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
// Prefer native better-sqlite3 (WAL-aware, full integrity_check). Module
|
|
495
|
+
// load is isolated in its own try/catch so ONLY "not installed" falls
|
|
496
|
+
// through to the sql.js fallback below — once the module loaded, any
|
|
497
|
+
// open/query failure is resolved (fail) right here, not silently
|
|
498
|
+
// reclassified as "sql.js fallback, main-image-only" by an outer catch.
|
|
499
|
+
let Database;
|
|
500
|
+
try {
|
|
501
|
+
Database = (await import('better-sqlite3')).default;
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
Database = null; // not installed — fall through to the sql.js fallback below.
|
|
505
|
+
}
|
|
506
|
+
if (Database) {
|
|
507
|
+
let db;
|
|
508
|
+
try {
|
|
509
|
+
db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
510
|
+
}
|
|
511
|
+
catch (e) {
|
|
512
|
+
const msg = e.message || String(e);
|
|
513
|
+
return {
|
|
514
|
+
name: 'Memory Integrity',
|
|
515
|
+
status: 'fail',
|
|
516
|
+
message: `${dbPath} — better-sqlite3 failed to open: ${msg} (unencrypted; definitively malformed) [native, WAL-aware]`,
|
|
517
|
+
fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const rows = db.pragma('integrity_check');
|
|
522
|
+
const values = rows.map((r) => String(Object.values(r)[0]));
|
|
523
|
+
if (values.length === 1 && values[0] === 'ok') {
|
|
524
|
+
return { name: 'Memory Integrity', status: 'pass', message: `${dbPath} — PRAGMA integrity_check: ok [native, WAL-aware]` };
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
name: 'Memory Integrity',
|
|
528
|
+
status: 'fail',
|
|
529
|
+
message: `${dbPath} — PRAGMA integrity_check: ${values.slice(0, 3).join('; ')}${values.length > 3 ? ` (+${values.length - 3} more)` : ''} [native, WAL-aware]`,
|
|
530
|
+
fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
catch (e) {
|
|
534
|
+
// DB opened but the pragma itself threw — still definitive,
|
|
535
|
+
// unencrypted breakage (encryption ruled out above). Fail, not warn,
|
|
536
|
+
// and NOT a fall-through to the sql.js fallback: better-sqlite3 IS
|
|
537
|
+
// installed and just gave us the answer.
|
|
538
|
+
const msg = e.message || String(e);
|
|
539
|
+
return {
|
|
540
|
+
name: 'Memory Integrity',
|
|
541
|
+
status: 'fail',
|
|
542
|
+
message: `${dbPath} — integrity_check probe threw: ${msg} (unencrypted; definitively malformed) [native, WAL-aware]`,
|
|
543
|
+
fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
try {
|
|
548
|
+
db.close();
|
|
549
|
+
}
|
|
550
|
+
catch { /* best-effort */ }
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Fallback: sql.js — main-image-only (WAL-blind). tryOpenSqlJs()
|
|
554
|
+
// readFileSync()s the base file directly, so any committed-but-not-
|
|
555
|
+
// checkpointed rows sitting in a sibling -wal file are invisible here.
|
|
556
|
+
// Install better-sqlite3 for the WAL-aware path above.
|
|
317
557
|
const db = await tryOpenSqlJs(dbPath);
|
|
318
558
|
if (!db) {
|
|
559
|
+
// Encryption already ruled out above — an unencrypted file sql.js can't
|
|
560
|
+
// even open is definitive corruption too.
|
|
319
561
|
return {
|
|
320
562
|
name: 'Memory Integrity',
|
|
321
|
-
status: '
|
|
322
|
-
message: `${dbPath} — sql.js can't open (
|
|
323
|
-
fix: '
|
|
563
|
+
status: 'fail',
|
|
564
|
+
message: `${dbPath} — sql.js can't open (unencrypted; definitively malformed) [main-image-only fallback — install better-sqlite3 for WAL-aware checking]`,
|
|
565
|
+
fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
|
|
324
566
|
};
|
|
325
567
|
}
|
|
326
568
|
try {
|
|
327
569
|
const res = db.exec('PRAGMA integrity_check');
|
|
328
570
|
const rows = res[0]?.values?.map((v) => String(v[0])) ?? [];
|
|
329
571
|
if (rows.length === 1 && rows[0] === 'ok') {
|
|
330
|
-
return { name: 'Memory Integrity', status: 'pass', message: `${dbPath} — PRAGMA integrity_check: ok` };
|
|
572
|
+
return { name: 'Memory Integrity', status: 'pass', message: `${dbPath} — PRAGMA integrity_check: ok [main-image-only fallback — sql.js can't see WAL-only data; install better-sqlite3 for full coverage]` };
|
|
331
573
|
}
|
|
332
574
|
return {
|
|
333
575
|
name: 'Memory Integrity',
|
|
334
576
|
status: 'fail',
|
|
335
|
-
message: `${dbPath} — PRAGMA integrity_check: ${rows.slice(0, 3).join('; ')}${rows.length > 3 ? ` (+${rows.length - 3} more)` : ''}`,
|
|
577
|
+
message: `${dbPath} — PRAGMA integrity_check: ${rows.slice(0, 3).join('; ')}${rows.length > 3 ? ` (+${rows.length - 3} more)` : ''} [main-image-only fallback]`,
|
|
336
578
|
fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
|
|
337
579
|
};
|
|
338
580
|
}
|
|
339
581
|
catch (e) {
|
|
340
582
|
const msg = e.message || String(e);
|
|
341
|
-
const
|
|
583
|
+
const definitivelyMalformed = msg.includes('file is not a database') || msg.includes('malformed');
|
|
584
|
+
// Encryption already ruled out above, so — matched pattern or not —
|
|
585
|
+
// this is a query refusal on a file we KNOW isn't RFE1-encrypted.
|
|
586
|
+
// #2737 part 3: fail, not warn.
|
|
342
587
|
return {
|
|
343
588
|
name: 'Memory Integrity',
|
|
344
|
-
status: '
|
|
345
|
-
message:
|
|
346
|
-
? `${dbPath} — DB refused query: ${msg} (
|
|
347
|
-
: `${dbPath} — probe threw: ${msg}`,
|
|
589
|
+
status: 'fail',
|
|
590
|
+
message: definitivelyMalformed
|
|
591
|
+
? `${dbPath} — DB refused query: ${msg} (unencrypted; definitively malformed) [main-image-only fallback]`
|
|
592
|
+
: `${dbPath} — probe threw: ${msg} (unencrypted — encryption ruled out above) [main-image-only fallback]`,
|
|
593
|
+
fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
|
|
348
594
|
};
|
|
349
595
|
}
|
|
350
596
|
finally {
|
|
@@ -1715,6 +1961,7 @@ export const doctorCommand = {
|
|
|
1715
1961
|
checkStaleSettingsNpx, // #2448 — runaway `npx @latest` in statusLine/hooks
|
|
1716
1962
|
checkDaemonStatus,
|
|
1717
1963
|
checkMemoryDatabase,
|
|
1964
|
+
checkMemoryStructuralIntegrity, // #2737 — bounded, native quick_check on every default run
|
|
1718
1965
|
checkLearningBridge, // #2545 — can the auto-memory hook actually load @claude-flow/memory?
|
|
1719
1966
|
checkApiKeys,
|
|
1720
1967
|
checkMcpServers,
|
|
@@ -3485,12 +3485,54 @@ const statuslineCommand = {
|
|
|
3485
3485
|
const contextPct = Math.min(100, Math.floor(learning.sessions * 5));
|
|
3486
3486
|
return { memoryMB, contextPct, intelligencePct, subAgents };
|
|
3487
3487
|
}
|
|
3488
|
+
// #2733: Claude Code pipes session JSON (incl. the real active model) on
|
|
3489
|
+
// stdin to statusline commands. Read it synchronously the same way
|
|
3490
|
+
// .claude/helpers/statusline.cjs's getModelFromStdin() does, so this CLI
|
|
3491
|
+
// subcommand is correct standalone — not just when the generated helper
|
|
3492
|
+
// happens to override its output. Read once, memoized per invocation
|
|
3493
|
+
// (mirrors statusline.cjs's _stdinData cache).
|
|
3494
|
+
let _stdinData;
|
|
3495
|
+
let _stdinRead = false;
|
|
3496
|
+
function getStdinData() {
|
|
3497
|
+
if (_stdinRead)
|
|
3498
|
+
return _stdinData;
|
|
3499
|
+
_stdinRead = true;
|
|
3500
|
+
try {
|
|
3501
|
+
if (process.stdin.isTTY) {
|
|
3502
|
+
_stdinData = null;
|
|
3503
|
+
return null;
|
|
3504
|
+
}
|
|
3505
|
+
const chunks = [];
|
|
3506
|
+
const buf = Buffer.alloc(4096);
|
|
3507
|
+
let bytesRead;
|
|
3508
|
+
try {
|
|
3509
|
+
while ((bytesRead = fs.readSync(0, buf, 0, buf.length, null)) > 0) {
|
|
3510
|
+
chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
catch { /* EOF or read error */ }
|
|
3514
|
+
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
3515
|
+
_stdinData = (raw && raw.startsWith('{')) ? JSON.parse(raw) : null;
|
|
3516
|
+
}
|
|
3517
|
+
catch {
|
|
3518
|
+
_stdinData = null;
|
|
3519
|
+
}
|
|
3520
|
+
return _stdinData;
|
|
3521
|
+
}
|
|
3522
|
+
function getModelFromStdin() {
|
|
3523
|
+
const data = getStdinData();
|
|
3524
|
+
return (data && data.model && typeof data.model.display_name === 'string') ? data.model.display_name : null;
|
|
3525
|
+
}
|
|
3488
3526
|
// Get user info
|
|
3489
3527
|
function getUserInfo() {
|
|
3490
3528
|
const identityMode = (process.env.RUFLO_STATUSLINE_IDENTITY || 'project').toLowerCase();
|
|
3491
3529
|
let name = path.basename(process.cwd()) || 'project';
|
|
3492
3530
|
let gitBranch = '';
|
|
3493
|
-
|
|
3531
|
+
// Real active model from Claude Code's stdin payload when available;
|
|
3532
|
+
// this string is only a last-resort label for direct/manual CLI
|
|
3533
|
+
// invocation with no stdin (e.g. a bare terminal run) — never shown
|
|
3534
|
+
// when Claude Code itself is the caller.
|
|
3535
|
+
const modelName = getModelFromStdin() || 'Claude Code';
|
|
3494
3536
|
const isWindows = process.platform === 'win32';
|
|
3495
3537
|
try {
|
|
3496
3538
|
const rootCmd = isWindows
|
|
@@ -5,16 +5,23 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Design discipline:
|
|
7
7
|
* 1. **Never blocks the render.** The statusline reads only the local
|
|
8
|
-
* cache. This module refreshes the cache in the background;
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* cache. This module refreshes the cache in the background; per
|
|
9
|
+
* ADR-311 "zero local promo content" there is NO in-code fallback
|
|
10
|
+
* pool — a genuinely fresh install (empty cache) shows nothing on
|
|
11
|
+
* the promo/disclosure row until the first background refresh lands
|
|
12
|
+
* (see hook-handler.cjs's SessionStart-triggered detached
|
|
13
|
+
* `hooks refresh-funnel`, usually within seconds of CLI startup, but
|
|
14
|
+
* contingent on network reachability to the messages endpoint). This
|
|
15
|
+
* comment previously (incorrectly) described an in-code fallback
|
|
16
|
+
* pool; there has never been one since ADR-311 — see disclosure.ts's
|
|
17
|
+
* own "fail-closed" doc comment for the authoritative statement.
|
|
11
18
|
* 2. **Content pipeline stays authoritative.** Every message the server
|
|
12
19
|
* returns is validated by `isValidMessage()` from `messages.ts` —
|
|
13
20
|
* same schema, same host allowlist, same control-char strip, same
|
|
14
21
|
* 80-column cap. A tampered or accidentally-broken remote feed can
|
|
15
22
|
* pollute nothing.
|
|
16
23
|
* 3. **Fail silent.** Any network/parse/validation failure leaves the
|
|
17
|
-
* previously-cached (
|
|
24
|
+
* previously-cached pool intact (empty, if nothing has landed yet).
|
|
18
25
|
* 4. **Bounded cache size.** ≤ 128 KiB and ≤ 200 messages — matches
|
|
19
26
|
* ADR-309's bounded-local-queue discipline.
|
|
20
27
|
* 5. **Kill switch.** `RUFLO_FUNNEL_MESSAGES=0` (or `RUFLO_FUNNEL=0`)
|
|
@@ -5,16 +5,23 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Design discipline:
|
|
7
7
|
* 1. **Never blocks the render.** The statusline reads only the local
|
|
8
|
-
* cache. This module refreshes the cache in the background;
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* cache. This module refreshes the cache in the background; per
|
|
9
|
+
* ADR-311 "zero local promo content" there is NO in-code fallback
|
|
10
|
+
* pool — a genuinely fresh install (empty cache) shows nothing on
|
|
11
|
+
* the promo/disclosure row until the first background refresh lands
|
|
12
|
+
* (see hook-handler.cjs's SessionStart-triggered detached
|
|
13
|
+
* `hooks refresh-funnel`, usually within seconds of CLI startup, but
|
|
14
|
+
* contingent on network reachability to the messages endpoint). This
|
|
15
|
+
* comment previously (incorrectly) described an in-code fallback
|
|
16
|
+
* pool; there has never been one since ADR-311 — see disclosure.ts's
|
|
17
|
+
* own "fail-closed" doc comment for the authoritative statement.
|
|
11
18
|
* 2. **Content pipeline stays authoritative.** Every message the server
|
|
12
19
|
* returns is validated by `isValidMessage()` from `messages.ts` —
|
|
13
20
|
* same schema, same host allowlist, same control-char strip, same
|
|
14
21
|
* 80-column cap. A tampered or accidentally-broken remote feed can
|
|
15
22
|
* pollute nothing.
|
|
16
23
|
* 3. **Fail silent.** Any network/parse/validation failure leaves the
|
|
17
|
-
* previously-cached (
|
|
24
|
+
* previously-cached pool intact (empty, if nothing has landed yet).
|
|
18
25
|
* 4. **Bounded cache size.** ≤ 128 KiB and ≤ 200 messages — matches
|
|
19
26
|
* ADR-309's bounded-local-queue discipline.
|
|
20
27
|
* 5. **Kill switch.** `RUFLO_FUNNEL_MESSAGES=0` (or `RUFLO_FUNNEL=0`)
|
|
@@ -90,6 +90,15 @@ export declare function countGraphEdges(dbPath?: string): Promise<number>;
|
|
|
90
90
|
* #2431 fix: also explicitly closes the prior handle so file locks
|
|
91
91
|
* release immediately — better-sqlite3 holds an OS-level file handle
|
|
92
92
|
* which the prior sql.js implementation did not.
|
|
93
|
+
*
|
|
94
|
+
* #2736-followup fix: `close()` alone only checkpoints the WAL back into
|
|
95
|
+
* the main file as a best-effort PASSIVE checkpoint when SQLite considers
|
|
96
|
+
* this the last connection — which is not guaranteed to fully flush under
|
|
97
|
+
* contention, and was observed leaving writes invisible to a same-process
|
|
98
|
+
* sql.js reader that re-reads the raw file immediately after close() on
|
|
99
|
+
* Linux CI runners (not reproduced on Windows). Force a blocking TRUNCATE
|
|
100
|
+
* checkpoint before close so cross-engine readers always see committed
|
|
101
|
+
* writes deterministically, regardless of platform/timing.
|
|
93
102
|
*/
|
|
94
103
|
export declare function _resetBridgeDb(): void;
|
|
95
104
|
//# sourceMappingURL=graph-edge-writer.d.ts.map
|
|
@@ -203,9 +203,22 @@ export async function countGraphEdges(dbPath) {
|
|
|
203
203
|
* #2431 fix: also explicitly closes the prior handle so file locks
|
|
204
204
|
* release immediately — better-sqlite3 holds an OS-level file handle
|
|
205
205
|
* which the prior sql.js implementation did not.
|
|
206
|
+
*
|
|
207
|
+
* #2736-followup fix: `close()` alone only checkpoints the WAL back into
|
|
208
|
+
* the main file as a best-effort PASSIVE checkpoint when SQLite considers
|
|
209
|
+
* this the last connection — which is not guaranteed to fully flush under
|
|
210
|
+
* contention, and was observed leaving writes invisible to a same-process
|
|
211
|
+
* sql.js reader that re-reads the raw file immediately after close() on
|
|
212
|
+
* Linux CI runners (not reproduced on Windows). Force a blocking TRUNCATE
|
|
213
|
+
* checkpoint before close so cross-engine readers always see committed
|
|
214
|
+
* writes deterministically, regardless of platform/timing.
|
|
206
215
|
*/
|
|
207
216
|
export function _resetBridgeDb() {
|
|
208
217
|
if (_db) {
|
|
218
|
+
try {
|
|
219
|
+
_db.pragma('wal_checkpoint(TRUNCATE)');
|
|
220
|
+
}
|
|
221
|
+
catch { /* best-effort */ }
|
|
209
222
|
try {
|
|
210
223
|
_db.close();
|
|
211
224
|
}
|
|
@@ -23,6 +23,28 @@ import { createRequire } from 'node:module';
|
|
|
23
23
|
let registryPromise = null;
|
|
24
24
|
let registryInstance = null;
|
|
25
25
|
let bridgeAvailable = null;
|
|
26
|
+
// #2735 — the native-bridge -> sql.js whole-image fallback used to be
|
|
27
|
+
// completely silent: a registry init failure cached `bridgeAvailable =
|
|
28
|
+
// false` for the rest of the process, and a per-operation bridge exception
|
|
29
|
+
// swallowed itself with a bare `catch { return null; }` — in both cases the
|
|
30
|
+
// caller fell back to memory-initializer.ts's unsafe whole-image sql.js
|
|
31
|
+
// path with zero diagnostic trail. When that path corrupted a database, the
|
|
32
|
+
// demotion that caused it was structurally unknowable after the fact. Log
|
|
33
|
+
// once per distinct reason (not once per call — a hot per-op catch could
|
|
34
|
+
// otherwise spam stderr on every single memory operation) so the first
|
|
35
|
+
// occurrence is attributable. Stderr only — never stdout, which callers may
|
|
36
|
+
// be parsing as JSON.
|
|
37
|
+
const loggedDemotions = new Set();
|
|
38
|
+
function logDemotionOnce(where, reason) {
|
|
39
|
+
const dedupeKey = `${where}:${reason}`;
|
|
40
|
+
if (loggedDemotions.has(dedupeKey))
|
|
41
|
+
return;
|
|
42
|
+
loggedDemotions.add(dedupeKey);
|
|
43
|
+
try {
|
|
44
|
+
process.stderr.write(`[memory-bridge] demoted to sql.js fallback in ${where}: ${reason}\n`);
|
|
45
|
+
}
|
|
46
|
+
catch { /* stderr unavailable — nothing more we can do */ }
|
|
47
|
+
}
|
|
26
48
|
/**
|
|
27
49
|
* Resolve database path with path traversal protection.
|
|
28
50
|
* Only allows paths within or below the project's working directory,
|
|
@@ -350,9 +372,10 @@ async function getRegistry(dbPath) {
|
|
|
350
372
|
bridgeAvailable = true;
|
|
351
373
|
return registry;
|
|
352
374
|
}
|
|
353
|
-
catch {
|
|
375
|
+
catch (err) {
|
|
354
376
|
bridgeAvailable = false;
|
|
355
377
|
registryPromise = null;
|
|
378
|
+
logDemotionOnce('getRegistry (process-wide, cached for the rest of this process)', err instanceof Error ? err.message : String(err));
|
|
356
379
|
return null;
|
|
357
380
|
}
|
|
358
381
|
})();
|
|
@@ -737,7 +760,8 @@ export async function bridgeStoreEntry(options) {
|
|
|
737
760
|
attested: true,
|
|
738
761
|
};
|
|
739
762
|
}
|
|
740
|
-
catch {
|
|
763
|
+
catch (err) {
|
|
764
|
+
logDemotionOnce('bridgeStoreEntry', err instanceof Error ? err.message : String(err));
|
|
741
765
|
return null;
|
|
742
766
|
}
|
|
743
767
|
}
|
|
@@ -1017,7 +1041,8 @@ export async function bridgeGetEntry(options) {
|
|
|
1017
1041
|
await cacheSet(registry, cacheKey, entry);
|
|
1018
1042
|
return { success: true, found: true, cacheHit: false, entry };
|
|
1019
1043
|
}
|
|
1020
|
-
catch {
|
|
1044
|
+
catch (err) {
|
|
1045
|
+
logDemotionOnce('bridgeGetEntry', err instanceof Error ? err.message : String(err));
|
|
1021
1046
|
return null;
|
|
1022
1047
|
}
|
|
1023
1048
|
}
|
|
@@ -1077,7 +1102,8 @@ export async function bridgeDeleteEntry(options) {
|
|
|
1077
1102
|
guarded: true,
|
|
1078
1103
|
};
|
|
1079
1104
|
}
|
|
1080
|
-
catch {
|
|
1105
|
+
catch (err) {
|
|
1106
|
+
logDemotionOnce('bridgeDeleteEntry', err instanceof Error ? err.message : String(err));
|
|
1081
1107
|
return null;
|
|
1082
1108
|
}
|
|
1083
1109
|
}
|
|
@@ -1128,7 +1154,8 @@ export async function bridgePurgeNamespace(options) {
|
|
|
1128
1154
|
guarded: true,
|
|
1129
1155
|
};
|
|
1130
1156
|
}
|
|
1131
|
-
catch {
|
|
1157
|
+
catch (err) {
|
|
1158
|
+
logDemotionOnce('bridgePurgeNamespace', err instanceof Error ? err.message : String(err));
|
|
1132
1159
|
return null;
|
|
1133
1160
|
}
|
|
1134
1161
|
}
|
|
@@ -35,6 +35,33 @@ function isRuvectorCoreResolvable() {
|
|
|
35
35
|
}
|
|
36
36
|
return _ruvectorCoreResolvable;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* #2735 — before a whole-image sql.js read-modify-persist (export() +
|
|
40
|
+
* rename over the live database path), refuse if there is evidence of a
|
|
41
|
+
* live native (better-sqlite3) WAL connection: `-wal`/`-shm` sidecar files
|
|
42
|
+
* on disk. A native connection in WAL mode keeps its sidecars present for
|
|
43
|
+
* its entire lifetime (removed only on the last connection's clean close),
|
|
44
|
+
* so their presence is strong evidence of a live native holder — and their
|
|
45
|
+
* absence means the image is a clean, checkpointed, standalone file that
|
|
46
|
+
* sql.js can safely read-modify-write.
|
|
47
|
+
*
|
|
48
|
+
* This is a scoped-down version of the fuller "scan live process holders"
|
|
49
|
+
* design discussed in #2735: it does not close the narrow assess-then-write
|
|
50
|
+
* race (a native opener could still attach in the gap between this check
|
|
51
|
+
* and the write), but it directly closes the demonstrated corruption
|
|
52
|
+
* mechanism — a whole-image write proceeding while an ALREADY-OPEN native
|
|
53
|
+
* connection's sidecars are on disk — with no platform-specific process
|
|
54
|
+
* scanning. Fails closed (treats a stat error as "unsafe") because this is
|
|
55
|
+
* a safety gate, not a best-effort probe.
|
|
56
|
+
*/
|
|
57
|
+
function hasNativeWalSidecars(dbPath) {
|
|
58
|
+
try {
|
|
59
|
+
return fs.existsSync(`${dbPath}-wal`) || fs.existsSync(`${dbPath}-shm`);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
38
65
|
/**
|
|
39
66
|
* #1854: previously every site that needed the memory directory hardcoded
|
|
40
67
|
* `getMemoryRoot()`, so the documented config entry
|
|
@@ -2304,6 +2331,22 @@ export async function storeEntry(options) {
|
|
|
2304
2331
|
if (!fs.existsSync(dbPath)) {
|
|
2305
2332
|
return { success: false, id: '', error: 'Database not initialized. Run: claude-flow memory init' };
|
|
2306
2333
|
}
|
|
2334
|
+
// #2735 — refuse an unsafe whole-image write while a native WAL
|
|
2335
|
+
// connection appears to be attached (sidecars on disk). See
|
|
2336
|
+
// hasNativeWalSidecars()'s doc comment for the corruption mechanism
|
|
2337
|
+
// this closes and its known residual (the narrow assess-then-write
|
|
2338
|
+
// race). This check gates ensureSchemaColumns()'s own whole-image
|
|
2339
|
+
// write below too, not just this function's.
|
|
2340
|
+
if (hasNativeWalSidecars(dbPath)) {
|
|
2341
|
+
return {
|
|
2342
|
+
success: false,
|
|
2343
|
+
id: '',
|
|
2344
|
+
error: 'memory database has an active native WAL connection ' +
|
|
2345
|
+
'(found -wal/-shm sidecar files) — refusing an unsafe sql.js ' +
|
|
2346
|
+
'whole-image write. Retry once the native writer completes, or ' +
|
|
2347
|
+
'restore the native better-sqlite3 bridge.',
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2307
2350
|
// Ensure schema has all required columns (migration for older DBs)
|
|
2308
2351
|
await ensureSchemaColumns(dbPath);
|
|
2309
2352
|
const initSqlJs = (await import('sql.js')).default;
|
|
@@ -2672,6 +2715,20 @@ export async function getEntry(options) {
|
|
|
2672
2715
|
if (!fs.existsSync(dbPath)) {
|
|
2673
2716
|
return { success: false, found: false, error: 'Database not found' };
|
|
2674
2717
|
}
|
|
2718
|
+
// #2735 — see storeEntry's identical gate for the corruption mechanism
|
|
2719
|
+
// this closes. Applies here too because the fallback's access_count
|
|
2720
|
+
// bump is itself a whole-image write, not a lightweight read, even
|
|
2721
|
+
// though this function's contract reads as a "get".
|
|
2722
|
+
if (hasNativeWalSidecars(dbPath)) {
|
|
2723
|
+
return {
|
|
2724
|
+
success: false,
|
|
2725
|
+
found: false,
|
|
2726
|
+
error: 'memory database has an active native WAL connection ' +
|
|
2727
|
+
'(found -wal/-shm sidecar files) — refusing an unsafe sql.js ' +
|
|
2728
|
+
'whole-image read/write. Retry once the native writer completes, ' +
|
|
2729
|
+
'or restore the native better-sqlite3 bridge.',
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2675
2732
|
// Ensure schema has all required columns (migration for older DBs)
|
|
2676
2733
|
await ensureSchemaColumns(dbPath);
|
|
2677
2734
|
const initSqlJs = (await import('sql.js')).default;
|
|
@@ -2783,6 +2840,21 @@ export async function deleteEntry(options) {
|
|
|
2783
2840
|
error: 'Database not found'
|
|
2784
2841
|
};
|
|
2785
2842
|
}
|
|
2843
|
+
// #2735 — see storeEntry's identical gate for the corruption mechanism
|
|
2844
|
+
// this closes.
|
|
2845
|
+
if (hasNativeWalSidecars(dbPath)) {
|
|
2846
|
+
return {
|
|
2847
|
+
success: false,
|
|
2848
|
+
deleted: false,
|
|
2849
|
+
key,
|
|
2850
|
+
namespace,
|
|
2851
|
+
remainingEntries: 0,
|
|
2852
|
+
error: 'memory database has an active native WAL connection ' +
|
|
2853
|
+
'(found -wal/-shm sidecar files) — refusing an unsafe sql.js ' +
|
|
2854
|
+
'whole-image write. Retry once the native writer completes, or ' +
|
|
2855
|
+
'restore the native better-sqlite3 bridge.',
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2786
2858
|
// Ensure schema has all required columns (migration for older DBs)
|
|
2787
2859
|
await ensureSchemaColumns(dbPath);
|
|
2788
2860
|
const initSqlJs = (await import('sql.js')).default;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.32.
|
|
3
|
+
"version": "3.32.9",
|
|
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",
|
|
@@ -115,6 +115,7 @@
|
|
|
115
115
|
"@claude-flow/security": "^3.0.0-alpha.12",
|
|
116
116
|
"agentdb": "^3.0.0-alpha.17",
|
|
117
117
|
"agentic-flow": "^3.0.0-alpha.1",
|
|
118
|
+
"better-sqlite3": "^12.9.0",
|
|
118
119
|
"ruvector": "^0.2.27"
|
|
119
120
|
},
|
|
120
121
|
"publishConfig": {
|
|
@@ -130,6 +131,7 @@
|
|
|
130
131
|
"@opentelemetry/resources": ">=2.8.0",
|
|
131
132
|
"@opentelemetry/sdk-trace-base": ">=2.8.0",
|
|
132
133
|
"@opentelemetry/sdk-node": ">=0.220.0",
|
|
133
|
-
"agentdb": "$agentdb"
|
|
134
|
+
"agentdb": "$agentdb",
|
|
135
|
+
"better-sqlite3": "^12.9.0"
|
|
134
136
|
}
|
|
135
137
|
}
|