janhq/jan · error · Error
Failed to normalize backend layout: ${String(e)}
Error message
Failed to normalize backend layout: ${String(e)} What it means
During the layout-normalization step — when the llama-server binary was found in a non-standard location and must be moved into build/bin/ — a file move operation fails. The staging directory and backend directory are cleaned up and the error is re-thrown. The normalization preserves relative symlinks (libggml.so chain) by using a single rename.
Source
Thrown at extensions/llamacpp-extension/src/index.ts:2925
}
if (foundDir !== expectedBinDir) {
const staging = `${backendDir}.staging`
try {
// Move the binary's dir into build/bin in one rename to keep relative
// symlinks intact (libggml.so → .so.0 → .so.0.10.0). A flat-root
// archive can't rename into its own subtree, so stage to a sibling.
if (foundDir === backendDir) {
await fs.mv(backendDir, staging)
await fs.mkdir(await joinPath([backendDir, 'build']))
await fs.mv(staging, expectedBinDir)
} else {
await fs.mkdir(await joinPath([backendDir, 'build']))
await fs.mv(foundDir, expectedBinDir)
}
} catch (e) {
if (await fs.existsSync(staging)) await fs.rm(staging)
if (await fs.existsSync(backendDir)) await fs.rm(backendDir)
throw new Error(`Failed to normalize backend layout: ${String(e)}`)
}
}
}
if (!(await fs.existsSync(expectedBinPath))) {
await fs.rm(backendDir)
throw new Error(
'Not a supported backend archive! Missing llama-server binary.'
)
}
try {
await this.refreshBackendOptions()
logger.info(
`Backend ${backendIdentifier}/${version} installed and UI refreshed`
)
} catch (e) {
logger.error('Backend installed but failed to refresh UI', e)View on GitHub (pinned to fad3f12a14)
Solutions
- Retry the installation — a transient lock or race may clear.
- Ensure the Jan data folder is on a single mount point with adequate free space.
- Manually remove any leftover *.staging directories from a previous failed attempt.
- Check for antivirus or backup software locking the backend directory during the move.
Defensive patterns
Strategy: try-catch
Validate before calling
import fs from 'node:fs'
function isStagingClear(backendDir: string): boolean {
return !fs.existsSync(`${backendDir}.staging`)
}
// Before install:
if (!isStagingClear(backendDir)) {
fs.rmSync(`${backendDir}.staging`, { recursive: true, force: true })
} Try / catch
try {
await extension.installBackend(path)
} catch (e) {
if (e instanceof Error && e.message.includes('normalize backend layout')) {
// Clean up leftover staging and retry
fs.rmSync(`${backendDir}.staging`, { recursive: true, force: true })
fs.rmSync(backendDir, { recursive: true, force: true })
await extension.installBackend(path)
} else throw e
} Prevention
- Ensure the Jan data folder resides on a single filesystem (not split across mounts).
- Clean up leftover .staging directories before retrying a failed install.
- Disable antivirus real-time scanning on the backend directory during installation.
When it happens
Trigger: fs.mv fails during the staging dance: cross-device move without copy support, permission denied, another process holds a lock on the files, disk full during the move, or a stale staging directory from a previous failed attempt.
Common situations: Backend directory and staging are on different mount points; antivirus scanning locks files during the move; leftover .staging directory from a prior crashed install causes a conflict; disk quota exceeded.
Related errors
- Failed to decompress archive: ${String(e)}
- No supported backend binaries found for this system. Backend
- Failed to fetch supported backends: ${error instanceof Error
- Invalid backend string: ${targetBackendString} supplied to u
- Invalid backend string format: "${targetBackendString}". Exp
AI-assisted analysis of janhq/jan@fad3f12a14 (2026-08-12).
Data as JSON: /api/errors/b75d5db5bea2d409.
Report an issue: GitHub.