hippo-memory 0.30.1 → 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,12 @@ 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
+
63
69
  ### What's new in v0.30.1
64
70
 
65
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.
package/dist/cli.js CHANGED
@@ -44,6 +44,7 @@ import { openHippoDb, closeHippoDb } from './db.js';
44
44
  import { captureError, extractLessons, deduplicateLesson, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
45
45
  import { extractInvalidationTarget, invalidateMatching } from './invalidation.js';
46
46
  import { extractPathTags } from './path-context.js';
47
+ import { detectScope, scopeMatch } from './scope.js';
47
48
  import { getGlobalRoot, initGlobal, promoteToGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
48
49
  import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
49
50
  import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, } from './importers.js';
@@ -413,6 +414,14 @@ function cmdRemember(hippoRoot, text, flags) {
413
414
  if (!entry.tags.includes(pt))
414
415
  entry.tags.push(pt);
415
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
+ }
416
425
  writeEntry(targetRoot, entry);
417
426
  updateStats(targetRoot, { remembered: 1 });
418
427
  const prefix = useGlobal ? '[global] ' : '';
@@ -459,6 +468,8 @@ async function cmdRecall(hippoRoot, query, flags) {
459
468
  const minResults = flags['min-results'] !== undefined
460
469
  ? parseInt(String(flags['min-results']), 10)
461
470
  : undefined;
471
+ const recallExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
472
+ const recallActiveScope = recallExplicitScope || detectScope();
462
473
  let results;
463
474
  if (usePhysics && !hasGlobal) {
464
475
  results = await physicsSearch(query, localEntries, {
@@ -466,17 +477,18 @@ async function cmdRecall(hippoRoot, query, flags) {
466
477
  hippoRoot,
467
478
  physicsConfig: config.physics,
468
479
  minResults,
480
+ scope: recallActiveScope,
469
481
  });
470
482
  }
471
483
  else if (hasGlobal) {
472
484
  // Use searchBothHybrid for merged results with embedding support
473
485
  results = await searchBothHybrid(query, hippoRoot, globalRoot, {
474
- budget, mmr: mmrEnabled, mmrLambda, localBump, minResults,
486
+ budget, mmr: mmrEnabled, mmrLambda, localBump, minResults, scope: recallActiveScope,
475
487
  });
476
488
  }
477
489
  else {
478
490
  results = await hybridSearch(query, localEntries, {
479
- budget, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults,
491
+ budget, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
480
492
  });
481
493
  }
482
494
  // --outcome filter: drop trace entries whose trace_outcome !== target.
@@ -602,6 +614,8 @@ async function cmdExplain(hippoRoot, query, flags) {
602
614
  : flags['local-bump'] !== undefined
603
615
  ? parseFloat(String(flags['local-bump']))
604
616
  : config.search.localBump;
617
+ const explainExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
618
+ const explainActiveScope = explainExplicitScope || detectScope();
605
619
  let results;
606
620
  let modeUsed;
607
621
  if (usePhysics && !hasGlobal) {
@@ -610,18 +624,19 @@ async function cmdExplain(hippoRoot, query, flags) {
610
624
  hippoRoot,
611
625
  physicsConfig: config.physics,
612
626
  explain: true,
627
+ scope: explainActiveScope,
613
628
  });
614
629
  modeUsed = 'physics';
615
630
  }
616
631
  else if (hasGlobal) {
617
632
  results = await searchBothHybrid(query, hippoRoot, globalRoot, {
618
- budget, explain: true, mmr: mmrEnabled, mmrLambda, localBump,
633
+ budget, explain: true, mmr: mmrEnabled, mmrLambda, localBump, scope: explainActiveScope,
619
634
  });
620
635
  modeUsed = 'searchBothHybrid';
621
636
  }
622
637
  else {
623
638
  results = await hybridSearch(query, localEntries, {
624
- budget, hippoRoot, explain: true, mmr: mmrEnabled, mmrLambda,
639
+ budget, hippoRoot, explain: true, mmr: mmrEnabled, mmrLambda, scope: explainActiveScope,
625
640
  });
626
641
  modeUsed = 'hybrid';
627
642
  }
@@ -691,6 +706,8 @@ async function cmdExplain(hippoRoot, query, flags) {
691
706
  console.log(` recency: x${fmt(b.recencyMultiplier, 3)} (age=${b.ageDays}d)`);
692
707
  if (b.decisionBoost !== 1)
693
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'})`);
694
711
  if (b.pathBoost !== 1)
695
712
  console.log(` path: x${fmt(b.pathBoost, 3)} (cwd path tag overlap)`);
696
713
  if (b.sourceBump !== 1)
@@ -2190,6 +2207,8 @@ async function cmdContext(hippoRoot, args, flags) {
2190
2207
  }
2191
2208
  const budget = parseInt(String(flags['budget'] ?? '1500'), 10);
2192
2209
  const limit = parseLimitFlag(flags['limit']);
2210
+ const ctxExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
2211
+ const ctxActiveScope = ctxExplicitScope || detectScope();
2193
2212
  // If budget is 0, skip entirely (zero token cost)
2194
2213
  if (budget <= 0)
2195
2214
  return;
@@ -2239,12 +2258,16 @@ async function cmdContext(hippoRoot, args, flags) {
2239
2258
  ...pinnedLocal.map((e) => ({ entry: e, isGlobal: false })),
2240
2259
  ...pinnedGlobal.map((e) => ({ entry: e, isGlobal: true })),
2241
2260
  ]
2242
- .map(({ entry, isGlobal }) => ({
2243
- entry,
2244
- score: calculateStrength(entry, nowP) * (isGlobal ? 1 / 1.2 : 1),
2245
- tokens: estimateTokens(entry.content),
2246
- isGlobal,
2247
- }))
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
+ })
2248
2271
  .sort((a, b) => b.score - a.score);
2249
2272
  let usedP = 0;
2250
2273
  for (const r of rankedPinned) {
@@ -2287,7 +2310,7 @@ async function cmdContext(hippoRoot, args, flags) {
2287
2310
  else {
2288
2311
  let results;
2289
2312
  if (hasGlobal) {
2290
- const merged = await searchBothHybrid(query, hippoRoot, globalRoot, { budget });
2313
+ const merged = await searchBothHybrid(query, hippoRoot, globalRoot, { budget, scope: ctxActiveScope });
2291
2314
  const localIndex = loadIndex(hippoRoot);
2292
2315
  results = merged.map((r) => ({
2293
2316
  entry: r.entry,
@@ -2300,8 +2323,8 @@ async function cmdContext(hippoRoot, args, flags) {
2300
2323
  const ctxConfig = loadConfig(hippoRoot);
2301
2324
  const usePhysicsCtx = ctxConfig.physics?.enabled !== false;
2302
2325
  const ctxResults = usePhysicsCtx
2303
- ? await physicsSearch(query, localEntries, { budget, hippoRoot, physicsConfig: ctxConfig.physics })
2304
- : 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 });
2305
2328
  results = ctxResults.map((r) => ({
2306
2329
  entry: r.entry,
2307
2330
  score: r.score,