memory-pulse 0.1.9 → 0.2.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 +7 -0
- package/package.json +1 -1
- package/server.mjs +38 -4
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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";
|
|
@@ -132,6 +133,32 @@ export function telemetryFooter(c) {
|
|
|
132
133
|
return `— memory-pulse · ${k.pulse} re-entries · ${k.correctionsSurfaced} corrections surfaced · ~${fmtK(k.tokensSavedEst)} tokens saved (est., signed)${drift}`;
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
// ------------------------------------------------------------ memory key ----
|
|
137
|
+
// State persistence without a database. After a read the engine hands back a
|
|
138
|
+
// signed memory key; presenting it on the next read resumes the memory and
|
|
139
|
+
// ingests only the events recorded since (measured: a full re-entry on an
|
|
140
|
+
// 825-event ledger went from 4.5 s to 1.5 s). It lives beside your ledger as
|
|
141
|
+
// memory.rain, it is yours, and a lost or stale key costs one rebuild — never
|
|
142
|
+
// data. It never enters the agent's context. MEMORY_PULSE_MEMORY_KEY=off disables it.
|
|
143
|
+
const memoryKeyPath = () => join(dirname(ledgerPath()), "memory.rain");
|
|
144
|
+
const memoryKeyOn = () => (process.env.MEMORY_PULSE_MEMORY_KEY || "on") !== "off";
|
|
145
|
+
function readMemoryKey() {
|
|
146
|
+
if (!memoryKeyOn()) return null;
|
|
147
|
+
try { return JSON.parse(readFileSync(memoryKeyPath(), "utf8")); } catch { return null; }
|
|
148
|
+
}
|
|
149
|
+
function writeMemoryKey(k) {
|
|
150
|
+
if (!memoryKeyOn() || !k || typeof k !== "object") return;
|
|
151
|
+
try {
|
|
152
|
+
const dir = dirname(memoryKeyPath());
|
|
153
|
+
mkdirSync(dir, { recursive: true });
|
|
154
|
+
writeFileSync(memoryKeyPath(), JSON.stringify(k));
|
|
155
|
+
// The ledger is the source of truth; the key is a rebuildable cache and
|
|
156
|
+
// has no business in version control.
|
|
157
|
+
const gi = join(dir, ".gitignore");
|
|
158
|
+
if (!existsSync(gi)) writeFileSync(gi, "memory.rain\n");
|
|
159
|
+
} catch { /* a read-only checkout must not break a read call */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
135
162
|
// ------------------------------------------------------------------- api ----
|
|
136
163
|
// Transport: Node's own http(s) on a FRESH HTTP/1.1 connection per call.
|
|
137
164
|
// The global fetch pools an HTTP/2 session that the edge retires after a
|
|
@@ -142,10 +169,14 @@ export function telemetryFooter(c) {
|
|
|
142
169
|
function postJson(url, headers, payload) {
|
|
143
170
|
const u = new URL(url);
|
|
144
171
|
const mod = u.protocol === "http:" ? http : https;
|
|
172
|
+
// Ledgers compress 5-10x (notes are prose); anything past 4 KB goes up
|
|
173
|
+
// gzipped. The engine inflates it; a small body is not worth the header.
|
|
174
|
+
const gz = Buffer.byteLength(payload) >= 4096;
|
|
175
|
+
const body = gz ? gzipSync(payload) : Buffer.from(payload);
|
|
145
176
|
return new Promise((resolve, reject) => {
|
|
146
177
|
const req = mod.request(u, {
|
|
147
178
|
method: "POST", agent: false,
|
|
148
|
-
headers: { ...headers, "content-length":
|
|
179
|
+
headers: { ...headers, ...(gz ? { "content-encoding": "gzip" } : {}), "content-length": body.length, connection: "close" },
|
|
149
180
|
}, (res) => {
|
|
150
181
|
let data = "";
|
|
151
182
|
res.setEncoding("utf8");
|
|
@@ -153,7 +184,7 @@ function postJson(url, headers, payload) {
|
|
|
153
184
|
res.on("end", () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, json: async () => JSON.parse(data) }));
|
|
154
185
|
});
|
|
155
186
|
req.on("error", reject);
|
|
156
|
-
req.end(
|
|
187
|
+
req.end(body);
|
|
157
188
|
});
|
|
158
189
|
}
|
|
159
190
|
|
|
@@ -171,7 +202,9 @@ async function postJsonRetry(url, headers, payload) {
|
|
|
171
202
|
|
|
172
203
|
async function callApi(route, body) {
|
|
173
204
|
const prior = readTelemetry();
|
|
174
|
-
|
|
205
|
+
const mk = readMemoryKey();
|
|
206
|
+
const wantKey = !mk && memoryKeyOn() && Array.isArray(body.events) && body.events.length >= 500;
|
|
207
|
+
body = { ...body, project: projectName(), ...(prior ? { telemetry: prior } : {}), ...(mk ? { key: mk } : wantKey ? { wantKey: true } : {}) };
|
|
175
208
|
let res;
|
|
176
209
|
try {
|
|
177
210
|
res = await postJsonRetry(`${API}${route}`, { "content-type": "application/json", ...(KEY ? { "x-mp-key": KEY } : {}) }, JSON.stringify(body));
|
|
@@ -184,6 +217,7 @@ async function callApi(route, body) {
|
|
|
184
217
|
}
|
|
185
218
|
const out = await res.json().catch(() => ({}));
|
|
186
219
|
if (res.ok && out.telemetry) { writeTelemetry(out.telemetry); }
|
|
220
|
+
if (res.ok && out.key) { writeMemoryKey(out.key); delete out.key; }
|
|
187
221
|
if (!res.ok) {
|
|
188
222
|
let msg = out.error ?? `API error ${res.status}`;
|
|
189
223
|
if (out.upgrade) msg += ` — upgrade: ${out.upgrade}`;
|
|
@@ -291,7 +325,7 @@ async function dispatch(msg) {
|
|
|
291
325
|
return ok(id, {
|
|
292
326
|
protocolVersion: SUPPORTED.includes(wanted) ? wanted : SUPPORTED[0],
|
|
293
327
|
capabilities: { tools: {} },
|
|
294
|
-
serverInfo: { name: "memory-pulse", version: "0.
|
|
328
|
+
serverInfo: { name: "memory-pulse", version: "0.2.0" },
|
|
295
329
|
});
|
|
296
330
|
}
|
|
297
331
|
if (method === "notifications/initialized" || method === "initialized") return;
|