hippo-memory 1.46.0 → 1.48.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 +4 -2
- package/bin/hippo.js +0 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +20 -7
- package/dist/capture-error.d.ts +18 -12
- package/dist/capture-error.js +70 -42
- package/dist/cli.js +91 -2
- package/dist/db.d.ts +7 -0
- package/dist/db.js +62 -2
- package/dist/doctor.js +52 -32
- package/dist/failure-log.d.ts +49 -0
- package/dist/failure-log.js +58 -0
- package/dist/reject-flow.d.ts +2 -2
- package/dist/reject-flow.js +4 -7
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +18 -6
- package/dist/store.d.ts +9 -26
- package/dist/store.js +18 -36
- package/dist/support-bundle.d.ts +12 -0
- package/dist/support-bundle.js +292 -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/doctor.js
CHANGED
|
@@ -9,8 +9,8 @@ import * as os from 'node:os';
|
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { findHippoStoreDir } from './project-identity.js';
|
|
11
11
|
import { getGlobalRoot } from './shared.js';
|
|
12
|
-
import { isInitialized
|
|
13
|
-
import {
|
|
12
|
+
import { isInitialized } from './store.js';
|
|
13
|
+
import { openHippoDbReadOnly, closeHippoDb, getSchemaVersion, getCurrentSchemaVersion, countTableRows, IncompatibleBinaryError } from './db.js';
|
|
14
14
|
import { isEmbeddingAvailable } from './embeddings.js';
|
|
15
15
|
/** Minimum Node.js version hippo supports (package.json engines). */
|
|
16
16
|
export const MIN_NODE = '22.16.0';
|
|
@@ -34,14 +34,42 @@ function readJson(file) {
|
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
// Migration 46 creates failure_log; a read-only open no longer creates it on an older store.
|
|
38
|
+
const FAILURE_LOG_SCHEMA = 46;
|
|
39
|
+
/** The failed-tool-call count over the last 7 days, or why it could not be read. */
|
|
40
|
+
function failuresCheck(db, since, schemaVersion) {
|
|
38
41
|
try {
|
|
39
|
-
// SAFETY: COUNT
|
|
40
|
-
const row = db.prepare(`SELECT COUNT(*) AS n FROM
|
|
41
|
-
return Number(row?.n ?? 0);
|
|
42
|
+
// SAFETY: COUNT aggregate row.
|
|
43
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM failure_log WHERE ts >= ?`).get(since);
|
|
44
|
+
return { id: 'failures', status: 'info', detail: `${Number(row?.n ?? 0)} failed tool calls logged in 7 days (hippo failures for detail)` };
|
|
42
45
|
}
|
|
43
|
-
catch {
|
|
44
|
-
|
|
46
|
+
catch (err) {
|
|
47
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
48
|
+
if (!message.includes('no such table')) {
|
|
49
|
+
return { id: 'failures', status: 'warn', detail: `cannot read the failure log: ${message}` };
|
|
50
|
+
}
|
|
51
|
+
return schemaVersion < FAILURE_LOG_SCHEMA
|
|
52
|
+
? { id: 'failures', status: 'info', detail: 'no failure log yet (hippo creates it on the next write)' }
|
|
53
|
+
: { id: 'failures', status: 'warn', detail: 'the failure_log table is missing, so failed tool calls are not being logged' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** How long ago the store last slept (consolidated), or why that history could not be read. */
|
|
57
|
+
function sleepCheck(db, now) {
|
|
58
|
+
try {
|
|
59
|
+
// SAFETY: row's shape matches the single `timestamp` column named in the SELECT above.
|
|
60
|
+
const row = db.prepare(`SELECT timestamp FROM consolidation_runs ORDER BY timestamp DESC, id DESC LIMIT 1`).get();
|
|
61
|
+
const when = row?.timestamp !== undefined ? Date.parse(row.timestamp) : Number.NaN;
|
|
62
|
+
if (Number.isNaN(when)) {
|
|
63
|
+
return { id: 'sleep', status: 'warn', detail: 'hippo has never slept (consolidated) in this store', fix: 'hippo sleep (the session-end hook runs it automatically)' };
|
|
64
|
+
}
|
|
65
|
+
const days = Math.floor((now.getTime() - when) / 86_400_000);
|
|
66
|
+
return days > 7
|
|
67
|
+
? { id: 'sleep', status: 'warn', detail: `last sleep ${days} days ago`, fix: 'hippo sleep, and check the session-end hook is installed' }
|
|
68
|
+
: { id: 'sleep', status: 'pass', detail: `last sleep ${days === 0 ? 'today' : `${days} day${days === 1 ? '' : 's'} ago`}` };
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
return { id: 'sleep', status: 'info', detail: `sleep history unavailable (${message})` };
|
|
45
73
|
}
|
|
46
74
|
}
|
|
47
75
|
/** Run every check. Never throws for a broken install; broken parts become failed checks. */
|
|
@@ -58,10 +86,14 @@ export function runDoctor(opts) {
|
|
|
58
86
|
const globalRoot = getGlobalRoot();
|
|
59
87
|
const hasGlobal = isInitialized(globalRoot);
|
|
60
88
|
let store = null;
|
|
61
|
-
if (local !== null) {
|
|
89
|
+
if (local !== null && isInitialized(local)) {
|
|
62
90
|
store = local;
|
|
63
91
|
checks.push({ id: 'store', status: 'pass', detail: `project store at ${local}${hasGlobal ? ` (global store at ${globalRoot} too)` : ''}` });
|
|
64
92
|
}
|
|
93
|
+
else if (local !== null) {
|
|
94
|
+
// The walk stops at the first .hippo it finds, so a bare one (no hippo.db) blocks a parent or global store too.
|
|
95
|
+
checks.push({ id: 'store', status: 'fail', detail: `${local} has no hippo.db, so hippo commands run here stop at it`, fix: `run hippo init in ${path.dirname(local)}, or remove that .hippo folder` });
|
|
96
|
+
}
|
|
65
97
|
else if (hasGlobal) {
|
|
66
98
|
store = globalRoot;
|
|
67
99
|
checks.push({ id: 'store', status: 'warn', detail: `no project store here; using the global store at ${globalRoot}`, fix: 'hippo init (in the project root)' });
|
|
@@ -72,7 +104,7 @@ export function runDoctor(opts) {
|
|
|
72
104
|
if (store !== null) {
|
|
73
105
|
let db = null;
|
|
74
106
|
try {
|
|
75
|
-
db =
|
|
107
|
+
db = openHippoDbReadOnly(store);
|
|
76
108
|
const have = getSchemaVersion(db);
|
|
77
109
|
const want = getCurrentSchemaVersion();
|
|
78
110
|
checks.push(have === want
|
|
@@ -80,8 +112,8 @@ export function runDoctor(opts) {
|
|
|
80
112
|
: have > want
|
|
81
113
|
? { id: 'schema', status: 'fail', detail: `database schema v${have} is newer than this hippo (v${want})`, fix: 'npm install -g hippo-memory@latest' }
|
|
82
114
|
: { id: 'schema', status: 'info', detail: `database schema v${have}; hippo migrates it to v${want} on the next write` });
|
|
83
|
-
const memories =
|
|
84
|
-
const dormant =
|
|
115
|
+
const memories = countTableRows(db, 'memories');
|
|
116
|
+
const dormant = countTableRows(db, 'dormant_memories');
|
|
85
117
|
const memoryCheck = { id: 'memories', status: 'info', detail: `${memories ?? '?'} memories${dormant !== null ? `, ${dormant} dormant` : ''}` };
|
|
86
118
|
if (memories === 0) {
|
|
87
119
|
memoryCheck.status = 'warn';
|
|
@@ -97,33 +129,21 @@ export function runDoctor(opts) {
|
|
|
97
129
|
catch {
|
|
98
130
|
checks.push({ id: 'tokens', status: 'info', detail: 'no token ledger yet (created on the next write)' });
|
|
99
131
|
}
|
|
132
|
+
checks.push(failuresCheck(db, since, have));
|
|
133
|
+
checks.push(sleepCheck(db, now));
|
|
100
134
|
}
|
|
101
135
|
catch (err) {
|
|
102
|
-
checks.push({
|
|
136
|
+
checks.push({
|
|
137
|
+
id: 'schema',
|
|
138
|
+
status: 'fail',
|
|
139
|
+
detail: `cannot open the database: ${err instanceof Error ? err.message : String(err)}`,
|
|
140
|
+
fix: err instanceof IncompatibleBinaryError ? 'npm install -g hippo-memory@latest' : 'check file permissions on the .hippo folder',
|
|
141
|
+
});
|
|
103
142
|
}
|
|
104
143
|
finally {
|
|
105
144
|
if (db !== null)
|
|
106
145
|
closeHippoDb(db);
|
|
107
146
|
}
|
|
108
|
-
try {
|
|
109
|
-
const runs = loadStats(store)['consolidation_runs'];
|
|
110
|
-
const last = Array.isArray(runs) && runs.length > 0 ? runs[runs.length - 1] : null;
|
|
111
|
-
// SAFETY: the constructor check above narrows `last` to a plain JSON object.
|
|
112
|
-
const ts = last !== null && last !== undefined && !Array.isArray(last) && last.constructor === Object ? last.timestamp : undefined;
|
|
113
|
-
const when = ts !== undefined && ts !== null ? Date.parse(String(ts)) : Number.NaN;
|
|
114
|
-
if (Number.isNaN(when)) {
|
|
115
|
-
checks.push({ id: 'sleep', status: 'warn', detail: 'hippo has never slept (consolidated) in this store', fix: 'hippo sleep (the session-end hook runs it automatically)' });
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
const days = Math.floor((now.getTime() - when) / 86_400_000);
|
|
119
|
-
checks.push(days > 7
|
|
120
|
-
? { id: 'sleep', status: 'warn', detail: `last sleep ${days} days ago`, fix: 'hippo sleep, and check the session-end hook is installed' }
|
|
121
|
-
: { id: 'sleep', status: 'pass', detail: `last sleep ${days === 0 ? 'today' : `${days} day${days === 1 ? '' : 's'} ago`}` });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
checks.push({ id: 'sleep', status: 'info', detail: 'sleep history unavailable' });
|
|
126
|
-
}
|
|
127
147
|
}
|
|
128
148
|
const claudeDir = path.join(home, '.claude');
|
|
129
149
|
if (fs.existsSync(claudeDir)) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Failure log (ROADMAP CD13): every failed tool call the capture-error hook sees, stored or not. */
|
|
2
|
+
import type { CaptureErrorOutcome, RoutineRule } from './capture-error.js';
|
|
3
|
+
import type { DatabaseSyncLike } from './db.js';
|
|
4
|
+
/** Rows older than this are pruned on write, which also bounds how far back a repeat can be found. */
|
|
5
|
+
export declare const FAILURE_LOG_RETENTION_DAYS = 90;
|
|
6
|
+
/** A capture-error outcome, or `store-failed` when storing the lesson threw. */
|
|
7
|
+
export type FailureOutcome = CaptureErrorOutcome | 'store-failed';
|
|
8
|
+
/** One failed tool call, for {@link recordFailure}. Never the failure text: it can carry paths and secrets. */
|
|
9
|
+
export interface FailureEvent {
|
|
10
|
+
tenantId: string;
|
|
11
|
+
/** Host session id from the hook payload; null when it had none. */
|
|
12
|
+
sessionId?: string | null;
|
|
13
|
+
tool?: string | null;
|
|
14
|
+
outcome: FailureOutcome;
|
|
15
|
+
/** The routine check that skipped it, for `skipped-routine`. */
|
|
16
|
+
rule?: RoutineRule | null;
|
|
17
|
+
/** Hash of the lesson text's signature, the key dedupe uses; null when the payload had no readable error. */
|
|
18
|
+
sigHash?: string | null;
|
|
19
|
+
/** Hash of the untruncated error plus the command's first two words, finer than `sigHash`. */
|
|
20
|
+
detailHash?: string | null;
|
|
21
|
+
/** Override the timestamp (tests). ISO string. */
|
|
22
|
+
now?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Append one failure row and prune rows past {@link FAILURE_LOG_RETENTION_DAYS}. */
|
|
25
|
+
export declare function recordFailure(db: DatabaseSyncLike, event: FailureEvent): void;
|
|
26
|
+
/** Rated failures and repeats in one session, for {@link failuresBySession}. */
|
|
27
|
+
export interface SessionFailures {
|
|
28
|
+
sessionId: string;
|
|
29
|
+
/** Failures hippo treats as lessons (stored, duplicate or store-failed) in the window. */
|
|
30
|
+
failures: number;
|
|
31
|
+
/** Of those, failures whose signature another session hit first. */
|
|
32
|
+
repeats: number;
|
|
33
|
+
}
|
|
34
|
+
/** Rated failures per session since `sinceIso`, the input for repeat-error rate per arm (CD11, CD12). */
|
|
35
|
+
export declare function failuresBySession(db: DatabaseSyncLike, tenantId: string, sinceIso: string): SessionFailures[];
|
|
36
|
+
/** Failure log totals over a window, for {@link summarizeFailures}. Counts only: a rate needs a holdout arm (CD11). */
|
|
37
|
+
export interface FailureSummary {
|
|
38
|
+
/** ISO start of the window (inclusive). */
|
|
39
|
+
since: string;
|
|
40
|
+
outcomes: Record<FailureOutcome, number>;
|
|
41
|
+
total: number;
|
|
42
|
+
/** Rated failures from sessions with an id: the failures a repeat is counted among. */
|
|
43
|
+
rated: number;
|
|
44
|
+
repeats: number;
|
|
45
|
+
sessions: number;
|
|
46
|
+
}
|
|
47
|
+
/** Sum the failure log for one tenant since `sinceIso`. */
|
|
48
|
+
export declare function summarizeFailures(db: DatabaseSyncLike, tenantId: string, sinceIso: string): FailureSummary;
|
|
49
|
+
//# sourceMappingURL=failure-log.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Rows older than this are pruned on write, which also bounds how far back a repeat can be found. */
|
|
2
|
+
export const FAILURE_LOG_RETENTION_DAYS = 90;
|
|
3
|
+
/** Longest session id or tool name kept; the hook payload is not trusted to be short. */
|
|
4
|
+
const MAX_FIELD = 128;
|
|
5
|
+
/** Append one failure row and prune rows past {@link FAILURE_LOG_RETENTION_DAYS}. */
|
|
6
|
+
export function recordFailure(db, event) {
|
|
7
|
+
// Normalised, because the window and prune compare timestamps as strings.
|
|
8
|
+
const now = new Date(event.now ?? Date.now()).toISOString();
|
|
9
|
+
db.prepare(`INSERT INTO failure_log (ts, tenant_id, session_id, tool, outcome, skip_rule, sig_hash, detail_hash)
|
|
10
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(now, event.tenantId, event.sessionId?.slice(0, MAX_FIELD) ?? null, event.tool?.slice(0, MAX_FIELD) ?? null, event.outcome, event.rule ?? null, event.sigHash ?? null, event.detailHash ?? null);
|
|
11
|
+
const cutoff = new Date(Date.parse(now) - FAILURE_LOG_RETENTION_DAYS * 86_400_000).toISOString();
|
|
12
|
+
db.prepare(`DELETE FROM failure_log WHERE ts < ?`).run(cutoff);
|
|
13
|
+
}
|
|
14
|
+
/** Rated failures per session since `sinceIso`, the input for repeat-error rate per arm (CD11, CD12). */
|
|
15
|
+
export function failuresBySession(db, tenantId, sinceIso) {
|
|
16
|
+
// SAFETY: the SELECT names exactly these three TEXT columns.
|
|
17
|
+
const rows = db.prepare(`SELECT ts, session_id, sig_hash FROM failure_log
|
|
18
|
+
WHERE tenant_id = ? AND session_id IS NOT NULL AND sig_hash IS NOT NULL
|
|
19
|
+
AND outcome IN ('stored', 'duplicate', 'store-failed')
|
|
20
|
+
ORDER BY id`).all(tenantId);
|
|
21
|
+
const firstSession = new Map();
|
|
22
|
+
const bySession = new Map();
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
if (!firstSession.has(row.sig_hash))
|
|
25
|
+
firstSession.set(row.sig_hash, row.session_id);
|
|
26
|
+
// Rows before the window are not counted but still decide which session hit a signature first.
|
|
27
|
+
if (row.ts < sinceIso)
|
|
28
|
+
continue;
|
|
29
|
+
const repeat = firstSession.get(row.sig_hash) !== row.session_id;
|
|
30
|
+
const s = bySession.get(row.session_id) ?? { sessionId: row.session_id, failures: 0, repeats: 0 };
|
|
31
|
+
bySession.set(row.session_id, { ...s, failures: s.failures + 1, repeats: s.repeats + (repeat ? 1 : 0) });
|
|
32
|
+
}
|
|
33
|
+
return [...bySession.values()];
|
|
34
|
+
}
|
|
35
|
+
/** Sum the failure log for one tenant since `sinceIso`. */
|
|
36
|
+
export function summarizeFailures(db, tenantId, sinceIso) {
|
|
37
|
+
// SAFETY: the SELECT names exactly these two columns, TEXT and an aggregate.
|
|
38
|
+
const rows = db.prepare(`SELECT outcome, COUNT(*) AS n FROM failure_log WHERE tenant_id = ? AND ts >= ? GROUP BY outcome`).all(tenantId, sinceIso);
|
|
39
|
+
const counts = new Map(rows.map((r) => [r.outcome, Number(r.n)]));
|
|
40
|
+
const outcomes = {
|
|
41
|
+
stored: counts.get('stored') ?? 0,
|
|
42
|
+
duplicate: counts.get('duplicate') ?? 0,
|
|
43
|
+
'store-failed': counts.get('store-failed') ?? 0,
|
|
44
|
+
'skipped-interrupt': counts.get('skipped-interrupt') ?? 0,
|
|
45
|
+
'skipped-routine': counts.get('skipped-routine') ?? 0,
|
|
46
|
+
'skipped-invalid': counts.get('skipped-invalid') ?? 0,
|
|
47
|
+
};
|
|
48
|
+
const sessions = failuresBySession(db, tenantId, sinceIso);
|
|
49
|
+
return {
|
|
50
|
+
since: sinceIso,
|
|
51
|
+
outcomes,
|
|
52
|
+
total: Object.values(outcomes).reduce((sum, n) => sum + n, 0),
|
|
53
|
+
rated: sessions.reduce((sum, s) => sum + s.failures, 0),
|
|
54
|
+
repeats: sessions.reduce((sum, s) => sum + s.repeats, 0),
|
|
55
|
+
sessions: sessions.length,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=failure-log.js.map
|
package/dist/reject-flow.d.ts
CHANGED
|
@@ -41,8 +41,8 @@ export interface RejectFlowResult {
|
|
|
41
41
|
* normalized digest matches (kind-aware), one aggregate `reject_value`
|
|
42
42
|
* audit, COMMIT. Then post-commit (mirrors the existing purge+reaper
|
|
43
43
|
* pattern verbatim from api.archiveRaw, api.ts:1913-1938): best-effort
|
|
44
|
-
* mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids
|
|
45
|
-
* index
|
|
44
|
+
* mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids.
|
|
45
|
+
* index.json itself is only refreshed by `rebuildIndex()`.
|
|
46
46
|
*/
|
|
47
47
|
export declare function rejectValue(opts: RejectFlowOpts): RejectFlowResult;
|
|
48
48
|
export type UnrejectOutcome = {
|
package/dist/reject-flow.js
CHANGED
|
@@ -15,7 +15,7 @@ import { openHippoDb, closeHippoDb } from './db.js';
|
|
|
15
15
|
import { appendAuditEvent } from './audit.js';
|
|
16
16
|
import { archiveRawMemory } from './raw-archive.js';
|
|
17
17
|
import { purgeDormantByDigest } from './dormant.js';
|
|
18
|
-
import { initStore, deleteEntryCore, purgeMirrorBestEffort,
|
|
18
|
+
import { initStore, deleteEntryCore, purgeMirrorBestEffort, } from './store.js';
|
|
19
19
|
import { rejectionDigest, normalizeValueForRejection, insertRejectedValue, deleteRejectedValue, listRejectedValues, } from './rejection.js';
|
|
20
20
|
/**
|
|
21
21
|
* `hippo reject` / `api.reject` core flow. ONE connection, one transaction:
|
|
@@ -23,8 +23,8 @@ import { rejectionDigest, normalizeValueForRejection, insertRejectedValue, delet
|
|
|
23
23
|
* normalized digest matches (kind-aware), one aggregate `reject_value`
|
|
24
24
|
* audit, COMMIT. Then post-commit (mirrors the existing purge+reaper
|
|
25
25
|
* pattern verbatim from api.archiveRaw, api.ts:1913-1938): best-effort
|
|
26
|
-
* mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids
|
|
27
|
-
* index
|
|
26
|
+
* mirror purge per removed id, `mirror_cleaned_at` stamps for raw ids.
|
|
27
|
+
* index.json itself is only refreshed by `rebuildIndex()`.
|
|
28
28
|
*/
|
|
29
29
|
export function rejectValue(opts) {
|
|
30
30
|
if (!opts.reason.trim()) {
|
|
@@ -138,7 +138,7 @@ export function rejectValue(opts) {
|
|
|
138
138
|
}
|
|
139
139
|
// Post-commit, db handle still open (same pattern as api.archiveRaw):
|
|
140
140
|
// best-effort mirror purge per removed id, reaper-backstop stamp for
|
|
141
|
-
// raw ids
|
|
141
|
+
// raw ids.
|
|
142
142
|
for (const id of removedIds) {
|
|
143
143
|
// AT1 fix: purgeMirrorBestEffort retries once, then — for non-raw ids,
|
|
144
144
|
// which cleanupArchivedMirrors' reaper never scans — reports the
|
|
@@ -150,9 +150,6 @@ export function rejectValue(opts) {
|
|
|
150
150
|
db.prepare(`UPDATE raw_archive SET mirror_cleaned_at = ? WHERE memory_id = ?`).run(new Date().toISOString(), id);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
-
if (removedIds.length > 0) {
|
|
154
|
-
writeIndexMirror(opts.hippoRoot, buildIndexFromDb(db));
|
|
155
|
-
}
|
|
156
153
|
return { digest, content, removedIds, removedRawIds };
|
|
157
154
|
}
|
|
158
155
|
finally {
|
package/dist/secret-detect.d.ts
CHANGED
|
@@ -38,4 +38,6 @@ export declare function detectSecret(entry: {
|
|
|
38
38
|
* CS1 pre-compact snapshot fields — can scrub it in place instead.
|
|
39
39
|
*/
|
|
40
40
|
export declare function redactSecrets(text: string): string;
|
|
41
|
+
/** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer headers and JWTs. */
|
|
42
|
+
export declare function redactSecretsStrict(text: string): string;
|
|
41
43
|
//# sourceMappingURL=secret-detect.d.ts.map
|
package/dist/secret-detect.js
CHANGED
|
@@ -48,6 +48,11 @@ const SECRET_PATTERNS = [
|
|
|
48
48
|
];
|
|
49
49
|
const KEYISH_CONTEXT_RE = /key|token|secret|credential|bearer|auth|password/i;
|
|
50
50
|
const CO_OCCURRENCE_GUARDED = new Set(['sk-style-key', 'sk-underscore-key']);
|
|
51
|
+
// redactSecretsStrict-only: too noisy for whole-entry memory scanning, worth hiding once text leaves the machine.
|
|
52
|
+
const STRICT_ONLY_PATTERNS = [
|
|
53
|
+
/\b[Bb]earer\s+[A-Za-z0-9._~+/-]{16,}=*/g,
|
|
54
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g,
|
|
55
|
+
];
|
|
51
56
|
/**
|
|
52
57
|
* Scan a memory's tags + content for secret material.
|
|
53
58
|
* Pure and deterministic; no filesystem or store access.
|
|
@@ -75,21 +80,28 @@ export function detectSecret(entry) {
|
|
|
75
80
|
* CS1 pre-compact snapshot fields — can scrub it in place instead.
|
|
76
81
|
*/
|
|
77
82
|
export function redactSecrets(text) {
|
|
83
|
+
return redactText(text, false);
|
|
84
|
+
}
|
|
85
|
+
/** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer headers and JWTs. */
|
|
86
|
+
export function redactSecretsStrict(text) {
|
|
87
|
+
return redactText(text, true);
|
|
88
|
+
}
|
|
89
|
+
function redactText(text, strict) {
|
|
78
90
|
if (!text)
|
|
79
91
|
return text;
|
|
80
92
|
let result = text;
|
|
81
|
-
// PEM/OpenSSH blocks first: the
|
|
82
|
-
// BEGIN delimiter, which is fine for detectSecret's flag-or-not decision
|
|
83
|
-
// but would leave the base64 payload behind here. Consume through the
|
|
84
|
-
// matching END delimiter; a truncated block with no END is redacted to
|
|
85
|
-
// the end of the text (codex round 3).
|
|
93
|
+
// PEM/OpenSSH blocks first: SECRET_PATTERNS only matches BEGIN; consume through END, or to the end of a truncated block.
|
|
86
94
|
result = result.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----(?:[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----|[\s\S]*$)/g, '[REDACTED]');
|
|
87
95
|
for (const { name, re } of SECRET_PATTERNS) {
|
|
88
|
-
if (CO_OCCURRENCE_GUARDED.has(name) && !KEYISH_CONTEXT_RE.test(text))
|
|
96
|
+
if (!strict && CO_OCCURRENCE_GUARDED.has(name) && !KEYISH_CONTEXT_RE.test(text))
|
|
89
97
|
continue;
|
|
90
98
|
const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
|
|
91
99
|
result = result.replace(new RegExp(re.source, flags), '[REDACTED]');
|
|
92
100
|
}
|
|
101
|
+
if (strict) {
|
|
102
|
+
for (const re of STRICT_ONLY_PATTERNS)
|
|
103
|
+
result = result.replace(re, '[REDACTED]');
|
|
104
|
+
}
|
|
93
105
|
return result;
|
|
94
106
|
}
|
|
95
107
|
//# sourceMappingURL=secret-detect.js.map
|
package/dist/store.d.ts
CHANGED
|
@@ -190,23 +190,13 @@ interface LegacyStats {
|
|
|
190
190
|
total_forgotten: JsonValue;
|
|
191
191
|
consolidation_runs: JsonValue;
|
|
192
192
|
}
|
|
193
|
-
/**
|
|
194
|
-
*
|
|
195
|
-
* meta) from SQLite, the source of truth. Exported (AT1) for the same
|
|
196
|
-
* reason as `writeIndexMirror` below: `src/reject-flow.ts` needs to rebuild
|
|
197
|
-
* the index mirror post-commit after a (possibly multi-row) reject removal,
|
|
198
|
-
* without duplicating this query.
|
|
199
|
-
*/
|
|
193
|
+
/** Derive the current `HippoIndex` from SQLite. Exported for `rebuildIndex`
|
|
194
|
+
* (the only index.json writer) and the longmemeval benchmark. */
|
|
200
195
|
export declare function buildIndexFromDb(db: ReturnType<typeof openHippoDb>): HippoIndex;
|
|
201
|
-
/**
|
|
202
|
-
*
|
|
203
|
-
* Exported (AT1) so `src/reject-flow.ts` can replicate `deleteEntry`'s exact
|
|
204
|
-
* post-commit "removeEntryMirrors then rewrite the index once" sequence for
|
|
205
|
-
* the reject verb's (possibly multi-row) removal, without duplicating
|
|
206
|
-
* `buildIndexFromDb`'s query.
|
|
207
|
-
*/
|
|
196
|
+
/** Write the `index.json` mirror file for an already-derived index. Exported for
|
|
197
|
+
* `rebuildIndex` (the only index.json writer) and the longmemeval benchmark. */
|
|
208
198
|
export declare function writeIndexMirror(hippoRoot: string, index: HippoIndex): void;
|
|
209
|
-
/** Load the derived index from SQLite. Read-only:
|
|
199
|
+
/** Load the derived index from SQLite. Read-only: index.json is only ever written by `rebuildIndex`. */
|
|
210
200
|
export declare function loadIndex(hippoRoot: string): HippoIndex;
|
|
211
201
|
/**
|
|
212
202
|
* Persist mutable index metadata. Entry rows themselves are derived from SQLite.
|
|
@@ -215,10 +205,8 @@ export declare function loadIndex(hippoRoot: string): HippoIndex;
|
|
|
215
205
|
* land atomically — callers (getContext, cmdRecall) fold a freshly-written
|
|
216
206
|
* trace id into `index.last_trace_id` before calling this, relying on BOTH
|
|
217
207
|
* meta keys committing together. Wrapped in BEGIN/COMMIT so a crash or a
|
|
218
|
-
* mid-write failure can never advance one key without the other.
|
|
219
|
-
*
|
|
220
|
-
* truth, the mirror is best-effort (matches every other per-call-handle
|
|
221
|
-
* site's convention).
|
|
208
|
+
* mid-write failure can never advance one key without the other. index.json
|
|
209
|
+
* is left untouched; only `rebuildIndex` writes it.
|
|
222
210
|
*/
|
|
223
211
|
export declare function saveIndex(hippoRoot: string, index: HippoIndex): void;
|
|
224
212
|
/**
|
|
@@ -277,13 +265,8 @@ export declare function writeEntryDbOnly(db: DatabaseSyncLike, entry: MemoryEntr
|
|
|
277
265
|
actor?: string;
|
|
278
266
|
afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
|
|
279
267
|
}): void;
|
|
280
|
-
/**
|
|
281
|
-
|
|
282
|
-
* (used by `buildIndexFromDb` to derive the index from the source of truth).
|
|
283
|
-
* MUST be invoked AFTER the outer transaction commits — a mirror write
|
|
284
|
-
* during a tx that subsequently rolls back would leave orphan markdown.
|
|
285
|
-
*/
|
|
286
|
-
export declare function writeEntryMirrors(hippoRoot: string, db: DatabaseSyncLike, entry: MemoryEntry): void;
|
|
268
|
+
/** Markdown mirror path, invoked AFTER commit (a rolled-back tx must leave no orphan markdown). */
|
|
269
|
+
export declare function writeEntryMirrors(hippoRoot: string, entry: MemoryEntry): void;
|
|
287
270
|
/**
|
|
288
271
|
* Read a memory entry by ID.
|
|
289
272
|
*
|
package/dist/store.js
CHANGED
|
@@ -940,6 +940,7 @@ function upsertEntryRow(db, entry, bypassRejectionGuard = false) {
|
|
|
940
940
|
if (!bypassRejectionGuard) {
|
|
941
941
|
checkRejectionGuard(db, entry.tenantId ?? 'default', entry.id, entry.content);
|
|
942
942
|
}
|
|
943
|
+
const isNewRow = db.prepare(`SELECT 1 FROM memories WHERE id = ?`).get(entry.id) === undefined;
|
|
943
944
|
db.prepare(`
|
|
944
945
|
INSERT INTO memories(
|
|
945
946
|
id, created, last_retrieved, retrieval_count, strength, half_life_days, layer,
|
|
@@ -996,13 +997,14 @@ function upsertEntryRow(db, entry, bypassRejectionGuard = false) {
|
|
|
996
997
|
dag_level_3_built_at = excluded.dag_level_3_built_at,
|
|
997
998
|
updated_at = datetime('now')
|
|
998
999
|
`).run(entry.id, entry.created, entry.last_retrieved, entry.retrieval_count, entry.strength, entry.half_life_days, entry.layer, JSON.stringify(entry.tags ?? []), entry.emotional_valence, entry.schema_fit, entry.source, entry.outcome_score, entry.outcome_positive ?? 0, entry.outcome_negative ?? 0, JSON.stringify(entry.conflicts_with ?? []), entry.pinned ? 1 : 0, entry.confidence, entry.content, JSON.stringify(entry.parents ?? []), entry.starred ? 1 : 0, entry.trace_outcome ?? null, entry.source_session_id ?? null, entry.valid_from ?? entry.created, entry.superseded_by ?? null, entry.extracted_from ?? null, entry.dag_level ?? 0, entry.dag_parent_id ?? null, entry.kind ?? 'distilled', entry.scope ?? null, entry.owner ?? null, entry.artifact_ref ?? null, entry.tenantId ?? 'default', entry.origin_project ?? null, entry.descendant_count ?? 0, entry.earliest_at ?? null, entry.latest_at ?? null, entry.dag_level_3_built_at ?? null);
|
|
999
|
-
syncFtsRow(db, entry);
|
|
1000
|
+
syncFtsRow(db, entry, isNewRow);
|
|
1000
1001
|
}
|
|
1001
|
-
function syncFtsRow(db, entry) {
|
|
1002
|
+
function syncFtsRow(db, entry, isNewRow = false) {
|
|
1002
1003
|
if (!isFtsAvailable(db))
|
|
1003
1004
|
return;
|
|
1004
1005
|
try {
|
|
1005
|
-
|
|
1006
|
+
if (!isNewRow)
|
|
1007
|
+
db.prepare(`DELETE FROM memories_fts WHERE id = ?`).run(entry.id);
|
|
1006
1008
|
db.prepare(`INSERT INTO memories_fts(id, content, tags) VALUES (?, ?, ?)`).run(entry.id, entry.content, entry.tags.join(' '));
|
|
1007
1009
|
}
|
|
1008
1010
|
catch {
|
|
@@ -1019,13 +1021,8 @@ function deleteFtsRow(db, id) {
|
|
|
1019
1021
|
// Best effort.
|
|
1020
1022
|
}
|
|
1021
1023
|
}
|
|
1022
|
-
/**
|
|
1023
|
-
*
|
|
1024
|
-
* meta) from SQLite, the source of truth. Exported (AT1) for the same
|
|
1025
|
-
* reason as `writeIndexMirror` below: `src/reject-flow.ts` needs to rebuild
|
|
1026
|
-
* the index mirror post-commit after a (possibly multi-row) reject removal,
|
|
1027
|
-
* without duplicating this query.
|
|
1028
|
-
*/
|
|
1024
|
+
/** Derive the current `HippoIndex` from SQLite. Exported for `rebuildIndex`
|
|
1025
|
+
* (the only index.json writer) and the longmemeval benchmark. */
|
|
1029
1026
|
export function buildIndexFromDb(db) {
|
|
1030
1027
|
// SAFETY: rows' shape matches the seven columns named in the SELECT below.
|
|
1031
1028
|
const rows = db.prepare(`SELECT id, created, last_retrieved, strength, layer, tags_json, pinned FROM memories ORDER BY created ASC, id ASC`).all();
|
|
@@ -1076,13 +1073,8 @@ function buildStatsFromDb(db) {
|
|
|
1076
1073
|
})),
|
|
1077
1074
|
};
|
|
1078
1075
|
}
|
|
1079
|
-
/**
|
|
1080
|
-
*
|
|
1081
|
-
* Exported (AT1) so `src/reject-flow.ts` can replicate `deleteEntry`'s exact
|
|
1082
|
-
* post-commit "removeEntryMirrors then rewrite the index once" sequence for
|
|
1083
|
-
* the reject verb's (possibly multi-row) removal, without duplicating
|
|
1084
|
-
* `buildIndexFromDb`'s query.
|
|
1085
|
-
*/
|
|
1076
|
+
/** Write the `index.json` mirror file for an already-derived index. Exported for
|
|
1077
|
+
* `rebuildIndex` (the only index.json writer) and the longmemeval benchmark. */
|
|
1086
1078
|
export function writeIndexMirror(hippoRoot, index) {
|
|
1087
1079
|
mirrorBestEffort('index.json', () => fs.writeFileSync(path.join(hippoRoot, 'index.json'), JSON.stringify(index, null, 2), 'utf8'));
|
|
1088
1080
|
}
|
|
@@ -1115,10 +1107,9 @@ function syncMirrorFiles(hippoRoot, db) {
|
|
|
1115
1107
|
ORDER BY updated_at DESC, id DESC
|
|
1116
1108
|
`).all();
|
|
1117
1109
|
mirrorBestEffort('conflict mirrors', () => writeConflictMirrors(hippoRoot, conflicts.map(rowToMemoryConflict)));
|
|
1118
|
-
writeIndexMirror(hippoRoot, buildIndexFromDb(db));
|
|
1119
1110
|
writeStatsMirror(hippoRoot, buildStatsFromDb(db));
|
|
1120
1111
|
}
|
|
1121
|
-
/** Load the derived index from SQLite. Read-only:
|
|
1112
|
+
/** Load the derived index from SQLite. Read-only: index.json is only ever written by `rebuildIndex`. */
|
|
1122
1113
|
export function loadIndex(hippoRoot) {
|
|
1123
1114
|
initStore(hippoRoot);
|
|
1124
1115
|
const db = openHippoDb(hippoRoot);
|
|
@@ -1136,10 +1127,8 @@ export function loadIndex(hippoRoot) {
|
|
|
1136
1127
|
* land atomically — callers (getContext, cmdRecall) fold a freshly-written
|
|
1137
1128
|
* trace id into `index.last_trace_id` before calling this, relying on BOTH
|
|
1138
1129
|
* meta keys committing together. Wrapped in BEGIN/COMMIT so a crash or a
|
|
1139
|
-
* mid-write failure can never advance one key without the other.
|
|
1140
|
-
*
|
|
1141
|
-
* truth, the mirror is best-effort (matches every other per-call-handle
|
|
1142
|
-
* site's convention).
|
|
1130
|
+
* mid-write failure can never advance one key without the other. index.json
|
|
1131
|
+
* is left untouched; only `rebuildIndex` writes it.
|
|
1143
1132
|
*/
|
|
1144
1133
|
export function saveIndex(hippoRoot, index) {
|
|
1145
1134
|
initStore(hippoRoot);
|
|
@@ -1158,7 +1147,6 @@ export function saveIndex(hippoRoot, index) {
|
|
|
1158
1147
|
catch { /* already rolled back; keep the original error */ }
|
|
1159
1148
|
throw error;
|
|
1160
1149
|
}
|
|
1161
|
-
writeIndexMirror(hippoRoot, buildIndexFromDb(db));
|
|
1162
1150
|
}
|
|
1163
1151
|
finally {
|
|
1164
1152
|
closeHippoDb(db);
|
|
@@ -1222,7 +1210,7 @@ export function writeEntry(hippoRoot, entry, opts) {
|
|
|
1222
1210
|
try {
|
|
1223
1211
|
writeEntryDbOnly(db, stamped, opts);
|
|
1224
1212
|
opts?.afterCommit?.();
|
|
1225
|
-
writeEntryMirrors(hippoRoot,
|
|
1213
|
+
writeEntryMirrors(hippoRoot, stamped);
|
|
1226
1214
|
}
|
|
1227
1215
|
catch (error) {
|
|
1228
1216
|
// AT1 (plan §3): writeEntryDbOnly's own SAVEPOINT has already unwound by
|
|
@@ -1286,15 +1274,9 @@ export function writeEntryDbOnly(db, entry, opts) {
|
|
|
1286
1274
|
throw e;
|
|
1287
1275
|
}
|
|
1288
1276
|
}
|
|
1289
|
-
/**
|
|
1290
|
-
|
|
1291
|
-
* (used by `buildIndexFromDb` to derive the index from the source of truth).
|
|
1292
|
-
* MUST be invoked AFTER the outer transaction commits — a mirror write
|
|
1293
|
-
* during a tx that subsequently rolls back would leave orphan markdown.
|
|
1294
|
-
*/
|
|
1295
|
-
export function writeEntryMirrors(hippoRoot, db, entry) {
|
|
1277
|
+
/** Markdown mirror path, invoked AFTER commit (a rolled-back tx must leave no orphan markdown). */
|
|
1278
|
+
export function writeEntryMirrors(hippoRoot, entry) {
|
|
1296
1279
|
mirrorBestEffort(`${entry.id}.md`, () => writeMarkdownMirror(hippoRoot, entry));
|
|
1297
|
-
writeIndexMirror(hippoRoot, buildIndexFromDb(db));
|
|
1298
1280
|
}
|
|
1299
1281
|
/**
|
|
1300
1282
|
* Read a memory entry by ID.
|
|
@@ -1611,7 +1593,6 @@ export function deleteEntry(hippoRoot, id, opts) {
|
|
|
1611
1593
|
if (!result)
|
|
1612
1594
|
return false;
|
|
1613
1595
|
purgeMirrorBestEffort(hippoRoot, id, false, 'deleteEntry');
|
|
1614
|
-
writeIndexMirror(hippoRoot, buildIndexFromDb(db));
|
|
1615
1596
|
return true;
|
|
1616
1597
|
}
|
|
1617
1598
|
finally {
|
|
@@ -1786,7 +1767,6 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
|
|
|
1786
1767
|
});
|
|
1787
1768
|
for (const id of removedIds)
|
|
1788
1769
|
purgeMirrorBestEffort(hippoRoot, id, false, 'batchWriteAndDelete');
|
|
1789
|
-
writeIndexMirror(hippoRoot, buildIndexFromDb(db));
|
|
1790
1770
|
return removedIds;
|
|
1791
1771
|
}
|
|
1792
1772
|
catch (error) {
|
|
@@ -1978,7 +1958,9 @@ export function rebuildIndex(hippoRoot) {
|
|
|
1978
1958
|
}
|
|
1979
1959
|
}
|
|
1980
1960
|
syncMirrorFiles(hippoRoot, db);
|
|
1981
|
-
|
|
1961
|
+
const index = buildIndexFromDb(db);
|
|
1962
|
+
writeIndexMirror(hippoRoot, index);
|
|
1963
|
+
return index;
|
|
1982
1964
|
}
|
|
1983
1965
|
finally {
|
|
1984
1966
|
closeHippoDb(db);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type DoctorOpts } from './doctor.js';
|
|
2
|
+
import type { JsonObject } from './working-memory.js';
|
|
3
|
+
export interface SupportBundleOpts extends DoctorOpts {
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly home: string;
|
|
6
|
+
readonly now: Date;
|
|
7
|
+
readonly includeLogs: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const TAIL_MAX_LINES = 200;
|
|
10
|
+
/** Read-only: builds one redacted support-ticket snapshot. Never reads a memory content column. */
|
|
11
|
+
export declare function buildSupportBundle(opts: SupportBundleOpts): JsonObject;
|
|
12
|
+
//# sourceMappingURL=support-bundle.d.ts.map
|