Skip to content

fix(python,nodejs): serialize concurrent client startup - #2570

Merged
SteveSandersonMS merged 4 commits into
github:mainfrom
xianjianlf2:fix/python-concurrent-start
Sep 10, 2026
Merged

SteveSandersonMS merged 4 commits into
github:mainfrom
xianjianlf2:fix/python-concurrent-start

Conversation

@xianjianlf2

@xianjianlf2 xianjianlf2 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Concurrent callers of CopilotClient.start() (including auto-start from createSession()/resumeSession()) can both observe a disconnected/connecting client, launch separate runtimes, and overwrite the shared connection. Serialize the state check and complete startup sequence per client so waiting callers reuse the established connection instead of racing.

Fixes the race in both Python and Node.js:

  • Python: guard start() with a per-client asyncio.Lock(); the original body moves into a private _start().
  • Node.js: guard start() with a shared in-flight startPromise; the original body moves into a private doStart(). Cleared on completion so a failed start can still be retried.

Related to #2560 (Node.js report) and #2561 (an earlier, since-closed Node.js attempt at this fix). This PR now fixes both languages and can close #2560.

Regression tests (Python and Node.js) pause startup independently during process creation, connection setup, and protocol verification, reproducing duplicate initialization before the fix. Additional tests cover retry after startup failure and (Python) cancellation of a waiting caller without cancelling the active startup.

Validation:

  • Python: before, 4 regression failures / 1 pass; after, 37 tests passed across test_client_start.py, test_jsonrpc.py, test_rpc_timeout.py, test_session.py, and test_github_token_provider.py. uv run ruff check, uv run ruff format --check ., and uv run ty check copilot passed.
  • Node.js: new client.test.ts single-flight and retry-after-failure tests pass; tsc --noEmit and eslint clean.
  • Manual validation (both languages): spawned a real fake-CLI subprocess and drove the real client startup path (only the network handshake mocked). Pre-fix: 2 concurrent start() calls spawned 2 real OS processes. Post-fix: 1.
  • Live CLI/E2E tests were not run; startup phases are mocked to control scheduling deterministically.

AI-assisted implementation and validation.

Node's CopilotClient.start() had the same race as the Python client:
concurrent callers (including auto-start from createSession()/resumeSession())
could each observe a non-connected state and spawn a separate CLI process,
with the last one silently overwriting cliProcess and orphaning the rest.

Guard start() with a shared in-flight startPromise so concurrent callers
join one startup instead of racing; the guard is cleared on completion so
a failed start can still be retried. The original start body moves into a
private doStart() with no behavior change.

Adds regression tests for single-flight behavior and retry-after-failure.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS changed the title fix(python): serialize concurrent client startup fix(python,nodejs): serialize concurrent client startup Sep 10, 2026

@SteveSandersonMS SteveSandersonMS left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. I reproduced the race with a real subprocess (not just mocks): concurrent start() calls spawned multiple CLI processes before this fix, and exactly one after, in both Python and Node.js.

I've also added the Node.js fix as a second commit, since the same race exists there (same early-return-on-connected-only pattern, same auto-start-from-createSession/resumeSession trigger) and the separate Node PR that would have covered it (#2561) was closed by its author without merging. It reuses that PR's approach (shared in-flight startPromise, doStart() split) since it was already minimal and idiomatic for JS. New tests pass, tsc --noEmit/eslint clean, and manual validation confirms 2→1 real subprocess spawns for both languages.

Thanks for the clear repro and tests on the Python side.

Comment thread python/test_client_start.py Fixed
SteveSandersonMS and others added 2 commits September 10, 2026 14:58
CodeQL flagged 'await second' as a statement with no effect. The await
is intentional (it drives the task to raise CancelledError inside
pytest.raises), so make the discard explicit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
"should recover marker after cold resume with explicit session id" is
the only raw-constructed CopilotClient test in this file that actually
calls sendAndWait (a real model round-trip). Every other raw
new CopilotClient({...}) test in this file only creates/resumes a
session (no model call), so it never exercises model-call auth.

Without an explicit gitHubToken, CopilotClient defaults
useLoggedInUser to true, so the runtime falls back to auto-detecting
credentials (keychain/gh CLI/ambient env) for the model call instead
of deterministically using the harness's fake token. That fallback
path is unreliable on some hosts (observed failing intermittently on
the Alpine ARM64 CI lane with "No GitHub OAuth token or Copilot HMAC
key provided", pre-dating this PR and unrelated to the startup-lock
fix). Passing gitHubToken explicitly - matching the already-reliable
createClient() harness helper and the passing "should resume a
session using a new client" test - removes the auto-login fallback
entirely.

Verified locally: full nodejs/test/e2e/session.e2e.test.ts suite
(37 passed, 2 pre-existing skips) passes with this change; tsc
--noEmit and eslint clean.
@SteveSandersonMS
SteveSandersonMS added this pull request to the merge queue Sep 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 10, 2026
@SteveSandersonMS
SteveSandersonMS added this pull request to the merge queue Sep 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 10, 2026
@SteveSandersonMS
SteveSandersonMS added this pull request to the merge queue Sep 10, 2026
Merged via the queue into github:main with commit 3dbd843 Sep 10, 2026
76 of 78 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CopilotClient.start() is not single-flight — concurrent auto-start spawns duplicate CLI processes

3 participants