task: wait on the first of two tasks (WEB F5) #104

Closed
opened 2026-10-05 21:07:20 +00:00 by orion · 7 comments
Collaborator

Goal

Give std/task a way to wait on the first of two tasks, so the session placement's loop (WEB.md section 6.4) can wait on "a client message or a server event, whichever comes first" without a second thread or polling.

Design record

WEB.md section 8, F5: Task.first(a, b) answering which of the two finished, on the scheduler that already races a task against a timer for Task.within (completedWithin, CONCURRENCY.md sections 14-16). The record fixes the need and the mechanism only. It does not fix the signature, the result type, the fate of the loser or the tie rule; see the owner decision comment.

Acceptance criteria (once the API is decided)

  • Task.first (name and signature per the owner decision) in std/task/src/lib.trb, with the doc comment and a conformance test for each outcome below.
  • The first task to finish decides the answer; the answer names which one it was and carries its value.
  • The loser's fate is as decided (the issue's first draft said: cancelled, like within cancels its task) and tested: a loser that was cancelled stops at its next suspension point.
  • A task cancelled before either finishes: passes the cancellation on as await() does (or as decided), tested.
  • Both tied or both already finished: the documented rule, deterministic, tested.
  • Both back ends: torb test (VM) and torb test --native run the same conformance tests (tier B; touches runtime/).
  • CONCURRENCY.md (the Task surface) and WEB.md F5 are updated to the decided form; docs change regenerates skills/.

Seed

Touches the seed: a new native in runtime/ and the C back end's native table (compiler/src/backend/c/natives.trb), declared like completedWithin. Two commits per CLAUDE.md "Seed and breaking changes": (1) teach the native, both back ends and the std declaration, then the seed refresh and the seed workflow by the owner; (2) the first user in std/ or the docs, if any. No syntax change, no string-looked-up std name.

Size

One PR, two Conventional Commits, for example feat(runtime): wait on the first of two tasks and feat(task): add Task.first. Gates: sh tools/gates.sh ab. Not in this issue: the session placement itself (WEB.md section 8 item 8), Task.firstOf(List) / n-ary select.

Dependencies

None.

## Goal Give `std/task` a way to wait on the first of two tasks, so the session placement's loop (WEB.md section 6.4) can wait on "a client message or a server event, whichever comes first" without a second thread or polling. ## Design record [WEB.md section 8, F5](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/WEB.md#L927): `Task.first(a, b)` answering which of the two finished, on the scheduler that already races a task against a timer for `Task.within` (`completedWithin`, CONCURRENCY.md sections 14-16). The record fixes the need and the mechanism only. It does not fix the signature, the result type, the fate of the loser or the tie rule; see the owner decision comment. ## Acceptance criteria (once the API is decided) - [ ] `Task.first` (name and signature per the owner decision) in `std/task/src/lib.trb`, with the doc comment and a conformance test for each outcome below. - [ ] The first task to finish decides the answer; the answer names which one it was and carries its value. - [ ] The loser's fate is as decided (the issue's first draft said: cancelled, like `within` cancels its task) and tested: a loser that was cancelled stops at its next suspension point. - [ ] A task cancelled before either finishes: passes the cancellation on as `await()` does (or as decided), tested. - [ ] Both tied or both already finished: the documented rule, deterministic, tested. - [ ] Both back ends: `torb test` (VM) and `torb test --native` run the same conformance tests (tier B; touches `runtime/`). - [ ] CONCURRENCY.md (the `Task` surface) and WEB.md F5 are updated to the decided form; docs change regenerates `skills/`. ## Seed Touches the seed: a new native in `runtime/` and the C back end's native table (`compiler/src/backend/c/natives.trb`), declared like `completedWithin`. Two commits per `CLAUDE.md` "Seed and breaking changes": (1) teach the native, both back ends and the std declaration, then the seed refresh and the `seed` workflow by the owner; (2) the first user in `std/` or the docs, if any. No syntax change, no string-looked-up std name. ## Size One PR, two Conventional Commits, for example `feat(runtime): wait on the first of two tasks` and `feat(task): add Task.first`. Gates: `sh tools/gates.sh ab`. Not in this issue: the session placement itself (WEB.md section 8 item 8), `Task.firstOf(List)` / n-ary select. ## Dependencies None.
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript until 2026-10-06T07:28:50Z

[loop:torbscript] claim refine by session:loop-torbscript until 2026-10-06T07:28:50Z
Author
Collaborator

[loop:torbscript] owner decision: Task.first API, not settled by a design record

WEB.md F5 only says "Task.first(a, b) answering which of the two finished". Choices a design record has to settle before this is implementable (library design, new public std surface):

  1. Result type. (a) Task.first(a, b): Task<Either<A, B>>-style, if an Either/sum type exists in std, else (b) a small First<A, B> enum { Left(A) | Right(B) }, or (c) same value type only: static fn first(a: Task<V>, b: Task<V>): Task<(Int, V)>.
  2. The loser. (a) cancelled, as within cancels its task (the issue's first draft assumed this); (b) left running, the caller cancels or awaits it. The session loop of WEB.md 6.4 keeps waiting on the same event task across turns, so cancelling the loser would kill a task it needs to keep.
  3. Cancellation and tie. A cancelled input: pass on like await() or answer it like result(). Both finished: left wins (deterministic) or the one the scheduler saw first.

Recommendation: result type (b), a two-case enum in std/task; loser left running (2b, which is what the F5 consumer needs, and mirrors Task.all not owning inputs beyond failure); a cancelled first finisher passes the cancellation on like await(); on a tie the left task wins. Once decided, the criteria in the issue body are made concrete and the issue gets ready. The change adds a native, so it lands as two commits with a seed refresh (not breaking).

[loop:torbscript] owner decision: `Task.first` API, not settled by a design record WEB.md F5 only says "`Task.first(a, b)` answering which of the two finished". Choices a design record has to settle before this is implementable (library design, new public std surface): 1. **Result type.** (a) `Task.first(a, b): Task<Either<A, B>>`-style, if an `Either`/sum type exists in std, else (b) a small `First<A, B>` enum `{ Left(A) | Right(B) }`, or (c) same value type only: `static fn first(a: Task<V>, b: Task<V>): Task<(Int, V)>`. 2. **The loser.** (a) cancelled, as `within` cancels its task (the issue's first draft assumed this); (b) left running, the caller cancels or awaits it. The session loop of WEB.md 6.4 keeps waiting on the same event task across turns, so cancelling the loser would kill a task it needs to keep. 3. **Cancellation and tie.** A cancelled input: pass on like `await()` or answer it like `result()`. Both finished: left wins (deterministic) or the one the scheduler saw first. Recommendation: result type (b), a two-case enum in `std/task`; loser left running (2b, which is what the F5 consumer needs, and mirrors `Task.all` not owning inputs beyond failure); a cancelled first finisher passes the cancellation on like `await()`; on a tie the left task wins. Once decided, the criteria in the issue body are made concrete and the issue gets `ready`. The change adds a native, so it lands as two commits with a seed refresh (not breaking).
Author
Collaborator

[loop:torbscript] refine: rewrote the body to goal, criteria, design point, seed impact (new native, two commits + seed refresh) and size (one PR, two commits); API shape (result type, loser's fate, cancellation, tie) is not in a design record, so no ready label. next: wait for the owner decision above.

[loop:torbscript] refine: rewrote the body to goal, criteria, design point, seed impact (new native, two commits + seed refresh) and size (one PR, two commits); API shape (result type, loser's fate, cancellation, tie) is not in a design record, so no `ready` label. next: wait for the owner decision above.
Author
Collaborator

[loop:torbscript] owner decision recorded (torben, 2026-10-06, relayed from the TorbScript WhatsApp group):

  • Result type: same value type only. Task.first(a: Task<V>, b: Task<V>): Task<V>. No Either and no new two-case enum in std/task. A caller that races tasks of different types wraps them in its own ADT first (e.g. enum Event { Message(M) | Timeout }), which also says which one finished. The (Int, V) index variant is not taken either: the value is the answer.
  • The loser keeps running (option 2b, as recommended); the caller cancels or awaits it. The WEB.md 6.4 session loop needs this.
  • Cancellation: a cancelled first finisher passes the cancellation on like await().
  • Tie: when both have finished, the left task wins (deterministic).

Make the criteria in the body concrete accordingly; WEB.md F5 gets the signature in the same PR. Seed impact unchanged (new native, two commits + seed refresh). Labelled ready.

[loop:torbscript] owner decision recorded (torben, 2026-10-06, relayed from the TorbScript WhatsApp group): - **Result type: same value type only.** `Task.first(a: Task<V>, b: Task<V>): Task<V>`. No `Either` and no new two-case enum in `std/task`. A caller that races tasks of different types wraps them in its own ADT first (e.g. `enum Event { Message(M) | Timeout }`), which also says which one finished. The `(Int, V)` index variant is not taken either: the value is the answer. - **The loser keeps running** (option 2b, as recommended); the caller cancels or awaits it. The WEB.md 6.4 session loop needs this. - **Cancellation:** a cancelled first finisher passes the cancellation on like `await()`. - **Tie:** when both have finished, the left task wins (deterministic). Make the criteria in the body concrete accordingly; WEB.md F5 gets the signature in the same PR. Seed impact unchanged (new native, two commits + seed refresh). Labelled `ready`.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261007 until 2026-10-07T12:41:30Z

[loop:torbscript] claim implement by session:loop-torbscript-20261007 until 2026-10-07T12:41:30Z
Author
Collaborator

[loop:torbscript] implement: PR #154 open (two commits: feat(runtime): wait on the first of two tasks, feat(task): add Task.first), Closes #104, Argus requested, implements the recorded owner decision. Forge CI: tier A and bootstrap green, tier B still running at collect time. Per CONTRIBUTING's two-commit rule the seed of commit 1 has to be published before commit 2 lands (seed refresh, owner side, as already noted in the decision); the loop does not touch the seed. The worker session was lost in a gateway restart before its own write-back; this comment is the collect step and releases the claim. Worker usage unknown (session gone). next: review (waiting on CI and merge gate argus, seed refresh at landing)

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/154 open (two commits: `feat(runtime): wait on the first of two tasks`, `feat(task): add Task.first`), `Closes #104`, Argus requested, implements the recorded owner decision. Forge CI: tier A and bootstrap green, tier B still running at collect time. Per CONTRIBUTING's two-commit rule the seed of commit 1 has to be published before commit 2 lands (seed refresh, owner side, as already noted in the decision); the loop does not touch the seed. The worker session was lost in a gateway restart before its own write-back; this comment is the collect step and releases the claim. Worker usage unknown (session gone). next: review (waiting on CI and merge gate argus, seed refresh at landing)
argus closed this issue 2026-10-07 11:56:38 +00:00
Author
Collaborator

[orion] PR #154 merged by argus (rebase, both commits on main: 70633ecee2 runtime native, 2bef8947b3 Task.first). Post-merge step from the PR (seed refresh): dispatched the seed workflow on main at 2026-10-07 13:58 Europe/Berlin, run https://git.torb.dev/torbscript/language/actions/runs/118 (tier A + B gates, then publish). Done when seeds.txt on the seeds release lists a seed at or after 2bef8947b3. Not verified yet: run was still waiting at dispatch time.

[orion] PR #154 merged by argus (rebase, both commits on main: 70633ecee2 runtime native, 2bef8947b3 Task.first). Post-merge step from the PR (seed refresh): dispatched the `seed` workflow on main at 2026-10-07 13:58 Europe/Berlin, run https://git.torb.dev/torbscript/language/actions/runs/118 (tier A + B gates, then publish). Done when `seeds.txt` on the `seeds` release lists a seed at or after 2bef8947b3. Not verified yet: run was still waiting at dispatch time.
Sign in to join this conversation.
No labels
loop
ready
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#104
No description provided.