import type { WakeQueueService } from '../inbox/wake-queue.service.js'; import { type AdvanceCursorExpected, type ObservedConversationEntry, type ObservedConversationStore } from '../storage/schema/observed-conversation.store.js'; import type { InboxItem, SlackInboxItem } from '../../shared/inbox.js'; /** Newest-fitting bound: max messages in the final provider-facing envelope. */ export declare const CURSOR_DELIVERY_MAX_MESSAGES = 20; /** * Max UTF-8 bytes of the final provider-facing rendered envelope (PRD / Iris): * snapshot rows + Latest wake + previews + file metadata — everything the * agent turn receives from the cursor view. */ export declare const CURSOR_DELIVERY_MAX_BYTES: number; /** Soft per-message clip so one row cannot consume the whole budget alone. */ export declare const CURSOR_DELIVERY_MAX_MESSAGE_CHARS = 2000; export type CursorDeliveryFailureReason = 'continuity_degraded' | 'missing_trigger_observation' | 'store_error' | 'cas_failure'; export declare class CursorDeliveryError extends Error { readonly reason: CursorDeliveryFailureReason; constructor(reason: CursorDeliveryFailureReason, message: string); } export interface SurfaceDeliveryPlan { surfaceId: string; /** What getCursor must match for CAS (absent or present@N). */ cursorExpected: AdvanceCursorExpected; /** Inclusive ordinal to advance to (0 allowed for empty child-thread establish). */ nextDeliveredOrdinal: number; lastDeliveredEventId?: string; lastDeliveredMessageTs?: string; /** Bounded journal rows delivered for this surface (chronological). */ entries: ObservedConversationEntry[]; /** * Exact after-cursor population from the reconciled ordinal index * (`tailOrdinal − deliveredOrdinal`), not the capped retained journal-read * length. Used for “N earlier messages not shown” and merge union so archive * retention / read limits cannot under-count the true conversation. */ candidateCount: number; /** Count of this surface's candidates not included due to the bound. */ omittedCount: number; /** True when establishing present@0 with no journal rows. */ establishOnly: boolean; } export interface CursorDeliveryPlan { agentId: string; triggerItemId: string; triggerEventId: string; surfaces: SurfaceDeliveryPlan[]; /** Prompt body for the Slack wake (includes bounded context + latest wake). */ promptBody: string; committed: boolean; } export type PrepareCursorDeliveryResult = { kind: 'disabled'; } | { kind: 'already_delivered'; settledItemIds: string[]; } | { kind: 'prepared'; plan: CursorDeliveryPlan; } | { kind: 'failed'; error: CursorDeliveryError; }; export declare function setCursorDeliveryEnabledForTests(enabled: boolean | undefined): void; /** * Resolve whether cursor delivery + send hold are on. * Default on (enable cut); explicit `cursorDelivery.enabled: false` opts out. * Settings/read failures are errors (fail-closed), not silent disable — * "enabled but unreadable" must not look like intentional off. */ export declare function resolveCursorDeliveryEnabled(): Promise<{ kind: 'enabled'; } | { kind: 'disabled'; } | { kind: 'error'; error: CursorDeliveryError; }>; /** Convenience: true unless explicitly disabled (throws on settings error). */ export declare function isCursorDeliveryEnabled(): Promise; /** * Surfaces to snapshot for a Slack wake. * - Top-level channel: channel surface + response-thread surface (thread:messageTs). * - Thread reply: thread surface only. * - DM: DM surface only. */ export declare function surfacesForSlackWake(item: SlackInboxItem): string[]; export declare function triggerEventId(item: SlackInboxItem): string; /** * Prepare at claim/start. Does not advance cursors or settle queue. * When the trigger is already covered by the primary surface cursor, returns * already_delivered so the worker can settle without a provider turn. */ export declare function prepareCursorDelivery(input: { agentId: string; item: InboxItem; store?: ObservedConversationStore; }): Promise; /** * Commit at runtime.started (or after follow-up accept). * * Idempotent only when live cursor is already at/past the plan target * (`current >= target`). Any other expectation mismatch is fail-closed — * never rewrite the prepared CAS expectation to the live cursor. */ export declare function commitCursorDelivery(input: { plan: CursorDeliveryPlan; queue: WakeQueueService; /** Active/trigger item must not be settled by coalesce. */ excludeItemIds?: Iterable; store?: ObservedConversationStore; }): Promise<{ advanced: string[]; coalescedItemIds: string[]; }>; /** * Settle still-queued Slack wakes on the same exact surface whose observed * ordinals are within the captured tail. Never touches the active item, other * surfaces, later-than-tail rows, staged rows, or non-Slack items. * * Candidate selection is read-side; the settle is one atomic queue-store batch * so the selected set moves to seen together. */ export declare function coalesceCoveredWakes(input: { agentId: string; queue: WakeQueueService; surfaces: SurfaceDeliveryPlan[]; excludeItemIds: Set; store?: ObservedConversationStore; }): Promise; /** * Exact after-cursor count from a captured reconciled ordinal tail, not * retained-window length. Fail-closed when the index cannot support the claim * (missing index with retained rows, tail behind retained max, retained longer * than the ordinal span, or captured tail row absent from the journal window). * * `candidates` must already be filtered through the same captured tail * (`afterOrdinal < ordinal <= tail`). */ export declare function exactCandidateCountFromIndex(input: { afterOrdinal: number; candidates: ObservedConversationEntry[]; /** Captured reconciled journal-tail index; undefined when empty / unknown. */ index: { tailOrdinal: number; lastEventId?: string; } | undefined; isResponseThreadEstablish: boolean; surfaceId: string; }): number; /** Conversation-time order for cross-surface selection (messageTs primary). */ export declare function compareByConversationTime(a: { messageTs: string; receivedAt?: string; ordinal?: number; }, b: { messageTs: string; receivedAt?: string; ordinal?: number; }): number; /** * Pure union of plans by exact surface id (no re-window). Entries deduped by * eventId; cursorExpected prefers the more conservative (lower) present ordinal. * * Per-surface candidateCount is max(candidateCount_i) across plans (exact journal * population after the same cursor). omitted = candidateCount − unique(entries). * Never sum plan-local omitted aggregates across overlapping windows. */ export declare function mergeSurfacePlans(plans: CursorDeliveryPlan[]): SurfaceDeliveryPlan[]; /** * Merge plans and rebuild the full provider-facing envelope. * Single-plan merge preserves surfaces/omitted; multi-plan unions then * envelope-aware re-windows under 20/16KiB with prior omissions retained. */ export declare function mergeCursorDeliveryPlans(plans: CursorDeliveryPlan[], triggerItem: SlackInboxItem): CursorDeliveryPlan; /** * Newest-fitting by conversation time using only line bytes (unit tests / * internal). Prefer selectNewestFittingForEnvelope when advancing cursors. */ export declare function selectNewestFitting(candidates: ObservedConversationEntry[], options: { maxMessages: number; maxBytes: number; mustIncludeEventId?: string; }): { entries: ObservedConversationEntry[]; omittedCount: number; }; /** * Newest-fitting selection measured against the final provider-facing envelope * (rows + Latest wake + previews + files). Only rows that fit are returned; * nextDeliveredOrdinal must be derived from this set alone. */ export declare function selectNewestFittingForEnvelope(candidates: ObservedConversationEntry[], options: { maxMessages: number; maxBytes: number; mustIncludeEventId?: string; triggerItem?: SlackInboxItem; totalCandidates?: number; }): { entries: ObservedConversationEntry[]; omittedCount: number; }; export declare function formatObservedLine(entry: ObservedConversationEntry): string; /** @deprecated Use renderCursorDeliveryEnvelope — same output, full envelope. */ export declare function renderCursorDeliveryPrompt(trigger: SlackInboxItem, surfaces: SurfaceDeliveryPlan[]): string; /** * Final provider-facing cursor envelope: chronological rows + Latest wake + * previews/files. Must not post-truncate after nextDeliveredOrdinal is chosen; * callers select rows so this string is already ≤ 16 KiB. */ export declare function renderCursorDeliveryEnvelope(trigger: SlackInboxItem, surfaces: SurfaceDeliveryPlan[]): string; export declare function renderCursorDeliveryEnvelopeFromEntries(trigger: SlackInboxItem, entries: ObservedConversationEntry[], omitted: number): string; /** * Truncate to maxBytes UTF-8 without splitting code points / surrogate pairs. * Used for per-message clip and extras clipping — not for post-plan row mutation. */ export declare function truncateUtf8(text: string, maxBytes: number): string; /** * Clip previews/files so chrome + Latest wake + extras fit in the envelope * budget even with zero snapshot rows. Never leave an over-cap prepared plan. */ export declare function clipTriggerExtrasForEnvelope(trigger: SlackInboxItem): SlackInboxItem; /** Primary surface id for a plan (grouping key for follow-up batching). */ export declare function primarySurfaceId(plan: CursorDeliveryPlan): string; //# sourceMappingURL=cursor-delivery.d.ts.map