@@ -116,6 +116,15 @@ async function writeMainSessionTranscript(sessionDir: string, lines: string[]) {
116116 await fs . writeFile ( path . join ( sessionDir , "sess-main.jsonl" ) , `${ lines . join ( "\n" ) } \n` , "utf-8" ) ;
117117}
118118
119+ async function readTimelineEvents ( filePath : string ) : Promise < Array < Record < string , unknown > > > {
120+ const raw = await fs . readFile ( filePath , "utf-8" ) ;
121+ return raw
122+ . trim ( )
123+ . split ( "\n" )
124+ . filter ( Boolean )
125+ . map ( ( line ) => JSON . parse ( line ) as Record < string , unknown > ) ;
126+ }
127+
119128async function fetchHistoryMessages (
120129 ws : GatewaySocket ,
121130 params ?: {
@@ -1178,6 +1187,60 @@ describe("gateway server chat", () => {
11781187 } ) ;
11791188 } ) ;
11801189
1190+ test ( "chat.send diagnostics timeline carries run correlation attributes" , async ( ) => {
1191+ const timelineDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-chat-timeline-" ) ) ;
1192+ const timelinePath = path . join ( timelineDir , "timeline.jsonl" ) ;
1193+ const previousDiagnostics = process . env . OPENCLAW_DIAGNOSTICS ;
1194+ const previousTimelinePath = process . env . OPENCLAW_DIAGNOSTICS_TIMELINE_PATH ;
1195+ process . env . OPENCLAW_DIAGNOSTICS = "timeline" ;
1196+ process . env . OPENCLAW_DIAGNOSTICS_TIMELINE_PATH = timelinePath ;
1197+ try {
1198+ await withGatewayChatHarness ( async ( { ws, createSessionDir } ) => {
1199+ const spy = getReplyFromConfig ;
1200+ await connectOk ( ws ) ;
1201+
1202+ await createSessionDir ( ) ;
1203+ await writeMainSessionStore ( ) ;
1204+ mockGetReplyFromConfigOnce ( async ( ) => undefined ) ;
1205+
1206+ const sendRes = await rpcReq ( ws , "chat.send" , {
1207+ sessionKey : "main" ,
1208+ message : "hello" ,
1209+ idempotencyKey : "idem-timeline" ,
1210+ } ) ;
1211+ expect ( sendRes . ok ) . toBe ( true ) ;
1212+
1213+ await vi . waitFor ( ( ) => {
1214+ expect ( spy . mock . calls . length ) . toBeGreaterThan ( 0 ) ;
1215+ } , FAST_WAIT_OPTS ) ;
1216+ await vi . waitFor ( async ( ) => {
1217+ const events = await readTimelineEvents ( timelinePath ) ;
1218+ expect (
1219+ events . some (
1220+ ( event ) =>
1221+ event . type === "span.end" &&
1222+ event . name === "gateway.chat_send.dispatch_inbound" &&
1223+ ( event . attributes as Record < string , unknown > | undefined ) ?. runId ===
1224+ "idem-timeline" ,
1225+ ) ,
1226+ ) . toBe ( true ) ;
1227+ } , FAST_WAIT_OPTS ) ;
1228+ } ) ;
1229+ } finally {
1230+ if ( previousDiagnostics === undefined ) {
1231+ delete process . env . OPENCLAW_DIAGNOSTICS ;
1232+ } else {
1233+ process . env . OPENCLAW_DIAGNOSTICS = previousDiagnostics ;
1234+ }
1235+ if ( previousTimelinePath === undefined ) {
1236+ delete process . env . OPENCLAW_DIAGNOSTICS_TIMELINE_PATH ;
1237+ } else {
1238+ process . env . OPENCLAW_DIAGNOSTICS_TIMELINE_PATH = previousTimelinePath ;
1239+ }
1240+ await fs . rm ( timelineDir , { recursive : true , force : true , maxRetries : 5 , retryDelay : 50 } ) ;
1241+ }
1242+ } ) ;
1243+
11811244 test ( "chat.history hard-caps single oversized nested payloads" , async ( ) => {
11821245 await withGatewayChatHarness ( async ( { ws, createSessionDir } ) => {
11831246 const historyMaxBytes = 64 * 1024 ;
0 commit comments