Skip to content

Commit 9153581

Browse files
zhuangzhuangzhoucodeclaude
authored
fix: implement sliding window expiration for MCP sessions (#39)
* fix: implement sliding window expiration for MCP sessions - Replace fixed 30-minute timeout with activity-based expiration - Add lastActivity tracking to session metadata - Update activity timestamp on each request via touchSession() - Implement global setInterval cleanup (every 5 minutes) - Sessions expire after 30 minutes of inactivity (not creation time) - Add comprehensive test suite for session lifecycle - Update documentation (ARCHITECTURE.md, MCP_TOOLS.md, skill docs) Fixes frequent "Session not found. Please reinitialize." errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: update MCP session tests to use vi.hoisted for mock bindings - Use vi.hoisted() to create mockTransport, making it available in mock factory - Change mock constructor from arrow function to regular function - Use dynamic imports in each test to avoid binding issues - Update cleanup test to verify behavior (404 response) instead of mock assertions This fixes CI test failures caused by mock binding issues with vi.resetModules() and fake timers. All 8 tests now pass reliably. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: add auto-reconnection for MCP session in Chorus Plugin - Fix mock constructor syntax in MCP session tests (use function instead of arrow function) - Add automatic retry mechanism (max 3 retries) on 404 session expired - Detect HTTP status code and reinitialize session on failure - Resolve user pain point: session expiry no longer interrupts agent conversations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: move Prisma query to service layer and add cleanup comment - Add getProjectUuidsByGroup() to project.service.ts - Replace direct Prisma query in route.ts with service call - Add comment about setInterval assuming persistent Node.js process Addresses PR review feedback: - Move Prisma operations to service layer per project convention - Document setInterval limitations in serverless/edge environments 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: correct mock constructor syntax in MCP session tests * chore: remove package-lock.json (project uses pnpm) * chore: add package-lock.json to .gitignore --------- Co-authored-by: code <dev@sxrzpt.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 7e04a61 commit 9153581

9 files changed

Lines changed: 515 additions & 75 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ node_modules
99
!.yarn/plugins
1010
!.yarn/releases
1111
!.yarn/versions
12+
package-lock.json
1213

1314
# testing
1415
/coverage

docs/ARCHITECTURE.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,36 @@ Agents can filter results by project(s) using optional HTTP headers:
10931093
}
10941094
```
10951095

1096+
#### Session Management
1097+
1098+
MCP sessions use **sliding window expiration** with activity tracking:
1099+
1100+
**Mechanism**:
1101+
- Each session tracks `lastActivity` timestamp
1102+
- **30-minute timeout**: Sessions expire after 30 minutes of **inactivity**
1103+
- **Auto-renewal**: Every request automatically renews the session (updates `lastActivity`)
1104+
- **Periodic cleanup**: Expired sessions are cleaned up every 5 minutes
1105+
- **Memory storage**: Sessions are stored in-memory (cleared on server restart)
1106+
1107+
**Example**:
1108+
```
1109+
Time 0:00 - Session created (lastActivity = 0:00)
1110+
Time 0:15 - Request made (lastActivity updated to 0:15)
1111+
Time 0:30 - Request made (lastActivity updated to 0:30)
1112+
Time 0:55 - No activity since 0:30 → Session expires (25 minutes inactive)
1113+
Time 1:00 - Cleanup runs, session deleted
1114+
```
1115+
1116+
**Client handling**:
1117+
- When a session expires, the client receives HTTP 404: `Session not found. Please reinitialize.`
1118+
- The client should automatically reinitialize by creating a new session
1119+
- This is transparent in MCP clients that support auto-reconnect
1120+
1121+
**Why this approach?**
1122+
-**No fixed timeout**: Active sessions don't expire mid-work
1123+
-**Resource efficiency**: Inactive sessions are cleaned up automatically
1124+
- ⚠️ **Server restart**: All sessions are lost on restart (mitigated by auto-reconnect)
1125+
10961126
#### Public Tools (All Agents)
10971127

10981128
| Tool | Description |

docs/MCP_TOOLS.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,50 @@ The following tools respect project filtering:
8484

8585
---
8686

87+
## Session Management
88+
89+
MCP sessions implement **sliding window expiration** with activity tracking to balance resource efficiency with user experience.
90+
91+
### Mechanism
92+
93+
- **Activity tracking**: Each session records `lastActivity` timestamp
94+
- **30-minute timeout**: Sessions expire after 30 minutes of **inactivity** (not from creation time)
95+
- **Auto-renewal**: Every MCP request automatically renews the session by updating `lastActivity`
96+
- **Periodic cleanup**: Server checks for expired sessions every 5 minutes and cleans them up
97+
- **Memory storage**: Sessions are stored in-memory and lost on server restart
98+
99+
### Example Timeline
100+
101+
```
102+
Time 0:00 - Session created (lastActivity = 0:00)
103+
Time 0:15 - API call made (lastActivity updated to 0:15)
104+
Time 0:30 - API call made (lastActivity updated to 0:30)
105+
Time 0:55 - No activity since 0:30 → Session expires (25 minutes inactive)
106+
Time 1:00 - Cleanup runs, session deleted from memory
107+
Time 1:05 - Client tries to use session → HTTP 404: "Session not found"
108+
```
109+
110+
### Client Behavior
111+
112+
When a session expires:
113+
1. Server returns HTTP 404: `{"jsonrpc":"2.0","error":{"code":-32001,"message":"Session not found. Please reinitialize."},"id":null}`
114+
2. MCP client should automatically reinitialize by creating a new session
115+
3. This reconnection is transparent in clients that support auto-reconnect
116+
117+
### Why Sliding Expiration?
118+
119+
**No mid-work expiration**: Active agents can work for hours without timeout
120+
**Resource efficient**: Inactive sessions are cleaned up automatically
121+
⚠️ **Server restart impact**: All sessions lost on restart (mitigated by auto-reconnect)
122+
123+
### Best Practices
124+
125+
- **Implement auto-reconnect**: Handle HTTP 404 by reinitializing the session
126+
- **Keep sessions alive**: Regular tool calls automatically prevent timeout
127+
- **Clean shutdown**: Call DELETE `/api/mcp` when done to free resources
128+
129+
---
130+
87131
## Public Tools
88132

89133
Tools available to all Agents.

public/chorus-plugin/bin/chorus-api.sh

Lines changed: 85 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -225,64 +225,98 @@ cmd_mcp_tool() {
225225

226226
local mcp_url="${CHORUS_URL}/api/mcp"
227227
local auth_header="Authorization: Bearer ${CHORUS_API_KEY}"
228-
229-
# Step 1: Initialize MCP session
230-
local init_payload
231-
init_payload=$(cat <<JSONEOF
228+
local max_retries=3
229+
local retry_count=0
230+
local session_id=""
231+
local tool_response=""
232+
233+
# Retry loop for auto-reconnection on 404 (session expired)
234+
while [ $retry_count -le $max_retries ]; do
235+
# Step 1: Initialize MCP session
236+
local init_payload
237+
init_payload=$(cat <<JSONEOF
232238
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"chorus-hook","version":"0.1.1"}}}
233239
JSONEOF
234240
)
235241

236-
# Use a unique temp file for headers to avoid concurrent hooks overwriting each other
237-
local headers_file
238-
headers_file=$(mktemp "${STATE_DIR}/.mcp_headers.XXXXXX")
239-
240-
local init_response
241-
init_response=$(curl -s -S -X POST \
242-
-H "$auth_header" \
243-
-H "Content-Type: application/json" \
244-
-H "Accept: application/json, text/event-stream" \
245-
-D "$headers_file" \
246-
-d "$init_payload" \
247-
"$mcp_url" 2>/dev/null) || { rm -f "$headers_file"; die "MCP initialize failed"; }
248-
249-
# Extract session ID from response headers
250-
local session_id
251-
session_id=$(grep -i "^mcp-session-id:" "$headers_file" | tr -d '\r' | awk '{print $2}')
252-
rm -f "$headers_file"
253-
254-
if [ -z "$session_id" ]; then
255-
die "No MCP session ID returned"
256-
fi
242+
# Use a unique temp file for headers to avoid concurrent hooks overwriting each other
243+
local headers_file
244+
headers_file=$(mktemp "${STATE_DIR}/.mcp_headers.XXXXXX")
245+
246+
local init_response
247+
init_response=$(curl -s -S -X POST \
248+
-H "$auth_header" \
249+
-H "Content-Type: application/json" \
250+
-H "Accept: application/json, text/event-stream" \
251+
-D "$headers_file" \
252+
-d "$init_payload" \
253+
"$mcp_url" 2>/dev/null) || { rm -f "$headers_file"; die "MCP initialize failed"; }
254+
255+
# Extract session ID from response headers
256+
session_id=$(grep -i "^mcp-session-id:" "$headers_file" | tr -d '\r' | awk '{print $2}')
257+
rm -f "$headers_file"
258+
259+
if [ -z "$session_id" ]; then
260+
die "No MCP session ID returned"
261+
fi
257262

258-
# Step 2: Send initialized notification
259-
local notif_payload='{"jsonrpc":"2.0","method":"notifications/initialized"}'
260-
curl -s -S -X POST \
261-
-H "$auth_header" \
262-
-H "Content-Type: application/json" \
263-
-H "Accept: application/json, text/event-stream" \
264-
-H "mcp-session-id: $session_id" \
265-
-d "$notif_payload" \
266-
"$mcp_url" >/dev/null 2>&1 || true
267-
268-
# Step 3: Call the tool
269-
local call_payload
270-
call_payload=$(printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":%s}}' "$tool_name" "$arguments")
271-
272-
local tool_response
273-
tool_response=$(curl -s -S -X POST \
274-
-H "$auth_header" \
275-
-H "Content-Type: application/json" \
276-
-H "Accept: application/json, text/event-stream" \
277-
-H "mcp-session-id: $session_id" \
278-
-d "$call_payload" \
279-
"$mcp_url") || die "MCP tool call failed"
263+
# Step 2: Send initialized notification
264+
local notif_payload='{"jsonrpc":"2.0","method":"notifications/initialized"}'
265+
curl -s -S -X POST \
266+
-H "$auth_header" \
267+
-H "Content-Type: application/json" \
268+
-H "Accept: application/json, text/event-stream" \
269+
-H "mcp-session-id: $session_id" \
270+
-d "$notif_payload" \
271+
"$mcp_url" >/dev/null 2>&1 || true
272+
273+
# Step 3: Call the tool
274+
local call_payload
275+
call_payload=$(printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":%s}}' "$tool_name" "$arguments")
276+
277+
# Capture HTTP status code and response separately
278+
local http_code
279+
local response_file
280+
response_file=$(mktemp "${STATE_DIR}/.mcp_response.XXXXXX")
281+
282+
http_code=$(curl -s -S -X POST \
283+
-H "$auth_header" \
284+
-H "Content-Type: application/json" \
285+
-H "Accept: application/json, text/event-stream" \
286+
-H "mcp-session-id: $session_id" \
287+
-d "$call_payload" \
288+
-w "%{http_code}" \
289+
-o "$response_file" \
290+
"$mcp_url" 2>/dev/null) || http_code="000"
291+
292+
# Check if session expired (404)
293+
if [ "$http_code" = "404" ]; then
294+
retry_count=$((retry_count + 1))
295+
rm -f "$response_file"
296+
297+
if [ $retry_count -le $max_retries ]; then
298+
# Session expired - retry with new session
299+
continue
300+
else
301+
die "MCP session expired after retry. Please reinitialize."
302+
fi
303+
fi
304+
305+
# Read the response
306+
tool_response=$(cat "$response_file")
307+
rm -f "$response_file"
308+
309+
# Break the retry loop - request succeeded
310+
break
311+
done
280312

281313
# Step 4: Close session (best effort)
282-
curl -s -S -X DELETE \
283-
-H "$auth_header" \
284-
-H "mcp-session-id: $session_id" \
285-
"$mcp_url" >/dev/null 2>&1 || true
314+
if [ -n "$session_id" ]; then
315+
curl -s -S -X DELETE \
316+
-H "$auth_header" \
317+
-H "mcp-session-id: $session_id" \
318+
"$mcp_url" >/dev/null 2>&1 || true
319+
fi
286320

287321
# Response may be SSE format (event: message\ndata: {...}) or plain JSON
288322
# Strip SSE framing to get the JSON payload

public/chorus-plugin/skills/chorus/references/00-common-tools.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ Results can be filtered by project(s) using optional HTTP headers in your `.mcp.
5353

5454
Sessions track which agent is working on which task, powering UI features (Kanban worker badges, Task Detail active workers, Settings page). The Chorus Plugin **fully automates** session lifecycle — sessions are created, heartbeated, and closed automatically. See [05-session-sub-agent.md](05-session-sub-agent.md) for details.
5555

56+
**MCP Session Lifecycle** (connection level):
57+
- Sessions expire after 30 minutes of **inactivity** (sliding window)
58+
- Each MCP request automatically renews the session
59+
- Server restart clears all sessions (plugin auto-reconnects)
60+
5661
**What you do manually (the plugin handles everything else):**
5762

5863
| Tool | Purpose |

public/skill/references/00-common-tools.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,23 @@ Results can be filtered by project(s) using optional HTTP headers in your `.mcp.
5151

5252
---
5353

54+
## Session Lifecycle
55+
56+
MCP sessions implement **sliding window expiration** — sessions expire after 30 minutes of **inactivity**, not from creation time.
57+
58+
**Key points**:
59+
- Each request automatically renews the session (updates `lastActivity`)
60+
- Active agents can work indefinitely without timeout
61+
- Inactive sessions are cleaned up every 5 minutes
62+
- Server restart clears all sessions (clients should auto-reconnect)
63+
64+
**When you see "Session not found" (HTTP 404)**:
65+
- The session expired due to inactivity or server restart
66+
- Your MCP client should automatically reinitialize
67+
- This is transparent in clients with auto-reconnect support
68+
69+
---
70+
5471
## Project Groups
5572

5673
Projects can be organized into **Project Groups** — a single-level grouping for categorizing related projects together (e.g., all projects for the same product). A project belongs to at most one group, or can be ungrouped.

0 commit comments

Comments
 (0)