Skip to content

Add inline script environment persistence (PEP 723 PR 7/16) - #1697

Open
Stella Huang (StellaHuang95) wants to merge 3 commits into
microsoft:mainfrom
StellaHuang95:pep723-pr7-persistence
Open

Add inline script environment persistence (PEP 723 PR 7/16)#1697
Stella Huang (StellaHuang95) wants to merge 3 commits into
microsoft:mainfrom
StellaHuang95:pep723-pr7-persistence

Conversation

@StellaHuang95

Copy link
Copy Markdown
Contributor

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

This replaces the earlier closed draft #1653 with the finalized implementation rebased on main.

Roadmap context

This is PR 7 of 16 in the PEP 723 inline-script roadmap. It adds durable per-script environment associations to the internal manager.

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 open (#1696)
PR 7: persistence (get / set + Memento) this PR
PR 8: activation-time discovery follow-up
PR 9: route PEP 723 scripts to the inline manager follow-up

Why this PR

PR 5 can build or reuse an inline-script environment, but the manager does not remember that the resulting environment belongs to a particular script. After an extension-host restart, the in-memory association is gone.

This PR implements the persistence portion of Q4 in the design:

  • maintain an independent environment association for each script;
  • persist script-to-environment executable paths in workspace Memento;
  • lazily and safely rehydrate those associations;
  • re-check current requires-python metadata before returning an environment;
  • report changes so the central environment API can update its last-known state.

What this PR does

Implements per-script set()

  • Accepts one or more local file: URIs and rejects invalid or mixed scopes atomically.
  • Validates that selected environments are owned inline-script cache entries.
  • Persists a normalized script path → environment executable path mapping under a dedicated Memento key.
  • Supports assigning and unassigning individual scripts or batches.
  • Updates in-memory state and emits onDidChangeEnvironment only for effective changes.
  • Leaves the existing create() behavior separate: creation alone does not implicitly establish a persisted association.

Implements per-script get()

  • Reads current PEP 723 metadata before returning an association.
  • Keeps unreadable or temporarily invalid script metadata from destructively clearing state.
  • Returns an in-memory association when valid.
  • Lazily reconstructs persisted environments after restart instead of resolving every script during activation.
  • Re-checks requires-python against the reconstructed Python version before returning it.

Safely rehydrates persisted associations

  • Requires an absolute executable path.
  • Preserves associations while their cache entry is locked or being created.
  • Verifies that the executable exists and is a regular file.
  • Resolves it into a PythonEnvironment and confirms that it belongs to the expected extension-owned cache entry.
  • Removes only definitively stale associations; transient filesystem or resolver failures remain retryable.
  • Emits a change event when a slow rehydration eventually succeeds, including when the public API's initial one-second wait has already elapsed.

Validates warm in-memory associations

  • Periodically revalidates cached associations without performing full resolution on every lookup.
  • Detects executables deleted while VS Code remains open.
  • Detects an environment rebuilt at the same cache path with a different Python version.
  • Preserves busy/locked entries instead of misclassifying them as stale.
  • Coalesces simultaneous validations for the same script.
  • Retains the existing environment object when resolution produces only a new generated ID for the same Python, avoiding false changes and duplicate ID-keyed resources.

Protects persistence and selection from races

  • Serializes Memento read-modify-write operations so concurrent script selections cannot lose one another.
  • Uses per-script association revisions so an older rehydration cannot overwrite a newer selection or unset.
  • Removes stale persisted values conditionally, only if the inspected path is still current.
  • Keeps failed persistence writes from changing in-memory state or emitting success-shaped events.
  • Does not globally serialize unrelated environment operations.

Updates central active-environment tracking

  • Keys inline-script selections by normalized script path rather than containing project, so two scripts in one workspace can retain different environments.
  • Uses per-scope revisions and manager identity checks so slow refreshes cannot overwrite newer selections.
  • Ensures failed selections and failed refreshes do not discard a valid in-flight refresh.
  • Groups same-manager batch unsets and calls the manager once with the complete URI array.
  • Updates central cache entries and events only after the manager operation succeeds.
  • Attributes inline-script change events to the script URI rather than the containing project URI.

Example

Given two scripts in the same workspace:

tools/report.py → Python 3.12 inline environment
tools/import.py → Python 3.13 inline environment

PR 7 stores and retrieves those associations independently. Selecting the environment for import.py does not overwrite the last-known environment for report.py.

After restart:

get(report.py)
→ read persisted executable
→ verify cache ownership and current metadata
→ resolve environment
→ cache and return it

If report.py later changes from requires-python = ">=3.11" to ">=3.13", its persisted Python 3.12 environment is no longer returned as compatible.

Persistence and failure semantics

Condition Behavior
Executable exists and cache ownership is valid Rehydrate and return
Cache entry is locked/in progress Preserve association; retry later
Resolver fails transiently Preserve association; retry later
Executable is definitively missing and unlocked Remove stale association and notify
A newer selection wins during rehydration Discard the stale result
Memento write fails Keep previous in-memory/persisted selection and propagate the error

Tests

Coverage includes:

  • assign, retrieve, unset, and batch persistence;
  • restart-time lazy rehydration and delayed success events;
  • metadata compatibility changes;
  • missing, malformed, unowned, busy, and transient cache states;
  • warm deletion and same-path rebuild detection;
  • concurrent persistence, rehydration, validation, selection, and unset races;
  • failed Memento writes;
  • strict URI-scope validation;
  • independent same-project script selections;
  • stale and failed central refresh ordering;
  • atomic same-manager batch unsets.

npm run compile-tests, npm run lint, the full unit suite, and the focused persistence/central-manager suites are clean.

Performance

  • Rehydration is lazy rather than activation-blocking.
  • Warm associations are cached and validation is throttled.
  • Same-script rehydration and validation work is coalesced.
  • Queues cover only shared persistence and mutation ordering; unrelated script reads and environment-manager operations remain independent.

User impact

No default-path user impact yet. This completes an internal Phase 2 manager capability. Automatic routing and user-facing entry points arrive in later roadmap PRs.

Once routing is wired, script-specific selections will survive extension-host restarts and remain independent even for multiple scripts in the same workspace.

Merge order

The core persistence behavior depends on the merged manager skeleton (#1610). This branch is rebased on current main; PR 8 and PR 9 build on this capability.

@StellaHuang95

Copy link
Copy Markdown
Contributor Author

🔒 Automated review in progress — Stella Huang (@StellaHuang95) is auto-reviewing this PR.

@StellaHuang95
Stella Huang (StellaHuang95) force-pushed the pep723-pr7-persistence branch 2 times, most recently from 2719448 to dbb5c72 Compare August 10, 2026 18:55
@eleanorjboyd

Copy link
Copy Markdown
Member

AI agent review findings:

  1. Selecting an inline-script environment currently uses the normal project settings persistence path, which can save inline-script as the containing project's default manager. Subsequent ordinary-file and unassociated-script lookups then route to InlineScriptEnvManager and can return no environment. Conversely, loose-script associations have no persisted manager routing and become unreachable after restart. This routing needs to remain per-script rather than changing the project-wide manager setting.

  2. validateCachedAssociation() captures the association revision only after awaiting the busy and filesystem checks, while still validating the earlier cached environment. An explicit selection during either await can therefore update the revision, after which the stale cached result is treated as current and may overwrite the newer selection. Please capture the revision with the original cached read, before any await, and add a regression test for that interleaving.

I validated the PR with npm run compile-tests, npm run compile, and the focused inline-script/last-known-environment tests (78 passing, 1 pending).

Reviewed and posted by Eleanor's AI agent.

@StellaHuang95

Copy link
Copy Markdown
Contributor Author

AI agent review findings:

  1. Selecting an inline-script environment currently uses the normal project settings persistence path, which can save inline-script as the containing project's default manager. Subsequent ordinary-file and unassociated-script lookups then route to InlineScriptEnvManager and can return no environment. Conversely, loose-script associations have no persisted manager routing and become unreachable after restart. This routing needs to remain per-script rather than changing the project-wide manager setting.
  2. validateCachedAssociation() captures the association revision only after awaiting the busy and filesystem checks, while still validating the earlier cached environment. An explicit selection during either await can therefore update the revision, after which the stale cached result is treated as current and may overwrite the newer selection. Please capture the revision with the original cached read, before any await, and add a regression test for that interleaving.

I validated the PR with npm run compile-tests, npm run compile, and the focused inline-script/last-known-environment tests (78 passing, 1 pending).

Reviewed and posted by Eleanor's AI agent.

Thanks Eleanor Boyd (@eleanorjboyd), both findings are valid.

  1. Per-script routing/settings:  pm.get(scriptUri)  can currently return the containing project, causing  setEnvironment()  to persist  inline-script  as that whole project’s manager. I’ll update both single and batch selection paths so an inline-script manager setting is persisted only when the resolved project URI exactly matches the script URI. Otherwise, only the inline manager’s per-script Memento association will be updated.
    The loose-script restart-routing gap is real, but the roadmap intentionally separates that work: PR7 persists and rehydrates the association inside the manager, PR9 routes known scripts to that manager, and PR10 registers scripts as individual projects so settings can be persisted per script. I’ll prevent the incorrect project-wide write in this PR without pulling all PR9/PR10 routing into it.
  2. Cached-association revision: Agreed. The revision is captured too late, after asynchronous lock/filesystem checks, so validation of an old cached environment can accidentally adopt a newer selection’s revision. I’ll capture the revision together with the cached environment before the first  await , pass it through validation and stale cleanup, and add a regression test where a new selection occurs while validation is paused.

@eleanorjboyd

Copy link
Copy Markdown
Member

will give a thumbs up to after the prior one merges as I assume that will create merge conflicts

Persist and safely rehydrate per-script environment associations. Harden selection races, cache-lock handling, corrupt-state repair, scope validation, and central batch selection consistency without globally serializing environment operations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1a9f6ba1-9bd3-4664-bc25-a0d34d7a2e91
Keep inline-script manager settings scoped to exact script projects and prevent stale warm validation from superseding a newer selection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1a9f6ba1-9bd3-4664-bc25-a0d34d7a2e91
Keep exact script-project settings authoritative while routing active inline selections ahead of containing-project defaults, and retain strict PEP 440 validation for persisted associations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1a9f6ba1-9bd3-4664-bc25-a0d34d7a2e91
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants