import type { AgMemoryRecord, AgReduceResult, JsonValue } from "@silverprotocol/core"; import type { ThreadMessageEvent, StoredHistoryMessage, ThreadMessageKind, ThreadMessageRole, ThreadPersistencePort, ThreadSnapshotRow, TurnOrigin, ClientClass } from "./rows.js"; export interface EnsureThreadInput { /** Client-supplied thread id (localStorage). Absent on first contact. */ threadId?: string; /** Resolved end-user id (`g_`). */ userId: string; appId: string; /** Region pin for a freshly-created Thread (the pod's `AWS_REGION`). */ region: string; } export interface AppendMessageInput { threadId: string; userId: string; role: ThreadMessageRole; /** Arbitrary JSON persisted on the row (string for plain text turns). */ content: unknown; /** Plain-text projection for the preview + transcript render. */ text?: string; /** Idempotency key — a retried invoke with the same key won't double-write. */ clientMessageId: string; kind?: ThreadMessageKind; /** * guuey#524: this write belongs to a PAGE-AWARE turn — the pod (the only * honest caller; never client-claimed) passes `true` and the row is * stamped `untrustedOrigin`. Vocabulary is {absent, true}: omit on every * trusted-origin write, never pass `false`. */ untrustedOrigin?: boolean; /** * guuey#552: the typed event payload for `kind:'event'` rows — carried * verbatim onto the row. The caller sets `kind:'event'` alongside; the * store does not infer one from the other. */ event?: ThreadMessageEvent; /** A user row's origin (see {@link ThreadMessageRow.turnOrigin}); carried verbatim. */ turnOrigin?: TurnOrigin; /** A `card_action` user row's answered card session id; carried verbatim. */ answeredCardSessionId?: string; /** A user row's client class (see {@link ThreadMessageRow.clientClass}); carried verbatim. */ clientClass?: ClientClass; } export interface AppendMessageResult { seq: number; /** True when an existing row matched `clientMessageId` (no new write). */ deduped: boolean; } export interface AppendFoldInput { threadId: string; userId: string; fold: AgReduceResult; /** Base for the turn-level idempotency sentinel + per-row derived keys. */ clientMessageIdBase: string; /** * Suppress the `putSnapshot` upsert (rows still write). Set by the caller * when the prior snapshot read failed OR the reducer parked * (`needsResync`), so a degraded/partial fold never clobbers a good * snapshot. Default: write the snapshot. */ skipSnapshot?: boolean; /** * guuey#524: the whole fold belongs to a PAGE-AWARE turn — every row it * writes (messages AND cards) is stamped `untrustedOrigin`. Same * {absent, true} vocabulary as {@link AppendMessageInput.untrustedOrigin}. */ untrustedOrigin?: boolean; /** * The prior snapshot's `carriedThreadMemory`: stored thread-memory elements * this runtime did not read or own. Written back unchanged beside the fold's * own thread records, so they are never dropped at rest. Absent = none. */ carriedThreadMemory?: readonly JsonValue[]; /** * The prior snapshot's `threadMemory`: the view the reducer was seeded * from. A record this turn did not change is persisted as this prior record, * with the members it was stored with (`keepUnchangedThreadMemory`), never as * the fold's rebuild of it. Absent = the fold's records as they are. */ priorThreadMemory?: readonly AgMemoryRecord[]; } export interface AppendFoldResult { messageSeqs: number[]; artifactSeqs: number[]; /** Count of non-thread (durable) memory records dropped in v1. */ droppedDurableMemory: number; /** * True when the turn-level sentinel matched a committed prior turn and the * WHOLE fold-persist was short-circuited (no rows written this call). */ deduped: boolean; } export declare class ThreadStore { private readonly db; constructor(db: ThreadPersistencePort); /** * Resolve the thread for this invoke: * - `threadId` found AND owned by `userId` → return it (the happy path); * - otherwise (no `threadId`, stale/unknown id, OR owned by a different * identity) → mint a fresh Thread with a server-assigned id and return it. * * Minting-fresh on an owner mismatch (rather than erroring) is deliberate: * the caller's anon `guuey_guest` cookie can rotate (cleared, or a cross-site * request where it isn't sent), which would otherwise orphan a stored * `threadId` behind a permanent 403. Minting fresh is both resilient (chat * continues on a new conversation) AND safe — the caller never sees another * user's thread, and there's no existence oracle. The pod never honours a * client-chosen id, so squatting is impossible. */ ensureThread(input: EnsureThreadInput): Promise; /** * Whether `threadId` exists AND is owned by `userId` — the SAME ownership * predicate `ensureThread` applies before honouring a client-replayed id, * exposed for doors that must BIND a client-echoed thread to the verified * caller without minting anything (the pod's consent-answer door binds a * `once` grant to a thread this way — guuey#207). No existence oracle: a * missing thread and another user's thread are the same `false`. */ ownsThread(threadId: string, userId: string): Promise; /** Prior messages for the thread (seq-ASC, capped to the most recent N). */ loadHistory(threadId: string, limit?: number): Promise; /** * Append one message with an atomic, gap-free seq. Idempotent on * `clientMessageId`: a prior row with the same key returns its seq without * a second write. Mirrors `ops/append-message.ts` steps 2–4. */ appendMessage(input: AppendMessageInput): Promise; /** The thread's latest fold snapshot, or undefined when none persisted yet. */ getSnapshot(threadId: string): Promise; /** * Persist a turn's fold delta: one row per message, one card row per * artifact (each via an atomic gap-free seq), turn records inlined on the * message rows, and an upserted snapshot for working state + thread-memory. * * Idempotency is TURN-LEVEL, not per-row. A retried invoke re-runs the LLM * and can yield a *different* fold (different message count / content); * per-index dedup would keep some prior rows and append new ones for the * rest → a Frankenstein/orphan thread + clobbered snapshot. Instead, the * FIRST row written (the first message, or — if there are no messages — the * first card) claims a single `${base}#agentTurn` sentinel key. On any later * call with the same base, the sentinel matches and the WHOLE persist is * short-circuited (a committed agent turn is never re-merged). * * Durable (non-thread) memory is logged-and-dropped in v1. `skipSnapshot` * suppresses the snapshot upsert so a degraded/parked fold never overwrites * a good snapshot. */ appendFold(input: AppendFoldInput): Promise; } //# sourceMappingURL=store.d.ts.map