ruvnet-brain 4.7.20 → 4.7.22
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 +2 -2
- package/bin/install.mjs +618 -104
- package/console/activity.js +122 -38
- package/kb/download-retry.mjs +93 -21
- package/kb/forge-guard-injection.mjs +277 -0
- package/kb/forge-update.mjs +166 -38
- package/kb/lifecycle-evidence-retention.mjs +151 -14
- package/kb/model-requirements.mjs +4 -1
- package/kb/refresh-run.mjs +175 -27
- package/kb/verify-citation.mjs +15 -2
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/mcp/managed-cli-interface.mjs +16 -8
- package/plugin/scripts/adr-currency-gate.mjs +2 -1
- package/plugin/scripts/advisory-contract.mjs +21 -0
- package/plugin/scripts/advocacy-route.mjs +5 -4
- package/plugin/scripts/agentdb-recall-process.mjs +115 -5
- package/plugin/scripts/agentdb-recall-reader.mjs +44 -2
- package/plugin/scripts/agentdb-recall.mjs +49 -17
- package/plugin/scripts/automatic-update.mjs +9 -1
- package/plugin/scripts/brain-confirmation.mjs +2 -1
- package/plugin/scripts/brain-footprint.mjs +72 -11
- package/plugin/scripts/capability-registry.mjs +1 -1
- package/plugin/scripts/capacity-aware-parallel-work.mjs +2 -1
- package/plugin/scripts/codex-overnight-notice.mjs +33 -0
- package/plugin/scripts/completion-claim-evidence.mjs +241 -23
- package/plugin/scripts/continuation-gate.mjs +75 -11
- package/plugin/scripts/decision-gate.mjs +13 -63
- package/plugin/scripts/decision-outcomes.mjs +36 -0
- package/plugin/scripts/decision-recall-gate.mjs +40 -5
- package/plugin/scripts/decision-record.mjs +8 -3
- package/plugin/scripts/degradation-watch.mjs +2 -1
- package/plugin/scripts/developer-update-lock.mjs +172 -33
- package/plugin/scripts/developer-update-owner.mjs +8 -5
- package/plugin/scripts/developer-update-policy.mjs +4 -1
- package/plugin/scripts/developer-update.mjs +155 -53
- package/plugin/scripts/doc-currency.mjs +2 -16
- package/plugin/scripts/duplicate-gate.mjs +2 -1
- package/plugin/scripts/entrypoint.mjs +34 -0
- package/plugin/scripts/footprint-io.mjs +135 -8
- package/plugin/scripts/grok-hook-adapter.mjs +2 -1
- package/plugin/scripts/ground-ruvnet.sh +6 -3
- package/plugin/scripts/grounding-answer.mjs +2 -1
- package/plugin/scripts/grounding-code-projection.mjs +2 -1
- package/plugin/scripts/grounding-substance.mjs +2 -1
- package/plugin/scripts/grounding-turn-gate.mjs +2 -2
- package/plugin/scripts/grounding-turn-mark.mjs +2 -2
- package/plugin/scripts/hook-context-budget.mjs +2 -1
- package/plugin/scripts/hook-input.mjs +2 -2
- package/plugin/scripts/hook-shim.mjs +9 -2
- package/plugin/scripts/host-update.mjs +29 -5
- package/plugin/scripts/identifier-preflight.mjs +2 -1
- package/plugin/scripts/kb-copy-proof.mjs +57 -9
- package/plugin/scripts/learn-capture.mjs +2 -1
- package/plugin/scripts/learning-store.mjs +62 -16
- package/plugin/scripts/lesson-bridge.mjs +2 -2
- package/plugin/scripts/lesson-gate.mjs +7 -39
- package/plugin/scripts/lesson-promote.mjs +83 -22
- package/plugin/scripts/lesson-scope.mjs +51 -0
- package/plugin/scripts/lesson-store.mjs +1 -1
- package/plugin/scripts/md-stamp.mjs +2 -1
- package/plugin/scripts/native-turn-transcript.mjs +98 -6
- package/plugin/scripts/nightly-scheduler.mjs +10 -5
- package/plugin/scripts/package-recommender.mjs +2 -2
- package/plugin/scripts/project-store-resolver.mjs +19 -0
- package/plugin/scripts/routing-outcome-capture.mjs +2 -1
- package/plugin/scripts/ruflo-bin.mjs +14 -6
- package/plugin/scripts/ruflo-memory-hooks.mjs +157 -19
- package/plugin/scripts/session-snapshot-hook.mjs +25 -5
- package/plugin/scripts/session-start-core.mjs +7 -16
- package/plugin/scripts/session-start-health.mjs +98 -10
- package/plugin/scripts/session-start-issue-alert.mjs +63 -0
- package/plugin/scripts/session-start-update-plane.mjs +15 -1
- package/plugin/scripts/signal-watch.mjs +2 -1
- package/plugin/scripts/spend-guard.mjs +2 -1
- package/plugin/scripts/substitution-wall.mjs +2 -1
- package/plugin/scripts/turn-outcome-capture.mjs +7 -3
- package/plugin/scripts/unprompted-runtime.mjs +20 -3
- package/plugin/scripts/update-apply.mjs +186 -15
- package/plugin/skills/release-proof/scripts/release-proof.mjs +2 -1
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +2 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +3 -2
- package/scripts/adr-072-completion.mjs +2 -1
- package/scripts/agentdb-context.mjs +2 -1
- package/scripts/agentdb-fleet-doctor.mjs +30 -8
- package/scripts/approved-runtime.mjs +2 -1
- package/scripts/architecture-review-lock.mjs +2 -1
- package/scripts/ascii-drift.mjs +2 -1
- package/scripts/brain-latency-50.mjs +2 -1
- package/scripts/brain-novice-50.mjs +2 -16
- package/scripts/brain-score.mjs +13 -2
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/build-concepts.mjs +2 -1
- package/scripts/candidate-ci-receipt.mjs +2 -1
- package/scripts/candidate-host-evidence.mjs +2 -1
- package/scripts/card-from-source.mjs +2 -1
- package/scripts/ci/warm-brain-models.mjs +2 -1
- package/scripts/claims-verify.mjs +7 -3
- package/scripts/claude-controlled-terminal.mjs +2 -1
- package/scripts/claude-terminal-mod.mjs +2 -1
- package/scripts/code-release-corpus.mjs +2 -1
- package/scripts/codex-boot-surface-proof.mjs +208 -0
- package/scripts/codex-hook-trust-reconcile.mjs +67 -1
- package/scripts/codex-hook-trust.mjs +40 -1
- package/scripts/codex-hook-wrapper-history.mjs +2 -1
- package/scripts/codex-host-execution-proof.mjs +2 -1
- package/scripts/completion-claim-replay.mjs +2 -1
- package/scripts/console-engine.mjs +8 -3
- package/scripts/corpus-canary.mjs +2 -1
- package/scripts/corpus-candidate.mjs +2 -2
- package/scripts/corpus-coverage-sidecar.mjs +2 -1
- package/scripts/corpus-dispatch-decision.mjs +2 -1
- package/scripts/corpus-dispatch-receipt.mjs +2 -1
- package/scripts/corpus-next-seed.mjs +2 -1
- package/scripts/corpus-qa.mjs +2 -1
- package/scripts/corpus-reconcile.mjs +2 -2
- package/scripts/corpus-watchdog.mjs +2 -1
- package/scripts/count-chunks.mjs +2 -1
- package/scripts/customer-state-matrix.mjs +2 -1
- package/scripts/derive-passage-content-map.mjs +2 -1
- package/scripts/development-push-check.mjs +2 -1
- package/scripts/dispatch-receipt.mjs +2 -1
- package/scripts/doc-currency.mjs +2 -1
- package/scripts/dream-issue-gate.mjs +2 -1
- package/scripts/dual-host-deliberation.mjs +2 -16
- package/scripts/dual-host-suggest.mjs +2 -16
- package/scripts/eval-brain.mjs +7 -3
- package/scripts/execution-policy.mjs +2 -1
- package/scripts/execution-preflight.mjs +2 -1
- package/scripts/falsify.mjs +2 -1
- package/scripts/fix-metaharness-memretrieve.mjs +2 -1
- package/scripts/fix-workstream.mjs +2 -1
- package/scripts/full-hints.mjs +2 -1
- package/scripts/full-suite-gate.mjs +2 -1
- package/scripts/grok-host.mjs +68 -0
- package/scripts/hook-qualify-core.mjs +3 -1
- package/scripts/hook-qualify.mjs +2 -1
- package/scripts/hook-retirement-check.mjs +2 -1
- package/scripts/host-registry.mjs +2 -1
- package/scripts/independent-review-receipt.mjs +2 -1
- package/scripts/ingest-new-repos.mjs +2 -1
- package/scripts/ingest-repo.mjs +12 -2
- package/scripts/integration-evidence.mjs +2 -1
- package/scripts/issue-fix.mjs +7 -24
- package/scripts/issue-watch.mjs +116 -40
- package/scripts/job-heartbeat.sh +54 -19
- package/scripts/key-canary.mjs +9 -3
- package/scripts/latency-to-surface.mjs +2 -1
- package/scripts/learning-replay-cli.mjs +97 -19
- package/scripts/learning-replay-execution.mjs +2 -0
- package/scripts/learning-replay-fixture.mjs +3 -0
- package/scripts/learning-replay-proof.mjs +14 -13
- package/scripts/learning-replay.mjs +3 -3
- package/scripts/learnings.mjs +2 -1
- package/scripts/lesson-ratify.mjs +88 -40
- package/scripts/loop-checkpoint.mjs +2 -1
- package/scripts/metaharness-receipts.mjs +2 -1
- package/scripts/model-catalog.mjs +2 -1
- package/scripts/model-currency-evidence.mjs +6 -4
- package/scripts/model-currency.mjs +33 -3
- package/scripts/model-native-catalog.mjs +2 -1
- package/scripts/model-router-agent-hook.mjs +2 -1
- package/scripts/model-router-dispatch.mjs +2 -1
- package/scripts/model-router-engine.mjs +2 -1
- package/scripts/model-router-outcome.mjs +2 -1
- package/scripts/model-router-setup.mjs +2 -1
- package/scripts/model-routing-eval.mjs +2 -1
- package/scripts/model-routing-gateway.mjs +2 -1
- package/scripts/model-routing-launchers.mjs +4 -1
- package/scripts/model-terminal-gateway.mjs +2 -1
- package/scripts/model-terminal-launchers.mjs +2 -1
- package/scripts/model-weekly-analyst.mjs +2 -1
- package/scripts/model-weekly-cycle.mjs +2 -1
- package/scripts/model-weekly-qualification.mjs +2 -1
- package/scripts/nightly-gists.sh +26 -20
- package/scripts/nightly-two-run-proof.mjs +2 -1
- package/scripts/nightly-watchdog.mjs +158 -47
- package/scripts/no-silent-substitution.mjs +2 -1
- package/scripts/notify.sh +11 -6
- package/scripts/ntfy-inbox.mjs +41 -18
- package/scripts/onboarding-console.mjs +123 -23
- package/scripts/oracle/abstain-threshold-sweep.mjs +2 -1
- package/scripts/oracle/abstain-trace.mjs +2 -1
- package/scripts/oracle/doc2query-generate.mjs +2 -1
- package/scripts/oracle/doc2query-reach.mjs +2 -1
- package/scripts/oracle/judge-train.mjs +2 -1
- package/scripts/oracle/need-set-split.mjs +2 -1
- package/scripts/oracle/produce-questions.mjs +2 -16
- package/scripts/oracle/repo-recall.mjs +2 -2
- package/scripts/oracle/retrieval-accuracy.mjs +2 -2
- package/scripts/oracle/sona-query-adapter-eval.mjs +2 -1
- package/scripts/oracle/source-tree.mjs +2 -15
- package/scripts/oracle/source-units.mjs +2 -16
- package/scripts/oracle/spike-run.mjs +2 -16
- package/scripts/oracle/validate-labels.mjs +2 -16
- package/scripts/org-repo-count.mjs +2 -1
- package/scripts/package-cards.mjs +2 -1
- package/scripts/prepublication-evidence.mjs +2 -1
- package/scripts/private-overlay.mjs +2 -1
- package/scripts/product-integrity-contract.mjs +2 -1
- package/scripts/protected-release-invocation.mjs +2 -1
- package/scripts/public-verification-abandon.mjs +2 -1
- package/scripts/public-verification-aggregate.mjs +2 -1
- package/scripts/public-verification-finalizer.mjs +2 -1
- package/scripts/public-verification-inputs.mjs +2 -1
- package/scripts/public-verification-lane.mjs +2 -1
- package/scripts/publication-receipt.mjs +2 -1
- package/scripts/published-surface-probe.mjs +2 -1
- package/scripts/qe/card-lane-gate.mjs +2 -16
- package/scripts/qe/session-start-gate.mjs +2 -16
- package/scripts/qe/ux-suite.mjs +2 -1
- package/scripts/qualified-candidate-check.mjs +2 -1
- package/scripts/rebuild-gists-from-receipts.mjs +2 -1
- package/scripts/recall-latency-probe.mjs +48 -0
- package/scripts/recommendation-eval.mjs +2 -1
- package/scripts/recommendation-floor.mjs +2 -1
- package/scripts/recommendation-judge-score.mjs +2 -1
- package/scripts/recommendation-real-host-score.mjs +2 -1
- package/scripts/recommendation-real-host.mjs +2 -1
- package/scripts/refresh-capability-only-store.mjs +2 -1
- package/scripts/refresh-model-catalog.mjs +2 -1
- package/scripts/rehearse-corpus-pipeline.mjs +2 -1
- package/scripts/release-authority.mjs +2 -1
- package/scripts/release-evidence-aggregate.mjs +2 -1
- package/scripts/release-payload.mjs +2 -1
- package/scripts/release-projection.mjs +2 -1
- package/scripts/release-proof.mjs +2 -1
- package/scripts/release-qualification-contract.mjs +82 -28
- package/scripts/release-qualification.mjs +8 -1
- package/scripts/release-vector.mjs +2 -1
- package/scripts/restore-local-ingests.mjs +2 -1
- package/scripts/retrieval-canary.mjs +2 -1
- package/scripts/route-cheap.mjs +2 -1
- package/scripts/route-gold-rank.mjs +2 -1
- package/scripts/route-index-memory.mjs +2 -1
- package/scripts/route-latency-warm.mjs +2 -1
- package/scripts/router-utilization.mjs +2 -1
- package/scripts/routing-flywheel.mjs +2 -1
- package/scripts/run-operational-benchmark.mjs +2 -1
- package/scripts/run-operational-benchmark.v3.mjs +2 -1
- package/scripts/rvf-index-audit.mjs +2 -16
- package/scripts/seal-gist-receipt.mjs +2 -1
- package/scripts/security-guidance-codex-compat.mjs +2 -1
- package/scripts/selfcheck.mjs +2 -1
- package/scripts/signal-watch.mjs +2 -1
- package/scripts/source-coverage.mjs +2 -1
- package/scripts/stabilization-receipt.mjs +2 -1
- package/scripts/stamp-sweep.mjs +2 -1
- package/scripts/status-honesty.mjs +2 -1
- package/scripts/sync-commands.mjs +2 -2
- package/scripts/top100-benchmark.mjs +2 -1
- package/scripts/top100-corpus.mjs +2 -1
- package/scripts/user-model-hook-install.mjs +2 -1
- package/scripts/user-model-prompt-hook.mjs +2 -1
- package/scripts/verify-bundle.mjs +3 -1
- package/scripts/verify-model-catalog.mjs +2 -1
- package/scripts/version.mjs +2 -1
- package/scripts/wired-check.mjs +13 -5
- package/scripts/worktree-integrity.mjs +2 -2
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { managedGenerationIdentity } from './managed-cli-generation.mjs';
|
|
7
7
|
import { loadRuntimePreferences, runtimeChildEnv } from '../scripts/runtime-preferences.mjs';
|
|
8
|
+
import { resolveRuflo } from '../scripts/ruflo-bin.mjs';
|
|
8
9
|
import { projectDirectory } from '../scripts/project-identity.mjs';
|
|
9
10
|
import { recordManagedCliObservation, recordRegistryLatestObservation } from '../scripts/capability-claim-evidence.mjs';
|
|
10
11
|
|
|
@@ -180,12 +181,19 @@ function writeStamps(executable, argv, env, binding) {
|
|
|
180
181
|
|
|
181
182
|
export function resolveManagedExecutable(executable, env = process.env) {
|
|
182
183
|
if (executable !== 'ruflo') return executable;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
// #420 item 5: same rules as every other Brain caller (RUFLO_BIN is authoritative, then ~/.npm-global/bin, then PATH); this used to check ~/.npm-global only and ignored RUFLO_BIN.
|
|
185
|
+
return resolveRuflo({ env, home: env.HOME || os.homedir() }) || executable;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* ruflo (and claude-flow, the same @claude-flow/cli) self-updates at startup by running `npm install` in its
|
|
190
|
+
* working directory, which here is the user's project: measured 2026-10-22, a 1.3 GB install landed there.
|
|
191
|
+
* ruflo-bin.mjs promises every Brain spawn sets CLAUDE_FLOW_AUTO_UPDATE=false; this boundary is a Brain spawn
|
|
192
|
+
* too. RUFLO_DAEMON_AUTOSTART=0 keeps a background daemon from being left behind in that project.
|
|
193
|
+
*/
|
|
194
|
+
export function managedChildEnv(executable, env) {
|
|
195
|
+
if (executable !== 'ruflo' && executable !== 'claude-flow') return env;
|
|
196
|
+
return { ...env, CLAUDE_FLOW_AUTO_UPDATE: 'false', RUFLO_DAEMON_AUTOSTART: '0' };
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
function quoteWindowsCommandArg(value) {
|
|
@@ -323,7 +331,7 @@ export async function callManagedCli(toolName, args, env = process.env, fetchImp
|
|
|
323
331
|
if (toolName === 'ruvnet_cli_help') {
|
|
324
332
|
stampKeysForHelp(executable, argv);
|
|
325
333
|
const commandArgv = [...argv, '--help'];
|
|
326
|
-
const execution = await execute(executable, commandArgv, env);
|
|
334
|
+
const execution = await execute(executable, commandArgv, managedChildEnv(executable, env));
|
|
327
335
|
if (normalizeManagedExecution(execution).outcome === 'success') writeStamps(executable, argv, env, binding);
|
|
328
336
|
recordManagedCliObservation({ toolName, executable, argv: commandArgv, execution, env });
|
|
329
337
|
return resultOf(executable, commandArgv, execution);
|
|
@@ -368,7 +376,7 @@ export async function callManagedCli(toolName, args, env = process.env, fetchImp
|
|
|
368
376
|
// host by supplying an arbitrary `host` property.
|
|
369
377
|
const childEnv = (executable === 'agentic-flow' || executable === 'agentic-qe')
|
|
370
378
|
? runtimeChildEnv({ env, cwd: projectRoot })
|
|
371
|
-
: env;
|
|
379
|
+
: managedChildEnv(executable, env);
|
|
372
380
|
if (lifecycle && typeof lifecycle.beforeExecute === 'function') {
|
|
373
381
|
await lifecycle.beforeExecute({ executable, argv, projectRoot, env: childEnv });
|
|
374
382
|
}
|
|
@@ -33,6 +33,7 @@ import fs from 'node:fs';
|
|
|
33
33
|
import { execFileSync, spawn } from 'node:child_process';
|
|
34
34
|
import path from 'node:path';
|
|
35
35
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
36
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
36
37
|
|
|
37
38
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
38
39
|
const REPO = path.resolve(HERE, '..', '..');
|
|
@@ -128,7 +129,7 @@ An ADR describing a world the code left is worse than no ADR: the next reader tr
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
const isMain = (() => {
|
|
131
|
-
try { return
|
|
132
|
+
try { return isDirectInvocation(import.meta.url); }
|
|
132
133
|
catch { return false; }
|
|
133
134
|
})();
|
|
134
135
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The delivery contract for OPTIONAL Brain advice (#489).
|
|
3
|
+
*
|
|
4
|
+
* A capability recommendation, a package recommendation or a routine stack-update notice is
|
|
5
|
+
* information, not a decision the user must make now. Phrased as a question ("want me to update
|
|
6
|
+
* it?"), it was promoted by Codex into a structured question dialog that suspended an already
|
|
7
|
+
* authorized task. So optional advice is worded as statements, and the model-visible context
|
|
8
|
+
* carries this one line saying so. Real permission gates and genuinely missing information keep
|
|
9
|
+
* their normal question behaviour; this applies only to optional advisory channels.
|
|
10
|
+
*
|
|
11
|
+
* ground-ruvnet.sh cannot import this module and carries the same sentence verbatim;
|
|
12
|
+
* tests/unit/advisory-contract.test.mjs fails if the two drift.
|
|
13
|
+
*/
|
|
14
|
+
export const ADVISORY_CONTRACT = 'Optional, informational advice: mention it at most once as a statement and continue the current task. '
|
|
15
|
+
+ 'Do not open a question or approval dialog because of it; it grants no authority to install or update anything.';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Question-shaped wording that invites a decision dialog, including the "say X to proceed, or ignore
|
|
19
|
+
* this" choice prompt the advocacy copy used to carry. Optional advice must match none of it.
|
|
20
|
+
*/
|
|
21
|
+
export const QUESTION_SHAPED = /\?|\b(want me to|would you like|should i|shall i|do you want|can i|to proceed|or ignore this)\b/i;
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
ACTIONS, record, shouldStillOffer, stateHashOf, precision, pendingOffers, reconcileIgnored, loadOutcomes,
|
|
62
62
|
} from './advocacy-outcomes.mjs';
|
|
63
63
|
import { packageRecommenderEnabled, offerNames } from './package-recommender-flag.mjs';
|
|
64
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
64
65
|
// Matcher + socket client load only when the flag is on at process start: default-off pays nothing.
|
|
65
66
|
const PKG = packageRecommenderEnabled() ? { ...(await import('./package-recommender.mjs')), ...(await import('./package-recommender-client.mjs')) } : null;
|
|
66
67
|
|
|
@@ -341,9 +342,9 @@ export function buildCandidate({ prompt, match, availability }) {
|
|
|
341
342
|
? `It is already installed here.`
|
|
342
343
|
: `Install state unknown from here — say so rather than claiming it is available.`;
|
|
343
344
|
const copy = [
|
|
344
|
-
`[RuvNet Brain — capability advocacy] If it genuinely fits,
|
|
345
|
-
+ `"
|
|
346
|
-
`If
|
|
345
|
+
`[RuvNet Brain — capability advocacy] If it genuinely fits, state it in ONE sentence: `
|
|
346
|
+
+ `"${cap.id} fits here — ${cap.benefit}; 'use ${cap.id}' adopts it." ${avail}`,
|
|
347
|
+
`If the user later says to use it, the safe first step is \`${cap.nextAction}\` (undo: ${cap.undo}); confirm it with `
|
|
347
348
|
+ `search_ruvnet before you build. Say it once, do not expand it, then carry on with the actual work.`,
|
|
348
349
|
].join('\n');
|
|
349
350
|
return {
|
|
@@ -429,7 +430,7 @@ export function decide({ prompt, sessionId, file, state, now = Date.now(), start
|
|
|
429
430
|
}
|
|
430
431
|
|
|
431
432
|
// ── CLI ───────────────────────────────────────────────────────────────────────────────────────────
|
|
432
|
-
const isMain =
|
|
433
|
+
const isMain = isDirectInvocation(import.meta.url);
|
|
433
434
|
|
|
434
435
|
async function main() {
|
|
435
436
|
const startedAt = Date.now();
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { rufloInvocation } from './ruflo-bin.mjs';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { rufloInvocation, nativeNodeFor } from './ruflo-bin.mjs';
|
|
7
7
|
export function searchJsonResult(stdout) {
|
|
8
8
|
const s = String(stdout || '');
|
|
9
9
|
// Live Ruflo prints warnings AFTER the JSON object as well as logs before it.
|
|
@@ -20,6 +20,105 @@ function killGroup(child) {
|
|
|
20
20
|
catch { try { child.kill('SIGKILL'); } catch { /* already gone */ } }
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
24
|
+
const inside = (root, file) => { const rel = path.relative(root, file); return rel && !rel.startsWith('..') && !path.isAbsolute(rel); };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Can reads go through Ruflo's own published code instead of one CLI process per read (#429)? Only when the resolved
|
|
28
|
+
* `ruflo` is the real package entry AND the `@claude-flow/cli` it loads (found as bin/ruflo.js finds it) declares a
|
|
29
|
+
* `./memory` export that resolves to a file inside it. Otherwise null and the caller keeps the per-read CLI route.
|
|
30
|
+
* The three functions and result shapes are checked again inside the child, where they are actually loaded.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveBatchTarget(bin, { env = process.env, deadlineAt } = {}) {
|
|
33
|
+
try {
|
|
34
|
+
if (!bin || process.platform === 'win32') return null; // npm .cmd shims are not inspected here
|
|
35
|
+
const entry = fs.realpathSync(bin);
|
|
36
|
+
const pkgRoot = path.dirname(path.dirname(entry));
|
|
37
|
+
const pkg = readJson(path.join(pkgRoot, 'package.json'));
|
|
38
|
+
const declared = typeof pkg?.bin === 'string' ? pkg.bin : pkg?.bin?.ruflo;
|
|
39
|
+
if (pkg?.name !== 'ruflo' || typeof declared !== 'string' || fs.realpathSync(path.resolve(pkgRoot, declared)) !== entry) return null;
|
|
40
|
+
let cliRoot = null;
|
|
41
|
+
for (let dir = pkgRoot, i = 0; i < 10 && !cliRoot; i += 1, dir = path.dirname(dir)) {
|
|
42
|
+
const candidate = path.join(dir, 'node_modules', '@claude-flow', 'cli');
|
|
43
|
+
if (fs.existsSync(path.join(candidate, 'package.json'))) cliRoot = candidate;
|
|
44
|
+
if (path.dirname(dir) === dir) break;
|
|
45
|
+
}
|
|
46
|
+
const cli = cliRoot && readJson(path.join(cliRoot, 'package.json'));
|
|
47
|
+
const target = cli?.name === '@claude-flow/cli' ? (cli.exports?.['./memory']?.import ?? cli.exports?.['./memory']) : null;
|
|
48
|
+
if (typeof target !== 'string') return null;
|
|
49
|
+
const file = fs.realpathSync(path.resolve(cliRoot, target));
|
|
50
|
+
if (!inside(fs.realpathSync(cliRoot), file) || !fs.statSync(file).isFile()) return null;
|
|
51
|
+
return { node: nativeNodeFor(bin, { env, deadlineAt })?.node ?? process.execPath, moduleUrl: pathToFileURL(file).href };
|
|
52
|
+
} catch { return null; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* ONE owned child answering `list` / `get` for a store through Ruflo's public memory export. Spawned on first use,
|
|
57
|
+
* killed (whole process group) at the absolute deadline or on close(). Every op resolves, never rejects:
|
|
58
|
+
* { state: 'ok' | 'unavailable' | 'timed out' | 'fallback', rows?, value? } — 'fallback' means this route cannot
|
|
59
|
+
* answer (capability or shape mismatch) and the caller must use the per-read CLI route. `{success:false}` from
|
|
60
|
+
* Ruflo is 'unavailable', never an empty result.
|
|
61
|
+
*/
|
|
62
|
+
export function openBatchSession({ bin, storePath, env, deadline, makeCwd, signal }) {
|
|
63
|
+
const target = resolveBatchTarget(bin, { env, deadlineAt: deadline });
|
|
64
|
+
if (!target || !storePath) return null;
|
|
65
|
+
let child = null, cwd = null, ready = null, resolveReady = null, seq = 0, closed = false, timer;
|
|
66
|
+
const pending = new Map();
|
|
67
|
+
const settleAll = (state) => { for (const finish of [...pending.values()]) finish({ state }); };
|
|
68
|
+
const close = (state = 'unavailable') => {
|
|
69
|
+
if (closed) return; closed = true; clearTimeout(timer);
|
|
70
|
+
settleAll(state);
|
|
71
|
+
if (child) { killGroup(child); try { child.stdin.destroy(); } catch { /* gone */ } }
|
|
72
|
+
cwd?.cleanup?.();
|
|
73
|
+
};
|
|
74
|
+
const start = () => {
|
|
75
|
+
if (child || closed) return;
|
|
76
|
+
try {
|
|
77
|
+
cwd = makeCwd();
|
|
78
|
+
child = spawn(target.node, [fileURLToPath(new URL('./agentdb-recall-reader.mjs', import.meta.url)), '--session', storePath, target.moduleUrl],
|
|
79
|
+
{ cwd: cwd.cwd, env: { ...env, RUFLO_DAEMON_AUTOSTART: '0', CLAUDE_FLOW_AUTO_UPDATE: 'false', CLAUDE_FLOW_MEMORY_PATH: undefined },
|
|
80
|
+
stdio: ['pipe', 'pipe', 'ignore'], detached: process.platform !== 'win32', windowsHide: true });
|
|
81
|
+
} catch { child = null; close(); return; }
|
|
82
|
+
ready = new Promise((resolve) => {
|
|
83
|
+
resolveReady = resolve;
|
|
84
|
+
child.once('close', () => { resolve(false); if (!closed) settleAll('unavailable'); });
|
|
85
|
+
child.once('error', () => resolve(false));
|
|
86
|
+
});
|
|
87
|
+
let buffer = '';
|
|
88
|
+
child.stdout.setEncoding('utf8');
|
|
89
|
+
child.stdout.on('data', (chunk) => {
|
|
90
|
+
buffer += chunk;
|
|
91
|
+
for (let at = buffer.indexOf('\n'); at >= 0; at = buffer.indexOf('\n')) {
|
|
92
|
+
const line = buffer.slice(0, at); buffer = buffer.slice(at + 1);
|
|
93
|
+
let row; try { row = JSON.parse(line); } catch { continue; }
|
|
94
|
+
if (typeof row.ready === 'boolean') resolveReady(row.ready);
|
|
95
|
+
else pending.get(row.id)?.(row);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
child.stdin.on('error', () => {});
|
|
99
|
+
timer = setTimeout(() => close('timed out'), Math.max(1, deadline - Date.now()));
|
|
100
|
+
signal?.addEventListener('abort', () => close('unavailable'), { once: true });
|
|
101
|
+
};
|
|
102
|
+
const ask = (request, opDeadline = deadline) => new Promise((resolve) => {
|
|
103
|
+
if (closed || signal?.aborted) return resolve({ state: signal?.aborted ? 'unavailable' : 'timed out' });
|
|
104
|
+
if (opDeadline - Date.now() <= 0) return resolve({ state: 'timed out' });
|
|
105
|
+
start();
|
|
106
|
+
if (!child) return resolve({ state: 'unavailable' });
|
|
107
|
+
const id = ++seq;
|
|
108
|
+
const opTimer = opDeadline < deadline ? setTimeout(() => finish({ state: 'timed out' }), Math.max(1, opDeadline - Date.now())) : null;
|
|
109
|
+
const finish = (row) => { if (!pending.delete(id)) return; clearTimeout(opTimer); const { id: _id, ...rest } = row; resolve(rest); };
|
|
110
|
+
pending.set(id, finish);
|
|
111
|
+
ready.then((ok) => {
|
|
112
|
+
if (!pending.has(id)) return;
|
|
113
|
+
if (!ok) return finish({ state: closed ? 'unavailable' : 'fallback' });
|
|
114
|
+
try { child.stdin.write(JSON.stringify({ id, ...request }) + '\n'); } catch { finish({ state: 'unavailable' }); }
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return { storePath, close,
|
|
118
|
+
list: (namespace, limit, opDeadline) => ask({ op: 'list', namespace, limit }, opDeadline),
|
|
119
|
+
get: async (namespace, key, opDeadline) => { const r = await ask({ op: 'get', namespace, key }, opDeadline); return r.state === 'ok' ? { ...r, value: r.value.trim() } : r; } };
|
|
120
|
+
}
|
|
121
|
+
|
|
23
122
|
/** One ruflo search, bounded by an absolute deadline. Resolves { rows, state } — never rejects. */
|
|
24
123
|
export function searchOnce({ ruflo, store, args, deadline, env, scratch, operation = 'search', signal, invocation }) {
|
|
25
124
|
return new Promise((resolve) => {
|
|
@@ -82,7 +181,18 @@ export function searchOnce({ ruflo, store, args, deadline, env, scratch, operati
|
|
|
82
181
|
}
|
|
83
182
|
|
|
84
183
|
/** Isolate synchronous read-only SQLite work inside the same cancellable process budget. */
|
|
85
|
-
export async function nativeRead({ store, requests, namespaces, deadline, env, scratch, signal }) {
|
|
184
|
+
export async function nativeRead({ store, requests, namespaces, deadline, env, scratch, signal, session }) {
|
|
185
|
+
if (session && session.storePath === store.path) {
|
|
186
|
+
const rows = requests ? await Promise.all(requests.map(({ namespace, key }) => session.get(namespace, key)))
|
|
187
|
+
: await Promise.all(namespaces.map((namespace) => session.list(namespace, 100000)));
|
|
188
|
+
const states = rows.map((row) => row.state);
|
|
189
|
+
if (!states.includes('fallback')) {
|
|
190
|
+
const dead = states.find((state) => state === 'timed out') ?? (!requests && states.find((state) => state !== 'ok'));
|
|
191
|
+
if (dead) return { ok: false, fatal: true, state: dead };
|
|
192
|
+
return { ok: true, state: 'ok', value: requests ? rows.map((row) => (row.state === 'ok' ? row.value : null))
|
|
193
|
+
: Object.fromEntries(namespaces.map((namespace, i) => [namespace, rows[i].rows.map((row) => row.key).filter((key) => typeof key === 'string')])) };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
86
196
|
const result = await searchOnce({ store, deadline, env, scratch, signal, operation: 'native',
|
|
87
197
|
invocation: { executable: process.execPath, args: [fileURLToPath(new URL('./agentdb-recall-reader.mjs', import.meta.url)),
|
|
88
198
|
store.path, JSON.stringify({ requests, namespaces, deadline })] } });
|
|
@@ -94,7 +204,7 @@ export async function nativeRead({ store, requests, namespaces, deadline, env, s
|
|
|
94
204
|
} catch { return { ok: false, fatal: true, state: 'unavailable' }; }
|
|
95
205
|
}
|
|
96
206
|
|
|
97
|
-
export async function exactReads({ candidates, bin, deadline, env, scratch, signal }) {
|
|
207
|
+
export async function exactReads({ candidates, bin, deadline, env, scratch, signal, session }) {
|
|
98
208
|
const groups = new Map();
|
|
99
209
|
for (const candidate of candidates) {
|
|
100
210
|
const storePath = candidate.storePath;
|
|
@@ -104,7 +214,7 @@ export async function exactReads({ candidates, bin, deadline, env, scratch, sign
|
|
|
104
214
|
const results = new Map();
|
|
105
215
|
await Promise.all([...groups].map(async ([storePath, rows]) => {
|
|
106
216
|
const native = await nativeRead({ store: { path: storePath }, requests: rows.map(({ namespace, key }) => ({ namespace, key })),
|
|
107
|
-
deadline, env, scratch, signal });
|
|
217
|
+
deadline, env, scratch, signal, session });
|
|
108
218
|
await Promise.all(rows.map(async (row, index) => {
|
|
109
219
|
const result = native.ok ? { state: 'ok', value: native.value[index] ?? '' }
|
|
110
220
|
: native.fatal ? { state: native.state, value: '' }
|
|
@@ -3,8 +3,46 @@
|
|
|
3
3
|
* so a stalled CLI is retired at the recall deadline. Output contract unchanged: {ok, value}.
|
|
4
4
|
*/
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
|
+
import readline from 'node:readline';
|
|
6
7
|
import { resolveRuflo, rufloInvocationOrPlain } from './ruflo-bin.mjs';
|
|
7
8
|
|
|
9
|
+
/** Batch session (#429): ONE process answering line-based `list` / `get` requests by calling the installed Ruflo
|
|
10
|
+
* package's own public `@claude-flow/cli/memory` export — the functions its `memory list` / `memory retrieve`
|
|
11
|
+
* commands call. argv: --session <storePath> <moduleUrl>. Replies are single JSON lines on the real stdout. */
|
|
12
|
+
async function session(storePath, moduleUrl) {
|
|
13
|
+
const write = process.stdout.write.bind(process.stdout);
|
|
14
|
+
process.stdout.write = (chunk, ...rest) => process.stderr.write(chunk, ...rest); // module chatter must not corrupt the protocol
|
|
15
|
+
const reply = (row) => write(JSON.stringify(row) + '\n');
|
|
16
|
+
let mod;
|
|
17
|
+
try { mod = await import(moduleUrl); } catch { mod = null; }
|
|
18
|
+
if (!mod || typeof mod.listEntries !== 'function' || typeof mod.getEntry !== 'function' || typeof mod.resolveDbPath !== 'function') {
|
|
19
|
+
reply({ ready: false }); process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
const dbPath = mod.resolveDbPath(storePath);
|
|
22
|
+
reply({ ready: true });
|
|
23
|
+
const run = async ({ op, namespace, key, limit }) => {
|
|
24
|
+
if (typeof namespace !== 'string') return { state: 'fallback' };
|
|
25
|
+
if (op === 'list') {
|
|
26
|
+
const r = await mod.listEntries({ namespace, limit, offset: 0, dbPath });
|
|
27
|
+
if (r?.success === false) return { state: 'unavailable' };
|
|
28
|
+
// Every entry must carry a string key. If Ruflo renames the field the list is UNREADABLE by this route, never
|
|
29
|
+
// an empty namespace: fall back to the CLI route rather than report no memory (#429).
|
|
30
|
+
return r?.success === true && Array.isArray(r.entries) && r.entries.every((e) => e && typeof e === 'object' && typeof e.key === 'string')
|
|
31
|
+
? { state: 'ok', rows: r.entries.map((e) => ({ key: e.key, createdAt: e.createdAt })) } : { state: 'fallback' };
|
|
32
|
+
}
|
|
33
|
+
if (op === 'get' && typeof key === 'string' && key) {
|
|
34
|
+
const r = await mod.getEntry({ key, namespace, dbPath });
|
|
35
|
+
if (r?.success === false || r?.success === true && r.found === false) return { state: 'unavailable' };
|
|
36
|
+
return r?.success === true && typeof r.entry?.content === 'string' ? { state: 'ok', value: r.entry.content } : { state: 'fallback' };
|
|
37
|
+
}
|
|
38
|
+
return { state: 'fallback' };
|
|
39
|
+
};
|
|
40
|
+
readline.createInterface({ input: process.stdin }).on('line', (line) => {
|
|
41
|
+
let request; try { request = JSON.parse(line); } catch { return; }
|
|
42
|
+
run(request).catch(() => ({ state: 'unavailable' })).then((out) => reply({ id: request.id, ...out }));
|
|
43
|
+
}).on('close', () => process.exit(0));
|
|
44
|
+
}
|
|
45
|
+
|
|
8
46
|
const call = (bin, args, deadline) => new Promise((resolve) => {
|
|
9
47
|
const timeout = Math.max(1, deadline - Date.now());
|
|
10
48
|
const inv = rufloInvocationOrPlain(bin, args, { env: process.env });
|
|
@@ -12,7 +50,8 @@ const call = (bin, args, deadline) => new Promise((resolve) => {
|
|
|
12
50
|
(error, stdout) => resolve(error ? null : String(stdout || '')));
|
|
13
51
|
});
|
|
14
52
|
|
|
15
|
-
|
|
53
|
+
if (process.argv[2] === '--session') await session(process.argv[3], process.argv[4]);
|
|
54
|
+
else try {
|
|
16
55
|
const [storePath, input] = process.argv.slice(2);
|
|
17
56
|
const { requests, namespaces, deadline } = JSON.parse(input);
|
|
18
57
|
if (!Number.isFinite(deadline) || Date.now() >= deadline
|
|
@@ -33,7 +72,10 @@ try {
|
|
|
33
72
|
const out = await call(bin, ['memory', 'list', '--namespace', namespace, '--limit', '100000', '--format', 'json', '--path', storePath], deadline);
|
|
34
73
|
if (out === null) throw new Error('list failed');
|
|
35
74
|
const at = out.indexOf('[');
|
|
36
|
-
|
|
75
|
+
const rows = at < 0 ? [] : JSON.parse(out.slice(at));
|
|
76
|
+
// A list whose entries lack a string key is a changed contract, not an empty namespace: fail the read.
|
|
77
|
+
if (!Array.isArray(rows) || rows.some((row) => typeof row?.key !== 'string')) throw new Error('list shape changed');
|
|
78
|
+
return [namespace, rows.map((row) => row.key)];
|
|
37
79
|
}));
|
|
38
80
|
value = Object.fromEntries(lists);
|
|
39
81
|
}
|
|
@@ -22,10 +22,11 @@ import { safeAction } from './learn-capture.mjs';
|
|
|
22
22
|
import { redactText } from './continuity-events.mjs';
|
|
23
23
|
import { resolveTurnDb } from './turn-outcome-capture.mjs';
|
|
24
24
|
import { maskExcludedPaths } from './turn-capture-privacy.mjs';
|
|
25
|
-
import { searchOnce, nativeRead, exactReads, searchJsonResult } from './agentdb-recall-process.mjs';
|
|
25
|
+
import { searchOnce, nativeRead, exactReads, searchJsonResult, openBatchSession } from './agentdb-recall-process.mjs';
|
|
26
26
|
import { makeLesson } from './lesson-store.mjs';
|
|
27
27
|
import { supersededKeys, tombstonedKeys } from './decision-record.mjs';
|
|
28
28
|
import { checkpointMs, checkpointNamespaces, newestCheckpoint } from './checkpoint-keys.mjs';
|
|
29
|
+
import { isDirectInvocation } from './entrypoint.mjs';
|
|
29
30
|
export const STORE_FILES = Object.freeze(['memory.db']);
|
|
30
31
|
export const DEFAULT_DEADLINE_MS = 1900;
|
|
31
32
|
export const BLOCK_MAX_BYTES = 600;
|
|
@@ -49,8 +50,8 @@ export function recallBinding({ binding = {}, projectRoot, storePath, prompt } =
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
/** Bounded structural enumeration uses the same cancellable read-only canonical provider. */
|
|
52
|
-
export function enumerateCanonicalKeys({ storePath, namespaces, deadline, signal, env, scratch }) {
|
|
53
|
-
return nativeRead({ store: { path: storePath }, namespaces, deadline, signal, env, scratch });
|
|
53
|
+
export function enumerateCanonicalKeys({ storePath, namespaces, deadline, signal, env, scratch, session }) {
|
|
54
|
+
return nativeRead({ store: { path: storePath }, namespaces, deadline, signal, env, scratch, session });
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export function applicableRecallLesson(value, projectRoot) {
|
|
@@ -115,7 +116,9 @@ function clean(value, limit) {
|
|
|
115
116
|
return out;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
const
|
|
119
|
+
const MISSING_KNOWLEDGE = /\b(?:cannot\s+(?:be\s+determined|determine)|can(?:'|’)?t\s+(?:be\s+)?determined?|do\s+not\s+know|don(?:'|’)?t\s+know|cannot\s+answer|no\s+(?:verified\s+|supplied\s+|historical\s+)?(?:record|memory|evidence)\s+(?:is\s+)?(?:available|found))\b/i;
|
|
120
|
+
const MISSING_KNOWLEDGE_QUERY = /\b(?:fail(?:ed|ing|ures?)?|missing|unavailable|unknown)\b/i;
|
|
121
|
+
const NOISE_KEY =/^(?:verify[-_]|probe[-_]|test[-_]|rnb-quality-probe|session[-_]|turn[-_]|project-progress[-_]|cevt[-_])/i;
|
|
119
122
|
const SIGNAL_NAMESPACES = new Set(['lessons', 'patterns', 'pattern']);
|
|
120
123
|
/** Turn knowledge is the actual redacted outcome, never its session/transcript wrapper. */
|
|
121
124
|
function turnOutcomeClauses(value, prompt) {
|
|
@@ -231,12 +234,12 @@ export function formatBlock({ picks, status, withheld = 0 }) {
|
|
|
231
234
|
export const SEARCH_BUDGET_SHARE = 0.8;
|
|
232
235
|
export function searchDeadlineFor(started, deadline) { return Math.min(deadline, started + Math.floor((deadline - started) * SEARCH_BUDGET_SHARE)); }
|
|
233
236
|
|
|
234
|
-
async function consequentialRecall({ prompt, root, store, bin, scratch, env, deadline, binding, signal, enumerateKeys, exclusions, rankedSearch = true }) {
|
|
237
|
+
async function consequentialRecall({ prompt, root, store, bin, scratch, env, deadline, binding, signal, enumerateKeys, exclusions, rankedSearch = true, session }) {
|
|
235
238
|
const searchDeadline = searchDeadlineFor(Date.now(), deadline);
|
|
236
239
|
const phaseBinding = recallBinding({ binding, projectRoot: root, storePath: store.path, prompt });
|
|
237
240
|
const stateNamespaces = checkpointNamespaces(root);
|
|
238
241
|
const namespaces = [...new Set([...stateNamespaces, 'lessons', 'continuity-events'])];
|
|
239
|
-
const enumeration = await enumerateKeys({ storePath: store.path, namespaces, deadline, signal, env, scratch });
|
|
242
|
+
const enumeration = await enumerateKeys({ storePath: store.path, namespaces, deadline, signal, env, scratch, session });
|
|
240
243
|
if (!enumeration?.ok || signal?.aborted || Date.now() >= deadline) return { block: formatBlock({ picks: [], status: 'unavailable structural enumeration' }), picks: [], stores: [store],
|
|
241
244
|
status: { 'memory.db': 'unavailable' }, outcome: signal?.aborted ? 'unavailable' : Date.now() >= deadline ? 'timed-out' : 'unavailable', categories: { state: 'unavailable', decisions: 'unavailable', lessons: 'unavailable' } };
|
|
242
245
|
const keys = Object.fromEntries(namespaces.map(namespace => [namespace, Array.isArray(enumeration.value?.[namespace]) ? enumeration.value[namespace] : []]));
|
|
@@ -276,7 +279,7 @@ async function consequentialRecall({ prompt, root, store, bin, scratch, env, dea
|
|
|
276
279
|
const categories = { state: unorderedState ? 'unavailable' : 'ok-empty', decisions: decisionAvailability, lessons: lessonAvailability };
|
|
277
280
|
const records = [];
|
|
278
281
|
candidates.forEach(candidate => { candidate.storePath = store.path; });
|
|
279
|
-
const exactResults = await exactReads({ candidates, bin, deadline, env, scratch, signal });
|
|
282
|
+
const exactResults = await exactReads({ candidates, bin, deadline, env, scratch, signal, session });
|
|
280
283
|
// ADR-105 §8: supersession is a link on the NEWER record, computed from every decision record read, plus
|
|
281
284
|
// the tombstones already in the enumeration so a superseder that was not read still retires its target.
|
|
282
285
|
const superseded = tombstonedKeys(keys[path.basename(root)]);
|
|
@@ -335,6 +338,8 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
335
338
|
binding = {}, signal, absoluteDeadline, consequential = false, enumerateKeys = enumerateCanonicalKeys } = {}) {
|
|
336
339
|
const started = Date.now();
|
|
337
340
|
const empty = { block: '', picks: [], stores: [], status: {} };
|
|
341
|
+
let session = null;
|
|
342
|
+
let specAbort = null; // hoisted so `finally` can stop a speculative search that a later throw would orphan
|
|
338
343
|
try {
|
|
339
344
|
if (signal?.aborted) return { ...empty, outcome: 'unavailable', reason: 'cancelled' };
|
|
340
345
|
if (!agentdbFirstEnabled(env)) return { ...empty, outcome: 'disabled' };
|
|
@@ -367,7 +372,15 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
367
372
|
const store = stores[0];
|
|
368
373
|
const query = recallQuery(prompt);
|
|
369
374
|
const scratchFor = scratch || ((storePath) => rufloCwdFor(storePath, { root: rufloScratchRoot(env) }));
|
|
370
|
-
|
|
375
|
+
// ONE owned child answers every list/get of this recall through Ruflo's public memory export (#429); null
|
|
376
|
+
// (not the real package, no ./memory export, an injected `ruflo`) keeps the per-read CLI route.
|
|
377
|
+
if (store) {
|
|
378
|
+
session = openBatchSession({ bin, storePath: store.path, env, deadline, signal, makeCwd: () => {
|
|
379
|
+
const cwd = fs.mkdtempSync(path.join(scratchFor(store.path), 'run-'));
|
|
380
|
+
return { cwd, cleanup: () => { try { fs.rmSync(cwd, { recursive: true, force: true }); } catch { /* swept later */ } } };
|
|
381
|
+
} });
|
|
382
|
+
}
|
|
383
|
+
if (consequential) return await consequentialRecall({ prompt, root, store, bin, scratch: scratchFor, env, deadline, binding, signal, enumerateKeys, exclusions, session });
|
|
371
384
|
// Only namespaces something below reads: curated lessons/decisions, the checkpoint namespaces (dirname, its
|
|
372
385
|
// lowercase form, default) and turns for the single ranked search. Each one is a `ruflo memory list` process.
|
|
373
386
|
const namespaces = [...new Set(['lessons', 'turns', 'continuity-events', ...checkpointNamespaces(root)])];
|
|
@@ -375,12 +388,21 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
375
388
|
// per namespace; run concurrently they starve every other read past the deadline. So: ONE native enumeration
|
|
376
389
|
// (reused for the curated pass and for pruning empty namespaces), then exact curated state/lessons/decisions,
|
|
377
390
|
// then spend what remains on ranking.
|
|
378
|
-
|
|
379
|
-
|
|
391
|
+
// The batch child's start-up (loading Ruflo's memory module, ~0.4 s) and the ranked search are independent, so with a
|
|
392
|
+
// session the search for the first-choice namespace starts NOW instead of after enumeration (#429 probe: the
|
|
393
|
+
// sequence left a 1.3-1.6 s tail against the 1.9 s budget). Still ONE search: it is cancelled and replaced only if
|
|
394
|
+
// enumeration shows that namespace is empty.
|
|
395
|
+
const specNamespace = path.basename(root);
|
|
396
|
+
specAbort = new AbortController();
|
|
397
|
+
const specSignal = signal ? AbortSignal.any([signal, specAbort.signal]) : specAbort.signal;
|
|
398
|
+
const specArgs = ['--format', 'json', '-q', query, '-n', specNamespace, '--limit', '12'];
|
|
399
|
+
const spec = session && store ? searchOnce({ ruflo: bin, store, deadline: searchDeadlineFor(Date.now(), deadline), env, scratch: scratchFor, args: specArgs, signal: specSignal }) : null;
|
|
400
|
+
const presence = store ? await nativeRead({ store, namespaces, deadline, env, scratch: scratchFor, signal, session }) : null;
|
|
401
|
+
if (presence?.fatal) { specAbort.abort(); return { ...empty, stores, outcome: presence.state === 'timed out' ? 'timed-out' : 'unavailable',
|
|
380
402
|
block: formatBlock({ picks: [], status: presence.state }), reason: 'namespace enumeration failed' }; }
|
|
381
403
|
const sharedKeys = presence?.ok ? async () => ({ ok: true, value: presence.value }) : enumerateCanonicalKeys;
|
|
382
404
|
const curated = store ? await consequentialRecall({ prompt, root, store, bin, scratch: scratchFor, env, deadline, binding, signal,
|
|
383
|
-
enumerateKeys: sharedKeys, exclusions, rankedSearch: false }).catch(() => null) : null;
|
|
405
|
+
enumerateKeys: sharedKeys, exclusions, rankedSearch: false, session }).catch(() => null) : null;
|
|
384
406
|
const searchDeadline = searchDeadlineFor(Date.now(), deadline);
|
|
385
407
|
const activeNamespaces = presence?.ok ? namespaces.filter(namespace => presence.value[namespace]?.length) : namespaces;
|
|
386
408
|
// ONE supplementary search (owner ruling 2026-10-12). Measured on a live-size store at load 17: ~10
|
|
@@ -389,16 +411,19 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
389
411
|
const searchNamespace = [path.basename(root), 'default', 'turns'].find((namespace) => activeNamespaces.includes(namespace));
|
|
390
412
|
const jobs = store && searchNamespace ? [{ store, namespace: searchNamespace, family: null,
|
|
391
413
|
args: ['--format', 'json', '-q', query, '-n', searchNamespace, '--limit', '12'] }] : [];
|
|
414
|
+
const useSpec = spec && jobs[0]?.namespace === specNamespace;
|
|
415
|
+
if (!useSpec) specAbort.abort();
|
|
392
416
|
const results = await Promise.all(jobs.map(async ({ store: jobStore, namespace, family: recordFamily, args }) => ({ namespace, family: recordFamily, storePath: jobStore.path,
|
|
393
|
-
...await searchOnce({ ruflo: bin, store: jobStore, deadline: searchDeadline, env, scratch: scratchFor, args, signal }) })));
|
|
417
|
+
...await (useSpec ? spec : searchOnce({ ruflo: bin, store: jobStore, deadline: searchDeadline, env, scratch: scratchFor, args, signal })) })));
|
|
394
418
|
let status = results.every((r) => r.state === 'ok') ? 'ok'
|
|
395
419
|
: results.some((r) => r.state === 'timed out') ? 'timed out' : 'unavailable';
|
|
396
|
-
// Overfetch a bounded
|
|
397
|
-
//
|
|
398
|
-
|
|
420
|
+
// Overfetch a bounded twelve exact values (the one search already returns twelve; the exact reads are
|
|
421
|
+
// one batched reader, so this adds no Ruflo startup) so rejected turn metadata and repeated "cannot
|
|
422
|
+
// determine" answers cannot hide the next useful outcome (#429); at most three excerpts are delivered.
|
|
423
|
+
const candidates = pickRows(results, 12);
|
|
399
424
|
// Ruflo previews are ~60 characters. Read the actual selected values within
|
|
400
425
|
// the same deadline so the block contains useful evidence rather than titles.
|
|
401
|
-
const exactResults = await exactReads({ candidates, bin, deadline, env, scratch: scratchFor, signal });
|
|
426
|
+
const exactResults = await exactReads({ candidates, bin, deadline, env, scratch: scratchFor, signal, session });
|
|
402
427
|
const retrieved = await Promise.all(candidates.map(async (p) => {
|
|
403
428
|
const r = exactResults.get(p);
|
|
404
429
|
if (r.state === 'ok' && r.value && !r.value.startsWith('[WARN]')) {
|
|
@@ -417,6 +442,12 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
417
442
|
}
|
|
418
443
|
return { pick: null, state: r.state === 'ok' ? 'unavailable' : r.state };
|
|
419
444
|
}));
|
|
445
|
+
// A turn that admitted it could not answer is evidence of a failed recall, not the missing fact (#429).
|
|
446
|
+
// It stays available, after concrete outcomes; a query about failures keeps the ranked order.
|
|
447
|
+
if (!MISSING_KNOWLEDGE_QUERY.test(prompt)) {
|
|
448
|
+
const rank = ({ pick }) => Number(pick?.namespace === 'turns' && MISSING_KNOWLEDGE.test(pick.preview));
|
|
449
|
+
retrieved.sort((a, b) => rank(a) - rank(b));
|
|
450
|
+
}
|
|
420
451
|
const seenClauses = new Set();
|
|
421
452
|
const picks = retrieved.flatMap(({ pick }) => {
|
|
422
453
|
if (!pick) return [];
|
|
@@ -452,6 +483,7 @@ export async function recall({ prompt, projectDir = process.cwd(), env = process
|
|
|
452
483
|
authority: false };
|
|
453
484
|
return { block: formatBlock({ picks, status, withheld: curated?.withheld || 0 }), picks, stores, status: { 'memory.db': status }, outcome, receipt };
|
|
454
485
|
} catch (error) { return { ...empty, outcome: 'unavailable', reason: 'canonical recall failed: ' + clean(error.message, 180) }; }
|
|
486
|
+
finally { specAbort?.abort(); session?.close(); }
|
|
455
487
|
}
|
|
456
488
|
|
|
457
489
|
async function main() {
|
|
@@ -470,7 +502,7 @@ async function main() {
|
|
|
470
502
|
}
|
|
471
503
|
|
|
472
504
|
function isMain() {
|
|
473
|
-
try { return
|
|
505
|
+
try { return isDirectInvocation(import.meta.url); }
|
|
474
506
|
catch { return false; }
|
|
475
507
|
}
|
|
476
508
|
if (isMain()) main().catch(() => process.exit(0));
|
|
@@ -98,7 +98,15 @@ export function automaticInvocation(args, { source, home = os.homedir(), nodePat
|
|
|
98
98
|
const coordinator = developerCoordinatorOwner({ home });
|
|
99
99
|
if (coordinator.active) {
|
|
100
100
|
if (!coordinator.ready) throw new Error(`coordinated updater is not ready: ${coordinator.reason}`);
|
|
101
|
-
|
|
101
|
+
// Hooks and timers get knowledge only, never the whole suite (brew, cargo, every npm install) under a session TTL
|
|
102
|
+
// that kills it mid-install (AuditC #10). The full suite runs only from the scheduled runner, which calls the
|
|
103
|
+
// coordinator directly, not through here.
|
|
104
|
+
// The registered snapshot moves only when an --update reconciles it, so it can predate --knowledge-only
|
|
105
|
+
// (4.7.20 and older refuse it: "unsupported developer update argument", exit 1). Pass the flag only to a
|
|
106
|
+
// coordinator whose source parses it, as runUpdate does for the updater's --result-file.
|
|
107
|
+
const knowledgeOnly = fs.readFileSync(coordinator.entry, 'utf8').includes("'--knowledge-only'");
|
|
108
|
+
return { executable: nodePath, args: [coordinator.entry, '--apply', ...(knowledgeOnly ? ['--knowledge-only'] : [])],
|
|
109
|
+
source: 'developer-suite', coordinator };
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
source ??= updateSource({ home });
|
|
@@ -143,7 +143,8 @@ export function confirm({ footprint, env = process.env, home = os.homedir(), now
|
|
|
143
143
|
const checkable = Boolean(integrity) && fs.existsSync(path.join(roots.kbDir, 'CORPUS-COVERAGE.json'));
|
|
144
144
|
if (checkable && !integrity.valid) knowledgeProblems.push([`the bytes on disk fail the release-coverage check (${integrity.failures?.[0] || 'unknown'})`, UPDATE]);
|
|
145
145
|
// A newer corpus is published and not yet installed (identity, not age): advisory, never a gate.
|
|
146
|
-
const pending = newerCorpusPending({ source, corpusCheck: readJson(autoUpdatePaths(roots.brainHome).checkFile)
|
|
146
|
+
const pending = newerCorpusPending({ source, corpusCheck: readJson(autoUpdatePaths(roots.brainHome).checkFile),
|
|
147
|
+
installedAtMs: signed ? Date.parse(signature.verifiedAt || '') : NaN });
|
|
147
148
|
if (pending) {
|
|
148
149
|
const lockAt = readJson(autoUpdatePaths(roots.brainHome).lockFile)?.at;
|
|
149
150
|
const running = Number.isFinite(Date.parse(lockAt || '')) && now - Date.parse(lockAt) < AUTO_UPDATE_LOCK_STALE_MS;
|