hippo-memory 1.13.1 → 1.13.3

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 (43) hide show
  1. package/dist/api.d.ts +48 -6
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +53 -1
  4. package/dist/api.js.map +1 -1
  5. package/dist/audit.d.ts +1 -1
  6. package/dist/audit.d.ts.map +1 -1
  7. package/dist/audit.js.map +1 -1
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +119 -25
  11. package/dist/cli.js.map +1 -1
  12. package/dist/client.d.ts.map +1 -1
  13. package/dist/client.js +12 -0
  14. package/dist/client.js.map +1 -1
  15. package/dist/mcp/server.d.ts +2 -0
  16. package/dist/mcp/server.d.ts.map +1 -1
  17. package/dist/mcp/server.js +140 -37
  18. package/dist/mcp/server.js.map +1 -1
  19. package/dist/recall-history.d.ts +127 -0
  20. package/dist/recall-history.d.ts.map +1 -0
  21. package/dist/recall-history.js +235 -0
  22. package/dist/recall-history.js.map +1 -0
  23. package/dist/server.d.ts +2 -0
  24. package/dist/server.d.ts.map +1 -1
  25. package/dist/server.js +78 -0
  26. package/dist/server.js.map +1 -1
  27. package/dist/src/api.js +53 -1
  28. package/dist/src/api.js.map +1 -1
  29. package/dist/src/audit.js.map +1 -1
  30. package/dist/src/cli.js +119 -25
  31. package/dist/src/cli.js.map +1 -1
  32. package/dist/src/client.js +12 -0
  33. package/dist/src/client.js.map +1 -1
  34. package/dist/src/mcp/server.js +140 -37
  35. package/dist/src/mcp/server.js.map +1 -1
  36. package/dist/src/recall-history.js +235 -0
  37. package/dist/src/recall-history.js.map +1 -0
  38. package/dist/src/server.js +78 -0
  39. package/dist/src/server.js.map +1 -1
  40. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  41. package/extensions/openclaw-plugin/package.json +1 -1
  42. package/openclaw.plugin.json +1 -1
  43. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -58,6 +58,30 @@ import { buildCorrectionLatency } from './correction-latency.js';
58
58
  import * as api from './api.js';
59
59
  import * as predictionsModule from './predictions.js';
60
60
  import { computePlanningFallacyHint } from './predictions.js';
61
+ import { createHash } from 'node:crypto';
62
+ import { detectAnchoring, hashQueryText, buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, } from './recall-history.js';
63
+ // v0.33 / J1 — Module-level per-(tenant, session) recall-history ring map.
64
+ // Each CLI process maintains its OWN Map; no IPC / no cross-process sharing
65
+ // (plan v3 decision: per-pipeline rings, see docs/plans/2026-05-26-j1-anchoring-detector.md).
66
+ //
67
+ // IMPORTANT single-shot CLI limitation (codex round-2 catch): in normal
68
+ // terminal usage each `hippo recall` invocation spawns a fresh Node
69
+ // process, so this Map is recreated empty every time and J1 cannot
70
+ // accumulate history across invocations. CLI J1 only fires in
71
+ // long-running processes (the cmdSleep / consolidate loops, batch
72
+ // scripts that call cmdRecall in-process, or tests). MCP and HTTP
73
+ // pipelines DO accumulate because their host processes are long-lived
74
+ // (hippo serve, MCP server). For CLI users who want per-session
75
+ // anchoring in single-shot mode, the recommendation is to run via
76
+ // `hippo serve` and call HTTP /v1/memories?session_id=... (the HTTP
77
+ // ring persists across calls within the server process). A J1-v1.1
78
+ // follow-up may add SQLite-backed CLI persistence (migration v30
79
+ // recall_history table per the original brainstorm option D).
80
+ const sessionRecallHistoryCli = new Map();
81
+ /** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
82
+ export function __resetSessionRecallHistoryCli() {
83
+ sessionRecallHistoryCli.clear();
84
+ }
61
85
  import * as client from './client.js';
62
86
  import { detectServer, removePidfileIfOwned } from './server-detect.js';
63
87
  import { resolveTenantId } from './tenant.js';
@@ -1022,18 +1046,66 @@ async function cmdRecall(hippoRoot, query, flags) {
1022
1046
  droppedByBudgetCountCmd = results.length - limit;
1023
1047
  results = results.slice(0, limit);
1024
1048
  }
1049
+ // v0.33 / J1 — CLI per-pipeline anchoring detector. Each pipeline (api.recall,
1050
+ // cmdRecall, MCP) computes its own AnchoringHint via the shared detectAnchoring
1051
+ // helper against its own top-1 + its own per-(tenant, session) ring buffer.
1052
+ // HIPPO_ANCHORING=off short-circuits BEFORE both the ring lookup and the
1053
+ // detect call so disabled tenants pay truly zero work. When sessionId is
1054
+ // absent we emit recall_anchor_skipped_no_session for J1-v2 telemetry.
1055
+ let cmdAnchoringHint = null;
1056
+ if (process.env.HIPPO_ANCHORING !== 'off') {
1057
+ if (sessionId) {
1058
+ const ringKey = buildSessionKey(tenantId, sessionId);
1059
+ const ring = getOrCreateRing(sessionRecallHistoryCli, ringKey);
1060
+ const queryHash = hashQueryText(query);
1061
+ const topId = results[0]?.entry.id ?? null;
1062
+ cmdAnchoringHint = detectAnchoring(snapshotRing(ring), queryHash, topId);
1063
+ // Append AFTER detect (snapshot was taken before). anchoredOn carries
1064
+ // the memoryId of any hint that fired, feeding the cooldown logic for
1065
+ // the NEXT cmdRecall on this session.
1066
+ appendRecall(ring, queryHash, topId, cmdAnchoringHint?.memoryId);
1067
+ }
1068
+ else {
1069
+ // Telemetry: caller had no sessionId so ring tracking is skipped.
1070
+ // Per the recall-audit convention at api.ts:854, use SHA-256/16
1071
+ // for prompt hashing (NOT hashQueryText which is FNV-1a 32-bit
1072
+ // for recall matching; brute-force trivial for low-entropy
1073
+ // queries). Codex round-1 P1 / round-2 P2 catch.
1074
+ emitCliAudit(hippoRoot, 'recall_anchor_skipped_no_session', undefined, {
1075
+ query_hash: createHash('sha256').update(query).digest('hex').slice(0, 16),
1076
+ query_length: query.length,
1077
+ });
1078
+ }
1079
+ }
1025
1080
  // v1.12.13 / C5 — Build suppressionSummary for cmdRecall pipeline. Surfaced
1026
1081
  // in --why text output and in the --json JSON output. cmdRecall does not
1027
1082
  // run the summarizeOverflow path (api.recall does) and does not currently
1028
1083
  // expose fresh-tail in the CLI, so those two counters are 0 here.
1084
+ // v0.33 / J1: suppressedByInterference is bumped by 1 when cmdAnchoringHint
1085
+ // fires with reason='memory_dominance' (the only reason that counts as
1086
+ // interference; query_repeat is a re-ask, not memory competition).
1087
+ const cmdSuppressedByInterference = cmdAnchoringHint?.reason === 'memory_dominance' ? 1 : 0;
1029
1088
  const cmdSuppressionSummary = api.buildSuppressionSummary({
1030
1089
  totalCandidates: totalCandidatesCountCmd,
1031
1090
  droppedPreRank: droppedPreRankCountCmd,
1032
1091
  droppedByBudget: droppedByBudgetCountCmd,
1033
1092
  summarySubstitutionsAdded: 0,
1034
1093
  freshTailAdded: 0,
1035
- suppressedByInterference: 0,
1094
+ suppressedByInterference: cmdSuppressedByInterference,
1036
1095
  });
1096
+ // v0.33 / J1 — emit pipeline-local audit op when a hint fires (lockstep
1097
+ // with api.recall's audit pattern; each pipeline emits for its own hits).
1098
+ if (cmdAnchoringHint?.reason === 'memory_dominance') {
1099
+ emitCliAudit(hippoRoot, 'recall_anchor_detected_memory_dominance', cmdAnchoringHint.memoryId, {
1100
+ memory_id: cmdAnchoringHint.memoryId,
1101
+ query_count: cmdAnchoringHint.queryCount ?? null,
1102
+ });
1103
+ }
1104
+ else if (cmdAnchoringHint?.reason === 'query_repeat') {
1105
+ emitCliAudit(hippoRoot, 'recall_anchor_detected_query_repeat', cmdAnchoringHint.memoryId, {
1106
+ memory_id: cmdAnchoringHint.memoryId,
1107
+ });
1108
+ }
1037
1109
  // v0.32 / J3.2 — auto-injection of reference-class baserate when the
1038
1110
  // CLI query carries a forward-prediction phrase AND a class matches.
1039
1111
  // cmdRecall runs its own pipeline (doesn't go through api.recall for
@@ -1135,6 +1207,7 @@ async function cmdRecall(hippoRoot, query, flags) {
1135
1207
  // matches. A forward-claim query that finds no memories STILL
1136
1208
  // produces useful planning-fallacy debias when the class resolves.
1137
1209
  ...(cmdPlanningFallacyHint ? { planningFallacyHint: cmdPlanningFallacyHint } : {}),
1210
+ ...(cmdAnchoringHint ? { anchoringHint: cmdAnchoringHint } : {}),
1138
1211
  };
1139
1212
  if (includeContinuity) {
1140
1213
  out.continuity = {
@@ -1147,6 +1220,11 @@ async function cmdRecall(hippoRoot, query, flags) {
1147
1220
  console.log(JSON.stringify(out));
1148
1221
  return;
1149
1222
  }
1223
+ // v0.33 / J1 — render anchoring hint above planning hint (anchoring
1224
+ // is the stronger cognitive-pull warning so it gets first position).
1225
+ if (cmdAnchoringHint) {
1226
+ console.log(`[anchored_on: ${cmdAnchoringHint.memoryId}] ${cmdAnchoringHint.summary}`);
1227
+ }
1150
1228
  // v0.32 / J3.2 — render hint BEFORE the no-memories message so the
1151
1229
  // calling agent sees its track record even when the query missed
1152
1230
  // every memory. Same single-line shape + JSON.stringify-safe phrase
@@ -1221,6 +1299,7 @@ async function cmdRecall(hippoRoot, query, flags) {
1221
1299
  total: output.length,
1222
1300
  suppressionSummary: cmdSuppressionSummary,
1223
1301
  ...(cmdPlanningFallacyHint ? { planningFallacyHint: cmdPlanningFallacyHint } : {}),
1302
+ ...(cmdAnchoringHint ? { anchoringHint: cmdAnchoringHint } : {}),
1224
1303
  };
1225
1304
  if (includeContinuity) {
1226
1305
  jsonOut.continuity = {
@@ -1242,6 +1321,14 @@ async function cmdRecall(hippoRoot, query, flags) {
1242
1321
  if (recentSessionEvents.length > 0)
1243
1322
  printSessionEvents(recentSessionEvents);
1244
1323
  }
1324
+ // v0.33 / J1 — render anchoring hint above planning-fallacy hint
1325
+ // (anchoring is the stronger cognitive-pull warning so it gets first
1326
+ // position). Hint absent (env disabled, no sessionId, or no R1/R2)
1327
+ // is silent.
1328
+ if (cmdAnchoringHint) {
1329
+ console.log(`[anchored_on: ${cmdAnchoringHint.memoryId}] ${cmdAnchoringHint.summary}`);
1330
+ console.log();
1331
+ }
1245
1332
  // v0.32 / J3.2 — render planning-fallacy hint ABOVE the result list so
1246
1333
  // the agent sees its track record before scrolling. Hint absent (env
1247
1334
  // disabled or no forward-claim match) is silent. detectedPhrase is
@@ -1253,6 +1340,31 @@ async function cmdRecall(hippoRoot, query, flags) {
1253
1340
  console.log(`Planning fallacy hint (class: ${cmdPlanningFallacyHint.classTag}): ${cmdPlanningFallacyHint.baserateSummary} [detected: ${safePhrase}]`);
1254
1341
  console.log();
1255
1342
  }
1343
+ // v1.13.3 / C5 follow-up — Cutoff line ABOVE the result list (was a
1344
+ // "WYSIATI:" line BELOW the result list in v1.12.13-v1.13.2). Dogfood
1345
+ // proof at docs/dogfood/2026-05-27-track-j-warnings.md: a fresh sub-agent
1346
+ // reading the v1.13.2 bottom-placed line ignored it entirely and
1347
+ // summarised the visible memories as if the dropped pool didn't exist
1348
+ // (the exact WYSIATI failure mode C5 is supposed to flag). Top placement
1349
+ // + plain English ("Cutoff:" not "WYSIATI:") closes the read gap.
1350
+ if (showWhy) {
1351
+ const s = cmdSuppressionSummary;
1352
+ const clauses = [];
1353
+ if (s.droppedByBudget > 0)
1354
+ clauses.push(`${s.droppedByBudget} dropped to fit limit`);
1355
+ if (s.droppedPreRank > 0)
1356
+ clauses.push(`${s.droppedPreRank} filtered pre-rank`);
1357
+ if (s.summarySubstitutionsAdded > 0)
1358
+ clauses.push(`${s.summarySubstitutionsAdded} summary substitutions added`);
1359
+ if (s.freshTailAdded > 0)
1360
+ clauses.push(`${s.freshTailAdded} fresh-tail added`);
1361
+ if (s.suppressedByInterference > 0)
1362
+ clauses.push(`${s.suppressedByInterference} suppressed by interference`);
1363
+ if (clauses.length > 0) {
1364
+ console.log(`Cutoff: showing ${results.length} of ${s.totalCandidates} candidates; ${clauses.join('; ')}.`);
1365
+ console.log();
1366
+ }
1367
+ }
1256
1368
  console.log(`Found ${results.length} memories (${totalTokens} tokens) for: "${query}"\n`);
1257
1369
  for (const r of results) {
1258
1370
  const e = r.entry;
@@ -1287,30 +1399,9 @@ async function cmdRecall(hippoRoot, query, flags) {
1287
1399
  console.log(e.content);
1288
1400
  console.log();
1289
1401
  }
1290
- // v1.12.13 / C5 WYSIATI cutoff transparency in --why text output.
1291
- // Single-line summary after the result list, emitted only when --why is
1292
- // set AND at least one counter is non-zero. Skip zero-count clauses to
1293
- // keep the line tight. The calling agent uses this to spot when the
1294
- // shown set is a small slice of a much larger candidate pool (Kahneman
1295
- // "What You See Is All There Is" failure mode).
1296
- if (showWhy) {
1297
- const s = cmdSuppressionSummary;
1298
- const clauses = [];
1299
- if (s.droppedByBudget > 0)
1300
- clauses.push(`${s.droppedByBudget} dropped by limit`);
1301
- if (s.droppedPreRank > 0)
1302
- clauses.push(`${s.droppedPreRank} pre-rank filtered`);
1303
- if (s.summarySubstitutionsAdded > 0)
1304
- clauses.push(`${s.summarySubstitutionsAdded} summary substitutions added`);
1305
- if (s.freshTailAdded > 0)
1306
- clauses.push(`${s.freshTailAdded} fresh-tail added`);
1307
- if (s.suppressedByInterference > 0)
1308
- clauses.push(`${s.suppressedByInterference} suppressed by interference`);
1309
- if (clauses.length > 0) {
1310
- console.log(`WYSIATI: showing ${results.length}/${s.totalCandidates}; ${clauses.join('; ')}.`);
1311
- console.log();
1312
- }
1313
- }
1402
+ // v1.12.13 / C5 -> v1.13.3 follow-up: the WYSIATI bottom block was
1403
+ // moved above the result list (see comment near the Cutoff render
1404
+ // above). Function ends here.
1314
1405
  }
1315
1406
  async function cmdExplain(hippoRoot, query, flags) {
1316
1407
  requireInit(hippoRoot);
@@ -4560,6 +4651,9 @@ const VALID_AUDIT_OPS = new Set([
4560
4651
  'recall_autodebias_hint', // v0.32 / J3.2 — emitted by computePlanningFallacyHint on success
4561
4652
  'recall_autodebias_hint_no_class_match', // v0.32 / J3.2 — telemetry: forward-claim, no class scored
4562
4653
  'recall_autodebias_hint_tiebreak', // v0.32 / J3.2 — telemetry: forward-claim, >=2 classes tied
4654
+ 'recall_anchor_detected_query_repeat', // v0.33 / J1 — emitted by detector on R1 fire
4655
+ 'recall_anchor_detected_memory_dominance', // v0.33 / J1 — emitted by detector on R2 fire
4656
+ 'recall_anchor_skipped_no_session', // v0.33 / J1 — telemetry: no sessionId, ring skipped
4563
4657
  ]);
4564
4658
  function formatAuditRow(ev) {
4565
4659
  const target = ev.targetId ?? '-';