@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
145 lines (144 loc) • 6.68 kB
TypeScript
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 {};