Add network issues tolerance #7

Closed
opened 2026-07-18 13:59:01 +00:00 by alex · 4 comments
Owner

At the moment, in case of a network issue whatsoever when connecting to the server, the process crashes:

Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]: [irc error] ConnectionRefused: Connection refused (os error 111)
Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]:     at async Object.connectTls (ext:deno_net/02_tls.js:1:1429)
Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]:     at async Object.connectTls (https://jsr.io/@irc/client/0.23.1/runtime/deno.ts:45:25)
Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]:     at async Client.connect (https://jsr.io/@irc/client/0.23.1/core/client.ts:189:13)
Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]:     at async main (file:///tmp/deno-compile-irc-greeter/src/main.ts:140:3)
Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]:     at async file:///tmp/deno-compile-irc-greeter/src/main.ts:187:5 {
Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]:   code: "ECONNREFUSED",
Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]:   type: "connect"
Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]: }

We should manage this gracefully instead of crashing. Let's add an infinite loop with some fixed backpressure on it.

At the moment, in case of a network issue whatsoever when connecting to the server, the process crashes: ``` Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]: [irc error] ConnectionRefused: Connection refused (os error 111) Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]: at async Object.connectTls (ext:deno_net/02_tls.js:1:1429) Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]: at async Object.connectTls (https://jsr.io/@irc/client/0.23.1/runtime/deno.ts:45:25) Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]: at async Client.connect (https://jsr.io/@irc/client/0.23.1/core/client.ts:189:13) Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]: at async main (file:///tmp/deno-compile-irc-greeter/src/main.ts:140:3) Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]: at async file:///tmp/deno-compile-irc-greeter/src/main.ts:187:5 { Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]: code: "ECONNREFUSED", Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]: type: "connect" Jul 18 11:16:36 ovh01.l3x.in horsebot[1211785]: } ``` We should manage this gracefully instead of crashing. Let's add an infinite loop with some fixed backpressure on it.
Author
Owner

/pi new PR

/pi new PR
Contributor

Now I understand the codebase. The issue is that client.connect() in main() is awaited directly, so a network failure (like ConnectionRefused) propagates up and crashes the process. I'll add a retry loop with a fixed backoff, extracted as a testable retryForever helper, and refactor main() to recreate a fresh client on each attempt.

Let me make the changes:Now let me add tests for the retry logic:I used assertRejects in the test but need to import it. Let me fix that:Now let me verify the changes and run the tests, lint, and type check:Type check passes. Now let me run tests, lint, and format:Two issues to fix: a lint error (async with no await) and an off-by-one in my test assertion. Let me look at the test file:All 54 tests pass and lint is clean. Let me verify formatting and run the type check once more:Everything passes. Let me check the README to see if there's a good place to document the new reconnect behavior:I'll add a brief note about the reconnect behavior to the README, after the compile instructions:All checks pass. Let me review the final diff before creating the PR:The diff is clean and all checks pass (type check, lint, 54 tests, format). Let me create the PR:PR #8 created.

The bot no longer crashes on transient network failures during the initial IRC connection. Instead, a new retryForever helper wraps client.connect() in an infinite loop with a fixed 10-second backoff between failed attempts. Each attempt builds a fresh client (via the extracted createClient) so a half-open socket from a prior failure never leaks forward, and failures are logged with the attempt number and backoff interval. The retry logic is covered by three new unit tests (success-first-try, retry-until-success, and never-gives-up), and a README section documents the new behaviour.


View action run | Model: zai/glm-5.2 (thinking: max) | Time: 6m 50s | Tokens: 595.6K | Pi SDK v0.80.10 | Action v2.26.0-develop (develop @ c0cad3d)

Now I understand the codebase. The issue is that `client.connect()` in `main()` is awaited directly, so a network failure (like `ConnectionRefused`) propagates up and crashes the process. I'll add a retry loop with a fixed backoff, extracted as a testable `retryForever` helper, and refactor `main()` to recreate a fresh client on each attempt. Let me make the changes:Now let me add tests for the retry logic:I used `assertRejects` in the test but need to import it. Let me fix that:Now let me verify the changes and run the tests, lint, and type check:Type check passes. Now let me run tests, lint, and format:Two issues to fix: a lint error (`async` with no `await`) and an off-by-one in my test assertion. Let me look at the test file:All 54 tests pass and lint is clean. Let me verify formatting and run the type check once more:Everything passes. Let me check the README to see if there's a good place to document the new reconnect behavior:I'll add a brief note about the reconnect behavior to the README, after the compile instructions:All checks pass. Let me review the final diff before creating the PR:The diff is clean and all checks pass (type check, lint, 54 tests, format). Let me create the PR:PR [#8](https://forge.l3x.in/alex/irc-greeter-bot/pulls/8) created. The bot no longer crashes on transient network failures during the initial IRC connection. Instead, a new `retryForever` helper wraps `client.connect()` in an infinite loop with a fixed 10-second backoff between failed attempts. Each attempt builds a fresh client (via the extracted `createClient`) so a half-open socket from a prior failure never leaks forward, and failures are logged with the attempt number and backoff interval. The retry logic is covered by three new unit tests (success-first-try, retry-until-success, and never-gives-up), and a README section documents the new behaviour. --- [View action run](https://forge.l3x.in/alex/irc-greeter-bot/actions/runs/41) | Model: zai/glm-5.2 (thinking: max) | Time: 6m 50s | Tokens: 595.6K | Pi SDK v0.80.10 | Action v2.26.0-develop (develop @ c0cad3d)
alex closed this issue 2026-07-18 14:16:23 +00:00
alex reopened this issue 2026-07-21 18:10:18 +00:00
Author
Owner

/pi apparently the PR didn't fix the issue, it's still failing miserably after e.g. IRC server upgrade:

Jul 21 09:50:47 ovh01.l3x.in horsebot[2746268]: aa5ab2610db1bb9864fc85eeadf1a0f3b9b141bbabd66a3f3a95c6a35a7389ab
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: [bot] Connecting to irc.gifthorse.dev:6697 (tls=true) as HorseBot...
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: [bot] Connected and greeting newcomers in: #lounge
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: [irc error] ConnectionRefused: Connection refused (os error 111)
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]:     at async Object.connectTls (ext:deno_net/02_tls.js:1:1429)
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]:     at async Object.connectTls (https://jsr.io/@irc/client/0.23.1/runtime/deno.ts:45:25)
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]:     at async Client.connect (https://jsr.io/@irc/client/0.23.1/core/client.ts:189:13)
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]:     at async file:///tmp/deno-compile-irc-greeter/src/main.ts:149:7
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]:     at async retryForever (file:///tmp/deno-compile-irc-greeter/src/main.ts:125:14)
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]:     at async main (file:///tmp/deno-compile-irc-greeter/src/main.ts:143:18)
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]:     at async file:///tmp/deno-compile-irc-greeter/src/main.ts:259:5 {
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]:   code: "ECONNREFUSED",
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]:   type: "connect"
Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: }
Jul 21 09:50:48 ovh01.l3x.in podman[2746301]: 2026-07-21 09:50:48.066315439 +0000 UTC m=+0.047269138 container died aa5ab2610db1bb9864fc85eeadf1a0f3b9b141bbabd66a3f3a95c6a35a7389ab (image=forge.l3x.in/ale>
Jul 21 09:50:48 ovh01.l3x.in podman[2746301]: 2026-07-21 09:50:48.112566505 +0000 UTC m=+0.093520180 container remove aa5ab2610db1bb9864fc85eeadf1a0f3b9b141bbabd66a3f3a95c6a35a7389ab (image=forge.l3x.in/a>
Jul 21 09:50:48 ovh01.l3x.in systemd[825]: horsebot.service: Scheduled restart job, restart counter is at 10.
Jul 21 09:50:48 ovh01.l3x.in systemd[825]: Starting horsebot.service...
Jul 21 09:50:48 ovh01.l3x.in podman[2746347]: 2026-07-21 09:50:48.922173803 +0000 UTC m=+0.205279878 container create a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1 (image=forge.l3x.in/a>
Jul 21 09:50:48 ovh01.l3x.in podman[2746347]: 2026-07-21 09:50:48.893492042 +0000 UTC m=+0.176598098 image pull 9346e5b8ccc4c433135657a9df656928ebf4b4e4b2959d13e7748ea51e0dc12c forge.l3x.in/alex/irc-greet>
Jul 21 09:50:49 ovh01.l3x.in podman[2746347]: 2026-07-21 09:50:49.008429028 +0000 UTC m=+0.291535129 container init a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1 (image=forge.l3x.in/ale>
Jul 21 09:50:49 ovh01.l3x.in podman[2746347]: 2026-07-21 09:50:49.014921072 +0000 UTC m=+0.298027143 container start a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1 (image=forge.l3x.in/al>
Jul 21 09:50:49 ovh01.l3x.in systemd[825]: Started horsebot.service.
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746347]: a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: [bot] Connecting to irc.gifthorse.dev:6697 (tls=true) as HorseBot...
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: [bot] Connected and greeting newcomers in: #lounge
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: [irc error] ConnectionRefused: Connection refused (os error 111)
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]:     at async Object.connectTls (ext:deno_net/02_tls.js:1:1429)
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]:     at async Object.connectTls (https://jsr.io/@irc/client/0.23.1/runtime/deno.ts:45:25)
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]:     at async Client.connect (https://jsr.io/@irc/client/0.23.1/core/client.ts:189:13)
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]:     at async file:///tmp/deno-compile-irc-greeter/src/main.ts:149:7
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]:     at async retryForever (file:///tmp/deno-compile-irc-greeter/src/main.ts:125:14)
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]:     at async main (file:///tmp/deno-compile-irc-greeter/src/main.ts:143:18)
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]:     at async file:///tmp/deno-compile-irc-greeter/src/main.ts:259:5 {
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]:   code: "ECONNREFUSED",
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]:   type: "connect"
Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: }
Jul 21 09:50:49 ovh01.l3x.in podman[2746381]: 2026-07-21 09:50:49.357868038 +0000 UTC m=+0.060192747 container died a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1 (image=forge.l3x.in/ale>
Jul 21 09:50:49 ovh01.l3x.in podman[2746381]: 2026-07-21 09:50:49.401882212 +0000 UTC m=+0.104206891 container remove a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1 (image=forge.l3x.in/a>
Jul 21 09:50:49 ovh01.l3x.in systemd[825]: horsebot.service: Scheduled restart job, restart counter is at 11.
Jul 21 09:50:49 ovh01.l3x.in systemd[825]: horsebot.service: Start request repeated too quickly.
Jul 21 09:50:49 ovh01.l3x.in systemd[825]: horsebot.service: Failed with result 'start-limit-hit'.
Jul 21 09:50:49 ovh01.l3x.in systemd[825]: Failed to start horsebot.service.

Let's fix it for real this time in a new PR

/pi apparently the PR didn't fix the issue, it's still failing miserably after e.g. IRC server upgrade: ``` Jul 21 09:50:47 ovh01.l3x.in horsebot[2746268]: aa5ab2610db1bb9864fc85eeadf1a0f3b9b141bbabd66a3f3a95c6a35a7389ab Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: [bot] Connecting to irc.gifthorse.dev:6697 (tls=true) as HorseBot... Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: [bot] Connected and greeting newcomers in: #lounge Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: [irc error] ConnectionRefused: Connection refused (os error 111) Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: at async Object.connectTls (ext:deno_net/02_tls.js:1:1429) Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: at async Object.connectTls (https://jsr.io/@irc/client/0.23.1/runtime/deno.ts:45:25) Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: at async Client.connect (https://jsr.io/@irc/client/0.23.1/core/client.ts:189:13) Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: at async file:///tmp/deno-compile-irc-greeter/src/main.ts:149:7 Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: at async retryForever (file:///tmp/deno-compile-irc-greeter/src/main.ts:125:14) Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: at async main (file:///tmp/deno-compile-irc-greeter/src/main.ts:143:18) Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: at async file:///tmp/deno-compile-irc-greeter/src/main.ts:259:5 { Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: code: "ECONNREFUSED", Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: type: "connect" Jul 21 09:50:47 ovh01.l3x.in horsebot[2746290]: } Jul 21 09:50:48 ovh01.l3x.in podman[2746301]: 2026-07-21 09:50:48.066315439 +0000 UTC m=+0.047269138 container died aa5ab2610db1bb9864fc85eeadf1a0f3b9b141bbabd66a3f3a95c6a35a7389ab (image=forge.l3x.in/ale> Jul 21 09:50:48 ovh01.l3x.in podman[2746301]: 2026-07-21 09:50:48.112566505 +0000 UTC m=+0.093520180 container remove aa5ab2610db1bb9864fc85eeadf1a0f3b9b141bbabd66a3f3a95c6a35a7389ab (image=forge.l3x.in/a> Jul 21 09:50:48 ovh01.l3x.in systemd[825]: horsebot.service: Scheduled restart job, restart counter is at 10. Jul 21 09:50:48 ovh01.l3x.in systemd[825]: Starting horsebot.service... Jul 21 09:50:48 ovh01.l3x.in podman[2746347]: 2026-07-21 09:50:48.922173803 +0000 UTC m=+0.205279878 container create a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1 (image=forge.l3x.in/a> Jul 21 09:50:48 ovh01.l3x.in podman[2746347]: 2026-07-21 09:50:48.893492042 +0000 UTC m=+0.176598098 image pull 9346e5b8ccc4c433135657a9df656928ebf4b4e4b2959d13e7748ea51e0dc12c forge.l3x.in/alex/irc-greet> Jul 21 09:50:49 ovh01.l3x.in podman[2746347]: 2026-07-21 09:50:49.008429028 +0000 UTC m=+0.291535129 container init a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1 (image=forge.l3x.in/ale> Jul 21 09:50:49 ovh01.l3x.in podman[2746347]: 2026-07-21 09:50:49.014921072 +0000 UTC m=+0.298027143 container start a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1 (image=forge.l3x.in/al> Jul 21 09:50:49 ovh01.l3x.in systemd[825]: Started horsebot.service. Jul 21 09:50:49 ovh01.l3x.in horsebot[2746347]: a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1 Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: [bot] Connecting to irc.gifthorse.dev:6697 (tls=true) as HorseBot... Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: [bot] Connected and greeting newcomers in: #lounge Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: [irc error] ConnectionRefused: Connection refused (os error 111) Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: at async Object.connectTls (ext:deno_net/02_tls.js:1:1429) Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: at async Object.connectTls (https://jsr.io/@irc/client/0.23.1/runtime/deno.ts:45:25) Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: at async Client.connect (https://jsr.io/@irc/client/0.23.1/core/client.ts:189:13) Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: at async file:///tmp/deno-compile-irc-greeter/src/main.ts:149:7 Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: at async retryForever (file:///tmp/deno-compile-irc-greeter/src/main.ts:125:14) Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: at async main (file:///tmp/deno-compile-irc-greeter/src/main.ts:143:18) Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: at async file:///tmp/deno-compile-irc-greeter/src/main.ts:259:5 { Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: code: "ECONNREFUSED", Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: type: "connect" Jul 21 09:50:49 ovh01.l3x.in horsebot[2746370]: } Jul 21 09:50:49 ovh01.l3x.in podman[2746381]: 2026-07-21 09:50:49.357868038 +0000 UTC m=+0.060192747 container died a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1 (image=forge.l3x.in/ale> Jul 21 09:50:49 ovh01.l3x.in podman[2746381]: 2026-07-21 09:50:49.401882212 +0000 UTC m=+0.104206891 container remove a888486255b68cabad40f06e9d44552b4077ab0fa882cd97f60bb2b71a78c3b1 (image=forge.l3x.in/a> Jul 21 09:50:49 ovh01.l3x.in systemd[825]: horsebot.service: Scheduled restart job, restart counter is at 11. Jul 21 09:50:49 ovh01.l3x.in systemd[825]: horsebot.service: Start request repeated too quickly. Jul 21 09:50:49 ovh01.l3x.in systemd[825]: horsebot.service: Failed with result 'start-limit-hit'. Jul 21 09:50:49 ovh01.l3x.in systemd[825]: Failed to start horsebot.service. ``` Let's fix it for real this time in a new PR
Contributor

Let me understand the full codebase structure and the issue better.Now I can see the root cause. Let me read the EventEmitter implementation to understand error handling:Now I have a complete picture of the root cause. Let me verify the key finding:

The @irc/client library's connect() method catches connection errors internally and returns null instead of throwing:

// From @irc/client core/client.ts:
try {
    this.conn = await this.runtime.connectTls(...);
    this.emit("connected", publicAddr);
} catch (error) {
    this.emitError("connect", error);  // emits error event, does NOT throw
    return null;                        // returns null!
}

The retryForever wrapper only catches thrown errors. Since connect() resolves with null, retryForever thinks the operation succeeded and returns — so the retry never happens. The bot proceeds with a dead connection, and the process exits when the event loop empties (no active I/O).

Let me now implement the fix:Now I'll implement the fix. The key changes:

  1. Check connect() return value@irc/client's connect() returns null on failure instead of throwing, so retryForever never detected the failure
  2. Wrap the entire connection lifecycle in retryForever — connect, stay alive until disconnected, then throw to trigger reconnection
  3. Inline shutdown handlers with a mutable client reference that works across reconnectionsLet me verify the result:The gracefulShutdown function has a bug — the inProgress guard is recreated on each call so it doesn't actually prevent re-entrancy. Let me clean this up with a simpler approach:Now fix the double blank line:Now let me type-check and run existing tests:All tests pass. Let me run the linter:All good. Now let me add a test that specifically documents the bug being fixed — @irc/client's connect() returning null instead of throwing:Now I'll add a test that documents the specific bug — @irc/client's connect() returns null instead of throwing:All 55 tests pass. Let me do a final review of the complete file:Let me also check the README for any documentation that needs updating:Now let me run the complete check suite one more time:Lint caught a missing await in the test. Let me fix it to more accurately simulate the async connect() pattern:All checks pass: type-check, lint, format, and 55 tests. Let me create the PR:PR created. Let me verify CI picks it up:The PR is created at #10. Here's a summary of the fix:

Why the previous fix didn't work

The retryForever wrapper only catches thrown errors. But @irc/client's Client.connect() never throws on connection failure — it catches the error internally, emits it as an "error" event, and returns null:

// Inside @irc/client core/client.ts
catch (error) {
    this.emitError("connect", error);  // → logs via our error handler
    return null;                        // → NOT a throw!
}

So retryForever saw a resolved promise, treated it as success, returned immediately, and the bot proceeded with a dead connection. No active I/O kept the event loop alive → process exited → systemd restart loop → start-limit-hit.

This explains the confusing logs where "Connected and greeting newcomers" appeared before the error — retryForever had already returned.

What changed

  1. Null check on connect() return — converts the silent null into a thrown error so retryForever actually retries

  2. Full lifecycle in the retry loop — after connecting, the operation blocks on await client.once("disconnected"). When the connection drops (e.g. IRC server upgrade), it throws to trigger reconnection with backoff

  3. SIGINT handler uses a mutable client reference — works across reconnections instead of holding a stale Client


View action run | Model: zai/glm-5.2 (thinking: max) | Time: 13m 4s | Tokens: 3.9M | Pi SDK v0.80.10 | Action v2.26.0-develop (develop @ c0cad3d)

Let me understand the full codebase structure and the issue better.Now I can see the root cause. Let me read the EventEmitter implementation to understand error handling:Now I have a complete picture of the root cause. Let me verify the key finding: The `@irc/client` library's `connect()` method **catches connection errors internally** and **returns `null`** instead of throwing: ```typescript // From @irc/client core/client.ts: try { this.conn = await this.runtime.connectTls(...); this.emit("connected", publicAddr); } catch (error) { this.emitError("connect", error); // emits error event, does NOT throw return null; // returns null! } ``` The `retryForever` wrapper only catches **thrown** errors. Since `connect()` resolves with `null`, `retryForever` thinks the operation succeeded and returns — so the retry never happens. The bot proceeds with a dead connection, and the process exits when the event loop empties (no active I/O). Let me now implement the fix:Now I'll implement the fix. The key changes: 1. **Check `connect()` return value** — `@irc/client`'s `connect()` returns `null` on failure instead of throwing, so `retryForever` never detected the failure 2. **Wrap the entire connection lifecycle** in `retryForever` — connect, stay alive until disconnected, then throw to trigger reconnection 3. **Inline shutdown handlers** with a mutable client reference that works across reconnectionsLet me verify the result:The `gracefulShutdown` function has a bug — the `inProgress` guard is recreated on each call so it doesn't actually prevent re-entrancy. Let me clean this up with a simpler approach:Now fix the double blank line:Now let me type-check and run existing tests:All tests pass. Let me run the linter:All good. Now let me add a test that specifically documents the bug being fixed — `@irc/client`'s `connect()` returning `null` instead of throwing:Now I'll add a test that documents the specific bug — `@irc/client`'s `connect()` returns `null` instead of throwing:All 55 tests pass. Let me do a final review of the complete file:Let me also check the README for any documentation that needs updating:Now let me run the complete check suite one more time:Lint caught a missing `await` in the test. Let me fix it to more accurately simulate the async `connect()` pattern:All checks pass: type-check, lint, format, and 55 tests. Let me create the PR:PR created. Let me verify CI picks it up:The PR is created at [#10](https://forge.l3x.in/alex/irc-greeter-bot/pulls/10). Here's a summary of the fix: ## Why the previous fix didn't work The `retryForever` wrapper only catches **thrown** errors. But `@irc/client`'s `Client.connect()` **never throws** on connection failure — it catches the error internally, emits it as an `"error"` event, and **returns `null`**: ```typescript // Inside @irc/client core/client.ts catch (error) { this.emitError("connect", error); // → logs via our error handler return null; // → NOT a throw! } ``` So `retryForever` saw a resolved promise, treated it as success, returned immediately, and the bot proceeded with a dead connection. No active I/O kept the event loop alive → process exited → systemd restart loop → `start-limit-hit`. This explains the confusing logs where `"Connected and greeting newcomers"` appeared **before** the error — `retryForever` had already returned. ## What changed 1. **Null check on `connect()` return** — converts the silent `null` into a thrown error so `retryForever` actually retries 2. **Full lifecycle in the retry loop** — after connecting, the operation blocks on `await client.once("disconnected")`. When the connection drops (e.g. IRC server upgrade), it throws to trigger reconnection with backoff 3. **SIGINT handler uses a mutable client reference** — works across reconnections instead of holding a stale `Client` --- [View action run](https://forge.l3x.in/alex/irc-greeter-bot/actions/runs/49) | Model: zai/glm-5.2 (thinking: max) | Time: 13m 4s | Tokens: 3.9M | Pi SDK v0.80.10 | Action v2.26.0-develop (develop @ c0cad3d)
alex closed this issue 2026-07-21 18:28:06 +00:00
Sign in to join this conversation.
No labels
dependencies
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
alex/irc-greeter-bot#7
No description provided.