nlm-memory 0.5.21 → 0.5.22
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 +26 -8
- package/dist/core/actions/overlay.d.ts +22 -0
- package/dist/core/actions/overlay.js +56 -0
- package/dist/core/actions/overlay.js.map +1 -1
- package/dist/core/dataset/build-dataset.d.ts +14 -1
- package/dist/core/dataset/build-dataset.js +68 -11
- package/dist/core/dataset/build-dataset.js.map +1 -1
- package/dist/core/hook/miss-detect.d.ts +32 -0
- package/dist/core/hook/miss-detect.js +67 -0
- package/dist/core/hook/miss-detect.js.map +1 -0
- package/dist/core/recall/miss-log.d.ts +47 -0
- package/dist/core/recall/miss-log.js +112 -0
- package/dist/core/recall/miss-log.js.map +1 -0
- package/dist/core/recall/query-log.d.ts +3 -0
- package/dist/core/recall/query-log.js +1 -0
- package/dist/core/recall/query-log.js.map +1 -1
- package/dist/core/recall/recency.d.ts +52 -0
- package/dist/core/recall/recency.js +98 -0
- package/dist/core/recall/recency.js.map +1 -0
- package/dist/core/recall/recent-log.d.ts +1 -0
- package/dist/core/recall/recent-log.js +1 -0
- package/dist/core/recall/recent-log.js.map +1 -1
- package/dist/core/recall/related-facts.d.ts +30 -0
- package/dist/core/recall/related-facts.js +134 -0
- package/dist/core/recall/related-facts.js.map +1 -0
- package/dist/core/recall/rewrite-cache.d.ts +25 -0
- package/dist/core/recall/rewrite-cache.js +60 -0
- package/dist/core/recall/rewrite-cache.js.map +1 -0
- package/dist/core/recall/rewrite-prompt.d.ts +23 -0
- package/dist/core/recall/rewrite-prompt.js +81 -0
- package/dist/core/recall/rewrite-prompt.js.map +1 -0
- package/dist/core/storage/pg-session-store.js +16 -1
- package/dist/core/storage/pg-session-store.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +3 -0
- package/dist/core/storage/sqlite-session-store.js +21 -1
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/hook/pi-extension.d.ts +3 -1
- package/dist/hook/pi-extension.js +4 -2
- package/dist/hook/pi-extension.js.map +1 -1
- package/dist/hook/prompt-recall-hook.js +1 -1
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/hook/recall-over-http.d.ts +1 -1
- package/dist/hook/recall-over-http.js +5 -2
- package/dist/hook/recall-over-http.js.map +1 -1
- package/dist/hook/session-start-hook.js +4 -1
- package/dist/hook/session-start-hook.js.map +1 -1
- package/dist/http/app.js +2 -0
- package/dist/http/app.js.map +1 -1
- package/dist/install/rules-content.d.ts +51 -0
- package/dist/install/rules-content.js +140 -0
- package/dist/install/rules-content.js.map +1 -0
- package/dist/install/rules-install.d.ts +35 -0
- package/dist/install/rules-install.js +79 -0
- package/dist/install/rules-install.js.map +1 -0
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/ui/assets/index-BR1BHwew.js +69 -0
- package/dist/ui/assets/index-CgHq6xeZ.css +1 -0
- package/dist/ui/index.html +2 -2
- package/nlm/index.js +6 -4
- package/package.json +2 -1
- package/plugin/scripts/prompt-recall-hook.mjs +5 -3
- package/plugin-hermes-agent/README.md +49 -0
- package/plugin-hermes-agent/__init__.py +75 -0
- package/plugin-hermes-agent/plugin.yaml +15 -0
- package/dist/ui/assets/index-B7j5U42U.css +0 -1
- package/dist/ui/assets/index-BO0dvNHn.js +0 -69
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only JSONL miss log. One line per session the agent explicitly
|
|
3
|
+
* fetched or cited but which the hook's pre-prompt recall never surfaced
|
|
4
|
+
* in this conversation. Source: Stop hook's miss-detection pass (spec E).
|
|
5
|
+
*
|
|
6
|
+
* Default path ~/.nlm/miss-log.jsonl, overridable via NLM_MISS_LOG.
|
|
7
|
+
* Disable emission entirely with NLM_MISS_LOG_ENABLED=0 — used by
|
|
8
|
+
* deployments that want recall but not telemetry. Telemetry path — never
|
|
9
|
+
* raises; failure to write is silently swallowed.
|
|
10
|
+
*/
|
|
11
|
+
export type MissKind = "get_session" | "cite_session";
|
|
12
|
+
export interface MissEntry {
|
|
13
|
+
readonly conversationId: string;
|
|
14
|
+
readonly missedId: string;
|
|
15
|
+
readonly kind: MissKind;
|
|
16
|
+
/**
|
|
17
|
+
* Count of how many sessions WERE surfaced this conversation. Helps
|
|
18
|
+
* distinguish "hook never fired" (surfacedCount=0) from "hook fired but
|
|
19
|
+
* missed this one" (surfacedCount>0). Aggregation can filter accordingly.
|
|
20
|
+
*/
|
|
21
|
+
readonly surfacedCount: number;
|
|
22
|
+
}
|
|
23
|
+
export interface MissStats {
|
|
24
|
+
readonly days: number;
|
|
25
|
+
readonly total: number;
|
|
26
|
+
readonly distinctIds: number;
|
|
27
|
+
/** Top missed sessions ordered by miss count, with diagnostic context. */
|
|
28
|
+
readonly topIds: ReadonlyArray<{
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly count: number;
|
|
31
|
+
readonly conversations: number;
|
|
32
|
+
}>;
|
|
33
|
+
readonly logPresent: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function appendMiss(entry: MissEntry, logPath?: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Append multiple misses in one call. Each gets the same timestamp.
|
|
38
|
+
* Quieter than appendMiss × N when several misses fire in a single Stop
|
|
39
|
+
* hook invocation (which is the common case).
|
|
40
|
+
*/
|
|
41
|
+
export declare function appendMisses(entries: ReadonlyArray<MissEntry>, logPath?: string): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Read the miss log and aggregate by missedId. Used by the `nlm misses`
|
|
44
|
+
* CLI. Days filter applied at parse time. Bad lines (non-JSON or missing
|
|
45
|
+
* fields) are skipped silently.
|
|
46
|
+
*/
|
|
47
|
+
export declare function missStats(days: number, logPath?: string): Promise<MissStats>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only JSONL miss log. One line per session the agent explicitly
|
|
3
|
+
* fetched or cited but which the hook's pre-prompt recall never surfaced
|
|
4
|
+
* in this conversation. Source: Stop hook's miss-detection pass (spec E).
|
|
5
|
+
*
|
|
6
|
+
* Default path ~/.nlm/miss-log.jsonl, overridable via NLM_MISS_LOG.
|
|
7
|
+
* Disable emission entirely with NLM_MISS_LOG_ENABLED=0 — used by
|
|
8
|
+
* deployments that want recall but not telemetry. Telemetry path — never
|
|
9
|
+
* raises; failure to write is silently swallowed.
|
|
10
|
+
*/
|
|
11
|
+
import { appendFile, mkdir, readFile, stat } from "node:fs/promises";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
function defaultLogPath() {
|
|
15
|
+
return process.env["NLM_MISS_LOG"] ?? join(homedir(), ".nlm", "miss-log.jsonl");
|
|
16
|
+
}
|
|
17
|
+
function isEnabled() {
|
|
18
|
+
const raw = process.env["NLM_MISS_LOG_ENABLED"];
|
|
19
|
+
if (raw === undefined)
|
|
20
|
+
return true;
|
|
21
|
+
return raw !== "0" && raw.toLowerCase() !== "false";
|
|
22
|
+
}
|
|
23
|
+
export async function appendMiss(entry, logPath) {
|
|
24
|
+
if (!isEnabled())
|
|
25
|
+
return;
|
|
26
|
+
const path = logPath ?? defaultLogPath();
|
|
27
|
+
try {
|
|
28
|
+
await mkdir(dirname(path), { recursive: true });
|
|
29
|
+
const line = `${JSON.stringify({
|
|
30
|
+
ts: new Date().toISOString(),
|
|
31
|
+
...entry,
|
|
32
|
+
})}\n`;
|
|
33
|
+
await appendFile(path, line, "utf8");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Telemetry must never break recall.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Append multiple misses in one call. Each gets the same timestamp.
|
|
41
|
+
* Quieter than appendMiss × N when several misses fire in a single Stop
|
|
42
|
+
* hook invocation (which is the common case).
|
|
43
|
+
*/
|
|
44
|
+
export async function appendMisses(entries, logPath) {
|
|
45
|
+
if (!isEnabled())
|
|
46
|
+
return;
|
|
47
|
+
if (entries.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
const path = logPath ?? defaultLogPath();
|
|
50
|
+
try {
|
|
51
|
+
await mkdir(dirname(path), { recursive: true });
|
|
52
|
+
const ts = new Date().toISOString();
|
|
53
|
+
const block = entries.map((e) => `${JSON.stringify({ ts, ...e })}\n`).join("");
|
|
54
|
+
await appendFile(path, block, "utf8");
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Swallow.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read the miss log and aggregate by missedId. Used by the `nlm misses`
|
|
62
|
+
* CLI. Days filter applied at parse time. Bad lines (non-JSON or missing
|
|
63
|
+
* fields) are skipped silently.
|
|
64
|
+
*/
|
|
65
|
+
export async function missStats(days, logPath) {
|
|
66
|
+
const path = logPath ?? defaultLogPath();
|
|
67
|
+
try {
|
|
68
|
+
await stat(path);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { days, total: 0, distinctIds: 0, topIds: [], logPresent: false };
|
|
72
|
+
}
|
|
73
|
+
const raw = await readFile(path, "utf8");
|
|
74
|
+
const cutoff = Date.now() - days * 86_400_000;
|
|
75
|
+
const counts = new Map();
|
|
76
|
+
let total = 0;
|
|
77
|
+
for (const line of raw.split("\n")) {
|
|
78
|
+
if (!line)
|
|
79
|
+
continue;
|
|
80
|
+
let parsed;
|
|
81
|
+
try {
|
|
82
|
+
parsed = JSON.parse(line);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!parsed.ts || !parsed.missedId || !parsed.conversationId)
|
|
88
|
+
continue;
|
|
89
|
+
if (Date.parse(parsed.ts) < cutoff)
|
|
90
|
+
continue;
|
|
91
|
+
total += 1;
|
|
92
|
+
let bucket = counts.get(parsed.missedId);
|
|
93
|
+
if (!bucket) {
|
|
94
|
+
bucket = { count: 0, convs: new Set() };
|
|
95
|
+
counts.set(parsed.missedId, bucket);
|
|
96
|
+
}
|
|
97
|
+
bucket.count += 1;
|
|
98
|
+
bucket.convs.add(parsed.conversationId);
|
|
99
|
+
}
|
|
100
|
+
const topIds = [...counts.entries()]
|
|
101
|
+
.map(([id, b]) => ({ id, count: b.count, conversations: b.convs.size }))
|
|
102
|
+
.sort((a, b) => b.count - a.count)
|
|
103
|
+
.slice(0, 20);
|
|
104
|
+
return {
|
|
105
|
+
days,
|
|
106
|
+
total,
|
|
107
|
+
distinctIds: counts.size,
|
|
108
|
+
topIds,
|
|
109
|
+
logPresent: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=miss-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"miss-log.js","sourceRoot":"","sources":["../../../src/core/recall/miss-log.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
import type { RecallKindFilter, RecallMode } from "../../shared/types.js";
|
|
12
12
|
export interface LogEntry {
|
|
13
13
|
readonly source: string;
|
|
14
|
+
/** Calling agent runtime (claude-code / hermes / pi.dev / codex / etc).
|
|
15
|
+
* Logged when the caller passes x-recall-runtime; null for legacy entries. */
|
|
16
|
+
readonly runtime: string | null;
|
|
14
17
|
readonly query: string | null;
|
|
15
18
|
readonly entity: string | null;
|
|
16
19
|
readonly kind: RecallKindFilter | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-log.js","sourceRoot":"","sources":["../../../src/core/recall/query-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"query-log.js","sourceRoot":"","sources":["../../../src/core/recall/query-log.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recency weighting for recall scoring.
|
|
3
|
+
*
|
|
4
|
+
* Multiplies each RecallHit's matchScore by an age-decay factor so identical-
|
|
5
|
+
* relevance sessions sort newer-first. The factor is exponential half-life:
|
|
6
|
+
*
|
|
7
|
+
* f(age_days) = max(FLOOR, 2 ^ (-age_days / half_life_days))
|
|
8
|
+
*
|
|
9
|
+
* Default half-life: 180 days (6 months). A 180-day-old session scores 0.5×,
|
|
10
|
+
* a 1-year-old session scores 0.25×, an 18-month-old session scores ~0.13×.
|
|
11
|
+
*
|
|
12
|
+
* Default floor: 0.25. A truly-relevant ancient session never drops below
|
|
13
|
+
* a quarter of its raw score — so age alone can't push it out of top results
|
|
14
|
+
* if the keyword/semantic signal is strong enough.
|
|
15
|
+
*
|
|
16
|
+
* Configuration:
|
|
17
|
+
* NLM_RECALL_DECAY_HALF_LIFE_DAYS — override the half-life. Set to 0 to
|
|
18
|
+
* disable decay entirely (returns 1.0).
|
|
19
|
+
* NLM_RECALL_DECAY_FLOOR — override the floor (0.0 - 1.0 range).
|
|
20
|
+
*
|
|
21
|
+
* Env vars are read once at module load and cached. To change at runtime,
|
|
22
|
+
* call resetRecencyConfigForTest() (test-only) and reimport.
|
|
23
|
+
*
|
|
24
|
+
* Design notes:
|
|
25
|
+
* - Applied once at finalize() time in recall-service.ts, after per-mode
|
|
26
|
+
* scoring but before the limit slice. Single call-site covers keyword,
|
|
27
|
+
* semantic, and hybrid modes uniformly.
|
|
28
|
+
* - Future startedAt (clock skew) is clamped to multiplier = 1.0.
|
|
29
|
+
* - Missing/invalid startedAt returns 1.0 (treat as fresh — don't penalize
|
|
30
|
+
* for data we don't have).
|
|
31
|
+
*/
|
|
32
|
+
interface RecencyConfig {
|
|
33
|
+
readonly halfLifeDays: number;
|
|
34
|
+
readonly floor: number;
|
|
35
|
+
readonly disabled: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Test-only: drop the cached config so the next call re-reads env vars.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resetRecencyConfigForTest(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Return the multiplier in (0, 1] for a session whose ISO 8601 `startedAt`
|
|
43
|
+
* timestamp is given. `now` defaults to Date.now() and may be overridden
|
|
44
|
+
* for deterministic tests.
|
|
45
|
+
*/
|
|
46
|
+
export declare function recencyMultiplier(startedAt: string | null | undefined, now?: number): number;
|
|
47
|
+
/**
|
|
48
|
+
* Read-only view of the active config. Exposed for diagnostics (e.g. the
|
|
49
|
+
* `/api/health` endpoint or operator-facing logs).
|
|
50
|
+
*/
|
|
51
|
+
export declare function getRecencyConfig(): RecencyConfig;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recency weighting for recall scoring.
|
|
3
|
+
*
|
|
4
|
+
* Multiplies each RecallHit's matchScore by an age-decay factor so identical-
|
|
5
|
+
* relevance sessions sort newer-first. The factor is exponential half-life:
|
|
6
|
+
*
|
|
7
|
+
* f(age_days) = max(FLOOR, 2 ^ (-age_days / half_life_days))
|
|
8
|
+
*
|
|
9
|
+
* Default half-life: 180 days (6 months). A 180-day-old session scores 0.5×,
|
|
10
|
+
* a 1-year-old session scores 0.25×, an 18-month-old session scores ~0.13×.
|
|
11
|
+
*
|
|
12
|
+
* Default floor: 0.25. A truly-relevant ancient session never drops below
|
|
13
|
+
* a quarter of its raw score — so age alone can't push it out of top results
|
|
14
|
+
* if the keyword/semantic signal is strong enough.
|
|
15
|
+
*
|
|
16
|
+
* Configuration:
|
|
17
|
+
* NLM_RECALL_DECAY_HALF_LIFE_DAYS — override the half-life. Set to 0 to
|
|
18
|
+
* disable decay entirely (returns 1.0).
|
|
19
|
+
* NLM_RECALL_DECAY_FLOOR — override the floor (0.0 - 1.0 range).
|
|
20
|
+
*
|
|
21
|
+
* Env vars are read once at module load and cached. To change at runtime,
|
|
22
|
+
* call resetRecencyConfigForTest() (test-only) and reimport.
|
|
23
|
+
*
|
|
24
|
+
* Design notes:
|
|
25
|
+
* - Applied once at finalize() time in recall-service.ts, after per-mode
|
|
26
|
+
* scoring but before the limit slice. Single call-site covers keyword,
|
|
27
|
+
* semantic, and hybrid modes uniformly.
|
|
28
|
+
* - Future startedAt (clock skew) is clamped to multiplier = 1.0.
|
|
29
|
+
* - Missing/invalid startedAt returns 1.0 (treat as fresh — don't penalize
|
|
30
|
+
* for data we don't have).
|
|
31
|
+
*/
|
|
32
|
+
const DEFAULT_HALF_LIFE_DAYS = 180;
|
|
33
|
+
const DEFAULT_FLOOR = 0.25;
|
|
34
|
+
const MS_PER_DAY = 86_400_000;
|
|
35
|
+
let cached = null;
|
|
36
|
+
function loadConfig() {
|
|
37
|
+
if (cached)
|
|
38
|
+
return cached;
|
|
39
|
+
const halfLifeRaw = process.env["NLM_RECALL_DECAY_HALF_LIFE_DAYS"];
|
|
40
|
+
const floorRaw = process.env["NLM_RECALL_DECAY_FLOOR"];
|
|
41
|
+
let halfLifeDays = DEFAULT_HALF_LIFE_DAYS;
|
|
42
|
+
let disabled = false;
|
|
43
|
+
if (halfLifeRaw !== undefined) {
|
|
44
|
+
const parsed = Number.parseFloat(halfLifeRaw);
|
|
45
|
+
if (Number.isFinite(parsed)) {
|
|
46
|
+
if (parsed <= 0) {
|
|
47
|
+
disabled = true;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
halfLifeDays = parsed;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let floor = DEFAULT_FLOOR;
|
|
55
|
+
if (floorRaw !== undefined) {
|
|
56
|
+
const parsed = Number.parseFloat(floorRaw);
|
|
57
|
+
if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 1) {
|
|
58
|
+
floor = parsed;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
cached = { halfLifeDays, floor, disabled };
|
|
62
|
+
return cached;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Test-only: drop the cached config so the next call re-reads env vars.
|
|
66
|
+
*/
|
|
67
|
+
export function resetRecencyConfigForTest() {
|
|
68
|
+
cached = null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Return the multiplier in (0, 1] for a session whose ISO 8601 `startedAt`
|
|
72
|
+
* timestamp is given. `now` defaults to Date.now() and may be overridden
|
|
73
|
+
* for deterministic tests.
|
|
74
|
+
*/
|
|
75
|
+
export function recencyMultiplier(startedAt, now = Date.now()) {
|
|
76
|
+
const cfg = loadConfig();
|
|
77
|
+
if (cfg.disabled)
|
|
78
|
+
return 1.0;
|
|
79
|
+
if (!startedAt)
|
|
80
|
+
return 1.0;
|
|
81
|
+
const started = Date.parse(startedAt);
|
|
82
|
+
if (Number.isNaN(started))
|
|
83
|
+
return 1.0;
|
|
84
|
+
const ageMs = now - started;
|
|
85
|
+
if (ageMs <= 0)
|
|
86
|
+
return 1.0;
|
|
87
|
+
const ageDays = ageMs / MS_PER_DAY;
|
|
88
|
+
const raw = Math.pow(2, -ageDays / cfg.halfLifeDays);
|
|
89
|
+
return Math.max(cfg.floor, raw);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read-only view of the active config. Exposed for diagnostics (e.g. the
|
|
93
|
+
* `/api/health` endpoint or operator-facing logs).
|
|
94
|
+
*/
|
|
95
|
+
export function getRecencyConfig() {
|
|
96
|
+
return loadConfig();
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=recency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recency.js","sourceRoot":"","sources":["../../../src/core/recall/recency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,sBAAsB,GAAG,GAAG,CAAC;AACnC,MAAM,aAAa,GAAG,IAAI,CAAC;AAC3B,MAAM,UAAU,GAAG,UAAU,CAAC;AAQ9B,IAAI,MAAM,GAAyB,IAAI,CAAC;AAExC,SAAS,UAAU;IACjB,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;IACnE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;IAEvD,IAAI,YAAY,GAAG,sBAAsB,CAAC;IAC1C,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;QAC9C,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;gBAChB,QAAQ,GAAG,IAAI,CAAC;YAClB,CAAC;iBAAM,CAAC;gBACN,YAAY,GAAG,MAAM,CAAC;YACxB,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,KAAK,GAAG,aAAa,CAAC;IAC1B,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;YAC1D,KAAK,GAAG,MAAM,CAAC;QACjB,CAAC;IACH,CAAC;IAED,MAAM,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;IAC3C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,yBAAyB;IACvC,MAAM,GAAG,IAAI,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAoC,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IAC9F,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,IAAI,GAAG,CAAC,QAAQ;QAAE,OAAO,GAAG,CAAC;IAC7B,IAAI,CAAC,SAAS;QAAE,OAAO,GAAG,CAAC;IAE3B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACtC,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;QAAE,OAAO,GAAG,CAAC;IAEtC,MAAM,KAAK,GAAG,GAAG,GAAG,OAAO,CAAC;IAC5B,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,GAAG,CAAC;IAE3B,MAAM,OAAO,GAAG,KAAK,GAAG,UAAU,CAAC;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC;IACrD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,UAAU,EAAE,CAAC;AACtB,CAAC"}
|
|
@@ -25,6 +25,7 @@ export function recentQueryLog(limit, logPath = defaultLogPath()) {
|
|
|
25
25
|
entries.push({
|
|
26
26
|
ts: typeof raw["ts"] === "string" ? raw["ts"] : "",
|
|
27
27
|
source: typeof raw["source"] === "string" ? raw["source"] : "unknown",
|
|
28
|
+
runtime: typeof raw["runtime"] === "string" ? raw["runtime"] : null,
|
|
28
29
|
query: typeof raw["query"] === "string" ? raw["query"] : null,
|
|
29
30
|
entity: typeof raw["entity"] === "string" ? raw["entity"] : null,
|
|
30
31
|
kind: typeof raw["kind"] === "string" ? raw["kind"] : null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recent-log.js","sourceRoot":"","sources":["../../../src/core/recall/recent-log.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"recent-log.js","sourceRoot":"","sources":["../../../src/core/recall/recent-log.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pickRelatedFacts — Spec G.2 server-side helper that selects 3–5 current
|
|
3
|
+
* high-confidence facts about the entities in the top recall hits.
|
|
4
|
+
*
|
|
5
|
+
* Algorithm:
|
|
6
|
+
* 1. Walk top-N hits, collect a deduped entity set (cap entities so very
|
|
7
|
+
* entity-rich sessions don't dominate).
|
|
8
|
+
* 2. For each entity, fetch its current (non-superseded) facts from
|
|
9
|
+
* FactStore.list. Filter to confidence ≥ min.
|
|
10
|
+
* 3. Compute corroboration counts (Spec G.1) for all candidates in a
|
|
11
|
+
* single batched query.
|
|
12
|
+
* 4. Filter to facts seen across ≥ MIN_CORROBORATION sessions — a fact
|
|
13
|
+
* asserted by only one session isn't "known" yet for the purposes
|
|
14
|
+
* of hook injection.
|
|
15
|
+
* 5. Sort by (corroborationCount DESC, confidence DESC) and take limit.
|
|
16
|
+
*
|
|
17
|
+
* Defaults are tunable via env vars (NLM_HOOK_FACT_LIMIT,
|
|
18
|
+
* NLM_HOOK_FACT_MIN_CORROBORATION, NLM_HOOK_FACT_MIN_CONFIDENCE) and any
|
|
19
|
+
* fail in the path returns an empty array so the pointer block degrades
|
|
20
|
+
* to the session-only format. This is a quality enhancement; never let
|
|
21
|
+
* it block recall.
|
|
22
|
+
*/
|
|
23
|
+
import type { FactStore } from "../../ports/fact-store.js";
|
|
24
|
+
import type { RecallHit, RelatedFact } from "../../shared/types.js";
|
|
25
|
+
export interface PickRelatedFactsOptions {
|
|
26
|
+
readonly limit?: number;
|
|
27
|
+
readonly minCorroboration?: number;
|
|
28
|
+
readonly minConfidence?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function pickRelatedFacts(hits: ReadonlyArray<RecallHit>, factStore: FactStore, opts?: PickRelatedFactsOptions): Promise<RelatedFact[]>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pickRelatedFacts — Spec G.2 server-side helper that selects 3–5 current
|
|
3
|
+
* high-confidence facts about the entities in the top recall hits.
|
|
4
|
+
*
|
|
5
|
+
* Algorithm:
|
|
6
|
+
* 1. Walk top-N hits, collect a deduped entity set (cap entities so very
|
|
7
|
+
* entity-rich sessions don't dominate).
|
|
8
|
+
* 2. For each entity, fetch its current (non-superseded) facts from
|
|
9
|
+
* FactStore.list. Filter to confidence ≥ min.
|
|
10
|
+
* 3. Compute corroboration counts (Spec G.1) for all candidates in a
|
|
11
|
+
* single batched query.
|
|
12
|
+
* 4. Filter to facts seen across ≥ MIN_CORROBORATION sessions — a fact
|
|
13
|
+
* asserted by only one session isn't "known" yet for the purposes
|
|
14
|
+
* of hook injection.
|
|
15
|
+
* 5. Sort by (corroborationCount DESC, confidence DESC) and take limit.
|
|
16
|
+
*
|
|
17
|
+
* Defaults are tunable via env vars (NLM_HOOK_FACT_LIMIT,
|
|
18
|
+
* NLM_HOOK_FACT_MIN_CORROBORATION, NLM_HOOK_FACT_MIN_CONFIDENCE) and any
|
|
19
|
+
* fail in the path returns an empty array so the pointer block degrades
|
|
20
|
+
* to the session-only format. This is a quality enhancement; never let
|
|
21
|
+
* it block recall.
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_FACT_LIMIT = 5;
|
|
24
|
+
const DEFAULT_MIN_CORROBORATION = 2;
|
|
25
|
+
const DEFAULT_MIN_CONFIDENCE = 0.7;
|
|
26
|
+
const TOP_HITS_FOR_ENTITY_SCAN = 5;
|
|
27
|
+
const MAX_ENTITIES_TO_QUERY = 8;
|
|
28
|
+
function readInt(envKey, fallback) {
|
|
29
|
+
const raw = process.env[envKey];
|
|
30
|
+
if (raw === undefined)
|
|
31
|
+
return fallback;
|
|
32
|
+
const parsed = Number.parseInt(raw, 10);
|
|
33
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
34
|
+
}
|
|
35
|
+
function readFloat(envKey, fallback) {
|
|
36
|
+
const raw = process.env[envKey];
|
|
37
|
+
if (raw === undefined)
|
|
38
|
+
return fallback;
|
|
39
|
+
const parsed = Number.parseFloat(raw);
|
|
40
|
+
return Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : fallback;
|
|
41
|
+
}
|
|
42
|
+
export async function pickRelatedFacts(hits, factStore, opts = {}) {
|
|
43
|
+
if (hits.length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
const limit = opts.limit ?? readInt("NLM_HOOK_FACT_LIMIT", DEFAULT_FACT_LIMIT);
|
|
46
|
+
const minCorroboration = opts.minCorroboration ?? readInt("NLM_HOOK_FACT_MIN_CORROBORATION", DEFAULT_MIN_CORROBORATION);
|
|
47
|
+
const minConfidence = opts.minConfidence ?? readFloat("NLM_HOOK_FACT_MIN_CONFIDENCE", DEFAULT_MIN_CONFIDENCE);
|
|
48
|
+
if (limit <= 0)
|
|
49
|
+
return [];
|
|
50
|
+
try {
|
|
51
|
+
// 1. Collect entities from the top hits.
|
|
52
|
+
const entities = collectEntities(hits.slice(0, TOP_HITS_FOR_ENTITY_SCAN));
|
|
53
|
+
if (entities.length === 0)
|
|
54
|
+
return [];
|
|
55
|
+
// 2. Fetch current facts per entity. Parallelize to keep latency bounded.
|
|
56
|
+
const factLists = await Promise.all(entities.map((entity) => factStore.list({ subject: entity, includeSuperseded: false })));
|
|
57
|
+
// 3. Flatten, filter by confidence.
|
|
58
|
+
const candidates = [];
|
|
59
|
+
for (const list of factLists) {
|
|
60
|
+
for (const f of list) {
|
|
61
|
+
if (f.confidence >= minConfidence)
|
|
62
|
+
candidates.push(f);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (candidates.length === 0)
|
|
66
|
+
return [];
|
|
67
|
+
// 4. Batch-compute corroboration for all candidates.
|
|
68
|
+
const triples = candidates.map((f) => ({
|
|
69
|
+
subject: f.subject,
|
|
70
|
+
predicate: f.predicate,
|
|
71
|
+
value: f.value,
|
|
72
|
+
}));
|
|
73
|
+
const counts = await factStore.corroborationCounts(triples);
|
|
74
|
+
// 5. Build RelatedFact[], filter by minCorroboration, dedupe by
|
|
75
|
+
// (subject, predicate) — only the most-corroborated value per pair
|
|
76
|
+
// survives so we don't list "polysignal uses: duckdb" AND
|
|
77
|
+
// "polysignal uses: postgres" as if both were current.
|
|
78
|
+
const byKey = new Map();
|
|
79
|
+
for (const f of candidates) {
|
|
80
|
+
const corrKey = `${f.subject} ${f.predicate} ${f.value}`;
|
|
81
|
+
const corroborationCount = counts.get(corrKey) ?? 1;
|
|
82
|
+
if (corroborationCount < minCorroboration)
|
|
83
|
+
continue;
|
|
84
|
+
const dedupeKey = `${f.subject} ${f.predicate}`;
|
|
85
|
+
const existing = byKey.get(dedupeKey);
|
|
86
|
+
if (!existing ||
|
|
87
|
+
corroborationCount > existing.corroborationCount ||
|
|
88
|
+
(corroborationCount === existing.corroborationCount && f.confidence > existing.confidence)) {
|
|
89
|
+
byKey.set(dedupeKey, {
|
|
90
|
+
subject: f.subject,
|
|
91
|
+
predicate: f.predicate,
|
|
92
|
+
value: f.value,
|
|
93
|
+
corroborationCount,
|
|
94
|
+
confidence: f.confidence,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const sorted = [...byKey.values()].sort((a, b) => {
|
|
99
|
+
if (b.corroborationCount !== a.corroborationCount) {
|
|
100
|
+
return b.corroborationCount - a.corroborationCount;
|
|
101
|
+
}
|
|
102
|
+
return b.confidence - a.confidence;
|
|
103
|
+
});
|
|
104
|
+
return sorted.slice(0, limit).map((r) => ({
|
|
105
|
+
subject: r.subject,
|
|
106
|
+
predicate: r.predicate,
|
|
107
|
+
value: r.value,
|
|
108
|
+
corroborationCount: r.corroborationCount,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Fail-open: empty array means the pointer block renders session-only.
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function collectEntities(hits) {
|
|
117
|
+
// Order matters: earlier hits contribute first so the top-ranked
|
|
118
|
+
// session's entities are most likely to make the cut.
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const hit of hits) {
|
|
122
|
+
for (const ent of hit.entities) {
|
|
123
|
+
const trimmed = ent.trim();
|
|
124
|
+
if (!trimmed || seen.has(trimmed))
|
|
125
|
+
continue;
|
|
126
|
+
seen.add(trimmed);
|
|
127
|
+
out.push(trimmed);
|
|
128
|
+
if (out.length >= MAX_ENTITIES_TO_QUERY)
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=related-facts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"related-facts.js","sourceRoot":"","sources":["../../../src/core/recall/related-facts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded in-memory TTL cache for query rewrites.
|
|
3
|
+
*
|
|
4
|
+
* Same raw query within the TTL window reuses the prior LLM rewrite —
|
|
5
|
+
* avoids burning ~hundreds-of-ms per repeated call when an agent hammers
|
|
6
|
+
* `recall_sessions` with slightly different phrasings of the same intent.
|
|
7
|
+
*
|
|
8
|
+
* Bounded by max entries (LRU eviction) AND TTL (lazy expiry on read).
|
|
9
|
+
* No background timer — entries are checked at read time and dropped if
|
|
10
|
+
* expired, which keeps the cache zero-cost when idle.
|
|
11
|
+
*/
|
|
12
|
+
import type { RewriteResult } from "../../ports/llm-client.js";
|
|
13
|
+
export declare class RewriteCache {
|
|
14
|
+
private readonly store;
|
|
15
|
+
private readonly maxEntries;
|
|
16
|
+
private readonly ttlMs;
|
|
17
|
+
constructor(opts?: {
|
|
18
|
+
readonly maxEntries?: number;
|
|
19
|
+
readonly ttlMs?: number;
|
|
20
|
+
});
|
|
21
|
+
get(rawQuery: string, now?: number): RewriteResult | null;
|
|
22
|
+
set(rawQuery: string, result: RewriteResult, now?: number): void;
|
|
23
|
+
size(): number;
|
|
24
|
+
clear(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded in-memory TTL cache for query rewrites.
|
|
3
|
+
*
|
|
4
|
+
* Same raw query within the TTL window reuses the prior LLM rewrite —
|
|
5
|
+
* avoids burning ~hundreds-of-ms per repeated call when an agent hammers
|
|
6
|
+
* `recall_sessions` with slightly different phrasings of the same intent.
|
|
7
|
+
*
|
|
8
|
+
* Bounded by max entries (LRU eviction) AND TTL (lazy expiry on read).
|
|
9
|
+
* No background timer — entries are checked at read time and dropped if
|
|
10
|
+
* expired, which keeps the cache zero-cost when idle.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_MAX_ENTRIES = 200;
|
|
13
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
14
|
+
export class RewriteCache {
|
|
15
|
+
store = new Map();
|
|
16
|
+
maxEntries;
|
|
17
|
+
ttlMs;
|
|
18
|
+
constructor(opts = {}) {
|
|
19
|
+
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
20
|
+
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
21
|
+
}
|
|
22
|
+
get(rawQuery, now = Date.now()) {
|
|
23
|
+
const key = normalize(rawQuery);
|
|
24
|
+
const entry = this.store.get(key);
|
|
25
|
+
if (!entry)
|
|
26
|
+
return null;
|
|
27
|
+
if (entry.expiresAt <= now) {
|
|
28
|
+
this.store.delete(key);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
// LRU touch: re-insert so this key is now the most recent.
|
|
32
|
+
this.store.delete(key);
|
|
33
|
+
this.store.set(key, entry);
|
|
34
|
+
return entry.result;
|
|
35
|
+
}
|
|
36
|
+
set(rawQuery, result, now = Date.now()) {
|
|
37
|
+
const key = normalize(rawQuery);
|
|
38
|
+
if (this.store.has(key))
|
|
39
|
+
this.store.delete(key);
|
|
40
|
+
this.store.set(key, { result, expiresAt: now + this.ttlMs });
|
|
41
|
+
while (this.store.size > this.maxEntries) {
|
|
42
|
+
const oldestKey = this.store.keys().next().value;
|
|
43
|
+
if (oldestKey === undefined)
|
|
44
|
+
break;
|
|
45
|
+
this.store.delete(oldestKey);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
size() {
|
|
49
|
+
return this.store.size;
|
|
50
|
+
}
|
|
51
|
+
clear() {
|
|
52
|
+
this.store.clear();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function normalize(s) {
|
|
56
|
+
// Case-insensitive, whitespace-collapsed key — "PGVector thing" hits the
|
|
57
|
+
// same cache slot as "pgvector thing".
|
|
58
|
+
return s.trim().toLowerCase().replace(/\s+/g, " ");
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=rewrite-cache.js.map
|