ADR-007: Live-chat liveness — the indicator reads backend Availability, never mirrors or infers it
Status: Accepted — 2026-06-28 (grill-with-docs session). Root causes verified; fix scheduled. · Datum: 2026-06-28
- Status: Accepted — 2026-06-28 (grill-with-docs session). Root causes verified; fix scheduled.
- Date: 2026-06-28
- Deciders: Frank (product) + AI (engineering)
- Related:
CONTEXT-conversation-types.md(Availability / Waiting room terms),ADR-005-matrix-federation-off-dns-server-name(the badge-gating trade-off touches this),ADR-006(the modality field). Findings from the 2026-06-28 liveness verification workflow.
Context
The recurring complaint "the live chat keeps showing people as live when nobody is" is not an architecture gap, contrary to a first reading. Verification established that the backend Availability model is sound and already implements exactly the product intent (an explicit "go live" toggle, auto-off on logout, TTL-bounded):
ConsultantActivityRegistry(in-memory) holds the currently-available counsellors;CONSULTANT_AVAILABILITY_ACTIVE_WINDOW_MS = 120000(120s TTL);ConsultantActivityInterceptor.refreshIfAvailableheartbeats on every authenticated counsellor request; logout callsapiSetLiveChatAvailability(false).- This is deliberately decoupled from Matrix/RC presence, because Matrix presence is unreliable for live chat (noted in
TopicConsultantRoutingService).
The bug is that the visible liveness has three independent wrong sources, none of which read that authoritative model:
- Nav green pill — driven entirely by a
localStorageflag (caritas_liveChatAvailability,liveChatToggle.ts), a write-only mirror that re-asserts FE→BE but never reads BE→FE. When the 120s TTL drops the counsellor, the pill stays lit indefinitely (NavigationBar.tsx). - Per-session "live" badge — derived from chat type (
isAnonymousChat ? 'live' : 'nearby',SessionHeaderComponent.tsx:742), so every Live Chat session shows "live" regardless of presence. - Persisted
Session.status = ACTIVE— never reset when a live chat is abandoned; the reaper (DeactivateAnonymousUserService) only sweeps after a 6h window (deactivateworkflow.periodMinutes=360), so a dead session reads live for hours.
Separately, the Live-Chat queue uses ORDER BY s.createDate DESC in three SessionRepository queries (lines 211/229/248) — newest-first — while the ratsuchende's "people ahead of me" position counts oldest-first, so list order and position disagree (a FIFO bug).
Decision
- Availability stays backend-authoritative; the indicator becomes a READ of it, never a mirror. Add
GET /conversations/consultants/availability(self) returning{available}fromConsultantActivityRegistry.filterActive(); the nav polls it (interval < 120s TTL, e.g. 30–45s) and drives the pill from the result. Make thelocalStoragestore TTL-aware (expiry stamp; treat as OFF on an unavailable/empty poll). The FE→BE re-assert must not silently re-mark a counsellor available without intent. - The per-session badge stops keying on chat type. Gate "live" on Availability / active-session state (and never show "live" for
DONE), not onisAnonymousChat. - Abandoned live sessions get a prompt, live-chat-specific teardown — transition the owned
IN_PROGRESSsession toDONEon counsellor logout/toggle-off (reuseDeactivateSessionActionCommand), and apply a live-chat-specific timeout in minutes, separate from the 6h anonymous-user deactivation window. - Queue is true FIFO — change the three
ORDER BY createDate DESCtoASCso list order matches the position count. - Keep the waiting room decoupled — the ratsuchende continues to see only FIFO position, never counsellor Availability (see CONTEXT). None of this needs a schema change, so it does not touch the Liquibase-off hazard.
Considered options (badge gating)
- Gate the per-session badge on real-time RC/Matrix presence. Most "honest", but couples the fix to the unreliable presence layer and the ADR-005 Matrix rebuild. Rejected for now.
- Gate it on Availability / active-session state (chosen). Transport-agnostic, ships immediately, consistent with the deliberate decoupling from Matrix/RC presence.
Consequences
Positive: the green pill and per-session badge finally reflect reality; the whole fix ships now, independent of the Matrix SDK rebuild; FIFO order and position agree. Negative / cost: a poll adds light traffic (bounded by the TTL); on a backend restart the in-memory registry empties, so all counsellors read unavailable until they re-toggle — which is the correct behaviour and must not be "fixed" by auto-re-asserting from the stale FE flag.
Resolved product questions (Frank, 2026-06-28)
- Time-to-DONE for an abandoned
IN_PROGRESSlive session → 10 minutes (a live-chat-specific window, separate from the 6h anonymous-user deactivation sweep). - Idle-but-toggled-on → keep the existing ~2.5h inactivity auto-logout; the per-request heartbeat keeps an actively-working counsellor available, so the availability poll should not flip a working counsellor OFF on its own. New feature spun off (separate task / mini-spec): a pre-logout warning ~5 min before auto-logout, delivered as a notification (optionally with a distinct "logged-out" sound), offering extend buttons (+1h / +3h / max 6h). To be located in the existing auto-logout code, then built or at minimum documented.
- Badge gating → option (b): gate the per-session "live" badge on Availability / active-session state, not real RC/Matrix presence — keeps the whole fix transport-agnostic and independent of ADR-005.
- After a backend restart → force counsellors to re-toggle; do not auto-re-assert availability from the stale FE flag (that resurrects stuck-live).
The exact auto-logout mechanism (≈2.5h) is being located in code so the warning/extend feature attaches to the real implementation.
Status & progress (2026-06-30)
- Slice 1 of 4 built locally (additive, green): the self-availability READ —
GET /conversations/consultants/availabilityreturning{available}fromConsultantActivityRegistry.filterActive()— is implemented on the existingConsultantLiveAvailabilityController(no SecurityConfig change — the/conversations/consultants/**rule already covers the GET) with a 3-caseWebMvcTest(active / inactive / non-consultant), red→green. Worktreefeature/adr007-consultant-availability-readofforigin/dev, not pushed. - Held (collide with open PRs, queued): the per-session badge gating (
SessionHeaderComponent— collision-safe, next up), the nav-pill read (NavigationBar— HOLD behind #126), and the FIFOASC+ 10-min live-chat teardown (SessionRepository— HOLD behind case-handover #186). Build after those merge / rebase.
ADR-006: `conversation_type` as a persisted modality field, rolled out selector-first
Status: Accepted — 2026-06-28 (grill-with-docs session). Decision made; implementation scheduled. · Datum: 2026-06-28
ADR-008: Supervision — keep per-session agency-scoped reach; move side-channels out of the client's room
Status: Proposed — 2026-06-28 (grill-with-docs session); U25 cohort + go-live date resolved 2026-06-30 (minors are in U25 scope; go-live 2026-10-01, no 30.06 launch). The separate-room side-channel exclusion is therefore a confirmed pre-go-live blocker (with runway). Still pending: the data-protection officer's exact disclosure wording and the add-supervisor authority/consent details. · Datum: 2026-06-28