crbro-memory 2.1.2 → 2.3.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 +413 -390
- package/bin/crbro.mjs +151 -1
- package/dist/engine/hippocampus.d.ts +8 -0
- package/dist/engine/hippocampus.d.ts.map +1 -1
- package/dist/engine/hippocampus.js +17 -2
- package/dist/engine/hippocampus.js.map +1 -1
- package/dist/search/index.d.ts +33 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +169 -10
- package/dist/search/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +50 -9
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/bin/crbro.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// ─── CRBRO CLI ───────────────────────────────────────────────────
|
|
4
4
|
// Command-line interface for CRBRO memory system
|
|
5
|
-
// Supports: init, status, mine, setup-miner, miner-status,
|
|
5
|
+
// Supports: init, status, secret, mine, setup-miner, miner-status,
|
|
6
6
|
// remove-miner, and MCP server mode (default)
|
|
7
7
|
|
|
8
8
|
import { platform, homedir } from 'os';
|
|
@@ -584,6 +584,149 @@ if (command === 'init') {
|
|
|
584
584
|
}
|
|
585
585
|
}).catch(console.error);
|
|
586
586
|
|
|
587
|
+
} else if (command === 'secret') {
|
|
588
|
+
// ─── Credentials, from the terminal ────────────────────────────
|
|
589
|
+
//
|
|
590
|
+
// Until 2.3 the only way to store a credential was the crbro_secret MCP
|
|
591
|
+
// tool, which means typing it into a conversation with a model. For a
|
|
592
|
+
// module whose whole point is that a secret never touches the brain, that
|
|
593
|
+
// was the wrong last mile. These subcommands close it: the value is read
|
|
594
|
+
// from stdin, never from argv, so it stays out of the shell history and
|
|
595
|
+
// out of the process table where `ps` and Task Manager can read it.
|
|
596
|
+
const sub = args[1];
|
|
597
|
+
const name = args[2];
|
|
598
|
+
|
|
599
|
+
const readPiped = () => new Promise((resolve, reject) => {
|
|
600
|
+
let data = '';
|
|
601
|
+
process.stdin.setEncoding('utf8');
|
|
602
|
+
process.stdin.on('data', (d) => { data += d; });
|
|
603
|
+
process.stdin.on('end', () => resolve(data.replace(/\r?\n$/, '')));
|
|
604
|
+
process.stdin.on('error', reject);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Interactive read with the echo off. Raw mode hands us every keystroke,
|
|
608
|
+
// so nothing is drawn and nothing survives in the terminal scrollback.
|
|
609
|
+
const readHidden = (promptText) => new Promise((resolve, reject) => {
|
|
610
|
+
const stdin = process.stdin;
|
|
611
|
+
let value = '';
|
|
612
|
+
const cleanup = () => {
|
|
613
|
+
stdin.removeListener('data', onData);
|
|
614
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
615
|
+
stdin.pause();
|
|
616
|
+
};
|
|
617
|
+
const onData = (chunk) => {
|
|
618
|
+
for (const ch of chunk.toString('utf8')) {
|
|
619
|
+
if (ch === '\r' || ch === '\n') { cleanup(); process.stderr.write('\n'); return resolve(value); }
|
|
620
|
+
if (ch === '\u0003') { cleanup(); process.stderr.write('\n'); return reject(new Error('Cancelled — nothing was stored.')); }
|
|
621
|
+
if (ch === '\u007f' || ch === '\b') { value = value.slice(0, -1); continue; }
|
|
622
|
+
value += ch;
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
process.stderr.write(promptText);
|
|
626
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
627
|
+
stdin.resume();
|
|
628
|
+
stdin.on('data', onData);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
const flagValue = (flag) => {
|
|
632
|
+
const i = args.indexOf(flag);
|
|
633
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : '';
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
import('../dist/engine/keychain.js').then(async (kc) => {
|
|
637
|
+
try {
|
|
638
|
+
if (sub === 'status') {
|
|
639
|
+
const { backend, reason } = kc.detectBackend();
|
|
640
|
+
console.log('');
|
|
641
|
+
console.log(' 🔐 CRBRO keychain');
|
|
642
|
+
console.log(' ─────────────────');
|
|
643
|
+
console.log(` Backend: ${backend || '❌ none available'}`);
|
|
644
|
+
if (reason) console.log(` Reason: ${reason}`);
|
|
645
|
+
if (backend === 'windows-dpapi') {
|
|
646
|
+
const dir = process.env['CRBRO_KEYS_DIR'] || join(homedir(), '.crbro-keys');
|
|
647
|
+
console.log(` Store: ${join(dir, 'keys.dpapi')}`);
|
|
648
|
+
console.log(' Sealed to this Windows account: copied to another');
|
|
649
|
+
console.log(' machine or lifted from a backup, it is unreadable.');
|
|
650
|
+
}
|
|
651
|
+
console.log('');
|
|
652
|
+
console.log(' The store lives outside the brain. No sync, no team space and no');
|
|
653
|
+
console.log(' crbro_share can reach it — per machine, on purpose.');
|
|
654
|
+
console.log('');
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (sub === 'list') {
|
|
659
|
+
const secrets = kc.listSecrets();
|
|
660
|
+
console.log('');
|
|
661
|
+
if (secrets.length === 0) {
|
|
662
|
+
console.log(' 🔐 No credentials stored on this machine yet.');
|
|
663
|
+
console.log(' Store one: npx crbro-memory secret set MY_TOKEN');
|
|
664
|
+
console.log('');
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
console.log(` 🔐 ${secrets.length} credential(s) — names only, values are never listed.`);
|
|
668
|
+
console.log(' ──────────────────────────────────────────────────────────');
|
|
669
|
+
for (const s of secrets) {
|
|
670
|
+
console.log(` ${s.name.padEnd(28)} ${s.updated} ${s.description || ''}`.trimEnd());
|
|
671
|
+
}
|
|
672
|
+
console.log('');
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (!name) {
|
|
677
|
+
console.error(` ❌ Missing name. Usage: npx crbro-memory secret ${sub || '<set|get|list|remove|status>'} NAME`);
|
|
678
|
+
process.exit(1);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (sub === 'set') {
|
|
682
|
+
const value = process.stdin.isTTY
|
|
683
|
+
? await readHidden(` Value for ${name} (input hidden, Enter to finish): `)
|
|
684
|
+
: await readPiped();
|
|
685
|
+
if (!value) {
|
|
686
|
+
console.error(' ❌ Empty value. Nothing was stored.');
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
kc.setSecret(name, value, flagValue('--description'));
|
|
690
|
+
console.log(` ✅ ${name} sealed in the OS keychain. CRBRO keeps no copy.`);
|
|
691
|
+
console.log(' Record only the NAME in the brain, never the value.');
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (sub === 'get') {
|
|
696
|
+
const value = kc.getSecret(name);
|
|
697
|
+
if (value === null) {
|
|
698
|
+
console.error(` ❌ ${name} not found on this machine.`);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
if (process.stdout.isTTY) {
|
|
702
|
+
process.stderr.write(' ⚠️ Printing a credential to the screen. Pipe it instead to keep it out of the scrollback.\n');
|
|
703
|
+
}
|
|
704
|
+
process.stdout.write(value + '\n');
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (sub === 'remove') {
|
|
709
|
+
if (!args.includes('--yes')) {
|
|
710
|
+
console.error(` ❌ This deletes ${name} from the keychain and cannot be undone.`);
|
|
711
|
+
console.error(` Re-run to confirm: npx crbro-memory secret remove ${name} --yes`);
|
|
712
|
+
process.exit(1);
|
|
713
|
+
}
|
|
714
|
+
const removed = kc.removeSecret(name);
|
|
715
|
+
console.log(removed ? ` ✅ ${name} removed.` : ` ⚪ ${name} was not there. Nothing changed.`);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
console.error(' Usage: npx crbro-memory secret <set|get|list|remove|status> [NAME]');
|
|
720
|
+
process.exit(1);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
console.error(` ❌ ${err instanceof Error ? err.message : err}`);
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
}).catch((err) => {
|
|
726
|
+
console.error(` ❌ ${err instanceof Error ? err.message : err}`);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
});
|
|
729
|
+
|
|
587
730
|
} else if (command === '--help' || command === '-h') {
|
|
588
731
|
// ─── Help ──────────────────────────────────────────────────────
|
|
589
732
|
console.log('');
|
|
@@ -609,6 +752,13 @@ if (command === 'init') {
|
|
|
609
752
|
console.log(' npx crbro-memory semantic build Embed the whole brain once (needs CRBRO_SEMANTIC=1)');
|
|
610
753
|
console.log(' npx crbro-memory semantic status Runtime, model and whether it is enabled');
|
|
611
754
|
console.log('');
|
|
755
|
+
console.log(' Credentials (per machine, sealed in the OS keychain):');
|
|
756
|
+
console.log(' npx crbro-memory secret set NAME Store one; the value is read from stdin, never argv');
|
|
757
|
+
console.log(' npx crbro-memory secret list Names only, never values');
|
|
758
|
+
console.log(' npx crbro-memory secret get NAME Print one; pipe it to keep it off the screen');
|
|
759
|
+
console.log(' npx crbro-memory secret remove NAME --yes');
|
|
760
|
+
console.log(' npx crbro-memory secret status Which keychain this machine offers');
|
|
761
|
+
console.log('');
|
|
612
762
|
console.log(' Server:');
|
|
613
763
|
console.log(' npx crbro-memory Start MCP server (stdio)');
|
|
614
764
|
console.log('');
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { Brain } from './brain.js';
|
|
2
2
|
import type { SessionLog } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Accept 'session_2026-09-03' or '2026-09-03'; null for anything that is not
|
|
5
|
+
* a day id, so a reference never turns into a path — "../manifest" was read
|
|
6
|
+
* back as a log before this check existed.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeSessionId(ref: string): string | null;
|
|
3
9
|
export declare class Hippocampus {
|
|
4
10
|
private brain;
|
|
5
11
|
constructor(brain: Brain);
|
|
@@ -39,6 +45,8 @@ export declare class Hippocampus {
|
|
|
39
45
|
date: string;
|
|
40
46
|
kinds: string[];
|
|
41
47
|
}>>;
|
|
48
|
+
/** One log by id, whole; null when the reference is not a day id or the log is gone. */
|
|
49
|
+
readSession(sessionRef: string): Promise<SessionLog | null>;
|
|
42
50
|
/**
|
|
43
51
|
* Delete one session log, after copying it to quarantine — the same
|
|
44
52
|
* "nothing is deleted outright" rule the cortex follows.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hippocampus.d.ts","sourceRoot":"","sources":["../../src/engine/hippocampus.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"hippocampus.d.ts","sourceRoot":"","sources":["../../src/engine/hippocampus.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAIpD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI7D;AAED,qBAAa,WAAW;IACV,OAAO,CAAC,KAAK;gBAAL,KAAK,EAAE,KAAK;IAEhC;;;;;;;OAOG;IACG,UAAU,CAAC,IAAI,EAAE;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,GAAG,OAAO,CAAC,UAAU,GAAG;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IA0ChD;;OAEG;IACG,YAAY,CAAC,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAe7D;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAK5C;;;;OAIG;IACG,YAAY,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAW3F,wFAAwF;IAClF,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAMjE;;;OAGG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CAiBlH"}
|
|
@@ -3,14 +3,20 @@
|
|
|
3
3
|
// Session logging and memory formation
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.Hippocampus = void 0;
|
|
6
|
+
exports.normalizeSessionId = normalizeSessionId;
|
|
6
7
|
const fs_js_1 = require("../utils/fs.js");
|
|
7
8
|
const ids_js_1 = require("../utils/ids.js");
|
|
8
9
|
const secrets_js_1 = require("./secrets.js");
|
|
9
10
|
const SESSION_PREFIX = 'session_';
|
|
10
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Accept 'session_2026-09-03' or '2026-09-03'; null for anything that is not
|
|
13
|
+
* a day id, so a reference never turns into a path — "../manifest" was read
|
|
14
|
+
* back as a log before this check existed.
|
|
15
|
+
*/
|
|
11
16
|
function normalizeSessionId(ref) {
|
|
12
17
|
const t = (ref || '').trim();
|
|
13
|
-
|
|
18
|
+
const id = t.startsWith(SESSION_PREFIX) ? t : `${SESSION_PREFIX}${t}`;
|
|
19
|
+
return /^session_\d{4}-\d{2}-\d{2}$/.test(id) ? id : null;
|
|
14
20
|
}
|
|
15
21
|
class Hippocampus {
|
|
16
22
|
brain;
|
|
@@ -99,12 +105,21 @@ class Hippocampus {
|
|
|
99
105
|
}
|
|
100
106
|
return out;
|
|
101
107
|
}
|
|
108
|
+
/** One log by id, whole; null when the reference is not a day id or the log is gone. */
|
|
109
|
+
async readSession(sessionRef) {
|
|
110
|
+
const session_id = normalizeSessionId(sessionRef);
|
|
111
|
+
if (!session_id)
|
|
112
|
+
return null;
|
|
113
|
+
return (0, fs_js_1.readJSON)(this.brain.paths.session(session_id));
|
|
114
|
+
}
|
|
102
115
|
/**
|
|
103
116
|
* Delete one session log, after copying it to quarantine — the same
|
|
104
117
|
* "nothing is deleted outright" rule the cortex follows.
|
|
105
118
|
*/
|
|
106
119
|
async forgetSession(sessionRef) {
|
|
107
120
|
const session_id = normalizeSessionId(sessionRef);
|
|
121
|
+
if (!session_id)
|
|
122
|
+
return { session_id: (sessionRef || '').trim(), removed: false, backup: null };
|
|
108
123
|
const ruta = this.brain.paths.session(session_id);
|
|
109
124
|
const log = await (0, fs_js_1.readJSON)(ruta);
|
|
110
125
|
if (!log)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hippocampus.js","sourceRoot":"","sources":["../../src/engine/hippocampus.ts"],"names":[],"mappings":";AAAA,oEAAoE;AACpE,uCAAuC;;;
|
|
1
|
+
{"version":3,"file":"hippocampus.js","sourceRoot":"","sources":["../../src/engine/hippocampus.ts"],"names":[],"mappings":"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"}
|
package/dist/search/index.d.ts
CHANGED
|
@@ -10,10 +10,30 @@ import type { Neuron, SearchResult } from '../types/index.js';
|
|
|
10
10
|
* 2.0) are no longer chunked, so an index built before it must be rebuilt or
|
|
11
11
|
* it would keep serving entries that were retired.
|
|
12
12
|
*/
|
|
13
|
-
export declare const INDEX_VERSION =
|
|
13
|
+
export declare const INDEX_VERSION = 7;
|
|
14
14
|
/** Filled in by search/searchMany when the caller passes it: what the list left out. */
|
|
15
15
|
export interface SearchStats {
|
|
16
16
|
matched_neurons?: number;
|
|
17
|
+
/** Day logs whose summary mentions the query — a list of their own, never mixed with neurons. */
|
|
18
|
+
sessions?: SessionMatch[];
|
|
19
|
+
/** How many day logs had a hit at all, before the cap on sessions: three never reads as "only three". */
|
|
20
|
+
sessions_total?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A session log that matched. The diary is searchable since 2.2, but it is
|
|
24
|
+
* narrative, not knowledge: a session hit points at where something was
|
|
25
|
+
* told, it never outranks the fact that answers. Read the whole log with
|
|
26
|
+
* crbro_inspect view=sessions session=<session_id>.
|
|
27
|
+
*/
|
|
28
|
+
export interface SessionMatch {
|
|
29
|
+
session_id: string;
|
|
30
|
+
date: string;
|
|
31
|
+
entry_id: string;
|
|
32
|
+
preview: string;
|
|
33
|
+
chars: number;
|
|
34
|
+
matched_terms: number;
|
|
35
|
+
query_terms: number;
|
|
36
|
+
confidence: 'strong' | 'weak';
|
|
17
37
|
}
|
|
18
38
|
export declare class SearchEngine {
|
|
19
39
|
private brain;
|
|
@@ -124,6 +144,8 @@ export declare class SearchEngine {
|
|
|
124
144
|
}): Promise<{
|
|
125
145
|
results: SearchResult[];
|
|
126
146
|
matched_neurons: number;
|
|
147
|
+
sessions: SessionMatch[];
|
|
148
|
+
sessions_total: number;
|
|
127
149
|
}>;
|
|
128
150
|
/**
|
|
129
151
|
* Blend the vector ranking into the lexical one with reciprocal-rank
|
|
@@ -164,6 +186,16 @@ export declare class SearchEngine {
|
|
|
164
186
|
private searchTerm;
|
|
165
187
|
/** Build and insert every chunk of a neuron. */
|
|
166
188
|
private insertNeuronChunks;
|
|
189
|
+
/**
|
|
190
|
+
* Index one session log, replacing whatever it had. Called by consolidate
|
|
191
|
+
* for the day just logged and by rebuild for the whole diary.
|
|
192
|
+
*/
|
|
193
|
+
indexSession(log: {
|
|
194
|
+
session_id: string;
|
|
195
|
+
date?: string;
|
|
196
|
+
summary?: string;
|
|
197
|
+
}): Promise<void>;
|
|
198
|
+
private insertSessionChunks;
|
|
167
199
|
private put;
|
|
168
200
|
/**
|
|
169
201
|
* Drop every chunk belonging to a neuron.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAQ,MAAM,mBAAmB,CAAC;AAEpE;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,IAAI,CAAC;AAoE/B,wFAAwF;AACxF,MAAM,WAAW,WAAW;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"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"}
|
package/dist/search/index.js
CHANGED
|
@@ -36,7 +36,7 @@ const ops_js_1 = require("../sync/ops.js");
|
|
|
36
36
|
* 2.0) are no longer chunked, so an index built before it must be rebuilt or
|
|
37
37
|
* it would keep serving entries that were retired.
|
|
38
38
|
*/
|
|
39
|
-
exports.INDEX_VERSION =
|
|
39
|
+
exports.INDEX_VERSION = 7; // 4: keys field (1.15) · 5: entry_status (2.0) · 6: eid per chunk (2.1) · 7: session logs indexed (2.2)
|
|
40
40
|
/** Weight given to a neuron for each *additional* chunk that matches. */
|
|
41
41
|
const BREADTH_BONUS = 0.05;
|
|
42
42
|
/** Cap on the breadth bonus — breadth is a tiebreaker, never the main signal. */
|
|
@@ -95,6 +95,62 @@ const SCHEMA = {
|
|
|
95
95
|
added: 'string',
|
|
96
96
|
heat: 'number',
|
|
97
97
|
};
|
|
98
|
+
const SESSION_MATCHES = 3;
|
|
99
|
+
const SESSION_CHUNK = 700;
|
|
100
|
+
/** Best chunk per session, top sessions by coverage-weighted score; total is how many days had a hit. */
|
|
101
|
+
function topSessions(hits, queryTerms) {
|
|
102
|
+
const porSesion = new Map();
|
|
103
|
+
for (const h of hits.values()) {
|
|
104
|
+
const prev = porSesion.get(h.session);
|
|
105
|
+
if (!prev || h.score * h.matched > prev.score * prev.matched)
|
|
106
|
+
porSesion.set(h.session, h);
|
|
107
|
+
}
|
|
108
|
+
const matches = [...porSesion.values()]
|
|
109
|
+
// Ties broken by id, newest first, so the list is the same on every run.
|
|
110
|
+
.sort((a, b) => b.score * b.matched - a.score * a.matched || (a.session < b.session ? 1 : -1))
|
|
111
|
+
.slice(0, SESSION_MATCHES)
|
|
112
|
+
.map((h) => {
|
|
113
|
+
const strong = queryTerms <= 1 ? h.matched >= 1 : h.matched >= 2 && h.matched / queryTerms >= 0.5;
|
|
114
|
+
return {
|
|
115
|
+
session_id: h.session,
|
|
116
|
+
date: h.date,
|
|
117
|
+
entry_id: h.eid,
|
|
118
|
+
preview: h.text.length > 300 ? `${h.text.slice(0, 300).trimEnd()}…` : h.text,
|
|
119
|
+
chars: h.text.length,
|
|
120
|
+
matched_terms: h.matched,
|
|
121
|
+
query_terms: queryTerms,
|
|
122
|
+
confidence: strong ? 'strong' : 'weak',
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
return { matches, total: porSesion.size };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A summary becomes paragraphs of at most SESSION_CHUNK characters: a match
|
|
129
|
+
* then points at the paragraph that mentions the thing, and the preview is
|
|
130
|
+
* the paragraph itself, not the opening of a 7,000-character day.
|
|
131
|
+
*/
|
|
132
|
+
function sessionChunks(summary) {
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const parrafo of summary.split(/\n-{3,}\n|\n{2,}/)) {
|
|
135
|
+
const p = parrafo.trim();
|
|
136
|
+
if (!p)
|
|
137
|
+
continue;
|
|
138
|
+
if (p.length <= SESSION_CHUNK) {
|
|
139
|
+
out.push(p);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
let resto = p;
|
|
143
|
+
while (resto.length > SESSION_CHUNK) {
|
|
144
|
+
const corte = resto.lastIndexOf(' ', SESSION_CHUNK);
|
|
145
|
+
const at = corte > SESSION_CHUNK / 2 ? corte : SESSION_CHUNK;
|
|
146
|
+
out.push(resto.slice(0, at).trim());
|
|
147
|
+
resto = resto.slice(at).trim();
|
|
148
|
+
}
|
|
149
|
+
if (resto)
|
|
150
|
+
out.push(resto);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
98
154
|
class SearchEngine {
|
|
99
155
|
brain;
|
|
100
156
|
db = null;
|
|
@@ -184,8 +240,10 @@ class SearchEngine {
|
|
|
184
240
|
if (indexAt === 0)
|
|
185
241
|
return true;
|
|
186
242
|
const cortexAt = await (0, fs_js_1.newestMtime)(this.brain.paths.cortex);
|
|
243
|
+
// The diary is indexed too (2.2): a log written by another process counts.
|
|
244
|
+
const diaryAt = await (0, fs_js_1.newestMtime)(this.brain.paths.hippocampus);
|
|
187
245
|
// One second of slack: a write landing during a rebuild is not staleness.
|
|
188
|
-
return cortexAt > indexAt + 1000;
|
|
246
|
+
return Math.max(cortexAt, diaryAt) > indexAt + 1000;
|
|
189
247
|
}
|
|
190
248
|
/**
|
|
191
249
|
* Rebuild the whole index from the cortex.
|
|
@@ -209,6 +267,17 @@ class SearchEngine {
|
|
|
209
267
|
// Skip unreadable neuron, keep going.
|
|
210
268
|
}
|
|
211
269
|
}
|
|
270
|
+
// The diary too (2.2): every session log, as paragraphs.
|
|
271
|
+
for (const id of await (0, fs_js_1.listJSONFiles)(this.brain.paths.hippocampus)) {
|
|
272
|
+
try {
|
|
273
|
+
const log = await (0, fs_js_1.readJSON)(this.brain.paths.session(id));
|
|
274
|
+
if (log?.summary)
|
|
275
|
+
await this.insertSessionChunks(log);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Skip unreadable log, keep going.
|
|
279
|
+
}
|
|
280
|
+
}
|
|
212
281
|
// Embeddings off the critical path: a rebuild after an upgrade must not
|
|
213
282
|
// hold boot for minutes on a big brain. Recall serves what is embedded so
|
|
214
283
|
// far; the vectors go to disk with the next index write.
|
|
@@ -293,6 +362,7 @@ class SearchEngine {
|
|
|
293
362
|
if (terms.length === 0)
|
|
294
363
|
return [];
|
|
295
364
|
const perChunk = new Map();
|
|
365
|
+
const perSession = new Map();
|
|
296
366
|
for (const term of terms) {
|
|
297
367
|
const hits = await this.searchTerm(term, options?.domain);
|
|
298
368
|
if (hits.length === 0)
|
|
@@ -302,6 +372,19 @@ class SearchEngine {
|
|
|
302
372
|
const doc = hit.document;
|
|
303
373
|
const key = doc.id;
|
|
304
374
|
const normalised = best > 0 ? hit.score / best : 0;
|
|
375
|
+
// Session logs are searched, but never as neurons: they keep a list
|
|
376
|
+
// of their own, so a long narrative cannot outrank the fact that
|
|
377
|
+
// answers, and the neuron path below never sees them.
|
|
378
|
+
if (doc.kind === 'session') {
|
|
379
|
+
const prev = perSession.get(key);
|
|
380
|
+
if (prev) {
|
|
381
|
+
prev.score += normalised;
|
|
382
|
+
prev.matched += 1;
|
|
383
|
+
}
|
|
384
|
+
else
|
|
385
|
+
perSession.set(key, { session: String(doc.name || ''), date: String(doc.added || ''), text: String(doc.text || ''), eid: String(doc.eid || ''), score: normalised, matched: 1 });
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
305
388
|
const existing = perChunk.get(key);
|
|
306
389
|
if (existing) {
|
|
307
390
|
existing.score += normalised;
|
|
@@ -334,6 +417,11 @@ class SearchEngine {
|
|
|
334
417
|
if (this.semantic && this.semantic.ready() && this.semantic.count() > 0) {
|
|
335
418
|
await this.fuseSemantic(query, perChunk, options?.domain);
|
|
336
419
|
}
|
|
420
|
+
if (options?.stats) {
|
|
421
|
+
const top = topSessions(perSession, terms.length);
|
|
422
|
+
options.stats.sessions = top.matches;
|
|
423
|
+
options.stats.sessions_total = top.total;
|
|
424
|
+
}
|
|
337
425
|
if (perChunk.size === 0)
|
|
338
426
|
return [];
|
|
339
427
|
// Group chunks by neuron, best first.
|
|
@@ -367,7 +455,36 @@ class SearchEngine {
|
|
|
367
455
|
if (distintas.length === 1)
|
|
368
456
|
return this.search(distintas[0], options);
|
|
369
457
|
const limit = options.limit ?? 10;
|
|
370
|
-
const
|
|
458
|
+
const porConsulta = distintas.map(() => ({}));
|
|
459
|
+
const listas = await Promise.all(distintas.map((q, i) => this.search(q, { ...options, stats: porConsulta[i], limit: Math.max(limit, 10) * 2 })));
|
|
460
|
+
if (options.stats) {
|
|
461
|
+
// Session hits: a day that several phrasings point at ranks first, then
|
|
462
|
+
// by accumulated coverage (matched over query terms, so 2/2 beats 3/8).
|
|
463
|
+
// The row kept is the best-covered one, strong if any phrasing found it
|
|
464
|
+
// so — and the same phrasings in any order give the same list.
|
|
465
|
+
const cobertura = (m) => (m.query_terms > 0 ? m.matched_terms / m.query_terms : 0);
|
|
466
|
+
const fusion = new Map();
|
|
467
|
+
for (const s of porConsulta) {
|
|
468
|
+
for (const m of s.sessions || []) {
|
|
469
|
+
const f = fusion.get(m.session_id);
|
|
470
|
+
if (!f) {
|
|
471
|
+
fusion.set(m.session_id, { row: m, cobertura: cobertura(m), frases: 1, strong: m.confidence === 'strong' });
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
f.cobertura += cobertura(m);
|
|
475
|
+
f.frases += 1;
|
|
476
|
+
f.strong = f.strong || m.confidence === 'strong';
|
|
477
|
+
if (cobertura(m) > cobertura(f.row) || (cobertura(m) === cobertura(f.row) && m.matched_terms > f.row.matched_terms))
|
|
478
|
+
f.row = m;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
options.stats.sessions = [...fusion.values()]
|
|
482
|
+
.sort((a, b) => b.frases - a.frases || b.cobertura - a.cobertura || (a.row.session_id < b.row.session_id ? 1 : -1))
|
|
483
|
+
.slice(0, SESSION_MATCHES)
|
|
484
|
+
.map(f => ({ ...f.row, confidence: f.strong ? 'strong' : f.row.confidence }));
|
|
485
|
+
// Each phrasing counted its own days: the union is at least the largest count.
|
|
486
|
+
options.stats.sessions_total = Math.max(fusion.size, ...porConsulta.map(s => s.sessions_total || 0));
|
|
487
|
+
}
|
|
371
488
|
const fused = new Map();
|
|
372
489
|
const alsoOf = (r) => alsoLine(r.matching_content, r.matched_kind || '', r.matched_added || '', r.entry_id);
|
|
373
490
|
for (const lista of listas) {
|
|
@@ -424,7 +541,8 @@ class SearchEngine {
|
|
|
424
541
|
async searchManyWithStats(queries, options = {}) {
|
|
425
542
|
const stats = {};
|
|
426
543
|
const results = await this.searchMany(queries, { ...options, stats });
|
|
427
|
-
|
|
544
|
+
const sessions = stats.sessions ?? [];
|
|
545
|
+
return { results, matched_neurons: stats.matched_neurons ?? results.length, sessions, sessions_total: stats.sessions_total ?? sessions.length };
|
|
428
546
|
}
|
|
429
547
|
/**
|
|
430
548
|
* Blend the vector ranking into the lexical one with reciprocal-rank
|
|
@@ -455,7 +573,7 @@ class SearchEngine {
|
|
|
455
573
|
catch {
|
|
456
574
|
doc = undefined;
|
|
457
575
|
}
|
|
458
|
-
if (!doc || doc.kind === 'header')
|
|
576
|
+
if (!doc || doc.kind === 'header' || doc.kind === 'session')
|
|
459
577
|
continue;
|
|
460
578
|
if (domain && doc.domain !== domain)
|
|
461
579
|
continue;
|
|
@@ -642,7 +760,9 @@ class SearchEngine {
|
|
|
642
760
|
// Filtered here, not with an Orama `where` clause: a `where` on a
|
|
643
761
|
// plain string field matches nothing at all, so the old code turned
|
|
644
762
|
// every domain-scoped recall into zero results (measured).
|
|
645
|
-
|
|
763
|
+
// Day logs have no domain: a domain-scoped recall still lists the days
|
|
764
|
+
// that mention it, in their own list, or the diary vanished in silence.
|
|
765
|
+
return domain ? hits.filter(h => h.document.kind === 'session' || h.document.domain === domain) : hits;
|
|
646
766
|
};
|
|
647
767
|
let hits = await run(0);
|
|
648
768
|
// Try the other grammatical number too, and keep the best score each
|
|
@@ -662,8 +782,14 @@ class SearchEngine {
|
|
|
662
782
|
}
|
|
663
783
|
hits = [...mejor.values()].sort((a, b) => b.score - a.score);
|
|
664
784
|
}
|
|
665
|
-
|
|
666
|
-
|
|
785
|
+
// Decided on neuron hits, not on hits at all: since 2.2 a day log that
|
|
786
|
+
// repeats the user's typo verbatim is an exact hit, and it must not switch
|
|
787
|
+
// off the slack that still finds the fact spelled right.
|
|
788
|
+
if (term.length >= 5 && !hits.some(h => h.document.kind !== 'session')) {
|
|
789
|
+
const vistos = new Set(hits.map(h => h.document.id));
|
|
790
|
+
const fuzzy = (await run(1)).filter(h => h.document.kind !== 'session' && !vistos.has(h.document.id));
|
|
791
|
+
if (fuzzy.length > 0)
|
|
792
|
+
hits = [...hits, ...fuzzy].sort((a, b) => b.score - a.score);
|
|
667
793
|
}
|
|
668
794
|
return hits;
|
|
669
795
|
}
|
|
@@ -795,6 +921,38 @@ class SearchEngine {
|
|
|
795
921
|
});
|
|
796
922
|
}
|
|
797
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* Index one session log, replacing whatever it had. Called by consolidate
|
|
926
|
+
* for the day just logged and by rebuild for the whole diary.
|
|
927
|
+
*/
|
|
928
|
+
async indexSession(log) {
|
|
929
|
+
if (!this.db)
|
|
930
|
+
await this.init();
|
|
931
|
+
if (!this.db)
|
|
932
|
+
return;
|
|
933
|
+
await this.removeNeuronChunks(`session:${log.session_id}`);
|
|
934
|
+
if (log.summary)
|
|
935
|
+
await this.insertSessionChunks(log);
|
|
936
|
+
this.markDirty();
|
|
937
|
+
}
|
|
938
|
+
async insertSessionChunks(log) {
|
|
939
|
+
const trozos = sessionChunks(String(log.summary || ''));
|
|
940
|
+
for (let i = 0; i < trozos.length; i++) {
|
|
941
|
+
await this.put({
|
|
942
|
+
id: (0, hash_js_1.chunkId)(`session:${log.session_id}`, trozos[i]),
|
|
943
|
+
neuron: `session:${log.session_id}`,
|
|
944
|
+
name: log.session_id,
|
|
945
|
+
text: trozos[i],
|
|
946
|
+
keys: '',
|
|
947
|
+
kind: 'session',
|
|
948
|
+
eid: `${log.session_id}#${i}`,
|
|
949
|
+
domain: '',
|
|
950
|
+
tags: '',
|
|
951
|
+
added: log.date || '',
|
|
952
|
+
heat: 0,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
798
956
|
async put(doc) {
|
|
799
957
|
try {
|
|
800
958
|
await (0, orama_1.insert)(this.db, doc);
|
|
@@ -815,8 +973,9 @@ class SearchEngine {
|
|
|
815
973
|
}
|
|
816
974
|
ids.add(String(doc.id));
|
|
817
975
|
// Headers are name + tags: lexical only, they would only add noise to
|
|
818
|
-
// the vector index.
|
|
819
|
-
|
|
976
|
+
// the vector index. Session logs stay lexical too, for now: the words a
|
|
977
|
+
// day was described with are the words it is asked about.
|
|
978
|
+
if (this.semantic && doc.kind !== 'header' && doc.kind !== 'session') {
|
|
820
979
|
this.pendingEmbed.set(String(doc.id), String(doc.text || ''));
|
|
821
980
|
}
|
|
822
981
|
}
|