Skip to content

Commit 59e0482

Browse files
anandgupta42claude
andcommitted
fix: Windows grep outage and test-run telemetry pollution
Two production defects found in Azure telemetry over 2026-07-22 → 2026-08-05. **Windows `grep` broken for 99 machines** `core_failure` showed 328 events across 99 distinct Windows machines (of 617 total) carrying `? is not recognized as an internal or external command, operable program or batch file.` and its German, French, Spanish and Portuguese translations. Present on released 0.9.2, 0.9.3 and 0.9.4. Root cause: ripgrep's Windows release is a zip, and `RipgrepBinary` extracted it via `powershell.exe -Command Expand-Archive`, falling back to the literal string `"powershell.exe"` when neither `powershell.exe` nor `pwsh.exe` resolved. `cross-spawn`'s `parseNonShell()` sets `needsShell = true` when `resolveCommand()` returns undefined and re-spawns through `cmd.exe /d /s /c`, so cmd.exe produced that message. `throw new Error(result.stderr.trim())` made it the error verbatim, and since `RipgrepBinary.filepath` is `Effect.cached`, one failed extraction broke grep for the whole session. Upstream carries the same fragility: anomalyco/opencode#24291 is open, reporting `Expand-Archive` unusable when spawned from the Bun-compiled binary, affecting `grep`, `glob` and `skill`. Their #23457 fix only corrected how paths were passed to PowerShell (the `$args` → inlined-and-escaped form we already carry); it did not remove the dependency on PowerShell being resolvable. Extract the zip in-process with `@zip.js/zip.js`, converging on the approach the `packages/opencode/src/file/ripgrep.ts` shim already uses in production. `unzipExecutable` is exported so archive handling is tested directly, and decodes with `checkSignature: true` — zip.js defaults it off, and a CRC-corrupt download would otherwise be written to the cache and trusted by every later session. Install the binary atomically (stage to `rg.exe.tmp`, then rename). `filepath` trusts the cached binary on existence alone, so an interrupted write previously left a truncated `rg.exe` that every later session reused — the same permanent breakage `checkSignature` guards against, which CRC cannot catch because it is verified before the write. The tar path installs the same way. Attribute resolution failures. Child stderr was reported verbatim, so a shell-level failure was indistinguishable from a tool bug; and because a resolve failure is memoized, it is re-reported on every later grep in the session. The tar branch now names ripgrep, and any typed filesystem or HTTP failure is wrapped as `ripgrep binary resolve failed: …`. Note the blast radius is wider than the `grep` tool: `@opencode-ai/core/ripgrep` also backs the HTTP-API file handlers and `cli/cmd/debug/ripgrep.ts`. **Test runs shipped telemetry to the production resource** 1,020 of 3,135 machine ids in the same window emitted `provider_id="test"` / `cli_version="local"` — test processes that regenerate their machine id every run, inflating install and active-machine counts by roughly a third. `doInit()` gated only on `ALTIMATE_TELEMETRY_DISABLED`. Refuse the baked-in connection string when `NODE_ENV=test`, `BUN_TEST`, `VITEST` or `JEST_WORKER_ID` is set. Keyed on test runners, deliberately not on CI: `altimate-code-actions` wraps this CLI, so every run of that shipped product sets `CI`/`GITHUB_ACTIONS`, and gating on those would blind a real product surface. `bun test` sets `NODE_ENV=test`, which covers CI and developer machines alike. An explicit `APPLICATIONINSIGHTS_CONNECTION_STRING` is always honoured, so suites with their own sink are unaffected; `ALTIMATE_TELEMETRY_FORCE=true` overrides the default-sink refusal, and the existing opt-outs still win over both. Test plan: 8 ripgrep tests — including a layer-level test that drives `filepath` through the Windows zip path with a spawner that fails if invoked, asserting the staged-then-renamed install, and a CRC case that fails without `checkSignature` — plus 15 telemetry-gate tests covering CI-alone-still-reports and one that relies on the real runner's `NODE_ENV`. `turbo typecheck` green, marker guard green, `packages/core` and `packages/opencode` suites show no failures beyond the pre-existing set on `main`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 03b9459 commit 59e0482

8 files changed

Lines changed: 605 additions & 32 deletions

File tree

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/docs/reference/telemetry.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,24 @@ export ALTIMATE_TELEMETRY_DISABLED=true
126126

127127
When telemetry is disabled, no events are sent and no network requests are made to the telemetry endpoint.
128128

129+
### Test runs are excluded
130+
131+
Test runners never reach the default telemetry endpoint. Telemetry is suppressed when `NODE_ENV=test`,
132+
`BUN_TEST`, `VITEST`, or `JEST_WORKER_ID` is present. This exists because test processes regenerate
133+
their machine ID on every run, so without the exclusion they dominate install and active-machine counts.
134+
135+
Running in CI is **not** excluded — that is ordinary product usage (for example
136+
[altimate-code-actions](https://github.com/AltimateAI/altimate-code-actions) wraps this CLI), so
137+
`CI` and `GITHUB_ACTIONS` on their own do not suppress anything.
138+
139+
Two escape hatches exist for reporting from a test run deliberately:
140+
141+
- Set `APPLICATIONINSIGHTS_CONNECTION_STRING` to your own endpoint — an explicitly-configured sink
142+
is always honoured, which is how the project's own telemetry tests work.
143+
- Set `ALTIMATE_TELEMETRY_FORCE=true` to use the default endpoint anyway.
144+
145+
`ALTIMATE_TELEMETRY_DISABLED` and the config opt-out take precedence over both.
146+
129147
## Privacy
130148

131149
We take your privacy seriously. Altimate Code telemetry **never** collects:

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"@opentelemetry/sdk-trace-base": "2.6.1",
9898
"@parcel/watcher": "2.5.1",
9999
"@silvia-odwyer/photon-node": "0.3.4",
100+
"@zip.js/zip.js": "2.7.62",
100101
"@openrouter/ai-sdk-provider": "2.9.0",
101102
"ai-gateway-provider": "3.1.2",
102103
"bun-pty": "0.4.8",

packages/core/src/ripgrep.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,11 @@ export const layer = Layer.effect(
140140
return yield* new InvalidPatternError({ pattern: input.pattern, message: stderr.trim() })
141141
}
142142
if (code !== 0 && code !== 1 && code !== 2) {
143-
return yield* failure(stderr.trim() || `ripgrep failed with code ${code}`)
143+
// altimate_change start — upstream_fix: keep child stderr attributable to ripgrep.
144+
// Reporting stderr verbatim made shell-level failures (e.g. a Windows "not recognized"
145+
// message) look like they came from the tool itself, with no hint of the real source.
146+
return yield* failure(`ripgrep failed with code ${code}: ${stderr.trim() || "no output"}`)
147+
// altimate_change end
144148
}
145149
return { items: code === 1 ? [] : rows, truncated: false, partial: code === 2 }
146150
}),

packages/core/src/ripgrep/binary.ts

Lines changed: 121 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { httpClient } from "../effect/layer-node-platform"
99
import { FSUtil } from "../fs-util"
1010
import { Global } from "../global"
1111
import { which } from "../util/which"
12+
// altimate_change start — upstream_fix: unzip in-process instead of shelling out to PowerShell.
13+
import { randomUUID } from "node:crypto"
14+
import { BlobReader, BlobWriter, ZipReader } from "@zip.js/zip.js"
15+
// altimate_change end
1216

1317
export namespace RipgrepBinary {
1418
const VERSION = "15.1.0"
@@ -26,6 +30,48 @@ export namespace RipgrepBinary {
2630
readonly filepath: Effect.Effect<string, Error>
2731
}
2832

33+
// altimate_change start — upstream_fix: unzip in-process instead of shelling out to PowerShell.
34+
// Windows is the only platform that ships ripgrep as a zip, and the previous `Expand-Archive`
35+
// implementation needed a resolvable powershell.exe/pwsh.exe. When neither resolved (locked-down
36+
// or non-English corporate images), cross-spawn silently re-spawned through `cmd.exe /d /s /c`,
37+
// which answers "'powershell.exe' is not recognized as an internal or external command". That
38+
// string became the thrown Error verbatim; `filepath` is Effect.cached, so one failed extraction
39+
// broke grep for the rest of the session. Telemetry showed 99 Windows machines stuck on this.
40+
// Decoding in-process removes the external dependency entirely. Upstream has the same fragility
41+
// open as anomalyco/opencode#24291 (Expand-Archive unusable from a Bun-spawned process) — their
42+
// #23457 fix only corrected how the paths were passed to PowerShell, not the dependency on it.
43+
/** Decode the `rg` executable out of a ripgrep release zip. Exported for tests. */
44+
export const unzipExecutable = Effect.fnUntraced(function* (bytes: ArrayBuffer) {
45+
const reader = new ZipReader(new BlobReader(new Blob([bytes])))
46+
47+
// The reader stays open across both getEntries() and getData() — closing after the first would
48+
// release it while entry reads are still outstanding.
49+
return yield* Effect.gen(function* () {
50+
const entries = yield* Effect.tryPromise({
51+
try: () => reader.getEntries(),
52+
catch: (cause) => new Error(`ripgrep archive could not be read: ${cause}`),
53+
})
54+
55+
// Release zips nest the binary under `ripgrep-<version>-<platform>/`, but match a bare
56+
// `rg.exe` too so a flattened or repackaged archive still works.
57+
const entry = entries.find((x) => !x.directory && /(^|[\\/])rg\.exe$/i.test(x.filename))
58+
if (!entry?.getData) return yield* Effect.fail(new Error("ripgrep archive did not contain rg.exe"))
59+
60+
// checkSignature defaults to false in zip.js, which would let a CRC-corrupt download decode
61+
// "successfully". The bytes are then written to Global.Path.bin and trusted by every later
62+
// session purely because the file exists — a corrupt download would break grep permanently,
63+
// which is the failure class this change exists to remove.
64+
const blob = yield* Effect.tryPromise({
65+
try: () => entry.getData!(new BlobWriter(), { checkSignature: true }),
66+
catch: (cause) => new Error(`ripgrep archive entry could not be decoded: ${cause}`),
67+
})
68+
const decoded = yield* Effect.promise(() => blob.arrayBuffer())
69+
if (decoded.byteLength === 0) return yield* Effect.fail(new Error("ripgrep archive contained an empty rg.exe"))
70+
return new Uint8Array(decoded)
71+
}).pipe(Effect.ensuring(Effect.promise(() => reader.close()).pipe(Effect.ignore)))
72+
})
73+
// altimate_change end
74+
2975
export class Service extends Context.Service<Service, Interface>()("@opencode/RipgrepBinary") {}
3076

3177
export const layer = Layer.effect(
@@ -48,34 +94,60 @@ export namespace RipgrepBinary {
4894
return { stdout, stderr, code }
4995
}, Effect.scoped)
5096

51-
const extract = Effect.fnUntraced(function* (
97+
// altimate_change start — upstream_fix: install the binary atomically.
98+
// `target` is the cache path every later session trusts on existence alone
99+
// (`fs.isFile(target)` below — no size or integrity check). A write interrupted partway
100+
// therefore leaves a truncated `rg.exe` that is reused forever, which is the same
101+
// "permanently broken until the cache is deleted by hand" failure this change exists to
102+
// remove — and `checkSignature` cannot help, since CRC is verified before the write.
103+
// Staging next to the target keeps the rename within one filesystem, so it is atomic.
104+
const install = Effect.fnUntraced(
105+
function* (target: string, write: (staged: string) => Effect.Effect<void, Error>) {
106+
// Staging name is unique per attempt. A shared `${target}.tmp` lets two cold-cache
107+
// processes clobber each other: one renames while the other is still writing, so the
108+
// loser publishes a partial binary or renames a file that no longer exists.
109+
const staged = `${target}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`
110+
yield* Effect.gen(function* () {
111+
yield* write(staged)
112+
if (process.platform !== "win32") yield* fs.chmod(staged, 0o755)
113+
// POSIX rename replaces atomically. Windows fails when the destination exists, so
114+
// retry once after removing it — but only after the first attempt has failed, so a
115+
// rename that fails for any other reason leaves the existing binary untouched.
116+
yield* fs.rename(staged, target).pipe(
117+
Effect.catch(() =>
118+
Effect.gen(function* () {
119+
yield* fs.remove(target, { force: true }).pipe(Effect.ignore)
120+
yield* fs.rename(staged, target)
121+
}),
122+
),
123+
)
124+
}).pipe(Effect.onError(() => fs.remove(staged, { force: true }).pipe(Effect.ignore)))
125+
},
126+
// Name the failure. A resolve failure is memoized by Effect.cached, so it is re-reported on
127+
// every later grep of the session — an unattributed filesystem message there is what made
128+
// the Windows outage read as a tool bug rather than a binary problem.
129+
Effect.mapError((cause) => {
130+
const message = cause instanceof Error ? cause.message : String(cause)
131+
return /ripgrep/i.test(message) ? cause : new Error(`ripgrep binary install failed: ${message}`)
132+
}),
133+
)
134+
// altimate_change end
135+
136+
// altimate_change start — upstream_fix: tar.gz path only; zip is handled by unzipExecutable.
137+
const extractTar = Effect.fnUntraced(function* (
52138
archive: string,
53139
config: (typeof PLATFORM)[keyof typeof PLATFORM],
54140
target: string,
55141
) {
56142
const dir = yield* fs.makeTempDirectoryScoped({ directory: Global.Path.bin, prefix: "ripgrep-" })
57143

58-
if (config.extension === "zip") {
59-
const shell = (yield* Effect.sync(() => which("powershell.exe") ?? which("pwsh.exe"))) ?? "powershell.exe"
60-
const result = yield* run(shell, [
61-
"-NoProfile",
62-
"-NonInteractive",
63-
"-Command",
64-
`$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -LiteralPath '${archive.replaceAll("'", "''")}' -DestinationPath '${dir.replaceAll("'", "''")}' -Force`,
65-
])
66-
if (result.code !== 0)
67-
throw new Error(
68-
result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`,
69-
)
70-
}
71-
72-
if (config.extension === "tar.gz") {
73-
const result = yield* run("tar", ["-xzf", archive, "-C", dir])
74-
if (result.code !== 0)
75-
throw new Error(
76-
result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`,
77-
)
78-
}
144+
const result = yield* run("tar", ["-xzf", archive, "-C", dir])
145+
// Attribute the failure to ripgrep extraction rather than reporting child stderr verbatim —
146+
// an unattributed shell string is what made the Windows outage undiagnosable.
147+
if (result.code !== 0)
148+
throw new Error(
149+
`ripgrep extraction failed with code ${result.code}: ${result.stderr.trim() || result.stdout.trim() || "no output"}`,
150+
)
79151

80152
const extracted = path.join(
81153
dir,
@@ -84,9 +156,9 @@ export namespace RipgrepBinary {
84156
)
85157
if (!(yield* fs.isFile(extracted))) throw new Error(`ripgrep archive did not contain executable: ${extracted}`)
86158

87-
yield* fs.copyFile(extracted, target)
88-
if (process.platform !== "win32") yield* fs.chmod(target, 0o755)
159+
yield* install(target, (staged) => fs.copyFile(extracted, staged))
89160
}, Effect.scoped)
161+
// altimate_change end
90162

91163
return Service.of({
92164
filepath: yield* Effect.cached(
@@ -103,20 +175,40 @@ export namespace RipgrepBinary {
103175

104176
const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}`
105177
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}`
106-
const archive = path.join(Global.Path.bin, filename)
107178

108179
yield* Effect.logInfo("downloading ripgrep", { url })
109180
yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie)
110181
const bytes = yield* HttpClientRequest.get(url).pipe(
111182
http.execute,
112183
Effect.flatMap((response) => response.arrayBuffer),
113-
Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))),
184+
// altimate_change start — upstream_fix: name the download failure.
185+
// A bare HttpClientError/ResponseError says nothing about ripgrep, and because
186+
// `filepath` is Effect.cached it is then re-reported on every later grep of the
187+
// session — a network block or proxy 403 would read as a grep bug.
188+
Effect.mapError((cause) => {
189+
const message = cause instanceof Error ? cause.message : String(cause)
190+
return /ripgrep/i.test(message)
191+
? cause instanceof Error
192+
? cause
193+
: new Error(message)
194+
: new Error(`ripgrep download failed from ${url}: ${message}`)
195+
}),
196+
// altimate_change end
114197
)
115198
if (bytes.byteLength === 0) throw new Error(`failed to download ripgrep from ${url}`)
116199

117-
yield* fs.writeWithDirs(archive, new Uint8Array(bytes))
118-
yield* extract(archive, config, target)
119-
yield* fs.remove(archive, { force: true }).pipe(Effect.ignore)
200+
// altimate_change start — upstream_fix: zip extracts in-process, no PowerShell.
201+
// The staging archive only exists on the tar path, so its cleanup lives there too.
202+
if (config.extension === "zip") {
203+
const decoded = yield* unzipExecutable(bytes)
204+
yield* install(target, (staged) => fs.writeWithDirs(staged, decoded))
205+
} else {
206+
const archive = path.join(Global.Path.bin, filename)
207+
yield* fs.writeWithDirs(archive, new Uint8Array(bytes))
208+
yield* extractTar(archive, config, target)
209+
yield* fs.remove(archive, { force: true }).pipe(Effect.ignore)
210+
}
211+
// altimate_change end
120212
return target
121213
}),
122214
),

0 commit comments

Comments
 (0)