@claude-flow/cli 3.54.1 → 3.56.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/helpers.manifest.json +2 -2
- package/.claude/settings.json +9 -9
- package/bin/cli.js +1 -1
- package/bin/mcp-server.js +1 -1
- package/catalog-manifest.json +2 -2
- package/dist/src/auth/client.js +40 -24
- package/dist/src/commands/agent.js +12 -0
- package/dist/src/commands/analyze.js +18 -14
- package/dist/src/commands/autopilot.js +2 -2
- package/dist/src/commands/daemon.js +15 -3
- package/dist/src/commands/doctor.js +65 -58
- package/dist/src/commands/hive-mind.js +7 -0
- package/dist/src/commands/mcp.js +35 -1
- package/dist/src/commands/memory-backup.d.ts +1 -1
- package/dist/src/commands/memory-backup.js +23 -9
- package/dist/src/commands/memory.d.ts +0 -4
- package/dist/src/commands/memory.js +10 -2
- package/dist/src/commands/plugins.js +13 -1
- package/dist/src/commands/policy.js +10 -2
- package/dist/src/commands/task.js +12 -0
- package/dist/src/init/settings-generator.js +3 -3
- package/dist/src/mcp-server.d.ts +33 -0
- package/dist/src/mcp-server.js +96 -4
- package/dist/src/mcp-tools/agent-execute-core.js +244 -188
- package/dist/src/mcp-tools/agent-tools.js +15 -16
- package/dist/src/mcp-tools/agentdb-tools.js +22 -3
- package/dist/src/mcp-tools/claims-tools.js +3 -11
- package/dist/src/mcp-tools/hive-mind-tools.d.ts +2 -4
- package/dist/src/mcp-tools/hive-mind-tools.js +214 -40
- package/dist/src/mcp-tools/hooks-tools.js +47 -2
- package/dist/src/mcp-tools/memory-tools.js +29 -10
- package/dist/src/mcp-tools/record-store.d.ts +3 -0
- package/dist/src/mcp-tools/record-store.js +21 -0
- package/dist/src/mcp-tools/session-tools.js +47 -13
- package/dist/src/mcp-tools/task-tools.js +5 -11
- package/dist/src/mcp-tools/workflow-tools.js +78 -29
- package/dist/src/memory/append-conditions.d.ts +28 -0
- package/dist/src/memory/append-conditions.js +56 -0
- package/dist/src/memory/graph-edge-writer.d.ts +9 -0
- package/dist/src/memory/graph-edge-writer.js +90 -0
- package/dist/src/memory/memory-bridge.d.ts +4 -0
- package/dist/src/memory/memory-bridge.js +50 -9
- package/dist/src/memory/memory-initializer.d.ts +4 -0
- package/dist/src/memory/memory-initializer.js +95 -61
- package/dist/src/memory/memory-root.d.ts +2 -0
- package/dist/src/memory/memory-root.js +28 -0
- package/dist/src/memory/sona-optimizer.d.ts +2 -0
- package/dist/src/memory/sona-optimizer.js +4 -0
- package/dist/src/missions/schemas.d.ts +125 -125
- package/dist/src/mods/command-registry/schema.d.ts +199 -199
- package/dist/src/parser.d.ts +3 -0
- package/dist/src/parser.js +71 -13
- package/dist/src/plugins/store/discovery.d.ts +6 -0
- package/dist/src/plugins/store/discovery.js +7 -3
- package/dist/src/production/retry.js +1 -1
- package/dist/src/ruvector/neural-router.js +7 -2
- package/dist/src/services/bounded-worker-pool.js +3 -0
- package/dist/src/services/claim-service.js +39 -18
- package/dist/src/services/claims-store.d.ts +3 -0
- package/dist/src/services/claims-store.js +40 -0
- package/dist/src/services/config-file-manager.d.ts +3 -0
- package/dist/src/services/config-file-manager.js +24 -7
- package/dist/src/services/distill-tuning.js +17 -10
- package/dist/src/services/memory-backup.d.ts +7 -0
- package/dist/src/services/memory-backup.js +29 -2
- package/dist/src/services/memory-distillation.js +2 -2
- package/dist/src/services/policy-ledger-anchor.d.ts +42 -0
- package/dist/src/services/policy-ledger-anchor.js +163 -0
- package/dist/src/services/policy-runtime.d.ts +11 -5
- package/dist/src/services/policy-runtime.js +70 -15
- package/dist/src/services/worker-daemon.d.ts +5 -2
- package/dist/src/services/worker-daemon.js +26 -6
- package/dist/src/services/worker-queue.js +11 -2
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/mcp/dist/connection-pool.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/connection-pool.js +5 -2
- package/node_modules/@claude-flow/mcp/dist/connection-pool.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts +7 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/transport/http.js +60 -47
- package/node_modules/@claude-flow/mcp/dist/transport/http.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts +3 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js +4 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js +1 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts +2 -1
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +48 -16
- package/node_modules/@claude-flow/security/dist/oauth/callback-server.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/path-validator.d.ts +2 -2
- package/node_modules/@claude-flow/security/dist/path-validator.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/path-validator.js +15 -4
- package/node_modules/@claude-flow/security/dist/path-validator.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +11 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +19 -3
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +4 -2
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +7 -7
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
- package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
- package/plugins/ruflo-metaharness/scripts/_invoke.mjs +80 -7
- package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
- package/plugins/ruflo-metaharness/scripts/test-cli-resolution-3558.mjs +141 -0
- package/plugins/ruflo-metaharness/scripts/test-no-tool-time-npx.mjs +5 -0
- package/.claude/agents/tmp.json +0 -0
|
@@ -35,6 +35,87 @@ import * as crypto from 'crypto';
|
|
|
35
35
|
import { getMemoryRoot } from './memory-initializer.js';
|
|
36
36
|
import { encodeEmbedding } from './embedding-quantization.js';
|
|
37
37
|
import { loadBetterSqlite3 } from './shared-sqlite.js';
|
|
38
|
+
import { isEncryptedBlob } from '../encryption/vault.js';
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// #2889 — RFE1 encryption-at-rest guard
|
|
41
|
+
// ============================================================================
|
|
42
|
+
//
|
|
43
|
+
// When CLAUDE_FLOW_ENCRYPT_AT_REST is on, memory.db is an opaque RFE1 blob
|
|
44
|
+
// (see encryption/vault.ts). `new BetterSqlite3(dbPath)` has no RFE1 sniff
|
|
45
|
+
// and throws SQLITE_NOTADB on it, which the generic catch below swallows —
|
|
46
|
+
// indistinguishable from genuine corruption, a missing native module, or a
|
|
47
|
+
// permissions failure. That ambiguity is what let #2889 happen: nothing
|
|
48
|
+
// stopped a caller from concluding the store was broken and running the
|
|
49
|
+
// destructive `memory init`, which creates a fresh EMPTY store instead of
|
|
50
|
+
// using the existing encrypted data.
|
|
51
|
+
//
|
|
52
|
+
// Fix: sniff the header bytes (never buffer the whole file — memory.db can
|
|
53
|
+
// be multi-GB) before attempting the native open. If it's RFE1-encrypted,
|
|
54
|
+
// skip the native open (it would only throw) and warn once per distinct
|
|
55
|
+
// dbPath (not once per process — a process can legitimately touch more
|
|
56
|
+
// than one encrypted store via a custom `dbPath` argument).
|
|
57
|
+
/** Read just the first `len` bytes of a file without loading the whole file
|
|
58
|
+
* into memory. Mirrors the pattern in commands/doctor.ts's readHeaderBytes
|
|
59
|
+
* (not imported from there — this is a memory-layer module and must not
|
|
60
|
+
* depend on the command layer). */
|
|
61
|
+
function readHeaderBytes(filePath, len) {
|
|
62
|
+
const fd = fs.openSync(filePath, 'r');
|
|
63
|
+
try {
|
|
64
|
+
const buf = Buffer.alloc(len);
|
|
65
|
+
const bytesRead = fs.readSync(fd, buf, 0, len, 0);
|
|
66
|
+
return buf.subarray(0, bytesRead);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
fs.closeSync(fd);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// RFE1 wire format is magic(4) + iv(12) + ciphertext(N) + tag(16); the
|
|
73
|
+
// shortest possible blob is 32 bytes. 64 gives headroom.
|
|
74
|
+
const ENCRYPTION_SNIFF_LEN = 64;
|
|
75
|
+
/** True iff `filePath` is a legitimately RFE1-encrypted-at-rest file. A read
|
|
76
|
+
* failure (permissions, races) is treated as "not encrypted" — the caller's
|
|
77
|
+
* own open/query error handling still runs right after. */
|
|
78
|
+
function isRfe1EncryptedAtRest(filePath) {
|
|
79
|
+
try {
|
|
80
|
+
return isEncryptedBlob(readHeaderBytes(filePath, ENCRYPTION_SNIFF_LEN));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Keyed by resolved path (not a single process-wide boolean) — a process
|
|
87
|
+
// that touches more than one RFE1-encrypted dbPath (a custom `dbPath` passed
|
|
88
|
+
// to insertGraphEdge/queryEdgesBySource/countGraphEdges, or any multi-store
|
|
89
|
+
// scenario) must still get the warning for EACH distinct path, not just the
|
|
90
|
+
// first one it happens to see.
|
|
91
|
+
const _warnedRfe1EncryptedPaths = new Set();
|
|
92
|
+
function warnRfe1EncryptedOnce(dbPath) {
|
|
93
|
+
const resolved = path.resolve(dbPath);
|
|
94
|
+
if (_warnedRfe1EncryptedPaths.has(resolved))
|
|
95
|
+
return;
|
|
96
|
+
_warnedRfe1EncryptedPaths.add(resolved);
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.error(`[graph-edge-writer] ${dbPath} is encrypted at rest (CLAUDE_FLOW_ENCRYPT_AT_REST). ` +
|
|
99
|
+
'The native graph-edge bridge cannot open an RFE1-encrypted store directly — ' +
|
|
100
|
+
'this is a known limitation, not corruption. Your existing data is NOT lost ' +
|
|
101
|
+
'or damaged. Do NOT run `memory init` in response to this — that would create ' +
|
|
102
|
+
'a fresh, empty store instead of using your existing encrypted data. ' +
|
|
103
|
+
'See #2889 (https://github.com/ruvnet/ruflo/issues/2889) for details and recovery options.');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* True iff the memory.db bridge at `customDbPath` (or the default location)
|
|
107
|
+
* is currently RFE1-encrypted-at-rest. Callers that receive `null` from
|
|
108
|
+
* getBridgeDb()/insertGraphEdge() can use this to avoid recommending the
|
|
109
|
+
* destructive `memory init` command for this specific, non-corrupt case.
|
|
110
|
+
*/
|
|
111
|
+
export function isBridgeDbEncryptedAtRest(customDbPath) {
|
|
112
|
+
const dbPath = customDbPath ?? path.join(getMemoryRoot(), 'memory.db');
|
|
113
|
+
return fs.existsSync(dbPath) && isRfe1EncryptedAtRest(dbPath);
|
|
114
|
+
}
|
|
115
|
+
/** Test-only: clear the once-per-path warning dedup set. */
|
|
116
|
+
export function _resetRfe1WarningFlag() {
|
|
117
|
+
_warnedRfe1EncryptedPaths.clear();
|
|
118
|
+
}
|
|
38
119
|
// ============================================================================
|
|
39
120
|
// Lazy-cached better-sqlite3 db handle
|
|
40
121
|
// ============================================================================
|
|
@@ -134,6 +215,15 @@ export async function getBridgeDb(customDbPath, opts) {
|
|
|
134
215
|
const dbExists = fs.existsSync(dbPath);
|
|
135
216
|
if (!dbExists && !createIfMissing)
|
|
136
217
|
return null;
|
|
218
|
+
// #2889 — detect RFE1 encryption-at-rest before even attempting the
|
|
219
|
+
// native open: `new BetterSqlite3(dbPath)` on an encrypted file would
|
|
220
|
+
// only throw SQLITE_NOTADB, caught by the generic catch below with no
|
|
221
|
+
// way to tell it apart from genuine corruption. Skip straight to a
|
|
222
|
+
// clear, actionable warning instead.
|
|
223
|
+
if (dbExists && isRfe1EncryptedAtRest(dbPath)) {
|
|
224
|
+
warnRfe1EncryptedOnce(dbPath);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
137
227
|
// Ensure parent dir exists for createIfMissing case.
|
|
138
228
|
if (!dbExists && createIfMissing) {
|
|
139
229
|
const dir = path.dirname(dbPath);
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module v3/cli/memory-bridge
|
|
18
18
|
*/
|
|
19
|
+
import { type AppendCondition } from './append-conditions.js';
|
|
19
20
|
/**
|
|
20
21
|
* #3024: AgentDB's optional native controller stack can abort the whole Node
|
|
21
22
|
* process on Windows during registry initialization (a Rust allocation panic),
|
|
@@ -67,6 +68,9 @@ export declare function bridgeStoreEntry(options: {
|
|
|
67
68
|
ttl?: number;
|
|
68
69
|
dbPath?: string;
|
|
69
70
|
upsert?: boolean;
|
|
71
|
+
requireNative?: boolean;
|
|
72
|
+
appendOnly?: boolean;
|
|
73
|
+
appendConditions?: AppendCondition[];
|
|
70
74
|
/** ADR-323: defaults to 'unknown' when omitted. */
|
|
71
75
|
provenanceType?: string;
|
|
72
76
|
}): Promise<{
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module v3/cli/memory-bridge
|
|
18
18
|
*/
|
|
19
|
+
import { AppendConditionFailed, assertAppendConditions, validateAppendConditions } from './append-conditions.js';
|
|
19
20
|
import { liveMemoryRowSql } from './live-memory-row.js';
|
|
20
21
|
import { resolveAgentdbBetterSqlite3 } from './shared-sqlite.js';
|
|
21
22
|
import { encodeEmbeddingQ8, MAX_LIST_EMBEDDINGS } from './embedding-q8.js';
|
|
@@ -1025,13 +1026,24 @@ export async function bridgeStoreEntry(options) {
|
|
|
1025
1026
|
const ctx = getDb(registry);
|
|
1026
1027
|
if (!ctx)
|
|
1027
1028
|
return null;
|
|
1029
|
+
if (options.appendOnly && !options.requireNative) {
|
|
1030
|
+
return { success: false, id: '', error: 'Immutable append requires a native writer' };
|
|
1031
|
+
}
|
|
1032
|
+
if (options.appendConditions && (!options.requireNative || !options.appendOnly)) {
|
|
1033
|
+
return { success: false, id: "", error: "Conditional append requires native append-only storage" };
|
|
1034
|
+
}
|
|
1035
|
+
if (options.requireNative && (ctx.agentdb?.isWasm === true || typeof ctx.db.inTransaction !== 'boolean'
|
|
1036
|
+
|| typeof ctx.db.transaction !== 'function' || ctx.db.memory !== false || typeof ctx.db.name !== 'string'
|
|
1037
|
+
|| !ctx.db.name || canonicalDbPath(ctx.db.name) !== canonicalDbPath(options.dbPath))) {
|
|
1038
|
+
return { success: false, id: '', error: 'Native memory writer required; refusing non-native bridge driver' };
|
|
1039
|
+
}
|
|
1028
1040
|
try {
|
|
1029
1041
|
const { key, value, namespace = 'default', tags = [], ttl } = options;
|
|
1030
1042
|
let provenanceType = options.provenanceType ?? 'unknown';
|
|
1031
1043
|
// An omitted type on an upsert means "update the value", not "erase the
|
|
1032
1044
|
// existing trust label". New rows still receive the backward-compatible
|
|
1033
1045
|
// unknown default.
|
|
1034
|
-
if (options.upsert && options.provenanceType === undefined) {
|
|
1046
|
+
if (options.upsert && !options.appendOnly && options.provenanceType === undefined) {
|
|
1035
1047
|
try {
|
|
1036
1048
|
const existing = ctx.db.prepare('SELECT provenance_type FROM memory_entries WHERE namespace = ? AND key = ? LIMIT 1').get(namespace, key);
|
|
1037
1049
|
provenanceType = existing?.provenance_type || 'unknown';
|
|
@@ -1089,7 +1101,12 @@ export async function bridgeStoreEntry(options) {
|
|
|
1089
1101
|
// below → typed "already exists"
|
|
1090
1102
|
// error, NEVER a null demotion).
|
|
1091
1103
|
// Upsert path (INSERT OR REPLACE) is unchanged.
|
|
1092
|
-
const
|
|
1104
|
+
const immutableInsertSql = `INSERT INTO memory_entries (
|
|
1105
|
+
id, key, namespace, content, type,
|
|
1106
|
+
embedding, embedding_dimensions, embedding_model,
|
|
1107
|
+
tags, metadata, provenance_type, created_at, updated_at, expires_at, status
|
|
1108
|
+
) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`;
|
|
1109
|
+
const insertSql = options.appendOnly ? immutableInsertSql : options.upsert
|
|
1093
1110
|
? `INSERT OR REPLACE INTO memory_entries (
|
|
1094
1111
|
id, key, namespace, content, type,
|
|
1095
1112
|
embedding, embedding_dimensions, embedding_model,
|
|
@@ -1126,7 +1143,28 @@ export async function bridgeStoreEntry(options) {
|
|
|
1126
1143
|
}
|
|
1127
1144
|
catch { /* vector_indexes may not exist on legacy DBs — fall through */ }
|
|
1128
1145
|
const stmt = ctx.db.prepare(insertSql);
|
|
1129
|
-
const
|
|
1146
|
+
const insert = () => stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', provenanceType, now, now, ttl ? now + (ttl * 1000) : null);
|
|
1147
|
+
let runResult;
|
|
1148
|
+
try {
|
|
1149
|
+
runResult = options.appendOnly
|
|
1150
|
+
? ctx.db.transaction(() => {
|
|
1151
|
+
// Legacy native tables may predate UNIQUE(namespace,key). Serialize the exact
|
|
1152
|
+
// logical-slot check with INSERT, including tombstones, instead of trusting DDL.
|
|
1153
|
+
if (ctx.db.prepare('SELECT key FROM memory_entries WHERE namespace = ? AND key = ?').all(namespace, key).length) {
|
|
1154
|
+
throw new AppendConditionFailed('immutable append rejected: logical key already exists');
|
|
1155
|
+
}
|
|
1156
|
+
if (options.appendConditions)
|
|
1157
|
+
assertAppendConditions(ctx.db, validateAppendConditions(options.appendConditions));
|
|
1158
|
+
return insert();
|
|
1159
|
+
}).immediate()
|
|
1160
|
+
: insert();
|
|
1161
|
+
}
|
|
1162
|
+
catch (error) {
|
|
1163
|
+
if (error instanceof AppendConditionFailed || error instanceof TypeError) {
|
|
1164
|
+
return { success: false, id: '', error: error.message };
|
|
1165
|
+
}
|
|
1166
|
+
throw error;
|
|
1167
|
+
}
|
|
1130
1168
|
// A completed native write proves the bridge is currently healthy. Do not
|
|
1131
1169
|
// retain a diagnostic from an earlier transient failure and append it to a
|
|
1132
1170
|
// later, unrelated sql.js fallback refusal.
|
|
@@ -1143,18 +1181,21 @@ export async function bridgeStoreEntry(options) {
|
|
|
1143
1181
|
error: `key "${key}" already exists in namespace "${namespace}" — pass upsert=true (--upsert on the CLI) to update it`,
|
|
1144
1182
|
};
|
|
1145
1183
|
}
|
|
1184
|
+
// A non-vector immutable INSERT cannot change the vector count, even with concurrent
|
|
1185
|
+
// writers. Unlike historical strict mode, it never resurrects a vector tombstone.
|
|
1146
1186
|
// #2558: keep `vector_indexes.total_vectors` accurate so status/tooling
|
|
1147
1187
|
// stop reporting "HNSW index: 0 vectors" while embedded entries exist.
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
.
|
|
1188
|
+
if (!(options.appendOnly && embeddingJson === null))
|
|
1189
|
+
try {
|
|
1190
|
+
ctx.db
|
|
1191
|
+
.prepare(`UPDATE vector_indexes SET
|
|
1151
1192
|
total_vectors = (SELECT COUNT(*) FROM memory_entries
|
|
1152
1193
|
WHERE namespace = ? AND embedding IS NOT NULL),
|
|
1153
1194
|
updated_at = ?
|
|
1154
1195
|
WHERE name = ?`)
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1196
|
+
.run(namespace, now, namespace);
|
|
1197
|
+
}
|
|
1198
|
+
catch { /* vector_indexes may not exist on legacy DBs — non-fatal */ }
|
|
1158
1199
|
// #2558: better-sqlite3 opens the DB in WAL mode and, for the small write
|
|
1159
1200
|
// volumes typical of CLI usage, may never reach the auto-checkpoint
|
|
1160
1201
|
// threshold — leaving committed rows only in the -wal file. WAL-blind
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module v3/cli/memory-initializer
|
|
10
10
|
*/
|
|
11
|
+
import type { AppendCondition } from './append-conditions.js';
|
|
11
12
|
import { type EmbeddingQ8 } from './embedding-q8.js';
|
|
12
13
|
/**
|
|
13
14
|
* ADR-323 — typed memory provenance. Distinguishes WHO/WHAT wrote a memory
|
|
@@ -442,6 +443,9 @@ export declare function storeEntry(options: {
|
|
|
442
443
|
ttl?: number;
|
|
443
444
|
dbPath?: string;
|
|
444
445
|
upsert?: boolean;
|
|
446
|
+
requireNative?: boolean;
|
|
447
|
+
appendOnly?: boolean;
|
|
448
|
+
appendConditions?: AppendCondition[];
|
|
445
449
|
/** ADR-323: defaults to 'unknown' when omitted. */
|
|
446
450
|
provenanceType?: string;
|
|
447
451
|
}): Promise<{
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* V3 Memory Initializer
|
|
3
|
-
* Properly initializes the memory database with sql.js (WASM SQLite)
|
|
4
|
-
* Includes pattern tables, vector embeddings, migration state tracking
|
|
5
|
-
*
|
|
6
|
-
* ADR-053: Routes through ControllerRegistry → AgentDB v3 when available,
|
|
7
|
-
* falls back to raw sql.js for backwards compatibility.
|
|
8
|
-
*
|
|
9
|
-
* @module v3/cli/memory-initializer
|
|
10
|
-
*/
|
|
11
1
|
import { loadBetterSqlite3 } from './shared-sqlite.js';
|
|
2
|
+
import { resolveMemoryRoot } from './memory-root.js';
|
|
12
3
|
import { liveMemoryRowSql } from './live-memory-row.js';
|
|
13
4
|
import { encodeEmbeddingQ8, MAX_LIST_EMBEDDINGS } from './embedding-q8.js';
|
|
14
5
|
import * as fs from 'fs';
|
|
@@ -59,25 +50,88 @@ function isRuvectorCoreResolvable() {
|
|
|
59
50
|
return _ruvectorCoreResolvable;
|
|
60
51
|
}
|
|
61
52
|
/**
|
|
62
|
-
* #2735 — before a whole-image sql.js read-modify-persist (export() +
|
|
63
|
-
* rename over the live database path), refuse if
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
53
|
+
* #2735 / #3161 — before a whole-image sql.js read-modify-persist (export() +
|
|
54
|
+
* rename over the live database path), refuse if a native (better-sqlite3)
|
|
55
|
+
* WAL connection is actually LIVE. `-wal`/`-shm` sidecar presence alone is
|
|
56
|
+
* not proof of that: #3161 found `ruflo doctor`'s readonly diagnostic
|
|
57
|
+
* connections leave orphaned sidecars behind after a plain `.close()` (WAL
|
|
58
|
+
* mode does not checkpoint on close unless the closing connection was the
|
|
59
|
+
* last holder in a state that triggers one), permanently blocking every
|
|
60
|
+
* later write in that project.
|
|
61
|
+
*
|
|
62
|
+
* Liveness is probed in two steps, because a single `wal_checkpoint`
|
|
63
|
+
* busy-check is not sufficient on its own:
|
|
70
64
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
65
|
+
* 1. Open our own probe connection and run `PRAGMA wal_checkpoint(TRUNCATE)`.
|
|
66
|
+
* `busy !== 0` means another connection genuinely holds a lock blocking
|
|
67
|
+
* the checkpoint RIGHT NOW (an open transaction) — unsafe, refuse
|
|
68
|
+
* immediately. This also truncates a genuinely-orphaned WAL as a side
|
|
69
|
+
* effect.
|
|
70
|
+
* 2. If `busy === 0`, close our probe connection and re-check whether the
|
|
71
|
+
* sidecars are still on disk. SQLite only deletes `-wal`/`-shm` when the
|
|
72
|
+
* LAST connection to the database closes. If nothing else is attached,
|
|
73
|
+
* OUR close is that last close and the sidecars vanish — safe, proceed.
|
|
74
|
+
* If they are still present after our own close, some OTHER connection
|
|
75
|
+
* is still attached (even idle, holding no lock — e.g. a foreign native
|
|
76
|
+
* handle that already finished its last statement), so step 1's
|
|
77
|
+
* busy-check alone would have missed it. Refuse.
|
|
78
|
+
*
|
|
79
|
+
* Step 2 is what makes this safe against an idle-but-attached native
|
|
80
|
+
* connection (an open handle holding no lock does not block checkpointing,
|
|
81
|
+
* so step 1 alone would report "safe" even though a real holder is still
|
|
82
|
+
* attached) without resorting to platform-specific process scanning.
|
|
83
|
+
*
|
|
84
|
+
* The common case (no sidecars at all) stays a couple of cheap `existsSync`
|
|
85
|
+
* calls — a DB connection is only opened when a sidecar is actually present
|
|
86
|
+
* to investigate.
|
|
87
|
+
*
|
|
88
|
+
* This is still a scoped-down version of the fuller "scan live process
|
|
89
|
+
* holders" design discussed in #2735: it does not close the narrow
|
|
90
|
+
* assess-then-write race (a native opener could still attach in the gap
|
|
91
|
+
* between this check and the write), but it directly closes the
|
|
92
|
+
* demonstrated corruption mechanism — a whole-image write proceeding while
|
|
93
|
+
* an ALREADY-OPEN native connection genuinely holds the file — with no
|
|
94
|
+
* platform-specific process scanning. Fails closed (treats any probe error
|
|
95
|
+
* as "unsafe") because this is a safety gate, not a best-effort probe.
|
|
79
96
|
*/
|
|
80
|
-
function hasNativeWalSidecars(dbPath) {
|
|
97
|
+
async function hasNativeWalSidecars(dbPath) {
|
|
98
|
+
try {
|
|
99
|
+
if (!fs.existsSync(`${dbPath}-wal`) && !fs.existsSync(`${dbPath}-shm`)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
let db;
|
|
107
|
+
try {
|
|
108
|
+
const Database = await loadBetterSqlite3();
|
|
109
|
+
const opened = new Database(dbPath);
|
|
110
|
+
db = opened;
|
|
111
|
+
// Report busy immediately instead of retrying for the driver's default
|
|
112
|
+
// (multi-second) busy_timeout — a live holder's checkpoint-blocking
|
|
113
|
+
// lock is a steady-state fact at probe time, not a transient we should
|
|
114
|
+
// wait out, and this is a safety gate on a hot write path.
|
|
115
|
+
opened.pragma('busy_timeout = 0');
|
|
116
|
+
const [{ busy }] = opened.pragma('wal_checkpoint(TRUNCATE)');
|
|
117
|
+
if (busy !== 0)
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
try {
|
|
125
|
+
db?.close();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// best-effort
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Nothing was mid-transaction at probe time, and our own (now-closed)
|
|
132
|
+
// probe connection just truncated the WAL. If the sidecars are still
|
|
133
|
+
// here, another connection is still attached and kept them alive through
|
|
134
|
+
// our close.
|
|
81
135
|
try {
|
|
82
136
|
return fs.existsSync(`${dbPath}-wal`) || fs.existsSync(`${dbPath}-shm`);
|
|
83
137
|
}
|
|
@@ -123,34 +177,7 @@ let _memoryRootCache;
|
|
|
123
177
|
export function getMemoryRoot() {
|
|
124
178
|
if (_memoryRootCache !== undefined)
|
|
125
179
|
return _memoryRootCache;
|
|
126
|
-
|
|
127
|
-
const envPath = process.env.CLAUDE_FLOW_MEMORY_PATH;
|
|
128
|
-
if (envPath && envPath.trim().length > 0) {
|
|
129
|
-
_memoryRootCache = path.resolve(envPath);
|
|
130
|
-
return _memoryRootCache;
|
|
131
|
-
}
|
|
132
|
-
// 2. Config file (claude-flow.config.json)
|
|
133
|
-
const configCandidates = [
|
|
134
|
-
path.resolve(process.cwd(), 'claude-flow.config.json'),
|
|
135
|
-
path.resolve(process.cwd(), '.claude-flow', 'config.json'),
|
|
136
|
-
];
|
|
137
|
-
for (const configPath of configCandidates) {
|
|
138
|
-
if (!fs.existsSync(configPath))
|
|
139
|
-
continue;
|
|
140
|
-
try {
|
|
141
|
-
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
142
|
-
const fromConfig = raw?.memory?.persistPath ?? raw?.memory?.path;
|
|
143
|
-
if (typeof fromConfig === 'string' && fromConfig.trim().length > 0) {
|
|
144
|
-
_memoryRootCache = path.resolve(fromConfig);
|
|
145
|
-
return _memoryRootCache;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
/* malformed config — fall through to default */
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// 3. Default
|
|
153
|
-
_memoryRootCache = path.resolve(process.cwd(), '.swarm');
|
|
180
|
+
_memoryRootCache = resolveMemoryRoot(process.cwd());
|
|
154
181
|
return _memoryRootCache;
|
|
155
182
|
}
|
|
156
183
|
/** For tests + the `memory configure` flow that mutates the config at runtime. */
|
|
@@ -2532,6 +2559,9 @@ export async function verifyMemoryInit(dbPath, options) {
|
|
|
2532
2559
|
* This bypasses MCP and writes directly to the database
|
|
2533
2560
|
*/
|
|
2534
2561
|
export async function storeEntry(options) {
|
|
2562
|
+
if (options.appendOnly && !options.requireNative) {
|
|
2563
|
+
return { success: false, id: '', error: 'Immutable append requires a native writer' };
|
|
2564
|
+
}
|
|
2535
2565
|
// ADR-323: validate before touching either backend so an invalid value
|
|
2536
2566
|
// gets one clear error instead of a raw SQLite CHECK-constraint failure
|
|
2537
2567
|
// from whichever path (bridge vs sql.js) happens to run.
|
|
@@ -2563,8 +2593,12 @@ export async function storeEntry(options) {
|
|
|
2563
2593
|
return bridgeResult;
|
|
2564
2594
|
}
|
|
2565
2595
|
}
|
|
2596
|
+
if (options.requireNative || options.appendConditions) {
|
|
2597
|
+
return { success: false, id: '', error: 'Native memory writer required; refusing whole-image sql.js fallback' };
|
|
2598
|
+
}
|
|
2566
2599
|
// Fallback: raw sql.js
|
|
2567
|
-
const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert = false, provenanceType } = options;
|
|
2600
|
+
const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert: requestedUpsert = false, provenanceType } = options;
|
|
2601
|
+
const upsert = requestedUpsert && !options.appendOnly;
|
|
2568
2602
|
const swarmDir = getMemoryRoot();
|
|
2569
2603
|
const dbPath = customPath ? path.resolve(customPath) : path.join(swarmDir, 'memory.db');
|
|
2570
2604
|
try {
|
|
@@ -2578,7 +2612,7 @@ export async function storeEntry(options) {
|
|
|
2578
2612
|
// race). This check gates ensureSchemaColumns()'s own whole-image
|
|
2579
2613
|
// write below too, not just this function's.
|
|
2580
2614
|
await releaseOwnNativeHandle(dbPath);
|
|
2581
|
-
if (hasNativeWalSidecars(dbPath)) {
|
|
2615
|
+
if (await hasNativeWalSidecars(dbPath)) {
|
|
2582
2616
|
return {
|
|
2583
2617
|
success: false,
|
|
2584
2618
|
id: '',
|
|
@@ -2985,7 +3019,7 @@ export async function listEntries(options) {
|
|
|
2985
3019
|
// Listing can migrate/backfill the schema, so it is also a whole-image
|
|
2986
3020
|
// writer. The newly selected native mirror may still have a live WAL.
|
|
2987
3021
|
await releaseOwnNativeHandle(dbPath);
|
|
2988
|
-
if (hasNativeWalSidecars(dbPath)) {
|
|
3022
|
+
if (await hasNativeWalSidecars(dbPath)) {
|
|
2989
3023
|
return { success: false, entries: [], total: 0, error: await walRefusalError('read/write') };
|
|
2990
3024
|
}
|
|
2991
3025
|
// Ensure schema has all required columns (migration for older DBs)
|
|
@@ -3097,7 +3131,7 @@ export async function getEntry(options) {
|
|
|
3097
3131
|
// bump is itself a whole-image write, not a lightweight read, even
|
|
3098
3132
|
// though this function's contract reads as a "get".
|
|
3099
3133
|
await releaseOwnNativeHandle(dbPath);
|
|
3100
|
-
if (hasNativeWalSidecars(dbPath)) {
|
|
3134
|
+
if (await hasNativeWalSidecars(dbPath)) {
|
|
3101
3135
|
return {
|
|
3102
3136
|
success: false,
|
|
3103
3137
|
found: false,
|
|
@@ -3217,7 +3251,7 @@ export async function deleteEntry(options) {
|
|
|
3217
3251
|
// #2735 — see storeEntry's identical gate for the corruption mechanism
|
|
3218
3252
|
// this closes.
|
|
3219
3253
|
await releaseOwnNativeHandle(dbPath);
|
|
3220
|
-
if (hasNativeWalSidecars(dbPath)) {
|
|
3254
|
+
if (await hasNativeWalSidecars(dbPath)) {
|
|
3221
3255
|
return {
|
|
3222
3256
|
success: false,
|
|
3223
3257
|
deleted: false,
|
|
@@ -3437,7 +3471,7 @@ export async function purgeNamespace(options) {
|
|
|
3437
3471
|
}
|
|
3438
3472
|
// Recheck at mutation time even when the CLI already read a preview.
|
|
3439
3473
|
await releaseOwnNativeHandle(dbPath);
|
|
3440
|
-
if (hasNativeWalSidecars(dbPath)) {
|
|
3474
|
+
if (await hasNativeWalSidecars(dbPath)) {
|
|
3441
3475
|
return { success: false, deletedCount: 0, remainingEntries: 0, error: await walRefusalError('write') };
|
|
3442
3476
|
}
|
|
3443
3477
|
await ensureSchemaColumns(dbPath, options);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Uncached memory-directory authority for an explicit project directory. */
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
export function resolveMemoryRoot(cwd) {
|
|
5
|
+
const envPath = process.env.CLAUDE_FLOW_MEMORY_PATH;
|
|
6
|
+
if (envPath && envPath.trim().length > 0)
|
|
7
|
+
return path.resolve(cwd, envPath);
|
|
8
|
+
const configCandidates = [
|
|
9
|
+
path.resolve(cwd, 'claude-flow.config.json'),
|
|
10
|
+
path.resolve(cwd, '.claude-flow', 'config.json'),
|
|
11
|
+
];
|
|
12
|
+
for (const configPath of configCandidates) {
|
|
13
|
+
if (!fs.existsSync(configPath))
|
|
14
|
+
continue;
|
|
15
|
+
try {
|
|
16
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
17
|
+
const fromConfig = raw?.memory?.persistPath ?? raw?.memory?.path;
|
|
18
|
+
if (typeof fromConfig === 'string' && fromConfig.trim().length > 0) {
|
|
19
|
+
return path.resolve(cwd, fromConfig);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* malformed config — fall through to default */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return path.resolve(cwd, '.swarm');
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=memory-root.js.map
|
|
@@ -93,6 +93,8 @@ export interface SONAStats {
|
|
|
93
93
|
agents: number;
|
|
94
94
|
} | 'unavailable';
|
|
95
95
|
}
|
|
96
|
+
/** Agent roles supported by the SONA routing optimizer. */
|
|
97
|
+
export declare function isSupportedSonaAgent(agent: string): boolean;
|
|
96
98
|
/**
|
|
97
99
|
* SONA Optimizer for adaptive routing based on trajectory outcomes
|
|
98
100
|
*
|
|
@@ -61,6 +61,10 @@ const AGENT_TYPES = [
|
|
|
61
61
|
'security-architect',
|
|
62
62
|
'performance-engineer',
|
|
63
63
|
];
|
|
64
|
+
/** Agent roles supported by the SONA routing optimizer. */
|
|
65
|
+
export function isSupportedSonaAgent(agent) {
|
|
66
|
+
return AGENT_TYPES.includes(agent);
|
|
67
|
+
}
|
|
64
68
|
/**
|
|
65
69
|
* Task keywords for pattern extraction
|
|
66
70
|
*/
|