total-recall-brain 3.18.2 → 3.18.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 (45) hide show
  1. package/frontend/dist/assets/index-CGqvdwHT.js +5742 -0
  2. package/frontend/dist/index.html +1 -1
  3. package/frontend/src/api/config.ts +16 -2
  4. package/frontend/src/api/keys.ts +111 -0
  5. package/frontend/src/api/notifications.ts +54 -6
  6. package/frontend/src/api/update.ts +12 -1
  7. package/frontend/src/components/BrainSelector.spec.tsx +71 -47
  8. package/frontend/src/components/BrainSelector.tsx +112 -35
  9. package/frontend/src/components/MobilePairing.spec.tsx +44 -14
  10. package/frontend/src/components/MobilePairing.tsx +229 -22
  11. package/frontend/src/pages/NotificationsPage.tsx +6 -3
  12. package/frontend/src/pages/RulesPage.spec.tsx +46 -15
  13. package/frontend/src/pages/RulesPage.tsx +41 -9
  14. package/frontend/src/pages/SecretsPage.tsx +316 -3
  15. package/frontend/src/pages/SettingsPage.spec.tsx +30 -6
  16. package/frontend/src/pages/SettingsPage.tsx +206 -85
  17. package/package.json +1 -1
  18. package/src/cli/agent-dir.mjs +2 -1
  19. package/src/cli/secret.mjs +180 -3
  20. package/src/core/daemon-loop.mjs +31 -5
  21. package/src/core/notifications.mjs +204 -34
  22. package/src/core/package-auto-update.mjs +99 -3
  23. package/src/core/pairing.mjs +168 -0
  24. package/src/core/provider-account-sync.mjs +1145 -0
  25. package/src/core/provider-catalog.mjs +11 -4
  26. package/src/core/research.mjs +98 -24
  27. package/src/core/scheduler.mjs +31 -9
  28. package/src/core/schema.mjs +23 -0
  29. package/src/core/secret-integration-research.mjs +325 -0
  30. package/src/core/secrets-store.mjs +278 -14
  31. package/src/core/secrets-sync.mjs +1 -1
  32. package/src/core/source-adapters.mjs +247 -22
  33. package/src/core/task-executors.mjs +161 -14
  34. package/src/core/webhook-handlers.mjs +123 -14
  35. package/src/server/auth.mjs +6 -0
  36. package/src/server/rest.mjs +2 -0
  37. package/src/server/route-manifest.json +24 -0
  38. package/src/server/routes/config.mjs +64 -23
  39. package/src/server/routes/mesh.mjs +2 -1
  40. package/src/server/routes/notifications.mjs +103 -0
  41. package/src/server/routes/rules.mjs +75 -20
  42. package/src/server/routes/secrets.mjs +110 -4
  43. package/src/server/routes/system.mjs +20 -1
  44. package/src/server/routes/update.mjs +11 -4
  45. package/frontend/dist/assets/index-D5xRw5uq.js +0 -5750
@@ -1,11 +1,40 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { fetchConfigJson, saveConfigJson, runSandbox, fetchHealth, runAgentDiagnostics, checkUpdate, runUpdate, fetchBrains } from '../api';
2
+ import {
3
+ fetchConfigJson,
4
+ saveConfigJson,
5
+ fetchHealth,
6
+ runAgentDiagnostics,
7
+ checkUpdate,
8
+ runUpdate,
9
+ triggerRecompile,
10
+ apiFetch,
11
+ getApiBase,
12
+ } from '../api';
3
13
  import type { HealthData } from '../types';
4
14
  import type { ConfigJson } from '../types';
5
15
  import { MobilePairing } from '../components/MobilePairing';
6
16
 
17
+ function normalizeConfigJson(data: ConfigJson): ConfigJson {
18
+ const next = { ...data } as ConfigJson & Record<string, any>;
19
+ if (!next.security) next.security = {};
20
+ if (!next.security.dashboard) next.security.dashboard = {};
21
+ if (!next.security.api) next.security.api = {};
22
+ if (!next.security.network) next.security.network = {};
23
+ if (!next.security.bind) next.security.bind = {};
24
+ if (!next.security.rate_limits) next.security.rate_limits = {};
25
+ if (!next.security.sandbox) next.security.sandbox = {};
26
+ if (!next.security.privacy) next.security.privacy = {};
27
+ if (!next.budget) next.budget = { budget: {} };
28
+ if (!next.budget.budget) next.budget.budget = {};
29
+ if (!next.brain) next.brain = {};
30
+ if (!next.secrets) next.secrets = {};
31
+ return next;
32
+ }
33
+
7
34
  export default function SettingsPage({ activeBrainId }: { activeBrainId?: string }) {
8
35
  const [configData, setConfigData] = useState<ConfigJson | null>(null);
36
+ const [loadError, setLoadError] = useState<string | null>(null);
37
+ const [loading, setLoading] = useState(true);
9
38
  const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
10
39
  const [sandboxLog, setSandboxLog] = useState('');
11
40
 
@@ -14,35 +43,43 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
14
43
  const [runningDiagnostics, setRunningDiagnostics] = useState(false);
15
44
  const [diagnosticLogs, setDiagnosticLogs] = useState<string | null>(null);
16
45
 
17
- const [updateInfo, setUpdateInfo] = useState<{ updateAvailable: boolean, latestVersion?: string } | null>(null);
46
+ const [updateInfo, setUpdateInfo] = useState<{
47
+ updateAvailable: boolean;
48
+ latestVersion?: string;
49
+ currentVersion?: string;
50
+ } | null>(null);
18
51
  const [updating, setUpdating] = useState(false);
19
52
  const [updateMessage, setUpdateMessage] = useState<string | null>(null);
53
+ const [rebuilding, setRebuilding] = useState(false);
20
54
 
21
- const [brains, setBrains] = useState<unknown[]>([]);
22
- const activeBrain = activeBrainId || localStorage.getItem('total-recall-active-brain') || '';
55
+ const loadConfig = async () => {
56
+ setLoading(true);
57
+ setLoadError(null);
58
+ try {
59
+ const data = await fetchConfigJson();
60
+ setConfigData(normalizeConfigJson(data));
61
+ } catch (err) {
62
+ console.error(err);
63
+ setLoadError(err instanceof Error ? err.message : 'Failed to load settings');
64
+ setConfigData(null);
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ };
23
69
 
24
70
  useEffect(() => {
25
71
  fetchHealth().then(setHealth).catch(console.error);
26
- checkUpdate().then(setUpdateInfo).catch(console.error);
27
- fetchBrains().then(setBrains).catch(console.error);
28
- fetchConfigJson()
29
- .then(data => {
30
- // Ensure all nested structures exist for controlled inputs
31
- if (!data.security) data.security = {};
32
- if (!data.security.dashboard) data.security.dashboard = {};
33
- if (!data.security.api) data.security.api = {};
34
- if (!data.security.network) data.security.network = {};
35
- if (!data.security.bind) data.security.bind = {};
36
- if (!data.security.rate_limits) data.security.rate_limits = {};
37
- if (!data.security.sandbox) data.security.sandbox = {};
38
- if (!data.security.privacy) data.security.privacy = {};
39
- if (!data.budget) data.budget = { budget: {} };
40
- if (!data.budget.budget) data.budget.budget = {};
41
- if (!data.brain) data.brain = {};
42
- if (!data.secrets) data.secrets = {};
43
- setConfigData(data);
44
- })
72
+ // Update check must never block config load (server used to spawnSync npm view).
73
+ checkUpdate()
74
+ .then((u) =>
75
+ setUpdateInfo({
76
+ updateAvailable: !!u.updateAvailable,
77
+ latestVersion: u.latestVersion,
78
+ currentVersion: u.currentVersion,
79
+ }),
80
+ )
45
81
  .catch(console.error);
82
+ loadConfig();
46
83
  }, [activeBrainId]);
47
84
 
48
85
 
@@ -54,6 +91,12 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
54
91
  { id: 'grok', name: 'Grok CLI', desc: 'xAI developer binary integration' }
55
92
  ];
56
93
 
94
+ const availableAgents: string[] = Array.isArray(health?.cli_agents)
95
+ ? health.cli_agents
96
+ : Array.isArray((health as unknown as { agents?: string[] } | null)?.agents)
97
+ ? ((health as unknown as { agents: string[] }).agents)
98
+ : [];
99
+
57
100
  const handleRunDiagnostics = async (e: React.FormEvent) => {
58
101
  e.preventDefault();
59
102
  setRunningDiagnostics(true);
@@ -75,6 +118,14 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
75
118
  try {
76
119
  const res = await runUpdate();
77
120
  setUpdateMessage(res.success ? 'Update complete. Restarting...' : 'Update failed: ' + res.message);
121
+ const refreshed = await checkUpdate().catch(() => null);
122
+ if (refreshed) {
123
+ setUpdateInfo({
124
+ updateAvailable: !!refreshed.updateAvailable,
125
+ latestVersion: refreshed.latestVersion,
126
+ currentVersion: refreshed.currentVersion,
127
+ });
128
+ }
78
129
  } catch (err: unknown) {
79
130
  setUpdateMessage('Update error: ' + (err as Error).message);
80
131
  } finally {
@@ -86,7 +137,18 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
86
137
  if (!configData) return;
87
138
  setSaveStatus('saving');
88
139
  try {
89
- await saveConfigJson(configData);
140
+ // Do not persist masked secret placeholders back into the store.
141
+ const payload = { ...configData } as ConfigJson & { secrets?: Record<string, string> };
142
+ if (payload.secrets) {
143
+ const cleaned: Record<string, string> = {};
144
+ for (const [k, v] of Object.entries(payload.secrets)) {
145
+ if (typeof v === 'string' && v && !v.includes('•') && v !== '[redacted]') {
146
+ cleaned[k] = v;
147
+ }
148
+ }
149
+ payload.secrets = cleaned;
150
+ }
151
+ await saveConfigJson(payload);
90
152
  setSaveStatus('saved');
91
153
  setTimeout(() => setSaveStatus('idle'), 2500);
92
154
  } catch (err) {
@@ -96,18 +158,39 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
96
158
  }
97
159
  };
98
160
 
99
- const executeSandboxCommand = async (command: string, description: string) => {
100
- setSandboxLog(`[Running] ${description}...\n$ ${command}`);
101
- const res = await runSandbox(`
102
- const { execSync } = require('child_process');
103
- try {
104
- const out = execSync('${command}', { encoding: 'utf8' });
105
- console.log(out);
106
- } catch (e) {
107
- console.error(e.stdout || e.message);
108
- }
109
- `);
110
- setSandboxLog(`[Finished] ${description}\n\n` + res.output);
161
+ const handleRebuildIndex = async () => {
162
+ setRebuilding(true);
163
+ setSandboxLog('[Running] Rebuilding instruction surfaces…');
164
+ try {
165
+ const res = await triggerRecompile();
166
+ setSandboxLog(
167
+ `[Finished] Rebuilding instruction surfaces\n\n${res.message || (res.success ? 'Compile complete.' : 'Compile finished with warnings.')}`,
168
+ );
169
+ } catch (err) {
170
+ setSandboxLog(`[Failed] Rebuild\n\n${err instanceof Error ? err.message : String(err)}`);
171
+ } finally {
172
+ setRebuilding(false);
173
+ }
174
+ };
175
+
176
+ const handleDownloadExtension = async () => {
177
+ try {
178
+ const res = await apiFetch(`${getApiBase()}/api/extension/download`);
179
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`);
180
+ const blob = await res.blob();
181
+ const url = URL.createObjectURL(blob);
182
+ const a = document.createElement('a');
183
+ a.href = url;
184
+ a.download = 'total-recall-extension.zip';
185
+ document.body.appendChild(a);
186
+ a.click();
187
+ a.remove();
188
+ URL.revokeObjectURL(url);
189
+ } catch (err) {
190
+ setSandboxLog(
191
+ `[Failed] Extension download\n\n${err instanceof Error ? err.message : String(err)}`,
192
+ );
193
+ }
111
194
  };
112
195
 
113
196
  // State Update Helpers
@@ -153,12 +236,16 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
153
236
 
154
237
  // Instead of parsing it back and forth on every keystroke,
155
238
  // we just use simple helpers that don't aggressively trim trailing commas.
156
- const getCsv = (arr: unknown) => Array.isArray(arr) ? arr.join(', ') : '';
157
- const setCsv = (str: string) => str.split(',').map(s => s.trimStart());
158
-
159
- if (!configData) {
239
+ const getCsv = (arr: unknown) => Array.isArray(arr) ? arr.filter(Boolean).join(', ') : '';
240
+ const setCsv = (str: string) =>
241
+ str
242
+ .split(',')
243
+ .map((s) => s.trim())
244
+ .filter((s) => s.length > 0);
245
+
246
+ if (loading && !configData) {
160
247
  return (
161
- <div style={{ padding: 40, color: 'var(--text-secondary)' }}>
248
+ <div style={{ padding: 40, color: 'var(--text-secondary)' }} data-testid="settings-loading">
162
249
  <div className="glass-card" style={{ display: 'inline-block', padding: '12px 24px' }}>
163
250
  Loading Settings...
164
251
  </div>
@@ -166,8 +253,38 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
166
253
  );
167
254
  }
168
255
 
256
+ if (loadError && !configData) {
257
+ return (
258
+ <div style={{ padding: 40 }} data-testid="settings-error" role="alert">
259
+ <div
260
+ className="glass-card"
261
+ style={{
262
+ display: 'inline-block',
263
+ padding: '16px 24px',
264
+ border: '1px solid #ef4444',
265
+ color: '#ef4444',
266
+ }}
267
+ >
268
+ Failed to load settings: {loadError}
269
+ <button
270
+ type="button"
271
+ className="btn-primary"
272
+ style={{ marginLeft: 16 }}
273
+ onClick={() => loadConfig()}
274
+ >
275
+ Retry
276
+ </button>
277
+ </div>
278
+ </div>
279
+ );
280
+ }
281
+
282
+ if (!configData) {
283
+ return null;
284
+ }
285
+
169
286
  return (
170
- <div className="settings-page-wrapper">
287
+ <div className="settings-page-wrapper" data-testid="settings-page">
171
288
  <style>{`
172
289
  .settings-page-wrapper {
173
290
  padding: 32px 40px 100px;
@@ -627,7 +744,12 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
627
744
  step="0.01"
628
745
  className="settings-input"
629
746
  value={configData.budget.budget?.daily_cap_usd ?? 5.0}
630
- onChange={(e) => updateBudgetProp('monthly_soft_cap', e.target.value === '' ? null : Number(e.target.value))}
747
+ onChange={(e) =>
748
+ updateBudgetProp(
749
+ 'daily_cap_usd',
750
+ e.target.value === '' ? null : Number(e.target.value),
751
+ )
752
+ }
631
753
  />
632
754
  </div>
633
755
  <div className="field-col" style={{ flex: 1 }}>
@@ -638,7 +760,12 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
638
760
  step="0.01"
639
761
  className="settings-input"
640
762
  value={configData.budget.budget?.weekly_cap_usd ?? 20.0}
641
- onChange={(e) => updateBudgetProp('weekly_cap_usd', parseFloat(e.target.value))}
763
+ onChange={(e) =>
764
+ updateBudgetProp(
765
+ 'weekly_cap_usd',
766
+ e.target.value === '' ? null : Number(e.target.value),
767
+ )
768
+ }
642
769
  />
643
770
  </div>
644
771
  </div>
@@ -654,28 +781,10 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
654
781
  </div>
655
782
  </div>
656
783
 
657
- <div className="field-col">
658
- <label className="field-label" style={{ color: 'var(--text-accent)', fontWeight: 600 }}>Select Active Brain</label>
659
- <select
660
- className="settings-select"
661
- style={{ border: '1px solid var(--border-accent)', background: 'rgba(59, 130, 246, 0.05)' }}
662
- value={activeBrain}
663
- onChange={(e) => {
664
- const val = e.target.value;
665
- if (val) {
666
- localStorage.setItem('total-recall-active-brain', val);
667
- } else {
668
- localStorage.removeItem('total-recall-active-brain');
669
- }
670
- window.location.reload();
671
- }}
672
- >
673
- <option value="">Global Brain (Root)</option>
674
- {brains.map((b: any) => (
675
- <option key={b.name} value={b.name}>{b.name} ({b.role} - {b.nodes} nodes)</option>
676
- ))}
677
- </select>
678
- </div>
784
+ <p style={{ fontSize: 12, color: 'var(--text-tertiary)', margin: '0 0 12px', lineHeight: 1.5 }}>
785
+ Active vault layer is chosen in the <strong>sidebar brain selector</strong> (bottom left).
786
+ Fields below edit this install’s <code>brain.json</code> identity — not the active layer.
787
+ </p>
679
788
 
680
789
  <div style={{ display: 'flex', gap: 16 }}>
681
790
  <div className="field-col" style={{ flex: 1 }}>
@@ -765,7 +874,7 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
765
874
  <option value="antigravity">Antigravity (Google Deepmind)</option>
766
875
  <option value="gemini">Gemini (Google)</option>
767
876
  <option value="claude">Claude (Anthropic)</option>
768
- <option value="openai">OpenAI (O-Series)</option>
877
+ <option value="codex">Codex (OpenAI)</option>
769
878
  <option value="grok">Grok (xAI)</option>
770
879
  </select>
771
880
  </div>
@@ -788,22 +897,30 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
788
897
  <p style={{ fontSize: 13, color: 'var(--text-tertiary)', margin: '0 0 16px', lineHeight: 1.5 }}>
789
898
  Capture context directly from your browser. Install from the Chrome Web Store or load unpacked from <code>/extension</code> directory.
790
899
  </p>
791
- <a href="/api/extension/download" download className="btn-primary" style={{ display: 'block', textAlign: 'center', textDecoration: 'none', width: '100%', boxSizing: 'border-box' }}>Download Extension</a>
900
+ <button
901
+ type="button"
902
+ className="btn-primary"
903
+ style={{ display: 'block', width: '100%', boxSizing: 'border-box' }}
904
+ onClick={handleDownloadExtension}
905
+ >
906
+ Download Extension
907
+ </button>
792
908
  </div>
793
909
 
794
910
  <div style={{ background: 'rgba(255,255,255,0.03)', padding: 20, borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)' }}>
795
911
  <div style={{ fontSize: 24, marginBottom: 12 }}>🛠️</div>
796
912
  <h4 style={{ margin: '0 0 8px', fontSize: 15 }}>Developer Utilities</h4>
797
913
  <p style={{ fontSize: 13, color: 'var(--text-tertiary)', margin: '0 0 16px', lineHeight: 1.5 }}>
798
- Run background maintenance tasks or re-index the local search tree.
914
+ Rebuild compiled instruction surfaces (vault compile) without a shell sandbox.
799
915
  </p>
800
916
  <div style={{ display: 'flex', gap: 12 }}>
801
917
  <button
802
918
  className="btn-secondary"
803
919
  style={{ flex: 1 }}
804
- onClick={() => executeSandboxCommand('npx total-recall compile', 'Rebuilding Search Indexes')}
920
+ disabled={rebuilding}
921
+ onClick={handleRebuildIndex}
805
922
  >
806
- Rebuild Index
923
+ {rebuilding ? 'Rebuilding…' : 'Rebuild Index'}
807
924
  </button>
808
925
  </div>
809
926
  </div>
@@ -826,13 +943,19 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
826
943
  </div>
827
944
  </div>
828
945
 
829
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 24, padding: '16px 20px', background: 'rgba(0,0,0,0.2)', borderRadius: 12, border: '1px solid rgba(255,255,255,0.05)' }}>
946
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 24, padding: '16px 20px', background: 'rgba(0,0,0,0.2)', borderRadius: 12, border: '1px solid rgba(255,255,255,0.05)', gap: 16, flexWrap: 'wrap' }}>
830
947
  <div>
831
948
  <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>
832
- {updateInfo?.updateAvailable ? `Update Available (v${updateInfo.latestVersion})` : 'System is up to date'}
949
+ {updateInfo?.updateAvailable
950
+ ? `Update available (latest v${updateInfo.latestVersion || '?'})`
951
+ : 'Host package is up to date'}
833
952
  </div>
834
953
  <div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 4 }}>
835
- {updateInfo?.updateAvailable ? 'A new version of Total Recall is available for download.' : 'You are running the latest version.'}
954
+ Running v{updateInfo?.currentVersion || health?.version || '—'}
955
+ {updateInfo?.latestVersion ? ` · npm latest v${updateInfo.latestVersion}` : ''}
956
+ {updateInfo?.updateAvailable
957
+ ? ' · registered projects or host package can be upgraded'
958
+ : ''}
836
959
  </div>
837
960
  {updateMessage && (
838
961
  <div style={{ fontSize: 12, color: updateMessage.includes('error') || updateMessage.includes('failed') ? '#ef4444' : '#10b981', marginTop: 8 }}>
@@ -840,16 +963,14 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
840
963
  </div>
841
964
  )}
842
965
  </div>
843
- {updateInfo?.updateAvailable && (
844
- <button
845
- className="btn-primary"
846
- onClick={handleRunUpdate}
847
- disabled={updating}
848
- style={{ background: '#3fb950', border: 'none' }}
849
- >
850
- {updating ? 'Updating...' : 'Download & Restart'}
851
- </button>
852
- )}
966
+ <button
967
+ className="btn-primary"
968
+ onClick={handleRunUpdate}
969
+ disabled={updating}
970
+ style={{ background: updateInfo?.updateAvailable ? '#3fb950' : undefined, border: 'none' }}
971
+ >
972
+ {updating ? 'Updating…' : updateInfo?.updateAvailable ? 'Apply Updates' : 'Check & Sync Projects'}
973
+ </button>
853
974
  </div>
854
975
  </div>
855
976
 
@@ -868,7 +989,7 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
868
989
 
869
990
  <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
870
991
  {AGENTS_LIST.map((a) => {
871
- const activeAgent = health?.cli_agents?.includes(a.id);
992
+ const activeAgent = availableAgents.includes(a.id);
872
993
  return (
873
994
  <div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 12, background: 'var(--bg-tertiary)', padding: 12, borderRadius: 8, border: '1px solid var(--border)' }}>
874
995
  <div style={{ flex: 1 }}>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.18.2",
3
+ "version": "3.18.3",
4
4
  "description": "Personal AI system — database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -56,7 +56,8 @@ export function detectProjectBrain(startDir = process.cwd()) {
56
56
  if (fs.existsSync(path.join(c.candidate, 'SKILL.md')) || fs.existsSync(path.join(c.candidate, 'memory-vault'))) {
57
57
  return {
58
58
  agentDir: c.dir,
59
- brainDir: getGlobalBrainDir(),
59
+ // Project brain lives at the detected candidate — never alias to global.
60
+ brainDir: c.candidate,
60
61
  projectRoot: dir,
61
62
  };
62
63
  }