Skip to content

Commit 3d868cf

Browse files
committed
fix: log WebUI shutdown diagnostics
1 parent cf003ae commit 3d868cf

4 files changed

Lines changed: 116 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
## [Unreleased]
55

6+
### Fixed
7+
8+
- WebUI now logs structured shutdown diagnostics when the server exits or `/api/shutdown` is called, including active stream IDs to help diagnose interrupted turns after restarts.
9+
610
## [v0.51.157] — 2026-05-28 — Release EC (stage-batch39 — 5-PR mixed-risk cleanup: gateway prefill forward + prefill budget + compressed-continuation sidebar + browser-transcript memory guidance + reasoning max parity)
711

812
### Added

api/routes.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3854,6 +3854,18 @@ def _serve_shell_unavailable(handler, exc: Exception) -> bool:
38543854

38553855
def _handle_shutdown(handler) -> bool:
38563856
"""Shut down the WebUI server process."""
3857+
headers = getattr(handler, "headers", {})
3858+
ua = headers.get("User-Agent", "no-ua") if hasattr(headers, "get") else "no-ua"
3859+
remote = "unknown"
3860+
if getattr(handler, "client_address", None):
3861+
remote = handler.client_address[0]
3862+
logger.info(
3863+
"[shutdown-request] remote=%s method=%s path=%s ua=%s",
3864+
remote,
3865+
getattr(handler, "command", "unknown"),
3866+
getattr(handler, "path", "unknown"),
3867+
ua,
3868+
)
38573869
j(handler, {"status": "shutting_down"})
38583870
import signal
38593871
import threading

server.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import re
99
import socket
1010
import sys
11+
import threading
1112
import time
1213
import traceback
1314
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@@ -403,6 +404,37 @@ def _raise_fd_soft_limit(target: int = 4096) -> dict:
403404
return {"status": "raised", "soft": desired, "hard": hard, "previous_soft": soft}
404405

405406

407+
_SHUTDOWN_AUDIT_LOGGED = False
408+
409+
410+
def _log_shutdown_audit(reason: str = "serve_forever_exit") -> None:
411+
"""Log runtime context when the WebUI server is exiting."""
412+
global _SHUTDOWN_AUDIT_LOGGED
413+
if _SHUTDOWN_AUDIT_LOGGED:
414+
return
415+
_SHUTDOWN_AUDIT_LOGGED = True
416+
417+
active_sessions = []
418+
try:
419+
from api.models import SESSIONS
420+
for sid, session in SESSIONS.items():
421+
stream_id = getattr(session, "active_stream_id", None)
422+
if stream_id:
423+
pending = bool(getattr(session, "pending_user_message", None))
424+
active_sessions.append(f"sid={sid} stream={stream_id} pending={pending}")
425+
except Exception:
426+
logger.debug("Failed to collect active-session shutdown audit state", exc_info=True)
427+
428+
logger.info(
429+
"[shutdown-audit] reason=%s pid=%s thread=%s(%s) active_sessions=[%s]",
430+
reason,
431+
os.getpid(),
432+
threading.current_thread().name,
433+
threading.current_thread().ident,
434+
"; ".join(active_sessions) if active_sessions else "none",
435+
)
436+
437+
406438
def main() -> None:
407439
from api.config import print_startup_config, verify_hermes_imports, _HERMES_FOUND
408440

@@ -516,6 +548,7 @@ def main() -> None:
516548
try:
517549
httpd.serve_forever()
518550
finally:
551+
_log_shutdown_audit()
519552
# Stop the gateway watcher on shutdown
520553
try:
521554
from api.gateway_watcher import stop_watcher
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import logging
2+
import types
3+
import threading
4+
5+
6+
def test_server_shutdown_audit_logs_active_stream_context(monkeypatch, caplog):
7+
import server
8+
from api import models
9+
10+
monkeypatch.setattr(server, "_SHUTDOWN_AUDIT_LOGGED", False)
11+
monkeypatch.setitem(
12+
models.SESSIONS,
13+
"session-1",
14+
types.SimpleNamespace(active_stream_id="stream-1", pending_user_message="hello"),
15+
)
16+
monkeypatch.setitem(
17+
models.SESSIONS,
18+
"session-2",
19+
types.SimpleNamespace(active_stream_id=None, pending_user_message=None),
20+
)
21+
22+
caplog.set_level(logging.INFO, logger="server")
23+
server._log_shutdown_audit(reason="test-exit")
24+
25+
logged = "\n".join(record.getMessage() for record in caplog.records)
26+
assert "[shutdown-audit]" in logged
27+
assert "reason=test-exit" in logged
28+
assert "sid=session-1 stream=stream-1 pending=True" in logged
29+
assert "session-2" not in logged
30+
31+
32+
def test_shutdown_route_logs_request_context_without_starting_real_shutdown(monkeypatch, caplog):
33+
from api import routes
34+
35+
responses = []
36+
monkeypatch.setattr(routes, "j", lambda handler, payload, **kw: responses.append(payload) or True)
37+
38+
started_threads = []
39+
40+
class FakeThread:
41+
def __init__(self, target, daemon=False):
42+
self.target = target
43+
self.daemon = daemon
44+
45+
def start(self):
46+
started_threads.append((self.target, self.daemon))
47+
48+
monkeypatch.setattr(threading, "Thread", FakeThread)
49+
50+
handler = types.SimpleNamespace(
51+
client_address=("127.0.0.1", 12345),
52+
command="POST",
53+
path="/api/shutdown",
54+
headers={"User-Agent": "pytest-agent"},
55+
)
56+
57+
caplog.set_level(logging.INFO, logger="api.routes")
58+
assert routes._handle_shutdown(handler) is True
59+
60+
logged = "\n".join(record.getMessage() for record in caplog.records)
61+
assert "[shutdown-request]" in logged
62+
assert "remote=127.0.0.1" in logged
63+
assert "method=POST" in logged
64+
assert "path=/api/shutdown" in logged
65+
assert "ua=pytest-agent" in logged
66+
assert responses == [{"status": "shutting_down"}]
67+
assert started_threads and started_threads[0][1] is True

0 commit comments

Comments
 (0)