hippo-memory 0.27.0 → 0.29.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 +42 -22
- package/dist/cli.js +214 -17
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/consolidate.d.ts +1 -0
- package/dist/consolidate.d.ts.map +1 -1
- package/dist/consolidate.js +38 -1
- package/dist/consolidate.js.map +1 -1
- package/dist/eval.d.ts +35 -0
- package/dist/eval.d.ts.map +1 -1
- package/dist/eval.js +68 -8
- package/dist/eval.js.map +1 -1
- package/dist/hooks.d.ts +1 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +24 -0
- package/dist/hooks.js.map +1 -1
- package/dist/refine-llm.d.ts +53 -0
- package/dist/refine-llm.d.ts.map +1 -0
- package/dist/refine-llm.js +147 -0
- package/dist/refine-llm.js.map +1 -0
- package/dist/replay.d.ts +41 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +117 -0
- package/dist/replay.js.map +1 -0
- package/dist/search.d.ts +26 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +70 -26
- package/dist/search.js.map +1 -1
- package/dist/shared.d.ts +4 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +19 -18
- package/dist/shared.js.map +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-powered refinement of consolidated semantic memories.
|
|
3
|
+
*
|
|
4
|
+
* The rule-based `mergeContents` in consolidate.ts produces functional but
|
|
5
|
+
* ugly semantic memories — typically "[Consolidated from N related memories]"
|
|
6
|
+
* prepended to the longest source, or a bulleted list. `hippo refine` takes
|
|
7
|
+
* those and asks Claude to synthesize a clean, generalized principle.
|
|
8
|
+
*
|
|
9
|
+
* Design choices:
|
|
10
|
+
* - Separate command (not baked into `hippo sleep`) so API-key users opt in.
|
|
11
|
+
* - Idempotent via the `llm-refined` tag — re-running skips already-refined.
|
|
12
|
+
* - Uses fetch directly so no SDK dependency.
|
|
13
|
+
* - On failure (API error, bad response), the original memory is untouched.
|
|
14
|
+
*/
|
|
15
|
+
import { MemoryEntry } from './memory.js';
|
|
16
|
+
export interface RefineOptions {
|
|
17
|
+
apiKey: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
limit?: number;
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
/** Ignore the llm-refined tag and re-refine everything eligible. */
|
|
22
|
+
all?: boolean;
|
|
23
|
+
/** Injected for testing — defaults to the real fetch. */
|
|
24
|
+
fetcher?: typeof fetch;
|
|
25
|
+
}
|
|
26
|
+
export interface RefineResult {
|
|
27
|
+
scanned: number;
|
|
28
|
+
refined: number;
|
|
29
|
+
skipped: number;
|
|
30
|
+
failed: number;
|
|
31
|
+
details: Array<{
|
|
32
|
+
id: string;
|
|
33
|
+
status: 'refined' | 'skipped' | 'failed';
|
|
34
|
+
reason?: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Ask Claude to synthesize a clean semantic memory from the merged content
|
|
39
|
+
* plus the original source memories. Returns the refined content string or
|
|
40
|
+
* `null` when the API call failed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function refineSemanticMemory(merged: string, sources: MemoryEntry[], opts: {
|
|
43
|
+
apiKey: string;
|
|
44
|
+
model?: string;
|
|
45
|
+
fetcher?: typeof fetch;
|
|
46
|
+
}): Promise<string | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Scan the store for consolidated semantic memories, refine each with the
|
|
49
|
+
* LLM, and write the refined content back. Tags with `llm-refined` so
|
|
50
|
+
* repeated runs are idempotent (unless `all` is set).
|
|
51
|
+
*/
|
|
52
|
+
export declare function refineStore(hippoRoot: string, opts: RefineOptions): Promise<RefineResult>;
|
|
53
|
+
//# sourceMappingURL=refine-llm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refine-llm.d.ts","sourceRoot":"","sources":["../src/refine-llm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAS,MAAM,aAAa,CAAC;AASjD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oEAAoE;IACpE,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,yDAAyD;IACzD,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3F;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,WAAW,EAAE,EACtB,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAA;CAAE,GAC/D,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqDxB;AAOD;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,YAAY,CAAC,CA+DvB"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-powered refinement of consolidated semantic memories.
|
|
3
|
+
*
|
|
4
|
+
* The rule-based `mergeContents` in consolidate.ts produces functional but
|
|
5
|
+
* ugly semantic memories — typically "[Consolidated from N related memories]"
|
|
6
|
+
* prepended to the longest source, or a bulleted list. `hippo refine` takes
|
|
7
|
+
* those and asks Claude to synthesize a clean, generalized principle.
|
|
8
|
+
*
|
|
9
|
+
* Design choices:
|
|
10
|
+
* - Separate command (not baked into `hippo sleep`) so API-key users opt in.
|
|
11
|
+
* - Idempotent via the `llm-refined` tag — re-running skips already-refined.
|
|
12
|
+
* - Uses fetch directly so no SDK dependency.
|
|
13
|
+
* - On failure (API error, bad response), the original memory is untouched.
|
|
14
|
+
*/
|
|
15
|
+
import { Layer } from './memory.js';
|
|
16
|
+
import { loadAllEntries, readEntry, writeEntry } from './store.js';
|
|
17
|
+
const REFINED_TAG = 'llm-refined';
|
|
18
|
+
const CONSOLIDATED_MARKERS = [
|
|
19
|
+
'[Consolidated from',
|
|
20
|
+
'[Consolidated pattern from',
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Ask Claude to synthesize a clean semantic memory from the merged content
|
|
24
|
+
* plus the original source memories. Returns the refined content string or
|
|
25
|
+
* `null` when the API call failed.
|
|
26
|
+
*/
|
|
27
|
+
export async function refineSemanticMemory(merged, sources, opts) {
|
|
28
|
+
const model = opts.model ?? 'claude-sonnet-4-6';
|
|
29
|
+
const fetchFn = opts.fetcher ?? fetch;
|
|
30
|
+
const sourceBlock = sources
|
|
31
|
+
.slice(0, 8)
|
|
32
|
+
.map((s, i) => `[source ${i + 1}] ${s.content.slice(0, 400)}`)
|
|
33
|
+
.join('\n\n');
|
|
34
|
+
const prompt = `You are refining a semantic memory in an agent's memory store. The rule-based consolidator merged several related episodic memories into one, but the output is clumsy. Produce a single coherent semantic memory that captures the underlying principle.
|
|
35
|
+
|
|
36
|
+
Rules:
|
|
37
|
+
- Output ONLY the refined content — no preamble, no quote marks, no "Here is...".
|
|
38
|
+
- Keep it concise: one paragraph, no headers, no bullet lists unless the sources are inherently a list.
|
|
39
|
+
- Preserve specific facts (names, numbers, paths, IDs) from the sources.
|
|
40
|
+
- Generalize: state the pattern, not each instance.
|
|
41
|
+
- Do NOT include the "[Consolidated from N ...]" marker.
|
|
42
|
+
|
|
43
|
+
Current merged content:
|
|
44
|
+
${merged}
|
|
45
|
+
|
|
46
|
+
Source memories (up to 8 shown):
|
|
47
|
+
${sourceBlock}`;
|
|
48
|
+
let res;
|
|
49
|
+
try {
|
|
50
|
+
res = await fetchFn('https://api.anthropic.com/v1/messages', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'content-type': 'application/json',
|
|
54
|
+
'x-api-key': opts.apiKey,
|
|
55
|
+
'anthropic-version': '2023-06-01',
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
model,
|
|
59
|
+
max_tokens: 800,
|
|
60
|
+
messages: [{ role: 'user', content: prompt }],
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (!res.ok)
|
|
68
|
+
return null;
|
|
69
|
+
try {
|
|
70
|
+
const data = await res.json();
|
|
71
|
+
const text = data.content?.[0]?.text?.trim() ?? '';
|
|
72
|
+
if (text.length < 10)
|
|
73
|
+
return null;
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isConsolidated(entry) {
|
|
81
|
+
if (entry.layer !== Layer.Semantic)
|
|
82
|
+
return false;
|
|
83
|
+
return CONSOLIDATED_MARKERS.some((m) => entry.content.startsWith(m));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Scan the store for consolidated semantic memories, refine each with the
|
|
87
|
+
* LLM, and write the refined content back. Tags with `llm-refined` so
|
|
88
|
+
* repeated runs are idempotent (unless `all` is set).
|
|
89
|
+
*/
|
|
90
|
+
export async function refineStore(hippoRoot, opts) {
|
|
91
|
+
const result = {
|
|
92
|
+
scanned: 0,
|
|
93
|
+
refined: 0,
|
|
94
|
+
skipped: 0,
|
|
95
|
+
failed: 0,
|
|
96
|
+
details: [],
|
|
97
|
+
};
|
|
98
|
+
const entries = loadAllEntries(hippoRoot);
|
|
99
|
+
let processed = 0;
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (!isConsolidated(entry))
|
|
102
|
+
continue;
|
|
103
|
+
result.scanned++;
|
|
104
|
+
if (!opts.all && entry.tags.includes(REFINED_TAG)) {
|
|
105
|
+
result.skipped++;
|
|
106
|
+
result.details.push({ id: entry.id, status: 'skipped', reason: 'already refined' });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (opts.limit !== undefined && processed >= opts.limit)
|
|
110
|
+
break;
|
|
111
|
+
processed++;
|
|
112
|
+
// Best-effort: walk parents_json (schema v9) to fetch originals. When
|
|
113
|
+
// parents aren't recorded we still refine using just the merged content.
|
|
114
|
+
const sources = [];
|
|
115
|
+
const parentIds = Array.isArray(entry.parents) ? entry.parents : [];
|
|
116
|
+
for (const pid of parentIds) {
|
|
117
|
+
const p = readEntry(hippoRoot, pid);
|
|
118
|
+
if (p)
|
|
119
|
+
sources.push(p);
|
|
120
|
+
}
|
|
121
|
+
const refined = await refineSemanticMemory(entry.content, sources, {
|
|
122
|
+
apiKey: opts.apiKey,
|
|
123
|
+
model: opts.model,
|
|
124
|
+
fetcher: opts.fetcher,
|
|
125
|
+
});
|
|
126
|
+
if (refined === null) {
|
|
127
|
+
result.failed++;
|
|
128
|
+
result.details.push({ id: entry.id, status: 'failed', reason: 'api error or empty response' });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (opts.dryRun) {
|
|
132
|
+
result.refined++;
|
|
133
|
+
result.details.push({ id: entry.id, status: 'refined', reason: 'dry-run (no write)' });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const updated = {
|
|
137
|
+
...entry,
|
|
138
|
+
content: refined,
|
|
139
|
+
tags: entry.tags.includes(REFINED_TAG) ? entry.tags : [...entry.tags, REFINED_TAG],
|
|
140
|
+
};
|
|
141
|
+
writeEntry(hippoRoot, updated);
|
|
142
|
+
result.refined++;
|
|
143
|
+
result.details.push({ id: entry.id, status: 'refined' });
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=refine-llm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refine-llm.js","sourceRoot":"","sources":["../src/refine-llm.ts"],"names":[],"mappings":"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"}
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay — biologically-inspired rehearsal during consolidation.
|
|
3
|
+
*
|
|
4
|
+
* Hippocampal replay is internally-driven reactivation of memories during
|
|
5
|
+
* slow-wave sleep: the brain picks important recent experiences and "plays
|
|
6
|
+
* them back" without external input, strengthening them before they decay.
|
|
7
|
+
* In McClelland's complementary-learning-systems framing, replay is also
|
|
8
|
+
* how episodic memories train the neocortex (interleaved rehearsal); in
|
|
9
|
+
* reward-modulated STDP, replay is gated by dopamine so rewarded
|
|
10
|
+
* experiences get preferentially consolidated.
|
|
11
|
+
*
|
|
12
|
+
* This module implements a lightweight, deterministic analog. During each
|
|
13
|
+
* `hippo sleep`, we sample N surviving memories by a priority score that
|
|
14
|
+
* weighs reward feedback, emotional valence, under-rehearsal, and age —
|
|
15
|
+
* then apply the same retrieval-strengthening dynamics `markRetrieved`
|
|
16
|
+
* applies to real queries. The effect is that important memories stay
|
|
17
|
+
* strong even when the user hasn't explicitly queried them recently.
|
|
18
|
+
*
|
|
19
|
+
* Distinct from the other consolidation passes:
|
|
20
|
+
* - decay: removes what's too weak
|
|
21
|
+
* - physics: moves particles in embedding space
|
|
22
|
+
* - merge: collapses near-duplicate episodics into semantics
|
|
23
|
+
* - REPLAY: picks winners and rehearses them (this file)
|
|
24
|
+
*/
|
|
25
|
+
import type { MemoryEntry } from './memory.js';
|
|
26
|
+
/**
|
|
27
|
+
* Priority score used to rank survivors for replay. Higher = more likely
|
|
28
|
+
* to be sampled. Pure function of the entry and current time.
|
|
29
|
+
*/
|
|
30
|
+
export declare function replayPriority(entry: MemoryEntry, now: Date): number;
|
|
31
|
+
/**
|
|
32
|
+
* Pick `count` memories for replay, weighted by `replayPriority`, without
|
|
33
|
+
* replacement. Deterministic given the same seed.
|
|
34
|
+
*
|
|
35
|
+
* The sampler is weighted but not greedy — picking by priority alone would
|
|
36
|
+
* always pick the top-N, which overfits. Biological replay shows both
|
|
37
|
+
* preferential-for-reward AND stochastic-exploration characteristics; we
|
|
38
|
+
* keep the stochastic element so adjacent survivors aren't always ignored.
|
|
39
|
+
*/
|
|
40
|
+
export declare function sampleForReplay(survivors: MemoryEntry[], count: number, now: Date, seed?: number): MemoryEntry[];
|
|
41
|
+
//# sourceMappingURL=replay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAoB,MAAM,aAAa,CAAC;AASjE;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,IAAI,GAAG,MAAM,CAyBpE;AAiBD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,WAAW,EAAE,EACxB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,IAAI,EACT,IAAI,GAAE,MAAyB,GAC9B,WAAW,EAAE,CAoCf"}
|
package/dist/replay.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay — biologically-inspired rehearsal during consolidation.
|
|
3
|
+
*
|
|
4
|
+
* Hippocampal replay is internally-driven reactivation of memories during
|
|
5
|
+
* slow-wave sleep: the brain picks important recent experiences and "plays
|
|
6
|
+
* them back" without external input, strengthening them before they decay.
|
|
7
|
+
* In McClelland's complementary-learning-systems framing, replay is also
|
|
8
|
+
* how episodic memories train the neocortex (interleaved rehearsal); in
|
|
9
|
+
* reward-modulated STDP, replay is gated by dopamine so rewarded
|
|
10
|
+
* experiences get preferentially consolidated.
|
|
11
|
+
*
|
|
12
|
+
* This module implements a lightweight, deterministic analog. During each
|
|
13
|
+
* `hippo sleep`, we sample N surviving memories by a priority score that
|
|
14
|
+
* weighs reward feedback, emotional valence, under-rehearsal, and age —
|
|
15
|
+
* then apply the same retrieval-strengthening dynamics `markRetrieved`
|
|
16
|
+
* applies to real queries. The effect is that important memories stay
|
|
17
|
+
* strong even when the user hasn't explicitly queried them recently.
|
|
18
|
+
*
|
|
19
|
+
* Distinct from the other consolidation passes:
|
|
20
|
+
* - decay: removes what's too weak
|
|
21
|
+
* - physics: moves particles in embedding space
|
|
22
|
+
* - merge: collapses near-duplicate episodics into semantics
|
|
23
|
+
* - REPLAY: picks winners and rehearses them (this file)
|
|
24
|
+
*/
|
|
25
|
+
const VALENCE_WEIGHT = {
|
|
26
|
+
neutral: 1.0,
|
|
27
|
+
positive: 1.3,
|
|
28
|
+
negative: 1.5,
|
|
29
|
+
critical: 2.0,
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Priority score used to rank survivors for replay. Higher = more likely
|
|
33
|
+
* to be sampled. Pure function of the entry and current time.
|
|
34
|
+
*/
|
|
35
|
+
export function replayPriority(entry, now) {
|
|
36
|
+
const pos = entry.outcome_positive ?? 0;
|
|
37
|
+
const neg = entry.outcome_negative ?? 0;
|
|
38
|
+
// Reward signal: neutral memories get 1, strongly-rewarded memories > 1,
|
|
39
|
+
// negative-dominated memories floor at 0.1 (so they're still eligible, just
|
|
40
|
+
// much less likely to be sampled than neutral peers). Clamp is required
|
|
41
|
+
// because sampleForReplay depends on all weights being positive.
|
|
42
|
+
const rewardSignal = Math.max(0.1, 1 + pos * 0.5 + (pos - neg) * 0.25);
|
|
43
|
+
const valence = VALENCE_WEIGHT[entry.emotional_valence] ?? 1.0;
|
|
44
|
+
// Under-rehearsed memories benefit most from replay.
|
|
45
|
+
const underRehearsed = 1 / (1 + (entry.retrieval_count ?? 0));
|
|
46
|
+
// Idle-time boost: memories that haven't been touched recently need rehearsal more.
|
|
47
|
+
const lastRetrieved = new Date(entry.last_retrieved);
|
|
48
|
+
const deltaMs = now.getTime() - lastRetrieved.getTime();
|
|
49
|
+
const ageHours = Number.isFinite(deltaMs) ? Math.max(0, deltaMs / 3_600_000) : 0;
|
|
50
|
+
const idleBoost = 1 + Math.log1p(ageHours) * 0.1;
|
|
51
|
+
// Weight by current strength so dead-and-decaying memories don't waste replay slots.
|
|
52
|
+
const rawStrength = Number.isFinite(entry.strength) ? entry.strength : 0;
|
|
53
|
+
const strengthFloor = Math.max(0.1, rawStrength);
|
|
54
|
+
return rewardSignal * valence * underRehearsed * idleBoost * strengthFloor;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Deterministic 32-bit RNG (Mulberry32). Same seed → same sequence.
|
|
58
|
+
* Keeps replay reproducible for tests and audit runs without bringing
|
|
59
|
+
* in a random-number dependency.
|
|
60
|
+
*/
|
|
61
|
+
function mulberry32(seed) {
|
|
62
|
+
let s = seed >>> 0;
|
|
63
|
+
return () => {
|
|
64
|
+
let t = (s += 0x6D2B79F5);
|
|
65
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
66
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
67
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Pick `count` memories for replay, weighted by `replayPriority`, without
|
|
72
|
+
* replacement. Deterministic given the same seed.
|
|
73
|
+
*
|
|
74
|
+
* The sampler is weighted but not greedy — picking by priority alone would
|
|
75
|
+
* always pick the top-N, which overfits. Biological replay shows both
|
|
76
|
+
* preferential-for-reward AND stochastic-exploration characteristics; we
|
|
77
|
+
* keep the stochastic element so adjacent survivors aren't always ignored.
|
|
78
|
+
*/
|
|
79
|
+
export function sampleForReplay(survivors, count, now, seed = Date.now() >>> 0) {
|
|
80
|
+
if (count <= 0 || survivors.length === 0)
|
|
81
|
+
return [];
|
|
82
|
+
const rng = mulberry32(seed);
|
|
83
|
+
// Stale memories have been deliberately marked as untrusted; rehearsing
|
|
84
|
+
// them would defeat the purpose of staleness. Skip them entirely.
|
|
85
|
+
const eligible = survivors.filter((e) => e.confidence !== 'stale');
|
|
86
|
+
if (eligible.length === 0)
|
|
87
|
+
return [];
|
|
88
|
+
const pool = eligible.map((entry, idx) => ({
|
|
89
|
+
entry,
|
|
90
|
+
idx,
|
|
91
|
+
weight: replayPriority(entry, now),
|
|
92
|
+
}));
|
|
93
|
+
const want = Math.min(count, pool.length);
|
|
94
|
+
const chosen = [];
|
|
95
|
+
const taken = new Set();
|
|
96
|
+
for (let k = 0; k < want; k++) {
|
|
97
|
+
let totalW = 0;
|
|
98
|
+
for (const p of pool)
|
|
99
|
+
if (!taken.has(p.idx))
|
|
100
|
+
totalW += p.weight;
|
|
101
|
+
if (totalW <= 0)
|
|
102
|
+
break;
|
|
103
|
+
let r = rng() * totalW;
|
|
104
|
+
for (const p of pool) {
|
|
105
|
+
if (taken.has(p.idx))
|
|
106
|
+
continue;
|
|
107
|
+
r -= p.weight;
|
|
108
|
+
if (r <= 0) {
|
|
109
|
+
chosen.push(p.entry);
|
|
110
|
+
taken.add(p.idx);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return chosen;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"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"}
|
package/dist/search.d.ts
CHANGED
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
import { MemoryEntry } from './memory.js';
|
|
6
6
|
import type { PhysicsConfig } from './physics-config.js';
|
|
7
7
|
export declare function tokenize(text: string): string[];
|
|
8
|
+
/**
|
|
9
|
+
* Tokenized BM25 corpus. Callers can pre-build this with `buildCorpus` once
|
|
10
|
+
* and reuse across many `hybridSearch` calls on the same entry set — the
|
|
11
|
+
* tokenization work is the bulk of per-query cost on large stores.
|
|
12
|
+
*/
|
|
13
|
+
export interface BM25Corpus {
|
|
14
|
+
docs: string[][];
|
|
15
|
+
avgLen: number;
|
|
16
|
+
df: Map<string, number>;
|
|
17
|
+
N: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function buildCorpus(texts: string[]): BM25Corpus;
|
|
8
20
|
/**
|
|
9
21
|
* Rough token estimate: characters / 4 (works well for English text).
|
|
10
22
|
*/
|
|
@@ -81,6 +93,18 @@ export declare function hybridSearch(query: string, entries: MemoryEntry[], opti
|
|
|
81
93
|
mmr?: boolean;
|
|
82
94
|
/** MMR balance: 1.0 = pure relevance, 0.0 = pure diversity. Default 0.7. */
|
|
83
95
|
mmrLambda?: number;
|
|
96
|
+
/** Scoring mode: 'blend' (weighted sum of BM25+cosine, default) or
|
|
97
|
+
* 'rrf' (reciprocal rank fusion - combines BM25 and cosine ranks
|
|
98
|
+
* instead of scores, more robust for long documents). */
|
|
99
|
+
scoring?: 'blend' | 'rrf';
|
|
100
|
+
/** Pre-built BM25 corpus from `buildCorpus`. Pass this across many
|
|
101
|
+
* queries on the same entry set to skip ~O(N*docLen) tokenization
|
|
102
|
+
* work per call. Must be built from the same `entries` in the same
|
|
103
|
+
* order (content + tags.join(' ')). */
|
|
104
|
+
preparedCorpus?: BM25Corpus;
|
|
105
|
+
/** Minimum number of results to return regardless of budget.
|
|
106
|
+
* Prevents budget saturation when memories are large. Default 1. */
|
|
107
|
+
minResults?: number;
|
|
84
108
|
}): Promise<SearchResult[]>;
|
|
85
109
|
/**
|
|
86
110
|
* MMR (Maximal Marginal Relevance) re-ranking.
|
|
@@ -105,6 +129,7 @@ export declare function physicsSearch(query: string, entries: MemoryEntry[], opt
|
|
|
105
129
|
physicsConfig?: PhysicsConfig;
|
|
106
130
|
queryEmbedding?: number[];
|
|
107
131
|
explain?: boolean;
|
|
132
|
+
minResults?: number;
|
|
108
133
|
}): Promise<SearchResult[]>;
|
|
109
134
|
/**
|
|
110
135
|
* Search entries using BM25 + strength + recency composite score.
|
|
@@ -118,6 +143,7 @@ export declare function search(query: string, entries: MemoryEntry[], options?:
|
|
|
118
143
|
budget?: number;
|
|
119
144
|
now?: Date;
|
|
120
145
|
hippoRoot?: string;
|
|
146
|
+
minResults?: number;
|
|
121
147
|
}): SearchResult[];
|
|
122
148
|
/**
|
|
123
149
|
* Update retrieval metadata on entries that were returned by a search.
|
package/dist/search.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAqB,MAAM,aAAa,CAAC;AAY7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAM/C;
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"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"}
|
package/dist/search.js
CHANGED
|
@@ -21,7 +21,7 @@ export function tokenize(text) {
|
|
|
21
21
|
}
|
|
22
22
|
const BM25_K1 = 1.5;
|
|
23
23
|
const BM25_B = 0.75;
|
|
24
|
-
function buildCorpus(texts) {
|
|
24
|
+
export function buildCorpus(texts) {
|
|
25
25
|
const docs = texts.map(tokenize);
|
|
26
26
|
const N = docs.length;
|
|
27
27
|
const df = new Map();
|
|
@@ -87,8 +87,10 @@ function recencyBoost(entry, now) {
|
|
|
87
87
|
export async function hybridSearch(query, entries, options = {}) {
|
|
88
88
|
const now = options.now ?? new Date();
|
|
89
89
|
const budget = options.budget ?? 4000;
|
|
90
|
+
const minResults = options.minResults ?? 1;
|
|
90
91
|
const embeddingWeight = options.embeddingWeight ?? 0.6;
|
|
91
92
|
const bm25Weight = 1 - embeddingWeight;
|
|
93
|
+
const scoringMode = options.scoring ?? 'blend';
|
|
92
94
|
const explain = options.explain ?? false;
|
|
93
95
|
const mmrEnabled = options.mmr ?? true;
|
|
94
96
|
const mmrLambda = options.mmrLambda ?? 0.7;
|
|
@@ -97,9 +99,9 @@ export async function hybridSearch(query, entries, options = {}) {
|
|
|
97
99
|
const queryTerms = tokenize(query);
|
|
98
100
|
if (queryTerms.length === 0)
|
|
99
101
|
return [];
|
|
100
|
-
// Build BM25 corpus
|
|
101
|
-
const
|
|
102
|
-
|
|
102
|
+
// Build BM25 corpus (or reuse one the caller already built).
|
|
103
|
+
const corpus = options.preparedCorpus
|
|
104
|
+
?? buildCorpus(entries.map((e) => `${e.content} ${e.tags.join(' ')}`));
|
|
103
105
|
// Score all entries with BM25
|
|
104
106
|
const bm25Scores = entries.map((_, i) => bm25Score(corpus, i, queryTerms));
|
|
105
107
|
const maxBm25 = bm25Scores.reduce((a, b) => Math.max(a, b), 1e-9);
|
|
@@ -122,12 +124,46 @@ export async function hybridSearch(query, entries, options = {}) {
|
|
|
122
124
|
// Fall through to BM25-only
|
|
123
125
|
}
|
|
124
126
|
}
|
|
127
|
+
// Compute cosine similarities for RRF ranking (need all before scoring)
|
|
128
|
+
const cosineScores = new Array(entries.length).fill(0);
|
|
129
|
+
const hadCachedVecs = new Array(entries.length).fill(false);
|
|
130
|
+
if (useEmbeddings) {
|
|
131
|
+
for (let i = 0; i < entries.length; i++) {
|
|
132
|
+
const cached = embeddingIndex[entries[i].id];
|
|
133
|
+
hadCachedVecs[i] = Boolean(cached && queryVector.length > 0);
|
|
134
|
+
cosineScores[i] = hadCachedVecs[i]
|
|
135
|
+
? Math.max(0, cosineSimilarity(queryVector, cached))
|
|
136
|
+
: 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// For RRF: build rank maps from BM25 and cosine orderings
|
|
140
|
+
let rrfScores = null;
|
|
141
|
+
if (useEmbeddings && scoringMode === 'rrf') {
|
|
142
|
+
const RRF_K = 60;
|
|
143
|
+
const bm25Ranked = entries.map((_, i) => i).filter(i => bm25Scores[i] > 0 || cosineScores[i] > 0);
|
|
144
|
+
bm25Ranked.sort((a, b) => bm25Scores[b] - bm25Scores[a]);
|
|
145
|
+
const cosineRanked = entries.map((_, i) => i).filter(i => bm25Scores[i] > 0 || cosineScores[i] > 0);
|
|
146
|
+
cosineRanked.sort((a, b) => cosineScores[b] - cosineScores[a]);
|
|
147
|
+
const bm25RankMap = new Map();
|
|
148
|
+
bm25Ranked.forEach((idx, rank) => bm25RankMap.set(idx, rank + 1));
|
|
149
|
+
const cosineRankMap = new Map();
|
|
150
|
+
cosineRanked.forEach((idx, rank) => cosineRankMap.set(idx, rank + 1));
|
|
151
|
+
rrfScores = new Map();
|
|
152
|
+
const allCandidates = new Set([...bm25Ranked, ...cosineRanked]);
|
|
153
|
+
for (const idx of allCandidates) {
|
|
154
|
+
const bm25Rank = bm25RankMap.get(idx) ?? (entries.length + 1);
|
|
155
|
+
const cosineRank = cosineRankMap.get(idx) ?? (entries.length + 1);
|
|
156
|
+
rrfScores.set(idx, bm25Weight / (RRF_K + bm25Rank) + embeddingWeight / (RRF_K + cosineRank));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
125
159
|
// Score each entry
|
|
126
160
|
const scored = [];
|
|
127
161
|
const currentPathTags = extractPathTags(process.cwd());
|
|
128
162
|
const queryTermSet = new Set(queryTerms);
|
|
129
163
|
for (let i = 0; i < entries.length; i++) {
|
|
130
164
|
const rawBm25 = bm25Scores[i];
|
|
165
|
+
const cosineScore = cosineScores[i];
|
|
166
|
+
const hadCachedVec = hadCachedVecs[i];
|
|
131
167
|
if (!useEmbeddings && rawBm25 <= 0)
|
|
132
168
|
continue;
|
|
133
169
|
const normBm25 = rawBm25 / maxBm25;
|
|
@@ -136,25 +172,19 @@ export async function hybridSearch(query, entries, options = {}) {
|
|
|
136
172
|
const strengthMultiplier = 0.5 + 0.5 * strength;
|
|
137
173
|
const recencyMultiplier = 0.8 + 0.2 * recency;
|
|
138
174
|
let compositeScore;
|
|
139
|
-
let cosineScore = 0;
|
|
140
175
|
let base;
|
|
141
176
|
let modeLabel;
|
|
142
|
-
let hadCachedVec = false;
|
|
143
177
|
if (useEmbeddings) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
178
|
+
if (rrfScores) {
|
|
179
|
+
base = rrfScores.get(i) ?? 0;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
base = bm25Weight * normBm25 + embeddingWeight * cosineScore;
|
|
183
|
+
}
|
|
150
184
|
compositeScore = base * strengthMultiplier * recencyMultiplier;
|
|
151
|
-
// Distinguish "real hybrid" from "query embedded but doc has no cached
|
|
152
|
-
// vector" so explain can tell users their index is stale without lying
|
|
153
|
-
// that the search actually used embeddings.
|
|
154
185
|
modeLabel = hadCachedVec ? 'hybrid' : 'hybrid-no-vec';
|
|
155
186
|
}
|
|
156
187
|
else {
|
|
157
|
-
// Pure BM25 path: identical to original behavior
|
|
158
188
|
base = queryTerms.length > 0 ? rawBm25 / queryTerms.length : rawBm25;
|
|
159
189
|
compositeScore = base * strengthMultiplier * recencyMultiplier;
|
|
160
190
|
modeLabel = 'bm25-only';
|
|
@@ -218,17 +248,29 @@ export async function hybridSearch(query, entries, options = {}) {
|
|
|
218
248
|
// diversity. Only applies when embeddings are loaded (doc-to-doc similarity
|
|
219
249
|
// is via cosine of cached vectors); otherwise we return the pure-relevance
|
|
220
250
|
// ordering unchanged.
|
|
251
|
+
//
|
|
252
|
+
// MMR is O(K^2) in cosine similarity ops, which on large corpora (1000+
|
|
253
|
+
// candidates) dominates query time. Cap the re-ranking window to the top
|
|
254
|
+
// relevance-scored candidates — anything below top-K was never going to
|
|
255
|
+
// surface anyway after budget filtering.
|
|
256
|
+
const MMR_CANDIDATE_CAP = 100;
|
|
221
257
|
const applyMmr = mmrEnabled && useEmbeddings && scored.length > 1 && mmrLambda < 1;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
258
|
+
let ordered;
|
|
259
|
+
if (applyMmr) {
|
|
260
|
+
const head = scored.slice(0, MMR_CANDIDATE_CAP);
|
|
261
|
+
const tail = scored.slice(MMR_CANDIDATE_CAP);
|
|
262
|
+
ordered = [...mmrRerank(head, embeddingIndex, mmrLambda, explain), ...tail];
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
ordered = scored;
|
|
266
|
+
}
|
|
267
|
+
// Apply token budget (guarantee at least minResults items)
|
|
226
268
|
const results = [];
|
|
227
269
|
let usedTokens = 0;
|
|
228
270
|
for (let i = 0; i < ordered.length; i++) {
|
|
229
271
|
const tokens = ordered[i].tokens;
|
|
230
|
-
if (
|
|
231
|
-
continue;
|
|
272
|
+
if (results.length >= minResults && usedTokens + tokens > budget)
|
|
273
|
+
continue;
|
|
232
274
|
usedTokens += tokens;
|
|
233
275
|
results.push(ordered[i]);
|
|
234
276
|
}
|
|
@@ -300,6 +342,7 @@ export function mmrRerank(scored, embeddingIndex, lambda, explain) {
|
|
|
300
342
|
export async function physicsSearch(query, entries, options = {}) {
|
|
301
343
|
const now = options.now ?? new Date();
|
|
302
344
|
const budget = options.budget ?? 4000;
|
|
345
|
+
const minResults = options.minResults ?? 1;
|
|
303
346
|
const config = options.physicsConfig ?? DEFAULT_PHYSICS_CONFIG;
|
|
304
347
|
const explain = options.explain ?? false;
|
|
305
348
|
if (entries.length === 0 || !options.hippoRoot)
|
|
@@ -403,8 +446,8 @@ export async function physicsSearch(query, entries, options = {}) {
|
|
|
403
446
|
let usedTokens = 0;
|
|
404
447
|
for (let i = 0; i < merged.length; i++) {
|
|
405
448
|
const tokens = merged[i].tokens;
|
|
406
|
-
if (
|
|
407
|
-
continue;
|
|
449
|
+
if (results.length >= minResults && usedTokens + tokens > budget)
|
|
450
|
+
continue;
|
|
408
451
|
usedTokens += tokens;
|
|
409
452
|
results.push(merged[i]);
|
|
410
453
|
}
|
|
@@ -435,6 +478,7 @@ export function search(query, entries, options = {}) {
|
|
|
435
478
|
// Synchronous path: BM25 only (no async hybrid)
|
|
436
479
|
const now = options.now ?? new Date();
|
|
437
480
|
const budget = options.budget ?? 4000;
|
|
481
|
+
const minResults = options.minResults ?? 1;
|
|
438
482
|
if (entries.length === 0)
|
|
439
483
|
return [];
|
|
440
484
|
const queryTerms = tokenize(query);
|
|
@@ -474,8 +518,8 @@ export function search(query, entries, options = {}) {
|
|
|
474
518
|
let usedTokens = 0;
|
|
475
519
|
for (let i = 0; i < scored.length; i++) {
|
|
476
520
|
const tokens = scored[i].tokens;
|
|
477
|
-
if (
|
|
478
|
-
continue;
|
|
521
|
+
if (results.length >= minResults && usedTokens + tokens > budget)
|
|
522
|
+
continue;
|
|
479
523
|
usedTokens += tokens;
|
|
480
524
|
results.push(scored[i]);
|
|
481
525
|
}
|