GitHub.Copilot.Test.E2E.RpcExtensionsLoadedE2ETests.Reload_Picks_Up_Extension_Added_After_Session_Create fails intermittently against github/copilot-agent-runtime CI with:
System.IO.IOException : Communication error with Copilot CLI: Request session.extensions.reload failed with message: session not found: <session-id>
---- GitHub.Copilot.RemoteRpcException : Request session.extensions.reload failed with message: session not found: <session-id>
at GitHub.Copilot.Test.Harness.TestHelper.WaitForConditionAsync(...)
at RpcExtensionsLoadedE2ETests.Reload_Picks_Up_Extension_Added_After_Session_Create() line 256
Mechanism
The test already knows extension setup is asynchronous after session create, and polls ReloadAsync for that reason:
transientExceptionFilter: ex => ex.ToString().Contains("Extensions not available", StringComparison.OrdinalIgnoreCase),
But the runtime has two distinct transient errors in that same window, not one. session.extensions.reload resolves the service via SessionExtensionsService::for_session(&call.session_id), which returns Result<Option<_>, SessionRegistryError> (src/runtime/src/shared_api/extensions.rs):
SessionExtensionsService::for_session(&call.session_id)
.map_err(|error| DispatchError::message(error.to_string()))
.and_then(|service| {
service.ok_or_else(|| DispatchError::message("Extensions not available"))
})
Err(SessionRegistryError::NotFound) → "session not found: {session_id}" — the session isn't in the runtime's global registry yet.
Ok(None) → "Extensions not available" — the session is registered, but the extensions service isn't installed yet.
Which one you get depends purely on whether session registration has landed by the time the first poll fires. The filter matches only the second, so when registration is the slower of the two, WaitForConditionAsync rethrows on the first attempt rather than retrying — the failing runs show Elapsed=00:00:00.24, RequestId=3, Status=Failed, i.e. it never polled a second time.
Evidence it's a race, not a runtime regression
Within a single workflow run all four backend legs use the same freshly built CLI binary, and the results differ between legs:
- Run 31840012655: failed on
capi, anthropic-messages, openai-completions; passed on openai-responses.
- Run 31843522932: failed on
capi, anthropic-messages, openai-responses.
The test doesn't exercise the model provider at all, so the backend is not the variable — startup timing is.
Suggested fix
Widen the transient filter to cover both messages, e.g.:
transientExceptionFilter: ex =>
ex.ToString().Contains("Extensions not available", StringComparison.OrdinalIgnoreCase)
|| ex.ToString().Contains("session not found", StringComparison.OrdinalIgnoreCase),
The same consideration applies to any other test that polls a session-scoped RPC immediately after CreateSessionAsync returns.
GitHub.Copilot.Test.E2E.RpcExtensionsLoadedE2ETests.Reload_Picks_Up_Extension_Added_After_Session_Createfails intermittently againstgithub/copilot-agent-runtimeCI with:Mechanism
The test already knows extension setup is asynchronous after session create, and polls
ReloadAsyncfor that reason:But the runtime has two distinct transient errors in that same window, not one.
session.extensions.reloadresolves the service viaSessionExtensionsService::for_session(&call.session_id), which returnsResult<Option<_>, SessionRegistryError>(src/runtime/src/shared_api/extensions.rs):Err(SessionRegistryError::NotFound)→"session not found: {session_id}"— the session isn't in the runtime's global registry yet.Ok(None)→"Extensions not available"— the session is registered, but the extensions service isn't installed yet.Which one you get depends purely on whether session registration has landed by the time the first poll fires. The filter matches only the second, so when registration is the slower of the two,
WaitForConditionAsyncrethrows on the first attempt rather than retrying — the failing runs showElapsed=00:00:00.24, RequestId=3, Status=Failed, i.e. it never polled a second time.Evidence it's a race, not a runtime regression
Within a single workflow run all four backend legs use the same freshly built CLI binary, and the results differ between legs:
capi,anthropic-messages,openai-completions; passed onopenai-responses.capi,anthropic-messages,openai-responses.The test doesn't exercise the model provider at all, so the backend is not the variable — startup timing is.
Suggested fix
Widen the transient filter to cover both messages, e.g.:
The same consideration applies to any other test that polls a session-scoped RPC immediately after
CreateSessionAsyncreturns.