crbro-memory 2.0.3 → 2.1.1
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/README.md +3 -2
- package/dist/search/index.d.ts +19 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +55 -6
- package/dist/search/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +251 -39
- package/dist/server.js.map +1 -1
- package/dist/types/index.d.ts +10 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -32,11 +32,15 @@ const prefrontal_js_1 = require("./engine/prefrontal.js");
|
|
|
32
32
|
const index_js_1 = require("./search/index.js");
|
|
33
33
|
const semantic_js_1 = require("./search/semantic.js");
|
|
34
34
|
const budget_js_1 = require("./utils/budget.js");
|
|
35
|
+
const secrets_js_1 = require("./engine/secrets.js");
|
|
36
|
+
/** Listings carry the day, not the millisecond: "2026-09-07" says what "2026-09-07T14:02:11.483Z" says, in a third of the tokens. Full stamps stay on single-entry reads. */
|
|
37
|
+
const dia = (iso) => (typeof iso === 'string' && iso.length >= 10 ? iso.slice(0, 10) : iso);
|
|
35
38
|
const maintenance_js_1 = require("./engine/maintenance.js");
|
|
36
39
|
const space_js_1 = require("./sync/space.js");
|
|
37
40
|
const keychain_js_1 = require("./engine/keychain.js");
|
|
38
41
|
const fs_js_1 = require("./utils/fs.js");
|
|
39
42
|
const hash_js_1 = require("./utils/hash.js");
|
|
43
|
+
const ops_js_1 = require("./sync/ops.js");
|
|
40
44
|
/**
|
|
41
45
|
* Old tool name → how to do the same thing on the 2.0 surface. Served by
|
|
42
46
|
* crbro_boot as `retired_tools` for the whole 2.x line, and asserted by the
|
|
@@ -78,7 +82,7 @@ function textResult(text, isError = false) {
|
|
|
78
82
|
return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) };
|
|
79
83
|
}
|
|
80
84
|
function jsonResult(payload) {
|
|
81
|
-
return { content: [{ type: 'text', text: JSON.stringify(payload
|
|
85
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload) }] };
|
|
82
86
|
}
|
|
83
87
|
function errorResult(where, err) {
|
|
84
88
|
return textResult(`CRBRO ${where} error: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
@@ -86,9 +90,18 @@ function errorResult(where, err) {
|
|
|
86
90
|
const NEURON_TYPES = ['project', 'tech', 'lang', 'person', 'domain', 'process', 'protocol'];
|
|
87
91
|
const INSPECT_VIEWS = ['status', 'neuron', 'neurons', 'sessions', 'global_map'];
|
|
88
92
|
function createServer() {
|
|
93
|
+
// Served at initialize for the clients that read it (Claude Desktop does
|
|
94
|
+
// not, as of anthropics/claude-code#43749; there the tool descriptions
|
|
95
|
+
// carry the same message). It is the one place to say how the memory is
|
|
96
|
+
// meant to be used, before any tool is called.
|
|
89
97
|
const server = new mcp_js_1.McpServer({
|
|
90
98
|
name: 'crbro-memory',
|
|
91
99
|
version: runningVersion(),
|
|
100
|
+
}, {
|
|
101
|
+
instructions: 'CRBRO is this user\'s persistent memory, kept on their own machine. Start every conversation with crbro_boot: it loads what earlier sessions left — protocols to follow, open items, hot topics. ' +
|
|
102
|
+
'Before answering anything about the user, their projects, preferences, decisions or past work, call crbro_recall: the answer is usually stored, and making them repeat it is the failure this memory exists to prevent. ' +
|
|
103
|
+
'Questions about CRBRO itself (version, counts, whether semantic recall is on) are crbro_inspect view=status. Read one entry, not a whole neuron: view=neuron gives an index, entries=[ids] the text. ' +
|
|
104
|
+
'Save with crbro_learn as you go, and close with crbro_consolidate before the conversation ends.',
|
|
92
105
|
});
|
|
93
106
|
// ─── Initialize engines ──────────────────────────────────────
|
|
94
107
|
const brain = new brain_js_1.Brain();
|
|
@@ -138,7 +151,52 @@ function createServer() {
|
|
|
138
151
|
// The last three real session logs, and last_session taken from them:
|
|
139
152
|
// the manifest field brain.boot reads was never written by anything,
|
|
140
153
|
// so it came back null in every session.
|
|
141
|
-
|
|
154
|
+
// Headlines, not transcripts. Three full summaries were 82% of an
|
|
155
|
+
// 81,000-character boot on a mature brain — text the model had already
|
|
156
|
+
// read once, at consolidation, re-read at every start. The opening
|
|
157
|
+
// line is where a summary says what happened; the rest is a call away.
|
|
158
|
+
const recientes = await hippocampus.listSessions(3);
|
|
159
|
+
response.recent_sessions = recientes.map((s) => {
|
|
160
|
+
const texto = String(s.summary || '');
|
|
161
|
+
const titular = texto.length > 240 ? `${texto.slice(0, 240).trimEnd()}…` : texto;
|
|
162
|
+
const topics = s.topics_touched || [];
|
|
163
|
+
// Same key as before, `summary`, so nothing that reads it breaks; when
|
|
164
|
+
// it is only the opening, summary_truncated says so next to it.
|
|
165
|
+
return {
|
|
166
|
+
session_id: s.session_id, date: s.date, summary: titular,
|
|
167
|
+
...(texto.length > 240 ? { summary_truncated: true, summary_chars: texto.length } : {}),
|
|
168
|
+
topics_touched: topics.slice(0, 5),
|
|
169
|
+
...(topics.length > 5 ? { topics_count: topics.length } : {}),
|
|
170
|
+
key_facts_added: s.key_facts_added, decisions_made: s.decisions_made,
|
|
171
|
+
new_neurons_created: s.new_neurons_created, synapses_updated: s.synapses_updated,
|
|
172
|
+
duration_estimate: s.duration_estimate,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
if (recientes.some((s) => String(s.summary || '').length > 240)) {
|
|
176
|
+
response.sessions_note = 'recent_sessions carries the first 240 characters of each summary (summary_chars is the full size). crbro_inspect view=sessions limit=1 returns the latest one whole; limit=3 returns the three, each capped at 3,000 characters and declared.';
|
|
177
|
+
}
|
|
178
|
+
// Ten hot topics with the day, not twenty with the millisecond: each
|
|
179
|
+
// row is a pointer the model follows with recall, not a record.
|
|
180
|
+
response.hot_topics = (result.hot_topics || []).slice(0, 10)
|
|
181
|
+
.map((h) => ({ ...h, last_access: dia(h.last_access) }));
|
|
182
|
+
// active_context repeated open_items and recently_closed, which boot
|
|
183
|
+
// already serves at the top level: 1,414 characters said twice.
|
|
184
|
+
if (result.active_context && typeof result.active_context === 'object') {
|
|
185
|
+
const { pending_tasks: _p, recently_closed: _r, ...resto } = result.active_context;
|
|
186
|
+
response.active_context = resto;
|
|
187
|
+
}
|
|
188
|
+
for (const [k, cap] of [['open_items', 12], ['recently_closed', 8]]) {
|
|
189
|
+
const lista = response[k];
|
|
190
|
+
if (!Array.isArray(lista))
|
|
191
|
+
continue;
|
|
192
|
+
if (lista.length > cap)
|
|
193
|
+
response[`${k}_total`] = lista.length;
|
|
194
|
+
response[k] = lista.slice(0, cap).map((it) => ({
|
|
195
|
+
...it,
|
|
196
|
+
...(it.added ? { added: dia(it.added) } : {}),
|
|
197
|
+
...(it.closed ? { closed: dia(it.closed) } : {}),
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
142
200
|
response.last_session = response.recent_sessions[0]?.session_id ?? result.last_session ?? null;
|
|
143
201
|
response.retired_tools = exports.RETIRED_TOOLS;
|
|
144
202
|
// Inject protocol enforcement.
|
|
@@ -187,8 +245,9 @@ function createServer() {
|
|
|
187
245
|
response.memory_discipline =
|
|
188
246
|
'Before crbro_learn, crbro_recall: what you are about to save may already exist — then pass ' +
|
|
189
247
|
'supersedes instead of adding a sibling (two versions of one fact compete on recall as equals). ' +
|
|
190
|
-
'
|
|
191
|
-
'
|
|
248
|
+
'Read what you need, not the neuron: crbro_recall finds the entry by content; crbro_inspect view=neuron ' +
|
|
249
|
+
'gives an index (id, kind, preview per entry) and entries=[ids] returns just those in full. Ask for a ' +
|
|
250
|
+
'whole neuron only when you truly need all of it. ' +
|
|
192
251
|
'Structure — paths, what serves what, traps — goes in crbro_map, not in facts; anything derivable ' +
|
|
193
252
|
'from the repo or git history is not worth storing. Write facts dense and self-contained: they are ' +
|
|
194
253
|
'recalled without this conversation, and add keywords: the words a future question may use that ' +
|
|
@@ -197,7 +256,8 @@ function createServer() {
|
|
|
197
256
|
'deliberate deferral with its ceiling and revisit trigger. Credentials never go in the brain: ' +
|
|
198
257
|
'crbro_secret, then record only the NAME. Recall results carry confidence — "weak" means the match ' +
|
|
199
258
|
'covers little of the question, verify before relying on it — and when two facts disagree, prefer ' +
|
|
200
|
-
'the more recent.
|
|
259
|
+
'the more recent. Lifecycle: supersedes replaces, crbro_revise retires, crbro_forget removes what ' +
|
|
260
|
+
'must not exist on disk — each tool describes its own stage. ' +
|
|
201
261
|
'Call crbro_consolidate before the conversation ends; it logs the session too.';
|
|
202
262
|
// A mature brain outgrew the boot payload: on a 1,145-neuron brain it
|
|
203
263
|
// reached 81,406 characters (~20,000 tokens), three quarters of it the
|
|
@@ -219,17 +279,19 @@ function createServer() {
|
|
|
219
279
|
// ═══════════════════════════════════════════════════════════════
|
|
220
280
|
server.registerTool('crbro_inspect', {
|
|
221
281
|
title: 'Inspect the brain',
|
|
222
|
-
description: 'Read-only views of the brain by id or name; to search by content use crbro_recall. Nothing is written by any view: every read leaves the brain untouched. view=status: version, brain path, totals, last boot/consolidation, semantic
|
|
282
|
+
description: 'Read-only views of the brain by id or name; to search by content use crbro_recall. Nothing is written by any view: every read leaves the brain untouched. view=status answers any question about CRBRO itself: version, brain path, totals, last boot/consolidation, whether semantic recall is installed and on, hot_topics_recalculated. view=neuron: an index of one neuron — header, counts, connections (min_strength filters) and every entry as id, kind, date and preview, paged with limit/offset; entries=[ids or exact text] reads those in full, detail=full returns the whole neuron, shortened and declared when large. view=neurons: rows hottest first (id, name, domain, type, heat, last_accessed, facts_count), filtered by domain, type, min_heat, paged with limit/offset. view=sessions: day logs newest first, the only place session summaries are read. view=global_map: one cluster per domain plus cross-domain bridges, computed live. Params of other views are ignored.',
|
|
223
283
|
inputSchema: {
|
|
224
284
|
view: zod_1.z.enum(INSPECT_VIEWS).describe('Which read to perform. Only the params listed for that view are honoured; the rest are ignored, never an error.'),
|
|
225
285
|
neuron: zod_1.z.string().optional().describe('view=neuron only, required there: neuron id (e.g. "project_octochat") or name (e.g. "OctoChat").'),
|
|
286
|
+
detail: zod_1.z.enum(['index', 'full']).optional().describe('view=neuron: "index" (default) returns the header, counts and every entry as id, kind, date and a short preview — cheap, then read what matters with `entries`. "full" returns the whole neuron with facts paged by limit/offset; large neurons are shortened and say so.'),
|
|
287
|
+
entries: zod_1.z.array(zod_1.z.string()).optional().describe('view=neuron: read these entries in full — their ids from the index or from crbro_recall, or their exact text. Any kind: fact, decision, pattern, preference, error, debt, or "map" for the system map. Ignores detail.'),
|
|
226
288
|
domain: zod_1.z.string().optional().describe('view=neurons: exact domain match, e.g. "proyectos-web".'),
|
|
227
289
|
type: zod_1.z.enum(NEURON_TYPES).optional().describe('view=neurons: only this neuron type.'),
|
|
228
290
|
min_heat: zod_1.z.number().min(0).max(1).optional().describe('view=neurons: minimum heat, 0.0-1.0. Heat blends access frequency, recency and connectivity.'),
|
|
229
291
|
min_strength: zod_1.z.number().min(0).max(1).optional().describe('view=neuron: drop connections weaker than this (0.0-1.0). Omit or 0 = all.'),
|
|
230
|
-
limit: zod_1.z.number().int().positive().optional().describe('Page size. view=neuron:
|
|
231
|
-
offset: zod_1.z.number().int().min(0).optional().describe('Items to skip. view=neuron:
|
|
232
|
-
include_superseded: zod_1.z.boolean().optional().describe('view=neuron: also
|
|
292
|
+
limit: zod_1.z.number().int().positive().optional().describe('Page size. view=neuron: index entries per page (default 25, max 200); view=neurons: rows (default 50, max 500); view=sessions: day logs (default 10, max 100). Other views ignore it.'),
|
|
293
|
+
offset: zod_1.z.number().int().min(0).optional().describe('Items to skip. view=neuron: index entries; view=neurons: rows after the heat sort. Default 0.'),
|
|
294
|
+
include_superseded: zod_1.z.boolean().optional().describe('view=neuron: also list superseded and retracted entries of every kind (default false; the index reports how many are hidden in entries_pagination.hidden_retired). detail=full always returns entry_status.'),
|
|
233
295
|
},
|
|
234
296
|
outputSchema: {
|
|
235
297
|
view: zod_1.z.enum(INSPECT_VIEWS),
|
|
@@ -280,12 +342,13 @@ function createServer() {
|
|
|
280
342
|
// a view added later inherits it without remembering to. It has to be
|
|
281
343
|
// this strict because the payload travels TWICE — once as text, once as
|
|
282
344
|
// structuredContent — so a client pays double for what a view emits.
|
|
283
|
-
const done = (payload) => {
|
|
345
|
+
const done = (payload, extra = {}) => {
|
|
284
346
|
const fitted = (0, budget_js_1.fitToBudget)(payload, {
|
|
285
347
|
howToGetMore: `Ask for a slice instead of the whole: crbro_inspect view=${args.view} with limit and offset, or crbro_recall to search by content.`,
|
|
348
|
+
...extra,
|
|
286
349
|
});
|
|
287
350
|
return {
|
|
288
|
-
content: [{ type: 'text', text: JSON.stringify(fitted
|
|
351
|
+
content: [{ type: 'text', text: JSON.stringify(fitted) }],
|
|
289
352
|
structuredContent: { view: args.view, [args.view]: fitted },
|
|
290
353
|
};
|
|
291
354
|
};
|
|
@@ -323,29 +386,119 @@ function createServer() {
|
|
|
323
386
|
if (!neuron) {
|
|
324
387
|
return textResult(`Neuron not found: "${args.neuron}". Find the id with crbro_recall or crbro_inspect view=neurons.`, true);
|
|
325
388
|
}
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
389
|
+
// Three ways to read a neuron, cheapest first. The default is an
|
|
390
|
+
// INDEX: header, counts and one line per entry with a stable id. The
|
|
391
|
+
// biggest neuron on the reference brain serialises to ~530,000
|
|
392
|
+
// characters; no client carries that, and a model rarely needs it —
|
|
393
|
+
// it needs two or three entries it can now name. `entries` reads
|
|
394
|
+
// those in full; `detail=full` is the old dump, and it goes through
|
|
395
|
+
// the ceiling like every other read.
|
|
396
|
+
const limit = Math.min(Math.max(args.limit ?? 25, 1), 200);
|
|
330
397
|
const offset = Math.max(args.offset ?? 0, 0);
|
|
331
|
-
const visible = (neuron.facts || []).filter(f => args.include_superseded ? true : (f.status !== 'superseded' && f.status !== 'retracted'));
|
|
332
|
-
const ordered = [...visible].sort((a, b) => String(b.added || '').localeCompare(String(a.added || '')));
|
|
333
|
-
const page = ordered.slice(offset, offset + limit);
|
|
334
398
|
const connections = await synapses.getConnections(neuron.id, args.min_strength);
|
|
399
|
+
const retired = (id) => neuron.entry_status?.[id]?.status;
|
|
400
|
+
const rows = [];
|
|
401
|
+
for (const f of neuron.facts || []) {
|
|
402
|
+
rows.push({ id: f.id || (0, hash_js_1.factId)(f.text), kind: 'fact', text: f.text, added: f.added || '',
|
|
403
|
+
status: f.status, confidence: f.confidence, keys: f.keys, revision_note: f.revision_note, revised: f.revised });
|
|
404
|
+
}
|
|
405
|
+
for (const d of neuron.decisions || []) {
|
|
406
|
+
const id = d.id || (0, ops_js_1.entryId)(d.text);
|
|
407
|
+
rows.push({ id, kind: 'decision', text: d.text, added: d.date || '', rationale: d.rationale, status: retired(id), revised: neuron.entry_status?.[id]?.revised, revision_note: neuron.entry_status?.[id]?.note });
|
|
408
|
+
}
|
|
409
|
+
const sidecar = (kind, list) => {
|
|
410
|
+
for (const t of list || []) {
|
|
411
|
+
const id = (0, ops_js_1.entryId)(t);
|
|
412
|
+
rows.push({ id, kind, text: t, added: neuron.entry_dates?.[id] || '', status: retired(id), revised: neuron.entry_status?.[id]?.revised, revision_note: neuron.entry_status?.[id]?.note });
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
sidecar('pattern', neuron.patterns);
|
|
416
|
+
sidecar('preference', neuron.preferences);
|
|
417
|
+
sidecar('error', neuron.errors);
|
|
418
|
+
sidecar('debt', neuron.debts);
|
|
419
|
+
if (neuron.map?.text)
|
|
420
|
+
rows.push({ id: 'map', kind: 'map', text: neuron.map.text, added: neuron.map.updated || '' });
|
|
421
|
+
const header = {
|
|
422
|
+
id: neuron.id, name: neuron.name, domain: neuron.domain, type: neuron.type,
|
|
423
|
+
heat: neuron.heat, summary: neuron.summary, tags: neuron.tags,
|
|
424
|
+
created: neuron.created, last_accessed: neuron.last_accessed,
|
|
425
|
+
};
|
|
426
|
+
// ── entries=[…]: exactly what was asked for, in full ──────────
|
|
427
|
+
const wanted = (args.entries || []).map(s => s.trim()).filter(Boolean);
|
|
428
|
+
if (wanted.length) {
|
|
429
|
+
const found = [];
|
|
430
|
+
const missing = [];
|
|
431
|
+
for (const w of wanted) {
|
|
432
|
+
const hit = rows.find(r => r.id === w) || rows.find(r => r.text.trim().toLowerCase() === w.toLowerCase());
|
|
433
|
+
if (hit) {
|
|
434
|
+
if (!found.includes(hit))
|
|
435
|
+
found.push(hit);
|
|
436
|
+
}
|
|
437
|
+
else
|
|
438
|
+
missing.push(w);
|
|
439
|
+
}
|
|
440
|
+
return done({
|
|
441
|
+
...header,
|
|
442
|
+
entries: found,
|
|
443
|
+
returned: found.length,
|
|
444
|
+
...(missing.length ? { not_found: missing, hint: 'Ids come from the index (view=neuron) or from crbro_recall as entry_id; exact text also works.' } : {}),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
// ── detail=full: the whole neuron, facts paged, as before ──────
|
|
448
|
+
if (args.detail === 'full') {
|
|
449
|
+
const visible = (neuron.facts || []).filter(f => args.include_superseded ? true : (f.status !== 'superseded' && f.status !== 'retracted'));
|
|
450
|
+
const ordered = [...visible].sort((a, b) => String(b.added || '').localeCompare(String(a.added || '')));
|
|
451
|
+
const page = ordered.slice(offset, offset + limit);
|
|
452
|
+
return done({
|
|
453
|
+
...neuron,
|
|
454
|
+
connection_ids: neuron.connections || [],
|
|
455
|
+
connections,
|
|
456
|
+
total_connections: connections.length,
|
|
457
|
+
facts: page,
|
|
458
|
+
facts_pagination: {
|
|
459
|
+
total: ordered.length,
|
|
460
|
+
returned: page.length,
|
|
461
|
+
offset,
|
|
462
|
+
has_more: offset + page.length < ordered.length,
|
|
463
|
+
order: 'newest first',
|
|
464
|
+
hidden_superseded: (neuron.facts || []).length - visible.length,
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
// ── default: the index ────────────────────────────────────────
|
|
469
|
+
// A fact may carry status 'active' explicitly; only the two retired
|
|
470
|
+
// states hide an entry from the default page.
|
|
471
|
+
const isRetired = (s) => s === 'superseded' || s === 'retracted';
|
|
472
|
+
const live = rows.filter(r => args.include_superseded ? true : !isRetired(r.status));
|
|
473
|
+
const byKind = (k) => live.filter(r => r.kind === k)
|
|
474
|
+
.sort((a, b) => String(b.added).localeCompare(String(a.added)));
|
|
475
|
+
// Few and heavy first — map, errors, debts — then the many: facts are the
|
|
476
|
+
// long tail and get paged; the map is one entry and rarely fits a preview.
|
|
477
|
+
const ordered = ['map', 'error', 'debt', 'preference', 'pattern', 'decision', 'fact'].flatMap(byKind);
|
|
478
|
+
const page = ordered.slice(offset, offset + limit);
|
|
479
|
+
const PREVIEW = 160;
|
|
480
|
+
const counts = {};
|
|
481
|
+
for (const r of rows)
|
|
482
|
+
counts[r.kind] = (counts[r.kind] || 0) + 1;
|
|
335
483
|
return done({
|
|
336
|
-
...
|
|
337
|
-
|
|
484
|
+
...header,
|
|
485
|
+
counts: { ...counts, connections: connections.length, retired: rows.filter(r => isRetired(r.status)).length },
|
|
338
486
|
connections,
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
487
|
+
entries: page.map(r => ({
|
|
488
|
+
id: r.id, kind: r.kind, added: dia(r.added),
|
|
489
|
+
preview: r.text.length > PREVIEW ? `${r.text.slice(0, PREVIEW).trimEnd()}…` : r.text,
|
|
490
|
+
chars: r.text.length,
|
|
491
|
+
...(isRetired(r.status) ? { status: r.status, ...(r.revised ? { revised: dia(r.revised) } : {}), ...(r.revision_note ? { retired_note: r.revision_note } : {}) } : {}),
|
|
492
|
+
})),
|
|
493
|
+
entries_pagination: {
|
|
342
494
|
total: ordered.length,
|
|
343
495
|
returned: page.length,
|
|
344
496
|
offset,
|
|
345
497
|
has_more: offset + page.length < ordered.length,
|
|
346
|
-
order: 'newest first',
|
|
347
|
-
|
|
498
|
+
order: 'map, errors, debts, preferences, patterns, decisions, facts — newest first within each',
|
|
499
|
+
...(args.include_superseded ? {} : { hidden_retired: rows.length - live.length, see_retired: 'pass include_superseded=true' }),
|
|
348
500
|
},
|
|
501
|
+
how_to_read: `crbro_inspect view=neuron neuron="${neuron.id}" entries=[<ids>] returns those in full; detail=full returns everything.`,
|
|
349
502
|
});
|
|
350
503
|
}
|
|
351
504
|
if (args.view === 'neurons') {
|
|
@@ -364,9 +517,21 @@ function createServer() {
|
|
|
364
517
|
}
|
|
365
518
|
if (args.view === 'sessions') {
|
|
366
519
|
const limit = Math.min(Math.max(args.limit ?? 10, 1), 100);
|
|
367
|
-
const
|
|
520
|
+
const offset = Math.max(args.offset ?? 0, 0);
|
|
521
|
+
const pagina = (await hippocampus.listSessions(limit + offset)).slice(offset, offset + limit);
|
|
368
522
|
const totalSessions = (await brain.getManifest()).total_sessions;
|
|
369
|
-
|
|
523
|
+
// One log asked for by itself comes whole: it is the door boot points
|
|
524
|
+
// at when it shortens a summary. A list caps each one and says so.
|
|
525
|
+
const CAP = 3_000;
|
|
526
|
+
const sessions = limit === 1 ? pagina : pagina.map((s) => {
|
|
527
|
+
const t = String(s.summary || '');
|
|
528
|
+
return t.length > CAP
|
|
529
|
+
? { ...s, summary: `${t.slice(0, CAP).trimEnd()}…`, summary_truncated: true, summary_chars: t.length }
|
|
530
|
+
: s;
|
|
531
|
+
});
|
|
532
|
+
return done({ total: totalSessions, returned: sessions.length, offset, has_more: offset + sessions.length < totalSessions, sessions }, limit === 1
|
|
533
|
+
? { stringCap: budget_js_1.DEFAULT_BUDGET_CHARS - 2_000, howToGetMore: 'This is one full session log; a longer one is only readable from disk, in hippocampus/<session_id>.json.' }
|
|
534
|
+
: { howToGetMore: 'Page with limit and offset; limit=1 returns a single log whole.' });
|
|
370
535
|
}
|
|
371
536
|
// view === 'global_map': computed live, never cached, nothing written.
|
|
372
537
|
const globalMap = await prefrontal.getGlobalMap();
|
|
@@ -503,12 +668,12 @@ function createServer() {
|
|
|
503
668
|
// ═══════════════════════════════════════════════════════════════
|
|
504
669
|
server.registerTool('crbro_recall', {
|
|
505
670
|
title: 'Recall',
|
|
506
|
-
description: 'Read-only search of everything saved in earlier sessions —
|
|
671
|
+
description: 'Read-only search of everything saved in earlier sessions — facts, decisions, patterns, preferences, errors, debts and maps by their full text. Call it BEFORE answering anything about the user, their projects, preferences, decisions or past work: the answer is usually stored, and making them repeat it is the failure this memory exists to prevent. Also before crbro_learn, so a fact is superseded instead of duplicated. One result per neuron: the best matching entry with entry_id (read it whole with crbro_inspect view=neuron entries=[id]), matched_kind, matched_added, a confidence label (weak = little of the question covered; verify) and also_matched previews. Retired entries never surface. Five ranked results by default; matched_neurons says how many more matched. If nothing matches, retry with 2-4 phrasings in queries or fewer, distinctive words. has_map:true: read the system map with crbro_map before touching that system.',
|
|
507
672
|
inputSchema: {
|
|
508
673
|
query: zod_1.z.string().describe('What to look for, e.g. "Firebase authentication setup". Fewer, distinctive terms beat full sentences.'),
|
|
509
674
|
queries: zod_1.z.array(zod_1.z.string()).optional().describe('Alternative phrasings of the same question, searched together with query and fused by rank. Use synonyms, the other language and the concrete product name; 2-4 is plenty.'),
|
|
510
675
|
domain: zod_1.z.string().optional().describe('Only neurons in this domain (exact match, e.g. "proyectos-web").'),
|
|
511
|
-
limit: zod_1.z.number().optional().describe('Max neurons returned (default
|
|
676
|
+
limit: zod_1.z.number().int().positive().optional().describe('Max neurons returned (default 5, ranked; ask for more only when the top five did not answer).'),
|
|
512
677
|
},
|
|
513
678
|
outputSchema: {
|
|
514
679
|
query: zod_1.z.string(),
|
|
@@ -520,25 +685,55 @@ function createServer() {
|
|
|
520
685
|
heat: zod_1.z.number(), has_map: zod_1.z.boolean().optional(),
|
|
521
686
|
matched_terms: zod_1.z.number().optional(), query_terms: zod_1.z.number().optional(),
|
|
522
687
|
confidence: zod_1.z.enum(['strong', 'weak']).optional(),
|
|
523
|
-
|
|
688
|
+
entry_id: zod_1.z.string().optional(),
|
|
689
|
+
content_truncated: zod_1.z.boolean().optional(), content_chars: zod_1.z.number().optional(),
|
|
690
|
+
also_matched: zod_1.z.array(zod_1.z.object({ entry_id: zod_1.z.string().optional(), kind: zod_1.z.string(), added: zod_1.z.string(), preview: zod_1.z.string(), chars: zod_1.z.number() }).loose()).optional(),
|
|
524
691
|
}).loose()),
|
|
692
|
+
returned: zod_1.z.number().optional(),
|
|
693
|
+
matched_neurons: zod_1.z.number().optional().describe('Neurons with any hit before limit; total_results is what came back'),
|
|
694
|
+
has_more: zod_1.z.boolean().optional(),
|
|
525
695
|
hint: zod_1.z.string(),
|
|
526
696
|
truncated: zod_1.z.object({}).loose().optional(),
|
|
527
697
|
},
|
|
528
698
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
529
699
|
}, async (args) => {
|
|
530
700
|
try {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
701
|
+
// Five by default, down from ten: results are ranked and each one
|
|
702
|
+
// carries its entry, so ten cost ~12,000 tokens on a dense brain for
|
|
703
|
+
// answers that live in the top three (recall@3 is the metric).
|
|
704
|
+
const { results, matched_neurons } = await searchEngine.searchManyWithStats([args.query, ...(args.queries || [])], { domain: args.domain, limit: args.limit ?? 5 });
|
|
705
|
+
// A hit carries its entry, but not without limit: one 6,000-character
|
|
706
|
+
// fact must not cost more than the five results around it. Past the
|
|
707
|
+
// cap the opening comes back, declared, and entry_id reads the rest.
|
|
708
|
+
const CONTENT_CAP = 1_200;
|
|
709
|
+
let cortados = 0;
|
|
710
|
+
const rows = results.map(r => {
|
|
711
|
+
const mc = r.matching_content;
|
|
712
|
+
const largo = mc.length > CONTENT_CAP;
|
|
713
|
+
if (largo)
|
|
714
|
+
cortados++;
|
|
715
|
+
return {
|
|
716
|
+
...r,
|
|
717
|
+
matched_added: dia(r.matched_added),
|
|
718
|
+
...(largo ? { matching_content: `${mc.slice(0, CONTENT_CAP).trimEnd()}…`, content_truncated: true, content_chars: mc.length } : {}),
|
|
719
|
+
...(r.also_matched ? { also_matched: r.also_matched.map(a => ({ ...a, added: dia(a.added) })) } : {}),
|
|
720
|
+
};
|
|
534
721
|
});
|
|
722
|
+
const sobran = matched_neurons - results.length;
|
|
535
723
|
const payload = {
|
|
536
724
|
query: args.query,
|
|
725
|
+
// total_results is what came back (capped by limit); matched_neurons is
|
|
726
|
+
// how many neurons had a hit at all, so five never reads as "only five".
|
|
537
727
|
total_results: results.length,
|
|
538
|
-
results,
|
|
728
|
+
returned: results.length,
|
|
729
|
+
matched_neurons,
|
|
730
|
+
has_more: sobran > 0,
|
|
731
|
+
results: rows,
|
|
539
732
|
hint: results.length === 0
|
|
540
733
|
? 'Nothing matched. Try fewer, more distinctive words - names, ids, filenames - rather than a full sentence.'
|
|
541
|
-
: '
|
|
734
|
+
: 'weak: verify. Newer wins on conflict. entry_id → crbro_inspect view=neuron entries=[id]. has_map → crbro_map first.'
|
|
735
|
+
+ (sobran > 0 ? ` ${sobran} more neuron${sobran === 1 ? '' : 's'} matched: raise limit or narrow the query.` : '')
|
|
736
|
+
+ (cortados > 0 ? ' content_truncated → read the entry by entry_id.' : ''),
|
|
542
737
|
};
|
|
543
738
|
// Recall is the one read whose size the caller sets, with limit: ten
|
|
544
739
|
// results already cost ~12,000 tokens on a dense brain because each
|
|
@@ -547,7 +742,7 @@ function createServer() {
|
|
|
547
742
|
howToGetMore: 'Ask again with a smaller limit, or narrow the query — the matches are still there.',
|
|
548
743
|
});
|
|
549
744
|
return {
|
|
550
|
-
content: [{ type: 'text', text: JSON.stringify(fitted
|
|
745
|
+
content: [{ type: 'text', text: JSON.stringify(fitted) }],
|
|
551
746
|
structuredContent: fitted,
|
|
552
747
|
};
|
|
553
748
|
}
|
|
@@ -956,13 +1151,26 @@ function createServer() {
|
|
|
956
1151
|
title: 'Consolidate the session',
|
|
957
1152
|
description: 'Write: close the session — the only way to log a session. Call it before the conversation ends. Persists pending knowledge and index writes, logs the session from summary (credentials stripped, kinds in redacted), sets the context\'s last_session, recalculates heat, links the neurons written this session with weak temporal synapses (synapses_updated), updates the manifest and syncs shared team spaces (offline is normal). Returns session_id, facts_saved, decisions_saved, topics_touched and per-space sync state; topics_touched logs neurons you only read. Not consolidating loses the session\'s knowledge. Mid-session open items go to crbro_context; housekeeping is crbro_maintenance.',
|
|
958
1153
|
inputSchema: {
|
|
959
|
-
summary: zod_1.z.string().describe('
|
|
1154
|
+
summary: zod_1.z.string().describe('A headline paragraph, not a report: what was done, decided and left open, in a few sentences. The facts themselves belong in crbro_learn, where recall finds them; this text is re-read at every boot. Stored whole, after credential redaction. Session logs are not searched by recall: what only lives here is invisible to it.'),
|
|
960
1155
|
topics_touched: zod_1.z.array(zod_1.z.string()).optional().describe('Neuron ids this session used WITHOUT writing (recalled, inspected, discussed). Added to the log\'s topics_touched next to the ids written this session; write counters stay real. Unknown ids are dropped and listed in topics_unknown.'),
|
|
961
1156
|
},
|
|
962
1157
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
963
1158
|
}, async (args) => {
|
|
964
1159
|
try {
|
|
965
|
-
|
|
1160
|
+
// A summary is re-read at every boot of every later session. The card
|
|
1161
|
+
// asks for one line; the field was taking whole reports — median 8,233
|
|
1162
|
+
// characters on one brain — so the ceiling is enforced here and declared,
|
|
1163
|
+
// never applied in silence. What a session learned goes through
|
|
1164
|
+
// crbro_learn, where it is found by content instead of re-read whole.
|
|
1165
|
+
// Stored whole. 2.1.0 cut it at 3,000 characters to protect boot, which
|
|
1166
|
+
// re-read three summaries in full; the same release made boot read only
|
|
1167
|
+
// their first 240, so the cut only ever lost the tail of what the
|
|
1168
|
+
// caller wrote. What is still true: session logs are not searched by
|
|
1169
|
+
// recall, so a fact that lives only here is invisible to it — the note
|
|
1170
|
+
// below says so past the size where that starts to matter.
|
|
1171
|
+
const SUMMARY_LONG = 3_000;
|
|
1172
|
+
const summary = (0, secrets_js_1.redact)(args.summary).text;
|
|
1173
|
+
const result = await maintenance.consolidate(summary, { topicsTouched: args.topics_touched });
|
|
966
1174
|
// Flush any index writes still sitting in the debounce window, so a
|
|
967
1175
|
// session that ends right after a learn does not lose it.
|
|
968
1176
|
await searchEngine.flush();
|
|
@@ -974,6 +1182,10 @@ function createServer() {
|
|
|
974
1182
|
? compartidos.map(c => ({ space: c.space, state: c.state, pushed: c.pushed }))
|
|
975
1183
|
: undefined,
|
|
976
1184
|
message: 'Session consolidated. Brain state persisted.',
|
|
1185
|
+
summary_chars: summary.length,
|
|
1186
|
+
...(summary.length > SUMMARY_LONG ? {
|
|
1187
|
+
note: `Long summary (${summary.length} characters). Session logs are not searched by crbro_recall: anything that lives only here is invisible to it. Store the facts with crbro_learn; boot reads only the first 240 characters of this text.`,
|
|
1188
|
+
} : {}),
|
|
977
1189
|
});
|
|
978
1190
|
}
|
|
979
1191
|
catch (err) {
|
|
@@ -1059,7 +1271,7 @@ function createServer() {
|
|
|
1059
1271
|
note: 'Values are never shown here, by design.',
|
|
1060
1272
|
};
|
|
1061
1273
|
return {
|
|
1062
|
-
content: [{ type: 'text', text: JSON.stringify(payload
|
|
1274
|
+
content: [{ type: 'text', text: JSON.stringify(payload) }],
|
|
1063
1275
|
structuredContent: payload,
|
|
1064
1276
|
};
|
|
1065
1277
|
}
|