@@ -68,6 +68,7 @@ async function sendToBeacon(client, payload) {
6868 ] )
6969 if ( "timeout" in outcome ) {
7070 proc . kill ( )
71+ await Promise . race ( [ proc . exited . catch ( ( ) => undefined ) , Bun . sleep ( 250 ) ] )
7172 await debugLog ( client , "Beacon hook command timed out" , { type : payload ?. type } )
7273 return
7374 }
@@ -122,7 +123,8 @@ function shellMutationPaths(tool, args) {
122123 if ( name !== "bash" && ! name . includes ( "shell" ) ) return [ ]
123124 const command = String ( args ?. command || args ?. cmd || "" )
124125 const paths = [ ]
125- const pattern = / \b (?: r m | u n l i n k ) \s + (?: - [ ^ \s ] + \s + ) * (?: " ( [ ^ " ] + ) " | ' ( [ ^ ' ] + ) ' | ( \S + ) ) / g
126+ const pattern =
127+ / (?: ^ | (?: & & | \| \| | ; | \n ) \s * ) (?: s u d o \s + ) ? (?: c o m m a n d \s + ) ? (?: \/ b i n \/ ) ? (?: r m | u n l i n k ) \s + (?: - [ ^ \s ] + \s + ) * (?: " ( [ ^ " ] + ) " | ' ( [ ^ ' ] + ) ' | ( [ ^ \s ; & | ] + ) ) / g
126128 for ( const match of command . matchAll ( pattern ) ) {
127129 const path = match [ 1 ] || match [ 2 ] || match [ 3 ]
128130 if ( path ) paths . push ( path )
@@ -133,12 +135,13 @@ function shellMutationPaths(tool, args) {
133135export const BeaconEndpointPlugin = async ( { project, directory, worktree, client } ) => {
134136 const context = { project, directory, worktree }
135137 const activeCalls = new Map ( )
136- const completedCalls = new Set ( )
138+ const completedCalls = new Map ( )
137139 const emittedParts = new Set ( )
138140 const pendingParts = new Map ( )
139141 const partDeltas = new Map ( )
140142 const messageModels = new Map ( )
141143 const messageRoles = new Map ( )
144+ const messageSessions = new Map ( )
142145 const completedMessages = new Set ( )
143146 const permissionRequests = new Map ( )
144147 const sessionStates = new Map ( )
@@ -204,10 +207,40 @@ export const BeaconEndpointPlugin = async ({ project, directory, worktree, clien
204207 emitPart ( part , sid , item . model )
205208 }
206209 }
210+ const flushMessageParts = ( messageID , sid , model , completedAt ) => {
211+ for ( const [ key , item ] of pendingParts ) {
212+ if ( item . part ?. messageID !== messageID ) continue
213+ const part = { ...item . part , time : { ...( item . part . time || { } ) , end : completedAt || Date . now ( ) } }
214+ pendingParts . delete ( key )
215+ emitPart ( part , sid , model || item . model )
216+ }
217+ }
218+ const cleanupSession = ( sid ) => {
219+ for ( const [ callID , item ] of activeCalls ) if ( item . session_id === sid ) activeCalls . delete ( callID )
220+ for ( const [ callID , session ] of completedCalls ) if ( session === sid ) completedCalls . delete ( callID )
221+ for ( const key of emittedParts ) if ( key . startsWith ( `${ sid } :` ) ) emittedParts . delete ( key )
222+ for ( const [ key , item ] of pendingParts ) if ( item . session_id === sid ) pendingParts . delete ( key )
223+ for ( const key of partDeltas . keys ( ) ) if ( key . startsWith ( `${ sid } :` ) ) partDeltas . delete ( key )
224+ for ( const [ messageID , session ] of messageSessions ) {
225+ if ( session !== sid ) continue
226+ messageSessions . delete ( messageID )
227+ messageModels . delete ( messageID )
228+ messageRoles . delete ( messageID )
229+ completedMessages . delete ( messageID )
230+ }
231+ for ( const [ requestID , request ] of permissionRequests ) {
232+ if ( request . sessionID === sid ) permissionRequests . delete ( requestID )
233+ }
234+ sessionStates . delete ( sid )
235+ for ( const [ path , item ] of recentFilePaths ) if ( item . sessionID === sid ) recentFilePaths . delete ( path )
236+ }
207237
208238 return {
209239 "chat.message" : async ( input , output ) => {
210- if ( output ?. message ?. id ) messageRoles . set ( output . message . id , "user" )
240+ if ( output ?. message ?. id ) {
241+ messageRoles . set ( output . message . id , "user" )
242+ messageSessions . set ( output . message . id , sessionID ( input ) )
243+ }
211244 await enqueue ( {
212245 type : "chat.message" ,
213246 session_id : sessionID ( input ) ,
@@ -261,7 +294,7 @@ export const BeaconEndpointPlugin = async ({ project, directory, worktree, clien
261294 } ) ,
262295 )
263296 activeCalls . delete ( input ?. callID )
264- completedCalls . add ( input ?. callID )
297+ completedCalls . set ( input ?. callID , sessionID ( input ) )
265298 } ,
266299 event : async ( { event } ) => {
267300 const type = event ?. type || "event"
@@ -271,11 +304,15 @@ export const BeaconEndpointPlugin = async ({ project, directory, worktree, clien
271304 const sid = sessionID ( properties )
272305 const info = properties ?. info || { }
273306 if ( type === "message.updated" ) {
274- if ( info ?. id && info ?. role ) messageRoles . set ( info . id , info . role )
307+ if ( info ?. id ) {
308+ if ( info ?. role ) messageRoles . set ( info . id , info . role )
309+ messageSessions . set ( info . id , sid )
310+ }
275311 if ( info ?. role !== "assistant" ) return
276312 const model = modelName ( info )
277313 if ( info ?. id && model ) messageModels . set ( info . id , model )
278314 if ( ! info ?. time ?. completed && ! info ?. error ) return
315+ flushMessageParts ( info . id , sid , model , info ?. time ?. completed )
279316 if ( info ?. id ) {
280317 if ( completedMessages . has ( info . id ) ) return
281318 completedMessages . add ( info . id )
@@ -302,9 +339,14 @@ export const BeaconEndpointPlugin = async ({ project, directory, worktree, clien
302339 if ( sessionStates . get ( sid ) === "idle" ) return
303340 sessionStates . set ( sid , "idle" )
304341 }
342+ if ( type === "session.deleted" ) flushParts ( sid )
305343 if ( type === "permission.asked" || type === "permission.v2.asked" ) {
306344 if ( properties ?. id ) {
307- permissionRequests . set ( properties . id , { tool : properties ?. tool , permission : properties ?. permission } )
345+ permissionRequests . set ( properties . id , {
346+ tool : properties ?. tool ,
347+ permission : properties ?. permission ,
348+ sessionID : sid ,
349+ } )
308350 }
309351 }
310352 if ( type === "permission.replied" || type === "permission.v2.replied" ) {
@@ -331,14 +373,15 @@ export const BeaconEndpointPlugin = async ({ project, directory, worktree, clien
331373 if ( seen && Date . now ( ) - seen . time < 5000 ) return
332374 if ( ! sid ) return
333375 }
334- void enqueue (
376+ const queued = enqueue (
335377 payload ( type , {
336378 session_id : sessionID ( properties ) || sid ,
337- model : modelName ( info || properties ) ,
379+ model : modelName ( Object . keys ( info ) . length > 0 ? info : properties ) ,
338380 properties,
339381 event,
340382 } ) ,
341383 )
384+ if ( type === "session.deleted" ) void queued . finally ( ( ) => cleanupSession ( sid ) )
342385 } ,
343386 }
344387}
0 commit comments