export { coalescePendingParentCompletionQueueItems } from "./completion-batching.js"; export { shouldHoldParentCompletionQueueDispatch, listReleasableParentCompletionQueuesForSource } from "./completion-drain.js"; import Database from 'better-sqlite3'; import type { ChatBlock, ChatBlockEnvelope, EngineSessionRef, EngineSessionRefs, JsonObject, ReplyContext, Session, SessionAttemptOutcome, SessionDelivery, SessionDeliveryIdentity, SessionDeliveryPayload, WorkflowAttemptInterruptionCause, WorkflowSessionProvenance } from '../shared/types.js'; export declare const RESTART_ACK_META_KEY = "restartAcknowledgedAt"; /** Stamped on a session the gateway itself interrupted, so the next boot can tell it apart from one that was already idle. Consumed in sessions/restart-resume.ts. */ export declare const RESTART_RESUME_META_KEY = "restartInterruptedAt"; export declare const GATEWAY_RESTARTED_MESSAGE = "Gateway restarted successfully."; /** * Run the FTS backfill to completion synchronously. Exposed for tests and for * callers that genuinely want to block; the request path uses * `scheduleFtsBackfill` (which yields between chunks) instead. */ export declare function backfillFtsSync(database: Database.Database, chunkSize?: number): void; /** * Drop all FTS infrastructure from `database` and reset the backfill progress flags so * the NEXT boot retries the migration + backfill from scratch. Sets `ftsAvailable = * false` for the lifetime of this process so that `searchMessages` returns [] without * hitting the (now-absent) table. * * Called automatically by `initDb()` when the boot drain throws. Also exported as a * seam for tests and for callers that want to explicitly disable FTS (e.g. on detecting * external corruption). */ export declare function disableFtsForProcess(database: Database.Database, reason?: unknown): void; /** * Kick the one-time FTS backfill off the hot path. startGateway calls this only * after listen(); searchMessages calls it as a lazy fallback for library/test * consumers. Guarded by the persistent `fts_backfill_done` flag and a per-DB * in-process promise so concurrent callers share one drain. Each chunk is its own * transaction with a `setImmediate` yield in between, so a large historical table * is seeded without blocking the event loop. */ export declare function scheduleFtsBackfill(database?: Database.Database, chunkSize?: number): Promise; export interface MessageSearchResult { /** Anchor for getMessageContext — the matched message's id. */ messageId: string; sessionId: string; snippet: string; role: string; timestamp: number; /** Owning session's employee/engine (null when the session row is gone). */ employee: string | null; engine: string | null; } /** Deterministic AND-composed narrowing for searchMessages (GRS-020a). All * values become bound SQL parameters — never spliced into the statement. */ export interface MessageSearchFilter { sessionId?: string; /** Exclude one session's messages (GRS-020a-fix finding 1: the MCP tool * passes the caller's own session here by default, so "search for X" never * returns the caller's own act of searching for X). */ excludeSessionId?: string; /** Case-insensitive equality on the owning session's employee. */ employee?: string; /** Case-insensitive equality on the owning session's engine. */ engine?: string; role?: 'user' | 'assistant'; /** Inclusive epoch-ms bounds on the message timestamp. */ since?: number; until?: number; } /** * Full-text search over user/assistant message bodies, newest-first. `snippet` * wraps matched terms in «»; results are capped by `limit` (default 50). Triggers * the one-time backfill on first call so older history becomes searchable. * * GRS-020a: optional AND-composed filters, every value a bound parameter. The * sessions join is a LEFT JOIN so an orphan message (invariant breach — deleteSession * removes both) still surfaces when no session-field filter is passed; an * employee/engine equality predicate on a NULL join simply never matches, which is * the correct narrowing semantics. */ export declare function searchMessages(query: string, limit?: number, filter?: MessageSearchFilter): MessageSearchResult[]; export interface CreateSessionOpts { engine: string; source: string; sourceRef: string; connector?: string | null; sessionKey?: string; replyContext?: ReplyContext | null; messageId?: string; transportMeta?: JsonObject | null; employee?: string | null; model?: string; title?: string; parentSessionId?: string; workflowProvenance?: WorkflowSessionProvenance | null; userId?: string | null; effortLevel?: string; /** * Optional human-facing excerpt override. When the prompt is scaffolded * (e.g. talk delegation wraps the operator's ask in a brief + verbatim * block), callers pass the original ask here so list UIs don't show * scaffold junk. Still flattened/truncated via promptExcerptOf. */ promptExcerpt?: string; } /** Whitespace-flattened, ≤140-char excerpt of a prompt (undefined when empty). */ export declare function promptExcerptOf(prompt: string | undefined): string | undefined; export declare function createSession(opts: CreateSessionOpts & { prompt?: string; portalName?: string; }): Session; type WorkflowAttemptSessionOpts = CreateSessionOpts & { prompt?: string; workflowProvenance: WorkflowSessionProvenance; }; export declare function getOrCreateWorkflowAttemptSession(opts: WorkflowAttemptSessionOpts): Session; export declare function getSession(id: string): Session | undefined; export declare function getSessionBySourceRef(sourceRef: string): Session | undefined; export declare function getSessionBySessionKey(sessionKey: string): Session | undefined; export interface UpdateSessionFields { sessionKey?: string; engine?: string; engineSessionId?: string | null; engineSessions?: EngineSessionRefs | null; status?: Session['status']; attemptOutcome?: SessionAttemptOutcome | null; attemptToken?: string | null; attemptTerminalVersion?: number; attemptTurn?: number; attemptInterruptionCause?: WorkflowAttemptInterruptionCause | null; attemptInterruptionTurn?: number | null; model?: string | null; effortLevel?: string | null; lastContextTokens?: number | null; replyContext?: ReplyContext | null; messageId?: string | null; transportMeta?: JsonObject | null; lastActivity?: string; lastError?: string | null; title?: string; archivedAt?: string | null; userId?: string | null; } export declare function updateSession(id: string, updates: UpdateSessionFields): Session | undefined; /** Hide a chat from normal lists without deleting its session, messages, or * engine state. Repeated archive requests preserve the original timestamp. */ export declare function archiveSession(id: string): Session | undefined; /** Restore an archived chat to every normal session list. */ export declare function unarchiveSession(id: string): Session | undefined; /** * Atomically claim the next delegation-completion nudge. The JSON guard and its compare predicate live in one * SQLite UPDATE, so two duplicate idle callbacks cannot both observe the same count and both win. The observed * count reads 0 for another work item or none and 1 for a guard written before it; a surfaced guard never matches. */ export declare function claimDelegationCompletionNudge(id: string, workItemId: string, sentNudges?: number): Session | undefined; /** Atomically consume a previously claimed nudge before surfacing to parent. */ export declare function markDelegationCompletionSurfaced(id: string, workItemId: string): Session | undefined; /** Roll back only the nudge this caller claimed, to the count that preceded it: a failed first nudge leaves no guard, a failed second leaves the first standing. */ export declare function releaseDelegationCompletionNudge(id: string, workItemId: string, sentNudges?: number): Session | undefined; /** * Atomically clear only the guard observed by the caller. A newer work-item * claim wins over a stale operator-cycle reset, and unrelated live metadata is * preserved because json_remove executes against the current row. */ export declare function clearDelegationCompletionGuard(id: string, expectedWorkItemId: string): Session | undefined; /** * Record that a child explicitly reported UP to its parent via send_to_session * during its current attempt. The automatic parent-completion callback for that * same attempt is a duplicate of the explicit relay, so notifyParentSession * suppresses it when this marker matches the child's live attempt token. The * marker is per-attempt: a new turn mints a new token, so it self-expires. */ export declare function recordChildReportedToParent(id: string, attemptToken: string): void; /** Persisted nudge claims whose queue post may have been lost to a restart. */ export declare function listDelegationCompletionNudgedSessions(): Session[]; /** Start a new execution generation and make it the sole owner of terminal * writes for this session. The token is durable so stop/reset wins across * asynchronous engine completion and process boundaries. */ export declare function beginSessionAttempt(id: string, updates?: UpdateSessionFields): Session | undefined; /** Compare-and-set an update against the active attempt generation and state. * Returns undefined when a stop/reset/newer turn has taken ownership. A fields * producer runs inside the fence, so its merge cannot outlive a rejected write. */ export declare function updateSessionForAttempt(id: string, attemptToken: string, updates: UpdateSessionFields | ((current: Session) => UpdateSessionFields), expectedStatuses?: readonly Session['status'][]): Session | undefined; /** Terminal attempt receipt. Only the same generation while actively running * may settle; an interrupted row is therefore immutable to late success. */ export declare function completeSessionAttempt(id: string, attemptToken: string, updates: UpdateSessionFields | ((current: Session) => UpdateSessionFields)): Session | undefined; export declare function interruptSessionAttempt(id: string, reason: string, completedAt: string): Session | undefined; /** Upgrade a legacy terminal row that predates attempt tokens. The outcome and * terminal version are compare predicates, so a stale callback can never borrow * the token of a newer resume generation. */ export declare function ensureCallbackAttemptToken(id: string, expectedOutcome: string, expectedTerminalVersion: number): string | undefined; export declare function getEngineSessionRef(session: Session, engine?: string): EngineSessionRef; /** The fields a recorded native id merges into a session, without writing them. * Exposed so a caller that must not race folds the merge into its own fence. */ export declare function nextEngineSessionFields(session: Session, engine: string, nativeId: string, meta?: Omit): UpdateSessionFields; export declare function recordEngineSessionId(sessionId: string, engine: string, nativeId: string, meta?: Omit): Session | undefined; export interface SwitchSessionEngineOptions { model?: string | null; effortLevel?: string | null; } export declare function switchSessionEngine(sessionId: string, nextEngine: string, opts?: SwitchSessionEngineOptions): Session | undefined; export declare function clearEngineSessionRefs(sessionId: string, engine?: string): Session | undefined; export interface ListSessionsFilter { status?: Session['status']; source?: string; engine?: string; } export declare function listSessions(filter?: ListSessionsFilter): Session[]; /** * Every session id in the registry — archived and workflow-phase rows included. * Retention sweeps over per-session on-disk state must use this rather than * `listSessions`, whose `archived_at IS NULL AND workflow_kind IS NULL` filter is * a display concern: to a sweep an absent id means "delete that session's data", * and both of those kinds still resume. */ export declare function listAllSessionIds(): string[]; export interface ChatPin { key: string; kind: 'session' | 'employee'; pinnedAt: string; } export declare function listChatPins(): ChatPin[]; export declare function pinChat(key: string): void; export declare function unpinChat(key: string): void; export declare function listPinnedSessions(): Session[]; /** * The N most-recently-active sessions, newest first — a bounded window for * polled endpoints that only ever surface the recent tail. * `offset` pages deeper (newest-first) when the first window is all non-emitting * rows. Backed by idx_sessions_last_activity; avoids hydrating every row. */ export declare function listRecentSessions(limit: number, offset?: number): Session[]; /** * Total session count. A pure `COUNT(*)` — no row hydration or JSON parse — * for endpoints (e.g. /api/onboarding) that only need the number, not the rows. */ export declare function countSessions(): number; export declare const CRON_GROUP = "__cron__"; export declare const DIRECT_GROUP = "__direct__"; /** * A session whose `employee` equals the portal name (case-insensitively) is a * direct/COO session that happened to be tagged with the portal slug — there is * no org employee by that name. Collapse it to `null` so it buckets into the * direct group instead of spawning a phantom pseudo-employee group that renders * with the same title as the portal. Real org employees are unaffected. */ export declare function coercePortalEmployee(employee: string | null | undefined, portalName: string | null | undefined): string | null; /** * True for the gateway's own top-level agent session — the portal COO the * operator talks to, which by design has no employee identity of its own. * * Having no employee is NOT on its own the test: an employee can spawn a plain * session, and that child is employee-less too. What no session can produce is * a PARENTLESS one — every spawn and delegation route records a session caller * as the child's parent, whatever the request body asks for — and a workflow * attempt always carries its run in `workflowProvenance`. So the shape below is * reachable only from a surface the operator drives: the web console, a * connector conversation, an operator-authored cron, or the gateway itself. */ export declare function isPortalAgentSession(session: Session): boolean; /** Most-recent `perGroup` sessions for each group — the bounded default payload. */ export declare function listRecentPerGroup(perGroup: number, portalSlug?: string | null): Session[]; /** One group's sessions, newest first — used by the sidebar "load more" button. */ export declare function listSessionsForGroup(group: string, limit: number, offset: number, portalSlug?: string | null): Session[]; /** Search across ALL sessions by identity, title, or settled message text. */ export declare function searchSessions(query: string, limit?: number): Session[]; /** Deterministic AND-composed session search (GRS-020a). At least one filter is * required — an empty filter would be an unbounded alias of listSessions. */ export interface SearchSessionsFilter { /** Escaped-LIKE substring over title + prompt_excerpt + id (%/_ are literal). */ text?: string; /** Case-insensitive equality. */ employee?: string; /** Case-insensitive equality. */ engine?: string; status?: Session['status']; source?: string; parentSessionId?: string; workflowId?: string; workflowRunId?: string; workflowPhaseName?: string; /** Inclusive ISO-8601 bounds on last_activity (ISO strings compare lexicographically). */ activeSince?: string; activeBefore?: string; /** Deterministic derivation: status IN ('error','interrupted'). `waiting` is * deliberately excluded (operator ruling — usage-limit pauses self-resolve). */ needsAttention?: boolean; } export declare function searchSessionsFiltered(filter: SearchSessionsFilter, limit?: number): Session[]; /** Child sessions of a parent — backed by idx_sessions_parent. */ export declare function listChildSessions(parentSessionId: string): Session[]; /** * Execution attempts (sessions) linked to a work item — backed by * idx_sessions_work_item. The read-back half of the work-item slice * (cron mints+links an item; this reads its sessions). Newest first. */ export declare function listSessionsByWorkItem(workItemId: string): Session[]; /** Total session count per group, so the UI can show accurate "+N more". */ export declare function getSessionGroupCounts(portalSlug?: string | null): Record; /** Mark any sessions stuck in "running" status as "interrupted". Called on gateway startup — if the * gateway is starting, no sessions can actually be running. Sessions with an engine_session_id can be * resumed via the Claude --resume flag, so each one is stamped for the restart resume nudge as well. */ export declare function recoverStaleSessions(): number; /** Settle workflow attempts whose engine process was lost with the old gateway. The cause is stamped over any same-turn marker — that turn died with the gateway, it did not end on a message — and is what lets the runtime replace the attempt rather than spend its retry budget (see workflows/restart-redispatch.ts). */ export declare function recoverStaleWorkflowAttemptSessions(): number; /** * Turn restart requests recorded by the old gateway into durable chat notices * after the replacement gateway is listening. Message insertion and marker * removal share one transaction, so a crash can neither lose nor duplicate the * acknowledgement on the next boot. */ export declare function consumeRestartAcknowledgements(): number; /** * Get sessions that were interrupted by a gateway restart and can be resumed. * A session is resumable if it has an engine_session_id (Claude's internal session ID). */ export declare function getInterruptedSessions(): Session[]; /** * Record one completed turn's cost and turn count against a session. * * Called from exactly one place: settleTurn, in sessions/turn/completion.ts. * It exists because two session runners once kept their own copies of the * completion sequence and drifted — the web runner had three completion sites * and none accumulated, so every web- and talk-sourced session recorded * total_turns = 0 and total_cost = 0, silently disabling the employee budget * caps enforced from SUM(total_cost). Both runners now settle through * settleTurn; a caller anywhere else is the second copy that opened the hole. * * `result.cost` MUST be a per-turn delta, not a session-to-date total — see the * note on sumTranscriptUsage in claude-interactive.ts. */ export declare function recordTurnAccounting(sessionId: string, result: { cost?: number; numTurns?: number; }): void; /** * Accumulate cost and turns for a session (called after each engine run). */ export declare function accumulateSessionCost(id: string, cost: number, turns: number): void; export declare function getSessionSpend(sessionIds: string[]): number; export interface CostReportFilter { groupBy?: 'employee' | 'day'; since?: string; until?: string; employee?: string; limit?: number; } export interface CostReportRow { key: string; cost: number; turns: number; sessions: number; } export interface CostReport { range: { since: string | null; until: string | null; }; groupBy: 'employee' | 'day'; rows: CostReportRow[]; total: { cost: number; turns: number; sessions: number; }; } /** * Deterministic cost/spend report over existing session accounting only. * No budgets, no work-item joins, no judgment: this wraps sessions.total_cost * and sessions.total_turns exactly as the engines recorded them. */ export declare function getCostReport(filter?: CostReportFilter): CostReport; /** * Duplicate a session and all its messages, returning a new session with a fresh ID. * Does NOT fork the engine session — the caller handles that separately. */ export declare function duplicateSession(sourceId: string, newTitle?: string): { session: Session; messageCount: number; }; export declare function deleteSession(id: string): boolean; export declare function deleteSessions(ids: string[]): number; /** Attachment descriptor stored alongside a message and rendered by the web UI. */ export interface MessageMedia { type: 'image' | 'audio' | 'video' | 'file'; url: string; name?: string; mimeType?: string; size?: number; /** Displayed pixel size of an image, so the client can reserve its box before * the bytes arrive. Absent when nothing measured it. */ width?: number; height?: number; } export interface SessionMessage { id: string; role: string; content: string; timestamp: number; /** Parsed from the `media` JSON column; undefined when the message has no attachments. */ media?: MessageMedia[]; /** True for a live mid-turn block. Most engines replace these at turn end. */ partial?: boolean; /** Tool name when this block is a tool call — lets a reloaded block render as a tool card. */ toolCall?: string; /** Native engine call id used to correlate interleaved tool results. */ toolId?: string; /** Structured Chat Mode blocks rendered by the web UI. */ blocks?: ChatBlock[]; /** Safe structured UI metadata, used for reload-stable callback attribution. */ meta?: JsonObject; } export interface MessagePage { messages: SessionMessage[]; hasOlder: boolean; } export interface MessagePageOptions { /** Fetch messages strictly older than this message id. Omit for the newest tail. */ before?: string; /** Number of messages to return. Clamped to a bounded positive page size. */ limit?: number; } export declare function insertMessage(sessionId: string, role: string, content: string, media?: MessageMedia[], blocks?: ChatBlock[], presetId?: string, meta?: JsonObject): string; /** Insert a canonical row strictly after streamed evidence, even when settlement * and the last delta share a millisecond. This keeps tail pagination and reload * order aligned with the live event order without rewriting evidence timestamps. */ export declare function insertMessageAfter(sessionId: string, role: string, content: string, afterTimestamp: number, media?: MessageMedia[], blocks?: ChatBlock[]): string; export declare function getMessages(sessionId: string): SessionMessage[]; /** * Just the live mid-turn (`partial=1`) blocks for a session, in stream order. * Backed by idx_messages_partial_order so turn-settle reads only the handful of live * rows instead of loading + parsing the whole transcript to filter them out * (the heaviest sessions were 600+ messages loaded on EVERY turn-settle). */ export declare function getPartialMessages(sessionId: string): SessionMessage[]; export declare function getMessagePage(sessionId: string, options?: MessagePageOptions): MessagePage; /** Max messages each side of the anchor. */ export declare const MESSAGE_CONTEXT_MAX_RADIUS = 100; export interface MessageContextEntry { id: string; role: string; content: string; timestamp: number; isAnchor: boolean; } export interface MessageContext { sessionId: string; anchorMessageId: string; messages: MessageContextEntry[]; } /** * GRS-020a — the ±radius window around a message anchor (a search_messages * hit), so a search result becomes readable in place without pulling a whole * transcript. The radius is clamped to {@link MESSAGE_CONTEXT_MAX_RADIUS}; * selected message bodies are returned as stored. * Returns undefined when the message doesn't exist IN THAT SESSION (an anchor * from another session must not leak across). */ export declare function getMessageContext(sessionId: string, messageId: string, radius?: number): MessageContext | undefined; export declare function applyBlockEnvelope(sessionId: string, input: ChatBlockEnvelope, fallbackText?: string, options?: { partial?: boolean; seq?: number; }): string | null; /** * Insert a live mid-turn block (`partial=1`). `seq` orders blocks within the turn; * `toolCall` is set when the block is a tool call (renders as a tool card on reload). * These rows are usually wiped by `deletePartialMessages` at turn end. */ export declare function insertPartialMessage(sessionId: string, role: string, content: string, seq: number, toolCall?: string, toolId?: string): string; /** Grow the current partial text block in place (debounced text streaming). */ export declare function updatePartialMessage(id: string, content: string): void; /** Settle one exact partial tool row and attach its durable activity receipt. */ export declare function settlePartialToolMessage(id: string, content: string, activityReceiptId?: string): void; /** Replace a stored (non-partial) message's text in place. Used by external-turn * sync to upgrade a truncated early-Stop assistant row to the complete transcript * text instead of inserting a duplicate row. */ export declare function updateMessageContent(id: string, content: string): void; /** Delete all live partial blocks for a session (called at turn end before the final insert). */ export declare function deletePartialMessages(sessionId: string): number; /** Keep streamed blocks as canonical history. Used by engines whose final * answer is already represented as interleaved text + tool rows. */ export declare function finalizePartialMessages(sessionId: string): number; /** Settle one completed stream atomically: selected rows become durable evidence * and every other partial row is discarded. Updating selected ids first lets the * indexed trailing DELETE remove transient/duplicate rows without a large IN list. */ export declare function settlePartialMessages(sessionId: string, preserveMessageIds: ReadonlySet): number; /** Boot sweep: drop any partial blocks stranded by a mid-turn gateway restart. */ export declare function clearAllPartialMessages(): number; export declare function getSessionDelivery(id: string): SessionDelivery | undefined; export declare function getSessionDeliveryByQueueItemId(queueItemId: string): SessionDelivery | undefined; export declare function listPendingSessionDeliveries(): SessionDelivery[]; export declare function listDeadLetterSessionDeliveries(): import("../shared/types.js").SessionDeliveryDeadLetter[]; /** Atomically return one valid exhausted receipt to the live outbox. The * durable identity and id are retained; accepted rows are immutable. */ export declare function requeueDeadLetterSessionDelivery(deliveryId: string): SessionDelivery; /** Persist the callback intent before any HTTP enqueue/send. The composite * unique index is the concurrency arbiter; losers reuse the winning outbox id. */ export declare function claimSessionDelivery(input: SessionDeliveryIdentity & { payload: SessionDeliveryPayload; }): { delivery: SessionDelivery; claimed: boolean; }; /** * Claim one source-bound delivery without letting that source exceed a durable * delivery budget. The source attempt is checked first without the target, so * retrying after a newer target appears reuses the original receipt. */ export declare function claimSessionDeliveryWithinSourceLimit(input: SessionDeliveryIdentity & { payload: SessionDeliveryPayload; }, maxDeliveries: number): { delivery?: SessionDelivery; claimed: boolean; capped: boolean; }; /** Lease one due pending receipt before network I/O. The lease itself is stored * in next_attempt_at, so duplicate emitters and retry sweeps cannot concurrently * spend multiple retry attempts for the same durable identity. */ export declare function claimSessionDeliveryAttempt(deliveryId: string, now: number, leaseMs: number): SessionDelivery | undefined; export declare function recordSessionDeliveryFailure(deliveryId: string, error: string, options: { now: number; nextAttemptAt: number; maxAttempts: number; }): SessionDelivery | undefined; /** Atomically turn one pending outbox row into the parent notification message * and its restart-safe internal queue intent. Accepted retries return the same * ids without inserting, emitting, or waking anything again. Completion * receipts may share a bounded pending row: this preserves every receipt and * banner while preventing a settled backlog from spawning one engine per row. */ export declare function acceptSessionDelivery(deliveryId: string, targetSessionId: string, sessionKey: string): { delivery: SessionDelivery; accepted: boolean; queueCreated: boolean; }; export { enqueueQueueItem, markQueueItemRunning, markQueueItemCompleted, markRunningQueueItemsCompletedForSession, getQueueItem, cancelQueueItem, getQueueItems, cancelAllPendingQueueItems, recoverStaleQueueItems, listAllPendingQueueItems, claimWorkflowAttemptDispatch, cancelWorkflowAttemptDispatch, listPendingWorkflowAttemptDispatches, editPendingQueueItem, reassignPendingQueuePayloads, type QueueItem, } from './queue-item-registry.js'; export { insertFile, getFile, listFiles, deleteFile, setFilePath, type FileMeta } from './file-registry.js'; //# sourceMappingURL=registry.d.ts.map