From 77b4ccfd0feb4709a47e60660113287bb2f44a27 Mon Sep 17 00:00:00 2001 From: mementorc Date: Thu, 18 Jun 2026 20:15:42 -0500 Subject: [PATCH 1/2] fix(lean): lower offloader default limit below client output cap The offloader default (unknown_token_limit=25000 tokens, ~88KB) sat ABOVE Claude Code's client MAX_MCP_OUTPUT_TOKENS (~60KB for git output), so 60-88KB results slipped past the server offloader and were truncated by the client instead of offloaded to /tmp. Lower the default to 12000 tokens (~42KB) so the offloader spills before the client truncates. Env override (MCP_GIT_UNKNOWN_TOKEN_LIMIT) still raises it. Co-Authored-By: Claude Opus 4.8 --- src/mcp_server_git/config/token_limits.py | 5 +- tests/unit/lean/test_offloader_token_limit.py | 81 +++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/mcp_server_git/config/token_limits.py b/src/mcp_server_git/config/token_limits.py index b310ad63..c8ca8323 100644 --- a/src/mcp_server_git/config/token_limits.py +++ b/src/mcp_server_git/config/token_limits.py @@ -33,7 +33,8 @@ class TokenLimitSettings: # Core token limits llm_token_limit: int = 20000 human_token_limit: int = 0 # 0 = unlimited - unknown_token_limit: int = 25000 + # kept below client MAX_MCP_OUTPUT_TOKENS so the offloader spills before the client truncates + unknown_token_limit: int = 12000 # Feature toggles enable_content_optimization: bool = True @@ -112,7 +113,7 @@ def from_profile(cls, profile: TokenLimitProfile) -> "TokenLimitSettings": ), TokenLimitProfile.BALANCED: cls( llm_token_limit=20000, - unknown_token_limit=25000, + unknown_token_limit=12000, enable_content_optimization=True, enable_intelligent_truncation=True, ), diff --git a/tests/unit/lean/test_offloader_token_limit.py b/tests/unit/lean/test_offloader_token_limit.py index b4b529ab..496857e9 100644 --- a/tests/unit/lean/test_offloader_token_limit.py +++ b/tests/unit/lean/test_offloader_token_limit.py @@ -82,3 +82,84 @@ def test_git_lean_interface_token_limiter_uses_configured_limit( f"Expected default_limit={_CONFIGURED_LIMIT}, " f"got {iface.token_limiter.default_limit} (hard-wired 2000 bug still present)" ) + + +# --------------------------------------------------------------------------- +# Regression: unknown_token_limit default must be below the client output cap +# --------------------------------------------------------------------------- + +# The old default (25 000 tokens ≈ 88 KB at 3.5 ch/tok) sat above the Claude +# Code client MAX_MCP_OUTPUT_TOKENS cap (~60 KB for git-hash-heavy text). +# Outputs in the 60-88 KB range were returned inline and silently truncated by +# the client. The new default (12 000 tokens ≈ 42 KB) ensures the server +# offloader triggers first. +_OLD_OFFLOADER_DEFAULT = 25_000 +_NEW_OFFLOADER_DEFAULT = 12_000 +_CLIENT_CAP_APPROX_TOKENS = 17_000 # ~60 KB / 3.5 ch per token, rounded down +_CHARS_PER_TOKEN_ESTIMATE = 3.5 + + +class TestOffloaderDefaultBelowClientCap: + """Guard the 25000→12000 reduction in unknown_token_limit.""" + + def test_unknown_token_limit_default_is_12000(self) -> None: + """TokenLimitSettings.unknown_token_limit default must be 12000. + + Regression guard: do not restore 25000 — that value sits above the + client MAX_MCP_OUTPUT_TOKENS (~60 KB) and causes silent truncation. + """ + settings = _token_limits_mod.TokenLimitSettings() + assert settings.unknown_token_limit == _NEW_OFFLOADER_DEFAULT, ( + f"unknown_token_limit default regressed: expected {_NEW_OFFLOADER_DEFAULT}, " + f"got {settings.unknown_token_limit}. The old value ({_OLD_OFFLOADER_DEFAULT}) " + f"exceeds the client cap and must not be restored." + ) + + def test_unknown_token_limit_default_is_below_client_cap(self) -> None: + """Default unknown_token_limit must be strictly below the client output cap.""" + settings = _token_limits_mod.TokenLimitSettings() + assert settings.unknown_token_limit < _CLIENT_CAP_APPROX_TOKENS, ( + f"unknown_token_limit ({settings.unknown_token_limit}) must be below the " + f"approximate client cap ({_CLIENT_CAP_APPROX_TOKENS} tokens ≈ 60 KB) so the " + f"server offloader spills before the client truncates." + ) + + def test_env_override_raises_limit_above_default( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """MCP_GIT_UNKNOWN_TOKEN_LIMIT env var must still override the default upward.""" + monkeypatch.setenv("MCP_GIT_UNKNOWN_TOKEN_LIMIT", "30000") + manager = _token_limits_mod.TokenLimitConfigManager() + settings = manager.load_configuration() + assert settings.unknown_token_limit == 30_000 + + def test_limiter_at_new_default_truncates_50kb_output(self) -> None: + """A ~50 KB payload must be truncated by a limiter at the new 12 000-token default. + + 50 KB / 3.5 ch per token ≈ 14 629 tokens — above 12 000, so the server + offloader must trigger before the ~60 KB client cap is reached. + """ + # ~50 KB dict value — representative of a large git log / diff output + fifty_kb_chars = int(50 * 1024) + large_payload = {"output": "a" * fifty_kb_chars} + + limiter = MCPTokenLimiter(default_limit=_NEW_OFFLOADER_DEFAULT) + assert limiter.would_truncate(large_payload, "git_log"), ( + f"A ~50 KB payload (~{fifty_kb_chars // int(_CHARS_PER_TOKEN_ESTIMATE)} tokens) " + f"must exceed the new {_NEW_OFFLOADER_DEFAULT}-token default so the offloader " + f"triggers before the client cap." + ) + + def test_limiter_at_old_default_passes_50kb_output(self) -> None: + """Regression anchor: the old 25 000-token default did NOT truncate ~50 KB output. + + This confirms the old default was the root cause of client-side truncation. + """ + fifty_kb_chars = int(50 * 1024) + large_payload = {"output": "a" * fifty_kb_chars} + + old_limiter = MCPTokenLimiter(default_limit=_OLD_OFFLOADER_DEFAULT) + assert not old_limiter.would_truncate(large_payload, "git_log"), ( + "Sanity-check failed: old 25 000-token limiter should NOT truncate a " + "~50 KB payload, confirming the regression was real." + ) From faf40b9413ab1256aa1911e98fc3dfcac1a0a0d8 Mon Sep 17 00:00:00 2001 From: mementorc Date: Thu, 18 Jun 2026 20:27:46 -0500 Subject: [PATCH 2/2] =?UTF-8?q?fix(lean):=20address=20PR=20#184=20review?= =?UTF-8?q?=20=E2=80=94=20tighten=20CONSERVATIVE=20profile,=20derive=20tes?= =?UTF-8?q?t=20cap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CONSERVATIVE profile unknown_token_limit 18000 -> 8000 (was above the new default and the client cap, undermining the 'conservative' intent). - Derive _CLIENT_CAP_APPROX_TOKENS from _CHARS_PER_TOKEN_ESTIMATE instead of hardcoding, so they can't drift. - Add profile-coverage tests asserting CONSERVATIVE/BALANCED stay below the client cap and BALANCED==12000. Co-Authored-By: Claude Opus 4.8 --- src/mcp_server_git/config/token_limits.py | 3 +- tests/unit/lean/test_offloader_token_limit.py | 46 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/mcp_server_git/config/token_limits.py b/src/mcp_server_git/config/token_limits.py index c8ca8323..e7075e7f 100644 --- a/src/mcp_server_git/config/token_limits.py +++ b/src/mcp_server_git/config/token_limits.py @@ -34,6 +34,7 @@ class TokenLimitSettings: llm_token_limit: int = 20000 human_token_limit: int = 0 # 0 = unlimited # kept below client MAX_MCP_OUTPUT_TOKENS so the offloader spills before the client truncates + # (~42 KB at 3.5 ch/token, under the ~60 KB client cap) unknown_token_limit: int = 12000 # Feature toggles @@ -106,7 +107,7 @@ def from_profile(cls, profile: TokenLimitProfile) -> "TokenLimitSettings": profiles = { TokenLimitProfile.CONSERVATIVE: cls( llm_token_limit=15000, - unknown_token_limit=18000, + unknown_token_limit=8000, enable_content_optimization=True, enable_intelligent_truncation=True, add_truncation_warnings=True, diff --git a/tests/unit/lean/test_offloader_token_limit.py b/tests/unit/lean/test_offloader_token_limit.py index 496857e9..b049b1f2 100644 --- a/tests/unit/lean/test_offloader_token_limit.py +++ b/tests/unit/lean/test_offloader_token_limit.py @@ -9,6 +9,7 @@ import pytest import mcp_server_git.config.token_limits as _token_limits_mod +from mcp_server_git.config.token_limits import TokenLimitProfile from mcp_server_git.lean.token_limiter import MCPTokenLimiter # A payload length that sits between the old 2000-token default and the @@ -95,8 +96,8 @@ def test_git_lean_interface_token_limiter_uses_configured_limit( # offloader triggers first. _OLD_OFFLOADER_DEFAULT = 25_000 _NEW_OFFLOADER_DEFAULT = 12_000 -_CLIENT_CAP_APPROX_TOKENS = 17_000 # ~60 KB / 3.5 ch per token, rounded down _CHARS_PER_TOKEN_ESTIMATE = 3.5 +_CLIENT_CAP_APPROX_TOKENS = int(60 * 1024 / _CHARS_PER_TOKEN_ESTIMATE) class TestOffloaderDefaultBelowClientCap: @@ -163,3 +164,46 @@ def test_limiter_at_old_default_passes_50kb_output(self) -> None: "Sanity-check failed: old 25 000-token limiter should NOT truncate a " "~50 KB payload, confirming the regression was real." ) + + +# --------------------------------------------------------------------------- +# Profile coverage: safe profiles must stay below the client output cap +# --------------------------------------------------------------------------- + +class TestProfilesBelowClientCap: + """Guard that CONSERVATIVE and BALANCED profiles stay below the client cap.""" + + def test_conservative_unknown_token_limit_is_below_default_and_client_cap( + self, + ) -> None: + """CONSERVATIVE profile unknown_token_limit must be below the new default (12000) + and strictly below the client cap — a 'conservative' profile must be the most + restrictive of the safe profiles. + """ + settings = _token_limits_mod.TokenLimitSettings.from_profile( + TokenLimitProfile.CONSERVATIVE + ) + assert settings.unknown_token_limit <= _NEW_OFFLOADER_DEFAULT, ( + f"CONSERVATIVE unknown_token_limit ({settings.unknown_token_limit}) must be " + f"<= the new default ({_NEW_OFFLOADER_DEFAULT})." + ) + assert settings.unknown_token_limit < _CLIENT_CAP_APPROX_TOKENS, ( + f"CONSERVATIVE unknown_token_limit ({settings.unknown_token_limit}) must be " + f"< client cap ({_CLIENT_CAP_APPROX_TOKENS} tokens ≈ 60 KB)." + ) + + def test_balanced_unknown_token_limit_equals_new_default_and_is_below_client_cap( + self, + ) -> None: + """BALANCED profile unknown_token_limit must equal 12000 and be below the client cap.""" + settings = _token_limits_mod.TokenLimitSettings.from_profile( + TokenLimitProfile.BALANCED + ) + assert settings.unknown_token_limit == _NEW_OFFLOADER_DEFAULT, ( + f"BALANCED unknown_token_limit ({settings.unknown_token_limit}) must equal " + f"{_NEW_OFFLOADER_DEFAULT}." + ) + assert settings.unknown_token_limit < _CLIENT_CAP_APPROX_TOKENS, ( + f"BALANCED unknown_token_limit ({settings.unknown_token_limit}) must be " + f"< client cap ({_CLIENT_CAP_APPROX_TOKENS} tokens ≈ 60 KB)." + )