# ADR 0264: A wait on a long-lived promise is owned by the step

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0264-a-wait-on-a-long-lived-promise-is-owned-by-the-step.md

---

# ADR 0264 — A wait on a long-lived promise is owned by the step

**Invariants:** I10.

## Context

`Promise.race([step, longLived])` leaves a reaction on `longLived` until it settles, and the
promise the race returns keeps the step's result alive through that reaction. In a loop
that runs for the life of a stream, a queue or a handler, that is one promise, one
reaction and one retained result per iteration. A contract stream that sent a frame a
second grew by about 1.6 MB a minute, and a source that never had to wait was never
stopped at its `lifetimeMs`, because an already-settled step wins the race.
The same shape sat in the speech queue (one wait per sentence on a promise of the
queue), in the MCP handler (one per request on a promise of the handler) and in the
grammY update gate (one listener per poll on the polling signal).

## Decision

A step that must give way to an abort waits through `untilAborted(pending, signal)`
(`internal/until-aborted.ts`): the step owns its listener, removes it when `pending`
settles, and an abort is observed before the settled result. A promise or a signal that
lives as long as the loop is never an argument of `Promise.race` in that loop. A signal
that is followed rather than waited on is followed once, keyed on the signal.

A wait that is made once per call of a short-lived object stays as it is: the restart,
rollback and shutdown deadlines of the application kernel (`untilDeadline`), the
generation retirement of live state, and the per-call abort of admission and credential
refresh end with the object they wait on, so nothing outlives them.

## Consequences

A long stream, queue or handler holds a constant number of objects, whatever it has
carried. `tests/long-lived-waits.test.ts` opens each of them, runs a small and a large
number of steps and requires that what is retained does not follow the count.
