Repository navigation
feat(tasks): add ctx.subscribe for race-safe event waits - #74
Merged
Merged
Conversation
psteinroe
force-pushed
the
feat/subscribe-wait
branch
3 times, most recently
from
October 2, 2026 23:11
dd31024 to
b00579b
Compare
psteinroe
force-pushed
the
feat/subscribe-wait
branch
from
October 2, 2026 23:17
b00579b to
7b4f2cc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add
ctx.subscribe()so a task can register an event subscription, run the side effect that causes the event, and then wait for it. Todayctx.waitForEvent()registers and suspends in one call, so a reply to something the task did first (post an approval card, then wait for the click) can be emitted before the wait exists and be lost.waitForEventkeeps its API.Semantics
subscribe()resolves, and the execution keeps its claim. It is memoized by the step key, so retries and resumes reuse it.waitForEventuses). The execution is not woken or unlocked.wait()returns the stored event at once. Otherwise it suspends exactly likewaitForEventwith the same key. The timeout starts at the firstwait(), not atsubscribe().subscribe()never match. Each subscription has exactly one outcome: whichever of event or timeout locks the execution first is recorded under the step key and is final.Implementation
_private_register_event_waitgainsp_suspendand returns the settled step result. It locks the execution, returns an existing result, creates or arms the subscription, and releases the execution only when suspending. It also setswaiting_step_keyas a "suspended at this wait" marker.waiting_step_key(child-invocation keys are kept). This stops a delayed event from waking a retry that is still in backoff.waitForEvent: an event dispatched while a timed-out waiter was already claimed used to be dropped, and the timeout won. Registration now returns that stored event instead.Verification
New integration tests in
wait-for-event.test.tscover: an event emitted inside the step between subscribe and wait (buffered while claimed), an event emitted before subscribe, an event after the wait suspended, a timeout measured fromwait(), 10 executions racing an event against the timeout (one outcome each, consistent with the stored step), retries after subscribe that keep one subscription and buffer an event without cutting the backoff short, an event dispatched to a claimed timed-out waiter, and a delayed event after a failed retry. Type tests coverctx.subscribe/EventSubscription.just lint,just format,bun run typecheckand the fullbun test(343 pass) are green. Docs:api/task-context.md(ctx.subscribe()) and a "Waiting for Events in a Task" section incrafting-tasks/triggers.md.Follow-ups
caseexpression. Expect a small conflict with the other claim-query branches in flight.