@eleboucher/pi-memini 0.5.7 → 0.5.9

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 (2) hide show
  1. package/dist/index.js +38 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -229,8 +229,12 @@ function createClient(cfg, warn) {
229
229
  signal: AbortSignal.timeout(cfg.timeout_ms),
230
230
  });
231
231
  if (!res.ok) {
232
- if (cfg.fallback_on_error)
232
+ if (cfg.fallback_on_error) {
233
+ // Degrade but never silently: a swallowed 401/500 on a capture or
234
+ // recall looks like "memory isn't working" with nothing to debug.
235
+ warn(`memini ${method} ${path} failed: ${res.status}`);
233
236
  return null;
237
+ }
234
238
  const text = await res.text().catch(() => "");
235
239
  throw new Error(`memini ${method} ${path} failed: ${res.status} ${text}`);
236
240
  }
@@ -348,6 +352,28 @@ export default function meminiExtension(pi) {
348
352
  // Assistant message ids already captured, so a re-fired agent_end never writes
349
353
  // a duplicate turn.
350
354
  const captured = new Set();
355
+ // Memory ids each session has already been shown (mirrors the openclaw
356
+ // plugin): the recall injection is a persistent context message, so
357
+ // re-injecting an unchanged match every turn stacks identical blocks in the
358
+ // prompt. Bounded so long-lived hosts can't grow the map without limit.
359
+ const injectedBySession = new Map();
360
+ const MAX_TRACKED_SESSIONS = 200;
361
+ const rememberInjected = (session, ids) => {
362
+ let seen = injectedBySession.get(session);
363
+ if (!seen) {
364
+ seen = new Set();
365
+ injectedBySession.set(session, seen);
366
+ while (injectedBySession.size > MAX_TRACKED_SESSIONS) {
367
+ const oldest = injectedBySession.keys().next().value;
368
+ if (oldest === undefined)
369
+ break;
370
+ injectedBySession.delete(oldest);
371
+ }
372
+ }
373
+ for (const id of ids)
374
+ if (id)
375
+ seen.add(id);
376
+ };
351
377
  // Recall before the turn: search for the user's prompt and inject the matches
352
378
  // as a persistent context message. Buffer the prompt for capture at agent_end.
353
379
  pi.on("before_agent_start", async (event, ctx) => {
@@ -366,7 +392,14 @@ export default function meminiExtension(pi) {
366
392
  body.min_score = cfg.recall_min_score;
367
393
  const result = await client.postJson("/v1/search", body);
368
394
  const floor = cfg.recall_min_score > 0 ? cfg.recall_min_score : 0;
369
- const rawHits = Array.isArray(result?.results) ? result.results : [];
395
+ let rawHits = Array.isArray(result?.results) ? result.results : [];
396
+ // Suppress memories this session has already been shown — the injected
397
+ // message persists in context, so a repeat adds nothing but noise.
398
+ if (sid) {
399
+ const seen = injectedBySession.get(sid);
400
+ if (seen?.size)
401
+ rawHits = rawHits.filter((r) => !seen.has(r?.memory?.id));
402
+ }
370
403
  const filtered = floor > 0
371
404
  ? rawHits.filter((r) => (typeof r?.score === "number" ? r.score : 0) >= floor)
372
405
  : rawHits;
@@ -376,6 +409,9 @@ export default function meminiExtension(pi) {
376
409
  const fit = fitByTokens(hits, cfg.recall_max_tokens);
377
410
  if (fit.items.length === 0)
378
411
  return;
412
+ if (sid) {
413
+ rememberInjected(sid, filtered.map((r) => r?.memory?.id).filter(Boolean));
414
+ }
379
415
  const lines = [
380
416
  "Relevant long-term memory from memini (background context — prefer " +
381
417
  "current workspace state and the user's instructions):",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eleboucher/pi-memini",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "Shared cross-session memory for the Pi coding agent, backed by a memini service.",
5
5
  "files": [
6
6
  "dist"