odysseus-dev/odysseus · error · Error
Import failed: server returned ${res.status}
Error message
Import failed: server returned ${res.status} What it means
Raised by Skills.install_bundle when the files dict passed in is empty (falsy). This is the earliest guard before pick_skill_md/from_markdown run — an empty bundle means nothing to install. Like error 986 it only fires on direct calls: fetch_skill_bundle either returns non-empty files or raises error 985 first.
Source
Thrown at static/js/admin.js:2831
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';
});
}
/* ── Danger Zone ── */
function initDangerZone() {
// Per-category Danger Zone wipes. Each button declares its target
// via data-wipe-kind; one delegated handler handles double-confirm,
// POSTs to /api/admin/wipe/{kind}, and writes the result.
const _LABELS = {
chats: 'chats', memory: 'memory entries', skills: 'skills',View on GitHub (pinned to f9235ebbf1)
Solutions
- Populate the bundle before installing: fetch via fetch_skill_bundle, or verify your local traversal actually collected files
- Check intermediate filters (extension allowlists, size checks) — they may be excluding everything
- Fail earlier with a clearer message at the collection site rather than passing an empty dict into install_bundle
- Log the dict size/keys just before calling install_bundle to confirm what the traversal produced
Example fix
# before
files = {p.read_text('utf-8') for p in dir.glob('*.x') if keep(p)} # keep() rejects all
skills.install_bundle(files) # 'empty bundle'
# after
files = {p.name: p.read_text('utf-8') for p in dir.glob('SKILL.md')}
if not files:
raise ValueError(f"no SKILL.md collected from {dir}")
skills.install_bundle(files) Defensive patterns
Strategy: validation
Validate before calling
def is_installable_bundle(files: dict[str, str]) -> bool:
return bool(files) and any(p.lower().endswith("skill.md") for p in files) Type guard
from typing import TypeGuard
def is_non_empty_bundle(files: dict[str, str]) -> TypeGuard[dict[str, str]]:
return len(files) > 0 Try / catch
try:
skills.install_bundle(files)
except SkillImportError as e:
if str(e) == "empty bundle":
raise ValueError("collected nothing to install — check source path and filters") from e
raise Prevention
- Validate the collection step produced files before calling install_bundle
- Warn loudly when traversal filters exclude everything instead of silently returning {}
- Log len(files) and its keys right before install in custom pipelines
When it happens
Trigger: Calling install_bundle({}) or install_bundle(files) where files was populated by a loop/traversal that matched nothing (empty directory walk, filtered-out extensions, failed downloads swallowed into an empty map).
Common situations: Custom import pipelines that glob a local directory which is empty or whose files were all filtered out; race where the source directory was deleted between listing and install; passing fetch output after a failed fetch that returned {} instead of raising.
Related errors
- d && d.detail ? d.detail : ('HTTP ' + res.status)
- result.detail || 'Failed to rename session'
- Invalid backup file: ' + e.message
- empty memory
- unsafe path: {rel!r}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/2f17415baf340cad.
Report an issue: GitHub.