backgroundCompletion (#310) resolves an idle-exited async lift on Task.onFinished — the moment the task's last thread unregisters. For a callback-ABI task that keeps spawned futures alive for the instance's life (an event loop, a driver, an accept loop — wit-bindgen spawn_local from within the export), that moment never comes. So a lift that goes idle before task.return and is later woken by another export call does task.return, has its results captured (resolvedSeen true, resolved set), and the host's Promise still never settles.
The normal (non-idle) path does not require threads to drain: driveDone = resolvedSeen && !midWasmCall() && !hopParked(). The idle path should answer on the same event — the task's resolve callback (the (result) => { resolved = result; resolvedSeen = true } seam the lift already owns), deferred to a point where no wasm call of this task is in flight — rather than on thread exit. Thread drain is relevant to nothing the host is waiting for.
Not hit by polyvisor today (its exports that spawn immortal futures return before idling); found while fixing #312, whose regression misrouted a done exit into this path and so made the hang visible. Repro shape: long-poll.wat's next plus a second thread that returns WAIT on a never-fired waitable set before task.return.
backgroundCompletion(#310) resolves an idle-exited async lift onTask.onFinished— the moment the task's last thread unregisters. For a callback-ABI task that keeps spawned futures alive for the instance's life (an event loop, a driver, an accept loop — wit-bindgenspawn_localfrom within the export), that moment never comes. So a lift that goes idle beforetask.returnand is later woken by another export call doestask.return, has its results captured (resolvedSeentrue,resolvedset), and the host's Promise still never settles.The normal (non-idle) path does not require threads to drain:
driveDone = resolvedSeen && !midWasmCall() && !hopParked(). The idle path should answer on the same event — the task's resolve callback (the(result) => { resolved = result; resolvedSeen = true }seam the lift already owns), deferred to a point where no wasm call of this task is in flight — rather than on thread exit. Thread drain is relevant to nothing the host is waiting for.Not hit by polyvisor today (its exports that spawn immortal futures return before idling); found while fixing #312, whose regression misrouted a
doneexit into this path and so made the hang visible. Repro shape:long-poll.wat'snextplus a second thread that returns WAIT on a never-fired waitable set beforetask.return.