Skip to content

core: ripgrep auto-download fails silently behind Windows system proxy and never recovers until restart #51022

Description

@ganfabo123-cmyk

Summary

On Windows, opencode's on-demand ripgrep download does not honor the system proxy configured in Windows Internet Settings (WinINET registry) — it only respects HTTP_PROXY/HTTPS_PROXY env vars. On networks where GitHub is only reachable through that proxy, the download fails, the failure is reported to the model only as the generic ripgrep execution failed (nothing in the server log after the downloading ripgrep INFO line), and the resolver never recovers for the lifetime of the server process: grep/glob/find stay broken until a full restart, even after a valid rg.exe is manually placed where the resolver looks.

Environment

  • opencode version: v2.0.12 (npm global @opencode/cli)
  • OS: Windows_NT 10.0.26200 (win32 x64)
  • Terminal: VS Code (TERM_PROGRAM=vscode)
  • Shell: agent shell runs PowerShell 5.1 (ComSpec is cmd.exe)
  • Install/channel: npm, release
  • Active plugins: none configured

Reproduction

  1. Windows machine where github.com is only reachable via a local proxy configured in Windows Internet Settings (WinINET registry: ProxyEnable=1, ProxyServer=127.0.0.1:7890), with no HTTP_PROXY/HTTPS_PROXY env vars set. (netsh winhttp show proxy shows direct; Invoke-WebRequest to the same release URL returns HTTP 200 because .NET honors the registry proxy — the network itself is fine.)
  2. Ensure rg is not on PATH and %USERPROFILE%\.cache\opencode\bin\rg.exe does not exist.
  3. Start opencode and trigger the grep tool.
  4. Server log (~/.local/share/opencode/log/opencode.log) shows:
    level=INFO message="downloading ripgrep" url=https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-x86_64-pc-windows-msvc.zip
    
    …and then nothing: no error entry, %USERPROFILE%\.cache\opencode\bin stays empty.
  5. The tool result returned to the model is only: ripgrep execution failed — no hint that the download was the failing step.
  6. Manually place a known-good rg.exe (verified runnable directly and spawnable from Node child_process) at %USERPROFILE%\.cache\opencode\bin\rg.exe and call grep again:
    • still ripgrep execution failed
    • no new downloading ripgrep entry (resolver does not re-run)
    • no process spawn for rg
  7. Only after fully exiting opencode (including the serve --service backend) and relaunching does grep work — the freshly placed binary is picked up on the first call, no network needed.

Expected Behavior

  • The download should either honor the OS proxy configuration or at minimum honor HTTPS_PROXY-style env vars consistently, and fail loudly (the underlying cause should reach the log / tool error).
  • A resolver failure should not persist for the process lifetime: subsequent calls should retry, and a manually-provided binary on disk should be picked up without a restart.

Actual Behavior

After the failed/hung download, every subsequent grep/glob call fails instantly with ripgrep execution failed. No retry is logged, no rg process is ever spawned, and manually fixing the binary on disk has no effect until the app is restarted. This made the root cause (proxy-blind download) very hard to diagnose: the error message points at "ripgrep execution" while the real failure is a network fetch several layers earlier.

Additional Context

  • Affected code (v2.0.12):
    • packages/core/src/ripgrep/binary.ts — resolver wrapped in Effect.cached: which() → <cache>/bin/rg.exe → download via effect HttpClient (no proxy options, no visible timeout). Effect.cached explains why the failed/pending computation is never retried for the process lifetime.
    • packages/core/src/ripgrep.ts — every non-Error/InvalidPatternError failure is mapped to failure("ripgrep execution failed", cause), and the cause never reaches the model or the log.
  • Timeline observed in the log: one downloading ripgrep entry per fresh failure on day 1 and day 2 (retries did happen while each fetch failed fast), then after the final attempt no further attempts were ever logged for that server process — consistent with the cached resolver holding a failed/pending result.
  • Workaround that works: download the ripgrep zip out-of-band (through the system proxy), extract, and copy rg.exe to %USERPROFILE%\.cache\opencode\bin\rg.exe, then restart opencode. Alternatively put rg.exe on PATH before first use.
  • Possible directions:
    • pass proxy config / honor env+OS proxy in the ripgrep download client, and add a fetch timeout
    • log the download failure at ERROR level and include the underlying cause in the mapped tool error
    • invalidate the cached resolver on failure (or re-check the disk path on every call) so a manually-placed binary is adopted without a restart

Activity

aron-intframe commented on Sep 24, 2026

@aron-intframe

Confirmed the "never recovers" half at the source level, and it is not proxy- or Windows-specific: Effect.cached at packages/core/src/ripgrep/binary.ts:92 memoizes the failure, so the resolver body never runs a second time and the <cache>/bin/rg check at line 98 is never re-evaluated.

I mirrored the resolver (same order: which() -> <cache>/bin/rg -> logInfo -> http.execute) against the effect build this repo pins ([email protected]) and ran it:

effect 4.0.0-beta.83 | node v24.21.0

== A. as shipped: Effect.cached(...), download fails once ==
    [log] level=INFO message="downloading ripgrep" url=https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-x86_64-pc-windows-msvc.zip
  call #1: FAIL -> Error: fetch failed   (resolver body ran 1x, disk checked 1x)
  -- operator drops a known-good rg into the cache dir (report step 6) --
  call #2: FAIL -> Error: fetch failed   (resolver body ran 1x, disk checked 1x)
  call #3: FAIL -> Error: fetch failed   (resolver body ran 1x, disk checked 1x)

== B. control: identical body, no Effect.cached ==
    [log] level=INFO message="downloading ripgrep" url=https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-x86_64-pc-windows-msvc.zip
  call #1: FAIL -> Error: fetch failed   (resolver body ran 1x, disk checked 1x)
  -- operator drops a known-good rg into the cache dir (report step 6) --
  call #2: ok -> /tmp/rg-cache-hzSR8w/rg   (resolver body ran 2x, disk checked 2x)
  call #3: ok -> /tmp/rg-cache-hzSR8w/rg   (resolver body ran 3x, disk checked 3x)

== C. as shipped: user hits Esc while the first download is in flight ==
    [log] level=INFO message="downloading ripgrep" url=https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-x86_64-pc-windows-msvc.zip
  call #1: FAIL -> Error: aborted   (resolver body ran 1x, disk checked 1x)
  -- operator drops a known-good rg into the cache dir (report step 6) --
  call #2: FAIL -> Error: All fibers interrupted without error   (resolver body ran 1x, disk checked 1x)
  call #3: FAIL -> Error: All fibers interrupted without error   (resolver body ran 1x, disk checked 1x)

== D. ripgrep.ts:147 mapError applied to that same failure ==
  (binary error) instanceof (module-local Error, ripgrep.ts:41) = false
  message handed to the model : "ripgrep execution failed"
  real cause, never logged    : "Error: fetch failed"

A is your steps 4-7 exactly: one failed download, then calls 2 and 3 still fail with the cached error, resolver body ran 1x, disk checked 1x, so the rg you placed in the cache dir is never looked at. B is the identical body without Effect.cached and recovers on the very next call, no restart.

Two things the report does not cover yet.

C - you do not need a proxy to hit this. ripgrep.ts:144 races the program against waitForAbort(input.signal). If the first grep is cancelled while the download is still in flight, the interrupt is what gets memoized, and every later call in that process fails with All fibers interrupted without error, which then maps to ripgrep execution failed. That reproduces on Linux/macOS with a perfectly good network. Related: binary.ts:110-114 has no timeout on http.execute, so a proxy that blackholes the connection parks the cached deferred instead of failing it, and every later caller waits on that same deferred.

D - why the cause never reaches the log or the model. ripgrep.ts:41 is export class Error extends Schema.TaggedErrorClass(...), which shadows the global Error for the rest of that module. The download failure built at binary.ts:113 is a plain Error, so cause instanceof Error at line 147 is false and it falls into failure("ripgrep execution failed", cause). The cause is attached to the tagged error but nothing ever logs it, which is exactly the "nothing in the log after the INFO line" you saw.

Minimal standalone repro for C, no Windows and no proxy needed:

import { Cause, Effect } from "effect"
const body = Effect.gen(function* () {
  console.log("resolver body ran")
  return yield* Effect.never                        // download blackholed by the proxy
})
const filepath = await Effect.runPromise(Effect.cached(body))
const abort = (ms) => Effect.callback((resume) => { setTimeout(() => resume(Effect.fail(new Error("aborted"))), ms) })
const show = async (n, eff) => {
  const exit = await Effect.runPromiseExit(eff)
  console.log(`call #${n}:`, exit._tag === "Success" ? exit.value : String(Cause.squash(exit.cause)))
}
await show(1, filepath.pipe(Effect.raceFirst(abort(300))))   // ripgrep.ts:144 abort race
await show(2, filepath)                                      // body never runs again
await show(3, filepath)
$ node snippet.mjs
resolver body ran
call #1: Error: aborted
call #2: Error: All fibers interrupted without error
call #3: Error: All fibers interrupted without error

The resolver body runs once and never again; call #1's interrupt is what the cache latches.

On the proxy half specifically, there is no proxy handling anywhere in the tree, so the download client is whatever the runtime's global fetch does and nothing more:

$ git log -1 --format='%h %ad %s' --date=short   # current main
5fcfb06 2026-09-24 chore: update nix node_modules hashes

$ grep -rniE 'proxyAgent|HTTPS?_PROXY|NODE_USE_ENV_PROXY|setGlobalDispatcher' --include='*.ts' packages/ | wc -l
0

$ sed -n '10p' packages/core/src/effect/app-node-platform.ts
export const httpClient = makeGlobalNode({ service: HttpClient.HttpClient, layer: FetchHttpClient.layer, deps: [] })

$ sed -n '92p;108,115p' packages/core/src/ripgrep/binary.ts
        filepath: yield* Effect.cached(
            yield* Effect.logInfo("downloading ripgrep", { url })
            yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie)
            const bytes = yield* HttpClientRequest.get(url).pipe(
              http.execute,
              Effect.flatMap((response) => response.arrayBuffer),
              Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))),
            )
            if (bytes.byteLength === 0) throw new Error(`failed to download ripgrep from ${url}`)

Any fix there has to install a dispatcher/agent explicitly; reading HTTP_PROXY/HTTPS_PROXY in application code will not change FetchHttpClient on its own.

Worth knowing before anyone writes a patch: the resolver half is already filed as Bug 2 of the still-open #40623, and a fix for it ("single-flight memo that caches only successful resolutions") has been submitted twice and landed neither time. #41015 was auto-closed by the PR-standards bot for a missing linked issue, and #41028 was closed by its author for lack of review bandwidth, with the branch kept at Yuxin-Qiao/opencode-fix fix/ripgrep-windows-resolution. Current main (5fcfb06) still has Effect.cached at binary.ts:92. So the genuinely new content in this issue is the proxy blindness, the missing fetch timeout, and the swallowed cause; the "never recovers" part has a ready patch that just needs a review.

Same shape bit us in our own scheduled agents: a failure that announces itself once and then latches, after which "broken" and "fine" emit identical output. The catalog entry we keep for it is in our repo (ours): https://github.com/aron-intframe/agent-guards#3-the-longer-something-rots-the-quieter-it-gets

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions