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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/package.json +1 -1
  3. 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.1.9",
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": Buffer.byteLength(payload), connection: "close" },
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(payload);
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
- body = { ...body, project: projectName(), ...(prior ? { telemetry: prior } : {}) };
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.1.9" },
328
+ serverInfo: { name: "memory-pulse", version: "0.2.0" },
295
329
  });
296
330
  }
297
331
  if (method === "notifications/initialized" || method === "initialized") return;