hippo-memory 1.12.12 → 1.13.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.
Files changed (44) hide show
  1. package/dist/api.d.ts +89 -0
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +57 -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.map +1 -1
  9. package/dist/cli.js +279 -2
  10. package/dist/cli.js.map +1 -1
  11. package/dist/db.d.ts.map +1 -1
  12. package/dist/db.js +77 -1
  13. package/dist/db.js.map +1 -1
  14. package/dist/mcp/server.d.ts.map +1 -1
  15. package/dist/mcp/server.js +105 -1
  16. package/dist/mcp/server.js.map +1 -1
  17. package/dist/predictions.d.ts +122 -0
  18. package/dist/predictions.d.ts.map +1 -0
  19. package/dist/predictions.js +386 -0
  20. package/dist/predictions.js.map +1 -0
  21. package/dist/server.d.ts.map +1 -1
  22. package/dist/server.js +175 -0
  23. package/dist/server.js.map +1 -1
  24. package/dist/src/api.js +57 -1
  25. package/dist/src/api.js.map +1 -1
  26. package/dist/src/audit.js.map +1 -1
  27. package/dist/src/cli.js +279 -2
  28. package/dist/src/cli.js.map +1 -1
  29. package/dist/src/db.js +77 -1
  30. package/dist/src/db.js.map +1 -1
  31. package/dist/src/mcp/server.js +105 -1
  32. package/dist/src/mcp/server.js.map +1 -1
  33. package/dist/src/predictions.js +386 -0
  34. package/dist/src/predictions.js.map +1 -0
  35. package/dist/src/server.js +175 -0
  36. package/dist/src/server.js.map +1 -1
  37. package/dist/src/store.js +1 -1
  38. package/dist/src/store.js.map +1 -1
  39. package/dist/store.d.ts +17 -0
  40. package/dist/store.d.ts.map +1 -1
  41. package/dist/store.js +1 -1
  42. package/dist/store.js.map +1 -1
  43. package/openclaw.plugin.json +1 -1
  44. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -56,6 +56,7 @@ import { listApiKeys, revokeApiKey } from './auth.js';
56
56
  import { buildProvenanceCoverage } from './provenance-coverage.js';
57
57
  import { buildCorrectionLatency } from './correction-latency.js';
58
58
  import * as api from './api.js';
59
+ import * as predictionsModule from './predictions.js';
59
60
  import * as client from './client.js';
60
61
  import { detectServer, removePidfileIfOwned } from './server-detect.js';
61
62
  import { resolveTenantId } from './tenant.js';
@@ -682,6 +683,13 @@ async function cmdRecall(hippoRoot, query, flags) {
682
683
  const tenantId = resolveTenantId({});
683
684
  let localEntries = loadSearchEntries(hippoRoot, query, undefined, tenantId);
684
685
  let globalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query, undefined, tenantId) : [];
686
+ // v1.12.13 / C5 — WYSIATI counters. Track filter activity per the plan v3
687
+ // Task 3 mapping table. dropped_pre_rank is the SUM of all non-budget
688
+ // filter drops (pre-rank AND post-rank). Search-engine internal drops
689
+ // (scored-to-zero rows that hybridSearch/physicsSearch returns fewer of)
690
+ // are NOT counted in v1 — they are part of the rank step, not a filter.
691
+ const totalCandidatesCountCmd = localEntries.length + globalEntries.length;
692
+ let droppedPreRankCountCmd = 0;
685
693
  // Bi-temporal filtering for physics path (hybridSearch handles it internally)
686
694
  if (asOf) {
687
695
  const filterAsOf = (entries) => {
@@ -703,12 +711,16 @@ async function cmdRecall(hippoRoot, query, flags) {
703
711
  return succVf ? new Date(succVf) > asOfDate : true;
704
712
  });
705
713
  };
714
+ const beforeAsOf = localEntries.length + globalEntries.length;
706
715
  localEntries = filterAsOf(localEntries);
707
716
  globalEntries = filterAsOf(globalEntries);
717
+ droppedPreRankCountCmd += beforeAsOf - (localEntries.length + globalEntries.length);
708
718
  }
709
719
  else if (!includeSuperseded) {
720
+ const beforeSupersededDrop = localEntries.length + globalEntries.length;
710
721
  localEntries = localEntries.filter(e => !e.superseded_by);
711
722
  globalEntries = globalEntries.filter(e => !e.superseded_by);
723
+ droppedPreRankCountCmd += beforeSupersededDrop - (localEntries.length + globalEntries.length);
712
724
  }
713
725
  const hasGlobal = globalEntries.length > 0;
714
726
  // Determine search mode: --physics forces physics, --classic forces BM25+cosine,
@@ -817,7 +829,9 @@ async function cmdRecall(hippoRoot, query, flags) {
817
829
  // We never infer conflicts from lexical overlap. The v1 salience gate did
818
830
  // that and destroyed LoCoMo (0.28 → 0.02). Recorded structure only.
819
831
  if (flags['filter-conflicts']) {
832
+ const beforeFilterConflicts = results.length;
820
833
  results = results.filter((r) => !r.entry.superseded_by);
834
+ droppedPreRankCountCmd += beforeFilterConflicts - results.length;
821
835
  const presentIds = new Set(results.map((r) => r.entry.id));
822
836
  results = results.map((r) => {
823
837
  const peers = r.entry.conflicts_with || [];
@@ -981,11 +995,13 @@ async function cmdRecall(hippoRoot, query, flags) {
981
995
  console.error(`Invalid --outcome: "${outcomeFilter}". Must be one of: ${validOutcomes.join(', ')}.`);
982
996
  process.exit(1);
983
997
  }
998
+ const beforeOutcomeFilter = results.length;
984
999
  results = results.filter((r) => {
985
1000
  if (r.entry.layer !== Layer.Trace)
986
1001
  return true;
987
1002
  return r.entry.trace_outcome === outcomeFilter;
988
1003
  });
1004
+ droppedPreRankCountCmd += beforeOutcomeFilter - results.length;
989
1005
  }
990
1006
  // --layer filter: strict, drops entries whose layer does not match.
991
1007
  const layerFilter = flags['layer'] !== undefined ? String(flags['layer']).trim() : '';
@@ -995,11 +1011,28 @@ async function cmdRecall(hippoRoot, query, flags) {
995
1011
  console.error(`Invalid --layer: "${layerFilter}". Must be one of: ${validLayers.join(', ')}.`);
996
1012
  process.exit(1);
997
1013
  }
1014
+ const beforeLayerFilter = results.length;
998
1015
  results = results.filter((r) => r.entry.layer === layerFilter);
1016
+ droppedPreRankCountCmd += beforeLayerFilter - results.length;
999
1017
  }
1018
+ // v1.12.13 / C5 — WYSIATI dropped_by_budget counter (final limit cut).
1019
+ let droppedByBudgetCountCmd = 0;
1000
1020
  if (limit < results.length) {
1021
+ droppedByBudgetCountCmd = results.length - limit;
1001
1022
  results = results.slice(0, limit);
1002
1023
  }
1024
+ // v1.12.13 / C5 — Build suppressionSummary for cmdRecall pipeline. Surfaced
1025
+ // in --why text output and in the --json JSON output. cmdRecall does not
1026
+ // run the summarizeOverflow path (api.recall does) and does not currently
1027
+ // expose fresh-tail in the CLI, so those two counters are 0 here.
1028
+ const cmdSuppressionSummary = api.buildSuppressionSummary({
1029
+ totalCandidates: totalCandidatesCountCmd,
1030
+ droppedPreRank: droppedPreRankCountCmd,
1031
+ droppedByBudget: droppedByBudgetCountCmd,
1032
+ summarySubstitutionsAdded: 0,
1033
+ freshTailAdded: 0,
1034
+ suppressedByInterference: 0,
1035
+ });
1003
1036
  // A5 audit: emit one 'recall' event per query, capturing the (truncated)
1004
1037
  // query text and the post-filter result count. Tenant resolved by emitCliAudit.
1005
1038
  // Emit before the early-empty return so zero-result recalls are still logged.
@@ -1079,7 +1112,12 @@ async function cmdRecall(hippoRoot, query, flags) {
1079
1112
  || recentSessionEvents.length > 0;
1080
1113
  if (results.length === 0) {
1081
1114
  if (asJson) {
1082
- const out = { query, results: [], total: 0 };
1115
+ const out = {
1116
+ query,
1117
+ results: [],
1118
+ total: 0,
1119
+ suppressionSummary: cmdSuppressionSummary,
1120
+ };
1083
1121
  if (includeContinuity) {
1084
1122
  out.continuity = {
1085
1123
  activeSnapshot,
@@ -1149,7 +1187,13 @@ async function cmdRecall(hippoRoot, query, flags) {
1149
1187
  }
1150
1188
  return base;
1151
1189
  });
1152
- const jsonOut = { query, budget, results: output, total: output.length };
1190
+ const jsonOut = {
1191
+ query,
1192
+ budget,
1193
+ results: output,
1194
+ total: output.length,
1195
+ suppressionSummary: cmdSuppressionSummary,
1196
+ };
1153
1197
  if (includeContinuity) {
1154
1198
  jsonOut.continuity = {
1155
1199
  activeSnapshot,
@@ -1204,6 +1248,30 @@ async function cmdRecall(hippoRoot, query, flags) {
1204
1248
  console.log(e.content);
1205
1249
  console.log();
1206
1250
  }
1251
+ // v1.12.13 / C5 — WYSIATI cutoff transparency in --why text output.
1252
+ // Single-line summary after the result list, emitted only when --why is
1253
+ // set AND at least one counter is non-zero. Skip zero-count clauses to
1254
+ // keep the line tight. The calling agent uses this to spot when the
1255
+ // shown set is a small slice of a much larger candidate pool (Kahneman
1256
+ // "What You See Is All There Is" failure mode).
1257
+ if (showWhy) {
1258
+ const s = cmdSuppressionSummary;
1259
+ const clauses = [];
1260
+ if (s.droppedByBudget > 0)
1261
+ clauses.push(`${s.droppedByBudget} dropped by limit`);
1262
+ if (s.droppedPreRank > 0)
1263
+ clauses.push(`${s.droppedPreRank} pre-rank filtered`);
1264
+ if (s.summarySubstitutionsAdded > 0)
1265
+ clauses.push(`${s.summarySubstitutionsAdded} summary substitutions added`);
1266
+ if (s.freshTailAdded > 0)
1267
+ clauses.push(`${s.freshTailAdded} fresh-tail added`);
1268
+ if (s.suppressedByInterference > 0)
1269
+ clauses.push(`${s.suppressedByInterference} suppressed by interference`);
1270
+ if (clauses.length > 0) {
1271
+ console.log(`WYSIATI: showing ${results.length}/${s.totalCandidates}; ${clauses.join('; ')}.`);
1272
+ console.log();
1273
+ }
1274
+ }
1207
1275
  }
1208
1276
  async function cmdExplain(hippoRoot, query, flags) {
1209
1277
  requireInit(hippoRoot);
@@ -2840,6 +2908,209 @@ function cmdHandoff(hippoRoot, args, flags) {
2840
2908
  console.error('Usage: hippo handoff <create|latest|show>');
2841
2909
  process.exit(1);
2842
2910
  }
2911
+ // ---------------------------------------------------------------------------
2912
+ // E2 prediction first-class object (v0.31)
2913
+ // docs/plans/2026-05-26-e2-prediction-object.md
2914
+ // ---------------------------------------------------------------------------
2915
+ function cmdPredict(hippoRoot, args, flags) {
2916
+ requireInit(hippoRoot);
2917
+ const tenantId = resolveTenantId({});
2918
+ const subcommand = args[0] ?? '';
2919
+ if (subcommand === 'close') {
2920
+ const idRaw = args[1];
2921
+ if (!idRaw) {
2922
+ console.error('Usage: hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
2923
+ process.exit(1);
2924
+ }
2925
+ const id = parseInt(String(idRaw), 10);
2926
+ if (!Number.isFinite(id) || id <= 0) {
2927
+ console.error(`Invalid prediction id: "${idRaw}"`);
2928
+ process.exit(1);
2929
+ }
2930
+ const stateRaw = typeof flags['state'] === 'string' ? flags['state'].trim() : '';
2931
+ if (!predictionsModule.VALID_CLOSURE_STATES.has(stateRaw) || stateRaw === 'open') {
2932
+ console.error(`Invalid --state: "${stateRaw}". Must be one of: closed | closed-unknown.`);
2933
+ process.exit(1);
2934
+ }
2935
+ const actualRaw = flags['actual'];
2936
+ const actualValue = actualRaw !== undefined ? Number(actualRaw) : undefined;
2937
+ if (actualRaw !== undefined && !Number.isFinite(actualValue)) {
2938
+ console.error(`Invalid --actual: "${actualRaw}". Must be a number.`);
2939
+ process.exit(1);
2940
+ }
2941
+ const noteRaw = flags['note'];
2942
+ const closureNote = typeof noteRaw === 'string' ? noteRaw : undefined;
2943
+ const closed = predictionsModule.closePrediction(hippoRoot, tenantId, id, {
2944
+ closureState: stateRaw,
2945
+ actualValue,
2946
+ closureNote,
2947
+ });
2948
+ console.log(`Prediction ${closed.id} closed: state=${closed.closureState}${closed.actualValue !== null ? ` actual=${closed.actualValue}` : ''}`);
2949
+ return;
2950
+ }
2951
+ if (subcommand === 'list') {
2952
+ const classTagRaw = flags['class'];
2953
+ const classTag = typeof classTagRaw === 'string' ? classTagRaw.trim() : '';
2954
+ const statusRaw = flags['status'];
2955
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
2956
+ const limitRaw = flags['limit'];
2957
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
2958
+ if (!Number.isFinite(limit) || limit <= 0) {
2959
+ console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
2960
+ process.exit(1);
2961
+ }
2962
+ let results;
2963
+ if (status === 'open') {
2964
+ results = predictionsModule.loadOpenPredictions(hippoRoot, tenantId, {
2965
+ classTag: classTag || undefined,
2966
+ limit,
2967
+ });
2968
+ }
2969
+ else if (status === 'all') {
2970
+ // No closure-state filter; pull both via loadPredictionsByClass if class given
2971
+ if (classTag) {
2972
+ results = predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, { limit });
2973
+ }
2974
+ else {
2975
+ // No class filter + status=all = pull open + closed across all classes
2976
+ // (kept simple: report open via loadOpenPredictions; closed via two
2977
+ // class scans isn't symmetrical. v1 callers typically pass --class.)
2978
+ results = predictionsModule.loadOpenPredictions(hippoRoot, tenantId, { limit });
2979
+ }
2980
+ }
2981
+ else {
2982
+ if (!predictionsModule.VALID_CLOSURE_STATES.has(status)) {
2983
+ console.error(`Invalid --status: "${status}". Must be one of: open | closed | closed-unknown | all.`);
2984
+ process.exit(1);
2985
+ }
2986
+ if (classTag) {
2987
+ results = predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, {
2988
+ closureState: status,
2989
+ limit,
2990
+ });
2991
+ }
2992
+ else {
2993
+ // status filter without class — scan all classes is more complex; v1 requires --class for non-default status
2994
+ console.error('--status filter (non-open) requires --class to be set.');
2995
+ process.exit(1);
2996
+ }
2997
+ }
2998
+ if (results.length === 0) {
2999
+ console.log(classTag ? `No predictions in class "${classTag}".` : 'No predictions.');
3000
+ return;
3001
+ }
3002
+ console.log(`Found ${results.length} predictions:\n`);
3003
+ for (const p of results) {
3004
+ const estPart = p.estimateValue !== null ? ` estimate=${p.estimateValue}${p.estimateUnit ? ` ${p.estimateUnit}` : ''}` : '';
3005
+ const actPart = p.actualValue !== null ? ` actual=${p.actualValue}` : '';
3006
+ const tgtPart = p.targetDate ? ` target=${p.targetDate}` : '';
3007
+ console.log(`#${p.id} [${p.closureState}] class=${p.classTag}${estPart}${actPart}${tgtPart}`);
3008
+ console.log(` ${p.claimText}`);
3009
+ if (p.closureNote)
3010
+ console.log(` note: ${p.closureNote}`);
3011
+ }
3012
+ return;
3013
+ }
3014
+ if (subcommand === 'show') {
3015
+ const idRaw = args[1];
3016
+ if (!idRaw) {
3017
+ console.error('Usage: hippo predict show <id>');
3018
+ process.exit(1);
3019
+ }
3020
+ const id = parseInt(String(idRaw), 10);
3021
+ if (!Number.isFinite(id) || id <= 0) {
3022
+ console.error(`Invalid prediction id: "${idRaw}"`);
3023
+ process.exit(1);
3024
+ }
3025
+ const pred = predictionsModule.loadPredictionById(hippoRoot, tenantId, id);
3026
+ if (!pred) {
3027
+ console.error(`Prediction ${id} not found.`);
3028
+ process.exit(1);
3029
+ }
3030
+ console.log(`Prediction #${pred.id}`);
3031
+ console.log(` class: ${pred.classTag}`);
3032
+ console.log(` claim: ${pred.claimText}`);
3033
+ console.log(` state: ${pred.closureState}`);
3034
+ if (pred.estimateValue !== null)
3035
+ console.log(` estimate: ${pred.estimateValue}${pred.estimateUnit ? ' ' + pred.estimateUnit : ''}`);
3036
+ if (pred.targetDate)
3037
+ console.log(` target: ${pred.targetDate}`);
3038
+ if (pred.actualValue !== null)
3039
+ console.log(` actual: ${pred.actualValue}`);
3040
+ if (pred.closedAt)
3041
+ console.log(` closed: ${pred.closedAt}`);
3042
+ if (pred.closureNote)
3043
+ console.log(` note: ${pred.closureNote}`);
3044
+ if (pred.memoryId)
3045
+ console.log(` memory: ${pred.memoryId}`);
3046
+ console.log(` created: ${pred.createdAt}`);
3047
+ return;
3048
+ }
3049
+ if (subcommand === 'baserate') {
3050
+ // J3 reference-class / planning-fallacy detector
3051
+ const classTagRaw = flags['class'];
3052
+ if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
3053
+ console.error('Usage: hippo predict baserate --class <c>');
3054
+ process.exit(1);
3055
+ }
3056
+ const baserate = predictionsModule.computePredictionBaserate(hippoRoot, tenantId, classTagRaw.trim());
3057
+ if (baserate.nClosed === 0) {
3058
+ console.log(`No closed predictions in class "${baserate.classTag}" yet.`);
3059
+ console.log(` Create one with: hippo predict "<claim>" --class ${baserate.classTag} --estimate N`);
3060
+ console.log(` Close it later: hippo predict close <id> --state closed --actual N`);
3061
+ return;
3062
+ }
3063
+ console.log(baserate.summary);
3064
+ console.log(` n_closed: ${baserate.nClosed}`);
3065
+ console.log(` n_ratio_eligible: ${baserate.nRatioEligible}`);
3066
+ if (baserate.meanEstimate !== null)
3067
+ console.log(` mean_estimate: ${baserate.meanEstimate.toFixed(3)}`);
3068
+ if (baserate.meanActual !== null)
3069
+ console.log(` mean_actual: ${baserate.meanActual.toFixed(3)}`);
3070
+ if (baserate.meanRatio !== null)
3071
+ console.log(` mean_ratio: ${baserate.meanRatio.toFixed(3)}x`);
3072
+ if (baserate.p50Ratio !== null)
3073
+ console.log(` p50_ratio: ${baserate.p50Ratio.toFixed(3)}x`);
3074
+ if (baserate.mae !== null)
3075
+ console.log(` mae: ${baserate.mae.toFixed(3)}`);
3076
+ return;
3077
+ }
3078
+ // Default subcommand: create. args[0] is the claim text.
3079
+ const claimText = subcommand;
3080
+ if (!claimText) {
3081
+ console.error('Usage: hippo predict "<claim>" --class <c> [--estimate <v>] [--unit <u>] [--target <YYYY-MM-DD>]');
3082
+ console.error(' hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
3083
+ console.error(' hippo predict list [--class X] [--status open|closed|closed-unknown|all] [--limit N]');
3084
+ console.error(' hippo predict show <id>');
3085
+ process.exit(1);
3086
+ }
3087
+ const classTagRaw = flags['class'];
3088
+ if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
3089
+ console.error('--class is required for prediction creation.');
3090
+ process.exit(1);
3091
+ }
3092
+ const classTag = classTagRaw.trim();
3093
+ const estimateRaw = flags['estimate'];
3094
+ const estimateValue = estimateRaw !== undefined ? Number(estimateRaw) : undefined;
3095
+ if (estimateRaw !== undefined && !Number.isFinite(estimateValue)) {
3096
+ console.error(`Invalid --estimate: "${estimateRaw}". Must be a number.`);
3097
+ process.exit(1);
3098
+ }
3099
+ const unitRaw = flags['unit'];
3100
+ const estimateUnit = typeof unitRaw === 'string' ? unitRaw : undefined;
3101
+ const targetRaw = flags['target'];
3102
+ const targetDate = typeof targetRaw === 'string' ? targetRaw : undefined;
3103
+ const created = predictionsModule.savePrediction(hippoRoot, tenantId, {
3104
+ classTag,
3105
+ claimText,
3106
+ estimateValue,
3107
+ estimateUnit,
3108
+ targetDate,
3109
+ });
3110
+ console.log(`Prediction recorded: #${created.id} class=${created.classTag}`);
3111
+ if (created.memoryId)
3112
+ console.log(` memory: ${created.memoryId}`);
3113
+ }
2843
3114
  function cmdCurrent(hippoRoot, args, flags) {
2844
3115
  requireInit(hippoRoot);
2845
3116
  const subcommand = args[0] ?? 'show';
@@ -4244,6 +4515,9 @@ const VALID_AUDIT_OPS = new Set([
4244
4515
  'summary_marked_dirty', // v0.30 / E1 — lockstep with AuditOp union + server.ts VALID_AUDIT_OPS (v1.11.5 CRIT A institutional rule)
4245
4516
  'summary_marked_clean', // v0.30 / E3 — buildDag post-link clean op; lockstep
4246
4517
  'summary_rebuilt', // v0.30 / E3 — sleep-cycle rebuild op; lockstep
4518
+ 'predict_create', // v0.31 / E2 prediction first-class object — emitted by savePrediction
4519
+ 'predict_close', // v0.31 / E2 — emitted by closePrediction
4520
+ 'predict_baserate', // v0.31 / J3 — emitted by computePredictionBaserate
4247
4521
  ]);
4248
4522
  function formatAuditRow(ev) {
4249
4523
  const target = ev.targetId ?? '-';
@@ -5426,6 +5700,9 @@ async function main() {
5426
5700
  case 'handoff':
5427
5701
  cmdHandoff(hippoRoot, args, flags);
5428
5702
  break;
5703
+ case 'predict':
5704
+ cmdPredict(hippoRoot, args, flags);
5705
+ break;
5429
5706
  case 'current':
5430
5707
  cmdCurrent(hippoRoot, args, flags);
5431
5708
  break;