-
Notifications
You must be signed in to change notification settings - Fork 79
Description
Bug & Fix: Compiled Binary Crashes on First Request Due to Worker Failure
Hello @snipeship! What a fantastic piece of technology you have assembled. Worra lorra work/focus. Humbling...
As it says on the tin, I hit a bug with a production build. I'm guessing you might have been using dev-builds lately & not spotted it.
I've patched it, and the patch tests working on dev and prod.
I suspect my patch isn't up to the engineering grade of the rest of the project, so dumping the diff here rather than PR.
Note: I've used Gemini to put this report together, please don't trust it further than you can spit it. Code-chunks are copy-pasted from my terminal, so trust those.
🚀 ❤️
π
Summary of the Issue
The application runs correctly when started from source in development mode (bun run dev). However, the single-file executable produced by the production build command (bun run build) crashes immediately upon receiving its first proxied API request.
The root cause is a multi-part issue involving how the post-processor.worker.ts is bundled, instantiated, and managed within the lifecycle of a compiled Bun executable.
Part 1: Problem Reproduction (Facts)
1. Scripts Used for Testing
The following two scripts are used to build, run the server, and make a test request.
Script ./x (Builds and runs the server):
#!/bin/bash
set -x
clear
rm -rf ./apps/tui/dist
bun run build
# DEVELOPMENT RUN (worked before patch, works after patch)
# bun run dev -- --serve --port 9080
# PRODUCTION RUN (FAILS without patch)
ccflare_DEBUG=1 LOG_LEVEL=DEBUG ./apps/tui/dist/ccflare --serve --port 9080Script ./t (Makes a request to the server):
#!/bin/bash
set -x
export ANTHROPIC_BASE_URL=http://localhost:9080
export PATH="/root/.bun/bin:$PATH"
claude -p "What is the flagship LLM of Google?"2. Observed Behavior (Before Patch)
- Development Run (
bun run dev ...): The server starts, successfully processes the request from the./tscript, and returns a valid response. - Production Run (
./apps/tui/dist/ccflare ...): The server starts, but when the./tscript sends a request, the server immediately crashes with aTypeError: undefined is not an object. This happens because the worker instance fails to be created and isundefinedwhen the code attempts to access it.
Part 2: The Solution
The following changes, applied together, completely resolve the issue and produce a stable, self-contained production binary.
root@L1 /opt/ccflare main
> git status
:
modified: apps/tui/package.json
modified: packages/proxy/src/proxy.ts
Untracked files:
t
xroot@L1 /opt/ccflare main
> git diff
diff --git a/apps/tui/package.json b/apps/tui/package.json
index 5400944..61c0fde 100644
--- a/apps/tui/package.json
+++ b/apps/tui/package.json
@@ -8,7 +8,7 @@
"type": "module",
"scripts": {
"dev": "bun run src/main.ts",
- "build": "bun build src/main.ts --compile --outfile dist/ccflare --target=bun",
+ "build": "bun build src/main.ts ../../packages/proxy/src/post-processor.worker.ts --compile --outfile dist/ccflare --target=bun",
"prepublishOnly": "bun run build",
"postpublish": "chmod +x dist/ccflare"
},
diff --git a/packages/proxy/src/proxy.ts b/packages/proxy/src/proxy.ts
index 4bfb75d..b30ca79 100644
--- a/packages/proxy/src/proxy.ts
+++ b/packages/proxy/src/proxy.ts
@@ -29,27 +29,26 @@ let usageWorkerInstance: Worker | null = null;
*/
export function getUsageWorker(): Worker {
if (!usageWorkerInstance) {
- usageWorkerInstance = new Worker(
- new URL("./post-processor.worker.ts", import.meta.url).href,
- { smol: true },
- );
- // Bun extends Worker with unref method
- if (
- "unref" in usageWorkerInstance &&
- typeof usageWorkerInstance.unref === "function"
- ) {
- usageWorkerInstance.unref(); // Don't keep process alive
- }
+ // In a compiled binary, import.meta.url is not relative to the source tree.
+ // We must provide a path relative to the project root, which Bun
+ // can resolve at compile time.
+ const workerPath = "packages/proxy/src/post-processor.worker.ts";
- // Listen for summary messages from worker
+ usageWorkerInstance = new Worker(workerPath, { smol: true });
+
+ // Restore the original onmessage handler to process data from the worker
usageWorkerInstance.onmessage = (ev) => {
- const data = ev.data as OutgoingWorkerMessage;
+ const data = ev.data as any; // Cast as any to avoid type conflicts in this scope
if (data.type === "summary") {
requestEvents.emit("event", { type: "summary", payload: data.summary });
} else if (data.type === "payload") {
requestEvents.emit("event", { type: "payload", payload: data.payload });
}
};
+
+ // DO NOT call unref(). This was the likely cause of the original
+ // "Worker has been terminated" error. The worker needs to stay
+ // alive to process background tasks.
}
return usageWorkerInstance;
}Part 3: Root Cause Analysis (Interpretation)
The following is an analysis of the root cause based on the successful application of the patch. The failure is not a single bug but a combination of three distinct issues that only manifest in the compiled binary.
1. The Build Problem: Unpackaged Worker Script
The original build script in apps/tui/package.json was bun build src/main.ts --compile .... This command only tells Bun to bundle the main application entry point. It has no knowledge of post-processor.worker.ts because it's instantiated dynamically inside a function. As a result, the worker's code was never included in the final executable.
- Fix: We added the worker script as a second entry point to the build command. This ensures
bun compileis aware of both the main thread and worker thread code and packages them into the same binary.
2. The Pathing Problem: Worker Instantiation Path
The original code used new URL("./post-processor.worker.ts", import.meta.url) to create the worker. While this works when running from source files on a physical filesystem, it fails inside the binary. The import.meta.url resolves to a path inside Bun's virtual filesystem ($bunfs), but the bundler did not map the worker's code to that specific relative path. This caused the new Worker() call to fail, as it couldn't find the module.
- Fix: We changed the instantiation path to be a static string relative to the project root:
const workerPath = "packages/proxy/src/post-processor.worker.ts";. At compile time, Bun sees this static path, recognizes it as a bundled entry point, and correctly replaces it with the proper internal virtual path to the worker's code.
3. The Lifecycle Problem: Premature Worker Termination
The original getUsageWorker function included a call to usageWorkerInstance.unref(). This is an optimization that tells the event loop not to wait for the worker to finish before the application exits. In the context of the compiled binary, this had the fatal side effect of allowing the worker's event loop to terminate immediately after creation, as it had no pending tasks. This led to the InvalidStateError: Worker has been terminated error observed during debugging.
- Fix: We removed the
usageWorkerInstance.unref()call. This ensures the main thread maintains a strong reference to the worker, keeping it alive and ready to receive messages for background processing.
Conclusion
The provided git diff implements the three necessary changes to fix the build process, the runtime path resolution, and the worker's lifecycle management. Together, these changes produce a stable, self-contained executable that functions as intended.