@@ -1076,6 +1076,157 @@ describe("initSessionState reset policy", () => {
10761076 } ) ;
10771077} ) ;
10781078
1079+ describe ( "initSessionState orphan short replies" , ( ) => {
1080+ async function writeTranscript ( params : {
1081+ transcriptPath : string ;
1082+ sessionId : string ;
1083+ userText : string ;
1084+ assistantText : string ;
1085+ } ) {
1086+ await fs . mkdir ( path . dirname ( params . transcriptPath ) , { recursive : true } ) ;
1087+ const header = {
1088+ type : "session" ,
1089+ version : 3 ,
1090+ id : params . sessionId ,
1091+ timestamp : new Date ( ) . toISOString ( ) ,
1092+ cwd : process . cwd ( ) ,
1093+ } ;
1094+ const userMessage = {
1095+ type : "message" ,
1096+ id : "m1" ,
1097+ parentId : null ,
1098+ timestamp : new Date ( ) . toISOString ( ) ,
1099+ message : { role : "user" , content : params . userText } ,
1100+ } ;
1101+ const assistantMessage = {
1102+ type : "message" ,
1103+ id : "m2" ,
1104+ parentId : "m1" ,
1105+ timestamp : new Date ( ) . toISOString ( ) ,
1106+ message : { role : "assistant" , content : params . assistantText } ,
1107+ } ;
1108+ await fs . writeFile (
1109+ params . transcriptPath ,
1110+ `${ JSON . stringify ( header ) } \n${ JSON . stringify ( userMessage ) } \n${ JSON . stringify ( assistantMessage ) } \n` ,
1111+ "utf-8" ,
1112+ ) ;
1113+ }
1114+
1115+ it ( "reattaches short confirmation replies to the latest awaiting TUI session" , async ( ) => {
1116+ vi . useFakeTimers ( ) ;
1117+ vi . setSystemTime ( new Date ( "2026-03-17T09:00:00.000Z" ) ) ;
1118+
1119+ const root = await makeCaseDir ( "openclaw-orphan-short-reply-" ) ;
1120+ const storePath = path . join ( root , "sessions.json" ) ;
1121+ const sessionsDir = path . join ( root , "sessions" ) ;
1122+ const resumedSessionId = "tui-session-resume" ;
1123+ const resumedTranscript = path . join ( sessionsDir , `${ resumedSessionId } .jsonl` ) ;
1124+ const resumedSessionKey = "agent:main:tui:main:tui-pane-1" ;
1125+
1126+ await writeTranscript ( {
1127+ transcriptPath : resumedTranscript ,
1128+ sessionId : resumedSessionId ,
1129+ userText : "Should we ship this now?" ,
1130+ assistantText : "Do you want me to continue with the current plan? Reply yes to continue." ,
1131+ } ) ;
1132+
1133+ await writeSessionStoreFast ( storePath , {
1134+ [ resumedSessionKey ] : {
1135+ sessionId : resumedSessionId ,
1136+ sessionFile : resumedTranscript ,
1137+ updatedAt : Date . now ( ) - 8 * 60 * 60 * 1000 ,
1138+ origin : {
1139+ provider : "webchat" ,
1140+ to : "session:tui:tui-pane-1" ,
1141+ } ,
1142+ deliveryContext : {
1143+ channel : "webchat" ,
1144+ to : "session:tui:tui-pane-1" ,
1145+ } ,
1146+ lastChannel : "webchat" ,
1147+ lastTo : "session:tui:tui-pane-1" ,
1148+ } ,
1149+ } ) ;
1150+
1151+ const cfg = {
1152+ session : {
1153+ store : storePath ,
1154+ } ,
1155+ } as OpenClawConfig ;
1156+
1157+ const result = await initSessionState ( {
1158+ ctx : {
1159+ Body : "yes" ,
1160+ RawBody : "yes" ,
1161+ BodyForCommands : "yes" ,
1162+ SessionKey : "agent:main:fresh-confirmation" ,
1163+ Surface : "webchat" ,
1164+ Provider : "webchat" ,
1165+ OriginatingChannel : "webchat" ,
1166+ OriginatingTo : "session:tui:tui-pane-1" ,
1167+ SenderId : "gateway-client" ,
1168+ } ,
1169+ cfg,
1170+ commandAuthorized : true ,
1171+ } ) ;
1172+
1173+ expect ( result . sessionKey ) . toBe ( resumedSessionKey ) ;
1174+ expect ( result . sessionEntry . sessionId ) . toBe ( resumedSessionId ) ;
1175+ expect ( result . isNewSession ) . toBe ( false ) ;
1176+ vi . useRealTimers ( ) ;
1177+ } ) ;
1178+
1179+ it ( "does not hijack short replies when the candidate belongs to a different TUI route" , async ( ) => {
1180+ const root = await makeCaseDir ( "openclaw-orphan-short-reply-other-route-" ) ;
1181+ const storePath = path . join ( root , "sessions.json" ) ;
1182+ const sessionsDir = path . join ( root , "sessions" ) ;
1183+ const resumedSessionId = "tui-session-other" ;
1184+ const resumedTranscript = path . join ( sessionsDir , `${ resumedSessionId } .jsonl` ) ;
1185+
1186+ await writeTranscript ( {
1187+ transcriptPath : resumedTranscript ,
1188+ sessionId : resumedSessionId ,
1189+ userText : "Should we ship this now?" ,
1190+ assistantText : "Do you want me to continue with the current plan? Reply yes to continue." ,
1191+ } ) ;
1192+
1193+ await writeSessionStoreFast ( storePath , {
1194+ "agent:main:tui:main:tui-pane-2" : {
1195+ sessionId : resumedSessionId ,
1196+ sessionFile : resumedTranscript ,
1197+ updatedAt : Date . now ( ) ,
1198+ lastChannel : "webchat" ,
1199+ lastTo : "session:tui:tui-pane-2" ,
1200+ } ,
1201+ } ) ;
1202+
1203+ const cfg = {
1204+ session : {
1205+ store : storePath ,
1206+ } ,
1207+ } as OpenClawConfig ;
1208+
1209+ const result = await initSessionState ( {
1210+ ctx : {
1211+ Body : "yes" ,
1212+ RawBody : "yes" ,
1213+ BodyForCommands : "yes" ,
1214+ SessionKey : "agent:main:fresh-confirmation" ,
1215+ Surface : "webchat" ,
1216+ Provider : "webchat" ,
1217+ OriginatingChannel : "webchat" ,
1218+ OriginatingTo : "session:tui:tui-pane-1" ,
1219+ SenderId : "gateway-client" ,
1220+ } ,
1221+ cfg,
1222+ commandAuthorized : true ,
1223+ } ) ;
1224+
1225+ expect ( result . sessionKey ) . toBe ( "agent:main:fresh-confirmation" ) ;
1226+ expect ( result . isNewSession ) . toBe ( true ) ;
1227+ } ) ;
1228+ } ) ;
1229+
10791230describe ( "initSessionState channel reset overrides" , ( ) => {
10801231 it ( "uses channel-specific reset policy when configured" , async ( ) => {
10811232 const root = await makeCaseDir ( "openclaw-channel-idle-" ) ;
0 commit comments