{"record":{"id":"0635a5fe5b47962a","repo":"aaif-goose/goose","slug":"acp-url-is-not-available","errorCode":null,"errorMessage":"ACP URL is not available","messagePattern":"ACP URL is not available","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"ui/desktop/src/acp/acpConnection.ts","lineNumber":132,"sourceCode":"  if (!pendingConnection) {\n    const generation = connectionGeneration;\n    let connectionAttempt: Promise<AcpConnection>;\n    connectionAttempt = openConnection(generation).catch((error) => {\n      if (pendingConnection === connectionAttempt) {\n        pendingConnection = null;\n      }\n      throw error;\n    });\n    pendingConnection = connectionAttempt;\n  }\n\n  return pendingConnection;\n}\n\nasync function openConnection(generation: number): Promise<AcpConnection> {\n  const wsUrl = await window.electron.getAcpUrl();\n  if (!wsUrl) {\n    throw new Error('ACP URL is not available');\n  }\n\n  // Electron treats an explicitly passed undefined protocol as a subprotocol.\n  const stream = createWebSocketStream(wsUrl, { protocols: [] });\n  const client = connectGooseAcpClient(stream, createClientCallbacks());\n\n  try {\n    const initializeResponse = await withTimeout(\n      client.connection.agent.request(methods.agent.initialize, {\n        protocolVersion: PROTOCOL_VERSION,\n        _meta: {\n          'goose/useLoginShellPath': true,\n        },\n        clientCapabilities: {\n          elicitation: { form: {} },\n          _meta: {\n            goose: {\n              mcpHostCapabilities: DEFAULT_GOOSE_MCP_HOST_CAPABILITIES,","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/aaif-goose/goose/blob/3810898a7447ec3299be72e223d3570a7aabf0ab/ui/desktop/src/acp/acpConnection.ts#L114-L150","documentation":"Thrown by openConnection() in the desktop UI when window.electron.getAcpUrl() resolves to a falsy value. That IPC call reaches main.ts (gooseServeLeases.getAcpUrl(windowId) ?? null), so the error means the Electron main process has no ACP WebSocket URL registered for this window: the goose serve backend either never started, exited, or the external-backend mode was not configured. The connection to the goose agent cannot even be attempted.","triggerScenarios":"Calling getAcpClient()/openConnection() before the main process has finished spawning 'goose serve' and recording the lease; after the serve process crashed or exited (gooseServeLeases.getAcpUrl then throws or returns null for that windowId); running with external-backend mode enabled but the URL setting missing.","commonSituations":"Startup race where the renderer connects faster than the backend binds its port; goose binary exits immediately (bad config, missing API key file) so the lease is torn down; window recreated with a new id that has no lease; development against an external goose daemon without setting the backend URL.","solutions":["Check main-process logs for the 'goose serve' spawn: if it exits immediately, fix the root cause (invalid config, missing binary, port conflict).","If connecting to an external backend, set the external ACP base URL so getAcpUrl has a value instead of null.","Retry the connection after a short delay — the lease is often registered milliseconds after the first attempt during startup.","Verify the IPC handler 'get-acp-url' is registered before the renderer calls it and that the windowId matches an active lease."],"exampleFix":"// before\nconst wsUrl = await window.electron.getAcpUrl();\nif (!wsUrl) {\n  throw new Error('ACP URL is not available');\n}\n\n// after (brief bounded retry for the startup race)\nlet wsUrl: string | null = null;\nfor (let attempt = 0; attempt < 10 && !wsUrl; attempt++) {\n  wsUrl = await window.electron.getAcpUrl();\n  if (!wsUrl) await new Promise((r) => setTimeout(r, 200));\n}\nif (!wsUrl) {\n  throw new Error('ACP URL is not available (goose backend not started or exited)');\n}","handlingStrategy":"retry","validationCode":"// Wait until the main process has a backend URL for this window\nasync function waitForAcpUrl(timeoutMs = 5000): Promise<string> {\n  const deadline = Date.now() + timeoutMs;\n  for (;;) {\n    const url = await window.electron.getAcpUrl();\n    if (url) return url;\n    if (Date.now() > deadline) throw new Error('ACP backend URL not available before timeout');\n    await new Promise((r) => setTimeout(r, 200));\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const client = await getAcpClient();\n} catch (error) {\n  if (/ACP URL is not available/.test(String(error))) {\n    // Backend not up (yet) — surface a 'starting backend' state and retry on next app event\n    setBackendState('starting');\n    scheduleReconnect();\n    return;\n  }\n  throw error;\n}","preventionTips":["Do not call getAcpClient until the main process signals the serve lease is ready.","Watch main-process logs for early goose-serve exits so 'not available' is diagnosed as a crash, not a race.","In external-backend mode, validate and persist the URL setting before enabling the mode."],"tags":["electron","acp","ipc","startup","connection"],"backgroundTag":null,"analyzedSha":"3810898a7447ec3299be72e223d3570a7aabf0ab","analyzedAt":"2026-08-16T10:14:26.282Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}