From 690b6668878721914e3d9977799230faf6eb7a74 Mon Sep 17 00:00:00 2001 From: Michael Lam Date: Wed, 13 May 2026 07:42:33 -0700 Subject: [PATCH 01/11] Add OpenRouter cost history backend --- api/providers.py | 249 ++++++++++++++ api/routes.py | 12 +- tests/test_provider_cost_history.py | 484 ++++++++++++++++++++++++++++ 3 files changed, 744 insertions(+), 1 deletion(-) create mode 100644 tests/test_provider_cost_history.py diff --git a/api/providers.py b/api/providers.py index 86a31155..b3ddb4d1 100644 --- a/api/providers.py +++ b/api/providers.py @@ -1441,6 +1441,255 @@ def _provider_is_oauth(provider_id: str) -> bool: return provider_id in _OAUTH_PROVIDERS +# ── OpenRouter cost-history snapshot helpers (#692) ────────────────────────── + +_COST_SNAPSHOTS_DIR_NAME = "cost-snapshots" +_COST_SNAPSHOT_MAX_DAYS = 365 # hard cap to prevent unbounded growth + + +def _cost_snapshots_dir() -> Path: + """Return the directory for cost-snapshot JSON files. + + Uses the Hermes home directory (profile-aware) so snapshots are + isolated per profile, matching the existing STATE_DIR convention. + """ + return _get_hermes_home() / _COST_SNAPSHOTS_DIR_NAME + + +def _fetch_openrouter_key_usage(api_key: str) -> dict[str, Any] | None: + """Fetch current usage/limit from the OpenRouter ``/auth/key`` endpoint. + + Returns a dict with ``usage``, ``limit``, ``label`` on success, or + ``None`` on any failure. Never raises; callers handle the None case. + """ + req = urllib.request.Request( + _OPENROUTER_KEY_URL, + headers={ + "Authorization": f"Bearer {api_key}", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=_PROVIDER_QUOTA_TIMEOUT_SECONDS) as resp: + raw = resp.read() + payload = json.loads(raw.decode("utf-8") if isinstance(raw, (bytes, bytearray)) else raw) + sanitized = _sanitize_openrouter_quota(payload) + label = None + if isinstance(payload, dict): + data = payload.get("data", payload) + if isinstance(data, dict): + label = str(data.get("label") or "").strip() or None + return { + "usage": sanitized.get("usage"), + "limit": sanitized.get("limit"), + "label": label, + } + except Exception: + logger.debug("OpenRouter key usage fetch failed for cost-history", exc_info=True) + return None + + +def _read_cost_snapshots(provider: str) -> list[dict[str, Any]]: + """Read persisted daily snapshots for *provider* from disk. + + Returns a list of ``{date, used, limit}`` dicts sorted by date + ascending. Returns an empty list if the file does not exist or is + corrupt. + """ + path = _cost_snapshots_dir() / f"{provider}.json" + if not path.exists(): + return [] + try: + raw = path.read_text(encoding="utf-8") + data = json.loads(raw) + except (OSError, json.JSONDecodeError, ValueError): + return [] + if not isinstance(data, dict): + return [] + snapshots = data.get("snapshots") + if not isinstance(snapshots, list): + return [] + # Validate and sort + valid = [] + for entry in snapshots: + if not isinstance(entry, dict): + continue + date = str(entry.get("date") or "").strip() + if not date: + continue + valid.append({ + "date": date, + "used": _quota_number(entry.get("used")), + "limit": _quota_number(entry.get("limit")), + }) + valid.sort(key=lambda e: e["date"]) + return valid + + +def _write_cost_snapshots(provider: str, snapshots: list[dict[str, Any]]) -> None: + """Persist daily snapshots for *provider* to disk atomically.""" + snap_dir = _cost_snapshots_dir() + snap_dir.mkdir(parents=True, exist_ok=True) + path = snap_dir / f"{provider}.json" + payload = {"provider": provider, "snapshots": snapshots} + body = json.dumps(payload, ensure_ascii=False, indent=2) + import tempfile as _tempfile + _tmp_fd, _tmp_path = _tempfile.mkstemp( + dir=str(snap_dir), prefix=f".{provider}_", suffix=".tmp" + ) + try: + with os.fdopen(_tmp_fd, "w", encoding="utf-8") as _f: + _f.write(body) + _f.flush() + os.fsync(_f.fileno()) + os.replace(_tmp_path, path) + except BaseException: + try: + os.unlink(_tmp_path) + except OSError: + pass + raise + + +def _append_cost_snapshot(provider: str, usage: int | float | None, limit: int | float | None) -> list[dict[str, Any]]: + """Append today's snapshot and return the updated list. + + If a snapshot for today already exists it is updated in-place so + repeated calls within the same day are idempotent. + """ + today = datetime.now(timezone.utc).strftime("%Y-%m-%d") + snapshots = _read_cost_snapshots(provider) + # Update or append today's entry + updated = False + for entry in snapshots: + if entry["date"] == today: + entry["used"] = usage + entry["limit"] = limit + updated = True + break + if not updated: + snapshots.append({"date": today, "used": usage, "limit": limit}) + snapshots.sort(key=lambda e: e["date"]) + # Cap to _COST_SNAPSHOT_MAX_DAYS entries (keep most recent) + if len(snapshots) > _COST_SNAPSHOT_MAX_DAYS: + snapshots = snapshots[-_COST_SNAPSHOT_MAX_DAYS:] + _write_cost_snapshots(provider, snapshots) + return snapshots + + +def _compute_deltas(snapshots: list[dict[str, Any]], window_days: int) -> list[dict[str, Any]]: + """Compute daily deltas from cumulative usage snapshots. + + Each snapshot carries cumulative ``used``; the delta for a day is + the difference between that day's cumulative value and the previous + day's. The oldest day in the window has ``delta=None`` (no + previous baseline). + """ + # Take only the last *window_days* entries + window = snapshots[-window_days:] if len(snapshots) > window_days else list(snapshots) + result: list[dict[str, Any]] = [] + for i, entry in enumerate(window): + delta = None + if i > 0 and entry.get("used") is not None and window[i - 1].get("used") is not None: + delta = float(entry["used"]) - float(window[i - 1]["used"]) + # Rounding: avoid -0.0 and tiny floating-point noise + if abs(delta) < 1e-9: + delta = 0.0 + else: + delta = round(delta, 6) + result.append({ + "date": entry["date"], + "used": entry.get("used"), + "delta": delta, + }) + return result + + +def get_provider_cost_history(provider_id: str | None = None, days: int = 7) -> dict[str, Any]: + """Return daily cost-history snapshots with deltas for a provider. + + Currently only ``openrouter`` is supported. On each call the + endpoint fetches the current cumulative usage from the OpenRouter + ``/auth/key`` endpoint, appends/updates today's snapshot, and + returns the last *days* snapshots with per-day deltas. + + Returns a dict matching the existing API style (``ok``, ``provider``, + ``status``, ``message``, …). + """ + provider = (provider_id or "").strip().lower() + if not provider: + return { + "ok": False, + "provider": None, + "status": "missing_provider", + "message": "Provider parameter is required. Use ?provider=openrouter", + } + + if provider != "openrouter": + display_name = _PROVIDER_DISPLAY.get(provider, provider.replace("-", " ").title()) + return { + "ok": False, + "provider": provider, + "display_name": display_name, + "supported": False, + "status": "unsupported", + "message": f"Cost history is not available for {display_name}. Only openrouter is supported in this release.", + } + + display_name = _PROVIDER_DISPLAY.get("openrouter", "OpenRouter") + api_key = _get_provider_api_key("openrouter") + if not api_key: + return { + "ok": False, + "provider": "openrouter", + "display_name": display_name, + "supported": True, + "status": "no_key", + "message": "OpenRouter cost history needs an OPENROUTER_API_KEY configured on the server.", + } + + # Fetch current cumulative usage from OpenRouter + key_info = _fetch_openrouter_key_usage(api_key) + if key_info is None: + # Upstream failure — still return any previously persisted snapshots + # so the chart degrades gracefully instead of going blank. + snapshots = _read_cost_snapshots("openrouter") + deltas = _compute_deltas(snapshots, days) + return { + "ok": False, + "provider": "openrouter", + "display_name": display_name, + "supported": True, + "status": "unavailable", + "window_days": days, + "snapshots": deltas, + "limit": None, + "label": None, + "message": "OpenRouter cost history is temporarily unavailable. Showing last known data.", + } + + # Persist today's snapshot + try: + snapshots = _append_cost_snapshot("openrouter", key_info["usage"], key_info["limit"]) + except Exception: + logger.debug("Failed to persist cost snapshot for openrouter", exc_info=True) + snapshots = _read_cost_snapshots("openrouter") + + deltas = _compute_deltas(snapshots, days) + return { + "ok": True, + "provider": "openrouter", + "display_name": display_name, + "supported": True, + "status": "available", + "window_days": days, + "snapshots": deltas, + "limit": key_info.get("limit"), + "label": key_info.get("label") or "OpenRouter credits", + "message": "OpenRouter cost history loaded.", + } + + # SECTION: Public API diff --git a/api/routes.py b/api/routes.py index 3ad444e9..dd25e76b 100644 --- a/api/routes.py +++ b/api/routes.py @@ -2033,7 +2033,7 @@ from api.run_journal import ( read_run_events, stale_interrupted_event, ) -from api.providers import get_providers, get_provider_quota, set_provider_key, remove_provider_key +from api.providers import get_providers, get_provider_quota, get_provider_cost_history, set_provider_key, remove_provider_key from api.onboarding import ( apply_onboarding_setup, get_onboarding_status, @@ -3323,6 +3323,16 @@ def handle_get(handler, parsed) -> bool: refresh = (query.get("refresh", [""])[0] or "").strip().lower() in {"1", "true", "yes", "on"} return j(handler, get_provider_quota(provider_id, refresh=refresh)) + if parsed.path == "/api/provider/cost-history": + query = parse_qs(parsed.query) + provider_id = (query.get("provider", [""])[0] or None) + days_raw = (query.get("days", ["7"])[0] or "7").strip() + try: + days = max(1, min(int(days_raw), 365)) + except (ValueError, TypeError): + days = 7 + return j(handler, get_provider_cost_history(provider_id, days)) + if parsed.path == "/api/settings": settings = load_settings() # Never expose the stored password hash to clients diff --git a/tests/test_provider_cost_history.py b/tests/test_provider_cost_history.py new file mode 100644 index 00000000..7ddf96c9 --- /dev/null +++ b/tests/test_provider_cost_history.py @@ -0,0 +1,484 @@ +"""Regression coverage for OpenRouter cost-history endpoint (#692). + +Tests cover: + - Happy-path snapshot append and delta computation + - Missing credentials (no_key) + - Unsupported provider (non-openrouter) + - Upstream failure (graceful degradation with stale data) + - Malformed / corrupt snapshot file on disk + - Idempotent same-day updates + - No real network calls or private credential leakage +""" + +from __future__ import annotations + +import json +import os +import sys +import types +import urllib.error +from datetime import datetime, timezone +from io import BytesIO +from pathlib import Path +from types import SimpleNamespace + +import api.config as config +import api.profiles as profiles + +ROOT = Path(__file__).resolve().parents[1] + + +class _FakeResponse: + """Minimal stand-in for urllib.request.urlopen context manager.""" + + def __init__(self, payload: bytes): + self._payload = payload + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + def read(self): + return self._payload + + +def _with_config(model=None, providers=None): + old_cfg = dict(config.cfg) + old_mtime = config._cfg_mtime + config.cfg.clear() + config.cfg["model"] = model or {} + if providers is not None: + config.cfg["providers"] = providers + try: + config._cfg_mtime = config.Path(config._get_config_path()).stat().st_mtime + except Exception: + config._cfg_mtime = 0.0 + return old_cfg, old_mtime + + +def _restore_config(old_cfg, old_mtime): + config.cfg.clear() + config.cfg.update(old_cfg) + config._cfg_mtime = old_mtime + + +# ── Happy path: snapshot append + delta response ────────────────────────────── + + +def test_openrouter_cost_history_happy_path(monkeypatch, tmp_path): + """On-demand snapshot append returns deltas from cumulative usage.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + (tmp_path / ".env").write_text("OPENROUTER_API_KEY=test-or-key\n", encoding="utf-8") + old_cfg, old_mtime = _with_config(model={"provider": "openrouter"}) + + import api.providers as providers + + call_count = {"n": 0} + + def fake_urlopen(req, timeout): + call_count["n"] += 1 + # Simulate cumulative usage of 5.0 credits used out of 20 limit + payload = { + "data": { + "limit_remaining": 15.0, + "usage": 5.0, + "limit": 20, + "label": "Test Label", + "key": "must-not-leak", + } + } + return _FakeResponse(json.dumps(payload).encode("utf-8")) + + monkeypatch.setattr(providers.urllib.request, "urlopen", fake_urlopen) + + # Freeze "today" so the test is deterministic + fake_today = "2030-04-15" + monkeypatch.setattr(providers, "datetime", type("DT", (), { + "now": staticmethod(lambda tz=None: datetime(2030, 4, 15, 12, 0, 0, tzinfo=tz or timezone.utc)), + "strftime": datetime.strftime, + })) + + try: + result = providers.get_provider_cost_history("openrouter", days=7) + finally: + _restore_config(old_cfg, old_mtime) + + assert result["ok"] is True + assert result["provider"] == "openrouter" + assert result["supported"] is True + assert result["status"] == "available" + assert result["window_days"] == 7 + assert result["limit"] == 20 + assert result["label"] == "Test Label" + assert result["message"] == "OpenRouter cost history loaded." + # One snapshot for today + assert len(result["snapshots"]) == 1 + snap = result["snapshots"][0] + assert snap["date"] == fake_today + assert snap["used"] == 5.0 + assert snap["delta"] is None # first entry has no previous baseline + # Verify the snapshot file was persisted + snap_file = tmp_path / "cost-snapshots" / "openrouter.json" + assert snap_file.exists() + persisted = json.loads(snap_file.read_text(encoding="utf-8")) + assert len(persisted["snapshots"]) == 1 + # No credential leakage + assert "test-or-key" not in repr(result) + assert "must-not-leak" not in repr(result) + + +def test_openrouter_cost_history_deltas_from_cumulative(monkeypatch, tmp_path): + """Deltas are computed as differences between consecutive cumulative values.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + (tmp_path / ".env").write_text("OPENROUTER_API_KEY=test-or-key\n", encoding="utf-8") + old_cfg, old_mtime = _with_config(model={"provider": "openrouter"}) + + import api.providers as providers + + # Pre-seed two historical snapshots + snap_dir = tmp_path / "cost-snapshots" + snap_dir.mkdir(parents=True, exist_ok=True) + historical = { + "provider": "openrouter", + "snapshots": [ + {"date": "2030-04-13", "used": 3.0, "limit": 20}, + {"date": "2030-04-14", "used": 4.5, "limit": 20}, + ], + } + (snap_dir / "openrouter.json").write_text(json.dumps(historical), encoding="utf-8") + + call_count = {"n": 0} + + def fake_urlopen(req, timeout): + call_count["n"] += 1 + # Current cumulative usage is 7.0 + payload = {"data": {"usage": 7.0, "limit": 20, "label": "Credits"}} + return _FakeResponse(json.dumps(payload).encode("utf-8")) + + monkeypatch.setattr(providers.urllib.request, "urlopen", fake_urlopen) + + # Freeze "today" + monkeypatch.setattr(providers, "datetime", type("DT", (), { + "now": staticmethod(lambda tz=None: datetime(2030, 4, 15, 12, 0, 0, tzinfo=tz or timezone.utc)), + "strftime": datetime.strftime, + })) + + try: + result = providers.get_provider_cost_history("openrouter", days=7) + finally: + _restore_config(old_cfg, old_mtime) + + assert result["ok"] is True + snaps = result["snapshots"] + assert len(snaps) == 3 + # Day 1: no delta (baseline) + assert snaps[0]["date"] == "2030-04-13" + assert snaps[0]["used"] == 3.0 + assert snaps[0]["delta"] is None + # Day 2: delta = 4.5 - 3.0 = 1.5 + assert snaps[1]["date"] == "2030-04-14" + assert snaps[1]["used"] == 4.5 + assert snaps[1]["delta"] == 1.5 + # Day 3 (today): delta = 7.0 - 4.5 = 2.5 + assert snaps[2]["date"] == "2030-04-15" + assert snaps[2]["used"] == 7.0 + assert snaps[2]["delta"] == 2.5 + + +# ── Missing credentials ─────────────────────────────────────────────────────── + + +def test_openrouter_cost_history_no_key(monkeypatch, tmp_path): + """No API key → safe no_key response without network call.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + old_cfg, old_mtime = _with_config(model={"provider": "openrouter"}) + + import api.providers as providers + + def explode(*_a, **_kw): + raise AssertionError("should not call network without a key") + + monkeypatch.setattr(providers.urllib.request, "urlopen", explode) + + try: + result = providers.get_provider_cost_history("openrouter", days=7) + finally: + _restore_config(old_cfg, old_mtime) + + assert result["ok"] is False + assert result["provider"] == "openrouter" + assert result["supported"] is True + assert result["status"] == "no_key" + assert "OPENROUTER_API_KEY" in result["message"] + + +# ── Unsupported provider ────────────────────────────────────────────────────── + + +def test_cost_history_unsupported_provider(monkeypatch, tmp_path): + """Non-openrouter providers return a clear unsupported response.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + old_cfg, old_mtime = _with_config(model={"provider": "anthropic"}) + + import api.providers as providers + + try: + result = providers.get_provider_cost_history("anthropic", days=7) + finally: + _restore_config(old_cfg, old_mtime) + + assert result["ok"] is False + assert result["provider"] == "anthropic" + assert result["supported"] is False + assert result["status"] == "unsupported" + assert "openrouter" in result["message"].lower() + + +def test_cost_history_missing_provider_param(monkeypatch, tmp_path): + """Empty provider parameter returns a clear error.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + + import api.providers as providers + + result = providers.get_provider_cost_history("", days=7) + assert result["ok"] is False + assert result["status"] == "missing_provider" + + result2 = providers.get_provider_cost_history(None, days=7) + assert result2["ok"] is False + assert result2["status"] == "missing_provider" + + +# ── Upstream failure / graceful degradation ──────────────────────────────────── + + +def test_openrouter_cost_history_upstream_failure_degrades_gracefully(monkeypatch, tmp_path): + """When OpenRouter API fails, previously persisted snapshots are still returned.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + (tmp_path / ".env").write_text("OPENROUTER_API_KEY=test-or-key\n", encoding="utf-8") + old_cfg, old_mtime = _with_config(model={"provider": "openrouter"}) + + import api.providers as providers + + # Pre-seed a snapshot + snap_dir = tmp_path / "cost-snapshots" + snap_dir.mkdir(parents=True, exist_ok=True) + historical = { + "provider": "openrouter", + "snapshots": [ + {"date": "2030-04-13", "used": 3.0, "limit": 20}, + ], + } + (snap_dir / "openrouter.json").write_text(json.dumps(historical), encoding="utf-8") + + req = providers.urllib.request.Request("https://openrouter.ai/api/v1/key") + def fake_urlopen(_req, timeout=None): + raise urllib.error.HTTPError(req.full_url, 500, "Server Error", {}, BytesIO(b"error")) + + monkeypatch.setattr(providers.urllib.request, "urlopen", fake_urlopen) + + try: + result = providers.get_provider_cost_history("openrouter", days=7) + finally: + _restore_config(old_cfg, old_mtime) + + assert result["ok"] is False + assert result["status"] == "unavailable" + # Still returns previously persisted data + assert len(result["snapshots"]) == 1 + assert result["snapshots"][0]["date"] == "2030-04-13" + assert "temporarily unavailable" in result["message"].lower() + + +def test_openrouter_cost_history_timeout_is_safe(monkeypatch, tmp_path): + """Timeout from OpenRouter does not produce a traceback or leak secrets.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + (tmp_path / ".env").write_text("OPENROUTER_API_KEY=test-or-key\n", encoding="utf-8") + old_cfg, old_mtime = _with_config(model={"provider": "openrouter"}) + + import api.providers as providers + + def fake_urlopen(_req, timeout=None): + raise TimeoutError("slow secret") + + monkeypatch.setattr(providers.urllib.request, "urlopen", fake_urlopen) + + try: + result = providers.get_provider_cost_history("openrouter", days=7) + finally: + _restore_config(old_cfg, old_mtime) + + assert result["ok"] is False + assert result["status"] == "unavailable" + assert "test-or-key" not in repr(result) + assert "secret" not in repr(result).lower() + + +# ── Malformed / corrupt snapshot file ───────────────────────────────────────── + + +def test_openrouter_cost_history_corrupt_snapshot_file(monkeypatch, tmp_path): + """A corrupt snapshot file on disk is handled gracefully (treated as empty).""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + (tmp_path / ".env").write_text("OPENROUTER_API_KEY=test-or-key\n", encoding="utf-8") + old_cfg, old_mtime = _with_config(model={"provider": "openrouter"}) + + import api.providers as providers + + # Write a corrupt file + snap_dir = tmp_path / "cost-snapshots" + snap_dir.mkdir(parents=True, exist_ok=True) + (snap_dir / "openrouter.json").write_text("NOT VALID JSON{{{{", encoding="utf-8") + + def fake_urlopen(req, timeout): + payload = {"data": {"usage": 2.0, "limit": 10, "label": "Credits"}} + return _FakeResponse(json.dumps(payload).encode("utf-8")) + + monkeypatch.setattr(providers.urllib.request, "urlopen", fake_urlopen) + + # Freeze "today" + monkeypatch.setattr(providers, "datetime", type("DT", (), { + "now": staticmethod(lambda tz=None: datetime(2030, 4, 15, 12, 0, 0, tzinfo=tz or timezone.utc)), + "strftime": datetime.strftime, + })) + + try: + result = providers.get_provider_cost_history("openrouter", days=7) + finally: + _restore_config(old_cfg, old_mtime) + + # Corrupt file is ignored; fresh snapshot is created + assert result["ok"] is True + assert len(result["snapshots"]) == 1 + assert result["snapshots"][0]["date"] == "2030-04-15" + + +# ── Idempotent same-day updates ─────────────────────────────────────────────── + + +def test_openrouter_cost_history_same_day_idempotent(monkeypatch, tmp_path): + """Repeated calls on the same day update the snapshot in-place.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + (tmp_path / ".env").write_text("OPENROUTER_API_KEY=test-or-key\n", encoding="utf-8") + old_cfg, old_mtime = _with_config(model={"provider": "openrouter"}) + + import api.providers as providers + + call_count = {"n": 0} + + def fake_urlopen(req, timeout): + call_count["n"] += 1 + usage = 5.0 + call_count["n"] # usage grows each call + payload = {"data": {"usage": usage, "limit": 20, "label": "Credits"}} + return _FakeResponse(json.dumps(payload).encode("utf-8")) + + monkeypatch.setattr(providers.urllib.request, "urlopen", fake_urlopen) + + # Freeze "today" + monkeypatch.setattr(providers, "datetime", type("DT", (), { + "now": staticmethod(lambda tz=None: datetime(2030, 4, 15, 12, 0, 0, tzinfo=tz or timezone.utc)), + "strftime": datetime.strftime, + })) + + try: + r1 = providers.get_provider_cost_history("openrouter", days=7) + r2 = providers.get_provider_cost_history("openrouter", days=7) + finally: + _restore_config(old_cfg, old_mtime) + + # Both calls succeed; only one snapshot date (today) + assert r1["ok"] is True + assert r2["ok"] is True + assert len(r1["snapshots"]) == 1 + assert len(r2["snapshots"]) == 1 + # Second call updated the same day's used value + assert r2["snapshots"][0]["used"] == 7.0 # 5.0 + 2 (second call) + # Verify persisted file has only one entry for today + snap_file = tmp_path / "cost-snapshots" / "openrouter.json" + persisted = json.loads(snap_file.read_text(encoding="utf-8")) + assert len(persisted["snapshots"]) == 1 + + +# ── Window days parameter ───────────────────────────────────────────────────── + + +def test_openrouter_cost_history_window_days_truncation(monkeypatch, tmp_path): + """The window_days parameter limits how many snapshots are returned.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + (tmp_path / ".env").write_text("OPENROUTER_API_KEY=test-or-key\n", encoding="utf-8") + old_cfg, old_mtime = _with_config(model={"provider": "openrouter"}) + + import api.providers as providers + + # Pre-seed 5 historical snapshots + snap_dir = tmp_path / "cost-snapshots" + snap_dir.mkdir(parents=True, exist_ok=True) + historical = { + "provider": "openrouter", + "snapshots": [ + {"date": f"2030-04-{d:02d}", "used": float(d), "limit": 20} + for d in range(10, 15) + ], + } + (snap_dir / "openrouter.json").write_text(json.dumps(historical), encoding="utf-8") + + def fake_urlopen(req, timeout): + payload = {"data": {"usage": 15.0, "limit": 20, "label": "Credits"}} + return _FakeResponse(json.dumps(payload).encode("utf-8")) + + monkeypatch.setattr(providers.urllib.request, "urlopen", fake_urlopen) + + # Freeze "today" + monkeypatch.setattr(providers, "datetime", type("DT", (), { + "now": staticmethod(lambda tz=None: datetime(2030, 4, 15, 12, 0, 0, tzinfo=tz or timezone.utc)), + "strftime": datetime.strftime, + })) + + try: + result = providers.get_provider_cost_history("openrouter", days=3) + finally: + _restore_config(old_cfg, old_mtime) + + # 5 historical + 1 today = 6 total, but window_days=3 returns last 3 + assert result["ok"] is True + assert result["window_days"] == 3 + assert len(result["snapshots"]) == 3 + # The returned snapshots are the most recent 3 + assert result["snapshots"][0]["date"] == "2030-04-13" + assert result["snapshots"][1]["date"] == "2030-04-14" + assert result["snapshots"][2]["date"] == "2030-04-15" + + +# ── No real network calls ───────────────────────────────────────────────────── + + +def test_cost_history_uses_no_real_network(monkeypatch, tmp_path): + """Every test path must monkeypatch urlopen; verify no real calls escape.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + old_cfg, old_mtime = _with_config(model={"provider": "openrouter"}) + + import api.providers as providers + + # Without a key, no network call is made at all + def explode(*_a, **_kw): + raise AssertionError("real network call detected") + + monkeypatch.setattr(providers.urllib.request, "urlopen", explode) + + try: + result = providers.get_provider_cost_history("openrouter", days=7) + finally: + _restore_config(old_cfg, old_mtime) + + assert result["status"] == "no_key" \ No newline at end of file From 15513b81f47147b5a8a52318cab30a0e7dda53bd Mon Sep 17 00:00:00 2001 From: Michael Lam Date: Fri, 15 May 2026 17:17:48 -0700 Subject: [PATCH 02/11] fix: harden OpenRouter cost snapshots --- api/providers.py | 47 +++++++++++-------- tests/test_provider_cost_history.py | 71 +++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 18 deletions(-) diff --git a/api/providers.py b/api/providers.py index b3ddb4d1..804e112a 100644 --- a/api/providers.py +++ b/api/providers.py @@ -1445,6 +1445,7 @@ def _provider_is_oauth(provider_id: str) -> bool: _COST_SNAPSHOTS_DIR_NAME = "cost-snapshots" _COST_SNAPSHOT_MAX_DAYS = 365 # hard cap to prevent unbounded growth +_COST_SNAPSHOT_LOCK = threading.Lock() def _cost_snapshots_dir() -> Path: @@ -1558,23 +1559,28 @@ def _append_cost_snapshot(provider: str, usage: int | float | None, limit: int | repeated calls within the same day are idempotent. """ today = datetime.now(timezone.utc).strftime("%Y-%m-%d") - snapshots = _read_cost_snapshots(provider) - # Update or append today's entry - updated = False - for entry in snapshots: - if entry["date"] == today: - entry["used"] = usage - entry["limit"] = limit - updated = True - break - if not updated: - snapshots.append({"date": today, "used": usage, "limit": limit}) - snapshots.sort(key=lambda e: e["date"]) - # Cap to _COST_SNAPSHOT_MAX_DAYS entries (keep most recent) - if len(snapshots) > _COST_SNAPSHOT_MAX_DAYS: - snapshots = snapshots[-_COST_SNAPSHOT_MAX_DAYS:] - _write_cost_snapshots(provider, snapshots) - return snapshots + # Serialize the read-modify-write cycle. The atomic os.replace in + # _write_cost_snapshots protects the file write itself, but without this + # lock two concurrent requests can both read the same old snapshot list and + # race to replace it with stale data. + with _COST_SNAPSHOT_LOCK: + snapshots = _read_cost_snapshots(provider) + # Update or append today's entry + updated = False + for entry in snapshots: + if entry["date"] == today: + entry["used"] = usage + entry["limit"] = limit + updated = True + break + if not updated: + snapshots.append({"date": today, "used": usage, "limit": limit}) + snapshots.sort(key=lambda e: e["date"]) + # Cap to _COST_SNAPSHOT_MAX_DAYS entries (keep most recent) + if len(snapshots) > _COST_SNAPSHOT_MAX_DAYS: + snapshots = snapshots[-_COST_SNAPSHOT_MAX_DAYS:] + _write_cost_snapshots(provider, snapshots) + return snapshots def _compute_deltas(snapshots: list[dict[str, Any]], window_days: int) -> list[dict[str, Any]]: @@ -1583,7 +1589,10 @@ def _compute_deltas(snapshots: list[dict[str, Any]], window_days: int) -> list[d Each snapshot carries cumulative ``used``; the delta for a day is the difference between that day's cumulative value and the previous day's. The oldest day in the window has ``delta=None`` (no - previous baseline). + previous baseline). If the cumulative value drops, treat that day + as the start of a fresh series (for example after an API-key rotation) + and use the current value as that day's delta instead of emitting a + negative spend bar. """ # Take only the last *window_days* entries window = snapshots[-window_days:] if len(snapshots) > window_days else list(snapshots) @@ -1592,6 +1601,8 @@ def _compute_deltas(snapshots: list[dict[str, Any]], window_days: int) -> list[d delta = None if i > 0 and entry.get("used") is not None and window[i - 1].get("used") is not None: delta = float(entry["used"]) - float(window[i - 1]["used"]) + if delta < 0: + delta = float(entry["used"]) # Rounding: avoid -0.0 and tiny floating-point noise if abs(delta) < 1e-9: delta = 0.0 diff --git a/tests/test_provider_cost_history.py b/tests/test_provider_cost_history.py index 7ddf96c9..54bd9971 100644 --- a/tests/test_provider_cost_history.py +++ b/tests/test_provider_cost_history.py @@ -189,6 +189,77 @@ def test_openrouter_cost_history_deltas_from_cumulative(monkeypatch, tmp_path): assert snaps[2]["delta"] == 2.5 +def test_openrouter_cost_history_reset_uses_fresh_series_delta(monkeypatch, tmp_path): + """A lower cumulative value starts a fresh series instead of a negative bar.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + (tmp_path / ".env").write_text("OPENROUTER_API_KEY=test-or-key\n", encoding="utf-8") + old_cfg, old_mtime = _with_config(model={"provider": "openrouter"}) + + import api.providers as providers + + snap_dir = tmp_path / "cost-snapshots" + snap_dir.mkdir(parents=True, exist_ok=True) + historical = { + "provider": "openrouter", + "snapshots": [ + {"date": "2030-04-13", "used": 9.0, "limit": 20}, + {"date": "2030-04-14", "used": 12.0, "limit": 20}, + ], + } + (snap_dir / "openrouter.json").write_text(json.dumps(historical), encoding="utf-8") + + def fake_urlopen(req, timeout): + # Simulate key rotation or provider reset: cumulative usage dropped. + payload = {"data": {"usage": 1.25, "limit": 20, "label": "Credits"}} + return _FakeResponse(json.dumps(payload).encode("utf-8")) + + monkeypatch.setattr(providers.urllib.request, "urlopen", fake_urlopen) + monkeypatch.setattr(providers, "datetime", type("DT", (), { + "now": staticmethod(lambda tz=None: datetime(2030, 4, 15, 12, 0, 0, tzinfo=tz or timezone.utc)), + "strftime": datetime.strftime, + })) + + try: + result = providers.get_provider_cost_history("openrouter", days=7) + finally: + _restore_config(old_cfg, old_mtime) + + assert result["ok"] is True + assert result["snapshots"][-1]["date"] == "2030-04-15" + assert result["snapshots"][-1]["used"] == 1.25 + assert result["snapshots"][-1]["delta"] == 1.25 + assert all(snap["delta"] is None or snap["delta"] >= 0 for snap in result["snapshots"]) + + +def test_cost_snapshot_append_uses_lock(monkeypatch, tmp_path): + """Snapshot append serializes the read-modify-write critical section.""" + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + + import api.providers as providers + + entered = {"count": 0} + + class RecordingLock: + def __enter__(self): + entered["count"] += 1 + return self + + def __exit__(self, *exc): + return False + + monkeypatch.setattr(providers, "_COST_SNAPSHOT_LOCK", RecordingLock()) + monkeypatch.setattr(providers, "datetime", type("DT", (), { + "now": staticmethod(lambda tz=None: datetime(2030, 4, 15, 12, 0, 0, tzinfo=tz or timezone.utc)), + "strftime": datetime.strftime, + })) + + snapshots = providers._append_cost_snapshot("openrouter", 4.0, 20.0) + + assert entered["count"] == 1 + assert snapshots == [{"date": "2030-04-15", "used": 4.0, "limit": 20.0}] + + # ── Missing credentials ─────────────────────────────────────────────────────── From faedcab7391e1242c1dccdc8eb587cf65ccdb790 Mon Sep 17 00:00:00 2001 From: Frank Song Date: Sat, 16 May 2026 10:50:16 +0800 Subject: [PATCH 03/11] Preserve live agent timeline across session switches --- CHANGELOG.md | 2 + .../after-live-timeline-preserved.png | Bin 0 -> 58630 bytes .../before-session-switch-rebuild.png | Bin 0 -> 71417 bytes static/messages.js | 43 +++-- static/sessions.js | 66 ++++++-- static/ui.js | 151 ++++++++++++++---- tests/test_auto_compression_card.py | 15 ++ tests/test_regressions.py | 39 ++++- tests/test_ui_tool_call_cleanup.py | 38 +++-- 9 files changed, 288 insertions(+), 66 deletions(-) create mode 100644 docs/pr-media/live-timeline-session-restore/after-live-timeline-preserved.png create mode 100644 docs/pr-media/live-timeline-session-restore/before-session-switch-rebuild.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa25bab..79b39b1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,8 @@ ### Added +- **PR TBD** by @franksong2702 — Long tool-heavy streaming turns now preserve the live Thinking / assistant progress / Tool / Command timeline when the user switches away and back. The active stream keeps accumulating token and interim-assistant state while inactive, reloads the persisted transcript before merging the live tail, restores the live turn DOM snapshot instead of replaying tools into a flat list, and anchors automatic compression cards inside the active turn to avoid duplicate cards while an answer is still streaming. + - **PR #2332** by @Michaelyklam (refs #2290) — Cron run history/output cards now surface token/cost metadata when the underlying cron output markdown includes it. The backend parses optional model/token/cost/duration frontmatter from cron output files and returns it from `/api/crons/history` and `/api/crons/run`; the Tasks panel renders a compact usage strip beside run rows and below expanded output without affecting older outputs that lack usage metadata. ### Fixed diff --git a/docs/pr-media/live-timeline-session-restore/after-live-timeline-preserved.png b/docs/pr-media/live-timeline-session-restore/after-live-timeline-preserved.png new file mode 100644 index 0000000000000000000000000000000000000000..6a3ace147d54c4e9e660a31e2746dd97af5e0cbd GIT binary patch literal 58630 zcmeFZRahNc)FpZn5=d}&3-0bt*l2Kf0wh3icM0wgg6qcJ-3jjQ?(XiJE^>Z*===5K zedw;oO;xdEt|?=TS;6wM;s~&~u>bt?4}zowNb#S4-j)CJ50v(Yx4F8UiOi#pnor(;5VO8UazPK10jjO zzh!4(BcuNPZJ!F-?>}w}`G2<>f+5A5+jg7LB(9LyC4UNrpjg3j@tqMJ$bbp!%4~_^9 z2@Ctc$zE4o+doqN3)rLy6kzRRdOrKOKR2%?XsIV^s7p1H@v(8yvGq}yoxrOmk^^tg zy!r`jOim85m(@Ez%s8AVj2L^QO8<=ZHy=KfuzZhw-R)L4_X2p792`HNJL4Y~S;^zX zU%M+FG6~6EpFbsp>NxH0jU>t`qaYZ5D#E;Ox;89!uvg9o-{NgSdrkGeuko7}Cr_u5 z|MY4U9YuMXY$IOhR20Bvdacb4 zhq;M~8p#|MqTuge;z{DFs;X!etMg;rTZsgSlRk^roh+}kdEM$#b8*e}^oU6j)ZjWl zTzwpHd0t?r4$m*WJJt$0>$zViZO~MtBO%$Sj7H<(TUute+t&(i``-3D91r3Fd2;Cq zo7tvS{^gc?uSvx3x>fEgGOFigF;<>&h42Xv69qGc`pZq1*qQsw$nbQHwtt9aX_+~T zaczyv4XNYp=`y`$aeZ+A%j)|5S-S`CBS~&>|VC zc%nsuMSvsVH_c9HI7p5D$iT)dm&QlPZ@cvq^31dlvOUPu|L4uK;IJT@a?IsKPUxN8 z>Pid0+mp|P7rVjB>_58 zmcPIMr+_ZLapT8Yi+)Xi7rR~ABqm{LcF!gCum+dOT<$7kwL*Eq7fgC>Y%DCw9{1am zSDwah_45V&M<*_NYkaXLfMzeG9a+705wZU(=h={vDp&Y#z$c6iv@e}Ud zSvw5>b!_O(&4pN9QPE)p!D6?`mv}lGdwcXyp}wo$(>4rR)ne`Y3(e9F+F3Ph%ll$EPVVYySmbZw!O&wk8e;f;xsD1DWoP-L4Ou@cRZfZ zcnY>D)ot+cO6IUqXhBdct$y?T5JDeh(P(^o@~-HEXvc_e8V<|1KYe*};Epz4r&IF! z@3$xOthQa&}q?4K~gEz)^HdJQd_LEAboMU*a{wuPqkcb2>rcQ zD4qxAkpgp;2R1#{n9Yu)@miUh^4yFHmMa?c>BGQ0iJ)RjnoDNw2JI=9$l*UoE{r1cUVMpIHavCJ~#GB3wn{m7sMsR zoGG`NbR_Wn47SkX&}nnUxh&SLa;yWp4JER-d_Szsk&16m-&dk=JeWX$No)V&%c`oXthqA1;BZ`a zPsqgf(7YYC>&Bf8<+hSmrD$k;I&=7aCG&tP!I!tLfnNyjoo?M>9sjKv~KbFe{*pcf%HnZr`Z zJepKI0JSkDqXFdganOFwwnM8WZ;r;tn~@e-+{O>(*rCcS^<5 zJWpix0x=*mRo>l9KA>qJnbKGq958Co9irFZNYEV}gT&0ktm)V}$mJYB@u zm?xJ^K~B!VK~WJOx-gHm&->N7RJ$R{nw6Ck>Ep+Uu#lltN~`XW*cY9;oy^UKUu$cc z4hJqSE|yQX;8rM@QLXx^<>~9KbHfiF@$|a#S*jM&3DnWyDHf{NkmsiPODrUOKJ(Gi zW2@EHU_mtIq`JD@DV(anaD2Oiu8QZWWG_Y>SCex&%w&U8xC3dn6GL49&a4t z+YS*J^jm!n%Oz|u0v^Y2fmQa9l%FLfCD^?S3%JXL9(432W8s)v-Ju~`*Of+tt~*0N z-u;uQ0NGoIa)Ee7;LR4;8E309Mf15_5GQEVy8ElAz_%yVT<7({R@Ic3|AIjvWO;|f zG?ejGxEoJ& z)Ti$We@2rudgq;>Kb%yvVWr_hVb~i!m_)xg+VeyV|+;rFifl%MTV4~6D z@tyHRr3#)o`(tMJuUxnDBaNrTpz%W0BJTk@AQenZ#JLEs)u^g9U*36`ro|cociurf zJ=x7>U7TMCOR2Tn9N!m+PD|HUU5Xryad0Fm;O+eZXL@Dj4wsumdA}z8QhBYnT4EmV z?x1g#Ct$BM0gs(*_mq8~EYb;iQztx%zRAl;v(-9_g9jCM+KuYVIvH zteruyUUqr4M!r!W1p&AF-O_xOVb2o<>i2M5&Q{OoNwd%5@I<*hm7$?ue%Dm_=t)O?>FEoD7}M+iG{;?$NL&$Iyw==c zS4x{9mst5-twb}GQ?K=o6pQAHT6A;cLeajfEc51=m)QCRb=mwd1_o20aG8LNeu}~Eyi%-a3K*R97 z%vBN|<+_+`_s)==*_#7A?4Is!ibJdq3ak_qbK7r4F+_W*3}o@}x^Ej5qK&xiwj)AS zitB^wQA3;H;ROl0toc89m*&$>CkAEo^z?wmSwC-_kNZBFslDjr>H3<_R|Z3ZZhuwV zd)}?})R_4O2xdYD{3jL=A+H0HX8R2Wm!@Vwd4*VJWqW=VQTPPCzbP{UgrV%J*4v(t zdR}G4l_2M{Kb{RQ`%08ej)!SmTef3d4h!}m^xqrQP1Z_ERctPayR%v*{M)219~dL? zRhlJp-2Gwaqr3;ssRibp*3wPVLgshmsKlgl*vdGwb@`W;@z_oIocH7<&-ZlRk-Q-l zk!e~zLm^$LH48-WINuO`XB!ksA(K@9;}R1g+9aQZl$2E3?RJFVo3>mk-}%|P$dPPc z2f`i8Q53q(SB7yEZ0rPTB?|jHXSYxli5b+@Ru@|86G3-UF`)y=K@)j5ThEE!$H`<4 z6vx5{CW`}fkZ+$gNkhn3zS}HAW?KqR92g9b2n&mZR99X3yYp){rfA@t`b82&Pv0CF z1;xS~7tWjY3R1)!Ow3`;YRxL!`#Z_WqvOzY#N$xR;W+w7|4U26RtCq^3o3Es|c-x-&`7Rj^*pjQ%#}z9ae&@5F+3r_nyC=&V z;QHW`wRVu#N1f~j-Ddf%%3qks=V5hqPB}nSQ!Tz5*q%N&6h}!RU5=bvKbvg#qSp?tNX=PKTtJBg89g2Mh3q`Q7J7}UPyPjIT7-^lChX4XjR5hS+a?m zo{wetj*pKQsY2*;YT90Oy;X$z)#&sB1W{HCTQ0=;+=FREa{{Do8reI=%T9DW53pU>#T&qymgiX zKX}+Xp|6vfzZ-1k4x{vSgZp>8Uj*BO=2eRnE<0g}!m8Z+Nw`IHGZeqCA`zKR7vynB zJ_m$fZVhv~-hF)%5r2jb7lz@w_EXf z54f@Bqphvu?bz$fW8K1E0iTsTrw3x)98Vn^Z5__mK0!U)YTe(409kV;Nmd%4!cx;* ztwqzj_`8ek!Sl@?se!tx>eDe{2l;8!X^?oLB6)8F9;f}BHMLE@ril*_0fC(RW7MA? zQxfCArE<8j%|T5WBFdp;I4j~jYMv(5aBAt}&%uae5+v{G#YMN(v{ zQiM*YEb)Zi^fVE3$1*=hr_Dq}5Qs=frPtaq0#vEXU0}`@B(4C+X=Wn6xBo;*RhDL8!@c{xrexc0^xDVJP3kDX=7+9U+%v| zrkRq)(5gYM_LY?KqoR|osdZWXmBBtM%k zSrv3<&AL52BwuNtNKUJo6%>@Qc`Wy-F({rR? z1;fWt8NJn+8OPZZewWK_kpLNxZ_DEq0k^5L51fjQtAWZ?xqcQ-l~D#1%1Bga;5f6_ z`sedOlJmNwm>-knTtgs`ufxI2a;shZ!E(X^-T1+*w_C=Dv)&aSu*hoTnYN|{FQ z*oC_Vq^J;--_2K1-!2m1cnEn(&hIAlM=5_vhZpIV?O%Fs_LG3UWRLnGMR zO(nHb_=}pQ0&Ok^F>gDn10d)6=mY7^NXMw2!U*GX<3;{F>8YC43^w zWDDTS0B~D7I9gV?W!GLKXA^66lV373OJUGJ$w1Ze6@d7EF!Io_{0|`d-FyepHN`}I zz52KJhM7Rv?vNP<##MRQ@b|sq02weo>r>9e$41OYhfNTaoJ|zf>ivmCh}G^3YiJn! z``0%w>;D&d(0aJ~={3s)z~!>Q{|i|ImKZ9X;9%Hb#M{sa_>j=>m{$zm-nZ2|I4U?c zDlt662H1;<2wiRIdeA<|IbHl)LZo+8L58FllnCoiC%C^_#1L=psuIfKH#8$|hz6TcD_ETmPy z`%BGnQ50~$3MC+Rf&4)M_iJaAuYmbJpGb6=32ufr8Hz$7GqE&GX6JL5G>g=$;gLx~ z2N*$%<(d#nf?`bJKTLY>^$~lJD!q;@DCQR#2uhM9Q`zwKmEB?&rO}k^4$-A$|Gs7QAE?jED#f;ea|tH) zd@TrV-?9_qtRa1O##xifdjT|u% zE3nS@ZwBLqr#$~Iq=5JPNj8AU~MMmx`+=*jdl>C^EEx^AU%+} zkn*<=jijHS#yvKtLi*0WdU*J9GM4FZv&dOcSVj^smt^-LM9kP;MB3OGJ3gJ9AF-$c z$@9~O_kM=3z$eJ*>lQu;i;DWSdV1o3QBgd!;6Hd#342;Swdh=3U->+*Lf_R(6&RYB zEa~?}+=w`C(P(3Mxwd)6teIdhSdPt%WFdXZhdZ+o38O^nAd5#FW@FmTMVf5xKaPUCMUT|zY4B0N ztQOZyKbE~BrwhC^dWpkhyQsp-Q6nV2J~8}viW&R+`?IH4@oja7Eno1+Nge{Qks(}} zB%fu5S^K?kv6H&GyCn;V1r_tl&e>Vje=XdZo}|1`w$CO~a5Q8@;RqOJc6Nc8nWJ7a zhv&U9&7enYGZS+PlyT0kpJ%<^?wZP=Ur)x`=GCpO-dEtlNl(*&uzBC7l_l;r)yD#s zT{@ppPR3G~HXDEjtamWVd9_bLHjuBeQ^asw)3^RY9kPE1B@YiTL)wYK{AnZqCz9oi z#)aeybZ-@_?PhW;15y_YM>%hff)#)iSUMx!dyf1CionLH3qKmN#ljD}mWA`}%5_%@_T zRpv1ak5lzGeu83hv5_a7X!I(0?*95blySAr_yOeCWBfm9-2W4=z5lb-ThbhGrLULN z|MZz*(!~ZOP=DQ=Vn}F&(0}xk<)CasN;KbAa%gw@vvjQHS}{zebXjRJvw2s1`-lH?Mtm$A>~{3jORO&FywAVz_6 z9WIRk|JRU7mVe4KQLqqm@Vsph7V`!D2zsN>gNBBIN^pWjfP;>ONBwv5&>m1|q-eCH ztavNbQZcpvP4)|j{67Hz2*mS$GKs&Y0OA?=Ij*wBSGFzzqZ6eEe*Yl0; z8w)Mg??W-%U*gKk%cqM}1N;LpsN{(u$^dKL8$q}X@Pp5fw=`;{wi{i+I=oKG#p?Zi zeG*cF+B$m2i%qv@`hfsN79JkHJCazEmj@Q~^4vdMZ2Ue5JT(|YO}hURxU4kl1mTBQ z+qZ1_ya=0IgHg#VioU!=l_^6X)6!TJUGY{M+_M<(if<3Z#<#LkGbHSIh+RLCE7)eQ zI|)1B{^{OckRSl3H=1q_By(Cx^`oPqU9Leqker#B)S`A*O z{mf@FB0*0Oa_lFO0NUBo#kslk(WiSjLOy5t8dDYO#HZ({Bu=x1TAfD>uNQg`Er4x@b*2ZiR%h1-H+YYQ zH6fwz0P4gG(jm&r%S-Lh3NDdbmA*Ne-(^()YG4rJTlG2vb=;F(YA!Di!_`qvW`rUg zUvy6BpuNYgpYm#3qk3Pb_Qv~@-eJ8(5vchwsS5Iy&oYvEik^s_Ez{eqQrM&jf*$vd z8yM5?B!7TauLG@$&04OW{8p-T&+*AwdlJ=CaW#iWj)HFf&gQ{uduLrm5x%|1VPIgO zS!X(5)ysJCfl@B*&j+xnGVEuqOhUd+1%K-O(x06MBam~YXV&Af3GbU6B18O-uCIhl5+!C^2i z9FL>gWCfnQ{jVAH_4dN}a#$?{owT;NWju}?FP^TpiT6G@yyVL^Ige*9u$z5txNzln zdqAi4>7Fjr+U*KP1Gvt?czV(KvE6uZlc385m*+hU0zT9C3o`qmWL_A%xjLsZzP**@ zrhIbgPzV*DVp*XXzvt4N6vjZ-Ka;t#HVz)-ISd&MA=hT(FP5EK8yPRs%0%Ai>+CVcK87Z(`P5JZd(YyOwE=$?-0TlL4dQ5 zK}XnrI$Q4yTJJ13IjZ)0f@oRG|Kg+i?m$9bsSp6bHjcilVv(M=C#eTxF|lu&oB4J} zlS6-hr82Z28#s+F9*lUrV6Zx7>!EXhqh0DnW93o3jp9JGEw~JVFoaq?Tb2oRXU^|4r zyEG^(TNpZ14*ozb&`joRKP_zu|5bJG5597sY$^*5e@jbq_EySf(4Jt|PbQB+z4Skw zfk=!x%~TL~>lXn7!s-%HP56;i{!kQHEQX`g-AKe~jzptc4GTD0E3OKFX|1XoP34Wu z9;(%+_`pjpsLN6@o%Y7-2xCQ|Y|ncdlVkg-NH5oJW*mV6(u%!qQO;NQin&-os*o>Jl3? zzI}Vwb9-_1J5O{{f8FOE6BBboSG+=3(CKt%e_NwUwPbe`OqzhtW&LM~+b#rfbH(b+ zAl&1H>L^l00KdAorBb68kHA+~?FqvX^%B%uWk}`C$WY2K5FNtt7ni1jUN;Y~S!wcpVD?EB`*t_xVX`nmD5db7lSA^X@TTW%F_+7Wy?Q+a; zs+}z5yAR>hOH3aClTkQFC9gCZew8%5d49}*UTJfiO&1oiva+g%m$wbWZ?i;OVlc=^ zfb}gO%N5+aZ|H@nif}rh7%pm@(aQqH^OB$$KLP;h|RAe#5<88A-~1N zCSfnBm1>K3YA|d3&Ilh>0Qz0wSc}7okkvKDU{}sG62q*{argiTaCo@5Y$kKadOXz> zPR9!c4N_6Laku^M_m^3VT~i=w+nuZZ_0E7KEp>ESReXeYPmjlZjzhHUjFfx^ZOTIR zg5Q7+;>Kdp_4b4uNlGKNs}3M7=(Yive7aDDlZ_2zSm!cHQ8+4}Qw+}~=5V}(Q=(|D z5wpJ04ToKbdHH1k4%Xy5U?x^23l0ml7V%#{vMW!9=-MbCT@iV;#eIE!6ydlA>_FBqS?b|tZgzxs!D@v& z9y9Fe$%RzZghY7S}iN+ z5JSW@>zPbZ3)^3arcmB>bBTV+TDZ%I|7MKe{){+K1m;T|9W^!B;;}&FvAmo~5(m&5 zF={l=_4eZJ!r?nI_3cmO^hTo9hfo@^oA)8(&q&2G=#+o`Gz=WyUVYH=a-+R&g}a8c zI(7sU71Z0WPX&!S%8?TvIz9JS2;5pLf|(KZ@@YYq`HN(?Xc_~EjKKstPk@+h0a6E3(rni@C?^>|Bq>!Y-D+;pp2 zrr~yZlM1LHGT)c4uX=6wMZ)fN5^{pIy>^cXH91(UfMcq%Af z<6PX__yV?1Thv3^C-c=Bms!0LWm+$%Ni`v8c(_;CZ@dV(wth~xC-Qn;8UPsy6b$0$ zUPgXb1C0-;Ag?e!=RWx%i^Us)T}G|bJHzV zI1w1=X>pNDGWYf-=9$j` zABNAUtJ$XSdl{W~N24#Y+1SM4M%V0mZF_io z@F-}pabU7w19Gbdk6VV?Q|?~4s@S>n909Q_ zs2jFkg6;c6HlRI2#1k-h^7|M(EoY!|aIzDiDgf(s3xT40t7Q%8@W*yJBJv`UYHtls zc*oq9J=7emp;Ny`#WUvKYg}_MJsk%aBKwd&ASWnNEfZEr;q$*`l})xRlJ@VpinM@PjjzY2Bv-PY_@AyDe`$GlDk-!b^Tjtg94LCF?Lb2QoMvZza= zN>!s>fwZw$4BQz4MBIX6=S)Oo4`W`3GxQ0CE}@uCC(FH`;J_ak+4GvMi|gG^1x&5F z`l6CJRVyPpS@URbwkkGz^kpiv&o(eo5jeDOF;$voIi_|qHG7j}2TlQTal7O7gK%vHOz z3^X~NTK^VG2$Td7Y2W@UL~YxAn{A>zoWec$=Z_G4DznmfGax}o;)5x!saJ*{POAHb zj-ZMp$!oD#Bfh9pVhnEe0EwqII*@<2gZ2Kkr&MRj@7{Vd<_H<9(-R!mH#QC|@eTJe z%TG%SDY^U-N%(y*7ENTlu)MBr_7|3v>T;oFl9^iB1{(dxb7Jy|3`5Ts9mU+3^I#*K z&jttqR3>PZ&LQT zObK{A>Mx_Ed}Nr|L_5!Z>bV!}jp*DxPOdwfZ;?bUHq-y} z$J-#krpBRDES%hYqv!BHv4Cyf2eRX@@q4Aszdli4bh?N~DDrKRDW*%R{&1h`-J)PW zKmgaDFytt|yn%v`E%WWwSB)`X5#5lT$J6 z{rw-Ap^|nCC`@j%8p0&RCMKhI`Rsb^K`8Z7qi@yvL3de08PX6Ch^Yo z>Ivh}2QduDJnih86;`n94g*MXI1G2BD_ZZm;uR+HU&Wd1O2-^=s=e!jY7ComK^C7N zU1@w45AgO&4Q}Fu;@4EiKn?q+SglO1u9hSxz~|Fg?CEXP?*$w-iKC|U`@O=P2tn$I zy4@Hx5;D>J0xsM-^Kq8WoJ1o`-B^4WMkIWy*8LwHZ}#1$H|)I9sAsZQZ`WUSu{?=>H|{1DQ3Zv@ONRNC=0$#x}oaC7`(uMy$Ye zx!8$?(a_LX?{fLF?LR9TiW%F4lDWo8!y`3;Sh~=2nn>E4*K~^fcAeEOj_!ik4i1i* zdyImKlQaH>PNfl@0V<1sy*LaXDBrg2f<2Ha+QCrpP<|?tR4QxW?9`=~eQuLzMnvXoak%1lDLMKphe6bg0fFi0K@D>-L zP)2<|D=;Jkm&IfZsIinEvs%bi(GUm%MFJrYW*kWYfdq(IH6dZ%9A*d`VvHHi!5c70 zN0AW|j-%9SkWeBmT7^o#?B)V#Ivu;%p1>RMQuu+WJfI0?$cd@Hm!ZtsLXBmV82Nr^Xs@5<+?wbQp3VRp&9IXfbOLc zORLu7K)xj@6-%WH)X~}!rjxlAfD|x0`@nrHz5V&&W`BMY3!loGN!+wZ^>~3qVvI?G z!WML~O(T31f=#9uF>aJ>s_q{%9h@|E#CncF^Yp?Timre!nVE_1luUZmg(4KeO}?Ev zt)Yy#cD_BFEO#=~Tv08ENUB3|^9@DalP2#XC=XX_Kzu(vo!BN66uL7!g&8gaTgv&P ziq+)ToRje7xXPC~BC_uT6*`YM5~PNw9Vgz{T6}D!+uRXB5CDsxY&l>@kAJorBs{WdF_v+HjjWO{tJa%lJnUwU(HV1)&!Go zUu0Rd|Op<+3gR}u4grQ?QOA{v+{CS44T4L z2Qdkoig21peSjR<1r7pR1BT+9HOy3H9c9a4oC?AdW33>4a%ZF zstMrw(JuMJEf(CM+hCoz-Q?3T5K5b(M4Bk^)^=``BU_wSb_vZ@tpQwy#psHc)dXufYB4F%7_+2u`l zjl$Oa1;y@XT?4!?kDJ!nl^5YMt#b?;oJ-rJZG~rN<#(Z&U+O9p_|Wo)N798eSN*di zN*Dfoe8`hcy*XM5r}f={=-YS`1Jy3cgB8Hq-C9#ut5c zWY966%CFF@x-RPIueWORybC_9{#LM45Q3qWA~UVK9Lx<872BX8 zylo+sQ$sw*1k{Ua;dEEm%`{^ z@?)5@15#dkmPAI~W{IMoOw1%-taq+(xRJt+X*E&LG1PK-0}$~T1mg;Fe^eP|iHGyK zJaskIg>YhS(A4ae>6c9UE!br<#;COdDH#N;To5-=C<{8+pT9INl+h<5A~H1GB-<@+ zfS(&xx%~LMQUeDaJ%QT})5UL4t*pj${0UrR7Byp*)oje=;;+^!cL$W!g+I`2o={xO zvrB$4p>VFu7HgnIp(18SMC^vrwpD!}jFGXf1xjuzc*rMQZriKAv3B1)*|abdntqZEMapxRMd|Mi@0Bg`CN?WN`p8a=s_&f%BjD8x@@B=>f+G9AsR3%(#|nr2ncZy0DoY&Z9o2*YSMtq4N&TVNlC+6|DGjw=Vd9#f%jHfdxmb0^ zHVOdh6e$EI ztuZxuE>>@amyyQ8!qRTMyPi!8Zn?`)rWSqtaebh2@rA|u7|LI-;bIa=NJ~Q_<71QY z=!r-mVy3A7;Am!BKuz55OqmiAf07l$Pt>P(sN2+<#U{U1Xl~G)I5{PGiDI*3;*?ZO z4?rq7+nrzFc1Pp&>O${hXq9k3Mw8`DYX<3i9;6Yg0W# zKLZ!KfPqs_pj-c2XWL=-sFB0~o>{jXi0lrL9)Yd+^S}YfLQrm4Age{SL~9dmIuwoM zV5@Jv6YAl5+S7KJ*BD5>F=$mf{J-4Kipz(GVp99GD1LnJ_Y(@MvTWnl_8mT9g$WW8 z61@}jCyl^(vdB`+Le&x;#Qiz{2m=0v8Z%CXfN!aPOJDf)$aK{bO%p93PG&eR^JL0B zKOl6ueLrJqX3LG? z2Y!HJ87NEcw|7r0H0ur)N|K|Z&Z9^JQ3%5bI9LAsN&hli3dqRj%GdQcojA#2n7mSK zkB*NYuZf5~jA>L#>8Obk0Y!lMY-#FFvO|UGmpFPrIH?BoOYztrnHU<{o&6E6whTld zLV3|GK%Xrc%Q341YT2)p!frXXu{33RZssf1AM*}=Oy(^v7PmSZG? zPUFDl+5=l|_w_z=+cU##T0o6#rQgC7jnQUpshgeTLb>3M4B^DO6OVEDS-X=P}so%lQ&VtTbDx zVn#%C3iXIVJ=XgCK*?RQ!y=u)^^raI4yATb_B|XXK-`?{3@3l!2I4AN+9z~abgKHN z(>Ay3qxn3i(@jcQ3fgYJa2AuZuWS!mH74WL8cJ4Hry?*Iyeq}?Rox#pLc?a7pB#Y- z7+uc55Df{BgRb3WL+YJo=Nyz9WPuiFU!#=8@9pDvym`X~fD zGCFEC|C>BPt1}>+4PH@)0Wob*wRrFOuneeY#VnWl#`gYrKXYNxX*bW2+{^J&*1}R3 zq18O|y?Q<5>Csfqmr+4aqOq6BqGoIBdZ$}UX~niIVv`l_Xb1c>wjV5|*q`bT>`U#^ z_*_Q8f`L=y6vY~>>(Lk4)X^2An|wi}$*C>RHAQN2U;+PViIb&|*zQ0qZTnC8nhv15G=yfM z-p2PXl~!87v%}BZ7xga6_uI6^lZp_pQD=_~z%Xz-T#(`7>?r{$-2S9-kQ(FBN)=WK zW>mEB*IJ9-krSP!y?sh;(R1Tc$eHnm18sph))r1$n|*`lG429PpDml^#5ea1tFRQV zACf={2;Q6m6^^(1aKj~*yK9pq&A`uvxke9|3keg5)BvMsnQjZEY#M%> zGXS>|vb$bwh=z(q;y38dv(0LoPty=^din$h2X|(OBxozVD6d)J78A1#;5B{8bJwmDLH8WB9-ev)H_{iLW-r1fWpSfn7#XmhQ{li zDd2Rn&|X65V#I^E46~+b~>iLe2cgR_=K8 z)ubmvLnZG+kAhJB4kx)*$()A;#O^18-kma{`uO?+TZ8KE%RSNsw3GFj}pRrx(Mw3xWKnJ0ms=J~}1 zpW_j>CQW#Z)t~s7wi)$`<_#nIT}n9wRIg*dfazJGPEAVcc1=;_#e893Kte21`!058 zbJKT5_sIfqe_650t3t`@>z7*t^XdAXGRbuk{d9Rd^SSl1Q+E#!1{9K|61Ias2hJ}q zhm+VB)nq8r&rGs&*L`Pz{?sTGl#h_BqJV3F(iY7`?d4b6P7TyVRP^oqfT^FetR==a zRt`)3Yt1ul!-GN+XxwbFyOjmNzirN9lFp@488dA$?Hbqco6GTLJBmZDuxX^T+u2qD zW`P|mGc&vw^2~)(cuhiG<(#9vLh`>03x3r(uHtIa)Ehn6V~sJ-i_RIz&)zwP0IZuhUo@3PF0db`H@ob(es24*iit z6#4P_e9=lNrVa-V4vW!b+JwmvvrMet;yE6ev+)yad~JhLc8;IpzNQ*Z)!kcG!O%KA2ABefEoB z&68s{n{b}!3f_Zw3?%>*Vqrur(4E9IH93j4>@}R^e*%L;3@MQ1g-0t>pwsg8kPta~>XnlQsugboEBN(m6xMoH;?a3!? zpt%cB;=Eow{qmWox{rtirqhKEKoe|IjVYs6>*vRh%#hzXUl0&5wC?+(d+*#|AkV={ zc>pgGe6l?lHj;GK<`i z4n|W(YJxLGLkE`q;jns)mF;8xV+g@-)+0$77#R9(fO`F}28o1(e1#=R7Y7%|-_P$I zH1vQ6FvV9XC~*AWeI3T1=KY(R0nlY^s{QSqJKU>be$zf*1$W@Mny&b%U};L%hR z5Tb&Fg@#_<+yE>p2P;D}UP}B89Qk zY?A{1^8+GCOsu}K`H!gDxv{-H8zd|&2-#~RB}lNd<8w`7Y#X?&6S8cIWxU$rQDrq- z3rM7>9X^YECR}RTyx2(8R&Kxb9c|&|OeIljqv6mipO6q%gs1E`BOFF=p zSTaxl^wvij%MrNU?u6e>E+1n++|Qqa?yY*GIR;}va zZY?`+*88rt<{Wd3-k;vpB_KwjE)HZjj-Gt>OD^XJV=iA8d+YawFCx>Zt*e1Pg zc3bxxF-CGnT3;{z&eIJ89w`o|LEZyC(qZ?UglB>+#ZgDO(i>gZ1cqXmdd6MV|QB z!~D`mmoWAELyFX~&*|}0mYSkY!;OxQ=UW#=EX5CZ8)N(k2tioXE6tZb*-i>4d@oF6 zdFoxn?NS{qR|KTs(WF<}T$=PdF`YKlSW0!^cIuy;T(j?_QdmRS%+%|~Z&ux?*Sy2) z{uJN4@(2hC$QR1JUJ?=#+U!?yzBu*Gc5L`;8K~DF6S8g`)$Gj|bw0@9u6$*+{M30#1O!q&1NWx@xmx)Tx1*PxoBVq&JQvKH0DU^o(G9aLB7n3xJh zDl>%qkmoS)@7*5exhz|yjXxt-IuA8wHtFqJvpV!@nflakOQn%3)Vh);4gOoR?{vCJ zD>Hi7Q!@a~onP|ja>x|Q1w77C?c2@1vjk9gfV{IqOJuat2lqc5VPe%9QM;Ks?m(Ikz4}< z*y#0O;m1;~7Ck+^kQazcul>RfXE)Ix%ua%x$4&+d_0JH@bx30h<&|*s47G}_YZ7-p zKamcpt^3&C^(a83t&=`YmAs8~bE^m=SJQb=FeFi20Za)GPrkF*VwH;{IDsm#IE~f$ zM&omHt+aU@TvB?+pk%Txn9Xr5sz2Ow0RvLyAO<7T3TS4x8INKW>&k`pE4gMHm%)f^(V~RKHLiWa)Skbg;}$=>XXFa zuq-60)|E!g$k_F3ROE4Da8X^3`;P3Yo$cy4!;Rd&Z(N_GWU0aFV7<3qET%{tyzF%N z_hYjZ3BgpnSB zzJ4TOI3YoyTC)jW_p?P|5(=vT11re}#S}>Lpn5iD8s=CYO~Lb_Pv8gpt%^Uc-Yp(G z{YlD5$FSUb3oNTXxrJ(|$J z4~7XqPvds7-R)qzS1GdahKLK;^Uds>oJ3|58a!LX5EM`?(?+7O5{5g}XgsEke)}bc zLa9PIaGX_x1bq+q@F|qYnzCKg_HgLaYzl+y7#crDAWpx%&dzL89h(9q-@Dr(l699-m+*3xn>C1550kTyMyft0MU zcbO6zDk+6)H%i*dr-@L&7e2D>HU;^eI_or6Y6U(Gm%58Y=?jAm+a-s`mQUJ|e=SZE z;kOvFxrU3CbzwLocb61QT0+J=3&-JGB$2W!|9UwYHnapK}JR=EuZo%|lx%U)5cF)<4gbks~m+I&vAx%Ks6%88>_|@b|V_VE;O8SZ^ zFwE&)KKzcNTW$p24Y$`|B?Idwy$*v_SSR%GV8<)6T*@hw@J*tOI4XzKA}KTmgK9c@ zdV5Y6M1HGB*yF2Uo+9d66}9SvZ(px2_mpTA6r@U3eEX*?)LS;TkAjg7)!@UgiGFVc zIBBvvKpGCFr~ed&KMF(}{HZ`DtQ3dFsxK@kczq*?zJnE@$V8 zm*}gY4*f=Nv``UTuV2l(ZU71-$8Sfq2@MUZpOs7b^pk)5XyVT%yUky|3f#~fV8#i@ zo^R3}Xj3Shs-&Z%P{`$UCWKbWbJulZ1X4`BSGt*P*KP}blB&12?yOGdKdy5GAbt>e zBz-l~%Vw7fl+zb1ud`cMh45($PpfrY4har3Bo^+*u>Cnox0cWYaThF*-mAyI=oI zJ&0V}h%b0masM_M0YiSEHtxz}hkbJ`2RSklk}N7OaM z<5mr|DdC9;{$CFJ$B#JuTIe(~v!(JFf*b_w?DL?E5lz@Z(j{b=%NqG?D_^S0KmKcU z^w;=la1@7iOu>yg451im%QMij)e;?@`Q5--5KJy8uI=y# zKhe;@d7?IgqsbF@X4{%%Y7t8xPp(e}6h%b8_%~HNOX*;kSX5LD#z8H)o4L$cV7)0n zz7Cflp4RdUU?e7HW#Tz`{vFJJPXS^gmkuC0YaPl;G9ELrmig~uhH#FUv^N;>7<_;K!vC3P^Z(*e@?Za3G;m>vNyv-X)@#}7$*7xU zKmRM)Sm&@3z)KY}Xa?+2A_CF&e|ec=WF4=$2s>S2LL^`%>466id`kg=MgnG1P8xPX zcKV-Pi_?SbWxyNd^HRGJ;m;Kz4NXwSU+Zh-&NmOypsMnUr~vYh$H3z9vVqr(8Yvk2 zuL)Wt4kJOogTz{Y0x&jTyza2YC#58&AAIZ2^L&*e+Zc&@0uxZM#DM{FplibZ0q4+A zFY?)hPDm(9|A~>|6TRSnIlX|nYZoiwuWdHlhnN6U1bA^VpFfg@IWipXx#`e1%D5)| z4Iu~mKi!D`8ozBE8@RYVejizITc$9LtZqK6$MXv)f9)qFd%nU< z%bzlY8a3t?QT6q+*AE=w?C6L)H`hI>>Tjq}$ThXhcUmY)8(=F`1~MP$O2z4V6SPmHuG#cW8){9SX1DL z%b&;K!qZxL*yw4cloa0jD0kMrSu$dQL#=UU80Sy+(4x6Xtz2MRf}2paQ1tQF&3tL^ z%$45ImrSWUV^t|!+&>=c8<7sB4j~~!luVe;cl(q?NWqG3Gk+fs@bFxN$tNidGdpz` ze41Qgi%rYsPuyK(K?rw_+I!d9 zHniuB}d1LwBJz67pRo3{=l6*=+^^CTEk4S?wZ; zxZ(lfKNsb%^VJ9wqci{-LHLd9x2oYt>04-ZG7TU+#MY3gq+kW1sm4Z*0LGn4fP_}6+9Bk~A@&kQEj2N<2L)WiJFp&#JYxR$& zo>u{2Y7QBy+NL7I#R7$b=-)=>k=oC@g)NNtBJ4zzgp+;Ru^AD@HK@8FA%kg4R`pgU z4d$h5k}lLy&o5VW$18P)t$ch0*+0krySq)gW+t9y8Fv%G*Lh`@AJEW(N5*TM|M71A zwY^^DFH18lrKumgpOLfE6YEa%IUSf9{EFvNvH80JWD?WiXCKM8>ewCs(~iz8^P`O+ z`1|tz4|~IZH}+4wbbB2&3-;i@*Eq_^_g@PmdV+Gd0Oa2*%0@=R@q~;N5Gd&R{t4$3 zeN|v+*w@JCF1JWP7|efj&0-n?2fO~?KY;=~g0cU69Cy6R_S6hZ&+6Zc9apHJTaCy# zBB6kjBmyJ_FHLMq0STUewhKTp_=A=3U)#mYjIR=UpUrF`ti8eF57wv-TT-yxvoF%J z)05LM%RPVNaoAf(QXx`BAp_>-FVW=S?*G$19hY_W`+tBl_}|+a|0g)Y{|`Pbx;?rN ziR2am*y22{%lUyL70ml`EXk><%`OUKv@^7U+v&%SONZoH8Wa>%JcnUzYN|URQs$KD zcAx4G4~K)1dH7+8TJ2(+N1H?fy__}Nt6ifN8IAU9`69k5AWK+oa;cM;t2}v|!fK^6 zGmHbEs`T{qks@_1jwd>IwIvDk2K~t-!RI)D`Jz$fi~_$d5M3KjAn|t8q%Vb`7YL?` zE;+64S)DKUPl8QGfCfEl8<2WO-7W_c8f>-z8%A7fZlTT+Z?O#z1B2ob1{$u%S~`WD zVi%?c^V*%$<@WqU*YHLfXOVHm`@dOQ84=OZ10IHEW|Zy$Nr27pa$gjQ4A6dlkJK1_ zELEj|G-iczP19Nse2?}UDt5N5#pO~&t27<6rf?LdN~OhF!OJ`MKTg_cb`$?}WbtV_ zW>Qi}BA5CmapTpSD#L3NPN!ReTW%j8^43CND~kntZdiuEN=qb(_}UpjCQhXq>5KJ4 z;tfZTl9Fa_4`I!msFbMM?PBrmnE^S{h*rIkp&{9B_E9Y)00^%i96X%gybpZjxY%Ps z=de%*$f;~b>$$!HkG9scE!v&w7LLckRIa=GH}54RBpCF5r9Vitj-=Ge?~ZeMJSf=m zxLzLv|0Q4t#V`~BIm;xcQ-2|0EAU-#Iv%Hk(HLTmlq!x_WKGlU-3^oFbhUZYC9u(` zfj90WfSJ4nob*IyQ@`lnz&&ieJ^ax*mD^6tHhoCG!hG2((#Pi_pwY7~Bp2T9&t&&D zfRA^jfgu_2h+2+kQ#7U|(*&jhvnd)jj(7}ZjNqoMKRh0H2s(GM*{z7oTkpnNOy_^m{hF8ozat@wYekz+-^4Ib1F=1p`RHWA=PsO-Dy(lJoA}I}@Nj zN?mFiUCNs&+i?e&NKV_QRySau>)8OaP22lfx+&;zTsC^6-`|1769e|2OCa%D;|Ctj_B^@#<EDm zK@mGD_z}#>v3M-Za^7u@+00K;l$Mqu7QGA!3?uS~A2{oJmpPK@`yMHIyZYwey?~@A zEkJ=HV=l@^J;!Fb908UICpgS$MqXg~ZHLinaV9}s%5-9w$|zW2#0)~l!fJ9~;>onA z@S5Fw_3Bj@{v`A}@5+ap-XmpU`K`$AU^?#p?~mwsUkav^E;rkU162YOek1Aj2nY}G zR|t}Z;Q3qYWVqd(14Bk5C)7%!$8;$Q)p(KN2o6W>fOq5~IcBbcZyDv5H#>qyW~K}o zIZSRxX?|_1==7&|4FC4#RxQ;z&^DfM;jrrkWTzl|m?Pc46AARtXEbXP4#!vy=^EF6S9nPZt97SrN zc+ip7pIUUBB8%|1?k^A2u+zH~Q@PCx{r&yXDO;)IXf&{ytRei?KGEFjmZ8%deYpMo zo7r>=M!uD^6QJsWE7^8${0&ub**$Ql%fJi@ENW1r`VW%*fh7u9QaIyIo^?<^n~@aR z{}4Tk=nsz-G*%uS$Wy^vk+^d~Yra?);B$!f(9(vQ5|u)-#=%Lan;6S;n<`GDwmJ z?^PHk;|_=lfqi39+L}#t;(Y1 zwh#$!XXOZp(17aj^qa#-H}=ABCXTw4;(JnyeAt|tK;TkSl{1w zy*)3WNn!W(^&@-;h`Jrbm;f{QVns?fH#ZI!m!!dju@qVlcUv${Xmq+5+?`-;Px{9t z*{b8OONi~^#>c(c<1h^q$-*wa?bo%uf3M)K|(nRI&9RD+!=cu*B;{5g-tkQsB zshIxxB_p+Jtr3l?i)iW6YA4*o-GyMaI)D!|FfdFB`uO|nBD~)MfZngIU<4pimkI4v zbq)B5`@zV4$!rXmj4AB)T^rETFJi;keuX=V_M=0)0We$2>^&Ff;@BB*hI8;Z%V!Bm z4mtsCqOMRlVw9=!MAz40@{k=N;nfb`O-SfiuHcq@E-rIqlp4SUQVXrHWHvg!Fl@GRcEC#) zU=iCnm0M^!mAkBw+^51PsT4PzwMPd|VIdHR`_Z$GHGU!CaS4LrOV#IsHG27~e$e1enGKoRhX`2{cu`AV0dkS-t5;8t|o^#N{z;^?R-0dOD|lyGx*=iBJ%Tl zD*#yFWX9{TARWH|T{0S<~J~?x%g9Sr1ID zU-vfpV~QM45S8PAsZ{`o^LfvwO2oHO2I74EeYN?41K@mNXY88canR~hd#MYMcc6Y$ zn=V$sa#33MgVkzvr7-qcYL(nmyzY;WkFPeH0vvMj$>r=oBzXt#F8fdSa|OC@^#;t% z%*wsBrLjiK(f}QN8Z=^VW%6YRHXgoZB6_f=@}vP*>buPW7UOYAa z>V%uv_SLig=^X}2bw*u!Nfx$>A3e2N>#Ml0EcZ?Br_lv#yKt7Yf}Ln85nL`;fi5az z_kv;@z!3>$I@bI-7-{<&xJwQDqoMifposS8s+z>(O69EgXEW*pT-G)ysHmuHi|<{! z$FhX2*cxmT=tWC$64e|YPBtQm%8NEM9PxYew*f= zewdg6akLyUR9c#Mr$C$A{`yYVOXedh zjRQ!R?iY}fFq3L{JN>Nhm?}auhpJCBn(SdOc&sPM z5ggNXL7E7QeeTp0QTh?M#qL&`;6lFHOhx7g{2%OOAc}I?$_?I zv9Usa6r3+t@9uijbp6B82`IE)sgy~XZ(Iq5U}Jbln1|`kg2Ttbyd99Lr`tP;#LilL zavB$)=Hs5czPl^ZUyt$$CS|c)s+G6U7Z77jB`WQ-N8qmrm@pikHy8@C05W-3j&Fpl zZkO0_*W=qq1G7Aji{-{h_GTL?GTXgjX&>UCe?<4>VAOOIgs4>77GzXzF&%XV9(Y2* zF}W^nydiOf8ZQ@c-K=^5Gs>n-1}g`C+x*^4Dm*S578ce>GB=J&J+O}ksx?_<6be@5ilDCrWLzSu zMvTP4lcqG;_8760YRw$S#6_c#4h{}g*em0RIw|B!6hsC{<&DG}U*Ayun1%D}Z-=|w zo~`Kgqq?*QzS?tx&(4l%$f3R_7*BTUsZ>T;Y8$kdQmxbE7OWJHQt3UbI?ZgmnDi%4Rz2x#htgvSV)&I^dBR^hl= zLV-3}C~d(madLJhY`X=X(M$?ziUsV;%S#g$9FpKa_xi6P;RL*2;7m_X!^TL<5$HuS zmKwjyUd;{EB36IJ^X(UJjdmy}0XKkLdps5zj%anusF$D}lt`s;1oHx$MQOb*Mg2d2umiHHj<)i>|*562VhG=P&K zmD6p@cC%2M(tR@HXqyw75MqNtmea~VFOfK0*T{(oUNO%?19mJu`r6`qux+6~X7N$B zXjINdJe_WC0HpRkME65zVIAZ8PoQk8chIup*z6LG5@dt|uTQ5P>PErZ+FI98?0T>C zhcW!~-Gz}yn=mOq(+}1kGKaXaJ>Vyg?V=(xodSu3!a8yVR~bOufi z-9rqpnB93^LeQju zN=iu%Qg-d0nBs^ISROat2n&mv#;YIp=u1_1c61`w`Exm6L#2V}4b>)t&_CC#B;tLH zMx(z0KLRKl-w(&x?M<65X9||g*Nm}`85{s_D5;!ppsZV3#u<@dg6s`EF6R%Ipje;! z04fqKuOo3}2X^RgapYe8Ya{(Cq6#L!DwJdn^&|B4_s8RO{5@QrIO80u`%jtwpF%e0Du^S-j{RE$x|7 zv9hQw{tyX;P5dc-MH;8;^^2F`J5jSjV+Qlr$G(D-w&3h6M-nuXuY#mC%WOVhs2D(P z&e9kQo+?IQgEJfkan4kMbVTR@0u{?cc4CHJDO&%viiODo%Lcc92&=V zA+E>5AN(vcc6_GxjT`o(rn*=dVt<^-dmvt!r7l%g)AU z3vmGCo4*FvrL%XY{0iUv1bF0#rhgVS%vKB0@n!+yy;vHpX&@1G<^53{?9oT9k4u%t z@#9G5128KRKxXijV=)?!%P&i3k4&LIt-;aX0KYE(JBNeBDJIh?lf`;ynn}X%(UD#| zaCd_#3l8`9t}kcob7;eZpbx)-IGQ#6mxcPBGJyf)B|vk^(iO!9a7KXYYJcjzWR$ZO&@ECde73H`eg$q&VAxuBD{IZhNZd*fY1Iz%=!~=(_q>SN z{qd*nOsj&{Z`vGFsUnVIsy}_dHDDR8ouSbjE;ON)_=6MFI^MV>I2Vi?97GDbkBi#* zk=yvbLs1yn!8BrG4FXc}md6Cn|2z=0@B9A!^jSQilrN zgRM0pB9oG^gQzQ};O9@;*wR~K8y&<2f2MuUK({601EcHd05Yr`+A`Q2IROEm^M*{l zp3G&?TZrKYu1a)DW--89M5raGay$O{($zOYt*5OoiWHY~BONuT$mxEz!EK?Im6s?% zz0|B>jYKTqyNrSt2YTr;sXVjpzi;NZ+%6`mJaH*juHTJjwPtgI9@+1#I+m-oy|W$F zoMFuke2uLR@Mhe$sa9NWKB6dT#&u6=nQM|*8tM5&8GZelUQ<)ElZC1}J2w}K!vgoT zxTsDhne6zu*Z{Pu4u2Ne%x6mDPa~gxxAXUSxNB=5b}Qyu$sb)#=BnhX=$KOGr0AI^ zb_MCBjH#ZahLkQDjHFQg$&g5>1SF@VXxBbUcp7{8GFg7k1N-%!Py%mAi{rv0@gE7^+ivVcWO%;? zq<%JcfG3U$EhQP!dTD7%Tf^PA6!*wqdWgIJcyF^kj02$Fsa%E@mATyt@Ed}bD~+4$ z2On2vDcZ_J#K9phIt$r&P#h4E{gkD= z;nIT^v&NG$gO}Lfi{AXqF9lYx}j6t23OSk7c5mFvp^1!5k0 zG?N;0GOYld`elE&pxp8w3=a$RhQAKi&R;7aVF}czfW(YOt5TTp0x+?90?o$ONKWHe zKFOTSRz)Kx7gU57#7JITx_~;O!w_mB>J}FB_qoT@atkXm#Q#)`|NR(vptVgr`pieV9a1$AQ$vyAGE2u7|zjat!g|a>V zT6-A}C?;I8>i~o`P?^cWN^S&vlmJwR3jK~zpv#nvuaj@W{veuRB)oB{#U<`O^L}S% z$4d7};APkNlnK6v=l!+3MkDaT$cY(iei7BZ9qZ_L1&eS}uNqnX+?Lr`KkyFY^L;;1 z7anxgo%bDdURYjjBJljgr?4C_o6V2NcDxWV0>q~BgJ%#xTjBD+K$TcdP$(ud@Em;1 zb0xF@MEcX>XBvax?{~q`(b1B@e3|)c%xvmMpRoh7UydW29ED6Zk99wiKKB$?;m^9sA7(AaG0!pT=&fGmlXKB z6UjS`Z>Z8}({zrOj*pyg$k>}(nve6cLpV=VD&$DA_c!pqw{Iy|i&)WB$TxonI{PYj zEG|`IPlSnQ-|Q6=E32|iL|2v9>E1WZHg~tJa|%%og+fJzk66qP9dF(f|5I~1%Yoj& z*nIwT7#&ic38N{pG%urh;X`OjRIWs;>)bBrAUN!fPYo|W&QBGA{4GkY46pmntN9|e zI-1hROgqQB%L6T!2h68hb_EjgSmcqkYU8O&tEgN$y0PayCzzmJDMYbM^Yup%w&C(P zo+S*JU{(mK7AwlCDlNbX5*isttCh|MB&7+J2AIrd(Y2;z6c((E%9UnLAc{_v*>XBV zR~QQ`3B(X&vT>MQLs_Afs>1%Rq&bB|&rpf#0hv7=IYtd9(XSF`Y_Cc+vVPM56-e(T0SCgk!UXBmjFTZc>f-jKy0x?9W)3=Dqz2 zh0X2vb9VuKUDOI!ELY zHX}cIy^~W?A`W)XzX*>Rp2yP#%O6UTgLp~M^3`m$mfTOF5`os)?ur#e_P_axhRH>` z8Uo;*B#^MUAx4D?!Kn45I|m1DJHL6))0kXpvt84MV$Bz<1{1i=DIZ1q-dyNHqXqj*kx3@*+wLN+ zjZ1B_Z}&w#H-u*eBZiQU>g9Yj-YCH)r{HPlS5))~ZHBUyEL2BCxAmx00ZP(T(a~I! zv^iyR8DJaW2OVC2AyX$>sBlZ#`m8bK6BzY@IJgq48Z4&RO+|Y z`z%%)e^Ez4<sDV}>I7x7#h#5kzEK9m=}B^v=dfp7HoI3617p zwcBX5-b1(}m!y8dmJk?43KACpJ5aU$^NF=!^^0${rD|oG_iS}WbBGzkg)tsk?cgk2 zU>MP^v&8chpzs;X0@(Oi$5L6{)#Ws~(>Odl9rgK4rlkheOA>-HHEUhF!OqYnZJ>=5 zlJ-gacX~T$>S0o;)g7UamL2hvbiLS{ep;xLwxrvJ*rt7yKe#d0-6gBYrRi5o>S|Ft z$vAL7m{-U~uvl)Us9T!Q0}uQ7yvKAX9*ZLJOH4*H);7bD6^}bfyRpKe0?V)8`|qF9 zbbLlQN@6#8?~cyrs@g&^yrBfnjpvK!fLpZN>(5kKoAc28#jIsCe}rH=zPiqj=%FT9 zEEdTm_RYz@sJxh&ebdQeK-Gk?;;DV^;mt#k=XJ-bR$ZnQd?5{H>S%<=r9Z#oc@$BE z)h$%<7+P-7<*39qA~JagwT=U!*Y_}Qa{Z3D{<&*6XS4Bh0}Rqej|WquN0E{O%k*Dy z)TVD*!Jss4rP+ai!rhaBk+@`?u8EGO(`$*yW1Mm zfT;*n)5Yw*>k8eepb~cep}TTFE}705$3J~Arp13%C=D85xXX$T+SE>)?p-fY{O38vT8_?In{&bsyf#^GqmU-J8& zY2;}kER7+!$+9&)Fz2wxY-aIE>=&fvVs(SA|0QN>5(%N$OQ0P?eq{XgfDRlk zRtBI_liYj{lNA$b*Mc`77|hToTs}J3QS&V%_P6N^V7a72R_aAE!^obR?;Gt14hZ-x zgV1(?)CLL0MaV=8^Cs?D1+j?g6M%o3!s*m8wbV&|!g9Ty$oMXONPJaa)dCYX=ICJ4 z_aH{m^7B|FXGBDVh`!!XA__*yHGjncFxqV3@yYlKrN+f6>Dd?WWKzu@Po)Y#W&dI9?QR)cm0@r& z+?p^gnPe*c*WW*{CkPL`ksE`Hu=w!*39UG2qeaK#tuYZe*xYp5pQVF{3gvFQK(3JK zJ`EE0AC^ufy$=0hJo{X0C$?D>uHkvfU6w>RC=H=dAvq)$J~l#MYEsWrMmYBjzP^8m zx>po~KA5U!_a>Kp%ov7vhXh;{>)!i~TYLC{?Fg^)_-$ zLM7^#B>B*&379at=$Y$t8ygdL`MIroHThc)WqmYrsrjP3+Th@sWa+l6j0XH zi;S3*Jb>;eHxGV6BV*&)Ma*Tm@aaN7H;Jlf$s(gjlL||t<(4Rv4KngKT57Obo)9GU z2d$S!hOP`C(N`kzllBz=dAeNisjcirP8@1*ZU1 z+n(q^Q1bi33ghkqKrO%o-cre*fxaXJYZ)G|?EY$DrdCCwUYN9bH zm1HC*sa9%py+I^!?xXcsTnp|%$IRW*)4O2OqLmQn`u>BwB`7Eej9TzcZ?2*Pcd+&V zT_kV&4r~w)%+A$2-a=2{WYv9?(pX=QFG8-$2&);BKn%g~yt;AQLdiv2kAPFUbOO7= z?W*fQ(A4Dy#~n6F$B3ErdVTo9rMk3H*g z{PS`2hYXlOQ5-?gXwSeOJ%e!sI6J#F^1L%ZOi8|n|NK#04 zcXiqBo=+U<_7tnM%+K9`YfI&EVRjhCcFp7h$1TiN12p`x|}B{8pdZ+EzlO+-pXGwGHLAwXTieefRk?K zQE~=|7fayrXa`xo00csrLlGB83*w+>^JUi+OsC@$6UEL^Cd*d7?$=u>6e&ctW(bPM zvhrA$$~RP=<2CGWOtxvQIGv%{?k#KF9=Ybw!h!GgF#F8S&l4I-ye7-30Zs1TRoYoX zA&_mUFc|bOILzv|wP3g(>Jd47C#HoBuH&}iBzDZUb6~*cvAF?cG}D+^&XkD9fhI)} zoZUB8M!ivd7?a8L_TmHva01@s#hI^p00!SG!{Kxy@Y4X`^Dzhw2TRjAwOS+cV<a#y^8_*@HwTkmKud{wsp__1mxa z|G<=qiHQX?Z$Vz;?fD-P)JZUL?hQ{-7+7qWkk+9f4*}^xJ8ed*uc%2F8Rh@2*FYUg z(jIJea|TJj{HfmLB_PyTnH`nRVn5B()APg>J{b3I8*k@>T|JnY*$N8_KULY6kiQm^ zDUzc`Bg*2YQ=5dW{9MS7Gm?ge66UO2!D>Vg;20+PgKzE6Z4X>iR z$zhLjivbvI0XV?PDDq-`!&X!x_@FWfR&?Xvy#VK_!g%yBeN~&yqzyK)fFk0f>!0v z2;E*V=?Y5)4s9beT<(1kYQ70dSgjUoz@AwGw)frDL(MA1gSS^l8PHfCK9qx?!I1sc z)v!-lrrT1>%gdl)=L~$zVC)ED=H2$YM2S5{w|Zo%+kjL>rck~B^m)EUKHOcx;xQxh zcV8RH@zi^G95mWNLPCO}lEr*ILeK8aUYVySFJRa4xZOstcZ-9?V!6U?;i&B)7Q|2j zh?sUu`MbC>^;XBezR}TRusbLR86jmsFO&r9bRQfX(0SyE$4O!HJht}=XJ-f)BOqX4 zq6e7FfxPnY@R7!gr$U9VkFRcCzq&^#-RMtglF8s(n?eKQ-fnA~jbE}&QYlzKm1M^2 z$qU8?UHDzqZMH{Yr*RZ&s89As^S&N$aj-!~pic&i#o}k2%-0uzBN!8T+D-W%Pda?x zlfwjshI)AsG(y3_A$ALKegZuUSzU6(X{aX}b!)!%Pss227`?)TLNS|~nh0KS+iX>L zt`dPL%9|`R9y_COvu%LE1+b*aSQ?Yu-jOAQbIUvb z6fk3YRSo+hb3kD@y9OXkEv|FMpd&cDS6zU&|1pMA0r7$XASQu{1Z;HMLrL&xGUe3! zBKDV;V3HkD_mR_ND&I3~zGe?dk&}5G;Ix+8))k+D_-DyAixZQ3yf=TJPh+bWFGIZ9 zdt3Dk3}W>P7+tH@juQS#G4lX~6rRCALRf~;dC)Q|=AicX@86`MSYX8!Y;%9;=;#=Y z=f}D_pR)nh^6~AhrGMsVQSsO(3*elPfe>;iibQHQdt4S#LHCM*QIpOI#0!qe2^VR& zvuR71?GLHdeJm*+ifc{+F<;8y7(b2x=e;wu9g=8HZ#ZZeD(wg+%iV++Ma4b zBa+L-9<&Y=^!V)e=Sc{ChBerUX8a*D3Go**M9x!Ko%+QZkc$Vv^c9bTBaY^akX^T- zlv43E2pERW3K8ef$zJ~x@0R0Mh@uE)%%;TnRBof$R_X0j= z3Xi0wa5zLnQy|F84=9!>ma2vtM<^LDG%_XD!eTNDoIC=&YejpAN|itpwx`8^rp)(q zZI*bhhf8%@oYW2nf2P84=8MefedO969(#YuW(kBc6>GtFTZ~sHq@>_@eVAOX+#ERD zR%-S`PI`^ieR9tR@iG407=Gph%3N69bc>c^$ z{aA}YBpk#u_G`J)Zs8Udis1RhQncx zTmZ9SVj{1UP0Pu}wb?xsPxnQ@`;)uZn!o6%RE&SE`HZH3_cy>1aeZX7FAji=Sz14K zy4(8bbPooVLV|*#{e~?cSPGdFEtl&FEV8jV^~fO$1iimk6P&`JQwB)tss1YsZZ*44_G=Q$}i4h{~&X3*>lpr_fLY2(2> zEQ=W(1L4s(QxrR&&x@EnkX>evrKKk_9>KZ83vQ$Ln9XHaU0~80pDZ)Wu%Cr-`CpJ( zv@v|!rcc;ZtU77Oy-u~yY~0DHSVXw@*c3cQIXB2(e{ z#cu!D9D`t#(bzQNj-_Q=*4BZaDNUw2BQ(ETMBJHnKR|q()o%n8@-4 zD~LFf97c7p(1t3IM*-IYg4kESPN`xo2zF!t9?H%u>`R^$$hh5{n=pKQbf1;mIioww zYSttXfArW=#_rEz-!4tzSO?*bRZ|v(p&;&^Gm{9^X`PsWZK@=;B^9ZBvrZ*{{bbxp zD2Tj3LrKZrPKT6~cOCk{%A@i-JB3o~{au!D6jPd7e=u@kQQ$Wvfspx0cs>Cq9)Y*5 zL_m{Caig+aD!ez8Dcr0=_hj2s%F;4nqb7Y6x=qNE0w zT2)}c5T_?WZlp;>k-!+u!|ldZV!=Q9X(V4n5GJeEA9Q<(9k_ua?3Zo9;YWJ&KfS*P zpplZLy*B*$D*VxC)Mw|}*FlS=AneC%G7%B7i#RCZAm?3~B3)jLRmwxVS3PUpZy%op zxj-*frPp@~;y!?qxaZBAHwMGWIoYQQY2YOlNae*1QitH)B(slvZ}Y`%uCo)=NCyW< z_ceZNBq&ZvK%yI*1hdZHcXzqx?;&#a(4y1b-=+lk0?#`Vx}KpSi~S<&y^W1c3ZvB; zkpG3@@hpobJR*WndcVFDJhu~~(=v7SW0gi*V+uQ9srQoa>FSH>h7COl4xQY*_kF(( zk9gE4-^5S<2pa_-?XcFTNts8f+3ZtRMoWmQry>nFq{}5<82!BBqWTt>&je8ntiJ?N z-G*d1|M2?8#xNlCl|EXA6AlBo15wdFK)d#15%0xL1UmNXXssVeXlNkMK`Mf5MnXVF^w78B{HSSpz5l3Fa_M4)A#4J9Bdjb)^Aie_=$)p z1VOq9S=+Tc$O$1JAOO^H@o$52Imn)K;Pz*7uLNCQ9xQ-R3NW6n%XaO)|4aUa5b4j0 zcfL?u6hwvLRfgPxm!{_g1_W`Lh z$buLW7gb7CqxtmY!ouAs zuOK$P>7^M(&JRR0Fu?#gL;$k+@s-!c{ycsK5gQ4k#Qot8ocGL=6hC6I7=zHq7O-Mh zJ9LYaj>;@I*!WkS5p6t#gi117%zjS=?e~i6O6RlN3lLBQN|Q0L0Q2y)>5C(uAjal1 zKZ!<>a%eRAOzY*@q<}M#%j10m7-r~NJPUo~^8T5YC|9`7Rpm)jDHWXF*7O1mYkBTO zuCx-hL%*K<1uc(5Z_w=Af>x9B1&CPQq>}%Y!co`S`UpyQKX|P9QpqIy0~A^%&T=hT zL6Z{Ka}0VT@B^FyX#|`5K5Jq#IrG+c2!u#kD9&=Gh)-PxTu!GxoG9b8Kyahps3%&4 zI;2)>aOx7qD6%l-M2C%4%j zdFoaUI!FieHBnj(#Ei!C$Vmmgv^?Ns15q)tHBxpc5eYo)%-}v}WJcP(5UrF?Y*R5g zwmTdzKw<~*!@wt9VxadwhXsol-YDe~APyo5uNjt&HS&2Gt!?z_?0{Z2ur6T3;qajf z%5ifsnO%RY20wb6W3`&y_g)_;YR z7$2Wm@Ue$hx%_t}3e}(KBL73}PCtUT^&lfg6bS{o6r0UDoq}S1#dM}5jaBj7x zZ@73rWB20C`>rBV`2laUk;9g|PlgdV-Pj*I!aytAF8O*d^7olWX5Kw9ir3VC#so&U zS<<12Oki?Ipm`LSy*aGW1lI5Jv$G=l=ctTw^}%eg;X&!Jfb=U7bdSf$f-k_b(L?T` z%}4V((h+o^lIYwLXEob^UecZAmCbC6AuXSJg%D7pE!gkxFV_U8KIVCa-W)jagW=NM z)kiRp3HhFQb8bv#y8G{5033YG1rsx~=G{nRV~uGBW<*qkhYwMOd7)D%afw4D@c?by z*%x2a3cJYU?ZLK3kme}xyF$cCoGj?!@L&NJ>rryrRYn&KV1hroAMVcqk4(BW$Xh_8 zQKde0=_|xznr(e1WSSA58Z=mM%Dsm&985+*#P%9Reo5J*?zw++it_U1V_~ECg zcAc~#tGYA&=jOi|NF{pv2SP>oB%S`b1?S>yMYoEWg9khv?(VHBRR&*z`yoIlsk&5d zqMA<^(-;X&iDeMNhJl%0&TSyr0QAN_(bJ0^w`X)gcK7rU5=Bsa$%Vj^k@rObOu@RL zL+Dyq->LSw}Us1&Ec%qxFN7_Hnt+FRsAxp=0gscoYZ?1JgAaks)TL zI?1z@yk(3uWSZSVJdn0^*0sC_`rjEQBuC37dq4nIF=4`hXJ%z-SvHtZMXda~pdbx& zV|-GeAvMsEKYaM{J2gF&_^O&QV)1pr*7ND1?-*xHDH0gucG`Y^|IwO``9TcCK|_3c zPUXX_6^+W>i(w&t&ZCvBJO(I2u`*n4$L~g?%ge0}Y243eTm%w42A}gcc$P<_66PgBI4j6;UZvQpx|I3 zQ9Xb2UC7t`uaR_#7``A85ix58jz|70XIbW(k+QN6=ocq6U~vES!!LaR&>>t@0}Bfi zePaTU?W_Gc-s1W#1ju^*n`tc)2aE*0-9m=_MkENbs=pb0c3)e2-~J!8y?0cT(bw;Z zT`3|;S3p6E(gXygD^)2Xy@OJv3lRc@q9VPCG^r7gUP7;dD7{0ZmxNBJ2@oK(B=h)t z?|biEGizq8nYHFG7A)Fxo^$ru`@8r4?5nDV6NdkzlRPy&eet$bn(Dt1%mD80`Nxl% z#>SU#2K{>p@k3%_l1l%3CpkNvqrIEZzyC7+A;L{${f!wJxkX!VbUW{*i^~AOXaj=Ze?O8l z3~0JR0s=hV6SWR6bQBh}Z)AE9Kf*r2Omql(%_a2(n(1A8I7|x8-#Q zSwI+b_LwXbzN*zpeK-tw`884XG@~Jy`Bhng#`E=o5^d<&6%B=q&U#Cs7NV}Y9@b6h z_`r{R1k^;as#4v!*%X8cJ!pTVQ#?1^a`hECp5Sh!06Bl6#q&a&`g`k*?Hf}ZBk@wz zJ1}CH!$6|Rzx6mB_|GWLfO$p5_Q8bHcc+AGy10N~%i}PgFcsGIEfZGW(_qd&IwEC9 zRxPJPxTSR32Rb5Jl(rf!18N*!c%|grEcqbvB@dHtdT=OgZfrzoH#*q5WMq@bhM#DhTC01tp>{tYg43+^XGISJhf6~f8M7tzR=E3#eBpGPDN1IBM$Km(o z)MbL36)p9_a02XpBs~L*FLn&65nBM24=jYi+|vbRRAnk4&L7@FGzdE>AM&%NzCm@^iEGLfX_Ee+mov7=6P z>qjZVS&$v9-`@GFQvGBBlflG!I!qXNIO9V@0KPb@mDH%GR>25RqxLBQSZ$_E0KG|7 zcO3TJWqM}00EOcWDeQ;Cs@#(PsOVG|fDP6m9eVkir7U>!PiA^$rHGX0T2}@@*kWY8 z9c@}GtUK4!W9-WC<`PDmLr@p~tF`sdYLtVOJmelDW3fa8U)OYk+8cXe7{Lu@$tb8J z{YTw!b$@ANIw8#666CkMn9T1UXtTQ3>~YAM3^e$}Ec~XZth`;Cu#-N~(bWO>JCT)@ z6^L@J%QPcBvaF((xGbUOqPe}X999ui(f*eA#x7wN0Ra_NF>u>)`fYWMhwMbXGntxJ zCc=+C490V%L=>e0{tAQt`M&oW8O<`RgbU3!y*~IHx-yBTvZ9XjEF*%3zjk-DvQHg^1g+Qb zYd;|!@N46Z)4YVU3f{GrUZ6u(yUWgQp0mEwp0sWx7rG$!(Ir6~P3bJG_cOz}a~|GJ zGkos=t)52n8_l0^R`n(Q+18z$naEc~$UNSE2+3**V57Hu0Ljl@rCb`Of+>8I_vezp zE;r?`&e9v!3~^a6oo2y7>#H)FQLFa#Pgce)hLxX%s!gZeRlrj@8s47;gwUud zTjr0aXtaYnB(VoF%fE06LDYFmfon=@dZ%p$117`dKhx~nXPV|3PnS-l%%9$T=EY>9 zrKhCg%|_M9>qHS>ZKX`_XV{;5VjQ-)KJ7oBl#U}(+1E6XKo&X+z`-Ajl*D!6?dmPI zT}0o~T~A`0F{tiOe4&1iGmPLa3vlS7x`>3PP zb-gY>&VKhm%dC!;dbingFC#C>E6e}NEz|>Vr!jbKf0RS4!|RnntLs#X_W>GM$OK!H zs#>=Xq*Y8@+!fihTwO49U0DW<142-yJ8U~UC#@d%I*HWk`wf1Z=RYlT*TQQ7%H8u3 z)1PnXzP+o)^58+6hr^viTf)nbT!p6nLX}#Qf9PwD-Vxy?$5)Kok^0sEc zq>3FhmB8yxDi+^r@h2UxATMP$5hNt*e@k=fw#gFD{b5$#HT=5pZx%Y5AqGm7T7v`2 zFvWOlZjQMYU(DM0myjsgp#4G%413|_@mDL>`bCy^4z$1`aY56H;cUYj#h_^ewq$80WGAmfwn;y=oxU` zT3rCp@FA&bX#l1OXe(U&`IX=AS>G$=gCs!9<@l`j#`t@Hep%3ZX!P9IVim<4n<3xh zm4UaBZ!oSe8yd4;1pC*0jz?B{MEV<5>^HVn{FwS#_+$N9Cr#uiO#TIhSe4?Uqj(e% z|C>g~Zc9<>Nnq5M$P86OP(qGGvKc$%zRtuG=ng`Qf7``?r5Z(2_HfP!Qhq9*YolQ8 zMh6XgRTo`$xU#9DLro6nLpmH1jn1+VZ06+)*omadfloc_myH%bYu~qiqHNV{X7ng| z`ZkiQ&%_;*mn)PNwUy*%`Q$9RL|a5L_uNEIiAzEahr(!N!5dz8#Rn{Lf*QrblQI~Op+YVOvTzkUe6 zT4@=znS^}{)u6XO=XUs-c5T)m)42A1i!5{>YEv@B^%rvIo)Ag-#xfI3FMiG#O!!@~ zO4HYHB{U-P@AFh*|EEL_T}iicp_j`KpRvA%zU<59O3-4o&3j^Bcdzl=$qQv2giil& zilB~f;ZbO~_Z{-BnwODJ#}cX-9%K05Fe6{$CD=e~eNhQ}XJ8mWu*{HCE9dM~XW+99 zyBMyLSYj;hT$hp8Z=hIz{r9&G${ROsE5-YlOjb0y*4-0#l6>;y$*wJH7(JwZ=W5;h$-ZndlU0+^Y`_2VM-_GlMG+7g3TFYmM*#YJrrCWcu=2i!wkvF+d z7Is3;+5PMXlUqy&{a%K@OHzQ@h_4X8X^V-~`|K=+#tRCFGT%-AY3PBJCO!V?bI2;L z)Jp%(A$32gGIfk6UgHZNo193`V;dftB!Rd1Y#snf^_7lQ+*z;K#rMGfaNnen(ts|~ zW+SP1(i3vXSt(5?8Ezf)ob^qaV)0~gSd4L_(_mp?&(BX^IRwrDZfN zo4qnPN^zRa96i~SdE)w^qvZD+gX5SE%aEKP_OWQ3cH+Pd>m%VJEPS4>|6LTR= zRlQv3HYi4lOb#pk7O#}$>bCQJy*!>DKdYSe)|~L02Fmw4*n;|_ZWAH5u}p`fmu(VB(!ytzOA!pXP&X;J$p z5~Z2ERGe~^;qM%9%Xrw9cHUN`f)+Ks$(zgVM(9&7sZt+V+O630hw5i$+%n2wS6Xu= z?XQ{E`!kKzKV1qb_jucSjyuzh3Vvm`TBhVQB0>ABygkP1tQKCL%753!ke9B1A6dj6 zQSJpNCQ{7P_OL{{MNvmm+dlsUMc~3_C1K%NA(TvImd$~0pFhtZ)=qiwNhf*@#~TpK zN6!{1BVhpy_Z6MD`|18k+h#TMTWF{%$Fh8%(jg6f@U1H*W)6ziozlgdWs^LIT93p0 zXA1TP86Y~St6DH~X~K4|(yv=|ko%O1_dH1ll`S$7oGpf>pHX!Gtp#MOd!2D|>rZNA znO&voH@0($HtziU_Y|LIT0E}w=ItlXarv^>uk8XEVrVbhbB-xHmX(T~oSmwNBvlh- zuj!j=eoXW$lNv2C z`+FSjpT<5UM`q6J-EQ@!d$cmMRE#C}eCE=jThO}MFCUgef0z))qU82SNO#v~51lUmzkxyKTm(`7}~;eLF+HB%kWK#4k_ ze%?!{E&6hTgOJL-7Vp)|UIO;_Pwv|vouHie?*(mmTszAMcZ`QAso&wg5f^bt8S`vo z&0s>pMuIAVa$2-J;g=4>wlrgJh|6N7QC6yEOk9J{kw9uaxahne*Wlv65;)O*cN!Ti zj3%Ng1R|d?UVd$W ziT|Nt3RMj{&drhSXo%56yW^qU3!_y0frnZ zMc)f;w=`4F6LnI5`Pb0CfO&Ok{-Cr86asyVC@M-G{^@kz{*!=URUc9Z>iw`Iut%Eo z7XqOD4i4=q2SZErk_Tk2Y7 zYZSdnFx(_XlCOHdd`WH5aL!9)Vp8pXn9o(6H^>1N7tI(5dvnbDC+UrkICC(4b4KL9 zS8g49c{X5o|B6AAmd5E_Z<@CcaCc_rlnWkN>|Lj{n)vy#-rDyK#Y0|lBaY`H;@B^F zK(bt&?jgZxr{NjtFBd&Sr_4-tlSMu?5GQ9smQ99l`-Ua*AJkX*^hht=L0vud3wO7X ze~eZ1XSywavcEi}0^%CvQ@_p35-`h85z!jN(byYhOc*I^TLT%}-TBpiaFBI+#YTQT z_4?U@nT5ofo?YmDaG(S_ThIU)X=};@DU#}KXD!s`QX9drzXK1|N zkpOas|Ncv-#3__?dvX6fqERJO`Y=6N$mj++t#)(akR+If3)hsLv%11Hx`M-yHb*tI zRA##MPDKC8`r;7*JER#D!#lJ=0q8pWlEd(QZIU9`-Kt&lRt7-RWNQ?DNc`5=w6?-b zM~7WK!LH(Y8}Vx?HJ^STPIwg*_VlO`aLQl9?E2%)0!)1I^Nn6Ki+3)LjdtHe!M$(T z%{JO)hRyQZ?p9$2@(K!`J!Jz=K`?6!dV1CVS14aWxfp4-Ba^9b+>rPCASoIX>kwB$ zqXNv~8%z<-RJt5KK3yFUJb4CwY0(uB-LCs4A&vt@OGVQgF9x>8ONvVxjm7Qr-dloQ zEIvJ-UOrOM7%QJ&IT|ck>dY!mC{!#T-Xh)=c-pHzlvz*lB?vs3QkWBk(aw?&e0cze zY_Up6C41u2^j^ngi*h#A!{ol7pS;lE?*#AcwVwgqCIopz&V+=Sw=FAfzVUO`l=Q4* z*^8ZbNx~U2bou6w03Id)epL?KpRwyE>@9uZ%(1Pk=y{tkos7JXtnUE$de7ksU0i(B zzKSbX>1cS4R9QM47jtKNf{MK%)bAsIbj)yGbi`4F!`kbd>}N{{PY zf{7>X1~Gk0R_9l|Du!2CSmVd%ln=c!dkzN+^BSzi=63Zij@AYx9FLZ}n+9qvd;ZZu z_Vm1p_^runBpewe|1SQzoyst9zdrLW3c1Mj`BCy2+}%dxJqWM8k=6jfUlJSYUY$5e zxoXN6EH9-fTYd!_+fEEeP0i%wq-JMp``Gg6aa|6~!SuIk320I4e4k6UzuLFz{rLM` z@|Zm{!Uw;Z*r6cuk%WoukPSL&_-UbYGVCJd$->Os)CA`<$mf(NV5h6wc}XnVEm=# zF;(T^J8DO`x&63b;p`eKy+#xfwV_&O7P8>m8h<>UCEr^++GUR1rayZX*fAgp!=js7 z@xuX2ur_=_+{m@@C~C7h7b&4Eg%G##SNi={H9n5rV%>C!oOH#F-vQ59uT}Su%0b&2 zpPH5`>4sJ{`7D0%5|O~4Qto3|{W`c~^RjwU*$3Pajk!pnc}sHQ^=*mkKRmK8REm&# z-D_VwMgR2Z2ec|IpVgdck(1Wfs#Xqv%N8HQU+-U)#shHn%asPjlYdr*q0fLs_xtcd zKzLr4`2|G2LP)~kXul+gk@V4creXI~X|yFpg^=TycP&ZqKJnH!c||kxoz_1MQ^rr1 zRJ2z9E#l^+t5n&u8gko)M2E zr-Ud1^)sB$^x0^TnUxU{5*mafKf>{1i&HHtfc^N%oFbfY(nxqae$roJ19W;GMY67l zRnotr;;Zvf6#ZlH((}9UX?JID=ZJZ4Vd{DzU+rz{{Wy;08s96>o&`s}us-p~7zDI% z=WQ`8IHg&^iMaI$K$*4?g58f?T<+850oC%q%$zX2`!(9R#;d|KYqh5yfjU}(;kWgJ zZ`F(?L|*cEsBfc!tA72%_tmy9O>AjMXNOiq*=~A7$c?|Q7lc&GR zZVUK+k*kfqnkVcmPI~-Ms+WmqP;3g~&b41kc_Yi|q z+g7~Xc2xS0Bzt>a^gmow=mYX~ahUnOav57$4@97#?E#wt1en<=y<(D)w- z|8#&53?N{Ox`4v>e_C|-$Nz5}z4f4N)RhbaJM^_(jaM2kBT<=CQ*6#FA(Ap+^c}}LCqMToT9=D&`0$S?;W#KbGS@XWw zmW$7GE-O+{d{Vygm4bpI;`a3m6co3)Z(XFIc&L7V1-4ee5$eB-O=lr}07youA~t?k zDL&>~|OISBVC2UiKFEw&O|`3W{X*i;Gt3t9=HRzI&Wr`VBwap`bVyDYieRns)0(oys5mo{xJ^v?jOS zb^YtP|0^O>=$R`a;O3m;WQ&kgy;7~yILTKJwx~cp-X4wX93VFyh8{OM_LrHt)uJ+{ ztIy;}Bfsk{(uY^zR}9|t#A{i=B0HVO3Pqe&!d*>w zYA}J zJCI%7#FxB)PL#}l_;cv8I)+1klR5oG|jg&Uk{| zQ^Vdp=)Go;Vq%|yB21sThefkWozL_S$FP*cv4HDjehUI#dgMSu-UuUHYc&BF1@T;= z2IabWUg>%zhJ&E2HI(v|xaGM<5Y+#GBxxBC*fqf75e=->Z0ogq$&h=?&t*r>6e?%3 zm0B%4@(g4w{k_UeYi)pTJ|Cslv!=5{BL;wyaMCrqT+zjY#ron4I9iNgFWMK|182O? zEMgTl4&Y@XYXfp~zz!wxsa_}RqWy?@g7xUY0@M-v`8WniE;mB_VLxMk>ihSrY3bvUN?gIRxW7YOVdV4mOW@8bH4gB8SGwDtE%a=u6XDh@ zoTGR-7!5vnwv(Y@y*EB-EMv%CP2+PIvT9cP@uXJL!hiK}tF-=|w*%B~X%R(`UzoY; z=02Nkb1p7Z*>1wbHr`~h4e`jlhiXEF(w_H1I^CGGlFPVj51WrcHErB$10cisM01qQEHCBf%B zV{DeV2=hQ*yzDMH0Joay9@$1qB&FLAnq>r;q#Tci;acRt=(*9MR-e(q`Wc^UK_gSU zS_309F~+&TkXp5+|JDKuQ26{mRTA;_K8@Dqd%3je#^d6D;|xpo1>T6e z^31?2E1umA*bh(s?FUQW#Z(h92Y|Umfw(WPX$$36Y+aUXvy%{PI zTci5-E)H=mwClbalS;8J0fTlJY9eWN$#*8x+cNsSYXj0=Y3&we@p?9LpJ8&n@Ax?^vxEbQq@g805Tq?eD1_SzY<_cepGCRzAOtxKUVs795 zh-Bf-^0)UM6bbUqd~o&W+8`866i?~c>UlX{EO_US3ygTPb)$*@wKK$4bIxg&tqR2M z>oVKyhxQ*1YU-9%yEmJk;ba0q0c$hKM;gqM;}0a-?=lT0yH}D_)4R5Iyr;ywp5TQJ ziS@xiBSk$Pd)BcvST}Xl#ol7Wfj890@^E^=nodoj{>N?}+-fzAVot)CPe58+(j5oY|6YW zAt%$u#Xg~rlS=3j=$h3DB6M?fV@>c8Z4_&2r-g)C|5Zk1MmkzbI+$atid{b@>E#nq5WhalT$cx|O&M{?Zp=%#ZyXgh*! zNd*8CIy(g7Z`3gIm*4xmxR2M4v4kWQkHdbnoZ~`gf18Ui}RoHWTw75{w_OA$~1C0{XYkcU$Uh zXs)sQX;OE0@_5gJ$oSU^4<vrMo&KwalO(!m#5Mt+S1Y+ zT_j>E9&lO{K>O@zzW*r_0!AOfrmG#g12aAT>ZH50j_>UZh3f!qgk1rrduPFjBj?bVSo3<-R`C$u+Aoa7fBdIw8-q~NS!u4G;V>(6s%3VR zEmG=+cZ=*VhQCuld6?CU4YkO|+ULK2UJFc(Z0u}4{aX#-Vi~RxtMJ-ox{do+fTzH2 zbmu-^T2~XX?rEBIMKz)Q9qz@?P5gi~h^f4CU)hBM5Mh?Mhmi?rphH!DKe=xEQ+8jUmJ!3tk{^PM@U2*pMj^kC6Wp% z5@1^7&L5$re9q5K)%VYoacraT-%H#E^q;$Mp0V_7Q*y0$`2Zf79?%J&%4SR@@{K_y zQ`FZw8kAg(^6bEZw>(droXgqhnlh&IwK8=JuvOdz`V~2%D&x8Vshde9=j2=O^(~YK znQxh`S5+c_`^99veAO-IOJ~0;MJnaJZWTdG^u-jXn7o_dy^bq*Ov)Tg6FIq2Z; zJx%fQ&#}jHy0}Ho&D-DTuBT9HuwN%rOIrnE0mIexe(_Hj*(O)Z)SgrTfNPm-Oyb41 zB;Wh|v9qXBilz9it9<|Ke8}?#`eWzIZNR1(Otm&-k~<03ZHO@j%K&fHLxkkFSS<;?2V*Rpg64 z3JuNs=9KzY`7;A7t<;PZ3rtt8fO3NqFC_l}{&=BDuHTDzY6^am=D znu1Z6u@lf46wnoI0<_LWs#%X0`_9W!e2WYRkX!@NP!S0}nl{|a`{sSX`O1`XKH8SL zf?pfzr=a+itv>#EJf+C6TGb0jA2Ofv{5l22H$Lct%M=tZ6aVXz&$lQj?(1B9@PB>q z|2+`;|K|rzw(jnuB$dMa(@oN}GM>F#n7yP*P4;a&_5jSVoBa)ck@ZpIS8G9w7$APy z2zwA!(*poJuLc}>z>W9WteVbKZjQoz@o0|YiMcOdsDlK#)TqhWS(w$`O<-=bwr!JYV4$L?>&zH6qC{c;OY-Ebd+X7Z62YEi z%ZO7yR)7u6cV23Nv;hBj>bya3zhmW6)WhG_cdnWNC0|GRe0dro8?>X4ZT@s_dcAV;`D8c=Ig1Wc7SU+>QDK=?ULhI zOyosn5z!zWDpa$=D~)64GcMux8u6XZkjK>mh{;v@pNYjR$&Z<`M&AX;NCYg`rU0z$ znK}iVkpd3HVOL4*)|k|CM%1J^1ohdSHvGj8p#7!wZ0Igciu2fMhn~cYEuPs>np~Oy zp79z!9_=m2s$T3$KWu2+VuA+gf~WleACe$Pm?n?LDKu;;)O$A{4gpuCAW*Nnt*0$p zu5}h8>3Z_J1x)R;+SzCD7}q*Ee^Yv;C&W!o?;!CMc@3hS_Uv7CvRS65X?O8>u}Zw9 z&ry-z;bzc2@=Rp7vb)|42y$lXr5@X3J;%9qif!T)Haam`rVcGMHh#Tw0oV;*sQ-30 zaJ8sVIt86J0#+$Y3e<&gZzbWi6`TN1Grl{EIlV__*2pR_@Jvv$>(bhqpy9KfQYu4Z zkJU5W$*^d@zQT6lgb0~PIEdUIhAOxNk#Ob+wg#Z#&!bAZfmjSdTq_aDRduT!JR(0^ zTrqy-A)-ExR!w)yG7Hr;i>~b~?>tB_<(N526sw=|YnHqQzda1kHO;yDT8cOaG!m)S z%91q^tl~}`DB^@A!3eMx;93)%3LzuL@-n6L+*}uNrwRXj+;WN)D4-;4Wss0k}AU(~+Ie#V0gZUt{ z0yoqIZ{568x?yK}!^Wv|D;c}R5gJHXgj|Ac%*f>fZI35^@x#Y^VGl}5>$-fIELfsC zI_8p-w}J`iA?z^&6L>DL00`y-a6gVWHHLvuD(}bfD6Nto3Qfdtgq6eloOq$$udCMt zBboeLgXRmed_DpkXB`F$_f3{rQ@inT0vh-8c+@6tPQj~h<^2xws{LTb$>D-<4v}2a zZGjw;M1RCTa{^JVVJnB{Z^|F612Fccn8oI2osW&mt7fy#*nPKeOs7qQ=7B0{2z=4A zpUvBL%OwP{54bbzt>BrlqRMKAOt9lU!qf<$ql5*LUB`ZJI+vmr2k<$5Afw~{Z1QP| z8hZn;dc&c&OvV_g^qHK4Ce(6sMX(m^!KX1kORIzsp1!^GG-A?2z4hRHbZ=>d%M93_ zfqR_w7x7t*+>>x-qhQ^`^VOfj)_sNX?UK_jQH9Ba!289Zj*1r?EzP5=`b?a*0Cu7* zX6%mpI-0+@956b-k%nYS&oftBfKIL;lj|I7mlZY4;GJ_f?i?!LD_5TK`6e3-bBVgW*lr`W5{v5B6n)P>FI<*uB2 zWQTJF9-XAvMY8`qXvNnBp^tHJj0HC0%_d`o3-L4$E6gqV^tk^8_z2ED`^$#dISRtb zP`UnY1jo$vJC@JoJ#I+nHu_#K+AqzvBz=qK8J0(|ex`1)w*XTYLtXLoiDE-yZ>!^O z29@xWWJA+PAd zI1}x&$&V4F>Uy3%i+VAHee zy`yCIR8p*DfRPtMMiv=m$qluVX04r_Mjwj|O1#nI_GLBu#XtaU3bfDE%{^w!cg(ddm) zPB**R=La@NC$lf6g=}i@7ho-8z`-U59wW7nd<$0=d+5Cyb2806Tv3TpzNhY+i6z zK90E9@f)IP;u=%SXgc%VY`}GwTljrmIkRs%Sl`GTDq{g^PjfB<;bStz5S9*sfL5#l zgO>*ehfaQM`p{oi*SuX^eup>2oeT2LdL8^ zl9s|dpgD`39uY3LU<#)TsD^t6@N)Iqmboh>24*~_`E)6y!5~up2%QJ+!BCH@L3_U5erIAs z7D2%=&f~?0fYX!dzd7v;Yknac9mo4;Z=u5P5QyHHg+)~?@_cGo^elr`Ls|$jA9mK= z+~L#7vo);86Q2N2<5YU@o?lod*vxl){HE0T4{)HxL)FUIXQ#*4Q$_OWwHw4j*no9& zz^@4|o!H6px9la$#6DYV_qt&&IuJ5j3ratZ!e z75r+DiJ41|ZBgXpt!pjkqKPw^;X(kiZesu>9Ff^)R_V)YpAr)|)wJcx91vSrv(0O3 zubD34PJ;W;u{FqGAp)v0QIhnzEVWJ(ZX+KWmv~2XW_%{t``xNhuK~G4K0k5%Z{J_1 zZi`$=jsVy(Q@qa^8<_2sD2cosE0=8%#b5+r&Sl|o<)FRu_{%vII?;uZ-)Yx0B9u7V z>5=n;Bv#b=)ZmZXEqL9`ewVChc5nLQ@@rYljC^$kQ9(W=Rr#xw0So?}EkaQ>Dg;s9=?u5NmpGRI&W5Da zb9Xenp+5|_q3ou#c4;i79FPNPm_(Pn@Q_mr>G-B1`#qc(6J_vpP4gY5Ij6w)RpaVF z^I?nx`n*K=YNzjQHQw@X!Sdu|K*5jLk#L>x&(0%iUFvcCcOIvGtBj(bNdsrH-V7IA znOL~91D+9NeZKAGT{N}#vcSVrrO0|!r)SXAP0nv69JrE!guoZ&I=PPfX3-PL#mQBs zZ?UUF;5Z3D12a+~#QLn?aVBjq)8@__eKiU^!E&y%s8>D33J@ZtYy_q_X z{&(@V$gE_sriS#&;rbfC({qe?Vub++o>%mRI)xUSdkqb*yRZ%gDPHrBl4t+dfpd!Y z7SsE9Gua^3S3B&(F|xaM&T|$Jxz^-5;%_f6Djv-P5{u|>==b_)p@8Hvqh~kY)H*H7 zh*ZhJefH&o(C6D$TjTEE0uog7)RWQ#5pCgg?S{geRud&g3W2fg2FH00{Y)qirH}MW zpY7mN9w~Cz(_z7G_IWG{e6@p(GoPB{Nbh`$&>;++DJE~VG-i|`6n4uRU^Oz>-K8A+ zYf+K&U3KQUT~|(8(ND0Ty!;M9jQcfqRxUv~$T`1T4sWf3vmf%76^36(Aj{#Pkj|s| zvA7vMjJSb8k8e#FS@{VW-aflcSX)w2jb&!9|Gm9c_!XG#Bw+mJxObo-fvEdkRf|Wf z2h`m+f&QVB>)C%xp?}&sf4xLJN^<5yqv{^0qXN3*fU-*&d@bNyOJx8L4N1;UWd8NI z^fM!Wa_u4fPObu-u;Fp-UPox_F?@Gap!?oDHrdTWIxA3DpY%?x4*d>;>Yx5t^LaZ8 z2#@^ro|@|!YVd1xEI+g4XjQ22%v0avWKUHamd}TJwviE&PrGO$;~cMuyEK@iJ-ZIp zFD9Rl3N_>8j{ApUt>@?KyF2jNEV{3Onbo@s_GS>-tT<$Wk0}SnDEdjFxqpb&yw7ya z^nQp6o0I@JWcvMNg--5vMuiUswwH#2Hv$~xm${0%WFh95=0Q=0)#b;&Pw zjybB^?y=s-%05FMbZ83Usjx}(oaY+K?`sAx2ZT?$IsSRwl;`YqDS-SiTk(GEV|Jck z5hsm2q_Zax2|trRdV}C)k74|t*|0Df7p8^|hmIE;mgrR5d#fLAjxYaFBPD#)TF?krK`tyy6a>o zfFWS)U!D3S&}DhvzRU&2&wjnmID3~z(tG>Iww80kpy_L<#10Vh#6r_u>g+Tl9KP+1 zNu9kaql~;_6U9J2O};|acb1l+6nEbJCyb2^Ts7;yzk zG;6)Jvt%`g$F`JUVkRx7b0;r#ib*>C{dK$(1W<({NLszk`-*ll>y37N4pFIXY36}r zJysVjqpx-h8~Y!u>rh$ja6Up%=XEH}?=5#*fCuv?(IZ^AiE6sSu@0AUfYK;2>wZ_p z`(uLrps0#Fb@NfES#An`dsO?VX=jaDyXQzHG6|BwRFJpRfM?$uk!v!aZuME_8RVQj zfMbxJj3yNeDZD&MkBzc)4H@8*2Mt_IY`|KMoIK$xI zOfFKjuqp>hA8~W4eFQDTnMKYqn$s9XtYenRL77@l<6-{Rl1`hwV149=(&lXpM%dnmUrth zVORwJETX6ZZr9V?X&1%5dXwb&FM?iX@lM1i#G&J)aE-ZY2UjE2pnWE;_BsMJ)Ol<3 zKvr@{8u!=7`_^i2^Py+t$6+>d?CWztQDJB6pov1~FTlUil}=)V2%lIY)o)wmI~<$tESyH{Cj@lMqCNj|kfp zPToI*ei)#?3FL}_Y)TA^oamABUMoGRG7J~xX&;%U2jtuO{O&sa$(T;f$-O0a$dnaO z?BneL=ZkjA@`}=g`W3W_T=$g9Uf9FxKF1p-RAs+AD@ICQP?sqi zM?Rx3Aai4&q}c%T9>-Q6v$$(4TIu2(yPHvfIZ&Y%7#93-28e%l-7QQTGfDM-Qs6(6 z5@+F5YxFm%yMB&jNQKPy=|4w1MF@6bJ5HE)-iSIv~U zJ3{zk70H2d4!mnwU@AGfJ_Mq)wQB~TO-7|f-+5yAJ;2%7z7i{AhZ65YWUN^@>3~7m zNhYx>PIrGpBk9@@X+BkVGY=i0fjOqNcCkRh8vtGBv46;dAQ>#mn|>+H+5kYd+rwpM zI}Og>r?Ji3^1C}1WJsq+1?_S2ZU8eKlj-Z3zplhKtyZxb)r|Zcb`QDJRKl4!QEYHF z0MZE3Z_!;N=eD(;k%_(C?5uL09lZsac%ar{ve>{Gb-P_)s~qu8wo?{U(fQb)YO43ja3KY7th+`q}|5C z0fE?{K@Z9C&%Eam-o2xB1NGk`OwI|azMBA4%XRzAG15TcVlb^-(^-rKCFNQNvNzDK zP0=Xbgx;6Xq|c%*s0(aNjQP|?TEB33dH}i%^LhaACG@3se%XkraFBY-y>URzO5Usj zQ26q)rf_QXDk8v4r>2r!(ksSB-)}_JFH;3o)cM{~CuW-9VJGvdQg~0Z$sx`Sc^Dyj z&tqUO4{bK?+3(5@S!orU

%@;Lu=l@O*&7^X^gY$UDDwcMKSqIb**hpNk)z4qkv| zj5Y1vHOw`wn{#SjdeHs7-vw~Lgf~S{>!yY)k;ox-KtD3w{15c{)l^7NbSGa`hsl)RupTpsDD ze@+exiV$l;6*-U#y0vr?JW7|dmAp^ea5A0OekorXf=u$^Jxz~v8RiDRZ#_bY_E)}i zy>zSx&`Pia)IkgY@Vred;Ot{d5$0fHbjj0v=uFoh;12*JSVCGo?A%`bne)X1-m4xR z6{Elpc~{dT1>TAgDW%KBK0X;>{fD@T+!cddVUwl)Qo!Vn5B-oYE7ks`ENuJd%Sk8n z*owOGpN-`+PAWF2PP!*HkR@Jnocn^oe23HIJxgB-c#E4?wR;^<$+yRwhrehp>hK$U zj{o2^S)?}+nxhRRzSlAV?B;4DW~tUG#Uyk}c@LeW;Gi;)CM7muxPSNKK9l2V4InA2 zA|9VP6WlB|C*416@9pGJMPipfO~)m0w2HMl^~O z_sWzs-l|U}pmtL5;Zc;nQC|6129qadsqr_2r3qt}1o>n$QWf^*>fGeM)zW?@colkm zb>51SEN$agBuvpSFcDBt+N^q#G78ra>~b@#NcdJy5%m#$A?PYlcar9bQ_a(_urlzW zGU@QYO^BmJ-+0W>OCLaz*IO*ZB+G*9j9sPXwco_cgHJsBzk#E;H5kMap;yHiDLZU?R0W%>?7+&e&2#EOd2KHWUJv*Xp%g5));?&;U7r!cvA(D z+{Qp9X01!$z}onwz)jU3VVvFowK~iTE$403HsG8s78|>1vZ}SA%4yg ziI}$wJK6LX`QI$Ih&QMbrxxY+Kb~KAjXr?ey9l-rJg*{4$b9zBzeqUxU3|v}Em=|z zk@oYsuf$)g9sU1v z{!i!%FfEBd`dz=qNN-CzBlWlnW`Fr2<$lx04m;PF+PUBPQ=1n^#tMf5r9eQI)*kqE zau*kM-yW5kpqjl>C0w1>5L})}8`^n7w@b;=Rrt~0Nh_MZOm>yQtBpVc>kwW$yRO3} z8kIMRa6jFT8eUR?hT*o(rrLqpvzKwtU)lqh!2k~OD0hyLmA}x{Jyd!YmvMpO(}DhAh5iYUE^Kkbk??U!MwWK93EDCRKnh-D4=`~% z_}!M#{MQyhS-?Ms1<1UhfdmeKy+yE}cLEqx()e;Y)$m3I;A}Cq{;7J4Wj{OLSx|}- zvRnl60XQJTw72wYBCJksb>L8P?x7`OcUwX6aQ9XxtC=}$&YDo_)#?S!kAqqUH|(0>`QK-JIxvT&Df z%EOm8T-{pBz@=gn6=#+~Tja52uavUM#b^)?7tl?s)cP-JCe(1S`Qvt!><1j%B}?B* z&!j$)*%x8=OsaMX^LVITa#7%vPFa6(o@eqEpzJk`t}U+@V1-Frc_8=uVIMpSg!(5+ zpfsLzh1iy@qnE-d=6l1}Khqwya`C**lwJiEVHODFGXqF=)4srq*9{XJCZ8>;DNJLs zJ#4z^VPcA=TM`8wQ5ydxR@?Ier8}b_;a1Pt9dN9;+FV~V>boBx`lAjdH$M*2IG5KB z4N7_s(FZ8vKBHSWt1Wy?FhGSQ!#gkZT-z|WH=Zti6VsD187f23U6X&T( z`(OIYEdyd|aSbg`NNlJkS3Dzs4d8476?H#XLUSng#3<_jfpH45fKS9d$zs>vsv_Yo zn1m3Zpb!|p3@p_M4gpjQpH}kLI9D#I$7}OM#|?-!y=Y&>`YrTY5T+bL7>v*#mbGju zgk>;PDhh3qqoZI-~Au$omo(mR}{z7!GdiOp)665qD8jICTlZ-sb!PJ zq99=@P!=hLum#xyhD;e9S&A7(ifjR_r5Ok$lz^;(fsvhHDTWvbP}YDfF%)DC^w2te z>C`u8>HeYt2;c5>cnGWn5a9@IrUhkGTNf^IF3`H+Jp_&HrkOK)QuL8Q46vgcy<@2rj&7&OfXP8vhMEf$wfwU)kd-uowc&967f2zip_hdUzY=Bu;uer4*i`d6U6ozj^~fh9Soa1;EPTWf7e?lCQJYp_X;k;Ue|B=jFieNAHl z=e;+dt8Wy8IDO*tjQNCxt>7CUwu*u{fYAlQTt-kvly_$ZJhX}#%8AIG;4|%q<}{E7 zxF$K>zTB46OznVNJcD@rdC&&SI3vRT;HqKJktcofD_Z78purzejr5r$<07Q#UxnYx zw;;H*6}F3Nf)+XCOAQd&pdeD`ZK6SD1HYfK8JDr|Wk`+kGnCwz>(7H=kHwUa=M%`6 z6Wx8ISH@V0{KTPHUK)qS{T9$Jmav)l;+YNvhPPHuYIThCqTE>9>#cjCSL0F{Kc)T* zk)`NUu|Ue4S1~(H9>;{vybN)r`jWz_QbN|v4;~Q3@S+MQ%q0*ANA!15AqQ@BFg%4n zULGZ#8WDHm4iqbp>LbB<&CEAJi)V%f6kY@vKD|2;<+jbnM~+l%0k}Q7mHrWc>>LZ@ zVASkbgh-nGRYeuYtg_KN7T+{2!|;7%8y5f*vKaM`N6Tr9^T3VWL&l(r8m8}1Y{|-l z57~nNFeGz0!~7dez119pM3XNhiM8lh= z`U5cbDirL2=GLd%4@yB`r;9vNq0E~w_(|{+nE{G9qh8vvI_#AtZI-JDxc=6b^rTB! z9Hp_8`iy?x(?~HbxM5Ted+L2A){DTorfydl-JS)2tLs%m!TfNzqvkK^)M~y-u8u=M zhFoWW)VXT1q9<122;z4}+tD-oHh{|&n8o(4>S4W?&Pu`p8%O`DdRQ$r`gife+hKm* zK2>zM!n;9t9iGEGa}NG7%@hRUh`hb^rR)C`Hyz&ZH%C5ymw+Aa(%~-sM|I%;-!xu8 nh7hs8e=bY@G<--NJP#5GkPs3Qew()-n_4R^9D*6_8oBMk6{Po z%^T!5(qh7@-_nm(K6s&;5yG67wtVT5p*}e|VesEzG5(=|y@bVKGF(Lr6@(bn!W-Wp z72Z-`CluJMvv~PLjd50}dBtf=FLu}Wr6s(Wk5!H2zm6^=I+TA$ zjIg1Ql-TQGoQ;M2`QJHgD(HZJr^KMc-V*=UWq$wvWv@;0ziAMbgpd#rvzCO1rK7H; z3<>nMR!Xce3kNehYhzVSLv4*i_xIOtTw*ZAnia^1iB&?A{JBj&t&11?BulH}^hrg9 z)yN2SfM@^(&TNo{b#iJ-nZEoOFFlNzj*NsPhcgOjdN7Uj#|dVX=rWt5a_b{9<+#-J zz*&d>|U$99A zePKVGjg5AMPJC4uXE7(7`10D<6S$qz)b;n^@|B6phH-gR)g+k;{kGgvOtOsCy7W{pMKuGfI(+#&fS|V&gEF==`ABf2`A(w~L03 z9*BL$NKGy6L408I`Sa(=Dj%?|UQ53*OYxtu=#PyJ)n>L#jN>xqv~|h<8kM5;Sde$3 zR`TZ4?pW!wvx7tNT6RZ#vC2?9b*XP}Uvw9*%MsE29yT`iO0!!MT$@g#tzr0RCO0YA z0yx?P1hBp#80!Gcmau#6?KIhJ}>#HEX*uJ(wPKv)&8$Lv3uEKDyiwyYTg5QwgIrlz8EIZ?3K;ZZD|VuKtM`e`sP{+UCyg@QKtYuKl_n=i zIgyILwTgnpdbJWs3i!ScS98;Qx3S!$q;YwuAHS#Vm+v6`ze zV0aYWf+yfY{u~NVSg`%>o)+Hsasx)+n|ot3@Ke9-uO0H8p19;@#~rxpQ)*k&Q;v1tNDYeF_Vykg-vS8%`#2 zB@fcF+kT1$Ben?|{>Jk0?S^G5yPrPINkoo%hD&6hv?df8cI??t<wjm5^6mhwAY+^=1zXiHFE(AD0Esgx5MZ(>F zz>EN9S6n=J7sQ`+c{L#{3GW2VK62R?_fsM+Iy$+CzJ3gPZ3+r9cznM1J3Ug)1!s0+ ztKRj8)1lw+{8(-vYx?az_!LboNrR)KnU^DTX&km_fo_n+Kwn+|!)dQT)}Iqs3z zsoSC=DR^AkhYXJf*p4*VT>-BbZ?^Efh(n({lima0xgw3*+iIhbkZ$dF2^D%R%`fZ3 z!%-Wun2fVUN?Y-jm73G_r5d>GwqEDk(vJ%@YF5(jyJ>l?3}+qwFoyJs+AaAxTFBJ$ zX)NOGpTAhI>R(9d#>nDazwacjK;s_5Xi!&X8qVP77NS_uy??{2eZhK`?U zhrOzC9#(WK21ZCg@+kD!mQtR4VvKFZMI|Chd*inaFHDU&9ZoydO081s1xHWUV5gm%|K1R^3hQ0mI$jIZ<8^P{dwV|ZJo%1n4 zf{Wed59aygzKp-UFM{IYE~;V(Rf{yG>DH>HV*Y&h{FFqep?`IjEBQE@$-&Nkyilrm z*l>)N@X@`_Yk#wE!ysSQGwSY^)zi~6qSfW%uiulWii+}2%A=J+!rsS5)2oH4HcgGG zzw|Ij_zgA}5(E83itTTc>o(fiXE`_!aM?k(bIVz#G$M!cxU(7OO5LSY%f9Ph;l21b zYQqv}^jqyuhAP|Xbrjq@)~2WFxS2u!*3n})iJ()6c=*o@Ar6a!_<;~mV!5V)jZNg4 z;L|B7h}U`V+b-x&Ol}`C6%7N`)!E%vw4m?(otZQ?52)2^rUc1$ zby>p9XEJC!Lo<$gJW;zFB%ekhpXqh7%#DJuIrh+jBoXCB*d_RU>YvwoCKs+z#M zlfMOBEtR?JWtq7$4ZzJAH}Y8)`m29~*Hq_jH{f{svqu%qeTMxZ!hz3f2 z(cje5oo?{MHK*08t}-9p?$5mNdxeRqh<85{$J%e-Iq){%_&(PD$c^tQ&kOY99ZuKS9l0j+k=5==*_Yc0nr1}9ObGf*^O&QH- zks7o=u}Ck5WgRDS*D8B1?+^9xFO@hD467TcWx3+hWn*Riw9CHec69l01gOh-qLNeALEo)0l?_>g7lbAFc;jCdr<6;{RqLOFV3+R_m7Kq3?Oo&%xo(N zfe9H4nTTHyk9BmAA5mdcAu1~Bo>P4%oUy%3yMD2c(6`m_jzu~K&XbHmC(y7b46@WX z*L^vl(CTtXQ8S`!doWWOjUlu}gebX9$Dn#LT^vUvUt(5$-$+7vfKcJlw=Wn5dNY|I z1x<6H-zwG>6p1pSR-t@+w6B7tQfSN1sxn0yj7ZcGZvP2k4>SnnTcPt{2dq^Oh*){m zMHjn|G#bSo_U)U@om}VQ$-z6Ewu^F};1$nSBO@b~YDaXj1Ykh-dN^oP5)(iFX7;m#u z5ScdwEV(cUSl{br6a{Go}+dR6vjCK&?W2L+Cz z$Zp(RTv8q%R(&0-b95#skp{-UJ%>r|PliAQ7a95>*nUR zZ=u4)^;dbuYx|(&_cyir0a$`}?@mGuB5_fm-n~2jjBL2Hc09GaHdE^D?cKA*JYkza zqoKa?{?;&jMiktkc1b9{7$GmWb56%EEmD1$g`p0Q%m3swRB*NX_^91rtMa(-H<4{X zniq(2`_y9jL4m=G-6E`aa(Ow7hnrfv&8*T5{|J9`b8|YWa&&vYzyKG$Ni2ngbayjn zo(Cq_JhN5zKP-Uc8*Qk(X!dPgGIptW%BHV>%ys@0qRm1iGZXyWV}K&IBHAaS+kys)ntvW5YW(Bee|D^@CVR2 zE-q7QK_F0Do4`_oZO`YQG}f!F;>SELgNgLGSXflHlvd-}26gAp!^eYZPt%apq0o|F zz;3YHTdPP}-Lzn8DqW4B7=p$>LKP4kSgK9~9fm<0Bq{=5SWrN+k=wj^bM>XZYxbB| zES(5ZX>2n8;&%XpUJD7)C69ydcG9Z%%}SEKwbx_ZwY0SKOtP_II?~+Fv=kj<)4|~p z{q2vmHz%t$i>=~wC;Z;tnS30*hqp>cdu|>#<*H2wId@@n%%8tIR5OR{{p64&K!O_4 zX^;ttmSnQ9EMPnlDX8`e3`0SQhAg+u9Ip8Egkf^Bvewo#RIFta_v$v=ulDy#c~<=S z^ZhgV%EKitG66;j&O)nrX_jwtQ1VM3c6%{qg>HV>+?i4N5{=Ju_immB?M_CRyC24u zodWXfR$b|*Pci|2d##xR2;^XqB58q?-jC_g* zq}zRg-o@v28GSIUZxqQ}i6Y?tVe~OL<+qCp!&u^ZLPEWssYG+;yI#dgUk>#TUln1R zoev%CUse_SB6%uXnXqKs<5vOQ_%SO=l#T1__!nsuXEpJYlPzXKQ&ZEZ$FHh?z%5vr zk0VJ6qYNb09HVy@Sy>TJdxek!*Q4e7xpt$|&ls_GBaKAka{62rMPtzGe#g_0@Cf?WFH;Eu*C&np{`o#r zs-AMrgMbef)NCA@=cOQhI~82RVL7c?_x*Rw=Z&oVe3O;tmv)OPAAe{b9XWWSQ`ET( zL>xA(kG+ugv^K*YN0gkoa*eU6+K3xzyZylJ>4*c9AVlmk9NW1z?}1oywf6R#v)>MJ zZO=T>icV*zRC=x79LT-`^eJk`oKHi=$S9_6*n->j`kHj@UM(V6DbNBXKCJKR?>OTp;%R&%`_5l}6bJ2I2BMkZUy$ha0Zr4NRB zVurq>?&nntm6VF8Q0TO3&pS5@v720#tS=B>mdPpl^32Crg9XPOvzK}*a`J`2L2ctx zdy2iAg$rba)YMcGf`^kreE{oCd?C>)Y4dV%!lX-#7o;@G$SDNIB>4Qo z3u}b}Mdc5tsUzrwy1s3(RvILm=!`iei7yd}&*) zzeFQe`D)1b7HwDBM3-p;t4^Z@U&OTGy>-RD48r`ZDAOuU%;T%CY{Mf9pDkY_$_)A; z?jFzZG!=op*%7*!6so1A_3I`A-5`<#=V+=Ca?}(yG!uqFPor$V>BcxsuV4KR>_mrW z!5kn|uH6V~@Cu}Aa$Vsm!_jj>>Git0;@lfQOnKdCxXiSOD~5%>&A{xG9t;j_<+M)D zd(=aCuWjCY%RAI~2i<)H#TJUi`g_DLW{u`MI>E?M;aC` zw(Qt!l~WRfVYUWjwIl}EMqH~$;B&S~PZb!_AksKUbj@qxk)>X}Zh=RJmpdmH8QAYW z_^KhzsEhm4!1l7yWcyU+1_lN?tTMQYGZjC1QP5f)>NYBn;!KaJQbTqo)XkYeECBTt zvkQm;ush9dHw1gH)$@y}jEsz>Wr@?Ot!Y=vb6+%JOH)%y@k&~Jg>mJTl-yg3;r{-9 zKzF8Ctc=E|r>Doq?;RWzOt_C&VDSGdxD*X2ek!Y#>@%Xv3;Fk~yu7%d`h1l5i?()l zrX${Ii=rq|5q9K&XjP#$rY_V)jhr&AN_Zv^r8NFRaw#r;YestX_Vo=Jv&=v^+u&*< zE~{V~@zR&&oII1^OiTwijYz^gRZag{0(N4=9|5>r9vhFGfsAXgyPmh}kV}?iM&8>u zkLgfb@7saO^IDM@Ly>qWa~^|XB{KufDHnT3W86hDe&W7DXi;XIe=Iu$TZF_z?I&eK z=r>~P<-KH?mjiih{Dk2_5j-ZT~wn zgEDbwNE7o*gp2c{kNWmYo6FYXHpKLRvGuH7ZTC?@K9!?up$FYJkxq+-ntEbh^XB6R zS}Q9Z-j+@BWoA5F+>4=`xPxgswp%b#-fnzC!qx6Dq*5Q8hjTI70(B3MRRa2Jl87Yi zcSAx-APY3Gou9S)^@QjHkL&*ah8DChG|Y!gF2`(#+Qb>F*^aE+&d%vJsI}ULh5;8P zz?MLX?m|Y!!N%sV4LhRS-w6=#sIt3prNvFFn^t92sTJ-L0DxQ9?Gk6DXBOAy_T~@! z2}s=-uQv=Aca!mwCJ!}5 z#`RT+Ugbv1QxLpFh`FuL@p;@u6K_MApwD(TR-5PU-;?7sSaDZP&beN3@#BgGRiT^$#u#A)O$~%(}TjwsmEZSEIUErQ`_TqmjRTA7xVLz z%4#38=|rm_L&i?7jIQoe(Utg|W^D6P?2r+c7D zZUHW#M71mH<5_jdJDj2DXcf6;_ou;2yLK$@-*bgVL6v&-s`bNNFs{B=3slyamOkgV z2FK9c$DW_lBHn3E&qi9gBLOoP?2|8>)G?Qj<%5M6P@BUAp+1=q%GR9h5>=Kkxg~jU zyW0e&0l{($E4{pf-%f5Y(rm^3Ze8@;Ir&AUE?;5WaQN=iT%}f&-9Xj&Pj=d*BDkyz z-bnJV>*rJFEGb9hek+=tB_`n&`rrsPF>#3^Mj8qpxB)P9WU2vHXa|oA?)%jiT519Y zqEgi+%Y+hkCTl~(gTLx*#Ka22h9)L926trdds_Q_8?2{yQ7v9Comprag5t7q&2FGyl&W8(>w6s=d zKNbMeQKnM)i;HmKw^k|53|iKek`js%3JM}(lN;?qa$2St59i8r4~fJj10YublrC+D;OFUnP>c=hBKDaou`ir?az((S&~TpGqelY z4y?l=W$RJ&xX6nJ;`N6d_O#l%YC=YL7#NAS3@LY8XkLZu54{5ecfj1DO&pc<+mb{q z(`o`BBkEE@YHW@5nZB9gLs2Cy1IJ%T`}3baMGjdlNi>lO_gPnfLmUEPG#=YDG(yqu z>FqThG^W>X97-G_3=ZC8BPS7!(dlY*{rYD`{g}t~;99}U>!{*5jhTg3B{4DeGb-vz zzGvk*dT9|xw%PjGe^|h`jb$ybEw{bpm5ypy4B8VZw7^_a(q`~fq2W%+vzb?1y#9_P z(lG+kN8ydAj28VSX8}@Dy5Ey8h6b#Z)Y!*U zgDS~=N(&*?8jFir!`~hwJe!u92(S*(_yS*YUG>u6C5G%>8477)X=-b+TQ9>G?zJl7 z2^(vICGa&GbMDQ!A->ROpg7ZV>*J%r#M19vGI?7HcxqxnYh%gmU;9G_)}AJRIYnPy z)=`Y;xF^y_pXoJUmFTp`+&coSmL@SM+A(iGT~Alm{aZ-^NyO1&E+;gfOwvBr z1+7&`1PKFW4WKVHYpBbyzyhflBv+ zN$1?u3#huy7A*0)A{_?C6M?$AItidZEG{mQhB?t|9|?~?B_~sf1>NR|8Hbo7>yN-9 z%Emj}trdHU=$66mW{VculFKC4G`Ks)5w~SCX5JjmxMhczMnwr9x@IhgVOWh9Q9F8c z2;ZEkJ?{zI*CyJWmTPye+LXiN0KrZ&`L z47XVWqluj6^QwRm9QD?K*iXfoE0hWwo85Y{Rn=llcZg8X<4xr%NYW>Z8Gns7Yy(x2 z+~fy}Fxt6K>^bi~I3~9{@`&4FF$V!OcAwemeSt$Fm$!de*%*3`N8H&^^;7cW+1c$Z zieiEA=NdD}GtLdX_0r{9W`@oIyETj5*@3S;JiEm2VdEp2U*{Lr%=S*kWGLV<68Q}O{=*l4lF%}2N0m{kykGvV8Dq_3i^{3;CR zhUUAw%gxVzO2Ur68=@NP-IS&0q9WSf5Z&IPqc8vID{8!V^tkO#zxkPf47w-l@7OBg zx4R#La9i7zqb`6EEi;IFZ7+dds6Q1yc8%u!rssQx|1}+~Qp{)76}6Z$uhb&TJ6LL@ zqlK85%+;q1ffC9w6Iy9*QMPcWL3(ZKX6DUJO?Pz+yJaE|8!TiD0uGLbTP7D**O7W- zN9E0O+&QN2mFabL(>Xn8m0c)ER?3R+vi^|;dU zPLi|6vMvh7y_B*KlsF%vC}?7$IwU+zCp9+Di-{T(j_IbM|E>*D(ud7w|6pWI?&+hB z^V8xEzRujM#;DZWXE8xR+@zIDU%RAJL0s#9>t^RpRf$<&)4_!q7{9N@n}m)s%Gm{3x*rR&D5$WN%Jx#soR7Ls#T zt!(p0{#|0aQ03b5%)fM1hDm92=CQrLiZ1(iD=Y7}pUD_C1QjUq81S$NG2a5@l1{*d zFPAXSPt0}}7GpINN-;=;@7XOU@#1`*9L_^?VSFFlX#|pbI3v?^n5EBB+Rm@b4{>Cz zmVU9;*b9IphcwEL)-}b9O2Gbx!^dfJXC_1h%8L!6n5_iMji{9DJsS9r+!qhbq%af$ z;7#!&y+#4h%bn*cx1j3X-e@`QnHl{Kc>y4wwGf3`>svjY@Y zP~Ppj(QgqE;gd#O3BX|DC|e=jG#?23aMFq zo~nnv4ew0`c!V&gvI<*JUlb8176#6b+B)_uAI7X}ZU@12+aZo)R+dRa!<_E8)ok6s z!Th24soKe!9a?xS9_1@V<#c^p{dN>aLUIrJTi$>mDL$iGt?=0r<`JFxo=_pX%3S}T zHbO$o;CHl%3_`Z$8f$)=u@kQNE$P=+&5rx63H2`dgEP}f>V+UtAo7qxCo1NV?XyCu z{9|QpzXj0YS61o(i|QQce20P~L83KRVk4*n9ih0buuKTaaGO(+H0z-kRX+9sHnwxSwwa#-i6lG;Q8{0a(U+_B(?sK zhl>Y#4T+7N^?0IX4bV;O;n>(4pPbiDY!2t>G*rvH0&(imp5CX(xjr43NT=+flE@508$@dBZyB5O6m&$yO@4x^gHSZ8+!l z%6s>}C-J*}*NzzQVQ|M0#Y0?N!V6GubO)6^rD}o&0mZP?^mKGCqv3Jcrhor(>mlV< zd%X>nx5CxAV$RVN@zPZ&R1)|5J&)xHBN>VPs+~D*H@lLGZ_UX&&&k7n&c!j>&e7q_{it`k1I=*To~{v5U&zg`p)0{=GaAMyBqD@w?cP{cF{P;+^Cc}2ys;o;cVKY`;?Q9=Fm z=@Tt2t&@{eXvjGdRHq1)fPeruchB(faBnYkm- zv(Cvi+IDR6_vily258uZG6>-HPtg$(5!>5nUNWy>M=TK2gLm&DdzTbtLe@w~^IKj)fwqp$YfTmlO#SVJ z#!na;0>Xjn>?GyqUm1?z3K&ZGe!VFc7RV2JG7(hBc-6=f$XL40X}y_1qF_eOPXWv%ZEgUhJ{AO!ew(h((xy) zXa9-cHh!fVtG4j4`|>h!ab`Mt9hJBMjNhm1DlVW`DiSfW09avji3y#9~ zK*pm2x@%%7QV~0!bN^ug6}dw0@j7@=t#nr#>(^S2dpp3Y;2A+p9n-33QZxRZZpQsA z2eWo`gLr}-_D`uhShv=j@i_GdGK?bw3ntycTRy?^eE){RSwyU%A{Kj1tFzeN5aoCQaNcUe@8!VS5 zRy!O9erPS$T2+ed7FbBh%CFQojC?01HO8(1s7k3i_{KfO3pE60m7Rs9Lb+VRUt&Zx?W?J&&(lG^EaqeN03d%wbXLIL zhlBYSj_%isYO)XTSYfx&C@#igSU7p=8PEX!z_@Z_+(EIw;rQGXB6Kiq6;O7>>cn5K zfE`A-+*qr;V;Py+5E2@7$?wibK{2@cOKPitJ}k+;Pc?(|N_0w(&kb7V;AnHUz}pPP z=04-B$@78Ynp(R|YpB7T`AI&k4$D>lxEUcvk%O7JW8ep{JXnEKE$wm4v)kRRkzmOltZze!(I=AQ2Ft;_rB#V{uFkEYz!|A#zbv>nl8b`>@;+ z*;%A~p?+)+Eb; zR#PS$W7KPN1_3NZ&-M5qzk=U`mxrgACSoNF7290&XcnV8p`u1g%-4GxfuZHo({=0CxME4>eFC6E zqoZkeHc~R^`_l526PCx|Udv$H7I`kT*;BL2#KeSJiTX;jdw6?RR%UEy=*M<1L5t~A zpInJcLRQE1E*N1fY;2S^kA)luLiqdhI3zdQB_iss5>G1>nw=q$7`%198gYNi zCmxXCf8TKN$(hM3$HtAN^8kU>t_uD&arwuwd&9gn3&L&@Zy5M;P;S&sD5<$n04mZC z$ju&mwq9@rXN6wxv$BndvU1KE_8La{(#_EdVdw%Na}rCxpd|EB06?Q}auTjXxqvR$ zD%wp+jZT00P0;-s*s0W|MZx8Ov)OqAk3+_YK@FkB6%siI?2d_-J8^D~S8dH8-}4Qk zze1tJ?&DGKqhsG;Iq3C72AZ1xhXoAI@VFc&&?gb{)+s9g4YLEa|6nQrs7pxrIBXL~ z@w|7Vt?vGOK8#`P#j_bQXGtaO#?%O|2Fg5}&FaB9xv!WOPFk^^khiPY{`}C4<*db8 zF|T@^@0cr0AIZENEo~{3C%S{`mvw%zeb_CODZ@7t@uMrj$5#Rlbuu$J87)_%!^3>P z?Sd_*j*juiVFBN8{?j1TLyD2Kk)`<_>CvHf{o$5*cZm|ed*b6l7d15%Q?}^t31Fsi zI~;wHkdSE1^j0!!pIYV7>8cJvy{4o1#V(V0F66c6@@QX=<5H!Zj@t2{my2>r>B_Jv znqh|*-;L<#;Bxq^K*$D!OpuJRu|K24oVU4oh4asW$;2*WlZn~rii?ZqDnJ?RW{9z5 z{R;1)p*gJMw?2F|^HPL{UOv7%d6VZ*q047xWfdILK9(HCa1>zI=6T7q*FH1EHxw^< zpKTebUB2id#?)ZGN?7O(9w(KI#u$JLz>~tbIu>N(;raY_VC~|iuAu?@=`5+^1`6?L z#gCylGQ%)sue;HD%ID^Kq|p{ssx5z`W)I-M7SG*+&}!)∋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 @@ -1197,19 +1201,18 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ source.addEventListener('token',e=>{ if(_terminalStateReached||_streamFinalized) return; - if(!S.session||S.session.session_id!==activeSid) return; const d=JSON.parse(e.data); assistantText+=d.text; syncInflightAssistantMessage(); if(!S.session||S.session.session_id!==activeSid) return; const parsed=_parseStreamState(); + if(_freshSegment&&window._showThinking!==false) appendThinking(_liveThinkingText()); if(String((parsed&&parsed.displayText)||'').trim()||assistantRow) ensureAssistantRow(); _scheduleRender(); }); source.addEventListener('interim_assistant',e=>{ if(_terminalStateReached||_streamFinalized) return; - if(!S.session||S.session.session_id!==activeSid) return; const d=JSON.parse(e.data); const visible=String(d&&d.text?d.text:'').trim(); const alreadyStreamed=!!(d&&d.already_streamed); @@ -1217,19 +1220,19 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ return; } if(alreadyStreamed){ + if(!S.session||S.session.session_id!==activeSid) return; _resetAssistantSegment(); return; } - assistantText+=visible; + assistantText += assistantText ? `\n\n${visible}` : visible; visibleInterimSnippets.push(visible); syncInflightAssistantMessage(); if(!S.session||S.session.session_id!==activeSid) return; - const parsed=_parseStreamState(); if(window._showThinking!==false){ if(typeof updateThinking==='function') updateThinking(_liveThinkingText()); else appendThinking(_liveThinkingText()); } - if(String((parsed&&parsed.displayText)||'').trim()||assistantRow) ensureAssistantRow(); + ensureAssistantRow(true); _scheduleRender(); }); @@ -1274,6 +1277,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ liveReasoningText=''; const oldRow=$('toolRunningRow');if(oldRow)oldRow.remove(); appendLiveToolCard(tc); + snapshotLiveTurn(); // Reset the live assistant row reference so that any text tokens arriving // after this tool call create a NEW segment appended below the tool card, // rather than updating the old segment that sits above it in the DOM. @@ -1310,6 +1314,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ persistInflightState(); if(!S.session||S.session.session_id!==activeSid) return; appendLiveToolCard(tc); + snapshotLiveTurn(); scrollIfPinned(); }); @@ -1603,14 +1608,25 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ try{ d=JSON.parse(e.data||'{}')||{}; }catch(_){ d={}; } if(d.session_id&&d.session_id!==activeSid) return; if(typeof setCompressionUi==='function'){ - setCompressionUi({ + const state={ sessionId:activeSid, phase:'running', automatic:true, message:d.message||'Auto-compressing context...', - }); + }; + setCompressionUi(state); + const liveAnswerStarted=!!(assistantRow||String(((_parseStreamState&&_parseStreamState())||{}).displayText||'').trim()); + if(liveAnswerStarted&&typeof appendLiveCompressionCard==='function'&&appendLiveCompressionCard(state)){ + // The live card is now anchored in the turn. Keeping the same running + // state in global transient UI makes later renderMessages() calls insert + // a duplicate Automatic Compression card. + window._compressionUi=null; + snapshotLiveTurn(); + return; + } } if(typeof renderMessages==='function') renderMessages({preserveScroll:true}); + snapshotLiveTurn(); }); source.addEventListener('compressed',e=>{ @@ -1627,13 +1643,22 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ _syncCtxIndicator(S.lastUsage); } if(typeof setCompressionUi==='function'){ - setCompressionUi({ + const state={ sessionId:activeSid, phase:'done', automatic:true, message, summary:{headline:message}, - }); + }; + setCompressionUi(state); + const appended=typeof appendLiveCompressionCard==='function'&&appendLiveCompressionCard(state); + if(appended){ + // The live card is now anchored in the turn. Do not keep the automatic + // completion state as global transient UI, otherwise every subsequent + // render projects the same Auto Compression card again. + window._compressionUi=null; + snapshotLiveTurn(); + } } if(typeof _setCompressionSessionLock==='function') _setCompressionSessionLock(null); if(!S.busy&&typeof renderMessages==='function') renderMessages(); diff --git a/static/sessions.js b/static/sessions.js index 771e1d80..0019b6db 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -550,8 +550,10 @@ async function loadSession(sid){ return true; } - // Phase 2a: If session is streaming, restore from INFLIGHT cache before - // loading full messages (INFLIGHT state is self-contained and sufficient). + // Phase 2a: If session is streaming, restore the persisted transcript first, + // then merge the local INFLIGHT live tail. INFLIGHT is a recovery tail, not a + // complete transcript; treating it as the full source makes long sessions look + // like they lost history after switching away and back. if(!INFLIGHT[sid]&&activeStreamId&&typeof loadInflightState==='function'){ const stored=loadInflightState(sid, activeStreamId); if(stored){ @@ -565,8 +567,15 @@ async function loadSession(sid){ } if(INFLIGHT[sid]){ - // Streaming session: use cached INFLIGHT messages (already has pending assistant output). - S.messages=INFLIGHT[sid].messages; + const inflightMessages=INFLIGHT[sid].messages||[]; + S.messages=[]; + S.toolCalls=[]; + try { + await _ensureMessagesLoaded(sid); + } catch(e) { + S.messages=inflightMessages; + } + S.messages=_mergeInflightTailMessages(S.messages,inflightMessages); S.toolCalls=(INFLIGHT[sid].toolCalls||[]); if(_mergePendingSessionMessage(S.session,S.messages)){ INFLIGHT[sid].messages=S.messages; @@ -576,12 +585,17 @@ async function loadSession(sid){ // replaying persisted live tools so the compact Activity count survives // switching away from and back to an active chat (#1715). S.activeStreamId=activeStreamId; - syncTopbar();renderMessages();appendThinking();loadDir('.'); - clearLiveToolCards(); - if(typeof placeLiveToolCardsHost==='function') placeLiveToolCardsHost(); - for(const tc of (S.toolCalls||[])){ - if(tc&&tc.name) appendLiveToolCard(tc); + syncTopbar();renderMessages(); + const restoredLiveTurn=typeof restoreLiveTurnHtmlForSession==='function'&&restoreLiveTurnHtmlForSession(sid); + if(!restoredLiveTurn){ + appendThinking(); + clearLiveToolCards(); + if(typeof placeLiveToolCardsHost==='function') placeLiveToolCardsHost(); + for(const tc of (S.toolCalls||[])){ + if(tc&&tc.name) appendLiveToolCard(tc); + } } + loadDir('.'); setBusy(true);setComposerStatus(''); startApprovalPolling(sid); if(typeof startClarifyPolling==='function') startClarifyPolling(sid); @@ -1128,6 +1142,40 @@ async function _ensureMessagesLoaded(sid) { } } +function _messageComparableText(m){ + if(!m) return ''; + if(typeof msgContent==='function'){ + try{return String(msgContent(m)||'').trim();} + catch(_){} + } + return String(m.content||'').trim(); +} + +function _sameTranscriptMessage(a,b){ + return !!(a&&b) && + String(a.role||'')===String(b.role||'') && + _messageComparableText(a)===_messageComparableText(b); +} + +function _mergeInflightTailMessages(baseMessages, inflightMessages){ + const base=Array.isArray(baseMessages)?baseMessages:[]; + const inflight=Array.isArray(inflightMessages)?inflightMessages:[]; + let liveIdx=-1; + for(let i=inflight.length-1;i>=0;i--){ + if(inflight[i]&&inflight[i]._live){liveIdx=i;break;} + } + if(liveIdx<0) return base; + let start=liveIdx; + if(liveIdx>0&&inflight[liveIdx-1]&&inflight[liveIdx-1].role==='user') start=liveIdx-1; + const tail=inflight.slice(start).filter(m=>m&&m.role); + const merged=[...base]; + for(const msg of tail){ + const duplicate=merged.slice(-Math.max(5,tail.length+2)).some(existing=>_sameTranscriptMessage(existing,msg)); + if(!duplicate) merged.push(msg); + } + return merged; +} + // Load older messages when the user scrolls to the top of the conversation. // Prepends them to S.messages and re-renders, preserving scroll position. let _loadingOlder = false; diff --git a/static/ui.js b/static/ui.js index c050811c..a7594f02 100644 --- a/static/ui.js +++ b/static/ui.js @@ -3717,6 +3717,64 @@ function clearInflightState(sid){ }catch(_){ } } +function snapshotLiveTurnHtmlForSession(sid){ + if(!sid||!INFLIGHT[sid]) return; + const turn=$('liveAssistantTurn'); + if(!turn) return; + if(turn.dataset&&turn.dataset.sessionId&&turn.dataset.sessionId!==sid) return; + INFLIGHT[sid].liveTurnHtml=turn.outerHTML; +} + +function _liveAssistantSegmentTextLength(seg){ + if(!seg) return 0; + const body=seg.querySelector('.msg-body')||seg; + return String(body.textContent||'').trim().length; +} + +function _mergeRestoredLiveAssistantSegment(restored, existing){ + if(!restored||!existing) return; + const existingLive=existing.querySelector('[data-live-assistant="1"]'); + if(!existingLive) return; + const restoredLive=restored.querySelector('[data-live-assistant="1"]'); + const existingLen=_liveAssistantSegmentTextLength(existingLive); + const restoredLen=_liveAssistantSegmentTextLength(restoredLive); + if(existingLen<=restoredLen) return; + const replacement=existingLive.cloneNode(true); + if(restoredLive){ + restoredLive.replaceWith(replacement); + return; + } + const blocks=_assistantTurnBlocks(restored); + if(!blocks) return; + const anchor=Array.from(blocks.children).filter(el=> + el.matches('.tool-call-group,.tool-card-row,.agent-activity-thinking,.thinking-card-row,[data-live-assistant="1"]') + ).pop(); + if(anchor) anchor.insertAdjacentElement('afterend', replacement); + else blocks.appendChild(replacement); +} + +function restoreLiveTurnHtmlForSession(sid){ + const inflight=INFLIGHT[sid]; + if(!sid||!inflight||!inflight.liveTurnHtml) return false; + const inner=$('msgInner'); + if(!inner) return false; + const template=document.createElement('template'); + template.innerHTML=String(inflight.liveTurnHtml||'').trim(); + const restored=template.content.firstElementChild; + if(!restored) return false; + restored.id='liveAssistantTurn'; + if(S.session) restored.dataset.sessionId=S.session.session_id; + const existing=$('liveAssistantTurn'); + _mergeRestoredLiveAssistantSegment(restored, existing); + if(existing) existing.replaceWith(restored); + else inner.appendChild(restored); + const liveGroup=restored.querySelector('.tool-call-group[data-live-tool-call-group="1"]'); + if(liveGroup&&typeof _startActivityElapsedTimer==='function') _startActivityElapsedTimer(liveGroup); + if(typeof placeLiveToolCardsHost==='function') placeLiveToolCardsHost(); + requestAnimationFrame(()=>postProcessRenderedMessages(restored)); + return true; +} + function markInflight(sid, streamId) { localStorage.setItem(INFLIGHT_KEY, JSON.stringify({sid, streamId, ts: Date.now()})); } @@ -4543,17 +4601,18 @@ function _createAssistantTurn(tsTitle='', tpsText=''){ function _assistantTurnBlocks(turn){ return turn?turn.querySelector('.assistant-turn-blocks'):null; } -function _thinkingCardHtml(text){ +function _thinkingCardHtml(text, open){ const clean=_sanitizeThinkingDisplayText(text); - return `

${li('lightbulb',14)}${t('thinking')}${li('chevron-right',12)}
${esc(clean)}
`; + const openClass=open?' open':''; + return `
${li('lightbulb',14)}${t('thinking')}${li('chevron-right',12)}
${esc(clean)}
`; } function isSimplifiedToolCalling(){ return window._simplifiedToolCalling!==false; } -function _thinkingActivityNode(text){ +function _thinkingActivityNode(text, open){ const row=document.createElement('div'); row.className='agent-activity-thinking'; - row.innerHTML=_thinkingCardHtml(text); + row.innerHTML=_thinkingCardHtml(text, open); return row; } // ── Activity-group user expand intent (#1298) ────────────────────────────── @@ -4737,17 +4796,24 @@ function _compressionCardsHtml(state){ } function _autoCompressionCardsHtml(state){ const fallback='Context auto-compressed to continue the conversation'; - const detail=String(state.message||fallback).trim()||fallback; - const preview=String(state.summary?.headline||detail).trim()||detail; + const running=state&&state.phase==='running'; + const detail=running + ? (String(state.message||'Auto-compressing context...').trim()||'Auto-compressing context...') + : (String(state.message||fallback).trim()||fallback); + const preview=running + ? detail + : (String(state.summary?.headline||detail).trim()||detail); return `
${_compressionStatusCardHtml({ statusLabel: t('auto_compress_label'), previewText: preview, detail, - icon: li('check',13), - open: false, - variantClass: 'tool-card-compress-complete tool-card-compress-auto', + icon: running ? '' : li('check',13), + open: running, + variantClass: running + ? 'tool-card-compress-running tool-card-compress-auto' + : 'tool-card-compress-complete tool-card-compress-auto', })}
`; } @@ -4757,6 +4823,26 @@ function _compressionCardsNode(state){ wrap.innerHTML=`
${_compressionCardsHtml(state)}
`; return wrap; } +function appendLiveCompressionCard(state){ + if(!S.session||!S.activeStreamId||!state) return false; + let turn=$('liveAssistantTurn'); + if(!turn){ + turn=_createAssistantTurn(); + turn.id='liveAssistantTurn'; + if(S.session) turn.dataset.sessionId=S.session.session_id; + $('msgInner').appendChild(turn); + } + const inner=_assistantTurnBlocks(turn); + if(!inner) return false; + const node=_compressionCardsNode(state); + if(!node) return false; + node.setAttribute('data-live-compression-card','1'); + const existing=inner.querySelector('[data-live-compression-card="1"]'); + if(existing) existing.replaceWith(node); + else inner.appendChild(node); + if(typeof scrollIfPinned==='function') scrollIfPinned(); + return true; +} function _isHandoffSummaryToolPayload(value){ if(!value||typeof value!=='object'||Array.isArray(value)) return false; return value._handoff_summary_card === true; @@ -5705,14 +5791,18 @@ function renderMessages(options){ } if(!anchorRow) continue; const anchorParent=anchorRow.parentElement; - const insertAfterNode = anchorInsertAfter.get(anchorRow) || anchorRow; + let insertAfterNode = anchorInsertAfter.get(anchorRow) || anchorRow; + const thinkingText=assistantThinking.get(aIdx); + if(thinkingText){ + const thinkingNode=_thinkingActivityNode(thinkingText, false); + anchorParent.insertBefore(thinkingNode, anchorRow); + } + if(!cards.length) continue; const group=ensureActivityGroup(anchorParent,{collapsed:true,anchor:insertAfterNode,activityKey:`assistant:${aIdx}`}); const sourceMsg=S.messages[aIdx]||{}; if(sourceMsg._turnDuration!==undefined) group.setAttribute('data-turn-duration', String(sourceMsg._turnDuration)); const body=group&&group.querySelector('.tool-call-group-body'); if(!body) continue; - const thinkingText=assistantThinking.get(aIdx); - if(thinkingText) body.appendChild(_thinkingActivityNode(thinkingText)); for(const tc of cards){ body.appendChild(buildToolCard(tc)); } @@ -6857,31 +6947,28 @@ function appendThinking(text=''){ } return; } - if(!String(text||'').trim()){ - scrollIfPinned(); - return; - } - const allChildren=Array.from(blocks.children); - const anchor=allChildren.filter(el=> - el.id!=='toolRunningRow' && - el.matches('[data-live-assistant="1"],.tool-call-group,.tool-card-row,.agent-activity-thinking') - ).pop(); - const group=ensureActivityGroup(blocks,{live:true,collapsed:true,anchor,activityKey:_activityKeyForLiveTurn()}); - const body=group&&group.querySelector('.tool-call-group-body'); - if(!body) return; - let row=body.querySelector('.agent-activity-thinking[data-thinking-active="1"]'); + const thinkingText=String(text||'').trim()||'Thinking…'; + blocks.querySelectorAll('.tool-call-group[data-live-tool-call-group="1"][data-live-activity-current="1"]').forEach(group=>{ + group.removeAttribute('data-live-activity-current'); + }); + let row=blocks.querySelector('.agent-activity-thinking[data-thinking-active="1"]'); if(!row){ - row=document.createElement('div'); - row.className='agent-activity-thinking'; + row=_thinkingActivityNode(thinkingText, false); row.setAttribute('data-thinking-active','1'); - body.insertBefore(row, body.firstChild); + const allChildren=Array.from(blocks.children); + const anchor=allChildren.filter(el=> + el.id!=='toolRunningRow' && + el.matches('[data-live-assistant="1"],.tool-call-group,.tool-card-row,.agent-activity-thinking') + ).pop(); + if(anchor) anchor.insertAdjacentElement('afterend', row); + else blocks.appendChild(row); + }else{ + _renderThinkingInto(row,thinkingText); } - _renderThinkingInto(row,text); - _syncToolCallGroupSummary(group); scrollIfPinned(); if(_scrollPinned){ - const thinkingBody=row&&row.querySelector('.thinking-card-body'); - if(thinkingBody) thinkingBody.scrollTop=thinkingBody.scrollHeight; + const body=row&&row.querySelector('.thinking-card-body'); + if(body) body.scrollTop=body.scrollHeight; } } function updateThinking(text=''){appendThinking(text);} diff --git a/tests/test_auto_compression_card.py b/tests/test_auto_compression_card.py index 21d7d5d0..d092f615 100644 --- a/tests/test_auto_compression_card.py +++ b/tests/test_auto_compression_card.py @@ -67,6 +67,18 @@ def test_auto_compression_completion_transition_is_preserved_after_running_liste assert "phase:'done'" in _compressed_listener_block() +def test_auto_compression_does_not_rerender_over_live_answer_text(): + block = _compressing_listener_block() + src = _read("static/ui.js") + + assert "const liveAnswerStarted=" in block + assert "appendLiveCompressionCard(state)" in block + assert block.index("appendLiveCompressionCard(state)") < block.index("renderMessages({preserveScroll:true})") + assert "window._compressionUi=null;" in block + assert "function appendLiveCompressionCard(state)" in src + assert 'data-live-compression-card' in src + + def test_auto_compression_sse_uses_transient_card_not_fake_message(): """Auto compression must not inject display-only text into S.messages.""" src = _read("static/messages.js") @@ -78,6 +90,9 @@ def test_auto_compression_sse_uses_transient_card_not_fake_message(): assert "phase:'done'" in block assert "automatic:true" in block assert "_setCompressionSessionLock" in block + assert "const appended=typeof appendLiveCompressionCard==='function'&&appendLiveCompressionCard(state);" in block + assert "window._compressionUi=null;" in block + assert block.index("appendLiveCompressionCard(state)") < block.index("window._compressionUi=null;") def test_auto_compression_sse_keeps_inactive_and_malformed_paths_safe(): diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 068afdcf..1de1ef34 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -7,6 +7,7 @@ Each test is tagged with the sprint/commit where the bug was found and fixed. import json import os import pathlib +import re import time import urllib.error import urllib.request @@ -582,10 +583,23 @@ def test_live_stream_tokens_persist_partial_assistant_for_session_switch(cleanup "messages.js must mark the persisted in-flight assistant row so renderMessages can re-anchor it" assert "syncInflightAssistantMessage();" in messages_src, \ "token handler must update INFLIGHT state before checking the active session" + token_match = re.search(r"source\.addEventListener\('token',e=>\{(.*?)\n\s*\}\);", messages_src, re.S) + assert token_match, "token listener not found" + token_fn = token_match.group(1) + assert token_fn.find("assistantText+=d.text") < token_fn.find("if(!S.session||S.session.session_id!==activeSid) return;"), ( + "token events must update the active stream's local state before DOM-only active-session guards" + ) + assert token_fn.find("syncInflightAssistantMessage();") < token_fn.find("if(!S.session||S.session.session_id!==activeSid) return;"), ( + "token events must persist INFLIGHT state even while another session is selected" + ) assert "assistantRow&&!assistantRow.isConnected" in messages_src, \ "live stream must drop stale detached assistant DOM references after session switches" assert "data-live-assistant" in ui_src, \ "renderMessages must preserve a live-assistant DOM anchor when rebuilding the thread" + assert "snapshotLiveTurnHtmlForSession(activeSid)" in messages_src, \ + "live turn DOM snapshots should preserve the interleaved timeline across session switches" + assert "restoreLiveTurnHtmlForSession(sid)" in (REPO_ROOT / "static/sessions.js").read_text(), \ + "loadSession should restore the live turn snapshot before replaying flat tool cards" def test_inflight_session_state_tracks_live_tool_cards_per_session(cleanup_test_sessions): @@ -612,13 +626,30 @@ def test_loadSession_inflight_sets_busy_before_renderMessages(cleanup_test_sessi assert inflight_idx >= 0, "INFLIGHT branch not found in loadSession" inflight_block = src[inflight_idx:inflight_idx+700] busy_pos = inflight_block.find("S.busy=true;") - render_pos = inflight_block.find("renderMessages();appendThinking();") + render_pos = inflight_block.find("renderMessages();") assert busy_pos >= 0, "loadSession INFLIGHT branch must set S.busy=true" assert render_pos >= 0, "loadSession INFLIGHT branch must call renderMessages()" assert busy_pos < render_pos, \ "loadSession must set S.busy=true before renderMessages() to avoid duplicate tool cards" +def test_loadSession_inflight_merges_tail_with_persisted_transcript(cleanup_test_sessions): + src = (REPO_ROOT / "static/sessions.js").read_text() + inflight_idx = src.find("if(INFLIGHT[sid]){") + assert inflight_idx >= 0, "INFLIGHT branch not found in loadSession" + inflight_block = src[inflight_idx:inflight_idx+1200] + + assert "await _ensureMessagesLoaded(sid);" in inflight_block, ( + "returning to an active stream should load the persisted transcript before adding the live tail" + ) + assert "_mergeInflightTailMessages(S.messages,inflightMessages)" in inflight_block, ( + "INFLIGHT messages should be merged as a tail, not replace the full transcript" + ) + assert "function _mergeInflightTailMessages" in src, ( + "sessions.js should centralize INFLIGHT tail merge logic for regression coverage" + ) + + def test_loadSession_inflight_sets_active_stream_before_replaying_live_tool_cards(cleanup_test_sessions): """#1715: returning to an active chat must replay persisted tool cards. @@ -630,7 +661,7 @@ def test_loadSession_inflight_sets_active_stream_before_replaying_live_tool_card src = (REPO_ROOT / "static/sessions.js").read_text() inflight_idx = src.find("if(INFLIGHT[sid]){") assert inflight_idx >= 0, "INFLIGHT branch not found in loadSession" - inflight_block = src[inflight_idx:inflight_idx+1000] + inflight_block = src[inflight_idx:inflight_idx+1600] active_pos = inflight_block.find("S.activeStreamId=activeStreamId;") replay_pos = inflight_block.find("appendLiveToolCard(tc);") attach_pos = inflight_block.find("attachLiveStream(sid, activeStreamId") @@ -769,8 +800,8 @@ def test_ui_js_does_not_hide_anchor_segments_that_contain_thinking(cleanup_test_ compact = src.replace(' ', '').replace('\n', '') assert "assistantThinking.set(rawIdx,thinkingText)" in compact, \ "renderMessages must preserve reasoning text before hiding empty anchor segments" - assert "_thinkingActivityNode(thinkingText)" in src, \ - "thinking-only assistant content should render inside the shared activity dropdown" + assert "_thinkingActivityNode(thinkingText, false)" in src, \ + "thinking-only assistant content should render as a collapsed timeline Thinking card" def test_messages_js_live_assistant_segment_reuses_live_turn_wrapper(cleanup_test_sessions): diff --git a/tests/test_ui_tool_call_cleanup.py b/tests/test_ui_tool_call_cleanup.py index 29fe6457..44204d00 100644 --- a/tests/test_ui_tool_call_cleanup.py +++ b/tests/test_ui_tool_call_cleanup.py @@ -260,35 +260,49 @@ class TestToolCallGroupingStatic: "Thinking echo suppression should remove exact visible assistant snippets from reasoning display." ) - def test_tools_and_thinking_share_one_collapsed_activity_dropdown(self): + def test_compact_activity_keeps_thinking_cards_after_session_switch(self): ui_min = re.sub(r"\s+", "", UI_JS) assert "functionensureActivityGroup(" in ui_min, ( - "Tool calls and thinking should share one agent-activity disclosure helper." + "Tool calls should still use the shared Activity disclosure helper." ) assert "data-agent-activity-group" in UI_JS, ( - "The shared tools/thinking disclosure needs a stable data-agent-activity-group hook." - ) - assert "agent-activity-thinking" in UI_JS, ( - "Thinking content should be nested inside the shared activity dropdown, not rendered separately." + "The Activity disclosure needs a stable data-agent-activity-group hook." ) render_fn = _function_body(UI_JS, "renderMessages") assert "isSimplifiedToolCalling()" in render_fn and "assistantThinking.set(rawIdx, thinkingText)" in render_fn, ( - "Settled thinking should move into the shared activity dropdown only when Compact tool activity is enabled." + "Compact settled transcript rendering should preserve Thinking cards after switching sessions." + ) + assert "_thinkingActivityNode(thinkingText, false)" in render_fn, ( + "Settled Thinking cards should render as collapsed timeline entries before related tools." + ) + assert "anchorParent.insertBefore(thinkingNode, anchorRow)" in render_fn, ( + "Settled Thinking cards should appear before their visible assistant process text." ) assert "seg.insertAdjacentHTML('beforeend', _thinkingCardHtml(thinkingText))" in render_fn, ( "The non-simplified path should preserve standalone settled thinking cards." ) - def test_live_thinking_uses_shared_activity_dropdown_only_when_simplified(self): + def test_live_thinking_is_shown_while_still_splitting_tool_bursts(self): live_thinking_fn = _function_body(UI_JS, "appendThinking") + live_tool_fn = _function_body(UI_JS, "appendLiveToolCard") + helper = _function_body(UI_JS, "ensureActivityGroup") assert "isSimplifiedToolCalling()" in live_thinking_fn, ( "Live thinking should branch on the Compact tool activity toggle." ) - assert "ensureActivityGroup" in live_thinking_fn, ( - "Compact live thinking should be inserted into the shared activity dropdown." + assert 'data-live-activity-current' in live_thinking_fn, ( + "Starting a new live thinking block should close the previous live tool burst." ) - assert "thinkingRow" in live_thinking_fn, ( - "The non-simplified live thinking path should preserve the upstream #thinkingRow card." + assert "body.insertBefore(row, body.firstChild)" not in live_thinking_fn, ( + "Live thinking should not be moved into the top Activity dropdown." + ) + assert "_thinkingActivityNode(thinkingText, false)" in live_thinking_fn, ( + "Compact live thinking should render a collapsed Thinking card in the timeline." + ) + assert '[data-live-activity-current="1"]' in live_thinking_fn, ( + "Starting a new Thinking card should mark the previous live tool burst as no longer current." + ) + assert "body.querySelector" in live_tool_fn and "data-live-tid" in live_tool_fn, ( + "tool_complete must still update its current live Activity burst by tool id." ) From 10db8b3bb6c9221b73e226db337749d3d1cb63b3 Mon Sep 17 00:00:00 2001 From: Frank Song Date: Sat, 16 May 2026 10:58:59 +0800 Subject: [PATCH 04/11] Preserve base Thinking card markup for animation tests --- static/ui.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/ui.js b/static/ui.js index a7594f02..994ad972 100644 --- a/static/ui.js +++ b/static/ui.js @@ -4603,8 +4603,9 @@ function _assistantTurnBlocks(turn){ } function _thinkingCardHtml(text, open){ const clean=_sanitizeThinkingDisplayText(text); - const openClass=open?' open':''; - return `
${li('lightbulb',14)}${t('thinking')}${li('chevron-right',12)}
${esc(clean)}
`; + return open + ? `
${li('lightbulb',14)}${t('thinking')}${li('chevron-right',12)}
${esc(clean)}
` + : `
${li('lightbulb',14)}${t('thinking')}${li('chevron-right',12)}
${esc(clean)}
`; } function isSimplifiedToolCalling(){ return window._simplifiedToolCalling!==false; From cdef03961368da9274ba91652767ecdfac42e63c Mon Sep 17 00:00:00 2001 From: Frank Song Date: Sat, 16 May 2026 11:02:33 +0800 Subject: [PATCH 05/11] Update inflight restore static test windows --- tests/test_regressions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 1de1ef34..624043a6 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -434,7 +434,7 @@ def test_loadSession_inflight_restores_live_tool_cards(cleanup_test_sessions): # INFLIGHT branch must call appendLiveToolCard inflight_idx = src.find("if(INFLIGHT[sid]){") assert inflight_idx >= 0, "INFLIGHT branch not found in loadSession" - inflight_block = src[inflight_idx:inflight_idx+900] + inflight_block = src[inflight_idx:inflight_idx+1600] assert "appendLiveToolCard" in inflight_block, "loadSession INFLIGHT branch must restore live tool cards via appendLiveToolCard" assert "clearLiveToolCards" in inflight_block, "loadSession INFLIGHT branch must clear old live cards before restoring" @@ -624,7 +624,7 @@ def test_loadSession_inflight_sets_busy_before_renderMessages(cleanup_test_sessi src = (REPO_ROOT / "static/sessions.js").read_text() inflight_idx = src.find("if(INFLIGHT[sid]){") assert inflight_idx >= 0, "INFLIGHT branch not found in loadSession" - inflight_block = src[inflight_idx:inflight_idx+700] + inflight_block = src[inflight_idx:inflight_idx+1600] busy_pos = inflight_block.find("S.busy=true;") render_pos = inflight_block.find("renderMessages();") assert busy_pos >= 0, "loadSession INFLIGHT branch must set S.busy=true" From 240fc42ad87b493df7008d479ac4f88eb94fe554 Mon Sep 17 00:00:00 2001 From: Frank Song Date: Sat, 16 May 2026 11:06:09 +0800 Subject: [PATCH 06/11] Align pending-user reattach test with live snapshot restore --- tests/test_issue2341_pending_user_reattach.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_issue2341_pending_user_reattach.py b/tests/test_issue2341_pending_user_reattach.py index 6239a1ac..b0a4faf3 100644 --- a/tests/test_issue2341_pending_user_reattach.py +++ b/tests/test_issue2341_pending_user_reattach.py @@ -25,7 +25,7 @@ def test_load_session_inflight_reattach_merges_pending_user_message_before_rende block = _load_session_inflight_branch() merge_pos = block.find("_mergePendingSessionMessage") - render_pos = block.find("renderMessages();appendThinking();") + render_pos = block.find("renderMessages();") assert merge_pos != -1, ( "loadSession's INFLIGHT reattach branch must merge pending_user_message " @@ -36,6 +36,10 @@ def test_load_session_inflight_reattach_merges_pending_user_message_before_rende "The pending user row must be present before renderMessages() rebuilds " "the active transcript" ) + assert "restoreLiveTurnHtmlForSession(sid)" in block, ( + "Session restore may keep a live DOM snapshot instead of always " + "recreating a fresh Thinking row after renderMessages()" + ) assert "INFLIGHT[sid].messages=S.messages;" in block, ( "After merging the pending user row, the INFLIGHT cache should be updated " "so later session switches keep the same visible turn" From e7e45fe98be058d42bdaf4e00efb36495696826f Mon Sep 17 00:00:00 2001 From: Frank Song Date: Sat, 16 May 2026 11:16:07 +0800 Subject: [PATCH 07/11] Stamp live assistant turns at creation --- static/ui.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/ui.js b/static/ui.js index 994ad972..ad3b0703 100644 --- a/static/ui.js +++ b/static/ui.js @@ -3718,6 +3718,9 @@ function clearInflightState(sid){ } function snapshotLiveTurnHtmlForSession(sid){ + // Keep the DOM snapshot memory-only. Persisted INFLIGHT state intentionally + // stores structured stream state, not outerHTML, so a hard reload still uses + // the safer flat replay path instead of reviving stale nodes/listeners. if(!sid||!INFLIGHT[sid]) return; const turn=$('liveAssistantTurn'); if(!turn) return; @@ -4595,6 +4598,7 @@ function _createAssistantTurn(tsTitle='', tpsText=''){ const row=document.createElement('div'); row.className='msg-row assistant-turn'; row.dataset.role='assistant'; + if(S.session) row.dataset.sessionId=S.session.session_id; row.innerHTML=`${_assistantRoleHtml(tsTitle, tpsText)}
`; return row; } From 0b64e21264de3c3ff0566cf2b643019545b8c962 Mon Sep 17 00:00:00 2001 From: Michael Lam Date: Sat, 16 May 2026 10:26:41 -0700 Subject: [PATCH 08/11] fix: cap live chat stream transports --- CHANGELOG.md | 4 ++++ static/messages.js | 11 +++++++++++ tests/test_inflight_stream_reuse.py | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa25bab..8b519f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- **PR #2393** by @Michaelyklam (refs #2313) — Chat streaming now keeps only the selected conversation's live `/api/chat/stream` EventSource open. Switching into another active session closes background chat SSE transports and relies on existing session-list status plus reattach-on-select behavior, preventing many active sessions from accumulating one long-lived browser connection each. + ## [v0.51.74] — 2026-05-16 — Release AX (stage-367 — 4-PR safe-lane batch — #2362 table-cell spacing + #2363 run-state-consistency RFC + #2365 custom_providers list-format + #2367 settings sidebar i18n) ### Added diff --git a/static/messages.js b/static/messages.js index d21d1144..64f0e7db 100644 --- a/static/messages.js +++ b/static/messages.js @@ -410,6 +410,16 @@ function closeLiveStream(sessionId, streamId){ delete LIVE_STREAMS[sessionId]; } +function closeOtherLiveStreams(activeSid){ + // Keep the live token SSE connection scoped to the conversation pane the user + // is actually viewing. Background sessions still show running/finished state + // through the session list and can reattach when selected, but they should not + // keep one EventSource each and exhaust the browser connection pool (#2313). + for(const sid of Object.keys(LIVE_STREAMS)){ + if(sid!==activeSid) closeLiveStream(sid); + } +} + function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ if(!activeSid||!streamId) return; const reconnecting=!!options.reconnecting; @@ -427,6 +437,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ ){ return; } + closeOtherLiveStreams(activeSid); closeLiveStream(activeSid); let assistantText=''; diff --git a/tests/test_inflight_stream_reuse.py b/tests/test_inflight_stream_reuse.py index 70809cd8..403df9ec 100644 --- a/tests/test_inflight_stream_reuse.py +++ b/tests/test_inflight_stream_reuse.py @@ -45,6 +45,25 @@ def test_attach_live_stream_reuses_existing_same_stream_transport(): assert "return" in body[reuse_pos:close_pos] +def test_attach_live_stream_closes_other_session_streams_before_opening_new_one(): + """Only the selected conversation pane should hold an open chat SSE transport.""" + body = _function_body(MESSAGES_JS, "attachLiveStream") + helper = _function_body(MESSAGES_JS, "closeOtherLiveStreams") + + helper_compact = helper.replace(" ", "") + assert "Object.keys(LIVE_STREAMS)" in helper + assert "if(sid!==activeSid)closeLiveStream(sid)" in helper_compact + + reuse_pos = body.find("const existingLive=LIVE_STREAMS[activeSid]") + close_other_pos = body.find("closeOtherLiveStreams(activeSid)") + close_current_pos = body.find("\n closeLiveStream(activeSid);\n") + assert close_other_pos != -1, "attachLiveStream() should prune background chat EventSources" + assert reuse_pos < close_other_pos < close_current_pos, ( + "same-stream reuse should happen before pruning, and pruning should happen " + "before replacing the active session transport" + ) + + def test_attach_live_stream_updates_uploads_before_same_stream_reuse(): """Reusing transport must not skip per-session uploaded attachment state.""" body = _function_body(MESSAGES_JS, "attachLiveStream") From 727e3c9c8f7ef98376b835ea63368a97ad818938 Mon Sep 17 00:00:00 2001 From: starship-s <45587122+starship-s@users.noreply.github.com> Date: Sat, 16 May 2026 12:48:20 -0600 Subject: [PATCH 09/11] fix(streaming): preserve session agents for credential pools --- api/streaming.py | 164 ++++++++++++++++++++++++++++++++++++++++- tests/test_sprint42.py | 136 ++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+), 2 deletions(-) diff --git a/api/streaming.py b/api/streaming.py index 18a32fc2..c954ae8c 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -2430,6 +2430,145 @@ def _attempt_credential_self_heal( return None +def _agent_cache_api_key_sig(resolved_api_key, credential_pool) -> str: + """Return the cache-signature component for runtime credentials. + + Credential-pool providers can legitimately hand WebUI a different runtime + token on each request (round-robin pools, OAuth refresh, auth self-heal). + The AIAgent object is also where cross-turn memory-provider state lives, so + using the volatile token itself in the cache signature silently defeats the + per-session agent cache and drops warmed Hindsight prefetch results. + """ + if credential_pool is not None: + return 'credential-pool' + import hashlib as _hashlib + return _hashlib.sha256((resolved_api_key or '').encode()).hexdigest()[:16] + + +def _refresh_cached_agent_runtime(agent, agent_kwargs: dict) -> bool: + """Refresh volatile runtime credentials on a reused cached AIAgent. + + The cache key intentionally ignores credential-pool token churn, but the + cached agent's LLM client still needs the latest selected/refreshed runtime + key. Keep long-lived provider/session state (memory prefetch, turn counters, + tool state) while swapping only the runtime credential/client. + """ + if agent is None or not isinstance(agent_kwargs, dict): + return False + + new_pool = agent_kwargs.get('credential_pool') + if new_pool is not None: + try: + agent._credential_pool = new_pool + except Exception: + pass + + new_key = agent_kwargs.get('api_key') or '' + if not new_key: + return True + + new_base = agent_kwargs.get('base_url') or getattr(agent, 'base_url', '') or '' + if getattr(agent, '_fallback_activated', False): + # Avoid mixing a refreshed primary credential into a live fallback + # runtime. Rebuilding is safer than mutating a fallback-active agent + # whose restore/cooldown state has not run yet for this turn. + return False + + if new_key == (getattr(agent, 'api_key', '') or ''): + _refresh_cached_agent_primary_runtime_snapshot(agent) + return True + + try: + if getattr(agent, 'api_mode', None) == 'anthropic_messages': + # Native Anthropic-style clients have their own construction path; + # switch_model() already handles token/client refresh there. + if hasattr(agent, 'switch_model'): + agent.switch_model( + agent_kwargs.get('model') or getattr(agent, 'model', None), + agent_kwargs.get('provider') or getattr(agent, 'provider', None), + api_key=new_key, + base_url=new_base, + api_mode=agent_kwargs.get('api_mode') or getattr(agent, 'api_mode', ''), + ) + return True + return False + + if not hasattr(agent, '_client_kwargs') or not hasattr(agent, '_replace_primary_openai_client'): + # Test/fake-agent fallback: keep metadata accurate even if no real + # OpenAI client exists to rebuild. + agent.api_key = new_key + if new_base: + agent.base_url = new_base + _refresh_cached_agent_primary_runtime_snapshot(agent) + return True + + client_kwargs = dict(getattr(agent, '_client_kwargs', {}) or {}) + client_kwargs['api_key'] = new_key + if new_base: + client_kwargs['base_url'] = new_base + agent._client_kwargs = client_kwargs + agent.api_key = new_key + if new_base: + agent.base_url = new_base + if hasattr(agent, '_apply_client_headers_for_base_url'): + agent._apply_client_headers_for_base_url(agent.base_url) + rebuilt = bool(agent._replace_primary_openai_client(reason='webui_credential_refresh')) + if rebuilt: + _refresh_cached_agent_primary_runtime_snapshot(agent) + return rebuilt + except Exception: + logger.debug('[webui] Failed to refresh cached agent runtime credentials', exc_info=True) + return False + + +def _refresh_cached_agent_primary_runtime_snapshot(agent) -> None: + """Keep AIAgent's primary-runtime snapshot aligned with refreshed creds. + + Long-lived AIAgent instances use `_primary_runtime` to restore the preferred + provider after fallback/transport recovery. If WebUI refreshes a cached + agent's runtime token but leaves that snapshot stale, a later restore can + resurrect the old credential and undo the refresh. + """ + rt = getattr(agent, '_primary_runtime', None) + if not isinstance(rt, dict): + return + + base_url = getattr(agent, 'base_url', rt.get('base_url')) + api_key = getattr(agent, 'api_key', rt.get('api_key', '')) + client_kwargs = dict(getattr(agent, '_client_kwargs', None) or rt.get('client_kwargs', {}) or {}) + + rt['base_url'] = base_url + rt['api_key'] = api_key + rt['client_kwargs'] = client_kwargs + + # The default context compressor usually tracks the primary runtime too; + # keep both the live compressor fields and the fallback-restoration + # snapshot aligned when those attributes exist. + cc = getattr(agent, 'context_compressor', None) + if cc is not None: + if hasattr(cc, 'base_url'): + cc.base_url = base_url + if hasattr(cc, 'api_key'): + cc.api_key = api_key + if 'compressor_base_url' in rt: + rt['compressor_base_url'] = getattr(cc, 'base_url', base_url) + if 'compressor_api_key' in rt: + rt['compressor_api_key'] = getattr(cc, 'api_key', api_key) + else: + if 'compressor_base_url' in rt: + rt['compressor_base_url'] = base_url + if 'compressor_api_key' in rt: + rt['compressor_api_key'] = api_key + + if getattr(agent, 'api_mode', None) == 'anthropic_messages': + if hasattr(agent, '_anthropic_api_key'): + rt['anthropic_api_key'] = getattr(agent, '_anthropic_api_key') + if hasattr(agent, '_anthropic_base_url'): + rt['anthropic_base_url'] = getattr(agent, '_anthropic_base_url') + if hasattr(agent, '_is_anthropic_oauth'): + rt['is_anthropic_oauth'] = getattr(agent, '_is_anthropic_oauth') + + def _run_agent_streaming( session_id, msg_text, @@ -3315,11 +3454,16 @@ def _run_agent_streaming( import hashlib as _hashlib import json as _json from api.config import SESSION_AGENT_CACHE, SESSION_AGENT_CACHE_LOCK + _credential_pool = _rt.get('credential_pool') _sig_blob = _json.dumps([ resolved_model or '', - _hashlib.sha256((resolved_api_key or '').encode()).hexdigest()[:16], + _agent_cache_api_key_sig(resolved_api_key, _credential_pool), resolved_base_url or '', resolved_provider or '', + _rt.get('api_mode') or '', + _rt.get('command') or '', + _rt.get('args') or [], + bool(_credential_pool), _max_iterations_cfg or '', _max_tokens_cfg or '', _fallback_resolved or {}, @@ -3343,6 +3487,23 @@ def _run_agent_streaming( SESSION_AGENT_CACHE.move_to_end(session_id) # LRU: mark as recently used logger.debug('[webui] Reusing cached agent for session %s', session_id) + if agent is not None: + # Refresh volatile runtime credentials selected from provider + # pools without discarding cross-turn agent/provider state. + if not _refresh_cached_agent_runtime(agent, _agent_kwargs): + logger.warning( + '[webui] Cached agent runtime could not be safely refreshed; rebuilding agent for session %s', + session_id, + ) + try: + if getattr(agent, '_session_db', None) is not None: + agent._session_db.close() + except Exception: + pass + with SESSION_AGENT_CACHE_LOCK: + SESSION_AGENT_CACHE.pop(session_id, None) + agent = None + if agent is not None: # Refresh per-turn callbacks — these close over request-scoped # objects (put queue, cancel_event) that are new each request. @@ -3392,7 +3553,6 @@ def _run_agent_streaming( # released until GC finalizes the agent, which on a # long-running server may be never. Close it # explicitly so the WAL handles release immediately. - # (Opus pre-release follow-up to #1421.) try: _evicted_agent = evicted_entry[0] if isinstance(evicted_entry, tuple) else None if _evicted_agent is not None and getattr(_evicted_agent, '_session_db', None) is not None: diff --git a/tests/test_sprint42.py b/tests/test_sprint42.py index 995a6614..951367b4 100644 --- a/tests/test_sprint42.py +++ b/tests/test_sprint42.py @@ -874,3 +874,139 @@ class TestCredentialPoolBackwardCompat(unittest.TestCase): # Agent was constructed successfully self.assertIn("session_id", captured["init_kwargs"]) self.assertEqual(captured["init_kwargs"]["session_id"], "sess-compat-test") + + +class TestAgentCacheCredentialPoolStability(unittest.TestCase): + """Credential-pool token churn must not evict cached WebUI agents.""" + + def test_credential_pool_signature_ignores_volatile_runtime_token(self): + import api.streaming as streaming + + pool = object() + self.assertEqual( + streaming._agent_cache_api_key_sig('token-a', pool), + streaming._agent_cache_api_key_sig('token-b', pool), + ) + self.assertNotEqual( + streaming._agent_cache_api_key_sig('token-a', None), + streaming._agent_cache_api_key_sig('token-b', None), + ) + + def test_cached_agent_runtime_refresh_swaps_key_without_losing_agent_state(self): + import api.streaming as streaming + + class FakeAgent: + def __init__(self): + self.api_key = 'old-token' + self.base_url = 'https://chatgpt.com/backend-api/codex' + self.api_mode = 'codex_responses' + self._client_kwargs = { + 'api_key': 'old-token', + 'base_url': self.base_url, + 'default_headers': {'old': 'header'}, + } + self._credential_pool = 'old-pool' + self.context_compressor = type('Compressor', (), { + 'base_url': self.base_url, + 'api_key': 'old-token', + })() + self._primary_runtime = { + 'base_url': self.base_url, + 'api_key': 'old-token', + 'client_kwargs': dict(self._client_kwargs), + 'compressor_base_url': self.base_url, + 'compressor_api_key': 'old-token', + } + self.header_refreshes = [] + self.replacements = [] + self.prefetch_survives = object() + + def _apply_client_headers_for_base_url(self, base_url): + self.header_refreshes.append((base_url, self._client_kwargs['api_key'])) + self._client_kwargs['default_headers'] = {'refreshed-for': self._client_kwargs['api_key']} + + def _replace_primary_openai_client(self, *, reason): + self.replacements.append(reason) + return True + + agent = FakeAgent() + preserved = agent.prefetch_survives + changed = streaming._refresh_cached_agent_runtime(agent, { + 'api_key': 'new-token', + 'base_url': 'https://chatgpt.com/backend-api/codex', + 'credential_pool': 'new-pool', + }) + + self.assertTrue(changed) + self.assertIs(agent.prefetch_survives, preserved) + self.assertEqual(agent.api_key, 'new-token') + self.assertEqual(agent._client_kwargs['api_key'], 'new-token') + self.assertEqual(agent._credential_pool, 'new-pool') + self.assertEqual(agent._primary_runtime['api_key'], 'new-token') + self.assertEqual(agent._primary_runtime['client_kwargs']['api_key'], 'new-token') + self.assertEqual(agent._primary_runtime['compressor_api_key'], 'new-token') + self.assertEqual(getattr(agent.context_compressor, 'api_key'), 'new-token') + self.assertEqual(agent.header_refreshes, [('https://chatgpt.com/backend-api/codex', 'new-token')]) + self.assertEqual(agent.replacements, ['webui_credential_refresh']) + + def test_same_key_refresh_repairs_stale_primary_runtime_snapshot(self): + import api.streaming as streaming + + class FakeAgent: + api_key = 'current-token' + base_url = 'https://chatgpt.com/backend-api/codex' + api_mode = 'codex_responses' + _client_kwargs = { + 'api_key': 'current-token', + 'base_url': 'https://chatgpt.com/backend-api/codex', + } + _primary_runtime = { + 'api_key': 'old-token', + 'base_url': 'https://chatgpt.com/backend-api/codex', + 'client_kwargs': { + 'api_key': 'old-token', + 'base_url': 'https://chatgpt.com/backend-api/codex', + }, + } + + agent = FakeAgent() + ok = streaming._refresh_cached_agent_runtime(agent, {'api_key': 'current-token'}) + + self.assertTrue(ok) + self.assertEqual(agent._primary_runtime['api_key'], 'current-token') + self.assertEqual(agent._primary_runtime['client_kwargs']['api_key'], 'current-token') + + def test_fallback_active_refresh_requests_rebuild_without_mutating_fallback(self): + import api.streaming as streaming + + class FakeAgent: + api_key = 'fallback-token' + base_url = 'https://fallback.example/v1' + api_mode = 'codex_responses' + _fallback_activated = True + _client_kwargs = { + 'api_key': 'fallback-token', + 'base_url': 'https://fallback.example/v1', + } + _primary_runtime = { + 'api_key': 'old-primary-token', + 'base_url': 'https://chatgpt.com/backend-api/codex', + 'client_kwargs': { + 'api_key': 'old-primary-token', + 'base_url': 'https://chatgpt.com/backend-api/codex', + }, + 'compressor_api_key': 'old-primary-token', + 'compressor_base_url': 'https://chatgpt.com/backend-api/codex', + } + + agent = FakeAgent() + ok = streaming._refresh_cached_agent_runtime(agent, { + 'api_key': 'new-primary-token', + 'base_url': 'https://chatgpt.com/backend-api/codex', + }) + + self.assertFalse(ok) + self.assertEqual(agent.api_key, 'fallback-token') + self.assertEqual(agent._client_kwargs['api_key'], 'fallback-token') + self.assertEqual(agent._primary_runtime['api_key'], 'old-primary-token') + self.assertEqual(agent._primary_runtime['client_kwargs']['api_key'], 'old-primary-token') From 9441e32adb64e2f5751acbbb371c5fe20ae32e4f Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 16 May 2026 20:11:06 +0000 Subject: [PATCH 10/11] test(stage-369): widen brittle setCompressionUi({ assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2347 hoisted the inline state object to a `state` variable so the auto-compression handler could share it with appendLiveCompressionCard. Behavior is identical — same setCompressionUi() dispatch, same calm compression-card path — but tests/test_run_journal_frontend_static.py pinned the literal substring `setCompressionUi({` to verify the call site. Relax the assertion to accept either inline (`{...}`) or hoisted (`state`) argument form. Both forms route through the same compression card path; the over-specific substring was the bug. --- tests/test_run_journal_frontend_static.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_run_journal_frontend_static.py b/tests/test_run_journal_frontend_static.py index 773b6618..5b8ea81a 100644 --- a/tests/test_run_journal_frontend_static.py +++ b/tests/test_run_journal_frontend_static.py @@ -66,7 +66,15 @@ def test_replayed_long_task_events_enter_the_same_live_timeline_handlers(): assert "updateThinking(" in wire_block, "reasoning replay should use the live Thinking card path" assert "appendLiveToolCard(tc)" in wire_block, "tool replay should use live tool-card rendering" - assert "setCompressionUi({" in wire_block, "compression replay should use the compression card path" + # Compression replay must dispatch through setCompressionUi(...). The handler + # body may build the state object inline (`setCompressionUi({...})`) or hoist + # it into a `state` variable first (`setCompressionUi(state)`) — both forms + # use the same compression-card path, so accept either. Pinning the literal + # `{` after the open-paren was over-specific and broke in v0.51.76 when + # PR #2347 hoisted the state object to share it with `appendLiveCompressionCard`. + assert ("setCompressionUi({" in wire_block) or ("setCompressionUi(state)" in wire_block), ( + "compression replay should use the compression card path" + ) assert "_runJournalReplayParams()" in MESSAGES_SRC, "replay attachments should enter _wireSSE via EventSource" From 069503f0bf9c9e40c6fb87db4ab1059749fcca6d Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 16 May 2026 20:11:43 +0000 Subject: [PATCH 11/11] fix(stage-369): replace 'PR TBD' placeholder with #2347 in CHANGELOG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opus advisor caught this on stage-369 review — PR #2347 left a 'PR TBD' placeholder in CHANGELOG that should reference its own number. One-line attribution fix, no behavior change. --- CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25281382..8a89a4a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ ## [Unreleased] +## [v0.51.76] — 2026-05-16 — Release AZ (stage-369 — 4-PR safe-lane batch — live timeline preservation + OpenRouter cost history + chat stream cap + credential pool cache) + +### Added + +- **PR #2195** by @Michaelyklam (refs #692) — OpenRouter cost history backend. New `GET /api/providers/openrouter/cost_history` endpoint backed by daily snapshots from OpenRouter's `/auth/key` cumulative spend. Process-local lock around the snapshot read-modify-write critical section so concurrent dashboard refreshes or multiple tabs cannot overwrite newer reads with stale ones. Delta computation handles cumulative-counter resets (key rotation, OpenRouter-side reset) by starting a fresh series and using the current value as that day's delta rather than emitting negative spend. Backend-only slice; the 7-day daily cost chart UI is a separate follow-up. + +### Fixed + +- **PR #2347** by @franksong2702 (fixes #2344) — Preserve live agent timeline across session switches. Previously, switching away from an active stream and returning rebuilt the turn from the persisted `INFLIGHT` tail, which is enough to reconnect the stream but is not a full-fidelity DOM timeline — Thinking/tool grouping flattened, interim assistant text moved away from its surrounding context, auto-compression cards could project twice. The restore path now snapshots the live assistant turn DOM during the active stream and, on return, loads the persisted transcript first then merges the live snapshot back in so the on-screen scene is preserved as the user left it. Stamping `row.dataset.sessionId` at turn creation prevents the new live-turn sites from re-triggering the lossy rebuild path. + +- **PR #2393** by @Michaelyklam (refs #2313) — Cap live chat stream transports to the selected conversation. Previously, keeping many sessions open accumulated one long-lived `/api/chat/stream` EventSource per session. New `closeOtherLiveStreams(activeSid)` helper in `static/messages.js`; `attachLiveStream()` now reuses an existing same-session transport first, closes other sessions' chat SSE transports, then opens or replaces the selected session's stream. Background sessions still reattach normally when the user selects them — only the SSE transport is pruned, not the server-side stream ownership. New regression test pins the ordering (reuse first, prune background streams next, replace active transport last). + +- **PR #2396** by @starship-s — Preserve session agents for credential pools. The per-session `AIAgent` cache signature previously mixed stable agent identity with the volatile resolved API key, so credential-pool providers (where each request can resolve a different runtime token even when provider/model config is unchanged) missed the cache every turn and rebuilt the agent — losing warmed cross-turn state such as memory-provider prefetch results for providers like Hindsight. New credential-aware cache-signature helper uses a stable sentinel for credential-pool routes while preserving hashed API-key identity for non-pool routes; reused cached agents refresh runtime credentials in place; `AIAgent._primary_runtime` stays aligned after refresh so fallback/transport recovery cannot resurrect an old token; agents still in fallback-active state rebuild rather than mutate to avoid mixed primary/fallback runtime state. Static non-pool API keys still participate in the cache signature so explicit credential changes continue to invalidate. + ## [v0.51.75] — 2026-05-16 — Release AY (stage-368 — 11-PR safe-lane batch — storage + i18n + run-journal parity + attachments + compression sidebar + restart-recovery + text-mode images + tables + settings i18n + German labels) ### Test infrastructure @@ -92,7 +106,7 @@ ### Added -- **PR TBD** by @franksong2702 — Long tool-heavy streaming turns now preserve the live Thinking / assistant progress / Tool / Command timeline when the user switches away and back. The active stream keeps accumulating token and interim-assistant state while inactive, reloads the persisted transcript before merging the live tail, restores the live turn DOM snapshot instead of replaying tools into a flat list, and anchors automatic compression cards inside the active turn to avoid duplicate cards while an answer is still streaming. +- **PR #2347** by @franksong2702 — Long tool-heavy streaming turns now preserve the live Thinking / assistant progress / Tool / Command timeline when the user switches away and back. The active stream keeps accumulating token and interim-assistant state while inactive, reloads the persisted transcript before merging the live tail, restores the live turn DOM snapshot instead of replaying tools into a flat list, and anchors automatic compression cards inside the active turn to avoid duplicate cards while an answer is still streaming. - **PR #2332** by @Michaelyklam (refs #2290) — Cron run history/output cards now surface token/cost metadata when the underlying cron output markdown includes it. The backend parses optional model/token/cost/duration frontmatter from cron output files and returns it from `/api/crons/history` and `/api/crons/run`; the Tasks panel renders a compact usage strip beside run rows and below expanded output without affecting older outputs that lack usage metadata.