Skip to content

fix(ui): debounce sessions.list reload after sessions.changed push events#73779

Closed
fancymatt wants to merge 1 commit into
openclaw:mainfrom
fancymatt:fix/debounce-sessions-list-after-push-events
Closed

fix(ui): debounce sessions.list reload after sessions.changed push events#73779
fancymatt wants to merge 1 commit into
openclaw:mainfrom
fancymatt:fix/debounce-sessions-list-after-push-events

Conversation

@fancymatt

Copy link
Copy Markdown

Problem

The dashboard subscribes to sessions.changed push events for real-time UI updates. However, every push event also triggered an immediate full sessions.list reload.

With 97 WebSocket connections, a single session change fans out to 97 simultaneous sessions.list calls. This creates unnecessary GC pressure on the gateway and contributes to memory growth under load (~5,648 calls/day measured).

Fix

Debounce the sessions.list reload to 5 seconds after the last sessions.changed event. The incremental update from applySessionsChangedEvent keeps the UI responsive immediately; the debounced full reload reconciles any edge cases (e.g., sessions that appeared/disappeared outside the push event scope).

Impact

  • ~80-95% reduction in sessions.list calls under normal load
  • No change to real-time UI responsiveness (incremental updates unchanged)
  • No server-side changes required
  • 5s debounce is conservative; could be tuned or made configurable later

…ents

The dashboard subscribes to sessions.changed push events for real-time
UI updates. However, every push event also triggered an immediate full
sessions.list reload, causing excessive polling under load.

With 97 WebSocket connections, a single session change would fan out
to 97 simultaneous sessions.list calls. This creates unnecessary GC
pressure on the gateway and contributes to memory growth.

This change debounces the sessions.list reload to 5 seconds after the
last sessions.changed event. The incremental update from
applySessionsChangedEvent keeps the UI responsive immediately; the
debounced full reload reconciles any edge cases (e.g., sessions that
appeared/disappeared outside the push event's scope).

Impact:
- Reduces sessions.list calls by ~80-95% under normal load
- No change to real-time UI responsiveness (incremental updates unchanged)
- No server-side changes required
- 5s debounce is conservative; could be tuned or made configurable later
Copilot AI review requested due to automatic review settings April 28, 2026 19:48
@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR debounces the sessions.list full reload to 5 seconds after the last sessions.changed push event, keeping incremental UI updates immediate via applySessionsChangedEvent. The optimization is sound and well-motivated, but the existing test in app-gateway.sessions.node.test.ts was not updated and will fail because it asserts loadSessions was called synchronously — which is no longer true with the debounce in place.

Confidence Score: 3/5

Not safe to merge as-is — the existing test asserting synchronous loadSessions invocation will fail with the new debounce.

One P1 finding: the existing sessions.changed test in app-gateway.sessions.node.test.ts checks that loadSessions was called immediately, but the debounce defers the call by 5 seconds. The test will fail without fake-timer support and updated assertions. Two P2 findings (missing fixture field, timer not cleared on disconnect) are non-blocking.

ui/src/ui/app-gateway.sessions.node.test.ts — needs fake timers and assertion updates for the debounced behavior

Comments Outside Diff (2)

  1. ui/src/ui/app-gateway.sessions.node.test.ts, line 114-128 (link)

    P1 Test will fail after debounce change

    loadSessions is now scheduled 5 seconds later via setTimeout, so it won't have been called by the time expect(loadSessionsMock).toHaveBeenCalledTimes(1) runs — the assertion will see a call count of 0. The test needs fake timers (vi.useFakeTimers) and vi.advanceTimersByTime(5000) (plus sessionsRefreshTimer: null in createHost()) to match the new behavior. Without this fix the entire test suite will be broken for this file.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: ui/src/ui/app-gateway.sessions.node.test.ts
    Line: 114-128
    
    Comment:
    **Test will fail after debounce change**
    
    `loadSessions` is now scheduled 5 seconds later via `setTimeout`, so it won't have been called by the time `expect(loadSessionsMock).toHaveBeenCalledTimes(1)` runs — the assertion will see a call count of 0. The test needs fake timers (`vi.useFakeTimers`) and `vi.advanceTimersByTime(5000)` (plus `sessionsRefreshTimer: null` in `createHost()`) to match the new behavior. Without this fix the entire test suite will be broken for this file.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. ui/src/ui/app-gateway.sessions.node.test.ts, line 60-111 (link)

    P2 sessionsRefreshTimer missing from host fixture

    createHost() does not include the new sessionsRefreshTimer field. While the as unknown as cast avoids a TypeScript error, the property will be undefined in tests, and any second sessions.changed event in the same host won't be able to cancel the first timer (the != null guard silently passes, but the old timer is never cleared).

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: ui/src/ui/app-gateway.sessions.node.test.ts
    Line: 60-111
    
    Comment:
    **`sessionsRefreshTimer` missing from host fixture**
    
    `createHost()` does not include the new `sessionsRefreshTimer` field. While the `as unknown as` cast avoids a TypeScript error, the property will be `undefined` in tests, and any second `sessions.changed` event in the same host won't be able to cancel the first timer (the `!= null` guard silently passes, but the old timer is never cleared).
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: ui/src/ui/app-gateway.sessions.node.test.ts
Line: 114-128

Comment:
**Test will fail after debounce change**

`loadSessions` is now scheduled 5 seconds later via `setTimeout`, so it won't have been called by the time `expect(loadSessionsMock).toHaveBeenCalledTimes(1)` runs — the assertion will see a call count of 0. The test needs fake timers (`vi.useFakeTimers`) and `vi.advanceTimersByTime(5000)` (plus `sessionsRefreshTimer: null` in `createHost()`) to match the new behavior. Without this fix the entire test suite will be broken for this file.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: ui/src/ui/app-gateway.sessions.node.test.ts
Line: 60-111

Comment:
**`sessionsRefreshTimer` missing from host fixture**

`createHost()` does not include the new `sessionsRefreshTimer` field. While the `as unknown as` cast avoids a TypeScript error, the property will be `undefined` in tests, and any second `sessions.changed` event in the same host won't be able to cancel the first timer (the `!= null` guard silently passes, but the old timer is never cleared).

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: ui/src/ui/app.ts
Line: 565

Comment:
**Timer not cancelled on disconnect**

`sessionsRefreshTimer` is not cleared in `disconnectedCallback` / `handleDisconnected`. If the component is torn down while the 5-second timer is pending, the timer fires on the dead host. `loadSessions` does guard with `!state.client || !state.connected`, so no network call escapes, but the dangling timer could interfere with test isolation and is inconsistent with how the other polling intervals (`nodesPollInterval`, `logsPollInterval`) are cleaned up in `handleDisconnected`.

Add `clearTimeout(host.sessionsRefreshTimer ?? undefined); host.sessionsRefreshTimer = null;` to `handleDisconnected` alongside the other interval teardowns.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(ui): debounce sessions.list reload a..." | Re-trigger Greptile

Comment thread ui/src/ui/app.ts
private toolStreamById = new Map<string, ToolStreamEntry>();
private toolStreamOrder: string[] = [];
refreshSessionsAfterChat = new Set<string>();
sessionsRefreshTimer: ReturnType<typeof setTimeout> | null = null;

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.

P2 Timer not cancelled on disconnect

sessionsRefreshTimer is not cleared in disconnectedCallback / handleDisconnected. If the component is torn down while the 5-second timer is pending, the timer fires on the dead host. loadSessions does guard with !state.client || !state.connected, so no network call escapes, but the dangling timer could interfere with test isolation and is inconsistent with how the other polling intervals (nodesPollInterval, logsPollInterval) are cleaned up in handleDisconnected.

Add clearTimeout(host.sessionsRefreshTimer ?? undefined); host.sessionsRefreshTimer = null; to handleDisconnected alongside the other interval teardowns.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/app.ts
Line: 565

Comment:
**Timer not cancelled on disconnect**

`sessionsRefreshTimer` is not cleared in `disconnectedCallback` / `handleDisconnected`. If the component is torn down while the 5-second timer is pending, the timer fires on the dead host. `loadSessions` does guard with `!state.client || !state.connected`, so no network call escapes, but the dangling timer could interfere with test isolation and is inconsistent with how the other polling intervals (`nodesPollInterval`, `logsPollInterval`) are cleaned up in `handleDisconnected`.

Add `clearTimeout(host.sessionsRefreshTimer ?? undefined); host.sessionsRefreshTimer = null;` to `handleDisconnected` alongside the other interval teardowns.

How can I resolve this? If you propose a fix, please make it concise.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs changes before merge.

Summary
The PR adds Control UI timer state and debounces the full sessions.list reload for 5 seconds after sessions.changed events while preserving the immediate incremental update.

Reproducibility: yes. Current main has a source-level reproduction: dispatching any sessions.changed event reaches applySessionsChangedEvent and then immediately calls loadSessions, and the existing unit test asserts that synchronous reload behavior.

Next step before merge
This is a narrow PR-branch repair: automation can add lifecycle cleanup, fake-timer tests, fixture state, and changelog handling without changing server-side session semantics or requiring product/security judgment.

Security
Cleared: The diff is limited to Control UI timer state and event handling, with no CI, dependency, lockfile, install/build/release, package publishing, secret, downloaded artifact, or generated/vendor changes.

Review findings

  • [P2] Update the session-change test for the debounce — ui/src/ui/app-gateway.ts:741-744
  • [P2] Clear the pending sessions timer on disconnect — ui/src/ui/app.ts:565
  • [P3] Add the required changelog entry — ui/src/ui/app-gateway.ts:741-744
Review details

Best possible solution:

Request changes on this PR: keep incremental session updates immediate, debounce only the full reconciliation reload, clear pending timers during teardown, add fake-timer coverage for delayed and coalesced reloads, initialize fixtures, and add the active-version changelog entry if this remains a user-facing Control UI performance fix.

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

Yes. Current main has a source-level reproduction: dispatching any sessions.changed event reaches applySessionsChangedEvent and then immediately calls loadSessions, and the existing unit test asserts that synchronous reload behavior.

Is this the best way to solve the issue?

No, not as submitted. The debounce is a narrow maintainable direction for the reported fanout, but the patch needs lifecycle cleanup, updated tests, and changelog handling before it is the best fix.

Full review comments:

  • [P2] Update the session-change test for the debounce — ui/src/ui/app-gateway.ts:741-744
    This changes the full loadSessions reconciliation to run from a timeout, but ui/src/ui/app-gateway.sessions.node.test.ts still asserts that loadSessions is called synchronously right after sessions.changed. Add fake-timer coverage for the delayed and coalesced reload path so the test matches the new behavior.
    Confidence: 0.92
  • [P2] Clear the pending sessions timer on disconnect — ui/src/ui/app.ts:565
    The new sessionsRefreshTimer survives teardown because handleDisconnected only clears existing polling/client state. A pending timeout can later run against a disconnected host, so clear and null this timer during lifecycle cleanup.
    Confidence: 0.87
  • [P3] Add the required changelog entry — ui/src/ui/app-gateway.ts:741-744
    This is presented as a user-facing Control UI performance fix, but the PR does not update CHANGELOG.md. Add a single-line active-version entry if maintainers keep this classified as a user-visible fix/perf change.
    Confidence: 0.72

Overall correctness: patch is incorrect
Overall confidence: 0.91

Acceptance criteria:

  • pnpm test ui/src/ui/app-gateway.sessions.node.test.ts ui/src/ui/app-lifecycle.node.test.ts
  • pnpm exec oxfmt --check --threads=1 ui/src/ui/app-gateway.ts ui/src/ui/app.ts ui/src/ui/app-lifecycle.ts ui/src/ui/app-gateway.sessions.node.test.ts ui/src/ui/app-lifecycle.node.test.ts CHANGELOG.md
  • pnpm check:changed

What I checked:

  • current_main_immediate_reload: Current main applies the incremental sessions.changed update and immediately calls loadSessions, so every received push event still starts a full session-list reconciliation path. (ui/src/ui/app-gateway.ts:740, 8283c5d6cc3f)
  • existing_test_expects_sync_reload: The existing unit test dispatches a sessions.changed event and immediately expects loadSessions to have been called once; the PR's delayed timeout would make this assertion fail until timers advance. (ui/src/ui/app-gateway.sessions.node.test.ts:114, 8283c5d6cc3f)
  • current_load_sessions_only_coalesces_in_flight_loads: loadSessions coalesces only when a load is already active; it does not debounce new sessions.changed events before starting a fresh request. (ui/src/ui/controllers/sessions.ts:302, 8283c5d6cc3f)
  • disconnect_cleanup_surface: handleDisconnected clears polling, realtime, client, theme, and observer state, but the PR adds a new timeout without extending this teardown path. (ui/src/ui/app-lifecycle.ts:82, 8283c5d6cc3f)
  • review_discussion_confirms_same_blockers: Greptile, Copilot, and the existing ClawSweeper review all flagged the same required follow-ups: fake-timer test updates, timer cleanup on disconnect, and changelog handling. (e47ee943b467)

Likely related people:

  • Peter Steinberger: Current main blame and local file history attribute the affected Control UI gateway handler, lifecycle cleanup, app state, and session-event test fixture to a recent release-maintenance commit. (role: recent maintainer / likely follow-up owner; confidence: medium; commits: ad264a9f5a2d; files: ui/src/ui/app-gateway.ts, ui/src/ui/app.ts, ui/src/ui/app-lifecycle.ts)

Remaining risk / open question:

  • The 5-second debounce interval still needs normal PR validation under the repaired tests; no live load measurement was performed in this read-only review.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 8283c5d6cc3f.

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Debounces the sessions.list reload triggered by sessions.changed push events to reduce fan-out load while keeping the UI responsive via incremental updates.

Changes:

  • Added a sessionsRefreshTimer field to the app/host to track a debounced reload timer.
  • Replaced immediate loadSessions calls on sessions.changed with a 5s debounce via setTimeout.
  • Added inline documentation describing the debounce rationale.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
ui/src/ui/app.ts Adds timer state to the app so the gateway event handler can debounce reloads.
ui/src/ui/app-gateway.ts Implements the debounce logic for sessions.changed events using the new timer state.

Comment thread ui/src/ui/app-gateway.ts
Comment on lines +739 to +745
if (host.sessionsRefreshTimer != null) {
clearTimeout(host.sessionsRefreshTimer);
}
host.sessionsRefreshTimer = setTimeout(() => {
host.sessionsRefreshTimer = null;
void loadSessions(host as unknown as SessionsState);
}, 5000);
Comment thread ui/src/ui/app-gateway.ts
host.sessionsRefreshTimer = setTimeout(() => {
host.sessionsRefreshTimer = null;
void loadSessions(host as unknown as SessionsState);
}, 5000);
robbiethompson18 pushed a commit to robbiethompson18/openclaw that referenced this pull request Apr 29, 2026
Same-time PR openclaw#73779 hit the identical 5 check failures (looks like a
plugin-test isolation flake at that point in time). Pushing an empty
commit to retrigger.
@BunsDev

BunsDev commented May 11, 2026

Copy link
Copy Markdown
Contributor

Closing as superseded by #80657.

The signed replacement includes this PR's core sessions.changed fix but with the missing maintainer guards covered: incremental updates remain immediate, the full sessions.list reconciliation is debounced, the delayed callback re-checks connected/active tab state, and pending timers are cleared on disconnect and tab changes. The replacement also includes focused timer cleanup/coalescing tests and passed pnpm check:changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants