{"record":{"id":"21c04ad5707b2cf9","repo":"multica-ai/multica","slug":"resolve-skill-bundle-expected-1-bundle-got-d","errorCode":null,"errorMessage":"resolve skill bundle: expected 1 bundle, got %d","messagePattern":"resolve skill bundle: expected 1 bundle, got (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/internal/daemon/client.go","lineNumber":316,"sourceCode":"// ResolveSkillBundle downloads a single skill bundle. It uses bundleClient (no\n// fixed timeout) so the deadline is governed entirely by ctx, which the daemon\n// scales to the bundle's size, and retries transient transport blips within\n// whatever budget ctx leaves. Resolving one skill per request — rather than the\n// agent's whole bundle in one atomic body read — lets each download fit its own\n// deadline and be cached independently, so a slow link makes incremental\n// progress instead of failing the entire set on every dispatch. (GitHub #4505)\nfunc (c *Client) ResolveSkillBundle(ctx context.Context, runtimeID, taskID string, ref SkillRefData) (SkillData, error) {\n\tvar resp struct {\n\t\tBundles []SkillData `json:\"bundles\"`\n\t}\n\tpath := fmt.Sprintf(\"/api/daemon/runtimes/%s/tasks/%s/skill-bundles/resolve\", runtimeID, taskID)\n\tif err := c.postJSONViaWithRetry(ctx, c.bundleClient, path, map[string]any{\n\t\t\"skills\": []SkillRefData{ref},\n\t}, &resp, skillBundleResolveRetrySchedule); err != nil {\n\t\treturn SkillData{}, err\n\t}\n\tif len(resp.Bundles) != 1 {\n\t\treturn SkillData{}, fmt.Errorf(\"resolve skill bundle: expected 1 bundle, got %d\", len(resp.Bundles))\n\t}\n\treturn resp.Bundles[0], nil\n}\n\nfunc (c *Client) ExtendTaskPrepareLease(ctx context.Context, runtimeID, taskID string) error {\n\treturn c.postJSON(ctx, fmt.Sprintf(\"/api/daemon/runtimes/%s/tasks/%s/prepare-lease\", runtimeID, taskID), map[string]any{}, nil)\n}\n\nfunc (c *Client) StartTask(ctx context.Context, taskID string) error {\n\treturn c.postJSON(ctx, fmt.Sprintf(\"/api/daemon/tasks/%s/start\", taskID), map[string]any{}, nil)\n}\n\n// MarkTaskWaitingLocalDirectory parks a freshly-dispatched task in the\n// waiting_local_directory state on the server. The daemon calls this after\n// it has claimed a task whose project carries a local_directory resource\n// but the path mutex is held by another in-flight task. reason is a short\n// human-readable hint (e.g. \"<path>\") surfaced by the UI alongside the\n// status. Idempotent on the daemon's side — calling twice with the same","sourceCodeStart":298,"sourceCodeEnd":334,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/daemon/client.go#L298-L334","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nif len(resp.Bundles) != 1 {\n    return SkillData{}, fmt.Errorf(\"resolve skill bundle: expected 1 bundle, got %d\", len(resp.Bundles))\n}\nreturn resp.Bundles[0], nil\n\n// after: tolerate zero with a clearer message; still reject ambiguity\nswitch len(resp.Bundles) {\ncase 1:\n    return resp.Bundles[0], nil\ncase 0:\n    return SkillData{}, fmt.Errorf(\"resolve skill bundle: ref %q resolved to no bundles\", ref.Name)\ndefault:\n    return SkillData{}, fmt.Errorf(\"resolve skill bundle: ref %q resolved to %d bundles, expected 1\", ref.Name, len(resp.Bundles))\n}","handlingStrategy":"validation","validationCode":"// Before resolving, confirm daemon and server speak the same protocol version.\nif serverVersion := client.ServerVersion(ctx); semverLT(serverVersion, minResolveContractVersion) {\n    return fmt.Errorf(\"server %s predates single-bundle resolve contract %s\", serverVersion, minResolveContractVersion)\n}","typeGuard":null,"tryCatchPattern":"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.","preventionTips":["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"],"tags":["daemon","skill-bundle","protocol-mismatch","api-contract"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}