/** * `outposts heartbeat` — publish this box's agent-session summary to the realtime * fabric. * * Runs ON an Outpost, under systemd. It enumerates the box's local Claude Code and * Codex sessions with cheap scandir + stat + bounded reads, projects them down to * the compact secret-free `AgentSession[]` payload, and publishes to * `hq/{personUid}/sessions` so Mission Control can see what the box is doing. * * ## Why the cadence lives in here * * The original box-side design was a bash `while` loop that, every 5 seconds, ran * `hq auth refresh` and then `npx -y --package=@indigoai-us/hq-cloud@latest * outpost-session-heartbeat-runner`. That is ~35k process launches a day, and every * `npx` re-resolves the package and re-reads it off disk — on a t3 box whose EBS * budget is the scarce resource, that is a meaningful, permanent tax for a job that * should be nearly free. * * So the loop lives in-process instead: * - identity is resolved ONCE per process, not once per tick; * - the Cognito session refreshes itself on demand (the injected `getJwt` * returns the cached token until it is close to expiry), so there is no * separate per-tick `hq auth refresh`; * - the IoT client is cached across ticks by the publisher and only rebuilt when * its credentials near expiry. * * The loop is deliberately unkillable-by-transients: a failed publish, a failed * enumeration, or a not-yet-warm session are all logged and retried on the next * tick. Nothing here should ever take the service down, because a service that * exits looks identical to a service that is quietly failing. */ import type { Command } from "commander"; import { type FileSystemPort, type PublishPort, type SessionsHeartbeatPayload } from "./session-heartbeat.js"; /** Everything the loop touches, injected so it is testable without a box. */ export interface HeartbeatLoopDeps { fs: FileSystemPort; publish: PublishPort; /** Resolve the caller's canonical `prs_*` id. Called once per process. */ getPersonUid: () => Promise; /** * Wait between ticks. MUST return early when `signal` aborts — otherwise a * SIGTERM arriving mid-sleep is not noticed until the full interval elapses, and * systemd force-kills the process instead of it stopping cooperatively. */ sleep: (ms: number, signal?: AbortSignal) => Promise; now: () => Date; log: (line: string) => void; /** * Record that a heartbeat actually LANDED. Called only after a successful * publish — never on failure, or the freshness signal it feeds would always read * fresh and be worth nothing. */ onPublished?: (payload: SessionsHeartbeatPayload) => Promise; } export interface HeartbeatLoopOptions { /** Emit a single heartbeat and return. */ once?: boolean; /** Cadence between ticks. Defaults to {@link DEFAULT_HEARTBEAT_INTERVAL_SECONDS}. */ intervalSeconds?: number; /** Home directory to scan (defaults to the process HOME). */ home?: string; /** Print each published payload as JSON. */ json?: boolean; /** Stop the loop cooperatively. */ signal?: AbortSignal; /** Bound the loop — used by tests; production runs until aborted. */ maxTicks?: number; } /** What the heartbeat subcommand needs from its host CLI. */ export interface HeartbeatCommandDeps { /** Resolve a Cognito JWT non-interactively. Called per credential refresh. */ getJwt: () => Promise; /** Resolve the caller's canonical `prs_*` uid against `apiBaseUrl`. */ resolveCallerPersonUid: (token: string, apiBaseUrl?: string) => Promise; /** Control-plane origin used when neither the flag nor `HQAPI_BASE_URL` is set. */ defaultApiBaseUrl: string; } /** * Resolve the cadence from the flag, then the environment, then the default. * * Floored at 1s: a sub-second cadence hammers the fabric for no benefit, and a * zero/negative value would spin the loop hot. */ export declare function resolveIntervalSeconds(flag: string | undefined, env?: NodeJS.ProcessEnv): number; /** * Where the box records its last LANDED heartbeat. The post-provision audit reads * this file's freshness, because "systemd says the unit is active" is not evidence * that anything was published — that gap is what let a heartbeat fail every five * seconds for nine days while every dashboard stayed green. * * Lives under the service user's home so the unit (User=ec2-user) can write it * without extra tmpfiles.d wiring; the SSM collector reads it as root. */ export declare const DEFAULT_HEARTBEAT_STATE_FILE: string; /** Persist the liveness marker the box audit reads. */ export declare function writeHeartbeatState(path: string, payload: SessionsHeartbeatPayload): Promise; /** * Run the heartbeat loop. Returns the number of ticks performed. * * A "tick" is one attempt, whether or not it published — the count is the loop's * liveness, not its success rate. */ export declare function runHeartbeatLoop(options: HeartbeatLoopOptions, deps: HeartbeatLoopDeps): Promise; /** Wire the production dependencies and register the subcommand. */ export declare function registerHeartbeatCommand(outposts: Command, hostDeps: HeartbeatCommandDeps): void; //# sourceMappingURL=heartbeat-command.d.ts.map