Summary
Found during the #158 mechanism-A fix (#207), split out per that PR's
"not in scope" note. Under jspi, a trapping cancellation delivery does not
take Task.requestCancellation's synchronous poisoning branch, because on
the SuspensionPoint arm the trap is delivered as a rejection of the
import's Promise rather than a synchronous throw.
Details
Task.requestCancellation (runtime/src/task/mod.ts:515-545) wraps the
delivery resume(CANCELLED_TRUE) in the bracket-break catch f7abf96
introduced: a trap escaping the resume skips leaveTo and poisons the
callee, matching definitions.py request_cancellation (delivery at line
531, no handler, so a Trap never reaches leave_to at 532).
That catch only ever fires on the generator-Thread arm, where
Thread.resume runs the body synchronously and a trap propagates as a
throw (pinned by task_test.ts "request_cancellation: a trap during
delivery poisons the callee").
On the jspi arm — the callee parked in a cancellable built-in whose
produce throws when handed cancelled — SuspensionPoint.resume never
rethrows: #resumeInner's catch routes the trap to #fail(e) (the
import-promise rejection the engine turns back into a wasm trap) and
returns normally. So requestCancellation sees no throw, leaveTo runs,
and the callee instance is left enterable and unpoisoned at that
moment; the callee's activation then unwinds asynchronously, its
awaitValue rejects, and poisoning finally lands via the settled-tail
failure path (Thread.resumeWith's catch) several microtasks later.
Pinned explicitly by the "guest-shaped" test in
runtime/tests/resume_claim_discipline_test.ts (state cancel-delivered,
trap delivered to the parked promise, isInstancePoisoned === false
immediately after delivery).
Why it may matter (and may not)
The end state converges with the reference: the callee ends up poisoned,
the trap reaches the caller. What diverges is the intermediate window —
in definitions.py the poisoning is atomic with the delivery; here there
are several microtasks in which:
- the callee instance answers
mayEnterFrom(null) === true and could
admit a host entry or a sibling delivery into a half-unwound instance;
Store.tick may schedule other threads of the callee instance ahead of
the unwind's settled tail.
Whether any observable behavior can actually diverge (a call admitted in
the window that the reference would refuse) needs either a demonstration
or an argument that the window is harmless — e.g. that the unwinding
activation's tail is always serviced before any admission point, or that
admission into the window is indistinguishable from a legal schedule.
This is the same displacement pattern the runtime uses deliberately
elsewhere (resumeWith's bracket, #66 retirement), so the answer may well
be "working as designed" — but it is currently undocumented and unproven
either way.
Suggested resolution
Either:
- demonstrate a divergent observable and close the window (e.g. mark the
instance poisoned/pending-poison at the delivery site when produce
throws, before #fail), or
- rule the displacement conforming (schedule nondeterminism), document
it at requestCancellation's catch and #resumeInner's trap arm, and
flip the pinning assertion's comment from "current behavior" to
"ruled".
Provenance
Found during the #158 investigation (2026-08-21/22); pinned in #207;
design discussion in #158's comments.
Summary
Found during the #158 mechanism-A fix (#207), split out per that PR's
"not in scope" note. Under jspi, a trapping cancellation delivery does not
take
Task.requestCancellation's synchronous poisoning branch, because onthe SuspensionPoint arm the trap is delivered as a rejection of the
import's Promise rather than a synchronous throw.
Details
Task.requestCancellation(runtime/src/task/mod.ts:515-545) wraps thedelivery
resume(CANCELLED_TRUE)in the bracket-break catch f7abf96introduced: a trap escaping the resume skips
leaveToand poisons thecallee, matching definitions.py
request_cancellation(delivery at line531, no handler, so a Trap never reaches
leave_toat 532).That catch only ever fires on the generator-
Threadarm, whereThread.resumeruns the body synchronously and a trap propagates as athrow (pinned by task_test.ts "request_cancellation: a trap during
delivery poisons the callee").
On the jspi arm — the callee parked in a cancellable built-in whose
producethrows when handedcancelled—SuspensionPoint.resumeneverrethrows:
#resumeInner's catch routes the trap to#fail(e)(theimport-promise rejection the engine turns back into a wasm trap) and
returns normally. So
requestCancellationsees no throw,leaveToruns,and the callee instance is left enterable and unpoisoned at that
moment; the callee's activation then unwinds asynchronously, its
awaitValuerejects, and poisoning finally lands via the settled-tailfailure path (
Thread.resumeWith's catch) several microtasks later.Pinned explicitly by the "guest-shaped" test in
runtime/tests/resume_claim_discipline_test.ts (state
cancel-delivered,trap delivered to the parked promise,
isInstancePoisoned === falseimmediately after delivery).
Why it may matter (and may not)
The end state converges with the reference: the callee ends up poisoned,
the trap reaches the caller. What diverges is the intermediate window —
in definitions.py the poisoning is atomic with the delivery; here there
are several microtasks in which:
mayEnterFrom(null) === trueand couldadmit a host entry or a sibling delivery into a half-unwound instance;
Store.tickmay schedule other threads of the callee instance ahead ofthe unwind's settled tail.
Whether any observable behavior can actually diverge (a call admitted in
the window that the reference would refuse) needs either a demonstration
or an argument that the window is harmless — e.g. that the unwinding
activation's tail is always serviced before any admission point, or that
admission into the window is indistinguishable from a legal schedule.
This is the same displacement pattern the runtime uses deliberately
elsewhere (resumeWith's bracket, #66 retirement), so the answer may well
be "working as designed" — but it is currently undocumented and unproven
either way.
Suggested resolution
Either:
instance poisoned/pending-poison at the delivery site when
producethrows, before
#fail), orit at
requestCancellation's catch and#resumeInner's trap arm, andflip the pinning assertion's comment from "current behavior" to
"ruled".
Provenance
Found during the #158 investigation (2026-08-21/22); pinned in #207;
design discussion in #158's comments.