import type { CoreEvent, SeatHealth } from '../core/types.js'; import { DaemonSignalLog } from '../core/daemon-signal-log.js'; export type { SeatHealth }; export declare function isSessionDeath(fallbackKind: string | undefined, reason: string | undefined): boolean; /** * What a seat's OWN output says when it has no credential (F-A45-006, wave 6): the fresh rig's pi * failed every ballot with "No API key found" while the roster read `signed_in: true` off a present * (empty) `auth.json`. The seat's stderr is the authoritative probe — when it says so, the roster's * `auth` must flip to `signed_out` (not only `council_eligible`), and the evidence rides with it. */ export declare const AUTH_REFUSAL_PATTERNS: RegExp[]; /** Whether a failure detail / stderr says the seat has no usable credential. */ export declare function isAuthRefusal(text: string): boolean; /** * A8 (DES-seats-page §4.3): what a seat's refusal SAID, in crew's own vocabulary — the words each * {@link AUTH_REFUSAL_PATTERNS} entry matched (`No API key`, `Not logged in`, `HTTP/1.1 401`, * `OAuth session expired`), deduplicated, joined `; ` — or `null` when none matched. Never the * CLI's surrounding prose: a refusal can carry a key, an account or an organisation in any shape, * and no mask can know them all, so the roster carries only what crew's patterns name. */ export declare function refusalSummary(text: string): string | null; /** How long a seat's own "no credential" report keeps its `auth` at `signed_out` with no ok output * since — the same window as the council bench; an ok unit output clears it at once. */ export declare const AUTH_FAILURE_WINDOW_MS: number; /** * How long a seat the ENGINE benched in a run (`seatBenched`) stays out of the next launches with * no ok output since. After this the seat is eligible again: if it is still dead, its next run * finds out with one refused turn and benches it again; an ok output (a chat turn, a unit the * operator sent there) clears the bench at once. */ export declare const SEAT_BENCH_WINDOW_MS: number; /** * (PRODUCT-WORKS J4) A QUOTA bench (`quota_exhausted`, `usage_limit`: a 429, a rate or usage limit) * expires on its own clock instead of the fixed window: the engine's `seatBenched.benchedUntilMs` * when the frame carries one (the provider's reset time when the refusal stated one, else the * engine's own backoff), else a backoff of 15, then 30, then 60 minutes by the seat's consecutive * quota benches with no good turn between ({@link quotaBackoffMs}). A copilot benched for quota on * the rig was otherwise never re-checked inside a run, and every later review had no distinct judge. * Other kinds (`not_logged_in`, `not_installed`, …) keep {@link SEAT_BENCH_WINDOW_MS}. */ export declare const QUOTA_BACKOFF_MS: readonly number[]; /** The furthest a bench's lift time may be (a provider reset beyond it is read as it); also how far * back the boot reads the bench trail. */ export declare const SEAT_BENCH_MAX_MS: number; /** Whether a bench reason is quota-class: its own clock, not the fixed window. */ export declare function isQuotaBench(reason: string): boolean; /** The backoff of a seat's `strikes`-th quota bench in a row: 15, 30, then 60 minutes. */ export declare function quotaBackoffMs(strikes: number): number; /** Where a bench's lift time came from: the engine's frame (`benchedUntilMs`), crew's own quota * backoff, or the fixed window of a non-quota bench. */ export type BenchUntilSource = 'engine' | 'backoff' | 'window'; /** When a bench recorded at `at` lifts, and why that time. */ export declare function benchUntil(at: number, reason: string, strikes: number, engineUntil?: number): { until: number; source: BenchUntilSource; }; /** * How long a seat's last step outcome stands as its live sign-in check on the container floor * (`roster-standing.ts`). There the daemon sends no live request of its own: the credential broker * answers the status, and the seat's own steps are the live check, because they ran on the floor * work runs on (the same seat container, the same broker credential). */ export declare const SEAT_ANSWER_WINDOW_MS: number; /** Why a seat's step did not answer: the engine's turn ceiling fired (`stepFailed{timedOut}`), or * the CLI process failed with no ok output (`stepFailed{workerError}`, not a deliver refusal and * not the seat's own "no credential" report, which signs it out instead). */ export type SeatNoAnswer = 'timed_out' | 'worker_error'; /** The seat's latest step outcome inside {@link SEAT_ANSWER_WINDOW_MS}. */ export interface SeatLastAnswer { /** ISO-8601 of the outcome. */ at: string; /** `true`: a unit's output came back ok (a chat's answer is one), or the engine vetoed the * substance of an answer the seat gave. */ answered: boolean; /** Present when `answered` is false. */ why?: SeatNoAnswer; } /** * A seat the engine benched in a run, carried to the next launches (`seatBenched`, wicked-core * after #590 S5). Since S5 no ballot finds a dead seat before routing, so a seat that reads signed * in but cannot work (out of quota, say) is only found when its first unit refuses. Without this, * every new run handed that seat a unit again. */ export interface SeatRecentBench { /** ISO-8601 of the bench. */ at: string; /** ISO-8601 of when the seat is eligible again ({@link benchUntil}: the engine's lift time, the * quota backoff, or {@link SEAT_BENCH_WINDOW_MS} after `at`). */ until: string; /** (J4) Where `until` came from. */ untilSource: BenchUntilSource; /** (J4) The seat's consecutive quota benches with no good turn between (1 on the first): the * backoff step. 0 on a non-quota bench. */ strikes: number; /** The engine's bench reason, as it persisted it (`quota_exhausted (no success in the run)`). */ reason: string; /** Who benched it in the run: the unit's `worker`, or a `judge`. */ source: string; /** The run that benched it, when the frame named one. */ run?: string; /** (A3) How many runs benched the seat while this bench was active (at least 1). */ runs: number; } /** * crew#1071: where a seat's recent bench is kept across a daemon restart. The bench used to live in * this tracker's memory only, and the boot replay re-reads only LIVE runs' events, so a bench * recorded by a run that has since ended (cancelled or failed: the normal end of a quota failure) * was gone after a restart. The seat read Ready again, and the next ask was seated on it alone and * failed with the same 429. The daemon wires this to its audit trail: one entry per bench and per * lift, read back at boot by {@link SeatHealthTracker.hydrateBenches}. */ export interface SeatBenchStore { /** A bench (or a later bench of the same seat) was recorded. */ benched(cli: string, rec: SeatBenchRecord): void; /** The seat's bench was lifted before its window ran out: an ok output, a chat reply, or its own * live sign-in check answering after it. */ lifted(cli: string, atMs: number): void; } /** One persisted bench, as {@link SeatBenchStore.benched} receives it and the boot reads it back. */ export interface SeatBenchRecord { /** Unix millis of the bench (the engine frame's own `ts` when it carried one). */ at: number; reason: string; source: string; session?: string; runs: number; /** (J4) Unix millis of when it lifts ({@link benchUntil}); absent on a record written before. */ until?: number; /** (J4) Where `until` came from; absent on a record written before. */ untilSource?: BenchUntilSource; /** (J4) The quota strikes ({@link SeatRecentBench.strikes}); absent on a record written before. */ strikes?: number; } /** One persisted entry read back at boot, oldest first or in any order (it is sorted). */ export type SeatBenchEntry = { kind: 'benched'; cli: string; rec: SeatBenchRecord; } | { kind: 'lifted'; cli: string; at: number; }; /** A seat's own report that it has no credential (`RosterSeat.auth_evidence`). */ export interface SeatAuthFailure { /** ISO-8601 of the report. */ at: string; /** A bounded excerpt of the seat's own words ("No API key found …"). */ detail: string; /** Where it was said: a council `ballot`, a unit's `worker` failure, or the `acp` handshake. */ source: 'ballot' | 'worker' | 'acp'; /** The run it happened in, when the frame named one. */ run?: string; } /** * In-memory per-seat health, folded from live CoreEvents. In-memory ON PURPOSE: health is a * statement about the running platform's ability to reach a CLI right now — a daemon restart * genuinely does not know, and "assume active, let the next failure/probe speak" is the honest * default (and the one the declarative-roster doctrine implies). One exception (crew#1071): the * engine's in-run bench of a seat is its own recorded verdict, not a guess, so it is kept through * the {@link SeatBenchStore} for its window and restored at boot. */ export declare class SeatHealthTracker { private readonly opts; private readonly entries; /** `${session}:${ord}` → cli key. `unitOutputCaptured` carries no seat; this is the correlation. */ private readonly assignments; /** cli key → the seat's latest own "no credential" report (F-A45-006), cleared by an ok output. */ private readonly authFailures; /** cli key → the engine's latest in-run bench of the seat (`seatBenched`), cleared by an ok * output or by {@link SEAT_BENCH_WINDOW_MS}. */ private readonly recentBenches; /** (J4) cli key → the seat's quota strikes: how many times in a row it was benched for quota * AFTER its previous quota bench had lifted (or failed the engine's re-probe), with no good turn * between. Kept after a bench's lift time passes, so the next quota bench backs off for longer; * cleared by an ok output, a chat reply, a live sign-in check after the bench, the engine's * `seatBenchLifted`, or a bench of another kind. */ private readonly quotaStrikes; /** (J4) cli key → the latest bench or lift time folded for the seat (unix millis). A `seatBenched` * frame older than it is a replay (a live run's events re-read at boot, after the trail already * restored or lifted the bench) and changes nothing. */ private readonly benchHighWater; /** cli key → the seat's latest step outcome (container-floor live check, {@link lastAnswerFor}). */ private readonly answers; /** Default `since` for seats that have never changed state. */ private readonly startedAt; constructor(opts?: { /** * When present, `acpFallback(session_died)` events check it for a correlated * daemon signal and log which case it was (crew#411). Absent: correlation skipped. */ signalLog?: DaemonSignalLog; /** Receives the correlation log lines (daemon wires `app.log.warn`). */ log?: (m: string) => void; }); /** crew#1071: where benches and their lifts are kept across a restart. `null`: memory only. */ private benchStore; /** crew#1071: keep every bench and lift from now on in `store` (the daemon's audit trail). */ attachBenchStore(store: SeatBenchStore): void; /** * crew#1071: rebuild the benches still inside {@link SEAT_BENCH_WINDOW_MS} from the persisted * entries (the daemon's audit trail), so a restart does not hand a seat that is out of quota its * next ask. Entries are folded in time order: a lift at or after a bench drops it, a later bench * replaces it. Nothing is re-persisted. */ hydrateBenches(entries: readonly SeatBenchEntry[], nowMs?: number): void; private raiseHighWater; /** Drop the seat's bench, and say so to the store when there was one to drop (crew#1071). */ private liftBench; /** Fold one CoreEvent into the map. Safe on every event type; unknown types are ignored. */ ingest(event: CoreEvent): void; /** Flip a seat ACTIVE: message cleared, `since` updated on a genuine transition. */ markActive(key: string, atMs?: number): void; /** Keep the seat's newest step outcome; an older frame (a replay folded late) never replaces it. */ private recordAnswer; /** * The seat's latest step outcome inside {@link SEAT_ANSWER_WINDOW_MS}, or `null` when no step ran * on it in that window. The roster reads it as the container floor's live sign-in check. */ lastAnswerFor(key: string, nowMs?: number): SeatLastAnswer | null; /** * (A3) Lift the seat's recent bench because something else proved it can work again — the roster * calls this when the seat's own LIVE sign-in check answered after an account bench (signed out, * out of quota). An ok output does the same through {@link markActive}. */ clearRecentBench(key: string, atMs?: number): void; /** * (J4) The seat's live sign-in check answered at `atMs` (an authenticated one-word request): a * quota bench that already lifted by its clock leaves strikes behind; a live answer after the * seat's latest bench clears them, as an ok turn would. No-op while a bench stands (the roster * lifts that one through {@link clearRecentBench}) or when the answer predates the latest bench. */ clearQuotaStrikesAfterLiveCheck(key: string, atMs: number): void; /** * Drop the seat's own "no credential" report when it is OLDER than `beforeMs` — the roster calls * this when the seat's LIVE sign-in check answered after it (the operator signed in since). A * report that landed after the check is kept: the seat refused again. */ clearAuthFailureBefore(key: string, beforeMs: number): void; /** Record a seat's own "no credential" report (F-A45-006) — the newest report wins. */ private recordAuthFailure; /** Stamp an OBSERVED error on the seat — `lastErrorAt` only; the status stays `active` (R5). */ private stampError; /** * The seat's own latest "no credential" report inside {@link AUTH_FAILURE_WINDOW_MS} with no ok * output since (F-A45-006), or `null`. `seatStanding` reads it as `auth: 'signed_out'` — the seat * said so itself — whatever the credential-file probe reads. */ authFailureFor(key: string, nowMs?: number): SeatAuthFailure | null; /** * The engine's latest in-run bench of the seat (`seatBenched`) inside * {@link SEAT_BENCH_WINDOW_MS} with no ok output since, or `null`. `seatStanding` reads it as * `council_eligible: false` with the reason and the time it lifts, so the next launch benches the * seat up front (`health.usable: false`) and the operator sees why on the roster. */ recentBenchFor(key: string, nowMs?: number): SeatRecentBench | null; /** The seat's health — a seat never seen in an event is ACTIVE with no message (the default). */ healthFor(key: string): SeatHealth; } //# sourceMappingURL=seat-health.d.ts.map