nlm-memory 0.5.22 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +106 -14
  2. package/dist/cli/classify-parity.js +1 -1
  3. package/dist/cli/classify-parity.js.map +1 -1
  4. package/dist/cli/nlm.js +152 -12
  5. package/dist/cli/nlm.js.map +1 -1
  6. package/dist/core/adapters/pi.js +13 -3
  7. package/dist/core/adapters/pi.js.map +1 -1
  8. package/dist/core/classifier/prompt.d.ts +1 -1
  9. package/dist/core/classifier/prompt.js +1 -1
  10. package/dist/core/hook/pointer-block.d.ts +12 -1
  11. package/dist/core/hook/pointer-block.js +25 -8
  12. package/dist/core/hook/pointer-block.js.map +1 -1
  13. package/dist/core/providers/provider-registry.js +1 -1
  14. package/dist/core/providers/provider-registry.js.map +1 -1
  15. package/dist/core/recall/recall-service.d.ts +9 -0
  16. package/dist/core/recall/recall-service.js +81 -18
  17. package/dist/core/recall/recall-service.js.map +1 -1
  18. package/dist/core/recall-facts/fact-recall-service.d.ts +11 -0
  19. package/dist/core/recall-facts/fact-recall-service.js +69 -4
  20. package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
  21. package/dist/core/scheduler/scheduler.d.ts +7 -0
  22. package/dist/core/scheduler/scheduler.js +26 -0
  23. package/dist/core/scheduler/scheduler.js.map +1 -1
  24. package/dist/core/signals/aggregate.d.ts +21 -0
  25. package/dist/core/signals/aggregate.js +34 -0
  26. package/dist/core/signals/aggregate.js.map +1 -0
  27. package/dist/core/signals/failure-mode-recall.d.ts +18 -0
  28. package/dist/core/signals/failure-mode-recall.js +28 -0
  29. package/dist/core/signals/failure-mode-recall.js.map +1 -0
  30. package/dist/core/signals/ingest-signal.d.ts +19 -0
  31. package/dist/core/signals/ingest-signal.js +63 -0
  32. package/dist/core/signals/ingest-signal.js.map +1 -0
  33. package/dist/core/signals/install-scope.d.ts +8 -0
  34. package/dist/core/signals/install-scope.js +41 -0
  35. package/dist/core/signals/install-scope.js.map +1 -0
  36. package/dist/core/signals/recommend.d.ts +13 -0
  37. package/dist/core/signals/recommend.js +24 -0
  38. package/dist/core/signals/recommend.js.map +1 -0
  39. package/dist/core/storage/pg-fact-store.d.ts +5 -0
  40. package/dist/core/storage/pg-fact-store.js +29 -0
  41. package/dist/core/storage/pg-fact-store.js.map +1 -1
  42. package/dist/core/storage/pg-signal-store.d.ts +16 -0
  43. package/dist/core/storage/pg-signal-store.js +100 -0
  44. package/dist/core/storage/pg-signal-store.js.map +1 -0
  45. package/dist/core/storage/pg-storage.d.ts +2 -0
  46. package/dist/core/storage/pg-storage.js +3 -0
  47. package/dist/core/storage/pg-storage.js.map +1 -1
  48. package/dist/core/storage/pg-tx-context.d.ts +5 -0
  49. package/dist/core/storage/pg-tx-context.js +3 -0
  50. package/dist/core/storage/pg-tx-context.js.map +1 -1
  51. package/dist/core/storage/sqlite-fact-store.d.ts +5 -0
  52. package/dist/core/storage/sqlite-fact-store.js +30 -0
  53. package/dist/core/storage/sqlite-fact-store.js.map +1 -1
  54. package/dist/core/storage/sqlite-signal-store.d.ts +20 -0
  55. package/dist/core/storage/sqlite-signal-store.js +112 -0
  56. package/dist/core/storage/sqlite-signal-store.js.map +1 -0
  57. package/dist/core/storage/sqlite-storage.d.ts +2 -0
  58. package/dist/core/storage/sqlite-storage.js +6 -2
  59. package/dist/core/storage/sqlite-storage.js.map +1 -1
  60. package/dist/hook/prompt-recall-hook.d.ts +10 -1
  61. package/dist/hook/prompt-recall-hook.js +10 -4
  62. package/dist/hook/prompt-recall-hook.js.map +1 -1
  63. package/dist/hook/recall-over-http.d.ts +11 -1
  64. package/dist/hook/recall-over-http.js +16 -4
  65. package/dist/hook/recall-over-http.js.map +1 -1
  66. package/dist/hook/session-start-hook.d.ts +2 -0
  67. package/dist/hook/session-start-hook.js +32 -2
  68. package/dist/hook/session-start-hook.js.map +1 -1
  69. package/dist/hook/stop-hook.js +16 -0
  70. package/dist/hook/stop-hook.js.map +1 -1
  71. package/dist/http/app.d.ts +9 -0
  72. package/dist/http/app.js +124 -11
  73. package/dist/http/app.js.map +1 -1
  74. package/dist/install/codex.d.ts +1 -1
  75. package/dist/install/codex.js +4 -4
  76. package/dist/install/codex.js.map +1 -1
  77. package/dist/install/ollama.d.ts +10 -0
  78. package/dist/install/ollama.js +19 -3
  79. package/dist/install/ollama.js.map +1 -1
  80. package/dist/install/pi.d.ts +1 -1
  81. package/dist/install/pi.js +1 -1
  82. package/dist/install/setup.js +63 -10
  83. package/dist/install/setup.js.map +1 -1
  84. package/dist/llm/classifier-box.d.ts +2 -1
  85. package/dist/llm/classifier-box.js +3 -0
  86. package/dist/llm/classifier-box.js.map +1 -1
  87. package/dist/llm/deepseek-client.d.ts +5 -2
  88. package/dist/llm/deepseek-client.js +51 -1
  89. package/dist/llm/deepseek-client.js.map +1 -1
  90. package/dist/llm/ollama-client.d.ts +3 -2
  91. package/dist/llm/ollama-client.js +45 -2
  92. package/dist/llm/ollama-client.js.map +1 -1
  93. package/dist/mcp/server.d.ts +1 -0
  94. package/dist/mcp/server.js +22 -3
  95. package/dist/mcp/server.js.map +1 -1
  96. package/dist/ports/fact-store.d.ts +0 -0
  97. package/dist/ports/fact-store.js +0 -0
  98. package/dist/ports/fact-store.js.map +1 -1
  99. package/dist/ports/llm-client.d.ts +24 -0
  100. package/dist/ports/signal-store.d.ts +30 -0
  101. package/dist/ports/signal-store.js +9 -0
  102. package/dist/ports/signal-store.js.map +1 -0
  103. package/dist/ports/storage.d.ts +2 -0
  104. package/dist/ports/transcript-adapter.d.ts +6 -0
  105. package/dist/shared/types.d.ts +76 -0
  106. package/dist/ui/assets/index-DLDd-OM_.js +69 -0
  107. package/dist/ui/index.html +1 -1
  108. package/nlm/README.md +1 -1
  109. package/nlm/index.js +40 -17
  110. package/package.json +6 -5
  111. package/plugin/.codex-plugin/plugin.json +2 -2
  112. package/plugin/README.md +3 -3
  113. package/plugin/scripts/prompt-recall-hook.mjs +40 -17
  114. package/plugin/scripts/stop-hook.mjs +80 -6
  115. package/dist/ui/assets/index-BR1BHwew.js +0 -69
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>nlm memory</title>
7
- <script type="module" crossorigin src="/ui/assets/index-BR1BHwew.js"></script>
7
+ <script type="module" crossorigin src="/ui/assets/index-DLDd-OM_.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/ui/assets/index-CgHq6xeZ.css">
9
9
  </head>
10
10
  <body>
package/nlm/README.md CHANGED
@@ -7,7 +7,7 @@ the prompt-recall hook.
7
7
 
8
8
  Pi's interactive UI strips `index.{ts,js}` from the load path, so this
9
9
  extension surfaces in pi's `[Extensions]` list as `nlm` — matching the
10
- naming convention used by `pi-mcp-adapter`, `whtnxt-tasks`, etc.
10
+ naming convention used by `pi-mcp-adapter`, `my-tasks`, etc.
11
11
 
12
12
  ## Install
13
13
 
package/nlm/index.js CHANGED
@@ -53,7 +53,7 @@ var RECALL_LIMIT = 5;
53
53
  var RECALL_TIMEOUT_MS = 2e3;
54
54
  async function recallOverHttp(prompt, runtime) {
55
55
  const portValue = process.env["NLM_PORT"] ?? "3940";
56
- const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(prompt)}&mode=keyword&limit=${RECALL_LIMIT}`;
56
+ const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(prompt)}&mode=keyword&limit=${RECALL_LIMIT}&withFacts=true`;
57
57
  const controller = new AbortController();
58
58
  const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
59
59
  try {
@@ -63,19 +63,26 @@ async function recallOverHttp(prompt, runtime) {
63
63
  headers: hookAuthHeaders(extra),
64
64
  signal: controller.signal
65
65
  });
66
- if (!res.ok) return [];
66
+ if (!res.ok) return { hits: [], facts: [] };
67
67
  let body;
68
68
  try {
69
69
  body = await res.json();
70
70
  } catch {
71
- return [];
71
+ return { hits: [], facts: [] };
72
72
  }
73
- return (body.results ?? []).map((r) => ({
73
+ const hits = (body.results ?? []).map((r) => ({
74
74
  id: r.id,
75
75
  label: r.label,
76
76
  startedAt: r.startedAt,
77
77
  matchScore: r.matchScore
78
78
  }));
79
+ const facts = (body.relatedFacts ?? []).map((f) => ({
80
+ subject: f.subject,
81
+ predicate: f.predicate,
82
+ value: f.value,
83
+ corroborationCount: f.corroborationCount
84
+ }));
85
+ return { hits, facts };
79
86
  } finally {
80
87
  clearTimeout(timer);
81
88
  }
@@ -145,16 +152,27 @@ function recordSurfaced(conversationId, ids) {
145
152
  }
146
153
 
147
154
  // src/core/hook/pointer-block.ts
148
- function formatPointerBlock(hits) {
149
- if (hits.length === 0) return "";
150
- const lines = hits.map(
151
- (h) => `- ${h.id} \xB7 ${h.label} (${h.startedAt.slice(0, 10)})`
152
- );
153
- return [
154
- "## Possibly-relevant prior sessions (nlm-memory)",
155
- ...lines,
155
+ function formatPointerBlock(hits, facts = []) {
156
+ if (hits.length === 0 && facts.length === 0) return "";
157
+ const out = [];
158
+ if (hits.length > 0) {
159
+ out.push("## Possibly-relevant prior sessions (nlm-memory)");
160
+ for (const h of hits) {
161
+ out.push(`- ${h.id} \xB7 ${h.label} (${h.startedAt.slice(0, 10)})`);
162
+ }
163
+ }
164
+ if (facts.length > 0) {
165
+ if (out.length > 0) out.push("");
166
+ out.push("## Known facts about top entities");
167
+ for (const f of facts) {
168
+ const tag = f.corroborationCount > 1 ? ` [${f.corroborationCount} sessions]` : "";
169
+ out.push(`- ${f.subject} ${f.predicate}: ${f.value}${tag}`);
170
+ }
171
+ }
172
+ out.push(
156
173
  "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved)."
157
- ].join("\n");
174
+ );
175
+ return out.join("\n");
158
176
  }
159
177
 
160
178
  // src/core/hook/select.ts
@@ -173,6 +191,10 @@ var SCORE_THRESHOLD = 0;
173
191
  var PER_FIRE_CAP = 3;
174
192
  var PER_CONVERSATION_CAP = 10;
175
193
  var PROMPT_PREVIEW_CHARS = 200;
194
+ function normalizeRecall(raw) {
195
+ if (Array.isArray(raw)) return { hits: raw, facts: [] };
196
+ return raw;
197
+ }
176
198
  async function runHook(input, deps) {
177
199
  const gate = classifyPrompt(input.prompt);
178
200
  const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
@@ -189,12 +211,13 @@ async function runHook(input, deps) {
189
211
  });
190
212
  return "";
191
213
  }
192
- let hits = [];
214
+ let fetched = { hits: [], facts: [] };
193
215
  try {
194
- hits = await deps.recall(input.prompt);
216
+ fetched = normalizeRecall(await deps.recall(input.prompt));
195
217
  } catch {
196
- hits = [];
218
+ fetched = { hits: [], facts: [] };
197
219
  }
220
+ const hits = fetched.hits;
198
221
  const surfaced = loadSurfaced(input.conversationId);
199
222
  const selected = selectHits({
200
223
  hits,
@@ -203,7 +226,7 @@ async function runHook(input, deps) {
203
226
  perFireCap: PER_FIRE_CAP,
204
227
  perConversationCap: PER_CONVERSATION_CAP
205
228
  });
206
- const block = formatPointerBlock(selected);
229
+ const block = formatPointerBlock(selected, fetched.facts);
207
230
  const estTokens = Math.ceil(block.length / 4);
208
231
  appendHookLog({
209
232
  ts: (/* @__PURE__ */ new Date()).toISOString(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nlm-memory",
3
- "version": "0.5.22",
3
+ "version": "0.6.0",
4
4
  "description": "Local-first non-linear memory operating system for AI operators.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -9,11 +9,11 @@
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://github.com/pbmagnet4/nlm-memory-ts.git"
12
+ "url": "git+https://github.com/pbmagnet4/nlm-memory.git"
13
13
  },
14
- "homepage": "https://github.com/pbmagnet4/nlm-memory-ts#readme",
14
+ "homepage": "https://github.com/pbmagnet4/nlm-memory#readme",
15
15
  "bugs": {
16
- "url": "https://github.com/pbmagnet4/nlm-memory-ts/issues"
16
+ "url": "https://github.com/pbmagnet4/nlm-memory/issues"
17
17
  },
18
18
  "keywords": [
19
19
  "ai",
@@ -54,7 +54,8 @@
54
54
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
55
55
  "bench:longmemeval": "tsx scripts/longmemeval/run-harness.ts",
56
56
  "bench:classifier": "tsx scripts/longmemeval/run-harness.ts --classifier",
57
- "bench:compare": "tsx scripts/longmemeval/compare-classifiers.ts"
57
+ "bench:compare": "tsx scripts/longmemeval/compare-classifiers.ts",
58
+ "eval:signals": "tsx scripts/eval/signals-eval.ts"
58
59
  },
59
60
  "dependencies": {
60
61
  "@clack/prompts": "^1.4.0",
@@ -7,8 +7,8 @@
7
7
  "url": "https://github.com/pbmagnet4"
8
8
  },
9
9
  "license": "MIT",
10
- "homepage": "https://github.com/pbmagnet4/nlm-memory-ts",
11
- "repository": "https://github.com/pbmagnet4/nlm-memory-ts",
10
+ "homepage": "https://github.com/pbmagnet4/nlm-memory",
11
+ "repository": "https://github.com/pbmagnet4/nlm-memory",
12
12
  "keywords": [
13
13
  "memory",
14
14
  "context",
package/plugin/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This directory is the Codex plugin distribution surface for nlm-memory. The
4
4
  repo root acts as a Codex marketplace (`codex plugin marketplace add
5
- pbmagnet4/nlm-memory-ts`), and this directory is the plugin Codex installs.
5
+ pbmagnet4/nlm-memory`), and this directory is the plugin Codex installs.
6
6
 
7
7
  ## Install
8
8
 
@@ -10,8 +10,8 @@ Prerequisite: `npm install -g nlm-memory` (puts `nlm` on PATH; the MCP server
10
10
  spawns `nlm mcp`).
11
11
 
12
12
  ```bash
13
- codex plugin marketplace add pbmagnet4/nlm-memory-ts
14
- codex plugin add nlm-memory@nlm-memory-ts
13
+ codex plugin marketplace add pbmagnet4/nlm-memory
14
+ codex plugin add nlm-memory@nlm-memory
15
15
  ```
16
16
 
17
17
  Or use the wrapper: `nlm connect codex`.
@@ -64,16 +64,27 @@ function recordSurfaced(conversationId, ids) {
64
64
  }
65
65
 
66
66
  // src/core/hook/pointer-block.ts
67
- function formatPointerBlock(hits) {
68
- if (hits.length === 0) return "";
69
- const lines = hits.map(
70
- (h) => `- ${h.id} \xB7 ${h.label} (${h.startedAt.slice(0, 10)})`
71
- );
72
- return [
73
- "## Possibly-relevant prior sessions (nlm-memory)",
74
- ...lines,
67
+ function formatPointerBlock(hits, facts = []) {
68
+ if (hits.length === 0 && facts.length === 0) return "";
69
+ const out = [];
70
+ if (hits.length > 0) {
71
+ out.push("## Possibly-relevant prior sessions (nlm-memory)");
72
+ for (const h of hits) {
73
+ out.push(`- ${h.id} \xB7 ${h.label} (${h.startedAt.slice(0, 10)})`);
74
+ }
75
+ }
76
+ if (facts.length > 0) {
77
+ if (out.length > 0) out.push("");
78
+ out.push("## Known facts about top entities");
79
+ for (const f of facts) {
80
+ const tag = f.corroborationCount > 1 ? ` [${f.corroborationCount} sessions]` : "";
81
+ out.push(`- ${f.subject} ${f.predicate}: ${f.value}${tag}`);
82
+ }
83
+ }
84
+ out.push(
75
85
  "NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved)."
76
- ].join("\n");
86
+ );
87
+ return out.join("\n");
77
88
  }
78
89
 
79
90
  // src/core/hook/select.ts
@@ -142,7 +153,7 @@ var RECALL_LIMIT = 5;
142
153
  var RECALL_TIMEOUT_MS = 2e3;
143
154
  async function recallOverHttp(prompt, runtime) {
144
155
  const portValue = process.env["NLM_PORT"] ?? "3940";
145
- const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(prompt)}&mode=keyword&limit=${RECALL_LIMIT}`;
156
+ const url = `http://localhost:${portValue}/api/recall?q=${encodeURIComponent(prompt)}&mode=keyword&limit=${RECALL_LIMIT}&withFacts=true`;
146
157
  const controller = new AbortController();
147
158
  const timer = setTimeout(() => controller.abort(), RECALL_TIMEOUT_MS);
148
159
  try {
@@ -152,19 +163,26 @@ async function recallOverHttp(prompt, runtime) {
152
163
  headers: hookAuthHeaders(extra),
153
164
  signal: controller.signal
154
165
  });
155
- if (!res.ok) return [];
166
+ if (!res.ok) return { hits: [], facts: [] };
156
167
  let body;
157
168
  try {
158
169
  body = await res.json();
159
170
  } catch {
160
- return [];
171
+ return { hits: [], facts: [] };
161
172
  }
162
- return (body.results ?? []).map((r) => ({
173
+ const hits = (body.results ?? []).map((r) => ({
163
174
  id: r.id,
164
175
  label: r.label,
165
176
  startedAt: r.startedAt,
166
177
  matchScore: r.matchScore
167
178
  }));
179
+ const facts = (body.relatedFacts ?? []).map((f) => ({
180
+ subject: f.subject,
181
+ predicate: f.predicate,
182
+ value: f.value,
183
+ corroborationCount: f.corroborationCount
184
+ }));
185
+ return { hits, facts };
168
186
  } finally {
169
187
  clearTimeout(timer);
170
188
  }
@@ -175,6 +193,10 @@ var SCORE_THRESHOLD = 0;
175
193
  var PER_FIRE_CAP = 3;
176
194
  var PER_CONVERSATION_CAP = 10;
177
195
  var PROMPT_PREVIEW_CHARS = 200;
196
+ function normalizeRecall(raw) {
197
+ if (Array.isArray(raw)) return { hits: raw, facts: [] };
198
+ return raw;
199
+ }
178
200
  async function runHook(input, deps) {
179
201
  const gate = classifyPrompt(input.prompt);
180
202
  const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
@@ -191,12 +213,13 @@ async function runHook(input, deps) {
191
213
  });
192
214
  return "";
193
215
  }
194
- let hits = [];
216
+ let fetched = { hits: [], facts: [] };
195
217
  try {
196
- hits = await deps.recall(input.prompt);
218
+ fetched = normalizeRecall(await deps.recall(input.prompt));
197
219
  } catch {
198
- hits = [];
220
+ fetched = { hits: [], facts: [] };
199
221
  }
222
+ const hits = fetched.hits;
200
223
  const surfaced = loadSurfaced(input.conversationId);
201
224
  const selected = selectHits({
202
225
  hits,
@@ -205,7 +228,7 @@ async function runHook(input, deps) {
205
228
  perFireCap: PER_FIRE_CAP,
206
229
  perConversationCap: PER_CONVERSATION_CAP
207
230
  });
208
- const block = formatPointerBlock(selected);
231
+ const block = formatPointerBlock(selected, fetched.facts);
209
232
  const estTokens = Math.ceil(block.length / 4);
210
233
  appendHookLog({
211
234
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3,8 +3,8 @@
3
3
  // src/hook/stop-hook.ts
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
6
- import { homedir as homedir4 } from "node:os";
7
- import { dirname, join as join3 } from "node:path";
6
+ import { homedir as homedir5 } from "node:os";
7
+ import { dirname as dirname2, join as join4 } from "node:path";
8
8
 
9
9
  // src/core/hook/citation-detect.ts
10
10
  var MIN_ID_LEN = 6;
@@ -58,6 +58,43 @@ function safeStringify(value) {
58
58
  }
59
59
  }
60
60
 
61
+ // src/core/hook/miss-detect.ts
62
+ var MIN_ID_LEN2 = 6;
63
+ function detectMisses(input) {
64
+ const surfaced = /* @__PURE__ */ new Set();
65
+ for (const id of input.surfacedIds) {
66
+ if (id.length >= MIN_ID_LEN2) surfaced.add(id);
67
+ }
68
+ const seenMissed = /* @__PURE__ */ new Set();
69
+ const misses = [];
70
+ for (const tu of input.toolUses) {
71
+ if (!isNlmTool2(tu.name)) continue;
72
+ const kind = explicitIdKind(tu.name);
73
+ if (!kind) continue;
74
+ const id = extractId(tu.input);
75
+ if (!id || id.length < MIN_ID_LEN2) continue;
76
+ if (surfaced.has(id)) continue;
77
+ if (seenMissed.has(id)) continue;
78
+ seenMissed.add(id);
79
+ misses.push({ id, kind });
80
+ }
81
+ return misses;
82
+ }
83
+ function isNlmTool2(name) {
84
+ return /^mcp__[^_]*nlm[^_]*__/.test(name);
85
+ }
86
+ function explicitIdKind(name) {
87
+ if (name.endsWith("__get_session")) return "get_session";
88
+ if (name.endsWith("__cite_session")) return "cite_session";
89
+ return null;
90
+ }
91
+ function extractId(input) {
92
+ if (input === null || typeof input !== "object") return null;
93
+ const obj = input;
94
+ const candidate = obj["id"];
95
+ return typeof candidate === "string" ? candidate : null;
96
+ }
97
+
61
98
  // src/core/hook/memo.ts
62
99
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
63
100
  import { homedir } from "node:os";
@@ -114,6 +151,32 @@ function recordCited(conversationId, ids) {
114
151
  }
115
152
  }
116
153
 
154
+ // src/core/recall/miss-log.ts
155
+ import { appendFile, mkdir, readFile, stat } from "node:fs/promises";
156
+ import { dirname, join as join3 } from "node:path";
157
+ import { homedir as homedir3 } from "node:os";
158
+ function defaultLogPath() {
159
+ return process.env["NLM_MISS_LOG"] ?? join3(homedir3(), ".nlm", "miss-log.jsonl");
160
+ }
161
+ function isEnabled() {
162
+ const raw = process.env["NLM_MISS_LOG_ENABLED"];
163
+ if (raw === void 0) return true;
164
+ return raw !== "0" && raw.toLowerCase() !== "false";
165
+ }
166
+ async function appendMisses(entries, logPath2) {
167
+ if (!isEnabled()) return;
168
+ if (entries.length === 0) return;
169
+ const path = logPath2 ?? defaultLogPath();
170
+ try {
171
+ await mkdir(dirname(path), { recursive: true });
172
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
173
+ const block = entries.map((e) => `${JSON.stringify({ ts, ...e })}
174
+ `).join("");
175
+ await appendFile(path, block, "utf8");
176
+ } catch {
177
+ }
178
+ }
179
+
117
180
  // src/core/hook/transcript.ts
118
181
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
119
182
  function parseTurn(parsed) {
@@ -164,7 +227,7 @@ function readAllAssistantTurns(transcriptPath) {
164
227
 
165
228
  // src/llm/env-autoload.ts
166
229
  import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
167
- import { homedir as homedir3 } from "node:os";
230
+ import { homedir as homedir4 } from "node:os";
168
231
  import { resolve } from "node:path";
169
232
  var DEFAULT_SEARCH_PATHS = [
170
233
  "~/.nlm/.env",
@@ -173,7 +236,7 @@ var DEFAULT_SEARCH_PATHS = [
173
236
  "../../.env"
174
237
  ];
175
238
  function expandHome(p) {
176
- if (p.startsWith("~/")) return resolve(homedir3(), p.slice(2));
239
+ if (p.startsWith("~/")) return resolve(homedir4(), p.slice(2));
177
240
  return p;
178
241
  }
179
242
  function autoloadEnv(extraPaths = []) {
@@ -259,6 +322,17 @@ async function runStopHook(input, deps) {
259
322
  });
260
323
  const alreadyCited = loadCited(input.conversationId);
261
324
  const fresh = detected.filter((c) => !alreadyCited.has(c.id));
325
+ const misses = detectMisses({ toolUses: allToolUses, surfacedIds: surfaced });
326
+ if (misses.length > 0) {
327
+ void appendMisses(
328
+ misses.map((m) => ({
329
+ conversationId: input.conversationId,
330
+ missedId: m.id,
331
+ kind: m.kind,
332
+ surfacedCount: surfaced.size
333
+ }))
334
+ );
335
+ }
262
336
  const lastText = turns[turns.length - 1]?.text ?? "";
263
337
  const preview = lastText.slice(0, RESPONSE_PREVIEW_CHARS);
264
338
  for (const c of fresh) {
@@ -279,12 +353,12 @@ async function runStopHook(input, deps) {
279
353
  };
280
354
  }
281
355
  function logPath() {
282
- return process.env["NLM_HOOK_LOG"] ?? join3(homedir4(), ".nlm", "hook-log.jsonl");
356
+ return process.env["NLM_HOOK_LOG"] ?? join4(homedir5(), ".nlm", "hook-log.jsonl");
283
357
  }
284
358
  function logStopResult(result) {
285
359
  try {
286
360
  const path = logPath();
287
- mkdirSync3(dirname(path), { recursive: true });
361
+ mkdirSync3(dirname2(path), { recursive: true });
288
362
  appendFileSync(
289
363
  path,
290
364
  `${JSON.stringify({