Skip to content

fix(reef): stop leaking the inbox loop across channel crash restarts#110870

Merged
steipete merged 2 commits into
mainfrom
claude/reef-debug-gateway-updates-f97455
Jul 18, 2026
Merged

fix(reef): stop leaking the inbox loop across channel crash restarts#110870
steipete merged 2 commits into
mainfrom
claude/reef-debug-gateway-updates-f97455

Conversation

@steipete

Copy link
Copy Markdown
Contributor

What Problem This Solves

The live clawd gateway's Reef channel was stuck in a self-sustaining relay rate-limit storm: 2,400+ reef inbox connection failed errors and ~140 rate_limited channel crash/restart cycles per day, with code=1012 reason=replaced by newer connection closes every ~600ms during bursts. Inbound delivery from peers (e.g. molty) was effectively dead while the relay 429'd every request, including WebSocket handshakes.

Root cause chain:

  1. reefPlugin.gateway.startAccount ran Promise.all([inbox.start(ctx.abortSignal), reconciliationLoop()]). When the 30s friend-reconcile loop threw (e.g. a transient relay 429 → ReefRelayError("rate_limited")), Promise.all rejected and the channel exited — while inbox.start() kept running in the background.
  2. The core channel supervisor (src/gateway/server-channels.ts) never aborts the crashed task's AbortController before starting the replacement (it only aborts on manual stop). The orphaned inbox loop therefore reconnected forever.
  3. Orphan and replacement fight over the single per-handle relay inbox socket (1012 replaced by newer connection), each completed catch-up resets the reconnect backoff to 250ms, and the resulting connect storm drives the relay into rate limiting — which crashes the next reconcile, leaks another loop, and sustains the storm indefinitely.

Why This Change Was Made

  • extensions/reef/src/channel.ts: new runReefChannelLifecycle owns one abort scope for both account loops. Any exit path aborts the inbox loop and awaits its settlement before startAccount settles, so no reconnect loop can outlive its account instance. It also inherits an already-aborted parent signal (listeners added after abort never fire). Periodic reconcile failures are now logged and retried instead of crashing the channel — the crash-restart cycle was itself the rate-limit amplifier.
  • src/gateway/server-channels.ts: defense-in-depth for every channel plugin — the supervisor aborts the settled task's controller before starting a crash replacement (both restart paths) and on terminal cleanup, so two account instances can never share a live lifetime.

User Impact

Reef channels no longer melt down into relay rate limiting after a single transient reconcile failure; inbound agent-to-agent delivery stays up. Any channel plugin that leaves background work racing on its abort signal after a crash is now cut off by the supervisor before the replacement starts.

Evidence

  • Live diagnosis on the affected gateway: 2,404 inbox failures / 141 rate_limited exits on 2026-07-18 (1,109 replacements the prior day), 1012-replacement bursts at ~600ms cadence, escalating to WS handshake 429s.
  • node scripts/run-vitest.mjs extensions/reef — 24 files, 263 passed / 2 skipped (includes new lifecycle tests: reconcile-failure containment, teardown-before-settle, already-aborted parent inheritance).
  • node scripts/run-vitest.mjs src/gateway/server-channels.test.ts — 51 passed (includes new "aborts the crashed task's signal before starting its replacement").
  • Autoreview (Codex gpt-5.6-sol, xhigh): clean after addressing one finding (already-aborted parent signal inheritance).

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime channel: reef Channel integration: reef size: S maintainer Maintainer-authored PR labels Jul 18, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 High-priority user-facing bug, regression, or broken workflow. labels Jul 18, 2026
@clawsweeper

clawsweeper Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 18, 2026, 1:59 PM ET / 17:59 UTC.

Summary
The PR aborts and awaits Reef inbox work when its account lifecycle exits, makes periodic reconciliation failures retryable, and aborts crashed channel task signals before supervisor replacements start.

PR surface: Source +57, Tests +115. Total +172 across 4 files.

Reproducibility: no. independent high-confidence current-main reproduction is available in the supplied evidence. The described failure chain is source-reproducible from the proposed lifecycle and supervisor behavior, but the reported live incident is not accompanied by a reusable reproduction transcript.

Review metrics: none identified.

Merge readiness
Overall: 🦪 silver shellfish
Proof: 🦪 silver shellfish
Patch quality: 🐚 platinum hermit
Result: blocked until real behavior proof from a real setup is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Add redacted after-fix runtime logs or a reproducible relay scenario showing a reconcile failure leaves no competing inbox connection.
  • Have a maintainer confirm the supervisor-wide abort-before-restart contract for channel plugins.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR includes pre-fix production diagnosis and focused test output, but no after-fix real Reef runtime evidence; add redacted live logs or a reproducible crash/restart transcript showing the orphan inbox loop has stopped. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] The available proof establishes the pre-fix incident and mocked regression coverage, but not an observed after-fix Reef crash/restart cycle with inbox reconnection quiescing before replacement.

Maintainer options:

  1. Decide the mitigation before merge
    Land only after a maintainer reviews the cross-channel abort contract and the contributor supplies redacted after-fix runtime evidence showing a transient reconcile failure does not leave a second Reef inbox connection alive.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • [P1] The maintainer label and missing after-fix real behavior proof require human review rather than automated repair or cleanup.

Security
Cleared: The diff changes lifecycle control flow and tests only; it introduces no dependency, permission, secret, workflow, artifact-download, or package-resolution change.

Review details

Best possible solution:

Land only after a maintainer reviews the cross-channel abort contract and the contributor supplies redacted after-fix runtime evidence showing a transient reconcile failure does not leave a second Reef inbox connection alive.

Do we have a high-confidence way to reproduce the issue?

No independent high-confidence current-main reproduction is available in the supplied evidence. The described failure chain is source-reproducible from the proposed lifecycle and supervisor behavior, but the reported live incident is not accompanied by a reusable reproduction transcript.

Is this the best way to solve the issue?

Likely yes: coupling Reef-owned loops under one lifecycle abort scope while also aborting every crashed supervisor task is a bounded defense at both ownership layers. A real after-fix runtime trace is still needed to confirm the relay behavior and teardown timing.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against e04ea7916c5b.

Label changes

Label changes:

  • add P1: The reported restart storm can make inbound Reef agent-to-agent delivery unavailable for affected gateways.
  • add rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦪 silver shellfish and patch quality is 🐚 platinum hermit.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR includes pre-fix production diagnosis and focused test output, but no after-fix real Reef runtime evidence; add redacted live logs or a reproducible crash/restart transcript showing the orphan inbox loop has stopped. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Label justifications:

  • P1: The reported restart storm can make inbound Reef agent-to-agent delivery unavailable for affected gateways.
  • rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦪 silver shellfish and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR includes pre-fix production diagnosis and focused test output, but no after-fix real Reef runtime evidence; add redacted live logs or a reproducible crash/restart transcript showing the orphan inbox loop has stopped. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +57, Tests +115. Total +172 across 4 files.

View PR surface stats
Area Files Added Removed Net
Source 2 66 9 +57
Tests 2 117 2 +115
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 4 183 11 +172

What I checked:

  • Reef lifecycle containment: The proposed lifecycle creates one child abort scope for inbox and reconciliation work, inherits an already-aborted parent signal, aborts it on exit, and awaits the inbox task so a reconnect loop cannot outlive the account instance. (extensions/reef/src/channel.ts, a5de69490159)
  • Supervisor defense-in-depth: Both restart paths abort the settled task controller before startChannelInternal starts a replacement, and terminal cleanup aborts the controller as well. (src/gateway/server-channels.ts, a5de69490159)
  • Focused regression coverage: The PR adds lifecycle tests for reconcile-failure containment, teardown-before-settlement, and parent-abort inheritance, plus a supervisor test asserting that a replacement sees the predecessor signal aborted. (extensions/reef/src/channel.test.ts, a5de69490159)
  • Proof gap: The PR body reports pre-fix production counters and focused test results, but it does not include an after-fix live relay run, redacted runtime logs, or a reproducible transcript demonstrating that only one inbox lifetime remains after a crash. (a5de69490159)
  • Protected-label policy: The repository review policy treats the maintainer label as requiring explicit maintainer handling, so this item should not receive automated cleanup or close treatment. (AGENTS.md, e04ea7916c5b)

Likely related people:

  • steipete: The PR commit and live gateway diagnosis tie steipete directly to the reported Reef restart-loop behavior; current-main feature-history attribution was not available from the supplied review evidence. (role: Reef incident reporter and patch author; confidence: medium; commits: a5de69490159; files: extensions/reef/src/channel.ts, src/gateway/server-channels.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@steipete
steipete force-pushed the claude/reef-debug-gateway-updates-f97455 branch from a5de694 to adeb838 Compare July 18, 2026 18:54

@Yigtwxx Yigtwxx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Traced the cancellation path and it holds up: finally { lifecycle.abort(); ...; await inboxTask; } combined with the loop-head guard in start() (if (this.stopped || signal?.aborted) { await this.processing; return; }) means the old loop genuinely can't run an extra iteration, and abort propagates through abortableSleep, live()'s listener and workAbort with removeEventListener called on every path. The .finally() at server-channels.ts:865 does cover every terminal exit, not just the crash path. The three reef tests are all mutation-sensitive.

Two things inline — one gap in the rate-limit argument, one supervisor test that I don't think pins what its name claims.

On sibling parity, the Promise.all([longRunning, throwingLoop]) shape this fixes doesn't recur in other channels — irc, googlechat, nostr, matrix, slack, mattermost, msteams, feishu, sms, qqbot and the rest all own their cleanup via runStoppablePassiveMonitor / runPassiveAccountLifecycle / waitUntilAbort. Two adjacent spots have the same class of issue if you want them on a follow-up list, both pre-existing:

  • extensions/clickclack/src/gateway.ts:281 — the abort path calls finishSocketCycle() directly while the close path correctly drains via finishAfterQueuedMessages(), so an in-flight processIncomingEvent can outlive startAccount. Aborting the signal doesn't drain a turn already in flight, which is exactly what reef's await inboxTask is for.
  • extensions/signal/src/monitor.ts:719await Promise.all([daemonLifecycle.stop(), monitorTaskRunner.waitForIdle()]): if stop() rejects, Promise.all rejects immediately and waitForIdle() is abandoned. The comment above it describes the invariant the code doesn't quite hold. extensions/qa-channel/src/gateway.ts:80 has the shape that does.

await runReefChannelLifecycle({
parentSignal: ctx.abortSignal,
startInbox: (signal) => inbox.start(signal),
reconcile,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The periodic reconcile now gets onReconcileError, but the startup one at line 405 is still a bare await reconcile() outside runReefChannelLifecycle, with no try/catch.

That path is friends.reconcile()transport.listFriends(), which throws ReefRelayError on a relay 429 (transport.ts:221). So while the relay is rate-limiting, the channel still dies here on every startAccount, and with MAX_RESTARTS = 10 (server-channels.ts:50) Reef gives up permanently after ten attempts.

Harmless for the leak this PR targets — line 405 runs before inbox is constructed, so there's no orphan — but it means the description's "the crash-restart cycle was itself the rate-limit amplifier" argument only half-lands: the WS handshake storm is gone, yet each restart attempt still fires a REST listFriends at the same rate-limited relay.

Making the startup reconcile best-effort too (log and continue, let the inbox start anyway) would close it, or it may be worth saying explicitly that startup reconcile is intentionally fail-fast.


// A crashed startAccount can leave background work racing on its signal
// (e.g. a reconnect loop). The replacement must never overlap that lifetime.
expect(signals[0]?.aborted).toBe(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This asserts "eventually aborted", not "aborted before the replacement started" as the test name says — the check runs after advanceTimersUntil has already let call #2 through.

The new .finally() block at server-channels.ts:865 aborts the same controller and runs as a microtask right after await startChannelInternal(...), and advanceTimersUntil flushes microtasks each step (:183). So I think reverting the abort.abort() at :847 — the one this test exists to pin — would leave it green, because the .finally() abort still makes signals[0].aborted true by the time it's read.

Capturing the state inside the replacement call would make it load-bearing, e.g. push signals[0]?.aborted from within the second startAccount invocation and assert on that.

Related: of the three abort.abort() calls added, only :847 is covered — :795 (timed-out-stop restart) and :865 (terminal) have no test.

@steipete
steipete merged commit e80b0b5 into main Jul 18, 2026
127 of 130 checks passed
@steipete

Copy link
Copy Markdown
Contributor Author

Merged via squash.

@steipete
steipete deleted the claude/reef-debug-gateway-updates-f97455 branch July 18, 2026 19:00
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 19, 2026
…penclaw#110870)

* fix(reef): stop leaking the inbox loop across channel crash restarts

* fix(reef): move channel lifecycle helper to its own module for lint/deadcode gates
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: reef Channel integration: reef gateway Gateway runtime maintainer Maintainer-authored PR P1 High-priority user-facing bug, regression, or broken workflow. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. size: M status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants