ScuttleBot
fix: codex and gemini relays restart input loops after SIGUSR1 Both relays had the old handleReconnectSignal that swapped the connector but didn't restart the input loop. After reconnection, messages from IRC were never delivered to the agent terminal. Now matches claude-relay: passes state + ptmx to the handler, restarts relayInputLoop (and mirrorSessionLoop for codex) after successful reconnect.
Commit
a4dd20bbfb79fedf6897f950d1d35d64a045b978e2afd5bef61c5966ba646a08
Parent
606670ed8028ba3…
2 files changed
+10
-3
+8
-3
+10
-3
| --- cmd/codex-relay/main.go | ||
| +++ cmd/codex-relay/main.go | ||
| @@ -217,11 +217,10 @@ | ||
| 217 | 217 | "SCUTTLEBOT_ACTIVITY_VIA_BROKER="+boolString(relayActive), |
| 218 | 218 | ) |
| 219 | 219 | if relayActive { |
| 220 | 220 | go mirrorSessionLoop(ctx, relay, cfg, startedAt) |
| 221 | 221 | go presenceLoopPtr(ctx, &relay, cfg.HeartbeatInterval) |
| 222 | - go handleReconnectSignal(ctx, &relay, cfg) | |
| 223 | 222 | } |
| 224 | 223 | |
| 225 | 224 | if !isInteractiveTTY() { |
| 226 | 225 | cmd.Stdin = os.Stdin |
| 227 | 226 | cmd.Stdout = os.Stdout |
| @@ -272,10 +271,11 @@ | ||
| 272 | 271 | go func() { |
| 273 | 272 | copyPTYOutput(ptmx, os.Stdout, state) |
| 274 | 273 | }() |
| 275 | 274 | if relayActive { |
| 276 | 275 | go relayInputLoop(ctx, relay, cfg, state, ptmx, onlineAt) |
| 276 | + go handleReconnectSignal(ctx, &relay, cfg, state, ptmx, startedAt) | |
| 277 | 277 | } |
| 278 | 278 | |
| 279 | 279 | err = cmd.Wait() |
| 280 | 280 | cancel() |
| 281 | 281 | |
| @@ -330,11 +330,11 @@ | ||
| 330 | 330 | } |
| 331 | 331 | } |
| 332 | 332 | } |
| 333 | 333 | } |
| 334 | 334 | |
| 335 | -func handleReconnectSignal(ctx context.Context, relayPtr *sessionrelay.Connector, cfg config) { | |
| 335 | +func handleReconnectSignal(ctx context.Context, relayPtr *sessionrelay.Connector, cfg config, state *relayState, ptmx *os.File, startedAt time.Time) { | |
| 336 | 336 | sigCh := make(chan os.Signal, 1) |
| 337 | 337 | signal.Notify(sigCh, syscall.SIGUSR1) |
| 338 | 338 | defer signal.Stop(sigCh) |
| 339 | 339 | |
| 340 | 340 | for { |
| @@ -385,15 +385,22 @@ | ||
| 385 | 385 | continue |
| 386 | 386 | } |
| 387 | 387 | cancel() |
| 388 | 388 | |
| 389 | 389 | *relayPtr = conn |
| 390 | + now := time.Now() | |
| 390 | 391 | _ = conn.Post(context.Background(), fmt.Sprintf( |
| 391 | 392 | "reconnected in %s; mention %s to interrupt", |
| 392 | 393 | filepath.Base(cfg.TargetCWD), cfg.Nick, |
| 393 | 394 | )) |
| 394 | - fmt.Fprintf(os.Stderr, "codex-relay: reconnected successfully\n") | |
| 395 | + fmt.Fprintf(os.Stderr, "codex-relay: reconnected, restarting mirror and input loops\n") | |
| 396 | + | |
| 397 | + // Restart mirror and input loops with the new connector. | |
| 398 | + // Use epoch time for mirror so it finds the existing session file | |
| 399 | + // regardless of when it was last modified. | |
| 400 | + go mirrorSessionLoop(ctx, conn, cfg, time.Time{}) | |
| 401 | + go relayInputLoop(ctx, conn, cfg, state, ptmx, now) | |
| 395 | 402 | break |
| 396 | 403 | } |
| 397 | 404 | } |
| 398 | 405 | } |
| 399 | 406 | |
| 400 | 407 |
| --- cmd/codex-relay/main.go | |
| +++ cmd/codex-relay/main.go | |
| @@ -217,11 +217,10 @@ | |
| 217 | "SCUTTLEBOT_ACTIVITY_VIA_BROKER="+boolString(relayActive), |
| 218 | ) |
| 219 | if relayActive { |
| 220 | go mirrorSessionLoop(ctx, relay, cfg, startedAt) |
| 221 | go presenceLoopPtr(ctx, &relay, cfg.HeartbeatInterval) |
| 222 | go handleReconnectSignal(ctx, &relay, cfg) |
| 223 | } |
| 224 | |
| 225 | if !isInteractiveTTY() { |
| 226 | cmd.Stdin = os.Stdin |
| 227 | cmd.Stdout = os.Stdout |
| @@ -272,10 +271,11 @@ | |
| 272 | go func() { |
| 273 | copyPTYOutput(ptmx, os.Stdout, state) |
| 274 | }() |
| 275 | if relayActive { |
| 276 | go relayInputLoop(ctx, relay, cfg, state, ptmx, onlineAt) |
| 277 | } |
| 278 | |
| 279 | err = cmd.Wait() |
| 280 | cancel() |
| 281 | |
| @@ -330,11 +330,11 @@ | |
| 330 | } |
| 331 | } |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | func handleReconnectSignal(ctx context.Context, relayPtr *sessionrelay.Connector, cfg config) { |
| 336 | sigCh := make(chan os.Signal, 1) |
| 337 | signal.Notify(sigCh, syscall.SIGUSR1) |
| 338 | defer signal.Stop(sigCh) |
| 339 | |
| 340 | for { |
| @@ -385,15 +385,22 @@ | |
| 385 | continue |
| 386 | } |
| 387 | cancel() |
| 388 | |
| 389 | *relayPtr = conn |
| 390 | _ = conn.Post(context.Background(), fmt.Sprintf( |
| 391 | "reconnected in %s; mention %s to interrupt", |
| 392 | filepath.Base(cfg.TargetCWD), cfg.Nick, |
| 393 | )) |
| 394 | fmt.Fprintf(os.Stderr, "codex-relay: reconnected successfully\n") |
| 395 | break |
| 396 | } |
| 397 | } |
| 398 | } |
| 399 | |
| 400 |
| --- cmd/codex-relay/main.go | |
| +++ cmd/codex-relay/main.go | |
| @@ -217,11 +217,10 @@ | |
| 217 | "SCUTTLEBOT_ACTIVITY_VIA_BROKER="+boolString(relayActive), |
| 218 | ) |
| 219 | if relayActive { |
| 220 | go mirrorSessionLoop(ctx, relay, cfg, startedAt) |
| 221 | go presenceLoopPtr(ctx, &relay, cfg.HeartbeatInterval) |
| 222 | } |
| 223 | |
| 224 | if !isInteractiveTTY() { |
| 225 | cmd.Stdin = os.Stdin |
| 226 | cmd.Stdout = os.Stdout |
| @@ -272,10 +271,11 @@ | |
| 271 | go func() { |
| 272 | copyPTYOutput(ptmx, os.Stdout, state) |
| 273 | }() |
| 274 | if relayActive { |
| 275 | go relayInputLoop(ctx, relay, cfg, state, ptmx, onlineAt) |
| 276 | go handleReconnectSignal(ctx, &relay, cfg, state, ptmx, startedAt) |
| 277 | } |
| 278 | |
| 279 | err = cmd.Wait() |
| 280 | cancel() |
| 281 | |
| @@ -330,11 +330,11 @@ | |
| 330 | } |
| 331 | } |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | func handleReconnectSignal(ctx context.Context, relayPtr *sessionrelay.Connector, cfg config, state *relayState, ptmx *os.File, startedAt time.Time) { |
| 336 | sigCh := make(chan os.Signal, 1) |
| 337 | signal.Notify(sigCh, syscall.SIGUSR1) |
| 338 | defer signal.Stop(sigCh) |
| 339 | |
| 340 | for { |
| @@ -385,15 +385,22 @@ | |
| 385 | continue |
| 386 | } |
| 387 | cancel() |
| 388 | |
| 389 | *relayPtr = conn |
| 390 | now := time.Now() |
| 391 | _ = conn.Post(context.Background(), fmt.Sprintf( |
| 392 | "reconnected in %s; mention %s to interrupt", |
| 393 | filepath.Base(cfg.TargetCWD), cfg.Nick, |
| 394 | )) |
| 395 | fmt.Fprintf(os.Stderr, "codex-relay: reconnected, restarting mirror and input loops\n") |
| 396 | |
| 397 | // Restart mirror and input loops with the new connector. |
| 398 | // Use epoch time for mirror so it finds the existing session file |
| 399 | // regardless of when it was last modified. |
| 400 | go mirrorSessionLoop(ctx, conn, cfg, time.Time{}) |
| 401 | go relayInputLoop(ctx, conn, cfg, state, ptmx, now) |
| 402 | break |
| 403 | } |
| 404 | } |
| 405 | } |
| 406 | |
| 407 |
+8
-3
| --- cmd/gemini-relay/main.go | ||
| +++ cmd/gemini-relay/main.go | ||
| @@ -152,10 +152,11 @@ | ||
| 152 | 152 | _ = relay.Close(closeCtx) |
| 153 | 153 | }() |
| 154 | 154 | } |
| 155 | 155 | |
| 156 | 156 | cmd := exec.Command(cfg.GeminiBin, cfg.Args...) |
| 157 | + startedAt := time.Now() | |
| 157 | 158 | cmd.Env = append(os.Environ(), |
| 158 | 159 | "SCUTTLEBOT_CONFIG_FILE="+cfg.ConfigFile, |
| 159 | 160 | "SCUTTLEBOT_URL="+cfg.URL, |
| 160 | 161 | "SCUTTLEBOT_TOKEN="+cfg.Token, |
| 161 | 162 | "SCUTTLEBOT_CHANNEL="+cfg.Channel, |
| @@ -165,11 +166,10 @@ | ||
| 165 | 166 | "SCUTTLEBOT_SESSION_ID="+cfg.SessionID, |
| 166 | 167 | "SCUTTLEBOT_NICK="+cfg.Nick, |
| 167 | 168 | ) |
| 168 | 169 | if relayActive { |
| 169 | 170 | go presenceLoopPtr(ctx, &relay, cfg.HeartbeatInterval) |
| 170 | - go handleReconnectSignal(ctx, &relay, cfg) | |
| 171 | 171 | } |
| 172 | 172 | |
| 173 | 173 | if !isInteractiveTTY() { |
| 174 | 174 | cmd.Stdin = os.Stdin |
| 175 | 175 | cmd.Stdout = os.Stdout |
| @@ -220,10 +220,11 @@ | ||
| 220 | 220 | go func() { |
| 221 | 221 | copyPTYOutput(ptmx, os.Stdout, state) |
| 222 | 222 | }() |
| 223 | 223 | if relayActive { |
| 224 | 224 | go relayInputLoop(ctx, relay, cfg, state, ptmx, onlineAt) |
| 225 | + go handleReconnectSignal(ctx, &relay, cfg, state, ptmx, startedAt) | |
| 225 | 226 | } |
| 226 | 227 | |
| 227 | 228 | err = cmd.Wait() |
| 228 | 229 | cancel() |
| 229 | 230 | |
| @@ -278,11 +279,11 @@ | ||
| 278 | 279 | } |
| 279 | 280 | } |
| 280 | 281 | } |
| 281 | 282 | } |
| 282 | 283 | |
| 283 | -func handleReconnectSignal(ctx context.Context, relayPtr *sessionrelay.Connector, cfg config) { | |
| 284 | +func handleReconnectSignal(ctx context.Context, relayPtr *sessionrelay.Connector, cfg config, state *relayState, ptmx *os.File, startedAt time.Time) { | |
| 284 | 285 | sigCh := make(chan os.Signal, 1) |
| 285 | 286 | signal.Notify(sigCh, syscall.SIGUSR1) |
| 286 | 287 | defer signal.Stop(sigCh) |
| 287 | 288 | |
| 288 | 289 | for { |
| @@ -333,15 +334,19 @@ | ||
| 333 | 334 | continue |
| 334 | 335 | } |
| 335 | 336 | cancel() |
| 336 | 337 | |
| 337 | 338 | *relayPtr = conn |
| 339 | + now := time.Now() | |
| 338 | 340 | _ = conn.Post(context.Background(), fmt.Sprintf( |
| 339 | 341 | "reconnected in %s; mention %s to interrupt", |
| 340 | 342 | filepath.Base(cfg.TargetCWD), cfg.Nick, |
| 341 | 343 | )) |
| 342 | - fmt.Fprintf(os.Stderr, "gemini-relay: reconnected successfully\n") | |
| 344 | + fmt.Fprintf(os.Stderr, "gemini-relay: reconnected, restarting input loop\n") | |
| 345 | + | |
| 346 | + // Restart input loop with the new connector. | |
| 347 | + go relayInputLoop(ctx, conn, cfg, state, ptmx, now) | |
| 343 | 348 | break |
| 344 | 349 | } |
| 345 | 350 | } |
| 346 | 351 | } |
| 347 | 352 | |
| 348 | 353 |
| --- cmd/gemini-relay/main.go | |
| +++ cmd/gemini-relay/main.go | |
| @@ -152,10 +152,11 @@ | |
| 152 | _ = relay.Close(closeCtx) |
| 153 | }() |
| 154 | } |
| 155 | |
| 156 | cmd := exec.Command(cfg.GeminiBin, cfg.Args...) |
| 157 | cmd.Env = append(os.Environ(), |
| 158 | "SCUTTLEBOT_CONFIG_FILE="+cfg.ConfigFile, |
| 159 | "SCUTTLEBOT_URL="+cfg.URL, |
| 160 | "SCUTTLEBOT_TOKEN="+cfg.Token, |
| 161 | "SCUTTLEBOT_CHANNEL="+cfg.Channel, |
| @@ -165,11 +166,10 @@ | |
| 165 | "SCUTTLEBOT_SESSION_ID="+cfg.SessionID, |
| 166 | "SCUTTLEBOT_NICK="+cfg.Nick, |
| 167 | ) |
| 168 | if relayActive { |
| 169 | go presenceLoopPtr(ctx, &relay, cfg.HeartbeatInterval) |
| 170 | go handleReconnectSignal(ctx, &relay, cfg) |
| 171 | } |
| 172 | |
| 173 | if !isInteractiveTTY() { |
| 174 | cmd.Stdin = os.Stdin |
| 175 | cmd.Stdout = os.Stdout |
| @@ -220,10 +220,11 @@ | |
| 220 | go func() { |
| 221 | copyPTYOutput(ptmx, os.Stdout, state) |
| 222 | }() |
| 223 | if relayActive { |
| 224 | go relayInputLoop(ctx, relay, cfg, state, ptmx, onlineAt) |
| 225 | } |
| 226 | |
| 227 | err = cmd.Wait() |
| 228 | cancel() |
| 229 | |
| @@ -278,11 +279,11 @@ | |
| 278 | } |
| 279 | } |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | func handleReconnectSignal(ctx context.Context, relayPtr *sessionrelay.Connector, cfg config) { |
| 284 | sigCh := make(chan os.Signal, 1) |
| 285 | signal.Notify(sigCh, syscall.SIGUSR1) |
| 286 | defer signal.Stop(sigCh) |
| 287 | |
| 288 | for { |
| @@ -333,15 +334,19 @@ | |
| 333 | continue |
| 334 | } |
| 335 | cancel() |
| 336 | |
| 337 | *relayPtr = conn |
| 338 | _ = conn.Post(context.Background(), fmt.Sprintf( |
| 339 | "reconnected in %s; mention %s to interrupt", |
| 340 | filepath.Base(cfg.TargetCWD), cfg.Nick, |
| 341 | )) |
| 342 | fmt.Fprintf(os.Stderr, "gemini-relay: reconnected successfully\n") |
| 343 | break |
| 344 | } |
| 345 | } |
| 346 | } |
| 347 | |
| 348 |
| --- cmd/gemini-relay/main.go | |
| +++ cmd/gemini-relay/main.go | |
| @@ -152,10 +152,11 @@ | |
| 152 | _ = relay.Close(closeCtx) |
| 153 | }() |
| 154 | } |
| 155 | |
| 156 | cmd := exec.Command(cfg.GeminiBin, cfg.Args...) |
| 157 | startedAt := time.Now() |
| 158 | cmd.Env = append(os.Environ(), |
| 159 | "SCUTTLEBOT_CONFIG_FILE="+cfg.ConfigFile, |
| 160 | "SCUTTLEBOT_URL="+cfg.URL, |
| 161 | "SCUTTLEBOT_TOKEN="+cfg.Token, |
| 162 | "SCUTTLEBOT_CHANNEL="+cfg.Channel, |
| @@ -165,11 +166,10 @@ | |
| 166 | "SCUTTLEBOT_SESSION_ID="+cfg.SessionID, |
| 167 | "SCUTTLEBOT_NICK="+cfg.Nick, |
| 168 | ) |
| 169 | if relayActive { |
| 170 | go presenceLoopPtr(ctx, &relay, cfg.HeartbeatInterval) |
| 171 | } |
| 172 | |
| 173 | if !isInteractiveTTY() { |
| 174 | cmd.Stdin = os.Stdin |
| 175 | cmd.Stdout = os.Stdout |
| @@ -220,10 +220,11 @@ | |
| 220 | go func() { |
| 221 | copyPTYOutput(ptmx, os.Stdout, state) |
| 222 | }() |
| 223 | if relayActive { |
| 224 | go relayInputLoop(ctx, relay, cfg, state, ptmx, onlineAt) |
| 225 | go handleReconnectSignal(ctx, &relay, cfg, state, ptmx, startedAt) |
| 226 | } |
| 227 | |
| 228 | err = cmd.Wait() |
| 229 | cancel() |
| 230 | |
| @@ -278,11 +279,11 @@ | |
| 279 | } |
| 280 | } |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | func handleReconnectSignal(ctx context.Context, relayPtr *sessionrelay.Connector, cfg config, state *relayState, ptmx *os.File, startedAt time.Time) { |
| 285 | sigCh := make(chan os.Signal, 1) |
| 286 | signal.Notify(sigCh, syscall.SIGUSR1) |
| 287 | defer signal.Stop(sigCh) |
| 288 | |
| 289 | for { |
| @@ -333,15 +334,19 @@ | |
| 334 | continue |
| 335 | } |
| 336 | cancel() |
| 337 | |
| 338 | *relayPtr = conn |
| 339 | now := time.Now() |
| 340 | _ = conn.Post(context.Background(), fmt.Sprintf( |
| 341 | "reconnected in %s; mention %s to interrupt", |
| 342 | filepath.Base(cfg.TargetCWD), cfg.Nick, |
| 343 | )) |
| 344 | fmt.Fprintf(os.Stderr, "gemini-relay: reconnected, restarting input loop\n") |
| 345 | |
| 346 | // Restart input loop with the new connector. |
| 347 | go relayInputLoop(ctx, conn, cfg, state, ptmx, now) |
| 348 | break |
| 349 | } |
| 350 | } |
| 351 | } |
| 352 | |
| 353 |