Thanks for all the suggestions about this. I'm going to keep plugging away and report back when I find the fix, though I'll probably ultimately take th8a's suggestion of using tables for the envelope segments so I can eliminate the hacky way I'm using threshold~.
I've been building in lots of print and env~ objects to try to figure out where the exact problem is. I believe I've successfully determined that it's NOT the voice stealing or the threshold~ object. All the midi info seems to be getting routed to the right voices, and the decay segment seems to be playing correctly even in notes that get stuck on, which it's the purpose of the threshold~ object to trigger.
For some reason the midi off control seems to be sent to the envelope, but it isn't registering it. I can fake another off event with messages and the envelope releases normally at that point. I've been playing around with a slightly more complex synth patch that uses the same envelope abstraction, which is actually a little more helpful because it has multiple oscillators for each voice, and they do NOT all seem to get stuck on at once. In fact, the base oscillator seems to be the only one that gets stuck on, whereas the 2 suboscillators do not, which suggest to me that I might have screwed up the construction of how the oscillator patch interacts with the envelope. But I'm still only closer to figuring it out; I haven't had much time to mess around with it lately.