bug: chat transport `watch: true` duplicates the previous assistant message when a turn starts out-of-band, and `useChat` status never returns to ready
Maintainer antworten meist innerhalb von 1 Tag
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Anfängerfreundlichkeit
- 48/100
- Issue-Typ
- Bug
- Klarheit
- Größtenteils klar
- Aktivitätsstatus
- Ruhig
- Tech-Stack
- typescript
Rechercherichtung
Start in packages/trigger-sdk/src/v3/chat.ts, especially the “Re-arm per-turn events” branch, and trace how watch mode handles turn completion with useChat. Compare that behavior with the AI SDK’s single-message-per-request stream processing. Done means out-of-band turns render without duplicating or corrupting the previous assistant message, useChat returns to ready, and an idle session does not reconnect indefinitely.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Provide environment information
System:
OS: macOS 26.6
CPU: (14) arm64 Apple M4 Pro
Memory: 294.73 MB / 48.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 24.14.0
Yarn: 1.22.22
npm: 11.9.0
pnpm: 10.25.0
bun: 1.3.5
trigger.dev CLI 4.5.4, @trigger.dev/sdk 4.5.4
ai 7.0.31, @ai-sdk/react 4.0.34, next 16.2.10
Describe the bug
I am building on chat.agent() with durable sessions, and background tasks wake a conversation by appending a user message to session.in (a data load finishing, a scheduled heartbeat). To render those wake turns live in open tabs I turned on watch: true on useTriggerChatTransport. When the wake turn streams in, the tab shows the previous assistant message twice, and the new turn's content renders inside a message that also contains the previous turn's parts. A refresh shows the conversation correctly, so persistence is fine; it is the live view that corrupts.
I dug in and I am fairly confident about the mechanism. Watch mode keeps one useChat response stream open across turns: on turn-complete the transport re-arms and continues instead of closing (packages/trigger-sdk/src/v3/chat.ts, the "Re-arm per-turn events" branch). But the AI SDK's stream processing is single-message per request: createStreamingUIMessageState seeds one in-progress message, and a start chunk only renames it (state.message.id = chunk.messageId) rather than beginning a new one. So the second turn's start renames the first turn's still-referenced message object and keeps appending parts into it. useChat's write path replaces the last message only when ids match, and after the rename they do not, so the renamed message (old parts plus new parts) is pushed alongside the stale snapshot of the first one. Two copies, and a structurally wrong message.
I verified the wire is innocent: replaying the session's .out stream from seq 0 shows each turn's chunks exactly once. The duplicate is manufactured client-side by the interaction above, which I think means any watch: true consumer rendering through useChat hits this on every out-of-band turn.
Two side effects of the same root, for completeness: because the stream never closes, useChat status never returns to "ready" after a watched turn (composers stay disabled, onFinish never fires), and once the session goes idle the subscription reconnects on its 60s stall timer forever.
Reproduction repo
The project is a hackathon submission that goes public at https://github.com/mcheemaa/rendi in the next couple of days. The three steps below reproduce on any chat.agent() project with useChat, and I am happy to put together a minimal public repro if that helps.
To reproduce
- Any
chat.agent()task plususeTriggerChatTransport({ watch: true })anduseChat. Send a message, let the turn complete, keep the tab open. - From a task or a script, append a user message to the same session out-of-band:
sessions.open(chatId).in.send({ kind: "message", payload: { chatId, trigger: "submit-message", messageId, message } }). - Watch the open tab as the new turn streams: the previous assistant message duplicates, the new content lands under the renamed message, and the composer stays on its streaming state indefinitely.
Additional information
Possibly related, while debugging this I noticed the dashboard's session view has its own trouble with these out-of-band messages: on first load it renders the injected user message directly under the conversation's first user message, before the previous turn's output, and toggling Raw and Rendered a couple of times re-sorts it into the correct position (easy to see on any session that has one of these injected messages). Different surface, but it looks like the same underlying edge: a session.in message that no browser sent, which rendering layers do not expect between turns. Happy to file that separately if that is more useful for triage.
The workaround I shipped, in case it helps others: drop watch: true; while idle, poll persisted messages and adopt new rows by id; when the adopted tail is a user-role message, set the session state's isStreaming back to true and call resumeStream(). reconnectToStream then attaches from the stored cursor, and because the last message is user-role the AI SDK seeds a fresh assistant message, so the wake turn streams in live and correctly.
On a fix direction: it seems like the transport is the right place, since the AI SDK's one-message-per-request model is its documented contract. If watch mode ended the useChat request at each turn-complete and surfaced the next turn as something the client attaches to (an event plus resumeStream, or an auto-reattach), each turn would get its own request and a fresh message.
- Vorherrschende Sprache
- TypeScript
- Sterne
- 16.5k
- Forks
- 1.5k
- Ø Merge
- 1 T. 15 Std.
- Gemergte PRs (30 T.)
- 17
Entwicklungsumgebung
Dieses Projekt bietet weder Dev-Container noch Dockerfile noch Beitragsleitfaden – die Einrichtung liegt bei Ihnen. Beginnen Sie mit der README; die allgemeinen Schritte stehen in unserem Leitfaden für den ersten Beitrag.
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus triggerdotdev/trigger.dev
-
bug: retry.fetch ignores the caller's abort signal and retries aborted requestsEvtl. wieder frei Ein Pull Request dazu wurde ohne Merge geschlossen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 76/100
triggerdotdev/trigger.dev#5116 · 2 Kommentare ·
Maintainer antworten meist innerhalb von 1 Tag
-
bug: Chat transport crashes on Safari < 17.4: AbortSignal.any is not a functionEvtl. wieder frei Ein Pull Request dazu wurde ohne Merge geschlossen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 83/100
triggerdotdev/trigger.dev#5098 · 2 Kommentare ·
Maintainer antworten meist innerhalb von 1 Tag
-
bug: Side menu items lose their active state on detail pagesEvtl. wieder frei Ein Pull Request dazu wurde ohne Merge geschlossen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
triggerdotdev/trigger.dev#5088 ·
Maintainer antworten meist innerhalb von 1 Tag
-
bug: RBAC loader logs "plugin found but failed to load" when @triggerdotdev/plugins is simply not installedEvtl. wieder frei @itsfaryal hat das vor 17 Tagen übernommen, und es ist kein Pull Request offen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 82/100
triggerdotdev/trigger.dev#5083 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 83/100
triggerdotdev/trigger.dev#5072 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag
Alle Issues in triggerdotdev/trigger.dev
Ähnliche Issues
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
Maintainer antworten meist innerhalb von 1 Tag
-
bug platform-android
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 68/100
Termix-SSH/Mobile#146 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 2 Tagen
-
bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 66/100
devswha/herdr-web-ui#3875 ·
-
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 88/100
-
area: runtime chore cli consistency dependencies developer-experience difficulty: easy discovery documentation enhancement good first issue help wanted hygiene json-output packaging priority: P3 tests version
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
Maintainer antworten meist innerhalb von 1 Tag