hippo-memory 1.49.0 → 1.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +26 -0
- package/dist/api.js +144 -6
- package/dist/audit.d.ts +1 -1
- package/dist/churn-git.d.ts +22 -0
- package/dist/churn-git.js +102 -0
- package/dist/cli.js +136 -2
- package/dist/config.d.ts +5 -0
- package/dist/config.js +24 -0
- package/dist/connectors/github/ingest.js +1 -0
- package/dist/connectors/slack/ingest.js +1 -0
- package/dist/consolidate.js +3 -0
- package/dist/db.js +26 -1
- package/dist/instruction-detect.d.ts +7 -0
- package/dist/instruction-detect.js +27 -0
- package/dist/invalidation.d.ts +30 -0
- package/dist/invalidation.js +266 -1
- package/dist/memory.d.ts +2 -0
- package/dist/memory.js +2 -0
- package/dist/quarantine.d.ts +31 -0
- package/dist/quarantine.js +63 -0
- package/dist/search.d.ts +4 -0
- package/dist/search.js +17 -6
- package/dist/server.js +59 -1
- package/dist/shared.js +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/search.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Zero external dependencies when embeddings are not available.
|
|
4
4
|
*/
|
|
5
5
|
import { estimateTokens } from './token-ledger.js';
|
|
6
|
-
import { calculateStrength, netWrong } from './memory.js';
|
|
6
|
+
import { calculateStrength, netWrong, CHURN_STALE_TAG } from './memory.js';
|
|
7
7
|
import { isOutcomeFastAblated, isRecallBoostAblated, isRecencyAblated, evalRecencyScaleDays, evalNow } from './ablation.js';
|
|
8
8
|
import { extractPathTags, pathBoostMultiplier } from './path-context.js';
|
|
9
9
|
import { detectScope, scopeMatch } from './scope.js';
|
|
@@ -16,6 +16,10 @@ import { openHippoDb, closeHippoDb } from './db.js';
|
|
|
16
16
|
import { rrfFuse } from './rrf.js';
|
|
17
17
|
import { graphRankStream, selectGraphSeeds, DEFAULT_GRAPH_SEED_COUNT } from './graph-stream.js';
|
|
18
18
|
import { compareEntryIdentity, compareScoredResults } from './compare.js';
|
|
19
|
+
export const CHURN_STALE_RANK_MULTIPLIER = 0.5; // SHORTCUT: untuned; FE3 measures before any default.
|
|
20
|
+
export function churnStaleFactor(entry) {
|
|
21
|
+
return entry.tags.includes(CHURN_STALE_TAG) ? CHURN_STALE_RANK_MULTIPLIER : 1.0;
|
|
22
|
+
}
|
|
19
23
|
// ---------------------------------------------------------------------------
|
|
20
24
|
// Tokenizer
|
|
21
25
|
// ---------------------------------------------------------------------------
|
|
@@ -368,6 +372,8 @@ export async function hybridSearch(query, entries, options = {}) {
|
|
|
368
372
|
// Decision-tagged memories get a 1.2x recall boost
|
|
369
373
|
const decisionBoost = entries[i].tags.includes('decision') ? 1.2 : 1.0;
|
|
370
374
|
compositeScore *= decisionBoost;
|
|
375
|
+
const churnStaleMultiplier = churnStaleFactor(entries[i]);
|
|
376
|
+
compositeScore *= churnStaleMultiplier;
|
|
371
377
|
// Path-based boost: memories tagged with matching path segments get up to 1.3x
|
|
372
378
|
const pathBoost = pathBoostMultiplier(entries[i].tags, currentPathTags);
|
|
373
379
|
compositeScore *= pathBoost;
|
|
@@ -424,6 +430,7 @@ export async function hybridSearch(query, entries, options = {}) {
|
|
|
424
430
|
scopeBoost,
|
|
425
431
|
sourceBump: 1,
|
|
426
432
|
outcomeBoost,
|
|
433
|
+
churnStaleMultiplier,
|
|
427
434
|
matchedTerms,
|
|
428
435
|
final: compositeScore,
|
|
429
436
|
ageDays,
|
|
@@ -476,7 +483,7 @@ export async function hybridSearch(query, entries, options = {}) {
|
|
|
476
483
|
for (const child of childEntriesAsync) {
|
|
477
484
|
if (!scoredDeduped.some((r) => r.entry.id === child.id)) {
|
|
478
485
|
const parentResult = scoredDeduped.find((r) => r.entry.id === child.dag_parent_id);
|
|
479
|
-
const childScore = parentResult ? parentResult.score * 0.9 : 0;
|
|
486
|
+
const childScore = parentResult ? parentResult.score * 0.9 * churnStaleFactor(child) : 0;
|
|
480
487
|
scoredDeduped.push({
|
|
481
488
|
entry: child,
|
|
482
489
|
score: childScore,
|
|
@@ -700,7 +707,8 @@ export async function physicsSearch(query, entries, options = {}) {
|
|
|
700
707
|
freshnessMultiplier = summaryFreshnessMultiplier(entry, now);
|
|
701
708
|
}
|
|
702
709
|
}
|
|
703
|
-
const
|
|
710
|
+
const churnStaleMultiplier = churnStaleFactor(entry);
|
|
711
|
+
const finalScore = s.finalScore * summaryDeboostMultiplier * freshnessMultiplier * churnStaleMultiplier;
|
|
704
712
|
if (finalScore <= 0)
|
|
705
713
|
continue;
|
|
706
714
|
const result = {
|
|
@@ -726,6 +734,7 @@ export async function physicsSearch(query, entries, options = {}) {
|
|
|
726
734
|
scopeBoost: 1,
|
|
727
735
|
sourceBump: 1,
|
|
728
736
|
outcomeBoost: 1,
|
|
737
|
+
churnStaleMultiplier,
|
|
729
738
|
matchedTerms: [],
|
|
730
739
|
final: finalScore,
|
|
731
740
|
ageDays,
|
|
@@ -768,8 +777,9 @@ export async function physicsSearch(query, entries, options = {}) {
|
|
|
768
777
|
}
|
|
769
778
|
/** Normalize two score pools to [0,1] and combine. */
|
|
770
779
|
function mergeScorePools(poolA, poolB) {
|
|
771
|
-
const
|
|
772
|
-
const
|
|
780
|
+
const unpenalised = (r) => r.score / churnStaleFactor(r.entry);
|
|
781
|
+
const maxA = poolA.reduce((m, r) => Math.max(m, unpenalised(r)), 1e-9);
|
|
782
|
+
const maxB = poolB.reduce((m, r) => Math.max(m, unpenalised(r)), 1e-9);
|
|
773
783
|
const merged = [];
|
|
774
784
|
for (const r of poolA) {
|
|
775
785
|
merged.push({ ...r, score: r.score / maxA });
|
|
@@ -842,6 +852,7 @@ export function search(query, entries, options = {}) {
|
|
|
842
852
|
// Decision-tagged memories get a 1.2x recall boost
|
|
843
853
|
const decisionBoost = entries[i].tags.includes('decision') ? 1.2 : 1.0;
|
|
844
854
|
composite *= decisionBoost;
|
|
855
|
+
composite *= churnStaleFactor(entries[i]);
|
|
845
856
|
// Path-based boost: memories tagged with matching path segments get up to 1.3x
|
|
846
857
|
composite *= pathBoostMultiplier(entries[i].tags, currentPathTagsSync);
|
|
847
858
|
// Scope boost (sync path)
|
|
@@ -883,7 +894,7 @@ export function search(query, entries, options = {}) {
|
|
|
883
894
|
for (const child of childEntries) {
|
|
884
895
|
if (!dedupedSync.some((r) => r.entry.id === child.id)) {
|
|
885
896
|
const parentResult = dedupedSync.find((r) => r.entry.id === child.dag_parent_id);
|
|
886
|
-
const childScore = parentResult ? parentResult.score * 0.9 : 0;
|
|
897
|
+
const childScore = parentResult ? parentResult.score * 0.9 * churnStaleFactor(child) : 0;
|
|
887
898
|
dedupedSync.push({
|
|
888
899
|
entry: child,
|
|
889
900
|
score: childScore,
|
package/dist/server.js
CHANGED
|
@@ -22,7 +22,7 @@ export function __resetSessionRecallHistoryHttp() {
|
|
|
22
22
|
import { PACKAGE_VERSION } from './version.js';
|
|
23
23
|
import { validateApiKey } from './auth.js';
|
|
24
24
|
import { createRateLimiter } from './rate-limit.js';
|
|
25
|
-
import { remember, retrieve, RecallContractError, ForbiddenError, drillDown, assemble, forget, promote, supersede, archiveRaw, authCreate, authList, authRevoke, auditList, outcome, outcomeForLastRecall, getContext, sleep, adminActor, recordTokens, } from './api.js';
|
|
25
|
+
import { remember, retrieve, RecallContractError, ForbiddenError, drillDown, assemble, forget, promote, supersede, archiveRaw, authCreate, authList, authRevoke, auditList, outcome, outcomeForLastRecall, getContext, sleep, adminActor, recordTokens, quarantineList, quarantineApprove, quarantineReject, } from './api.js';
|
|
26
26
|
import { buildGraphModel } from './graph-view.js';
|
|
27
27
|
import { MAX_ENTITY_NAME_LEN } from './graph.js';
|
|
28
28
|
import { savePrediction, closePrediction, loadPredictionById, loadPredictionsByClass, loadOpenPredictions, computePredictionBaserate, VALID_CLOSURE_STATES, } from './predictions.js';
|
|
@@ -118,6 +118,9 @@ const VALID_AUDIT_OPS = new Set([
|
|
|
118
118
|
'dormant_restore', // Dormant memories — emitted by api.restoreDormant; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
|
|
119
119
|
'auth_grant', // EI2: emitted by api.authGrant; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
|
|
120
120
|
'auth_ungrant', // EI2: emitted by api.authUngrant; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
|
|
121
|
+
'quarantine', // CD5: emitted by recordQuarantine; lockstep with AuditOp union + cli.ts VALID_AUDIT_OPS
|
|
122
|
+
'quarantine_approve', // CD5: emitted by api.quarantineApprove; lockstep
|
|
123
|
+
'quarantine_reject', // CD5: emitted by api.quarantineReject; lockstep
|
|
121
124
|
]);
|
|
122
125
|
// Cap on GET /v1/audit?limit=. Matches docs/api.md (when written) and is large
|
|
123
126
|
// enough to dump a small deployment's full audit log without paginating, but
|
|
@@ -1163,6 +1166,61 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
1163
1166
|
sendJson(res, 200, result);
|
|
1164
1167
|
return;
|
|
1165
1168
|
}
|
|
1169
|
+
// GET /v1/quarantine?status=: CD5 review queue. quarantineList carries no role gate itself, so it's checked here.
|
|
1170
|
+
if (method === 'GET' && path === '/v1/quarantine') {
|
|
1171
|
+
const ctx = buildContextWithAuth(req, opts.hippoRoot);
|
|
1172
|
+
if (ctx.actor.role !== 'admin') {
|
|
1173
|
+
throw new HttpError(403, '/v1/quarantine requires admin role');
|
|
1174
|
+
}
|
|
1175
|
+
const statusRaw = query.get('status');
|
|
1176
|
+
let status = 'pending';
|
|
1177
|
+
if (statusRaw !== null) {
|
|
1178
|
+
if (statusRaw !== 'pending' && statusRaw !== 'approved' && statusRaw !== 'rejected' && statusRaw !== 'all') {
|
|
1179
|
+
throw new HttpError(400, 'status must be one of: pending | approved | rejected | all');
|
|
1180
|
+
}
|
|
1181
|
+
status = statusRaw;
|
|
1182
|
+
}
|
|
1183
|
+
sendJson(res, 200, { quarantine: quarantineList(ctx, { status }) });
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
// POST /v1/quarantine/:id/approve: admin only; ForbiddenError falls through to mapApiError's 403.
|
|
1187
|
+
const quarantineApproveMatch = matchPath('/v1/quarantine/:id/approve', path);
|
|
1188
|
+
if (method === 'POST' && quarantineApproveMatch) {
|
|
1189
|
+
validateIdSegment(quarantineApproveMatch.id, 'memory id');
|
|
1190
|
+
const ctx = buildContextWithAuth(req, opts.hippoRoot);
|
|
1191
|
+
try {
|
|
1192
|
+
quarantineApprove(ctx, quarantineApproveMatch.id);
|
|
1193
|
+
sendJson(res, 200, { approved: quarantineApproveMatch.id });
|
|
1194
|
+
}
|
|
1195
|
+
catch (e) {
|
|
1196
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1197
|
+
if (msg.includes('not quarantined'))
|
|
1198
|
+
throw new HttpError(404, msg);
|
|
1199
|
+
if (msg.includes('is already') || msg.includes('scope changed'))
|
|
1200
|
+
throw new HttpError(409, msg);
|
|
1201
|
+
throw e;
|
|
1202
|
+
}
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
// POST /v1/quarantine/:id/reject: admin only; ForbiddenError falls through to mapApiError's 403.
|
|
1206
|
+
const quarantineRejectMatch = matchPath('/v1/quarantine/:id/reject', path);
|
|
1207
|
+
if (method === 'POST' && quarantineRejectMatch) {
|
|
1208
|
+
validateIdSegment(quarantineRejectMatch.id, 'memory id');
|
|
1209
|
+
const ctx = buildContextWithAuth(req, opts.hippoRoot);
|
|
1210
|
+
try {
|
|
1211
|
+
quarantineReject(ctx, quarantineRejectMatch.id);
|
|
1212
|
+
sendJson(res, 200, { rejected: quarantineRejectMatch.id });
|
|
1213
|
+
}
|
|
1214
|
+
catch (e) {
|
|
1215
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1216
|
+
if (msg.includes('not quarantined'))
|
|
1217
|
+
throw new HttpError(404, msg);
|
|
1218
|
+
if (msg.includes('is already'))
|
|
1219
|
+
throw new HttpError(409, msg);
|
|
1220
|
+
throw e;
|
|
1221
|
+
}
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1166
1224
|
// GET /v1/audit?op=&since=&limit= — read audit events. All three filters
|
|
1167
1225
|
// validated at the route boundary so an invalid value lands a 400 before
|
|
1168
1226
|
// we hit the DB.
|
package/dist/shared.js
CHANGED
|
@@ -13,6 +13,7 @@ import { search, hybridSearch } from './search.js';
|
|
|
13
13
|
import { evalNow } from './ablation.js';
|
|
14
14
|
import { deriveOriginProject, classifyOriginProject, resolveGlobalRootDir } from './project-identity.js';
|
|
15
15
|
import { detectSecret } from './secret-detect.js';
|
|
16
|
+
import { isQuarantineScope } from './quarantine.js';
|
|
16
17
|
import { RejectedValueError } from './rejection.js';
|
|
17
18
|
import { embedMemory, embedAll } from './embeddings.js';
|
|
18
19
|
/**
|
|
@@ -46,6 +47,10 @@ export function promoteToGlobal(localRoot, id, opts) {
|
|
|
46
47
|
const entry = readEntry(localRoot, id, opts?.tenantId);
|
|
47
48
|
if (!entry)
|
|
48
49
|
throw new Error(`Memory not found: ${id}`);
|
|
50
|
+
// CD5: same veto as shareMemory; a promoted copy would have no quarantine record to review.
|
|
51
|
+
if (isQuarantineScope(entry.scope)) {
|
|
52
|
+
throw new Error(`Refusing to promote ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
|
|
53
|
+
}
|
|
49
54
|
// v39 S4 producer veto: promote is a producer path to the global store
|
|
50
55
|
// exactly like shareMemory - same hard rule (codex gating review P2).
|
|
51
56
|
const promoteSecret = detectSecret(entry);
|
|
@@ -272,6 +277,10 @@ export function shareMemory(localRoot, id, options = {}) {
|
|
|
272
277
|
throw new Error(`Refusing to share ${id} to the global store: content matches secret material (${secret.reason}). ` +
|
|
273
278
|
`Secrets stay in their owning project's store.`);
|
|
274
279
|
}
|
|
280
|
+
// CD5: a quarantined row is unreviewed input, not a lesson; sharing it would spread poison globally.
|
|
281
|
+
if (isQuarantineScope(entry.scope)) {
|
|
282
|
+
throw new Error(`Refusing to share ${id}: it is quarantined pending review. Approve it first via 'hippo quarantine approve ${id}'.`);
|
|
283
|
+
}
|
|
275
284
|
const score = transferScore(entry);
|
|
276
285
|
if (score < 0.3 && !options.force)
|
|
277
286
|
return null;
|
|
@@ -385,6 +394,9 @@ export function autoShare(localRoot, options = {}) {
|
|
|
385
394
|
// Build set of global content hashes to avoid duplicates
|
|
386
395
|
const globalContentSet = new Set(globalEntries.map((e) => e.content.toLowerCase().trim().slice(0, 200)));
|
|
387
396
|
const candidates = localEntries.filter((entry) => {
|
|
397
|
+
// CD5: shareMemory refuses quarantined rows; filtering here keeps sleep from aborting on one.
|
|
398
|
+
if (isQuarantineScope(entry.scope ?? null))
|
|
399
|
+
return false;
|
|
388
400
|
const score = transferScore(entry);
|
|
389
401
|
if (score < minScore)
|
|
390
402
|
return false;
|
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.
|
|
19
|
+
export declare const PACKAGE_VERSION = "1.51.0";
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export declare function compareSemver(a: string, b: string): number;
|
|
22
22
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* an ESM `import` can resolve cleanly, and a hardcoded constant survives
|
|
17
17
|
* any packager that drops .json files.
|
|
18
18
|
*/
|
|
19
|
-
export const PACKAGE_VERSION = '1.
|
|
19
|
+
export const PACKAGE_VERSION = '1.51.0';
|
|
20
20
|
/** Compares plain x.y.z versions, positive if a > b; tags throw so the rollback guard never misfires silently. */
|
|
21
21
|
export function compareSemver(a, b) {
|
|
22
22
|
const parse = (v) => {
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "hippo-memory",
|
|
3
3
|
"name": "Hippo Memory",
|
|
4
4
|
"description": "Biologically-inspired memory for AI agents. Decay by default, retrieval strengthening, sleep consolidation.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.51.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hippo-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.51.0",
|
|
4
4
|
"description": "Biologically-inspired memory for AI agents. Zero runtime deps, SQLite, MCP server, and an opt-in hosted TypeSafe Jev reranker. Decay, retrieval strengthening, consolidation.",
|
|
5
5
|
"mcpName": "io.github.kitfunso/hippo-memory",
|
|
6
6
|
"type": "module",
|