You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #2058. Design: openspec/changes/mcp-tool-outcome-receipts/design.md (D3).
Problem
McpClientManager.IsTransportOrSessionFailure (src/Netclaw.Daemon/Mcp/McpClientManager.cs:1782-1800) returns true for every HttpRequestException. It does not look at StatusCode. An HTTP 500, 429, or 403 on tools/call takes the same path as a dropped socket:
InvokeSharedAsync (:604-612) → ReconnectAfterTransportFailureAsync (:628-660) → ReconnectAsync builds a new HttpClientTransport and McpClient, sends server/discover, initialize, notifications/initialized, and tools/list, and disposes the old client with an HTTP DELETE.
That is about five extra requests per failed call. The generation counter climbs by one each time.
Impact:
A new session cannot fix a server-side application error. The reconnect is wasted work.
Servers with a request budget get hit harder. short.io allows 30 requests per 60 s across all MCP traffic and returns 429 with a non-JSON-RPC body. One 429 costs about five more requests, so the next call is more likely to get a 429. The loop feeds itself.
MCP server '<name>' connected as generation N climbs with no config change. This misleads the operator.
The predicate has a second consumer. McpClientManager.LoadAsync (:541-551, the MCP prompt-skill path) catches McpException when not a transport failure, and any exception when a transport failure. After the predicate changes, an HttpRequestException with an application status would match neither clause and escape to the dispatcher.
Expected
IsTransportOrSessionFailure returns true for HttpRequestException only when StatusCode is null or 404 (session expiry per the Streamable HTTP spec). Every other status is an application error. The other branches of the predicate do not change.
Prompt path: the first catch clause in LoadAsync widens to McpException or HttpRequestException when not a transport failure. It returns the existing failed load result. No new clause, no new type.
Keep the rule: never replay the failed invocation.
Out of scope: Retry-After propagation. HttpRequestException exposes StatusCode only, not headers.
Tests
HttpRequestException with StatusCode = 500 or 429: no reconnect, generation unchanged, result names the status.
HttpRequestException with StatusCode = 404: one reconnect.
HttpRequestException with no status: one reconnect.
Prompt load with HttpRequestExceptionStatusCode = 500: failed load result that names the prompt; no reconnect; no exception reaches the dispatcher.
Part of #2058. Design:
openspec/changes/mcp-tool-outcome-receipts/design.md(D3).Problem
McpClientManager.IsTransportOrSessionFailure(src/Netclaw.Daemon/Mcp/McpClientManager.cs:1782-1800) returns true for everyHttpRequestException. It does not look atStatusCode. An HTTP 500, 429, or 403 ontools/calltakes the same path as a dropped socket:InvokeSharedAsync(:604-612) →ReconnectAfterTransportFailureAsync(:628-660) →ReconnectAsyncbuilds a newHttpClientTransportandMcpClient, sendsserver/discover,initialize,notifications/initialized, andtools/list, and disposes the old client with an HTTPDELETE.That is about five extra requests per failed call. The generation counter climbs by one each time.
Impact:
MCP server '<name>' connected as generation Nclimbs with no config change. This misleads the operator.The predicate has a second consumer.
McpClientManager.LoadAsync(:541-551, the MCP prompt-skill path) catchesMcpExceptionwhen not a transport failure, and any exception when a transport failure. After the predicate changes, anHttpRequestExceptionwith an application status would match neither clause and escape to the dispatcher.Expected
IsTransportOrSessionFailurereturns true forHttpRequestExceptiononly whenStatusCodeis null or 404 (session expiry per the Streamable HTTP spec). Every other status is an application error. The other branches of the predicate do not change.LoadAsyncwidens toMcpExceptionorHttpRequestExceptionwhen not a transport failure. It returns the existing failed load result. No new clause, no new type.Out of scope:
Retry-Afterpropagation.HttpRequestExceptionexposesStatusCodeonly, not headers.Tests
HttpRequestExceptionwithStatusCode = 500or429: no reconnect, generation unchanged, result names the status.HttpRequestExceptionwithStatusCode = 404: one reconnect.HttpRequestExceptionwith no status: one reconnect.HttpRequestExceptionStatusCode = 500: failed load result that names the prompt; no reconnect; no exception reaches the dispatcher.