{"record":{"id":"63c1ee96a38adfca","repo":"vercel/ai","slug":"invalid-host-tool-catalog-poll-response","errorCode":null,"errorMessage":"Invalid host tool catalog poll response.","messagePattern":"Invalid host tool catalog poll response\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/harness-acp/src/v1/bridge/host-tool-mcp.ts","lineNumber":80,"sourceCode":"  initialRevision: number;\n  updateCatalog: (options: {\n    revision: number;\n    tools: ReadonlyArray<HarnessV1BridgeToolWire>;\n  }) => Promise<void>;\n}): Promise<void> {\n  let revision = initialRevision;\n  for (;;) {\n    const value = await postRelay({\n      path: '/catalog/next',\n      body: { afterRevision: revision },\n    });\n    if (isRecord(value) && value.closed === true) return;\n    if (\n      !isRecord(value) ||\n      !Number.isSafeInteger(value.revision) ||\n      (value.revision as number) < revision\n    ) {\n      throw new Error('Invalid host tool catalog poll response.');\n    }\n    const nextRevision = value.revision as number;\n    if (nextRevision === revision) continue;\n    const nextTools = validateToolCatalog({ value: value.tools });\n    await updateCatalog({ revision: nextRevision, tools: nextTools });\n    revision = nextRevision;\n  }\n}\n\nasync function postRelay({\n  path,\n  body,\n}: {\n  path: string;\n  body: Readonly<Record<string, unknown>>;\n}): Promise<unknown> {\n  const response = await postHostToolRelay({\n    relayUrl,","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/harness-acp/src/v1/bridge/host-tool-mcp.ts#L62-L98","documentation":"Thrown by watchCatalog in host-tool-mcp.ts when a long-poll response from the host tool relay endpoint (/catalog/next) is malformed: the body is not an object, lacks a safe-integer 'revision', or reports a revision older than the one already held. The bridge validates every poll response to guarantee catalog synchronization only ever moves forward with well-formed data. A closed signal ({closed:true}) is handled separately and does not throw.","triggerScenarios":"Calling watchCatalog (indirectly, via the bridge startup at host-tool-mcp.ts:45) when the relay's /catalog/next response body is: (1) not a JSON object (array, string, null), (2) missing 'revision' or 'revision' is a float/NaN/non-number, or (3) 'revision' is lower than the current local revision (relay restarted or rewound its catalog).","commonSituations":"Running a mismatched or older relay server that returns a legacy response shape; a proxy/gateway stripping or rewriting the JSON body; a relay that lost state and restarted at revision 0/1 while the bridge is already past that revision; a load balancer serving an error page with HTTP 200.","solutions":["Verify the relay at AI_SDK_ACP_HOST_TOOL_RELAY_URL implements the /catalog/next long-poll contract returning {revision: <safe integer>, tools: [...]} or {closed: true}.","Check relay logs for state loss/restart; if the relay rewound its catalog revision, restart the bridge process so it resumes from initialRevision.","Inspect the raw response with curl -X POST <relayUrl>/catalog/next to confirm the body shape and rule out proxies rewriting it.","Ensure bridge and relay versions match (same AI SDK release line) so the wire format agrees."],"exampleFix":"// before: relay returns { rev: 3, tools: [...] } (legacy field name)\n// after: relay returns the expected shape\n{ \"revision\": 3, \"tools\": [ { \"name\": \"read_file\", \"inputSchema\": {} } ] }","handlingStrategy":"validation","validationCode":"function isValidCatalogPoll(value) {\n  return (\n    value != null &&\n    typeof value === 'object' &&\n    !Array.isArray(value) &&\n    Number.isSafeInteger(value.revision)\n  );\n}\n// wrap the poll: only proceed when isValidCatalogPoll(body) && body.revision >= currentRevision","typeGuard":"function isCatalogPollResponse(value: unknown): value is { revision: number; tools: unknown[] } {\n  return (\n    value != null && typeof value === 'object' && !Array.isArray(value) &&\n    Number.isSafeInteger((value as any).revision)\n  );\n}","tryCatchPattern":"try {\n  await watchCatalog({ initialRevision: 1, updateCatalog });\n} catch (error) {\n  process.stderr.write(`catalog sync failed: ${error instanceof Error ? error.message : String(error)}\\n`);\n  process.exitCode = 1; // restart the bridge to resync from initialRevision\n}","preventionTips":["Run relay and bridge from the same release so the /catalog/next wire format matches.","Add a contract test asserting the relay's /catalog/next response shape.","Avoid proxies that rewrite JSON bodies on internal endpoints.","If the relay restarts and revisions rewind, restart the bridge too."],"tags":["network","relay","protocol-mismatch","validation"],"backgroundTag":"invalid-api-response-shape","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}