import type { BrandProfile, SessionKey, SessionStore, SessionStoreEntry, SessionSummaryEntry } from "@yanlinglabs/winter-agent-sdk"; import { RuntimeSdkError } from "../errors.js"; import type { RuntimeSdkPeers } from "../sdk.js"; /** * The concrete store's surface, as the router uses it. * * STRUCTURAL, not `InstanceType`, for the same * reason `seams/official-sdk-shapes.ts` types the official module structurally: the peer is INJECTED, * so the router must describe what it needs rather than name a class it never imports. The four * Winter-only members below are on the concrete class and deliberately NOT on the pinned * `SessionStore` type (WS-03 §10 pins that at exactly six members) — the store's own comments say so. */ export interface CanonicalSessionStore extends SessionStore { append(key: SessionKey, entries: SessionStoreEntry[]): Promise; load(key: SessionKey): Promise; listSessions(projectKey: string): Promise>; listSessionSummaries(projectKey: string): Promise; delete(key: SessionKey): Promise; listSubkeys(key: { projectKey: string; sessionId: string; }): Promise; listProjectKeys(): Promise; readSessionSummary(key: { projectKey: string; sessionId: string; }): Promise; acquireSessionLease(key: { projectKey: string; sessionId: string; }): Promise; /** * WS-18 W18-5 (P10b, SDK 0.0.10's S8): releases the writer lease `acquireSessionLease`/`append`'s * own opening sequence took — same-pid only, idempotent, never another pid's lease (`true` iff THIS * pid held it and released it). The router calls this for a WINTER destination, after its own * write-ahead producer record lands and BEFORE `confirmInit`, so the daemon's pid writes NOTHING to * the canonical transcript once the winter child holds the lease. */ releaseSessionLease(key: { projectKey: string; sessionId: string; }): Promise; } /** The constructor the injected peer exports. */ export type CanonicalSessionStoreConstructor = new (opts: { winterHome: string; }) => CanonicalSessionStore; /** WS-14 §5.1: an options combination the shared store cannot be used with. */ export declare class SharedStoreOptionsError extends RuntimeSdkError { readonly option: "persistSession" | "enableFileCheckpointing" | "sessionStore"; constructor(args: { option: "persistSession" | "enableFileCheckpointing" | "sessionStore"; reason: string; }); } /** * WS-14 §5: "Blind `importSessionToStore()` after partial mirror failure is forbidden." * * A THROW AND NOT A WARNING. The import transports a whole local transcript into the canonical store; * run after a PARTIAL mirror it re-imports entries that already landed and re-orders around the ones * that did not, which is the one failure the canonical file cannot recover from by itself. The safe * door is `reconcile.ts`'s suffix-only reconciliation, and this class names it. */ export declare class BlindStoreImportError extends RuntimeSdkError { readonly key: Readonly; constructor(key: SessionKey, detail: string); } /** The injected Winter peer does not carry the concrete store (a peer built for a different purpose). */ export declare class SharedStoreUnavailableError extends RuntimeSdkError { constructor(); } export interface MirrorPolicy { /** §5's "~100 ms batches". Appends inside one window become ONE canonical append. */ batchWindowMs: number; /** §5's "≤3 append attempts". */ maxAttempts: number; /** §5's "short backoff" between attempts. */ backoffMs: number; /** * How long ONE attempt may take before it is abandoned. * * §5: "a timed-out append is **not** retried". The reason is not politeness — a timed-out append may * still land, so a retry is the one action that can duplicate entries in an append-only file. */ attemptTimeoutMs: number; } export declare const DEFAULT_MIRROR_POLICY: MirrorPolicy; export type TranscriptHealth = "ok" | "repair-required"; /** How many recent uuids per key the append idempotence guard remembers (WS-21 §3.8). */ export declare const RECENT_UUIDS_PER_KEY = 4096; /** What a failed mirror records. COUNTS AND A CAUSE — never entry content (WS-05 §13). */ export interface MirrorErrorRecord { projectKey: string; sessionId: string; subpath?: string; /** How many entries were in the batch that failed. Never what they were. */ entryCount: number; attempts: number; cause: "append-failed" | "timed-out"; /** The failure's own message, from the store's typed error — never an entry. */ detail: string; at: string; } export interface SessionMirrorHealth { transcriptHealth: TranscriptHealth; errors: readonly MirrorErrorRecord[]; /** Canonical appends this session has completed — the "~100 ms batches" observation. */ batchesCommitted: number; /** `append()` calls received from a branch. Two inside one window commit as one batch. */ appendsReceived: number; } export interface SettleReport { /** True when every pending batch reached a terminal state (committed or recorded as a mirror error). */ settled: boolean; batchesCommitted: number; errors: readonly MirrorErrorRecord[]; transcriptHealth: TranscriptHealth; } /** The identity that makes "one instance, one version" checkable rather than asserted. */ export interface SharedStoreIdentity { packageName: string; /** The injected peer's own version identity — the version BOTH branches are therefore using. */ packageVersion: string; /** Unique per `createSharedSessionStore` call, so "the same store" is provable by value. */ instanceId: string; /** The daemon's home, as the host named it. The handoff leases live under it. */ winterHome: string; /** * WS-21: where the canonical store actually lives — `/sdk` (the shared runtime home) * for a router created with `requireRunHome`, `winterHome` itself on the pre-WS-21 layout. Every * canonical-path consumer (transcripts, the provider-state sidecar, the default memory dir) reads * THIS, never `winterHome`. */ storeHome: string; } /** Options members WS-14 §5.1 rules on. Structural, so both branches' option objects fit. */ export interface StoreBearingOptions { sessionStore?: unknown; persistSession?: boolean; enableFileCheckpointing?: boolean; } export interface SharedSessionStore { /** THE object handed to both branches as `Options.sessionStore`. */ readonly store: SessionStore; /** The underlying concrete store, for the barrier's lease/summary doors. Never handed to a branch. */ readonly canonical: CanonicalSessionStore; readonly identity: SharedStoreIdentity; readonly policy: MirrorPolicy; /** WS-05 §12 step 3's host-side pending barrier. No key = every session. */ settle(key?: SessionKey): Promise; health(key: SessionKey): SessionMirrorHealth; /** * Clears a session's `repair-required` flag. THE ONLY CALLER IS A COMPLETED RECONCILIATION * (`reconcile.ts`) — the flag exists to block a handoff until the canonical store is reconciled, so * anything else clearing it would be clearing the evidence rather than the cause. */ markReconciled(key: SessionKey, detail: string): void; /** Attaches the shared store to an options object, refusing §5.1's two combinations. */ attach(options: T): T & { sessionStore: SessionStore; }; /** WS-14 §5's blind-import ban, as a guard a host calls before `importSessionToStore()`. */ assertImportAllowed(key: SessionKey): void; /** * WS-18 W18-5 (P10b): delegates to the SDK's `releaseSessionLease` — same-pid, idempotent, never * another pid's lease. `true` iff this pid held the lease and released it. */ releaseLease(session: SessionKey): Promise; /** WS-13 §8.2's no-wash-back mechanism. Shared with the decorator and the reconciler. */ readonly decorations: DecorationRegistry; } /** * The uuids of entries that exist ONLY in a materialized resume copy. * * WHY THIS EXISTS AT ALL. WS-13 §8.2's PREFERRED door bakes decorations into the copy the destination * runtime reads, "the canonical file stays byte-pure". But the destination then WRITES its next turn * with `parentUuid` pointing at whatever it read last — including a decoration — and mirrors that turn * back through this store. Without a registry, the byte-pure canonical file would acquire either the * decoration itself (via reconciliation's suffix append) or an entry whose parent is unreachable in it * (WS-05 §12 step 5's own validation would then fail the NEXT handoff). Both are the wash-back the * probe is about; this is the mechanism that makes the probe pass rather than a hope that it does. * * IN-MEMORY BY DESIGN. §8.2: "decorations are recomputed fresh at every leg spawn (never stale)". A * copy outlives neither the generation that staged it nor the process that decorated it, so a durable * ledger would only ever hold entries about copies that no longer exist. */ export interface DecorationRegistry { /** Records an entry that lives only in the copy, with the parent the canonical chain should keep. */ record(key: SessionKey, decoration: { uuid: string; parentUuid: string | null; }): void; has(key: SessionKey, uuid: string): boolean; list(key: SessionKey): readonly string[]; /** The parent a decoration stands in front of, resolving a chain of them. */ canonicalParentOf(key: SessionKey, uuid: string): string | null; forget(key: SessionKey): void; } export declare function createDecorationRegistry(): DecorationRegistry; export interface StripDecorationsResult { entries: SessionStoreEntry[]; dropped: number; reparented: number; } /** * Removes copy-only entries from a batch on its way into the canonical store, and re-links the chain. * * RE-PARENTING IS NOT OPTIONAL. Dropping a decoration whose child points at it would leave the child * with an unreachable `parentUuid` — exactly what WS-05 §12 step 5 validates and refuses. The child's * `parentUuid` becomes the decoration's own parent, which is the link the canonical file would have had * if the decoration had never been staged. Nothing else about the entry is touched, and an entry with * no decorated parent is returned by IDENTITY (never a copy), so a batch with no decorations in it is * byte-identical to the one the branch handed us. */ export declare function stripDecorations(key: SessionKey, entries: readonly SessionStoreEntry[], registry: DecorationRegistry): StripDecorationsResult; /** * WS-14 §5.1's two refusals, over any options object carrying a store. * * BOTH BRANCHES, ONE RULE. The Winter SDK's own `query()` refuses the same two combinations at * construction (`packages/sdk/src/query.ts`), and Lane A's `assertOptionsInvariants` refuses them on * the official template. This is the third site on purpose: it is the one that runs when the ROUTER * attaches the store, i.e. before either branch exists, and it throws a TYPED class rather than the * bare `Error` the SDK's constructor-time check throws. */ export declare function assertStoreCompatibleOptions(options: StoreBearingOptions): void; /** * Asserts that every options object listed carries the SAME store object (WS-05 §6). * * BY IDENTITY, because that is the only check that means anything: two stores over one home are * type-identical, version-identical, and still two writers. */ export declare function assertOneSharedStore(shared: SharedSessionStore, ...options: ReadonlyArray<{ sessionStore?: unknown; }>): void; export interface SharedSessionStoreInput { peers: RuntimeSdkPeers; winterHome: string; /** WS-21: the directory the canonical store is rooted at. Absent = `winterHome` (the pre-WS-21 layout). */ storeHome?: string; policy?: Partial; /** Injectable clock for the records' timestamps. */ now?: () => Date; /** WS-13 §8.2's decoration registry. A fresh one per store unless a host shares one deliberately. */ decorations?: DecorationRegistry; } /** * Builds the one store both branches share. * * THE CLASS COMES FROM THE INJECTED PEER, never from an import of this package's own: a host that * vendored its own copy of the Winter SDK gets ITS store, ITS lease semantics and ITS typed errors — * the same argument `createRuntimeSdk` already makes for `resolveBrand`/`InvalidBrandError`. */ export declare function createSharedSessionStore(input: SharedSessionStoreInput): SharedSessionStore; /** * A shared store resolved on FIRST USE, from the injected peer's own `resolveWinterHome()`. * * WHY LAZY MATTERS, and it is not a micro-optimisation. The spine's wiring line is * `stubHandoffBarrier(context)` becoming `createHandoffBarrier(context)`, called for EVERY * `createRuntimeSdk` — including the ones in `test/spine/*`, whose fake peer exports five members and * neither a store class nor `resolveWinterHome`. A factory that built its store eagerly would throw at * construction for every host that never hands a session off, and would break every spine test the day * it was wired in. Resolved on the first `plan()`/`execute()`, it costs nothing until a handoff exists, * and a host that already has a store passes it and never reaches this at all. */ export declare function lazySharedSessionStore(input: { peers: RuntimeSdkPeers; winterHome?: string; /** WS-21: the store's root when it is not the home itself (`sdkHomeOf(winterHome)`). */ storeHome?: string; /** * The RESOLVED profile. NOT optional in practice: `resolveWinterHome()` defaults to `WINTER_BRAND`, * so a call without it would send a rebranded host's sessions to Winter's own home directory — the * exact failure the brand sweep gate exists to catch, and did catch, on this line. */ brand: Pick; policy?: Partial; }): () => SharedSessionStore;