@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/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 only fetch what you actually need. When the memory shapes your answer, cite it as given in the response.",
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");
@@ -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 only fetch what you actually need. When the memory shapes your answer, cite it to the user (id + date + origin agent).", {
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
- retrieveFn: (query, limit, filters) => retrieval.retrieve(db, embedder_js_1.embed, query, {
668
- limit,
669
- noStrengthen: true,
670
- project: filters?.project,
671
- privacy: filters?.privacy,
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)` only when the entry is relevant to your current task.",
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,
@@ -52,7 +52,10 @@ export interface RecallFilters {
52
52
  export interface RecallIndexDeps {
53
53
  db: Database.Database;
54
54
  registry: SessionRecallRegistry;
55
- retrieveFn: (query: string, limit: number, filters?: RecallFilters) => Promise<MemorySearchResult[]>;
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
@@ -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)` only when relevant; cite any memory you rely on (id, date):",
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;
@@ -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;
@@ -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.4 + effective_strength * 0.3 + connection_score * 0.2 + recency * 0.1
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): number;
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.