hippo-memory 1.53.0 → 1.53.1
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 +13 -3
- package/dist/api.d.ts +1 -1
- package/dist/api.js +2 -2
- package/dist/audit.d.ts +3 -2
- package/dist/audit.js +7 -6
- package/dist/cli.js +2 -19
- package/dist/compaction-record.js +39 -12
- package/dist/consolidate.d.ts +0 -2
- package/dist/consolidate.js +1 -34
- package/dist/dedupe.js +3 -2
- package/dist/memory.d.ts +1 -1
- package/dist/memory.js +1 -4
- package/dist/store.d.ts +8 -2
- package/dist/store.js +56 -19
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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';
|
|
@@ -7707,23 +7707,6 @@ function cmdDailyRunner() {
|
|
|
7707
7707
|
});
|
|
7708
7708
|
console.log(`Daily maintenance complete: ${processed} workspace${processed === 1 ? '' : 's'} processed, ${failed} command failure${failed === 1 ? '' : 's'}.`);
|
|
7709
7709
|
}
|
|
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
7710
|
// ---------------------------------------------------------------------------
|
|
7728
7711
|
// Working Memory
|
|
7729
7712
|
// ---------------------------------------------------------------------------
|
|
@@ -9611,7 +9594,7 @@ async function main(command, args, flags, hippoRoot) {
|
|
|
9611
9594
|
}
|
|
9612
9595
|
requireInit(hippoRoot);
|
|
9613
9596
|
const entries = loadAllEntries(hippoRoot, resolveTenantId({}));
|
|
9614
|
-
const result = auditMemories(entries);
|
|
9597
|
+
const result = auditMemories(entries, memoriesBackingObjects(hippoRoot));
|
|
9615
9598
|
const shouldFix = Boolean(flags['fix']);
|
|
9616
9599
|
if (result.issues.length === 0) {
|
|
9617
9600
|
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
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
189
|
+
const held = heldRows(db, ctx.tenantId, ctx.originProject);
|
|
190
|
+
const restated = new Set();
|
|
169
191
|
for (const text of rows) {
|
|
170
|
-
const
|
|
171
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/consolidate.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
1353
|
-
|
|
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
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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.
|
|
19
|
+
export declare const PACKAGE_VERSION = "1.53.1";
|
|
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.
|
|
19
|
+
export const PACKAGE_VERSION = '1.53.1';
|
|
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.
|
|
5
|
+
"version": "1.53.1",
|
|
6
6
|
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.53.1",
|
|
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.
|
|
3
|
+
"version": "1.53.1",
|
|
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",
|