hippo-memory 1.37.0 → 1.38.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/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA2BH,OAAO,EASL,WAAW,EAEZ,MAAM,aAAa,CAAC;AA6GrB,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AA4ChC,kFAAkF;AAClF,wBAAgB,8BAA8B,IAAI,IAAI,CAErD;AA4ID,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,CAAC,CAAA;CAAE,CAwCjI;AA+tED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,MAAqB,GAAG,MAAM,CAyF3F;AAyGD;;;GAGG;AACH,oHAAoH;AACpH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAuD/D;AA2jGD,oHAAoH;AACpH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,EACtF,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,MAAkB,GAC1B,IAAI,CA6BN"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA2BH,OAAO,EASL,WAAW,EAEZ,MAAM,aAAa,CAAC;AA6GrB,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AA4ChC,kFAAkF;AAClF,wBAAgB,8BAA8B,IAAI,IAAI,CAErD;AA4ID,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,CAAC,CAAA;CAAE,CAwCjI;AA6xED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,MAAqB,GAAG,MAAM,CAyF3F;AAyGD;;;GAGG;AACH,oHAAoH;AACpH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAuD/D;AA2jGD,oHAAoH;AACpH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,EACtF,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,MAAkB,GAC1B,IAAI,CA6BN"}
package/dist/cli.js CHANGED
@@ -789,9 +789,21 @@ async function cmdRecall(hippoRoot, query, flags) {
789
789
  let globalEntries = isInitialized(globalRoot) ? loadRecallSearchEntries(globalRoot, query, undefined, tenantId, requestedScopeForFilter, 'additive') : [];
790
790
  // v1.12.13 / C5 — WYSIATI counters. Track filter activity per the plan v3
791
791
  // Task 3 mapping table. dropped_pre_rank is the SUM of all non-budget
792
- // filter drops (pre-rank AND post-rank). Search-engine internal drops
793
- // (scored-to-zero rows that hybridSearch/physicsSearch returns fewer of)
794
- // are NOT counted in v1 they are part of the rank step, not a filter.
792
+ // filter drops (pre-rank AND post-rank); its meaning is unchanged by C5.
793
+ //
794
+ // C5 (2026-08-24): search-engine internal drops (scored-to-zero rows that
795
+ // hybridSearch/physicsSearch returns fewer of than they were given) now
796
+ // count toward droppedByBudget, not "not counted at all" as the old v1
797
+ // convention had it. That old convention is exactly why the `Cutoff:` line
798
+ // never printed: cmdRecall measured droppedByBudget from `results.length -
799
+ // limit` (cli.ts ~1547) AFTER the search call, but `results` had already
800
+ // been ranked and truncated by the search engine to a handful of rows, so
801
+ // `limit < results.length` was almost always false and the counter stayed
802
+ // 0 while hundreds of candidates silently vanished (see the plan's measured
803
+ // table: 397 of 400 candidates gone, every counter reading 0). droppedByBudget
804
+ // is now derived as "everything not attributed to a named pre-rank filter",
805
+ // computed after the final `--limit` slice — see the definition near
806
+ // line ~1545 for the exact formula and the double-count argument.
795
807
  // totalCandidates = post-SQL-predicate count (api.recall parity: measured
796
808
  // after loadRecallSearchEntries, before the JS scope filter). NOTE the
797
809
  // v1.12.13 accounting convention: SQL-excluded rows (quarantine + the
@@ -800,6 +812,9 @@ async function cmdRecall(hippoRoot, query, flags) {
800
812
  // defense-in-depth (LIKE/regex divergence, exact-mode mismatch).
801
813
  const totalCandidatesCountCmd = localEntries.length + globalEntries.length;
802
814
  let droppedPreRankCountCmd = 0;
815
+ // Graph expansion adds candidates AFTER totalCandidatesCountCmd is taken,
816
+ // so they are folded back in before the budget residual is derived.
817
+ let graphAddedCountCmd = 0;
803
818
  // v1.25.0: JS half of the recall scope rule (private-scope regex deny with
804
819
  // explicit-request unlock), via the canonical helper — do not inline a
805
820
  // fourth copy of this predicate.
@@ -987,6 +1002,19 @@ async function cmdRecall(hippoRoot, query, flags) {
987
1002
  }
988
1003
  }
989
1004
  if (hops > 0) {
1005
+ // graphExpandRecall can SURFACE rows that were never in the lexical
1006
+ // candidate pool (a graph neighbour reached by entity edge, not by
1007
+ // query match), and totalCandidatesCountCmd was snapshotted before the
1008
+ // search. Without this the derived budget count goes negative, clamps
1009
+ // to 0, and the accounting silently breaks: 1 candidate, 2 returned,
1010
+ // 0 drops. Found independently by two reviewers. Count the additions
1011
+ // so the invariant holds on graph-expanded recalls too.
1012
+ // GROSS, not net. graphExpandRecall both adds neighbours AND evicts weak
1013
+ // base rows in one call (graph-recall.ts:285), so a net delta of 0 hides
1014
+ // 3 added + 3 evicted: the additions escape the candidate total and the
1015
+ // evictions escape the drop count, and the Cutoff line goes silent again.
1016
+ // Compare ID sets so both directions are counted.
1017
+ const beforeGraphIds = new Set(results.map((r) => r.entry.id));
990
1018
  results = graphExpandRecall(results, {
991
1019
  hops,
992
1020
  maxNeighbors,
@@ -1001,6 +1029,11 @@ async function cmdRecall(hippoRoot, query, flags) {
1001
1029
  ? { requested: recallExplicitScope, additive: true }
1002
1030
  : {},
1003
1031
  });
1032
+ // Rows the graph surfaced that the lexical pool never held.
1033
+ for (const r of results) {
1034
+ if (!beforeGraphIds.has(r.entry.id))
1035
+ graphAddedCountCmd++;
1036
+ }
1004
1037
  }
1005
1038
  }
1006
1039
  // ACC EVC-adaptive recall (RESEARCH.md §PFC.ACC). When the initial top-K is
@@ -1347,12 +1380,29 @@ async function cmdRecall(hippoRoot, query, flags) {
1347
1380
  results = results.filter((r) => r.entry.layer === layerFilter);
1348
1381
  droppedPreRankCountCmd += beforeLayerFilter - results.length;
1349
1382
  }
1350
- // v1.12.13 / C5 — WYSIATI dropped_by_budget counter (final limit cut).
1351
- let droppedByBudgetCountCmd = 0;
1383
+ // v1.12.13 / C5 — WYSIATI dropped_by_budget counter. Apply the final
1384
+ // `--limit` slice first, then derive the count ARITHMETICALLY as
1385
+ // "everything lost that droppedPreRank did not already claim":
1386
+ //
1387
+ // droppedByBudget = totalCandidates - droppedPreRank - returned
1388
+ //
1389
+ // This is the invariant the plan requires (totalCandidates == droppedPreRank
1390
+ // + droppedByBudget + returned) restated as an assignment, so it holds by
1391
+ // construction rather than by two counters happening to agree. It also
1392
+ // cannot double-count the post-search droppedPreRank sites (--filter-
1393
+ // conflicts, --outcome, --layer, ~1270/1528/1541): those are subtracted
1394
+ // once here, not re-counted, because this line does not re-walk any filter
1395
+ // — it only compares the two totals already tracked above. Everything left
1396
+ // over — search-engine internal rank-step drops AND the `--limit` slice
1397
+ // itself — lands in droppedByBudget, per the C5 accounting change in the
1398
+ // comment near line ~976. Clamped at 0 as a defensive floor: if a future
1399
+ // pipeline change ever returns MORE rows than totalCandidates minus
1400
+ // droppedPreRank (should not happen), report "nothing dropped" rather than
1401
+ // a negative count.
1352
1402
  if (limit < results.length) {
1353
- droppedByBudgetCountCmd = results.length - limit;
1354
1403
  results = results.slice(0, limit);
1355
1404
  }
1405
+ const droppedByBudgetCountCmd = Math.max(0, totalCandidatesCountCmd + graphAddedCountCmd - droppedPreRankCountCmd - results.length);
1356
1406
  // v0.33 / J1 — CLI per-pipeline anchoring detector. Each pipeline (api.recall,
1357
1407
  // cmdRecall, MCP) computes its own AnchoringHint via the shared detectAnchoring
1358
1408
  // helper against its own top-1 + its own per-(tenant, session) ring buffer.
@@ -1393,7 +1443,13 @@ async function cmdRecall(hippoRoot, query, flags) {
1393
1443
  // interference; query_repeat is a re-ask, not memory competition).
1394
1444
  const cmdSuppressedByInterference = cmdAnchoringHint?.reason === 'memory_dominance' ? 1 : 0;
1395
1445
  const cmdSuppressionSummary = api.buildSuppressionSummary({
1396
- totalCandidates: totalCandidatesCountCmd,
1446
+ // PUBLISHED total includes graph-surfaced rows. Folding graphAdded into
1447
+ // the derivation but not into the reported total made the invariant hold
1448
+ // internally and break externally by exactly that count: JSON consumers
1449
+ // saw total != preRank + byBudget + returned, and the text line could
1450
+ // read "showing 8 of 10" having actually considered 12. The number a
1451
+ // caller sees must be the number the arithmetic used.
1452
+ totalCandidates: totalCandidatesCountCmd + graphAddedCountCmd,
1397
1453
  droppedPreRank: droppedPreRankCountCmd,
1398
1454
  droppedByBudget: droppedByBudgetCountCmd,
1399
1455
  summarySubstitutionsAdded: 0,
@@ -1769,8 +1825,12 @@ async function cmdRecall(hippoRoot, query, flags) {
1769
1825
  if (showWhy) {
1770
1826
  const s = cmdSuppressionSummary;
1771
1827
  const clauses = [];
1828
+ // "dropped to fit limit" pointed at the wrong control: the residual covers
1829
+ // search-ranking and token-budget drops too, and fires even when --limit
1830
+ // was never passed. Measured: `recall --budget 20 --why` printed "39
1831
+ // dropped to fit limit" with no --limit flag in the command at all.
1772
1832
  if (s.droppedByBudget > 0)
1773
- clauses.push(`${s.droppedByBudget} dropped to fit limit`);
1833
+ clauses.push(`${s.droppedByBudget} not shown (rank, budget or limit)`);
1774
1834
  if (s.droppedPreRank > 0)
1775
1835
  clauses.push(`${s.droppedPreRank} filtered pre-rank`);
1776
1836
  if (s.summarySubstitutionsAdded > 0)