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/audit.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface AuditResult {
|
|
|
16
16
|
export declare function auditMemory(entry: MemoryEntry): AuditIssue | null;
|
|
17
17
|
export declare function auditMemories(entries: MemoryEntry[]): AuditResult;
|
|
18
18
|
export declare function isContentWorthStoring(content: string): boolean;
|
|
19
|
-
export type AuditOp = 'remember' | 'recall' | 'promote' | 'supersede' | 'forget' | 'archive_raw' | 'auth_revoke' | 'auth_create' | 'outcome' | 'consolidate' | 'audit_prune' | 'summary_marked_dirty' | 'summary_marked_clean' | 'summary_rebuilt' | 'predict_create' | 'predict_close' | 'predict_baserate' | 'recall_autodebias_hint' | 'recall_autodebias_hint_no_class_match' | 'recall_autodebias_hint_tiebreak' | 'recall_anchor_detected_query_repeat' | 'recall_anchor_detected_memory_dominance' | 'recall_anchor_skipped_no_session' | 'recall_availability_detected' | 'decision_create' | 'decision_supersede' | 'decision_close' | 'incident_open' | 'incident_resolve' | 'incident_close' | 'process_create' | 'process_supersede' | 'process_close' | 'policy_create' | 'policy_supersede' | 'policy_close' | 'skill_create' | 'skill_supersede' | 'skill_close' | 'project_brief_create' | 'project_brief_supersede' | 'project_brief_close' | 'customer_note_create' | 'customer_note_supersede' | 'customer_note_close' | 'mv_rescue' | 'reject_value' | 'reject_refusal' | 'unreject_value' | 'conflict_resolve';
|
|
19
|
+
export type AuditOp = 'remember' | 'recall' | 'promote' | 'supersede' | 'forget' | 'archive_raw' | 'auth_revoke' | 'auth_create' | 'outcome' | 'consolidate' | 'audit_prune' | 'summary_marked_dirty' | 'summary_marked_clean' | 'summary_rebuilt' | 'predict_create' | 'predict_close' | 'predict_baserate' | 'recall_autodebias_hint' | 'recall_autodebias_hint_no_class_match' | 'recall_autodebias_hint_tiebreak' | 'recall_anchor_detected_query_repeat' | 'recall_anchor_detected_memory_dominance' | 'recall_anchor_skipped_no_session' | 'recall_availability_detected' | 'decision_create' | 'decision_supersede' | 'decision_close' | 'incident_open' | 'incident_resolve' | 'incident_close' | 'process_create' | 'process_supersede' | 'process_close' | 'policy_create' | 'policy_supersede' | 'policy_close' | 'skill_create' | 'skill_supersede' | 'skill_close' | 'project_brief_create' | 'project_brief_supersede' | 'project_brief_close' | 'customer_note_create' | 'customer_note_supersede' | 'customer_note_close' | 'mv_rescue' | 'reject_value' | 'reject_refusal' | 'unreject_value' | 'half_life_migrate' | 'dormant_restore' | 'conflict_resolve';
|
|
20
20
|
export interface AppendAuditOpts {
|
|
21
21
|
tenantId: string;
|
|
22
22
|
actor: string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { JsonValue } from './working-memory.js';
|
|
2
|
+
/** Why a failure was not stored, or `stored`. */
|
|
3
|
+
export type CaptureErrorOutcome = 'stored' | 'duplicate' | 'skipped-interrupt' | 'skipped-routine' | 'skipped-invalid';
|
|
4
|
+
/** Which routine check skipped a failure; the log keeps it so declines can be told apart from empty searches. */
|
|
5
|
+
export type RoutineRule = 'declined' | 'os-permission' | 'no-match' | 'search-tool' | 'quiet-exit';
|
|
6
|
+
/** What {@link lessonFromFailure} read from a payload; `detail` is the finer failure-log key (untruncated, command head). */
|
|
7
|
+
export type FailureReading = {
|
|
8
|
+
text: string;
|
|
9
|
+
detail: string;
|
|
10
|
+
} | {
|
|
11
|
+
skip: 'skipped-routine';
|
|
12
|
+
rule: RoutineRule;
|
|
13
|
+
text: string;
|
|
14
|
+
detail: string;
|
|
15
|
+
} | {
|
|
16
|
+
skip: 'skipped-interrupt' | 'skipped-invalid';
|
|
17
|
+
text: null;
|
|
18
|
+
detail: null;
|
|
19
|
+
};
|
|
20
|
+
/** Normalised form used to spot repeats. The failure log keeps only its hash, so changing it breaks repeat counts. */
|
|
21
|
+
export declare function failureSignature(text: string): string;
|
|
22
|
+
/** The memory text for a failure payload, or why it is not stored; a routine skip keeps its text for the log. Pure. */
|
|
23
|
+
export declare function lessonFromFailure(payload: JsonValue): FailureReading;
|
|
24
|
+
/** Store a failure as an error memory unless it is routine or a repeat, and log it either way, even when storing throws. */
|
|
25
|
+
export declare function captureToolFailure(hippoRoot: string, tenantId: string, payload: JsonValue): CaptureErrorOutcome;
|
|
26
|
+
//# sourceMappingURL=capture-error.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// `hippo capture-error`, run by the Claude Code PostToolUseFailure hook: routine failures and repeats are not
|
|
2
|
+
// stored, because error memories decay slowly and would crowd out real lessons; what is stored stays `observed`
|
|
3
|
+
// until outcome feedback confirms it. Every failure, stored or not, goes to the failure log (ROADMAP CD13).
|
|
4
|
+
import { createMemory } from './memory.js';
|
|
5
|
+
import { writeEntry, loadAllEntries } from './store.js';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
7
|
+
import { closeHippoDb, openHippoDb } from './db.js';
|
|
8
|
+
import { recordFailure } from './failure-log.js';
|
|
9
|
+
import { blockHash } from './token-ledger.js';
|
|
10
|
+
const MAX_LEN = 200;
|
|
11
|
+
/** The user, a permission prompt or a hook said no: routine, not a lesson. */
|
|
12
|
+
const DECLINED = /user (?:doesn't|does not) want|denied by (?:the )?user|user (?:rejected|declined|denied)|permission to use|was blocked by (?:a )?hook/i;
|
|
13
|
+
/** The OS or a remote host refused access (EACCES, SSH publickey): routine too, but nobody declined anything. */
|
|
14
|
+
const OS_PERMISSION = /permission denied/i;
|
|
15
|
+
/** Leading `cd` or `pushd` steps say where a command ran, not what it ran. */
|
|
16
|
+
const LEADING_CD = /^\s*(?:(?:cd|pushd)\b[^;&|]*(?:&&|\|\||;)\s*)+/;
|
|
17
|
+
const NO_MATCH = /\bno (?:matches|files|results) found\b/i;
|
|
18
|
+
/** Shell commands whose exit code 1 means "nothing found" or "differs", not an error. */
|
|
19
|
+
const QUIET_EXIT_1 = /^\s*(?:grep|rg|egrep|fgrep|find|test|\[|diff|cmp|git diff|git grep)\b/;
|
|
20
|
+
function isString(v) {
|
|
21
|
+
return v !== undefined && v !== null && v.constructor === String;
|
|
22
|
+
}
|
|
23
|
+
function isObject(v) {
|
|
24
|
+
return v !== undefined && v !== null && !Array.isArray(v) && v.constructor === Object;
|
|
25
|
+
}
|
|
26
|
+
function payloadString(payload, key) {
|
|
27
|
+
if (!isObject(payload))
|
|
28
|
+
return null;
|
|
29
|
+
const value = payload[key];
|
|
30
|
+
return isString(value) && value.trim() !== '' ? value : null;
|
|
31
|
+
}
|
|
32
|
+
/** Normalised form used to spot repeats. The failure log keeps only its hash, so changing it breaks repeat counts. */
|
|
33
|
+
export function failureSignature(text) {
|
|
34
|
+
return text.toLowerCase().replace(/[0-9a-f]{7,}/g, '#').replace(/\d+/g, '#').replace(/\s+/g, ' ').trim();
|
|
35
|
+
}
|
|
36
|
+
/** The memory text for a failure payload, or why it is not stored; a routine skip keeps its text for the log. Pure. */
|
|
37
|
+
export function lessonFromFailure(payload) {
|
|
38
|
+
if (!isObject(payload))
|
|
39
|
+
return { skip: 'skipped-invalid', text: null, detail: null };
|
|
40
|
+
// SAFETY: isObject narrowed payload to a plain JSON object; the fields read are all optional.
|
|
41
|
+
const p = payload;
|
|
42
|
+
if (p.is_interrupt === true)
|
|
43
|
+
return { skip: 'skipped-interrupt', text: null, detail: null };
|
|
44
|
+
if (!isString(p.error) || p.error.trim().length < 12)
|
|
45
|
+
return { skip: 'skipped-invalid', text: null, detail: null };
|
|
46
|
+
const tool = isString(p.tool_name) ? p.tool_name : 'tool';
|
|
47
|
+
const error = p.error.replace(/\s+/g, ' ').trim();
|
|
48
|
+
const text = `${tool}: ${error}`.slice(0, MAX_LEN);
|
|
49
|
+
const command = isObject(p.tool_input) && isString(p.tool_input['command']) ? p.tool_input['command'].replace(LEADING_CD, '') : '';
|
|
50
|
+
const head = command.trim().split(/\s+/).slice(0, 2).join(' ');
|
|
51
|
+
const detail = `${tool}${head ? ` ${head}` : ''}: ${error}`;
|
|
52
|
+
const routine = (rule) => ({ skip: 'skipped-routine', rule, text, detail });
|
|
53
|
+
if (DECLINED.test(error))
|
|
54
|
+
return routine('declined');
|
|
55
|
+
if (OS_PERMISSION.test(error))
|
|
56
|
+
return routine('os-permission');
|
|
57
|
+
if (NO_MATCH.test(error))
|
|
58
|
+
return routine('no-match');
|
|
59
|
+
if (tool === 'Grep' || tool === 'Glob')
|
|
60
|
+
return routine('search-tool');
|
|
61
|
+
if (tool === 'Bash' && QUIET_EXIT_1.test(command) && /exit code 1\b/i.test(error))
|
|
62
|
+
return routine('quiet-exit');
|
|
63
|
+
return { text, detail };
|
|
64
|
+
}
|
|
65
|
+
/** Store a failure as an error memory unless it is routine or a repeat, and log it either way, even when storing throws. */
|
|
66
|
+
export function captureToolFailure(hippoRoot, tenantId, payload) {
|
|
67
|
+
const lesson = lessonFromFailure(payload);
|
|
68
|
+
let outcome = 'store-failed';
|
|
69
|
+
try {
|
|
70
|
+
outcome = 'skip' in lesson ? lesson.skip : storeLesson(hippoRoot, tenantId, lesson.text);
|
|
71
|
+
return outcome;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
logFailure(hippoRoot, tenantId, payload, lesson, outcome);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function logFailure(hippoRoot, tenantId, payload, lesson, outcome) {
|
|
78
|
+
const hash = (s) => (s === null ? null : blockHash(failureSignature(s)));
|
|
79
|
+
const db = openHippoDb(hippoRoot);
|
|
80
|
+
try {
|
|
81
|
+
recordFailure(db, {
|
|
82
|
+
tenantId,
|
|
83
|
+
sessionId: payloadString(payload, 'session_id'),
|
|
84
|
+
tool: payloadString(payload, 'tool_name'),
|
|
85
|
+
outcome,
|
|
86
|
+
rule: 'rule' in lesson ? lesson.rule : null,
|
|
87
|
+
sigHash: hash(lesson.text),
|
|
88
|
+
detailHash: hash(lesson.detail),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
closeHippoDb(db);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function storeLesson(hippoRoot, tenantId, text) {
|
|
96
|
+
const sig = failureSignature(text);
|
|
97
|
+
const repeat = loadAllEntries(hippoRoot, tenantId).some((e) => e.tags.includes('auto-captured') && failureSignature(e.content) === sig);
|
|
98
|
+
if (repeat)
|
|
99
|
+
return 'duplicate';
|
|
100
|
+
const entry = createMemory(text, {
|
|
101
|
+
tags: ['error', 'auto-captured'],
|
|
102
|
+
source: 'tool-failure',
|
|
103
|
+
confidence: 'observed',
|
|
104
|
+
tenantId,
|
|
105
|
+
baseHalfLifeDays: loadConfig(hippoRoot).defaultHalfLifeDays,
|
|
106
|
+
});
|
|
107
|
+
writeEntry(hippoRoot, entry);
|
|
108
|
+
return 'stored';
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=capture-error.js.map
|
package/dist/capture.d.ts
CHANGED
|
@@ -45,14 +45,6 @@ export interface CaptureOptions {
|
|
|
45
45
|
*/
|
|
46
46
|
tenantId?: string;
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Build a compact text summary from a Claude Code / OpenCode JSONL transcript.
|
|
50
|
-
* Keeps plain user messages and the final chunk of assistant text, drops
|
|
51
|
-
* thinking blocks, tool_use, and tool_result noise. Output is fed to the
|
|
52
|
-
* existing `extractFromText` pipeline.
|
|
53
|
-
*
|
|
54
|
-
* Exported for tests.
|
|
55
|
-
*/
|
|
56
48
|
export declare function summariseTranscript(jsonl: string): string;
|
|
57
49
|
/**
|
|
58
50
|
* Resolve a transcript path for `--last-session`.
|
|
@@ -111,6 +103,24 @@ export declare function readTranscriptTail(transcriptPath: string, capBytes?: nu
|
|
|
111
103
|
* guard, not per-file copies.
|
|
112
104
|
*/
|
|
113
105
|
export declare function sanitizeLogMessage(message: string): string;
|
|
106
|
+
/** What one pre-compact run saved, reported to the user after compaction. */
|
|
107
|
+
export interface PreCompactReport {
|
|
108
|
+
snapshotSaved: boolean;
|
|
109
|
+
captured: number;
|
|
110
|
+
/** Claude Code session the run belonged to, when the payload named one. */
|
|
111
|
+
sessionId: string | null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The line shown to the user after compaction, or null when nothing was
|
|
115
|
+
* saved. Without it the only sign was Claude Code's generic
|
|
116
|
+
* "PreCompact [...] completed successfully".
|
|
117
|
+
*/
|
|
118
|
+
export declare function preCompactMessage(report: Pick<PreCompactReport, 'snapshotSaved' | 'captured'>): string | null;
|
|
119
|
+
/**
|
|
120
|
+
* Where pre-compact leaves its report for `hippo post-compact`: next to the
|
|
121
|
+
* pre-compact log, so both hooks find it from the same `--log-file`.
|
|
122
|
+
*/
|
|
123
|
+
export declare function preCompactReportPath(logFile: string): string;
|
|
114
124
|
export interface PreCompactOptions {
|
|
115
125
|
stdinText?: string;
|
|
116
126
|
stdinTimedOut?: boolean;
|
|
@@ -124,4 +134,11 @@ export interface PreCompactOptions {
|
|
|
124
134
|
* could turn a caught-and-logged failure back into a non-zero exit.
|
|
125
135
|
*/
|
|
126
136
|
export declare function cmdPreCompact(hippoRoot: string, options: PreCompactOptions): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* The message for the PostCompact hook, from the report pre-compact left,
|
|
139
|
+
* or null when there is nothing to say. Consumes the report, so the message
|
|
140
|
+
* shows once. A report from another session, or an old one, is dropped.
|
|
141
|
+
* Never throws.
|
|
142
|
+
*/
|
|
143
|
+
export declare function postCompactMessage(stdinText: string | undefined, logFile?: string, now?: Date): string | null;
|
|
127
144
|
//# sourceMappingURL=capture.d.ts.map
|
package/dist/capture.js
CHANGED
|
@@ -21,6 +21,7 @@ import { defaultPreCompactLogPath } from './hooks.js';
|
|
|
21
21
|
import { redactSecrets } from './secret-detect.js';
|
|
22
22
|
import { RejectedValueError, checkRejectionGuard } from './rejection.js';
|
|
23
23
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
24
|
+
import { loadConfig } from './config.js';
|
|
24
25
|
// Sentence-level patterns
|
|
25
26
|
//
|
|
26
27
|
// T1 (DF2): each pattern now carries TWO capture groups — group 1 is the
|
|
@@ -489,6 +490,7 @@ function writeExtractedItems(hippoRoot, tenantId, extracted) {
|
|
|
489
490
|
let captured = 0;
|
|
490
491
|
let skipped = 0;
|
|
491
492
|
let rejected = 0;
|
|
493
|
+
const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
|
|
492
494
|
for (const item of extracted) {
|
|
493
495
|
if (isDuplicate(item.content, existing)) {
|
|
494
496
|
skipped++;
|
|
@@ -500,6 +502,7 @@ function writeExtractedItems(hippoRoot, tenantId, extracted) {
|
|
|
500
502
|
source: 'capture',
|
|
501
503
|
confidence: 'observed',
|
|
502
504
|
tenantId,
|
|
505
|
+
baseHalfLifeDays,
|
|
503
506
|
});
|
|
504
507
|
// AT1 (plan §3 containment): a refusal is per-VALUE — one rejected
|
|
505
508
|
// extraction must not abort the rest of this transcript's captures.
|
|
@@ -551,6 +554,14 @@ function errorMessage(cause) {
|
|
|
551
554
|
*
|
|
552
555
|
* Exported for tests.
|
|
553
556
|
*/
|
|
557
|
+
/** Leading markers of the command lines Claude Code writes with type 'user'. */
|
|
558
|
+
const CLAUDE_CODE_COMMAND_PREFIXES = ['<local-command-', '<command-name>', '<command-message>', '<command-args>'];
|
|
559
|
+
function isNonHumanUserLine(entry, content) {
|
|
560
|
+
if (entry.isMeta === true || entry.isSidechain === true || entry.isCompactSummary === true)
|
|
561
|
+
return true;
|
|
562
|
+
const head = content.trimStart();
|
|
563
|
+
return CLAUDE_CODE_COMMAND_PREFIXES.some((p) => head.startsWith(p));
|
|
564
|
+
}
|
|
554
565
|
export function summariseTranscript(jsonl) {
|
|
555
566
|
const lines = jsonl.split('\n').filter((l) => l.trim());
|
|
556
567
|
const userMessages = [];
|
|
@@ -571,8 +582,9 @@ export function summariseTranscript(jsonl) {
|
|
|
571
582
|
continue;
|
|
572
583
|
const content = 'content' in message ? message.content : undefined;
|
|
573
584
|
if (entry.type === 'user') {
|
|
574
|
-
// Plain text user messages only (skip tool_result arrays)
|
|
575
|
-
|
|
585
|
+
// Plain text user messages only (skip tool_result arrays), and only
|
|
586
|
+
// ones the human wrote (see isNonHumanUserLine).
|
|
587
|
+
if (isStringValue(content) && content.trim() && !isNonHumanUserLine(entry, content)) {
|
|
576
588
|
userMessages.push(content.trim());
|
|
577
589
|
}
|
|
578
590
|
}
|
|
@@ -1030,7 +1042,7 @@ function lastPlainUserMessage(jsonl) {
|
|
|
1030
1042
|
if (!message)
|
|
1031
1043
|
continue;
|
|
1032
1044
|
const content = 'content' in message ? message.content : undefined;
|
|
1033
|
-
if (isStringValue(content) && content.trim())
|
|
1045
|
+
if (isStringValue(content) && content.trim() && !isNonHumanUserLine(entry, content))
|
|
1034
1046
|
return content.trim();
|
|
1035
1047
|
}
|
|
1036
1048
|
return '';
|
|
@@ -1109,12 +1121,34 @@ function isReadableFile(filePath) {
|
|
|
1109
1121
|
return false;
|
|
1110
1122
|
}
|
|
1111
1123
|
}
|
|
1124
|
+
/**
|
|
1125
|
+
* The line shown to the user after compaction, or null when nothing was
|
|
1126
|
+
* saved. Without it the only sign was Claude Code's generic
|
|
1127
|
+
* "PreCompact [...] completed successfully".
|
|
1128
|
+
*/
|
|
1129
|
+
export function preCompactMessage(report) {
|
|
1130
|
+
if (!report.snapshotSaved && report.captured === 0)
|
|
1131
|
+
return null;
|
|
1132
|
+
const parts = [];
|
|
1133
|
+
if (report.snapshotSaved)
|
|
1134
|
+
parts.push('your task snapshot');
|
|
1135
|
+
if (report.captured > 0)
|
|
1136
|
+
parts.push(`${report.captured} new memor${report.captured === 1 ? 'y' : 'ies'}`);
|
|
1137
|
+
return `Hippo saved ${parts.join(' and ')} before compacting.${report.snapshotSaved ? ' The snapshot is restored into the new context.' : ''}`;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Where pre-compact leaves its report for `hippo post-compact`: next to the
|
|
1141
|
+
* pre-compact log, so both hooks find it from the same `--log-file`.
|
|
1142
|
+
*/
|
|
1143
|
+
export function preCompactReportPath(logFile) {
|
|
1144
|
+
return path.join(path.dirname(logFile), 'pre-compact-last.json');
|
|
1145
|
+
}
|
|
1112
1146
|
/**
|
|
1113
1147
|
* Runs the PreCompact producer. Returns any `embedMemory` promises kicked
|
|
1114
1148
|
* off along the way (empty on every skip path) so `cmdPreCompact` can await
|
|
1115
1149
|
* them, bounded, before it exits (X6).
|
|
1116
1150
|
*/
|
|
1117
|
-
function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile) {
|
|
1151
|
+
function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
1118
1152
|
// X3: the PreCompact hook fires in every Claude Code project, including
|
|
1119
1153
|
// ones that never ran `hippo init`, so gate before any store-opening call
|
|
1120
1154
|
// (saveActiveTaskSnapshot etc. call initStore, which would create one).
|
|
@@ -1156,6 +1190,7 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile) {
|
|
|
1156
1190
|
}
|
|
1157
1191
|
if ('session_id' in payload && isStringValue(payload.session_id))
|
|
1158
1192
|
sessionId = payload.session_id;
|
|
1193
|
+
report.sessionId = sessionId;
|
|
1159
1194
|
payloadTranscriptPath = payload.transcript_path;
|
|
1160
1195
|
}
|
|
1161
1196
|
// X11: payload transcript_path must end .jsonl. No directory-containment
|
|
@@ -1291,6 +1326,7 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile) {
|
|
|
1291
1326
|
session_id: sessionId,
|
|
1292
1327
|
});
|
|
1293
1328
|
appendPreCompactLog(logFile, 'snapshot saved');
|
|
1329
|
+
report.snapshotSaved = true;
|
|
1294
1330
|
}
|
|
1295
1331
|
catch (err) {
|
|
1296
1332
|
appendPreCompactLog(logFile, `snapshot save failed: ${errorMessage(err)}`);
|
|
@@ -1300,6 +1336,7 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile) {
|
|
|
1300
1336
|
// the next SessionEnd capture (existing dedup absorbs the overlap).
|
|
1301
1337
|
try {
|
|
1302
1338
|
const { captured, skipped, rejected, embeds } = writeExtractedItems(hippoRoot, tenantId, extracted);
|
|
1339
|
+
report.captured = captured;
|
|
1303
1340
|
appendPreCompactLog(logFile, `capture: ${captured} items captured, ${skipped} skipped` +
|
|
1304
1341
|
(rejected > 0 ? `, ${rejected} rejected` : ''));
|
|
1305
1342
|
return embeds;
|
|
@@ -1324,8 +1361,9 @@ const EMBED_SETTLE_TIMEOUT_MS = 3000;
|
|
|
1324
1361
|
export async function cmdPreCompact(hippoRoot, options) {
|
|
1325
1362
|
const logFile = options.logFile ?? defaultPreCompactLogPath();
|
|
1326
1363
|
let embeds = [];
|
|
1364
|
+
const report = { snapshotSaved: false, captured: 0, sessionId: null };
|
|
1327
1365
|
try {
|
|
1328
|
-
embeds = runPreCompact(hippoRoot, options.stdinText, options.stdinTimedOut ?? false, logFile);
|
|
1366
|
+
embeds = runPreCompact(hippoRoot, options.stdinText, options.stdinTimedOut ?? false, logFile, report);
|
|
1329
1367
|
}
|
|
1330
1368
|
catch (err) {
|
|
1331
1369
|
appendPreCompactLog(logFile, `pre-compact failed: ${errorMessage(err)}`);
|
|
@@ -1341,6 +1379,63 @@ export async function cmdPreCompact(hippoRoot, options) {
|
|
|
1341
1379
|
clearTimeout(timer);
|
|
1342
1380
|
appendPreCompactLog(logFile, outcome === 'settled' ? 'embeddings settled' : 'embeddings timeout');
|
|
1343
1381
|
}
|
|
1382
|
+
// Nothing goes to stdout: Claude Code passes PreCompact stdout to the
|
|
1383
|
+
// summarising model as extra instructions. The PostCompact hook
|
|
1384
|
+
// (`hippo post-compact`) tells the user instead, from this report.
|
|
1385
|
+
if (report.snapshotSaved || report.captured > 0) {
|
|
1386
|
+
try {
|
|
1387
|
+
fs.writeFileSync(preCompactReportPath(logFile), JSON.stringify({ ...report, at: new Date().toISOString() }));
|
|
1388
|
+
}
|
|
1389
|
+
catch {
|
|
1390
|
+
// Losing the message must never fail the hook.
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1344
1393
|
process.exit(0);
|
|
1345
1394
|
}
|
|
1395
|
+
/** How old a pre-compact report may be and still describe this compaction. */
|
|
1396
|
+
const POST_COMPACT_REPORT_MAX_AGE_MS = 10 * 60_000;
|
|
1397
|
+
/**
|
|
1398
|
+
* The message for the PostCompact hook, from the report pre-compact left,
|
|
1399
|
+
* or null when there is nothing to say. Consumes the report, so the message
|
|
1400
|
+
* shows once. A report from another session, or an old one, is dropped.
|
|
1401
|
+
* Never throws.
|
|
1402
|
+
*/
|
|
1403
|
+
export function postCompactMessage(stdinText, logFile = defaultPreCompactLogPath(), now = new Date()) {
|
|
1404
|
+
const reportFile = preCompactReportPath(logFile);
|
|
1405
|
+
let raw;
|
|
1406
|
+
try {
|
|
1407
|
+
raw = fs.readFileSync(reportFile, 'utf8');
|
|
1408
|
+
fs.rmSync(reportFile, { force: true });
|
|
1409
|
+
}
|
|
1410
|
+
catch {
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
try {
|
|
1414
|
+
const report = JSON.parse(raw);
|
|
1415
|
+
if (!isObjectLike(report))
|
|
1416
|
+
return null;
|
|
1417
|
+
const at = 'at' in report && isStringValue(report.at) ? Date.parse(report.at) : Number.NaN;
|
|
1418
|
+
if (Number.isNaN(at) || now.getTime() - at > POST_COMPACT_REPORT_MAX_AGE_MS)
|
|
1419
|
+
return null;
|
|
1420
|
+
let payloadSession = null;
|
|
1421
|
+
try {
|
|
1422
|
+
const payload = JSON.parse((stdinText ?? '').trim() || 'null');
|
|
1423
|
+
if (isObjectLike(payload) && 'session_id' in payload && isStringValue(payload.session_id))
|
|
1424
|
+
payloadSession = payload.session_id;
|
|
1425
|
+
}
|
|
1426
|
+
catch {
|
|
1427
|
+
// No usable payload: fall back to the age check alone.
|
|
1428
|
+
}
|
|
1429
|
+
const reportSession = 'sessionId' in report && isStringValue(report.sessionId) ? report.sessionId : null;
|
|
1430
|
+
if (payloadSession !== null && reportSession !== null && payloadSession !== reportSession)
|
|
1431
|
+
return null;
|
|
1432
|
+
return preCompactMessage({
|
|
1433
|
+
snapshotSaved: 'snapshotSaved' in report && report.snapshotSaved === true,
|
|
1434
|
+
captured: 'captured' in report && Number.isInteger(report.captured) ? Number(report.captured) : 0,
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
catch {
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1346
1441
|
//# sourceMappingURL=capture.js.map
|
package/dist/cli.d.ts
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>"
|
|
@@ -58,6 +61,8 @@ export declare function printContextMarkdown(items: Array<{
|
|
|
58
61
|
score: number;
|
|
59
62
|
tokens: number;
|
|
60
63
|
isGlobal: boolean;
|
|
61
|
-
}>, totalTokens: number, framing?: string
|
|
64
|
+
}>, totalTokens: number, framing?: string, opts?: {
|
|
65
|
+
showStrength?: boolean;
|
|
66
|
+
}): void;
|
|
62
67
|
export declare function runCli(argv?: string[]): Promise<void>;
|
|
63
68
|
//# sourceMappingURL=cli.d.ts.map
|