@@ -12431,6 +12431,7 @@ import { spawn } from "child_process";
1243112431import { appendFileSync , readFileSync , writeFileSync , existsSync } from "fs" ;
1243212432import { dirname , join } from "path" ;
1243312433import { fileURLToPath } from "url" ;
12434+ import { homedir } from "os" ;
1243412435var LOG_FILE = "/tmp/slack-agent-plugin.log" ;
1243512436var SLACK_MSG_LIMIT = 3900 ;
1243612437var RESPONSE_STALL_MS = 45e3 ;
@@ -12441,6 +12442,7 @@ var BG_OUTPUT_CANDIDATE_PATHS = ["/background/output", "/background_output", "/a
1244112442var BG_TASK_ID_PATTERN = / ^ b g _ [ A - Z a - z 0 - 9 ] [ A - Z a - z 0 - 9 _ - ] * $ / ;
1244212443var AUTO_ATTACH_MAX_TASKS_PER_MESSAGE = 3 ;
1244312444var AUTO_ATTACH_MAX_MONITOR_BUDGET_MS = 12e4 ;
12445+ var INBOUND_EVENT_DEDUPE_TTL_MS = 10 * 60 * 1e3 ;
1244412446var initialized = false ;
1244512447var worker = null ;
1244612448var pluginClient = null ;
@@ -12453,8 +12455,14 @@ var allowedUsers = null;
1245312455var allowlistReady = true ;
1245412456var attachBgTimeoutMs = DEFAULT_ATTACH_TIMEOUT_SEC * 1e3 ;
1245512457var cachedBackgroundOutputPath = null ;
12458+ var seenInboundEventKeys = /* @__PURE__ */ new Map ( ) ;
1245612459var pendingPermissions = /* @__PURE__ */ new Map ( ) ;
1245712460var pendingQuestions = /* @__PURE__ */ new Map ( ) ;
12461+ function expandTilde ( p ) {
12462+ if ( p === "~" ) return homedir ( ) ;
12463+ if ( p . startsWith ( "~/" ) ) return join ( homedir ( ) , p . slice ( 2 ) ) ;
12464+ return p ;
12465+ }
1245812466function log ( m ) {
1245912467 try {
1246012468 appendFileSync ( LOG_FILE , `[${ ( /* @__PURE__ */ new Date ( ) ) . toISOString ( ) } ] plugin: ${ m }
@@ -12519,6 +12527,42 @@ function markdownToSlackMrkdwn(text) {
1251912527function sendIPC ( msg ) {
1252012528 if ( worker && worker . connected ) worker . send ( msg ) ;
1252112529}
12530+ function buildInboundEventKey ( msg ) {
12531+ const eventId = typeof msg ?. eventId === "string" ? msg . eventId : "" ;
12532+ if ( eventId ) return `event:${ eventId } ` ;
12533+ const channel = typeof msg ?. channel === "string" ? msg . channel : "" ;
12534+ const messageTs = typeof msg ?. messageTs === "string" ? msg . messageTs : "" ;
12535+ if ( channel && messageTs ) return `msg:${ channel } :${ messageTs } ` ;
12536+ return "" ;
12537+ }
12538+ function inboundMeta ( msg ) {
12539+ const eventId = typeof msg ?. eventId === "string" ? msg . eventId : "-" ;
12540+ const channel = typeof msg ?. channel === "string" ? msg . channel : "-" ;
12541+ const threadTs = typeof msg ?. threadTs === "string" ? msg . threadTs : "-" ;
12542+ const messageTs = typeof msg ?. messageTs === "string" ? msg . messageTs : "-" ;
12543+ const user = typeof msg ?. user === "string" ? msg . user : "-" ;
12544+ return `event_id=${ eventId } channel=${ channel } thread_ts=${ threadTs } message_ts=${ messageTs } user=${ user } ` ;
12545+ }
12546+ function shouldProcessInboundEvent ( msg ) {
12547+ const key = buildInboundEventKey ( msg ) ;
12548+ if ( ! key ) {
12549+ log ( `inbound key_missing ${ inboundMeta ( msg ) } ` ) ;
12550+ return true ;
12551+ }
12552+ if ( seenInboundEventKeys . has ( key ) ) {
12553+ log ( `inbound duplicate skipped key=${ key } ${ inboundMeta ( msg ) } ` ) ;
12554+ return false ;
12555+ }
12556+ seenInboundEventKeys . set ( key , Date . now ( ) + INBOUND_EVENT_DEDUPE_TTL_MS ) ;
12557+ log ( `inbound accepted key=${ key } ${ inboundMeta ( msg ) } ` ) ;
12558+ return true ;
12559+ }
12560+ setInterval ( ( ) => {
12561+ const now = Date . now ( ) ;
12562+ for ( const [ key , expiresAt ] of seenInboundEventKeys ) {
12563+ if ( expiresAt <= now ) seenInboundEventKeys . delete ( key ) ;
12564+ }
12565+ } , 6e4 ) ;
1252212566function slackSend ( channel , text , threadTs ) {
1252312567 sendIPC ( { type : "slack_send" , channel, text : markdownToSlackMrkdwn ( text ) , threadTs } ) ;
1252412568}
@@ -13287,13 +13331,14 @@ async function handleCommand(channel, text, ts) {
1328713331 slackSend ( channel , `*\uD604\uC7AC \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4:* \`${ currentDir } \`` , ts ) ;
1328813332 return true ;
1328913333 }
13334+ const resolvedDir = expandTilde ( arg ) ;
1329013335 if ( ! sessions [ ts ] ) {
13291- sessions [ ts ] = { sessionId : "" , channel, lastUsed : Date . now ( ) , directory : arg } ;
13336+ sessions [ ts ] = { sessionId : "" , channel, lastUsed : Date . now ( ) , directory : resolvedDir } ;
1329213337 } else {
13293- sessions [ ts ] . directory = arg ;
13338+ sessions [ ts ] . directory = resolvedDir ;
1329413339 }
1329513340 saveSessions ( ) ;
13296- slackSend ( channel , `\u2705 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uBCC0\uACBD: \`${ arg } \`
13341+ slackSend ( channel , `\u2705 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uBCC0\uACBD: \`${ resolvedDir } \`
1329713342\uB2E4\uC74C \uBA54\uC2DC\uC9C0\uBD80\uD130 \uC774 \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC138\uC158 \uC0DD\uC131.` , ts ) ;
1329813343 return true ;
1329913344 }
@@ -13404,22 +13449,20 @@ ${textParts}`, ts);
1340413449 }
1340513450 return false ;
1340613451}
13407- function startWorker ( env ) {
13408- const workerPath = join ( dirname ( fileURLToPath ( import . meta. url ) ) , "socket-worker.js" ) ;
13409- log ( `starting worker: ${ workerPath } ` ) ;
13410- worker = spawn ( "node" , [ workerPath ] , {
13411- env : { ...process . env , ...env } ,
13412- stdio : [ "ignore" , "ignore" , "ignore" , "ipc" ] ,
13413- detached : false
13414- } ) ;
13415- worker . on ( "message" , ( msg ) => {
13452+ function attachWorkerHandlers ( w ) {
13453+ w . on ( "message" , ( msg ) => {
1341613454 if ( msg ?. type === "slack_event" ) {
13455+ log ( `inbound received ${ inboundMeta ( msg ) } ` ) ;
13456+ if ( ! shouldProcessInboundEvent ( msg ) ) {
13457+ return ;
13458+ }
1341713459 const isAllowed = ! allowedUsers || ! allowlistReady || allowedUsers . has ( msg . user ) ;
1341813460 const isThreadReply = msg . threadTs !== msg . messageTs ;
1341913461 if ( ! isAllowed && ! isThreadReply ) {
13420- log ( `blocked user: ${ msg . user } (new thread) ` ) ;
13462+ log ( `inbound blocked_user ${ inboundMeta ( msg ) } reason=new_thread_not_allowlisted ` ) ;
1342113463 return ;
1342213464 }
13465+ log ( `inbound dispatch_handleMessage ${ inboundMeta ( msg ) } ` ) ;
1342313466 handleMessage ( msg . channel , msg . text , msg . threadTs , msg . messageTs , isAllowed ) ;
1342413467 } else if ( msg ?. type === "resolved_emails" ) {
1342513468 if ( allowedUsers && msg . users ) {
@@ -13429,18 +13472,61 @@ function startWorker(env) {
1342913472 log ( `resolved email users: ${ msg . users . join ( ", " ) } ` ) ;
1343013473 }
1343113474 allowlistReady = true ;
13475+ } else if ( msg ?. type === "worker_connected" ) {
1343213476 }
1343313477 } ) ;
13434- worker . on ( "exit" , ( code ) => {
13478+ }
13479+ var GRACEFUL_RESTART_TIMEOUT_MS = 3e4 ;
13480+ var workerEnvCache = null ;
13481+ function startWorker ( env , dyingWorker ) {
13482+ workerEnvCache = env ;
13483+ const workerPath = join ( dirname ( fileURLToPath ( import . meta. url ) ) , "socket-worker.js" ) ;
13484+ log ( `starting worker: ${ workerPath } ` ) ;
13485+ const newWorker = spawn ( "node" , [ workerPath ] , {
13486+ env : { ...process . env , ...env } ,
13487+ stdio : [ "ignore" , "ignore" , "ignore" , "ipc" ] ,
13488+ detached : false
13489+ } ) ;
13490+ attachWorkerHandlers ( newWorker ) ;
13491+ const oldWorker = dyingWorker ?? null ;
13492+ let swapped = false ;
13493+ const doSwap = ( reason ) => {
13494+ if ( swapped ) return ;
13495+ swapped = true ;
13496+ worker = newWorker ;
13497+ if ( oldWorker && ! oldWorker . killed ) {
13498+ log ( `graceful swap: ${ reason } \u2014 terminating old worker` ) ;
13499+ oldWorker . removeAllListeners ( ) ;
13500+ oldWorker . kill ( "SIGTERM" ) ;
13501+ } else {
13502+ log ( `worker ready: ${ reason } (no old worker to terminate)` ) ;
13503+ }
13504+ } ;
13505+ const onNewWorkerMessage = ( msg ) => {
13506+ if ( msg ?. type === "worker_connected" ) {
13507+ doSwap ( "new worker connected" ) ;
13508+ }
13509+ } ;
13510+ newWorker . on ( "message" , onNewWorkerMessage ) ;
13511+ const swapTimer = setTimeout ( ( ) => {
13512+ doSwap ( "timeout \u2014 forcing swap" ) ;
13513+ } , GRACEFUL_RESTART_TIMEOUT_MS ) ;
13514+ newWorker . on ( "exit" , ( code ) => {
13515+ clearTimeout ( swapTimer ) ;
1343513516 log ( `worker exited: code=${ code } ` ) ;
13436- worker = null ;
13437- if ( initialized ) {
13517+ if ( worker === newWorker ) {
13518+ worker = null ;
13519+ }
13520+ if ( initialized && workerEnvCache ) {
1343813521 setTimeout ( ( ) => {
1343913522 log ( "restarting worker..." ) ;
13440- startWorker ( env ) ;
13523+ startWorker ( workerEnvCache , newWorker ) ;
1344113524 } , 5e3 ) ;
1344213525 }
1344313526 } ) ;
13527+ if ( ! oldWorker ) {
13528+ doSwap ( "initial start" ) ;
13529+ }
1344413530 log ( "worker spawned" ) ;
1344513531}
1344613532function stopWorker ( ) {
@@ -13479,7 +13565,7 @@ var pluginModule = {
1347913565 return { tool : { slack_status : slackStatusTool } } ;
1348013566 }
1348113567 pluginClient = input . client ;
13482- defaultDirectory = options ?. DEFAULT_DIRECTORY || process . env . SLACK_DEFAULT_DIRECTORY || input . directory ;
13568+ defaultDirectory = expandTilde ( options ?. DEFAULT_DIRECTORY || process . env . SLACK_DEFAULT_DIRECTORY || input . directory ) ;
1348313569 attachBgTimeoutMs = resolveAttachTimeoutMs ( options ?. ATTACH_TIMEOUT_SEC ) ;
1348413570 log ( `attach timeout set to ${ attachBgTimeoutMs } ms` ) ;
1348513571 sessionsPath = join ( input . directory , "slack-sessions.json" ) ;
0 commit comments