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.
Files changed (65) hide show
  1. package/README.md +22 -13
  2. package/SECURITY.md +27 -2
  3. package/bin/crbro.mjs +18 -1
  4. package/dist/engine/backup.d.ts.map +1 -1
  5. package/dist/engine/backup.js +4 -0
  6. package/dist/engine/backup.js.map +1 -1
  7. package/dist/engine/cortex.d.ts +18 -6
  8. package/dist/engine/cortex.d.ts.map +1 -1
  9. package/dist/engine/cortex.js +99 -83
  10. package/dist/engine/cortex.js.map +1 -1
  11. package/dist/engine/doctor.d.ts +93 -0
  12. package/dist/engine/doctor.d.ts.map +1 -0
  13. package/dist/engine/doctor.js +378 -0
  14. package/dist/engine/doctor.js.map +1 -0
  15. package/dist/engine/evidence.d.ts +26 -0
  16. package/dist/engine/evidence.d.ts.map +1 -0
  17. package/dist/engine/evidence.js +171 -0
  18. package/dist/engine/evidence.js.map +1 -0
  19. package/dist/engine/hippocampus.d.ts.map +1 -1
  20. package/dist/engine/hippocampus.js +32 -29
  21. package/dist/engine/hippocampus.js.map +1 -1
  22. package/dist/engine/keychain.d.ts.map +1 -1
  23. package/dist/engine/keychain.js +52 -20
  24. package/dist/engine/keychain.js.map +1 -1
  25. package/dist/engine/prefrontal.d.ts +4 -1
  26. package/dist/engine/prefrontal.d.ts.map +1 -1
  27. package/dist/engine/prefrontal.js +100 -66
  28. package/dist/engine/prefrontal.js.map +1 -1
  29. package/dist/engine/projects.d.ts +20 -0
  30. package/dist/engine/projects.d.ts.map +1 -0
  31. package/dist/engine/projects.js +80 -0
  32. package/dist/engine/projects.js.map +1 -0
  33. package/dist/engine/shelf.d.ts +5 -6
  34. package/dist/engine/shelf.d.ts.map +1 -1
  35. package/dist/engine/shelf.js +9 -8
  36. package/dist/engine/shelf.js.map +1 -1
  37. package/dist/search/index.d.ts +5 -0
  38. package/dist/search/index.d.ts.map +1 -1
  39. package/dist/search/index.js +18 -5
  40. package/dist/search/index.js.map +1 -1
  41. package/dist/search/quality.d.ts +18 -0
  42. package/dist/search/quality.d.ts.map +1 -0
  43. package/dist/search/quality.js +31 -0
  44. package/dist/search/quality.js.map +1 -0
  45. package/dist/server.d.ts.map +1 -1
  46. package/dist/server.js +112 -16
  47. package/dist/server.js.map +1 -1
  48. package/dist/sync/materialize.d.ts +1 -0
  49. package/dist/sync/materialize.d.ts.map +1 -1
  50. package/dist/sync/materialize.js +33 -8
  51. package/dist/sync/materialize.js.map +1 -1
  52. package/dist/sync/ops.d.ts +7 -2
  53. package/dist/sync/ops.d.ts.map +1 -1
  54. package/dist/sync/ops.js.map +1 -1
  55. package/dist/sync/space.d.ts +5 -3
  56. package/dist/sync/space.d.ts.map +1 -1
  57. package/dist/sync/space.js +187 -77
  58. package/dist/sync/space.js.map +1 -1
  59. package/dist/types/index.d.ts +25 -2
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/utils/lock.d.ts +2 -0
  62. package/dist/utils/lock.d.ts.map +1 -1
  63. package/dist/utils/lock.js +132 -52
  64. package/dist/utils/lock.js.map +1 -1
  65. 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('The folder or repository this session works in: a path ("C:/code/crbro-memory"), a repo name or a remote ("owner/repo"); only the last segment counts. Neurons whose name, system map or fact keywords name it come first in hot_topics and are listed in project_neurons (at most 5). Omit it and boot is exactly as without.'),
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 (result.active_context && typeof result.active_context === 'object') {
303
- const { pending_tasks: _p, recently_closed: _r, ...resto } = result.active_context;
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: 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).',
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
- // The same line learned again, and nothing else: its shelf-life clock restarts now.
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 { results, matched_neurons, sessions, sessions_total, undated_skipped } = await searchEngine.searchManyWithStats([args.query, ...(args.queries || [])], { domain: args.domain, limit: args.limit ?? 5, since, kinds });
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 the working context: active topics, open items, recently closed, last session. Called with no arguments it only reads (written:false, nothing touched); any argument writes and returns the full state plus resolved and discarded. Close items as soon as they are done — resolve_pending records them in recently_closed, discard_pending drops one without recording it, clear empties everything — because an item left open is repeated back to the user in later sessions long after it was finished. Sessions are logged by crbro_consolidate, not here.',
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
- set_topics: zod_1.z.array(zod_1.z.string()).optional().describe('Replace the whole active-topics list with these neuron ids (no merge). crbro_consolidate also rewrites it from the session.'),
1454
- add_pending: zod_1.z.string().optional().describe('Add an open item, written so it can be checked later. Identical text is deduplicated, so re-adding is a safe no-op.'),
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,