Skip to content

Keep Python sessions in sync when a workspace .venv is deleted or recreated - #16431

Open
dhruvisompura wants to merge 15 commits into
mainfrom
dhruvi/python-venv-version-change
Open

dhruvisompura wants to merge 15 commits into
mainfrom
dhruvi/python-venv-version-change

Conversation

@dhruvisompura

@dhruvisompura dhruvisompura commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4556
Fixes #16191

If you delete a workspace .venv and recreate it with a different Python version while Positron is open, the console and the session picker keep showing the old version. Restarting the console reports "Python 3.12" while it actually runs 3.11.

The cause is how the file watcher reports a deleted folder: it sends one delete for the folder, not one for each file inside it. So the Python extension got one of two things:

  • A fast delete and recreate arrived as an update to the same interpreter path. The runtime manager ignored same-path updates.
  • A slower one arrived as deletes of .venv/bin and .venv. Neither matches the **/python pattern the extension watches, so the deletion went unnoticed.

Either way, the old runtime stayed registered. New sessions and restarts used its name and version, while launching whatever binary was now at that path. Both cases showed up in real runs.

Changes

  1. Same-path update (manager.ts). When the interpreter at a path changes major or minor version, the runtime manager registers the path again and shuts down sessions still on the old runtime. A patch-only change is ignored, because discovery and a live lookup can disagree on the patch version of the same venv. The handler logs what it decided either way.
  2. Folder delete (pythonWatcher.ts, nativeAPI.ts).
    • A deletes-only watcher sends every deleted workspace path through the existing workspace event. The delete handler then removes every env whose executable is the deleted path or inside it. The watcher sends the same watch request as the existing **/python watcher, so VS Code merges the two and starts no extra file system watcher.
    • resolveEnv now skips a path that doesn't exist. PET still resolves a just-deleted executable. Without this check, the lookups that follow a deletion added the env straight back, as "Python 3.12.14 (Unknown)".
    • When the folder comes back, so does the env. On Linux the file watcher reports a new folder but not the files created inside it, so a recreated .venv/bin/python was never seen and the venv dropped out of both interpreter pickers. When a delete removes a workspace env, nativeAPI.ts now watches the folders between the workspace and its executable. Once one is created, it waits briefly for the executable and adds the env back. addEnv also checks the list again after its last await, so two reports of the same recreate add the env once.

The folder-delete fix is its own commit (542eb9f), and so is the Linux re-add (ed127af), in case reviewers want either split out.

Both upstream files keep their changes inside Positron markers.

Behavior change: after the swap or the deletion, the console session on the old interpreter shuts down. This already happened when Positron noticed a deleted interpreter. Nothing new starts on its own.

Notes for review

  • Cost of the same-path branch: same-path updates only fire when an env's details actually change (hasChanged in nativeAPI.ts), so the extra lookup is rare.
  • Cost of the delete watcher: a deleted folder usually arrives as one event, not one per file inside it. Handling a delete compares the deleted path against the known envs' executables. When that removes an env, a watcher for each folder on the way to its executable stays open until the folder comes back.
  • Not covered: venvs outside the workspace folders, such as ~/.virtualenvs. The new watcher only watches workspace folders.

Videos

Recreating .venv with Python 3.11:

4556-venv-version-change.mp4

Deleting .venv:

4556-venv-deleted.mp4

Release Notes

New Features

  • N/A

Bug Fixes

Validation Steps

@:interpreter @:sessions @:new-folder-flow

  1. Open a folder with a uv venv on Python 3.12 (uv venv -p 3.12 && uv pip install ipykernel), and start its Python session.
  2. In the terminal, run rm -rf .venv && uv venv -p 3.11 && uv pip install ipykernel.
  3. The 3.12 session shuts down, and the session picker offers only Python 3.11 for the folder. Start it and run import sys; sys.version. The label and the output should both say 3.11.
  4. Repeat step 1, then run only rm -rf .venv. The session shuts down, and the session picker no longer lists the folder's venv.
  5. On Linux, with a different Python selected and no console on the venv, run rm -rf .venv && uv venv .venv. The session picker still lists the folder's venv. The new e2e test python-venv-recreated.test.ts covers this.

Deleting a venv and recreating it with a different Python version reaches
the runtime manager as an update to the same interpreter path, not as a
removal and an addition, because the file watcher only reports the
deletion of the .venv folder. The manager ignored same-path updates, so
the old version stayed registered and sessions started or restarted from
it showed the old version while running the new interpreter.

Re-resolve the path on a same-path update. If the runtime changed,
replace the registered runtime and shut down sessions still backed by
the old one.
Deleting a folder is reported by the file watcher as the deletion of that
folder, not of each file inside it. Deleting .venv often reports only
.venv and .venv/bin, which don't match the watcher's **/python pattern,
so the env stayed registered: its session kept running and the picker
kept offering it.

Watch the workspace for deletes of any path, and remove the envs whose
executable was inside the deleted path. The extra watcher sends the same
watch request as the existing one, so it adds no file system watcher.

Also skip resolving an executable that no longer exists. PET can still
resolve a deleted executable, and the lookups that follow a deletion
added the env straight back.
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

E2E Tests 🚀
This PR will run tests tagged with: @:critical @:interpreter @:sessions @:new-folder-flow @:console

Why these tags?
Tag Source
@:critical Always runs (required)
@:interpreter PR description
@:sessions PR description
@:new-folder-flow PR description
@:console Changed files

More on automatic tags from changed files.

readme  valid tags

@dhruvisompura

Copy link
Copy Markdown
Contributor Author

/test

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

🔎 Exploratory testing 542eb9f

Not run: this branch predates the drive-positron helper update that exploratory runs need. Rebase it onto main and comment /explore again.
View run →

The same-path branch only acted when a runtime was already registered
for the path. Runtimes that came from the workspace recommendation are
never added to that registry, so a venv recreated with another Python
could leave its old session running with the old version. Discovery and
a live resolve can also disagree on the patch version of the same
interpreter, which would have shut down a working session.

Act when the major or minor version changes, without consulting the
registry, and log what the handler decided. The recreateRuntime branch
now uses the same shutdown helper.

Report folder deletes through the existing workspace event instead of a
new one, so pythonWatcher.ts keeps a single Positron block.
@dhruvisompura

Copy link
Copy Markdown
Contributor Author

/test

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

🔎 Exploratory testing d60beab

1 moderate
View report →

@dhruvisompura
dhruvisompura marked this pull request as ready for review October 6, 2026 00:02
The nativeAPI tests fire the workspace event on a mocked watcher, so
they pass even if the deletes-only watcher is removed. Pin the watcher
itself: a deleted .venv folder must come through as a Deleted
workspace event.
@dhruvisompura

Copy link
Copy Markdown
Contributor Author

/pete

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

PETE's assessment 🧪

Verdict: 🟢 Adequate -- Every source branch in this three-file fix has a matching extension-host unit test that asserts the exact new behavior, including the #4556 regression.

What changed

  • manager.ts: new "changed in place" branch -- a same-path interpreter update with a different major/minor version re-registers the path and shuts down stale sessions (keeping the current one); patch-only changes are ignored. Session-shutdown logic extracted into a shutdownSessionsForPath helper.
  • pythonWatcher.ts: a second ** delete-only watcher so rm -rf .venv (reported as one folder delete) still fires a workspace-delete event.
  • nativeAPI.ts: _doResolveEnv now skips non-existent paths (so a just-deleted exe isn't re-added), and the delete handler removes every env at or inside the deleted folder.

Tests in this PR

  • Unit (Vitest/Mocha) ✅ (existing coverage -- these are extension-host Mocha, see below)
  • Extension host ✅ (added pythonWatcher.unit.test.ts, nativeAPI.unit.test.ts suite, manager.unit.test.ts cases)
  • E2E (Playwright) ✅ (not warranted -- logic is reachable at the unit seam)

Existing coverage

New tests are the primary coverage and each maps to a changed branch:

  • manager.unit.test.ts -- minor-version change replaces runtime + shuts down the stale session while sparing the current one (keepRuntimeId); patch-only change is a no-op. Covers manager.ts:230-249.
  • nativeAPI.unit.test.ts ("workspace path deleted") -- folder delete removes the nested env, deleted exe isn't resolved back (the pathExists guard), delete outside the env is a no-op. Covers nativeAPI.ts:787-789, 876-881.
  • pythonWatcher.unit.test.ts -- the ** delete watcher fires a workspace-delete with the expected ignore flags. Covers pythonWatcher.ts:85-91.

Suggested additions

None.

Deployment note (optional)

The fix centers on file-watcher delete semantics, a known Windows/web hotspot (file watchers use different backends; web has no direct fs). The unit tests stub the watcher, so they don't exercise real platform behavior. The PR body tags @:interpreter @:sessions @:new-folder-flow but no @:win or @:web. This is an indirect gap -- the pure logic is well-covered at the unit level, so the verdict stays Adequate -- but if any @:interpreter/@:sessions e2e exercises venv recreate/delete, consider tagging it @:win so a Windows watcher-ordering regression would surface at PR time rather than in nightly.


PETE (Positron Extreme Test Experiment) - LLM-based test-coverage advisor, in pilot. Runs only on demand -- comment /pete (or /recheck-tests, /rePETE, /re-pete) on this PR to run (or re-run) it. Please share feedback on how PETE performed here.

@dhruvisompura

Copy link
Copy Markdown
Contributor Author

This also fixes #16191

BEFORE

2-main.mp4

AFTER

1-with-16431.mp4

@isabelizimm isabelizimm 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.

when running in a workspace that already has a venv: Python: Create Environment... > Delete and Recreate, I am consistently getting this modal. I don't see any errors in the python language pack or kernel supervisor, but my suspicion is there's a race condition somewhere that is conflicting with the venv watcher

Image

In other use cases, including the uv sync, this works super well, I'm excited that this feels less clunky! 🙌

@dhruvisompura

Copy link
Copy Markdown
Contributor Author

when running in a workspace that already has a venv: Python: Create Environment... > Delete and Recreate, I am consistently getting this modal. I don't see any errors in the python language pack or kernel supervisor, but my suspicion is there's a race condition somewhere that is conflicting with the venv watcher

Image In other use cases, including the `uv sync`, this works super well, I'm excited that this feels less clunky! 🙌

Ah, thanks for finding this issue! I didn't think to test the Python: Create Environment... > Delete and Recreate command so I appreciate you catching this. I'll dig into this and see if I can figure out what's going on.

On Linux the file watcher reports a new folder but not the files created
inside it, so after .venv was deleted and recreated, nothing saw the new
bin/python. The folder delete removed the env, and it stayed out of both
interpreter pickers until the next full discovery. A venv that was the
selected interpreter or had a console was unaffected, because another
lookup re-added it.

When a folder delete removes a workspace env, watch the folders between
the workspace and its executable. Once one is created, wait briefly for
the executable to appear and add the env back.

addEnv now checks the list again after its last await, so the folder
watcher and the executable watcher reporting the same recreate add the
env once instead of twice.

The e2e test runs the real file watcher, which is where Linux differs.
@isabelizimm

Copy link
Copy Markdown
Contributor

Okay, I clean-slated everything and can no longer reproduce that Create Environment error 💀 if I see it again, I can open up a follow up!

private async _doResolveEnv(envPath: string): Promise<PythonEnvInfo | undefined> {
// PET can resolve an executable that no longer exists, which would add a
// just-deleted env straight back.
if (!(await pathExists(envPath))) {

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.

The PET can also resolve from PATH, which means things like python are legitimate interpreters, even though they would not pass here. Easy fix here would be to only run this when path.isAbsolute(envPath), so deleted-venv protection stays and command names still go to PET.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 1a7a310!

checking = true;
try {
// The executable can lag its folder by a moment while the venv is written.
for (let attempt = 0; attempt < 10; attempt += 1) {

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.

It would be safer to keep the retry loop going until resolving/adding the environment succeeds, not just until the executable file exists.

🤖 suggestion is to dispose the temporary watcher after  finder.resolve() returns an environment and addEnv() has had a chance to add it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in d1ba6c1!

@dhruvisompura

dhruvisompura commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

@isabelizimm testing the Python: Create Environment... flow turned up three problems that I addressed. I think I may have figured out what was causing that modal to show up for you (by looking at the code). You can see the new behavior below.

Use Existing:

1-use-existing.mp4

Delete and Recreate (same Python version):

2-delete-and-recreate.mp4

I'm least sure about the changes I made to Use Existing flow so maybe you can double check that for me (its commit c944a03). I thought it was a little odd that we shutdown the console session for that case. We now leave the console session - let me know if this is wrong and I'm missing something though!

And here's a quick summary of the three commits for you

26a429a Don't shut down the same Python session twice

  • Cause: with Delete and Recreate, the session got two shutdown requests. The first came from the file watcher seeing .venv deleted (new in this PR). The second came from the command recreating the runtime (already on main). The second request reached a kernel that had already exited, which produced Cannot send message to session ...: the kernel has exited.
  • Fix: the manager skips sessions that have exited or are shutting down. It also doesn't send a second shutdown to a session it's already shutting down. Both paths stay, because the watcher catches deletes outside the command (rm -rf .venv) and the command covers environments outside the folder.

afcc654 Start a new session after recreating a Python environment

  • Cause: Delete and Recreate with the same Python version left no running session (happens on main too). The new venv keeps the same runtime ID, so selecting it brought the old, exited console back to the front instead of starting a new session.
  • Fix: when a runtime is recreated, a new session starts unless one is already running. This also covers the uv.lock "Use uv sync?" prompt, pixi install, and the global environment.

c944a03 Keep the session running when Create Environment reuses a venv

  • Cause: Use Existing leaves .venv unchanged, but it was handled like a re-create, so the running session was shut down. On main, no new session started.
  • Fix: the providers now mark a Use Existing result as reused, and the runtime is recreated only for Delete and Recreate or a new environment, not for Use Existing. The reused marker lives in a new Positron file (reusedEnvironment.ts), so the proposed API's result type is unchanged.

@dhruvisompura
dhruvisompura force-pushed the dhruvi/python-venv-version-change branch from d432a4b to 724927d Compare October 7, 2026 01:35
@dhruvisompura
dhruvisompura force-pushed the dhruvi/python-venv-version-change branch from 724927d to 2cdb448 Compare October 7, 2026 17:45
* removed env, this watches every folder between the workspace and the executable, and
* looks the executable up once one of them is created.
*/
export class RecreatedEnvWatcher implements Disposable {

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.

we may want an eventual timeout for this watcher, since not every deleted venv will get recreated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants