Skip to content

Keep locally provisioned sessions out of the list until they are real - #332457

Merged
Osvaldo Ortega (osortega) merged 4 commits into
mainfrom
osortega/sessions-list-provisional-sessions
Aug 25, 2026
Merged

Keep locally provisioned sessions out of the list until they are real#332457
Osvaldo Ortega (osortega) merged 4 commits into
mainfrom
osortega/sessions-list-provisional-sessions

Conversation

@osortega

Copy link
Copy Markdown
Contributor

Two list bugs with the same root: a session that exists remotely before the host knows about it.

1. The row appeared twice for ~8 seconds

A provider that creates a session remotely seeds it into its cache before connecting, so a later discovery pass reconciles against it instead of creating a second entry. But the caller is still showing an optimistic placeholder row for that same session until the first turn is dispatched — so publishing the seed straight away listed the session twice while the environment woke.

Measured on a real run: seed at 19.658, placeholder retired at 27.615.

Retiring the placeholder earlier is not an option — swapping when the session merely exists hands the UI a session it cannot open yet and the view falls back to the new-session screen. That was tried and reverted.

So the seed is withheld from getSessions instead, and revealed as the placeholder is retired:

  • The reveal is silent. The list re-reads on any change, so a single event swaps both rows. Announcing separately would trade an 8-second overlap for a one-frame one.
  • Withheld is not unreachable. getSessionByResource reads the cache directly, so opening still resolves — hidden from the list, not from the app.
  • A failed first turn still reveals it, since the session outlives the failure and would otherwise be invisible.

2. The row vanished, the view went home, then it came back

Connecting triggers a reconciliation that prunes anything cached but not listed by the host. But the session id is minted before the host materializes the session, so that first listing can legitimately omit it. The prune landed ~400ms after the placeholder swap, deleting the row the user was looking at:

02.735  host: "not found: ahp-session:/355591eb…"
02.742  swap to the committed session
03.131  committed session removed → back to the new-session screen

Sessions seeded as provisional now resist that eviction until the host lists them. The base provider already made this exemption for a sibling case ("some hosts briefly omit the just-sent eager session"), so that is generalized into an overridable hook rather than adding a parallel mechanism.

The protection is bounded (2 min). Without a deadline, a session the host will never list becomes a permanent row that only a reload clears — trading a visible flash for an invisible one. Both directions are tested.

Risk

_isSessionEvictable defaults to true and _onHostListedSessions is empty, so every provider that does not opt in behaves exactly as before. Only sessions seeded with provisional take the new paths.

Validation

  • npm run typecheck-client, npm run valid-layers-check
  • ./scripts/test.sh --glob "**/vs/sessions/**/*.test.js" — 2469 passing
  • The eviction guard was verified by deleting it and confirming the new test fails, then restoring it — a test that passes for the wrong reason is worse than no test.

Copilot AI balanced review requested due to automatic review settings August 25, 2026 00:09

Copilot AI 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.

Pull request overview

Prevents locally provisioned sandbox sessions from duplicating or disappearing before the host recognizes them.

Changes:

  • Withholds provisional sessions from listings while keeping them resolvable.
  • Adds bounded eviction protection and host-list reconciliation hooks.
  • Publishes sessions when placeholders retire, including failed-send handling.
Show a summary per file
File Description
baseAgentHostSessionsProvider.ts Adds overridable eviction hooks.
remoteAgentHostSessionsProvider.ts Tracks withheld and provisional sessions.
cloudSandboxAgentHostContribution.ts Seeds provisioned sessions provisionally.
copilotChatSessionsProvider.ts Publishes sessions during placeholder retirement.
remoteAgentHostSessionsProvider.test.ts Tests listing and eviction behavior.
cloudSandboxAgentHostContribution.test.ts Updates provisioning test provider.
copilotChatSessionsProvider.test.ts Tests successful and failed publication.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Balanced

A provider that creates a session remotely has to seed it into its cache
before connecting, so a later discovery pass reconciles against it instead of
creating a second entry. But the caller is still showing an optimistic
placeholder row for that same session until the first turn is dispatched, so
publishing the seed straight away listed the session twice for as long as the
environment took to wake (~8s observed).

Retiring the placeholder earlier is not an option: swapping when the session
merely exists hands the UI a session it cannot open yet and the view falls
back to the new-session screen. So the seed is now withheld from
`getSessions` instead, and revealed as the placeholder is retired. The reveal
is silent because the list re-reads on any change, letting a single event
swap both rows rather than listing both for a frame. Withheld sessions stay
reachable by resource, so opening one still works.

Separately, such a session could be evicted before it existed on the host.
Connecting triggers a reconciliation that prunes anything cached but not
listed, and the session id is minted before the host materializes the
session, so that first listing can legitimately omit it. Evicting there
dropped the row the user was looking at and bounced the view to the
new-session screen, re-adding the session seconds later.

Sessions seeded as provisional now resist that eviction until the host lists
them. The base provider already made this exemption for a sibling case, so
that is generalized into an overridable hook rather than adding a parallel
mechanism. The protection is bounded: without a deadline, a session the host
will never list becomes a permanent row that only a reload clears, trading a
visible flash for an invisible one.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two ways the provisional guard could leave a session worse off than no guard
at all.

A failure after the seed — a rejected connect, or teardown while the sandbox
wakes — returned before the caller received a provider, so nothing could
publish the seed. Discovery does not rescue it either: a later pass finds the
entry already in the cache and only backfills its project. The session
existed remotely and was invisible until reload. It is now published on the
way out, while the provider is still the registered one.

The grace period also started at seed time, before connecting. Waking a
sandbox can take minutes, so a slow wake burned the whole period before the
host had said anything, and the first listing that omitted the session met an
already-expired deadline — evicting it immediately and recreating the
disappearance the guard exists to prevent. The clock now starts when a
connected host first omits the session, which is what the grace is actually
measuring.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@osortega
Osvaldo Ortega (osortega) force-pushed the osortega/sessions-list-provisional-sessions branch from bd4be98 to 0b715a0 Compare August 25, 2026 02:53
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The withheld/provisional handling lived on RemoteAgentHostSessionsProvider,
which SSH and tunnel connections also use, even though only the cloud sandbox
calls any of it. Those connections carried ~95 lines of state and API that
mean nothing to them.

It now lives in CloudSandboxSessionsProvider, wired through the contribution's
existing provider-construction seam, and RemoteAgentHostSessionsProvider is
byte-identical to before this change.

What stays in the base provider is the pair of eviction hooks, which default
to "evictable" and a no-op. Eviction happens inside `_refreshSessions`, so a
subclass can only influence it from there, and the base already made this
exact exemption for its own pending session.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@osortega
Osvaldo Ortega (osortega) marked this pull request as ready for review August 25, 2026 04:07
@osortega
Osvaldo Ortega (osortega) enabled auto-merge (squash) August 25, 2026 04:09
@osortega
Osvaldo Ortega (osortega) merged commit 7c7068e into main Aug 25, 2026
27 checks passed
@osortega
Osvaldo Ortega (osortega) deleted the osortega/sessions-list-provisional-sessions branch August 25, 2026 07:52
@vs-code-engineering vs-code-engineering Bot added this to the 1.136.0 milestone Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants