@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.
@@ -19,6 +19,8 @@ import { buildTraceback, cellName, TRACEBACK_MIME } from "./traceback.js";
19
19
  * installed TypeScript's: the major version package.json depends on.
20
20
  */
21
21
  export const TYPESCRIPT_VERSION = "6";
22
+ /** Errors from launching a target, which `KernelHost.fault` doesn't get. */
23
+ const launchFailures = new WeakSet();
22
24
  function fromBase64(text) {
23
25
  return Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
24
26
  }
@@ -28,8 +30,46 @@ function fromBase64(text) {
28
30
  */
29
31
  const INTERRUPT_REPLY_WAIT_MS = 1_000;
30
32
  const NO_INPUT = { value: null, interrupted: false };
33
+ /**
34
+ * Shell messages handled one at a time, in order: the main shell, or a
35
+ * kernel subshell (JEP 91, #417), each with its own.
36
+ */
37
+ class ShellQueue {
38
+ items = [];
39
+ #wake = null;
40
+ #closed = false;
41
+ push(item) {
42
+ if (this.#closed)
43
+ return;
44
+ this.items.push(item);
45
+ this.#wake?.();
46
+ }
47
+ /** The next message; null once closed. */
48
+ async next() {
49
+ for (;;) {
50
+ if (this.#closed)
51
+ return null;
52
+ const item = this.items.shift();
53
+ if (item !== undefined)
54
+ return item;
55
+ await new Promise((resolve) => (this.#wake = resolve));
56
+ this.#wake = null;
57
+ }
58
+ }
59
+ /** Stops taking messages; returns those still queued. */
60
+ close() {
61
+ this.#closed = true;
62
+ const dropped = this.items.splice(0);
63
+ this.#wake?.();
64
+ return dropped;
65
+ }
66
+ }
31
67
  /** Cells remembered for tracebacks: a frame can be in a function any earlier cell defined. */
32
68
  const MAX_CELLS = 1000;
69
+ /** The error reply for a subshell that doesn't exist (#417). */
70
+ function subshellNotFound(evalue) {
71
+ return { status: "error", ename: "SubshellNotFound", evalue, traceback: [`SubshellNotFound: ${evalue}`] };
72
+ }
33
73
  /** Shell requests other than execute_request, and their reply types. */
34
74
  const SHELL_REPLIES = {
35
75
  kernel_info_request: "kernel_info_reply",
@@ -84,13 +124,16 @@ export class KernelCore {
84
124
  #executionCount = 0;
85
125
  #parent = {};
86
126
  #allowStdin = false;
87
- #executing = false;
88
- /**
89
- * stop_on_error (#123): how many of the shell messages queued when a cell
90
- * failed are still to come, and whether the one being handled is one.
91
- */
92
- #abortQueued = 0;
127
+ /** Cells and comm handlers running in the target. */
128
+ #running = 0;
129
+ /** A cell is running: its parent header and allow_stdin hold until it ends (#417). */
130
+ #cellRunning = false;
131
+ /** stop_on_error (#123): the message being handled was queued behind a failed cell. */
93
132
  #aborting = false;
133
+ /** Messages without a subshell_id, in the order they came. */
134
+ #mainShell = new ShellQueue();
135
+ /** Kernel subshells (JEP 91, #417): subshell_id -> its queue. */
136
+ #subshells = new Map();
94
137
  /** The input_request waiting for its input_reply, if any. */
95
138
  #input = null;
96
139
  /** An interrupt is waiting on the target. */
@@ -117,7 +160,10 @@ export class KernelCore {
117
160
  // Transforms are synchronous, so the stripper loads before any cell.
118
161
  if (typeof this.#typeStrip === "string")
119
162
  await loadTypeStripper(this.#typeStrip);
120
- await Promise.all([this.#loop("shell", (m, p) => this.#handleShell(m, p)),
163
+ // No shell or control reply until every port is bound: a client that
164
+ // has kernel_info_reply goes on to the heartbeat port (#438).
165
+ await this.#channels.ready;
166
+ await Promise.all([this.#readShell(), this.#runShell(this.#mainShell),
121
167
  this.#loop("control", (m, p) => this.#handleControl(m, p)),
122
168
  this.#loop("stdin", (m) => this.#handleStdin(m))]);
123
169
  }
@@ -141,8 +187,9 @@ export class KernelCore {
141
187
  targetState = await target.diagnose().catch((err) => `diagnose failed: ${err}`);
142
188
  this.#log(`state: ${JSON.stringify({
143
189
  shell: handling && { msgType: handling.msgType, msgId: handling.msgId, ms: Date.now() - handling.since },
144
- queued: this.#channels.shellQueued(),
145
- executing: this.#executing,
190
+ queued: this.#mainShell.items.length + this.#channels.shellQueued(),
191
+ subshells: this.#subshells.size,
192
+ running: this.#running,
146
193
  interrupting: this.#interrupting,
147
194
  awaitingInput: this.#input !== null,
148
195
  target: targetState,
@@ -175,16 +222,21 @@ export class KernelCore {
175
222
  const type = msg.header.msg_type;
176
223
  const commId = String(msg.content.comm_id ?? "");
177
224
  const target = await this.#ensureTarget();
178
- // Kernel messages the handlers send are replies to this message.
179
- this.#parent = msg.header;
180
- this.#allowStdin = false;
181
- this.#executing = true;
225
+ // Kernel messages the handlers send are replies to this message, unless
226
+ // it came on a subshell while a cell runs (#417): the target's output
227
+ // doesn't say which it came from, so it stays the cell's.
228
+ if (!this.#cellRunning) {
229
+ this.#parent = msg.header;
230
+ this.#allowStdin = false;
231
+ }
232
+ const parent = msg.header;
182
233
  let handled = false;
234
+ this.#running++;
183
235
  try {
184
236
  const result = await target.dispatchComm(type, msg.content, msg.metadata, msg.buffers);
185
237
  if ("error" in result) {
186
238
  const { name, message, stack } = result.error;
187
- this.#publish("stream", { name: "stderr", text: `Error in ${type} handler: ${stack || `${name}: ${message}`}\n` });
239
+ this.#publish("stream", { name: "stderr", text: `Error in ${type} handler: ${stack || `${name}: ${message}`}\n` }, { parent });
188
240
  }
189
241
  else {
190
242
  handled = result.handled;
@@ -193,10 +245,10 @@ export class KernelCore {
193
245
  catch (err) {
194
246
  if (!(err instanceof Interrupted))
195
247
  throw err;
196
- this.#publish("stream", { name: "stderr", text: `${type} handler interrupted\n` });
248
+ this.#publish("stream", { name: "stderr", text: `${type} handler interrupted\n` }, { parent });
197
249
  }
198
250
  finally {
199
- this.#executing = false;
251
+ this.#running--;
200
252
  }
201
253
  await target.flush().catch(() => { });
202
254
  if (type === "comm_open") {
@@ -206,7 +258,7 @@ export class KernelCore {
206
258
  else {
207
259
  // An unknown target, or a handler that failed: the protocol asks the
208
260
  // kernel to close the comm so the frontend doesn't wait on it.
209
- this.#publish("comm_close", { comm_id: commId, data: {} });
261
+ this.#publish("comm_close", { comm_id: commId, data: {} }, { parent });
210
262
  }
211
263
  }
212
264
  else if (type === "comm_close") {
@@ -228,6 +280,10 @@ export class KernelCore {
228
280
  return target;
229
281
  }, (err) => {
230
282
  this.#targetStarting = null;
283
+ // Usually the user's environment (a missing runtime, a bad config),
284
+ // not a kernel fault, and its message quotes the child's stderr.
285
+ if (typeof err === "object" && err !== null)
286
+ launchFailures.add(err);
231
287
  throw err;
232
288
  });
233
289
  return this.#targetStarting;
@@ -249,10 +305,19 @@ export class KernelCore {
249
305
  this.#interruptStart = null;
250
306
  }
251
307
  }
308
+ /** Tells the host of a fault in the kernel's own code; a target that failed to launch isn't one (#422). */
309
+ #fault(err, where) {
310
+ if (typeof err === "object" && err !== null && launchFailures.has(err))
311
+ return;
312
+ this.#host.fault?.(err, where);
313
+ }
252
314
  #targetEvents() {
253
315
  return {
254
316
  stream: (name, text) => this.#publish("stream", { name, text }),
255
- message: (msg) => void this.#onTargetMessage(msg).catch((err) => this.#log(`error handling target message: ${err}`)),
317
+ message: (msg) => void this.#onTargetMessage(msg).catch((err) => {
318
+ this.#log(`error handling target message: ${err}`);
319
+ this.#fault(err, "target message");
320
+ }),
256
321
  exit: (code, note) => {
257
322
  this.#target = null;
258
323
  // What its cells declared died with it.
@@ -302,7 +367,7 @@ export class KernelCore {
302
367
  this.#interruptStart();
303
368
  return;
304
369
  }
305
- if (!this.#target || !this.#executing)
370
+ if (!this.#target || this.#running === 0)
306
371
  return;
307
372
  this.#interrupting = true;
308
373
  try {
@@ -334,27 +399,85 @@ export class KernelCore {
334
399
  async #loop(channel, handle) {
335
400
  while (!this.#shuttingDown) {
336
401
  const received = await this.#channels.recv(channel);
337
- // Before the drop below: shellQueued() counted this message too, so
338
- // a dropped one takes its own abort slot, not a queued cell's (#340).
339
- if (channel === "shell") {
340
- this.#aborting = this.#abortQueued > 0;
341
- if (this.#aborting)
342
- this.#abortQueued--;
402
+ // Malformed, or on ZeroMQ unsigned or forged: dropped, as in Deno.
403
+ if (received === null)
404
+ continue;
405
+ try {
406
+ await handle(received.msg, received.peer);
407
+ }
408
+ catch (err) {
409
+ this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
410
+ this.#fault(err, `${channel} handler`);
343
411
  }
412
+ }
413
+ }
414
+ /**
415
+ * Takes every shell message as it comes and queues it for the main shell
416
+ * or its subshell, so a subshell's message isn't stuck behind a running
417
+ * cell (#417).
418
+ */
419
+ async #readShell() {
420
+ while (!this.#shuttingDown) {
421
+ const received = await this.#channels.recv("shell");
344
422
  // Malformed, or on ZeroMQ unsigned or forged: dropped, as in Deno.
345
423
  if (received === null)
346
424
  continue;
347
- const { peer, msg } = received;
425
+ const subshellId = received.msg.header.subshell_id;
426
+ if (typeof subshellId !== "string") {
427
+ this.#mainShell.push({ ...received, abort: false });
428
+ continue;
429
+ }
430
+ const queue = this.#subshells.get(subshellId);
431
+ if (queue === undefined) {
432
+ this.#refuseShell(received, `No subshell with id ${subshellId}`);
433
+ }
434
+ else if (received.msg.header.msg_type === "execute_request") {
435
+ // Cells share one target and one parent header, so a cell sent on
436
+ // a subshell waits its turn on the main shell.
437
+ this.#mainShell.push({ ...received, abort: false });
438
+ }
439
+ else {
440
+ queue.push({ ...received, abort: false });
441
+ }
442
+ }
443
+ }
444
+ /**
445
+ * Answers a shell message for a subshell that doesn't exist, or was
446
+ * deleted with the message queued, so the frontend doesn't wait on it.
447
+ */
448
+ #refuseShell({ peer, msg }, evalue) {
449
+ const type = msg.header.msg_type;
450
+ this.#log(`refusing ${type}: ${evalue}`);
451
+ this.#status("busy", msg.header);
452
+ const replyType = type === "execute_request" ? "execute_reply" : SHELL_REPLIES[type];
453
+ if (replyType !== undefined)
454
+ this.#reply("shell", peer, msg, replyType, subshellNotFound(evalue));
455
+ // As for an unknown target: the frontend shouldn't wait on the comm.
456
+ if (type === "comm_open")
457
+ this.#publish("comm_close", { comm_id: String(msg.content.comm_id ?? ""), data: {} }, { parent: msg.header });
458
+ this.#status("idle", msg.header);
459
+ }
460
+ /** Handles one queue's messages in turn. */
461
+ async #runShell(queue) {
462
+ const main = queue === this.#mainShell;
463
+ while (!this.#shuttingDown) {
464
+ const queued = await queue.next();
465
+ if (queued === null)
466
+ return;
467
+ const { peer, msg } = queued;
348
468
  try {
349
- if (channel === "shell")
469
+ if (main) {
470
+ this.#aborting = queued.abort;
350
471
  this.#handling = { msgType: msg.header.msg_type, msgId: msg.header.msg_id, since: Date.now() };
351
- await handle(msg, peer);
472
+ }
473
+ await this.#handleShell(msg, peer);
352
474
  }
353
475
  catch (err) {
354
476
  this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
477
+ this.#fault(err, "shell handler");
355
478
  }
356
479
  finally {
357
- if (channel === "shell")
480
+ if (main)
358
481
  this.#handling = null;
359
482
  }
360
483
  }
@@ -369,7 +492,12 @@ export class KernelCore {
369
492
  language_info: {
370
493
  name: "typescript",
371
494
  version: this.#host.languageVersion ?? TYPESCRIPT_VERSION,
372
- mimetype: "text/x.typescript",
495
+ // JupyterLab's LSP adapter picks the language server by a language
496
+ // it derives from this MIME type, not from name: it drops the type
497
+ // and an "x-" prefix, so "text/x.typescript" became "x.typescript"
498
+ // and matched no server (#400). This is CodeMirror's MIME type for
499
+ // TypeScript and in jupyter-lsp's typescript-language-server spec.
500
+ mimetype: "application/typescript",
373
501
  file_extension: ".ts",
374
502
  pygments_lexer: "typescript",
375
503
  codemirror_mode: "typescript",
@@ -381,12 +509,22 @@ export class KernelCore {
381
509
  debugger: this.#debugger !== null,
382
510
  // What current ipykernel advertises; JupyterLab itself goes by the
383
511
  // kernelspec's metadata.debugger.
384
- supported_features: this.#debugger !== null ? ["debugger"] : [],
512
+ supported_features: [...(this.#debugger !== null ? ["debugger"] : []), "kernel subshells"],
385
513
  ...(this.#host.capabilities ? { capabilities: this.#host.capabilities } : {}),
386
514
  };
387
515
  }
388
516
  // --- control channel ------------------------------------------------------
389
517
  async #handleControl(msg, peer) {
518
+ // A frontend polls usage, and measuring can take a moment (`ps` on
519
+ // macOS): answer it beside the queue, so an interrupt or shutdown
520
+ // never waits behind a poll (#409).
521
+ if (msg.header.msg_type === "usage_request") {
522
+ void this.#handleControlRequest(msg, peer).catch((err) => this.#log(`usage_request failed: ${err}`));
523
+ return;
524
+ }
525
+ return this.#handleControlRequest(msg, peer);
526
+ }
527
+ async #handleControlRequest(msg, peer) {
390
528
  // Every request is wrapped in busy/idle, as on shell (#124). JupyterLab
391
529
  // treats a debug request as done only once the kernel reports idle for
392
530
  // it. (Jupyter Server leaves debug_request out of the kernel's status.)
@@ -412,6 +550,42 @@ export class KernelCore {
412
550
  ]);
413
551
  this.#reply(channel, peer, msg, "interrupt_reply", { status: "ok" });
414
552
  break;
553
+ case "create_subshell_request": {
554
+ const subshellId = crypto.randomUUID();
555
+ const queue = new ShellQueue();
556
+ this.#subshells.set(subshellId, queue);
557
+ void this.#runShell(queue);
558
+ this.#reply(channel, peer, msg, "create_subshell_reply", { status: "ok", subshell_id: subshellId });
559
+ break;
560
+ }
561
+ case "delete_subshell_request": {
562
+ const subshellId = String(msg.content.subshell_id ?? "");
563
+ const queue = this.#subshells.get(subshellId);
564
+ if (queue === undefined) {
565
+ this.#reply(channel, peer, msg, "delete_subshell_reply", subshellNotFound(`No subshell with id ${subshellId}`));
566
+ break;
567
+ }
568
+ // A message it is handling finishes; what's queued is refused.
569
+ this.#subshells.delete(subshellId);
570
+ for (const dropped of queue.close())
571
+ this.#refuseShell(dropped, `Subshell ${subshellId} was deleted`);
572
+ this.#reply(channel, peer, msg, "delete_subshell_reply", { status: "ok" });
573
+ break;
574
+ }
575
+ case "list_subshell_request":
576
+ this.#reply(channel, peer, msg, "list_subshell_reply", { status: "ok", subshell_id: [...this.#subshells.keys()] });
577
+ break;
578
+ case "usage_request": {
579
+ // Without starting a target: a frontend polls this (#409). A host
580
+ // that can't measure anything still answers, so the frontend
581
+ // doesn't wait out its timeout.
582
+ const usage = this.#host.usage?.(this.#target).catch((err) => {
583
+ this.#log(`usage_request failed: ${err}`);
584
+ return {};
585
+ });
586
+ this.#reply(channel, peer, msg, "usage_reply", (await usage) ?? {});
587
+ break;
588
+ }
415
589
  case "debug_request": {
416
590
  let reply;
417
591
  if (this.#debugger === null) {
@@ -745,7 +919,8 @@ export class KernelCore {
745
919
  let userExpressions = {};
746
920
  // nteract's structured traceback, sent as the error message's metadata (#322).
747
921
  let structured = null;
748
- this.#executing = true;
922
+ this.#running++;
923
+ this.#cellRunning = true;
749
924
  try {
750
925
  const target = await this.#targetForCell();
751
926
  if (target.notice !== undefined && this.#noticeShownFor !== target) {
@@ -796,7 +971,8 @@ export class KernelCore {
796
971
  }
797
972
  }
798
973
  finally {
799
- this.#executing = false;
974
+ this.#running--;
975
+ this.#cellRunning = false;
800
976
  }
801
977
  // The result and error are shown: nothing refers to them any more (#286).
802
978
  await this.#target?.releaseResults?.();
@@ -805,10 +981,14 @@ export class KernelCore {
805
981
  // Abort what was queued behind this cell (JupyterLab's Run All sends
806
982
  // every cell at once), but nothing sent after the client sees the
807
983
  // error. A macrotask first lets bytes already received be parsed.
984
+ // Shell messages are queued as they're read, so by then each is.
808
985
  await new Promise((r) => setTimeout(r, 0));
809
- this.#abortQueued = this.#channels.shellQueued();
986
+ for (const queued of this.#mainShell.items)
987
+ queued.abort = true;
810
988
  }
811
- this.#publish("error", error, structured ? { metadata: { [TRACEBACK_MIME]: structured } } : {});
989
+ // With its parent: a subshell's comm handler may have taken #parent
990
+ // since the cell ended (#417).
991
+ this.#publish("error", error, { parent, ...(structured ? { metadata: { [TRACEBACK_MIME]: structured } } : {}) });
812
992
  this.#reply("shell", peer, msg, "execute_reply", { status: "error", execution_count: count, ...error });
813
993
  }
814
994
  else {
package/dist/kernel.js CHANGED
@@ -18,6 +18,8 @@ import { cacheRoot, VERSION } from "./package.js";
18
18
  import { watchParent } from "./parent.js";
19
19
  import { KernelCore } from "./kernel-core.js";
20
20
  import { ZmqChannels } from "./zmq-channels.js";
21
+ import { UsageMeter } from "./usage.js";
22
+ import { crashReporter, reportUncaught } from "./crash-reports.js";
21
23
  /** The installed TypeScript's version, for kernel_info's language_info (#340). */
22
24
  function typescriptVersion() {
23
25
  try {
@@ -141,6 +143,12 @@ export class Kernel {
141
143
  }
142
144
  }
143
145
  const denoCompat = options.denoCompat ?? false;
146
+ const usage = new UsageMeter();
147
+ // Opt-in reports of the kernel's own faults (#422): null, and nothing
148
+ // installed, unless CDP_KERNEL_CRASH_REPORTS=1.
149
+ const reporter = crashReporter({ target: runtime });
150
+ if (reporter)
151
+ reportUncaught(reporter);
144
152
  this.#core = new KernelCore({
145
153
  runtime,
146
154
  version: VERSION,
@@ -164,9 +172,11 @@ export class Kernel {
164
172
  createDebugger: runtime === "node" || runtime === "deno"
165
173
  ? (publish, transformOptions) => new Debugger(publish, transformOptions)
166
174
  : undefined,
175
+ usage: (target) => usage.reply(target),
167
176
  readSource,
168
177
  kernelFiles: kernelFiles(),
169
178
  log,
179
+ fault: reporter ? (err, where) => void reporter.report(err, where) : undefined,
170
180
  exit: () => process.exit(0),
171
181
  });
172
182
  }
package/dist/main.js CHANGED
@@ -135,19 +135,36 @@ function uninstall(names, all) {
135
135
  if (failed)
136
136
  process.exit(1);
137
137
  }
138
- const { positionals, values } = parseArgs({
139
- allowPositionals: true,
140
- options: {
141
- runtime: { type: "string", default: "node" },
142
- conn: { type: "string" },
143
- name: { type: "string" },
144
- display: { type: "string" },
145
- all: { type: "boolean", default: false },
146
- "deno-compat": { type: "boolean", default: false },
147
- "type-strip": { type: "string", default: "esbuild" },
148
- "no-language-service": { type: "boolean", default: false },
149
- },
150
- });
138
+ const USAGE = "usage: cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] [--type-strip esbuild|sucrase] [--no-language-service] | list | uninstall [NAME...] [--all] | kernel --runtime node|deno|bun|workerd|celld --conn FILE | version";
139
+ /** The command line, or exit 2 with the usage when it doesn't parse (an unknown option, a missing value). */
140
+ function parseCommandLine() {
141
+ try {
142
+ return parseArgs({
143
+ allowPositionals: true,
144
+ options: {
145
+ runtime: { type: "string", default: "node" },
146
+ conn: { type: "string" },
147
+ name: { type: "string" },
148
+ display: { type: "string" },
149
+ all: { type: "boolean", default: false },
150
+ "deno-compat": { type: "boolean", default: false },
151
+ "type-strip": { type: "string", default: "esbuild" },
152
+ "no-language-service": { type: "boolean", default: false },
153
+ help: { type: "boolean", short: "h", default: false },
154
+ },
155
+ });
156
+ }
157
+ catch (err) {
158
+ console.error(`cdp-kernel: ${err instanceof Error ? err.message.split(". ")[0] : String(err)}`);
159
+ console.error(USAGE);
160
+ process.exit(2);
161
+ }
162
+ }
163
+ const { positionals, values } = parseCommandLine();
164
+ if (values.help || positionals[0] === "help") {
165
+ console.log(USAGE);
166
+ process.exit(0);
167
+ }
151
168
  const runtime = values.runtime;
152
169
  if (!isRuntime(runtime)) {
153
170
  console.error(`unknown runtime ${runtime}; expected node, deno, bun, workerd or celld`);
@@ -186,6 +203,6 @@ switch (positionals[0]) {
186
203
  console.log(VERSION);
187
204
  break;
188
205
  default:
189
- console.error("usage: cdp-kernel install [--runtime node|deno|bun|workerd|celld] [--deno-compat] [--type-strip esbuild|sucrase] [--no-language-service] | list | uninstall [NAME...] [--all] | kernel --runtime node|deno|bun|workerd|celld --conn FILE | version");
206
+ console.error(USAGE);
190
207
  process.exit(2);
191
208
  }
package/dist/message.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const PROTOCOL_VERSION = "5.3";
1
+ export declare const PROTOCOL_VERSION = "5.5";
2
2
  export type JsonObject = {
3
3
  [key: string]: unknown;
4
4
  };
package/dist/message.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Jupyter message shapes and headers, without the wire format: what the
2
2
  // kernel's message handling (src/kernel-core.ts) builds and reads. The
3
3
  // framing and HMAC signing that ZeroMQ peers need are in src/wire.ts.
4
- export const PROTOCOL_VERSION = "5.3";
4
+ export const PROTOCOL_VERSION = "5.5";
5
5
  export function makeHeader(session, msgType) {
6
6
  return {
7
7
  msg_id: crypto.randomUUID(),
package/dist/package.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
2
- export declare const VERSION = "0.7.2";
2
+ export declare const VERSION = "0.9.0";
3
3
  /**
4
4
  * The UTC day this version was released, set with VERSION. The publish
5
5
  * workflow refuses a tag unless it's today or yesterday, so it can't go
6
6
  * stale: see dependencyCutoff.
7
7
  */
8
- export declare const RELEASE_DATE = "2026-11-02";
8
+ export declare const RELEASE_DATE = "2026-11-09";
9
9
  /**
10
10
  * Deno blocks packages less than 24 hours old (minimum dependency age), so
11
11
  * a kernelspec relaunching a release from JSR would fail on release day. It
package/dist/package.js CHANGED
@@ -14,13 +14,13 @@ import { join, sep } from "node:path";
14
14
  import process from "node:process";
15
15
  import { fileURLToPath } from "node:url";
16
16
  /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
17
- export const VERSION = "0.7.2";
17
+ export const VERSION = "0.9.0";
18
18
  /**
19
19
  * The UTC day this version was released, set with VERSION. The publish
20
20
  * workflow refuses a tag unless it's today or yesterday, so it can't go
21
21
  * stale: see dependencyCutoff.
22
22
  */
23
- export const RELEASE_DATE = "2026-11-02";
23
+ export const RELEASE_DATE = "2026-11-09";
24
24
  /**
25
25
  * Deno blocks packages less than 24 hours old (minimum dependency age), so
26
26
  * a kernelspec relaunching a release from JSR would fail on release day. It
@@ -52,8 +52,19 @@ export interface KernelTarget {
52
52
  diagnose?(): Promise<CdpParams>;
53
53
  /** Lets go of the last cell's result and error, once they are shown (#286). */
54
54
  releaseResults?(): Promise<void>;
55
+ /** The target process's memory and CPU time, for usage_request (#409). */
56
+ usage?(): Promise<TargetUsage>;
55
57
  kill(): void;
56
58
  }
59
+ /** What a target measured of its own process; a field it can't measure is left out (#409). */
60
+ export interface TargetUsage {
61
+ /** Resident memory, in bytes. */
62
+ memory?: number;
63
+ /** CPU time used since it started, in µs. */
64
+ cpuTime?: number;
65
+ /** When it started (ms since the epoch), the start of its first CPU interval. */
66
+ startedAt?: number;
67
+ }
57
68
  /** Messages the bootstrap sends: message records on stdout, or Bun's channel. */
58
69
  export type TargetMessage = {
59
70
  kind: "iopub";
package/dist/target.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type CdpParams, type EvaluateResult, type RemoteObject } from "./cdp.ts";
2
2
  import { transformCell } from "./transform.ts";
3
3
  import type { DebugChannel } from "./debugger.ts";
4
- import { type CdpRuntime, Interrupted, isTargetMessage, type KernelTarget, parseBundle, parseJson, type TargetEvents, type TargetMessage } from "./target-api.ts";
4
+ import { type CdpRuntime, Interrupted, isTargetMessage, type KernelTarget, parseBundle, parseJson, type TargetEvents, type TargetMessage, type TargetUsage } from "./target-api.ts";
5
5
  export { Interrupted, isTargetMessage, parseBundle, parseJson };
6
6
  export type { CdpRuntime, KernelTarget, RuntimeName, TargetEvents, TargetMessage } from "./target-api.ts";
7
7
  export interface LaunchOptions {
@@ -89,6 +89,12 @@ export declare class Target implements KernelTarget {
89
89
  }): Promise<Target>;
90
90
  /** The Debugger domain on this target's session, for src/debugger.ts. */
91
91
  get debug(): DebugChannel;
92
+ /**
93
+ * The runtime process's resident memory and CPU time, from the OS (#409).
94
+ * Where the OS can't say, the memory is the V8 heap's size instead,
95
+ * which leaves out native memory such as Buffers; CDP has no CPU time.
96
+ */
97
+ usage(): Promise<TargetUsage>;
92
98
  /**
93
99
  * What the host is waiting on, whether the runtime still evaluates, runs
94
100
  * immediates and runs timers, and where its JavaScript is if it is
@@ -106,7 +112,8 @@ export declare class Target implements KernelTarget {
106
112
  * CDP equivalent is Runtime.terminateExecution, sent on a second session
107
113
  * because the shell session is the one blocked on the running evaluate.
108
114
  * Terminated code never settles the awaited promise, so the pending
109
- * evaluate is abandoned here and the session stays usable.
115
+ * evaluate is abandoned here and the session stays usable; jupyter.signal
116
+ * tells code still waiting on such a promise to stop (#418).
110
117
  */
111
118
  interrupt(): Promise<void>;
112
119
  /**