fix: scope process leak snapshots
This commit is contained in:
parent
d626f7b5e5
commit
6468a077b7
@ -233,6 +233,7 @@ export function captureProcessSnapshot(options = {}) {
|
||||
const gatewayPid = envName ? resolveGatewayPid(envName) : null;
|
||||
const allProcesses = listProcesses();
|
||||
const gatewayTreePids = gatewayPid === null ? new Set() : collectProcessTreePids(allProcesses, gatewayPid);
|
||||
const scopeTokens = snapshotScopeTokens(options);
|
||||
const included = [];
|
||||
|
||||
for (const process of allProcesses) {
|
||||
@ -243,7 +244,11 @@ export function captureProcessSnapshot(options = {}) {
|
||||
if (gatewayTreePids.has(process.pid)) {
|
||||
roles.add("gateway-tree");
|
||||
}
|
||||
for (const role of matchingRegistryProcessRoles(process, roleMatchers)) {
|
||||
for (const role of matchingSnapshotRegistryRoles(process, roleMatchers, {
|
||||
allowGlobalProcessRoleMatches: options.allowGlobalProcessRoleMatches === true,
|
||||
existingRoles: roles,
|
||||
scopeTokens
|
||||
})) {
|
||||
roles.add(role);
|
||||
}
|
||||
if (roles.size === 0) {
|
||||
@ -297,6 +302,20 @@ export function classifyRegistryRolesForProcess(process, options = {}) {
|
||||
return matchingRegistryRoles(process, options.rootCommand, roleMatchers, existingRoles);
|
||||
}
|
||||
|
||||
export function classifySnapshotRolesForProcess(process, options = {}) {
|
||||
const roleMatchers = compileRoleMatchers(options.processRoles ?? []);
|
||||
const existingRoles = new Set(options.existingRoles ?? []);
|
||||
const roles = new Set(existingRoles);
|
||||
for (const role of matchingSnapshotRegistryRoles(process, roleMatchers, {
|
||||
allowGlobalProcessRoleMatches: options.allowGlobalProcessRoleMatches === true,
|
||||
existingRoles,
|
||||
scopeTokens: snapshotScopeTokens(options)
|
||||
})) {
|
||||
roles.add(role);
|
||||
}
|
||||
return [...roles].sort();
|
||||
}
|
||||
|
||||
function compileRoleMatchers(roles) {
|
||||
return roles.map((role) => ({
|
||||
id: role.id,
|
||||
@ -345,6 +364,30 @@ function matchingRegistryProcessRoles(process, roleMatchers) {
|
||||
return roles;
|
||||
}
|
||||
|
||||
function matchingSnapshotRegistryRoles(process, roleMatchers, options = {}) {
|
||||
const existingRoles = options.existingRoles ?? new Set();
|
||||
if (existingRoles.size === 0 && options.allowGlobalProcessRoleMatches !== true && !processMatchesSnapshotScope(process, options.scopeTokens ?? [])) {
|
||||
return [];
|
||||
}
|
||||
return matchingRegistryProcessRoles(process, roleMatchers);
|
||||
}
|
||||
|
||||
function processMatchesSnapshotScope(process, scopeTokens) {
|
||||
const command = String(process?.command ?? "");
|
||||
return scopeTokens.some((token) => token.length > 0 && command.includes(token));
|
||||
}
|
||||
|
||||
function snapshotScopeTokens(options = {}) {
|
||||
const tokens = new Set();
|
||||
if (typeof options.envName === "string" && options.envName.trim().length > 0) {
|
||||
tokens.add(options.envName.trim());
|
||||
}
|
||||
for (const token of String(options.rootCommand ?? "").match(/\bkova-[A-Za-z0-9_.-]+\b/g) ?? []) {
|
||||
tokens.add(token);
|
||||
}
|
||||
return [...tokens].filter((token) => token.length >= 4);
|
||||
}
|
||||
|
||||
function summarizeRoleCounts(processes) {
|
||||
const counts = new Map();
|
||||
for (const process of processes) {
|
||||
|
||||
@ -41,7 +41,7 @@ import {
|
||||
parseProviderRequestLog,
|
||||
parseTimelineProviderRequestLog
|
||||
} from "./collectors/provider.mjs";
|
||||
import { captureProcessSnapshot, classifyRegistryRolesForProcess, diffProcessSnapshots } from "./collectors/resources.mjs";
|
||||
import { captureProcessSnapshot, classifyRegistryRolesForProcess, classifySnapshotRolesForProcess, diffProcessSnapshots } from "./collectors/resources.mjs";
|
||||
import { renderMarkdownReport, renderPasteSummary, renderReportSummary } from "./reporting/report.mjs";
|
||||
import { compareReports, renderCompareSummary } from "./reporting/compare.mjs";
|
||||
import {
|
||||
@ -5202,19 +5202,22 @@ async function resourceRolePollutionCheck() {
|
||||
}
|
||||
|
||||
async function processSnapshotCheck(tmp) {
|
||||
const processRoles = await loadProcessRoles();
|
||||
const child = runCommand("node -e 'setTimeout(() => {}, 1200)'", {
|
||||
timeoutMs: 5000,
|
||||
resourceSample: null
|
||||
});
|
||||
await sleep(250);
|
||||
const before = captureProcessSnapshot({
|
||||
processRoles: await loadProcessRoles(),
|
||||
rootCommand: "ocm @kova -- agent --local --message hi"
|
||||
processRoles,
|
||||
envName: "kova-self-check",
|
||||
rootCommand: "ocm @kova-self-check -- agent --local --session-id kova-agent-self-check --message hi"
|
||||
});
|
||||
const result = await child;
|
||||
const after = captureProcessSnapshot({
|
||||
processRoles: await loadProcessRoles(),
|
||||
rootCommand: "ocm @kova -- agent --local --message hi"
|
||||
processRoles,
|
||||
envName: "kova-self-check",
|
||||
rootCommand: "ocm @kova-self-check -- agent --local --session-id kova-agent-self-check --message hi"
|
||||
});
|
||||
const leaks = diffProcessSnapshots(before, after, {
|
||||
roles: ["agent-cli", "agent-process", "mcp-runtime", "plugin-cli", "mock-provider", "browser-sidecar"]
|
||||
@ -5223,10 +5226,42 @@ async function processSnapshotCheck(tmp) {
|
||||
await writeFile(artifactPath, `${JSON.stringify(leaks, null, 2)}\n`, "utf8");
|
||||
|
||||
try {
|
||||
const unrelatedBrowserRoles = classifySnapshotRolesForProcess({
|
||||
command: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --type=renderer"
|
||||
}, {
|
||||
processRoles,
|
||||
envName: "kova-self-check",
|
||||
rootCommand: "ocm @kova-self-check -- agent --local --session-id kova-agent-self-check --message hi"
|
||||
});
|
||||
const scopedBrowserRoles = classifySnapshotRolesForProcess({
|
||||
command: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --user-data-dir=/tmp/kova-self-check/browser"
|
||||
}, {
|
||||
processRoles,
|
||||
envName: "kova-self-check",
|
||||
rootCommand: "ocm @kova-self-check -- agent --local --session-id kova-agent-self-check --message hi"
|
||||
});
|
||||
const gatewayBrowserRoles = classifySnapshotRolesForProcess({
|
||||
command: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --type=renderer"
|
||||
}, {
|
||||
processRoles,
|
||||
existingRoles: ["gateway-tree"],
|
||||
envName: "kova-self-check"
|
||||
});
|
||||
const scopedAgentRoles = classifySnapshotRolesForProcess({
|
||||
command: "openclaw-agent --session-id kova-agent-self-check"
|
||||
}, {
|
||||
processRoles,
|
||||
envName: "kova-self-check",
|
||||
rootCommand: "ocm @kova-self-check -- agent --local --session-id kova-agent-self-check --message hi"
|
||||
});
|
||||
assertEqual(result.status, 0, "snapshot command status");
|
||||
assertEqual(before.schemaVersion, "kova.processSnapshot.v1", "snapshot schema");
|
||||
assertEqual(leaks.schemaVersion, "kova.processLeakSummary.v1", "leak summary schema");
|
||||
assertEqual(typeof leaks.leakCount, "number", "leak count type");
|
||||
assertEqual(unrelatedBrowserRoles.includes("browser-sidecar"), false, "unrelated browser process excluded from snapshot role");
|
||||
assertEqual(scopedBrowserRoles.includes("browser-sidecar"), true, "scoped browser process retained");
|
||||
assertEqual(gatewayBrowserRoles.includes("browser-sidecar"), true, "gateway child browser process retained");
|
||||
assertEqual(scopedAgentRoles.includes("agent-cli"), true, "scoped agent process retained");
|
||||
return {
|
||||
id: "process-snapshot-leak-contract",
|
||||
status: "PASS",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user