From 0f90600a59ace93628df9f64ec413f22bc079884 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Mon, 29 Jun 2026 23:14:42 -0400 Subject: [PATCH] =?UTF-8?q?lotus(#2):=20address=20review=20=E2=80=94=20ack?= =?UTF-8?q?=20contract,=20rate,=20param=20precedence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Document that io.lotus.call_state is request/response and the host must ack it (cinny listenAction replies {}) to avoid 10s-timeout churn (H1). - Throttle 150ms -> 250ms to reduce widget traffic (M1). - lotusParam: hash fragment wins over query, matching EC's ParamParser (L1). - Fix the misleading "opaque" id comment; id is userId:deviceId (L2). Co-Authored-By: Claude Opus 4.8 --- src/lotus/lotusActions.ts | 8 +++++++- src/lotus/lotusCallState.ts | 6 ++++-- src/lotus/lotusWidget.ts | 6 ++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/lotus/lotusActions.ts b/src/lotus/lotusActions.ts index b19e9f37..44cb4339 100644 --- a/src/lotus/lotusActions.ts +++ b/src/lotus/lotusActions.ts @@ -11,7 +11,13 @@ Please see LICENSE in the repository root for full details. * a circular dependency. */ export enum LotusWidgetActions { - /** fromWidget: in-call per-participant speaking / mute state. */ + /** + * fromWidget: in-call per-participant speaking / mute state. + * NOTE: matrix-widget-api `transport.send` is request/response — the host + * MUST reply/ack each one (cinny's `listenAction` does, replying `{}`), + * otherwise every send sits pending for the 10s transport timeout and then + * rejects, producing continuous churn + log noise for the whole call. + */ CallState = "io.lotus.call_state", /** toWidget: pin/spotlight (or clear, with userId=null) a participant. */ FocusParticipant = "io.lotus.focus_participant", diff --git a/src/lotus/lotusCallState.ts b/src/lotus/lotusCallState.ts index ee60d38e..86c7ea88 100644 --- a/src/lotus/lotusCallState.ts +++ b/src/lotus/lotusCallState.ts @@ -12,7 +12,7 @@ import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { LotusWidgetActions, lotusFlag, lotusSendToHost } from "./lotusWidget"; interface ParticipantState { - /** Opaque media id (stable per tile). */ + /** EC media id (`${userId}:${deviceId}`), stable per participant device. */ id: string; /** Matrix user id this media belongs to. */ userId: string; @@ -59,7 +59,9 @@ export function startLotusCallState(vm: CallViewModel): () => void { ), ), // `speaking` flips rapidly; cap the send rate and drop no-op repeats. - throttleTime(150, undefined, { leading: true, trailing: true }), + // 250ms is plenty for speaking rings / mute badges and keeps the + // request/response widget traffic modest. + throttleTime(250, undefined, { leading: true, trailing: true }), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), ) .subscribe((participants) => { diff --git a/src/lotus/lotusWidget.ts b/src/lotus/lotusWidget.ts index 4144b315..77d7d7ba 100644 --- a/src/lotus/lotusWidget.ts +++ b/src/lotus/lotusWidget.ts @@ -30,10 +30,12 @@ let cachedParams: URLSearchParams | undefined; */ export function lotusParam(name: string): string | null { if (!cachedParams) { - cachedParams = new URLSearchParams(window.location.search); + // Match EC's own ParamParser precedence: the hash fragment wins over the + // query string. So seed from the fragment first, then fill gaps from query. const hash = window.location.hash.replace(/^#\/?/, ""); const hashQuery = hash.includes("?") ? hash.slice(hash.indexOf("?") + 1) : ""; - for (const [k, v] of new URLSearchParams(hashQuery)) { + cachedParams = new URLSearchParams(hashQuery); + for (const [k, v] of new URLSearchParams(window.location.search)) { if (!cachedParams.has(k)) cachedParams.append(k, v); } }