fix(chat-summary-history): join summary-batch-heartbeat protocol to dedupe polling (#334)#371
Conversation
✅ Phase 5 Runtime Verification — Two-Gate Compliance ConfirmedPer project rule Must Reproduce Every Issue (PRE-FIX repro + POST-FIX verify in real browser are both mandatory before PR), this PR has both gates documented in the PR body's "Test Plan → Real-browser runtime verification" section: ✅ Gate 1 — PRE-FIX repro (real browser)Test fixture:
✅ Gate 2 — POST-FIX verify (real browser, same setup)Same fixture, same UI state, same 60 s window — fix applied:
Net: −12 requests / 60 s (−28%) — (Full data + timeline + XHR hook installation are in the PR body's Test Plan section.) |
…glamp-OSS#334) Subset check shared by all summary-batch-heartbeat consumers. A heartbeat is treated as covering iff every id in the listener's active set is in the broadcast payload — partial-overlap payloads must not suppress polling, or the uncovered ids would never receive status updates. Used in the next commit to bring ChatSummaryHistory into the existing heartbeat protocol. Refs Mininglamp-OSS#334
…istener (Mininglamp-OSS#334) Add a window 'summary-batch-heartbeat' listener that flips an in-instance 'peerActive' flag with a 15s freshness window when the broadcast covers our entire active-task set. The existing 5s tick now consults this flag and skips a tick (without stopping the timer) when a covering peer poll is fresh. Subset semantics (containsAllTaskIds): partial-overlap or disjoint payloads must not suppress — the uncovered ids would otherwise never receive status updates. Refs Mininglamp-OSS#334
… poll (Mininglamp-OSS#334) After every successful batchStatus call, broadcast a heartbeat carrying our own active-task ids. This lets peers in the same protocol — SummaryListPage, SummaryDetailPage's fallback, or another ChatSummaryHistory instance — defer their own tick symmetrically when we are already covering their interest set. Mirrors SummaryListPage.doBatchPoll's existing dispatch (line 152) with the same event name and payload shape. Refs Mininglamp-OSS#334
…ry-list-unmount (Mininglamp-OSS#334) * summary-status-change: refresh history from server when a peer surfaces a status flip for any currently-visible item. Ignored when taskIds do not intersect — avoids unrelated-channel refreshes. * summary-list-unmount: clear peerActive suppression so the next 5s tick polls — we may now be the only poller covering our active set. Handler named handleStatusChangeEvent to mirror SummaryDetailPage and to avoid collision with SummaryListPage.handleStatusChange (a different class's filter-dropdown callback). Closes the listener half of the protocol (Task 2 already added the batch-heartbeat listener; Task 3 added the dispatcher). Refs Mininglamp-OSS#334
…ininglamp-OSS#334) When the conversation summary panel switches to detail view it passes paused=true to ChatSummaryHistory, which stops the 5s tick via the existing maybeStartPoll guard. The new handleStatusChangeEvent handler (commit fb5b095) did not honor paused — a peer's status-change event would still trigger a full loadHistory network round-trip + setState even though the list is hidden behind the detail view. Mirror maybeStartPoll's paused guard at the top of the handler so a hidden panel does no fetches. Refs Mininglamp-OSS#334
…t consumers (Mininglamp-OSS#334) The 15s freshness window for the summary-batch-heartbeat protocol now lives once in utils/heartbeatCoverage.ts and is imported by both ChatSummaryHistory (added in this PR) and SummaryDetailPage (where it was inline as a bare 15000 literal). Tuning the window touches one site. Pure refactor: no behavior change. Test suite (198 total) unchanged. Refs Mininglamp-OSS#334
…glamp-OSS#334) doPoll's comment now references handleBatchHeartbeat's authoritative explanation instead of restating it. Pure documentation cleanup; no behavior change. Refs Mininglamp-OSS#334
…SS#334) The 5 new heartbeat tests with identical setup now use a single helper. Reduces the file's await-act boilerplate count without changing test semantics — assertions and mocks per-test still live inline. Helper opts in via the paused param when needed. Refs Mininglamp-OSS#334 Gate-1-Verified: PRE-FIX 51 batch-status req/73s (2s+5s interleave) — see PR Mininglamp-OSS#371 comment Gate-2-Verified: POST-FIX 38 req/75s pure-2s, ChatSummaryHistory 5s tick fully suppressed — see PR Mininglamp-OSS#371 comment
d83c617 to
57b0a55
Compare
|
This pull request has been automatically marked as stale due to inactivity. Please add an update or it will be closed. |
QA Verdict: PASSEmbedded qa-engineer persona (review-lead, tick Coverage
Edge / boundary
Flakiness risk
Verify completeness (real-browser evidence)
Existing-suite delta
Risk noted (not a block)
VerdictPASS — comprehensive unit coverage on both the helper and the protocol integration, an explicit regression test for the adversarial finding (self-echo loop), real-browser pre/post measurement that confirms the intended cadence drop, and no test edits to pre-existing files (additive only). The sprint-board CI red is an unrelated process gate. — qa persona drain complete (case-A first-fanout, 1/3). |
lml2468
left a comment
There was a problem hiding this comment.
QA Verdict: PASS — see verdict comment above for coverage, edge-case, flakiness, real-browser evidence, and the unrelated check-sprint sprint-board CI red note. (qa-engineer persona, tick bb5bd690)
Security Verdict: CLEAREDEmbedded security-engineer persona (review-lead, tick Attack surface delta
STRIDE
Input validation
Code hygiene flags
Risk
VerdictCLEARED — no auth/crypto/SBOM/network-surface delta; same-origin event bus only; defensive — security persona drain complete (case-A first-fanout, 2/3). |
lml2468
left a comment
There was a problem hiding this comment.
Security Verdict: CLEARED — see verdict comment for STRIDE walkthrough, attack-surface delta (no new HTTP / dep / auth / crypto), and the same-origin DoS bound. (security-engineer persona, tick bb5bd690)
Code Review Verdict: APPROVEDEmbedded code-reviewer persona (review-lead, tick Correctness
Readability & maintainability
Design fit
Findings
Process note (not a code finding)
VerdictAPPROVED — minimal, well-targeted, evidence-backed (real-browser pre/post pollers + adversarial verify caught self-echo trap before any code shipped). Comment density tracks complexity; tests track the regression story; design intentionally stops short of Approach C and points to its follow-up issue. — code persona drain complete (case-A first-fanout, 3/3). |
lml2468
left a comment
There was a problem hiding this comment.
Code Review Verdict: APPROVED — see verdict comment for correctness walk-through, readability/design fit, and 2 nits (constant extraction / unmount tidy-up, both cosmetic). Findings: 0 Must / 0 Should / 2 Nit. (code-reviewer persona, tick bb5bd690)
Aggregate Verdict: APPROVEDThree reviewer personas have completed their drain. All clear.
Next step: awaiting human merge. The 🤖 Aggregated by review-lead ( |
Summary
Fixes #334 —
POST /summary/api/v1/summaries/batch-statusis hit redundantly when the conversation sidebar's summary history is shown after the standalone Smart Summary tab has been visited. The reporter (jeff-wilson2010) traced the root cause to three independent pollers running with no cross-coordination; the maintainer triage (lml2468) confirmed the diagnosis and recommended #2 minimal patch — this PR — with #1 root-fix as a follow-up (now filed as #370).Root Cause (verified)
The codebase already has a 3-event
window.dispatchEventheartbeat protocol coordinatingSummaryListPage(dispatcher) andSummaryDetailPage(listener):summary-batch-heartbeat—SummaryListPage:152dispatches per poll;SummaryDetailPage:120listens → stops 15s fallback.summary-status-change— same, dispatched on status change → reload detail.summary-list-unmount—SummaryListPage:84on cleanup → restart fallback if needed.ChatSummaryHistory(sidebar 5s poller) is completely outside this protocol:When the user has visited the Smart Summary tab once,
MainkeepsSummaryListPagemounted underdisplay:noneand its 2 s timer continues polling. Opening the sidebar startsChatSummaryHistory's 5 s timer on the sametaskIds. The two are mutually unaware — same task IDs hit at both 2 s and 5 s cadence.Approach (Minimal — Approach A per spec)
Bring
ChatSummaryHistoryinto the existing 3-event protocol:summary-batch-heartbeat, if payload'staskIds⊇ our active set (subset check), setpeerActive=truewith a 15 s freshness window. The 5 s tick consults this flag and skips a tick without stopping the timer.summary-batch-heartbeatwith ourtaskIds, so peers can defer to us symmetrically.summary-status-changelistener refreshesloadHistory()when payloadtaskIdsintersects current items; ignored otherwise.summary-list-unmountlistener clearspeerActiveso the next tick polls.window.dispatchEventis synchronous, so our own dispatch re-enters our own listener. A transientisDispatchingOwnHeartbeatflag indoPoll(set true before dispatch, cleared infinally) suppresses the re-entry — without it, solo polling silently degrades from 5 s → ~15 s (caught in Phase 3.5 adversarial verify before any code was written).Subset semantics (in
containsAllTaskIdshelper): we skip ONLY when every one of our active IDs is in the heartbeat payload. Partial-overlap payloads must not suppress, or the uncovered IDs would never get a status update.Existing coordination preserved:
ChatSummaryHistory.pausedprop (sibling coordination within the sameChatSummaryPanel),chat-summary-created/chat-summary-deletedhandlers, all existing pre-existing tests — untouched.Files Changed
Commit history (9 commits, fix → refactor cleanup):
Test Plan
Unit tests
containsAllTaskIdshelper — 7/7 pass (covers superset, equal, partial-overlap, disjoint, empty-active triplet, empty/undefined-payload-with-active, large-payload O(1) lookup).ChatSummaryHistoryheartbeat coordination — 9 new cases:summary-status-changerefreshes when payload intersectssummary-status-changeignored when no intersectionsummary-list-unmountclears suppressiondoes not fetch on summary-status-change while pausedfrom quality-reviewer follow-up)dmworksummarysuite: 198 total / 194 pass / 4 pre-existing failures (no delta frommainbaseline — failures live in an unrelatedObject.defineProperty(window, 'innerWidth', ...)test file).Real-browser runtime verification
Local dev (
octo-devscript), setup:test_user_a+test_user_b, made them mutual friends via DB, created SpaceTestSpace334, created GroupTestGroup334(members: both users), and INSERT'd one summary task withstatus=WAITING_CONFIRM(long-lived active state —workerdoesn't process it without user confirmation, so the polling window is bounded only byconfirm_deadline).test_user_a→ visited Smart Summary tab (mountsSummaryListPage, starts 2 s poll) → switched back to chat → openedTestGroup334→ opened sidebar 智能总结 panel (mountsChatSummaryHistory, would start 5 s poll).XMLHttpRequest.prototype.open/sendhook in the page console to count/summaries/batch-statusrequests over a 60 s window.git checkout origin/main -- ChatSummaryHistory.tsx)The 60s post-fix figure (~30) exactly matches the theoretical SummaryListPage-only cadence (60 / 2 = 30 ticks). The pre-fix figure (~42) matches the theoretical sum (30 + 12 = 42, where 12 = 60/5).
No regression scenarios (also verified in unit tests):
paused === truewhile a status-change event arrives → no fetch (covered bydoes not fetch on summary-status-change while paused).summary-list-unmountarrives → next 5s tick polls normally (covered).Code review
superpowers:subagent-driven-developmentper task: implementer → spec reviewer → code-quality reviewer (5 tasks total, all pass)./simplify4 reviewers (reuse / simplification / efficiency / altitude) in parallel; applied:COVERING_HEARTBEAT_WINDOW_MSfromutils/heartbeatCoverage.tsand replacedSummaryDetailPage's inline15000literal so the protocol's freshness window lives in one place.mountHistory()test helper to deduperender + advanceTimersboilerplate across the new heartbeat tests.doPollto a one-line reference to the authoritative explanation onhandleBatchHeartbeat.canCancel/isInFlightconsolidation belongs to its own cleanup).Adversarial Verification (Phase 3.5)
Before any subagent dispatch, ran a plan-stage adversarial check (Critical Rule 5 Layer 1) — caught one would-have-shipped regression:
Fix patched into plan + added a dedicated regression test (
does not self-suppress: solo polling stays at 5s cadence) before any implementation work — see commitafda5a31's tests and theisDispatchingOwnHeartbeatfield inb3053db3.Risks & Mitigations
containsAllTaskIdsuses aSetfor O(1) payload lookup; tested at scale (5000 elements).paused+ listener interaction (setState while invisible)pausedguard added at top ofhandleStatusChangeEvent(commitaa8f12ad);handleBatchHeartbeatonly flips instance fields, never setState.getActiveTaskIds(); a no-longer-covering peer stops being suppressive within at most 15 s.window.dispatchEventis synchronous (self-echo)isDispatchingOwnHeartbeatflag indoPoll(try/finally) — listener returns early during own dispatch. Tested.Out-of-scope Follow-ups (each tracked separately)
summaryPollingServicesingleton (Approach C / "proper root fix" per maintainer triage).isInFlight(status)predicate — thePENDING || WAITING_CONFIRM || PROCESSINGtriple is inlined in 4 sites across this package (SummaryListPage.tsx:117-123,:132-138,SummaryDetailPage.tsx:182-184, and the newChatSummaryHistory.tsx:63-71); consolidating via the existingcanCancel()helper or a new alias is a separate clean-up. (Out of scope to avoid scope-creep beyond bug: sidebar smart-summary batch-status duplicate polling (uncoordinated multi-poller) #334.)Rollback
git revert d83c617c 9eb00b00 bba604d2 aa8f12ad fb5b0958 afda5a31 b3053db3 658d6030 # Spec doc (9ddebcd7) is harmless prose; can be kept or reverted independently.Closes #334
🤖 Generated with Claude Code