import { z } from "zod";
/**
 * A small registry of the process groups this computer started for or by a
 * dispatched worker. It answers two questions the plain process table cannot:
 *
 * 1. Which agent owns a heavy process? A worker's helpers, a detached server
 *    (`nohup … &`, reparented to PID 1) and a build in a new process group are
 *    matched by the registered process group as well as by ancestry, so the
 *    resources report names the owning agent instead of guessing from a lone
 *    `node`.
 * 2. May a leftover process group be ended? Only a group the Hook registered
 *    and whose owning pane is gone is ever signalled; the Hook's own service,
 *    the owner's sessions and anything unregistered are never touched.
 *
 * A job is recorded when the Hook launches a worker in a pane. The process
 * group is filled in on the next read, because a pane's process does not exist
 * until the multiplexer has started it; matching the pane's shell in the
 * process table resolves it. Records are small and kept briefly.
 */
/** The pane that owns a job, structurally the shape the terminal provider reports. */
export interface JobPane {
    server: string;
    workspace?: string;
    pane: string;
    agent?: string;
    label?: string;
}
/** The fields of a process table row this registry reads; `ProcessRow` satisfies it. */
export interface ProcessLike {
    pid: number;
    ppid: number;
    pgid: number;
    command: string;
    args: string; /** Epoch ms, to about a second. */
    startedAt?: number;
}
declare const jobSchema: z.ZodObject<{
    pgid: z.ZodOptional<z.ZodNumber>;
    leaderStartedAt: z.ZodOptional<z.ZodNumber>;
    pane: z.ZodOptional<z.ZodObject<{
        server: z.ZodString;
        workspace: z.ZodOptional<z.ZodString>;
        pane: z.ZodString;
        agent: z.ZodOptional<z.ZodString>;
        label: z.ZodOptional<z.ZodString>;
    }, z.core.$strict>>;
    session: z.ZodOptional<z.ZodString>;
    agent: z.ZodOptional<z.ZodString>;
    label: z.ZodOptional<z.ZodString>;
    command: z.ZodString;
    startedAt: z.ZodNumber;
}, z.core.$strict>;
export type TrackedJob = z.infer<typeof jobSchema>;
/** The identity of a job: its pane while one exists, else its session, else its group. */
export declare const paneKey: (pane: JobPane) => string;
export declare function jobKey(job: TrackedJob): string;
/** The registered jobs by their process group, for attribution. */
export declare function jobPgids(jobs: readonly TrackedJob[]): Map<number, TrackedJob>;
/**
 * Fill each job's process group from the pane it owns: a job registered before
 * its terminal started has no pgid until the process table shows the pane's
 * shell (or an already-detached process) in a group. Never invents a group.
 */
export declare function resolveJobGroups(jobs: readonly TrackedJob[], rows: readonly ProcessLike[], owners: ReadonlyMap<number, JobPane>): TrackedJob[];
/** How long a job's process group is left alone after its pane was requested,
 * so a launch that is still coming up is never signalled. */
export declare const CLEANUP_LEASE_MS: number;
/** `ps` reports elapsed time in whole seconds, read at slightly different moments. */
export declare const LEADER_START_TOLERANCE_MS = 3000;
/** How long a finished or reused record is kept before it is dropped. */
export declare const CLEANUP_RETAIN_MS: number;
export interface CleanupOptions {
    now?: number;
    leaseMs?: number;
    retainMs?: number;
    /** The Hook's own pid; its process group is never signalled. */
    selfPid?: number;
    /** False when the caller could not read the live panes: nothing is ended. */
    panesKnown?: boolean;
    /** When given, unresolved jobs are matched to their panes before planning. */
    owners?: ReadonlyMap<number, JobPane>;
    /** The terminal servers whose panes are all in `live`. A job on any other
     * server (one that didn't answer, or isn't listed) is never ended: its pane
     * is unknown, not gone. Omitted, nothing is ended. */
    servers?: ReadonlySet<string>;
    kill?: (pgid: number, signal: NodeJS.Signals) => void;
}
export interface CleanupPlan {
    kill: TrackedJob[];
    keep: Array<{
        job: TrackedJob;
        reason: string;
    }>;
    forget: TrackedJob[];
}
/**
 * Decide which registered process groups may be ended. A job is ended only when
 * every one of these holds: it has a resolved group above 1, its owning pane is
 * not in `live`, its group still has processes, none of them is the Hook itself,
 * the lease has passed, and a process still matches the command the Hook
 * started. Anything else is kept (with a reason) or, when the group is gone or
 * the record is old, forgotten. A caller that could not read the live panes
 * passes `panesKnown: false`, so a failed snapshot never triggers a kill.
 */
export declare function planCleanup(jobs: readonly TrackedJob[], rows: readonly ProcessLike[], live: ReadonlySet<string>, options?: CleanupOptions): CleanupPlan;
export interface CleanupReport {
    killed: number[];
    kept: Array<{
        pgid?: number;
        reason: string;
    }>;
    forgotten: number[];
}
/** What the resources report needs from a registry; `JobRegistry` implements it, tests can stub it. */
export interface JobSource {
    list(): Promise<TrackedJob[]>;
    reconcile(rows: readonly ProcessLike[], owners: ReadonlyMap<number, JobPane>): Promise<TrackedJob[]>;
    cleanup(rows: readonly ProcessLike[], live: ReadonlySet<string>, options?: CleanupOptions): Promise<CleanupReport>;
}
/**
 * The on-disk job registry. The Hook is the only writer, so the file is read
 * whole; a damaged file reads as no jobs rather than failing a report.
 */
export declare class JobRegistry {
    private readonly file;
    private readonly now;
    constructor(file?: string, now?: () => number);
    list(): Promise<TrackedJob[]>;
    private replace;
    /** Record (or refresh) one worker's job, keyed by its pane. */
    register(job: Omit<TrackedJob, "startedAt"> & {
        startedAt?: number;
    }): Promise<TrackedJob>;
    /** Resolve process groups against the current table and persist any change. */
    reconcile(rows: readonly ProcessLike[], owners: ReadonlyMap<number, JobPane>): Promise<TrackedJob[]>;
    /**
     * End every registered process group that is safe to end (`planCleanup`) and
     * drop the records that are gone. `live` is the set of pane keys the caller
     * still sees; a caller whose snapshot failed passes `panesKnown: false`, and
     * nothing is ended.
     */
    cleanup(rows: readonly ProcessLike[], live: ReadonlySet<string>, options?: CleanupOptions): Promise<CleanupReport>;
}
export {};
