claude-flow 3.57.1 → 3.58.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/.claude/helpers/intelligence.cjs +3 -5
- package/.claude/proven-config.json +1 -1
- package/.claude-plugin/marketplace.json +11 -0
- package/README.md +1 -0
- package/node_modules/@claude-flow/codex/README.md +87 -9
- package/node_modules/@claude-flow/codex/dist/activity/cli.d.ts +3 -0
- package/node_modules/@claude-flow/codex/dist/activity/cli.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/cli.js +115 -0
- package/node_modules/@claude-flow/codex/dist/activity/cli.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/files.d.ts +14 -0
- package/node_modules/@claude-flow/codex/dist/activity/files.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/files.js +142 -0
- package/node_modules/@claude-flow/codex/dist/activity/files.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/parser.d.ts +21 -0
- package/node_modules/@claude-flow/codex/dist/activity/parser.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/parser.js +179 -0
- package/node_modules/@claude-flow/codex/dist/activity/parser.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/reader.d.ts +20 -0
- package/node_modules/@claude-flow/codex/dist/activity/reader.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/reader.js +106 -0
- package/node_modules/@claude-flow/codex/dist/activity/reader.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/sanitize.d.ts +4 -0
- package/node_modules/@claude-flow/codex/dist/activity/sanitize.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/sanitize.js +27 -0
- package/node_modules/@claude-flow/codex/dist/activity/sanitize.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/types.d.ts +31 -0
- package/node_modules/@claude-flow/codex/dist/activity/types.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/types.js +5 -0
- package/node_modules/@claude-flow/codex/dist/activity/types.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/viewer.d.ts +21 -0
- package/node_modules/@claude-flow/codex/dist/activity/viewer.d.ts.map +1 -0
- package/node_modules/@claude-flow/codex/dist/activity/viewer.js +168 -0
- package/node_modules/@claude-flow/codex/dist/activity/viewer.js.map +1 -0
- package/node_modules/@claude-flow/codex/dist/cli.js +3 -0
- package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +3 -3
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/server.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/server.js +5 -3
- package/node_modules/@claude-flow/mcp/dist/server.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/types.d.ts +11 -0
- package/node_modules/@claude-flow/mcp/dist/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/types.js.map +1 -1
- package/package.json +3 -2
- package/v3/@claude-flow/cli/README.md +1 -0
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/agent.js +4 -1
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +20 -1
- package/v3/@claude-flow/cli/dist/src/commands/mcp.js +66 -6
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +78 -3
- package/v3/@claude-flow/cli/dist/src/commands/workflow.js +55 -15
- package/v3/@claude-flow/cli/dist/src/init/executor.js +9 -5
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +4 -3
- package/v3/@claude-flow/cli/dist/src/mcp-server.d.ts +5 -0
- package/v3/@claude-flow/cli/dist/src/mcp-server.js +39 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +28 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/claims-tools.js +25 -15
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +9 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +58 -9
- package/v3/@claude-flow/cli/dist/src/mcp-tools/workflow-tools.js +2 -1
- package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.d.ts +16 -4
- package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js +19 -7
- package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +4 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +55 -9
- package/v3/@claude-flow/cli/dist/src/memory/memory-peek.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-peek.js +222 -0
- package/v3/@claude-flow/cli/dist/src/memory/model-cache-dir.d.ts +11 -0
- package/v3/@claude-flow/cli/dist/src/memory/model-cache-dir.js +17 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/task-embedder.js +38 -4
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +6 -0
- package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +109 -81
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.js +4 -4
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +54 -3
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +4 -0
- package/v3/@claude-flow/cli/package.json +5 -4
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3508: read-only memory route/peek.
|
|
3
|
+
*
|
|
4
|
+
* A health check needs to know which memory store files a cwd resolves to,
|
|
5
|
+
* and what is in them, without changing anything to find out. `memory stats`
|
|
6
|
+
* (even after #3463/#3448) still calls `ensureInitialized()` when no explicit
|
|
7
|
+
* path is given, and answers for one file per call. This module reports
|
|
8
|
+
* BOTH files — the sql.js `memory.db` the CLI writes and its native
|
|
9
|
+
* `agentdb-memory.db` sibling the MCP/bridge path writes — in one read-only
|
|
10
|
+
* pass, and says which resolution rule picked the root.
|
|
11
|
+
*
|
|
12
|
+
* Contract (do not relax without re-reading #3508):
|
|
13
|
+
* - never creates, initializes, or migrates anything;
|
|
14
|
+
* - never opens a file that does not exist (`existsSync` gated before any
|
|
15
|
+
* open attempt, for both engines);
|
|
16
|
+
* - every open is `readonly`/`fileMustExist` (native) or a one-shot byte
|
|
17
|
+
* buffer handed to sql.js, which is never written back to disk;
|
|
18
|
+
* - `rows: null` means "could not read" and is never a count of 0, so an
|
|
19
|
+
* empty store and an unreadable one never look the same;
|
|
20
|
+
* - an encrypted-at-rest `memory.db` reports `encrypted: true` and
|
|
21
|
+
* `rows: null`, but its size is still reported.
|
|
22
|
+
*/
|
|
23
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync } from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { getMemoryRoot, resolveDbPath } from './memory-initializer.js';
|
|
26
|
+
import { siblingAgentDbPath } from './memory-bridge.js';
|
|
27
|
+
import { isEncryptedBlob } from '../encryption/vault.js';
|
|
28
|
+
// Mirrors the `ACTIVE_MEMORY_ROW_SQL` convention in memory-initializer.ts:
|
|
29
|
+
// legacy rows with a NULL status predate soft-delete semantics and count.
|
|
30
|
+
const ACTIVE_ROW_SQL = `(status = 'active' OR status IS NULL)`;
|
|
31
|
+
const ENCRYPTION_SNIFF_LEN = 64; // RFE1 magic(4)+iv(12)+tag(16); 64 gives headroom.
|
|
32
|
+
function safeStatSize(filePath) {
|
|
33
|
+
try {
|
|
34
|
+
return statSync(filePath).size;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function readHeaderBytes(filePath, len) {
|
|
41
|
+
const fd = openSync(filePath, 'r');
|
|
42
|
+
try {
|
|
43
|
+
const buf = Buffer.alloc(len);
|
|
44
|
+
const bytesRead = readSync(fd, buf, 0, len, 0);
|
|
45
|
+
return buf.subarray(0, bytesRead);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function latestMillis(values) {
|
|
52
|
+
let latest = -Infinity;
|
|
53
|
+
for (const raw of values) {
|
|
54
|
+
const millis = typeof raw === 'number' ? raw : Number(raw);
|
|
55
|
+
if (Number.isFinite(millis))
|
|
56
|
+
latest = Math.max(latest, millis);
|
|
57
|
+
}
|
|
58
|
+
return latest;
|
|
59
|
+
}
|
|
60
|
+
/** sql.js is read as a one-shot in-memory byte buffer — nothing is ever
|
|
61
|
+
* written back, so reading this file has no on-disk side effect. */
|
|
62
|
+
async function probeSqlJsStore(dbPath) {
|
|
63
|
+
try {
|
|
64
|
+
if (isEncryptedBlob(readHeaderBytes(dbPath, ENCRYPTION_SNIFF_LEN))) {
|
|
65
|
+
return { rows: null, lastWrite: null, keys: null, encrypted: true };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
/* header read failed (permissions/race) — fall through to the open attempt */
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const initSqlJs = (await import('sql.js')).default ?? (await import('sql.js'));
|
|
73
|
+
const SQL = await (typeof initSqlJs === 'function' ? initSqlJs() : initSqlJs.default());
|
|
74
|
+
const buf = readFileSync(dbPath);
|
|
75
|
+
const db = new SQL.Database(new Uint8Array(buf));
|
|
76
|
+
try {
|
|
77
|
+
const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries'");
|
|
78
|
+
if (!tables.length)
|
|
79
|
+
return { rows: null, lastWrite: null, keys: null };
|
|
80
|
+
const result = db.exec(`SELECT namespace, key, created_at, updated_at FROM memory_entries WHERE ${ACTIVE_ROW_SQL}`);
|
|
81
|
+
const values = result[0]?.values ?? [];
|
|
82
|
+
const keys = new Set();
|
|
83
|
+
let last = -Infinity;
|
|
84
|
+
for (const [namespace, key, createdAt, updatedAt] of values) {
|
|
85
|
+
keys.add(`${String(namespace)}\u0000${String(key)}`);
|
|
86
|
+
last = Math.max(last, latestMillis([createdAt, updatedAt]));
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
rows: values.length,
|
|
90
|
+
lastWrite: Number.isFinite(last) ? new Date(last).toISOString() : null,
|
|
91
|
+
keys,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
try {
|
|
96
|
+
db.close();
|
|
97
|
+
}
|
|
98
|
+
catch { /* best effort */ }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return { rows: null, lastWrite: null, keys: null };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** better-sqlite3 is an optional native dependency — absence must degrade
|
|
106
|
+
* to "could not read", never throw. Opened `readonly`/`fileMustExist`, same
|
|
107
|
+
* as `countSiblingStoreRows` in sibling-store.ts. */
|
|
108
|
+
async function probeNativeStore(dbPath) {
|
|
109
|
+
try {
|
|
110
|
+
const require = (await import('node:module')).createRequire(import.meta.url);
|
|
111
|
+
const Database = require('better-sqlite3');
|
|
112
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
113
|
+
try {
|
|
114
|
+
const table = db
|
|
115
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries'")
|
|
116
|
+
.get();
|
|
117
|
+
if (!table)
|
|
118
|
+
return { rows: null, lastWrite: null, keys: null };
|
|
119
|
+
const rows = db
|
|
120
|
+
.prepare(`SELECT namespace, key, created_at, updated_at FROM memory_entries WHERE ${ACTIVE_ROW_SQL}`)
|
|
121
|
+
.all();
|
|
122
|
+
const keys = new Set();
|
|
123
|
+
let last = -Infinity;
|
|
124
|
+
for (const row of rows) {
|
|
125
|
+
keys.add(`${String(row.namespace)}\u0000${String(row.key)}`);
|
|
126
|
+
last = Math.max(last, latestMillis([row.created_at, row.updated_at]));
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
rows: rows.length,
|
|
130
|
+
lastWrite: Number.isFinite(last) ? new Date(last).toISOString() : null,
|
|
131
|
+
keys,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
try {
|
|
136
|
+
db.close();
|
|
137
|
+
}
|
|
138
|
+
catch { /* best effort */ }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { rows: null, lastWrite: null, keys: null };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Same precedence `getMemoryRoot()`/`resolveDbPath()` already own — this
|
|
146
|
+
* only *labels* which rule decided, it never re-derives the path itself. */
|
|
147
|
+
function decideSource(cliFlag) {
|
|
148
|
+
if (cliFlag && cliFlag.trim().length > 0)
|
|
149
|
+
return 'path';
|
|
150
|
+
if (process.env.CLAUDE_FLOW_DB_PATH && process.env.CLAUDE_FLOW_DB_PATH.trim().length > 0) {
|
|
151
|
+
return 'CLAUDE_FLOW_DB_PATH';
|
|
152
|
+
}
|
|
153
|
+
if (process.env.CLAUDE_FLOW_MEMORY_PATH && process.env.CLAUDE_FLOW_MEMORY_PATH.trim().length > 0) {
|
|
154
|
+
return 'CLAUDE_FLOW_MEMORY_PATH';
|
|
155
|
+
}
|
|
156
|
+
const configCandidates = [
|
|
157
|
+
path.resolve(process.cwd(), 'claude-flow.config.json'),
|
|
158
|
+
path.resolve(process.cwd(), '.claude-flow', 'config.json'),
|
|
159
|
+
];
|
|
160
|
+
for (const configPath of configCandidates) {
|
|
161
|
+
if (!existsSync(configPath))
|
|
162
|
+
continue;
|
|
163
|
+
try {
|
|
164
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
165
|
+
const fromConfig = raw?.memory?.persistPath ?? raw?.memory?.path;
|
|
166
|
+
if (typeof fromConfig === 'string' && fromConfig.trim().length > 0)
|
|
167
|
+
return 'config';
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* malformed config — getMemoryRoot() falls through to default too */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return 'default';
|
|
174
|
+
}
|
|
175
|
+
async function describeStore(role, storePath, probe) {
|
|
176
|
+
if (!existsSync(storePath)) {
|
|
177
|
+
return {
|
|
178
|
+
store: { role, path: storePath, exists: false, rows: null, lastWrite: null, sizeBytes: null, walBytes: null },
|
|
179
|
+
keys: null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const result = await probe(storePath);
|
|
183
|
+
const sizeBytes = safeStatSize(storePath);
|
|
184
|
+
const walBytes = safeStatSize(`${storePath}-wal`) ?? 0;
|
|
185
|
+
return {
|
|
186
|
+
store: {
|
|
187
|
+
role,
|
|
188
|
+
path: storePath,
|
|
189
|
+
exists: true,
|
|
190
|
+
rows: result.rows,
|
|
191
|
+
lastWrite: result.lastWrite,
|
|
192
|
+
sizeBytes,
|
|
193
|
+
walBytes,
|
|
194
|
+
...(result.encrypted ? { encrypted: true } : {}),
|
|
195
|
+
},
|
|
196
|
+
keys: result.keys,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Resolve the route and report both stores, read-only. Never creates,
|
|
201
|
+
* initializes, or migrates anything, on an empty project or any other.
|
|
202
|
+
*/
|
|
203
|
+
export async function memoryPeek(opts) {
|
|
204
|
+
const cliFlag = opts?.path;
|
|
205
|
+
const cliPath = resolveDbPath(cliFlag);
|
|
206
|
+
const mcpPath = siblingAgentDbPath(cliPath);
|
|
207
|
+
const root = { path: cliFlag ? path.dirname(cliPath) : getMemoryRoot(), source: decideSource(cliFlag) };
|
|
208
|
+
const cliDescribed = await describeStore('cli', cliPath, probeSqlJsStore);
|
|
209
|
+
const stores = [cliDescribed.store];
|
|
210
|
+
let mcpKeys = null;
|
|
211
|
+
if (mcpPath) {
|
|
212
|
+
const mcpDescribed = await describeStore('mcp', mcpPath, probeNativeStore);
|
|
213
|
+
stores.push(mcpDescribed.store);
|
|
214
|
+
mcpKeys = mcpDescribed.keys;
|
|
215
|
+
}
|
|
216
|
+
const cliKeys = cliDescribed.keys;
|
|
217
|
+
const sharedKeys = cliKeys && mcpKeys
|
|
218
|
+
? [...cliKeys].filter((key) => mcpKeys.has(key)).length
|
|
219
|
+
: null;
|
|
220
|
+
return { cwd: process.cwd(), root, stores, sharedKeys };
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=memory-peek.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a writable cache directory for ONNX model downloads.
|
|
3
|
+
*
|
|
4
|
+
* ADR-454: duplicated intentionally from
|
|
5
|
+
* `@claude-flow/embeddings/src/transformers-loader.ts`'s
|
|
6
|
+
* `resolveModelCacheDir()` rather than imported, since `@claude-flow/cli`
|
|
7
|
+
* has no dependency on `@claude-flow/embeddings` (see the circular
|
|
8
|
+
* optional-dep note in `memory-initializer.ts`).
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveModelCacheDir(): string;
|
|
11
|
+
//# sourceMappingURL=model-cache-dir.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a writable cache directory for ONNX model downloads.
|
|
5
|
+
*
|
|
6
|
+
* ADR-454: duplicated intentionally from
|
|
7
|
+
* `@claude-flow/embeddings/src/transformers-loader.ts`'s
|
|
8
|
+
* `resolveModelCacheDir()` rather than imported, since `@claude-flow/cli`
|
|
9
|
+
* has no dependency on `@claude-flow/embeddings` (see the circular
|
|
10
|
+
* optional-dep note in `memory-initializer.ts`).
|
|
11
|
+
*/
|
|
12
|
+
export function resolveModelCacheDir() {
|
|
13
|
+
return (process.env.TRANSFORMERS_CACHE ||
|
|
14
|
+
process.env.HF_HOME ||
|
|
15
|
+
path.join(os.homedir(), '.cache', 'ruflo', 'models'));
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=model-cache-dir.js.map
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
*
|
|
20
20
|
* @module task-embedder
|
|
21
21
|
*/
|
|
22
|
+
import * as fs from 'node:fs';
|
|
23
|
+
import { resolveModelCacheDir } from '../memory/model-cache-dir.js';
|
|
22
24
|
// ============================================================================
|
|
23
25
|
// FNV-1a-32 hash (matches scripts/gen-seed-corpus.mjs + router-trajectory.ts)
|
|
24
26
|
// ============================================================================
|
|
@@ -38,18 +40,48 @@ function cacheKey(task) {
|
|
|
38
40
|
// same function (xenova's pipeline returns a callable that accepts either),
|
|
39
41
|
// so we don't pay the ONNX model-load cost twice.
|
|
40
42
|
let _extractorPromise = null;
|
|
43
|
+
/**
|
|
44
|
+
* ADR-454: distinguishes a null-extractor result caused by a genuinely
|
|
45
|
+
* missing dependency (permanent — nothing changes without a reinstall) from
|
|
46
|
+
* one caused by an environmental/fixable failure during `pipeline(...)`
|
|
47
|
+
* init, such as EACCES on the model cache dir (retryable).
|
|
48
|
+
*/
|
|
49
|
+
let _extractorDegradedReason = null;
|
|
50
|
+
let _extractorLastAttemptAt = 0;
|
|
51
|
+
const EXTRACTOR_RETRY_INTERVAL_MS = 60_000;
|
|
41
52
|
function loadExtractor() {
|
|
42
|
-
if (_extractorPromise !== null)
|
|
43
|
-
|
|
53
|
+
if (_extractorPromise !== null) {
|
|
54
|
+
const isRetryableDue = _extractorDegradedReason === 'retryable' &&
|
|
55
|
+
Date.now() - _extractorLastAttemptAt >= EXTRACTOR_RETRY_INTERVAL_MS;
|
|
56
|
+
if (!isRetryableDue)
|
|
57
|
+
return _extractorPromise;
|
|
58
|
+
// Fall through to re-attempt — the retryable failure may now be fixed.
|
|
59
|
+
}
|
|
60
|
+
_extractorLastAttemptAt = Date.now();
|
|
44
61
|
_extractorPromise = (async () => {
|
|
62
|
+
let packageImportable = false;
|
|
45
63
|
try {
|
|
46
64
|
const specifier = '@xenova/transformers';
|
|
47
65
|
const mod = await import(/* @vite-ignore */ specifier).catch(() => null);
|
|
48
|
-
if (!mod || typeof mod.pipeline !== 'function')
|
|
66
|
+
if (!mod || typeof mod.pipeline !== 'function') {
|
|
67
|
+
_extractorDegradedReason = 'permanent';
|
|
49
68
|
return null;
|
|
50
|
-
|
|
69
|
+
}
|
|
70
|
+
packageImportable = true;
|
|
71
|
+
// ADR-454: route model downloads to a user-writable cache dir — the
|
|
72
|
+
// package-relative default is root-owned under a system-prefix
|
|
73
|
+
// global install, which throws EACCES for the runtime user.
|
|
74
|
+
const cacheDir = resolveModelCacheDir();
|
|
75
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
76
|
+
const env = mod.env;
|
|
77
|
+
if (env)
|
|
78
|
+
env.cacheDir = cacheDir;
|
|
79
|
+
const extractor = await mod.pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { quantized: true });
|
|
80
|
+
_extractorDegradedReason = null;
|
|
81
|
+
return extractor;
|
|
51
82
|
}
|
|
52
83
|
catch {
|
|
84
|
+
_extractorDegradedReason = packageImportable ? 'retryable' : 'permanent';
|
|
53
85
|
return null;
|
|
54
86
|
}
|
|
55
87
|
})();
|
|
@@ -233,5 +265,7 @@ export function __resetTaskEmbedderForTests() {
|
|
|
233
265
|
_hits = 0;
|
|
234
266
|
_misses = 0;
|
|
235
267
|
_extractorPromise = null;
|
|
268
|
+
_extractorDegradedReason = null;
|
|
269
|
+
_extractorLastAttemptAt = 0;
|
|
236
270
|
}
|
|
237
271
|
//# sourceMappingURL=task-embedder.js.map
|
|
@@ -10,7 +10,12 @@ export interface BackupOptions {
|
|
|
10
10
|
/** Injected epoch millis (tests pass a fixed value; avoids Date.now in logic). */
|
|
11
11
|
timestamp?: number;
|
|
12
12
|
verbose?: boolean;
|
|
13
|
+
/** Test seam: inject a fake better-sqlite3-shaped constructor instead of importing the real module. */
|
|
14
|
+
Database?: new (path: string, opts?: Record<string, unknown>) => any;
|
|
13
15
|
}
|
|
16
|
+
/** Which method actually produced the snapshot — downstream migration tooling
|
|
17
|
+
* should require `native-online-backup` rather than trusting a blanket `backedUp: true`. */
|
|
18
|
+
export type BackupMethod = 'native-online-backup' | 'raw-copy-fallback';
|
|
14
19
|
export interface BackupResult {
|
|
15
20
|
backedUp: boolean;
|
|
16
21
|
path?: string;
|
|
@@ -18,6 +23,7 @@ export interface BackupResult {
|
|
|
18
23
|
rotatedAway?: string[];
|
|
19
24
|
gcsUri?: string;
|
|
20
25
|
skipped?: string;
|
|
26
|
+
method?: BackupMethod;
|
|
21
27
|
}
|
|
22
28
|
export declare function defaultMemoryDbPath(cwd?: string): string;
|
|
23
29
|
export interface ProjectBackupResult extends BackupResult {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import * as path from 'path';
|
|
17
|
-
import { readFileMaybeEncrypted } from '../fs-secure.js';
|
|
17
|
+
import { existingFileIsEncrypted, readFileMaybeEncrypted } from '../fs-secure.js';
|
|
18
18
|
import { loadBetterSqlite3 } from '../memory/shared-sqlite.js';
|
|
19
19
|
import { resolveMemoryRoot } from '../memory/memory-root.js';
|
|
20
20
|
export function defaultMemoryDbPath(cwd = process.cwd()) {
|
|
@@ -37,12 +37,18 @@ export async function backupProjectMemoryDbs(cwd, opts = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
const successful = backups.filter(result => result.backedUp);
|
|
39
39
|
const failed = backups.find(result => !result.backedUp && result.skipped !== 'no-db');
|
|
40
|
+
// Only report an aggregate `method` when every successful store agrees — a
|
|
41
|
+
// mixed native/raw-copy result must not collapse to whichever store
|
|
42
|
+
// happened to be first, the exact false-confidence #4287 is about.
|
|
43
|
+
const methods = new Set(successful.map(result => result.method));
|
|
44
|
+
const aggregateMethod = methods.size === 1 ? successful[0]?.method : undefined;
|
|
40
45
|
return {
|
|
41
46
|
...successful[0],
|
|
42
47
|
backedUp: successful.length > 0 && !failed,
|
|
43
48
|
sizeBytes: successful.reduce((sum, result) => sum + (result.sizeBytes ?? 0), 0),
|
|
44
49
|
rotatedAway: successful.flatMap(result => result.rotatedAway ?? []),
|
|
45
50
|
skipped: failed?.skipped ?? (successful.length === 0 ? 'no-db' : undefined),
|
|
51
|
+
method: aggregateMethod,
|
|
46
52
|
backups,
|
|
47
53
|
};
|
|
48
54
|
}
|
|
@@ -63,88 +69,25 @@ function isSnapshotFor(file, dbPath) {
|
|
|
63
69
|
return file.startsWith(prefix)
|
|
64
70
|
&& /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.db$/.test(file.slice(prefix.length));
|
|
65
71
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
/**
|
|
73
|
+
* A plain file copy of a live WAL-mode SQLite database is not a consistent
|
|
74
|
+
* snapshot — committed data can live only in the `-wal` sidecar until it's
|
|
75
|
+
* checkpointed back into the main file. We can't open an RFE1-encrypted
|
|
76
|
+
* source natively to run a real `wal_checkpoint` probe (it isn't a SQLite
|
|
77
|
+
* image), so for the raw-copy-fallback path the only signal available is
|
|
78
|
+
* sidecar presence: a non-empty `-wal` file means there are frames that a
|
|
79
|
+
* copy of the main file alone would silently drop. Fail closed on that.
|
|
80
|
+
*/
|
|
81
|
+
function hasActiveWal(dbPath) {
|
|
71
82
|
try {
|
|
72
|
-
|
|
73
|
-
Database = (await import(mod)).default;
|
|
83
|
+
return fs.statSync(`${dbPath}-wal`).size > 0;
|
|
74
84
|
}
|
|
75
85
|
catch {
|
|
76
|
-
return
|
|
86
|
+
return false;
|
|
77
87
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
catch { /* */ }
|
|
83
|
-
const destPath = path.join(destDir, `${snapshotPrefix(dbPath)}${fileStamp(opts.timestamp ?? Date.now())}.db`);
|
|
84
|
-
// WAL-safe online backup: read-only source, consistent snapshot to destPath.
|
|
85
|
-
let db;
|
|
86
|
-
try {
|
|
87
|
-
db = new Database(dbPath, { readonly: true });
|
|
88
|
-
await db.backup(destPath);
|
|
89
|
-
db.close();
|
|
90
|
-
}
|
|
91
|
-
catch (e) {
|
|
92
|
-
try {
|
|
93
|
-
db?.close();
|
|
94
|
-
}
|
|
95
|
-
catch { /* */ }
|
|
96
|
-
// #2798 — under CLAUDE_FLOW_ENCRYPT_AT_REST the source is an RFE1
|
|
97
|
-
// blob, not native SQLite, so better-sqlite3's online-backup API
|
|
98
|
-
// throws "file is not a database" and the run was silently recorded
|
|
99
|
-
// as skipped (users with encryption had ZERO backups behind a green
|
|
100
|
-
// status). The online-backup API only exists to avoid tearing a live
|
|
101
|
-
// WAL-mode DB; that concern doesn't apply to the encrypted file (sql.js
|
|
102
|
-
// rewrites it wholesale per flush), so a plain byte copy is a valid
|
|
103
|
-
// snapshot. Fall back to it before giving up.
|
|
104
|
-
try {
|
|
105
|
-
fs.copyFileSync(dbPath, destPath);
|
|
106
|
-
const copiedBytes = fs.statSync(destPath).size;
|
|
107
|
-
if (copiedBytes > 0) {
|
|
108
|
-
// Fall through to rotation/offsite/return below via a flag path.
|
|
109
|
-
// (Mirror the success path's tail by re-using the same code.)
|
|
110
|
-
const keep = typeof opts.keep === 'number' && opts.keep > 0 ? opts.keep : 7;
|
|
111
|
-
const rotatedAway = [];
|
|
112
|
-
try {
|
|
113
|
-
const snaps = fs.readdirSync(destDir).filter(f => isSnapshotFor(f, dbPath)).sort();
|
|
114
|
-
while (snaps.length > keep) {
|
|
115
|
-
const old = snaps.shift();
|
|
116
|
-
try {
|
|
117
|
-
fs.rmSync(path.join(destDir, old), { force: true });
|
|
118
|
-
rotatedAway.push(old);
|
|
119
|
-
}
|
|
120
|
-
catch { /* */ }
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch { /* */ }
|
|
124
|
-
let gcsUri;
|
|
125
|
-
if (opts.gcs) {
|
|
126
|
-
try {
|
|
127
|
-
const { execFileSync } = await import('child_process');
|
|
128
|
-
const dest = opts.gcs.replace(/\/+$/, '') + '/' + path.basename(destPath);
|
|
129
|
-
execFileSync('gcloud', ['storage', 'cp', destPath, dest], { stdio: ['ignore', 'ignore', 'inherit'] });
|
|
130
|
-
gcsUri = dest;
|
|
131
|
-
}
|
|
132
|
-
catch { /* offsite failed — local snapshot stands */ }
|
|
133
|
-
}
|
|
134
|
-
if (opts.verbose) {
|
|
135
|
-
console.log(`memory DB backed up (byte-copy, encrypted-at-rest) → ${destPath} (${Math.round(copiedBytes / 1024)} KB)`);
|
|
136
|
-
}
|
|
137
|
-
return { backedUp: true, path: destPath, sizeBytes: copiedBytes, rotatedAway, gcsUri };
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch { /* byte-copy also failed — report the original error */ }
|
|
141
|
-
return { backedUp: false, skipped: `backup failed: ${e?.message ?? e}` };
|
|
142
|
-
}
|
|
143
|
-
let sizeBytes = 0;
|
|
144
|
-
try {
|
|
145
|
-
sizeBytes = fs.statSync(destPath).size;
|
|
146
|
-
}
|
|
147
|
-
catch { /* */ }
|
|
88
|
+
}
|
|
89
|
+
/** Shared tail: rotation + optional GCS offsite + verbose logging + the success receipt. */
|
|
90
|
+
async function finishBackup(destPath, dbPath, destDir, opts, method, sizeBytes) {
|
|
148
91
|
// Rotation — ISO-stamped names sort chronologically, so keep the tail.
|
|
149
92
|
const keep = typeof opts.keep === 'number' && opts.keep > 0 ? opts.keep : 7;
|
|
150
93
|
const rotatedAway = [];
|
|
@@ -172,11 +115,96 @@ export async function backupMemoryDb(opts = {}) {
|
|
|
172
115
|
catch { /* offsite failed — local snapshot stands */ }
|
|
173
116
|
}
|
|
174
117
|
if (opts.verbose) {
|
|
175
|
-
|
|
118
|
+
const label = method === 'raw-copy-fallback' ? ' (byte-copy, encrypted-at-rest)' : '';
|
|
119
|
+
console.log(`memory DB backed up${label} → ${destPath} (${Math.round(sizeBytes / 1024)} KB)` +
|
|
176
120
|
(rotatedAway.length ? `, rotated ${rotatedAway.length} old` : '') +
|
|
177
121
|
(gcsUri ? `, offsite ${gcsUri}` : ''));
|
|
178
122
|
}
|
|
179
|
-
return { backedUp: true, path: destPath, sizeBytes, rotatedAway, gcsUri };
|
|
123
|
+
return { backedUp: true, path: destPath, sizeBytes, rotatedAway, gcsUri, method };
|
|
124
|
+
}
|
|
125
|
+
export async function backupMemoryDb(opts = {}) {
|
|
126
|
+
const dbPath = opts.dbPath ?? defaultMemoryDbPath();
|
|
127
|
+
if (!dbPath || !fs.existsSync(dbPath))
|
|
128
|
+
return { backedUp: false, skipped: 'no-db' };
|
|
129
|
+
const destDir = opts.destDir ?? path.join(path.dirname(dbPath), 'backups');
|
|
130
|
+
try {
|
|
131
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
catch { /* */ }
|
|
134
|
+
const destPath = path.join(destDir, `${snapshotPrefix(dbPath)}${fileStamp(opts.timestamp ?? Date.now())}.db`);
|
|
135
|
+
let Database = opts.Database;
|
|
136
|
+
let nativeError = null;
|
|
137
|
+
if (!Database) {
|
|
138
|
+
try {
|
|
139
|
+
const mod = 'better-sqlite3';
|
|
140
|
+
Database = (await import(mod)).default;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
nativeError = 'better-sqlite3 unavailable';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!nativeError) {
|
|
147
|
+
// WAL-safe online backup: read-only source, consistent snapshot to destPath.
|
|
148
|
+
let db;
|
|
149
|
+
try {
|
|
150
|
+
db = new Database(dbPath, { readonly: true });
|
|
151
|
+
await db.backup(destPath);
|
|
152
|
+
db.close();
|
|
153
|
+
let sizeBytes = 0;
|
|
154
|
+
try {
|
|
155
|
+
sizeBytes = fs.statSync(destPath).size;
|
|
156
|
+
}
|
|
157
|
+
catch { /* */ }
|
|
158
|
+
return await finishBackup(destPath, dbPath, destDir, opts, 'native-online-backup', sizeBytes);
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
try {
|
|
162
|
+
db?.close();
|
|
163
|
+
}
|
|
164
|
+
catch { /* */ }
|
|
165
|
+
try {
|
|
166
|
+
fs.rmSync(destPath, { force: true });
|
|
167
|
+
}
|
|
168
|
+
catch { /* a native backup() that throws mid-write can leave a torn destPath */ }
|
|
169
|
+
nativeError = e?.message ?? String(e);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// #2798 — under CLAUDE_FLOW_ENCRYPT_AT_REST the source is an RFE1 blob, not
|
|
173
|
+
// native SQLite, so better-sqlite3's online-backup API throws "file is not
|
|
174
|
+
// a database" (or isn't even importable on a WASM-only host). The
|
|
175
|
+
// online-backup API exists to avoid tearing a live WAL-mode DB; that
|
|
176
|
+
// concern doesn't apply to the encrypted file (sql.js rewrites it wholesale
|
|
177
|
+
// per flush), so a plain byte copy IS a valid snapshot there.
|
|
178
|
+
//
|
|
179
|
+
// But this fallback must never fire for an ORDINARY (non-encrypted) source
|
|
180
|
+
// just because the native backup threw — SQLITE_BUSY, a disk error, or any
|
|
181
|
+
// other native-backup failure on a perfectly normal database must fail
|
|
182
|
+
// closed, not silently report `backedUp: true` off a raw copy. Positively
|
|
183
|
+
// verify encryption (4-byte RFE1 magic sniff) before ever taking this path;
|
|
184
|
+
// "the native backup threw" is never by itself evidence the source is
|
|
185
|
+
// encrypted.
|
|
186
|
+
if (existingFileIsEncrypted(dbPath) !== true) {
|
|
187
|
+
return { backedUp: false, skipped: `native backup failed: ${nativeError}` };
|
|
188
|
+
}
|
|
189
|
+
// Even for a confirmed-encrypted source, refuse the raw-copy fallback if
|
|
190
|
+
// there's an active WAL — a plain copy of the main file alone would miss
|
|
191
|
+
// uncommitted WAL frames and silently produce an inconsistent snapshot.
|
|
192
|
+
if (hasActiveWal(dbPath)) {
|
|
193
|
+
return { backedUp: false, skipped: 'active WAL present — refusing raw-copy fallback (would be inconsistent)' };
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
fs.copyFileSync(dbPath, destPath);
|
|
197
|
+
const copiedBytes = fs.statSync(destPath).size;
|
|
198
|
+
if (copiedBytes > 0) {
|
|
199
|
+
return await finishBackup(destPath, dbPath, destDir, opts, 'raw-copy-fallback', copiedBytes);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch { /* byte-copy also failed — report the original native error below */ }
|
|
203
|
+
try {
|
|
204
|
+
fs.rmSync(destPath, { force: true });
|
|
205
|
+
}
|
|
206
|
+
catch { /* */ }
|
|
207
|
+
return { backedUp: false, skipped: `native backup failed: ${nativeError}` };
|
|
180
208
|
}
|
|
181
209
|
/** Verify a snapshot without modifying it, including RFE1 images and WASM-only hosts. */
|
|
182
210
|
async function verifiedBackupRows(file, Database) {
|
|
@@ -32,6 +32,8 @@ export type AnchorAssessment = {
|
|
|
32
32
|
};
|
|
33
33
|
export type AnchorWriter = (file: string, value: unknown) => Promise<void>;
|
|
34
34
|
export type StreamingAnchorWriter = (file: string, value: AtomicJsonSource) => Promise<void>;
|
|
35
|
+
export declare const ANCHOR_LOG_FILE = "ledger-anchors.json";
|
|
36
|
+
export declare const ANCHOR_MIRROR_FILE = "ledger-anchor-head.json";
|
|
35
37
|
export declare const anchorLogPath: (projectRoot: string) => string;
|
|
36
38
|
/**
|
|
37
39
|
* Directory holding per-project trust material (anchor mirror, HMAC key).
|
|
@@ -33,9 +33,9 @@ import { closeSync, constants as fsConstants, fstatSync, existsSync, mkdtempSync
|
|
|
33
33
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
34
34
|
import { homedir, tmpdir, userInfo } from 'node:os';
|
|
35
35
|
import { AgenticPolicyEngine, canonicalizePolicy } from '@claude-flow/security/policy/index';
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
export const anchorLogPath = (projectRoot) => join(resolve(projectRoot), '.claude-flow', 'policy',
|
|
36
|
+
export const ANCHOR_LOG_FILE = 'ledger-anchors.json';
|
|
37
|
+
export const ANCHOR_MIRROR_FILE = 'ledger-anchor-head.json';
|
|
38
|
+
export const anchorLogPath = (projectRoot) => join(resolve(projectRoot), '.claude-flow', 'policy', ANCHOR_LOG_FILE);
|
|
39
39
|
/**
|
|
40
40
|
* The home directory trust material lives under: $HOME when it is an absolute path, else the account's home. An empty or relative
|
|
41
41
|
* $HOME (an unset variable in a wrapper script, `HOME=. cmd`) would make join() yield a path relative to the working directory and
|
|
@@ -71,7 +71,7 @@ export function policyTrustRoot() {
|
|
|
71
71
|
}
|
|
72
72
|
function mirrorPath(projectRoot) {
|
|
73
73
|
const id = createHash('sha256').update(realpathSync(projectRoot)).digest('hex');
|
|
74
|
-
return join(policyTrustRoot(), id,
|
|
74
|
+
return join(policyTrustRoot(), id, ANCHOR_MIRROR_FILE);
|
|
75
75
|
}
|
|
76
76
|
// Duplicated from policy-runtime.ts's RECEIPT_ARCHIVE_FILE rather than
|
|
77
77
|
// imported, to avoid a circular dependency (policy-runtime.ts imports this
|