[GH-ISSUE #2540] fix(process): report one unreadable child as one live process #858

Closed
opened 2026-10-15 18:29:02 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2540

Summary

When capture cleanup scans for its run token and a live child cannot expose that token, the cleanup result repeats the same child failure and reports two live processes. The process remains unsignaled, but the reported count and candidate list misstate the observation.

Steps to reproduce

Use the source runtime described below. Place this test beside the ownership module as src/process/cleanup-count.regression.test.ts, then run npm run test:focused -- src/process/cleanup-count.regression.test.ts.

import { expect, it, vi } from "vitest";
import { cleanupOwnedProcessGroup, type ProcessOwnershipHost } from "./ProcessOwnership.js";

it("counts one live unreadable child once", async () => {
  const rows = [
    { pid: 100, parentPid: 1, processGroupId: 100, state: "S", command: "capture" },
    { pid: 101, parentPid: 100, processGroupId: 100, state: "S", command: "child" },
  ];
  const signalGroup = vi.fn();
  const host: ProcessOwnershipHost = {
    listProcesses: async () => rows,
    environment: async () => ({}),
    runTokens: async (members) => new Map(members.map(({ pid }) => [pid,
      pid === 100
        ? { state: "readable" as const, runId: "capture-token" }
        : { state: "unavailable" as const, reason: "environment_unavailable" },
    ])),
    signalGroup,
  };
  const result = await cleanupOwnedProcessGroup({
    runId: "capture-token", leaderPid: 100, processGroupId: 100,
    sweepTokenOwnedProcesses: true, captureBaseline: [],
  }, host);
  expect(signalGroup).not.toHaveBeenCalled();
  expect(result.cleaned).toBe(false);
  if (result.cleaned) throw new Error("cleanup unexpectedly succeeded");
  expect(result.failures).toHaveLength(1);
  expect(result.reason).toContain("1 live process(es)");
});

The fixture keeps both PIDs live across all reads. The launcher token matches; the child token is unavailable throughout the bounded retries. This is a simulated operating-system host, so it tests the production cleanup boundary without launching or signaling actual processes.

Expected behavior

Cleanup must report the one unreadable child once, name one live process, and preserve the failure without signaling the unverified group.

Actual behavior

A focused run returned two identical failures for PID 101 and the reason below. The one-entry assertion failed and the zero-signal assertion passed.

process ownership token could not be read for 2 live process(es): environment_unavailable=2; live candidates 101=environment_unavailable, 101=environment_unavailable

Evidence

  • Expected source: src/process/ProcessOwnership.ts:163-169 defines retained member failures; src/process/ProcessOwnership.ts:629 presents a count of live processes; src/process/ProcessOwnership.test.ts:145-208 checks the single-member result.
  • Failure source: src/process/ProcessOwnership.ts:459 retains the sweep failure and src/process/ProcessOwnership.ts:492-510 adds a second failure for the same live member before src/process/ProcessOwnership.ts:611-632 counts both.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The capture owner enables this sweep and returns its failure reason in src/process/capture/ProcessCaptureLifecycle.ts:1018-1021 and src/process/capture/ProcessCaptureLifecycle.ts:1126-1138. Runtime verification covered the ownership helper with a simulated host, while the capture-report propagation was checked from source.

Restoration check

The fixture must return one failure for PID 101 and a count of one, with cleanup still unsuccessful and zero signal calls. A control with two distinct unreadable live children must report each PID once and count two, preserving separate observations.

Runtime or environment

Source revision 8b181716aca2cb467be499cba226abfb5ae19141, Node v26.10.0, npm 11.19.1, installed lockfile dependencies and Vitest v5.0.3. Verification used focused source tests.

Triage assessment

  • Impact level: P3
  • Assessment status: supported
  • Impact basis: A single member appears twice in diagnostic records and the count. The probe confirms that cleanup remains unsuccessful and no signal occurs; the process identity remains visible.
  • Workaround status: available
  • Workaround basis: The returned candidate list still names the affected PID, so consumers can count distinct PIDs when interpreting this diagnostic.
Originally created by @coygeek on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2540 ## Summary When capture cleanup scans for its run token and a live child cannot expose that token, the cleanup result repeats the same child failure and reports two live processes. The process remains unsignaled, but the reported count and candidate list misstate the observation. ## Steps to reproduce Use the source runtime described below. Place this test beside the ownership module as `src/process/cleanup-count.regression.test.ts`, then run `npm run test:focused -- src/process/cleanup-count.regression.test.ts`. ```ts import { expect, it, vi } from "vitest"; import { cleanupOwnedProcessGroup, type ProcessOwnershipHost } from "./ProcessOwnership.js"; it("counts one live unreadable child once", async () => { const rows = [ { pid: 100, parentPid: 1, processGroupId: 100, state: "S", command: "capture" }, { pid: 101, parentPid: 100, processGroupId: 100, state: "S", command: "child" }, ]; const signalGroup = vi.fn(); const host: ProcessOwnershipHost = { listProcesses: async () => rows, environment: async () => ({}), runTokens: async (members) => new Map(members.map(({ pid }) => [pid, pid === 100 ? { state: "readable" as const, runId: "capture-token" } : { state: "unavailable" as const, reason: "environment_unavailable" }, ])), signalGroup, }; const result = await cleanupOwnedProcessGroup({ runId: "capture-token", leaderPid: 100, processGroupId: 100, sweepTokenOwnedProcesses: true, captureBaseline: [], }, host); expect(signalGroup).not.toHaveBeenCalled(); expect(result.cleaned).toBe(false); if (result.cleaned) throw new Error("cleanup unexpectedly succeeded"); expect(result.failures).toHaveLength(1); expect(result.reason).toContain("1 live process(es)"); }); ``` The fixture keeps both PIDs live across all reads. The launcher token matches; the child token is unavailable throughout the bounded retries. This is a simulated operating-system host, so it tests the production cleanup boundary without launching or signaling actual processes. ## Expected behavior Cleanup must report the one unreadable child once, name one live process, and preserve the failure without signaling the unverified group. ## Actual behavior A focused run returned two identical failures for PID 101 and the reason below. The one-entry assertion failed and the zero-signal assertion passed. ```text process ownership token could not be read for 2 live process(es): environment_unavailable=2; live candidates 101=environment_unavailable, 101=environment_unavailable ``` ## Evidence - Expected source: `src/process/ProcessOwnership.ts:163-169` defines retained member failures; `src/process/ProcessOwnership.ts:629` presents a count of live processes; `src/process/ProcessOwnership.test.ts:145-208` checks the single-member result. - Failure source: `src/process/ProcessOwnership.ts:459` retains the sweep failure and `src/process/ProcessOwnership.ts:492-510` adds a second failure for the same live member before `src/process/ProcessOwnership.ts:611-632` counts both. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The capture owner enables this sweep and returns its failure reason in `src/process/capture/ProcessCaptureLifecycle.ts:1018-1021` and `src/process/capture/ProcessCaptureLifecycle.ts:1126-1138`. Runtime verification covered the ownership helper with a simulated host, while the capture-report propagation was checked from source. ## Restoration check The fixture must return one failure for PID 101 and a count of one, with cleanup still unsuccessful and zero signal calls. A control with two distinct unreadable live children must report each PID once and count two, preserving separate observations. ## Runtime or environment Source revision `8b181716aca2cb467be499cba226abfb5ae19141`, Node v26.10.0, npm 11.19.1, installed lockfile dependencies and Vitest v5.0.3. Verification used focused source tests. ## Triage assessment - Impact level: P3 - Assessment status: supported - Impact basis: A single member appears twice in diagnostic records and the count. The probe confirms that cleanup remains unsuccessful and no signal occurs; the process identity remains visible. - Workaround status: available - Workaround basis: The returned candidate list still names the affected PID, so consumers can count distinct PIDs when interpreting this diagnostic.
bsakaguchi 2026-10-15 18:29:02 +00:00
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Verified the reported defect is addressed on main by 599a8ee11d.

Root cause and resolution: The same unreadable live process could be observed by the initial sweep and again during group cleanup, then counted twice.

Validation: src/process/ProcessOwnership.test.ts — counts each unreadable live member once gives duplicate observations for one PID and a second unreadable PID; asserts one count per identity and no signal without established ownership.

Evidence: CI run 500e: owner unchanged and assertion passed.

Verification scope: Deterministic supervisor observations, not platform process enumeration.

Current main CI has separate failures under repair; this closure does not claim the complete main workflow is green.

<!-- gh-comment-id:6162520212 --> @morluto commented on GitHub (Oct 14, 2026): Verified the reported defect is addressed on main by 599a8ee11de6b1a7fb270e669b88d78b6be982d2. Root cause and resolution: The same unreadable live process could be observed by the initial sweep and again during group cleanup, then counted twice. Validation: `src/process/ProcessOwnership.test.ts` — `counts each unreadable live member once` gives duplicate observations for one PID and a second unreadable PID; asserts one count per identity and no signal without established ownership. Evidence: CI run 500e: owner unchanged and assertion passed. Verification scope: Deterministic supervisor observations, not platform process enumeration. Current main CI has separate failures under repair; this closure does not claim the complete main workflow is green.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#858
No description provided.