hippo-memory 1.52.9 → 1.53.1
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 +47 -13
- package/dist/agent-memories/apply.d.ts +47 -0
- package/dist/agent-memories/apply.js +253 -0
- package/dist/agent-memories/claude-code.d.ts +11 -0
- package/dist/agent-memories/claude-code.js +113 -0
- package/dist/agent-memories/codex.d.ts +3 -0
- package/dist/agent-memories/codex.js +47 -0
- package/dist/agent-memories/copilot.d.ts +3 -0
- package/dist/agent-memories/copilot.js +125 -0
- package/dist/agent-memories/files.d.ts +37 -0
- package/dist/agent-memories/files.js +77 -0
- package/dist/agent-memories/folder-store.d.ts +17 -0
- package/dist/agent-memories/folder-store.js +44 -0
- package/dist/agent-memories/gemini.d.ts +3 -0
- package/dist/agent-memories/gemini.js +103 -0
- package/dist/agent-memories/git.d.ts +8 -0
- package/dist/agent-memories/git.js +11 -0
- package/dist/agent-memories/keys.d.ts +9 -0
- package/dist/agent-memories/keys.js +20 -0
- package/dist/agent-memories/legacy.d.ts +17 -0
- package/dist/agent-memories/legacy.js +45 -0
- package/dist/agent-memories/markdown.d.ts +13 -0
- package/dist/agent-memories/markdown.js +123 -0
- package/dist/agent-memories/openclaw.d.ts +3 -0
- package/dist/agent-memories/openclaw.js +42 -0
- package/dist/agent-memories/plan.d.ts +78 -0
- package/dist/agent-memories/plan.js +123 -0
- package/dist/agent-memories/qwen-code.d.ts +5 -0
- package/dist/agent-memories/qwen-code.js +50 -0
- package/dist/agent-memories/report.d.ts +52 -0
- package/dist/agent-memories/report.js +88 -0
- package/dist/agent-memories/source.d.ts +16 -0
- package/dist/agent-memories/source.js +32 -0
- package/dist/agent-memories/sync.d.ts +33 -0
- package/dist/agent-memories/sync.js +336 -0
- package/dist/agent-memories/tools.d.ts +33 -0
- package/dist/agent-memories/tools.js +19 -0
- package/dist/agent-memories/types.d.ts +42 -0
- package/dist/agent-memories/types.js +2 -0
- package/dist/api.d.ts +2 -2
- package/dist/api.js +28 -29
- package/dist/audit.d.ts +4 -3
- package/dist/audit.js +10 -6
- package/dist/capture.d.ts +11 -22
- package/dist/capture.js +76 -81
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +133 -170
- package/dist/compaction-items.d.ts +18 -0
- package/dist/compaction-items.js +60 -0
- package/dist/compaction-record.d.ts +94 -0
- package/dist/compaction-record.js +573 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +13 -0
- package/dist/consolidate.d.ts +0 -2
- package/dist/consolidate.js +3 -36
- package/dist/db.d.ts +5 -1
- package/dist/db.js +40 -8
- package/dist/dedupe.js +3 -2
- package/dist/doctor.js +34 -2
- package/dist/dormant.d.ts +5 -3
- package/dist/dormant.js +9 -0
- package/dist/gated-write.d.ts +9 -0
- package/dist/gated-write.js +24 -0
- package/dist/hooks.d.ts +4 -2
- package/dist/hooks.js +8 -7
- package/dist/memory.d.ts +19 -2
- package/dist/memory.js +32 -3
- package/dist/shared.js +10 -7
- package/dist/store.d.ts +14 -2
- package/dist/store.js +75 -19
- 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/cli.js
CHANGED
|
@@ -37,11 +37,10 @@ import * as path from 'path';
|
|
|
37
37
|
import * as fs from 'fs';
|
|
38
38
|
import * as os from 'os';
|
|
39
39
|
import { fileURLToPath } from 'node:url';
|
|
40
|
-
import { execFileSync, execSync, spawn
|
|
40
|
+
import { execFileSync, execSync, spawn } from 'child_process';
|
|
41
41
|
import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, detectRealCodexPath, isCodexPresent, isCodexWrapperInstalled, CODEX_TRUST_LINE, repairCodexWrapperIfInstalled, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
|
|
42
|
-
import { createMemory, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, confidenceFacets, confidenceLabel, computeSchemaFit, Layer, } from './memory.js';
|
|
43
|
-
import {
|
|
44
|
-
import { getHippoRoot, isInitialized, initStore, writeEntry, strengthenRetrieved, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, createCard, loadCard, listCards, loadCardRuns, claimCard, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, } from './store.js';
|
|
42
|
+
import { createMemory, createSuccessor, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, confidenceFacets, confidenceLabel, computeSchemaFit, Layer, } from './memory.js';
|
|
43
|
+
import { getHippoRoot, isInitialized, initStore, writeEntry, strengthenRetrieved, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, createCard, loadCard, listCards, loadCardRuns, claimCard, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, memoriesBackingObjects, } from './store.js';
|
|
45
44
|
import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
|
|
46
45
|
import { RejectedValueError } from './rejection.js';
|
|
47
46
|
import { isHandoffOutcome, formatHandoffEvidenceLine } from './handoff.js';
|
|
@@ -70,14 +69,17 @@ import { pushGoal, getActiveGoals, completeGoal, suspendGoal, resumeGoal, applyG
|
|
|
70
69
|
import { rowToGoal } from './goals.js';
|
|
71
70
|
import { captureError, extractLessons, partitionLessons, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
|
|
72
71
|
import { dropHeldCopies, duplicateKey, storedTextKeys } from './same-text.js';
|
|
72
|
+
import { currentMachine, importAtCompaction, importAtSessionEnd, importForStore, importProjectMemories, importUserMemories, } from './agent-memories/sync.js';
|
|
73
|
+
import { detailLines, emptyReport, mergeReports, summaryLine } from './agent-memories/report.js';
|
|
73
74
|
import { extractInvalidationTarget, invalidateMatching, detectChurnStale } from './invalidation.js';
|
|
74
|
-
import { deriveOriginProject,
|
|
75
|
+
import { deriveOriginProject, isGlobalStoreRoot, resolveProjectIdentity } from './project-identity.js';
|
|
75
76
|
import { extractPathTags } from './path-context.js';
|
|
76
77
|
import { detectScope } from './scope.js';
|
|
77
78
|
import { getGlobalRoot, initGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
|
|
78
79
|
import { DAILY_TASK_NAME, buildDailyRunnerCommand, buildSchtasksCreateArgs, buildWindowsTaskRun, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
79
80
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, importVault, } from './importers.js';
|
|
80
|
-
import { cmdCapture, cmdPreCompact,
|
|
81
|
+
import { cmdCapture, cmdPreCompact, cmdPostCompact, resolveLastSessionTranscript, truncateCodePointSafe, sanitizeLogMessage, transcriptWorkingState } from './capture.js';
|
|
82
|
+
import { COMPACTION_DB_WAIT_MS, replayCompactionsAt } from './compaction-record.js';
|
|
81
83
|
import { readStdinBounded } from './stdin.js';
|
|
82
84
|
import { auditMemories, appendAuditEvent, AUDIT_OPS, } from './audit.js';
|
|
83
85
|
import { listApiKeys, revokeApiKey } from './auth.js';
|
|
@@ -285,7 +287,7 @@ async function runViaServerIfAvailable(hippoRoot, httpFn) {
|
|
|
285
287
|
// and as off under === true (`--pin=true` would not pin), so parseArgs and main() refuse one.
|
|
286
288
|
// tests/cli-parse-flag-equals.test.ts fails when a switch read is missing from this set.
|
|
287
289
|
export const BOOLEAN_FLAGS = new Set([
|
|
288
|
-
'all', 'all-tenants', 'archive', 'auto', 'bad', 'bootstrap', 'classic', 'churn', 'continuity',
|
|
290
|
+
'agents', 'all', 'all-tenants', 'archive', 'auto', 'bad', 'bootstrap', 'classic', 'churn', 'continuity',
|
|
289
291
|
'cross-project', 'dry-run', 'equal-sources', 'error', 'evc-adaptive', 'extract',
|
|
290
292
|
'filter-conflicts', 'fix', 'force', 'forget', 'git', 'global', 'good', 'graph-stream',
|
|
291
293
|
'help', 'include-logs', 'include-superseded', 'inferred', 'json', 'last-session', 'multihop', 'no-hooks',
|
|
@@ -528,6 +530,8 @@ function cmdInitScan(scanDir, flags) {
|
|
|
528
530
|
// that a dropped subject is never invisible.
|
|
529
531
|
let totalLowInfo = 0;
|
|
530
532
|
const seedDays = parseInt(String(flags['days'] ?? '365'), 10);
|
|
533
|
+
const machine = currentMachine();
|
|
534
|
+
const agentImport = emptyReport();
|
|
531
535
|
for (const repo of repos) {
|
|
532
536
|
const name = path.basename(repo);
|
|
533
537
|
const repoHippo = path.join(repo, '.hippo');
|
|
@@ -544,6 +548,8 @@ function cmdInitScan(scanDir, flags) {
|
|
|
544
548
|
totalLessons += added;
|
|
545
549
|
totalLowInfo += result.lowInfo;
|
|
546
550
|
}
|
|
551
|
+
if (!flags['no-learn'])
|
|
552
|
+
mergeReports(agentImport, importProjectMemories(repoHippo, { machine }));
|
|
547
553
|
const status = alreadyExists ? 'existing' : 'new';
|
|
548
554
|
const entries = loadAllEntries(repoHippo);
|
|
549
555
|
console.log(` ${name.padEnd(25)} ${status.padEnd(10)} ${entries.length} memories${added > 0 ? ` (+${added} from git)` : ''}`);
|
|
@@ -551,6 +557,10 @@ function cmdInitScan(scanDir, flags) {
|
|
|
551
557
|
console.log(`\n${repos.length} repositories, ${totalLessons} new lessons learned` +
|
|
552
558
|
(totalLowInfo > 0 ? `, ${totalLowInfo} low-information subject(s) dropped` : '') +
|
|
553
559
|
'.');
|
|
560
|
+
if (!flags['no-learn']) {
|
|
561
|
+
mergeReports(agentImport, importUserMemories(globalRoot, { machine }));
|
|
562
|
+
printAgentImport(agentImport, '');
|
|
563
|
+
}
|
|
554
564
|
console.log(`Global store: ${globalRoot}`);
|
|
555
565
|
if (initInstallsIntegrations(flags)) {
|
|
556
566
|
// User-level hooks only: a hippo block in each repo's CLAUDE.md or AGENTS.md would leave a diff in every repo.
|
|
@@ -576,10 +586,13 @@ function cmdInit(hippoRoot, flags) {
|
|
|
576
586
|
const globalRoot = getGlobalRoot();
|
|
577
587
|
if (isInitialized(globalRoot)) {
|
|
578
588
|
console.log('Already initialized global store at', globalRoot);
|
|
579
|
-
return;
|
|
580
589
|
}
|
|
581
|
-
|
|
582
|
-
|
|
590
|
+
else {
|
|
591
|
+
initGlobal();
|
|
592
|
+
console.log('Initialized global Hippo store at', globalRoot);
|
|
593
|
+
}
|
|
594
|
+
if (!flags['no-learn'])
|
|
595
|
+
printAgentImport(importUserMemories(globalRoot, { machine: currentMachine() }));
|
|
583
596
|
return;
|
|
584
597
|
}
|
|
585
598
|
const alreadyExists = isInitialized(hippoRoot);
|
|
@@ -614,11 +627,18 @@ function cmdInit(hippoRoot, flags) {
|
|
|
614
627
|
console.log(` No matching commits found in git history.`);
|
|
615
628
|
}
|
|
616
629
|
}
|
|
617
|
-
const memImported = learnFromMemoryMd(hippoRoot);
|
|
618
|
-
if (memImported > 0) {
|
|
619
|
-
console.log(` Imported ${memImported} memories from this project's Claude Code auto memory.`);
|
|
620
|
-
}
|
|
621
630
|
}
|
|
631
|
+
// Every run, not only the first: an agent's notes change between inits.
|
|
632
|
+
if (!flags['no-learn'])
|
|
633
|
+
printAgentImport(importForStore(hippoRoot, { machine: currentMachine() }));
|
|
634
|
+
}
|
|
635
|
+
/** One line when an agent memory import moved anything; its warnings go to stderr. */
|
|
636
|
+
function printAgentImport(report, indent = ' ') {
|
|
637
|
+
const line = summaryLine(report);
|
|
638
|
+
if (line !== null)
|
|
639
|
+
console.log(`${indent}${line}`);
|
|
640
|
+
for (const warning of report.warnings)
|
|
641
|
+
console.error(`hippo: agent memories: ${warning}`);
|
|
622
642
|
}
|
|
623
643
|
/** Every write init makes into agent config (instruction blocks, hooks, plugins) is an automatic integration, so one switch skips them all. */
|
|
624
644
|
function initInstallsIntegrations(flags) {
|
|
@@ -990,23 +1010,20 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
|
990
1010
|
console.error(`Error: memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
|
|
991
1011
|
process.exit(1);
|
|
992
1012
|
}
|
|
993
|
-
const layer =
|
|
1013
|
+
const layer = typeof flags['layer'] === 'string' ? flags['layer'] : undefined;
|
|
994
1014
|
const rawTags = flags['tag'];
|
|
995
1015
|
const tags = Array.isArray(rawTags)
|
|
996
1016
|
? rawTags.map((t) => String(t))
|
|
997
1017
|
: typeof rawTags === 'string'
|
|
998
1018
|
? rawTags.split(',').map((t) => t.trim()).filter(Boolean)
|
|
999
|
-
:
|
|
1019
|
+
: undefined;
|
|
1000
1020
|
const pinned = flags['pin'] === true || old.pinned;
|
|
1001
|
-
const newEntry =
|
|
1021
|
+
const newEntry = createSuccessor(old, newContent, {
|
|
1022
|
+
tenantId: old.tenantId,
|
|
1023
|
+
baseHalfLifeDays: loadConfig(hippoRoot).defaultHalfLifeDays,
|
|
1002
1024
|
layer,
|
|
1003
1025
|
tags,
|
|
1004
1026
|
pinned,
|
|
1005
|
-
source: old.source,
|
|
1006
|
-
confidence: 'verified',
|
|
1007
|
-
tenantId: old.tenantId,
|
|
1008
|
-
scope: old.scope,
|
|
1009
|
-
baseHalfLifeDays: loadConfig(hippoRoot).defaultHalfLifeDays,
|
|
1010
1027
|
});
|
|
1011
1028
|
// AT1: write the SUCCESSOR first. The rejection guard fires on the new
|
|
1012
1029
|
// content — if it refuses, nothing has been mutated yet (the old ordering
|
|
@@ -1047,6 +1064,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1047
1064
|
process.exit(1);
|
|
1048
1065
|
}
|
|
1049
1066
|
const globalRoot = getGlobalRoot();
|
|
1067
|
+
const primaryIsGlobal = isGlobalStoreRoot(hippoRoot);
|
|
1050
1068
|
// A5 stub auth: resolve the active tenant once and thread it through every
|
|
1051
1069
|
// recall-time SELECT against `memories`. Cross-tenant rows must never surface.
|
|
1052
1070
|
const tenantId = resolveTenantId({});
|
|
@@ -1066,7 +1084,9 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1066
1084
|
const requestedScopeForFilter = recallExplicitScope || undefined;
|
|
1067
1085
|
const loadSuperseded = includeSuperseded || Boolean(asOf);
|
|
1068
1086
|
let localEntries = loadRecallSearchEntries(hippoRoot, query, undefined, tenantId, requestedScopeForFilter, 'additive', loadSuperseded);
|
|
1069
|
-
let globalEntries =
|
|
1087
|
+
let globalEntries = globalRoot !== hippoRoot && isInitialized(globalRoot)
|
|
1088
|
+
? loadRecallSearchEntries(globalRoot, query, undefined, tenantId, requestedScopeForFilter, 'additive', loadSuperseded)
|
|
1089
|
+
: [];
|
|
1070
1090
|
// v1.12.13 / C5 — WYSIATI counters. Track filter activity per the plan v3
|
|
1071
1091
|
// Task 3 mapping table. dropped_pre_rank is the SUM of all non-budget
|
|
1072
1092
|
// filter drops (pre-rank AND post-rank); its meaning is unchanged by C5.
|
|
@@ -1192,7 +1212,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1192
1212
|
// Engines spend the budget on the text each result prints as, less the header, so selection and print agree.
|
|
1193
1213
|
const localIndex = loadIndex(hippoRoot);
|
|
1194
1214
|
const globalOn = isInitialized(globalRoot);
|
|
1195
|
-
const entryText = (r) => recallEntryText(r, query, showWhy, globalOn && !localIndex.entries[r.entry.id]);
|
|
1215
|
+
const entryText = (r) => recallEntryText(r, query, showWhy, primaryIsGlobal || (globalOn && !localIndex.entries[r.entry.id]));
|
|
1196
1216
|
const printCost = (r) => printedTokens(entryText(r));
|
|
1197
1217
|
const entryBudget = Math.max(0, budget - printedTokens(recallHeading(budget, budget, query)));
|
|
1198
1218
|
let results;
|
|
@@ -1699,7 +1719,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1699
1719
|
let activeSnapshot = null;
|
|
1700
1720
|
let sessionHandoff = null;
|
|
1701
1721
|
let recentSessionEvents = [];
|
|
1702
|
-
if (includeContinuity) {
|
|
1722
|
+
if (includeContinuity && !primaryIsGlobal) {
|
|
1703
1723
|
const rawSnapshot = loadActiveTaskSnapshot(hippoRoot, tenantId);
|
|
1704
1724
|
const sessionId = rawSnapshot?.session_id ?? undefined;
|
|
1705
1725
|
const rawHandoff = sessionId
|
|
@@ -1958,7 +1978,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1958
1978
|
updateStats(hippoRoot, { recalled: results.length });
|
|
1959
1979
|
if (asJson) {
|
|
1960
1980
|
const output = results.map((r) => {
|
|
1961
|
-
const isGlobal = isInitialized(globalRoot) && !localIndex.entries[r.entry.id];
|
|
1981
|
+
const isGlobal = primaryIsGlobal || (isInitialized(globalRoot) && !localIndex.entries[r.entry.id]);
|
|
1962
1982
|
const base = {
|
|
1963
1983
|
id: r.entry.id,
|
|
1964
1984
|
score: r.score,
|
|
@@ -2605,112 +2625,6 @@ async function cmdRefine(hippoRoot, flags) {
|
|
|
2605
2625
|
}
|
|
2606
2626
|
}
|
|
2607
2627
|
}
|
|
2608
|
-
/** Claude Code's auto memory folder names for a project: its checkout, which subfolders share, or the folder itself outside a repository. */
|
|
2609
|
-
function claudeMemoryFolderNames(projectRoot) {
|
|
2610
|
-
const roots = [projectRoot, realpathOrResolve(projectRoot)];
|
|
2611
|
-
const git = spawnSync('git', ['rev-parse', '--path-format=absolute', '--show-toplevel', '--absolute-git-dir', '--git-common-dir'], { cwd: projectRoot, encoding: 'utf8', timeout: 10000, windowsHide: true });
|
|
2612
|
-
if (git.status === 0) {
|
|
2613
|
-
const [top, gitDir, common] = git.stdout.trim().split(/\r?\n/);
|
|
2614
|
-
roots.push(claudeCheckoutRoot(top, gitDir, common));
|
|
2615
|
-
}
|
|
2616
|
-
return new Set(roots.map((root) => (process.platform === 'win32' ? claudeFolderName(root).toLowerCase() : claudeFolderName(root))));
|
|
2617
|
-
}
|
|
2618
|
-
/** Claude Code's rule: a linked worktree shares its main checkout's folder, or the git folder's when that sits outside a checkout (a bare repository, or --separate-git-dir); any other checkout, a submodule included, keeps its own. */
|
|
2619
|
-
function claudeCheckoutRoot(top, gitDir, common) {
|
|
2620
|
-
if (gitDir === common)
|
|
2621
|
-
return top;
|
|
2622
|
-
if (path.basename(common) === '.git')
|
|
2623
|
-
return path.dirname(common);
|
|
2624
|
-
return fs.existsSync(path.join(common, '.git')) ? top : common;
|
|
2625
|
-
}
|
|
2626
|
-
/** Claude Code's folder name for a path: non-alphanumerics made '-', and a name over 200 characters cut to 200 plus a base-36 hash of the whole path. */
|
|
2627
|
-
function claudeFolderName(root) {
|
|
2628
|
-
const full = path.resolve(root);
|
|
2629
|
-
const name = full.replace(/[^a-zA-Z0-9]/g, '-');
|
|
2630
|
-
if (name.length <= 200)
|
|
2631
|
-
return name;
|
|
2632
|
-
let hash = 0;
|
|
2633
|
-
for (let i = 0; i < full.length; i++)
|
|
2634
|
-
hash = ((hash << 5) - hash + full.charCodeAt(i)) | 0;
|
|
2635
|
-
return `${name.slice(0, 200)}-${Math.abs(hash).toString(36)}`;
|
|
2636
|
-
}
|
|
2637
|
-
/** Import new entries from the Claude Code auto memory of the store's project, the frontmatter .md files in ~/.claude/projects/<project>/memory/. */
|
|
2638
|
-
export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
2639
|
-
// Only this project's folder, because every entry written here counts as this project's memory.
|
|
2640
|
-
// SHORTCUT: an autoMemoryDirectory or CLAUDE_CODE_PROJECT_DIR_NAME setting imports nothing; read Claude's settings if users ask.
|
|
2641
|
-
const claudeProjectsDir = path.join(homeDir, '.claude', 'projects');
|
|
2642
|
-
const memoryDirs = [...claudeMemoryFolderNames(path.dirname(hippoRoot))]
|
|
2643
|
-
.map((folder) => path.join(claudeProjectsDir, folder, 'memory'))
|
|
2644
|
-
.filter((memDir) => fs.existsSync(memDir));
|
|
2645
|
-
if (memoryDirs.length === 0)
|
|
2646
|
-
return 0;
|
|
2647
|
-
const keys = storedTextKeys(loadAllEntries(hippoRoot, resolveTenantId({})));
|
|
2648
|
-
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
2649
|
-
let imported = 0;
|
|
2650
|
-
let skippedSecret = 0;
|
|
2651
|
-
// AT1 (plan §3 containment): a rejection guard refusal is per-VALUE — one
|
|
2652
|
-
// tombstoned memory file must not abort the whole directory scan. No
|
|
2653
|
-
// signature change (bare number return, cli.ts:540 + cli.ts:2903 callers
|
|
2654
|
-
// unchanged) — counted internally and printed as one summary line.
|
|
2655
|
-
let rejected = 0;
|
|
2656
|
-
for (const memDir of memoryDirs) {
|
|
2657
|
-
try {
|
|
2658
|
-
const files = fs.readdirSync(memDir).filter(f => f.endsWith('.md') && f !== 'MEMORY.md');
|
|
2659
|
-
for (const file of files) {
|
|
2660
|
-
const raw = fs.readFileSync(path.join(memDir, file), 'utf8');
|
|
2661
|
-
// Parse YAML frontmatter
|
|
2662
|
-
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
2663
|
-
if (!fmMatch)
|
|
2664
|
-
continue;
|
|
2665
|
-
const body = fmMatch[2].trim();
|
|
2666
|
-
if (!body || body.length < 10)
|
|
2667
|
-
continue;
|
|
2668
|
-
// Truncate to reasonable size
|
|
2669
|
-
const content = body.length > 1500 ? body.slice(0, 1500) + ' [truncated]' : body;
|
|
2670
|
-
// Never ingest secret-bearing memory files. Some Claude Code memory
|
|
2671
|
-
// files exist purely to hold a live credential (e.g. an API-key
|
|
2672
|
-
// reference). The scope-isolation secret veto (v1.24.0) gated
|
|
2673
|
-
// share/promote/sync/ambient but missed this import path, so a live
|
|
2674
|
-
// key could land in the store here. Veto it at ingest. (v1.24.1)
|
|
2675
|
-
if (detectSecret({ content, tags: ['claude-code-memory'] }).flagged) {
|
|
2676
|
-
skippedSecret++;
|
|
2677
|
-
continue;
|
|
2678
|
-
}
|
|
2679
|
-
// Dedup: skip only when the same text is already stored
|
|
2680
|
-
if (keys.has(duplicateKey(content)))
|
|
2681
|
-
continue;
|
|
2682
|
-
const entry = createMemory(content, {
|
|
2683
|
-
layer: Layer.Episodic,
|
|
2684
|
-
tags: ['claude-code-memory'],
|
|
2685
|
-
source: `claude-memory:${file}`,
|
|
2686
|
-
confidence: 'observed',
|
|
2687
|
-
tenantId: resolveTenantId({}),
|
|
2688
|
-
baseHalfLifeDays,
|
|
2689
|
-
});
|
|
2690
|
-
try {
|
|
2691
|
-
writeEntry(hippoRoot, entry);
|
|
2692
|
-
}
|
|
2693
|
-
catch (err) {
|
|
2694
|
-
if (err instanceof RejectedValueError) {
|
|
2695
|
-
rejected++;
|
|
2696
|
-
continue;
|
|
2697
|
-
}
|
|
2698
|
-
throw err;
|
|
2699
|
-
}
|
|
2700
|
-
keys.add(duplicateKey(content)); // prevent self-dedup within batch
|
|
2701
|
-
imported++;
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
catch { /* skip broken dirs */ }
|
|
2705
|
-
}
|
|
2706
|
-
if (skippedSecret > 0) {
|
|
2707
|
-
console.log(`Skipped ${skippedSecret} secret-bearing memory file${skippedSecret === 1 ? '' : 's'} (not ingested).`);
|
|
2708
|
-
}
|
|
2709
|
-
if (rejected > 0) {
|
|
2710
|
-
console.log(`Skipped ${rejected} rejected value${rejected === 1 ? '' : 's'} (run \`hippo unreject\` to allow).`);
|
|
2711
|
-
}
|
|
2712
|
-
return imported;
|
|
2713
|
-
}
|
|
2714
2628
|
function cmdDedup(hippoRoot, flags) {
|
|
2715
2629
|
requireInit(hippoRoot);
|
|
2716
2630
|
const dryRun = Boolean(flags['dry-run']);
|
|
@@ -2871,10 +2785,10 @@ export function renderSleepResult(result) {
|
|
|
2871
2785
|
}
|
|
2872
2786
|
async function cmdSleepCore(hippoRoot, flags) {
|
|
2873
2787
|
requireInit(hippoRoot);
|
|
2874
|
-
// Phase 1: Auto-learn from git
|
|
2788
|
+
// Phase 1: Auto-learn from git and every coding agent's own memories (CLI-only, uses process.cwd() / os.homedir()).
|
|
2875
2789
|
// Stays in cli.ts; api.sleep covers Phase 2-6 only.
|
|
2876
2790
|
if (!flags['no-learn'] && flags['dry-run']) {
|
|
2877
|
-
console.log("Dry run: skipped learning from git commits and
|
|
2791
|
+
console.log("Dry run: skipped learning from git commits and coding agents' own memories (`hippo import --agents --dry-run` previews those).");
|
|
2878
2792
|
}
|
|
2879
2793
|
else if (!flags['no-learn']) {
|
|
2880
2794
|
const config = loadConfig(hippoRoot);
|
|
@@ -2892,9 +2806,13 @@ async function cmdSleepCore(hippoRoot, flags) {
|
|
|
2892
2806
|
console.error(`Churn-staleness check failed for ${root}: ${result.error}`);
|
|
2893
2807
|
}
|
|
2894
2808
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2809
|
+
printAgentImport(importForStore(hippoRoot, { machine: currentMachine() }), '');
|
|
2810
|
+
}
|
|
2811
|
+
// Finishes compactions a killed or busy post-compact hook left; never throws, and a dry run writes nothing.
|
|
2812
|
+
if (!flags['dry-run']) {
|
|
2813
|
+
const finished = replayCompactionsAt(hippoRoot, (message) => console.error(`compaction replay: ${message}`));
|
|
2814
|
+
if (finished > 0)
|
|
2815
|
+
console.log(`Finished saving ${finished} compaction${finished === 1 ? '' : 's'} left over from earlier sessions.`);
|
|
2898
2816
|
}
|
|
2899
2817
|
// Phase 2-6: Pure-storage pipeline (consolidate + dedup + audit + share + ambient).
|
|
2900
2818
|
const ctx = {
|
|
@@ -3068,7 +2986,7 @@ async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
3068
2986
|
// Bounded read (DF1 T3, docs/plans/2026-08-23-df1-snapshot-lifecycle.md):
|
|
3069
2987
|
// extracts transcript_path + session_id for the detached worker's argv.
|
|
3070
2988
|
let sessionId = null;
|
|
3071
|
-
const { text: stdinText
|
|
2989
|
+
const { text: stdinText } = await readStdinBounded();
|
|
3072
2990
|
try {
|
|
3073
2991
|
if (stdinText && stdinText.trim().startsWith('{')) {
|
|
3074
2992
|
const payload = JSON.parse(stdinText);
|
|
@@ -3081,7 +2999,8 @@ async function cmdSessionEnd(hippoRoot, flags) {
|
|
|
3081
2999
|
// No stdin, not JSON, or read failure: the snapshot close below will no-op.
|
|
3082
3000
|
}
|
|
3083
3001
|
// Resolved here because only this process saw the payload; the worker captures just the path it is handed.
|
|
3084
|
-
|
|
3002
|
+
// Always a hook, so never scan: an empty stdin here is not a manual run.
|
|
3003
|
+
const transcriptPath = resolveLastSessionTranscript(undefined, stdinText, { mayScan: false });
|
|
3085
3004
|
const workerArgs = [process.argv[1], '__session-end-worker'];
|
|
3086
3005
|
if (logFile)
|
|
3087
3006
|
workerArgs.push('--log-file', logFile);
|
|
@@ -3136,6 +3055,20 @@ function collectHandoffEvidence(cwd, testStatus) {
|
|
|
3136
3055
|
}
|
|
3137
3056
|
return { gitRef, dirtyTree, testStatus };
|
|
3138
3057
|
}
|
|
3058
|
+
/** A folder without its own store never sleeps at session end, so its project's agent notes go to the global store here. */
|
|
3059
|
+
function logSessionEndImport(logFile, transcriptPath) {
|
|
3060
|
+
try {
|
|
3061
|
+
const report = importAtSessionEnd(process.cwd(), transcriptPath, { machine: currentMachine() });
|
|
3062
|
+
const line = summaryLine(report);
|
|
3063
|
+
if (line !== null)
|
|
3064
|
+
appendSessionEndCloseLog(logFile, line);
|
|
3065
|
+
for (const warning of report.warnings)
|
|
3066
|
+
appendSessionEndCloseLog(logFile, `agent memories: ${warning}`);
|
|
3067
|
+
}
|
|
3068
|
+
catch (err) {
|
|
3069
|
+
appendSessionEndCloseLog(logFile, `agent memory import failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3139
3072
|
async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
3140
3073
|
const transcriptPath = typeof flags['transcript'] === 'string' ? flags['transcript'] : undefined;
|
|
3141
3074
|
const closeLogFile = typeof flags['log-file'] === 'string' ? flags['log-file'] : null;
|
|
@@ -3165,6 +3098,7 @@ async function cmdSessionEndWorker(hippoRoot, flags) {
|
|
|
3165
3098
|
}
|
|
3166
3099
|
else {
|
|
3167
3100
|
appendSessionEndCloseLog(closeLogFile, 'skip sleep: this folder has no store of its own', { startFresh: true });
|
|
3101
|
+
logSessionEndImport(closeLogFile, transcriptPath);
|
|
3168
3102
|
}
|
|
3169
3103
|
flushRereadLog();
|
|
3170
3104
|
const digestLog = (message) => appendSessionEndCloseLog(closeLogFile, message);
|
|
@@ -3407,6 +3341,7 @@ async function cmdCodexSessionEndWorker(hippoRoot, flags) {
|
|
|
3407
3341
|
}
|
|
3408
3342
|
else {
|
|
3409
3343
|
appendSessionEndCloseLog(logFile ?? null, 'skip sleep: this folder has no store of its own', { startFresh: true });
|
|
3344
|
+
logSessionEndImport(logFile ?? null, undefined);
|
|
3410
3345
|
}
|
|
3411
3346
|
try {
|
|
3412
3347
|
const codexHome = typeof flags['codex-home'] === 'string'
|
|
@@ -7000,6 +6935,18 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
7000
6935
|
? [String(flags['tag'])]
|
|
7001
6936
|
: [];
|
|
7002
6937
|
const targetRoot = useGlobal ? getGlobalRoot() : hippoRoot;
|
|
6938
|
+
if (flags['agents']) {
|
|
6939
|
+
const opts = { machine: currentMachine(), dryRun };
|
|
6940
|
+
// A folder without a store of its own imports as session end would there, so its notes are not hidden.
|
|
6941
|
+
const report = useGlobal || isInitialized(hippoRoot)
|
|
6942
|
+
? importForStore(useGlobal ? getGlobalRoot() : hippoRoot, opts)
|
|
6943
|
+
: importAtSessionEnd(process.cwd(), undefined, opts);
|
|
6944
|
+
for (const line of detailLines(report, dryRun))
|
|
6945
|
+
console.log(line);
|
|
6946
|
+
for (const warning of report.warnings)
|
|
6947
|
+
console.error(`hippo: agent memories: ${warning}`);
|
|
6948
|
+
return;
|
|
6949
|
+
}
|
|
7003
6950
|
if (useGlobal) {
|
|
7004
6951
|
initGlobal();
|
|
7005
6952
|
}
|
|
@@ -7105,7 +7052,7 @@ function cmdImport(hippoRoot, args, flags) {
|
|
|
7105
7052
|
importerName = 'File';
|
|
7106
7053
|
}
|
|
7107
7054
|
if (!filePath || !importer) {
|
|
7108
|
-
console.error('Usage: hippo import <--chatgpt|--claude|--cursor|--file|--markdown|--vault> <path
|
|
7055
|
+
console.error('Usage: hippo import <--chatgpt|--claude|--cursor|--file|--markdown|--vault> <path>, or hippo import --agents [--dry-run]');
|
|
7109
7056
|
process.exit(1);
|
|
7110
7057
|
}
|
|
7111
7058
|
if (!fs.existsSync(filePath)) {
|
|
@@ -7718,11 +7665,22 @@ function cmdSetup(flags) {
|
|
|
7718
7665
|
setupDailySchedule(globalRoot);
|
|
7719
7666
|
}
|
|
7720
7667
|
}
|
|
7668
|
+
if (!flags['no-learn']) {
|
|
7669
|
+
console.log('');
|
|
7670
|
+
printAgentImport(importUserMemories(globalRoot, { machine: currentMachine(), dryRun }), dryRun ? '[dry-run] ' : '');
|
|
7671
|
+
}
|
|
7721
7672
|
console.log('');
|
|
7722
7673
|
console.log('Done. Restart your AI tool to activate the hooks.');
|
|
7723
7674
|
}
|
|
7724
7675
|
function cmdDailyRunner() {
|
|
7725
7676
|
const globalRoot = getGlobalRoot();
|
|
7677
|
+
// No workspace sleep ever opens the global store, yet hooks in folders without a store compact into it.
|
|
7678
|
+
if (isInitialized(globalRoot)) {
|
|
7679
|
+
const finished = replayCompactionsAt(globalRoot, (message) => console.error(`compaction replay: ${message}`));
|
|
7680
|
+
if (finished > 0)
|
|
7681
|
+
console.log(`Finished saving ${finished} compaction${finished === 1 ? '' : 's'} left over in the global store.`);
|
|
7682
|
+
}
|
|
7683
|
+
printAgentImport(importUserMemories(globalRoot, { machine: currentMachine() }), '');
|
|
7726
7684
|
const workspaces = listRegisteredWorkspaces(globalRoot);
|
|
7727
7685
|
if (workspaces.length === 0) {
|
|
7728
7686
|
console.log('No registered Hippo workspaces found. Run `hippo init` inside a project first.');
|
|
@@ -7749,23 +7707,6 @@ function cmdDailyRunner() {
|
|
|
7749
7707
|
});
|
|
7750
7708
|
console.log(`Daily maintenance complete: ${processed} workspace${processed === 1 ? '' : 's'} processed, ${failed} command failure${failed === 1 ? '' : 's'}.`);
|
|
7751
7709
|
}
|
|
7752
|
-
// JSON-hook install/uninstall lives in ./hooks.ts so tests can import it
|
|
7753
|
-
// without running the CLI main(). Backwards-compatible wrappers below keep
|
|
7754
|
-
// older call sites working.
|
|
7755
|
-
function installClaudeCodeSessionEndHook() {
|
|
7756
|
-
const result = installJsonHooks('claude-code');
|
|
7757
|
-
return {
|
|
7758
|
-
installed: result.installedSessionEnd ||
|
|
7759
|
-
result.installedSessionStart ||
|
|
7760
|
-
result.installedUserPromptSubmit ||
|
|
7761
|
-
result.installedPreCompact ||
|
|
7762
|
-
result.installedCompactResume,
|
|
7763
|
-
migratedFromStop: result.migratedFromStop,
|
|
7764
|
-
};
|
|
7765
|
-
}
|
|
7766
|
-
function uninstallClaudeCodeSessionEndHook() {
|
|
7767
|
-
return uninstallJsonHooks('claude-code');
|
|
7768
|
-
}
|
|
7769
7710
|
// ---------------------------------------------------------------------------
|
|
7770
7711
|
// Working Memory
|
|
7771
7712
|
// ---------------------------------------------------------------------------
|
|
@@ -8704,7 +8645,8 @@ Commands:
|
|
|
8704
8645
|
--no-hooks Skip auto-detecting and installing agent hooks
|
|
8705
8646
|
(HIPPO_SKIP_AUTO_INTEGRATIONS=1 does the same)
|
|
8706
8647
|
--no-schedule Skip auto-creating the machine-level daily runner
|
|
8707
|
-
--no-learn Skip seeding memories from git history
|
|
8648
|
+
--no-learn Skip seeding memories from git history and importing
|
|
8649
|
+
coding agents' own memories (every init imports those)
|
|
8708
8650
|
remember <text> Store a memory
|
|
8709
8651
|
--tag <tag> Add a tag (repeatable)
|
|
8710
8652
|
--error Tag as error (boosts retention)
|
|
@@ -8841,7 +8783,7 @@ Commands:
|
|
|
8841
8783
|
With ANTHROPIC_API_KEY set it sends memory text to Anthropic for
|
|
8842
8784
|
fact extraction; {"extraction":{"enabled":false}} turns that off
|
|
8843
8785
|
--dry-run Preview without writing
|
|
8844
|
-
--no-learn Skip auto git-learn before consolidation
|
|
8786
|
+
--no-learn Skip auto git-learn and the agent memory import before consolidation
|
|
8845
8787
|
--no-share Skip auto-sharing to global store
|
|
8846
8788
|
daily-runner Sweep registered workspaces and run daily learn+sleep
|
|
8847
8789
|
dedup Remove duplicate memories (keeps stronger copy)
|
|
@@ -9039,6 +8981,11 @@ Commands:
|
|
|
9039
8981
|
--vault <path> Import a markdown-vault FOLDER as kind='raw' notes
|
|
9040
8982
|
(Obsidian/Foam/Dendron). Requires --name <vault>.
|
|
9041
8983
|
[--scope <scope>]
|
|
8984
|
+
--agents Sync every coding agent's own memories (Claude Code, Codex,
|
|
8985
|
+
Gemini CLI, Copilot, OpenClaw, Qwen Code) now; with
|
|
8986
|
+
--dry-run, print each tool's folders and what would change.
|
|
8987
|
+
HIPPO_AGENT_MEMORY_TOOLS=<ids> or config agentMemories.tools
|
|
8988
|
+
picks the tools; "none" or [] turns the import off
|
|
9042
8989
|
--dry-run Preview without writing
|
|
9043
8990
|
--global Write to global store ($HIPPO_HOME or ~/.hippo/)
|
|
9044
8991
|
--tag <tag> Add extra tag (repeatable)
|
|
@@ -9056,10 +9003,12 @@ Commands:
|
|
|
9056
9003
|
setup One-shot: detect installed AI tools and install their hooks:
|
|
9057
9004
|
claude-code gets 7 hooks in ~/.claude/settings.json, opencode
|
|
9058
9005
|
a plugin, codex 2 hooks in its hooks.json plus a launcher
|
|
9059
|
-
wrapper; other tools get a hint
|
|
9006
|
+
wrapper; other tools get a hint. Then imports each agent's
|
|
9007
|
+
user-level memories into the global store
|
|
9060
9008
|
--all Install for every JSON-hook tool, even if not detected
|
|
9061
9009
|
--dry-run Show what would be installed without writing
|
|
9062
9010
|
--no-schedule Skip installing or repairing the daily runner
|
|
9011
|
+
--no-learn Skip the agent memory import
|
|
9063
9012
|
last-sleep Print the last 'hippo sleep --log-file' output to stderr and clear it
|
|
9064
9013
|
--path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
|
|
9065
9014
|
--keep Print without clearing
|
|
@@ -9067,10 +9016,12 @@ Commands:
|
|
|
9067
9016
|
capture from the session's last 20 user and 10 assistant messages,
|
|
9068
9017
|
in a detached worker
|
|
9069
9018
|
--log-file <path> Tee the worker's output to a log file (paired with 'hippo last-sleep')
|
|
9070
|
-
pre-compact PreCompact hook: save a working-state snapshot
|
|
9019
|
+
pre-compact PreCompact hook: record the compaction, save a working-state snapshot, and
|
|
9020
|
+
ask the summariser to end with a "Memories for hippo" list
|
|
9071
9021
|
--log-file <p> Diagnostic log path (default: ~/.hippo/logs/pre-compact.log)
|
|
9072
9022
|
compact-resume SessionStart(compact) hook: re-print the snapshot, if under 15 minutes old
|
|
9073
|
-
post-compact PostCompact hook:
|
|
9023
|
+
post-compact PostCompact hook: keep that list as memories (a busy store leaves the save to
|
|
9024
|
+
the next hippo sleep) and print one line saying how many
|
|
9074
9025
|
--log-file <p> Same log path as pre-compact (default: ~/.hippo/logs/pre-compact.log)
|
|
9075
9026
|
codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper
|
|
9076
9027
|
hook <sub> [target] Manage framework integrations
|
|
@@ -9480,7 +9431,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9480
9431
|
console.error('Please provide a search query.');
|
|
9481
9432
|
process.exit(1);
|
|
9482
9433
|
}
|
|
9483
|
-
await cmdRecall(hippoRoot, query, flags);
|
|
9434
|
+
await cmdRecall(hookStoreRoot(hippoRoot), query, flags);
|
|
9484
9435
|
break;
|
|
9485
9436
|
}
|
|
9486
9437
|
case 'drill': {
|
|
@@ -9565,13 +9516,25 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9565
9516
|
break;
|
|
9566
9517
|
}
|
|
9567
9518
|
case 'post-compact': {
|
|
9568
|
-
// PostCompact hook:
|
|
9569
|
-
// because Claude Code shows this hook's stdout as-is. Always exits 0.
|
|
9519
|
+
// PostCompact hook: saves the compaction summary and its memories, then prints one plain line, because Claude Code shows this hook's stdout as-is. Always exits 0.
|
|
9570
9520
|
const { text } = await readStdinBounded();
|
|
9571
9521
|
const logFlag = flags['log-file'];
|
|
9572
|
-
const
|
|
9573
|
-
|
|
9574
|
-
|
|
9522
|
+
const store = hookStoreRoot(hippoRoot);
|
|
9523
|
+
const line = cmdPostCompact(store, {
|
|
9524
|
+
stdinText: text,
|
|
9525
|
+
logFile: logFlag === true || logFlag === false || Array.isArray(logFlag) ? undefined : logFlag,
|
|
9526
|
+
// Passed in, since capture.ts importing the sync would close an import cycle.
|
|
9527
|
+
afterSave: (transcriptPath, originProject, log) => {
|
|
9528
|
+
const report = importAtCompaction(store, transcriptPath, originProject, { machine: currentMachine(), busyWaitMs: COMPACTION_DB_WAIT_MS });
|
|
9529
|
+
const summary = summaryLine(report);
|
|
9530
|
+
if (summary !== null)
|
|
9531
|
+
log(summary);
|
|
9532
|
+
for (const warning of report.warnings)
|
|
9533
|
+
log(`agent memories: ${warning}`);
|
|
9534
|
+
},
|
|
9535
|
+
});
|
|
9536
|
+
if (line !== null)
|
|
9537
|
+
console.log(line);
|
|
9575
9538
|
break;
|
|
9576
9539
|
}
|
|
9577
9540
|
case 'capture-error': {
|
|
@@ -9631,7 +9594,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9631
9594
|
}
|
|
9632
9595
|
requireInit(hippoRoot);
|
|
9633
9596
|
const entries = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
9634
|
-
const result = auditMemories(entries);
|
|
9597
|
+
const result = auditMemories(entries, memoriesBackingObjects(hippoRoot));
|
|
9635
9598
|
const shouldFix = Boolean(flags['fix']);
|
|
9636
9599
|
if (result.issues.length === 0) {
|
|
9637
9600
|
console.log(`All ${result.total} memories passed quality checks.`);
|
|
@@ -9869,7 +9832,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9869
9832
|
// Bounded, not a TTY guard (DF1 T2, docs/plans/2026-08-23-df1-snapshot-lifecycle.md):
|
|
9870
9833
|
// the hot stdin path and a manual run share this one command.
|
|
9871
9834
|
const { text: stdinText } = await readStdinBounded();
|
|
9872
|
-
await cmdContext(hippoRoot, args, flags, stdinText);
|
|
9835
|
+
await cmdContext(hookStoreRoot(hippoRoot), args, flags, stdinText);
|
|
9873
9836
|
break;
|
|
9874
9837
|
}
|
|
9875
9838
|
case 'hook':
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const COMPACTION_ITEM_MAX_CHARS = 500;
|
|
2
|
+
export declare const COMPACTION_ITEM_ROW_CAP = 10;
|
|
3
|
+
export interface ParsedCompactionItems {
|
|
4
|
+
found: boolean;
|
|
5
|
+
items: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface CompactionItemRows {
|
|
8
|
+
rows: string[];
|
|
9
|
+
tooLong: number;
|
|
10
|
+
capped: number;
|
|
11
|
+
}
|
|
12
|
+
/** The <summary> body of a PostCompact compact_summary with any <analysis> block removed; the whole text trimmed when there is no <summary> tag. */
|
|
13
|
+
export declare function compactSummaryBody(compactSummary: string): string;
|
|
14
|
+
/** found=false when no "Memories for hippo" heading; items in order, "none" dropped, never cut. */
|
|
15
|
+
export declare function parseCompactionItems(summaryBody: string): ParsedCompactionItems;
|
|
16
|
+
/** First COMPACTION_ITEM_ROW_CAP items of at most COMPACTION_ITEM_MAX_CHARS chars become rows. */
|
|
17
|
+
export declare function selectItemRows(items: readonly string[]): CompactionItemRows;
|
|
18
|
+
//# sourceMappingURL=compaction-items.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Reads the "Memories for hippo" list out of a compaction summary. Pure text in, text out: hooks and replay share it.
|
|
2
|
+
export const COMPACTION_ITEM_MAX_CHARS = 500;
|
|
3
|
+
export const COMPACTION_ITEM_ROW_CAP = 10;
|
|
4
|
+
const HEADING = 'memories for hippo';
|
|
5
|
+
const ITEM_LINE = /^\s*(?:[-*]|\d+[.)])(?:\s+(.*))?$/;
|
|
6
|
+
/** The <summary> body of a PostCompact compact_summary with any <analysis> block removed; the whole text trimmed when there is no <summary> tag. */
|
|
7
|
+
export function compactSummaryBody(compactSummary) {
|
|
8
|
+
const text = compactSummary.replace(/<analysis>[\s\S]*?<\/analysis>/g, '');
|
|
9
|
+
const open = text.lastIndexOf('<summary>');
|
|
10
|
+
if (open < 0)
|
|
11
|
+
return text.trim();
|
|
12
|
+
const from = open + '<summary>'.length;
|
|
13
|
+
const close = text.indexOf('</summary>', from);
|
|
14
|
+
return text.slice(from, close < 0 ? undefined : close).trim();
|
|
15
|
+
}
|
|
16
|
+
function isHeading(line) {
|
|
17
|
+
const bare = line.replace(/^(?:[#*\s]|\d+\.)+/, '').replace(/[*:\s]+$/, '');
|
|
18
|
+
return bare.toLowerCase() === HEADING;
|
|
19
|
+
}
|
|
20
|
+
/** found=false when no "Memories for hippo" heading; items in order, "none" dropped, never cut. */
|
|
21
|
+
export function parseCompactionItems(summaryBody) {
|
|
22
|
+
const lines = summaryBody.split(/\r?\n/);
|
|
23
|
+
let heading = -1;
|
|
24
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
25
|
+
if (isHeading(lines[i])) {
|
|
26
|
+
heading = i;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (heading < 0)
|
|
31
|
+
return { found: false, items: [] };
|
|
32
|
+
const after = lines.slice(heading + 1).join('\n');
|
|
33
|
+
const end = after.indexOf('</summary>');
|
|
34
|
+
const items = [];
|
|
35
|
+
let current = -1;
|
|
36
|
+
for (const line of (end < 0 ? after : after.slice(0, end)).split('\n')) {
|
|
37
|
+
if (line.trim() === '')
|
|
38
|
+
continue;
|
|
39
|
+
const marker = ITEM_LINE.exec(line);
|
|
40
|
+
if (marker) {
|
|
41
|
+
const text = (marker[1] ?? '').trim();
|
|
42
|
+
current = text === '' ? -1 : items.push(text) - 1;
|
|
43
|
+
}
|
|
44
|
+
else if (/^\s/.test(line)) {
|
|
45
|
+
if (current >= 0)
|
|
46
|
+
items[current] += ` ${line.trim()}`;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { found: true, items: items.filter((item) => !/^none\.?$/i.test(item)) };
|
|
53
|
+
}
|
|
54
|
+
/** First COMPACTION_ITEM_ROW_CAP items of at most COMPACTION_ITEM_MAX_CHARS chars become rows. */
|
|
55
|
+
export function selectItemRows(items) {
|
|
56
|
+
const fitting = items.filter((item) => item.length <= COMPACTION_ITEM_MAX_CHARS);
|
|
57
|
+
const rows = fitting.slice(0, COMPACTION_ITEM_ROW_CAP);
|
|
58
|
+
return { rows, tooLong: items.length - fitting.length, capped: fitting.length - rows.length };
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=compaction-items.js.map
|