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

  1. Check the socket's parent directory exists, is writable, and the path is short enough for a unix socket
  2. Remove stale socket files: the worker only force-removes the socket AFTER a successful start; clean leftover sockets before restart
  3. Run the daemon with enough privileges (not as a different user than the directory owner)
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/030732b95a97b037. Report an issue: GitHub.