import type { MonitorConfig, MonitorInterrupt, NotifyEventType } from './config.js'; import { type FailureEvidence } from './model-recovery.js'; /** A content-free arrival event as the daemon serves it over the notifications API. */ export interface NotifyEvent { event?: NotifyEventType | string; from?: string; msg_id?: number | string; file_id?: number | string; date?: string; queued?: number | string; } export interface FetchResponse { status: number; ok: boolean; json(): Promise<{ cursor?: number; events?: NotifyEvent[]; identities?: Array; }>; } export type FetchLike = (url: string, init?: { headers?: Record; signal?: AbortSignal; }) => Promise; export interface MonitorDeps { fetch: FetchLike; isAlive(pid: number): boolean; sleep(ms: number): Promise; now(): number; log(line: string): void; env: NodeJS.ProcessEnv; timers: { set(fn: () => void, ms: number): ReturnType; clear(t: ReturnType): void; }; /** * Structured prompt delivery used by agent sessions. * `succeeded` is the turn's TERMINAL result, not merely that the session took * the prompt: a refused or cancelled wake was seen and not acted on, and must * not commit the cursor. */ delivery?: { submit(text: string, options?: { interrupt?: MonitorInterrupt; }): Promise<{ succeeded: boolean; outcome: string; detail?: string; safeBoundary?: 'direct' | 'after_tool' | 'timeout' | 'unsupported'; }>; }; /** Body-free, typed evidence for runner-owned model recovery. */ onFailureEvidence?(evidence: FailureEvidence): boolean; } /** * Why the monitor is not healthy. Each cause clears on its OWN recovery signal * and nothing else — a successful poll proves the stream works, and proves * nothing whatsoever about whether wakes are being delivered or whether the * turns they trigger keep dying. */ export type StatusCause = 'connectivity' | 'delivery' | 'modal' | 'offline' | 'turns-failing' | 'safe-boundary' | 'auth'; /** Best-effort daemon config (issue #17): the fields the MCP client reads. */ interface DaemonConfig { apiToken?: string; port?: number; stateDir?: string; } /** * Read the daemon config the way the MCP client does — best-effort. Any missing, * malformed, or unreadable config yields `{}` so token resolution falls through * (issue #17). Only the well-typed fields we consume are surfaced. */ export declare function readDaemonConfig(env: NodeJS.ProcessEnv): DaemonConfig; /** * Resolve the daemon API token exactly like the MCP client (issue #17), a 3-step * chain: `OURS_API_TOKEN` (trimmed) → config `apiToken` (trimmed) → the 0600 owner * token at `/daemon-token`. Never generates a token; a failed read of * any source (missing/unreadable) silently falls through to the next. */ export declare function resolveApiToken(env: NodeJS.ProcessEnv, file?: DaemonConfig): string | undefined; export interface DaemonEndpoint { origin: string; port: number; configPath: string; stateDir: string; url(name: string): string; headers: Record; } export type IdentityPresence = { state: 'present'; temporary: boolean; stale: boolean; } | { state: 'absent'; } | { state: 'unknown'; detail: string; }; /** Resolve the daemon endpoint + auth header from env → config → defaults. */ export declare function resolveEndpoint(env: NodeJS.ProcessEnv): DaemonEndpoint; /** * Ask the daemon's authoritative identity index. The notifications endpoint is * intentionally unsuitable for lifecycle: it serves an empty 200 page for a * valid but missing identity, which made a closed temp identity look healthy. */ export declare function probeIdentityPresence(name: string, fetch: FetchLike, env: NodeJS.ProcessEnv): Promise; /** Actionable, secret-free description of every token source for this profile. */ export declare function authResolutionHint(ep: DaemonEndpoint): string; /** Keep only the events whose type the role asked to wake on. */ export declare function filterEvents(events: NotifyEvent[], wakeSources: string[]): NotifyEvent[]; /** * Summarize a (coalesced) batch of events into one content-free console line — * count + senders + ids, ending in the call to action. Falls back to compact * counts when a burst would exceed the length cap. */ export declare function formatNotificationLine(events: NotifyEvent[]): string; /** * Heuristic: does the pane show a modal selection dialog we must not `Enter` * into? Two independent signals, both requiring the *option* shape, not just a * loose numbered line: * * 1. the `❯` pointer sitting on a numbered option — `❯ 1. Use this MCP server`; * 2. a dialog marker ("Do you want …", "Enter to confirm") with ≥2 numbered * options within `OPTION_WINDOW` lines — this still catches a dialog captured * mid-redraw, before its pointer row is painted. * * A running turn, a prose list, and a markdown step list are all NOT modal. * Erring modal is the safe direction (a wake is retried; an `Enter` into a live * permission dialog is not undoable), which is why signal 2 is kept — but a bare * marker with no options no longer suffices, because Claude Code closes turns * with exactly that prose ("Do you want me to open the PR?"). */ export declare function looksModal(pane: string): boolean; /** * Heuristic: did the turn shown in this pane TERMINATE in an API-level error? * Claude Code renders a failed turn's tail as an `API Error:` line (a Usage-Policy * refusal, a 4xx, etc.). We scan a generous tail window so the marker survives a * trailing idle composer redrawn beneath it; refine empirically. * The N-consecutive threshold in the Monitor debounces the odd false match. */ export declare function looksApiError(pane: string): boolean; /** * Heuristic: is a turn still RUNNING in this pane? Claude Code shows a live * "esc to interrupt" footer (often with an elapsed-seconds meter) while a turn * streams. Absence of any running marker — and no API error — means the turn has * settled (completed). Kept a positive check so a quiet idle pane reads as done. */ export declare function looksRunning(pane: string): boolean; export interface MonitorOpts { name: string; /** Ours identity whose notification stream is authoritative (may differ from role name). */ identity?: string; agentDir: string; cfg: MonitorConfig; deps: MonitorDeps; } /** The lifecycle surface the runner drives: prime pre-launch, run, stop on pid death. */ export interface MonitorHandle { prime(options?: { resetCursor?: boolean; }): Promise; run(pid: number): Promise; stop(): void; } export declare class Monitor { private readonly name; private readonly identity; private readonly cfg; private readonly deps; private readonly ep; private readonly statusPath; private readonly cursorPath; private readonly statePath; private cursor; private deliveredCursor; private pendingState; private fatal; private stopped; private bootDeadline; private currentAbort; private apiErrorStreak; private readonly turnFailThreshold; /** Active degradations, keyed by cause. Empty means armed. */ private readonly causes; constructor(o: MonitorOpts); /** * Resume the last delivered cursor during ordinary fleet-owned restarts. * A native→fleet ownership transition resets at stream tip because the native * owner was responsible for arrivals while the supervisor was inactive. */ prime(options?: { resetCursor?: boolean; }): Promise; /** Long-poll → filter → coalesce → inject, until the pane pid dies or stop(). */ run(pid: number): Promise; stop(): void; /** Gather stragglers arriving within batch_ms so a burst lands as one line. */ private coalesce; private deliver; /** Update the consecutive-API-error streak and derive `.monitor-status` from it. */ private recordTurn; /** * Reset the composer to empty before typing a wake. Without this, any * unsubmitted text a human left in the input concatenates with the injected * line (`stray[fleet-monitor] …`) or, when it has opened a slash-command menu, * swallows the submit Enter entirely — wedging every subsequent injection until * the composer is cleared by hand. Best-effort: a dead pane just makes the keys * no-ops (delivery is still verified downstream). */ private doFetch; private advance; private persistCursor; private readPersistedCursor; /** Atomically persist body-free delivery state; restart always resumes from deliveredCursor. */ private persistState; /** Record a degradation under its own cause and republish the status. */ private degrade; /** * Clear exactly the causes this recovery signal speaks to. Anything else * stays: one successful poll must never be able to erase `turns failing`. */ private recover; /** * One line per active cause, each dated; `armed` when there are none. Every * line carries an ISO timestamp so an operator can tell a live status from a * stale one left behind by a monitor that stopped writing. */ private writeStatus; } export declare function createMonitor(o: MonitorOpts): Monitor; export {};