Filed from a source investigation. Everything below is cited to file:line; the one unverified link is called out explicitly at the end.
Environment data
- Python Environments extension version: source, branch
stellahuang-microsoft-inline-script-code-action @ 528346d7
- Python extension (
ms-python.python) version: N/A (observed manually)
- VS Code version (Help → About): N/A
- OS and version: Windows
- Python version (& distribution if applicable, e.g. Anaconda): N/A
- Environment manager in use: inline-script (PEP 723), behind
python-envs.inlineScripts.enabled
- Shell (bash / zsh / fish / pwsh / cmd / other): pwsh
- Remote / container scenario (none / WSL / SSH Remote / Dev Container / Codespaces): none
- Workspace type (single folder / multi-root / mono-repo): single folder
- Is this a regression? If yes, last known working extension version: unknown
Repro Steps
Requires python-envs.inlineScripts.enabled: true.
- Create
q01.py with a # /// script block declaring requires-python = ">=3.11" and dependencies = ["rich"], plus import rich.
- Run inline-script environment setup on it. A new cached environment is built.
- Create
q02.py with a byte-identical metadata block (same requires-python, same dependencies) and import rich.
- Run inline-script environment setup on
q02.py. The cached environment from step 2 is reused.
Expected behavior
After step 4, import rich resolves in q02.py without further action — as it did for q01.py in step 2.
Actual behavior
The environment is correctly selected and reused (visible in the UI), but import rich stays unresolved in q02.py until the file is saved. q01.py needed no save.
Analysis
There are four paths that can publish onDidChangeActiveEnvironment
|
Path |
Depends on manager.get()? |
| A |
manager list change → onDidChangeEnvironments |
No |
| B |
setEnvironmentCore fires directly (src/features/envManagers.ts:523) |
No |
| C |
routeability flip → handleInlineScriptRouteabilityChange (src/features/envManagers.ts:126-132, :974, :999) |
Yes |
| D |
manager's own onDidChangeEnvironment → refreshEnvironment (src/features/pythonApi.ts:120-137 → src/features/envManagers.ts:807) |
Yes |
onDidChangeActiveEnvironment is the only signal ms-python.python / Pylance consumes, bridged at src/features/pythonApi.ts:91-92.
Path B is designed to lose to path C: commitSelectionOperation (src/features/envManagers.ts:1211-1217) rejects B's now-stale revision because C already committed a newer one during manager.set.
Why the q01 / q02 asymmetry
Path A fires only for genuinely new environments — src/managers/builtin/inlineScript/envManager.ts:893-901 pushes a change only when !previous || !isSameDiscoveredEnvironment(...), then fires only if (changes.length > 0).
- q01 built a new environment → path A fired, and path A does not depend on
manager.get().
- q02 reused the cache entry (routing identity is content-only — sorted/normalized/deduped deps +
requires-python, src/common/inlineScript/routingRegistry.ts:243-252) → changes.length === 0 → no path A, leaving only C and D.
Why C and D can silently publish nothing
Both call manager.get(uri) → getInternal (envManager.ts:1076-1089) → getAssociationForMetadata (:1113).
set calls invalidateCachedAssociationValidation (:1049 → :2024-2027), which deletes cachedAssociationValidatedAt. That makes the fast cached return at :1129-1136 unreachable immediately after setup, so the full validateCachedAssociation (:1183) always runs.
validateCachedAssociation has roughly ten return undefined exits. When it returns undefined, both C and D compute newEnv = undefined, isSameEnvironment(undefined, undefined) is true, and nothing is published — with no log line. A later save re-enters via handleSavedMetadataChange (:1513) and publishes then, which matches the observed "save fixes it".
The root design problem: a transient "cannot validate right now" is indistinguishable from "there is no environment". Notably :1196, :1204, and :1216 already handle the analogous case by returning this.fsPathToEnv.get(scriptPath) instead of undefined, so there is in-file precedent for the fix shape.
Candidates ruled out
- Publish gate sampled before
manager.set (envManagers.ts:432 vs :445). Disproven experimentally: a unit test asserting the first-time inline publish passes identically with and without a fix to that gate, because path C publishes anyway.
isCacheEntryBusy (:1198-1200, :2601-2611). Disconfirmed: setupEnvironment.ts:68 awaits manager.create(...) to completion before :80 calls setEnvironment, and pendingCreations is cleared in a finally at envManager.ts:424-426, so neither busy signal is set by then.
inspectAssociationOwnership (:1497-1511). Env-dir/cache-root scoped, not script scoped, so it returns the same result for both scripts sharing one environment.
Still unidentified
Which specific return undefined branch fires for the reused-cache case. Remaining candidates are inspectAssociationMetadata → 'mismatched' (:1238-1243), sidecar 'unavailable' (:1245-1247), cacheEntryMatchesRuntimeAndMetadata (:1251-1253), and hasInstalledPackagesChanged (:1254-1259). None is obviously script-specific, since the metadata identity is content-based and identical across q01/q02.
This cannot be narrowed further from source alone.
Suggested next step
Add a traceVerbose at each return undefined in validateCachedAssociation identifying the branch, reproduce, and read the Python Environments output channel. That converts this from inference to a single log line. Once the branch is known, the fix is likely to follow the :1196 precedent — return the cached environment for transient validation failures rather than undefined.
Additional context
Not related to the unresolved-import quick fix on the same branch; this reproduces through the CodeLens path too. This is an envs-extension issue, not a Pylance one: the interpreter change is never published, so there is nothing for Pylance to react to.
Environment data
stellahuang-microsoft-inline-script-code-action@528346d7ms-python.python) version: N/A (observed manually)python-envs.inlineScripts.enabledRepro Steps
Requires
python-envs.inlineScripts.enabled: true.q01.pywith a# /// scriptblock declaringrequires-python = ">=3.11"anddependencies = ["rich"], plusimport rich.q02.pywith a byte-identical metadata block (samerequires-python, samedependencies) andimport rich.q02.py. The cached environment from step 2 is reused.Expected behavior
After step 4,
import richresolves inq02.pywithout further action — as it did forq01.pyin step 2.Actual behavior
The environment is correctly selected and reused (visible in the UI), but
import richstays unresolved inq02.pyuntil the file is saved.q01.pyneeded no save.Analysis
There are four paths that can publish
onDidChangeActiveEnvironmentmanager.get()?onDidChangeEnvironmentssetEnvironmentCorefires directly (src/features/envManagers.ts:523)handleInlineScriptRouteabilityChange(src/features/envManagers.ts:126-132,:974,:999)onDidChangeEnvironment→refreshEnvironment(src/features/pythonApi.ts:120-137→src/features/envManagers.ts:807)onDidChangeActiveEnvironmentis the only signalms-python.python/ Pylance consumes, bridged atsrc/features/pythonApi.ts:91-92.Path B is designed to lose to path C:
commitSelectionOperation(src/features/envManagers.ts:1211-1217) rejects B's now-stale revision because C already committed a newer one duringmanager.set.Why the q01 / q02 asymmetry
Path A fires only for genuinely new environments —
src/managers/builtin/inlineScript/envManager.ts:893-901pushes a change only when!previous || !isSameDiscoveredEnvironment(...), then fires onlyif (changes.length > 0).manager.get().requires-python,src/common/inlineScript/routingRegistry.ts:243-252) →changes.length === 0→ no path A, leaving only C and D.Why C and D can silently publish nothing
Both call
manager.get(uri)→getInternal(envManager.ts:1076-1089) →getAssociationForMetadata(:1113).setcallsinvalidateCachedAssociationValidation(:1049→:2024-2027), which deletescachedAssociationValidatedAt. That makes the fast cached return at:1129-1136unreachable immediately after setup, so the fullvalidateCachedAssociation(:1183) always runs.validateCachedAssociationhas roughly tenreturn undefinedexits. When it returnsundefined, both C and D computenewEnv = undefined,isSameEnvironment(undefined, undefined)istrue, and nothing is published — with no log line. A later save re-enters viahandleSavedMetadataChange(:1513) and publishes then, which matches the observed "save fixes it".The root design problem: a transient "cannot validate right now" is indistinguishable from "there is no environment". Notably
:1196,:1204, and:1216already handle the analogous case by returningthis.fsPathToEnv.get(scriptPath)instead ofundefined, so there is in-file precedent for the fix shape.Candidates ruled out
manager.set(envManagers.ts:432vs:445). Disproven experimentally: a unit test asserting the first-time inline publish passes identically with and without a fix to that gate, because path C publishes anyway.isCacheEntryBusy(:1198-1200,:2601-2611). Disconfirmed:setupEnvironment.ts:68awaitsmanager.create(...)to completion before:80callssetEnvironment, andpendingCreationsis cleared in afinallyatenvManager.ts:424-426, so neither busy signal is set by then.inspectAssociationOwnership(:1497-1511). Env-dir/cache-root scoped, not script scoped, so it returns the same result for both scripts sharing one environment.Still unidentified
Which specific
return undefinedbranch fires for the reused-cache case. Remaining candidates areinspectAssociationMetadata→'mismatched'(:1238-1243), sidecar'unavailable'(:1245-1247),cacheEntryMatchesRuntimeAndMetadata(:1251-1253), andhasInstalledPackagesChanged(:1254-1259). None is obviously script-specific, since the metadata identity is content-based and identical across q01/q02.This cannot be narrowed further from source alone.
Suggested next step
Add a
traceVerboseat eachreturn undefinedinvalidateCachedAssociationidentifying the branch, reproduce, and read the Python Environments output channel. That converts this from inference to a single log line. Once the branch is known, the fix is likely to follow the:1196precedent — return the cached environment for transient validation failures rather thanundefined.Additional context
Not related to the unresolved-import quick fix on the same branch; this reproduces through the CodeLens path too. This is an envs-extension issue, not a Pylance one: the interpreter change is never published, so there is nothing for Pylance to react to.