Skip to content

fix(discord): self-healing CDP scanner — relaxed target match + idle-watchdog pump (#3693) - #4108

Merged
senamakel merged 6 commits into
tinyhumansai:mainfrom
oxoxDev:fix/3693-discord-cdp-pump-watchdog
Jun 25, 2026
Merged

senamakel merged 6 commits into
tinyhumansai:mainfrom
oxoxDev:fix/3693-discord-cdp-pump-watchdog

Conversation

@oxoxDev

@oxoxDev oxoxDev commented Jun 25, 2026 •

Copy link
Copy Markdown
Member

Summary

Makes the Discord webview scanner actually attach and self-heal. Three coupled fixes, all validated against a live Discord login on a CEF staging build:

  1. Resilient event pump — idle watchdog + non-lossy drain (the original Discord CDP pump: add idle watchdog + handle lossy broadcast (follow-up to #3636) #3693 ask).
  2. Relaxed target match — port the Slack scanner's pin → strict → relaxed resolution so attach survives Discord stripping the URL fragment (without this, the scanner never attaches at all once the page settles).
  3. Watchdog liveness scoping — count only own-session frames as liveness, so a dead/reloaded page actually trips re-attach (a defect a live renderer-crash smoke caught that all unit tests missed).

Problem

#3636 moved the Discord scanner onto the in-process WebviewCdpTransport long-lived pump_events. Three issues compound there:

  • No self-heal: pump_events only returns on RecvError::Closed (whole transport dropped). When the attached page target dies (Ctrl+R reload, renderer crash, SPA hard-nav) the session goes stale but the pump awaits forever — MITM silently stops until the webview is torn down.
  • Lossy ring: the 256-slot broadcast drops frames on Lagged with no re-sync, and the Discord pump had no idle watchdog at all.
  • Attach never succeeded (found during smoke): Discord's client replaceStates to its canonical /channels URL on boot, stripping the #openhuman-account-<id> fragment the webview opens with. Both the MITM attach and the DOM poll matched strictly on that fragment, so after the 4s settle the strict match failed forever — the scanner looped no matching page target indefinitely and never attached. Slack hit the identical SPA-strip problem and fixed it long ago (fix(webview/slack): scanner spawn + notifications + post-pushState target match (#1016) #1028); Discord never got the equivalent.

Solution

  • cdp/conn.rs — pump_events_resilient / pump_resilient_core: a drain task forwards frames off the fixed broadcast ring into an unbounded queue (decoupling slow per-frame work from the ring); the consumer reads under a tokio::time::timeout idle watchdog (PUMP_IDLE_TIMEOUT = 90s, > Discord's ~41s gateway heartbeat). On idle it returns Ok(()) so the outer loop re-attaches. The drain forwards only own-session frames — empty-session (browser/target-level) chatter is dropped so it can't falsely reset the idle timer on a dead page.
  • discord_scanner/mod.rs: shared attach_account_target + pure resolve_page_target implement pin → strict → relaxed target selection (mirrors slack_scanner::scan_once), persisting a target-id pin across reconnects. Wired into both run_mitm_session (with the resilient pump) and dom_scan_once. Per-account data_directory isolation makes the relaxed prefix-only fallback safe.
  • Fixed the now-inaccurate doc comments in both files.

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/pr-ci.yml. Run pnpm test:coverage and pnpm test:rust locally; PRs below 80% on changed lines will not merge.
  • N/A: behaviour-only resilience fix — no feature row added/removed in docs/TEST-COVERAGE-MATRIX.md. (Coverage matrix updated)
  • N/A: no coverage-matrix feature IDs affected (behaviour-only). (All affected feature IDs listed under ## Related)
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • N/A: background-scanner CDP-attach resilience; no release-cut UI/flow surface changed in docs/RELEASE-MANUAL-SMOKE.md. (Manual smoke checklist)
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

Discord MITM + DOM ingest now attach and recover. Validated live (staging, real Discord account):

  • Attach: succeeds via the relaxed fallback after the hash is stripped (attached session=…, Network.enable ok, gateway ws-open, dom scan rows=8). Previously looped no matching page target forever.
  • Self-heal: killed the Discord renderer → idle watchdog fired at exactly +90s (event pump idle for 90s, forcing re-attach) → session ended cleanly → re-attached to a new session. Before the liveness-scoping fix the watchdog never fired (empty-session transport chatter kept resetting the timer for 3+ min).

Known limits (honest): the non-lossy drain still logged a minor drain lagged skipped≈22 during the gateway zlib cold-start burst (near-non-lossy under extreme bursts; DOM poll backfills state). A hard-SIGKILL'd renderer leaves a zombie target with no live renderer, so re-attach's Network.enable times out (35s) and retries — a test artifact of the kill; a real reload/crash-recovery has a live renderer and completes immediately.

Related

Closes #3693
Follow-up to #3636. Relaxed-match pattern mirrors #1028 (Slack).

AI Authored PR Metadata

  • AI-authored: yes — implemented with Claude Code.
  • Human review: author reviewed all changes; live smoke-tested on a staging CEF build with a real Discord login (attach, renderer-crash watchdog, re-attach).
  • Scope: app/src-tauri/src/cdp/conn.rs, app/src-tauri/src/discord_scanner/mod.rs, app/src-tauri/src/discord_scanner/mod_tests.rs.

oxoxDev added 2 commits June 25, 2026 15:09
…inyhumansai#3693)

Add CdpConn::pump_events_resilient for long-lived consumers: a background
drain task forwards session-matched frames off the fixed-capacity broadcast
ring into an unbounded queue (so a burst over EVENT_CHANNEL_CAP is buffered,
not silently dropped), and an idle-timeout watchdog returns Ok once no frame
arrives within the window so a stale/destroyed page target forces re-attach.
Plain pump_events is unchanged (cdp/session.rs has its own watchdog). Core
logic split into pump_resilient_core over raw primitives + unit tests
(idle-timeout, close-flush, non-lossy burst, session filter).
…targets (tinyhumansai#3693)

The Discord scanner is the only long-lived CDP pump consumer. The plain pump
returned only when the whole transport closed, so a stale page target (Ctrl+R
reload, renderer crash, hard navigation) left it awaiting forever and the
outer reconnect loop never re-entered — MITM silently stopped. Switch to
pump_events_resilient with PUMP_IDLE_TIMEOUT=90s (>2x Discord's ~41s gateway
heartbeat) so a dead session forces re-attach, and bursts buffer instead of
dropping frames. Correct the now-inaccurate run_mitm_session + pump doc
comments to match the in-process implementation.
@coderabbitai

coderabbitai Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@oxoxDev, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 49 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97ff359b-f086-4145-8233-42e8ec5d4c08

📥 Commits

Reviewing files that changed from the base of the PR and between b220bd8 and 3fe31bd.

📒 Files selected for processing (3)
  • app/src-tauri/src/cdp/conn.rs
  • app/src-tauri/src/discord_scanner/mod.rs
  • app/src-tauri/src/discord_scanner/mod_tests.rs
📝 Walkthrough

Walkthrough

The PR adds a resilient CDP event pump with idle-timeout handling and buffered delivery, updates Discord scanner attachment to pin and resolve page targets across reconnects, threads the pin through DOM polling, and adds target-resolution tests.

Changes

Resilient CDP attachment

Layer / File(s) Summary
Resilient event pump
app/src-tauri/src/cdp/conn.rs
Adds pump_events_resilient and pump_resilient_core, with idle-timeout exit, own-session filtering, unbounded buffering, and coverage for timeout, close, overflow, and frame-filtering cases.
Pinned target attachment
app/src-tauri/src/discord_scanner/mod.rs
Adds PUMP_IDLE_TIMEOUT, keeps pinned_target_id across reconnects, resolves Discord page targets by pinned/strict/relaxed matching, and uses pump_events_resilient in run_mitm_session.
DOM poll attachment
app/src-tauri/src/discord_scanner/mod.rs
Threads pinned_target_id through spawn_dom_poll and dom_scan_once, and routes DOM polling through attach_account_target.
Target resolution tests
app/src-tauri/src/discord_scanner/mod_tests.rs
Adds tests for strict matches, relaxed matches, pinned selection, stale pins, and non-page target rejection in resolve_page_target.

Sequence Diagram(s)

sequenceDiagram
  participant spawn_scanner
  participant run_mitm_session
  participant attach_account_target
  participant resolve_page_target
  participant CdpConn
  spawn_scanner->>run_mitm_session: pinned_target_id
  run_mitm_session->>attach_account_target: attach_account_target(..., pinned_target_id)
  attach_account_target->>resolve_page_target: resolve_page_target(...)
  resolve_page_target-->>attach_account_target: targetId + strict
  run_mitm_session->>CdpConn: pump_events_resilient(session_id, PUMP_IDLE_TIMEOUT, ...)
  CdpConn-->>run_mitm_session: return Ok(()) on idle timeout
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tinyhumansai/openhuman#3498: Both PRs modify app/src-tauri/src/cdp/conn.rs’s CDP event-pumping path (CdpConn/pump_events behavior and session-scoped event handling), so the resilient event consumer added here is directly coupled to the same CdpConn event infrastructure.

Suggested labels

rust-core

Poem

A bunny hopped through the CDP night,
With pinned little targets held snug and tight.
When silence grew long, I still knew the way,
Buffers went boing! and events found the play.
\o/ 🌙

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: a self-healing Discord CDP scanner with relaxed target matching and an idle-watchdog pump.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands.

oxoxDev added 2 commits June 25, 2026 20:13
…hash strip (tinyhumansai#3693)

Discord's web client replaceState's to its canonical /channels URL on boot,
stripping the #openhuman-account-<id> fragment the webview opens with. Both the
MITM attach and the DOM poll matched strictly on that fragment, so attach failed
forever once the hash was gone — the scanner never attached after the page
settled. Port the Slack scanner's pin→strict→relaxed resolution (shared
attach_account_target + pure resolve_page_target) into run_mitm_session and
dom_scan_once, persisting a target-id pin across reconnects. Adds 5 resolution
unit tests.
…sai#3693)

The resilient pump's drain forwarded empty-session (browser/target-level)
frames alongside own-session ones. A dead/reloaded page still emits empty-session
transport chatter (target lifecycle, crash churn), which reached the consumer and
reset the 90s idle timer — so a crashed session never tripped re-attach (caught
in live renderer-crash smoke; the watchdog failed to fire for 3+ min). Forward
strictly own-session frames; Discord's real traffic (Network.*, incl. ~41s
gateway heartbeats) is always page-session-tagged, so nothing useful is dropped.
Updates the session-filter test as a regression guard.
@oxoxDev oxoxDev changed the title fix(discord): resilient CDP pump — idle watchdog + non-lossy drain (#3693) fix(discord): self-healing CDP scanner — relaxed target match + idle-watchdog pump (#3693) Jun 25, 2026
@oxoxDev
oxoxDev marked this pull request as ready for review June 25, 2026 14:47
@oxoxDev
oxoxDev requested a review from a team June 25, 2026 14:47
@coderabbitai coderabbitai Bot added the rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. label Jun 25, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b220bd806e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 381 to 383
cdp.pump_events_resilient(&session_id, PUMP_IDLE_TIMEOUT, |method, params| {
dispatch_event(app, account_id, method, params, &mut ingest_state);
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Detach idle CDP sessions before reconnecting

When the new idle watchdog fires while the Discord page target is still alive but quiet (for example a logged-out Discord page, temporary offline state, or a throttled gateway), this .await returns Ok(()) and the outer loop immediately attaches a new session, but the old Target.attachToTarget session is never detached. Because CdpConn is just a handle over the shared in-process transport, dropping it does not send Target.detachFromTarget, so this can accumulate enabled Network sessions and duplicate/extra CDP traffic every 90 seconds until the webview is closed.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src-tauri/src/cdp/conn.rs`:
- Around line 174-229: The drain loop in the async pump is swallowing
RecvError::Lagged and continuing, which breaks the non-lossy contract; update
the tokio::spawn drain path to treat lag as a hard failure by returning an error
or triggering the same re-attach flow used on idle/closed transport, instead of
logging and continuing. Keep the behavior localized around the rx_u receiver
loop and the RecvError::Lagged arm, and add a regression test that intentionally
overflows the queue without yielding to verify the pump fails fast rather than
processing a partial stream.

In `@app/src-tauri/src/discord_scanner/mod.rs`:
- Around line 416-419: The pinned page fast-path in
discord_scanner::mod::find_target currently returns a page target by id even
when it no longer matches url_prefix. Update the pinned_target_id branch to also
verify the candidate target is a page under the current Discord URL prefix
before returning it, matching the same prefix constraint already enforced by the
strict/relaxed lookup paths.
- Around line 454-462: The pin update logic in discord_scanner’s strict-match
path is too restrictive because pinned_target_id.is_none() blocks refreshing an
existing stale pin even after a successful strict fragment match. Update the
pinning condition around the strict recovery branch so a new strict match can
replace an outdated pin, and keep the relaxed-match path from pinning; use the
existing is_strict check and the pinned_target_id assignment/logging in the
pinning block to locate the change.
- Around line 381-384: The idle-reconnect path in discord_scanner::mod::scan
leaves the previous CDP target session attached when pump_events_resilient
returns normally, so clean it up before the outer loop reattaches. Mirror the
DOM scan teardown pattern by detaching the current session in the
watchdog/reconnect path, using the existing session_id and cdp handling around
dispatch_event and pump_events_resilient, so stale sessions do not accumulate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81287ff1-8d96-4bc5-8bf2-4d4123cda3fe

📥 Commits

Reviewing files that changed from the base of the PR and between c2e040a and b220bd8.

📒 Files selected for processing (3)
  • app/src-tauri/src/cdp/conn.rs
  • app/src-tauri/src/discord_scanner/mod.rs
  • app/src-tauri/src/discord_scanner/mod_tests.rs

Comment thread app/src-tauri/src/cdp/conn.rs Resolved
oxoxDev added 2 commits June 25, 2026 20:38
…stream (tinyhumansai#3693)

CodeRabbit review: RecvError::Lagged means frames were already dropped before
the drain could pull, so continuing fed a partial event stream downstream with
no re-sync. The drain now forwards Result<EventFrame,String> and signals lag as
Err, so the pump returns and the outer loop re-attaches (restarts capture)
rather than processing a gap. Adds a regression test that overflows the ring
without yielding and asserts the error path. Doc updated to match.
…nnect (tinyhumansai#3693)

CodeRabbit review, three target-resolution/lifecycle fixes:
- Detach the CDP session after the pump returns, so idle/lag-forced reconnects
  don't accumulate orphaned sessions on the transport.
- Constrain the pinned-id branch to url_prefix — a pinned tab can keep its
  target id while navigating off Discord; don't keep scanning an off-prefix page.
- Re-pin on every strict fragment match (not only when unset) so a stale pin
  recovers after a renderer-swap gives a new target id, instead of being stuck
  on relaxed matching forever.
Adds an off-prefix pin-rejection test; moves the resolution-hierarchy doc onto
attach_account_target.
@senamakel
senamakel merged commit a1c5355 into tinyhumansai:main Jun 25, 2026
20 checks passed
senamakel pushed a commit to nocstah/openhuman that referenced this pull request Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discord CDP pump: add idle watchdog + handle lossy broadcast (follow-up to #3636)

2 participants