@gamaze/hicortex 0.15.1 → 0.15.3
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 +10 -2
- package/dist/consolidate.d.ts +0 -3
- package/dist/consolidate.js +7 -6
- package/dist/eval/recall-sweep.d.ts +59 -0
- package/dist/eval/recall-sweep.js +715 -0
- package/dist/index.js +1 -1
- package/dist/init.js +27 -0
- package/dist/mcp-server.js +32 -8
- package/dist/memory-instructions.js +1 -1
- package/dist/nightly.js +3 -1
- package/dist/recall-index.d.ts +4 -1
- package/dist/recall-index.js +2 -2
- package/dist/recall-registry.d.ts +39 -1
- package/dist/recall-registry.js +52 -1
- package/dist/retrieval.d.ts +68 -3
- package/dist/retrieval.js +158 -9
- package/dist/schema-prototypes.d.ts +15 -0
- package/dist/schema-prototypes.js +24 -0
- package/dist/storage.js +10 -0
- package/dist/telemetry.d.ts +17 -0
- package/dist/telemetry.js +31 -0
- package/dist/uninstall.js +10 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -378,7 +378,7 @@ exports.default = {
|
|
|
378
378
|
}), { name: "hicortex_search" });
|
|
379
379
|
api.registerTool((_ctx) => ({
|
|
380
380
|
name: "hicortex_get",
|
|
381
|
-
description: "Fetch ONE memory's full content by id — use this to lazy-load entries from the '## Memory recall (auto)' index or from search results whose snippet was not enough. Fetching a memory marks it as used (strengthens it), so
|
|
381
|
+
description: "Fetch ONE memory's full content by id — use this to lazy-load entries from the '## Memory recall (auto)' index or from search results whose snippet was not enough. Fetching a memory marks it as used (strengthens it), so fetch entries that could change your action — not every shown one. When the memory shapes your answer, cite it as given in the response.",
|
|
382
382
|
parameters: {
|
|
383
383
|
type: "object",
|
|
384
384
|
properties: {
|
package/dist/init.js
CHANGED
|
@@ -31,6 +31,7 @@ exports.installRecallHooks = installRecallHooks;
|
|
|
31
31
|
exports.runInit = runInit;
|
|
32
32
|
exports.resolveNightlyHour = resolveNightlyHour;
|
|
33
33
|
const paths_js_1 = require("./paths.js");
|
|
34
|
+
const telemetry_js_1 = require("./telemetry.js");
|
|
34
35
|
const node_fs_1 = require("node:fs");
|
|
35
36
|
const node_path_1 = require("node:path");
|
|
36
37
|
const node_os_1 = require("node:os");
|
|
@@ -40,6 +41,24 @@ const node_crypto_1 = require("node:crypto");
|
|
|
40
41
|
const claude_md_js_1 = require("./claude-md.js");
|
|
41
42
|
const context_store_js_1 = require("./context-store.js");
|
|
42
43
|
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
44
|
+
/** This package's version, for the install lifecycle ping (0.15.2). */
|
|
45
|
+
function pkgVersion() {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf-8")).version;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return "0.0.0";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Read the just-written config so the install ping honours an opt-out. */
|
|
54
|
+
function readHomeConfig(home) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(home, "config.json"), "utf-8"));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
43
62
|
const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
|
|
44
63
|
const CC_COMMANDS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "commands");
|
|
45
64
|
const OC_CONFIG = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
|
|
@@ -1301,6 +1320,10 @@ async function runInit(options = {}) {
|
|
|
1301
1320
|
console.log("Anonymous usage telemetry (aggregate counts only, no content) is on by default —");
|
|
1302
1321
|
console.log("see exactly what is sent with `hicortex telemetry`; to opt out, add");
|
|
1303
1322
|
console.log('"telemetry": false to ~/.hicortex/config.json or set HICORTEX_TELEMETRY=off.\n');
|
|
1323
|
+
// Install ping (0.15.2) — sent AFTER the disclosure above, opt-out aware, so
|
|
1324
|
+
// the install → first-nightly → retained funnel is measurable. Never blocks:
|
|
1325
|
+
// failures are swallowed inside sendLifecycleEvent.
|
|
1326
|
+
await (0, telemetry_js_1.sendLifecycleEvent)("install", HICORTEX_HOME, readHomeConfig(HICORTEX_HOME), pkgVersion());
|
|
1304
1327
|
console.log("Next steps:");
|
|
1305
1328
|
console.log(" 1. Restart Claude Code to pick up the new MCP server and SessionStart hook");
|
|
1306
1329
|
if (d.hermesFound) {
|
|
@@ -1471,6 +1494,10 @@ async function runClientInit(serverUrl, agentName) {
|
|
|
1471
1494
|
console.log("Anonymous usage telemetry (aggregate counts only, no content) is on by default —");
|
|
1472
1495
|
console.log("see exactly what is sent with `hicortex telemetry`; to opt out, add");
|
|
1473
1496
|
console.log('"telemetry": false to ~/.hicortex/config.json or set HICORTEX_TELEMETRY=off.\n');
|
|
1497
|
+
// Install ping (0.15.2) — sent AFTER the disclosure above, opt-out aware, so
|
|
1498
|
+
// the install → first-nightly → retained funnel is measurable. Never blocks:
|
|
1499
|
+
// failures are swallowed inside sendLifecycleEvent.
|
|
1500
|
+
await (0, telemetry_js_1.sendLifecycleEvent)("install", HICORTEX_HOME, readHomeConfig(HICORTEX_HOME), pkgVersion());
|
|
1474
1501
|
console.log("How it works:");
|
|
1475
1502
|
console.log(" • MCP tools (search, context, ingest) talk to the remote server");
|
|
1476
1503
|
console.log(" • Nightly pipeline denoises CC transcripts, POSTs to server for distillation");
|
package/dist/mcp-server.js
CHANGED
|
@@ -128,7 +128,7 @@ function createMcpServer() {
|
|
|
128
128
|
}
|
|
129
129
|
});
|
|
130
130
|
// -- hicortex_get --
|
|
131
|
-
server.tool("hicortex_get", "Fetch ONE memory's full content by id — use this to lazy-load entries from the '## Memory recall (auto)' index or from search results whose snippet was not enough. Fetching a memory marks it as used (strengthens it), so
|
|
131
|
+
server.tool("hicortex_get", "Fetch ONE memory's full content by id — use this to lazy-load entries from the '## Memory recall (auto)' index or from search results whose snippet was not enough. Fetching a memory marks it as used (strengthens it), so fetch entries that could change your action — not every shown one. When the memory shapes your answer, cite it to the user (id + date + origin agent).", {
|
|
132
132
|
id: zod_1.z.string().describe("Memory id (as shown in recall index/search results)"),
|
|
133
133
|
}, async ({ id }) => {
|
|
134
134
|
if (!db)
|
|
@@ -487,8 +487,15 @@ async function startServer(options = {}) {
|
|
|
487
487
|
// so calibration is a config edit + restart, never a release.
|
|
488
488
|
retrieval.configureDecay({ halfLifeDays: savedConfig?.decayHalfLifeDays });
|
|
489
489
|
const recallCfg = retrieval.configureRecall(savedConfig);
|
|
490
|
+
const scoringCfg = retrieval.configureScoring(savedConfig);
|
|
491
|
+
const sessionIntentCfg = retrieval.configureSessionIntent(savedConfig);
|
|
490
492
|
console.log(`[hicortex] Recall: k=${recallCfg.searchLimit}/recent=${recallCfg.recentLimit}` +
|
|
491
|
-
`/window=${recallCfg.recentWindowDays}d/cold=${recallCfg.coldExposureSlots}`
|
|
493
|
+
`/window=${recallCfg.recentWindowDays}d/cold=${recallCfg.coldExposureSlots} · ` +
|
|
494
|
+
`score sim=${scoringCfg.similarity}/str=${scoringCfg.strength}/conn=${scoringCfg.connections}` +
|
|
495
|
+
`/rec=${scoringCfg.recency}, fresh=${scoringCfg.freshnessBoostWeight}@${scoringCfg.freshnessBoostDays}d, ` +
|
|
496
|
+
`superseded×${scoringCfg.supersededDemotion}` +
|
|
497
|
+
`, intent w=${sessionIntentCfg.weight}` +
|
|
498
|
+
(sessionIntentCfg.weight === 0 ? " (disabled)" : ""));
|
|
492
499
|
recallRegistry = new recall_registry_js_1.SessionRecallRegistry({
|
|
493
500
|
reshowTurns: savedConfig?.recallReshowTurns,
|
|
494
501
|
});
|
|
@@ -664,12 +671,29 @@ async function startServer(options = {}) {
|
|
|
664
671
|
registry: recallRegistry,
|
|
665
672
|
// Client-pushed project/privacy scoping (F1) rides through to
|
|
666
673
|
// retrieval, which handles the filtered over-fetch itself.
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
674
|
+
// #192 session-intent keying (0.15.3): embed the prompt ONCE here,
|
|
675
|
+
// blend with the session's rolling centroid, and pass the blended
|
|
676
|
+
// vector to retrieve() via queryEmbedding so retrieve() does NOT
|
|
677
|
+
// re-embed. Turn 1 (no centroid yet) and weight=0 both reduce to a
|
|
678
|
+
// pure-prompt search (the kill-switch). The centroid is updated AFTER
|
|
679
|
+
// reading the prior one — so turn 1 searches with pure prompt, then
|
|
680
|
+
// seeds the centroid for turn 2+ to blend against.
|
|
681
|
+
retrieveFn: async (query, limit, filters, sessionId) => {
|
|
682
|
+
const { weight, alpha } = retrieval.getSessionIntent();
|
|
683
|
+
const promptEmb = await (0, embedder_js_1.embed)(query);
|
|
684
|
+
// weight=0 (kill-switch): the centroid is neither read nor written.
|
|
685
|
+
const centroid = weight > 0 ? recallRegistry.getCentroid(sessionId) : undefined;
|
|
686
|
+
const queryVec = retrieval.blendQueryVector(promptEmb, centroid, weight);
|
|
687
|
+
if (weight > 0)
|
|
688
|
+
recallRegistry.updateCentroid(sessionId, promptEmb, alpha);
|
|
689
|
+
return retrieval.retrieve(db, embedder_js_1.embed, query, {
|
|
690
|
+
limit,
|
|
691
|
+
noStrengthen: true,
|
|
692
|
+
project: filters?.project,
|
|
693
|
+
privacy: filters?.privacy,
|
|
694
|
+
queryEmbedding: queryVec,
|
|
695
|
+
});
|
|
696
|
+
},
|
|
673
697
|
options: recallIndexOptions,
|
|
674
698
|
}, req.body);
|
|
675
699
|
res.status(r.status).json(r.body);
|
|
@@ -31,7 +31,7 @@ exports.MEMORY_SECTION_NAME = "memory";
|
|
|
31
31
|
function renderMemoryInstructions() {
|
|
32
32
|
return [
|
|
33
33
|
"Your long-term memory is Hicortex — shared across all agents and sessions.",
|
|
34
|
-
"- A `## Memory recall (auto)` index may arrive with prompts: it is a MENU, not content. Fetch a full memory with `hicortex_get(id)`
|
|
34
|
+
"- A `## Memory recall (auto)` index may arrive with prompts: it is a MENU, not content. Fetch a full memory with `hicortex_get(id)` when the entry could change how you handle the current task.",
|
|
35
35
|
"- Recall before assuming: `hicortex_search` for prior decisions/facts/preferences, `hicortex_recent` to catch up on a project.",
|
|
36
36
|
"- Cite any memory you rely on (id, date); on conflicts, newer memories supersede older.",
|
|
37
37
|
"- Capture is automatic (nightly). Do not manually ingest routine content — `hicortex_ingest` is for explicitly requested learnings only.",
|
package/dist/nightly.js
CHANGED
|
@@ -214,6 +214,7 @@ async function runNightly(options = {}) {
|
|
|
214
214
|
// the server's retrieval path (config decayHalfLifeDays, default 365).
|
|
215
215
|
(0, retrieval_js_1.configureDecay)({ halfLifeDays: savedConfig?.decayHalfLifeDays });
|
|
216
216
|
(0, retrieval_js_1.configureRecall)(savedConfig);
|
|
217
|
+
(0, retrieval_js_1.configureScoring)(savedConfig);
|
|
217
218
|
const modeLabel = captureOnly ? " (capture-only)" : dryRun ? " (dry run)" : "";
|
|
218
219
|
console.log(`[hicortex] Nightly pipeline starting${modeLabel}`);
|
|
219
220
|
if (captureOnly) {
|
|
@@ -393,7 +394,6 @@ async function runNightly(options = {}) {
|
|
|
393
394
|
}, {
|
|
394
395
|
minSimilarity: savedConfig?.supersessionMinSimilarity,
|
|
395
396
|
maxCalls: savedConfig?.supersessionMaxCalls,
|
|
396
|
-
penalty: savedConfig?.supersessionPenalty,
|
|
397
397
|
});
|
|
398
398
|
console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
|
|
399
399
|
(report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
|
|
@@ -436,6 +436,7 @@ async function runNightly(options = {}) {
|
|
|
436
436
|
id: (0, telemetry_js_1.getTelemetryId)(stateDir),
|
|
437
437
|
v: VERSION,
|
|
438
438
|
pv: telemetry_js_1.TELEMETRY_PAYLOAD_VERSION,
|
|
439
|
+
event: "nightly",
|
|
439
440
|
mode: "server",
|
|
440
441
|
agent: agentType,
|
|
441
442
|
mem: storage.countMemories(db),
|
|
@@ -568,6 +569,7 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
|
|
|
568
569
|
id: (0, telemetry_js_1.getTelemetryId)(stateDir),
|
|
569
570
|
v: VERSION,
|
|
570
571
|
pv: telemetry_js_1.TELEMETRY_PAYLOAD_VERSION,
|
|
572
|
+
event: "nightly",
|
|
571
573
|
mode: "client",
|
|
572
574
|
agent: agentType,
|
|
573
575
|
mem: memoriesIngested,
|
package/dist/recall-index.d.ts
CHANGED
|
@@ -52,7 +52,10 @@ export interface RecallFilters {
|
|
|
52
52
|
export interface RecallIndexDeps {
|
|
53
53
|
db: Database.Database;
|
|
54
54
|
registry: SessionRecallRegistry;
|
|
55
|
-
|
|
55
|
+
/** Search closure. `sessionId` is forwarded so the closure (in mcp-server)
|
|
56
|
+
* can resolve/update the session-intent centroid and pass a blended query
|
|
57
|
+
* vector into retrieve() — see #192 session-intent keying (0.15.3). */
|
|
58
|
+
retrieveFn: (query: string, limit: number, filters: RecallFilters | undefined, sessionId: string) => Promise<MemorySearchResult[]>;
|
|
56
59
|
options?: RecallIndexOptions;
|
|
57
60
|
}
|
|
58
61
|
/** Normalize a request-supplied privacy filter: array of strings or a CSV
|
package/dist/recall-index.js
CHANGED
|
@@ -142,7 +142,7 @@ async function handleRecallIndex(deps, body) {
|
|
|
142
142
|
};
|
|
143
143
|
let results;
|
|
144
144
|
try {
|
|
145
|
-
results = await deps.retrieveFn(prompt, maxItems * CANDIDATE_MULTIPLIER, filters);
|
|
145
|
+
results = await deps.retrieveFn(prompt, maxItems * CANDIDATE_MULTIPLIER, filters, sessionId);
|
|
146
146
|
}
|
|
147
147
|
catch (err) {
|
|
148
148
|
return {
|
|
@@ -170,7 +170,7 @@ async function handleRecallIndex(deps, body) {
|
|
|
170
170
|
// and cite-what-you-rely-on (covers snippet-only use, the common case per
|
|
171
171
|
// the 0.14.0 field test). The full citation format + origin agent ride on
|
|
172
172
|
// the hicortex_get response / GET /memory `citation` field (use-time).
|
|
173
|
-
"Possibly relevant memories — dates matter, newer supersedes older. Fetch full content with `hicortex_get(id)`
|
|
173
|
+
"Possibly relevant memories — dates matter, newer supersedes older. Fetch full content with `hicortex_get(id)` when an entry could change your action; cite any memory you rely on (id, date):",
|
|
174
174
|
...lines,
|
|
175
175
|
].join("\n");
|
|
176
176
|
return { status: 200, body: { block, shown: ids, turn } };
|
|
@@ -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
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* Retrieval layer with composite scoring, RRF fusion, and graph traversal.
|
|
3
3
|
* Ported from hicortex/retrieval.py — same scoring model and weights.
|
|
4
4
|
*
|
|
5
|
-
* Scoring model:
|
|
6
|
-
* score = similarity * 0.
|
|
5
|
+
* Scoring model (weights are config-driven since 0.15.2 — see configureScoring):
|
|
6
|
+
* score = similarity * 0.50 + effective_strength * 0.20
|
|
7
|
+
* + connection_score * 0.15 + recency * 0.15
|
|
8
|
+
* + fresh-memory bonus (≤ 0.15, linear over the first 7 days)
|
|
9
|
+
* then × 0.50 if the memory was superseded by a later decision
|
|
7
10
|
*
|
|
8
11
|
* Decay model (B+E+D):
|
|
9
12
|
* base_decay = derived from decayHalfLifeDays (config; default 365 → ~1-year
|
|
@@ -49,6 +52,59 @@ interface RecallDefaults {
|
|
|
49
52
|
* Returns the resolved values (for logging + tests).
|
|
50
53
|
*/
|
|
51
54
|
export declare function configureRecall(config?: Record<string, unknown> | null): RecallDefaults;
|
|
55
|
+
interface ScoringWeights {
|
|
56
|
+
similarity: number;
|
|
57
|
+
strength: number;
|
|
58
|
+
connections: number;
|
|
59
|
+
recency: number;
|
|
60
|
+
freshnessBoostDays: number;
|
|
61
|
+
freshnessBoostWeight: number;
|
|
62
|
+
supersededDemotion: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Configure scoring weights + ranking knobs from config. Called at boot by the
|
|
66
|
+
* server and the nightly (alongside configureDecay/configureRecall) so
|
|
67
|
+
* retrieval and consolidation rank identically. Invalid/absent values keep the
|
|
68
|
+
* shipped default per key. Returns the resolved set for logging/tests.
|
|
69
|
+
*/
|
|
70
|
+
export declare function configureScoring(config?: Record<string, unknown> | null): ScoringWeights;
|
|
71
|
+
/** Current resolved weights (tests + status output). */
|
|
72
|
+
export declare function getScoringWeights(): ScoringWeights;
|
|
73
|
+
/** EMA rate for the session-intent centroid: centroid_new = (1-α)·old + α·prompt. */
|
|
74
|
+
export declare const SESSION_INTENT_ALPHA = 0.4;
|
|
75
|
+
/**
|
|
76
|
+
* Configure session-intent keying from config. Called at server boot next to
|
|
77
|
+
* configureScoring (the nightly does no recall, so it does not need this).
|
|
78
|
+
* Reads only `sessionIntentWeight` ([0,1]; 0 = disabled). Invalid/out-of-range
|
|
79
|
+
* values keep the shipped default. Returns `{ weight, alpha }` — alpha is the
|
|
80
|
+
* fixed constant, surfaced so the recall closure passes it to the registry in
|
|
81
|
+
* one call.
|
|
82
|
+
*/
|
|
83
|
+
export declare function configureSessionIntent(config?: Record<string, unknown> | null): {
|
|
84
|
+
weight: number;
|
|
85
|
+
alpha: number;
|
|
86
|
+
};
|
|
87
|
+
/** Current resolved session-intent weight + the shipped alpha (closure + tests). */
|
|
88
|
+
export declare function getSessionIntent(): {
|
|
89
|
+
weight: number;
|
|
90
|
+
alpha: number;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Blend the prompt embedding with the session-intent centroid for the vector
|
|
94
|
+
* search: `query = l2Normalize((1-w)·prompt + w·centroid)`. Returns the prompt
|
|
95
|
+
* UNCHANGED when `centroid` is undefined (first turn — no behavior change) or
|
|
96
|
+
* `weight` is 0 (the kill-switch — pure prompt). Extracted from the
|
|
97
|
+
* /recall-index closure (mcp-server.ts) so the exact blend decision is
|
|
98
|
+
* unit-testable directly, locking the ternary against a refactor without a
|
|
99
|
+
* closure-integration harness.
|
|
100
|
+
*/
|
|
101
|
+
export declare function blendQueryVector(promptEmb: Float32Array, centroid: Float32Array | undefined, weight: number): Float32Array;
|
|
102
|
+
/**
|
|
103
|
+
* Ids among `candidateIds` that have been superseded by a later memory — i.e.
|
|
104
|
+
* they are the SOURCE of a `superseded_by` link (stageSupersession links
|
|
105
|
+
* old → new). One query, not per-candidate.
|
|
106
|
+
*/
|
|
107
|
+
export declare function findSupersededIds(db: Database.Database, candidateIds: string[]): Set<string>;
|
|
52
108
|
/**
|
|
53
109
|
* Convert an L2 distance (as returned by sqlite-vec's vec0 `distance`) to
|
|
54
110
|
* cosine similarity. Valid because our embeddings are L2-normalized
|
|
@@ -72,7 +128,9 @@ export declare function effectiveStrength(baseStrength: number, lastAccessed: st
|
|
|
72
128
|
* Return a composite relevance score in [0, 1] for a candidate memory.
|
|
73
129
|
* Exported for exact-value tests of the similarity component (#145).
|
|
74
130
|
*/
|
|
75
|
-
export declare function computeScore(memory: Memory, distance: number, connectionCount: number, maxConnections: number, now: Date
|
|
131
|
+
export declare function computeScore(memory: Memory, distance: number, connectionCount: number, maxConnections: number, now: Date, options?: {
|
|
132
|
+
superseded?: boolean;
|
|
133
|
+
}): number;
|
|
76
134
|
export interface EmbedFn {
|
|
77
135
|
(text: string): Promise<Float32Array>;
|
|
78
136
|
}
|
|
@@ -88,6 +146,13 @@ export declare function retrieve(db: Database.Database, embedFn: EmbedFn, query:
|
|
|
88
146
|
/** #192: skip access strengthening — for pushed recall (/recall-index),
|
|
89
147
|
* where appearing in results must not count as use. */
|
|
90
148
|
noStrengthen?: boolean;
|
|
149
|
+
/** #192 session-intent keying (0.15.3): a pre-computed query embedding
|
|
150
|
+
* (e.g. the session-centroid blend from the /recall-index closure). When
|
|
151
|
+
* provided, the internal embed() call is SKIPPED — the caller owns the
|
|
152
|
+
* one embed per recall. /search and other unblended callers omit this
|
|
153
|
+
* and get pure-prompt behavior (the query string is embedded here). The
|
|
154
|
+
* FTS path still uses the raw `query` text regardless. */
|
|
155
|
+
queryEmbedding?: Float32Array;
|
|
91
156
|
}): Promise<MemorySearchResult[]>;
|
|
92
157
|
/**
|
|
93
158
|
* Get recent context, optionally filtered by project and privacy.
|