File tree Expand file tree Collapse file tree
app/api/mothership/execute Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -350,7 +350,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
350350
351351 const stream = new ReadableStream < Uint8Array > ( {
352352 start ( controller ) {
353- let forwardedAssistantContent = ''
353+ let lastForwardedTextSeq = - 1
354354 const send = ( event : unknown ) => {
355355 if ( ! cancelled ) {
356356 controller . enqueue ( encodeNdjson ( event ) )
@@ -372,14 +372,15 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
372372 event . payload . channel === MothershipStreamV1TextChannel . assistant &&
373373 event . payload . text
374374 ) {
375- const text = event . payload . text
376- const content = text . startsWith ( forwardedAssistantContent )
377- ? text . slice ( forwardedAssistantContent . length )
378- : text
379- if ( content ) {
380- forwardedAssistantContent += content
381- send ( { type : 'chunk' , content } )
375+ /* The wire carries text DELTAS with monotone seqs; a transport-retry
376+ replay re-delivers earlier seqs. Dedupe replays by seq — the old
377+ string-prefix guess sliced characters off a genuine delta that
378+ happened to begin with the already-forwarded content. */
379+ if ( typeof event . seq === 'number' ) {
380+ if ( event . seq <= lastForwardedTextSeq ) return
381+ lastForwardedTextSeq = event . seq
382382 }
383+ send ( { type : 'chunk' , content : event . payload . text } )
383384 }
384385 } )
385386 allowExplicitAbort = false
Original file line number Diff line number Diff line change @@ -405,7 +405,10 @@ export async function runStreamLoop(
405405 // Yield a macrotask so Node.js flushes the HTTP response buffer to
406406 // the browser. Microtask yields (await Promise.resolve()) are not
407407 // enough — the I/O layer needs a full event loop tick to write.
408- await new Promise < void > ( ( resolve ) => setImmediate ( resolve ) )
408+ // Headless legs (no client response attached) opt out via flushAfterEvent.
409+ if ( options . flushAfterEvent !== false ) {
410+ await new Promise < void > ( ( resolve ) => setImmediate ( resolve ) )
411+ }
409412
410413 if ( options . onBeforeDispatch ?.( streamEvent , context ) ) {
411414 return context . streamComplete || undefined
Original file line number Diff line number Diff line change @@ -912,6 +912,10 @@ async function runCheckpointLoop(
912912
913913 const loopOptions = {
914914 ...options ,
915+ /* The wrapper below always exists (checkpoint bookkeeping), so the forwarder can't
916+ infer "headless" from onEvent's absence — declare it: only a caller-attached sink
917+ has an HTTP buffer worth a per-event macrotask flush. */
918+ flushAfterEvent : options . flushAfterEvent ?? Boolean ( callerOnEvent ) ,
915919 onEvent : async ( event : StreamEvent ) => {
916920 if (
917921 event . type === MothershipStreamV1EventType . run &&
Original file line number Diff line number Diff line change @@ -33,7 +33,13 @@ type EnvelopeToStreamEvent<T> = T extends {
3333 payload : infer TPayload
3434 scope ?: infer TScope
3535}
36- ? { type : TType ; payload : TPayload ; scope ?: Exclude < TScope , undefined > }
36+ ? {
37+ type : TType
38+ payload : TPayload
39+ scope ?: Exclude < TScope , undefined >
40+ /** Wire ordering key, carried off the envelope; absent on synthetic events. */
41+ seq ?: number
42+ }
3743 : never
3844
3945export type SyntheticFilePreviewPhase = ( typeof FILE_PREVIEW_PHASE ) [ keyof typeof FILE_PREVIEW_PHASE ]
Original file line number Diff line number Diff line change @@ -53,6 +53,7 @@ describe('createEvent', () => {
5353 const streamEvent = eventToStreamEvent ( envelope )
5454 expect ( streamEvent ) . toEqual ( {
5555 type : MothershipStreamV1EventType . tool ,
56+ seq : 2 ,
5657 payload : {
5758 previewPhase : 'file_preview_start' ,
5859 toolCallId : 'preview-1' ,
Original file line number Diff line number Diff line change @@ -69,6 +69,7 @@ export function eventToStreamEvent<TEnvelope extends PersistedStreamEventEnvelop
6969 return {
7070 type : envelope . type ,
7171 payload : envelope . payload ,
72+ seq : envelope . seq ,
7273 ...( envelope . scope ? { scope : envelope . scope } : { } ) ,
7374 } as StreamEventFromEnvelope < TEnvelope >
7475}
Original file line number Diff line number Diff line change @@ -220,6 +220,12 @@ export interface OrchestratorOptions {
220220 autoExecuteTools ?: boolean
221221 timeout ?: number
222222 onEvent ?: ( event : StreamEvent ) => void | Promise < void >
223+ /**
224+ * Whether the per-event macrotask yield (which lets Node flush the HTTP response buffer)
225+ * should run. The sink owner sets this: legs with no client response attached have
226+ * nothing to flush, and the yield only slows the forwarder. Defaults to true.
227+ */
228+ flushAfterEvent ?: boolean
223229 onComplete ?: ( result : OrchestratorResult ) => void | Promise < void >
224230 onError ?: ( error : Error , result ?: OrchestratorResult ) => void | Promise < void >
225231 abortSignal ?: AbortSignal
You can’t perform that action at this time.
0 commit comments