@claude-flow/cli 3.32.33 → 3.32.35
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/catalog-manifest.json +4 -4
- package/dist/src/commands/agent.js +49 -1
- package/dist/src/commands/hooks.js +24 -0
- package/dist/src/commands/metaharness.js +4 -0
- package/dist/src/commands/swarm.js +61 -5
- package/dist/src/config-adapter.js +1 -0
- package/dist/src/index.js +3 -2
- package/dist/src/mcp-tools/agent-tools.js +10 -0
- package/dist/src/mcp-tools/hooks-tools.js +86 -52
- package/dist/src/mcp-tools/memory-tools.js +14 -2
- package/dist/src/mcp-tools/metaharness-tools.js +7 -0
- package/dist/src/mcp-tools/swarm-tools.d.ts +16 -0
- package/dist/src/mcp-tools/swarm-tools.js +172 -7
- package/dist/src/memory/memory-bridge.d.ts +42 -0
- package/dist/src/memory/memory-bridge.js +144 -40
- package/dist/src/memory/memory-initializer.d.ts +9 -1
- package/dist/src/memory/memory-initializer.js +95 -31
- package/dist/src/services/daemon-autostart.js +7 -4
- package/dist/src/services/flywheel-receipt.d.ts +3 -0
- package/dist/src/services/flywheel-receipt.js +1 -0
- package/dist/src/services/harness-flywheel-runtime.d.ts +6 -0
- package/dist/src/services/harness-flywheel-runtime.js +19 -19
- package/dist/src/services/harness-flywheel.d.ts +2 -0
- package/dist/src/services/harness-flywheel.js +1 -0
- package/dist/src/services/harness-project-anchor.d.ts +23 -0
- package/dist/src/services/harness-project-anchor.js +129 -0
- package/dist/src/services/learned-routing.d.ts +34 -0
- package/dist/src/services/learned-routing.js +85 -0
- package/dist/src/services/pheromone-adaptive.d.ts +71 -0
- package/dist/src/services/pheromone-adaptive.js +214 -0
- package/dist/src/services/worker-daemon.js +4 -1
- package/package.json +1 -1
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
* Tool definitions for swarm coordination with file-based state persistence.
|
|
5
5
|
* Replaces previous stub implementations with real state tracking.
|
|
6
6
|
*/
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { getProjectCwd } from './types.js';
|
|
10
10
|
import { validateIdentifier } from './validate-input.js';
|
|
11
|
+
import { createApscState, isApscAgentEligible, normalizeApscConfig, recordApscSignal, } from '../services/pheromone-adaptive.js';
|
|
11
12
|
// Swarm state persistence
|
|
12
13
|
const SWARM_DIR = '.claude-flow/swarm';
|
|
13
14
|
const SWARM_STATE_FILE = 'swarm-state.json';
|
|
15
|
+
const SWARM_STATE_LOCK = 'swarm-state.lock';
|
|
16
|
+
const SWARM_LOCK_STALE_MS = 10_000;
|
|
14
17
|
function getSwarmDir() {
|
|
15
18
|
return join(getProjectCwd(), SWARM_DIR);
|
|
16
19
|
}
|
|
@@ -103,12 +106,101 @@ export function loadSwarmStore() {
|
|
|
103
106
|
}
|
|
104
107
|
export function saveSwarmStore(store) {
|
|
105
108
|
ensureSwarmDir();
|
|
106
|
-
|
|
109
|
+
const target = getSwarmStatePath();
|
|
110
|
+
const temporary = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
111
|
+
try {
|
|
112
|
+
writeFileSync(temporary, JSON.stringify(store, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
113
|
+
renameSync(temporary, target);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
try {
|
|
117
|
+
unlinkSync(temporary);
|
|
118
|
+
}
|
|
119
|
+
catch { /* already renamed or never created */ }
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function withSwarmStoreLock(operation) {
|
|
124
|
+
ensureSwarmDir();
|
|
125
|
+
const lockPath = join(getSwarmDir(), SWARM_STATE_LOCK);
|
|
126
|
+
let descriptor;
|
|
127
|
+
const waiter = new Int32Array(new SharedArrayBuffer(4));
|
|
128
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
129
|
+
try {
|
|
130
|
+
descriptor = openSync(lockPath, 'wx', 0o600);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code !== 'EEXIST')
|
|
135
|
+
throw error;
|
|
136
|
+
try {
|
|
137
|
+
if (Date.now() - statSync(lockPath).mtimeMs > SWARM_LOCK_STALE_MS) {
|
|
138
|
+
unlinkSync(lockPath);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch { /* another writer released it */ }
|
|
143
|
+
Atomics.wait(waiter, 0, 0, 10);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (descriptor === undefined)
|
|
147
|
+
throw new Error('swarm state is busy; retry the outcome update');
|
|
148
|
+
try {
|
|
149
|
+
return operation();
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
try {
|
|
153
|
+
closeSync(descriptor);
|
|
154
|
+
}
|
|
155
|
+
catch { /* best effort */ }
|
|
156
|
+
try {
|
|
157
|
+
unlinkSync(lockPath);
|
|
158
|
+
}
|
|
159
|
+
catch { /* best effort */ }
|
|
160
|
+
}
|
|
107
161
|
}
|
|
108
162
|
// Input validation
|
|
109
163
|
const VALID_TOPOLOGIES = new Set([
|
|
110
|
-
'hierarchical', 'mesh', 'hierarchical-mesh', 'ring', 'star', 'hybrid', 'adaptive',
|
|
164
|
+
'hierarchical', 'mesh', 'hierarchical-mesh', 'ring', 'star', 'hybrid', 'adaptive', 'pheromone-adaptive',
|
|
111
165
|
]);
|
|
166
|
+
function latestRunningSwarm(store) {
|
|
167
|
+
return Object.values(store.swarms)
|
|
168
|
+
.filter((swarm) => swarm.status === 'running')
|
|
169
|
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())[0];
|
|
170
|
+
}
|
|
171
|
+
function apscStateOf(swarm) {
|
|
172
|
+
if (!swarm || swarm.topology !== 'pheromone-adaptive')
|
|
173
|
+
return undefined;
|
|
174
|
+
const state = swarm.config.apscState;
|
|
175
|
+
return state?.schemaVersion ? state : undefined;
|
|
176
|
+
}
|
|
177
|
+
/** Shared by hooks_post-task so outcome labels immediately drive APSC. */
|
|
178
|
+
export function recordSwarmPheromoneSignal(signal) {
|
|
179
|
+
return withSwarmStoreLock(() => {
|
|
180
|
+
const store = loadSwarmStore();
|
|
181
|
+
const swarm = latestRunningSwarm(store);
|
|
182
|
+
const current = apscStateOf(swarm);
|
|
183
|
+
if (!swarm || !current)
|
|
184
|
+
return { active: false, reason: 'no running pheromone-adaptive swarm' };
|
|
185
|
+
const result = recordApscSignal(current, signal);
|
|
186
|
+
swarm.config.apscState = result.state;
|
|
187
|
+
swarm.updatedAt = new Date().toISOString();
|
|
188
|
+
saveSwarmStore(store);
|
|
189
|
+
return { active: true, swarmId: swarm.swarmId, decision: result.decision };
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/** Scheduling gate used by agent_execute. Dry-run topologies remain eligible. */
|
|
193
|
+
export function pheromoneAgentEligibility(agentId) {
|
|
194
|
+
const current = apscStateOf(latestRunningSwarm(loadSwarmStore()));
|
|
195
|
+
if (!current)
|
|
196
|
+
return { eligible: true, active: false };
|
|
197
|
+
const eligible = isApscAgentEligible(current, agentId);
|
|
198
|
+
return {
|
|
199
|
+
eligible,
|
|
200
|
+
active: true,
|
|
201
|
+
reason: eligible ? undefined : 'agent suspended by pheromone-adaptive scheduling gate',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
112
204
|
export const swarmTools = [
|
|
113
205
|
{
|
|
114
206
|
name: 'swarm_init',
|
|
@@ -117,7 +209,7 @@ export const swarmTools = [
|
|
|
117
209
|
inputSchema: {
|
|
118
210
|
type: 'object',
|
|
119
211
|
properties: {
|
|
120
|
-
topology: { type: 'string', description: 'Swarm topology type (hierarchical, mesh, hierarchical-mesh, ring, star, hybrid, adaptive)' },
|
|
212
|
+
topology: { type: 'string', description: 'Swarm topology type (hierarchical, mesh, hierarchical-mesh, ring, star, hybrid, adaptive, pheromone-adaptive)' },
|
|
121
213
|
maxAgents: { type: 'number', description: 'Maximum number of agents (1-50)' },
|
|
122
214
|
strategy: { type: 'string', description: 'Agent strategy (specialized, balanced, adaptive)' },
|
|
123
215
|
config: { type: 'object', description: 'Additional swarm configuration' },
|
|
@@ -145,6 +237,19 @@ export const swarmTools = [
|
|
|
145
237
|
error: `Invalid topology: ${topology}. Valid: ${[...VALID_TOPOLOGIES].join(', ')}`,
|
|
146
238
|
};
|
|
147
239
|
}
|
|
240
|
+
let apscState;
|
|
241
|
+
if (topology === 'pheromone-adaptive') {
|
|
242
|
+
try {
|
|
243
|
+
const apscConfig = normalizeApscConfig((config.apsc ?? {}));
|
|
244
|
+
if (apscConfig.minActiveAgents > maxAgents) {
|
|
245
|
+
return { success: false, error: 'APSC minActiveAgents cannot exceed maxAgents' };
|
|
246
|
+
}
|
|
247
|
+
apscState = createApscState(apscConfig);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
return { success: false, error: `Invalid APSC config: ${error.message}` };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
148
253
|
const swarmId = `swarm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
149
254
|
const now = new Date().toISOString();
|
|
150
255
|
const swarmState = {
|
|
@@ -161,12 +266,14 @@ export const swarmTools = [
|
|
|
161
266
|
communicationProtocol: config.communicationProtocol || 'message-bus',
|
|
162
267
|
autoScaling: config.autoScaling ?? true,
|
|
163
268
|
consensusMechanism: config.consensusMechanism || 'majority',
|
|
269
|
+
...(apscState ? { apscState } : {}),
|
|
164
270
|
},
|
|
165
271
|
createdAt: now,
|
|
166
272
|
updatedAt: now,
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
273
|
+
// A normal CLI invocation exits immediately after creating this
|
|
274
|
+
// coordination record; its PID is therefore not the swarm lifetime.
|
|
275
|
+
// Long-lived hosts may opt into PID ownership explicitly.
|
|
276
|
+
pid: config.trackHostProcess === true ? process.pid : undefined,
|
|
170
277
|
};
|
|
171
278
|
const store = loadSwarmStore();
|
|
172
279
|
store.swarms[swarmId] = swarmState;
|
|
@@ -180,6 +287,62 @@ export const swarmTools = [
|
|
|
180
287
|
initializedAt: now,
|
|
181
288
|
config: swarmState.config,
|
|
182
289
|
persisted: true,
|
|
290
|
+
apsc: apscState ? {
|
|
291
|
+
dryRun: apscState.config.dryRun,
|
|
292
|
+
minActiveAgents: apscState.config.minActiveAgents,
|
|
293
|
+
minSamples: apscState.config.minSamples,
|
|
294
|
+
} : undefined,
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'swarm_pheromone_update',
|
|
300
|
+
description: 'Record a normalized per-agent task outcome for a running pheromone-adaptive swarm. Use when a static agent pool is wrong because repeated task evidence should update role-aware EMA fitness and produce a bounded keep/suspend/reactivate decision; native Task has no persistent swarm eligibility gate.',
|
|
301
|
+
category: 'swarm',
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties: {
|
|
305
|
+
agentId: { type: 'string', description: 'Stable tracked agent identifier' },
|
|
306
|
+
role: { type: 'string', description: 'Agent role used for role-local normalization' },
|
|
307
|
+
taskSuccess: { type: 'number', description: 'Observed task success in [0,1]' },
|
|
308
|
+
normalizedLatency: { type: 'number', description: 'Latency divided by the configured budget, clamped to [0,1]' },
|
|
309
|
+
consensusAlignment: { type: 'number', description: 'Agreement with the accepted consensus in [0,1]' },
|
|
310
|
+
},
|
|
311
|
+
required: ['agentId', 'role', 'taskSuccess', 'normalizedLatency', 'consensusAlignment'],
|
|
312
|
+
},
|
|
313
|
+
handler: async (input) => {
|
|
314
|
+
try {
|
|
315
|
+
return recordSwarmPheromoneSignal({
|
|
316
|
+
agentId: String(input.agentId),
|
|
317
|
+
role: String(input.role),
|
|
318
|
+
taskSuccess: Number(input.taskSuccess),
|
|
319
|
+
normalizedLatency: Number(input.normalizedLatency),
|
|
320
|
+
consensusAlignment: Number(input.consensusAlignment),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
return { active: false, reason: error.message };
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: 'swarm_pheromone_status',
|
|
330
|
+
description: 'Inspect the threshold, safety configuration, per-agent EMA scores, and scheduling eligibility of the active pheromone-adaptive swarm. Use when aggregate swarm status is wrong because calibration requires the exact APSC threshold and per-agent evidence before switching from dry-run to live suspension.',
|
|
331
|
+
category: 'swarm',
|
|
332
|
+
inputSchema: { type: 'object', properties: {} },
|
|
333
|
+
handler: async () => {
|
|
334
|
+
const store = loadSwarmStore();
|
|
335
|
+
const swarm = latestRunningSwarm(store);
|
|
336
|
+
const state = apscStateOf(swarm);
|
|
337
|
+
if (!swarm || !state)
|
|
338
|
+
return { active: false, reason: 'no running pheromone-adaptive swarm' };
|
|
339
|
+
return {
|
|
340
|
+
active: true,
|
|
341
|
+
swarmId: swarm.swarmId,
|
|
342
|
+
threshold: state.threshold,
|
|
343
|
+
round: state.round,
|
|
344
|
+
config: state.config,
|
|
345
|
+
agents: Object.values(state.agents).sort((a, b) => a.agentId.localeCompare(b.agentId)),
|
|
183
346
|
};
|
|
184
347
|
},
|
|
185
348
|
},
|
|
@@ -212,6 +375,7 @@ export const swarmTools = [
|
|
|
212
375
|
agentCount: swarm.agents.length,
|
|
213
376
|
taskCount: swarm.tasks.length,
|
|
214
377
|
config: swarm.config,
|
|
378
|
+
apsc: apscStateOf(swarm),
|
|
215
379
|
createdAt: swarm.createdAt,
|
|
216
380
|
updatedAt: swarm.updatedAt,
|
|
217
381
|
};
|
|
@@ -236,6 +400,7 @@ export const swarmTools = [
|
|
|
236
400
|
agentCount: latest.agents.length,
|
|
237
401
|
taskCount: latest.tasks.length,
|
|
238
402
|
config: latest.config,
|
|
403
|
+
apsc: apscStateOf(latest),
|
|
239
404
|
createdAt: latest.createdAt,
|
|
240
405
|
updatedAt: latest.updatedAt,
|
|
241
406
|
totalSwarms: swarmIds.length,
|
|
@@ -16,6 +16,29 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module v3/cli/memory-bridge
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* Should this init-time log line be swallowed?
|
|
21
|
+
*
|
|
22
|
+
* A DEGRADATION notice never is. AgentDB logs "[AgentDB] better-sqlite3 not
|
|
23
|
+
* available, using sql.js WASM" when it falls back to WASM, and both the
|
|
24
|
+
* '[AgentDB]' and 'better-sqlite3' entries above match it — so the one line
|
|
25
|
+
* explaining why the native driver was not in use was being discarded as
|
|
26
|
+
* noise. Suppress the banners, keep the bad news.
|
|
27
|
+
*/
|
|
28
|
+
export declare function shouldSuppressInitLog(msg: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Create/migrate the bridge's `memory_entries` table on `db`.
|
|
31
|
+
*
|
|
32
|
+
* Returns true when the schema is known-good, false when the database was not
|
|
33
|
+
* writable (caller then leaves it un-ensured so a later writable call retries).
|
|
34
|
+
*
|
|
35
|
+
* Exported so the ADR-323 column migration can be tested directly: the bug it
|
|
36
|
+
* fixes was invisible end-to-end, because the resulting error was swallowed and
|
|
37
|
+
* reported as an unrelated WAL-sidecar refusal.
|
|
38
|
+
*/
|
|
39
|
+
export declare function ensureBridgeSchema(db: {
|
|
40
|
+
exec: (sql: string) => unknown;
|
|
41
|
+
}): boolean;
|
|
19
42
|
/**
|
|
20
43
|
* Store an entry via AgentDB v3.
|
|
21
44
|
* Phase 2-5: Routes through MutationGuard → TieredCache → DB → AttestationLog.
|
|
@@ -239,8 +262,27 @@ export declare function isBridgeAvailable(dbPath?: string): Promise<boolean>;
|
|
|
239
262
|
* Get the ControllerRegistry instance (for advanced consumers).
|
|
240
263
|
*/
|
|
241
264
|
export declare function getControllerRegistry(dbPath?: string): Promise<any | null>;
|
|
265
|
+
/**
|
|
266
|
+
* Why the bridge last declined a write, or null when it has not.
|
|
267
|
+
*
|
|
268
|
+
* Deliberately NOT gated on `bridgeAvailable === false`. A bridge that
|
|
269
|
+
* initialised fine can still fail every write — a schema mismatch throws
|
|
270
|
+
* per-operation while the registry stays healthy — and that case is exactly
|
|
271
|
+
* the one worth reporting, since the caller then demotes to a fallback whose
|
|
272
|
+
* error message describes something else entirely.
|
|
273
|
+
*
|
|
274
|
+
* Callers that surface a degraded-path error should include this so the
|
|
275
|
+
* operator learns the cause instead of only the symptom.
|
|
276
|
+
*/
|
|
277
|
+
export declare function getBridgeFailureReason(): string | null;
|
|
242
278
|
/**
|
|
243
279
|
* Shutdown the bridge and release resources.
|
|
280
|
+
*
|
|
281
|
+
* The cached state is cleared unconditionally. Previously the reset lived
|
|
282
|
+
* inside `if (registryInstance)`, so it could not clear a FAILED init — the
|
|
283
|
+
* one state that actually needs clearing, since `registryInstance` is null
|
|
284
|
+
* precisely when init failed. A process that latched `bridgeAvailable = false`
|
|
285
|
+
* therefore had no recovery path short of a restart.
|
|
244
286
|
*/
|
|
245
287
|
export declare function shutdownBridge(): Promise<void>;
|
|
246
288
|
/**
|
|
@@ -23,6 +23,17 @@ import { createRequire } from 'node:module';
|
|
|
23
23
|
let registryPromise = null;
|
|
24
24
|
let registryInstance = null;
|
|
25
25
|
let bridgeAvailable = null;
|
|
26
|
+
/**
|
|
27
|
+
* Why the bridge is unavailable, when it is.
|
|
28
|
+
*
|
|
29
|
+
* `bridgeAvailable = false` latches for the life of the process, so a single
|
|
30
|
+
* transient init failure (a slow Xenova/ONNX fetch, a locked db) routes every
|
|
31
|
+
* later write to the sql.js whole-image fallback — which then refuses whenever
|
|
32
|
+
* -wal/-shm sidecars are present. Without this, that refusal is the only
|
|
33
|
+
* symptom the caller ever sees, and it names a cause ("restore the native
|
|
34
|
+
* better-sqlite3 bridge") the caller has no way to check.
|
|
35
|
+
*/
|
|
36
|
+
let bridgeFailureReason = null;
|
|
26
37
|
/**
|
|
27
38
|
* ADR-323: reuse memory-initializer's provenance-type allowlist rather than
|
|
28
39
|
* duplicating it (drift risk). Lazy CJS require for the same circular-ESM-
|
|
@@ -109,6 +120,31 @@ function getAgentDbPath() {
|
|
|
109
120
|
function generateId(prefix) {
|
|
110
121
|
return `${prefix}_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
|
|
111
122
|
}
|
|
123
|
+
/** Noisy init banners that carry no operational signal. */
|
|
124
|
+
const INIT_LOG_NOISE = [
|
|
125
|
+
'Transformers.js',
|
|
126
|
+
'better-sqlite3',
|
|
127
|
+
'[AgentDB]',
|
|
128
|
+
'[HNSWLibBackend]',
|
|
129
|
+
'RuVector graph',
|
|
130
|
+
];
|
|
131
|
+
/**
|
|
132
|
+
* Should this init-time log line be swallowed?
|
|
133
|
+
*
|
|
134
|
+
* A DEGRADATION notice never is. AgentDB logs "[AgentDB] better-sqlite3 not
|
|
135
|
+
* available, using sql.js WASM" when it falls back to WASM, and both the
|
|
136
|
+
* '[AgentDB]' and 'better-sqlite3' entries above match it — so the one line
|
|
137
|
+
* explaining why the native driver was not in use was being discarded as
|
|
138
|
+
* noise. Suppress the banners, keep the bad news.
|
|
139
|
+
*/
|
|
140
|
+
export function shouldSuppressInitLog(msg) {
|
|
141
|
+
const degradation = msg.includes('not available')
|
|
142
|
+
|| msg.includes('falling back')
|
|
143
|
+
|| msg.includes('fallback');
|
|
144
|
+
if (degradation)
|
|
145
|
+
return false;
|
|
146
|
+
return INIT_LOG_NOISE.some((needle) => msg.includes(needle));
|
|
147
|
+
}
|
|
112
148
|
/**
|
|
113
149
|
* Lazily initialize the ControllerRegistry singleton.
|
|
114
150
|
* Returns null if @claude-flow/memory is not available.
|
|
@@ -123,15 +159,12 @@ async function getRegistry(dbPath) {
|
|
|
123
159
|
try {
|
|
124
160
|
const { ControllerRegistry } = await import('@claude-flow/memory');
|
|
125
161
|
const registry = new ControllerRegistry();
|
|
126
|
-
// Suppress noisy console.log during init
|
|
162
|
+
// Suppress noisy console.log during init — but never suppress a
|
|
163
|
+
// DEGRADATION notice (see shouldSuppressInitLog).
|
|
127
164
|
const origLog = console.log;
|
|
128
165
|
console.log = (...args) => {
|
|
129
166
|
const msg = String(args[0] ?? '');
|
|
130
|
-
if (msg
|
|
131
|
-
msg.includes('better-sqlite3') ||
|
|
132
|
-
msg.includes('[AgentDB]') ||
|
|
133
|
-
msg.includes('[HNSWLibBackend]') ||
|
|
134
|
-
msg.includes('RuVector graph'))
|
|
167
|
+
if (shouldSuppressInitLog(msg))
|
|
135
168
|
return;
|
|
136
169
|
origLog.apply(console, args);
|
|
137
170
|
};
|
|
@@ -387,9 +420,14 @@ async function getRegistry(dbPath) {
|
|
|
387
420
|
}
|
|
388
421
|
registryInstance = registry;
|
|
389
422
|
bridgeAvailable = true;
|
|
423
|
+
bridgeFailureReason = null;
|
|
390
424
|
return registry;
|
|
391
425
|
}
|
|
392
|
-
catch {
|
|
426
|
+
catch (err) {
|
|
427
|
+
// Record WHY. This latches for the process lifetime (see the
|
|
428
|
+
// bridgeFailureReason doc comment), so discarding the error here
|
|
429
|
+
// makes the resulting sql.js-fallback refusal undiagnosable.
|
|
430
|
+
bridgeFailureReason = err instanceof Error ? err.message : String(err);
|
|
393
431
|
bridgeAvailable = false;
|
|
394
432
|
registryPromise = null;
|
|
395
433
|
return null;
|
|
@@ -529,6 +567,70 @@ async function logAttestation(registry, operation, entryId, metadata) {
|
|
|
529
567
|
// runs the CREATE…IF NOT EXISTS block at most once per handle instead of on
|
|
530
568
|
// every bridge call. WeakSet so handles GC without leaking.
|
|
531
569
|
const _schemaEnsuredDbs = new WeakSet();
|
|
570
|
+
/**
|
|
571
|
+
* Create/migrate the bridge's `memory_entries` table on `db`.
|
|
572
|
+
*
|
|
573
|
+
* Returns true when the schema is known-good, false when the database was not
|
|
574
|
+
* writable (caller then leaves it un-ensured so a later writable call retries).
|
|
575
|
+
*
|
|
576
|
+
* Exported so the ADR-323 column migration can be tested directly: the bug it
|
|
577
|
+
* fixes was invisible end-to-end, because the resulting error was swallowed and
|
|
578
|
+
* reported as an unrelated WAL-sidecar refusal.
|
|
579
|
+
*/
|
|
580
|
+
export function ensureBridgeSchema(db) {
|
|
581
|
+
try {
|
|
582
|
+
db.exec(`CREATE TABLE IF NOT EXISTS memory_entries (
|
|
583
|
+
id TEXT PRIMARY KEY,
|
|
584
|
+
key TEXT NOT NULL,
|
|
585
|
+
namespace TEXT DEFAULT 'default',
|
|
586
|
+
content TEXT NOT NULL,
|
|
587
|
+
type TEXT DEFAULT 'semantic',
|
|
588
|
+
embedding TEXT,
|
|
589
|
+
embedding_model TEXT DEFAULT 'local',
|
|
590
|
+
embedding_dimensions INTEGER,
|
|
591
|
+
tags TEXT,
|
|
592
|
+
metadata TEXT,
|
|
593
|
+
owner_id TEXT,
|
|
594
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
595
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
596
|
+
expires_at INTEGER,
|
|
597
|
+
last_accessed_at INTEGER,
|
|
598
|
+
access_count INTEGER DEFAULT 0,
|
|
599
|
+
status TEXT DEFAULT 'active',
|
|
600
|
+
provenance_type TEXT DEFAULT 'unknown',
|
|
601
|
+
UNIQUE(namespace, key)
|
|
602
|
+
)`);
|
|
603
|
+
// ADR-323 added provenance_type to bridgeStoreEntry's INSERT, and
|
|
604
|
+
// ensureSchemaColumns() backfills it on the sql.js path — the bridge path
|
|
605
|
+
// had no equivalent. `CREATE TABLE IF NOT EXISTS` is a no-op on a table
|
|
606
|
+
// created before ADR-323, so on any pre-existing database every bridge
|
|
607
|
+
// write threw "table memory_entries has no column named provenance_type",
|
|
608
|
+
// was swallowed by the catch at the end of bridgeStoreEntry(), and demoted
|
|
609
|
+
// the caller to the sql.js whole-image path — which then refused with a
|
|
610
|
+
// WAL-sidecar error naming an unrelated cause. Silent, total write loss.
|
|
611
|
+
try {
|
|
612
|
+
db.exec(`ALTER TABLE memory_entries ADD COLUMN provenance_type TEXT DEFAULT 'unknown'`);
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
// Already present is the common case. SQLite has no ADD COLUMN IF NOT
|
|
616
|
+
// EXISTS, so attempt-and-ignore only that exact condition. Treating a
|
|
617
|
+
// read-only, locked, or corrupt database as migrated would cache a
|
|
618
|
+
// schema that bridgeStoreEntry() still cannot use.
|
|
619
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
620
|
+
if (!/duplicate column name:\s*provenance_type/i.test(msg)) {
|
|
621
|
+
throw err;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_ns ON memory_entries(namespace)`);
|
|
625
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_key ON memory_entries(key)`);
|
|
626
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_status ON memory_entries(status)`);
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// Read-only database — leave it un-ensured so a writable call can retry.
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
532
634
|
/**
|
|
533
635
|
* Get the AgentDB database handle and ensure memory_entries table exists.
|
|
534
636
|
* Returns null if not available.
|
|
@@ -543,37 +645,8 @@ function getDb(registry) {
|
|
|
543
645
|
// call (store/search/get) was pure per-op overhead. Keyed by handle via a
|
|
544
646
|
// WeakSet so a new db instance re-ensures without a stale global flag.
|
|
545
647
|
if (!_schemaEnsuredDbs.has(db)) {
|
|
546
|
-
|
|
547
|
-
db.exec(`CREATE TABLE IF NOT EXISTS memory_entries (
|
|
548
|
-
id TEXT PRIMARY KEY,
|
|
549
|
-
key TEXT NOT NULL,
|
|
550
|
-
namespace TEXT DEFAULT 'default',
|
|
551
|
-
content TEXT NOT NULL,
|
|
552
|
-
type TEXT DEFAULT 'semantic',
|
|
553
|
-
embedding TEXT,
|
|
554
|
-
embedding_model TEXT DEFAULT 'local',
|
|
555
|
-
embedding_dimensions INTEGER,
|
|
556
|
-
tags TEXT,
|
|
557
|
-
metadata TEXT,
|
|
558
|
-
owner_id TEXT,
|
|
559
|
-
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
560
|
-
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
561
|
-
expires_at INTEGER,
|
|
562
|
-
last_accessed_at INTEGER,
|
|
563
|
-
access_count INTEGER DEFAULT 0,
|
|
564
|
-
status TEXT DEFAULT 'active',
|
|
565
|
-
UNIQUE(namespace, key)
|
|
566
|
-
)`);
|
|
567
|
-
// Ensure indexes
|
|
568
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_ns ON memory_entries(namespace)`);
|
|
569
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_key ON memory_entries(key)`);
|
|
570
|
-
db.exec(`CREATE INDEX IF NOT EXISTS idx_bridge_status ON memory_entries(status)`);
|
|
648
|
+
if (ensureBridgeSchema(db))
|
|
571
649
|
_schemaEnsuredDbs.add(db);
|
|
572
|
-
}
|
|
573
|
-
catch {
|
|
574
|
-
// Table already exists or db is read-only — that's fine. Don't mark
|
|
575
|
-
// ensured on failure so a later writable call can retry.
|
|
576
|
-
}
|
|
577
650
|
}
|
|
578
651
|
// ─── #2256-followup: rescue agentdb.embedder when its transformers.js
|
|
579
652
|
// path fell through to mock embeddings.
|
|
@@ -785,6 +858,10 @@ export async function bridgeStoreEntry(options) {
|
|
|
785
858
|
catch { /* vector_indexes may not exist on legacy DBs — fall through */ }
|
|
786
859
|
const stmt = ctx.db.prepare(insertSql);
|
|
787
860
|
const runResult = 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);
|
|
861
|
+
// A completed native write proves the bridge is currently healthy. Do not
|
|
862
|
+
// retain a diagnostic from an earlier transient failure and append it to a
|
|
863
|
+
// later, unrelated sql.js fallback refusal.
|
|
864
|
+
bridgeFailureReason = null;
|
|
788
865
|
// #2775: strict insert against an ACTIVE existing row → changes === 0
|
|
789
866
|
// (the ON CONFLICT WHERE clause above suppressed the update). Surface
|
|
790
867
|
// this as a typed data-level error rather than a bridge failure —
|
|
@@ -858,6 +935,11 @@ export async function bridgeStoreEntry(options) {
|
|
|
858
935
|
error: `key "${options.key}" already exists in namespace "${options.namespace ?? 'default'}" — pass upsert=true (--upsert on the CLI) to update it`,
|
|
859
936
|
};
|
|
860
937
|
}
|
|
938
|
+
// Returning null below demotes the caller to the sql.js whole-image path,
|
|
939
|
+
// whose WAL-sidecar guard then reports a cause that has nothing to do with
|
|
940
|
+
// what actually went wrong here. Record the real error so it can be
|
|
941
|
+
// surfaced alongside that guard's message.
|
|
942
|
+
bridgeFailureReason = msg;
|
|
861
943
|
return null;
|
|
862
944
|
}
|
|
863
945
|
}
|
|
@@ -1562,8 +1644,29 @@ export async function isBridgeAvailable(dbPath) {
|
|
|
1562
1644
|
export async function getControllerRegistry(dbPath) {
|
|
1563
1645
|
return getRegistry(dbPath);
|
|
1564
1646
|
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Why the bridge last declined a write, or null when it has not.
|
|
1649
|
+
*
|
|
1650
|
+
* Deliberately NOT gated on `bridgeAvailable === false`. A bridge that
|
|
1651
|
+
* initialised fine can still fail every write — a schema mismatch throws
|
|
1652
|
+
* per-operation while the registry stays healthy — and that case is exactly
|
|
1653
|
+
* the one worth reporting, since the caller then demotes to a fallback whose
|
|
1654
|
+
* error message describes something else entirely.
|
|
1655
|
+
*
|
|
1656
|
+
* Callers that surface a degraded-path error should include this so the
|
|
1657
|
+
* operator learns the cause instead of only the symptom.
|
|
1658
|
+
*/
|
|
1659
|
+
export function getBridgeFailureReason() {
|
|
1660
|
+
return bridgeFailureReason;
|
|
1661
|
+
}
|
|
1565
1662
|
/**
|
|
1566
1663
|
* Shutdown the bridge and release resources.
|
|
1664
|
+
*
|
|
1665
|
+
* The cached state is cleared unconditionally. Previously the reset lived
|
|
1666
|
+
* inside `if (registryInstance)`, so it could not clear a FAILED init — the
|
|
1667
|
+
* one state that actually needs clearing, since `registryInstance` is null
|
|
1668
|
+
* precisely when init failed. A process that latched `bridgeAvailable = false`
|
|
1669
|
+
* therefore had no recovery path short of a restart.
|
|
1567
1670
|
*/
|
|
1568
1671
|
export async function shutdownBridge() {
|
|
1569
1672
|
if (registryInstance) {
|
|
@@ -1573,10 +1676,11 @@ export async function shutdownBridge() {
|
|
|
1573
1676
|
catch {
|
|
1574
1677
|
// Best-effort
|
|
1575
1678
|
}
|
|
1576
|
-
registryInstance = null;
|
|
1577
|
-
registryPromise = null;
|
|
1578
|
-
bridgeAvailable = null;
|
|
1579
1679
|
}
|
|
1680
|
+
registryInstance = null;
|
|
1681
|
+
registryPromise = null;
|
|
1682
|
+
bridgeAvailable = null;
|
|
1683
|
+
bridgeFailureReason = null;
|
|
1580
1684
|
}
|
|
1581
1685
|
// ===== Phase 3: ReasoningBank pattern operations =====
|
|
1582
1686
|
/**
|
|
@@ -36,7 +36,7 @@ export declare function resolveDbPath(cliFlag?: string): string;
|
|
|
36
36
|
* Vector embeddings enabled for semantic search
|
|
37
37
|
*/
|
|
38
38
|
export declare const MEMORY_SCHEMA_V3 = "\n-- RuFlo V3 Memory Database\n-- Version: 3.0.0\n-- Features: Pattern learning, vector embeddings, temporal decay, migration tracking\n\nPRAGMA journal_mode = WAL;\nPRAGMA synchronous = NORMAL;\nPRAGMA foreign_keys = ON;\n\n-- ============================================\n-- CORE MEMORY TABLES\n-- ============================================\n\n-- Memory entries (main storage)\nCREATE TABLE IF NOT EXISTS memory_entries (\n id TEXT PRIMARY KEY,\n key TEXT NOT NULL,\n namespace TEXT DEFAULT 'default',\n content TEXT NOT NULL,\n type TEXT DEFAULT 'semantic' CHECK(type IN ('semantic', 'episodic', 'procedural', 'working', 'pattern')),\n\n -- Vector embedding for semantic search (stored as JSON array)\n embedding TEXT,\n embedding_model TEXT DEFAULT 'local',\n embedding_dimensions INTEGER,\n\n -- Metadata\n tags TEXT, -- JSON array\n metadata TEXT, -- JSON object\n owner_id TEXT,\n\n -- ADR-323: who/what produced this entry \u2014 lets shared-namespace retrieval\n -- filter by trust level instead of conflating a user's stated claim with\n -- an agent's own output or a raw tool/system observation.\n provenance_type TEXT DEFAULT 'unknown' CHECK(provenance_type IN (\n 'user_claim', 'agent_output', 'system_observation', 'tool_result', 'unknown'\n )),\n\n -- Timestamps\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n expires_at INTEGER,\n last_accessed_at INTEGER,\n\n -- Access tracking for hot/cold detection\n access_count INTEGER DEFAULT 0,\n\n -- Status\n status TEXT DEFAULT 'active' CHECK(status IN ('active', 'archived', 'deleted')),\n\n UNIQUE(namespace, key)\n);\n\n-- Indexes for memory entries\nCREATE INDEX IF NOT EXISTS idx_memory_namespace ON memory_entries(namespace);\nCREATE INDEX IF NOT EXISTS idx_memory_key ON memory_entries(key);\nCREATE INDEX IF NOT EXISTS idx_memory_type ON memory_entries(type);\nCREATE INDEX IF NOT EXISTS idx_memory_status ON memory_entries(status);\nCREATE INDEX IF NOT EXISTS idx_memory_created ON memory_entries(created_at);\nCREATE INDEX IF NOT EXISTS idx_memory_accessed ON memory_entries(last_accessed_at);\nCREATE INDEX IF NOT EXISTS idx_memory_owner ON memory_entries(owner_id);\n\n-- ============================================\n-- PATTERN LEARNING TABLES\n-- ============================================\n\n-- Learned patterns with confidence scoring and versioning\nCREATE TABLE IF NOT EXISTS patterns (\n id TEXT PRIMARY KEY,\n\n -- Pattern identification\n name TEXT NOT NULL,\n pattern_type TEXT NOT NULL CHECK(pattern_type IN (\n 'task-routing', 'error-recovery', 'optimization', 'learning',\n 'coordination', 'prediction', 'code-pattern', 'workflow'\n )),\n\n -- Pattern definition\n condition TEXT NOT NULL, -- Regex or semantic match\n action TEXT NOT NULL, -- What to do when pattern matches\n description TEXT,\n\n -- Confidence scoring (0.0 - 1.0)\n confidence REAL DEFAULT 0.5,\n success_count INTEGER DEFAULT 0,\n failure_count INTEGER DEFAULT 0,\n\n -- Temporal decay\n decay_rate REAL DEFAULT 0.01, -- How fast confidence decays\n half_life_days INTEGER DEFAULT 30, -- Days until confidence halves without use\n\n -- Vector embedding for semantic pattern matching\n embedding TEXT,\n embedding_dimensions INTEGER,\n\n -- Versioning\n version INTEGER DEFAULT 1,\n parent_id TEXT REFERENCES patterns(id),\n\n -- Metadata\n tags TEXT, -- JSON array\n metadata TEXT, -- JSON object\n source TEXT, -- Where the pattern was learned from\n\n -- Timestamps\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n last_matched_at INTEGER,\n last_success_at INTEGER,\n last_failure_at INTEGER,\n\n -- Status\n status TEXT DEFAULT 'active' CHECK(status IN ('active', 'archived', 'deprecated', 'experimental'))\n);\n\n-- Indexes for patterns\nCREATE INDEX IF NOT EXISTS idx_patterns_type ON patterns(pattern_type);\nCREATE INDEX IF NOT EXISTS idx_patterns_confidence ON patterns(confidence DESC);\nCREATE INDEX IF NOT EXISTS idx_patterns_status ON patterns(status);\nCREATE INDEX IF NOT EXISTS idx_patterns_last_matched ON patterns(last_matched_at);\n\n-- Pattern evolution history (for versioning)\nCREATE TABLE IF NOT EXISTS pattern_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n pattern_id TEXT NOT NULL REFERENCES patterns(id),\n version INTEGER NOT NULL,\n\n -- Snapshot of pattern state\n confidence REAL,\n success_count INTEGER,\n failure_count INTEGER,\n condition TEXT,\n action TEXT,\n\n -- What changed\n change_type TEXT CHECK(change_type IN ('created', 'updated', 'success', 'failure', 'decay', 'merged', 'split')),\n change_reason TEXT,\n\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)\n);\n\nCREATE INDEX IF NOT EXISTS idx_pattern_history_pattern ON pattern_history(pattern_id);\n\n-- ============================================\n-- LEARNING & TRAJECTORY TABLES\n-- ============================================\n\n-- Learning trajectories (SONA integration)\nCREATE TABLE IF NOT EXISTS trajectories (\n id TEXT PRIMARY KEY,\n session_id TEXT,\n\n -- Trajectory state\n status TEXT DEFAULT 'active' CHECK(status IN ('active', 'completed', 'failed', 'abandoned')),\n verdict TEXT CHECK(verdict IN ('success', 'failure', 'partial', NULL)),\n\n -- Context\n task TEXT,\n context TEXT, -- JSON object\n\n -- Metrics\n total_steps INTEGER DEFAULT 0,\n total_reward REAL DEFAULT 0,\n\n -- Timestamps\n started_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n ended_at INTEGER,\n\n -- Reference to extracted pattern (if any)\n extracted_pattern_id TEXT REFERENCES patterns(id)\n);\n\n-- Trajectory steps\nCREATE TABLE IF NOT EXISTS trajectory_steps (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n trajectory_id TEXT NOT NULL REFERENCES trajectories(id),\n step_number INTEGER NOT NULL,\n\n -- Step data\n action TEXT NOT NULL,\n observation TEXT,\n reward REAL DEFAULT 0,\n\n -- Metadata\n metadata TEXT, -- JSON object\n\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)\n);\n\nCREATE INDEX IF NOT EXISTS idx_steps_trajectory ON trajectory_steps(trajectory_id);\n\n-- ============================================\n-- MIGRATION STATE TRACKING\n-- ============================================\n\n-- Migration state (for resume capability)\nCREATE TABLE IF NOT EXISTS migration_state (\n id TEXT PRIMARY KEY,\n migration_type TEXT NOT NULL, -- 'v2-to-v3', 'pattern', 'memory', etc.\n\n -- Progress tracking\n status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'failed', 'rolled_back')),\n total_items INTEGER DEFAULT 0,\n processed_items INTEGER DEFAULT 0,\n failed_items INTEGER DEFAULT 0,\n skipped_items INTEGER DEFAULT 0,\n\n -- Current position (for resume)\n current_batch INTEGER DEFAULT 0,\n last_processed_id TEXT,\n\n -- Source/destination info\n source_path TEXT,\n source_type TEXT,\n destination_path TEXT,\n\n -- Backup info\n backup_path TEXT,\n backup_created_at INTEGER,\n\n -- Error tracking\n last_error TEXT,\n errors TEXT, -- JSON array of errors\n\n -- Timestamps\n started_at INTEGER,\n completed_at INTEGER,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)\n);\n\n-- ============================================\n-- SESSION MANAGEMENT\n-- ============================================\n\n-- Sessions for context persistence\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n\n -- Session state\n state TEXT NOT NULL, -- JSON object with full session state\n status TEXT DEFAULT 'active' CHECK(status IN ('active', 'paused', 'completed', 'expired')),\n\n -- Context\n project_path TEXT,\n branch TEXT,\n\n -- Metrics\n tasks_completed INTEGER DEFAULT 0,\n patterns_learned INTEGER DEFAULT 0,\n\n -- Timestamps\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n expires_at INTEGER\n);\n\n-- ============================================\n-- VECTOR INDEX METADATA (for HNSW)\n-- ============================================\n\n-- Track HNSW index state\nCREATE TABLE IF NOT EXISTS vector_indexes (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n\n -- Index configuration\n dimensions INTEGER NOT NULL,\n metric TEXT DEFAULT 'cosine' CHECK(metric IN ('cosine', 'euclidean', 'dot')),\n\n -- HNSW parameters\n hnsw_m INTEGER DEFAULT 16,\n hnsw_ef_construction INTEGER DEFAULT 200,\n hnsw_ef_search INTEGER DEFAULT 100,\n\n -- Quantization\n quantization_type TEXT CHECK(quantization_type IN ('none', 'scalar', 'product')),\n quantization_bits INTEGER DEFAULT 8,\n\n -- Statistics\n total_vectors INTEGER DEFAULT 0,\n last_rebuild_at INTEGER,\n\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)\n);\n\n-- ============================================\n-- GRAPH EDGES (ADR-130 Phase 1)\n-- Unified knowledge graph backend \u2014 sql.js canonical store\n-- ============================================\n\n-- Unified graph edges table (ADR-130)\n-- Node IDs use domain-prefixed format: {domain}:{uuid}\n-- where domain in (mem, agent, task, entity, span, pattern)\nCREATE TABLE IF NOT EXISTS graph_edges (\n id TEXT PRIMARY KEY, -- edge-{uuid}\n source_id TEXT NOT NULL, -- domain-prefixed node ID\n target_id TEXT NOT NULL, -- domain-prefixed node ID\n relation TEXT NOT NULL, -- e.g. \"caused\", \"depends-on\", \"imports\"\n weight REAL DEFAULT 1.0,\n -- Temporal / reliability semantics (ADR-130 \u00A7\"graph that forgets\" property)\n confidence REAL DEFAULT 1.0, -- [0,1]; updated by JUDGE step\n decay_rate REAL DEFAULT 0.0, -- per-day exponential decay applied at read time\n last_reinforced TEXT, -- ISO-8601; set when CONSOLIDATE re-touches edge\n witness_id TEXT, -- FK to verification/witness-fixes.json (ADR-103)\n -- Embedding storage: \"inline:{base64}\" | \"vector_indexes:{id}\" | NULL\n embedding_ref TEXT,\n metadata TEXT, -- JSON blob for plugin-specific fields\n created_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_graph_edges_source ON graph_edges (source_id);\nCREATE INDEX IF NOT EXISTS idx_graph_edges_target ON graph_edges (target_id);\nCREATE INDEX IF NOT EXISTS idx_graph_edges_relation ON graph_edges (relation);\nCREATE INDEX IF NOT EXISTS idx_graph_edges_reinforced ON graph_edges (last_reinforced);\n\n-- ============================================\n-- SYSTEM METADATA\n-- ============================================\n\nCREATE TABLE IF NOT EXISTS metadata (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at INTEGER DEFAULT (strftime('%s', 'now') * 1000)\n);\n";
|
|
39
|
-
interface HNSWEntry {
|
|
39
|
+
export interface HNSWEntry {
|
|
40
40
|
id: string;
|
|
41
41
|
key: string;
|
|
42
42
|
namespace: string;
|
|
@@ -57,6 +57,14 @@ export declare function getHNSWIndex(options?: {
|
|
|
57
57
|
dimensions?: number;
|
|
58
58
|
forceRebuild?: boolean;
|
|
59
59
|
}): Promise<HNSWIndex | null>;
|
|
60
|
+
export declare function removeHNSWEntriesByLogicalKey(entries: Map<string, HNSWEntry>, key: string, namespace: string): number;
|
|
61
|
+
/**
|
|
62
|
+
* Remove HNSW metadata whose authoritative SQLite row is no longer active.
|
|
63
|
+
* Persistent graph nodes may remain physically allocated, but without metadata
|
|
64
|
+
* they cannot resolve into search results; the next rebuild repopulates only
|
|
65
|
+
* active rows. Returns the number of searchable vectors invalidated.
|
|
66
|
+
*/
|
|
67
|
+
export declare function reconcileHNSWIndex(dbPath?: string): Promise<number>;
|
|
60
68
|
/**
|
|
61
69
|
* Add entry to HNSW index (with automatic persistence)
|
|
62
70
|
*/
|