@@ -74,6 +74,41 @@ const threadLookup = new Map(); // "channel:thread_ts" β thread-N
7474let threadCounter = 0 ;
7575const MAX_THREADS = 10_000 ;
7676
77+ // Track inbound message timestamps pending a β
reaction.
78+ // Key: "channel:thread_ts" (the thread root), Value: { channel, messageTs, receivedAt }
79+ // When the agent replies via /send with a matching thread_ts, we react with β
80+ // on the original inbound message and remove the entry.
81+ const pendingAckReactions = new Map ( ) ;
82+ const PENDING_ACK_TTL_MS = 10 * 60 * 1000 ; // 10 minutes
83+
84+ /**
85+ * When the agent sends a reply in a thread, resolve the pending ack by
86+ * adding a β
reaction to the original inbound message and removing the entry.
87+ * Also prunes expired entries.
88+ */
89+ function resolveAckReaction ( channel , threadTs ) {
90+ const now = Date . now ( ) ;
91+ for ( const [ key , entry ] of pendingAckReactions ) {
92+ if ( now - entry . receivedAt > PENDING_ACK_TTL_MS ) {
93+ pendingAckReactions . delete ( key ) ;
94+ }
95+ }
96+
97+ const threadKey = `${ channel } :${ threadTs } ` ;
98+ const pending = pendingAckReactions . get ( threadKey ) ;
99+ if ( ! pending ) return ;
100+
101+ pendingAckReactions . delete ( threadKey ) ;
102+ app . client . reactions . add ( {
103+ token : process . env . SLACK_BOT_TOKEN ,
104+ channel : pending . channel ,
105+ timestamp : pending . messageTs ,
106+ name : "white_check_mark" ,
107+ } ) . catch ( ( err ) => {
108+ console . warn ( `β
check reaction failed: ${ err . message } ` ) ;
109+ } ) ;
110+ }
111+
77112/**
78113 * Evict the oldest entries when the registry exceeds MAX_THREADS.
79114 * Maps iterate in insertion order, so the first entries are the oldest.
@@ -259,6 +294,24 @@ async function handleMessage(userMessage, event, say) {
259294
260295 console . log ( `π¬ from <@${ event . user } >: ${ userMessage } ` ) ;
261296
297+ // React with π immediately so the user knows we saw their message.
298+ app . client . reactions . add ( {
299+ token : process . env . SLACK_BOT_TOKEN ,
300+ channel : event . channel ,
301+ timestamp : event . ts ,
302+ name : "eyes" ,
303+ } ) . catch ( ( err ) => {
304+ console . warn ( `π eyes reaction failed: ${ err . message } ` ) ;
305+ } ) ;
306+
307+ // Track this message so we can add β
when the agent replies.
308+ const threadKey = `${ event . channel } :${ event . thread_ts || event . ts } ` ;
309+ pendingAckReactions . set ( threadKey , {
310+ channel : event . channel ,
311+ messageTs : event . ts ,
312+ receivedAt : Date . now ( ) ,
313+ } ) ;
314+
262315 try {
263316 // Always re-resolve the socket before sending (handles agent restarts).
264317 // Capture into a local to avoid TOCTOU with concurrent handleMessage calls.
@@ -422,6 +475,12 @@ function startApiServer() {
422475 } ) ;
423476
424477 console . log ( `π€ Sent to ${ channel } : ${ text . slice ( 0 , 80 ) } ${ text . length > 80 ? "..." : "" } ` ) ;
478+
479+ // If this is a threaded reply, check for a pending β
ack reaction.
480+ if ( thread_ts ) {
481+ resolveAckReaction ( channel , thread_ts ) ;
482+ }
483+
425484 res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
426485 res . end ( JSON . stringify ( { ok : true , ts : result . ts , channel : result . channel } ) ) ;
427486
@@ -460,6 +519,10 @@ function startApiServer() {
460519 } ) ;
461520
462521 console . log ( `π€ Reply to ${ thread_id } (${ thread . channel } ): ${ text . slice ( 0 , 80 ) } ${ text . length > 80 ? "..." : "" } ` ) ;
522+
523+ // Check for a pending β
ack reaction on the /reply path too.
524+ resolveAckReaction ( thread . channel , thread . thread_ts ) ;
525+
463526 res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
464527 res . end ( JSON . stringify ( { ok : true , ts : result . ts , channel : result . channel } ) ) ;
465528
0 commit comments