nlm-memory 0.5.20 → 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.
Files changed (75) hide show
  1. package/README.md +34 -14
  2. package/dist/cli/nlm.js +46 -34
  3. package/dist/cli/nlm.js.map +1 -1
  4. package/dist/cli/restart-helpers.d.ts +21 -0
  5. package/dist/cli/restart-helpers.js +38 -0
  6. package/dist/cli/restart-helpers.js.map +1 -1
  7. package/dist/cli/upgrade-helpers.d.ts +14 -0
  8. package/dist/cli/upgrade-helpers.js +22 -0
  9. package/dist/cli/upgrade-helpers.js.map +1 -0
  10. package/dist/core/actions/overlay.d.ts +22 -0
  11. package/dist/core/actions/overlay.js +56 -0
  12. package/dist/core/actions/overlay.js.map +1 -1
  13. package/dist/core/dataset/build-dataset.d.ts +14 -1
  14. package/dist/core/dataset/build-dataset.js +68 -11
  15. package/dist/core/dataset/build-dataset.js.map +1 -1
  16. package/dist/core/hook/miss-detect.d.ts +32 -0
  17. package/dist/core/hook/miss-detect.js +67 -0
  18. package/dist/core/hook/miss-detect.js.map +1 -0
  19. package/dist/core/recall/miss-log.d.ts +47 -0
  20. package/dist/core/recall/miss-log.js +112 -0
  21. package/dist/core/recall/miss-log.js.map +1 -0
  22. package/dist/core/recall/query-log.d.ts +3 -0
  23. package/dist/core/recall/query-log.js +1 -0
  24. package/dist/core/recall/query-log.js.map +1 -1
  25. package/dist/core/recall/recency.d.ts +52 -0
  26. package/dist/core/recall/recency.js +98 -0
  27. package/dist/core/recall/recency.js.map +1 -0
  28. package/dist/core/recall/recent-log.d.ts +1 -0
  29. package/dist/core/recall/recent-log.js +1 -0
  30. package/dist/core/recall/recent-log.js.map +1 -1
  31. package/dist/core/recall/related-facts.d.ts +30 -0
  32. package/dist/core/recall/related-facts.js +134 -0
  33. package/dist/core/recall/related-facts.js.map +1 -0
  34. package/dist/core/recall/rewrite-cache.d.ts +25 -0
  35. package/dist/core/recall/rewrite-cache.js +60 -0
  36. package/dist/core/recall/rewrite-cache.js.map +1 -0
  37. package/dist/core/recall/rewrite-prompt.d.ts +23 -0
  38. package/dist/core/recall/rewrite-prompt.js +81 -0
  39. package/dist/core/recall/rewrite-prompt.js.map +1 -0
  40. package/dist/core/storage/pg-session-store.js +16 -1
  41. package/dist/core/storage/pg-session-store.js.map +1 -1
  42. package/dist/core/storage/sqlite-session-store.d.ts +3 -0
  43. package/dist/core/storage/sqlite-session-store.js +21 -1
  44. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  45. package/dist/hook/pi-extension.d.ts +3 -1
  46. package/dist/hook/pi-extension.js +4 -2
  47. package/dist/hook/pi-extension.js.map +1 -1
  48. package/dist/hook/prompt-recall-hook.js +1 -1
  49. package/dist/hook/prompt-recall-hook.js.map +1 -1
  50. package/dist/hook/recall-over-http.d.ts +1 -1
  51. package/dist/hook/recall-over-http.js +5 -2
  52. package/dist/hook/recall-over-http.js.map +1 -1
  53. package/dist/hook/session-start-hook.js +4 -1
  54. package/dist/hook/session-start-hook.js.map +1 -1
  55. package/dist/http/app.js +2 -0
  56. package/dist/http/app.js.map +1 -1
  57. package/dist/install/rules-content.d.ts +51 -0
  58. package/dist/install/rules-content.js +140 -0
  59. package/dist/install/rules-content.js.map +1 -0
  60. package/dist/install/rules-install.d.ts +35 -0
  61. package/dist/install/rules-install.js +79 -0
  62. package/dist/install/rules-install.js.map +1 -0
  63. package/dist/mcp/server.js +1 -0
  64. package/dist/mcp/server.js.map +1 -1
  65. package/dist/ui/assets/index-BR1BHwew.js +69 -0
  66. package/dist/ui/assets/index-CgHq6xeZ.css +1 -0
  67. package/dist/ui/index.html +2 -2
  68. package/nlm/index.js +6 -4
  69. package/package.json +3 -1
  70. package/plugin/scripts/prompt-recall-hook.mjs +5 -3
  71. package/plugin-hermes-agent/README.md +49 -0
  72. package/plugin-hermes-agent/__init__.py +75 -0
  73. package/plugin-hermes-agent/plugin.yaml +15 -0
  74. package/dist/ui/assets/index-B7j5U42U.css +0 -1
  75. package/dist/ui/assets/index-CmY9CBuo.js +0 -69
@@ -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;
@@ -20,6 +20,7 @@ export async function logQuery(entry, logPath = defaultLogPath()) {
20
20
  const payload = {
21
21
  ts: new Date().toISOString(),
22
22
  source: entry.source,
23
+ runtime: entry.runtime,
23
24
  query: entry.query,
24
25
  entity: entry.entity,
25
26
  kind: entry.kind,
@@ -1 +1 @@
1
- {"version":3,"file":"query-log.js","sourceRoot":"","sources":["../../../src/core/recall/query-log.ts"],"names":[],"mappings":"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"}
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;AA2BlC,SAAS,cAAc;IACrB,OAAO,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,iBAAiB,CAAC,CAAC;AACpF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,KAAe,EACf,UAAkB,cAAc,EAAE;IAElC,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG;YACd,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,SAAS,EAAE,KAAK,CAAC,QAAQ;YACzB,YAAY,EAAE,KAAK,CAAC,WAAW;SAChC,CAAC;QACF,MAAM,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,4CAA4C;IAC9C,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAY,EACZ,UAAkB,cAAc,EAAE;IAElC,MAAM,IAAI,GAAgB;QACxB,IAAI;QACJ,KAAK,EAAE,CAAC;QACR,YAAY,EAAE,CAAC;QACf,QAAQ,EAAE,CAAC;QACX,SAAS,EAAE,EAAE;QACb,WAAW,EAAE,EAAE;QACf,WAAW,EAAE,KAAK;KACnB,CAAC;IAEF,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IACvD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC9C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,WAAW,GAAG,CAAC,CAAC;IAEpB,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,GAAG,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IACxC,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO;YAAE,SAAS;QACvB,IAAI,KAA8B,CAAC;QACnC,IAAI,CAAC;YACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAA4B,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,SAAS;QACxC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC7B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,MAAM;YAAE,SAAS;QAElD,KAAK,IAAI,CAAC,CAAC;QACX,MAAM,CAAC,GAAG,OAAO,KAAK,CAAC,WAAW,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,IAAI,CAAC,GAAG,CAAC;YAAE,WAAW,IAAI,CAAC,CAAC;QAE5B,MAAM,MAAM,GAAG,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACjF,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAEtD,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;QACzB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;YACpC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,CAAC,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1E,MAAM,aAAa,GAAG,CAAC,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;SAC7C,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;SAC3B,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAEf,OAAO;QACL,IAAI;QACJ,KAAK;QACL,YAAY,EAAE,WAAW;QACzB,QAAQ,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI;QAC3E,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,aAAa,CAAC;QAC5C,WAAW,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,WAAW,EAAE,IAAI;KAClB,CAAC;AACJ,CAAC"}
@@ -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"}
@@ -5,6 +5,7 @@
5
5
  export interface RecentLogEntry {
6
6
  readonly ts: string;
7
7
  readonly source: string;
8
+ readonly runtime: string | null;
8
9
  readonly query: string | null;
9
10
  readonly entity: string | null;
10
11
  readonly kind: string | null;
@@ -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":"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"}
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rewrite-cache.js","sourceRoot":"","sources":["../../../src/core/recall/rewrite-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAChC,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,YAAY;AAOlD,MAAM,OAAO,YAAY;IACN,KAAK,GAAG,IAAI,GAAG,EAAiB,CAAC;IACjC,UAAU,CAAS;IACnB,KAAK,CAAS;IAE/B,YAAY,OAAkE,EAAE;QAC9E,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,mBAAmB,CAAC;QACzD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,cAAc,CAAC;IAC5C,CAAC;IAED,GAAG,CAAC,QAAgB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;QAC5C,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;YAC3B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,2DAA2D;QAC3D,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC3B,OAAO,KAAK,CAAC,MAAM,CAAC;IACtB,CAAC;IAED,GAAG,CAAC,QAAgB,EAAE,MAAqB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;QACnE,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;QAChC,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QAC7D,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YACzC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YACjD,IAAI,SAAS,KAAK,SAAS;gBAAE,MAAM;YACnC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACF;AAED,SAAS,SAAS,CAAC,CAAS;IAC1B,0EAA0E;IAC1E,uCAAuC;IACvC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACrD,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Prompt + parser for query rewriting (Spec C).
3
+ *
4
+ * Vague natural-language queries ("that pgvector thing", "where did we leave
5
+ * the auth migration") get rewritten into two phrasings: a keyword-optimized
6
+ * one (entity-rich, conversational filler stripped) for FTS5 BM25, and a
7
+ * semantic-optimized one (cleaner prose, paraphrase preserved) for embeddings.
8
+ *
9
+ * Already-specific queries are preserved — the prompt instructs the model to
10
+ * pass them through. Recall is upstream of all scoring, so a wrong rewrite
11
+ * silently corrupts every downstream result; the bar for "do something" is
12
+ * intentionally high.
13
+ *
14
+ * Output schema is strict JSON; parse failures throw so the caller fails
15
+ * open back to the raw query rather than embedding garbage.
16
+ */
17
+ import { type RewriteResult } from "../../ports/llm-client.js";
18
+ export declare const REWRITE_SYSTEM_PROMPT = "You rewrite vague recall queries for a session-search engine.\n\nGiven a user query, return strict JSON with three fields:\n- \"keywordQuery\": entity-rich phrasing for keyword search. Strip conversational\n filler (\"what was that\", \"remember when\", \"did we\"). Preserve named entities,\n technical tokens (camelCase, snake_case, dotted names), and any specific\n identifiers. If the query is already specific (e.g. \"pgvector migration plan\"),\n return it unchanged.\n- \"semanticQuery\": natural-prose phrasing for semantic embedding. Smooth out\n vague references into a clearer paraphrase if you can, but never invent\n entities or facts not present in the input. If the query is already a clean\n prose phrasing, return it unchanged.\n- \"rationale\": one short sentence explaining what you changed (for debugging).\n\nExamples:\n\nInput: \"what was that pgvector thing we decided on\"\nOutput: {\"keywordQuery\":\"pgvector\",\"semanticQuery\":\"pgvector decision\",\"rationale\":\"stripped conversational filler\"}\n\nInput: \"pgvector migration plan\"\nOutput: {\"keywordQuery\":\"pgvector migration plan\",\"semanticQuery\":\"pgvector migration plan\",\"rationale\":\"already specific, passed through\"}\n\nInput: \"where did we leave the auth migration\"\nOutput: {\"keywordQuery\":\"auth migration\",\"semanticQuery\":\"current state of the auth migration\",\"rationale\":\"extracted topic and reshaped as status query\"}\n\nInput: \"Hermes\"\nOutput: {\"keywordQuery\":\"Hermes\",\"semanticQuery\":\"Hermes\",\"rationale\":\"single entity, passed through\"}\n\nRules:\n- NEVER invent entities, products, or technical names not in the input.\n- NEVER expand acronyms unless you are confident.\n- If the input is empty or makes no sense as a recall query, return it as-is in both fields.\n- Output strict JSON only, no markdown fence, no commentary.\n";
19
+ /**
20
+ * Parse strict JSON rewrite output. Throws LLMUnreachableError on any parse
21
+ * or shape error so the caller falls back to raw query.
22
+ */
23
+ export declare function parseRewriteJson(raw: string, providerLabel: string): RewriteResult;