diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index 288331ae..78fca142 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -259,7 +259,7 @@ def __init__(self, llm_config: LlmConfig) -> None: # (estimated_tokens, iso_ts) at anchor-loss warning time, so the # estimator drift of the anchor-less window can be measured when # the provider re-anchors (countable metric, not just a log line). - self._missing_anchor_est: dict[str, tuple[int, str]] = {} + self._missing_anchor_est: dict[str, tuple[int, str, str]] = {} # Rant 2026-08-23T13:54:14: per-session dynamic-context snapshot # (session_id → (text, injected_ms)) so unchanged context is not # re-sent (context_refresh_interval_ms gating). @@ -3023,12 +3023,18 @@ def _warn_missing_usage_anchor( return # already warned once for this session self._warned_missing_usage_anchor.add(session.session_id) loss_ts = datetime.now().astimezone().isoformat() - self._missing_anchor_est[session.session_id] = (estimated, loss_ts) + # Issue #1011 (Dev.to post-3, heinrichneb): record WHICH provider went + # silent so cross-provider loss windows are attributable, not just + # countable — "a counter that can't say WHICH provider went silent is + # half a counter". The provider at loss time is stored so the drift + # measurement (possibly after a switch) can attribute both ends. + self._missing_anchor_est[session.session_id] = (estimated, loss_ts, self.llm.config.model) try: _append_usage_anchor_event({ "type": "anchor_loss", "timestamp": loss_ts, "session": session.session_id, + "provider": self.llm.config.model, "est": estimated, }) except OSError as exc: @@ -3055,7 +3061,7 @@ def _record_anchor_drift(self, session, real_pt: int) -> None: stored = self._missing_anchor_est.pop(session.session_id, None) if not stored: return - est_before, loss_ts = stored + est_before, loss_ts, provider_at_loss = stored try: _append_usage_anchor_event({ "type": "anchor_drift", @@ -3064,6 +3070,11 @@ def _record_anchor_drift(self, session, real_pt: int) -> None: "real_after": real_pt, "delta": real_pt - est_before, "loss_ts": loss_ts, + # Issue #1011: attribute both ends of a cross-provider loss + # window (a mid-session switch deliberately drops anchors per + # #1003, so provider_at_loss may differ from provider_after). + "provider_at_loss": provider_at_loss, + "provider_after": self.llm.config.model, }) except OSError as exc: logger.warning("usage-anchor stats append failed: %s", exc) diff --git a/tests/test_daemon.py b/tests/test_daemon.py index b7087c6c..cef013fd 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -613,7 +613,7 @@ def test_invalidate_usage_anchors_on_switch(): server = _make_server() server._usage_anchors["s1"] = (222_000, 148_000) server._usage_anchors["s2"] = (100_000, 60_000) - server._missing_anchor_est["s1"] = (150_000, "2026-08-26T00:00:00+08:00") + server._missing_anchor_est["s1"] = (150_000, "2026-08-26T00:00:00+08:00", "gpt-4o-mini") server._invalidate_usage_anchors_on_switch() @@ -710,7 +710,10 @@ def test_usage_anchor_loss_event_is_countable(tmp_path, monkeypatch): e1, e2 = json.loads(lines[0]), json.loads(lines[1]) assert e1["type"] == "anchor_loss" and e1["session"] == "s1" assert e1["total"] == 1 and "timestamp" in e1 + # Issue #1011: the loss event must name the provider that went silent + assert e1["provider"] == "gpt-4o-mini" assert e2["session"] == "s2" and e2["est"] == 60_000 and e2["total"] == 2 + assert e2["provider"] == "gpt-4o-mini" def test_usage_anchor_stats_survive_restart(tmp_path, monkeypatch): @@ -759,6 +762,10 @@ def test_usage_anchor_drift_measured_on_reanchor(tmp_path, monkeypatch): assert drift["real_after"] == 180_000 assert drift["delta"] == 80_000 assert drift["loss_ts"] == loss["timestamp"] + # Issue #1011: both ends of the loss window carry provider identity + assert loss["provider"] == "gpt-4o-mini" + assert drift["provider_at_loss"] == "gpt-4o-mini" + assert drift["provider_after"] == "gpt-4o-mini" # Re-anchor measured once — a second call (no pending loss) is a no-op server._record_anchor_drift(_SidSession(sid), 200_000)