/** * ingress-alarm.ts, a message from the owner that the daemon failed to process * is an INCIDENT, not a log line. * * The failure this exists for, in full: a route binding pointed at a closed * session, every inbound Telegram update threw `Session is closed`, and the * poller did the only sensible thing with a poison update, logged it at warn * and advanced the cursor past it. That warn went into a multi-megabyte debug * file. Nothing else happened. Channel health went on reporting the surface * fine, because health watches whether the poll LOOP is running and the loop * was running perfectly; it was the processing behind it that was eating * messages. The owner found out by noticing silence. * * Advancing the cursor is still right, a wedged cursor redelivering one poison * update forever is the worse failure, and it takes the whole channel down * rather than one message. What changes here is that advancing past is LOUD: * * 1. The surface's health goes degraded with the real reason, through the * observation the health rule already reads (`ChannelRuntimeObservation.lastError` * → `resolveChannelHealthState` → `degraded`). No parallel health mechanism. * 2. The owner is told once, on a channel that still works. Not once per * message: a broken processing path fails every message, and an alarm that * fires per message is an alarm nobody reads. First failure notifies; * repeats inside the window are counted, not re-sent; recovery says so once. * * The rollover in session-broker-intent.ts means the closed-session class * should not reach here at all any more. This is for the classes nobody has * predicted yet, which is the point, since the predicted one is exactly the * one that went unnoticed for a day. */ import type { ChannelSurface } from './types.js'; /** How long after a notified failure the same surface stays quiet. */ export declare const DEFAULT_INGRESS_ALARM_WINDOW_MS: number; /** What the owner is told, and what the health observation reports. */ export interface ChannelIngressFailureState { /** The named reason processing failed, carried verbatim into health. */ readonly detail: string; /** When the CURRENT run of failures started. */ readonly since: number; /** When the most recent failure happened. */ readonly at: number; /** How many messages this run has failed to process. */ readonly count: number; /** When the owner was last told about this run, or null if never. */ readonly notifiedAt: number | null; } export interface ChannelIngressAlarmDeps { /** * Put one line in front of the owner. The implementation picks a channel * that still works, deliberately not necessarily `surface`, because the * surface that just failed to RECEIVE may well still SEND (Telegram's sends * worked throughout the incident this exists for), and if it cannot, another * connected channel can. */ readonly notify: (surface: ChannelSurface, text: string) => void; /** Quiet window after a notification; defaults to 30 minutes. */ readonly windowMs?: number | undefined; /** Test seam. */ readonly now?: (() => number) | undefined; } /** * Per-surface latch over inbound-processing failures. * * Deliberately not per-message and not per-error-text: the question the owner * needs answered is "is this channel carrying my messages", which is a property * of the surface over time, not of any one update. */ export declare class ChannelIngressAlarm { private readonly deps; private readonly states; constructor(deps: ChannelIngressAlarmDeps); private now; private windowMs; /** The live failure run for a surface, or null when it is processing cleanly. */ failure(surface: ChannelSurface): ChannelIngressFailureState | null; /** * One inbound message could not be processed and was skipped. * * Returns true when this call notified the owner, so a caller can assert on * the rate limit rather than on the notification side effect. */ recordFailure(surface: ChannelSurface, detail: string): boolean; /** * A message processed cleanly. Ends a failure run, and says so once if the * owner had been told about it. */ recordSuccess(surface: ChannelSurface): void; /** * Notifying must never be a way for the alarm to take down the caller that * was already handling a failure. */ private send; } /** * One ingress loop's answer to "can I process what I am receiving?", kept * separate from "am I receiving?". * * Those are different questions and conflating them is the entire defect. The * Telegram poll loop was turning over perfectly, mode polling, running true, * reason "long-polling", while every update it handed on threw, and the * surface reported `healthy` for a day. So the failure is held HERE rather than * on the supervisor's own status object, which the mode transitions replace * wholesale and would have quietly erased it on the next refresh. * * `lastError` is what an ingress folds into the health observation it publishes * (`observedRuntime(running, reason, lastError)` → `degraded`). The alarm, if * one is wired, decides separately whether the OWNER hears about it; a node * without one still logs and still reports degraded. * * Written against `ChannelSurface` rather than Telegram so any ingress with a * catch-and-continue loop gets the same behaviour by holding one of these. */ export declare class IngressProcessingHealth { private readonly surface; private readonly alarm?; private failure; constructor(surface: ChannelSurface, alarm?: ChannelIngressAlarm | undefined); /** The reason processing is currently failing, or undefined when it is not. */ get lastError(): string | undefined; /** * One inbound message could not be processed and was skipped past. * * Skipping is still right, a cursor wedged on one poison message redelivers * it forever and takes the whole channel down instead of one message. What * changed is that it is loud: ERROR rather than the warn line that sat unread * in a multi-megabyte debug file, plus degraded health, plus the alarm. */ recordFailure(detail: string, context?: Record): void; /** A message processed cleanly, ending any run of failures. */ recordSuccess(): void; } //# sourceMappingURL=ingress-alarm.d.ts.map