Skip to content

Commit 1d5f156

Browse files
committed
v0.2.106 — P110 v5: undici first, Chromium fallback
curl confirmed DNS+TCP+TLS to the API are all healthy, but net.fetch (Chromium) kept failing. On campus networks a misconfigured proxy (PAC/ WPAD) is the #1 cause: net.fetch respects system proxy and gets stuck. Reverted stack order: undici (no proxy, direct) goes first; net.fetch is the fallback. When DNS works, undici succeeds immediately; when a proxy is genuinely needed, undici fails transiently and net.fetch kicks in. Also: fetchWithConnectTimeout no longer removes the idle-timeout listener from its finally block — silent streams still time out after 120s.
1 parent 56164cd commit 1d5f156

3 files changed

Lines changed: 48 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@
66
77
## [Unreleased]
88

9+
## [0.2.106] - 2026-08-04
10+
11+
### Fixed (P110 v5 — undici first, Chromium fallback)
12+
13+
curl confirmed DNS + TCP + TLS to the API are all healthy, but net.fetch
14+
(Chromium) kept failing. On campus/corporate networks a misconfigured
15+
proxy (PAC / WPAD) is the #1 cause: net.fetch respects system proxy and
16+
gets stuck when proxy routing breaks.
17+
18+
Reverted the stack order: undici (no proxy, direct) goes first; net.fetch
19+
(system-proxy) is now the fallback. When DNS works the fast path succeeds
20+
immediately; when a proxy is genuinely needed, undici fails transiently
21+
and net.fetch kicks in.
22+
23+
Also: fetchWithConnectTimeout no longer removes the idle-timeout signal
24+
listener from its finally block — the idle timeout stays connected so
25+
silent streams still time out after 120s.
26+
27+
### Changed
28+
29+
- `src/main/llm/net.ts` — undici first, Chromium second; idle signal fix
30+
931
## [0.2.104] - 2026-08-04
1032

1133
### Fixed (P110 — idle timeout signal chain broken by P109)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "millwright",
3-
"version": "0.2.105",
3+
"version": "0.2.106",
44
"description": "Open-source AI automation for SolidWorks — talk to your CAD.",
55
"keywords": [
66
"solidworks",

src/main/llm/net.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -107,37 +107,42 @@ export async function llmFetch(
107107
let lastErr: unknown;
108108

109109
for (let attempt = 0; attempt <= retries; attempt++) {
110-
// Try BOTH stacks every attempt: Chromium (system-proxy aware) first, then
111-
// undici. P108: the two behave differently — one may resolve where the other
112-
// fails (proxy config broken vs DNS broken). Never `break`/`continue` past
113-
// undici: even a transient Chromium failure gets the undici shot this attempt.
114-
// P109: each stack gets a 20s connect-stage cap so failures fail fast.
115-
let electronFailed = false;
110+
// P110: try undici FIRST — it bypasses the system proxy and goes direct.
111+
// On campus/corporate networks a misconfigured proxy (PAC / WPAD) is the
112+
// #1 cause of net.fetch failures when DNS itself is fine (confirmed with
113+
// curl). undici was the original problem when DNS was broken; now DNS is
114+
// healthy, so undici should be the fast path. net.fetch still gets a turn
115+
// as a fallback for users who genuinely need system-proxy routing.
116+
117+
// 1) Undici path — no proxy, direct DNS.
118+
try {
119+
return await fetchWithConnectTimeout(fetch, url, init, init.signal ?? undefined);
120+
} catch (err) {
121+
lastErr = err;
122+
if (!isTransient(err)) {
123+
// Non-transient (cert / abort / 4xx): don't bother with net.fetch.
124+
throw err;
125+
}
126+
// Transient — try the Chromium stack (system-proxy) below this attempt.
127+
}
128+
129+
// 2) Electron path — Chromium network stack honors system proxy.
116130
if (typeof net !== 'undefined' && typeof net.fetch === 'function') {
117131
try {
118132
return await fetchWithConnectTimeout(
119133
(u, i) => net.fetch(u, i as any), url, init, init.signal ?? undefined,
120134
);
121135
} catch (err) {
122136
lastErr = err;
123-
electronFailed = true;
124-
// No continue here — fall through so undici still gets a try this attempt.
125137
}
126138
}
127139

128-
// Fallback path — global fetch (also the only path outside Electron).
129-
try {
130-
return await fetchWithConnectTimeout(fetch, url, init, init.signal ?? undefined);
131-
} catch (err) {
132-
lastErr = err;
133-
if (isTransient(err) || electronFailed) {
134-
// Either stack reported a transient failure (or Electron died and undici
135-
// inherited a transient) → back off and retry the round.
136-
if (attempt < retries) await sleep(400 * (attempt + 1));
137-
continue;
138-
}
139-
throw err;
140+
// Both stacks failed — retry if transient.
141+
if (isTransient(lastErr)) {
142+
if (attempt < retries) await sleep(400 * (attempt + 1));
143+
continue;
140144
}
145+
throw lastErr;
141146
}
142147

143148
throw lastErr;

0 commit comments

Comments
 (0)