import { type AgentSubmissionReference, type AgentSubmissionPayload, type AgentRunOptions } from "@agent-native/agentkit/protocol"; import { type SteeredQueuedMessageRecord, type ThreadAnnotationSnapshotConflict } from "../agent/thread-data-builder.js"; import { type DbExec } from "../db/client.js"; import { type AccessContext } from "../sharing/access.js"; import { type ShareRole } from "../sharing/schema.js"; export declare function withThreadDataLock(threadId: string, fn: () => Promise): Promise; export declare const chatThreadsStore: import("../db/store-registry.js").Store; /** * Explicitly repair `message_count` for legacy rows written before the count * was maintained. This must never run from table/bootstrap initialization: * serverless isolates would each scan the full `thread_data` blob column on * cold start. Operators may invoke it once when upgrading an old database. */ export declare function repairLegacyChatThreadMessageCounts(options?: { batchSize?: number; }): Promise<{ scanned: number; updated: number; }>; /** * A resource the chat is bound to, e.g. `{ type: "deck", id: "deck-abc" }`. * The framework is opaque to the type string — each template chooses what * its primary resource is and the surface it scopes to (deck, design, * dashboard, etc.). `label` is a denormalized snapshot for display when * the resource isn't on hand at render time; the live template can * overwrite it via the next createThread call. */ export interface ChatThreadScope { type: string; id: string; label?: string; } export declare function isAppOwnedChatScope(scope?: ChatThreadScope | null): boolean; /** * A scoped rail may claim a legacy unscoped thread on its first write, but * once a thread has a scope, a non-null incoming scope must match it. App- * owned threads additionally require a scope on every subsequent write. */ export declare function threadScopeMismatch(existing?: ChatThreadScope | null, incoming?: ChatThreadScope | null): boolean; export interface ChatThreadSource { platform?: string | null; appId?: string | null; url?: string | null; } export interface ChatThread { id: string; ownerEmail: string; title: string; preview: string; threadData: string; messageCount: number; createdAt: number; updatedAt: number; scope: ChatThreadScope | null; pinnedAt: number | null; hiddenAt?: number | null; archivedAt: number | null; source: ChatThreadSource | null; orgId: string | null; visibility: "private" | "org" | "public"; } /** * Which of the caller's lists a thread belongs to. `mine` is ownership alone, * so a chat the caller owns stays in it however widely it is shared; `shared` * is accessible-but-owned-by-someone-else. */ export type ChatThreadOwnership = "all" | "mine" | "shared"; export declare const CHAT_THREAD_OWNERSHIPS: readonly ChatThreadOwnership[]; export declare function isChatThreadOwnership(value: unknown): value is ChatThreadOwnership; /** * Why the caller can see a thread. A direct per-user grant outranks * organization access when both apply, so revoking the organization grant never * looks like it removed access the person was given by name. */ export type ChatThreadAccessBasis = "owner" | "direct" | "organization"; export interface ChatThreadSummary { id: string; ownerEmail?: string; accessBasis?: ChatThreadAccessBasis; title: string; preview: string; messageCount: number; createdAt: number; updatedAt: number; scope: ChatThreadScope | null; pinnedAt: number | null; hiddenAt?: number | null; archivedAt: number | null; source: ChatThreadSource | null; orgId: string | null; visibility: "private" | "org" | "public"; } /** List and search rows always include the caller's ownership and access basis. */ export interface ChatThreadListRow extends ChatThreadSummary { ownerEmail: string; accessBasis: ChatThreadAccessBasis; } export interface ForkThreadSourceSnapshot { threadData: string; title?: string; preview?: string; messageCount?: number; fromMessageId?: string; scope?: ChatThreadScope | null; } export declare function createThread(ownerEmail: string, opts?: { id?: string; title?: string; scope?: ChatThreadScope | null; source?: ChatThreadSource | null; /** Explicit owner organization for durable/background callers. */ orgId?: string | null; initialData?: { threadData: string; preview: string; messageCount: number; }; }): Promise; export declare function registerChatThreadsShareable(): void; export declare function ensureChatThreadTables(): Promise; /** * Yes/no access check that never reads the conversation body. Callers that * only need the answer (run polling, run ownership) must use this, not * `resolveThreadAccess`: that one returns the whole thread, and `thread_data` * is the full message history, so a boolean check would pull megabytes per call. */ export declare function hasThreadAccess(userEmail: string | null | undefined, threadId: string | null | undefined, minRole?: ShareRole | "owner", ctx?: Omit): Promise; export declare function resolveThreadAccess(userEmail: string | null | undefined, threadId: string | null | undefined, minRole?: ShareRole | "owner", ctx?: Omit): Promise; export declare function resolveThreadAccessScope(userEmail: string | null | undefined, threadId: string | null | undefined, minRole?: ShareRole | "owner", ctx?: Omit): Promise<{ scope: ChatThreadScope | null; } | null>; export declare function resolveThreadsAccess(userEmail: string | null | undefined, threadIds: readonly string[], ctx?: Pick): Promise>; export declare function getThread(id: string): Promise; /** * Fill missing provenance on a thread without rewriting an established origin. * Integration retries and long-lived mapped conversations both pass through * this path, so the first source remains the source shown in chat history. */ export declare function setThreadSourceIfMissing(id: string, ownerEmail: string, source: ChatThreadSource | null | undefined): Promise; export declare function forkThread(sourceId: string, ownerEmail: string, opts?: { id?: string; source?: ForkThreadSourceSnapshot | null; sourceAccessGranted?: boolean; }): Promise; export interface ListThreadsOptions { limit?: number; offset?: number; /** * Filter for chats bound to a specific resource. The default (undefined) * returns every thread the user owns. `{ type: "deck", id: "abc" }` * returns only that resource's threads. `{ type: "deck", id: null }` is * NOT supported — pass `unscopedOnly: true` to get only general chats. */ scope?: { type: string; id: string; }; /** When true, returns only threads with no scope (general chats). */ unscopedOnly?: boolean; orgId?: string | null; /** * Include archived threads in the results. Defaults to false: archived * threads (`archived_at` set via `setThreadArchived`) are hidden from the * ordinary chat list/search so archiving actually removes a thread from * view. Pass true for surfaces that explicitly need to see archived chats * (e.g. an "Archived" filter or restoring one via `setThreadArchived`). */ includeArchived?: boolean; /** Include chats hidden by this caller. Defaults to false. */ includeHidden?: boolean; /** * Include connected and other-app threads. The HTTP chat list defaults this * to false so each app shows its own local chats first; internal callers * keep the historical all-sources behavior unless they opt out explicitly. */ includeExternal?: boolean; /** Current app id used by the local-only view. */ sourceAppId?: string | null; /** Defaults to `all`. Narrowed in SQL, so LIMIT/OFFSET page within the list. */ ownership?: ChatThreadOwnership; } export declare function listThreads(ownerEmail: string, options?: ListThreadsOptions | number, legacyOffset?: number): Promise; export declare function searchThreads(ownerEmail: string, query: string, limit?: number, options?: { scope?: { type: string; id: string; }; orgId?: string | null; /** See `ListThreadsOptions.includeArchived` — defaults to false. */ includeArchived?: boolean; /** Include chats hidden by this caller. Defaults to false. */ includeHidden?: boolean; /** See `ListThreadsOptions.includeExternal`. */ includeExternal?: boolean; /** Current app id used by the local-only view. */ sourceAppId?: string | null; /** See `ListThreadsOptions.ownership`. */ ownership?: ChatThreadOwnership; offset?: number; }): Promise; /** * Scope a thread should carry after a run inside a resource: adopt when it has * none, otherwise keep what it has. An unscoped thread reads as general, and a * general chat renders inside every resource — so never retag, never clear. */ export declare function resolveRunThreadScope(existing: ChatThreadScope | null, incoming: ChatThreadScope | null | undefined): ChatThreadScope | null; /** * Claim an unscoped thread for `scope`, returning the scope it actually ends up * with. `withThreadDataLock` only serializes one process, so two workers can * both read the same unscoped row; the `scope_type IS NULL` guard makes the * first writer win and the loser reports the winner instead of retagging. */ export declare function adoptThreadScopeIfUnscoped(id: string, ownerEmail: string, scope: ChatThreadScope): Promise; /** * Detach or rebind a chat's scope. Used by the UI's "Detach from " * action and by templates that need to retag a chat after a rename. Pass * `null` to clear the scope (chat becomes general). */ export declare function setThreadScope(id: string, ownerEmail: string, scope: ChatThreadScope | null): Promise; export declare function assertThreadOwner(thread: Pick, ownerEmail: string): void; export declare function renameThread(id: string, title: string, options: { ownerEmail: string; }): Promise; export declare function setThreadPinned(id: string, pinned: boolean, options: { ownerEmail: string; orgId?: string | null; }): Promise; export declare function setThreadHidden(id: string, hidden: boolean, options: { ownerEmail: string; orgId?: string | null; }): Promise; export declare function setThreadArchived(id: string, archived: boolean, options: { ownerEmail: string; }): Promise; export interface UpdateThreadDataOptions { preserveExistingQueuedMessages?: boolean; preserveExistingTopLevelKeys?: boolean; preserveCurrentMetadata?: boolean; /** * Preserve the title and its override from each current CAS revision; a * blank title still fills from the merged visible prompts. */ preserveCurrentTitle?: boolean; preserveCurrentTitleAndPreview?: boolean; trustedRunSequenceResetIds?: ReadonlySet; onAnnotationConflict?: (conflict: ThreadAnnotationSnapshotConflict) => void; beforeUpdate?: (tx: DbExec) => Promise; afterUpdate?: (tx: DbExec) => Promise; transformThreadData?: (currentThreadData: string) => string | { threadData: string; preview?: string; }; maxAttempts?: number; ignoreConflicts?: boolean; } export declare class InlineAttachmentDataNotPersistableError extends Error { readonly code = "inline_attachment_data_not_persistable"; constructor(message?: string); } export declare function updateThreadData(id: string, threadData: string, title: string, preview: string, messageCount: number, options?: UpdateThreadDataOptions): Promise; export interface ThreadEngineMeta { engineName: string; model: string; } /** * Read the engine pinned to a thread (stored in thread_data JSON). * Returns null if no engine is pinned. */ export declare function getThreadEngineMeta(threadId: string): Promise; /** * Pin an engine to a thread by storing engineMeta in thread_data JSON. * Does not change messages, title, or preview. */ export declare function setThreadEngineMeta(threadId: string, meta: ThreadEngineMeta): Promise; export interface QueuedMessage { id: string; text: string; threadId?: string; createdAt?: string; attachments?: unknown[]; requestAttachments?: unknown[]; references?: AgentSubmissionReference[]; metadata?: Record; options?: AgentRunOptions; interruptActiveRun?: boolean; promotionClaim?: { id: string; expiresAt: number; }; /** * Send now was pressed during a run: `steering` waits for the run's next * tool boundary, `after_reply` sends as the next turn. Thread state only, * never part of the admitted payload. */ delivery?: QueuedMessageDelivery; } export type QueuedMessageDelivery = "steering" | "after_reply"; export type QueuedSubmissionState = "queued" | "submitted" | "removed" | "expired" | "blocked"; export interface QueuedSubmissionActor { email: string; authUserId?: string; identityAuthenticatedAtMs?: number; name?: string; orgId?: string | null; orgScope?: "personal" | null; anonymous?: boolean; } export interface QueuedSubmissionReceipt { threadId: string; messageId: string; state: QueuedSubmissionState; actor: QueuedSubmissionActor; message?: QueuedMessage; payloadHash: string; submittedRunId?: string; interruptRunId?: string; retryAfter?: number; retryCount: number; createdAt: number; updatedAt: number; } export declare class QueuedSubmissionAdmissionError extends Error { readonly code: "queued_message_already_submitted" | "queued_message_id_conflict" | "queued_message_id_terminal"; readonly submittedRunId?: string | undefined; constructor(code: "queued_message_already_submitted" | "queued_message_id_conflict" | "queued_message_id_terminal", messageId: string, submittedRunId?: string | undefined); } export declare function hashQueuedSubmissionPayload(threadId: string, message: QueuedMessage | AgentSubmissionPayload): string; export declare function queuedSubmissionBehaviorMatches(threadId: string, message: QueuedMessage, identity: AgentSubmissionPayload): boolean; /** Returns the durable admission receipt used to dedupe direct starts and queue retries. */ export declare function getQueuedSubmissionReceipt(threadId: string, messageId: string): Promise; export declare function getQueuedSubmissionReceiptStatus(threadId: string, messageId: string): Promise<{ state: QueuedSubmissionState; submittedRunId?: string; } | null>; /** Checks the inbox inside a caller's transaction before a fresh run claims its slot. */ export declare function hasPendingQueuedSubmissionReceipt(tx: DbExec, threadId: string): Promise; /** Finds accepted queue rows in priority order for the signed server sweep. */ export declare function listQueuedSubmissionReceipts(options?: { limit?: number; now?: number; threadId?: string; interruptActiveRun?: boolean; }): Promise; /** Records a run admission in the same transaction that persists its user message. */ export declare function markQueuedSubmissionSubmitted(tx: DbExec, input: { threadId: string; messageId: string; runId?: string; actor: QueuedSubmissionActor; message?: QueuedMessage; hashInput?: AgentSubmissionPayload; }): Promise; /** Delays a retryable drain failure without releasing the accepted submission. */ export declare function retryQueuedSubmissionReceipt(threadId: string, messageId: string, retryAfter: number, options?: { claimId?: string; countRetry?: boolean; }): Promise; /** * True when a queued receipt on this thread was moved to the head, which Send * now does. Lets a run check its tool boundaries without reading thread data. */ export declare function hasPrioritizedQueuedSubmission(threadId: string): Promise; /** * Takes every Send now message off the queue for delivery into `runId`, and * marks each admitted receipt submitted to that run in the same transaction, * so the inbox can never also start it as a new turn. */ export declare function claimSteeredQueuedMessages(threadId: string, runId: string): Promise>; /** Marks Send now messages that missed the run's last tool boundary. */ export declare function deferSteeredQueuedMessages(threadId: string): Promise; /** Retains but stops automatically promoting a row that can no longer be authorized. */ export declare function blockQueuedSubmissionReceipt(threadId: string, messageId: string, options?: { claimId?: string; }): Promise; /** Persist the active run before interrupting it so a later inbox claim can finish the abort. */ export declare function recordQueuedSubmissionInterruptRun(input: { threadId: string; messageId: string; runId: string; claimId: string; }): Promise; /** Renews a queued submission's live promotion claim during admission. */ export declare function renewQueuedSubmissionClaim(input: { threadId: string; messageId: string; claimId: string; runId?: string; renewUntil?: number; }): Promise; /** Renews a live interruption claim before its recorded run is aborted. */ export declare function renewQueuedSubmissionInterruptClaim(input: { threadId: string; messageId: string; runId: string; claimId: string; }): Promise; /** Clear a recorded interruption only after its turn abort is durable. */ export declare function clearQueuedSubmissionInterruptRun(input: { threadId: string; messageId: string; runId: string; claimId: string; }): Promise; export type ThreadQueuedMessageMutation = { type: "append"; message: QueuedMessage; hashInput?: AgentSubmissionPayload; } | { type: "remove"; messageId: string; } | { type: "cancel"; messageId: string; } | { type: "moveToTop"; messageId: string; } | { type: "steer"; messageId: string; } | { type: "claimSteered"; runId: string; } | { type: "deferSteered"; } | { type: "reorder"; messageIds: readonly string[]; } | { type: "claim"; messageId: string; claimId: string; } | { type: "claimFromInbox"; receipt: QueuedSubmissionReceipt; claimId: string; } | { type: "retryReceipt"; messageId: string; retryAfter: number; claimId?: string; countRetry?: boolean; } | { type: "blockReceipt"; messageId: string; claimId?: string; } | { type: "recordInterruptRun" | "clearInterruptRun"; messageId: string; runId: string; claimId: string; } | { type: "renewClaim"; messageId: string; runId?: string; renewUntil?: number; claimId: string; } | { type: "release"; messageId: string; claimId: string; }; export interface ThreadQueuedMessageMutationResult { queuedMessages: QueuedMessage[]; message?: QueuedMessage; /** A claim check that loses the claim clears this to undefined. */ claimedMessage?: QueuedMessage | undefined; alreadySubmitted?: boolean; submittedRunId?: string; claimBusy?: boolean | undefined; submissionExpired?: boolean; promotionBusy?: boolean; /** A reorder named a different message set than the queue holds. */ queueOrderStale?: boolean; receiptManaged?: boolean; receiptUnavailable?: boolean; retryDeferred?: boolean; receiptConflict?: boolean; receiptUpdated?: boolean; claimLost?: boolean; released?: boolean; /** Messages a run took from the queue at a tool boundary. */ steeredMessages?: QueuedMessage[]; steeredRecords?: SteeredQueuedMessageRecord[]; } /** Applies a queue operation to the latest durable thread state on every CAS retry. */ export declare function mutateThreadQueuedMessages(threadId: string, mutation: ThreadQueuedMessageMutation, options?: { actor?: QueuedSubmissionActor; }): Promise; export interface ChatThreadShareState { enabled: boolean; createdAt: number | null; updatedAt: number | null; revokedAt: number | null; } export interface ChatThreadShareLink extends ChatThreadShareState { enabled: true; token: string; } export declare function hashThreadShareToken(token: string): string; export declare function getThreadShareState(threadId: string, options?: { ownerEmail?: string; }): Promise; export declare function createThreadShareLink(threadId: string, options?: { ownerEmail?: string; }): Promise; export declare function revokeThreadShareLink(threadId: string, options?: { ownerEmail?: string; }): Promise; export declare function getThreadByShareToken(token: string): Promise; /** * Grant a user an explicit share on a thread they don't own. Used by the * messaging-integration path, where a channel conversation runs as the * integration service principal and so creates a thread owned by * `integration@` rather than the human who asked — without this the * "Open thread" deep link resolves to a 404 for them. * * Idempotent, and never downgrades an existing stronger role. */ export declare function grantThreadUserShare(threadId: string, userEmail: string, role: ShareRole, grantedBy: string): Promise; export declare function deleteThread(id: string, ownerEmail: string): Promise;