You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
> 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
```text
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.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3cb82ae9-7424-40a4-9156-8c54ac6e0895
Copilot-Session: 6b12d843-8011-4bfc-9ba9-f75761eadee2
0 commit comments