Skip to content

Commit d8c4f3b

Browse files
committed
fix(websocket): three-level fallback for keystroke_timings extraction
Iter 58 — the user reported the dashboard's 'Typing rhythm' tile still reading 0 ms after iter 41 + restart, even though composition cadence (1.29 s avg) was being recorded correctly on the same turn. Root cause: the iter 41 fix correctly routed JS-format ``iki_ms`` into the keystroke_buffer, but the buffer's first sampled keystroke event always has ``iki_ms = 0`` (no preceding keystroke). When a short message produced exactly ONE keystroke event (every 3rd keystroke is sampled — so a 3-key message lands on event #3 with keyTimings.length=2 → iki_ms is the gap between #2 and #3, OK; but when the very first sampled event is the only one, the buffer has [0]). The pre-fix server passed ``[0]`` straight to ``Pipeline._iki_stats``, which filtered the zero out and returned mean=0 — even though composition_metrics.keystroke_timings had real data right there. Three-level fallback: 1. server-side keystroke_buffer (filter to non-zero entries) 2. composition_metrics.keystroke_timings array 3. composition_metrics.mean_iki scalar (last resort: synthesize a single timing from the JS-precomputed mean) The dashboard's 'Typing rhythm' tile now never reads 0 ms when the JS client has any meaningful inter-key data — even on edge-case messages where the per-event sampler captured only zero-IKI entries.
1 parent f98c2db commit d8c4f3b

1 file changed

Lines changed: 37 additions & 19 deletions

File tree

server/websocket.py

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,42 @@ def _pick_metric(key: str, default=0):
805805
user_id,
806806
)
807807

808+
# Iter 58 — three-level fallback so the dashboard's
809+
# "Typing rhythm" tile never silently zeroes:
810+
# 1. server-side keystroke_buffer from streamed events
811+
# 2. composition_metrics.keystroke_timings array
812+
# 3. composition_metrics.mean_iki scalar (precomputed
813+
# JS-side; last-resort signal so the headline still
814+
# shows something realistic even if the timings
815+
# array got dropped at any point).
816+
buffer_timings = [
817+
float(ks.get("inter_key_interval_ms", 0))
818+
for ks in keystroke_buffer
819+
]
820+
# Filter out zeros — they're "no prior key" entries that
821+
# _iki_stats throws away anyway. We only fall back to
822+
# comp_metrics when buffer has no useful entries.
823+
buffer_timings_clean = [t for t in buffer_timings if t > 0]
824+
comp_timings = [
825+
float(t)
826+
for t in (comp_metrics.get("keystroke_timings") or [])
827+
if isinstance(t, (int, float)) and t > 0
828+
]
829+
if buffer_timings_clean:
830+
final_timings = buffer_timings
831+
elif comp_timings:
832+
final_timings = comp_timings
833+
else:
834+
# Last-resort scalar fallback: synthesize a single
835+
# timing from the precomputed mean. ``_iki_stats``
836+
# then returns this exact value with std=0.
837+
mean_iki_scalar = comp_metrics.get("mean_iki", 0)
838+
try:
839+
mean_iki_val = float(mean_iki_scalar)
840+
except (TypeError, ValueError):
841+
mean_iki_val = 0.0
842+
final_timings = [mean_iki_val] if mean_iki_val > 0 else []
843+
808844
pipeline_input = PipelineInput(
809845
user_id=user_id,
810846
session_id=session_id,
@@ -813,25 +849,7 @@ def _pick_metric(key: str, default=0):
813849
composition_time_ms=composition_ms,
814850
edit_count=edit_count,
815851
pause_before_send_ms=pause_ms,
816-
# Prefer the per-event server-side buffer (sampled
817-
# by the live JS client). Fall back to a client-
818-
# supplied keystroke_timings array on the message
819-
# frame (used by Python probes that don't stream
820-
# individual keystroke events).
821-
keystroke_timings=(
822-
[
823-
float(ks.get("inter_key_interval_ms", 0))
824-
for ks in keystroke_buffer
825-
]
826-
if keystroke_buffer
827-
else [
828-
float(t)
829-
for t in (
830-
comp_metrics.get("keystroke_timings") or []
831-
)
832-
if isinstance(t, (int, float))
833-
]
834-
),
852+
keystroke_timings=final_timings,
835853
prosody_features=prosody_features_dict,
836854
gaze_features=gaze_features_dict,
837855
)

0 commit comments

Comments
 (0)