Split out of #507 (minor-findings item 11) on demand; reproduced during the 2026-07-10 audit fix campaign (verified with a faulthandler watchdog dump).
Symptom
Reuse one sim_time_handler (same SimTimeRuntime) across two scheduled() runs, where run 1 ends while a WaitUntil is still parked. Run 1 leaves _driver_running=True and a stale TimeQueue entry. Run 2's first Delay/WaitUntil then never spawns a clock driver (the runtime believes one is running) and the run hangs forever in _drain_one_external (scheduler.py:351 blocking external_queue.get()).
Why this is likely in practice
Handler caching via Hy defhandler + lazy-var (the common composition pattern downstream) makes accidental cross-run reuse of one runtime instance easy.
Related latent hazard (same family)
Cross-run promise-ID collision: both runs' fresh_id sequences start at 0, and promises[pid] completion is unguarded, so a stale TimeQueue entry from run 1 can complete the WRONG promise in run 2.
Fix directions (either)
- Make
sim_time_handler() construct a fresh SimTimeRuntime per scheduled() run (bind runtime state to the run, not the handler closure), or
- Fail fast: detect a second run reusing a runtime whose driver state/TimeQueue is non-empty and raise with a clear message.
Found by the systematic concurrency audit (#507); reproduced on the fix-time-contract branch (post-#499 fix), so it is orthogonal to the shipped PRs (#508-#511).
Split out of #507 (minor-findings item 11) on demand; reproduced during the 2026-07-10 audit fix campaign (verified with a faulthandler watchdog dump).
Symptom
Reuse one
sim_time_handler(sameSimTimeRuntime) across twoscheduled()runs, where run 1 ends while aWaitUntilis still parked. Run 1 leaves_driver_running=Trueand a staleTimeQueueentry. Run 2's firstDelay/WaitUntilthen never spawns a clock driver (the runtime believes one is running) and the run hangs forever in_drain_one_external(scheduler.py:351blockingexternal_queue.get()).Why this is likely in practice
Handler caching via Hy
defhandler+lazy-var(the common composition pattern downstream) makes accidental cross-run reuse of one runtime instance easy.Related latent hazard (same family)
Cross-run promise-ID collision: both runs'
fresh_idsequences start at 0, andpromises[pid]completion is unguarded, so a stale TimeQueue entry from run 1 can complete the WRONG promise in run 2.Fix directions (either)
sim_time_handler()construct a freshSimTimeRuntimeperscheduled()run (bind runtime state to the run, not the handler closure), orFound by the systematic concurrency audit (#507); reproduced on the fix-time-contract branch (post-#499 fix), so it is orthogonal to the shipped PRs (#508-#511).