[eric] browser: a load Chromium refuses is a failed navigation, not "Navigated to" an empty page; exp.3 changelog names the popup, self-heal and navigation fixes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012G8kyALnPjsA7aJFmMBq3R
This commit is contained in:
ciregenz
2026-09-02 00:02:58 -07:00
co-authored by Claude Fable 5.1
parent 68b9ad32ea
commit 4d9ecb9a16
4 changed files with 37 additions and 6 deletions
+4
View File
@@ -45,6 +45,10 @@ P_RELEASES: List[ReleaseNote] = [
"Picking a very long chat back up no longer restarts it at the edge of the context window. The recap it rebuilds from is now bounded (the newest steps in full, older ones as one-line stubs, the rest counted), so a chat with hundreds of tool calls resumes at a fraction of the size and stops compacting on its first step.",
"A chat that was still running when OpenSwarm's engine shut down now says so in the chat, with a line to send a message to continue, instead of looking like you stopped it yourself.",
"If something on your computer stops the agent's engine process mid-task, OpenSwarm restarts it and continues the same conversation, instead of showing an error and starting the chat over from a summary.",
"Links and downloads inside an App card work again. Every target=_blank link and window.open used to be dropped silently; they now open in their own window that shares the app's session, so an app's PDF or report export actually opens.",
"Self-heals show themselves. A stuck built-in tool being restarted, the model summarizing its own history, and a run waiting on a lost connection or a rate limit are now visible on the card, and a raw runtime error shows a card with the details behind a disclosure instead of a blank transcript.",
"A page the browser refuses to load (a blocked port, a dead address) is reported as a failed navigation instead of being described as a blank page.",
"A collapsed chat card never previews an internal prompt as if you had typed it, and a chat interrupted by a restart keeps its Resume chip.",
],
),
ReleaseNote(
+16 -5
View File
@@ -11,7 +11,7 @@ import { resolveInput } from './resolveUrl';
import { rankAndCapInteractives, type RankItem } from './interactiveRanking';
import { shouldStopWaiting, SETTLE_POLL_MS, settleProbeJs } from './browserSettle';
import { unwrapCdpEval } from './cdpEval';
import { navigationOutcome, sameDoc } from './navigationOutcome';
import { loadFailureError, navigationOutcome, sameDoc } from './navigationOutcome';
import { handleCanvasCommand } from './canvasCommandHandler';
import { typeChars, type TypedKeys } from './typeChars';
@@ -381,17 +381,28 @@ async function handleNavigate(wv: BrowserWebview, params: Record<string, any>):
removeReady = () => wv.removeEventListener('dom-ready', onReady);
});
let aborted = false;
const fullyLoaded = wv.loadURL(url).catch((err: any) => {
// A load Chromium refuses (unsafe port, DNS, connection refused) still fires dom-ready for its empty error document, which used to win the race and turn the refusal into "Navigated to" (ENG-404 drill, 2026-09-01).
let loadFailure: string | null = null;
const onFail = (e: Event) => {
const f = e as Event & { errorCode?: number; errorDescription?: string; isMainFrame?: boolean };
if (f.isMainFrame !== false && f.errorCode !== undefined && f.errorCode !== -3) loadFailure = `${f.errorDescription || 'load failed'} (${f.errorCode})`;
};
wv.addEventListener('did-fail-load', onFail);
const fullyLoaded = wv.loadURL(url).catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
// ERR_ABORTED can be a benign supersede OR a nav the guest refused outright; the landed-URL check below tells them apart.
if (err?.message?.includes('ERR_ABORTED')) { aborted = true; return; }
throw err;
if (msg.includes('ERR_ABORTED')) { aborted = true; return; }
loadFailure = loadFailure || msg;
});
fullyLoaded.catch(() => {}); // a late load failure shouldn't throw once dom-ready returned
try {
await Promise.race([fullyLoaded, domReady]);
// dom-ready can beat the rejection by a few ms; give the refusal its say before trusting the document.
await Promise.race([fullyLoaded, new Promise<void>((r) => setTimeout(r, 300))]);
} finally {
removeReady();
wv.removeEventListener('did-fail-load', onFail);
}
if (loadFailure) return loadFailureError(url, loadFailure);
// A navigate that lands on a raw JSON/API document (Instagram's topsearch, any /api/... GET)
// paints an unreadable wall in the card and reads as a crash to the user. Hand the data to the
// agent as the result instead, and quietly get the card off the wall, so a person never sees
@@ -2,7 +2,7 @@
// so a nav that leaves the document parked on the old URL can only ever read as a failure.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { navigationOutcome, sameDoc } from './navigationOutcome';
import { loadFailureError, navigationOutcome, sameDoc } from './navigationOutcome';
test('a landed URL equal to the old page is stuck, never ok', () => {
const v = navigationOutcome('https://en.wikipedia.org/wiki/Helium', 'https://en.wikipedia.org/wiki/Argon', 'https://en.wikipedia.org/wiki/Argon');
@@ -39,3 +39,11 @@ test('sameDoc ignores only the trailing slash', () => {
assert.equal(sameDoc('https://a.com', 'https://a.com/'), true);
assert.equal(sameDoc('https://a.com/x', 'https://a.com/x#frag'), false);
});
test('a refused load is an error that names the failure and the empty document, never "Navigated to"', () => {
const r = loadFailureError('http://127.0.0.1:1/', 'ERR_UNSAFE_PORT (-312)');
assert.match(r.error, /FAILED \(ERR_UNSAFE_PORT \(-312\)\)/);
assert.match(r.error, /empty error document/);
assert.match(r.error, /report the failure instead of describing the page/);
assert.equal(r.url, 'http://127.0.0.1:1/');
});
+8
View File
@@ -16,3 +16,11 @@ export function navigationOutcome(requested: string, before: string, landed: str
if (!sameDoc(landed, requested)) return { kind: 'redirected', url: landed, requested };
return { kind: 'ok', url: landed };
}
/** The error a navigate returns when Chromium refused the load outright (unsafe port, DNS, connection refused): the guest then shows its own empty document, which reads as "a blank page" unless the agent is told. */
export function loadFailureError(requested: string, failure: string): { error: string; url: string } {
return {
error: `Navigation to ${requested} FAILED (${failure}). The browser is showing its empty error document, not the site; retry once, and if it fails again report the failure instead of describing the page.`,
url: requested,
};
}