multica-ai/multica · error
resolve skill bundle: expected 1 bundle, got %d
Error message
resolve skill bundle: expected 1 bundle, got %d
What it means
Daemon Client.ResolveSkillBundle posts exactly one SkillRefData and asserts the server's 'bundles' array has exactly one element. Any other count — zero or more than one — is a protocol violation and fails with this error. It typically indicates a server/daemon version mismatch or a server bug in the resolve endpoint, not a client input problem.
Source
Thrown at server/internal/daemon/client.go:316
// ResolveSkillBundle downloads a single skill bundle. It uses bundleClient (no
// fixed timeout) so the deadline is governed entirely by ctx, which the daemon
// scales to the bundle's size, and retries transient transport blips within
// whatever budget ctx leaves. Resolving one skill per request — rather than the
// agent's whole bundle in one atomic body read — lets each download fit its own
// deadline and be cached independently, so a slow link makes incremental
// progress instead of failing the entire set on every dispatch. (GitHub #4505)
func (c *Client) ResolveSkillBundle(ctx context.Context, runtimeID, taskID string, ref SkillRefData) (SkillData, error) {
var resp struct {
Bundles []SkillData `json:"bundles"`
}
path := fmt.Sprintf("/api/daemon/runtimes/%s/tasks/%s/skill-bundles/resolve", runtimeID, taskID)
if err := c.postJSONViaWithRetry(ctx, c.bundleClient, path, map[string]any{
"skills": []SkillRefData{ref},
}, &resp, skillBundleResolveRetrySchedule); err != nil {
return SkillData{}, err
}
if len(resp.Bundles) != 1 {
return SkillData{}, fmt.Errorf("resolve skill bundle: expected 1 bundle, got %d", len(resp.Bundles))
}
return resp.Bundles[0], nil
}
func (c *Client) ExtendTaskPrepareLease(ctx context.Context, runtimeID, taskID string) error {
return c.postJSON(ctx, fmt.Sprintf("/api/daemon/runtimes/%s/tasks/%s/prepare-lease", runtimeID, taskID), map[string]any{}, nil)
}
func (c *Client) StartTask(ctx context.Context, taskID string) error {
return c.postJSON(ctx, fmt.Sprintf("/api/daemon/tasks/%s/start", taskID), map[string]any{}, nil)
}
// MarkTaskWaitingLocalDirectory parks a freshly-dispatched task in the
// waiting_local_directory state on the server. The daemon calls this after
// it has claimed a task whose project carries a local_directory resource
// but the path mutex is held by another in-flight task. reason is a short
// human-readable hint (e.g. "<path>") surfaced by the UI alongside the
// status. Idempotent on the daemon's side — calling twice with the sameView on GitHub (pinned to 2c0912b6ec)
Solutions
- Check daemon and server versions — upgrade the daemon so both sides agree on the resolve contract
- Inspect the raw response of POST /api/daemon/runtimes/{runtimeID}/tasks/{taskID}/skill-bundles/resolve to see the actual bundle count
- If the ref legitimately resolves to zero, fix the ref (typo'd name/scope) before blaming the protocol
- Report a server bug if one input ref deterministically yields >1 bundle
Example fix
// before
if len(resp.Bundles) != 1 {
return SkillData{}, fmt.Errorf("resolve skill bundle: expected 1 bundle, got %d", len(resp.Bundles))
}
return resp.Bundles[0], nil
// after: tolerate zero with a clearer message; still reject ambiguity
switch len(resp.Bundles) {
case 1:
return resp.Bundles[0], nil
case 0:
return SkillData{}, fmt.Errorf("resolve skill bundle: ref %q resolved to no bundles", ref.Name)
default:
return SkillData{}, fmt.Errorf("resolve skill bundle: ref %q resolved to %d bundles, expected 1", ref.Name, len(resp.Bundles))
} Defensive patterns
Strategy: validation
Validate before calling
// Before resolving, confirm daemon and server speak the same protocol version.
if serverVersion := client.ServerVersion(ctx); semverLT(serverVersion, minResolveContractVersion) {
return fmt.Errorf("server %s predates single-bundle resolve contract %s", serverVersion, minResolveContractVersion)
} Try / catch
Match on the 'expected 1 bundle' substring (or introduce a typed sentinel) and handle as a contract mismatch: skip retry, report version info, and fail the task dispatch with a clear message.
Prevention
- Upgrade daemon and server together
- In tests, always return a one-element bundles array from mocked resolve endpoints
- Add an integration test asserting the 1:1 request/response invariant
When it happens
Trigger: Calling ResolveSkillBundle where the server returns zero bundles (skill ref resolved to nothing) or multiple bundles (dedup failure / new multi-resolution semantics the old daemon doesn't understand). Retry-wrapped transport errors are a different failure; this fires only after a successful HTTP+JSON round trip.
Common situations: Daemon older than the server after a server upgrade, skill ref formats the server now expands into several bundles, or a stubbed/mocked server in tests returning an empty bundles array.
Related errors
- creation_studio.create_failed
- daemon exited during startup.
- repo is not configured for this workspace
- local_directory: local_path is empty
- ErrRepoBusy
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/21c04ad5707b2cf9.
Report an issue: GitHub.