/**
 * HeartbeatManager — owns the always-on heartbeat lifecycle.
 *
 * Heartbeats are intentionally decoupled from the wake/sleep cycle so the
 * onboarding service sees an accurate liveness signal even during long
 * sleeps (notably the 2-hour overnight interval). The runtime loop is a
 * single recursive `setTimeout` whose cadence adapts to `agentState.busy`:
 *
 *   - busy === true  -> 30s   (preserves dashboard precision during work)
 *   - busy === false -> 5min  (idle keep-alive)
 *
 * Every heartbeat (startup, runtime tick, subscription gate, nudge)
 * funnels through one private `sendHeartbeat()` method that owns the
 * mutex, payload build, and full response handling (subscription,
 * billing, shared keys, credential bundle, pending commands, ack
 * confirm/requeue). This eliminates the previously scattered duplicate
 * response-handling blocks in init/onWake/sendHeartbeatForSubscription.
 *
 * The local on-disk heartbeat file is a separate liveness signal
 * consumed by the in-process child watchdog (see heartbeat-watchdog.ts).
 * It is refreshed at the top of every send, so it stays fresh even when
 * cloud heartbeat credentials are absent.
 */
import type { VisionClawConfig } from "../config/types.js";
import type { ProviderFallbackManager } from "./provider-fallback.js";
import type { ChannelManager } from "../channels/manager.js";
import { type HeartbeatBillingPlan, type SubscriptionStatus } from "../onboarding/onboarding-service-client.js";
import { CommandDispatcher } from "./command-dispatcher.js";
import { type PendingUpgradeInfo } from "../config/index.js";
import type { UsageStatus } from "./usage/usage-report-types.js";
export interface HeartbeatManagerOptions {
    /**
     * Callback that bounces the cloudflared tunnel (typically the
     * `restartTunnel` returned from `startObsServer`). Forwarded verbatim
     * to the tunnel credential handler — HeartbeatManager itself has no
     * tunnel knowledge beyond passing this through.
     *
     * HeartbeatManager is constructed before `agentState` is initialised
     * (the heartbeat fires during `init` while the agent state is still
     * being built), so we take the callback directly rather than reaching
     * into `getAgentState().restartTunnel`.
     */
    restartTunnel?: () => Promise<string | null>;
    obsPort?: number;
}
export declare class HeartbeatManager {
    private client;
    readonly commandDispatcher: CommandDispatcher;
    private readonly config;
    private channelManager;
    private _lastSubscriptionStatus;
    private _lastBillingPlans;
    private _lastBillingMessage;
    private _lastUsageStatus;
    private _lastLlmProviderPriority;
    private _lastCardAvailable;
    /**
     * Owns all server-issued credential orchestration: signature tracking,
     * bundle materialisation, restart coalescing. HeartbeatManager hands
     * it each heartbeat response and reads its `appliedSignature` when
     * building the next payload.
     */
    private readonly credentialBundle;
    private providerFallback;
    /** Recursive setTimeout handle. `null` when the loop is not armed. */
    private runtimeTimer;
    /** Provider for current `agentState.busy`. Set by `startRuntime`. */
    private getBusy;
    /**
     * Mutex tail — every `sendHeartbeat` awaits this then replaces it
     * with the new in-flight promise so concurrent callers serialise.
     * Initialised to a resolved promise so the first send doesn't block.
     */
    private sendMutex;
    private pendingHealthEvents;
    constructor(config: VisionClawConfig, options?: HeartbeatManagerOptions);
    /** Build a base heartbeat payload including owner info for server-side backfill. */
    private buildPayload;
    /**
     * The single place every heartbeat goes through.
     *
     * - Always refreshes the on-disk heartbeat file (cheap, even when no
     *   onboarding-service client is configured).
     * - Mutex-serialises concurrent callers (busy ticks, idle ticks,
     *   subscription gate polls, nudges) so command acks cannot be
     *   double-submitted and response handlers cannot interleave.
     * - In `strict` mode, errors are rethrown after re-queuing acks so
     *   `init()` can surface auth failures and disable the client. In
     *   non-strict mode errors are caught and acks are requeued for the
     *   next attempt, identical to the previous best-effort behaviour.
     * - Does NOT wake the main loop. The `notify` command handler in
     *   loop.ts is responsible for waking after it enqueues the message;
     *   waking from inside `sendHeartbeat` would race that handler.
     */
    private sendHeartbeat;
    /**
     * Body of a single heartbeat send, executed under the mutex. Split
     * out from `sendHeartbeat` so the locking shell is a thin wrapper.
     */
    private runSend;
    /**
     * Initialize the heartbeat client and validate the API key.
     * Should be called once at startup. If validation fails, heartbeats
     * are silently disabled.
     */
    init(channelManager: ChannelManager, telegramChatId: number | undefined, justUpgraded: PendingUpgradeInfo | null): Promise<void>;
    /** Whether a heartbeat client is active. */
    get enabled(): boolean;
    /** The subscription status from the most recent heartbeat response. */
    get lastSubscriptionStatus(): SubscriptionStatus;
    /** Billing plans from the most recent heartbeat response (populated when expired). */
    get lastBillingPlans(): HeartbeatBillingPlan[];
    /** Billing message from the most recent heartbeat response (populated when expired). */
    get lastBillingMessage(): string | undefined;
    get lastUsageStatus(): UsageStatus | undefined;
    get lastLlmProviderPriority(): string[] | undefined;
    /** Runtime-only card availability from the most recent heartbeat response. */
    get lastCardAvailable(): boolean;
    /** Wire the provider fallback manager for heartbeat reporting and priority change detection. */
    setProviderFallback(fb: ProviderFallbackManager): void;
    /**
     * Request a Stripe Billing Portal session.
     * Returns subscription info + portal URL, or null if unavailable.
     */
    getBillingPortal(): Promise<{
        status: string;
        planName: string | null;
        currentPeriodEnd: string | null;
        portalUrl?: string;
    } | null>;
    /**
     * Send a single heartbeat and return the subscription status from the response.
     * Used by the subscription gate for fast-polling (10s interval).
     */
    sendHeartbeatForSubscription(): Promise<SubscriptionStatus>;
    sendHeartbeatForUsageGate(): Promise<UsageStatus | undefined>;
    /**
     * Arm the always-on runtime heartbeat loop.
     *
     * Called once at process startup, after `agentState` is initialised so
     * the `getBusy` provider can read the live busy flag. Does NOT fire
     * an immediate heartbeat — `init()` already sent the strict startup
     * heartbeat, and an immediate runtime tick would just duplicate it.
     * State-change latency is covered by `nudge()`, not by an immediate
     * fire.
     *
     * Calling `startRuntime` while the loop is already armed is a no-op
     * apart from refreshing the `getBusy` reference; this makes the
     * subscription gate's `stop -> poll -> start` pattern safe even if
     * something inside the gate accidentally calls `startRuntime` again.
     */
    startRuntime(getBusy: () => boolean): void;
    /**
     * Stop the runtime heartbeat loop and wait for any in-flight send to
     * finish. Used by graceful shutdown and by the subscription gate
     * before it takes over with its own 10s polling cadence.
     *
     * IMPORTANT: clearing the timer is not enough on its own — a tick
     * that is already mid-send would still re-arm itself when it
     * completes (see `runRuntimeTick`). We null `getBusy` so the
     * `runRuntimeTick` re-arm guard short-circuits, and `nudge()`
     * (which keys off `getBusy`) becomes a no-op until `startRuntime`
     * is called again. `startRuntime` is the only caller that brings
     * `getBusy` back, so the lifecycle is start/stop-symmetric.
     */
    stopRuntime(): Promise<void>;
    /**
     * Fire an out-of-band heartbeat right now via the same mutex path
     * the runtime loop uses. Used by the main loop on busy<->idle
     * transitions to flatten dashboard latency without waiting for the
     * next scheduled tick (up to 5min in the idle case).
     */
    nudge(): void;
    private scheduleNextTick;
    private runRuntimeTick;
    private runDiskCheck;
}
//# sourceMappingURL=heartbeat-manager.d.ts.map