mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(cli): suppress EPIPE crashes in piped agent environments (#1184)
* fix(cli): suppress EPIPE crashes in piped agent environments When the CLI runs inside a piped environment (Claude Code, Codex, Cursor), the reader may close the pipe before we finish writing. Node treats EPIPE on stdout/stderr as an uncaughtException, crashing the process with a non-zero exit code. Add stream-level EPIPE handlers on stdout/stderr at the top of the entry point (before any output) and make the uncaughtException handler EPIPE-aware so it exits cleanly (code 0) instead of crash-reporting. PostHog data: ~10,300 EPIPE errors over 10 days, contributing to the preview command's 43-59% failure rate in agent environments. * fix(cli): set commandFailed before EPIPE exit to prevent false success telemetry EPIPE is a pipe-reader-closed signal, not a successful run. The exit handler uses 'code === 0 && !commandFailed' to determine success — without setting commandFailed=true before process.exit(0), every EPIPE exit was recorded as success:true in telemetry. Moves the commandFailed declaration to the top of the file so the stream-error EPIPE handlers (which must run before any writes) can reference it. Also sets commandFailed=true in the uncaughtException EPIPE path for the same reason.
This commit is contained in:
+25
-1
@@ -1,5 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// ── EPIPE suppression (must run before ANY stdout/stderr write) ────────────
|
||||
// When the CLI runs inside a piped agent environment (Claude Code, Codex,
|
||||
// Cursor, etc.), the reader may close the pipe before we finish writing.
|
||||
// Node treats EPIPE on stdout/stderr as an uncaughtException, which crashes
|
||||
// the process. This is a normal lifecycle event — suppress it.
|
||||
//
|
||||
// commandFailed must be declared here (before the handlers) so the EPIPE
|
||||
// stream-error path can set it before process.exit(0). The telemetry exit
|
||||
// handler reads this flag to determine success/failure — an EPIPE exit
|
||||
// should NOT score as success:true in telemetry.
|
||||
let commandFailed = false;
|
||||
|
||||
for (const stream of [process.stdout, process.stderr]) {
|
||||
stream.on("error", (err) => {
|
||||
if ((err as NodeJS.ErrnoException).code === "EPIPE") {
|
||||
commandFailed = true;
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Worker entry path bootstrap (must run before any producer/engine load) ──
|
||||
// The hf#677 worker_threads pools (`pngDecodeBlitWorkerPool`,
|
||||
// `shaderTransitionWorkerPool`) live in the producer package and try to
|
||||
@@ -194,7 +215,6 @@ if (!isHelp && !hasJsonFlag && command !== "upgrade") {
|
||||
}
|
||||
|
||||
const commandStart = Date.now();
|
||||
let commandFailed = false;
|
||||
|
||||
// Async flush for normal exit. `beforeExit` re-fires every time the
|
||||
// event loop drains, and the async `_flush()` itself schedules new
|
||||
@@ -220,6 +240,10 @@ process.on("exit", (code) => {
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
if ((error as NodeJS.ErrnoException).code === "EPIPE") {
|
||||
commandFailed = true;
|
||||
process.exit(0);
|
||||
}
|
||||
commandFailed = true;
|
||||
_trackCliError?.({
|
||||
error_name: error.name,
|
||||
|
||||
Reference in New Issue
Block a user