kernel-checkpoint 0.1.6__tar.gz → 0.1.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/PKG-INFO +1 -1
  2. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/jupyter-notebook-image/Dockerfile +1 -1
  3. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/kernel_checkpoint/__init__.py +1 -0
  4. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/kernel_checkpoint/_version.py +1 -1
  5. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/build_log.json +1 -1
  6. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/package.json +2 -2
  7. kernel_checkpoint-0.1.6/kernel_checkpoint/labextension/static/lib_index_js.3a33b915212dd7a87b4f.js → kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/lib_index_js.6dc5fa8b4a9bc62a809d.js +442 -10
  8. kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/lib_index_js.6dc5fa8b4a9bc62a809d.js.map +1 -0
  9. kernel_checkpoint-0.1.6/kernel_checkpoint/labextension/static/remoteEntry.2bcd73c22427069814a9.js → kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/remoteEntry.5ffc549e28b042648bca.js +3 -3
  10. kernel_checkpoint-0.1.6/kernel_checkpoint/labextension/static/remoteEntry.2bcd73c22427069814a9.js.map → kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/remoteEntry.5ffc549e28b042648bca.js.map +1 -1
  11. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/package.json +1 -1
  12. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/src/checkpoint-panel.tsx +22 -3
  13. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/src/index.ts +106 -5
  14. kernel_checkpoint-0.1.8/src/restore-handler.ts +418 -0
  15. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/src/types.ts +30 -1
  16. kernel_checkpoint-0.1.6/kernel_checkpoint/labextension/static/lib_index_js.3a33b915212dd7a87b4f.js.map +0 -1
  17. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/.copier-answers.yml +0 -0
  18. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/.gitignore +0 -0
  19. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/.prettierignore +0 -0
  20. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/.yarnrc.yml +0 -0
  21. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/4 +0 -0
  22. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/=4 +0 -0
  23. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/CHANGELOG.md +0 -0
  24. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/LICENSE +0 -0
  25. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/README.md +0 -0
  26. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/RELEASE.md +0 -0
  27. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/babel.config.js +0 -0
  28. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/cursor_jupyterlab_extension_for_checkpo.md +0 -0
  29. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/install.json +0 -0
  30. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/jest.config.js +0 -0
  31. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/jupyter-config/jupyter_server_config.d/kernel_checkpoint.json +0 -0
  32. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/kernel_checkpoint/Untitled.ipynb +0 -0
  33. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/kernel_checkpoint/handlers.py +0 -0
  34. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/static/style.js +0 -0
  35. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/static/style_index_js.c3e72438ed03e9dee0fc.js +0 -0
  36. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/static/style_index_js.c3e72438ed03e9dee0fc.js.map +0 -0
  37. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/pyproject.toml +0 -0
  38. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/setup.py +0 -0
  39. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/src/__tests__/kernel_checkpoint.spec.ts +0 -0
  40. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/src/api.ts +0 -0
  41. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/style/base.css +0 -0
  42. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/style/index.css +0 -0
  43. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/style/index.js +0 -0
  44. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/tsconfig.json +0 -0
  45. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/tsconfig.test.json +0 -0
  46. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/ui-tests/README.md +0 -0
  47. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/ui-tests/jupyter_server_test_config.py +0 -0
  48. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/ui-tests/package.json +0 -0
  49. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/ui-tests/playwright.config.js +0 -0
  50. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/ui-tests/tests/kernel_checkpoint.spec.ts +0 -0
  51. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/ui-tests/yarn.lock +0 -0
  52. {kernel_checkpoint-0.1.6 → kernel_checkpoint-0.1.8}/yarn.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kernel_checkpoint
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: An extension for saving and restoring kernel pod state with jupyter enterprise gateway
5
5
  Project-URL: Homepage, https://github.com/lehuannhatrang/kernel-checkpoint-extension.git
6
6
  Project-URL: Bug Tracker, https://github.com/lehuannhatrang/kernel-checkpoint-extension.git/issues
@@ -4,6 +4,6 @@ USER root
4
4
 
5
5
  RUN pip install --upgrade pip
6
6
 
7
- RUN pip install --no-cache-dir kernel-checkpoint==0.1.5
7
+ RUN pip install --no-cache-dir kernel-checkpoint==0.1.7
8
8
 
9
9
  USER jovyan
@@ -86,6 +86,7 @@ def _patch_kernel_handler_for_env_passthrough(server_app):
86
86
  "KERNEL_CHECKPOINT_NAME",
87
87
  "KERNEL_CHECKPOINT_FILE_PATH",
88
88
  "KERNEL_CHECKPOINT_CONTAINER_NAME",
89
+ "KERNEL_ID",
89
90
  ]
90
91
  except Exception:
91
92
  pass
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.1.6'
4
+ __version__ = VERSION = '0.1.8'
@@ -700,7 +700,7 @@
700
700
  "singleton": true
701
701
  },
702
702
  "kernel-checkpoint": {
703
- "version": "0.1.5",
703
+ "version": "0.1.7",
704
704
  "singleton": true,
705
705
  "import": "/home/ubuntu/projects/kernel-checkpoint-extension/lib/index.js"
706
706
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kernel-checkpoint",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "An extension for saving and restoring kernel pod state with jupyter enterprise gateway",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -114,7 +114,7 @@
114
114
  },
115
115
  "outputDir": "kernel_checkpoint/labextension",
116
116
  "_build": {
117
- "load": "static/remoteEntry.2bcd73c22427069814a9.js",
117
+ "load": "static/remoteEntry.5ffc549e28b042648bca.js",
118
118
  "extension": "./extension",
119
119
  "style": "./style"
120
120
  }
@@ -119,7 +119,7 @@ function formatDate(iso) {
119
119
  return new Date(iso).toLocaleString();
120
120
  }
121
121
  function CheckpointPanel(props) {
122
- const { namespace, kernelId, onRestore } = props;
122
+ const { namespace, kernelId, kernelSpecName, notebookName, onRestore, onBeforeCreate } = props;
123
123
  const [checkpoints, setCheckpoints] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
124
124
  const [loading, setLoading] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(true);
125
125
  const [error, setError] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
@@ -170,11 +170,18 @@ function CheckpointPanel(props) {
170
170
  setCreating(true);
171
171
  setFlash(null);
172
172
  try {
173
+ const busyCells = onBeforeCreate ? onBeforeCreate() : [];
173
174
  await _api__WEBPACK_IMPORTED_MODULE_1__.CheckpointAPI.createCheckpoint({
174
175
  name: newName.trim(),
175
176
  namespace,
176
177
  kernelId,
177
- buildImage: false
178
+ buildImage: false,
179
+ metadata: {
180
+ kernelId,
181
+ kernelName: kernelSpecName,
182
+ notebookName,
183
+ busyCells
184
+ }
178
185
  });
179
186
  setNewName('');
180
187
  setShowCreateForm(false);
@@ -231,7 +238,7 @@ function CheckpointPanel(props) {
231
238
  }
232
239
  };
233
240
  const handleRestore = async (checkpointName) => {
234
- var _a, _b;
241
+ var _a, _b, _c, _d, _e, _f;
235
242
  setConfirmRestore(null);
236
243
  setRestoring(checkpointName);
237
244
  setFlash(null);
@@ -241,7 +248,12 @@ function CheckpointPanel(props) {
241
248
  if (!(checkpointFile === null || checkpointFile === void 0 ? void 0 : checkpointFile.storagePath)) {
242
249
  throw new Error('No checkpoint file path available for this checkpoint');
243
250
  }
244
- await onRestore(checkpointName, checkpointFile.storagePath, checkpointFile.containerName);
251
+ const cpKernelId = (_c = (_b = detail.metadata) === null || _b === void 0 ? void 0 : _b.kernelId) !== null && _c !== void 0 ? _c : '';
252
+ if (!cpKernelId) {
253
+ throw new Error('No kernel ID found in checkpoint metadata');
254
+ }
255
+ const busyCells = (_e = (_d = detail.metadata) === null || _d === void 0 ? void 0 : _d.busyCells) !== null && _e !== void 0 ? _e : [];
256
+ await onRestore(checkpointName, checkpointFile.storagePath, checkpointFile.containerName, cpKernelId, busyCells);
245
257
  setFlash({
246
258
  type: 'success',
247
259
  text: `Kernel restored from "${checkpointName}". The kernel is restarting.`
@@ -250,7 +262,7 @@ function CheckpointPanel(props) {
250
262
  catch (err) {
251
263
  setFlash({
252
264
  type: 'error',
253
- text: (_b = err.message) !== null && _b !== void 0 ? _b : 'Failed to restore checkpoint'
265
+ text: (_f = err.message) !== null && _f !== void 0 ? _f : 'Failed to restore checkpoint'
254
266
  });
255
267
  }
256
268
  finally {
@@ -380,6 +392,8 @@ __webpack_require__.r(__webpack_exports__);
380
392
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_4__);
381
393
  /* harmony import */ var _checkpoint_panel__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./checkpoint-panel */ "./lib/checkpoint-panel.js");
382
394
  /* harmony import */ var _api__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./api */ "./lib/api.js");
395
+ /* harmony import */ var _restore_handler__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./restore-handler */ "./lib/restore-handler.js");
396
+
383
397
 
384
398
 
385
399
 
@@ -409,6 +423,29 @@ const plugin = {
409
423
  requires: [_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__.INotebookTracker],
410
424
  activate: (app, notebookTracker) => {
411
425
  console.log('JupyterLab extension kernel-checkpoint is activated!');
426
+ // ── Per-notebook execution tracking ──────────────────────────────
427
+ //
428
+ // Each open notebook gets its own CellExecutionTracker that
429
+ // continuously records in-flight msg_id → execution_count pairs
430
+ // from the kernel's iopub channel. At checkpoint time this map
431
+ // is consulted to capture the execution counts for busy cells.
432
+ const trackerMap = new Map();
433
+ function ensureTracker(panel) {
434
+ let cellTracker = trackerMap.get(panel.id);
435
+ if (!cellTracker) {
436
+ cellTracker = new _restore_handler__WEBPACK_IMPORTED_MODULE_7__.CellExecutionTracker();
437
+ trackerMap.set(panel.id, cellTracker);
438
+ }
439
+ return cellTracker;
440
+ }
441
+ function connectTrackerToKernel(panel) {
442
+ var _a;
443
+ const kernel = (_a = panel.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel;
444
+ if (kernel) {
445
+ ensureTracker(panel).connectToKernel(kernel);
446
+ }
447
+ }
448
+ // ── Command ──────────────────────────────────────────────────────
412
449
  app.commands.addCommand(COMMAND_ID, {
413
450
  label: 'Saving Points',
414
451
  caption: 'Manage kernel checkpoints',
@@ -451,7 +488,14 @@ const plugin = {
451
488
  }
452
489
  const kernelId = kernel.id;
453
490
  const kernelSpecName = kernel.name || 'python_kubernetes';
454
- const onRestore = async (checkpointName, checkpointFilePath, containerName) => {
491
+ const notebookName = panel.title.label || '';
492
+ // ── Restore callback ─────────────────────────────────────────
493
+ //
494
+ // Called by the CheckpointPanel when the user confirms a restore.
495
+ // Creates a new kernel seeded with the checkpoint environment
496
+ // variables, switches the notebook session to it, and then
497
+ // activates the RestoreHandler to catch orphaned iopub messages.
498
+ const onRestore = async (checkpointName, checkpointFilePath, containerName, cpKernelId, busyCells) => {
455
499
  const settings = _jupyterlab_services__WEBPACK_IMPORTED_MODULE_3__.ServerConnection.makeSettings();
456
500
  const kernelUrl = _jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2__.URLExt.join(settings.baseUrl, 'api', 'kernels');
457
501
  const response = await _jupyterlab_services__WEBPACK_IMPORTED_MODULE_3__.ServerConnection.makeRequest(kernelUrl, {
@@ -462,7 +506,8 @@ const plugin = {
462
506
  env: {
463
507
  KERNEL_CHECKPOINT_NAME: checkpointName,
464
508
  KERNEL_CHECKPOINT_FILE_PATH: checkpointFilePath,
465
- KERNEL_CHECKPOINT_CONTAINER_NAME: containerName
509
+ KERNEL_CHECKPOINT_CONTAINER_NAME: containerName,
510
+ KERNEL_ID: cpKernelId
466
511
  }
467
512
  })
468
513
  }, settings);
@@ -472,12 +517,37 @@ const plugin = {
472
517
  }
473
518
  const kernelData = await response.json();
474
519
  await sessionContext.changeKernel({ id: kernelData.id });
520
+ // ── Activate Catch-and-Render (Steps 3-6) ──────────────────
521
+ //
522
+ // The restored kernel is now connected. If there were busy
523
+ // cells at checkpoint time, spin up a RestoreHandler that
524
+ // hooks iopub and intercepts orphaned messages.
525
+ if (busyCells.length > 0) {
526
+ console.log(`[kernel-checkpoint] Restore detected ${busyCells.length} ` +
527
+ 'busy cell(s). Activating RestoreHandler.');
528
+ const handler = new _restore_handler__WEBPACK_IMPORTED_MODULE_7__.RestoreHandler();
529
+ handler.hookRestoredKernel(panel, busyCells);
530
+ }
475
531
  };
532
+ // ── Dialog construction ──────────────────────────────────────
476
533
  const body = new CheckpointDialogBody({
477
534
  namespace: config.namespace,
478
535
  kernelId,
479
536
  kernelSpecName,
480
- onRestore
537
+ notebookName,
538
+ onRestore,
539
+ onBeforeCreate: () => {
540
+ const cellTracker = trackerMap.get(panel.id);
541
+ if (!cellTracker) {
542
+ return [];
543
+ }
544
+ const busyCells = (0,_restore_handler__WEBPACK_IMPORTED_MODULE_7__.scanBusyCells)(panel, cellTracker);
545
+ if (busyCells.length > 0) {
546
+ console.log(`[kernel-checkpoint] Scanned ${busyCells.length} busy cell(s) ` +
547
+ 'before checkpoint:', busyCells);
548
+ }
549
+ return busyCells;
550
+ }
481
551
  });
482
552
  await (0,_jupyterlab_apputils__WEBPACK_IMPORTED_MODULE_1__.showDialog)({
483
553
  title: 'Kernel Checkpoints',
@@ -487,8 +557,9 @@ const plugin = {
487
557
  body.dispose();
488
558
  }
489
559
  });
490
- // Inject a toolbar button into every notebook panel
560
+ // ── Notebook lifecycle wiring ────────────────────────────────────
491
561
  notebookTracker.widgetAdded.connect((_sender, panel) => {
562
+ // Toolbar button
492
563
  const button = new _jupyterlab_apputils__WEBPACK_IMPORTED_MODULE_1__.ToolbarButton({
493
564
  label: 'Saving Points',
494
565
  tooltip: 'Open kernel checkpoint manager',
@@ -497,13 +568,374 @@ const plugin = {
497
568
  }
498
569
  });
499
570
  panel.toolbar.insertItem(10, 'kernel-checkpoint', button);
571
+ // Execution tracker setup — connect once the session is ready,
572
+ // and reconnect whenever the kernel is swapped.
573
+ panel.sessionContext.ready.then(() => {
574
+ connectTrackerToKernel(panel);
575
+ });
576
+ panel.sessionContext.kernelChanged.connect(() => {
577
+ connectTrackerToKernel(panel);
578
+ });
579
+ panel.disposed.connect(() => {
580
+ const cellTracker = trackerMap.get(panel.id);
581
+ if (cellTracker) {
582
+ cellTracker.disconnectFromKernel();
583
+ trackerMap.delete(panel.id);
584
+ }
585
+ });
500
586
  });
501
587
  }
502
588
  };
503
589
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (plugin);
504
590
 
505
591
 
592
+ /***/ },
593
+
594
+ /***/ "./lib/restore-handler.js"
595
+ /*!********************************!*\
596
+ !*** ./lib/restore-handler.js ***!
597
+ \********************************/
598
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
599
+
600
+ __webpack_require__.r(__webpack_exports__);
601
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
602
+ /* harmony export */ CellExecutionTracker: () => (/* binding */ CellExecutionTracker),
603
+ /* harmony export */ RestoreHandler: () => (/* binding */ RestoreHandler),
604
+ /* harmony export */ loadCheckpointCellMetadata: () => (/* binding */ loadCheckpointCellMetadata),
605
+ /* harmony export */ saveCheckpointCellMetadata: () => (/* binding */ saveCheckpointCellMetadata),
606
+ /* harmony export */ scanBusyCells: () => (/* binding */ scanBusyCells)
607
+ /* harmony export */ });
608
+ /**
609
+ * Safely extracts `msg_id` from a kernel message's `parent_header`.
610
+ * Returns undefined when the parent_header is empty (e.g. unsolicited
611
+ * kernel-status broadcasts at startup).
612
+ */
613
+ function getParentMsgId(msg) {
614
+ const ph = msg.parent_header;
615
+ return ph && 'msg_id' in ph
616
+ ? ph.msg_id
617
+ : undefined;
618
+ }
619
+ // ─────────────────────────────────────────────────────────────────────────────
620
+ // Mock save / load helpers.
621
+ // These demonstrate the checkpoint-metadata persistence contract.
622
+ // Replace the bodies with real API calls (e.g. CheckpointAPI.updateCheckpoint)
623
+ // when wiring to your backend.
624
+ // ─────────────────────────────────────────────────────────────────────────────
625
+ async function saveCheckpointCellMetadata(_checkpointName, _namespace, busyCells) {
626
+ console.log(`[RestoreHandler] Would persist ${busyCells.length} busy-cell record(s)`, busyCells);
627
+ }
628
+ async function loadCheckpointCellMetadata(_checkpointName, _namespace) {
629
+ console.log('[RestoreHandler] Would load busy-cell records from backend');
630
+ return [];
631
+ }
632
+ // ─────────────────────────────────────────────────────────────────────────────
633
+ // CellExecutionTracker
634
+ // ─────────────────────────────────────────────────────────────────────────────
635
+ /**
636
+ * Continuously monitors a kernel's iopub channel to maintain a live map of
637
+ * **in-flight** execute requests: `msg_id → execution_count`.
638
+ *
639
+ * When the kernel broadcasts `execute_input`, the tracker records the
640
+ * execution count assigned by the kernel (this is the number that will
641
+ * eventually be shown as `[N]` in the cell prompt). When `status: idle`
642
+ * arrives for that same `msg_id`, the entry is removed.
643
+ *
644
+ * The tracker must be connected to the kernel **before** any cells are
645
+ * executed so that the execution count is available at checkpoint time.
646
+ */
647
+ class CellExecutionTracker {
648
+ constructor() {
649
+ this._pendingExecutions = new Map();
650
+ this._kernel = null;
651
+ }
652
+ get pendingExecutions() {
653
+ return this._pendingExecutions;
654
+ }
655
+ connectToKernel(kernel) {
656
+ this.disconnectFromKernel();
657
+ this._kernel = kernel;
658
+ kernel.iopubMessage.connect(this._onIopubMessage, this);
659
+ }
660
+ disconnectFromKernel() {
661
+ if (this._kernel) {
662
+ this._kernel.iopubMessage.disconnect(this._onIopubMessage, this);
663
+ this._kernel = null;
664
+ }
665
+ this._pendingExecutions.clear();
666
+ }
667
+ _onIopubMessage(_sender, msg) {
668
+ const parentMsgId = getParentMsgId(msg);
669
+ if (!parentMsgId) {
670
+ return;
671
+ }
672
+ if (msg.header.msg_type === 'execute_input') {
673
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
674
+ const execCount = msg.content.execution_count;
675
+ this._pendingExecutions.set(parentMsgId, execCount);
676
+ }
677
+ if (msg.header.msg_type === 'status' &&
678
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
679
+ msg.content.execution_state === 'idle') {
680
+ this._pendingExecutions.delete(parentMsgId);
681
+ }
682
+ }
683
+ }
684
+ // ─────────────────────────────────────────────────────────────────────────────
685
+ // scanBusyCells (Step 1 of the Catch-and-Render architecture)
686
+ // ─────────────────────────────────────────────────────────────────────────────
687
+ /**
688
+ * Scans the active notebook for cells that are currently executing (`[*]`
689
+ * prompt) and extracts the `msg_id` of each in-flight execution request.
690
+ *
691
+ * ### How the msg_id is obtained
692
+ *
693
+ * When JupyterLab executes a cell it calls `kernel.requestExecute()` and
694
+ * stores the resulting `IShellFuture` on the cell's `OutputArea`:
695
+ *
696
+ * `codeCell.outputArea.future = kernel.requestExecute({ code })`
697
+ *
698
+ * JupyterLab exposes `future` as a **write-only** setter on `OutputArea`.
699
+ * The underlying field is `OutputArea._future` (private). We read it here
700
+ * because no public getter exists, and the `msg_id` it contains is the only
701
+ * reliable link between the cell widget and the kernel message stream.
702
+ *
703
+ * ### Why we also need the CellExecutionTracker
704
+ *
705
+ * The `_future.msg.header.msg_id` gives us the execute-request id, but not
706
+ * the **execution count** the kernel assigned (the `[N]` prompt number).
707
+ * During execution the cell's `model.executionCount` is `null` (that is
708
+ * what produces the `[*]` display). The actual count was broadcast earlier
709
+ * in an `execute_input` iopub message which the `CellExecutionTracker`
710
+ * recorded. We look it up here so that the restore phase can set the
711
+ * correct prompt after the cell finishes.
712
+ */
713
+ function scanBusyCells(panel, tracker) {
714
+ var _a, _b, _c;
715
+ const records = [];
716
+ const cells = panel.content.widgets;
717
+ for (let i = 0; i < cells.length; i++) {
718
+ const cell = cells[i];
719
+ if (cell.model.type !== 'code') {
720
+ continue;
721
+ }
722
+ const codeCell = cell;
723
+ // `executionCount === null` is the canonical indicator for a busy cell
724
+ // (the prompt renders `[*]`). Fresh cells also have `null`, so we
725
+ // additionally require an active future below.
726
+ if (codeCell.model.executionCount !== null) {
727
+ continue;
728
+ }
729
+ // Read the private `_future` field. This is an `IShellFuture` whose
730
+ // `.msg.header.msg_id` is the id the kernel uses in `parent_header`
731
+ // for every message belonging to this execution.
732
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
733
+ const future = codeCell.outputArea._future;
734
+ if (!((_b = (_a = future === null || future === void 0 ? void 0 : future.msg) === null || _a === void 0 ? void 0 : _a.header) === null || _b === void 0 ? void 0 : _b.msg_id)) {
735
+ continue;
736
+ }
737
+ const msgId = future.msg.header.msg_id;
738
+ const executionCount = (_c = tracker.pendingExecutions.get(msgId)) !== null && _c !== void 0 ? _c : null;
739
+ records.push({
740
+ cellIndex: i,
741
+ cellId: cell.model.id,
742
+ msgId,
743
+ executionCount
744
+ });
745
+ }
746
+ return records;
747
+ }
748
+ // ─────────────────────────────────────────────────────────────────────────────
749
+ // RestoreHandler (Steps 3–6 of the Catch-and-Render architecture)
750
+ // ─────────────────────────────────────────────────────────────────────────────
751
+ /**
752
+ * Hooks into a **restored** kernel's iopub channel and intercepts "orphaned"
753
+ * messages whose `parent_header.msg_id` matches a cell that was busy at
754
+ * checkpoint time.
755
+ *
756
+ * For each intercepted message the handler manually injects the output into
757
+ * the cell's `OutputArea`, replicating what JupyterLab would normally do
758
+ * if the original execution future were still alive.
759
+ *
760
+ * ### Output injection mechanism
761
+ *
762
+ * `codeCell.outputArea.model` implements `IOutputAreaModel`. Calling
763
+ * `.add(output)` with an `nbformat.IOutput`-shaped object triggers the
764
+ * following internal chain:
765
+ *
766
+ * 1. The model wraps the raw JSON in an `OutputModel` instance.
767
+ * 2. It appends the model to its internal list.
768
+ * – For `stream` outputs with the same `name` (stdout / stderr) as the
769
+ * preceding entry, the text is **merged** instead of creating a new
770
+ * item. This matches standard notebook behavior.
771
+ * 3. A Lumino `changed` signal fires.
772
+ * 4. The `OutputArea` widget reacts and creates the visual renderer
773
+ * (e.g. `<pre>` for streams, a MIME renderer for display_data).
774
+ *
775
+ * Because all of this happens through the model's public API the rendered
776
+ * output is indistinguishable from one produced by the normal future-based
777
+ * routing.
778
+ */
779
+ class RestoreHandler {
780
+ constructor() {
781
+ this._activeMsgIds = new Map();
782
+ this._panel = null;
783
+ this._kernel = null;
784
+ }
785
+ /**
786
+ * Begin intercepting orphaned messages for the given busy cells.
787
+ * Call immediately after `sessionContext.changeKernel()` resolves.
788
+ */
789
+ hookRestoredKernel(panel, busyCells) {
790
+ var _a;
791
+ if (busyCells.length === 0) {
792
+ return;
793
+ }
794
+ this.dispose();
795
+ this._panel = panel;
796
+ for (const record of busyCells) {
797
+ this._activeMsgIds.set(record.msgId, record);
798
+ }
799
+ // Re-apply the busy indicator ([*]) so the user sees the cells as
800
+ // still running while output continues to stream in.
801
+ for (const record of busyCells) {
802
+ const cell = this._findCell(panel, record);
803
+ if (cell) {
804
+ cell.model.executionCount = null;
805
+ }
806
+ }
807
+ const kernel = (_a = panel.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel;
808
+ if (!kernel) {
809
+ console.warn('[RestoreHandler] No kernel on panel after changeKernel(); ' +
810
+ 'cannot hook iopub. Orphaned outputs will be lost.');
811
+ return;
812
+ }
813
+ this._kernel = kernel;
814
+ kernel.iopubMessage.connect(this._onIopubMessage, this);
815
+ console.log(`[RestoreHandler] Hooked into restored kernel iopub. ` +
816
+ `Tracking ${busyCells.length} busy cell(s):`, busyCells.map(r => `cell[${r.cellIndex}] (${r.cellId}) → ${r.msgId}`));
817
+ }
818
+ dispose() {
819
+ if (this._kernel) {
820
+ this._kernel.iopubMessage.disconnect(this._onIopubMessage, this);
821
+ this._kernel = null;
822
+ }
823
+ this._activeMsgIds.clear();
824
+ this._panel = null;
825
+ }
826
+ /**
827
+ * Locate the CodeCell for a given record. We first try the saved index
828
+ * (fast path) and verify by cell ID, then fall back to a linear scan.
829
+ * This handles the case where the user inserted or deleted cells between
830
+ * checkpoint and restore.
831
+ */
832
+ _findCell(panel, record) {
833
+ const cells = panel.content.widgets;
834
+ if (record.cellIndex < cells.length) {
835
+ const candidate = cells[record.cellIndex];
836
+ if (candidate.model.type === 'code' &&
837
+ candidate.model.id === record.cellId) {
838
+ return candidate;
839
+ }
840
+ }
841
+ for (let i = 0; i < cells.length; i++) {
842
+ const c = cells[i];
843
+ if (c.model.id === record.cellId && c.model.type === 'code') {
844
+ return c;
845
+ }
846
+ }
847
+ return null;
848
+ }
849
+ // ── iopub signal handler ──────────────────────────────────────────────
850
+ _onIopubMessage(_sender, msg) {
851
+ const parentMsgId = getParentMsgId(msg);
852
+ if (!parentMsgId) {
853
+ return;
854
+ }
855
+ const record = this._activeMsgIds.get(parentMsgId);
856
+ if (!record) {
857
+ // Not one of our tracked orphaned executions — let JupyterLab's
858
+ // normal future-based routing handle this message.
859
+ return;
860
+ }
861
+ if (!this._panel) {
862
+ return;
863
+ }
864
+ const codeCell = this._findCell(this._panel, record);
865
+ if (!codeCell) {
866
+ return;
867
+ }
868
+ const msgType = msg.header.msg_type;
869
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
870
+ const content = msg.content;
871
+ // ── Step 5: Inject output into the cell's OutputArea ────────────
872
+ //
873
+ // We construct an nbformat-compatible output object and pass it to
874
+ // `outputArea.model.add()`. The model validates the shape, wraps it
875
+ // in an OutputModel, and emits a `changed` signal that causes the
876
+ // OutputArea widget to render the new entry.
877
+ switch (msgType) {
878
+ case 'stream':
879
+ codeCell.outputArea.model.add({
880
+ output_type: 'stream',
881
+ name: content.name,
882
+ text: content.text
883
+ });
884
+ break;
885
+ case 'error':
886
+ codeCell.outputArea.model.add({
887
+ output_type: 'error',
888
+ ename: content.ename,
889
+ evalue: content.evalue,
890
+ traceback: content.traceback
891
+ });
892
+ break;
893
+ case 'execute_result':
894
+ codeCell.outputArea.model.add({
895
+ output_type: 'execute_result',
896
+ data: content.data,
897
+ metadata: content.metadata,
898
+ execution_count: content.execution_count
899
+ });
900
+ break;
901
+ case 'display_data':
902
+ codeCell.outputArea.model.add({
903
+ output_type: 'display_data',
904
+ data: content.data,
905
+ metadata: content.metadata
906
+ });
907
+ break;
908
+ // ── Step 6: Lifecycle Cleanup ─────────────────────────────────
909
+ //
910
+ // A `status: idle` message with our tracked msg_id means the
911
+ // kernel finished executing this cell. We:
912
+ // a) Set the cell's execution count → prompt changes [*] → [N]
913
+ // b) Remove the msg_id from the tracking map
914
+ // c) Disconnect from iopub when no busy cells remain
915
+ case 'status':
916
+ if (content.execution_state === 'idle') {
917
+ if (record.executionCount !== null) {
918
+ codeCell.model.executionCount = record.executionCount;
919
+ }
920
+ this._activeMsgIds.delete(parentMsgId);
921
+ console.log(`[RestoreHandler] Cell[${record.cellIndex}] execution ` +
922
+ `complete (exec count: ${record.executionCount}). ` +
923
+ `${this._activeMsgIds.size} cell(s) still pending.`);
924
+ if (this._activeMsgIds.size === 0) {
925
+ console.log('[RestoreHandler] All restored cells complete. ' +
926
+ 'Disconnecting iopub hook.');
927
+ this.dispose();
928
+ }
929
+ }
930
+ break;
931
+ default:
932
+ break;
933
+ }
934
+ }
935
+ }
936
+
937
+
506
938
  /***/ }
507
939
 
508
940
  }]);
509
- //# sourceMappingURL=lib_index_js.3a33b915212dd7a87b4f.js.map
941
+ //# sourceMappingURL=lib_index_js.6dc5fa8b4a9bc62a809d.js.map