@tlockney/cdp-kernel 0.7.2 → 0.9.0

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.
package/README.md CHANGED
@@ -7,7 +7,7 @@ runtime: Node or Deno over the Chrome DevTools Protocol, or Bun, workerd and
7
7
  celld, which evaluate cells in-process. In principle, anything else whose
8
8
  inspector speaks CDP works too. The kernel can also run inside a web page,
9
9
  with cells in a Web Worker and no server
10
- ([The browser](https://cdp-kernel.org/docs/integration-browser), experimental).
10
+ ([The browser](https://cdp-kernel.org/docs/integration-browser)).
11
11
 
12
12
  **Website:** <https://cdp-kernel.org/> ·
13
13
  **Docs:** [cdp-kernel.org/docs](https://cdp-kernel.org/docs/)
@@ -87,7 +87,9 @@ New to Jupyter? Start with
87
87
  [Choosing a runtime](https://cdp-kernel.org/docs/runtimes),
88
88
  [Widgets and comms](https://cdp-kernel.org/docs/widgets),
89
89
  [Debugger](https://cdp-kernel.org/docs/debugger)
90
- - Reference: [Editor types](https://cdp-kernel.org/docs/editor-types),
90
+ - Reference: [`jupyter` reference](https://cdp-kernel.org/docs/jupyter-reference),
91
+ [Widgets and comms reference](https://cdp-kernel.org/docs/widgets-reference),
92
+ [Editor types](https://cdp-kernel.org/docs/editor-types),
91
93
  [Known gaps](https://cdp-kernel.org/docs/known-gaps),
92
94
  [Support policy](https://cdp-kernel.org/docs/support)
93
95
  - Internals: [Architecture](https://cdp-kernel.org/docs/architecture),
@@ -28199,7 +28199,7 @@ function rewriteImport(code, node, index) {
28199
28199
  if (spec.type === "ImportNamespaceSpecifier") return `${local} = ${ns}`;
28200
28200
  return `${local} = ${ns}[${JSON.stringify(importedName(spec))}]`;
28201
28201
  });
28202
- return `var ${ns} = ${load}; var ${bindings.join(", ")};`;
28202
+ return `var ${ns} = ${load}; let ${bindings.join(", ")};`;
28203
28203
  }
28204
28204
  function parseCell(js) {
28205
28205
  try {
@@ -28216,6 +28216,12 @@ function parseCell(js) {
28216
28216
  function rewriteModuleSyntax(js) {
28217
28217
  const program = parseCell(js);
28218
28218
  const edits = [];
28219
+ for (const statement of program.body) {
28220
+ const decl = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
28221
+ if (decl?.type === "VariableDeclaration" && decl.kind === "const") {
28222
+ edits.push({ start: decl.start, end: decl.start + "const".length, text: "let " });
28223
+ }
28224
+ }
28219
28225
  let importIndex = 0;
28220
28226
  full(program, (node) => {
28221
28227
  switch (node.type) {
@@ -28376,7 +28382,7 @@ var init_transform = __esm({
28376
28382
  SHIM = "__cdpk";
28377
28383
  YIELD = `await ${SHIM}.tick();
28378
28384
  `;
28379
- PRELUDE = `'use strict'; ${YIELD}`;
28385
+ PRELUDE = `'use strict'; ${SHIM}.startCell(); ${YIELD}`;
28380
28386
  SETTLE = `
28381
28387
  ;let ${SHIM}_settled = await ${SHIM}.tick();`;
28382
28388
  CellSyntaxError = class extends Error {
@@ -28388,7 +28394,7 @@ var init_transform = __esm({
28388
28394
  });
28389
28395
 
28390
28396
  // src/message.ts
28391
- var PROTOCOL_VERSION = "5.3";
28397
+ var PROTOCOL_VERSION = "5.5";
28392
28398
  function makeHeader(session, msgType) {
28393
28399
  return {
28394
28400
  msg_id: crypto.randomUUID(),
@@ -28758,12 +28764,43 @@ function clip(frames) {
28758
28764
 
28759
28765
  // src/kernel-core.ts
28760
28766
  var TYPESCRIPT_VERSION = "6";
28767
+ var launchFailures = /* @__PURE__ */ new WeakSet();
28761
28768
  function fromBase64(text) {
28762
28769
  return Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
28763
28770
  }
28764
28771
  var INTERRUPT_REPLY_WAIT_MS = 1e3;
28765
28772
  var NO_INPUT = { value: null, interrupted: false };
28773
+ var ShellQueue = class {
28774
+ items = [];
28775
+ #wake = null;
28776
+ #closed = false;
28777
+ push(item) {
28778
+ if (this.#closed) return;
28779
+ this.items.push(item);
28780
+ this.#wake?.();
28781
+ }
28782
+ /** The next message; null once closed. */
28783
+ async next() {
28784
+ for (; ; ) {
28785
+ if (this.#closed) return null;
28786
+ const item = this.items.shift();
28787
+ if (item !== void 0) return item;
28788
+ await new Promise((resolve) => this.#wake = resolve);
28789
+ this.#wake = null;
28790
+ }
28791
+ }
28792
+ /** Stops taking messages; returns those still queued. */
28793
+ close() {
28794
+ this.#closed = true;
28795
+ const dropped = this.items.splice(0);
28796
+ this.#wake?.();
28797
+ return dropped;
28798
+ }
28799
+ };
28766
28800
  var MAX_CELLS = 1e3;
28801
+ function subshellNotFound(evalue) {
28802
+ return { status: "error", ename: "SubshellNotFound", evalue, traceback: [`SubshellNotFound: ${evalue}`] };
28803
+ }
28767
28804
  var SHELL_REPLIES = {
28768
28805
  kernel_info_request: "kernel_info_reply",
28769
28806
  complete_request: "complete_reply",
@@ -28815,13 +28852,16 @@ var KernelCore = class {
28815
28852
  #executionCount = 0;
28816
28853
  #parent = {};
28817
28854
  #allowStdin = false;
28818
- #executing = false;
28819
- /**
28820
- * stop_on_error (#123): how many of the shell messages queued when a cell
28821
- * failed are still to come, and whether the one being handled is one.
28822
- */
28823
- #abortQueued = 0;
28855
+ /** Cells and comm handlers running in the target. */
28856
+ #running = 0;
28857
+ /** A cell is running: its parent header and allow_stdin hold until it ends (#417). */
28858
+ #cellRunning = false;
28859
+ /** stop_on_error (#123): the message being handled was queued behind a failed cell. */
28824
28860
  #aborting = false;
28861
+ /** Messages without a subshell_id, in the order they came. */
28862
+ #mainShell = new ShellQueue();
28863
+ /** Kernel subshells (JEP 91, #417): subshell_id -> its queue. */
28864
+ #subshells = /* @__PURE__ */ new Map();
28825
28865
  /** The input_request waiting for its input_reply, if any. */
28826
28866
  #input = null;
28827
28867
  /** An interrupt is waiting on the target. */
@@ -28845,8 +28885,10 @@ var KernelCore = class {
28845
28885
  async run() {
28846
28886
  this.#ensureTarget().catch((err) => this.#log(`target failed to start: ${err}`));
28847
28887
  if (typeof this.#typeStrip === "string") await loadTypeStripper(this.#typeStrip);
28888
+ await this.#channels.ready;
28848
28889
  await Promise.all([
28849
- this.#loop("shell", (m, p) => this.#handleShell(m, p)),
28890
+ this.#readShell(),
28891
+ this.#runShell(this.#mainShell),
28850
28892
  this.#loop("control", (m, p) => this.#handleControl(m, p)),
28851
28893
  this.#loop("stdin", (m) => this.#handleStdin(m))
28852
28894
  ]);
@@ -28870,8 +28912,9 @@ var KernelCore = class {
28870
28912
  if (target?.diagnose) targetState = await target.diagnose().catch((err) => `diagnose failed: ${err}`);
28871
28913
  this.#log(`state: ${JSON.stringify({
28872
28914
  shell: handling && { msgType: handling.msgType, msgId: handling.msgId, ms: Date.now() - handling.since },
28873
- queued: this.#channels.shellQueued(),
28874
- executing: this.#executing,
28915
+ queued: this.#mainShell.items.length + this.#channels.shellQueued(),
28916
+ subshells: this.#subshells.size,
28917
+ running: this.#running,
28875
28918
  interrupting: this.#interrupting,
28876
28919
  awaitingInput: this.#input !== null,
28877
28920
  target: targetState
@@ -28900,25 +28943,28 @@ var KernelCore = class {
28900
28943
  const type = msg.header.msg_type;
28901
28944
  const commId = String(msg.content.comm_id ?? "");
28902
28945
  const target = await this.#ensureTarget();
28903
- this.#parent = msg.header;
28904
- this.#allowStdin = false;
28905
- this.#executing = true;
28946
+ if (!this.#cellRunning) {
28947
+ this.#parent = msg.header;
28948
+ this.#allowStdin = false;
28949
+ }
28950
+ const parent = msg.header;
28906
28951
  let handled = false;
28952
+ this.#running++;
28907
28953
  try {
28908
28954
  const result = await target.dispatchComm(type, msg.content, msg.metadata, msg.buffers);
28909
28955
  if ("error" in result) {
28910
28956
  const { name, message, stack } = result.error;
28911
28957
  this.#publish("stream", { name: "stderr", text: `Error in ${type} handler: ${stack || `${name}: ${message}`}
28912
- ` });
28958
+ ` }, { parent });
28913
28959
  } else {
28914
28960
  handled = result.handled;
28915
28961
  }
28916
28962
  } catch (err) {
28917
28963
  if (!(err instanceof Interrupted)) throw err;
28918
28964
  this.#publish("stream", { name: "stderr", text: `${type} handler interrupted
28919
- ` });
28965
+ ` }, { parent });
28920
28966
  } finally {
28921
- this.#executing = false;
28967
+ this.#running--;
28922
28968
  }
28923
28969
  await target.flush().catch(() => {
28924
28970
  });
@@ -28926,7 +28972,7 @@ var KernelCore = class {
28926
28972
  if (handled) {
28927
28973
  this.#comms.set(commId, String(msg.content.target_name ?? ""));
28928
28974
  } else {
28929
- this.#publish("comm_close", { comm_id: commId, data: {} });
28975
+ this.#publish("comm_close", { comm_id: commId, data: {} }, { parent });
28930
28976
  }
28931
28977
  } else if (type === "comm_close") {
28932
28978
  this.#comms.delete(commId);
@@ -28942,6 +28988,7 @@ var KernelCore = class {
28942
28988
  return target;
28943
28989
  }, (err) => {
28944
28990
  this.#targetStarting = null;
28991
+ if (typeof err === "object" && err !== null) launchFailures.add(err);
28945
28992
  throw err;
28946
28993
  });
28947
28994
  return this.#targetStarting;
@@ -28961,10 +29008,18 @@ var KernelCore = class {
28961
29008
  this.#interruptStart = null;
28962
29009
  }
28963
29010
  }
29011
+ /** Tells the host of a fault in the kernel's own code; a target that failed to launch isn't one (#422). */
29012
+ #fault(err, where) {
29013
+ if (typeof err === "object" && err !== null && launchFailures.has(err)) return;
29014
+ this.#host.fault?.(err, where);
29015
+ }
28964
29016
  #targetEvents() {
28965
29017
  return {
28966
29018
  stream: (name, text) => this.#publish("stream", { name, text }),
28967
- message: (msg) => void this.#onTargetMessage(msg).catch((err) => this.#log(`error handling target message: ${err}`)),
29019
+ message: (msg) => void this.#onTargetMessage(msg).catch((err) => {
29020
+ this.#log(`error handling target message: ${err}`);
29021
+ this.#fault(err, "target message");
29022
+ }),
28968
29023
  exit: (code, note) => {
28969
29024
  this.#target = null;
28970
29025
  this.#types?.reset();
@@ -29007,7 +29062,7 @@ ${text}
29007
29062
  this.#interruptStart();
29008
29063
  return;
29009
29064
  }
29010
- if (!this.#target || !this.#executing) return;
29065
+ if (!this.#target || this.#running === 0) return;
29011
29066
  this.#interrupting = true;
29012
29067
  try {
29013
29068
  await this.#target.interrupt();
@@ -29037,19 +29092,70 @@ ${text}
29037
29092
  async #loop(channel, handle) {
29038
29093
  while (!this.#shuttingDown) {
29039
29094
  const received = await this.#channels.recv(channel);
29040
- if (channel === "shell") {
29041
- this.#aborting = this.#abortQueued > 0;
29042
- if (this.#aborting) this.#abortQueued--;
29095
+ if (received === null) continue;
29096
+ try {
29097
+ await handle(received.msg, received.peer);
29098
+ } catch (err) {
29099
+ this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
29100
+ this.#fault(err, `${channel} handler`);
29043
29101
  }
29102
+ }
29103
+ }
29104
+ /**
29105
+ * Takes every shell message as it comes and queues it for the main shell
29106
+ * or its subshell, so a subshell's message isn't stuck behind a running
29107
+ * cell (#417).
29108
+ */
29109
+ async #readShell() {
29110
+ while (!this.#shuttingDown) {
29111
+ const received = await this.#channels.recv("shell");
29044
29112
  if (received === null) continue;
29045
- const { peer, msg } = received;
29113
+ const subshellId = received.msg.header.subshell_id;
29114
+ if (typeof subshellId !== "string") {
29115
+ this.#mainShell.push({ ...received, abort: false });
29116
+ continue;
29117
+ }
29118
+ const queue = this.#subshells.get(subshellId);
29119
+ if (queue === void 0) {
29120
+ this.#refuseShell(received, `No subshell with id ${subshellId}`);
29121
+ } else if (received.msg.header.msg_type === "execute_request") {
29122
+ this.#mainShell.push({ ...received, abort: false });
29123
+ } else {
29124
+ queue.push({ ...received, abort: false });
29125
+ }
29126
+ }
29127
+ }
29128
+ /**
29129
+ * Answers a shell message for a subshell that doesn't exist, or was
29130
+ * deleted with the message queued, so the frontend doesn't wait on it.
29131
+ */
29132
+ #refuseShell({ peer, msg }, evalue) {
29133
+ const type = msg.header.msg_type;
29134
+ this.#log(`refusing ${type}: ${evalue}`);
29135
+ this.#status("busy", msg.header);
29136
+ const replyType = type === "execute_request" ? "execute_reply" : SHELL_REPLIES[type];
29137
+ if (replyType !== void 0) this.#reply("shell", peer, msg, replyType, subshellNotFound(evalue));
29138
+ if (type === "comm_open") this.#publish("comm_close", { comm_id: String(msg.content.comm_id ?? ""), data: {} }, { parent: msg.header });
29139
+ this.#status("idle", msg.header);
29140
+ }
29141
+ /** Handles one queue's messages in turn. */
29142
+ async #runShell(queue) {
29143
+ const main = queue === this.#mainShell;
29144
+ while (!this.#shuttingDown) {
29145
+ const queued = await queue.next();
29146
+ if (queued === null) return;
29147
+ const { peer, msg } = queued;
29046
29148
  try {
29047
- if (channel === "shell") this.#handling = { msgType: msg.header.msg_type, msgId: msg.header.msg_id, since: Date.now() };
29048
- await handle(msg, peer);
29149
+ if (main) {
29150
+ this.#aborting = queued.abort;
29151
+ this.#handling = { msgType: msg.header.msg_type, msgId: msg.header.msg_id, since: Date.now() };
29152
+ }
29153
+ await this.#handleShell(msg, peer);
29049
29154
  } catch (err) {
29050
29155
  this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
29156
+ this.#fault(err, "shell handler");
29051
29157
  } finally {
29052
- if (channel === "shell") this.#handling = null;
29158
+ if (main) this.#handling = null;
29053
29159
  }
29054
29160
  }
29055
29161
  }
@@ -29063,7 +29169,12 @@ ${text}
29063
29169
  language_info: {
29064
29170
  name: "typescript",
29065
29171
  version: this.#host.languageVersion ?? TYPESCRIPT_VERSION,
29066
- mimetype: "text/x.typescript",
29172
+ // JupyterLab's LSP adapter picks the language server by a language
29173
+ // it derives from this MIME type, not from name: it drops the type
29174
+ // and an "x-" prefix, so "text/x.typescript" became "x.typescript"
29175
+ // and matched no server (#400). This is CodeMirror's MIME type for
29176
+ // TypeScript and in jupyter-lsp's typescript-language-server spec.
29177
+ mimetype: "application/typescript",
29067
29178
  file_extension: ".ts",
29068
29179
  pygments_lexer: "typescript",
29069
29180
  codemirror_mode: "typescript",
@@ -29075,12 +29186,19 @@ ${text}
29075
29186
  debugger: this.#debugger !== null,
29076
29187
  // What current ipykernel advertises; JupyterLab itself goes by the
29077
29188
  // kernelspec's metadata.debugger.
29078
- supported_features: this.#debugger !== null ? ["debugger"] : [],
29189
+ supported_features: [...this.#debugger !== null ? ["debugger"] : [], "kernel subshells"],
29079
29190
  ...this.#host.capabilities ? { capabilities: this.#host.capabilities } : {}
29080
29191
  };
29081
29192
  }
29082
29193
  // --- control channel ------------------------------------------------------
29083
29194
  async #handleControl(msg, peer) {
29195
+ if (msg.header.msg_type === "usage_request") {
29196
+ void this.#handleControlRequest(msg, peer).catch((err) => this.#log(`usage_request failed: ${err}`));
29197
+ return;
29198
+ }
29199
+ return this.#handleControlRequest(msg, peer);
29200
+ }
29201
+ async #handleControlRequest(msg, peer) {
29084
29202
  this.#status("busy", msg.header);
29085
29203
  try {
29086
29204
  await this.#controlRequest("control", msg, peer);
@@ -29101,6 +29219,37 @@ ${text}
29101
29219
  ]);
29102
29220
  this.#reply(channel, peer, msg, "interrupt_reply", { status: "ok" });
29103
29221
  break;
29222
+ case "create_subshell_request": {
29223
+ const subshellId = crypto.randomUUID();
29224
+ const queue = new ShellQueue();
29225
+ this.#subshells.set(subshellId, queue);
29226
+ void this.#runShell(queue);
29227
+ this.#reply(channel, peer, msg, "create_subshell_reply", { status: "ok", subshell_id: subshellId });
29228
+ break;
29229
+ }
29230
+ case "delete_subshell_request": {
29231
+ const subshellId = String(msg.content.subshell_id ?? "");
29232
+ const queue = this.#subshells.get(subshellId);
29233
+ if (queue === void 0) {
29234
+ this.#reply(channel, peer, msg, "delete_subshell_reply", subshellNotFound(`No subshell with id ${subshellId}`));
29235
+ break;
29236
+ }
29237
+ this.#subshells.delete(subshellId);
29238
+ for (const dropped of queue.close()) this.#refuseShell(dropped, `Subshell ${subshellId} was deleted`);
29239
+ this.#reply(channel, peer, msg, "delete_subshell_reply", { status: "ok" });
29240
+ break;
29241
+ }
29242
+ case "list_subshell_request":
29243
+ this.#reply(channel, peer, msg, "list_subshell_reply", { status: "ok", subshell_id: [...this.#subshells.keys()] });
29244
+ break;
29245
+ case "usage_request": {
29246
+ const usage = this.#host.usage?.(this.#target).catch((err) => {
29247
+ this.#log(`usage_request failed: ${err}`);
29248
+ return {};
29249
+ });
29250
+ this.#reply(channel, peer, msg, "usage_reply", await usage ?? {});
29251
+ break;
29252
+ }
29104
29253
  case "debug_request": {
29105
29254
  let reply;
29106
29255
  if (this.#debugger === null) {
@@ -29375,7 +29524,8 @@ ${expression}
29375
29524
  let error = null;
29376
29525
  let userExpressions = {};
29377
29526
  let structured = null;
29378
- this.#executing = true;
29527
+ this.#running++;
29528
+ this.#cellRunning = true;
29379
29529
  try {
29380
29530
  const target = await this.#targetForCell();
29381
29531
  if (target.notice !== void 0 && this.#noticeShownFor !== target) {
@@ -29416,15 +29566,16 @@ ${expression}
29416
29566
  error = { ename: "KernelError", evalue: message, traceback: [message] };
29417
29567
  }
29418
29568
  } finally {
29419
- this.#executing = false;
29569
+ this.#running--;
29570
+ this.#cellRunning = false;
29420
29571
  }
29421
29572
  await this.#target?.releaseResults?.();
29422
29573
  if (error) {
29423
29574
  if (!silent && msg.content.stop_on_error !== false) {
29424
29575
  await new Promise((r) => setTimeout(r, 0));
29425
- this.#abortQueued = this.#channels.shellQueued();
29576
+ for (const queued of this.#mainShell.items) queued.abort = true;
29426
29577
  }
29427
- this.#publish("error", error, structured ? { metadata: { [TRACEBACK_MIME]: structured } } : {});
29578
+ this.#publish("error", error, { parent, ...structured ? { metadata: { [TRACEBACK_MIME]: structured } } : {} });
29428
29579
  this.#reply("shell", peer, msg, "execute_reply", { status: "error", execution_count: count, ...error });
29429
29580
  } else {
29430
29581
  this.#reply("shell", peer, msg, "execute_reply", {
@@ -29439,7 +29590,7 @@ ${expression}
29439
29590
  };
29440
29591
 
29441
29592
  // stub:package
29442
- var VERSION = "0.7.2";
29593
+ var VERSION = "0.9.0";
29443
29594
 
29444
29595
  // src/browser/channels.ts
29445
29596
  var PEER = "page";
@@ -29558,7 +29709,10 @@ var BrowserTarget = class _BrowserTarget {
29558
29709
  #hostCalls = /* @__PURE__ */ new Map();
29559
29710
  #host;
29560
29711
  #nextId = 0;
29561
- #interrupted = false;
29712
+ /** Counts interrupt() calls: a call that saw one start during it was interrupted (#417). */
29713
+ #interrupts = 0;
29714
+ /** Cells running: a comm handler that starts with none gets its own jupyter.signal (#421). */
29715
+ #cells = 0;
29562
29716
  #stopNote;
29563
29717
  #exited = false;
29564
29718
  constructor(worker, events, host) {
@@ -29662,26 +29816,33 @@ var BrowserTarget = class _BrowserTarget {
29662
29816
  });
29663
29817
  }
29664
29818
  async #interruptibly(op, params, what) {
29665
- this.#interrupted = false;
29819
+ const interrupts = this.#interrupts;
29666
29820
  try {
29667
29821
  return await this.#request(op, params, true);
29668
29822
  } catch (err) {
29669
- if (err instanceof Abandoned || this.#interrupted) throw new Interrupted(`${what} interrupted`);
29823
+ if (err instanceof Abandoned || this.#interrupts !== interrupts) throw new Interrupted(`${what} interrupted`);
29670
29824
  throw err;
29671
29825
  }
29672
29826
  }
29673
29827
  async evaluate(script) {
29674
- return asEvaluateResult(await this.#interruptibly("eval", { code: script }, "Execution"));
29828
+ this.#cells++;
29829
+ try {
29830
+ return asEvaluateResult(await this.#interruptibly("eval", { code: script }, "Execution"));
29831
+ } finally {
29832
+ this.#cells--;
29833
+ }
29675
29834
  }
29676
29835
  /**
29677
29836
  * A cell that's waiting on something leaves the Worker free: it answers a
29678
29837
  * ping, and the cell is abandoned with its state intact; host requests
29679
29838
  * in flight are cancelled on the page, and reject in the Worker with an
29680
- * AbortError (#388). A busy cell can't be reached, so the Worker is
29839
+ * AbortError (#388), and jupyter.signal is aborted (#418). A busy cell
29840
+ * can't be reached, so the Worker is
29681
29841
  * terminated, and a new one starts on the next cell.
29682
29842
  */
29683
29843
  async interrupt() {
29684
- this.#interrupted = true;
29844
+ this.#interrupts++;
29845
+ const cellRunning = this.#cells > 0;
29685
29846
  const running = [...this.#interruptible];
29686
29847
  const answered = await Promise.race([
29687
29848
  this.#request("ping").then(() => true, () => false),
@@ -29694,6 +29855,8 @@ var BrowserTarget = class _BrowserTarget {
29694
29855
  this.#interruptible.delete(id);
29695
29856
  }
29696
29857
  this.#cancelHostCalls();
29858
+ void this.#request("abort", { cell: cellRunning }).catch(() => {
29859
+ });
29697
29860
  return;
29698
29861
  }
29699
29862
  this.#stopNote = RESTART_NOTE;
@@ -29708,7 +29871,13 @@ var BrowserTarget = class _BrowserTarget {
29708
29871
  await this.#request("broadcast", { count: executionCount, handle: result.objectId });
29709
29872
  }
29710
29873
  async dispatchComm(msgType, content, metadata, buffers) {
29711
- const reply = await this.#interruptibly("comm", { msgType, content, metadata, buffers: buffers.map(toBase64) }, "Comm handler");
29874
+ const reply = await this.#interruptibly("comm", {
29875
+ msgType,
29876
+ content,
29877
+ metadata,
29878
+ buffers: buffers.map(toBase64),
29879
+ ownSignal: this.#cells === 0
29880
+ }, "Comm handler");
29712
29881
  if (isRecord(reply.error)) {
29713
29882
  const { name, message, stack } = reply.error;
29714
29883
  return { error: { name: String(name), message: String(message), stack: String(stack) } };