import { type AuditEvent, type Json, type PermissionGrant } from "@vendoai/core"; import type { Db } from "../db.js"; import type { AppRow, ApprovalRow, RunRow, ThreadRow } from "./types.js"; export declare function appFromRow(row: Record): AppRow; export declare function putAppRow(db: Db, input: Pick, now?: string): Promise; /** Build contract §6 — `vendo_threads` no longer stores `messages`, but the * reserved-collection door and `threadStore` still hand callers a whole * thread. This aggregate reassembles the transcript **by seq** (never by * timestamp: approval flips rewrite older messages) so those read paths keep * their shape while storage is one row per message. * * Interpolated, not parameterized, because it names a correlated alias rather * than carrying a value — there is no user input anywhere in it. */ export declare const THREAD_MESSAGES_AGGREGATE: (alias: string) => string; /** Every row id this transcript will occupy, in array order. * * Split out because it is also the DOOR's validation: `ON CONFLICT` cannot be * given the same key twice in one statement (Postgres raises a bare 21000 * cardinality violation), so a transcript carrying two messages with one id used * to fail with a raw driver error and lose the whole write. Callers check here * first and refuse with a typed error that names the offender. */ export declare function threadMessageRowIds(messages: Json[]): string[]; /** The first row id that appears more than once, or undefined if all are unique. */ export declare function duplicateThreadMessageId(messages: Json[]): string | undefined; /** Land this thread's transcript as rows: array index becomes `seq`, an unchanged * message keeps its revision, and a message that left the array loses its row. * Two statements, each set-based — never one round trip per message. * * Requires ids to be unique already (`duplicateThreadMessageId`); the door * enforces that, because this statement cannot express a collision without * dropping one side. */ export declare function replaceThreadMessages(db: Db, threadId: string, messages: Json[], now?: string): Promise; /** Append (or edit) a batch of messages on a thread this subject owns, without * reading the transcript first — the one-statement ownership the hosted path * never had (the read-then-write TOCTOU named at helpers/thread-messages.ts). * * Two statements, both set-based, and the CALLER runs them in one transaction: * 1. create-or-touch the thread row, guarded on the subject exactly as * putThreadRow's upsert is — an empty RETURNING means a foreign row holds * the id, and the whole append is refused before a message row exists; * 2. one multi-row insert for the messages. * Statement 1 leaves the thread row write-locked for the rest of the * transaction, so statement 2 needs neither the ownership join nor the * `FOR KEY SHARE OF t` its single-row sibling carries: a concurrent * `deleteThread` cascade cannot slip between them and leave orphan rows. * * `seq` is assigned HERE, by statement 2, and never by the caller — the fix * for a real race (proven on PostgreSQL 17: 40 concurrent appends, 21 distinct * seqs). `seq` carries conversation order and has no unique constraint, so two * turns landing on the same number make the transcript fall back to ordering * by message id, which is not turn order. Any `max(seq) + 1` read taken BEFORE * the thread row is held is read by both racers under READ COMMITTED and hands * them the same answer. Statement 1 above is what serialises them: the loser * blocks there (on the row lock, or on the primary key when both are creating * the thread) until the winner COMMITs, so statement 2's subquery — a new * snapshot in READ COMMITTED — already sees the winner's rows. * * `seq` is NOT updated on conflict. An append names where NEW messages go; a * message the thread already holds has a decided position, and moving it would * reorder the conversation the next turn reads. New rows land after the tail in * batch order, which is the only rule the wire half can honor anyway — a wire * body carries no seq — so both halves of `upsertMany` share this exactly. */ export declare function appendThreadMessages(db: Db, input: { threadId: string; subject: string; messages: ReadonlyArray<{ id: string; message: unknown; }>; title?: string; }, now?: string): Promise<{ revision: string; count: number; }>; export declare function threadFromRow(row: Record): ThreadRow; export declare function putThreadRow(db: Db, input: Pick, now?: string): Promise; export declare function grantFromRow(row: Record): PermissionGrant; export declare function putGrantRow(db: Db, grant: PermissionGrant): Promise; export declare function approvalFromRow(row: Record): ApprovalRow; export declare function putApprovalRow(db: Db, row: ApprovalRow, upsert?: boolean): Promise; /** 02-store §2: vendo_audit is append-only. The insert refuses to touch an * existing row ATOMICALLY — ON CONFLICT DO NOTHING plus an empty RETURNING * means the id already exists, and the write is rejected instead of replacing * history. Deletion happens only through the store erase API (02 §5). */ export declare function putAuditRow(db: Db, event: AuditEvent): Promise; export declare function runFromRow(row: Record): RunRow; export declare function putRunRow(db: Db, run: RunRow): Promise; //# sourceMappingURL=rows.d.ts.map