fix(frontend): reconcile activity indicator stuck-ON latch across sessions (lr-96e7da) - #398
Conversation
…hoke point (lr-96e7da) Server-side session-scoping backstop for the client's activity-indicator latch. status/done/auth_required broadcasts carried no session-identifying field, unlike the existing scheduled_message_* pattern, so a client had no way to reject an edge meant for a different session. Stamped centrally in sessions.js's doSendToSession/doSendAndRecord (and reused by project.js's own sendToSession/sendToSessionOthers) rather than at each of the ~15 individual send call sites.
…ion_switched/status (lr-96e7da) The fresh-connect/reconnect hydration path's session_switched send omitted isProcessing, unlike switchSession()'s own send — a client hydrating via this path never received the authoritative reconciled activity state on the message it uses to derive the footer. Also stamps localId on the trailing status:"processing" hydration send so the client can session-scope it, matching the choke-point stamping added for the other send paths.
Extracts two DOM-free decision functions used by the activity-indicator stuck-ON fix: shouldApplyActivityEdge (session-scoping predicate for status/done/auth_required) and createActivityStaleBackstop (one-shot, never-stacking staleness timer state machine). Carved out because app-favicon.js/app-messages.js's import graph is not importable in a plain Node test process (a pre-existing circular-import ordering hazard between theme.js and markdown.js), mirroring why activity-state.js was carved out of the sidebar/hub render sites for the same reason.
…and reconcile on session_switched (lr-96e7da) Client-side half of the activity-indicator stuck-ON fix. The status/done/auth_required handlers wrote the global 'processing' latch unconditionally regardless of which session a message belonged to -- a background session's status:"processing" could raise the focused session's dot with no matching done ever routed back to clear it. Guards now use activity-latch.js's shouldApplyActivityEdge, mirroring the existing lr-0827ba scheduled_message_* pattern. session_switched also now reconciles the footer's 'processing' field from the authoritative sessionIsProcessing snapshot on every switch/reconnect/hydration, closing the gap where connect/reconnect never corrected a stale latch.
…oter (lr-96e7da) Mirrors sdk-bridge.js's server-side ACTIVITY_STALE_MS (5 min) sweep, which sweeps only the (already-correct) server registry and can never repair this client-local latch. A single timer arms on the 0->1 'processing' transition and disarms on 1->0 -- never a recurring poll, never more than one in-flight timer. On fire, it re-sends the existing switch_session request for the focused session, which the server answers with a fresh session_switched carrying authoritative isProcessing; the session_switched reconciliation added in the prior commit then applies it. Backstop for a trigger this fix's session-scoping/reconciliation did not anticipate, not a substitute for them.
…-96e7da) Replaces the CI blind spot in the predecessor stuck-OFF fix's test suite, which proved only that store.subscribe(['processing']) exists via static source-text regex -- never driving a real transition, never asserting the widget clears, never exercising a missed-edge or cross-session scenario. This file drives real state through the frontend store.js + the new pure activity-latch.js module: status:processing -> done removes the widget; a cross-session done leaves the focused session's widget unaffected; a cross-session status:processing does not raise the focused session's widget. Also covers the staleness-backstop timer's arm/clear/one-shot bound, and source-inspects (the correct/limited grep use per this suite's convention) that the real DOM-driving files call into the proven pure functions rather than reimplementing the guard inline.
|
BOBBIE — clean Audited PR #398 (fix/lr-96e7da-activity-stuck-on -> main), base d14992b, head e451f29. 7 files changed, +646/-7, matches reported scope. Specific exposure checks (per dispatch):
package.json / package-lock.json: no delta in base..head. bobbie.dep.* not applicable to this PR. Scanners: gitleaks (0 leaks, 6 commits scanned), trufflehog (0 verified/unverified secrets, 27 chunks/38640 bytes), semgrep --config=auto (14 findings, all outside this PR diff hunks or false positives on trusted-literal input, e.g. test/activity-latch-lr-96e7da.test.js:77 path.join uses only hardcoded literal rel args from this same file), osv-scanner skipped (no lockfile delta). No findings meet the Pre-Report Gate. review.status: clean. |
|
PEACHES — clean
|
|
Merged via clagentic-loadout v0.2.0
|
What changed
Fix for lr-96e7da (MILLER diagnosis): activity indicator STUCK-ON, the inverse failure of lr-6e20f7 stuck-OFF. store.processing (the field app-favicon.js initActivityFooter subscribes to) was a client-local edge latch with no server reconciliation. Missing a single 1-\u003e0 done edge left the footer widget stuck on forever.
All four items from MILLER hard evidence fix class, one PR:
(a) SESSION-SCOPE the latch writers. app-messages.js status/done/auth_required handlers now guard on shouldApplyActivityEdge(msg.localId, activeSessionId), mirroring the existing lr-0827ba scheduled_message_* pattern. Server-side, localId is stamped centrally at the sessions.js doSendToSession/doSendAndRecord choke point (reused by project.js sendToSession/sendToSessionOthers) instead of at each of the ~15 individual send call sites -- reuse-first, one place, not per-call-site duplication.
(b) RECONCILE on snapshot. project-connection.js hydration path session_switched previously omitted isProcessing (sessions.js own switchSession() send already had it -- this was the smoking gun MILLER cited). Now both server send sites carry it. Client session_switched handler now reconciles store.processing from the authoritative sessionIsProcessing snapshot on every switch/reconnect/hydration, not only on live status/done edges.
(c) CLIENT-SIDE STALENESS BACKSTOP. A single one-shot timer (mirrors sdk-bridge.js ACTIVITY_STALE_MS = 5 min) arms on the 0-\u003e1 processing transition and disarms on 1-\u003e0 -- never a recurring poll, never more than one in-flight timer regardless of turn count. On fire it re-sends the EXISTING switch_session request (same message every sidebar click already sends), which the server answers with a fresh session_switched carrying authoritative isProcessing; item (b) reconciliation then applies it. Bound named explicitly: at most one re-request per 5-minute window per session-focus-duration, zero new wire messages, zero polling.
(d) CI: added test/activity-latch-lr-96e7da.test.js with EXECUTED behavioral transitions (not a regex) driving status:processing -\u003e done and asserting the widget is removed, plus a cross-session done leaving the focused session widget unaffected, plus a cross-session status:processing not raising the focused widget. This directly replaces the blind spot MILLER named at activity-transcript-footer-lr-6e20f7.test.js:157 (a static source-text regex that only proved the wire exists, never that it converges to false). Because app-favicon.js/app-messages.js are DOM-heavy and unimportable in a plain Node test process (a pre-existing circular-import ordering hazard between theme.js and markdown.js, unrelated to this fix and out of scope to repair), the pure decision logic (session-scoping predicate + staleness-timer state machine) was extracted into a new DOM-free module, lib/public/modules/activity-latch.js, mirroring why activity-state.js was carved out of the sidebar/hub render sites for the same reason. Source-inspection tests confirm the real DOM-driving files call into the proven pure functions rather than reimplementing the guard inline. Did NOT modify lib/session-activity.js -- the server registry is untouched, confirmed correct by MILLER.
Highest-risk item: two PRE-EXISTING test files needed source changes to accommodate the fix (not to make failing assertions pass -- both were correctly asserting the pre-existing shape and my diff legitimately changed that shape)
Neither existing invariant was touched, loosened, or had its pattern relaxed. Both failures were my code needing to fit the existing correct shape, not the other way around.
Trade-off named explicitly (chattiness bound)
Item (c) staleness backstop is bounded: one-shot timer, re-arms only on a fresh 0-\u003e1 transition (a genuinely new turn), never a fixed-interval poll. At most one switch_session re-request per ACTIVITY_STALE_BACKSTOP_MS (5 min) per session-focus-duration. This reuses the EXISTING switch_session wire message (same one every sidebar click sends) rather than adding a new message type, so it introduces zero new O(sessions x clients) broadcast surface -- it is a client-initiated single targeted request, not a server broadcast, so it does not interact with lr-9bcd7b transition-only broadcast bound at all.
Tests
npm test: 1429 total (varies run-to-run per project convention), 1428 pass, 0 fail, 1 skipped (test/session pre-existing flaky-under-load skip, unrelated to this diff, present on unmodified main). New file test/activity-latch-lr-96e7da.test.js confirmed EXECUTED (not silently dropped) -- all 16 of its tests appear in the TAP log with individual ok lines, and npm test exit code 0 confirms check-test-count.js per-file completion gate passed. Confirmed the new behavioral tests actually fail on unmodified pre-fix source (verified via git stash before finalizing).
TASK: lr-96e7da