can1357/oh-my-pi · critical
blob broker exposure failed to start
Error message
blob broker exposure failed to start
What it means
After parsing the config, the worker constructs a LocalBlobBackend and calls ensureStarted() to bring up the exposure (server/socket). If ensureStarted() returns null instead of a base URL, the daemon cannot serve, and the worker throws before advertising readiness — by design, a ready daemon must be a serving daemon.
Source
Thrown at packages/coding-agent/src/blob-broker/server.ts:160
}
return json({ error: "not found" }, 404);
};
}
/** Boot the blob daemon from worker environment variables and serve forever. */
export async function startBlobBrokerFromEnvironment(): Promise<void> {
const socketPath = Bun.env[BLOB_BROKER_SOCKET_ENV];
const configJson = Bun.env[BLOB_BROKER_CONFIG_ENV];
if (!socketPath || !configJson) {
throw new Error(`blob broker worker requires ${BLOB_BROKER_SOCKET_ENV} and ${BLOB_BROKER_CONFIG_ENV}`);
}
const config = JSON.parse(configJson) as BlobBrokerWorkerConfig;
const backend = new LocalBlobBackend(config);
// Bring the exposure up before advertising readiness so a ready daemon is a
// serving daemon. Uploader configs have nothing to start.
const baseUrl = isUploaderKind(config.kind) ? "" : await backend.ensureStarted();
if (baseUrl === null) {
throw new Error("blob broker exposure failed to start");
}
try {
fs.rmSync(socketPath, { force: true });
} catch {
// A live daemon holding the socket loses the start race in the broker.
}
Bun.serve({ unix: socketPath, fetch: createControlHandler(backend, config, baseUrl) });
// The daemon broker tears us down with a signal; flush the persisted
// url index rather than losing the debounced write.
process.on("SIGTERM", () => {
backend.stop();
process.exit(0);
});
logger.info("blob-broker daemon up", { kind: config.kind, baseUrl, socketPath });
// Readiness banner consumed by the daemon broker's ready matcher.
console.log(blobBrokerReadyBanner(baseUrl || `upload:${config.kind}`));
// Serve until the daemon broker tears the process down with the project.
await Promise.withResolvers<never>().promise;View on GitHub (pinned to 9690622007)
Solutions
- Check the socket's parent directory exists, is writable, and the path is short enough for a unix socket
- Remove stale socket files: the worker only force-removes the socket AFTER a successful start; clean leftover sockets before restart
- Run the daemon with enough privileges (not as a different user than the directory owner)
- Inspect LocalBlobBackend.ensureStarted logs to find the underlying bind error (EADDRINUSE, EACCES, ENOENT)
Example fix
// before: launching with a socket dir that may not exist
const socketPath = "/tmp/omp-broker/agent.sock";
// after: pre-create and sanitize the directory
await fs.mkdir(path.dirname(socketPath), { recursive: true });
await fs.rm(socketPath, { force: true }); // clear stale socket
await startBlobBrokerFromEnvironment(); Defensive patterns
Strategy: validation
Validate before calling
// pre-flight the socket location before booting the daemon
import * as fs from "node:fs/promises";
await fs.mkdir(path.dirname(socketPath), { recursive: true });
await fs.rm(socketPath, { force: true }); // clear stale socket from a dead daemon Try / catch
try {
await startBlobBrokerFromEnvironment();
} catch (err) {
if (String(err.message).includes("exposure failed to start")) {
logger.error("blob broker listener failed to bind; check socket path/permissions", { socketPath, err });
}
process.exit(1); // worker must not advertise readiness
} Prevention
- Pre-create the socket's parent directory with correct permissions
- Clean stale socket files before restart (a live daemon holding the socket wins the start race)
- Keep socket paths short and inside writable temp dirs
- Check for unix-socket restrictions in sandboxes/containers before deploying
When it happens
Trigger: backend.ensureStarted() returning null: the underlying HTTP listener fails to bind (socket path in a non-existent or unwritable directory, port/path already in use with an unresponsive stale process, permission denied).
Common situations: Stale socket file owned by another user; temp directory cleaned or remounted read-only; path length limits on the Unix socket; sandboxed environment forbidding unix sockets.
Related errors
- Daemon broker socket is unavailable
- Failed to start daemon broker: ${lastError?.message ?? "sock
- Unable to read OMP_AUTH_BROKER_ACCOUNT_POOL_FILE at ${filePa
- blob daemon ${input} responded ${response.status}
- Cannot change working directory to ${parsed.cwd}: ${reason}.
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/030732b95a97b037.
Report an issue: GitHub.