gastownhall/beads · error
cannot combine a metadata replacement with incremental metad
Error message
cannot combine a metadata replacement with incremental metadata edits
What it means
The update payload supplied both a direct replacement of the metadata field and incremental metadata operations (merge/set/unset). The library cannot apply both semantics in one update, so it rejects the mutation before touching the database.
Source
Thrown at internal/storage/issueops/update.go:880
return true
default:
return false
}
}
// resolveMetadataMergeOps folds OpMergeMetadata/OpSetMetadata/OpUnsetMetadata
// into a concrete "metadata" value on resolved, using oldIssue.Metadata (read in
// the same mutation transaction) as the base. It is a no-op when no metadata
// operation keys are present.
func resolveMetadataMergeOps(oldIssue *types.Issue, updates, resolved map[string]interface{}) error {
_, hasMerge := updates[OpMergeMetadata]
_, hasSet := updates[OpSetMetadata]
_, hasUnset := updates[OpUnsetMetadata]
if !hasMerge && !hasSet && !hasUnset {
return nil
}
if _, direct := resolved["metadata"]; direct {
return fmt.Errorf("cannot combine a metadata replacement with incremental metadata edits")
}
current := oldIssue.Metadata
if hasMerge {
normalized, err := storage.NormalizeMetadataValue(updates[OpMergeMetadata])
if err != nil {
return fmt.Errorf("invalid %s: %w", OpMergeMetadata, err)
}
merged, err := storage.MergeMetadataJSON(current, json.RawMessage(normalized))
if err != nil {
return fmt.Errorf("metadata merge failed: %w", err)
}
current = merged
}
if hasSet || hasUnset {
unset, err := mergeOpStrings(OpUnsetMetadata, updates[OpUnsetMetadata], hasUnset)
if err != nil {
return errView on GitHub (pinned to 71377f2769)
Solutions
- Remove the direct metadata replacement and keep only merge/set/unset ops
- Or drop the incremental ops and supply the full metadata object as a replacement
- If combining is intended, perform two sequential update calls
Example fix
// before
updates := map[string]any{
issueops.OpSetMetadata: map[string]any{"a": 1},
issueops.OpMergeMetadata: map[string]any{"b": 2},
}
// after
updates := map[string]any{
issueops.OpMergeMetadata: map[string]any{"b": 2}, // incremental only
} Defensive patterns
Strategy: validation
Validate before calling
func checkNoConflict(updates map[string]any, resolved map[string]any) error {
_, direct := resolved["metadata"]
_, hasOp := updates[issueops.OpSetMetadata]
hasOp = hasOp || updates[issueops.OpMergeMetadata] != nil || updates[issueops.OpUnsetMetadata] != nil
if direct && hasOp {
return errors.New("remove either the metadata replacement or the merge/set/unset ops")
}
return nil
} Try / catch
if err := issueops.ResolveMergeOps(issue, updates, resolved); err != nil {
if strings.Contains(err.Error(), "cannot combine a metadata replacement") {
// strip incremental ops and retry with replacement only
}
} Prevention
- Pick one metadata mutation style (replacement OR ops) per update call
- Centralize metadata update construction in one helper
- Audit middleware that injects metadata ops into caller-built updates
When it happens
Trigger: Calling ResolveMergeOps (via an issue update) with updates containing OpSetMetadata, OpMergeMetadata, or OpUnsetMetadata while the resolved map already contains a direct "metadata" value.
Common situations: Building an update struct where both Metadata and MetadataOps/Merge fields are populated by different code paths (e.g. a CLI flag plus a default merge), or double-applying metadata in a middleware layer.
Related errors
- ExternalDoltConfig: must set Socket or (Host, Port)
- proxy.ForceStopUnverified: at most one options value is allo
- metadata.repo: %w
- metadata.repo must not be null
- invalid repo metadata: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/906cddc1d1cd050a.
Report an issue: GitHub.