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.
Files changed (59) hide show
  1. package/dist/api.d.ts +2 -0
  2. package/dist/api.js +25 -9
  3. package/dist/audit.js +1 -0
  4. package/dist/autolearn.js +5 -2
  5. package/dist/capture.js +8 -6
  6. package/dist/cli.d.ts +355 -0
  7. package/dist/cli.js +1236 -1081
  8. package/dist/compaction-record.js +7 -6
  9. package/dist/config.js +12 -11
  10. package/dist/connectors/github/cli-impl.js +1 -0
  11. package/dist/consolidate.js +4 -3
  12. package/dist/dag.js +7 -7
  13. package/dist/dashboard.js +4 -2
  14. package/dist/db.js +16 -3
  15. package/dist/delivery-recorder.js +1 -0
  16. package/dist/doctor.js +1 -0
  17. package/dist/dormant.js +1 -0
  18. package/dist/embedding-provider.js +3 -2
  19. package/dist/embeddings.js +10 -7
  20. package/dist/extract.js +4 -3
  21. package/dist/graph.js +3 -8
  22. package/dist/handoff.js +3 -0
  23. package/dist/hooks.js +3 -0
  24. package/dist/importers.js +6 -3
  25. package/dist/incidents.js +1 -0
  26. package/dist/judgment.js +5 -2
  27. package/dist/mcp/server.d.ts +5 -0
  28. package/dist/mcp/server.js +32 -13
  29. package/dist/memory.d.ts +5 -3
  30. package/dist/processes.js +1 -0
  31. package/dist/project-identity.d.ts +1 -1
  32. package/dist/project-identity.js +9 -4
  33. package/dist/raw-archive-mirror-cleanup.js +2 -1
  34. package/dist/recall-trace.d.ts +2 -2
  35. package/dist/recall-trace.js +10 -14
  36. package/dist/refine-llm.js +18 -10
  37. package/dist/rerankers/clef.d.ts +2 -2
  38. package/dist/rerankers/clef.js +68 -27
  39. package/dist/rerankers/cross-encoder.js +6 -4
  40. package/dist/rerankers/jev.d.ts +3 -1
  41. package/dist/rerankers/jev.js +20 -15
  42. package/dist/rerankers/llm.js +3 -3
  43. package/dist/same-text.d.ts +2 -0
  44. package/dist/same-text.js +4 -0
  45. package/dist/scheduler.js +1 -0
  46. package/dist/search.js +4 -2
  47. package/dist/secret-detect.js +1 -0
  48. package/dist/server.js +29 -12
  49. package/dist/shared.js +21 -19
  50. package/dist/stdin.js +1 -0
  51. package/dist/store.d.ts +33 -1
  52. package/dist/store.js +117 -16
  53. package/dist/token-ledger.js +1 -0
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  57. package/extensions/openclaw-plugin/package.json +1 -1
  58. package/openclaw.plugin.json +1 -1
  59. 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
- console.error(`hippo post-compact: record step failed: ${errorMessage(err)}`);
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
- console.error(`hippo post-compact: spool failed: ${errorMessage(spoolErr)}`);
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
- console.error(`hippo post-compact: items step failed: ${errorMessage(err)}`);
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
- console.error(`Warning: config.json's "deliveryLedger" must be an object like {"enabled": true} ` +
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
- console.error(`Warning: config.json's "pilot" must be an object like {"holdoutRateBp": 2000}, an integer from 0 to 10000 ` +
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
- console.error(`Warning: config.json's "agentMemories.tools" must be a list of tool ids like ["claude-code", "codex"] ` +
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
- console.error(`Warning: config.json's "memoryValue" must be an object like {"enabled": true} ` +
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
- console.error(`Warning: config.json's "dormant" must be an object like {"enabled": false} ` +
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
- console.error(`Warning: config.json's "dormant.enabled" must be true or false ` +
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
- console.error(`Warning: config.json's "dormant.retentionDays" must be a number of days, 0 or more ` +
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
- console.error(`Warning: config.json's "churnStaleness" must be an object like {"enabled": true} ` +
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
- console.error(`Warning: config.json's "churnStaleness.enabled" must be true or false ` +
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
- console.error(`Warning: config.json's "defaultHalfLifeDays" must be a number of days above 0 ` +
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
- console.error(`Warning: failed to parse ${configPath}: ${err instanceof Error ? err.message : err}`);
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)');
@@ -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
- console.error(`consolidate: skipped ${tracesSkippedRejected} auto-promoted trace(s) whose content matches a rejected value`);
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
- console.error(`consolidate ${phase}: ${msg}`);
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
- console.error(`consolidate: skipped ${mergesSkippedRejected} merge(s) whose content matches a rejected value`);
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 { redactSecrets } from './secret-detect.js';
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}. ${redactSecrets(f)}`).join('\n');
51
+ const factsBlock = factContents.map((f, i) => `${i + 1}. ${redactSecretsStrict(f)}`).join('\n');
51
52
  const prompt = DAG_SUMMARY_PROMPT
52
- .replace('{label}', redactSecrets(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
- console.error(`[buildDag] cluster "${cluster.label}" skipped: summary matches a rejected value`);
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
- // eslint-disable-next-line no-console
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
- console.error(`[buildEntityProfiles] cluster "${cluster.label}" skipped: profile matches a rejected value`);
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
- console.error('Dashboard request failed:', err);
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 = 50;
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
- console.error('openHippoDb: cleanupArchivedMirrors failed (non-fatal):', cleanupErr);
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
- console.error(`hippo: full-text index unavailable (${err instanceof Error ? err.message : String(err)}); search falls back to slower LIKE matching`);
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
@@ -36,6 +36,7 @@ function readJson(file) {
36
36
  return JSON.parse(fs.readFileSync(file, 'utf8'));
37
37
  }
38
38
  catch {
39
+ // A missing or corrupt file is the finding doctor reports, so null is the answer.
39
40
  return null;
40
41
  }
41
42
  }
package/dist/dormant.js CHANGED
@@ -36,6 +36,7 @@ function parseSnapshot(row) {
36
36
  return entry;
37
37
  }
38
38
  catch {
39
+ // A row that will not parse is out of shape, so it is not restorable.
39
40
  return null;
40
41
  }
41
42
  }
@@ -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 { redactSecrets } from './secret-detect.js';
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(redactSecrets), role)),
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
  }
@@ -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
- console.error(`hippo: embedding failed (${source}): ${message}. Memories are stored without embeddings until this is fixed.`);
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
- console.error(`hippo: skipped embedding ${entry.id} (${err instanceof Error ? err.message : String(err)}); run 'hippo embed' to backfill`);
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 { redactSecrets } from './secret-detect.js';
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 + redactSecrets(text) }],
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
- console.error(`storeExtractedFacts: skipped ${rejected} rejected value(s)`);
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
- // SAFETY: this is a best-effort log message only; property access on any
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
- // SAFETY: this is a best-effort log message only; property access on any
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
- console.error(`Warning: imported memory truncated from ${trimmed.length} to 1000 chars`);
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
@@ -48,6 +48,7 @@ function parseLinkedMemoryIds(raw) {
48
48
  return [];
49
49
  }
50
50
  catch {
51
+ // A malformed list column reads as empty instead of failing the incident read.
51
52
  return [];
52
53
  }
53
54
  }
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;