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

  1. Retry the installation — a transient lock or race may clear.
  2. Ensure the Jan data folder is on a single mount point with adequate free space.
  3. Manually remove any leftover *.staging directories from a previous failed attempt.
  4. 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

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


AI-assisted analysis of janhq/jan@fad3f12a14 (2026-08-12). Data as JSON: /api/errors/b75d5db5bea2d409. Report an issue: GitHub.