@@ -7,6 +7,11 @@ import {
77 MothershipStreamV1EventType ,
88} from '@/lib/copilot/generated/mothership-stream-v1'
99import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
10+ import {
11+ CopilotResumeOutcome ,
12+ CopilotTransport ,
13+ } from '@/lib/copilot/generated/trace-attribute-values-v1'
14+ import { contextFromRequestHeaders } from '@/lib/copilot/request/go/propagation'
1015import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http'
1116import { getCopilotTracer } from '@/lib/copilot/request/otel'
1217import {
@@ -125,15 +130,31 @@ export async function GET(request: NextRequest) {
125130 // manually, capture its context, and re-enter that context inside the
126131 // stream callback so every nested `withCopilotSpan` / `withDbSpan` call
127132 // attaches to this root.
128- const rootSpan = getCopilotTracer ( ) . startSpan ( 'copilot.resume.request' , {
129- attributes : {
130- [ TraceAttr . CopilotTransport ] : batchMode ? 'batch' : 'stream' ,
131- [ TraceAttr . StreamId ] : streamId ,
132- [ TraceAttr . UserId ] : authenticatedUserId ,
133- [ TraceAttr . CopilotResumeAfterCursor ] : afterCursor || '0' ,
133+ //
134+ // `contextFromRequestHeaders` extracts the W3C `traceparent` the
135+ // client echoed (set via `streamTraceparentRef` on Sim's chat POST
136+ // response), so the resume span becomes a child of the original
137+ // chat's `gen_ai.agent.execute` trace instead of a disconnected
138+ // new root. On reconnects after page reload (client ref was wiped)
139+ // the header is absent and extraction leaves the ambient context
140+ // alone → the resume span becomes its own root. Same as pre-
141+ // linking behavior; no regression.
142+ const incomingContext = contextFromRequestHeaders ( request . headers )
143+ const rootSpan = getCopilotTracer ( ) . startSpan (
144+ 'copilot.resume.request' ,
145+ {
146+ attributes : {
147+ [ TraceAttr . CopilotTransport ] : batchMode
148+ ? CopilotTransport . Batch
149+ : CopilotTransport . Stream ,
150+ [ TraceAttr . StreamId ] : streamId ,
151+ [ TraceAttr . UserId ] : authenticatedUserId ,
152+ [ TraceAttr . CopilotResumeAfterCursor ] : afterCursor || '0' ,
153+ } ,
134154 } ,
135- } )
136- const rootContext = trace . setSpan ( otelContext . active ( ) , rootSpan )
155+ incomingContext
156+ )
157+ const rootContext = trace . setSpan ( incomingContext , rootSpan )
137158
138159 try {
139160 return await otelContext . with ( rootContext , ( ) =>
@@ -190,7 +211,7 @@ async function handleResumeRequestBody({
190211 runStatus : run ?. status ,
191212 } )
192213 if ( ! run ) {
193- rootSpan . setAttribute ( TraceAttr . CopilotResumeOutcome , 'stream_not_found' )
214+ rootSpan . setAttribute ( TraceAttr . CopilotResumeOutcome , CopilotResumeOutcome . StreamNotFound )
194215 rootSpan . end ( )
195216 return NextResponse . json ( { error : 'Stream not found' } , { status : 404 } )
196217 }
@@ -217,7 +238,7 @@ async function handleResumeRequestBody({
217238 runStatus : run . status ,
218239 } )
219240 rootSpan . setAttributes ( {
220- [ TraceAttr . CopilotResumeOutcome ] : 'batch_delivered' ,
241+ [ TraceAttr . CopilotResumeOutcome ] : CopilotResumeOutcome . BatchDelivered ,
221242 [ TraceAttr . CopilotResumeEventCount ] : batchEvents . length ,
222243 [ TraceAttr . CopilotResumePreviewSessionCount ] : previewSessions . length ,
223244 } )
@@ -411,10 +432,10 @@ async function handleResumeRequestBody({
411432 closeController ( )
412433 rootSpan . setAttributes ( {
413434 [ TraceAttr . CopilotResumeOutcome ] : sawTerminalEvent
414- ? 'terminal_delivered'
435+ ? CopilotResumeOutcome . TerminalDelivered
415436 : controllerClosed
416- ? 'client_disconnected'
417- : 'ended_without_terminal' ,
437+ ? CopilotResumeOutcome . ClientDisconnected
438+ : CopilotResumeOutcome . EndedWithoutTerminal ,
418439 [ TraceAttr . CopilotResumeEventCount ] : totalEventsFlushed ,
419440 [ TraceAttr . CopilotResumePollIterations ] : pollIterations ,
420441 [ TraceAttr . CopilotResumeDurationMs ] : Date . now ( ) - startTime ,
0 commit comments