Skip to content

chore(runtime-core): Add deepnote runtime-core dependency, and use it instead of existing implementation#355

Open
tkislan wants to merge 15 commits intomainfrom
tk/deepnote-runtime-core
Open

chore(runtime-core): Add deepnote runtime-core dependency, and use it instead of existing implementation#355
tkislan wants to merge 15 commits intomainfrom
tk/deepnote-runtime-core

Conversation

@tkislan
Copy link
Contributor

@tkislan tkislan commented Mar 10, 2026

Summary by CodeRabbit

  • Chores

    • Added a runtime dependency to enable a runtime-driven server lifecycle.
  • Refactor

    • Moved server lifecycle to a runtime-managed flow for more reliable startup, readiness checks, and shutdown.
    • Improved virtual environment interpreter resolution and preserved SQL integration environment handling.
    • Streamlined startup, output monitoring, concurrency handling, and orphan cleanup for greater stability.
  • Tests

    • Converted integration-style tests to unit-focused suites and added a mock child-process helper for deterministic testing.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4609c82e-b89f-4c64-aadf-86aca77ba029

📥 Commits

Reviewing files that changed from the base of the PR and between 4f0248d and d8f73e2.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • package.json

📝 Walkthrough

Walkthrough

Migrates Deepnote server lifecycle to @deepnote/runtime-core: start/stop are delegated to runtime-core and DeepnoteServerInfo now extends runtime-core's ServerInfo. startServer gained an additionalPackages parameter and stopServer was added/updated. Constructor removed an IHttpClient dependency. Toolkit installer now uses resolvePythonExecutable. Tests converted from port-allocation integration tests to unit tests, added createMockChildProcess test helper, and updated tests to include mocked process fields. package.json adds @deepnote/runtime-core.

Sequence Diagram(s)

sequenceDiagram
  participant Extension as "VS Code Extension\n(DeepnoteServerStarter)"
  participant Runtime as "runtime-core\n(startServer / stopServer)"
  participant SQL as "SQL Env Provider\n(optional)"
  participant Output as "VS Code OutputChannel"
  participant Network as "Server URL / Health"

  Extension->>SQL: request SQL env vars (optional)
  SQL-->>Extension: sqlEnvVars / cancelled
  Extension->>Runtime: startServer({interpreter, venvPath, additionalPackages, env: sqlEnvVars})
  Runtime-->>Extension: ServerInfo (url, ports, id, processRef)
  Extension->>Output: monitorServerOutput(processRef)
  Extension->>Network: probe ServerInfo.url for health
  Network-->>Extension: healthy/unhealthy
  Extension->>Runtime: stopServer(ServerInfo.id)
  Runtime-->>Extension: stopped
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Updates Docs ⚠️ Warning PR introduces major architectural changes and public API modifications without updating documentation files. Update README.md, CONTRIBUTING.md, and specs/ to document new @deepnote/runtime-core dependency, startServer signature changes, lifecycle management updates, and CHANGELOG entries.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately describes the main change: adding @deepnote/runtime-core dependency and refactoring to use it instead of the existing in-process server implementation across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts`:
- Around line 258-259: reserveStartPort(serverKey) currently only serializes
maxPort computation and releases the lock before server startup, allowing
duplicate base-port assignments; modify the flow so the reservation is recorded
and held until startup completes (or explicitly tracked and released) — e.g.,
have reserveStartPort return and persist a concrete reservation object/port for
serverKey and only release or remove that reservation after
startServer/launchServer (or the function that calls reserveStartPort) finishes
or fails, and ensure the locking in the maxPort calculation (the block around
calculateMaxPort/getMaxPort) still wraps reservation creation to prevent races;
update related callers (reserveStartPort, startServer/launchServer, and any
unlock/release logic around lines referenced) to use the reservation lifecycle
instead of releasing the lock immediately.
- Around line 298-299: The output listeners are being stored under serverKey
while stopServerForEnvironment looks them up by fileKey, causing leaked
listeners; update the monitorServerOutput path (where disposables are set at
lines 442-445) to store the disposables using the same key
stopServerForEnvironment expects (fileKey) or alternatively change
stopServerForEnvironment to use serverKey—make the key consistent between
monitorServerOutput (and where disposables are assigned) and
stopServerForEnvironment so listeners are removed correctly; reference
monitorServerOutput, stopServerForEnvironment, serverKey, fileKey and the
disposables map when making the change.
- Around line 266-277: The cancellation path is lost between
gatherSqlIntegrationEnvVars(...) and startServer(...): update
gatherSqlIntegrationEnvVars to propagate/throw an OperationCanceled (or the same
cancellation) instead of swallowing it, then after startServer(...) check for
cancellation (e.g. via the same token or a isCancelled check) and if canceled,
call the runtime shutdown/teardown routine on the returned RuntimeCoreServerInfo
(or call the server stop/close function provided by startServer) before
re-throwing the cancellation; also ensure the earlier provider-failure fallback
that returns {} (from the SQL provider logic) does not hide cancellations by
converting cancellation errors into non-cancel errors so that cancellation
bubbles up consistently.
- Around line 481-485: The code is passing wrapper objects from
pendingOperations to Promise.race instead of their promises, so change the await
to use each wrapper's promise: map pendingOps to op.promise (or a safe fallback
if missing) when creating the Promise.race with
sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS) so the actual async operations tracked in
pendingOperations are awaited before dispose clears state; locate
pendingOperations/pendingOps and the Promise.race call to update it.

In `@src/kernels/deepnote/deepnoteServerStarter.unit.test.ts`:
- Around line 85-99: The test "should serialize concurrent calls" currently
allows duplicate ports and won't catch races in reserveStartPort; update the
test to assert that concurrent calls to
reserveStartPort('key-1'/'key-2'/'key-3') return distinct/non-overlapping port
numbers (or replace the test to call the public startup flow that uses
reserveStartPort) so duplicate reservations fail the test; locate
reserveStartPort in deepnoteServerStarter and change assertions in the unit test
to enforce uniqueness of port1, port2, port3 (or exercise the public start
method) to surface concurrency issues.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6b086b66-7c45-4d48-87d4-78e3a43b7520

📥 Commits

Reviewing files that changed from the base of the PR and between 5148b03 and d5f67f6.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • package.json
  • src/kernels/deepnote/deepnoteServerStarter.node.ts
  • src/kernels/deepnote/deepnoteServerStarter.unit.test.ts
  • src/kernels/deepnote/deepnoteToolkitInstaller.node.ts
  • src/kernels/deepnote/types.ts

Base automatically changed from tk/update-deepnote-database-integrations to main March 16, 2026 13:59
@codecov
Copy link

codecov bot commented Mar 16, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0%. Comparing base (4cffc41) to head (4f0248d).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@     Coverage Diff     @@
##   main   #355   +/-   ##
===========================
===========================
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ss helper

- Introduced `createMockChildProcess` in `deepnoteTestHelpers.ts` for consistent mock process creation in tests.
- Updated `DeepnoteLspClientManager` and `DeepnoteServerStarter` to include the mock process in server info.
- Removed unnecessary `runtimeCoreServerInfo` from `ProjectContext` and adjusted related logic to use the new `serverInfo` structure.
- Ensured all relevant tests are updated to reflect these changes, improving test reliability and maintainability.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/kernels/deepnote/deepnoteServerStarter.node.ts (2)

474-474: ⚠️ Potential issue | 🔴 Critical

Promise.race receives wrapper objects, not promises.

pendingOps contains { type, promise } objects. This immediately resolves with the wrapper. Use op.promise.

-await Promise.allSettled(pendingOps.map((op) => Promise.race([op, sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)])));
+await Promise.allSettled(pendingOps.map((op) => Promise.race([op.promise, sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)])));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` at line 474, The current
shutdown waiter passes the wrapper objects from pendingOps into Promise.race,
causing immediate resolution; update the Promise.race call inside the
Promise.allSettled to use each wrapper's promise property (i.e., change
pendingOps.map((op) => Promise.race([op, sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)]))
to use op.promise) so that Promise.race races the actual operation promise
against sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS).

432-435: ⚠️ Potential issue | 🟠 Major

Disposables keyed by serverKey but cleanup uses fileKey.

Line 342 looks up disposables by fileKey, but line 435 stores them under serverKey. Listeners will leak on notebook close.

-    private monitorServerOutput(serverKey: string, serverInfo: DeepnoteServerInfo): void {
+    private monitorServerOutput(fileKey: string, serverInfo: DeepnoteServerInfo): void {
         const proc = serverInfo.process;
         const disposables: IDisposable[] = [];
-        this.disposablesByFile.set(serverKey, disposables);
+        this.disposablesByFile.set(fileKey, disposables);

Update the call site at line 290 to pass fileKey instead of serverKey.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 432 - 435,
The disposables are stored in disposablesByFile using serverKey in
monitorServerOutput (method monitorServerOutput and property disposablesByFile)
but cleanup looks them up by fileKey, causing listener leaks; update the caller
that invokes monitorServerOutput (the call passing serverKey) to pass the
notebook's fileKey instead of serverKey so disposables are indexed by the same
fileKey used for cleanup, and verify references to serverKey/fileKey across
monitorServerOutput, disposablesByFile, and the cleanup lookup are consistent.
src/kernels/deepnote/deepnoteServerStarter.unit.test.ts (1)

90-104: 🧹 Nitpick | 🔵 Trivial

Test doesn't verify distinct port reservations.

All three calls return the same port since no server info is stored between reservations. Consider asserting uniqueness or testing the full startup flow to catch races.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.unit.test.ts` around lines 90 -
104, The test currently calls the private method reserveStartPort concurrently
and only checks that returned values are numbers, but it doesn't catch the race
that returns the same port; update the test to verify uniqueness (e.g., collect
[port1, port2, port3] into a Set and assert its size is 3) or, better, exercise
the full startup flow that records server info (use serverStarter.startServer or
the public method that persists server metadata) so concurrent reservations
produce distinct ports; locate the private helper reserveStartPort via
getPrivateMethod(serverStarter, 'reserveStartPort') and replace the final
assertions with a uniqueness assertion or a full-start integration assertion to
ensure distinct port allocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/kernels/deepnote/deepnoteTestHelpers.ts`:
- Line 1: Replace the Node-prefixed import for the ChildProcess type with the
standard module path: change the import statement that currently references
'node:child_process' and the type symbol ChildProcess to import from
'child_process' instead, ensuring all usages of the ChildProcess type in this
file (e.g., in deepnoteTestHelpers.ts) remain the same.

---

Duplicate comments:
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts`:
- Line 474: The current shutdown waiter passes the wrapper objects from
pendingOps into Promise.race, causing immediate resolution; update the
Promise.race call inside the Promise.allSettled to use each wrapper's promise
property (i.e., change pendingOps.map((op) => Promise.race([op,
sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)])) to use op.promise) so that Promise.race
races the actual operation promise against sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS).
- Around line 432-435: The disposables are stored in disposablesByFile using
serverKey in monitorServerOutput (method monitorServerOutput and property
disposablesByFile) but cleanup looks them up by fileKey, causing listener leaks;
update the caller that invokes monitorServerOutput (the call passing serverKey)
to pass the notebook's fileKey instead of serverKey so disposables are indexed
by the same fileKey used for cleanup, and verify references to serverKey/fileKey
across monitorServerOutput, disposablesByFile, and the cleanup lookup are
consistent.

In `@src/kernels/deepnote/deepnoteServerStarter.unit.test.ts`:
- Around line 90-104: The test currently calls the private method
reserveStartPort concurrently and only checks that returned values are numbers,
but it doesn't catch the race that returns the same port; update the test to
verify uniqueness (e.g., collect [port1, port2, port3] into a Set and assert its
size is 3) or, better, exercise the full startup flow that records server info
(use serverStarter.startServer or the public method that persists server
metadata) so concurrent reservations produce distinct ports; locate the private
helper reserveStartPort via getPrivateMethod(serverStarter, 'reserveStartPort')
and replace the final assertions with a uniqueness assertion or a full-start
integration assertion to ensure distinct port allocation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1bd083f2-2827-4d07-bfdf-f36afa80cc25

📥 Commits

Reviewing files that changed from the base of the PR and between d5f67f6 and 6f7b4aa.

📒 Files selected for processing (7)
  • src/kernels/deepnote/deepnoteLspClientManager.node.vscode.test.ts
  • src/kernels/deepnote/deepnoteServerStarter.node.ts
  • src/kernels/deepnote/deepnoteServerStarter.unit.test.ts
  • src/kernels/deepnote/deepnoteTestHelpers.ts
  • src/kernels/deepnote/environments/deepnoteEnvironmentTreeDataProvider.unit.test.ts
  • src/kernels/deepnote/types.ts
  • src/notebooks/deepnote/deepnoteKernelAutoSelector.node.unit.test.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
src/kernels/deepnote/deepnoteServerStarter.unit.test.ts (1)

90-104: 🧹 Nitpick | 🔵 Trivial

Concurrency test verifies serialization but not uniqueness.

Test confirms no crash on concurrent calls. Since reserveStartPort only reads existing projectContexts (none here), all return 8888. To truly test uniqueness, inject mock contexts between calls or test the full startup path.

Current behavior is correct per implementation—just noting for coverage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.unit.test.ts` around lines 90 -
104, The test for reserveStartPort only ensures concurrent calls don't crash but
doesn't assert uniqueness; update the test to simulate occupied ports (e.g.,
inject/mock serverStarter.projectContexts or the method that reads
projectContexts) or call the full startup path that registers servers so
concurrent reservations must pick unique ports; specifically modify the unit
test around reserveStartPort to pre-populate/mock projectContexts with existing
ports (or spy on startNotebookServer/startServer registration) and then assert
the returned ports are distinct instead of merely numbers.
src/kernels/deepnote/deepnoteTestHelpers.node.ts (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Use child_process without the node: prefix.

-import type { ChildProcess } from 'node:child_process';
+import type { ChildProcess } from 'child_process';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteTestHelpers.node.ts` at line 1, Replace the
node-prefixed builtin import with the plain module name: change the import of
the type ChildProcess from 'node:child_process' to 'child_process' (i.e., update
the import statement that references ChildProcess in deepnoteTestHelpers.node.ts
and any other occurrences to use 'child_process' instead of
'node:child_process').
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/kernels/deepnote/deepnoteServerStarter.unit.test.ts`:
- Around line 90-104: The test for reserveStartPort only ensures concurrent
calls don't crash but doesn't assert uniqueness; update the test to simulate
occupied ports (e.g., inject/mock serverStarter.projectContexts or the method
that reads projectContexts) or call the full startup path that registers servers
so concurrent reservations must pick unique ports; specifically modify the unit
test around reserveStartPort to pre-populate/mock projectContexts with existing
ports (or spy on startNotebookServer/startServer registration) and then assert
the returned ports are distinct instead of merely numbers.

In `@src/kernels/deepnote/deepnoteTestHelpers.node.ts`:
- Line 1: Replace the node-prefixed builtin import with the plain module name:
change the import of the type ChildProcess from 'node:child_process' to
'child_process' (i.e., update the import statement that references ChildProcess
in deepnoteTestHelpers.node.ts and any other occurrences to use 'child_process'
instead of 'node:child_process').

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e0cebc08-6b64-49c2-8cb5-b661870a8068

📥 Commits

Reviewing files that changed from the base of the PR and between 6f7b4aa and 7d8ee86.

📒 Files selected for processing (5)
  • src/kernels/deepnote/deepnoteLspClientManager.node.vscode.test.ts
  • src/kernels/deepnote/deepnoteServerStarter.unit.test.ts
  • src/kernels/deepnote/deepnoteTestHelpers.node.ts
  • src/kernels/deepnote/environments/deepnoteEnvironmentTreeDataProvider.unit.test.ts
  • src/notebooks/deepnote/deepnoteKernelAutoSelector.node.unit.test.ts

- Added a warning log when no project context is found, preventing server stop attempts.
- Updated the `stopServerForEnvironment` method to require a non-null project context, ensuring safer operation handling.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/kernels/deepnote/deepnoteServerStarter.node.ts (2)

85-177: 🧹 Nitpick | 🔵 Trivial

Method ordering doesn't follow guideline.

Per coding guidelines, methods should be ordered by accessibility then alphabetically. dispose (line 473) should precede startServer (line 99).

As per coding guidelines: "Order method, fields and properties, first by accessibility and then by alphabetical order"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 85 - 177,
The class method ordering violates the project's "accessibility then
alphabetical" rule: move the public dispose method so it appears before
startServer; locate the dispose method (named dispose) and the startServer
method (named startServer) and cut/paste dispose to precede startServer in the
class body, keeping its implementation and any related imports/fields intact and
ensuring no references or pendingOperations logic are changed.

182-218: ⚠️ Potential issue | 🔴 Critical

Key mismatch between startServer and stopServer.

startServer uses serverKey (${fileKey}-${environmentId}) for projectContexts and pendingOperations, but stopServer uses bare fileKey. This means:

  1. Line 186 won't find contexts created by startServer
  2. Line 193 won't detect pending start operations
  3. Stop operations could incorrectly conflict
Proposed fix

Either pass environmentId to stopServer and use serverKey, or document that stopServer stops all servers for a file (and iterate over matching keys).

-    public async stopServer(deepnoteFileUri: Uri, token?: CancellationToken): Promise<void> {
+    public async stopServer(deepnoteFileUri: Uri, environmentId: string, token?: CancellationToken): Promise<void> {
         Cancellation.throwIfCanceled(token);
 
         const fileKey = deepnoteFileUri.fsPath;
-        const projectContext = this.projectContexts.get(fileKey) ?? null;
+        const serverKey = `${fileKey}-${environmentId}`;
+        const projectContext = this.projectContexts.get(serverKey) ?? null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 182 - 218,
stopServer currently looks up projectContexts and pendingOperations by fileKey
but startServer stores them under serverKey (`${fileKey}-${environmentId}`), so
stopServer won't find matching contexts or pending ops and can mis-handle
concurrency; fix by either (A) adding an environmentId parameter to stopServer
and change lookups/sets to use serverKey (mirror how startServer creates entries
in projectContexts and pendingOperations), ensuring you pass serverKey into
stopServerForEnvironment, or (B) if the intent is to stop all environments for a
file, iterate over projectContexts.keys() and pendingOperations.keys(), select
keys that startWith(fileKey) and perform the same wait/stop logic for each
matching serverKey (using the existing stopServerForEnvironment and clearing
pendingOperations/projectContexts for those serverKeys).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts`:
- Around line 281-290: The catch block currently discards any captured process
output and only passes error.message to DeepnoteServerStartupError; instead,
inspect the thrown error for captured output fields (e.g., error.stdout,
error.stderr, error.output, or nested properties), extract relevant
stdout/stderr (falling back to error.message or String(error)), and pass those
values into DeepnoteServerStartupError so the constructor receives the real
stdout and stderr content (referencing DeepnoteServerStartupError,
interpreter.uri.fsPath, and port) and include the original Error object as
before.
- Around line 357-364: The isServerRunning method currently calls
fetch(`${serverInfo.url}/api`) with no timeout, which can hang; update
isServerRunning to use an AbortSignal-based timeout (e.g., AbortSignal.timeout
or an AbortController with setTimeout) and pass the signal to fetch via the
options object so the request is aborted after a reasonable timeout (e.g.,
3–5s); ensure you clean up any controller timers if using AbortController and
handle the aborted error path to return false from isServerRunning.
- Around line 335-337: The conditional check around resetting
projectContext.serverInfo is redundant because projectContext is a required
parameter and cannot be null; in the function/method where the block appears
(look for the code that sets projectContext.serverInfo to null in
deepnoteServerStarter.node.ts), remove the surrounding "if (projectContext) {
... }" and directly assign projectContext.serverInfo = null so the intent
remains but without the unnecessary null-check.

---

Outside diff comments:
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts`:
- Around line 85-177: The class method ordering violates the project's
"accessibility then alphabetical" rule: move the public dispose method so it
appears before startServer; locate the dispose method (named dispose) and the
startServer method (named startServer) and cut/paste dispose to precede
startServer in the class body, keeping its implementation and any related
imports/fields intact and ensuring no references or pendingOperations logic are
changed.
- Around line 182-218: stopServer currently looks up projectContexts and
pendingOperations by fileKey but startServer stores them under serverKey
(`${fileKey}-${environmentId}`), so stopServer won't find matching contexts or
pending ops and can mis-handle concurrency; fix by either (A) adding an
environmentId parameter to stopServer and change lookups/sets to use serverKey
(mirror how startServer creates entries in projectContexts and
pendingOperations), ensuring you pass serverKey into stopServerForEnvironment,
or (B) if the intent is to stop all environments for a file, iterate over
projectContexts.keys() and pendingOperations.keys(), select keys that
startWith(fileKey) and perform the same wait/stop logic for each matching
serverKey (using the existing stopServerForEnvironment and clearing
pendingOperations/projectContexts for those serverKeys).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4a76f561-b8cb-437c-82d4-06797d18993c

📥 Commits

Reviewing files that changed from the base of the PR and between 7d8ee86 and 9c4151a.

📒 Files selected for processing (1)
  • src/kernels/deepnote/deepnoteServerStarter.node.ts

- Updated the DeepnoteServerStarter class to consistently use fileKey for managing pending operations and project contexts, improving clarity and reducing potential errors.
- Adjusted logging messages to reflect the change, ensuring accurate information is logged during server operations.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (6)
src/kernels/deepnote/deepnoteServerStarter.node.ts (6)

424-426: ⚠️ Potential issue | 🟠 Major

Cancellation swallowed in catch block.

If getEnvironmentVariables throws due to cancellation, it's logged and ignored. Check for cancellation errors and re-throw.

Proposed fix
         } catch (error) {
+            if (Cancellation.isCanceled(error)) {
+                throw error;
+            }
             logger.error('DeepnoteServerStarter: Failed to get SQL integration env vars', error);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 424 - 426,
The catch in DeepnoteServerStarter around getEnvironmentVariables swallows
cancellation errors; update the catch to detect cancellation/abort errors (e.g.,
inspect error.name === 'AbortError' or error.code === 'ERR_ABORTED' or other
library-specific cancellation marker) and re-throw them instead of continuing,
while still logging non-cancellation errors via logger.error; ensure the logic
is applied to the try/catch that calls getEnvironmentVariables so cancellation
propagates properly.

280-288: 🧹 Nitpick | 🔵 Trivial

Consider extracting captured output from error.

If runtime-core's error includes stdout/stderr properties, they're discarded. Could improve diagnostics.

Proposed enhancement
         } catch (error) {
+            const stdout = error instanceof Error && 'stdout' in error ? String(error.stdout) : '';
+            const stderr = error instanceof Error && 'stderr' in error ? String(error.stderr) : (error instanceof Error ? error.message : String(error));
             throw new DeepnoteServerStartupError(
                 interpreter.uri.fsPath,
                 port,
                 'unknown',
-                '',
-                error instanceof Error ? error.message : String(error),
+                stdout,
+                stderr,
                 error instanceof Error ? error : undefined
             );
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 280 - 288,
The catch block that throws DeepnoteServerStartupError currently only forwards
message and error object but drops any runtime-core diagnostic output; update
the catch in deepnoteServerStarter.node.ts to detect stdout/stderr (e.g. via
const stdout = (error as any).stdout; const stderr = (error as any).stderr) and
pass them into the DeepnoteServerStartupError (or append them to the error
message) so interpreter.uri.fsPath, port and the original Error are preserved
while including stdout/stderr for better diagnostics; modify the
DeepnoteServerStartupError constructor call sites (or its signature if needed)
to accept and propagate these stdout/stderr values.

473-477: ⚠️ Potential issue | 🔴 Critical

Promise.race() receives wrapper objects, not promises.

pendingOps contains { type, promise } wrappers. Passing op to Promise.race() resolves immediately with the wrapper. Use op.promise.

Proposed fix
-            await Promise.allSettled(pendingOps.map((op) => Promise.race([op, sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)])));
+            await Promise.allSettled(pendingOps.map((op) => Promise.race([op.promise, sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)])));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 473 - 477,
The shutdown logic races each pending operation against the timeout but
currently passes the wrapper objects from pendingOperations (pendingOps entries
like { type, promise }) into Promise.race, causing immediate resolution; update
the mapping to use the actual Promise property (use op.promise) so
Promise.race([op.promise, sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)]) is awaited for
each entry, keeping the existing logger, sleep, and GRACEFUL_SHUTDOWN_TIMEOUT_MS
symbols intact.

354-361: ⚠️ Potential issue | 🟡 Minor

Add timeout to health check fetch.

fetch without timeout can hang indefinitely. Use AbortSignal.timeout().

Proposed fix
     private async isServerRunning(serverInfo: DeepnoteServerInfo): Promise<boolean> {
         try {
-            const response = await fetch(`${serverInfo.url}/api`);
+            const response = await fetch(`${serverInfo.url}/api`, {
+                signal: AbortSignal.timeout(5000)
+            });
             return response.ok;
         } catch {
             return false;
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 354 - 361,
The isServerRunning method calls fetch without a timeout; update
isServerRunning(DeepnoteServerInfo) to create an AbortSignal via
AbortSignal.timeout(<ms>) and pass that signal into fetch
(fetch(`${serverInfo.url}/api`, { signal })), then catch aborted requests (and
other errors) and return false; ensure the signal is properly used and cleaned
up (no extra unhandled promise) so the health check always times out instead of
hanging indefinitely.

370-394: ⚠️ Potential issue | 🟠 Major

Port reservation race window remains.

Lock releases before startServer binds. Concurrent starts between lock release (line 392) and actual port binding can receive the same base port. If runtime-core handles conflicts internally, this is mitigated; otherwise, consider holding the lock until startup completes or tracking reserved ports explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 370 - 394,
reserveStartPort currently releases portAllocationLock before the server
actually binds, creating a race; fix by tracking reserved start ports explicitly
instead of relying solely on the lock: add a module-level Set (e.g.,
reservedStartPorts) and in reserveStartPort (keep name) mark the chosen maxPort
as reserved before releasing the lock, skipping any ports already present in
reservedStartPorts or projectContexts' serverInfo; then update the server
startup flow (startServer or whatever calls reserveStartPort) to remove the port
from reservedStartPorts once binding succeeds or on failure so future callers
can reuse it; ensure all references to portAllocationLock, projectContexts, and
serverInfo continue to coordinate with the new reservedStartPorts tracking.

269-279: ⚠️ Potential issue | 🟠 Major

Missing cancellation check before startServer.

After gatherSqlIntegrationEnvVars returns, cancellation isn't checked before starting the server. If cancelled while gathering env vars, startup proceeds anyway.

Proposed fix
         const extraEnv = await this.gatherSqlIntegrationEnvVars(deepnoteFileUri, environmentId, token);
 
+        Cancellation.throwIfCanceled(token);
+
         let serverInfo: DeepnoteServerInfo;
         try {
             serverInfo = await startServer({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 269 - 279,
After awaiting gatherSqlIntegrationEnvVars(deepnoteFileUri, environmentId,
token) add an immediate check for cancellation using the provided token (e.g.,
if (token.isCancellationRequested) or the project's cancel-check helper) and
bail out (throw or return) before calling startServer; this ensures
startServer(...) is not invoked when the operation was cancelled. Update the
method surrounding startServer and any serverInfo assignment accordingly (the
symbols to edit are gatherSqlIntegrationEnvVars and startServer in the current
method).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts`:
- Around line 65-70: Reorder the private readonly field declarations so they
follow alphabetical order within the same accessibility group; specifically, in
deepnoteServerStarter.node.ts reorder the fields disposablesByFile, lockFileDir,
pendingOperations, portAllocationLock, projectContexts, sessionId (all declared
as private readonly) into alphabetical order while preserving the accessibility
and readonly modifiers and keeping their initializers unchanged.

---

Duplicate comments:
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts`:
- Around line 424-426: The catch in DeepnoteServerStarter around
getEnvironmentVariables swallows cancellation errors; update the catch to detect
cancellation/abort errors (e.g., inspect error.name === 'AbortError' or
error.code === 'ERR_ABORTED' or other library-specific cancellation marker) and
re-throw them instead of continuing, while still logging non-cancellation errors
via logger.error; ensure the logic is applied to the try/catch that calls
getEnvironmentVariables so cancellation propagates properly.
- Around line 280-288: The catch block that throws DeepnoteServerStartupError
currently only forwards message and error object but drops any runtime-core
diagnostic output; update the catch in deepnoteServerStarter.node.ts to detect
stdout/stderr (e.g. via const stdout = (error as any).stdout; const stderr =
(error as any).stderr) and pass them into the DeepnoteServerStartupError (or
append them to the error message) so interpreter.uri.fsPath, port and the
original Error are preserved while including stdout/stderr for better
diagnostics; modify the DeepnoteServerStartupError constructor call sites (or
its signature if needed) to accept and propagate these stdout/stderr values.
- Around line 473-477: The shutdown logic races each pending operation against
the timeout but currently passes the wrapper objects from pendingOperations
(pendingOps entries like { type, promise }) into Promise.race, causing immediate
resolution; update the mapping to use the actual Promise property (use
op.promise) so Promise.race([op.promise, sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)])
is awaited for each entry, keeping the existing logger, sleep, and
GRACEFUL_SHUTDOWN_TIMEOUT_MS symbols intact.
- Around line 354-361: The isServerRunning method calls fetch without a timeout;
update isServerRunning(DeepnoteServerInfo) to create an AbortSignal via
AbortSignal.timeout(<ms>) and pass that signal into fetch
(fetch(`${serverInfo.url}/api`, { signal })), then catch aborted requests (and
other errors) and return false; ensure the signal is properly used and cleaned
up (no extra unhandled promise) so the health check always times out instead of
hanging indefinitely.
- Around line 370-394: reserveStartPort currently releases portAllocationLock
before the server actually binds, creating a race; fix by tracking reserved
start ports explicitly instead of relying solely on the lock: add a module-level
Set (e.g., reservedStartPorts) and in reserveStartPort (keep name) mark the
chosen maxPort as reserved before releasing the lock, skipping any ports already
present in reservedStartPorts or projectContexts' serverInfo; then update the
server startup flow (startServer or whatever calls reserveStartPort) to remove
the port from reservedStartPorts once binding succeeds or on failure so future
callers can reuse it; ensure all references to portAllocationLock,
projectContexts, and serverInfo continue to coordinate with the new
reservedStartPorts tracking.
- Around line 269-279: After awaiting
gatherSqlIntegrationEnvVars(deepnoteFileUri, environmentId, token) add an
immediate check for cancellation using the provided token (e.g., if
(token.isCancellationRequested) or the project's cancel-check helper) and bail
out (throw or return) before calling startServer; this ensures startServer(...)
is not invoked when the operation was cancelled. Update the method surrounding
startServer and any serverInfo assignment accordingly (the symbols to edit are
gatherSqlIntegrationEnvVars and startServer in the current method).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6a755e4f-d3d3-4555-9241-2bb9ee4de1cd

📥 Commits

Reviewing files that changed from the base of the PR and between 9c4151a and 347a809.

📒 Files selected for processing (1)
  • src/kernels/deepnote/deepnoteServerStarter.node.ts

- Eliminated the port allocation serialization logic from the DeepnoteServerStarter class, as it is now handled by the @deepnote/runtime-core's startServer method.
- Updated related logging messages to reflect the changes in server startup processes.
- Adjusted unit tests to focus on SQL environment variable gathering and lifecycle orchestration, removing tests related to port reservation.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/kernels/deepnote/deepnoteServerStarter.node.ts (2)

262-289: ⚠️ Potential issue | 🟠 Major

Don't swallow cancellation in the SQL-env/startup path.

gatherSqlIntegrationEnvVars() logs and returns {} for every exception, so a canceled provider call falls through into startServer(). After Line 266 returns, this method also never re-checks token before caching serverInfo, attaching listeners, and writing the lock file. Re-throw cancellation in the helper, and if cancellation is requested after runtime-core starts, stop the returned server before bubbling the cancel.

Also applies to: 358-387

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 262 - 289,
gatherSqlIntegrationEnvVars currently swallows cancellations; change it to
re-throw cancellation errors (check for token.isCancellationRequested or
CancellationError from provider and rethrow) so upstream knows to abort, and in
the DeepnoteServerStarter flow around startServer (the block using
gatherSqlIntegrationEnvVars, startServer, DeepnoteServerStartupError) add a
post-start cancellation check that, if cancellation was requested, stops the
started server (use serverInfo.process.pid or any server stop API) before
re-throwing/propagating the cancellation; ensure you do not cache
projectContext.serverInfo, call monitorServerOutput, or write the lock file
(serverInfo.process.pid) when cancelled.

432-436: ⚠️ Potential issue | 🟠 Major

dispose() is racing wrapper objects, not the tracked promises.

pendingOperations stores { type, promise }, but Line 435 passes op into Promise.race(). Plain objects are treated as already-resolved values, so dispose() stops waiting immediately and can clear state while starts/stops are still in flight.

Suggested fix
-            await Promise.allSettled(pendingOps.map((op) => Promise.race([op, sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)])));
+            await Promise.allSettled(
+                pendingOps.map((op) => Promise.race([op.promise, sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)]))
+            );

Run this to verify the wrapper shape and the current Promise.race() call. Expect PendingOperation to expose a promise field and dispose() to race op instead of op.promise.

#!/bin/bash
rg -n -C2 'type PendingOperation|promise: Promise|Promise\.race' src/kernels/deepnote/deepnoteServerStarter.node.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 432 - 436,
dispose() is racing the wrapper objects instead of their inner promises:
pendingOperations contains PendingOperation objects with a promise field, but
the code calls Promise.race([op, ...]) which treats op as an already-resolved
value; change the dispose() logic that builds pendingOps (and the Promise.race
call) to use op.promise (e.g. Promise.race([op.promise,
sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)])) so Promise.race waits on the actual
tracked Promise; update references in the dispose() function that currently use
pendingOps.map(op => ...) and ensure you still handle Promise.allSettled(...)
semantics with the awaited raced promises.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts`:
- Around line 118-127: The cached-server early return is skipping validation of
requested additionalPackages, so update the warm-start path in the block using
projectContexts.get(fileKey) / existingContext to also compare or reconcile
additionalPackages before returning: when existingEnvironmentId ===
environmentId and isServerRunning(existingServerInfo) is true, fetch the
requested additionalPackages from the current call context and either (a) verify
they are already installed against existingServerInfo (and only return if they
match) or (b) call installAdditionalPackages(existingServerInfo,
requestedAdditionalPackages) to install missing deps and update the stored
serverInfo in projectContexts before returning; ensure this logic lives
alongside the isServerRunning check and still returns existingServerInfo only
after verification/reconciliation.

In `@src/kernels/deepnote/deepnoteServerStarter.unit.test.ts`:
- Around line 59-77: Add a test case to cover the canceled-provider branch in
gatherSqlIntegrationEnvVars: instantiate DeepnoteServerStarter (as in the
existing test), register or mock an SQL provider that rejects with an
OperationCanceled-like error when called, call the private method
gatherSqlIntegrationEnvVars (use getPrivateMethod) with a cancelled token or
simulate cancellation, and assert that the result is {} (or that cancellation is
propagated as expected for your implementation); ensure you clean up by
disposing starter and use the same mocks (mockProcessServiceFactory,
mockToolkitInstaller, mockAgentSkillsManager, mockOutputChannel,
mockAsyncRegistry) and the Uri import to mirror the existing test structure so
the new test exercises the provider-rejection-with-cancellation path.
- Around line 80-89: The test must verify dispose() waits for in-flight
operations instead of just checking maps are empty; modify the 'dispose' test to
seed starter.pendingOperations with a deferred (create a Promise whose resolve
you control) representing a running start/stop, then call
serverStarter.dispose() asynchronously and assert it does not resolve
immediately (use a short timeout or Promise.race to ensure it's still pending),
then resolve the deferred and finally await dispose() to ensure it completes;
reference the dispose() method and the starter.pendingOperations map when adding
and removing the deferred entry and asserting behavior.

---

Duplicate comments:
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts`:
- Around line 262-289: gatherSqlIntegrationEnvVars currently swallows
cancellations; change it to re-throw cancellation errors (check for
token.isCancellationRequested or CancellationError from provider and rethrow) so
upstream knows to abort, and in the DeepnoteServerStarter flow around
startServer (the block using gatherSqlIntegrationEnvVars, startServer,
DeepnoteServerStartupError) add a post-start cancellation check that, if
cancellation was requested, stops the started server (use serverInfo.process.pid
or any server stop API) before re-throwing/propagating the cancellation; ensure
you do not cache projectContext.serverInfo, call monitorServerOutput, or write
the lock file (serverInfo.process.pid) when cancelled.
- Around line 432-436: dispose() is racing the wrapper objects instead of their
inner promises: pendingOperations contains PendingOperation objects with a
promise field, but the code calls Promise.race([op, ...]) which treats op as an
already-resolved value; change the dispose() logic that builds pendingOps (and
the Promise.race call) to use op.promise (e.g. Promise.race([op.promise,
sleep(GRACEFUL_SHUTDOWN_TIMEOUT_MS)])) so Promise.race waits on the actual
tracked Promise; update references in the dispose() function that currently use
pendingOps.map(op => ...) and ensure you still handle Promise.allSettled(...)
semantics with the awaited raced promises.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 87a75357-053a-49a2-87db-46c1eed6c556

📥 Commits

Reviewing files that changed from the base of the PR and between 347a809 and 5eb2c17.

📒 Files selected for processing (2)
  • src/kernels/deepnote/deepnoteServerStarter.node.ts
  • src/kernels/deepnote/deepnoteServerStarter.unit.test.ts

tkislan added 2 commits March 17, 2026 15:02
…g improvements

- Introduced a new `serverOutputByFile` map to track stdout and stderr outputs for each server instance, limiting the output length to improve performance and manageability.
- Updated error handling in the server startup process to capture and report both stdout and stderr in case of failures, providing better diagnostics.
- Adjusted the `dispose` method to ensure all internal states, including the new output tracking, are cleared appropriately.
- Enhanced unit tests to validate the new output tracking functionality and ensure proper handling of cancellation errors.
- Modified the error reporting logic to ensure that stderr output is captured only when available, enhancing clarity in error messages.
- This change aims to streamline the error handling process during server startup, providing more accurate feedback in case of failures.
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 17, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
src/kernels/deepnote/deepnoteServerStarter.node.ts (3)

31-32: ⚠️ Potential issue | 🟠 Major

A slow startup can escape dispose().

Line 31 allows startup to run for 120s, but Lines 455-457 wait only 3s before Lines 496-499 clear all tracking. A pending start can therefore finish after disposal and recreate listeners or lock files for an untracked server.

As per coding guidelines: "Use real cancellation tokens tied to lifecycle events instead of fake/never-cancelled tokens; create CancellationTokenSource tied to relevant events (e.g., notebook close, cell cancel)"

Also applies to: 455-499

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 31 - 32, The
long fixed startup timeout (SERVER_STARTUP_TIMEOUT_MS) allows startServer to
continue after dispose(), and the short GRACEFUL_SHUTDOWN_TIMEOUT_MS means
servers/trackers are cleared while a startup may still be running; fix by
introducing a CancellationTokenSource tied to dispose() (or the server
lifecycle) and pass its token into startServer/startup logic and any async
waiters so startup is aborted immediately on dispose; ensure dispose() cancels
that CTS and awaits the pending startTask (or observes the cancellation) before
running the cleanup that clears tracked servers/listeners (the code around
dispose(), startServer, and the cleanup that removes tracked servers/listeners
should be updated to use the CTS rather than relying on the 3s timeout).

103-129: ⚠️ Potential issue | 🟠 Major

Don't return a warm server before honoring additionalPackages.

Lines 124-129 only check environmentId and liveness. A later call for the same notebook can request extra packages and still get the old server back, so those dependencies are never installed.

As per coding guidelines: "Validate cached state before early returns - before returning 'already configured', verify the state is still valid (e.g., interpreter paths match, controllers aren't stale)"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 103 - 129,
The current early return when an existingContext matches environmentId and
isServerRunning may bypass honoring requested additionalPackages; update the
logic in the method that uses this.projectContexts/existingContext (check
existingEnvironmentId and existingServerInfo) to validate that the server
already has the requested additionalPackages installed before returning: fetch
or read the server's installed package list (or store package metadata in
projectContexts), compare against the passed additionalPackages array, and if
any are missing, either trigger installation on the existingServerInfo (await
installation) or proceed to create/configure a new server; ensure this check
uses the existingServerInfo and isServerRunning helpers and only returns early
when both liveness and package parity are satisfied.

264-305: ⚠️ Potential issue | 🟠 Major

Cancellation still turns into a successful startup.

Lines 393-395 swallow provider cancellation, and Lines 271-305 never re-check token after runtime-core returns. If the user cancels mid-start, this path can still launch, cache, and monitor a live server. Please propagate cancellation here and stop the just-started server before returning.

As per coding guidelines: "Use real cancellation tokens tied to lifecycle events instead of fake/never-cancelled tokens; create CancellationTokenSource tied to relevant events (e.g., notebook close, cell cancel)"

Also applies to: 368-397

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/kernels/deepnote/deepnoteServerStarter.node.ts` around lines 264 - 305,
The startup path currently ignores user cancellation: after calling startServer
(and earlier gatherSqlIntegrationEnvVars) you must re-check the incoming
CancellationToken (token) immediately after startServer resolves and before
caching/monitoring the server; if cancellation occurred, stop the started server
(use serverInfo.process.kill() or the server shutdown method on
DeepnoteServerInfo), remove the entry from serverOutputByFile, avoid calling
monitorServerOutput/writeLockFile or setting projectContext.serverInfo, and
rethrow a cancellation error (or call token.throwIfCancellationRequested). Also
replace any fake/never-cancelled tokens used in
gatherSqlIntegrationEnvVars/startServer calls with a real
CancellationTokenSource tied to the notebook/cell lifecycle so cancellation is
propagated from the provider to this code path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/kernels/deepnote/deepnoteServerStarter.node.ts`:
- Around line 31-32: The long fixed startup timeout (SERVER_STARTUP_TIMEOUT_MS)
allows startServer to continue after dispose(), and the short
GRACEFUL_SHUTDOWN_TIMEOUT_MS means servers/trackers are cleared while a startup
may still be running; fix by introducing a CancellationTokenSource tied to
dispose() (or the server lifecycle) and pass its token into startServer/startup
logic and any async waiters so startup is aborted immediately on dispose; ensure
dispose() cancels that CTS and awaits the pending startTask (or observes the
cancellation) before running the cleanup that clears tracked servers/listeners
(the code around dispose(), startServer, and the cleanup that removes tracked
servers/listeners should be updated to use the CTS rather than relying on the 3s
timeout).
- Around line 103-129: The current early return when an existingContext matches
environmentId and isServerRunning may bypass honoring requested
additionalPackages; update the logic in the method that uses
this.projectContexts/existingContext (check existingEnvironmentId and
existingServerInfo) to validate that the server already has the requested
additionalPackages installed before returning: fetch or read the server's
installed package list (or store package metadata in projectContexts), compare
against the passed additionalPackages array, and if any are missing, either
trigger installation on the existingServerInfo (await installation) or proceed
to create/configure a new server; ensure this check uses the existingServerInfo
and isServerRunning helpers and only returns early when both liveness and
package parity are satisfied.
- Around line 264-305: The startup path currently ignores user cancellation:
after calling startServer (and earlier gatherSqlIntegrationEnvVars) you must
re-check the incoming CancellationToken (token) immediately after startServer
resolves and before caching/monitoring the server; if cancellation occurred,
stop the started server (use serverInfo.process.kill() or the server shutdown
method on DeepnoteServerInfo), remove the entry from serverOutputByFile, avoid
calling monitorServerOutput/writeLockFile or setting projectContext.serverInfo,
and rethrow a cancellation error (or call token.throwIfCancellationRequested).
Also replace any fake/never-cancelled tokens used in
gatherSqlIntegrationEnvVars/startServer calls with a real
CancellationTokenSource tied to the notebook/cell lifecycle so cancellation is
propagated from the provider to this code path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 640e39ea-fc04-4fc3-9bdb-20a134aa15c4

📥 Commits

Reviewing files that changed from the base of the PR and between 5eb2c17 and 1e7cb07.

📒 Files selected for processing (2)
  • src/kernels/deepnote/deepnoteServerStarter.node.ts
  • src/kernels/deepnote/deepnoteServerStarter.unit.test.ts

@tkislan tkislan marked this pull request as ready for review March 18, 2026 06:58
@tkislan tkislan requested a review from a team as a code owner March 18, 2026 06:58
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.

1 participant