total-recall-brain 3.41.1 → 3.41.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/bin/total-recall.mjs +9 -3
  2. package/frontend/dist/assets/{index-BG9NkVUy.css → index-BIQmqDKP.css} +1 -1
  3. package/frontend/dist/assets/{index-BtuiZqwP.js → index-DFenQQEQ.js} +74 -79
  4. package/frontend/dist/index.html +2 -2
  5. package/frontend/dist/sw.js +8 -5
  6. package/frontend/src/api/auth.ts +0 -3
  7. package/frontend/src/components/plugins/PluginPanel.tsx +7 -3
  8. package/frontend/src/components/plugins/card/CardEngine.tsx +1 -1
  9. package/frontend/src/pages/LoginPage.tsx +1 -41
  10. package/frontend/src/pages/PluginsPage.tsx +14 -7
  11. package/package.json +2 -2
  12. package/scaffold/.agent/skills/plugin/scripts/record.mjs +6 -3
  13. package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +10 -0
  14. package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +197 -31
  15. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +1456 -12
  16. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +4 -4
  17. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +8 -1
  18. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +210 -27
  19. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +10 -1
  20. package/scaffold/.agent/skills/total-recall/plugins/auth/src/rotation-capability.mjs +15 -0
  21. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +78 -6
  22. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-merge.mjs +131 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +111 -2
  24. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +26 -5
  25. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +56 -8
  26. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +1068 -204
  27. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/panel.js +4 -2
  28. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +32 -0
  29. package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +436 -274
  30. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
  31. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
  32. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +16 -2
  33. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +461 -242
  34. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
  35. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  36. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -7
  37. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +17 -2
  38. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +426 -300
  39. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
  40. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +32 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +350 -283
  42. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
  43. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +31 -19
  44. package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/plugins/mesh/{src/core/headscale-server.mjs → capability/control-server.mjs} +5 -2
  46. package/scaffold/.agent/skills/total-recall/plugins/mesh/capability/headscale-api.mjs +192 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/mesh/capability/index.mjs +46 -0
  48. package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +9 -1
  49. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +23 -22
  50. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +6 -3
  51. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +242 -8
  52. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +14 -9
  53. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/server.mjs +1 -1
  54. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/credentials.mjs +4 -2
  55. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/headscale-client.mjs +34 -181
  56. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-auth.mjs +2 -2
  57. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/presence.mjs +5 -5
  58. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/screen-view.mjs +78 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/DESIGN.md +211 -12
  60. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +323 -388
  61. package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +12 -1
  62. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +7 -4
  63. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
  64. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -2
  65. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +34 -1
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/bootstrap.mjs +99 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/README.md +10 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/architecture.md +5 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/ide.md +10 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/memory.md +11 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/secrets.md +7 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/openwiki/templates/openwiki/skills.md +7 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +16 -2
  74. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +380 -298
  75. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -1
  76. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +6 -6
  77. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
  78. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
  79. package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +17 -3
  80. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +38 -15
  81. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +60 -34
  82. package/scaffold/.agent/skills/total-recall/references/auth.md +2 -0
  83. package/src/cli/command.mjs +23 -18
  84. package/src/cli/daemon.mjs +23 -1
  85. package/src/cli/plugin/copies.mjs +8 -5
  86. package/src/cli/plugin/index.mjs +25 -2
  87. package/src/cli/rebuild.mjs +22 -0
  88. package/src/cli/reset-password.mjs +3 -2
  89. package/src/cli/skill.mjs +8 -2
  90. package/src/cli/status.mjs +25 -7
  91. package/src/core/automatic-brain-updates.mjs +9 -3
  92. package/src/core/brain-updates.mjs +63 -5
  93. package/src/core/global-brain-updates.mjs +21 -1
  94. package/src/core/mesh-brain-updates.mjs +5 -2
  95. package/src/core/meta-harness.mjs +16 -41
  96. package/src/core/plugin-runner.mjs +6 -1
  97. package/src/core/plugin-store.mjs +67 -1
  98. package/src/core/plugin-updates.mjs +1 -1
  99. package/src/core/portable-skills-maintenance.mjs +19 -0
  100. package/src/core/required-plugins.mjs +7 -0
  101. package/src/core/skills-registry.mjs +33 -3
  102. package/src/server/auth.mjs +41 -1
  103. package/src/server/memory-app.mjs +2 -1
  104. package/src/server/routes/auth.mjs +25 -1
  105. package/src/server/routes/plugins.mjs +17 -4
package/src/cli/skill.mjs CHANGED
@@ -82,6 +82,8 @@ function printHelp() {
82
82
  deploy options:
83
83
  --repo <path> Target repository root (default: cwd)
84
84
  --adapt Rewrite description with openwiki + package.json stack signals
85
+ --route-core Link preserved wrapper to a layered portable core
86
+ --dry-run Report deployment actions without writes
85
87
  --force Replace existing destination
86
88
 
87
89
  discover / sync options (any user repos — no hardcoded paths):
@@ -527,7 +529,10 @@ export default async function skillCli(args) {
527
529
  agentSkillsDir: skillsDir,
528
530
  adapt,
529
531
  force,
532
+ routeCore: remainingArgs.includes('--route-core'),
533
+ dryRun: remainingArgs.includes('--dry-run'),
530
534
  });
535
+ if (result.dryRun) { console.log(JSON.stringify(result, null, 2)); return; }
531
536
  console.log(`\n ✅ Deployed: ${result.skillId}`);
532
537
  console.log(` → ${result.destDir}`);
533
538
  console.log(` hash=${result.install.content_hash} adapted=${result.adapt.adapted}`);
@@ -560,8 +565,9 @@ export default async function skillCli(args) {
560
565
  }
561
566
  try {
562
567
  const st = skillStatus(brainDir, skillId);
568
+ if (remainingArgs.includes('--json')) { console.log(JSON.stringify(st, null, 2)); if (!st.verified) process.exitCode = 1; return; }
563
569
  console.log(`\n Skill: ${skillId}`);
564
- console.log(` Registered: ${st.registered}`);
570
+ console.log(` Registered: ${st.registered} verified=${st.verified} missing=${st.missing_count} errors=${st.error_count}`);
565
571
  if (st.entry) {
566
572
  console.log(` Version: ${st.entry.version} hash=${st.entry.content_hash}`);
567
573
  console.log(` Source: ${st.entry.source_path || st.entry.source}`);
@@ -584,7 +590,7 @@ export default async function skillCli(args) {
584
590
  }
585
591
  }
586
592
  console.log('');
587
- if (st.any_contract_failure) process.exitCode = 1;
593
+ if (!st.verified || st.any_contract_failure) process.exitCode = 1;
588
594
  } catch (err) {
589
595
  console.error(`❌ ${err.message}`);
590
596
  process.exit(1);
@@ -21,7 +21,7 @@ import fs from 'node:fs';
21
21
  import crypto from 'node:crypto';
22
22
  import { loadQueue } from '../core/research-queue.mjs';
23
23
 
24
- import { totalRecallToken } from '../core/config.mjs';
24
+ import { totalRecallToken, globalBrainDir } from '../core/config.mjs';
25
25
  import { resolveBrainDir, resolveSurfaceDir } from './agent-dir.mjs';
26
26
 
27
27
  function parseArgs(args) {
@@ -82,6 +82,24 @@ function projectionsStatus(clients, instructionsMtime) {
82
82
  });
83
83
  }
84
84
 
85
+ /**
86
+ * The brain id the server knows this brain by. Project brain.json files carry
87
+ * no layer/name, so the id comes from the global project registry. Without it
88
+ * every status run inside a project compared the project's INSTRUCTIONS.md with
89
+ * the server's GLOBAL copy and reported "In sync: no" for two healthy brains.
90
+ */
91
+ export function serverBrainId(brainDir, brainConfig, registryPath = path.join(globalBrainDir, 'config', 'project-registry.json')) {
92
+ if (brainConfig?.layer === 'project' && brainConfig?.name) return `project:${brainConfig.name}`;
93
+ try {
94
+ const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
95
+ const own = path.resolve(brainDir);
96
+ const project = Array.isArray(registry) ? registry.find((p) => p?.brainDir && path.resolve(p.brainDir) === own) : null;
97
+ return project?.name ? `project:${project.name}` : null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
85
103
  function sha256(buf) {
86
104
  return crypto.createHash('sha256').update(buf).digest('hex');
87
105
  }
@@ -199,10 +217,7 @@ export default async function statusCmd(args) {
199
217
 
200
218
  let remote = null;
201
219
  if (brainUrl) {
202
- const brainId = brainConfig?.layer === 'project' && brainConfig?.name
203
- ? `project:${brainConfig.name}`
204
- : null;
205
- remote = await probeBrain(brainUrl, token, brainId);
220
+ remote = await probeBrain(brainUrl, token, serverBrainId(brainDir, brainConfig));
206
221
  }
207
222
 
208
223
  const inSync = !!(localInstructions && remote?.instructions_sha256 &&
@@ -250,7 +265,7 @@ export default async function statusCmd(args) {
250
265
  // Load ongoing research agenda
251
266
  let ongoingResearch = [];
252
267
  try {
253
- ongoingResearch = loadQueue().filter(item => item.status === 'in_progress' || item.status === 'pending');
268
+ ongoingResearch = loadQueue(brainDir).filter(item => item.status === 'in_progress' || item.status === 'pending');
254
269
  report.research = ongoingResearch.map(item => ({
255
270
  id: item.id,
256
271
  topic: item.topic,
@@ -284,7 +299,10 @@ export default async function statusCmd(args) {
284
299
  console.log(` Remote INSTR.: ${remote.instructions_bytes} bytes, sha=${remote.instructions_sha256.slice(0, 12)}…`);
285
300
  }
286
301
  console.log(` In sync: ${inSync ? 'yes' : 'no'}`);
287
- console.log(` Last sync: ${report.last_sync || 'never'}`);
302
+ // Push sync only exists for remote deploys; a local server serves this brain
303
+ // directly, so "never" there read as a fault that was not one.
304
+ const pushSync = deployMode !== 'local' || report.last_sync || report.targets.length;
305
+ console.log(` Last sync: ${report.last_sync || (pushSync ? 'never' : 'not used (local deploy serves this brain directly)')}`);
288
306
  console.log(` Sync targets: ${report.targets.length ? report.targets.join(', ') : '(none)'}`);
289
307
 
290
308
  if (clientProjections.length) {
@@ -1,3 +1,4 @@
1
+ import { maintainPortableSkills } from './portable-skills-maintenance.mjs';
1
2
  /** The existing TR daemon maintains both published brain packages, not only TR. */
2
3
  import fs from 'node:fs';
3
4
  import path from 'node:path';
@@ -35,7 +36,7 @@ export function startAutomaticBrainUpdates({brainDir,intervalMs=60*60*1000,run=r
35
36
  return {tick,stop(){stopped=true;clearInterval(timer);}};
36
37
  }
37
38
 
38
- export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false,skipThrottle=false,intervalMs=60*60*1000,update=updateAllBrains,compile=maintainCliSurfaces,globalUpdate=updateGlobalBrainPackages,meshUpdate=updateMeshBrainPackages,sourceUpdate=updateSourceBrainPackages,pluginUpdate=updateInstalledPlugins,now=Date.now}={}) {
39
+ export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false,skipThrottle=false,intervalMs=60*60*1000,update=updateAllBrains,compile=maintainCliSurfaces,globalUpdate=updateGlobalBrainPackages,meshUpdate=updateMeshBrainPackages,sourceUpdate=updateSourceBrainPackages,pluginUpdate=updateInstalledPlugins,skillMaintenance=maintainPortableSkills,now=Date.now}={}) {
39
40
  if(!force && !isPackageAutoUpdateEnabled())return {skipped:true,reason:'disabled',results:[]};
40
41
  const file=path.join(brainDir,'memory-derived/automatic-brain-updates.json');
41
42
  let saved;try{saved=JSON.parse(fs.readFileSync(file,'utf8'));}catch{}
@@ -61,8 +62,13 @@ export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false
61
62
  const plugins=await pluginUpdate({roots:[path.resolve(brainDir,'../../..')]});
62
63
  const mesh=fleet && summary.versions?await meshUpdate({versions:summary.versions,version:summary.versions['total-recall-brain']}):{failed:0,results:[],scope:'local-only'};
63
64
  const changed=summary.updated>0 || plugins.updated>0 || global.results.some(row=>row.status==='updated') || sources.results.some(row=>row.status==='updated') || JSON.stringify(saved?.versions)!==JSON.stringify(summary.versions);
64
- const surfaces=await compile({brainDir,force:changed});
65
- report={...summary,global,sources,plugins,mesh,surfaces,failed:(summary.failed || 0)+global.failed+sources.failed+plugins.failed+mesh.failed+(surfaces.ok?0:surfaces.results.filter(row=>row.status==='failed').length),checkedAt:now()};
65
+ const skills=await skillMaintenance({brainDir});
66
+ const surfaces=await compile({brainDir,force:changed || skills.updated > 0});
67
+ // local_failed is this node's own maintenance. Peers judge a node by it, not by
68
+ // `failed`, which also counts the node's view of its peers: judging by that made
69
+ // every fleet pass mark every peer failed forever, each because the others had.
70
+ const local_failed=(summary.failed || 0)+global.failed+sources.failed+plugins.failed+(skills.failed || 0)+(surfaces.ok?0:surfaces.results.filter(row=>row.status==='failed').length);
71
+ report={...summary,global,sources,plugins,mesh,skills,surfaces,local_failed,failed:local_failed+mesh.failed,checkedAt:now()};
66
72
  }catch(error){report={failed:1,error:error.message,checkedAt:now(),results:[]};}
67
73
  const temporary=`${file}.${process.pid}.tmp`;fs.writeFileSync(temporary,JSON.stringify(report,null,2));fs.renameSync(temporary,file);
68
74
  return report;
@@ -22,6 +22,47 @@ const isLink = file => { try { return fs.lstatSync(file).isSymbolicLink(); } cat
22
22
  export const brainUpdateLockPath = owner => path.join(os.tmpdir(),
23
23
  `total-recall-update-${createHash('sha256').update(owner).digest('hex')}.lock`);
24
24
 
25
+ const pause = ms => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
26
+ const alive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; } };
27
+
28
+ /**
29
+ * Take a per-owner update lock. The daemon's hourly `update --automatic` and a
30
+ * manual `update --all-brains` take the same locks, and a plain exclusive create
31
+ * failed the manual run outright with EEXIST. A lock whose process is gone is
32
+ * reclaimed; a live holder is waited for, up to `waitMs`.
33
+ */
34
+ export function acquireBrainUpdateLock(lock, { waitMs = 600000, pollMs = 1000, isAlive = alive, sleep = pause } = {}) {
35
+ const deadline = Date.now() + waitMs;
36
+ for (;;) {
37
+ try {
38
+ const fd = fs.openSync(lock, 'wx', 0o600);
39
+ fs.writeFileSync(fd, String(process.pid));
40
+ return fd;
41
+ } catch (error) {
42
+ if (error.code !== 'EEXIST') throw error;
43
+ }
44
+ let holder, age;
45
+ try {
46
+ holder = Number.parseInt(fs.readFileSync(lock, 'utf8').trim(), 10);
47
+ age = Date.now() - fs.statSync(lock).mtimeMs;
48
+ } catch (error) {
49
+ if (error.code === 'ENOENT') continue; // released between the create and the read
50
+ throw error;
51
+ }
52
+ // A creator writes its pid right after the exclusive create; an empty lock
53
+ // younger than a few seconds is still being taken, not abandoned. Content
54
+ // that is not a pid names an unknown owner and is respected.
55
+ const content = holder ? 'pid' : fs.statSync(lock).size ? 'unknown' : 'empty';
56
+ const stale = content === 'pid' ? !isAlive(holder) : content === 'empty' && age > 5000;
57
+ if (stale) {
58
+ try { fs.unlinkSync(lock); } catch (error) { if (error.code !== 'ENOENT') throw error; }
59
+ continue;
60
+ }
61
+ if (Date.now() >= deadline) throw new Error(`Another update (${holder ? `pid ${holder}` : 'unknown owner'}) still holds ${lock}`);
62
+ sleep(pollMs);
63
+ }
64
+ }
65
+
25
66
  export function installedBrainPackage(root, key) {
26
67
  try {
27
68
  const paths = createRequire(path.join(root, 'package.json')).resolve.paths(key) || [];
@@ -44,7 +85,14 @@ export function discoverBrainConsumers(roots, { onError } = {}) {
44
85
  let pkg;
45
86
  try { pkg = read(file); }
46
87
  catch (error) { if (onError) { onError(real, error); return; } throw error; }
47
- const deps = sections.flatMap(section => Object.entries(pkg[section] || {})
88
+ // A plugin copied into a shared plugin folder is installed without its dev
89
+ // dependencies, on purpose: full dev trees there filled a disk. Counting its
90
+ // devDependencies made every such plugin a permanently failing consumer, and
91
+ // "fixing" it would mean installing exactly what must never be installed.
92
+ const sharedPluginCopy = fs.existsSync(path.join(real, 'plugin.json'))
93
+ && real.split(path.sep).join('/').includes('/skills/total-recall/plugins/');
94
+ const deps = sections.filter(section => !(sharedPluginCopy && section === 'devDependencies'))
95
+ .flatMap(section => Object.entries(pkg[section] || {})
48
96
  .filter(([key]) => BRAIN_PACKAGES.includes(canonical(key)) && canonical(key) !== pkg.name)
49
97
  .map(([key, declared]) => ({ section, key, package: canonical(key), declared,
50
98
  ...installedBrainPackage(real, key) })));
@@ -168,8 +216,7 @@ export async function updateAllBrains(opts = {}) {
168
216
  isLink(path.join(consumer.root, 'node_modules')) || consumer.deps.some(dep =>
169
217
  dep.version !== versions[dep.package] || !dep.file?.startsWith(path.join(group.owner, 'node_modules') + path.sep)));
170
218
  try {
171
- lockFd = fs.openSync(lock, 'wx', 0o600);
172
- fs.writeFileSync(lockFd, String(process.pid));
219
+ lockFd = acquireBrainUpdateLock(lock, opts.lockWaitMs === undefined ? {} : { waitMs: opts.lockWaitMs });
173
220
  for (const name of ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml']) {
174
221
  const file = path.join(group.owner, name);
175
222
  saved.set(file, fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null);
@@ -198,11 +245,22 @@ export async function updateAllBrains(opts = {}) {
198
245
  if (install.status !== 0) throw new Error(install.error?.message || install.stderr?.trim().slice(-1500) || `Install exited ${install.status}`);
199
246
  for (const { consumer, row } of group.consumers) {
200
247
  row.packages = row.packages.map(dep => ({ ...dep, installed: installedBrainPackage(consumer.root, dep.key).version }));
248
+ // A consumer may legitimately resolve the package from its own node_modules — nested plugin
249
+ // directories do exactly that. Requiring the owner's hoisted copy reported a correct install
250
+ // as a failure and rolled the whole batch back, which is why 30 consumers never updated.
251
+ const allowedRoots = [
252
+ path.join(group.owner, 'node_modules') + path.sep,
253
+ path.join(consumer.root, 'node_modules') + path.sep,
254
+ ];
255
+ const placed = (file) => Boolean(file) && allowedRoots.some((root) => file.startsWith(root));
201
256
  const bad = consumer.deps.find(dep => {
202
257
  const actual = installedBrainPackage(consumer.root, dep.key);
203
- return actual.version !== versions[dep.package] || !actual.file?.startsWith(path.join(group.owner, 'node_modules') + path.sep);
258
+ return actual.version !== versions[dep.package] || !placed(actual.file);
204
259
  });
205
- if (bad || isLink(path.join(consumer.root, 'node_modules'))) throw new Error(`Installed release verification failed: ${consumer.root}${bad ? ` (${bad.key})` : ' (shared link)'}`);
260
+ // A link is only a fault when it escapes every legitimate root; a self-contained one is fine.
261
+ const linkPath = path.join(consumer.root, 'node_modules');
262
+ const linked = isLink(linkPath) && !placed(path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath)));
263
+ if (bad || linked) throw new Error(`Installed release verification failed: ${consumer.root}${bad ? ` (${bad.key})` : ' (shared link)'}`);
206
264
  row.status = 'updated';
207
265
  }
208
266
  } catch (error) {
@@ -6,6 +6,24 @@ import {BRAIN_PACKAGES} from './brain-updates.mjs';
6
6
  import {comparePackageVersions} from './package-version.mjs';
7
7
  import {updateSourceBrainPackage} from './source-brain-updates.mjs';
8
8
 
9
+ /**
10
+ * A global link that points at a source checkout of `name` under another
11
+ * directory name (the package was once published as `total-recall`). Missing it
12
+ * made the update `npm install -g` over the bin and replace the operator's
13
+ * source checkout with a registry copy.
14
+ */
15
+ export function findLinkedSourceCheckout(root,name){
16
+ let entries=[];try{entries=fs.readdirSync(root,{withFileTypes:true});}catch{return null;}
17
+ for(const entry of entries){
18
+ if(!entry.isSymbolicLink())continue;
19
+ try{
20
+ const dir=fs.realpathSync(path.join(root,entry.name));
21
+ if(JSON.parse(fs.readFileSync(path.join(dir,'package.json'),'utf8')).name===name&&fs.existsSync(path.join(dir,'.git')))return dir;
22
+ }catch{}
23
+ }
24
+ return null;
25
+ }
26
+
9
27
  export function updateGlobalBrainPackages({versions,exec=spawnSync,sourceUpdate=updateSourceBrainPackage}={}) {
10
28
  const prefix=exec('npm',['root','--global'],{encoding:'utf8',timeout:15000});
11
29
  if(prefix.status!==0)return {failed:1,results:[{status:'failed',error:'Cannot resolve global npm root'}]};
@@ -13,7 +31,9 @@ export function updateGlobalBrainPackages({versions,exec=spawnSync,sourceUpdate=
13
31
  const results=[];
14
32
  const install=[];
15
33
  for(const name of BRAIN_PACKAGES){
16
- const directory=path.join(root,name),file=path.join(directory,'package.json');
34
+ let directory=path.join(root,name);
35
+ if(!fs.existsSync(directory))directory=findLinkedSourceCheckout(root,name)||directory;
36
+ const file=path.join(directory,'package.json');
17
37
  let installed;try{installed=JSON.parse(fs.readFileSync(file,'utf8')).version;}catch{}
18
38
  const row={package:name,installed:installed || null,latest:versions[name],status:'up_to_date'};results.push(row);
19
39
  if(installed===versions[name])continue;
@@ -15,14 +15,17 @@ export async function updateMeshBrainPackages({version,versions,listNodes,exec,n
15
15
  // Read the durable report first, then launch only when stale or failed.
16
16
  const observed=await exec(node.ip,'cd "$HOME" && total-recall update --status --json',{timeoutMs:20000});
17
17
  let report;try{report=JSON.parse(observed.stdout).last;}catch{}
18
- const current=observed.success && report && !report.failed && now()>=report.checkedAt && now()-report.checkedAt<2*60*60*1000 && report.versions?.['total-recall-brain']===version && (!versions?.['@gregiteen/ssss-cli'] || report.versions?.['@gregiteen/ssss-cli']===versions['@gregiteen/ssss-cli']);
18
+ const localFailed=report?(report.local_failed ?? Math.max(0,(report.failed || 0)-(report.mesh?.failed || 0))):null;
19
+ const current=observed.success && report && localFailed===0 && now()>=report.checkedAt && now()-report.checkedAt<2*60*60*1000 && report.versions?.['total-recall-brain']===version && (!versions?.['@gregiteen/ssss-cli'] || report.versions?.['@gregiteen/ssss-cli']===versions['@gregiteen/ssss-cli']);
19
20
  if(current){row.status='verified';row.report=report;continue;}
20
21
  // npm exec pins the verified release so an old installed CLI can bootstrap
21
22
  // this capability. The remote command owns its lock, verification and report.
22
23
  const command=`cd "$HOME" && nohup npm exec --yes --package=total-recall-brain@${version} -- total-recall update --automatic --local-only --json </dev/null >/tmp/total-recall-automatic-update.log 2>&1 &`;
23
24
  const launched=await exec(node.ip,command,{timeoutMs:20000});
24
25
  row.status=launched.success?'dispatched':'failed';
25
- row.error=launched.success?'Awaiting independently verified remote report':launched.stderr || 'Mesh maintenance dispatch failed';
26
+ row.error=!launched.success?launched.stderr || 'Mesh maintenance dispatch failed'
27
+ :localFailed?`Peer reports ${localFailed} local maintenance failure(s); re-dispatched`
28
+ :'Awaiting independently verified remote report';
26
29
  }catch(error){row.status='failed';row.error=error.message;}
27
30
  }
28
31
  // A dispatched process is not a verified update. Keep it visible for retries.
@@ -593,9 +593,9 @@ export async function queryHarnessUsage() {
593
593
  status: 'not_installed',
594
594
  identity: '—',
595
595
  plan: '—',
596
- rolling5hRemainingPct: '0%',
597
- weeklyRemainingPct: '0%',
598
- quotaRemaining: 'Not available'
596
+ rolling5hRemainingPct: '—',
597
+ weeklyRemainingPct: '—',
598
+ quotaRemaining: 'not installed'
599
599
  });
600
600
  continue;
601
601
  }
@@ -606,9 +606,13 @@ export async function queryHarnessUsage() {
606
606
 
607
607
  let identity = 'Detected local install';
608
608
  let plan = 'Standard';
609
- let quotaRemaining = 'Active';
610
- let rolling5hRemainingPct = '100%';
611
- let weeklyRemainingPct = '100%';
609
+ // Never invent a quota. These fields held hardcoded literals ('100%', '98%', '85%') that read
610
+ // like measurements but came from no source at all — no provider usage endpoint was called, so
611
+ // the table reported fictional headroom for every harness. A figure appears here only when a
612
+ // real source produced it; otherwise the value is honestly unknown.
613
+ let quotaRemaining = 'unknown — no usage endpoint integrated';
614
+ let rolling5hRemainingPct = '—';
615
+ let weeklyRemainingPct = '—';
612
616
  let isAuthed = h.authed;
613
617
  let authRoute = h.authRoute;
614
618
 
@@ -620,9 +624,6 @@ export async function queryHarnessUsage() {
620
624
  if (acc.active) identity = acc.active;
621
625
  }
622
626
  plan = 'Google AI Ultra / Pro';
623
- rolling5hRemainingPct = '100%';
624
- weeklyRemainingPct = '98%';
625
- quotaRemaining = '5h window: 100% | Weekly compute: 98%';
626
627
  } catch {}
627
628
  } else if (h.id === 'claude') {
628
629
  try {
@@ -632,9 +633,6 @@ export async function queryHarnessUsage() {
632
633
  if (cData.userID) identity = `User: ${cData.userID.slice(0, 8)}...`;
633
634
  plan = cData.opusProMigrationComplete ? 'Claude Pro / Max (Opus enabled)' : 'Claude Pro';
634
635
  }
635
- rolling5hRemainingPct = '85%';
636
- weeklyRemainingPct = '92%';
637
- quotaRemaining = '5h window: 85% remaining | Weekly cap: 92% remaining';
638
636
  } catch {}
639
637
  } else if (h.id === 'codex') {
640
638
  try {
@@ -659,15 +657,7 @@ export async function queryHarnessUsage() {
659
657
  }
660
658
  plan = 'ChatGPT Subscription (GPT-4o/Codex)';
661
659
  }
662
- if (isAuthed) {
663
- rolling5hRemainingPct = '94%';
664
- weeklyRemainingPct = '96%';
665
- quotaRemaining = '5h reasoning: 94% remaining | Weekly quota: 96% remaining';
666
- } else {
667
- rolling5hRemainingPct = '0% (expired)';
668
- weeklyRemainingPct = '0% (expired)';
669
- quotaRemaining = 'Authentication expired';
670
- }
660
+ if (!isAuthed) quotaRemaining = 'unavailable — authentication expired';
671
661
  } catch {}
672
662
  } else if (h.id === 'grok') {
673
663
  try {
@@ -685,15 +675,7 @@ export async function queryHarnessUsage() {
685
675
  }
686
676
  plan = 'Grok Build Tier';
687
677
  }
688
- if (isAuthed) {
689
- rolling5hRemainingPct = '100%';
690
- weeklyRemainingPct = '100%';
691
- quotaRemaining = '5h rate window: 100% | Weekly allowance: 100%';
692
- } else {
693
- rolling5hRemainingPct = '0% (expired)';
694
- weeklyRemainingPct = '0% (expired)';
695
- quotaRemaining = 'Token expired (refresh needed)';
696
- }
678
+ if (!isAuthed) quotaRemaining = 'unavailable — token expired (refresh needed)';
697
679
  } catch {}
698
680
  } else if (h.id === 'gemini') {
699
681
  try {
@@ -703,9 +685,6 @@ export async function queryHarnessUsage() {
703
685
  if (acc.active) identity = acc.active;
704
686
  }
705
687
  plan = 'Google AI API / Subscription';
706
- rolling5hRemainingPct = '100%';
707
- weeklyRemainingPct = '100%';
708
- quotaRemaining = 'Standard rate limits active';
709
688
  } catch {}
710
689
  } else if (h.id === 'ollama') {
711
690
  identity = 'Local Machine';
@@ -716,9 +695,7 @@ export async function queryHarnessUsage() {
716
695
  } else if (h.id === 'hermes') {
717
696
  identity = 'Hermes Supervisor';
718
697
  plan = 'Autonomous Agent Runtime';
719
- rolling5hRemainingPct = '100%';
720
- weeklyRemainingPct = '100%';
721
- quotaRemaining = 'Active broker & gateway';
698
+ quotaRemaining = 'local runtime — no metered quota';
722
699
  } else if (h.id === 'dsh') {
723
700
  let model = 'deepseek/deepseek-v4.1-flash';
724
701
  try {
@@ -729,9 +706,9 @@ export async function queryHarnessUsage() {
729
706
  } catch {}
730
707
  identity = model;
731
708
  plan = 'DeepSeek Dedicated Harness';
732
- rolling5hRemainingPct = '95%';
733
- weeklyRemainingPct = '98%';
734
- quotaRemaining = 'DeepSeek reasoning pool active';
709
+ // dsh bills through OpenRouter metered credit and publishes no quota figures of its own, so
710
+ // none are shown. Point the reader at the one place the real number exists.
711
+ quotaRemaining = 'metered — see OpenRouter credits';
735
712
  } else if (h.id === 'openclaw') {
736
713
  let channelsStr = 'telegram, qqbot';
737
714
  try {
@@ -745,8 +722,6 @@ export async function queryHarnessUsage() {
745
722
  }
746
723
  } catch {}
747
724
  plan = 'Multi-Channel Gateway';
748
- rolling5hRemainingPct = '100%';
749
- weeklyRemainingPct = '100%';
750
725
  quotaRemaining = `Channels: ${channelsStr}`;
751
726
  }
752
727
 
@@ -56,7 +56,12 @@ export function runPluginCommand(plugin, options = {}) {
56
56
  if (!pluginExecutionEnabled(plugin, { projectRoot: cwd }) && !['status', 'health', 'jobs', '--help', '-h', 'help'].includes(operation)) {
57
57
  return Promise.reject(new Error(`Plugin ${plugin.id} is inactive; run total-recall plugin on ${plugin.id}`));
58
58
  }
59
- const declaredTimeout = plugin?.manifest?.cli?.subcommands?.find(item => item.name === operation)?.timeout_ms;
59
+ // Manifests declare this as `timeoutMs`; the runner only read `timeout_ms`, so every declared
60
+ // timeout was ignored and each command fell back to DEFAULT_TIMEOUT_MS (30s). That killed work
61
+ // that legitimately takes longer — `mesh nodes` and `tr-transcribe transcribe` (model load) both
62
+ // died at 30s while their manifests asked for far more. Accept both spellings.
63
+ const declaration = plugin?.manifest?.cli?.subcommands?.find(item => item.name === operation);
64
+ const declaredTimeout = declaration?.timeout_ms ?? declaration?.timeoutMs;
60
65
  const timeoutMs = requestedTimeoutMs ?? declaredTimeout ?? DEFAULT_TIMEOUT_MS;
61
66
  if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 600000) return Promise.reject(new Error('Plugin timeout must be 1..600000 ms'));
62
67
  const handlerRel = plugin?.manifest?.cli?.handler;
@@ -417,6 +417,67 @@ export async function installPluginDependencies(dest, id) {
417
417
  return { installed: true, packages };
418
418
  }
419
419
 
420
+ const REPLACEMENT_NAME = /^(.+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
421
+
422
+ /** Where `install --replace` keeps the copy it replaced, beside a plugins directory. */
423
+ export function replacementArchiveDir(pluginsDir) {
424
+ return path.join(path.dirname(pluginsDir), 'memory-derived', 'plugin-replacements');
425
+ }
426
+
427
+ function directoryBytes(dir) {
428
+ let total = 0;
429
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
430
+ const full = path.join(dir, entry.name);
431
+ if (entry.isDirectory()) total += directoryBytes(full);
432
+ else if (entry.isFile()) total += fs.statSync(full).size;
433
+ }
434
+ return total;
435
+ }
436
+
437
+ /**
438
+ * Bound the copies `install --replace` keeps. Every replacement archives the
439
+ * previous install, dependencies included, and nothing removed them, so the
440
+ * hourly automatic plugin update grew the archive without limit: 73 copies
441
+ * filled a laptop disk. Keep the newest `keep` copies of each plugin (or of
442
+ * `id` only), without node_modules, which a rollback reinstalls. A rename
443
+ * keeps a directory's old mtime, so the copy just archived is named in
444
+ * `prefer` and always counts as the newest.
445
+ *
446
+ * @param {string} archiveDir
447
+ * @param {{ id?: string, keep?: number, apply?: boolean, prefer?: string }} [opts]
448
+ */
449
+ export function pruneReplacementArchives(archiveDir, { id, keep = 1, apply = true, prefer } = {}) {
450
+ if (!fs.existsSync(archiveDir)) return { archiveDir, kept: [], removed: [], stripped: [], bytesFreed: 0 };
451
+ const byId = new Map();
452
+ for (const name of fs.readdirSync(archiveDir)) {
453
+ const m = REPLACEMENT_NAME.exec(name);
454
+ const full = path.join(archiveDir, name);
455
+ if (!m || (id && m[1] !== id) || !fs.statSync(full).isDirectory()) continue;
456
+ (byId.get(m[1]) || byId.set(m[1], []).get(m[1])).push({ name, full, mtime: fs.statSync(full).mtimeMs });
457
+ }
458
+ const kept = [], removed = [], stripped = [];
459
+ let bytesFreed = 0;
460
+ for (const copies of byId.values()) {
461
+ copies.sort((a, b) => (b.name === prefer) - (a.name === prefer) || b.mtime - a.mtime);
462
+ copies.forEach((copy, index) => {
463
+ if (index >= keep) {
464
+ bytesFreed += directoryBytes(copy.full);
465
+ if (apply) fs.rmSync(copy.full, { recursive: true, force: true });
466
+ removed.push(copy.name);
467
+ return;
468
+ }
469
+ kept.push(copy.name);
470
+ const modules = path.join(copy.full, 'node_modules');
471
+ if (fs.existsSync(modules)) {
472
+ bytesFreed += directoryBytes(modules);
473
+ if (apply) fs.rmSync(modules, { recursive: true, force: true });
474
+ stripped.push(copy.name);
475
+ }
476
+ });
477
+ }
478
+ return { archiveDir, kept, removed, stripped, bytesFreed };
479
+ }
480
+
420
481
  /**
421
482
  * Install a plugin.
422
483
  * @param {string} source bundled id | public share URL | `peer:<host>/<id>` | git URL | local path
@@ -439,7 +500,7 @@ export async function installPlugin(source, options = {}) {
439
500
  const prepareDestination=id=>{
440
501
  const target=path.join(pluginsDir,id);
441
502
  if(!options.replace || !fs.existsSync(target))return assertNotInstalled(id,pluginsDir);
442
- const archive=path.join(path.dirname(pluginsDir),'memory-derived/plugin-replacements',`${id}-${crypto.randomUUID()}`);
503
+ const archive=path.join(replacementArchiveDir(pluginsDir),`${id}-${crypto.randomUUID()}`);
443
504
  fs.mkdirSync(path.dirname(archive),{recursive:true});
444
505
  fs.renameSync(target,archive);previousDirectory={target,archive};
445
506
  };
@@ -524,6 +585,11 @@ export async function installPlugin(source, options = {}) {
524
585
  throw new Error(`Plugin files were written but its record could not be: ${err.message}`);
525
586
  }
526
587
 
588
+ // The install succeeded: keep only this replacement's copy of the plugin, without dependencies.
589
+ if (previousDirectory) {
590
+ pruneReplacementArchives(replacementArchiveDir(pluginsDir), { id: manifest.id, keep: 1, prefer: path.basename(previousDirectory.archive) });
591
+ }
592
+
527
593
  // A batch of installs (automatic updates) compiles once after the batch instead.
528
594
  const compilation = options.recompile === false ? { skipped: true } : await recompileAfterPluginMutation(projectRoot, isGlobal);
529
595
  return {
@@ -50,7 +50,7 @@ export async function checkPlugin(plugin, { gitHead = remoteGitHead, peerHash =
50
50
  return { status: 'pinned', ref: source.ref };
51
51
  case 'bundled': {
52
52
  const shipped = bundled().find(p => p.id === id);
53
- if (!shipped) return { status: 'failed', error: `Bundled plugin '${id}' is no longer shipped in this package` };
53
+ if (!shipped) return { status: 'failed', error: `Bundled plugin '${id}' is no longer shipped in this package; reinstall it from its git repository with: total-recall plugin install <git-url> --global --replace` };
54
54
  return hashPluginDir(shipped.dir).sha256 === plugin.installed_sha256 ? { status: 'current' } : { status: 'behind', install: id };
55
55
  }
56
56
  case 'git': {
@@ -0,0 +1,19 @@
1
+ /** Existing daemon maintenance verifies explicitly enrolled owner skill packages. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { loadRegistry } from './skills-registry.mjs';
5
+ import { runBounded } from './startup-health.mjs';
6
+
7
+ export async function maintainPortableSkills({ brainDir, apply = true, run = runBounded } = {}) {
8
+ const entry = loadRegistry(brainDir).skills.ssss;
9
+ if (!entry || !entry.layered || entry.source_type !== 'portable-owner-package') return { skipped: true, reason: 'no-enrolled-portable-source', failed: 0, results: [] };
10
+ const script = path.join(entry.source_path, 'core/scripts/maintain.mjs');
11
+ if (!fs.existsSync(script)) return { failed: 1, results: [], error: 'Registered portable source maintenance helper is missing' };
12
+ const result = await run(process.execPath, [script, '--source', entry.source_path, ...(apply ? ['--apply'] : [])], { timeoutMs: 5 * 60 * 1000, maxBytes: 8 * 1024 * 1024 });
13
+ try {
14
+ const report = JSON.parse(result.output);
15
+ return { ...report, failed: Math.max(report.failed || 0, result.code === 0 ? 0 : 1) };
16
+ } catch {
17
+ return { failed: 1, results: [], error: result.reason || result.output || 'Portable skill verification returned no report' };
18
+ }
19
+ }
@@ -152,6 +152,13 @@ export function auditProjectPluginCopies(projectRoot, { sharedDir = globalPlugin
152
152
  row.sourceVersion = row.sourceDir ? version(row.sourceDir) : null;
153
153
  row.sharedVersion = row.sharedDir ? version(row.sharedDir) : null;
154
154
  row.edited = row.sourceDir ? !sameTree(copyDir, row.sourceDir) : null;
155
+ // A copy declaring an older release than the shared install cannot be a deliberate local edit —
156
+ // it predates the shared plugin. It is a leftover from the release that vendored the defaults into
157
+ // every brain, and because discovery prefers a brain's own copy it SHADOWS the shared install and
158
+ // breaks the repository: a stale auth manifest fails validation, so every total-recall command in
159
+ // that repo dies with "Required plugin auth is missing or invalid". Same staleness rule the updater
160
+ // already applies when it refreshes a vendored copy.
161
+ row.stale = Boolean(row.version && row.sharedVersion && comparePackageVersions(row.version, row.sharedVersion) < 0);
155
162
  copies.push(row);
156
163
  }
157
164
  return { copies, count: copies.length, sourceDir };