hippo-memory 1.52.8 → 1.52.9
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 +158 -98
- package/dist/api.d.ts +51 -18
- package/dist/api.js +121 -76
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +63 -0
- package/dist/capture.d.ts +37 -0
- package/dist/capture.js +111 -81
- package/dist/cli.js +627 -654
- package/dist/codex-patch.d.ts +12 -0
- package/dist/codex-patch.js +71 -0
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -4
- package/dist/connectors/slack/types.d.ts +0 -1
- package/dist/consolidate.js +85 -32
- package/dist/context-render.d.ts +36 -0
- package/dist/context-render.js +154 -0
- package/dist/dag.js +3 -2
- package/dist/db.js +6 -6
- package/dist/dedupe.d.ts +6 -6
- package/dist/dedupe.js +10 -9
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +35 -2
- package/dist/dormant.d.ts +4 -0
- package/dist/dormant.js +17 -2
- package/dist/embedding-provider.d.ts +2 -1
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +23 -3
- package/dist/extract.js +5 -1
- package/dist/forward-claim-detector.d.ts +1 -1
- package/dist/forward-claim-detector.js +1 -1
- package/dist/graph-recall.d.ts +3 -1
- package/dist/graph-recall.js +5 -3
- package/dist/hooks.d.ts +15 -1
- package/dist/hooks.js +122 -27
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +171 -210
- package/dist/merged-row.d.ts +6 -0
- package/dist/merged-row.js +35 -0
- package/dist/multihop.d.ts +2 -1
- package/dist/multihop.js +7 -4
- package/dist/physics-state.d.ts +0 -4
- package/dist/physics-state.js +0 -6
- package/dist/predictions.d.ts +2 -17
- package/dist/predictions.js +2 -15
- package/dist/reject-flow.d.ts +7 -5
- package/dist/reject-flow.js +41 -12
- package/dist/salience.js +12 -5
- package/dist/same-text.d.ts +17 -0
- package/dist/same-text.js +38 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.js +8 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.js +16 -32
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +6 -0
- package/dist/server-detect.js +9 -33
- package/dist/server.js +6 -62
- package/dist/session-digest.d.ts +79 -0
- package/dist/session-digest.js +528 -0
- package/dist/shared.d.ts +10 -2
- package/dist/shared.js +35 -30
- package/dist/store.d.ts +1 -0
- package/dist/store.js +4 -0
- package/dist/token-ledger.d.ts +46 -8
- package/dist/token-ledger.js +140 -21
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/README.md +4 -4
- package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// A sleep-merged row copies its sources' texts, so retiring a text (reject, or superseding its source) must reach the row too.
|
|
2
|
+
// SHORTCUT: reject rewrites merged rows at once, live and dormant; supersede and `resolve --reject-loser` wait for the next sleep's check, which sees a dormant row only once restored.
|
|
3
|
+
import { generateId } from './memory.js';
|
|
4
|
+
import { duplicateKey, heldTexts, mergedText } from './same-text.js';
|
|
5
|
+
/** The row that replaces a merged row once its retired texts leave: undefined when it holds none, null when nothing else is left. */
|
|
6
|
+
export function mergedSuccessor(row, retired, retiredIds) {
|
|
7
|
+
const texts = heldTexts(row);
|
|
8
|
+
const kept = texts.filter((t) => !retired(t));
|
|
9
|
+
if (kept.length === texts.length)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (kept.length === 0)
|
|
12
|
+
return null;
|
|
13
|
+
const count = `${kept.length} related ${kept.length === 1 ? 'memory' : 'memories'}`;
|
|
14
|
+
const header = row.content.slice(0, row.content.indexOf('\n\n')).replace(/\d+ related memor(?:y|ies)/, count);
|
|
15
|
+
// A new id, as a fresh merge would get; age and half-life carry over so the rewrite buys the row no extra life.
|
|
16
|
+
return {
|
|
17
|
+
...row,
|
|
18
|
+
id: generateId('sem'),
|
|
19
|
+
content: mergedText(header, kept),
|
|
20
|
+
parents: row.parents.filter((id) => !retiredIds.has(id)),
|
|
21
|
+
conflicts_with: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Sleep's check on a merged row: drops texts whose source was superseded since the merge, or that a rejection now covers. */
|
|
25
|
+
export function successorAfterRetirement(row, byId, rejected) {
|
|
26
|
+
if (row.source !== 'consolidation' || row.superseded_by)
|
|
27
|
+
return undefined;
|
|
28
|
+
const sources = row.parents.flatMap((id) => byId.get(id) ?? []);
|
|
29
|
+
const retiredIds = new Set(sources.filter((s) => Boolean(s.superseded_by) || rejected(s.content)).map((s) => s.id));
|
|
30
|
+
const live = new Set(sources.filter((s) => !retiredIds.has(s.id)).map((s) => duplicateKey(s.content)));
|
|
31
|
+
const gone = new Set(sources.filter((s) => retiredIds.has(s.id)).map((s) => duplicateKey(s.content)));
|
|
32
|
+
const retired = (text) => (gone.has(duplicateKey(text)) && !live.has(duplicateKey(text))) || rejected(text);
|
|
33
|
+
return mergedSuccessor(row, retired, retiredIds);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=merged-row.js.map
|
package/dist/multihop.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { MemoryEntry } from './memory.js';
|
|
2
|
-
import { type SearchResult } from './search.js';
|
|
2
|
+
import { type ResultCost, type SearchResult } from './search.js';
|
|
3
3
|
export declare function multihopSearch(query: string, entries: MemoryEntry[], options?: {
|
|
4
4
|
budget?: number;
|
|
5
5
|
now?: Date;
|
|
6
6
|
hippoRoot?: string;
|
|
7
7
|
minResults?: number;
|
|
8
|
+
cost?: ResultCost;
|
|
8
9
|
includeSuperseded?: boolean;
|
|
9
10
|
asOf?: string;
|
|
10
11
|
}): SearchResult[];
|
package/dist/multihop.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { search } from './search.js';
|
|
1
|
+
import { fitBudget, search } from './search.js';
|
|
2
2
|
export function multihopSearch(query, entries, options = {}) {
|
|
3
|
-
const
|
|
3
|
+
const budget = options.budget ?? 4000;
|
|
4
|
+
// Pass 1 searches wide to find entities, so each return fits the caller's budget, as search() does.
|
|
5
|
+
const fit = (ordered) => fitBudget(ordered, budget, options.minResults ?? 1, options.cost);
|
|
6
|
+
const pass1 = search(query, entries, { ...options, budget: budget * 2 });
|
|
4
7
|
const topK = pass1.slice(0, 10);
|
|
5
8
|
if (topK.length === 0)
|
|
6
9
|
return [];
|
|
@@ -17,7 +20,7 @@ export function multihopSearch(query, entries, options = {}) {
|
|
|
17
20
|
.map((t) => t.split(':')[1])
|
|
18
21
|
.filter((e) => !queryLower.includes(e.toLowerCase()));
|
|
19
22
|
if (newEntities.length === 0)
|
|
20
|
-
return pass1;
|
|
23
|
+
return fit(pass1);
|
|
21
24
|
const followUpQuery = newEntities.join(' ') + ' ' + query;
|
|
22
25
|
const pass2 = search(followUpQuery, entries, options);
|
|
23
26
|
const merged = new Map();
|
|
@@ -30,6 +33,6 @@ export function multihopSearch(query, entries, options = {}) {
|
|
|
30
33
|
// T2 note: PLAIN stable score sort on purpose -- pass1/pass2 inputs are
|
|
31
34
|
// deterministically ordered (search() carries the content tail), stability
|
|
32
35
|
// inherits that, and ties keep pass-1 results ahead of pass-2 follow-ups.
|
|
33
|
-
return [...merged.values()].sort((a, b) => b.score - a.score);
|
|
36
|
+
return fit([...merged.values()].sort((a, b) => b.score - a.score));
|
|
34
37
|
}
|
|
35
38
|
//# sourceMappingURL=multihop.js.map
|
package/dist/physics-state.d.ts
CHANGED
|
@@ -26,10 +26,6 @@ export declare function savePhysicsState(db: DatabaseSyncLike, particles: Physic
|
|
|
26
26
|
* Returns the new particle (does not persist — caller must save).
|
|
27
27
|
*/
|
|
28
28
|
export declare function initializeParticle(entry: MemoryEntry, embedding: number[], now?: Date): PhysicsParticle;
|
|
29
|
-
/**
|
|
30
|
-
* Delete physics state for a memory. (Also handled by CASCADE, but explicit for clarity.)
|
|
31
|
-
*/
|
|
32
|
-
export declare function deletePhysicsState(db: DatabaseSyncLike, memoryId: string): void;
|
|
33
29
|
/**
|
|
34
30
|
* Reset all physics states from original embeddings.
|
|
35
31
|
* Drops existing physics data and re-initializes from the embedding index.
|
package/dist/physics-state.js
CHANGED
|
@@ -129,12 +129,6 @@ export function initializeParticle(entry, embedding, now = evalNow()) {
|
|
|
129
129
|
lastSimulation: now.toISOString(),
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Delete physics state for a memory. (Also handled by CASCADE, but explicit for clarity.)
|
|
134
|
-
*/
|
|
135
|
-
export function deletePhysicsState(db, memoryId) {
|
|
136
|
-
db.prepare('DELETE FROM memory_physics WHERE memory_id = ?').run(memoryId);
|
|
137
|
-
}
|
|
138
132
|
/**
|
|
139
133
|
* Reset all physics states from original embeddings.
|
|
140
134
|
* Drops existing physics data and re-initializes from the embedding index.
|
package/dist/predictions.d.ts
CHANGED
|
@@ -116,7 +116,7 @@ export interface PredictionBaserate {
|
|
|
116
116
|
*/
|
|
117
117
|
export declare function computePredictionBaserate(hippoRoot: string, tenantId: string, classTag: string, actor?: string,
|
|
118
118
|
/** v0.32 / J3.2 — when false, skip the predict_baserate audit emit. The
|
|
119
|
-
* J3.2 orchestrator (
|
|
119
|
+
* J3.2 orchestrator (computePlanningFallacyOutput, below) calls this with
|
|
120
120
|
* emitAudit=false and emits its own `recall_autodebias_hint` audit row
|
|
121
121
|
* instead, so the predict_baserate channel stays scoped to deliberate
|
|
122
122
|
* CLI / HTTP / MCP predict-baserate calls and does NOT pollute on every
|
|
@@ -183,10 +183,6 @@ export interface PlanningFallacyWatching {
|
|
|
183
183
|
* `computePlanningFallacyOutput`. Carries EITHER `hint` (baserate
|
|
184
184
|
* available) OR `watching` (regex fired, no baserate), or NEITHER (mode=off,
|
|
185
185
|
* no queryText, no regex match, or nClosed=0 silent path). Never both.
|
|
186
|
-
*
|
|
187
|
-
* Existing `computePlanningFallacyHint` (preserved as a backward-compat
|
|
188
|
-
* wrapper) returns only the hint variant; new code should call
|
|
189
|
-
* `computePlanningFallacyOutput` directly to surface the watching variant.
|
|
190
186
|
*/
|
|
191
187
|
export interface PlanningFallacyOutput {
|
|
192
188
|
hint?: PlanningFallacyHint;
|
|
@@ -206,8 +202,7 @@ export interface ComputePlanningFallacyHintOpts {
|
|
|
206
202
|
actor?: string;
|
|
207
203
|
}
|
|
208
204
|
/**
|
|
209
|
-
* J3.2 orchestrator
|
|
210
|
-
* below for the backward-compat wrapper that returns only the hint variant).
|
|
205
|
+
* J3.2 orchestrator.
|
|
211
206
|
*
|
|
212
207
|
* Composes the forward-claim detector + class resolver + baserate compute,
|
|
213
208
|
* with telemetry-grade audit emission at every decision point (success,
|
|
@@ -233,14 +228,4 @@ export interface ComputePlanningFallacyHintOpts {
|
|
|
233
228
|
* on full match+resolve+baserate path. Well under 50ms target.
|
|
234
229
|
*/
|
|
235
230
|
export declare function computePlanningFallacyOutput(hippoRoot: string, tenantId: string, queryText: string, opts?: ComputePlanningFallacyHintOpts): PlanningFallacyOutput;
|
|
236
|
-
/**
|
|
237
|
-
* v1.13.4 backward-compat wrapper: thin shim around
|
|
238
|
-
* computePlanningFallacyOutput that returns only the hint variant.
|
|
239
|
-
* Existing callers (api.recall, cmdRecall, MCP handler) that don't yet
|
|
240
|
-
* consume the watching variant continue to work unchanged.
|
|
241
|
-
*
|
|
242
|
-
* New callers that want to surface the silent no-class-match / tiebreak
|
|
243
|
-
* paths to users should call computePlanningFallacyOutput directly.
|
|
244
|
-
*/
|
|
245
|
-
export declare function computePlanningFallacyHint(hippoRoot: string, tenantId: string, queryText: string, opts?: ComputePlanningFallacyHintOpts): PlanningFallacyHint | null;
|
|
246
231
|
//# sourceMappingURL=predictions.d.ts.map
|
package/dist/predictions.js
CHANGED
|
@@ -284,7 +284,7 @@ export function loadPredictionsByClass(hippoRoot, tenantId, classTag, opts = {})
|
|
|
284
284
|
*/
|
|
285
285
|
export function computePredictionBaserate(hippoRoot, tenantId, classTag, actor = 'cli',
|
|
286
286
|
/** v0.32 / J3.2 — when false, skip the predict_baserate audit emit. The
|
|
287
|
-
* J3.2 orchestrator (
|
|
287
|
+
* J3.2 orchestrator (computePlanningFallacyOutput, below) calls this with
|
|
288
288
|
* emitAudit=false and emits its own `recall_autodebias_hint` audit row
|
|
289
289
|
* instead, so the predict_baserate channel stays scoped to deliberate
|
|
290
290
|
* CLI / HTTP / MCP predict-baserate calls and does NOT pollute on every
|
|
@@ -487,8 +487,7 @@ function resolveClassFromTokens(hippoRoot, tenantId, queryTokens) {
|
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
489
|
/**
|
|
490
|
-
* J3.2 orchestrator
|
|
491
|
-
* below for the backward-compat wrapper that returns only the hint variant).
|
|
490
|
+
* J3.2 orchestrator.
|
|
492
491
|
*
|
|
493
492
|
* Composes the forward-claim detector + class resolver + baserate compute,
|
|
494
493
|
* with telemetry-grade audit emission at every decision point (success,
|
|
@@ -616,16 +615,4 @@ export function computePlanningFallacyOutput(hippoRoot, tenantId, queryText, opt
|
|
|
616
615
|
},
|
|
617
616
|
};
|
|
618
617
|
}
|
|
619
|
-
/**
|
|
620
|
-
* v1.13.4 backward-compat wrapper: thin shim around
|
|
621
|
-
* computePlanningFallacyOutput that returns only the hint variant.
|
|
622
|
-
* Existing callers (api.recall, cmdRecall, MCP handler) that don't yet
|
|
623
|
-
* consume the watching variant continue to work unchanged.
|
|
624
|
-
*
|
|
625
|
-
* New callers that want to surface the silent no-class-match / tiebreak
|
|
626
|
-
* paths to users should call computePlanningFallacyOutput directly.
|
|
627
|
-
*/
|
|
628
|
-
export function computePlanningFallacyHint(hippoRoot, tenantId, queryText, opts = {}) {
|
|
629
|
-
return computePlanningFallacyOutput(hippoRoot, tenantId, queryText, opts).hint ?? null;
|
|
630
|
-
}
|
|
631
618
|
//# sourceMappingURL=predictions.js.map
|
package/dist/reject-flow.d.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* SAME multi-step transaction + post-commit mirror-purge flow. Extracted
|
|
8
8
|
* here (leaf module) so neither duplicates it.
|
|
9
9
|
*
|
|
10
|
-
* Module direction: this file imports from store.ts, rejection.ts,
|
|
11
|
-
*
|
|
10
|
+
* Module direction: this file imports from store.ts, rejection.ts, raw-archive.ts,
|
|
11
|
+
* dormant.ts, same-text.ts and merged-row.ts. Nothing imports FROM this file except
|
|
12
12
|
* cli.ts and api.ts, so it introduces no cycle.
|
|
13
13
|
*/
|
|
14
14
|
import { type RejectedValueRow } from './rejection.js';
|
|
@@ -28,12 +28,14 @@ export interface RejectFlowResult {
|
|
|
28
28
|
* tombstone itself stores no content — this is the only place it's seen
|
|
29
29
|
* again after this call returns). */
|
|
30
30
|
content: string;
|
|
31
|
-
/** Every
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
/** Every row removed this call, live or dormant: all whose normalized digest matched (not just the id
|
|
32
|
+
* passed, per the K1/R7 duplicate lesson), and each sleep-merged row holding the value, whose other
|
|
33
|
+
* texts move to a new row: listed in successorIds when it was live, dormantSuccessorIds when dormant. */
|
|
34
34
|
removedIds: string[];
|
|
35
35
|
/** Subset of removedIds that were kind='raw' (archived, not deleted). */
|
|
36
36
|
removedRawIds: string[];
|
|
37
|
+
successorIds: string[];
|
|
38
|
+
dormantSuccessorIds: string[];
|
|
37
39
|
}
|
|
38
40
|
/**
|
|
39
41
|
* `hippo reject` / `api.reject` core flow. ONE connection, one transaction:
|
package/dist/reject-flow.js
CHANGED
|
@@ -7,15 +7,17 @@
|
|
|
7
7
|
* SAME multi-step transaction + post-commit mirror-purge flow. Extracted
|
|
8
8
|
* here (leaf module) so neither duplicates it.
|
|
9
9
|
*
|
|
10
|
-
* Module direction: this file imports from store.ts, rejection.ts,
|
|
11
|
-
*
|
|
10
|
+
* Module direction: this file imports from store.ts, rejection.ts, raw-archive.ts,
|
|
11
|
+
* dormant.ts, same-text.ts and merged-row.ts. Nothing imports FROM this file except
|
|
12
12
|
* cli.ts and api.ts, so it introduces no cycle.
|
|
13
13
|
*/
|
|
14
14
|
import { closeHippoDb } from './db.js';
|
|
15
15
|
import { appendAuditEvent } from './audit.js';
|
|
16
16
|
import { archiveRawMemory } from './raw-archive.js';
|
|
17
|
-
import { purgeDormantByDigest } from './dormant.js';
|
|
18
|
-
import { openStore, deleteEntryCore, purgeMirrorBestEffort, } from './store.js';
|
|
17
|
+
import { deleteDormantRow, listDormantSnapshots, purgeDormantByDigest, replaceDormantEntry } from './dormant.js';
|
|
18
|
+
import { openStore, deleteEntryCore, purgeMirrorBestEffort, selectAllEntries, stampOriginProject, writeEntryDbOnly, writeEntryMirrors, } from './store.js';
|
|
19
|
+
import { heldTexts } from './same-text.js';
|
|
20
|
+
import { mergedSuccessor } from './merged-row.js';
|
|
19
21
|
import { rejectionDigest, normalizeValueForRejection, insertRejectedValue, deleteRejectedValue, listRejectedValues, } from './rejection.js';
|
|
20
22
|
/**
|
|
21
23
|
* `hippo reject` / `api.reject` core flow. ONE connection, one transaction:
|
|
@@ -66,6 +68,8 @@ export function rejectValue(opts) {
|
|
|
66
68
|
const now = new Date().toISOString();
|
|
67
69
|
const removedIds = [];
|
|
68
70
|
const removedRawIds = [];
|
|
71
|
+
const successors = [];
|
|
72
|
+
const dormantSuccessorIds = [];
|
|
69
73
|
db.exec('BEGIN');
|
|
70
74
|
try {
|
|
71
75
|
insertRejectedValue(db, {
|
|
@@ -80,13 +84,14 @@ export function rejectValue(opts) {
|
|
|
80
84
|
// O(N) scan over the tenant's rows (plan §4): human-triggered command
|
|
81
85
|
// on ~1-5k-row stores — acceptable, documented. A digest column on
|
|
82
86
|
// memories is the escape if stores grow 100x; not needed now.
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const holdsValue = (text) => rejectionDigest(text) === digest;
|
|
88
|
+
const merged = [];
|
|
89
|
+
for (const row of selectAllEntries(db, opts.tenantId)) {
|
|
90
|
+
if (!holdsValue(row.content)) {
|
|
91
|
+
if (heldTexts(row).some(holdsValue))
|
|
92
|
+
merged.push(row);
|
|
89
93
|
continue;
|
|
94
|
+
}
|
|
90
95
|
if (row.kind === 'raw') {
|
|
91
96
|
// Append-only trigger respected — archiveRawMemory is the only
|
|
92
97
|
// legitimate removal path for kind='raw', and its inner SAVEPOINT
|
|
@@ -102,12 +107,34 @@ export function rejectValue(opts) {
|
|
|
102
107
|
}
|
|
103
108
|
removedIds.push(row.id);
|
|
104
109
|
}
|
|
105
|
-
|
|
110
|
+
for (const row of merged) {
|
|
111
|
+
const successor = mergedSuccessor(row, holdsValue, new Set(removedIds));
|
|
112
|
+
deleteEntryCore(db, row.id, { actor: opts.actor, suppressForgetAudit: true });
|
|
113
|
+
removedIds.push(row.id);
|
|
114
|
+
if (!successor)
|
|
115
|
+
continue;
|
|
116
|
+
const kept = stampOriginProject(opts.hippoRoot, successor);
|
|
117
|
+
writeEntryDbOnly(db, kept, { actor: opts.actor });
|
|
118
|
+
successors.push(kept);
|
|
119
|
+
}
|
|
120
|
+
// Dormant copies (src/dormant.ts), whole or inside a merged row, go too, in the same transaction: a
|
|
106
121
|
// rejected value may not linger where `hippo dormant restore` could
|
|
107
122
|
// bring it back. They have no markdown mirror, so the post-commit
|
|
108
123
|
// mirror purge below is a no-op for them; they join removedIds for the
|
|
109
124
|
// audit trail and the caller's report.
|
|
110
125
|
removedIds.push(...purgeDormantByDigest(db, opts.tenantId, digest));
|
|
126
|
+
for (const dormant of listDormantSnapshots(db, opts.tenantId)) {
|
|
127
|
+
const successor = mergedSuccessor(dormant.entry, holdsValue, new Set(removedIds));
|
|
128
|
+
if (successor === undefined)
|
|
129
|
+
continue;
|
|
130
|
+
removedIds.push(dormant.entry.id);
|
|
131
|
+
if (!successor) {
|
|
132
|
+
deleteDormantRow(db, opts.tenantId, dormant.entry.id);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
replaceDormantEntry(db, opts.tenantId, dormant.entry.id, successor);
|
|
136
|
+
dormantSuccessorIds.push(successor.id);
|
|
137
|
+
}
|
|
111
138
|
try {
|
|
112
139
|
appendAuditEvent(db, {
|
|
113
140
|
tenantId: opts.tenantId,
|
|
@@ -149,7 +176,9 @@ export function rejectValue(opts) {
|
|
|
149
176
|
db.prepare(`UPDATE raw_archive SET mirror_cleaned_at = ? WHERE memory_id = ?`).run(new Date().toISOString(), id);
|
|
150
177
|
}
|
|
151
178
|
}
|
|
152
|
-
|
|
179
|
+
for (const successor of successors)
|
|
180
|
+
writeEntryMirrors(opts.hippoRoot, successor);
|
|
181
|
+
return { digest, content, removedIds, removedRawIds, successorIds: successors.map((s) => s.id), dormantSuccessorIds };
|
|
153
182
|
}
|
|
154
183
|
finally {
|
|
155
184
|
closeHippoDb(db);
|
package/dist/salience.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* not everything that enters working memory deserves long-term storage.
|
|
7
7
|
*/
|
|
8
8
|
import { textOverlap } from './search.js';
|
|
9
|
+
import { duplicateKey, heldTextKeys } from './same-text.js';
|
|
9
10
|
const DEFAULTS = {
|
|
10
11
|
recentWindow: 20,
|
|
11
12
|
overlapThreshold: 0.6,
|
|
@@ -37,11 +38,17 @@ export function computeSalience(content, tags, recentMemories, options = {}) {
|
|
|
37
38
|
}
|
|
38
39
|
return { decision: 'store', reason: 'error_despite_overlap', score: 0.7 };
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
// A near-duplicate may be a changed value (port 8080 then 8081), so only the same text is skipped.
|
|
42
|
+
const key = duplicateKey(trimmed);
|
|
43
|
+
const same = window.find((m) => heldTextKeys(m).includes(key));
|
|
44
|
+
if (!same) {
|
|
45
|
+
return {
|
|
46
|
+
decision: 'store',
|
|
47
|
+
reason: `near_duplicate (${(duplicateMatch.overlap * 100).toFixed(0)}% overlap with ${duplicateMatch.matchId})`,
|
|
48
|
+
score: 0.5,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { decision: 'skip', reason: `duplicate (same text as ${same.id})`, score: 0.1 };
|
|
45
52
|
}
|
|
46
53
|
if (isError) {
|
|
47
54
|
return { decision: 'store', reason: 'error_novel', score: 0.9 };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MemoryEntry } from './memory.js';
|
|
2
|
+
type Text = Pick<MemoryEntry, 'content'> & {
|
|
3
|
+
source?: string;
|
|
4
|
+
pinned?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function duplicateKey(text: string): string;
|
|
7
|
+
/** A sleep-merged row's text: a header, a blank line, then each text as a "- " bullet with its later lines indented. */
|
|
8
|
+
export declare function mergedText(header: string, texts: readonly string[]): string;
|
|
9
|
+
/** Each source text a sleep-merged row holds; a two-row merge by an older release kept one text whole after its header. */
|
|
10
|
+
export declare function heldTexts(entry: Text): string[];
|
|
11
|
+
/** Keys of every text a row holds word for word: its own, plus each source text inside a sleep-merged row. */
|
|
12
|
+
export declare function heldTextKeys(entry: Text): string[];
|
|
13
|
+
export declare function storedTextKeys(entries: readonly Text[]): Set<string>;
|
|
14
|
+
/** A final result list without copies: drops each unpinned row a sleep-merged row in the list holds word for word, and each later unpinned copy of a text. */
|
|
15
|
+
export declare function dropHeldCopies<T>(rows: readonly T[], textOf: (row: T) => Text): T[];
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=same-text.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function duplicateKey(text) {
|
|
2
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
3
|
+
}
|
|
4
|
+
/** A sleep-merged row's text: a header, a blank line, then each text as a "- " bullet with its later lines indented. */
|
|
5
|
+
export function mergedText(header, texts) {
|
|
6
|
+
return `${header}\n\n${texts.map((t) => `- ${t.trim().replace(/\n/g, '\n ')}`).join('\n')}`;
|
|
7
|
+
}
|
|
8
|
+
/** Each source text a sleep-merged row holds; a two-row merge by an older release kept one text whole after its header. */
|
|
9
|
+
export function heldTexts(entry) {
|
|
10
|
+
const cut = entry.content.indexOf('\n\n');
|
|
11
|
+
if (entry.source !== 'consolidation' || cut < 0)
|
|
12
|
+
return [];
|
|
13
|
+
const body = entry.content.slice(cut + 2);
|
|
14
|
+
if (/^\[Consolidated from \d+ related memories\]$/.test(entry.content.slice(0, cut)))
|
|
15
|
+
return [body];
|
|
16
|
+
return `\n${body}`.split('\n- ').slice(1).map((t) => t.replace(/\n {2}/g, '\n'));
|
|
17
|
+
}
|
|
18
|
+
/** Keys of every text a row holds word for word: its own, plus each source text inside a sleep-merged row. */
|
|
19
|
+
export function heldTextKeys(entry) {
|
|
20
|
+
return [duplicateKey(entry.content), ...heldTexts(entry).map(duplicateKey)];
|
|
21
|
+
}
|
|
22
|
+
export function storedTextKeys(entries) {
|
|
23
|
+
return new Set(entries.flatMap(heldTextKeys));
|
|
24
|
+
}
|
|
25
|
+
/** A final result list without copies: drops each unpinned row a sleep-merged row in the list holds word for word, and each later unpinned copy of a text. */
|
|
26
|
+
export function dropHeldCopies(rows, textOf) {
|
|
27
|
+
const held = new Set(rows.flatMap((r) => heldTextKeys(textOf(r)).slice(1)));
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
return rows.filter((r) => {
|
|
30
|
+
const keys = heldTextKeys(textOf(r));
|
|
31
|
+
const hidden = seen.has(keys[0]) || (keys.length === 1 && held.has(keys[0]));
|
|
32
|
+
if (hidden && !textOf(r).pinned)
|
|
33
|
+
return false; // rows merged by older releases can hold a pin's text
|
|
34
|
+
seen.add(keys[0]);
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=same-text.js.map
|
package/dist/scheduler.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export declare function saveWorkspaceRegistry(globalRoot: string, registry: Work
|
|
|
25
25
|
export declare function registerWorkspace(globalRoot: string, projectDir: string): WorkspaceRegistry;
|
|
26
26
|
export declare function listRegisteredWorkspaces(globalRoot: string): string[];
|
|
27
27
|
export declare function buildDailyRunnerCommand(projectDir: string, platform?: NodeJS.Platform): string;
|
|
28
|
+
/** What the Windows task runs. A plain `cmd /c` task opens a visible console every morning. */
|
|
29
|
+
export declare function buildWindowsTaskRun(cmd: string): string;
|
|
30
|
+
/** Argv for `schtasks`, passed without a shell: through cmd.exe the `&&` in /tr split the command. */
|
|
31
|
+
export declare function buildSchtasksCreateArgs(taskName: string, cmd: string): string[];
|
|
28
32
|
export declare function runDailyMaintenance(workspaces: readonly string[], runCommand: (cwd: string, args: string[]) => void): void;
|
|
29
33
|
export {};
|
|
30
34
|
//# sourceMappingURL=scheduler.d.ts.map
|
package/dist/scheduler.js
CHANGED
|
@@ -65,6 +65,14 @@ export function buildDailyRunnerCommand(projectDir, platform = process.platform)
|
|
|
65
65
|
const resolved = path.posix.resolve(projectDir.replace(/\\/g, '/'));
|
|
66
66
|
return `cd "${resolved}" && hippo daily-runner`;
|
|
67
67
|
}
|
|
68
|
+
/** What the Windows task runs. A plain `cmd /c` task opens a visible console every morning. */
|
|
69
|
+
export function buildWindowsTaskRun(cmd) {
|
|
70
|
+
return `conhost.exe --headless cmd /c ${cmd}`;
|
|
71
|
+
}
|
|
72
|
+
/** Argv for `schtasks`, passed without a shell: through cmd.exe the `&&` in /tr split the command. */
|
|
73
|
+
export function buildSchtasksCreateArgs(taskName, cmd) {
|
|
74
|
+
return ['/create', '/tn', taskName, '/tr', buildWindowsTaskRun(cmd), '/sc', 'daily', '/st', '06:15', '/f'];
|
|
75
|
+
}
|
|
68
76
|
export function runDailyMaintenance(workspaces, runCommand) {
|
|
69
77
|
for (const workspace of workspaces) {
|
|
70
78
|
const resolved = normalizeWorkspace(workspace);
|
package/dist/search.d.ts
CHANGED
|
@@ -84,6 +84,9 @@ export interface SearchResult {
|
|
|
84
84
|
direction: 'from' | 'to';
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
/** What a result costs against a budget: the tokens of the text it prints as. */
|
|
88
|
+
export type ResultCost = (r: SearchResult) => number;
|
|
89
|
+
export declare function fitBudget<T extends SearchResult>(ordered: T[], budget: number, minResults: number, cost?: ResultCost): T[];
|
|
87
90
|
export interface ScoreBreakdown {
|
|
88
91
|
/**
|
|
89
92
|
* - `hybrid`: BM25 blended with a non-zero cosine from a cached doc vector.
|
|
@@ -186,6 +189,8 @@ export declare function hybridSearch(query: string, entries: MemoryEntry[], opti
|
|
|
186
189
|
/** Minimum number of results to return regardless of budget.
|
|
187
190
|
* Prevents budget saturation when memories are large. Default 1. */
|
|
188
191
|
minResults?: number;
|
|
192
|
+
/** Budget cost per result; the caller that prints passes the cost of its printed text. */
|
|
193
|
+
cost?: ResultCost;
|
|
189
194
|
/** Active scope for scope-boost scoring. Auto-detected if not provided. */
|
|
190
195
|
scope?: string | null;
|
|
191
196
|
/** Include superseded memories in results. Default false. */
|
|
@@ -249,6 +254,7 @@ export declare function physicsSearch(query: string, entries: MemoryEntry[], opt
|
|
|
249
254
|
queryEmbedding?: number[];
|
|
250
255
|
explain?: boolean;
|
|
251
256
|
minResults?: number;
|
|
257
|
+
cost?: ResultCost;
|
|
252
258
|
/** Active scope for scope-boost scoring. Auto-detected if not provided. */
|
|
253
259
|
scope?: string | null;
|
|
254
260
|
/** Include superseded memories. Default false. Must be threaded through
|
|
@@ -277,6 +283,7 @@ export declare function search(query: string, entries: MemoryEntry[], options?:
|
|
|
277
283
|
now?: Date;
|
|
278
284
|
hippoRoot?: string;
|
|
279
285
|
minResults?: number;
|
|
286
|
+
cost?: ResultCost;
|
|
280
287
|
includeSuperseded?: boolean;
|
|
281
288
|
asOf?: string;
|
|
282
289
|
}): SearchResult[];
|
package/dist/search.js
CHANGED
|
@@ -138,6 +138,19 @@ export function temporalBoost(entry, direction, range) {
|
|
|
138
138
|
return 0.8 + 0.4 * (1 - normalized);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
+
// Skip-and-continue, with the first minResults kept whatever they cost; one loop so every engine spends alike.
|
|
142
|
+
export function fitBudget(ordered, budget, minResults, cost) {
|
|
143
|
+
const results = [];
|
|
144
|
+
let used = 0;
|
|
145
|
+
for (const r of ordered) {
|
|
146
|
+
const tokens = cost ? cost(r) : r.tokens;
|
|
147
|
+
if (results.length >= minResults && used + tokens > budget)
|
|
148
|
+
continue;
|
|
149
|
+
used += tokens;
|
|
150
|
+
results.push(r);
|
|
151
|
+
}
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
141
154
|
// ---------------------------------------------------------------------------
|
|
142
155
|
// v0.30 / E4 — DAG L2 summary scoring helpers
|
|
143
156
|
// ---------------------------------------------------------------------------
|
|
@@ -526,17 +539,7 @@ export async function hybridSearch(query, entries, options = {}) {
|
|
|
526
539
|
const withPostRank = reranked.map((r, i) => ({ ...r, postRerankRank: i + 1 }));
|
|
527
540
|
ordered = [...withPostRank, ...tail];
|
|
528
541
|
}
|
|
529
|
-
|
|
530
|
-
const results = [];
|
|
531
|
-
let usedTokens = 0;
|
|
532
|
-
for (let i = 0; i < ordered.length; i++) {
|
|
533
|
-
const tokens = ordered[i].tokens;
|
|
534
|
-
if (results.length >= minResults && usedTokens + tokens > budget)
|
|
535
|
-
continue;
|
|
536
|
-
usedTokens += tokens;
|
|
537
|
-
results.push(ordered[i]);
|
|
538
|
-
}
|
|
539
|
-
return results;
|
|
542
|
+
return fitBudget(ordered, budget, minResults, options.cost);
|
|
540
543
|
}
|
|
541
544
|
/**
|
|
542
545
|
* MMR (Maximal Marginal Relevance) re-ranking.
|
|
@@ -764,16 +767,7 @@ export async function physicsSearch(query, entries, options = {}) {
|
|
|
764
767
|
const merged = mergeScorePools(physicsResults, classicResults);
|
|
765
768
|
// Sort and apply budget, deterministic tiebreak on ties.
|
|
766
769
|
merged.sort(compareScoredResults);
|
|
767
|
-
|
|
768
|
-
let usedTokens = 0;
|
|
769
|
-
for (let i = 0; i < merged.length; i++) {
|
|
770
|
-
const tokens = merged[i].tokens;
|
|
771
|
-
if (results.length >= minResults && usedTokens + tokens > budget)
|
|
772
|
-
continue;
|
|
773
|
-
usedTokens += tokens;
|
|
774
|
-
results.push(merged[i]);
|
|
775
|
-
}
|
|
776
|
-
return results;
|
|
770
|
+
return fitBudget(merged, budget, minResults, options.cost);
|
|
777
771
|
}
|
|
778
772
|
/** Normalize two score pools to [0,1] and combine. */
|
|
779
773
|
function mergeScorePools(poolA, poolB) {
|
|
@@ -906,17 +900,7 @@ export function search(query, entries, options = {}) {
|
|
|
906
900
|
}
|
|
907
901
|
dedupedSync.sort(compareScoredResults);
|
|
908
902
|
}
|
|
909
|
-
|
|
910
|
-
const results = [];
|
|
911
|
-
let usedTokens = 0;
|
|
912
|
-
for (let i = 0; i < dedupedSync.length; i++) {
|
|
913
|
-
const tokens = dedupedSync[i].tokens;
|
|
914
|
-
if (results.length >= minResults && usedTokens + tokens > budget)
|
|
915
|
-
continue;
|
|
916
|
-
usedTokens += tokens;
|
|
917
|
-
results.push(dedupedSync[i]);
|
|
918
|
-
}
|
|
919
|
-
return results;
|
|
903
|
+
return fitBudget(dedupedSync, budget, minResults, options.cost);
|
|
920
904
|
}
|
|
921
905
|
/**
|
|
922
906
|
* Update retrieval metadata on entries that were returned by a search.
|
package/dist/secret-detect.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export declare function detectSecret(entry: {
|
|
|
38
38
|
* CS1 pre-compact snapshot fields — can scrub it in place instead.
|
|
39
39
|
*/
|
|
40
40
|
export declare function redactSecrets(text: string): string;
|
|
41
|
+
/** Memories never hold a raw email address (AGENTS.md); phone numbers are left alone, as their patterns misfire on ids. */
|
|
42
|
+
export declare function maskEmails(text: string): string;
|
|
41
43
|
/** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer and Basic auth headers and JWTs. */
|
|
42
44
|
export declare function redactSecretsStrict(text: string): string;
|
|
43
45
|
//# sourceMappingURL=secret-detect.d.ts.map
|
package/dist/secret-detect.js
CHANGED
|
@@ -90,6 +90,12 @@ export function detectSecret(entry) {
|
|
|
90
90
|
export function redactSecrets(text) {
|
|
91
91
|
return redactText(text, false);
|
|
92
92
|
}
|
|
93
|
+
/** A domain's first label must open on a letter, so asset names like logo@2x.png and pins like react@18.2.0 stay. */
|
|
94
|
+
const EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z][A-Za-z0-9-]*(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}\b/g;
|
|
95
|
+
/** Memories never hold a raw email address (AGENTS.md); phone numbers are left alone, as their patterns misfire on ids. */
|
|
96
|
+
export function maskEmails(text) {
|
|
97
|
+
return text.replace(EMAIL, '[email]');
|
|
98
|
+
}
|
|
93
99
|
/** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer and Basic auth headers and JWTs. */
|
|
94
100
|
export function redactSecretsStrict(text) {
|
|
95
101
|
return redactText(text, true);
|