Skip to content

ggsql runtime sessions do not restore after a window reload in Positron #506

Description

@juliasilge

After Developer: Reload Window in Positron, the ggsql console session does not come back. R and Python sessions in the same window reconnect normally.

The cause is the extension identity that Positron records for the ggsql runtime. The extension gets the Positron API through a global accessor, and Positron cannot attribute that call to an extension. Positron therefore records the runtime under nullExtensionDescription. The reconnect path uses this identity to activate the owning extension, so the reconnect fails.

Steps to reproduce

  1. Open a workspace in Positron.
  2. Start a ggsql console session.
  3. Run Developer: Reload Window.
  4. Look at the Console pane.

Expected: the ggsql session reconnects, in the same way as R and Python.

Actual: the ggsql session does not come back.

Evidence

Positron stores the affiliated runtime metadata for each workspace in state.vscdb, under ~/Library/Application Support/Positron/User/workspaceStorage/<workspace-hash>/ on macOS. The ggsql entry names no extension:

// positron.affiliatedRuntimeMetadata.v2.ggsql
{
  "extensionId": { "value": "nullExtensionDescription", "_lower": "nullextensiondescription" },
  "runtimeId": "ggsql-1c7963a6d5c5",
  "runtimeName": "ggsql (Jupyter)",
  "runtimeVersion": "0.4.1",
  "languageId": "ggsql",
  "startupBehavior": "explicit",
  "sessionLocation": "workspace"
}

The R entry in the same workspace names the correct extension:

// positron.affiliatedRuntimeMetadata.v2.r
{
  "extensionId": { "value": "positron.positron-r", "_lower": "positron.positron-r" },
  "runtimeId": "003663c34c4e2f88a5145a4c13757481",
  "runtimeName": "R 4.6.0",
  "languageId": "r",
  "startupBehavior": "implicit",
  "sessionLocation": "workspace",
  "cacheable": true
}

Root cause

src/extension.ts calls tryAcquirePositronApi() from @posit-dev/positron. That helper calls globalThis.acquirePositronApi(). Positron defines this global in src/bootstrap-esm.ts as createRequire(import.meta.url) and then require('positron').

The require parent is therefore the Positron bootstrap file, not the ggsql extension. The require interceptor in src/vs/workbench/api/common/extHostRequireInterceptor.ts looks up the parent path in its extension map and finds nothing. It logs Could not identify extension for 'positron' require call from ... and returns an API object bound to nullExtensionDescription. Every runtime that GgsqlRuntimeManager yields carries that identity.

R and Python use a plain import * as positron from 'positron' inside their own source files, which the interceptor attributes correctly.

Why this breaks the reload path

restoreWorkspaceSessions in src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts activates the owning extension before it reconnects:

await this.activateExtension(
    session.runtimeMetadata.extensionId,
    session.runtimeMetadata.languageId);

For ggsql this argument is nullExtensionDescription, so activateById fails. The error is caught and logged at debug level, and the ggsql extension is never activated on this path.

Validation and restore then call runtimeManagerForRuntime(metadata, wait: true) in src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts. This function waits 10 seconds for a runtime manager that only registers when onStartupFinished fires. doRestoreRuntimeSession in src/vs/workbench/services/runtimeSession/common/runtimeSession.ts also throws at once when no session manager is registered. The session is dropped.

Ruled out

  • The kernel survives the reload. The supervisor only tears down sessions on desktop Quit, not on Reload (extensions/positron-supervisor/src/extension.ts). A live ggsql-jupyter process and its kcserver are both present after a reload.
  • sessionLocation: 'workspace' and startupBehavior: 'explicit' are both correct. R and Python also use workspace by default, and only manual blocks affiliated startup.
  • runtimeId is stable across reloads, because it is a hash of the kernel path.

Proposed fix

Get the Positron API inside a source file of the ggsql extension, so the require interceptor can attribute the call:

let positronApi: typeof import('@posit-dev/positron') | undefined;
try {
    positronApi = require('positron');
} catch {
    // Running in VS Code, where the module does not exist.
}

Add 'positron' to the external array in esbuild.js, which today lists only 'vscode'.

Other gaps found during the investigation

  1. package.json declares no extensionDependencies. Both positron-r and positron-python declare positron.positron-supervisor. ggsql activates the supervisor ad hoc inside createSession and restoreSession. We can't do this, because the extension has to work in VS Code as well.
  2. restoreSession in src/manager.ts awaits supervisorApi.restoreSession(...) before it returns. RSession returns at once and defers the supervisor call to start(). Any transient supervisor error currently rejects the whole restore.
  3. restoreSession discards the sessionName argument that Positron passes, and hardcodes sessionName: 'ggsql'. A renamed session loses its name after a reload.
  4. The runtime metadata does not set cacheable: true, so ggsql is excluded from the runtime discovery cache that R and Python use for warm starts.
  5. GgsqlRuntimeManager._sessions is written but never read.

Metadata

Metadata

Assignees

No one assigned

    Labels

    positron-sqlSQL integration in Positron

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions