hippo-memory 1.45.0 → 1.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -15
- package/bin/hippo.js +0 -0
- package/dist/ablation.d.ts +10 -1
- package/dist/ablation.js +17 -1
- package/dist/api.d.ts +66 -1
- package/dist/api.js +202 -7
- package/dist/audit.d.ts +1 -1
- package/dist/capture-error.d.ts +26 -0
- package/dist/capture-error.js +110 -0
- package/dist/capture.d.ts +25 -8
- package/dist/capture.js +100 -5
- package/dist/cli.d.ts +6 -1
- package/dist/cli.js +432 -48
- package/dist/config.d.ts +20 -0
- package/dist/config.js +35 -0
- package/dist/consolidate.d.ts +6 -0
- package/dist/consolidate.js +98 -13
- package/dist/db.js +81 -1
- package/dist/doctor.d.ts +34 -0
- package/dist/doctor.js +183 -0
- package/dist/dormant.d.ts +91 -0
- package/dist/dormant.js +121 -0
- package/dist/eval-stats.d.ts +123 -0
- package/dist/eval-stats.js +187 -0
- package/dist/failure-log.d.ts +49 -0
- package/dist/failure-log.js +58 -0
- package/dist/half-life-migration.d.ts +55 -0
- package/dist/half-life-migration.js +111 -0
- package/dist/hooks.d.ts +4 -0
- package/dist/hooks.js +47 -0
- package/dist/mcp/server.d.ts +6 -0
- package/dist/mcp/server.js +70 -13
- package/dist/memory.d.ts +16 -2
- package/dist/memory.js +27 -5
- package/dist/physics-config.js +5 -1
- package/dist/recall-scope.d.ts +24 -0
- package/dist/recall-scope.js +41 -0
- package/dist/reject-flow.d.ts +3 -3
- package/dist/reject-flow.js +10 -3
- package/dist/search.d.ts +4 -4
- package/dist/search.js +23 -18
- package/dist/server.js +11 -1
- package/dist/store.d.ts +12 -1
- package/dist/store.js +58 -12
- package/dist/token-ledger.d.ts +119 -0
- package/dist/token-ledger.js +181 -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 +2 -1
package/dist/cli.js
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* hippo reject <id>|--value "<text>" --reason "<why>"
|
|
20
20
|
* hippo rejections
|
|
21
21
|
* hippo unreject <digest-prefix>
|
|
22
|
+
* hippo dormant [<query>] [--limit <n>] [--json] | restore <id> | forget <id>
|
|
23
|
+
* hippo tokens [--days <n>] [--json] [--global]
|
|
24
|
+
* hippo doctor [--json]
|
|
22
25
|
* hippo inspect <id>
|
|
23
26
|
* hippo embed [--status]
|
|
24
27
|
* hippo watch "<command>"
|
|
@@ -45,7 +48,7 @@ import { isHandoffOutcome, formatHandoffEvidenceLine } from './handoff.js';
|
|
|
45
48
|
import { isCardStatus } from './card.js';
|
|
46
49
|
import { loadCardDetail } from './card-detail.js';
|
|
47
50
|
import { passesScopeFilterForRecall } from './recall-scope.js';
|
|
48
|
-
import { hybridSearch, physicsSearch, explainMatch, textOverlap, tokenize as tokenizeQuery } from './search.js';
|
|
51
|
+
import { estimateTokens, hybridSearch, physicsSearch, explainMatch, textOverlap, tokenize as tokenizeQuery } from './search.js';
|
|
49
52
|
import { compareEntryIdentity } from './compare.js';
|
|
50
53
|
import { renderTraceContent, parseSteps } from './trace.js';
|
|
51
54
|
import { writeRecallTraceAtRoot } from './recall-trace.js';
|
|
@@ -56,6 +59,10 @@ import { loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
|
|
|
56
59
|
import { computeSystemEnergy, vecNorm } from './physics.js';
|
|
57
60
|
import { loadConfig } from './config.js';
|
|
58
61
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
62
|
+
import { runDoctor, formatDoctor } from './doctor.js';
|
|
63
|
+
import { captureToolFailure } from './capture-error.js';
|
|
64
|
+
import { blockHash, hookPayloadSessionId, lastSentState, recordTokenUse, shouldSkipUnchanged } from './token-ledger.js';
|
|
65
|
+
import { FAILURE_LOG_RETENTION_DAYS } from './failure-log.js';
|
|
59
66
|
import { pushGoal, getActiveGoals, completeGoal, suspendGoal, resumeGoal, applyGoalStackBoost } from './goals.js';
|
|
60
67
|
import { rowToGoal } from './goals.js';
|
|
61
68
|
import { captureError, extractLessons, partitionLessons, deduplicateLesson, runWatched, fetchGitLog, isGitRepo, } from './autolearn.js';
|
|
@@ -65,7 +72,7 @@ import { detectScope } from './scope.js';
|
|
|
65
72
|
import { getGlobalRoot, initGlobal, shareMemory, listPeers, autoShare, transferScore, searchBothHybrid, syncGlobalToLocal, } from './shared.js';
|
|
66
73
|
import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
67
74
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, importVault, } from './importers.js';
|
|
68
|
-
import { cmdCapture, cmdPreCompact, resolveLastSessionTranscript, truncateCodePointSafe, sanitizeLogMessage } from './capture.js';
|
|
75
|
+
import { cmdCapture, cmdPreCompact, postCompactMessage, resolveLastSessionTranscript, truncateCodePointSafe, sanitizeLogMessage } from './capture.js';
|
|
69
76
|
import { readStdinBounded } from './stdin.js';
|
|
70
77
|
import { auditMemories, appendAuditEvent, } from './audit.js';
|
|
71
78
|
import { listApiKeys, revokeApiKey } from './auth.js';
|
|
@@ -542,17 +549,17 @@ function autoInstallHooks(quiet) {
|
|
|
542
549
|
// Skip if we already installed a hook into this file
|
|
543
550
|
if (installed.has(targetPath))
|
|
544
551
|
continue;
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
552
|
+
// An instruction block already present is not rewritten, but the JSON
|
|
553
|
+
// hooks and plugins below still run: they are idempotent, and skipping
|
|
554
|
+
// them meant a store set up by an older hippo never got hooks added in
|
|
555
|
+
// later releases (PreCompact, PostToolUseFailure) on a re-run of init.
|
|
556
|
+
const blockPresent = fs.existsSync(targetPath)
|
|
557
|
+
&& fs.readFileSync(targetPath, 'utf8').includes(HOOK_MARKERS.start);
|
|
551
558
|
// Only patch the agent-instructions file if it already exists.
|
|
552
559
|
// Never create a new CLAUDE.md / AGENTS.md / etc. just because a sibling
|
|
553
560
|
// marker file (.claude/settings.json, .codex, etc.) was detected — that
|
|
554
561
|
// pollutes dirs the user didn't intend to configure.
|
|
555
|
-
if (fs.existsSync(targetPath)) {
|
|
562
|
+
if (!blockPresent && fs.existsSync(targetPath)) {
|
|
556
563
|
const block = `${HOOK_MARKERS.start}\n${hookDef.content}\n${HOOK_MARKERS.end}`;
|
|
557
564
|
const existing = fs.readFileSync(targetPath, 'utf8');
|
|
558
565
|
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
@@ -563,7 +570,7 @@ function autoInstallHooks(quiet) {
|
|
|
563
570
|
// The Codex session-capture wrapper swaps the codex launcher binary, so
|
|
564
571
|
// init never installs it silently — it points at the explicit opt-in
|
|
565
572
|
// command instead (issue #133).
|
|
566
|
-
if (hook === 'codex' && !isCodexWrapperInstalled()) {
|
|
573
|
+
if (hook === 'codex' && !blockPresent && !isCodexWrapperInstalled()) {
|
|
567
574
|
console.log(' Codex detected. To capture Codex sessions: hippo hook install codex');
|
|
568
575
|
}
|
|
569
576
|
// For Claude Code, also install SessionEnd+SessionStart entries in its
|
|
@@ -586,6 +593,12 @@ function autoInstallHooks(quiet) {
|
|
|
586
593
|
if (result.installedCompactResume) {
|
|
587
594
|
console.log(` Auto-installed hippo compact-resume SessionStart(compact) hook in ${hook} settings`);
|
|
588
595
|
}
|
|
596
|
+
if (result.installedPostCompact) {
|
|
597
|
+
console.log(` Auto-installed hippo post-compact PostCompact hook in ${hook} settings`);
|
|
598
|
+
}
|
|
599
|
+
if (result.installedCaptureError) {
|
|
600
|
+
console.log(` Auto-installed hippo capture-error PostToolUseFailure hook in ${hook} settings`);
|
|
601
|
+
}
|
|
589
602
|
if (result.migratedFromStop) {
|
|
590
603
|
console.log(` Migrated legacy Stop hook → SessionEnd (no longer runs every turn)`);
|
|
591
604
|
}
|
|
@@ -1657,7 +1670,7 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1657
1670
|
sessionHandoff =
|
|
1658
1671
|
rawHandoff && passesScopeFilterForRecall(rowScope(rawHandoff), effectiveScope) ? rawHandoff : null;
|
|
1659
1672
|
recentSessionEvents = rawEvents.filter((e) => passesScopeFilterForRecall(rowScope(e), effectiveScope));
|
|
1660
|
-
const tokenize = (s) => s ?
|
|
1673
|
+
const tokenize = (s) => s ? estimateTokens(s) : 0;
|
|
1661
1674
|
continuityTokens =
|
|
1662
1675
|
tokenize(activeSnapshot?.task) +
|
|
1663
1676
|
tokenize(activeSnapshot?.summary) +
|
|
@@ -1675,6 +1688,16 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
1675
1688
|
const hasContinuity = activeSnapshot !== null
|
|
1676
1689
|
|| sessionHandoff !== null
|
|
1677
1690
|
|| recentSessionEvents.length > 0;
|
|
1691
|
+
// TE0 token ledger: the memory text this recall hands back (results plus
|
|
1692
|
+
// any continuity block), recorded once per query.
|
|
1693
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
1694
|
+
tenantId,
|
|
1695
|
+
sessionId: hostSessionId() ?? null,
|
|
1696
|
+
surface: 'recall',
|
|
1697
|
+
event: 'inject',
|
|
1698
|
+
items: results.length,
|
|
1699
|
+
tokens: results.reduce((acc, r) => acc + (r.tokens || 0), 0) + continuityTokens,
|
|
1700
|
+
}));
|
|
1678
1701
|
if (results.length === 0) {
|
|
1679
1702
|
// LC1 F1 structural fix (docs/plans/2026-08-02-lc1-recall-trace-persistence.md):
|
|
1680
1703
|
// trace the zero-result recall too (result_count 0, no result rows) so
|
|
@@ -2748,6 +2771,14 @@ export function renderSleepResult(result) {
|
|
|
2748
2771
|
console.log(`\nResults:`);
|
|
2749
2772
|
console.log(` Active memories: ${result.active}`);
|
|
2750
2773
|
console.log(` Removed (decayed): ${result.removed}`);
|
|
2774
|
+
// Only when dormant.enabled moved something, so every other render stays
|
|
2775
|
+
// byte-identical (tests/cli-context-render-snapshot.test.ts).
|
|
2776
|
+
if (result.dormant !== undefined && result.dormant > 0) {
|
|
2777
|
+
console.log(` Kept dormant: ${result.dormant} (hippo dormant to list)`);
|
|
2778
|
+
}
|
|
2779
|
+
if (result.dormantExpired !== undefined && result.dormantExpired > 0) {
|
|
2780
|
+
console.log(` Expired dormant: ${result.dormantExpired} (past dormant.retentionDays)`);
|
|
2781
|
+
}
|
|
2751
2782
|
console.log(` Merged episodic: ${result.mergedEpisodic}`);
|
|
2752
2783
|
console.log(` New semantic: ${result.newSemantic}`);
|
|
2753
2784
|
if (result.details && result.details.length > 0) {
|
|
@@ -3506,6 +3537,12 @@ function cmdForget(hippoRoot, id, flags) {
|
|
|
3506
3537
|
// memory, not a missing one. Point the user at the archive path.
|
|
3507
3538
|
console.error(rawForgetRefusal(id));
|
|
3508
3539
|
}
|
|
3540
|
+
else if (api.isDormant(ctx, id)) {
|
|
3541
|
+
// Sleep moved it to the dormant store: it is not in active memory, so
|
|
3542
|
+
// point at the command that owns it.
|
|
3543
|
+
console.error(`${id} is dormant, not in active memory. Delete it for good: hippo dormant forget ${id} ` +
|
|
3544
|
+
`(or bring it back: hippo dormant restore ${id})`);
|
|
3545
|
+
}
|
|
3509
3546
|
else {
|
|
3510
3547
|
console.error(`Memory not found: ${id}`);
|
|
3511
3548
|
}
|
|
@@ -3789,6 +3826,152 @@ function cmdUnreject(hippoRoot, args, flags) {
|
|
|
3789
3826
|
}
|
|
3790
3827
|
console.log(`Unrejected [${outcome.digest.slice(0, 12)}...] (was: ${outcome.reason ?? 'none given'})`);
|
|
3791
3828
|
}
|
|
3829
|
+
/**
|
|
3830
|
+
* `hippo dormant [list] [<query>...] [--limit <n>] [--json] [--global]`,
|
|
3831
|
+
* `hippo dormant restore <id>`, `hippo dormant forget <id>`.
|
|
3832
|
+
* Dormant memories are what sleep keeps instead of deleting (on by default;
|
|
3833
|
+
* `"dormant": { "enabled": false }` in .hippo/config.json deletes instead).
|
|
3834
|
+
*/
|
|
3835
|
+
function cmdDormant(hippoRoot, args, flags) {
|
|
3836
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3837
|
+
const ctx = {
|
|
3838
|
+
hippoRoot: root,
|
|
3839
|
+
tenantId: resolveTenantId({}),
|
|
3840
|
+
actor: api.adminActor('cli'),
|
|
3841
|
+
};
|
|
3842
|
+
const sub = args[0];
|
|
3843
|
+
if (sub === 'restore' || sub === 'forget') {
|
|
3844
|
+
const id = (args[1] ?? '').trim();
|
|
3845
|
+
if (!id) {
|
|
3846
|
+
console.error(`Usage: hippo dormant ${sub} <id>`);
|
|
3847
|
+
process.exit(1);
|
|
3848
|
+
}
|
|
3849
|
+
try {
|
|
3850
|
+
if (sub === 'restore') {
|
|
3851
|
+
api.restoreDormant(ctx, id);
|
|
3852
|
+
console.log(`Restored ${id} to active memory.`);
|
|
3853
|
+
}
|
|
3854
|
+
else {
|
|
3855
|
+
api.forgetDormant(ctx, id);
|
|
3856
|
+
console.log(`Forgot dormant memory ${id} permanently.`);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
catch (err) {
|
|
3860
|
+
if (err instanceof RejectedValueError) {
|
|
3861
|
+
console.error(`Cannot restore ${id}: its value was rejected (${err.reason ?? 'no reason given'}). Run \`hippo unreject\` first to allow it.`);
|
|
3862
|
+
}
|
|
3863
|
+
else {
|
|
3864
|
+
console.error(`Could not ${sub} ${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3865
|
+
}
|
|
3866
|
+
process.exit(1);
|
|
3867
|
+
}
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3870
|
+
const queryArgs = sub === 'list' ? args.slice(1) : args;
|
|
3871
|
+
const limit = parseCountFlag(flags['limit']);
|
|
3872
|
+
const rows = api.listDormant(ctx, {
|
|
3873
|
+
query: queryArgs.join(' '),
|
|
3874
|
+
limit: limit > 0 ? limit : undefined,
|
|
3875
|
+
});
|
|
3876
|
+
if (flags['json']) {
|
|
3877
|
+
console.log(JSON.stringify({ dormant: rows }, null, 2));
|
|
3878
|
+
return;
|
|
3879
|
+
}
|
|
3880
|
+
if (rows.length === 0) {
|
|
3881
|
+
console.log(queryArgs.length > 0 ? 'No dormant memories match.' : 'No dormant memories.');
|
|
3882
|
+
if (!loadConfig(root).dormant.enabled) {
|
|
3883
|
+
console.log(`Sleep deletes faded memories. To keep them dormant instead, set "dormant": { "enabled": true } in ${path.join(root, 'config.json')}.`);
|
|
3884
|
+
}
|
|
3885
|
+
return;
|
|
3886
|
+
}
|
|
3887
|
+
console.log(`${rows.length} dormant memor${rows.length === 1 ? 'y' : 'ies'}${queryArgs.length > 0 ? ' matching' : ''} (newest first):\n`);
|
|
3888
|
+
for (const row of rows) {
|
|
3889
|
+
const preview = row.content.length > 100 ? `${row.content.slice(0, 100)}...` : row.content;
|
|
3890
|
+
console.log(`--- ${row.id}`);
|
|
3891
|
+
console.log(` ${preview}`);
|
|
3892
|
+
console.log(` Dormant since ${row.dormantAt.slice(0, 10)} (${row.reason}, strength ${row.strength.toFixed(3)})${row.tags.length > 0 ? ` tags: ${row.tags.join(', ')}` : ''}`);
|
|
3893
|
+
console.log('');
|
|
3894
|
+
}
|
|
3895
|
+
console.log('Bring one back: hippo dormant restore <id> Delete for good: hippo dormant forget <id>');
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* `hippo tokens [--days <n>] [--json] [--global]`: the token ledger
|
|
3899
|
+
* (ROADMAP TE0). Tokens of memory text handed to agents per surface, blocks
|
|
3900
|
+
* the per-prompt hook skipped as unchanged (TE2) and the tokens that saved.
|
|
3901
|
+
* Counts are estimates (characters / 4), the same estimate every budget uses.
|
|
3902
|
+
*/
|
|
3903
|
+
function cmdTokens(hippoRoot, flags) {
|
|
3904
|
+
const root = resolveAuthRoot(hippoRoot, flags);
|
|
3905
|
+
const ctx = {
|
|
3906
|
+
hippoRoot: root,
|
|
3907
|
+
tenantId: resolveTenantId({}),
|
|
3908
|
+
actor: api.adminActor('cli'),
|
|
3909
|
+
};
|
|
3910
|
+
const days = parseCountFlag(flags['days']);
|
|
3911
|
+
const summary = api.tokenSummary(ctx, { days: days > 0 ? days : undefined });
|
|
3912
|
+
if (flags['json']) {
|
|
3913
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
3914
|
+
return;
|
|
3915
|
+
}
|
|
3916
|
+
const windowDays = days > 0 ? days : 30;
|
|
3917
|
+
if (summary.surfaces.length === 0) {
|
|
3918
|
+
console.log(`No memory text recorded in the last ${windowDays} days.`);
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
console.log(`Memory text handed to agents, last ${windowDays} days (estimated tokens)\n`);
|
|
3922
|
+
console.log(` ${'surface'.padEnd(14)}${'sent'.padStart(8)}${'tokens'.padStart(12)}${'skipped'.padStart(10)}${'saved'.padStart(12)}`);
|
|
3923
|
+
for (const row of summary.surfaces) {
|
|
3924
|
+
console.log(` ${row.surface.padEnd(14)}${String(row.injected).padStart(8)}${String(row.tokens).padStart(12)}`
|
|
3925
|
+
+ `${String(row.skipped).padStart(10)}${String(row.tokensAvoided).padStart(12)}`);
|
|
3926
|
+
}
|
|
3927
|
+
console.log('');
|
|
3928
|
+
console.log(` Total sent: ${summary.totalTokens} tokens. Saved by skipping unchanged blocks: ${summary.totalTokensAvoided}.`);
|
|
3929
|
+
if (summary.meanTokensPerSession > 0) {
|
|
3930
|
+
console.log(` Mean per session (rows with a session id): ${summary.meanTokensPerSession} tokens.`);
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
/** `hippo failures [--days <n>] [--json] [--global]`: failed tool calls by outcome, and repeats across sessions (CD13). */
|
|
3934
|
+
function cmdFailures(hippoRoot, flags) {
|
|
3935
|
+
// The store the capture-error hook writes to; a report never creates one.
|
|
3936
|
+
const root = flags['global'] ? getGlobalRoot() : hookStoreRoot(hippoRoot);
|
|
3937
|
+
requireInit(root);
|
|
3938
|
+
const ctx = {
|
|
3939
|
+
hippoRoot: root,
|
|
3940
|
+
tenantId: resolveTenantId({}),
|
|
3941
|
+
actor: api.adminActor('cli'),
|
|
3942
|
+
};
|
|
3943
|
+
const days = parseCountFlag(flags['days']);
|
|
3944
|
+
const summary = api.failureSummary(ctx, { days: days > 0 ? days : undefined });
|
|
3945
|
+
if (flags['json']) {
|
|
3946
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
const windowDays = days > 0 ? days : 30;
|
|
3950
|
+
const kept = windowDays > FAILURE_LOG_RETENTION_DAYS ? ` (rows are kept ${FAILURE_LOG_RETENTION_DAYS} days)` : '';
|
|
3951
|
+
if (summary.total === 0) {
|
|
3952
|
+
console.log(`No failed tool calls recorded in the last ${windowDays} days${kept}.`);
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3955
|
+
const o = summary.outcomes;
|
|
3956
|
+
const errors = o.stored + o.duplicate + o['store-failed'];
|
|
3957
|
+
const unsaved = o['store-failed'] > 0 ? `, ${o['store-failed']} could not be saved` : '';
|
|
3958
|
+
const rows = [
|
|
3959
|
+
['errors', errors, `(${o.stored} new, ${o.duplicate} already in memory${unsaved})`],
|
|
3960
|
+
['routine', o['skipped-routine'], ''],
|
|
3961
|
+
['interrupted', o['skipped-interrupt'], ''],
|
|
3962
|
+
['unreadable', o['skipped-invalid'], ''],
|
|
3963
|
+
];
|
|
3964
|
+
console.log(`Failed tool calls seen by the capture-error hook, last ${windowDays} days${kept}\n`);
|
|
3965
|
+
for (const [label, count, note] of rows) {
|
|
3966
|
+
console.log(` ${label.padEnd(13)}${String(count).padStart(6)} ${note}`.trimEnd());
|
|
3967
|
+
}
|
|
3968
|
+
// Counts, not a rate: a share means little without a holdout arm to compare against (CD11).
|
|
3969
|
+
if (summary.rated > 0) {
|
|
3970
|
+
const noSession = errors - summary.rated;
|
|
3971
|
+
const unrated = noSession > 0 ? ` ${noSession} more had no session id.` : '';
|
|
3972
|
+
console.log(`\n Repeats: ${summary.repeats} of ${summary.rated} errors first happened in another session.${unrated}`);
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3792
3975
|
function cmdSnapshot(hippoRoot, args, flags) {
|
|
3793
3976
|
requireInit(hippoRoot);
|
|
3794
3977
|
const subcommand = args[0] ?? 'show';
|
|
@@ -6117,22 +6300,24 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6117
6300
|
origin: r.origin ?? null,
|
|
6118
6301
|
category: r.category ?? null,
|
|
6119
6302
|
}));
|
|
6120
|
-
|
|
6303
|
+
const jsonText = JSON.stringify({
|
|
6121
6304
|
query: query || '*',
|
|
6122
6305
|
activeSnapshot: result.activeSnapshot ?? null,
|
|
6123
6306
|
sessionHandoff: result.sessionHandoff ?? null,
|
|
6124
6307
|
recentSessionEvents: result.recentEvents ?? [],
|
|
6125
6308
|
memories: output,
|
|
6126
6309
|
tokens: result.tokens,
|
|
6310
|
+
});
|
|
6311
|
+
console.log(jsonText);
|
|
6312
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6313
|
+
tenantId: ctx.tenantId, sessionId: currentSessionId, surface: pinnedOnly ? 'hook' : 'context',
|
|
6314
|
+
event: 'inject', items: output.length, tokens: estimateTokens(jsonText),
|
|
6127
6315
|
}));
|
|
6128
6316
|
}
|
|
6129
6317
|
else if (format === 'additional-context') {
|
|
6130
6318
|
// Claude Code UserPromptSubmit hook JSON shape. Capture print* helpers'
|
|
6131
6319
|
// output into a string buffer and wrap as `additionalContext`.
|
|
6132
|
-
const
|
|
6133
|
-
const realLog = console.log;
|
|
6134
|
-
console.log = (...parts) => { lines.push(parts.map(String).join(' ')); };
|
|
6135
|
-
try {
|
|
6320
|
+
const textBlock = captureConsole(() => {
|
|
6136
6321
|
if (result.activeSnapshot)
|
|
6137
6322
|
printActiveTaskSnapshot(result.activeSnapshot);
|
|
6138
6323
|
if (result.sessionHandoff)
|
|
@@ -6141,16 +6326,37 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6141
6326
|
printSessionEvents(result.recentEvents);
|
|
6142
6327
|
}
|
|
6143
6328
|
if (renderItems.length > 0) {
|
|
6144
|
-
|
|
6329
|
+
// TE1: no live strength percentage, so an unchanged set of memories
|
|
6330
|
+
// renders byte-identically turn after turn.
|
|
6331
|
+
printContextMarkdown(renderItems, result.tokens, framing, { showStrength: false });
|
|
6145
6332
|
}
|
|
6146
6333
|
printCrossProjectSection(crossEntries);
|
|
6147
|
-
}
|
|
6148
|
-
finally {
|
|
6149
|
-
console.log = realLog;
|
|
6150
|
-
}
|
|
6151
|
-
const textBlock = lines.join('\n');
|
|
6334
|
+
});
|
|
6152
6335
|
if (!textBlock.trim())
|
|
6153
6336
|
return;
|
|
6337
|
+
const surface = pinnedOnly ? 'hook' : 'context';
|
|
6338
|
+
const hash = blockHash(textBlock);
|
|
6339
|
+
const tokens = estimateTokens(textBlock);
|
|
6340
|
+
// TE2: the per-prompt hook skips a block identical to the one this
|
|
6341
|
+
// session already has, and resends it every refreshTurns skips. Only
|
|
6342
|
+
// with a session id from the hook payload itself: an inherited env id
|
|
6343
|
+
// (a manual run inside an agent's shell) must never suppress output.
|
|
6344
|
+
if (pinnedOnly && payloadSessionId !== undefined) {
|
|
6345
|
+
const injectCfg = loadConfig(hippoRoot).pinnedInject;
|
|
6346
|
+
if (injectCfg.skipUnchanged !== false) {
|
|
6347
|
+
const refreshTurns = Number.isFinite(injectCfg.refreshTurns) && injectCfg.refreshTurns >= 0
|
|
6348
|
+
? injectCfg.refreshTurns
|
|
6349
|
+
: 10;
|
|
6350
|
+
const last = withLedgerDb(hippoRoot, (db) => lastSentState(db, ctx.tenantId, payloadSessionId, surface));
|
|
6351
|
+
if (shouldSkipUnchanged(last ?? null, hash, refreshTurns)) {
|
|
6352
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6353
|
+
tenantId: ctx.tenantId, sessionId: payloadSessionId, surface, event: 'skip',
|
|
6354
|
+
items: renderItems.length, tokens, hash,
|
|
6355
|
+
}));
|
|
6356
|
+
return;
|
|
6357
|
+
}
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6154
6360
|
const payload = {
|
|
6155
6361
|
hookSpecificOutput: {
|
|
6156
6362
|
hookEventName: 'UserPromptSubmit',
|
|
@@ -6158,25 +6364,113 @@ async function cmdContext(hippoRoot, args, flags, stdinText) {
|
|
|
6158
6364
|
},
|
|
6159
6365
|
};
|
|
6160
6366
|
process.stdout.write(JSON.stringify(payload));
|
|
6367
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6368
|
+
tenantId: ctx.tenantId, sessionId: currentSessionId, surface, event: 'inject',
|
|
6369
|
+
items: renderItems.length, tokens, hash,
|
|
6370
|
+
}));
|
|
6161
6371
|
}
|
|
6162
6372
|
else {
|
|
6163
6373
|
// markdown (default)
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6374
|
+
const text = captureConsole(() => {
|
|
6375
|
+
if (result.activeSnapshot) {
|
|
6376
|
+
printActiveTaskSnapshot(result.activeSnapshot);
|
|
6377
|
+
}
|
|
6378
|
+
if (result.sessionHandoff) {
|
|
6379
|
+
printHandoff(result.sessionHandoff);
|
|
6380
|
+
}
|
|
6381
|
+
if (result.recentEvents && result.recentEvents.length > 0) {
|
|
6382
|
+
printSessionEvents(result.recentEvents);
|
|
6383
|
+
}
|
|
6384
|
+
if (renderItems.length > 0) {
|
|
6385
|
+
printContextMarkdown(renderItems, result.tokens, framing);
|
|
6386
|
+
}
|
|
6387
|
+
printCrossProjectSection(crossEntries);
|
|
6388
|
+
if (result.ambientState) {
|
|
6389
|
+
console.log(`\n${renderAmbientSummary(result.ambientState)}`);
|
|
6390
|
+
}
|
|
6391
|
+
});
|
|
6392
|
+
if (text.length > 0)
|
|
6393
|
+
console.log(text);
|
|
6394
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6395
|
+
tenantId: ctx.tenantId, sessionId: currentSessionId, surface: pinnedOnly ? 'hook' : 'context',
|
|
6396
|
+
event: 'inject', items: renderItems.length, tokens: estimateTokens(text),
|
|
6397
|
+
}));
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
/**
|
|
6401
|
+
* TE2: compaction drops the pinned blocks the per-prompt hook injected
|
|
6402
|
+
* earlier, so record a `reset` for the payload's session and the next prompt
|
|
6403
|
+
* injects again even if nothing changed. `requiredSource` limits it to hook
|
|
6404
|
+
* payloads with that `source` (SessionStart fires for other reasons too).
|
|
6405
|
+
* Best-effort and silent: a malformed payload records nothing.
|
|
6406
|
+
*/
|
|
6407
|
+
function resetHookInjection(hippoRoot, stdinText, requiredSource) {
|
|
6408
|
+
const sessionId = hookPayloadSessionId(stdinText, requiredSource);
|
|
6409
|
+
if (sessionId === null)
|
|
6410
|
+
return;
|
|
6411
|
+
withLedgerDb(hippoRoot, (db) => recordTokenUse(db, {
|
|
6412
|
+
tenantId: resolveTenantId({}), sessionId, surface: 'hook', event: 'reset', items: 0, tokens: 0,
|
|
6413
|
+
}));
|
|
6414
|
+
}
|
|
6415
|
+
/**
|
|
6416
|
+
* Run `fn` with console.log captured; returns the captured lines joined by
|
|
6417
|
+
* newlines (what the same calls would have printed, minus the final newline).
|
|
6418
|
+
*/
|
|
6419
|
+
function captureConsole(fn) {
|
|
6420
|
+
const lines = [];
|
|
6421
|
+
const realLog = console.log;
|
|
6422
|
+
console.log = (...parts) => { lines.push(parts.map(String).join(' ')); };
|
|
6423
|
+
try {
|
|
6424
|
+
fn();
|
|
6425
|
+
}
|
|
6426
|
+
finally {
|
|
6427
|
+
console.log = realLog;
|
|
6428
|
+
}
|
|
6429
|
+
return lines.join('\n');
|
|
6430
|
+
}
|
|
6431
|
+
/**
|
|
6432
|
+
* The store a Claude Code hook writes to: the project store when there is
|
|
6433
|
+
* one, else an existing global store, else the project path (which the hook
|
|
6434
|
+
* then skips, since hooks fire in every directory and must not create one).
|
|
6435
|
+
* Pre-compact and compact-resume must agree, or a snapshot saved to one store
|
|
6436
|
+
* is looked for in the other.
|
|
6437
|
+
*/
|
|
6438
|
+
function hookStoreRoot(hippoRoot) {
|
|
6439
|
+
if (isInitialized(hippoRoot))
|
|
6440
|
+
return hippoRoot;
|
|
6441
|
+
const globalRoot = getGlobalRoot();
|
|
6442
|
+
return isInitialized(globalRoot) ? globalRoot : hippoRoot;
|
|
6443
|
+
}
|
|
6444
|
+
/**
|
|
6445
|
+
* Run `fn` against the token ledger's store: the local store when it is
|
|
6446
|
+
* initialized, else the global one (the per-prompt hook runs in directories
|
|
6447
|
+
* without a local store). Best-effort: returns undefined and never throws,
|
|
6448
|
+
* because a ledger failure must not break context or recall.
|
|
6449
|
+
*/
|
|
6450
|
+
function withLedgerDb(hippoRoot, fn) {
|
|
6451
|
+
let root = null;
|
|
6452
|
+
try {
|
|
6453
|
+
if (isInitialized(hippoRoot))
|
|
6454
|
+
root = hippoRoot;
|
|
6455
|
+
else if (isInitialized(getGlobalRoot()))
|
|
6456
|
+
root = getGlobalRoot();
|
|
6457
|
+
}
|
|
6458
|
+
catch {
|
|
6459
|
+
return undefined;
|
|
6460
|
+
}
|
|
6461
|
+
if (root === null)
|
|
6462
|
+
return undefined;
|
|
6463
|
+
let db;
|
|
6464
|
+
try {
|
|
6465
|
+
db = openHippoDb(root);
|
|
6466
|
+
return fn(db);
|
|
6467
|
+
}
|
|
6468
|
+
catch {
|
|
6469
|
+
return undefined;
|
|
6470
|
+
}
|
|
6471
|
+
finally {
|
|
6472
|
+
if (db)
|
|
6473
|
+
closeHippoDb(db);
|
|
6180
6474
|
}
|
|
6181
6475
|
}
|
|
6182
6476
|
/**
|
|
@@ -6195,13 +6489,15 @@ function printCrossProjectSection(items) {
|
|
|
6195
6489
|
}
|
|
6196
6490
|
}
|
|
6197
6491
|
/** @internal — exported for snapshot tests (tests/cli-context-render-snapshot.test.ts). NOT a stable public API. */
|
|
6198
|
-
export function printContextMarkdown(items, totalTokens, framing = 'observe') {
|
|
6492
|
+
export function printContextMarkdown(items, totalTokens, framing = 'observe', opts = {}) {
|
|
6199
6493
|
const now = evalNow();
|
|
6494
|
+
const showStrength = opts.showStrength !== false;
|
|
6200
6495
|
console.log(`## Project Memory (${items.length} entries, ${totalTokens} tokens)\n`);
|
|
6201
6496
|
for (const item of items) {
|
|
6202
6497
|
const e = item.entry;
|
|
6203
6498
|
const tagStr = e.tags.length > 0 ? ` [${e.tags.join(', ')}]` : '';
|
|
6204
6499
|
const strengthPct = Math.round(calculateStrength(e) * 100);
|
|
6500
|
+
const strengthStr = showStrength ? ` (${strengthPct}%)` : '';
|
|
6205
6501
|
const globalPrefix = item.isGlobal ? '[global] ' : '';
|
|
6206
6502
|
const effectiveConf = confidenceFacets(e, now).tier;
|
|
6207
6503
|
const label = confidenceLabel(e, now);
|
|
@@ -6211,21 +6507,21 @@ export function printContextMarkdown(items, totalTokens, framing = 'observe') {
|
|
|
6211
6507
|
const dateStr = new Date(e.created).toISOString().slice(0, 10);
|
|
6212
6508
|
if (effectiveConf === 'verified') {
|
|
6213
6509
|
// Verified: no date prefix, just the rule
|
|
6214
|
-
console.log(`- **${confTag} ${globalPrefix}${e.content}**${tagStr}
|
|
6510
|
+
console.log(`- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6215
6511
|
}
|
|
6216
6512
|
else if (effectiveConf === 'stale') {
|
|
6217
|
-
console.log(`- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}
|
|
6513
|
+
console.log(`- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6218
6514
|
}
|
|
6219
6515
|
else {
|
|
6220
|
-
console.log(`- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}
|
|
6516
|
+
console.log(`- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6221
6517
|
}
|
|
6222
6518
|
}
|
|
6223
6519
|
else if (framing === 'suggest') {
|
|
6224
|
-
console.log(`- **${confTag} Consider checking: ${globalPrefix}${e.content}**${tagStr}
|
|
6520
|
+
console.log(`- **${confTag} Consider checking: ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6225
6521
|
}
|
|
6226
6522
|
else {
|
|
6227
6523
|
// framing === 'assert': no prefix (bare facts)
|
|
6228
|
-
console.log(`- **${confTag} ${globalPrefix}${e.content}**${tagStr}
|
|
6524
|
+
console.log(`- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`);
|
|
6229
6525
|
}
|
|
6230
6526
|
}
|
|
6231
6527
|
}
|
|
@@ -6369,7 +6665,7 @@ async function cmdWatch(command, hippoRoot) {
|
|
|
6369
6665
|
const existingWatch = loadAllEntries(hippoRoot, entry.tenantId);
|
|
6370
6666
|
const watchFit = computeSchemaFit(entry.content, entry.tags, existingWatch);
|
|
6371
6667
|
entry.schema_fit = watchFit;
|
|
6372
|
-
entry.half_life_days = deriveHalfLife(
|
|
6668
|
+
entry.half_life_days = deriveHalfLife(loadConfig(hippoRoot).defaultHalfLifeDays, entry);
|
|
6373
6669
|
entry.strength = calculateStrength(entry);
|
|
6374
6670
|
// AT1 (plan §3 containment): mechanical content from a failed command — a
|
|
6375
6671
|
// rejection-guard refusal here must not crash the watcher. Skip silently
|
|
@@ -6408,7 +6704,9 @@ function learnFromRepo(hippoRoot, repoPath, days, label) {
|
|
|
6408
6704
|
console.log(`${prefix}No fix/revert/bug commits found in the specified period.`);
|
|
6409
6705
|
return { added: 0, skipped: 0, lowInfo: 0 };
|
|
6410
6706
|
}
|
|
6411
|
-
|
|
6707
|
+
// Same patterns as MCP hippo_learn: config.gitLearnPatterns (whose default
|
|
6708
|
+
// equals extractLessons' built-in list) so a custom list applies everywhere.
|
|
6709
|
+
const parsedLessons = extractLessons(gitLog, loadConfig(hippoRoot).gitLearnPatterns);
|
|
6412
6710
|
if (parsedLessons.length === 0) {
|
|
6413
6711
|
console.log(`${prefix}No fix/revert/bug commits found in the specified period.`);
|
|
6414
6712
|
return { added: 0, skipped: 0, lowInfo: 0 };
|
|
@@ -6968,6 +7266,12 @@ function cmdHook(args, flags) {
|
|
|
6968
7266
|
if (result.installedCompactResume) {
|
|
6969
7267
|
console.log(`Installed hippo compact-resume SessionStart(compact) hook in ${result.target} settings`);
|
|
6970
7268
|
}
|
|
7269
|
+
if (result.installedPostCompact) {
|
|
7270
|
+
console.log(`Installed hippo post-compact PostCompact hook in ${result.target} settings`);
|
|
7271
|
+
}
|
|
7272
|
+
if (result.installedCaptureError) {
|
|
7273
|
+
console.log(`Installed hippo capture-error PostToolUseFailure hook in ${result.target} settings`);
|
|
7274
|
+
}
|
|
6971
7275
|
if (result.migratedFromStop) {
|
|
6972
7276
|
console.log(`Migrated legacy Stop hook → SessionEnd (was running every turn; now fires once on session exit)`);
|
|
6973
7277
|
}
|
|
@@ -7088,6 +7392,10 @@ function cmdSetup(flags) {
|
|
|
7088
7392
|
bits.push('PreCompact (pre-compact)');
|
|
7089
7393
|
if (result.installedCompactResume)
|
|
7090
7394
|
bits.push('SessionStart(compact) (compact-resume)');
|
|
7395
|
+
if (result.installedPostCompact)
|
|
7396
|
+
bits.push('PostCompact (post-compact)');
|
|
7397
|
+
if (result.installedCaptureError)
|
|
7398
|
+
bits.push('PostToolUseFailure (capture-error)');
|
|
7091
7399
|
if (result.migratedFromStop)
|
|
7092
7400
|
bits.push('migrated legacy Stop');
|
|
7093
7401
|
if (result.migratedSplitSessionEnd)
|
|
@@ -7629,6 +7937,8 @@ const VALID_AUDIT_OPS = new Set([
|
|
|
7629
7937
|
'reject_refusal', // AT1 — emitted when the rejection guard refuses a write; lockstep
|
|
7630
7938
|
'unreject_value', // AT1 — emitted by `hippo unreject`; lockstep
|
|
7631
7939
|
'conflict_resolve', // AT1 — emitted by resolveConflict on every resolution path; lockstep
|
|
7940
|
+
'half_life_migrate', // Decay default change — emitted by migrateDefaultHalfLife; lockstep with AuditOp union
|
|
7941
|
+
'dormant_restore', // Dormant memories — emitted by api.restoreDormant; lockstep with AuditOp union + server.ts VALID_AUDIT_OPS
|
|
7632
7942
|
]);
|
|
7633
7943
|
function formatAuditRow(ev) {
|
|
7634
7944
|
const target = ev.targetId ?? '-';
|
|
@@ -8354,6 +8664,28 @@ Commands:
|
|
|
8354
8664
|
--global Operate on the global store
|
|
8355
8665
|
unreject <digest-prefix> Delete a tombstone (the only escape hatch)
|
|
8356
8666
|
--global Operate on the global store
|
|
8667
|
+
dormant [<query>] List faded memories sleep kept instead of deleting
|
|
8668
|
+
(on by default; "dormant": {"enabled": false} deletes instead)
|
|
8669
|
+
--limit <n> Max rows, newest first (default: 20)
|
|
8670
|
+
--json Output as JSON
|
|
8671
|
+
--global Operate on the global store
|
|
8672
|
+
dormant restore <id> Bring a dormant memory back to active memory
|
|
8673
|
+
dormant forget <id> Delete a dormant memory permanently
|
|
8674
|
+
capture-error Store a failed tool call as an error memory (reads the Claude Code
|
|
8675
|
+
PostToolUseFailure hook payload on stdin; skips routine failures)
|
|
8676
|
+
doctor Check the install: Node, store, schema, sleep, agent hooks
|
|
8677
|
+
--json Machine-readable report (exit code 1 on any failure)
|
|
8678
|
+
tokens Tokens of memory text hippo handed agents, per surface
|
|
8679
|
+
(hook, context, recall, MCP, HTTP), and what skipping
|
|
8680
|
+
unchanged hook blocks saved
|
|
8681
|
+
--days <n> Window in days (default: 30)
|
|
8682
|
+
--json Output as JSON
|
|
8683
|
+
--global Operate on the global store
|
|
8684
|
+
failures Failed tool calls capture-error saw, by outcome, and how
|
|
8685
|
+
many errors first happened in another session
|
|
8686
|
+
--days <n> Window in days (default: 30)
|
|
8687
|
+
--json Output as JSON
|
|
8688
|
+
--global Operate on the global store
|
|
8357
8689
|
snapshot <sub> Persist or inspect the current active task
|
|
8358
8690
|
snapshot save Save active task state
|
|
8359
8691
|
--task <task>
|
|
@@ -8481,6 +8813,8 @@ Commands:
|
|
|
8481
8813
|
pre-compact PreCompact hook: snapshot + capture the tail before compaction
|
|
8482
8814
|
--log-file <p> Diagnostic log path (default: ~/.hippo/logs/pre-compact.log)
|
|
8483
8815
|
compact-resume SessionStart(compact) hook: re-print the snapshot + session trail
|
|
8816
|
+
post-compact PostCompact hook: tell the user what pre-compact saved
|
|
8817
|
+
--log-file <p> Same log path as pre-compact (default: ~/.hippo/logs/pre-compact.log)
|
|
8484
8818
|
codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper
|
|
8485
8819
|
hook <sub> [target] Manage framework integrations
|
|
8486
8820
|
hook list Show available hooks
|
|
@@ -8645,6 +8979,9 @@ Examples:
|
|
|
8645
8979
|
hippo reject --value "never store my key again" --reason "secret"
|
|
8646
8980
|
hippo rejections
|
|
8647
8981
|
hippo unreject a1b2c3d4e5f6
|
|
8982
|
+
hippo dormant "staging hostname"
|
|
8983
|
+
hippo dormant restore mem_abc123
|
|
8984
|
+
hippo tokens --days 7
|
|
8648
8985
|
hippo session log --id sess_123 --task "Ship feature" --type progress --content "Build is green, next step is docs"
|
|
8649
8986
|
hippo session latest --json
|
|
8650
8987
|
hippo session resume
|
|
@@ -8893,16 +9230,45 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
8893
9230
|
case 'pre-compact': {
|
|
8894
9231
|
// Bounded wait, not a TTY guard: an idle non-TTY pipe must not hang.
|
|
8895
9232
|
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
8896
|
-
|
|
9233
|
+
resetHookInjection(hippoRoot, stdinText, null);
|
|
9234
|
+
await cmdPreCompact(hookStoreRoot(hippoRoot), {
|
|
8897
9235
|
stdinText,
|
|
8898
9236
|
stdinTimedOut,
|
|
8899
9237
|
logFile: typeof flags['log-file'] === 'string' ? flags['log-file'] : undefined,
|
|
8900
9238
|
});
|
|
8901
9239
|
break;
|
|
8902
9240
|
}
|
|
9241
|
+
case 'post-compact': {
|
|
9242
|
+
// PostCompact hook: tells the user what pre-compact saved. Plain text,
|
|
9243
|
+
// because Claude Code shows this hook's stdout as-is. Always exits 0.
|
|
9244
|
+
const { text } = await readStdinBounded();
|
|
9245
|
+
const logFlag = flags['log-file'];
|
|
9246
|
+
const message = postCompactMessage(text, logFlag === true || logFlag === false || Array.isArray(logFlag) ? undefined : logFlag);
|
|
9247
|
+
if (message !== null)
|
|
9248
|
+
console.log(message);
|
|
9249
|
+
break;
|
|
9250
|
+
}
|
|
9251
|
+
case 'capture-error': {
|
|
9252
|
+
// PostToolUseFailure hook: every path exits 0, and nothing is created
|
|
9253
|
+
// when no store exists (the hook fires in every directory).
|
|
9254
|
+
const { text } = await readStdinBounded();
|
|
9255
|
+
try {
|
|
9256
|
+
const root = hookStoreRoot(hippoRoot);
|
|
9257
|
+
const payload = (text ?? '').trim();
|
|
9258
|
+
if (isInitialized(root) && payload) {
|
|
9259
|
+
// SAFETY: JSON.parse returns a JSON value by definition.
|
|
9260
|
+
captureToolFailure(root, resolveTenantId({}), JSON.parse(payload));
|
|
9261
|
+
}
|
|
9262
|
+
}
|
|
9263
|
+
catch {
|
|
9264
|
+
// A malformed payload or store error must never fail the agent's tool call.
|
|
9265
|
+
}
|
|
9266
|
+
break;
|
|
9267
|
+
}
|
|
8903
9268
|
case 'compact-resume': {
|
|
8904
9269
|
const { text: stdinText, timedOut: stdinTimedOut } = await readStdinBounded();
|
|
8905
|
-
|
|
9270
|
+
resetHookInjection(hippoRoot, stdinText, 'compact');
|
|
9271
|
+
cmdCompactResume(hookStoreRoot(hippoRoot), stdinText, stdinTimedOut);
|
|
8906
9272
|
break;
|
|
8907
9273
|
}
|
|
8908
9274
|
case 'codex-run':
|
|
@@ -9049,6 +9415,24 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9049
9415
|
case 'unreject':
|
|
9050
9416
|
cmdUnreject(hippoRoot, args, flags);
|
|
9051
9417
|
break;
|
|
9418
|
+
case 'dormant':
|
|
9419
|
+
cmdDormant(hippoRoot, args, flags);
|
|
9420
|
+
break;
|
|
9421
|
+
case 'tokens':
|
|
9422
|
+
cmdTokens(hippoRoot, flags);
|
|
9423
|
+
break;
|
|
9424
|
+
case 'failures':
|
|
9425
|
+
cmdFailures(hippoRoot, flags);
|
|
9426
|
+
break;
|
|
9427
|
+
case 'doctor': {
|
|
9428
|
+
// SAFETY: package.json always carries a string "version" (checked at release by check-manifest-versions).
|
|
9429
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8'));
|
|
9430
|
+
const report = runDoctor({ version: pkg.version });
|
|
9431
|
+
console.log(flags['json'] ? JSON.stringify(report, null, 2) : formatDoctor(report));
|
|
9432
|
+
if (!report.ok)
|
|
9433
|
+
process.exit(1);
|
|
9434
|
+
break;
|
|
9435
|
+
}
|
|
9052
9436
|
case 'snapshot':
|
|
9053
9437
|
cmdSnapshot(hippoRoot, args, flags);
|
|
9054
9438
|
break;
|