/** * TG-05/TG-06: Telegram owner-report context inbox store. * * The messenger SQLite database is the single report-context authority for * owner-report delivery events and turn-consumption receipts * (docs/development/telegram-outbound-context-inbox-design.md, Decision 4). */ import type { SQLiteDatabase } from '../sqlite.js'; export type ReportContextEventState = 'prepared_retryable' | 'prepared_definite_rejection' | 'delivered' | 'cancelled'; export interface ReportContextTarget { source: 'telegram'; channelId: string; } export interface ReportContextReservationInput { deliveryId: string; target: ReportContextTarget; mode: 'digest' | 'full'; occurrence: Record; provenance?: Record; text: string; payloadIdentity: string; } export interface ReportContextEvent { seq: number; deliveryId: string; state: ReportContextEventState; } export interface ReportContextEventDetail extends ReportContextEvent { attemptCount: number; nextAttemptAt: string | null; leaseUntil: string | null; rejectionReason: string | null; cancelReason: string | null; } export interface TelegramReportContextStoreOptions { nowIso?: () => string; /** * TG-05 Slice K (design Decision 8): when set, the constructor solely * migrates the legacy V2 carry file (last-full-report.json) into the * event/legacy-restoration tables and renames the source `.migrated`. * No component reads or writes V2 afterward. */ legacyCarryPath?: string; /** Design Decision 5 live capacity (prepared-or-unconsumed) per target. */ liveRowCapPerTarget?: number; liveByteCapPerTarget?: number; /** Retained exact-text capacity per target before consumed rows compact. */ retainedRowCapPerTarget?: number; retainedByteCapPerTarget?: number; } export declare class TelegramReportContextStore { private readonly db; private readonly nowIso; private readonly liveRowCap; private readonly liveByteCap; private readonly retainedRowCap; private readonly retainedByteCap; constructor(db: SQLiteDatabase, options?: TelegramReportContextStoreOptions); /** * TG-05 Slice K: migrate the legacy V2 carry file. Unconsumed -> a * delivered-pending event (even past the old 24h TTL); consumed -> a * target-scoped legacy restoration record carrying the EXACT old prefix * bytes. Transactionally insert/verify, commit, then atomically rename the * source `.migrated`. Invalid files are quarantined and never injected. */ private migrateLegacyCarry; /** * Idempotently insert the exact prepared report before any external send. * A replay of the same delivery ID must carry the identical payload; any * divergence is an identity conflict and never silently overwrites. */ reserve(input: ReportContextReservationInput): ReportContextEvent; /** * Atomically transition a confirmed send to `delivered`. Idempotent: an * already-delivered row keeps its original delivery time so crash-recovery * replays converge instead of rewriting history. */ markDelivered(deliveryId: string, deliveredAtIso: string): void; /** * Compare-and-swap attempt lease: exactly one holder may execute a Telegram * send at a time. A live lease excludes every new claim - another worker, * another tick, or the same owner - and an expired lease is recoverable * after restart. Returns null when no claim is possible. */ claimAttempt(deliveryId: string, owner: string, nowIso: string, leaseUntilIso: string): { deliveryId: string; owner: string; leaseUntilIso: string; } | null; /** * Safe cancellation (design Decision 3): permitted only from * prepared_definite_rejection - the one state where the Telegram ledger * proves definite non-acceptance and no attempt lease is live. Ambiguous or * confirmed acceptance can never be cancelled. Replay on an already * cancelled row is a converging no-op that keeps the first audit record. */ cancel(deliveryId: string, reason: string, operatorTimeIso: string): void; /** * Explicit operator reactivation of a definite rejection after conditions * are corrected (for example the bot was unblocked). Same delivery ID, same * immutable target; the row becomes immediately recoverable. */ reactivate(deliveryId: string): void; /** * Delivered reports not yet consumed by an owner turn, for one exact * target, oldest first. This is the pending-projection source; one chat can * never read another target's reports. */ listDeliveredPending(target: ReportContextTarget): Array<{ seq: number; deliveryId: string; mode: 'digest' | 'full'; deliveredAtIso: string; text: string; }>; /** * Committed receipts for the given source-message references, in the input * order. Only text-bearing receipts are returned; a compacted receipt no * longer restores. */ listReceiptsByRefs(refs: string[]): Array<{ sourceMessageRef: string; deliveryIds: string[]; projectionText: string; committedAtIso: string; }>; /** * Legacy V2 restorations for one target, eligible for fresh-session history * (30 days after consumption, design Decision 8). Restoration text is * JSON-decoded back to the exact legacy prefix string. */ listLegacyRestorations(target: ReportContextTarget, nowIso: string): Array<{ deliveryId: string; consumedAtIso: string; deliveredAtIso: string; restorationText: string; }>; private compactRetained; private enforceLiveCapacity; /** * Design Decision 5: explicit two-step operator action (token flow lives at * the API layer). Archives ONLY already-delivered pending rows up to and * including `throughSeq`, records the audit fields, and removes them from * pending projection and live capacity. Never Telegram cancellation. */ archiveDelivered(target: ReportContextTarget, throughSeq: number, actor: string, reason: string, nowIso: string): string[]; /** Live usage for the status surface; warn at 80% of either bound. */ liveUsage(target: ReportContextTarget): { rows: number; bytes: number; rowCap: number; byteCap: number; warn: boolean; }; /** * Tombstone GC (design Decision 4): identity tombstones are retained up to * 365 days with a global count cap. No tombstone younger than Telegram's * seven-day replay window is ever pruned; beyond that floor the oldest * terminal tombstones go first. Returns the number of removed rows. */ pruneTombstones(nowIso: string, options?: { maxTombstones?: number; }): number; /** Read one event's attempt/terminal detail; null when unknown. */ getEvent(deliveryId: string): ReportContextEventDetail | null; /** * Record a retryable send failure: keep the row nonterminal, stamp the next * attempt time, and release the lease. Only the current lease holder may * reshape backoff - a stale worker's outcome must not race a live one. */ scheduleRetry(deliveryId: string, owner: string, nextAttemptAtIso: string): void; /** * Record a definite Telegram non-acceptance. The row stays pinned in the * ledger (nonterminal) and is never automatically retried; only an explicit * operator action may reactivate it (design Decision 3). */ markDefiniteRejection(deliveryId: string, owner: string, reason: string): void; /** * Startup/tick recovery: prepared_retryable rows that are due (no future * retry scheduled) and not held by a live attempt lease, oldest first. */ listRecoverable(nowIso: string): ReportContextEvent[]; /** * Startup ledger reconciliation input: every nonterminal row must hold a * Telegram-ledger pin, every terminal row must not (design Decision 2). */ listPinReconciliation(): { nonterminal: string[]; terminal: string[]; }; private runMigration; } //# sourceMappingURL=telegram-report-context-store.d.ts.map