Skip to content

fix(#5551): refresh sidebar recency after missed session events - #5562

Closed
rodboev wants to merge 1 commit into
nesquena:masterfrom
rodboev:pr/5551-sidebar-recency-refresh
Closed

rodboev wants to merge 1 commit into
nesquena:masterfrom
rodboev:pr/5551-sidebar-recency-refresh

Conversation

@rodboev

@rodboev rodboev commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • The sidebar already sorts existing rows into the right order and Today bucket once /api/sessions returns fresh timestamps.
  • The stale state comes from the recovery path before sorting runs: blur and visibility close the sidebar session-events stream, then focus, visible, and reconnect recovery use ordinary unforced refresh calls.
  • The fix narrows the forced behavior to sidebar-list catch-up, leaving active transcript reloads and the [Regression] PWA: Two windows open → connection pool saturation → "Request timed out" toasts every 30s #4151 stream lifecycle boundaries unchanged.

What Changed

  • static/sessions.js: force the sidebar list refresh after sessions_changed, focus, visibility, and session-events reconnect recovery, and mark intentional session-events closes so the next open performs catch-up.
  • tests/test_webui_external_refresh_frontend.py: add regression coverage for hidden session-event and resume sidebar refresh recovery, plus the sidebar-only helper contract.
  • tests/test_issue3916_external_refresh_poll.py: keep the session-events active-refresh contract pinned while separating sidebar force from transcript refresh.
  • tests/test_issue4151_pwa_focus_sse.py: preserve the global sidebar stream focus boundary and per-session stream exclusion.

Why It Matters

When a mobile or second client updates an older conversation, the desktop sidebar should move that existing row to the top and into Today without needing a manual reload. This keeps the live session list consistent with the authoritative session metadata while preserving the connection-pool safeguards for unfocused PWA windows.

Verification

  • pytest tests/test_webui_external_refresh_frontend.py -v --timeout=60
  • pytest tests/test_issue3916_external_refresh_poll.py -v --timeout=60
  • pytest tests/test_issue4151_pwa_focus_sse.py -v --timeout=60
  • npx eslint --no-config-lookup -c eslint.runtime-guard.config.mjs "static/**/*.js"

Full-suite CI context, not a required local check: pytest tests/ -v --timeout=60.

Upstream

Closes #5551.

Model Used

GPT 5.5 via Codex CLI

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes stale sidebar recency when an unfocused window (desktop) misses session-events while a second client updates an older conversation. It narrows forced refreshes to sidebar-only catch-up, adds option merging so force and refreshActive flags survive queueing and debounce windows, and expands regression coverage across three test files.

  • static/sessions.js: Introduces _mergeSessionListRefreshOptions and _refreshSessionListAfterSidebarResume to carry force:true through the focus, visibility, and SSE-reconnect recovery paths; upgrades _scheduleSessionEventsRefresh to accumulate a pending-request object instead of a plain string; and makes sessions_changed events force-render the sidebar ({force:true, refreshActive:true}) so hidden tabs catch up on remote changes.
  • Tests: Three test files are updated to pin the new contract — test_webui_external_refresh_frontend.py adds four Node-executed harness tests covering hidden-tab forced render, inflight-resume force survival, and debounce-window option merging; the two issue-regression files update their string assertions to match the refactored function signatures.

Confidence Score: 5/5

Safe to merge — the changes are narrowly scoped to sidebar refresh plumbing, are covered by both static assertions and live Node harness tests, and preserve the existing connection-pool safeguards.

The option-merging logic is sound, the force flag propagation through the debounce and in-flight queue is consistent, and the three test files exercise the critical paths. No broken contracts or data-loss paths were identified in the changed code.

No files require special attention.

Important Files Changed

Filename Overview
static/sessions.js Adds forced sidebar refresh helpers, option-merging for pending refresh requests, and propagates force+refreshActive through the sessions_changed event path; logic is consistent and well-guarded.
tests/test_webui_external_refresh_frontend.py Adds four Node-executed harness tests that verify forced hidden-tab render, inflight-resume force survival, and timer debounce option merging; covers the new helpers exhaustively.
tests/test_issue3916_external_refresh_poll.py Updates string assertions to match the refactored _scheduleSessionEventsRefresh signature and sessions_changed call site; no logic changes, just assertion alignment.
tests/test_issue4151_pwa_focus_sse.py Adds one assertion confirming _refreshSessionListAfterSidebarResume is called in the focus hook block; preserves all existing PWA stream boundary checks.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant W as Window/Tab
    participant FV as Focus/Visibility Handler
    participant CLOSE as _closeSessionEventsSSE
    participant ENSURE as ensureSessionEventsSSE
    participant RESUME as _refreshSessionListAfterSidebarResume
    participant SCHED as _scheduleSessionEventsRefresh
    participant RSL as refreshSessionList

    Note over W,RSL: Blur / hide - tear down SSE
    W->>FV: blur or visibilitychange(hidden)
    FV->>CLOSE: _closeSessionEventsSSE()
    CLOSE-->>CLOSE: "_sessionEventsNeedsRefreshOnOpen = true"

    Note over W,RSL: Focus / show - reconnect and force catch-up
    W->>FV: focus or visibilitychange(visible)
    FV->>ENSURE: ensureSessionEventsSSE()
    ENSURE-->>ENSURE: new EventSource (onopen async)
    FV->>RESUME: _refreshSessionListAfterSidebarResume
    RESUME-->>RESUME: "_sessionEventsNeedsRefreshOnOpen = false"
    RESUME->>RSL: "refreshSessionList(reason, {force:true})"
    RSL->>RSL: "renderSessionList({deferWhileInteracting:false})"

    Note over W,RSL: SSE onopen fires - flag already cleared, no double fetch
    ENSURE-->>ENSURE: if(!_sessionEventsNeedsRefreshOnOpen) return

    Note over W,RSL: sessions_changed event
    ENSURE->>SCHED: "_scheduleSessionEventsRefresh('event', {force:true, refreshActive:true})"
    SCHED-->>SCHED: merge into _sessionEventsRefreshPendingRequest
    SCHED->>RSL: setTimeout 300ms then refreshSessionList
    RSL->>RSL: "renderSessionList({deferWhileInteracting:false})"
    RSL->>RSL: refreshActiveSessionIfExternallyUpdated
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant W as Window/Tab
    participant FV as Focus/Visibility Handler
    participant CLOSE as _closeSessionEventsSSE
    participant ENSURE as ensureSessionEventsSSE
    participant RESUME as _refreshSessionListAfterSidebarResume
    participant SCHED as _scheduleSessionEventsRefresh
    participant RSL as refreshSessionList

    Note over W,RSL: Blur / hide - tear down SSE
    W->>FV: blur or visibilitychange(hidden)
    FV->>CLOSE: _closeSessionEventsSSE()
    CLOSE-->>CLOSE: "_sessionEventsNeedsRefreshOnOpen = true"

    Note over W,RSL: Focus / show - reconnect and force catch-up
    W->>FV: focus or visibilitychange(visible)
    FV->>ENSURE: ensureSessionEventsSSE()
    ENSURE-->>ENSURE: new EventSource (onopen async)
    FV->>RESUME: _refreshSessionListAfterSidebarResume
    RESUME-->>RESUME: "_sessionEventsNeedsRefreshOnOpen = false"
    RESUME->>RSL: "refreshSessionList(reason, {force:true})"
    RSL->>RSL: "renderSessionList({deferWhileInteracting:false})"

    Note over W,RSL: SSE onopen fires - flag already cleared, no double fetch
    ENSURE-->>ENSURE: if(!_sessionEventsNeedsRefreshOnOpen) return

    Note over W,RSL: sessions_changed event
    ENSURE->>SCHED: "_scheduleSessionEventsRefresh('event', {force:true, refreshActive:true})"
    SCHED-->>SCHED: merge into _sessionEventsRefreshPendingRequest
    SCHED->>RSL: setTimeout 300ms then refreshSessionList
    RSL->>RSL: "renderSessionList({deferWhileInteracting:false})"
    RSL->>RSL: refreshActiveSessionIfExternallyUpdated
Loading

Reviews (2): Last reviewed commit: "fix(#5551): refresh sidebar recency afte..." | Re-trigger Greptile

Comment thread static/sessions.js
Comment thread tests/test_webui_external_refresh_frontend.py
@rodboev
rodboev force-pushed the pr/5551-sidebar-recency-refresh branch from 253f1d3 to e5571c2 Compare July 4, 2026 19:19
@nesquena-hermes nesquena-hermes added the size:M Medium PR (≤10 files, ≤250 LOC) label Jul 4, 2026
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

🔬 Gate certification — GREEN ✅ (sidebar recency refresh after missed SSE events)

Certified head: sha:9df45cc9 (clean rebase, branch gate-rebase/5562-sidebar-recency-refresh) · PR: #5562 · rodboev, fix(#5551): refresh sidebar recency after missed session events
Verdict: Full gate GREEN. A well-built sidebar-reliability fix — after missed SSE events (reconnect / backgrounded-tab resume) the sidebar recency ordering no longer stays stale; a forced resume-refresh with request-coalescing recovers it without a refresh stampede. Codex SAFE, suite fully green.

What I ran (rebased worktree /tmp/wt-rebase-5562)

Gate Result
Rebase onto current master git apply clean
Codex (reproduce) SAFE TO SHIP — 0 findings
Full pytest suite 12000 passed, 0 failed
refresh/poll/pwa-focus tests ✅ 31 passed

Findings

✅ Correct missed-events recovery + coalescing: _refreshSessionListAfterSidebarResume(reason) forces refreshSessionList(reason, {force:true}) on sidebar resume (after an SSE close/reconnect or backgrounded-tab resume), so the recency ordering isn't left stale (#5551). A pending-request coalescer (_sessionListRefreshPendingRequest + _mergeSessionListRefreshOptions folding refreshActive) prevents overlapping refreshes stampeding, and re-schedules a pending request after the in-flight one completes (so a needed refresh isn't dropped); _sessionEventsNeedsRefreshOnOpen is cleared when a direct resume satisfies the pending onopen catch-up (no double-refresh). Codex confirmed no dropped refresh, no stampede/infinite reschedule, no fight with the periodic poll, and no regression to #3916 (external-refresh-poll) or #4151 (pwa-focus-sse) — both touched tests pass. 31 targeted + full suite green (0 failures).

Recommendation to the next agent

Ready to merge — use branch gate-rebase/5562-sidebar-recency-refresh (sha:9df45cc9), NOT the PR's stale head e5571c23. A solid existing-flow reliability fix (sidebar recency recovers after a missed-events gap, with careful refresh-coalescing that neither drops a needed refresh nor stampedes), Codex SAFE + 31 targeted tests + full suite green (0 failures). Frontend logic — no visible-design change (just correct recency ordering after reconnect). concept 4/5 (real #5551 reliability fix). Author @rodboev (T1). crit=3.


Gate-certifier layer (warm-up → gate → release). I do not merge/tag/deploy. Rebased onto current master; verified the resume-refresh + coalescer preserves needed refreshes (re-schedules pending after in-flight, folds refreshActive), no stampede/loop, no poll fight, #3916/#4151 intact, Codex SAFE + 31 targeted + full suite green (0 failed). Cert valid for sha:9df45cc9.

@nesquena-hermes nesquena-hermes added the gate-pass Full gate passed (Codex+Opus+suite+browser); queued Tier 1 for release agent label Jul 4, 2026
nesquena-hermes added a commit that referenced this pull request Jul 4, 2026
release #5562: refresh sidebar recency after missed session events
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in v0.51.866 — thanks @rodboev! 🎉

Your sidebar recency fix (#5551) is live. A conversation reactivated from another device/tab while your current tab is backgrounded now bumps to the top / into "Today" on its own — no manual refresh needed.

Root cause was the session-list refresh coalescing preserving only the refresh reason and dropping its options, so a force refresh queued behind an in-flight one lost its force flag and got skipped while the tab was hidden. Refresh options are now preserved through coalescing, plus a resume-refresh forces a catch-up when the sidebar regains focus.

Gate (all green):

  • Codex (regression): SAFE TO SHIP, no findings.
  • Fable (UX): SHIP-UX — zero visible chrome; the tab-return double-refresh (visibilitychange + focus) coalesces into the existing render-skip no-op path → one quiet in-place re-sort, no flicker, no hidden-tab rebuild storm.
  • Full suite: 12012 passed; your frontend/poll/SSE tests 31/31.

Merged via release PR #5591 with your authorship preserved (Co-authored-by). Appreciate the careful fix on the sidebar sync path.

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

Labels

gate-pass Full gate passed (Codex+Opus+suite+browser); queued Tier 1 for release agent size:M Medium PR (≤10 files, ≤250 LOC)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(session-list): reactivated older conversation not bumped to top / into Today until manual refresh

2 participants