hippo-memory 1.64.0 → 1.65.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/README.md +1 -1
- package/dist/agent-memories/sync.js +3 -2
- package/dist/api/recall-record.d.ts +46 -0
- package/dist/api/recall-record.js +98 -0
- package/dist/api/recall-request.d.ts +32 -0
- package/dist/api/recall-request.js +121 -0
- package/dist/api/recall-types.d.ts +7 -1
- package/dist/api/recall.d.ts +3 -2
- package/dist/api/recall.js +174 -181
- package/dist/api/tokens.d.ts +1 -1
- package/dist/api/tokens.js +13 -17
- package/dist/cli/maintenance.d.ts +2 -1
- package/dist/cli/maintenance.js +17 -11
- package/dist/cli/recall.d.ts +1 -1
- package/dist/cli/recall.js +18 -49
- package/dist/cli/shared.js +2 -2
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +18 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +4 -0
- package/dist/db/connect.d.ts +5 -0
- package/dist/db/connect.js +52 -0
- package/dist/db/open.d.ts +9 -11
- package/dist/db/open.js +23 -116
- package/dist/db/request-stores.d.ts +30 -0
- package/dist/db/request-stores.js +100 -0
- package/dist/db/sqlite-blocked.d.ts +6 -0
- package/dist/db/sqlite-blocked.js +11 -0
- package/dist/db.d.ts +3 -1
- package/dist/db.js +3 -1
- package/dist/embedding-provider.js +4 -3
- package/dist/embeddings.js +2 -1
- package/dist/goals.d.ts +18 -11
- package/dist/goals.js +38 -36
- package/dist/http-util.d.ts +2 -0
- package/dist/http-util.js +5 -0
- package/dist/local-embedding.d.ts +2 -0
- package/dist/local-embedding.js +20 -4
- package/dist/mcp/memory-tools.js +4 -1
- package/dist/mcp/recall-tools.js +28 -133
- package/dist/mcp/request.d.ts +7 -0
- package/dist/mcp/request.js +63 -41
- package/dist/mcp/session-state.d.ts +1 -3
- package/dist/mcp/session-state.js +3 -5
- package/dist/predictions/planning-fallacy.d.ts +52 -0
- package/dist/predictions/planning-fallacy.js +70 -55
- package/dist/recall-trace.d.ts +1 -1
- package/dist/recall-trace.js +5 -4
- package/dist/search/physics-search.js +4 -1
- package/dist/search/vector.js +2 -0
- package/dist/server/routes/recall.d.ts +1 -1
- package/dist/server/routes/recall.js +30 -218
- package/dist/server.d.ts +12 -1
- package/dist/server.js +33 -22
- package/dist/store/entry-writes.d.ts +2 -0
- package/dist/store/entry-writes.js +24 -16
- package/dist/store-port.d.ts +70 -1
- package/dist/store-port.js +87 -9
- 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/api/recall.js
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
// Read path: recall (sync) and retrieve (async, adds the vector arm).
|
|
2
2
|
import { envRequireSessionScopedFreshTail } from '../env.js';
|
|
3
|
-
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
4
3
|
import { DEFAULT_SEARCH_CANDIDATE_LIMIT } from '../store/rows.js';
|
|
5
|
-
import { strengthenRetrieved } from '../store/entry-writes.js';
|
|
6
4
|
import { isRecallBoostAblated } from '../ablation.js';
|
|
7
5
|
import { loadEntriesByIds, loadFreshRawMemories } from '../store/entry-reads.js';
|
|
8
6
|
import { loadRecallSearchEntries, recallScopeFilter } from '../store/search-rows.js';
|
|
9
|
-
import { loadActiveTaskSnapshot, listSessionEvents } from '../store/sessions.js';
|
|
10
|
-
import { loadLatestHandoff } from '../store/handoffs.js';
|
|
11
7
|
import { estimateTokens } from '../token-ledger.js';
|
|
12
8
|
import { formatHandoffEvidenceLine } from '../handoff.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { applyGoalStackBoost } from '../goals.js';
|
|
9
|
+
import { activeGoalsWithPolicies, boostByGoals } from '../goals.js';
|
|
10
|
+
import { continuityAt, finishRecallAt, storeFor } from '../store-port.js';
|
|
16
11
|
import { hybridSearch } from '../search/hybrid.js';
|
|
17
12
|
import { physicsSearch } from '../search/physics-search.js';
|
|
18
13
|
import { churnStaleFactor } from '../search/boosts.js';
|
|
@@ -20,11 +15,12 @@ import { compareEntryIdentity } from '../compare.js';
|
|
|
20
15
|
import { dropHeldCopies, duplicateKey, storedTextKeys } from '../same-text.js';
|
|
21
16
|
import { isSharedStore, loadConfig } from '../config.js';
|
|
22
17
|
import { classifyOriginProject, projectNames } from '../project-identity.js';
|
|
23
|
-
import {
|
|
18
|
+
import { decidePlanningFallacy, detectPlanningClaim, planningFallacyEvidenceAt } from '../predictions/planning-fallacy.js';
|
|
24
19
|
import { detectAnchoring, hashQueryText, biasHintEnabled } from '../recall-history.js';
|
|
25
20
|
import { detectAvailabilityBias } from '../availability.js';
|
|
26
21
|
import { passesScopeFilterForRecall, assertScopeRequestAllowed, personalScopeOf } from '../recall-scope.js';
|
|
27
22
|
import { ownerOrSubject, RecallContractError } from './types.js';
|
|
23
|
+
import { anchoringRows, availabilityRows, recallAuditMetadata, recallAuditRow } from './recall-record.js';
|
|
28
24
|
/**
|
|
29
25
|
* Shared construction helper for `RecallSuppressionSummary`. Used by
|
|
30
26
|
* `api.recall`, `cmdRecall`, and the MCP `hippo_recall` handler so all three
|
|
@@ -43,7 +39,8 @@ export function buildSuppressionSummary(counts) {
|
|
|
43
39
|
};
|
|
44
40
|
}
|
|
45
41
|
/**
|
|
46
|
-
* Domain-level recall.
|
|
42
|
+
* Domain-level recall on hippo.db only, never `ctx.store`, since it is synchronous: under another store its first open throws
|
|
43
|
+
* `SqliteBlockedError`. Loads BM25-ranked candidates from SQLite scoped to
|
|
47
44
|
* `ctx.tenantId` and keeps that order whatever `mode` says; `retrieve` is the
|
|
48
45
|
* mode-aware, strengthening variant the HTTP route uses.
|
|
49
46
|
*
|
|
@@ -62,7 +59,11 @@ export function recall(ctx, opts) {
|
|
|
62
59
|
assertScopeRequestAllowed(ctx.actor, opts.scope);
|
|
63
60
|
const windowSize = recallWindowSize(opts);
|
|
64
61
|
const own = personalScopeOf(ctx.actor) ?? undefined;
|
|
65
|
-
|
|
62
|
+
const all = loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false, recallOrigin(opts), own);
|
|
63
|
+
const plan = planRecall(ctx, opts, all, own);
|
|
64
|
+
const { result, writes } = composeRecall(ctx, opts, windowSize, all, plan, readRecallSync(ctx, opts, plan));
|
|
65
|
+
finishRecallAt(ctx.hippoRoot, writes);
|
|
66
|
+
return result;
|
|
66
67
|
}
|
|
67
68
|
function recallOrigin(opts) {
|
|
68
69
|
return opts.project ? projectNames(opts.project) : undefined;
|
|
@@ -70,7 +71,7 @@ function recallOrigin(opts) {
|
|
|
70
71
|
function inCallerProject(entry, opts) {
|
|
71
72
|
return !opts.project || classifyOriginProject(entry.origin_project, opts.project) !== 'cross-project';
|
|
72
73
|
}
|
|
73
|
-
/** Mode-aware recall that strengthens each returned row; never writes last_retrieval_ids (contract lock). */
|
|
74
|
+
/** Mode-aware recall through the served store that strengthens each returned row; never writes last_retrieval_ids (contract lock). */
|
|
74
75
|
export async function retrieve(ctx, opts) {
|
|
75
76
|
assertScopeRequestAllowed(ctx.actor, opts.scope);
|
|
76
77
|
const windowSize = recallWindowSize(opts);
|
|
@@ -78,7 +79,8 @@ export async function retrieve(ctx, opts) {
|
|
|
78
79
|
const own = personalScopeOf(ctx.actor) ?? undefined;
|
|
79
80
|
if (opts.showRanked)
|
|
80
81
|
return retrieveFromStore(ctx, opts, windowSize, opts.showRanked, own);
|
|
81
|
-
|
|
82
|
+
const store = storeFor(ctx);
|
|
83
|
+
let candidates = await store.searchRecallEntries(opts.query, recallSearchArgs(ctx, opts, windowSize, own));
|
|
82
84
|
if (opts.mode === 'hybrid' || opts.mode === 'physics') {
|
|
83
85
|
const searchOpts = { budget: Infinity, hippoRoot: ctx.hippoRoot, scope: opts.scope ?? null, vectorCandidates: recallVectorSpec(ctx, opts, own) };
|
|
84
86
|
const ranked = opts.mode === 'physics'
|
|
@@ -87,10 +89,26 @@ export async function retrieve(ctx, opts) {
|
|
|
87
89
|
const rankedIds = new Set(ranked.map((r) => r.entry.id));
|
|
88
90
|
candidates = [...ranked.map((r) => r.entry), ...candidates.filter((e) => !rankedIds.has(e.id))];
|
|
89
91
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
+
const plan = planRecall(ctx, opts, candidates, own);
|
|
93
|
+
const { result, writes } = composeRecall(ctx, opts, windowSize, candidates, plan, await readRecall(store, ctx, opts, plan));
|
|
94
|
+
await store.finishRecall({ ...writes, strengthen: strengthenOf(ctx, result.results.map((r) => r.id)) });
|
|
92
95
|
return result;
|
|
93
96
|
}
|
|
97
|
+
/** The recall load's arguments: exact scope, current rows only, the caller's project and personal scope. */
|
|
98
|
+
function recallSearchArgs(ctx, opts, limit, own) {
|
|
99
|
+
return {
|
|
100
|
+
limit,
|
|
101
|
+
tenantId: ctx.tenantId,
|
|
102
|
+
requestedScope: opts.scope,
|
|
103
|
+
explicitScopeMode: 'exact',
|
|
104
|
+
includeSuperseded: false,
|
|
105
|
+
originProjects: recallOrigin(opts),
|
|
106
|
+
ownScope: own,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function strengthenOf(ctx, ids) {
|
|
110
|
+
return { ids, opts: { tenantId: ctx.tenantId, recallBoostAblated: isRecallBoostAblated() } };
|
|
111
|
+
}
|
|
94
112
|
/** api.retrieve's vector arm: the recall load's exact-scope rule, current rows only. */
|
|
95
113
|
function recallVectorSpec(ctx, opts, own) {
|
|
96
114
|
return {
|
|
@@ -105,39 +123,44 @@ function recallVectorSpec(ctx, opts, own) {
|
|
|
105
123
|
const SHOW_RANKED_LEXICAL_WINDOW = 1000;
|
|
106
124
|
/** `retrieve` under `showRanked`: physics when `mode` says so, hybrid otherwise, over a wide lexical window plus the nearest vectors. */
|
|
107
125
|
async function retrieveFromStore(ctx, opts, windowSize, show, own) {
|
|
108
|
-
const
|
|
126
|
+
const store = storeFor(ctx);
|
|
127
|
+
const loaded = await store.searchRecallEntries(opts.query, recallSearchArgs(ctx, opts, Math.max(windowSize, SHOW_RANKED_LEXICAL_WINDOW), own));
|
|
109
128
|
const pool = loaded.filter((e) => passesScopeFilterForRecall(e.scope ?? null, opts.scope, own));
|
|
110
129
|
// No scope option: the scope boost follows HIPPO_SCOPE and the skill env, as MCP recall always ranked.
|
|
111
130
|
const searchOpts = { budget: Infinity, hippoRoot: ctx.hippoRoot, vectorCandidates: recallVectorSpec(ctx, opts, own) };
|
|
112
131
|
let ranked = opts.mode === 'physics'
|
|
113
132
|
? await physicsSearch(opts.query, pool, { ...searchOpts, physicsConfig: loadConfig(ctx.hippoRoot).physics })
|
|
114
133
|
: await hybridSearch(opts.query, pool, searchOpts);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
134
|
+
// One goal read serves both boosts; the ranked list's log rows come first, so its scores win the log's first-write.
|
|
135
|
+
const goals = opts.sessionId && !opts.goalTag ? await store.activeGoals({ sessionId: opts.sessionId, tenantId: ctx.tenantId }) : null;
|
|
136
|
+
let rankedLog = [];
|
|
137
|
+
if (goals && opts.sessionId) {
|
|
138
|
+
const boost = boostByGoals(ranked, goals, { sessionId: opts.sessionId, tenantId: ctx.tenantId, limit: ranked.length });
|
|
139
|
+
ranked = boost.results;
|
|
140
|
+
rankedLog = boost.log;
|
|
123
141
|
}
|
|
124
142
|
const window = ranked.slice(0, windowSize).map((r) => r.entry);
|
|
125
|
-
const
|
|
143
|
+
const bandOpts = { ...opts, suppressRecallTrace: true };
|
|
144
|
+
const plan = planRecall(ctx, bandOpts, window, own);
|
|
145
|
+
const { result, writes } = composeRecall(ctx, bandOpts, windowSize, window, plan, await readRecall(store, ctx, bandOpts, plan, goals), false);
|
|
126
146
|
// Rows the vector arm added count as candidates too.
|
|
127
147
|
const inPool = new Set(pool.map((e) => e.id));
|
|
128
148
|
const candidates = [...pool, ...ranked.map((r) => r.entry).filter((e) => !inPool.has(e.id))];
|
|
129
149
|
const shown = show({ ranked, pool: candidates, droppedByScope: loaded.length - pool.length }, result);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
150
|
+
const scores = new Map(ranked.map((r) => [r.entry.id, r.score]));
|
|
151
|
+
const shownRow = recallAuditRow(callerOf(ctx), 'recall', undefined, recallAuditMetadata(opts.query, shown.ids.length));
|
|
152
|
+
await store.finishRecall({
|
|
153
|
+
goalLog: [...rankedLog, ...writes.goalLog],
|
|
154
|
+
audit: [...writes.audit, shownRow, ...shown.audit],
|
|
155
|
+
trace: opts.suppressRecallTrace ? undefined : {
|
|
134
156
|
tenantId: ctx.tenantId,
|
|
135
157
|
sessionId: opts.sessionId ?? null,
|
|
136
158
|
pipeline: 'mcp',
|
|
137
159
|
query: opts.query,
|
|
138
|
-
results: shown.map((id) => ({ memoryId: id, score: scores.get(id) ?? 0 })),
|
|
139
|
-
}
|
|
140
|
-
|
|
160
|
+
results: shown.ids.map((id) => ({ memoryId: id, score: scores.get(id) ?? 0 })),
|
|
161
|
+
},
|
|
162
|
+
strengthen: strengthenOf(ctx, shown.ids),
|
|
163
|
+
});
|
|
141
164
|
return result;
|
|
142
165
|
}
|
|
143
166
|
/** Contract preflight: throws before any store-touching work. */
|
|
@@ -162,36 +185,66 @@ function recallWindowSize(opts) {
|
|
|
162
185
|
}
|
|
163
186
|
return opts.scorerWindow ?? DEFAULT_SEARCH_CANDIDATE_LIMIT;
|
|
164
187
|
}
|
|
165
|
-
|
|
188
|
+
const CONTINUITY_EVENT_LIMIT = 5;
|
|
189
|
+
function planRecall(ctx, opts, all, own) {
|
|
166
190
|
const limit = opts.limit ?? 10;
|
|
167
191
|
const window = admitCandidates(opts, all, limit, own);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
return {
|
|
193
|
+
limit,
|
|
194
|
+
window,
|
|
195
|
+
own,
|
|
196
|
+
goalBoost: opts.sessionId && !opts.goalTag ? { sessionId: opts.sessionId, tenantId: ctx.tenantId, limit } : null,
|
|
197
|
+
overflow: (opts.summarizeOverflow ?? true) && window.entries.length > limit ? overflowGroups(window, limit) : new Map(),
|
|
198
|
+
claim: detectPlanningClaim(opts.query),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** The plan's reads on hippo.db directly, for the synchronous recall that cannot await the port. */
|
|
202
|
+
function readRecallSync(ctx, opts, plan) {
|
|
203
|
+
const { hippoRoot, tenantId } = ctx;
|
|
204
|
+
const freshCount = opts.freshTailCount ?? 0;
|
|
205
|
+
return {
|
|
206
|
+
goals: plan.goalBoost ? activeGoalsWithPolicies(hippoRoot, { sessionId: plan.goalBoost.sessionId, tenantId }) : null,
|
|
207
|
+
parents: plan.overflow.size > 0 ? loadEntriesByIds(hippoRoot, [...plan.overflow.keys()], tenantId) : [],
|
|
208
|
+
freshRaws: freshCount > 0 ? loadFreshRawMemories(hippoRoot, freshCount, tenantId, opts.freshTailSessionId, recallOrigin(opts)) : [],
|
|
209
|
+
continuity: opts.includeContinuity ? continuityAt(hippoRoot, tenantId, CONTINUITY_EVENT_LIMIT, continuityKey(ctx, opts)) : undefined,
|
|
210
|
+
planning: plan.claim ? planningFallacyEvidenceAt(hippoRoot, tenantId, plan.claim.classQueryTokens) : null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/** The plan's reads through the store; `goals` already read by the caller skips that read. */
|
|
214
|
+
async function readRecall(store, ctx, opts, plan, goals) {
|
|
215
|
+
const { tenantId } = ctx;
|
|
216
|
+
const freshCount = opts.freshTailCount ?? 0;
|
|
217
|
+
return {
|
|
218
|
+
goals: goals !== undefined ? goals : plan.goalBoost ? await store.activeGoals({ sessionId: plan.goalBoost.sessionId, tenantId }) : null,
|
|
219
|
+
parents: plan.overflow.size > 0 ? await store.entriesByIds([...plan.overflow.keys()], tenantId) : [],
|
|
220
|
+
freshRaws: freshCount > 0 ? await store.freshRawEntries(freshCount, tenantId, opts.freshTailSessionId, recallOrigin(opts) ?? null) : [],
|
|
221
|
+
continuity: opts.includeContinuity ? await store.continuity(tenantId, CONTINUITY_EVENT_LIMIT, continuityKey(ctx, opts)) : undefined,
|
|
222
|
+
planning: plan.claim ? await store.planningFallacyEvidence(tenantId, plan.claim.classQueryTokens) : null,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// On a shared store the tenant's newest row is another developer's; no project keys to nothing, so the block is empty.
|
|
226
|
+
function continuityKey(ctx, opts) {
|
|
227
|
+
if (!isSharedStore(ctx.hippoRoot))
|
|
228
|
+
return null;
|
|
229
|
+
return { owner: ownerOrSubject(ctx.actor), project: opts.project ? projectNames(opts.project) : [] };
|
|
230
|
+
}
|
|
231
|
+
/** The reply and the rows it writes, from the candidates and the plan's reads; touches no store.
|
|
232
|
+
* `auditBand` false: the caller shows a cut of the band and audits the rows it shows. */
|
|
233
|
+
function composeRecall(ctx, opts, windowSize, all, plan, reads, auditBand = true) {
|
|
234
|
+
const { window } = plan;
|
|
235
|
+
const bands = rankBands(opts, plan, reads);
|
|
178
236
|
const rankedOut = bands.rankedOut;
|
|
179
|
-
const
|
|
180
|
-
? loadContinuity(ctx, opts, own)
|
|
181
|
-
: { continuity: undefined, continuityTokens: undefined };
|
|
237
|
+
const continuity = reads.continuity ? scopeContinuity(reads.continuity, opts, plan.own) : undefined;
|
|
182
238
|
// Query-derived, so MCP and CLI read this one hint instead of recomputing; HIPPO_AUTODEBIAS=off disables it.
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const planningFallacyWatching = planningFallacyOutput.watching ?? null;
|
|
187
|
-
const { anchoringHint, suppressedByInterference } = detectRecallAnchoring(ctx, opts, rankedOut[0]?.id ?? null);
|
|
188
|
-
const availabilityHint = detectRecallAvailability(ctx, opts, bands.baseSlice, window.entries);
|
|
239
|
+
const planning = plan.claim && reads.planning ? decidePlanningFallacy(plan.claim, reads.planning, ctx.tenantId, ctx.actor.subject) : null;
|
|
240
|
+
const { anchoringHint, suppressedByInterference } = detectRecallAnchoring(opts, rankedOut[0]?.id ?? null);
|
|
241
|
+
const availabilityHint = detectRecallAvailability(opts, bands.baseSlice, window.entries);
|
|
189
242
|
const result = {
|
|
190
243
|
results: rankedOut,
|
|
191
244
|
total: window.entries.length,
|
|
192
245
|
tokens: rankedOut.reduce((acc, r) => acc + estimateTokens(r.content), 0),
|
|
193
246
|
continuity,
|
|
194
|
-
continuityTokens,
|
|
247
|
+
continuityTokens: continuity ? continuityTokensOf(continuity) : undefined,
|
|
195
248
|
windowSize,
|
|
196
249
|
suppressionSummary: buildSuppressionSummary({
|
|
197
250
|
totalCandidates: all.length,
|
|
@@ -202,15 +255,21 @@ function recallFrom(ctx, opts, windowSize, all, own) {
|
|
|
202
255
|
suppressedByInterference,
|
|
203
256
|
}),
|
|
204
257
|
};
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
258
|
+
// The hint and the no-class-match / tiebreak watching variant are mutually exclusive; both go out as optional fields.
|
|
259
|
+
if (planning?.output.hint)
|
|
260
|
+
result.planningFallacyHint = planning.output.hint;
|
|
261
|
+
if (planning?.output.watching)
|
|
262
|
+
result.planningFallacyWatching = planning.output.watching;
|
|
209
263
|
if (anchoringHint)
|
|
210
264
|
result.anchoringHint = anchoringHint;
|
|
211
265
|
if (availabilityHint)
|
|
212
266
|
result.availabilityHint = availabilityHint;
|
|
213
|
-
|
|
267
|
+
const who = callerOf(ctx);
|
|
268
|
+
const audit = auditBand ? [recallAuditRow(who, 'recall', undefined, recallAuditMetadata(opts.query, rankedOut.length))] : [];
|
|
269
|
+
if (planning?.audit)
|
|
270
|
+
audit.push(planning.audit);
|
|
271
|
+
audit.push(...anchoringRows(who, anchoringHint), ...availabilityRows(who, availabilityHint));
|
|
272
|
+
return { result, writes: { goalLog: bands.goalLog, audit, trace: recallTrace(ctx, opts, rankedOut) } };
|
|
214
273
|
}
|
|
215
274
|
// The SQL load already applied tenant and scope; any recall-mode loader must go through loadRecallSearchEntries.
|
|
216
275
|
function admitCandidates(opts, all, limit, own) {
|
|
@@ -234,24 +293,21 @@ function admitCandidates(opts, all, limit, own) {
|
|
|
234
293
|
return { entries, baseSlice, droppedPreRank, droppedByBudget: entries.length - baseSlice.length };
|
|
235
294
|
}
|
|
236
295
|
// The goal-stack boost touches the primary band only; the fresh-tail and summary bands keep their fixed placement.
|
|
237
|
-
function rankBands(
|
|
296
|
+
function rankBands(opts, plan, reads) {
|
|
297
|
+
const { window, limit, own } = plan;
|
|
238
298
|
let baseScored = window.baseSlice.map((entry, idx) => ({
|
|
239
299
|
entry,
|
|
240
300
|
score: Math.max(0, 1 - idx / Math.max(1, limit)),
|
|
241
301
|
}));
|
|
242
302
|
// Allocated only under explain, so the boost's default-path math stays byte-identical.
|
|
243
303
|
const explainTrace = opts.explain ? new Map() : undefined;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
trace: explainTrace,
|
|
250
|
-
});
|
|
304
|
+
let goalLog = [];
|
|
305
|
+
if (plan.goalBoost && reads.goals) {
|
|
306
|
+
const boost = boostByGoals(baseScored, reads.goals, { ...plan.goalBoost, trace: explainTrace });
|
|
307
|
+
baseScored = boost.results;
|
|
308
|
+
goalLog = boost.log;
|
|
251
309
|
}
|
|
252
|
-
let substituted = (opts.
|
|
253
|
-
? substituteOverflow(ctx, opts, window.entries, baseScored.map((r) => r.entry), limit, own)
|
|
254
|
-
: [];
|
|
310
|
+
let substituted = substituteOverflow(opts, plan.overflow, reads.parents, limit, own);
|
|
255
311
|
let heldDropped = 0;
|
|
256
312
|
if (!opts.keepHeldCopies) {
|
|
257
313
|
const shownIds = new Set(dropHeldCopies([...baseScored.map((r) => r.entry), ...substituted.map((s) => s.entry)], (e) => e).map((e) => e.id));
|
|
@@ -263,7 +319,7 @@ function rankBands(db, ctx, opts, window, limit, own) {
|
|
|
263
319
|
const baseRanked = baseScored.map((r) => baseItem(r, opts, explainTrace));
|
|
264
320
|
const summaryRanked = substituted.map((s) => summaryItem(s, opts));
|
|
265
321
|
const freshRanked = (opts.freshTailCount ?? 0) > 0
|
|
266
|
-
? freshTailBand(
|
|
322
|
+
? freshTailBand(opts, reads.freshRaws, baseRanked, summaryRanked, opts.keepHeldCopies ? [] : [...baseSlice, ...substituted.map((s) => s.entry)], own)
|
|
267
323
|
: [];
|
|
268
324
|
return {
|
|
269
325
|
rankedOut: [...freshRanked, ...baseRanked, ...summaryRanked],
|
|
@@ -271,28 +327,12 @@ function rankBands(db, ctx, opts, window, limit, own) {
|
|
|
271
327
|
heldDropped,
|
|
272
328
|
summarySubstitutions: substituted.length,
|
|
273
329
|
freshTailAdded: freshRanked.length,
|
|
330
|
+
goalLog,
|
|
274
331
|
};
|
|
275
332
|
}
|
|
276
333
|
// When the overflow holds 2+ children of one level-2 summary, that summary stands in for them, capped at 30% of
|
|
277
334
|
// `limit`. Each one is tenant-scoped and re-checked against the scope filter; drillDown recovers the children.
|
|
278
|
-
function substituteOverflow(
|
|
279
|
-
const overflow = entries.slice(limit);
|
|
280
|
-
const baseIds = new Set(baseSlice.map((e) => e.id));
|
|
281
|
-
const overflowByParent = new Map();
|
|
282
|
-
for (const e of overflow) {
|
|
283
|
-
const parentId = e.dag_parent_id;
|
|
284
|
-
if (!parentId)
|
|
285
|
-
continue;
|
|
286
|
-
if ((e.dag_level ?? 0) > 1)
|
|
287
|
-
continue;
|
|
288
|
-
const list = overflowByParent.get(parentId) ?? [];
|
|
289
|
-
list.push(e);
|
|
290
|
-
overflowByParent.set(parentId, list);
|
|
291
|
-
}
|
|
292
|
-
const eligibleParentIds = Array.from(overflowByParent.keys()).filter((pid) => (overflowByParent.get(pid)?.length ?? 0) >= 2 && !baseIds.has(pid));
|
|
293
|
-
if (eligibleParentIds.length === 0)
|
|
294
|
-
return [];
|
|
295
|
-
const parents = loadEntriesByIds(ctx.hippoRoot, eligibleParentIds, ctx.tenantId);
|
|
335
|
+
function substituteOverflow(opts, overflowByParent, parents, limit, own) {
|
|
296
336
|
const eligibleParents = parents.filter((p) => (p.dag_level ?? 0) === 2 && !p.superseded_by && passesScopeFilterForRecall(p.scope ?? null, opts.scope, own) && inCallerProject(p, opts));
|
|
297
337
|
const maxSub = Math.max(1, Math.ceil(limit * 0.3));
|
|
298
338
|
// Most overflowed children first; compareEntryIdentity only breaks a tie, which used to fall to scan order.
|
|
@@ -306,6 +346,23 @@ function substituteOverflow(ctx, opts, entries, baseSlice, limit, own) {
|
|
|
306
346
|
childIds: (overflowByParent.get(p.id) ?? []).map((e) => e.id),
|
|
307
347
|
}));
|
|
308
348
|
}
|
|
349
|
+
/** Overflowed leaf rows by summary parent, kept only for a parent with 2+ of them that is not already in the base band. */
|
|
350
|
+
function overflowGroups(window, limit) {
|
|
351
|
+
// The goal boost only re-scores the base band, so its ids are known before the boost runs.
|
|
352
|
+
const baseIds = new Set(window.baseSlice.map((e) => e.id));
|
|
353
|
+
const overflowByParent = new Map();
|
|
354
|
+
for (const e of window.entries.slice(limit)) {
|
|
355
|
+
const parentId = e.dag_parent_id;
|
|
356
|
+
if (!parentId)
|
|
357
|
+
continue;
|
|
358
|
+
if ((e.dag_level ?? 0) > 1)
|
|
359
|
+
continue;
|
|
360
|
+
const list = overflowByParent.get(parentId) ?? [];
|
|
361
|
+
list.push(e);
|
|
362
|
+
overflowByParent.set(parentId, list);
|
|
363
|
+
}
|
|
364
|
+
return new Map([...overflowByParent].filter(([pid, children]) => children.length >= 2 && !baseIds.has(pid)));
|
|
365
|
+
}
|
|
309
366
|
function baseItem(r, opts, explainTrace) {
|
|
310
367
|
const item = {
|
|
311
368
|
id: r.entry.id,
|
|
@@ -341,10 +398,8 @@ function summaryItem(s, opts) {
|
|
|
341
398
|
}
|
|
342
399
|
// The last N raw rows, so "what did I just see" always covers the recent window. A recent row already in the BM25
|
|
343
400
|
// band is only tagged isFreshTail; new ones are prepended, so every recent row appears exactly once.
|
|
344
|
-
function freshTailBand(
|
|
345
|
-
|
|
346
|
-
const recent = loadFreshRawMemories(ctx.hippoRoot, opts.freshTailCount ?? 0, ctx.tenantId, opts.freshTailSessionId, recallOrigin(opts));
|
|
347
|
-
const recentScoped = recent.filter((m) => passesScopeFilterForRecall(m.scope ?? null, opts.scope, own));
|
|
401
|
+
function freshTailBand(opts, recent, baseRanked, summaryRanked, shownEntries, own) {
|
|
402
|
+
const recentScoped = recent.filter((m) => passesScopeFilterForRecall(m.scope ?? null, opts.scope, own) && inCallerProject(m, opts));
|
|
348
403
|
const recentIdSet = new Set(recentScoped.map((m) => m.id));
|
|
349
404
|
for (const r of baseRanked) {
|
|
350
405
|
if (recentIdSet.has(r.id))
|
|
@@ -372,58 +427,32 @@ function freshTailBand(ctx, opts, baseRanked, summaryRanked, shownEntries, own)
|
|
|
372
427
|
}
|
|
373
428
|
return freshRanked;
|
|
374
429
|
}
|
|
375
|
-
// The
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
430
|
+
// The trace is observability, not retrieval state; a caller that traces its own result set suppresses it.
|
|
431
|
+
function recallTrace(ctx, opts, rankedOut) {
|
|
432
|
+
if (opts.suppressRecallTrace)
|
|
433
|
+
return undefined;
|
|
434
|
+
return {
|
|
379
435
|
tenantId: ctx.tenantId,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
sessionId: opts.sessionId ?? null,
|
|
391
|
-
pipeline: 'api',
|
|
392
|
-
query: opts.query,
|
|
393
|
-
explainMode: opts.explain === true,
|
|
394
|
-
results: rankedOut.map((r) => ({
|
|
395
|
-
memoryId: r.id,
|
|
396
|
-
score: r.score,
|
|
397
|
-
rerankSteps: r.rerankTrace,
|
|
398
|
-
})),
|
|
399
|
-
});
|
|
400
|
-
}
|
|
436
|
+
sessionId: opts.sessionId ?? null,
|
|
437
|
+
pipeline: 'api',
|
|
438
|
+
query: opts.query,
|
|
439
|
+
explainMode: opts.explain === true,
|
|
440
|
+
results: rankedOut.map((r) => ({
|
|
441
|
+
memoryId: r.id,
|
|
442
|
+
score: r.score,
|
|
443
|
+
rerankSteps: r.rerankTrace,
|
|
444
|
+
})),
|
|
445
|
+
};
|
|
401
446
|
}
|
|
402
|
-
//
|
|
403
|
-
function
|
|
404
|
-
// On a shared store the tenant's newest row is another developer's; no project keys to nothing, so the block is empty.
|
|
405
|
-
const key = isSharedStore(ctx.hippoRoot)
|
|
406
|
-
? { owner: ownerOrSubject(ctx.actor), project: opts.project ? projectNames(opts.project) : [] }
|
|
407
|
-
: undefined;
|
|
408
|
-
const snapshot = loadActiveTaskSnapshot(ctx.hippoRoot, ctx.tenantId, key);
|
|
409
|
-
const sessionId = snapshot?.session_id ?? undefined;
|
|
410
|
-
const sessionHandoff = sessionId
|
|
411
|
-
? loadLatestHandoff(ctx.hippoRoot, ctx.tenantId, sessionId, {}, key)
|
|
412
|
-
: null;
|
|
413
|
-
const recentSessionEvents = sessionId
|
|
414
|
-
? listSessionEvents(ctx.hippoRoot, ctx.tenantId, { session_id: sessionId, limit: 5 })
|
|
415
|
-
: [];
|
|
416
|
-
// The memory-recall scope rule: an explicit scope must match exactly; without one, private and legacy rows are denied.
|
|
447
|
+
// The memory-recall scope rule: an explicit scope must match exactly; without one, private and legacy rows are denied.
|
|
448
|
+
function scopeContinuity(block, opts, own) {
|
|
417
449
|
const rowScope = (r) => r?.scope ?? null;
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
sessionHandoff: filteredHandoff,
|
|
424
|
-
recentSessionEvents: filteredEvents,
|
|
450
|
+
const { activeSnapshot: snapshot, sessionHandoff } = block;
|
|
451
|
+
return {
|
|
452
|
+
activeSnapshot: snapshot && passesScopeFilterForRecall(rowScope(snapshot), opts.scope, own) ? snapshot : null,
|
|
453
|
+
sessionHandoff: sessionHandoff && passesScopeFilterForRecall(rowScope(sessionHandoff), opts.scope, own) ? sessionHandoff : null,
|
|
454
|
+
recentSessionEvents: block.recentSessionEvents.filter((e) => passesScopeFilterForRecall(rowScope(e), opts.scope, own)),
|
|
425
455
|
};
|
|
426
|
-
return { continuity, continuityTokens: continuityTokensOf(continuity) };
|
|
427
456
|
}
|
|
428
457
|
function continuityTokensOf(c) {
|
|
429
458
|
const filteredSnapshot = c.activeSnapshot;
|
|
@@ -442,62 +471,26 @@ function continuityTokensOf(c) {
|
|
|
442
471
|
tokenize(filteredHandoff?.cardId) +
|
|
443
472
|
c.recentSessionEvents.reduce((acc, e) => acc + tokenize(e.content), 0);
|
|
444
473
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const db = openHippoDb(ctx.hippoRoot);
|
|
448
|
-
try {
|
|
449
|
-
appendAuditEvent(db, { tenantId: ctx.tenantId, actor: ctx.actor.subject, ...event });
|
|
450
|
-
}
|
|
451
|
-
finally {
|
|
452
|
-
closeHippoDb(db);
|
|
453
|
-
}
|
|
474
|
+
function callerOf(ctx) {
|
|
475
|
+
return { tenantId: ctx.tenantId, actor: ctx.actor.subject };
|
|
454
476
|
}
|
|
455
477
|
// A pure read of the caller's recallHistory snapshot against this top-1. HIPPO_ANCHORING=off skips even the detect
|
|
456
478
|
// call; CLI paths pass no history (cmdRecall computes its own hint), so the hint stays absent there.
|
|
457
|
-
function detectRecallAnchoring(
|
|
479
|
+
function detectRecallAnchoring(opts, topMemoryId) {
|
|
458
480
|
if (!biasHintEnabled('anchoring') || !opts.recallHistory)
|
|
459
481
|
return { anchoringHint: null, suppressedByInterference: 0 };
|
|
460
482
|
const queryHash = hashQueryText(opts.query);
|
|
461
483
|
const anchoringHint = detectAnchoring(opts.recallHistory, queryHash, topMemoryId);
|
|
462
|
-
|
|
463
|
-
appendRecallAudit(ctx, {
|
|
464
|
-
op: 'recall_anchor_detected_memory_dominance',
|
|
465
|
-
targetId: anchoringHint.memoryId,
|
|
466
|
-
metadata: {
|
|
467
|
-
memory_id: anchoringHint.memoryId,
|
|
468
|
-
query_count: anchoringHint.queryCount ?? null,
|
|
469
|
-
},
|
|
470
|
-
});
|
|
471
|
-
return { anchoringHint, suppressedByInterference: 1 };
|
|
472
|
-
}
|
|
473
|
-
if (anchoringHint?.reason === 'query_repeat') {
|
|
474
|
-
appendRecallAudit(ctx, {
|
|
475
|
-
op: 'recall_anchor_detected_query_repeat',
|
|
476
|
-
targetId: anchoringHint.memoryId,
|
|
477
|
-
metadata: { memory_id: anchoringHint.memoryId },
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
return { anchoringHint, suppressedByInterference: 0 };
|
|
484
|
+
return { anchoringHint, suppressedByInterference: anchoringHint?.reason === 'memory_dominance' ? 1 : 0 };
|
|
481
485
|
}
|
|
482
486
|
// Compares the returned top-K's ages with the scope-filtered pool it came from, never `all`, whose hidden rows would
|
|
483
487
|
// leak pool shape. A soft warning only; HIPPO_AVAILABILITY=off or a caller computing its own hint skips it.
|
|
484
|
-
function detectRecallAvailability(
|
|
488
|
+
function detectRecallAvailability(opts, baseSlice, entries) {
|
|
485
489
|
if (!biasHintEnabled('availability') || opts.suppressAvailabilityHint)
|
|
486
490
|
return null;
|
|
487
|
-
|
|
491
|
+
return detectAvailabilityBias({
|
|
488
492
|
topK: baseSlice.map((e) => ({ id: e.id, created: e.created })),
|
|
489
493
|
pool: entries.map((e) => ({ id: e.id, created: e.created })),
|
|
490
494
|
});
|
|
491
|
-
if (availabilityHint) {
|
|
492
|
-
appendRecallAudit(ctx, {
|
|
493
|
-
op: 'recall_availability_detected',
|
|
494
|
-
metadata: {
|
|
495
|
-
recent_fraction: availabilityHint.recentFraction,
|
|
496
|
-
older_passed_over: availabilityHint.olderCandidatesPassedOver,
|
|
497
|
-
returned_count: availabilityHint.returnedCount,
|
|
498
|
-
},
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
return availabilityHint;
|
|
502
495
|
}
|
|
503
496
|
//# sourceMappingURL=recall.js.map
|
package/dist/api/tokens.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare function recordTokens(ctx: Context, surface: TokenSurface, use: {
|
|
|
10
10
|
items: number;
|
|
11
11
|
tokens: number;
|
|
12
12
|
sessionId?: string | null;
|
|
13
|
-
}): void
|
|
13
|
+
}): Promise<void>;
|
|
14
14
|
/**
|
|
15
15
|
* Token ledger totals for the tenant over the last `days` days (default 30):
|
|
16
16
|
* tokens sent, skipped as unchanged and re-read by later model calls, per
|
package/dist/api/tokens.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Token-use and failure reports.
|
|
2
|
-
import { openHippoDb, closeHippoDb } from '../db.js';
|
|
3
|
-
import {
|
|
2
|
+
import { openHippoDb, closeHippoDb, rethrowIfSqliteBlocked } from '../db.js';
|
|
3
|
+
import { summarizeTokenUse } from '../token-ledger.js';
|
|
4
|
+
import { storeFor } from '../store-port.js';
|
|
4
5
|
import { summarizeFailures } from '../failure-log.js';
|
|
5
6
|
import { log } from '../log.js';
|
|
6
7
|
/**
|
|
@@ -8,24 +9,19 @@ import { log } from '../log.js';
|
|
|
8
9
|
* Best-effort: never throws, because a ledger failure must not fail the
|
|
9
10
|
* recall or context call that produced the text.
|
|
10
11
|
*/
|
|
11
|
-
export function recordTokens(ctx, surface, use) {
|
|
12
|
+
export async function recordTokens(ctx, surface, use) {
|
|
12
13
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
tokens: use.tokens,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
finally {
|
|
25
|
-
closeHippoDb(db);
|
|
26
|
-
}
|
|
14
|
+
await storeFor(ctx).recordTokens({
|
|
15
|
+
tenantId: ctx.tenantId,
|
|
16
|
+
sessionId: use.sessionId ?? null,
|
|
17
|
+
surface,
|
|
18
|
+
event: 'inject',
|
|
19
|
+
items: use.items,
|
|
20
|
+
tokens: use.tokens,
|
|
21
|
+
});
|
|
27
22
|
}
|
|
28
23
|
catch (err) {
|
|
24
|
+
rethrowIfSqliteBlocked(err);
|
|
29
25
|
log.warnThenDebug('api-token-ledger', `token ledger write failed; the reply is unaffected: ${err instanceof Error ? err.message : String(err)}`);
|
|
30
26
|
}
|
|
31
27
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import { type EmbeddingProvider } from '../embedding-provider.js';
|
|
1
2
|
export declare function cmdRefine(hippoRoot: string, flags: Record<string, string | boolean | string[]>): Promise<void>;
|
|
2
3
|
export declare function cmdDedup(hippoRoot: string, flags: Record<string, string | boolean | string[]>): void;
|
|
3
|
-
export declare function cmdEmbed(hippoRoot: string, flags: Record<string, string | boolean | string[]
|
|
4
|
+
export declare function cmdEmbed(hippoRoot: string, flags: Record<string, string | boolean | string[]>, given?: EmbeddingProvider): Promise<void>;
|
|
4
5
|
//# sourceMappingURL=maintenance.d.ts.map
|