/** * windows.ts, the approval gate and the veto window. * * ══ READ THIS BEFORE UNIFYING THEM ════════════════════════════════════════ * * These two look like near-duplicates. Merging them into one timed-prompt * primitive with a configurable default would be a natural cleanup and it would * be a serious defect. See docs/decisions/2026-07-27-payment-windows-are-deliberately-opposite.md. * * ABOVE budget → an APPROVAL. Silence means DENIED. * WITHIN budget → a VETO. Silence means PROCEEDS. * * Owner's reasoning for the approval side: * * "if i didn't want the approval to expire, I should have just increased the * limit... puts it directly in the human's hands, never lets automated * spending happen" * * They answer different questions. The approval asks *may this happen at all*, * and an unanswered question about money above the limit must resolve to no. The * veto announces *this is about to happen*, and an unanswered announcement about * money inside a limit the owner already set must resolve to yes, otherwise the limit * does nothing and every purchase is an approval. * * Collapsing them means picking one silence rule for both. Either every * above-budget purchase starts going through unattended, or every in-budget one * stalls waiting for a human, and the second gets "fixed" by flipping the * default, which produces the first. * * The duplication below is load-bearing. There is deliberately no shared * `openTimedPrompt()`. * * ══ Presence is not attention ═════════════════════════════════════════════ * * The window runs its full configured duration regardless of where the owner is. No * presence, focus, idle or activity signal shortens, skips or extends it, the * deadline is a function of the start instant and the configured duration and * nothing else. Owner's reasoning: * * "this is for situations where the user is multitasking and doesn't look at * the specific terminal session for an extended period of time" * * Only an explicit acknowledgement short-circuits it. */ import type { CommandAuthorityChannel } from './types.js'; /** * What happens when a window closes with no answer. * * The two windows must never share a value. A test asserts both values * individually and asserts they differ, so a change to either fails with a * message naming the ruling rather than silently converting denials into * purchases. */ export type SilenceMeaning = 'denied' | 'proceeds'; export declare const APPROVAL_GATE: { readonly kind: "approval"; readonly silenceMeans: "denied"; }; export declare const VETO_WINDOW: { readonly kind: "veto"; readonly silenceMeans: "proceeds"; }; /** What the delivery layer reported, per channel. */ export interface ChannelDelivery { readonly channel: CommandAuthorityChannel; readonly delivered: boolean; /** Whether this channel's history can be re-read for a span we were down. */ readonly backfillable: boolean; } export type ApprovalState = 'pending-dispatch' | 'awaiting-approval' | 'approved' | 'denied-explicit' | 'denied-timeout' | 'denied-undeliverable' | 'void'; export type VetoState = 'pending-dispatch' | 'open' | 'proceeding-acknowledged' | 'proceeding-silent' | 'proceeding-undelivered' | 'cancelled' | 'void'; /** Every terminal state settles its reservation exactly one way. */ export type Settlement = 'commit' | 'release' | 'hold'; export declare function approvalSettlement(state: ApprovalState): Settlement; export declare function vetoSettlement(state: VetoState): Settlement; export declare function isTerminalApproval(state: ApprovalState): boolean; export declare function isTerminalVeto(state: VetoState): boolean; /** The deadline. A pure function of the start and the duration, nothing else. */ export declare function windowDeadlineMs(startedAtMs: number, minutes: number): number; export type ApprovalEvent = { readonly kind: 'dispatched'; readonly deliveries: readonly ChannelDelivery[]; } | { readonly kind: 'undeliverable'; } | { readonly kind: 'approve'; readonly channel: CommandAuthorityChannel; } | { readonly kind: 'deny'; readonly channel: CommandAuthorityChannel; } | { readonly kind: 'deadline'; } | { readonly kind: 'total-changed'; }; export type VetoEvent = { readonly kind: 'dispatched'; readonly deliveries: readonly ChannelDelivery[]; } | { readonly kind: 'undeliverable'; } | { readonly kind: 'acknowledge'; readonly channel: CommandAuthorityChannel; } | { readonly kind: 'object'; readonly channel: CommandAuthorityChannel; } | { readonly kind: 'deadline'; } | { readonly kind: 'total-changed'; }; /** * The approval gate. SILENCE DENIES. * * Note `undeliverable` → `denied-undeliverable`. An above-budget purchase whose * notification could not reach the owner does not happen: there is nobody to put it in * front of, and the whole point of the above-budget branch is that a human * decides. */ export declare function advanceApproval(state: ApprovalState, event: ApprovalEvent): ApprovalState; /** * The veto window. SILENCE PROCEEDS. * * Note `undeliverable` → `proceeding-undelivered`. Owner ruling: under or at * budget, items get through. This is the exact mirror of the approval's * undeliverable edge, and that pair of edges is the owner's undeliverable ruling in its * entirety. */ export declare function advanceVeto(state: VetoState, event: VetoEvent): VetoState; /** * What a restart should do with a window whose deadline passed while we were * down. * * ── Keyed on DELIVERY, not on uptime ────────────────────────────────────── * * Silence means "the owner had the chance to object and did not." Whether OUR * process was alive has nothing to do with whether they had that chance. An * earlier draft keyed this on uptime and re-opened every interrupted window; * that is wrong, because it re-pings them about something they deliberately * ignored, and a system that repeats itself is one they stop reading. * * - delivered, then expired → the expiry STANDS. Backfill each live channel * for the downtime span and honour any objection found there before charging. * Do not re-notify: the owner already saw it. * - never delivered → the undeliverable rule governs, unchanged. * - cannot be backfilled → re-open on THAT CHANNEL ONLY, because only that * channel cannot distinguish silence from an objection we dropped. */ export interface WindowRecovery { readonly outcome: 'expiry-stands' | 'undeliverable-rule' | 'reopen'; /** Channels whose history must be read for the downtime span before settling. */ readonly backfillChannels: readonly CommandAuthorityChannel[]; /** Channels the window re-opens on, when it re-opens at all. */ readonly reopenChannels: readonly CommandAuthorityChannel[]; readonly reason: string; } export declare function recoverInterruptedWindow(input: { readonly deliveries: readonly ChannelDelivery[]; readonly deadlinePassed: boolean; }): WindowRecovery; //# sourceMappingURL=windows.d.ts.map