/** * resolveTurnNumber — which turn of this conversation is about to happen? * * A memory entry's id is turn-stamped (`msg-{turn}-{index}`, `snap-{turn}`, * `beat-{turn}-{index}`), so the turn number is not a label: it is the KEY * two consecutive turns must not share. Get it wrong and every turn * overwrites the previous one — a conversation memory that silently holds * exactly one exchange no matter how large the window says it is. That is * the bug this function exists to make impossible (9.6.0). * * **Why the host's own counter is not enough.** A host that runs one Agent * instance for a whole conversation can count turns in memory. The shape * seen in a production field deployment cannot: it builds a FRESH agent per * turn (new process, new instance, same `conversationId`, same store), so * anything counted in a variable is back to 1 on every request. The only * thing that remembers how far the conversation has got is the STORE. * * **The rule** — `max(hostTurn, maxStoredTurn + 1)`: * - Hosts that track the turn honestly keep their numbering (`hostTurn: 5` * → turn 5; gaps preserved — an ordinal, not a count). * - A host whose counter is stale still gets a fresh, ordered turn: the * second turn of a conversation whose store already holds turn 1 is * turn 2, never turn 1 again. * - The two sources compose: whichever knows more wins, and neither can * drag the conversation backwards. * * The rule is the one `writeSnapshot` has applied to causal snapshots since * 9.1 — this is that carve-out generalised so every memory kind shares one * definition of "which turn is this", resolved ONCE per run (in the Agent's * seed stage) rather than re-derived per write stage. Re-deriving per stage * is not equivalent and is why this is a shared helper: `writeMessages` * stamps turn N, and a scan taken after it would read N and answer N+1 — so * beats written later in the same turn would carry a turn number no message * of theirs has, and their `msg-{turn}-{index}` references would point at * entries that do not exist. * * Cost: one paged `list()` per store per run, on the store the memory was * already going to talk to. Bounded by {@link MAX_SCAN_PAGES}. * * @see ../stages/writeMessages.ts the id format this protects * @see ../causal/writeSnapshot.ts the same rule, kept as self-defense for * hand-composed (non-Agent) pipelines */ import type { MemoryEntry } from '../entry/index.js'; import type { MemoryIdentity } from '../identity/index.js'; import type { MemoryStore } from '../store/index.js'; export interface MaxStoredTurnOptions { /** * Which entries count. Default: every entry that carries * `source.turn`. `writeSnapshot` narrows this to its own `snap-{n}` ids * so that its self-defense stays stable no matter what else shares the * namespace. */ readonly turnFor?: (entry: MemoryEntry) => number; } /** * The highest turn already stored in this identity's namespace — 0 when the * conversation has never been written to. * * Only LIVE entries are seen: `store.list` drops TTL-expired ones, the same * as every read path, so an expired turn 4 does not hold the numbering open. */ export declare function maxStoredTurn(store: MemoryStore, identity: MemoryIdentity, options?: MaxStoredTurnOptions): Promise; export interface ResolveTurnNumberOptions { /** * The durable stores this conversation is written to. Every one is * consulted and the highest turn wins, so two memories sharing a * conversation agree on which turn it is even when they keep their * entries in different backends. * * Duplicates (the usual case — one store, several memories) are scanned * once. */ readonly stores: readonly MemoryStore[]; /** The conversation namespace to scan. */ readonly identity: MemoryIdentity; /** * What the host believes the turn is — a FLOOR, never the last word. The * Agent derives it from the conversation it was handed (how many user * turns are already in the history), which is right for a followed-up * conversation and stale for a fresh agent per turn. Default 1. */ readonly hostTurn?: number; } /** * Resolve the turn number this run should stamp on everything it writes. * * Returns at least 1, always. A store that throws is NOT swallowed: a memory * whose store is unreachable is going to fail on the next read anyway, and a * turn number quietly guessed here is how entries overwrite each other. */ export declare function resolveTurnNumber(options: ResolveTurnNumberOptions): Promise; /** A host turn below 1 (or not a number at all) means "I do not know" → 1. */ export declare function normalizeHostTurn(hostTurn: number | undefined): number;