/** * Input-blocked retry — keeps retrying messages the input guard held back, * and tells someone when an agent's input stays blocked. * * Since crewly#1014 the harness types into an agent only when its input box * is readable and empty, and presses Enter only on text it can prove it * wrote. A refused message goes back on the agent's queue * (`[INPUT_NOT_OURS]`). The queue otherwise drains only on registration or * on a busy→idle change — an agent sitting idle in front of a box holding * someone's half-typed text would never get it. This service: * * - retries on a timer while the queue is non-empty and the agent is idle, * with backoff (15 s → 2 min); * - after a few minutes or a few refusals, tells the owner/orchestrator once * per blocked episode what kind of content blocks it (never the text: a * box can hold a password or a code); * - clears its state when a delivery to the agent succeeds; * - restarts the agent's session when its input box has stayed unreadable * for RESTART_AFTER_UNREADABLE_MS with messages queued and the agent not * mid-turn — the last step of the unreadable ladder (2026-10-08 Ella: * held 50 min until the owner restarted her by hand). Once per agent per * RESTART_COOLDOWN_MS, traced as `harness.recover` (`input-unreadable`). * A box holding someone else's text is never a reason to restart; * - sends at most one alert per agent per NOTIFY_COOLDOWN_MS (30 min). The * one exception: an alert the owner must act on (box unreadable or holding * someone's text) still goes out once after an informational "busy in a * long turn" alert in the same window — the FYI must not hide a real block. * * @module services/messaging/input-blocked-retry.service */ /** What the guard saw when it refused. */ export interface InputRefusal { /** `unknown` (unreadable), `foreign` (someone else's text), or `busy` (held while the agent looked mid-turn) */ state: string; /** How many characters the box held (its text is never kept or shown) */ inputLength: number; /** The message that was held */ message: string; } /** A blocked episode, as passed to the notifier. */ export interface InputBlockedNotice { sessionName: string; /** * What blocks it: `foreign` / `unknown` (refusals), `busy`, `stuck`, or * `circuit-open` (refused for so long that automatic redelivery was * slowed to a probe now and then — crewly#1028); `blockedState` then * says what the box held. */ state: string; /** With `circuit-open`: the guard reading that kept refusing */ blockedState?: string; /** How many characters of text not written by Crewly the box held */ inputLength: number; refusals: number; blockedForMs: number; /** The first held message (to find its origin) */ message: string; } /** Collaborators (wired in index.ts). */ export interface InputBlockedRetryDeps { /** Messages still queued for the agent */ hasQueued(sessionName: string): boolean; /** The agent is not in a turn */ isIdle(sessionName: string): boolean; /** Deliver the agent's queued messages now */ flush(sessionName: string): Promise; /** Tell the owner / orchestrator once */ notify(notice: InputBlockedNotice): Promise; /** * The agent may be mid-turn (live spinner / busy bar, or output within * RESTART_QUIET_MS). Without it the unreadable-input restart never runs. */ isMidTurn?(sessionName: string): Promise; /** * Restart the agent's session through the normal stop/start path, * resuming its conversation (or starting fresh with a handover when it * is too big). Without it the unreadable-input restart never runs. */ restart?(sessionName: string): Promise; } /** Why messages to an agent are held right now, for the owner's notes. */ export type InputHold = 'unreadable' | 'foreign'; /** * Retries held messages and reports agents whose input stays blocked. */ export declare class InputBlockedRetryService { private static instance; private deps; private readonly episodes; /** Agents whose long busy hold was already reported (until a delivery) */ private readonly busyHoldNotified; /** Agents whose blocked input was already reported via the circuit breaker (until a delivery) */ private readonly circuitNotified; /** Last alert sent per agent, for the per-agent cooldown (survives deliveries) */ private readonly lastAlert; /** Last unreadable-input restart per agent (survives deliveries, for the cooldown) */ private readonly lastRestart; /** Agents being restarted right now */ private readonly restarting; private readonly logger; private readonly now; /** * @param now - Clock (tests) */ constructor(now?: () => number); /** * The process-wide service. * * @returns The singleton */ static getInstance(): InputBlockedRetryService; /** * Replace the singleton (tests). * * @param service - The service, or null to rebuild lazily */ static setInstance(service: InputBlockedRetryService | null): void; /** * Wire collaborators. Without them refusals are only counted. * * @param deps - Collaborators */ setDeps(deps: InputBlockedRetryDeps): void; /** * The guard refused to deliver to this agent; the message is queued. * * @param sessionName - The agent * @param refusal - What the guard saw */ noteRefusal(sessionName: string, refusal: InputRefusal): void; /** * Messages to this agent are being held because it looks mid-turn. A * hold that lasts BUSY_HOLD_NOTIFY_MS is reported once (until the next * delivery): an agent that looks busy for that long may be stuck, and * nothing else would say so. * * @param sessionName - The agent * @param heldForMs - How long messages have been held for it * @param message - The first held message */ noteBusyHold(sessionName: string, heldForMs: number, message: string): void; /** * An idle agent's input box has held text the harness cannot attribute * (not provably its own pastes, though no outside input arrived since * them) for STUCK_INPUT_NOTIFY_MS. Reported once; nothing is submitted. * * @param sessionName - The agent * @param inputLength - Characters in the box (never the text) * @param forMs - How long it has been there */ noteStuckInput(sessionName: string, inputLength: number, forMs: number): void; /** * The input circuit breaker opened for this agent: every delivery has * been refused for a while and automatic redelivery is now only a probe * now and then (crewly#1028). Tells the owner once per blocked episode — * unless this episode was already reported, so one blocked box is one * alert, not two. * * @param sessionName - The agent * @param info - What the breaker saw */ noteCircuitOpen(sessionName: string, info: { state: string; inputLength: number; refusals: number; blockedForMs: number; }): void; /** * A delivery to this agent went through: the episode is over. * * @param sessionName - The agent */ noteDelivered(sessionName: string): void; /** * Whether an agent is in a blocked episode. * * @param sessionName - The agent * @returns True while blocked */ isBlocked(sessionName: string): boolean; /** * Why messages to an agent are held, while they are: its box cannot be * read, or holds text Crewly did not write. Null when nothing is held * for those reasons (busy agents are not "held" here). * * @param sessionName - The agent * @returns The hold, or null */ holdOf(sessionName: string): InputHold | null; /** * Stop all timers (shutdown, tests). */ stop(): void; /** * Schedule the next retry with backoff, unless one is pending. * * @param sessionName - The agent * @param ep - Its episode */ private schedule; /** * One retry: flush the queue when the agent is idle, else wait again. * * @param sessionName - The agent * @param ep - Its episode */ private retry; /** * Last step of the unreadable ladder: restart the agent's session when * its box has been unreadable for RESTART_AFTER_UNREADABLE_MS, messages * are still queued for it, it is not mid-turn, and it was not restarted * for this within RESTART_COOLDOWN_MS. The queue drains once the * restarted agent registers. Never throws. * * @param sessionName - The agent * @param ep - Its episode */ private maybeRestart; /** * Whether an alert about this agent may go out now, and if so record it. * One alert per agent per NOTIFY_COOLDOWN_MS; an actionable alert may * still follow an informational one (a long-turn notice) in that window. * * @param sessionName - The agent * @param actionable - The owner has to do something (vs. busy in a long turn) * @returns True when the alert may be sent */ private claimAlert; /** * Tell someone once per episode, after long enough or often enough. * * @param sessionName - The agent * @param ep - Its episode */ private maybeNotify; } //# sourceMappingURL=input-blocked-retry.service.d.ts.map