hippo-memory 0.30.0 → 0.31.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/README.md CHANGED
@@ -60,6 +60,18 @@ hippo recall "data pipeline issues" --budget 2000
60
60
 
61
61
  ---
62
62
 
63
+ ### What's new in v0.31.0
64
+
65
+ - **Scope-aware corrections.** Tag a memory with `hippo remember --scope plan-eng-review` and it only surfaces strongly when that scope is active again. Matching scope gets 1.5x boost, mismatching scope is suppressed 0.5x, unscoped memories stay neutral. Corrections said during one skill stop polluting unrelated contexts.
66
+ - **Auto-detect from env.** `HIPPO_SCOPE`, `GSTACK_SKILL`, `OPENCLAW_SKILL` populate the scope automatically. Explicit `--scope` on any command overrides.
67
+ - **`hippo explain --why`** now shows the `scope:` multiplier when it fires, so you can see why a memory got ranked up or down.
68
+
69
+ ### What's new in v0.30.1
70
+
71
+ - **`hippo recall --layer <L>` is now a strict filter.** Previously the flag was accepted but silently dropped; other layers leaked into results. The RSI demo's `recall --layer trace` now does what it says.
72
+ - **`hippo status` prints a `Trace:` counter.** The new layer is visible in status output.
73
+ - **`hippo --version` / `-v`** works as expected. Previously errored.
74
+
63
75
  ### What's new in v0.30.0
64
76
 
65
77
  - **Sequence binding for recursive-self-improvement agents.** New `Layer.Trace` memories store ordered `A → B → C → outcome` traces. Agents can `hippo trace record` explicitly, or just call `hippo session complete --outcome success` and let `hippo sleep` auto-promote completed sessions into queryable traces.
package/dist/cli.js CHANGED
@@ -28,6 +28,7 @@
28
28
  import * as path from 'path';
29
29
  import * as fs from 'fs';
30
30
  import * as os from 'os';
31
+ import { fileURLToPath } from 'node:url';
31
32
  import { execFileSync, execSync, spawn } from 'child_process';
32
33
  import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, } from './hooks.js';
33
34
  import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, applyOutcome, computeSchemaFit, Layer, DECISION_HALF_LIFE_DAYS, } from './memory.js';
@@ -43,6 +44,7 @@ import { openHippoDb, closeHippoDb } from './db.js';
43
44
  import { captureError, extractLessons, deduplicateLesson, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
44
45
  import { extractInvalidationTarget, invalidateMatching } from './invalidation.js';
45
46
  import { extractPathTags } from './path-context.js';
47
+ import { detectScope, scopeMatch } from './scope.js';
46
48
  import { getGlobalRoot, initGlobal, promoteToGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
47
49
  import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
48
50
  import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, } from './importers.js';
@@ -412,6 +414,14 @@ function cmdRemember(hippoRoot, text, flags) {
412
414
  if (!entry.tags.includes(pt))
413
415
  entry.tags.push(pt);
414
416
  }
417
+ // Scope tagging: explicit --scope or auto-detected
418
+ const explicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
419
+ const activeScope = explicitScope || detectScope();
420
+ if (activeScope) {
421
+ const scopeTag = `scope:${activeScope}`;
422
+ if (!entry.tags.includes(scopeTag))
423
+ entry.tags.push(scopeTag);
424
+ }
415
425
  writeEntry(targetRoot, entry);
416
426
  updateStats(targetRoot, { remembered: 1 });
417
427
  const prefix = useGlobal ? '[global] ' : '';
@@ -458,6 +468,8 @@ async function cmdRecall(hippoRoot, query, flags) {
458
468
  const minResults = flags['min-results'] !== undefined
459
469
  ? parseInt(String(flags['min-results']), 10)
460
470
  : undefined;
471
+ const recallExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
472
+ const recallActiveScope = recallExplicitScope || detectScope();
461
473
  let results;
462
474
  if (usePhysics && !hasGlobal) {
463
475
  results = await physicsSearch(query, localEntries, {
@@ -465,17 +477,18 @@ async function cmdRecall(hippoRoot, query, flags) {
465
477
  hippoRoot,
466
478
  physicsConfig: config.physics,
467
479
  minResults,
480
+ scope: recallActiveScope,
468
481
  });
469
482
  }
470
483
  else if (hasGlobal) {
471
484
  // Use searchBothHybrid for merged results with embedding support
472
485
  results = await searchBothHybrid(query, hippoRoot, globalRoot, {
473
- budget, mmr: mmrEnabled, mmrLambda, localBump, minResults,
486
+ budget, mmr: mmrEnabled, mmrLambda, localBump, minResults, scope: recallActiveScope,
474
487
  });
475
488
  }
476
489
  else {
477
490
  results = await hybridSearch(query, localEntries, {
478
- budget, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults,
491
+ budget, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
479
492
  });
480
493
  }
481
494
  // --outcome filter: drop trace entries whose trace_outcome !== target.
@@ -494,6 +507,16 @@ async function cmdRecall(hippoRoot, query, flags) {
494
507
  return r.entry.trace_outcome === outcomeFilter;
495
508
  });
496
509
  }
510
+ // --layer filter: strict, drops entries whose layer does not match.
511
+ const layerFilter = flags['layer'] !== undefined ? String(flags['layer']).trim() : '';
512
+ if (layerFilter) {
513
+ const validLayers = Object.values(Layer);
514
+ if (!validLayers.includes(layerFilter)) {
515
+ console.error(`Invalid --layer: "${layerFilter}". Must be one of: ${validLayers.join(', ')}.`);
516
+ process.exit(1);
517
+ }
518
+ results = results.filter((r) => r.entry.layer === layerFilter);
519
+ }
497
520
  if (limit < results.length) {
498
521
  results = results.slice(0, limit);
499
522
  }
@@ -591,6 +614,8 @@ async function cmdExplain(hippoRoot, query, flags) {
591
614
  : flags['local-bump'] !== undefined
592
615
  ? parseFloat(String(flags['local-bump']))
593
616
  : config.search.localBump;
617
+ const explainExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
618
+ const explainActiveScope = explainExplicitScope || detectScope();
594
619
  let results;
595
620
  let modeUsed;
596
621
  if (usePhysics && !hasGlobal) {
@@ -599,18 +624,19 @@ async function cmdExplain(hippoRoot, query, flags) {
599
624
  hippoRoot,
600
625
  physicsConfig: config.physics,
601
626
  explain: true,
627
+ scope: explainActiveScope,
602
628
  });
603
629
  modeUsed = 'physics';
604
630
  }
605
631
  else if (hasGlobal) {
606
632
  results = await searchBothHybrid(query, hippoRoot, globalRoot, {
607
- budget, explain: true, mmr: mmrEnabled, mmrLambda, localBump,
633
+ budget, explain: true, mmr: mmrEnabled, mmrLambda, localBump, scope: explainActiveScope,
608
634
  });
609
635
  modeUsed = 'searchBothHybrid';
610
636
  }
611
637
  else {
612
638
  results = await hybridSearch(query, localEntries, {
613
- budget, hippoRoot, explain: true, mmr: mmrEnabled, mmrLambda,
639
+ budget, hippoRoot, explain: true, mmr: mmrEnabled, mmrLambda, scope: explainActiveScope,
614
640
  });
615
641
  modeUsed = 'hybrid';
616
642
  }
@@ -680,6 +706,8 @@ async function cmdExplain(hippoRoot, query, flags) {
680
706
  console.log(` recency: x${fmt(b.recencyMultiplier, 3)} (age=${b.ageDays}d)`);
681
707
  if (b.decisionBoost !== 1)
682
708
  console.log(` decision: x${fmt(b.decisionBoost, 2)} (tagged 'decision')`);
709
+ if (b.scopeBoost !== 1)
710
+ console.log(` scope: x${fmt(b.scopeBoost, 2)} (scope tag ${b.scopeBoost > 1 ? 'match' : 'mismatch'})`);
683
711
  if (b.pathBoost !== 1)
684
712
  console.log(` path: x${fmt(b.pathBoost, 3)} (cwd path tag overlap)`);
685
713
  if (b.sourceBump !== 1)
@@ -1586,6 +1614,7 @@ function cmdStatus(hippoRoot) {
1586
1614
  console.log(` Buffer: ${byLayer[Layer.Buffer]}`);
1587
1615
  console.log(` Episodic: ${byLayer[Layer.Episodic]}`);
1588
1616
  console.log(` Semantic: ${byLayer[Layer.Semantic]}`);
1617
+ console.log(` Trace: ${byLayer[Layer.Trace]}`);
1589
1618
  const conflictCount = listMemoryConflicts(hippoRoot).length;
1590
1619
  console.log(`Pinned: ${pinned}`);
1591
1620
  console.log(`At risk (<0.2): ${atRisk}`);
@@ -2178,6 +2207,8 @@ async function cmdContext(hippoRoot, args, flags) {
2178
2207
  }
2179
2208
  const budget = parseInt(String(flags['budget'] ?? '1500'), 10);
2180
2209
  const limit = parseLimitFlag(flags['limit']);
2210
+ const ctxExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
2211
+ const ctxActiveScope = ctxExplicitScope || detectScope();
2181
2212
  // If budget is 0, skip entirely (zero token cost)
2182
2213
  if (budget <= 0)
2183
2214
  return;
@@ -2227,12 +2258,16 @@ async function cmdContext(hippoRoot, args, flags) {
2227
2258
  ...pinnedLocal.map((e) => ({ entry: e, isGlobal: false })),
2228
2259
  ...pinnedGlobal.map((e) => ({ entry: e, isGlobal: true })),
2229
2260
  ]
2230
- .map(({ entry, isGlobal }) => ({
2231
- entry,
2232
- score: calculateStrength(entry, nowP) * (isGlobal ? 1 / 1.2 : 1),
2233
- tokens: estimateTokens(entry.content),
2234
- isGlobal,
2235
- }))
2261
+ .map(({ entry, isGlobal }) => {
2262
+ const scopeSig = scopeMatch(entry.tags, ctxActiveScope);
2263
+ const sBst = scopeSig === 1 ? 1.5 : scopeSig === -1 ? 0.5 : 1.0;
2264
+ return {
2265
+ entry,
2266
+ score: calculateStrength(entry, nowP) * (isGlobal ? 1 / 1.2 : 1) * sBst,
2267
+ tokens: estimateTokens(entry.content),
2268
+ isGlobal,
2269
+ };
2270
+ })
2236
2271
  .sort((a, b) => b.score - a.score);
2237
2272
  let usedP = 0;
2238
2273
  for (const r of rankedPinned) {
@@ -2275,7 +2310,7 @@ async function cmdContext(hippoRoot, args, flags) {
2275
2310
  else {
2276
2311
  let results;
2277
2312
  if (hasGlobal) {
2278
- const merged = await searchBothHybrid(query, hippoRoot, globalRoot, { budget });
2313
+ const merged = await searchBothHybrid(query, hippoRoot, globalRoot, { budget, scope: ctxActiveScope });
2279
2314
  const localIndex = loadIndex(hippoRoot);
2280
2315
  results = merged.map((r) => ({
2281
2316
  entry: r.entry,
@@ -2288,8 +2323,8 @@ async function cmdContext(hippoRoot, args, flags) {
2288
2323
  const ctxConfig = loadConfig(hippoRoot);
2289
2324
  const usePhysicsCtx = ctxConfig.physics?.enabled !== false;
2290
2325
  const ctxResults = usePhysicsCtx
2291
- ? await physicsSearch(query, localEntries, { budget, hippoRoot, physicsConfig: ctxConfig.physics })
2292
- : await hybridSearch(query, localEntries, { budget, hippoRoot });
2326
+ ? await physicsSearch(query, localEntries, { budget, hippoRoot, physicsConfig: ctxConfig.physics, scope: ctxActiveScope })
2327
+ : await hybridSearch(query, localEntries, { budget, hippoRoot, scope: ctxActiveScope });
2293
2328
  results = ctxResults.map((r) => ({
2294
2329
  entry: r.entry,
2295
2330
  score: r.score,
@@ -3468,6 +3503,21 @@ Examples:
3468
3503
  const { command, args, flags } = parseArgs(process.argv);
3469
3504
  const hippoRoot = getHippoRoot(process.cwd());
3470
3505
  async function main() {
3506
+ if (command === '--version' || command === '-v' || flags['version']) {
3507
+ const __filename_local = fileURLToPath(import.meta.url);
3508
+ const __dirname_local = path.dirname(__filename_local);
3509
+ const pkgPath = path.join(__dirname_local, '..', 'package.json');
3510
+ let pkgJson;
3511
+ try {
3512
+ pkgJson = fs.readFileSync(pkgPath, 'utf-8');
3513
+ }
3514
+ catch {
3515
+ pkgJson = fs.readFileSync(path.join(__dirname_local, '..', '..', 'package.json'), 'utf-8');
3516
+ }
3517
+ const { version } = JSON.parse(pkgJson);
3518
+ console.log(version);
3519
+ process.exit(0);
3520
+ }
3471
3521
  maybeAutoInstallCodexWrapper(command, args);
3472
3522
  switch (command) {
3473
3523
  case 'init':