@gamaze/hicortex 0.15.3 → 0.16.1
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 +3 -1
- package/dist/consolidate.d.ts +4 -3
- package/dist/consolidate.js +37 -13
- package/dist/db.js +83 -3
- package/dist/distiller.js +19 -12
- package/dist/eval/recall-sweep.d.ts +23 -0
- package/dist/eval/recall-sweep.js +288 -2
- package/dist/eval/relevance-eval.d.ts +64 -0
- package/dist/eval/relevance-eval.js +1954 -0
- package/dist/index.js +16 -6
- package/dist/init.d.ts +1 -1
- package/dist/init.js +53 -100
- package/dist/lessons-context.js +3 -2
- package/dist/llm.d.ts +3 -1
- package/dist/llm.js +18 -4
- package/dist/mcp-server.js +24 -19
- package/dist/memory-instructions.js +1 -1
- package/dist/prompts.js +22 -6
- package/dist/recall-hook-cli.d.ts +1 -1
- package/dist/recall-hook-cli.js +7 -2
- package/dist/recall-index.d.ts +76 -6
- package/dist/recall-index.js +101 -22
- package/dist/retrieval.d.ts +52 -1
- package/dist/retrieval.js +144 -23
- package/dist/seed-lesson.d.ts +1 -1
- package/dist/seed-lesson.js +1 -1
- package/dist/storage.d.ts +56 -3
- package/dist/storage.js +93 -17
- package/dist/types.d.ts +4 -0
- package/dist/uninstall.js +18 -4
- package/hermes-plugin/hicortex/client.py +8 -4
- package/hermes-plugin/hicortex/config.py +11 -0
- package/hermes-plugin/hicortex/provider.py +11 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/skills/hicortex-activate/SKILL.md +0 -53
- package/skills/hicortex-learn/SKILL.md +0 -40
package/dist/recall-index.d.ts
CHANGED
|
@@ -25,13 +25,31 @@ import { SessionRecallRegistry } from "./recall-registry.js";
|
|
|
25
25
|
export interface RecallIndexOptions {
|
|
26
26
|
/** Minimum measured cosine for vector-only candidates (config
|
|
27
27
|
* `recallMinSimilarity`). FTS-matched candidates pass regardless — a BM25
|
|
28
|
-
* text match is direct evidence of relevance. Default 0.
|
|
29
|
-
*
|
|
28
|
+
* text match is direct evidence of relevance. Default 0.62 (raised from 0.55
|
|
29
|
+
* on 2026-08-03 per a 0.01-step floor sweep on the rewritten corpus): steady
|
|
30
|
+
* ~3:1 noise:signal removal with no knee; 0.62 = +2.2pts precision, 10/98
|
|
31
|
+
* prompts silent, sits below the 0.63 local pessimum. The floor is a noise
|
|
32
|
+
* dial, NOT a silence mechanism — at 0.62 each correctly-silenced empty prompt
|
|
33
|
+
* comes with ~1.5 wrongly-silenced (real signal); a non-cosine gate is the
|
|
34
|
+
* real silence fix (eval #3 §4). */
|
|
30
35
|
minSimilarity?: number;
|
|
31
|
-
/** Max index lines per response (config `recallMaxItems`). Default
|
|
36
|
+
/** Max index lines per response (config `recallMaxItems`). Default 5
|
|
37
|
+
* (lowered from 6 on 2026-08-03). Per-slot decomposition at floor 0.62:
|
|
38
|
+
* slot 6 gives NO prompt its first relevant memory — "6 is wrong" is the
|
|
39
|
+
* robust, prompt-set-independent finding, and 5 captures it. The K-sweep
|
|
40
|
+
* is monotone (precision@4 33.7% > @6 30.6% > @8 28.3%), so 4 is
|
|
41
|
+
* lower-noise — but the 4-vs-5 distinction rests on 5 of 98 prompts and is
|
|
42
|
+
* overfitting-fragile (K and the floor were tuned on the same set); 5 hedges
|
|
43
|
+
* with coverage at modest cost. Lower to 4 if a fresh-prompt eval replicates. */
|
|
32
44
|
maxItems?: number;
|
|
33
45
|
/** Prompts shorter than this are skipped (continuations, "yes", "do it"). */
|
|
34
46
|
minPromptLength?: number;
|
|
47
|
+
/** Max chars of the memory's first line shown in an index entry (config
|
|
48
|
+
* `recallTitleChars`). Default 100 (reverted from 150 on 2026-08-03): the
|
|
49
|
+
* full-corpus relevance eval (#3, §5) found 100 vs 150 statistically
|
|
50
|
+
* identical (0.6pts apart, N=40, full CI overlap); 100 saves ~13% tokens
|
|
51
|
+
* per block. */
|
|
52
|
+
titleChars?: number;
|
|
35
53
|
}
|
|
36
54
|
export interface RecallIndexResult {
|
|
37
55
|
status: number;
|
|
@@ -39,15 +57,44 @@ export interface RecallIndexResult {
|
|
|
39
57
|
}
|
|
40
58
|
/** First content line, de-markdowned and truncated — the index line title. */
|
|
41
59
|
export declare function memoryTitle(content: string, maxLen?: number): string;
|
|
42
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Render one production index line. Exported (2026-08-02, relevance eval #v2)
|
|
62
|
+
* so the eval can measure the REAL rendered surface instead of reimplementing
|
|
63
|
+
* it — `maxLen` threads through to `memoryTitle` unchanged (default
|
|
64
|
+
* DEFAULT_TITLE_CHARS = 100, config `recallTitleChars`) so the eval's snippet-length
|
|
65
|
+
* sweep (spec §4.2) can call this SAME function at 100/150/title1sent without
|
|
66
|
+
* duplicating the date/scope/agent/type meta-line logic.
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatIndexLine(r: MemorySearchResult & {
|
|
69
|
+
domain?: string | null;
|
|
70
|
+
}, maxLen?: number): string;
|
|
71
|
+
/**
|
|
72
|
+
* Relevance gate: a real BM25 text match (FTS) passes unconditionally; a
|
|
73
|
+
* vector-only candidate must clear `minSimilarity`.
|
|
74
|
+
*
|
|
75
|
+
* NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
|
|
76
|
+
* weight toward FTS-sourced entries. In practice FTS is currently inert on
|
|
77
|
+
* real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
|
|
78
|
+
* a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
|
|
79
|
+
* is safe as measured. But FTS quality is unmeasured; if FTS starts firing
|
|
80
|
+
* (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
|
|
81
|
+
*/
|
|
43
82
|
export declare function passesRelevanceGate(r: MemorySearchResult, minSimilarity: number): boolean;
|
|
44
83
|
/** Recall filters a client may push per request (#193 review F1): a scoped
|
|
45
84
|
* plugin (Hermes privacy_filter / default_project) must be able to narrow
|
|
46
85
|
* recall exactly like the legacy /search prefetch did — dropping them
|
|
47
|
-
* silently would leak out-of-scope memory titles into the injected index.
|
|
86
|
+
* silently would leak out-of-scope memory titles into the injected index.
|
|
87
|
+
*
|
|
88
|
+
* #203: `project` and `mission_domains` are now SOFT affinity signals in
|
|
89
|
+
* retrieval (zero-boost neutral, never a filter / penalty); `privacy` stays a
|
|
90
|
+
* hard filter (security boundary). They ride the body → retrieveFn →
|
|
91
|
+
* retrieve() → computeScore path unchanged in shape. */
|
|
48
92
|
export interface RecallFilters {
|
|
49
93
|
project?: string;
|
|
50
94
|
privacy?: string[];
|
|
95
|
+
/** #203: Hermes mission domains (declared in plugin config). Soft domain
|
|
96
|
+
* affinity in computeScore via max overlapping memory_tags.weight. */
|
|
97
|
+
mission_domains?: string[];
|
|
51
98
|
}
|
|
52
99
|
export interface RecallIndexDeps {
|
|
53
100
|
db: Database.Database;
|
|
@@ -58,9 +105,15 @@ export interface RecallIndexDeps {
|
|
|
58
105
|
retrieveFn: (query: string, limit: number, filters: RecallFilters | undefined, sessionId: string) => Promise<MemorySearchResult[]>;
|
|
59
106
|
options?: RecallIndexOptions;
|
|
60
107
|
}
|
|
108
|
+
/** Normalize a request-supplied string-list param: array of strings or a CSV
|
|
109
|
+
* string → string[] | undefined. Anything else (or an empty result) means
|
|
110
|
+
* "absent" — never a partial guess. Shared by `parsePrivacyParam` and
|
|
111
|
+
* `mission_domains` (#203) so both accept `["A","B"]` and `"A, B"` alike. */
|
|
112
|
+
export declare function parseStringListParam(v: unknown): string[] | undefined;
|
|
61
113
|
/** Normalize a request-supplied privacy filter: array of strings or a CSV
|
|
62
114
|
* string → string[] | undefined. Anything else (or an empty result) means
|
|
63
|
-
* "no filter" — never a partial guess.
|
|
115
|
+
* "no filter" — never a partial guess. Delegates to parseStringListParam;
|
|
116
|
+
* kept as a named export for tests and handleMemoryGet callers. */
|
|
64
117
|
export declare function parsePrivacyParam(v: unknown): string[] | undefined;
|
|
65
118
|
/**
|
|
66
119
|
* Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
|
|
@@ -83,3 +136,20 @@ export declare function handleMemoryGet(db: Database.Database, query: {
|
|
|
83
136
|
id?: unknown;
|
|
84
137
|
privacy?: unknown;
|
|
85
138
|
}): RecallIndexResult;
|
|
139
|
+
/**
|
|
140
|
+
* MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
|
|
141
|
+
* text block the MCP tool returns (provenance header + the SHARED citation +
|
|
142
|
+
* content). Extracted from the MCP tool handler so its output — incl. the
|
|
143
|
+
* #204 FETCHED marker, which rides on handleMemoryGet's citation — is unit-
|
|
144
|
+
* testable. The citation string is built ONCE (handleMemoryGet); this only
|
|
145
|
+
* frames it, mirroring how /recall-index is shared across harnesses. The
|
|
146
|
+
* extraction closes the #207 gap (CC's MCP path had a marker-less citation
|
|
147
|
+
* built inline, while the REST path used handleMemoryGet — same contract, two
|
|
148
|
+
* implementations, one updated).
|
|
149
|
+
*/
|
|
150
|
+
export declare function formatMemoryGetText(db: Database.Database, query: {
|
|
151
|
+
id?: unknown;
|
|
152
|
+
}): {
|
|
153
|
+
status: number;
|
|
154
|
+
text: string;
|
|
155
|
+
};
|
package/dist/recall-index.js
CHANGED
|
@@ -55,18 +55,33 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
55
55
|
})();
|
|
56
56
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
57
|
exports.memoryTitle = memoryTitle;
|
|
58
|
+
exports.formatIndexLine = formatIndexLine;
|
|
58
59
|
exports.passesRelevanceGate = passesRelevanceGate;
|
|
60
|
+
exports.parseStringListParam = parseStringListParam;
|
|
59
61
|
exports.parsePrivacyParam = parsePrivacyParam;
|
|
60
62
|
exports.handleRecallIndex = handleRecallIndex;
|
|
61
63
|
exports.handleMemoryGet = handleMemoryGet;
|
|
64
|
+
exports.formatMemoryGetText = formatMemoryGetText;
|
|
62
65
|
const storage = __importStar(require("./storage.js"));
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
/** Relevance-gate floor for vector-only candidates (config `recallMinSimilarity`).
|
|
67
|
+
* 0.62 (was 0.55; raised 2026-08-03 on the fine-grain floor sweep — see the
|
|
68
|
+
* minSimilarity doc above). */
|
|
69
|
+
const DEFAULT_MIN_SIMILARITY = 0.62;
|
|
70
|
+
/** Max index lines per pushed recall block (config `recallMaxItems`).
|
|
71
|
+
* 5 (was 6; lowered 2026-08-03 — slot 6 is pure padding at floor 0.62). */
|
|
72
|
+
const DEFAULT_MAX_ITEMS = 5;
|
|
65
73
|
const DEFAULT_MIN_PROMPT_LENGTH = 20;
|
|
66
|
-
/**
|
|
74
|
+
/** Default index-line title length. 100 (reverted from 150 on 2026-08-03:
|
|
75
|
+
* eval #3 §5 showed 100 vs 150 statistically identical; 100 saves ~13% tokens). */
|
|
76
|
+
const DEFAULT_TITLE_CHARS = 100;
|
|
77
|
+
/** Over-fetch multiplier: retrieve `maxItems × 3` candidates so gating + dedup
|
|
78
|
+
* still leave a full menu. Kept at 3 after maxItems 6→5 and the higher floor —
|
|
79
|
+
* permit-short is intended (returning fewer than maxItems when fewer clear the
|
|
80
|
+
* gate is correct, not a defect); raise only if blocks are persistently
|
|
81
|
+
* under-filled in production. */
|
|
67
82
|
const CANDIDATE_MULTIPLIER = 3;
|
|
68
83
|
/** First content line, de-markdowned and truncated — the index line title. */
|
|
69
|
-
function memoryTitle(content, maxLen =
|
|
84
|
+
function memoryTitle(content, maxLen = DEFAULT_TITLE_CHARS) {
|
|
70
85
|
const firstLine = content
|
|
71
86
|
.split("\n")
|
|
72
87
|
.map((l) => l.trim())
|
|
@@ -86,22 +101,49 @@ function formatDate(iso) {
|
|
|
86
101
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
87
102
|
return `${dd}.${mm}.${d.getFullYear()}`;
|
|
88
103
|
}
|
|
89
|
-
|
|
90
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Render one production index line. Exported (2026-08-02, relevance eval #v2)
|
|
106
|
+
* so the eval can measure the REAL rendered surface instead of reimplementing
|
|
107
|
+
* it — `maxLen` threads through to `memoryTitle` unchanged (default
|
|
108
|
+
* DEFAULT_TITLE_CHARS = 100, config `recallTitleChars`) so the eval's snippet-length
|
|
109
|
+
* sweep (spec §4.2) can call this SAME function at 100/150/title1sent without
|
|
110
|
+
* duplicating the date/scope/agent/type meta-line logic.
|
|
111
|
+
*/
|
|
112
|
+
function formatIndexLine(r, maxLen = DEFAULT_TITLE_CHARS) {
|
|
113
|
+
// Provenance (#202): date, scope (domain else project), ORIGIN AGENT, type.
|
|
114
|
+
// The origin agent lets a reader calibrate trust — "from my session" vs
|
|
115
|
+
// another agent/project — before fetching or acting on an entry.
|
|
116
|
+
const meta = [
|
|
117
|
+
formatDate(r.created_at),
|
|
118
|
+
r.domain ?? r.project ?? undefined,
|
|
119
|
+
r.source_agent ?? undefined,
|
|
120
|
+
r.memory_type,
|
|
121
|
+
]
|
|
91
122
|
.filter(Boolean)
|
|
92
123
|
.join(", ");
|
|
93
|
-
return `- [${r.id}] ${memoryTitle(r.content)}${meta ? ` (${meta})` : ""}`;
|
|
124
|
+
return `- [${r.id}] ${memoryTitle(r.content, maxLen)}${meta ? ` (${meta})` : ""}`;
|
|
94
125
|
}
|
|
95
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Relevance gate: a real BM25 text match (FTS) passes unconditionally; a
|
|
128
|
+
* vector-only candidate must clear `minSimilarity`.
|
|
129
|
+
*
|
|
130
|
+
* NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
|
|
131
|
+
* weight toward FTS-sourced entries. In practice FTS is currently inert on
|
|
132
|
+
* real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
|
|
133
|
+
* a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
|
|
134
|
+
* is safe as measured. But FTS quality is unmeasured; if FTS starts firing
|
|
135
|
+
* (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
|
|
136
|
+
*/
|
|
96
137
|
function passesRelevanceGate(r, minSimilarity) {
|
|
97
138
|
if (r.source === "fts" || r.source === "both")
|
|
98
139
|
return true;
|
|
99
140
|
return typeof r.similarity === "number" && r.similarity >= minSimilarity;
|
|
100
141
|
}
|
|
101
|
-
/** Normalize a request-supplied
|
|
142
|
+
/** Normalize a request-supplied string-list param: array of strings or a CSV
|
|
102
143
|
* string → string[] | undefined. Anything else (or an empty result) means
|
|
103
|
-
* "
|
|
104
|
-
|
|
144
|
+
* "absent" — never a partial guess. Shared by `parsePrivacyParam` and
|
|
145
|
+
* `mission_domains` (#203) so both accept `["A","B"]` and `"A, B"` alike. */
|
|
146
|
+
function parseStringListParam(v) {
|
|
105
147
|
const items = Array.isArray(v)
|
|
106
148
|
? v.filter((x) => typeof x === "string")
|
|
107
149
|
: typeof v === "string"
|
|
@@ -110,6 +152,13 @@ function parsePrivacyParam(v) {
|
|
|
110
152
|
const cleaned = items.map((s) => s.trim()).filter(Boolean);
|
|
111
153
|
return cleaned.length > 0 ? cleaned : undefined;
|
|
112
154
|
}
|
|
155
|
+
/** Normalize a request-supplied privacy filter: array of strings or a CSV
|
|
156
|
+
* string → string[] | undefined. Anything else (or an empty result) means
|
|
157
|
+
* "no filter" — never a partial guess. Delegates to parseStringListParam;
|
|
158
|
+
* kept as a named export for tests and handleMemoryGet callers. */
|
|
159
|
+
function parsePrivacyParam(v) {
|
|
160
|
+
return parseStringListParam(v);
|
|
161
|
+
}
|
|
113
162
|
/**
|
|
114
163
|
* Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
|
|
115
164
|
* all behavior lives here so tests exercise it directly.
|
|
@@ -132,13 +181,18 @@ async function handleRecallIndex(deps, body) {
|
|
|
132
181
|
return { status: 200, body: { block: null, skipped: "short-prompt" } };
|
|
133
182
|
}
|
|
134
183
|
const maxItems = clampInt(deps.options?.maxItems, DEFAULT_MAX_ITEMS, 1, 20);
|
|
184
|
+
const titleChars = clampInt(deps.options?.titleChars, DEFAULT_TITLE_CHARS, 40, 400);
|
|
135
185
|
const minSimilarity = clampNumber(deps.options?.minSimilarity, DEFAULT_MIN_SIMILARITY, 0, 1);
|
|
136
186
|
const turn = deps.registry.beginTurn(sessionId);
|
|
137
|
-
// Optional client-side scoping (F1): project +
|
|
138
|
-
//
|
|
187
|
+
// Optional client-side scoping (F1 + #203): project + mission_domains (soft
|
|
188
|
+
// affinity) and privacy (hard filter) ride the body and are pushed into
|
|
189
|
+
// retrieval. project is cwd-derived (CC/OC) or gateway-supplied; mission_domains
|
|
190
|
+
// is Hermes-declared (plugin config). Neither excludes anything — both are
|
|
191
|
+
// zero-boost-neutral score terms in computeScore.
|
|
139
192
|
const filters = {
|
|
140
193
|
project: typeof req.project === "string" && req.project ? req.project : undefined,
|
|
141
194
|
privacy: parsePrivacyParam(req.privacy),
|
|
195
|
+
mission_domains: parseStringListParam(req.mission_domains),
|
|
142
196
|
};
|
|
143
197
|
let results;
|
|
144
198
|
try {
|
|
@@ -161,16 +215,18 @@ async function handleRecallIndex(deps, body) {
|
|
|
161
215
|
deps.registry.markShown(sessionId, ids);
|
|
162
216
|
// Exposure signal: shown_count + last_accessed refresh, NOT access_count.
|
|
163
217
|
storage.touchMemoriesShown(deps.db, ids, new Date().toISOString());
|
|
164
|
-
const lines = picked.map((r) => formatIndexLine(r));
|
|
218
|
+
const lines = picked.map((r) => formatIndexLine(r, titleChars));
|
|
165
219
|
const block = [
|
|
166
220
|
"## Memory recall (auto)",
|
|
167
|
-
// Provenance is BUILT IN
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
221
|
+
// Provenance is BUILT IN (owner decision 27.07, option D; extended #202/#204):
|
|
222
|
+
// - #202: origin agent in each one-liner (formatIndexLine) — trust calibration.
|
|
223
|
+
// - #204: confidence levels — FETCHED (read in full) vs SNIPPET (one-line entry only),
|
|
224
|
+
// so "the agent cited a memory" can no longer pass as "the agent read it".
|
|
225
|
+
// This header carries the SELECTION-time rules: supersession (the one moment
|
|
226
|
+
// competing dates are visible side by side) and cite-with-confidence. The
|
|
227
|
+
// full citation format rides on the hicortex_get response / GET /memory
|
|
228
|
+
// `citation` field (use-time, marked FETCHED).
|
|
229
|
+
"Possibly relevant memories — dates matter, newer supersedes older. Fetch with `hicortex_get(id)` when an entry could change your action. Cite what you rely on by id + date, and mark it `FETCHED` if you read the full memory or `SNIPPET` if you're citing the one-line entry unread — don't pass a SNIPPET citation off as established fact.",
|
|
174
230
|
...lines,
|
|
175
231
|
].join("\n");
|
|
176
232
|
return { status: 200, body: { block, shown: ids, turn } };
|
|
@@ -212,10 +268,33 @@ function handleMemoryGet(db, query) {
|
|
|
212
268
|
status: 200,
|
|
213
269
|
body: {
|
|
214
270
|
memory: mem,
|
|
215
|
-
citation: `(memory ${String(mem.id).slice(0, 8)}, ${date}, from ${mem.source_agent ?? "unknown"})`,
|
|
271
|
+
citation: `(memory ${String(mem.id).slice(0, 8)}, ${date}, from ${mem.source_agent ?? "unknown"}, FETCHED)`,
|
|
216
272
|
},
|
|
217
273
|
};
|
|
218
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
|
|
277
|
+
* text block the MCP tool returns (provenance header + the SHARED citation +
|
|
278
|
+
* content). Extracted from the MCP tool handler so its output — incl. the
|
|
279
|
+
* #204 FETCHED marker, which rides on handleMemoryGet's citation — is unit-
|
|
280
|
+
* testable. The citation string is built ONCE (handleMemoryGet); this only
|
|
281
|
+
* frames it, mirroring how /recall-index is shared across harnesses. The
|
|
282
|
+
* extraction closes the #207 gap (CC's MCP path had a marker-less citation
|
|
283
|
+
* built inline, while the REST path used handleMemoryGet — same contract, two
|
|
284
|
+
* implementations, one updated).
|
|
285
|
+
*/
|
|
286
|
+
function formatMemoryGetText(db, query) {
|
|
287
|
+
const r = handleMemoryGet(db, query);
|
|
288
|
+
if (r.status !== 200) {
|
|
289
|
+
return { status: r.status, text: String(r.body.error ?? `No memory with id ${query.id ?? ""}`) };
|
|
290
|
+
}
|
|
291
|
+
const mem = r.body.memory;
|
|
292
|
+
const citation = r.body.citation; // carries FETCHED (#204)
|
|
293
|
+
const date = (mem.created_at ?? "").slice(0, 10);
|
|
294
|
+
const header = `[memory ${mem.id} | ${mem.memory_type ?? "episode"} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
|
|
295
|
+
`Cite as ${citation} where this shapes your answer; it may be stale — newer memories supersede older.`;
|
|
296
|
+
return { status: 200, text: `${header}\n\n${mem.content ?? ""}` };
|
|
297
|
+
}
|
|
219
298
|
function clampInt(v, dflt, min, max) {
|
|
220
299
|
const n = Number(v);
|
|
221
300
|
if (!Number.isFinite(n))
|
package/dist/retrieval.d.ts
CHANGED
|
@@ -60,12 +60,28 @@ interface ScoringWeights {
|
|
|
60
60
|
freshnessBoostDays: number;
|
|
61
61
|
freshnessBoostWeight: number;
|
|
62
62
|
supersededDemotion: number;
|
|
63
|
+
/** #203 soft affinity boost on exact project match. */
|
|
64
|
+
projectAffinity: number;
|
|
65
|
+
/** #203 soft affinity boost multiplier on max overlapping domain-tag weight. */
|
|
66
|
+
domainAffinity: number;
|
|
67
|
+
/** #205 RRF k parameter (1/(k+rank+1)). Larger ⇒ shallower rank curve. */
|
|
68
|
+
rrfK: number;
|
|
69
|
+
/** #205 composite-score share of the final blend (RRF gets the remainder). */
|
|
70
|
+
rrfCompositeWeight: number;
|
|
71
|
+
/** #205 per-list RRF weight for the FTS list (BM25-driven candidates). */
|
|
72
|
+
rrfFtsWeight: number;
|
|
73
|
+
/** #205 per-list RRF weight for the vector list (KNN-driven candidates). */
|
|
74
|
+
rrfVectorWeight: number;
|
|
63
75
|
}
|
|
64
76
|
/**
|
|
65
77
|
* Configure scoring weights + ranking knobs from config. Called at boot by the
|
|
66
78
|
* server and the nightly (alongside configureDecay/configureRecall) so
|
|
67
79
|
* retrieval and consolidation rank identically. Invalid/absent values keep the
|
|
68
|
-
* shipped default per key. Returns the resolved set for logging/tests.
|
|
80
|
+
* shipped default per key. Returns the resolved set for logging/tests. Also
|
|
81
|
+
* pushes the #205 BM25F field weights into storage (storage.configureBm25Fts)
|
|
82
|
+
* so searchFts ranks with the same config — BM25F weights live in storage.ts
|
|
83
|
+
* (next to the FTS column declaration they mirror) but are read here from the
|
|
84
|
+
* SAME config object for one-place tuning.
|
|
69
85
|
*/
|
|
70
86
|
export declare function configureScoring(config?: Record<string, unknown> | null): ScoringWeights;
|
|
71
87
|
/** Current resolved weights (tests + status output). */
|
|
@@ -127,9 +143,31 @@ export declare function effectiveStrength(baseStrength: number, lastAccessed: st
|
|
|
127
143
|
/**
|
|
128
144
|
* Return a composite relevance score in [0, 1] for a candidate memory.
|
|
129
145
|
* Exported for exact-value tests of the similarity component (#145).
|
|
146
|
+
*
|
|
147
|
+
* #203 soft affinity (options.scope + options.tagWeights): two additive,
|
|
148
|
+
* graded, zero-boost-neutral terms — project affinity (exact project match)
|
|
149
|
+
* and domain affinity (max overlapping memory_tags.weight × scope). Both are
|
|
150
|
+
* 0 when the scope is absent (byte-identical to pre-#203) and NEVER negative
|
|
151
|
+
* (a foreign memory adds 0, never a penalty — penalties re-introduce
|
|
152
|
+
* soft-exclusion). See `AffinityScope`.
|
|
130
153
|
*/
|
|
154
|
+
export interface AffinityScope {
|
|
155
|
+
/** Exact-match project from the client (CC/OC cwd-derived; /search project). */
|
|
156
|
+
project?: string | null;
|
|
157
|
+
/** Hermes mission domains declared in plugin config. Drawn from the same
|
|
158
|
+
* vocabulary as memory_tags (config `domains`). */
|
|
159
|
+
missionDomains?: string[];
|
|
160
|
+
}
|
|
131
161
|
export declare function computeScore(memory: Memory, distance: number, connectionCount: number, maxConnections: number, now: Date, options?: {
|
|
132
162
|
superseded?: boolean;
|
|
163
|
+
/** #203: when present, project/domain affinity boosts are applied. */
|
|
164
|
+
scope?: AffinityScope;
|
|
165
|
+
/** Candidate's graded domain tags (memory_tags rows). Loaded batched for
|
|
166
|
+
* the whole candidate set in retrieve(); used for domain affinity. */
|
|
167
|
+
tagWeights?: Array<{
|
|
168
|
+
tag: string;
|
|
169
|
+
weight: number | null;
|
|
170
|
+
}>;
|
|
133
171
|
}): number;
|
|
134
172
|
export interface EmbedFn {
|
|
135
173
|
(text: string): Promise<Float32Array>;
|
|
@@ -137,12 +175,25 @@ export interface EmbedFn {
|
|
|
137
175
|
/**
|
|
138
176
|
* Main retrieval: BM25 + vector search with RRF fusion, graph traversal,
|
|
139
177
|
* and composite scoring. Strengthens accessed memories.
|
|
178
|
+
*
|
|
179
|
+
* #203 retrieval scoping: `project` and `missionDomains` are SOFT affinity
|
|
180
|
+
* terms in computeScore (zero-boost neutral, never a penalty), NOT filters.
|
|
181
|
+
* `privacy` remains a hard filter (security boundary). `sourceAgent` remains a
|
|
182
|
+
* hard filter (kept for completeness; no production caller currently passes
|
|
183
|
+
* it). When neither project nor missionDomains is sent, scoring is byte-
|
|
184
|
+
* identical to pre-#203 — the kill-switch / no-op guarantee.
|
|
140
185
|
*/
|
|
141
186
|
export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query: string, options?: {
|
|
142
187
|
limit?: number;
|
|
188
|
+
/** #203: soft project affinity (exact match boost in computeScore).
|
|
189
|
+
* Formerly a hard WHERE filter (#192); softening removes cross-scope
|
|
190
|
+
* starvation without excluding anything. */
|
|
143
191
|
project?: string | null;
|
|
144
192
|
privacy?: string[];
|
|
145
193
|
sourceAgent?: string;
|
|
194
|
+
/** #203: Hermes mission domains (declared in plugin config). Soft domain
|
|
195
|
+
* affinity in computeScore via max overlapping memory_tags.weight. */
|
|
196
|
+
missionDomains?: string[];
|
|
146
197
|
/** #192: skip access strengthening — for pushed recall (/recall-index),
|
|
147
198
|
* where appearing in results must not count as use. */
|
|
148
199
|
noStrengthen?: boolean;
|