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 same

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Check daemon and server versions — upgrade the daemon so both sides agree on the resolve contract
  2. Inspect the raw response of POST /api/daemon/runtimes/{runtimeID}/tasks/{taskID}/skill-bundles/resolve to see the actual bundle count
  3. If the ref legitimately resolves to zero, fix the ref (typo'd name/scope) before blaming the protocol
  4. 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

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


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/21c04ad5707b2cf9. Report an issue: GitHub.