hippo-memory 1.12.12 → 1.13.1

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 (52) hide show
  1. package/dist/api.d.ts +110 -0
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +68 -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 +321 -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/forward-claim-detector.d.ts +38 -0
  15. package/dist/forward-claim-detector.d.ts.map +1 -0
  16. package/dist/forward-claim-detector.js +117 -0
  17. package/dist/forward-claim-detector.js.map +1 -0
  18. package/dist/mcp/server.d.ts.map +1 -1
  19. package/dist/mcp/server.js +128 -2
  20. package/dist/mcp/server.js.map +1 -1
  21. package/dist/predictions.d.ts +194 -0
  22. package/dist/predictions.d.ts.map +1 -0
  23. package/dist/predictions.js +580 -0
  24. package/dist/predictions.js.map +1 -0
  25. package/dist/server.d.ts.map +1 -1
  26. package/dist/server.js +178 -0
  27. package/dist/server.js.map +1 -1
  28. package/dist/src/api.js +68 -1
  29. package/dist/src/api.js.map +1 -1
  30. package/dist/src/audit.js.map +1 -1
  31. package/dist/src/cli.js +321 -2
  32. package/dist/src/cli.js.map +1 -1
  33. package/dist/src/db.js +77 -1
  34. package/dist/src/db.js.map +1 -1
  35. package/dist/src/forward-claim-detector.js +117 -0
  36. package/dist/src/forward-claim-detector.js.map +1 -0
  37. package/dist/src/mcp/server.js +128 -2
  38. package/dist/src/mcp/server.js.map +1 -1
  39. package/dist/src/predictions.js +580 -0
  40. package/dist/src/predictions.js.map +1 -0
  41. package/dist/src/server.js +178 -0
  42. package/dist/src/server.js.map +1 -1
  43. package/dist/src/store.js +1 -1
  44. package/dist/src/store.js.map +1 -1
  45. package/dist/store.d.ts +17 -0
  46. package/dist/store.d.ts.map +1 -1
  47. package/dist/store.js +1 -1
  48. package/dist/store.js.map +1 -1
  49. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  50. package/extensions/openclaw-plugin/package.json +1 -1
  51. package/openclaw.plugin.json +1 -1
  52. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -56,6 +56,8 @@ 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';
60
+ import { computePlanningFallacyHint } from './predictions.js';
59
61
  import * as client from './client.js';
60
62
  import { detectServer, removePidfileIfOwned } from './server-detect.js';
61
63
  import { resolveTenantId } from './tenant.js';
@@ -682,6 +684,13 @@ async function cmdRecall(hippoRoot, query, flags) {
682
684
  const tenantId = resolveTenantId({});
683
685
  let localEntries = loadSearchEntries(hippoRoot, query, undefined, tenantId);
684
686
  let globalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query, undefined, tenantId) : [];
687
+ // v1.12.13 / C5 — WYSIATI counters. Track filter activity per the plan v3
688
+ // Task 3 mapping table. dropped_pre_rank is the SUM of all non-budget
689
+ // filter drops (pre-rank AND post-rank). Search-engine internal drops
690
+ // (scored-to-zero rows that hybridSearch/physicsSearch returns fewer of)
691
+ // are NOT counted in v1 — they are part of the rank step, not a filter.
692
+ const totalCandidatesCountCmd = localEntries.length + globalEntries.length;
693
+ let droppedPreRankCountCmd = 0;
685
694
  // Bi-temporal filtering for physics path (hybridSearch handles it internally)
686
695
  if (asOf) {
687
696
  const filterAsOf = (entries) => {
@@ -703,12 +712,16 @@ async function cmdRecall(hippoRoot, query, flags) {
703
712
  return succVf ? new Date(succVf) > asOfDate : true;
704
713
  });
705
714
  };
715
+ const beforeAsOf = localEntries.length + globalEntries.length;
706
716
  localEntries = filterAsOf(localEntries);
707
717
  globalEntries = filterAsOf(globalEntries);
718
+ droppedPreRankCountCmd += beforeAsOf - (localEntries.length + globalEntries.length);
708
719
  }
709
720
  else if (!includeSuperseded) {
721
+ const beforeSupersededDrop = localEntries.length + globalEntries.length;
710
722
  localEntries = localEntries.filter(e => !e.superseded_by);
711
723
  globalEntries = globalEntries.filter(e => !e.superseded_by);
724
+ droppedPreRankCountCmd += beforeSupersededDrop - (localEntries.length + globalEntries.length);
712
725
  }
713
726
  const hasGlobal = globalEntries.length > 0;
714
727
  // Determine search mode: --physics forces physics, --classic forces BM25+cosine,
@@ -817,7 +830,9 @@ async function cmdRecall(hippoRoot, query, flags) {
817
830
  // We never infer conflicts from lexical overlap. The v1 salience gate did
818
831
  // that and destroyed LoCoMo (0.28 → 0.02). Recorded structure only.
819
832
  if (flags['filter-conflicts']) {
833
+ const beforeFilterConflicts = results.length;
820
834
  results = results.filter((r) => !r.entry.superseded_by);
835
+ droppedPreRankCountCmd += beforeFilterConflicts - results.length;
821
836
  const presentIds = new Set(results.map((r) => r.entry.id));
822
837
  results = results.map((r) => {
823
838
  const peers = r.entry.conflicts_with || [];
@@ -981,11 +996,13 @@ async function cmdRecall(hippoRoot, query, flags) {
981
996
  console.error(`Invalid --outcome: "${outcomeFilter}". Must be one of: ${validOutcomes.join(', ')}.`);
982
997
  process.exit(1);
983
998
  }
999
+ const beforeOutcomeFilter = results.length;
984
1000
  results = results.filter((r) => {
985
1001
  if (r.entry.layer !== Layer.Trace)
986
1002
  return true;
987
1003
  return r.entry.trace_outcome === outcomeFilter;
988
1004
  });
1005
+ droppedPreRankCountCmd += beforeOutcomeFilter - results.length;
989
1006
  }
990
1007
  // --layer filter: strict, drops entries whose layer does not match.
991
1008
  const layerFilter = flags['layer'] !== undefined ? String(flags['layer']).trim() : '';
@@ -995,11 +1012,38 @@ async function cmdRecall(hippoRoot, query, flags) {
995
1012
  console.error(`Invalid --layer: "${layerFilter}". Must be one of: ${validLayers.join(', ')}.`);
996
1013
  process.exit(1);
997
1014
  }
1015
+ const beforeLayerFilter = results.length;
998
1016
  results = results.filter((r) => r.entry.layer === layerFilter);
1017
+ droppedPreRankCountCmd += beforeLayerFilter - results.length;
999
1018
  }
1019
+ // v1.12.13 / C5 — WYSIATI dropped_by_budget counter (final limit cut).
1020
+ let droppedByBudgetCountCmd = 0;
1000
1021
  if (limit < results.length) {
1022
+ droppedByBudgetCountCmd = results.length - limit;
1001
1023
  results = results.slice(0, limit);
1002
1024
  }
1025
+ // v1.12.13 / C5 — Build suppressionSummary for cmdRecall pipeline. Surfaced
1026
+ // in --why text output and in the --json JSON output. cmdRecall does not
1027
+ // run the summarizeOverflow path (api.recall does) and does not currently
1028
+ // expose fresh-tail in the CLI, so those two counters are 0 here.
1029
+ const cmdSuppressionSummary = api.buildSuppressionSummary({
1030
+ totalCandidates: totalCandidatesCountCmd,
1031
+ droppedPreRank: droppedPreRankCountCmd,
1032
+ droppedByBudget: droppedByBudgetCountCmd,
1033
+ summarySubstitutionsAdded: 0,
1034
+ freshTailAdded: 0,
1035
+ suppressedByInterference: 0,
1036
+ });
1037
+ // v0.32 / J3.2 — auto-injection of reference-class baserate when the
1038
+ // CLI query carries a forward-prediction phrase AND a class matches.
1039
+ // cmdRecall runs its own pipeline (doesn't go through api.recall for
1040
+ // the memory list), so it computes the hint here. The hint VALUE is
1041
+ // pipeline-invariant — same (hippoRoot, tenantId, query) inputs would
1042
+ // produce the same hint in api.recall — but the audit emission is
1043
+ // pipeline-local (one audit row per actual call, actor='cli' here).
1044
+ // computePlanningFallacyHint short-circuits BEFORE the regex gate
1045
+ // when HIPPO_AUTODEBIAS=off so the no-match path is effectively free.
1046
+ const cmdPlanningFallacyHint = computePlanningFallacyHint(hippoRoot, tenantId, query, { actor: 'cli' });
1003
1047
  // A5 audit: emit one 'recall' event per query, capturing the (truncated)
1004
1048
  // query text and the post-filter result count. Tenant resolved by emitCliAudit.
1005
1049
  // Emit before the early-empty return so zero-result recalls are still logged.
@@ -1079,7 +1123,19 @@ async function cmdRecall(hippoRoot, query, flags) {
1079
1123
  || recentSessionEvents.length > 0;
1080
1124
  if (results.length === 0) {
1081
1125
  if (asJson) {
1082
- const out = { query, results: [], total: 0 };
1126
+ const out = {
1127
+ query,
1128
+ results: [],
1129
+ total: 0,
1130
+ suppressionSummary: cmdSuppressionSummary,
1131
+ // v0.32 / J3.2 — preserve planningFallacyHint on zero-result
1132
+ // recalls. Codex review round 1 catch: hint was previously only
1133
+ // included in the populated-results JSON branch, breaking parity
1134
+ // with HTTP/MCP which surface the hint regardless of memory
1135
+ // matches. A forward-claim query that finds no memories STILL
1136
+ // produces useful planning-fallacy debias when the class resolves.
1137
+ ...(cmdPlanningFallacyHint ? { planningFallacyHint: cmdPlanningFallacyHint } : {}),
1138
+ };
1083
1139
  if (includeContinuity) {
1084
1140
  out.continuity = {
1085
1141
  activeSnapshot,
@@ -1091,6 +1147,15 @@ async function cmdRecall(hippoRoot, query, flags) {
1091
1147
  console.log(JSON.stringify(out));
1092
1148
  return;
1093
1149
  }
1150
+ // v0.32 / J3.2 — render hint BEFORE the no-memories message so the
1151
+ // calling agent sees its track record even when the query missed
1152
+ // every memory. Same single-line shape + JSON.stringify-safe phrase
1153
+ // as the populated-results path below.
1154
+ if (cmdPlanningFallacyHint) {
1155
+ const safePhrase = JSON.stringify(cmdPlanningFallacyHint.detectedPhrase);
1156
+ console.log(`Planning fallacy hint (class: ${cmdPlanningFallacyHint.classTag}): ${cmdPlanningFallacyHint.baserateSummary} [detected: ${safePhrase}]`);
1157
+ console.log();
1158
+ }
1094
1159
  if (hasContinuity) {
1095
1160
  // Print continuity even when no memories matched. The resume packet
1096
1161
  // is the whole point of `--continuity` and must not be dropped here.
@@ -1149,7 +1214,14 @@ async function cmdRecall(hippoRoot, query, flags) {
1149
1214
  }
1150
1215
  return base;
1151
1216
  });
1152
- const jsonOut = { query, budget, results: output, total: output.length };
1217
+ const jsonOut = {
1218
+ query,
1219
+ budget,
1220
+ results: output,
1221
+ total: output.length,
1222
+ suppressionSummary: cmdSuppressionSummary,
1223
+ ...(cmdPlanningFallacyHint ? { planningFallacyHint: cmdPlanningFallacyHint } : {}),
1224
+ };
1153
1225
  if (includeContinuity) {
1154
1226
  jsonOut.continuity = {
1155
1227
  activeSnapshot,
@@ -1170,6 +1242,17 @@ async function cmdRecall(hippoRoot, query, flags) {
1170
1242
  if (recentSessionEvents.length > 0)
1171
1243
  printSessionEvents(recentSessionEvents);
1172
1244
  }
1245
+ // v0.32 / J3.2 — render planning-fallacy hint ABOVE the result list so
1246
+ // the agent sees its track record before scrolling. Hint absent (env
1247
+ // disabled or no forward-claim match) is silent. detectedPhrase is
1248
+ // sanitised against control chars and ASCII quotes via JSON.stringify
1249
+ // to head off rendering ambiguity when a regex match contains quotes
1250
+ // or parens (plan-eng-critic round 2 LOW).
1251
+ if (cmdPlanningFallacyHint) {
1252
+ const safePhrase = JSON.stringify(cmdPlanningFallacyHint.detectedPhrase);
1253
+ console.log(`Planning fallacy hint (class: ${cmdPlanningFallacyHint.classTag}): ${cmdPlanningFallacyHint.baserateSummary} [detected: ${safePhrase}]`);
1254
+ console.log();
1255
+ }
1173
1256
  console.log(`Found ${results.length} memories (${totalTokens} tokens) for: "${query}"\n`);
1174
1257
  for (const r of results) {
1175
1258
  const e = r.entry;
@@ -1204,6 +1287,30 @@ async function cmdRecall(hippoRoot, query, flags) {
1204
1287
  console.log(e.content);
1205
1288
  console.log();
1206
1289
  }
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
+ }
1207
1314
  }
1208
1315
  async function cmdExplain(hippoRoot, query, flags) {
1209
1316
  requireInit(hippoRoot);
@@ -2840,6 +2947,209 @@ function cmdHandoff(hippoRoot, args, flags) {
2840
2947
  console.error('Usage: hippo handoff <create|latest|show>');
2841
2948
  process.exit(1);
2842
2949
  }
2950
+ // ---------------------------------------------------------------------------
2951
+ // E2 prediction first-class object (v0.31)
2952
+ // docs/plans/2026-05-26-e2-prediction-object.md
2953
+ // ---------------------------------------------------------------------------
2954
+ function cmdPredict(hippoRoot, args, flags) {
2955
+ requireInit(hippoRoot);
2956
+ const tenantId = resolveTenantId({});
2957
+ const subcommand = args[0] ?? '';
2958
+ if (subcommand === 'close') {
2959
+ const idRaw = args[1];
2960
+ if (!idRaw) {
2961
+ console.error('Usage: hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
2962
+ process.exit(1);
2963
+ }
2964
+ const id = parseInt(String(idRaw), 10);
2965
+ if (!Number.isFinite(id) || id <= 0) {
2966
+ console.error(`Invalid prediction id: "${idRaw}"`);
2967
+ process.exit(1);
2968
+ }
2969
+ const stateRaw = typeof flags['state'] === 'string' ? flags['state'].trim() : '';
2970
+ if (!predictionsModule.VALID_CLOSURE_STATES.has(stateRaw) || stateRaw === 'open') {
2971
+ console.error(`Invalid --state: "${stateRaw}". Must be one of: closed | closed-unknown.`);
2972
+ process.exit(1);
2973
+ }
2974
+ const actualRaw = flags['actual'];
2975
+ const actualValue = actualRaw !== undefined ? Number(actualRaw) : undefined;
2976
+ if (actualRaw !== undefined && !Number.isFinite(actualValue)) {
2977
+ console.error(`Invalid --actual: "${actualRaw}". Must be a number.`);
2978
+ process.exit(1);
2979
+ }
2980
+ const noteRaw = flags['note'];
2981
+ const closureNote = typeof noteRaw === 'string' ? noteRaw : undefined;
2982
+ const closed = predictionsModule.closePrediction(hippoRoot, tenantId, id, {
2983
+ closureState: stateRaw,
2984
+ actualValue,
2985
+ closureNote,
2986
+ });
2987
+ console.log(`Prediction ${closed.id} closed: state=${closed.closureState}${closed.actualValue !== null ? ` actual=${closed.actualValue}` : ''}`);
2988
+ return;
2989
+ }
2990
+ if (subcommand === 'list') {
2991
+ const classTagRaw = flags['class'];
2992
+ const classTag = typeof classTagRaw === 'string' ? classTagRaw.trim() : '';
2993
+ const statusRaw = flags['status'];
2994
+ const status = typeof statusRaw === 'string' ? statusRaw.trim() : 'all';
2995
+ const limitRaw = flags['limit'];
2996
+ const limit = limitRaw !== undefined ? parseInt(String(limitRaw), 10) : 100;
2997
+ if (!Number.isFinite(limit) || limit <= 0) {
2998
+ console.error(`Invalid --limit: "${limitRaw}". Must be a positive integer.`);
2999
+ process.exit(1);
3000
+ }
3001
+ let results;
3002
+ if (status === 'open') {
3003
+ results = predictionsModule.loadOpenPredictions(hippoRoot, tenantId, {
3004
+ classTag: classTag || undefined,
3005
+ limit,
3006
+ });
3007
+ }
3008
+ else if (status === 'all') {
3009
+ // No closure-state filter; pull both via loadPredictionsByClass if class given
3010
+ if (classTag) {
3011
+ results = predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, { limit });
3012
+ }
3013
+ else {
3014
+ // No class filter + status=all = pull open + closed across all classes
3015
+ // (kept simple: report open via loadOpenPredictions; closed via two
3016
+ // class scans isn't symmetrical. v1 callers typically pass --class.)
3017
+ results = predictionsModule.loadOpenPredictions(hippoRoot, tenantId, { limit });
3018
+ }
3019
+ }
3020
+ else {
3021
+ if (!predictionsModule.VALID_CLOSURE_STATES.has(status)) {
3022
+ console.error(`Invalid --status: "${status}". Must be one of: open | closed | closed-unknown | all.`);
3023
+ process.exit(1);
3024
+ }
3025
+ if (classTag) {
3026
+ results = predictionsModule.loadPredictionsByClass(hippoRoot, tenantId, classTag, {
3027
+ closureState: status,
3028
+ limit,
3029
+ });
3030
+ }
3031
+ else {
3032
+ // status filter without class — scan all classes is more complex; v1 requires --class for non-default status
3033
+ console.error('--status filter (non-open) requires --class to be set.');
3034
+ process.exit(1);
3035
+ }
3036
+ }
3037
+ if (results.length === 0) {
3038
+ console.log(classTag ? `No predictions in class "${classTag}".` : 'No predictions.');
3039
+ return;
3040
+ }
3041
+ console.log(`Found ${results.length} predictions:\n`);
3042
+ for (const p of results) {
3043
+ const estPart = p.estimateValue !== null ? ` estimate=${p.estimateValue}${p.estimateUnit ? ` ${p.estimateUnit}` : ''}` : '';
3044
+ const actPart = p.actualValue !== null ? ` actual=${p.actualValue}` : '';
3045
+ const tgtPart = p.targetDate ? ` target=${p.targetDate}` : '';
3046
+ console.log(`#${p.id} [${p.closureState}] class=${p.classTag}${estPart}${actPart}${tgtPart}`);
3047
+ console.log(` ${p.claimText}`);
3048
+ if (p.closureNote)
3049
+ console.log(` note: ${p.closureNote}`);
3050
+ }
3051
+ return;
3052
+ }
3053
+ if (subcommand === 'show') {
3054
+ const idRaw = args[1];
3055
+ if (!idRaw) {
3056
+ console.error('Usage: hippo predict show <id>');
3057
+ process.exit(1);
3058
+ }
3059
+ const id = parseInt(String(idRaw), 10);
3060
+ if (!Number.isFinite(id) || id <= 0) {
3061
+ console.error(`Invalid prediction id: "${idRaw}"`);
3062
+ process.exit(1);
3063
+ }
3064
+ const pred = predictionsModule.loadPredictionById(hippoRoot, tenantId, id);
3065
+ if (!pred) {
3066
+ console.error(`Prediction ${id} not found.`);
3067
+ process.exit(1);
3068
+ }
3069
+ console.log(`Prediction #${pred.id}`);
3070
+ console.log(` class: ${pred.classTag}`);
3071
+ console.log(` claim: ${pred.claimText}`);
3072
+ console.log(` state: ${pred.closureState}`);
3073
+ if (pred.estimateValue !== null)
3074
+ console.log(` estimate: ${pred.estimateValue}${pred.estimateUnit ? ' ' + pred.estimateUnit : ''}`);
3075
+ if (pred.targetDate)
3076
+ console.log(` target: ${pred.targetDate}`);
3077
+ if (pred.actualValue !== null)
3078
+ console.log(` actual: ${pred.actualValue}`);
3079
+ if (pred.closedAt)
3080
+ console.log(` closed: ${pred.closedAt}`);
3081
+ if (pred.closureNote)
3082
+ console.log(` note: ${pred.closureNote}`);
3083
+ if (pred.memoryId)
3084
+ console.log(` memory: ${pred.memoryId}`);
3085
+ console.log(` created: ${pred.createdAt}`);
3086
+ return;
3087
+ }
3088
+ if (subcommand === 'baserate') {
3089
+ // J3 reference-class / planning-fallacy detector
3090
+ const classTagRaw = flags['class'];
3091
+ if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
3092
+ console.error('Usage: hippo predict baserate --class <c>');
3093
+ process.exit(1);
3094
+ }
3095
+ const baserate = predictionsModule.computePredictionBaserate(hippoRoot, tenantId, classTagRaw.trim());
3096
+ if (baserate.nClosed === 0) {
3097
+ console.log(`No closed predictions in class "${baserate.classTag}" yet.`);
3098
+ console.log(` Create one with: hippo predict "<claim>" --class ${baserate.classTag} --estimate N`);
3099
+ console.log(` Close it later: hippo predict close <id> --state closed --actual N`);
3100
+ return;
3101
+ }
3102
+ console.log(baserate.summary);
3103
+ console.log(` n_closed: ${baserate.nClosed}`);
3104
+ console.log(` n_ratio_eligible: ${baserate.nRatioEligible}`);
3105
+ if (baserate.meanEstimate !== null)
3106
+ console.log(` mean_estimate: ${baserate.meanEstimate.toFixed(3)}`);
3107
+ if (baserate.meanActual !== null)
3108
+ console.log(` mean_actual: ${baserate.meanActual.toFixed(3)}`);
3109
+ if (baserate.meanRatio !== null)
3110
+ console.log(` mean_ratio: ${baserate.meanRatio.toFixed(3)}x`);
3111
+ if (baserate.p50Ratio !== null)
3112
+ console.log(` p50_ratio: ${baserate.p50Ratio.toFixed(3)}x`);
3113
+ if (baserate.mae !== null)
3114
+ console.log(` mae: ${baserate.mae.toFixed(3)}`);
3115
+ return;
3116
+ }
3117
+ // Default subcommand: create. args[0] is the claim text.
3118
+ const claimText = subcommand;
3119
+ if (!claimText) {
3120
+ console.error('Usage: hippo predict "<claim>" --class <c> [--estimate <v>] [--unit <u>] [--target <YYYY-MM-DD>]');
3121
+ console.error(' hippo predict close <id> --state <closed|closed-unknown> [--actual <v>] [--note "..."]');
3122
+ console.error(' hippo predict list [--class X] [--status open|closed|closed-unknown|all] [--limit N]');
3123
+ console.error(' hippo predict show <id>');
3124
+ process.exit(1);
3125
+ }
3126
+ const classTagRaw = flags['class'];
3127
+ if (typeof classTagRaw !== 'string' || !classTagRaw.trim()) {
3128
+ console.error('--class is required for prediction creation.');
3129
+ process.exit(1);
3130
+ }
3131
+ const classTag = classTagRaw.trim();
3132
+ const estimateRaw = flags['estimate'];
3133
+ const estimateValue = estimateRaw !== undefined ? Number(estimateRaw) : undefined;
3134
+ if (estimateRaw !== undefined && !Number.isFinite(estimateValue)) {
3135
+ console.error(`Invalid --estimate: "${estimateRaw}". Must be a number.`);
3136
+ process.exit(1);
3137
+ }
3138
+ const unitRaw = flags['unit'];
3139
+ const estimateUnit = typeof unitRaw === 'string' ? unitRaw : undefined;
3140
+ const targetRaw = flags['target'];
3141
+ const targetDate = typeof targetRaw === 'string' ? targetRaw : undefined;
3142
+ const created = predictionsModule.savePrediction(hippoRoot, tenantId, {
3143
+ classTag,
3144
+ claimText,
3145
+ estimateValue,
3146
+ estimateUnit,
3147
+ targetDate,
3148
+ });
3149
+ console.log(`Prediction recorded: #${created.id} class=${created.classTag}`);
3150
+ if (created.memoryId)
3151
+ console.log(` memory: ${created.memoryId}`);
3152
+ }
2843
3153
  function cmdCurrent(hippoRoot, args, flags) {
2844
3154
  requireInit(hippoRoot);
2845
3155
  const subcommand = args[0] ?? 'show';
@@ -4244,6 +4554,12 @@ const VALID_AUDIT_OPS = new Set([
4244
4554
  'summary_marked_dirty', // v0.30 / E1 — lockstep with AuditOp union + server.ts VALID_AUDIT_OPS (v1.11.5 CRIT A institutional rule)
4245
4555
  'summary_marked_clean', // v0.30 / E3 — buildDag post-link clean op; lockstep
4246
4556
  'summary_rebuilt', // v0.30 / E3 — sleep-cycle rebuild op; lockstep
4557
+ 'predict_create', // v0.31 / E2 prediction first-class object — emitted by savePrediction
4558
+ 'predict_close', // v0.31 / E2 — emitted by closePrediction
4559
+ 'predict_baserate', // v0.31 / J3 — emitted by computePredictionBaserate
4560
+ 'recall_autodebias_hint', // v0.32 / J3.2 — emitted by computePlanningFallacyHint on success
4561
+ 'recall_autodebias_hint_no_class_match', // v0.32 / J3.2 — telemetry: forward-claim, no class scored
4562
+ 'recall_autodebias_hint_tiebreak', // v0.32 / J3.2 — telemetry: forward-claim, >=2 classes tied
4247
4563
  ]);
4248
4564
  function formatAuditRow(ev) {
4249
4565
  const target = ev.targetId ?? '-';
@@ -5426,6 +5742,9 @@ async function main() {
5426
5742
  case 'handoff':
5427
5743
  cmdHandoff(hippoRoot, args, flags);
5428
5744
  break;
5745
+ case 'predict':
5746
+ cmdPredict(hippoRoot, args, flags);
5747
+ break;
5429
5748
  case 'current':
5430
5749
  cmdCurrent(hippoRoot, args, flags);
5431
5750
  break;