Skip to content

Add inline script activation-time discovery (PEP 723 PR 8/16) - #1722

Open
Stella Huang (StellaHuang95) wants to merge 4 commits into
microsoft:mainfrom
StellaHuang95:pep723-pr8-activation-discovery-v2
Open

Add inline script activation-time discovery (PEP 723 PR 8/16)#1722
Stella Huang (StellaHuang95) wants to merge 4 commits into
microsoft:mainfrom
StellaHuang95:pep723-pr8-activation-discovery-v2

Conversation

@StellaHuang95

@StellaHuang95 Stella Huang (StellaHuang95) commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Part of #1602 (PEP 723 inline script env support). Design doc: #1601.

Builds on the merged persistence work in #1697 and is rebased on current main.

Roadmap context

This is PR 8 of 16 in the PEP 723 inline-script roadmap. It makes extension-owned cached environments discoverable after activation; automatic script routing remains a separate follow-up.

Phase 2: Manager PR Status
PR 4: InlineScriptEnvManager skeleton merged (#1610)
PR 5a: generic env-creation utilities merged (#1651)
PR 5b: inline-script cache + interpreter utilities merged (#1655)
PR 5c: create() happy path merged (#1656)
PR 6: create() uv-install fallback merged (#1696)
PR 7: persistence (get / set + Memento) merged (#1697)
PR 8: activation-time discovery this PR
PR 9: route PEP 723 scripts to the inline manager follow-up

Why this PR

PR 7 persists and lazily rehydrates a specific script's selected inline environment when the manager is asked for that script. The manager still has no global inventory, however: getEnvironments() returns [], and cached environments are not published after an extension-host restart unless a script-specific lookup happens to reconstruct one.

This PR implements the activation-discovery portion of Q4 in the design:

  • walk the versioned global cache without blocking extension activation;
  • validate each cache entry before exposing it as a PythonEnvironment;
  • publish add/remove events as valid entries appear or become definitively invalid;
  • preserve previously known entries through locks and transient filesystem failures;
  • retry bounded transient work without creating a permanent watcher or polling loop.

What this PR does

Defers discovery until after manager registration

  • Starts activation discovery with setImmediate() after the manager is registered.
  • Leaves the feature-gate-off path unchanged: no manager registration, cache scan, timer, or filesystem work.
  • Keeps extension activation non-blocking.

Publishes a global discovered collection

  • getEnvironments('all') returns a copy of the validated cache collection.
  • Other scopes remain empty because inline cache entries live in extension global storage rather than belonging to one workspace.
  • Results are sorted deterministically.
  • Collection reconciliation emits precise EnvironmentChangeKind.add and remove events only when manager identity, executable path, or Python version materially changes.

Validates every cache entry before publication

A candidate must:

  1. be a normal directory rather than a symlink;
  2. be unlocked and physically contained beneath the expected cache root;
  3. have a valid .meta.json sidecar;
  4. have an available cached launcher and base interpreter;
  5. resolve into a real PythonEnvironment;
  6. prove direct-child cache ownership through sysPrefix; and
  7. match the Python release recorded in the sidecar.

Discovery is read-only. It does not delete, rebuild, or rewrite invalid entries.

Distinguishes definitive invalidity from uncertainty

  • Missing, malformed, unowned, or version-mismatched entries are omitted.
  • Locked entries and transient I/O/resolver/ownership failures preserve the previously published environment and request another activation pass.
  • A missing cache root is treated as an empty cache.
  • An unavailable cache root preserves the current collection and remains retryable.

Handles concurrent cache changes

  • Activation scans compare an initial and final directory snapshot so an entry created during the scan triggers a follow-up pass.
  • Locks use the cache entry name/hash as their identity.
  • Published entries use that same cache-key identity rather than mixing lexical cache paths with canonical sysPrefix paths, avoiding false removal through symlink/junction path differences.
  • Lock probing uses lstat; only ENOENT means unlocked. EIO, access failures, and other uncertain states fail closed.

Coalesces refresh work without weakening activation discovery

  • Concurrent compatible refreshes share one scan.
  • Activation joining an in-flight explicit refresh receives one snapshot-aware follow-up rather than accepting the explicit pass's weaker single-snapshot semantics.
  • Explicit refresh() cancels activation retries, performs one settled pass, and schedules no delayed work afterward.

Retries activation discovery only while useful

  • Follow-up delays are bounded at 1 second, 5 seconds, and 30 seconds.
  • Retries are requested for locks, transient failures, or a changed cache snapshot.
  • No permanent filesystem watcher or unbounded polling loop is introduced.
  • Disposal cancels pending timers and prevents in-flight scans from publishing late results.

Strengthens the Windows usability guard

  • Windows cache validation now checks both the cached environment launcher and the base interpreter referenced by pyvenv.cfg.
  • A surviving base interpreter no longer makes an environment with a missing Scripts\python.exe appear usable.

Discovery semantics

Cache state Behavior
Valid sidecar, launcher, interpreter, ownership, and version Publish environment
Entry is locked or being built Preserve previous publication; retry
Filesystem/resolver state is temporarily unavailable Preserve previous publication; retry
Entry appears during the scan Schedule snapshot-aware follow-up
Missing or malformed sidecar Omit/remove from collection
Missing launcher/base interpreter Omit/remove from collection
Ownership or recorded-version mismatch Omit/remove from collection
Cache root is absent Publish empty collection

Example

extension activation
→ register InlineScriptEnvManager
→ defer one event-loop turn
→ scan <globalStorage>/script-envs-v1
→ validate sidecar + launcher + ownership + version
→ publish valid cached environments through getEnvironments('all')
→ retry only if the scan observed a lock, transient state, or snapshot change

This inventory does not associate an environment with a script. PR 7 owns persisted script associations, and PR 9 will use those associations for automatic per-file routing.

Tests

Coverage includes:

  • deferred feature-gated activation startup;
  • valid cache discovery and all-scope publication;
  • missing, malformed, unavailable, non-directory, and symlinked entries;
  • missing and non-regular Windows launchers;
  • lock preservation, including unavailable (EIO) lock probes;
  • canonical sysPrefix versus lexical cache-root identities;
  • add/remove event reconciliation;
  • concurrent refresh coalescing;
  • activation joining an explicit refresh;
  • cache entries created during a scan;
  • builds completing after the short retry window;
  • bounded retry exhaustion;
  • explicit single-pass refresh behavior; and
  • disposal during in-flight scans and pending retries.

Validation on the rebased branch:

  • npm run compile-tests
  • npm run compile
  • npm run lint
  • focused activation-discovery/cache-launcher/registration suites

The full Windows unit run reaches 1613 passing and 5 pending; the existing concurrent writeMetaJson rename test can still intermittently fail with EPERM on Windows. That writer is unchanged by this PR and the same failure is reproducible on main.

Performance

  • Activation is deferred and never waits for discovery.
  • Cache scans are coalesced.
  • Retries are bounded and stop after a stable pass.
  • Explicit refresh remains single-pass.
  • There is no persistent watcher, unbounded polling, or per-document work.

User impact

No default-path user impact. The manager and discovery remain behind the undeclared, default-off python-envs.inlineScripts.enabled flag.

With the internal flag manually enabled, valid cached inline environments become available through the manager after restart. This PR does not automatically select one for a script and introduces no public command, setting, picker item, project registration, cache deletion, TTL cleanup, or telemetry.

Scope and follow-up

This PR intentionally does not implement:

  • automatic PEP 723 script routing (PR 9);
  • exact script project registration (PR 10);
  • CodeLens or bulk setup UX (PRs 11-12);
  • cache clearing or TTL eviction (PRs 13-14); or
  • lifecycle telemetry (PR 15).

PR 7 (#1697) is merged, and this branch is rebased on current main. Automatic routing can follow independently after this manager-discovery layer lands.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3cb82ae9-7424-40a4-9156-8c54ac6e0895
Use stable cache identities, fail closed on lock probes, and retry snapshot changes safely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6b12d843-8011-4bfc-9ba9-f75761eadee2
@rchiodo

Rich Chiodo (rchiodo) commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🔒 Automated review in progress — Rich Chiodo (@rchiodo) is auto-reviewing this PR.

Comment thread src/managers/builtin/inlineScript/envManager.ts

@rchiodo Rich Chiodo (rchiodo) 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.

Approved via Review Center.

@rchiodo Rich Chiodo (rchiodo) added the review-auto:approved Automated review: no blocking findings (approval posted). label Aug 18, 2026
Comment thread src/managers/builtin/inlineScript/envManager.ts Outdated
Use async and await without changing refresh behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6b12d843-8011-4bfc-9ba9-f75761eadee2
Comment thread src/managers/builtin/inlineScript/envManager.ts
@rchiodo Rich Chiodo (rchiodo) added review-auto:changes-requested Automated review: posted blocking findings to address. and removed review-auto:approved Automated review: no blocking findings (approval posted). labels Aug 18, 2026
Avoid bounded retries when the cache remains definitively empty.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6b12d843-8011-4bfc-9ba9-f75761eadee2
@rchiodo Rich Chiodo (rchiodo) added review-auto:approved Automated review: no blocking findings (approval posted). and removed review-auto:changes-requested Automated review: posted blocking findings to address. labels Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-request Request for new features or functionality review-auto:approved Automated review: no blocking findings (approval posted).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants