@matt82198/aesop 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +50 -260
- package/bin/cli.js +7 -3
- package/docs/HOOK-INSTALL.md +15 -56
- package/docs/INSTALL.md +4 -3
- package/docs/PORTING.md +166 -0
- package/docs/TEAM-STATE.md +16 -184
- package/docs/reproduce.md +33 -3
- package/driver/CLAUDE.md +50 -48
- package/driver/codex_driver.py +59 -12
- package/driver/openai_transport.py +33 -0
- package/driver/wave_bridge.py +9 -1
- package/driver/wave_loop.py +437 -70
- package/driver/wave_scheduler.py +890 -0
- package/hooks/pre-push-policy.sh +22 -5
- package/monitor/collect-signals.mjs +69 -0
- package/package.json +4 -4
- package/state_store/__init__.py +2 -1
- package/state_store/projections.py +63 -0
- package/state_store/read_api.py +156 -0
- package/state_store/write_api.py +462 -0
- package/tools/cost_ceiling.py +106 -15
- package/tools/cost_projection.py +559 -0
- package/tools/crossos_drift.py +394 -0
- package/tools/eod_sweep.py +137 -33
- package/tools/mutation_test.py +130 -8
- package/tools/proposals.mjs +47 -2
- package/tools/reproduce.js +405 -0
- package/tools/secret_scan.py +73 -18
- package/tools/stall_check.py +247 -16
- package/tools/stateapi_lint.py +325 -0
- package/tools/test_battery.py +173 -0
- package/tools/verify_cost_panel.py +345 -0
- package/tools/wave_preflight.py +296 -29
- package/tools/wave_templates.py +170 -45
- package/ui/collectors.py +81 -0
- package/ui/handler.py +230 -85
- package/ui/sse.py +3 -3
- package/ui/wave_telemetry.py +24 -18
- package/ui/web/dist/assets/{index-DqZLgwNg.css → index-CNQxaiOW.css} +1 -1
- package/ui/web/dist/assets/index-CP68RIh3.js +9 -0
- package/ui/web/dist/index.html +2 -2
- package/docs/QUICKSTART.md +0 -80
- package/ui/web/dist/assets/index-BGbgw2Nh.js +0 -9
package/ui/collectors.py
CHANGED
|
@@ -315,6 +315,87 @@ def get_alerts():
|
|
|
315
315
|
print(f"[collectors] Failed to read alerts: {e}", file=sys.stderr)
|
|
316
316
|
return alerts
|
|
317
317
|
|
|
318
|
+
def get_agent_lifecycle_events():
|
|
319
|
+
"""Collect agent lifecycle events from transcript analysis.
|
|
320
|
+
|
|
321
|
+
Scans agent-*.jsonl files to infer agent state transitions.
|
|
322
|
+
Returns structured events for Activity view rendering.
|
|
323
|
+
|
|
324
|
+
Event format (list of dicts):
|
|
325
|
+
[
|
|
326
|
+
{
|
|
327
|
+
"agent_id": "12345",
|
|
328
|
+
"state": "dispatch" | "working" | "done" | "stalled",
|
|
329
|
+
"last_activity": ISO 8601 timestamp (transcript mtime),
|
|
330
|
+
"age_sec": seconds since last activity,
|
|
331
|
+
"transitions": [
|
|
332
|
+
{"state": "dispatch", "at": "2026-07-22T...Z"},
|
|
333
|
+
{"state": "working", "at": "2026-07-22T...Z"},
|
|
334
|
+
]
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
If transcripts unavailable or no agents found, returns empty list.
|
|
339
|
+
"""
|
|
340
|
+
events = []
|
|
341
|
+
try:
|
|
342
|
+
# Use wave_dispatch.py's existing logic for consistency
|
|
343
|
+
import wave_dispatch as wd
|
|
344
|
+
|
|
345
|
+
dispatch_data = wd.get_wave_dispatch(force=False)
|
|
346
|
+
if not dispatch_data.get("available"):
|
|
347
|
+
return events
|
|
348
|
+
|
|
349
|
+
# Convert phase and timing data to lifecycle event structure
|
|
350
|
+
agents_data = dispatch_data.get("agents", [])
|
|
351
|
+
now_ts = datetime.now(timezone.utc)
|
|
352
|
+
|
|
353
|
+
for agent in agents_data:
|
|
354
|
+
agent_id = agent.get("id")
|
|
355
|
+
phase = agent.get("phase")
|
|
356
|
+
age_sec = agent.get("last_activity_age_sec", 0)
|
|
357
|
+
|
|
358
|
+
if not agent_id:
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
# Map phase to state
|
|
362
|
+
phase_to_state = {
|
|
363
|
+
"dispatch": "dispatch",
|
|
364
|
+
"thinking": "working",
|
|
365
|
+
"tool-use": "working",
|
|
366
|
+
"stall": "stalled",
|
|
367
|
+
"done": "done",
|
|
368
|
+
"unknown": "working",
|
|
369
|
+
}
|
|
370
|
+
state = phase_to_state.get(phase, "working")
|
|
371
|
+
|
|
372
|
+
# Estimate activity timestamp from age
|
|
373
|
+
if age_sec >= 0:
|
|
374
|
+
activity_ts = now_ts.timestamp() - age_sec
|
|
375
|
+
activity_dt = datetime.fromtimestamp(activity_ts, timezone.utc)
|
|
376
|
+
last_activity = activity_dt.isoformat(timespec='seconds').replace('+00:00', 'Z')
|
|
377
|
+
else:
|
|
378
|
+
last_activity = now_ts.isoformat(timespec='seconds').replace('+00:00', 'Z')
|
|
379
|
+
|
|
380
|
+
event = {
|
|
381
|
+
"agent_id": agent_id,
|
|
382
|
+
"state": state,
|
|
383
|
+
"last_activity": last_activity,
|
|
384
|
+
"age_sec": age_sec,
|
|
385
|
+
"transitions": [
|
|
386
|
+
{
|
|
387
|
+
"state": state,
|
|
388
|
+
"at": last_activity,
|
|
389
|
+
}
|
|
390
|
+
],
|
|
391
|
+
}
|
|
392
|
+
events.append(event)
|
|
393
|
+
|
|
394
|
+
except Exception as e:
|
|
395
|
+
print(f"[collectors] Failed to collect agent lifecycle events: {e}", file=sys.stderr)
|
|
396
|
+
|
|
397
|
+
return events
|
|
398
|
+
|
|
318
399
|
def load_tracker():
|
|
319
400
|
"""Load tracker.json, return empty tracker if missing or corrupt.
|
|
320
401
|
|
package/ui/handler.py
CHANGED
|
@@ -82,6 +82,32 @@ _ASSET_MIME = {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def _is_client_disconnect_error(exc):
|
|
86
|
+
"""True if exception indicates a normal client disconnect (not an error).
|
|
87
|
+
|
|
88
|
+
When clients close connections mid-stream (tab closed, network drop, etc.),
|
|
89
|
+
we get BrokenPipeError, ConnectionAbortedError, or ConnectionResetError.
|
|
90
|
+
On Windows, rare plain OSError with winerror 10053/10054 may also indicate
|
|
91
|
+
a disconnect. These are normal lifecycle events, not bugs, and should not
|
|
92
|
+
be logged as errors.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
exc: The exception to check
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
bool: True if this is a normal disconnect, False if it's an unexpected error
|
|
99
|
+
"""
|
|
100
|
+
if isinstance(exc, (BrokenPipeError, ConnectionAbortedError,
|
|
101
|
+
ConnectionResetError)):
|
|
102
|
+
return True
|
|
103
|
+
if isinstance(exc, OSError):
|
|
104
|
+
# Windows disconnect errors: 10053 (WSAECONNABORTED), 10054 (WSAECONNRESET)
|
|
105
|
+
winerror = getattr(exc, 'winerror', None)
|
|
106
|
+
if winerror in (10053, 10054):
|
|
107
|
+
return True
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
85
111
|
def _is_local_origin(origin):
|
|
86
112
|
"""True if an Origin header value is on the local allowlist.
|
|
87
113
|
|
|
@@ -335,10 +361,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
335
361
|
self.send_header("Cache-Control", "public, max-age=31536000, immutable")
|
|
336
362
|
self.end_headers()
|
|
337
363
|
self.wfile.write(content)
|
|
338
|
-
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError
|
|
364
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
339
365
|
pass # client went away mid-response — normal, not an error
|
|
366
|
+
except OSError as e:
|
|
367
|
+
if not _is_client_disconnect_error(e):
|
|
368
|
+
print(f"[serve_asset] Uncaught OSError: {e}", file=sys.stderr)
|
|
369
|
+
self.send_error(500)
|
|
370
|
+
# else: silently pass for disconnect errors
|
|
340
371
|
except Exception as e:
|
|
341
|
-
|
|
372
|
+
if not _is_client_disconnect_error(e):
|
|
373
|
+
print(f"[serve_asset] Uncaught exception: {e}", file=sys.stderr)
|
|
342
374
|
self.send_error(500)
|
|
343
375
|
|
|
344
376
|
def serve_api_state(self):
|
|
@@ -376,11 +408,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
376
408
|
self.end_headers()
|
|
377
409
|
self.wfile.write(json.dumps(state, default=str).encode('utf-8'))
|
|
378
410
|
except Exception as e:
|
|
411
|
+
if _is_client_disconnect_error(e):
|
|
412
|
+
return # client gone: nothing to send or log
|
|
379
413
|
print(f"[serve_api_state] Uncaught exception: {e}", file=sys.stderr)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
414
|
+
try:
|
|
415
|
+
self.send_response(500)
|
|
416
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
417
|
+
self.end_headers()
|
|
418
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
419
|
+
except Exception:
|
|
420
|
+
pass # best-effort error response; client may be gone
|
|
384
421
|
|
|
385
422
|
def serve_api_session(self):
|
|
386
423
|
"""GET /api/session — the CSRF token for same-origin JS (plan D3.3).
|
|
@@ -421,11 +458,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
421
458
|
self.end_headers()
|
|
422
459
|
self.wfile.write(json.dumps(summary, default=str).encode('utf-8'))
|
|
423
460
|
except Exception as e:
|
|
461
|
+
if _is_client_disconnect_error(e):
|
|
462
|
+
return # client gone: nothing to send or log
|
|
424
463
|
print(f"[serve_api_cost] Uncaught exception: {e}", file=sys.stderr)
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
464
|
+
try:
|
|
465
|
+
self.send_response(500)
|
|
466
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
467
|
+
self.end_headers()
|
|
468
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
469
|
+
except Exception:
|
|
470
|
+
pass # best-effort error response; client may be gone
|
|
429
471
|
|
|
430
472
|
def serve_api_wave_prs(self):
|
|
431
473
|
"""GET /api/wave/prs — open PRs + PR-less feat/* branches for the PR board.
|
|
@@ -442,11 +484,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
442
484
|
self.end_headers()
|
|
443
485
|
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
444
486
|
except Exception as e:
|
|
487
|
+
if _is_client_disconnect_error(e):
|
|
488
|
+
return # client gone: nothing to send or log
|
|
445
489
|
print(f"[serve_api_wave_prs] Uncaught exception: {e}", file=sys.stderr)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
490
|
+
try:
|
|
491
|
+
self.send_response(500)
|
|
492
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
493
|
+
self.end_headers()
|
|
494
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
495
|
+
except Exception:
|
|
496
|
+
pass # best-effort error response; client may be gone
|
|
450
497
|
|
|
451
498
|
def serve_api_wave_telemetry(self):
|
|
452
499
|
"""GET /api/wave/telemetry — current wave phase, cost metrics, and top blocker.
|
|
@@ -462,11 +509,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
462
509
|
self.end_headers()
|
|
463
510
|
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
464
511
|
except Exception as e:
|
|
512
|
+
if _is_client_disconnect_error(e):
|
|
513
|
+
return # client gone: nothing to send or log
|
|
465
514
|
print(f"[serve_api_wave_telemetry] Uncaught exception: {e}", file=sys.stderr)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
515
|
+
try:
|
|
516
|
+
self.send_response(500)
|
|
517
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
518
|
+
self.end_headers()
|
|
519
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
520
|
+
except Exception:
|
|
521
|
+
pass # best-effort error response; client may be gone
|
|
470
522
|
|
|
471
523
|
def serve_api_wave_dispatch(self):
|
|
472
524
|
"""GET /api/wave/dispatch — live per-agent phase and activity visibility.
|
|
@@ -483,11 +535,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
483
535
|
self.end_headers()
|
|
484
536
|
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
485
537
|
except Exception as e:
|
|
538
|
+
if _is_client_disconnect_error(e):
|
|
539
|
+
return # client gone: nothing to send or log
|
|
486
540
|
print(f"[serve_api_wave_dispatch] Uncaught exception: {e}", file=sys.stderr)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
541
|
+
try:
|
|
542
|
+
self.send_response(500)
|
|
543
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
544
|
+
self.end_headers()
|
|
545
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
546
|
+
except Exception:
|
|
547
|
+
pass # best-effort error response; client may be gone
|
|
491
548
|
|
|
492
549
|
def serve_api_wave_gantt(self):
|
|
493
550
|
"""GET /api/wave/gantt — Gantt timeline data for agents in current wave.
|
|
@@ -504,11 +561,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
504
561
|
self.end_headers()
|
|
505
562
|
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
506
563
|
except Exception as e:
|
|
564
|
+
if _is_client_disconnect_error(e):
|
|
565
|
+
return # client gone: nothing to send or log
|
|
507
566
|
print(f"[serve_api_wave_gantt] Uncaught exception: {e}", file=sys.stderr)
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
567
|
+
try:
|
|
568
|
+
self.send_response(500)
|
|
569
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
570
|
+
self.end_headers()
|
|
571
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
572
|
+
except Exception:
|
|
573
|
+
pass # best-effort error response; client may be gone
|
|
512
574
|
|
|
513
575
|
def serve_api_wave_audit_tail(self):
|
|
514
576
|
"""GET /api/wave/audit-tail — latest audit/verification outcomes.
|
|
@@ -525,11 +587,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
525
587
|
self.end_headers()
|
|
526
588
|
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
527
589
|
except Exception as e:
|
|
590
|
+
if _is_client_disconnect_error(e):
|
|
591
|
+
return # client gone: nothing to send or log
|
|
528
592
|
print(f"[serve_api_wave_audit_tail] Uncaught exception: {e}", file=sys.stderr)
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
593
|
+
try:
|
|
594
|
+
self.send_response(500)
|
|
595
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
596
|
+
self.end_headers()
|
|
597
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
598
|
+
except Exception:
|
|
599
|
+
pass # best-effort error response; client may be gone
|
|
533
600
|
|
|
534
601
|
def serve_api_wave_reasoning_tail(self):
|
|
535
602
|
"""GET /api/wave/reasoning-tail — per-agent live reasoning/transcript summary.
|
|
@@ -546,11 +613,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
546
613
|
self.end_headers()
|
|
547
614
|
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
548
615
|
except Exception as e:
|
|
616
|
+
if _is_client_disconnect_error(e):
|
|
617
|
+
return # client gone: nothing to send or log
|
|
549
618
|
print(f"[serve_api_wave_reasoning_tail] Uncaught exception: {e}", file=sys.stderr)
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
619
|
+
try:
|
|
620
|
+
self.send_response(500)
|
|
621
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
622
|
+
self.end_headers()
|
|
623
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
624
|
+
except Exception:
|
|
625
|
+
pass # best-effort error response; client may be gone
|
|
554
626
|
|
|
555
627
|
def serve_api_wave_quality_scorecards(self):
|
|
556
628
|
"""GET /api/wave/quality-scorecards — per-agent-specialty quality metrics.
|
|
@@ -567,11 +639,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
567
639
|
self.end_headers()
|
|
568
640
|
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
569
641
|
except Exception as e:
|
|
642
|
+
if _is_client_disconnect_error(e):
|
|
643
|
+
return # client gone: nothing to send or log
|
|
570
644
|
print(f"[serve_api_wave_quality_scorecards] Uncaught exception: {e}", file=sys.stderr)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
645
|
+
try:
|
|
646
|
+
self.send_response(500)
|
|
647
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
648
|
+
self.end_headers()
|
|
649
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
650
|
+
except Exception:
|
|
651
|
+
pass # best-effort error response; client may be gone
|
|
575
652
|
|
|
576
653
|
def serve_api_wave_failure(self):
|
|
577
654
|
"""GET /api/wave/failure?pr=N — CI job logs and failure details for a PR.
|
|
@@ -604,28 +681,47 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
604
681
|
self.end_headers()
|
|
605
682
|
self.wfile.write(json.dumps(payload, default=str).encode('utf-8'))
|
|
606
683
|
except Exception as e:
|
|
684
|
+
if _is_client_disconnect_error(e):
|
|
685
|
+
return # client gone: nothing to send or log
|
|
607
686
|
print(f"[serve_api_wave_failure] Uncaught exception: {e}", file=sys.stderr)
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
687
|
+
try:
|
|
688
|
+
self.send_response(500)
|
|
689
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
690
|
+
self.end_headers()
|
|
691
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
692
|
+
except Exception:
|
|
693
|
+
pass # best-effort error response; client may be gone
|
|
612
694
|
|
|
613
695
|
def serve_data(self):
|
|
614
696
|
"""Serve dashboard data as JSON."""
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
697
|
+
try:
|
|
698
|
+
data = {
|
|
699
|
+
"watchdog": get_heartbeat_status(),
|
|
700
|
+
"monitor": get_monitor_heartbeat_status(),
|
|
701
|
+
"agents": get_fleet_agents(),
|
|
702
|
+
"repos": get_repos_status(),
|
|
703
|
+
"events": get_recent_events(),
|
|
704
|
+
"alerts": get_alerts(),
|
|
705
|
+
"messages": get_main_thread_messages(),
|
|
706
|
+
}
|
|
707
|
+
self.send_response(200)
|
|
708
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
709
|
+
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
710
|
+
self.end_headers()
|
|
711
|
+
self.wfile.write(json.dumps(data, default=str).encode('utf-8'))
|
|
712
|
+
except Exception as e:
|
|
713
|
+
if _is_client_disconnect_error(e):
|
|
714
|
+
return # client gone: nothing to send or log
|
|
715
|
+
print(f"[serve_data] Uncaught exception: {e}", file=sys.stderr)
|
|
716
|
+
try:
|
|
717
|
+
# Best-effort 500 response
|
|
718
|
+
self.send_response(500)
|
|
719
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
720
|
+
self.end_headers()
|
|
721
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
722
|
+
except Exception:
|
|
723
|
+
# Sending to a dead client must not cascade
|
|
724
|
+
pass
|
|
629
725
|
|
|
630
726
|
def serve_tracker(self):
|
|
631
727
|
"""Serve tracker items as JSON via GET /api/tracker."""
|
|
@@ -643,11 +739,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
643
739
|
self.end_headers()
|
|
644
740
|
self.wfile.write(json.dumps(body, default=str).encode('utf-8'))
|
|
645
741
|
except Exception as e:
|
|
742
|
+
if _is_client_disconnect_error(e):
|
|
743
|
+
return # client gone: nothing to send or log
|
|
646
744
|
print(f"[serve_tracker] Uncaught exception: {e}", file=sys.stderr)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
745
|
+
try:
|
|
746
|
+
self.send_response(500)
|
|
747
|
+
self.send_header("Content-Type", "application/json")
|
|
748
|
+
self.end_headers()
|
|
749
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
750
|
+
except Exception:
|
|
751
|
+
pass # best-effort error response; client may be gone
|
|
651
752
|
|
|
652
753
|
def handle_tracker_create(self):
|
|
653
754
|
"""Handle POST /api/tracker (create item)."""
|
|
@@ -668,12 +769,19 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
668
769
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
669
770
|
self.end_headers()
|
|
670
771
|
self.wfile.write(json.dumps(result, default=str).encode('utf-8'))
|
|
772
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
773
|
+
pass # Client disconnected — normal lifecycle, not an error.
|
|
671
774
|
except Exception as e:
|
|
775
|
+
if _is_client_disconnect_error(e):
|
|
776
|
+
return # client gone: nothing to send or log
|
|
672
777
|
print(f"[handle_tracker_create] Uncaught exception: {e}", file=sys.stderr)
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
778
|
+
try:
|
|
779
|
+
self.send_response(500)
|
|
780
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
781
|
+
self.end_headers()
|
|
782
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
783
|
+
except Exception:
|
|
784
|
+
pass # best-effort error response; client may be gone
|
|
677
785
|
|
|
678
786
|
def handle_tracker_mutate(self):
|
|
679
787
|
"""Handle POST /api/tracker/<id> (update or delete)."""
|
|
@@ -717,12 +825,19 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
717
825
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
718
826
|
self.end_headers()
|
|
719
827
|
self.wfile.write(json.dumps(result, default=str).encode('utf-8'))
|
|
828
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
829
|
+
pass # Client disconnected — normal lifecycle, not an error.
|
|
720
830
|
except Exception as e:
|
|
831
|
+
if _is_client_disconnect_error(e):
|
|
832
|
+
return # client gone: nothing to send or log
|
|
721
833
|
print(f"[handle_tracker_mutate] Uncaught exception: {e}", file=sys.stderr)
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
834
|
+
try:
|
|
835
|
+
self.send_response(500)
|
|
836
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
837
|
+
self.end_headers()
|
|
838
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
839
|
+
except Exception:
|
|
840
|
+
pass # best-effort error response; client may be gone
|
|
726
841
|
|
|
727
842
|
|
|
728
843
|
def serve_backlog(self):
|
|
@@ -735,11 +850,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
735
850
|
self.end_headers()
|
|
736
851
|
self.wfile.write(json.dumps(data, default=str).encode('utf-8'))
|
|
737
852
|
except Exception as e:
|
|
853
|
+
if _is_client_disconnect_error(e):
|
|
854
|
+
return # client gone: nothing to send or log
|
|
738
855
|
print(f"[serve_backlog] Uncaught exception: {e}", file=sys.stderr)
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
856
|
+
try:
|
|
857
|
+
self.send_response(500)
|
|
858
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
859
|
+
self.end_headers()
|
|
860
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
861
|
+
except Exception:
|
|
862
|
+
pass # best-effort error response; client may be gone
|
|
743
863
|
|
|
744
864
|
def serve_agents(self):
|
|
745
865
|
"""Serve rich agent list with metadata via GET /api/agents."""
|
|
@@ -751,11 +871,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
751
871
|
self.end_headers()
|
|
752
872
|
self.wfile.write(json.dumps(agents, default=str).encode('utf-8'))
|
|
753
873
|
except Exception as e:
|
|
874
|
+
if _is_client_disconnect_error(e):
|
|
875
|
+
return # client gone: nothing to send or log
|
|
754
876
|
print(f"[serve_agents] Uncaught exception: {e}", file=sys.stderr)
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
877
|
+
try:
|
|
878
|
+
self.send_response(500)
|
|
879
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
880
|
+
self.end_headers()
|
|
881
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
882
|
+
except Exception:
|
|
883
|
+
pass # best-effort error response; client may be gone
|
|
759
884
|
|
|
760
885
|
def serve_agent(self):
|
|
761
886
|
"""Serve agent dispatch prompt and metadata via GET /agent?id=<agent_id>"""
|
|
@@ -795,7 +920,8 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
795
920
|
self.send_response(500)
|
|
796
921
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
797
922
|
self.end_headers()
|
|
798
|
-
|
|
923
|
+
if not _is_client_disconnect_error(e):
|
|
924
|
+
print(f"[serve_agent] Uncaught exception: {e}", file=sys.stderr)
|
|
799
925
|
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
800
926
|
|
|
801
927
|
def serve_api_agent(self):
|
|
@@ -838,7 +964,8 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
838
964
|
self.send_response(500)
|
|
839
965
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
840
966
|
self.end_headers()
|
|
841
|
-
|
|
967
|
+
if not _is_client_disconnect_error(e):
|
|
968
|
+
print(f"[serve_api_agent] Uncaught exception: {e}", file=sys.stderr)
|
|
842
969
|
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
843
970
|
|
|
844
971
|
def _write_sse_event(self, event_name, payload):
|
|
@@ -883,8 +1010,12 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
883
1010
|
self.send_header("Retry-After", "30")
|
|
884
1011
|
self.end_headers()
|
|
885
1012
|
self.wfile.write(b"Service overloaded: too many concurrent clients\n")
|
|
886
|
-
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError
|
|
1013
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
887
1014
|
pass
|
|
1015
|
+
except OSError as e:
|
|
1016
|
+
if not _is_client_disconnect_error(e):
|
|
1017
|
+
print(f"[serve_events cap-exceeded] Uncaught OSError: {e}", file=sys.stderr)
|
|
1018
|
+
# else: silently pass for disconnect errors
|
|
888
1019
|
return
|
|
889
1020
|
|
|
890
1021
|
try:
|
|
@@ -894,7 +1025,12 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
894
1025
|
self.send_header("Connection", "keep-alive")
|
|
895
1026
|
self.send_header("X-Accel-Buffering", "no")
|
|
896
1027
|
self.end_headers()
|
|
897
|
-
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError
|
|
1028
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
1029
|
+
unregister_sse_client(q)
|
|
1030
|
+
return
|
|
1031
|
+
except OSError as e:
|
|
1032
|
+
if not _is_client_disconnect_error(e):
|
|
1033
|
+
print(f"[serve_events headers] Uncaught OSError: {e}", file=sys.stderr)
|
|
898
1034
|
unregister_sse_client(q)
|
|
899
1035
|
return
|
|
900
1036
|
try:
|
|
@@ -925,9 +1061,13 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
925
1061
|
except queue.Empty:
|
|
926
1062
|
self.wfile.write(b": keepalive\n\n")
|
|
927
1063
|
self.wfile.flush()
|
|
928
|
-
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError
|
|
1064
|
+
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
|
929
1065
|
# Client disconnected (tab closed, network drop) — normal, not an error.
|
|
930
1066
|
pass
|
|
1067
|
+
except OSError as e:
|
|
1068
|
+
if not _is_client_disconnect_error(e):
|
|
1069
|
+
print(f"[serve_events stream] Uncaught OSError: {e}", file=sys.stderr)
|
|
1070
|
+
# else: silently pass for disconnect errors
|
|
931
1071
|
except Exception:
|
|
932
1072
|
pass
|
|
933
1073
|
finally:
|
|
@@ -982,11 +1122,16 @@ class DashboardHandler(http.server.BaseHTTPRequestHandler):
|
|
|
982
1122
|
self.end_headers()
|
|
983
1123
|
self.wfile.write(json.dumps({"ok": True}).encode('utf-8'))
|
|
984
1124
|
except Exception as e:
|
|
1125
|
+
if _is_client_disconnect_error(e):
|
|
1126
|
+
return # client gone: nothing to send or log
|
|
985
1127
|
print(f"[handle_submit] Uncaught exception: {e}", file=sys.stderr)
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1128
|
+
try:
|
|
1129
|
+
self.send_response(500)
|
|
1130
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
1131
|
+
self.end_headers()
|
|
1132
|
+
self.wfile.write(json.dumps({"error": "Internal server error"}).encode('utf-8'))
|
|
1133
|
+
except Exception:
|
|
1134
|
+
pass # best-effort error response; client may be gone
|
|
990
1135
|
|
|
991
1136
|
|
|
992
1137
|
class QuietThreadingHTTPServer(http.server.ThreadingHTTPServer):
|
package/ui/sse.py
CHANGED
|
@@ -165,14 +165,14 @@ def collector_loop(stop_event):
|
|
|
165
165
|
# Wave-19: Gate data section on mtimes+sizes to avoid expensive file reads every tick.
|
|
166
166
|
# Only regenerate the snapshot if one of the underlying log files changed.
|
|
167
167
|
try:
|
|
168
|
-
backup_log_stat = config.BACKUP_LOG.stat() if config.BACKUP_LOG.exists() else None
|
|
168
|
+
backup_log_stat = config.BACKUP_LOG.stat() if config.BACKUP_LOG and config.BACKUP_LOG.exists() else None
|
|
169
169
|
backup_log_mtime = backup_log_stat.st_mtime if backup_log_stat else None
|
|
170
170
|
backup_log_size = backup_log_stat.st_size if backup_log_stat else None
|
|
171
171
|
except OSError:
|
|
172
172
|
backup_log_mtime = None
|
|
173
173
|
backup_log_size = None
|
|
174
174
|
try:
|
|
175
|
-
alerts_log_stat = config.ALERTS_LOG.stat() if config.ALERTS_LOG.exists() else None
|
|
175
|
+
alerts_log_stat = config.ALERTS_LOG.stat() if config.ALERTS_LOG and config.ALERTS_LOG.exists() else None
|
|
176
176
|
alerts_log_mtime = alerts_log_stat.st_mtime if alerts_log_stat else None
|
|
177
177
|
alerts_log_size = alerts_log_stat.st_size if alerts_log_stat else None
|
|
178
178
|
except OSError:
|
|
@@ -236,7 +236,7 @@ def collector_loop(stop_event):
|
|
|
236
236
|
|
|
237
237
|
# Emit cost section (mtime+size-gated on the outcomes ledger)
|
|
238
238
|
try:
|
|
239
|
-
cost_stat = config.LEDGER_FILE.stat() if config.LEDGER_FILE.exists() else None
|
|
239
|
+
cost_stat = config.LEDGER_FILE.stat() if config.LEDGER_FILE and config.LEDGER_FILE.exists() else None
|
|
240
240
|
cost_mtime = cost_stat.st_mtime if cost_stat else None
|
|
241
241
|
cost_size = cost_stat.st_size if cost_stat else None
|
|
242
242
|
except OSError:
|