fix(process): retain child scope and bound native exit waits (#5280)

Retain original parent/child scope and prior native-generation proof across exit settlement and token retries. Keep historical scope separate from signal authority, count each supplied snapshot once, and share one existing native-exit deadline throughout cleanup.

Validated 387 affected tests (one Darwin-only skip), five controls repeated three times, and 30 synced-main integration tests (one platform skip). Synced-head static, architecture, docs and discovery checks pass; real GDB qualification passes 44 CLI/MCP cases. Native-observer edge controls are simulated.

Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage)

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
This commit is contained in:
gensokyo-dev authored and GitHub committed 2026-10-19 19:27:43 +00:00
1 parent 10f1994fe1
commit 71ae02c6ef
5 files changed
+293 -18

No files matched your search

+27 -4
View File
@@ -6,6 +6,7 @@ import {
processIdentityIsExiting,
PROCESS_CLEANUP_VERIFICATION_GRACE_MS,
PROCESS_CLEANUP_VERIFICATION_INTERVAL_MS,
type ProcessExitSettlementBudget,
} from "./ProcessLifecycle.js";
import { parseLinuxProcessStat } from "./LinuxProcessStat.js";
import { parseLinuxProcessEffectiveUid } from "./LinuxProcessStatus.js";
@@ -539,6 +540,7 @@ export const settleExitingProcessIdentities = async (
host: ProcessOwnershipHost,
initial: ReadonlyMap<number, ProcessIdentityObservation>,
signal?: AbortSignal,
budget: ProcessExitSettlementBudget = { deadline: undefined },
): Promise<ReadonlyMap<number, ProcessIdentityObservation>> => {
signal?.throwIfAborted();
let pending = processes.filter(({ pid }) =>
@@ -546,7 +548,8 @@ export const settleExitingProcessIdentities = async (
);
if (pending.length === 0) return initial;
const identities = new Map(initial);
const deadline = performance.now() + PROCESS_CLEANUP_VERIFICATION_GRACE_MS;
const deadline = (budget.deadline ??=
performance.now() + PROCESS_CLEANUP_VERIFICATION_GRACE_MS);
while (pending.length > 0) {
const remaining = deadline - performance.now();
if (remaining <= 0) break;
@@ -589,6 +592,7 @@ export const readProcessOwnershipFacts = async (
signal?: AbortSignal,
options: {
readonly identityBefore?: ReadonlyMap<number, ProcessIdentityObservation>;
readonly exitSettlement?: ProcessExitSettlementBudget;
} = {},
): Promise<readonly ProcessOwnershipFact[]> => {
signal?.throwIfAborted();
@@ -627,19 +631,30 @@ export const readProcessOwnershipFacts = async (
.filter(({ pid }) => tokens.get(pid)?.state !== "readable")
.map(({ pid }) => pid),
);
const observedAfter = await readIdentityFacts(
processes,
host,
"after",
signal,
effectiveUidPids,
);
const identityAfter = await settleExitingProcessIdentities(
processes,
host,
await readIdentityFacts(processes, host, "after", signal, effectiveUidPids),
observedAfter,
signal,
options.exitSettlement,
);
signal?.throwIfAborted();
const facts = processes.map((process): ProcessOwnershipFact => {
const deriveFact = (
process: ProcessTableEntry,
observation: ProcessIdentityObservation | undefined,
): ProcessOwnershipFact => {
const before = identityBefore.get(process.pid) ?? {
state: "unavailable" as const,
reason: "process identity was not returned during before observation",
};
const after = identityAfter.get(process.pid) ?? {
const after = observation ?? {
state: "unavailable" as const,
reason: "process identity was not returned during after observation",
};
@@ -664,6 +679,14 @@ export const readProcessOwnershipFacts = async (
token,
identityAfter: after,
};
};
const facts = processes.map((process): ProcessOwnershipFact => {
const after = identityAfter.get(process.pid);
const original = observedAfter.get(process.pid);
const fact = deriveFact(process, after);
return after === original
? fact
: { ...fact, beforeExitSettlement: deriveFact(process, original) };
});
signal?.throwIfAborted();
return facts;