@@ -68,6 +68,31 @@ console.log(`🔒 Access control: ${ALLOWED_USERS.length} allowed user(s)`);
6868const slackRateLimiter = createRateLimiter ( { maxRequests : 5 , windowMs : 60_000 } ) ;
6969const apiRateLimiter = createRateLimiter ( { maxRequests : 30 , windowMs : 60_000 } ) ;
7070
71+ // ── Thread Registry ─────────────────────────────────────────────────────────
72+ // Maps friendly thread IDs (e.g. "thread-1") to { channel, thread_ts }.
73+ // Deterministic: same channel+thread_ts always maps to the same ID.
74+
75+ const threadRegistry = new Map ( ) ; // thread-N → { channel, thread_ts }
76+ const threadLookup = new Map ( ) ; // "channel:thread_ts" → thread-N
77+ let threadCounter = 0 ;
78+
79+ /**
80+ * Get or create a friendly thread ID for a channel + thread_ts pair.
81+ * Returns the thread ID string (e.g. "thread-3").
82+ */
83+ function getThreadId ( channel , threadTs ) {
84+ const key = `${ channel } :${ threadTs } ` ;
85+ let id = threadLookup . get ( key ) ;
86+ if ( ! id ) {
87+ threadCounter ++ ;
88+ id = `thread-${ threadCounter } ` ;
89+ threadRegistry . set ( id , { channel, thread_ts : threadTs } ) ;
90+ threadLookup . set ( key , id ) ;
91+ console . log ( `🧵 Registered ${ id } → channel=${ channel } thread_ts=${ threadTs } ` ) ;
92+ }
93+ return id ;
94+ }
95+
7196// ── Session Socket ──────────────────────────────────────────────────────────
7297
7398function findSessionSocket ( targetId ) {
@@ -248,14 +273,18 @@ async function handleMessage(userMessage, event, say) {
248273 }
249274
250275 // Wrap the message with security boundaries before sending to agent
251- const contextMessage = wrapExternalContent ( {
276+ const wrappedMessage = wrapExternalContent ( {
252277 text : userMessage ,
253278 source : "Slack" ,
254279 user : event . user ,
255280 channel : event . channel ,
256281 threadTs : event . ts ,
257282 } ) ;
258283
284+ // Enrich with friendly thread ID so the agent can use /reply endpoint
285+ const threadId = getThreadId ( event . channel , event . ts ) ;
286+ const contextMessage = `${ wrappedMessage } \nThread-ID: ${ threadId } ` ;
287+
259288 const reply = await enqueue ( ( ) => sendToAgent ( currentSocket , contextMessage ) ) ;
260289 const formatted = formatForSlack ( reply ) ;
261290 await say ( { text : formatted , thread_ts : event . ts } ) ;
@@ -305,13 +334,18 @@ app.event("message", async ({ event, say }) => {
305334 const text = event . text ?. trim ( ) ;
306335 if ( ! text ) return ;
307336 // Don't filter bot_id here — Sentry posts as a bot
308- const contextMessage = wrapExternalContent ( {
337+ const wrappedSentryMessage = wrapExternalContent ( {
309338 text,
310339 source : "Slack (#bots-sentry)" ,
311340 user : event . user || event . bot_id || "sentry-bot" ,
312341 channel : event . channel ,
313342 threadTs : event . ts ,
314343 } ) ;
344+
345+ // Enrich with friendly thread ID
346+ const sentryThreadId = getThreadId ( event . channel , event . ts ) ;
347+ const contextMessage = `${ wrappedSentryMessage } \nThread-ID: ${ sentryThreadId } ` ;
348+
315349 try {
316350 // Re-resolve socket before sending (capture local to avoid TOCTOU)
317351 refreshSocket ( ) ;
@@ -344,6 +378,9 @@ app.event("message", async ({ event, say }) => {
344378// POST http://localhost:7890/send
345379// { "channel": "C07...", "text": "hello", "thread_ts": "1234.5678" }
346380//
381+ // POST http://localhost:7890/reply
382+ // { "thread_id": "thread-1", "text": "hello" }
383+ //
347384// POST http://localhost:7890/react
348385// { "channel": "C07...", "timestamp": "1234.5678", "emoji": "white_check_mark" }
349386
@@ -408,6 +445,44 @@ function startApiServer() {
408445 res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
409446 res . end ( JSON . stringify ( { ok : true , ts : result . ts , channel : result . channel } ) ) ;
410447
448+ } else if ( pathname === "/reply" ) {
449+ // Look up thread by friendly ID and post a reply
450+ const { thread_id, text } = params ;
451+
452+ if ( typeof thread_id !== "string" || ! thread_id ) {
453+ res . writeHead ( 400 , { "Content-Type" : "application/json" } ) ;
454+ res . end ( JSON . stringify ( { error : "thread_id must be a non-empty string" } ) ) ;
455+ return ;
456+ }
457+ if ( typeof text !== "string" || text . length === 0 ) {
458+ res . writeHead ( 400 , { "Content-Type" : "application/json" } ) ;
459+ res . end ( JSON . stringify ( { error : "text must be a non-empty string" } ) ) ;
460+ return ;
461+ }
462+ if ( text . length > 4000 ) {
463+ res . writeHead ( 400 , { "Content-Type" : "application/json" } ) ;
464+ res . end ( JSON . stringify ( { error : "text too long (max 4000)" } ) ) ;
465+ return ;
466+ }
467+
468+ const thread = threadRegistry . get ( thread_id ) ;
469+ if ( ! thread ) {
470+ res . writeHead ( 404 , { "Content-Type" : "application/json" } ) ;
471+ res . end ( JSON . stringify ( { error : `Unknown thread_id: ${ thread_id } ` } ) ) ;
472+ return ;
473+ }
474+
475+ const result = await app . client . chat . postMessage ( {
476+ token : process . env . SLACK_BOT_TOKEN ,
477+ channel : thread . channel ,
478+ text,
479+ thread_ts : thread . thread_ts ,
480+ } ) ;
481+
482+ console . log ( `📤 Reply to ${ thread_id } (${ thread . channel } ): ${ text . slice ( 0 , 80 ) } ${ text . length > 80 ? "..." : "" } ` ) ;
483+ res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
484+ res . end ( JSON . stringify ( { ok : true , ts : result . ts , channel : result . channel } ) ) ;
485+
411486 } else if ( pathname === "/react" ) {
412487 const validationError = validateReactParams ( params ) ;
413488 if ( validationError ) {
@@ -429,7 +504,7 @@ function startApiServer() {
429504
430505 } else {
431506 res . writeHead ( 404 , { "Content-Type" : "application/json" } ) ;
432- res . end ( JSON . stringify ( { error : "Not found. Endpoints: POST /send, POST /react" } ) ) ;
507+ res . end ( JSON . stringify ( { error : "Not found. Endpoints: POST /send, POST /reply, POST / react" } ) ) ;
433508 }
434509 } catch ( err ) {
435510 console . error ( "API error:" , err . message ) ;
@@ -441,6 +516,7 @@ function startApiServer() {
441516 server . listen ( API_PORT , "127.0.0.1" , ( ) => {
442517 console . log ( `📡 Outbound API listening on http://127.0.0.1:${ API_PORT } ` ) ;
443518 console . log ( ` POST /send {"channel":"C...","text":"...","thread_ts":"..."}` ) ;
519+ console . log ( ` POST /reply {"thread_id":"thread-1","text":"..."}` ) ;
444520 console . log ( ` POST /react {"channel":"C...","timestamp":"...","emoji":"..."}` ) ;
445521 } ) ;
446522}
0 commit comments