hippo-memory 1.24.0 → 1.25.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/bin/hippo.js +2 -2
- package/dist/api.d.ts +12 -17
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +25 -55
- package/dist/api.js.map +1 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +88 -10
- package/dist/cli.js.map +1 -1
- package/dist/connectors/github/backfill.js +4 -4
- package/dist/connectors/github/cli-impl.js +6 -6
- package/dist/connectors/github/dlq.js +14 -14
- package/dist/connectors/slack/backfill.js +1 -1
- package/dist/connectors/slack/dlq.js +10 -10
- package/dist/connectors/slack/workspaces.js +4 -4
- package/dist/dag.js +6 -6
- package/dist/dashboard.js +7 -7
- package/dist/hooks.js +24 -24
- package/dist/predictions.js +67 -67
- package/dist/recall-scope.d.ts +60 -0
- package/dist/recall-scope.d.ts.map +1 -0
- package/dist/recall-scope.js +84 -0
- package/dist/recall-scope.js.map +1 -0
- package/dist/refine-llm.js +13 -13
- package/dist/shared.d.ts +30 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +41 -9
- package/dist/shared.js.map +1 -1
- package/dist/sleep-redact.d.ts +3 -0
- package/dist/sleep-redact.d.ts.map +1 -1
- package/dist/sleep-redact.js +3 -0
- package/dist/sleep-redact.js.map +1 -1
- package/dist/src/api.js +25 -55
- package/dist/src/api.js.map +1 -1
- package/dist/src/cli.js +88 -10
- package/dist/src/cli.js.map +1 -1
- package/dist/src/connectors/github/backfill.js +4 -4
- package/dist/src/connectors/github/cli-impl.js +6 -6
- package/dist/src/connectors/github/dlq.js +14 -14
- package/dist/src/connectors/slack/backfill.js +1 -1
- package/dist/src/connectors/slack/dlq.js +10 -10
- package/dist/src/connectors/slack/workspaces.js +4 -4
- package/dist/src/dag.js +6 -6
- package/dist/src/dashboard.js +7 -7
- package/dist/src/hooks.js +24 -24
- package/dist/src/predictions.js +67 -67
- package/dist/src/recall-scope.js +84 -0
- package/dist/src/recall-scope.js.map +1 -0
- package/dist/src/refine-llm.js +13 -13
- package/dist/src/shared.js +41 -9
- package/dist/src/shared.js.map +1 -1
- package/dist/src/sleep-redact.js +3 -0
- package/dist/src/sleep-redact.js.map +1 -1
- package/dist/src/store.js +57 -17
- package/dist/src/store.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/working-memory.js +19 -19
- package/dist/store.d.ts +30 -9
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +57 -17
- package/dist/store.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.js +19 -19
- package/dist-ui/index.html +12 -12
- package/extensions/openclaw-plugin/index.ts +650 -650
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/benchmarks/e1.3/scenarios.json +0 -2587
package/dist/cli.js
CHANGED
|
@@ -34,7 +34,8 @@ import { execFileSync, execSync, spawn } from 'child_process';
|
|
|
34
34
|
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
35
35
|
import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, computeSchemaFit, Layer, } from './memory.js';
|
|
36
36
|
import { resolveProjectIdentity } from './project-identity.js';
|
|
37
|
-
import {
|
|
37
|
+
import { detectSecret } from './secret-detect.js';
|
|
38
|
+
import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, RECALL_DEFAULT_DENY_SCOPES, } from './store.js';
|
|
38
39
|
import { markRetrieved, hybridSearch, physicsSearch, explainMatch, textOverlap } from './search.js';
|
|
39
40
|
import { renderTraceContent, parseSteps } from './trace.js';
|
|
40
41
|
import { deduplicateStore } from './dedupe.js';
|
|
@@ -721,15 +722,43 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
721
722
|
// A5 stub auth: resolve the active tenant once and thread it through every
|
|
722
723
|
// recall-time SELECT against `memories`. Cross-tenant rows must never surface.
|
|
723
724
|
const tenantId = resolveTenantId({});
|
|
724
|
-
|
|
725
|
-
|
|
725
|
+
// v1.25.0 (v39 follow-up #1): the direct CLI recall path applies the recall
|
|
726
|
+
// scope rule. SQL half via loadRecallSearchEntries in 'additive' mode —
|
|
727
|
+
// default-deny (`unknown:legacy` quarantine) always, and an explicit
|
|
728
|
+
// `--scope X` UNLOCKS envelope scope X on top of the default-admitted set
|
|
729
|
+
// (the CLI flag is historically a tag-boost hint over scope-NULL rows, so
|
|
730
|
+
// api.recall's narrowing exact-match would empty every tag-scoped recall —
|
|
731
|
+
// see passesCliRecallScopeFilter in recall-scope.ts). The regex-only
|
|
732
|
+
// `<source>:private:*` half is the JS post-filter below. Hoisted here
|
|
733
|
+
// (it previously lived with the boost flags): recallExplicitScope is the
|
|
734
|
+
// FILTER input; recallActiveScope (which falls back to detectScope()) stays
|
|
735
|
+
// boost-only — auto-detection must never become a filter input or
|
|
736
|
+
// detected-project recalls would change shape.
|
|
737
|
+
const recallExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
738
|
+
const requestedScopeForFilter = recallExplicitScope || undefined;
|
|
739
|
+
let localEntries = loadRecallSearchEntries(hippoRoot, query, undefined, tenantId, requestedScopeForFilter, 'additive');
|
|
740
|
+
let globalEntries = isInitialized(globalRoot) ? loadRecallSearchEntries(globalRoot, query, undefined, tenantId, requestedScopeForFilter, 'additive') : [];
|
|
726
741
|
// v1.12.13 / C5 — WYSIATI counters. Track filter activity per the plan v3
|
|
727
742
|
// Task 3 mapping table. dropped_pre_rank is the SUM of all non-budget
|
|
728
743
|
// filter drops (pre-rank AND post-rank). Search-engine internal drops
|
|
729
744
|
// (scored-to-zero rows that hybridSearch/physicsSearch returns fewer of)
|
|
730
745
|
// are NOT counted in v1 — they are part of the rank step, not a filter.
|
|
746
|
+
// totalCandidates = post-SQL-predicate count (api.recall parity: measured
|
|
747
|
+
// after loadRecallSearchEntries, before the JS scope filter). NOTE the
|
|
748
|
+
// v1.12.13 accounting convention: SQL-excluded rows (quarantine + the
|
|
749
|
+
// v1.25.0 pre-window ':private:' exclusion) are pre-candidate and are NOT
|
|
750
|
+
// counted as drops; the JS half below normally drops 0 and exists as
|
|
751
|
+
// defense-in-depth (LIKE/regex divergence, exact-mode mismatch).
|
|
731
752
|
const totalCandidatesCountCmd = localEntries.length + globalEntries.length;
|
|
732
753
|
let droppedPreRankCountCmd = 0;
|
|
754
|
+
// v1.25.0: JS half of the recall scope rule (private-scope regex deny with
|
|
755
|
+
// explicit-request unlock), via the canonical helper — do not inline a
|
|
756
|
+
// fourth copy of this predicate.
|
|
757
|
+
const passesRecallScope = (e) => api.passesCliRecallScopeFilter(e.scope ?? null, requestedScopeForFilter);
|
|
758
|
+
const beforeScopeFilterCmd = localEntries.length + globalEntries.length;
|
|
759
|
+
localEntries = localEntries.filter(passesRecallScope);
|
|
760
|
+
globalEntries = globalEntries.filter(passesRecallScope);
|
|
761
|
+
droppedPreRankCountCmd += beforeScopeFilterCmd - (localEntries.length + globalEntries.length);
|
|
733
762
|
// Bi-temporal filtering for physics path (hybridSearch handles it internally)
|
|
734
763
|
if (asOf) {
|
|
735
764
|
const filterAsOf = (entries) => {
|
|
@@ -781,7 +810,8 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
781
810
|
const minResults = flags['min-results'] !== undefined
|
|
782
811
|
? parseInt(String(flags['min-results']), 10)
|
|
783
812
|
: undefined;
|
|
784
|
-
|
|
813
|
+
// recallExplicitScope hoisted above the candidate loads (v1.25.0) — see the
|
|
814
|
+
// scope-filter block near the top of cmdRecall.
|
|
785
815
|
const recallActiveScope = recallExplicitScope || detectScope();
|
|
786
816
|
const useMultihop = flags['multihop'] === true || config.multihop.enabled;
|
|
787
817
|
// L1 — graph-retrieval stream. `--graph-stream` forces rrf fusion + the graph stream
|
|
@@ -858,10 +888,16 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
858
888
|
});
|
|
859
889
|
}
|
|
860
890
|
else if (hasGlobal) {
|
|
861
|
-
// Use searchBothHybrid for merged results with embedding support
|
|
891
|
+
// Use searchBothHybrid for merged results with embedding support.
|
|
892
|
+
// recallScope (v1.25.0): searchBothHybrid re-loads candidates internally,
|
|
893
|
+
// so the scope rule must be plumbed in — the filtered localEntries /
|
|
894
|
+
// globalEntries above are NOT what this path ranks.
|
|
862
895
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
863
896
|
budget, mmr: mmrEnabled, mmrLambda, localBump, minResults, scope: recallActiveScope, tenantId,
|
|
864
897
|
includeSuperseded, asOf,
|
|
898
|
+
recallScope: recallExplicitScope
|
|
899
|
+
? { requested: recallExplicitScope, additive: true }
|
|
900
|
+
: {},
|
|
865
901
|
});
|
|
866
902
|
}
|
|
867
903
|
else {
|
|
@@ -1694,8 +1730,30 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
1694
1730
|
const globalRoot = getGlobalRoot();
|
|
1695
1731
|
// A5: scope explain results to the active tenant.
|
|
1696
1732
|
const tenantId = resolveTenantId({});
|
|
1697
|
-
|
|
1698
|
-
|
|
1733
|
+
// v1.25.0 (v39 follow-up #1): cmdExplain shares cmdRecall's leak class —
|
|
1734
|
+
// same unscoped loads, same fix, same semantics (explain explains what
|
|
1735
|
+
// recall would see). Flag hoisted above the loads; the note below keeps the
|
|
1736
|
+
// command honest for an operator debugging a hidden row.
|
|
1737
|
+
const explainExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
1738
|
+
const explainRequestedScope = explainExplicitScope || undefined;
|
|
1739
|
+
let explainLocalEntries = loadRecallSearchEntries(hippoRoot, query, undefined, tenantId, explainRequestedScope, 'additive');
|
|
1740
|
+
let explainGlobalEntries = isInitialized(globalRoot) ? loadRecallSearchEntries(globalRoot, query, undefined, tenantId, explainRequestedScope, 'additive') : [];
|
|
1741
|
+
const passesExplainScope = (e) => api.passesCliRecallScopeFilter(e.scope ?? null, explainRequestedScope);
|
|
1742
|
+
explainLocalEntries = explainLocalEntries.filter(passesExplainScope);
|
|
1743
|
+
explainGlobalEntries = explainGlobalEntries.filter(passesExplainScope);
|
|
1744
|
+
// Honesty note (grill finding #2): the SQL predicate excludes denied rows
|
|
1745
|
+
// BEFORE the candidate window (codex P2 fix), so the pipeline never sees
|
|
1746
|
+
// them. For the diagnostic note only, probe the unscoped window and count
|
|
1747
|
+
// what the scope policy hides — window-capped, so the count is a floor on
|
|
1748
|
+
// large stores, which is fine for a "why is my row missing" hint.
|
|
1749
|
+
const explainUnscopedProbe = [
|
|
1750
|
+
...loadSearchEntries(hippoRoot, query, undefined, tenantId),
|
|
1751
|
+
...(isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query, undefined, tenantId) : []),
|
|
1752
|
+
];
|
|
1753
|
+
const explainScopeDropped = explainUnscopedProbe.filter((e) => !passesExplainScope(e)).length;
|
|
1754
|
+
if (explainScopeDropped > 0) {
|
|
1755
|
+
console.error(`[note] ${explainScopeDropped} candidate${explainScopeDropped === 1 ? '' : 's'} hidden by recall scope policy (pass an explicit --scope to inspect).`);
|
|
1756
|
+
}
|
|
1699
1757
|
// Bi-temporal filtering
|
|
1700
1758
|
if (explainAsOf) {
|
|
1701
1759
|
const filterAsOfExplain = (entries) => {
|
|
@@ -1738,7 +1796,7 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
1738
1796
|
: flags['local-bump'] !== undefined
|
|
1739
1797
|
? parseFloat(String(flags['local-bump']))
|
|
1740
1798
|
: config.search.localBump;
|
|
1741
|
-
|
|
1799
|
+
// explainExplicitScope hoisted above the candidate loads (v1.25.0).
|
|
1742
1800
|
const explainActiveScope = explainExplicitScope || detectScope();
|
|
1743
1801
|
let results;
|
|
1744
1802
|
let modeUsed;
|
|
@@ -1756,6 +1814,9 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
1756
1814
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
1757
1815
|
budget, explain: true, mmr: mmrEnabled, mmrLambda, localBump, scope: explainActiveScope,
|
|
1758
1816
|
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf, tenantId,
|
|
1817
|
+
recallScope: explainExplicitScope
|
|
1818
|
+
? { requested: explainExplicitScope, additive: true }
|
|
1819
|
+
: {},
|
|
1759
1820
|
});
|
|
1760
1821
|
modeUsed = 'searchBothHybrid';
|
|
1761
1822
|
}
|
|
@@ -2224,8 +2285,8 @@ async function cmdRefine(hippoRoot, flags) {
|
|
|
2224
2285
|
* Scan for Claude Code MEMORY.md files and import new entries into hippo.
|
|
2225
2286
|
* Looks in ~/.claude/projects/<project>/memory/ for .md files with YAML frontmatter.
|
|
2226
2287
|
*/
|
|
2227
|
-
function learnFromMemoryMd(hippoRoot) {
|
|
2228
|
-
const home =
|
|
2288
|
+
export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
2289
|
+
const home = homeDir;
|
|
2229
2290
|
const memoryDirs = [];
|
|
2230
2291
|
// Claude Code project memories
|
|
2231
2292
|
const claudeProjectsDir = path.join(home, '.claude', 'projects');
|
|
@@ -2243,6 +2304,7 @@ function learnFromMemoryMd(hippoRoot) {
|
|
|
2243
2304
|
return 0;
|
|
2244
2305
|
const existing = loadAllEntries(hippoRoot);
|
|
2245
2306
|
let imported = 0;
|
|
2307
|
+
let skippedSecret = 0;
|
|
2246
2308
|
for (const memDir of memoryDirs) {
|
|
2247
2309
|
try {
|
|
2248
2310
|
const files = fs.readdirSync(memDir).filter(f => f.endsWith('.md') && f !== 'MEMORY.md');
|
|
@@ -2257,6 +2319,15 @@ function learnFromMemoryMd(hippoRoot) {
|
|
|
2257
2319
|
continue;
|
|
2258
2320
|
// Truncate to reasonable size
|
|
2259
2321
|
const content = body.length > 1500 ? body.slice(0, 1500) + ' [truncated]' : body;
|
|
2322
|
+
// Never ingest secret-bearing memory files. Some Claude Code memory
|
|
2323
|
+
// files exist purely to hold a live credential (e.g. an API-key
|
|
2324
|
+
// reference). The scope-isolation secret veto (v1.24.0) gated
|
|
2325
|
+
// share/promote/sync/ambient but missed this import path, so a live
|
|
2326
|
+
// key could land in the store here. Veto it at ingest. (v1.24.1)
|
|
2327
|
+
if (detectSecret({ content, tags: ['claude-code-memory'] }).flagged) {
|
|
2328
|
+
skippedSecret++;
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2260
2331
|
// Dedup: check if substantially similar content already exists
|
|
2261
2332
|
const isDup = existing.some(e => {
|
|
2262
2333
|
const overlap = textOverlap(content.slice(0, 200), e.content.slice(0, 200));
|
|
@@ -2277,6 +2348,9 @@ function learnFromMemoryMd(hippoRoot) {
|
|
|
2277
2348
|
}
|
|
2278
2349
|
catch { /* skip broken dirs */ }
|
|
2279
2350
|
}
|
|
2351
|
+
if (skippedSecret > 0) {
|
|
2352
|
+
console.log(`Skipped ${skippedSecret} secret-bearing memory file${skippedSecret === 1 ? '' : 's'} (not ingested).`);
|
|
2353
|
+
}
|
|
2280
2354
|
return imported;
|
|
2281
2355
|
}
|
|
2282
2356
|
function cmdDedup(hippoRoot, flags) {
|
|
@@ -2415,6 +2489,10 @@ export function renderSleepResult(result) {
|
|
|
2415
2489
|
if (result.shared !== undefined && result.shared > 0) {
|
|
2416
2490
|
console.log(`\nAuto-shared ${result.shared} high-value memories to global store.`);
|
|
2417
2491
|
}
|
|
2492
|
+
if (result.secretSkipped !== undefined && result.secretSkipped > 0) {
|
|
2493
|
+
// v1.25.0 (v39 follow-up #2): the secret veto is no longer silent.
|
|
2494
|
+
console.log(`\nAuto-share: withheld ${result.secretSkipped} secret-flagged ${result.secretSkipped === 1 ? 'memory' : 'memories'} (secret veto).`);
|
|
2495
|
+
}
|
|
2418
2496
|
if (result.ambient) {
|
|
2419
2497
|
console.log(`\n${renderAmbientSummary(result.ambient)}`);
|
|
2420
2498
|
}
|