import { MemoryGraph } from "./graph.js"; import { EmbeddingManager } from "./embeddings.js"; import type { ExtractionResult, NewObservation } from "./extraction.js"; import type { ConversationSummary, MemoryStore as MemoryStoreType } from "./types.js"; export type ExtractionOutcome = { newObservations: NewObservation[]; expiredObservationIds: string[]; }; /** * One memoryId's worth of in-memory state — graph + embeddings index + * conversation summary + the obs→entity reverse index — plus the * mutation methods that keep all four in sync. * * Why a class (not a record): the graph, the embedding index, and the * obsToEntity map all have to move in lockstep. A new observation * needs an entry in the reverse index AND eventually an embedding * vector; an expired observation needs its embedding dropped and its * graph row marked expired. Encapsulating those updates in this class * means `MemoryManager` can't accidentally update one and forget * another. The reverse index is private; outsiders use * `lookupEntityIdByObs` instead of touching the map. * * Lifecycle: one instance per memoryId per `MemoryManager`. Constructed * from already-loaded graph + embeddings + summary by * `MemoryManager.getEntry`. Never shared across `MemoryManager` * instances. Never evicted while the manager lives. * * The class deliberately does NOT do LLM/embed calls. Embedding * vectors are computed by the manager (which owns the LLM client) and * handed back via `setEmbedding`. That keeps this class pure (no I/O * except `persist`) and unit-testable without mocks. */ export declare class MemoryCacheEntry { /** * Captured once at construction; every persist uses this id rather * than the manager's "current" id, so a concurrent `setMemoryId()` * during an in-flight write won't route the data to the wrong * memory namespace. */ readonly memoryId: string; /** * Auto-extraction cadence counter, incremented per agent turn, * reset when extraction runs. Public field because the manager * orchestrates the cadence — there's no invariant for the entry * to enforce. */ turnsSinceExtraction: number; /** * How many thread messages auto-extraction has already consumed. * `MemoryManager.onTurn` slices the thread from this index, so each * extraction pass reads only what arrived since the previous one and * the extraction prompt stays bounded no matter how long the thread * grows. `compactIfNeeded` remaps it when compaction rewrites the * thread. In-memory only, like `turnsSinceExtraction` — a fresh * manager starts at 0 and simply re-reads the thread once. */ extractedUpTo: number; private readonly graph; private readonly embeddings; private summary; /** * obs id → owning entity id. Maintained on every mutation that * adds or expires observations so Tier-2 embedding recall can map * a similarity hit back to its entity in O(1) rather than scanning * every entity × observation per recall. * * Null-prototype object: user-controlled observation ids cannot * collide with `Object.prototype` methods or pollute the prototype. */ private readonly obsToEntity; constructor(memoryId: string, graph: MemoryGraph, embeddings: EmbeddingManager, summary: ConversationSummary | null); getGraph(): MemoryGraph; getEmbeddings(): EmbeddingManager; getSummary(): ConversationSummary | null; setSummary(summary: ConversationSummary | null): void; /** O(1) reverse lookup. Returns undefined for unknown ids. */ lookupEntityIdByObs(obsId: string): string | undefined; /** * Apply an extraction result to the graph and maintain the * embedding + obsToEntity invariants in lockstep: * - expired observations have their embedding entries dropped * - newly-added observations get registered in the reverse index * * The caller still has to compute embedding vectors for the new * observations — that requires an embed-API call which is the * manager's responsibility, not the entry's. The returned * `newObservations` is exactly the set the manager needs to feed * to `setEmbedding` after embedding each one. */ applyExtraction(result: ExtractionResult, source: string): ExtractionOutcome; /** * Stash a freshly-computed embedding vector for an observation. * Idempotent — the underlying `EmbeddingManager.addEntry` replaces * any previous entry for the same obs id. */ setEmbedding(obsId: string, vector: number[]): void; /** * Expire a single observation and drop its embedding entry. The * graph row stays (with `validTo` timestamped) so the audit trail * is preserved; only the embedding is purged because a similarity * hit on an expired observation would be misleading at recall time. */ expireObservation(obsId: string): void; /** * Expire a single relation. Relations have no embeddings, so this * is a thin pass-through; living on the entry makes the * "graph mutations always go through the entry" rule consistent. */ expireRelation(relId: string): void; /** * Save graph + embeddings + summary to the store under `memoryId`. * Always uses the captured id, never any "current" id from the * manager — same reasoning as the readonly `memoryId` field above. */ persist(store: MemoryStoreType): Promise; /** * Register the obs→entity reverse-index pair for each newly-added * observation. O(N) over the new set — the entity id is captured * at add-time in `applyExtractionResult`, so no graph walk is * needed. */ private indexNewObservations; }