{"record":{"id":"f07987d20df39a49","repo":"multica-ai/multica","slug":"model-discovery-timed-out","errorCode":null,"errorMessage":"model discovery timed out","messagePattern":"model discovery timed out","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/runtimes/models.ts","lineNumber":45,"sourceCode":"// the request times out. Returns both the models list and a\n// `supported` flag: `supported=false` means the provider ignores\n// per-agent model selection entirely (hermes today) — the UI uses\n// this to disable its dropdown instead of accepting a value that\n// wouldn't be honoured at runtime.\n//\n// `cached` reports that the server answered from its catalog cache rather than\n// a live daemon round trip (MUL-5444). It is not cosmetic: it feeds the\n// staleTime policy below so the client never extends the server's staleness\n// window past what the server itself promises.\nexport async function resolveRuntimeModels(\n  runtimeId: string,\n): Promise<RuntimeModelsResult> {\n  const initial = await api.initiateListModels(runtimeId);\n  const start = Date.now();\n  let current = initial;\n  while (current.status === \"pending\" || current.status === \"running\") {\n    if (Date.now() - start > POLL_TIMEOUT_MS) {\n      throw new Error(\"model discovery timed out\");\n    }\n    await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n    current = await api.getListModelsResult(runtimeId, initial.id);\n  }\n  // Only an explicit `completed` is a catalog. Anything else — failed, timeout,\n  // or a status this client does not know (newer server, or a response that fell\n  // back to the malformed-record shape) — is surfaced as an error so the picker\n  // shows \"discovery failed\" and keeps manual entry available. Treating an\n  // unrecognised status as success would render an empty dropdown that looks\n  // authoritative.\n  if (current.status !== \"completed\") {\n    throw new Error(\n      current.error || `model discovery failed (status: ${current.status})`,\n    );\n  }\n  return {\n    models: current.models ?? [],\n    supported: current.supported !== false,","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/packages/core/runtimes/models.ts#L27-L63","documentation":"Thrown by resolveRuntimeModels in packages/core/runtimes/models.ts when the async model-discovery job on the runtime did not reach a terminal status within POLL_TIMEOUT_MS (30s, checked every 500ms). The function initiates a list-models job via api.initiateListModels and polls api.getListModelsResult until the status leaves 'pending'/'running'. It is a client-side deadline, not a server error: the daemon may still be working (large model catalog, slow runtime startup) when the client gives up.","triggerScenarios":"Calling resolveRuntimeModels(runtimeId) against a runtime whose daemon takes >30s to enumerate models (cold Ollama start, large model directory on slow disk, network-attached model store); or a runtime that reports status 'pending'/'running' indefinitely because the daemon job hung; or getListModelsResult repeatedly returning a still-running result.","commonSituations":"First model discovery after installing a runtime with many models; runtime hosted on a remote/slow machine; daemon stuck mid-discovery after a partial crash; CI environments with slow disk I/O.","solutions":["Retry the call — a second invocation often succeeds because the daemon-side discovery continues and later answers from its catalog cache (the 'cached' flag path).","Check runtime/daemon health and logs: a job stuck in 'running' usually means the daemon worker hung; restart the runtime daemon and retry.","If discovery legitimately takes >30s in your environment, raise POLL_TIMEOUT_MS in packages/core/runtimes/models.ts (it is a module constant next to the poll loop).","If it always times out, verify the runtime is reachable at all — use the runtime status API before invoking the picker."],"exampleFix":"// before\nconst POLL_TIMEOUT_MS = 30_000;\n\n// after (only if slow runtimes are expected)\nconst POLL_TIMEOUT_MS = 90_000;","handlingStrategy":"retry","validationCode":"// Pre-check runtime health before discovery\nconst status = await api.getRuntime(runtimeId);\nif (status.state !== \"online\") {\n  throw new Error(`runtime ${runtimeId} is ${status.state}; not ready for discovery`);\n}","typeGuard":"function isRuntimeOnline(s: { state: string } | undefined): boolean {\n  return s?.state === \"online\";\n}","tryCatchPattern":"try {\n  const result = await resolveRuntimeModels(runtimeId);\n} catch (err) {\n  if (err instanceof Error && err.message === \"model discovery timed out\") {\n    // daemon may finish server-side; one retry often hits the catalog cache\n    return resolveRuntimeModels(runtimeId);\n  }\n  throw err;\n}","preventionTips":["Keep runtimes warm (start the daemon before opening the model picker) so discovery is not cold.","If your fleet has slow runtimes, raise POLL_TIMEOUT_MS to match measured discovery time.","Surface the timed-out state in the UI with a retry affordance and manual model entry, matching the intended picker behavior."],"tags":["runtime","polling","timeout","models","typescript"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}