What happened
With a network proxy configured (policy.networkProxy.enabled === true),
createProxiedFetchTransport().close() in @maka/runtime never settles. It
aborts the shared AbortController first, which destroys the proxied CONNECT
tunnel socket out from under undici's ProxyAgent; ProxyAgent.destroy() then
never resolves, so every caller that awaits close() in a finally hangs
forever.
User-visible on the CLI/TUI: adding a custom connection stops at the verify
step and shows "Verifying…" indefinitely — no error, no timeout.
connection.onboarding.verify never returns, and the Host's per-provider lane
stays wedged for the life of the process, so every later attempt for that
provider queues behind it.
This is not limited to onboarding. Eight Runtime Host modules use the same
try { … } finally { await transport.close() } shape — including
web-fetch-tool.js, web-search-tool.js and model-metadata-refresh.js — so
the WebFetch/WebSearch tools and model metadata refresh hang the same way behind
a proxy. I confirmed the WebFetch shape directly against the unmodified
published package: fetch completes, then the finally never returns.
Introduced by #5043 (see "Notes").
How to reproduce
Needs a reachable HTTP proxy — the failing path is a proxied CONNECT tunnel.
The minimal case is the transport itself, no Host needed:
import { createProxiedFetchTransport } from '@maka/runtime/dist/network/scoped-fetch-transport.js';
const t = createProxiedFetchTransport({
enabled: true, type: 'http', host: '127.0.0.1', port: 10808,
bypassList: [], username: '', password: '',
});
const r = await t.fetch('https://example.com/models', {
headers: { authorization: 'Bearer …' }, signal: AbortSignal.timeout(20_000),
});
await r.json(); // succeeds — 200
await t.close(); // never resolves
Same script with createProxiedFetchTransport(null) closes in ~1ms, and against
a bare new ProxyAgent(...) instead of this wrapper it also closes in ~1ms.
Through the product:
- Set a network proxy in Settings, with
enabled on.
/setup → Custom connection (OpenAI Chat Completions)
- Enter a base URL and API key for a reachable OpenAI-compatible endpoint
- The wizard reaches "Verifying…" and stays there indefinitely
The shipped CLI passes no timeout on this call, so nothing ever surfaces the
failure — dist/runtime-host-onboarding.js calls
connection.request('connection.onboarding.verify', { … }) with no deadline
argument (unlike the OAuth paths in the same file, which do pass a
remainingTimeout()).
Environment
- Maka version or commit:
maka-agent@0.2.0-dev.64.20260929 (npm nightly), installed package
- OS and version: Omarchy, Linux 7.2.5-3-omarchy x86_64
- Surface: Runtime Host (reached from TUI/CLI onboarding; also affects Desktop's WebFetch/WebSearch and model metadata refresh, all of which run in the Host)
- Node.js version: v26.7.0
- undici: 8.11.2 (
@maka/runtime-host 0.1.0)
- Proxy: HTTP,
127.0.0.1:10808, enabled: true
Logs, screenshots, or additional context
Affected code
packages/runtime/src/network/scoped-fetch-transport.ts (shipped compiled as
dist/network/scoped-fetch-transport.js). The close() implementation is the
same in both; only the surrounding types differ.
const close = (): Promise<void> => {
if (closePromise) return closePromise;
closed = true;
connections.abort(new Error('Connection effect fetch transport closed')); // (1)
closePromise = Promise.all([
directDispatcher.destroy(new Error('Connection effect fetch transport closed')).catch(() => {}),
proxyDispatcher?.destroy(new Error('Connection effect fetch transport closed')).catch(() => {}), // (2)
]).then(() => undefined);
return closePromise; // (3)
};
(1) destroys the CONNECT tunnel socket, because buildProxyDispatcher registers
that socket with abortSocket(signal, socket) in its clientFactory:
handler.onRequestUpgrade = (controller, status, headers, socket) => {
abortSocket(signal, socket);
onUpgrade?.call(handler, controller, status, headers, socket);
};
undici never observes that external socket.destroy(), so the ProxyAgent
client promise stays unsettled, (2) never settles, and (3) hangs.
Evidence narrowing the cause
Bisecting the dispatcher construction, with the abort performed before destroy()
in each case:
| Dispatcher configuration |
destroy() |
plain ProxyAgent |
settles, 1ms |
+ factory (cancellable connect) |
settles, 0ms |
+ clientFactory overriding onRequestUpgrade — what buildProxyDispatcher builds |
never settles |
Probes injected into a running Host isolate it to the exact await:
DISCOVERY:after ok=true n=39 ← model discovery succeeds, 39 models
CLOSE:before ← enters transport.close()
CLOSEIMPL:direct-destroyed ← directDispatcher resolves immediately
← proxy-destroyed never fires
So the discovery work is fine; only teardown wedges. buildSocks5Dispatcher
builds its own Agent and is unaffected — this is specific to the HTTP-proxy
ProxyAgent path.
Attribution to #5043
I fetched 0.2.0-dev.51.20260925 (before #5043) and ran the same reproduction
against it: close() returns in 1ms. Against pristine
0.2.0-dev.64.20260929 the identical script leaves close() still pending
after 15s. dev.51 has neither connections.abort() inside close() nor
onRequestUpgrade in proxy-dispatcher.js; dev.64 has both.
The same pristine dev.64 also shows the proxy gating: with
createProxiedFetchTransport(null) → close() settles in 1ms; with an HTTP
proxy → never settles. The no-proxy path is genuinely unaffected, which is
likely why this has gone unnoticed — the untested default is the no-proxy path.
End-to-end impact
Against the published package:
|
pristine dev.64 |
with a bounded teardown |
close() after one proxied fetch |
pending after 15s |
~1.0s |
connection.onboarding.verify over RPC |
no return |
~1.5s, kind: 'verified', 39 models |
Host activeOperations after the call |
+1, never released |
flat across 5 consecutive calls |
The activeOperations row matters most: with the pristine build a single wedge
wedges that provider's lane for the life of the Host process. We also completed
the real onboarding wizard end-to-end twice against a real endpoint — it reaches
the model picker and persists the connection, where it previously stopped at
verify.
Suggested direction
Teardown should not gate the caller. The sockets are already aborted by (1), so
awaiting undici's dispatcher teardown afterwards is best-effort and should be
bounded — e.g. race the destroy against a short grace period:
+const TRANSPORT_CLOSE_GRACE_MS = 1_000;
...
- return closePromise;
+ return Promise.race([
+ closePromise,
+ new Promise((resolve) => setTimeout(resolve, TRANSPORT_CLOSE_GRACE_MS, undefined)),
+ ]);
We applied exactly this locally (to the shipped dist) plus a 30s deadline on
the verify/save RPCs, and measured no descriptor retention: three cycles of
create → proxied fetch → close() hold /proc/self/fd flat at 23 in both the
pristine and patched builds, and both processes exit on their own. That is one
observation on one platform, not a proof.
Fixing it at the root instead — having abortSocket notify undici rather than
unilaterally destroying the socket — would also be reasonable; #5043's own
follow-up note about releasing cancellation listeners suggests that area is still
in flux. How this should be fixed is entirely the maintainers' call — the
above is only a measurement of one approach, offered as a validation target. We
have not built this repository or run its test suite.
Notes
- Not a regression in
connection.onboarding.verify itself; the RPC is fine, its
finally just never completes.
- A regression test in
packages/runtime/src/network/__tests__/scoped-fetch-transport.test.ts that
configures a proxy, performs one fetch, and asserts close() settles would
cover this.
Provenance
Generative tooling contributed substantively to this report. The investigation —
source reading, instrumented probes, bisection, and reproduction scripts — was
performed with an AI coding assistant, and this report was drafted with its
assistance. All version numbers, the close() source, the attribution to #5043,
and every measurement above were verified directly against the repository and the
published maka-agent packages, not against our own modified working tree. No
API keys, tokens, or other credentials are included here.
Supersedes #5897, which we closed: it was filed without the Bug report template
and its stated impact was too narrow (it did not cover the WebFetch/WebSearch
and model-metadata paths, and two of its claims needed correcting).
What happened
With a network proxy configured (
policy.networkProxy.enabled === true),createProxiedFetchTransport().close()in@maka/runtimenever settles. Itaborts the shared
AbortControllerfirst, which destroys the proxied CONNECTtunnel socket out from under undici's
ProxyAgent;ProxyAgent.destroy()thennever resolves, so every caller that awaits
close()in afinallyhangsforever.
User-visible on the CLI/TUI: adding a custom connection stops at the verify
step and shows "Verifying…" indefinitely — no error, no timeout.
connection.onboarding.verifynever returns, and the Host's per-provider lanestays wedged for the life of the process, so every later attempt for that
provider queues behind it.
This is not limited to onboarding. Eight Runtime Host modules use the same
try { … } finally { await transport.close() }shape — includingweb-fetch-tool.js,web-search-tool.jsandmodel-metadata-refresh.js— sothe WebFetch/WebSearch tools and model metadata refresh hang the same way behind
a proxy. I confirmed the WebFetch shape directly against the unmodified
published package: fetch completes, then the
finallynever returns.Introduced by #5043 (see "Notes").
How to reproduce
Needs a reachable HTTP proxy — the failing path is a proxied CONNECT tunnel.
The minimal case is the transport itself, no Host needed:
Same script with
createProxiedFetchTransport(null)closes in ~1ms, and againsta bare
new ProxyAgent(...)instead of this wrapper it also closes in ~1ms.Through the product:
enabledon./setup→ Custom connection (OpenAI Chat Completions)The shipped CLI passes no timeout on this call, so nothing ever surfaces the
failure —
dist/runtime-host-onboarding.jscallsconnection.request('connection.onboarding.verify', { … })with no deadlineargument (unlike the OAuth paths in the same file, which do pass a
remainingTimeout()).Environment
maka-agent@0.2.0-dev.64.20260929(npmnightly), installed package@maka/runtime-host0.1.0)127.0.0.1:10808,enabled: trueLogs, screenshots, or additional context
Affected code
packages/runtime/src/network/scoped-fetch-transport.ts(shipped compiled asdist/network/scoped-fetch-transport.js). Theclose()implementation is thesame in both; only the surrounding types differ.
(1) destroys the CONNECT tunnel socket, because
buildProxyDispatcherregistersthat socket with
abortSocket(signal, socket)in itsclientFactory:undici never observes that external
socket.destroy(), so theProxyAgentclient promise stays unsettled, (2) never settles, and (3) hangs.
Evidence narrowing the cause
Bisecting the dispatcher construction, with the abort performed before
destroy()in each case:
destroy()ProxyAgent+ factory(cancellable connect)+ clientFactoryoverridingonRequestUpgrade— whatbuildProxyDispatcherbuildsProbes injected into a running Host isolate it to the exact await:
So the discovery work is fine; only teardown wedges.
buildSocks5Dispatcherbuilds its own
Agentand is unaffected — this is specific to the HTTP-proxyProxyAgentpath.Attribution to #5043
I fetched
0.2.0-dev.51.20260925(before #5043) and ran the same reproductionagainst it:
close()returns in 1ms. Against pristine0.2.0-dev.64.20260929the identical script leavesclose()still pendingafter 15s. dev.51 has neither
connections.abort()insideclose()noronRequestUpgradeinproxy-dispatcher.js; dev.64 has both.The same pristine dev.64 also shows the proxy gating: with
createProxiedFetchTransport(null)→close()settles in 1ms; with an HTTPproxy → never settles. The no-proxy path is genuinely unaffected, which is
likely why this has gone unnoticed — the untested default is the no-proxy path.
End-to-end impact
Against the published package:
close()after one proxied fetchconnection.onboarding.verifyover RPCkind: 'verified', 39 modelsactiveOperationsafter the callThe
activeOperationsrow matters most: with the pristine build a single wedgewedges that provider's lane for the life of the Host process. We also completed
the real onboarding wizard end-to-end twice against a real endpoint — it reaches
the model picker and persists the connection, where it previously stopped at
verify.
Suggested direction
Teardown should not gate the caller. The sockets are already aborted by (1), so
awaiting undici's dispatcher teardown afterwards is best-effort and should be
bounded — e.g. race the destroy against a short grace period:
We applied exactly this locally (to the shipped
dist) plus a 30s deadline onthe verify/save RPCs, and measured no descriptor retention: three cycles of
create → proxied fetch →
close()hold/proc/self/fdflat at 23 in both thepristine and patched builds, and both processes exit on their own. That is one
observation on one platform, not a proof.
Fixing it at the root instead — having
abortSocketnotify undici rather thanunilaterally destroying the socket — would also be reasonable; #5043's own
follow-up note about releasing cancellation listeners suggests that area is still
in flux. How this should be fixed is entirely the maintainers' call — the
above is only a measurement of one approach, offered as a validation target. We
have not built this repository or run its test suite.
Notes
connection.onboarding.verifyitself; the RPC is fine, itsfinallyjust never completes.packages/runtime/src/network/__tests__/scoped-fetch-transport.test.tsthatconfigures a proxy, performs one fetch, and asserts
close()settles wouldcover this.
Provenance
Generative tooling contributed substantively to this report. The investigation —
source reading, instrumented probes, bisection, and reproduction scripts — was
performed with an AI coding assistant, and this report was drafted with its
assistance. All version numbers, the
close()source, the attribution to #5043,and every measurement above were verified directly against the repository and the
published
maka-agentpackages, not against our own modified working tree. NoAPI keys, tokens, or other credentials are included here.
Supersedes #5897, which we closed: it was filed without the Bug report template
and its stated impact was too narrow (it did not cover the WebFetch/WebSearch
and model-metadata paths, and two of its claims needed correcting).