{"record":{"id":"030732b95a97b037","repo":"can1357/oh-my-pi","slug":"blob-broker-exposure-failed-to-start","errorCode":null,"errorMessage":"blob broker exposure failed to start","messagePattern":"blob broker exposure failed to start","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"packages/coding-agent/src/blob-broker/server.ts","lineNumber":160,"sourceCode":"\t\t}\n\t\treturn json({ error: \"not found\" }, 404);\n\t};\n}\n\n/** Boot the blob daemon from worker environment variables and serve forever. */\nexport async function startBlobBrokerFromEnvironment(): Promise<void> {\n\tconst socketPath = Bun.env[BLOB_BROKER_SOCKET_ENV];\n\tconst configJson = Bun.env[BLOB_BROKER_CONFIG_ENV];\n\tif (!socketPath || !configJson) {\n\t\tthrow new Error(`blob broker worker requires ${BLOB_BROKER_SOCKET_ENV} and ${BLOB_BROKER_CONFIG_ENV}`);\n\t}\n\tconst config = JSON.parse(configJson) as BlobBrokerWorkerConfig;\n\tconst backend = new LocalBlobBackend(config);\n\t// Bring the exposure up before advertising readiness so a ready daemon is a\n\t// serving daemon. Uploader configs have nothing to start.\n\tconst baseUrl = isUploaderKind(config.kind) ? \"\" : await backend.ensureStarted();\n\tif (baseUrl === null) {\n\t\tthrow new Error(\"blob broker exposure failed to start\");\n\t}\n\ttry {\n\t\tfs.rmSync(socketPath, { force: true });\n\t} catch {\n\t\t// A live daemon holding the socket loses the start race in the broker.\n\t}\n\tBun.serve({ unix: socketPath, fetch: createControlHandler(backend, config, baseUrl) });\n\t// The daemon broker tears us down with a signal; flush the persisted\n\t// url index rather than losing the debounced write.\n\tprocess.on(\"SIGTERM\", () => {\n\t\tbackend.stop();\n\t\tprocess.exit(0);\n\t});\n\tlogger.info(\"blob-broker daemon up\", { kind: config.kind, baseUrl, socketPath });\n\t// Readiness banner consumed by the daemon broker's ready matcher.\n\tconsole.log(blobBrokerReadyBanner(baseUrl || `upload:${config.kind}`));\n\t// Serve until the daemon broker tears the process down with the project.\n\tawait Promise.withResolvers<never>().promise;","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/server.ts#L142-L178","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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)"],"exampleFix":"// before: launching with a socket dir that may not exist\nconst socketPath = \"/tmp/omp-broker/agent.sock\";\n// after: pre-create and sanitize the directory\nawait fs.mkdir(path.dirname(socketPath), { recursive: true });\nawait fs.rm(socketPath, { force: true }); // clear stale socket\nawait startBlobBrokerFromEnvironment();","handlingStrategy":"validation","validationCode":"// pre-flight the socket location before booting the daemon\nimport * as fs from \"node:fs/promises\";\nawait fs.mkdir(path.dirname(socketPath), { recursive: true });\nawait fs.rm(socketPath, { force: true }); // clear stale socket from a dead daemon","typeGuard":null,"tryCatchPattern":"try {\n  await startBlobBrokerFromEnvironment();\n} catch (err) {\n  if (String(err.message).includes(\"exposure failed to start\")) {\n    logger.error(\"blob broker listener failed to bind; check socket path/permissions\", { socketPath, err });\n  }\n  process.exit(1); // worker must not advertise readiness\n}","preventionTips":["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"],"tags":["daemon","startup","socket","server"],"backgroundTag":"server-start-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}