[GH-ISSUE #3985] [Bug] Hopper close succeeds while a timed-out probe remains alive after failed cleanup #1581

Closed
opened 2026-10-16 21:07:24 +00:00 by bsakaguchi · 4 comments
Owner

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

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

When a Linux private-display probe times out and cannot complete its owned-process cleanup, the public MCP query hides the cleanup failure and close_binary succeeds while the probe is alive. The session makes no further stop attempt. This was observed with a controlled first cleanup failure at the production supervisor boundary. It does not establish a naturally occurring operating-system failure or a real-Hopper failure.

Reproduction

  1. From the baseline checkout with its dependencies and runtime artifacts prepared, save the following temporary test as src/hopper/probeOwnerReproduction.test.ts. The test calls the registered MCP tools through an in-memory client and uses the production provider, launcher, probe runner, process creation and supervisor. Only the helper location, timeout, lease location, platform selection and first cleanup outcome are controlled. It retains the supervisor separately to stop the probe after the observation.
import { mkdir, writeFile } from "node:fs/promises";
import { expect, it, vi } from "vitest";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { ProviderProcessSupervisor } from "../process/ProviderProcess.js";
import * as probe from "./LinuxPrivateDisplayProbe.js";
import * as lease from "./HopperTargetLease.js";
import { HopperProvider } from "./HopperProvider.js";
import { parseConfig } from "../config/parseConfig.js";
import { silentLogger } from "../logger.js";
import { composeBinarySession } from "../application/binary/BinarySessionComposition.js";
import { AnalysisProviderRegistry } from "../application/binary/AnalysisProviderRegistry.js";
import { createServer } from "../server/createServer.js";
import { createGoBinaryFixture } from "../../tests/fixtures/go/image.js";
import { parseMcpToolError } from "../../tests/fixtures/mcpToolError.js";

it.each([true, false])("MCP timeout with cleanup failure=%s", async (failCleanup) => {
  const root = `${process.env.REA_VERIFY_ROOT}/${failCleanup ? "failure" : "control"}`;
  await mkdir(`${root}/home`, {recursive:true});
  const helper = `${root}/probe.py`;
  const target = `${root}/target`;
  await writeFile(helper, "import time\ntime.sleep(60)\n");
  await writeFile(target, createGoBinaryFixture({format:"macho"}).bytes);
  const realStop = ProviderProcessSupervisor.prototype.stop;
  const realSelect = probe.selectLinuxPrivateDisplayStrategy;
  const realAcquire = lease.acquireLinuxHopperApplicationLease;
  let supervisor: ProviderProcessSupervisor | undefined;
  let stopCalls = 0;
  let processResult: probe.LinuxPrivateDisplayProbeProcessResult | undefined;
  const stopSpy = vi.spyOn(ProviderProcessSupervisor.prototype,"stop").mockImplementation(function(this:ProviderProcessSupervisor, options) {
    supervisor = this;
    stopCalls++;
    return realStop.call(this, failCleanup && stopCalls === 1 ? {cleanupResult:{cleaned:false,reason:"controlled first cleanup failure"}} : options);
  });
  const selectSpy = vi.spyOn(probe,"selectLinuxPrivateDisplayStrategy").mockImplementation(options => realSelect({
    ...options, helperPath:helper, timeoutMs:100,
    runProbe: async (strategy, settings) => {
      processResult = await probe.runLinuxPrivateDisplayProbe(strategy,settings);
      return processResult;
    },
  }));
  const leaseSpy = vi.spyOn(lease,"acquireLinuxHopperApplicationLease").mockImplementation(input => realAcquire({...input,directory:`${root}/leases`}));
  const config = parseConfig({HOPPER_LAUNCHER_PATH:process.execPath,HOPPER_LOADER_ARGS_JSON:'["--fixture"]'});
  if (!config.ok) throw config.error;
  const provider = new HopperProvider(config.value,silentLogger,{PATH:process.env.PATH,HOME:`${root}/home`},"linux");
  const session = composeBinarySession(new AnalysisProviderRegistry([provider]));
  const server = createServer({kind:"session",session},{logger:silentLogger});
  const client = new Client({name:"cleanup-reproduction",version:"1"});
  const [clientTransport,serverTransport] = InMemoryTransport.createLinkedPair();
  try {
    await server.connect(serverTransport);
    await client.connect(clientTransport);
    const opened = await client.callTool({name:"open_binary",arguments:{path:target,provider_id:"hopper"}});
    expect(opened.isError,JSON.stringify(opened)).not.toBe(true);
    const queried = await client.callTool({name:"current_document",arguments:{}});
    const error = parseMcpToolError(queried);
    expect(error.error.code).toBe("provider_unavailable");
    expect(error.error.details).toMatchObject({provider_state:"exited",diagnostics:{reason:"probe_timeout"}});
    expect(JSON.stringify(error)).not.toContain("cleanup_incomplete");
    expect(processResult).toMatchObject({outcome:"timed-out",cleanupIncomplete:failCleanup});
    const closed = await client.callTool({name:"close_binary",arguments:{}});
    expect(closed.isError,JSON.stringify(closed)).not.toBe(true);
    expect(stopCalls).toBe(1);
    const child = supervisor?.launch.process;
    if (child?.pid === undefined) throw new Error("No probe captured");
    if (failCleanup) {
      expect(child.exitCode).toBeNull();
      expect(child.signalCode).toBeNull();
      expect(process.kill(child.pid,0)).toBe(true);
    } else expect(child.exitCode !== null || child.signalCode !== null).toBe(true);
    console.log(JSON.stringify({failCleanup,processResult,error:error.error,closeSucceeded:true,probeAliveAfterClose:child.exitCode===null&&child.signalCode===null,stopCalls}));
  } finally {
    stopSpy.mockRestore();
    selectSpy.mockRestore();
    leaseSpy.mockRestore();
    if (supervisor !== undefined) {
      const cleanup = await realStop.call(supervisor);
      console.log(JSON.stringify({failCleanup,finalCleanup:cleanup,exit:supervisor.launch.process.exitCode,signal:supervisor.launch.process.signalCode}));
      expect(cleanup.status).not.toBe("incomplete");
      expect(supervisor.launch.process.exitCode!==null||supervisor.launch.process.signalCode!==null).toBe(true);
    }
    await client.close();
    await server.close();
    const cleanup = await session.close();
    if (!cleanup.ok) throw cleanup.error;
  }
});
  1. Run this focused test with a disposable home and temporary directory. Use the repository's pinned Node toolchain.
probe_root="$(mktemp -d)"
mkdir -p "$probe_root/home" "$probe_root/temp"
HOME="$probe_root/home" TMPDIR="$probe_root/temp" REA_VERIFY_ROOT="$probe_root"   ./node_modules/.bin/vitest run src/hopper/probeOwnerReproduction.test.ts
  1. Observe the failing-cleanup case and successful-cleanup control. The test checks operating-system liveness after successful close in the failure case and confirms process exit in the control. Its final cleanup invokes the retained supervisor and checks verified exit. Remove the temporary test and disposable directory after the run.

Expected behavior

An owned probe whose cleanup failed must remain represented by the session. The caller must receive a cleanup failure identifying the remaining owned resource, and retrying close_binary on that connection must retry cleanup. A successful close must not discard a still-live owned probe. An ordinary probe timeout whose cleanup succeeds may remain a provider startup failure.

Impact and scope

Under the controlled cleanup failure, a real owned probe stays alive after successful session close, and public recovery never retries its stop. This is a bounded lifecycle and recovery defect after startup has already failed. Severe host harm and natural frequency were not established.

The local reproduction recovers only by retaining an internal supervisor reference that the public session does not expose. No safe public recovery workflow for the discarded owner was demonstrated, and manual process identification and termination were not verified.

REA version

rea 6.5.0 at source commit 4692bb3330; source re-checked on origin/main fc712b3750

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Generated Mach-O fixture; Linux Hopper route selected through a platform seam on macOS; real sleeping Python helper; no Hopper or Xvfb.

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

  • Expected source: AGENTS.md, Architecture and Cleanup, requires retaining ownership after failed cleanup; docs/mcp-contracts.md:420 requires cleanup_incomplete for failed cleanup; the Hopper lifecycle contract in docs/installation.md:693 requires retaining unresolved cleanup ownership and permits retrying close_binary on the same connection.
  • Failure source: The inline MCP reproduction observes the timed-out probe's incomplete cleanup, the provider_unavailable response, successful close, one stop attempt and the live probe. src/hopper/LinuxPrivateDisplayProbe.ts:130 keeps the supervisor local and returns only scalar facts at line 147; the timeout branch at line 230 precedes the cleanup check at line 240. src/hopper/BridgeLauncher.ts:219 returns the failed preflight before a provider launch is returned, and line 385 converts the selection into an error without a probe cleanup capability.

The source-derived explanation is that the failed preflight never transfers a probe cleanup capability into the session's owned resources. The successful public close and absence of another stop attempt were observed directly. Cancellation, natural cleanup failures, real Linux and real Hopper behavior were not executed.

Both observation cases passed in one local run. In the injected-failure case, the real probe runner returned outcome: "timed-out" and cleanupIncomplete: true. The current_document tool returned this diagnostic subset, with no cleanup_incomplete indication:

{
  "code": "provider_unavailable",
  "retryable": true,
  "details": {
    "exit_code": 70,
    "stage": "launch",
    "provider_state": "exited",
    "retry_action": "retry",
    "failure_code": "private_display_unavailable",
    "diagnostics": { "reason": "probe_timeout" }
  }
}

The error recommended installing Xvfb and rerunning doctor. close_binary then succeeded while the probe had null exit and signal codes and passed an operating-system liveness check. The supervisor stop counter stayed at one, so the public close did not retry the failed stop. A separate test-held reference subsequently completed real cleanup with status: "verified-cleanup" and observed SIGKILL termination.

The control used the same timeout and normal cleanup. Its runner returned cleanupIncomplete: false, the query returned the same startup diagnostic, and close_binary succeeded with the probe already exited. The failure requires the controlled cleanup failure; a timeout alone did not leave the probe alive in this run.

Typed outcome or recovery expected

Repeat the same MCP sequence and first-attempt cleanup failure. The caller must receive the cleanup failure and remaining owned process information. A subsequent close_binary must retry the probe's cleanup and confirm its exit, or return cleanup_incomplete while ownership remains available for another retry. Keep the ordinary-timeout control: when cleanup succeeds, the provider may report startup failure, but no probe may remain alive after close. Do not pass by suppressing the timeout or skipping probe creation.

Analysis or suspected cause

The source references above explain the observed behavior. Runtime claims are limited to the explicitly described simulation boundaries.

Additional context

Runtime claims above describe the supplied local reproduction at the baseline commit. Real-provider frequency remains unmeasured.

Still holds by source on current origin/main fc712b3750. The newer launcher cleanup method retries publisher-verification resources. The private-display probe still returns only scalar facts and loses its local supervisor, and the timeout path still precedes the cleanup-incomplete diagnostic. No current-main runtime rerun is claimed. The reproduction above targets the baseline; the lease helper moved to src/process/HopperLease.ts on current main.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3985 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior When a Linux private-display probe times out and cannot complete its owned-process cleanup, the public MCP query hides the cleanup failure and close_binary succeeds while the probe is alive. The session makes no further stop attempt. This was observed with a controlled first cleanup failure at the production supervisor boundary. It does not establish a naturally occurring operating-system failure or a real-Hopper failure. ### Reproduction 1. From the baseline checkout with its dependencies and runtime artifacts prepared, save the following temporary test as `src/hopper/probeOwnerReproduction.test.ts`. The test calls the registered MCP tools through an in-memory client and uses the production provider, launcher, probe runner, process creation and supervisor. Only the helper location, timeout, lease location, platform selection and first cleanup outcome are controlled. It retains the supervisor separately to stop the probe after the observation. ```ts import { mkdir, writeFile } from "node:fs/promises"; import { expect, it, vi } from "vitest"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { ProviderProcessSupervisor } from "../process/ProviderProcess.js"; import * as probe from "./LinuxPrivateDisplayProbe.js"; import * as lease from "./HopperTargetLease.js"; import { HopperProvider } from "./HopperProvider.js"; import { parseConfig } from "../config/parseConfig.js"; import { silentLogger } from "../logger.js"; import { composeBinarySession } from "../application/binary/BinarySessionComposition.js"; import { AnalysisProviderRegistry } from "../application/binary/AnalysisProviderRegistry.js"; import { createServer } from "../server/createServer.js"; import { createGoBinaryFixture } from "../../tests/fixtures/go/image.js"; import { parseMcpToolError } from "../../tests/fixtures/mcpToolError.js"; it.each([true, false])("MCP timeout with cleanup failure=%s", async (failCleanup) => { const root = `${process.env.REA_VERIFY_ROOT}/${failCleanup ? "failure" : "control"}`; await mkdir(`${root}/home`, {recursive:true}); const helper = `${root}/probe.py`; const target = `${root}/target`; await writeFile(helper, "import time\ntime.sleep(60)\n"); await writeFile(target, createGoBinaryFixture({format:"macho"}).bytes); const realStop = ProviderProcessSupervisor.prototype.stop; const realSelect = probe.selectLinuxPrivateDisplayStrategy; const realAcquire = lease.acquireLinuxHopperApplicationLease; let supervisor: ProviderProcessSupervisor | undefined; let stopCalls = 0; let processResult: probe.LinuxPrivateDisplayProbeProcessResult | undefined; const stopSpy = vi.spyOn(ProviderProcessSupervisor.prototype,"stop").mockImplementation(function(this:ProviderProcessSupervisor, options) { supervisor = this; stopCalls++; return realStop.call(this, failCleanup && stopCalls === 1 ? {cleanupResult:{cleaned:false,reason:"controlled first cleanup failure"}} : options); }); const selectSpy = vi.spyOn(probe,"selectLinuxPrivateDisplayStrategy").mockImplementation(options => realSelect({ ...options, helperPath:helper, timeoutMs:100, runProbe: async (strategy, settings) => { processResult = await probe.runLinuxPrivateDisplayProbe(strategy,settings); return processResult; }, })); const leaseSpy = vi.spyOn(lease,"acquireLinuxHopperApplicationLease").mockImplementation(input => realAcquire({...input,directory:`${root}/leases`})); const config = parseConfig({HOPPER_LAUNCHER_PATH:process.execPath,HOPPER_LOADER_ARGS_JSON:'["--fixture"]'}); if (!config.ok) throw config.error; const provider = new HopperProvider(config.value,silentLogger,{PATH:process.env.PATH,HOME:`${root}/home`},"linux"); const session = composeBinarySession(new AnalysisProviderRegistry([provider])); const server = createServer({kind:"session",session},{logger:silentLogger}); const client = new Client({name:"cleanup-reproduction",version:"1"}); const [clientTransport,serverTransport] = InMemoryTransport.createLinkedPair(); try { await server.connect(serverTransport); await client.connect(clientTransport); const opened = await client.callTool({name:"open_binary",arguments:{path:target,provider_id:"hopper"}}); expect(opened.isError,JSON.stringify(opened)).not.toBe(true); const queried = await client.callTool({name:"current_document",arguments:{}}); const error = parseMcpToolError(queried); expect(error.error.code).toBe("provider_unavailable"); expect(error.error.details).toMatchObject({provider_state:"exited",diagnostics:{reason:"probe_timeout"}}); expect(JSON.stringify(error)).not.toContain("cleanup_incomplete"); expect(processResult).toMatchObject({outcome:"timed-out",cleanupIncomplete:failCleanup}); const closed = await client.callTool({name:"close_binary",arguments:{}}); expect(closed.isError,JSON.stringify(closed)).not.toBe(true); expect(stopCalls).toBe(1); const child = supervisor?.launch.process; if (child?.pid === undefined) throw new Error("No probe captured"); if (failCleanup) { expect(child.exitCode).toBeNull(); expect(child.signalCode).toBeNull(); expect(process.kill(child.pid,0)).toBe(true); } else expect(child.exitCode !== null || child.signalCode !== null).toBe(true); console.log(JSON.stringify({failCleanup,processResult,error:error.error,closeSucceeded:true,probeAliveAfterClose:child.exitCode===null&&child.signalCode===null,stopCalls})); } finally { stopSpy.mockRestore(); selectSpy.mockRestore(); leaseSpy.mockRestore(); if (supervisor !== undefined) { const cleanup = await realStop.call(supervisor); console.log(JSON.stringify({failCleanup,finalCleanup:cleanup,exit:supervisor.launch.process.exitCode,signal:supervisor.launch.process.signalCode})); expect(cleanup.status).not.toBe("incomplete"); expect(supervisor.launch.process.exitCode!==null||supervisor.launch.process.signalCode!==null).toBe(true); } await client.close(); await server.close(); const cleanup = await session.close(); if (!cleanup.ok) throw cleanup.error; } }); ``` 2. Run this focused test with a disposable home and temporary directory. Use the repository's pinned Node toolchain. ```sh probe_root="$(mktemp -d)" mkdir -p "$probe_root/home" "$probe_root/temp" HOME="$probe_root/home" TMPDIR="$probe_root/temp" REA_VERIFY_ROOT="$probe_root" ./node_modules/.bin/vitest run src/hopper/probeOwnerReproduction.test.ts ``` 3. Observe the failing-cleanup case and successful-cleanup control. The test checks operating-system liveness after successful close in the failure case and confirms process exit in the control. Its final cleanup invokes the retained supervisor and checks verified exit. Remove the temporary test and disposable directory after the run. ### Expected behavior An owned probe whose cleanup failed must remain represented by the session. The caller must receive a cleanup failure identifying the remaining owned resource, and retrying close_binary on that connection must retry cleanup. A successful close must not discard a still-live owned probe. An ordinary probe timeout whose cleanup succeeds may remain a provider startup failure. ### Impact and scope Under the controlled cleanup failure, a real owned probe stays alive after successful session close, and public recovery never retries its stop. This is a bounded lifecycle and recovery defect after startup has already failed. Severe host harm and natural frequency were not established. The local reproduction recovers only by retaining an internal supervisor reference that the public session does not expose. No safe public recovery workflow for the discarded owner was demonstrated, and manual process identification and termination were not verified. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860; source re-checked on origin/main fc712b3750cbe16755c4f78c1f90fdf09fbaca31 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Generated Mach-O fixture; Linux Hopper route selected through a platform seam on macOS; real sleeping Python helper; no Hopper or Xvfb. ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence - Expected source: `AGENTS.md`, Architecture and Cleanup, requires retaining ownership after failed cleanup; `docs/mcp-contracts.md:420` requires cleanup_incomplete for failed cleanup; the Hopper lifecycle contract in `docs/installation.md:693` requires retaining unresolved cleanup ownership and permits retrying close_binary on the same connection. - Failure source: The inline MCP reproduction observes the timed-out probe's incomplete cleanup, the provider_unavailable response, successful close, one stop attempt and the live probe. `src/hopper/LinuxPrivateDisplayProbe.ts:130` keeps the supervisor local and returns only scalar facts at line 147; the timeout branch at line 230 precedes the cleanup check at line 240. `src/hopper/BridgeLauncher.ts:219` returns the failed preflight before a provider launch is returned, and line 385 converts the selection into an error without a probe cleanup capability. The source-derived explanation is that the failed preflight never transfers a probe cleanup capability into the session's owned resources. The successful public close and absence of another stop attempt were observed directly. Cancellation, natural cleanup failures, real Linux and real Hopper behavior were not executed. Both observation cases passed in one local run. In the injected-failure case, the real probe runner returned `outcome: "timed-out"` and `cleanupIncomplete: true`. The current_document tool returned this diagnostic subset, with no cleanup_incomplete indication: ```json { "code": "provider_unavailable", "retryable": true, "details": { "exit_code": 70, "stage": "launch", "provider_state": "exited", "retry_action": "retry", "failure_code": "private_display_unavailable", "diagnostics": { "reason": "probe_timeout" } } } ``` The error recommended installing Xvfb and rerunning doctor. close_binary then succeeded while the probe had null exit and signal codes and passed an operating-system liveness check. The supervisor stop counter stayed at one, so the public close did not retry the failed stop. A separate test-held reference subsequently completed real cleanup with `status: "verified-cleanup"` and observed SIGKILL termination. The control used the same timeout and normal cleanup. Its runner returned `cleanupIncomplete: false`, the query returned the same startup diagnostic, and close_binary succeeded with the probe already exited. The failure requires the controlled cleanup failure; a timeout alone did not leave the probe alive in this run. ### Typed outcome or recovery expected Repeat the same MCP sequence and first-attempt cleanup failure. The caller must receive the cleanup failure and remaining owned process information. A subsequent close_binary must retry the probe's cleanup and confirm its exit, or return cleanup_incomplete while ownership remains available for another retry. Keep the ordinary-timeout control: when cleanup succeeds, the provider may report startup failure, but no probe may remain alive after close. Do not pass by suppressing the timeout or skipping probe creation. ### Analysis or suspected cause The source references above explain the observed behavior. Runtime claims are limited to the explicitly described simulation boundaries. ### Additional context Runtime claims above describe the supplied local reproduction at the baseline commit. Real-provider frequency remains unmeasured. Still holds by source on current `origin/main` fc712b3750cbe16755c4f78c1f90fdf09fbaca31. The newer launcher cleanup method retries publisher-verification resources. The private-display probe still returns only scalar facts and loses its local supervisor, and the timeout path still precedes the cleanup-incomplete diagnostic. No current-main runtime rerun is claimed. The reproduction above targets the baseline; the lease helper moved to `src/process/HopperLease.ts` on current main.
Author
Owner

@XinwangTao commented on GitHub (Oct 15, 2026):

Confirmed — the lifecycle gap is at the ownership transfer boundary. When the preflight fails (BridgeLauncher.ts:219), the probe's cleanup capability never becomes a session-owned resource, so close_binary sees nothing to clean and reports success while the timed-out probe keeps running. The expected contract is right: a close that would discard a still-live owned probe must instead surface cleanup_incomplete and remain retryable. Concrete direction: on the failed-preflight path, register the probe supervisor's stop capability as an owned resource in the session (or retain the supervisor reference long enough for close_binary to retry), and reorder the timeout branch in LinuxPrivateDisplayProbe.ts so cleanup is attempted and its result recorded before the outcome is projected to provider_unavailable. Also worth asserting in the reproduction test that a second close_binary call after cleanup_incomplete actually retries stop and lands the probe exited.

<!-- gh-comment-id:6189096353 --> @XinwangTao commented on GitHub (Oct 15, 2026): Confirmed — the lifecycle gap is at the ownership transfer boundary. When the preflight fails (BridgeLauncher.ts:219), the probe's cleanup capability never becomes a session-owned resource, so close_binary sees nothing to clean and reports success while the timed-out probe keeps running. The expected contract is right: a close that would discard a still-live owned probe must instead surface cleanup_incomplete and remain retryable. Concrete direction: on the failed-preflight path, register the probe supervisor's stop capability as an owned resource in the session (or retain the supervisor reference long enough for close_binary to retry), and reorder the timeout branch in LinuxPrivateDisplayProbe.ts so cleanup is attempted and its result recorded before the outcome is projected to provider_unavailable. Also worth asserting in the reproduction test that a second close_binary call after cleanup_incomplete actually retries stop and lands the probe exited.
Author
Owner

@yankikucuk commented on GitHub (Oct 15, 2026):

Working on this. Reproduced with your inline MCP test on a clean build: the probe runner dropped its supervisor, so the timed-out probe with a failed stop was never stopped again and close succeeded. The probe result now carries the still-owned probe, the selection hands every retained probe to the launcher with the timeout reason intact, and the launcher retries their stop on every cleanup: a verified stop releases ownership, anything else is reported as the hopper-private-display-probe resource, composed into the startup failure as cleanup_incomplete, blocks restart and is retried by close_binary. With your single controlled failure the retry during startup cleanup already stops the probe (two stop calls, dead after close); a client test covers the repeated-failure path. No real Linux or Hopper run. Draft PR follows shortly.

<!-- gh-comment-id:6191609070 --> @yankikucuk commented on GitHub (Oct 15, 2026): Working on this. Reproduced with your inline MCP test on a clean build: the probe runner dropped its supervisor, so the timed-out probe with a failed stop was never stopped again and close succeeded. The probe result now carries the still-owned probe, the selection hands every retained probe to the launcher with the timeout reason intact, and the launcher retries their stop on every cleanup: a verified stop releases ownership, anything else is reported as the `hopper-private-display-probe` resource, composed into the startup failure as `cleanup_incomplete`, blocks restart and is retried by `close_binary`. With your single controlled failure the retry during startup cleanup already stops the probe (two stop calls, dead after close); a client test covers the repeated-failure path. No real Linux or Hopper run. Draft PR follows shortly.
Author
Owner

@XinwangTao commented on GitHub (Oct 16, 2026):

yankikucuk, your approach lines up with the contract — retaining the probe as an owned resource and letting the launcher retry stop on every cleanup is exactly the ownership-transfer fix, with cleanup_incomplete blocking restart and keeping close_binary retryable. Two acceptance points I would watch in the draft: (1) a second close_binary after cleanup_incomplete actually re-attempts stop and lands the probe exited, not just the startup-cleanup retry; (2) the retained probe resource shows up in the failure diagnostics so callers can act on it. Looking forward to the PR.

<!-- gh-comment-id:6208601344 --> @XinwangTao commented on GitHub (Oct 16, 2026): yankikucuk, your approach lines up with the contract — retaining the probe as an owned resource and letting the launcher retry stop on every cleanup is exactly the ownership-transfer fix, with cleanup_incomplete blocking restart and keeping close_binary retryable. Two acceptance points I would watch in the draft: (1) a second close_binary after cleanup_incomplete actually re-attempts stop and lands the probe exited, not just the startup-cleanup retry; (2) the retained probe resource shows up in the failure diagnostics so callers can act on it. Looking forward to the PR.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Timed-out probe cleanup is an AnalysisCleanupLease retaining the exact supervisor until stop succeeds. Private-display selection transfers a pending lease into launcher-owned probe resources and its startup failure; launcher cleanup retries that scope. The real owned probe regression denies initial stops repeatedly, requires cleanup failure to remain live, then requires exact process exit, cleared cleanup resources and correct retry count.

Implementation: LinuxPrivateDisplayProbe.ts, BridgeLauncher.ts.

The relevant regression checks in bridgeLauncher.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380767344 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Timed-out probe cleanup is an AnalysisCleanupLease retaining the exact supervisor until stop succeeds. Private-display selection transfers a pending lease into launcher-owned probe resources and its startup failure; launcher cleanup retries that scope. The real owned probe regression denies initial stops repeatedly, requires cleanup failure to remain live, then requires exact process exit, cleared cleanup resources and correct retry count. Implementation: [LinuxPrivateDisplayProbe.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/hopper/LinuxPrivateDisplayProbe.ts#L158), [BridgeLauncher.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/hopper/BridgeLauncher.ts#L584). The relevant regression checks in [bridgeLauncher.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/providers/hopper/bridgeLauncher.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#1581
No description provided.