hippo-memory 1.11.2 → 1.11.4
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/dist/api.d.ts +155 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +421 -11
- package/dist/api.js.map +1 -1
- package/dist/cli.js +160 -369
- package/dist/cli.js.map +1 -1
- package/dist/dedupe.d.ts +34 -0
- package/dist/dedupe.d.ts.map +1 -0
- package/dist/dedupe.js +61 -0
- package/dist/dedupe.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +122 -1
- package/dist/server.js.map +1 -1
- package/dist/src/api.js +421 -11
- package/dist/src/api.js.map +1 -1
- package/dist/src/cli.js +160 -369
- package/dist/src/cli.js.map +1 -1
- package/dist/src/dedupe.js +61 -0
- package/dist/src/dedupe.js.map +1 -0
- package/dist/src/server.js +122 -1
- package/dist/src/server.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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/src/cli.js
CHANGED
|
@@ -31,11 +31,11 @@ import * as os from 'os';
|
|
|
31
31
|
import { fileURLToPath } from 'node:url';
|
|
32
32
|
import { execFileSync, execSync, spawn } from 'child_process';
|
|
33
33
|
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
34
|
-
import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence,
|
|
34
|
+
import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, computeSchemaFit, Layer, DECISION_HALF_LIFE_DAYS, } from './memory.js';
|
|
35
35
|
import { getHippoRoot, isInitialized, initStore, writeEntry, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, RECALL_DEFAULT_DENY_SCOPES, } from './store.js';
|
|
36
|
-
import { markRetrieved,
|
|
36
|
+
import { markRetrieved, hybridSearch, physicsSearch, explainMatch, textOverlap } from './search.js';
|
|
37
37
|
import { renderTraceContent, parseSteps } from './trace.js';
|
|
38
|
-
import {
|
|
38
|
+
import { deduplicateStore } from './dedupe.js';
|
|
39
39
|
import { isEmbeddingAvailable, embedAll, embedMemory, loadEmbeddingIndex, resolveEmbeddingModel, } from './embeddings.js';
|
|
40
40
|
import { loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
|
|
41
41
|
import { computeSystemEnergy, vecNorm } from './physics.js';
|
|
@@ -46,7 +46,7 @@ import { rowToGoal } from './goals.js';
|
|
|
46
46
|
import { captureError, extractLessons, deduplicateLesson, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
|
|
47
47
|
import { extractInvalidationTarget, invalidateMatching } from './invalidation.js';
|
|
48
48
|
import { extractPathTags } from './path-context.js';
|
|
49
|
-
import { detectScope
|
|
49
|
+
import { detectScope } from './scope.js';
|
|
50
50
|
import { getGlobalRoot, initGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
|
|
51
51
|
import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
52
52
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, } from './importers.js';
|
|
@@ -1788,49 +1788,6 @@ function learnFromMemoryMd(hippoRoot) {
|
|
|
1788
1788
|
}
|
|
1789
1789
|
return imported;
|
|
1790
1790
|
}
|
|
1791
|
-
function deduplicateStore(hippoRoot, options = {}) {
|
|
1792
|
-
const threshold = options.threshold ?? 0.7;
|
|
1793
|
-
const dryRun = options.dryRun ?? false;
|
|
1794
|
-
const entries = loadAllEntries(hippoRoot);
|
|
1795
|
-
// Sort by strength desc, then retrieval count, so we keep the most valuable copy
|
|
1796
|
-
entries.sort((a, b) => {
|
|
1797
|
-
const sDiff = (b.strength ?? 0) - (a.strength ?? 0);
|
|
1798
|
-
if (Math.abs(sDiff) > 0.01)
|
|
1799
|
-
return sDiff;
|
|
1800
|
-
return (b.retrieval_count ?? 0) - (a.retrieval_count ?? 0);
|
|
1801
|
-
});
|
|
1802
|
-
const removed = new Set();
|
|
1803
|
-
const pairs = [];
|
|
1804
|
-
for (let i = 0; i < entries.length; i++) {
|
|
1805
|
-
if (removed.has(entries[i].id))
|
|
1806
|
-
continue;
|
|
1807
|
-
for (let j = i + 1; j < entries.length; j++) {
|
|
1808
|
-
if (removed.has(entries[j].id))
|
|
1809
|
-
continue;
|
|
1810
|
-
const similarity = textOverlap(entries[i].content, entries[j].content);
|
|
1811
|
-
if (similarity <= threshold)
|
|
1812
|
-
continue;
|
|
1813
|
-
removed.add(entries[j].id);
|
|
1814
|
-
pairs.push({
|
|
1815
|
-
kept: entries[i].id,
|
|
1816
|
-
keptContent: entries[i].content,
|
|
1817
|
-
keptLayer: entries[i].layer,
|
|
1818
|
-
keptStrength: entries[i].strength ?? 0,
|
|
1819
|
-
removed: entries[j].id,
|
|
1820
|
-
removedContent: entries[j].content,
|
|
1821
|
-
removedLayer: entries[j].layer,
|
|
1822
|
-
removedStrength: entries[j].strength ?? 0,
|
|
1823
|
-
similarity,
|
|
1824
|
-
});
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
if (!dryRun) {
|
|
1828
|
-
for (const id of removed) {
|
|
1829
|
-
deleteEntry(hippoRoot, id);
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
return { removed: removed.size, pairs };
|
|
1833
|
-
}
|
|
1834
1791
|
function cmdDedup(hippoRoot, flags) {
|
|
1835
1792
|
requireInit(hippoRoot);
|
|
1836
1793
|
const dryRun = Boolean(flags['dry-run']);
|
|
@@ -1925,9 +1882,55 @@ async function cmdSleep(hippoRoot, flags) {
|
|
|
1925
1882
|
restoreStdout();
|
|
1926
1883
|
}
|
|
1927
1884
|
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Render an api.sleep result as console output, byte-identical to the
|
|
1887
|
+
* pre-extraction inline implementation in cmdSleepCore.
|
|
1888
|
+
*/
|
|
1889
|
+
function renderSleepResult(result) {
|
|
1890
|
+
console.log(`Running consolidation${result.dryRun ? ' (dry run)' : ''}...`);
|
|
1891
|
+
console.log(`\nResults:`);
|
|
1892
|
+
console.log(` Active memories: ${result.active}`);
|
|
1893
|
+
console.log(` Removed (decayed): ${result.removed}`);
|
|
1894
|
+
console.log(` Merged episodic: ${result.mergedEpisodic}`);
|
|
1895
|
+
console.log(` New semantic: ${result.newSemantic}`);
|
|
1896
|
+
if (result.details && result.details.length > 0) {
|
|
1897
|
+
console.log('\nDetails:');
|
|
1898
|
+
for (const d of result.details) {
|
|
1899
|
+
console.log(d);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (result.dryRun)
|
|
1903
|
+
console.log('\n(dry run - nothing written)');
|
|
1904
|
+
if (result.deduped && result.deduped.removed > 0) {
|
|
1905
|
+
const { removed, semDups, epiDups, crossDups } = result.deduped;
|
|
1906
|
+
const parts = [];
|
|
1907
|
+
if (semDups > 0)
|
|
1908
|
+
parts.push(`${semDups} redundant semantic patterns`);
|
|
1909
|
+
if (epiDups > 0)
|
|
1910
|
+
parts.push(`${epiDups} duplicate episodic lessons`);
|
|
1911
|
+
if (crossDups > 0)
|
|
1912
|
+
parts.push(`${crossDups} cross-layer duplicates`);
|
|
1913
|
+
console.log(`\nDeduped ${removed} duplicates (${parts.join(', ')}). Kept stronger copies.`);
|
|
1914
|
+
}
|
|
1915
|
+
if (result.audit) {
|
|
1916
|
+
if (result.audit.errorsRemoved > 0) {
|
|
1917
|
+
console.log(`\nAudit: removed ${result.audit.errorsRemoved} junk memories (too short/empty).`);
|
|
1918
|
+
}
|
|
1919
|
+
if (result.audit.warningCount > 0) {
|
|
1920
|
+
console.log(`Audit: ${result.audit.warningCount} low-quality memories detected (run \`hippo audit\` for details).`);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
if (result.shared !== undefined && result.shared > 0) {
|
|
1924
|
+
console.log(`\nAuto-shared ${result.shared} high-value memories to global store.`);
|
|
1925
|
+
}
|
|
1926
|
+
if (result.ambient) {
|
|
1927
|
+
console.log(`\n${renderAmbientSummary(result.ambient)}`);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1928
1930
|
async function cmdSleepCore(hippoRoot, flags) {
|
|
1929
1931
|
requireInit(hippoRoot);
|
|
1930
|
-
// Auto-learn from git
|
|
1932
|
+
// Phase 1: Auto-learn from git + MEMORY.md (CLI-only, uses process.cwd() / os.homedir()).
|
|
1933
|
+
// Stays in cli.ts; api.sleep covers Phase 2-6 only.
|
|
1931
1934
|
if (!flags['no-learn']) {
|
|
1932
1935
|
const config = loadConfig(hippoRoot);
|
|
1933
1936
|
if (config.autoLearnOnSleep && isGitRepo(process.cwd())) {
|
|
@@ -1940,78 +1943,17 @@ async function cmdSleepCore(hippoRoot, flags) {
|
|
|
1940
1943
|
if (memImported > 0)
|
|
1941
1944
|
console.log(`Imported ${memImported} memories from Claude Code MEMORY.md files.`);
|
|
1942
1945
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
console.log(d);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
if (dryRun)
|
|
1958
|
-
console.log('\n(dry run - nothing written)');
|
|
1959
|
-
// Auto-dedup after consolidation (unless dry-run)
|
|
1960
|
-
if (!dryRun) {
|
|
1961
|
-
const dedupResult = deduplicateStore(hippoRoot);
|
|
1962
|
-
if (dedupResult.removed > 0) {
|
|
1963
|
-
const semDups = dedupResult.pairs.filter(p => p.keptLayer === 'semantic' && p.removedLayer === 'semantic').length;
|
|
1964
|
-
const epiDups = dedupResult.pairs.filter(p => p.keptLayer === 'episodic' && p.removedLayer === 'episodic').length;
|
|
1965
|
-
const crossDups = dedupResult.pairs.filter(p => p.keptLayer !== p.removedLayer).length;
|
|
1966
|
-
const parts = [];
|
|
1967
|
-
if (semDups > 0)
|
|
1968
|
-
parts.push(`${semDups} redundant semantic patterns`);
|
|
1969
|
-
if (epiDups > 0)
|
|
1970
|
-
parts.push(`${epiDups} duplicate episodic lessons`);
|
|
1971
|
-
if (crossDups > 0)
|
|
1972
|
-
parts.push(`${crossDups} cross-layer duplicates`);
|
|
1973
|
-
console.log(`\nDeduped ${dedupResult.removed} duplicates (${parts.join(', ')}). Kept stronger copies.`);
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
// Quality audit — remove junk, report warnings
|
|
1977
|
-
if (!dryRun) {
|
|
1978
|
-
const allEntries = loadAllEntries(hippoRoot);
|
|
1979
|
-
const audit = auditMemories(allEntries);
|
|
1980
|
-
if (audit.issues.length > 0) {
|
|
1981
|
-
const errors = audit.issues.filter(i => i.severity === 'error');
|
|
1982
|
-
const warnings = audit.issues.filter(i => i.severity === 'warning');
|
|
1983
|
-
if (errors.length > 0) {
|
|
1984
|
-
for (const issue of errors) {
|
|
1985
|
-
deleteEntry(hippoRoot, issue.memoryId);
|
|
1986
|
-
}
|
|
1987
|
-
console.log(`\nAudit: removed ${errors.length} junk memories (too short/empty).`);
|
|
1988
|
-
}
|
|
1989
|
-
if (warnings.length > 0) {
|
|
1990
|
-
console.log(`Audit: ${warnings.length} low-quality memories detected (run \`hippo audit\` for details).`);
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
// Auto-share high-transfer-score memories to global (unless --no-share or dry-run)
|
|
1995
|
-
if (!dryRun && !flags['no-share']) {
|
|
1996
|
-
const sleepConfig = loadConfig(hippoRoot);
|
|
1997
|
-
if (sleepConfig.autoShareOnSleep) {
|
|
1998
|
-
const shared = autoShare(hippoRoot, { minScore: 0.6 });
|
|
1999
|
-
if (shared.length > 0) {
|
|
2000
|
-
console.log(`\nAuto-shared ${shared.length} high-value memories to global store.`);
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
// Post-sleep ambient state summary
|
|
2005
|
-
if (!dryRun) {
|
|
2006
|
-
const postSleepConfig = loadConfig(hippoRoot);
|
|
2007
|
-
if (postSleepConfig.ambient.enabled) {
|
|
2008
|
-
const postSleepEntries = loadAllEntries(hippoRoot).filter(e => !e.superseded_by);
|
|
2009
|
-
if (postSleepEntries.length > 0) {
|
|
2010
|
-
const ambientState = computeAmbientState(postSleepEntries);
|
|
2011
|
-
console.log(`\n${renderAmbientSummary(ambientState)}`);
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
1946
|
+
// Phase 2-6: Pure-storage pipeline (consolidate + dedup + audit + share + ambient).
|
|
1947
|
+
const ctx = {
|
|
1948
|
+
hippoRoot,
|
|
1949
|
+
tenantId: resolveTenantId({}),
|
|
1950
|
+
actor: 'cli',
|
|
1951
|
+
};
|
|
1952
|
+
const result = await api.sleep(ctx, {
|
|
1953
|
+
dryRun: Boolean(flags['dry-run']),
|
|
1954
|
+
noShare: Boolean(flags['no-share']),
|
|
1955
|
+
});
|
|
1956
|
+
renderSleepResult(result);
|
|
2015
1957
|
}
|
|
2016
1958
|
/**
|
|
2017
1959
|
* Print the contents of the SessionEnd sleep log to stdout, then clear it.
|
|
@@ -2383,21 +2325,28 @@ function cmdOutcome(hippoRoot, flags) {
|
|
|
2383
2325
|
console.error('Specify --good or --bad');
|
|
2384
2326
|
process.exit(1);
|
|
2385
2327
|
}
|
|
2328
|
+
// Behavior fix (v1.11.3): cmdOutcome used to bypass api.outcome and do its
|
|
2329
|
+
// own readEntry/writeEntry inline, which silently skipped the audit_log
|
|
2330
|
+
// emission that the MCP outcome path already has via api.outcome. T6
|
|
2331
|
+
// rewires through api.outcome so every successful CLI 'outcome' call now
|
|
2332
|
+
// writes one audit_log row per affected id, matching MCP parity.
|
|
2333
|
+
const ctx = {
|
|
2334
|
+
hippoRoot,
|
|
2335
|
+
tenantId: resolveTenantId({}),
|
|
2336
|
+
actor: 'cli',
|
|
2337
|
+
};
|
|
2386
2338
|
const specificId = flags['id'] ? String(flags['id']) : null;
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
console.log('No recent recall to apply outcome to. Use --id <id> to target a specific memory.');
|
|
2391
|
-
return;
|
|
2339
|
+
let updated;
|
|
2340
|
+
if (specificId) {
|
|
2341
|
+
updated = api.outcome(ctx, [specificId], good).applied;
|
|
2392
2342
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
updated++;
|
|
2343
|
+
else {
|
|
2344
|
+
const r = api.outcomeForLastRecall(ctx, good);
|
|
2345
|
+
if (r.ids.length === 0) {
|
|
2346
|
+
console.log('No recent recall to apply outcome to. Use --id <id> to target a specific memory.');
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
updated = r.applied;
|
|
2401
2350
|
}
|
|
2402
2351
|
console.log(`Applied ${good ? 'positive' : 'negative'} outcome to ${updated} memor${updated === 1 ? 'y' : 'ies'}`);
|
|
2403
2352
|
}
|
|
@@ -2927,226 +2876,59 @@ function cmdCurrent(hippoRoot, args, flags) {
|
|
|
2927
2876
|
async function cmdContext(hippoRoot, args, flags) {
|
|
2928
2877
|
// --pinned-only fires on every UserPromptSubmit — including in directories
|
|
2929
2878
|
// that don't have a local .hippo. Skip requireInit for that path and fall
|
|
2930
|
-
// back to global-only
|
|
2879
|
+
// back to global-only inside api.getContext. The non-pinned path still
|
|
2880
|
+
// requires init (handled by CLI for user-friendly error messaging).
|
|
2931
2881
|
const pinnedOnly = flags['pinned-only'] === true;
|
|
2932
|
-
const hasLocal = isInitialized(hippoRoot);
|
|
2933
2882
|
if (!pinnedOnly) {
|
|
2934
2883
|
requireInit(hippoRoot);
|
|
2935
2884
|
}
|
|
2936
2885
|
const budget = parseInt(String(flags['budget'] ?? '1500'), 10);
|
|
2937
|
-
const limit = parseLimitFlag(flags['limit']);
|
|
2938
|
-
const includeRecent = parseCountFlag(flags['include-recent']);
|
|
2939
|
-
const ctxExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
2940
|
-
const ctxActiveScope = ctxExplicitScope || detectScope();
|
|
2941
|
-
// If budget is 0, skip entirely (zero token cost)
|
|
2942
2886
|
if (budget <= 0)
|
|
2943
2887
|
return;
|
|
2944
|
-
//
|
|
2888
|
+
// Resolve query: explicit args, --auto (git diff via CLI-side helper), or
|
|
2889
|
+
// fall through to api.getContext's '*' fallback. api.getContext is host-
|
|
2890
|
+
// agnostic so the auto-detect (which shells out to git) stays CLI-side.
|
|
2945
2891
|
let query = args.join(' ').trim();
|
|
2946
2892
|
if (!query && flags['auto']) {
|
|
2947
2893
|
query = autoDetectContext();
|
|
2948
2894
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
const
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
: null;
|
|
2974
|
-
const recentSessionEvents = hasLocal && activeSnapshot?.session_id
|
|
2975
|
-
? listSessionEvents(hippoRoot, resolveTenantId({}), { session_id: activeSnapshot.session_id, limit: 5 })
|
|
2976
|
-
: [];
|
|
2977
|
-
if (localEntries.length === 0 && globalEntries.length === 0 && !activeSnapshot && !sessionHandoff && recentSessionEvents.length === 0) {
|
|
2978
|
-
return;
|
|
2979
|
-
}
|
|
2980
|
-
// --pinned-only: restrict to pinned entries only. Used by the Claude Code
|
|
2981
|
-
// UserPromptSubmit hook so invariants stay in context every turn.
|
|
2982
|
-
// (pinnedOnly and hasLocal are declared at the top of this function.)
|
|
2983
|
-
if (pinnedOnly) {
|
|
2984
|
-
// loadConfig is safe even when local isn't initialized — it returns defaults.
|
|
2985
|
-
const pinnedCfg = loadConfig(hippoRoot);
|
|
2986
|
-
if (!pinnedCfg.pinnedInject.enabled)
|
|
2987
|
-
return; // user disabled via config
|
|
2988
|
-
// Effective budget: explicit --budget wins over config.
|
|
2989
|
-
const effBudget = flags['budget'] !== undefined ? budget : pinnedCfg.pinnedInject.budget;
|
|
2990
|
-
const nowP = new Date();
|
|
2991
|
-
const selectedIds = new Set();
|
|
2992
|
-
let usedP = 0;
|
|
2993
|
-
if (includeRecent > 0) {
|
|
2994
|
-
const recent = [
|
|
2995
|
-
...localEntries.map((entry) => ({ entry, isGlobal: false })),
|
|
2996
|
-
...globalEntries.map((entry) => ({ entry, isGlobal: true })),
|
|
2997
|
-
]
|
|
2998
|
-
.sort((a, b) => {
|
|
2999
|
-
const byCreated = Date.parse(b.entry.created) - Date.parse(a.entry.created);
|
|
3000
|
-
return byCreated !== 0 ? byCreated : b.entry.id.localeCompare(a.entry.id);
|
|
3001
|
-
})
|
|
3002
|
-
.slice(0, includeRecent)
|
|
3003
|
-
.map(({ entry, isGlobal }) => ({
|
|
3004
|
-
entry,
|
|
3005
|
-
score: calculateStrength(entry, nowP) * (isGlobal ? 1 / 1.2 : 1),
|
|
3006
|
-
tokens: estimateTokens(entry.content),
|
|
3007
|
-
isGlobal,
|
|
3008
|
-
}));
|
|
3009
|
-
for (const r of recent) {
|
|
3010
|
-
if (selectedIds.has(r.entry.id))
|
|
3011
|
-
continue;
|
|
3012
|
-
if (usedP + r.tokens > effBudget)
|
|
3013
|
-
continue;
|
|
3014
|
-
selectedItems.push(r);
|
|
3015
|
-
selectedIds.add(r.entry.id);
|
|
3016
|
-
usedP += r.tokens;
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
const pinnedLocal = localEntries.filter((e) => e.pinned);
|
|
3020
|
-
const pinnedGlobal = globalEntries.filter((e) => e.pinned);
|
|
3021
|
-
if (pinnedLocal.length === 0 && pinnedGlobal.length === 0 && selectedItems.length === 0)
|
|
3022
|
-
return; // zero output
|
|
3023
|
-
const rankedPinned = [
|
|
3024
|
-
...pinnedLocal.map((e) => ({ entry: e, isGlobal: false })),
|
|
3025
|
-
...pinnedGlobal.map((e) => ({ entry: e, isGlobal: true })),
|
|
3026
|
-
]
|
|
3027
|
-
.map(({ entry, isGlobal }) => {
|
|
3028
|
-
const scopeSig = scopeMatch(entry.tags, ctxActiveScope);
|
|
3029
|
-
const sBst = scopeSig === 1 ? 1.5 : scopeSig === -1 ? 0.5 : 1.0;
|
|
3030
|
-
return {
|
|
3031
|
-
entry,
|
|
3032
|
-
score: calculateStrength(entry, nowP) * (isGlobal ? 1 / 1.2 : 1) * sBst,
|
|
3033
|
-
tokens: estimateTokens(entry.content),
|
|
3034
|
-
isGlobal,
|
|
3035
|
-
};
|
|
3036
|
-
})
|
|
3037
|
-
.sort((a, b) => b.score - a.score);
|
|
3038
|
-
for (const r of rankedPinned) {
|
|
3039
|
-
if (selectedIds.has(r.entry.id))
|
|
3040
|
-
continue;
|
|
3041
|
-
if (usedP + r.tokens > effBudget)
|
|
3042
|
-
continue;
|
|
3043
|
-
selectedItems.push(r);
|
|
3044
|
-
selectedIds.add(r.entry.id);
|
|
3045
|
-
usedP += r.tokens;
|
|
3046
|
-
}
|
|
3047
|
-
totalTokens = usedP;
|
|
3048
|
-
}
|
|
3049
|
-
else if (query === '*') {
|
|
3050
|
-
// No query: return strongest memories by strength, up to budget
|
|
3051
|
-
const now = new Date();
|
|
3052
|
-
const localRanked = localEntries
|
|
3053
|
-
.map((e) => ({
|
|
3054
|
-
entry: e,
|
|
3055
|
-
score: calculateStrength(e, now),
|
|
3056
|
-
tokens: estimateTokens(e.content),
|
|
3057
|
-
isGlobal: false,
|
|
3058
|
-
}))
|
|
3059
|
-
.sort((a, b) => b.score - a.score);
|
|
3060
|
-
const globalRanked = globalEntries
|
|
3061
|
-
.map((e) => ({
|
|
3062
|
-
entry: e,
|
|
3063
|
-
score: calculateStrength(e, now) * (1 / 1.2), // global slightly lower
|
|
3064
|
-
tokens: estimateTokens(e.content),
|
|
3065
|
-
isGlobal: true,
|
|
3066
|
-
}))
|
|
3067
|
-
.sort((a, b) => b.score - a.score);
|
|
3068
|
-
const combined = [...localRanked, ...globalRanked].sort((a, b) => b.score - a.score);
|
|
3069
|
-
let used = 0;
|
|
3070
|
-
for (const r of combined) {
|
|
3071
|
-
if (used + r.tokens > budget)
|
|
3072
|
-
continue;
|
|
3073
|
-
selectedItems.push(r);
|
|
3074
|
-
used += r.tokens;
|
|
3075
|
-
}
|
|
3076
|
-
totalTokens = used;
|
|
3077
|
-
}
|
|
3078
|
-
else {
|
|
3079
|
-
let results;
|
|
3080
|
-
if (hasGlobal) {
|
|
3081
|
-
const merged = await searchBothHybrid(query, hippoRoot, globalRoot, { budget, scope: ctxActiveScope, tenantId });
|
|
3082
|
-
const localIndex = loadIndex(hippoRoot);
|
|
3083
|
-
results = merged.map((r) => ({
|
|
3084
|
-
entry: r.entry,
|
|
3085
|
-
score: r.score,
|
|
3086
|
-
tokens: r.tokens,
|
|
3087
|
-
isGlobal: !localIndex.entries[r.entry.id],
|
|
3088
|
-
}));
|
|
3089
|
-
}
|
|
3090
|
-
else {
|
|
3091
|
-
const ctxConfig = loadConfig(hippoRoot);
|
|
3092
|
-
const usePhysicsCtx = ctxConfig.physics?.enabled !== false;
|
|
3093
|
-
const ctxResults = usePhysicsCtx
|
|
3094
|
-
? await physicsSearch(query, localEntries, { budget, hippoRoot, physicsConfig: ctxConfig.physics, scope: ctxActiveScope })
|
|
3095
|
-
: await hybridSearch(query, localEntries, { budget, hippoRoot, scope: ctxActiveScope });
|
|
3096
|
-
results = ctxResults.map((r) => ({
|
|
3097
|
-
entry: r.entry,
|
|
3098
|
-
score: r.score,
|
|
3099
|
-
tokens: r.tokens,
|
|
3100
|
-
isGlobal: false,
|
|
3101
|
-
}));
|
|
3102
|
-
}
|
|
3103
|
-
selectedItems = results;
|
|
3104
|
-
totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
3105
|
-
// A5 H4: emit recall audit event for context-mode searches. The recall
|
|
3106
|
-
// handler emits one of these per `hippo recall` invocation; context mode
|
|
3107
|
-
// is the same surface (search → user) and must leave the same audit trail.
|
|
3108
|
-
// Skip pinned-only and '*' fallback (handled in branches above which never
|
|
3109
|
-
// hit the search engines).
|
|
3110
|
-
const ctxRecallMetadata = {
|
|
3111
|
-
query: query.slice(0, 200),
|
|
3112
|
-
results: selectedItems.length,
|
|
3113
|
-
mode: 'context',
|
|
3114
|
-
};
|
|
3115
|
-
if (hasLocal)
|
|
3116
|
-
emitCliAudit(hippoRoot, 'recall', undefined, ctxRecallMetadata);
|
|
3117
|
-
if (hasGlobal)
|
|
3118
|
-
emitCliAudit(globalRoot, 'recall', undefined, ctxRecallMetadata);
|
|
3119
|
-
}
|
|
3120
|
-
if (limit < selectedItems.length) {
|
|
3121
|
-
selectedItems = selectedItems.slice(0, limit);
|
|
3122
|
-
totalTokens = selectedItems.reduce((sum, r) => sum + r.tokens, 0);
|
|
3123
|
-
}
|
|
3124
|
-
if (selectedItems.length === 0 && !activeSnapshot && !sessionHandoff && recentSessionEvents.length === 0)
|
|
2895
|
+
// Scope detection (CLI-side: uses cwd). api.getContext takes the resolved
|
|
2896
|
+
// scope via opts.scope to stay host-agnostic.
|
|
2897
|
+
const ctxExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
2898
|
+
const ctxActiveScope = ctxExplicitScope || detectScope();
|
|
2899
|
+
const ctx = {
|
|
2900
|
+
hippoRoot,
|
|
2901
|
+
tenantId: resolveTenantId({}),
|
|
2902
|
+
actor: 'cli',
|
|
2903
|
+
};
|
|
2904
|
+
const opts = {
|
|
2905
|
+
q: query,
|
|
2906
|
+
budget,
|
|
2907
|
+
limit: parseLimitFlag(flags['limit']),
|
|
2908
|
+
pinnedOnly,
|
|
2909
|
+
scope: ctxActiveScope ?? undefined,
|
|
2910
|
+
includeRecent: parseCountFlag(flags['include-recent']),
|
|
2911
|
+
};
|
|
2912
|
+
const result = await api.getContext(ctx, opts);
|
|
2913
|
+
// Early exit when there's nothing to render (matches pre-extraction behavior).
|
|
2914
|
+
const hasContextData = result.entries.length > 0 ||
|
|
2915
|
+
result.activeSnapshot ||
|
|
2916
|
+
result.sessionHandoff ||
|
|
2917
|
+
(result.recentEvents && result.recentEvents.length > 0);
|
|
2918
|
+
if (!hasContextData)
|
|
3125
2919
|
return;
|
|
3126
|
-
//
|
|
3127
|
-
// as read-only so pinned memories don't inflate retrieval_count or extend
|
|
3128
|
-
// their half_life by 2 days * turn-count over a long session.
|
|
3129
|
-
let updatedEntries;
|
|
3130
|
-
if (pinnedOnly) {
|
|
3131
|
-
updatedEntries = selectedItems.map((s) => s.entry);
|
|
3132
|
-
}
|
|
3133
|
-
else {
|
|
3134
|
-
// Mark retrieved and persist
|
|
3135
|
-
const toUpdate = selectedItems.map((s) => s.entry);
|
|
3136
|
-
updatedEntries = markRetrieved(toUpdate);
|
|
3137
|
-
const localIndex = loadIndex(hippoRoot);
|
|
3138
|
-
for (const u of updatedEntries) {
|
|
3139
|
-
const targetRoot = localIndex.entries[u.id] ? hippoRoot : (hasGlobal ? globalRoot : hippoRoot);
|
|
3140
|
-
writeEntry(targetRoot, u);
|
|
3141
|
-
}
|
|
3142
|
-
localIndex.last_retrieval_ids = updatedEntries.map((u) => u.id);
|
|
3143
|
-
saveIndex(hippoRoot, localIndex);
|
|
3144
|
-
updateStats(hippoRoot, { recalled: selectedItems.length });
|
|
3145
|
-
}
|
|
2920
|
+
// Format + framing are CLI rendering concerns; api.getContext doesn't see them.
|
|
3146
2921
|
const format = String(flags['format'] ?? 'markdown');
|
|
3147
2922
|
const framing = String(flags['framing'] ?? 'observe');
|
|
2923
|
+
// Adapter: ContextResultEntry -> the print-helper input shape.
|
|
2924
|
+
const renderItems = result.entries.map((r) => ({
|
|
2925
|
+
entry: r.entry,
|
|
2926
|
+
score: r.score,
|
|
2927
|
+
tokens: r.tokens,
|
|
2928
|
+
isGlobal: r.isGlobal ?? false,
|
|
2929
|
+
}));
|
|
3148
2930
|
if (format === 'json') {
|
|
3149
|
-
const output =
|
|
2931
|
+
const output = result.entries.map((r) => ({
|
|
3150
2932
|
id: r.entry.id,
|
|
3151
2933
|
score: r.score,
|
|
3152
2934
|
strength: r.entry.strength,
|
|
@@ -3155,28 +2937,31 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
3155
2937
|
content: r.entry.content,
|
|
3156
2938
|
global: r.isGlobal ?? false,
|
|
3157
2939
|
}));
|
|
3158
|
-
console.log(JSON.stringify({
|
|
2940
|
+
console.log(JSON.stringify({
|
|
2941
|
+
query: query || '*',
|
|
2942
|
+
activeSnapshot: result.activeSnapshot ?? null,
|
|
2943
|
+
sessionHandoff: result.sessionHandoff ?? null,
|
|
2944
|
+
recentSessionEvents: result.recentEvents ?? [],
|
|
2945
|
+
memories: output,
|
|
2946
|
+
tokens: result.tokens,
|
|
2947
|
+
}));
|
|
3159
2948
|
}
|
|
3160
2949
|
else if (format === 'additional-context') {
|
|
3161
|
-
// Claude Code UserPromptSubmit hook JSON shape. Capture
|
|
3162
|
-
//
|
|
2950
|
+
// Claude Code UserPromptSubmit hook JSON shape. Capture print* helpers'
|
|
2951
|
+
// output into a string buffer and wrap as `additionalContext`.
|
|
3163
2952
|
const lines = [];
|
|
3164
2953
|
const realLog = console.log;
|
|
3165
2954
|
console.log = (...parts) => { lines.push(parts.map(String).join(' ')); };
|
|
3166
2955
|
try {
|
|
3167
|
-
if (activeSnapshot)
|
|
3168
|
-
printActiveTaskSnapshot(activeSnapshot);
|
|
3169
|
-
if (sessionHandoff)
|
|
3170
|
-
printHandoff(sessionHandoff);
|
|
3171
|
-
if (
|
|
3172
|
-
printSessionEvents(
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
score: r.score,
|
|
3177
|
-
tokens: r.tokens,
|
|
3178
|
-
isGlobal: r.isGlobal ?? false,
|
|
3179
|
-
})), totalTokens, framing);
|
|
2956
|
+
if (result.activeSnapshot)
|
|
2957
|
+
printActiveTaskSnapshot(result.activeSnapshot);
|
|
2958
|
+
if (result.sessionHandoff)
|
|
2959
|
+
printHandoff(result.sessionHandoff);
|
|
2960
|
+
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
2961
|
+
printSessionEvents(result.recentEvents);
|
|
2962
|
+
}
|
|
2963
|
+
if (renderItems.length > 0) {
|
|
2964
|
+
printContextMarkdown(renderItems, result.tokens, framing);
|
|
3180
2965
|
}
|
|
3181
2966
|
}
|
|
3182
2967
|
finally {
|
|
@@ -3194,27 +2979,33 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
3194
2979
|
process.stdout.write(JSON.stringify(payload));
|
|
3195
2980
|
}
|
|
3196
2981
|
else {
|
|
3197
|
-
|
|
3198
|
-
|
|
2982
|
+
// markdown (default)
|
|
2983
|
+
if (result.activeSnapshot) {
|
|
2984
|
+
printActiveTaskSnapshot(result.activeSnapshot);
|
|
3199
2985
|
}
|
|
3200
|
-
if (sessionHandoff) {
|
|
3201
|
-
printHandoff(sessionHandoff);
|
|
2986
|
+
if (result.sessionHandoff) {
|
|
2987
|
+
printHandoff(result.sessionHandoff);
|
|
3202
2988
|
}
|
|
3203
|
-
if (
|
|
3204
|
-
printSessionEvents(
|
|
2989
|
+
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
2990
|
+
printSessionEvents(result.recentEvents);
|
|
3205
2991
|
}
|
|
3206
|
-
if (
|
|
3207
|
-
printContextMarkdown(
|
|
3208
|
-
entry: updatedEntries.find((u) => u.id === r.entry.id) ?? r.entry,
|
|
3209
|
-
score: r.score,
|
|
3210
|
-
tokens: r.tokens,
|
|
3211
|
-
isGlobal: r.isGlobal ?? false,
|
|
3212
|
-
})), totalTokens, framing);
|
|
2992
|
+
if (renderItems.length > 0) {
|
|
2993
|
+
printContextMarkdown(renderItems, result.tokens, framing);
|
|
3213
2994
|
}
|
|
3214
|
-
// Ambient state summary (
|
|
2995
|
+
// Ambient state summary (CLI-side: api.getContext doesn't load all entries
|
|
2996
|
+
// post-selection, so we re-load here for the landscape summary).
|
|
3215
2997
|
const ambientConfig = loadConfig(hippoRoot);
|
|
3216
2998
|
if (ambientConfig.ambient.enabled && !pinnedOnly) {
|
|
3217
|
-
const
|
|
2999
|
+
const tenantId = ctx.tenantId;
|
|
3000
|
+
const globalRoot = getGlobalRoot();
|
|
3001
|
+
const hasGlobalForAmbient = isInitialized(globalRoot);
|
|
3002
|
+
const localForAmbient = isInitialized(hippoRoot)
|
|
3003
|
+
? loadAllEntries(hippoRoot, tenantId).filter(e => !e.superseded_by)
|
|
3004
|
+
: [];
|
|
3005
|
+
const globalForAmbient = hasGlobalForAmbient
|
|
3006
|
+
? loadAllEntries(globalRoot, tenantId).filter(e => !e.superseded_by)
|
|
3007
|
+
: [];
|
|
3008
|
+
const allForAmbient = [...localForAmbient, ...globalForAmbient];
|
|
3218
3009
|
if (allForAmbient.length > 0) {
|
|
3219
3010
|
const ambientState = computeAmbientState(allForAmbient);
|
|
3220
3011
|
console.log(`\n${renderAmbientSummary(ambientState)}`);
|