Skip to content

Commit c20b262

Browse files
authored
fix: prevent compression death spiral from API disconnects (NousResearch#2153) (NousResearch#4750)
Three fixes for long-running gateway sessions that enter a death spiral when API disconnects prevent token data collection, which prevents compression, which causes more disconnects: Layer 1 β€” Stale token counter fallback (run_agent.py in-loop): When last_prompt_tokens is 0 (stale after API disconnect or provider returned no usage data), fall back to estimate_messages_tokens_rough() instead of passing 0 to should_compress(), which would never fire. Layer 2 β€” Server disconnect heuristic (run_agent.py error handler): When ReadError/RemoteProtocolError hits a large session (>60% context or >200 messages), treat it as a context-length error and trigger compression rather than burning through retries that all fail the same way. Layer 3 β€” Hard message count limit (gateway/run.py hygiene): Force compression when a session exceeds 400 messages, regardless of token estimates. This catches runaway growth even when all token-based checks fail due to missing API data. Based on the analysis from PR NousResearch#2157 by ygd58 β€” the gateway threshold direction fix (1.4x multiplier) was already resolved on main.
1 parent 23c8798 commit c20b262

2 files changed

Lines changed: 52 additions & 6 deletions

File tree

β€Žgateway/run.pyβ€Ž

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2361,7 +2361,18 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str):
23612361
# 85% * 1.4 = 119% of context β€” which exceeds the model's limit
23622362
# and prevented hygiene from ever firing for ~200K models (GLM-5).
23632363

2364-
_needs_compress = _approx_tokens >= _compress_token_threshold
2364+
# Hard safety valve: force compression if message count is
2365+
# extreme, regardless of token estimates. This breaks the
2366+
# death spiral where API disconnects prevent token data
2367+
# collection, which prevents compression, which causes more
2368+
# disconnects. 400 messages is well above normal sessions
2369+
# but catches runaway growth before it becomes unrecoverable.
2370+
# (#2153)
2371+
_HARD_MSG_LIMIT = 400
2372+
_needs_compress = (
2373+
_approx_tokens >= _compress_token_threshold
2374+
or _msg_count >= _HARD_MSG_LIMIT
2375+
)
23652376

23662377
if _needs_compress:
23672378
logger.info(

β€Žrun_agent.pyβ€Ž

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7540,7 +7540,33 @@ def _stop_spinner():
75407540
f"treating as probable context overflow.",
75417541
force=True,
75427542
)
7543-
7543+
7544+
# Server disconnects on large sessions are often caused by
7545+
# the request exceeding the provider's context/payload limit
7546+
# without a proper HTTP error response. Treat these as
7547+
# context-length errors to trigger compression rather than
7548+
# burning through retries that will all fail the same way.
7549+
# This breaks the death spiral: disconnect β†’ no token data
7550+
# β†’ no compression β†’ bigger session β†’ more disconnects.
7551+
# (#2153)
7552+
if not is_context_length_error and not status_code:
7553+
_is_server_disconnect = (
7554+
'server disconnected' in error_msg
7555+
or 'peer closed connection' in error_msg
7556+
or error_type in ('ReadError', 'RemoteProtocolError', 'ServerDisconnectedError')
7557+
)
7558+
if _is_server_disconnect:
7559+
ctx_len = getattr(getattr(self, 'context_compressor', None), 'context_length', 200000)
7560+
_is_large = approx_tokens > ctx_len * 0.6 or len(api_messages) > 200
7561+
if _is_large:
7562+
is_context_length_error = True
7563+
self._vprint(
7564+
f"{self.log_prefix}⚠️ Server disconnected with large session "
7565+
f"(~{approx_tokens:,} tokens, {len(api_messages)} msgs) β€” "
7566+
f"treating as context-length error, attempting compression.",
7567+
force=True,
7568+
)
7569+
75447570
if is_context_length_error:
75457571
compressor = self.context_compressor
75467572
old_ctx = compressor.context_length
@@ -8175,11 +8201,20 @@ def _stop_spinner():
81758201
# threshold (default 50%) leaves ample headroom; if tool
81768202
# results push past it, the next API call will report the
81778203
# real total and trigger compression then.
8204+
#
8205+
# If last_prompt_tokens is 0 (stale after API disconnect
8206+
# or provider returned no usage data), fall back to rough
8207+
# estimate to avoid missing compression. Without this,
8208+
# a session can grow unbounded after disconnects because
8209+
# should_compress(0) never fires. (#2153)
81788210
_compressor = self.context_compressor
8179-
_real_tokens = (
8180-
_compressor.last_prompt_tokens
8181-
+ _compressor.last_completion_tokens
8182-
)
8211+
if _compressor.last_prompt_tokens > 0:
8212+
_real_tokens = (
8213+
_compressor.last_prompt_tokens
8214+
+ _compressor.last_completion_tokens
8215+
)
8216+
else:
8217+
_real_tokens = estimate_messages_tokens_rough(messages)
81838218

81848219
# ── Context pressure warnings (user-facing only) ──────────
81858220
# Notify the user (NOT the LLM) as context approaches the

0 commit comments

Comments
Β (0)