Repository navigation
fix(mcp): bound every read tool call by an explicit deadline - #43
Merged
Merged
Conversation
This was referenced Oct 9, 2026
kyle-elliott
added this pull request to stack #45
October 9, 2026 23:21
Every read-only MCP tool call now ends within one deadline. The default is 25 seconds, below the 30 seconds after which Oh My Pi ends an MCP request. SEMCTX_TOOL_DEADLINE_SECS overrides it. The session reads it once, clamps it to 5..=600 seconds, and carries it through the attach handshake. - call_tool builds a CallBudget for each call, passes it to the tool through an Extension, and drops the call at the deadline as a backstop. Edit tools get an unbounded budget, so a call cut off after a commit cannot misreport an applied edit. - Each tool passes the budget on explicitly. client_for and bound return a Client that carries the deadline. The client the server keeps for watchers never carries one. - The client keeps each attempt, loading retry, and retry wait inside the deadline. A loading answer that no longer fits returns a retryable failure. A call with no time left fails without sending. - One retry after 1 second for 502, 503, 504, 530, and a refused connection. Only requests that are safe to repeat use it: every GET, the search POST, and the batch lookup POST. Sync uploads, registration, and edit planning are never retried. The transport gets a 10 second connect timeout and no request timeout. - Retrieval waits at most 5 seconds for a first index, then reports IndexPending. A partial first index is still never served. - index_codebase registers its codebase in a task of its own, so the call deadline cannot leave a claimed first-index gate unfinished. Its body moves to a child module to keep tools.rs under the size limit. Refs #37
The refused-connect write test measured elapsed time. macOS holds a connect to a bound socket that does not listen until the 10 s connect timeout, and Windows retries a refused connect for about 2 s, so the test failed on both. Check the retry decision that the connect branch uses instead. Refs #37
kyle-elliott
force-pushed
the
fix/tool-call-deadlines
branch
from
October 10, 2026 00:05
1faf245 to
50300cc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 2 of 3 for #37, plus follow-up #46. Stack: #42 → this PR → #44 → #46.
Problem
Client::sendretried409 GraphLoadingfor up to 60 s, and the HTTP client had no deadline. OMP stops an MCP request after 30 s, so agents saw a generic timeout. Gateway errors were never retried. Retrieval calls waited for a first index with no limit.Change
CallBudget(src/mcp/budget.rs): our owncall_toolbuilds one budget per call and puts it in the request extensions. Each tool receives it throughExtension<CallBudget>and passes it explicitly toclient_for*and to the first-index waits. No deadline is read from ambient state. Atimeout_atbackstop returns anisErrorresult when the deadline passes.SEMCTX_TOOL_DEADLINE_SECSsets it (5–600; unset,0, or unreadable gives 25). It travels through the IPC handshake likeSEMCTX_MCP_RESYNC_SECS.rename_symbol,safe_delete_symbol,replace_symbol_body,insert_before_symbol,insert_after_symbol,undo_edit) get an unbounded budget. Their commit runs inspawn_blocking, so a deadline could only misreport an applied edit and lose itsedit_id.src/client/retry.rs: a client with a deadline keeps loading retries, token fetches, permit waits, and each attempt inside the deadline minus a 500 ms margin. A loading answer that no longer fits becomesRetryable { after }. A client without a deadline (CLI, daemon) behaves as before./v1/search, and the graph batch lookup. Sync uploads, registration, and edit planning are never retried.IndexPending. A partial first index is still never served.index_codebaseregisters its codebase in its own task, so a deadline cannot leave a claimed first-index gate unfinished. The body moves tosrc/mcp/indexing.rs(size limit).Verification
409 GraphLoading+Retry-After: 1forever, withSEMCTX_TOOL_DEADLINE_SECS=5.find_definitionandgrepreturnedisError: truewithnext: retry this call once after 1 s…after 4.0 s. Before this change the client kept retrying for 60 s, so OMP would have stopped the call at 30 s.