kernel-checkpoint 0.1.7__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.7 → kernel_checkpoint-0.1.8}/PKG-INFO +1 -1
  2. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/jupyter-notebook-image/Dockerfile +1 -1
  3. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/_version.py +1 -1
  4. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/build_log.json +1 -1
  5. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/package.json +2 -2
  6. kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/lib_index_js.3571ff1d8cbf7ea37b2e.js → kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/lib_index_js.6dc5fa8b4a9bc62a809d.js +429 -9
  7. kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/lib_index_js.6dc5fa8b4a9bc62a809d.js.map +1 -0
  8. kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/remoteEntry.900312eadd6aa5546ed7.js → kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/remoteEntry.5ffc549e28b042648bca.js +3 -3
  9. kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/remoteEntry.900312eadd6aa5546ed7.js.map → kernel_checkpoint-0.1.8/kernel_checkpoint/labextension/static/remoteEntry.5ffc549e28b042648bca.js.map +1 -1
  10. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/package.json +1 -1
  11. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/src/checkpoint-panel.tsx +13 -3
  12. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/src/index.ts +102 -4
  13. kernel_checkpoint-0.1.8/src/restore-handler.ts +418 -0
  14. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/src/types.ts +21 -1
  15. kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/lib_index_js.3571ff1d8cbf7ea37b2e.js.map +0 -1
  16. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/.copier-answers.yml +0 -0
  17. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/.gitignore +0 -0
  18. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/.prettierignore +0 -0
  19. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/.yarnrc.yml +0 -0
  20. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/4 +0 -0
  21. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/=4 +0 -0
  22. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/CHANGELOG.md +0 -0
  23. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/LICENSE +0 -0
  24. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/README.md +0 -0
  25. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/RELEASE.md +0 -0
  26. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/babel.config.js +0 -0
  27. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/cursor_jupyterlab_extension_for_checkpo.md +0 -0
  28. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/install.json +0 -0
  29. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/jest.config.js +0 -0
  30. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/jupyter-config/jupyter_server_config.d/kernel_checkpoint.json +0 -0
  31. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/Untitled.ipynb +0 -0
  32. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/__init__.py +0 -0
  33. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/handlers.py +0 -0
  34. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/static/style.js +0 -0
  35. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/static/style_index_js.c3e72438ed03e9dee0fc.js +0 -0
  36. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/kernel_checkpoint/labextension/static/style_index_js.c3e72438ed03e9dee0fc.js.map +0 -0
  37. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/pyproject.toml +0 -0
  38. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/setup.py +0 -0
  39. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/src/__tests__/kernel_checkpoint.spec.ts +0 -0
  40. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/src/api.ts +0 -0
  41. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/style/base.css +0 -0
  42. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/style/index.css +0 -0
  43. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/style/index.js +0 -0
  44. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/tsconfig.json +0 -0
  45. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/tsconfig.test.json +0 -0
  46. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/README.md +0 -0
  47. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/jupyter_server_test_config.py +0 -0
  48. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/package.json +0 -0
  49. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/playwright.config.js +0 -0
  50. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/tests/kernel_checkpoint.spec.ts +0 -0
  51. {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.8}/ui-tests/yarn.lock +0 -0
  52. {kernel_checkpoint-0.1.7 → 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.7
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.6
7
+ RUN pip install --no-cache-dir kernel-checkpoint==0.1.7
8
8
 
9
9
  USER jovyan
@@ -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.7'
4
+ __version__ = VERSION = '0.1.8'
@@ -700,7 +700,7 @@
700
700
  "singleton": true
701
701
  },
702
702
  "kernel-checkpoint": {
703
- "version": "0.1.6",
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.6",
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.900312eadd6aa5546ed7.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, kernelSpecName, notebookName, 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,6 +170,7 @@ 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,
@@ -178,7 +179,8 @@ function CheckpointPanel(props) {
178
179
  metadata: {
179
180
  kernelId,
180
181
  kernelName: kernelSpecName,
181
- notebookName
182
+ notebookName,
183
+ busyCells
182
184
  }
183
185
  });
184
186
  setNewName('');
@@ -236,7 +238,7 @@ function CheckpointPanel(props) {
236
238
  }
237
239
  };
238
240
  const handleRestore = async (checkpointName) => {
239
- var _a, _b, _c, _d;
241
+ var _a, _b, _c, _d, _e, _f;
240
242
  setConfirmRestore(null);
241
243
  setRestoring(checkpointName);
242
244
  setFlash(null);
@@ -250,7 +252,8 @@ function CheckpointPanel(props) {
250
252
  if (!cpKernelId) {
251
253
  throw new Error('No kernel ID found in checkpoint metadata');
252
254
  }
253
- await onRestore(checkpointName, checkpointFile.storagePath, checkpointFile.containerName, cpKernelId);
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);
254
257
  setFlash({
255
258
  type: 'success',
256
259
  text: `Kernel restored from "${checkpointName}". The kernel is restarting.`
@@ -259,7 +262,7 @@ function CheckpointPanel(props) {
259
262
  catch (err) {
260
263
  setFlash({
261
264
  type: 'error',
262
- text: (_d = err.message) !== null && _d !== void 0 ? _d : 'Failed to restore checkpoint'
265
+ text: (_f = err.message) !== null && _f !== void 0 ? _f : 'Failed to restore checkpoint'
263
266
  });
264
267
  }
265
268
  finally {
@@ -389,6 +392,8 @@ __webpack_require__.r(__webpack_exports__);
389
392
  /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_4__);
390
393
  /* harmony import */ var _checkpoint_panel__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./checkpoint-panel */ "./lib/checkpoint-panel.js");
391
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
+
392
397
 
393
398
 
394
399
 
@@ -418,6 +423,29 @@ const plugin = {
418
423
  requires: [_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__.INotebookTracker],
419
424
  activate: (app, notebookTracker) => {
420
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 ──────────────────────────────────────────────────────
421
449
  app.commands.addCommand(COMMAND_ID, {
422
450
  label: 'Saving Points',
423
451
  caption: 'Manage kernel checkpoints',
@@ -461,7 +489,13 @@ const plugin = {
461
489
  const kernelId = kernel.id;
462
490
  const kernelSpecName = kernel.name || 'python_kubernetes';
463
491
  const notebookName = panel.title.label || '';
464
- const onRestore = async (checkpointName, checkpointFilePath, containerName, cpKernelId) => {
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) => {
465
499
  const settings = _jupyterlab_services__WEBPACK_IMPORTED_MODULE_3__.ServerConnection.makeSettings();
466
500
  const kernelUrl = _jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2__.URLExt.join(settings.baseUrl, 'api', 'kernels');
467
501
  const response = await _jupyterlab_services__WEBPACK_IMPORTED_MODULE_3__.ServerConnection.makeRequest(kernelUrl, {
@@ -483,13 +517,37 @@ const plugin = {
483
517
  }
484
518
  const kernelData = await response.json();
485
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
+ }
486
531
  };
532
+ // ── Dialog construction ──────────────────────────────────────
487
533
  const body = new CheckpointDialogBody({
488
534
  namespace: config.namespace,
489
535
  kernelId,
490
536
  kernelSpecName,
491
537
  notebookName,
492
- onRestore
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
+ }
493
551
  });
494
552
  await (0,_jupyterlab_apputils__WEBPACK_IMPORTED_MODULE_1__.showDialog)({
495
553
  title: 'Kernel Checkpoints',
@@ -499,8 +557,9 @@ const plugin = {
499
557
  body.dispose();
500
558
  }
501
559
  });
502
- // Inject a toolbar button into every notebook panel
560
+ // ── Notebook lifecycle wiring ────────────────────────────────────
503
561
  notebookTracker.widgetAdded.connect((_sender, panel) => {
562
+ // Toolbar button
504
563
  const button = new _jupyterlab_apputils__WEBPACK_IMPORTED_MODULE_1__.ToolbarButton({
505
564
  label: 'Saving Points',
506
565
  tooltip: 'Open kernel checkpoint manager',
@@ -509,13 +568,374 @@ const plugin = {
509
568
  }
510
569
  });
511
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
+ });
512
586
  });
513
587
  }
514
588
  };
515
589
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (plugin);
516
590
 
517
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
+
518
938
  /***/ }
519
939
 
520
940
  }]);
521
- //# sourceMappingURL=lib_index_js.3571ff1d8cbf7ea37b2e.js.map
941
+ //# sourceMappingURL=lib_index_js.6dc5fa8b4a9bc62a809d.js.map