Repository navigation
moved-path-guard reads URLs as paths; on Windows the UNC probe blocks the session loop for about 2 minutes #3404
Description
Environment
- Senpi 2026.10.13 (omo-ai 5.1.33), Bun 1.4.3, Windows 11 Home 10.0.26300.
- MCP server: Z.ai web-reader,
type: httpwithbearerTokenEnv(the transport is not involved, see below).
Symptom
Every call of an MCP tool whose arguments contain a URL with a path segment fails after about 124 s with:
ToolExecError: TimeoutError: MCP metadata was not ready before the request timeout.
Reproduced 7 times on 2026-10-15 with mcp_web-reader_webReader and url: "https://docs.z.ai/devpack/overview"
(each 122–124 s). The same tool with url: "https://example.com" succeeds in about 3 s, in the same kind of session.
~/.omo/agent/logs/moved-path-guard.log has a call_bound_reached (bound: step) entry at the start of every failing
call and none for the passing calls.
During the stall the Senpi process uses no CPU and sends no network traffic (Bun BUN_CONFIG_VERBOSE_FETCH trace and a
process sampler). The MCP handshake gets as far as initialize, notifications/initialized and sometimes ping. Then
nothing happens until about 122 s later, when the catalog requests and the error go out together. The 2 s ping timeout
and the 30 s requestTimeoutMs timer both fire late, which means the event loop is blocked.
Root cause
Two defects combine.
commandPaths(dist/core/extensions/builtin/moved-path-guard/command-paths.js) extracts paths from URLs. An
unclassified tool (every MCP tool) has all of its string arguments joined and scanned.EMBEDDED_PATHaccepts an
anchored path after:, so forhttps://docs.z.ai/devpack/overviewit yields//docs.z.ai/devpack/overview.
path.win32.isAbsoluteaccepts that, andtargets()resolves it to the UNC share\\docs.z.ai\devpack\overview.
With no path segment (https://example.com),//example.comresolves toC:\example.com, which fails fast, and
that hid the bug. The same applies tobashcommands such ascurl https://host/a/b.- Probing that UNC path opens SMB to an internet host. Here each attempt ends in
ECONNRESETafter about 20 s, and
Windows queues the attempts: a standalone Bun probe of the same path withaccess,realpath,statandlstat
settles at 41, 62, 103 and 124 s. The guard's 500 ms step deadline and 2 s call deadline let the tool call go on.
But the guard'scanonicalstep callscanonicalizeFilesystemPath
(dist/core/tools/filesystem-policy.js). After its own 2 sRESOLUTION_DEADLINE_MS, it falls back to
realpathWithoutOpenStrict(dist/utils/paths.js), which walks the path with synchronouslstatSync. That
lstat on the UNC share blocks the main thread until Windows gives up on SMB. Measured with the unbundled 2026.10.13
module, an event-loop lag monitor and a 500 ms race (the guard's step deadline): the race returns at +500 ms,
then the loop is blocked for 101,653 ms, starting at +2.0 s. The guard has already let the tool call proceed by
then, so the block lands in the middle of the MCP handshake. MCP times out only because its timer is overdue when
the loop wakes up. A wedged mount on macOS would hit the same fallback, so the bound the module comment promises
("a wedged mount never blocks the session loop") does not hold.
Reproduction
- On Windows, call any MCP tool with a string argument
https://<reachable host>/<segment>/<more>. In a session
(--session-id), have the model callmcp_web-reader_webReaderwithurl: https://docs.z.ai/devpack/overview. - Observe the tool result after about 2 minutes:
MCP metadata was not ready before the request timeout. - Standalone: in Bun on Windows, run
path.resolve("C:\\x", "//docs.z.ai/devpack/overview"). It returns
\\docs.z.ai\devpack\overview.
Suggested fix
- Drop
scheme://...URLs before extraction incommandPaths. The local patch below does this, and the same web-reader
call then succeeds in 3.6 s. - Defense in depth: never probe UNC paths produced by the text heuristic unless the user's home is itself on a UNC
share. Keep every fallback after a deadline asynchronous, so the bound actually holds.
Local patch, applied to the installed package:
export function commandPaths(command, cwd) {
command = command.replace(/(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]+:\/\/[^\s"'`;|&<>()]*/g, " ");
const paths = new Set(embeddedPaths(command));A scheme needs at least two characters, so C://dir stays a drive path. Text without a URL extracts what it extracted
before (checked against the verbatim 2026.10.13 source and bundle).
Evidence
- Three headless sessions (
omo -p --session-id) each show about 124 s between the tool call and the error. - Loop-block probe: Bun 1.4.3 imported
dist/core/tools/filesystem-policy.jsand called
canonicalizeFilesystemPath("\\\\docs.z.ai\\devpack\\overview"). The step race returned at +500 ms, and one
event-loop lag of 101,653 ms ended at +103,667 ms. - Control: in a plain Bun process, pending async UNC probes alone do not delay local file I/O, DNS or new HTTPS
connections (all under 2 s). The stall comes only from the synchronous fallback. - After the patch: the docs.z.ai call returned in 3.6 s and a
github.com issues URL in 5.6 s, with nocall_bound_reachedentry.
Activity
rhyme227 commentedon Oct 23, 2026
Confirmed on a second Windows machine, and still present in the current release (2026.10.21-3).
Environment
10.0.26300, Bun 1.4.2Reproduction
Any tool whose arguments are scanned as command text, given a URL that carries a path segment:
evalwhosecodecontainshttps://<host>/<segment>/<file>.jsbashwhosecommandcontainscurl https://<host>/<segment>The TUI shows
Running PreToolUse hook: running moved-path-guardand the session stalls for roughly 100 s. Other tool calls queued in the same turn time out, and in a team run the child agents die at their webfetch/eval steps. Calls without a URL are unaffected, andwebfetchitself is exempt (kind: "none"), so the freeze lands oneval,bash,monitorand every unclassified MCP tool.Evidence
<agent-dir>/logs/moved-path-guard.logrecordscall_bound_reachedfor exactly the stalled calls and for nothing else:{"ts":"...","level":"debug","event":"call_bound_reached","bound":"step","count":"21"} {"ts":"...","level":"debug","event":"call_bound_reached","bound":"deadline","count":"21"}Counts between 3 and 99 across one session, all on calls whose text contained a URL. Your
access/realpath/stat/lstatmeasurements match what this machine does too: the step race returns at +500 ms and the loop stays blocked afterwards.Still unfixed in 2026.10.21-3
Diffed against the published package, all three sites are unchanged:
dist/core/extensions/builtin/moved-path-guard/command-paths.js— noscheme://strippingdist/core/extensions/builtin/moved-path-guard/index.js—checkTargetshas no UNC filterdist/core/tools/filesystem-policy.js— still falls back to the synchronousrealpathWithoutOpenStrictThe changelog for 2026.10.21-2 and 2026.10.21-3 does not mention this issue.
Local fix that holds
Your proposed patch plus the defense-in-depth you suggested — drop
scheme://...before extraction, and skip\\\\-prefixed targets on win32 insidecheckTargets— applied to both the module and the bundled copy the CLI actually loads. With a fakepiregistering thetool_callhandler and calling it directly:curl https://<host>/<segment>/<file>.jsdir \\\\<host>\\<share>\\<path>cat <home>/a.txtNo
call_bound_reachedentries after the fix.The sync fallback is the part I cannot fix locally without touching shared path modules, so I would expect that one to land with #3993.
katamana commentedon Oct 25, 2026
Still reproducible on senpi 2026.10.23-1 (omo-ai 5.1.46), Windows 11 Pro 10.0.26300, Bun 1.4.2. On
main,command-paths.tsstill has noscheme://stripping andfilesystem-policy.tsstill falls back to the synchronousrealpathWithoutOpenStrict.Adding a native stack of the frozen main thread, since the evidence so far is lag measurements. A sampler on this machine attaches
cdb -pvr(non-invasive, no suspend) when the TUI main thread stays inWait/Executivefor 5 s. Two captures, 2026-10-24 and 2026-10-25, each taken while anevalwhose code containedhttps://gitee.com/<user>/<repo>/raw/master/<file>.txtwas running. Main thread:At the same moment two worker threads sat in the same
GetFileInformationByNamecall, which fits the async probes plus a synchronous lstat on the main thread. I could not resolve which JS function issued the main-thread lstat, so tying it to therealpathWithoutOpenStrictfallback is inference from the source, not from the stack.Numbers from this machine (traffic goes through a TUN proxy):
lstatSync("//gitee.com/<user>")= 27.9 s,lstatSync("//msdl.microsoft.com/download")= 20.0 s, bothECONNRESET.commandPathsfrom the installed 2026.10.23-1 returns//gitee.com/<user>/<repo>/raw/master/<file>.txtand//msdl.microsoft.com/download/symbolsfor code containing those URLs.record_factcalls, whose text contained URLs, and their results were delayed by about 60 s.evalthat only waits 20 s on a subprocess, with no URL in its code, does not stall the main thread.katamana commentedon Oct 26, 2026
Still present on senpi 2026.10.25-2 (omo-ai 5.1.48), Windows 11 Pro 10.0.26300, Bun 1.4.2. On
main,command-paths.tsstill has noscheme://handling andfilesystem-policy.tsstill falls back torealpathWithoutOpenStrict.New data point: the stall can hit an idle session, with no user-visible tool call running.
claude.exechild.Wait/Executive. That is 69 s, the longest stall I have measured. The input is dead the whole time.https://api.anthropic.com/api/oauth/usage.The sampler captured the main thread at 17:55:28. The stack is the same as in my previous comment:
Measured vs inferred: the stall window, the stack, and the commit times are measured. Linking the stall to the extractor's URL-bearing fact is inference from the timing, because no JS frames are resolved. It does match the earlier observation that
record_factresults were delayed while their text contained URLs.So with the guard as it is, any background writer that passes URL text through a scanned tool argument can freeze the TUI, even when the user is not doing anything. That includes the facts extractor and other memory tools. Skipping tokens that start with
scheme://incommandPathswould cover both the foreground and the background cases.