Skip to content

Fix MetaAgent.remove() leaving stale references on constituent agents#3506

Merged
tpike3 merged 2 commits intomesa:mainfrom
codebyNJ:fix/meta-agent-remove-cleanup
Mar 25, 2026
Merged

Fix MetaAgent.remove() leaving stale references on constituent agents#3506
tpike3 merged 2 commits intomesa:mainfrom
codebyNJ:fix/meta-agent-remove-cleanup

Conversation

@codebyNJ
Copy link
Copy Markdown
Contributor

@codebyNJ codebyNJ commented Mar 10, 2026

Pre-PR Checklist

  • This PR is a bug fix, not a new feature or enhancement.

Summary

MetaAgent.remove() leaves stale meta_agents and meta_agent references on constituent agents after the meta-agent is removed from the model.

Bug / Issue

  • MetaAgent.__init__ sets agent.meta_agents.add(self) and agent.meta_agent = self on every constituent
  • Agent.remove() only calls model.deregister_agent(self) — no cleanup of constituent references
  • MetaAgent had no remove() override, so constituent agents keep dangling pointers to a dead meta-agent
  • Relates to lifecycle concerns in discussion Meta-Agents Refactor (GSOC related) #3403

Implementation

  • Override remove() in MetaAgent to call remove_constituting_agents() on all constituents before super().remove()
  • Reuses existing cleanup logic that already handles meta_agents set removal and meta_agent pointer updates
  • Also fixes incorrect return type hint on get_constituting_agent_instance: was -> set[type], actually returns -> Agent

Testing

Added two tests:

  • test_meta_agent_remove_cleans_up_references — all constituents have clean references after removal
  • test_meta_agent_remove_with_multiple_memberships — removing one meta-agent preserves the other's references correctly

All 22 meta-agent tests pass.

Additional Notes

None.


GSoC contributor checklist

Context & motivation

While working on meta-agent bugs from #3184, I traced what happens when a MetaAgent is removed from the model. I realized that Agent.remove() just deregisters from the model but never cleans up the references that MetaAgent.__init__ wrote onto constituent agents. This means after removal, constituents still think they belong to a dead group. This is a real problem for any model with dynamic group formation and dissolution, which is the core use case for meta-agents.

What I learned

The fix turned out to be simple — just override remove() and call the existing remove_constituting_agents() before super().remove(). The harder part was verifying the edge case where an agent belongs to two meta-agents and one is removed — the surviving references need to stay intact. I also noticed the return type hint on get_constituting_agent_instance was wrong (set[type] instead of Agent), so I fixed that too.

Learning repo

My learning repo: https://github.com/codebyNJ/GSoC-learning-space

Readiness checks

@codebyNJ codebyNJ marked this pull request as draft March 10, 2026 18:47
@codebyNJ codebyNJ marked this pull request as ready for review March 10, 2026 19:06
@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +0.4% [+0.1%, +0.7%] 🔵 -0.3% [-0.4%, -0.1%]
BoltzmannWealth large 🔵 -0.4% [-0.8%, -0.1%] 🔵 -0.3% [-1.3%, +0.9%]
Schelling small 🔵 +0.6% [+0.3%, +0.8%] 🔵 +0.7% [+0.5%, +0.8%]
Schelling large 🔵 -0.1% [-0.7%, +0.6%] 🔵 +0.0% [-1.0%, +1.0%]
WolfSheep small 🔵 -0.9% [-1.2%, -0.5%] 🔵 +0.1% [-0.0%, +0.3%]
WolfSheep large 🔵 -0.5% [-1.6%, +0.5%] 🔵 +1.6% [+0.6%, +2.6%]
SugarscapeG1mt small 🔵 -0.8% [-1.2%, -0.5%] 🔵 +0.3% [+0.1%, +0.6%]
SugarscapeG1mt large 🔵 +0.4% [-0.6%, +1.4%] 🔵 +0.0% [-0.2%, +0.2%]
BoidFlockers small 🔵 +0.1% [-0.3%, +0.6%] 🔵 -1.3% [-1.5%, -1.0%]
BoidFlockers large 🔵 +0.7% [+0.2%, +1.1%] 🔵 -0.2% [-0.4%, -0.0%]

@github-actions
Copy link
Copy Markdown

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +2.3% [+2.0%, +2.5%] 🔴 +3.1% [+3.0%, +3.3%]
BoltzmannWealth large 🔵 +2.3% [+1.7%, +3.0%] 🔴 +4.9% [+3.6%, +6.0%]
Schelling small 🔵 +1.4% [+1.3%, +1.6%] 🔴 +4.6% [+4.5%, +4.7%]
Schelling large 🔵 +0.7% [+0.3%, +1.1%] 🔵 +2.8% [+1.8%, +3.8%]
WolfSheep small 🔵 +1.5% [+1.3%, +1.6%] 🔵 +1.1% [+1.0%, +1.2%]
WolfSheep large 🔵 -0.7% [-1.2%, -0.4%] 🔵 -3.9% [-5.2%, -2.7%]
SugarscapeG1mt small 🔵 -1.4% [-1.6%, -1.1%] 🔵 -1.2% [-1.4%, -1.0%]
SugarscapeG1mt large 🔵 -0.7% [-1.2%, -0.2%] 🔵 +0.3% [+0.0%, +0.6%]
BoidFlockers small 🔵 -0.8% [-1.2%, -0.3%] 🔵 -1.0% [-1.2%, -0.8%]
BoidFlockers large 🔵 +0.1% [-0.1%, +0.4%] 🔵 -0.2% [-0.2%, -0.1%]

Copy link
Copy Markdown
Member

@tpike3 tpike3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @codebyNJ

@tpike3 tpike3 merged commit b813d90 into mesa:main Mar 25, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants