UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

145 lines (144 loc) • 6.68 kB
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 {};