Title: feat(runtime-core,cli,mcp): typed runtime failures, fail-fast shutdown, and real toolkit integration tests by jamesbhobbs · Pull Request #519 · deepnote/deepnote · GitHub
Open Graph Title: feat(runtime-core,cli,mcp): typed runtime failures, fail-fast shutdown, and real toolkit integration tests by jamesbhobbs · Pull Request #519 · deepnote/deepnote
X Title: feat(runtime-core,cli,mcp): typed runtime failures, fail-fast shutdown, and real toolkit integration tests by jamesbhobbs · Pull Request #519 · deepnote/deepnote
Description: Stacked on #518 (interpreter resolution for #288); merge that first. Part of #162: for headless execution to be a first-class feature, the runtime has to fail loudly, fail fast, and be tested against the real toolkit.
Warm server pooling for the MCP is deliberately not in this PR. It lands as a follow-up stacked on this one (#520), with real concurrency and crash-recovery tests, so the reliability work here can be reviewed and merged on its own.
Problems
Read against the runtime as it stands on main:
Nothing in CI starts the toolkit. The "CLI E2E" job runs --version and --help; every engine and kernel test mocks the Jupyter client. A toolkit release or a @jupyterlab/services bump can break deepnote run with all checks green.
A dead server hangs the run. If the Jupyter server dies mid-run, the websocket drops and the Jupyter client retries with backoff for about two minutes without ever marking the kernel dead; the in-flight execution waits forever. The server starter only watches for process exit during startup.
A failed start delays exit by two minutes. When python -m deepnote_toolkit server exits immediately (toolkit not installed), the startup race rejects at once, but the health-check loop is never cancelled and keeps polling until the 120 s startup timeout. The CLI has already printed its error and sits there. Measured at 2:00 before, 0.3 s after.
Every start failure says "install deepnote-toolkit". The CLI appends that hint to any engine.start() error, including port conflicts, timeouts and bad --python paths. Machine output has no failure category, and exit code 1 covers infra failures and user code alike.
Interpreter fallback ignores a project .venv. #518 adds the extension sidecar and DEEPNOTE_PYTHON; a .venv next to the notebook was still ignored in favour of bare python.
Timeouts are constants (30 s kernel idle, 120 s server start) and server logs are dropped unless the process dies during startup.
Stopping the server can leak Jupyter. stopServer SIGKILLs the toolkit supervisor two seconds after SIGTERM. The supervisor terminates each child and waits up to five seconds for it, so under load it is still shutting Jupyter down when it is killed, and the Jupyter server, language server and kernel are orphaned. Found while adding a leaked-process check to the integration tests; it reproduced twice in full runs and never in isolation.
Changes
Typed failures (@deepnote/runtime-core). New RuntimeError family with a closed category: server-launch, kernel-launch, kernel-died, server-exited, execution-timeout, plus in-block for the block's own code. Errors carry an optional hint. startServer names the real cause (toolkit missing for that interpreter, a missing server dependency, process exit with stderr tail, health-check timeout). BlockExecutionResult and ExecutionSummary gain failureCategory.
No more hangs. ServerInfo.exited settles when the process ends; the engine fails the in-flight execution with ServerExitedError when that happens mid-run, through a removable process listener it detaches on stop. The kernel client fails in-flight executions with KernelDiedError on dead/autorestarting status (and translates the client's opaque "Canceled future" rejection), and runs a connection watchdog: a dropped websocket that is not back within 10 s is probed; a dead server or missing kernel fails immediately, a server that still answers gets exactly one more grace period. After a fatal error every later execute() rejects immediately, and a client can connect() again from a clean slate. Integration tests measure a dead kernel reported in ~8 s and a killed Jupyter server in ~14 s.
Fast startup failure. The health-check loop is abortable and is aborted when startup fails, and every health request is bounded by the time left in the startup timeout, so a server that accepts the connection but never answers cannot stall past --startup-timeout. Kernel disposal detaches the kernel's Lumino listeners when the connection is down, because @jupyterlab/services awaits its post-restart reconnect() without handling rejection; disposing mid-reconnect otherwise surfaces an unhandled rejection. Adds @lumino/signaling as a direct dependency (already in the tree).
No more orphaned servers. The toolkit supervisor starts Jupyter and the language server in their own sessions, so nothing but its explicit cleanup ever reaches them. startServer records those child pids once the server is ready, and stopServer waits up to 10 s for the supervisor to finish its own cleanup, kills the children before force-killing a supervisor that ignores SIGTERM (also on a failed startup), and afterwards terminates any recorded child that outlived the supervisor. Every integration test file checks after each test that no toolkit, Jupyter, language-server or kernel process from the interpreter's environment was left behind, and prints the toolkit server's log when one is.
Configurable timeouts and logs. RuntimeConfig gains serverStartupTimeoutMs, kernelStartupTimeoutMs, blockTimeoutMs and onServerLog. A block that exceeds the block timeout is interrupted and the run fails with execution-timeout. CLI: --startup-timeout
Open Graph Description: Stacked on #518 (interpreter resolution for #288); merge that first. Part of #162: for headless execution to be a first-class feature, the runtime has to fail loudly, fail fast, and be tested again...
X Description: Stacked on #518 (interpreter resolution for #288); merge that first. Part of #162: for headless execution to be a first-class feature, the runtime has to fail loudly, fail fast, and be tested again...
Opengraph URL: https://github.com/deepnote/deepnote/pull/519
X: @github
Domain: github.com
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /:user_id/:repository/pull/:id/commits/:range(.:format) |
| route-controller | pull_requests |
| route-action | commits |
| fetch-nonce | v2:c2dcc3eb-0ae1-43c4-634b-1e3395c09eb3 |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | E9DC:BC32:A0350C:D5030F:6AEDCF44 |
| html-safe-nonce | 7fdfcd2c8a8504af7102d3a6697c46bfd50bec906eb6a28299658c64e8c318c0 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJFOURDOkJDMzI6QTAzNTBDOkQ1MDMwRjo2QUVEQ0Y0NCIsInZpc2l0b3JfaWQiOiI2NTg3MDA0MDEwMDgwNjIwMzU2IiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | beb16aea2cdd148721dad47f7390fde3daaae1fa2955d0b05e0378a41e3b0bd6 |
| hovercard-subject-tag | pull_request:4485761092 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/deepnote/deepnote/pull/519/commits/c70381aaca4695b7bd2aa871791e3c1119b695e8 |
| twitter:image | https://avatars.githubusercontent.com/u/15235276?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/15235276?s=400&v=4 |
| og:image:alt | Stacked on #518 (interpreter resolution for #288); merge that first. Part of #162: for headless execution to be a first-class feature, the runtime has to fail loudly, fail fast, and be tested again... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | f60373873bb1ef795e8511ea1f2a76997e9c649d72b02942aedf2def3423a0ce |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/deepnote/deepnote git https://github.com/deepnote/deepnote.git |
| octolytics-dimension-user_id | 45339858 |
| octolytics-dimension-user_login | deepnote |
| octolytics-dimension-repository_id | 1066509034 |
| octolytics-dimension-repository_nwo | deepnote/deepnote |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 1066509034 |
| octolytics-dimension-repository_network_root_nwo | deepnote/deepnote |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 0f55cfa61438c7dc93d1024a1e5e5182b36830e2 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width