crbro-memory 2.8.0 → 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 +15 -4
- package/dist/daemon/endpoint.d.ts.map +1 -1
- package/dist/daemon/endpoint.js +11 -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/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 +213 -32
- 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/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -39,6 +39,8 @@ const backup_js_1 = require("./engine/backup.js");
|
|
|
39
39
|
const modinstall_js_1 = require("./engine/modinstall.js");
|
|
40
40
|
const triggers_js_1 = require("./engine/triggers.js");
|
|
41
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");
|
|
42
44
|
/** A neuron this size with no summary is worth two lines from whoever is closing the session. */
|
|
43
45
|
const SUMMARY_NUDGE_MIN_ENTRIES = 25;
|
|
44
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. */
|
|
@@ -88,6 +90,35 @@ const THREE_STAGES = 'A new truth that REPLACES an old one → crbro_learn with
|
|
|
88
90
|
'Something stopped being true, or was never true, and nothing replaces it → crbro_revise ' +
|
|
89
91
|
'(kept in the file, gone from recall, reversible with status active). Something must not exist ' +
|
|
90
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
|
+
}
|
|
91
122
|
/**
|
|
92
123
|
* The version of CRBRO that is actually running. The manifest carries its own
|
|
93
124
|
* version, but that one stamps the brain FORMAT and has not moved since 1.0.0
|
|
@@ -155,6 +186,7 @@ function createServer(shared) {
|
|
|
155
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. ' +
|
|
156
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. ' +
|
|
157
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. ' +
|
|
158
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. ' +
|
|
159
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. ' +
|
|
160
192
|
'Save with crbro_learn as you go, and close with crbro_consolidate before the conversation ends.',
|
|
@@ -360,7 +392,10 @@ function createServer(shared) {
|
|
|
360
392
|
'deliberate deferral with its ceiling and revisit trigger. Credentials never go in the brain: ' +
|
|
361
393
|
'crbro_secret, then record only the NAME. Recall results carry confidence — "weak" means the match ' +
|
|
362
394
|
'covers little of the question, verify before relying on it — and when two facts disagree, prefer ' +
|
|
363
|
-
'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 ' +
|
|
364
399
|
'must not exist on disk — each tool describes its own stage. ' +
|
|
365
400
|
'Call crbro_consolidate before the conversation ends; it logs the session too.';
|
|
366
401
|
// A mature brain outgrew the boot payload: on a 1,145-neuron brain it
|
|
@@ -472,6 +507,13 @@ function createServer(shared) {
|
|
|
472
507
|
last_consolidation: manifest.last_consolidation,
|
|
473
508
|
semantic: (0, semantic_js_1.semanticStatus)(),
|
|
474
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
|
+
},
|
|
475
517
|
});
|
|
476
518
|
}
|
|
477
519
|
if (args.view === 'neuron') {
|
|
@@ -503,21 +545,38 @@ function createServer(shared) {
|
|
|
503
545
|
const offset = Math.max(args.offset ?? 0, 0);
|
|
504
546
|
const connections = await synapses.getConnections(neuron.id, args.min_strength);
|
|
505
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 } : {});
|
|
506
552
|
const rows = [];
|
|
507
553
|
for (const f of neuron.facts || []) {
|
|
508
554
|
rows.push({ id: f.id || (0, hash_js_1.factId)(f.text), kind: 'fact', text: f.text, added: f.added || '',
|
|
509
555
|
status: f.status, confidence: f.confidence, keys: f.keys, revision_note: f.revision_note, revised: f.revised,
|
|
510
556
|
// Only when it says something: 1 is every fact's default (2.7).
|
|
511
|
-
...((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)) : {}) });
|
|
512
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
|
+
};
|
|
513
570
|
for (const d of neuron.decisions || []) {
|
|
514
571
|
const id = d.id || (0, ops_js_1.entryId)(d.text);
|
|
515
|
-
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)) });
|
|
516
574
|
}
|
|
517
575
|
const sidecar = (kind, list) => {
|
|
518
576
|
for (const t of list || []) {
|
|
519
577
|
const id = (0, ops_js_1.entryId)(t);
|
|
520
|
-
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) });
|
|
521
580
|
}
|
|
522
581
|
};
|
|
523
582
|
sidecar('pattern', neuron.patterns);
|
|
@@ -597,6 +656,9 @@ function createServer(shared) {
|
|
|
597
656
|
preview: r.text.length > PREVIEW ? `${r.text.slice(0, PREVIEW).trimEnd()}…` : r.text,
|
|
598
657
|
chars: r.text.length,
|
|
599
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 } : {}),
|
|
600
662
|
...(isRetired(r.status) ? { status: r.status, ...(r.revised ? { revised: dia(r.revised) } : {}), ...(r.revision_note ? { retired_note: r.revision_note } : {}) } : {}),
|
|
601
663
|
})),
|
|
602
664
|
entries_pagination: {
|
|
@@ -677,7 +739,7 @@ function createServer(shared) {
|
|
|
677
739
|
// ═══════════════════════════════════════════════════════════════
|
|
678
740
|
server.registerTool('crbro_learn', {
|
|
679
741
|
title: 'Learn something',
|
|
680
|
-
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).',
|
|
681
743
|
inputSchema: {
|
|
682
744
|
// Optional since 2.0.3, and the reason is measured: the description
|
|
683
745
|
// told callers that neuron_id "skips name matching entirely", the
|
|
@@ -688,7 +750,7 @@ function createServer(shared) {
|
|
|
688
750
|
// message that says what to pass.
|
|
689
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.'),
|
|
690
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."'),
|
|
691
|
-
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.'),
|
|
692
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).'),
|
|
693
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).'),
|
|
694
756
|
rationale: zod_1.z.string().optional().describe('Why the decision was taken. Stored and indexed with it; ignored for other types.'),
|
|
@@ -696,6 +758,7 @@ function createServer(shared) {
|
|
|
696
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.'),
|
|
697
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.'),
|
|
698
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.'),
|
|
699
762
|
},
|
|
700
763
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
701
764
|
}, async (args) => {
|
|
@@ -723,6 +786,7 @@ function createServer(shared) {
|
|
|
723
786
|
supersedes: args.supersedes,
|
|
724
787
|
keys: args.keywords,
|
|
725
788
|
keysReplace: args.keywords_replace,
|
|
789
|
+
shelfLife: args.type === 'fact' ? args.shelf_life : undefined,
|
|
726
790
|
});
|
|
727
791
|
// Indexing happens inside cortex.learn, through the indexer hook.
|
|
728
792
|
if (result.action === 'skipped_retired' && result.skipped_retired) {
|
|
@@ -741,6 +805,13 @@ function createServer(shared) {
|
|
|
741
805
|
if (!result.neuron) {
|
|
742
806
|
return textResult(`No neuron matched "${topic}" and none was created.`);
|
|
743
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;
|
|
744
815
|
const keywordsMissing = args.type === 'fact' && result.action !== 'skipped' &&
|
|
745
816
|
!(args.keywords && args.keywords.some(k => typeof k === 'string' && k.trim().length > 0));
|
|
746
817
|
return jsonResult({
|
|
@@ -784,6 +855,15 @@ function createServer(shared) {
|
|
|
784
855
|
'question may use that the text lacks (synonyms, the other language, the generic name of the product): ' +
|
|
785
856
|
'they merge into this fact instead of adding a new one.'
|
|
786
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,
|
|
787
867
|
total_facts: result.neuron.facts.length,
|
|
788
868
|
total_decisions: result.neuron.decisions.length,
|
|
789
869
|
total_patterns: result.neuron.patterns.length,
|
|
@@ -806,13 +886,13 @@ function createServer(shared) {
|
|
|
806
886
|
// ═══════════════════════════════════════════════════════════════
|
|
807
887
|
server.registerTool('crbro_recall', {
|
|
808
888
|
title: 'Recall',
|
|
809
|
-
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.',
|
|
810
890
|
inputSchema: {
|
|
811
891
|
query: zod_1.z.string().describe('What to look for, e.g. "Firebase authentication setup". Fewer, distinctive terms beat full sentences.'),
|
|
812
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.'),
|
|
813
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.'),
|
|
814
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).'),
|
|
815
|
-
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.'),
|
|
816
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.'),
|
|
817
897
|
},
|
|
818
898
|
outputSchema: {
|
|
@@ -826,11 +906,18 @@ function createServer(shared) {
|
|
|
826
906
|
matched_terms: zod_1.z.number().optional(), query_terms: zod_1.z.number().optional(),
|
|
827
907
|
confidence: zod_1.z.enum(['strong', 'weak']).optional(),
|
|
828
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'),
|
|
829
910
|
origin: zod_1.z.string().optional(), by: zod_1.z.string().optional(),
|
|
830
911
|
content_truncated: zod_1.z.boolean().optional(), content_chars: zod_1.z.number().optional(),
|
|
831
|
-
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(),
|
|
832
913
|
}).loose()),
|
|
833
|
-
|
|
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'),
|
|
834
921
|
matched_neurons: zod_1.z.number().optional().describe('Neurons with any hit before limit; total_results is what came back'),
|
|
835
922
|
has_more: zod_1.z.boolean().optional(),
|
|
836
923
|
sessions_matched: zod_1.z.array(zod_1.z.object({}).loose()).optional(),
|
|
@@ -874,36 +961,94 @@ function createServer(shared) {
|
|
|
874
961
|
...(r.also_matched ? { also_matched: r.also_matched.map(a => ({ ...a, added: dia(a.added) })) } : {}),
|
|
875
962
|
};
|
|
876
963
|
});
|
|
877
|
-
|
|
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
|
+
: '';
|
|
878
1017
|
const payload = {
|
|
1018
|
+
...(avisoRancio ? { stale_warning: avisoRancio } : {}),
|
|
879
1019
|
query: args.query,
|
|
880
|
-
// total_results is what came back (capped by limit);
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
|
|
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,
|
|
884
1025
|
matched_neurons,
|
|
885
1026
|
has_more: sobran > 0,
|
|
886
1027
|
// The filter as it was understood ("30d" becomes a day), so a narrowed
|
|
887
1028
|
// answer never reads as the whole brain.
|
|
888
1029
|
...(filtrado ? { filters: { ...(since ? { since } : {}), ...(kinds ? { kind: kinds } : {}) } } : {}),
|
|
889
1030
|
...(since ? { undated_skipped: sinFecha } : {}),
|
|
890
|
-
results:
|
|
1031
|
+
results: actuales,
|
|
1032
|
+
...(rancios.length ? { possibly_stale: rancios, possibly_stale_count: rancios.length } : {}),
|
|
891
1033
|
// The diary, searched since 2.2: day logs whose summary mentions the
|
|
892
1034
|
// question, in a list of their own so narrative never outranks a fact.
|
|
893
1035
|
// sessions_total: how many days had a hit; three shown never reads as three.
|
|
894
1036
|
...(sessions.length ? { sessions_matched: sessions.map(s => ({ ...s, date: dia(s.date) })), sessions_total } : {}),
|
|
895
|
-
hint: (
|
|
896
|
-
? (
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
: '
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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.' : '')
|
|
907
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).` : '')
|
|
908
1053
|
+ (sessions_total > sessions.length ? ` ${sessions_total - sessions.length} more day${sessions_total - sessions.length === 1 ? '' : 's'} mention it: narrow the query.` : ''),
|
|
909
1054
|
};
|
|
@@ -928,9 +1073,9 @@ function createServer(shared) {
|
|
|
928
1073
|
const reviseSchema = zod_1.z.object({
|
|
929
1074
|
neuron: zod_1.z.string().describe('Neuron id or name holding what to revise, e.g. "project_octochat".'),
|
|
930
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.'),
|
|
931
|
-
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.'),
|
|
932
|
-
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
|
|
933
|
-
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.'),
|
|
934
1079
|
summary: zod_1.z.string().optional().describe('Replace the neuron summary. Credentials are redacted and listed in redacted.'),
|
|
935
1080
|
domain: zod_1.z.string().optional().describe('Replace the neuron domain unconditionally, e.g. "proyectos-web".'),
|
|
936
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.'),
|
|
@@ -950,7 +1095,7 @@ function createServer(shared) {
|
|
|
950
1095
|
});
|
|
951
1096
|
server.registerTool('crbro_revise', {
|
|
952
1097
|
title: 'Revise a neuron',
|
|
953
|
-
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.',
|
|
954
1099
|
inputSchema: reviseSchema,
|
|
955
1100
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
956
1101
|
}, async (args) => {
|
|
@@ -989,6 +1134,42 @@ function createServer(shared) {
|
|
|
989
1134
|
: 'Nothing matched, nothing moved. Pass entry ids from crbro_inspect view=neuron, or the exact text.',
|
|
990
1135
|
});
|
|
991
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
|
+
}
|
|
992
1173
|
let revisedFacts = 0;
|
|
993
1174
|
let revisedEntries = 0;
|
|
994
1175
|
const unmatched = [];
|