@eleboucher/pi-memini 0.7.7 → 0.7.8

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 +1 -1
  2. package/dist/index.js +129 -62
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -155,7 +155,7 @@ authority is restored.
155
155
  | `MEMINI_CAPTURE` | on | Settled-turn capture. |
156
156
  | `MEMINI_RECALL_LIMIT` | `3` | Maximum automatic recall hits. |
157
157
  | `MEMINI_INJECT_RECALL_MAX_TOK` | `250` | Recall injection token ceiling; `0` is unbounded. |
158
- | `MEMINI_INJECT_RECALL_MIN_SCORE` | `0` | Minimum automatic recall score. |
158
+ | `MEMINI_INJECT_RECALL_MIN_SCORE` | `0.5` | Minimum automatic recall score (composite post-rerank scale); `0` disables the floor. |
159
159
  | `MEMINI_INJECT_DEDUPE` | on | Shared cross-surface suppression state. |
160
160
  | `MEMINI_INJECT_COOLDOWN_MS` | `1800000` | Time cooldown for repeated injection; `0` disables this dimension. |
161
161
  | `MEMINI_INJECT_COOLDOWN_PROMPTS` | `3` | Prompt cooldown; `0` disables this dimension. Both cooldowns at `0` suppress for the session. |
package/dist/index.js CHANGED
@@ -2,7 +2,6 @@
2
2
  import { Text } from "@earendil-works/pi-tui";
3
3
  import { Type } from "typebox";
4
4
  import { readFileSync } from "node:fs";
5
- import { createHash } from "node:crypto";
6
5
 
7
6
  // ../../../packages/memini-client/src/redact.ts
8
7
  function redactValue(value) {
@@ -111,12 +110,16 @@ var BEHAVIOR_KNOBS = [
111
110
  { envName: "MEMINI_INJECT_BRIEFING_MAX_TOK", wireKey: "inject_briefing_max_tok", kind: "int", default: 600 },
112
111
  { envName: "MEMINI_INJECT_PRETOOL_ITEMS", wireKey: "inject_pretool_items", kind: "int", default: 3 },
113
112
  { envName: "MEMINI_INJECT_PRETOOL_MAX_TOK", wireKey: "inject_pretool_max_tok", kind: "int", default: 200 },
114
- { envName: "MEMINI_INJECT_PRETOOL_MIN_SCORE", wireKey: "inject_pretool_min_score", kind: "float", default: 0 },
113
+ { envName: "MEMINI_INJECT_PRETOOL_MIN_SCORE", wireKey: "inject_pretool_min_score", kind: "float", default: 0.5 },
114
+ // Glob and Grep are deliberately not in the default allowlist:
115
+ // pattern-derived queries ("Grep on <pattern>") are near-zero-signal and
116
+ // each ungated call costs a server embed+rerank — list them to restore the
117
+ // old behavior.
115
118
  {
116
119
  envName: "MEMINI_INJECT_PRETOOL_TOOLS",
117
120
  wireKey: "inject_pretool_tools",
118
121
  kind: "list",
119
- default: ["Read", "Write", "Edit", "MultiEdit", "Glob", "Grep"]
122
+ default: ["Read", "Write", "Edit", "MultiEdit"]
120
123
  },
121
124
  { envName: "MEMINI_INJECT_PRETOOL_GATE_MS", wireKey: "inject_pretool_gate_ms", kind: "int", default: 9e4 },
122
125
  { envName: "MEMINI_INJECT_DEDUPE", wireKey: "inject_dedupe", kind: "bool", default: true },
@@ -128,7 +131,7 @@ var BEHAVIOR_KNOBS = [
128
131
  { envName: "MEMINI_CAPTURE", wireKey: "capture", kind: "bool", default: true },
129
132
  { envName: "MEMINI_RECALL_LIMIT", wireKey: "recall_limit", kind: "int", default: 3 },
130
133
  { envName: "MEMINI_INJECT_RECALL_MAX_TOK", wireKey: "inject_recall_max_tok", kind: "int", default: 250 },
131
- { envName: "MEMINI_INJECT_RECALL_MIN_SCORE", wireKey: "inject_recall_min_score", kind: "float", default: 0 },
134
+ { envName: "MEMINI_INJECT_RECALL_MIN_SCORE", wireKey: "inject_recall_min_score", kind: "float", default: 0.5 },
132
135
  { envName: "MEMINI_MIN_CAPTURE_CHARS", wireKey: "min_capture_chars", kind: "int", default: 0 },
133
136
  { envName: "MEMINI_CAPTURE_USER_MAX_CHARS", wireKey: "capture_user_max_chars", kind: "int", default: 1e3 },
134
137
  {
@@ -322,6 +325,88 @@ async function performHandshake(boot, facts, opts = {}) {
322
325
  }
323
326
  }
324
327
 
328
+ // ../../../packages/memini-client/src/enforce/identity.ts
329
+ import crypto from "node:crypto";
330
+ function isContentHash(s) {
331
+ return typeof s === "string" && /^[0-9a-f]{16}$/.test(s);
332
+ }
333
+ function injectedIdentity(m) {
334
+ const ch = m?.content_hash ?? m?.memory?.content_hash;
335
+ if (isContentHash(ch)) return ch;
336
+ const text = m?.content || m?.summary || "";
337
+ return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
338
+ }
339
+
340
+ // ../../../packages/memini-client/src/enforce/seen.ts
341
+ function injectedSuppressed(entry, identity, { now, counter, cooldownMs, cooldownPrompts }) {
342
+ if (!entry || typeof entry !== "object") return false;
343
+ if (entry.h === "") return true;
344
+ if (identity && entry.h !== identity) return false;
345
+ if (cooldownMs === 0 && cooldownPrompts === 0) return true;
346
+ const promptDim = cooldownPrompts > 0 && counter > 0 && counter - entry.n < cooldownPrompts;
347
+ const timeDim = cooldownMs > 0 && now - entry.at < cooldownMs;
348
+ return promptDim || timeDim;
349
+ }
350
+
351
+ // ../../../packages/memini-client/src/enforce/budget.ts
352
+ function approxTokens(text) {
353
+ if (!text) return 0;
354
+ const words = String(text).trim().split(/\s+/).filter(Boolean).length;
355
+ return Math.max(1, Math.ceil(words * 4 / 3));
356
+ }
357
+ function fitByTokens(items, maxTokens) {
358
+ if (!Array.isArray(items) || items.length === 0) return { items: [], tokens: 0, dropped: 0 };
359
+ if (!Number.isFinite(maxTokens) || maxTokens <= 0) {
360
+ const tokens = items.reduce((sum, s) => sum + approxTokens(s), 0);
361
+ return { items: items.slice(), tokens, dropped: 0 };
362
+ }
363
+ const out = [];
364
+ let used = 0;
365
+ let dropped = 0;
366
+ for (const s of items) {
367
+ const t = approxTokens(s);
368
+ if (used + t > maxTokens) {
369
+ const charBudget = (maxTokens - used) * 4;
370
+ if (charBudget > 20) {
371
+ let cut = s.slice(0, charBudget);
372
+ const lastNL = cut.lastIndexOf("\n");
373
+ if (lastNL > 20) cut = cut.slice(0, lastNL);
374
+ if (cut.length > 20) {
375
+ out.push(cut + "\n[...truncated]");
376
+ used += approxTokens(cut);
377
+ continue;
378
+ }
379
+ }
380
+ dropped++;
381
+ continue;
382
+ }
383
+ out.push(s);
384
+ used += t;
385
+ }
386
+ return { items: out, tokens: used, dropped };
387
+ }
388
+
389
+ // ../../../packages/memini-client/src/enforce/render.ts
390
+ function escapeMeminiTags(content) {
391
+ if (typeof content !== "string") return content;
392
+ return content.replace(/<(\/?)memini/gi, "&lt;$1memini");
393
+ }
394
+ function truncate(value, max) {
395
+ if (typeof value === "string") {
396
+ return value.length > max ? value.slice(0, max) + "\n[...truncated]" : value;
397
+ }
398
+ if (value && typeof value === "object") {
399
+ let str;
400
+ try {
401
+ str = JSON.stringify(value);
402
+ } catch {
403
+ return value;
404
+ }
405
+ return str.length > max ? str.slice(0, max) + "...[truncated]" : str;
406
+ }
407
+ return value;
408
+ }
409
+
325
410
  // src/index.ts
326
411
  var DEFAULT_TIMEOUT_MS2 = 3e4;
327
412
  var DEFAULT_RECALL_LIMIT = 3;
@@ -460,45 +545,12 @@ async function sessionLive(ctx, env = process.env) {
460
545
  const hs = await ctx.memo.get();
461
546
  return resolveLiveConfig(ctx.boot, ctx.facts, hs, env);
462
547
  }
463
- function approxTokens(text) {
464
- if (!text) return 0;
465
- const words = String(text).trim().split(/\s+/).filter(Boolean).length;
466
- return Math.max(1, Math.ceil(words * 4 / 3));
467
- }
468
- function fitByTokens(items, maxTokens) {
469
- if (!Array.isArray(items) || items.length === 0) return { items: [], tokens: 0, dropped: 0 };
470
- if (!Number.isFinite(maxTokens) || maxTokens <= 0) {
471
- const tokens = items.reduce((sum, s) => sum + approxTokens(s), 0);
472
- return { items: items.slice(), tokens, dropped: 0 };
473
- }
474
- const out = [];
475
- let used = 0;
476
- let dropped = 0;
477
- for (const s of items) {
478
- const t = approxTokens(s);
479
- if (used + t > maxTokens) {
480
- dropped++;
481
- continue;
482
- }
483
- out.push(s);
484
- used += t;
485
- }
486
- return { items: out, tokens: used, dropped };
487
- }
488
- function truncate(value, max) {
489
- return value.length > max ? value.slice(0, max) + "\n[...truncated]" : value;
490
- }
491
- function escapeMeminiTags(value) {
492
- return String(value ?? "").replace(/<(\/?)memini/gi, (_match, slash) => `&lt;${slash}memini`);
493
- }
494
548
  function boundedInjectedText(value, max) {
495
- const escaped = escapeMeminiTags(value).replace(/\s+/g, " ").trim();
549
+ const escaped = escapeMeminiTags(String(value ?? "")).replace(/\s+/g, " ").trim();
496
550
  return unicodePrefix(escaped, max);
497
551
  }
498
- function injectedIdentity(raw) {
499
- const memory = raw?.memory ?? raw ?? {};
500
- const content = String(memory?.content || memory?.summary || "");
501
- return createHash("sha256").update(content).digest("hex").slice(0, 16);
552
+ function injectedIdentity2(raw) {
553
+ return injectedIdentity(raw?.memory ?? raw);
502
554
  }
503
555
  function formatResults(results, limit, labels) {
504
556
  if (!Array.isArray(results) || results.length === 0) return [];
@@ -1213,6 +1265,10 @@ function isExplicitExcludeIdsRejection(result) {
1213
1265
  if (result.status !== 400 || !result.error || !/exclude_ids/i.test(result.error)) return false;
1214
1266
  return /(unknown|unsupported|unrecognized|unexpected|not allowed|additional propert)/i.test(result.error);
1215
1267
  }
1268
+ function isExplicitMinRankScoreRejection(result) {
1269
+ if (result.status !== 400 || !result.error || !/min_rank_score/i.test(result.error)) return false;
1270
+ return /(unknown|unsupported|unrecognized|unexpected|not allowed|additional propert)/i.test(result.error);
1271
+ }
1216
1272
  function extractSettledTurn(entries) {
1217
1273
  if (!Array.isArray(entries)) return null;
1218
1274
  let userIndex = -1;
@@ -1412,7 +1468,7 @@ function meminiExtension(pi) {
1412
1468
  if (!id) continue;
1413
1469
  changed = true;
1414
1470
  injected.delete(id);
1415
- injected.set(id, { h: explicitRead ? "" : injectedIdentity(raw), at: now, n: promptCount });
1471
+ injected.set(id, { h: explicitRead ? "" : injectedIdentity2(raw), at: now, n: promptCount });
1416
1472
  }
1417
1473
  while (injected.size > MAX_INJECTED) injected.delete(injected.keys().next().value);
1418
1474
  if (changed) persistState();
@@ -1445,14 +1501,7 @@ function meminiExtension(pi) {
1445
1501
  });
1446
1502
  rememberInjected(eligible, transition.explicit);
1447
1503
  };
1448
- const suppressed = (entry, now, cooldownMs, cooldownPrompts, identity) => {
1449
- if (entry.h === "") return true;
1450
- if (identity && entry.h !== identity) return false;
1451
- if (cooldownMs === 0 && cooldownPrompts === 0) return true;
1452
- const promptDim = cooldownPrompts > 0 && promptCount > 0 && promptCount - entry.n < cooldownPrompts;
1453
- const timeDim = cooldownMs > 0 && now - entry.at < cooldownMs;
1454
- return promptDim || timeDim;
1455
- };
1504
+ const suppressed = (entry, now, cooldownMs, cooldownPrompts, identity) => injectedSuppressed(entry, identity ?? null, { now, counter: promptCount, cooldownMs, cooldownPrompts });
1456
1505
  const injectedInWindow = (live) => {
1457
1506
  const inWindow = /* @__PURE__ */ new Map();
1458
1507
  if (!live.inject_dedupe) return inWindow;
@@ -1582,18 +1631,27 @@ function meminiExtension(pi) {
1582
1631
  return null;
1583
1632
  };
1584
1633
  const searchExcluding = async (body, excludeIds, namespace) => {
1634
+ const rankFloorInBody = body.min_rank_score !== void 0;
1585
1635
  const capped = excludeIds.slice(0, MAX_SERVER_EXCLUDE_IDS);
1586
- if (!serverExcludeIds || capped.length === 0) return client.postJson("/v1/search", body, namespace);
1587
- const first = await client.postJsonResult("/v1/search", { ...body, exclude_ids: capped }, namespace);
1588
- if (first.ok) return first.data;
1589
- if (!isExplicitExcludeIdsRejection(first)) return searchFailure(first);
1590
- const retry = await client.postJsonResult("/v1/search", body, namespace);
1591
- if (retry.ok) {
1636
+ const withExcludeIds = serverExcludeIds && capped.length > 0;
1637
+ const first = await client.postJsonResult(
1638
+ "/v1/search",
1639
+ withExcludeIds ? { ...body, exclude_ids: capped } : body,
1640
+ namespace
1641
+ );
1642
+ if (first.ok) return { data: first.data, rankFloorStripped: false };
1643
+ const excludeIdsRejected = withExcludeIds && isExplicitExcludeIdsRejection(first);
1644
+ const rankFloorRejected = rankFloorInBody && isExplicitMinRankScoreRejection(first);
1645
+ if (!excludeIdsRejected && !rankFloorRejected) return { data: searchFailure(first), rankFloorStripped: false };
1646
+ if (excludeIdsRejected) {
1592
1647
  serverExcludeIds = false;
1593
1648
  warn("memini: server does not accept exclude_ids; using client-side dedupe only");
1594
- return retry.data;
1595
1649
  }
1596
- return searchFailure(retry);
1650
+ const stripped = { ...body };
1651
+ delete stripped.min_rank_score;
1652
+ const retry = await client.postJsonResult("/v1/search", stripped, namespace);
1653
+ if (retry.ok) return { data: retry.data, rankFloorStripped: rankFloorInBody };
1654
+ return { data: searchFailure(retry), rankFloorStripped: false };
1597
1655
  };
1598
1656
  pi.on("before_agent_start", async (event, ctx) => {
1599
1657
  const sid = sessionIdOf(ctx);
@@ -1612,12 +1670,16 @@ function meminiExtension(pi) {
1612
1670
  limit: live.recall_limit
1613
1671
  };
1614
1672
  if (live.inject_dedupe && sid) body.exclude_metadata = { session_id: sid };
1615
- if (live.recall_min_score > 0) body.min_score = live.recall_min_score;
1673
+ const rankFloor = live.recall_min_score;
1674
+ const rankFloorInRange = rankFloor > 0 && rankFloor < 1;
1675
+ if (rankFloorInRange) body.min_rank_score = rankFloor;
1616
1676
  const inWindow = injectedInWindow(live);
1617
1677
  const readVersion = mutationClock;
1618
1678
  const readEpoch = stateEpoch;
1619
- const result = await searchExcluding(body, live.inject_dedupe ? [...inWindow.keys()] : [], live.namespace);
1620
- const floor = live.recall_min_score > 0 ? live.recall_min_score : 0;
1679
+ const searchResult = await searchExcluding(body, live.inject_dedupe ? [...inWindow.keys()] : [], live.namespace);
1680
+ const result = searchResult.data;
1681
+ const serverEnforcedFloor = rankFloorInRange && !searchResult.rankFloorStripped;
1682
+ const floor = rankFloor > 0 && !serverEnforcedFloor ? rankFloor : 0;
1621
1683
  let rawHits = Array.isArray(result?.results) ? result.results : [];
1622
1684
  if (live.inject_dedupe && inWindow.size) {
1623
1685
  rawHits = rawHits.filter((raw) => {
@@ -1628,7 +1690,7 @@ function meminiExtension(pi) {
1628
1690
  Date.now(),
1629
1691
  live.inject_cooldown_ms,
1630
1692
  live.inject_cooldown_prompts,
1631
- injectedIdentity(raw)
1693
+ injectedIdentity2(raw)
1632
1694
  );
1633
1695
  });
1634
1696
  }
@@ -1729,6 +1791,9 @@ function meminiExtension(pi) {
1729
1791
  maxItems: MAX_SERVER_EXCLUDE_IDS,
1730
1792
  description: "Drop these memory ids before ranking and limit."
1731
1793
  })),
1794
+ min_rank_score: Type.Optional(Type.Number({
1795
+ description: "drop results whose final ranked score is below this ([0,1)); rarely needed \u2014 the server already gates relevance"
1796
+ })),
1732
1797
  include_fresh_turns: Type.Optional(Type.Boolean({
1733
1798
  description: "Include just-captured turns normally hidden by the temporal echo guard."
1734
1799
  })),
@@ -1757,6 +1822,7 @@ function meminiExtension(pi) {
1757
1822
  "metadata",
1758
1823
  "exclude_metadata",
1759
1824
  "exclude_ids",
1825
+ "min_rank_score",
1760
1826
  "include_fresh_turns",
1761
1827
  "query_rewrite",
1762
1828
  "as_of"
@@ -2123,9 +2189,10 @@ export {
2123
2189
  fitByTokens,
2124
2190
  floatEnv,
2125
2191
  formatResults,
2126
- injectedIdentity,
2192
+ injectedIdentity2 as injectedIdentity,
2127
2193
  intEnv,
2128
2194
  isExplicitExcludeIdsRejection,
2195
+ isExplicitMinRankScoreRejection,
2129
2196
  meminiListPath,
2130
2197
  memoizeAsync,
2131
2198
  memoryResultDetails,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eleboucher/pi-memini",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "Shared cross-session memory for the Pi coding agent, backed by a memini service.",
5
5
  "keywords": [
6
6
  "memini",