hippo-memory 1.53.0 → 1.53.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,6 +22,8 @@ npm install -g hippo-memory && hippo init
22
22
 
23
23
  Setting up every git repo under a folder in one go is a second step. The [Quick start](#quick-start) says what it changes, then gives the command.
24
24
 
25
+ Package installation alone does not enable automatic preservation on every agent. Complete the documented setup and required host trust; capture and compaction coverage depend on the integration. See [automatic-save coverage](#does-installation-automatically-save-before-compaction).
26
+
25
27
  Having an AI agent install it? Point it at [llms-install.md](llms-install.md): it installs, wires hippo into the agents it finds, and verifies with `hippo doctor`.
26
28
 
27
29
  ```
@@ -909,7 +911,7 @@ The rows from where the data lives to the graph were checked against each tool's
909
911
  | Feature | Hippo | [MemPalace](https://github.com/milla-jovovich/mempalace) | [Mem0](https://github.com/mem0ai/mem0) | [Basic Memory](https://github.com/basicmachines-co/basic-memory) | [gbrain](https://hermesatlas.com/projects/garrytan/gbrain) | [Zep](https://www.getzep.com/) | [Letta](https://github.com/letta-ai/letta-code) | [Cognee](https://www.cognee.ai/) | [Memoria](https://github.com/matrixorigin/Memoria) | [EverMind](https://evermind.ai/) |
910
912
  |---------|-------|-----------|------|-------------|--------|-----|-------|--------|---------|----------|
911
913
  | Where your data lives | Your machine or your server (SQLite) | Your machine (ChromaDB by default) | Where you run it, or Mem0's cloud | Your machine (Markdown files); cloud optional | Your machine (PGLite) or your Postgres | Zep's cloud (your own cloud on Enterprise) | Your machine; cloud backup with /login | Your machine by default; Cognee Cloud optional | Memoria Cloud, or self-hosted (Docker or embedded) | Your machine by default; EverOS Cloud optional |
912
- | Managed multi-user service | No (self-hosted, with tenants and API keys; the commercial edition adds hosted SaaS) | ? | Yes (hosted platform) | Yes (Teams) | ? (self-hosted server with OAuth) | Yes (Zep Cloud) | ? (cloud backup with /login) | Yes (Cognee Cloud) | ? (Memoria Cloud) | Yes (EverOS Cloud) |
914
+ | Managed multi-user service | No (self-hosted, with tenants and API keys; hosted SaaS is planned for the commercial edition) | ? | Yes (hosted platform) | Yes (Teams) | ? (self-hosted server with OAuth) | Yes (Zep Cloud) | ? (cloud backup with /login) | Yes (Cognee Cloud) | ? (Memoria Cloud) | Yes (EverOS Cloud) |
913
915
  | Needs an account or model key | No | No (core path) | Yes (model key; account for the platform) | No (account for the cloud) | No (keyless mode) | Yes (Zep account; Graphiti needs a model key) | Yes (your own model keys) | No (local) | ? (account for Memoria Cloud) | Yes (an LLM key, OpenRouter) |
914
916
  | License | MIT | MIT | Apache-2.0 | AGPL-3.0 | MIT | Proprietary cloud (Graphiti: Apache-2.0) | Apache-2.0 | Apache-2.0 | Apache-2.0 | Apache-2.0 (EverOS) + cloud |
915
917
  | Runtime | Node.js 22.16+, no runtime deps | Python 3.9+ (ChromaDB by default) | Python or Node.js (server: Postgres + pgvector) | Python (SQLite by default) | Bun (PGLite or Postgres + pgvector) | Managed service (Graphiti: Python + a graph database) | Node.js (npm) | Python (graph and vector stores, or Postgres) | A CLI binary + a MatrixOne database | Python (SQLite + LanceDB) |
@@ -1068,6 +1070,14 @@ Run `npm install -g hippo-memory`, then `hippo init` in the project. If the proj
1068
1070
 
1069
1071
  `hippo init` detects Claude Code, Codex, Cursor, OpenClaw, OpenCode and Pi. It installs hooks for Claude Code and OpenCode, adds 2 hooks to Codex's `hooks.json` when Codex is installed (Codex runs them once you trust them in `/hooks`), and adds instructions to an existing `AGENTS.md` for Codex, Cursor, OpenClaw and Pi. It only patches instruction files that already exist. Any MCP client can use the [MCP server](#mcp-server), and other tools can call the CLI or the HTTP API that `hippo serve` starts.
1070
1072
 
1073
+ ### Does installation automatically save before compaction?
1074
+
1075
+ Package installation alone does not enable automatic preservation on every agent. Complete the documented setup and required host trust; capture and compaction coverage depend on the integration.
1076
+
1077
+ With the Claude Code hooks or native plugin configured, PreCompact saves a derivable working-state snapshot and a compaction record; PostCompact extracts the lessons listed in the compaction summary. A snapshot, a compaction record and a useful memory are different outputs, and post-compaction extraction is not a guarantee that every earlier lesson was saved before loss. Hippo's Codex hooks currently provide prompt delivery and post-compaction re-injection, with one-time trust in `/hooks`; they install no PreCompact save hook. Codex session-end capture requires the opt-in wrapper. MCP tool access and instruction files alone do not provide automatic lifecycle capture.
1078
+
1079
+ Follow the [integration recipes](https://github.com/kitfunso/hippo-memory/tree/master/integrations) for the exact agent and mode. Automatic pre-loss preservation across all supported agents is planned under AZ4-AZ6 in the [roadmap](https://github.com/kitfunso/hippo-memory/blob/master/ROADMAP.md#current-execution-index); it is not a universal install-only capability today.
1080
+
1071
1081
  ### Can I use hippo as an MCP memory server?
1072
1082
 
1073
1083
  Yes. `hippo mcp` runs the server over stdio, and `npx -y hippo-memory mcp` runs it without a global install. Add it to the MCP config of Claude Desktop, Cursor, Windsurf (now Devin Desktop), Cline or any other client (example [above](#mcp-server)); in Claude Code, run `claude mcp add hippo-memory -- hippo mcp`. The agent gets tools such as `hippo_recall`, `hippo_remember` and `hippo_outcome`.
@@ -1125,9 +1135,9 @@ The interesting problems:
1125
1135
 
1126
1136
  ## Open source and commercial
1127
1137
 
1128
- Hippo is open core, and the line is drawn by who pays. Everything an individual developer or a self-hosted team needs is in this repository under MIT: the CLI, the MCP server, hooks, connectors, the dashboard, tenants, API keys, roles, per-key scope grants, the audit log and zero-touch memory. Code published here stays MIT and stays here.
1138
+ Hippo is open core. This repository provides the MIT core for individual developers and self-hosted teams: the CLI, the MCP server, supported hooks and adapters, connectors, the dashboard, tenants, API keys, admin/member roles, per-key scope grants and the audit log. Automatic capture and delivery depend on the configured integration; the all-agent low-touch acceptance work remains planned. Code published here stays MIT and stays here.
1129
1139
 
1130
- A commercial edition for larger companies ships as a separate package under a commercial licence from KITFUNSO LTD. It adds SSO (OIDC and SAML sign-in), SCIM, an org admin view, the pilot report and telemetry join, SIEM export of the audit log, offline licence keys, hosted SaaS, and support with an SLA. Pull requests for those features belong there, not here; see [CONTRIBUTING.md](CONTRIBUTING.md).
1140
+ The commercial edition is planned; its private repository is a scaffold, not a released enterprise product. It is intended for larger companies as a separate package under a commercial licence from KITFUNSO LTD. Planned capabilities include SSO (OIDC and SAML sign-in), SCIM, organisation/team/project policy, an org admin view, the pilot report and telemetry join, SIEM export of the audit log, offline licence keys, hosted SaaS, and support with an SLA. Pull requests implementing those commercial features belong there; see [CONTRIBUTING.md](CONTRIBUTING.md). Release availability and independently verified benefit are separate milestones in the [roadmap](https://github.com/kitfunso/hippo-memory/blob/master/ROADMAP.md#current-execution-index).
1131
1141
 
1132
1142
  ## License
1133
1143
 
package/dist/api.d.ts CHANGED
@@ -1195,7 +1195,7 @@ export interface SleepResult {
1195
1195
  * api.sleep itself will need to scope dedup / audit / delete by ctx.tenantId.
1196
1196
  *
1197
1197
  * Dedup and audit deletes each log a `forget` row with the ctx actor and a
1198
- * `metadata.reason`. Pinned, raw and kept compaction-memory rows are never auto-deleted (canAutoDelete).
1198
+ * `metadata.reason`. Pinned, raw, kept and object-backing rows are never auto-deleted (AUTOMATIC_DELETE_SQL).
1199
1199
  * dryRun previews consolidate, dedup and audit, then returns before share/ambient.
1200
1200
  */
1201
1201
  /**
package/dist/api.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { createHash } from 'node:crypto';
10
10
  import { openHippoDb, closeHippoDb } from './db.js';
11
- import { writeEntry, writeEntryDbOnly, strengthenRetrieved, stampOriginProject, writeEntryMirrors, readEntry, deleteEntry, loadRecallSearchEntries, loadEntriesByIds, loadChildrenOf, loadFreshRawMemories, loadSessionRawMemories, countSessionRawMemories, DEFAULT_SEARCH_CANDIDATE_LIMIT, removeEntryMirrors, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, loadLatestHandoff, listSessionEvents, SNAPSHOT_AMBIENT_MAX_AGE_MS, loadIndex, saveIndex, loadAllEntries, loadAmbientCandidates, updateStats, isInitialized, markSummaryDirtyInTx, auditRejectionRefusal, } from './store.js';
11
+ import { writeEntry, writeEntryDbOnly, strengthenRetrieved, stampOriginProject, writeEntryMirrors, readEntry, deleteEntry, loadRecallSearchEntries, loadEntriesByIds, loadChildrenOf, loadFreshRawMemories, loadSessionRawMemories, countSessionRawMemories, DEFAULT_SEARCH_CANDIDATE_LIMIT, removeEntryMirrors, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, loadLatestHandoff, listSessionEvents, SNAPSHOT_AMBIENT_MAX_AGE_MS, loadIndex, saveIndex, loadAllEntries, loadAmbientCandidates, updateStats, isInitialized, markSummaryDirtyInTx, auditRejectionRefusal, memoriesBackingObjects, } from './store.js';
12
12
  import { RejectedValueError } from './rejection.js';
13
13
  import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
14
14
  import { listDormantRows, readDormantSnapshot, deleteDormantRow, hasDormantRow, } from './dormant.js';
@@ -2443,7 +2443,7 @@ export async function sleep(ctx, opts = {}) {
2443
2443
  // Phase 3: Quality audit (remove junk, report warnings; a dry run skips rows earlier phases would remove).
2444
2444
  const planned = new Set(dryRun ? [...(consolidateResult.removedIds ?? []), ...dedupResult.pairs.map((p) => p.removed)] : []);
2445
2445
  const allEntries = phases.loadAllEntries(ctx.hippoRoot).filter((e) => !planned.has(e.id));
2446
- const auditOut = phases.auditMemories(allEntries);
2446
+ const auditOut = phases.auditMemories(allEntries, memoriesBackingObjects(ctx.hippoRoot));
2447
2447
  if (auditOut.issues.length > 0) {
2448
2448
  const errors = auditOut.issues.filter((i) => i.severity === 'error');
2449
2449
  const warnings = auditOut.issues.filter((i) => i.severity === 'warning');
package/dist/audit.d.ts CHANGED
@@ -14,8 +14,9 @@ export interface AuditResult {
14
14
  clean: number;
15
15
  }
16
16
  export declare const STOP_WORDS: Set<string>;
17
- export declare function auditMemory(entry: MemoryEntry): AuditIssue | null;
18
- export declare function auditMemories(entries: MemoryEntry[]): AuditResult;
17
+ export declare function auditMemory(entry: MemoryEntry, backsObject?: boolean): AuditIssue | null;
18
+ /** `backing`: ids of memories that back an object (store.memoriesBackingObjects). */
19
+ export declare function auditMemories(entries: MemoryEntry[], backing: ReadonlySet<string>): AuditResult;
19
20
  export declare function isContentWorthStoring(content: string): boolean;
20
21
  export declare const AUDIT_OPS: readonly ["remember", "recall", "promote", "supersede", "forget", "archive_raw", "auth_revoke", "auth_create", "outcome", "consolidate", "audit_prune", "summary_marked_dirty", "summary_marked_clean", "summary_rebuilt", "predict_create", "predict_close", "predict_baserate", "recall_autodebias_hint", "recall_autodebias_hint_no_class_match", "recall_autodebias_hint_tiebreak", "recall_anchor_detected_query_repeat", "recall_anchor_detected_memory_dominance", "recall_anchor_skipped_no_session", "recall_availability_detected", "decision_create", "decision_supersede", "decision_close", "incident_open", "incident_resolve", "incident_close", "process_create", "process_supersede", "process_close", "policy_create", "policy_supersede", "policy_close", "skill_create", "skill_supersede", "skill_close", "project_brief_create", "project_brief_supersede", "project_brief_close", "customer_note_create", "customer_note_supersede", "customer_note_close", "mv_rescue", "reject_value", "reject_refusal", "unreject_value", "conflict_resolve", "half_life_migrate", "dormant_restore", "auth_grant", "auth_ungrant", "quarantine", "quarantine_approve", "quarantine_reject", "agent_memory_restore", "agent_memory_set_aside"];
21
22
  export type AuditOp = (typeof AUDIT_OPS)[number];
package/dist/audit.js CHANGED
@@ -109,11 +109,11 @@ function hasNoSpecificity(text) {
109
109
  return false;
110
110
  return words.length < 8 && VAGUE_ONLY.test(text);
111
111
  }
112
- export function auditMemory(entry) {
112
+ export function auditMemory(entry, backsObject = false) {
113
113
  const issue = classifyMemory(entry);
114
- // Error means "auto-remove"; a pinned, raw or kept-for-good row is never auto-removed, so it can only warn.
115
- if (issue?.severity === 'error' && !canAutoDelete(entry)) {
116
- const why = entry.pinned ? 'pinned' : entry.kind === 'raw' ? 'raw' : 'keep rule';
114
+ // Error means "auto-remove"; a pinned, raw, kept-for-good or object-backing row is never auto-removed, so it can only warn.
115
+ if (issue?.severity === 'error' && (!canAutoDelete(entry) || backsObject)) {
116
+ const why = entry.pinned ? 'pinned' : entry.kind === 'raw' ? 'raw' : backsObject ? 'backs an object' : 'keep rule';
117
117
  return { ...issue, severity: 'warning', reason: `${issue.reason} (${why}, kept)` };
118
118
  }
119
119
  return issue;
@@ -141,10 +141,11 @@ function classifyMemory(entry) {
141
141
  }
142
142
  return null;
143
143
  }
144
- export function auditMemories(entries) {
144
+ /** `backing`: ids of memories that back an object (store.memoriesBackingObjects). */
145
+ export function auditMemories(entries, backing) {
145
146
  const issues = [];
146
147
  for (const entry of entries) {
147
- const issue = auditMemory(entry);
148
+ const issue = auditMemory(entry, backing.has(entry.id));
148
149
  if (issue)
149
150
  issues.push(issue);
150
151
  }
package/dist/cli.js CHANGED
@@ -40,7 +40,7 @@ import { fileURLToPath } from 'node:url';
40
40
  import { execFileSync, execSync, spawn } from 'child_process';
41
41
  import { installJsonHooks, uninstallJsonHooks, resolveJsonHookPaths, detectInstalledTools, defaultSleepLogPath, ensureCodexWrapperInstalled, installCodexWrapper, detectRealCodexPath, isCodexPresent, isCodexWrapperInstalled, CODEX_TRUST_LINE, repairCodexWrapperIfInstalled, uninstallCodexWrapper, resolveCodexSessionTranscript, resolveCodexWrapperPaths, installOpencodePlugin, uninstallOpencodePlugin, resolveOpencodePluginPath, } from './hooks.js';
42
42
  import { createMemory, createSuccessor, calculateStrength, calculateRewardFactor, deriveHalfLife, resolveConfidence, confidenceFacets, confidenceLabel, computeSchemaFit, Layer, } from './memory.js';
43
- import { getHippoRoot, isInitialized, initStore, writeEntry, strengthenRetrieved, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, createCard, loadCard, listCards, loadCardRuns, claimCard, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, } from './store.js';
43
+ import { getHippoRoot, isInitialized, initStore, writeEntry, strengthenRetrieved, readEntry, deleteEntry, loadAllEntries, loadSearchEntries, loadRecallSearchEntries, loadIndex, saveIndex, loadStats, updateStats, saveActiveTaskSnapshot, loadActiveTaskSnapshot, loadFreshActiveTaskSnapshot, closeTaskSnapshotsForSession, clearActiveTaskSnapshot, appendSessionEvent, listSessionEvents, listMemoryConflicts, resolveConflict, saveSessionHandoff, loadLatestHandoff, loadHandoffById, stampHandoffOutcome, writeSessionEndHandoff, createCard, loadCard, listCards, loadCardRuns, claimCard, heartbeatCard, blockCard, reviewCard, completeCard, reclaimExpiredCards, addCardComment, memoriesBackingObjects, } from './store.js';
44
44
  import { rejectValue, unrejectValue, listRejectionsForTenant } from './reject-flow.js';
45
45
  import { RejectedValueError } from './rejection.js';
46
46
  import { isHandoffOutcome, formatHandoffEvidenceLine } from './handoff.js';
@@ -3251,7 +3251,8 @@ function spawnRealCodex(realCodexPath, forwardArgs, cwd) {
3251
3251
  }
3252
3252
  if (process.platform === 'win32' && (ext === '.cmd' || ext === '.bat')) {
3253
3253
  const command = `"${realCodexPath}"${forwardArgs.length > 0 ? ` ${forwardArgs.map(quoteCmdArg).join(' ')}` : ''}`;
3254
- return spawn('cmd.exe', ['/d', '/s', '/c', command], { cwd, stdio: 'inherit', windowsHide: false });
3254
+ // The line is already quoted for cmd.exe, so Node must not quote it again; /s strips the outer pair.
3255
+ return spawn('cmd.exe', ['/d', '/s', '/c', `"${command}"`], { cwd, stdio: 'inherit', windowsHide: false, windowsVerbatimArguments: true });
3255
3256
  }
3256
3257
  return spawn(realCodexPath, forwardArgs, { cwd, stdio: 'inherit', windowsHide: false });
3257
3258
  }
@@ -7707,23 +7708,6 @@ function cmdDailyRunner() {
7707
7708
  });
7708
7709
  console.log(`Daily maintenance complete: ${processed} workspace${processed === 1 ? '' : 's'} processed, ${failed} command failure${failed === 1 ? '' : 's'}.`);
7709
7710
  }
7710
- // JSON-hook install/uninstall lives in ./hooks.ts so tests can import it
7711
- // without running the CLI main(). Backwards-compatible wrappers below keep
7712
- // older call sites working.
7713
- function installClaudeCodeSessionEndHook() {
7714
- const result = installJsonHooks('claude-code');
7715
- return {
7716
- installed: result.installedSessionEnd ||
7717
- result.installedSessionStart ||
7718
- result.installedUserPromptSubmit ||
7719
- result.installedPreCompact ||
7720
- result.installedCompactResume,
7721
- migratedFromStop: result.migratedFromStop,
7722
- };
7723
- }
7724
- function uninstallClaudeCodeSessionEndHook() {
7725
- return uninstallJsonHooks('claude-code');
7726
- }
7727
7711
  // ---------------------------------------------------------------------------
7728
7712
  // Working Memory
7729
7713
  // ---------------------------------------------------------------------------
@@ -9611,7 +9595,7 @@ async function main(command, args, flags, hippoRoot) {
9611
9595
  }
9612
9596
  requireInit(hippoRoot);
9613
9597
  const entries = loadAllEntries(hippoRoot, resolveTenantId({}));
9614
- const result = auditMemories(entries);
9598
+ const result = auditMemories(entries, memoriesBackingObjects(hippoRoot));
9615
9599
  const shouldFix = Boolean(flags['fix']);
9616
9600
  if (result.issues.length === 0) {
9617
9601
  console.log(`All ${result.total} memories passed quality checks.`);
@@ -8,9 +8,8 @@ import { closeHippoDb, isSqliteBusy, openHippoDb } from './db.js';
8
8
  import { gatedWrite } from './gated-write.js';
9
9
  import { COMPACTION_MEMORY_TAG, COMPACTION_SOURCE_PREFIX, Layer, createMemory, generateId } from './memory.js';
10
10
  import { deriveOriginProject, isGlobalStoreRoot } from './project-identity.js';
11
- import { duplicateKey } from './same-text.js';
12
11
  import { maskEmails, redactSecretsStrict } from './secret-detect.js';
13
- import { updateStats, writeEntryMirrors } from './store.js';
12
+ import { strengthenRetrievedOn, updateStats, writeEntryMirrors } from './store.js';
14
13
  import { resolveTenantId } from './tenant.js';
15
14
  /** PostCompact has 10 s in all (PreCompact 30 s), so a locked store must be given up on early. */
16
15
  export const COMPACTION_DB_WAIT_MS = 2000;
@@ -133,11 +132,33 @@ function recordSummary(db, hippoRoot, tenantId, meta, text, at) {
133
132
  snapshotSaved: false, startedAt: at.toISOString(), summarisedAt: now, summary: text.summary, items: text.items, itemsWritten: 0, status: 'summarised',
134
133
  };
135
134
  }
136
- /** Content of live compaction rows in one tenant and origin, by SQL: a compaction repeats what an earlier one listed. */
137
- function liveItemKeys(db, tenantId, originProject) {
138
- // SAFETY: the SELECT names the single `content` column.
139
- const rows = db.prepare(`SELECT content FROM memories WHERE tenant_id = ? AND origin_project = ? AND superseded_by IS NULL AND instr(tags_json, ?) > 0`).all(tenantId, originProject, `"${COMPACTION_MEMORY_TAG}"`);
140
- return new Set(rows.map((r) => duplicateKey(r.content)));
135
+ /** Words whose loss reverses or narrows a statement: "do not", "can't", "only", "unless". */
136
+ const PROTECTED_WORDS = new Set(['not', 'no', 'never', 'none', 'nor', 'without', 'cannot', 't', 'only', 'except', 'unless', 'until']);
137
+ /** Below this share of the held text's words, an item is a fragment of something longer, not a restatement. */
138
+ const RESTATE_MIN_SHARE = 0.5;
139
+ function words(text) {
140
+ return text.normalize('NFC').toLowerCase().split(/[^\p{L}\p{M}\p{N}_]+/u).filter(Boolean);
141
+ }
142
+ /** True when `item` is `held` with words left out, in the same order, keeping every number and protected word. Spacing, case and punctuation may differ. */
143
+ function restates(item, held) {
144
+ if (item.length === 0 || item.length < RESTATE_MIN_SHARE * held.length)
145
+ return false;
146
+ const kept = new Set(item);
147
+ let next = 0;
148
+ for (const w of held) {
149
+ if (next < item.length && w === item[next])
150
+ next++;
151
+ else if (!kept.has(w) && (PROTECTED_WORDS.has(w) || /\p{N}/u.test(w)))
152
+ return false;
153
+ }
154
+ return next === item.length;
155
+ }
156
+ /** Live rows of one tenant and origin that default recall shows: a compaction often restates what an earlier one, or the user, already saved. */
157
+ function heldRows(db, tenantId, originProject) {
158
+ // SAFETY: the SELECT names the id, source_session_id and content columns.
159
+ const rows = db.prepare(`SELECT id, source_session_id, content FROM memories WHERE tenant_id = ? AND origin_project = ? AND superseded_by IS NULL AND kind != 'raw'
160
+ AND (scope IS NULL OR (scope != 'unknown:legacy' AND scope NOT LIKE '%:private:%'))`).all(tenantId, originProject);
161
+ return rows.map((r) => ({ id: r.id, sessionId: r.source_session_id, words: words(r.content) }));
141
162
  }
142
163
  /** Items that become rows, then the record `done`, in one transaction so two sessions cannot both insert one text. Mirrors after commit. */
143
164
  export function saveItems(db, hippoRoot, ctx, log) {
@@ -165,11 +186,15 @@ export function saveItems(db, hippoRoot, ctx, log) {
165
186
  return current?.items_written ?? 0;
166
187
  }
167
188
  }
168
- const seen = liveItemKeys(db, ctx.tenantId, ctx.originProject);
189
+ const held = heldRows(db, ctx.tenantId, ctx.originProject);
190
+ const restated = new Set();
169
191
  for (const text of rows) {
170
- const key = duplicateKey(text);
171
- if (seen.has(key)) {
192
+ const itemWords = words(text);
193
+ const match = held.find((h) => restates(itemWords, h.words));
194
+ if (match) {
172
195
  repeats++;
196
+ if (match.id !== null && match.sessionId !== ctx.sessionId)
197
+ restated.add(match.id);
173
198
  continue;
174
199
  }
175
200
  // createMemory throws below 3 chars, which would sink the whole transaction.
@@ -192,12 +217,14 @@ export function saveItems(db, hippoRoot, ctx, log) {
192
217
  };
193
218
  if (gatedWrite(db, hippoRoot, entry, { actor: 'post-compact' }) === 'written') {
194
219
  written.push(entry);
195
- seen.add(key);
220
+ held.push({ id: null, sessionId: ctx.sessionId, words: itemWords });
196
221
  }
197
222
  else {
198
223
  refused++;
199
224
  }
200
225
  }
226
+ // Said again by another session is the same signal as being recalled; the same session carrying it forward is not.
227
+ strengthenRetrievedOn(db, [...restated], ctx.tenantId);
201
228
  if (ctx.recordId !== null) {
202
229
  db.prepare(`UPDATE compactions SET items_written = ?, status = 'done' WHERE tenant_id = ? AND id = ?`).run(written.length, ctx.tenantId, ctx.recordId);
203
230
  }
@@ -211,7 +238,7 @@ export function saveItems(db, hippoRoot, ctx, log) {
211
238
  throw err;
212
239
  }
213
240
  if (repeats > 0)
214
- log(`skipped ${repeats} item(s) an earlier compaction already saved`);
241
+ log(`skipped ${repeats} item(s) the store already holds`);
215
242
  if (refused > 0)
216
243
  log(`skipped ${refused} item(s) the write gate refused`);
217
244
  for (const entry of written)
@@ -37,8 +37,6 @@ export interface ConsolidationResult {
37
37
  /** Ids the decay pass removes (or would remove, under dryRun). */
38
38
  removedIds?: string[];
39
39
  }
40
- /** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
41
- export declare const MEMORY_BACKED_TABLES: readonly ["predictions", "decisions", "incidents", "processes", "policies", "skills", "project_briefs", "customer_notes"];
42
40
  /**
43
41
  * Run a full consolidation pass.
44
42
  */
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { evalNow, isRecallBoostAblated } from './ablation.js';
10
10
  import { Layer, calculateStrength, canAutoDelete, createMemory } from './memory.js';
11
- import { loadAllEntries, batchWriteAndDelete, appendConsolidationRun, replaceDetectedConflicts, loadSessionDecayContext, incrementSleepCount, findPromotableSessions, traceExistsForSession, listSessionEvents, } from './store.js';
11
+ import { loadAllEntries, batchWriteAndDelete, appendConsolidationRun, replaceDetectedConflicts, loadSessionDecayContext, incrementSleepCount, findPromotableSessions, traceExistsForSession, listSessionEvents, memoriesBackingObjects, } from './store.js';
12
12
  import { textOverlap, markRetrieved, tokenize } from './search.js';
13
13
  import { compareEntryIdentity } from './compare.js';
14
14
  import { duplicateKey, mergedText } from './same-text.js';
@@ -72,39 +72,6 @@ function keptAsWritten(entry) {
72
72
  function isJsonString(value) {
73
73
  return typeof value === 'string';
74
74
  }
75
- /** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
76
- export const MEMORY_BACKED_TABLES = ['predictions', 'decisions', 'incidents', 'processes', 'policies', 'skills', 'project_briefs', 'customer_notes'];
77
- /**
78
- * Ids of memories that back a first-class object (a decision, incident, prediction,
79
- * process, policy, skill, project brief or customer note). Sleep never
80
- * retires these: deleting or moving one to dormant storage fires the
81
- * object's ON DELETE SET NULL and a restore cannot repair the link. Their
82
- * lifecycle belongs to the object. A table missing from an older schema is
83
- * skipped.
84
- */
85
- function memoriesBackingObjects(hippoRoot) {
86
- const ids = new Set();
87
- const db = openHippoDb(hippoRoot);
88
- try {
89
- for (const table of MEMORY_BACKED_TABLES) {
90
- try {
91
- // SAFETY: SELECT of one nullable TEXT column, filtered to non-null.
92
- const rows = db.prepare(`SELECT memory_id FROM ${table} WHERE memory_id IS NOT NULL`).all();
93
- for (const r of rows)
94
- ids.add(r.memory_id);
95
- }
96
- catch (err) {
97
- // A missing table is an older schema; any other error could hide a backing memory, so sleep stops.
98
- if (!(err instanceof Error && err.message.includes('no such table')))
99
- throw err;
100
- }
101
- }
102
- }
103
- finally {
104
- closeHippoDb(db);
105
- }
106
- return ids;
107
- }
108
75
  /**
109
76
  * Run a full consolidation pass.
110
77
  */
package/dist/dedupe.js CHANGED
@@ -21,7 +21,7 @@
21
21
  * superseded rows are not candidates at all (v1.38.1).
22
22
  */
23
23
  import { textOverlap } from './search.js';
24
- import { loadAllEntries, deleteEntry } from './store.js';
24
+ import { loadAllEntries, deleteEntry, memoriesBackingObjects } from './store.js';
25
25
  import { compareEntryIdentity } from './compare.js';
26
26
  import { canAutoDelete } from './memory.js';
27
27
  import { derivationPartitionKey } from './recall-scope.js';
@@ -97,6 +97,7 @@ export function deduplicateStore(hippoRoot, options = {}) {
97
97
  // primary-key id alone.
98
98
  const removed = new Set();
99
99
  const pairs = [];
100
+ const backing = memoriesBackingObjects(hippoRoot);
100
101
  for (const tenantEntries of entriesByTenant.values()) {
101
102
  // The v1.26.3 survivor total order (see the file-level docstring:
102
103
  // strength bucket desc -> retrieval_count desc -> compareEntryIdentity),
@@ -116,7 +117,7 @@ export function deduplicateStore(hippoRoot, options = {}) {
116
117
  if (removed.has(tenantEntries[i].id))
117
118
  continue;
118
119
  for (let j = i + 1; j < tenantEntries.length; j++) {
119
- if (removed.has(tenantEntries[j].id) || !canAutoDelete(tenantEntries[j]))
120
+ if (removed.has(tenantEntries[j].id) || !canAutoDelete(tenantEntries[j]) || backing.has(tenantEntries[j].id))
120
121
  continue;
121
122
  if (texts[j] !== texts[i])
122
123
  continue;
package/dist/memory.d.ts CHANGED
@@ -215,7 +215,7 @@ export declare function resolveConfidence(entry: MemoryEntry, now?: Date): Confi
215
215
  export declare const DEFAULT_HALF_LIFE_DAYS = 365;
216
216
  export declare const COMPACTION_MEMORY_TAG = "compaction-memory";
217
217
  export declare const COMPACTION_SOURCE_PREFIX = "compaction:";
218
- /** A row with `tag` and a source starting `sourcePrefix` is kept for good. Both, since merge copies source tags onto rows whose source is 'consolidation'. */
218
+ /** An imported agent memory (its tool's tag, and a source starting that tool's prefix) is kept for good: the agent's note file is its record. Both, since merge copies source tags onto rows whose source is 'consolidation'. */
219
219
  export interface KeepPair {
220
220
  readonly tag: string;
221
221
  readonly sourcePrefix: string;
package/dist/memory.js CHANGED
@@ -317,10 +317,7 @@ export function resolveConfidence(entry, now = evalNow()) {
317
317
  export const DEFAULT_HALF_LIFE_DAYS = 365;
318
318
  export const COMPACTION_MEMORY_TAG = 'compaction-memory';
319
319
  export const COMPACTION_SOURCE_PREFIX = 'compaction:';
320
- export const KEEP_PAIRS = [
321
- { tag: COMPACTION_MEMORY_TAG, sourcePrefix: COMPACTION_SOURCE_PREFIX },
322
- ...AGENT_MEMORY_TOOLS.map((t) => ({ tag: t.tag, sourcePrefix: toolSourcePrefix(t.id) })),
323
- ];
320
+ export const KEEP_PAIRS = AGENT_MEMORY_TOOLS.map((t) => ({ tag: t.tag, sourcePrefix: toolSourcePrefix(t.id) }));
324
321
  const sqlText = (s) => `'${s.replace(/'/g, "''")}'`;
325
322
  // json_each matches the tag as a whole element; substr, not LIKE, keeps the prefix case-sensitive like startsWith.
326
323
  const keepPairSql = (p) => `(COALESCE(superseded_by, '') = '' AND EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(tags_json) THEN tags_json ELSE '[]' END) WHERE value = ${sqlText(p.tag)}) AND substr(source, 1, ${p.sourcePrefix.length}) = ${sqlText(p.sourcePrefix)})`;
package/dist/store.d.ts CHANGED
@@ -289,6 +289,8 @@ export declare function loadEntriesByIds(hippoRoot: string, ids: readonly string
289
289
  /** Strengthen what a read returned: update only the four retrieval columns on the live row, never a stale copy.
290
290
  * Best effort: a failure logs and never fails the read. Returns the ids found in this store. */
291
291
  export declare function strengthenRetrieved(hippoRoot: string, ids: readonly string[], tenantId?: string): Set<string>;
292
+ /** strengthenRetrieved on the caller's handle, inside the caller's transaction. Throws; the caller decides. */
293
+ export declare function strengthenRetrievedOn(db: DatabaseSyncLike, ids: readonly string[], tenantId?: string): Set<string>;
292
294
  /**
293
295
  * All `kind='raw'` rows for a given session, tenant-scoped, returned
294
296
  * oldest-first. Used by `api.assemble` to walk a session's chronological
@@ -350,6 +352,10 @@ export declare function loadFreshRawMemories(hippoRoot: string, count: number, t
350
352
  * Used by `drillDown` (Task 3).
351
353
  */
352
354
  export declare function loadChildrenOf(hippoRoot: string, parentId: string, tenantId?: string): MemoryEntry[];
355
+ /** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
356
+ export declare const MEMORY_BACKED_TABLES: readonly ["predictions", "decisions", "incidents", "processes", "policies", "skills", "project_briefs", "customer_notes"];
357
+ /** Ids of memories that back a first-class object, for passes that plan deletes before making them. A table missing from an older schema is skipped. */
358
+ export declare function memoriesBackingObjects(hippoRoot: string): Set<string>;
353
359
  /**
354
360
  * AT1 (plan §4, round-2 fix, designed from source): db-scoped delete core.
355
361
  * `deleteEntry` used to open+close its OWN connection, which meant it could
@@ -367,7 +373,7 @@ export declare function loadChildrenOf(hippoRoot: string, parentId: string, tena
367
373
  * Default keeps `deleteEntry` byte-identical to its pre-split behavior.
368
374
  *
369
375
  * Returns `{tenantId, dagParentId}` for the removed row, or `null` if no row with `id`
370
- * existed or `automatic` refused it (pinned, raw or kept for good at DELETE time, so a late pin wins).
376
+ * existed or `automatic` refused it (pinned, raw, kept for good or backing an object at DELETE time, so a late pin wins).
371
377
  */
372
378
  export declare function deleteEntryCore(db: ReturnType<typeof openHippoDb>, id: string, opts?: {
373
379
  actor?: string;
@@ -400,7 +406,7 @@ export declare function deleteEntry(hippoRoot: string, id: string, opts?: {
400
406
  * `dormant` (src/dormant.ts): each move's snapshot is inserted into `dormant_memories` and its `memories` row
401
407
  * leaves exactly like a delete (FTS row, DAG parent dirty-mark, mirrors), in the same transaction, so a memory
402
408
  * is never in both places or in neither. Deletes and moves both skip rows that are no longer auto-deletable
403
- * (pinned, raw or kept for good since the caller decided). Returns the ids that left `memories`, deleted or moved. */
409
+ * (pinned, raw, kept for good or backing an object since the caller decided). Returns the ids that left `memories`, deleted or moved. */
404
410
  export declare function batchWriteAndDelete(hippoRoot: string, toWrite: MemoryEntry[], toDeleteIds: string[], opts?: {
405
411
  snapshot?: ReadonlyMap<string, MemoryEntry>;
406
412
  dormant?: DormantMove[];
package/dist/store.js CHANGED
@@ -1349,20 +1349,8 @@ export function strengthenRetrieved(hippoRoot, ids, tenantId) {
1349
1349
  try {
1350
1350
  db = openHippoDb(hippoRoot);
1351
1351
  db.exec('BEGIN IMMEDIATE');
1352
- const tenantClause = tenantId !== undefined ? ' AND tenant_id = ?' : '';
1353
- const select = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id = ?${tenantClause}`);
1354
- const live = [];
1355
- for (const id of ids) {
1356
- // SAFETY: the SELECT names exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
1357
- const row = (tenantId !== undefined ? select.get(id, tenantId) : select.get(id));
1358
- if (row)
1359
- live.push(rowToEntry(row));
1360
- }
1361
- const update = db.prepare('UPDATE memories SET retrieval_count = ?, last_retrieved = ?, half_life_days = ?, strength = ? WHERE id = ?');
1362
- for (const e of markRetrieved(live)) {
1363
- update.run(e.retrieval_count, e.last_retrieved, e.half_life_days, e.strength, e.id);
1364
- found.add(e.id);
1365
- }
1352
+ for (const id of strengthenRetrievedOn(db, ids, tenantId))
1353
+ found.add(id);
1366
1354
  db.exec('COMMIT');
1367
1355
  }
1368
1356
  catch (error) {
@@ -1379,6 +1367,27 @@ export function strengthenRetrieved(hippoRoot, ids, tenantId) {
1379
1367
  }
1380
1368
  return found;
1381
1369
  }
1370
+ /** strengthenRetrieved on the caller's handle, inside the caller's transaction. Throws; the caller decides. */
1371
+ export function strengthenRetrievedOn(db, ids, tenantId) {
1372
+ const found = new Set();
1373
+ if (ids.length === 0 || isRecallBoostAblated())
1374
+ return found;
1375
+ const tenantClause = tenantId !== undefined ? ' AND tenant_id = ?' : '';
1376
+ const select = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id = ?${tenantClause}`);
1377
+ const live = [];
1378
+ for (const id of ids) {
1379
+ // SAFETY: the SELECT names exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
1380
+ const row = (tenantId !== undefined ? select.get(id, tenantId) : select.get(id));
1381
+ if (row)
1382
+ live.push(rowToEntry(row));
1383
+ }
1384
+ const update = db.prepare('UPDATE memories SET retrieval_count = ?, last_retrieved = ?, half_life_days = ?, strength = ? WHERE id = ?');
1385
+ for (const e of markRetrieved(live)) {
1386
+ update.run(e.retrieval_count, e.last_retrieved, e.half_life_days, e.strength, e.id);
1387
+ found.add(e.id);
1388
+ }
1389
+ return found;
1390
+ }
1382
1391
  /**
1383
1392
  * All `kind='raw'` rows for a given session, tenant-scoped, returned
1384
1393
  * oldest-first. Used by `api.assemble` to walk a session's chronological
@@ -1538,6 +1547,34 @@ export function loadChildrenOf(hippoRoot, parentId, tenantId) {
1538
1547
  closeHippoDb(db);
1539
1548
  }
1540
1549
  }
1550
+ /** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
1551
+ export const MEMORY_BACKED_TABLES = ['predictions', 'decisions', 'incidents', 'processes', 'policies', 'skills', 'project_briefs', 'customer_notes'];
1552
+ /** Deleting a memory that backs an object nulls the object's link, and no restore can repair it, so no automatic pass may. */
1553
+ const AUTOMATIC_DELETE_SQL = `${AUTO_DELETABLE_SQL}${MEMORY_BACKED_TABLES.map((t) => ` AND NOT EXISTS (SELECT 1 FROM ${t} WHERE ${t}.memory_id = memories.id)`).join('')}`;
1554
+ /** Ids of memories that back a first-class object, for passes that plan deletes before making them. A table missing from an older schema is skipped. */
1555
+ export function memoriesBackingObjects(hippoRoot) {
1556
+ const ids = new Set();
1557
+ const db = openHippoDb(hippoRoot);
1558
+ try {
1559
+ for (const table of MEMORY_BACKED_TABLES) {
1560
+ try {
1561
+ // SAFETY: SELECT of one nullable TEXT column, filtered to non-null.
1562
+ const rows = db.prepare(`SELECT memory_id FROM ${table} WHERE memory_id IS NOT NULL`).all();
1563
+ for (const r of rows)
1564
+ ids.add(r.memory_id);
1565
+ }
1566
+ catch (err) {
1567
+ // A missing table is an older schema; any other error could hide a backing memory, so the caller stops.
1568
+ if (!(err instanceof Error && err.message.includes('no such table')))
1569
+ throw err;
1570
+ }
1571
+ }
1572
+ }
1573
+ finally {
1574
+ closeHippoDb(db);
1575
+ }
1576
+ return ids;
1577
+ }
1541
1578
  /**
1542
1579
  * AT1 (plan §4, round-2 fix, designed from source): db-scoped delete core.
1543
1580
  * `deleteEntry` used to open+close its OWN connection, which meant it could
@@ -1555,7 +1592,7 @@ export function loadChildrenOf(hippoRoot, parentId, tenantId) {
1555
1592
  * Default keeps `deleteEntry` byte-identical to its pre-split behavior.
1556
1593
  *
1557
1594
  * Returns `{tenantId, dagParentId}` for the removed row, or `null` if no row with `id`
1558
- * existed or `automatic` refused it (pinned, raw or kept for good at DELETE time, so a late pin wins).
1595
+ * existed or `automatic` refused it (pinned, raw, kept for good or backing an object at DELETE time, so a late pin wins).
1559
1596
  */
1560
1597
  export function deleteEntryCore(db, id, opts) {
1561
1598
  // SAFETY: row's shape matches the three columns named in the SELECT above.
@@ -1564,7 +1601,7 @@ export function deleteEntryCore(db, id, opts) {
1564
1601
  .get(id);
1565
1602
  if (!row?.id)
1566
1603
  return null;
1567
- const guard = opts?.automatic ? ` AND ${AUTO_DELETABLE_SQL}` : '';
1604
+ const guard = opts?.automatic ? ` AND ${AUTOMATIC_DELETE_SQL}` : '';
1568
1605
  if (Number(db.prepare(`DELETE FROM memories WHERE id = ?${guard}`).run(id).changes ?? 0) === 0)
1569
1606
  return null;
1570
1607
  deleteFtsRow(db, id);
@@ -1622,7 +1659,7 @@ function mergeOwnChanges(base, ours, live) {
1622
1659
  * `dormant` (src/dormant.ts): each move's snapshot is inserted into `dormant_memories` and its `memories` row
1623
1660
  * leaves exactly like a delete (FTS row, DAG parent dirty-mark, mirrors), in the same transaction, so a memory
1624
1661
  * is never in both places or in neither. Deletes and moves both skip rows that are no longer auto-deletable
1625
- * (pinned, raw or kept for good since the caller decided). Returns the ids that left `memories`, deleted or moved. */
1662
+ * (pinned, raw, kept for good or backing an object since the caller decided). Returns the ids that left `memories`, deleted or moved. */
1626
1663
  export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
1627
1664
  const dormantMoves = opts?.dormant ?? [];
1628
1665
  if (toWrite.length === 0 && toDeleteIds.length === 0 && dormantMoves.length === 0)
@@ -1650,7 +1687,7 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
1650
1687
  const placeholders = toDeleteIds.map(() => '?').join(',');
1651
1688
  // A row pinned after the caller decided to delete it survives.
1652
1689
  // SAFETY: rows' shape matches the three columns named in the SELECT.
1653
- const rows = db.prepare(`SELECT id, dag_parent_id, tenant_id FROM memories WHERE id IN (${placeholders}) AND ${AUTO_DELETABLE_SQL}`).all(...toDeleteIds);
1690
+ const rows = db.prepare(`SELECT id, dag_parent_id, tenant_id FROM memories WHERE id IN (${placeholders}) AND ${AUTOMATIC_DELETE_SQL}`).all(...toDeleteIds);
1654
1691
  for (const row of rows) {
1655
1692
  deletableIds.push(row.id);
1656
1693
  if (row.dag_parent_id) {
@@ -1737,7 +1774,7 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
1737
1774
  const byId = new Map(dormantMoves.map((m) => [m.entry.id, m]));
1738
1775
  const placeholders = dormantMoves.map(() => '?').join(',');
1739
1776
  // SAFETY: rows' shape matches the three columns named in the SELECT.
1740
- const rows = db.prepare(`SELECT id, dag_parent_id, tenant_id FROM memories WHERE id IN (${placeholders}) AND ${AUTO_DELETABLE_SQL}`).all(...byId.keys());
1777
+ const rows = db.prepare(`SELECT id, dag_parent_id, tenant_id FROM memories WHERE id IN (${placeholders}) AND ${AUTOMATIC_DELETE_SQL}`).all(...byId.keys());
1741
1778
  for (const row of rows) {
1742
1779
  movable.push(byId.get(row.id));
1743
1780
  if (row.dag_parent_id) {
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.53.0";
19
+ export declare const PACKAGE_VERSION = "1.53.2";
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.53.0';
19
+ export const PACKAGE_VERSION = '1.53.2';
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.53.0",
5
+ "version": "1.53.2",
6
6
 
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.53.0",
3
+ "version": "1.53.2",
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.53.0",
5
+ "version": "1.53.2",
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.53.0",
3
+ "version": "1.53.2",
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",