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.
- package/README.md +8 -0
- package/dist/ambient.d.ts +26 -0
- package/dist/ambient.d.ts.map +1 -0
- package/dist/ambient.js +147 -0
- package/dist/ambient.js.map +1 -0
- package/dist/capture.js +4 -0
- package/dist/capture.js.map +1 -1
- package/dist/cli.js +338 -21
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +110 -1
- package/dist/db.js.map +1 -1
- package/dist/eval-suite.d.ts +82 -0
- package/dist/eval-suite.d.ts.map +1 -0
- package/dist/eval-suite.js +289 -0
- package/dist/eval-suite.js.map +1 -0
- package/dist/importers.d.ts.map +1 -1
- package/dist/importers.js +5 -0
- package/dist/importers.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/framing.d.ts +12 -0
- package/dist/mcp/framing.d.ts.map +1 -0
- package/dist/mcp/framing.js +45 -0
- package/dist/mcp/framing.js.map +1 -0
- package/dist/mcp/server.js +28 -33
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory.d.ts +9 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +4 -0
- package/dist/memory.js.map +1 -1
- package/dist/raw-archive.d.ts +16 -0
- package/dist/raw-archive.d.ts.map +1 -0
- package/dist/raw-archive.js +53 -0
- package/dist/raw-archive.js.map +1 -0
- package/dist/salience.d.ts +22 -0
- package/dist/salience.d.ts.map +1 -0
- package/dist/salience.js +74 -0
- package/dist/salience.js.map +1 -0
- package/dist/search.d.ts +9 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +8 -0
- package/dist/search.js.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +35 -8
- package/dist/store.js.map +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +46 -46
- package/extensions/openclaw-plugin/package.json +13 -13
- package/openclaw.plugin.json +45 -45
- 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
|
-
|
|
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 --
|
|
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
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
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
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
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
|
-
|
|
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);
|