My guess is that this aggregation is being done across a lot more rows than you were expecting, and the resultant array/string therefore eats lots of memory. You might try replacing that with COUNT(*), or even better SUM(LENGTH(MM.ID::CHARACTER VARYING)), just to get some definitive evidence about what the query is asking to compute.
The devs have moved on and want to stick with their new query, so I'll just chalk this up to a bad query and let it go. But I'm glad to have learned a few new tricks, thanks.
Meanwhile, it seems like ulimit -v would provide the safety valve you asked for originally.
Thank you Amador and Tom for the ulimit solution, that's exactly what I needed.