@@ -68,117 +68,112 @@ const StopSchema = z.object({
6868// when the user stops mid-stream. Lock release is handled by the
6969// aborted server stream unwinding, not this handler.
7070export const POST = withRouteHandler ( ( req : NextRequest ) =>
71- withIncomingGoSpan (
72- req . headers ,
73- TraceSpan . CopilotChatStopStream ,
74- undefined ,
75- async ( span ) => {
76- try {
77- const session = await getSession ( )
78- if ( ! session ?. user ?. id ) {
79- span . setAttribute ( TraceAttr . CopilotStopOutcome , CopilotStopOutcome . Unauthorized )
80- return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } )
81- }
71+ withIncomingGoSpan ( req . headers , TraceSpan . CopilotChatStopStream , undefined , async ( span ) => {
72+ try {
73+ const session = await getSession ( )
74+ if ( ! session ?. user ?. id ) {
75+ span . setAttribute ( TraceAttr . CopilotStopOutcome , CopilotStopOutcome . Unauthorized )
76+ return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } )
77+ }
8278
83- const { chatId, streamId, content, contentBlocks, requestId } = StopSchema . parse (
84- await req . json ( )
85- )
86- span . setAttributes ( {
87- [ TraceAttr . ChatId ] : chatId ,
88- [ TraceAttr . StreamId ] : streamId ,
89- [ TraceAttr . UserId ] : session . user . id ,
90- [ TraceAttr . CopilotStopContentLength ] : content . length ,
91- [ TraceAttr . CopilotStopBlocksCount ] : contentBlocks ?. length ?? 0 ,
92- ...( requestId ? { [ TraceAttr . RequestId ] : requestId } : { } ) ,
93- } )
79+ const { chatId, streamId, content, contentBlocks, requestId } = StopSchema . parse (
80+ await req . json ( )
81+ )
82+ span . setAttributes ( {
83+ [ TraceAttr . ChatId ] : chatId ,
84+ [ TraceAttr . StreamId ] : streamId ,
85+ [ TraceAttr . UserId ] : session . user . id ,
86+ [ TraceAttr . CopilotStopContentLength ] : content . length ,
87+ [ TraceAttr . CopilotStopBlocksCount ] : contentBlocks ?. length ?? 0 ,
88+ ...( requestId ? { [ TraceAttr . RequestId ] : requestId } : { } ) ,
89+ } )
9490
95- const [ row ] = await db
96- . select ( {
97- workspaceId : copilotChats . workspaceId ,
98- messages : copilotChats . messages ,
99- } )
100- . from ( copilotChats )
101- . where ( and ( eq ( copilotChats . id , chatId ) , eq ( copilotChats . userId , session . user . id ) ) )
102- . limit ( 1 )
91+ const [ row ] = await db
92+ . select ( {
93+ workspaceId : copilotChats . workspaceId ,
94+ messages : copilotChats . messages ,
95+ } )
96+ . from ( copilotChats )
97+ . where ( and ( eq ( copilotChats . id , chatId ) , eq ( copilotChats . userId , session . user . id ) ) )
98+ . limit ( 1 )
10399
104- if ( ! row ) {
105- span . setAttribute ( TraceAttr . CopilotStopOutcome , CopilotStopOutcome . ChatNotFound )
106- return NextResponse . json ( { success : true } )
107- }
100+ if ( ! row ) {
101+ span . setAttribute ( TraceAttr . CopilotStopOutcome , CopilotStopOutcome . ChatNotFound )
102+ return NextResponse . json ( { success : true } )
103+ }
108104
109- const messages : Record < string , unknown > [ ] = Array . isArray ( row . messages ) ? row . messages : [ ]
110- const userIdx = messages . findIndex ( ( message ) => message . id === streamId )
111- const alreadyHasResponse =
112- userIdx >= 0 &&
113- userIdx + 1 < messages . length &&
114- ( messages [ userIdx + 1 ] as Record < string , unknown > ) ?. role === 'assistant'
115- const canAppendAssistant =
116- userIdx >= 0 && userIdx === messages . length - 1 && ! alreadyHasResponse
105+ const messages : Record < string , unknown > [ ] = Array . isArray ( row . messages ) ? row . messages : [ ]
106+ const userIdx = messages . findIndex ( ( message ) => message . id === streamId )
107+ const alreadyHasResponse =
108+ userIdx >= 0 &&
109+ userIdx + 1 < messages . length &&
110+ ( messages [ userIdx + 1 ] as Record < string , unknown > ) ?. role === 'assistant'
111+ const canAppendAssistant =
112+ userIdx >= 0 && userIdx === messages . length - 1 && ! alreadyHasResponse
117113
118- const updateWhere = and (
119- eq ( copilotChats . id , chatId ) ,
120- eq ( copilotChats . userId , session . user . id ) ,
121- eq ( copilotChats . conversationId , streamId )
122- )
114+ const updateWhere = and (
115+ eq ( copilotChats . id , chatId ) ,
116+ eq ( copilotChats . userId , session . user . id ) ,
117+ eq ( copilotChats . conversationId , streamId )
118+ )
123119
124- const setClause : Record < string , unknown > = {
125- conversationId : null ,
126- updatedAt : new Date ( ) ,
127- }
120+ const setClause : Record < string , unknown > = {
121+ conversationId : null ,
122+ updatedAt : new Date ( ) ,
123+ }
128124
129- const hasContent = content . trim ( ) . length > 0
130- const hasBlocks = Array . isArray ( contentBlocks ) && contentBlocks . length > 0
131- const synthesizedStoppedBlocks = hasBlocks
132- ? contentBlocks
133- : hasContent
134- ? [ { type : 'text' , channel : 'assistant' , content } , { type : 'stopped' } ]
135- : [ { type : 'stopped' } ]
136- if ( canAppendAssistant ) {
137- const normalized = normalizeMessage ( {
138- id : generateId ( ) ,
139- role : 'assistant' ,
140- content,
141- timestamp : new Date ( ) . toISOString ( ) ,
142- contentBlocks : synthesizedStoppedBlocks ,
143- // Persist so the UI copy-request-id button survives refetch.
144- ...( requestId ? { requestId } : { } ) ,
145- } )
146- const assistantMessage : PersistedMessage = normalized
147- setClause . messages = sql `${ copilotChats . messages } || ${ JSON . stringify ( [ assistantMessage ] ) } ::jsonb`
148- }
149- span . setAttribute ( TraceAttr . CopilotStopAppendedAssistant , canAppendAssistant )
125+ const hasContent = content . trim ( ) . length > 0
126+ const hasBlocks = Array . isArray ( contentBlocks ) && contentBlocks . length > 0
127+ const synthesizedStoppedBlocks = hasBlocks
128+ ? contentBlocks
129+ : hasContent
130+ ? [ { type : 'text' , channel : 'assistant' , content } , { type : 'stopped' } ]
131+ : [ { type : 'stopped' } ]
132+ if ( canAppendAssistant ) {
133+ const normalized = normalizeMessage ( {
134+ id : generateId ( ) ,
135+ role : 'assistant' ,
136+ content,
137+ timestamp : new Date ( ) . toISOString ( ) ,
138+ contentBlocks : synthesizedStoppedBlocks ,
139+ // Persist so the UI copy-request-id button survives refetch.
140+ ...( requestId ? { requestId } : { } ) ,
141+ } )
142+ const assistantMessage : PersistedMessage = normalized
143+ setClause . messages = sql `${ copilotChats . messages } || ${ JSON . stringify ( [ assistantMessage ] ) } ::jsonb`
144+ }
145+ span . setAttribute ( TraceAttr . CopilotStopAppendedAssistant , canAppendAssistant )
150146
151- const [ updated ] = await db
152- . update ( copilotChats )
153- . set ( setClause )
154- . where ( updateWhere )
155- . returning ( { workspaceId : copilotChats . workspaceId } )
147+ const [ updated ] = await db
148+ . update ( copilotChats )
149+ . set ( setClause )
150+ . where ( updateWhere )
151+ . returning ( { workspaceId : copilotChats . workspaceId } )
156152
157- if ( updated ?. workspaceId ) {
158- taskPubSub ?. publishStatusChanged ( {
159- workspaceId : updated . workspaceId ,
160- chatId,
161- type : 'completed' ,
162- } )
163- }
153+ if ( updated ?. workspaceId ) {
154+ taskPubSub ?. publishStatusChanged ( {
155+ workspaceId : updated . workspaceId ,
156+ chatId,
157+ type : 'completed' ,
158+ } )
159+ }
164160
165- span . setAttribute (
166- TraceAttr . CopilotStopOutcome ,
167- updated ? CopilotStopOutcome . Persisted : CopilotStopOutcome . NoMatchingRow
161+ span . setAttribute (
162+ TraceAttr . CopilotStopOutcome ,
163+ updated ? CopilotStopOutcome . Persisted : CopilotStopOutcome . NoMatchingRow
164+ )
165+ return NextResponse . json ( { success : true } )
166+ } catch ( error ) {
167+ if ( error instanceof z . ZodError ) {
168+ span . setAttribute ( TraceAttr . CopilotStopOutcome , CopilotStopOutcome . ValidationError )
169+ return NextResponse . json (
170+ { error : 'Invalid request data' , details : error . errors } ,
171+ { status : 400 }
168172 )
169- return NextResponse . json ( { success : true } )
170- } catch ( error ) {
171- if ( error instanceof z . ZodError ) {
172- span . setAttribute ( TraceAttr . CopilotStopOutcome , CopilotStopOutcome . ValidationError )
173- return NextResponse . json (
174- { error : 'Invalid request data' , details : error . errors } ,
175- { status : 400 }
176- )
177- }
178- logger . error ( 'Error stopping chat stream:' , error )
179- span . setAttribute ( TraceAttr . CopilotStopOutcome , CopilotStopOutcome . InternalError )
180- return NextResponse . json ( { error : 'Internal server error' } , { status : 500 } )
181173 }
174+ logger . error ( 'Error stopping chat stream:' , error )
175+ span . setAttribute ( TraceAttr . CopilotStopOutcome , CopilotStopOutcome . InternalError )
176+ return NextResponse . json ( { error : 'Internal server error' } , { status : 500 } )
182177 }
183- )
178+ } )
184179)
0 commit comments