You can inspect the 'dead' coroutine using the debug library, e.g. add "print(debug.traceback(thread))". On my system the output is:
stack traceback:
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
... (skipping 2499976 levels)
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:9: in upvalue 'recurse'
co.lua:13: in function <co.lua:13>
If you put "thread = nil" before the loop at the end the thread object and the stack will be collected.
In short, it's not a bug, it's a feature 😀
Ge'