Skip to content

Add clear script environment cache command (PEP 723 PR 13/16) - #1724

Merged
Stella Huang (StellaHuang95) merged 6 commits into
microsoft:mainfrom
StellaHuang95:pep723-pr13-clear-cache-v2
Aug 19, 2026
Merged

Add clear script environment cache command (PEP 723 PR 13/16)#1724
Stella Huang (StellaHuang95) merged 6 commits into
microsoft:mainfrom
StellaHuang95:pep723-pr13-clear-cache-v2

Conversation

@StellaHuang95

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 13 of 16 in the PEP 723 inline-script roadmap. It adds the explicit, user-confirmed cache lifecycle operation that the later TTL work will reuse.

Phase 5: Lifecycle and polish PR Status
PR 7: persistence (get / set + Memento) merged (#1697)
PR 13: clear inline-script cache this PR
PR 14: opportunistic 14-day TTL eviction follow-up
PR 15: lifecycle telemetry separate
PR 16: status-bar decision resolved; no code PR

Why this PR

The extension can create extension-owned inline-script environments and persist script associations, but it has no complete way to remove that state. Clearing only the files would leave Memento associations and pythonProjects entries pointing at deleted interpreters; clearing only settings would leave disk usage behind.

This PR adds one coordinated lifecycle operation covering:

  • extension-owned cache entries;
  • persisted and in-memory script associations;
  • active selection events; and
  • generated inline-script project settings.

Because this is destructive and the cache is shared by extension-host processes, the implementation is intentionally fail-closed around path ownership and locks.

What this PR does

Adds an internal, confirmation-gated clear command

  • Registers python-envs.clearScriptEnvCache only while the hidden inline-script feature flag is enabled.
  • Does not contribute the command to package.json or the Command Palette before rollout.
  • Shows a modal warning covering cached environments, associations, and project entries.
  • Cancelling the prompt performs no filesystem, state, or settings changes.
  • Runs cache cleanup before settings removal, so failed/partial cache cleanup does not silently rewrite project configuration.

Keeps generic cache clearing behavior safe

  • The existing public python-envs.clearCache command continues to clear existing non-inline managers.
  • It skips the preview inline manager because the generic path has no inline-specific confirmation or project-settings lifecycle.
  • The dedicated command invokes the inline manager directly and performs the complete cleanup transaction.

Serializes in-process maintenance

  • Adds a manager-local maintenance queue and barrier.
  • create(), get(), and set() cannot observe or mutate half-cleared state.
  • A clear request refuses to begin when creation already started.
  • A creation request that arrives after clear begins waits for maintenance to settle.
  • Multiple maintenance requests are serialized without globally serializing unrelated managers.

Coordinates deletion across extension hosts

  • Acquires and holds each cache entry's cross-process lock through deletion.
  • Classifies locks as missing, held, retained, stale, orphaned, malformed, or unavailable.
  • Uses PID liveness to distinguish a live owner from a stale owner.
  • Makes retained markers generation-specific by preserving the owner's PID/nonce.
  • Reclaims only the exact stale/retained generation marker that was inspected.
  • If another process replaces that generation before the atomic claim, reclamation loses safely and touches nothing.
  • Ambiguous legacy fixed retained markers remain recognizable but are conservatively not reclaimed.

Validates every destructive path

Before deleting an entry, cleanup verifies that:

  • global storage and script-envs-v1 are normal directories rather than symlinks/junctions;
  • the versioned cache root is the expected direct child of global storage;
  • neither path is a filesystem root or dangerously shallow;
  • physical realpath containment matches the expected ownership boundary;
  • the cache root has not changed since the cleanup snapshot; and
  • the target entry is a normal direct-child directory inside that same physical root.

The entry lock is acquired first, then root and entry ownership are revalidated immediately before removal.

Keeps state consistent through partial failures

  • Attempts cache entries independently and records successful removals.
  • Aggregates and surfaces deletion/persistence failures rather than returning success-shaped output.
  • Invalidates only associations whose environment was removed or is definitively missing.
  • Preserves associations for cache entries that could not safely be removed.
  • Cancels pending rehydration for invalidated scripts.
  • Clears warm environment and validation caches.
  • Advances association revisions so stale async work cannot restore removed selections.
  • Emits onDidChangeEnvironment only for selections actually invalidated.

Removes generated inline project settings safely

  • Resolves pythonProjects entries independently from global, workspace, and workspace-folder sources.
  • Removes only entries whose manager is the inline-script manager.
  • Preserves non-inline duplicate entries and higher-precedence overrides.
  • Handles same relative paths across multiple workspace roots.
  • Aggregates global/workspace updates so each shared scope is written once.
  • Unloads only loaded projects that have no remaining configuration source.

Cleanup semantics

Condition Behavior
Entry is unlocked and physically owned Lock, revalidate, delete
Lock belongs to a live process Refuse that deletion
Exact stale/retained generation can be claimed Reclaim, acquire a fresh lock, delete
Lock is unavailable, malformed, orphaned, or legacy-ambiguous Preserve entry and surface failure
Root or entry is redirected/outside ownership boundary Refuse deletion
One entry fails after another was removed Preserve valid survivors; invalidate removed associations; report aggregate failure
Persistence update fails after disk cleanup Keep in-memory state consistent and surface the persistence error
Cache is already absent Clear stale associations safely; remain idempotent

Example

Clear Script Environment Cache
→ modal confirmation
→ enter manager maintenance barrier
→ verify physical cache root
→ acquire exact per-entry lock
→ revalidate ownership immediately before deletion
→ delete safe entries
→ reconcile Memento + in-memory selections + events
→ remove generated inline pythonProjects settings

Tests

Coverage includes:

  • prompt cancellation and command ordering;
  • generic clear behavior with the preview manager absent/present;
  • in-process create/clear ordering;
  • live, stale, retained, orphaned, malformed, unavailable, and legacy lock states;
  • exact-generation reclamation and delayed-reclaimer/new-creator races;
  • holding entry locks through deletion;
  • unsafe, shallow, redirected, symlinked, and root-swapped paths;
  • successful, missing-cache, idempotent, and partial-failure cleanup;
  • persistence failures and pending-rehydration races;
  • global/workspace/workspace-folder setting precedence;
  • multi-root projects and same-path entries; and
  • default-off command registration.

Validation on the rebased branch:

  • npm run compile-tests
  • npm run compile
  • npm run lint
  • focused lock/cache-clear/settings/command suites: 38 passing

The full Windows unit run reaches 1638 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

  • No activation scan, timer, or background maintenance is added.
  • All work is initiated by the internal clear command.
  • Per-entry locks avoid globally serializing independent environment creation across extension hosts.
  • The maintenance barrier exists only inside the enabled inline manager and is active only during cleanup.

User impact

No default-path user impact. The manager and command remain behind the undeclared, default-off python-envs.inlineScripts.enabled flag, and the command is not publicly contributed.

When the internal flag is manually enabled, the existing generic cache command still behaves as before for non-inline managers. Inline cleanup is available only through the dedicated confirmed lifecycle.

Scope and follow-up

This PR intentionally does not implement:

  • automatic routing or project registration;
  • activation-time discovery;
  • silent/opportunistic deletion;
  • TTL expiration; or
  • lifecycle telemetry.

PR 14 will reuse this safety and state-cleanup foundation to remove entries whose lastUsedAt exceeds the planned 14-day TTL.

@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/features/settings/settingHelpers.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/features/settings/settingHelpers.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 19, 2026
Comment thread src/managers/builtin/inlineScript/envManager.ts
}

function getResolvedPythonProjectSettings(
workspaceFolder: WorkspaceFolder,

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.

Info · Optional note

Workspace-name and relative-path resolution is duplicated between these helpers, creating a maintainability risk if project-setting identity rules evolve. Consolidate this behind one canonical resolver.

Comment thread src/features/envManagers.ts
@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 19, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3cb82ae9-7424-40a4-9156-8c54ac6e0895
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3cb82ae9-7424-40a4-9156-8c54ac6e0895
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3cb82ae9-7424-40a4-9156-8c54ac6e0895
Coordinate per-entry deletion locks, keep partial failures consistent, and clean inline project settings safely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6b12d843-8011-4bfc-9ba9-f75761eadee2
Claim exact stale or retained lock markers before inline cache cleanup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6b12d843-8011-4bfc-9ba9-f75761eadee2
Handle user-scope project cleanup independently of open workspace folders.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approved via Review Center.

@StellaHuang95
Stella Huang (StellaHuang95) merged commit fa47921 into microsoft:main Aug 19, 2026
43 of 44 checks passed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Post-merge review: three concurrency/project-model findings from the clear-cache implementation.

return undefined;
}
if (
normalizePath(currentPhysicalCacheRootPath) !== normalizePath(originalPhysicalCacheRootPath)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

High — guard against same-path cache-root replacement\n\nThis revalidation compares only canonical path strings. If another process removes and recreates the cache root at the same pathname after the initial enumeration, the comparison still succeeds, and cleanup can lock and delete an entry from the replacement root generation. Path containment protects where deletion happens, but not which filesystem generation owns that path. Please coordinate cleanup and creation with a root-generation lock/identity token and revalidate that identity immediately before recursive deletion.

`.reclaim-${process.pid}-${crypto.randomBytes(16).toString('hex')}-${snapshot.marker}`,
);
try {
await fsapi.rename(path.join(lockPath, snapshot.marker), claimedMarker);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Medium — make interrupted reclaim markers recoverable\n\nAfter this rename, a crash—or an error during the following unlink/rmdir—can leave .reclaim-* behind. inspectFileLockSnapshot() treats that name as unknown, so every later inspection reports malformed and the cache entry can no longer be reclaimed automatically. Please either model reclaim markers as a recognized generation with PID/liveness recovery, or restore the original observed marker when reclamation does not complete.

existingSettings.some((projectSetting) => matchesProjectSettingEdit(projectSetting, edit, workspaceFolder)) &&
!hasProjectSetting(remainingSettings, edit.project, workspaceFolder),
).forEach((edit) => {
removedProjects.set(edit.project.uri.toString(), edit.project);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Medium — do not remove an intrinsic workspace-root project\n\nAn inline-script override such as { path: ".", envManager: "ms-python.python:inline-script" } resolves to the workspace root. Once the override is removed, this code adds that URI to removedProjects; the command then calls wm.remove(), even though workspace roots are intrinsic projects created independently of settings. This temporarily drops the workspace root from the in-memory project model until a later refresh. Please exclude workspace-folder roots from loadedProjectsToRemove (or recompute removal from the effective project set after edits).

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.

4 participants