gastownhall/beads · error

edge %s->%s: %w

Error message

edge %s->%s: %w

What it means

This wraps an error returned by types.NewGraphEdgeDependency while constructing the Dependency record for a plan edge. Construction validates that edge endpoints resolve to real issue IDs (via keyToID) and that gate/spawner/thread metadata is consistent; a failure here means the edge references an unknown node key or has invalid attributes, so the apply transaction stops.

Source

Thrown at cmd/bd/graph_apply.go:1052

			for i, edge := range plan.Edges {
				fromID := resolveEdgeRef(edge.FromKey, edge.FromID, keyToID)
				toID := resolveEdgeRef(edge.ToKey, edge.ToID, keyToID)
				depType := graphApplyDependencyType(edge.Type)
				if (depType == types.DepParentChild) != parentPhase {
					continue
				}
				if parentDepPairs[graphApplyDepPairKey(fromID, toID)] {
					if depType == types.DepParentChild {
						continue
					}
					return fmt.Errorf("edge %d %s->%s duplicates a parent-child relationship with dependency type %q", i, fromID, toID, depType)
				}
				if parentDepPairs[graphApplyDepPairKey(toID, fromID)] && graphApplyCycleRelevantDependencyType(depType) {
					return fmt.Errorf("edge %d %s->%s creates a blocking reverse of a parent-child relationship", i, fromID, toID)
				}
				dep, err := types.NewGraphEdgeDependency(fromID, toID, depType, edge.Gate, edge.SpawnerKey, edge.SpawnerID, edge.ThreadID, keyToID)
				if err != nil {
					return fmt.Errorf("edge %s->%s: %w", fromID, toID, err)
				}
				if err := tx.AddDependencyWithOptions(ctx, dep, actor, storage.DependencyAddOptions{}); err != nil {
					return fmt.Errorf("adding edge %s->%s: %w", fromID, toID, err)
				}
				if graphApplySchedulingDependencyType(depType) {
					newSchedulingEdges = append(newSchedulingEdges, [2]string{fromID, toID})
				}
			}

			// Add per-node inline dependencies in stable order for this phase.
			for i, node := range plan.Nodes {
				for _, dep := range node.Deps {
					depType := types.DependencyType(dep.Type)
					if depType == "" {
						depType = types.DepBlocks
					}
					if (depType == types.DepParentChild) != parentPhase {
						continue

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check that both fromKey/toKey exist in the plan's nodes or as existing issue keys in the database
  2. Fix typos in edge endpoint keys in the graph JSON
  3. Remove or correct gate/spawner/thread fields that reference unknown nodes
  4. Run bd doctor / db integrity checks if keys appear valid but don't resolve

Example fix

// before
{"from":"tsk-1","to":"task-1","type":"blocks"} // typo: tsk-1
// after
{"from":"task-1","to":"task-2","type":"blocks"}
Defensive patterns

Strategy: validation

Validate before calling

for _, e := range plan.Edges {
  if keys[e.FromKey] == nil && !existsInDB(e.FromKey) { return fmt.Errorf("unknown edge endpoint %s", e.FromKey) }
  if keys[e.ToKey] == nil && !existsInDB(e.ToKey) { return fmt.Errorf("unknown edge endpoint %s", e.ToKey) }
}

Type guard

func endpointsResolve(e Edge, keyToID map[string]string) bool {
  return keyToID[e.FromKey] != "" && keyToID[e.ToKey] != ""
}

Try / catch

if err := bd.GraphApply(ctx, plan); err != nil && strings.Contains(err.Error(), "edge ") {
  return fmt.Errorf("plan has unresolved edge endpoints: %w", err)
}

Prevention

When it happens

Trigger: plan.Edges contains fromKey/toKey values not present in keyToID (nodes not created in this plan nor existing in the DB), or invalid gate/spawner references passed to NewGraphEdgeDependency.

Common situations: Typos in edge endpoint keys in graph JSON, referencing an external issue key that was never imported, or spawner/gate metadata pointing at nodes not in the plan.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/3b4a910f9ad6a03a. Report an issue: GitHub.