hippo-memory 1.58.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 (78) hide show
  1. package/README.md +13 -1
  2. package/dist/agent-memories/apply.d.ts +1 -1
  3. package/dist/agent-memories/legacy.js +4 -1
  4. package/dist/api.d.ts +2 -0
  5. package/dist/api.js +25 -9
  6. package/dist/audit.d.ts +1 -1
  7. package/dist/audit.js +3 -0
  8. package/dist/autolearn.js +5 -2
  9. package/dist/capture.js +8 -6
  10. package/dist/cli/output.d.ts +3 -0
  11. package/dist/cli/output.js +7 -0
  12. package/dist/cli/projects.d.ts +4 -0
  13. package/dist/cli/projects.js +90 -0
  14. package/dist/cli/shared.js +14 -8
  15. package/dist/cli/sleep.js +5 -3
  16. package/dist/cli.d.ts +356 -0
  17. package/dist/cli.js +1587 -1404
  18. package/dist/compaction-record.js +7 -6
  19. package/dist/config.js +12 -11
  20. package/dist/connectors/github/cli-impl.js +1 -0
  21. package/dist/consolidate.js +16 -4
  22. package/dist/dag.js +11 -9
  23. package/dist/dashboard.js +4 -2
  24. package/dist/db.js +16 -3
  25. package/dist/dedupe.js +1 -1
  26. package/dist/delivery-recorder.js +1 -0
  27. package/dist/doctor.js +24 -0
  28. package/dist/dormant.d.ts +2 -2
  29. package/dist/dormant.js +1 -0
  30. package/dist/embedding-provider.js +3 -2
  31. package/dist/embeddings.js +10 -7
  32. package/dist/extract.js +20 -19
  33. package/dist/graph.js +3 -8
  34. package/dist/handoff.js +3 -0
  35. package/dist/hooks.js +3 -0
  36. package/dist/importers.js +6 -3
  37. package/dist/incidents.js +1 -0
  38. package/dist/judgment.js +5 -2
  39. package/dist/mcp/server.d.ts +5 -0
  40. package/dist/mcp/server.js +32 -13
  41. package/dist/memory.d.ts +5 -3
  42. package/dist/processes.js +1 -0
  43. package/dist/project-identity.d.ts +1 -1
  44. package/dist/project-identity.js +9 -4
  45. package/dist/project-merge.d.ts +52 -0
  46. package/dist/project-merge.js +168 -0
  47. package/dist/raw-archive-mirror-cleanup.js +2 -1
  48. package/dist/recall-scope.d.ts +3 -3
  49. package/dist/recall-scope.js +5 -4
  50. package/dist/recall-trace.d.ts +2 -2
  51. package/dist/recall-trace.js +10 -14
  52. package/dist/refine-llm.js +18 -10
  53. package/dist/rerankers/clef.d.ts +29 -0
  54. package/dist/rerankers/clef.js +223 -0
  55. package/dist/rerankers/cross-encoder.js +6 -4
  56. package/dist/rerankers/index.js +3 -0
  57. package/dist/rerankers/jev.d.ts +14 -1
  58. package/dist/rerankers/jev.js +30 -20
  59. package/dist/rerankers/llm.js +3 -3
  60. package/dist/rerankers/types.d.ts +16 -0
  61. package/dist/same-text.d.ts +2 -0
  62. package/dist/same-text.js +4 -0
  63. package/dist/scheduler.js +1 -0
  64. package/dist/search.js +4 -2
  65. package/dist/secret-detect.js +1 -0
  66. package/dist/server.d.ts +6 -1
  67. package/dist/server.js +38 -15
  68. package/dist/shared.js +21 -19
  69. package/dist/stdin.js +1 -0
  70. package/dist/store.d.ts +33 -1
  71. package/dist/store.js +123 -22
  72. package/dist/token-ledger.js +1 -0
  73. package/dist/version.d.ts +1 -1
  74. package/dist/version.js +1 -1
  75. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  76. package/extensions/openclaw-plugin/package.json +1 -1
  77. package/openclaw.plugin.json +1 -1
  78. package/package.json +6 -2
package/dist/store.js CHANGED
@@ -27,6 +27,7 @@ import { checkRejectionGuard, RejectedValueError, rejectionDigest, normalizeValu
27
27
  // is the standard safe mutual-function-reference shape under NodeNext ESM.
28
28
  import { archiveRawMemory } from './raw-archive.js';
29
29
  import { insertDormantRow } from './dormant.js';
30
+ import { log } from './log.js';
30
31
  /**
31
32
  * Emit an audit event for a mutation against `db`. Wrapped so a broken audit
32
33
  * log can never crash the surrounding mutation — the SQLite store is still the
@@ -58,7 +59,7 @@ export function auditRejectionRefusal(db, err, actor) {
58
59
  audit(db, 'reject_refusal', err.entryId, { digest: err.digest, reason: err.reason }, actor, err.tenantId);
59
60
  }
60
61
  const INDEX_VERSION = 3;
61
- const MEMORY_SELECT_COLUMNS = `id, created, last_retrieved, retrieval_count, strength, half_life_days, layer, tags_json, emotional_valence, schema_fit, source, outcome_score, outcome_positive, outcome_negative, conflicts_with_json, pinned, confidence, content, parents_json, starred, trace_outcome, source_session_id, valid_from, superseded_by, extracted_from, dag_level, dag_parent_id, kind, scope, owner, artifact_ref, tenant_id, origin_project, descendant_count, earliest_at, latest_at, summary_dirty, last_rebuilt_at, rebuild_count, dag_level_3_built_at`;
62
+ export const MEMORY_SELECT_COLUMNS = `id, created, last_retrieved, retrieval_count, strength, half_life_days, layer, tags_json, emotional_valence, schema_fit, source, outcome_score, outcome_positive, outcome_negative, conflicts_with_json, pinned, confidence, content, parents_json, starred, trace_outcome, source_session_id, valid_from, superseded_by, extracted_from, dag_level, dag_parent_id, kind, scope, owner, artifact_ref, tenant_id, origin_project, descendant_count, earliest_at, latest_at, summary_dirty, last_rebuilt_at, rebuild_count, dag_level_3_built_at`;
62
63
  // F1 (v1.7.0): qualified-and-aliased columns for the FTS join in
63
64
  // loadSearchRows. Every column is `m.<col> AS <col>` so rowToEntry's
64
65
  // unqualified field reads keep working unchanged. The trailing
@@ -180,9 +181,9 @@ export function serializeEntry(entry) {
180
181
  if (tenantId !== 'default') {
181
182
  frontmatter['tenant_id'] = tenantId;
182
183
  }
183
- // v39: origin_project '' (user-global) is meaningful and must round-trip;
184
- // only undefined/null (legacy/unstamped) is omitted.
185
- if (entry.origin_project !== undefined && entry.origin_project !== null) {
184
+ // v39: '' (user-global) and null (unknown, hidden by default) must both round-trip;
185
+ // only undefined (unstamped) is omitted, so a rebuild stamps nothing it can read.
186
+ if (entry.origin_project !== undefined) {
186
187
  frontmatter['origin_project'] = entry.origin_project;
187
188
  }
188
189
  // Spread into a fresh object literal: dumpFrontmatter's Record<string,
@@ -241,7 +242,7 @@ export function deserializeEntry(raw) {
241
242
  owner: data['owner'] === null || data['owner'] === undefined ? null : String(data['owner']),
242
243
  artifact_ref: data['artifact_ref'] === null || data['artifact_ref'] === undefined ? null : String(data['artifact_ref']),
243
244
  tenantId: data['tenant_id'] === null || data['tenant_id'] === undefined ? 'default' : String(data['tenant_id']),
244
- origin_project: data['origin_project'] === null || data['origin_project'] === undefined ? null : String(data['origin_project']),
245
+ origin_project: !('origin_project' in data) ? undefined : data['origin_project'] === null ? null : String(data['origin_project']),
245
246
  };
246
247
  }
247
248
  function normalizeStringArray(value) {
@@ -312,7 +313,8 @@ function parseJsonArray(raw) {
312
313
  const parsed = JSON.parse(raw);
313
314
  return Array.isArray(parsed) ? parsed.map((item) => String(item)) : [];
314
315
  }
315
- catch {
316
+ catch (err) {
317
+ log.debug(`store: corrupt JSON array column read as empty: ${err instanceof Error ? err.message : String(err)}`);
316
318
  return [];
317
319
  }
318
320
  }
@@ -346,7 +348,8 @@ function parseJsonObject(raw) {
346
348
  }
347
349
  return {};
348
350
  }
349
- catch {
351
+ catch (err) {
352
+ log.debug(`store: corrupt JSON object column read as empty: ${err instanceof Error ? err.message : String(err)}`);
350
353
  return {};
351
354
  }
352
355
  }
@@ -748,12 +751,12 @@ export function purgeMirrorBestEffort(hippoRoot, id, isRaw, logPrefix) {
748
751
  catch (secondErr) {
749
752
  const msg = secondErr instanceof Error ? secondErr.message : String(secondErr);
750
753
  if (isRaw) {
751
- console.error(`${logPrefix}: mirror cleanup failed for ${id} (will retry via reaper on next open): ${msg}`);
754
+ log.error(`${logPrefix}: mirror cleanup failed for ${id} (will retry via reaper on next open): ${msg}`);
752
755
  }
753
756
  else {
754
757
  const leftover = getExistingEntryMirrorPaths(hippoRoot, id);
755
758
  const pathsNote = leftover.length > 0 ? leftover.join(', ') : `${id}.md (path unresolved)`;
756
- console.error(`${logPrefix}: mirror cleanup failed for ${id} - no automatic retry exists for this file, ` +
759
+ log.error(`${logPrefix}: mirror cleanup failed for ${id} - no automatic retry exists for this file, ` +
757
760
  `delete it manually: ${pathsNote} (${msg})`);
758
761
  }
759
762
  return false;
@@ -809,7 +812,7 @@ function bootstrapLegacyStore(db, hippoRoot) {
809
812
  }
810
813
  }
811
814
  if (rejectedCount > 0) {
812
- console.error(`bootstrapLegacyStore: skipped ${rejectedCount} rejected value(s) found in legacy mirrors`);
815
+ log.warn(`bootstrapLegacyStore: skipped ${rejectedCount} rejected value(s) found in legacy mirrors`);
813
816
  }
814
817
  const legacyIndex = loadLegacyIndexFile(hippoRoot);
815
818
  setMeta(db, 'last_retrieval_ids', JSON.stringify(legacyIndex.last_retrieval_ids ?? []));
@@ -874,7 +877,8 @@ function loadLegacyIndexFile(hippoRoot) {
874
877
  // that violates the shape falls through to the catch block's fallback.
875
878
  return JSON.parse(fs.readFileSync(indexPath, 'utf8'));
876
879
  }
877
- catch {
880
+ catch (err) {
881
+ log.debug(`store: unreadable index.json read as empty: ${err instanceof Error ? err.message : String(err)}`);
878
882
  return { version: 1, entries: {}, last_retrieval_ids: [], last_trace_id: null };
879
883
  }
880
884
  }
@@ -895,7 +899,8 @@ function loadLegacyStatsFile(hippoRoot) {
895
899
  // hand-edited or corrupted file even if this optimistic cast is wrong.
896
900
  return JSON.parse(fs.readFileSync(statsPath, 'utf8'));
897
901
  }
898
- catch {
902
+ catch (err) {
903
+ log.debug(`store: unreadable stats.json read as zero: ${err instanceof Error ? err.message : String(err)}`);
899
904
  return {
900
905
  total_remembered: 0,
901
906
  total_recalled: 0,
@@ -1077,7 +1082,7 @@ function mirrorBestEffort(what, write) {
1077
1082
  write();
1078
1083
  }
1079
1084
  catch (err) {
1080
- console.error(`hippo: ${what} not refreshed (${err instanceof Error ? err.message : String(err)}); the database write succeeded`);
1085
+ log.warn(`${what} not refreshed (${err instanceof Error ? err.message : String(err)}); the database write succeeded`);
1081
1086
  }
1082
1087
  }
1083
1088
  function syncMirrorFiles(hippoRoot, db) {
@@ -1174,7 +1179,7 @@ export function stampOriginProject(hippoRoot, entry) {
1174
1179
  return { ...entry, origin_project: deriveOriginProject(path.dirname(hippoRoot)) };
1175
1180
  }
1176
1181
  /**
1177
- * Import-time variant that ALSO stamps null: used only where evidence exists
1182
+ * Import-time variant for a mirror with no origin field (an explicit null stays null): used only where evidence exists
1178
1183
  * for rows that predate the origin column - the legacy-markdown bootstrap and
1179
1184
  * rebuildIndex import, which are the markdown-store equivalent of the v39 SQL
1180
1185
  * backfill. Same evidence order as the migration: the provenance source
@@ -1183,7 +1188,7 @@ export function stampOriginProject(hippoRoot, entry) {
1183
1188
  * owning project instead of becoming user-global (codex gating round 3 P1).
1184
1189
  */
1185
1190
  function stampOriginProjectForImport(hippoRoot, entry) {
1186
- if (entry.origin_project !== undefined && entry.origin_project !== null)
1191
+ if (entry.origin_project !== undefined)
1187
1192
  return entry;
1188
1193
  const fromSource = originFromSource(entry.source);
1189
1194
  return {
@@ -1332,7 +1337,7 @@ export function strengthenRetrieved(hippoRoot, ids, tenantId) {
1332
1337
  db?.exec('ROLLBACK');
1333
1338
  }
1334
1339
  catch { /* already rolled back; keep the original error */ }
1335
- console.error(`hippo: retrieval stats not saved (${error instanceof Error ? error.message : String(error)})`);
1340
+ log.warn(`retrieval stats not saved (${error instanceof Error ? error.message : String(error)})`);
1336
1341
  found.clear();
1337
1342
  }
1338
1343
  finally {
@@ -1772,7 +1777,7 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
1772
1777
  }
1773
1778
  db.exec('COMMIT');
1774
1779
  if (batchRejectedSkips > 0) {
1775
- console.error(`batchWriteAndDelete: skipped ${batchRejectedSkips} write(s) whose content matches a rejected value (tombstone hit during the batch transaction)`);
1780
+ log.warn(`batchWriteAndDelete: skipped ${batchRejectedSkips} write(s) whose content matches a rejected value (tombstone hit during the batch transaction)`);
1776
1781
  }
1777
1782
  // Sync mirrors once after all DB writes. Entries skipped above were
1778
1783
  // never inserted — writing their markdown mirror would resurrect the
@@ -1853,6 +1858,11 @@ export function loadContentsWithTag(hippoRoot, tenantId, tag) {
1853
1858
  closeHippoDb(db);
1854
1859
  }
1855
1860
  }
1861
+ const AMBIENT_SCOPED = 'superseded_by IS NULL AND tenant_id = ?';
1862
+ /** Exported so the plan test runs the exact SQL; idx_memories_pinned (db.ts v51) serves it. */
1863
+ export const AMBIENT_PINNED_WHERE = `pinned = 1 AND ${AMBIENT_SCOPED} ORDER BY created ASC, id ASC`;
1864
+ /** Exported so the plan test runs the exact SQL; idx_memories_created_drift (db.ts v51) serves it. */
1865
+ export const AMBIENT_DRIFT_SQL = `SELECT 1 FROM memories WHERE ${AMBIENT_SCOPED} AND (length(created) <> 24 OR created NOT LIKE '%Z') LIMIT 1`;
1856
1866
  // The pins plus the `recentNeeded` newest rows that pass `admit`, for ambient
1857
1867
  // injection. One connection; `recall` piggybacks the Z1 FTS query on it too.
1858
1868
  export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit, recall) {
@@ -1864,14 +1874,14 @@ export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit,
1864
1874
  // SAFETY: every `where` below starts from MEMORY_SELECT_COLUMNS' table.
1865
1875
  const run = (where, params) => db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE ${where}`).all(...params).map(rowToEntry);
1866
1876
  const byId = new Map();
1867
- const scoped = 'superseded_by IS NULL AND tenant_id = ?';
1868
- for (const e of run(`pinned = 1 AND ${scoped} ORDER BY created ASC, id ASC`, [tenantId])) {
1877
+ const scoped = AMBIENT_SCOPED;
1878
+ for (const e of run(AMBIENT_PINNED_WHERE, [tenantId])) {
1869
1879
  if (admit(e))
1870
1880
  byId.set(e.id, e);
1871
1881
  }
1872
1882
  if (needed > 0) {
1873
1883
  // Text order is chronological only for canonical UTC ISO (memory.ts).
1874
- const drifted = db.prepare(`SELECT 1 FROM memories WHERE ${scoped} AND (length(created) <> 24 OR created NOT LIKE '%Z') LIMIT 1`).get(tenantId) !== undefined;
1884
+ const drifted = db.prepare(AMBIENT_DRIFT_SQL).get(tenantId) !== undefined;
1875
1885
  // `id DESC` mirrors getContext's comparator, not loadFreshRawMemories'
1876
1886
  // cross-ingest-stable order: that would change what the hook injects.
1877
1887
  const window = Math.max(needed * 4, 32);
@@ -1903,6 +1913,97 @@ export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit,
1903
1913
  closeHippoDb(db);
1904
1914
  }
1905
1915
  }
1916
+ // calculateStrength's decay exponent with its reward factor, pins first: the strongest rows, without scoring each one in JS.
1917
+ const DECAY_RANK_SQL = `pinned DESC,
1918
+ CASE WHEN half_life_days > 0 THEN (julianday(?) - julianday(last_retrieved))
1919
+ / (half_life_days * (1.0 + 0.5 * (COALESCE(outcome_positive, 0) - COALESCE(outcome_negative, 0))
1920
+ / (COALESCE(outcome_positive, 0) + COALESCE(outcome_negative, 0) + 1.0))) END ASC NULLS LAST,
1921
+ id ASC`;
1922
+ /** Live tenant rows passing `filter`, at most `filter.cap`, in loadAllEntries' order; below the cap, every such row. */
1923
+ export function loadContextCandidates(hippoRoot, tenantId, filter) {
1924
+ const where = ['tenant_id = ?', `COALESCE(superseded_by, '') = ''`];
1925
+ const params = [tenantId];
1926
+ if (filter.exactScope) {
1927
+ where.push('scope = ?');
1928
+ params.push(filter.exactScope);
1929
+ }
1930
+ else {
1931
+ // isRestrictedScope's rule; NOT LIKE folds ASCII case as its /:private:/i does.
1932
+ where.push(`(scope IS NULL OR (scope NOT IN (${RECALL_DEFAULT_DENY_SCOPES.map(() => '?').join(', ')}) AND scope NOT LIKE '%:private:%'))`);
1933
+ params.push(...RECALL_DEFAULT_DENY_SCOPES);
1934
+ }
1935
+ if (filter.project !== undefined) {
1936
+ where.push(`(origin_project = '' OR origin_project = ?)`);
1937
+ params.push(filter.project);
1938
+ }
1939
+ const db = openStore(hippoRoot);
1940
+ try {
1941
+ // SAFETY: the outer SELECT names exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set; the rank sort carries ids only.
1942
+ const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (
1943
+ SELECT id FROM memories WHERE ${where.join(' AND ')} ORDER BY ${DECAY_RANK_SQL} LIMIT ?
1944
+ ) ORDER BY created ASC, id ASC`).all(...params, filter.now.toISOString(), Math.max(0, Math.trunc(filter.cap)));
1945
+ return rows.map(rowToEntry);
1946
+ }
1947
+ finally {
1948
+ closeHippoDb(db);
1949
+ }
1950
+ }
1951
+ /** Every tenant row as a StrengthRow, for whole-store health numbers; defaults match rowToEntry's. */
1952
+ export function loadStrengthRows(hippoRoot, tenantId) {
1953
+ const db = openStore(hippoRoot);
1954
+ try {
1955
+ // SAFETY: rows' shape matches the columns named in the SELECT below.
1956
+ const rows = db.prepare(`SELECT pinned, created, last_retrieved, half_life_days, retrieval_count, emotional_valence, outcome_positive, outcome_negative, tags_json
1957
+ FROM memories WHERE tenant_id = ?`).all(tenantId);
1958
+ return rows.map((row) => ({
1959
+ pinned: Boolean(row.pinned),
1960
+ created: row.created,
1961
+ last_retrieved: row.last_retrieved,
1962
+ half_life_days: Number(row.half_life_days ?? 7),
1963
+ retrieval_count: Number(row.retrieval_count ?? 0),
1964
+ emotional_valence: row.emotional_valence ?? 'neutral',
1965
+ outcome_positive: Number(row.outcome_positive ?? 0),
1966
+ outcome_negative: Number(row.outcome_negative ?? 0),
1967
+ tags: parseJsonArray(row.tags_json),
1968
+ }));
1969
+ }
1970
+ finally {
1971
+ closeHippoDb(db);
1972
+ }
1973
+ }
1974
+ /** Text and source of tenant rows holding any of `words`; a row equal to a text apart from spacing holds its every word. */
1975
+ export function loadTextsHoldingWords(hippoRoot, tenantId, words) {
1976
+ const unique = [...new Set(words)];
1977
+ const out = [];
1978
+ const db = openStore(hippoRoot);
1979
+ try {
1980
+ // Chunked so one statement stays far under SQLite's bound-parameter limit.
1981
+ for (let i = 0; i < unique.length; i += 200) {
1982
+ const chunk = unique.slice(i, i + 200);
1983
+ // SAFETY: rows' shape matches the two columns named in the SELECT below.
1984
+ const rows = db.prepare(`SELECT content, source FROM memories WHERE tenant_id = ? AND (${chunk.map(() => 'instr(content, ?) > 0').join(' OR ')})`).all(tenantId, ...chunk);
1985
+ for (const row of rows)
1986
+ out.push({ content: row.content, source: row.source ?? 'cli' });
1987
+ }
1988
+ return out;
1989
+ }
1990
+ finally {
1991
+ closeHippoDb(db);
1992
+ }
1993
+ }
1994
+ /** Row count and newest `created` per source, for peer listings that need no row; all tenants when `tenantId` is absent. */
1995
+ export function tallySources(hippoRoot, tenantId) {
1996
+ const db = openStore(hippoRoot);
1997
+ try {
1998
+ const where = tenantId !== undefined ? 'WHERE tenant_id = ?' : '';
1999
+ // SAFETY: rows' shape matches the four aliased columns in the SELECT below.
2000
+ return db.prepare(`SELECT COALESCE(source, 'cli') AS source, COUNT(*) AS count, MAX(created) AS latest, MIN(created || char(31) || id) AS first
2001
+ FROM memories ${where} GROUP BY COALESCE(source, 'cli')`).all(...(tenantId !== undefined ? [tenantId] : []));
2002
+ }
2003
+ finally {
2004
+ closeHippoDb(db);
2005
+ }
2006
+ }
1906
2007
  /**
1907
2008
  * Load likely search candidates directly from SQLite.
1908
2009
  * Uses FTS5 when available, falls back to LIKE matching, then full-store fallback.
@@ -2019,7 +2120,7 @@ export function rebuildIndex(hippoRoot) {
2019
2120
  }
2020
2121
  }
2021
2122
  if (rejectedCount > 0) {
2022
- console.error(`rebuildIndex: skipped ${rejectedCount} rejected value(s) found in legacy mirrors`);
2123
+ log.warn(`rebuildIndex: skipped ${rejectedCount} rejected value(s) found in legacy mirrors`);
2023
2124
  }
2024
2125
  db.exec('COMMIT');
2025
2126
  }
@@ -3279,7 +3380,7 @@ export function applyRebuildResult(hippoRoot, summary, patch) {
3279
3380
  // tool) is the wrong one here; a direct audit() call is correct and
3280
3381
  // commits with the rest of this savepoint.
3281
3382
  audit(db, 'reject_refusal', summary.id, { digest: tombstone.digest, reason: tombstone.reason }, patch.actor, summary.tenantId);
3282
- console.error(`applyRebuildResult: refused rebuild content for ${summary.id} — matches a rejected value ` +
3383
+ log.warn(`applyRebuildResult: refused rebuild content for ${summary.id} — matches a rejected value ` +
3283
3384
  `(digest ${tombstone.digest.slice(0, 12)}...); metadata updated, content unchanged`);
3284
3385
  }
3285
3386
  if (changed) {
@@ -161,6 +161,7 @@ function parseHookPayload(stdinText) {
161
161
  return isJsonObject(payload) ? payload : null;
162
162
  }
163
163
  catch {
164
+ // Malformed is one of the null cases the docblock names.
164
165
  return null;
165
166
  }
166
167
  }
package/dist/version.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
17
17
  * any packager that drops .json files.
18
18
  */
19
- export declare const PACKAGE_VERSION = "1.58.0";
19
+ export declare const PACKAGE_VERSION = "1.60.0";
20
20
  /** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
21
21
  export declare function compareSemver(a: string, b: string): number;
22
22
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
17
17
  * any packager that drops .json files.
18
18
  */
19
- export const PACKAGE_VERSION = '1.58.0';
19
+ export const PACKAGE_VERSION = '1.60.0';
20
20
  /** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
21
21
  export function compareSemver(a, b) {
22
22
  const parse = (v) => {
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Memory for AI agents that learns what is wrong and ranks it down. Injects context at session start and captures errors.",
5
- "version": "1.58.0",
5
+ "version": "1.60.0",
6
6
 
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.58.0",
3
+ "version": "1.60.0",
4
4
  "type": "module",
5
5
  "description": "Hippo Memory plugin for OpenClaw - biologically-inspired agent memory",
6
6
  "main": "index.ts",
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Memory for AI agents that learns what is wrong and ranks it down. Injects context at session start and captures errors.",
5
- "version": "1.58.0",
5
+ "version": "1.60.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.58.0",
3
+ "version": "1.60.0",
4
4
  "description": "Memory for AI agents that learns what is wrong and ranks it down. MCP server, hooks for Claude Code, OpenCode and Codex, AGENTS.md instructions for Codex, Cursor, OpenClaw and Pi. SQLite, zero runtime deps.",
5
5
  "mcpName": "io.github.kitfunso/hippo-memory",
6
6
  "type": "module",
@@ -30,10 +30,12 @@
30
30
  "scripts": {
31
31
  "baseline": "npm run build && npm test",
32
32
  "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc && tsc -p tsconfig.benchmarks.json && tsc -p tsconfig.extensions.json",
33
+ "typecheck:tests": "tsc -p tsconfig.tests.json",
33
34
  "dev": "tsc --watch",
34
35
  "pretest": "npm run build",
35
36
  "test": "vitest run",
36
37
  "test:watch": "vitest",
38
+ "test:coverage": "vitest run --coverage",
37
39
  "test:delivery-ledger": "vitest run tests/delivery-ledger",
38
40
  "lint": "oxlint",
39
41
  "sbom": "node scripts/sbom.mjs",
@@ -90,9 +92,11 @@
90
92
  "devDependencies": {
91
93
  "@oxlint/plugins": "^1.78.0",
92
94
  "@types/node": "^22.16.0",
95
+ "@vitest/coverage-v8": "^5.0.3",
93
96
  "oxlint": "^1.78.0",
94
97
  "typescript": "^5.4.0",
95
- "vitest": "^3.2.6"
98
+ "vite": "^8.3.2",
99
+ "vitest": "^5.0.3"
96
100
  },
97
101
  "peerDependenciesMeta": {
98
102
  "@xenova/transformers": {