odysseus-dev/odysseus · error · Error

Invalid backup file: ' + e.message

Error message

Invalid backup file: ' + e.message

What it means

Raised by pick_skill_md when no key in the files dict ends (case-insensitively) in 'skill.md'. In the normal flow fetch_skill_bundle already guarantees a SKILL.md exists (error 985), so this fires only when pick_skill_md or install_bundle is called directly with a caller-constructed file map that lacks one.

Source

Thrown at static/js/admin.js:2822

    } catch (e) { msg.textContent = 'Export failed: ' + e.message; msg.className = 'admin-error'; }
    btn.disabled = false; btn.textContent = 'Export Data';
  });

  const fileInput = el('adm-importFile');
  el('adm-importDataBtn').addEventListener('click', () => { fileInput.value = ''; fileInput.click(); });
  fileInput.addEventListener('change', async () => {
    const file = fileInput.files[0];
    if (!file) return;
    const msg = el('adm-backupMsg');
    const btn = el('adm-importDataBtn');
    btn.disabled = true; btn.textContent = 'Importing...'; msg.textContent = '';
    try {
      const text = (await file.text()).replace(/^\uFEFF/, '').trim();
      let data;
      try {
        data = JSON.parse(text);
      } catch (e) {
        throw new Error('Invalid backup file: ' + e.message);
      }
      const res = await fetch('/api/import', {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });
      const result = await res.json().catch(() => null);
      if (!result) {
        throw new Error(`Import failed: server returned ${res.status}`);
      }
      if (res.ok && result.ok) {
        msg.textContent = result.message || 'Import successful.'; msg.className = 'admin-success';
      } else {
        msg.textContent = result.message || result.detail || 'Import failed'; msg.className = 'admin-error';
      }
    } catch (e) { msg.textContent = 'Import failed: ' + e.message; msg.className = 'admin-error'; }
    btn.disabled = false; btn.textContent = 'Import Data';
  });

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Ensure the files dict passed to install_bundle/pick_skill_md contains at least one key whose path ends in 'skill.md' (any directory prefix is fine, e.g. 'my-skill/SKILL.md')
  2. Prefer using fetch_skill_bundle's output unmodified — it validates this invariant before returning
  3. If building bundles locally, copy your skill manifest to '<name>/SKILL.md' in the dict before calling install
  4. Check for key-mangling middleware (path normalization, lowercasing that renames the file) between fetch and install

Example fix

# before
files = {"guide.md": md_text, "prompt.txt": prompt}
skills.install_bundle(files)  # 'bundle has no SKILL.md'

# after
files = {"my-skill/SKILL.md": md_text, "my-skill/prompt.txt": prompt}
skills.install_bundle(files)
Defensive patterns

Strategy: type-guard

Validate before calling

def bundle_has_skill_md(files: dict[str, str]) -> bool:
    return any(p.lower().endswith("skill.md") for p in files)

Type guard

from typing import TypeGuard

def is_valid_bundle(files: dict[str, str]) -> TypeGuard[dict[str, str]]:
    """Narrows to a bundle install_bundle can process."""
    return bool(files) and any(p.lower().endswith("skill.md") for p in files)

Try / catch

from services.memory.skill_importer import SkillImportError

if not is_valid_bundle(files):
    raise ValueError("bundle must contain a path ending in skill.md")
try:
    skills.install_bundle(files)
except SkillImportError as e:
    if "no SKILL.md" in str(e):
        fix_bundle_naming(files)  # e.g. rename primary doc to '<name>/SKILL.md'
    else:
        raise

Prevention

When it happens

Trigger: Calling skills install_bundle(files) / pick_skill_md(files) with a hand-built dict like {'README.md': '...', 'prompt.txt': '...'}; or a caller that filtered/renamed fetch_skill_bundle's output and dropped the skill.md entry before installing.

Common situations: Programmatic integrations that assemble bundles from local directories or templates and forget the naming contract (path must end in 'skill.md'); post-processing steps that rewrite keys (e.g. stripping directories) and break the suffix.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/9fa9b63dc6e54f30. Report an issue: GitHub.