{"record":{"id":"3fd26f99de1bc6f8","repo":"janhq/jan","slug":"conversational-extension-not-available-yet","errorCode":null,"errorMessage":"Conversational extension not available yet","messagePattern":"Conversational extension not available yet","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"web-app/src/services/threads/default.ts","lineNumber":33,"sourceCode":"  assistantModel: { id: string; engine?: string } | undefined,\n  fallback?: Thread['model']\n): Thread['model'] | undefined {\n  if (assistantModel) {\n    return { id: assistantModel.id, provider: assistantModel.engine ?? 'llamacpp' }\n  }\n  return fallback\n}\n\nexport class DefaultThreadsService implements ThreadsService {\n  async fetchThreads(): Promise<Thread[]> {\n    const ext = ExtensionManager.getInstance().get<ConversationalExtension>(\n      ExtensionTypeEnum.Conversational\n    )\n    // The extension may not be registered yet during a startup race (e.g. a\n    // reload while the llamacpp router is busy). Throw so the caller can retry\n    // instead of treating \"not ready\" as \"no threads\" and wiping the list.\n    if (!ext) {\n      throw new Error('Conversational extension not available yet')\n    }\n\n    // Let listThreads failures propagate: a rejected invoke means \"backend not\n    // ready / errored\", not \"no threads\" — the caller retries instead of\n    // wiping the list with [].\n    const threads = await ext.listThreads()\n    if (!Array.isArray(threads)) return []\n\n    // new String(\"id\") !== \"id\"\n    threads.forEach((e) => {\n      e.id = e.id?.toString()\n      e.assistants?.forEach((a) => {\n        a.id = a.id?.toString()\n        if (a.model) a.model.id = a.model.id?.toString()\n      })\n    })\n\n    // Filter out temporary threads from the list","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/janhq/jan/blob/fad3f12a147d138388a66f0d92a02b2675f65294/web-app/src/services/threads/default.ts#L15-L51","documentation":"Thrown by DefaultThreadsService.fetchThreads when ExtensionManager.get<ConversationalExtension>(ExtensionTypeEnum.Conversational) returns undefined. By design it signals a transient startup race (e.g., a reload while the llamacpp router is still initializing) so the caller retries instead of treating 'not ready' as 'no threads' and wiping the cached list.","triggerScenarios":"The Conversational extension is not registered yet at the moment of the call - get() returns undefined. Happens during cold start, during a reload while another router is busy, or when the extension failed/was disabled.","commonSituations":"App cold start before extensions finish registering; reload during model loading; the conversational extension crashed on init; a build that excluded the conversational extension.","solutions":["Retry fetchThreads after a short backoff - the extension usually registers within a few hundred ms of startup.","Check the extension manager logs to confirm the Conversational extension loaded successfully.","If it persists, verify the conversational extension is packaged and enabled for this build."],"exampleFix":"// before: a single call on a cold start wipes the thread list\nconst threads = await threadsService.fetchThreads()\n// after: retry specifically on the 'not available yet' signal\nasync function loadThreads(maxAttempts = 5) {\n  for (let i = 0; i < maxAttempts; i++) {\n    try { return await threadsService.fetchThreads() }\n    catch (e) {\n      if (e instanceof Error && e.message.includes('not available yet') && i < maxAttempts - 1) {\n        await new Promise(r => setTimeout(r, 300 * (i + 1))); continue\n      }\n      throw e\n    }\n  }\n}","handlingStrategy":"retry","validationCode":"function isConversationalReady(): boolean {\n  return !!ExtensionManager.getInstance().get(ExtensionTypeEnum.Conversational)\n}","typeGuard":"function isExtensionNotReadyError(e: unknown): boolean {\n  return e instanceof Error && e.message.includes('not available yet')\n}","tryCatchPattern":"for (let i = 0; i < 5; i++) {\n  try { setThreads(await threadsService.fetchThreads()); break }\n  catch (e) {\n    if (isExtensionNotReadyError(e) && i < 4) {\n      await new Promise(r => setTimeout(r, 300 * (i + 1))); continue\n    }\n    throw e\n  }\n}","preventionTips":["Treat this specific message as retryable; never clear cached thread lists on it.","Gate initial thread load behind an 'extensions ready' signal if one exists."],"tags":["startup-race","extension","threads","transient","retry"],"backgroundTag":null,"analyzedSha":"fad3f12a147d138388a66f0d92a02b2675f65294","analyzedAt":"2026-08-12T20:33:47.516Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}