ruvnet-brain 4.6.1 → 4.6.3

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- Updated: 2026-10-10 10:41:47 EDT | Version 4.6.1
1
+ Updated: 2026-10-10 10:41:47 EDT | Version 4.6.2
2
2
  Created: 2026-06-29 22:36:38 EDT
3
3
 
4
4
  <div align="center">
@@ -9,7 +9,7 @@ Created: 2026-06-29 22:36:38 EDT
9
9
 
10
10
  ### Discover the stack. Read the source. Build with confidence.
11
11
 
12
- [![RuvNet Brain version 4.6.1 — README updated 2026-10-10 EDT](https://img.shields.io/badge/version_4.6.1-updated_2026--10--10_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](plugin/.claude-plugin/plugin.json)
12
+ [![RuvNet Brain version 4.6.2 — README updated 2026-10-10 EDT](https://img.shields.io/badge/version_4.6.3-updated_2026--10--10_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](plugin/.claude-plugin/plugin.json)
13
13
 
14
14
  **A source-grounded companion for Claude Code and Codex that helps you find and use Reuven Cohen's (rUv's) RuvNet ecosystem.**
15
15
 
@@ -259,7 +259,8 @@ The **4.6.0 published-corpus baseline** is **199 public stores · 162,680 public
259
259
  Those are bundle census values, not a claim that every upstream repository is fresh today.
260
260
  Later corpus publication may advance the knowledge generation independently of code changes.
261
261
 
262
- See [4.6.1 release notes](docs/RELEASE-NOTES-4.6.1.md) and
262
+ See [4.6.2 native update repair notes](docs/RELEASE-NOTES-4.6.2.md),
263
+ [4.6.1 README/admin notes](docs/RELEASE-NOTES-4.6.1.md) and
263
264
  [4.6.0 coordinated-update notes](docs/RELEASE-NOTES-4.6.0.md) and the authoritative
264
265
  [update and release policy](CONTRIBUTING.md).
265
266
 
@@ -80,7 +80,11 @@ try {
80
80
  if (registration.mode === 'developer-suite') {
81
81
  const coordinator = await import(pathToFileURL(modules['developer-update.mjs'].path).href);
82
82
  const receipt = await coordinator.runDeveloperUpdate({ mode: 'apply', env: { ...process.env,
83
- RUVNET_NIGHTLY_IDENTITY: registration.identity, RUVNET_NIGHTLY: '1' } });
83
+ RUVNET_NIGHTLY_IDENTITY: registration.identity, RUVNET_NIGHTLY: '1',
84
+ RUVNET_NIGHTLY_REGISTRATION: registrationPath,
85
+ RUVNET_NIGHTLY_NODE_PATH: registration.nodePath,
86
+ RUVNET_NIGHTLY_RUNNER_PATH: registration.runnerPath,
87
+ RUVNET_NIGHTLY_RUNNER_SHA256: registration.runnerSha256 } });
84
88
  console.log(JSON.stringify(receipt));
85
89
  process.exit(0);
86
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.6.1",
3
+ "version": "4.6.3",
4
4
  "description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
3
  "description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
4
- "version": "4.6.1",
4
+ "version": "4.6.3",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.6.1",
3
+ "version": "4.6.3",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -0,0 +1,118 @@
1
+ /** Owned, deadline-bound native reads and global Ruflo commands for canonical recall. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { rufloInvocation } from './ruflo-bin.mjs';
7
+ export function searchJsonResult(stdout) {
8
+ const s = String(stdout || '');
9
+ // Live Ruflo prints warnings AFTER the JSON object as well as logs before it.
10
+ const start = s.indexOf('{'); const end = s.lastIndexOf('}');
11
+ if (start < 0 || end < start) return { valid: false, rows: [] };
12
+ try { const rows = JSON.parse(s.slice(start, end + 1)).results;
13
+ return Array.isArray(rows) && rows.every(r => r && typeof r.key === 'string' && r.key)
14
+ ? { valid: true, rows } : { valid: false, rows: [] };
15
+ } catch { return { valid: false, rows: [] }; }
16
+ }
17
+
18
+ function killGroup(child) {
19
+ try { if (process.platform !== 'win32') process.kill(-child.pid, 'SIGKILL'); else child.kill('SIGKILL'); }
20
+ catch { try { child.kill('SIGKILL'); } catch { /* already gone */ } }
21
+ }
22
+
23
+ /** One ruflo search, bounded by an absolute deadline. Resolves { rows, state } — never rejects. */
24
+ export function searchOnce({ ruflo, store, args, deadline, env, scratch, operation = 'search', signal, invocation }) {
25
+ return new Promise((resolve) => {
26
+ const remaining = deadline - Date.now();
27
+ if (signal?.aborted || remaining <= 0) { resolve({ rows: [], state: signal?.aborted ? 'unavailable' : 'timed out' }); return; }
28
+ let cwd;
29
+ try { cwd = fs.mkdtempSync(path.join(scratch(store.path), 'run-')); } catch { resolve({ rows: [], state: 'unavailable' }); return; }
30
+ const cleanup = () => { try { fs.rmSync(cwd, { recursive: true, force: true }); } catch { /* swept later as a stale run- dir */ } };
31
+ let inv;
32
+ try { inv = invocation ?? rufloInvocation(ruflo, ['memory', operation, '--path', store.path, ...args]); }
33
+ catch { cleanup(); resolve({ rows: [], state: 'unavailable' }); return; }
34
+ let child;
35
+ try {
36
+ child = spawn(inv.executable, inv.args, { cwd, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_MEMORY_PATH: undefined },
37
+ stdio: ['ignore', 'pipe', 'ignore'], detached: process.platform !== 'win32', windowsHide: true });
38
+ } catch { cleanup(); resolve({ rows: [], state: 'unavailable' }); return; }
39
+ let out = '', outputBytes = 0;
40
+ const decoder = new TextDecoder('utf-8', { fatal: true });
41
+ let done = false, retiring = null, retirementTimer, timer;
42
+ const finish = (state, retirement = process.platform === 'win32' ? 'direct-child-only' : 'confirmed') => {
43
+ if (done) return;
44
+ done = true;
45
+ clearTimeout(timer);
46
+ clearTimeout(retirementTimer);
47
+ signal?.removeEventListener('abort', cancel);
48
+ cleanup();
49
+ const parsed = operation === 'search' ? searchJsonResult(out) : null;
50
+ if (state === 'ok' && parsed && !parsed.valid) state = 'unavailable';
51
+ resolve({ rows: state === 'ok' && parsed ? parsed.rows : [], value: state === 'ok' ? out.trim() : '', state, retirement });
52
+ };
53
+ const retire = state => {
54
+ if (done || retiring) return;
55
+ retiring = state; killGroup(child);
56
+ retirementTimer = setTimeout(() => finish('unavailable', 'unconfirmed'), Math.max(1, deadline - Date.now()));
57
+ };
58
+ const cancel = () => retire('unavailable');
59
+ signal?.addEventListener('abort', cancel, { once: true });
60
+ // Reserve cleanup inside the same caller deadline; pipe inheritance cannot report success.
61
+ timer = setTimeout(() => retire('timed out'), Math.max(1, deadline - Date.now() - 100));
62
+ child.stdout.on('data', c => { if (done || retiring) return; outputBytes += c.length;
63
+ if (outputBytes > 1 << 20) return retire('unavailable');
64
+ try { out += decoder.decode(c, { stream: true }); } catch { retire('unavailable'); } });
65
+ child.on('error', () => retire('unavailable'));
66
+ child.on('close', code => {
67
+ killGroup(child); // Successful parent exit must not leave an owned detached grandchild.
68
+ let valid = true; try { out += decoder.decode(); } catch { valid = false; }
69
+ const settle = () => {
70
+ if (done) return;
71
+ let alive = false;
72
+ if (process.platform !== 'win32') {
73
+ try { process.kill(-child.pid, 0); alive = true; } catch (error) { alive = error.code !== 'ESRCH'; }
74
+ }
75
+ if (alive && Date.now() < deadline) { retirementTimer = setTimeout(settle, 5); return; }
76
+ finish(alive || !valid ? 'unavailable' : retiring ?? (code === 0 ? 'ok' : 'failed'), alive ? 'unconfirmed' : undefined);
77
+ };
78
+ clearTimeout(retirementTimer); settle();
79
+ });
80
+ if (signal?.aborted) cancel();
81
+ });
82
+ }
83
+
84
+ /** Isolate synchronous read-only SQLite work inside the same cancellable process budget. */
85
+ export async function nativeRead({ store, requests, namespaces, deadline, env, scratch, signal }) {
86
+ const result = await searchOnce({ store, deadline, env, scratch, signal, operation: 'native',
87
+ invocation: { executable: process.execPath, args: [fileURLToPath(new URL('./agentdb-recall-reader.mjs', import.meta.url)),
88
+ store.path, JSON.stringify({ requests, namespaces, deadline })] } });
89
+ if (result.state !== 'ok') return { ok: false, fatal: true, state: result.state };
90
+ try {
91
+ const parsed = JSON.parse(result.value);
92
+ if (typeof parsed.ok !== 'boolean') throw new Error('invalid native result');
93
+ return { ...parsed, state: parsed.ok ? 'ok' : 'unavailable' };
94
+ } catch { return { ok: false, fatal: true, state: 'unavailable' }; }
95
+ }
96
+
97
+ export async function exactReads({ candidates, bin, deadline, env, scratch, signal }) {
98
+ const groups = new Map();
99
+ for (const candidate of candidates) {
100
+ const storePath = candidate.storePath;
101
+ if (!groups.has(storePath)) groups.set(storePath, []);
102
+ groups.get(storePath).push(candidate);
103
+ }
104
+ const results = new Map();
105
+ await Promise.all([...groups].map(async ([storePath, rows]) => {
106
+ const native = await nativeRead({ store: { path: storePath }, requests: rows.map(({ namespace, key }) => ({ namespace, key })),
107
+ deadline, env, scratch, signal });
108
+ await Promise.all(rows.map(async (row, index) => {
109
+ const result = native.ok ? { state: 'ok', value: native.value[index] ?? '' }
110
+ : native.fatal ? { state: native.state, value: '' }
111
+ : await searchOnce({ ruflo: bin, store: { path: storePath }, deadline, env, scratch, signal,
112
+ operation: 'retrieve', args: ['-k', row.key, '-n', row.namespace, '--value-only'] });
113
+ results.set(row, result);
114
+ }));
115
+ }));
116
+ return results;
117
+ }
118
+
@@ -0,0 +1,22 @@
1
+ /** Bounded read-only projection of canonical Ruflo rows; never opens a second store.
2
+ * Run as an owned child so SQLite/filesystem stalls can be retired at the recall deadline.
3
+ * Unsupported schema stands down to global Ruflo; malformed/duplicate rows fail closed.
4
+ */
5
+ import { withProgressionReader } from './project-progression-reader.mjs';
6
+
7
+ try {
8
+ const [storePath, input] = process.argv.slice(2);
9
+ const { requests, namespaces, deadline } = JSON.parse(input);
10
+ if (!Number.isFinite(deadline) || Date.now() >= deadline
11
+ || requests && (!Array.isArray(requests) || requests.length > 32
12
+ || requests.some(row => typeof row?.namespace !== 'string' || typeof row?.key !== 'string' || !row.key))
13
+ || namespaces && (!Array.isArray(namespaces) || namespaces.length > 8 || namespaces.some(value => typeof value !== 'string'))
14
+ || Boolean(requests) === Boolean(namespaces)) throw new Error('invalid bounded read');
15
+ const result = withProgressionReader(storePath, reader => requests
16
+ ? requests.map(({ namespace, key }) => reader.readContent(namespace, key))
17
+ : Object.fromEntries(namespaces.map(namespace => [namespace, reader.listKeys(namespace, { maxEntries: 100000 })])),
18
+ { deadlineAt: deadline });
19
+ process.stdout.write(JSON.stringify(result));
20
+ } catch {
21
+ process.stdout.write(JSON.stringify({ ok: false, fatal: true }));
22
+ }
@@ -2,15 +2,16 @@
2
2
  /** Prompt-time canonical AgentDB recall (ADR-101, G-022).
3
3
  * Every nonempty human prompt searches curated signal, turn outcomes and project/default namespaces in
4
4
  * .swarm/memory.db. Recalled records are untrusted evidence, never instructions.
5
- * Global Ruflo executes in isolated scratch directories; all processes share one
5
+ * Global Ruflo ranks semantic matches; an isolated read-only child prunes empty namespaces
6
+ * and batches exact values through the schema-checked canonical reader. All processes share one
6
7
  * <=2s deadline. ground-ruvnet.sh delivers <=600 bytes on each eligible prompt.
7
8
  */
8
9
  import fs from 'node:fs';
10
+ import os from 'node:os';
9
11
  import path from 'node:path';
10
12
  import crypto from 'node:crypto';
11
- import { spawn } from 'node:child_process';
12
13
  import { fileURLToPath } from 'node:url';
13
- import { resolveRuflo, rufloInvocation } from './ruflo-bin.mjs';
14
+ import { resolveRuflo } from './ruflo-bin.mjs';
14
15
  import { rufloCwdFor, rufloScratchRoot } from './project-progression-store.mjs';
15
16
  import { resolveProjectStore } from './project-store-resolver.mjs';
16
17
  import { isHarnessGenerated, readStdinBounded } from './hook-input.mjs';
@@ -19,7 +20,9 @@ import { learningTarget, LEARNING_NAMESPACE } from './learning-store.mjs';
19
20
  import { safeAction } from './learning-queue.mjs';
20
21
 
21
22
  import { redactText } from './continuity-events.mjs';
22
- import { withProgressionReader } from './project-progression-reader.mjs';
23
+ import { resolveTurnDb } from './turn-outcome-capture.mjs';
24
+ import { maskExcludedPaths } from './turn-capture-privacy.mjs';
25
+ import { searchOnce, nativeRead, exactReads, searchJsonResult } from './agentdb-recall-process.mjs';
23
26
  import { makeLesson } from './lesson-store.mjs';
24
27
  export const STORE_FILES = Object.freeze(['memory.db']);
25
28
  export const DEFAULT_DEADLINE_MS = 1900;
@@ -39,9 +42,9 @@ export function recallBinding({ binding = {}, projectRoot, storePath, prompt } =
39
42
  requestDigest: binding.requestDigest ?? crypto.createHash('sha256').update(String(prompt)).digest('hex') };
40
43
  }
41
44
 
42
- /** Bounded enumeration is structural evidence; exact values still come from global Ruflo. */
43
- function enumerateCanonicalKeys({ storePath, namespaces, deadline, signal }) {
44
- return withProgressionReader(storePath, reader => Object.fromEntries(namespaces.map(namespace => [namespace, reader.listKeys(namespace, { maxEntries: 2000 })])), { deadlineAt: deadline, signal });
45
+ /** Bounded structural enumeration uses the same cancellable read-only canonical provider. */
46
+ function enumerateCanonicalKeys({ storePath, namespaces, deadline, signal, env, scratch }) {
47
+ return nativeRead({ store: { path: storePath }, namespaces, deadline, signal, env, scratch });
45
48
  }
46
49
 
47
50
  export function applicableRecallLesson(value, projectRoot) {
@@ -98,17 +101,6 @@ export function parseSearchJson(stdout) {
98
101
  return searchJsonResult(stdout).rows;
99
102
  }
100
103
 
101
- function searchJsonResult(stdout) {
102
- const s = String(stdout || '');
103
- // Live Ruflo prints warnings AFTER the JSON object as well as logs before it.
104
- const start = s.indexOf('{'); const end = s.lastIndexOf('}');
105
- if (start < 0 || end < start) return { valid: false, rows: [] };
106
- try { const rows = JSON.parse(s.slice(start, end + 1)).results;
107
- return Array.isArray(rows) && rows.every(r => r && typeof r.key === 'string' && r.key)
108
- ? { valid: true, rows } : { valid: false, rows: [] };
109
- } catch { return { valid: false, rows: [] }; }
110
- }
111
-
112
104
  function clean(value, limit) {
113
105
  // Redact before truncation so a clipped token cannot escape recognition.
114
106
  const text = redactText(value).replace(/[\u0000-\u001f\u007f`]+/g, ' ').replace(/\s+/g, ' ').trim();
@@ -195,7 +187,7 @@ export function learningObservationExcerpt(raw) {
195
187
  }
196
188
 
197
189
  export function pickRows(results, limit = 3) {
198
- const candidates = results.flatMap((r) => r.rows.filter((p) => p.namespace === r.namespace && (!NOISE_KEY.test(p.key) || p.namespace === 'turns' && /^turn[-_]/i.test(p.key))
190
+ const candidates = results.flatMap((r) => r.rows.filter((p) => p.namespace === r.namespace && (!NOISE_KEY.test(p.key) || p.namespace === 'turns' && /^turn[-_]/i.test(p.key) || p.namespace === 'continuity-events' && /^cevt-.*-(?:decision|lesson|open-item)-/i.test(p.key))
199
191
  && (!r.family || p.key.toLowerCase().includes(r.family))
200
192
  && Number.isFinite(p.score) && p.score >= MIN_RELEVANCE).map((p) => ({ ...p, storePath: r.storePath, targeted: Boolean(r.family) })));
201
193
  const signal = (p) => SIGNAL_NAMESPACES.has(p.namespace) ? 2 : p.namespace === 'turns' ? 1 : p.namespace === LEARNING_NAMESPACE ? -1 : 0;
@@ -227,76 +219,15 @@ export function formatBlock({ picks, status }) {
227
219
  return render(limit);
228
220
  }
229
221
 
230
- function killGroup(child) {
231
- try { if (process.platform !== 'win32') process.kill(-child.pid, 'SIGKILL'); else child.kill('SIGKILL'); }
232
- catch { try { child.kill('SIGKILL'); } catch { /* already gone */ } }
233
- }
222
+ /** Semantic ranking gets a bounded slice; exact curated reads keep the rest so load cannot erase all history. */
223
+ export const SEARCH_BUDGET_SHARE = 0.8;
224
+ export function searchDeadlineFor(started, deadline) { return Math.min(deadline, started + Math.floor((deadline - started) * SEARCH_BUDGET_SHARE)); }
234
225
 
235
- /** One ruflo search, bounded by an absolute deadline. Resolves { rows, state } — never rejects. */
236
- function searchOnce({ ruflo, store, args, deadline, env, scratch, operation = 'search', signal }) {
237
- return new Promise((resolve) => {
238
- const remaining = deadline - Date.now();
239
- if (signal?.aborted || remaining <= 0) { resolve({ rows: [], state: signal?.aborted ? 'unavailable' : 'timed out' }); return; }
240
- let cwd;
241
- try { cwd = fs.mkdtempSync(path.join(scratch(store.path), 'run-')); } catch { resolve({ rows: [], state: 'unavailable' }); return; }
242
- const cleanup = () => { try { fs.rmSync(cwd, { recursive: true, force: true }); } catch { /* swept later as a stale run- dir */ } };
243
- let inv;
244
- try { inv = rufloInvocation(ruflo, ['memory', operation, '--path', store.path, ...args]); }
245
- catch { cleanup(); resolve({ rows: [], state: 'unavailable' }); return; }
246
- let child;
247
- try {
248
- child = spawn(inv.executable, inv.args, { cwd, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0' },
249
- stdio: ['ignore', 'pipe', 'ignore'], detached: process.platform !== 'win32', windowsHide: true });
250
- } catch { cleanup(); resolve({ rows: [], state: 'unavailable' }); return; }
251
- let out = '', outputBytes = 0;
252
- const decoder = new TextDecoder('utf-8', { fatal: true });
253
- let done = false, retiring = null, retirementTimer, timer;
254
- const finish = (state, retirement = process.platform === 'win32' ? 'direct-child-only' : 'confirmed') => {
255
- if (done) return;
256
- done = true;
257
- clearTimeout(timer);
258
- clearTimeout(retirementTimer);
259
- signal?.removeEventListener('abort', cancel);
260
- cleanup();
261
- const parsed = operation === 'search' ? searchJsonResult(out) : null;
262
- if (state === 'ok' && parsed && !parsed.valid) state = 'unavailable';
263
- resolve({ rows: state === 'ok' && parsed ? parsed.rows : [], value: state === 'ok' ? out.trim() : '', state, retirement });
264
- };
265
- const retire = state => {
266
- if (done || retiring) return;
267
- retiring = state; killGroup(child);
268
- retirementTimer = setTimeout(() => finish('unavailable', 'unconfirmed'), Math.max(1, deadline - Date.now()));
269
- };
270
- const cancel = () => retire('unavailable');
271
- signal?.addEventListener('abort', cancel, { once: true });
272
- // Reserve cleanup inside the same caller deadline; pipe inheritance cannot report success.
273
- timer = setTimeout(() => retire('timed out'), Math.max(1, deadline - Date.now() - 100));
274
- child.stdout.on('data', c => { if (done || retiring) return; outputBytes += c.length;
275
- if (outputBytes > 1 << 20) return retire('unavailable');
276
- try { out += decoder.decode(c, { stream: true }); } catch { retire('unavailable'); } });
277
- child.on('error', () => retire('unavailable'));
278
- child.on('close', code => {
279
- killGroup(child); // Successful parent exit must not leave an owned detached grandchild.
280
- let valid = true; try { out += decoder.decode(); } catch { valid = false; }
281
- const settle = () => {
282
- if (done) return;
283
- let alive = false;
284
- if (process.platform !== 'win32') {
285
- try { process.kill(-child.pid, 0); alive = true; } catch (error) { alive = error.code !== 'ESRCH'; }
286
- }
287
- if (alive && Date.now() < deadline) { retirementTimer = setTimeout(settle, 5); return; }
288
- finish(alive || !valid ? 'unavailable' : retiring ?? (code === 0 ? 'ok' : 'failed'), alive ? 'unconfirmed' : undefined);
289
- };
290
- clearTimeout(retirementTimer); settle();
291
- });
292
- if (signal?.aborted) cancel();
293
- });
294
- }
295
-
296
- async function consequentialRecall({ prompt, root, store, bin, scratch, env, deadline, binding, signal, enumerateKeys }) {
226
+ async function consequentialRecall({ prompt, root, store, bin, scratch, env, deadline, binding, signal, enumerateKeys, exclusions, rankedSearch = true }) {
227
+ const searchDeadline = searchDeadlineFor(Date.now(), deadline);
297
228
  const phaseBinding = recallBinding({ binding, projectRoot: root, storePath: store.path, prompt });
298
229
  const namespaces = [...new Set([path.basename(root), 'default', 'lessons', 'continuity-events'])];
299
- const enumeration = await enumerateKeys({ storePath: store.path, namespaces, deadline, signal });
230
+ const enumeration = await enumerateKeys({ storePath: store.path, namespaces, deadline, signal, env, scratch });
300
231
  if (!enumeration?.ok || signal?.aborted || Date.now() >= deadline) return { block: formatBlock({ picks: [], status: 'unavailable structural enumeration' }), picks: [], stores: [store],
301
232
  status: { 'memory.db': 'unavailable' }, outcome: signal?.aborted ? 'unavailable' : Date.now() >= deadline ? 'timed-out' : 'unavailable', categories: { state: 'unavailable', decisions: 'unavailable', lessons: 'unavailable' } };
302
233
  const keys = enumeration.value;
@@ -306,13 +237,21 @@ async function consequentialRecall({ prompt, root, store, bin, scratch, env, dea
306
237
  const decisionKeys = decisionNamespaces.flatMap(namespace => keys[namespace].filter(key => /^decision[-_]|^cevt-.*-decision-/i.test(key)).map(key => ({ category: 'decisions', namespace, key })));
307
238
  let relevantDecisions = decisionKeys;
308
239
  let decisionAvailability = 'ok-empty';
309
- if (decisionKeys.length > 8) {
240
+ if (decisionKeys.length > 8 && !rankedSearch) {
241
+ relevantDecisions = decisionKeys.slice(-8);
242
+ decisionAvailability = 'unavailable'; // unranked newest slice only; not a claim about all decisions
243
+ } else if (decisionKeys.length > 8) {
310
244
  const searched = await Promise.all(decisionNamespaces.map(async namespace => ({ namespace,
311
- ...await searchOnce({ ruflo: bin, store, deadline, env, scratch, signal, args: ['--format', 'json', '-q', recallQuery(prompt), '-n', namespace, '--limit', '12'] }) })));
312
- if (searched.some(row => row.state !== 'ok')) decisionAvailability = searched.some(row => row.state === 'timed out') ? 'timed-out' : 'unavailable';
245
+ ...await searchOnce({ ruflo: bin, store, deadline: searchDeadline, env, scratch, signal, args: ['--format', 'json', '-q', recallQuery(prompt), '-n', namespace, '--limit', '12'] }) })));
246
+ const rankingFailed = searched.some(row => row.state !== 'ok');
247
+ if (rankingFailed) decisionAvailability = searched.some(row => row.state === 'timed out') ? 'timed-out' : 'unavailable';
313
248
  relevantDecisions = searched.flatMap(result => result.rows.filter(row => row.namespace === result.namespace && Number.isFinite(row.score) && row.score >= MIN_RELEVANCE
314
249
  && decisionKeys.some(key => key.namespace === row.namespace && key.key === row.key)).map(row => ({ category: 'decisions', namespace: row.namespace, key: row.key, score: row.score })))
315
250
  .sort((a, b) => b.score - a.score).slice(0, 8);
251
+ if (rankingFailed && !relevantDecisions.length) {
252
+ // Semantic ranking is gone, not the history: read the newest bounded slice natively and keyword-match it exactly.
253
+ relevantDecisions = decisionKeys.slice(-8);
254
+ }
316
255
  if (!relevantDecisions.length) decisionAvailability = 'unavailable'; // semantic miss is not absent history
317
256
  }
318
257
  const candidates = [
@@ -323,9 +262,12 @@ async function consequentialRecall({ prompt, root, store, bin, scratch, env, dea
323
262
  if (candidates.length > 32) return { block: formatBlock({ picks: [], status: 'unavailable bounded category selection' }), picks: [], stores: [store], status: { 'memory.db': 'unavailable' }, outcome: 'unavailable', categories: { state: 'unavailable', decisions: 'unavailable', lessons: 'unavailable' } };
324
263
  const categories = { state: !stateKeys.length && keys[path.basename(root)].includes('project-state-current') ? 'unavailable' : 'ok-empty', decisions: decisionAvailability, lessons: 'ok-empty' };
325
264
  const records = [];
265
+ candidates.forEach(candidate => { candidate.storePath = store.path; });
266
+ const exactResults = await exactReads({ candidates, bin, deadline, env, scratch, signal });
326
267
  await Promise.all(candidates.map(async candidate => {
327
- const exact = await searchOnce({ ruflo: bin, store, deadline, env, scratch, operation: 'retrieve', signal, args: ['-k', candidate.key, '-n', candidate.namespace, '--value-only'] });
268
+ const exact = exactResults.get(candidate);
328
269
  if (exact.state !== 'ok' || !exact.value || exact.value.startsWith('[WARN]')) { categories[candidate.category] = /timed out/.test(exact.state) ? 'timed-out' : 'unavailable'; return; }
270
+ if (maskExcludedPaths(exact.value, exclusions, root) !== exact.value) return;
329
271
  let structured; try { structured = JSON.parse(exact.value); } catch { structured = null; }
330
272
  const declaredRoot = structured?.projectRoot ?? structured?.scope?.projectRoot;
331
273
  if (declaredRoot && declaredRoot !== root || structured?.supersededBy || structured?.superseded === true) {
@@ -347,7 +289,7 @@ async function consequentialRecall({ prompt, root, store, bin, scratch, env, dea
347
289
  let limit = 350;
348
290
  while (limit > 0 && Buffer.byteLength(render(limit)) > CONSEQUENTIAL_BLOCK_MAX_BYTES) limit--;
349
291
  const block = render(limit);
350
- return { block, picks: selected, stores: [store], status: { 'memory.db': outcome }, outcome, categories,
292
+ return { block, picks: selected, stores: [store], status: { 'memory.db': outcome }, outcome, categories, enumerated: true,
351
293
  receipt: { schemaVersion: 1, kind: 'canonical-memory-recall', binding: phaseBinding, outcome, categories,
352
294
  observedAt: new Date().toISOString(), deadline, queryDigest: crypto.createHash('sha256').update(String(prompt)).digest('hex'),
353
295
  records: selected.map(({ category, namespace, key, storePath, valueDigest }) => ({ category, namespace, key, storePath, valueDigest })), authority: false } };
@@ -365,7 +307,15 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
365
307
  const budget = Number.isFinite(requested) && requested > 0 ? Math.min(requested, 1900) : DEFAULT_DEADLINE_MS;
366
308
  const deadline = Math.min(started + budget, absoluteDeadline ?? Infinity);
367
309
  if (deadline <= started) return { ...empty, outcome: 'timed-out' };
368
- const { root, stores } = agentdbStores(projectDir, Math.max(1, Math.min(100, Math.floor(budget / 4))));
310
+ const gitTimeoutMs = Math.max(1, Math.min(600, Math.floor(budget / 3)));
311
+ const privacy = resolveTurnDb({ projectDir, brainHome: env.RUVNET_BRAIN_HOME || path.join(env.HOME || os.homedir(), '.cache', 'ruvnet-brain'), gitTimeoutMs, deadlineAt: deadline, signal });
312
+ if (privacy.skipped) {
313
+ const outcome = /opt-out/.test(privacy.skipped) ? 'disabled' : /no project memory db/.test(privacy.skipped) ? 'not-adopted' : 'unavailable';
314
+ return { ...empty, outcome, reason: privacy.skipped, block: outcome === 'unavailable' ? formatBlock({ picks: [], status: 'unavailable privacy policy' }) : '' };
315
+ }
316
+ const exclusions = privacy.contentPathExcludes;
317
+ const root = privacy.projectRoot; const stores = [];
318
+ try { if (fs.statSync(privacy.db).isFile()) stores.push({ name: 'memory.db', path: privacy.db }); } catch { /* absent */ }
369
319
  recallBinding({ binding, projectRoot: root, storePath: stores[0]?.path ?? null, prompt });
370
320
  let learningStore;
371
321
  try {
@@ -381,14 +331,26 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
381
331
  const store = stores[0];
382
332
  const query = recallQuery(prompt);
383
333
  const scratchFor = scratch || ((storePath) => rufloCwdFor(storePath, { root: rufloScratchRoot(env) }));
384
- if (consequential) return await consequentialRecall({ prompt, root, store, bin, scratch: scratchFor, env, deadline, binding, signal, enumerateKeys });
385
- const namespaces = [...new Set(['lessons', 'patterns', 'pattern', 'turns', path.basename(root), 'default'])];
334
+ if (consequential) return await consequentialRecall({ prompt, root, store, bin, scratch: scratchFor, env, deadline, binding, signal, enumerateKeys, exclusions });
335
+ const namespaces = [...new Set(['lessons', 'patterns', 'pattern', 'turns', 'continuity-events', path.basename(root), 'default'])];
386
336
  const family = /\bhooks?\b|hook.harness/i.test(prompt) ? 'decision-hook-harness-index'
387
337
  : /score|grade|north.star/i.test(prompt) ? 'scorecard'
388
338
  : /where are we|status|catch me up/i.test(prompt) ? 'project-state-current'
389
339
  : /releas|publish|workflow run|dispatch/i.test(prompt) ? 'release'
390
340
  : /requirement|always|every prompt/i.test(prompt) ? 'decision-agentdb' : null;
391
- const jobs = (store ? namespaces : []).flatMap((namespace) => [{ store, namespace, family: null, args: ['--format', 'json', '-q', query, '-n', namespace, '--limit', '12'] },
341
+ // This machine runs loaded (load average 30-60 measured 2026-10-10) and ranked search spawns one Ruflo process
342
+ // per namespace; run concurrently they starve every other read past the deadline. So: ONE native enumeration
343
+ // (reused for the curated pass and for pruning empty namespaces), then exact curated state/lessons/decisions,
344
+ // then spend what remains on ranking.
345
+ const presence = store ? await nativeRead({ store, namespaces, deadline, env, scratch: scratchFor, signal }) : null;
346
+ if (presence?.fatal) { return { ...empty, stores, outcome: presence.state === 'timed out' ? 'timed-out' : 'unavailable',
347
+ block: formatBlock({ picks: [], status: presence.state }), reason: 'namespace enumeration failed' }; }
348
+ const sharedKeys = presence?.ok ? async () => ({ ok: true, value: presence.value }) : enumerateCanonicalKeys;
349
+ const curated = store ? await consequentialRecall({ prompt, root, store, bin, scratch: scratchFor, env, deadline, binding, signal,
350
+ enumerateKeys: sharedKeys, exclusions, rankedSearch: false }).catch(() => null) : null;
351
+ const searchDeadline = searchDeadlineFor(Date.now(), deadline);
352
+ const activeNamespaces = presence?.ok ? namespaces.filter(namespace => presence.value[namespace]?.length) : namespaces;
353
+ const jobs = (store ? activeNamespaces : []).flatMap((namespace) => [{ store, namespace, family: null, args: ['--format', 'json', '-q', query, '-n', namespace, '--limit', '12'] },
392
354
  ...(family && !SIGNAL_NAMESPACES.has(namespace) && namespace !== 'turns' ? [{ store, namespace, family, args: ['--format', 'json', '-q', family, '-n', namespace, '-t', 'keyword', '--limit', '4'] }] : [])]);
393
355
  const words = promptKeywords(prompt, 14);
394
356
  const workflowQuery = words.map(word => safeAction('Bash', word))
@@ -396,7 +358,7 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
396
358
  if (learningStore && workflowQuery) jobs.push({ store: learningStore, namespace: LEARNING_NAMESPACE, family: null,
397
359
  args: ['--format', 'json', '-q', workflowQuery, '-n', LEARNING_NAMESPACE, '-t', 'keyword', '--limit', '4'] });
398
360
  const results = await Promise.all(jobs.map(async ({ store: jobStore, namespace, family: recordFamily, args }) => ({ namespace, family: recordFamily, storePath: jobStore.path,
399
- ...await searchOnce({ ruflo: bin, store: jobStore, deadline, env, scratch: scratchFor, args, signal }) })));
361
+ ...await searchOnce({ ruflo: bin, store: jobStore, deadline: searchDeadline, env, scratch: scratchFor, args, signal }) })));
400
362
  let status = results.every((r) => r.state === 'ok') ? 'ok'
401
363
  : results.some((r) => r.state === 'timed out') ? 'timed out' : 'unavailable';
402
364
  // Overfetch a bounded six exact values so rejected turn metadata cannot hide
@@ -404,13 +366,21 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
404
366
  const candidates = pickRows(results, 6);
405
367
  // Ruflo previews are ~60 characters. Read the actual selected values within
406
368
  // the same deadline so the block contains useful evidence rather than titles.
369
+ const exactResults = await exactReads({ candidates, bin, deadline, env, scratch: scratchFor, signal });
407
370
  const retrieved = await Promise.all(candidates.map(async (p) => {
408
- const r = await searchOnce({ ruflo: bin, store: { name: 'memory.db', path: p.storePath }, deadline, env, scratch: scratchFor, operation: 'retrieve',
409
- args: ['-k', p.key, '-n', p.namespace, '--value-only'], signal });
371
+ const r = exactResults.get(p);
410
372
  if (r.state === 'ok' && r.value && !r.value.startsWith('[WARN]')) {
373
+ if (maskExcludedPaths(r.value, exclusions, root) !== r.value || maskExcludedPaths(p.key, exclusions, root) !== p.key) return { pick: null, state: r.state };
374
+ let event;
375
+ if (p.namespace === 'continuity-events') {
376
+ try { event = JSON.parse(r.value); } catch { return { pick: null, state: r.state }; }
377
+ if (event.schema !== 'ruvnet-brain.continuity-event' || event.schemaVersion !== 1 || event.project !== root
378
+ || !['decision', 'lesson', 'open-item'].includes(event.kind) || typeof event.summary !== 'string'
379
+ || !promptKeywords(prompt, 14).some(word => redactText(event.summary).toLowerCase().includes(word))) return { pick: null, state: r.state };
380
+ }
411
381
  const clauses = p.namespace === 'turns' ? turnOutcomeClauses(r.value, prompt) : null;
412
382
  const preview = p.namespace === LEARNING_NAMESPACE ? learningObservationExcerpt(r.value)
413
- : clauses ? turnOutcomeExcerpt(r.value, prompt) : evidenceExcerpt(r.value, p.key, prompt);
383
+ : event ? clean(event.summary, 280) : clauses ? turnOutcomeExcerpt(r.value, prompt) : evidenceExcerpt(r.value, p.key, prompt);
414
384
  return { pick: preview ? { ...p, preview, clauses, valueDigest: crypto.createHash('sha256').update(r.value).digest('hex') } : null, state: 'ok' };
415
385
  }
416
386
  return { pick: null, state: r.state === 'ok' ? 'unavailable' : r.state };
@@ -427,6 +397,19 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
427
397
  });
428
398
  return unique.length ? [{ ...p, preview: clean('OUTCOME: ' + unique.join(' '), 280) }] : [];
429
399
  }).slice(0, 3);
400
+ // Curated exact records were read before ranking and are already verified: they lead, ranked hits fill the
401
+ // rest, deduplicated by (namespace, key). Ranked search failing or finding nothing never discards them, and
402
+ // everything leaves through formatBlock so the prompt path stays within BLOCK_MAX_BYTES.
403
+ const exactCurated = (curated?.picks ?? []).map(p => ({ store: 'memory.db', storePath: p.storePath, key: p.key, namespace: p.namespace,
404
+ score: 1, preview: p.preview, valueDigest: p.valueDigest }));
405
+ const seenKeys = new Set(exactCurated.map(p => `${p.namespace}\u0000${p.key}`));
406
+ const merged = [...exactCurated, ...picks.filter(p => !seenKeys.has(`${p.namespace}\u0000${p.key}`))].slice(0, 3);
407
+ picks.length = 0; picks.push(...merged);
408
+ // A failed curated read of state or lessons is not empty history: it must surface even when ranked search was ok.
409
+ // (Decisions are unranked by design in this pass, so their availability marker is not folded in.)
410
+ if (status === 'ok' && curated?.enumerated && ['state', 'lessons'].some(c => ['timed-out', 'unavailable'].includes(curated.categories?.[c]))) {
411
+ status = ['state', 'lessons'].some(c => curated.categories[c] === 'timed-out') ? 'timed out reading curated records' : 'unavailable curated records';
412
+ }
430
413
  if (retrieved.some((r) => r.state !== 'ok')) status = retrieved.some((r) => r.state === 'timed out') ? 'timed out reading exact values' : 'unavailable exact values';
431
414
  const outcome = status === 'ok' ? (picks.length ? 'ok-with-results' : 'ok-empty') : /timed out/.test(status) ? 'timed-out' : 'unavailable';
432
415
  const receipt = { schemaVersion: 1, kind: 'canonical-memory-recall', outcome,
@@ -28,6 +28,7 @@
28
28
  * detail.status: 'detected-unconfirmed'`, written only to the project's own local store; the brief never
29
29
  * presents one as a standing rule. RUVNET_CONTINUITY_LESSON_DETECT=off turns that detector off.
30
30
  */
31
+ import { nativeTurnLines } from './native-turn-transcript.mjs';
31
32
  import crypto from 'node:crypto';
32
33
  import fs from 'node:fs';
33
34
  import os from 'node:os';
@@ -265,12 +266,12 @@ function exitOutcome(result) {
265
266
  }
266
267
 
267
268
  /**
268
- * Typed events from one Claude JSONL turn. Codex rollouts are not parsed (project-progression-sources
269
- * declares that format unknown); Codex gets decisions/lessons from `last_assistant_message` only.
269
+ * Typed events from the current native turn. Codex JSONL is normalized mechanically; opaque
270
+ * custom-tool orchestration inputs cannot manufacture a gate outcome or semantic decision.
270
271
  */
271
272
  export function collectTurnEvents({ lines = null, lastAssistantMessage = '', host, session, project, env = process.env, at = Date.now() }) {
272
273
  const events = [];
273
- const turn = lines ? currentTurnRecords(lines) : { userMessage: '', records: [] };
274
+ const turn = lines ? currentTurnRecords(nativeTurnLines(lines, host)) : { userMessage: '', records: [] };
274
275
  const uses = new Map();
275
276
  const assistantTexts = [];
276
277
  for (const rec of turn.records) {
@@ -306,9 +307,9 @@ export function collectTurnEvents({ lines = null, lastAssistantMessage = '', hos
306
307
  for (const text of assistantTexts) {
307
308
  for (const line of text.split('\n')) {
308
309
  if (DECISION_LINE.test(line)) {
309
- events.push(makeEvent({ kind: 'decision', at, host, session, project, source: 'assistant-detected', authoritative: false, summary: line.replace(/\*\*/g, '') }));
310
+ events.push(makeEvent({ kind: 'decision', at, host, session, project, source: 'assistant-detected', authoritative: false, detail: { status: 'detected-unconfirmed' }, summary: line.replace(/\*\*/g, '') }));
310
311
  } else if (LESSON_LINE.test(line)) {
311
- events.push(makeEvent({ kind: 'lesson', at, host, session, project, source: 'assistant-detected', authoritative: false, summary: line.replace(/\*\*/g, '') }));
312
+ events.push(makeEvent({ kind: 'lesson', at, host, session, project, source: 'assistant-detected', authoritative: false, detail: { status: 'detected-unconfirmed' }, summary: line.replace(/\*\*/g, '') }));
312
313
  }
313
314
  }
314
315
  }