/** * J1 anchoring detector (recall-recurrence) — pure module. * * Implements two detection rules from ROADMAP-RESEARCH.md L546: * R1 query_repeat: same queryHash within recentRepeatWindow returned * same topMemoryId (caller is re-asking the same question). * R2 memory_dominance: same topMemoryId across >= minDominance distinct * queryHashes (memory acts as a fixed-point anchor regardless of what * the agent asks). * * Per the plan v3 architectural decision: each pipeline (api.recall via * HTTP, cmdRecall, MCP hippo_recall) owns its OWN ring buffer Map keyed * by (tenant, session). No cross-pipeline sharing (the typical multi- * process deployment makes IPC ring-sharing impractical; per-pipeline * is correct because each pipeline has its own top-1 ranking anyway). * * Plan: docs/plans/2026-05-26-j1-anchoring-detector.md. * Composes with J3.2: AnchoringHint + PlanningFallacyHint are independent * signals; both can fire on the same recall. */ export type AnchoringReason = 'query_repeat' | 'memory_dominance'; export interface AnchoringHint { reason: AnchoringReason; /** The memory ID that is anchoring the agent's reasoning. */ memoryId: string; /** For 'memory_dominance': how many distinct queries in the recent window * had this memory as their top-1 result. Always >= 3 when emitted. */ queryCount?: number; /** Human-readable summary surfaced to the agent. */ summary: string; /** Discriminator for hint origin; reserved for future variants. */ source: 'j1-recurrence'; } export interface RecallHistoryEntry { /** Hash of the queryText that produced this entry (see hashQueryText). */ queryHash: number; /** Top-1 memory id this recall surfaced; null if zero results. */ topMemoryId: string | null; /** ISO-8601 timestamp; advisory, not used by R1/R2 logic. */ ts: string; /** Memory id of the AnchoringHint that fired on this recall, if any. * Used by R1/R2 cooldown logic to prevent re-emitting the same hint on * consecutive recalls within the dominance window. Caller-written * AFTER detectAnchoring returns; reads next time detectAnchoring runs. */ anchoredOn?: string; } export type RecallHistorySnapshot = readonly RecallHistoryEntry[]; export interface DetectAnchoringOpts { /** R2 threshold: number of distinct queryHashes that must have returned * the same topMemoryId. Default 3. */ minDominance?: number; /** R1 window: how many recent history entries to scan for query repeat. * Default 5. */ recentRepeatWindow?: number; /** Cooldown: if the immediately-prior fire (per `anchoredOn`) was for * the same topMemoryId within this many history entries, suppress. * Default 3. */ cooldown?: number; } /** * Normalize + hash a query text into a 32-bit integer. * Lowercase → strip non-alphanumeric → split → drop empty + short tokens → * sort tokens → join → FNV-1a 32-bit. * * Token sort + dedup means semantically-equivalent queries with reordered * words collide intentionally (the roadmap's "semantically-distinct" v1 * uses textual normalization; embedding-based distinctness is J1-v2). * * Deterministic across processes; stable across Node + V8 versions. */ export declare function hashQueryText(query: string): number; /** * Detect anchoring patterns in the recall history against the current * recall's (queryHash, topMemoryId). * * Rule precedence: R2 (memory_dominance) wins on tie. When both R1 and R2 * fire on the same recall, return only the R2 hint — R2's signal is the * cognitively stronger one (a memory dominating multiple DIFFERENT queries * is a fixed-point anchor; R1 alone is just a literal re-ask). * * Cooldown: if the immediately-prior recall fired a hint on the SAME * topMemoryId within `cooldown=3` history entries, suppress. Prevents * spam when the agent repeatedly recalls within the dominance window. * Cooldown is per-memory, not per-rule: if R2 fired on M (cooldown * engaged for M), and the next recall has top=N + repeated query → * R1 fires on N (different memory, not in cooldown). * * @returns AnchoringHint when a pattern fires; null otherwise. */ export declare function detectAnchoring(history: RecallHistorySnapshot, currentQueryHash: number, currentTopMemoryId: string | null, opts?: DetectAnchoringOpts): AnchoringHint | null; /** * Bounded FIFO ring of RecallHistoryEntry. Newest entries pushed via * append; oldest evicted when the ring is full. The class is intentionally * a thin wrapper around an array so snapshotRing returns a readonly view * without copying on the hot path. */ export declare class RingBuffer { private entries; append(entry: RecallHistoryEntry): void; snapshot(): RecallHistorySnapshot; size(): number; } /** * Build the (tenant, session) key for a per-session ring Map. Uses a NUL * (`\x00`) byte as delimiter because tenant ids and session ids are * validated elsewhere to reject NUL chars — guarantees collision-free * concatenation regardless of what `:` or other delimiters might appear * inside either field (notably API-key-derived subjects can contain `:`). */ export declare function buildSessionKey(tenantId: string, sessionId: string): string; /** * Get-or-create a RingBuffer for a session key. Caps total tracked keys * at `maxSessions` (default 1000) with LRU eviction — when the cap is * hit, deletes the oldest-inserted key before inserting the new one. * Map iteration order preserves insertion order per ECMA-262 spec, so * "oldest" = first key returned by Map.prototype.keys(). */ export declare function getOrCreateRing(map: Map, key: string, maxSessions?: number): RingBuffer; /** * Append a recall to a ring. The `anchoredOn` argument carries the * memoryId of the AnchoringHint that fired on THIS recall (or undefined * if no hint). detectAnchoring reads it next time for cooldown gating. */ export declare function appendRecall(ring: RingBuffer, queryHash: number, topMemoryId: string | null, anchoredOn?: string): void; /** Snapshot a ring as a readonly RecallHistorySnapshot. */ export declare function snapshotRing(ring: RingBuffer): RecallHistorySnapshot; //# sourceMappingURL=recall-history.d.ts.map