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.
Files changed (76) hide show
  1. package/README.md +47 -13
  2. package/dist/agent-memories/apply.d.ts +47 -0
  3. package/dist/agent-memories/apply.js +253 -0
  4. package/dist/agent-memories/claude-code.d.ts +11 -0
  5. package/dist/agent-memories/claude-code.js +113 -0
  6. package/dist/agent-memories/codex.d.ts +3 -0
  7. package/dist/agent-memories/codex.js +47 -0
  8. package/dist/agent-memories/copilot.d.ts +3 -0
  9. package/dist/agent-memories/copilot.js +125 -0
  10. package/dist/agent-memories/files.d.ts +37 -0
  11. package/dist/agent-memories/files.js +77 -0
  12. package/dist/agent-memories/folder-store.d.ts +17 -0
  13. package/dist/agent-memories/folder-store.js +44 -0
  14. package/dist/agent-memories/gemini.d.ts +3 -0
  15. package/dist/agent-memories/gemini.js +103 -0
  16. package/dist/agent-memories/git.d.ts +8 -0
  17. package/dist/agent-memories/git.js +11 -0
  18. package/dist/agent-memories/keys.d.ts +9 -0
  19. package/dist/agent-memories/keys.js +20 -0
  20. package/dist/agent-memories/legacy.d.ts +17 -0
  21. package/dist/agent-memories/legacy.js +45 -0
  22. package/dist/agent-memories/markdown.d.ts +13 -0
  23. package/dist/agent-memories/markdown.js +123 -0
  24. package/dist/agent-memories/openclaw.d.ts +3 -0
  25. package/dist/agent-memories/openclaw.js +42 -0
  26. package/dist/agent-memories/plan.d.ts +78 -0
  27. package/dist/agent-memories/plan.js +123 -0
  28. package/dist/agent-memories/qwen-code.d.ts +5 -0
  29. package/dist/agent-memories/qwen-code.js +50 -0
  30. package/dist/agent-memories/report.d.ts +52 -0
  31. package/dist/agent-memories/report.js +88 -0
  32. package/dist/agent-memories/source.d.ts +16 -0
  33. package/dist/agent-memories/source.js +32 -0
  34. package/dist/agent-memories/sync.d.ts +33 -0
  35. package/dist/agent-memories/sync.js +336 -0
  36. package/dist/agent-memories/tools.d.ts +33 -0
  37. package/dist/agent-memories/tools.js +19 -0
  38. package/dist/agent-memories/types.d.ts +42 -0
  39. package/dist/agent-memories/types.js +2 -0
  40. package/dist/api.d.ts +2 -2
  41. package/dist/api.js +28 -29
  42. package/dist/audit.d.ts +4 -3
  43. package/dist/audit.js +10 -6
  44. package/dist/capture.d.ts +11 -22
  45. package/dist/capture.js +76 -81
  46. package/dist/cli.d.ts +0 -2
  47. package/dist/cli.js +133 -170
  48. package/dist/compaction-items.d.ts +18 -0
  49. package/dist/compaction-items.js +60 -0
  50. package/dist/compaction-record.d.ts +94 -0
  51. package/dist/compaction-record.js +573 -0
  52. package/dist/config.d.ts +4 -0
  53. package/dist/config.js +13 -0
  54. package/dist/consolidate.d.ts +0 -2
  55. package/dist/consolidate.js +3 -36
  56. package/dist/db.d.ts +5 -1
  57. package/dist/db.js +40 -8
  58. package/dist/dedupe.js +3 -2
  59. package/dist/doctor.js +34 -2
  60. package/dist/dormant.d.ts +5 -3
  61. package/dist/dormant.js +9 -0
  62. package/dist/gated-write.d.ts +9 -0
  63. package/dist/gated-write.js +24 -0
  64. package/dist/hooks.d.ts +4 -2
  65. package/dist/hooks.js +8 -7
  66. package/dist/memory.d.ts +19 -2
  67. package/dist/memory.js +32 -3
  68. package/dist/shared.js +10 -7
  69. package/dist/store.d.ts +14 -2
  70. package/dist/store.js +75 -19
  71. package/dist/version.d.ts +1 -1
  72. package/dist/version.js +1 -1
  73. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  74. package/extensions/openclaw-plugin/package.json +1 -1
  75. package/openclaw.plugin.json +1 -1
  76. 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, spawnSync } from 'child_process';
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 { detectSecret } from './secret-detect.js';
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, realpathOrResolve, resolveProjectIdentity } from './project-identity.js';
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, postCompactMessage, resolveLastSessionTranscript, truncateCodePointSafe, sanitizeLogMessage, transcriptWorkingState } from './capture.js';
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
- initGlobal();
582
- console.log('Initialized global Hippo store at', globalRoot);
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 = (typeof flags['layer'] === 'string' ? flags['layer'] : old.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
- : [...old.tags];
1019
+ : undefined;
1000
1020
  const pinned = flags['pin'] === true || old.pinned;
1001
- const newEntry = createMemory(newContent, {
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 = isInitialized(globalRoot) ? loadRecallSearchEntries(globalRoot, query, undefined, tenantId, requestedScopeForFilter, 'additive', loadSuperseded) : [];
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 + Claude Code auto memory (CLI-only, uses process.cwd() / os.homedir()).
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 this project's Claude Code auto memory.");
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
- const memImported = learnFromMemoryMd(hippoRoot);
2896
- if (memImported > 0)
2897
- console.log(`Imported ${memImported} memories from this project's Claude Code auto memory.`);
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, timedOut: stdinTimedOut } = await readStdinBounded();
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
- const transcriptPath = resolveLastSessionTranscript(undefined, stdinText, stdinTimedOut);
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 before compaction
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: tell the user what pre-compact saved
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: tells the user what pre-compact saved. Plain text,
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 message = postCompactMessage(text, logFlag === true || logFlag === false || Array.isArray(logFlag) ? undefined : logFlag);
9573
- if (message !== null)
9574
- console.log(message);
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