From 11e8ee68c3d843642e931779fe28b24cbd665329 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sat, 15 Aug 2026 14:14:07 -0700 Subject: [PATCH 1/2] fix: honor context cancel during bridge reconnect backoff Reconnect used time.Sleep(backoff) after connect failure and on reconnect, so SIGINT could not interrupt up to 15s. Replace both sleeps with sleepContext so run returns on ctx cancel. Signed-off-by: Sebastien Tardif --- cmd/clawgo/main.go | 14 +++++++++++-- cmd/clawgo/sleep.go | 17 ++++++++++++++++ cmd/clawgo/sleep_test.go | 44 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 cmd/clawgo/sleep.go create mode 100644 cmd/clawgo/sleep_test.go diff --git a/cmd/clawgo/main.go b/cmd/clawgo/main.go index cd521e3..5d3a4ca 100644 --- a/cmd/clawgo/main.go +++ b/cmd/clawgo/main.go @@ -350,7 +350,12 @@ func runNode(cfg NodeConfig) error { client, err := connectBridge(cfg.BridgeAddr) if err != nil { logf("bridge connect failed: %v", err) - time.Sleep(backoff) + if err := sleepContext(ctx, backoff); err != nil { + if mdnsCleanup != nil { + mdnsCleanup() + } + return nil + } if backoff < 15*time.Second { backoff *= 2 if backoff > 15*time.Second { @@ -482,7 +487,12 @@ func runNode(cfg NodeConfig) error { } return nil } - time.Sleep(backoff) + if err := sleepContext(ctx, backoff); err != nil { + if mdnsCleanup != nil { + mdnsCleanup() + } + return nil + } if backoff < 15*time.Second { backoff *= 2 if backoff > 15*time.Second { diff --git a/cmd/clawgo/sleep.go b/cmd/clawgo/sleep.go new file mode 100644 index 0000000..b3f1645 --- /dev/null +++ b/cmd/clawgo/sleep.go @@ -0,0 +1,17 @@ +package main + +import ( + "context" + "time" +) + +func sleepContext(ctx context.Context, d time.Duration) error { + t := time.NewTimer(d) + defer t.Stop() + select { + case <-ctx.Done(): + return ctx.Err() + case <-t.C: + return nil + } +} diff --git a/cmd/clawgo/sleep_test.go b/cmd/clawgo/sleep_test.go new file mode 100644 index 0000000..4f4cc6d --- /dev/null +++ b/cmd/clawgo/sleep_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "errors" + "testing" + "time" +) + +func TestSleepContextCanceledReturnsCanceled(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + err := sleepContext(ctx, 30*time.Second) + if !errors.Is(err, context.Canceled) { + t.Fatalf("sleepContext(canceled, 30s) = %v, want context.Canceled", err) + } +} + +func TestSleepContextCancelDuringWait(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { + done <- sleepContext(ctx, 30*time.Second) + }() + cancel() + + var err error + select { + case err = <-done: + case <-time.After(2 * time.Second): + t.Fatal("sleepContext did not return after cancel") + } + if !errors.Is(err, context.Canceled) { + t.Fatalf("sleepContext canceled mid-wait = %v, want context.Canceled", err) + } +} + +func TestSleepContextCompletesWhenContextStaysOpen(t *testing.T) { + err := sleepContext(context.Background(), time.Millisecond) + if err != nil { + t.Fatalf("sleepContext(background, 1ms) = %v, want nil", err) + } +} From 6f3de3e9dfe8e1cb08d7a0d11e38291bed40bed1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 15 Sep 2026 20:05:06 -0700 Subject: [PATCH 2/2] docs: record interruptible reconnect behavior --- CHANGELOG.md | 5 +++++ README.md | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e39a0a6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +- Interrupt bridge reconnect backoff promptly on SIGINT/SIGTERM. Thanks @SebTardif! (#5) diff --git a/README.md b/README.md index 9ac4896..3082c3a 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ printf hey computer turn on the lights Each line on the FIFO becomes a `voice.transcript`; chat responses from the `main` session are spoken via `espeak-ng`. +SIGINT and SIGTERM interrupt reconnect backoff immediately, including when the bridge is unavailable. + ## systemd example Minimal steps: