77 "os/signal"
88 "strings"
99 "sync"
10+ "sync/atomic"
1011 "syscall"
1112 "time"
1213
@@ -105,12 +106,32 @@ func telegramCmd(args []string) error {
105106 return "" , nil
106107 }
107108
109+ // restartRequested is set atomically when a /restart command is received.
110+ // Checked after the update loop exits to decide between restart and exit.
111+ var restartRequested atomic.Bool
112+
108113 handler .OnCommand = func (chatID int64 , cmdName string , argsStr string ) (string , error ) {
109114 cmd := telegram .FindCommand (cmdName )
110115 if cmd == nil {
111116 return fmt .Sprintf ("Unknown command: /%s" , cmdName ), nil
112117 }
113118
119+ // Handle /restart — send confirmation directly, then trigger SIGHUP.
120+ if cmdName == "restart" {
121+ // Send the restart message directly via the bot to ensure it's
122+ // delivered before the process re-execs.
123+ if _ , err := bot .SendMessage (chatID ,
124+ "🔄 *Restarting...*\n \n The bot will restart momentarily. This may take a few seconds." ,
125+ nil ); err != nil {
126+ handlerLog .Error ("send restart message failed" , "chat_id" , chatID , "error" , err )
127+ }
128+ // Signal SIGHUP to self — the signal handler will cancel the
129+ // context, stopping the poller, and the main loop will re-exec.
130+ restartRequested .Store (true )
131+ syscall .Kill (os .Getpid (), syscall .SIGHUP )
132+ return "" , nil
133+ }
134+
114135 // Handle /new — clear session and reset trust in the approver.
115136 if cmdName == "new" {
116137 sessionManager .Delete (chatID )
@@ -119,6 +140,15 @@ func telegramCmd(args []string) error {
119140 }
120141 }
121142
143+ // Handle /stats — read from session store.
144+ if cmdName == "stats" {
145+ cs , err := sessionManager .Load (chatID )
146+ if err != nil || cs == nil {
147+ return "📊 *Session Stats*\n \n No active session yet. Send a message to start one." , nil
148+ }
149+ return formatStats (cs ), nil
150+ }
151+
122152 return cmd .Handler (argsStr )
123153 }
124154
@@ -160,12 +190,17 @@ func telegramCmd(args []string) error {
160190 ctx , cancel := context .WithCancel (context .Background ())
161191 defer cancel ()
162192
163- // 15. Handle SIGINT/SIGTERM for graceful shutdown.
193+ // 15. Handle SIGINT/SIGTERM/SIGHUP for graceful shutdown and restart.
194+ // SIGHUP triggers a full process restart (used by /restart command).
164195 sigCh := make (chan os.Signal , 1 )
165- signal .Notify (sigCh , syscall .SIGINT , syscall .SIGTERM )
196+ signal .Notify (sigCh , syscall .SIGINT , syscall .SIGTERM , syscall . SIGHUP )
166197 go func () {
167- <- sigCh
168- fmt .Fprintf (os .Stderr , "\n odek telegram: shutting down...\n " )
198+ sig := <- sigCh
199+ if sig == syscall .SIGHUP {
200+ fmt .Fprintf (os .Stderr , "\n odek telegram: restart requested...\n " )
201+ } else {
202+ fmt .Fprintf (os .Stderr , "\n odek telegram: shutting down...\n " )
203+ }
169204 cancel ()
170205 }()
171206
@@ -178,6 +213,17 @@ func telegramCmd(args []string) error {
178213 handler .HandleUpdate (upd )
179214 }
180215
216+ // 18. If restart was requested (via /restart command), re-exec the binary.
217+ // This preserves the exact same arguments so the bot comes back with
218+ // the same configuration. If syscall.Exec fails, fall through to exit.
219+ if restartRequested .Load () {
220+ fmt .Fprintf (os .Stderr , "odek telegram: re-executing %s %v...\n " , os .Args [0 ], os .Args [1 :])
221+ if err := syscall .Exec (os .Args [0 ], os .Args , os .Environ ()); err != nil {
222+ fmt .Fprintf (os .Stderr , "odek telegram: restart failed: %v\n " , err )
223+ return err
224+ }
225+ }
226+
181227 return nil
182228}
183229
@@ -277,6 +323,25 @@ func handleChatMessage(
277323 }
278324}
279325
326+ // formatStats formats session statistics for the Telegram stats command.
327+ func formatStats (cs * telegram.ChatSession ) string {
328+ duration := time .Since (cs .CreatedAt ).Truncate (time .Second )
329+
330+ return fmt .Sprintf (
331+ "📊 *Session Stats*\n \n " +
332+ "Messages: %d\n " +
333+ "Turns: %d\n " +
334+ "Started: %s\n " +
335+ "Duration: %s\n " +
336+ "Last active: %s" ,
337+ len (cs .Messages ),
338+ cs .TurnCount ,
339+ cs .CreatedAt .Format ("Jan 02, 2006 15:04 UTC" ),
340+ duration .String (),
341+ cs .LastActive .Format ("15:04 UTC" ),
342+ )
343+ }
344+
280345// reportError sends an error message to the given chat and logs to stderr.
281346func reportError (bot * telegram.Bot , chatID int64 , msg string ) {
282347 fmt .Fprintf (os .Stderr , "odek telegram: %s\n " , msg )
0 commit comments