/** * writeSnapshot — write-side stage for Causal memory. * * Captures the current run's `(query, finalContent)` pair from * `scope.newMessages` (populated by the Agent's PrepareFinal stage), * embeds the query for retrieval, and persists a `SnapshotEntry` to * the store. Future turns can match new questions against past * queries via cosine similarity to replay decision evidence. * * Reads from scope: `identity`, `turnNumber`, `newMessages` * Writes to store: one `SnapshotEntry` per call, id = `snap-{turn}` * * Why per-turn (not per-iteration)? * Causal memory captures TURN outcomes — "user asked X, agent said Y." * Mid-iteration state isn't useful for cross-run replay. * * Turn derivation — collisions are impossible by construction: * The effective turn is `max(scope.turnNumber, maxStoredTurn + 1)` where * `maxStoredTurn` is the highest `snap-{n}` already live in THIS * conversation's namespace (`identityNamespace(identity)` — the durable * conversation anchor across `run()` calls, Agent instances, and * processes). Rationale: * - Hosts that track `turnNumber` correctly keep their numbering * (`turnNumber: 5` → `snap-5`, gaps preserved). * - Hosts with a stale counter still get a fresh, ordered id — turn 2 * of the same conversation lands `snap-2` instead of silently * replacing `snap-1`. * Causal snapshots are decision evidence (audit/replay data): when * "stale counter" and "deliberate same-turn rewrite" are * indistinguishable, never destroying a prior turn's evidence wins. * TTL-expired snapshots are ignored by the scan (same as every read). * * Since 9.6.0 this is SELF-DEFENSE, not the primary mechanism. The Agent * resolves the turn once per run against the same stores (`seed` → * `resolveTurnNumber`), so under an Agent `scope.turnNumber` already * satisfies this rule and the scan changes nothing — it re-answers what * it is given. It stays for hand-composed pipelines mounted through * `mountMemoryWrite` in a host that numbers its own turns, which is * exactly the case the Agent's resolution cannot reach. * * The scan is deliberately narrowed to `snap-{n}` ids rather than to * every turn-stamped entry: `writeMessages` may already have written * turn N into a SHARED store by the time this stage runs, and a scan that * counted those would answer N+1 and file this turn's evidence under a * turn its own messages do not have. * * Empty-newMessages handling: * When `newMessages` is empty (no final answer produced — e.g. * pause-resume mid-flight), the stage no-ops. Re-runs after resume * capture the snapshot then. * * @see ./types.ts for the SnapshotEntry shape this writes * @see ./loadSnapshot.ts for the read-side counterpart */ import type { TypedScope } from 'footprintjs'; import type { MemoryStore } from '../store/index.js'; import type { Embedder } from '../embedding/index.js'; import type { MemoryState } from '../stages/index.js'; export interface WriteSnapshotConfig { /** The store to persist the snapshot to. */ readonly store: MemoryStore; /** * Embedder used to vectorize the query for later cosine-search. * Required — Causal memory's value comes from semantic retrieval * across past runs. */ readonly embedder: Embedder; /** * Stable id for the embedder. Stored on the entry so a later * embedder swap doesn't cross-pollute similarity scores. * Default: `'unknown-embedder'` — pass an explicit id when you * may swap embedder instances over time. */ readonly embedderId?: string; /** * TTL in milliseconds — drop snapshots after this duration. Useful * for compliance ("delete causal trace after 30 days"). */ readonly ttlMs?: number; /** * Tier to tag the snapshot with — typical: `'hot'` for current, * `'warm'`/`'cold'` for archived. Read stages can filter by tier. */ readonly tier?: 'hot' | 'warm' | 'cold'; } export declare function writeSnapshot(config: WriteSnapshotConfig): (scope: TypedScope) => Promise;