[GH-ISSUE #2975] [Bug] macOS: after the Hopper app exits, the MCP session cannot open any target until the server restarts #1046

Open
opened 2026-10-15 18:30:38 +00:00 by bsakaguchi · 2 comments
Owner

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

Before submitting

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

Area

MCP or agent integration

Observed behavior

On macOS, once the Hopper application exits while an MCP session has a Hopper target open, that session cannot open any target again, for any provider, until the MCP server restarts. The remediations REA returns along the way point at a recovery that cannot succeed.

After the exit:

  1. The next analysis call that reaches Hopper returns provider_unavailable (retryable: true, retry_action: "restart_provider"), whose remediation says: "Restart the owned provider by calling close_binary, then open the target again and retry."
  2. close_binary returns cleanup_incomplete (retryable: false), listing hopper-document ("authenticated shutdown acknowledgement was not observed") and the private runtime directory, with the remediation "Review the reported local resources before opening another target."
  3. Every later open_binary returns the same cleanup_incomplete, including when called with provider_id: "ghidra"; the error still names "provider_id": "hopper" and "operation": "close_binary".

On main, close_binary and each open_binary wait 30 s before failing; on 6.4.0 they fail at once. A new rea mcp process opens and analyzes the same targets normally.

This follows the documented fail-closed rule (docs/installation.md#L460-L469): "After addressing the reported failure, retry close_binary on the same connection", "Unconfirmed document or process cleanup retains the target/application lease", and "If that bridge has disconnected, another close cannot confirm the document: inspect and close the reported document in Hopper before ending the owning REA connection." #1275 likewise keeps an external GUI document unconfirmed when only the owned helper stops. This report does not ask to relax that rule. It reports two gaps: once Hopper itself is gone there is no document left to inspect or close, yet nothing in REA can confirm it afterwards; and neither in-band remediation mentions ending the connection, the only step that recovers.

Reproduction

  1. macOS on Apple silicon, Hopper Disassembler 6.6.0-demo in /Applications. Start rea mcp (stdio) and initialize.
  2. open_binary {"path": "<arm64 Mach-O>", "provider_id": "hopper"}, then analyze_function on one function: Evidence.
  3. Terminate the Hopper app with kill <pid> (SIGTERM) on .../Hopper Disassembler.app/Contents/MacOS/Hopper Disassembler, and wait until the process is gone.
  4. analyze_function on another function: provider_unavailable.
  5. close_binary: cleanup_incomplete.
  6. open_binary with the same path and provider_id "hopper" or "ghidra", more than once: cleanup_incomplete every time.
  7. Kill the server and start a new rea mcp. open_binary and analyze_function on the same target: success.

Each run drove the servers over stdio from a small script and confirmed the Hopper process was gone before step 4:

REA step 6 calls runs step 4 step 5 step 6 (each call) step 7
6.4.0 (npm) hopper ×2 3 provider_unavailable 3/3 cleanup_incomplete 3/3, 0 s cleanup_incomplete 3/3, 0 s Evidence 3/3
6.4.0 (npm) ghidra ×2 3 provider_unavailable 3/3 cleanup_incomplete 3/3, 0 s cleanup_incomplete 3/3, 0 s Evidence 3/3
main ae9a7f34e hopper ×2 3 provider_unavailable 3/3 cleanup_incomplete 3/3, 30 s cleanup_incomplete 3/3, 30 s each Evidence 3/3
main 204a6f3f2 ghidra ×2 3 provider_unavailable 3/3 cleanup_incomplete 3/3, 30 s cleanup_incomplete 3/3, 30 s each Evidence 3/3
main ebaaa79ca ghidra ×2, then hopper 3 provider_unavailable 3/3 cleanup_incomplete 3/3, 30 s cleanup_incomplete 3/3, 30 s each Evidence 3/3

One further run on 204a6f3f2 was discarded because step 2 returned cleanup_incomplete after 120 s instead of Evidence. Two more runs on ebaaa79ca skipped step 5 and went straight to step 6: each open_binary still returned cleanup_incomplete after 30 s (2/2).

The exit in these runs was a forced termination. An AppleScript quit (osascript -e 'tell application "Hopper Disassembler" to quit') left Hopper running in 7/7 runs. In the one run whose osascript output and windows were read, it failed with an Apple event timeout (-1712) while Hopper showed REA's "Executing Python Script..." (see #494 for that long-running bridge script). A Cmd-Q was not tried.

The Hopper demo leads to the same exit. About 30 minutes after launch it shows a modal alert, "Demo period has expired. Document will be closed.", whose only button is OK. Hopper keeps running behind the alert (still up about 70 minutes after launch in one 6.4.0 run), and pressing OK quits it at once. Afterwards close_binary and an open_binary for Ghidra both returned cleanup_incomplete after 30 s (2/2 runs on 204a6f3f2, with the script pressing OK).

Expected behavior

Two parts:

  1. The Hopper remediations should cover this case. provider_unavailable cannot tell an exited Hopper from a lost transport, and when the transport is lost but the bridge still listens, cleanup through close is tested (hopperClient.handshakeRecovery.test.ts#L181-L206), so that advice can stay. It could add that if Hopper is no longer running, close_binary will not confirm cleanup, so the Evidence bundle should be exported first. When the retained Hopper document cannot be confirmed and its bridge is disconnected, the Hopper-specific details of cleanup_incomplete could say what the docs say: close the document in Hopper and then end the MCP connection; if Hopper is no longer running, end the connection. The shared message (providerCleanupError.ts#L10) need not change. The installation docs could add that the demo stops about 30 minutes after launch and that pressing OK on its alert quits Hopper, which ends Hopper's use in the current session.
  2. Add positive proof of exit to that rule. When REA can verify that the exact Hopper process that served the authenticated bridge has exited, its document cannot outlive it. REA should then treat the document as closed: release the private runtime directory and the target lease, clear the pending cleanup, and let the next open_binary start Hopper again. This extends to a post-handoff exit the invariant #2795 states for a failed launch: the session stays reusable.

Impact and scope

  • Affected: every macOS Hopper session over MCP, because every macOS launch is external-application (only the Linux demo owns the process lifetime; see the analysis below). For a long-lived MCP client, one Hopper exit blocks every provider (Ghidra shown above) until the client restarts its server.
  • With Hopper in demo mode, which REA's macOS setup lets the operator choose (installation.md#L321-L324), any MCP session that keeps a Hopper target open past Hopper's first 30 minutes or so ends this way once the alert's only button is pressed (2/2 runs).
  • Following the step 1 remediation clears the connection's Evidence ledger, as documented for close_binary (mcp-contracts.md#L273-L276), yet the session still cannot continue: get_evidence_bundle held 1 record before close_binary and 0 after (3/3 on ebaaa79ca). Skipping close_binary keeps the record since #2754 (1 after the failed opens, 2/2), but the session is just as stuck. A restarted server starts with a fresh ledger, so the records survive only if they are exported before the restart, and before any close_binary.
  • Not affected: the CLI, once the server holding the target has exited. The next rea function --provider hopper then started a new Hopper and returned Evidence (3 runs on 204a6f3f2, 5 on ebaaa79ca). While that server is alive it keeps the target lease, and a CLI call on the same target is refused as "already open in REA session …".
  • Evidence integrity and process isolation are not affected.

REA version

rea-agents 6.4.0 (npm), and main at ae9a7f34e, 204a6f3f2 and ebaaa79ca (npm ci --ignore-scripts && npm run compile); all reproduce. Between ae9a7f34e and ebaaa79ca, the only commit that touches the Hopper or session code cited below is af7638fa9 (#2754), which keeps the records after a failed open but does not change this outcome. Main has since moved to 7a6defab1; its two new commits touch none of the files cited here.

Runtime and platform

macOS 27.2 (26B5101f), arm64. Node.js 26.0.0. Hopper Disassembler 6.6.0-demo (build 42.6316032). Ghidra 12.1.4 for the Ghidra target. rea ran through a local shell wrapper that exports GHIDRA_INSTALL_DIR, JAVA_HOME (arm64 JDK 21), REA_ANALYSIS_PROVIDER=ghidra and REA_IDA_MCP_CONFIG before exec-ing REA. The wrapper can also stop a Hopper process older than a set age; no Hopper in these runs reached that age.

Target and artifact context

Any target. Reproduced with a small arm64 Mach-O compiled locally.

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

Step 4 on ebaaa79ca, identical on 204a6f3f2 (a function not analyzed earlier in the session, so the call reaches Hopper):

{"error":{"code":"provider_unavailable","category":"unavailable","message":"Hopper became unreachable during analyze_function; its process exit was not observed. Restart the owned provider by calling close_binary, then open the target again and retry. Request id 3.","retryable":true,"remediation":{"action":"Restart the owned provider by calling close_binary, then open the target again and retry."},"details":{"exit_code":null,"stage":"analysis","provider_state":"unreachable","retry_action":"restart_provider","operation":"analyze_function","request_id":3}}}

Step 5 on ebaaa79ca, identical on ae9a7f34e (runtime path redacted); every open_binary at step 6 returns the same error:

{"error":{"code":"cleanup_incomplete","category":"execution_failure","message":"Provider cleanup could not be fully confirmed. Review the reported local resources before opening another target.","retryable":false,"remediation":{"action":"Provider cleanup could not be fully confirmed. Review the reported local resources before opening another target."},"details":{"provider_id":"hopper","operation":"close_binary","cleanup":"incomplete","resources":["hopper-document","/tmp/rea-XXXXXX"],"diagnostics":{"issues":[{"resource":"hopper-document","reason":"authenticated shutdown acknowledgement was not observed"},{"resource":"/tmp/rea-XXXXXX","reason":"Private runtime retained because process or native document closure was not confirmed"}],"runtime_root":"/tmp/rea-XXXXXX"},"execution_failure":"execution_failure"}}}

Typed outcome or recovery expected

After a verified exit: close_binary succeeds, and open_binary succeeds for Hopper (relaunching it) and for other providers. Without one: retained cleanup as today, with Hopper cleanup_incomplete details that name restarting the MCP connection when the bridge is disconnected and Hopper is not running, and a Hopper provider_unavailable remediation that says to export the Evidence bundle before close_binary if Hopper may have exited.

Analysis or suspected cause

Source-derived, at ebaaa79ca; not confirmed by instrumentation.

  • On macOS the launch is external-application with bridge-request shutdown, because const ownsProcessLifetime = usesLinuxDemo(this.options) (BridgeLauncher.ts#L165) is false there (#L250-L259). REA owns the launcher helper, not the app.
  • For that lifetime, the document counts as closed only after an authenticated shutdown acknowledgement (HopperCleanup.ts#L162-L166; otherwise it is recorded as unconfirmed, #L291-L316); without one, the runtime root (#L89-L102) and the lease (#L106-L118) are retained. After the app exits, #reconnectForCleanup (HopperClient.ts#L549) retries bridge.sock within HOPPER_SHUTDOWN_TIMEOUT_MS (30 s), the retry #1866 added; this is the 30 s per call on main, and 6.4.0 predates it. Nothing can answer, so the acknowledgement never arrives.
  • open_binary retries that pending cleanup first and returns its error (BinarySession.ts#L265-L266) whatever provider is requested, which explains the Ghidra rows. When close_binary is skipped, the first open_binary fails the same way while retiring the previous client (#L280-L291), and later ones at the pending retry.
  • The bridge's authenticated health reply carries name, version and run_id but no process identity (hopper_bridge.py#L732-L733), so REA cannot tell an exited app from a lost transport. The tests cover a helper exit that does not prove a GUI exit (hopperClient.helperFailure.test.ts#L82) and a lost transport whose app is still alive (hopperClient.handshakeRecovery.test.ts#L181), but not an exited app.

A possible fix that keeps cleanup fail-closed and reuses src/process identity, as AGENTS.md asks:

  • The bridge reports os.getpid() in its authenticated health reply. The bridge's Python runs inside the Hopper process: lsof shows the Python framework mapped into the Hopper app process, which also holds the bridge.sock descriptors.
  • At the handshake, REA records that process's start identity with observeProcessStartIdentity (ProcessOwnershipObservation.ts#L458); on Darwin that identity is the kp_proc.p_starttime second and microsecond pair.
  • On transport loss, REA confirms the document closed only when that exact identity is gone. An unreadable process table keeps everything retained, as now. On Darwin the start identity comes from the Swift helper that every Hopper launch there already compiles before it spawns the launcher (ProviderProcess.ts#L174, ProcessOwnershipObservation.ts#L362-L363); a compile failure already fails the launch, so the fix adds no new dependency. The denied-launch path already confirms shutdown on positive evidence (HopperClient.ts#L760-L764). The executable-path probe in BridgeLauncher.ts treats a failed probe as "not running", so it should not be reused here.

Additional context

Related but not duplicates:

  • #2795 (open PR): the same symptom from a launch that fails before handoff.
  • #1808 (open): a failed open_binary during startup leaves the session unusable.
  • #1817 and #1866: the cleanup reconnect, and its retry for a late bridge within 30 s.
  • #340 (open): real-Hopper shutdown verification.
  • #1275 (merged): stopping only the owned helper leaves an external GUI document unconfirmed.
  • #2459 (closed): the reverse Linux case, where the app outlives its owner.
  • #2753 (closed issue) and #2754 (merged PR): records now survive a failed open_binary; the session above still cannot open a target.

Proposed regression test, next to hopperClient.handshakeRecovery.test.ts: an external-application fixture whose app process exits after a successful health. close() then succeeds without an acknowledgement, the runtime directory and lease are released, and a later open_binary for Hopper or another provider succeeds in the same session. A second case keeps the process alive, or makes its identity unreadable, and expects today's retained cleanup.

Originally created by @dogskip on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2975 ### 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 MCP or agent integration ### Observed behavior On macOS, once the Hopper application exits while an MCP session has a Hopper target open, that session cannot open any target again, for any provider, until the MCP server restarts. The remediations REA returns along the way point at a recovery that cannot succeed. After the exit: 1. The next analysis call that reaches Hopper returns `provider_unavailable` (`retryable: true`, `retry_action: "restart_provider"`), whose remediation says: "Restart the owned provider by calling close_binary, then open the target again and retry." 2. `close_binary` returns `cleanup_incomplete` (`retryable: false`), listing `hopper-document` ("authenticated shutdown acknowledgement was not observed") and the private runtime directory, with the remediation "Review the reported local resources before opening another target." 3. Every later `open_binary` returns the same `cleanup_incomplete`, including when called with `provider_id: "ghidra"`; the error still names `"provider_id": "hopper"` and `"operation": "close_binary"`. On main, `close_binary` and each `open_binary` wait 30 s before failing; on 6.4.0 they fail at once. A new `rea mcp` process opens and analyzes the same targets normally. This follows the documented fail-closed rule ([`docs/installation.md#L460-L469`](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/docs/installation.md#L460-L469)): "After addressing the reported failure, retry `close_binary` on the same connection", "Unconfirmed document or process cleanup retains the target/application lease", and "If that bridge has disconnected, another close cannot confirm the document: inspect and close the reported document in Hopper before ending the owning REA connection." #1275 likewise keeps an external GUI document unconfirmed when only the owned helper stops. This report does not ask to relax that rule. It reports two gaps: once Hopper itself is gone there is no document left to inspect or close, yet nothing in REA can confirm it afterwards; and neither in-band remediation mentions ending the connection, the only step that recovers. ### Reproduction 1. macOS on Apple silicon, Hopper Disassembler 6.6.0-demo in `/Applications`. Start `rea mcp` (stdio) and initialize. 2. `open_binary {"path": "<arm64 Mach-O>", "provider_id": "hopper"}`, then `analyze_function` on one function: Evidence. 3. Terminate the Hopper app with `kill <pid>` (SIGTERM) on `.../Hopper Disassembler.app/Contents/MacOS/Hopper Disassembler`, and wait until the process is gone. 4. `analyze_function` on another function: `provider_unavailable`. 5. `close_binary`: `cleanup_incomplete`. 6. `open_binary` with the same path and `provider_id` `"hopper"` or `"ghidra"`, more than once: `cleanup_incomplete` every time. 7. Kill the server and start a new `rea mcp`. `open_binary` and `analyze_function` on the same target: success. Each run drove the servers over stdio from a small script and confirmed the Hopper process was gone before step 4: | REA | step 6 calls | runs | step 4 | step 5 | step 6 (each call) | step 7 | | --- | --- | --- | --- | --- | --- | --- | | 6.4.0 (npm) | hopper ×2 | 3 | `provider_unavailable` 3/3 | `cleanup_incomplete` 3/3, 0 s | `cleanup_incomplete` 3/3, 0 s | Evidence 3/3 | | 6.4.0 (npm) | ghidra ×2 | 3 | `provider_unavailable` 3/3 | `cleanup_incomplete` 3/3, 0 s | `cleanup_incomplete` 3/3, 0 s | Evidence 3/3 | | main ae9a7f34e | hopper ×2 | 3 | `provider_unavailable` 3/3 | `cleanup_incomplete` 3/3, 30 s | `cleanup_incomplete` 3/3, 30 s each | Evidence 3/3 | | main 204a6f3f2 | ghidra ×2 | 3 | `provider_unavailable` 3/3 | `cleanup_incomplete` 3/3, 30 s | `cleanup_incomplete` 3/3, 30 s each | Evidence 3/3 | | main ebaaa79ca | ghidra ×2, then hopper | 3 | `provider_unavailable` 3/3 | `cleanup_incomplete` 3/3, 30 s | `cleanup_incomplete` 3/3, 30 s each | Evidence 3/3 | One further run on 204a6f3f2 was discarded because step 2 returned `cleanup_incomplete` after 120 s instead of Evidence. Two more runs on ebaaa79ca skipped step 5 and went straight to step 6: each `open_binary` still returned `cleanup_incomplete` after 30 s (2/2). The exit in these runs was a forced termination. An AppleScript quit (`osascript -e 'tell application "Hopper Disassembler" to quit'`) left Hopper running in 7/7 runs. In the one run whose `osascript` output and windows were read, it failed with an Apple event timeout (-1712) while Hopper showed REA's "Executing Python Script..." (see #494 for that long-running bridge script). A Cmd-Q was not tried. The Hopper demo leads to the same exit. About 30 minutes after launch it shows a modal alert, "Demo period has expired. Document will be closed.", whose only button is OK. Hopper keeps running behind the alert (still up about 70 minutes after launch in one 6.4.0 run), and pressing OK quits it at once. Afterwards `close_binary` and an `open_binary` for Ghidra both returned `cleanup_incomplete` after 30 s (2/2 runs on 204a6f3f2, with the script pressing OK). ### Expected behavior Two parts: 1. The Hopper remediations should cover this case. `provider_unavailable` cannot tell an exited Hopper from a lost transport, and when the transport is lost but the bridge still listens, cleanup through close is tested ([hopperClient.handshakeRecovery.test.ts#L181-L206](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/tests/boundary/providers/hopper/hopperClient.handshakeRecovery.test.ts#L181-L206)), so that advice can stay. It could add that if Hopper is no longer running, `close_binary` will not confirm cleanup, so the Evidence bundle should be exported first. When the retained Hopper document cannot be confirmed and its bridge is disconnected, the Hopper-specific details of `cleanup_incomplete` could say what the docs say: close the document in Hopper and then end the MCP connection; if Hopper is no longer running, end the connection. The shared message ([providerCleanupError.ts#L10](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/domain/providerCleanupError.ts#L10)) need not change. The installation docs could add that the demo stops about 30 minutes after launch and that pressing OK on its alert quits Hopper, which ends Hopper's use in the current session. 2. Add positive proof of exit to that rule. When REA can verify that the exact Hopper process that served the authenticated bridge has exited, its document cannot outlive it. REA should then treat the document as closed: release the private runtime directory and the target lease, clear the pending cleanup, and let the next `open_binary` start Hopper again. This extends to a post-handoff exit the invariant #2795 states for a failed launch: the session stays reusable. ### Impact and scope - Affected: every macOS Hopper session over MCP, because every macOS launch is `external-application` (only the Linux demo owns the process lifetime; see the analysis below). For a long-lived MCP client, one Hopper exit blocks every provider (Ghidra shown above) until the client restarts its server. - With Hopper in demo mode, which REA's macOS setup lets the operator choose ([installation.md#L321-L324](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/docs/installation.md#L321-L324)), any MCP session that keeps a Hopper target open past Hopper's first 30 minutes or so ends this way once the alert's only button is pressed (2/2 runs). - Following the step 1 remediation clears the connection's Evidence ledger, as documented for `close_binary` ([mcp-contracts.md#L273-L276](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/docs/mcp-contracts.md#L273-L276)), yet the session still cannot continue: `get_evidence_bundle` held 1 record before `close_binary` and 0 after (3/3 on ebaaa79ca). Skipping `close_binary` keeps the record since #2754 (1 after the failed opens, 2/2), but the session is just as stuck. A restarted server starts with a fresh ledger, so the records survive only if they are exported before the restart, and before any `close_binary`. - Not affected: the CLI, once the server holding the target has exited. The next `rea function --provider hopper` then started a new Hopper and returned Evidence (3 runs on 204a6f3f2, 5 on ebaaa79ca). While that server is alive it keeps the target lease, and a CLI call on the same target is refused as "already open in REA session …". - Evidence integrity and process isolation are not affected. ### REA version rea-agents 6.4.0 (npm), and main at ae9a7f34e, 204a6f3f2 and ebaaa79ca (`npm ci --ignore-scripts && npm run compile`); all reproduce. Between ae9a7f34e and ebaaa79ca, the only commit that touches the Hopper or session code cited below is af7638fa9 (#2754), which keeps the records after a failed open but does not change this outcome. Main has since moved to 7a6defab1; its two new commits touch none of the files cited here. ### Runtime and platform macOS 27.2 (26B5101f), arm64. Node.js 26.0.0. Hopper Disassembler 6.6.0-demo (build 42.6316032). Ghidra 12.1.4 for the Ghidra target. `rea` ran through a local shell wrapper that exports `GHIDRA_INSTALL_DIR`, `JAVA_HOME` (arm64 JDK 21), `REA_ANALYSIS_PROVIDER=ghidra` and `REA_IDA_MCP_CONFIG` before `exec`-ing REA. The wrapper can also stop a Hopper process older than a set age; no Hopper in these runs reached that age. ### Target and artifact context Any target. Reproduced with a small arm64 Mach-O compiled locally. ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence Step 4 on ebaaa79ca, identical on 204a6f3f2 (a function not analyzed earlier in the session, so the call reaches Hopper): ```json {"error":{"code":"provider_unavailable","category":"unavailable","message":"Hopper became unreachable during analyze_function; its process exit was not observed. Restart the owned provider by calling close_binary, then open the target again and retry. Request id 3.","retryable":true,"remediation":{"action":"Restart the owned provider by calling close_binary, then open the target again and retry."},"details":{"exit_code":null,"stage":"analysis","provider_state":"unreachable","retry_action":"restart_provider","operation":"analyze_function","request_id":3}}} ``` Step 5 on ebaaa79ca, identical on ae9a7f34e (runtime path redacted); every `open_binary` at step 6 returns the same error: ```json {"error":{"code":"cleanup_incomplete","category":"execution_failure","message":"Provider cleanup could not be fully confirmed. Review the reported local resources before opening another target.","retryable":false,"remediation":{"action":"Provider cleanup could not be fully confirmed. Review the reported local resources before opening another target."},"details":{"provider_id":"hopper","operation":"close_binary","cleanup":"incomplete","resources":["hopper-document","/tmp/rea-XXXXXX"],"diagnostics":{"issues":[{"resource":"hopper-document","reason":"authenticated shutdown acknowledgement was not observed"},{"resource":"/tmp/rea-XXXXXX","reason":"Private runtime retained because process or native document closure was not confirmed"}],"runtime_root":"/tmp/rea-XXXXXX"},"execution_failure":"execution_failure"}}} ``` ### Typed outcome or recovery expected After a verified exit: `close_binary` succeeds, and `open_binary` succeeds for Hopper (relaunching it) and for other providers. Without one: retained cleanup as today, with Hopper `cleanup_incomplete` details that name restarting the MCP connection when the bridge is disconnected and Hopper is not running, and a Hopper `provider_unavailable` remediation that says to export the Evidence bundle before `close_binary` if Hopper may have exited. ### Analysis or suspected cause Source-derived, at ebaaa79ca; not confirmed by instrumentation. - On macOS the launch is `external-application` with `bridge-request` shutdown, because `const ownsProcessLifetime = usesLinuxDemo(this.options)` ([BridgeLauncher.ts#L165](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/hopper/BridgeLauncher.ts#L165)) is false there ([#L250-L259](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/hopper/BridgeLauncher.ts#L250-L259)). REA owns the launcher helper, not the app. - For that lifetime, the document counts as closed only after an authenticated shutdown acknowledgement ([HopperCleanup.ts#L162-L166](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/hopper/HopperCleanup.ts#L162-L166); otherwise it is recorded as unconfirmed, [#L291-L316](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/hopper/HopperCleanup.ts#L291-L316)); without one, the runtime root ([#L89-L102](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/hopper/HopperCleanup.ts#L89-L102)) and the lease ([#L106-L118](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/hopper/HopperCleanup.ts#L106-L118)) are retained. After the app exits, `#reconnectForCleanup` ([HopperClient.ts#L549](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/hopper/HopperClient.ts#L549)) retries `bridge.sock` within `HOPPER_SHUTDOWN_TIMEOUT_MS` (30 s), the retry #1866 added; this is the 30 s per call on main, and 6.4.0 predates it. Nothing can answer, so the acknowledgement never arrives. - `open_binary` retries that pending cleanup first and returns its error ([BinarySession.ts#L265-L266](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/application/binary/BinarySession.ts#L265-L266)) whatever provider is requested, which explains the Ghidra rows. When `close_binary` is skipped, the first `open_binary` fails the same way while retiring the previous client ([#L280-L291](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/application/binary/BinarySession.ts#L280-L291)), and later ones at the pending retry. - The bridge's authenticated `health` reply carries `name`, `version` and `run_id` but no process identity ([hopper_bridge.py#L732-L733](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/bridge/hopper_bridge.py#L732-L733)), so REA cannot tell an exited app from a lost transport. The tests cover a helper exit that does not prove a GUI exit ([hopperClient.helperFailure.test.ts#L82](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/tests/boundary/providers/hopper/hopperClient.helperFailure.test.ts#L82)) and a lost transport whose app is still alive ([hopperClient.handshakeRecovery.test.ts#L181](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/tests/boundary/providers/hopper/hopperClient.handshakeRecovery.test.ts#L181)), but not an exited app. A possible fix that keeps cleanup fail-closed and reuses `src/process` identity, as AGENTS.md asks: - The bridge reports `os.getpid()` in its authenticated `health` reply. The bridge's Python runs inside the Hopper process: `lsof` shows the Python framework mapped into the Hopper app process, which also holds the `bridge.sock` descriptors. - At the handshake, REA records that process's start identity with `observeProcessStartIdentity` ([ProcessOwnershipObservation.ts#L458](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/process/ProcessOwnershipObservation.ts#L458)); on Darwin that identity is the `kp_proc.p_starttime` second and microsecond pair. - On transport loss, REA confirms the document closed only when that exact identity is gone. An unreadable process table keeps everything retained, as now. On Darwin the start identity comes from the Swift helper that every Hopper launch there already compiles before it spawns the launcher ([ProviderProcess.ts#L174](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/process/ProviderProcess.ts#L174), [ProcessOwnershipObservation.ts#L362-L363](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/process/ProcessOwnershipObservation.ts#L362-L363)); a compile failure already fails the launch, so the fix adds no new dependency. The denied-launch path already confirms shutdown on positive evidence ([HopperClient.ts#L760-L764](https://github.com/morluto/rea/blob/ebaaa79ca02bde4a73013a2a9a781d0b07a57ee4/src/hopper/HopperClient.ts#L760-L764)). The executable-path probe in `BridgeLauncher.ts` treats a failed probe as "not running", so it should not be reused here. ### Additional context Related but not duplicates: - #2795 (open PR): the same symptom from a launch that fails before handoff. - #1808 (open): a failed `open_binary` during startup leaves the session unusable. - #1817 and #1866: the cleanup reconnect, and its retry for a late bridge within 30 s. - #340 (open): real-Hopper shutdown verification. - #1275 (merged): stopping only the owned helper leaves an external GUI document unconfirmed. - #2459 (closed): the reverse Linux case, where the app outlives its owner. - #2753 (closed issue) and #2754 (merged PR): records now survive a failed `open_binary`; the session above still cannot open a target. Proposed regression test, next to `hopperClient.handshakeRecovery.test.ts`: an `external-application` fixture whose app process exits after a successful `health`. `close()` then succeeds without an acknowledgement, the runtime directory and lease are released, and a later `open_binary` for Hopper or another provider succeeds in the same session. A second case keeps the process alive, or makes its identity unreadable, and expects today's retained cleanup.
bsakaguchi 2026-10-15 18:30:38 +00:00
Author
Owner

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

Consolidating my duplicate report #3269 here.

Additional report context: rea-agents 6.4.0, Hopper 6.6.0, macOS 27.0.1 arm64, native launch. After opening a small Mach-O and confirming a successful query, terminating Hopper caused close_binary and subsequent open_binary calls to return cleanup_incomplete until the MCP server restarted.

My report traces the retained state through HopperCleanup and BinarySession pending cleanup. It also connects this recovery failure with the startup-timeout scenario in #3268: terminating a timed-out Hopper instance can leave the same session blocked.

Correction to my proposed fix: processStopped alone is insufficient if it describes the launcher/helper. Cleanup must require positive proof that the exact external Hopper process serving the authenticated bridge has exited, preserving retained cleanup when that identity cannot be verified. This aligns with the approach already described here.

The complete original report and version-specific references remain available in #3269.

<!-- gh-comment-id:6163065694 --> @trigger2k20 commented on GitHub (Oct 14, 2026): Consolidating my duplicate report #3269 here. Additional report context: rea-agents 6.4.0, Hopper 6.6.0, macOS 27.0.1 arm64, native launch. After opening a small Mach-O and confirming a successful query, terminating Hopper caused close_binary and subsequent open_binary calls to return cleanup_incomplete until the MCP server restarted. My report traces the retained state through HopperCleanup and BinarySession pending cleanup. It also connects this recovery failure with the startup-timeout scenario in #3268: terminating a timed-out Hopper instance can leave the same session blocked. Correction to my proposed fix: processStopped alone is insufficient if it describes the launcher/helper. Cleanup must require positive proof that the exact external Hopper process serving the authenticated bridge has exited, preserving retained cleanup when that identity cannot be verified. This aligns with the approach already described here. The complete original report and version-specific references remain available in #3269.
Author
Owner

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

Delivered on main in fffb07419.

External Hopper exit could strand retained session ownership. Authenticated bridge process identity and fresh shared process observations now distinguish confirmed exit from a live application with lost transport, retaining resources when cleanup is unresolved.

Validation: Hopper external-exit recovery and live/lost-transport negative controls. No real Hopper GUI engine was launched. Full exact-revision qualification passed.

<!-- gh-comment-id:6177584357 --> @morluto commented on GitHub (Oct 14, 2026): Delivered on main in [fffb07419](https://github.com/morluto/rea/commit/fffb0741936067530d1ce9777461cd53e87398c2). External Hopper exit could strand retained session ownership. Authenticated bridge process identity and fresh shared process observations now distinguish confirmed exit from a live application with lost transport, retaining resources when cleanup is unresolved. Validation: Hopper external-exit recovery and live/lost-transport negative controls. No real Hopper GUI engine was launched. Full exact-revision [qualification](https://github.com/morluto/rea/actions/runs/38629313022) passed.
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#1046
No description provided.