contextecho2026 commited on
Commit
2db8d38
·
verified ·
1 Parent(s): 33ce584

Deploy ContextEcho donation relay

Browse files
Files changed (2) hide show
  1. donate/relay_server.py +52 -2
  2. donate/web.py +98 -39
donate/relay_server.py CHANGED
@@ -160,6 +160,51 @@ def _read_jsonl(path: Path, limit: int = 200) -> list[dict]:
160
 
161
  _SUBMISSION_ID_RE = re.compile(r"submission-[A-Za-z0-9_-]{4,64}")
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  def _record_device_donation(device_id: str, submission_id: str, turns: int = 0,
165
  submitted_utc: str = "", source: str = "submission") -> bool:
@@ -168,6 +213,7 @@ def _record_device_donation(device_id: str, submission_id: str, turns: int = 0,
168
  return False
169
  if not _SUBMISSION_ID_RE.fullmatch(str(submission_id or "")):
170
  return False
 
171
  existing = {
172
  (row.get("device_id"), row.get("submission_id"))
173
  for row in _read_jsonl(DEVICE_DONATIONS, limit=100_000)
@@ -189,6 +235,7 @@ def _record_device_donation(device_id: str, submission_id: str, turns: int = 0,
189
 
190
  def _device_donations(device_id: str) -> list[dict]:
191
  """All recorded donations for one device, newest first, deduped by submission."""
 
192
  rows = [
193
  row for row in _read_jsonl(DEVICE_DONATIONS, limit=100_000)
194
  if row.get("device_id") == device_id
@@ -1556,6 +1603,8 @@ def claim_donations(payload: Annotated[dict, Body()]) -> dict:
1556
  source="claim",
1557
  ):
1558
  claimed += 1
 
 
1559
  return {"ok": True, "claimed": claimed}
1560
 
1561
 
@@ -1638,13 +1687,14 @@ async def donate(
1638
  )
1639
  raise
1640
  _record_seen_hash(artifact_hash, submission_id, manifest)
1641
- _record_device_donation(
1642
  str(manifest.get("donor_device_id") or ""),
1643
  submission_id,
1644
  turns=_count_value(manifest.get("turns")),
1645
  submitted_utc=str(manifest.get("submitted_utc") or ""),
1646
  source="submission",
1647
- )
 
1648
  _append_submission_event(
1649
  "submitted",
1650
  submission_id=submission_id,
 
160
 
161
  _SUBMISSION_ID_RE = re.compile(r"submission-[A-Za-z0-9_-]{4,64}")
162
 
163
+ # Free-tier Spaces have no persistent disk: the local state dir is wiped on
164
+ # every rebuild/restart. The device->donations index is therefore mirrored
165
+ # to the private staging dataset (like the seen-hashes backfill) and lazily
166
+ # restored when the local copy is missing.
167
+ DEVICE_DONATIONS_REPO_PATH = "maintainer/device_donations.jsonl"
168
+ _DEVICE_BACKFILL = {"attempted": False}
169
+
170
+
171
+ def _staging_write_token() -> str | None:
172
+ return os.environ.get("HF_STAGING_TOKEN") or os.environ.get("CONTEXTECHO_STAGING_TOKEN")
173
+
174
+
175
+ def _backfill_device_donations() -> None:
176
+ if _DEVICE_BACKFILL["attempted"]:
177
+ return
178
+ _DEVICE_BACKFILL["attempted"] = True
179
+ if DEVICE_DONATIONS.exists() and DEVICE_DONATIONS.stat().st_size > 0:
180
+ return
181
+ token = _staging_write_token()
182
+ if not token:
183
+ return
184
+ try:
185
+ data = _read_hf_file(STAGING_REPO, DEVICE_DONATIONS_REPO_PATH, token)
186
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
187
+ DEVICE_DONATIONS.write_bytes(data)
188
+ except Exception:
189
+ pass
190
+
191
+
192
+ def _persist_device_donations() -> None:
193
+ token = _staging_write_token()
194
+ if not token or not DEVICE_DONATIONS.exists():
195
+ return
196
+ try:
197
+ api = HfApi(token=token)
198
+ api.upload_file(
199
+ path_or_fileobj=str(DEVICE_DONATIONS),
200
+ path_in_repo=DEVICE_DONATIONS_REPO_PATH,
201
+ repo_id=STAGING_REPO,
202
+ repo_type="dataset",
203
+ commit_message="Update device donations index",
204
+ )
205
+ except Exception:
206
+ pass
207
+
208
 
209
  def _record_device_donation(device_id: str, submission_id: str, turns: int = 0,
210
  submitted_utc: str = "", source: str = "submission") -> bool:
 
213
  return False
214
  if not _SUBMISSION_ID_RE.fullmatch(str(submission_id or "")):
215
  return False
216
+ _backfill_device_donations()
217
  existing = {
218
  (row.get("device_id"), row.get("submission_id"))
219
  for row in _read_jsonl(DEVICE_DONATIONS, limit=100_000)
 
235
 
236
  def _device_donations(device_id: str) -> list[dict]:
237
  """All recorded donations for one device, newest first, deduped by submission."""
238
+ _backfill_device_donations()
239
  rows = [
240
  row for row in _read_jsonl(DEVICE_DONATIONS, limit=100_000)
241
  if row.get("device_id") == device_id
 
1603
  source="claim",
1604
  ):
1605
  claimed += 1
1606
+ if claimed:
1607
+ _persist_device_donations()
1608
  return {"ok": True, "claimed": claimed}
1609
 
1610
 
 
1687
  )
1688
  raise
1689
  _record_seen_hash(artifact_hash, submission_id, manifest)
1690
+ if _record_device_donation(
1691
  str(manifest.get("donor_device_id") or ""),
1692
  submission_id,
1693
  turns=_count_value(manifest.get("turns")),
1694
  submitted_utc=str(manifest.get("submitted_utc") or ""),
1695
  source="submission",
1696
+ ):
1697
+ _persist_device_donations()
1698
  _append_submission_event(
1699
  "submitted",
1700
  submission_id=submission_id,
donate/web.py CHANGED
@@ -335,10 +335,36 @@ def relay_url() -> str:
335
  RELAY_STATUS_CHUNK = 200
336
 
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  def relay_donation_status(sessions: list[dict]) -> list[dict]:
339
  url = relay_url()
340
  if not url or not sessions:
341
  return []
 
 
342
  statuses: list[dict] = []
343
  for start in range(0, len(sessions), RELAY_STATUS_CHUNK):
344
  chunk = sessions[start : start + RELAY_STATUS_CHUNK]
@@ -361,7 +387,7 @@ def relay_donation_status(sessions: list[dict]) -> list[dict]:
361
  method="POST",
362
  )
363
  try:
364
- with urlopen(req, timeout=30) as resp:
365
  result = json.loads(resp.read().decode("utf-8"))
366
  except Exception:
367
  return []
@@ -374,10 +400,11 @@ def relay_donation_status(sessions: list[dict]) -> list[dict]:
374
  return statuses
375
 
376
 
377
- def _relay_post_json(path: str, payload: dict, timeout: int = 15) -> dict:
378
  url = relay_url()
379
  if not url:
380
  raise ValueError("Relay URL is not configured.")
 
381
  data = json.dumps(payload).encode("utf-8")
382
  req = Request(
383
  f"{url}{path}",
@@ -442,6 +469,65 @@ def my_donations_summary() -> dict:
442
  }
443
 
444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  def relay_metadata_update(payload: dict) -> dict:
446
  url = relay_url()
447
  if not url:
@@ -1765,7 +1851,6 @@ INDEX_HTML = r"""<!doctype html>
1765
  </div>
1766
  </div>
1767
  <div id="datasetComposition" class="composition-panel" aria-label="Public dataset composition"></div>
1768
- <div id="myDonations" class="composition-panel" aria-label="My donations from this machine" style="display:none"></div>
1769
  <button id="discoverBtn" class="discover-main">Discover Sessions</button>
1770
  <div id="discoverStatus" class="muted" style="margin-top:16px; text-align:center">Scanning starts automatically. Use Discover Sessions to rerun the scan.</div>
1771
  <div id="discoverProgress" class="progress"><div></div></div>
@@ -2470,35 +2555,6 @@ async function loadProjectStats(){
2470
  renderProjectStats();
2471
  }
2472
  }
2473
- async function loadMyDonations(){
2474
- const target = $('myDonations');
2475
- if(!target) return;
2476
- try {
2477
- const r = await fetch('/api/my_donations', {cache:'no-store'});
2478
- if(!r.ok) return;
2479
- const data = await r.json();
2480
- const donations = data.donations || [];
2481
- if(!donations.length){ target.style.display = 'none'; return; }
2482
- const source = data.relay_checked
2483
- ? 'Synced with the donation relay by this machine’s anonymous device hash.'
2484
- : 'From local receipts; relay sync unavailable right now.';
2485
- target.innerHTML = `
2486
- <div class="composition-head">
2487
- <div class="composition-title">My Donations · ${donations.length}</div>
2488
- <div class="composition-subtitle">${escapeHtml(fmtStat(data.total_turns || 0))} turns donated from this machine. ${escapeHtml(source)} Donations stay in the dataset even after local session logs are deleted.</div>
2489
- </div>
2490
- <div class="composition-list" style="max-height:220px; overflow-y:auto">
2491
- ${donations.map(d => `
2492
- <div class="composition-row">
2493
- <div class="composition-label"><code>${escapeHtml(d.submission_id)}</code><small>${escapeHtml((d.submitted_utc || '').slice(0, 10))}</small></div>
2494
- <div class="composition-value">${escapeHtml(fmtStat(d.turns))} turns</div>
2495
- </div>
2496
- `).join('')}
2497
- </div>
2498
- `;
2499
- target.style.display = '';
2500
- } catch(e) { /* panel is best-effort; never block the flow */ }
2501
- }
2502
  function renderRedactResult(data){
2503
  const stats = data.stats || {};
2504
  const autoStats = {};
@@ -3499,6 +3555,7 @@ function renderSessions(){
3499
  const readyCount = (readyCounts.best || 0) + (readyCounts.good || 0);
3500
  const improveCount = sessions.reduce((count, s) => count + (sessionNeedsMoreTurns(s) ? 1 : 0), 0);
3501
  const donatedTotal = sessions.reduce((count, s) => count + (sessionIsBlockedDonation(s) ? 1 : 0), 0);
 
3502
  const agentCounts = agentFamilyCounts();
3503
  const sessionSummaryTitle = `Claude: ${agentCounts.claude}\nCodex: ${agentCounts.codex}\nOther: ${agentCounts.other}`;
3504
  const readySummaryTitle = `Best: ${readyCounts.best || 0}\nExcellent: ${readyCounts.good || 0}`;
@@ -3512,7 +3569,7 @@ function renderSessions(){
3512
  $('sessionCount').setAttribute('aria-label', sessionSummaryTitle);
3513
  $('sessionCount').classList.toggle('active', sessionStatusFilter === 'all');
3514
  $('sessionCount').setAttribute('aria-pressed', sessionStatusFilter === 'all' ? 'true' : 'false');
3515
- $('sessionCount').innerHTML = `<strong>${sessions.length}</strong><span>found</span>`;
3516
  $('fitSummary').innerHTML = sessions.length
3517
  ? `<button type="button" class="fit-chip donated${sessionStatusFilter === 'donated' ? ' active' : ''}" data-session-filter="donated" data-tooltip="${escapeHtml(donatedSummaryTitle)}" aria-label="${escapeHtml(donatedSummaryTitle)}" aria-pressed="${sessionStatusFilter === 'donated' ? 'true' : 'false'}">Donated ${donatedDisplay}</button><button type="button" class="fit-chip ready${sessionStatusFilter === 'ready' ? ' active' : ''}" data-session-filter="ready" data-tooltip="${escapeHtml(readySummaryTitle)}" aria-label="${escapeHtml(readySummaryTitle)}" aria-pressed="${sessionStatusFilter === 'ready' ? 'true' : 'false'}">Ready ${readyCount}</button><button type="button" class="fit-chip improve${sessionStatusFilter === 'improve' ? ' active' : ''}" data-session-filter="improve" data-tooltip="Not ready yet: needs more turns or a context compaction" aria-label="Not ready yet: needs more turns or a context compaction" aria-pressed="${sessionStatusFilter === 'improve' ? 'true' : 'false'}">Keep chatting ${improveCount}</button>`
3518
  : '';
@@ -3688,11 +3745,14 @@ async function discoverSessions(){
3688
  donatedLifetime = (final && final.donated_lifetime) || 0;
3689
  page = 0;
3690
  discoverTiming = `Completed in ${fmtElapsed(Date.now() - progressTimers.discoverProgress.start)}`;
3691
- status('discoverStatus', sessions.length === 0
 
 
 
3692
  ? noSessionsMessage()
3693
  : (!relayStatusChecked()
3694
- ? `Found ${sessions.length} sessions. Donation status could not be checked with the relay, so previously donated sessions may not be marked here. You can still pick a session to donate.`
3695
- : (allSessionsDonated() ? allSessionsDonatedMessage() : `Found ${sessions.length} sessions. Click a row to select.`)));
3696
  renderSessions();
3697
  } catch(e) { status('discoverStatus','ERROR: '+friendlyRequestError(e, 'discovery scan')); }
3698
  finally {
@@ -3996,7 +4056,6 @@ $('submitBtn').onclick = async () => {
3996
  }
3997
  };
3998
  loadProjectStats();
3999
- loadMyDonations();
4000
  discoverSessions();
4001
  </script>
4002
  </body>
@@ -4087,7 +4146,7 @@ class Handler(BaseHTTPRequestHandler):
4087
  max_per_agent = None if raw_max == "all" else int(raw_max)
4088
  sessions = discover_mod.discover(max_per_agent=max_per_agent, progress=False)
4089
  self._json({
4090
- "sessions": annotate_donated(sessions),
4091
  "donated_lifetime": donated_lifetime_count(),
4092
  })
4093
  return
@@ -4109,7 +4168,7 @@ class Handler(BaseHTTPRequestHandler):
4109
  for event in discover_mod.discover_iter(max_per_agent=max_per_agent):
4110
  if event.get("event") == "done":
4111
  event = dict(event)
4112
- event["sessions"] = annotate_donated(list(event.get("sessions") or []))
4113
  event["donated_lifetime"] = donated_lifetime_count()
4114
  self._write_body((json.dumps(event) + "\n").encode(), stream=True)
4115
  except ClientDisconnected:
 
335
  RELAY_STATUS_CHUNK = 200
336
 
337
 
338
+ # The relay runs on a free-tier Space that sleeps when idle and takes
339
+ # 45-60s to wake; a request racing the cold start times out and the wizard
340
+ # silently degrades to local receipts. Wake it once per process first.
341
+ _RELAY_AWAKE = {"ok": False}
342
+
343
+
344
+ def _ensure_relay_awake(timeout_each: int = 45, attempts: int = 3) -> bool:
345
+ if _RELAY_AWAKE["ok"]:
346
+ return True
347
+ url = relay_url()
348
+ if not url:
349
+ return False
350
+ for _ in range(attempts):
351
+ try:
352
+ req = Request(f"{url}/health", headers={"user-agent": "contextecho-donate"})
353
+ with urlopen(req, timeout=timeout_each) as resp:
354
+ if getattr(resp, "status", 200) == 200:
355
+ _RELAY_AWAKE["ok"] = True
356
+ return True
357
+ except Exception:
358
+ time.sleep(2)
359
+ return False
360
+
361
+
362
  def relay_donation_status(sessions: list[dict]) -> list[dict]:
363
  url = relay_url()
364
  if not url or not sessions:
365
  return []
366
+ if not _ensure_relay_awake():
367
+ return []
368
  statuses: list[dict] = []
369
  for start in range(0, len(sessions), RELAY_STATUS_CHUNK):
370
  chunk = sessions[start : start + RELAY_STATUS_CHUNK]
 
387
  method="POST",
388
  )
389
  try:
390
+ with urlopen(req, timeout=90) as resp:
391
  result = json.loads(resp.read().decode("utf-8"))
392
  except Exception:
393
  return []
 
400
  return statuses
401
 
402
 
403
+ def _relay_post_json(path: str, payload: dict, timeout: int = 60) -> dict:
404
  url = relay_url()
405
  if not url:
406
  raise ValueError("Relay URL is not configured.")
407
+ _ensure_relay_awake()
408
  data = json.dumps(payload).encode("utf-8")
409
  req = Request(
410
  f"{url}{path}",
 
469
  }
470
 
471
 
472
+ def merge_archived_donations(sessions: list[dict]) -> list[dict]:
473
+ """Append donated sessions whose local source file no longer exists.
474
+
475
+ The picker lists what is on disk; donations outlive the local logs, so
476
+ donations with no matching discovered row are synthesized from the
477
+ device-linked history and shown (non-actionable) in the same table.
478
+ """
479
+ try:
480
+ donations = my_donations_summary().get("donations") or []
481
+ except Exception:
482
+ return sessions
483
+ if not donations:
484
+ return sessions
485
+ present_ids = {normalize_submission_id(row.get("relay_submission_id")) for row in sessions}
486
+ present_ids.discard("")
487
+ present_path_keys = {source_path_key(row["path"]) for row in sessions if row.get("path")}
488
+ receipt_path_keys: dict[str, str] = {}
489
+ for item in load_donation_registry().get("submissions", []):
490
+ sid = normalize_submission_id(item.get("submission_id") or item.get("submission"))
491
+ if sid:
492
+ receipt_path_keys.setdefault(sid, str(item.get("source_path_key") or ""))
493
+ out = list(sessions)
494
+ for d in donations:
495
+ sid = normalize_submission_id(d.get("submission_id"))
496
+ if not sid or sid in present_ids:
497
+ continue
498
+ # If the receipt maps to a file that is still discoverable, the
499
+ # discovered row already represents this donation (covers the
500
+ # relay-unreachable case where relay_submission_id is empty).
501
+ if receipt_path_keys.get(sid) and receipt_path_keys[sid] in present_path_keys:
502
+ continue
503
+ turns = int(d.get("turns") or 0)
504
+ out.append({
505
+ "agent": "Donated session",
506
+ "project": "archive",
507
+ "session_label": "local log deleted",
508
+ "path": f"archived://{sid}",
509
+ "turns": turns,
510
+ "records": 0,
511
+ "compactions": 0,
512
+ "last_active": str(d.get("submitted_utc") or "")[:10],
513
+ "donated": True,
514
+ "donated_before": True,
515
+ "donated_turns": turns,
516
+ "new_turns": 0,
517
+ "update_ready": False,
518
+ "relay_submission_id": sid if is_support_submission_id(sid) else "",
519
+ "relay_public_session_id": "",
520
+ "relay_received": True,
521
+ "relay_checked": True,
522
+ "local_credit_name": "",
523
+ "local_contributor_email": "",
524
+ "local_institute": "",
525
+ "local_public_anonymous": True,
526
+ "archived_donation": True,
527
+ })
528
+ return out
529
+
530
+
531
  def relay_metadata_update(payload: dict) -> dict:
532
  url = relay_url()
533
  if not url:
 
1851
  </div>
1852
  </div>
1853
  <div id="datasetComposition" class="composition-panel" aria-label="Public dataset composition"></div>
 
1854
  <button id="discoverBtn" class="discover-main">Discover Sessions</button>
1855
  <div id="discoverStatus" class="muted" style="margin-top:16px; text-align:center">Scanning starts automatically. Use Discover Sessions to rerun the scan.</div>
1856
  <div id="discoverProgress" class="progress"><div></div></div>
 
2555
  renderProjectStats();
2556
  }
2557
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2558
  function renderRedactResult(data){
2559
  const stats = data.stats || {};
2560
  const autoStats = {};
 
3555
  const readyCount = (readyCounts.best || 0) + (readyCounts.good || 0);
3556
  const improveCount = sessions.reduce((count, s) => count + (sessionNeedsMoreTurns(s) ? 1 : 0), 0);
3557
  const donatedTotal = sessions.reduce((count, s) => count + (sessionIsBlockedDonation(s) ? 1 : 0), 0);
3558
+ const discoveredCount = sessions.reduce((count, s) => count + (s.archived_donation ? 0 : 1), 0);
3559
  const agentCounts = agentFamilyCounts();
3560
  const sessionSummaryTitle = `Claude: ${agentCounts.claude}\nCodex: ${agentCounts.codex}\nOther: ${agentCounts.other}`;
3561
  const readySummaryTitle = `Best: ${readyCounts.best || 0}\nExcellent: ${readyCounts.good || 0}`;
 
3569
  $('sessionCount').setAttribute('aria-label', sessionSummaryTitle);
3570
  $('sessionCount').classList.toggle('active', sessionStatusFilter === 'all');
3571
  $('sessionCount').setAttribute('aria-pressed', sessionStatusFilter === 'all' ? 'true' : 'false');
3572
+ $('sessionCount').innerHTML = `<strong>${discoveredCount}</strong><span>found</span>`;
3573
  $('fitSummary').innerHTML = sessions.length
3574
  ? `<button type="button" class="fit-chip donated${sessionStatusFilter === 'donated' ? ' active' : ''}" data-session-filter="donated" data-tooltip="${escapeHtml(donatedSummaryTitle)}" aria-label="${escapeHtml(donatedSummaryTitle)}" aria-pressed="${sessionStatusFilter === 'donated' ? 'true' : 'false'}">Donated ${donatedDisplay}</button><button type="button" class="fit-chip ready${sessionStatusFilter === 'ready' ? ' active' : ''}" data-session-filter="ready" data-tooltip="${escapeHtml(readySummaryTitle)}" aria-label="${escapeHtml(readySummaryTitle)}" aria-pressed="${sessionStatusFilter === 'ready' ? 'true' : 'false'}">Ready ${readyCount}</button><button type="button" class="fit-chip improve${sessionStatusFilter === 'improve' ? ' active' : ''}" data-session-filter="improve" data-tooltip="Not ready yet: needs more turns or a context compaction" aria-label="Not ready yet: needs more turns or a context compaction" aria-pressed="${sessionStatusFilter === 'improve' ? 'true' : 'false'}">Keep chatting ${improveCount}</button>`
3575
  : '';
 
3745
  donatedLifetime = (final && final.donated_lifetime) || 0;
3746
  page = 0;
3747
  discoverTiming = `Completed in ${fmtElapsed(Date.now() - progressTimers.discoverProgress.start)}`;
3748
+ const discovered = sessions.filter(s => !s.archived_donation).length;
3749
+ const archived = sessions.length - discovered;
3750
+ const archivedNote = archived ? ` Includes ${archived} donated session${archived === 1 ? '' : 's'} whose local logs were deleted (kept in the dataset).` : '';
3751
+ status('discoverStatus', discovered === 0 && !archived
3752
  ? noSessionsMessage()
3753
  : (!relayStatusChecked()
3754
+ ? `Found ${discovered} sessions. Donation status could not be checked with the relay, so previously donated sessions may not be marked here. You can still pick a session to donate.`
3755
+ : (allSessionsDonated() ? allSessionsDonatedMessage() : `Found ${discovered} sessions.${archivedNote} Click a row to select.`)));
3756
  renderSessions();
3757
  } catch(e) { status('discoverStatus','ERROR: '+friendlyRequestError(e, 'discovery scan')); }
3758
  finally {
 
4056
  }
4057
  };
4058
  loadProjectStats();
 
4059
  discoverSessions();
4060
  </script>
4061
  </body>
 
4146
  max_per_agent = None if raw_max == "all" else int(raw_max)
4147
  sessions = discover_mod.discover(max_per_agent=max_per_agent, progress=False)
4148
  self._json({
4149
+ "sessions": merge_archived_donations(annotate_donated(sessions)),
4150
  "donated_lifetime": donated_lifetime_count(),
4151
  })
4152
  return
 
4168
  for event in discover_mod.discover_iter(max_per_agent=max_per_agent):
4169
  if event.get("event") == "done":
4170
  event = dict(event)
4171
+ event["sessions"] = merge_archived_donations(annotate_donated(list(event.get("sessions") or [])))
4172
  event["donated_lifetime"] = donated_lifetime_count()
4173
  self._write_body((json.dumps(event) + "\n").encode(), stream=True)
4174
  except ClientDisconnected: