total-recall-brain 3.47.1 → 3.47.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.47.1",
3
+ "version": "3.47.3",
4
4
  "description": "The Vibe Coding Brain & Autonomous Software Factory: memory, rules, secrets, and 33 visual plugins automatically managed across all your devices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "tr-plugin-meta-harness",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-meta-harness",
9
- "version": "0.4.0",
9
+ "version": "0.4.3",
10
10
  "dependencies": {
11
11
  "@gregiteen/ssss-cli": "0.14.5"
12
12
  },
13
13
  "devDependencies": {
14
- "total-recall-brain": "3.45.0"
14
+ "total-recall-brain": "3.47.1"
15
15
  }
16
16
  },
17
17
  "node_modules/@alcalzone/ansi-tokenize": {
@@ -5546,9 +5546,9 @@
5546
5546
  }
5547
5547
  },
5548
5548
  "node_modules/total-recall-brain": {
5549
- "version": "3.45.0",
5550
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.45.0.tgz",
5551
- "integrity": "sha512-HDYfROazZbsLHQsFPnlBcSpe1HL1QEi7JG8x/eNRUI06RxVQsZWl8LwBo2lTWjWRDrKmCy7BUmSXQDuWF3lrEw==",
5549
+ "version": "3.47.1",
5550
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.47.1.tgz",
5551
+ "integrity": "sha512-9yDByVZMNxNE10hjPf0vEmvBLGGUpD8sqAN7svuZpJEb2TSTPA1eSSwaZ5H4QScJQTT3DcV8MeQjuyLy8urnEw==",
5552
5552
  "dev": true,
5553
5553
  "license": "MIT",
5554
5554
  "workspaces": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-meta-harness",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Total Recall Metaharness plugin",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "meta-harness",
3
3
  "name": "Metaharness",
4
- "version": "0.4.1",
4
+ "version": "0.4.3",
5
5
  "description": "Discover, configure, document, track, and run coding agent harnesses through Total Recall across mesh nodes.",
6
6
  "gate": {
7
7
  "tasks": ["Run work in another agent harness", "Choose which harness takes a task", "Check harness versions, sign-in and quota", "Read a harness's official documentation"],
@@ -79,7 +79,7 @@ export async function dispatchWithReceipt(request, { dispatchFn, node = request.
79
79
  // exhausted until its reset and picks another included harness instead of choosing it again.
80
80
  if (result.failureKind === 'quota') {
81
81
  try { await record({ harness: request.agent, role, task: String(request.contextTask || request.prompt || '').slice(0, 200), success: false,
82
- failureKind: 'quota', error: result.error, resetsAt: result.resetsAt || null }); } catch { /* bookkeeping must not hide the refusal */ }
82
+ failureKind: 'quota', billingMode, error: result.error, resetsAt: result.resetsAt || null }); } catch { /* bookkeeping must not hide the refusal */ }
83
83
  }
84
84
  return { ...result, launchPrevented, durationMs };
85
85
  }
@@ -7,7 +7,7 @@ import { createInterface } from 'node:readline';
7
7
  import { pathToFileURL } from 'node:url';
8
8
  import { configDir } from './paths.mjs';
9
9
  import { agentEnvironment, CODEX_PLAN_ARGS, findBinary, OPENROUTER_AGENTS, readClaudeMaxOrganization } from './worker.mjs';
10
- import { activeQuotaWalls, readReceipts } from './learner.mjs';
10
+ import { activeQuotaWalls, includedBillingReceipts, readReceipts } from './learner.mjs';
11
11
 
12
12
  export const INCLUDED_AGENTS = ['claude', 'codex'];
13
13
  const VERSION = 1;
@@ -235,7 +235,7 @@ export async function includedAdmission(agent, cwd, policy, { readKey = authKey,
235
235
  if (agent === 'claude' && !policy.claudePlan && !claudeProofCurrent(policy)) {
236
236
  return { ok: false, failureKind: 'billing-policy', error: 'Verify the native Claude Max login with Usage credits off, or bind a verified included grant.' };
237
237
  }
238
- const wall = activeQuotaWalls((await receipts()).filter(run => run.billingMode === 'included'))[agent];
238
+ const wall = activeQuotaWalls(includedBillingReceipts(await receipts()))[agent];
239
239
  if (wall) return { ok: false, failureKind: 'quota', error: 'This harness is waiting for included usage.', resetsAt: wall.resetReported ? wall.until : null };
240
240
  if (agent === 'codex') return codex(cwd);
241
241
  if (policy.claudePlan) return { ok: true, claudePlan: policy.claudePlan.organization };
@@ -60,6 +60,13 @@ export async function recordRunReceipt(receipt = {}) {
60
60
  return entry;
61
61
  }
62
62
 
63
+ /**
64
+ * Receipts that count under the included usage policy: included runs, plus quota refusals recorded before
65
+ * refusals carried a billingMode. Those legacy walls expire at their reset, so nothing stale lingers.
66
+ */
67
+ export const includedBillingReceipts = (entries = []) =>
68
+ entries.filter(run => run.billingMode === 'included' || (!run.billingMode && run.failureKind === 'quota'));
69
+
63
70
  /** A harness is walled from its last quota failure until the reported reset, or until a later success proves it clear. */
64
71
  export function activeQuotaWalls(entries = [], now = Date.now(), unknownResetMs = 60 * 60 * 1000) {
65
72
  const latest = new Map();
@@ -20,7 +20,11 @@ export function queryAiModels(harness, model, { bin = process.env.TOTAL_RECALL_B
20
20
  const line = output.trim().split('\n').filter(Boolean).at(-1);
21
21
  try {
22
22
  const answer = JSON.parse(line);
23
- return answer && typeof answer.ok === 'boolean' ? answer : null;
23
+ if (!answer || typeof answer.ok !== 'boolean') return null;
24
+ // An error reply (an older ai-models without `validate` answers invalid_arguments) is the command
25
+ // failing, not a verdict on the model; reading it as one would refuse every model.
26
+ if (answer.error != null || answer.code != null) return null;
27
+ return answer;
24
28
  } catch { return null; }
25
29
  }
26
30
 
@@ -32,7 +36,7 @@ export function createModelValidator({ aiModels = queryAiModels, catalog = getMo
32
36
  if (refusal) return { ok: false, harness, model, source: 'policy', reason: refusal };
33
37
  if (ownerAnswers) {
34
38
  const answer = aiModels(harness, model);
35
- if (answer) return { ok: answer.ok, harness, model, source: 'ai-models', ...(answer.ok ? {} : { reason: answer.reason || answer.error || `AI Models does not list ${model} for ${harness}` }) };
39
+ if (answer) return { ok: answer.ok, harness, model, source: 'ai-models', ...(answer.ok ? {} : { reason: answer.reason || `AI Models does not list ${model} for ${harness}` }) };
36
40
  ownerAnswers = false;
37
41
  }
38
42
  // Another node's catalog is not this one's: its destination validates against its own.
@@ -1,7 +1,7 @@
1
1
  /** The harnesses that can actually take work right now, and why the others cannot. */
2
2
  import { ADAPTER_CATALOG } from '../capability/index.mjs';
3
3
  import { inspectAgents, getActiveAdapters } from './worker.mjs';
4
- import { readReceipts, activeQuotaWalls, activeCredentialBlocks } from './learner.mjs';
4
+ import { readReceipts, activeQuotaWalls, activeCredentialBlocks, includedBillingReceipts } from './learner.mjs';
5
5
  import { measuredWalls } from './telemetry.mjs';
6
6
  import { claudeProofCurrent, INCLUDED_AGENTS, readIncludedPolicy } from './included-usage.mjs';
7
7
  import os from 'node:os';
@@ -29,7 +29,7 @@ export async function resolvePool({ availableHarnesses, inspect = inspectAgents,
29
29
  if (!policy.enabled && Array.isArray(availableHarnesses) && availableHarnesses.length) return { ready: availableHarnesses.map(id => describe(id)), excluded: [], walls: {} };
30
30
  const claudePlan = policy.enabled ? policy.claudePlan?.organization : undefined;
31
31
  const [{ agents = [] }, entries] = await Promise.all([inspect({ env: process.env, claudePlan }), receipts()]);
32
- const billingEntries = policy.enabled ? entries.filter(run => run.billingMode === 'included') : entries;
32
+ const billingEntries = policy.enabled ? includedBillingReceipts(entries) : entries;
33
33
  const walls = { ...activeQuotaWalls(billingEntries), ...(!policy.enabled ? measuredWalls() : {}) };
34
34
  const blocks = activeCredentialBlocks(billingEntries);
35
35
  const ready = [], excluded = [];
@@ -18,10 +18,10 @@ import { classifyTaskComplexity, selectOptimalHarness, computeEmpiricalScorecard
18
18
  import { inspectAgents } from './worker.mjs';
19
19
  import { inspectNode } from './executor.mjs';
20
20
  import { getModelCatalog, joinHarnessModels } from './model-catalog.mjs';
21
- import { readReceipts, activeQuotaWalls, activeCredentialBlocks } from './learner.mjs';
21
+ import { readReceipts, activeQuotaWalls, activeCredentialBlocks, includedBillingReceipts, recordRunReceipt } from './learner.mjs';
22
22
  import { resolvePool, exclusionReason, isLocalExecutionNode } from './pool.mjs';
23
23
  import { readCodexRateLimits, measuredWalls } from './telemetry.mjs';
24
- import { claudeProofCurrent, INCLUDED_AGENTS, readIncludedPolicy } from './included-usage.mjs';
24
+ import { claudeProofCurrent, INCLUDED_AGENTS, readIncludedPolicy, readCodexAllowance } from './included-usage.mjs';
25
25
  import { derivedDir } from './paths.mjs';
26
26
  import { createModelValidator, filterPoolByModel } from './model-selection.mjs';
27
27
 
@@ -169,7 +169,7 @@ export async function inspectQuotas(options = {}) {
169
169
  const inspected = remote ? await (options.inspectNodeFn || inspectNode)(options.node) :
170
170
  await (options.inspect || inspectAgents)({ env: process.env, cwd: options.cwd, claudePlan });
171
171
  const codexLimits = remote ? null : readCodexRateLimits();
172
- const receipts = remote ? [] : (await readReceipts()).filter(run => !policy.enabled || run.billingMode === 'included');
172
+ const receipts = remote ? [] : (policy.enabled ? includedBillingReceipts : runs => runs)(await (options.receipts || readReceipts)());
173
173
  const walls = remote ? {} : { ...activeQuotaWalls(receipts), ...(!policy.enabled ? measuredWalls(codexLimits) : {}) };
174
174
  const metered = METERED_HARNESSES;
175
175
 
@@ -286,7 +286,26 @@ export async function routeTask(taskDescription, options = {}) {
286
286
  ...(remote || lean.fleet ? {} : { fleet: complexity.tier === 'local' ? cachedFleet : () => ({ ok: false, error: 'Not read: this route places no local work', nodes: [] }) }),
287
287
  });
288
288
  const quotas = quotaReport.quotas;
289
- const routing = selectOptimalHarness(taskDescription, pool.ready.map(h => h.id), quotas, { ...options, tierOverride: complexity.tier, empiricalScorecard: computeEmpiricalScorecard(receipts) });
289
+ const rank = ready => selectOptimalHarness(taskDescription, ready.map(h => h.id), quotas, { ...options, tierOverride: complexity.tier, empiricalScorecard: computeEmpiricalScorecard(receipts) });
290
+ let excluded = pool.excluded;
291
+ let routing = rank(pool.ready);
292
+ // Quota walls come from launches this machine saw refused. With none recorded here, a route kept
293
+ // choosing a Codex that was out of quota, and the selected-harness write gate then blocked every
294
+ // other harness. Before choosing Codex, ask it for its allowance (no turn), and record a quota
295
+ // answer as the wall a refused launch records (A-124), so later routes skip it without asking.
296
+ if (policy.enabled && !remote && routing.selected === 'codex') {
297
+ const allowance = await (options.codexAllowance || readCodexAllowance)(options.cwd || process.cwd());
298
+ if (allowance?.ok === false && allowance.failureKind === 'quota') {
299
+ const resetsAt = allowance.resetsAt || null;
300
+ try {
301
+ await (options.recordReceipt || recordRunReceipt)({ harness: 'codex', role: 'Route', task: String(taskDescription).slice(0, 200),
302
+ success: false, failureKind: 'quota', billingMode: 'included', error: allowance.error, resetsAt });
303
+ } catch { /* the route still skips Codex; only the next route asks again */ }
304
+ excluded = [...excluded, { harness: 'codex', reason: resetsAt ? `out of quota until ${resetsAt}` : 'out of quota', failureKind: 'quota', resetsAt }];
305
+ quotas.codex = { ...quotas.codex, status: 'exhausted', wallMessage: allowance.error, ...(resetsAt ? { resetsAt } : {}) };
306
+ routing = rank(pool.ready.filter(h => h.id !== 'codex'));
307
+ }
308
+ }
290
309
  const selectedQuota = quotas[routing.selected] || {};
291
310
  const warnings = [];
292
311
  if (policy.enabled && options.jev) warnings.push('Included usage policy uses the local task classifier.');
@@ -298,8 +317,8 @@ export async function routeTask(taskDescription, options = {}) {
298
317
  if (selectedModel?.source === 'unverified') warnings.push(selectedModel.note);
299
318
  return {
300
319
  ok: Boolean(routing.selected), task: taskDescription, classifier, complexity,
301
- selected: routing.selected, tier: complexity.tier, model: options.model ?? null, ...(modelSelection ? { modelSelection } : {}), candidates: routing.candidates, excluded: pool.excluded,
302
- ...(policy.enabled && !routing.selected ? { deferred: pool.excluded.find(row => row.failureKind === 'quota') || null } : {}),
320
+ selected: routing.selected, tier: complexity.tier, model: options.model ?? null, ...(modelSelection ? { modelSelection } : {}), candidates: routing.candidates, excluded,
321
+ ...(policy.enabled && !routing.selected ? { deferred: excluded.find(row => row.failureKind === 'quota') || null } : {}),
303
322
  placement: complexity.tier === 'local' ? placeLocalWork(quotaReport.fleet) : null,
304
323
  scoring: 'Adapter quality and cost scores are configured priors, adjusted by pass rates measured on this machine.',
305
324
  quotaSnapshot: { selectedHarnessStatus: selectedQuota.status || null, selectedHarnessUsage: selectedQuota.usagePercent ?? null, freeMemGB: quotas.system.freeMemGB ?? null },
@@ -17,7 +17,7 @@ import {
17
17
  } from '../core/package-auto-update.mjs';
18
18
  import { brainDir } from '../core/config.mjs';
19
19
  import { installAutoPull } from '../core/autopull-install.mjs';
20
- import { updateAllBrains } from '../core/brain-updates.mjs';
20
+ import { updateAllBrains, ensureBrainDefaults } from '../core/brain-updates.mjs';
21
21
  import { installHarnessHooks } from '../core/harness-hooks.mjs';
22
22
  import { automaticUpdateStatus, runAutomaticBrainUpdates } from '../core/automatic-brain-updates.mjs';
23
23
  import { runningServerStatus } from '../core/running-server.mjs';
@@ -38,6 +38,7 @@ export function parseArgs(args) {
38
38
  automatic: false,
39
39
  status: false,
40
40
  localOnly: false,
41
+ defaults: false,
41
42
  };
42
43
  for (let i = 0; i < args.length; i++) {
43
44
  const a = args[i];
@@ -55,6 +56,7 @@ export function parseArgs(args) {
55
56
  else if (a === '--automatic') opts.automatic = true;
56
57
  else if (a === '--status') opts.status = true;
57
58
  else if (a === '--local-only') opts.localOnly = true;
59
+ else if (a === '--defaults') opts.defaults = true;
58
60
  }
59
61
  return opts;
60
62
  }
@@ -85,6 +87,12 @@ function printHelp() {
85
87
  installed plugin from its recorded source and refresh surfaces. Where a
86
88
  per-user service (LaunchAgent, systemd user unit) runs the server from
87
89
  the global install, also restart a stale server and verify /health.
90
+ When this run installed a release newer than the running code, that release
91
+ refreshes the defaults with its own code (update --defaults) in the same pass.
92
+ total-recall update --defaults [--repo <path>] [--json]
93
+ Refresh the shared default plugins from this release's bundled copies (a
94
+ linked owner checkout is left alone) and provision every registered brain.
95
+ Exit 1 when any brain fails.
88
96
  total-recall update --status [--json]
89
97
  Read the actual last automatic maintenance report without starting work.
90
98
  Also reports what the server on this host runs against the release.
@@ -121,6 +129,17 @@ export default async function update(args = []) {
121
129
  console.log(JSON.stringify(status,null,2));
122
130
  return status;
123
131
  }
132
+ if (opts.defaults) {
133
+ const report = ensureBrainDefaults({ brainDir, roots: opts.repos });
134
+ if (opts.json) console.log(JSON.stringify(report, null, 2));
135
+ else {
136
+ console.log(`\n Default plugins: ${report.results.length - report.failed} brain(s) ready, ${report.failed} failed`);
137
+ for (const row of report.results) console.log(` ${row.status === 'ready' ? '✓' : '✗'} ${row.root}${row.error ? `: ${row.error}` : ''}`);
138
+ console.log('');
139
+ }
140
+ if (report.failed) process.exitCode = 1;
141
+ return report;
142
+ }
124
143
  if(opts.automatic){
125
144
  const report=await runAutomaticBrainUpdates({brainDir,force:opts.force,fleet:!opts.localOnly});
126
145
  console.log(JSON.stringify(report,null,2));
@@ -14,6 +14,7 @@ import { updateSourceBrainPackages } from './source-brain-updates.mjs';
14
14
  import { updateInstalledPlugins } from './plugin-updates.mjs';
15
15
  import { runningServerStatus } from './running-server.mjs';
16
16
  import { replaceStaleServer } from './server-restart.mjs';
17
+ import { handOffDefaultsToRelease } from './release-defaults-handoff.mjs';
17
18
 
18
19
  export function automaticUpdateStatus(brainDir) {
19
20
  let last;
@@ -38,7 +39,7 @@ export function startAutomaticBrainUpdates({brainDir,intervalMs=60*60*1000,run=r
38
39
  return {tick,stop(){stopped=true;clearInterval(timer);}};
39
40
  }
40
41
 
41
- 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,serverCheck=runningServerStatus,replaceServer=replaceStaleServer,now=Date.now}={}) {
42
+ 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,defaultsHandoff=handOffDefaultsToRelease,skillMaintenance=maintainPortableSkills,serverCheck=runningServerStatus,replaceServer=replaceStaleServer,now=Date.now}={}) {
42
43
  if(!force && !isPackageAutoUpdateEnabled())return {skipped:true,reason:'disabled',results:[]};
43
44
  const file=path.join(brainDir,'memory-derived/automatic-brain-updates.json');
44
45
  let saved;try{saved=JSON.parse(fs.readFileSync(file,'utf8'));}catch{}
@@ -62,10 +63,13 @@ export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false
62
63
  const summary=await update({brainDir,dryRun:false,ensureDefaults:true});
63
64
  const global=summary.versions?await globalUpdate({versions:summary.versions}):{failed:1,results:[],error:'Cannot maintain global packages without verified published versions'};
64
65
  const sources=summary.versions?await sourceUpdate({brainDir,versions:summary.versions}):{failed:1,results:[]};
66
+ // The defaults above came from the running code, which is the release being replaced. A newer
67
+ // release installed just now refreshes them with its own code and bundled copies, this pass.
68
+ const defaults=await defaultsHandoff({global,sources});
65
69
  // The shared plugin folder follows the source each plugin was installed from (git, mesh peer, directory).
66
70
  const plugins=await pluginUpdate({roots:[path.resolve(brainDir,'../../..')]});
67
71
  const mesh=fleet && summary.versions?await meshUpdate({versions:summary.versions,version:summary.versions['total-recall-brain']}):{failed:0,results:[],scope:'local-only'};
68
- 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);
72
+ const changed=summary.updated>0 || plugins.updated>0 || defaults.status==='refreshed' || global.results.some(row=>row.status==='updated') || sources.results.some(row=>row.status==='updated') || JSON.stringify(saved?.versions)!==JSON.stringify(summary.versions);
69
73
  const skills=await skillMaintenance({brainDir});
70
74
  const surfaces=await compile({brainDir,force:changed || skills.updated > 0});
71
75
  // Last, so a replaced server starts on everything this run moved. Where maintenance cannot
@@ -79,8 +83,8 @@ export async function runAutomaticBrainUpdates({brainDir,fleet=false,force=false
79
83
  // local_failed is this node's own maintenance. Peers judge a node by it, not by
80
84
  // `failed`, which also counts the node's view of its peers: judging by that made
81
85
  // every fleet pass mark every peer failed forever, each because the others had.
82
- 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)+serverFailed;
83
- report={...summary,global,sources,plugins,mesh,server,skills,surfaces,local_failed,failed:local_failed+mesh.failed,checkedAt:now()};
86
+ const local_failed=(summary.failed || 0)+global.failed+sources.failed+(defaults.status==='failed'?1:0)+plugins.failed+(skills.failed || 0)+(surfaces.ok?0:surfaces.results.filter(row=>row.status==='failed').length)+serverFailed;
87
+ report={...summary,global,sources,defaults,plugins,mesh,server,skills,surfaces,local_failed,failed:local_failed+mesh.failed,checkedAt:now()};
84
88
  }catch(error){report={failed:1,error:error.message,checkedAt:now(),results:[]};}
85
89
  const temporary=`${file}.${process.pid}.tmp`;fs.writeFileSync(temporary,JSON.stringify(report,null,2));fs.renameSync(temporary,file);
86
90
  return report;
@@ -217,18 +217,37 @@ export function installEnv(env = process.env) {
217
217
  return kept;
218
218
  }
219
219
 
220
+ /** The brains maintenance provisions: each update root that has a brain, plus the selected brain. */
221
+ function maintainedBrains(roots, brainDir) {
222
+ const brains = roots.map(root => ({ root, brain: path.join(root, '.agent', 'skills', 'total-recall') }))
223
+ .filter(({ brain }) => fs.existsSync(brain));
224
+ if (brainDir) brains.push({ root: brainDir, brain: brainDir });
225
+ return brains;
226
+ }
227
+
228
+ /**
229
+ * Refresh the shared default plugins from this package's bundled copies and provision every
230
+ * registered brain, reporting each brain's outcome instead of throwing. `update --defaults` runs
231
+ * this, so a release maintenance has just installed can do it with its own code and its own
232
+ * bundled copies (release-defaults-handoff.mjs).
233
+ */
234
+ export function ensureBrainDefaults(opts = {}, { ensure = ensureRequiredPlugins } = {}) {
235
+ const roots = listUpdateRoots(opts).map(row => row.root);
236
+ const results = maintainedBrains(roots, opts.brainDir).map(({ root, brain }) => {
237
+ try { ensure(brain); return { root, status: 'ready' }; }
238
+ catch (error) { return { root, status: 'failed', error: error.message }; }
239
+ });
240
+ return { failed: results.filter(row => row.status === 'failed').length, results };
241
+ }
242
+
220
243
  export async function updateAllBrains(opts = {}) {
221
244
  const dryRun = opts.dryRun !== false;
222
245
  const installMissing = opts.installMissing === true;
223
246
  const exec = opts.exec || spawnSync;
224
247
  const roots = listUpdateRoots(opts).map(row => row.root);
225
248
  const errors = [];
226
- const brains = roots.map(root => ({ root, brain: path.join(root, '.agent', 'skills', 'total-recall') }))
227
- .filter(({ brain }) => fs.existsSync(brain));
228
- if (opts.brainDir) {
229
- brains.push({ root: opts.brainDir, brain: opts.brainDir });
230
- roots.push(opts.brainDir);
231
- }
249
+ const brains = maintainedBrains(roots, opts.brainDir);
250
+ if (opts.brainDir) roots.push(opts.brainDir);
232
251
  for (const { root, brain } of brains) {
233
252
  if (!dryRun && opts.ensureDefaults === true) {
234
253
  try { ensureRequiredPlugins(brain); }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Automatic maintenance refreshes the default plugins before it installs a new release: the code
3
+ * that runs it is the old release, and so are its bundled copies. A default is replaced only by a
4
+ * higher bundled version, so that pass changed nothing and a release's defaults arrived one pass
5
+ * (about an hour) later. After the global and source updates, the newest installed release of this
6
+ * package refreshes the defaults itself, with its own code and its own bundled copies.
7
+ */
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { PACKAGE_ROOT } from './plugin-loader.mjs';
11
+ import { comparePackageVersions } from './package-version.mjs';
12
+ import { findLinkedSourceCheckout } from './global-brain-updates.mjs';
13
+ import { runBounded } from './startup-health.mjs';
14
+
15
+ const PACKAGE = 'total-recall-brain';
16
+
17
+ function readVersion(dir) {
18
+ try { return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version || null; } catch { return null; }
19
+ }
20
+
21
+ /**
22
+ * The release this process runs, read once at import. A source update can fast-forward this
23
+ * checkout during the run, so reading the file again later would report the new release.
24
+ */
25
+ export const RUNNING_VERSION = readVersion(PACKAGE_ROOT);
26
+
27
+ /** Installed releases this run may have moved: the global install and source checkouts it updated. */
28
+ export function installedReleases({ global, sources } = {}) {
29
+ const dirs = new Set();
30
+ const add = dir => { try { dirs.add(fs.realpathSync(dir)); } catch {} };
31
+ if (global?.root) {
32
+ const direct = path.join(global.root, PACKAGE);
33
+ const dir = fs.existsSync(direct) ? direct : findLinkedSourceCheckout(global.root, PACKAGE);
34
+ if (dir) add(dir);
35
+ }
36
+ for (const row of sources?.results || []) {
37
+ if (row?.package === PACKAGE && row.status === 'updated' && row.root) add(row.root);
38
+ }
39
+ return [...dirs]
40
+ .map(dir => ({ dir, version: readVersion(dir) }))
41
+ .filter(release => release.version && fs.existsSync(path.join(release.dir, 'bin', 'total-recall.mjs')));
42
+ }
43
+
44
+ /** The JSON report `update --defaults --json` prints, even after other output on stdout. */
45
+ export function parseDefaultsReport(text) {
46
+ const trimmed = String(text || '').trim();
47
+ try { return JSON.parse(trimmed); } catch {}
48
+ const start = trimmed.lastIndexOf('\n{\n');
49
+ if (start >= 0) { try { return JSON.parse(trimmed.slice(start + 1)); } catch {} }
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * When an installed release is newer than the running code, run its `update --defaults` and report
55
+ * what it did. Never throws: the outcome is a row in the maintenance report.
56
+ * @returns {Promise<{status:'current'|'refreshed'|'failed', running?:string|null, installed?:string, version?:string, from?:string|null, results?:object[], error?:string}>}
57
+ */
58
+ export async function handOffDefaultsToRelease({ global, sources, cwd = process.cwd(), running = RUNNING_VERSION,
59
+ execPath = process.execPath, run = runBounded, timeoutMs = 10 * 60 * 1000 } = {}) {
60
+ let newest = null;
61
+ try {
62
+ for (const release of installedReleases({ global, sources })) {
63
+ if (!newest || comparePackageVersions(release.version, newest.version) === 1) newest = release;
64
+ }
65
+ } catch (error) {
66
+ return { status: 'failed', running, error: `Installed releases could not be read: ${error.message}` };
67
+ }
68
+ if (!newest || comparePackageVersions(newest.version, running) !== 1) {
69
+ return { status: 'current', running, ...(newest ? { installed: newest.version } : {}) };
70
+ }
71
+ const handoff = { version: newest.version, from: running };
72
+ let result;
73
+ try {
74
+ result = await run(execPath, [path.join(newest.dir, 'bin', 'total-recall.mjs'), 'update', '--defaults', '--json'],
75
+ { cwd, timeoutMs, maxBytes: 1024 * 1024 });
76
+ } catch (error) {
77
+ return { status: 'failed', ...handoff, error: `update --defaults could not run: ${error.message}` };
78
+ }
79
+ const report = parseDefaultsReport(result?.output);
80
+ if (result?.code === 0 && report && report.failed === 0) {
81
+ return { status: 'refreshed', ...handoff, results: report.results || [] };
82
+ }
83
+ const failures = (report?.results || []).filter(row => row.status === 'failed').map(row => `${row.root}: ${row.error}`);
84
+ const error = result?.reason ? `update --defaults stopped: ${result.reason}`
85
+ : failures.length ? failures.join('; ')
86
+ : !report ? `update --defaults printed no report (exit ${result?.code ?? 'unknown'}): ${String(result?.stderr || result?.output || '').trim().slice(-1000)}`
87
+ : `update --defaults exited ${result?.code}`;
88
+ return { status: 'failed', ...handoff, error, ...(report?.results ? { results: report.results } : {}) };
89
+ }