Summary
When the engine sidecar is briefly unreachable, the frontend fetch to 127.0.0.1:<port> rejects with WebKit's TypeError: Load failed (127.0.0.1:<port>). A single underlying failure (engine momentarily unreachable) is currently reported to Sentry as many distinct issues, fragmented along three axes:
- Per operation —
read_agent_file, list_skills, list_project_files, check_provider_status, list_all_conversations, … each become their own Sentry issue.
- Per ephemeral port — the engine's port changes every launch and WebKit bakes it into the error message, so the issue title is unique per run.
- Per release / sourcemap state — older releases without resolved sourcemaps group by the minified frame (
zA), newer ones by the real frame (HoustonClient.request).
Net effect: one bug currently spawns ~20+ separate Sentry issues and keeps creating new ones, which buries the signal and breaks escalation tracking.
Concrete example (the two issues this came from): the same read_agent_file failure exists as HOUSTON-APP-65 (7532375190, source-mapped, houston-app@0.4.19) and HOUSTON-APP-2 (7510343192, minified, houston-app@0.4.15) — identical exc.type: read_agent_file and Load failed, differing only by ephemeral port and minified-vs-source stack. (APP-65 kept as canonical; APP-2 muted as a dup.) The same split exists for list_all_conversations, check_provider_status, list_project_files, list_routine_runs, and more.
Root cause
app/src/lib/sentry-report-error.ts:
const error = new Error(message); // message = "Load failed (127.0.0.1:52119)" — raw WebKit text, includes the ephemeral port
error.name = command; // → Sentry exception type = operation name → per-operation split
error.stack = originalError.stack; // real engine-client stack → per-method-frame split
error.name = command → metadata.type becomes the operation label, so each op is a distinct Sentry exception type.
- The preserved real stack differs per engine-client method (
readAgentFile vs listSkills …), reinforcing the per-operation split.
message carries the raw WebKit TypeError text, which includes the ephemeral 127.0.0.1:<port> → volatile titles per app launch. (Confirmed: nothing in Houston appends the port — it comes straight from the WKWebView fetch rejection.)
- (Historical, already fixed — not an action item.) Sourcemap upload was gated on an empty
SENTRY_AUTH_TOKEN and skipped on every build (see the comment in .github/workflows/release.yml, ~L259–264); fixed by hoisting the token to job scope. That is why 0.4.15 frames are minified while 0.4.19 resolves. Sourcemaps are uploaded today (sentry-cli sourcemaps upload for both macOS and Windows in release.yml).
Note: the per-operation split was a deliberate earlier fix — engine errors used to all collapse into a single issue, so a 404 was indistinguishable from a network failure (see app/src/lib/tauri.ts, ~L84–86). The fix overcorrected; the goal now is the middle ground.
Proposed durable fix
Group by the kind of failure, not by operation or ephemeral port. Keep genuinely-different failures distinct (the original goal) while collapsing the engine-unreachable family into one tracked issue.
-
Type transport failures in engine-client. In ui/engine-client/src/client.ts, wrap the fetch in request / rawRequest and throw a typed HoustonTransportError (carrying the original cause) when the fetch itself rejects (engine unreachable) — distinct from HoustonEngineError (a real HTTP response with a status). Do not rely on string-sniffing "Load failed": it is WebKit-specific and becomes "Failed to fetch" on the Windows WebView2 / Chromium webview.
-
Stable fingerprint at capture. Extend captureException in app/src/lib/sentry.ts to accept a fingerprint, and in the report path set:
- transport failure →
fingerprint: ['engine-transport-failure'] → one "Engine unreachable" issue regardless of operation / port / release.
HoustonEngineError → fingerprint: ['engine-http', String(status)] (optionally + a route group) → meaningful per-status issues.
- keep the operation as the existing
source tag for triage (already set in error-toast.ts), not as the group key.
-
Sanitize the volatile port out of the title. Before building the Sentry error, normalize 127.0.0.1:<port> → 127.0.0.1 (or <engine>) in the message; stash the real port / base URL in Sentry extra/context, not the title.
-
(Optional stopgap.) A Sentry server-side fingerprint/merge rule collapsing message:"Load failed*" would dedupe existing noise immediately, but the code fix above is the real cure and lives in-repo.
Acceptance criteria
- A burst of engine-unreachable failures across N operations produces one Sentry issue, not N.
- That issue's title is stable across app launches (no ephemeral port).
- HTTP-status and domain errors stay distinct — no regression to the old "everything is one issue" behavior.
- Unit tests cover the transport-vs-HTTP classification with verbatim WebKit (
Load failed) and Chromium (Failed to fetch) fetch-rejection fixtures.
References
app/src/lib/sentry-report-error.ts — error.name = command; message carries the ephemeral port
app/src/lib/tauri.ts (call / surfaceError, ~L50–92) — source of the operation label
app/src/lib/error-toast.ts (reportError / showErrorToast) — capture path + source / error_kind tags
app/src/lib/sentry.ts (captureException, ~L132–150) — extend to accept fingerprint
ui/engine-client/src/client.ts (request / rawRequest / toError, ~L103–164) — add the typed transport error
.github/workflows/release.yml — sourcemap upload (already wired); gate-bug context ~L259–264
- Sentry:
HOUSTON-APP-65 (7532375190, canonical) · HOUSTON-APP-2 (7510343192, muted dup)
Summary
When the engine sidecar is briefly unreachable, the frontend
fetchto127.0.0.1:<port>rejects with WebKit'sTypeError: Load failed (127.0.0.1:<port>). A single underlying failure (engine momentarily unreachable) is currently reported to Sentry as many distinct issues, fragmented along three axes:read_agent_file,list_skills,list_project_files,check_provider_status,list_all_conversations, … each become their own Sentry issue.zA), newer ones by the real frame (HoustonClient.request).Net effect: one bug currently spawns ~20+ separate Sentry issues and keeps creating new ones, which buries the signal and breaks escalation tracking.
Concrete example (the two issues this came from): the same
read_agent_filefailure exists asHOUSTON-APP-65(7532375190, source-mapped,houston-app@0.4.19) andHOUSTON-APP-2(7510343192, minified,houston-app@0.4.15) — identicalexc.type: read_agent_fileandLoad failed, differing only by ephemeral port and minified-vs-source stack. (APP-65 kept as canonical; APP-2 muted as a dup.) The same split exists forlist_all_conversations,check_provider_status,list_project_files,list_routine_runs, and more.Root cause
app/src/lib/sentry-report-error.ts:error.name = command→metadata.typebecomes the operation label, so each op is a distinct Sentry exception type.readAgentFilevslistSkills…), reinforcing the per-operation split.messagecarries the raw WebKitTypeErrortext, which includes the ephemeral127.0.0.1:<port>→ volatile titles per app launch. (Confirmed: nothing in Houston appends the port — it comes straight from the WKWebView fetch rejection.)SENTRY_AUTH_TOKENand skipped on every build (see the comment in.github/workflows/release.yml, ~L259–264); fixed by hoisting the token to job scope. That is why0.4.15frames are minified while0.4.19resolves. Sourcemaps are uploaded today (sentry-cli sourcemaps uploadfor both macOS and Windows inrelease.yml).Note: the per-operation split was a deliberate earlier fix — engine errors used to all collapse into a single issue, so a 404 was indistinguishable from a network failure (see
app/src/lib/tauri.ts, ~L84–86). The fix overcorrected; the goal now is the middle ground.Proposed durable fix
Group by the kind of failure, not by operation or ephemeral port. Keep genuinely-different failures distinct (the original goal) while collapsing the engine-unreachable family into one tracked issue.
Type transport failures in engine-client. In
ui/engine-client/src/client.ts, wrap thefetchinrequest/rawRequestand throw a typedHoustonTransportError(carrying the original cause) when the fetch itself rejects (engine unreachable) — distinct fromHoustonEngineError(a real HTTP response with a status). Do not rely on string-sniffing"Load failed": it is WebKit-specific and becomes"Failed to fetch"on the Windows WebView2 / Chromium webview.Stable fingerprint at capture. Extend
captureExceptioninapp/src/lib/sentry.tsto accept afingerprint, and in the report path set:fingerprint: ['engine-transport-failure']→ one "Engine unreachable" issue regardless of operation / port / release.HoustonEngineError→fingerprint: ['engine-http', String(status)](optionally + a route group) → meaningful per-status issues.sourcetag for triage (already set inerror-toast.ts), not as the group key.Sanitize the volatile port out of the title. Before building the Sentry error, normalize
127.0.0.1:<port>→127.0.0.1(or<engine>) in the message; stash the real port / base URL in Sentryextra/context, not the title.(Optional stopgap.) A Sentry server-side fingerprint/merge rule collapsing
message:"Load failed*"would dedupe existing noise immediately, but the code fix above is the real cure and lives in-repo.Acceptance criteria
Load failed) and Chromium (Failed to fetch) fetch-rejection fixtures.References
app/src/lib/sentry-report-error.ts—error.name = command; message carries the ephemeral portapp/src/lib/tauri.ts(call/surfaceError, ~L50–92) — source of the operation labelapp/src/lib/error-toast.ts(reportError/showErrorToast) — capture path +source/error_kindtagsapp/src/lib/sentry.ts(captureException, ~L132–150) — extend to acceptfingerprintui/engine-client/src/client.ts(request/rawRequest/toError, ~L103–164) — add the typed transport error.github/workflows/release.yml— sourcemap upload (already wired); gate-bug context ~L259–264HOUSTON-APP-65(7532375190, canonical) ·HOUSTON-APP-2(7510343192, muted dup)