experiment: reduce keepalive to 5s and add progress bar

EXPERIMENT (NOT FOR PRODUCTION YET)
Changes:
- KEEPALIVE_INTERVAL_SECONDS reduced from 20s to 5s
- Keepalive messages now show progress bar with dots: ●●●●○○○○○○
- Show elapsed time, total wait, and remaining seconds
- Example:  Waiting for instructions... ●●●●○○○○○○ 20s / 50s (agent=copilot, 30s remaining)
Goal: Test if more frequent progress updates provide better UX and prevent
      perceived freezing during the 50s wait. No functional change - the
      60s client timeout limit remains the binding constraint.
This commit is contained in:
Brandon Zhang
2026-03-27 15:44:49 +08:00
parent 204f7c4ff6
commit ba91349232

View File

@@ -33,6 +33,14 @@ mcp_asgi_app = mcp.streamable_http_app()
# asyncio.Event wakeup, so there is no practical danger in long waits. # asyncio.Event wakeup, so there is no practical danger in long waits.
_MAX_WAIT_SECONDS = 86400 # 24 hours _MAX_WAIT_SECONDS = 86400 # 24 hours
# Default wait time when no instruction is available (seconds)
# Set to 50s to stay safely under the 60s client timeout while allowing
# multiple keepalive progress updates.
DEFAULT_WAIT_SECONDS = 50
# Default response when queue is empty after waiting
DEFAULT_EMPTY_RESPONSE = "call this tool `get_user_request` again to fetch latest user input..."
# Per-agent generation counter — incremented on every new call. # Per-agent generation counter — incremented on every new call.
# The wait loop only consumes an instruction when it holds the latest generation, # The wait loop only consumes an instruction when it holds the latest generation,
# preventing abandoned (timed-out) coroutines from silently consuming queue items. # preventing abandoned (timed-out) coroutines from silently consuming queue items.
@@ -50,7 +58,8 @@ _agent_generations: dict[str, int] = {}
# start and are unaffected by intermediate SSE events. # start and are unaffected by intermediate SSE events.
# #
# Set to 0 to disable keepalives entirely. # Set to 0 to disable keepalives entirely.
KEEPALIVE_INTERVAL_SECONDS: float = 20.0 # EXPERIMENT: Reduced from 20.0 to 5.0 for more frequent progress updates
KEEPALIVE_INTERVAL_SECONDS: float = 5.0
@mcp.tool() @mcp.tool()
@@ -74,10 +83,8 @@ async def get_user_request(
A dict with keys: status, result_type, instruction, response, A dict with keys: status, result_type, instruction, response,
remaining_pending, waited_seconds. remaining_pending, waited_seconds.
""" """
cfg = config_service.get_config() # Wait time is hardcoded to stay safely under the 60s client timeout
actual_wait = min(DEFAULT_WAIT_SECONDS, _MAX_WAIT_SECONDS)
# Wait time is entirely server-controlled — the user sets it via the web UI.
actual_wait = min(cfg.default_wait_seconds, _MAX_WAIT_SECONDS)
# Register this call as the newest for this agent. # Register this call as the newest for this agent.
my_gen = _agent_generations.get(agent_id, 0) + 1 my_gen = _agent_generations.get(agent_id, 0) + 1
@@ -171,14 +178,19 @@ async def get_user_request(
if KEEPALIVE_INTERVAL_SECONDS > 0 and ctx is not None: if KEEPALIVE_INTERVAL_SECONDS > 0 and ctx is not None:
if now - last_keepalive >= KEEPALIVE_INTERVAL_SECONDS: if now - last_keepalive >= KEEPALIVE_INTERVAL_SECONDS:
waited_so_far = int(now - start) waited_so_far = int(now - start)
remaining_sec = max(0, actual_wait - waited_so_far)
# Progress bar: filled dots proportional to elapsed time
progress_pct = min(100, int((waited_so_far / actual_wait) * 100))
filled = int(progress_pct / 10)
bar = "" * filled + "" * (10 - filled)
try: try:
await ctx.info( await ctx.info(
f"keepalive: waiting for instructions " f"⏳ Waiting for instructions... {bar} "
f"(agent={agent_id}, waited={waited_so_far}s)" f"{waited_so_far}s / {actual_wait}s (agent={agent_id}, {remaining_sec}s remaining)"
) )
logger.debug( logger.debug(
"get_user_request: keepalive sent agent=%s waited=%ds", "get_user_request: keepalive sent agent=%s waited=%ds progress=%d%%",
agent_id, waited_so_far, agent_id, waited_so_far, progress_pct,
) )
except Exception as exc: except Exception as exc:
# Client disconnected — no point continuing # Client disconnected — no point continuing
@@ -213,7 +225,7 @@ async def get_user_request(
empty_response = ( empty_response = (
default_response_override default_response_override
if default_response_override is not None if default_response_override is not None
else cfg.default_empty_response else DEFAULT_EMPTY_RESPONSE
) )
result_type = "default_response" if empty_response else "empty" result_type = "default_response" if empty_response else "empty"