Skip to content

Add --session-ttl flag and fix three session timeout bugs#5117

Open
JAORMX wants to merge 3 commits intomainfrom
feature/configurable-session-ttl
Open

Add --session-ttl flag and fix three session timeout bugs#5117
JAORMX wants to merge 3 commits intomainfrom
feature/configurable-session-ttl

Conversation

@JAORMX
Copy link
Copy Markdown
Collaborator

@JAORMX JAORMX commented Apr 29, 2026

Summary

MCP sessions were being evicted by the background TTL cleanup goroutine even while actively
in use, and there was no way to configure how long sessions should live. This PR fixes both.

Configurable TTL (--session-ttl flag):

  • Added SessionTTL time.Duration to types.Config, RunConfig, and vmcp.ServeConfig
  • Added WithSessionTTL builder option in pkg/runner
  • Added --session-ttl flag to thv run, thv vmcp serve, and vmcp serve
  • Fixed option-ordering trap: all proxy constructors defer session manager creation until
    after all options are applied (previously WithSessionTTL after WithSessionStorage
    would silently drop Redis storage)

Three session timeout bug fixes:

  1. SSE keep-alive ticks now call sessionManager.Get to refresh the TTL while the socket
    is open, preventing eviction of idle-SSE clients
  2. Notifications forwarded via the streamable proxy now refresh the session TTL (the session
    header was not being read in that code path)
  3. Removed unbounded ephemeral session creation for requests without Mcp-Session-Id
    sessionless requests now use sessID="" for in-process waiter-channel correlation
    without creating persistent session objects (memory leak fix)

Type of change

  • Bug fix
  • New feature

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

All tests in the changed packages pass. Pre-existing failures in pkg/mcp/server are
unrelated (missing container runtime in the sandbox environment).

Changes

File Change
pkg/transport/types/transport.go Add SessionTTL to Config
pkg/transport/proxy/httpsse/http_proxy.go Defer manager construction; fix keep-alive TTL refresh
pkg/transport/proxy/streamable/streamable_proxy.go Defer manager construction; fix notification TTL refresh; remove ephemeral sessions
pkg/transport/proxy/transparent/transparent_proxy.go Defer manager construction; wire SessionTTL
pkg/transport/http.go, pkg/transport/stdio.go Add sessionTTL field; wire from factory
pkg/transport/factory.go Pass SessionTTL from config to transports
pkg/runner/config.go Add SessionTTL to RunConfig
pkg/runner/config_builder.go Add WithSessionTTL option
pkg/runner/runner.go Resolve effective TTL; pass to transport config and Redis
cmd/thv/app/run_flags.go, cmd/thv/app/vmcp.go Add --session-ttl flag
cmd/vmcp/app/commands.go Add --session-ttl flag
pkg/vmcp/cli/serve.go Add SessionTTL to ServeConfig; validation
pkg/runner/config_builder_test.go Tests for WithSessionTTL

Does this introduce a user-facing change?

Yes. New optional --session-ttl flag on thv run, thv vmcp serve, and vmcp serve.
Accepts a Go duration string (e.g. --session-ttl=4h). Defaults to the existing hardcoded
values (2h for proxies, 30m for vMCP) so existing behaviour is unchanged when omitted.

Generated with Claude Code

@github-actions github-actions Bot added the size/S Small PR: 100-299 lines changed label Apr 29, 2026
@JAORMX JAORMX force-pushed the feature/configurable-session-ttl branch from cc9c396 to bdca9c6 Compare April 29, 2026 11:59
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/S Small PR: 100-299 lines changed labels Apr 29, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 29, 2026

Codecov Report

❌ Patch coverage is 47.88732% with 37 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.33%. Comparing base (4c23ade) to head (72521d0).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
pkg/transport/proxy/httpsse/http_proxy.go 46.15% 6 Missing and 1 partial ⚠️
...g/transport/proxy/transparent/transparent_proxy.go 50.00% 5 Missing and 1 partial ⚠️
pkg/transport/stdio.go 0.00% 6 Missing ⚠️
pkg/transport/proxy/streamable/streamable_proxy.go 70.58% 5 Missing ⚠️
cmd/vmcp/app/commands.go 0.00% 4 Missing ⚠️
pkg/transport/factory.go 0.00% 3 Missing ⚠️
pkg/vmcp/cli/serve.go 0.00% 2 Missing and 1 partial ⚠️
pkg/transport/http.go 0.00% 1 Missing and 1 partial ⚠️
pkg/runner/runner.go 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5117      +/-   ##
==========================================
+ Coverage   67.12%   67.33%   +0.20%     
==========================================
  Files         597      598       +1     
  Lines       60170    60533     +363     
==========================================
+ Hits        40390    40759     +369     
+ Misses      16706    16690      -16     
- Partials     3074     3084      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

JAORMX and others added 2 commits April 29, 2026 15:17
The session inactivity timeout was previously a hardcoded constant
(2h for transport proxies, 30m for vMCP). This adds a --session-ttl
flag to thv run, thv vmcp serve, and the standalone vmcp serve command,
and threads it through the runner, transport factory, and all three
proxy types (HTTP+SSE, Streamable HTTP, Transparent).

Zero means "use the default"; negative values are rejected at the
CLI/builder layer. The runner resolves the effective TTL once and
passes the non-zero value to both the transport config and Redis
storage, so all consumers use the same session lifetime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sessions were evicted by the TTL cleanup goroutine even while actively in
use because session activity was not being recorded in the right places:

1. SSE keep-alive ticks now call sessionManager.Get to refresh the TTL
   while the SSE socket is open, so the cleanup goroutine does not evict
   clients that have not sent a POST request recently.

2. Single notifications/client-responses forwarded via the streamable
   HTTP proxy now also refresh the session TTL; previously the session
   header was not read from the request in that code path.

3. Removed ephemeral session creation in the streamable proxy for
   requests that arrive without an Mcp-Session-Id header.  The old code
   created a UUID-keyed session per request and never cleaned it up,
   leaking memory over time.  Sessionless requests are now routed with
   an empty sessID that is used only for in-process waiter-channel
   correlation; no persistent session object is created.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@JAORMX JAORMX force-pushed the feature/configurable-session-ttl branch from bdca9c6 to d62c210 Compare April 29, 2026 12:17
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/M Medium PR: 300-599 lines changed labels Apr 29, 2026
Comment thread pkg/transport/proxy/streamable/streamable_proxy.go Outdated
Returning sessID="" for sessionless POSTs collapsed concurrent
sessionless requests with the same JSON-RPC id onto the same
compositeKey("", idKey). The in-process waiters and idRestore
sync.Maps silently overwrote, so one client could receive another
client's response payload (with the JSON-RPC id rewritten to its
own) and the losing client hung until the proxy request timeout.

Generate a per-request UUID in resolveSessionForRequest and
resolveSessionForBatch when no session header is provided. The
token is used solely as an in-process routing key and is not
registered with the session manager, so the original memory-leak
fix is preserved.

Add TestSessionlessConcurrentRequestsAreNotMixed: fires two
concurrent sessionless POSTs with the same JSON-RPC id but
different methods, with a sync barrier in the fake backend to
guarantee both proxy waiters are registered before any response
is dispatched. The backend echoes req.Method back so each client
can prove it received its own payload, not the other client's.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/S Small PR: 100-299 lines changed labels Apr 30, 2026
@JAORMX JAORMX requested a review from jerm-dro May 5, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants