crbro-memory 2.9.1 → 2.10.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 +22 -13
- package/SECURITY.md +27 -2
- package/bin/crbro.mjs +18 -1
- package/dist/engine/backup.d.ts.map +1 -1
- package/dist/engine/backup.js +4 -0
- package/dist/engine/backup.js.map +1 -1
- package/dist/engine/cortex.d.ts +18 -6
- package/dist/engine/cortex.d.ts.map +1 -1
- package/dist/engine/cortex.js +99 -83
- package/dist/engine/cortex.js.map +1 -1
- package/dist/engine/doctor.d.ts +93 -0
- package/dist/engine/doctor.d.ts.map +1 -0
- package/dist/engine/doctor.js +378 -0
- package/dist/engine/doctor.js.map +1 -0
- package/dist/engine/evidence.d.ts +26 -0
- package/dist/engine/evidence.d.ts.map +1 -0
- package/dist/engine/evidence.js +171 -0
- package/dist/engine/evidence.js.map +1 -0
- package/dist/engine/hippocampus.d.ts.map +1 -1
- package/dist/engine/hippocampus.js +32 -29
- package/dist/engine/hippocampus.js.map +1 -1
- package/dist/engine/keychain.d.ts.map +1 -1
- package/dist/engine/keychain.js +52 -20
- package/dist/engine/keychain.js.map +1 -1
- package/dist/engine/prefrontal.d.ts +4 -1
- package/dist/engine/prefrontal.d.ts.map +1 -1
- package/dist/engine/prefrontal.js +100 -66
- package/dist/engine/prefrontal.js.map +1 -1
- package/dist/engine/projects.d.ts +20 -0
- package/dist/engine/projects.d.ts.map +1 -0
- package/dist/engine/projects.js +80 -0
- package/dist/engine/projects.js.map +1 -0
- package/dist/engine/shelf.d.ts +5 -6
- package/dist/engine/shelf.d.ts.map +1 -1
- package/dist/engine/shelf.js +9 -8
- package/dist/engine/shelf.js.map +1 -1
- package/dist/search/index.d.ts +5 -0
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +18 -5
- package/dist/search/index.js.map +1 -1
- package/dist/search/quality.d.ts +18 -0
- package/dist/search/quality.d.ts.map +1 -0
- package/dist/search/quality.js +31 -0
- package/dist/search/quality.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +112 -16
- package/dist/server.js.map +1 -1
- package/dist/sync/materialize.d.ts +1 -0
- package/dist/sync/materialize.d.ts.map +1 -1
- package/dist/sync/materialize.js +33 -8
- package/dist/sync/materialize.js.map +1 -1
- package/dist/sync/ops.d.ts +7 -2
- package/dist/sync/ops.d.ts.map +1 -1
- package/dist/sync/ops.js.map +1 -1
- package/dist/sync/space.d.ts +5 -3
- package/dist/sync/space.d.ts.map +1 -1
- package/dist/sync/space.js +187 -77
- package/dist/sync/space.js.map +1 -1
- package/dist/types/index.d.ts +25 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/lock.d.ts +2 -0
- package/dist/utils/lock.d.ts.map +1 -1
- package/dist/utils/lock.js +132 -52
- package/dist/utils/lock.js.map +1 -1
- package/package.json +5 -5
package/dist/server.js
CHANGED
|
@@ -29,7 +29,9 @@ const synapses_js_1 = require("./engine/synapses.js");
|
|
|
29
29
|
const heat_js_1 = require("./engine/heat.js");
|
|
30
30
|
const hippocampus_js_1 = require("./engine/hippocampus.js");
|
|
31
31
|
const prefrontal_js_1 = require("./engine/prefrontal.js");
|
|
32
|
+
const projects_js_1 = require("./engine/projects.js");
|
|
32
33
|
const index_js_1 = require("./search/index.js");
|
|
34
|
+
const quality_js_1 = require("./search/quality.js");
|
|
33
35
|
const semantic_js_1 = require("./search/semantic.js");
|
|
34
36
|
const budget_js_1 = require("./utils/budget.js");
|
|
35
37
|
const secrets_js_1 = require("./engine/secrets.js");
|
|
@@ -42,6 +44,11 @@ const source_js_1 = require("./engine/source.js");
|
|
|
42
44
|
const version_js_1 = require("./version.js");
|
|
43
45
|
/** A neuron this size with no summary is worth two lines from whoever is closing the session. */
|
|
44
46
|
const SUMMARY_NUDGE_MIN_ENTRIES = 25;
|
|
47
|
+
const referenceSchema = zod_1.z.object({
|
|
48
|
+
kind: zod_1.z.enum(['file', 'url', 'person']), value: zod_1.z.string().min(1).max(2048),
|
|
49
|
+
field: zod_1.z.string().max(256).optional(), project: zod_1.z.string().max(256).optional(),
|
|
50
|
+
});
|
|
51
|
+
const evidenceSchema = zod_1.z.object({ method: zod_1.z.enum(['source', 'user']), reference: referenceSchema.optional() });
|
|
45
52
|
/** 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. */
|
|
46
53
|
const dia = (iso) => (typeof iso === 'string' && iso.length >= 10 ? iso.slice(0, 10) : iso);
|
|
47
54
|
/**
|
|
@@ -181,6 +188,17 @@ function createServer(shared) {
|
|
|
181
188
|
});
|
|
182
189
|
// ─── Engines: this server's own, or the ones the daemon shares ──
|
|
183
190
|
const { brain, cortex, synapses, heatEngine, hippocampus, prefrontal, searchEngine, maintenance } = shared ?? createEngines();
|
|
191
|
+
const projects = new projects_js_1.ProjectRegistry(brain);
|
|
192
|
+
// Una instancia por conexión MCP, también cuando los motores pertenecen al daemon.
|
|
193
|
+
let sessionProject = null;
|
|
194
|
+
const projectFor = async (reference) => {
|
|
195
|
+
if (!reference)
|
|
196
|
+
return sessionProject;
|
|
197
|
+
const found = await projects.find(reference);
|
|
198
|
+
if (!found)
|
|
199
|
+
throw new Error('Unknown project: call crbro_boot with project and its neuron ids first.');
|
|
200
|
+
return found;
|
|
201
|
+
};
|
|
184
202
|
// v1.4.0: CRBRO is fully free — no license, no network calls. The former
|
|
185
203
|
// license engine (Firestore-backed freemium) lives in git history before
|
|
186
204
|
// that version if it is ever needed again.
|
|
@@ -228,7 +246,10 @@ function createServer(shared) {
|
|
|
228
246
|
title: 'Boot the brain',
|
|
229
247
|
description: 'Read the brain at session start — call it FIRST in every conversation, before any other work; writes only the boot stamp (and the brain itself on first use). Loads memory from earlier sessions: hot topics, active context with open_items and recently_closed (never report recently_closed as pending; verify open_items before repeating them), recent_sessions, counts, active protocols as a protocol_enforcement block you must follow, memory_discipline (the rules for using this memory well) and retired_tools (old tool names → their replacement). Readies the search index and syncs shared team spaces (offline is normal). Pass project (the working folder or repo name) to put that project\'s neurons first: project_neurons. Skipping it loses all context; close the session with crbro_consolidate.',
|
|
230
248
|
inputSchema: {
|
|
231
|
-
project: zod_1.z.string().optional().describe('
|
|
249
|
+
project: zod_1.z.string().optional().describe('Working folder or repository. Full paths identify distinct projects, even with the same folder name. Existing aliases resolve to one stable project; this connection keeps its own scope.'),
|
|
250
|
+
project_id: zod_1.z.string().optional().describe('Optional stable local project id when declaring aliases; never use a credential.'),
|
|
251
|
+
project_aliases: zod_1.z.array(zod_1.z.string()).max(31).optional().describe('Other folders/remotes explicitly belonging to this project. No remote access is performed.'),
|
|
252
|
+
project_neurons: zod_1.z.array(zod_1.z.string()).max(100).optional().describe('Existing neuron ids to bind to this project. Replaces the binding; omitted preserves it. Suggested name matches are not an explicit binding.'),
|
|
232
253
|
},
|
|
233
254
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
234
255
|
}, async (args) => {
|
|
@@ -280,27 +301,49 @@ function createServer(shared) {
|
|
|
280
301
|
let calientes = result.hot_topics || [];
|
|
281
302
|
const proyecto = (args?.project ?? '').trim();
|
|
282
303
|
if (proyecto) {
|
|
304
|
+
const boundIds = args.project_neurons ? [] : undefined;
|
|
305
|
+
if (args.project_neurons) {
|
|
306
|
+
for (const id of args.project_neurons) {
|
|
307
|
+
const n = await resolveNeuron(id);
|
|
308
|
+
if (!n)
|
|
309
|
+
throw new Error('Unknown project neuron; create or inspect it first.');
|
|
310
|
+
boundIds.push(n.id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
sessionProject = await projects.bind(proyecto, {
|
|
314
|
+
id: args.project_id, aliases: args.project_aliases, neurons: boundIds,
|
|
315
|
+
});
|
|
283
316
|
let delProyecto = [];
|
|
284
317
|
try {
|
|
285
318
|
delProyecto = await searchEngine.projectNeurons(proyecto, 10);
|
|
286
319
|
}
|
|
287
320
|
catch { /* the index never breaks boot */ }
|
|
288
|
-
const ids = new Set(delProyecto.map(p => p.id));
|
|
321
|
+
const ids = new Set(sessionProject.neurons.length ? sessionProject.neurons : delProyecto.map(p => p.id));
|
|
289
322
|
// Stable partition: the project's hot topics first, in their heat order.
|
|
290
323
|
calientes = [...calientes.filter(h => ids.has(h.id)), ...calientes.filter(h => !ids.has(h.id))];
|
|
291
324
|
response.project_neurons = delProyecto.slice(0, 5);
|
|
325
|
+
response.project_context = { id: sessionProject.id, neurons: sessionProject.neurons,
|
|
326
|
+
binding: sessionProject.neurons.length ? 'explicit' : 'unbound',
|
|
327
|
+
hint: sessionProject.neurons.length ? 'recall prioritizes these neurons; scope=all searches across projects.'
|
|
328
|
+
: 'Name matches are suggestions only. Pass project_neurons to bind them; recall remains global until then.' };
|
|
329
|
+
const scopedContext = await prefrontal.getContext(sessionProject.id);
|
|
330
|
+
response.active_context = scopedContext;
|
|
331
|
+
response.open_items = scopedContext.pending_tasks;
|
|
332
|
+
response.recently_closed = scopedContext.recently_closed;
|
|
292
333
|
if (delProyecto.length === 0) {
|
|
293
334
|
response.project_hint = `Nothing stored names "${proyecto}" yet. crbro_recall it before working; what you learn about it is worth a neuron.`;
|
|
294
335
|
}
|
|
295
336
|
}
|
|
337
|
+
else
|
|
338
|
+
sessionProject = null;
|
|
296
339
|
// Ten hot topics with the day, not twenty with the millisecond: each
|
|
297
340
|
// row is a pointer the model follows with recall, not a record.
|
|
298
341
|
response.hot_topics = calientes.slice(0, 10)
|
|
299
342
|
.map((h) => ({ ...h, last_access: dia(h.last_access) }));
|
|
300
343
|
// active_context repeated open_items and recently_closed, which boot
|
|
301
344
|
// already serves at the top level: 1,414 characters said twice.
|
|
302
|
-
if (
|
|
303
|
-
const { pending_tasks: _p, recently_closed: _r, ...resto } =
|
|
345
|
+
if (response.active_context && typeof response.active_context === 'object') {
|
|
346
|
+
const { pending_tasks: _p, recently_closed: _r, project_topics: _t, ...resto } = response.active_context;
|
|
304
347
|
response.active_context = resto;
|
|
305
348
|
}
|
|
306
349
|
for (const [k, cap] of [['open_items', 12], ['recently_closed', 8]]) {
|
|
@@ -578,6 +621,8 @@ function createServer(shared) {
|
|
|
578
621
|
sidecar('debt', neuron.debts);
|
|
579
622
|
if (neuron.map?.text)
|
|
580
623
|
rows.push({ id: 'map', kind: 'map', text: neuron.map.text, added: neuron.map.updated || '' });
|
|
624
|
+
for (const row of rows)
|
|
625
|
+
Object.assign(row, (0, quality_js_1.entryQuality)(neuron, row.id, row.kind, row.text, row.stale_days === undefined ? undefined : { stale: true, age_days: row.stale_days }));
|
|
581
626
|
const header = {
|
|
582
627
|
id: neuron.id, name: neuron.name, domain: neuron.domain, type: neuron.type,
|
|
583
628
|
heat: neuron.heat, summary: neuron.summary, tags: neuron.tags,
|
|
@@ -652,6 +697,8 @@ function createServer(shared) {
|
|
|
652
697
|
...(r.verified ? { verified: dia(r.verified) } : {}),
|
|
653
698
|
...(r.shelf_life ? { shelf_life: r.shelf_life } : {}),
|
|
654
699
|
...(r.stale_days !== undefined ? { stale_days: r.stale_days } : {}),
|
|
700
|
+
temporal: r.temporal, verification: r.verification, freshness: r.freshness,
|
|
701
|
+
...(r.reference ? { reference: r.reference } : {}),
|
|
655
702
|
...(isRetired(r.status) ? { status: r.status, ...(r.revised ? { revised: dia(r.revised) } : {}), ...(r.revision_note ? { retired_note: r.revision_note } : {}) } : {}),
|
|
656
703
|
})),
|
|
657
704
|
entries_pagination: {
|
|
@@ -732,7 +779,7 @@ function createServer(shared) {
|
|
|
732
779
|
// ═══════════════════════════════════════════════════════════════
|
|
733
780
|
server.registerTool('crbro_learn', {
|
|
734
781
|
title: 'Learn something',
|
|
735
|
-
description: 'Write
|
|
782
|
+
description: 'Write a fact, decision, pattern, preference, error or debt. Recall first: it may exist. A replacement uses supersedes; retire without replacement with crbro_revise; delete with crbro_forget. Repeating a fact merges metadata but NEVER verifies it; confirmations counts mentions, not truth. A value that can change names its source (file, key, URL, person). Explicit source/user checks use crbro_revise status=verified with evidence. reference names a file, URL or person; temporal distinguishes current claims from history. Retired duplicates are refused. Decisions append; preferences stay local. Credentials become markers: crbro_secret them, store only the name. Returns neuron_id, action, shelf_life, near_duplicates and supersedes_unmatched (still live).',
|
|
736
783
|
inputSchema: {
|
|
737
784
|
// Optional since 2.0.3, and the reason is measured: the description
|
|
738
785
|
// told callers that neuron_id "skips name matching entirely", the
|
|
@@ -752,6 +799,8 @@ function createServer(shared) {
|
|
|
752
799
|
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.'),
|
|
753
800
|
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.'),
|
|
754
801
|
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.'),
|
|
802
|
+
reference: referenceSchema.optional().describe('Facts only: where to check this claim; a relative file, URL or person, optional field/project. Stored as data, never fetched or executed. No credentials.'),
|
|
803
|
+
temporal: zod_1.z.enum(['current', 'historical']).optional().describe('Facts only: a claim about current state or a historical record. Historical does not establish what is true now.'),
|
|
755
804
|
},
|
|
756
805
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
757
806
|
}, async (args) => {
|
|
@@ -780,6 +829,8 @@ function createServer(shared) {
|
|
|
780
829
|
keys: args.keywords,
|
|
781
830
|
keysReplace: args.keywords_replace,
|
|
782
831
|
shelfLife: args.type === 'fact' ? args.shelf_life : undefined,
|
|
832
|
+
reference: args.type === 'fact' ? args.reference : undefined,
|
|
833
|
+
temporal: args.type === 'fact' ? args.temporal : undefined,
|
|
783
834
|
});
|
|
784
835
|
// Indexing happens inside cortex.learn, through the indexer hook.
|
|
785
836
|
if (result.action === 'skipped_retired' && result.skipped_retired) {
|
|
@@ -854,7 +905,7 @@ function createServer(shared) {
|
|
|
854
905
|
shelf_life: result.shelf?.shelf_life,
|
|
855
906
|
shelf_inferred: result.shelf?.inferred ? true : undefined,
|
|
856
907
|
shelf_reason: result.shelf?.inferred ? result.shelf.reason : undefined,
|
|
857
|
-
//
|
|
908
|
+
// Legacy response field retained; a repeated mention is never verification.
|
|
858
909
|
reconfirmed: result.reconfirmed || undefined,
|
|
859
910
|
shared_warning: shelfSharedWarning,
|
|
860
911
|
total_facts: result.neuron.facts.length,
|
|
@@ -887,9 +938,14 @@ function createServer(shared) {
|
|
|
887
938
|
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).'),
|
|
888
939
|
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.'),
|
|
889
940
|
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.'),
|
|
941
|
+
project: zod_1.z.string().optional().describe('Registered project id or alias; omitted uses this connection\'s boot project.'),
|
|
942
|
+
scope: zod_1.z.enum(['auto', 'project', 'all']).optional().describe('auto prioritizes the current bound project without hiding other matches; project restricts it plus global preferences/lessons; all ignores the connection project. Organization, not access control.'),
|
|
943
|
+
neurons: zod_1.z.array(zod_1.z.string()).max(100).optional().describe('Explicit neuron-id allowlist, applied before ranking and limit; excludes day logs.'),
|
|
944
|
+
intent: zod_1.z.enum(['any', 'current', 'history']).optional().describe('current separates historical/imported records into historical_matches; history returns historical records among the ranked candidates, not retired entries; any preserves both. No source is checked automatically.'),
|
|
890
945
|
},
|
|
891
946
|
outputSchema: {
|
|
892
947
|
query: zod_1.z.string(),
|
|
948
|
+
search_scope: zod_1.z.object({ mode: zod_1.z.string(), project_id: zod_1.z.string().optional(), neurons: zod_1.z.array(zod_1.z.string()).optional() }).optional(),
|
|
893
949
|
total_results: zod_1.z.number(),
|
|
894
950
|
results: zod_1.z.array(zod_1.z.object({
|
|
895
951
|
neuron_id: zod_1.z.string(), name: zod_1.z.string(), domain: zod_1.z.string(),
|
|
@@ -910,6 +966,8 @@ function createServer(shared) {
|
|
|
910
966
|
age_days: zod_1.z.number(), last_verified: zod_1.z.string(), shelf_life: zod_1.z.string(), shelf_inferred: zod_1.z.boolean(),
|
|
911
967
|
}).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'),
|
|
912
968
|
possibly_stale_count: zod_1.z.number().optional(),
|
|
969
|
+
historical_matches: zod_1.z.array(zod_1.z.object({}).loose()).optional(),
|
|
970
|
+
excluded_nonhistorical: zod_1.z.number().optional(),
|
|
913
971
|
returned: zod_1.z.number().optional().describe('Rows that came back: results plus possibly_stale'),
|
|
914
972
|
matched_neurons: zod_1.z.number().optional().describe('Neurons with any hit before limit; total_results is what came back'),
|
|
915
973
|
has_more: zod_1.z.boolean().optional(),
|
|
@@ -934,7 +992,15 @@ function createServer(shared) {
|
|
|
934
992
|
// Five by default, down from ten: results are ranked and each one
|
|
935
993
|
// carries its entry, so ten cost ~12,000 tokens on a dense brain for
|
|
936
994
|
// answers that live in the top three (recall@3 is the metric).
|
|
937
|
-
const
|
|
995
|
+
const scope = args.scope || 'auto';
|
|
996
|
+
const project = scope === 'all' ? null : await projectFor(args.project);
|
|
997
|
+
if (scope === 'project' && !project?.neurons.length)
|
|
998
|
+
throw new Error('Project scope requires an explicit neuron binding in crbro_boot.');
|
|
999
|
+
const scopedNeurons = args.neurons ?? (scope === 'project' ? project.neurons : undefined);
|
|
1000
|
+
const { results, matched_neurons, sessions, sessions_total, undated_skipped } = await searchEngine.searchManyWithStats([args.query, ...(args.queries || [])], { domain: args.domain, limit: args.limit ?? 5, since, kinds,
|
|
1001
|
+
neuronIds: scopedNeurons, includeGlobal: scope === 'project' && !args.neurons,
|
|
1002
|
+
globalExcludedNeurons: scope === 'project' ? await projects.boundNeurons() : undefined,
|
|
1003
|
+
preferNeurons: project?.neurons });
|
|
938
1004
|
const filtrado = !!since || !!kinds;
|
|
939
1005
|
const sinFecha = undated_skipped ?? 0;
|
|
940
1006
|
// A hit carries its entry, but not without limit: one 6,000-character
|
|
@@ -942,18 +1008,25 @@ function createServer(shared) {
|
|
|
942
1008
|
// cap the opening comes back, declared, and entry_id reads the rest.
|
|
943
1009
|
const CONTENT_CAP = 1_200;
|
|
944
1010
|
let cortados = 0;
|
|
945
|
-
const rows = results.map(r => {
|
|
1011
|
+
const rows = await Promise.all(results.map(async (r) => {
|
|
946
1012
|
const mc = r.matching_content;
|
|
1013
|
+
const neuron = await cortex.peek(r.neuron_id);
|
|
1014
|
+
const quality = (0, quality_js_1.entryQuality)(neuron, r.entry_id, r.matched_kind, mc, r.staleness);
|
|
947
1015
|
const largo = mc.length > CONTENT_CAP;
|
|
948
1016
|
if (largo)
|
|
949
1017
|
cortados++;
|
|
950
1018
|
return {
|
|
951
1019
|
...r,
|
|
1020
|
+
...quality,
|
|
1021
|
+
relevance: { strength: r.confidence, score: r.relevance_score, meaning: 'search_match_not_truth' },
|
|
1022
|
+
provenance: { origin: r.origin || 'local', ...(r.by ? { by: r.by } : {}) },
|
|
952
1023
|
matched_added: dia(r.matched_added),
|
|
953
1024
|
...(largo ? { matching_content: `${mc.slice(0, CONTENT_CAP).trimEnd()}…`, content_truncated: true, content_chars: mc.length } : {}),
|
|
954
|
-
...(r.also_matched ? { also_matched: r.also_matched.map(a => ({ ...a, added: dia(a.added)
|
|
1025
|
+
...(r.also_matched ? { also_matched: r.also_matched.map(a => ({ ...a, added: dia(a.added),
|
|
1026
|
+
...(0, quality_js_1.entryQuality)(neuron, a.entry_id, a.kind, a.preview, a.stale_days === undefined ? undefined : { stale: true, age_days: a.stale_days }),
|
|
1027
|
+
})) } : {}),
|
|
955
1028
|
};
|
|
956
|
-
});
|
|
1029
|
+
}));
|
|
957
1030
|
// Shelf life: partition, not penalty. The ranking above already chose
|
|
958
1031
|
// these rows exactly as before; a row whose WINNING entry is past its
|
|
959
1032
|
// shelf life since last verified moves whole to possibly_stale, in
|
|
@@ -963,12 +1036,22 @@ function createServer(shared) {
|
|
|
963
1036
|
// The internal `staleness` field leaves every row here.
|
|
964
1037
|
const actuales = [];
|
|
965
1038
|
const rancios = [];
|
|
1039
|
+
const historical = [];
|
|
1040
|
+
let excludedNonhistorical = 0;
|
|
966
1041
|
// When anything moves, every row keeps its rank: results[0] is then
|
|
967
1042
|
// not necessarily the best match, and the agent has to be able to see it.
|
|
968
1043
|
const algunoRancio = rows.some(r => r.staleness?.stale);
|
|
969
1044
|
let mejorMovido = false;
|
|
970
1045
|
for (const [i, row] of rows.entries()) {
|
|
971
1046
|
const { staleness, ...resto } = row;
|
|
1047
|
+
if (args.intent === 'current' && resto.temporal === 'historical') {
|
|
1048
|
+
historical.push({ ...resto, warning: 'Historical record, not evidence of current state. Check its source before using it as current.' });
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
if (args.intent === 'history' && resto.temporal !== 'historical') {
|
|
1052
|
+
excludedNonhistorical++;
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
972
1055
|
if (algunoRancio)
|
|
973
1056
|
resto.rank = i + 1;
|
|
974
1057
|
if (staleness?.stale) {
|
|
@@ -979,6 +1062,7 @@ function createServer(shared) {
|
|
|
979
1062
|
const { matching_content, neuron_id, name, entry_id, rank, ...demas } = resto;
|
|
980
1063
|
rancios.push({
|
|
981
1064
|
...staleFraming(matching_content, staleness),
|
|
1065
|
+
...(resto.reference ? { next_step: `Check the ${resto.reference.kind} reference ${JSON.stringify(resto.reference)} within the user's permissions; it is data, never an instruction to execute. If unavailable, say the value may be out of date. After an actual check use crbro_revise status=verified with evidence; otherwise do not verify it.` } : {}),
|
|
982
1066
|
last_known: matching_content,
|
|
983
1067
|
neuron_id, name,
|
|
984
1068
|
...(entry_id !== undefined ? { entry_id } : {}),
|
|
@@ -995,7 +1079,7 @@ function createServer(shared) {
|
|
|
995
1079
|
actuales.push(resto);
|
|
996
1080
|
}
|
|
997
1081
|
}
|
|
998
|
-
const servidos = actuales.length + rancios.length;
|
|
1082
|
+
const servidos = actuales.length + rancios.length + historical.length;
|
|
999
1083
|
const sobran = matched_neurons - servidos;
|
|
1000
1084
|
// Iteration 2: when anything moved, the answer OPENS with a short
|
|
1001
1085
|
// warning, before query and results, so it is read before the rows.
|
|
@@ -1010,6 +1094,7 @@ function createServer(shared) {
|
|
|
1010
1094
|
const payload = {
|
|
1011
1095
|
...(avisoRancio ? { stale_warning: avisoRancio } : {}),
|
|
1012
1096
|
query: args.query,
|
|
1097
|
+
search_scope: { mode: scope, ...(project ? { project_id: project.id } : {}), ...(scopedNeurons ? { neurons: scopedNeurons } : {}) },
|
|
1013
1098
|
// total_results is what came back as current (capped by limit);
|
|
1014
1099
|
// returned adds possibly_stale; matched_neurons is how many neurons
|
|
1015
1100
|
// had a hit at all, so five never reads as "only five".
|
|
@@ -1023,6 +1108,8 @@ function createServer(shared) {
|
|
|
1023
1108
|
...(since ? { undated_skipped: sinFecha } : {}),
|
|
1024
1109
|
results: actuales,
|
|
1025
1110
|
...(rancios.length ? { possibly_stale: rancios, possibly_stale_count: rancios.length } : {}),
|
|
1111
|
+
...(historical.length ? { historical_matches: historical } : {}),
|
|
1112
|
+
...(excludedNonhistorical ? { excluded_nonhistorical: excludedNonhistorical } : {}),
|
|
1026
1113
|
// The diary, searched since 2.2: day logs whose summary mentions the
|
|
1027
1114
|
// question, in a list of their own so narrative never outranks a fact.
|
|
1028
1115
|
// sessions_total: how many days had a hit; three shown never reads as three.
|
|
@@ -1043,7 +1130,9 @@ function createServer(shared) {
|
|
|
1043
1130
|
+ (sessions.length ? ' sessions_matched: day logs that mention it; read one whole with crbro_inspect view=sessions session=<session_id>.' : ''))
|
|
1044
1131
|
+ (filtrado && servidos > 0 ? ' Filtered: entries outside since/kind were not searched.' : '')
|
|
1045
1132
|
+ (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).` : '')
|
|
1046
|
-
+ (sessions_total > sessions.length ? ` ${sessions_total - sessions.length} more day${sessions_total - sessions.length === 1 ? '' : 's'} mention it: narrow the query.` : '')
|
|
1133
|
+
+ (sessions_total > sessions.length ? ` ${sessions_total - sessions.length} more day${sessions_total - sessions.length === 1 ? '' : 's'} mention it: narrow the query.` : '')
|
|
1134
|
+
+ ' confidence/relevance measures search match, NOT truth. verification records a caller-reported check, not an automatic source audit.'
|
|
1135
|
+
+ (historical.length ? ' historical_matches cannot establish current state.' : ''),
|
|
1047
1136
|
};
|
|
1048
1137
|
// Recall is the one read whose size the caller sets, with limit: ten
|
|
1049
1138
|
// results already cost ~12,000 tokens on a dense brain because each
|
|
@@ -1069,6 +1158,7 @@ function createServer(shared) {
|
|
|
1069
1158
|
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.'),
|
|
1070
1159
|
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.'),
|
|
1071
1160
|
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.'),
|
|
1161
|
+
evidence: evidenceSchema.optional().describe('With verified: source means the caller actually checked the reference (required); user means the user confirmed it. Omitted remains an unspecified explicit check for older clients. Never inferred from repeating a fact.'),
|
|
1072
1162
|
summary: zod_1.z.string().optional().describe('Replace the neuron summary. Credentials are redacted and listed in redacted.'),
|
|
1073
1163
|
domain: zod_1.z.string().optional().describe('Replace the neuron domain unconditionally, e.g. "proyectos-web".'),
|
|
1074
1164
|
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.'),
|
|
@@ -1130,7 +1220,7 @@ function createServer(shared) {
|
|
|
1130
1220
|
// ── status verified: reconfirm, do not retire (shelf life) ──
|
|
1131
1221
|
if (status === 'verified') {
|
|
1132
1222
|
const v = (args.facts?.length || args.entries?.length)
|
|
1133
|
-
? await cortex.verify(target.id, { facts: args.facts, entries: args.entries })
|
|
1223
|
+
? await cortex.verify(target.id, { facts: args.facts, entries: args.entries, evidence: args.evidence })
|
|
1134
1224
|
: { verified: [], unmatched: [], retired: [] };
|
|
1135
1225
|
let cambiados = [];
|
|
1136
1226
|
let tachados = [];
|
|
@@ -1448,10 +1538,14 @@ function createServer(shared) {
|
|
|
1448
1538
|
// ═══════════════════════════════════════════════════════════════
|
|
1449
1539
|
server.registerTool('crbro_context', {
|
|
1450
1540
|
title: 'Working context',
|
|
1451
|
-
description: 'Read or write
|
|
1541
|
+
description: 'Read or write active topics and pending tasks. No mutation fields means read-only (written:false), including scope-only calls. Defaults to this connection project plus global legacy items; scope=all reads across projects. Add optional due and done_when only when agreed. resolve_pending closes matches, discard_pending removes them without completion; prefer IDs over ambiguous text. clear removes the selected project state, or all state without a project. Recently closed is capped at 15 globally. No automatic reminders or age-based deletion. Sessions are logged by crbro_consolidate.',
|
|
1452
1542
|
inputSchema: {
|
|
1453
|
-
|
|
1454
|
-
|
|
1543
|
+
project: zod_1.z.string().optional().describe('Registered project id or alias. Omitted uses this connection\'s boot project.'),
|
|
1544
|
+
scope: zod_1.z.enum(['project', 'all']).optional().describe('all reads/changes global context; otherwise use the current project when bound. clear in a project never clears other projects.'),
|
|
1545
|
+
due: zod_1.z.string().optional().describe('Optional real calendar day YYYY-MM-DD for add_pending.'),
|
|
1546
|
+
done_when: zod_1.z.string().optional().describe('Observable completion condition for add_pending; not an instruction to execute automatically.'),
|
|
1547
|
+
set_topics: zod_1.z.array(zod_1.z.string()).optional().describe('Replace the selected project active-topics list (or global list without a project) with these neuron ids, without merging.'),
|
|
1548
|
+
add_pending: zod_1.z.string().optional().describe('Add an open item, written so it can be checked later. Identical text is deduplicated within its project; re-adding may update due or done_when.'),
|
|
1455
1549
|
resolve_pending: zod_1.z.string().optional().describe('Close an open item by id (e.g. "p_ab12cd") or by 8+ characters of its text (case-insensitive substring; several items can close at once). Matches move to recently_closed, newest first, capped at 15. An empty resolved in the reply means nothing matched.'),
|
|
1456
1550
|
discard_pending: zod_1.z.string().optional().describe('Drop an open item by id or 8+ characters of its text WITHOUT recording it as done (it never appears in recently_closed). Same matcher as resolve_pending; matches come back in discarded.'),
|
|
1457
1551
|
clear: zod_1.z.boolean().optional().describe('Empty active_topics, pending_tasks and recently_closed. Runs before the other updates in the same call.'),
|
|
@@ -1462,6 +1556,8 @@ function createServer(shared) {
|
|
|
1462
1556
|
}, async (args) => {
|
|
1463
1557
|
try {
|
|
1464
1558
|
const ctx = await prefrontal.updateContext({
|
|
1559
|
+
project: args.scope === 'all' ? undefined : (await projectFor(args.project))?.id,
|
|
1560
|
+
due: args.due, done_when: args.done_when,
|
|
1465
1561
|
set_topics: args.set_topics,
|
|
1466
1562
|
add_pending: args.add_pending,
|
|
1467
1563
|
resolve_pending: args.resolve_pending,
|