gastownhall/beads · error
ErrSelfDependency
ErrSelfDependency
Error message
%w: %s
What it means
A dependency-add batch item tried to make an issue depend on itself, which is forbidden. The error is wrapped in publicops.ErrSelfDependency and again in an ItemError carrying the batch index and source key so callers can attribute the failure to one item.
Source
Thrown at internal/storage/uow/batch_applier.go:385
// WHETHER THE EDGE WAS NEW IS READ BEFORE THE WRITE, because this backend's
// insert does not report it: BulkAddDepsResult echoes the request. A pre-read
// per edge is bounded by the request's hundred-item cap and is the only way
// ItemResult.Changed can mean the same thing on all three legs — an idempotent
// re-add of the same pair with the same type changed nothing.
func (r *uowApplyRun) applyDepAdd(ctx context.Context, index int, item *publicops.DepAddItem) error {
source, err := r.resolve(item.Source, index, "source")
if err != nil {
return err
}
target, err := r.resolve(item.Target, index, "target")
if err != nil {
return err
}
itemErr := func(err error) error {
return &publicops.ItemError{Index: index, Kind: publicops.ItemDepAdd, Key: item.Source.Key, IssueID: source, Err: err}
}
if source == target {
return itemErr(fmt.Errorf("%w: %s", publicops.ErrSelfDependency, source))
}
sourceWisp, sourceMine := r.planes[source]
targetWisp, targetMine := r.planes[target]
if sourceMine && targetMine && sourceWisp != targetWisp {
return itemErr(storageissueops.CrossPlaneBatchEdgeError(source, target))
}
existed, err := r.edgeExists(ctx, source, target, item.Type)
if err != nil {
return itemErr(err)
}
dep := &types.Dependency{
IssueID: source,
DependsOnID: target,
Type: item.Type,
Metadata: item.Metadata,
}
if _, err := r.uw.DependencyUseCase().AddDependencies(ctx, []*types.Dependency{dep}, r.plan.Actor, domain.BulkAddDepsOpts{
SkipPerEdgeCycleCheck: r.plan.SkipPerEdgeCycleCheck,View on GitHub (pinned to 71377f2769)
Solutions
- Fix the batch/CLI input so source and target differ
- Pre-filter items where Source.Key == DependsOn.Key before submitting the batch
- Assert source != target where the dependency list is generated programmatically
Example fix
// before
if source == target { return itemErr(...) } // rejected at apply time
// after (caller-side)
for _, it := range items {
if it.DepAdd.Source.Key == it.DepAdd.DependsOn.Key { continue } // skip self-dep
} Defensive patterns
Strategy: validation
Validate before calling
for i, it := range items {
if it.DepAdd != nil && it.DepAdd.Source.Key == it.DepAdd.DependsOn.Key {
return fmt.Errorf("item %d: self-dependency on %s", i, it.DepAdd.Source.Key)
}
} Try / catch
err := applyBatch(ctx, batch)
var itemErr *publicops.ItemError
if errors.As(err, &itemErr) && errors.Is(itemErr.Err, publicops.ErrSelfDependency) {
log.Printf("batch item %d (%s): self-dependency", itemErr.Index, itemErr.Key)
} Prevention
- Filter source == target items before submitting batches
- Assert non-self edges where dependency lists are generated
- Treat issue ids as immutable keys when templating scripts
- Add a unit test that no generated batch contains self-edges
When it happens
Trigger: applyDepAdd receives a DepAdd item where item.Source.Key resolves to the same issue id as the target (source == target). E.g. 'bd dep add bd-1 bd-1' or a programmatic batch with identical source and target keys.
Common situations: Template-generated batch scripts where a variable accidentally equals the source id; loops that include the seed issue in its own dependency list; copy-paste of the same issue id into both fields.
Related errors
- no store is open for this workspace
- no absolute native user directory is available
- ExternalDoltConfig: set either Socket OR (Host, Port), not b
- ExternalDoltConfig: must set Socket or (Host, Port)
- ExternalDoltConfig: Host requires Port
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/64f34170a999ccdb.
Report an issue: GitHub.