@gamaze/hicortex 0.15.2 → 0.16.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 +3 -1
- package/dist/consolidate.d.ts +4 -3
- package/dist/consolidate.js +33 -12
- package/dist/db.js +83 -3
- package/dist/eval/recall-sweep.d.ts +82 -0
- package/dist/eval/recall-sweep.js +1001 -0
- package/dist/index.js +13 -4
- package/dist/init.d.ts +1 -1
- package/dist/init.js +53 -100
- package/dist/llm.d.ts +3 -1
- package/dist/llm.js +18 -4
- package/dist/mcp-server.js +50 -26
- package/dist/memory-instructions.js +2 -2
- package/dist/recall-hook-cli.d.ts +1 -1
- package/dist/recall-hook-cli.js +7 -2
- package/dist/recall-index.d.ts +37 -3
- package/dist/recall-index.js +62 -15
- package/dist/recall-registry.d.ts +39 -1
- package/dist/recall-registry.js +52 -1
- package/dist/retrieval.d.ts +88 -1
- package/dist/retrieval.js +204 -26
- package/dist/schema-prototypes.d.ts +15 -0
- package/dist/schema-prototypes.js +24 -0
- 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.js
CHANGED
|
@@ -56,9 +56,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
56
56
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
57
|
exports.memoryTitle = memoryTitle;
|
|
58
58
|
exports.passesRelevanceGate = passesRelevanceGate;
|
|
59
|
+
exports.parseStringListParam = parseStringListParam;
|
|
59
60
|
exports.parsePrivacyParam = parsePrivacyParam;
|
|
60
61
|
exports.handleRecallIndex = handleRecallIndex;
|
|
61
62
|
exports.handleMemoryGet = handleMemoryGet;
|
|
63
|
+
exports.formatMemoryGetText = formatMemoryGetText;
|
|
62
64
|
const storage = __importStar(require("./storage.js"));
|
|
63
65
|
const DEFAULT_MIN_SIMILARITY = 0.55;
|
|
64
66
|
const DEFAULT_MAX_ITEMS = 6;
|
|
@@ -87,7 +89,15 @@ function formatDate(iso) {
|
|
|
87
89
|
return `${dd}.${mm}.${d.getFullYear()}`;
|
|
88
90
|
}
|
|
89
91
|
function formatIndexLine(r) {
|
|
90
|
-
|
|
92
|
+
// Provenance (#202): date, scope (domain else project), ORIGIN AGENT, type.
|
|
93
|
+
// The origin agent lets a reader calibrate trust — "from my session" vs
|
|
94
|
+
// another agent/project — before fetching or acting on an entry.
|
|
95
|
+
const meta = [
|
|
96
|
+
formatDate(r.created_at),
|
|
97
|
+
r.domain ?? r.project ?? undefined,
|
|
98
|
+
r.source_agent ?? undefined,
|
|
99
|
+
r.memory_type,
|
|
100
|
+
]
|
|
91
101
|
.filter(Boolean)
|
|
92
102
|
.join(", ");
|
|
93
103
|
return `- [${r.id}] ${memoryTitle(r.content)}${meta ? ` (${meta})` : ""}`;
|
|
@@ -98,10 +108,11 @@ function passesRelevanceGate(r, minSimilarity) {
|
|
|
98
108
|
return true;
|
|
99
109
|
return typeof r.similarity === "number" && r.similarity >= minSimilarity;
|
|
100
110
|
}
|
|
101
|
-
/** Normalize a request-supplied
|
|
111
|
+
/** Normalize a request-supplied string-list param: array of strings or a CSV
|
|
102
112
|
* string → string[] | undefined. Anything else (or an empty result) means
|
|
103
|
-
* "
|
|
104
|
-
|
|
113
|
+
* "absent" — never a partial guess. Shared by `parsePrivacyParam` and
|
|
114
|
+
* `mission_domains` (#203) so both accept `["A","B"]` and `"A, B"` alike. */
|
|
115
|
+
function parseStringListParam(v) {
|
|
105
116
|
const items = Array.isArray(v)
|
|
106
117
|
? v.filter((x) => typeof x === "string")
|
|
107
118
|
: typeof v === "string"
|
|
@@ -110,6 +121,13 @@ function parsePrivacyParam(v) {
|
|
|
110
121
|
const cleaned = items.map((s) => s.trim()).filter(Boolean);
|
|
111
122
|
return cleaned.length > 0 ? cleaned : undefined;
|
|
112
123
|
}
|
|
124
|
+
/** Normalize a request-supplied privacy filter: array of strings or a CSV
|
|
125
|
+
* string → string[] | undefined. Anything else (or an empty result) means
|
|
126
|
+
* "no filter" — never a partial guess. Delegates to parseStringListParam;
|
|
127
|
+
* kept as a named export for tests and handleMemoryGet callers. */
|
|
128
|
+
function parsePrivacyParam(v) {
|
|
129
|
+
return parseStringListParam(v);
|
|
130
|
+
}
|
|
113
131
|
/**
|
|
114
132
|
* Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
|
|
115
133
|
* all behavior lives here so tests exercise it directly.
|
|
@@ -134,15 +152,19 @@ async function handleRecallIndex(deps, body) {
|
|
|
134
152
|
const maxItems = clampInt(deps.options?.maxItems, DEFAULT_MAX_ITEMS, 1, 20);
|
|
135
153
|
const minSimilarity = clampNumber(deps.options?.minSimilarity, DEFAULT_MIN_SIMILARITY, 0, 1);
|
|
136
154
|
const turn = deps.registry.beginTurn(sessionId);
|
|
137
|
-
// Optional client-side scoping (F1): project +
|
|
138
|
-
//
|
|
155
|
+
// Optional client-side scoping (F1 + #203): project + mission_domains (soft
|
|
156
|
+
// affinity) and privacy (hard filter) ride the body and are pushed into
|
|
157
|
+
// retrieval. project is cwd-derived (CC/OC) or gateway-supplied; mission_domains
|
|
158
|
+
// is Hermes-declared (plugin config). Neither excludes anything — both are
|
|
159
|
+
// zero-boost-neutral score terms in computeScore.
|
|
139
160
|
const filters = {
|
|
140
161
|
project: typeof req.project === "string" && req.project ? req.project : undefined,
|
|
141
162
|
privacy: parsePrivacyParam(req.privacy),
|
|
163
|
+
mission_domains: parseStringListParam(req.mission_domains),
|
|
142
164
|
};
|
|
143
165
|
let results;
|
|
144
166
|
try {
|
|
145
|
-
results = await deps.retrieveFn(prompt, maxItems * CANDIDATE_MULTIPLIER, filters);
|
|
167
|
+
results = await deps.retrieveFn(prompt, maxItems * CANDIDATE_MULTIPLIER, filters, sessionId);
|
|
146
168
|
}
|
|
147
169
|
catch (err) {
|
|
148
170
|
return {
|
|
@@ -164,13 +186,15 @@ async function handleRecallIndex(deps, body) {
|
|
|
164
186
|
const lines = picked.map((r) => formatIndexLine(r));
|
|
165
187
|
const block = [
|
|
166
188
|
"## Memory recall (auto)",
|
|
167
|
-
// Provenance is BUILT IN
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
189
|
+
// Provenance is BUILT IN (owner decision 27.07, option D; extended #202/#204):
|
|
190
|
+
// - #202: origin agent in each one-liner (formatIndexLine) — trust calibration.
|
|
191
|
+
// - #204: confidence levels — FETCHED (read in full) vs SNIPPET (one-line entry only),
|
|
192
|
+
// so "the agent cited a memory" can no longer pass as "the agent read it".
|
|
193
|
+
// This header carries the SELECTION-time rules: supersession (the one moment
|
|
194
|
+
// competing dates are visible side by side) and cite-with-confidence. The
|
|
195
|
+
// full citation format rides on the hicortex_get response / GET /memory
|
|
196
|
+
// `citation` field (use-time, marked FETCHED).
|
|
197
|
+
"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
198
|
...lines,
|
|
175
199
|
].join("\n");
|
|
176
200
|
return { status: 200, body: { block, shown: ids, turn } };
|
|
@@ -212,10 +236,33 @@ function handleMemoryGet(db, query) {
|
|
|
212
236
|
status: 200,
|
|
213
237
|
body: {
|
|
214
238
|
memory: mem,
|
|
215
|
-
citation: `(memory ${String(mem.id).slice(0, 8)}, ${date}, from ${mem.source_agent ?? "unknown"})`,
|
|
239
|
+
citation: `(memory ${String(mem.id).slice(0, 8)}, ${date}, from ${mem.source_agent ?? "unknown"}, FETCHED)`,
|
|
216
240
|
},
|
|
217
241
|
};
|
|
218
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
|
|
245
|
+
* text block the MCP tool returns (provenance header + the SHARED citation +
|
|
246
|
+
* content). Extracted from the MCP tool handler so its output — incl. the
|
|
247
|
+
* #204 FETCHED marker, which rides on handleMemoryGet's citation — is unit-
|
|
248
|
+
* testable. The citation string is built ONCE (handleMemoryGet); this only
|
|
249
|
+
* frames it, mirroring how /recall-index is shared across harnesses. The
|
|
250
|
+
* extraction closes the #207 gap (CC's MCP path had a marker-less citation
|
|
251
|
+
* built inline, while the REST path used handleMemoryGet — same contract, two
|
|
252
|
+
* implementations, one updated).
|
|
253
|
+
*/
|
|
254
|
+
function formatMemoryGetText(db, query) {
|
|
255
|
+
const r = handleMemoryGet(db, query);
|
|
256
|
+
if (r.status !== 200) {
|
|
257
|
+
return { status: r.status, text: String(r.body.error ?? `No memory with id ${query.id ?? ""}`) };
|
|
258
|
+
}
|
|
259
|
+
const mem = r.body.memory;
|
|
260
|
+
const citation = r.body.citation; // carries FETCHED (#204)
|
|
261
|
+
const date = (mem.created_at ?? "").slice(0, 10);
|
|
262
|
+
const header = `[memory ${mem.id} | ${mem.memory_type ?? "episode"} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
|
|
263
|
+
`Cite as ${citation} where this shapes your answer; it may be stale — newer memories supersede older.`;
|
|
264
|
+
return { status: 200, text: `${header}\n\n${mem.content ?? ""}` };
|
|
265
|
+
}
|
|
219
266
|
function clampInt(v, dflt, min, max) {
|
|
220
267
|
const n = Number(v);
|
|
221
268
|
if (!Number.isFinite(n))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SessionRecallRegistry — per-session, TURN-based dedup for pushed recall
|
|
3
|
-
* (#192, POST /recall-index)
|
|
3
|
+
* (#192, POST /recall-index), and owner of the session-intent rolling
|
|
4
|
+
* centroid (#192 session-intent keying, 0.15.3).
|
|
4
5
|
*
|
|
5
6
|
* Why turn-based, not time-based: suppression must track the session's
|
|
6
7
|
* CONTEXT, not the wall clock. A multi-day CC session with a 1M window can
|
|
@@ -17,6 +18,19 @@
|
|
|
17
18
|
* SessionStart hook — which includes source=compact, i.e. after
|
|
18
19
|
* compaction the fresh context may legitimately re-receive everything).
|
|
19
20
|
*
|
|
21
|
+
* Session-intent centroid (0.15.3): a rolling EMA of the session's prompt
|
|
22
|
+
* embeddings lives on SessionState. The recall path blends the current prompt
|
|
23
|
+
* with this centroid before the vector search so recall follows the session's
|
|
24
|
+
* intent instead of being query-literal. reset() deletes the whole session
|
|
25
|
+
* entry, so the centroid is cleared for free on SessionStart/compact — the
|
|
26
|
+
* next recall re-seeds.
|
|
27
|
+
*
|
|
28
|
+
* Concurrency: the registry assumes ONE in-flight recall per session at a time
|
|
29
|
+
* (CC's UserPromptSubmit fires once per turn; Hermes/OC plugins call per-turn
|
|
30
|
+
* too). Two concurrent same-session calls could race updateCentroid and drop
|
|
31
|
+
* one EMA step — harmless (self-correcting on the next turn) and not worth a
|
|
32
|
+
* lock for a path that does not fire concurrently in any current harness.
|
|
33
|
+
*
|
|
20
34
|
* Purely in-memory: a server restart forgets shown-state, worst case a few
|
|
21
35
|
* early re-shows (~15 tokens each) — harmless by design. Sessions are pruned
|
|
22
36
|
* LRU beyond maxSessions so long-running servers don't accumulate state.
|
|
@@ -41,6 +55,30 @@ export declare class SessionRecallRegistry {
|
|
|
41
55
|
markShown(sessionId: string, memoryIds: string[]): void;
|
|
42
56
|
/** Forget a session's shown-set (SessionStart / compaction). */
|
|
43
57
|
reset(sessionId: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Current session-intent centroid, or undefined when no prompt has seeded it
|
|
60
|
+
* yet (first turn / after a reset). The recall path reads this BEFORE
|
|
61
|
+
* updateCentroid to decide whether to blend — a missing centroid means
|
|
62
|
+
* "first turn, pure prompt, no behavior change".
|
|
63
|
+
*/
|
|
64
|
+
getCentroid(sessionId: string): Float32Array | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Fold this turn's prompt embedding into the session-intent centroid via
|
|
67
|
+
* EMA: `centroid_new = l2Normalize((1-α)·centroid_old + α·prompt)`.
|
|
68
|
+
*
|
|
69
|
+
* First call (no centroid yet) SEEDS the centroid = l2Normalize(prompt) —
|
|
70
|
+
* this is the "after that first recall" step in the design: turn 1's search
|
|
71
|
+
* runs with pure prompt, then the centroid is seeded so turn 2+ can blend.
|
|
72
|
+
*
|
|
73
|
+
* `alpha` is the EMA rate (a shipped constant — retrieval.SESSION_INTENT_ALPHA,
|
|
74
|
+
* 0.4; NOT a config knob per the 0.15.3 scope). Callers (the recall closure)
|
|
75
|
+
* read it from retrieval.getSessionIntent(). We do not re-clamp here — the
|
|
76
|
+
* registry is a pure data owner, not a config interpreter.
|
|
77
|
+
*
|
|
78
|
+
* Returns the new centroid. The centroid lives on SessionState, so reset()
|
|
79
|
+
* (which deletes the session entry) clears it for free.
|
|
80
|
+
*/
|
|
81
|
+
updateCentroid(sessionId: string, promptEmbedding: Float32Array, alpha: number): Float32Array;
|
|
44
82
|
/** Number of tracked sessions (for /recall-index introspection + tests). */
|
|
45
83
|
size(): number;
|
|
46
84
|
private getOrCreate;
|
package/dist/recall-registry.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* SessionRecallRegistry — per-session, TURN-based dedup for pushed recall
|
|
4
|
-
* (#192, POST /recall-index)
|
|
4
|
+
* (#192, POST /recall-index), and owner of the session-intent rolling
|
|
5
|
+
* centroid (#192 session-intent keying, 0.15.3).
|
|
5
6
|
*
|
|
6
7
|
* Why turn-based, not time-based: suppression must track the session's
|
|
7
8
|
* CONTEXT, not the wall clock. A multi-day CC session with a 1M window can
|
|
@@ -18,12 +19,26 @@
|
|
|
18
19
|
* SessionStart hook — which includes source=compact, i.e. after
|
|
19
20
|
* compaction the fresh context may legitimately re-receive everything).
|
|
20
21
|
*
|
|
22
|
+
* Session-intent centroid (0.15.3): a rolling EMA of the session's prompt
|
|
23
|
+
* embeddings lives on SessionState. The recall path blends the current prompt
|
|
24
|
+
* with this centroid before the vector search so recall follows the session's
|
|
25
|
+
* intent instead of being query-literal. reset() deletes the whole session
|
|
26
|
+
* entry, so the centroid is cleared for free on SessionStart/compact — the
|
|
27
|
+
* next recall re-seeds.
|
|
28
|
+
*
|
|
29
|
+
* Concurrency: the registry assumes ONE in-flight recall per session at a time
|
|
30
|
+
* (CC's UserPromptSubmit fires once per turn; Hermes/OC plugins call per-turn
|
|
31
|
+
* too). Two concurrent same-session calls could race updateCentroid and drop
|
|
32
|
+
* one EMA step — harmless (self-correcting on the next turn) and not worth a
|
|
33
|
+
* lock for a path that does not fire concurrently in any current harness.
|
|
34
|
+
*
|
|
21
35
|
* Purely in-memory: a server restart forgets shown-state, worst case a few
|
|
22
36
|
* early re-shows (~15 tokens each) — harmless by design. Sessions are pruned
|
|
23
37
|
* LRU beyond maxSessions so long-running servers don't accumulate state.
|
|
24
38
|
*/
|
|
25
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
40
|
exports.SessionRecallRegistry = exports.DEFAULT_RESHOW_TURNS = void 0;
|
|
41
|
+
const schema_prototypes_js_1 = require("./schema-prototypes.js");
|
|
27
42
|
exports.DEFAULT_RESHOW_TURNS = 30;
|
|
28
43
|
const DEFAULT_MAX_SESSIONS = 500;
|
|
29
44
|
class SessionRecallRegistry {
|
|
@@ -68,6 +83,42 @@ class SessionRecallRegistry {
|
|
|
68
83
|
reset(sessionId) {
|
|
69
84
|
this.sessions.delete(sessionId);
|
|
70
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Current session-intent centroid, or undefined when no prompt has seeded it
|
|
88
|
+
* yet (first turn / after a reset). The recall path reads this BEFORE
|
|
89
|
+
* updateCentroid to decide whether to blend — a missing centroid means
|
|
90
|
+
* "first turn, pure prompt, no behavior change".
|
|
91
|
+
*/
|
|
92
|
+
getCentroid(sessionId) {
|
|
93
|
+
return this.sessions.get(sessionId)?.centroid;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Fold this turn's prompt embedding into the session-intent centroid via
|
|
97
|
+
* EMA: `centroid_new = l2Normalize((1-α)·centroid_old + α·prompt)`.
|
|
98
|
+
*
|
|
99
|
+
* First call (no centroid yet) SEEDS the centroid = l2Normalize(prompt) —
|
|
100
|
+
* this is the "after that first recall" step in the design: turn 1's search
|
|
101
|
+
* runs with pure prompt, then the centroid is seeded so turn 2+ can blend.
|
|
102
|
+
*
|
|
103
|
+
* `alpha` is the EMA rate (a shipped constant — retrieval.SESSION_INTENT_ALPHA,
|
|
104
|
+
* 0.4; NOT a config knob per the 0.15.3 scope). Callers (the recall closure)
|
|
105
|
+
* read it from retrieval.getSessionIntent(). We do not re-clamp here — the
|
|
106
|
+
* registry is a pure data owner, not a config interpreter.
|
|
107
|
+
*
|
|
108
|
+
* Returns the new centroid. The centroid lives on SessionState, so reset()
|
|
109
|
+
* (which deletes the session entry) clears it for free.
|
|
110
|
+
*/
|
|
111
|
+
updateCentroid(sessionId, promptEmbedding, alpha) {
|
|
112
|
+
const s = this.getOrCreate(sessionId);
|
|
113
|
+
if (!s.centroid) {
|
|
114
|
+
s.centroid = (0, schema_prototypes_js_1.l2Normalize)(promptEmbedding);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
s.centroid = (0, schema_prototypes_js_1.l2Normalize)((0, schema_prototypes_js_1.weightedAdd)(s.centroid, 1 - alpha, promptEmbedding, alpha));
|
|
118
|
+
}
|
|
119
|
+
s.lastUsedAt = Date.now();
|
|
120
|
+
return s.centroid;
|
|
121
|
+
}
|
|
71
122
|
/** Number of tracked sessions (for /recall-index introspection + tests). */
|
|
72
123
|
size() {
|
|
73
124
|
return this.sessions.size;
|
package/dist/retrieval.d.ts
CHANGED
|
@@ -60,16 +60,61 @@ 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). */
|
|
72
88
|
export declare function getScoringWeights(): ScoringWeights;
|
|
89
|
+
/** EMA rate for the session-intent centroid: centroid_new = (1-α)·old + α·prompt. */
|
|
90
|
+
export declare const SESSION_INTENT_ALPHA = 0.4;
|
|
91
|
+
/**
|
|
92
|
+
* Configure session-intent keying from config. Called at server boot next to
|
|
93
|
+
* configureScoring (the nightly does no recall, so it does not need this).
|
|
94
|
+
* Reads only `sessionIntentWeight` ([0,1]; 0 = disabled). Invalid/out-of-range
|
|
95
|
+
* values keep the shipped default. Returns `{ weight, alpha }` — alpha is the
|
|
96
|
+
* fixed constant, surfaced so the recall closure passes it to the registry in
|
|
97
|
+
* one call.
|
|
98
|
+
*/
|
|
99
|
+
export declare function configureSessionIntent(config?: Record<string, unknown> | null): {
|
|
100
|
+
weight: number;
|
|
101
|
+
alpha: number;
|
|
102
|
+
};
|
|
103
|
+
/** Current resolved session-intent weight + the shipped alpha (closure + tests). */
|
|
104
|
+
export declare function getSessionIntent(): {
|
|
105
|
+
weight: number;
|
|
106
|
+
alpha: number;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Blend the prompt embedding with the session-intent centroid for the vector
|
|
110
|
+
* search: `query = l2Normalize((1-w)·prompt + w·centroid)`. Returns the prompt
|
|
111
|
+
* UNCHANGED when `centroid` is undefined (first turn — no behavior change) or
|
|
112
|
+
* `weight` is 0 (the kill-switch — pure prompt). Extracted from the
|
|
113
|
+
* /recall-index closure (mcp-server.ts) so the exact blend decision is
|
|
114
|
+
* unit-testable directly, locking the ternary against a refactor without a
|
|
115
|
+
* closure-integration harness.
|
|
116
|
+
*/
|
|
117
|
+
export declare function blendQueryVector(promptEmb: Float32Array, centroid: Float32Array | undefined, weight: number): Float32Array;
|
|
73
118
|
/**
|
|
74
119
|
* Ids among `candidateIds` that have been superseded by a later memory — i.e.
|
|
75
120
|
* they are the SOURCE of a `superseded_by` link (stageSupersession links
|
|
@@ -98,9 +143,31 @@ export declare function effectiveStrength(baseStrength: number, lastAccessed: st
|
|
|
98
143
|
/**
|
|
99
144
|
* Return a composite relevance score in [0, 1] for a candidate memory.
|
|
100
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`.
|
|
101
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
|
+
}
|
|
102
161
|
export declare function computeScore(memory: Memory, distance: number, connectionCount: number, maxConnections: number, now: Date, options?: {
|
|
103
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
|
+
}>;
|
|
104
171
|
}): number;
|
|
105
172
|
export interface EmbedFn {
|
|
106
173
|
(text: string): Promise<Float32Array>;
|
|
@@ -108,15 +175,35 @@ export interface EmbedFn {
|
|
|
108
175
|
/**
|
|
109
176
|
* Main retrieval: BM25 + vector search with RRF fusion, graph traversal,
|
|
110
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.
|
|
111
185
|
*/
|
|
112
186
|
export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query: string, options?: {
|
|
113
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. */
|
|
114
191
|
project?: string | null;
|
|
115
192
|
privacy?: string[];
|
|
116
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[];
|
|
117
197
|
/** #192: skip access strengthening — for pushed recall (/recall-index),
|
|
118
198
|
* where appearing in results must not count as use. */
|
|
119
199
|
noStrengthen?: boolean;
|
|
200
|
+
/** #192 session-intent keying (0.15.3): a pre-computed query embedding
|
|
201
|
+
* (e.g. the session-centroid blend from the /recall-index closure). When
|
|
202
|
+
* provided, the internal embed() call is SKIPPED — the caller owns the
|
|
203
|
+
* one embed per recall. /search and other unblended callers omit this
|
|
204
|
+
* and get pure-prompt behavior (the query string is embedded here). The
|
|
205
|
+
* FTS path still uses the raw `query` text regardless. */
|
|
206
|
+
queryEmbedding?: Float32Array;
|
|
120
207
|
}): Promise<MemorySearchResult[]>;
|
|
121
208
|
/**
|
|
122
209
|
* Get recent context, optionally filtered by project and privacy.
|