@eleboucher/pi-memini 0.7.5 → 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 +3 -3
  2. package/dist/index.js +139 -66
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -154,8 +154,8 @@ authority is restored.
154
154
  | `MEMINI_RECALL` | on | Automatic prompt recall. |
155
155
  | `MEMINI_CAPTURE` | on | Settled-turn capture. |
156
156
  | `MEMINI_RECALL_LIMIT` | `3` | Maximum automatic recall hits. |
157
- | `MEMINI_INJECT_RECALL_MAX_TOK` | `0` | Recall injection token ceiling; `0` is unbounded. |
158
- | `MEMINI_INJECT_RECALL_MIN_SCORE` | `0` | Minimum automatic recall score. |
157
+ | `MEMINI_INJECT_RECALL_MAX_TOK` | `250` | Recall injection token ceiling; `0` is unbounded. |
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. |
@@ -164,7 +164,7 @@ authority is restored.
164
164
  | `MEMINI_INJECT_BRIEFING_FACTS` | `5` | Startup durable-fact cap. |
165
165
  | `MEMINI_INJECT_BRIEFING_PROCEDURES` | `5` | Startup procedure cap. |
166
166
  | `MEMINI_INJECT_BRIEFING_RECENT` | `3` | Startup recent-memory cap. |
167
- | `MEMINI_INJECT_BRIEFING_MAX_TOK` | `0` | Whole briefing token ceiling; `0` is unbounded. |
167
+ | `MEMINI_INJECT_BRIEFING_MAX_TOK` | `600` | Whole briefing token ceiling; `0` is unbounded. |
168
168
  | `MEMINI_SESSION_DIGEST` | on | Pre-compaction and shutdown activity checkpoints. |
169
169
  | `MEMINI_MIN_CAPTURE_CHARS` | `0` | Minimum settled user-text length required for capture. |
170
170
  | `MEMINI_CAPTURE_USER_MAX_CHARS` | `1000` | Per-turn captured user-text cap; `0` is unbounded. |
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) {
@@ -108,26 +107,31 @@ var BEHAVIOR_KNOBS = [
108
107
  { envName: "MEMINI_INJECT_BRIEFING_FACTS", wireKey: "inject_briefing_facts", kind: "int", default: 5 },
109
108
  { envName: "MEMINI_INJECT_BRIEFING_PROCEDURES", wireKey: "inject_briefing_procedures", kind: "int", default: 5 },
110
109
  { envName: "MEMINI_INJECT_BRIEFING_RECENT", wireKey: "inject_briefing_recent", kind: "int", default: 3 },
111
- { envName: "MEMINI_INJECT_BRIEFING_MAX_TOK", wireKey: "inject_briefing_max_tok", kind: "int", default: 0 },
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
- { envName: "MEMINI_INJECT_PRETOOL_MAX_TOK", wireKey: "inject_pretool_max_tok", kind: "int", default: 0 },
114
- { envName: "MEMINI_INJECT_PRETOOL_MIN_SCORE", wireKey: "inject_pretool_min_score", kind: "float", default: 0 },
112
+ { envName: "MEMINI_INJECT_PRETOOL_MAX_TOK", wireKey: "inject_pretool_max_tok", kind: "int", default: 200 },
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 },
126
+ { envName: "MEMINI_INJECT_TELEMETRY", wireKey: "inject_telemetry", kind: "bool", default: true },
123
127
  { envName: "MEMINI_INJECT_COOLDOWN_MS", wireKey: "inject_cooldown_ms", kind: "int", default: 18e5 },
124
128
  { envName: "MEMINI_INJECT_COOLDOWN_PROMPTS", wireKey: "inject_cooldown_prompts", kind: "int", default: 3 },
125
129
  { envName: "MEMINI_INJECT_LABELS", wireKey: "inject_labels", kind: "list", default: [] },
126
130
  { envName: "MEMINI_RECALL", wireKey: "recall", kind: "bool", default: true },
127
131
  { envName: "MEMINI_CAPTURE", wireKey: "capture", kind: "bool", default: true },
128
132
  { envName: "MEMINI_RECALL_LIMIT", wireKey: "recall_limit", kind: "int", default: 3 },
129
- { envName: "MEMINI_INJECT_RECALL_MAX_TOK", wireKey: "inject_recall_max_tok", kind: "int", default: 0 },
130
- { envName: "MEMINI_INJECT_RECALL_MIN_SCORE", wireKey: "inject_recall_min_score", kind: "float", default: 0 },
133
+ { envName: "MEMINI_INJECT_RECALL_MAX_TOK", wireKey: "inject_recall_max_tok", kind: "int", default: 250 },
134
+ { envName: "MEMINI_INJECT_RECALL_MIN_SCORE", wireKey: "inject_recall_min_score", kind: "float", default: 0.5 },
131
135
  { envName: "MEMINI_MIN_CAPTURE_CHARS", wireKey: "min_capture_chars", kind: "int", default: 0 },
132
136
  { envName: "MEMINI_CAPTURE_USER_MAX_CHARS", wireKey: "capture_user_max_chars", kind: "int", default: 1e3 },
133
137
  {
@@ -321,6 +325,88 @@ async function performHandshake(boot, facts, opts = {}) {
321
325
  }
322
326
  }
323
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
+
324
410
  // src/index.ts
325
411
  var DEFAULT_TIMEOUT_MS2 = 3e4;
326
412
  var DEFAULT_RECALL_LIMIT = 3;
@@ -459,45 +545,12 @@ async function sessionLive(ctx, env = process.env) {
459
545
  const hs = await ctx.memo.get();
460
546
  return resolveLiveConfig(ctx.boot, ctx.facts, hs, env);
461
547
  }
462
- function approxTokens(text) {
463
- if (!text) return 0;
464
- const words = String(text).trim().split(/\s+/).filter(Boolean).length;
465
- return Math.max(1, Math.ceil(words * 4 / 3));
466
- }
467
- function fitByTokens(items, maxTokens) {
468
- if (!Array.isArray(items) || items.length === 0) return { items: [], tokens: 0, dropped: 0 };
469
- if (!Number.isFinite(maxTokens) || maxTokens <= 0) {
470
- const tokens = items.reduce((sum, s) => sum + approxTokens(s), 0);
471
- return { items: items.slice(), tokens, dropped: 0 };
472
- }
473
- const out = [];
474
- let used = 0;
475
- let dropped = 0;
476
- for (const s of items) {
477
- const t = approxTokens(s);
478
- if (used + t > maxTokens) {
479
- dropped++;
480
- continue;
481
- }
482
- out.push(s);
483
- used += t;
484
- }
485
- return { items: out, tokens: used, dropped };
486
- }
487
- function truncate(value, max) {
488
- return value.length > max ? value.slice(0, max) + "\n[...truncated]" : value;
489
- }
490
- function escapeMeminiTags(value) {
491
- return String(value ?? "").replace(/<(\/?)memini/gi, (_match, slash) => `&lt;${slash}memini`);
492
- }
493
548
  function boundedInjectedText(value, max) {
494
- const escaped = escapeMeminiTags(value).replace(/\s+/g, " ").trim();
549
+ const escaped = escapeMeminiTags(String(value ?? "")).replace(/\s+/g, " ").trim();
495
550
  return unicodePrefix(escaped, max);
496
551
  }
497
- function injectedIdentity(raw) {
498
- const memory = raw?.memory ?? raw ?? {};
499
- const content = String(memory?.content || memory?.summary || "");
500
- return createHash("sha256").update(content).digest("hex").slice(0, 16);
552
+ function injectedIdentity2(raw) {
553
+ return injectedIdentity(raw?.memory ?? raw);
501
554
  }
502
555
  function formatResults(results, limit, labels) {
503
556
  if (!Array.isArray(results) || results.length === 0) return [];
@@ -640,7 +693,12 @@ var MEMORY_FIELDS = [
640
693
  "superseded_by",
641
694
  "valid_from",
642
695
  "valid_to",
643
- "confidence"
696
+ "confidence",
697
+ // Progressive-disclosure fields: content_hash (16-hex identity over the
698
+ // full stored content, stable across response formats) and
699
+ // content_truncated (set on concise projections that actually cut).
700
+ "content_hash",
701
+ "content_truncated"
644
702
  ];
645
703
  function normalizeMemory(raw) {
646
704
  const memory = raw?.memory ?? raw ?? {};
@@ -1207,6 +1265,10 @@ function isExplicitExcludeIdsRejection(result) {
1207
1265
  if (result.status !== 400 || !result.error || !/exclude_ids/i.test(result.error)) return false;
1208
1266
  return /(unknown|unsupported|unrecognized|unexpected|not allowed|additional propert)/i.test(result.error);
1209
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
+ }
1210
1272
  function extractSettledTurn(entries) {
1211
1273
  if (!Array.isArray(entries)) return null;
1212
1274
  let userIndex = -1;
@@ -1406,7 +1468,7 @@ function meminiExtension(pi) {
1406
1468
  if (!id) continue;
1407
1469
  changed = true;
1408
1470
  injected.delete(id);
1409
- injected.set(id, { h: explicitRead ? "" : injectedIdentity(raw), at: now, n: promptCount });
1471
+ injected.set(id, { h: explicitRead ? "" : injectedIdentity2(raw), at: now, n: promptCount });
1410
1472
  }
1411
1473
  while (injected.size > MAX_INJECTED) injected.delete(injected.keys().next().value);
1412
1474
  if (changed) persistState();
@@ -1439,14 +1501,7 @@ function meminiExtension(pi) {
1439
1501
  });
1440
1502
  rememberInjected(eligible, transition.explicit);
1441
1503
  };
1442
- const suppressed = (entry, now, cooldownMs, cooldownPrompts, identity) => {
1443
- if (entry.h === "") return true;
1444
- if (identity && entry.h !== identity) return false;
1445
- if (cooldownMs === 0 && cooldownPrompts === 0) return true;
1446
- const promptDim = cooldownPrompts > 0 && promptCount > 0 && promptCount - entry.n < cooldownPrompts;
1447
- const timeDim = cooldownMs > 0 && now - entry.at < cooldownMs;
1448
- return promptDim || timeDim;
1449
- };
1504
+ const suppressed = (entry, now, cooldownMs, cooldownPrompts, identity) => injectedSuppressed(entry, identity ?? null, { now, counter: promptCount, cooldownMs, cooldownPrompts });
1450
1505
  const injectedInWindow = (live) => {
1451
1506
  const inWindow = /* @__PURE__ */ new Map();
1452
1507
  if (!live.inject_dedupe) return inWindow;
@@ -1576,18 +1631,27 @@ function meminiExtension(pi) {
1576
1631
  return null;
1577
1632
  };
1578
1633
  const searchExcluding = async (body, excludeIds, namespace) => {
1634
+ const rankFloorInBody = body.min_rank_score !== void 0;
1579
1635
  const capped = excludeIds.slice(0, MAX_SERVER_EXCLUDE_IDS);
1580
- if (!serverExcludeIds || capped.length === 0) return client.postJson("/v1/search", body, namespace);
1581
- const first = await client.postJsonResult("/v1/search", { ...body, exclude_ids: capped }, namespace);
1582
- if (first.ok) return first.data;
1583
- if (!isExplicitExcludeIdsRejection(first)) return searchFailure(first);
1584
- const retry = await client.postJsonResult("/v1/search", body, namespace);
1585
- 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) {
1586
1647
  serverExcludeIds = false;
1587
1648
  warn("memini: server does not accept exclude_ids; using client-side dedupe only");
1588
- return retry.data;
1589
1649
  }
1590
- 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 };
1591
1655
  };
1592
1656
  pi.on("before_agent_start", async (event, ctx) => {
1593
1657
  const sid = sessionIdOf(ctx);
@@ -1606,12 +1670,16 @@ function meminiExtension(pi) {
1606
1670
  limit: live.recall_limit
1607
1671
  };
1608
1672
  if (live.inject_dedupe && sid) body.exclude_metadata = { session_id: sid };
1609
- 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;
1610
1676
  const inWindow = injectedInWindow(live);
1611
1677
  const readVersion = mutationClock;
1612
1678
  const readEpoch = stateEpoch;
1613
- const result = await searchExcluding(body, live.inject_dedupe ? [...inWindow.keys()] : [], live.namespace);
1614
- 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;
1615
1683
  let rawHits = Array.isArray(result?.results) ? result.results : [];
1616
1684
  if (live.inject_dedupe && inWindow.size) {
1617
1685
  rawHits = rawHits.filter((raw) => {
@@ -1622,7 +1690,7 @@ function meminiExtension(pi) {
1622
1690
  Date.now(),
1623
1691
  live.inject_cooldown_ms,
1624
1692
  live.inject_cooldown_prompts,
1625
- injectedIdentity(raw)
1693
+ injectedIdentity2(raw)
1626
1694
  );
1627
1695
  });
1628
1696
  }
@@ -1723,6 +1791,9 @@ function meminiExtension(pi) {
1723
1791
  maxItems: MAX_SERVER_EXCLUDE_IDS,
1724
1792
  description: "Drop these memory ids before ranking and limit."
1725
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
+ })),
1726
1797
  include_fresh_turns: Type.Optional(Type.Boolean({
1727
1798
  description: "Include just-captured turns normally hidden by the temporal echo guard."
1728
1799
  })),
@@ -1751,6 +1822,7 @@ function meminiExtension(pi) {
1751
1822
  "metadata",
1752
1823
  "exclude_metadata",
1753
1824
  "exclude_ids",
1825
+ "min_rank_score",
1754
1826
  "include_fresh_turns",
1755
1827
  "query_rewrite",
1756
1828
  "as_of"
@@ -2117,9 +2189,10 @@ export {
2117
2189
  fitByTokens,
2118
2190
  floatEnv,
2119
2191
  formatResults,
2120
- injectedIdentity,
2192
+ injectedIdentity2 as injectedIdentity,
2121
2193
  intEnv,
2122
2194
  isExplicitExcludeIdsRejection,
2195
+ isExplicitMinRankScoreRejection,
2123
2196
  meminiListPath,
2124
2197
  memoizeAsync,
2125
2198
  memoryResultDetails,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eleboucher/pi-memini",
3
- "version": "0.7.5",
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",