nlm-memory 0.5.21 → 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.
- package/README.md +129 -19
- package/dist/cli/classify-parity.js +1 -1
- package/dist/cli/classify-parity.js.map +1 -1
- package/dist/cli/nlm.js +152 -12
- package/dist/cli/nlm.js.map +1 -1
- 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/adapters/pi.js +13 -3
- package/dist/core/adapters/pi.js.map +1 -1
- package/dist/core/classifier/prompt.d.ts +1 -1
- package/dist/core/classifier/prompt.js +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/hook/pointer-block.d.ts +12 -1
- package/dist/core/hook/pointer-block.js +25 -8
- package/dist/core/hook/pointer-block.js.map +1 -1
- package/dist/core/providers/provider-registry.js +1 -1
- package/dist/core/providers/provider-registry.js.map +1 -1
- 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/recall-service.d.ts +9 -0
- package/dist/core/recall/recall-service.js +81 -18
- package/dist/core/recall/recall-service.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/recall-facts/fact-recall-service.d.ts +11 -0
- package/dist/core/recall-facts/fact-recall-service.js +69 -4
- package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
- package/dist/core/scheduler/scheduler.d.ts +7 -0
- package/dist/core/scheduler/scheduler.js +26 -0
- package/dist/core/scheduler/scheduler.js.map +1 -1
- package/dist/core/signals/aggregate.d.ts +21 -0
- package/dist/core/signals/aggregate.js +34 -0
- package/dist/core/signals/aggregate.js.map +1 -0
- package/dist/core/signals/failure-mode-recall.d.ts +18 -0
- package/dist/core/signals/failure-mode-recall.js +28 -0
- package/dist/core/signals/failure-mode-recall.js.map +1 -0
- package/dist/core/signals/ingest-signal.d.ts +19 -0
- package/dist/core/signals/ingest-signal.js +63 -0
- package/dist/core/signals/ingest-signal.js.map +1 -0
- package/dist/core/signals/install-scope.d.ts +8 -0
- package/dist/core/signals/install-scope.js +41 -0
- package/dist/core/signals/install-scope.js.map +1 -0
- package/dist/core/signals/recommend.d.ts +13 -0
- package/dist/core/signals/recommend.js +24 -0
- package/dist/core/signals/recommend.js.map +1 -0
- package/dist/core/storage/pg-fact-store.d.ts +5 -0
- package/dist/core/storage/pg-fact-store.js +29 -0
- package/dist/core/storage/pg-fact-store.js.map +1 -1
- 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/pg-signal-store.d.ts +16 -0
- package/dist/core/storage/pg-signal-store.js +100 -0
- package/dist/core/storage/pg-signal-store.js.map +1 -0
- package/dist/core/storage/pg-storage.d.ts +2 -0
- package/dist/core/storage/pg-storage.js +3 -0
- package/dist/core/storage/pg-storage.js.map +1 -1
- package/dist/core/storage/pg-tx-context.d.ts +5 -0
- package/dist/core/storage/pg-tx-context.js +3 -0
- package/dist/core/storage/pg-tx-context.js.map +1 -1
- package/dist/core/storage/sqlite-fact-store.d.ts +5 -0
- package/dist/core/storage/sqlite-fact-store.js +30 -0
- package/dist/core/storage/sqlite-fact-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/core/storage/sqlite-signal-store.d.ts +20 -0
- package/dist/core/storage/sqlite-signal-store.js +112 -0
- package/dist/core/storage/sqlite-signal-store.js.map +1 -0
- package/dist/core/storage/sqlite-storage.d.ts +2 -0
- package/dist/core/storage/sqlite-storage.js +6 -2
- package/dist/core/storage/sqlite-storage.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.d.ts +10 -1
- package/dist/hook/prompt-recall-hook.js +11 -5
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/hook/recall-over-http.d.ts +11 -1
- package/dist/hook/recall-over-http.js +21 -6
- package/dist/hook/recall-over-http.js.map +1 -1
- package/dist/hook/session-start-hook.d.ts +2 -0
- package/dist/hook/session-start-hook.js +36 -3
- package/dist/hook/session-start-hook.js.map +1 -1
- package/dist/hook/stop-hook.js +16 -0
- package/dist/hook/stop-hook.js.map +1 -1
- package/dist/http/app.d.ts +9 -0
- package/dist/http/app.js +125 -10
- package/dist/http/app.js.map +1 -1
- package/dist/install/codex.d.ts +1 -1
- package/dist/install/codex.js +4 -4
- package/dist/install/codex.js.map +1 -1
- package/dist/install/ollama.d.ts +10 -0
- package/dist/install/ollama.js +19 -3
- package/dist/install/ollama.js.map +1 -1
- package/dist/install/pi.d.ts +1 -1
- package/dist/install/pi.js +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/install/setup.js +63 -10
- package/dist/install/setup.js.map +1 -1
- package/dist/llm/classifier-box.d.ts +2 -1
- package/dist/llm/classifier-box.js +3 -0
- package/dist/llm/classifier-box.js.map +1 -1
- package/dist/llm/deepseek-client.d.ts +5 -2
- package/dist/llm/deepseek-client.js +51 -1
- package/dist/llm/deepseek-client.js.map +1 -1
- package/dist/llm/ollama-client.d.ts +3 -2
- package/dist/llm/ollama-client.js +45 -2
- package/dist/llm/ollama-client.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +23 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/ports/fact-store.d.ts +0 -0
- package/dist/ports/fact-store.js +0 -0
- package/dist/ports/fact-store.js.map +1 -1
- package/dist/ports/llm-client.d.ts +24 -0
- package/dist/ports/signal-store.d.ts +30 -0
- package/dist/ports/signal-store.js +9 -0
- package/dist/ports/signal-store.js.map +1 -0
- package/dist/ports/storage.d.ts +2 -0
- package/dist/ports/transcript-adapter.d.ts +6 -0
- package/dist/shared/types.d.ts +76 -0
- package/dist/ui/assets/index-CgHq6xeZ.css +1 -0
- package/dist/ui/assets/index-DLDd-OM_.js +69 -0
- package/dist/ui/index.html +2 -2
- package/nlm/README.md +1 -1
- package/nlm/index.js +46 -21
- package/package.json +7 -5
- package/plugin/.codex-plugin/plugin.json +2 -2
- package/plugin/README.md +3 -3
- package/plugin/scripts/prompt-recall-hook.mjs +45 -20
- package/plugin/scripts/stop-hook.mjs +80 -6
- 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
package/dist/ui/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/ui/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/ui/assets/index-DLDd-OM_.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/ui/assets/index-CgHq6xeZ.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
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`, `
|
|
10
|
+
naming convention used by `pi-mcp-adapter`, `my-tasks`, etc.
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
package/nlm/index.js
CHANGED
|
@@ -51,29 +51,38 @@ function hookAuthHeaders(extra = {}) {
|
|
|
51
51
|
// src/hook/recall-over-http.ts
|
|
52
52
|
var RECALL_LIMIT = 5;
|
|
53
53
|
var RECALL_TIMEOUT_MS = 2e3;
|
|
54
|
-
async function recallOverHttp(prompt) {
|
|
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 {
|
|
60
|
+
const extra = { "x-recall-source": "hook" };
|
|
61
|
+
if (runtime) extra["x-recall-runtime"] = runtime;
|
|
60
62
|
const res = await fetch(url, {
|
|
61
|
-
headers: hookAuthHeaders(
|
|
63
|
+
headers: hookAuthHeaders(extra),
|
|
62
64
|
signal: controller.signal
|
|
63
65
|
});
|
|
64
|
-
if (!res.ok) return [];
|
|
66
|
+
if (!res.ok) return { hits: [], facts: [] };
|
|
65
67
|
let body;
|
|
66
68
|
try {
|
|
67
69
|
body = await res.json();
|
|
68
70
|
} catch {
|
|
69
|
-
return [];
|
|
71
|
+
return { hits: [], facts: [] };
|
|
70
72
|
}
|
|
71
|
-
|
|
73
|
+
const hits = (body.results ?? []).map((r) => ({
|
|
72
74
|
id: r.id,
|
|
73
75
|
label: r.label,
|
|
74
76
|
startedAt: r.startedAt,
|
|
75
77
|
matchScore: r.matchScore
|
|
76
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 };
|
|
77
86
|
} finally {
|
|
78
87
|
clearTimeout(timer);
|
|
79
88
|
}
|
|
@@ -143,16 +152,27 @@ function recordSurfaced(conversationId, ids) {
|
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
// src/core/hook/pointer-block.ts
|
|
146
|
-
function formatPointerBlock(hits) {
|
|
147
|
-
if (hits.length === 0) return "";
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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(
|
|
154
173
|
"NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved)."
|
|
155
|
-
|
|
174
|
+
);
|
|
175
|
+
return out.join("\n");
|
|
156
176
|
}
|
|
157
177
|
|
|
158
178
|
// src/core/hook/select.ts
|
|
@@ -171,6 +191,10 @@ var SCORE_THRESHOLD = 0;
|
|
|
171
191
|
var PER_FIRE_CAP = 3;
|
|
172
192
|
var PER_CONVERSATION_CAP = 10;
|
|
173
193
|
var PROMPT_PREVIEW_CHARS = 200;
|
|
194
|
+
function normalizeRecall(raw) {
|
|
195
|
+
if (Array.isArray(raw)) return { hits: raw, facts: [] };
|
|
196
|
+
return raw;
|
|
197
|
+
}
|
|
174
198
|
async function runHook(input, deps) {
|
|
175
199
|
const gate = classifyPrompt(input.prompt);
|
|
176
200
|
const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
|
|
@@ -187,12 +211,13 @@ async function runHook(input, deps) {
|
|
|
187
211
|
});
|
|
188
212
|
return "";
|
|
189
213
|
}
|
|
190
|
-
let
|
|
214
|
+
let fetched = { hits: [], facts: [] };
|
|
191
215
|
try {
|
|
192
|
-
|
|
216
|
+
fetched = normalizeRecall(await deps.recall(input.prompt));
|
|
193
217
|
} catch {
|
|
194
|
-
|
|
218
|
+
fetched = { hits: [], facts: [] };
|
|
195
219
|
}
|
|
220
|
+
const hits = fetched.hits;
|
|
196
221
|
const surfaced = loadSurfaced(input.conversationId);
|
|
197
222
|
const selected = selectHits({
|
|
198
223
|
hits,
|
|
@@ -201,7 +226,7 @@ async function runHook(input, deps) {
|
|
|
201
226
|
perFireCap: PER_FIRE_CAP,
|
|
202
227
|
perConversationCap: PER_CONVERSATION_CAP
|
|
203
228
|
});
|
|
204
|
-
const block = formatPointerBlock(selected);
|
|
229
|
+
const block = formatPointerBlock(selected, fetched.facts);
|
|
205
230
|
const estTokens = Math.ceil(block.length / 4);
|
|
206
231
|
appendHookLog({
|
|
207
232
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -239,7 +264,7 @@ async function main() {
|
|
|
239
264
|
const mode = process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
|
|
240
265
|
const out = await runHook(
|
|
241
266
|
{ prompt, conversationId },
|
|
242
|
-
{ mode, recall: recallOverHttp }
|
|
267
|
+
{ mode, recall: (q) => recallOverHttp(q, "claude-code") }
|
|
243
268
|
);
|
|
244
269
|
if (out) process.stdout.write(out);
|
|
245
270
|
} catch {
|
|
@@ -264,7 +289,7 @@ function nlmExtension(pi) {
|
|
|
264
289
|
const conversationId = ctx.sessionManager.getSessionId() || "unknown";
|
|
265
290
|
const block = await runHook(
|
|
266
291
|
{ prompt: event.text, conversationId },
|
|
267
|
-
{ mode, recall: recallOverHttp }
|
|
292
|
+
{ mode, recall: (q) => recallOverHttp(q, "pi") }
|
|
268
293
|
);
|
|
269
294
|
if (!block) return { action: "continue" };
|
|
270
295
|
return { action: "transform", text: `${block}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nlm-memory",
|
|
3
|
-
"version": "0.
|
|
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
|
|
12
|
+
"url": "git+https://github.com/pbmagnet4/nlm-memory.git"
|
|
13
13
|
},
|
|
14
|
-
"homepage": "https://github.com/pbmagnet4/nlm-memory
|
|
14
|
+
"homepage": "https://github.com/pbmagnet4/nlm-memory#readme",
|
|
15
15
|
"bugs": {
|
|
16
|
-
"url": "https://github.com/pbmagnet4/nlm-memory
|
|
16
|
+
"url": "https://github.com/pbmagnet4/nlm-memory/issues"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"ai",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
31
|
"plugin",
|
|
32
|
+
"plugin-hermes-agent",
|
|
32
33
|
"nlm",
|
|
33
34
|
"assets",
|
|
34
35
|
"LICENSE",
|
|
@@ -53,7 +54,8 @@
|
|
|
53
54
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
54
55
|
"bench:longmemeval": "tsx scripts/longmemeval/run-harness.ts",
|
|
55
56
|
"bench:classifier": "tsx scripts/longmemeval/run-harness.ts --classifier",
|
|
56
|
-
"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"
|
|
57
59
|
},
|
|
58
60
|
"dependencies": {
|
|
59
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
|
|
11
|
-
"repository": "https://github.com/pbmagnet4/nlm-memory
|
|
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
|
|
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
|
|
14
|
-
codex plugin add nlm-memory@nlm-memory
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
86
|
+
);
|
|
87
|
+
return out.join("\n");
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
// src/core/hook/select.ts
|
|
@@ -140,29 +151,38 @@ function hookAuthHeaders(extra = {}) {
|
|
|
140
151
|
// src/hook/recall-over-http.ts
|
|
141
152
|
var RECALL_LIMIT = 5;
|
|
142
153
|
var RECALL_TIMEOUT_MS = 2e3;
|
|
143
|
-
async function recallOverHttp(prompt) {
|
|
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 {
|
|
160
|
+
const extra = { "x-recall-source": "hook" };
|
|
161
|
+
if (runtime) extra["x-recall-runtime"] = runtime;
|
|
149
162
|
const res = await fetch(url, {
|
|
150
|
-
headers: hookAuthHeaders(
|
|
163
|
+
headers: hookAuthHeaders(extra),
|
|
151
164
|
signal: controller.signal
|
|
152
165
|
});
|
|
153
|
-
if (!res.ok) return [];
|
|
166
|
+
if (!res.ok) return { hits: [], facts: [] };
|
|
154
167
|
let body;
|
|
155
168
|
try {
|
|
156
169
|
body = await res.json();
|
|
157
170
|
} catch {
|
|
158
|
-
return [];
|
|
171
|
+
return { hits: [], facts: [] };
|
|
159
172
|
}
|
|
160
|
-
|
|
173
|
+
const hits = (body.results ?? []).map((r) => ({
|
|
161
174
|
id: r.id,
|
|
162
175
|
label: r.label,
|
|
163
176
|
startedAt: r.startedAt,
|
|
164
177
|
matchScore: r.matchScore
|
|
165
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 };
|
|
166
186
|
} finally {
|
|
167
187
|
clearTimeout(timer);
|
|
168
188
|
}
|
|
@@ -173,6 +193,10 @@ var SCORE_THRESHOLD = 0;
|
|
|
173
193
|
var PER_FIRE_CAP = 3;
|
|
174
194
|
var PER_CONVERSATION_CAP = 10;
|
|
175
195
|
var PROMPT_PREVIEW_CHARS = 200;
|
|
196
|
+
function normalizeRecall(raw) {
|
|
197
|
+
if (Array.isArray(raw)) return { hits: raw, facts: [] };
|
|
198
|
+
return raw;
|
|
199
|
+
}
|
|
176
200
|
async function runHook(input, deps) {
|
|
177
201
|
const gate = classifyPrompt(input.prompt);
|
|
178
202
|
const preview = input.prompt.slice(0, PROMPT_PREVIEW_CHARS);
|
|
@@ -189,12 +213,13 @@ async function runHook(input, deps) {
|
|
|
189
213
|
});
|
|
190
214
|
return "";
|
|
191
215
|
}
|
|
192
|
-
let
|
|
216
|
+
let fetched = { hits: [], facts: [] };
|
|
193
217
|
try {
|
|
194
|
-
|
|
218
|
+
fetched = normalizeRecall(await deps.recall(input.prompt));
|
|
195
219
|
} catch {
|
|
196
|
-
|
|
220
|
+
fetched = { hits: [], facts: [] };
|
|
197
221
|
}
|
|
222
|
+
const hits = fetched.hits;
|
|
198
223
|
const surfaced = loadSurfaced(input.conversationId);
|
|
199
224
|
const selected = selectHits({
|
|
200
225
|
hits,
|
|
@@ -203,7 +228,7 @@ async function runHook(input, deps) {
|
|
|
203
228
|
perFireCap: PER_FIRE_CAP,
|
|
204
229
|
perConversationCap: PER_CONVERSATION_CAP
|
|
205
230
|
});
|
|
206
|
-
const block = formatPointerBlock(selected);
|
|
231
|
+
const block = formatPointerBlock(selected, fetched.facts);
|
|
207
232
|
const estTokens = Math.ceil(block.length / 4);
|
|
208
233
|
appendHookLog({
|
|
209
234
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -241,7 +266,7 @@ async function main() {
|
|
|
241
266
|
const mode = process.env["NLM_HOOK_MODE"] === "live" ? "live" : "shadow";
|
|
242
267
|
const out = await runHook(
|
|
243
268
|
{ prompt, conversationId },
|
|
244
|
-
{ mode, recall: recallOverHttp }
|
|
269
|
+
{ mode, recall: (q) => recallOverHttp(q, "claude-code") }
|
|
245
270
|
);
|
|
246
271
|
if (out) process.stdout.write(out);
|
|
247
272
|
} catch {
|
|
@@ -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
|
|
7
|
-
import { dirname, join as
|
|
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
|
|
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(
|
|
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"] ??
|
|
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(
|
|
361
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
288
362
|
appendFileSync(
|
|
289
363
|
path,
|
|
290
364
|
`${JSON.stringify({
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# nlm-memory Hermes Agent plugin
|
|
2
|
+
|
|
3
|
+
This directory is the [NousResearch Hermes Agent](https://github.com/NousResearch/hermes-agent) plugin distribution surface for nlm-memory.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Prerequisite: `npm install -g nlm-memory` (puts `nlm` on PATH; the MCP server spawns `nlm mcp`).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Recommended — uses nlm connect to copy the plugin and enable it
|
|
11
|
+
nlm connect hermes-agent
|
|
12
|
+
|
|
13
|
+
# Or manually
|
|
14
|
+
cp -r plugin-hermes-agent ~/.hermes/plugins/nlm-memory
|
|
15
|
+
hermes plugins enable nlm-memory
|
|
16
|
+
nlm connect hermes # also writes the MCP server entry to ~/.hermes/config.yaml
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What this ships
|
|
20
|
+
|
|
21
|
+
- **`plugin.yaml`** — Hermes plugin manifest (name: `nlm-memory`, kind: `memory`)
|
|
22
|
+
- **`__init__.py`** — Python shim that registers 6 lifecycle hooks; all delegate to the local nlm daemon at `http://localhost:3940`
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
|
|
26
|
+
| Hermes hook | Action |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `pre_llm_call` | Keyword-recalls up to 3 relevant prior sessions; injects them as context above the user message |
|
|
29
|
+
| `post_llm_call` | Scans the assistant response for cited session IDs and logs prose citation events |
|
|
30
|
+
| `on_session_start` | No-op (memo is created lazily on first recall hit) |
|
|
31
|
+
| `on_session_end` | Clears the per-session surfaced-ID memo |
|
|
32
|
+
| `on_session_finalize` | Clears the per-session surfaced-ID memo |
|
|
33
|
+
| `on_session_reset` | Clears the per-session surfaced-ID memo |
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
| Env var | Default | Purpose |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `NLM_DAEMON_PORT` | `3940` | Port the nlm daemon listens on |
|
|
40
|
+
|
|
41
|
+
## Daemon required
|
|
42
|
+
|
|
43
|
+
The plugin delegates entirely to the nlm daemon. Start it with `nlm start` or `nlm install` (macOS LaunchAgent). If the daemon is unreachable, every hook silently returns `None` — the agent loop is never blocked.
|
|
44
|
+
|
|
45
|
+
## Uninstall
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
nlm disconnect hermes-agent
|
|
49
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nlm-memory Hermes Agent plugin.
|
|
3
|
+
|
|
4
|
+
Wires 6 Hermes lifecycle hooks to the local nlm-memory daemon
|
|
5
|
+
(http://localhost:3940 by default). All calls are fire-and-forget
|
|
6
|
+
except pre_llm_call, which returns a context string for injection.
|
|
7
|
+
|
|
8
|
+
The daemon must be running for hooks to have effect. If it is unreachable,
|
|
9
|
+
every hook silently returns None so the agent loop is never blocked.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.request
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _base_url() -> str:
|
|
20
|
+
port = os.environ.get("NLM_DAEMON_PORT", "3940")
|
|
21
|
+
return f"http://localhost:{port}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _post(path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
25
|
+
url = f"{_base_url()}/api/{path}"
|
|
26
|
+
data = json.dumps(payload).encode()
|
|
27
|
+
req = urllib.request.Request(
|
|
28
|
+
url,
|
|
29
|
+
data=data,
|
|
30
|
+
headers={"Content-Type": "application/json"},
|
|
31
|
+
method="POST",
|
|
32
|
+
)
|
|
33
|
+
try:
|
|
34
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
35
|
+
return json.loads(resp.read())
|
|
36
|
+
except (urllib.error.URLError, OSError, json.JSONDecodeError):
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register(ctx: Any) -> None:
|
|
41
|
+
def pre_llm_call(
|
|
42
|
+
session_id: str,
|
|
43
|
+
user_message: str,
|
|
44
|
+
is_first_turn: bool = False,
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
) -> str | None:
|
|
47
|
+
result = _post(
|
|
48
|
+
"hook/hermes-agent/pre-turn",
|
|
49
|
+
{"session_id": session_id, "user_message": user_message, "is_first_turn": is_first_turn},
|
|
50
|
+
)
|
|
51
|
+
context = result.get("context")
|
|
52
|
+
return context if isinstance(context, str) and context else None
|
|
53
|
+
|
|
54
|
+
def post_llm_call(
|
|
55
|
+
session_id: str,
|
|
56
|
+
assistant_response: str,
|
|
57
|
+
**kwargs: Any,
|
|
58
|
+
) -> None:
|
|
59
|
+
_post(
|
|
60
|
+
"hook/hermes-agent/post-turn",
|
|
61
|
+
{"session_id": session_id, "assistant_response": assistant_response},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _session_event(event: str, session_id: str | None, **kwargs: Any) -> None:
|
|
65
|
+
_post(
|
|
66
|
+
"hook/hermes-agent/session-lifecycle",
|
|
67
|
+
{"event": event, "session_id": session_id},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
ctx.register_hook("pre_llm_call", pre_llm_call)
|
|
71
|
+
ctx.register_hook("post_llm_call", post_llm_call)
|
|
72
|
+
ctx.register_hook("on_session_start", lambda session_id, **kw: _session_event("start", session_id, **kw))
|
|
73
|
+
ctx.register_hook("on_session_end", lambda session_id, **kw: _session_event("end", session_id, **kw))
|
|
74
|
+
ctx.register_hook("on_session_finalize", lambda session_id=None, **kw: _session_event("finalize", session_id, **kw))
|
|
75
|
+
ctx.register_hook("on_session_reset", lambda session_id, **kw: _session_event("reset", session_id, **kw))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: nlm-memory
|
|
2
|
+
version: 0.1.0
|
|
3
|
+
description: Non-linear memory for Hermes — surfaces relevant prior sessions at each turn via keyword recall
|
|
4
|
+
kind: memory
|
|
5
|
+
author: pbmagnet4
|
|
6
|
+
provides_hooks:
|
|
7
|
+
- pre_llm_call
|
|
8
|
+
- post_llm_call
|
|
9
|
+
- on_session_start
|
|
10
|
+
- on_session_end
|
|
11
|
+
- on_session_finalize
|
|
12
|
+
- on_session_reset
|
|
13
|
+
requires_env:
|
|
14
|
+
- name: NLM_DAEMON_PORT
|
|
15
|
+
description: "Port the nlm-memory daemon listens on (default: 3940)"
|