crbro-memory 2.7.2 → 2.9.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/README.md +66 -7
- package/SECURITY.md +36 -0
- package/bin/crbro.mjs +1437 -1366
- package/dist/daemon/endpoint.d.ts.map +1 -1
- package/dist/daemon/endpoint.js +18 -0
- package/dist/daemon/endpoint.js.map +1 -1
- package/dist/engine/brain.d.ts.map +1 -1
- package/dist/engine/brain.js +30 -0
- package/dist/engine/brain.js.map +1 -1
- package/dist/engine/cortex.d.ts +55 -1
- package/dist/engine/cortex.d.ts.map +1 -1
- package/dist/engine/cortex.js +183 -3
- package/dist/engine/cortex.js.map +1 -1
- package/dist/engine/maintenance.d.ts +22 -0
- package/dist/engine/maintenance.d.ts.map +1 -1
- package/dist/engine/maintenance.js +59 -2
- package/dist/engine/maintenance.js.map +1 -1
- package/dist/engine/modinstall.d.ts +215 -0
- package/dist/engine/modinstall.d.ts.map +1 -0
- package/dist/engine/modinstall.js +1342 -0
- package/dist/engine/modinstall.js.map +1 -0
- package/dist/engine/shelf.d.ts +97 -0
- package/dist/engine/shelf.d.ts.map +1 -0
- package/dist/engine/shelf.js +343 -0
- package/dist/engine/shelf.js.map +1 -0
- package/dist/engine/source.d.ts +6 -0
- package/dist/engine/source.d.ts.map +1 -0
- package/dist/engine/source.js +101 -0
- package/dist/engine/source.js.map +1 -0
- package/dist/search/index.d.ts +6 -0
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +46 -1
- package/dist/search/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +226 -33
- package/dist/server.js.map +1 -1
- package/dist/sync/materialize.d.ts +4 -0
- package/dist/sync/materialize.d.ts.map +1 -1
- package/dist/sync/materialize.js +80 -1
- package/dist/sync/materialize.js.map +1 -1
- package/dist/sync/ops.d.ts +29 -2
- package/dist/sync/ops.d.ts.map +1 -1
- package/dist/sync/ops.js.map +1 -1
- package/dist/sync/space.d.ts.map +1 -1
- package/dist/sync/space.js +15 -2
- package/dist/sync/space.js.map +1 -1
- package/dist/types/index.d.ts +47 -0
- package/dist/types/index.d.ts.map +1 -1
- package/mods/crbro-pending/.claude-plugin/plugin.json +21 -0
- package/mods/crbro-pending/hooks/hooks.json +1 -0
- package/mods/crbro-pending/hooks/register.tsx +576 -0
- package/mods/crbro-pending/hooks/strings.ts +200 -0
- package/mods/crbro-pending/types/index.d.ts +27 -0
- package/package.json +2 -1
package/dist/server.js
CHANGED
|
@@ -36,8 +36,11 @@ const semantic_js_1 = require("./search/semantic.js");
|
|
|
36
36
|
const budget_js_1 = require("./utils/budget.js");
|
|
37
37
|
const secrets_js_1 = require("./engine/secrets.js");
|
|
38
38
|
const backup_js_1 = require("./engine/backup.js");
|
|
39
|
+
const modinstall_js_1 = require("./engine/modinstall.js");
|
|
39
40
|
const triggers_js_1 = require("./engine/triggers.js");
|
|
40
41
|
const ids_js_1 = require("./utils/ids.js");
|
|
42
|
+
const shelf_js_1 = require("./engine/shelf.js");
|
|
43
|
+
const source_js_1 = require("./engine/source.js");
|
|
41
44
|
/** A neuron this size with no summary is worth two lines from whoever is closing the session. */
|
|
42
45
|
const SUMMARY_NUDGE_MIN_ENTRIES = 25;
|
|
43
46
|
/** 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. */
|
|
@@ -87,6 +90,35 @@ const THREE_STAGES = 'A new truth that REPLACES an old one → crbro_learn with
|
|
|
87
90
|
'Something stopped being true, or was never true, and nothing replaces it → crbro_revise ' +
|
|
88
91
|
'(kept in the file, gone from recall, reversible with status active). Something must not exist ' +
|
|
89
92
|
'on disk at all — a credential, personal data, a whole neuron → crbro_forget (quarantine copy first).';
|
|
93
|
+
/**
|
|
94
|
+
* What to do with a recall's possibly_stale block (shelf life), said once per
|
|
95
|
+
* answer at the START of `hint` (iteration 2, staleness.md §14): the order to
|
|
96
|
+
* check comes first, the follow-up writes after it. Each row carries its own
|
|
97
|
+
* next_step (where to look); this says what to do with the result.
|
|
98
|
+
*/
|
|
99
|
+
const STALE_HINT = 'possibly_stale holds last-known values that may have changed: do not answer with one as current. Check it first ' +
|
|
100
|
+
'(its next_step says where), then: still true → crbro_revise neuron=<neuron_id> status=verified facts=[entry_id] ' +
|
|
101
|
+
'(entries=[entry_id] for a decision or pattern); changed → crbro_learn the new value with supersedes=[entry_id]. ' +
|
|
102
|
+
'If you cannot check, say it may be out of date.';
|
|
103
|
+
/**
|
|
104
|
+
* The per-row framing of a possibly_stale row (iteration 2). The stored line
|
|
105
|
+
* comes back as `last_known`, not as `matching_content`, behind a warning and
|
|
106
|
+
* a concrete next step: open what the line itself names, when it names a
|
|
107
|
+
* file, path or URL; otherwise look where that kind of value lives, and if
|
|
108
|
+
* that is not possible, say the value may be out of date.
|
|
109
|
+
*/
|
|
110
|
+
function staleFraming(text, s) {
|
|
111
|
+
const since = s.last_verified;
|
|
112
|
+
const warning = s.age_from
|
|
113
|
+
? `last known value, not verified since ${since}: past its shelf life, may have changed`
|
|
114
|
+
: `last known value, unverified for ${s.age_days} days (since ${since}): may have changed`;
|
|
115
|
+
const named = (0, source_js_1.namedSources)(text);
|
|
116
|
+
const fallback = `If you cannot check, say this value is from ${since} and may be out of date; do not state it as current.`;
|
|
117
|
+
const next_step = named.length
|
|
118
|
+
? `Before answering, open ${named.join(' / ')} (named in this entry) and answer with what it says now. ${fallback}`
|
|
119
|
+
: `Before answering, look for the current value where it lives: the project's files or config if you can read them, or the user. ${fallback}`;
|
|
120
|
+
return { warning, next_step };
|
|
121
|
+
}
|
|
90
122
|
/**
|
|
91
123
|
* The version of CRBRO that is actually running. The manifest carries its own
|
|
92
124
|
* version, but that one stamps the brain FORMAT and has not moved since 1.0.0
|
|
@@ -154,6 +186,7 @@ function createServer(shared) {
|
|
|
154
186
|
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. ' +
|
|
155
187
|
'Before answering OR ACTING ON 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. ' +
|
|
156
188
|
'Recall even when you think you know. Only when the current message itself states the answer does it outrank memory: then use it — a recall that finds nothing does not make it unknown, and a stored value older than what the user just said is the one to update, not to repeat. ' +
|
|
189
|
+
'A row in a recall\'s possibly_stale is a last-known value that may have changed: before answering with it, check it where it lives (the file it names, the project\'s files or config, or the user) and answer from that; if you cannot check, say it may be out of date — never state it as current. Then crbro_revise status=verified or crbro_learn with supersedes. ' +
|
|
157
190
|
'Acting includes touching one of their systems: before the first command that explores or changes a project of theirs, recall what is already known about it — a stored pattern or map usually holds the very procedure you were about to reconstruct by reading files, and reconstructing it is how you end up doing the steps in the wrong order. ' +
|
|
158
191
|
'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. ' +
|
|
159
192
|
'Save with crbro_learn as you go, and close with crbro_consolidate before the conversation ends.',
|
|
@@ -212,6 +245,12 @@ function createServer(shared) {
|
|
|
212
245
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
213
246
|
}, async (args) => {
|
|
214
247
|
try {
|
|
248
|
+
// The Claude Code mod (open items above the prompt): installed or
|
|
249
|
+
// refreshed on its own, once per process, on a short budget that
|
|
250
|
+
// never fails or holds up the boot. Started first so it runs beside
|
|
251
|
+
// the brain's own work; its notice is taken only once this boot has
|
|
252
|
+
// an answer to carry it, so a boot that fails does not lose it.
|
|
253
|
+
const modNotice = (0, modinstall_js_1.startModOnBoot)();
|
|
215
254
|
const result = await brain.boot();
|
|
216
255
|
// Initialize search engine
|
|
217
256
|
await searchEngine.init();
|
|
@@ -333,6 +372,11 @@ function createServer(shared) {
|
|
|
333
372
|
'Semantic recall is not installed on this machine: run once npx crbro-memory init (about 500 MB). ' +
|
|
334
373
|
'Until then recall is keyword-only; keywords at save time still work.';
|
|
335
374
|
}
|
|
375
|
+
// Said once, the boot after the mod was installed or updated: what
|
|
376
|
+
// changed on the user's machine and how to undo it.
|
|
377
|
+
const aviso = await modNotice();
|
|
378
|
+
if (aviso)
|
|
379
|
+
response.mod_notice = aviso;
|
|
336
380
|
response.memory_discipline =
|
|
337
381
|
'Before crbro_learn, crbro_recall: what you are about to save may already exist — then pass ' +
|
|
338
382
|
'supersedes instead of adding a sibling (two versions of one fact compete on recall as equals). ' +
|
|
@@ -348,7 +392,10 @@ function createServer(shared) {
|
|
|
348
392
|
'deliberate deferral with its ceiling and revisit trigger. Credentials never go in the brain: ' +
|
|
349
393
|
'crbro_secret, then record only the NAME. Recall results carry confidence — "weak" means the match ' +
|
|
350
394
|
'covers little of the question, verify before relying on it — and when two facts disagree, prefer ' +
|
|
351
|
-
'the more recent.
|
|
395
|
+
'the more recent. possibly_stale in a recall = a last-known value that may have changed: check its ' +
|
|
396
|
+
'source before answering with it, or say it may be out of date (then crbro_revise status=verified if it ' +
|
|
397
|
+
'holds, crbro_learn supersedes if it changed). A value that can change (version, price, port, host, ' +
|
|
398
|
+
'setting, who holds a role) should say where it came from — the file, key, URL or person. Lifecycle: supersedes replaces, crbro_revise retires, crbro_forget removes what ' +
|
|
352
399
|
'must not exist on disk — each tool describes its own stage. ' +
|
|
353
400
|
'Call crbro_consolidate before the conversation ends; it logs the session too.';
|
|
354
401
|
// A mature brain outgrew the boot payload: on a 1,145-neuron brain it
|
|
@@ -358,7 +405,7 @@ function createServer(shared) {
|
|
|
358
405
|
// start correctly are kept whole; what grows without bound is shortened
|
|
359
406
|
// and says so, with the call that reads it in full.
|
|
360
407
|
return jsonResult((0, budget_js_1.fitToBudget)(response, {
|
|
361
|
-
keep: ['protocol_enforcement', 'memory_discipline', 'retired_tools', 'pending_guidance', 'semantic_hint'],
|
|
408
|
+
keep: ['protocol_enforcement', 'memory_discipline', 'retired_tools', 'pending_guidance', 'semantic_hint', 'mod_notice'],
|
|
362
409
|
howToGetMore: 'Session summaries were shortened. Read one in full with crbro_inspect view=sessions.',
|
|
363
410
|
}));
|
|
364
411
|
}
|
|
@@ -460,6 +507,13 @@ function createServer(shared) {
|
|
|
460
507
|
last_consolidation: manifest.last_consolidation,
|
|
461
508
|
semantic: (0, semantic_js_1.semanticStatus)(),
|
|
462
509
|
hot_topics_recalculated: hot?.last_recalculated ?? null,
|
|
510
|
+
// Shelf life: whether recall splits off possibly_stale, the windows
|
|
511
|
+
// in days, and the day this brain started counting (legacy grace).
|
|
512
|
+
staleness: {
|
|
513
|
+
enabled: (0, shelf_js_1.stalenessEnabled)(),
|
|
514
|
+
windows: (0, shelf_js_1.shelfWindows)(),
|
|
515
|
+
since: manifest.staleness_since ? dia(manifest.staleness_since) : null,
|
|
516
|
+
},
|
|
463
517
|
});
|
|
464
518
|
}
|
|
465
519
|
if (args.view === 'neuron') {
|
|
@@ -491,21 +545,38 @@ function createServer(shared) {
|
|
|
491
545
|
const offset = Math.max(args.offset ?? 0, 0);
|
|
492
546
|
const connections = await synapses.getConnections(neuron.id, args.min_strength);
|
|
493
547
|
const retired = (id) => neuron.entry_status?.[id]?.status;
|
|
548
|
+
// Shelf life: the same three facts recall shows, so the index of a
|
|
549
|
+
// neuron and a recall never disagree about what is old.
|
|
550
|
+
const vida = (0, shelf_js_1.stalenessContext)((await brain.getManifest()).staleness_since);
|
|
551
|
+
const rancio = (s) => (s?.stale ? { stale_days: s.age_days } : {});
|
|
494
552
|
const rows = [];
|
|
495
553
|
for (const f of neuron.facts || []) {
|
|
496
554
|
rows.push({ id: f.id || (0, hash_js_1.factId)(f.text), kind: 'fact', text: f.text, added: f.added || '',
|
|
497
555
|
status: f.status, confidence: f.confidence, keys: f.keys, revision_note: f.revision_note, revised: f.revised,
|
|
498
556
|
// Only when it says something: 1 is every fact's default (2.7).
|
|
499
|
-
...((f.confirmations ?? 1) > 1 ? { confirmations: f.confirmations } : {})
|
|
557
|
+
...((f.confirmations ?? 1) > 1 ? { confirmations: f.confirmations } : {}),
|
|
558
|
+
...(f.verified ? { verified: f.verified } : {}),
|
|
559
|
+
...(f.shelf_life ? { shelf_life: f.shelf_life } : {}),
|
|
560
|
+
...(vida && neuron.type !== 'protocol' ? rancio((0, shelf_js_1.factStaleness)(f, vida)) : {}) });
|
|
500
561
|
}
|
|
562
|
+
const entrada = (kind, text, id) => {
|
|
563
|
+
const v = neuron.entry_verified?.[id];
|
|
564
|
+
const retirada = !!neuron.entry_status?.[id];
|
|
565
|
+
return {
|
|
566
|
+
...(v ? { verified: v } : {}),
|
|
567
|
+
...(vida && !retirada && neuron.type !== 'protocol' ? rancio((0, shelf_js_1.entryStaleness)(neuron, kind, text, vida)) : {}),
|
|
568
|
+
};
|
|
569
|
+
};
|
|
501
570
|
for (const d of neuron.decisions || []) {
|
|
502
571
|
const id = d.id || (0, ops_js_1.entryId)(d.text);
|
|
503
|
-
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
|
|
572
|
+
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,
|
|
573
|
+
...entrada('decision', d.text, (0, ops_js_1.entryId)(d.text)) });
|
|
504
574
|
}
|
|
505
575
|
const sidecar = (kind, list) => {
|
|
506
576
|
for (const t of list || []) {
|
|
507
577
|
const id = (0, ops_js_1.entryId)(t);
|
|
508
|
-
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
|
|
578
|
+
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,
|
|
579
|
+
...entrada(kind, t, id) });
|
|
509
580
|
}
|
|
510
581
|
};
|
|
511
582
|
sidecar('pattern', neuron.patterns);
|
|
@@ -585,6 +656,9 @@ function createServer(shared) {
|
|
|
585
656
|
preview: r.text.length > PREVIEW ? `${r.text.slice(0, PREVIEW).trimEnd()}…` : r.text,
|
|
586
657
|
chars: r.text.length,
|
|
587
658
|
...(r.confirmations ? { confirmations: r.confirmations } : {}),
|
|
659
|
+
...(r.verified ? { verified: dia(r.verified) } : {}),
|
|
660
|
+
...(r.shelf_life ? { shelf_life: r.shelf_life } : {}),
|
|
661
|
+
...(r.stale_days !== undefined ? { stale_days: r.stale_days } : {}),
|
|
588
662
|
...(isRetired(r.status) ? { status: r.status, ...(r.revised ? { revised: dia(r.revised) } : {}), ...(r.revision_note ? { retired_note: r.revision_note } : {}) } : {}),
|
|
589
663
|
})),
|
|
590
664
|
entries_pagination: {
|
|
@@ -665,7 +739,7 @@ function createServer(shared) {
|
|
|
665
739
|
// ═══════════════════════════════════════════════════════════════
|
|
666
740
|
server.registerTool('crbro_learn', {
|
|
667
741
|
title: 'Learn something',
|
|
668
|
-
description: 'Write: store a fact, decision, pattern, preference, error or debt on a topic; the neuron is created if missing (or pass neuron_id). Stage 1 of the lifecycle: a new truth that REPLACES an old one → crbro_learn with supersedes
|
|
742
|
+
description: 'Write: store a fact, decision, pattern, preference, error or debt on a topic; the neuron is created if missing (or pass neuron_id). A value that can change names its source (file, key, URL, person). Stage 1 of the lifecycle: a new truth that REPLACES an old one → crbro_learn with supersedes; retire with no replacement → crbro_revise; delete from disk → crbro_forget. crbro_recall first — it may already exist. The same fact text again is not duplicated: keywords merge, a changed confidence or shelf_life applies (updated_in_place), a bare repeat counts as re-verified and another session repeating it raises confirmations; text matching a retired entry is refused with skipped_retired. Decisions always append; preferences never leave this machine. Credentials become a marker listed in redacted: crbro_secret them, keep only the name. Returns neuron_id, action, the fact\'s shelf_life, near_duplicates (stored anyway; retire the old telling) and supersedes_unmatched (still live).',
|
|
669
743
|
inputSchema: {
|
|
670
744
|
// Optional since 2.0.3, and the reason is measured: the description
|
|
671
745
|
// told callers that neuron_id "skips name matching entirely", the
|
|
@@ -676,7 +750,7 @@ function createServer(shared) {
|
|
|
676
750
|
// message that says what to pass.
|
|
677
751
|
topic: zod_1.z.string().optional().describe('Topic name, e.g. "OctoChat", "Firebase", "SEO Strategy". Required UNLESS you pass neuron_id, in which case the topic is taken from that neuron.'),
|
|
678
752
|
type: zod_1.z.enum(['fact', 'decision', 'pattern', 'preference', 'error', 'debt']).describe('error = a mistake plus its correction, in one entry. debt = a deliberate deferral: what was NOT done on purpose, its ceiling, and the revisit condition, e.g. "DEFERRED: protecting the PDFs. CEILING: anyone can download them without signing up. REVISIT WHEN: the signup flow works."'),
|
|
679
|
-
content: zod_1.z.string().describe('The knowledge itself. Dense and self-contained: it is recalled without this conversation as context.'),
|
|
753
|
+
content: zod_1.z.string().describe('The knowledge itself. Dense and self-contained: it is recalled without this conversation as context. For a value that can change (a version, price, port, host, setting, who holds a role), say where it came from — the file, config key, URL or person — so a later check knows where to look.'),
|
|
680
754
|
confidence: zod_1.z.number().min(0).max(1).optional().describe('0.0-1.0, default 1.0. Facts only. On an exact-duplicate active fact the stored confidence is updated to this value (updated_in_place:true).'),
|
|
681
755
|
domain: zod_1.z.string().optional().describe('Domain, e.g. "proyectos-web". Applied when the neuron is created; on an existing neuron it only replaces the default "general" (crbro_revise domain replaces it unconditionally).'),
|
|
682
756
|
rationale: zod_1.z.string().optional().describe('Why the decision was taken. Stored and indexed with it; ignored for other types.'),
|
|
@@ -684,6 +758,7 @@ function createServer(shared) {
|
|
|
684
758
|
supersedes: zod_1.z.array(zod_1.z.string()).optional().describe('Facts this one replaces: their ids or exact text. They leave recall but stay in the file. Unmatched targets are reported and stay live.'),
|
|
685
759
|
keywords: zod_1.z.array(zod_1.z.string()).optional().describe('Facts only, and expected on every fact: 2-5 words a future question may use that the text does not contain — synonyms, the other language, the generic name of the product named. Indexed with the fact, never shown; the largest measured lever on recall. Without them the fact is stored and the answer carries keywords_missing. The same text again with new keywords merges them.'),
|
|
686
760
|
keywords_replace: zod_1.z.boolean().optional().describe('When the exact fact text already exists, replace its stored keywords with `keywords` instead of merging (default false). Teammates in a shared space only ever receive the union.'),
|
|
761
|
+
shelf_life: zod_1.z.enum(shelf_js_1.SHELF_LIVES).optional().describe('Facts only: how fast this value goes stale. volatile = versions, prices, ports, hosts, paths, config, who holds a role; durable = rarely moves; permanent = history that cannot change; normal otherwise. Omitted: inferred from the text, and returned.'),
|
|
687
762
|
},
|
|
688
763
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
689
764
|
}, async (args) => {
|
|
@@ -711,6 +786,7 @@ function createServer(shared) {
|
|
|
711
786
|
supersedes: args.supersedes,
|
|
712
787
|
keys: args.keywords,
|
|
713
788
|
keysReplace: args.keywords_replace,
|
|
789
|
+
shelfLife: args.type === 'fact' ? args.shelf_life : undefined,
|
|
714
790
|
});
|
|
715
791
|
// Indexing happens inside cortex.learn, through the indexer hook.
|
|
716
792
|
if (result.action === 'skipped_retired' && result.skipped_retired) {
|
|
@@ -729,6 +805,13 @@ function createServer(shared) {
|
|
|
729
805
|
if (!result.neuron) {
|
|
730
806
|
return textResult(`No neuron matched "${topic}" and none was created.`);
|
|
731
807
|
}
|
|
808
|
+
// Lengthening an explicit shelf life does not hold on a shared neuron:
|
|
809
|
+
// the team merge keeps the most volatile value, and the older op is
|
|
810
|
+
// still in the append-only log, so the next sync brings it back.
|
|
811
|
+
const sharedIn = result.shelf_lengthened ? (await (0, space_js_1.sharedMap)(brain))[result.neuron.id] : undefined;
|
|
812
|
+
const shelfSharedWarning = sharedIn
|
|
813
|
+
? `"${result.neuron.id}" is shared in space "${sharedIn}": a longer shelf_life is local only — the next sync restores the more volatile value from the shared log.`
|
|
814
|
+
: undefined;
|
|
732
815
|
const keywordsMissing = args.type === 'fact' && result.action !== 'skipped' &&
|
|
733
816
|
!(args.keywords && args.keywords.some(k => typeof k === 'string' && k.trim().length > 0));
|
|
734
817
|
return jsonResult({
|
|
@@ -772,6 +855,15 @@ function createServer(shared) {
|
|
|
772
855
|
'question may use that the text lacks (synonyms, the other language, the generic name of the product): ' +
|
|
773
856
|
'they merge into this fact instead of adding a new one.'
|
|
774
857
|
: undefined,
|
|
858
|
+
// Shelf life, facts only: the class that applies, and when it was not
|
|
859
|
+
// given, that it was inferred and by which rule, so a model that
|
|
860
|
+
// disagrees can say otherwise in the next call.
|
|
861
|
+
shelf_life: result.shelf?.shelf_life,
|
|
862
|
+
shelf_inferred: result.shelf?.inferred ? true : undefined,
|
|
863
|
+
shelf_reason: result.shelf?.inferred ? result.shelf.reason : undefined,
|
|
864
|
+
// The same line learned again, and nothing else: its shelf-life clock restarts now.
|
|
865
|
+
reconfirmed: result.reconfirmed || undefined,
|
|
866
|
+
shared_warning: shelfSharedWarning,
|
|
775
867
|
total_facts: result.neuron.facts.length,
|
|
776
868
|
total_decisions: result.neuron.decisions.length,
|
|
777
869
|
total_patterns: result.neuron.patterns.length,
|
|
@@ -794,13 +886,13 @@ function createServer(shared) {
|
|
|
794
886
|
// ═══════════════════════════════════════════════════════════════
|
|
795
887
|
server.registerTool('crbro_recall', {
|
|
796
888
|
title: 'Recall',
|
|
797
|
-
description: 'Read-only search of everything saved in earlier sessions — facts, decisions, patterns, preferences, errors, debts and maps. 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.
|
|
889
|
+
description: 'Read-only search of everything saved in earlier sessions — facts, decisions, patterns, preferences, errors, debts and maps. 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. One result per neuron: the best matching entry with entry_id (read it whole: crbro_inspect view=neuron entries=[id]), matched_kind, matched_added, a confidence label (weak = little of the question covered; verify) and also_matched previews. Lines not the user\'s own carry origin, also_matched too: team:<space> (team if unshared) with self-declared by, or miner. Rows past their shelf life move to possibly_stale as last_known with a next_step: check before answering with one, or say it may be out of date. If nothing matches, retry with 2-4 phrasings in queries or fewer, rarer words. has_map:true: read the system map with crbro_map before touching that system.',
|
|
798
890
|
inputSchema: {
|
|
799
891
|
query: zod_1.z.string().describe('What to look for, e.g. "Firebase authentication setup". Fewer, distinctive terms beat full sentences.'),
|
|
800
892
|
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.'),
|
|
801
893
|
domain: zod_1.z.string().optional().describe('Only neurons in this domain (exact match, e.g. "proyectos-web"). Day logs have no domain: sessions_matched is listed regardless.'),
|
|
802
894
|
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).'),
|
|
803
|
-
since: zod_1.z.string().optional().describe('Only entries
|
|
895
|
+
since: zod_1.z.string().optional().describe('Only entries recorded on or after this: a day ("2026-09-01") or a span back from today ("7d", "2w", "3m"). For "what changed lately" and to keep an old telling out. A later verification does not make an old entry new. Undated entries cannot prove they are recent: they are left out and counted in undated_skipped.'),
|
|
804
896
|
kind: zod_1.z.array(zod_1.z.enum(index_js_1.RECALL_KINDS)).optional().describe('Only these entry kinds, e.g. ["error"] for past mistakes before repeating one, ["decision"] for what was agreed and why, ["debt"] for what was deferred. Day logs are left out when set.'),
|
|
805
897
|
},
|
|
806
898
|
outputSchema: {
|
|
@@ -814,11 +906,18 @@ function createServer(shared) {
|
|
|
814
906
|
matched_terms: zod_1.z.number().optional(), query_terms: zod_1.z.number().optional(),
|
|
815
907
|
confidence: zod_1.z.enum(['strong', 'weak']).optional(),
|
|
816
908
|
entry_id: zod_1.z.string().optional(),
|
|
909
|
+
rank: zod_1.z.number().optional().describe('Position in the ranking, set when some row moved to possibly_stale'),
|
|
817
910
|
origin: zod_1.z.string().optional(), by: zod_1.z.string().optional(),
|
|
818
911
|
content_truncated: zod_1.z.boolean().optional(), content_chars: zod_1.z.number().optional(),
|
|
819
|
-
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(), origin: zod_1.z.string().optional(), by: zod_1.z.string().optional() }).loose()).optional(),
|
|
912
|
+
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(), origin: zod_1.z.string().optional(), by: zod_1.z.string().optional(), stale_days: zod_1.z.number().optional() }).loose()).optional(),
|
|
820
913
|
}).loose()),
|
|
821
|
-
|
|
914
|
+
stale_warning: zod_1.z.string().optional().describe('Set, first, when some row moved to possibly_stale: do not answer with those as current'),
|
|
915
|
+
possibly_stale: zod_1.z.array(zod_1.z.object({
|
|
916
|
+
warning: zod_1.z.string(), next_step: zod_1.z.string(), last_known: zod_1.z.string(), neuron_id: zod_1.z.string(),
|
|
917
|
+
age_days: zod_1.z.number(), last_verified: zod_1.z.string(), shelf_life: zod_1.z.string(), shelf_inferred: zod_1.z.boolean(),
|
|
918
|
+
}).loose()).optional().describe('Rows whose best entry is past its shelf life since last verified: warning, next_step (what to check before answering), the entry as last_known instead of matching_content, then the rest of a results row plus age_days, last_verified, shelf_life, shelf_inferred'),
|
|
919
|
+
possibly_stale_count: zod_1.z.number().optional(),
|
|
920
|
+
returned: zod_1.z.number().optional().describe('Rows that came back: results plus possibly_stale'),
|
|
822
921
|
matched_neurons: zod_1.z.number().optional().describe('Neurons with any hit before limit; total_results is what came back'),
|
|
823
922
|
has_more: zod_1.z.boolean().optional(),
|
|
824
923
|
sessions_matched: zod_1.z.array(zod_1.z.object({}).loose()).optional(),
|
|
@@ -862,36 +961,94 @@ function createServer(shared) {
|
|
|
862
961
|
...(r.also_matched ? { also_matched: r.also_matched.map(a => ({ ...a, added: dia(a.added) })) } : {}),
|
|
863
962
|
};
|
|
864
963
|
});
|
|
865
|
-
|
|
964
|
+
// Shelf life: partition, not penalty. The ranking above already chose
|
|
965
|
+
// these rows exactly as before; a row whose WINNING entry is past its
|
|
966
|
+
// shelf life since last verified moves whole to possibly_stale, in
|
|
967
|
+
// rank order. No backfill (the cost stays bounded by limit, and "what
|
|
968
|
+
// does memory say about X" never silently becomes a weaker line about
|
|
969
|
+
// something else), and never re-headed with an also_matched line.
|
|
970
|
+
// The internal `staleness` field leaves every row here.
|
|
971
|
+
const actuales = [];
|
|
972
|
+
const rancios = [];
|
|
973
|
+
// When anything moves, every row keeps its rank: results[0] is then
|
|
974
|
+
// not necessarily the best match, and the agent has to be able to see it.
|
|
975
|
+
const algunoRancio = rows.some(r => r.staleness?.stale);
|
|
976
|
+
let mejorMovido = false;
|
|
977
|
+
for (const [i, row] of rows.entries()) {
|
|
978
|
+
const { staleness, ...resto } = row;
|
|
979
|
+
if (algunoRancio)
|
|
980
|
+
resto.rank = i + 1;
|
|
981
|
+
if (staleness?.stale) {
|
|
982
|
+
if (i === 0)
|
|
983
|
+
mejorMovido = true;
|
|
984
|
+
// Iteration 2: the old line is not served as ordinary content. Its
|
|
985
|
+
// warning and next step come first, then the line as last_known.
|
|
986
|
+
const { matching_content, neuron_id, name, entry_id, rank, ...demas } = resto;
|
|
987
|
+
rancios.push({
|
|
988
|
+
...staleFraming(matching_content, staleness),
|
|
989
|
+
last_known: matching_content,
|
|
990
|
+
neuron_id, name,
|
|
991
|
+
...(entry_id !== undefined ? { entry_id } : {}),
|
|
992
|
+
...(rank !== undefined ? { rank } : {}),
|
|
993
|
+
...demas,
|
|
994
|
+
age_days: staleness.age_days,
|
|
995
|
+
last_verified: staleness.last_verified,
|
|
996
|
+
shelf_life: staleness.shelf_life,
|
|
997
|
+
shelf_inferred: staleness.shelf_inferred,
|
|
998
|
+
...(staleness.age_from ? { age_counted_from: staleness.age_from } : {}),
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
actuales.push(resto);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const servidos = actuales.length + rancios.length;
|
|
1006
|
+
const sobran = matched_neurons - servidos;
|
|
1007
|
+
// Iteration 2: when anything moved, the answer OPENS with a short
|
|
1008
|
+
// warning, before query and results, so it is read before the rows.
|
|
1009
|
+
const menorEdad = rancios.length ? Math.min(...rancios.map(r => r.age_days)) : 0;
|
|
1010
|
+
const uno = rancios.length === 1;
|
|
1011
|
+
const avisoRancio = rancios.length
|
|
1012
|
+
? (uno
|
|
1013
|
+
? `1 matching row is in possibly_stale${mejorMovido ? ', the best match' : ''}: a last-known value, unchecked for ${menorEdad} days, that may have changed. Do not answer with it as current. Check it first (its next_step says where)`
|
|
1014
|
+
: `${rancios.length} matching rows are in possibly_stale${mejorMovido ? ', the best match among them' : ''}: last-known values, unchecked for ${menorEdad}+ days, that may have changed. Do not answer with one as current. Check it first (each row's next_step says where)`) +
|
|
1015
|
+
'; if you cannot, say it may be out of date, even in a short answer.'
|
|
1016
|
+
: '';
|
|
866
1017
|
const payload = {
|
|
1018
|
+
...(avisoRancio ? { stale_warning: avisoRancio } : {}),
|
|
867
1019
|
query: args.query,
|
|
868
|
-
// total_results is what came back (capped by limit);
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
1020
|
+
// total_results is what came back as current (capped by limit);
|
|
1021
|
+
// returned adds possibly_stale; matched_neurons is how many neurons
|
|
1022
|
+
// had a hit at all, so five never reads as "only five".
|
|
1023
|
+
total_results: actuales.length,
|
|
1024
|
+
returned: servidos,
|
|
872
1025
|
matched_neurons,
|
|
873
1026
|
has_more: sobran > 0,
|
|
874
1027
|
// The filter as it was understood ("30d" becomes a day), so a narrowed
|
|
875
1028
|
// answer never reads as the whole brain.
|
|
876
1029
|
...(filtrado ? { filters: { ...(since ? { since } : {}), ...(kinds ? { kind: kinds } : {}) } } : {}),
|
|
877
1030
|
...(since ? { undated_skipped: sinFecha } : {}),
|
|
878
|
-
results:
|
|
1031
|
+
results: actuales,
|
|
1032
|
+
...(rancios.length ? { possibly_stale: rancios, possibly_stale_count: rancios.length } : {}),
|
|
879
1033
|
// The diary, searched since 2.2: day logs whose summary mentions the
|
|
880
1034
|
// question, in a list of their own so narrative never outranks a fact.
|
|
881
1035
|
// sessions_total: how many days had a hit; three shown never reads as three.
|
|
882
1036
|
...(sessions.length ? { sessions_matched: sessions.map(s => ({ ...s, date: dia(s.date) })), sessions_total } : {}),
|
|
883
|
-
hint: (
|
|
884
|
-
? (
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
: '
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1037
|
+
hint: (rancios.length && !actuales.length ? 'Nothing current matched; the rows that did are in possibly_stale. '
|
|
1038
|
+
: mejorMovido ? 'The best match (rank 1) moved to possibly_stale; results holds lower-ranked rows that may be about something else. ' : '') +
|
|
1039
|
+
(rancios.length ? `${STALE_HINT} ` : '') +
|
|
1040
|
+
(servidos === 0
|
|
1041
|
+
? (sessions.length
|
|
1042
|
+
// A day mentions it and no fact does: point at the day, not at rephrasing.
|
|
1043
|
+
? `No stored fact matched; ${sessions_total} day log${sessions_total === 1 ? '' : 's'} mention it (sessions_matched): read one whole with crbro_inspect view=sessions session=<session_id>. If it is a fact worth keeping, save it with crbro_learn.`
|
|
1044
|
+
: (filtrado
|
|
1045
|
+
? 'Nothing matched inside the filter. Drop since/kind and ask again before concluding it is not stored.'
|
|
1046
|
+
: 'Nothing matched. Try fewer, more distinctive words - names, ids, filenames - rather than a full sentence.'))
|
|
1047
|
+
: 'weak: verify. Newer wins on conflict. entry_id → crbro_inspect view=neuron entries=[id]. has_map → crbro_map first.'
|
|
1048
|
+
+ (sobran > 0 ? ` ${sobran} more neuron${sobran === 1 ? '' : 's'} matched: raise limit or narrow the query.` : '')
|
|
1049
|
+
+ (cortados > 0 ? ' content_truncated → read the entry by entry_id.' : '')
|
|
1050
|
+
+ (sessions.length ? ' sessions_matched: day logs that mention it; read one whole with crbro_inspect view=sessions session=<session_id>.' : ''))
|
|
1051
|
+
+ (filtrado && servidos > 0 ? ' Filtered: entries outside since/kind were not searched.' : '')
|
|
895
1052
|
+ (sinFecha > 0 ? ` ${sinFecha} matching entr${sinFecha === 1 ? 'y has' : 'ies have'} no date and were left out by since (crbro_maintenance backfill_dates dates the ones that state a day).` : '')
|
|
896
1053
|
+ (sessions_total > sessions.length ? ` ${sessions_total - sessions.length} more day${sessions_total - sessions.length === 1 ? '' : 's'} mention it: narrow the query.` : ''),
|
|
897
1054
|
};
|
|
@@ -916,9 +1073,9 @@ function createServer(shared) {
|
|
|
916
1073
|
const reviseSchema = zod_1.z.object({
|
|
917
1074
|
neuron: zod_1.z.string().describe('Neuron id or name holding what to revise, e.g. "project_octochat".'),
|
|
918
1075
|
facts: zod_1.z.array(zod_1.z.string()).optional().describe('Facts to move to `status`: their ids (from crbro_recall) or exact text (trimmed, case-insensitive). For superseded/retracted only active facts match; for active only retired ones do.'),
|
|
919
|
-
entries: zod_1.z.array(zod_1.z.string()).optional().describe('Exact texts of decisions, patterns, errors or debts to move to `status`. Retired entries stay in the file (entry_status) but leave recall like a superseded fact.'),
|
|
920
|
-
status: zod_1.z.enum(['superseded', 'retracted', 'active']).optional().describe('superseded = a newer truth exists (default); retracted = it was never true; active = reactivate a retired fact or entry
|
|
921
|
-
note: zod_1.z.string().optional().describe('Why. Stored as revision_note on facts and entry_status.note on entries. The next reader will wonder.'),
|
|
1076
|
+
entries: zod_1.z.array(zod_1.z.string()).optional().describe('Exact texts of decisions, patterns, errors or debts to move to `status`. Retired entries stay in the file (entry_status) but leave recall like a superseded fact. With status verified: decisions or patterns, by exact text or entry id.'),
|
|
1077
|
+
status: zod_1.z.enum(['superseded', 'retracted', 'active', 'verified']).optional().describe('superseded = a newer truth exists (default); retracted = it was never true; active = reactivate a retired fact or entry (local only on a shared neuron: the next sync re-applies the retirement; shared_warning says so). verified = you checked these active facts, or live decisions or patterns, against their source and they still hold: their last verification becomes now and they leave possibly_stale. A retired target is not verifiable: it comes back in unmatched and retired_targets.'),
|
|
1078
|
+
note: zod_1.z.string().optional().describe('Why. Stored as revision_note on facts and entry_status.note on entries. The next reader will wonder. Ignored with status verified.'),
|
|
922
1079
|
summary: zod_1.z.string().optional().describe('Replace the neuron summary. Credentials are redacted and listed in redacted.'),
|
|
923
1080
|
domain: zod_1.z.string().optional().describe('Replace the neuron domain unconditionally, e.g. "proyectos-web".'),
|
|
924
1081
|
tags: zod_1.z.array(zod_1.z.string()).optional().describe('Replace the WHOLE tag list (trimmed, deduplicated). On protocol neurons re-send the priority: and source: tags or they are gone.'),
|
|
@@ -938,7 +1095,7 @@ function createServer(shared) {
|
|
|
938
1095
|
});
|
|
939
1096
|
server.registerTool('crbro_revise', {
|
|
940
1097
|
title: 'Revise a neuron',
|
|
941
|
-
description: 'Write: change what a neuron says without deleting anything. Stage 2 of the lifecycle: something stopped being true, or was never true, and nothing replaces it → crbro_revise (kept in the file, gone from recall, reversible with status active). If a replacement exists, crbro_learn with supersedes does both; for what must not exist on disk use crbro_forget. facts retires facts by id or exact text; entries retires decisions, patterns, errors and debts by exact text; status active reactivates either (local only on a shared neuron: the next sync re-applies the retirement, shared_warning says so). summary, domain, tags and name edit metadata in the same call (tags replaces the whole list; the id never changes). move_to splits: the listed entries go to another neuron with their dates. Anything in unmatched is STILL LIVE — fix and re-run.',
|
|
1098
|
+
description: 'Write: change what a neuron says without deleting anything. Stage 2 of the lifecycle: something stopped being true, or was never true, and nothing replaces it → crbro_revise (kept in the file, gone from recall, reversible with status active). If a replacement exists, crbro_learn with supersedes does both; for what must not exist on disk use crbro_forget. facts retires facts by id or exact text; entries retires decisions, patterns, errors and debts by exact text; status active reactivates either (local only on a shared neuron: the next sync re-applies the retirement, shared_warning says so); status verified records that a fact, decision or pattern was checked against its source and still holds. summary, domain, tags and name edit metadata in the same call (tags replaces the whole list; the id never changes). move_to splits: the listed entries go to another neuron with their dates. Anything in unmatched is STILL LIVE — fix and re-run.',
|
|
942
1099
|
inputSchema: reviseSchema,
|
|
943
1100
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
944
1101
|
}, async (args) => {
|
|
@@ -977,6 +1134,42 @@ function createServer(shared) {
|
|
|
977
1134
|
: 'Nothing matched, nothing moved. Pass entry ids from crbro_inspect view=neuron, or the exact text.',
|
|
978
1135
|
});
|
|
979
1136
|
}
|
|
1137
|
+
// ── status verified: reconfirm, do not retire (shelf life) ──
|
|
1138
|
+
if (status === 'verified') {
|
|
1139
|
+
const v = (args.facts?.length || args.entries?.length)
|
|
1140
|
+
? await cortex.verify(target.id, { facts: args.facts, entries: args.entries })
|
|
1141
|
+
: { verified: [], unmatched: [], retired: [] };
|
|
1142
|
+
let cambiados = [];
|
|
1143
|
+
let tachados = [];
|
|
1144
|
+
const metaV = { summary: args.summary, domain: args.domain, tags: args.tags, name: args.name };
|
|
1145
|
+
if (Object.values(metaV).some(x => x !== undefined)) {
|
|
1146
|
+
const r = await cortex.setMeta(target.id, metaV);
|
|
1147
|
+
cambiados = r.changed;
|
|
1148
|
+
tachados = r.redacted;
|
|
1149
|
+
}
|
|
1150
|
+
const n = v.verified.length;
|
|
1151
|
+
const partes = [];
|
|
1152
|
+
if (n > 0)
|
|
1153
|
+
partes.push(`${n} entr${n === 1 ? 'y' : 'ies'} verified: last checked now, shelf-life clock restarted`);
|
|
1154
|
+
if (cambiados.length > 0)
|
|
1155
|
+
partes.push(`${cambiados.join(', ')} updated`);
|
|
1156
|
+
return jsonResult({
|
|
1157
|
+
neuron_id: target.id,
|
|
1158
|
+
status,
|
|
1159
|
+
verified: v.verified,
|
|
1160
|
+
unmatched: v.unmatched.length > 0 ? v.unmatched : undefined,
|
|
1161
|
+
retired_targets: v.retired.length > 0 ? v.retired : undefined,
|
|
1162
|
+
changed: cambiados,
|
|
1163
|
+
redacted: tachados.length > 0 ? tachados : undefined,
|
|
1164
|
+
message: partes.length > 0
|
|
1165
|
+
? `${partes.join('; ')} in "${target.name}".` +
|
|
1166
|
+
(v.unmatched.length > 0 ? ` WARNING: ${v.unmatched.length} target(s) matched no active fact or live decision/pattern and were not verified.` : '') +
|
|
1167
|
+
(v.retired.length > 0 ? ' A retired line is not verifiable: if it holds again, reactivate it with status active first.' : '')
|
|
1168
|
+
: (v.retired.length > 0
|
|
1169
|
+
? 'Nothing verified: the targets are retired. If one holds again, reactivate it with status active first, then verify.'
|
|
1170
|
+
: 'Nothing verified. Pass the fact id (entry_id from crbro_recall) or exact text in facts; decisions and patterns go in entries. Preferences, errors and debts never go stale.'),
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
980
1173
|
let revisedFacts = 0;
|
|
981
1174
|
let revisedEntries = 0;
|
|
982
1175
|
const unmatched = [];
|