crbro-memory 2.9.0 → 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 (80) hide show
  1. package/README.md +25 -16
  2. package/SECURITY.md +27 -2
  3. package/bin/crbro.mjs +18 -1
  4. package/dist/daemon/endpoint.d.ts +1 -0
  5. package/dist/daemon/endpoint.d.ts.map +1 -1
  6. package/dist/daemon/endpoint.js +4 -13
  7. package/dist/daemon/endpoint.js.map +1 -1
  8. package/dist/engine/backup.d.ts.map +1 -1
  9. package/dist/engine/backup.js +4 -0
  10. package/dist/engine/backup.js.map +1 -1
  11. package/dist/engine/cortex.d.ts +18 -6
  12. package/dist/engine/cortex.d.ts.map +1 -1
  13. package/dist/engine/cortex.js +119 -92
  14. package/dist/engine/cortex.js.map +1 -1
  15. package/dist/engine/doctor.d.ts +93 -0
  16. package/dist/engine/doctor.d.ts.map +1 -0
  17. package/dist/engine/doctor.js +378 -0
  18. package/dist/engine/doctor.js.map +1 -0
  19. package/dist/engine/evidence.d.ts +26 -0
  20. package/dist/engine/evidence.d.ts.map +1 -0
  21. package/dist/engine/evidence.js +171 -0
  22. package/dist/engine/evidence.js.map +1 -0
  23. package/dist/engine/hippocampus.d.ts.map +1 -1
  24. package/dist/engine/hippocampus.js +32 -29
  25. package/dist/engine/hippocampus.js.map +1 -1
  26. package/dist/engine/keychain.d.ts.map +1 -1
  27. package/dist/engine/keychain.js +52 -20
  28. package/dist/engine/keychain.js.map +1 -1
  29. package/dist/engine/maintenance.d.ts.map +1 -1
  30. package/dist/engine/maintenance.js +3 -3
  31. package/dist/engine/maintenance.js.map +1 -1
  32. package/dist/engine/prefrontal.d.ts +4 -1
  33. package/dist/engine/prefrontal.d.ts.map +1 -1
  34. package/dist/engine/prefrontal.js +100 -66
  35. package/dist/engine/prefrontal.js.map +1 -1
  36. package/dist/engine/projects.d.ts +20 -0
  37. package/dist/engine/projects.d.ts.map +1 -0
  38. package/dist/engine/projects.js +80 -0
  39. package/dist/engine/projects.js.map +1 -0
  40. package/dist/engine/secrets.d.ts.map +1 -1
  41. package/dist/engine/secrets.js +12 -1
  42. package/dist/engine/secrets.js.map +1 -1
  43. package/dist/engine/shelf.d.ts +66 -8
  44. package/dist/engine/shelf.d.ts.map +1 -1
  45. package/dist/engine/shelf.js +350 -12
  46. package/dist/engine/shelf.js.map +1 -1
  47. package/dist/search/index.d.ts +5 -0
  48. package/dist/search/index.d.ts.map +1 -1
  49. package/dist/search/index.js +21 -8
  50. package/dist/search/index.js.map +1 -1
  51. package/dist/search/quality.d.ts +18 -0
  52. package/dist/search/quality.d.ts.map +1 -0
  53. package/dist/search/quality.js +31 -0
  54. package/dist/search/quality.js.map +1 -0
  55. package/dist/server.d.ts.map +1 -1
  56. package/dist/server.js +123 -34
  57. package/dist/server.js.map +1 -1
  58. package/dist/sync/materialize.d.ts +1 -0
  59. package/dist/sync/materialize.d.ts.map +1 -1
  60. package/dist/sync/materialize.js +33 -8
  61. package/dist/sync/materialize.js.map +1 -1
  62. package/dist/sync/ops.d.ts +7 -2
  63. package/dist/sync/ops.d.ts.map +1 -1
  64. package/dist/sync/ops.js.map +1 -1
  65. package/dist/sync/space.d.ts +5 -3
  66. package/dist/sync/space.d.ts.map +1 -1
  67. package/dist/sync/space.js +187 -77
  68. package/dist/sync/space.js.map +1 -1
  69. package/dist/types/index.d.ts +30 -5
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/dist/utils/lock.d.ts +2 -0
  72. package/dist/utils/lock.d.ts.map +1 -1
  73. package/dist/utils/lock.js +132 -52
  74. package/dist/utils/lock.js.map +1 -1
  75. package/dist/version.d.ts +26 -0
  76. package/dist/version.d.ts.map +1 -0
  77. package/dist/version.js +65 -0
  78. package/dist/version.js.map +1 -0
  79. package/hooks/crbro-lifecycle.mjs +612 -608
  80. package/package.json +5 -5
package/dist/server.js CHANGED
@@ -23,15 +23,15 @@ exports.createEngines = createEngines;
23
23
  exports.createServer = createServer;
24
24
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
25
25
  const zod_1 = require("zod");
26
- const node_fs_1 = require("node:fs");
27
- const node_path_1 = require("node:path");
28
26
  const brain_js_1 = require("./engine/brain.js");
29
27
  const cortex_js_1 = require("./engine/cortex.js");
30
28
  const synapses_js_1 = require("./engine/synapses.js");
31
29
  const heat_js_1 = require("./engine/heat.js");
32
30
  const hippocampus_js_1 = require("./engine/hippocampus.js");
33
31
  const prefrontal_js_1 = require("./engine/prefrontal.js");
32
+ const projects_js_1 = require("./engine/projects.js");
34
33
  const index_js_1 = require("./search/index.js");
34
+ const quality_js_1 = require("./search/quality.js");
35
35
  const semantic_js_1 = require("./search/semantic.js");
36
36
  const budget_js_1 = require("./utils/budget.js");
37
37
  const secrets_js_1 = require("./engine/secrets.js");
@@ -41,8 +41,14 @@ const triggers_js_1 = require("./engine/triggers.js");
41
41
  const ids_js_1 = require("./utils/ids.js");
42
42
  const shelf_js_1 = require("./engine/shelf.js");
43
43
  const source_js_1 = require("./engine/source.js");
44
+ const version_js_1 = require("./version.js");
44
45
  /** A neuron this size with no summary is worth two lines from whoever is closing the session. */
45
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() });
46
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. */
47
53
  const dia = (iso) => (typeof iso === 'string' && iso.length >= 10 ? iso.slice(0, 10) : iso);
48
54
  /**
@@ -119,20 +125,9 @@ function staleFraming(text, s) {
119
125
  : `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
126
  return { warning, next_step };
121
127
  }
122
- /**
123
- * The version of CRBRO that is actually running. The manifest carries its own
124
- * version, but that one stamps the brain FORMAT and has not moved since 1.0.0
125
- * — reporting it as "the version" told every user the same thing regardless of
126
- * what they had installed, which is no use to anyone deciding whether to
127
- * update.
128
- */
128
+ /** The version this process runs (read once at load: src/version.ts). */
129
129
  function runningVersion() {
130
- try {
131
- return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '..', 'package.json'), 'utf8')).version;
132
- }
133
- catch {
134
- return 'unknown';
135
- }
130
+ return version_js_1.RUNNING_VERSION;
136
131
  }
137
132
  function textResult(text, isError = false) {
138
133
  return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) };
@@ -193,6 +188,17 @@ function createServer(shared) {
193
188
  });
194
189
  // ─── Engines: this server's own, or the ones the daemon shares ──
195
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
+ };
196
202
  // v1.4.0: CRBRO is fully free — no license, no network calls. The former
197
203
  // license engine (Firestore-backed freemium) lives in git history before
198
204
  // that version if it is ever needed again.
@@ -240,7 +246,10 @@ function createServer(shared) {
240
246
  title: 'Boot the brain',
241
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.',
242
248
  inputSchema: {
243
- 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.'),
244
253
  },
245
254
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
246
255
  }, async (args) => {
@@ -292,27 +301,49 @@ function createServer(shared) {
292
301
  let calientes = result.hot_topics || [];
293
302
  const proyecto = (args?.project ?? '').trim();
294
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
+ });
295
316
  let delProyecto = [];
296
317
  try {
297
318
  delProyecto = await searchEngine.projectNeurons(proyecto, 10);
298
319
  }
299
320
  catch { /* the index never breaks boot */ }
300
- const ids = new Set(delProyecto.map(p => p.id));
321
+ const ids = new Set(sessionProject.neurons.length ? sessionProject.neurons : delProyecto.map(p => p.id));
301
322
  // Stable partition: the project's hot topics first, in their heat order.
302
323
  calientes = [...calientes.filter(h => ids.has(h.id)), ...calientes.filter(h => !ids.has(h.id))];
303
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;
304
333
  if (delProyecto.length === 0) {
305
334
  response.project_hint = `Nothing stored names "${proyecto}" yet. crbro_recall it before working; what you learn about it is worth a neuron.`;
306
335
  }
307
336
  }
337
+ else
338
+ sessionProject = null;
308
339
  // Ten hot topics with the day, not twenty with the millisecond: each
309
340
  // row is a pointer the model follows with recall, not a record.
310
341
  response.hot_topics = calientes.slice(0, 10)
311
342
  .map((h) => ({ ...h, last_access: dia(h.last_access) }));
312
343
  // active_context repeated open_items and recently_closed, which boot
313
344
  // already serves at the top level: 1,414 characters said twice.
314
- if (result.active_context && typeof result.active_context === 'object') {
315
- 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;
316
347
  response.active_context = resto;
317
348
  }
318
349
  for (const [k, cap] of [['open_items', 12], ['recently_closed', 8]]) {
@@ -437,6 +468,8 @@ function createServer(shared) {
437
468
  view: zod_1.z.enum(INSPECT_VIEWS),
438
469
  status: zod_1.z.object({
439
470
  crbro_version: zod_1.z.string(),
471
+ installed_version: zod_1.z.string().optional(),
472
+ version_note: zod_1.z.string().optional(),
440
473
  brain_format: zod_1.z.string().optional(),
441
474
  total_neurons: zod_1.z.number(),
442
475
  total_synapses: zod_1.z.number(),
@@ -497,7 +530,10 @@ function createServer(shared) {
497
530
  const manifest = await brain.getManifest();
498
531
  const hot = await (0, fs_js_1.readJSON)(brain.paths.hotTopics());
499
532
  return done({
500
- crbro_version: runningVersion(),
533
+ // The version this process RUNS, read once at start; when the
534
+ // package on disk says another one (npx replaced the files under
535
+ // a running process), it is added apart, with a note (2.9.1).
536
+ ...(0, version_js_1.versionStatus)(),
501
537
  brain_format: manifest.version,
502
538
  total_neurons: manifest.total_neurons,
503
539
  total_synapses: manifest.total_synapses,
@@ -547,7 +583,7 @@ function createServer(shared) {
547
583
  const retired = (id) => neuron.entry_status?.[id]?.status;
548
584
  // Shelf life: the same three facts recall shows, so the index of a
549
585
  // neuron and a recall never disagree about what is old.
550
- const vida = (0, shelf_js_1.stalenessContext)((await brain.getManifest()).staleness_since);
586
+ const vida = (0, shelf_js_1.stalenessContextOf)(await brain.getManifest());
551
587
  const rancio = (s) => (s?.stale ? { stale_days: s.age_days } : {});
552
588
  const rows = [];
553
589
  for (const f of neuron.facts || []) {
@@ -585,6 +621,8 @@ function createServer(shared) {
585
621
  sidecar('debt', neuron.debts);
586
622
  if (neuron.map?.text)
587
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 }));
588
626
  const header = {
589
627
  id: neuron.id, name: neuron.name, domain: neuron.domain, type: neuron.type,
590
628
  heat: neuron.heat, summary: neuron.summary, tags: neuron.tags,
@@ -659,6 +697,8 @@ function createServer(shared) {
659
697
  ...(r.verified ? { verified: dia(r.verified) } : {}),
660
698
  ...(r.shelf_life ? { shelf_life: r.shelf_life } : {}),
661
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 } : {}),
662
702
  ...(isRetired(r.status) ? { status: r.status, ...(r.revised ? { revised: dia(r.revised) } : {}), ...(r.revision_note ? { retired_note: r.revision_note } : {}) } : {}),
663
703
  })),
664
704
  entries_pagination: {
@@ -739,7 +779,7 @@ function createServer(shared) {
739
779
  // ═══════════════════════════════════════════════════════════════
740
780
  server.registerTool('crbro_learn', {
741
781
  title: 'Learn something',
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).',
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).',
743
783
  inputSchema: {
744
784
  // Optional since 2.0.3, and the reason is measured: the description
745
785
  // told callers that neuron_id "skips name matching entirely", the
@@ -759,6 +799,8 @@ function createServer(shared) {
759
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.'),
760
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.'),
761
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.'),
762
804
  },
763
805
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
764
806
  }, async (args) => {
@@ -787,6 +829,8 @@ function createServer(shared) {
787
829
  keys: args.keywords,
788
830
  keysReplace: args.keywords_replace,
789
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,
790
834
  });
791
835
  // Indexing happens inside cortex.learn, through the indexer hook.
792
836
  if (result.action === 'skipped_retired' && result.skipped_retired) {
@@ -861,7 +905,7 @@ function createServer(shared) {
861
905
  shelf_life: result.shelf?.shelf_life,
862
906
  shelf_inferred: result.shelf?.inferred ? true : undefined,
863
907
  shelf_reason: result.shelf?.inferred ? result.shelf.reason : undefined,
864
- // 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.
865
909
  reconfirmed: result.reconfirmed || undefined,
866
910
  shared_warning: shelfSharedWarning,
867
911
  total_facts: result.neuron.facts.length,
@@ -894,9 +938,14 @@ function createServer(shared) {
894
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).'),
895
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.'),
896
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.'),
897
945
  },
898
946
  outputSchema: {
899
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(),
900
949
  total_results: zod_1.z.number(),
901
950
  results: zod_1.z.array(zod_1.z.object({
902
951
  neuron_id: zod_1.z.string(), name: zod_1.z.string(), domain: zod_1.z.string(),
@@ -917,6 +966,8 @@ function createServer(shared) {
917
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(),
918
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'),
919
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(),
920
971
  returned: zod_1.z.number().optional().describe('Rows that came back: results plus possibly_stale'),
921
972
  matched_neurons: zod_1.z.number().optional().describe('Neurons with any hit before limit; total_results is what came back'),
922
973
  has_more: zod_1.z.boolean().optional(),
@@ -941,7 +992,15 @@ function createServer(shared) {
941
992
  // Five by default, down from ten: results are ranked and each one
942
993
  // carries its entry, so ten cost ~12,000 tokens on a dense brain for
943
994
  // answers that live in the top three (recall@3 is the metric).
944
- 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 });
945
1004
  const filtrado = !!since || !!kinds;
946
1005
  const sinFecha = undated_skipped ?? 0;
947
1006
  // A hit carries its entry, but not without limit: one 6,000-character
@@ -949,18 +1008,25 @@ function createServer(shared) {
949
1008
  // cap the opening comes back, declared, and entry_id reads the rest.
950
1009
  const CONTENT_CAP = 1_200;
951
1010
  let cortados = 0;
952
- const rows = results.map(r => {
1011
+ const rows = await Promise.all(results.map(async (r) => {
953
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);
954
1015
  const largo = mc.length > CONTENT_CAP;
955
1016
  if (largo)
956
1017
  cortados++;
957
1018
  return {
958
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 } : {}) },
959
1023
  matched_added: dia(r.matched_added),
960
1024
  ...(largo ? { matching_content: `${mc.slice(0, CONTENT_CAP).trimEnd()}…`, content_truncated: true, content_chars: mc.length } : {}),
961
- ...(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
+ })) } : {}),
962
1028
  };
963
- });
1029
+ }));
964
1030
  // Shelf life: partition, not penalty. The ranking above already chose
965
1031
  // these rows exactly as before; a row whose WINNING entry is past its
966
1032
  // shelf life since last verified moves whole to possibly_stale, in
@@ -970,12 +1036,22 @@ function createServer(shared) {
970
1036
  // The internal `staleness` field leaves every row here.
971
1037
  const actuales = [];
972
1038
  const rancios = [];
1039
+ const historical = [];
1040
+ let excludedNonhistorical = 0;
973
1041
  // When anything moves, every row keeps its rank: results[0] is then
974
1042
  // not necessarily the best match, and the agent has to be able to see it.
975
1043
  const algunoRancio = rows.some(r => r.staleness?.stale);
976
1044
  let mejorMovido = false;
977
1045
  for (const [i, row] of rows.entries()) {
978
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
+ }
979
1055
  if (algunoRancio)
980
1056
  resto.rank = i + 1;
981
1057
  if (staleness?.stale) {
@@ -986,6 +1062,7 @@ function createServer(shared) {
986
1062
  const { matching_content, neuron_id, name, entry_id, rank, ...demas } = resto;
987
1063
  rancios.push({
988
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.` } : {}),
989
1066
  last_known: matching_content,
990
1067
  neuron_id, name,
991
1068
  ...(entry_id !== undefined ? { entry_id } : {}),
@@ -1002,7 +1079,7 @@ function createServer(shared) {
1002
1079
  actuales.push(resto);
1003
1080
  }
1004
1081
  }
1005
- const servidos = actuales.length + rancios.length;
1082
+ const servidos = actuales.length + rancios.length + historical.length;
1006
1083
  const sobran = matched_neurons - servidos;
1007
1084
  // Iteration 2: when anything moved, the answer OPENS with a short
1008
1085
  // warning, before query and results, so it is read before the rows.
@@ -1017,6 +1094,7 @@ function createServer(shared) {
1017
1094
  const payload = {
1018
1095
  ...(avisoRancio ? { stale_warning: avisoRancio } : {}),
1019
1096
  query: args.query,
1097
+ search_scope: { mode: scope, ...(project ? { project_id: project.id } : {}), ...(scopedNeurons ? { neurons: scopedNeurons } : {}) },
1020
1098
  // total_results is what came back as current (capped by limit);
1021
1099
  // returned adds possibly_stale; matched_neurons is how many neurons
1022
1100
  // had a hit at all, so five never reads as "only five".
@@ -1030,6 +1108,8 @@ function createServer(shared) {
1030
1108
  ...(since ? { undated_skipped: sinFecha } : {}),
1031
1109
  results: actuales,
1032
1110
  ...(rancios.length ? { possibly_stale: rancios, possibly_stale_count: rancios.length } : {}),
1111
+ ...(historical.length ? { historical_matches: historical } : {}),
1112
+ ...(excludedNonhistorical ? { excluded_nonhistorical: excludedNonhistorical } : {}),
1033
1113
  // The diary, searched since 2.2: day logs whose summary mentions the
1034
1114
  // question, in a list of their own so narrative never outranks a fact.
1035
1115
  // sessions_total: how many days had a hit; three shown never reads as three.
@@ -1050,7 +1130,9 @@ function createServer(shared) {
1050
1130
  + (sessions.length ? ' sessions_matched: day logs that mention it; read one whole with crbro_inspect view=sessions session=<session_id>.' : ''))
1051
1131
  + (filtrado && servidos > 0 ? ' Filtered: entries outside since/kind were not searched.' : '')
1052
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).` : '')
1053
- + (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.' : ''),
1054
1136
  };
1055
1137
  // Recall is the one read whose size the caller sets, with limit: ten
1056
1138
  // results already cost ~12,000 tokens on a dense brain because each
@@ -1076,6 +1158,7 @@ function createServer(shared) {
1076
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.'),
1077
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.'),
1078
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.'),
1079
1162
  summary: zod_1.z.string().optional().describe('Replace the neuron summary. Credentials are redacted and listed in redacted.'),
1080
1163
  domain: zod_1.z.string().optional().describe('Replace the neuron domain unconditionally, e.g. "proyectos-web".'),
1081
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.'),
@@ -1137,7 +1220,7 @@ function createServer(shared) {
1137
1220
  // ── status verified: reconfirm, do not retire (shelf life) ──
1138
1221
  if (status === 'verified') {
1139
1222
  const v = (args.facts?.length || args.entries?.length)
1140
- ? 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 })
1141
1224
  : { verified: [], unmatched: [], retired: [] };
1142
1225
  let cambiados = [];
1143
1226
  let tachados = [];
@@ -1230,7 +1313,7 @@ function createServer(shared) {
1230
1313
  // ═══════════════════════════════════════════════════════════════
1231
1314
  const forgetSchema = zod_1.z.object({
1232
1315
  neuron: zod_1.z.string().optional().describe('Neuron id or name the mode acts on. Required for every mode except session. restore needs the exact neuron id.'),
1233
- facts: zod_1.z.array(zod_1.z.string()).optional().describe('Mode facts: fact ids, or the exact text of a fact, decision, pattern, preference, error or debt; the exact full text of the map removes the map. Deleted for good after a quarantine copy; decision/pattern removals travel to shared spaces like errors and debts.'),
1316
+ facts: zod_1.z.array(zod_1.z.string()).optional().describe('Mode facts: the ids crbro_inspect and crbro_recall show, or the exact text, of facts, decisions, patterns, preferences, errors or debts; the exact full text of the map removes the map. Deleted for good after a quarantine copy; decision/pattern removals travel to shared spaces like errors and debts.'),
1234
1317
  entire: zod_1.z.boolean().optional().describe('Mode entire: delete the whole neuron and its synapses. Without confirm_token it is a dry run — { neuron_id, dry_run:true, counts, shared_in, confirm_token }. Refused (no token) while the neuron is shared: crbro_share unshare first.'),
1235
1318
  confirm_token: zod_1.z.string().optional().describe('Only with entire:true — the token from the dry run. Derived from the neuron\'s counts, so it goes stale (and is refused) when the neuron changed in between.'),
1236
1319
  restore: zod_1.z.boolean().optional().describe('Mode restore: bring back the newest quarantine copy of `neuron` (exact id). If the neuron exists again, the copy is merged into it (merged_into_existing:true, moved counts). The quarantine file stays, so restore is repeatable.'),
@@ -1455,10 +1538,14 @@ function createServer(shared) {
1455
1538
  // ═══════════════════════════════════════════════════════════════
1456
1539
  server.registerTool('crbro_context', {
1457
1540
  title: 'Working context',
1458
- 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.',
1459
1542
  inputSchema: {
1460
- 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.'),
1461
- 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.'),
1462
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.'),
1463
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.'),
1464
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.'),
@@ -1469,6 +1556,8 @@ function createServer(shared) {
1469
1556
  }, async (args) => {
1470
1557
  try {
1471
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,
1472
1561
  set_topics: args.set_topics,
1473
1562
  add_pending: args.add_pending,
1474
1563
  resolve_pending: args.resolve_pending,