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.
- package/LICENSE +21 -21
- package/README.md +256 -456
- package/SECURITY.md +135 -8
- package/bin/crbro.mjs +555 -299
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +143 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +762 -311
- package/dist/engine/doctor.js +676 -0
- package/dist/engine/evidence.js +171 -0
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +50 -36
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +735 -144
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +126 -69
- package/dist/engine/projects.js +80 -0
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +34 -11
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +810 -176
- package/dist/search/quality.js +31 -0
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +649 -343
- package/dist/sync/git.js +203 -72
- package/dist/sync/materialize.js +33 -8
- package/dist/sync/space.js +286 -101
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +219 -52
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +12 -8
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -670
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -61
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -156
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -323
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -35
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -142
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -116
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -346
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -11
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- 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
|
package/dist/daemon/daemon.js
CHANGED
|
@@ -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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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'));
|