{"version":3,"file":"process-stewardship.d.ts","sourceRoot":"","sources":["../../../src/core/governance/process-stewardship.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdir, readFile } from \"node:fs/promises\";\nimport { cpus, freemem, hostname, loadavg, totalmem } from \"node:os\";\nimport type { BackgroundJobOwnership, BackgroundJobRecord, BackgroundJobRegistry } from \"../jobs/index.js\";\n\nexport type ProcessStewardshipClassification =\n\t| \"owned_alive\"\n\t| \"stale_owned\"\n\t| \"unknown_ownership\"\n\t| \"not_owned\"\n\t| \"identity_mismatch\"\n\t| \"terminal\";\n\nexport interface ProcessStewardshipOwner {\n\townerKind: string;\n\townerId: string;\n}\n\nexport interface ProcessStewardshipResult {\n\tjobId: string;\n\tclassification: ProcessStewardshipClassification;\n\trecord: BackgroundJobRecord;\n\treason?: string;\n}\n\nexport interface HostResourceSnapshot {\n\tcapturedAtMs: number;\n\thost: string;\n\tplatform: NodeJS.Platform;\n\tcpuCount: number;\n\tloadAverage: readonly [number, number, number];\n\ttotalMemoryBytes: number;\n\tfreeMemoryBytes: number;\n\tavailableMemoryBytes?: number;\n\tmemoryPressureRatio?: number;\n\tprocessCount?: number;\n\tjensenOwnedProcessCount?: number;\n\tstaleJensenOwnedProcessCount?: number;\n}\n\nexport type HostPressureClassification = \"NORMAL\" | \"ELEVATED\" | \"CRITICAL\" | \"LEAK_SUSPECTED\" | \"UNKNOWN\";\n\nexport interface CleanupResult {\n\taction: \"stopped\" | \"skipped\";\n\treason: string;\n\trecord?: BackgroundJobRecord;\n}\n\nfunction sameOwner(ownership: BackgroundJobOwnership | undefined, owner: ProcessStewardshipOwner): boolean {\n\treturn ownership?.ownerKind === owner.ownerKind && ownership.ownerId === owner.ownerId;\n}\n\n/**\n * Reconcile only jobs belonging to the supplied authority. Missing ownership\n * metadata is deliberately unknown, not implicitly owned by the caller.\n */\nexport async function reconcileOwnedProcesses(\n\tregistry: BackgroundJobRegistry,\n\towner: ProcessStewardshipOwner,\n): Promise<ProcessStewardshipResult[]> {\n\tconst records = await registry.list();\n\tconst results: ProcessStewardshipResult[] = [];\n\tfor (const record of records) {\n\t\tif (!record.ownership) {\n\t\t\tresults.push({ jobId: record.jobId, classification: \"unknown_ownership\", record });\n\t\t\tcontinue;\n\t\t}\n\t\tif (!sameOwner(record.ownership, owner)) {\n\t\t\tresults.push({ jobId: record.jobId, classification: \"not_owned\", record });\n\t\t\tcontinue;\n\t\t}\n\t\tif ([\"stopped\", \"exited\", \"failed\"].includes(record.state)) {\n\t\t\tresults.push({ jobId: record.jobId, classification: \"terminal\", record });\n\t\t\tcontinue;\n\t\t}\n\t\tconst status = await registry.status(record.jobId);\n\t\tif (status?.kind === \"recorded_running_and_alive\") {\n\t\t\tresults.push({ jobId: record.jobId, classification: \"owned_alive\", record: status.record });\n\t\t} else if (status?.kind === \"recorded_running_but_missing\") {\n\t\t\tresults.push({ jobId: record.jobId, classification: \"stale_owned\", record: status.record });\n\t\t} else {\n\t\t\tresults.push({\n\t\t\t\tjobId: record.jobId,\n\t\t\t\tclassification: \"identity_mismatch\",\n\t\t\t\trecord: status?.record ?? record,\n\t\t\t\treason: status?.kind ?? \"status_unavailable\",\n\t\t\t});\n\t\t}\n\t}\n\treturn results;\n}\n\n/**\n * Cleanup seam for a future supervisor. It is intentionally narrow and\n * fail-closed: ownership must match exactly before the registry can stop a job.\n */\nexport async function cleanupOwnedProcess(\n\tregistry: BackgroundJobRegistry,\n\tjobId: string,\n\towner: ProcessStewardshipOwner,\n): Promise<CleanupResult> {\n\tconst record = await registry.read(jobId);\n\tif (!record) return { action: \"skipped\", reason: \"job_not_found\" };\n\tif (!record.ownership) return { action: \"skipped\", reason: \"unknown_ownership\" };\n\tif (!sameOwner(record.ownership, owner)) return { action: \"skipped\", reason: \"not_owned\" };\n\tconst stopped = await registry.stop(jobId);\n\treturn stopped\n\t\t? { action: \"stopped\", reason: \"owned_process_cleanup_requested\", record: stopped }\n\t\t: { action: \"skipped\", reason: \"job_not_found\" };\n}\n\nexport interface HostResourceSnapshotOptions {\n\t/** Linux proc fixture root; defaults to /proc. */\n\tprocRoot?: string;\n\t/** Platform override for deterministic fixture tests. */\n\tplatform?: NodeJS.Platform;\n\t/** Directory reader seam; no process or command execution is performed. */\n\treadDirectory?: (path: string) => Promise<string[]>;\n\t/** UTF-8 file reader seam for deterministic /proc/meminfo fixtures. */\n\treadFile?: (path: string) => Promise<string>;\n}\n\n/** Count numeric Linux /proc entries without invoking ps or another process. */\nexport async function countLinuxProcesses(\n\tprocRoot = \"/proc\",\n\treadDirectory: (path: string) => Promise<string[]> = readdir,\n\tplatform: NodeJS.Platform = process.platform,\n): Promise<number | undefined> {\n\tif (platform !== \"linux\") return undefined;\n\ttry {\n\t\tconst entries = await readDirectory(procRoot);\n\t\treturn entries.filter((entry) => /^\\d+$/u.test(entry)).length;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nasync function availableMemoryBytes(\n\tprocRoot = \"/proc\",\n\treadFileUtf8: (path: string) => Promise<string> = (path) => readFile(path, \"utf8\"),\n\tplatform: NodeJS.Platform = process.platform,\n): Promise<number | undefined> {\n\tif (platform !== \"linux\") return undefined;\n\ttry {\n\t\tconst content = await readFileUtf8(`${procRoot}/meminfo`);\n\t\tconst match = content.match(/^MemAvailable:\\s+(\\d+)\\s+kB$/m);\n\t\treturn match ? Number(match[1]) * 1024 : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Capture host-only resource facts; no process or command execution occurs. */\nexport async function captureHostResourceSnapshot(\n\tnow = Date.now(),\n\toptions: HostResourceSnapshotOptions = {},\n): Promise<HostResourceSnapshot> {\n\tconst platform = options.platform ?? process.platform;\n\tconst totalMemoryBytes = totalmem();\n\tconst freeMemoryBytes = freemem();\n\tconst available = await availableMemoryBytes(options.procRoot, options.readFile, platform);\n\tconst processCount = await countLinuxProcesses(options.procRoot, options.readDirectory, platform);\n\tconst memoryPressureRatio =\n\t\tavailable !== undefined && totalMemoryBytes > 0\n\t\t\t? Math.min(1, Math.max(0, 1 - available / totalMemoryBytes))\n\t\t\t: undefined;\n\tconst averages = loadavg();\n\treturn {\n\t\tcapturedAtMs: now,\n\t\thost: hostname(),\n\t\tplatform,\n\t\tcpuCount: Math.max(1, cpus().length),\n\t\tloadAverage: [averages[0] ?? 0, averages[1] ?? 0, averages[2] ?? 0],\n\t\ttotalMemoryBytes,\n\t\tfreeMemoryBytes,\n\t\tavailableMemoryBytes: available,\n\t\tmemoryPressureRatio,\n\t\tprocessCount,\n\t};\n}\n\n/**\n * Classify only measured memory pressure. Missing telemetry is UNKNOWN; it is\n * never converted into permission to clean up processes.\n */\nexport function classifyHostPressure(\n\tsnapshot: HostResourceSnapshot,\n\tthresholds: { elevatedMemoryPressureRatio?: number; criticalMemoryPressureRatio?: number; leakCount?: number } = {},\n): HostPressureClassification {\n\tconst ratio = snapshot.memoryPressureRatio;\n\tif (ratio === undefined || !Number.isFinite(ratio)) return \"UNKNOWN\";\n\tif ((thresholds.leakCount ?? 1) <= (snapshot.staleJensenOwnedProcessCount ?? 0)) return \"LEAK_SUSPECTED\";\n\tif (ratio >= (thresholds.criticalMemoryPressureRatio ?? 0.95)) return \"CRITICAL\";\n\tif (ratio >= (thresholds.elevatedMemoryPressureRatio ?? 0.8)) return \"ELEVATED\";\n\treturn \"NORMAL\";\n}\n"]}