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.

lmata 2026-04-04 04:06 trunk
Commit a4dd20bbfb79fedf6897f950d1d35d64a045b978e2afd5bef61c5966ba646a08
--- cmd/codex-relay/main.go
+++ cmd/codex-relay/main.go
@@ -217,11 +217,10 @@
217217
"SCUTTLEBOT_ACTIVITY_VIA_BROKER="+boolString(relayActive),
218218
)
219219
if relayActive {
220220
go mirrorSessionLoop(ctx, relay, cfg, startedAt)
221221
go presenceLoopPtr(ctx, &relay, cfg.HeartbeatInterval)
222
- go handleReconnectSignal(ctx, &relay, cfg)
223222
}
224223
225224
if !isInteractiveTTY() {
226225
cmd.Stdin = os.Stdin
227226
cmd.Stdout = os.Stdout
@@ -272,10 +271,11 @@
272271
go func() {
273272
copyPTYOutput(ptmx, os.Stdout, state)
274273
}()
275274
if relayActive {
276275
go relayInputLoop(ctx, relay, cfg, state, ptmx, onlineAt)
276
+ go handleReconnectSignal(ctx, &relay, cfg, state, ptmx, startedAt)
277277
}
278278
279279
err = cmd.Wait()
280280
cancel()
281281
@@ -330,11 +330,11 @@
330330
}
331331
}
332332
}
333333
}
334334
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) {
336336
sigCh := make(chan os.Signal, 1)
337337
signal.Notify(sigCh, syscall.SIGUSR1)
338338
defer signal.Stop(sigCh)
339339
340340
for {
@@ -385,15 +385,22 @@
385385
continue
386386
}
387387
cancel()
388388
389389
*relayPtr = conn
390
+ now := time.Now()
390391
_ = conn.Post(context.Background(), fmt.Sprintf(
391392
"reconnected in %s; mention %s to interrupt",
392393
filepath.Base(cfg.TargetCWD), cfg.Nick,
393394
))
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)
395402
break
396403
}
397404
}
398405
}
399406
400407
--- 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
--- cmd/gemini-relay/main.go
+++ cmd/gemini-relay/main.go
@@ -152,10 +152,11 @@
152152
_ = relay.Close(closeCtx)
153153
}()
154154
}
155155
156156
cmd := exec.Command(cfg.GeminiBin, cfg.Args...)
157
+ startedAt := time.Now()
157158
cmd.Env = append(os.Environ(),
158159
"SCUTTLEBOT_CONFIG_FILE="+cfg.ConfigFile,
159160
"SCUTTLEBOT_URL="+cfg.URL,
160161
"SCUTTLEBOT_TOKEN="+cfg.Token,
161162
"SCUTTLEBOT_CHANNEL="+cfg.Channel,
@@ -165,11 +166,10 @@
165166
"SCUTTLEBOT_SESSION_ID="+cfg.SessionID,
166167
"SCUTTLEBOT_NICK="+cfg.Nick,
167168
)
168169
if relayActive {
169170
go presenceLoopPtr(ctx, &relay, cfg.HeartbeatInterval)
170
- go handleReconnectSignal(ctx, &relay, cfg)
171171
}
172172
173173
if !isInteractiveTTY() {
174174
cmd.Stdin = os.Stdin
175175
cmd.Stdout = os.Stdout
@@ -220,10 +220,11 @@
220220
go func() {
221221
copyPTYOutput(ptmx, os.Stdout, state)
222222
}()
223223
if relayActive {
224224
go relayInputLoop(ctx, relay, cfg, state, ptmx, onlineAt)
225
+ go handleReconnectSignal(ctx, &relay, cfg, state, ptmx, startedAt)
225226
}
226227
227228
err = cmd.Wait()
228229
cancel()
229230
@@ -278,11 +279,11 @@
278279
}
279280
}
280281
}
281282
}
282283
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) {
284285
sigCh := make(chan os.Signal, 1)
285286
signal.Notify(sigCh, syscall.SIGUSR1)
286287
defer signal.Stop(sigCh)
287288
288289
for {
@@ -333,15 +334,19 @@
333334
continue
334335
}
335336
cancel()
336337
337338
*relayPtr = conn
339
+ now := time.Now()
338340
_ = conn.Post(context.Background(), fmt.Sprintf(
339341
"reconnected in %s; mention %s to interrupt",
340342
filepath.Base(cfg.TargetCWD), cfg.Nick,
341343
))
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)
343348
break
344349
}
345350
}
346351
}
347352
348353
--- 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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button