hippo-memory 1.13.1 → 1.13.3
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 +48 -6
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +53 -1
- package/dist/api.js.map +1 -1
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +119 -25
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -0
- package/dist/client.js.map +1 -1
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +140 -37
- package/dist/mcp/server.js.map +1 -1
- package/dist/recall-history.d.ts +127 -0
- package/dist/recall-history.d.ts.map +1 -0
- package/dist/recall-history.js +235 -0
- package/dist/recall-history.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +78 -0
- package/dist/server.js.map +1 -1
- package/dist/src/api.js +53 -1
- package/dist/src/api.js.map +1 -1
- package/dist/src/audit.js.map +1 -1
- package/dist/src/cli.js +119 -25
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.js +12 -0
- package/dist/src/client.js.map +1 -1
- package/dist/src/mcp/server.js +140 -37
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/recall-history.js +235 -0
- package/dist/src/recall-history.js.map +1 -0
- package/dist/src/server.js +78 -0
- package/dist/src/server.js.map +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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* J1 anchoring detector (recall-recurrence) — pure module.
|
|
3
|
+
*
|
|
4
|
+
* Implements two detection rules from ROADMAP-RESEARCH.md L546:
|
|
5
|
+
* R1 query_repeat: same queryHash within recentRepeatWindow returned
|
|
6
|
+
* same topMemoryId (caller is re-asking the same question).
|
|
7
|
+
* R2 memory_dominance: same topMemoryId across >= minDominance distinct
|
|
8
|
+
* queryHashes (memory acts as a fixed-point anchor regardless of what
|
|
9
|
+
* the agent asks).
|
|
10
|
+
*
|
|
11
|
+
* Per the plan v3 architectural decision: each pipeline (api.recall via
|
|
12
|
+
* HTTP, cmdRecall, MCP hippo_recall) owns its OWN ring buffer Map keyed
|
|
13
|
+
* by (tenant, session). No cross-pipeline sharing (the typical multi-
|
|
14
|
+
* process deployment makes IPC ring-sharing impractical; per-pipeline
|
|
15
|
+
* is correct because each pipeline has its own top-1 ranking anyway).
|
|
16
|
+
*
|
|
17
|
+
* Plan: docs/plans/2026-05-26-j1-anchoring-detector.md.
|
|
18
|
+
* Composes with J3.2: AnchoringHint + PlanningFallacyHint are independent
|
|
19
|
+
* signals; both can fire on the same recall.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_MIN_DOMINANCE = 3;
|
|
22
|
+
const DEFAULT_RECENT_REPEAT_WINDOW = 5;
|
|
23
|
+
const DEFAULT_COOLDOWN = 3;
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Query text normalization + hashing
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Normalize + hash a query text into a 32-bit integer.
|
|
29
|
+
* Lowercase → strip non-alphanumeric → split → drop empty + short tokens →
|
|
30
|
+
* sort tokens → join → FNV-1a 32-bit.
|
|
31
|
+
*
|
|
32
|
+
* Token sort + dedup means semantically-equivalent queries with reordered
|
|
33
|
+
* words collide intentionally (the roadmap's "semantically-distinct" v1
|
|
34
|
+
* uses textual normalization; embedding-based distinctness is J1-v2).
|
|
35
|
+
*
|
|
36
|
+
* Deterministic across processes; stable across Node + V8 versions.
|
|
37
|
+
*/
|
|
38
|
+
export function hashQueryText(query) {
|
|
39
|
+
if (!query)
|
|
40
|
+
return 0;
|
|
41
|
+
// Token dedup before join: the R2 contract says "distinct queries"
|
|
42
|
+
// means semantically distinct, so `foo bar` and `foo foo bar` should
|
|
43
|
+
// collapse to the same hash. Without dedup, simple phrasing
|
|
44
|
+
// variations (typos, doubled tokens, intensifiers) would inflate
|
|
45
|
+
// distinct-query counts and trip R2 on essentially the same question.
|
|
46
|
+
// Codex round-1 catch.
|
|
47
|
+
// Unicode-aware tokenization (codex round-3 P2 catch): the prior
|
|
48
|
+
// ASCII-only [^a-z0-9\s] pattern stripped every non-Latin letter, so
|
|
49
|
+
// Japanese, Arabic, Cyrillic, accented-Latin etc. queries collapsed
|
|
50
|
+
// to empty token set -> hash 0 -> false R1 collisions across distinct
|
|
51
|
+
// non-English queries. \p{L} = any Unicode letter, \p{N} = any Unicode
|
|
52
|
+
// number, \p{M} = combining marks (preserve composed accented chars).
|
|
53
|
+
// Requires the /u flag and Node >= 12.
|
|
54
|
+
const normalized = query
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.replace(/[^\p{L}\p{N}\p{M}\s]/gu, ' ')
|
|
57
|
+
.split(/\s+/)
|
|
58
|
+
.filter((t) => t.length > 0);
|
|
59
|
+
// Drop tokens shorter than 3 chars to match the normalizer contract
|
|
60
|
+
// (filler / stop words). Without this, `a login bug` vs `login bug`
|
|
61
|
+
// hash differently and inflate R2 distinct-query counts, firing
|
|
62
|
+
// memory_dominance on repeated phrasings of the same question.
|
|
63
|
+
// Codex round-4 P2 catch. Matches the same >=3 filter in
|
|
64
|
+
// src/forward-claim-detector.ts for class-resolver tokens.
|
|
65
|
+
const filtered = normalized.filter((t) => t.length >= 3);
|
|
66
|
+
// Fallback when the >=3 filter would collapse the entire query to
|
|
67
|
+
// empty: CJK queries like `测试` / `环境` are 2-char tokens; English
|
|
68
|
+
// acronyms like `AI` / `UI` are 2 chars. Without this fallback they
|
|
69
|
+
// all hash to fnv1a32(''), producing false R1 collisions across
|
|
70
|
+
// distinct short-token queries. Codex round-5 P2 catch.
|
|
71
|
+
const tokens = filtered.length > 0 ? filtered : normalized;
|
|
72
|
+
const deduped = Array.from(new Set(tokens)).sort();
|
|
73
|
+
return fnv1a32(deduped.join(' '));
|
|
74
|
+
}
|
|
75
|
+
function fnv1a32(text) {
|
|
76
|
+
// FNV-1a 32-bit. Offset basis 2166136261, prime 16777619.
|
|
77
|
+
let hash = 2166136261;
|
|
78
|
+
for (let i = 0; i < text.length; i++) {
|
|
79
|
+
hash ^= text.charCodeAt(i);
|
|
80
|
+
// 32-bit multiply via Math.imul — handles overflow correctly.
|
|
81
|
+
hash = Math.imul(hash, 16777619);
|
|
82
|
+
}
|
|
83
|
+
// Coerce to unsigned 32-bit for stable comparison.
|
|
84
|
+
return hash >>> 0;
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Anchoring detection
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
/**
|
|
90
|
+
* Detect anchoring patterns in the recall history against the current
|
|
91
|
+
* recall's (queryHash, topMemoryId).
|
|
92
|
+
*
|
|
93
|
+
* Rule precedence: R2 (memory_dominance) wins on tie. When both R1 and R2
|
|
94
|
+
* fire on the same recall, return only the R2 hint — R2's signal is the
|
|
95
|
+
* cognitively stronger one (a memory dominating multiple DIFFERENT queries
|
|
96
|
+
* is a fixed-point anchor; R1 alone is just a literal re-ask).
|
|
97
|
+
*
|
|
98
|
+
* Cooldown: if the immediately-prior recall fired a hint on the SAME
|
|
99
|
+
* topMemoryId within `cooldown=3` history entries, suppress. Prevents
|
|
100
|
+
* spam when the agent repeatedly recalls within the dominance window.
|
|
101
|
+
* Cooldown is per-memory, not per-rule: if R2 fired on M (cooldown
|
|
102
|
+
* engaged for M), and the next recall has top=N + repeated query →
|
|
103
|
+
* R1 fires on N (different memory, not in cooldown).
|
|
104
|
+
*
|
|
105
|
+
* @returns AnchoringHint when a pattern fires; null otherwise.
|
|
106
|
+
*/
|
|
107
|
+
export function detectAnchoring(history, currentQueryHash, currentTopMemoryId, opts = {}) {
|
|
108
|
+
if (currentTopMemoryId === null)
|
|
109
|
+
return null;
|
|
110
|
+
const minDominance = opts.minDominance ?? DEFAULT_MIN_DOMINANCE;
|
|
111
|
+
const recentRepeatWindow = opts.recentRepeatWindow ?? DEFAULT_RECENT_REPEAT_WINDOW;
|
|
112
|
+
const cooldown = opts.cooldown ?? DEFAULT_COOLDOWN;
|
|
113
|
+
// Cooldown gate: was the most-recent hint (across the last `cooldown`
|
|
114
|
+
// entries) for THIS memory? If yes, suppress regardless of rule.
|
|
115
|
+
const cooldownSlice = history.slice(-cooldown);
|
|
116
|
+
for (const entry of cooldownSlice) {
|
|
117
|
+
if (entry.anchoredOn === currentTopMemoryId) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// R2 check FIRST (wins on tie). Count distinct queryHashes in history
|
|
122
|
+
// where topMemoryId === currentTopMemoryId (excluding null tops).
|
|
123
|
+
const matchingQueryHashes = new Set();
|
|
124
|
+
for (const entry of history) {
|
|
125
|
+
if (entry.topMemoryId === currentTopMemoryId) {
|
|
126
|
+
matchingQueryHashes.add(entry.queryHash);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Include current query in the count.
|
|
130
|
+
matchingQueryHashes.add(currentQueryHash);
|
|
131
|
+
const queryCount = matchingQueryHashes.size;
|
|
132
|
+
if (queryCount >= minDominance) {
|
|
133
|
+
return {
|
|
134
|
+
reason: 'memory_dominance',
|
|
135
|
+
memoryId: currentTopMemoryId,
|
|
136
|
+
queryCount,
|
|
137
|
+
summary: `Memory ${currentTopMemoryId} has been the top result for ${queryCount} distinct queries in this session and may be anchoring your reasoning.`,
|
|
138
|
+
source: 'j1-recurrence',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// R1 check: is currentQueryHash present in the last `recentRepeatWindow`
|
|
142
|
+
// entries AND was that entry's topMemoryId === currentTopMemoryId?
|
|
143
|
+
const r1Slice = history.slice(-recentRepeatWindow);
|
|
144
|
+
for (const entry of r1Slice) {
|
|
145
|
+
if (entry.queryHash === currentQueryHash && entry.topMemoryId === currentTopMemoryId) {
|
|
146
|
+
return {
|
|
147
|
+
reason: 'query_repeat',
|
|
148
|
+
memoryId: currentTopMemoryId,
|
|
149
|
+
summary: `Same query phrasing as a recent recall returned the same top result (${currentTopMemoryId}); you may be re-asking the same question.`,
|
|
150
|
+
source: 'j1-recurrence',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// RingBuffer + caller-side state helpers
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
const MAX_HISTORY = 10;
|
|
160
|
+
const DEFAULT_MAX_SESSIONS = 1000;
|
|
161
|
+
/**
|
|
162
|
+
* Bounded FIFO ring of RecallHistoryEntry. Newest entries pushed via
|
|
163
|
+
* append; oldest evicted when the ring is full. The class is intentionally
|
|
164
|
+
* a thin wrapper around an array so snapshotRing returns a readonly view
|
|
165
|
+
* without copying on the hot path.
|
|
166
|
+
*/
|
|
167
|
+
export class RingBuffer {
|
|
168
|
+
entries = [];
|
|
169
|
+
append(entry) {
|
|
170
|
+
this.entries.push(entry);
|
|
171
|
+
if (this.entries.length > MAX_HISTORY) {
|
|
172
|
+
this.entries.shift();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
snapshot() {
|
|
176
|
+
return this.entries;
|
|
177
|
+
}
|
|
178
|
+
size() {
|
|
179
|
+
return this.entries.length;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Build the (tenant, session) key for a per-session ring Map. Uses a NUL
|
|
184
|
+
* (`\x00`) byte as delimiter because tenant ids and session ids are
|
|
185
|
+
* validated elsewhere to reject NUL chars — guarantees collision-free
|
|
186
|
+
* concatenation regardless of what `:` or other delimiters might appear
|
|
187
|
+
* inside either field (notably API-key-derived subjects can contain `:`).
|
|
188
|
+
*/
|
|
189
|
+
export function buildSessionKey(tenantId, sessionId) {
|
|
190
|
+
return `${tenantId}\x00${sessionId}`;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get-or-create a RingBuffer for a session key. Caps total tracked keys
|
|
194
|
+
* at `maxSessions` (default 1000) with LRU eviction — when the cap is
|
|
195
|
+
* hit, deletes the oldest-inserted key before inserting the new one.
|
|
196
|
+
* Map iteration order preserves insertion order per ECMA-262 spec, so
|
|
197
|
+
* "oldest" = first key returned by Map.prototype.keys().
|
|
198
|
+
*/
|
|
199
|
+
export function getOrCreateRing(map, key, maxSessions = DEFAULT_MAX_SESSIONS) {
|
|
200
|
+
const existing = map.get(key);
|
|
201
|
+
if (existing) {
|
|
202
|
+
// LRU touch: delete + re-insert to move to back of iteration order.
|
|
203
|
+
map.delete(key);
|
|
204
|
+
map.set(key, existing);
|
|
205
|
+
return existing;
|
|
206
|
+
}
|
|
207
|
+
if (map.size >= maxSessions) {
|
|
208
|
+
const oldest = map.keys().next().value;
|
|
209
|
+
if (oldest !== undefined)
|
|
210
|
+
map.delete(oldest);
|
|
211
|
+
}
|
|
212
|
+
const ring = new RingBuffer();
|
|
213
|
+
map.set(key, ring);
|
|
214
|
+
return ring;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Append a recall to a ring. The `anchoredOn` argument carries the
|
|
218
|
+
* memoryId of the AnchoringHint that fired on THIS recall (or undefined
|
|
219
|
+
* if no hint). detectAnchoring reads it next time for cooldown gating.
|
|
220
|
+
*/
|
|
221
|
+
export function appendRecall(ring, queryHash, topMemoryId, anchoredOn) {
|
|
222
|
+
const entry = {
|
|
223
|
+
queryHash,
|
|
224
|
+
topMemoryId,
|
|
225
|
+
ts: new Date().toISOString(),
|
|
226
|
+
};
|
|
227
|
+
if (anchoredOn !== undefined)
|
|
228
|
+
entry.anchoredOn = anchoredOn;
|
|
229
|
+
ring.append(entry);
|
|
230
|
+
}
|
|
231
|
+
/** Snapshot a ring as a readonly RecallHistorySnapshot. */
|
|
232
|
+
export function snapshotRing(ring) {
|
|
233
|
+
return ring.snapshot();
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=recall-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recall-history.js","sourceRoot":"","sources":["../src/recall-history.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAuBA,kFAAkF;AAClF,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD;AA6HD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA0HD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMrE;AAqkDD;;;;;;;;;;GAUG;AACH,wBAAsB,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAyIlE"}
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,19 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { detectServer, writePidfile, removePidfileIfOwned } from './server-detect.js';
|
|
4
4
|
import { resolveTenantId } from './tenant.js';
|
|
5
5
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
6
|
+
import { buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, hashQueryText, } from './recall-history.js';
|
|
7
|
+
import { appendAuditEvent } from './audit.js';
|
|
8
|
+
// v0.33 / J1 — Module-level per-(tenant, session) recall-history ring map
|
|
9
|
+
// for the HTTP pipeline. Separate from CLI/MCP rings per plan v3 (per-
|
|
10
|
+
// pipeline rings; no IPC). HTTP is the only caller that threads its
|
|
11
|
+
// snapshot through opts.recallHistory to api.recall — api.recall's
|
|
12
|
+
// anchoringHint on the returned RecallResult IS the user-visible hint
|
|
13
|
+
// here (no separate compute needed).
|
|
14
|
+
const sessionRecallHistoryHttp = new Map();
|
|
15
|
+
/** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
|
|
16
|
+
export function __resetSessionRecallHistoryHttp() {
|
|
17
|
+
sessionRecallHistoryHttp.clear();
|
|
18
|
+
}
|
|
6
19
|
import { PACKAGE_VERSION } from './version.js';
|
|
7
20
|
import { validateApiKey } from './auth.js';
|
|
8
21
|
import { createRateLimiter } from './rate-limit.js';
|
|
@@ -59,6 +72,9 @@ const VALID_AUDIT_OPS = new Set([
|
|
|
59
72
|
'recall_autodebias_hint', // v0.32 / J3.2 — emitted by computePlanningFallacyHint on success
|
|
60
73
|
'recall_autodebias_hint_no_class_match', // v0.32 / J3.2 — telemetry: forward-claim, no class scored
|
|
61
74
|
'recall_autodebias_hint_tiebreak', // v0.32 / J3.2 — telemetry: forward-claim, >=2 classes tied
|
|
75
|
+
'recall_anchor_detected_query_repeat', // v0.33 / J1 — emitted by detector on R1 fire
|
|
76
|
+
'recall_anchor_detected_memory_dominance', // v0.33 / J1 — emitted by detector on R2 fire
|
|
77
|
+
'recall_anchor_skipped_no_session', // v0.33 / J1 — telemetry: no sessionId, ring skipped
|
|
62
78
|
]);
|
|
63
79
|
// Cap on GET /v1/audit?limit=. Matches docs/api.md (when written) and is large
|
|
64
80
|
// enough to dump a small deployment's full audit log without paginating, but
|
|
@@ -485,6 +501,55 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
485
501
|
? sessionIdRaw.trim()
|
|
486
502
|
: undefined;
|
|
487
503
|
const ctx = buildContextWithAuth(req, opts.hippoRoot);
|
|
504
|
+
// v0.33 / J1 — HTTP per-pipeline anchoring detector. HTTP threads its
|
|
505
|
+
// ring snapshot via opts.recallHistory so api.recall's own
|
|
506
|
+
// anchoringHint compute path activates. Unlike CLI (which computes
|
|
507
|
+
// its own hint separately because cmdRecall runs its own physics/
|
|
508
|
+
// hybrid pipeline outside api.recall), HTTP's /v1/memories response
|
|
509
|
+
// body IS api.recall's result directly. So the api.recall-computed
|
|
510
|
+
// hint flows through. HIPPO_ANCHORING=off short-circuits.
|
|
511
|
+
let httpRecallHistory;
|
|
512
|
+
let httpRingKey;
|
|
513
|
+
if (process.env.HIPPO_ANCHORING !== 'off') {
|
|
514
|
+
if (sessionId) {
|
|
515
|
+
// Codex round-5 P2 catch: do NOT mutate sessionRecallHistoryHttp
|
|
516
|
+
// before recall() preflight runs. A request with an invalid
|
|
517
|
+
// scorer_window / fresh_tail_count would create-or-touch the
|
|
518
|
+
// session ring (LRU-evicting valid sessions) even though recall
|
|
519
|
+
// throws 400. Snapshot the EXISTING ring if present; only
|
|
520
|
+
// create-or-touch after the recall returns successfully.
|
|
521
|
+
httpRingKey = buildSessionKey(ctx.tenantId, sessionId);
|
|
522
|
+
const existingRing = sessionRecallHistoryHttp.get(httpRingKey);
|
|
523
|
+
httpRecallHistory = existingRing ? snapshotRing(existingRing) : [];
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
// Telemetry: caller had no session_id so ring tracking skipped.
|
|
527
|
+
// Per the normal recall-audit convention (api.ts:854 stores
|
|
528
|
+
// SHA-256/16 hash of the query, NOT raw text), avoid retaining
|
|
529
|
+
// prompts in audit_log here too — query content can contain
|
|
530
|
+
// secrets, PII, or RTBF-restricted material. Codex round-2 P2
|
|
531
|
+
// catch: hashQueryText is a 32-bit FNV-1a designed for recall
|
|
532
|
+
// matching, NOT a privacy hash; brute-force trivial for low-
|
|
533
|
+
// entropy queries. Use the same SHA-256/16 truncation as the
|
|
534
|
+
// canonical recall audit.
|
|
535
|
+
const dbForAudit = openHippoDb(opts.hippoRoot);
|
|
536
|
+
try {
|
|
537
|
+
appendAuditEvent(dbForAudit, {
|
|
538
|
+
tenantId: ctx.tenantId,
|
|
539
|
+
actor: ctx.actor.subject,
|
|
540
|
+
op: 'recall_anchor_skipped_no_session',
|
|
541
|
+
targetId: undefined,
|
|
542
|
+
metadata: {
|
|
543
|
+
query_hash: createHash('sha256').update(q).digest('hex').slice(0, 16),
|
|
544
|
+
query_length: q.length,
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
closeHippoDb(dbForAudit);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
488
553
|
const result = recall(ctx, {
|
|
489
554
|
query: q,
|
|
490
555
|
limit,
|
|
@@ -496,7 +561,20 @@ async function handleRequest(req, res, opts, startedAt, limiter) {
|
|
|
496
561
|
...(summarizeOverflow !== undefined ? { summarizeOverflow } : {}),
|
|
497
562
|
...(scorerWindow !== undefined ? { scorerWindow } : {}),
|
|
498
563
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
564
|
+
...(httpRecallHistory !== undefined ? { recallHistory: httpRecallHistory } : {}),
|
|
499
565
|
});
|
|
566
|
+
// v0.33 / J1 — append AFTER recall completes (snapshot was taken before
|
|
567
|
+
// recall() ran). anchoredOn carries the memoryId of any hint that fired
|
|
568
|
+
// (api.recall computed it from the same snapshot we passed in), feeding
|
|
569
|
+
// the cooldown logic for the NEXT recall on this session.
|
|
570
|
+
// Codex round-5 P2 fix: create-or-touch the ring ONLY HERE, after recall
|
|
571
|
+
// returns successfully. Invalid requests that throw 400 in recall()
|
|
572
|
+
// never reach this point, so they cannot LRU-evict valid sessions.
|
|
573
|
+
if (httpRingKey) {
|
|
574
|
+
const httpRing = getOrCreateRing(sessionRecallHistoryHttp, httpRingKey);
|
|
575
|
+
const topId = result.results[0]?.id ?? null;
|
|
576
|
+
appendRecall(httpRing, hashQueryText(q), topId, result.anchoringHint?.memoryId);
|
|
577
|
+
}
|
|
500
578
|
// Continuity payloads should never be cached. The caller is asking for
|
|
501
579
|
// session-state-aware data; intermediaries must not reuse it across users.
|
|
502
580
|
if (includeContinuity) {
|