Skip to content

moved-path-guard reads URLs as paths; on Windows the UNC probe blocks the session loop for about 2 minutes #3404

Open

Description

@willowite

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: http with bearerTokenEnv (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.

  1. 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_PATH accepts an
    anchored path after :, so for https://docs.z.ai/devpack/overview it yields //docs.z.ai/devpack/overview.
    path.win32.isAbsolute accepts that, and targets() resolves it to the UNC share \\docs.z.ai\devpack\overview.
    With no path segment (https://example.com), //example.com resolves to C:\example.com, which fails fast, and
    that hid the bug. The same applies to bash commands such as curl https://host/a/b.
  2. Probing that UNC path opens SMB to an internet host. Here each attempt ends in ECONNRESET after about 20 s, and
    Windows queues the attempts: a standalone Bun probe of the same path with access, realpath, stat and lstat
    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's canonical step calls canonicalizeFilesystemPath
    (dist/core/tools/filesystem-policy.js). After its own 2 s RESOLUTION_DEADLINE_MS, it falls back to
    realpathWithoutOpenStrict (dist/utils/paths.js), which walks the path with synchronous lstatSync. 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

  1. On Windows, call any MCP tool with a string argument https://<reachable host>/<segment>/<more>. In a session
    (--session-id), have the model call mcp_web-reader_webReader with url: https://docs.z.ai/devpack/overview.
  2. Observe the tool result after about 2 minutes: MCP metadata was not ready before the request timeout.
  3. 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 in commandPaths. 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.js and 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 no call_bound_reached entry.

Activity

  1. rhyme227 commented on Oct 23, 2026

    @rhyme227

    Confirmed on a second Windows machine, and still present in the current release (2026.10.21-3).

    Environment

    • senpi 2026.10.21-1 (runtime shipped with the OmO desktop), and 2026.10.21-3 verified by diffing the published package
    • Windows 11 10.0.26300, Bun 1.4.2
    • No MCP server involved in the reproduction below

    Reproduction

    Any tool whose arguments are scanned as command text, given a URL that carries a path segment:

    • eval whose code contains https://<host>/<segment>/<file>.js
    • bash whose command contains curl https://<host>/<segment>

    The TUI shows Running PreToolUse hook: running moved-path-guard and 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, and webfetch itself is exempt (kind: "none"), so the freeze lands on eval, bash, monitor and every unclassified MCP tool.

    Evidence

    <agent-dir>/logs/moved-path-guard.log records call_bound_reached for 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/lstat measurements 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 — no scheme:// stripping
    • dist/core/extensions/builtin/moved-path-guard/index.js — checkTargets has no UNC filter
    • dist/core/tools/filesystem-policy.js — still falls back to the synchronous realpathWithoutOpenStrict

    The 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 inside checkTargets — applied to both the module and the bundled copy the CLI actually loads. With a fake pi registering the tool_call handler and calling it directly:

    input before after
    curl https://<host>/<segment>/<file>.js UNC probe, ~100 s stall 2 ms, not blocked
    dir \\\\<host>\\<share>\\<path> UNC probe 1 ms, not blocked
    cat <home>/a.txt 1 ms 1 ms

    No call_bound_reached entries 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.

  2. katamana commented on Oct 25, 2026

    @katamana

    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.ts still has no scheme:// stripping and filesystem-policy.ts still falls back to the synchronous realpathWithoutOpenStrict.

    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 in Wait/Executive for 5 s. Two captures, 2026-10-24 and 2026-10-25, each taken while an eval whose code contained https://gitee.com/<user>/<repo>/raw/master/<file>.txt was running. Main thread:

    ntdll!NtQueryInformationByName+0x14
    KERNELBASE!GetFileInformationByName+0xd0
    bun!uv_fs_get_system_error+0x7a
    bun!uv_fs_open+0xaa8
    bun!uv_fs_lstat+0x39d
    bun!v8::internal::Internals::GetCurrentIsolate+0x4b72bd   <- JS frames follow (no bun symbols)
    

    At the same moment two worker threads sat in the same GetFileInformationByName call, 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 the realpathWithoutOpenStrict fallback is inference from the source, not from the stack.

    Numbers from this machine (traffic goes through a TUN proxy):

    • Each stall is 28-32 s here. Standalone lstatSync("//gitee.com/<user>") = 27.9 s, lstatSync("//msdl.microsoft.com/download") = 20.0 s, both ECONNRESET.
    • commandPaths from the installed 2026.10.23-1 returns //gitee.com/<user>/<repo>/raw/master/<file>.txt and //msdl.microsoft.com/download/symbols for code containing those URLs.
    • User-visible effect: TUI animation freezes and input is dead, while terminal scrollback still works. One session on 2026-10-23 had 5 stalls of 27-58 s within 30 minutes. One stall coincided with the memory facts extractor's record_fact calls, whose text contained URLs, and their results were delayed by about 60 s.
    • An eval that only waits 20 s on a subprocess, with no URL in its code, does not stall the main thread.
  3. katamana commented on Oct 26, 2026

    @katamana

    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.ts still has no scheme:// handling and filesystem-policy.ts still falls back to realpathWithoutOpenStrict.

    New data point: the stall can hit an idle session, with no user-visible tool call running.

    • 17:54:15 local: the turn ends. The session is idle.
    • 17:55:09: the background memory facts extractor starts a new claude.exe child.
    • 17:55:24 to 17:56:32: the TUI main thread sits in Wait/Executive. That is 69 s, the longest stall I have measured. The input is dead the whole time.
    • 17:56:34: the extractor commits its facts. One of them contains 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:

    ntdll!NtQueryInformationByName+0x14
    KERNELBASE!GetFileInformationByName+0xd0
    bun!uv_fs_get_system_error+0x7a
    bun!uv_fs_open+0xaa8
    bun!uv_fs_lstat+0x39d
    

    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_fact results 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:// in commandPaths would cover both the foreground and the background cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions