{"record":{"id":"e81e8f850ad4efe0","repo":"can1357/oh-my-pi","slug":"no-built-in-uploader-or-serving-adapter-is-impleme","errorCode":null,"errorMessage":"no built-in uploader or serving adapter is implemented","messagePattern":"no built-in uploader or serving adapter is implemented","errorType":"exception","errorClass":"DestinationUnavailableError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/broker.ts","lineNumber":104,"sourceCode":"\t#exposure: { baseUrl: string; stop(): void } | undefined;\n\t#startPromise: Promise<string | null> | undefined;\n\t#upload: ((hash: string, request: BlobUploadRequest) => Promise<BlobPublication | null>) | undefined;\n\t#fetch: typeof globalThis.fetch;\n\t#dead = false;\n\n\t/** Create one local serving or configured upload backend. */\n\tconstructor(config: BlobBrokerWorkerConfig, fetchFn: typeof globalThis.fetch = globalThis.fetch) {\n\t\tthis.#config = config;\n\t\tthis.#fetch = fetchFn;\n\t\tconst servesBlobs = isServeKind(config.kind);\n\t\tconst uploader = servesBlobs\n\t\t\t? null\n\t\t\t: createConfiguredUploader(config.kind, {\n\t\t\t\t\toptions: config.options,\n\t\t\t\t\tcredentials: config.credentials,\n\t\t\t\t});\n\t\tif (!servesBlobs && !uploader) {\n\t\t\tthrow new DestinationUnavailableError(config.kind, \"no built-in uploader or serving adapter is implemented\");\n\t\t}\n\t\tthis.#store = new BlobRegistry({ persist: config.persist });\n\t\tif (uploader) this.#upload = memoizeUploader(uploader);\n\t}\n\n\t/** Whether this backend can render blobs on fetch. */\n\tget supportsLazy(): boolean {\n\t\treturn this.#upload === undefined;\n\t}\n\n\t/**\n\t * Start the local server and exposure once (serve mode); resolves to the\n\t * public base URL or `null` after a failure (sticky for this backend).\n\t */\n\tensureStarted(): Promise<string | null> {\n\t\tif (this.#upload) return Promise.resolve(null);\n\t\tthis.#startPromise ??= this.#start();\n\t\treturn this.#startPromise;","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/broker.ts#L86-L122","documentation":"The LocalBlobBackend constructor resolves the configured destination kind either to a serve adapter (tunnel/direct kinds) or an uploader factory. If the kind is neither in SERVE_KINDS nor has a registered uploader, no adapter exists for it and the constructor throws DestinationUnavailableError. This guards against misconfigured or unknown destination ids reaching the broker.","triggerScenarios":"Constructing LocalBlobBackend with a BlobBrokerWorkerConfig whose kind is not one of the serve kinds (cloudflared, ngrok, tailscale, ssh, direct, localhost-run, pinggy, devtunnel, zrok, bore, named-cloudflared) and has no uploader implementation — e.g. a typo'd destination id or a new kind added to config before the adapter shipped.","commonSituations":"Settings file contains an unsupported imageUrls destination string; version skew where a config written by a newer omp is read by an older binary; typo like \"cloudflare\" instead of \"cloudflared\".","solutions":["Correct the destination kind in the blob/imageUrls settings to a supported value (e.g. cloudflared, ngrok, tailscale, ssh, direct).","Upgrade omp so the destination kind's adapter exists.","Run the blob broker doctor command to validate the configured destination before starting sessions."],"exampleFix":"// before\nnew LocalBlobBackend({ kind: \"cloudflare\" as BlobDestinationId, options: {}, credentials: {} });\n// after\nnew LocalBlobBackend({ kind: \"cloudflared\", options: {}, credentials: {} });","handlingStrategy":"validation","validationCode":"import { isServeKind, isUploaderKind } from \"./blob-broker/broker\";\nconst SERVE = [\"cloudflared\",\"ngrok\",\"tailscale\",\"ssh\",\"direct\",\"localhost-run\",\"pinggy\",\"devtunnel\",\"zrok\",\"bore\",\"named-cloudflared\"];\nif (!isServeKind(config.kind) && !isUploaderKind(config.kind)) {\n  throw new Error(`destination \"${config.kind}\" has no adapter; pick one of: ${SERVE.join(\", \")}`);\n}","typeGuard":"function isKnownDestination(kind: string): kind is BlobDestinationId {\n  return [\"cloudflared\",\"ngrok\",\"tailscale\",\"ssh\",\"direct\",\"localhost-run\",\"pinggy\",\"devtunnel\",\"zrok\",\"bore\",\"named-cloudflared\"].includes(kind);\n}","tryCatchPattern":"try {\n  backend = new LocalBlobBackend(config);\n} catch (err) {\n  if (err instanceof DestinationUnavailableError) {\n    logger.warn(\"destination unavailable; disabling image URLs\", { kind: config.kind });\n    backend = null;\n  } else throw err;\n}","preventionTips":["Validate the imageUrls destination id against the supported list at settings-load time.","Avoid hand-editing config strings; use the settings UI/enum so typos are impossible.","Pin the omp version that introduced a new destination before enabling it in config."],"tags":["configuration","unsupported-feature"],"backgroundTag":"unsupported-destination-kind","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}