crbro-memory 2.9.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +256 -456
  3. package/SECURITY.md +135 -8
  4. package/bin/crbro.mjs +555 -299
  5. package/dist/build-id.json +10 -0
  6. package/dist/cli/common.js +211 -0
  7. package/dist/cli/export.js +522 -0
  8. package/dist/cli/import.js +683 -0
  9. package/dist/cli/markdown.js +313 -0
  10. package/dist/cli/mcpclient.js +107 -0
  11. package/dist/cli/serve.js +453 -0
  12. package/dist/cli/view.js +358 -0
  13. package/dist/cli/viewer-page.js +378 -0
  14. package/dist/daemon/daemon.js +119 -11
  15. package/dist/daemon/endpoint.js +222 -21
  16. package/dist/daemon/lines.js +102 -13
  17. package/dist/daemon/proxy.js +480 -125
  18. package/dist/daemon/surface.js +116 -0
  19. package/dist/daemon/trace.js +73 -0
  20. package/dist/engine/atrest.js +132 -0
  21. package/dist/engine/backup.js +143 -19
  22. package/dist/engine/brain.js +22 -6
  23. package/dist/engine/codexconfig.js +476 -0
  24. package/dist/engine/cortex.js +762 -311
  25. package/dist/engine/doctor.js +676 -0
  26. package/dist/engine/evidence.js +171 -0
  27. package/dist/engine/heat.js +81 -37
  28. package/dist/engine/hippocampus.js +50 -36
  29. package/dist/engine/hookrefresh.js +296 -0
  30. package/dist/engine/hookverify.js +15 -3
  31. package/dist/engine/installer.js +1022 -0
  32. package/dist/engine/installio.js +279 -0
  33. package/dist/engine/keychain.js +735 -144
  34. package/dist/engine/maintenance.js +355 -87
  35. package/dist/engine/modinstall.js +98 -45
  36. package/dist/engine/prefrontal.js +126 -69
  37. package/dist/engine/projects.js +80 -0
  38. package/dist/engine/quarantine.js +204 -0
  39. package/dist/engine/secrets.js +41 -5
  40. package/dist/engine/shelf.js +34 -11
  41. package/dist/engine/synapses.js +183 -80
  42. package/dist/engine/triggers.js +141 -11
  43. package/dist/index.js +37 -3
  44. package/dist/mcp-surface.json +1 -0
  45. package/dist/miner/extractor.js +64 -17
  46. package/dist/miner/scheduler.js +144 -68
  47. package/dist/search/index.js +810 -176
  48. package/dist/search/quality.js +31 -0
  49. package/dist/search/semantic.js +208 -18
  50. package/dist/search/tokenize.js +44 -0
  51. package/dist/server.js +649 -343
  52. package/dist/sync/git.js +203 -72
  53. package/dist/sync/materialize.js +33 -8
  54. package/dist/sync/space.js +286 -101
  55. package/dist/sync/validate.js +204 -0
  56. package/dist/utils/budget.js +15 -2
  57. package/dist/utils/fs.js +40 -6
  58. package/dist/utils/ids.js +128 -14
  59. package/dist/utils/lock.js +219 -52
  60. package/hooks/crbro-codex.mjs +284 -0
  61. package/hooks/crbro-guard.mjs +281 -173
  62. package/hooks/crbro-hooks.shipped.json +20 -0
  63. package/hooks/crbro-lifecycle.mjs +16 -4
  64. package/hooks/crbro-subagent.mjs +180 -180
  65. package/package.json +12 -8
  66. package/dist/daemon/daemon.d.ts +0 -25
  67. package/dist/daemon/daemon.d.ts.map +0 -1
  68. package/dist/daemon/daemon.js.map +0 -1
  69. package/dist/daemon/endpoint.d.ts +0 -66
  70. package/dist/daemon/endpoint.d.ts.map +0 -1
  71. package/dist/daemon/endpoint.js.map +0 -1
  72. package/dist/daemon/lines.d.ts +0 -55
  73. package/dist/daemon/lines.d.ts.map +0 -1
  74. package/dist/daemon/lines.js.map +0 -1
  75. package/dist/daemon/proxy.d.ts +0 -55
  76. package/dist/daemon/proxy.d.ts.map +0 -1
  77. package/dist/daemon/proxy.js.map +0 -1
  78. package/dist/engine/backup.d.ts +0 -68
  79. package/dist/engine/backup.d.ts.map +0 -1
  80. package/dist/engine/backup.js.map +0 -1
  81. package/dist/engine/brain.d.ts +0 -79
  82. package/dist/engine/brain.d.ts.map +0 -1
  83. package/dist/engine/brain.js.map +0 -1
  84. package/dist/engine/cortex.d.ts +0 -670
  85. package/dist/engine/cortex.d.ts.map +0 -1
  86. package/dist/engine/cortex.js.map +0 -1
  87. package/dist/engine/dates.d.ts +0 -28
  88. package/dist/engine/dates.d.ts.map +0 -1
  89. package/dist/engine/dates.js.map +0 -1
  90. package/dist/engine/heat.d.ts +0 -28
  91. package/dist/engine/heat.d.ts.map +0 -1
  92. package/dist/engine/heat.js.map +0 -1
  93. package/dist/engine/hippocampus.d.ts +0 -60
  94. package/dist/engine/hippocampus.d.ts.map +0 -1
  95. package/dist/engine/hippocampus.js.map +0 -1
  96. package/dist/engine/hookverify.d.ts +0 -42
  97. package/dist/engine/hookverify.d.ts.map +0 -1
  98. package/dist/engine/hookverify.js.map +0 -1
  99. package/dist/engine/keychain.d.ts +0 -32
  100. package/dist/engine/keychain.d.ts.map +0 -1
  101. package/dist/engine/keychain.js.map +0 -1
  102. package/dist/engine/maintenance.d.ts +0 -218
  103. package/dist/engine/maintenance.d.ts.map +0 -1
  104. package/dist/engine/maintenance.js.map +0 -1
  105. package/dist/engine/modinstall.d.ts +0 -215
  106. package/dist/engine/modinstall.d.ts.map +0 -1
  107. package/dist/engine/modinstall.js.map +0 -1
  108. package/dist/engine/postmortem.d.ts +0 -81
  109. package/dist/engine/postmortem.d.ts.map +0 -1
  110. package/dist/engine/postmortem.js.map +0 -1
  111. package/dist/engine/prefrontal.d.ts +0 -61
  112. package/dist/engine/prefrontal.d.ts.map +0 -1
  113. package/dist/engine/prefrontal.js.map +0 -1
  114. package/dist/engine/secrets.d.ts +0 -30
  115. package/dist/engine/secrets.d.ts.map +0 -1
  116. package/dist/engine/secrets.js.map +0 -1
  117. package/dist/engine/shelf.d.ts +0 -156
  118. package/dist/engine/shelf.d.ts.map +0 -1
  119. package/dist/engine/shelf.js.map +0 -1
  120. package/dist/engine/source.d.ts +0 -6
  121. package/dist/engine/source.d.ts.map +0 -1
  122. package/dist/engine/source.js.map +0 -1
  123. package/dist/engine/synapses.d.ts +0 -82
  124. package/dist/engine/synapses.d.ts.map +0 -1
  125. package/dist/engine/synapses.js.map +0 -1
  126. package/dist/engine/triggers.d.ts +0 -53
  127. package/dist/engine/triggers.d.ts.map +0 -1
  128. package/dist/engine/triggers.js.map +0 -1
  129. package/dist/engine/usage.d.ts +0 -61
  130. package/dist/engine/usage.d.ts.map +0 -1
  131. package/dist/engine/usage.js.map +0 -1
  132. package/dist/index.d.ts +0 -2
  133. package/dist/index.d.ts.map +0 -1
  134. package/dist/index.js.map +0 -1
  135. package/dist/miner/extractor.d.ts +0 -12
  136. package/dist/miner/extractor.d.ts.map +0 -1
  137. package/dist/miner/extractor.js.map +0 -1
  138. package/dist/miner/index.d.ts +0 -61
  139. package/dist/miner/index.d.ts.map +0 -1
  140. package/dist/miner/index.js.map +0 -1
  141. package/dist/miner/scheduler.d.ts +0 -30
  142. package/dist/miner/scheduler.d.ts.map +0 -1
  143. package/dist/miner/scheduler.js.map +0 -1
  144. package/dist/miner/types.d.ts +0 -11
  145. package/dist/miner/types.d.ts.map +0 -1
  146. package/dist/miner/types.js.map +0 -1
  147. package/dist/search/index.d.ts +0 -323
  148. package/dist/search/index.d.ts.map +0 -1
  149. package/dist/search/index.js.map +0 -1
  150. package/dist/search/semantic.d.ts +0 -80
  151. package/dist/search/semantic.d.ts.map +0 -1
  152. package/dist/search/semantic.js.map +0 -1
  153. package/dist/search/synonyms.d.ts +0 -2
  154. package/dist/search/synonyms.d.ts.map +0 -1
  155. package/dist/search/synonyms.js.map +0 -1
  156. package/dist/search/tokenize.d.ts +0 -39
  157. package/dist/search/tokenize.d.ts.map +0 -1
  158. package/dist/search/tokenize.js.map +0 -1
  159. package/dist/server.d.ts +0 -41
  160. package/dist/server.d.ts.map +0 -1
  161. package/dist/server.js.map +0 -1
  162. package/dist/sync/git.d.ts +0 -36
  163. package/dist/sync/git.d.ts.map +0 -1
  164. package/dist/sync/git.js.map +0 -1
  165. package/dist/sync/materialize.d.ts +0 -35
  166. package/dist/sync/materialize.d.ts.map +0 -1
  167. package/dist/sync/materialize.js.map +0 -1
  168. package/dist/sync/ops.d.ts +0 -142
  169. package/dist/sync/ops.d.ts.map +0 -1
  170. package/dist/sync/ops.js.map +0 -1
  171. package/dist/sync/space.d.ts +0 -116
  172. package/dist/sync/space.d.ts.map +0 -1
  173. package/dist/sync/space.js.map +0 -1
  174. package/dist/types/index.d.ts +0 -346
  175. package/dist/types/index.d.ts.map +0 -1
  176. package/dist/types/index.js.map +0 -1
  177. package/dist/utils/budget.d.ts +0 -44
  178. package/dist/utils/budget.d.ts.map +0 -1
  179. package/dist/utils/budget.js.map +0 -1
  180. package/dist/utils/fs.d.ts +0 -59
  181. package/dist/utils/fs.d.ts.map +0 -1
  182. package/dist/utils/fs.js.map +0 -1
  183. package/dist/utils/hash.d.ts +0 -20
  184. package/dist/utils/hash.d.ts.map +0 -1
  185. package/dist/utils/hash.js.map +0 -1
  186. package/dist/utils/ids.d.ts +0 -47
  187. package/dist/utils/ids.d.ts.map +0 -1
  188. package/dist/utils/ids.js.map +0 -1
  189. package/dist/utils/lock.d.ts +0 -11
  190. package/dist/utils/lock.d.ts.map +0 -1
  191. package/dist/utils/lock.js.map +0 -1
  192. package/dist/utils/transcripts.d.ts +0 -63
  193. package/dist/utils/transcripts.d.ts.map +0 -1
  194. package/dist/utils/transcripts.js.map +0 -1
  195. package/dist/version.d.ts +0 -26
  196. package/dist/version.d.ts.map +0 -1
  197. package/dist/version.js.map +0 -1
@@ -0,0 +1,378 @@
1
+ "use strict";
2
+ // ─── The one page of crbro-memory view ───────────────────────────
3
+ //
4
+ // Self-contained: no external script, style, font or image, so the strict
5
+ // CSP of the viewer holds. Every piece of brain text reaches the DOM through
6
+ // textContent, never innerHTML. The token arrives once in the URL fragment
7
+ // (never sent to the server, never in a Referer), moves to sessionStorage,
8
+ // and leaves the address bar before anything else runs.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.viewerPage = viewerPage;
11
+ exports.viewerCsp = viewerCsp;
12
+ const STYLE = `
13
+ :root{color-scheme:light dark;--bg:#f7f7f5;--panel:#fff;--ink:#1d1d1b;--muted:#6b6b66;--line:#e2e1dc;--accent:#2f5bd3;--accent-ink:#fff;--warn:#a15c00;--warn-bg:#fff4e0;--bad:#b42318;--bad-bg:#fdecea;--ok:#1b7f3b;--chip:#efeee9}
14
+ @media (prefers-color-scheme:dark){:root{--bg:#141413;--panel:#1d1d1b;--ink:#ecebe6;--muted:#a3a29b;--line:#34332f;--accent:#7d9bff;--accent-ink:#0e0e0d;--warn:#f0b257;--warn-bg:#2e2412;--bad:#ff8a80;--bad-bg:#3a1a17;--ok:#6fd38f;--chip:#2a2926}}
15
+ *{box-sizing:border-box}
16
+ body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
17
+ header{position:sticky;top:0;z-index:2;background:var(--panel);border-bottom:1px solid var(--line);padding:10px 16px;display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center}
18
+ header h1{font-size:17px;margin:0;font-weight:650}
19
+ #status{color:var(--muted);font-size:13px}
20
+ #searchForm{flex:1 1 260px;display:flex;gap:6px;min-width:0}
21
+ input,select,button,textarea{font:inherit;color:inherit}
22
+ input[type=search],input[type=text],select{background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:7px 10px;min-width:0}
23
+ #q{flex:1}
24
+ button{background:var(--chip);border:1px solid var(--line);border-radius:8px;padding:6px 11px;cursor:pointer}
25
+ button:hover{border-color:var(--muted)}
26
+ button.primary{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}
27
+ button.danger{color:var(--bad)}
28
+ button:focus-visible,input:focus-visible,select:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
29
+ nav{display:flex;gap:4px;padding:8px 16px;border-bottom:1px solid var(--line);background:var(--panel);overflow-x:auto}
30
+ nav button{border:none;background:none;border-radius:6px}
31
+ nav button[aria-selected=true]{background:var(--chip);font-weight:600}
32
+ main{display:grid;grid-template-columns:minmax(260px,360px) 1fr;gap:16px;padding:16px;max-width:1400px;margin:0 auto}
33
+ @media (max-width:760px){main{grid-template-columns:1fr;padding:12px}}
34
+ .panel{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px;min-width:0}
35
+ .filters{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
36
+ .filters>*{flex:1 1 110px}
37
+ ul.list{list-style:none;margin:0;padding:0;max-height:70vh;overflow:auto}
38
+ @media (max-width:760px){ul.list{max-height:40vh}}
39
+ ul.list li{padding:8px;border-radius:8px;cursor:pointer;border:1px solid transparent}
40
+ ul.list li:hover{background:var(--chip)}
41
+ ul.list li[aria-current=true]{border-color:var(--accent)}
42
+ .name{font-weight:600;overflow-wrap:anywhere}
43
+ .sub{color:var(--muted);font-size:12.5px}
44
+ .chip{display:inline-block;background:var(--chip);border-radius:999px;padding:1px 8px;font-size:12px;margin:2px 4px 2px 0;color:var(--muted)}
45
+ .chip.stale,.chip.warn{background:var(--warn-bg);color:var(--warn)}
46
+ .chip.bad{background:var(--bad-bg);color:var(--bad)}
47
+ .chip.ok{color:var(--ok)}
48
+ h2{font-size:18px;margin:0 0 4px}
49
+ h3{font-size:14px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:18px 0 6px}
50
+ .summary{white-space:pre-wrap;border-left:3px solid var(--line);padding-left:10px;color:var(--muted)}
51
+ .entry{border-top:1px solid var(--line);padding:8px 0}
52
+ .entry .text{white-space:pre-wrap;overflow-wrap:anywhere;margin:4px 0}
53
+ .actions{display:flex;flex-wrap:wrap;gap:6px}
54
+ .actions button{font-size:13px;padding:3px 9px}
55
+ .empty{color:var(--muted);padding:12px 0}
56
+ #toast{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:var(--ink);color:var(--bg);padding:8px 14px;border-radius:8px;font-size:14px;display:none;max-width:90vw}
57
+ dialog{border:1px solid var(--line);border-radius:12px;background:var(--panel);color:var(--ink);max-width:520px;width:calc(100% - 32px)}
58
+ dialog::backdrop{background:rgba(0,0,0,.45)}
59
+ .hidden{display:none!important}
60
+ .wide{grid-column:1/-1}
61
+ `;
62
+ const SCRIPT = String.raw `
63
+ (() => {
64
+ 'use strict';
65
+ const KEY = 'crbro_view_token';
66
+ const m = /(?:^#|&)t=([0-9a-f]{64})(?:&|$)/.exec(location.hash);
67
+ if (m) { try { sessionStorage.setItem(KEY, m[1]); } catch (e) {} }
68
+ if (location.hash) history.replaceState(null, '', location.pathname);
69
+ let token = null;
70
+ try { token = sessionStorage.getItem(KEY); } catch (e) {}
71
+
72
+ const $ = (id) => document.getElementById(id);
73
+ function h(tag, attrs, ...kids) {
74
+ const el = document.createElement(tag);
75
+ for (const [k, v] of Object.entries(attrs || {})) {
76
+ if (v === undefined || v === null || v === false) continue;
77
+ if (k === 'class') el.className = v;
78
+ else if (k.startsWith('on')) el.addEventListener(k.slice(2), v);
79
+ else el.setAttribute(k, v === true ? '' : String(v));
80
+ }
81
+ for (const kid of kids.flat()) if (kid !== null && kid !== undefined && kid !== false) el.append(kid instanceof Node ? kid : String(kid));
82
+ return el;
83
+ }
84
+ const chip = (text, cls) => h('span', { class: 'chip' + (cls ? ' ' + cls : '') }, text);
85
+ const day = (s) => (typeof s === 'string' && s.length >= 10 ? s.slice(0, 10) : (s || ''));
86
+ function toast(msg) { const t = $('toast'); t.textContent = msg; t.style.display = 'block'; clearTimeout(toast.t); toast.t = setTimeout(() => { t.style.display = 'none'; }, 3500); }
87
+
88
+ async function api(path, body) {
89
+ const res = await fetch(path, {
90
+ method: body ? 'POST' : 'GET', cache: 'no-store', credentials: 'omit',
91
+ headers: Object.assign({ 'X-CRBRO-Token': token || '' }, body ? { 'Content-Type': 'application/json' } : {}),
92
+ body: body ? JSON.stringify(body) : undefined,
93
+ });
94
+ let data = null;
95
+ try { data = await res.json(); } catch (e) {}
96
+ if (!res.ok) throw new Error((data && data.error) || ('HTTP ' + res.status));
97
+ return data;
98
+ }
99
+
100
+ function confirmDialog(title, text, okLabel) {
101
+ return new Promise((resolve) => {
102
+ const d = $('confirm');
103
+ $('confirmTitle').textContent = title;
104
+ $('confirmText').textContent = text;
105
+ $('confirmOk').textContent = okLabel || 'Confirm';
106
+ const done = (v) => { d.close(); resolve(v); };
107
+ $('confirmOk').onclick = () => done(true);
108
+ $('confirmCancel').onclick = () => done(false);
109
+ d.oncancel = () => resolve(false);
110
+ d.showModal();
111
+ });
112
+ }
113
+
114
+ const state = { neurons: [], current: null, offset: 0, tab: 'neurons' };
115
+
116
+ function setTab(tab) {
117
+ state.tab = tab;
118
+ for (const b of document.querySelectorAll('nav button')) b.setAttribute('aria-selected', String(b.dataset.tab === tab));
119
+ for (const s of document.querySelectorAll('[data-view]')) s.classList.toggle('hidden', s.dataset.view !== tab);
120
+ if (tab === 'pending') loadPending();
121
+ if (tab === 'review') loadReview();
122
+ }
123
+
124
+ async function loadStatus() {
125
+ try {
126
+ const s = (await api('/api/status')).status || {};
127
+ $('status').textContent = 'v' + (s.crbro_version || '?') + ' · ' + (s.total_neurons ?? '?') + ' neurons · ' + (s.total_sessions ?? '?') + ' day logs';
128
+ } catch (e) { $('status').textContent = e.message; }
129
+ }
130
+
131
+ async function loadNeurons() {
132
+ $('neuronList').replaceChildren(h('li', { class: 'empty' }, 'Loading…'));
133
+ try {
134
+ const r = await api('/api/neurons');
135
+ state.neurons = r.neurons || [];
136
+ const domains = [...new Set(state.neurons.map(n => n.domain))].sort();
137
+ const sel = $('fDomain');
138
+ const keep = sel.value;
139
+ sel.replaceChildren(h('option', { value: '' }, 'All domains'), ...domains.map(d => h('option', { value: d }, d)));
140
+ sel.value = domains.includes(keep) ? keep : '';
141
+ renderNeurons();
142
+ } catch (e) { $('neuronList').replaceChildren(h('li', { class: 'empty' }, e.message)); }
143
+ }
144
+
145
+ function renderNeurons() {
146
+ const d = $('fDomain').value, t = $('fType').value, q = $('fText').value.trim().toLowerCase();
147
+ const rows = state.neurons.filter(n => (!d || n.domain === d) && (!t || n.type === t) &&
148
+ (!q || (n.name + ' ' + n.id).toLowerCase().includes(q)));
149
+ $('neuronCount').textContent = rows.length + ' of ' + state.neurons.length;
150
+ const list = $('neuronList');
151
+ if (!rows.length) { list.replaceChildren(h('li', { class: 'empty' }, 'No neuron matches.')); return; }
152
+ list.replaceChildren(...rows.slice(0, 1000).map(n => h('li', {
153
+ tabindex: '0', 'aria-current': String(state.current === n.id),
154
+ onclick: () => openNeuron(n.id), onkeydown: (ev) => { if (ev.key === 'Enter') openNeuron(n.id); },
155
+ }, h('div', { class: 'name' }, n.name), h('div', { class: 'sub' }, n.type + ' · ' + n.domain + ' · heat ' + Number(n.heat || 0).toFixed(2) + ' · ' + (n.facts_count ?? 0) + ' facts'))));
156
+ }
157
+
158
+ const KIND_TITLES = { map: 'Map', error: 'Errors', debt: 'Debts', preference: 'Preferences', pattern: 'Patterns', decision: 'Decisions', fact: 'Facts' };
159
+
160
+ function freshnessChips(e) {
161
+ const out = [];
162
+ if (e.status) out.push(chip(e.status, 'bad'));
163
+ if (e.stale_days !== undefined) out.push(chip('possibly stale · ' + e.stale_days + ' d', 'stale'));
164
+ else if (e.freshness && e.freshness.state && e.freshness.state !== 'unverified') out.push(chip(e.freshness.state.replace(/_/g, ' ')));
165
+ const v = e.verification;
166
+ if (v && v.method && v.method !== 'none') out.push(chip('verified: ' + v.method + (v.at ? ' ' + day(v.at) : ''), 'ok'));
167
+ else if (e.verified) out.push(chip('verified ' + day(e.verified), 'ok'));
168
+ else out.push(chip('unverified'));
169
+ if (e.temporal === 'historical') out.push(chip('historical'));
170
+ if (e.shelf_life) out.push(chip('shelf: ' + e.shelf_life));
171
+ return out;
172
+ }
173
+
174
+ function entryActions(neuronId, e, after) {
175
+ const acts = [];
176
+ if (e.kind === 'map') return acts;
177
+ if (!e.status && ['fact', 'decision', 'pattern'].includes(e.kind)) {
178
+ acts.push(h('button', { onclick: async () => {
179
+ try { await api('/api/verify', { neuron: neuronId, id: e.id, kind: e.kind }); toast('Marked as checked and still true.'); after(); } catch (err) { toast(err.message); }
180
+ } }, 'Verify'));
181
+ }
182
+ if (!e.status) {
183
+ acts.push(h('button', { onclick: async () => {
184
+ if (!(await confirmDialog('Retire this entry?', 'It stays in the file but leaves recall (reversible with crbro_revise status=active).', 'Retire'))) return;
185
+ try { await api('/api/retire', { neuron: neuronId, id: e.id, kind: e.kind }); toast('Retired.'); after(); } catch (err) { toast(err.message); }
186
+ } }, 'Retire'));
187
+ }
188
+ acts.push(h('button', { class: 'danger', onclick: async () => {
189
+ if (!(await confirmDialog('Remove this entry for good?', 'It is deleted from the neuron after a quarantine copy (crbro_forget). This is for what must not exist on disk at all; to mark something as no longer true, use Retire.', 'Remove'))) return;
190
+ try { await api('/api/remove', { neuron: neuronId, id: e.id }); toast('Removed (a copy is in quarantine).'); after(); } catch (err) { toast(err.message); }
191
+ } }, 'Remove'));
192
+ return acts;
193
+ }
194
+
195
+ function renderEntry(neuronId, e, after) {
196
+ const text = h('div', { class: 'text' }, e.preview ?? e.text ?? '');
197
+ const more = (e.chars && e.preview && e.chars > e.preview.length - 1) ? h('button', { onclick: async () => {
198
+ try {
199
+ const r = await api('/api/entry?neuron=' + encodeURIComponent(neuronId) + '&id=' + encodeURIComponent(e.id));
200
+ text.textContent = r.entry ? (r.entry.text + (r.entry.rationale ? '\n\nWhy: ' + r.entry.rationale : '')) : text.textContent;
201
+ more.remove();
202
+ } catch (err) { toast(err.message); }
203
+ } }, 'Show full') : null;
204
+ return h('div', { class: 'entry' },
205
+ h('div', { class: 'sub' }, (e.kind !== 'map' ? 'id ' + e.id + ' · ' : '') + (day(e.added) || 'undated')),
206
+ h('div', {}, ...freshnessChips(e)),
207
+ text,
208
+ h('div', { class: 'actions' }, more, ...entryActions(neuronId, e, after)));
209
+ }
210
+
211
+ async function openNeuron(id, keepOffset) {
212
+ state.current = id;
213
+ if (!keepOffset) state.offset = 0;
214
+ renderNeurons();
215
+ const box = $('detail');
216
+ box.replaceChildren(h('div', { class: 'empty' }, 'Loading…'));
217
+ try {
218
+ const r = await api('/api/neuron?id=' + encodeURIComponent(id) + '&offset=' + state.offset + ($('fRetired').checked ? '&retired=1' : ''));
219
+ const n = r.neuron;
220
+ const reload = () => openNeuron(id, true);
221
+ const groups = {};
222
+ for (const e of n.entries || []) (groups[e.kind] = groups[e.kind] || []).push(e);
223
+ const pag = n.entries_pagination || {};
224
+ box.replaceChildren(
225
+ h('h2', {}, n.name),
226
+ h('div', { class: 'sub' }, n.id + ' · ' + n.type + ' · ' + n.domain + ' · heat ' + Number(n.heat || 0).toFixed(2) + ' · created ' + day(n.created)),
227
+ n.tags && n.tags.length ? h('div', {}, ...n.tags.map(t => chip(t))) : null,
228
+ n.summary ? h('p', { class: 'summary' }, n.summary) : null,
229
+ n.connections && n.connections.length ? h('div', { class: 'sub' }, 'Connected to: ', ...n.connections.slice(0, 30).map(c => h('button', { onclick: () => openNeuron(c.target_id) }, c.target_name || c.target_id))) : null,
230
+ ...['map', 'error', 'debt', 'preference', 'pattern', 'decision', 'fact'].filter(k => groups[k]).flatMap(k => [
231
+ h('h3', {}, KIND_TITLES[k] + ' (' + groups[k].length + (n.counts && n.counts[k] > groups[k].length ? ' of ' + n.counts[k] : '') + ')'),
232
+ ...groups[k].map(e => renderEntry(n.id, e, reload)),
233
+ ]),
234
+ (n.entries || []).length ? null : h('div', { class: 'empty' }, 'No live entries.'),
235
+ h('div', { class: 'actions' },
236
+ state.offset > 0 ? h('button', { onclick: () => { state.offset = Math.max(0, state.offset - 50); openNeuron(id, true); } }, 'Previous') : null,
237
+ pag.has_more ? h('button', { onclick: () => { state.offset += (n.entries || []).length; openNeuron(id, true); } }, 'Next (' + (pag.total - state.offset - (n.entries || []).length) + ' more)') : null),
238
+ );
239
+ if (window.matchMedia('(max-width:760px)').matches) box.scrollIntoView({ behavior: 'smooth' });
240
+ } catch (e) { box.replaceChildren(h('div', { class: 'empty' }, e.message)); }
241
+ }
242
+
243
+ async function search(ev) {
244
+ ev.preventDefault();
245
+ const q = $('q').value.trim();
246
+ if (!q) return;
247
+ setTab('search');
248
+ const box = $('searchResults');
249
+ box.replaceChildren(h('div', { class: 'empty' }, 'Searching…'));
250
+ try {
251
+ const r = await api('/api/search?q=' + encodeURIComponent(q));
252
+ const row = (x, stale) => h('div', { class: 'entry' },
253
+ h('div', {}, h('button', { onclick: () => { setTab('neurons'); openNeuron(x.neuron_id); } }, x.name || x.neuron_id), ' ',
254
+ x.matched_kind ? chip(x.matched_kind) : null, x.confidence ? chip(x.confidence, x.confidence === 'weak' ? 'warn' : 'ok') : null,
255
+ stale ? chip('possibly stale', 'stale') : null, x.matched_added ? chip(day(x.matched_added)) : null),
256
+ h('div', { class: 'text' }, x.matching_content || x.last_known || ''));
257
+ const rows = (r.results || []).map(x => row(x, false));
258
+ const stale = (r.possibly_stale || []).map(x => row(x, true));
259
+ box.replaceChildren(h('h2', {}, 'Results for “' + q + '”'),
260
+ ...(rows.length ? rows : [h('div', { class: 'empty' }, r.no_answer ? 'Nothing really matches.' : 'No results.')]),
261
+ ...(stale.length ? [h('h3', {}, 'Possibly stale'), ...stale] : []));
262
+ } catch (e) { box.replaceChildren(h('div', { class: 'empty' }, e.message)); }
263
+ }
264
+
265
+ async function loadPending() {
266
+ const box = $('pendingBox');
267
+ box.replaceChildren(h('div', { class: 'empty' }, 'Loading…'));
268
+ try {
269
+ const r = await api('/api/pending');
270
+ const open = (r.open || []);
271
+ const act = (t, action, label, ask) => h('button', { onclick: async () => {
272
+ if (ask && !(await confirmDialog('Discard this item?', 'It is dropped without being recorded as done.', 'Discard'))) return;
273
+ try { await api('/api/pending', { action, id: t.id }); toast(action === 'resolve' ? 'Closed.' : 'Discarded.'); loadPending(); } catch (e) { toast(e.message); }
274
+ } }, label);
275
+ box.replaceChildren(
276
+ h('h2', {}, 'Open items (' + open.length + ')'),
277
+ ...(open.length ? open.map(t => h('div', { class: 'entry' },
278
+ h('div', { class: 'sub' }, (t.id || '') + (t.added ? ' · added ' + day(t.added) : '') + (t.due ? ' · due ' + t.due : '') + (t.project ? ' · project ' + t.project : '')),
279
+ h('div', { class: 'text' }, t.text),
280
+ t.done_when ? h('div', { class: 'sub' }, 'Done when: ' + t.done_when) : null,
281
+ t.id ? h('div', { class: 'actions' }, act(t, 'resolve', 'Resolve'), act(t, 'discard', 'Discard', true)) : null)) : [h('div', { class: 'empty' }, 'Nothing open.')]),
282
+ ...((r.closed || []).length ? [h('h3', {}, 'Recently closed'), ...r.closed.map(t => h('div', { class: 'entry' }, h('div', { class: 'sub' }, 'closed ' + day(t.closed)), h('div', { class: 'text' }, t.text)))] : []));
283
+ } catch (e) { box.replaceChildren(h('div', { class: 'empty' }, e.message)); }
284
+ }
285
+
286
+ async function loadReview() {
287
+ const box = $('reviewBox');
288
+ box.replaceChildren(h('div', { class: 'empty' }, 'Checking the brain (read-only)…'));
289
+ try {
290
+ const r = await api('/api/stale');
291
+ const row = (s, label) => h('div', { class: 'entry' },
292
+ h('div', {}, h('button', { onclick: () => { setTab('neurons'); openNeuron(s.neuron_id); } }, s.neuron_id), ' ', chip(s.kind), chip(label, 'stale'),
293
+ s.last_verified ? chip('last checked ' + day(s.last_verified)) : null),
294
+ h('div', { class: 'text' }, s.preview || s.text || ''),
295
+ h('div', { class: 'actions' }, ...entryActions(s.neuron_id, { id: s.entry_id, kind: s.kind }, loadReview)));
296
+ const stale = r.stale || [], expired = r.expired || [];
297
+ box.replaceChildren(
298
+ h('h2', {}, 'Possibly stale (' + (r.stale_count ?? stale.length) + ')'),
299
+ h('p', { class: 'sub' }, 'Past their shelf life since last checked; the most overdue are listed. Verify what still holds, retire what does not.'),
300
+ ...(stale.length ? stale.map(s => row(s, (s.age_days ?? '?') + ' d old')) : [h('div', { class: 'empty' }, 'Nothing past its shelf life.')]),
301
+ h('h3', {}, 'Named a date that has passed (' + (r.expired_count ?? expired.length) + ')'),
302
+ ...(expired.length ? expired.map(s => row(s, 'date passed')) : [h('div', { class: 'empty' }, 'None.')]));
303
+ } catch (e) { box.replaceChildren(h('div', { class: 'empty' }, e.message)); }
304
+ }
305
+
306
+ if (!token) {
307
+ document.body.replaceChildren(h('main', {}, h('div', { class: 'panel' }, h('h2', {}, 'No access token'),
308
+ h('p', {}, 'Open the link that crbro-memory view printed in the terminal. The token lives only in this tab.'))));
309
+ return;
310
+ }
311
+ for (const b of document.querySelectorAll('nav button')) b.addEventListener('click', () => setTab(b.dataset.tab));
312
+ $('searchForm').addEventListener('submit', search);
313
+ for (const id of ['fDomain', 'fType']) $(id).addEventListener('change', renderNeurons);
314
+ $('fText').addEventListener('input', renderNeurons);
315
+ $('fRetired').addEventListener('change', () => { if (state.current) openNeuron(state.current); });
316
+ loadStatus();
317
+ loadNeurons();
318
+ })();
319
+ `;
320
+ const BODY = `
321
+ <header>
322
+ <h1>CRBRO memory</h1>
323
+ <span id="status">…</span>
324
+ <form id="searchForm" role="search"><input id="q" type="search" placeholder="Search the memory (crbro_recall)" aria-label="Search"><button class="primary" type="submit">Search</button></form>
325
+ </header>
326
+ <nav role="tablist">
327
+ <button role="tab" data-tab="neurons" aria-selected="true">Neurons</button>
328
+ <button role="tab" data-tab="search" aria-selected="false">Search</button>
329
+ <button role="tab" data-tab="pending" aria-selected="false">Open items</button>
330
+ <button role="tab" data-tab="review" aria-selected="false">Needs review</button>
331
+ </nav>
332
+ <main data-view="neurons">
333
+ <section class="panel" aria-label="Neurons">
334
+ <div class="filters">
335
+ <select id="fDomain" aria-label="Domain"><option value="">All domains</option></select>
336
+ <select id="fType" aria-label="Type"><option value="">All types</option><option>project</option><option>tech</option><option>lang</option><option>person</option><option>domain</option><option>process</option><option>protocol</option></select>
337
+ <input id="fText" type="text" placeholder="Filter by name" aria-label="Filter by name">
338
+ </div>
339
+ <div class="sub"><span id="neuronCount"></span> · <label><input id="fRetired" type="checkbox"> show retired entries</label></div>
340
+ <ul id="neuronList" class="list"></ul>
341
+ </section>
342
+ <section class="panel" id="detail" aria-live="polite"><div class="empty">Pick a neuron to see its entries.</div></section>
343
+ </main>
344
+ <main data-view="search" class="hidden"><section id="searchResults" class="panel wide"></section></main>
345
+ <main data-view="pending" class="hidden"><section id="pendingBox" class="panel wide"></section></main>
346
+ <main data-view="review" class="hidden"><section id="reviewBox" class="panel wide"></section></main>
347
+ <dialog id="confirm"><h2 id="confirmTitle"></h2><p id="confirmText"></p><div class="actions"><button id="confirmCancel" type="button">Cancel</button><button id="confirmOk" class="primary" type="button">Confirm</button></div></dialog>
348
+ <div id="toast" role="status"></div>
349
+ `;
350
+ /** The page, with `nonce` on its one style and one script. */
351
+ function viewerPage(nonce) {
352
+ return `<!doctype html>
353
+ <html lang="en">
354
+ <head>
355
+ <meta charset="utf-8">
356
+ <meta name="viewport" content="width=device-width,initial-scale=1">
357
+ <meta name="referrer" content="no-referrer">
358
+ <title>CRBRO memory</title>
359
+ <style nonce="${nonce}">${STYLE}</style>
360
+ </head>
361
+ <body>${BODY}<script nonce="${nonce}">${SCRIPT}</script>
362
+ </body>
363
+ </html>`;
364
+ }
365
+ /** The CSP the page is served with: nothing external, nothing inline without the nonce, no framing, no forms. */
366
+ function viewerCsp(nonce) {
367
+ return [
368
+ "default-src 'none'",
369
+ `script-src 'nonce-${nonce}'`,
370
+ `style-src 'nonce-${nonce}'`,
371
+ "connect-src 'self'",
372
+ "img-src 'none'",
373
+ "base-uri 'none'",
374
+ "form-action 'none'",
375
+ "frame-ancestors 'none'",
376
+ ].join('; ');
377
+ }
378
+ //# sourceMappingURL=viewer-page.js.map
@@ -32,7 +32,16 @@ const cortex_js_1 = require("../engine/cortex.js");
32
32
  const lines_js_1 = require("./lines.js");
33
33
  const proxy_js_1 = require("./proxy.js");
34
34
  const endpoint_js_1 = require("./endpoint.js");
35
+ const brain_js_1 = require("../engine/brain.js");
36
+ const trace_js_1 = require("./trace.js");
37
+ /** Until a connection has proved itself. After that a quiet connection costs nothing and is never cut for being quiet. */
35
38
  const HANDSHAKE_TIMEOUT_MS = 5_000;
39
+ /** With nobody arriving, the index warms up this long after listening; the first handshake through starts it sooner. */
40
+ const WARMUP_DELAY_MS = 1_000;
41
+ /** Stopping: how long calls already running may take to finish before their connections are cut anyway. */
42
+ const STOP_DRAIN_MS = 10_000;
43
+ /** A conversation that comes back after its backend was lost: how long its first message may wait for the index to catch up with the disk. */
44
+ const RESUME_REFRESH_MS = 3_000;
36
45
  const DEFAULT_IDLE_MIN = 20;
37
46
  /** setTimeout takes a 32-bit delay: past this it fires at once, and a daemon asked to wait a month exited in a millisecond. */
38
47
  const MAX_TIMER_MS = 2_147_000_000;
@@ -136,24 +145,61 @@ async function startDaemon(options = {}) {
136
145
  if (++refusals <= REFUSALS_PER_MINUTE)
137
146
  log(`connection refused: ${why}`);
138
147
  };
148
+ // ── The index warms up off every handshake's path (A7) ──
149
+ // Loading it parses tens of MB synchronously, and a handshake that arrives
150
+ // meanwhile waits for the parse. So it starts only when no handshake is under
151
+ // way: as soon as the first one is through, or a moment after listening if
152
+ // nobody comes.
153
+ let handshakes = 0;
154
+ let warmDue = false;
155
+ let warmStarted = false;
156
+ let warmDone = false;
157
+ const warmUp = () => {
158
+ if (!warmDue || warmStarted || handshakes > 0 || stopping)
159
+ return;
160
+ warmStarted = true;
161
+ (0, trace_js_1.trace)('index warm-up started');
162
+ // The index and the model warm up now, off anybody's critical path: the
163
+ // first client to boot finds them ready, the second never waits at all.
164
+ void engines.searchEngine.init()
165
+ .then(() => { warmDone = true; (0, trace_js_1.trace)('engine ready', 'index loaded', true); })
166
+ .catch(err => log(`index warm-up failed: ${err.message}`));
167
+ };
139
168
  const listener = node_net_1.default.createServer({ allowHalfOpen: false }, socket => { accept(socket); });
140
169
  function accept(socket) {
141
- const entry = { socket, server: null };
170
+ const entry = { socket, server: null, transport: null };
142
171
  clients.add(entry);
143
172
  socket.setNoDelay(true);
144
173
  let stage = 'hello';
145
174
  let nonce = '';
146
175
  let resumed = false;
147
176
  let transport = null;
177
+ handshakes++;
178
+ let counted = true;
179
+ const handshakeOver = () => {
180
+ if (!counted)
181
+ return;
182
+ counted = false;
183
+ handshakes--;
184
+ warmUp();
185
+ };
148
186
  const kill = (why) => {
149
187
  if (stage === 'dead')
150
188
  return;
151
189
  stage = 'dead';
190
+ handshakeOver();
152
191
  logRefusal(why);
153
192
  socket.destroy();
154
193
  };
155
- const timer = setTimeout(() => { if (stage !== 'mcp')
156
- kill('handshake timeout'); }, HANDSHAKE_TIMEOUT_MS);
194
+ // Only until the connection proves itself (stage 'auth' passed): a proxy
195
+ // whose client took 17 s to send initialize was cut every 5 s, three times,
196
+ // and then served itself in-process for good. The verdict waits one turn of
197
+ // the loop, so bytes that arrived while the loop was busy are read before
198
+ // "too slow" is believed.
199
+ const timer = setTimeout(() => setImmediate(() => {
200
+ if (stage === 'hello' || stage === 'auth')
201
+ kill('handshake timeout');
202
+ }), HANDSHAKE_TIMEOUT_MS);
157
203
  timer.unref();
158
204
  const say = (o) => { if (!socket.destroyed)
159
205
  socket.write(`${JSON.stringify(o)}\n`); };
@@ -184,8 +230,12 @@ async function startDaemon(options = {}) {
184
230
  if (m?.crbro !== 'auth' || !(0, endpoint_js_1.sameProof)(String(m.proof || ''), (0, endpoint_js_1.proof)(token, 'client', nonce)))
185
231
  return kill('bad proof');
186
232
  resumed = m.resumed === true;
233
+ clearTimeout(timer);
187
234
  say({ crbro: 'ready' });
188
235
  stage = 'first';
236
+ warmDue = true;
237
+ handshakeOver();
238
+ (0, trace_js_1.trace)('first client authenticated', '', true);
189
239
  return;
190
240
  }
191
241
  // stage === 'first': a control message, or the first MCP message.
@@ -217,15 +267,27 @@ async function startDaemon(options = {}) {
217
267
  transport = new lines_js_1.SocketServerTransport(socket, fn => cortex_js_1.sessionScope.run(scope, fn));
218
268
  const server = (0, server_js_1.createServer)(engines);
219
269
  entry.server = server;
270
+ entry.transport = transport;
220
271
  stage = 'mcp';
221
272
  armIdle();
222
273
  transport.receive(line); // queued: an `initialized` arriving in the same chunk must not overtake `initialize`
223
- server.connect(transport).then(() => transport.open()).catch(err => kill(`server.connect failed: ${err.message}`));
274
+ // A resumed conversation was served elsewhere meanwhile (an in-process
275
+ // fallback, a daemon that died): what it wrote there is on disk, maybe not
276
+ // in this index. Catch up before its first message — briefly, and only
277
+ // when the index is loaded (a load still under way reads the disk anyway).
278
+ const catchUp = resumed && warmDone
279
+ ? Promise.race([engines.searchEngine.refresh().catch(() => undefined), new Promise(r => setTimeout(r, RESUME_REFRESH_MS))])
280
+ : null;
281
+ server.connect(transport)
282
+ .then(() => catchUp)
283
+ .then(() => transport.open())
284
+ .catch(err => kill(`server.connect failed: ${err.message}`));
224
285
  }, () => kill('line too long'));
225
286
  socket.on('data', chunk => reader.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk));
226
287
  socket.on('error', () => { });
227
288
  socket.on('close', () => {
228
289
  clearTimeout(timer);
290
+ handshakeOver();
229
291
  const wasConversation = entry.server !== null;
230
292
  clients.delete(entry);
231
293
  if (entry.server)
@@ -307,15 +369,29 @@ async function startDaemon(options = {}) {
307
369
  protocol: endpoint_js_1.DAEMON_PROTOCOL, pid: process.pid, version, build, endpoint, token,
308
370
  started: new Date().toISOString(), brain: brainRoot,
309
371
  };
310
- await (0, endpoint_js_1.writeState)(brainRoot, state);
372
+ try {
373
+ await (0, endpoint_js_1.writeState)(brainRoot, state);
374
+ }
375
+ catch (err) {
376
+ // Without its descriptor nobody can reach this daemon. Say so where it
377
+ // will be read — the log, and the marker the waiting proxy checks — and
378
+ // give the endpoint back, instead of leaving the proxy 12 s of silence.
379
+ listener.close();
380
+ await blocked(`could not write its descriptor: ${err.message}`);
381
+ }
311
382
  }
312
383
  finally {
313
384
  await release();
314
385
  }
315
386
  log(`listening on ${endpoint} · ${state.version} (${build}) · brain ${brainRoot} · config ${config} · idle exit ${Math.round(idleMs / 60_000)} min`);
316
- // The index and the model warm up now, off anybody's critical path: the
317
- // first client to boot finds them ready, the second never waits at all.
318
- void engines.searchEngine.init().catch(err => log(`index warm-up failed: ${err.message}`));
387
+ (0, trace_js_1.trace)('daemon listening', build, true);
388
+ const warmTimer = setTimeout(() => { warmDue = true; warmUp(); }, WARMUP_DELAY_MS);
389
+ warmTimer.unref();
390
+ // What dead daemons left behind: descriptors nobody answers for, temporaries, stale locks.
391
+ void (0, endpoint_js_1.pruneDaemonDir)(brainRoot, { keep: build })
392
+ .then(r => { if (r.removed.length)
393
+ log(`removed from .daemon: ${r.removed.join(', ')}`); })
394
+ .catch(() => undefined);
319
395
  listener.on('error', err => log(`listener error: ${err.message}`));
320
396
  armIdle();
321
397
  function stop(reason = 'stop') {
@@ -325,10 +401,31 @@ async function startDaemon(options = {}) {
325
401
  log(`stopping: ${reason} · ${conversations()} conversation(s)`);
326
402
  if (idleTimer)
327
403
  clearTimeout(idleTimer);
404
+ clearTimeout(warmTimer);
328
405
  // The state file goes first: a client that arrives now must not find a token for a door that is closing.
329
406
  await (0, endpoint_js_1.removeState)(brainRoot, build, process.pid);
330
- await new Promise(resolve => { listener.close(() => resolve()); for (const c of clients)
331
- c.socket.destroy(); });
407
+ const listenerClosed = new Promise(resolve => listener.close(() => resolve()));
408
+ // Connections that are not conversations owe nobody anything.
409
+ for (const c of clients)
410
+ if (!c.transport)
411
+ c.socket.destroy();
412
+ // Calls already running finish (a crbro_learn writes neuron, synapses and
413
+ // manifest: cutting it between them is a half-made write). New calls are
414
+ // refused as never started, and the proxy sends them to whoever is next.
415
+ const running = [...clients].reduce((n, c) => n + (c.transport?.inFlight() ?? 0), 0);
416
+ if (running > 0)
417
+ log(`stopping: waiting for ${running} call(s) to finish`);
418
+ let drainTimer;
419
+ const outcome = await Promise.race([
420
+ Promise.all([...clients].map(c => c.transport?.drain() ?? Promise.resolve())).then(() => 'drained'),
421
+ new Promise(r => { drainTimer = setTimeout(() => r('timeout'), STOP_DRAIN_MS); }),
422
+ ]);
423
+ clearTimeout(drainTimer);
424
+ if (outcome === 'timeout')
425
+ log(`stopping: calls still running after ${STOP_DRAIN_MS / 1000} s were cut`);
426
+ for (const c of clients)
427
+ c.socket.destroy();
428
+ await listenerClosed;
332
429
  try {
333
430
  await engines.searchEngine.flush();
334
431
  }
@@ -361,6 +458,8 @@ async function runDaemon() {
361
458
  process.chdir(node_path_1.default.parse(process.cwd()).root);
362
459
  }
363
460
  catch { /* stay where we are */ }
461
+ const brainRoot = (0, brain_js_1.resolveBrainDir)();
462
+ (0, trace_js_1.traceStart)(brainRoot, 'daemon');
364
463
  let handle;
365
464
  try {
366
465
  handle = await startDaemon();
@@ -372,7 +471,16 @@ async function runDaemon() {
372
471
  process.exit(0);
373
472
  if (msg.startsWith('DAEMON_ENDPOINT_BLOCKED'))
374
473
  process.exit(3);
375
- throw err;
474
+ // Anything else: this process has no stderr anybody reads. Leave the
475
+ // reason in daemon.log, and the marker that ends the proxy's wait at once.
476
+ try {
477
+ const log = await fileLogger(brainRoot);
478
+ log(`could not start: ${err.stack || msg}`);
479
+ await (0, endpoint_js_1.markBlocked)(brainRoot, (0, endpoint_js_1.buildId)(), `the daemon could not start: ${msg}`);
480
+ await new Promise(r => setTimeout(r, 100)); // the log line is queued: let it land
481
+ }
482
+ catch { /* nothing more we can do */ }
483
+ process.exit(1);
376
484
  }
377
485
  const bye = (reason) => { void handle.stop(reason).then(() => process.exit(0)); };
378
486
  process.on('SIGINT', () => bye('SIGINT'));