janhq/jan · warning
Conversational extension not available yet
Error message
Conversational extension not available yet
What it means
DefaultThreadsService.fetchThreads throws this when the ConversationalExtension is not yet registered in the ExtensionManager. It is deliberately thrown (instead of returning []) so callers can retry during a startup race — e.g. a reload while the llamacpp router is busy — rather than wiping the thread list believing there are no threads.
Solutions
- Retry fetchThreads after a short delay until the extension registers (the error is designed to be retried).
- Gate the initial fetchThreads call on an 'extensions ready' event from the ExtensionManager.
- Check that the conversational/llamacpp extension actually initialized (look at backend/router logs).
- If persistent, restart the app so extension registration completes.
Example fix
// before: fetch immediately on mount
useEffect(() => { fetchThreads() }, [])
// after: wait for extension readiness, then fetch with retry
await extensionsReady; try { await fetchThreads() } catch { setTimeout(fetchThreads, 1000) } Defensive patterns
Strategy: retry
Validate before calling
if (!ExtensionManager.getInstance().get(ExtensionTypeEnum.Conversational)) {
await waitForExtensionReady(ExtensionTypeEnum.Conversational, { timeoutMs: 10000 })
} Type guard
function hasConversationalExt(): boolean {
return !!ExtensionManager.getInstance().get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
} Try / catch
try {
threads = await fetchThreads()
} catch (e) {
if (e instanceof Error && e.message === 'Conversational extension not available yet') {
retryWithBackoff(fetchThreads) // do NOT treat as empty thread list
}
} Prevention
- Await the extension-manager 'ready' signal before the first threads fetch.
- Treat this error as retryable, never as an empty result.
- On reload, re-initialize extensions before remounting thread-list UI.
When it happens
Trigger: ExtensionManager.getInstance().get<ConversationalExtension>(ExtensionTypeEnum.Conversational) returns undefined at call time; typically when fetchThreads runs before extensions finish registering after app start or reload.
Common situations: App reload triggered while the llamacpp backend/router is still initializing; hot-reload in dev re-mounting the thread list before the extension manager is ready; slow machine where extension registration lags the UI.
Understand the failure class
Background: "not installed", "pip install", "required for": how missing-dependency errors surface across open-source libraries — this error's family across 34 libraries.
Related errors
- Could not unload the model. Stop its active requests and…
- Extension does not support backend installation
- Extension does not support backend updates
- Extension does not support CUDA runtime installation
- llama.cpp router is not running. Please restart the app.
AI-assisted analysis of janhq/jan@7205d770c1 (2026-09-17).
Data as JSON: /api/errors/3fd26f99de1bc6f8.
Report an issue: GitHub.
Appendix: source
Thrown at web-app/src/services/threads/default.ts:33
assistantModel: { id: string; engine?: string } | undefined,
fallback?: Thread['model']
): Thread['model'] | undefined {
if (assistantModel) {
return { id: assistantModel.id, provider: assistantModel.engine ?? 'llamacpp' }
}
return fallback
}
export class DefaultThreadsService implements ThreadsService {
async fetchThreads(): Promise<Thread[]> {
const ext = ExtensionManager.getInstance().get<ConversationalExtension>(
ExtensionTypeEnum.Conversational
)
// The extension may not be registered yet during a startup race (e.g. a
// reload while the llamacpp router is busy). Throw so the caller can retry
// instead of treating "not ready" as "no threads" and wiping the list.
if (!ext) {
throw new Error('Conversational extension not available yet')
}
// Let listThreads failures propagate: a rejected invoke means "backend not
// ready / errored", not "no threads" — the caller retries instead of
// wiping the list with [].
const threads = await ext.listThreads()
if (!Array.isArray(threads)) return []
// new String("id") !== "id"
threads.forEach((e) => {
e.id = e.id?.toString()
e.assistants?.forEach((a) => {
a.id = a.id?.toString()
if (a.model) a.model.id = a.model.id?.toString()
})
})
// Filter out temporary threads from the listView on GitHub (pinned to 7205d770c1)