fix(session): reclaim sessions whose worker is gone; raise cap to 2048 (v0.2.23) #43

Merged
jlxq0 merged 4 commits from fix/session-reclaim into main 2026-10-07 07:59:23 +00:00
Owner

Incident fix: caldav-mcp and carddav-mcp refused every new initialize today with "session cap reached (256 sessions active)" while /health stayed green; both pods were restarted as a stopgap at 15:27 SGT on 2026-10-07. The same CappedSessionManager is in this repo, so it leaks the same way.

Root cause. rmcp 1.x removes a session from LocalSessionManager only in close_session. The streamable-HTTP service calls create_session for every POST without a session id and only then checks that the body is initialize (and the protocol header); on those error paths the transport is dropped and close_session never runs, so the handle stays in the map forever. The cap counted those dead handles.

Fix. CappedSessionManager keeps each session's worker-transport cancellation token, which fires exactly when the transport is dropped. Sessions in that state are closed before every cap check (under the existing create gate) and by a 60 s background sweeper. Sessions whose transport is still held, including idle ones on a standalone SSE stream, are never reclaimed; rmcp's 30-minute keep-alive still ends them. This is the signal m365-mcp has shipped since v0.4.1 (#60). MAX_SESSIONS 256 → 2048.

Tests. Session-less non-initialize POSTs no longer burn slots (end to end through StreamableHttpService); dead sessions are reclaimed and free capacity; live and idle initialized sessions are never reclaimed and the cap still holds; the sweeper reclaims without a new initialize and stops with the manager. Mutation: removing the reclaim from create_session reddens two of them.

Gates run locally: cargo fmt --all --check, cargo clippy --all-targets --all-features --locked -- -D warnings, cargo test --all-features --locked.

🤖 Generated with Claude Code

Incident fix: caldav-mcp and carddav-mcp refused every new `initialize` today with "session cap reached (256 sessions active)" while `/health` stayed green; both pods were restarted as a stopgap at 15:27 SGT on 2026-10-07. The same `CappedSessionManager` is in this repo, so it leaks the same way. **Root cause.** rmcp 1.x removes a session from `LocalSessionManager` only in `close_session`. The streamable-HTTP service calls `create_session` for every POST without a session id and only then checks that the body is `initialize` (and the protocol header); on those error paths the transport is dropped and `close_session` never runs, so the handle stays in the map forever. The cap counted those dead handles. **Fix.** `CappedSessionManager` keeps each session's worker-transport cancellation token, which fires exactly when the transport is dropped. Sessions in that state are closed before every cap check (under the existing create gate) and by a 60 s background sweeper. Sessions whose transport is still held, including idle ones on a standalone SSE stream, are never reclaimed; rmcp's 30-minute keep-alive still ends them. This is the signal m365-mcp has shipped since v0.4.1 (#60). `MAX_SESSIONS` 256 → 2048. **Tests.** Session-less non-`initialize` POSTs no longer burn slots (end to end through `StreamableHttpService`); dead sessions are reclaimed and free capacity; live and idle initialized sessions are never reclaimed and the cap still holds; the sweeper reclaims without a new `initialize` and stops with the manager. Mutation: removing the reclaim from `create_session` reddens two of them. Gates run locally: `cargo fmt --all --check`, `cargo clippy --all-targets --all-features --locked -- -D warnings`, `cargo test --all-features --locked`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
rmcp 1.x removes a session handle from LocalSessionManager only in
close_session. Its streamable-HTTP service calls create_session for every
POST without a session id and only then checks that the body is
initialize (and that the protocol header matches); on those error paths
the transport is dropped and close_session is never called, so the handle
stays in the map for the life of the process. The cap counted those dead
handles, and after weeks of uptime it read 256 with nothing live behind
it: every new initialize was refused with "session cap reached" while
/health stayed green.

CappedSessionManager now keeps each session's worker-transport
cancellation token. The token fires exactly when the transport is
dropped, i.e. when nobody can serve the session any more. Such sessions
are closed before every cap check (under the existing create gate) and
by a background sweeper every 60 s, so the count stays accurate between
initializes. A session whose transport is still held, including one
idling on a standalone SSE stream, is never reclaimed here: rmcp's own
30-minute keep-alive ends it, and only then does its token fire. This is
the same signal m365-mcp has shipped since v0.4.1 (#60).

MAX_SESSIONS goes from 256 to 2048.

Tests: session-less non-initialize POSTs no longer burn slots (the
production failure, end to end through StreamableHttpService); dead
sessions are reclaimed and free capacity; live and idle initialized
sessions are never reclaimed and the cap still holds against them; the
sweeper reclaims without a new initialize and stops with the manager.
Removing the reclaim from create_session reddens two of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chore(release): 0.2.23
Some checks failed
CI / tag-ancestry (pull_request) Successful in 4s
CI / cargo (pull_request) Failing after 4m48s
CI / docker (pull_request) Has been skipped
440ccaab1c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(deps): rmcp 2.2 for GHSA-9pj6-vhgr-3mwh and three OAuth-client advisories
Some checks failed
CI / tag-ancestry (pull_request) Successful in 3s
CI / docker (pull_request) Has been cancelled
CI / cargo (pull_request) Has been cancelled
f3062a137b
The CI OSV gate flags rmcp 1.8.0 with four advisories, all fixed only in
2.x: GHSA-9pj6-vhgr-3mwh (permanent streamable-HTTP session-table leak,
the same leak the session commit reclaims), GHSA-33f5-2c5q-wgwj,
GHSA-9g45-5xwm-f3wc and GHSA-c9xm-49cp-xcr9 (OAuth client and redirect
handling). rmcp 2.0 also ships upstream #934, prevent streamable HTTP
session leak. The crate compiles unchanged against 2.2.0 and the full
suite passes; the reclamation wrapper stays as defence in depth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbPgHm7ND8CnztzqxLEn5T
docs(changelog): rmcp 2.2 security bump
All checks were successful
CI / tag-ancestry (pull_request) Successful in 3s
CI / cargo (pull_request) Successful in 4m21s
CI / docker (pull_request) Successful in 1m45s
0877ee4fa4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbPgHm7ND8CnztzqxLEn5T
jlxq0 merged commit 256e134e53 into main 2026-10-07 07:59:23 +00:00
jlxq0 deleted branch fix/session-reclaim 2026-10-07 07:59:23 +00:00
Sign in to join this conversation.
No reviewers
No labels
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jlxq0/jmap-mcp!43
No description provided.