{"record":{"id":"a28f3f892141c25e","repo":"can1357/oh-my-pi","slug":"error-a28f3f","errorCode":null,"errorMessage":"error","messagePattern":"error","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/collab/host.ts","lineNumber":222,"sourceCode":"\t#sendWritablePeers(frame: CollabFrame): void {\n\t\tconst socket = this.#socket;\n\t\tif (!socket) return;\n\t\tfor (const [peerId, peer] of this.#peers) {\n\t\t\tif (peer.canWrite) socket.send(frame, peerId);\n\t\t}\n\t}\n\n\tasync start(relayUrl: string, webUrl = \"\"): Promise<void> {\n\t\tconst rawKey = generateRoomKey();\n\t\tconst writeToken = generateWriteToken();\n\t\tconst roomId = generateRoomId();\n\t\tthis.#writeToken = writeToken;\n\t\tthis.#link = formatCollabLink(relayUrl, roomId, rawKey, writeToken);\n\t\tthis.#webLink = formatCollabWebLink(relayUrl, roomId, rawKey, writeToken, webUrl);\n\t\tthis.#viewLink = formatCollabLink(relayUrl, roomId, rawKey);\n\t\tthis.#webViewLink = formatCollabWebLink(relayUrl, roomId, rawKey, undefined, webUrl);\n\t\tconst parsed = parseCollabLink(this.#link);\n\t\tif (\"error\" in parsed) throw new Error(parsed.error);\n\t\tconst key = await importRoomKey(rawKey);\n\n\t\tconst socket = new CollabSocket({ wsUrl: parsed.wsUrl, role: \"host\", key });\n\t\tthis.#socket = socket;\n\t\tthis.#sessionId = this.#ctx.sessionManager.getSessionId();\n\n\t\tconst firstOpen = Promise.withResolvers<void>();\n\t\tlet opened = false;\n\t\tsocket.onOpen = () => {\n\t\t\tif (!opened) {\n\t\t\t\topened = true;\n\t\t\t\tfirstOpen.resolve();\n\t\t\t}\n\t\t};\n\t\tsocket.onFrame = (frame, fromPeer) => this.#handleFrame(frame, fromPeer);\n\t\tsocket.onControl = msg => {\n\t\t\tif (msg.t === \"peer-left\") this.#handlePeerLeft(msg.peer);\n\t\t};","sourceCodeStart":204,"sourceCodeEnd":240,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/collab/host.ts#L204-L240","documentation":"CollabHost.start re-parses the link it just formatted with formatCollabLink as a self-check, and throws when the parser returns an \"error\" result. Since the link is built from an internally generated roomId/key, this almost always means the relay URL configured for the session could not be normalized (invalid URL or unsupported/forbidden scheme).","triggerScenarios":"Calling CollabHost.start with a relayUrl that is not a valid URL ('', 'myrelay.example', 'localhost:9000'), uses an unsupported scheme (ftp://, wss:garbage), or uses ws:// to a non-localhost host (normalizeRelayOrigin rejects plain ws:// off localhost).","commonSituations":"A config file sets collab.relayUrl to a bare hostname with no scheme; a user enters 'ws://relay.example.com' for a public relay (plain ws is localhost-only); a typo like 'wss:/' or stray spaces makes new URL() throw; port-only or protocol-relative strings.","solutions":["Set the relay URL to a full ws:// or wss:// URL including scheme and host (e.g. wss://relay.example.com).","Use wss:// (not ws://) for any non-localhost relay; plain ws:// is only accepted for localhost/127.0.0.1.","Test the value with new URL(relayUrl) before saving it to config.","Omit the setting entirely to use the default relay, which needs no normalization."],"exampleFix":"// before\nrelayUrl: \"myrelay.example.com:8080\" // not a valid absolute URL\n// after\nrelayUrl: \"wss://myrelay.example.com:8080\"","handlingStrategy":"validation","validationCode":"function relayUrlOk(url: string): boolean {\n  try {\n    const u = new URL(url);\n    if (![\"ws:\", \"wss:\", \"http:\", \"https:\"].includes(u.protocol)) return false;\n    if (u.protocol === \"ws:\" && ![\"localhost\", \"127.0.0.1\", \"::1\"].includes(u.hostname)) return false;\n    return true;\n  } catch { return false; }\n}\nif (!relayUrlOk(relayUrl)) throw new Error(`bad relayUrl: ${relayUrl}`);\nawait host.start(relayUrl);","typeGuard":null,"tryCatchPattern":"try {\n  await host.start(relayUrl);\n} catch (err) {\n  ui.showError(`Cannot start collaboration: ${(err as Error).message} — check relayUrl in config`);\n}","preventionTips":["Store the relay as a full absolute ws:// or wss:// URL in config.","Never use ws:// for non-localhost relays.","Validate relayUrl with new URL() at config load time, before starting the host."],"tags":["configuration","url-validation","collaboration"],"backgroundTag":"invalid-url-config","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}