hippo-memory 1.52.6 → 1.52.7
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 +56 -33
- package/dist/api.js +2 -1
- package/dist/autolearn.d.ts +1 -4
- package/dist/autolearn.js +3 -5
- package/dist/capture.js +3 -0
- package/dist/cli.js +167 -97
- package/dist/config.js +8 -1
- package/dist/consolidate.d.ts +2 -0
- package/dist/consolidate.js +13 -6
- package/dist/customer-notes.js +3 -2
- package/dist/dag.js +5 -0
- package/dist/decisions.d.ts +1 -1
- package/dist/decisions.js +4 -3
- package/dist/extract.js +3 -0
- package/dist/half-life-migration.d.ts +14 -5
- package/dist/half-life-migration.js +89 -21
- package/dist/hooks.js +1 -1
- package/dist/importers.d.ts +2 -1
- package/dist/importers.js +19 -7
- package/dist/incidents.d.ts +1 -1
- package/dist/incidents.js +4 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +6 -1
- package/dist/memory.d.ts +6 -13
- package/dist/memory.js +0 -10
- package/dist/policies.js +3 -2
- package/dist/predictions.js +2 -0
- package/dist/processes.js +3 -2
- package/dist/project-briefs.js +3 -2
- package/dist/skills.js +3 -2
- package/dist/store.d.ts +2 -0
- package/dist/store.js +3 -0
- 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
|
@@ -484,6 +484,14 @@ function cmdInitScan(scanDir, flags) {
|
|
|
484
484
|
(totalLowInfo > 0 ? `, ${totalLowInfo} low-information subject(s) dropped` : '') +
|
|
485
485
|
'.');
|
|
486
486
|
console.log(`Global store: ${globalRoot}`);
|
|
487
|
+
if (!flags['no-hooks']) {
|
|
488
|
+
// User-level hooks only: a hippo block in each repo's CLAUDE.md or AGENTS.md would leave a diff in every repo.
|
|
489
|
+
const agents = detectAgentHooks(repos);
|
|
490
|
+
if (agents.length === 0) {
|
|
491
|
+
console.log(' No agent config found in these repositories. Run `hippo setup` to add hooks for the agents on this machine.');
|
|
492
|
+
}
|
|
493
|
+
installUserLevelHooks(agents, true);
|
|
494
|
+
}
|
|
487
495
|
if (!flags['no-schedule']) {
|
|
488
496
|
setupDailySchedule(globalRoot);
|
|
489
497
|
}
|
|
@@ -520,7 +528,7 @@ function cmdInit(hippoRoot, flags) {
|
|
|
520
528
|
registerWorkspace(globalRoot, path.dirname(hippoRoot));
|
|
521
529
|
// Auto-detect and install hooks (unless --no-hooks)
|
|
522
530
|
if (!flags['no-hooks']) {
|
|
523
|
-
autoInstallHooks(
|
|
531
|
+
autoInstallHooks();
|
|
524
532
|
}
|
|
525
533
|
// Auto-setup daily schedule (unless --no-schedule)
|
|
526
534
|
if (!flags['no-schedule'] && !flags['global']) {
|
|
@@ -546,57 +554,86 @@ function cmdInit(hippoRoot, flags) {
|
|
|
546
554
|
}
|
|
547
555
|
}
|
|
548
556
|
}
|
|
549
|
-
/**
|
|
550
|
-
|
|
551
|
-
* Skips files that already have a <!-- hippo:start --> block.
|
|
552
|
-
*/
|
|
553
|
-
function autoInstallHooks(quiet) {
|
|
557
|
+
/** Plain init: patch the detected agents' instruction files in cwd, then install their user-level hooks. */
|
|
558
|
+
function autoInstallHooks() {
|
|
554
559
|
const cwd = process.cwd();
|
|
560
|
+
const agents = detectAgentHooks([cwd]);
|
|
561
|
+
const agentsMd = path.join(cwd, HOOKS.codex.file);
|
|
562
|
+
// Read before patching: a re-run of init finds its own block and skips the Codex hint.
|
|
563
|
+
const codexHint = !(fs.existsSync(agentsMd) && fs.readFileSync(agentsMd, 'utf8').includes(HOOK_MARKERS.start));
|
|
564
|
+
patchInstructionFiles(cwd, agents);
|
|
565
|
+
installUserLevelHooks(agents, codexHint);
|
|
566
|
+
}
|
|
567
|
+
/** HOOKS keys of the agents with a marker file in any of dirs, in detector order. */
|
|
568
|
+
function detectAgentHooks(dirs) {
|
|
555
569
|
// Map: filename to check -> hook key(s) to install
|
|
556
570
|
const detectors = [
|
|
557
571
|
{ files: ['CLAUDE.md', '.claude/settings.json'], hook: 'claude-code' },
|
|
558
572
|
{ files: ['AGENTS.md', '.codex'], hook: 'codex' },
|
|
559
|
-
|
|
573
|
+
// No Cursor row: Cursor reads the root AGENTS.md, which the rows either side patch.
|
|
560
574
|
{ files: ['.openclaw', 'AGENTS.md'], hook: 'openclaw' },
|
|
561
575
|
{ files: ['.opencode', 'opencode.json'], hook: 'opencode' },
|
|
562
576
|
{ files: ['.pi', '.pi/agent'], hook: 'pi' },
|
|
563
577
|
];
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
578
|
+
return detectors
|
|
579
|
+
.filter(({ files }) => dirs.some((dir) => files.some((f) => fs.existsSync(path.join(dir, f)))))
|
|
580
|
+
.map(({ hook }) => hook);
|
|
581
|
+
}
|
|
582
|
+
function patchInstructionFiles(dir, agents) {
|
|
583
|
+
// One block per file: several agents share AGENTS.md.
|
|
584
|
+
const seen = new Set();
|
|
585
|
+
for (const hook of agents) {
|
|
567
586
|
const hookDef = HOOKS[hook];
|
|
568
587
|
if (!hookDef)
|
|
569
588
|
continue;
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (!
|
|
589
|
+
const targetPath = path.resolve(dir, hookDef.file);
|
|
590
|
+
// Never create the file: a marker such as .codex or .claude/settings.json does not ask for a new AGENTS.md or CLAUDE.md.
|
|
591
|
+
if (!fs.existsSync(targetPath) || seen.has(targetPath))
|
|
573
592
|
continue;
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (
|
|
593
|
+
seen.add(targetPath);
|
|
594
|
+
const existing = fs.readFileSync(targetPath, 'utf8');
|
|
595
|
+
if (existing.includes(HOOK_MARKERS.start)) {
|
|
596
|
+
refreshShippedBlock(targetPath, existing, hook);
|
|
577
597
|
continue;
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
598
|
+
}
|
|
599
|
+
const block = `${HOOK_MARKERS.start}\n${hookDef.content}\n${HOOK_MARKERS.end}`;
|
|
600
|
+
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
601
|
+
fs.writeFileSync(targetPath, existing + sep + block + '\n', 'utf8');
|
|
602
|
+
console.log(` Auto-installed ${hook} hook in ${hookDef.file}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/** The first hippo block in `text` and the agent whose current or shipped text it is; `owner` is undefined for an edited block. */
|
|
606
|
+
function hippoBlock(text) {
|
|
607
|
+
const at = text.indexOf(HOOK_MARKERS.start);
|
|
608
|
+
const start = at + HOOK_MARKERS.start.length;
|
|
609
|
+
const end = text.indexOf(HOOK_MARKERS.end, start);
|
|
610
|
+
if (at < 0 || end < 0)
|
|
611
|
+
return null;
|
|
612
|
+
// git autocrlf checks these files out with CRLF: match as LF, write back in the file's own ending.
|
|
613
|
+
const raw = text.slice(start, end);
|
|
614
|
+
const inner = raw.replace(/\r\n/g, '\n').trim();
|
|
615
|
+
const owner = Object.keys(HOOKS).find((k) => HOOKS[k].content === inner) ?? SHIPPED_HOOK_HASHES.get(createHash('sha256').update(inner).digest('hex'));
|
|
616
|
+
return { start, end, eol: raw.includes('\r\n') ? '\r\n' : '\n', inner, owner };
|
|
617
|
+
}
|
|
618
|
+
/** Swap an unedited block from an earlier hippo for the current one; an edited block stays, with a hint. */
|
|
619
|
+
function refreshShippedBlock(filePath, text, hook) {
|
|
620
|
+
const block = hippoBlock(text);
|
|
621
|
+
if (!block || (block.owner && HOOKS[block.owner].content === block.inner))
|
|
622
|
+
return;
|
|
623
|
+
const { start, end, eol, owner } = block;
|
|
624
|
+
const name = path.basename(filePath);
|
|
625
|
+
if (!owner) {
|
|
626
|
+
console.log(` Left the edited hippo block in ${name} as is; \`hippo hook install ${hook}\` replaces it.`);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
fs.writeFileSync(filePath, `${text.slice(0, start)}${eol}${HOOKS[owner].content.replace(/\n/g, eol)}${eol}${text.slice(end)}`, 'utf8');
|
|
630
|
+
console.log(` Refreshed the ${owner} hippo block in ${name}`);
|
|
631
|
+
}
|
|
632
|
+
/** Claude Code settings hooks and the OpenCode plugin, under the home directory; idempotent, so re-running init adds newer hooks. */
|
|
633
|
+
function installUserLevelHooks(agents, codexHint) {
|
|
634
|
+
for (const hook of agents) {
|
|
635
|
+
// The Codex capture wrapper swaps the codex launcher binary, so init only points at the opt-in (issue #133).
|
|
636
|
+
if (hook === 'codex' && codexHint && !isCodexWrapperInstalled()) {
|
|
600
637
|
console.log(' Codex detected. To capture Codex sessions: hippo hook install codex');
|
|
601
638
|
}
|
|
602
639
|
// For Claude Code, also install SessionEnd+SessionStart entries in its
|
|
@@ -877,6 +914,7 @@ function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
|
877
914
|
confidence: 'verified',
|
|
878
915
|
tenantId: old.tenantId,
|
|
879
916
|
scope: old.scope,
|
|
917
|
+
baseHalfLifeDays: loadConfig(hippoRoot).defaultHalfLifeDays,
|
|
880
918
|
});
|
|
881
919
|
// AT1: write the SUCCESSOR first. The rejection guard fires on the new
|
|
882
920
|
// content — if it refuses, nothing has been mutated yet (the old ordering
|
|
@@ -2463,6 +2501,7 @@ function cmdTraceRecord(hippoRoot, flags) {
|
|
|
2463
2501
|
trace_outcome: outcome,
|
|
2464
2502
|
source_session_id: sessionId,
|
|
2465
2503
|
tenantId: resolveTenantId({}),
|
|
2504
|
+
baseHalfLifeDays: loadConfig(hippoRoot).defaultHalfLifeDays,
|
|
2466
2505
|
});
|
|
2467
2506
|
writeEntry(hippoRoot, entry);
|
|
2468
2507
|
console.log(`Recorded trace ${entry.id} (outcome=${outcome}, ${steps.length} steps)`);
|
|
@@ -2485,7 +2524,7 @@ function cmdTrace(hippoRoot, id, flags) {
|
|
|
2485
2524
|
}
|
|
2486
2525
|
const now = evalNow();
|
|
2487
2526
|
const strength = calculateStrength(entry, now);
|
|
2488
|
-
const halfLife =
|
|
2527
|
+
const halfLife = entry.half_life_days;
|
|
2489
2528
|
const rewardFactor = calculateRewardFactor(entry);
|
|
2490
2529
|
const effHalfLife = halfLife * rewardFactor;
|
|
2491
2530
|
const createdMs = new Date(entry.created).getTime();
|
|
@@ -2548,7 +2587,7 @@ function cmdTrace(hippoRoot, id, flags) {
|
|
|
2548
2587
|
console.log(` now: ${fmt(strength, 3)}`);
|
|
2549
2588
|
console.log(` in 30 days: ${fmt(projectedAt(30), 3)}`);
|
|
2550
2589
|
console.log(` in 90 days: ${fmt(projectedAt(90), 3)}`);
|
|
2551
|
-
console.log(` half-life: ${fmt(halfLife, 1)}d (
|
|
2590
|
+
console.log(` half-life: ${fmt(halfLife, 1)}d (stored) x ${fmt(rewardFactor, 2)} reward = ${fmt(effHalfLife, 1)}d effective`);
|
|
2552
2591
|
console.log();
|
|
2553
2592
|
console.log(`Retrieval:`);
|
|
2554
2593
|
console.log(` count: ${entry.retrieval_count}`);
|
|
@@ -2628,6 +2667,7 @@ export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
|
2628
2667
|
if (memoryDirs.length === 0)
|
|
2629
2668
|
return 0;
|
|
2630
2669
|
const existing = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
2670
|
+
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
2631
2671
|
let imported = 0;
|
|
2632
2672
|
let skippedSecret = 0;
|
|
2633
2673
|
// AT1 (plan §3 containment): a rejection guard refusal is per-VALUE — one
|
|
@@ -2671,6 +2711,7 @@ export function learnFromMemoryMd(hippoRoot, homeDir = os.homedir()) {
|
|
|
2671
2711
|
source: `claude-memory:${file}`,
|
|
2672
2712
|
confidence: 'observed',
|
|
2673
2713
|
tenantId: resolveTenantId({}),
|
|
2714
|
+
baseHalfLifeDays,
|
|
2674
2715
|
});
|
|
2675
2716
|
try {
|
|
2676
2717
|
writeEntry(hippoRoot, entry);
|
|
@@ -6839,7 +6880,8 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6839
6880
|
}
|
|
6840
6881
|
// Same patterns as MCP hippo_learn: config.gitLearnPatterns (whose default
|
|
6841
6882
|
// equals extractLessons' built-in list) so a custom list applies everywhere.
|
|
6842
|
-
const
|
|
6883
|
+
const config = loadConfig(hippoRoot);
|
|
6884
|
+
const parsedLessons = extractLessons(gitLog, config.gitLearnPatterns);
|
|
6843
6885
|
if (parsedLessons.length === 0) {
|
|
6844
6886
|
console.log(`${prefix}No fix/revert/bug commits found in the specified period.`);
|
|
6845
6887
|
return { added: 0, skipped: 0, lowInfo: 0 };
|
|
@@ -6905,6 +6947,7 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6905
6947
|
confidence: 'observed',
|
|
6906
6948
|
schema_fit: schemaFitVal,
|
|
6907
6949
|
tenantId: resolveTenantId({}),
|
|
6950
|
+
baseHalfLifeDays: config.defaultHalfLifeDays,
|
|
6908
6951
|
});
|
|
6909
6952
|
// Auto-tag with path context from the repo being learned
|
|
6910
6953
|
const learnPathTags = extractPathTags(repoPath);
|
|
@@ -7180,25 +7223,14 @@ run it if the hook is not installed:
|
|
|
7180
7223
|
hippo context --auto --budget 1500
|
|
7181
7224
|
\`\`\`
|
|
7182
7225
|
|
|
7183
|
-
When you
|
|
7184
|
-
|
|
7185
|
-
hippo remember "<lesson>"
|
|
7186
|
-
\`\`\`
|
|
7187
|
-
|
|
7188
|
-
When you hit an error or discover a gotcha:
|
|
7226
|
+
When you find out why something failed, record it right then, while you
|
|
7227
|
+
work, never as a closing step:
|
|
7189
7228
|
\`\`\`bash
|
|
7190
7229
|
hippo remember "<what went wrong and why>" --error
|
|
7191
7230
|
\`\`\`
|
|
7192
7231
|
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
hippo outcome --good
|
|
7196
|
-
\`\`\`
|
|
7197
|
-
|
|
7198
|
-
When the user ends the session, capture a brief summary:
|
|
7199
|
-
\`\`\`bash
|
|
7200
|
-
hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
|
|
7201
|
-
\`\`\`
|
|
7232
|
+
The installed hooks store failed tool calls and capture the session when it
|
|
7233
|
+
ends, so there is nothing to run before you finish.
|
|
7202
7234
|
`.trim(),
|
|
7203
7235
|
},
|
|
7204
7236
|
'codex': {
|
|
@@ -7213,34 +7245,41 @@ hippo context --auto --budget 1500
|
|
|
7213
7245
|
\`\`\`
|
|
7214
7246
|
Read the output before writing any code.
|
|
7215
7247
|
|
|
7216
|
-
On errors or unexpected behaviour
|
|
7248
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
7249
|
+
never as a closing step:
|
|
7217
7250
|
\`\`\`bash
|
|
7218
7251
|
hippo remember "<description of what went wrong>" --error
|
|
7219
7252
|
\`\`\`
|
|
7220
7253
|
|
|
7221
|
-
On task completion:
|
|
7222
|
-
\`\`\`bash
|
|
7223
|
-
hippo outcome --good
|
|
7224
|
-
\`\`\`
|
|
7225
|
-
|
|
7226
7254
|
When Hippo's Codex wrapper is installed, session-end capture runs automatically.
|
|
7227
7255
|
If the wrapper is not installed, capture a brief summary manually:
|
|
7228
7256
|
\`\`\`bash
|
|
7229
|
-
hippo capture --stdin <<< '<decisions, errors, lessons
|
|
7257
|
+
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
7230
7258
|
\`\`\`
|
|
7231
7259
|
`.trim(),
|
|
7232
7260
|
},
|
|
7233
7261
|
'cursor': {
|
|
7234
|
-
file: '.
|
|
7262
|
+
file: 'AGENTS.md',
|
|
7235
7263
|
description: 'Cursor',
|
|
7236
7264
|
content: `
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7265
|
+
## Project Memory (Hippo)
|
|
7266
|
+
|
|
7267
|
+
At the start of every task, run:
|
|
7268
|
+
\`\`\`bash
|
|
7269
|
+
hippo context --auto --budget 1500
|
|
7270
|
+
\`\`\`
|
|
7271
|
+
Read the output before writing any code.
|
|
7272
|
+
|
|
7273
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
7274
|
+
never as a closing step:
|
|
7275
|
+
\`\`\`bash
|
|
7276
|
+
hippo remember "<description of what went wrong>" --error
|
|
7277
|
+
\`\`\`
|
|
7278
|
+
|
|
7279
|
+
When ending a session, capture a brief summary:
|
|
7280
|
+
\`\`\`bash
|
|
7281
|
+
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
7282
|
+
\`\`\`
|
|
7244
7283
|
`.trim(),
|
|
7245
7284
|
},
|
|
7246
7285
|
'openclaw': {
|
|
@@ -7255,19 +7294,15 @@ hippo context --auto --budget 1500
|
|
|
7255
7294
|
\`\`\`
|
|
7256
7295
|
Read the output before writing any code.
|
|
7257
7296
|
|
|
7258
|
-
On errors or unexpected behaviour
|
|
7297
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
7298
|
+
never as a closing step:
|
|
7259
7299
|
\`\`\`bash
|
|
7260
7300
|
hippo remember "<description of what went wrong>" --error
|
|
7261
7301
|
\`\`\`
|
|
7262
7302
|
|
|
7263
|
-
On task completion:
|
|
7264
|
-
\`\`\`bash
|
|
7265
|
-
hippo outcome --good
|
|
7266
|
-
\`\`\`
|
|
7267
|
-
|
|
7268
7303
|
When ending a session, capture a brief summary:
|
|
7269
7304
|
\`\`\`bash
|
|
7270
|
-
hippo capture --stdin <<< '<decisions, errors, lessons
|
|
7305
|
+
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
7271
7306
|
\`\`\`
|
|
7272
7307
|
`.trim(),
|
|
7273
7308
|
},
|
|
@@ -7283,7 +7318,8 @@ hippo context --auto --budget 1500
|
|
|
7283
7318
|
\`\`\`
|
|
7284
7319
|
Read the output before writing any code.
|
|
7285
7320
|
|
|
7286
|
-
When you learn
|
|
7321
|
+
When you learn a non-obvious lesson or hit an error, record it right then,
|
|
7322
|
+
while you work, never as a closing step:
|
|
7287
7323
|
\`\`\`bash
|
|
7288
7324
|
hippo remember "<lesson>" --error
|
|
7289
7325
|
\`\`\`
|
|
@@ -7293,14 +7329,9 @@ When stuck or repeating yourself, check if this happened before:
|
|
|
7293
7329
|
hippo recall "<what's going wrong>" --budget 2000
|
|
7294
7330
|
\`\`\`
|
|
7295
7331
|
|
|
7296
|
-
On task completion:
|
|
7297
|
-
\`\`\`bash
|
|
7298
|
-
hippo outcome --good
|
|
7299
|
-
\`\`\`
|
|
7300
|
-
|
|
7301
7332
|
When ending a session, capture a brief summary:
|
|
7302
7333
|
\`\`\`bash
|
|
7303
|
-
hippo capture --stdin <<< '<decisions, errors, lessons
|
|
7334
|
+
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
7304
7335
|
\`\`\`
|
|
7305
7336
|
`.trim(),
|
|
7306
7337
|
},
|
|
@@ -7316,28 +7347,44 @@ hippo context --auto --budget 1500
|
|
|
7316
7347
|
\`\`\`
|
|
7317
7348
|
Read the output before writing any code.
|
|
7318
7349
|
|
|
7319
|
-
On errors or unexpected behaviour
|
|
7350
|
+
On errors or unexpected behaviour, record it right then, while you work,
|
|
7351
|
+
never as a closing step:
|
|
7320
7352
|
\`\`\`bash
|
|
7321
7353
|
hippo remember "<description of what went wrong>" --error
|
|
7322
7354
|
\`\`\`
|
|
7323
7355
|
|
|
7324
|
-
On task completion:
|
|
7325
|
-
\`\`\`bash
|
|
7326
|
-
hippo outcome --good
|
|
7327
|
-
\`\`\`
|
|
7328
|
-
|
|
7329
7356
|
When ending a session, capture a brief summary:
|
|
7330
7357
|
\`\`\`bash
|
|
7331
|
-
hippo capture --stdin <<< '<decisions, errors, lessons
|
|
7358
|
+
hippo capture --stdin <<< '<decisions, errors, lessons: 2-5 bullets>'
|
|
7332
7359
|
\`\`\`
|
|
7333
7360
|
|
|
7334
7361
|
For full integration, copy the hippo-memory Pi extension to \`~/.pi/agent/extensions/hippo-memory/\`.
|
|
7335
7362
|
`.trim(),
|
|
7336
7363
|
},
|
|
7337
7364
|
};
|
|
7365
|
+
// sha256 of each trimmed block an earlier hippo wrote, so init refreshes only blocks nobody edited. Add the old hash when a block changes.
|
|
7366
|
+
const SHIPPED_HOOK_HASHES = new Map([
|
|
7367
|
+
['c04e48f2896a4fee9ae98f8f832e2d26a3910269df3beb5bcd6baee3cd9db68e', 'claude-code'],
|
|
7368
|
+
['e6b12bd8983c032e5ca8e95a97aeff4178a5a05026d10acad5b2e1b25d5656dd', 'claude-code'],
|
|
7369
|
+
['4c64e11d3e5be68fa547c9248d7553feb645a02f7cf13ba02f7275e1854baf44', 'claude-code'],
|
|
7370
|
+
['293bd319bbc86225a0ee027490a3322a0336257f5832f7fade65e4ffb2530654', 'claude-code'],
|
|
7371
|
+
['15abcece9712279fb4721f7a8f0ba117457400278977beb5cf5b5d7ba49f7b1a', 'codex'],
|
|
7372
|
+
['0c81a6b2c21473313001f624b80ea870e661aecbfda9bfe8503febc0d5f34533', 'codex'],
|
|
7373
|
+
['88e45358aba4f17912f113221c991dc758275991335d1daa4aa1974a69c46769', 'codex'],
|
|
7374
|
+
['a1415ecda9b2f8f317c233738e4a5ac16e6b2cc385a017c0c8ecfbfacbcab6a3', 'cursor'],
|
|
7375
|
+
['a38c428bbdfc14ec50f6f7b9183785170a4eae1ce9cde60257cca6efc7206b3a', 'cursor'],
|
|
7376
|
+
['40524c3bd5a2eb04036567cc761451961d950995768bccd93a9900b0f75eafea', 'openclaw'],
|
|
7377
|
+
['7b3518e8c0feaa7b8b454cde7743f7598ad14cd9979e1680d0954484e2464aae', 'openclaw'],
|
|
7378
|
+
['4601c67c31f41cd5b1324cfccdb1afc66872b7fb0bc1e7c5789ecabb1f6bd942', 'opencode'],
|
|
7379
|
+
['90d9e21d8d1ecbe99a0fc7b7f2d9f8af7b5315a6b4b0203df4f7a9bdc0699b98', 'opencode'],
|
|
7380
|
+
['8b8f5986d7f7ed15f06e68720d8913c3cab23d94366b411935ca2bbaa334553b', 'pi'],
|
|
7381
|
+
['37767b355e18beac726b05b9e2b898dab8c6135fd7b98f3aa52edc734d5dd283', 'pi'],
|
|
7382
|
+
]);
|
|
7338
7383
|
function cmdHook(args, flags) {
|
|
7339
7384
|
const subcommand = args[0];
|
|
7340
7385
|
const target = args[1];
|
|
7386
|
+
// Cursor has no init row, so the AGENTS.md block is usually another agent's, and Cursor reads that one as it is.
|
|
7387
|
+
const othersBlock = (text) => target === 'cursor' && hippoBlock(text)?.owner !== 'cursor';
|
|
7341
7388
|
if (subcommand === 'list') {
|
|
7342
7389
|
console.log('Available hooks:\n');
|
|
7343
7390
|
for (const [name, hook] of Object.entries(HOOKS)) {
|
|
@@ -7359,7 +7406,10 @@ function cmdHook(args, flags) {
|
|
|
7359
7406
|
let agentFileTouched = false;
|
|
7360
7407
|
if (fs.existsSync(filepath)) {
|
|
7361
7408
|
const existing = fs.readFileSync(filepath, 'utf8');
|
|
7362
|
-
if (existing.includes(HOOK_MARKERS.start)) {
|
|
7409
|
+
if (existing.includes(HOOK_MARKERS.start) && othersBlock(existing)) {
|
|
7410
|
+
console.log(`${hook.file} already has a hippo block, which Cursor reads; left it as is.`);
|
|
7411
|
+
}
|
|
7412
|
+
else if (existing.includes(HOOK_MARKERS.start)) {
|
|
7363
7413
|
const re = new RegExp(`${escapeRegex(HOOK_MARKERS.start)}[\\s\\S]*?${escapeRegex(HOOK_MARKERS.end)}`, 'g');
|
|
7364
7414
|
const updated = existing.replace(re, block);
|
|
7365
7415
|
fs.writeFileSync(filepath, updated, 'utf8');
|
|
@@ -7447,10 +7497,13 @@ function cmdHook(args, flags) {
|
|
|
7447
7497
|
const filepath = path.resolve(process.cwd(), hook.file);
|
|
7448
7498
|
if (fs.existsSync(filepath)) {
|
|
7449
7499
|
const existing = fs.readFileSync(filepath, 'utf8');
|
|
7450
|
-
if (existing.includes(HOOK_MARKERS.start)) {
|
|
7451
|
-
const
|
|
7452
|
-
const
|
|
7453
|
-
|
|
7500
|
+
if (existing.includes(HOOK_MARKERS.start) && othersBlock(existing)) {
|
|
7501
|
+
const owner = hippoBlock(existing)?.owner;
|
|
7502
|
+
const whose = owner ? `hippo wrote it for ${owner}` : 'it has been edited, so hippo cannot tell whose it is';
|
|
7503
|
+
console.log(`Left the hippo block in ${hook.file}: ${whose}. Delete it by hand if no agent needs it.`);
|
|
7504
|
+
}
|
|
7505
|
+
else if (existing.includes(HOOK_MARKERS.start)) {
|
|
7506
|
+
fs.writeFileSync(filepath, withoutHookBlock(existing) + '\n', 'utf8');
|
|
7454
7507
|
console.log(`Removed Hippo hook from ${hook.file}`);
|
|
7455
7508
|
}
|
|
7456
7509
|
else {
|
|
@@ -7479,11 +7532,28 @@ function cmdHook(args, flags) {
|
|
|
7479
7532
|
console.log('Removed Codex wrapper integration');
|
|
7480
7533
|
}
|
|
7481
7534
|
}
|
|
7535
|
+
else if (target === 'cursor') {
|
|
7536
|
+
// Older hippo wrote Cursor's block to .cursorrules, creating the file when it was missing.
|
|
7537
|
+
const legacy = path.resolve(process.cwd(), '.cursorrules');
|
|
7538
|
+
const old = fs.existsSync(legacy) ? fs.readFileSync(legacy, 'utf8') : '';
|
|
7539
|
+
if (old.includes(HOOK_MARKERS.start)) {
|
|
7540
|
+
const left = withoutHookBlock(old);
|
|
7541
|
+
if (left)
|
|
7542
|
+
fs.writeFileSync(legacy, left + '\n', 'utf8');
|
|
7543
|
+
else
|
|
7544
|
+
fs.unlinkSync(legacy);
|
|
7545
|
+
console.log(left ? 'Removed the old Hippo hook from .cursorrules' : 'Deleted .cursorrules, which held only the old Hippo hook');
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7482
7548
|
return;
|
|
7483
7549
|
}
|
|
7484
7550
|
console.error('Usage: hippo hook <install|uninstall|list> [target]');
|
|
7485
7551
|
process.exit(1);
|
|
7486
7552
|
}
|
|
7553
|
+
function withoutHookBlock(text) {
|
|
7554
|
+
const re = new RegExp(`\\n?${escapeRegex(HOOK_MARKERS.start)}[\\s\\S]*?${escapeRegex(HOOK_MARKERS.end)}\\n?`, 'g');
|
|
7555
|
+
return text.replace(re, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
7556
|
+
}
|
|
7487
7557
|
function escapeRegex(s) {
|
|
7488
7558
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
7489
7559
|
}
|
package/dist/config.js
CHANGED
|
@@ -151,8 +151,15 @@ export function loadConfig(hippoRoot) {
|
|
|
151
151
|
`(got ${JSON.stringify(churnStalenessEnabled)}) - using false.`);
|
|
152
152
|
churnStalenessEnabled = false;
|
|
153
153
|
}
|
|
154
|
+
// Every writer starts a memory on this, and a zero or negative half-life scores zero strength, so sleep would retire it.
|
|
155
|
+
let defaultHalfLifeDays = raw.defaultHalfLifeDays ?? DEFAULT_CONFIG.defaultHalfLifeDays;
|
|
156
|
+
if (!Number.isFinite(defaultHalfLifeDays) || defaultHalfLifeDays <= 0) {
|
|
157
|
+
console.error(`Warning: config.json's "defaultHalfLifeDays" must be a number of days above 0 ` +
|
|
158
|
+
`(got ${JSON.stringify(defaultHalfLifeDays)}) - using ${DEFAULT_CONFIG.defaultHalfLifeDays}.`);
|
|
159
|
+
defaultHalfLifeDays = DEFAULT_CONFIG.defaultHalfLifeDays;
|
|
160
|
+
}
|
|
154
161
|
return {
|
|
155
|
-
defaultHalfLifeDays
|
|
162
|
+
defaultHalfLifeDays,
|
|
156
163
|
defaultBudget: raw.defaultBudget ?? DEFAULT_CONFIG.defaultBudget,
|
|
157
164
|
defaultContextBudget: raw.defaultContextBudget ?? DEFAULT_CONFIG.defaultContextBudget,
|
|
158
165
|
decayBasis: validBasis ? basis : DEFAULT_CONFIG.decayBasis,
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface ConsolidationResult {
|
|
|
37
37
|
/** Ids the decay pass removes (or would remove, under dryRun). */
|
|
38
38
|
removedIds?: string[];
|
|
39
39
|
}
|
|
40
|
+
/** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
|
|
41
|
+
export declare const MEMORY_BACKED_TABLES: readonly ["predictions", "decisions", "incidents", "processes", "policies", "skills", "project_briefs", "customer_notes"];
|
|
40
42
|
/**
|
|
41
43
|
* Run a full consolidation pass.
|
|
42
44
|
*/
|
package/dist/consolidate.js
CHANGED
|
@@ -24,7 +24,7 @@ import { resolveTenantId } from './tenant.js';
|
|
|
24
24
|
import { rescueSet, rankNonPinnedByTenant, validateWeights } from './memory-value.js';
|
|
25
25
|
import { MEMORY_VALUE_WEIGHTS, SOURCE_ARTIFACT_SHA256 } from './memory-value-weights.js';
|
|
26
26
|
import { appendAuditEvent } from './audit.js';
|
|
27
|
-
import { migrateDefaultHalfLife } from './half-life-migration.js';
|
|
27
|
+
import { migrateDefaultHalfLife, LEGACY_TYPED_HALF_LIFE } from './half-life-migration.js';
|
|
28
28
|
import { derivationScope, commonDerivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
29
29
|
import { isQuarantineScope } from './quarantine.js';
|
|
30
30
|
const DECAY_THRESHOLD = 0.05;
|
|
@@ -64,10 +64,10 @@ const REPLAY_COUNT_DEFAULT = 5;
|
|
|
64
64
|
function isJsonString(value) {
|
|
65
65
|
return typeof value === 'string';
|
|
66
66
|
}
|
|
67
|
-
/** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL). */
|
|
68
|
-
const MEMORY_BACKED_TABLES = ['predictions', 'decisions', 'processes', 'policies', 'skills', 'project_briefs', 'customer_notes'];
|
|
67
|
+
/** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
|
|
68
|
+
export const MEMORY_BACKED_TABLES = ['predictions', 'decisions', 'incidents', 'processes', 'policies', 'skills', 'project_briefs', 'customer_notes'];
|
|
69
69
|
/**
|
|
70
|
-
* Ids of memories that back a first-class object (a decision, prediction,
|
|
70
|
+
* Ids of memories that back a first-class object (a decision, incident, prediction,
|
|
71
71
|
* process, policy, skill, project brief or customer note). Sleep never
|
|
72
72
|
* retires these: deleting or moving one to dormant storage fires the
|
|
73
73
|
* object's ON DELETE SET NULL and a restore cannot repair the link. Their
|
|
@@ -85,8 +85,10 @@ function memoriesBackingObjects(hippoRoot) {
|
|
|
85
85
|
for (const r of rows)
|
|
86
86
|
ids.add(r.memory_id);
|
|
87
87
|
}
|
|
88
|
-
catch {
|
|
89
|
-
//
|
|
88
|
+
catch (err) {
|
|
89
|
+
// A missing table is an older schema; any other error could hide a backing memory, so sleep stops.
|
|
90
|
+
if (!(err instanceof Error && err.message.includes('no such table')))
|
|
91
|
+
throw err;
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
}
|
|
@@ -131,6 +133,9 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
131
133
|
if (halfLife.rescaled > 0) {
|
|
132
134
|
result.details.push(` ⏳ ${dryRun ? 'would move' : 'moved'} ${halfLife.rescaled} memories from the ${halfLife.from}-day to the ${halfLife.to}-day half-life`);
|
|
133
135
|
}
|
|
136
|
+
if (halfLife.typed > 0) {
|
|
137
|
+
result.details.push(` ⏳ ${dryRun ? 'would move' : 'moved'} ${halfLife.typed} memories of decisions, incidents and other objects from the ${LEGACY_TYPED_HALF_LIFE}-day to the ${halfLife.to}-day half-life`);
|
|
138
|
+
}
|
|
134
139
|
// L9: host-wide by design. Consolidation runs across all tenants in one
|
|
135
140
|
// pass — per-tenant filtering would create N consolidation runs per host
|
|
136
141
|
// with no cross-tenant dedup. The api.sleep audit row tags this with the
|
|
@@ -410,6 +415,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
410
415
|
// consolidationTenant — for any non-default tenant that check never
|
|
411
416
|
// hit, and the trace regenerated every sleep.
|
|
412
417
|
tenantId: consolidationTenant,
|
|
418
|
+
baseHalfLifeDays: config.defaultHalfLifeDays,
|
|
413
419
|
});
|
|
414
420
|
// AT1 (same producer-side pattern as the merge pass below): traceExistsForSession
|
|
415
421
|
// only sees rows CURRENTLY in the store — once a rejected trace is
|
|
@@ -737,6 +743,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
737
743
|
confidence: 'inferred',
|
|
738
744
|
tenantId: mergeTenant,
|
|
739
745
|
scope: mergeScope,
|
|
746
|
+
baseHalfLifeDays: config.defaultHalfLifeDays,
|
|
740
747
|
});
|
|
741
748
|
}
|
|
742
749
|
// mergeContents is DETERMINISTIC CONCATENATION (not an LLM paraphrase)
|
package/dist/customer-notes.js
CHANGED
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
23
23
|
import { writeEntry, assertTenantId } from './store.js';
|
|
24
24
|
import { markGraphDirty, removeGraphEntitiesForObject } from './graph.js';
|
|
25
|
-
import { createMemory, Layer
|
|
25
|
+
import { createMemory, Layer } from './memory.js';
|
|
26
26
|
import { appendAuditEvent } from './audit.js';
|
|
27
|
+
import { objectHalfLifeDays } from './half-life-migration.js';
|
|
27
28
|
export const VALID_NOTE_STATES = new Set([
|
|
28
29
|
'active',
|
|
29
30
|
'superseded',
|
|
@@ -118,9 +119,9 @@ export function saveCustomerNote(hippoRoot, tenantId, opts, actor = 'cli') {
|
|
|
118
119
|
layer: Layer.Semantic,
|
|
119
120
|
confidence: 'verified',
|
|
120
121
|
source: 'customer_note',
|
|
122
|
+
baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
|
|
121
123
|
tenantId,
|
|
122
124
|
});
|
|
123
|
-
mem.half_life_days = CUSTOMER_NOTE_HALF_LIFE_DAYS;
|
|
124
125
|
let savedRow;
|
|
125
126
|
writeEntry(hippoRoot, mem, {
|
|
126
127
|
actor,
|
package/dist/dag.js
CHANGED
|
@@ -3,6 +3,7 @@ import { writeEntry, loadAllDirtySummaries, loadChildrenOfSummary, applyRebuildR
|
|
|
3
3
|
import { RejectedValueError } from './rejection.js';
|
|
4
4
|
import { redactSecrets } from './secret-detect.js';
|
|
5
5
|
import { derivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
6
7
|
export function clusterFacts(facts) {
|
|
7
8
|
if (facts.length === 0)
|
|
8
9
|
return [];
|
|
@@ -84,6 +85,7 @@ export async function generateDagSummary(label, factContents, opts) {
|
|
|
84
85
|
}
|
|
85
86
|
export async function buildDag(hippoRoot, facts, opts) {
|
|
86
87
|
const result = { candidateClusters: 0, summariesCreated: 0, factsLinked: 0, rejected: 0 };
|
|
88
|
+
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
87
89
|
const unparented = facts.filter((f) => f.dag_level === 1 && !f.dag_parent_id && f.tags.includes('extracted'));
|
|
88
90
|
// Hardening follow-up (mirrors consolidate.ts's mergeCandidatesByTenant,
|
|
89
91
|
// T1): partition unparented facts by tenantId BEFORE clustering so a
|
|
@@ -123,6 +125,7 @@ export async function buildDag(hippoRoot, facts, opts) {
|
|
|
123
125
|
dag_level: 2,
|
|
124
126
|
tenantId: factTenant,
|
|
125
127
|
scope: factScope,
|
|
128
|
+
baseHalfLifeDays,
|
|
126
129
|
});
|
|
127
130
|
// Schema v25: cache descendant_count + earliest/latest_at on the summary
|
|
128
131
|
// row so DAG-aware recall (docs/plans/2026-05-05-dag-recall.md Task 2)
|
|
@@ -293,6 +296,7 @@ export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
|
|
|
293
296
|
failed: 0,
|
|
294
297
|
rejected: 0,
|
|
295
298
|
};
|
|
299
|
+
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
296
300
|
// Only L2 with no L3 parent yet (avoid re-clustering already-profiled L2s).
|
|
297
301
|
const unparented = l2Summaries.filter((s) => s.dag_level === 2 && !s.dag_parent_id);
|
|
298
302
|
// independent-review HIGH #1 fold: cluster ONLY within-tenant.
|
|
@@ -330,6 +334,7 @@ export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
|
|
|
330
334
|
dag_level: 3,
|
|
331
335
|
tenantId, // HIGH #1 fold: thread tenant explicitly
|
|
332
336
|
scope,
|
|
337
|
+
baseHalfLifeDays,
|
|
333
338
|
});
|
|
334
339
|
profileEntry.descendant_count = cluster.members.length;
|
|
335
340
|
profileEntry.earliest_at = memberCreatedAts[0];
|
package/dist/decisions.d.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface ListDecisionsOpts {
|
|
|
64
64
|
*
|
|
65
65
|
* The memory mirror preserves the legacy `hippo decide` shape: tags
|
|
66
66
|
* ['decision', ...extraTags], source 'decision', confidence 'verified',
|
|
67
|
-
*
|
|
67
|
+
* the half-life objectHalfLifeDays picks, content = "<text>\n\nContext: <context>"
|
|
68
68
|
* when context is given (so existing recall output is unchanged).
|
|
69
69
|
*/
|
|
70
70
|
export declare function saveDecision(hippoRoot: string, tenantId: string, opts: SaveDecisionOpts, actor?: string): Decision;
|
package/dist/decisions.js
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
27
27
|
import { writeEntry, assertTenantId } from './store.js';
|
|
28
28
|
import { markGraphDirty, removeGraphEntitiesForObject } from './graph.js';
|
|
29
|
-
import { createMemory, Layer
|
|
29
|
+
import { createMemory, Layer } from './memory.js';
|
|
30
30
|
import { appendAuditEvent } from './audit.js';
|
|
31
|
+
import { objectHalfLifeDays } from './half-life-migration.js';
|
|
31
32
|
export const VALID_DECISION_STATES = new Set([
|
|
32
33
|
'active',
|
|
33
34
|
'superseded',
|
|
@@ -65,7 +66,7 @@ const DECISION_COLS = `
|
|
|
65
66
|
*
|
|
66
67
|
* The memory mirror preserves the legacy `hippo decide` shape: tags
|
|
67
68
|
* ['decision', ...extraTags], source 'decision', confidence 'verified',
|
|
68
|
-
*
|
|
69
|
+
* the half-life objectHalfLifeDays picks, content = "<text>\n\nContext: <context>"
|
|
69
70
|
* when context is given (so existing recall output is unchanged).
|
|
70
71
|
*/
|
|
71
72
|
export function saveDecision(hippoRoot, tenantId, opts, actor = 'cli') {
|
|
@@ -82,9 +83,9 @@ export function saveDecision(hippoRoot, tenantId, opts, actor = 'cli') {
|
|
|
82
83
|
layer: Layer.Semantic,
|
|
83
84
|
confidence: 'verified',
|
|
84
85
|
source: 'decision',
|
|
86
|
+
baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
|
|
85
87
|
tenantId,
|
|
86
88
|
});
|
|
87
|
-
mem.half_life_days = DECISION_HALF_LIFE_DAYS;
|
|
88
89
|
// Populated inside afterWrite so the INSERT, the supersede UPDATE, and the
|
|
89
90
|
// memory write all share one SAVEPOINT.
|
|
90
91
|
let savedRow;
|