export type CronScope = 'profile' | 'global'; export type CronOverlap = 'skip' | 'queue' | 'replace'; export type CronOnOutput = 'silent' | 'on-failure' | 'always' | 'on-change'; export type CronState = 'active' | 'paused'; export type CronRunState = 'idle' | 'running'; /** A hydrated `crons` row. */ export interface Cron { cron_id: string; name: string; created_by: string | null; command: string; fire_at: string; recur: string | null; tz: string | null; expires_at: string | null; anchor_node: string | null; cancel_on_wake: boolean; cwd: string; env_json: string | null; profile: string | null; scope: CronScope; run_timeout_s: number; overlap: CronOverlap; on_output: CronOnOutput; /** Sink JSON: `{node}` | `{spawn,kind,...}` | `{human}` (stored raw). */ sink: string; tier: string; last_output_hash: string | null; state: CronState; run_state: CronRunState; run_pid: number | null; /** `capturePidIdentities` fingerprint of `run_pid`, taken at lease * acquisition. NULL on a pre-v23 row or when the capture failed — either * way `killLeasedCronRun` refuses to signal. */ run_pid_identity: string | null; run_started_at: string | null; run_lease_owner: string | null; /** True when the in-flight lease belongs to an OUT-OF-BAND run (`cron run`) * rather than a scheduled fire. The overlap pass excludes such rows: a * manual verification must neither eat a real occurrence (`skip`) nor be * killed by one (`replace`). */ run_out_of_band: boolean; created: string; updated: string; } /** The pinned arm-time spec — every stored column except the runtime lease * fields, which always start idle. */ export interface ArmCronSpec { cron_id: string; name: string; created_by: string | null; command: string; fire_at: string; recur: string | null; tz: string | null; expires_at: string | null; anchor_node: string | null; cancel_on_wake: boolean; cwd: string; env_json: string | null; profile: string | null; scope: CronScope; run_timeout_s: number; overlap: CronOverlap; on_output: CronOnOutput; sink: string; tier: string; } /** One settled `cron_runs` record, written after the subprocess closes. */ export interface CronRunRecord { run_id: string; cron_id: string; started: string; finished: string; duration_ms: number; /** -1 = timeout kill (schema contract); otherwise the observed exit code, * null when no exit was observed (process error). */ exit_code: number | null; stdout_head: string | null; stderr_head: string | null; delivered: string | null; } /** Ring bound for `cron_runs`: recordCronRun prunes each cron's history to * its most recent this-many rows. */ export declare const CRON_RUN_RING = 20; /** The ONLY sanctioned way to signal a cron run's process group from a stored * row. Re-captures the CURRENT identity of `run_pid` and signals only when it * MATCHES the fingerprint recorded at lease acquisition. Returns whether the * signal was sent. * * FAIL-CLOSED, unlike the rest of this repo's pid handling. Elsewhere the * baseline was captured by the same daemon life that is now signaling, so an * absent baseline or a failed `ps` probe means "no evidence of reuse" and * failing open to the signal is right. A cron lease outlives its daemon: after * a SIGKILL or a host reboot, `run_pid` is a number from a dead kernel's pid * space that the OS has very likely handed to an unrelated process, and * `isPidAlive` cheerfully confirms that stranger. So here the ONLY thing that * authorizes a kill is positive proof of sameness — no identity (pre-v23 row, * capture failed), no `ps` table (probe failed), no row for the pid (process * gone), or a mismatch (reused) all mean: do not signal. The caller settles the * lease instead; the worst case is a leaked runaway process, versus SIGTERMing * a stranger's process group. */ export declare function killLeasedCronRun(c: Cron, signal?: NodeJS.Signals): boolean; /** Positive proof that `run_pid` is STILL the process this lease launched: * a stored identity exists, the `ps` probe succeeded, the pid has a row, and * its current identity matches. Anything else — including "we could not tell" * — is `false`, which is what makes `killLeasedCronRun` fail closed. */ export declare function verifyLeasedCronRun(c: Cron): boolean; /** INSERT one cron row from the pinned arm spec, returning its (caller- * supplied) `cron_id` plus the ids of any deadline it displaced. Stamps * `created`/`updated`; lease fields start idle. * * ONE DEADLINE PER ANCHOR: a `cancel_on_wake` arm REPLACES the anchor's * pending cancel-on-wake cron rather than stacking a second one — re-arming a * bounded wait moves the deadline. This is the invariant the dropped * `triggers` partial unique index carried, and `idx_crons_deadline` * (`crons(anchor_node) WHERE cancel_on_wake = 1`) is still its structural * backstop; the displace-then-insert runs inside ONE canvas write boundary so * two concurrent arms cannot both pass. Only the anchor's own cancel-on-wake * rows are displaced — a standing declarative cron on the same anchor is * untouched. Any displaced in-flight run's process group is killed first (the * cancelCron discipline). */ export declare function armCron(spec: ArmCronSpec): { cron_id: string; replaced: string[]; }; /** SELECT one cron by id. */ export declare function getCron(cron_id: string): Cron | null; /** List crons for a scope, next-fire order. `{profile}` = that profile's * profile-scoped rows; `{global:true}` = global-scoped rows only; * `{all:true}` = every cron on the canvas. */ export declare function listCrons(scope: { profile: string; } | { global: true; } | { all: true; }): Cron[]; /** DELETE one cron — the explicit user/surface cancel. Idempotent: an absent * row is a no-op `false`. Kills any in-flight run process group before * deleting, under one canvas write boundary (the reapThenDelete precedent): * lease acquisition linearizes either before this selection (the run is * observed and killed) or after the delete (and loses the lease). The FK * cascade reaps the cron's run history with it. */ export declare function cancelCron(cron_id: string): boolean; /** DELETE one cron by id — one-shot consumption after its run settles. * Returns whether a live row was removed, so a consumer racing an explicit * cancel can no-op instead of double-settling. */ export declare function consumeCron(cron_id: string): boolean; /** UPDATE `state` (active|paused). Returns whether a row changed — pausing an * already-paused cron (or a gone one) reports `false`. Pausing does NOT kill * an in-flight run: the run settles normally, the row just stops being due. */ export declare function setCronState(cron_id: string, state: CronState): boolean; /** UPDATE `last_output_hash` — the on-change disposition's memory of the * previous run's stdout. Written after EVERY settled run of an on-change * cron, delivered or not. */ export declare function setCronLastOutputHash(cron_id: string, hash: string): void; /** True when this node has a pending cancel-on-wake cron. The stop guard uses * this focused deadline check to distinguish legitimate clock dormancy from a * terminal node that stopped without finishing. */ export declare function hasPendingCancelOnWakeCron(anchor_node: string): boolean; /** DELETE every cancel-on-wake cron anchored to this node — the deadline rule. * Two seams call it: reviveNode on every revive-for-any-reason (the wake won * the race), and the `finish`/`cancel` lifecycle transitions (the wait ended * with the node, so its deadline must not fire against a finalized or closed * row). Standing declarative crons are untouched — only cancel-on-wake rows. * Kills any in-flight * run process group first (the cancelCron discipline), under one canvas write * boundary. Returns the removed cron ids. */ export declare function cancelCronsOnWake(anchor_node: string): string[]; /** UPDATE the next fire time — recurrence advancement. The daemon settles a * recurring row to its next slot BEFORE enacting the run (crash-safe). */ export declare function advanceCron(cron_id: string, nextFireIso: string): void; /** The daemon's per-tick due query: active, idle, fire_at arrived, in * deterministic order. The idle-only filter IS the default `overlap='skip'` * behavior — a row whose previous run is still leased is simply not due. * Returns exactly ONE row per cron regardless of missed slots — coalescing * is structural (fire_at holds a single next occurrence). */ export declare function dueClockCrons(nowIso: string): Cron[]; /** Atomically acquire the idle run lease: flips `run_state` to 'running' and * records lease/pid metadata, but ONLY if the row is still idle (the * `WHERE run_state = 'idle'` guard is the atomicity). Returns whether THIS * call acquired it. * * The pid is recorded WITH its launch-time identity fingerprint, captured here * from the just-spawned child: that fingerprint is what later authorizes any * signal against this pid (`killLeasedCronRun`). A failed capture stores NULL, * which costs the row its kill rights rather than licensing a blind kill. * * `outOfBand` stamps the lease as a `cron run` (manual verification) so the * overlap pass leaves it alone — see `dueOverlapCrons`. */ export declare function acquireCronRunLease(cron_id: string, leaseOwner: string, pid: number, startedAtIso: string, outOfBand: boolean): boolean; /** Release the run lease back to idle (run settled, or stale-lease recovery * for a dead pid). */ export declare function releaseCronRunLease(cron_id: string): void; /** Recurring crons whose NEXT fire has arrived while their previous run is * still leased — the overlap-policy pass. One-shots never appear here (a * one-shot has exactly one fire, already consumed by its running lease). * * An OUT-OF-BAND lease (`cron run`) is excluded: overlap policy arbitrates * between a SCHEDULED fire and the scheduled run already in flight. A manual * verification is neither, so letting it participate would either advance * `fire_at` past a genuine occurrence (`skip`) or SIGTERM the invoker's own * run (`replace`). Such a row simply is not due for overlap; the real * occurrence stays past-due and fires the moment the manual lease releases. */ export declare function dueOverlapCrons(nowIso: string): Cron[]; /** Crons whose clock bound (`expires_at`) has passed — the daemon deletes * these each tick. Idle rows only: an expired row whose run is still in * flight is picked up on a later tick, after the run settles. */ export declare function expiredIdleCrons(nowIso: string): Cron[]; /** The run-log ring for one cron, most recent first. */ export declare function listCronRuns(cron_id: string): CronRunRecord[]; /** Every cron currently marked 'running' — the daemon's stale-lease recovery * scan checks each recorded `run_pid` for liveness. */ export declare function runningCrons(): Cron[]; /** INSERT one settled run record, then prune that cron's history down to the * most recent CRON_RUN_RING rows — one canvas write boundary, so the ring * bound holds atomically with the insert. Returns whether the record was * written: a cron canceled while its run was in flight is already gone (and * its history cascaded), so a settling run no-ops with `false` instead of * violating the FK. */ export declare function recordCronRun(run: CronRunRecord): boolean;