Skip to content

Commit bc7ec7a

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 bc7ec7a

8 files changed

Lines changed: 556 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: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ 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 { BlobReader, BlobWriter, ZipReader } from "@zip.js/zip.js"
14+
// altimate_change end
1215

1316
export namespace RipgrepBinary {
1417
const VERSION = "15.1.0"
@@ -26,6 +29,48 @@ export namespace RipgrepBinary {
2629
readonly filepath: Effect.Effect<string, Error>
2730
}
2831

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

3176
export const layer = Layer.effect(
@@ -48,34 +93,40 @@ export namespace RipgrepBinary {
4893
return { stdout, stderr, code }
4994
}, Effect.scoped)
5095

51-
const extract = Effect.fnUntraced(function* (
96+
// altimate_change start — upstream_fix: install the binary atomically.
97+
// `target` is the cache path every later session trusts on existence alone
98+
// (`fs.isFile(target)` below — no size or integrity check). A write interrupted partway
99+
// therefore leaves a truncated `rg.exe` that is reused forever, which is the same
100+
// "permanently broken until the cache is deleted by hand" failure this change exists to
101+
// remove — and `checkSignature` cannot help, since CRC is verified before the write.
102+
// Staging next to the target keeps the rename within one filesystem, so it is atomic.
103+
const install = Effect.fnUntraced(function* (target: string, write: (staged: string) => Effect.Effect<void, Error>) {
104+
const staged = `${target}.tmp`
105+
yield* fs.remove(staged, { force: true }).pipe(Effect.ignore)
106+
yield* write(staged)
107+
if (process.platform !== "win32") yield* fs.chmod(staged, 0o755)
108+
// Windows rename fails if the destination exists; the old binary is already unusable
109+
// by the time we are reinstalling, so dropping it first is safe.
110+
if (process.platform === "win32") yield* fs.remove(target, { force: true }).pipe(Effect.ignore)
111+
yield* fs.rename(staged, target)
112+
})
113+
// altimate_change end
114+
115+
// altimate_change start — upstream_fix: tar.gz path only; zip is handled by unzipExecutable.
116+
const extractTar = Effect.fnUntraced(function* (
52117
archive: string,
53118
config: (typeof PLATFORM)[keyof typeof PLATFORM],
54119
target: string,
55120
) {
56121
const dir = yield* fs.makeTempDirectoryScoped({ directory: Global.Path.bin, prefix: "ripgrep-" })
57122

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-
}
123+
const result = yield* run("tar", ["-xzf", archive, "-C", dir])
124+
// Attribute the failure to ripgrep extraction rather than reporting child stderr verbatim —
125+
// an unattributed shell string is what made the Windows outage undiagnosable.
126+
if (result.code !== 0)
127+
throw new Error(
128+
`ripgrep extraction failed with code ${result.code}: ${result.stderr.trim() || result.stdout.trim() || "no output"}`,
129+
)
79130

80131
const extracted = path.join(
81132
dir,
@@ -84,9 +135,9 @@ export namespace RipgrepBinary {
84135
)
85136
if (!(yield* fs.isFile(extracted))) throw new Error(`ripgrep archive did not contain executable: ${extracted}`)
86137

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

91142
return Service.of({
92143
filepath: yield* Effect.cached(
@@ -103,7 +154,6 @@ export namespace RipgrepBinary {
103154

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

108158
yield* Effect.logInfo("downloading ripgrep", { url })
109159
yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie)
@@ -114,11 +164,31 @@ export namespace RipgrepBinary {
114164
)
115165
if (bytes.byteLength === 0) throw new Error(`failed to download ripgrep from ${url}`)
116166

117-
yield* fs.writeWithDirs(archive, new Uint8Array(bytes))
118-
yield* extract(archive, config, target)
119-
yield* fs.remove(archive, { force: true }).pipe(Effect.ignore)
167+
// altimate_change start — upstream_fix: zip extracts in-process, no PowerShell.
168+
// The staging archive only exists on the tar path, so its cleanup lives there too.
169+
if (config.extension === "zip") {
170+
const decoded = yield* unzipExecutable(bytes)
171+
yield* install(target, (staged) => fs.writeWithDirs(staged, decoded))
172+
} else {
173+
const archive = path.join(Global.Path.bin, filename)
174+
yield* fs.writeWithDirs(archive, new Uint8Array(bytes))
175+
yield* extractTar(archive, config, target)
176+
yield* fs.remove(archive, { force: true }).pipe(Effect.ignore)
177+
}
178+
// altimate_change end
120179
return target
121-
}),
180+
}).pipe(
181+
// altimate_change start — upstream_fix: attribute binary-resolution failures.
182+
// Everything this effect throws itself already names ripgrep, but typed filesystem and
183+
// HTTP failures do not — and a resolve failure is memoized by Effect.cached, so it is
184+
// reported on every subsequent grep of the session. An unattributed message there is
185+
// exactly what made the Windows outage read as a tool bug rather than a binary problem.
186+
Effect.mapError((cause) => {
187+
const message = cause instanceof Error ? cause.message : String(cause)
188+
return /ripgrep/i.test(message) ? cause : new Error(`ripgrep binary resolve failed: ${message}`)
189+
}),
190+
// altimate_change end
191+
),
122192
),
123193
})
124194
}),

0 commit comments

Comments
 (0)