[GH-ISSUE #4375] [Bug] Hopper startup diagnostics disappear beside generic error diagnostics #1734

Closed
opened 2026-10-16 21:09:36 +00:00 by bsakaguchi · 1 comment
Owner

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

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

SOURCE-BOUNDARY FINDING: A correctly shaped, production-parser-accepted Hopper private-display failure diagnostic disappears from projectAnalysisError when the same HopperProcessError also has provider-neutral diagnostics. The original error still holds its provider diagnostic; the serialized projection loses it. Both direct supported constructor input and retainSecondaryFailure enrichment exhibit the loss. No real Hopper startup producing both fields was observed.

Reproduction

This is an executed production-parser/error-projection boundary probe on both source baselines, not a real Hopper session or compiled CLI/MCP reproduction. Prepare separate checkouts of current main and the release baseline and run npm ci in each with Node 24.18.0. Current main supplies the locked tsx runner; the release baseline does not include tsx. Save the following as repro.mts outside both checkouts. Set REA_TSX to the absolute path of node_modules/.bin/tsx in the current-main checkout, then run this from each checkout root using that same runner:

REA_CHECKOUT="$PWD" "$REA_TSX" /path/to/repro.mts
import assert from "node:assert/strict";
import { pathToFileURL } from "node:url";
import { resolve } from "node:path";
const root = process.env.REA_CHECKOUT!;
const load = (p: string) => import(pathToFileURL(resolve(root, p)).href);
const { HopperProcessError } = await load("src/domain/hopperErrors.ts");
const { projectAnalysisError } = await load("src/domain/analysisErrorProjection.ts");
const { parseLinuxPrivateDisplayDiagnostic } = await load("src/hopper/LinuxPrivateDisplayDiagnostic.ts");
const fixture = { component: "hopper_private_display", operation: "launch", reason: "synthetic_failure", socket_directory: "/tmp/.X11-unix", socket_directory_mode: null, mount_read_only: null, effective_socket_directory_mode: null, effective_mount_read_only: null, wsl: false, strategy: "unavailable", fallback_reason: null, xvfb_stderr_bytes: 0, status: "error", failure_code: "private_display_unavailable" };
const parsed = parseLinuxPrivateDisplayDiagnostic("REA_X11_DIAGNOSTIC=" + JSON.stringify(fixture));
assert.equal(parsed.ok, true);
if (!parsed.ok || parsed.value.status !== "error") throw new Error("Invalid failure diagnostic");
const diagnostic = parsed.value;
const control = projectAnalysisError(new HopperProcessError(70, { diagnostic }));
assert.deepEqual(control.details.diagnostics, diagnostic);
const primary = new HopperProcessError(70, { diagnostic, diagnostics: { caller_context: "retained" } });
const mixed = projectAnalysisError(primary);
assert.deepEqual(mixed.details.diagnostics, { caller_context: "retained" });
assert.deepEqual(primary.diagnostic, diagnostic);
assert.equal(JSON.stringify(mixed).includes("hopper_private_display"), false);
const enriched = new HopperProcessError(70, { diagnostic }).retainSecondaryFailure(new HopperProcessError(null), { retirement_error: "synthetic_secondary_failure" });
const secondary = projectAnalysisError(enriched);
assert.deepEqual(secondary.details.diagnostics, { retirement_error: "synthetic_secondary_failure" });
assert.equal(JSON.stringify(secondary).includes("hopper_private_display"), false);
console.log(JSON.stringify({ parser_accepted: true, control_retained: true, mixed_diagnostics: mixed.details.diagnostics, secondary_diagnostics: secondary.details.diagnostics, provider_diagnostic_lost: true, code: mixed.code }));

Each baseline ran once, exited 0, and printed the result shown below. The provider-only control retains the diagnostic. The mixed constructor case and secondary-failure enrichment both lose it. No native app, display, target binary, or provider process is started.

Expected behavior

Retain both the parsed Hopper startup diagnostic and provider-neutral diagnostic context through typed projection. AnalysisErrorOptions explicitly describes its diagnostics as caller-safe facts retained through every typed projection, while the Hopper projection explicitly includes the provider startup diagnostic. Preserve the existing provider_unavailable classification and the provider-only control.

Impact and scope

The bounded effect is loss of structured provider-startup facts in the projected error while the error code and generic context remain. Frequency and reach in real CLI/MCP sessions are NOT_ENOUGH_INFO: no real Hopper session producing both diagnostic fields was run. No source-input loss, cleanup behavior change, or security impact is claimed. No workaround was exercised.

REA version

Current main source 8ab5024397ddbd3b37ab68a13cabedc2eb7ce7dd; latest published rea-agents 6.5.0 source 3b79af28e80785afce9f5998d8f03289402a1847.

Runtime and platform

Node v24.18.0; macOS arm64. No native provider was started.

Target and artifact context

Synthetic in-memory startup diagnostic and errors; no binary, display, Hopper installation, or provider process.

Interface

Other

Earliest failure stage

Transport or result projection

Relevant output or evidence

Executed output on each source baseline:

{"parser_accepted":true,"control_retained":true,"mixed_diagnostics":{"caller_context":"retained"},"secondary_diagnostics":{"retirement_error":"synthetic_secondary_failure"},"provider_diagnostic_lost":true,"code":"provider_unavailable"}

The diagnostic parser accepts the fixture through its strict schema. The Hopper error context supports both diagnostic and inherited generic options. The provider-neutral contract and enrichment require retained facts. The projection applies generic diagnostics after provider details; its Hopper branch at lines 398–415 supplies the startup diagnostic under the same key. The production Hopper startup producer creates HopperProcessError with the launcher diagnostic. Producer reachability was inspected statically; the synthetic diagnostic was parsed and projected in process.

Typed outcome or recovery expected

With the same parser-accepted input, retain the provider startup fields and generic caller/secondary context in the caller-visible projection. Keep the provider-only control intact and the error classification unchanged. The storage shape is an implementation choice. Proposed further verification, not executed: exercise a real Hopper failure with both contexts through CLI/MCP and check the final delivered error.

Analysis or suspected cause

Source-derived: providerErrorDetails puts error.diagnostic in details.diagnostics, then the outer projection overwrites that entire key with error.diagnostics. This explains the boundary result. The constructor explicitly supports both fields. Secondary-failure enrichment is another supported way to add generic context; this report does not establish which real Hopper workflow, if any, currently produces that combination.

Additional context

Related reports were checked: #4226 concerns pre-handshake recovery/session state, #3985 concerns private-display probe cleanup ownership, #4240/#3935 and merged #4238 concern cleanup composition at other boundaries, and #641/merged #739 concern preserving shutdown rejection causes. None describes this domain projection key collision. No last-known-good revision or regression claim is available.

Triage assessment

  • Impact level: P2
  • Assessment status: provisional
  • Impact basis: The executed boundary probe loses structured startup facts while retaining the error code and generic context; real-session trigger frequency and reach are unmeasured.
  • Workaround status: unverified
  • Workaround basis: No caller workaround was exercised; the original in-memory error retains the diagnostic, but its availability to CLI/MCP users was not established.
Originally created by @coygeek on GitHub (Oct 16, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4375 ### 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 SOURCE-BOUNDARY FINDING: A correctly shaped, production-parser-accepted Hopper private-display failure diagnostic disappears from `projectAnalysisError` when the same `HopperProcessError` also has provider-neutral `diagnostics`. The original error still holds its provider diagnostic; the serialized projection loses it. Both direct supported constructor input and `retainSecondaryFailure` enrichment exhibit the loss. No real Hopper startup producing both fields was observed. ### Reproduction This is an executed production-parser/error-projection boundary probe on both source baselines, not a real Hopper session or compiled CLI/MCP reproduction. Prepare separate checkouts of current main and the release baseline and run `npm ci` in each with Node 24.18.0. Current main supplies the locked tsx runner; the release baseline does not include tsx. Save the following as `repro.mts` outside both checkouts. Set `REA_TSX` to the absolute path of `node_modules/.bin/tsx` in the current-main checkout, then run this from each checkout root using that same runner: ```sh REA_CHECKOUT="$PWD" "$REA_TSX" /path/to/repro.mts ``` ```ts import assert from "node:assert/strict"; import { pathToFileURL } from "node:url"; import { resolve } from "node:path"; const root = process.env.REA_CHECKOUT!; const load = (p: string) => import(pathToFileURL(resolve(root, p)).href); const { HopperProcessError } = await load("src/domain/hopperErrors.ts"); const { projectAnalysisError } = await load("src/domain/analysisErrorProjection.ts"); const { parseLinuxPrivateDisplayDiagnostic } = await load("src/hopper/LinuxPrivateDisplayDiagnostic.ts"); const fixture = { component: "hopper_private_display", operation: "launch", reason: "synthetic_failure", socket_directory: "/tmp/.X11-unix", socket_directory_mode: null, mount_read_only: null, effective_socket_directory_mode: null, effective_mount_read_only: null, wsl: false, strategy: "unavailable", fallback_reason: null, xvfb_stderr_bytes: 0, status: "error", failure_code: "private_display_unavailable" }; const parsed = parseLinuxPrivateDisplayDiagnostic("REA_X11_DIAGNOSTIC=" + JSON.stringify(fixture)); assert.equal(parsed.ok, true); if (!parsed.ok || parsed.value.status !== "error") throw new Error("Invalid failure diagnostic"); const diagnostic = parsed.value; const control = projectAnalysisError(new HopperProcessError(70, { diagnostic })); assert.deepEqual(control.details.diagnostics, diagnostic); const primary = new HopperProcessError(70, { diagnostic, diagnostics: { caller_context: "retained" } }); const mixed = projectAnalysisError(primary); assert.deepEqual(mixed.details.diagnostics, { caller_context: "retained" }); assert.deepEqual(primary.diagnostic, diagnostic); assert.equal(JSON.stringify(mixed).includes("hopper_private_display"), false); const enriched = new HopperProcessError(70, { diagnostic }).retainSecondaryFailure(new HopperProcessError(null), { retirement_error: "synthetic_secondary_failure" }); const secondary = projectAnalysisError(enriched); assert.deepEqual(secondary.details.diagnostics, { retirement_error: "synthetic_secondary_failure" }); assert.equal(JSON.stringify(secondary).includes("hopper_private_display"), false); console.log(JSON.stringify({ parser_accepted: true, control_retained: true, mixed_diagnostics: mixed.details.diagnostics, secondary_diagnostics: secondary.details.diagnostics, provider_diagnostic_lost: true, code: mixed.code })); ``` Each baseline ran once, exited 0, and printed the result shown below. The provider-only control retains the diagnostic. The mixed constructor case and secondary-failure enrichment both lose it. No native app, display, target binary, or provider process is started. ### Expected behavior Retain both the parsed Hopper startup diagnostic and provider-neutral diagnostic context through typed projection. `AnalysisErrorOptions` explicitly describes its diagnostics as caller-safe facts retained through every typed projection, while the Hopper projection explicitly includes the provider startup diagnostic. Preserve the existing `provider_unavailable` classification and the provider-only control. ### Impact and scope The bounded effect is loss of structured provider-startup facts in the projected error while the error code and generic context remain. Frequency and reach in real CLI/MCP sessions are NOT_ENOUGH_INFO: no real Hopper session producing both diagnostic fields was run. No source-input loss, cleanup behavior change, or security impact is claimed. No workaround was exercised. ### REA version Current main source `8ab5024397ddbd3b37ab68a13cabedc2eb7ce7dd`; latest published rea-agents 6.5.0 source `3b79af28e80785afce9f5998d8f03289402a1847`. ### Runtime and platform Node v24.18.0; macOS arm64. No native provider was started. ### Target and artifact context Synthetic in-memory startup diagnostic and errors; no binary, display, Hopper installation, or provider process. ### Interface Other ### Earliest failure stage Transport or result projection ### Relevant output or evidence Executed output on each source baseline: ```json {"parser_accepted":true,"control_retained":true,"mixed_diagnostics":{"caller_context":"retained"},"secondary_diagnostics":{"retirement_error":"synthetic_secondary_failure"},"provider_diagnostic_lost":true,"code":"provider_unavailable"} ``` The [diagnostic parser](https://github.com/morluto/rea/blob/8ab5024397ddbd3b37ab68a13cabedc2eb7ce7dd/src/hopper/LinuxPrivateDisplayDiagnostic.ts#L11-L88) accepts the fixture through its strict schema. The [Hopper error context](https://github.com/morluto/rea/blob/8ab5024397ddbd3b37ab68a13cabedc2eb7ce7dd/src/domain/hopperErrors.ts#L104-L132) supports both `diagnostic` and inherited generic options. The [provider-neutral contract and enrichment](https://github.com/morluto/rea/blob/8ab5024397ddbd3b37ab68a13cabedc2eb7ce7dd/src/domain/analysisErrorBase.ts#L54-L108) require retained facts. The [projection](https://github.com/morluto/rea/blob/8ab5024397ddbd3b37ab68a13cabedc2eb7ce7dd/src/domain/analysisErrorProjection.ts#L46-L57) applies generic diagnostics after provider details; its Hopper branch at lines 398–415 supplies the startup diagnostic under the same key. The [production Hopper startup producer](https://github.com/morluto/rea/blob/8ab5024397ddbd3b37ab68a13cabedc2eb7ce7dd/src/hopper/HopperClient.ts#L875-L894) creates `HopperProcessError` with the launcher diagnostic. Producer reachability was inspected statically; the synthetic diagnostic was parsed and projected in process. ### Typed outcome or recovery expected With the same parser-accepted input, retain the provider startup fields and generic caller/secondary context in the caller-visible projection. Keep the provider-only control intact and the error classification unchanged. The storage shape is an implementation choice. Proposed further verification, not executed: exercise a real Hopper failure with both contexts through CLI/MCP and check the final delivered error. ### Analysis or suspected cause Source-derived: `providerErrorDetails` puts `error.diagnostic` in `details.diagnostics`, then the outer projection overwrites that entire key with `error.diagnostics`. This explains the boundary result. The constructor explicitly supports both fields. Secondary-failure enrichment is another supported way to add generic context; this report does not establish which real Hopper workflow, if any, currently produces that combination. ### Additional context Related reports were checked: #4226 concerns pre-handshake recovery/session state, #3985 concerns private-display probe cleanup ownership, #4240/#3935 and merged #4238 concern cleanup composition at other boundaries, and #641/merged #739 concern preserving shutdown rejection causes. None describes this domain projection key collision. No last-known-good revision or regression claim is available. ## Triage assessment - Impact level: P2 - Assessment status: provisional - Impact basis: The executed boundary probe loses structured startup facts while retaining the error code and generic context; real-session trigger frequency and reach are unmeasured. - Workaround status: unverified - Workaround basis: No caller workaround was exercised; the original in-memory error retains the diagnostic, but its availability to CLI/MCP users was not established.
Author
Owner

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

Checked the reported fix on main at d87e73f2db.

Error projection now preserves provider-specific diagnostics as provider_diagnostics when provider-neutral diagnostics occupies diagnostics. Both constructor-supplied generic context and retainSecondaryFailure enrichment are explicitly covered by equality assertions; baseline provider-only diagnostics still uses diagnostics.

Implementation: analysisErrorProjection.ts, analysisErrorProjection.test.ts.

The relevant regression checks in analysisErrorProjection.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:6380765519 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Error projection now preserves provider-specific diagnostics as provider_diagnostics when provider-neutral diagnostics occupies diagnostics. Both constructor-supplied generic context and retainSecondaryFailure enrichment are explicitly covered by equality assertions; baseline provider-only diagnostics still uses diagnostics. Implementation: [analysisErrorProjection.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/analysisErrorProjection.ts#L92), [analysisErrorProjection.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/analysisErrorProjection.test.ts#L1115). The relevant regression checks in [analysisErrorProjection.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/analysisErrorProjection.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#1734
No description provided.