hippo-memory 1.59.0 → 1.60.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/dist/api.d.ts +2 -0
- package/dist/api.js +25 -9
- package/dist/audit.js +1 -0
- package/dist/autolearn.js +5 -2
- package/dist/capture.js +8 -6
- package/dist/cli.d.ts +355 -0
- package/dist/cli.js +1236 -1081
- package/dist/compaction-record.js +7 -6
- package/dist/config.js +12 -11
- package/dist/connectors/github/cli-impl.js +1 -0
- package/dist/consolidate.js +4 -3
- package/dist/dag.js +7 -7
- package/dist/dashboard.js +4 -2
- package/dist/db.js +16 -3
- package/dist/delivery-recorder.js +1 -0
- package/dist/doctor.js +1 -0
- package/dist/dormant.js +1 -0
- package/dist/embedding-provider.js +3 -2
- package/dist/embeddings.js +10 -7
- package/dist/extract.js +4 -3
- package/dist/graph.js +3 -8
- package/dist/handoff.js +3 -0
- package/dist/hooks.js +3 -0
- package/dist/importers.js +6 -3
- package/dist/incidents.js +1 -0
- package/dist/judgment.js +5 -2
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.js +32 -13
- package/dist/memory.d.ts +5 -3
- package/dist/processes.js +1 -0
- package/dist/project-identity.d.ts +1 -1
- package/dist/project-identity.js +9 -4
- package/dist/raw-archive-mirror-cleanup.js +2 -1
- package/dist/recall-trace.d.ts +2 -2
- package/dist/recall-trace.js +10 -14
- package/dist/refine-llm.js +18 -10
- package/dist/rerankers/clef.d.ts +2 -2
- package/dist/rerankers/clef.js +68 -27
- package/dist/rerankers/cross-encoder.js +6 -4
- package/dist/rerankers/jev.d.ts +3 -1
- package/dist/rerankers/jev.js +20 -15
- package/dist/rerankers/llm.js +3 -3
- package/dist/same-text.d.ts +2 -0
- package/dist/same-text.js +4 -0
- package/dist/scheduler.js +1 -0
- package/dist/search.js +4 -2
- package/dist/secret-detect.js +1 -0
- package/dist/server.js +29 -12
- package/dist/shared.js +21 -19
- package/dist/stdin.js +1 -0
- package/dist/store.d.ts +33 -1
- package/dist/store.js +117 -16
- package/dist/token-ledger.js +1 -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
|
@@ -12,6 +12,7 @@ import { deriveOriginProject, isGlobalStoreRoot } from './project-identity.js';
|
|
|
12
12
|
import { maskEmails, redactSecretsStrict } from './secret-detect.js';
|
|
13
13
|
import { strengthenRetrievedOn, updateStats, writeEntryMirrors } from './store.js';
|
|
14
14
|
import { resolveTenantId } from './tenant.js';
|
|
15
|
+
import { log as logger } from './log.js';
|
|
15
16
|
/** PostCompact has 10 s in all (PreCompact 30 s), so a locked store must be given up on early. */
|
|
16
17
|
export const COMPACTION_DB_WAIT_MS = 2000;
|
|
17
18
|
/** Tested verbatim: Claude Code hands PreCompact stdout to the summariser as instructions. */
|
|
@@ -262,7 +263,7 @@ export function parsePostCompactPayload(stdinText) {
|
|
|
262
263
|
raw = JSON.parse((stdinText ?? '').trim());
|
|
263
264
|
}
|
|
264
265
|
catch {
|
|
265
|
-
return null;
|
|
266
|
+
return null; // non-JSON stdin is not a PostCompact payload; the caller skips it
|
|
266
267
|
}
|
|
267
268
|
if (!isObjectLike(raw) || !('session_id' in raw) || !isStringValue(raw.session_id) || raw.session_id === '')
|
|
268
269
|
return null;
|
|
@@ -309,7 +310,7 @@ export function saveCompaction(hippoRoot, payload, log) {
|
|
|
309
310
|
if (isSqliteBusy(err))
|
|
310
311
|
throw err;
|
|
311
312
|
log(`record step failed: ${errorMessage(err)}`);
|
|
312
|
-
|
|
313
|
+
logger.error(`post-compact: record step failed: ${errorMessage(err)}`);
|
|
313
314
|
}
|
|
314
315
|
try {
|
|
315
316
|
result.written = saveItems(db, hippoRoot, {
|
|
@@ -337,12 +338,12 @@ export function saveCompaction(hippoRoot, payload, log) {
|
|
|
337
338
|
}
|
|
338
339
|
catch (spoolErr) {
|
|
339
340
|
log(`spool failed: ${errorMessage(spoolErr)}`);
|
|
340
|
-
|
|
341
|
+
logger.error(`post-compact: spool failed: ${errorMessage(spoolErr)}`);
|
|
341
342
|
}
|
|
342
343
|
}
|
|
343
344
|
else {
|
|
344
345
|
log(`items step failed: ${errorMessage(err)}`);
|
|
345
|
-
|
|
346
|
+
logger.error(`post-compact: items step failed: ${errorMessage(err)}`);
|
|
346
347
|
}
|
|
347
348
|
}
|
|
348
349
|
finally {
|
|
@@ -380,7 +381,7 @@ function transcriptSummary(transcriptPath, afterMs, beforeMs) {
|
|
|
380
381
|
lines.push(JSON.parse(raw));
|
|
381
382
|
}
|
|
382
383
|
catch {
|
|
383
|
-
continue;
|
|
384
|
+
continue; // the tail can start mid-line; a torn line holds no summary
|
|
384
385
|
}
|
|
385
386
|
}
|
|
386
387
|
const stamp = (line) => (isStringValue(line.timestamp) ? Date.parse(line.timestamp) : Number.NaN);
|
|
@@ -409,7 +410,7 @@ function readSpooled(file, fallbackTenantId) {
|
|
|
409
410
|
raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
410
411
|
}
|
|
411
412
|
catch {
|
|
412
|
-
return null;
|
|
413
|
+
return null; // a half-written or vanished spool file is skipped; the caller treats null as unreadable
|
|
413
414
|
}
|
|
414
415
|
if (!isObjectLike(raw) || !('sessionId' in raw) || !isStringValue(raw.sessionId) || !('summary' in raw) || !isStringValue(raw.summary))
|
|
415
416
|
return null;
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as fs from 'fs';
|
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import { DEFAULT_PHYSICS_CONFIG, mergePhysicsConfig } from './physics-config.js';
|
|
7
7
|
import { DEFAULT_HALF_LIFE_DAYS } from './memory.js';
|
|
8
|
+
import { log } from './log.js';
|
|
8
9
|
const DEFAULT_CONFIG = {
|
|
9
10
|
defaultHalfLifeDays: DEFAULT_HALF_LIFE_DAYS,
|
|
10
11
|
defaultBudget: 4000,
|
|
@@ -114,7 +115,7 @@ function deliveryLedgerEnabled(value) {
|
|
|
114
115
|
return enabled;
|
|
115
116
|
if (isObject && enabled === undefined)
|
|
116
117
|
return false;
|
|
117
|
-
|
|
118
|
+
log.warn(`config.json's "deliveryLedger" must be an object like {"enabled": true} ` +
|
|
118
119
|
`(got ${JSON.stringify(value)}) - using false.`);
|
|
119
120
|
return false;
|
|
120
121
|
}
|
|
@@ -127,7 +128,7 @@ function pilotHoldoutRate(value) {
|
|
|
127
128
|
return 0;
|
|
128
129
|
if (Number.isInteger(rate) && rate >= 0 && rate <= 10000)
|
|
129
130
|
return rate;
|
|
130
|
-
|
|
131
|
+
log.warn(`config.json's "pilot" must be an object like {"holdoutRateBp": 2000}, an integer from 0 to 10000 ` +
|
|
131
132
|
`(got ${JSON.stringify(value)}) - using 0 (pilot off).`);
|
|
132
133
|
return 0;
|
|
133
134
|
}
|
|
@@ -136,7 +137,7 @@ function agentMemoryTools(value) {
|
|
|
136
137
|
return null;
|
|
137
138
|
if (Array.isArray(value) && value.every((t) => String(t) === t))
|
|
138
139
|
return value;
|
|
139
|
-
|
|
140
|
+
log.warn(`config.json's "agentMemories.tools" must be a list of tool ids like ["claude-code", "codex"] ` +
|
|
140
141
|
`(got ${JSON.stringify(value)}) - importing none.`);
|
|
141
142
|
return [];
|
|
142
143
|
}
|
|
@@ -158,7 +159,7 @@ export function loadConfig(hippoRoot) {
|
|
|
158
159
|
const memoryValueRaw = raw.memoryValue;
|
|
159
160
|
const validMemoryValueConfig = memoryValueRaw === undefined || isMemoryValueConfig(memoryValueRaw);
|
|
160
161
|
if (!validMemoryValueConfig) {
|
|
161
|
-
|
|
162
|
+
log.warn(`config.json's "memoryValue" must be an object like {"enabled": true} ` +
|
|
162
163
|
`(got ${JSON.stringify(memoryValueRaw)}) - using defaults.`);
|
|
163
164
|
}
|
|
164
165
|
const memoryValueOverride = memoryValueRaw !== undefined && isMemoryValueConfig(memoryValueRaw) ? memoryValueRaw : {};
|
|
@@ -167,20 +168,20 @@ export function loadConfig(hippoRoot) {
|
|
|
167
168
|
// warns and falls back to the default, which keeps faded memories.
|
|
168
169
|
const dormantRaw = raw.dormant;
|
|
169
170
|
if (dormantRaw !== undefined && !isDormantConfig(dormantRaw)) {
|
|
170
|
-
|
|
171
|
+
log.warn(`config.json's "dormant" must be an object like {"enabled": false} ` +
|
|
171
172
|
`(got ${JSON.stringify(dormantRaw)}) - using the default (faded memories kept dormant).`);
|
|
172
173
|
}
|
|
173
174
|
const dormantOverride = dormantRaw !== undefined && isDormantConfig(dormantRaw) ? dormantRaw : {};
|
|
174
175
|
// Only a real boolean counts: {"enabled": "false"} is a truthy string.
|
|
175
176
|
let dormantEnabled = dormantOverride.enabled ?? DEFAULT_CONFIG.dormant.enabled;
|
|
176
177
|
if (dormantEnabled !== true && dormantEnabled !== false) {
|
|
177
|
-
|
|
178
|
+
log.warn(`config.json's "dormant.enabled" must be true or false ` +
|
|
178
179
|
`(got ${JSON.stringify(dormantEnabled)}) - using the default (faded memories kept dormant).`);
|
|
179
180
|
dormantEnabled = DEFAULT_CONFIG.dormant.enabled;
|
|
180
181
|
}
|
|
181
182
|
let dormantRetentionDays = dormantOverride.retentionDays ?? DEFAULT_CONFIG.dormant.retentionDays;
|
|
182
183
|
if (!Number.isFinite(dormantRetentionDays) || dormantRetentionDays < 0) {
|
|
183
|
-
|
|
184
|
+
log.warn(`config.json's "dormant.retentionDays" must be a number of days, 0 or more ` +
|
|
184
185
|
`(got ${JSON.stringify(dormantRetentionDays)}) - using ${DEFAULT_CONFIG.dormant.retentionDays}.`);
|
|
185
186
|
dormantRetentionDays = DEFAULT_CONFIG.dormant.retentionDays;
|
|
186
187
|
}
|
|
@@ -188,21 +189,21 @@ export function loadConfig(hippoRoot) {
|
|
|
188
189
|
const churnStalenessRaw = raw.churnStaleness;
|
|
189
190
|
const validChurnStalenessConfig = churnStalenessRaw === undefined || isChurnStalenessConfig(churnStalenessRaw);
|
|
190
191
|
if (!validChurnStalenessConfig) {
|
|
191
|
-
|
|
192
|
+
log.warn(`config.json's "churnStaleness" must be an object like {"enabled": true} ` +
|
|
192
193
|
`(got ${JSON.stringify(churnStalenessRaw)}) - using defaults.`);
|
|
193
194
|
}
|
|
194
195
|
let churnStalenessEnabled = churnStalenessRaw !== undefined && isChurnStalenessConfig(churnStalenessRaw)
|
|
195
196
|
? churnStalenessRaw.enabled
|
|
196
197
|
: DEFAULT_CONFIG.churnStaleness.enabled;
|
|
197
198
|
if (churnStalenessEnabled !== true && churnStalenessEnabled !== false) {
|
|
198
|
-
|
|
199
|
+
log.warn(`config.json's "churnStaleness.enabled" must be true or false ` +
|
|
199
200
|
`(got ${JSON.stringify(churnStalenessEnabled)}) - using false.`);
|
|
200
201
|
churnStalenessEnabled = false;
|
|
201
202
|
}
|
|
202
203
|
// Every writer starts a memory on this, and a zero or negative half-life scores zero strength, so sleep would retire it.
|
|
203
204
|
let defaultHalfLifeDays = raw.defaultHalfLifeDays ?? DEFAULT_CONFIG.defaultHalfLifeDays;
|
|
204
205
|
if (!Number.isFinite(defaultHalfLifeDays) || defaultHalfLifeDays <= 0) {
|
|
205
|
-
|
|
206
|
+
log.warn(`config.json's "defaultHalfLifeDays" must be a number of days above 0 ` +
|
|
206
207
|
`(got ${JSON.stringify(defaultHalfLifeDays)}) - using ${DEFAULT_CONFIG.defaultHalfLifeDays}.`);
|
|
207
208
|
defaultHalfLifeDays = DEFAULT_CONFIG.defaultHalfLifeDays;
|
|
208
209
|
}
|
|
@@ -247,7 +248,7 @@ export function loadConfig(hippoRoot) {
|
|
|
247
248
|
}
|
|
248
249
|
catch (err) {
|
|
249
250
|
if (fs.existsSync(configPath)) {
|
|
250
|
-
|
|
251
|
+
log.warn(`failed to parse ${configPath}: ${err instanceof Error ? err.message : err}`);
|
|
251
252
|
}
|
|
252
253
|
return { ...DEFAULT_CONFIG };
|
|
253
254
|
}
|
|
@@ -52,6 +52,7 @@ function parsedToIngestEvent(parsed, eventName) {
|
|
|
52
52
|
}
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
|
+
// Console lines below are the `hippo github` command's printed result and usage text, so they stay off the logger.
|
|
55
56
|
export function printGithubBackfillUsage() {
|
|
56
57
|
console.log('hippo github backfill --repo <owner/name> [--since ISO] [--max <N>]');
|
|
57
58
|
console.log(' --repo GitHub repository in owner/name format (required, e.g. acme/widgets)');
|
package/dist/consolidate.js
CHANGED
|
@@ -31,6 +31,7 @@ import { migrateDefaultHalfLife, LEGACY_TYPED_HALF_LIFE } from './half-life-migr
|
|
|
31
31
|
import { derivationScope, commonDerivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
32
32
|
import { isQuarantineScope } from './quarantine.js';
|
|
33
33
|
import { NO_MERGE_TAGS } from './shared.js';
|
|
34
|
+
import { log } from './log.js';
|
|
34
35
|
const DECAY_THRESHOLD = 0.05;
|
|
35
36
|
const MERGE_OVERLAP_THRESHOLD = 0.35; // Jaccard similarity for "related"
|
|
36
37
|
const MERGE_MIN_CLUSTER = 2; // minimum cluster size to merge
|
|
@@ -434,7 +435,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
434
435
|
result.details.push(` 🧬 promoted ${result.promotedTraces} trace${result.promotedTraces === 1 ? '' : 's'} from completed session${result.promotedTraces === 1 ? '' : 's'}`);
|
|
435
436
|
}
|
|
436
437
|
if (tracesSkippedRejected > 0) {
|
|
437
|
-
|
|
438
|
+
log.warn(`consolidate: skipped ${tracesSkippedRejected} auto-promoted trace(s) whose content matches a rejected value`);
|
|
438
439
|
}
|
|
439
440
|
}
|
|
440
441
|
// -------------------------------------------------------------------------
|
|
@@ -490,7 +491,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
490
491
|
return;
|
|
491
492
|
llmErrorsSeen.add(line);
|
|
492
493
|
result.details.push(line);
|
|
493
|
-
|
|
494
|
+
log.warn(`consolidate ${phase}: ${msg}`);
|
|
494
495
|
};
|
|
495
496
|
const llmOpts = { apiKey, model: config.extraction.model, fetcher: options.fetcher };
|
|
496
497
|
if (apiKey && extractionCandidates.length > 0 && !dryRun) {
|
|
@@ -831,7 +832,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
831
832
|
closeHippoDb(consolidateDbHandle);
|
|
832
833
|
}
|
|
833
834
|
if (mergesSkippedRejected > 0) {
|
|
834
|
-
|
|
835
|
+
log.warn(`consolidate: skipped ${mergesSkippedRejected} merge(s) whose content matches a rejected value`);
|
|
835
836
|
}
|
|
836
837
|
result.removedIds = pendingDeletes;
|
|
837
838
|
// One transaction; the snapshot keeps what the DAG passes and other writers changed while sleep ran.
|
package/dist/dag.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createMemory, Layer } from './memory.js';
|
|
2
2
|
import { writeEntry, loadAllDirtySummaries, loadChildrenOfSummary, applyRebuildResult, clearSummaryDirtyAfterBuild, } from './store.js';
|
|
3
3
|
import { RejectedValueError } from './rejection.js';
|
|
4
|
-
import {
|
|
4
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
5
5
|
import { fetchWithRetry, llmTimeoutMs } from './http-retry.js';
|
|
6
6
|
import { derivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
7
7
|
import { loadConfig } from './config.js';
|
|
8
8
|
import { neverAutoShareTags } from './shared.js';
|
|
9
|
+
import { log } from './log.js';
|
|
9
10
|
export function clusterFacts(facts) {
|
|
10
11
|
if (facts.length === 0)
|
|
11
12
|
return [];
|
|
@@ -47,9 +48,9 @@ Write a single concise paragraph (2-4 sentences) that captures all the key infor
|
|
|
47
48
|
export async function generateDagSummary(label, factContents, opts) {
|
|
48
49
|
const model = opts.model ?? 'claude-sonnet-4-6';
|
|
49
50
|
const fetchFn = opts.fetcher ?? fetch;
|
|
50
|
-
const factsBlock = factContents.map((f, i) => `${i + 1}. ${
|
|
51
|
+
const factsBlock = factContents.map((f, i) => `${i + 1}. ${redactSecretsStrict(f)}`).join('\n');
|
|
51
52
|
const prompt = DAG_SUMMARY_PROMPT
|
|
52
|
-
.replace('{label}',
|
|
53
|
+
.replace('{label}', redactSecretsStrict(label))
|
|
53
54
|
.replace('{facts}', factsBlock);
|
|
54
55
|
let res;
|
|
55
56
|
try {
|
|
@@ -157,7 +158,7 @@ export async function buildDag(hippoRoot, facts, opts) {
|
|
|
157
158
|
catch (err) {
|
|
158
159
|
if (err instanceof RejectedValueError) {
|
|
159
160
|
result.rejected++;
|
|
160
|
-
|
|
161
|
+
log.warn(`buildDag: cluster "${cluster.label}" skipped: summary matches a rejected value`);
|
|
161
162
|
continue;
|
|
162
163
|
}
|
|
163
164
|
throw err;
|
|
@@ -274,8 +275,7 @@ export async function rebuildDirtySummaries(hippoRoot, opts) {
|
|
|
274
275
|
// (audit() wraps its own writes try/catch per store.ts:2566, so a
|
|
275
276
|
// throw here is exotic: SQLite I/O error, prepare failure, etc).
|
|
276
277
|
result.failed++;
|
|
277
|
-
|
|
278
|
-
console.error(`[rebuildDirtySummaries] summary ${summary.id} (tenant ${summary.tenantId}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
278
|
+
log.error(`rebuildDirtySummaries: summary ${summary.id} (tenant ${summary.tenantId}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
281
|
return result;
|
|
@@ -353,7 +353,7 @@ export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
|
|
|
353
353
|
catch (err) {
|
|
354
354
|
if (err instanceof RejectedValueError) {
|
|
355
355
|
result.rejected++;
|
|
356
|
-
|
|
356
|
+
log.warn(`buildEntityProfiles: cluster "${cluster.label}" skipped: profile matches a rejected value`);
|
|
357
357
|
continue;
|
|
358
358
|
}
|
|
359
359
|
throw err;
|
package/dist/dashboard.js
CHANGED
|
@@ -18,6 +18,7 @@ import { loadEmbeddingIndex } from './embeddings.js';
|
|
|
18
18
|
import { resolveTenantId } from './tenant.js';
|
|
19
19
|
import { loadCardDetail } from './card-detail.js';
|
|
20
20
|
import { isCrossSite, LOOPBACK_HOST_HEADER } from './server.js';
|
|
21
|
+
import { log } from './log.js';
|
|
21
22
|
function buildDashboardData(hippoRoot) {
|
|
22
23
|
// A5: scope dashboard to active tenant. Without this, the UI surfaces every
|
|
23
24
|
// tenant's memories on a multi-tenant deployment.
|
|
@@ -152,7 +153,7 @@ function serveStaticFile(res, filePath) {
|
|
|
152
153
|
return true;
|
|
153
154
|
}
|
|
154
155
|
catch {
|
|
155
|
-
return false;
|
|
156
|
+
return false; // an unreadable asset falls through to the next route, which answers 404 or the fallback page
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
export function serveDashboard(hippoRoot, port = 3333) {
|
|
@@ -249,7 +250,7 @@ export function serveDashboard(hippoRoot, port = 3333) {
|
|
|
249
250
|
handleRequest(req, res);
|
|
250
251
|
}
|
|
251
252
|
catch (err) {
|
|
252
|
-
|
|
253
|
+
log.error(`dashboard request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
253
254
|
if (res.headersSent)
|
|
254
255
|
res.end();
|
|
255
256
|
else
|
|
@@ -257,6 +258,7 @@ export function serveDashboard(hippoRoot, port = 3333) {
|
|
|
257
258
|
}
|
|
258
259
|
});
|
|
259
260
|
server.listen(port, '127.0.0.1', () => {
|
|
261
|
+
// The banner is the `hippo dashboard` command's printed result, so it stays on stdout.
|
|
260
262
|
console.log(`Hippo Dashboard running at http://localhost:${port}`);
|
|
261
263
|
if (hasDistUi)
|
|
262
264
|
console.log(`Serving React UI from ${distUiDir}`);
|
package/dist/db.js
CHANGED
|
@@ -12,7 +12,7 @@ const require = createRequire(import.meta.url);
|
|
|
12
12
|
// runtime (Node's built-in synchronous SQLite module); there are no bundled
|
|
13
13
|
// types for it here, so this require + cast is the module's documented boundary.
|
|
14
14
|
const { DatabaseSync } = require('node:sqlite');
|
|
15
|
-
const CURRENT_SCHEMA_VERSION =
|
|
15
|
+
const CURRENT_SCHEMA_VERSION = 51;
|
|
16
16
|
const MIGRATIONS = [
|
|
17
17
|
{
|
|
18
18
|
version: 1,
|
|
@@ -2568,6 +2568,19 @@ const MIGRATIONS = [
|
|
|
2568
2568
|
`);
|
|
2569
2569
|
},
|
|
2570
2570
|
},
|
|
2571
|
+
{
|
|
2572
|
+
version: 51,
|
|
2573
|
+
up: (db) => {
|
|
2574
|
+
// Both indexes serve loadAmbientCandidates (src/store.ts); their WHERE text must match its SQL for the planner to pick them.
|
|
2575
|
+
// The drift index stays empty on a healthy store, so the drift probe is one index seek instead of a tenant scan. Additive only.
|
|
2576
|
+
db.exec(`
|
|
2577
|
+
CREATE INDEX IF NOT EXISTS idx_memories_pinned
|
|
2578
|
+
ON memories(tenant_id, created, id) WHERE pinned = 1;
|
|
2579
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created_drift
|
|
2580
|
+
ON memories(tenant_id) WHERE superseded_by IS NULL AND (length(created) <> 24 OR created NOT LIKE '%Z');
|
|
2581
|
+
`);
|
|
2582
|
+
},
|
|
2583
|
+
},
|
|
2571
2584
|
];
|
|
2572
2585
|
function tableHasColumn(db, tableName, columnName) {
|
|
2573
2586
|
if (!/^[a-z_]+$/i.test(tableName))
|
|
@@ -2700,7 +2713,7 @@ function openOwnHippoDb(hippoRoot, opts) {
|
|
|
2700
2713
|
cleanupArchivedMirrors(hippoRoot, db);
|
|
2701
2714
|
}
|
|
2702
2715
|
catch (cleanupErr) {
|
|
2703
|
-
|
|
2716
|
+
log.error(`openHippoDb: cleanupArchivedMirrors failed (non-fatal): ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`);
|
|
2704
2717
|
}
|
|
2705
2718
|
return db;
|
|
2706
2719
|
}
|
|
@@ -2964,7 +2977,7 @@ function ensureOptionalFts(db) {
|
|
|
2964
2977
|
available = true;
|
|
2965
2978
|
}
|
|
2966
2979
|
catch (err) {
|
|
2967
|
-
|
|
2980
|
+
log.warn(`full-text index unavailable (${err instanceof Error ? err.message : String(err)}); search falls back to slower LIKE matching`);
|
|
2968
2981
|
}
|
|
2969
2982
|
// Read-first: only write when the flag actually changed, so a healthy
|
|
2970
2983
|
// store's open doesn't take a write lock for a same-value upsert.
|
|
@@ -205,6 +205,7 @@ export function createDeliveryRecorder(init) {
|
|
|
205
205
|
return;
|
|
206
206
|
flushed = true;
|
|
207
207
|
if (broken !== null) {
|
|
208
|
+
// Same pinned `[hippo] delivery ledger` hook stderr line as recall-trace.ts's write failure.
|
|
208
209
|
console.error(`[hippo] delivery ledger skipped: recorder failed: ${broken}`);
|
|
209
210
|
return;
|
|
210
211
|
}
|
package/dist/doctor.js
CHANGED
package/dist/dormant.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { getEmbedding, isEmbeddingAvailable, resolveEmbeddingModel, DEFAULT_EMBEDDING_MODEL, } from './embeddings.js';
|
|
34
34
|
import { loadConfig } from './config.js';
|
|
35
|
-
import {
|
|
35
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
36
36
|
import { fetchWithRetry } from './http-retry.js';
|
|
37
37
|
export const API_PROVIDER_KINDS = ['openai', 'voyage', 'cohere'];
|
|
38
38
|
function isApiProviderKind(x) {
|
|
@@ -191,7 +191,7 @@ class ApiEmbeddingProvider {
|
|
|
191
191
|
'content-type': 'application/json',
|
|
192
192
|
authorization: `Bearer ${key}`,
|
|
193
193
|
},
|
|
194
|
-
body: JSON.stringify(spec.buildBody(this.model, chunk.map(
|
|
194
|
+
body: JSON.stringify(spec.buildBody(this.model, chunk.map(redactSecretsStrict), role)),
|
|
195
195
|
}, { timeoutMs: REQUEST_TIMEOUT_MS });
|
|
196
196
|
}
|
|
197
197
|
catch (err) {
|
|
@@ -237,6 +237,7 @@ function readEmbeddingsConfig(hippoRoot) {
|
|
|
237
237
|
return loadConfig(hippoRoot).embeddings;
|
|
238
238
|
}
|
|
239
239
|
catch {
|
|
240
|
+
// An unreadable config falls back to provider defaults; loadConfig warns on a bad parse.
|
|
240
241
|
return {};
|
|
241
242
|
}
|
|
242
243
|
}
|
package/dist/embeddings.js
CHANGED
|
@@ -147,7 +147,7 @@ function resolveTransformersPackage() {
|
|
|
147
147
|
return '@xenova/transformers';
|
|
148
148
|
}
|
|
149
149
|
catch {
|
|
150
|
-
return null;
|
|
150
|
+
return null; // neither optional package is installed; callers fall back to no local embeddings
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -177,7 +177,8 @@ async function loadPipeline(model) {
|
|
|
177
177
|
}
|
|
178
178
|
pipelineFn = mod.pipeline ?? mod.default?.pipeline;
|
|
179
179
|
}
|
|
180
|
-
catch {
|
|
180
|
+
catch (err) {
|
|
181
|
+
log.debug(`transformers import failed (${pkg}): ${err instanceof Error ? err.message : String(err)}`);
|
|
181
182
|
return null;
|
|
182
183
|
}
|
|
183
184
|
if (!pipelineFn)
|
|
@@ -193,7 +194,8 @@ async function loadPipeline(model) {
|
|
|
193
194
|
_pipelineInstances.set(model, instance);
|
|
194
195
|
return instance;
|
|
195
196
|
}
|
|
196
|
-
catch {
|
|
197
|
+
catch (err) {
|
|
198
|
+
log.debug(`embedding pipeline load failed (${model}): ${err instanceof Error ? err.message : String(err)}`);
|
|
197
199
|
return null;
|
|
198
200
|
}
|
|
199
201
|
finally {
|
|
@@ -228,7 +230,8 @@ function loadStoredEmbeddingModel(hippoRoot) {
|
|
|
228
230
|
closeHippoDb(db);
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
|
-
catch {
|
|
233
|
+
catch (err) {
|
|
234
|
+
log.debug(`stored embedding model unreadable: ${err instanceof Error ? err.message : String(err)}`);
|
|
232
235
|
return null;
|
|
233
236
|
}
|
|
234
237
|
}
|
|
@@ -363,7 +366,7 @@ function parseEmbeddingIndex(raw) {
|
|
|
363
366
|
return parsed instanceof Object && !Array.isArray(parsed) ? parsed : null;
|
|
364
367
|
}
|
|
365
368
|
catch {
|
|
366
|
-
return null;
|
|
369
|
+
return null; // the caller quarantines the corrupt file and logs it
|
|
367
370
|
}
|
|
368
371
|
}
|
|
369
372
|
/** Move a corrupt index aside and flag a full rebuild, so no later save can write over the only copy of those bytes. */
|
|
@@ -523,7 +526,7 @@ function warnEmbedFailureOnce(source, rawMessage) {
|
|
|
523
526
|
_embedFailureWarned = true;
|
|
524
527
|
// Strict scrub: this line can land in a hook log file, and an API may echo the key back in its error body.
|
|
525
528
|
const message = redactSecretsStrict(rawMessage).replace(/\s+/g, ' ').replace(/\.+$/, '');
|
|
526
|
-
|
|
529
|
+
log.warn(`embedding failed (${source}): ${message}. Memories are stored without embeddings until this is fixed.`);
|
|
527
530
|
}
|
|
528
531
|
/**
|
|
529
532
|
* Embed a single memory entry and cache the result in the embedding index.
|
|
@@ -591,7 +594,7 @@ export async function embedMemory(hippoRoot, entry, model) {
|
|
|
591
594
|
warnEmbedFailureOnce(provider.kind, err instanceof Error ? err.message : String(err));
|
|
592
595
|
}
|
|
593
596
|
}).catch((err) => {
|
|
594
|
-
|
|
597
|
+
log.warn(`skipped embedding ${entry.id} (${err instanceof Error ? err.message : String(err)}); run 'hippo embed' to backfill`);
|
|
595
598
|
});
|
|
596
599
|
}
|
|
597
600
|
/**
|
package/dist/extract.js
CHANGED
|
@@ -2,9 +2,10 @@ import { Layer, createMemory } from './memory.js';
|
|
|
2
2
|
import { writeEntry } from './store.js';
|
|
3
3
|
import { loadConfig } from './config.js';
|
|
4
4
|
import { RejectedValueError } from './rejection.js';
|
|
5
|
-
import {
|
|
5
|
+
import { redactSecretsStrict } from './secret-detect.js';
|
|
6
6
|
import { fetchWithRetry, llmTimeoutMs } from './http-retry.js';
|
|
7
7
|
import { neverAutoShareTags } from './shared.js';
|
|
8
|
+
import { log } from './log.js';
|
|
8
9
|
function isJsonString(value) {
|
|
9
10
|
return typeof value === 'string';
|
|
10
11
|
}
|
|
@@ -35,7 +36,7 @@ export async function extractFacts(text, opts) {
|
|
|
35
36
|
body: JSON.stringify({
|
|
36
37
|
model,
|
|
37
38
|
max_tokens: 1200,
|
|
38
|
-
messages: [{ role: 'user', content: EXTRACTION_PROMPT +
|
|
39
|
+
messages: [{ role: 'user', content: EXTRACTION_PROMPT + redactSecretsStrict(text) }],
|
|
39
40
|
}),
|
|
40
41
|
}, { timeoutMs: llmTimeoutMs(), fetchFn });
|
|
41
42
|
}
|
|
@@ -124,7 +125,7 @@ export function storeExtractedFacts(hippoRoot, source, facts) {
|
|
|
124
125
|
entries.push(entry);
|
|
125
126
|
}
|
|
126
127
|
if (rejected > 0) {
|
|
127
|
-
|
|
128
|
+
log.warn(`storeExtractedFacts: skipped ${rejected} rejected value(s)`);
|
|
128
129
|
}
|
|
129
130
|
return entries;
|
|
130
131
|
}
|
package/dist/graph.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
21
21
|
import { assertTenantId } from './tenant.js';
|
|
22
|
+
import { log } from './log.js';
|
|
22
23
|
const SOURCE_OBJECT_TABLE = {
|
|
23
24
|
decision: 'decisions',
|
|
24
25
|
policy: 'policies',
|
|
@@ -665,10 +666,7 @@ export function markGraphDirty(hippoRoot, tenantId, memoryId) {
|
|
|
665
666
|
catch (err) {
|
|
666
667
|
// Logged (warn) so a SYSTEMATIC enqueue failure surfaces to operators, but
|
|
667
668
|
// swallowed so the already-committed E2 write is never rolled back.
|
|
668
|
-
|
|
669
|
-
// JS value is safe (undefined if absent), preserving the existing lenient
|
|
670
|
-
// formatting even when something non-Error was thrown.
|
|
671
|
-
console.warn(`markGraphDirty: enqueue failed for tenant=${tenantId} memory=${memoryId}: ${err.message}`);
|
|
669
|
+
log.warn(`markGraphDirty: enqueue failed for tenant=${tenantId} memory=${memoryId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
672
670
|
}
|
|
673
671
|
}
|
|
674
672
|
/**
|
|
@@ -707,10 +705,7 @@ export function removeGraphEntitiesForObject(hippoRoot, tenantId, sourceObjectTy
|
|
|
707
705
|
}
|
|
708
706
|
}
|
|
709
707
|
catch (err) {
|
|
710
|
-
|
|
711
|
-
// JS value is safe (undefined if absent), preserving the existing lenient
|
|
712
|
-
// formatting even when something non-Error was thrown.
|
|
713
|
-
console.warn(`removeGraphEntitiesForObject: failed for tenant=${tenantId} ${sourceObjectType}#${sourceObjectId}: ${err.message}`);
|
|
708
|
+
log.warn(`removeGraphEntitiesForObject: failed for tenant=${tenantId} ${sourceObjectType}#${sourceObjectId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
714
709
|
}
|
|
715
710
|
}
|
|
716
711
|
/**
|
package/dist/handoff.js
CHANGED
|
@@ -25,6 +25,7 @@ export function rowToSessionHandoff(row) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
catch {
|
|
28
|
+
// A corrupt column degrades to empty instead of hiding the whole handoff.
|
|
28
29
|
artifacts = [];
|
|
29
30
|
}
|
|
30
31
|
let constraints = [];
|
|
@@ -35,6 +36,7 @@ export function rowToSessionHandoff(row) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
catch {
|
|
39
|
+
// Same degrade-to-empty rule as artifacts.
|
|
38
40
|
constraints = [];
|
|
39
41
|
}
|
|
40
42
|
let evidence = null;
|
|
@@ -43,6 +45,7 @@ export function rowToSessionHandoff(row) {
|
|
|
43
45
|
evidence = row.evidence_json ? JSON.parse(row.evidence_json) : null;
|
|
44
46
|
}
|
|
45
47
|
catch {
|
|
48
|
+
// Same degrade rule as artifacts: bad evidence reads as none.
|
|
46
49
|
evidence = null;
|
|
47
50
|
}
|
|
48
51
|
return {
|
package/dist/hooks.js
CHANGED
|
@@ -183,6 +183,7 @@ function readCodexWrapperMetadata() {
|
|
|
183
183
|
return parsed;
|
|
184
184
|
}
|
|
185
185
|
catch {
|
|
186
|
+
// Unreadable metadata is treated like the missing-file case above.
|
|
186
187
|
return null;
|
|
187
188
|
}
|
|
188
189
|
}
|
|
@@ -191,6 +192,7 @@ function readTextFile(filePath) {
|
|
|
191
192
|
return fs.readFileSync(filePath, 'utf8');
|
|
192
193
|
}
|
|
193
194
|
catch {
|
|
195
|
+
// Callers treat an unreadable file as absent.
|
|
194
196
|
return null;
|
|
195
197
|
}
|
|
196
198
|
}
|
|
@@ -879,6 +881,7 @@ export function uninstallJsonHooks(target) {
|
|
|
879
881
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
880
882
|
}
|
|
881
883
|
catch {
|
|
884
|
+
// Never rewrite a settings file we cannot parse; report nothing uninstalled.
|
|
882
885
|
return false;
|
|
883
886
|
}
|
|
884
887
|
if (!isJsonObject(settings) || !isJsonObject(settings.hooks))
|
package/dist/importers.js
CHANGED
|
@@ -14,6 +14,7 @@ import { openHippoDb, closeHippoDb } from './db.js';
|
|
|
14
14
|
import { RejectedValueError, checkRejectionGuard } from './rejection.js';
|
|
15
15
|
import { loadConfig } from './config.js';
|
|
16
16
|
import { vetSecrets } from './secret-detect.js';
|
|
17
|
+
import { log } from './log.js';
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Shared core: dedup + write
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
@@ -48,7 +49,7 @@ export function importEntries(chunks, source, tags, options) {
|
|
|
48
49
|
const trimmed = vetSecrets(original, allTags, true).content;
|
|
49
50
|
const wasRedacted = trimmed !== original;
|
|
50
51
|
if (trimmed.length > 1000) {
|
|
51
|
-
|
|
52
|
+
log.warn(`imported memory truncated from ${trimmed.length} to 1000 chars`);
|
|
52
53
|
}
|
|
53
54
|
const chunk = trimmed.slice(0, 1000);
|
|
54
55
|
// Skip empty or too-short chunks
|
|
@@ -584,7 +585,8 @@ function realpathOrResolve(p) {
|
|
|
584
585
|
try {
|
|
585
586
|
return fs.realpathSync.native(p);
|
|
586
587
|
}
|
|
587
|
-
catch {
|
|
588
|
+
catch (err) {
|
|
589
|
+
log.debug(`import: realpath fell back to resolve for ${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
588
590
|
return path.resolve(p);
|
|
589
591
|
}
|
|
590
592
|
}
|
|
@@ -890,7 +892,8 @@ function parseJsonArrayLoose(value) {
|
|
|
890
892
|
const parsed = JSON.parse(value);
|
|
891
893
|
return Array.isArray(parsed) ? parsed.filter(isJsonString) : [];
|
|
892
894
|
}
|
|
893
|
-
catch {
|
|
895
|
+
catch (err) {
|
|
896
|
+
log.debug(`import: unreadable tags_json read as no tags: ${err instanceof Error ? err.message : String(err)}`);
|
|
894
897
|
return [];
|
|
895
898
|
}
|
|
896
899
|
}
|
package/dist/incidents.js
CHANGED
package/dist/judgment.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Regex picks WHAT is a candidate; it cannot say what is worth keeping, so
|
|
3
3
|
* every captured memory currently lands on a flat schema_fit of 0.5. */
|
|
4
4
|
import { fetchWithRetry } from './http-retry.js';
|
|
5
|
+
import { log } from './log.js';
|
|
5
6
|
const ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
|
|
6
7
|
const DEFAULT_MODEL = 'jev-1.13.0';
|
|
7
8
|
const MAX_CONCURRENCY = 8;
|
|
@@ -66,7 +67,8 @@ async function post(content, opts) {
|
|
|
66
67
|
}),
|
|
67
68
|
}, { timeoutMs: JUDGE_TIMEOUT_MS, fetchFn: opts.fetcher });
|
|
68
69
|
}
|
|
69
|
-
catch {
|
|
70
|
+
catch (err) {
|
|
71
|
+
log.debug(`judge: request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
70
72
|
return null;
|
|
71
73
|
}
|
|
72
74
|
return res.ok ? res : null;
|
|
@@ -87,7 +89,8 @@ export async function judge(content, opts) {
|
|
|
87
89
|
// optional-chained and range-checked before use, so a lie here returns null.
|
|
88
90
|
data = await res.json();
|
|
89
91
|
}
|
|
90
|
-
catch {
|
|
92
|
+
catch (err) {
|
|
93
|
+
log.debug(`judge: unreadable response: ${err instanceof Error ? err.message : String(err)}`);
|
|
91
94
|
return null;
|
|
92
95
|
}
|
|
93
96
|
const durable = data.answers?.durable?.noul;
|