Real cancellation of an in-flight MCP call is blocked on nr-vault, not on nr-llm. Filing here so the dependency is visible from this side.
When a run is cancelled, the agent runtime ends but the HTTP call it started keeps going until its timeout. Two independent reasons make an abort impossible on the current stack, either of them sufficient:
McpHttpTransport holds a PSR-18 ClientInterface and calls sendRequest() (:182, :189). PSR-18 returns a response, never a handle — there is nothing to cancel.
- Even with the Guzzle client in hand,
Client::sendRequest() sets RequestOptions::SYNCHRONOUS and waits inline, so Proxy::wrapSync routes to CurlHandler, a blocking curl_exec() whose promise is already settled when it is built. No cancel callback is ever attached on that branch. Guzzle promises support cancel() in general; this promise does not, in particular.
A genuine abort needs CurlMultiHandler, which does attach a real cancel function and exposes tick(). Reaching it requires an async send plus a reference to the handler — and both are private to nr-vault: the Guzzle client is created inside SecureHttpClientFactory::create(), its handler stack is built locally, and the interface exposes only ClientInterface plus builders.
Going around nr-vault is not an acceptable implementation. It would drop credential injection, the request-time SSRF DNS pin via CURLOPT_RESOLVE, and the audit write — none of which nr-llm's own host gate replaces.
So the first ticket belongs in nr-vault: a cancellable secure outbound-request primitive that preserves credential injection, SSRF and DNS pinning, and audit semantics. Ideally without exporting a Guzzle promise — nr-vault should stay the owner of the transport detail, or the abstraction leaks and the next transport change breaks nr-llm.
Only afterwards can nr-llm thread cancellation through to the transport, and only then does a distinct cancellation outcome at tool level make sense. Today the run level already has one (AgentRunOutcome::CANCELLED, non-retryable); the tool level has no way to receive it.
Independent of this, the worst-case stall is reducible on our side alone — see the operation-wide deadline issue.
Ref: ADR-161, ADR-154, ADR-116, ADR-103.
Real cancellation of an in-flight MCP call is blocked on nr-vault, not on nr-llm. Filing here so the dependency is visible from this side.
When a run is cancelled, the agent runtime ends but the HTTP call it started keeps going until its timeout. Two independent reasons make an abort impossible on the current stack, either of them sufficient:
McpHttpTransportholds a PSR-18ClientInterfaceand callssendRequest()(:182,:189). PSR-18 returns a response, never a handle — there is nothing to cancel.Client::sendRequest()setsRequestOptions::SYNCHRONOUSand waits inline, soProxy::wrapSyncroutes toCurlHandler, a blockingcurl_exec()whose promise is already settled when it is built. No cancel callback is ever attached on that branch. Guzzle promises supportcancel()in general; this promise does not, in particular.A genuine abort needs
CurlMultiHandler, which does attach a real cancel function and exposestick(). Reaching it requires an async send plus a reference to the handler — and both are private to nr-vault: the Guzzle client is created insideSecureHttpClientFactory::create(), its handler stack is built locally, and the interface exposes onlyClientInterfaceplus builders.Going around nr-vault is not an acceptable implementation. It would drop credential injection, the request-time SSRF DNS pin via
CURLOPT_RESOLVE, and the audit write — none of which nr-llm's own host gate replaces.So the first ticket belongs in nr-vault: a cancellable secure outbound-request primitive that preserves credential injection, SSRF and DNS pinning, and audit semantics. Ideally without exporting a Guzzle promise — nr-vault should stay the owner of the transport detail, or the abstraction leaks and the next transport change breaks nr-llm.
Only afterwards can nr-llm thread cancellation through to the transport, and only then does a distinct cancellation outcome at tool level make sense. Today the run level already has one (
AgentRunOutcome::CANCELLED, non-retryable); the tool level has no way to receive it.Independent of this, the worst-case stall is reducible on our side alone — see the operation-wide deadline issue.
Ref: ADR-161, ADR-154, ADR-116, ADR-103.