/** * `gjc gc` runtime — a global, liveness-only, dry-run-by-default garbage * collector for stale GJC session/PID records. * * Design (see .gjc/plans/ralplan/2026-06-13-1347-954f/pending-approval.md): * - This module is an ORCHESTRATOR only. It owns the shared PID probe, the * report/exit-code policy, and text/JSON rendering. It must NOT parse private * store layouts directly; every store is reached through an injectable * `GcStoreAdapter` that lives next to its store owner. * - Liveness-only and fail-closed: only `ESRCH` (no such process) is `dead` * (removable). `process.kill(pid, 0)` success, `EPERM`, and any unknown probe * error all mean KEEP — a live process is never signalled or killed. * - Dry-run by default: nothing is deleted unless `--prune`/`--force`. */ import { type GcSessionScopeUsage } from "./gc-session-scope"; export type GcStore = "harness_leases" | "file_locks" | "tmux_sessions" | "registry_entries" | "local_roots"; export declare const GC_STORES: readonly GcStore[]; /** Why a probed pid is kept instead of treated as dead. */ export type GcPidKeepReason = "alive" | "eperm" | "unknown"; export interface GcPidProbeResult { /** `dead` only on ESRCH; `keep` for alive/eperm/unknown (fail-closed). */ status: "dead" | "keep"; reason?: GcPidKeepReason; error?: string; } /** Single shared liveness contract threaded through every classifier + prune path. */ export type GcPidProbe = (pid: number) => GcPidProbeResult; export type GcPidStatus = "dead" | "alive" | "eperm" | "unknown" | "none"; export type GcAction = "none" | "would_remove" | "removed" | "remove_failed" | "skipped"; export interface GcRecord { store: GcStore; /** Stable identifier: session id, lock dir path, worker id, tmux name, registry session id. */ id: string; path?: string; root?: string; pid?: number; pid_status?: GcPidStatus; /** Store-specific classification label (e.g. "dead", "live", "unclassified", "terminal_lifecycle"). */ status: string; stale: boolean; removable: boolean; action: GcAction; reason: string; detail?: string; error?: string; removed?: boolean; } export interface GcError { store: GcStore; scope: string; message: string; } /** Non-fatal discovery partials (e.g. traversal caps). Does not affect exit code. */ export interface GcWarning { store: GcStore; scope: string; message: string; } export interface GcCollectResult { records: GcRecord[]; errors: GcError[]; /** Optional partial-result notices; omitted by adapters that have none. */ warnings?: GcWarning[]; } export interface GcPruneOutcome { removed: boolean; error?: string; /** Set when a removable record was skipped at prune time (e.g. TOCTOU became live). */ skipped?: string; } export interface GcContext { probe: GcPidProbe; force: boolean; env: NodeJS.ProcessEnv; cwd: string; } /** * A store-owned GC adapter. `collect` discovers + classifies (using the shared * probe) without mutating anything. `prune` removes a single record, and MUST * re-validate / re-probe immediately before any destructive action. */ export interface GcStoreAdapter { store: GcStore; collect(ctx: GcContext): Promise; prune(record: GcRecord, ctx: GcContext): Promise; } export interface GcCounts { discovered: number; stale: number; alive: number; eperm: number; unknown: number; terminal_lifecycle: number; unclassified: number; would_remove: number; removed: number; failed: number; errors: number; by_store: Record; } export interface GcSessionIndexHealth { status: "healthy" | "corrupt" | "repaired" | "unsupported" | "repair_failed"; valid_prefix_seq: number; snapshot_seq?: number; reason?: string; quarantine_path?: string; } export interface GcReport { dry_run: boolean; operation?: "dry_run" | "prune" | "repair_session_index"; stores: Record; counts: GcCounts; errors: GcError[]; /** Partial-result notices that do not fail the run (e.g. walk caps). */ warnings: GcWarning[]; session_index?: GcSessionIndexHealth; /** Managed-scope capacity, reported only when it is near or past the budget. */ session_scope?: GcSessionScopeUsage; /** * Disk-retention findings. Present only when `--disk` was passed; the * PID-liveness axis above is unchanged by its absence or presence. */ disk?: GcDiskReport; } export interface GcRunResult { stdout: string; stderr: string; status: number; } /** * The shared, fail-closed PID probe. ESRCH => dead/removable; success => alive; * EPERM => kept (owned by another user); any other error => kept as unknown. */ export declare const gcPidProbe: GcPidProbe; /** Map a `GcPidProbe` onto the harness lease probe shape (`"alive"|"dead"|"eperm"`). */ export declare function gcProbeToLeasePidStatus(probe: GcPidProbe): (pid: number) => "alive" | "dead" | "eperm"; /** Translate a probe result into a record-friendly pid status label. */ export declare function gcPidStatusLabel(result: GcPidProbeResult): Exclude; /** * Collect every store's records (catching hard discovery errors per adapter), * then optionally prune removable records with per-record revalidation. */ export declare function collectGcReport(adapters: GcStoreAdapter[], ctx: GcContext, prune: boolean): Promise; /** * Exit-code policy: * - usage/parse error => 2 * - hard discovery errors => 1 (both modes) * - prune mode with a failed intended removal => 1 * - warnings alone never fail the run * - otherwise => 0 * * The disk axis reuses the same policy against its own errors/failures, and is * inert when `--disk` was not passed (`report.disk` is then undefined). A * fail-closed KEEP is never a failure — only a hard scan error or a reclaim * that was attempted and threw. */ export declare function computeExitCode(report: GcReport): number; export declare function runGjcGcCommand(argv: string[], cwd?: string, env?: NodeJS.ProcessEnv, adapters?: GcStoreAdapter[], diskPolicy?: Partial): Promise; export declare function gcHelpText(): string; /** Lazily assemble the real store adapters (kept lazy to avoid import cycles). */ export declare function defaultGcAdapters(): Promise; /** On-disk surfaces the retention axis can reclaim. */ export type GcDiskSurface = "sessions" | "blobs" | "artifacts" | "natives" | "backups"; export declare const GC_DISK_SURFACES: readonly GcDiskSurface[]; export type GcDiskAction = "keep" | "would_reclaim" | "reclaimed" | "reclaim_failed"; export interface GcDiskRecord { surface: GcDiskSurface; /** Session id, blob hash, `/` artifact, natives version, or backup entry name. */ id: string; path: string; bytes: number; age_days: number; action: GcDiskAction; reason: string; error?: string; /** Set when `bytes` is a floor because a walk was capped or partially unreadable. */ partial?: true; /** Set when this entry was a reclaim candidate that its surface withheld on incomplete evidence. */ withheld?: true; } /** * Per-family rollup of a surface whose growth is many small files. * * The artifacts surface writes one record per file, which answers "what may go" * but not "what filled the scope". A family is derived from the filename shape * (`*.bash.log`, `.artifact-id-*`, …) rather than a fixed list, so a tool that * starts writing a new kind of log is counted the day it ships. */ export interface GcDiskFamilyUsage { family: string; count: number; bytes: number; } /** * Why a surface withheld reclaim candidates because its evidence was incomplete. */ export interface GcDiskDeclined { reason: string; withheld: number; withheld_bytes: number; } export interface GcDiskSurfaceReport { surface: GcDiskSurface; root: string; scanned: number; scanned_bytes: number; reclaimable: number; reclaimable_bytes: number; reclaimed: number; reclaimed_bytes: number; kept: number; kept_bytes: number; failed: number; /** Set when the surface withheld reclaim candidates because its evidence was incomplete. */ declined?: GcDiskDeclined; /** Per-family counts and bytes. Only surfaces whose records are individual files set this. */ families?: GcDiskFamilyUsage[]; records: GcDiskRecord[]; } export interface GcDiskError { surface: GcDiskSurface; scope: string; message: string; } /** Retention policy, mirroring the `gc.*` settings one-for-one. */ export interface GcDiskPolicy { sessions_max_age_days: number; /** 0 disables the size axis; only the age axis retires transcripts then. */ sessions_max_total_bytes: number; natives_keep_versions: number; backups_max_age_days: number; } export interface GcDiskReport { dry_run: boolean; policy: GcDiskPolicy; surfaces: Record; totals: { scanned_bytes: number; reclaimable_bytes: number; reclaimed_bytes: number; kept_bytes: number; failed: number; }; errors: GcDiskError[]; } /** Schema-backed defaults, so the CLI and the settings surface cannot drift. */ export declare const GC_DISK_POLICY_DEFAULTS: GcDiskPolicy; export declare function resolveGcDiskPolicy(overrides?: Partial): GcDiskPolicy; /** * Run the disk-retention axis. Nothing is mutated unless `prune` is true; the * dry-run report projects exactly the same decisions a prune would make. */ export declare function collectGcDiskReport(input: { agentDir: string; env: NodeJS.ProcessEnv; policy: GcDiskPolicy; prune: boolean; now?: number; runningVersion?: string; }): Promise; export declare function buildGcDiskReportText(disk: GcDiskReport): string;