memory-pulse 0.1.9 → 0.2.1
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 +7 -0
- package/package.json +1 -1
- package/server.mjs +54 -15
package/README.md
CHANGED
|
@@ -107,6 +107,13 @@ to respect the guard.
|
|
|
107
107
|
(`.memory-pulse/telemetry.rain`): the engine advances it on each read call
|
|
108
108
|
and hands it back — it never stores it. `stats` verifies the signature;
|
|
109
109
|
`badge` turns it into a README badge. Delete the file and it restarts.
|
|
110
|
+
- **State persistence, no database.** After a read the engine hands back a
|
|
111
|
+
signed **memory key** (`.memory-pulse/memory.rain`, git-ignored). The next
|
|
112
|
+
read presents it and the engine resumes from it, ingesting only the events
|
|
113
|
+
recorded since — the answer is byte-identical to a full rebuild, and any
|
|
114
|
+
mismatch (edited history, a stepped ledger size, a bad signature) falls back
|
|
115
|
+
to a rebuild and says why. Lose the file and you lose nothing but one
|
|
116
|
+
rebuild. `MEMORY_PULSE_MEMORY_KEY=off` disables it.
|
|
110
117
|
- **Memory integrity.** A note that reads like an instruction ("ignore previous
|
|
111
118
|
instructions", "run this command", a fake system tag) is refused by
|
|
112
119
|
`remember` and, if one is already in a ledger, quarantined at read time and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-pulse",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Causal memory for coding agents that costs ~670 tokens, not your context window. Four MCP tools: re-enter a project, recall what caused what, record findings, run code against memory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server.mjs
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import readline from "node:readline";
|
|
23
23
|
import http from "node:http";
|
|
24
24
|
import https from "node:https";
|
|
25
|
+
import { gzipSync } from "node:zlib";
|
|
25
26
|
import { existsSync, mkdirSync, readFileSync, appendFileSync, writeFileSync, realpathSync } from "node:fs";
|
|
26
27
|
import { dirname, isAbsolute, join } from "node:path";
|
|
27
28
|
import { fileURLToPath } from "node:url";
|
|
@@ -51,18 +52,18 @@ function readEvents() {
|
|
|
51
52
|
return { path, events };
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
// The engine's
|
|
55
|
+
// The engine's memory-safety checks, mirrored so a refusal happens
|
|
55
56
|
// before the write. Keep in step with the engine; the engine is authoritative.
|
|
56
57
|
const INSTRUCTION_PATTERNS = [
|
|
57
|
-
["
|
|
58
|
-
["
|
|
59
|
-
["
|
|
60
|
-
["
|
|
61
|
-
["
|
|
62
|
-
["
|
|
63
|
-
["
|
|
64
|
-
["
|
|
65
|
-
["
|
|
58
|
+
["protect-instructions", /\b(ignore|disregard|forget)\b[^.\n]{0,40}\b(previous|prior|above|all|earlier)\b[^.\n]{0,20}\b(instructions?|rules?|prompts?)\b/i],
|
|
59
|
+
["protect-role", /\byou are now\b|\bfrom now on,? you\b|\bact as (an?|the) (system|admin|developer)\b/i],
|
|
60
|
+
["protect-boundaries", /<\/?\s*(system|assistant|tool|user|human|developer)\s*>|\[(system|assistant|tool)\s*:?\s*\]/i],
|
|
61
|
+
["protect-configuration", /\b(reveal|print|show|dump)\b[^.\n]{0,30}\b(system prompt|hidden prompt|your instructions)\b/i],
|
|
62
|
+
["protect-execution", /\b(run|execute|paste)\b[^.\n]{0,25}\b(this|the following)\b[^.\n]{0,15}\b(command|script|shell|code)\b/i],
|
|
63
|
+
["protect-installs", /\b(curl|wget)\b[^\n]{0,120}\|\s*(sudo\s+)?(sh|bash|zsh)\b/i],
|
|
64
|
+
["protect-data", /\b(rm\s+-rf\s+[\/~]|drop\s+table|truncate\s+table|format\s+c:)/i],
|
|
65
|
+
["protect-credentials", /\b(send|post|upload|exfiltrat\w*|leak|forward|email)\b[^.\n]{0,40}\b(api[\s_-]?keys?|secrets?|tokens?|passwords?|credentials?)\b[^.\n]{0,40}\b(to|via|at)\b[^.\n]{0,4}(https?:\/\/|[\w.-]+@[\w.-]+\b|this\b|the following\b|that (address|url|endpoint|server)\b|me\b)|\b(send|give|show|tell) me\b[^.\n]{0,30}\b(api[\s_-]?keys?|secrets?|tokens?|passwords?|credentials?)\b/i ],
|
|
66
|
+
["protect-transparency", /\b(do not|don't|never)\b[^.\n]{0,20}\b(tell|show|mention|reveal)\b[^.\n]{0,20}\b(the )?(user|human|operator)\b/i],
|
|
66
67
|
];
|
|
67
68
|
export function instructionLike(text) {
|
|
68
69
|
if (typeof text !== "string" || !text) return [];
|
|
@@ -128,10 +129,41 @@ const fmtK = (n) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 10
|
|
|
128
129
|
export function telemetryFooter(c) {
|
|
129
130
|
const k = c?.counters;
|
|
130
131
|
if (!k || !k.calls) return "";
|
|
131
|
-
|
|
132
|
+
// Quarantine is printed on its own line by the brief; the footer carries
|
|
133
|
+
// the drift reasons that are actually about the ledger changing.
|
|
134
|
+
const reasons = (c.drift?.reasons ?? []).filter((r) => !/quarantined/.test(r));
|
|
135
|
+
const drift = reasons.length ? ` · ⚠ drift: ${reasons.join("; ")}` : "";
|
|
132
136
|
return `— memory-pulse · ${k.pulse} re-entries · ${k.correctionsSurfaced} corrections surfaced · ~${fmtK(k.tokensSavedEst)} tokens saved (est., signed)${drift}`;
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
// ------------------------------------------------------------ memory key ----
|
|
140
|
+
// State persistence without a database. After a read the engine hands back a
|
|
141
|
+
// signed memory key; presenting it on the next read resumes the memory and
|
|
142
|
+
// ingests only the events recorded since (measured: a full re-entry on an
|
|
143
|
+
// 825-event ledger went from 4.5 s to 1.5 s). It lives beside your ledger as
|
|
144
|
+
// memory.rain, it is yours, and a lost or stale key costs one rebuild — never
|
|
145
|
+
// data. It never enters the agent's context. MEMORY_PULSE_MEMORY_KEY=off disables it.
|
|
146
|
+
const memoryKeyPath = () => join(dirname(ledgerPath()), "memory.rain");
|
|
147
|
+
const memoryKeyOn = () => (process.env.MEMORY_PULSE_MEMORY_KEY || "on") !== "off";
|
|
148
|
+
function readMemoryKey() {
|
|
149
|
+
if (!memoryKeyOn()) return null;
|
|
150
|
+
try { return JSON.parse(readFileSync(memoryKeyPath(), "utf8")); } catch { return null; }
|
|
151
|
+
}
|
|
152
|
+
function writeMemoryKey(k) {
|
|
153
|
+
if (!memoryKeyOn() || !k || typeof k !== "object") return;
|
|
154
|
+
try {
|
|
155
|
+
const dir = dirname(memoryKeyPath());
|
|
156
|
+
mkdirSync(dir, { recursive: true });
|
|
157
|
+
writeFileSync(memoryKeyPath(), JSON.stringify(k));
|
|
158
|
+
// The ledger is the source of truth; the key is a rebuildable cache and
|
|
159
|
+
// has no business in version control.
|
|
160
|
+
// Only in the default layout: a custom ledger directory is the user's to
|
|
161
|
+
// manage, and this client does not leave files in it uninvited.
|
|
162
|
+
const gi = join(dir, ".gitignore");
|
|
163
|
+
if (dir.endsWith(".memory-pulse") && !existsSync(gi)) writeFileSync(gi, "memory.rain\n");
|
|
164
|
+
} catch { /* a read-only checkout must not break a read call */ }
|
|
165
|
+
}
|
|
166
|
+
|
|
135
167
|
// ------------------------------------------------------------------- api ----
|
|
136
168
|
// Transport: Node's own http(s) on a FRESH HTTP/1.1 connection per call.
|
|
137
169
|
// The global fetch pools an HTTP/2 session that the edge retires after a
|
|
@@ -142,10 +174,14 @@ export function telemetryFooter(c) {
|
|
|
142
174
|
function postJson(url, headers, payload) {
|
|
143
175
|
const u = new URL(url);
|
|
144
176
|
const mod = u.protocol === "http:" ? http : https;
|
|
177
|
+
// Ledgers compress 5-10x (notes are prose); anything past 4 KB goes up
|
|
178
|
+
// gzipped. The engine inflates it; a small body is not worth the header.
|
|
179
|
+
const gz = Buffer.byteLength(payload) >= 4096;
|
|
180
|
+
const body = gz ? gzipSync(payload) : Buffer.from(payload);
|
|
145
181
|
return new Promise((resolve, reject) => {
|
|
146
182
|
const req = mod.request(u, {
|
|
147
183
|
method: "POST", agent: false,
|
|
148
|
-
headers: { ...headers, "content-length":
|
|
184
|
+
headers: { ...headers, ...(gz ? { "content-encoding": "gzip" } : {}), "content-length": body.length, connection: "close" },
|
|
149
185
|
}, (res) => {
|
|
150
186
|
let data = "";
|
|
151
187
|
res.setEncoding("utf8");
|
|
@@ -153,7 +189,7 @@ function postJson(url, headers, payload) {
|
|
|
153
189
|
res.on("end", () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, json: async () => JSON.parse(data) }));
|
|
154
190
|
});
|
|
155
191
|
req.on("error", reject);
|
|
156
|
-
req.end(
|
|
192
|
+
req.end(body);
|
|
157
193
|
});
|
|
158
194
|
}
|
|
159
195
|
|
|
@@ -171,7 +207,9 @@ async function postJsonRetry(url, headers, payload) {
|
|
|
171
207
|
|
|
172
208
|
async function callApi(route, body) {
|
|
173
209
|
const prior = readTelemetry();
|
|
174
|
-
|
|
210
|
+
const mk = readMemoryKey();
|
|
211
|
+
const wantKey = !mk && memoryKeyOn() && Array.isArray(body.events) && body.events.length >= 500;
|
|
212
|
+
body = { ...body, project: projectName(), ...(prior ? { telemetry: prior } : {}), ...(mk ? { key: mk } : wantKey ? { wantKey: true } : {}) };
|
|
175
213
|
let res;
|
|
176
214
|
try {
|
|
177
215
|
res = await postJsonRetry(`${API}${route}`, { "content-type": "application/json", ...(KEY ? { "x-mp-key": KEY } : {}) }, JSON.stringify(body));
|
|
@@ -184,6 +222,7 @@ async function callApi(route, body) {
|
|
|
184
222
|
}
|
|
185
223
|
const out = await res.json().catch(() => ({}));
|
|
186
224
|
if (res.ok && out.telemetry) { writeTelemetry(out.telemetry); }
|
|
225
|
+
if (res.ok && out.key) { writeMemoryKey(out.key); delete out.key; }
|
|
187
226
|
if (!res.ok) {
|
|
188
227
|
let msg = out.error ?? `API error ${res.status}`;
|
|
189
228
|
if (out.upgrade) msg += ` — upgrade: ${out.upgrade}`;
|
|
@@ -291,7 +330,7 @@ async function dispatch(msg) {
|
|
|
291
330
|
return ok(id, {
|
|
292
331
|
protocolVersion: SUPPORTED.includes(wanted) ? wanted : SUPPORTED[0],
|
|
293
332
|
capabilities: { tools: {} },
|
|
294
|
-
serverInfo: { name: "memory-pulse", version: "0.1
|
|
333
|
+
serverInfo: { name: "memory-pulse", version: "0.2.1" },
|
|
295
334
|
});
|
|
296
335
|
}
|
|
297
336
|
if (method === "notifications/initialized" || method === "initialized") return;
|