hippo-memory 1.52.7 → 1.52.9
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 +159 -99
- package/dist/api.d.ts +52 -18
- package/dist/api.js +155 -86
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +63 -0
- package/dist/autolearn.js +3 -2
- package/dist/capture.d.ts +40 -0
- package/dist/capture.js +141 -119
- package/dist/churn-git.js +2 -2
- package/dist/cli.d.ts +1 -4
- package/dist/cli.js +695 -702
- package/dist/codex-patch.d.ts +12 -0
- package/dist/codex-patch.js +71 -0
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -4
- package/dist/connectors/slack/types.d.ts +0 -1
- package/dist/consolidate.js +85 -32
- package/dist/context-render.d.ts +36 -0
- package/dist/context-render.js +154 -0
- package/dist/dag.js +3 -2
- package/dist/dashboard.js +4 -0
- package/dist/db.js +6 -6
- package/dist/dedupe.d.ts +6 -6
- package/dist/dedupe.js +10 -9
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +35 -2
- package/dist/dormant.d.ts +4 -0
- package/dist/dormant.js +17 -2
- package/dist/embedding-provider.d.ts +2 -1
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +23 -3
- package/dist/extensions/openclaw-plugin/index.js +1 -0
- package/dist/extract.js +5 -1
- package/dist/forward-claim-detector.d.ts +1 -1
- package/dist/forward-claim-detector.js +1 -1
- package/dist/graph-recall.d.ts +3 -1
- package/dist/graph-recall.js +13 -10
- package/dist/handoff.d.ts +2 -0
- package/dist/hooks.d.ts +19 -5
- package/dist/hooks.js +131 -30
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +174 -213
- package/dist/merged-row.d.ts +6 -0
- package/dist/merged-row.js +35 -0
- package/dist/multihop.d.ts +2 -1
- package/dist/multihop.js +7 -4
- package/dist/physics-state.d.ts +0 -4
- package/dist/physics-state.js +0 -6
- package/dist/predictions.d.ts +2 -17
- package/dist/predictions.js +2 -15
- package/dist/reject-flow.d.ts +7 -5
- package/dist/reject-flow.js +41 -12
- package/dist/salience.js +12 -5
- package/dist/same-text.d.ts +17 -0
- package/dist/same-text.js +38 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.js +8 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.js +16 -32
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +9 -3
- package/dist/server-detect.js +9 -33
- package/dist/server.js +6 -62
- package/dist/session-digest.d.ts +79 -0
- package/dist/session-digest.js +528 -0
- package/dist/shared.d.ts +11 -3
- package/dist/shared.js +41 -31
- package/dist/store.d.ts +4 -5
- package/dist/store.js +26 -13
- package/dist/token-ledger.d.ts +46 -8
- package/dist/token-ledger.js +140 -21
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist-ui/assets/index-BhT8RvO6.js +61 -0
- package/dist-ui/index.html +1 -1
- package/extensions/openclaw-plugin/README.md +4 -4
- package/extensions/openclaw-plugin/index.ts +1 -0
- package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
- package/dist-ui/assets/index-BgmA7Hwe.js +0 -61
package/dist/audit.js
CHANGED
|
@@ -167,6 +167,69 @@ export function isContentWorthStoring(content) {
|
|
|
167
167
|
return false;
|
|
168
168
|
return true;
|
|
169
169
|
}
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// A5 audit log primitives (append-only mutation trail)
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// The one list of audit ops: the AuditOp type, `hippo audit list --op` and GET /v1/audit?op= all read it.
|
|
174
|
+
export const AUDIT_OPS = [
|
|
175
|
+
'remember',
|
|
176
|
+
'recall',
|
|
177
|
+
'promote',
|
|
178
|
+
'supersede',
|
|
179
|
+
'forget',
|
|
180
|
+
'archive_raw',
|
|
181
|
+
'auth_revoke',
|
|
182
|
+
'auth_create', // emitted by api.authCreate
|
|
183
|
+
'outcome',
|
|
184
|
+
'consolidate', // emitted once per api.sleep invocation
|
|
185
|
+
'audit_prune', // emitted by pruneAuditLog after each retention prune
|
|
186
|
+
'summary_marked_dirty', // emitted by markSummaryDirty on the 0->1 transition
|
|
187
|
+
'summary_marked_clean', // emitted by clearSummaryDirtyAfterBuild after the buildDag child-link loop
|
|
188
|
+
'summary_rebuilt', // emitted by applyRebuildResult on a successful sleep-cycle rebuild
|
|
189
|
+
'predict_create', // emitted by savePrediction
|
|
190
|
+
'predict_close', // emitted by closePrediction
|
|
191
|
+
'predict_baserate', // emitted by computePredictionBaserate
|
|
192
|
+
'recall_autodebias_hint', // emitted by computePlanningFallacyOutput on success
|
|
193
|
+
'recall_autodebias_hint_no_class_match', // telemetry: forward-claim detected, no class scored
|
|
194
|
+
'recall_autodebias_hint_tiebreak', // telemetry: forward-claim detected, two or more classes tied
|
|
195
|
+
'recall_anchor_detected_query_repeat', // emitted by the anchoring detector when the same query returns the same top-1
|
|
196
|
+
'recall_anchor_detected_memory_dominance', // emitted by the anchoring detector when one memory wins top-1 across distinct queries
|
|
197
|
+
'recall_anchor_skipped_no_session', // telemetry: no sessionId, so ring tracking was skipped
|
|
198
|
+
'recall_availability_detected', // emitted when the availability/recency-bias hint fires
|
|
199
|
+
'decision_create', // emitted by saveDecision
|
|
200
|
+
'decision_supersede', // emitted by saveDecision when --supersedes resolves to an active decision
|
|
201
|
+
'decision_close', // emitted by closeDecision
|
|
202
|
+
'incident_open', // emitted by saveIncident
|
|
203
|
+
'incident_resolve', // emitted by resolveIncident
|
|
204
|
+
'incident_close', // emitted by closeIncident
|
|
205
|
+
'process_create', // emitted by saveProcess
|
|
206
|
+
'process_supersede', // emitted by saveProcess on a supersession
|
|
207
|
+
'process_close', // emitted by closeProcess
|
|
208
|
+
'policy_create', // emitted by savePolicy
|
|
209
|
+
'policy_supersede', // emitted by savePolicy on a supersession
|
|
210
|
+
'policy_close', // emitted by closePolicy
|
|
211
|
+
'skill_create', // emitted by saveSkill
|
|
212
|
+
'skill_supersede', // emitted by saveSkill on a supersession
|
|
213
|
+
'skill_close', // emitted by closeSkill
|
|
214
|
+
'project_brief_create', // emitted by saveProjectBrief
|
|
215
|
+
'project_brief_supersede', // emitted by saveProjectBrief on a supersession, including a refresh
|
|
216
|
+
'project_brief_close', // emitted by closeProjectBrief
|
|
217
|
+
'customer_note_create', // emitted by saveCustomerNote
|
|
218
|
+
'customer_note_supersede', // emitted by saveCustomerNote on a supersession
|
|
219
|
+
'customer_note_close', // emitted by closeCustomerNote
|
|
220
|
+
'mv_rescue', // emitted by consolidate() per rescued entry when config.memoryValue.enabled
|
|
221
|
+
'reject_value', // emitted by the `hippo reject` verb
|
|
222
|
+
'reject_refusal', // emitted when the rejection guard refuses a write
|
|
223
|
+
'unreject_value', // emitted by the `hippo unreject` verb
|
|
224
|
+
'conflict_resolve', // emitted by resolveConflict on every resolution path
|
|
225
|
+
'half_life_migrate', // emitted by migrateDefaultHalfLife with the rescaled ids
|
|
226
|
+
'dormant_restore', // emitted by api.restoreDormant
|
|
227
|
+
'auth_grant', // emitted by api.authGrant
|
|
228
|
+
'auth_ungrant', // emitted by api.authUngrant
|
|
229
|
+
'quarantine', // emitted by recordQuarantine inside remember's write transaction
|
|
230
|
+
'quarantine_approve', // emitted by api.quarantineApprove
|
|
231
|
+
'quarantine_reject', // emitted by api.quarantineReject
|
|
232
|
+
];
|
|
170
233
|
function isBigIntValue(value) {
|
|
171
234
|
return typeof value === 'bigint';
|
|
172
235
|
}
|
package/dist/autolearn.js
CHANGED
|
@@ -120,7 +120,7 @@ export function deduplicateLesson(hippoRootOrEntries, lesson, threshold = 0.7, t
|
|
|
120
120
|
export function runWatched(command) {
|
|
121
121
|
return new Promise((resolve) => {
|
|
122
122
|
// Use shell: true so the command string is handled by the shell as-is
|
|
123
|
-
const child = spawn(command, { shell: true, stdio: ['inherit', 'inherit', 'pipe'] });
|
|
123
|
+
const child = spawn(command, { shell: true, stdio: ['inherit', 'inherit', 'pipe'], windowsHide: true });
|
|
124
124
|
const stderrChunks = [];
|
|
125
125
|
child.stderr.on('data', (chunk) => {
|
|
126
126
|
stderrChunks.push(chunk);
|
|
@@ -148,6 +148,7 @@ export function isGitRepo(cwd) {
|
|
|
148
148
|
cwd,
|
|
149
149
|
timeout: 10000,
|
|
150
150
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
151
|
+
windowsHide: true,
|
|
151
152
|
});
|
|
152
153
|
return raw.trim() === 'true';
|
|
153
154
|
}
|
|
@@ -163,7 +164,7 @@ export function fetchGitLog(cwd, days) {
|
|
|
163
164
|
try {
|
|
164
165
|
const raw = execFileSync('git', [
|
|
165
166
|
'log', `--since=${days} days ago`, '--pretty=format:%s',
|
|
166
|
-
], { encoding: 'utf8', cwd, timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
167
|
+
], { encoding: 'utf8', cwd, timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
167
168
|
return raw;
|
|
168
169
|
}
|
|
169
170
|
catch {
|
package/dist/capture.d.ts
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* - Errors / gotchas ("error:", "bug:", "gotcha:", "watch out")
|
|
9
9
|
* - Preferences ("prefer", "use X instead of Y", "don't use")
|
|
10
10
|
*/
|
|
11
|
+
import { type TaskSnapshot } from './store.js';
|
|
11
12
|
export interface ExtractedItem {
|
|
12
13
|
content: string;
|
|
13
14
|
category: string;
|
|
14
15
|
tags: string[];
|
|
15
16
|
}
|
|
17
|
+
export declare function splitSentences(text: string): string[];
|
|
16
18
|
/**
|
|
17
19
|
* Main extraction function. Scans text for actionable items using heuristics.
|
|
18
20
|
*/
|
|
@@ -27,6 +29,7 @@ export interface CaptureOptions {
|
|
|
27
29
|
* `stdinTimedOut` marks an empty read "unknown", not "no payload". */
|
|
28
30
|
stdinText?: string;
|
|
29
31
|
stdinTimedOut?: boolean;
|
|
32
|
+
sessionTurns?: readonly SessionTurn[];
|
|
30
33
|
/**
|
|
31
34
|
* Tee stdout/stderr to this log file while capture runs. Mirrors the
|
|
32
35
|
* pattern used by `hippo sleep --log-file` so the SessionEnd hook output
|
|
@@ -44,7 +47,41 @@ export interface CaptureOptions {
|
|
|
44
47
|
* behaviour. Ignored when `global: true` (global captures are host-wide).
|
|
45
48
|
*/
|
|
46
49
|
tenantId?: string;
|
|
50
|
+
originProject?: string;
|
|
47
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Runtime shape guards used throughout this file wherever a value arrives
|
|
54
|
+
* unparsed (JSONL transcript records, stdout/stderr write() chunks). Generic
|
|
55
|
+
* over the input so the parameter is never annotated `unknown` directly — TS
|
|
56
|
+
* infers it from the call site — while the check itself avoids `typeof` by
|
|
57
|
+
* testing identity against the coercion (`String(x) === x`) /
|
|
58
|
+
* prototype-chain (`instanceof Object`) instead. Behaviourally equivalent to
|
|
59
|
+
* `typeof x === 'string'` / a truthy `typeof x === 'object'` check for
|
|
60
|
+
* anything `JSON.parse` can produce (the only divergence is boxed
|
|
61
|
+
* primitives, which JSON.parse never yields).
|
|
62
|
+
*/
|
|
63
|
+
export declare function isStringValue<T>(value: T): value is T & string;
|
|
64
|
+
export declare function isObjectLike<T>(value: T): value is T & object;
|
|
65
|
+
/** Message for a caught value of unknown shape. `cause` names the sanctioned unknown-input case (error-cause enrichment). */
|
|
66
|
+
export declare function errorMessage(cause: unknown): string;
|
|
67
|
+
/** Transcript-line flags isNonHumanUserLine reads (any may be absent); `promptSource: 'system'` marks Claude Code's own notices. */
|
|
68
|
+
interface TranscriptLineFlags {
|
|
69
|
+
type?: unknown;
|
|
70
|
+
isMeta?: unknown;
|
|
71
|
+
isSidechain?: unknown;
|
|
72
|
+
isCompactSummary?: unknown;
|
|
73
|
+
promptSource?: unknown;
|
|
74
|
+
}
|
|
75
|
+
export interface SessionTurn {
|
|
76
|
+
role: 'user' | 'assistant';
|
|
77
|
+
text: string;
|
|
78
|
+
}
|
|
79
|
+
export interface TranscriptRecord extends TranscriptLineFlags {
|
|
80
|
+
message?: unknown;
|
|
81
|
+
payload?: unknown;
|
|
82
|
+
cwd?: unknown;
|
|
83
|
+
}
|
|
84
|
+
export declare function collectSessionTurns(jsonl: string, visit?: (record: TranscriptRecord) => void): SessionTurn[];
|
|
48
85
|
export declare function summariseTranscript(jsonl: string): string;
|
|
49
86
|
/**
|
|
50
87
|
* Resolve a transcript path for `--last-session`.
|
|
@@ -116,6 +153,8 @@ export declare function preCompactMessage(report: Pick<PreCompactReport, 'snapsh
|
|
|
116
153
|
* pre-compact log, so both hooks find it from the same `--log-file`.
|
|
117
154
|
*/
|
|
118
155
|
export declare function preCompactReportPath(logFile: string): string;
|
|
156
|
+
/** A session's task, summary and next step from its transcript tail, secrets scrubbed and capped, '' where none; null with a logged reason when nothing is derivable. */
|
|
157
|
+
export declare function transcriptWorkingState(transcriptPath: string, log: (message: string) => void): Pick<TaskSnapshot, 'task' | 'summary' | 'next_step'> | null;
|
|
119
158
|
export interface PreCompactOptions {
|
|
120
159
|
stdinText?: string;
|
|
121
160
|
stdinTimedOut?: boolean;
|
|
@@ -136,4 +175,5 @@ export declare function cmdPreCompact(hippoRoot: string, options: PreCompactOpti
|
|
|
136
175
|
* Never throws.
|
|
137
176
|
*/
|
|
138
177
|
export declare function postCompactMessage(stdinText: string | undefined, logFile?: string, now?: Date): string | null;
|
|
178
|
+
export {};
|
|
139
179
|
//# sourceMappingURL=capture.d.ts.map
|
package/dist/capture.js
CHANGED
|
@@ -11,17 +11,18 @@
|
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { createMemory, Layer } from './memory.js';
|
|
14
|
+
import { duplicateKey, storedTextKeys } from './same-text.js';
|
|
14
15
|
import { isContentWorthStoring } from './audit.js';
|
|
15
16
|
import { isInitialized, writeEntry, loadAllEntries, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, } from './store.js';
|
|
16
17
|
import { getGlobalRoot, initGlobal } from './shared.js';
|
|
17
18
|
import { embedMemory } from './embeddings.js';
|
|
18
|
-
import { isEmbeddingConfigured } from './embedding-provider.js';
|
|
19
19
|
import { resolveTenantId } from './tenant.js';
|
|
20
20
|
import { defaultPreCompactLogPath } from './hooks.js';
|
|
21
|
-
import {
|
|
21
|
+
import { maskEmails, redactSecretsStrict } from './secret-detect.js';
|
|
22
22
|
import { RejectedValueError, checkRejectionGuard } from './rejection.js';
|
|
23
23
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
24
24
|
import { loadConfig } from './config.js';
|
|
25
|
+
import { classifyOriginProject } from './project-identity.js';
|
|
25
26
|
// Sentence-level patterns
|
|
26
27
|
//
|
|
27
28
|
// T1 (DF2): each pattern now carries TWO capture groups — group 1 is the
|
|
@@ -50,7 +51,7 @@ const DECISION_PATTERNS = [
|
|
|
50
51
|
/(decision:\s*)(.{1,500})/i,
|
|
51
52
|
];
|
|
52
53
|
const RULE_PATTERNS = [
|
|
53
|
-
|
|
54
|
+
/\b((?:never|always|must(?:\s+not)?|do(?:n't| not)\s+ever)\s+)(.{1,500})/i,
|
|
54
55
|
/((?:the rule is|rule:)\s*)(.{1,500})/i,
|
|
55
56
|
/((?:important|critical|remember):\s*)(.{1,500})/i,
|
|
56
57
|
/((?:make sure|ensure)\s+(?:to\s+)?)(.{1,500})/i,
|
|
@@ -77,7 +78,7 @@ const SPEC_HEADING_PATTERNS = [
|
|
|
77
78
|
// ---------------------------------------------------------------------------
|
|
78
79
|
// Extraction engine
|
|
79
80
|
// ---------------------------------------------------------------------------
|
|
80
|
-
function splitSentences(text) {
|
|
81
|
+
export function splitSentences(text) {
|
|
81
82
|
// Split on sentence boundaries, keeping reasonable chunks
|
|
82
83
|
return text
|
|
83
84
|
.split(/(?<=[.!?])\s+|\n/)
|
|
@@ -300,6 +301,21 @@ function cleanExtract(raw) {
|
|
|
300
301
|
}
|
|
301
302
|
return content;
|
|
302
303
|
}
|
|
304
|
+
/** The 500-char write gate minus the 200-char bound counted from the keyword. */
|
|
305
|
+
const RULE_LEAD_CAP = 300;
|
|
306
|
+
/** A modal rule keeps its subject ("We must never ..."); null keeps the keyword-start form when the lead is long or the keyword sits inside a bracket. */
|
|
307
|
+
function ruleLead(sentence, keywordStart, contentStart) {
|
|
308
|
+
if (keywordStart < 0 || contentStart < 0 || contentStart > RULE_LEAD_CAP)
|
|
309
|
+
return null;
|
|
310
|
+
let depth = 0;
|
|
311
|
+
for (const ch of sentence.slice(0, keywordStart)) {
|
|
312
|
+
if (ch === '(' || ch === '[' || ch === '{')
|
|
313
|
+
depth++;
|
|
314
|
+
else if ((ch === ')' || ch === ']' || ch === '}') && depth > 0)
|
|
315
|
+
depth--;
|
|
316
|
+
}
|
|
317
|
+
return depth > 0 ? null : sentence.slice(0, contentStart);
|
|
318
|
+
}
|
|
303
319
|
function extractFromPatterns(sentence, patterns, category, tag) {
|
|
304
320
|
for (const pat of patterns) {
|
|
305
321
|
// `d` gives per-group offsets; see the contentStart comment below.
|
|
@@ -355,7 +371,11 @@ function extractFromPatterns(sentence, patterns, category, tag) {
|
|
|
355
371
|
// capture, shipped in the previous commit. Codex P1, r6.
|
|
356
372
|
const contentStart = match.indices?.[2]?.[0] ?? -1;
|
|
357
373
|
const afterKeyword = contentStart >= 0 ? sentence.slice(contentStart) : (match[2] ?? match[0]);
|
|
358
|
-
|
|
374
|
+
const keywordStart = match.indices?.[1]?.[0] ?? -1;
|
|
375
|
+
const lead = pat === RULE_PATTERNS[0] ? ruleLead(sentence, keywordStart, contentStart) : null;
|
|
376
|
+
bounded = lead !== null
|
|
377
|
+
? boundToClause(lead + afterKeyword, lead.length, keywordStart + 200)
|
|
378
|
+
: boundToClause(keywordPrefix + afterKeyword, keywordPrefix.length);
|
|
359
379
|
}
|
|
360
380
|
const content = cleanExtract(bounded);
|
|
361
381
|
if (content.length >= 8 && content.length <= 500) {
|
|
@@ -410,7 +430,7 @@ export function extractFromText(text) {
|
|
|
410
430
|
const items = [];
|
|
411
431
|
const seen = new Set();
|
|
412
432
|
const addIfNew = (item) => {
|
|
413
|
-
const norm = item.content
|
|
433
|
+
const norm = duplicateKey(item.content);
|
|
414
434
|
if (seen.has(norm))
|
|
415
435
|
return;
|
|
416
436
|
if (!isContentWorthStoring(item.content))
|
|
@@ -449,26 +469,6 @@ export function extractFromText(text) {
|
|
|
449
469
|
}
|
|
450
470
|
return items;
|
|
451
471
|
}
|
|
452
|
-
// ---------------------------------------------------------------------------
|
|
453
|
-
// Normalisation for deduplication (mirrors import.ts)
|
|
454
|
-
// ---------------------------------------------------------------------------
|
|
455
|
-
function normalise(text) {
|
|
456
|
-
return text
|
|
457
|
-
.toLowerCase()
|
|
458
|
-
.replace(/[^\w\s]/g, '')
|
|
459
|
-
.replace(/\s+/g, ' ')
|
|
460
|
-
.trim();
|
|
461
|
-
}
|
|
462
|
-
function isDuplicate(content, existing) {
|
|
463
|
-
const norm = normalise(content);
|
|
464
|
-
if (!norm)
|
|
465
|
-
return true;
|
|
466
|
-
for (const e of existing) {
|
|
467
|
-
if (normalise(e.content) === norm)
|
|
468
|
-
return true;
|
|
469
|
-
}
|
|
470
|
-
return false;
|
|
471
|
-
}
|
|
472
472
|
/**
|
|
473
473
|
* Runtime shape guards used throughout this file wherever a value arrives
|
|
474
474
|
* unparsed (JSONL transcript records, stdout/stderr write() chunks). Generic
|
|
@@ -480,14 +480,14 @@ function isDuplicate(content, existing) {
|
|
|
480
480
|
* anything `JSON.parse` can produce (the only divergence is boxed
|
|
481
481
|
* primitives, which JSON.parse never yields).
|
|
482
482
|
*/
|
|
483
|
-
function isStringValue(value) {
|
|
483
|
+
export function isStringValue(value) {
|
|
484
484
|
return String(value) === value;
|
|
485
485
|
}
|
|
486
|
-
function isObjectLike(value) {
|
|
486
|
+
export function isObjectLike(value) {
|
|
487
487
|
return value !== null && value instanceof Object;
|
|
488
488
|
}
|
|
489
489
|
/** Message for a caught value of unknown shape. `cause` names the sanctioned unknown-input case (error-cause enrichment). */
|
|
490
|
-
function errorMessage(cause) {
|
|
490
|
+
export function errorMessage(cause) {
|
|
491
491
|
return cause instanceof Error ? cause.message : String(cause);
|
|
492
492
|
}
|
|
493
493
|
/**
|
|
@@ -506,10 +506,29 @@ function isNonHumanUserLine(entry, content) {
|
|
|
506
506
|
const head = content.trimStart();
|
|
507
507
|
return CLAUDE_CODE_COMMAND_PREFIXES.some((p) => head.startsWith(p));
|
|
508
508
|
}
|
|
509
|
-
|
|
509
|
+
const NON_HUMAN_BLOCK_PREFIXES = ['<ide_', '[Request interrupted by user'];
|
|
510
|
+
/** The human's words on a Claude Code user line, '' when none; VS Code stores prompts as text blocks, beside its own open-file, selection and interrupt blocks. */
|
|
511
|
+
function humanUserText(entry, message) {
|
|
512
|
+
const content = message.content;
|
|
513
|
+
let text = '';
|
|
514
|
+
if (isStringValue(content)) {
|
|
515
|
+
text = content;
|
|
516
|
+
}
|
|
517
|
+
else if (Array.isArray(content) && !content.some((b) => isObjectLike(b) && 'type' in b && b.type === 'tool_result')) {
|
|
518
|
+
const parts = [];
|
|
519
|
+
for (const block of content) {
|
|
520
|
+
const blockText = isObjectLike(block) && 'type' in block && block.type === 'text' && 'text' in block ? block.text : undefined;
|
|
521
|
+
if (isStringValue(blockText) && !NON_HUMAN_BLOCK_PREFIXES.some((p) => blockText.trimStart().startsWith(p)))
|
|
522
|
+
parts.push(blockText);
|
|
523
|
+
}
|
|
524
|
+
text = parts.join('\n');
|
|
525
|
+
}
|
|
526
|
+
text = text.trim();
|
|
527
|
+
return text && !isNonHumanUserLine(entry, text) ? text : '';
|
|
528
|
+
}
|
|
529
|
+
export function collectSessionTurns(jsonl, visit) {
|
|
510
530
|
const lines = jsonl.split('\n').filter((l) => l.trim());
|
|
511
|
-
const
|
|
512
|
-
const assistantTexts = [];
|
|
531
|
+
const turns = [];
|
|
513
532
|
for (const line of lines) {
|
|
514
533
|
let entry;
|
|
515
534
|
try {
|
|
@@ -518,19 +537,18 @@ export function summariseTranscript(jsonl) {
|
|
|
518
537
|
catch {
|
|
519
538
|
continue;
|
|
520
539
|
}
|
|
521
|
-
if (!isObjectLike(entry))
|
|
540
|
+
if (!isObjectLike(entry) || !('type' in entry))
|
|
522
541
|
continue;
|
|
523
|
-
|
|
542
|
+
visit?.(entry);
|
|
543
|
+
if (entry.type === 'user' || entry.type === 'assistant') {
|
|
524
544
|
const message = 'message' in entry && isObjectLike(entry.message) ? entry.message : undefined;
|
|
525
545
|
if (!message)
|
|
526
546
|
continue;
|
|
527
547
|
const content = 'content' in message ? message.content : undefined;
|
|
528
548
|
if (entry.type === 'user') {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
userMessages.push(content.trim());
|
|
533
|
-
}
|
|
549
|
+
const text = humanUserText(entry, message);
|
|
550
|
+
if (text)
|
|
551
|
+
turns.push({ role: 'user', text });
|
|
534
552
|
}
|
|
535
553
|
else if (Array.isArray(content)) {
|
|
536
554
|
// Keep assistant text blocks; drop thinking + tool_use
|
|
@@ -544,13 +562,13 @@ export function summariseTranscript(jsonl) {
|
|
|
544
562
|
}
|
|
545
563
|
}
|
|
546
564
|
if (chunks.length > 0) {
|
|
547
|
-
|
|
565
|
+
turns.push({ role: 'assistant', text: chunks.join('\n') });
|
|
548
566
|
}
|
|
549
567
|
}
|
|
550
568
|
continue;
|
|
551
569
|
}
|
|
552
570
|
// Codex rollout transcript shape: response_item -> payload.message
|
|
553
|
-
if (
|
|
571
|
+
if (entry.type === 'response_item') {
|
|
554
572
|
const payload = 'payload' in entry && isObjectLike(entry.payload) ? entry.payload : undefined;
|
|
555
573
|
if (!payload || !('type' in payload) || payload.type !== 'message')
|
|
556
574
|
continue;
|
|
@@ -574,11 +592,19 @@ export function summariseTranscript(jsonl) {
|
|
|
574
592
|
if (chunks.length === 0)
|
|
575
593
|
continue;
|
|
576
594
|
if (role === 'user')
|
|
577
|
-
|
|
595
|
+
turns.push({ role: 'user', text: chunks.join('\n') });
|
|
578
596
|
if (role === 'assistant')
|
|
579
|
-
|
|
597
|
+
turns.push({ role: 'assistant', text: chunks.join('\n') });
|
|
580
598
|
}
|
|
581
599
|
}
|
|
600
|
+
return turns;
|
|
601
|
+
}
|
|
602
|
+
export function summariseTranscript(jsonl) {
|
|
603
|
+
return summariseSessionTurns(collectSessionTurns(jsonl));
|
|
604
|
+
}
|
|
605
|
+
function summariseSessionTurns(turns) {
|
|
606
|
+
const userMessages = turns.filter((t) => t.role === 'user').map((t) => t.text);
|
|
607
|
+
const assistantTexts = turns.filter((t) => t.role === 'assistant').map((t) => t.text);
|
|
582
608
|
if (userMessages.length === 0 && assistantTexts.length === 0)
|
|
583
609
|
return '';
|
|
584
610
|
// Keep the tail: last ~20 user turns and last ~10 assistant replies.
|
|
@@ -760,13 +786,16 @@ function cmdCaptureCore(hippoRoot, options) {
|
|
|
760
786
|
break;
|
|
761
787
|
}
|
|
762
788
|
case 'last-session': {
|
|
763
|
-
|
|
764
|
-
if (!
|
|
765
|
-
|
|
766
|
-
|
|
789
|
+
let turns = options.sessionTurns;
|
|
790
|
+
if (!turns) {
|
|
791
|
+
const resolved = resolveLastSessionTranscript(options.transcriptPath, options.stdinText, options.stdinTimedOut);
|
|
792
|
+
if (!resolved) {
|
|
793
|
+
console.log('No transcript found. Pass --transcript <path> or run from a SessionEnd hook.');
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
turns = collectSessionTurns(fs.readFileSync(resolved, 'utf8'));
|
|
767
797
|
}
|
|
768
|
-
|
|
769
|
-
text = summariseTranscript(jsonl);
|
|
798
|
+
text = summariseSessionTurns(turns);
|
|
770
799
|
if (!text) {
|
|
771
800
|
console.log('Transcript had no user/assistant messages to summarise.');
|
|
772
801
|
return;
|
|
@@ -778,16 +807,19 @@ function cmdCaptureCore(hippoRoot, options) {
|
|
|
778
807
|
console.log('No text to capture from.');
|
|
779
808
|
return;
|
|
780
809
|
}
|
|
781
|
-
//
|
|
782
|
-
const extracted = extractFromText(text);
|
|
810
|
+
// Scrub once here, as the snapshot fields are: every source can carry a pasted token (AGENTS.md: no secrets in memories).
|
|
811
|
+
const extracted = extractFromText(maskEmails(redactSecretsStrict(text)));
|
|
783
812
|
if (extracted.length === 0) {
|
|
784
813
|
console.log('No actionable items found in the input.');
|
|
785
814
|
return;
|
|
786
815
|
}
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
|
|
790
|
-
const
|
|
816
|
+
// Dedup only against rows this capture's reader sees: another tenant's rows (L9), or another
|
|
817
|
+
// project's, are hidden from it, so they must not stop its own copy.
|
|
818
|
+
const stored = loadAllEntries(targetRoot, useGlobal ? undefined : options.tenantId);
|
|
819
|
+
const origin = options.originProject;
|
|
820
|
+
const keys = storedTextKeys(origin === undefined
|
|
821
|
+
? stored
|
|
822
|
+
: stored.filter((e) => classifyOriginProject(e.origin_project, origin) !== 'cross-project'));
|
|
791
823
|
let captured = 0;
|
|
792
824
|
let skipped = 0;
|
|
793
825
|
let rejected = 0;
|
|
@@ -802,7 +834,7 @@ function cmdCaptureCore(hippoRoot, options) {
|
|
|
802
834
|
const dryRunDb = options.dryRun ? openHippoDb(targetRoot) : null;
|
|
803
835
|
try {
|
|
804
836
|
for (const item of extracted) {
|
|
805
|
-
if (
|
|
837
|
+
if (keys.has(duplicateKey(item.content))) {
|
|
806
838
|
skipped++;
|
|
807
839
|
if (options.dryRun) {
|
|
808
840
|
console.log(` [skip] (${item.category}) ${item.content.slice(0, 80)}`);
|
|
@@ -819,7 +851,7 @@ function cmdCaptureCore(hippoRoot, options) {
|
|
|
819
851
|
// global: true, the global store is host-wide and tenant is irrelevant
|
|
820
852
|
// (createMemory's default 'default' applies). When global: false,
|
|
821
853
|
// options.tenantId scopes the write to the same tenant as the dedup.
|
|
822
|
-
const
|
|
854
|
+
const created = createMemory(item.content, {
|
|
823
855
|
layer: Layer.Episodic,
|
|
824
856
|
tags: item.tags,
|
|
825
857
|
source: 'capture',
|
|
@@ -827,6 +859,7 @@ function cmdCaptureCore(hippoRoot, options) {
|
|
|
827
859
|
tenantId: useGlobal ? undefined : options.tenantId,
|
|
828
860
|
baseHalfLifeDays,
|
|
829
861
|
});
|
|
862
|
+
const entry = options.originProject === undefined ? created : { ...created, origin_project: options.originProject };
|
|
830
863
|
if (options.dryRun) {
|
|
831
864
|
if (dryRunDb) {
|
|
832
865
|
try {
|
|
@@ -857,10 +890,8 @@ function cmdCaptureCore(hippoRoot, options) {
|
|
|
857
890
|
throw err;
|
|
858
891
|
}
|
|
859
892
|
updateStats(targetRoot, { remembered: 1 });
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
embedMemory(targetRoot, entry).catch(() => { });
|
|
863
|
-
}
|
|
893
|
+
keys.add(duplicateKey(item.content)); // within-batch dedup
|
|
894
|
+
void embedMemory(targetRoot, entry);
|
|
864
895
|
}
|
|
865
896
|
captured++;
|
|
866
897
|
}
|
|
@@ -987,9 +1018,9 @@ function lastPlainUserMessage(jsonl) {
|
|
|
987
1018
|
const message = 'message' in entry && isObjectLike(entry.message) ? entry.message : undefined;
|
|
988
1019
|
if (!message)
|
|
989
1020
|
continue;
|
|
990
|
-
const
|
|
991
|
-
if (
|
|
992
|
-
return
|
|
1021
|
+
const text = humanUserText(entry, message);
|
|
1022
|
+
if (text)
|
|
1023
|
+
return text;
|
|
993
1024
|
}
|
|
994
1025
|
return '';
|
|
995
1026
|
}
|
|
@@ -1080,6 +1111,45 @@ export function preCompactMessage(report) {
|
|
|
1080
1111
|
export function preCompactReportPath(logFile) {
|
|
1081
1112
|
return path.join(path.dirname(logFile), 'pre-compact-last.json');
|
|
1082
1113
|
}
|
|
1114
|
+
/** A session's task, summary and next step from its transcript tail, secrets scrubbed and capped, '' where none; null with a logged reason when nothing is derivable. */
|
|
1115
|
+
export function transcriptWorkingState(transcriptPath, log) {
|
|
1116
|
+
let tail = '';
|
|
1117
|
+
let rawTask = '';
|
|
1118
|
+
let rawNextStep = '';
|
|
1119
|
+
try {
|
|
1120
|
+
// Compaction fires when a big tool_result lands (CX7), so the last human and assistant turns can sit
|
|
1121
|
+
// megabytes back: grow the window a bounded number of times until both turns are in it.
|
|
1122
|
+
const size = fs.statSync(transcriptPath).size;
|
|
1123
|
+
for (const cap of [PRE_COMPACT_TAIL_BYTES, PRE_COMPACT_TAIL_BYTES * 4, PRE_COMPACT_TAIL_BYTES * 16]) {
|
|
1124
|
+
if (cap > PRE_COMPACT_TAIL_BYTES)
|
|
1125
|
+
log(`tail window grown to ${cap} bytes (last ${rawTask ? 'assistant' : 'user'} turn is further back)`);
|
|
1126
|
+
tail = readTranscriptTail(transcriptPath, cap);
|
|
1127
|
+
rawTask = lastPlainUserMessage(tail);
|
|
1128
|
+
rawNextStep = lastAssistantTextBlock(tail);
|
|
1129
|
+
if ((rawTask && rawNextStep) || size <= cap)
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
catch (err) {
|
|
1134
|
+
log(`skip: could not read transcript tail: ${errorMessage(err)}`);
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
const rawSummary = summariseTranscript(tail);
|
|
1138
|
+
if (!rawTask.trim() && !rawSummary.trim() && !rawNextStep.trim()) {
|
|
1139
|
+
log('skip: empty summary');
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
// X9: these fields skip the capture content gate and reach a prompt, so the strict scrub runs. The caps protect the
|
|
1143
|
+
// re-injection token budget and never split a surrogate pair (X2); `hippo snapshot save` stays uncapped.
|
|
1144
|
+
const task = maskEmails(redactSecretsStrict(rawTask));
|
|
1145
|
+
const summary = maskEmails(redactSecretsStrict(rawSummary));
|
|
1146
|
+
const nextStep = maskEmails(redactSecretsStrict(rawNextStep));
|
|
1147
|
+
return {
|
|
1148
|
+
task: task.trim() ? truncateCodePointSafe(task, PRE_COMPACT_TASK_CAP) : '',
|
|
1149
|
+
summary: summary.trim() ? truncateKeepNewest(summary, PRE_COMPACT_SUMMARY_CAP) : '',
|
|
1150
|
+
next_step: nextStep.trim() ? truncateCodePointSafe(nextStep, PRE_COMPACT_NEXT_STEP_CAP) : '',
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1083
1153
|
/** Runs the PreCompact producer: saves a working-state snapshot into `report`. Never extracts memories; SessionEnd capture owns that. */
|
|
1084
1154
|
function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
1085
1155
|
// X3: the PreCompact hook fires in every Claude Code project, including
|
|
@@ -1160,40 +1230,10 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1160
1230
|
appendPreCompactLog(logFile, 'skip: no transcript resolved');
|
|
1161
1231
|
return;
|
|
1162
1232
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
// swallows the whole tail — the seek lands inside it and alignment
|
|
1167
|
-
// drops everything up to its terminator, which is exactly the shape of
|
|
1168
|
-
// a huge tool_result that itself triggered compaction. Grow the window
|
|
1169
|
-
// a bounded number of times until at least one complete line survives.
|
|
1170
|
-
tail = readTranscriptTail(transcriptPath, PRE_COMPACT_TAIL_BYTES);
|
|
1171
|
-
let prevCap = PRE_COMPACT_TAIL_BYTES;
|
|
1172
|
-
for (const grownCap of [PRE_COMPACT_TAIL_BYTES * 4, PRE_COMPACT_TAIL_BYTES * 16]) {
|
|
1173
|
-
if (tail.trim() !== '')
|
|
1174
|
-
break;
|
|
1175
|
-
if (fs.statSync(transcriptPath).size <= prevCap)
|
|
1176
|
-
break; // already read the whole file
|
|
1177
|
-
appendPreCompactLog(logFile, `tail window grown to ${grownCap} bytes (oversized final record)`);
|
|
1178
|
-
tail = readTranscriptTail(transcriptPath, grownCap);
|
|
1179
|
-
prevCap = grownCap;
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
catch (err) {
|
|
1183
|
-
appendPreCompactLog(logFile, `skip: could not read transcript tail: ${errorMessage(err)}`);
|
|
1233
|
+
// Nothing derivable skips the write, so a user-authored active snapshot is never clobbered with junk.
|
|
1234
|
+
const derived = transcriptWorkingState(transcriptPath, (message) => appendPreCompactLog(logFile, message));
|
|
1235
|
+
if (!derived)
|
|
1184
1236
|
return;
|
|
1185
|
-
}
|
|
1186
|
-
const summaryFull = summariseTranscript(tail);
|
|
1187
|
-
// Snapshot fields must never carry raw secrets, same rule X9 applies below to task/next_step.
|
|
1188
|
-
const scrubbedSummary = redactSecrets(summaryFull);
|
|
1189
|
-
const rawTask = lastPlainUserMessage(tail);
|
|
1190
|
-
const rawNextStep = lastAssistantTextBlock(tail);
|
|
1191
|
-
// Full skip only when every derived field is empty: never clobber a
|
|
1192
|
-
// user-authored active snapshot with junk.
|
|
1193
|
-
if (!rawTask.trim() && !summaryFull.trim() && !rawNextStep.trim()) {
|
|
1194
|
-
appendPreCompactLog(logFile, 'skip: empty summary');
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
1237
|
const tenantId = resolveTenantId({});
|
|
1198
1238
|
// Per-field merge (X1): a tool-heavy tail whose only user turns are
|
|
1199
1239
|
// tool_result arrays derives an empty task even though the summary is
|
|
@@ -1207,15 +1247,6 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1207
1247
|
catch {
|
|
1208
1248
|
// No existing snapshot to merge against — proceed with derived-only.
|
|
1209
1249
|
}
|
|
1210
|
-
// X9: scrub secret-shaped substrings out of freshly-derived text before it
|
|
1211
|
-
// is capped/stored. These fields bypass the normal capture content gate
|
|
1212
|
-
// (they're not extracted items), so this producer is the only place that
|
|
1213
|
-
// ever sees them before they land in task_snapshots. Carried-over
|
|
1214
|
-
// existing field values are NOT re-scrubbed here — they already passed
|
|
1215
|
-
// through this same gate (or were set via `hippo snapshot save`, which is
|
|
1216
|
-
// deliberately untouched, same as the caps below).
|
|
1217
|
-
const scrubbedTask = redactSecrets(rawTask);
|
|
1218
|
-
const scrubbedNextStep = redactSecrets(rawNextStep);
|
|
1219
1250
|
// CX6 (codex round 2): field fallback must never move content across
|
|
1220
1251
|
// sessions — session A's task carried into a snapshot saved under session
|
|
1221
1252
|
// B's id would pass compact-resume's session gate wearing the wrong
|
|
@@ -1225,19 +1256,10 @@ function runPreCompact(hippoRoot, stdinText, stdinTimedOut, logFile, report) {
|
|
|
1225
1256
|
(existing.session_id === null || sessionId === null || existing.session_id === sessionId)
|
|
1226
1257
|
? existing
|
|
1227
1258
|
: null;
|
|
1228
|
-
//
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const task = scrubbedTask.trim()
|
|
1233
|
-
? truncateCodePointSafe(scrubbedTask, PRE_COMPACT_TASK_CAP)
|
|
1234
|
-
: (fallback?.task ?? '');
|
|
1235
|
-
const summary = scrubbedSummary.trim()
|
|
1236
|
-
? truncateKeepNewest(scrubbedSummary, PRE_COMPACT_SUMMARY_CAP)
|
|
1237
|
-
: (fallback?.summary ?? '');
|
|
1238
|
-
const nextStep = scrubbedNextStep.trim()
|
|
1239
|
-
? truncateCodePointSafe(scrubbedNextStep, PRE_COMPACT_NEXT_STEP_CAP)
|
|
1240
|
-
: (fallback?.next_step ?? '');
|
|
1259
|
+
// Carried-over fields are not re-capped, as `hippo snapshot save` stays uncapped; saveActiveTaskSnapshot scrubs every field.
|
|
1260
|
+
const task = derived.task || (fallback?.task ?? '');
|
|
1261
|
+
const summary = derived.summary || (fallback?.summary ?? '');
|
|
1262
|
+
const nextStep = derived.next_step || (fallback?.next_step ?? '');
|
|
1241
1263
|
// All-empty fields (cross-session tail with nothing derivable) skip the
|
|
1242
1264
|
// write so a foreign session's junk never displaces the owning snapshot.
|
|
1243
1265
|
if (!task && !summary && !nextStep) {
|
package/dist/churn-git.js
CHANGED
|
@@ -10,7 +10,7 @@ export class GitReadError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
function runGit(args, repoRoot) {
|
|
12
12
|
try {
|
|
13
|
-
return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER });
|
|
13
|
+
return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
14
14
|
}
|
|
15
15
|
catch (err) {
|
|
16
16
|
throw new GitReadError(`git ${args.join(' ')} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -19,7 +19,7 @@ function runGit(args, repoRoot) {
|
|
|
19
19
|
// git grep exits 1 for "no match" -- a normal empty result, not a failure.
|
|
20
20
|
function runGitGrepOrEmpty(args, repoRoot) {
|
|
21
21
|
try {
|
|
22
|
-
return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER });
|
|
22
|
+
return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
23
23
|
}
|
|
24
24
|
catch (err) {
|
|
25
25
|
// SAFETY: execFileSync attaches `status` to the thrown Error on a non-zero child exit.
|
package/dist/cli.d.ts
CHANGED
|
@@ -43,10 +43,7 @@ export declare function parseArgs(argv: string[]): {
|
|
|
43
43
|
args: string[];
|
|
44
44
|
flags: Record<string, string | boolean | string[]>;
|
|
45
45
|
};
|
|
46
|
-
/**
|
|
47
|
-
* Scan for Claude Code MEMORY.md files and import new entries into hippo.
|
|
48
|
-
* Looks in ~/.claude/projects/<project>/memory/ for .md files with YAML frontmatter.
|
|
49
|
-
*/
|
|
46
|
+
/** Import new entries from the Claude Code auto memory of the store's project, the frontmatter .md files in ~/.claude/projects/<project>/memory/. */
|
|
50
47
|
export declare function learnFromMemoryMd(hippoRoot: string, homeDir?: string): number;
|
|
51
48
|
/**
|
|
52
49
|
* Render an api.sleep result as console output, byte-identical to the
|