feat: gate session cleanup by runtime capabilities

This commit is contained in:
Federico Jaramillo Martinez
2026-06-09 11:40:17 +02:00
parent a3b5b722c9
commit fc20b95fed
23 changed files with 528 additions and 38 deletions
+93 -10
View File
@@ -5,8 +5,9 @@ import { homedir } from "node:os";
import { dirname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
import type { PiWebComponentStatus, PiWebInstallationInfo, PiWebReleaseStatus, PiWebServiceComponent, PiWebStatusMessage, PiWebStatusResponse, PiWebVersionResponse } from "../shared/apiTypes.js";
import { parsePiWebComponentStatus } from "../shared/piWebStatusParsing.js";
import type { PiWebCapability, PiWebComponentStatus, PiWebInstallationInfo, PiWebReleaseStatus, PiWebRuntimeComponent, PiWebRuntimeResponse, PiWebServiceComponent, PiWebStatusMessage, PiWebStatusResponse, PiWebVersionResponse } from "../shared/apiTypes.js";
import { effectivePiWebCapabilities, WEB_RUNTIME_CAPABILITIES } from "../shared/capabilities.js";
import { parsePiWebComponentStatus, parsePiWebRuntimeComponent } from "../shared/piWebStatusParsing.js";
import { SessionDaemonClient } from "../sessiond/sessionDaemonClient.js";
const PI_WEB_PACKAGE_NAME = "@jmfederico/pi-web";
@@ -61,10 +62,35 @@ interface PackageInfo {
path: string;
}
interface PiWebStatusDaemon {
request(method: string, path: string, body?: unknown): Promise<{ statusCode: number; headers: Record<string, string>; body: string }>;
}
let latestReleaseCache: { checkedAtMs: number; latestVersion?: string; error?: string } | undefined;
const runtimePackageInfo = readPackageInfoSync();
export function getPiWebRuntimeComponent(component: PiWebServiceComponent, capabilities: readonly PiWebCapability[] = []): PiWebRuntimeComponent {
return {
component,
label: component === "web" ? "Web/UI" : "Session daemon",
runtimeVersion: runtimePackageInfo?.version ?? DEFAULT_VERSION,
available: true,
capabilities: [...capabilities],
};
}
export async function getPiWebRuntime(daemon: PiWebStatusDaemon = new SessionDaemonClient()): Promise<PiWebRuntimeResponse> {
const web = getPiWebRuntimeComponent("web", WEB_RUNTIME_CAPABILITIES);
const sessiond = await getSessiondRuntimeComponent(daemon);
return {
packageName: PI_WEB_PACKAGE_NAME,
generatedAt: new Date().toISOString(),
components: { web, sessiond },
capabilities: effectivePiWebCapabilities({ web, sessiond }),
};
}
export async function getPiWebComponentStatus(component: PiWebServiceComponent): Promise<PiWebComponentStatus> {
const [installed, installation] = await Promise.all([
readInstalledPackageInfo(),
@@ -83,7 +109,7 @@ export async function getPiWebComponentStatus(component: PiWebServiceComponent):
};
}
export async function getPiWebVersionStatus(daemon = new SessionDaemonClient()): Promise<PiWebVersionResponse> {
export async function getPiWebVersionStatus(daemon: PiWebStatusDaemon = new SessionDaemonClient()): Promise<PiWebVersionResponse> {
const [web, sessiond] = await Promise.all([
getPiWebComponentStatus("web"),
getSessiondComponentStatus(daemon),
@@ -95,7 +121,7 @@ export async function getPiWebVersionStatus(daemon = new SessionDaemonClient()):
};
}
export async function getPiWebStatus(daemon = new SessionDaemonClient()): Promise<PiWebStatusResponse> {
export async function getPiWebStatus(daemon: PiWebStatusDaemon = new SessionDaemonClient()): Promise<PiWebStatusResponse> {
const versionStatus = await getPiWebVersionStatus(daemon);
const { web, sessiond } = versionStatus.components;
const release = await getLatestReleaseStatus(web.installedVersion ?? web.runtimeVersion ?? DEFAULT_VERSION);
@@ -214,21 +240,78 @@ function isSameOrWithin(parent: string, candidate: string): boolean {
return rel === "" || (!rel.startsWith("..") && !rel.startsWith(sep));
}
async function getSessiondComponentStatus(daemon: SessionDaemonClient): Promise<PiWebComponentStatus> {
async function getSessiondRuntimeComponent(daemon: PiWebStatusDaemon): Promise<PiWebRuntimeComponent> {
try {
const upstream = await daemon.request("GET", "/health");
const upstream = await daemon.request("GET", "/runtime");
if (upstream.statusCode < 200 || upstream.statusCode >= 300) {
return unavailableSessiond(`health check returned HTTP ${String(upstream.statusCode)}`);
return await legacySessiondRuntimeComponent(daemon) ?? unavailableSessiondRuntime(`runtime check returned HTTP ${String(upstream.statusCode)}`);
}
const parsed: unknown = upstream.body === "" ? undefined : JSON.parse(upstream.body);
const version = isRecord(parsed) ? parsed["version"] : undefined;
const component = parsePiWebComponentStatus(version);
return component ?? unavailableSessiond("health response did not include version information");
const runtime = parsePiWebRuntimeComponent(parsed);
if (runtime !== undefined) return runtime;
const legacyVersion = isRecord(parsed) ? parsePiWebComponentStatus(parsed["version"]) : undefined;
if (legacyVersion !== undefined) return runtimeComponentFromStatus(legacyVersion);
return await legacySessiondRuntimeComponent(daemon) ?? unavailableSessiondRuntime("runtime response did not include valid runtime information");
} catch (error) {
return unavailableSessiondRuntime(error instanceof Error ? error.message : String(error));
}
}
async function getSessiondComponentStatus(daemon: PiWebStatusDaemon): Promise<PiWebComponentStatus> {
try {
const upstream = await daemon.request("GET", "/runtime");
if (upstream.statusCode < 200 || upstream.statusCode >= 300) {
return await legacySessiondComponentStatus(daemon) ?? unavailableSessiond(`runtime check returned HTTP ${String(upstream.statusCode)}`);
}
const parsed: unknown = upstream.body === "" ? undefined : JSON.parse(upstream.body);
const legacyVersion = isRecord(parsed) ? parsePiWebComponentStatus(parsed["version"]) : undefined;
if (legacyVersion !== undefined) return legacyVersion;
const runtime = parsePiWebRuntimeComponent(parsed);
if (runtime?.available !== true) return await legacySessiondComponentStatus(daemon) ?? unavailableSessiond(runtime?.error ?? "runtime response did not include valid runtime information");
const status = await getPiWebComponentStatus("sessiond");
return { ...status, ...(runtime.runtimeVersion === undefined ? {} : { runtimeVersion: runtime.runtimeVersion }), available: true };
} catch (error) {
return unavailableSessiond(error instanceof Error ? error.message : String(error));
}
}
async function legacySessiondRuntimeComponent(daemon: PiWebStatusDaemon): Promise<PiWebRuntimeComponent | undefined> {
const status = await legacySessiondComponentStatus(daemon);
return status === undefined ? undefined : runtimeComponentFromStatus(status);
}
async function legacySessiondComponentStatus(daemon: PiWebStatusDaemon): Promise<PiWebComponentStatus | undefined> {
try {
const upstream = await daemon.request("GET", "/health");
if (upstream.statusCode < 200 || upstream.statusCode >= 300) return undefined;
const parsed: unknown = upstream.body === "" ? undefined : JSON.parse(upstream.body);
return isRecord(parsed) ? parsePiWebComponentStatus(parsed["version"]) : undefined;
} catch {
return undefined;
}
}
function runtimeComponentFromStatus(status: PiWebComponentStatus): PiWebRuntimeComponent {
return {
component: status.component,
label: status.label,
...(status.runtimeVersion === undefined ? {} : { runtimeVersion: status.runtimeVersion }),
available: status.available,
capabilities: [],
...(status.error === undefined ? {} : { error: status.error }),
};
}
function unavailableSessiondRuntime(error: string): PiWebRuntimeComponent {
return {
component: "sessiond",
label: "Session daemon",
available: false,
capabilities: [],
error,
};
}
function unavailableSessiond(error: string): PiWebComponentStatus {
return {
component: "sessiond",