Skip to content

task core: record tick's poison marker; keep the bracket broken on cancellation-delivery traps - #164

Merged
lannbot merged 1 commit into
mainfrom
fix/poison-marker-and-cancel-bracket
Aug 20, 2026
Merged

task core: record tick's poison marker; keep the bracket broken on cancellation-delivery traps#164
lannbot merged 1 commit into
mainfrom
fix/poison-marker-and-cancel-bracket

Conversation

@lannbot

@lannbot lannbot commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

task core: record tick's poison marker; keep the bracket broken on cancellation-delivery traps

Two conformance fixes from the definitions.py parity review (five parallel independent review tracks against the pinned reference; both findings re-verified in source before dispatch).

1. Store.tick's poison path bypassed the poison marker

The catch block called the raw onInstancePoisoned hook instead of notifyInstancePoisoned, so an instance poisoned by a trap under tick — a background/sibling thread, outside any export's entered set (boundary.ts's poison only marks entered-set members) — was never recorded in poisonedInstances. Every marker consumer misfired for exactly that instance:

Reference anchor: definitions.py Store.tick (~:597) — the trap skipping leave_to is the poisoning this marker records. Fix is one line: route through the seam like every other bracket-break site (thread.ts, boundary.ts, fact_calls.ts, handles.ts).

2. Task.requestCancellation un-poisoned on a trapping delivery

Cancellation delivery was wrapped in try/finally { leaveTo(caller) }. The reference (Task.request_cancellation, definitions.py:519-532) has no handler around resume(Cancelled.TRUE): a trap skips leave_to — the Component Model's instance poisoning, the same discipline Store.tick preserves with its "deliberately NOT a finally" catch. The finally restored mayEnter on a half-unwound callee, leaving it enterable where the reference permanently locks it.

Reachable: subtask.cancel delivering TASK_CANCELLED to a cancellable block point whose resumed path traps (e.g. task.cancel with live borrows — the num_borrows trap in the same file). Now mirrors tick's catch exactly: capability signals (NeedsJspi/PendingCapability) release the bracket (the runtime is incomplete, not the component faulted); traps keep it broken, release the synthetic root (poisoning stays per-instance, plan v3 amendment 4 — a no-op for a guest caller, whose entering set is the leaf alone), and notify the poison seam. state stays cancel-delivered, matching the reference's pre-delivery assignment.

Tests

Three regression pins in task_test.ts, pre-fix failure modes confirmed against a stash:

  • tick's trap path records the marker; a late settled tail of the poisoned instance drains quietly (pre-fix: expected true, got false on the marker, and the tail deferred forever);
  • a trapping cancellation delivery leaves the callee locked (mayEnterFrom(caller) === false), poisoned, and in cancel-delivered (pre-fix: enterable);
  • a capability signal during delivery still releases the gate and does not poison (guards the new catch against over-poisoning).

Gates

just gates — full pass. Conformance 1416 commands: 1257 passed, 0 failed, no expectation changes; sched-seeds clean. No published-surface change; lockstep version untouched per AGENTS.md.

…ncellation-delivery traps

Two conformance fixes out of the definitions.py parity review.

1. Store.tick's poison path called the raw onInstancePoisoned hook
   instead of notifyInstancePoisoned, so an instance poisoned by a trap
   under tick (a background/sibling thread, outside any export's
   entered set — exec/boundary.ts's poison only marks entered-set
   members) was never recorded in the poisonedInstances marker. Every
   marker consumer then misfired for exactly that instance:
   Thread.resumeWith's quiet-retire of late settled tails missed
   (#156's backstop assert became reachable again pre-#161), #161's
   dispatchableTail deferred its tails forever (a silent leak,
   invisible to hasServiceableSettled — the observed pre-fix mode on
   current main), and withPoisonCause lost the #145 diagnostic suffix.
   One line: route through the seam like every other bracket-break
   site.

2. Task.requestCancellation wrapped the cancellation delivery in
   try/finally { leaveTo }, un-poisoning a half-unwound callee. The
   reference (definitions.py request_cancellation, lines 519-532) has
   no handler around resume(Cancelled.TRUE): a trap skips leave_to —
   instance poisoning — the discipline Store.tick preserves with its
   "deliberately NOT a finally" catch. Reachable via subtask.cancel
   delivering TASK_CANCELLED to a cancellable block point whose
   resumption traps (e.g. task.cancel with live borrows). Now mirrors
   tick's catch exactly: capability signals release the bracket, traps
   keep it broken, release the synthetic root (per-instance poisoning,
   plan v3 amendment 4), and notify the poison seam.

Regression tests pin: the marker is recorded on tick's trap path and a
late settled tail of the poisoned instance drains quietly (pre-fix:
deferred forever); a trapping cancellation delivery leaves the callee
locked, poisoned, and in cancel-delivered state (pre-fix: enterable);
a capability signal during delivery still releases the gate and does
not poison.

Gates: just gates (full pass; conformance 1257/0, no expectation
changes; sched-seeds clean). No published-surface change; lockstep
version untouched.
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