total-recall-brain 3.28.2 → 3.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/total-recall.mjs +24 -1
- package/docs/ARCHITECTURE.md +26 -18
- package/extension/background.js +259 -84
- package/extension/content-script.css +7 -13
- package/extension/content-script.js +252 -477
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/icons/mark.svg +6 -0
- package/extension/lib/brain-client.js +160 -73
- package/extension/lib/tokens.css +187 -0
- package/extension/lib/ui-helpers.js +229 -0
- package/extension/lib/ui-helpers.spec.mjs +94 -0
- package/extension/manifest.json +20 -5
- package/extension/options/options.css +64 -218
- package/extension/options/options.html +91 -43
- package/extension/options/options.js +208 -85
- package/extension/sidepanel/sidepanel.css +268 -970
- package/extension/sidepanel/sidepanel.html +200 -166
- package/extension/sidepanel/sidepanel.js +788 -942
- package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/plugins.ts +124 -92
- package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
- package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
- package/frontend/src/pages/PluginsPage.tsx +454 -1218
- package/package.json +2 -1
- package/plugins/code-quality/README.md +25 -0
- package/plugins/code-quality/cli.mjs +25 -0
- package/plugins/code-quality/plugin.json +17 -0
- package/plugins/git-sentinel/README.md +27 -0
- package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
- package/plugins/git-sentinel/plugin.json +18 -0
- package/plugins/system-monitor/README.md +26 -0
- package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
- package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
- package/plugins/system-monitor/plugin.json +29 -0
- package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
- package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
- package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
- package/src/cli/agent.mjs +1 -1
- package/src/cli/app/index.mjs +493 -0
- package/src/cli/brain.mjs +33 -3
- package/src/cli/command.mjs +80 -41
- package/src/cli/harness.mjs +1 -1
- package/src/cli/init.mjs +3 -1
- package/src/cli/mesh.mjs +118 -12
- package/src/cli/plugin/create.mjs +144 -25
- package/src/cli/plugin/index.mjs +83 -53
- package/src/cli/plugin/install.mjs +10 -127
- package/src/cli/plugin/list.mjs +44 -44
- package/src/cli/plugin/remove.mjs +23 -30
- package/src/cli/plugin/scaffold-cmd.mjs +30 -0
- package/src/cli/plugin/search.mjs +108 -23
- package/src/cli/rebuild.mjs +37 -1
- package/src/cli/recall.mjs +17 -2
- package/src/cli/research.mjs +4 -6
- package/src/cli/share.mjs +2 -0
- package/src/core/app-deploy/apply.mjs +386 -0
- package/src/core/app-deploy/design-tokens.mjs +217 -0
- package/src/core/app-deploy/plan.mjs +249 -0
- package/src/core/app-deploy/resolve.mjs +295 -0
- package/src/core/app-deploy/source.mjs +337 -0
- package/src/core/app-deploy/standalone.mjs +248 -0
- package/src/core/app-deploy/upgrade.mjs +256 -0
- package/src/core/app-deploy/verify.mjs +213 -0
- package/src/core/bound-hosts.mjs +1 -1
- package/src/core/brain-registry.mjs +146 -0
- package/src/core/brain-state.json +1 -0
- package/src/core/daemon-loop.mjs +30 -26
- package/src/core/dream.mjs +5 -17
- package/src/core/fact-seeker.mjs +53 -368
- package/src/core/lan-discovery.mjs +3 -3
- package/src/core/mesh-access-sync.mjs +134 -0
- package/src/core/mesh-access.mjs +18 -3
- package/src/core/mesh-discover.mjs +173 -0
- package/src/core/mesh.mjs +131 -32
- package/src/core/optimizer.mjs +4 -41
- package/src/core/plugin-bundle.mjs +167 -0
- package/src/core/plugin-context.mjs +8 -13
- package/src/core/plugin-loader.mjs +272 -68
- package/src/core/plugin-peers.mjs +163 -0
- package/src/core/plugin-public.mjs +85 -0
- package/src/core/plugin-runner-child.mjs +24 -0
- package/src/core/plugin-runner.mjs +102 -0
- package/src/core/plugin-store.mjs +384 -0
- package/src/core/plugin-tasks.mjs +212 -0
- package/src/core/post-mortem.mjs +40 -9
- package/src/core/research-gate.mjs +182 -0
- package/src/core/research-queue.mjs +47 -7
- package/src/core/research-surface.mjs +211 -0
- package/src/core/research.mjs +25 -32
- package/src/core/scheduler.mjs +67 -153
- package/src/core/schema.mjs +129 -0
- package/src/core/search.mjs +12 -1
- package/src/core/secret-integration-research.mjs +12 -8
- package/src/core/secrets-store.mjs +32 -2
- package/src/core/session-watcher.mjs +6 -0
- package/src/core/surface.mjs +64 -3
- package/src/core/task-executors.mjs +8 -45
- package/src/core/vault-cache.mjs +70 -4
- package/src/server/api.mjs +21 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +21 -5
- package/src/server/routes/collab.mjs +38 -11
- package/src/server/routes/extension.mjs +3 -2
- package/src/server/routes/memory.mjs +24 -2
- package/src/server/routes/mesh.mjs +7 -7
- package/src/server/routes/plugins-mesh.mjs +49 -0
- package/src/server/routes/plugins.mjs +91 -426
- package/src/server/routes/research.mjs +5 -2
- package/src/server/routes/share.mjs +2 -0
- package/src/server/tools.mjs +29 -2
- package/extension/icons/README.md +0 -27
- package/extension/lib/preconfigured.js +0 -5
- package/extension/popup/popup.css +0 -227
- package/extension/popup/popup.html +0 -50
- package/extension/popup/popup.js +0 -175
- package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
- package/scaffold/.agent/plugins/.gitkeep +0 -0
- package/scaffold/.agent/plugins/code-quality/README.md +0 -16
- package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
- package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
- package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
- package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
- package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
- package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
- package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
- package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
- package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
package/src/core/daemon-loop.mjs
CHANGED
|
@@ -190,12 +190,15 @@ import { releaseLease } from './leader-election.mjs';
|
|
|
190
190
|
* daemon.log across three separate SIGTERMs. The fallback timer bounds that, so
|
|
191
191
|
* even a wedged iteration cannot keep the daemon alive indefinitely.
|
|
192
192
|
*/
|
|
193
|
+
let stopPluginTasks = null;
|
|
194
|
+
|
|
193
195
|
function shutdown(signal) {
|
|
194
196
|
if (shuttingDown) return;
|
|
195
197
|
shuttingDown = true;
|
|
196
198
|
logger.info({ subsystem: 'daemon-loop', message: `${signal} received — shutting down gracefully` });
|
|
197
199
|
running = false;
|
|
198
200
|
if (vaultWatcher) vaultWatcher.stop();
|
|
201
|
+
if (stopPluginTasks) stopPluginTasks();
|
|
199
202
|
|
|
200
203
|
const fallback = setTimeout(() => {
|
|
201
204
|
logger.info({
|
|
@@ -365,6 +368,15 @@ async function main() {
|
|
|
365
368
|
|
|
366
369
|
let emptyTicks = 0;
|
|
367
370
|
|
|
371
|
+
// Plugin tasks are per node (a plugin installed here runs here), so they are
|
|
372
|
+
// scheduled on their own timer rather than inside the leader-only task loop.
|
|
373
|
+
try {
|
|
374
|
+
const { startPluginTaskScheduler } = await import('./plugin-tasks.mjs');
|
|
375
|
+
stopPluginTasks = startPluginTaskScheduler();
|
|
376
|
+
} catch (err) {
|
|
377
|
+
logger.warn('daemon-loop', `Plugin task scheduler failed to start: ${err.message}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
368
380
|
while (running) {
|
|
369
381
|
try {
|
|
370
382
|
const leader = await isLeader();
|
|
@@ -649,33 +661,25 @@ async function main() {
|
|
|
649
661
|
} else if (task._research_phase === 'deliberation') {
|
|
650
662
|
patch.status = 'pending';
|
|
651
663
|
patch.research_phase = 'improvement';
|
|
652
|
-
} else
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
patch.
|
|
657
|
-
patch.
|
|
658
|
-
} else if (task._research_phase === 'expansion') {
|
|
659
|
-
// Only mark done when we still have a real node
|
|
660
|
-
if (nodeSlug && nodeSlug !== 'pending') {
|
|
661
|
-
patch.status = 'done';
|
|
662
|
-
patch.node_slug = nodeSlug;
|
|
663
|
-
patch.completed_at = new Date().toISOString();
|
|
664
|
-
} else {
|
|
665
|
-
patch.status = 'failed';
|
|
666
|
-
patch.notes = 'Expansion finished without a research memory node.';
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
} else {
|
|
670
|
-
// Failure on later phase: if no node, reset to acquisition for retry
|
|
671
|
-
if (
|
|
672
|
-
(!task._node_slug || task._node_slug === 'pending') &&
|
|
673
|
-
task._research_phase !== 'acquisition'
|
|
674
|
-
) {
|
|
675
|
-
patch.status = 'pending';
|
|
676
|
-
patch.research_phase = 'acquisition';
|
|
677
|
-
patch.notes = `Reset to acquisition after ${task._research_phase} failure: ${result.error || 'unknown'}`;
|
|
664
|
+
} else {
|
|
665
|
+
// Improvement is the last phase: research ends here and never
|
|
666
|
+
// re-opens or spawns follow-ups (RESEARCH_SYSTEM2).
|
|
667
|
+
patch.status = 'done';
|
|
668
|
+
patch.node_slug = nodeSlug;
|
|
669
|
+
patch.completed_at = new Date().toISOString();
|
|
678
670
|
}
|
|
671
|
+
} else if (task._research_phase !== 'acquisition' && task._node_slug && task._node_slug !== 'pending') {
|
|
672
|
+
// A refinement phase failed but the report already exists: it is
|
|
673
|
+
// usable as-is, so finish rather than parking it in a retry loop.
|
|
674
|
+
patch.status = 'done';
|
|
675
|
+
patch.node_slug = task._node_slug;
|
|
676
|
+
patch.completed_at = new Date().toISOString();
|
|
677
|
+
patch.notes = `Report kept; ${task._research_phase} skipped after error: ${result.error || 'unknown'}`;
|
|
678
|
+
} else if (task._research_phase !== 'acquisition') {
|
|
679
|
+
// Later phase with no node to refine: start over from acquisition.
|
|
680
|
+
patch.status = 'pending';
|
|
681
|
+
patch.research_phase = 'acquisition';
|
|
682
|
+
patch.notes = `Reset to acquisition after ${task._research_phase} failure: ${result.error || 'unknown'}`;
|
|
679
683
|
}
|
|
680
684
|
updateQueueItem(task._research_id, patch);
|
|
681
685
|
} catch (err) {
|
package/src/core/dream.mjs
CHANGED
|
@@ -247,17 +247,17 @@ import {
|
|
|
247
247
|
generateStaleKnowledgeRefreshProposals,
|
|
248
248
|
evaluateProposalGate,
|
|
249
249
|
dedupeProposals,
|
|
250
|
-
refreshStaleKnowledge
|
|
251
250
|
} from './optimizer.mjs';
|
|
252
251
|
import { applyAcceptedProposals } from './proposal-applier.mjs';
|
|
253
252
|
|
|
254
253
|
const DREAM_PROMOTION_THRESHOLD = 0.7;
|
|
255
254
|
|
|
256
255
|
// Master switch for the stale-knowledge-refresh *ticket generator* (see PHASE 4).
|
|
257
|
-
// Disabled 2026-08-01
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
256
|
+
// Disabled 2026-08-01: it filed one .md per stale node every cycle (16,401
|
|
257
|
+
// unread tickets at its peak). Its successor, which queued "Verify
|
|
258
|
+
// still-current" research every cycle, was removed too (RESEARCH_SYSTEM2):
|
|
259
|
+
// research runs only when a human asks or a project needs it. Stale nodes are
|
|
260
|
+
// listed for a human by `total-recall proposals stale` / GET /api/proposals/stale.
|
|
261
261
|
const ENABLE_STALE_KNOWLEDGE_REFRESH = false;
|
|
262
262
|
|
|
263
263
|
/**
|
|
@@ -563,18 +563,6 @@ export async function runDreamCycle({
|
|
|
563
563
|
logger.error('dream', `Optimizer failed: ${err.message}`);
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
-
// Staleness is handled here rather than as proposals: the research daemon can
|
|
567
|
-
// actually re-verify a stale node and commit the result, whereas a proposal
|
|
568
|
-
// could only ask someone to. Rate-limited so the backlog drains over cycles.
|
|
569
|
-
try {
|
|
570
|
-
const { enqueued, stale } = await refreshStaleKnowledge(vaultDir);
|
|
571
|
-
if (stale > 0) {
|
|
572
|
-
logger.info('dream', `Staleness sweep: ${stale} stale nodes, ${enqueued.length} queued for research this cycle.`);
|
|
573
|
-
}
|
|
574
|
-
} catch (err) {
|
|
575
|
-
logger.error('dream', `Staleness sweep failed: ${err.message}`);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
566
|
// Write daily note summary (native SSSS node; Obsidian Daily Notes reads it directly)
|
|
579
567
|
try {
|
|
580
568
|
const existingNodes = getNodes(vaultDir);
|
package/src/core/fact-seeker.mjs
CHANGED
|
@@ -7,8 +7,6 @@ import { callLocalRuntime, cleanAndParseJSON } from './runtime.mjs';
|
|
|
7
7
|
import { writeNode, atomicWrite, safeStringify } from './vault.mjs';
|
|
8
8
|
import { getNodes } from './vault-cache.mjs';
|
|
9
9
|
import { logger } from './logger.mjs';
|
|
10
|
-
import { addToQueue } from './research-queue.mjs';
|
|
11
|
-
import { persistTaskToDisk } from './scheduler.mjs';
|
|
12
10
|
import {
|
|
13
11
|
loadResearchConfig,
|
|
14
12
|
checkSourceAvailability,
|
|
@@ -271,48 +269,51 @@ function registerSource(sourceResult, factSlug) {
|
|
|
271
269
|
|
|
272
270
|
// ─── Topic Inference ─────────────────────────────────────────────────────────────
|
|
273
271
|
|
|
274
|
-
function TOPIC_INFERENCE_SYSTEM(runtimeConfig) {
|
|
272
|
+
function TOPIC_INFERENCE_SYSTEM(runtimeConfig, project) {
|
|
275
273
|
const today = getLocalizedDateTime();
|
|
276
274
|
const cutoff = runtimeConfig?.training_cutoff || 'January 2025';
|
|
277
|
-
return `You are
|
|
275
|
+
return `You are the background (System 2) research planner for an AI coding agent. Today's date and time is ${today}. The agent's training data ends around ${cutoff}.
|
|
276
|
+
|
|
277
|
+
You are given a transcript of the agent working on the project "${project}". Identify KNOWLEDGE GAPS: specific facts the agent lacked that would measurably improve its work on THIS project if researched in the background. Good gaps:
|
|
278
|
+
- APIs, SDKs, libraries, services or versions the project actually uses whose behaviour may have changed after ${cutoff}.
|
|
279
|
+
- Errors or behaviours the agent could not explain or resolve.
|
|
280
|
+
- Places where the agent guessed, hedged, or contradicted itself about an external tool.
|
|
281
|
+
|
|
282
|
+
Not gaps: general curiosity, topics merely mentioned, things the transcript already answers, and anything unrelated to "${project}". Returning an empty list is normal and correct when there is no real gap.
|
|
278
283
|
|
|
279
284
|
Output valid JSON:
|
|
280
285
|
{
|
|
281
|
-
"
|
|
286
|
+
"gaps": [
|
|
282
287
|
{
|
|
283
|
-
"topic": "string (specific
|
|
284
|
-
"
|
|
285
|
-
"
|
|
286
|
-
"tags": ["tag1", "tag2"],
|
|
287
|
-
"suggested_sources": ["brave-search", "arxiv", "npm", "github", "wikipedia"]
|
|
288
|
+
"topic": "string (specific and searchable, e.g. 'Stripe Node SDK v17 webhook signature verification')",
|
|
289
|
+
"rationale": "string (the moment in the transcript that exposed the gap, and how the answer helps this project)",
|
|
290
|
+
"priority": 1-100
|
|
288
291
|
}
|
|
289
292
|
]
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
Rules:
|
|
293
|
-
-
|
|
294
|
-
- Topics must be SPECIFIC and SEARCHABLE — not vague categories.
|
|
295
|
-
- Include version numbers, tool names, library names when relevant.
|
|
296
|
-
- Prioritize topics post-${cutoff} or where the agent showed uncertainty.
|
|
296
|
+
- At most 3 gaps.
|
|
297
297
|
- Output ONLY valid JSON.`;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/**
|
|
301
|
-
* Infer
|
|
301
|
+
* Infer project knowledge gaps from a session transcript via the local LLM.
|
|
302
302
|
*/
|
|
303
|
-
export async function inferTopicsFromSession(transcript, runtimeConfig) {
|
|
303
|
+
export async function inferTopicsFromSession(transcript, runtimeConfig, { project = 'this project' } = {}) {
|
|
304
304
|
if (!transcript || transcript.length < 100) return [];
|
|
305
305
|
|
|
306
|
-
const prompt = `
|
|
306
|
+
const prompt = `Transcript of the agent working on "${project}":\n\n${transcript.slice(0, 8000)}`;
|
|
307
307
|
|
|
308
308
|
try {
|
|
309
|
-
const raw = await callLocalRuntime(prompt, TOPIC_INFERENCE_SYSTEM(runtimeConfig), runtimeConfig);
|
|
309
|
+
const raw = await callLocalRuntime(prompt, TOPIC_INFERENCE_SYSTEM(runtimeConfig, project), runtimeConfig);
|
|
310
310
|
const match = raw.match(/\{[\s\S]*\}/);
|
|
311
311
|
if (!match) return [];
|
|
312
312
|
const result = cleanAndParseJSON(match[0]);
|
|
313
|
-
|
|
313
|
+
const gaps = Array.isArray(result.gaps) ? result.gaps : (Array.isArray(result.topics) ? result.topics : []);
|
|
314
|
+
return gaps.filter((g) => g && typeof g.topic === 'string' && g.topic.trim()).slice(0, 3);
|
|
314
315
|
} catch (err) {
|
|
315
|
-
logger.info({ subsystem: 'fact-seeker', message: `
|
|
316
|
+
logger.info({ subsystem: 'fact-seeker', message: `Gap inference failed: ${err.message}` });
|
|
316
317
|
return [];
|
|
317
318
|
}
|
|
318
319
|
}
|
|
@@ -762,7 +763,10 @@ async function writeAndSurfaceImmediately(topic, synthesis, sourceResults, {
|
|
|
762
763
|
try {
|
|
763
764
|
const { compileSurface } = await import('./surface.mjs');
|
|
764
765
|
await compileSurface({ vaultDir, skillsDir, derivedDir, instructionsFile });
|
|
765
|
-
|
|
766
|
+
// Research reaches the instructions through the Background Research section
|
|
767
|
+
// once its queue item is done (research-surface.mjs); this recompile only
|
|
768
|
+
// refreshes indexes and any rules the synthesis produced.
|
|
769
|
+
logger.info({ subsystem: 'fact-seeker', message: `[FAST-PATH] Surface recompiled after writing "${topic}"` });
|
|
766
770
|
} catch (err) {
|
|
767
771
|
logger.info({ subsystem: 'fact-seeker', message: `[FAST-PATH] Surface recompile failed (non-fatal): ${err.message}` });
|
|
768
772
|
}
|
|
@@ -872,17 +876,10 @@ export async function runSelfDiagnosis({ vaultDir, runtimeConfig }) {
|
|
|
872
876
|
if (!match) throw new Error('No JSON in diagnosis response');
|
|
873
877
|
const diagnosis = cleanAndParseJSON(match[0]);
|
|
874
878
|
|
|
875
|
-
//
|
|
879
|
+
// Report only. Recommendations are not queued: research runs only when a
|
|
880
|
+
// human asks or the project gate approves a gap (RESEARCH_SYSTEM2).
|
|
876
881
|
const recommended = Array.isArray(diagnosis.recommended_immediate_research)
|
|
877
882
|
? diagnosis.recommended_immediate_research : [];
|
|
878
|
-
for (const rec of recommended.filter(r => r.priority >= 70)) {
|
|
879
|
-
addToAgenda({
|
|
880
|
-
topic: rec.topic,
|
|
881
|
-
priority: rec.priority,
|
|
882
|
-
source: 'self-diagnosis',
|
|
883
|
-
rationale: rec.reason,
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
883
|
|
|
887
884
|
// Log source warnings
|
|
888
885
|
for (const warning of availability.warnings) {
|
|
@@ -932,23 +929,13 @@ export async function runKnowledgeAcquisitionCycle({
|
|
|
932
929
|
}) {
|
|
933
930
|
const researchConfig = loadResearchConfig();
|
|
934
931
|
|
|
935
|
-
//
|
|
936
|
-
let
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
topicEntry = getNextAgendaTopic();
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
if (!topicEntry) {
|
|
944
|
-
// No agenda items — run self-diagnosis to generate new ones
|
|
945
|
-
logger.info({ subsystem: 'fact-seeker', message: 'Agenda empty — running self-diagnosis to generate topics' });
|
|
946
|
-
await runSelfDiagnosis({ vaultDir, runtimeConfig });
|
|
947
|
-
topicEntry = getNextAgendaTopic();
|
|
948
|
-
if (!topicEntry) {
|
|
949
|
-
return { topic: null, skipped: 'empty-agenda-after-diagnosis' };
|
|
950
|
-
}
|
|
932
|
+
// Research needs a topic somebody chose. There is no "next agenda topic"
|
|
933
|
+
// fallback and no self-diagnosis topic invention: those let research feed on
|
|
934
|
+
// itself (RESEARCH_SYSTEM2). The agenda is only a coverage ledger now.
|
|
935
|
+
if (!forceTopic || !String(forceTopic).trim()) {
|
|
936
|
+
return { topic: null, skipped: 'no-topic' };
|
|
951
937
|
}
|
|
938
|
+
const topicEntry = addToAgenda({ topic: forceTopic, priority: 90, source: 'direct-instruction', directInstruction: true });
|
|
952
939
|
|
|
953
940
|
const { topic, id: topicId } = topicEntry;
|
|
954
941
|
logger.info({ subsystem: 'fact-seeker', message: `Researching: "${topic}" (priority: ${topicEntry.priority})` });
|
|
@@ -994,32 +981,8 @@ export async function runKnowledgeAcquisitionCycle({
|
|
|
994
981
|
const coverageScore = Math.min(1.0, (results.length / 5) * confidence);
|
|
995
982
|
markTopicResearched(topicId, { coverageScore, sourcesConsulted: sourcesUsed });
|
|
996
983
|
|
|
997
|
-
//
|
|
998
|
-
//
|
|
999
|
-
// Bounded per parent topic. Research runs five phases and the monitoring and
|
|
1000
|
-
// expansion phases repeat, so an uncapped 3 gaps per cycle compounds without
|
|
1001
|
-
// limit — two completed reports generated 116 and 98 follow-ups respectively,
|
|
1002
|
-
// none of which ever ran. The agenda is a backlog, not a fan-out tree.
|
|
1003
|
-
const FOLLOW_UP_CAP_PER_TOPIC = 5;
|
|
1004
|
-
const alreadySpawned = loadAgenda().filter(
|
|
1005
|
-
(t) => t.source === `follow-up:${topic}`,
|
|
1006
|
-
).length;
|
|
1007
|
-
const remaining = Math.max(0, FOLLOW_UP_CAP_PER_TOPIC - alreadySpawned);
|
|
1008
|
-
const gaps = (synthesis.further_research_needed || []).slice(0, remaining);
|
|
1009
|
-
if (remaining === 0 && (synthesis.further_research_needed || []).length > 0) {
|
|
1010
|
-
logger.info({
|
|
1011
|
-
subsystem: 'fact-seeker',
|
|
1012
|
-
message: `Follow-up cap reached for "${topic}" (${alreadySpawned} already queued); not spawning more.`,
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
for (const gap of gaps) {
|
|
1016
|
-
addToAgenda({
|
|
1017
|
-
topic: gap,
|
|
1018
|
-
priority: Math.max(20, topicEntry.priority - 15),
|
|
1019
|
-
source: `follow-up:${topic}`,
|
|
1020
|
-
rationale: `Gap identified while researching "${topic}"`,
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
984
|
+
// Gaps the synthesis names stay in the report (further_research_needed) for a
|
|
985
|
+
// human to act on; they are never queued automatically.
|
|
1023
986
|
|
|
1024
987
|
logger.info({
|
|
1025
988
|
subsystem: 'fact-seeker',
|
|
@@ -1030,32 +993,20 @@ export async function runKnowledgeAcquisitionCycle({
|
|
|
1030
993
|
}
|
|
1031
994
|
|
|
1032
995
|
/**
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
996
|
+
* Background research for a project session (System 2): infer knowledge gaps
|
|
997
|
+
* from the transcript and hand them to the research gate, which applies the
|
|
998
|
+
* autonomous budgets and skips anything existing research already covers.
|
|
999
|
+
*
|
|
1000
|
+
* @returns {Promise<string[]>} topics actually queued
|
|
1035
1001
|
*/
|
|
1036
|
-
export async function ingestSessionTopics(sessionTranscript, runtimeConfig) {
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
source: 'session-inference',
|
|
1045
|
-
rationale: t.rationale || '',
|
|
1046
|
-
tags: t.tags || [],
|
|
1047
|
-
});
|
|
1048
|
-
added.push(entry.topic);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
if (added.length > 0) {
|
|
1052
|
-
logger.info({
|
|
1053
|
-
subsystem: 'fact-seeker',
|
|
1054
|
-
message: `Added ${added.length} topics from session: ${added.join(', ')}`,
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
return added;
|
|
1002
|
+
export async function ingestSessionTopics(sessionTranscript, runtimeConfig, { project, sessionId = null, gate } = {}) {
|
|
1003
|
+
if (!project) return [];
|
|
1004
|
+
const gaps = await inferTopicsFromSession(sessionTranscript, runtimeConfig, { project });
|
|
1005
|
+
if (gaps.length === 0) return [];
|
|
1006
|
+
|
|
1007
|
+
const propose = gate || (await import('./research-gate.mjs')).proposeAutonomousResearch;
|
|
1008
|
+
const { queued } = await propose(gaps, { project, sessionId, via: 'session' });
|
|
1009
|
+
return queued.map((item) => item.topic);
|
|
1059
1010
|
}
|
|
1060
1011
|
|
|
1061
1012
|
// ─── Specialized Cognitive Research Phase Engines ────────────────────────────────
|
|
@@ -1178,14 +1129,6 @@ Output valid JSON:
|
|
|
1178
1129
|
"predicate": "use",
|
|
1179
1130
|
"object": "string (what the rule applies to)"
|
|
1180
1131
|
}
|
|
1181
|
-
],
|
|
1182
|
-
"autonomous_tasks": [
|
|
1183
|
-
{
|
|
1184
|
-
"priority": 50,
|
|
1185
|
-
"category": "proactive-research",
|
|
1186
|
-
"reason": "string (why this task is needed)",
|
|
1187
|
-
"body": "string (detailed markdown instructions for the task)"
|
|
1188
|
-
}
|
|
1189
1132
|
]
|
|
1190
1133
|
}
|
|
1191
1134
|
|
|
@@ -1196,10 +1139,6 @@ Rules for synthesized_instructions:
|
|
|
1196
1139
|
- Subject and predicate MUST be alphanumeric (letters, numbers, hyphens, underscores, dots, spaces only). E.g., subject="agent", predicate="use".
|
|
1197
1140
|
- Keep rules highly actionable.
|
|
1198
1141
|
|
|
1199
|
-
Rules for autonomous_tasks:
|
|
1200
|
-
- Category MUST be one of: "memory-maintenance", "system2-deliberation", "skill-engineering", "proactive-research", "self-evaluation", "exploration".
|
|
1201
|
-
- Priority MUST be an integer between 1 and 100.
|
|
1202
|
-
|
|
1203
1142
|
Output ONLY valid JSON.`;
|
|
1204
1143
|
|
|
1205
1144
|
try {
|
|
@@ -1388,37 +1327,12 @@ Output ONLY valid JSON.`;
|
|
|
1388
1327
|
}
|
|
1389
1328
|
}
|
|
1390
1329
|
|
|
1391
|
-
//
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const { persistTaskToDisk } = await import('./scheduler.mjs');
|
|
1395
|
-
for (const t of result.autonomous_tasks) {
|
|
1396
|
-
const taskSlug = `delib-task-${crypto.createHash('md5').update(t.task || t.topic || t.description || '').digest('hex').slice(0, 8)}`;
|
|
1397
|
-
const taskNode = {
|
|
1398
|
-
type: 'task',
|
|
1399
|
-
slug: taskSlug,
|
|
1400
|
-
priority: t.priority || 50,
|
|
1401
|
-
category: t.category || 'proactive-research',
|
|
1402
|
-
status: 'pending',
|
|
1403
|
-
created_by: 'fact-seeker-deliberation',
|
|
1404
|
-
reason: t.reason || `Autonomously generated from deliberation on "${topic}"`,
|
|
1405
|
-
body: t.body || `Follow up on research for "${topic}"`,
|
|
1406
|
-
progress: 0,
|
|
1407
|
-
estimated_calls: 5,
|
|
1408
|
-
deadline: new Date(Date.now() + 86400000).toISOString().split('T')[0]
|
|
1409
|
-
};
|
|
1410
|
-
|
|
1411
|
-
persistTaskToDisk(taskNode, queueDir);
|
|
1412
|
-
logger.info({
|
|
1413
|
-
subsystem: 'fact-seeker',
|
|
1414
|
-
message: `Scheduled autonomous deliberation task: ${taskSlug} (${t.category})`
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1330
|
+
// Deliberation refines this report only. It does not queue follow-up work:
|
|
1331
|
+
// research never spawns research (RESEARCH_SYSTEM2).
|
|
1332
|
+
|
|
1419
1333
|
return {
|
|
1420
1334
|
success: true,
|
|
1421
|
-
output: `Deliberated on "${topic}". Generated ${result.insights?.length || 0} insights, ${result.connections?.length || 0} connections, ${result.synthesized_instructions?.length || 0} instructions
|
|
1335
|
+
output: `Deliberated on "${topic}". Generated ${result.insights?.length || 0} insights, ${result.connections?.length || 0} connections, ${result.synthesized_instructions?.length || 0} instructions.`,
|
|
1422
1336
|
factSlug: nodeSlug
|
|
1423
1337
|
};
|
|
1424
1338
|
} catch (err) {
|
|
@@ -1494,236 +1408,7 @@ Output ONLY the raw markdown of the polished body, starting immediately with no
|
|
|
1494
1408
|
}
|
|
1495
1409
|
|
|
1496
1410
|
/**
|
|
1497
|
-
*
|
|
1498
|
-
*/
|
|
1499
|
-
export async function runResearchMonitoringCycle({
|
|
1500
|
-
vaultDir,
|
|
1501
|
-
nodeSlug,
|
|
1502
|
-
topic,
|
|
1503
|
-
runtimeConfig,
|
|
1504
|
-
skillsDir,
|
|
1505
|
-
derivedDir,
|
|
1506
|
-
instructionsFile
|
|
1507
|
-
}) {
|
|
1508
|
-
const nodes = getNodes(vaultDir);
|
|
1509
|
-
const targetNode = nodes.find(n => n.slug === nodeSlug);
|
|
1510
|
-
if (!targetNode) {
|
|
1511
|
-
return { success: false, error: `Target node not found for slug: ${nodeSlug}` };
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
const researchConfig = loadResearchConfig();
|
|
1515
|
-
const searchQuery = `${topic} newsletters blogs release notes feeds updates`;
|
|
1516
|
-
|
|
1517
|
-
let results = [];
|
|
1518
|
-
try {
|
|
1519
|
-
results = await webSearch(searchQuery, researchConfig, 5);
|
|
1520
|
-
} catch (err) {
|
|
1521
|
-
logger.info({ subsystem: 'fact-seeker', message: `Web search for monitoring failed: ${err.message}` });
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
if (results.length === 0) {
|
|
1525
|
-
return { success: true, output: `Monitoring skipped: no search results found for query: "${searchQuery}"`, factSlug: nodeSlug };
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
const sourceText = results.map(r => `Title: ${r.title}\nURL: ${r.url}\nSnippet: ${r.snippet}`).join('\n\n');
|
|
1529
|
-
|
|
1530
|
-
const today = getLocalizedDateTime();
|
|
1531
|
-
const prompt = `Today's Date/Time: ${today}
|
|
1532
|
-
Research Topic: "${topic}"
|
|
1533
|
-
Search Results for ongoing updates/feeds/newsletters/releases:
|
|
1534
|
-
${sourceText}
|
|
1535
|
-
|
|
1536
|
-
Extract 2-4 authoritative, high-quality ongoing sources (newsletters, RSS feeds, official release notes, blogs, communities) for this topic.`;
|
|
1537
|
-
|
|
1538
|
-
const systemPrompt = `You are a Research Monitoring Specialist. Today's date and time is ${today}.
|
|
1539
|
-
Your job is to identify high-quality ongoing sources of information (e.g. RSS feeds, newsletters, official release notes, specific blogs) from search results.
|
|
1540
|
-
|
|
1541
|
-
Output valid JSON:
|
|
1542
|
-
{
|
|
1543
|
-
"sources": [
|
|
1544
|
-
{
|
|
1545
|
-
"name": "string (name of the newsletter, blog, or release feed)",
|
|
1546
|
-
"url": "string (direct URL)",
|
|
1547
|
-
"type": "string (newsletter / RSS feed / official blog / release notes / etc.)",
|
|
1548
|
-
"description": "string (why this is a reliable ongoing source)"
|
|
1549
|
-
}
|
|
1550
|
-
]
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
Output ONLY valid JSON.`;
|
|
1554
|
-
|
|
1555
|
-
try {
|
|
1556
|
-
const raw = await callLocalRuntime(prompt, systemPrompt, runtimeConfig);
|
|
1557
|
-
const match = raw.match(/\{[\s\S]*\}/);
|
|
1558
|
-
if (!match) throw new Error('No JSON in monitoring response');
|
|
1559
|
-
const parsed = cleanAndParseJSON(match[0]);
|
|
1560
|
-
const sources = parsed.sources || [];
|
|
1561
|
-
|
|
1562
|
-
if (sources.length === 0) {
|
|
1563
|
-
return { success: true, output: `Monitoring complete: no ongoing sources extracted for "${topic}".`, factSlug: nodeSlug };
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
let updatedBody = targetNode.body;
|
|
1567
|
-
const linesToAdd = ['', '## Ongoing Monitoring & Sources'];
|
|
1568
|
-
sources.forEach(src => {
|
|
1569
|
-
linesToAdd.push(`- **[${src.name}](${src.url})** (${src.type}) — ${src.description}`);
|
|
1570
|
-
});
|
|
1571
|
-
|
|
1572
|
-
updatedBody += '\n' + linesToAdd.join('\n');
|
|
1573
|
-
|
|
1574
|
-
const updatedNode = {
|
|
1575
|
-
...targetNode,
|
|
1576
|
-
body: updatedBody,
|
|
1577
|
-
tags: [...new Set([...(targetNode.tags || []), 'monitored'])],
|
|
1578
|
-
updated: new Date().toISOString()
|
|
1579
|
-
};
|
|
1580
|
-
|
|
1581
|
-
await writeNode(updatedNode, vaultDir);
|
|
1582
|
-
|
|
1583
|
-
// Trigger immediate surface recompilation if active rules/sources were added
|
|
1584
|
-
if (skillsDir && derivedDir && instructionsFile) {
|
|
1585
|
-
try {
|
|
1586
|
-
const { compileSurface } = await import('./surface.mjs');
|
|
1587
|
-
await compileSurface({ vaultDir, skillsDir, derivedDir, instructionsFile });
|
|
1588
|
-
logger.info({
|
|
1589
|
-
subsystem: 'fact-seeker',
|
|
1590
|
-
message: `Surface compiled with newly monitored sources.`
|
|
1591
|
-
});
|
|
1592
|
-
} catch (err) {
|
|
1593
|
-
logger.info({
|
|
1594
|
-
subsystem: 'fact-seeker',
|
|
1595
|
-
message: `Surface compilation failed (non-fatal): ${err.message}`
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
return {
|
|
1601
|
-
success: true,
|
|
1602
|
-
output: `Monitoring complete: added ${sources.length} ongoing sources for "${topic}".`,
|
|
1603
|
-
factSlug: nodeSlug
|
|
1604
|
-
};
|
|
1605
|
-
} catch (err) {
|
|
1606
|
-
return { success: false, error: `Monitoring failed: ${err.message}` };
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
/**
|
|
1611
|
-
* Phase 5/5: Domain Tangents Expansion
|
|
1612
|
-
*/
|
|
1613
|
-
export async function runResearchExpansionCycle({ vaultDir, nodeSlug, topic, runtimeConfig }) {
|
|
1614
|
-
const nodes = getNodes(vaultDir);
|
|
1615
|
-
const targetNode = nodes.find(n => n.slug === nodeSlug);
|
|
1616
|
-
if (!targetNode) {
|
|
1617
|
-
return { success: false, error: `Target node not found for slug: ${nodeSlug}` };
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
const today = getLocalizedDateTime();
|
|
1621
|
-
const prompt = `Today's Date/Time: ${today}
|
|
1622
|
-
Target Node Topic: "${topic}"
|
|
1623
|
-
Title: ${targetNode.title}
|
|
1624
|
-
Body:
|
|
1625
|
-
${targetNode.body}
|
|
1626
|
-
|
|
1627
|
-
Deliberate on what adjacent domain topics must be researched next to build complete expertise.
|
|
1628
|
-
Brainstorm 2-3 highly specific, searchable research topics/questions.`;
|
|
1629
|
-
|
|
1630
|
-
const systemPrompt = `You are a Research Expansion Specialist. Today's date and time is ${today}.
|
|
1631
|
-
Your job is to brainstorm the next logical adjacent topics that must be researched to build comprehensive domain expertise on the target topic.
|
|
1632
|
-
|
|
1633
|
-
Output valid JSON:
|
|
1634
|
-
{
|
|
1635
|
-
"tangents": [
|
|
1636
|
-
{
|
|
1637
|
-
"topic": "string (specific, searchable topic)",
|
|
1638
|
-
"priority": "low" | "medium" | "high",
|
|
1639
|
-
"rationale": "string (why this tangent is crucial next step)"
|
|
1640
|
-
}
|
|
1641
|
-
]
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
Output ONLY valid JSON.`;
|
|
1645
|
-
|
|
1646
|
-
try {
|
|
1647
|
-
const raw = await callLocalRuntime(prompt, systemPrompt, runtimeConfig);
|
|
1648
|
-
const match = raw.match(/\{[\s\S]*\}/);
|
|
1649
|
-
if (!match) throw new Error('No JSON in expansion response');
|
|
1650
|
-
const parsed = cleanAndParseJSON(match[0]);
|
|
1651
|
-
const tangents = parsed.tangents || [];
|
|
1652
|
-
|
|
1653
|
-
if (tangents.length === 0) {
|
|
1654
|
-
return { success: true, output: `Expansion complete: no new tangents brainstormed for "${topic}".`, factSlug: nodeSlug };
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
const localBrainDir = getBrainDir();
|
|
1658
|
-
const queueDir = path.join(localBrainDir, 'scheduler', 'queue');
|
|
1659
|
-
const { persistTaskToDisk } = await import('./scheduler.mjs');
|
|
1660
|
-
|
|
1661
|
-
const spawnedList = [];
|
|
1662
|
-
for (const tangent of tangents.slice(0, 3)) {
|
|
1663
|
-
try {
|
|
1664
|
-
const added = addToQueue({
|
|
1665
|
-
topic: tangent.topic,
|
|
1666
|
-
priority: tangent.priority || 'medium',
|
|
1667
|
-
notes: `Autonomously spawned from research on "${topic}". Rationale: ${tangent.rationale}`
|
|
1668
|
-
});
|
|
1669
|
-
spawnedList.push(added);
|
|
1670
|
-
|
|
1671
|
-
// Also schedule a scheduler task to run proactive research for this topic
|
|
1672
|
-
const prio = tangent.priority === 'high' ? 65 : (tangent.priority === 'low' ? 45 : 55);
|
|
1673
|
-
const taskSlug = `proactive-research-${crypto.createHash('md5').update(tangent.topic || '').digest('hex').slice(0, 8)}`;
|
|
1674
|
-
const taskNode = {
|
|
1675
|
-
type: 'task',
|
|
1676
|
-
slug: taskSlug,
|
|
1677
|
-
priority: prio,
|
|
1678
|
-
category: 'proactive-research',
|
|
1679
|
-
status: 'pending',
|
|
1680
|
-
created_by: 'fact-seeker-expansion',
|
|
1681
|
-
reason: tangent.rationale || `Explore research tangent on: ${tangent.topic}`,
|
|
1682
|
-
body: `Autonomously spawned from research on "${topic}".\n\nTopic: ${tangent.topic}\n\nRationale:\n${tangent.rationale}`,
|
|
1683
|
-
progress: 0,
|
|
1684
|
-
estimated_calls: 5,
|
|
1685
|
-
deadline: new Date(Date.now() + 86400000).toISOString().split('T')[0]
|
|
1686
|
-
};
|
|
1687
|
-
|
|
1688
|
-
persistTaskToDisk(taskNode, queueDir);
|
|
1689
|
-
logger.info({
|
|
1690
|
-
subsystem: 'fact-seeker',
|
|
1691
|
-
message: `Scheduled expansion task: ${taskSlug} for topic "${tangent.topic}"`
|
|
1692
|
-
});
|
|
1693
|
-
} catch (err) {
|
|
1694
|
-
logger.error({ subsystem: 'fact-seeker', message: `Failed to auto-spawn tangent "${tangent.topic}": ${err.message}` });
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
let updatedBody = targetNode.body;
|
|
1699
|
-
const linesToAdd = ['', '## Spawned Research Expansion Avenues'];
|
|
1700
|
-
tangents.forEach(t => {
|
|
1701
|
-
linesToAdd.push(`- **${t.topic}** (${t.priority || 'medium'}) — ${t.rationale}`);
|
|
1702
|
-
});
|
|
1703
|
-
|
|
1704
|
-
updatedBody += '\n' + linesToAdd.join('\n');
|
|
1705
|
-
|
|
1706
|
-
const updatedNode = {
|
|
1707
|
-
...targetNode,
|
|
1708
|
-
body: updatedBody,
|
|
1709
|
-
tags: [...new Set([...(targetNode.tags || []), 'expanded'])],
|
|
1710
|
-
updated: new Date().toISOString()
|
|
1711
|
-
};
|
|
1712
|
-
|
|
1713
|
-
await writeNode(updatedNode, vaultDir);
|
|
1714
|
-
|
|
1715
|
-
return {
|
|
1716
|
-
success: true,
|
|
1717
|
-
output: `Autonomously spawned ${spawnedList.length} new research tangents: ${spawnedList.map(s => `"${s.topic}"`).join(', ')}.`,
|
|
1718
|
-
factSlug: nodeSlug
|
|
1719
|
-
};
|
|
1720
|
-
} catch (err) {
|
|
1721
|
-
return { success: false, error: `Tangent expansion failed: ${err.message}` };
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
/**
|
|
1726
|
-
* Phase 6: Memory Compaction (Consolidation)
|
|
1411
|
+
* Memory Compaction (Consolidation)
|
|
1727
1412
|
* Finds redundant or overlapping fact nodes and merges them into a consolidated node.
|
|
1728
1413
|
*/
|
|
1729
1414
|
export async function runMemoryCompaction({ vaultDir, inboxDir, runtimeConfig }) {
|
|
@@ -210,13 +210,13 @@ export function lanHostnameFromIp(ip) {
|
|
|
210
210
|
* @param {{ vaultRoot?: string, workspace_id?: string, onlyReachable?: boolean }} [options]
|
|
211
211
|
*/
|
|
212
212
|
export async function registerLanMeshNodes(hosts, options = {}) {
|
|
213
|
-
const {
|
|
214
|
-
const { meshNodeDocSlug } = await import('./mesh.mjs');
|
|
213
|
+
const { findVfsDocumentByPath, listVfsDocumentsUnder } = await import('./vfs-documents.mjs');
|
|
214
|
+
const { meshNodeDocSlug, meshVaultRoot } = await import('./mesh.mjs');
|
|
215
215
|
const { processViaPackageKernel } = await import('./ssss-kernel-bridge.mjs');
|
|
216
216
|
const crypto = await import('node:crypto');
|
|
217
217
|
const path = await import('node:path');
|
|
218
218
|
|
|
219
|
-
const vaultRoot = options.vaultRoot ||
|
|
219
|
+
const vaultRoot = options.vaultRoot || meshVaultRoot();
|
|
220
220
|
const onlyReachable = options.onlyReachable !== false;
|
|
221
221
|
const candidates = (hosts || []).filter((h) => h?.ip && isLanIpv4(h.ip) && (!onlyReachable || h.tr_reachable));
|
|
222
222
|
|