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