hippo-memory 0.33.0 → 0.34.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 (56) hide show
  1. package/README.md +8 -0
  2. package/dist/ambient.d.ts +26 -0
  3. package/dist/ambient.d.ts.map +1 -0
  4. package/dist/ambient.js +147 -0
  5. package/dist/ambient.js.map +1 -0
  6. package/dist/capture.js +4 -0
  7. package/dist/capture.js.map +1 -1
  8. package/dist/cli.js +338 -21
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +10 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +12 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/db.d.ts.map +1 -1
  15. package/dist/db.js +110 -1
  16. package/dist/db.js.map +1 -1
  17. package/dist/eval-suite.d.ts +82 -0
  18. package/dist/eval-suite.d.ts.map +1 -0
  19. package/dist/eval-suite.js +289 -0
  20. package/dist/eval-suite.js.map +1 -0
  21. package/dist/importers.d.ts.map +1 -1
  22. package/dist/importers.js +5 -0
  23. package/dist/importers.js.map +1 -1
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +6 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/mcp/framing.d.ts +12 -0
  29. package/dist/mcp/framing.d.ts.map +1 -0
  30. package/dist/mcp/framing.js +45 -0
  31. package/dist/mcp/framing.js.map +1 -0
  32. package/dist/mcp/server.js +28 -33
  33. package/dist/mcp/server.js.map +1 -1
  34. package/dist/memory.d.ts +9 -0
  35. package/dist/memory.d.ts.map +1 -1
  36. package/dist/memory.js +4 -0
  37. package/dist/memory.js.map +1 -1
  38. package/dist/raw-archive.d.ts +16 -0
  39. package/dist/raw-archive.d.ts.map +1 -0
  40. package/dist/raw-archive.js +53 -0
  41. package/dist/raw-archive.js.map +1 -0
  42. package/dist/salience.d.ts +22 -0
  43. package/dist/salience.d.ts.map +1 -0
  44. package/dist/salience.js +74 -0
  45. package/dist/salience.js.map +1 -0
  46. package/dist/search.d.ts +9 -0
  47. package/dist/search.d.ts.map +1 -1
  48. package/dist/search.js +8 -0
  49. package/dist/search.js.map +1 -1
  50. package/dist/store.d.ts.map +1 -1
  51. package/dist/store.js +35 -8
  52. package/dist/store.js.map +1 -1
  53. package/extensions/openclaw-plugin/openclaw.plugin.json +46 -46
  54. package/extensions/openclaw-plugin/package.json +13 -13
  55. package/openclaw.plugin.json +45 -45
  56. package/package.json +74 -73
package/dist/cli.js CHANGED
@@ -51,9 +51,12 @@ import { importChatGPT, importClaude, importCursor, importGenericFile, importMar
51
51
  import { cmdCapture } from './capture.js';
52
52
  import { auditMemories } from './audit.js';
53
53
  import { runEval, bootstrapCorpus, compareSummaries } from './eval.js';
54
+ import { runFeatureEval, formatResult, resultToBaseline, detectRegressions } from './eval-suite.js';
54
55
  import { refineStore } from './refine-llm.js';
55
56
  import { wmPush, wmRead, wmClear, wmFlush } from './working-memory.js';
56
57
  import { multihopSearch } from './multihop.js';
58
+ import { computeSalience } from './salience.js';
59
+ import { computeAmbientState, renderAmbientSummary } from './ambient.js';
57
60
  // ---------------------------------------------------------------------------
58
61
  // Helpers
59
62
  // ---------------------------------------------------------------------------
@@ -401,6 +404,23 @@ async function cmdRemember(hippoRoot, text, flags) {
401
404
  // Compute schema fit against existing memories
402
405
  const existing = loadAllEntries(targetRoot);
403
406
  const schemaFit = computeSchemaFit(text, rawTags, existing);
407
+ // A3 envelope flags
408
+ const kindFlagRaw = typeof flags['kind'] === 'string' ? flags['kind'] : undefined;
409
+ const kindFlag = kindFlagRaw === undefined ? undefined : kindFlagRaw.toLowerCase();
410
+ // CLI surface intentionally restricted: 'raw' is reserved for ingestion connectors
411
+ // (E1.x: Slack/Jira/Gmail) that route deletions through archiveRawMemory. Existing
412
+ // forget/consolidate/conflict-resolve paths abort on kind='raw' via the append-only
413
+ // trigger, so exposing --kind raw here would create unforgettable memories.
414
+ // 'archived' is an internal sentinel set only inside archiveRawMemory's transaction.
415
+ const userVisibleKinds = ['distilled', 'superseded'];
416
+ if (kindFlag !== undefined && !userVisibleKinds.includes(kindFlag)) {
417
+ console.error(`Invalid --kind: "${kindFlagRaw}". Must be one of: ${userVisibleKinds.join(', ')}`);
418
+ console.error(`(kind='raw' is reserved for ingestion connectors; kind='archived' is internal.)`);
419
+ process.exit(1);
420
+ }
421
+ const ownerFlag = typeof flags['owner'] === 'string' ? flags['owner'] : null;
422
+ const artifactRefFlag = typeof flags['artifact-ref'] === 'string' ? flags['artifact-ref'] : null;
423
+ const scopeForEnvelope = typeof flags['scope'] === 'string' ? flags['scope'].trim() || null : null;
404
424
  const entry = createMemory(text, {
405
425
  layer: Layer.Episodic,
406
426
  tags: rawTags,
@@ -408,6 +428,10 @@ async function cmdRemember(hippoRoot, text, flags) {
408
428
  source: useGlobal ? 'cli-global' : 'cli',
409
429
  confidence,
410
430
  schema_fit: schemaFit,
431
+ kind: kindFlag,
432
+ scope: scopeForEnvelope,
433
+ owner: ownerFlag,
434
+ artifact_ref: artifactRefFlag,
411
435
  });
412
436
  // Auto-tag with path context
413
437
  const pathTags = extractPathTags(process.cwd());
@@ -423,6 +447,25 @@ async function cmdRemember(hippoRoot, text, flags) {
423
447
  if (!entry.tags.includes(scopeTag))
424
448
  entry.tags.push(scopeTag);
425
449
  }
450
+ // Salience gate: decide if this memory is worth storing
451
+ const rememberConfig = loadConfig(targetRoot);
452
+ if (rememberConfig.salience.enabled && !Boolean(flags['pin']) && !Boolean(flags['force'])) {
453
+ const salienceResult = computeSalience(text, entry.tags, existing, {
454
+ recentWindow: rememberConfig.salience.recentWindow,
455
+ overlapThreshold: rememberConfig.salience.overlapThreshold,
456
+ minContentLength: rememberConfig.salience.minContentLength,
457
+ maxRepeatErrors: rememberConfig.salience.maxRepeatErrors,
458
+ });
459
+ if (salienceResult.decision === 'skip') {
460
+ console.log(`Skipped (salience: ${salienceResult.reason}, score ${salienceResult.score.toFixed(2)})`);
461
+ return;
462
+ }
463
+ if (salienceResult.decision === 'start_weak') {
464
+ entry.strength = salienceResult.score;
465
+ entry.half_life_days = Math.max(1, entry.half_life_days * 0.5);
466
+ console.log(`Weakened (salience: ${salienceResult.reason}, strength ${salienceResult.score.toFixed(2)})`);
467
+ }
468
+ }
426
469
  writeEntry(targetRoot, entry);
427
470
  updateStats(targetRoot, { remembered: 1 });
428
471
  const prefix = useGlobal ? '[global] ' : '';
@@ -590,6 +633,155 @@ async function cmdRecall(hippoRoot, query, flags) {
590
633
  budget, hippoRoot, mmr: mmrEnabled, mmrLambda, minResults, scope: recallActiveScope,
591
634
  });
592
635
  }
636
+ // ACC EVC-adaptive recall (RESEARCH.md §PFC.ACC). When the initial top-K is
637
+ // dominated by lexically similar but distinct memories (high pairwise token
638
+ // overlap = same topic, different facts = conflict), allocate extra retrieval
639
+ // effort: take a wider candidate pool, drop low-relevance distractors, and
640
+ // re-rank by recency to surface the most up-to-date item from the cluster.
641
+ // Default off; opt-in via --evc-adaptive.
642
+ if (flags['evc-adaptive'] && results.length >= 2) {
643
+ const sliceSize = Math.min(3, results.length);
644
+ const slice = results.slice(0, sliceSize);
645
+ let pairs = 0;
646
+ let overlapSum = 0;
647
+ for (let i = 0; i < slice.length; i++) {
648
+ for (let j = i + 1; j < slice.length; j++) {
649
+ overlapSum += textOverlap(slice[i].entry.content, slice[j].entry.content);
650
+ pairs++;
651
+ }
652
+ }
653
+ const avgOverlap = pairs > 0 ? overlapSum / pairs : 0;
654
+ if (avgOverlap >= 0.4) {
655
+ const poolSize = Math.min(results.length, Math.max(sliceSize * 3, 9));
656
+ const pool = results.slice(0, poolSize);
657
+ const tail = results.slice(poolSize);
658
+ const maxScore = pool.reduce((m, r) => Math.max(m, r.score), 0);
659
+ const scoreFloor = maxScore * 0.5;
660
+ const onTopic = [];
661
+ const offTopic = [];
662
+ for (const r of pool) {
663
+ (r.score >= scoreFloor ? onTopic : offTopic).push(r);
664
+ }
665
+ onTopic.sort((a, b) => {
666
+ const ta = new Date(a.entry.created).getTime();
667
+ const tb = new Date(b.entry.created).getTime();
668
+ return tb - ta;
669
+ });
670
+ results = [...onTopic, ...offTopic, ...tail];
671
+ }
672
+ }
673
+ // vlPFC interference filter (RESEARCH.md §PFC.vlPFC). Suppress task-irrelevant
674
+ // memories using *recorded* supersession + conflict structure only. Default
675
+ // off; opt-in via --filter-conflicts. Two effects, both surgical:
676
+ // 1. Drop entries with `superseded_by` set. (No-op under default recall,
677
+ // which already filters them; matters when `--include-superseded` was
678
+ // passed. The flag re-asserts the gate.)
679
+ // 2. Apply a 0.3x score multiplier to entries whose `conflicts_with` list
680
+ // references another entry that ALSO appears in the result set. The
681
+ // multiplier is conservative — we never delete on conflict, only
682
+ // down-rank, so the user can still surface the loser via --include-*.
683
+ // We never infer conflicts from lexical overlap. The v1 salience gate did
684
+ // that and destroyed LoCoMo (0.28 → 0.02). Recorded structure only.
685
+ if (flags['filter-conflicts']) {
686
+ results = results.filter((r) => !r.entry.superseded_by);
687
+ const presentIds = new Set(results.map((r) => r.entry.id));
688
+ results = results.map((r) => {
689
+ const peers = r.entry.conflicts_with || [];
690
+ const hasPeerInResults = peers.some((peerId) => presentIds.has(peerId));
691
+ return hasPeerInResults ? { ...r, score: r.score * 0.3 } : r;
692
+ });
693
+ results.sort((a, b) => b.score - a.score);
694
+ }
695
+ // vmPFC continuous value attribution (RESEARCH.md §PFC.vmPFC). Continuous
696
+ // value scoring per memory based on cumulative outcome attribution. Memories
697
+ // with positive cumulative outcomes are boosted; those with negative outcomes
698
+ // are demoted. The multiplier is a tanh-shaped function clamped to [0.7, 1.3]
699
+ // — wider than the always-on outcomeBoost (which clamps [0.85, 1.15]) so this
700
+ // flag has additional decisive effect when value attribution should drive
701
+ // ranking. Default off; opt-in via --value-aware. Reuses outcome_positive /
702
+ // outcome_negative columns; no schema change.
703
+ if (flags['value-aware'] && results.length >= 1) {
704
+ results = results.map((r) => {
705
+ const pos = r.entry.outcome_positive ?? 0;
706
+ const neg = r.entry.outcome_negative ?? 0;
707
+ if (pos === 0 && neg === 0)
708
+ return r;
709
+ const raw = 1 + 0.3 * Math.tanh(pos - neg);
710
+ const valueMult = Math.max(0.7, Math.min(1.3, raw));
711
+ return { ...r, score: r.score * valueMult };
712
+ });
713
+ results.sort((a, b) => b.score - a.score);
714
+ }
715
+ // OFC option-value re-ranker MVP (RESEARCH.md §PFC.OFC). Combine relevance,
716
+ // strength, and integration cost into a single utility score and re-sort.
717
+ // OFC neurons encode a "common currency" across heterogeneous attributes
718
+ // (Rangel et al., 2008); this is the simplest demonstration of that mechanism.
719
+ // Default off; opt-in via --rerank-utility.
720
+ //
721
+ // utility = score * (0.5 + 0.5 * strength) * (1 - cost_factor)
722
+ // cost_factor = min(0.3, tokens / 10000)
723
+ //
724
+ // The full OFC spec (option_valuation table in RESEARCH.md) decomposes value
725
+ // into reward / cost / risk / confidence components. The MVP collapses these
726
+ // to: score (relevance proxy), strength (persistence proxy), tokens (cost).
727
+ // CAVEAT: cost penalty is monotone with token count; LoCoMo's harder QAs
728
+ // often live in long evidence-rich memories. Default off — needs LoCoMo
729
+ // eval before enabling broadly.
730
+ if (flags['rerank-utility']) {
731
+ results = results
732
+ .map((r) => {
733
+ const strength = typeof r.entry.strength === 'number' ? r.entry.strength : 1.0;
734
+ const costFactor = Math.min(0.3, (r.tokens || 0) / 10000);
735
+ const utility = r.score * (0.5 + 0.5 * strength) * (1 - costFactor);
736
+ return { ...r, score: utility };
737
+ })
738
+ .sort((a, b) => b.score - a.score);
739
+ }
740
+ // dlPFC goal-conditioned recall MVP (RESEARCH.md §PFC.dlPFC). When --goal
741
+ // <tag> is set, memories whose `tags` array contains the goal tag receive
742
+ // a 1.5x score boost and results are re-sorted. The full dlPFC spec
743
+ // (goal_stack + retrieval_policy tables) maintains a hierarchical task
744
+ // stack with weighted retrieval policies; this MVP collapses that to a
745
+ // single-tag boost — the smallest demonstrable goal-conditioning signal.
746
+ // Default off; opt-in via --goal <tag>. No schema change.
747
+ const goalTag = flags['goal'] !== undefined ? String(flags['goal']).trim() : '';
748
+ if (goalTag) {
749
+ results = results
750
+ .map((r) => (r.entry.tags?.includes(goalTag) ? { ...r, score: r.score * 1.5 } : r))
751
+ .sort((a, b) => b.score - a.score);
752
+ }
753
+ // Pineal salience MVP (RESEARCH.md §"AI Pineal Gland — Intuition and Awareness
754
+ // Module"). When --salience-threshold T is set (T > 0), memories whose
755
+ // retrieval_count is below T are downweighted: score *= max(0.5, count / T).
756
+ // At or above T, no change. This makes salience emerge from USE — high-recall
757
+ // memories earn full ranking weight, low-recall memories are softly demoted.
758
+ //
759
+ // CRITICAL HISTORY: The v1 salience gate (60% lexical-overlap gate at memory
760
+ // CREATION time) destroyed LoCoMo recall (0.28 -> 0.02) by dropping same-
761
+ // session relevant turns at intake. See MEMORY.md "Hippo salience gate
762
+ // destroys benchmark recall". This v2 is the inverse:
763
+ // - retrieval-side only (no creation-time gating)
764
+ // - retrieval_count signal only (no lexical overlap, no novelty heuristic)
765
+ // - default OFF, opt-in via the flag (no behaviour change without it)
766
+ // - 0.5 floor so non-salient entries stay reachable, never dropped
767
+ // Reuses the existing retrieval_count column; no schema change.
768
+ const salienceThresholdRaw = flags['salience-threshold'];
769
+ if (salienceThresholdRaw !== undefined) {
770
+ const T = Number(salienceThresholdRaw);
771
+ if (!Number.isFinite(T) || T <= 0) {
772
+ console.error(`Invalid --salience-threshold: "${salienceThresholdRaw}". Must be a positive number.`);
773
+ process.exit(1);
774
+ }
775
+ results = results
776
+ .map((r) => {
777
+ const count = r.entry.retrieval_count ?? 0;
778
+ if (count >= T)
779
+ return r;
780
+ const mult = Math.max(0.5, count / T);
781
+ return { ...r, score: r.score * mult };
782
+ })
783
+ .sort((a, b) => b.score - a.score);
784
+ }
593
785
  // --outcome filter: drop trace entries whose trace_outcome !== target.
594
786
  // Non-trace entries pass through unaffected (traces are the only layer with
595
787
  // a meaningful outcome; filtering non-traces by outcome would be incoherent).
@@ -665,6 +857,9 @@ async function cmdRecall(hippoRoot, query, flags) {
665
857
  base.reason = explanation.reason;
666
858
  base.bm25 = r.bm25;
667
859
  base.cosine = r.cosine;
860
+ if (explanation.envelope) {
861
+ base.envelope = explanation.envelope;
862
+ }
668
863
  }
669
864
  return base;
670
865
  });
@@ -688,6 +883,19 @@ async function cmdRecall(hippoRoot, query, flags) {
688
883
  const explanation = explainMatch(query, r);
689
884
  console.log(` source:${sourceMark} | layer: [${e.layer}] | confidence: [${conf}]`);
690
885
  console.log(` reason: ${explanation.reason}`);
886
+ if (explanation.envelope) {
887
+ const env = explanation.envelope;
888
+ console.log(` kind: ${env.kind}`);
889
+ if (env.scope)
890
+ console.log(` scope: ${env.scope}`);
891
+ if (env.owner)
892
+ console.log(` owner: ${env.owner}`);
893
+ if (env.artifact_ref)
894
+ console.log(` artifact_ref: ${env.artifact_ref}`);
895
+ if (env.session_id)
896
+ console.log(` session_id: ${env.session_id}`);
897
+ console.log(` confidence: ${env.confidence}`);
898
+ }
691
899
  }
692
900
  console.log();
693
901
  console.log(e.content);
@@ -865,7 +1073,6 @@ async function cmdExplain(hippoRoot, query, flags) {
865
1073
  console.log('Note: explain does not mark memories as retrieved (read-only).');
866
1074
  }
867
1075
  async function cmdEval(hippoRoot, corpusPath, flags) {
868
- requireInit(hippoRoot);
869
1076
  const asJson = Boolean(flags['json']);
870
1077
  const minMrr = flags['min-mrr'] !== undefined ? parseFloat(String(flags['min-mrr'])) : null;
871
1078
  const showCases = Boolean(flags['show-cases']);
@@ -873,7 +1080,14 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
873
1080
  const noMmr = Boolean(flags['no-mmr']);
874
1081
  const mmrLambda = flags['mmr-lambda'] !== undefined ? parseFloat(String(flags['mmr-lambda'])) : undefined;
875
1082
  const embeddingWeight = flags['embedding-weight'] !== undefined ? parseFloat(String(flags['embedding-weight'])) : undefined;
876
- const entries = loadAllEntries(hippoRoot);
1083
+ // Suite mode doesn't need an initialized store
1084
+ if (flags['suite']) {
1085
+ // handled below after bootstrap check
1086
+ }
1087
+ else {
1088
+ requireInit(hippoRoot);
1089
+ }
1090
+ const entries = flags['suite'] ? [] : loadAllEntries(hippoRoot);
877
1091
  // Bootstrap mode: emit a synthetic corpus and exit.
878
1092
  if (flags['bootstrap']) {
879
1093
  const outPath = flags['out'] ? String(flags['out']) : null;
@@ -890,8 +1104,41 @@ async function cmdEval(hippoRoot, corpusPath, flags) {
890
1104
  }
891
1105
  return;
892
1106
  }
1107
+ // Suite mode: run built-in feature eval (no corpus file needed, no init needed)
1108
+ if (flags['suite']) {
1109
+ const pkg = JSON.parse(fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), '..', 'package.json'), 'utf8'));
1110
+ const version = pkg.version || 'unknown';
1111
+ const baselinePath = flags['baseline'] ? String(flags['baseline']) : path.join(hippoRoot, 'eval-baseline.json');
1112
+ let baseline;
1113
+ if (fs.existsSync(baselinePath)) {
1114
+ try {
1115
+ baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
1116
+ }
1117
+ catch { }
1118
+ }
1119
+ const result = await runFeatureEval(version);
1120
+ if (asJson) {
1121
+ console.log(JSON.stringify(result, null, 2));
1122
+ }
1123
+ else {
1124
+ console.log(formatResult(result, baseline));
1125
+ }
1126
+ if (flags['save-baseline']) {
1127
+ const newBaseline = resultToBaseline(result);
1128
+ fs.mkdirSync(path.dirname(baselinePath), { recursive: true });
1129
+ fs.writeFileSync(baselinePath, JSON.stringify(newBaseline, null, 2), 'utf8');
1130
+ console.log(`\nBaseline saved to ${baselinePath}`);
1131
+ }
1132
+ if (baseline) {
1133
+ const report = detectRegressions(baseline, result);
1134
+ if (report.verdict === 'REGRESSION' && minMrr === null) {
1135
+ process.exit(1);
1136
+ }
1137
+ }
1138
+ return;
1139
+ }
893
1140
  if (!corpusPath) {
894
- console.error('Usage: hippo eval <corpus.json> OR hippo eval --bootstrap [--out <path>]');
1141
+ console.error('Usage: hippo eval <corpus.json> OR hippo eval --suite [--save-baseline] OR hippo eval --bootstrap');
895
1142
  process.exit(1);
896
1143
  }
897
1144
  if (!fs.existsSync(corpusPath)) {
@@ -1459,6 +1706,17 @@ async function cmdSleepCore(hippoRoot, flags) {
1459
1706
  }
1460
1707
  }
1461
1708
  }
1709
+ // Post-sleep ambient state summary
1710
+ if (!dryRun) {
1711
+ const postSleepConfig = loadConfig(hippoRoot);
1712
+ if (postSleepConfig.ambient.enabled) {
1713
+ const postSleepEntries = loadAllEntries(hippoRoot).filter(e => !e.superseded_by);
1714
+ if (postSleepEntries.length > 0) {
1715
+ const ambientState = computeAmbientState(postSleepEntries);
1716
+ console.log(`\n${renderAmbientSummary(ambientState)}`);
1717
+ }
1718
+ }
1719
+ }
1462
1720
  }
1463
1721
  /**
1464
1722
  * Print the contents of the SessionEnd sleep log to stdout, then clear it.
@@ -2371,17 +2629,20 @@ async function cmdContext(hippoRoot, args, flags) {
2371
2629
  // Default context always filters superseded (no --include-superseded / --as-of for context)
2372
2630
  localEntries = localEntries.filter(e => !e.superseded_by);
2373
2631
  globalEntries = globalEntries.filter(e => !e.superseded_by);
2374
- const allEntries = [...localEntries];
2375
- if (allEntries.length === 0 && globalEntries.length === 0)
2376
- return; // no memories, zero output
2377
2632
  let selectedItems = [];
2378
2633
  let totalTokens = 0;
2379
2634
  // Task snapshots / session events live in the local store. Skip when
2380
2635
  // local isn't initialized — loading would auto-create .hippo in the cwd.
2381
2636
  const activeSnapshot = hasLocal ? loadActiveTaskSnapshot(hippoRoot) : null;
2637
+ const sessionHandoff = hasLocal && activeSnapshot?.session_id
2638
+ ? loadLatestHandoff(hippoRoot, activeSnapshot.session_id)
2639
+ : null;
2382
2640
  const recentSessionEvents = hasLocal && activeSnapshot?.session_id
2383
2641
  ? listSessionEvents(hippoRoot, { session_id: activeSnapshot.session_id, limit: 5 })
2384
2642
  : [];
2643
+ if (localEntries.length === 0 && globalEntries.length === 0 && !activeSnapshot && !sessionHandoff && recentSessionEvents.length === 0) {
2644
+ return;
2645
+ }
2385
2646
  // --pinned-only: restrict to pinned entries only. Used by the Claude Code
2386
2647
  // UserPromptSubmit hook so invariants stay in context every turn.
2387
2648
  // (pinnedOnly and hasLocal are declared at the top of this function.)
@@ -2482,7 +2743,7 @@ async function cmdContext(hippoRoot, args, flags) {
2482
2743
  selectedItems = selectedItems.slice(0, limit);
2483
2744
  totalTokens = selectedItems.reduce((sum, r) => sum + r.tokens, 0);
2484
2745
  }
2485
- if (selectedItems.length === 0 && !activeSnapshot && recentSessionEvents.length === 0)
2746
+ if (selectedItems.length === 0 && !activeSnapshot && !sessionHandoff && recentSessionEvents.length === 0)
2486
2747
  return;
2487
2748
  // --pinned-only is called by the UserPromptSubmit hook every turn. Treat it
2488
2749
  // as read-only so pinned memories don't inflate retrieval_count or extend
@@ -2516,7 +2777,7 @@ async function cmdContext(hippoRoot, args, flags) {
2516
2777
  content: r.entry.content,
2517
2778
  global: r.isGlobal ?? false,
2518
2779
  }));
2519
- console.log(JSON.stringify({ query, activeSnapshot, recentSessionEvents, memories: output, tokens: totalTokens }));
2780
+ console.log(JSON.stringify({ query, activeSnapshot, sessionHandoff, recentSessionEvents, memories: output, tokens: totalTokens }));
2520
2781
  }
2521
2782
  else if (format === 'additional-context') {
2522
2783
  // Claude Code UserPromptSubmit hook JSON shape. Capture the markdown that
@@ -2527,14 +2788,18 @@ async function cmdContext(hippoRoot, args, flags) {
2527
2788
  try {
2528
2789
  if (activeSnapshot)
2529
2790
  printActiveTaskSnapshot(activeSnapshot);
2791
+ if (sessionHandoff)
2792
+ printHandoff(sessionHandoff);
2530
2793
  if (recentSessionEvents.length > 0)
2531
2794
  printSessionEvents(recentSessionEvents);
2532
- printContextMarkdown(selectedItems.map((r) => ({
2533
- entry: updatedEntries.find((u) => u.id === r.entry.id) ?? r.entry,
2534
- score: r.score,
2535
- tokens: r.tokens,
2536
- isGlobal: r.isGlobal ?? false,
2537
- })), totalTokens, framing);
2795
+ if (selectedItems.length > 0) {
2796
+ printContextMarkdown(selectedItems.map((r) => ({
2797
+ entry: updatedEntries.find((u) => u.id === r.entry.id) ?? r.entry,
2798
+ score: r.score,
2799
+ tokens: r.tokens,
2800
+ isGlobal: r.isGlobal ?? false,
2801
+ })), totalTokens, framing);
2802
+ }
2538
2803
  }
2539
2804
  finally {
2540
2805
  console.log = realLog;
@@ -2554,15 +2819,29 @@ async function cmdContext(hippoRoot, args, flags) {
2554
2819
  if (activeSnapshot) {
2555
2820
  printActiveTaskSnapshot(activeSnapshot);
2556
2821
  }
2822
+ if (sessionHandoff) {
2823
+ printHandoff(sessionHandoff);
2824
+ }
2557
2825
  if (recentSessionEvents.length > 0) {
2558
2826
  printSessionEvents(recentSessionEvents);
2559
2827
  }
2560
- printContextMarkdown(selectedItems.map((r) => ({
2561
- entry: updatedEntries.find((u) => u.id === r.entry.id) ?? r.entry,
2562
- score: r.score,
2563
- tokens: r.tokens,
2564
- isGlobal: r.isGlobal ?? false,
2565
- })), totalTokens, framing);
2828
+ if (selectedItems.length > 0) {
2829
+ printContextMarkdown(selectedItems.map((r) => ({
2830
+ entry: updatedEntries.find((u) => u.id === r.entry.id) ?? r.entry,
2831
+ score: r.score,
2832
+ tokens: r.tokens,
2833
+ isGlobal: r.isGlobal ?? false,
2834
+ })), totalTokens, framing);
2835
+ }
2836
+ // Ambient state summary (one-line landscape overview)
2837
+ const ambientConfig = loadConfig(hippoRoot);
2838
+ if (ambientConfig.ambient.enabled && !pinnedOnly) {
2839
+ const allForAmbient = [...localEntries, ...globalEntries];
2840
+ if (allForAmbient.length > 0) {
2841
+ const ambientState = computeAmbientState(allForAmbient);
2842
+ console.log(`\n${renderAmbientSummary(ambientState)}`);
2843
+ }
2844
+ }
2566
2845
  }
2567
2846
  }
2568
2847
  function printContextMarkdown(items, totalTokens, framing = 'observe') {
@@ -3475,6 +3754,38 @@ Commands:
3475
3754
  --why Show match reasons and source annotations
3476
3755
  --no-mmr Disable MMR diversity re-ranking
3477
3756
  --mmr-lambda <f> MMR balance 0..1 (default: 0.7, 1.0 = pure relevance)
3757
+ --evc-adaptive ACC-style: when top-K shows high inter-item overlap
3758
+ (= conflict cluster), expand pool and re-rank by
3759
+ recency. Default off. RESEARCH.md §PFC.ACC.
3760
+ --filter-conflicts vlPFC interference filter: drop superseded entries
3761
+ and 0.3x-downweight entries flagged in an open
3762
+ conflict with a peer in the same result set.
3763
+ Uses recorded supersession + conflicts only — never
3764
+ lexical inference. Default off. RESEARCH.md §PFC.vlPFC.
3765
+ --value-aware vmPFC value attribution: boost memories with positive
3766
+ cumulative outcomes and demote those with negative
3767
+ outcomes during ranking. Multiplier
3768
+ clip(1 + 0.3*tanh(pos - neg), 0.7, 1.3). Reuses
3769
+ outcome_positive / outcome_negative; no schema
3770
+ change. Default off. RESEARCH.md §PFC.vmPFC.
3771
+ --rerank-utility OFC option-value re-ranker: combine relevance,
3772
+ strength, and integration cost into a single utility
3773
+ = score * (0.5 + 0.5 * strength) * (1 - cost_factor)
3774
+ where cost_factor = min(0.3, tokens / 10000). Re-sorts
3775
+ results by utility. Default off. RESEARCH.md §PFC.OFC.
3776
+ --goal <tag> dlPFC goal-conditioned recall: memories tagged with
3777
+ the goal tag get a 1.5x score boost and results are
3778
+ re-sorted. Default off. RESEARCH.md §PFC.dlPFC.
3779
+ --salience-threshold <n>
3780
+ Pineal salience: down-weight memories whose
3781
+ retrieval_count is below n. score *= max(0.5,
3782
+ retrieval_count / n) for entries with count < n;
3783
+ entries at or above n are unchanged. Salience emerges
3784
+ from USE, not from lexical overlap. Default off.
3785
+ RESEARCH.md §"AI Pineal Gland". (v1's creation-time
3786
+ lexical gate destroyed LoCoMo 0.28 -> 0.02; this v2
3787
+ is retrieval-side, opt-in only — see MEMORY.md
3788
+ "Hippo salience gate destroys benchmark recall".)
3478
3789
  explain <query> Show full score breakdown for each retrieved memory
3479
3790
  --budget <n> Token budget (default: 4000)
3480
3791
  --limit <n> Cap the number of results displayed
@@ -3705,7 +4016,13 @@ async function main() {
3705
4016
  cmdInit(hippoRoot, flags);
3706
4017
  break;
3707
4018
  case 'remember': {
3708
- const text = args.join(' ').trim();
4019
+ let text;
4020
+ if (args.length === 1 && args[0] === '-') {
4021
+ text = fs.readFileSync(0, 'utf-8').trim();
4022
+ }
4023
+ else {
4024
+ text = args.join(' ').trim();
4025
+ }
3709
4026
  if (!text || text.length < 3) {
3710
4027
  console.error('Memory content too short (minimum 3 characters).');
3711
4028
  process.exit(1);