From ba91349232c086ca32dfff10c5c0b169b0afacb8 Mon Sep 17 00:00:00 2001 From: Brandon Zhang Date: Fri, 27 Mar 2026 15:44:49 +0800 Subject: [PATCH] =?UTF-8?q?experiment:=20reduce=20keepalive=20to=205s=20an?= =?UTF-8?q?d=20add=20progress=20bar=20EXPERIMENT=20(NOT=20FOR=20PRODUCTION?= =?UTF-8?q?=20YET)=20Changes:=20-=20KEEPALIVE=5FINTERVAL=5FSECONDS=20reduc?= =?UTF-8?q?ed=20from=2020s=20to=205s=20-=20Keepalive=20messages=20now=20sh?= =?UTF-8?q?ow=20progress=20bar=20with=20dots:=20=E2=97=8F=E2=97=8F?= =?UTF-8?q?=E2=97=8F=E2=97=8F=E2=97=8B=E2=97=8B=E2=97=8B=E2=97=8B=E2=97=8B?= =?UTF-8?q?=E2=97=8B=20-=20Show=20elapsed=20time,=20total=20wait,=20and=20?= =?UTF-8?q?remaining=20seconds=20-=20Example:=20=E2=8F=B3=20Waiting=20for?= =?UTF-8?q?=20instructions...=20=E2=97=8F=E2=97=8F=E2=97=8F=E2=97=8F?= =?UTF-8?q?=E2=97=8B=E2=97=8B=E2=97=8B=E2=97=8B=E2=97=8B=E2=97=8B=2020s=20?= =?UTF-8?q?/=2050s=20(agent=3Dcopilot,=2030s=20remaining)=20Goal:=20Test?= =?UTF-8?q?=20if=20more=20frequent=20progress=20updates=20provide=20better?= =?UTF-8?q?=20UX=20and=20prevent=20=20=20=20=20=20=20perceived=20freezing?= =?UTF-8?q?=20during=20the=2050s=20wait.=20No=20functional=20change=20-=20?= =?UTF-8?q?the=20=20=20=20=20=20=2060s=20client=20timeout=20limit=20remain?= =?UTF-8?q?s=20the=20binding=20constraint.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/mcp_server.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/app/mcp_server.py b/app/mcp_server.py index 42427fe..1589fba 100644 --- a/app/mcp_server.py +++ b/app/mcp_server.py @@ -33,6 +33,14 @@ mcp_asgi_app = mcp.streamable_http_app() # asyncio.Event wakeup, so there is no practical danger in long waits. _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. # The wait loop only consumes an instruction when it holds the latest generation, # 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. # # 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() @@ -74,10 +83,8 @@ async def get_user_request( A dict with keys: status, result_type, instruction, response, remaining_pending, waited_seconds. """ - cfg = config_service.get_config() - - # Wait time is entirely server-controlled — the user sets it via the web UI. - actual_wait = min(cfg.default_wait_seconds, _MAX_WAIT_SECONDS) + # Wait time is hardcoded to stay safely under the 60s client timeout + actual_wait = min(DEFAULT_WAIT_SECONDS, _MAX_WAIT_SECONDS) # Register this call as the newest for this agent. 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 now - last_keepalive >= KEEPALIVE_INTERVAL_SECONDS: 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: await ctx.info( - f"keepalive: waiting for instructions " - f"(agent={agent_id}, waited={waited_so_far}s)" + f"⏳ Waiting for instructions... {bar} " + f"{waited_so_far}s / {actual_wait}s (agent={agent_id}, {remaining_sec}s remaining)" ) logger.debug( - "get_user_request: keepalive sent agent=%s waited=%ds", - agent_id, waited_so_far, + "get_user_request: keepalive sent agent=%s waited=%ds progress=%d%%", + agent_id, waited_so_far, progress_pct, ) except Exception as exc: # Client disconnected — no point continuing @@ -213,7 +225,7 @@ async def get_user_request( empty_response = ( default_response_override 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"