import type { SessionEntry } from './types.js'; /** * Append-only conversation stream — the durable, offset-addressable * projection of a session's active path (v2 migration, phase A4). * * The SessionEntry DAG remains the single source of truth for model context; * this stream exists so clients can read a conversation with offsets * (catch-up + live tail), across processes, without count-based replay. The * projection appends one record per active-path entry, and — because the DAG * can branch (fork/replay/checkpoint-restore) while a stream cannot — an * explicit `truncated` record whenever the active path rewinds, paired with * a producer-epoch bump so stale writers are fenced out. */ export type ConversationStreamRecord = { kind: 'entry'; entry: SessionEntry; } | { kind: 'truncated'; /** * Last entry id shared with the previously projected path (empty when * the paths diverge from the very first entry). Readers drop every * projected record after this id and continue from the next records. */ rewoundTo: string; /** The new active leaf after the rewind. */ newLeafId: string; /** Why the path branched: replay, checkpoint restore, manual fork… */ reason: string; }; export interface ConversationStreamIdentity { /** Store session id the stream projects (e.g. `agent:name:id:session`). */ storeSessionId: string; } export interface ConversationProducerClaim { producerId: string; producerEpoch: number; /** Stable generation identity; changes when a deleted stream is recreated. */ incarnation: string; nextProducerSequence: number; offset: string; } export interface ConversationStreamBatch { offset: string; records: ConversationStreamRecord[]; } export interface ConversationStreamReadResult { batches: ConversationStreamBatch[]; /** Pass back as `offset` on the next read to continue. */ nextOffset: string; /** True when the read reached the stream tail. */ upToDate: boolean; } export interface ConversationStreamMeta { /** Portable persisted layout version. Unknown future versions fail closed. */ formatVersion: typeof CONVERSATION_STREAM_FORMAT_VERSION; identity: ConversationStreamIdentity; incarnation: string; nextOffset: string; producerId: string | null; producerEpoch: number; nextProducerSequence: number; } export declare const CONVERSATION_STREAM_FORMAT_VERSION: 1; /** Disposable durable cache of a folded conversation at one committed batch. */ export interface ConversationFoldCheckpoint { offset: string; incarnation: string; formatVersion: number; data: string; } export interface ConversationStreamAppendInput { path: string; producerId: string; producerEpoch: number; incarnation: string; producerSequence: number; /** Correlates the batch to a submission attempt when one owns the turn. */ submission?: { submissionId: string; attemptId: string; }; records: readonly ConversationStreamRecord[]; } /** * Durable append-only conversation stream storage. * * **Batch atomicity is a hard contract requirement**: every record in an * `append` must be persisted together under one offset, all-or-nothing. * First-party adapters satisfy this by serializing the batch into a single * row/document write; an adapter that splits records across non-atomic * writes violates the contract. * * **Producer fencing**: `acquireProducer` bumps the producer epoch; appends * carrying a stale epoch are rejected. The (path, producerId, epoch, * sequence) uniqueness makes redelivered appends idempotent — a retried * append with the same coordinates returns the already-committed offset. */ export interface ConversationStreamStore { /** Idempotently ensure the stream exists. */ createStream(path: string, identity: ConversationStreamIdentity): Promise; /** Claim single-writer ownership, fencing out previous producers. */ acquireProducer(path: string, producerId: string): Promise; /** Append one atomic batch; returns its offset. */ append(input: ConversationStreamAppendInput): Promise<{ offset: string; }>; /** Read batches from `offset` (default: the start), up to `limit` batches. */ read(path: string, options?: { offset?: string; limit?: number; }): Promise; getMeta(path: string): Promise; putFoldCheckpoint?(path: string, checkpoint: ConversationFoldCheckpoint): Promise; getFoldCheckpoint?(path: string, options?: { atOrBefore?: string; }): Promise; delete(path: string): Promise; /** Process-local change notification; returns an unsubscribe function. */ subscribe(path: string, listener: () => void): () => void; } export declare class ConversationStreamStoreError extends Error { readonly code: 'STREAM_NOT_FOUND' | 'PRODUCER_FENCED' | 'INCARNATION_MISMATCH' | 'SEQUENCE_CONFLICT'; constructor(code: ConversationStreamStoreError['code'], message: string); } export declare const CONVERSATION_STREAM_DEFAULT_READ_LIMIT = 100; export declare const CONVERSATION_STREAM_MAX_READ_LIMIT = 1000; export declare function clampReadLimit(limit: number | undefined): number; export declare function formatStreamOffset(offset: number): string; export declare function parseStreamOffset(offset: string | undefined): number; /** * Read a conversation from its newest validated fold checkpoint plus the log * suffix. Non-origin offsets always use the raw store so resume semantics stay * unchanged. Invalid checkpoints are disposable and fall back to full replay. */ export declare function readConversationFromFold(store: ConversationStreamStore, path: string, options?: { offset?: string; limit?: number; }): Promise; /** Stream path for a session's conversation projection. */ export declare function conversationStreamPath(storeSessionId: string): string; /** * Process-local listener registry shared by store implementations — * registration, unsubscribe-and-prune, and error-swallowing notify. */ export declare class StreamListenerRegistry { private readonly listeners; subscribe(path: string, listener: () => void): () => void; notify(path: string): void; } /** In-memory conversation stream store (dev / `runtime: 'stateless'` / tests). */ export declare class InMemoryConversationStreamStore implements ConversationStreamStore { private readonly streams; private readonly registry; createStream(path: string, identity: ConversationStreamIdentity): Promise; acquireProducer(path: string, producerId: string): Promise; append(input: ConversationStreamAppendInput): Promise<{ offset: string; }>; read(path: string, options?: { offset?: string; limit?: number; }): Promise; getMeta(path: string): Promise; delete(path: string): Promise; putFoldCheckpoint(path: string, checkpoint: ConversationFoldCheckpoint): Promise; getFoldCheckpoint(path: string, options?: { atOrBefore?: string; }): Promise; subscribe(path: string, listener: () => void): () => void; /** Serializable state used by durable adapters that wrap the reference semantics. */ exportPersistenceSnapshot(): unknown; importPersistenceSnapshot(snapshot: unknown): void; private requireStream; } //# sourceMappingURL=conversation-stream.d.ts.map