Skip to content

Commit 693c646

Browse files
committed
fix(sdk): await the in-flight stream edit before the final channel egress
In stream delivery the debounce editor's stop() cleared the pending timer but returned immediately even with an edit request already in flight. The turn then issued the authoritative final edit to the same message ref, so a slower in-flight partial-text edit could land after it and leave truncated text as the last write. stop() now awaits any in-flight edit (and makeChannelStreamTap's flush/cancel and the turn finally await stop()), so the final edit is always the last write.
1 parent 093d0cd commit 693c646

2 files changed

Lines changed: 46 additions & 29 deletions

File tree

packages/trigger-sdk/src/v3/ai.ts

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5062,7 +5062,7 @@ function makeChannelStreamEditor<TEvent>(
50625062
let latest = "";
50635063
let lastSent = "";
50645064
let timer: ReturnType<typeof setTimeout> | undefined;
5065-
let inFlight = false;
5065+
let inFlightPromise: Promise<void> | undefined;
50665066
let stopped = false;
50675067

50685068
const arm = () => {
@@ -5074,7 +5074,7 @@ function makeChannelStreamEditor<TEvent>(
50745074
};
50755075

50765076
const edit = async () => {
5077-
if (stopped || inFlight) return;
5077+
if (stopped || inFlightPromise) return;
50785078
const text = latest;
50795079
if (text === lastSent) return;
50805080
const message = outbound({
@@ -5084,22 +5084,24 @@ function makeChannelStreamEditor<TEvent>(
50845084
stopped: false,
50855085
});
50865086
if (!message) return;
5087-
inFlight = true;
5088-
try {
5089-
await connector.send!(message, {
5090-
event: channelEvent.event as TEvent,
5091-
deliveryId: channelEvent.deliveryId,
5092-
previousRef: ackRef,
5093-
mode: "stream",
5094-
final: false,
5095-
});
5096-
lastSent = text;
5097-
} catch (error) {
5098-
logger.warn("chat.agent: channel stream edit failed", { error });
5099-
} finally {
5100-
inFlight = false;
5101-
if (!stopped && latest !== lastSent) arm();
5102-
}
5087+
inFlightPromise = (async () => {
5088+
try {
5089+
await connector.send!(message, {
5090+
event: channelEvent.event as TEvent,
5091+
deliveryId: channelEvent.deliveryId,
5092+
previousRef: ackRef,
5093+
mode: "stream",
5094+
final: false,
5095+
});
5096+
lastSent = text;
5097+
} catch (error) {
5098+
logger.warn("chat.agent: channel stream edit failed", { error });
5099+
} finally {
5100+
inFlightPromise = undefined;
5101+
if (!stopped && latest !== lastSent) arm();
5102+
}
5103+
})();
5104+
await inFlightPromise;
51035105
};
51045106

51055107
return {
@@ -5111,17 +5113,22 @@ function makeChannelStreamEditor<TEvent>(
51115113
arm();
51125114
}
51135115
},
5114-
stop() {
5116+
async stop() {
51155117
stopped = true;
51165118
if (timer) {
51175119
clearTimeout(timer);
51185120
timer = undefined;
51195121
}
5122+
if (inFlightPromise) {
5123+
try {
5124+
await inFlightPromise;
5125+
} catch {}
5126+
}
51205127
},
51215128
};
51225129
}
51235130

5124-
type ChannelStreamEditor = { observe(chunk: unknown): void; stop(): void };
5131+
type ChannelStreamEditor = { observe(chunk: unknown): void; stop(): void | Promise<void> };
51255132

51265133
/**
51275134
* Wrap the reply stream so it debounce-edits the channel message as it streams.
@@ -5132,18 +5139,18 @@ type ChannelStreamEditor = { observe(chunk: unknown): void; stop(): void };
51325139
function makeChannelStreamTap(editor: ChannelStreamEditor): TransformStream<unknown, unknown> {
51335140
const transformer: {
51345141
transform(chunk: unknown, controller: TransformStreamDefaultController<unknown>): void;
5135-
flush(): void;
5136-
cancel?: (reason?: unknown) => void;
5142+
flush(): Promise<void>;
5143+
cancel?: (reason?: unknown) => Promise<void>;
51375144
} = {
51385145
transform(chunk, controller) {
51395146
editor.observe(chunk);
51405147
controller.enqueue(chunk);
51415148
},
5142-
flush() {
5143-
editor.stop();
5149+
async flush() {
5150+
await editor.stop();
51445151
},
5145-
cancel() {
5146-
editor.stop();
5152+
async cancel() {
5153+
await editor.stop();
51475154
},
51485155
};
51495156
return new TransformStream<unknown, unknown>(transformer);
@@ -7938,7 +7945,7 @@ function chatAgent<
79387945
}
79397946
} finally {
79407947
msgSub.off();
7941-
channelStreamEditor?.stop();
7948+
await channelStreamEditor?.stop();
79427949
}
79437950

79447951
// Wait for onFinish to fire — on abort this may resolve slightly

packages/trigger-sdk/test/chatChannels.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,12 @@ describe("makeChannelStreamEditor", () => {
350350
describe("makeChannelStreamTap", () => {
351351
it("stops the editor when the stream completes (flush)", async () => {
352352
let stops = 0;
353-
const tap = __makeChannelStreamTapForTests({ observe: () => {}, stop: () => (stops += 1) });
353+
const tap = __makeChannelStreamTapForTests({
354+
observe: () => {},
355+
stop: () => {
356+
stops += 1;
357+
},
358+
});
354359
const source = new ReadableStream({
355360
start(controller) {
356361
controller.enqueue({ type: "text-delta", delta: "x" });
@@ -367,7 +372,12 @@ describe("makeChannelStreamTap", () => {
367372

368373
it("stops the editor when the stream is cancelled mid-flight (abort)", async () => {
369374
let stops = 0;
370-
const tap = __makeChannelStreamTapForTests({ observe: () => {}, stop: () => (stops += 1) });
375+
const tap = __makeChannelStreamTapForTests({
376+
observe: () => {},
377+
stop: () => {
378+
stops += 1;
379+
},
380+
});
371381
const source = new ReadableStream({
372382
start(controller) {
373383
controller.enqueue({ type: "text-delta", delta: "x" });

0 commit comments

Comments
 (0)