Skip to content

lift: branch on the driver's exit verdict, not a re-derived done() (0.6.6 regression) - #312

Merged
lannbot merged 1 commit into
mainfrom
fix/lift-done-verdict-latched
Sep 7, 2026
Merged

lift: branch on the driver's exit verdict, not a re-derived done() (0.6.6 regression)#312
lannbot merged 1 commit into
mainfrom
fix/lift-done-verdict-latched

Conversation

@lannbot

@lannbot lannbot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

An async-typed lifted export's driver exits on one of two verdicts:
done() held (task.return seen, no wasm call or hop in flight) or
idle (#292 / #310). Since #310 the lift RE-EVALUATED driveDone() in
its .then continuation to decide between finishHostEntry and
backgroundCompletion. That predicate is time-varying: one microtask
after an EXIT-done, another driver of the same store (the settlement
pump servicing a settled host call) can resume a background activation
that transiently hop-parks, so midWasmCall()/hopParked() read true
and the lift took backgroundCompletion() — which resolves on
Task.onFinished, i.e. never for a callback-ABI task whose spawned
futures are immortal. Regression from 0.6.5, whose continuation was
pending.then(finishHostEntry) unconditionally.

Observed in polyvisor: lifecycle.boot (spawns the engine driver,
event pump and accept loop, then task.returns) never resolved on a
reload once a store sync task added enough host-call traffic for the
pump to win the race. Trace: EXIT-done ... done=true followed by
-> backgroundCompletion threads=1 resolvedSeen=true.

Fix: drive/driveLoop/driveAsync return their exit verdict
(DriveExit = "done" | "idle") and the lift branches on that. The
verdict is a fact about the loop that ran; re-deriving it later asks a
different question. driveStoreAsync discards the verdict (the pumps
have no completion to route).

Test: runtime/tests/lift_done_verdict_test.ts — a callback-ABI lift
with an immortal second thread task.returns and exits done; a foreign
thread hop-parks in the microtask window before the continuation.
Red on main ("expected resolved, got pending"), green with the fix.

Consumer evidence: polymorph-components/polyvisor, deno task e2e on 0.6.6 fails drive-connect/drive-round-trip (boot never resolves on reload); forcing trapOnIdle: true or latching the verdict makes all 16 scenarios pass. Traces: POLYENGINE_DRIVE_TRACE + a log at the lift's exit sites.

Not in this PR (to file separately): backgroundCompletion resolves on Task.onFinished (last thread unregisters) rather than on the task's resolve callback, so a task with long-lived spawned futures that idles before task.return never answers the host even after it returns.

Automerge armed.

An async-typed lifted export's driver exits on one of two verdicts:
`done()` held (task.return seen, no wasm call or hop in flight) or
idle (#292 / #310). Since #310 the lift RE-EVALUATED `driveDone()` in
its `.then` continuation to decide between `finishHostEntry` and
`backgroundCompletion`. That predicate is time-varying: one microtask
after an EXIT-done, another driver of the same store (the settlement
pump servicing a settled host call) can resume a background activation
that transiently hop-parks, so `midWasmCall()`/`hopParked()` read true
and the lift took `backgroundCompletion()` — which resolves on
`Task.onFinished`, i.e. never for a callback-ABI task whose spawned
futures are immortal. Regression from 0.6.5, whose continuation was
`pending.then(finishHostEntry)` unconditionally.

Observed in polyvisor: `lifecycle.boot` (spawns the engine driver,
event pump and accept loop, then task.returns) never resolved on a
reload once a store sync task added enough host-call traffic for the
pump to win the race. Trace: `EXIT-done ... done=true` followed by
`-> backgroundCompletion threads=1 resolvedSeen=true`.

Fix: `drive`/`driveLoop`/`driveAsync` return their exit verdict
(`DriveExit = "done" | "idle"`) and the lift branches on that. The
verdict is a fact about the loop that ran; re-deriving it later asks a
different question. `driveStoreAsync` discards the verdict (the pumps
have no completion to route).

Test: `runtime/tests/lift_done_verdict_test.ts` — a callback-ABI lift
with an immortal second thread task.returns and exits done; a foreign
thread hop-parks in the microtask window before the continuation.
Red on main ("expected resolved, got pending"), green with the fix.
@lannbot
lannbot enabled auto-merge September 7, 2026 15:52
@lannbot
lannbot merged commit 0b7cdc9 into main Sep 7, 2026
4 checks passed
@lannbot
lannbot deleted the fix/lift-done-verdict-latched branch September 7, 2026 15:57
lannbot pushed a commit that referenced this pull request Sep 8, 2026
…rain

`backgroundCompletion` (#310) resolved the host's Promise from
`Task.onFinished` — the moment the task's last thread unregistered. A
callback-ABI export that keeps spawned futures alive for the instance's
life (wit-bindgen `spawn_local`: an event loop, a driver, an accept
loop) never reaches that moment, so a lift that went idle before
`task.return` and was later woken by another driver had its results
captured and its Promise left pending forever. Silent hang; found while
tracing #312.

The reference's answer event is `on_resolve`, called from
`Task.return_`; `lift_and_run` keeps ticking afterwards for OTHER work,
not to produce the result. wasmtime's `call_concurrent` is the same
shape. So: the lift's own `onResolve` closure now fires a one-shot hook
that settles the Promise, and an already-resolved task settles at once
(generalizing the old `threads.length === 0` case). No deferral to
"no wasm call in flight": `driveDone`'s liveness clauses keep a DRIVER
driving, and on this path there is no lift driver left to stop.

`Task.onFinished` and its `unregisterThread` firing block are deleted;
nothing else used them.

Test: runtime/tests/lift_background_return_test.ts — an export goes
idle with an immortal thread and a parked returner, a later export call
drives the store, the returner calls `task.return`, the first Promise
resolves while the immortal thread is still parked. Red on main, green
with the fix.

Closes #313.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants