SIGN IN SIGN UP

fix(chat): stop losing a user message that arrived mid-turn (#4795)

Follow-up to
[#4644](https://github.com/triggerdotdev/trigger.dev/pull/4644), now
rebased onto main so the diff is just these three commits.

## Summary

Two ways a chat could lose a user message, both pre-existing and both
raised while reviewing #4644.

A message arriving while a turn was streaming was handed to that turn's
push handler and parked in an in-memory array. The router counts a
record handed to a handler as terminally decided, so it stopped holding
the resume floor behind it, and the turn boundary published a cursor
past a message that existed only in that process. A crash before the
next turn lost it, silently. Measured: with the message at sequence 1,
the boundary published `session-in-event-id: 1`, so a resume skipped it.

Separately, a message the agent declined to inject was discarded with
the turn. Never injected, never written to the wire buffer, never
answered. That was also the documented default, since a
`pendingMessages` config without `shouldInject` declines every batch.

## Design

Notification and consumption are now separate concerns on the router.

`observe` reports that a record arrived without taking it, so the record
stays queued and keeps holding the floor. It is rejected on an
`at-arrival` route: an observer there would either have to count as a
listener, which would stop an unconsumed stop being discarded and bring
back a wedged mailbox, or watch records it cannot affect. `take` removes
exactly one queued record.

The managed loop and the `chat.createSession()` iterator now only
subscribe when there is a steering config to feed, and injection is the
point of consumption. A declined batch never reaches the take, so its
records stay queued and become later turns. Both in-memory wire buffers
are gone, so a message waiting for its turn is durable rather than
living in whichever worker received it.

The floor doubles as the wake cursor: `awaitWake` registers with it and
the server completes the waitpoint immediately if anything sits after
that sequence. An over-advanced floor was therefore also a missed wake.
It is now recorded on the wait span so a run that never woke can be
diagnosed from its trace.

## Verification

Both fixes have a red and green pair, each checked against the
unmodified source rather than only observed to pass:

- the resume cursor test fails on the parent branch and passes here
- the declined-message test fails without the second commit and passes
with it

Also 8 new router tests for `observe` and `take`. Suites green at 385
for the SDK and 886 for core.

## Not addressed

A `pendingMessages` config with no `chat.toStreamTextOptions()` spread
still swallows messages, because nothing drains the queue at all. Same
shape, different trigger, tracked separately.
E
Eric Allam committed
d54bcaa29cceab293e4a20330274f12a1d2b5ad0
Parent: 1065251
Committed by GitHub <noreply@github.com> on 8/27/2026, 10:57:29 AM