import { type AgentChannel } from "./agent-dispatch.js"; /** A calendar the connector-sync turn can read free/busy from. */ export type CalendarConnector = "google" | "m365"; /** * The outcome of one account's provisioning pass. * - `not-applicable` — no calendar connector, so there is nothing to sync. * - `provisioned` — the recurring :Event was created by this pass. * - `already-provisioned`— a live :Event was already armed. * - `unconfigured` — a connector exists but no exception destination does. * - `error` — the pass threw; the reason is on the log line. */ export type ProvisionOutcome = "not-applicable" | "provisioned" | "already-provisioned" | "unconfigured" | "error"; /** Where a provisioned turn sends an exception it needs the operator to see. */ export interface DispatchDestination { channel: AgentChannel; destination: string; } /** * The calendar connectors currently authorised for an account, sorted. An * authorised identity is one whose directory holds a `tokens.enc` — a * half-registered identity (directory created, consent never completed) is * deliberately NOT counted, so provisioning does not arm a turn that has * nothing to read. */ export declare function detectCalendarConnectors(accountDir: string): CalendarConnector[]; /** * Why no destination could be resolved. The platform computes the true cause * (Task 1439 gave `validateAgentDestination` reason codes precisely so the * diagnosis is not guessed); discarding it one frame below the log statement * would leave the operator reading `unconfigured` at an account whose * `account.json` visibly holds a phone number. * * - `none-configured` — no WhatsApp phone and no Telegram admin on the account. * - `not-registered` — a candidate exists but is in no authoritative list. * - `cross-account` — a candidate is registered, but to another account. * - `house-unresolvable` — the socket-owning house could not be resolved. This * is an INSTALL-WIDE fault, not a per-account configuration gap. * - `unreadable-config` — `account.json` is present but unreadable or malformed. */ export type DestinationRefusal = "none-configured" | "not-registered" | "cross-account" | "house-unresolvable" | "unreadable-config"; export type DispatchResolution = { destination: DispatchDestination; reason: null; } | { destination: null; reason: DestinationRefusal; }; /** * Where this account's connector-sync turn reports an exception, or the reason * no channel destination is reachable. * * WhatsApp is preferred, Telegram is the fallback. Every candidate is passed * through `validateAgentDestination` — the same authority the dispatcher's * standing audit applies at fire time — so provisioning can never arm a binding * the audit would immediately report as stale. `accountsDir` is the parent * accounts directory (`$PLATFORM_ROOT/../data/accounts`). */ export declare function resolveDispatchDestination(accountsDir: string, accountId: string): DispatchResolution; /** The last outcome reported for one account, and when it was reported. * * `signature` is the value the change detector compares, not `outcome`: an * `error` carries a prefix of its message, so a CHANGED error is reported * immediately while an unchanged one heartbeats like any other steady state. * Without it a persistently failing account would log every 120 s forever, * reproducing the noise this task removes, and its eventual recovery would be * invisible because the state map would still hold the pre-error outcome. */ export interface ProvisionStateEntry { outcome: ProvisionOutcome; signature: string; loggedAtMs: number; } /** The comparison key for one observation. */ export declare function provisionSignature(outcome: ProvisionOutcome, detail?: string): string; export type ProvisionState = Record; /** * Whether this tick reports the account's outcome. * * A permanent condition reported at tick rate is noise that hides its own * meaning: the evidence for the Task 1809 defect was 5,688 identical lines that * everyone read as benign. So a line is emitted when the outcome CHANGES, when * the account has never been reported, or when the heartbeat window has * elapsed — the heartbeat being what keeps "inert on every account" and "steady * state" distinguishable without counting log lines by hand. */ export declare function shouldEmitProvisionLine(prev: ProvisionStateEntry | undefined, nextSignature: string, nowMs: number, heartbeatMs: number): boolean; /** How this observation is labelled on the log line. `first` is NOT `change`: * labelling a first observation as a change means that any state loss produces * a synchronised fleet-wide burst of `emit=change`, indistinguishable from * every account genuinely flipping outcome at once — which is a real * emergency, and the one moment the label most needs to be trustworthy. */ export declare function emitLabel(prev: ProvisionStateEntry | undefined, nextSignature: string): "first" | "change" | "heartbeat"; /** Read the persisted state. An absent or corrupt file reads as empty, which * costs one redundant line per account and never suppresses one. A corrupt * file is reported: it is the trigger for a fleet-wide `emit=first` burst, and * that burst must carry its own explanation. */ export declare function readProvisionState(path: string): ProvisionState; /** * Persist the state. Written to a temp path and renamed, so a kill or a hard * freeze mid-write cannot leave truncated JSON — which would read back as empty * and produce the fleet-wide burst described above. * * A failed write must never abort the reconcile pass, but it must not be silent * either: every realistic cause (disk full, filesystem remounted read-only, * mode drift) is PERSISTENT, so the failure degrades emit-on-change back to * per-tick logging indefinitely — worse than what it replaced, because every * line then falsely claims to be a first observation. */ export declare function writeProvisionState(path: string, state: ProvisionState): void; //# sourceMappingURL=connector-sync-provision.d.ts.map