{"record":{"id":"5c877e43508226a3","repo":"gastownhall/beads","slug":"applygraph-node-q-storage-class-ephemeral-t-n","errorCode":null,"errorMessage":"applyGraph: node %q storage class (ephemeral=%t, no_history=%t) does not match plan routing (wisp=%t)","messagePattern":"applyGraph: node %q storage class \\(ephemeral=%t, no_history=%t\\) does not match plan routing \\(wisp=%t\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/issue.go","lineNumber":1122,"sourceCode":"\tkeyToID := make(map[string]string, len(plan.Nodes))\n\tpendingAssignees := make(map[int]string, len(plan.Nodes))\n\n\t// Pass 1 — create every node as a top-level issue. We deliberately do\n\t// not pass ParentID to u.create: graph nodes with parent_key/parent_id\n\t// receive top-level hash (or counter) IDs and have their parent linkage\n\t// added as a separate parent-child dep below. This matches embedded\n\t// executeGraphApply (cmd/bd/graph_apply.go) and lets children precede\n\t// parents in plan order — keyToID is only consulted after every node\n\t// has minted its ID.\n\tfor i, node := range plan.Nodes {\n\t\tif node.Issue == nil {\n\t\t\treturn GraphApplyResult{}, fmt.Errorf(\"applyGraph: node %d (key=%q) has nil Issue\", i, node.Key)\n\t\t}\n\t\t// The whole plan routes to one table, so every node's storage class\n\t\t// must match. The CLI pre-validates this; guard here too so other\n\t\t// callers cannot route wisp-flagged issues into the durable table.\n\t\tif nodeWisp := node.Issue.Ephemeral || node.Issue.NoHistory; nodeWisp != useWisp {\n\t\t\treturn GraphApplyResult{}, fmt.Errorf(\"applyGraph: node %q storage class (ephemeral=%t, no_history=%t) does not match plan routing (wisp=%t)\", node.Key, node.Issue.Ephemeral, node.Issue.NoHistory, useWisp)\n\t\t}\n\n\t\tif node.AssignAfterCreate {\n\t\t\tpendingAssignees[i] = node.Assignee\n\t\t\tnode.Issue.Assignee = \"\"\n\t\t} else if node.Assignee != \"\" {\n\t\t\tnode.Issue.Assignee = node.Assignee\n\t\t}\n\n\t\tparams := CreateIssueParams{\n\t\t\tIssue:  node.Issue,\n\t\t\tLabels: node.Labels,\n\t\t}\n\t\tr, err := u.create(ctx, params, actor, useWisp)\n\t\tif err != nil {\n\t\t\treturn GraphApplyResult{}, fmt.Errorf(\"applyGraph: node %q: %w\", node.Key, err)\n\t\t}\n\t\tkeyToID[node.Key] = r.Issue.ID","sourceCodeStart":1104,"sourceCodeEnd":1140,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/issue.go#L1104-L1140","documentation":"applyGraph routes the entire plan to either the durable issues table or the wisps (ephemeral) table based on the useWisp flag. Every node's storage class (Ephemeral || NoHistory) must agree with that routing; this guard prevents wisp-flagged issues being written to the durable table and vice versa. The CLI pre-validates this; the domain layer re-checks for other callers.","triggerScenarios":"Calling ApplyGraph with a plan mixing ephemeral/no_history issues and durable issues, or calling the wisp variant with durable nodes (or vice versa).","commonSituations":"Programmatically constructing plans that mix wisp and non-wisp issues; importing a graph from another workspace with different ephemeral flags; version drift where a flag was added to some nodes only.","solutions":["Split the plan into two homogeneous plans (one all-wisp, one all-durable) and apply each with the matching variant","Set Ephemeral/NoHistory consistently across all nodes to match the intended routing","Use the CLI path (bd graph apply) which pre-validates storage class before reaching the domain layer","Fix flags on imported issue data before building the plan"],"exampleFix":"// before: mixed storage classes in one plan\nnode1.Issue.Ephemeral = true; node2.Issue.Ephemeral = false\n// after: one storage class per plan, or split\nwispPlan.Nodes = filter(plan.Nodes, func(n) { return n.Issue.Ephemeral || n.Issue.NoHistory })","handlingStrategy":"validation","validationCode":"func homogeneous(plan GraphPlan) (allWisp bool, ok bool) {\n    if len(plan.Nodes) == 0 { return false, true }\n    want := plan.Nodes[0].Issue.Ephemeral || plan.Nodes[0].Issue.NoHistory\n    for _, n := range plan.Nodes {\n        if (n.Issue.Ephemeral || n.Issue.NoHistory) != want { return want, false }\n    }\n    return want, true\n}\n// split into wispPlan/durablePlan if !ok before calling ApplyGraph","typeGuard":null,"tryCatchPattern":"err := uc.ApplyGraph(ctx, plan, actor)\nif err != nil && strings.Contains(err.Error(), \"storage class\") {\n    wispPlan, durablePlan := splitByStorageClass(plan)\n    uc.ApplyGraph(ctx, wispPlan, actor)\n    uc.ApplyGraph(ctx, durablePlan, actor)\n}","preventionTips":["Never mix ephemeral/no_history issues with durable issues in one plan","Set Ephemeral/NoHistory flags deliberately at plan construction","Use the CLI path which pre-validates storage class","Normalize flags when importing graphs from other sources"],"tags":["graph-apply","validation","wisp","storage-class"],"backgroundTag":"storage-class-mismatch","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}