total-recall-brain 3.28.2 → 3.30.1
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/SKILL.md +22 -6
- 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/connect.mjs +10 -1
- 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 +76 -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/scheduler.mjs
CHANGED
|
@@ -189,24 +189,40 @@ export function persistTaskToDisk(task, queueDir) {
|
|
|
189
189
|
return filepath;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// ─── Research Pipeline ──────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/** A failed research item is retried at most this many times, then stays failed. */
|
|
195
|
+
export const MAX_RESEARCH_ATTEMPTS = 3;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The research pipeline. It ends at improvement: there is deliberately no
|
|
199
|
+
* monitoring or expansion phase, because research must never spawn research.
|
|
200
|
+
*/
|
|
201
|
+
export const RESEARCH_PHASES = Object.freeze({
|
|
202
|
+
acquisition: {
|
|
203
|
+
step: 1, label: 'Acquisition', category: 'proactive-research',
|
|
204
|
+
body: (i) => `Run knowledge acquisition cycle for queued topic: ${i.topic}.\nNotes: ${i.notes || 'None'}`,
|
|
205
|
+
},
|
|
206
|
+
deliberation: {
|
|
207
|
+
step: 2, label: 'Deliberation', category: 'system2-deliberation',
|
|
208
|
+
body: (i) => `Run deep System 2 cognitive deliberation for topic: ${i.topic}.\nTarget node: ${i.node_slug || 'pending'}`,
|
|
209
|
+
},
|
|
210
|
+
improvement: {
|
|
211
|
+
step: 3, label: 'Improvement', category: 'memory-maintenance',
|
|
212
|
+
body: (i) => `Run document improvement and formatting refinement for topic: ${i.topic}.\nTarget node: ${i.node_slug || 'pending'}`,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
const RESEARCH_PHASE_COUNT = Object.keys(RESEARCH_PHASES).length;
|
|
216
|
+
|
|
192
217
|
// ─── Idle Task Generation ───────────────────────────────────────────────────────
|
|
193
218
|
|
|
194
219
|
let _idleCycleCounter = 0;
|
|
195
220
|
|
|
196
221
|
/**
|
|
197
222
|
* Generate an idle improvement task when the explicit queue is empty.
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* 0: proactive-research — web search + synthesize from agenda
|
|
202
|
-
* 1: inference — draw System 2 conclusions from vault clusters
|
|
203
|
-
* 2: proactive-research — web search (second topic)
|
|
204
|
-
* 3: staleness-check — flag outdated facts
|
|
205
|
-
* 4: proactive-research — web search (third topic)
|
|
206
|
-
* 5: inference — draw more conclusions
|
|
207
|
-
* 6: post-mortem — re-analyze most recent session
|
|
208
|
-
* 7: clarity-review — improve a vault node
|
|
209
|
-
* 8: cutoff-audit — check for training-drift
|
|
223
|
+
* Purely local maintenance, round-robin: inference, post-mortem, clarity
|
|
224
|
+
* review, memory compaction. Idle ticks never research on their own; research
|
|
225
|
+
* only runs for topics a human asked for or the project gate approved.
|
|
210
226
|
*/
|
|
211
227
|
export function generateIdleTask({ vaultDir, sessionsDir }) {
|
|
212
228
|
// Only allow purely local task generation strategies.
|
|
@@ -229,23 +245,6 @@ export function generateIdleTask({ vaultDir, sessionsDir }) {
|
|
|
229
245
|
}
|
|
230
246
|
}
|
|
231
247
|
|
|
232
|
-
/**
|
|
233
|
-
* Generate a proactive research task — pulls the next topic from the agenda
|
|
234
|
-
* and queues a full web search + synthesis cycle.
|
|
235
|
-
*/
|
|
236
|
-
function generateProactiveResearchTask() {
|
|
237
|
-
return {
|
|
238
|
-
type: 'task',
|
|
239
|
-
slug: `proactive-research-${Date.now().toString(36)}`,
|
|
240
|
-
priority: 55,
|
|
241
|
-
category: 'proactive-research',
|
|
242
|
-
status: 'pending',
|
|
243
|
-
created_by: 'scheduler-idle',
|
|
244
|
-
reason: 'Idle task: pull next topic from research agenda and execute web search + synthesis.',
|
|
245
|
-
body: 'Run one knowledge acquisition cycle: pull highest-priority pending topic from agenda, gather from web/wikipedia/arXiv/npm/github, synthesize with LLM, write cited fact node to vault.',
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
248
|
/**
|
|
250
249
|
* Pick a random active memory node and create a clarity review task.
|
|
251
250
|
*/
|
|
@@ -285,54 +284,6 @@ function generateMemoryCompactionTask(vaultDir) {
|
|
|
285
284
|
};
|
|
286
285
|
}
|
|
287
286
|
|
|
288
|
-
/**
|
|
289
|
-
* Pick the oldest fact node and create a staleness check task.
|
|
290
|
-
*/
|
|
291
|
-
function generateStalenessCheckTask(vaultDir) {
|
|
292
|
-
const nodes = getNodes(vaultDir)
|
|
293
|
-
.filter((n) => n.status === 'active' && n.category === 'facts')
|
|
294
|
-
.sort((a, b) => {
|
|
295
|
-
const aDate = new Date(a.last_accessed || a.updated || 0).getTime();
|
|
296
|
-
const bDate = new Date(b.last_accessed || b.updated || 0).getTime();
|
|
297
|
-
return aDate - bDate; // oldest first
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
if (nodes.length === 0) {
|
|
301
|
-
// No facts? Pick any old node
|
|
302
|
-
const allNodes = getNodes(vaultDir).filter((n) => n.status === 'active');
|
|
303
|
-
if (allNodes.length === 0) {
|
|
304
|
-
return makeFallbackTask('research-acquisition', 'No facts to verify');
|
|
305
|
-
}
|
|
306
|
-
const oldest = allNodes.sort((a, b) => {
|
|
307
|
-
return new Date(a.updated || 0).getTime() - new Date(b.updated || 0).getTime();
|
|
308
|
-
})[0];
|
|
309
|
-
return {
|
|
310
|
-
type: 'task',
|
|
311
|
-
slug: `staleness-check-${oldest.slug}-${Date.now().toString(36)}`,
|
|
312
|
-
priority: 25,
|
|
313
|
-
category: 'research-acquisition',
|
|
314
|
-
target: oldest.slug,
|
|
315
|
-
status: 'pending',
|
|
316
|
-
created_by: 'scheduler-idle',
|
|
317
|
-
reason: `Idle task: verify if "${oldest.title}" is still current.`,
|
|
318
|
-
body: `## Objective\nEvaluate whether memory node "${oldest.slug}" is still accurate given its age and domain.`,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const target = nodes[0];
|
|
323
|
-
return {
|
|
324
|
-
type: 'task',
|
|
325
|
-
slug: `staleness-check-${target.slug}-${Date.now().toString(36)}`,
|
|
326
|
-
priority: 25,
|
|
327
|
-
category: 'research-acquisition',
|
|
328
|
-
target: target.slug,
|
|
329
|
-
status: 'pending',
|
|
330
|
-
created_by: 'scheduler-idle',
|
|
331
|
-
reason: `Idle task: verify if fact "${target.title}" is still current.`,
|
|
332
|
-
body: `## Objective\nThis fact was last accessed on ${target.last_accessed || 'unknown'}.\nDetermine if it is: STILL_VALID, POSSIBLY_STALE, or LIKELY_OUTDATED.\nIf stale, suggest a verification search query.`,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
287
|
/**
|
|
337
288
|
* Pick a cluster of related nodes and create an inference task.
|
|
338
289
|
*/
|
|
@@ -499,107 +450,70 @@ export function createScheduler({ queueDir, vaultDir, sessionsDir }) {
|
|
|
499
450
|
});
|
|
500
451
|
}
|
|
501
452
|
|
|
502
|
-
// Load pending research queue tasks
|
|
453
|
+
// Load pending research queue tasks.
|
|
454
|
+
//
|
|
455
|
+
// Research ends when it is answered: acquisition → deliberation → improvement →
|
|
456
|
+
// done. Nothing here re-opens finished work. (A `done` item used to be reset to
|
|
457
|
+
// a monitoring pass every hour, forever, and the expansion phase spawned three
|
|
458
|
+
// new topics per finished one — see docs/projects/.../RESEARCH_SYSTEM2_AUDIT.md.)
|
|
459
|
+
// Failed items retry after a cooldown, at most MAX_RESEARCH_ATTEMPTS times.
|
|
503
460
|
try {
|
|
504
461
|
const COOLDOWN_MS = process.env.RESEARCH_COOLDOWN_MS
|
|
505
462
|
? parseInt(process.env.RESEARCH_COOLDOWN_MS, 10)
|
|
506
463
|
: 60 * 60 * 1000; // 1 hour default
|
|
507
464
|
|
|
508
|
-
const allItems = loadQueue();
|
|
509
465
|
let resetCount = 0;
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (item.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
item.
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
status: 'pending',
|
|
528
|
-
research_phase: 'acquisition',
|
|
529
|
-
notes: emptyDone
|
|
530
|
-
? 'Auto-reset empty done project → re-run acquisition (previous run produced no memory node)'
|
|
531
|
-
: item.notes || null,
|
|
532
|
-
});
|
|
533
|
-
} else {
|
|
534
|
-
// Has a real node — optional re-monitor after cooldown (don't wipe conclusions)
|
|
535
|
-
updateQueueItem(item.id, {
|
|
536
|
-
status: 'pending',
|
|
537
|
-
research_phase: 'monitoring',
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
resetCount++;
|
|
541
|
-
} catch (updateErr) {
|
|
542
|
-
logger.error({
|
|
543
|
-
subsystem: 'scheduler',
|
|
544
|
-
message: `Failed to reset research queue item ${item.id} to pending: ${updateErr.message}`,
|
|
545
|
-
});
|
|
546
|
-
}
|
|
466
|
+
for (const item of loadQueue()) {
|
|
467
|
+
if (item.status !== 'failed') continue;
|
|
468
|
+
if ((item.attempts || 0) >= MAX_RESEARCH_ATTEMPTS) continue;
|
|
469
|
+
const failedAt = new Date(item.updated_at || item.created_at || 0).getTime();
|
|
470
|
+
if (Date.now() - failedAt < COOLDOWN_MS) continue;
|
|
471
|
+
try {
|
|
472
|
+
updateQueueItem(item.id, {
|
|
473
|
+
status: 'pending',
|
|
474
|
+
research_phase: item.node_slug ? (item.research_phase || 'acquisition') : 'acquisition',
|
|
475
|
+
attempts: (item.attempts || 0) + 1,
|
|
476
|
+
});
|
|
477
|
+
resetCount++;
|
|
478
|
+
} catch (updateErr) {
|
|
479
|
+
logger.error({
|
|
480
|
+
subsystem: 'scheduler',
|
|
481
|
+
message: `Failed to retry research queue item ${item.id}: ${updateErr.message}`,
|
|
482
|
+
});
|
|
547
483
|
}
|
|
548
484
|
}
|
|
549
|
-
|
|
550
485
|
if (resetCount > 0) {
|
|
551
486
|
logger.info({
|
|
552
487
|
subsystem: 'scheduler',
|
|
553
|
-
message: `
|
|
488
|
+
message: `Retrying ${resetCount} failed research items (max ${MAX_RESEARCH_ATTEMPTS} attempts each).`,
|
|
554
489
|
});
|
|
555
490
|
}
|
|
556
491
|
|
|
492
|
+
// User requests run before autonomous background research; oldest first within each.
|
|
557
493
|
const researchItems = loadQueue()
|
|
558
|
-
.filter((i) => i.status === 'pending')
|
|
559
|
-
.sort((a, b) =>
|
|
494
|
+
.filter((i) => i.status === 'pending' && RESEARCH_PHASES[i.research_phase || 'acquisition'])
|
|
495
|
+
.sort((a, b) => {
|
|
496
|
+
const ua = a.origin === 'autonomous' ? 1 : 0;
|
|
497
|
+
const ub = b.origin === 'autonomous' ? 1 : 0;
|
|
498
|
+
if (ua !== ub) return ua - ub;
|
|
499
|
+
return new Date(a.updated_at || a.created_at || 0).getTime() - new Date(b.updated_at || b.created_at || 0).getTime();
|
|
500
|
+
});
|
|
560
501
|
|
|
561
502
|
for (let idx = 0; idx < researchItems.length; idx++) {
|
|
562
503
|
const item = researchItems[idx];
|
|
563
|
-
const dynamicPriority = 85 + (researchItems.length - idx);
|
|
564
504
|
const phase = item.research_phase || 'acquisition';
|
|
565
|
-
|
|
566
|
-
let category = 'proactive-research';
|
|
567
|
-
let slug = `research-acquisition-${item.id}`;
|
|
568
|
-
let reason = `Queued research project (Phase 1/5: Acquisition): ${item.topic}`;
|
|
569
|
-
let body = `Run knowledge acquisition cycle for queued topic: ${item.topic}.\nNotes: ${item.notes || 'None'}`;
|
|
570
|
-
|
|
571
|
-
if (phase === 'deliberation') {
|
|
572
|
-
category = 'system2-deliberation';
|
|
573
|
-
slug = `research-deliberation-${item.id}`;
|
|
574
|
-
reason = `Queued research project (Phase 2/5: Deliberation): ${item.topic}`;
|
|
575
|
-
body = `Run deep System 2 cognitive deliberation for topic: ${item.topic}.\nTarget node: ${item.node_slug || 'pending'}`;
|
|
576
|
-
} else if (phase === 'improvement') {
|
|
577
|
-
category = 'memory-maintenance';
|
|
578
|
-
slug = `research-improvement-${item.id}`;
|
|
579
|
-
reason = `Queued research project (Phase 3/5: Improvement): ${item.topic}`;
|
|
580
|
-
body = `Run document improvement and formatting refinement for topic: ${item.topic}.\nTarget node: ${item.node_slug || 'pending'}`;
|
|
581
|
-
} else if (phase === 'monitoring') {
|
|
582
|
-
category = 'proactive-research';
|
|
583
|
-
slug = `research-monitoring-${item.id}`;
|
|
584
|
-
reason = `Queued research project (Phase 4/5: Monitoring): ${item.topic}`;
|
|
585
|
-
body = `Identify reliable sources of ongoing information, news, feeds or release notes for: ${item.topic}.\nTarget node: ${item.node_slug || 'pending'}`;
|
|
586
|
-
} else if (phase === 'expansion') {
|
|
587
|
-
category = 'exploration';
|
|
588
|
-
slug = `research-expansion-${item.id}`;
|
|
589
|
-
reason = `Queued research project (Phase 5/5: Expansion): ${item.topic}`;
|
|
590
|
-
body = `Discover adjacent research domains, brainstorm tangents, and auto-spawn follow-up tasks for: ${item.topic}.\nTarget node: ${item.node_slug || 'pending'}`;
|
|
591
|
-
}
|
|
505
|
+
const spec = RESEARCH_PHASES[phase];
|
|
592
506
|
|
|
593
507
|
queue.enqueue({
|
|
594
508
|
type: 'task',
|
|
595
|
-
slug
|
|
596
|
-
priority:
|
|
597
|
-
category,
|
|
509
|
+
slug: `research-${phase}-${item.id}`,
|
|
510
|
+
priority: 85 + (researchItems.length - idx),
|
|
511
|
+
category: spec.category,
|
|
598
512
|
target: item.topic,
|
|
599
513
|
status: 'pending',
|
|
600
514
|
created_by: 'research-queue',
|
|
601
|
-
reason
|
|
602
|
-
body,
|
|
515
|
+
reason: `Queued research project (Phase ${spec.step}/${RESEARCH_PHASE_COUNT}: ${spec.label}): ${item.topic}`,
|
|
516
|
+
body: spec.body(item),
|
|
603
517
|
_research_id: item.id,
|
|
604
518
|
_research_phase: phase,
|
|
605
519
|
_node_slug: item.node_slug,
|
package/src/core/schema.mjs
CHANGED
|
@@ -1043,6 +1043,8 @@ export const MeshNodeSchema = z.object({
|
|
|
1043
1043
|
/** Snapshot of host NICs (kinds are classified heuristically). */
|
|
1044
1044
|
interfaces: z.array(NetworkInterfaceEntrySchema).optional(),
|
|
1045
1045
|
lan_ip: z.string().nullable().optional(),
|
|
1046
|
+
/** Port of this node's brain HTTP service, for mesh service discovery. */
|
|
1047
|
+
brain_port: z.number().int().positive().max(65535).nullable().optional(),
|
|
1046
1048
|
/**
|
|
1047
1049
|
* Device I/O profile — entity variables for agent UI generation
|
|
1048
1050
|
* (touchscreen, mic, speaker, screen, keyboard, …).
|
|
@@ -1064,6 +1066,125 @@ export const DaemonLeaderSchema = z.object({
|
|
|
1064
1066
|
lease_ttl_seconds: z.number().int().positive().default(60),
|
|
1065
1067
|
}).passthrough();
|
|
1066
1068
|
|
|
1069
|
+
/**
|
|
1070
|
+
* Install record for one plugin. Lives beside the plugin, in the vault of the
|
|
1071
|
+
* brain that owns its plugins directory: `system/plugins/<id>.md`.
|
|
1072
|
+
* Every field is a fact this node observed — there is deliberately no rating,
|
|
1073
|
+
* review, or popularity data anywhere in the plugin model.
|
|
1074
|
+
*/
|
|
1075
|
+
export const PluginRecordSchema = z.object({
|
|
1076
|
+
type: z.literal('plugin_record'),
|
|
1077
|
+
title: z.string().min(1),
|
|
1078
|
+
description: z.string().min(1),
|
|
1079
|
+
timestamp: ssssDatetime(),
|
|
1080
|
+
plugin_id: z.string().regex(/^[a-z][a-z0-9-]{1,63}$/),
|
|
1081
|
+
version: z.string().min(1),
|
|
1082
|
+
scope: z.enum(['project', 'global']),
|
|
1083
|
+
/** Legacy private mesh sharing. Public sharing requires a separate opt-in. */
|
|
1084
|
+
shared: z.boolean().default(false),
|
|
1085
|
+
public_shared: z.boolean().default(false),
|
|
1086
|
+
source: z.object({
|
|
1087
|
+
kind: z.enum(['bundled', 'public', 'peer', 'git', 'local', 'link']),
|
|
1088
|
+
ref: z.string().min(1),
|
|
1089
|
+
peer_hostname: z.string().nullable().optional(),
|
|
1090
|
+
}).passthrough(),
|
|
1091
|
+
/** Content hash at install time (see plugin-bundle.mjs). */
|
|
1092
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/).nullable().optional(),
|
|
1093
|
+
installed_at: ssssDatetime(),
|
|
1094
|
+
/** Last minute-slot (YYYY-MM-DDTHH:MM) each scheduled task command ran in. */
|
|
1095
|
+
task_runs: z.record(z.string()).optional(),
|
|
1096
|
+
}).passthrough();
|
|
1097
|
+
|
|
1098
|
+
const SECRET_KEY_PATTERN = /^(.*_)?(secret|token|password|api[_-]?key|private[_-]?key|credentials|auth[_-]?header|bearer)$/i;
|
|
1099
|
+
|
|
1100
|
+
function hasProhibitedSecretData(obj) {
|
|
1101
|
+
if (!obj || typeof obj !== 'object') return false;
|
|
1102
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1103
|
+
if (SECRET_KEY_PATTERN.test(key) && value) return true;
|
|
1104
|
+
if (typeof value === 'object' && hasProhibitedSecretData(value)) return true;
|
|
1105
|
+
}
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* App-local capability installation record.
|
|
1111
|
+
* Lives in the application's vault (e.g. `system/capabilities/<id>.md`).
|
|
1112
|
+
* Tracks installed version, source hash, adapter, granted access, generated files, and state.
|
|
1113
|
+
* Strictly prohibits raw credentials and secret values.
|
|
1114
|
+
*/
|
|
1115
|
+
export const AppCapabilityInstallationSchema = z.object({
|
|
1116
|
+
type: z.literal('app_capability_installation'),
|
|
1117
|
+
title: z.string().min(1),
|
|
1118
|
+
description: z.string().min(1),
|
|
1119
|
+
timestamp: ssssDatetime(),
|
|
1120
|
+
capability_id: z.string().regex(/^[a-z][a-z0-9-]{1,63}$/),
|
|
1121
|
+
version: z.string().min(1),
|
|
1122
|
+
source_sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
1123
|
+
adapter: z.string().min(1),
|
|
1124
|
+
status: z.enum(['installing', 'installed', 'failed', 'upgrading', 'upgrade_failed', 'uninstalled', 'repair_needed']),
|
|
1125
|
+
access_grants: z.array(z.string()).default([]),
|
|
1126
|
+
resources: z.record(z.any()).optional().default({}),
|
|
1127
|
+
files: z.array(z.object({
|
|
1128
|
+
path: z.string(),
|
|
1129
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
1130
|
+
size: z.number().int().nonnegative().optional()
|
|
1131
|
+
})).default([]),
|
|
1132
|
+
plan_hash: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
1133
|
+
installed_at: ssssDatetime(),
|
|
1134
|
+
updated_at: ssssDatetime().optional(),
|
|
1135
|
+
error: z.string().nullable().optional()
|
|
1136
|
+
}).passthrough().superRefine((data, ctx) => {
|
|
1137
|
+
if (hasProhibitedSecretData(data.resources)) {
|
|
1138
|
+
ctx.addIssue({
|
|
1139
|
+
code: z.ZodIssueCode.custom,
|
|
1140
|
+
message: 'Security error: Prohibited credential or secret value detected in capability resources. Installation records must not store plaintext secrets.'
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* App-local access grant record.
|
|
1147
|
+
* Grants a capability or principal explicit access to specific scopes and resources.
|
|
1148
|
+
*/
|
|
1149
|
+
export const AccessGrantSchema = z.object({
|
|
1150
|
+
type: z.literal('access_grant'),
|
|
1151
|
+
title: z.string().min(1),
|
|
1152
|
+
description: z.string().min(1),
|
|
1153
|
+
timestamp: ssssDatetime(),
|
|
1154
|
+
grantee: z.string().min(1),
|
|
1155
|
+
grantee_type: z.enum(['capability', 'agent', 'service', 'user']).default('capability'),
|
|
1156
|
+
scopes: z.array(z.string()).min(1),
|
|
1157
|
+
resources: z.array(z.string()).optional().default([]),
|
|
1158
|
+
status: z.enum(['active', 'revoked', 'expired']).default('active'),
|
|
1159
|
+
granted_at: ssssDatetime(),
|
|
1160
|
+
granted_by: z.string().min(1),
|
|
1161
|
+
expires_at: ssssDatetimeNullable().optional()
|
|
1162
|
+
}).passthrough();
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Canonical repo-layer skill configuration record.
|
|
1166
|
+
* Persisted as an SSSS document in the project vault (e.g. `system/skills/<id>.md`).
|
|
1167
|
+
* Controls gate commands, tier rules, and repo customizations.
|
|
1168
|
+
* Projected into `config.json` for skill scripts to read.
|
|
1169
|
+
*/
|
|
1170
|
+
export const SkillConfigSchema = z.object({
|
|
1171
|
+
type: z.literal('skill_config'),
|
|
1172
|
+
title: z.string().min(1),
|
|
1173
|
+
description: z.string().min(1),
|
|
1174
|
+
timestamp: ssssDatetime(),
|
|
1175
|
+
skill_id: z.string().regex(/^[a-z][a-z0-9-]{1,63}$/),
|
|
1176
|
+
version: z.string().min(1),
|
|
1177
|
+
config: z.record(z.any()).default({}),
|
|
1178
|
+
tiers: z.record(z.any()).optional().default({}),
|
|
1179
|
+
gates: z.array(z.object({
|
|
1180
|
+
name: z.string(),
|
|
1181
|
+
command: z.string(),
|
|
1182
|
+
tier: z.string().optional()
|
|
1183
|
+
})).optional().default([]),
|
|
1184
|
+
created_at: ssssDatetime(),
|
|
1185
|
+
updated_at: ssssDatetime().optional()
|
|
1186
|
+
}).passthrough();
|
|
1187
|
+
|
|
1067
1188
|
// ─── Schema Registry (§5 of the SSSS spec) ─────────────────────────────────
|
|
1068
1189
|
|
|
1069
1190
|
/** Total Recall host-extension primitives; peer hosts are not required to register them. */
|
|
@@ -1073,6 +1194,10 @@ export const SSSS_HOST_EXTENSION_TYPES = [
|
|
|
1073
1194
|
'notification_rule',
|
|
1074
1195
|
'mesh_node',
|
|
1075
1196
|
'daemon_leader',
|
|
1197
|
+
'plugin_record',
|
|
1198
|
+
'app_capability_installation',
|
|
1199
|
+
'access_grant',
|
|
1200
|
+
'skill_config',
|
|
1076
1201
|
];
|
|
1077
1202
|
|
|
1078
1203
|
/** Map from SSSS `type` value to its Zod schema. Used by the operation validator. */
|
|
@@ -1082,6 +1207,10 @@ export const SSSS_SCHEMAS = {
|
|
|
1082
1207
|
notification_rule: NotificationRuleSchema,
|
|
1083
1208
|
mesh_node: MeshNodeSchema,
|
|
1084
1209
|
daemon_leader: DaemonLeaderSchema,
|
|
1210
|
+
plugin_record: PluginRecordSchema,
|
|
1211
|
+
app_capability_installation: AppCapabilityInstallationSchema,
|
|
1212
|
+
access_grant: AccessGrantSchema,
|
|
1213
|
+
skill_config: SkillConfigSchema,
|
|
1085
1214
|
memory: MemoryNodeSchema,
|
|
1086
1215
|
conflict: ConflictRecordSchema,
|
|
1087
1216
|
task: TaskSchema,
|
package/src/core/search.mjs
CHANGED
|
@@ -155,12 +155,17 @@ export async function semanticSearch(query, {
|
|
|
155
155
|
|
|
156
156
|
// 2. Perform Semantic Search Channel (if embedding & index available)
|
|
157
157
|
let semanticRanked = [];
|
|
158
|
+
// Raw cosine similarity per slug. RRF scores below are rank-based, so the top
|
|
159
|
+
// hit scores ~0.49 however unrelated it is; callers that must decide whether a
|
|
160
|
+
// hit is actually relevant (e.g. the browser extension's page recall) need this.
|
|
161
|
+
const similarityBySlug = new Map();
|
|
158
162
|
if (queryEmbedding && vaultEntries.length > 0) {
|
|
159
163
|
const store = new VectorStore();
|
|
160
164
|
store.load(vaultIndex);
|
|
161
165
|
const matches = store.search(queryEmbedding, filteredNodes.length, filteredSlugs, cosineSimilarity);
|
|
162
166
|
|
|
163
167
|
semanticRanked = matches.map(({ slug, similarity }) => {
|
|
168
|
+
similarityBySlug.set(slug, similarity);
|
|
164
169
|
const node = filteredNodes.find(n => n.slug === slug);
|
|
165
170
|
const temporal = computeTemporalBoost(node);
|
|
166
171
|
return { slug, score: similarity * temporal };
|
|
@@ -231,7 +236,13 @@ export async function semanticSearch(query, {
|
|
|
231
236
|
// Slice top results
|
|
232
237
|
for (const { node, score } of blendedRanked.slice(0, k)) {
|
|
233
238
|
if (node) {
|
|
234
|
-
|
|
239
|
+
const similarity = similarityBySlug.get(node.slug);
|
|
240
|
+
results.push({
|
|
241
|
+
...node,
|
|
242
|
+
type: 'vault',
|
|
243
|
+
score,
|
|
244
|
+
...(similarity !== undefined && { similarity: Math.round(similarity * 1000) / 1000 }),
|
|
245
|
+
});
|
|
235
246
|
}
|
|
236
247
|
}
|
|
237
248
|
|
|
@@ -289,14 +289,18 @@ export async function maybeEnqueueIntegrationResearch(brainDir, key, meta = {},
|
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
292
|
+
// Nobody asked for this, so it goes through the autonomous gate and counts
|
|
293
|
+
// against the same daily budget as project gap research.
|
|
294
|
+
const { proposeAutonomousResearch } = await import('./research-gate.mjs');
|
|
295
|
+
const notes = inference.notes || `AI-inferred product research for secret \`${key}\`.`;
|
|
296
|
+
const { queued, skipped } = await proposeAutonomousResearch(
|
|
297
|
+
[{ topic: inference.topic, rationale: notes, priority: inference.priority || 'low' }],
|
|
298
|
+
{ project: null, via: 'secret', brainDir, coverageCheck: opts.coverageCheck },
|
|
299
|
+
);
|
|
300
|
+
if (!queued.length) {
|
|
301
|
+
return { enqueued: false, skipped: skipped[0]?.reason || 'gate declined', inference };
|
|
302
|
+
}
|
|
303
|
+
return { enqueued: true, item: queued[0], inference };
|
|
300
304
|
}
|
|
301
305
|
|
|
302
306
|
/**
|
|
@@ -12,6 +12,7 @@ import crypto from 'crypto';
|
|
|
12
12
|
import os from 'os';
|
|
13
13
|
import YAML from 'yaml';
|
|
14
14
|
import { encryptSecrets, decryptSecrets, encryptSecretsSync, decryptSecretsSync } from './crypto.mjs';
|
|
15
|
+
import { DEFAULT_KEYCHAIN_SERVICE, readKeychainPassword } from './secrets-keychain.mjs';
|
|
15
16
|
|
|
16
17
|
const META_KEY = '__tr_secrets_meta';
|
|
17
18
|
|
|
@@ -39,8 +40,81 @@ export function resolveUsagePath(brainDir) {
|
|
|
39
40
|
return path.join(brainDir, 'logs', 'usage.jsonl');
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The master password: the environment first, then — on macOS — the Keychain
|
|
45
|
+
* entry `secret rekey` already maintains (service `total-recall-secrets`,
|
|
46
|
+
* overridable with TR_SECRETS_KEYCHAIN_SERVICE), then the host env file
|
|
47
|
+
* (see readTrEnvFilePassword).
|
|
48
|
+
*
|
|
49
|
+
* Only an interactive shell profile used to read the Keychain, so a
|
|
50
|
+
* non-interactive process on the same Mac — a mesh `exec`, a release script —
|
|
51
|
+
* could not open a store its own user owns. Resolved once per process; the
|
|
52
|
+
* value is returned to the caller and never logged. TR_SECRETS_NO_KEYCHAIN=1
|
|
53
|
+
* disables the fallback.
|
|
54
|
+
*/
|
|
55
|
+
let keychainPassword;
|
|
56
|
+
export function secretsPassword({
|
|
57
|
+
env = process.env,
|
|
58
|
+
readKeychain = readTrKeychainPassword,
|
|
59
|
+
readEnvFile = readTrEnvFilePassword,
|
|
60
|
+
} = {}) {
|
|
61
|
+
const fromEnv = env.TR_SECRETS_PASSWORD || env.TR_MASTER_PASSWORD;
|
|
62
|
+
if (fromEnv) return fromEnv;
|
|
63
|
+
if (env.TR_SECRETS_NO_KEYCHAIN !== '1') {
|
|
64
|
+
let fromKeychain;
|
|
65
|
+
if (readKeychain !== readTrKeychainPassword) fromKeychain = readKeychain(env) || null;
|
|
66
|
+
else {
|
|
67
|
+
if (keychainPassword === undefined) keychainPassword = readKeychain(env) || null;
|
|
68
|
+
fromKeychain = keychainPassword;
|
|
69
|
+
}
|
|
70
|
+
if (fromKeychain) return fromKeychain;
|
|
71
|
+
}
|
|
72
|
+
return readEnvFile(env) || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The host's env-file carrier: `TR_ENV_FILE`, else `~/.agent/tr.env` — the file
|
|
77
|
+
* `auto-pull.sh` sources and `secret rekey --env-file` rotates. Only a shell
|
|
78
|
+
* that sources it used to see the password, so a non-interactive process on a
|
|
79
|
+
* Linux host (an agent's tool shell, cron, a systemd unit) could not open the
|
|
80
|
+
* store its own user owns. The file is honoured only when it is a regular file
|
|
81
|
+
* (not a symlink) that this user owns and no one else can read or write, in a
|
|
82
|
+
* directory this user owns that no one else can write to.
|
|
83
|
+
*/
|
|
84
|
+
export function readTrEnvFilePassword(env = process.env) {
|
|
85
|
+
const file = env.TR_ENV_FILE || path.join(env.HOME || os.homedir(), '.agent', 'tr.env');
|
|
86
|
+
try {
|
|
87
|
+
// lstat: a symlink could point the carrier at a file someone else controls.
|
|
88
|
+
const stat = fs.lstatSync(file);
|
|
89
|
+
if (!stat.isFile() || (stat.mode & 0o077) !== 0) return null;
|
|
90
|
+
// A directory others can write to lets them replace the file.
|
|
91
|
+
const dir = fs.statSync(path.dirname(file));
|
|
92
|
+
if ((dir.mode & 0o022) !== 0) return null;
|
|
93
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
94
|
+
if (uid !== null && (stat.uid !== uid || dir.uid !== uid)) return null;
|
|
95
|
+
const line = fs
|
|
96
|
+
.readFileSync(file, 'utf8')
|
|
97
|
+
.split(/\r?\n/)
|
|
98
|
+
.find((entry) => /^\s*(?:export\s+)?TR_SECRETS_PASSWORD\s*=/.test(entry));
|
|
99
|
+
if (!line) return null;
|
|
100
|
+
let value = line.slice(line.indexOf('=') + 1).trim();
|
|
101
|
+
if (value.length >= 2 && /^(['"]).*\1$/.test(value)) value = value.slice(1, -1);
|
|
102
|
+
return value || null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readTrKeychainPassword(env) {
|
|
109
|
+
if (process.platform !== 'darwin') return null;
|
|
110
|
+
try {
|
|
111
|
+
return readKeychainPassword({
|
|
112
|
+
service: env.TR_SECRETS_KEYCHAIN_SERVICE || DEFAULT_KEYCHAIN_SERVICE,
|
|
113
|
+
account: env.USER || os.userInfo().username,
|
|
114
|
+
});
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
44
118
|
}
|
|
45
119
|
|
|
46
120
|
/**
|
|
@@ -102,6 +102,7 @@ export function createSessionEntry({
|
|
|
102
102
|
content,
|
|
103
103
|
role = 'assistant',
|
|
104
104
|
source = 'unknown',
|
|
105
|
+
cwd = null,
|
|
105
106
|
}) {
|
|
106
107
|
return {
|
|
107
108
|
id: id || crypto.randomBytes(4).toString('hex'),
|
|
@@ -111,6 +112,9 @@ export function createSessionEntry({
|
|
|
111
112
|
content: content || '',
|
|
112
113
|
role,
|
|
113
114
|
source,
|
|
115
|
+
// Working directory of the agent, when the tool records it. It is how a
|
|
116
|
+
// session is attributed to a project for project-scoped research.
|
|
117
|
+
...(cwd ? { cwd } : {}),
|
|
114
118
|
};
|
|
115
119
|
}
|
|
116
120
|
|
|
@@ -229,6 +233,7 @@ export function parseClaudeCode(filePath) {
|
|
|
229
233
|
content: content.slice(0, 5000), // cap individual entries
|
|
230
234
|
role,
|
|
231
235
|
source: 'claude-code',
|
|
236
|
+
cwd: typeof parsed.cwd === 'string' ? parsed.cwd : null,
|
|
232
237
|
}),
|
|
233
238
|
);
|
|
234
239
|
}
|
|
@@ -282,6 +287,7 @@ export function parseCodex(filePath) {
|
|
|
282
287
|
content: content.slice(0, 5000),
|
|
283
288
|
role,
|
|
284
289
|
source: 'codex',
|
|
290
|
+
cwd: typeof (parsed.cwd || parsed.payload?.cwd) === 'string' ? (parsed.cwd || parsed.payload.cwd) : null,
|
|
285
291
|
}),
|
|
286
292
|
);
|
|
287
293
|
prevId = id;
|