Skip to content

Commit 92b6a91

Browse files
committed
fix(sdk): recover stopped sessions after transcript reload
1 parent 4ca683d commit 92b6a91

7 files changed

Lines changed: 521 additions & 16 deletions

File tree

‎.changeset/chat-stop-successor-boundary.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44

55
Keep new chat responses intact after Stop, including slow Stop acknowledgments and page reloads.
66
Sequence-free replies after Stop require a transcript reload before further messages.
7+
Loading a fresh transcript through `useLoadTranscript` restores blocked sessions without replaying old output.

‎packages/trigger-sdk/src/v3/chat-react.ts‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export type UseLoadTranscriptOptions = {
6565
* the loaded transcript, so the live subscription opens just past the
6666
* persisted history instead of replaying it. Only applies once the
6767
* transport knows the session (from `sessions` or after `start`).
68+
* For a blocked session, a fresh load with a newer saved cursor also clears
69+
* the transcript reload requirement. A stale result leaves sends blocked.
6870
*/
6971
transport?: TriggerChatTransport;
7072
/** Page size passed to the action. */
@@ -77,14 +79,17 @@ export type UseLoadTranscriptOptions = {
7779
* history. Applied to the session now if it exists, otherwise held by the
7880
* transport until the session is created, so a load that resolves before the
7981
* session exists still moves the cursor. A no-op when the transcript carries
80-
* no cursor. Returns whether a cursor was provided.
82+
* no cursor. A captured recovery callback replaces ordinary cursor seeding.
83+
* Returns whether the cursor was accepted.
8184
*/
8285
export function seedTranscriptCursor(
8386
transport: Pick<TriggerChatTransport, "seedResumeCursor">,
8487
chatId: string,
85-
cursors: { lastOutEventId?: string } | undefined
88+
cursors: { lastOutEventId?: string } | undefined,
89+
completeRecovery?: (lastEventId: string | undefined) => boolean
8690
): boolean {
8791
const lastEventId = cursors?.lastOutEventId;
92+
if (completeRecovery) return completeRecovery(lastEventId);
8893
if (!lastEventId) return false;
8994
transport.seedResumeCursor(chatId, lastEventId);
9095
return true;
@@ -130,8 +135,7 @@ export function useLoadTranscript<TUIMessage extends UIMessage = UIMessage>(
130135

131136
const loadRef = useRef(load);
132137
loadRef.current = load;
133-
const transportRef = useRef(options?.transport);
134-
transportRef.current = options?.transport;
138+
const transport = options?.transport;
135139
const limit = options?.limit;
136140

137141
useEffect(() => {
@@ -140,13 +144,17 @@ export function useLoadTranscript<TUIMessage extends UIMessage = UIMessage>(
140144
return;
141145
}
142146
let cancelled = false;
147+
const completeRecovery = transport?.prepareTranscriptRecovery(chatId);
143148
setState({ chatId, messages: [], isLoading: true, error: undefined, nextCursor: undefined });
144149
loadRef
145150
.current({ chatId, ...(limit !== undefined ? { limit } : {}) })
146151
.then((result) => {
147152
if (cancelled) return;
148-
if (transportRef.current) {
149-
seedTranscriptCursor(transportRef.current, chatId, result.cursors);
153+
if (transport) {
154+
const seeded = seedTranscriptCursor(transport, chatId, result.cursors, completeRecovery);
155+
if (completeRecovery && !seeded) {
156+
throw new Error("The loaded transcript is not current. Reload the chat again.");
157+
}
150158
}
151159
setState({
152160
chatId,
@@ -164,7 +172,7 @@ export function useLoadTranscript<TUIMessage extends UIMessage = UIMessage>(
164172
return () => {
165173
cancelled = true;
166174
};
167-
}, [chatId, limit]);
175+
}, [chatId, limit, transport]);
168176

169177
return {
170178
messages: state.chatId === chatId ? state.messages : [],

0 commit comments

Comments
 (0)