better-dsh 0.3.0 → 0.3.1
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 +49 -178
- package/cordis.patch.yml +4 -35
- package/docs/adr/0015-independent-bundles-agent-remote.md +40 -0
- package/docs/remote-tools.md +56 -0
- package/docs/specs/omp-python-repl-port/runtime-guide.md +33 -0
- package/docs/specs/omp-python-repl-port/spec.md +61 -0
- package/docs/specs/remote-tool-enhancement/spec.md +203 -0
- package/dsh-docs/baseline.md +13 -0
- package/dsh-docs/subsystems/invariants.md +3 -0
- package/dsh-docs/subsystems/invariants.zh.md +3 -0
- package/eval-description.md +7 -3
- package/lib/{LaunchOptions-kGXcZOe0.js → LaunchOptions-5eg7E0ye.js} +1 -1
- package/lib/LaunchOptions-DxQQ0s-A.js +4 -0
- package/lib/{bidi-BalpKaJc.js → bidi-CwabFH07.js} +1 -1
- package/lib/client/index.js +30 -6
- package/lib/failover/index.js +1 -1
- package/lib/fs-aware/sandbox-plugin.js +3 -3
- package/lib/{index-browser-CysrjMyM.js → index-browser-B-9jm12n.js} +4 -4
- package/lib/index.d.ts +17 -41
- package/lib/index.js +473 -1678
- package/lib/kernel-env-BOF0fNHn.js +91 -0
- package/lib/kernel-env.d.ts +4 -58
- package/lib/kernel-env.js +2 -2
- package/lib/{lsp-server-registry-BXvUTBY7.js → lsp-server-registry-BcdkHROD.js} +1 -1
- package/lib/{lsp-server-registry-CH9WNFFt.js → lsp-server-registry-D4VLs1Sm.js} +1 -1
- package/lib/{main-CupOhMp5.js → main-BeCgUd20.js} +8 -6
- package/lib/{main-BQeFrBM1.js → main-Dv_UQqG2.js} +1 -1
- package/lib/mobile/plugin.d.ts +1 -1
- package/lib/omp-python/DILL-LICENSE +35 -0
- package/lib/omp-python/OMP-LICENSE +23 -0
- package/lib/omp-python/better_snapshot.py +391 -0
- package/lib/omp-python/better_support.py +425 -0
- package/lib/omp-python/dill-provenance.json +55 -0
- package/lib/omp-python/provenance.json +13 -0
- package/lib/omp-python/runner.py +2499 -0
- package/lib/omp-python/vendor/dill/__diff.py +234 -0
- package/lib/omp-python/vendor/dill/__info__.py +291 -0
- package/lib/omp-python/vendor/dill/__init__.py +119 -0
- package/lib/omp-python/vendor/dill/_dill.py +2265 -0
- package/lib/omp-python/vendor/dill/_objects.py +545 -0
- package/lib/omp-python/vendor/dill/_shims.py +193 -0
- package/lib/omp-python/vendor/dill/detect.py +287 -0
- package/lib/omp-python/vendor/dill/logger.py +285 -0
- package/lib/omp-python/vendor/dill/objtypes.py +24 -0
- package/lib/omp-python/vendor/dill/pointers.py +122 -0
- package/lib/omp-python/vendor/dill/session.py +612 -0
- package/lib/omp-python/vendor/dill/settings.py +25 -0
- package/lib/omp-python/vendor/dill/source.py +1025 -0
- package/lib/omp-python/vendor/dill/temp.py +252 -0
- package/lib/{puppeteer-core-CSk2PtSm.js → puppeteer-core-Dnd7eqel.js} +9 -9
- package/lib/remote/plugin.d.ts +459 -5
- package/lib/remote/plugin.js +2389 -210
- package/lib/url-schemes/index.d.ts +11 -11
- package/lib/url-schemes/index.js +12 -12
- package/lib/web-password.d.ts +8 -8
- package/lib/web-password.js +5 -1
- package/lib/{web-trust-Dej_RI2B.d.ts → web-trust-CwZrTBcl.d.ts} +11 -11
- package/lib/web-trust.d.ts +1 -2
- package/lib/welcome-notice.d.ts +5 -7
- package/lib/welcome-notice.js +9 -48
- package/lib/{wrap-T4I0nfPM.js → wrap-0VA0uFx8.js} +1 -1
- package/package.json +6 -23
- package/scripts/kernel-provision.mjs +4 -34
- package/lib/LaunchOptions-B874eBu2.js +0 -4
- package/lib/bundle-delivery.d.ts +0 -20
- package/lib/bundle-delivery.js +0 -209
- package/lib/hmr.d.ts +0 -32
- package/lib/hmr.js +0 -49
- package/lib/kernel-bridge.py +0 -172
- package/lib/kernel-env-D8UOWeji.js +0 -195
- package/lib/webserver.d.ts +0 -10
- package/lib/webserver.js +0 -19
- package/scripts/register-bundle.mjs +0 -80
package/lib/index.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { t as Schema } from "./lib-C8_jXvwA.js";
|
|
2
2
|
import { a as RESERVED_ERROR_MEMBERS, i as RESERVED_BINDING_GLOBALS, n as DUNDER_MEMBER, o as ReplRuntime, r as PORTABLE_RESERVED_WORDS, t as isFlatBindableName } from "./py-sdk-D7klhpWb.js";
|
|
3
|
-
import {
|
|
3
|
+
import { a as resolveKernelEnv } from "./kernel-env-BOF0fNHn.js";
|
|
4
4
|
import { t as captureAllTools } from "./native-capture-BEByDXYM.js";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
-
import { randomBytes, randomUUID } from "node:crypto";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
6
|
import { Context } from "@deepseek-ai/cordis";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
7
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { createServer } from "node:http";
|
|
14
12
|
import { createInterface } from "node:readline";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
15
14
|
import { TOOL_RUNTIME_SCHEDULER, defineTool } from "@deepseek-ai/dsh-tools";
|
|
16
15
|
import * as dshLlm from "@deepseek-ai/dsh-llm";
|
|
17
16
|
import { BlockAssembler, HarnessError, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
@@ -175,870 +174,32 @@ function snapshotJsonValue(value) {
|
|
|
175
174
|
return walkJsonValue(value, true);
|
|
176
175
|
}
|
|
177
176
|
|
|
178
|
-
//#endregion
|
|
179
|
-
//#region src/kernel-transport.ts
|
|
180
|
-
/** The bridge script shipped beside the built entry (`lib/kernel-bridge.py`). */
|
|
181
|
-
const BRIDGE_SCRIPT = fileURLToPath(new URL("./kernel-bridge.py", import.meta.url));
|
|
182
|
-
/**
|
|
183
|
-
* One bridge subprocess speaking JSON lines. Lifecycle is owned by the caller
|
|
184
|
-
* (the kernel bridge registers it as an `ctx.effect`); this type installs no
|
|
185
|
-
* process-level signal handlers.
|
|
186
|
-
*/
|
|
187
|
-
var KernelTransport = class {
|
|
188
|
-
child;
|
|
189
|
-
reader;
|
|
190
|
-
startup = false;
|
|
191
|
-
closed = false;
|
|
192
|
-
readySettled = false;
|
|
193
|
-
readyResolve;
|
|
194
|
-
readyReject;
|
|
195
|
-
bridgeStderr = "";
|
|
196
|
-
handlers = /* @__PURE__ */ new Set();
|
|
197
|
-
waiters = /* @__PURE__ */ new Set();
|
|
198
|
-
constructor(options) {
|
|
199
|
-
this.options = options;
|
|
200
|
-
}
|
|
201
|
-
/** Spawn the bridge and resolve once it reports the channels are up. */
|
|
202
|
-
async start() {
|
|
203
|
-
if (this.reader !== void 0) return;
|
|
204
|
-
if (this.closed) throw new Error("kernel transport is closed");
|
|
205
|
-
const child = spawn(this.options.python, [BRIDGE_SCRIPT, this.options.connectionFile], {
|
|
206
|
-
...this.options.cwd === void 0 ? {} : { cwd: this.options.cwd },
|
|
207
|
-
env: {
|
|
208
|
-
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
209
|
-
LANG: "C.UTF-8",
|
|
210
|
-
HOME: process.env.HOME ?? tmpdir(),
|
|
211
|
-
PYTHONUNBUFFERED: "1"
|
|
212
|
-
},
|
|
213
|
-
stdio: [
|
|
214
|
-
"pipe",
|
|
215
|
-
"pipe",
|
|
216
|
-
"pipe"
|
|
217
|
-
]
|
|
218
|
-
});
|
|
219
|
-
this.child = child;
|
|
220
|
-
this.startup = true;
|
|
221
|
-
const ready = new Promise((resolve$1, reject) => {
|
|
222
|
-
this.readyResolve = resolve$1;
|
|
223
|
-
this.readyReject = reject;
|
|
224
|
-
});
|
|
225
|
-
const timer = setTimeout(() => {
|
|
226
|
-
this.failReady(/* @__PURE__ */ new Error(`kernel bridge did not report ready within ${this.options.readyTimeoutMs}ms; stderr tail:\n${this.stderrTail()}`));
|
|
227
|
-
}, this.options.readyTimeoutMs);
|
|
228
|
-
timer.unref?.();
|
|
229
|
-
child.stderr?.on("data", (buffer) => {
|
|
230
|
-
this.bridgeStderr += buffer.toString();
|
|
231
|
-
});
|
|
232
|
-
child.stdin?.on("error", (error) => {
|
|
233
|
-
this.bridgeStderr += `stdin error: ${error.code ?? error.message}\n`;
|
|
234
|
-
this.closed = true;
|
|
235
|
-
this.failWaiters();
|
|
236
|
-
});
|
|
237
|
-
child.stdout?.on("error", (error) => {
|
|
238
|
-
this.bridgeStderr += `stdout error: ${error.code ?? error.message}\n`;
|
|
239
|
-
});
|
|
240
|
-
child.on("error", (error) => {
|
|
241
|
-
this.bridgeStderr += `spawn error: ${error.message}\n`;
|
|
242
|
-
this.failReady(/* @__PURE__ */ new Error(`kernel bridge failed to spawn: ${error.message}`));
|
|
243
|
-
this.failWaiters();
|
|
244
|
-
this.failExit();
|
|
245
|
-
});
|
|
246
|
-
child.on("exit", (code, signal) => {
|
|
247
|
-
const detail = `kernel bridge exited (code=${String(code)} signal=${String(signal)})`;
|
|
248
|
-
this.bridgeStderr += `${detail}\n`;
|
|
249
|
-
this.closed = true;
|
|
250
|
-
this.failReady(/* @__PURE__ */ new Error(`${detail}; stderr tail:\n${this.stderrTail()}`));
|
|
251
|
-
this.failWaiters();
|
|
252
|
-
this.failExit();
|
|
253
|
-
});
|
|
254
|
-
if (child.stdout) {
|
|
255
|
-
this.reader = createInterface({ input: child.stdout });
|
|
256
|
-
this.reader.on("line", (line) => {
|
|
257
|
-
this.accept(line);
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
try {
|
|
261
|
-
await ready;
|
|
262
|
-
} finally {
|
|
263
|
-
clearTimeout(timer);
|
|
264
|
-
this.startup = false;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
/** Whether the bridge process is still live. */
|
|
268
|
-
get alive() {
|
|
269
|
-
return this.child !== void 0 && this.child.exitCode === null && !this.closed;
|
|
270
|
-
}
|
|
271
|
-
/** The bridge's stderr tail, for startup and crash diagnostics. */
|
|
272
|
-
stderrTail() {
|
|
273
|
-
return this.bridgeStderr.slice(-1024) || "(empty)";
|
|
274
|
-
}
|
|
275
|
-
/** Register a message handler; the returned function removes it. */
|
|
276
|
-
onMessage(handler) {
|
|
277
|
-
this.handlers.add(handler);
|
|
278
|
-
return () => {
|
|
279
|
-
this.handlers.delete(handler);
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Register a bridge-exit handler; the returned function removes it. A bridge
|
|
284
|
-
* that dies mid-cell must settle the active execution at once rather than at
|
|
285
|
-
* the cell budget.
|
|
286
|
-
*/
|
|
287
|
-
onExit(handler) {
|
|
288
|
-
this.exitHandlers.add(handler);
|
|
289
|
-
return () => {
|
|
290
|
-
this.exitHandlers.delete(handler);
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
exitHandlers = /* @__PURE__ */ new Set();
|
|
294
|
-
/** Notify exit handlers once per bridge process end. */
|
|
295
|
-
failExit() {
|
|
296
|
-
const handlers = [...this.exitHandlers];
|
|
297
|
-
this.exitHandlers.clear();
|
|
298
|
-
for (const handler of handlers) handler();
|
|
299
|
-
}
|
|
300
|
-
/** Send one message on the shell or control channel. */
|
|
301
|
-
send(channel, message) {
|
|
302
|
-
this.writeLine({
|
|
303
|
-
op: "send",
|
|
304
|
-
channel,
|
|
305
|
-
header: message.header,
|
|
306
|
-
content: message.content
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
/** Ask the kernel to interrupt the running cell (control channel). */
|
|
310
|
-
interrupt() {
|
|
311
|
-
this.writeLine({ op: "interrupt" });
|
|
312
|
-
}
|
|
313
|
-
/** Write one protocol line, refusing instead of throwing when the pipe is gone. */
|
|
314
|
-
writeLine(payload) {
|
|
315
|
-
const stdin = this.child?.stdin;
|
|
316
|
-
if (stdin === null || stdin === void 0 || this.closed || !stdin.writable) throw new Error("kernel bridge is not connected");
|
|
317
|
-
stdin.write(`${JSON.stringify(payload)}\n`);
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Wait for the next message on `channel` matching `match`.
|
|
321
|
-
* @returns The message, or null when the budget expires or the bridge dies.
|
|
322
|
-
*/
|
|
323
|
-
waitFor(channel, match, timeoutMs) {
|
|
324
|
-
if (this.closed) return Promise.resolve(null);
|
|
325
|
-
return new Promise((resolve$1) => {
|
|
326
|
-
const waiter = {
|
|
327
|
-
channel,
|
|
328
|
-
match,
|
|
329
|
-
settle: (message) => {
|
|
330
|
-
clearTimeout(waiter.timer);
|
|
331
|
-
this.waiters.delete(waiter);
|
|
332
|
-
resolve$1(message);
|
|
333
|
-
},
|
|
334
|
-
timer: setTimeout(() => {
|
|
335
|
-
this.waiters.delete(waiter);
|
|
336
|
-
resolve$1(null);
|
|
337
|
-
}, timeoutMs)
|
|
338
|
-
};
|
|
339
|
-
waiter.timer.unref?.();
|
|
340
|
-
this.waiters.add(waiter);
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
/** Stop the bridge (graceful shutdown note, then kill). */
|
|
344
|
-
async dispose() {
|
|
345
|
-
const child = this.child;
|
|
346
|
-
this.closed = true;
|
|
347
|
-
this.failWaiters();
|
|
348
|
-
this.reader?.close();
|
|
349
|
-
this.reader = void 0;
|
|
350
|
-
if (child === void 0) return;
|
|
351
|
-
try {
|
|
352
|
-
if (child.stdin?.writable === true) child.stdin.write(`${JSON.stringify({ op: "shutdown" })}\n`);
|
|
353
|
-
} catch {}
|
|
354
|
-
try {
|
|
355
|
-
child.stdin?.end();
|
|
356
|
-
} catch {}
|
|
357
|
-
if (child.exitCode !== null) return;
|
|
358
|
-
await new Promise((resolve$1) => {
|
|
359
|
-
const timer = setTimeout(() => {
|
|
360
|
-
try {
|
|
361
|
-
child.kill("SIGKILL");
|
|
362
|
-
} catch {}
|
|
363
|
-
resolve$1();
|
|
364
|
-
}, 500);
|
|
365
|
-
timer.unref?.();
|
|
366
|
-
child.once("exit", () => {
|
|
367
|
-
clearTimeout(timer);
|
|
368
|
-
resolve$1();
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
failReady(error) {
|
|
373
|
-
if (this.readySettled) return;
|
|
374
|
-
this.readySettled = true;
|
|
375
|
-
this.readyReject?.(error);
|
|
376
|
-
}
|
|
377
|
-
failWaiters() {
|
|
378
|
-
for (const waiter of [...this.waiters]) waiter.settle(null);
|
|
379
|
-
}
|
|
380
|
-
/** Route one decoded line; unknown events are ignored. */
|
|
381
|
-
accept(line) {
|
|
382
|
-
let event;
|
|
383
|
-
try {
|
|
384
|
-
event = JSON.parse(line);
|
|
385
|
-
} catch {
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
if (event.ev === "ready") {
|
|
389
|
-
if (!this.readySettled) {
|
|
390
|
-
this.readySettled = true;
|
|
391
|
-
this.readyResolve?.();
|
|
392
|
-
}
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
if (event.ev === "msg") {
|
|
396
|
-
const channel = event.channel;
|
|
397
|
-
const raw = event.msg;
|
|
398
|
-
if (typeof channel !== "string" || raw === void 0 || typeof raw.header !== "object" || raw.header === null) return;
|
|
399
|
-
const message = {
|
|
400
|
-
header: raw.header,
|
|
401
|
-
parent_header: raw.parent_header ?? {},
|
|
402
|
-
content: raw.content ?? {}
|
|
403
|
-
};
|
|
404
|
-
for (const waiter of [...this.waiters]) if (waiter.channel === channel && waiter.match(message)) waiter.settle(message);
|
|
405
|
-
for (const handler of this.handlers) handler(channel, message);
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
if (event.ev === "fatal") this.bridgeStderr += `fatal: ${String(event.message ?? "unknown")}\n`;
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
|
|
412
177
|
//#endregion
|
|
413
178
|
//#region src/bootstrap.ts
|
|
414
|
-
/**
|
|
415
|
-
|
|
416
|
-
* startup. Seeds the user namespace with the host-request comm bridge
|
|
417
|
-
* (`dashr.host` target, the PA `host_request()` pattern), the binding-proxy
|
|
418
|
-
* factories, the typed rejection classes, and the lossless-JSON completion
|
|
419
|
-
* encoder. Names are `_dashr`/`__dashr` prefixed (any case) so they are
|
|
420
|
-
* excluded from namespace snapshots and never leak into user state.
|
|
421
|
-
* @module dashr/bootstrap
|
|
422
|
-
*/
|
|
423
|
-
/** Comm target the kernel-side shim opens for typed host requests. */
|
|
424
|
-
const HOST_COMM_TARGET = "dashr.host";
|
|
425
|
-
/**
|
|
426
|
-
* The bootstrap source. Kept dependency-free apart from `ast`, `asyncio`,
|
|
427
|
-
* `json`, and `comm` (present by construction — it IS the kernel).
|
|
428
|
-
*/
|
|
429
|
-
const KERNEL_BOOTSTRAP = `
|
|
430
|
-
import ast as _dashr_ast
|
|
431
|
-
import json as _dashr_json
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def _dashr_install_control_comm_handlers():
|
|
435
|
-
# Host replies arrive as comm_msg on the CONTROL channel while the shell
|
|
436
|
-
# channel is busy executing our cell; teach the kernel to route them.
|
|
437
|
-
try:
|
|
438
|
-
from IPython import get_ipython
|
|
439
|
-
except Exception:
|
|
440
|
-
return
|
|
441
|
-
shell = get_ipython()
|
|
442
|
-
kernel = getattr(shell, 'kernel', None)
|
|
443
|
-
comm_manager = getattr(kernel, 'comm_manager', None)
|
|
444
|
-
control_handlers = getattr(kernel, 'control_handlers', None)
|
|
445
|
-
if comm_manager is None or not isinstance(control_handlers, dict):
|
|
446
|
-
return
|
|
447
|
-
control_handlers.setdefault('comm_msg', comm_manager.comm_msg)
|
|
448
|
-
control_handlers.setdefault('comm_close', comm_manager.comm_close)
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
def _dashr_install_interrupt():
|
|
452
|
-
# ipykernel's async-cell SIGINT handling only schedules a callback on the
|
|
453
|
-
# kernel event loop, and a non-yielding cell ('while True: pass') blocks
|
|
454
|
-
# that very loop, so the control-channel interrupt can never break it.
|
|
455
|
-
# SIGALRM is untouched by ipykernel: raising KeyboardInterrupt from its
|
|
456
|
-
# handler breaks any cell, and the host sends it (after a confirm window,
|
|
457
|
-
# see the bridge) on timeout/abort. Best effort — where SIGALRM is
|
|
458
|
-
# unavailable the control-channel interrupt is all there is.
|
|
459
|
-
#
|
|
460
|
-
# The busy guard is load-bearing (M3-A, blueprint §5 "SIGALRM-at-idle 双重
|
|
461
|
-
# 窗口"): a KeyboardInterrupt raised OUTSIDE cell execution — while the
|
|
462
|
-
# kernel boots, idles between cells, or unwinds a finished one — escapes
|
|
463
|
-
# the shell's containment and terminates the process cleanly, which used
|
|
464
|
-
# to kill the kernel deterministically (10/10 same-tick aborts, 40/40
|
|
465
|
-
# during cold boot; dev/m2a-report.md §5.1). The handler therefore only
|
|
466
|
-
# raises while a dashr cell is actually executing, tracked by the
|
|
467
|
-
# module-level _dashr_state dict below and set/cleared by the run/snapshot
|
|
468
|
-
# cell scaffolds around their whole body. A stray SIGALRM outside that
|
|
469
|
-
# window is swallowed — the host's force-settle already resolves the run,
|
|
470
|
-
# and the kernel lives. The dict (not a bare flag) is captured by the
|
|
471
|
-
# handler closure, so user code rebinding the NAME cannot disarm the
|
|
472
|
-
# guard; mutating the dict itself stays possible and accepted — this shim
|
|
473
|
-
# is a capability seam, not a security boundary.
|
|
474
|
-
try:
|
|
475
|
-
import signal as _dashr_signal
|
|
476
|
-
|
|
477
|
-
def _dashr_on_sigalrm(_signum, _frame):
|
|
478
|
-
if not _dashr_state['executing']:
|
|
479
|
-
return
|
|
480
|
-
raise KeyboardInterrupt
|
|
481
|
-
|
|
482
|
-
_dashr_signal.signal(_dashr_signal.SIGALRM, _dashr_on_sigalrm)
|
|
483
|
-
except (ImportError, AttributeError, OSError, ValueError):
|
|
484
|
-
pass
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
# The dashr busy-guard flag (see _dashr_install_interrupt): True for the whole
|
|
488
|
-
# duration of a run or snapshot cell — the only window in which a SIGALRM may
|
|
489
|
-
# raise KeyboardInterrupt. Lives at module (user-namespace) level so the cell
|
|
490
|
-
# scaffolds can flip it; excluded from snapshots by the _dashr prefix rule.
|
|
491
|
-
_dashr_state = {'executing': False}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
_dashr_install_interrupt()
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
class _DashrRejected(Exception):
|
|
498
|
-
def __init__(self, message):
|
|
499
|
-
Exception.__init__(self, message)
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
def _dashr_host_request(payload):
|
|
503
|
-
from comm import create_comm
|
|
504
|
-
import asyncio
|
|
505
|
-
_dashr_install_control_comm_handlers()
|
|
506
|
-
loop = asyncio.get_running_loop()
|
|
507
|
-
future = loop.create_future()
|
|
508
|
-
# primary=True publishes comm_open so the host learns this comm_id; the
|
|
509
|
-
# request itself travels as the first comm_msg.
|
|
510
|
-
comm = create_comm(target_name=${JSON.stringify(HOST_COMM_TARGET)})
|
|
511
|
-
|
|
512
|
-
def _resolve(action):
|
|
513
|
-
def _apply():
|
|
514
|
-
if future.done():
|
|
515
|
-
return
|
|
516
|
-
action()
|
|
517
|
-
comm.close()
|
|
518
|
-
# Replies arrive on the kernel's control channel, which may run off
|
|
519
|
-
# the event-loop thread; only call_soon_threadsafe may touch the
|
|
520
|
-
# future from there.
|
|
521
|
-
loop.call_soon_threadsafe(_apply)
|
|
522
|
-
|
|
523
|
-
def _on_msg(msg):
|
|
524
|
-
content = msg.get('content', {})
|
|
525
|
-
reply = content.get('data', {}) if isinstance(content, dict) else {}
|
|
526
|
-
if not isinstance(reply, dict) or future.done():
|
|
527
|
-
return
|
|
528
|
-
status = reply.get('status')
|
|
529
|
-
if status == 'ok':
|
|
530
|
-
_resolve(lambda: future.set_result(reply.get('result')))
|
|
531
|
-
elif status == 'error':
|
|
532
|
-
_resolve(lambda: future.set_exception(_DashrRejected(reply.get('error') or 'host request failed')))
|
|
533
|
-
|
|
534
|
-
comm.on_msg(_on_msg)
|
|
535
|
-
comm.send(dict(payload))
|
|
536
|
-
return future
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
def _dashr_make_error_class(name, member_property):
|
|
540
|
-
def _init(self, message, member=None):
|
|
541
|
-
Exception.__init__(self, message)
|
|
542
|
-
if member is not None:
|
|
543
|
-
setattr(self, member_property, member)
|
|
544
|
-
return type(name, (Exception,), {'__init__': _init})
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
def _dashr_make_proxy(global_name, function_name, error_class_name, member_property):
|
|
548
|
-
async def _dashr_proxy(*args, **kwargs):
|
|
549
|
-
if kwargs:
|
|
550
|
-
raise TypeError('dashr binding calls do not accept keyword arguments')
|
|
551
|
-
if len(args) == 1:
|
|
552
|
-
call_args = args[0]
|
|
553
|
-
elif len(args) == 0:
|
|
554
|
-
call_args = None
|
|
555
|
-
else:
|
|
556
|
-
call_args = list(args)
|
|
557
|
-
payload = {
|
|
558
|
-
'type': 'binding.call',
|
|
559
|
-
'global': global_name,
|
|
560
|
-
'name': function_name,
|
|
561
|
-
'args': call_args,
|
|
562
|
-
}
|
|
563
|
-
try:
|
|
564
|
-
return await _dashr_host_request(payload)
|
|
565
|
-
except _DashrRejected as rejected:
|
|
566
|
-
cls = globals().get(error_class_name) if error_class_name else None
|
|
567
|
-
if cls is None:
|
|
568
|
-
raise
|
|
569
|
-
raise cls(str(rejected), function_name) from None
|
|
570
|
-
return _dashr_proxy
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
def _dashr_make_holder(global_name, error_class_name, member_property, members):
|
|
574
|
-
# Attribute misses travel to the host instead of raising AttributeError
|
|
575
|
-
# here: the host owns the namespace, so IT rejects an unknown member as an
|
|
576
|
-
# unknown binding rather than the kernel inventing a local error class.
|
|
577
|
-
class _DashrBindingHolder:
|
|
578
|
-
def __dir__(self):
|
|
579
|
-
# F9 (v0.2.0-a): introspection tells the truth — dir(tool) lists
|
|
580
|
-
# exactly the bound names. A custom __dir__ fully replaces the
|
|
581
|
-
# default object.__dir__, so no dunder noise leaks in.
|
|
582
|
-
return list(members)
|
|
583
|
-
|
|
584
|
-
def __getattr__(self, function_name):
|
|
585
|
-
return _dashr_make_proxy(
|
|
586
|
-
global_name,
|
|
587
|
-
function_name,
|
|
588
|
-
error_class_name,
|
|
589
|
-
member_property,
|
|
590
|
-
)
|
|
591
|
-
|
|
592
|
-
return _DashrBindingHolder()
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
def _dashr_make_callable(global_name, function_name, error_class_name, member_property):
|
|
596
|
-
# A BARE callable global (the flat per-tool bindings AND the bridge
|
|
597
|
-
# tools: 'callable: true' on the namespace) instead of an object
|
|
598
|
-
# holder. The one-object form is the single convention: every callable
|
|
599
|
-
# takes exactly one positional arguments object (e.g.
|
|
600
|
-
# agent_message({'receiver': 'child', ...})), and keyword / multi-positional forms are
|
|
601
|
-
# rejected HERE, at the Python call boundary, so the introspectable
|
|
602
|
-
# signature tells the truth and a TypeError names the binding global
|
|
603
|
-
# (read(...) got an unexpected keyword argument ...). The wire envelope
|
|
604
|
-
# keeps the historical {'args': [...], 'kwargs': {}} shape so the host's
|
|
605
|
-
# parseReplCall / flatToolArgs / flatBridgeToolArgs validation layers stay
|
|
606
|
-
# authoritative and unchanged. The single functions-entry name is
|
|
607
|
-
# transport-only: the program never sees it, and a typed rejection
|
|
608
|
-
# carries the GLOBAL name as its member (for a flat tool binding the
|
|
609
|
-
# global IS the tool name the program knows). __name__/__qualname__ are
|
|
610
|
-
# pinned to the global name so introspection shows the binding's name,
|
|
611
|
-
# not the factory-local _dashr_callable.
|
|
612
|
-
async def _dashr_callable(args=None, /):
|
|
613
|
-
payload = {
|
|
614
|
-
'type': 'binding.call',
|
|
615
|
-
'global': global_name,
|
|
616
|
-
'name': function_name,
|
|
617
|
-
'args': {'args': [] if args is None else [args], 'kwargs': {}},
|
|
618
|
-
}
|
|
619
|
-
try:
|
|
620
|
-
return await _dashr_host_request(payload)
|
|
621
|
-
except _DashrRejected as rejected:
|
|
622
|
-
cls = globals().get(error_class_name) if error_class_name else None
|
|
623
|
-
if cls is None:
|
|
624
|
-
raise
|
|
625
|
-
raise cls(str(rejected), global_name) from None
|
|
626
|
-
|
|
627
|
-
_dashr_callable.__name__ = global_name
|
|
628
|
-
_dashr_callable.__qualname__ = global_name
|
|
629
|
-
return _dashr_callable
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
class _DashrNoValue:
|
|
633
|
-
# Marks 'no completion value': a cell whose last statement is not an
|
|
634
|
-
# expression, or whose last expression evaluates to None — the REPL
|
|
635
|
-
# displayhook suppresses None (IPython behavior), so no value crosses.
|
|
636
|
-
__slots__ = ()
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
_DASHR_NO_VALUE = _DashrNoValue()
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
class _DashrTopLevelReturnChecker(_dashr_ast.NodeVisitor):
|
|
643
|
-
# A cell runs at module scope, like any native IPython cell: a top-level
|
|
644
|
-
# 'return'/'yield' is a SyntaxError decided by the kernel itself, exactly
|
|
645
|
-
# as in a real REPL. Nested scopes (functions, lambdas, classes) keep
|
|
646
|
-
# their own return semantics.
|
|
647
|
-
def visit_FunctionDef(self, node):
|
|
648
|
-
return
|
|
649
|
-
|
|
650
|
-
def visit_AsyncFunctionDef(self, node):
|
|
651
|
-
return
|
|
652
|
-
|
|
653
|
-
def visit_ClassDef(self, node):
|
|
654
|
-
return
|
|
655
|
-
|
|
656
|
-
def visit_Lambda(self, node):
|
|
657
|
-
return
|
|
658
|
-
|
|
659
|
-
def visit_Return(self, node):
|
|
660
|
-
raise SyntaxError("'return' outside function")
|
|
661
|
-
|
|
662
|
-
def visit_Yield(self, node):
|
|
663
|
-
raise SyntaxError("'yield' outside function")
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
async def _dashr_run_program(source):
|
|
667
|
-
# Run one cell with the user namespace as BOTH globals and locals —
|
|
668
|
-
# module-scope REPL semantics: state persists across cells, top-level
|
|
669
|
-
# 'await' is legal (PyCF_ALLOW_TOP_LEVEL_AWAIT, the same mechanism IPython
|
|
670
|
-
# uses), a top-level 'return'/'yield' is a SyntaxError decided here by the
|
|
671
|
-
# kernel — exactly as in a native IPython cell — and the last expression's
|
|
672
|
-
# value is the cell's result (the REPL displayhook handoff).
|
|
673
|
-
import inspect as _dashr_inspect
|
|
674
|
-
user_ns = globals()
|
|
675
|
-
if not source.strip():
|
|
676
|
-
source = 'pass'
|
|
677
|
-
parsed = _dashr_ast.parse(source, '<dashr program>', 'exec')
|
|
678
|
-
_DashrTopLevelReturnChecker().visit(parsed)
|
|
679
|
-
body = parsed.body
|
|
680
|
-
if body and isinstance(body[-1], _dashr_ast.Expr):
|
|
681
|
-
# Capture the last expression the way a REPL's displayhook shows it:
|
|
682
|
-
# its value feeds the completion envelope instead of being discarded.
|
|
683
|
-
# The name is distinct from the scaffold's __dashr_completion__ (the
|
|
684
|
-
# envelope holder) so a statement-ending cell can never pop a stale
|
|
685
|
-
# envelope left by an earlier run.
|
|
686
|
-
last = body[-1]
|
|
687
|
-
body[-1] = _dashr_ast.Assign(
|
|
688
|
-
targets=[_dashr_ast.Name(id='__dashr_cell_value__', ctx=_dashr_ast.Store())],
|
|
689
|
-
value=last.value,
|
|
690
|
-
)
|
|
691
|
-
_dashr_ast.fix_missing_locations(body[-1])
|
|
692
|
-
code = compile(
|
|
693
|
-
parsed,
|
|
694
|
-
'<dashr program>',
|
|
695
|
-
'exec',
|
|
696
|
-
flags=_dashr_ast.PyCF_ALLOW_TOP_LEVEL_AWAIT,
|
|
697
|
-
)
|
|
698
|
-
# IPython's own run_code pattern: with PyCF_ALLOW_TOP_LEVEL_AWAIT, plain
|
|
699
|
-
# exec() DISCARDS the module coroutine silently; eval() under await
|
|
700
|
-
# returns it and runs the code (IPython InteractiveShell.run_code does
|
|
701
|
-
# exactly 'await eval(code_obj, user_global_ns, user_ns)').
|
|
702
|
-
result = eval(code, user_ns, user_ns)
|
|
703
|
-
if _dashr_inspect.iscoroutine(result):
|
|
704
|
-
await result
|
|
705
|
-
value = user_ns.pop('__dashr_cell_value__', _DASHR_NO_VALUE)
|
|
706
|
-
if value is None:
|
|
707
|
-
return _DASHR_NO_VALUE
|
|
708
|
-
return value
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
def _dashr_install_bindings(spec_json):
|
|
712
|
-
spec = _dashr_json.loads(spec_json)
|
|
713
|
-
user_ns = globals()
|
|
714
|
-
injected = user_ns.get('__dashr_injected__', {})
|
|
715
|
-
for old_name, old_spec in list(injected.items()):
|
|
716
|
-
if old_name not in spec:
|
|
717
|
-
user_ns.pop(old_name, None)
|
|
718
|
-
if old_spec.get('errorClass'):
|
|
719
|
-
user_ns.pop(old_spec['errorClass']['name'], None)
|
|
720
|
-
fresh = {}
|
|
721
|
-
# The flat per-tool shape declares the SAME error class on many
|
|
722
|
-
# namespaces: materialize each DISTINCT name once, so every proxy's
|
|
723
|
-
# raise-time globals() lookup finds ONE class and a single
|
|
724
|
-
# 'except ToolCallError' catches failures from every binding.
|
|
725
|
-
error_classes = {}
|
|
726
|
-
for global_name, namespace in spec.items():
|
|
727
|
-
error_class = namespace.get('errorClass')
|
|
728
|
-
if namespace.get('callable'):
|
|
729
|
-
# Exactly one function entry was validated by the host; its key is
|
|
730
|
-
# the transport name, the bare global itself is the callable.
|
|
731
|
-
function_name = next(iter(namespace['functions']))
|
|
732
|
-
user_ns[global_name] = _dashr_make_callable(
|
|
733
|
-
global_name,
|
|
734
|
-
function_name,
|
|
735
|
-
error_class['name'] if error_class else None,
|
|
736
|
-
error_class['memberNameProperty'] if error_class else None,
|
|
737
|
-
)
|
|
738
|
-
else:
|
|
739
|
-
user_ns[global_name] = _dashr_make_holder(
|
|
740
|
-
global_name,
|
|
741
|
-
error_class['name'] if error_class else None,
|
|
742
|
-
error_class['memberNameProperty'] if error_class else None,
|
|
743
|
-
sorted(namespace['functions']),
|
|
744
|
-
)
|
|
745
|
-
if error_class and error_class['name'] not in error_classes:
|
|
746
|
-
cls = _dashr_make_error_class(
|
|
747
|
-
error_class['name'],
|
|
748
|
-
error_class['memberNameProperty'],
|
|
749
|
-
)
|
|
750
|
-
error_classes[error_class['name']] = cls
|
|
751
|
-
user_ns[error_class['name']] = cls
|
|
752
|
-
fresh[global_name] = namespace
|
|
753
|
-
user_ns['__dashr_injected__'] = fresh
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
def _dashr_encode(value):
|
|
757
|
-
if value is _DASHR_NO_VALUE:
|
|
758
|
-
return {'ok': True}
|
|
759
|
-
try:
|
|
760
|
-
return {'ok': True, 'json': _dashr_json.dumps(value, allow_nan=False)}
|
|
761
|
-
except (TypeError, ValueError, OverflowError):
|
|
762
|
-
# Not lossless JSON: pass its repr TEXT through instead of failing
|
|
763
|
-
# the cell. The upstream renders the tool result as text either way,
|
|
764
|
-
# so a non-JSON value costs the model nothing; the host keeps a
|
|
765
|
-
# plain string it forwards untouched. The envelope itself never
|
|
766
|
-
# fails, so the completion never gate-rejects a return value.
|
|
767
|
-
return {'ok': True, 'json': _dashr_json.dumps(repr(value))}
|
|
768
|
-
`;
|
|
179
|
+
/** Module-qualified initialization survives OMP %reset without leaking imports. */
|
|
180
|
+
const KERNEL_BOOTSTRAP = `__import__('better_support').initialize(globals())`;
|
|
769
181
|
|
|
770
182
|
//#endregion
|
|
771
183
|
//#region src/python.ts
|
|
772
|
-
/**
|
|
773
|
-
* Build the single cell for one run.
|
|
774
|
-
* @param program - the model's cell program, executed at module scope.
|
|
775
|
-
* @param specJson - the JSON-encoded {@link NamespaceSpecs} map.
|
|
776
|
-
* @param sentinel - per-run nonce prefix; the envelope line starts with it.
|
|
777
|
-
* @returns the complete cell source.
|
|
778
|
-
*/
|
|
779
184
|
function buildRunCell(program, specJson, sentinel) {
|
|
780
|
-
return
|
|
781
|
-
`_dashr_state['executing'] = True`,
|
|
782
|
-
`try:`,
|
|
783
|
-
` _dashr_install_bindings(${JSON.stringify(specJson)})`,
|
|
784
|
-
` __dashr_completion__ = _dashr_encode(await _dashr_run_program(${JSON.stringify(program)}))`,
|
|
785
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps(__dashr_completion__))`,
|
|
786
|
-
`finally:`,
|
|
787
|
-
` _dashr_state['executing'] = False`
|
|
788
|
-
].join("\n");
|
|
185
|
+
return `await __import__('better_support').run(globals(), ${JSON.stringify(program)}, ${JSON.stringify(specJson)}, ${JSON.stringify(sentinel)})`;
|
|
789
186
|
}
|
|
790
|
-
/**
|
|
791
|
-
* Build the internal cell that dumps the user namespace to a dill snapshot and
|
|
792
|
-
* writes a JSON manifest next to it. Only USER state is captured: IPython's
|
|
793
|
-
* own session plumbing (`exit`, `quit`, `get_ipython`, … tracked in the
|
|
794
|
-
* shell's `user_ns_hidden` and still bound to the object IPython installed)
|
|
795
|
-
* reaches unpicklable kernel machinery — e.g. the history manager's
|
|
796
|
-
* sqlite3.Connection — and is never restored as program state anyway. Shim
|
|
797
|
-
* names (any `_dashr`/`__dashr` spelling) are excluded by prefix.
|
|
798
|
-
*
|
|
799
|
-
* Size-cap policy (M3-B, blueprint §8.3): dill cannot report a serialized
|
|
800
|
-
* size before dumping, and a GB-sized `DataFrame` dumped EVERY turn is the IO
|
|
801
|
-
* storm the cap exists to stop. The cell therefore gates in two tiers:
|
|
802
|
-
* first a bounded-depth namespace walk estimates the footprint (counting
|
|
803
|
-
* container lengths and reading `nbytes` / `memory_usage(deep=True)` for
|
|
804
|
-
* numpy/pandas objects — the stated pathological case — so the dump is
|
|
805
|
-
* skipped BEFORE any dill IO), then the actual dump goes to a sibling
|
|
806
|
-
* `.part` file whose true byte size is measured before it replaces the
|
|
807
|
-
* previous good snapshot. Skipped snapshots leave the previous
|
|
808
|
-
* `state.dill` + manifest untouched, so a revive restores the last good turn.
|
|
809
|
-
* @param payloadPath - absolute path for the dill payload.
|
|
810
|
-
* @param manifestPath - absolute path for the JSON manifest.
|
|
811
|
-
* @param spec - turn counter, skills list, and the size cap.
|
|
812
|
-
* @param sentinel - per-cell nonce prefix for the outcome envelope.
|
|
813
|
-
* @returns the complete cell source.
|
|
814
|
-
*/
|
|
815
|
-
function buildSnapshotCell(payloadPath, manifestPath, spec, sentinel) {
|
|
816
|
-
return [
|
|
817
|
-
`_dashr_state['executing'] = True`,
|
|
818
|
-
`try:`,
|
|
819
|
-
` import dill as _dashr_dill, json as _dashr_json, platform as _dashr_platform`,
|
|
820
|
-
` import os as _dashr_os, sys as _dashr_sys, datetime as _dashr_dt`,
|
|
821
|
-
` _dashr_shell = globals().get('get_ipython', lambda: None)()`,
|
|
822
|
-
` _dashr_hidden = getattr(_dashr_shell, 'user_ns_hidden', None) or {}`,
|
|
823
|
-
` _dashr_user_ns = {`,
|
|
824
|
-
` name: value`,
|
|
825
|
-
` for name, value in globals().items()`,
|
|
826
|
-
` if not name.lower().startswith(('_dashr', '__dashr'))`,
|
|
827
|
-
` and (name not in _dashr_hidden or _dashr_hidden[name] is not value)`,
|
|
828
|
-
` }`,
|
|
829
|
-
` _dashr_cap = ${JSON.stringify(spec.sizeCapBytes)}`,
|
|
830
|
-
` def _dashr_estimate(value, depth):`,
|
|
831
|
-
` if value is None or isinstance(value, (bool, int, float, complex)):`,
|
|
832
|
-
` return 32`,
|
|
833
|
-
` if isinstance(value, (str, bytes, bytearray)):`,
|
|
834
|
-
` return len(value) + 64`,
|
|
835
|
-
` if isinstance(value, (list, tuple, set, frozenset)):`,
|
|
836
|
-
` if depth <= 0:`,
|
|
837
|
-
` return 4096`,
|
|
838
|
-
` return 64 + sum(_dashr_estimate(item, depth - 1) for item in value)`,
|
|
839
|
-
` if isinstance(value, dict):`,
|
|
840
|
-
` if depth <= 0:`,
|
|
841
|
-
` return 4096`,
|
|
842
|
-
` total = 64`,
|
|
843
|
-
` for key, item in value.items():`,
|
|
844
|
-
` total += _dashr_estimate(key, depth - 1)`,
|
|
845
|
-
` total += _dashr_estimate(item, depth - 1)`,
|
|
846
|
-
` if total > _dashr_cap:`,
|
|
847
|
-
` return total`,
|
|
848
|
-
` return total`,
|
|
849
|
-
` _dashr_nbytes = getattr(value, 'nbytes', None)`,
|
|
850
|
-
` if isinstance(_dashr_nbytes, int) and _dashr_nbytes > 0:`,
|
|
851
|
-
` return _dashr_nbytes + 128`,
|
|
852
|
-
` _dashr_mem = getattr(value, 'memory_usage', None)`,
|
|
853
|
-
` if callable(_dashr_mem):`,
|
|
854
|
-
` try:`,
|
|
855
|
-
` _dashr_usage = _dashr_mem(deep=True)`,
|
|
856
|
-
` _dashr_sum = getattr(_dashr_usage, 'sum', None)`,
|
|
857
|
-
` if callable(_dashr_sum):`,
|
|
858
|
-
` # pandas returns a numpy scalar (np.int64), which is`,
|
|
859
|
-
` # not a Python int subclass; coerce, don't isinstance.`,
|
|
860
|
-
` _dashr_n = int(_dashr_sum())`,
|
|
861
|
-
` if _dashr_n > 0:`,
|
|
862
|
-
` return _dashr_n + 128`,
|
|
863
|
-
` except Exception:`,
|
|
864
|
-
` pass`,
|
|
865
|
-
` return 4096`,
|
|
866
|
-
` _dashr_estimate_bytes = sum(_dashr_estimate(value, 4) for value in _dashr_user_ns.values())`,
|
|
867
|
-
` if _dashr_estimate_bytes > _dashr_cap:`,
|
|
868
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({`,
|
|
869
|
-
` 'ok': False, 'skipped': True, 'reason': 'estimate',`,
|
|
870
|
-
` 'estimateBytes': _dashr_estimate_bytes, 'capBytes': _dashr_cap,`,
|
|
871
|
-
` }))`,
|
|
872
|
-
` else:`,
|
|
873
|
-
` _dashr_part = ${JSON.stringify(payloadPath)} + '.part'`,
|
|
874
|
-
` try:`,
|
|
875
|
-
` with open(_dashr_part, 'wb') as _dashr_f:`,
|
|
876
|
-
` _dashr_dill.dump(_dashr_user_ns, _dashr_f)`,
|
|
877
|
-
` _dashr_size = _dashr_os.path.getsize(_dashr_part)`,
|
|
878
|
-
` if _dashr_size > _dashr_cap:`,
|
|
879
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({`,
|
|
880
|
-
` 'ok': False, 'skipped': True, 'reason': 'actual',`,
|
|
881
|
-
` 'sizeBytes': _dashr_size, 'capBytes': _dashr_cap,`,
|
|
882
|
-
` }))`,
|
|
883
|
-
` else:`,
|
|
884
|
-
` _dashr_os.replace(_dashr_part, ${JSON.stringify(payloadPath)})`,
|
|
885
|
-
` _dashr_manifest = {`,
|
|
886
|
-
` 'snapshotFormat': 1,`,
|
|
887
|
-
` 'turn': ${JSON.stringify(spec.turn)},`,
|
|
888
|
-
` 'pythonVersion': _dashr_platform.python_version(),`,
|
|
889
|
-
` 'venvPath': _dashr_sys.executable,`,
|
|
890
|
-
` 'skills': ${JSON.stringify(spec.skills)},`,
|
|
891
|
-
` 'names': sorted(_dashr_user_ns),`,
|
|
892
|
-
` 'sizeBytes': _dashr_size,`,
|
|
893
|
-
` 'skipped': False,`,
|
|
894
|
-
` 'createdAt': _dashr_dt.datetime.now(_dashr_dt.timezone.utc).isoformat(),`,
|
|
895
|
-
` }`,
|
|
896
|
-
` with open(${JSON.stringify(manifestPath)}, 'w', encoding='utf-8') as _dashr_f:`,
|
|
897
|
-
` _dashr_json.dump(_dashr_manifest, _dashr_f)`,
|
|
898
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({`,
|
|
899
|
-
` 'ok': True, 'sizeBytes': _dashr_size, 'names': len(_dashr_user_ns),`,
|
|
900
|
-
` }))`,
|
|
901
|
-
` finally:`,
|
|
902
|
-
` if _dashr_os.path.exists(_dashr_part):`,
|
|
903
|
-
` try:`,
|
|
904
|
-
` _dashr_os.unlink(_dashr_part)`,
|
|
905
|
-
` except OSError:`,
|
|
906
|
-
` pass`,
|
|
907
|
-
`finally:`,
|
|
908
|
-
` _dashr_state['executing'] = False`
|
|
909
|
-
].join("\n");
|
|
910
|
-
}
|
|
911
|
-
/**
|
|
912
|
-
* Build the internal cell that restores a dill snapshot into the user
|
|
913
|
-
* namespace. The kernel validates the manifest ITSELF (it is the only party
|
|
914
|
-
* that knows its own python version and `sys.executable`): a python-version
|
|
915
|
-
* mismatch, a different interpreter (`venvPath`), a non-empty skills list, or
|
|
916
|
-
* an unreadable/corrupt payload degrades to an empty namespace WITHOUT
|
|
917
|
-
* touching globals, and reports the reason through the envelope so the host
|
|
918
|
-
* can tell the model "snapshot not replayable, started from empty". Shim and
|
|
919
|
-
* IPython-hidden names are re-filtered on the way in — the same exclusion the
|
|
920
|
-
* snapshot applied on the way out, applied again defensively.
|
|
921
|
-
* @param payloadPath - absolute path of the dill payload.
|
|
922
|
-
* @param manifestPath - absolute path of the JSON manifest.
|
|
923
|
-
* @param sentinel - per-cell nonce prefix for the outcome envelope.
|
|
924
|
-
* @returns the complete cell source.
|
|
925
|
-
*/
|
|
926
|
-
function buildRestoreCell(payloadPath, manifestPath, sentinel) {
|
|
927
|
-
return [
|
|
928
|
-
`_dashr_state['executing'] = True`,
|
|
929
|
-
`try:`,
|
|
930
|
-
` import dill as _dashr_dill, json as _dashr_json, platform as _dashr_platform`,
|
|
931
|
-
` import os as _dashr_os, sys as _dashr_sys`,
|
|
932
|
-
` _dashr_manifest = None`,
|
|
933
|
-
` _dashr_reason = 'snapshot manifest could not be read'`,
|
|
934
|
-
` try:`,
|
|
935
|
-
` with open(${JSON.stringify(manifestPath)}, 'r', encoding='utf-8') as _dashr_f:`,
|
|
936
|
-
` _dashr_manifest = _dashr_json.load(_dashr_f)`,
|
|
937
|
-
` except Exception as _dashr_err:`,
|
|
938
|
-
` _dashr_reason = 'snapshot manifest could not be read: ' + repr(_dashr_err)`,
|
|
939
|
-
` if _dashr_manifest is None:`,
|
|
940
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': False, 'reason': _dashr_reason}))`,
|
|
941
|
-
` elif _dashr_manifest.get('snapshotFormat') != 1:`,
|
|
942
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': False, 'reason': 'snapshot format is not replayable'}))`,
|
|
943
|
-
` elif _dashr_manifest.get('skipped'):`,
|
|
944
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': False, 'reason': 'snapshot was skipped (namespace exceeded the size cap)'}))`,
|
|
945
|
-
` elif _dashr_manifest.get('pythonVersion') != _dashr_platform.python_version():`,
|
|
946
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': False, 'reason': 'python version mismatch: snapshot ' + str(_dashr_manifest.get('pythonVersion')) + ' vs kernel ' + _dashr_platform.python_version()}))`,
|
|
947
|
-
` elif _dashr_os.path.realpath(_dashr_manifest.get('venvPath') or '') != _dashr_os.path.realpath(_dashr_sys.executable):`,
|
|
948
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': False, 'reason': 'kernel interpreter differs from the snapshot venv'}))`,
|
|
949
|
-
` elif _dashr_manifest.get('skills'):`,
|
|
950
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': False, 'reason': 'snapshot skills are not replayable in this provider'}))`,
|
|
951
|
-
` else:`,
|
|
952
|
-
` try:`,
|
|
953
|
-
` with open(${JSON.stringify(payloadPath)}, 'rb') as _dashr_f:`,
|
|
954
|
-
` _dashr_restored = _dashr_dill.load(_dashr_f)`,
|
|
955
|
-
` except Exception as _dashr_err:`,
|
|
956
|
-
` _dashr_restored = None`,
|
|
957
|
-
` _dashr_reason = 'snapshot payload could not be loaded: ' + repr(_dashr_err)`,
|
|
958
|
-
` if not isinstance(_dashr_restored, dict):`,
|
|
959
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': False, 'reason': _dashr_reason if _dashr_restored is None else 'snapshot payload is not a namespace dict'}))`,
|
|
960
|
-
` else:`,
|
|
961
|
-
` _dashr_shell = globals().get('get_ipython', lambda: None)()`,
|
|
962
|
-
` _dashr_hidden = getattr(_dashr_shell, 'user_ns_hidden', None) or {}`,
|
|
963
|
-
` _dashr_user_ns = globals()`,
|
|
964
|
-
` _dashr_restored_count = 0`,
|
|
965
|
-
` for _dashr_name, _dashr_value in _dashr_restored.items():`,
|
|
966
|
-
` if not isinstance(_dashr_name, str):`,
|
|
967
|
-
` continue`,
|
|
968
|
-
` if _dashr_name.lower().startswith(('_dashr', '__dashr')):`,
|
|
969
|
-
` continue`,
|
|
970
|
-
` if _dashr_name in _dashr_hidden and _dashr_hidden[_dashr_name] is _dashr_value:`,
|
|
971
|
-
` continue`,
|
|
972
|
-
` _dashr_user_ns[_dashr_name] = _dashr_value`,
|
|
973
|
-
` _dashr_restored_count += 1`,
|
|
974
|
-
` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': True, 'restored': _dashr_restored_count}))`,
|
|
975
|
-
`finally:`,
|
|
976
|
-
` _dashr_state['executing'] = False`
|
|
977
|
-
].join("\n");
|
|
978
|
-
}
|
|
979
|
-
/**
|
|
980
|
-
* Build the internal cell that reads ONE user-namespace variable by name, or
|
|
981
|
-
* — when `name` is `null` — lists the namespace's user-variable names. The
|
|
982
|
-
* same exclusion the snapshot applies on the way out (any `_dashr`/`__dashr`
|
|
983
|
-
* spelling plus IPython's `user_ns_hidden`) defines what "user variable"
|
|
984
|
-
* means here, so `ctx://` sees exactly the namespace a revive would restore.
|
|
985
|
-
*
|
|
986
|
-
* Serialization boundary (design.md D4): a value that survives
|
|
987
|
-
* `json.dumps(…, allow_nan=False)` crosses as `kind: 'json'` text; anything
|
|
988
|
-
* else falls back to `repr` text (`kind: 'repr'`) so a non-JSON variable
|
|
989
|
-
* (the reason the snapshot uses dill at all) is still readable as text. The
|
|
990
|
-
* host keeps the `kind` so the presentation can annotate a repr as "this is
|
|
991
|
-
* repr text, not a JSON value".
|
|
992
|
-
* @param name - the exact variable name, or `null` to list namespace names.
|
|
993
|
-
* @param sentinel - per-cell nonce prefix for the outcome envelope.
|
|
994
|
-
* @returns the complete cell source.
|
|
995
|
-
*/
|
|
996
187
|
function buildQueryVarCell(name$1, sentinel) {
|
|
997
|
-
|
|
998
|
-
`_dashr_state['executing'] = True`,
|
|
999
|
-
`try:`,
|
|
1000
|
-
` import json as _dashr_json`,
|
|
1001
|
-
` _dashr_shell = globals().get('get_ipython', lambda: None)()`,
|
|
1002
|
-
` _dashr_hidden = getattr(_dashr_shell, 'user_ns_hidden', None) or {}`,
|
|
1003
|
-
` _dashr_user_ns = {`,
|
|
1004
|
-
` _name: _value`,
|
|
1005
|
-
` for _name, _value in globals().items()`,
|
|
1006
|
-
` if not _name.lower().startswith(('_dashr', '__dashr'))`,
|
|
1007
|
-
` and (_name not in _dashr_hidden or _dashr_hidden[_name] is not _value)`,
|
|
1008
|
-
` }`
|
|
1009
|
-
];
|
|
1010
|
-
if (name$1 === null) lines.push(` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': True, 'kind': 'names', 'names': sorted(_dashr_user_ns)}))`);
|
|
1011
|
-
else lines.push(` _dashr_name = ${JSON.stringify(name$1)}`, ` if _dashr_name not in _dashr_user_ns:`, ` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': True, 'kind': 'missing'}))`, ` else:`, ` _dashr_value = _dashr_user_ns[_dashr_name]`, ` try:`, ` _dashr_text = _dashr_json.dumps(_dashr_value, allow_nan=False)`, ` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': True, 'kind': 'json', 'text': _dashr_text}))`, ` except (TypeError, ValueError, OverflowError):`, ` print(${JSON.stringify(sentinel)} + _dashr_json.dumps({'ok': True, 'kind': 'repr', 'text': repr(_dashr_value)}))`);
|
|
1012
|
-
lines.push(`finally:`, ` _dashr_state['executing'] = False`);
|
|
1013
|
-
return lines.join("\n");
|
|
188
|
+
return `__import__('better_support').query(globals(), ${JSON.stringify(JSON.stringify(name$1))}, ${JSON.stringify(sentinel)})`;
|
|
1014
189
|
}
|
|
1015
|
-
/**
|
|
1016
|
-
* Build the internal cell that assigns one JSON value into the user
|
|
1017
|
-
* namespace under `name`. The value is carried as pre-serialized JSON text
|
|
1018
|
-
* (the host validated it as lossless JSON and the name as a usable
|
|
1019
|
-
* identifier), so the cell only decodes and binds — no arbitrary object can
|
|
1020
|
-
* cross the wire. Bracketed by the same SIGALRM busy guard as a run cell.
|
|
1021
|
-
* @param name - the validated identifier to assign under.
|
|
1022
|
-
* @param valueJson - the lossless-JSON text to decode and bind.
|
|
1023
|
-
* @param sentinel - per-cell nonce prefix for the outcome envelope.
|
|
1024
|
-
* @returns the complete cell source.
|
|
1025
|
-
*/
|
|
1026
190
|
function buildSetVarCell(name$1, valueJson, sentinel) {
|
|
1027
|
-
return
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
`finally:`,
|
|
1035
|
-
` _dashr_state['executing'] = False`
|
|
1036
|
-
].join("\n");
|
|
191
|
+
return `__import__('better_support').set_var(globals(), ${JSON.stringify(name$1)}, ${JSON.stringify(valueJson)}, ${JSON.stringify(sentinel)})`;
|
|
192
|
+
}
|
|
193
|
+
function buildSnapshotCell(payloadPath, manifestPath, spec, sentinel) {
|
|
194
|
+
return `__import__('better_snapshot').save(globals(), ${JSON.stringify(payloadPath)}, ${JSON.stringify(manifestPath)}, ${JSON.stringify(JSON.stringify(spec))}, ${JSON.stringify(sentinel)})`;
|
|
195
|
+
}
|
|
196
|
+
function buildRestoreCell(payloadPath, manifestPath, sentinel, bindings = []) {
|
|
197
|
+
return `__import__('better_snapshot').restore(globals(), ${JSON.stringify(payloadPath)}, ${JSON.stringify(manifestPath)}, ${JSON.stringify(sentinel)}, ${JSON.stringify(JSON.stringify(bindings))})`;
|
|
1037
198
|
}
|
|
1038
199
|
|
|
1039
200
|
//#endregion
|
|
1040
201
|
//#region src/output.ts
|
|
1041
|
-
/** ANSI color escapes, which
|
|
202
|
+
/** ANSI color escapes, which Python may embed in tracebacks. */
|
|
1042
203
|
const ANSI_ESCAPE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
1043
204
|
/** Smallest cap that can represent an empty logs array plus a quoted message. */
|
|
1044
205
|
const MIN_OUTPUT_BYTES = 4;
|
|
@@ -1067,7 +228,7 @@ function extractStream(streamText, sentinel) {
|
|
|
1067
228
|
envelope: envelope === "" ? void 0 : envelope
|
|
1068
229
|
};
|
|
1069
230
|
}
|
|
1070
|
-
/** Strip ANSI escapes from
|
|
231
|
+
/** Strip ANSI escapes from a Python traceback for a model-facing message. */
|
|
1071
232
|
function plainTraceback(ename, evalue, traceback) {
|
|
1072
233
|
const body = traceback.map((line) => line.replace(ANSI_ESCAPE, "")).join("\n");
|
|
1073
234
|
return body.length > 0 ? `${ename}: ${evalue}\n${body}` : `${ename}: ${evalue}`;
|
|
@@ -1122,775 +283,436 @@ function finalizeOutput(logs, value, error, maxBytes) {
|
|
|
1122
283
|
|
|
1123
284
|
//#endregion
|
|
1124
285
|
//#region src/kernel.ts
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const IOPUB_SUBSCRIBE_DELAY_MS = 50;
|
|
1128
|
-
const CONNECT_POLL_MS = 25;
|
|
1129
|
-
const SHUTDOWN_DRAIN_MS = 200;
|
|
1130
|
-
function isRecord(value) {
|
|
1131
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1132
|
-
}
|
|
1133
|
-
function buildMessage(msgType, content, session, username) {
|
|
1134
|
-
return {
|
|
1135
|
-
header: {
|
|
1136
|
-
msg_id: randomUUID(),
|
|
1137
|
-
session,
|
|
1138
|
-
username,
|
|
1139
|
-
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1140
|
-
msg_type: msgType,
|
|
1141
|
-
version: PROTOCOL_VERSION
|
|
1142
|
-
},
|
|
1143
|
-
parent_header: {},
|
|
1144
|
-
content
|
|
1145
|
-
};
|
|
286
|
+
function record(value) {
|
|
287
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1146
288
|
}
|
|
1147
|
-
|
|
1148
|
-
const info = {
|
|
1149
|
-
ip: "127.0.0.1",
|
|
1150
|
-
transport: "tcp",
|
|
1151
|
-
shell_port: 0,
|
|
1152
|
-
iopub_port: 0,
|
|
1153
|
-
stdin_port: 0,
|
|
1154
|
-
control_port: 0,
|
|
1155
|
-
hb_port: 0,
|
|
1156
|
-
signature_scheme: "hmac-sha256",
|
|
1157
|
-
key: randomBytes(16).toString("hex"),
|
|
1158
|
-
kernel_name: "python3"
|
|
1159
|
-
};
|
|
1160
|
-
const tempDir = mkdtempSync(join(tmpdir(), "dashr-kernel-"));
|
|
1161
|
-
const path$1 = join(tempDir, "connection.json");
|
|
1162
|
-
writeFileSync(path$1, JSON.stringify(info, null, 2), { mode: 384 });
|
|
1163
|
-
return {
|
|
1164
|
-
path: path$1,
|
|
1165
|
-
tempDir
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
function readResolvedConnection(path$1) {
|
|
1169
|
-
try {
|
|
1170
|
-
const parsed = JSON.parse(readFileSync(path$1, "utf8"));
|
|
1171
|
-
if (!isRecord(parsed) || parsed.ip !== "127.0.0.1" || parsed.transport !== "tcp" || parsed.signature_scheme !== "hmac-sha256" || typeof parsed.key !== "string") return null;
|
|
1172
|
-
for (const port of [
|
|
1173
|
-
"shell_port",
|
|
1174
|
-
"iopub_port",
|
|
1175
|
-
"stdin_port",
|
|
1176
|
-
"control_port",
|
|
1177
|
-
"hb_port"
|
|
1178
|
-
]) {
|
|
1179
|
-
const value = parsed[port];
|
|
1180
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return null;
|
|
1181
|
-
}
|
|
1182
|
-
return parsed;
|
|
1183
|
-
} catch {
|
|
1184
|
-
return null;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
/**
|
|
1188
|
-
* One persistent IPython kernel subprocess with its ZeroMQ channels. Cell
|
|
1189
|
-
* execution is serialized on an internal queue (the Jupyter shell channel is
|
|
1190
|
-
* request/reply).
|
|
1191
|
-
*/
|
|
289
|
+
/** Historical class name retained for the runtime's private integration seam. */
|
|
1192
290
|
var IpyKernelBridge = class {
|
|
1193
|
-
session = randomUUID();
|
|
1194
291
|
state = "idle";
|
|
292
|
+
child;
|
|
1195
293
|
startPromise;
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
servedComms = /* @__PURE__ */ new Set();
|
|
294
|
+
disposePromise;
|
|
295
|
+
queue = Promise.resolve();
|
|
296
|
+
active;
|
|
297
|
+
stderr = "";
|
|
298
|
+
deathFailure;
|
|
299
|
+
callbackServer;
|
|
300
|
+
closePromise;
|
|
301
|
+
ownedReaders = [];
|
|
1205
302
|
constructor(options) {
|
|
1206
303
|
this.options = options;
|
|
1207
304
|
}
|
|
1208
|
-
/** The kernel subprocess pid, for lifecycle assertions; absent before spawn. */
|
|
1209
305
|
get pid() {
|
|
1210
|
-
return this.
|
|
306
|
+
return this.child?.pid;
|
|
1211
307
|
}
|
|
1212
308
|
get isRunning() {
|
|
1213
309
|
return this.state === "running";
|
|
1214
310
|
}
|
|
1215
|
-
/**
|
|
1216
|
-
* Whether the subprocess is gone (crashed or disposed). A dead bridge is
|
|
1217
|
-
* never resurrected — the owning provider replaces it with a fresh one
|
|
1218
|
-
* (respawn, M3-A), because kernel state is unrecoverable in-process.
|
|
1219
|
-
*/
|
|
1220
311
|
get isDead() {
|
|
1221
312
|
return this.state === "shutdown";
|
|
1222
313
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
* @throws when the subprocess cannot spawn, ports never resolve, the
|
|
1226
|
-
* kernel_info probe times out, or the bootstrap cell fails.
|
|
1227
|
-
*/
|
|
1228
|
-
async start() {
|
|
1229
|
-
if (!this.startPromise) this.startPromise = this.doStart().catch((error) => {
|
|
1230
|
-
this.startPromise = void 0;
|
|
1231
|
-
throw error;
|
|
1232
|
-
});
|
|
314
|
+
start() {
|
|
315
|
+
this.startPromise ??= this.doStart();
|
|
1233
316
|
return this.startPromise;
|
|
1234
317
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
318
|
+
async windowsCallbacks() {
|
|
319
|
+
const token = randomBytes(32).toString("hex");
|
|
320
|
+
const server = createServer((req, response) => {
|
|
321
|
+
if (req.method !== "POST" || req.url !== "/callback" || req.headers.authorization !== `Bearer ${token}`) {
|
|
322
|
+
response.writeHead(403).end();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
let body = "";
|
|
326
|
+
req.setEncoding("utf8");
|
|
327
|
+
req.on("data", (data) => {
|
|
328
|
+
body += data;
|
|
329
|
+
});
|
|
330
|
+
req.on("end", () => {
|
|
331
|
+
let parsed;
|
|
332
|
+
try {
|
|
333
|
+
parsed = JSON.parse(body);
|
|
334
|
+
} catch {
|
|
335
|
+
response.writeHead(400).end();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this.callback(parsed).then((reply) => {
|
|
339
|
+
if (!response.destroyed) response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(reply));
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
this.callbackServer = server;
|
|
344
|
+
await new Promise((resolve$1, reject) => {
|
|
345
|
+
server.once("error", reject);
|
|
346
|
+
server.listen(0, "127.0.0.1", () => {
|
|
347
|
+
server.off("error", reject);
|
|
348
|
+
resolve$1();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
const address = server.address();
|
|
352
|
+
if (!address || typeof address === "string") throw new Error("callback endpoint unavailable");
|
|
353
|
+
return {
|
|
354
|
+
BETTER_CALLBACK_URL: `http://127.0.0.1:${address.port}/callback`,
|
|
355
|
+
BETTER_CALLBACK_TOKEN: token
|
|
356
|
+
};
|
|
1237
357
|
}
|
|
1238
358
|
async doStart() {
|
|
1239
|
-
if (this.state !== "idle")
|
|
359
|
+
if (this.state !== "idle") throw new Error("Python bridge cannot restart after shutdown");
|
|
1240
360
|
this.state = "starting";
|
|
1241
|
-
const { path: path$1, tempDir } = makeConnection();
|
|
1242
|
-
this.tempDir = tempDir;
|
|
1243
|
-
const kernel = spawn(this.options.python, [
|
|
1244
|
-
"-m",
|
|
1245
|
-
"ipykernel_launcher",
|
|
1246
|
-
"-f",
|
|
1247
|
-
path$1
|
|
1248
|
-
], {
|
|
1249
|
-
cwd: this.options.cwd,
|
|
1250
|
-
env: {
|
|
1251
|
-
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
1252
|
-
LANG: "C.UTF-8",
|
|
1253
|
-
HOME: process.env.HOME ?? tmpdir()
|
|
1254
|
-
},
|
|
1255
|
-
stdio: [
|
|
1256
|
-
"ignore",
|
|
1257
|
-
"pipe",
|
|
1258
|
-
"pipe"
|
|
1259
|
-
]
|
|
1260
|
-
});
|
|
1261
|
-
this.kernel = kernel;
|
|
1262
|
-
kernel.stderr?.on("data", (buf) => {
|
|
1263
|
-
this.kernelStderr += buf.toString();
|
|
1264
|
-
});
|
|
1265
|
-
kernel.on("error", (error) => {
|
|
1266
|
-
this.kernelStderr += `spawn error: ${error.message}\n`;
|
|
1267
|
-
this.onKernelDeath();
|
|
1268
|
-
});
|
|
1269
|
-
kernel.on("exit", (code, signal) => {
|
|
1270
|
-
if (this.state !== "shutdown") this.kernelStderr += `unexpected exit code=${code} signal=${signal}\n`;
|
|
1271
|
-
this.onKernelDeath();
|
|
1272
|
-
});
|
|
1273
|
-
let conn;
|
|
1274
361
|
try {
|
|
1275
|
-
|
|
1276
|
-
this.
|
|
1277
|
-
|
|
1278
|
-
this.
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
362
|
+
const resources = fileURLToPath(new URL("./omp-python/", import.meta.url));
|
|
363
|
+
const callbackEnv = process.platform === "win32" ? await this.windowsCallbacks() : {};
|
|
364
|
+
if (this.isDead) throw new Error("Python startup cancelled by disposal");
|
|
365
|
+
const child = spawn(this.options.python, ["-u", fileURLToPath(new URL("./omp-python/runner.py", import.meta.url))], {
|
|
366
|
+
...this.options.cwd ? { cwd: this.options.cwd } : {},
|
|
367
|
+
detached: process.platform !== "win32",
|
|
368
|
+
stdio: process.platform === "win32" ? [
|
|
369
|
+
"pipe",
|
|
370
|
+
"pipe",
|
|
371
|
+
"pipe"
|
|
372
|
+
] : [
|
|
373
|
+
"pipe",
|
|
374
|
+
"pipe",
|
|
375
|
+
"pipe",
|
|
376
|
+
"pipe",
|
|
377
|
+
"pipe"
|
|
378
|
+
],
|
|
379
|
+
env: {
|
|
380
|
+
...process.env,
|
|
381
|
+
PYTHONUNBUFFERED: "1",
|
|
382
|
+
PYTHONIOENCODING: "utf-8",
|
|
383
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
384
|
+
PYTHONPATH: [resources, process.env.PYTHONPATH].filter(Boolean).join(process.platform === "win32" ? ";" : ":"),
|
|
385
|
+
...callbackEnv
|
|
386
|
+
}
|
|
1296
387
|
});
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
this.
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
388
|
+
this.child = child;
|
|
389
|
+
this.closePromise = new Promise((resolve$1) => child.once("close", () => resolve$1()));
|
|
390
|
+
child.on("error", (error) => this.died(`Python process error: ${error.message}`));
|
|
391
|
+
child.on("exit", (code, signal) => this.died(`Python process exited (${signal ?? code}); stderr: ${this.stderr.slice(-2048)}`));
|
|
392
|
+
child.stdin?.on("error", (error) => this.died(`Python input pipe failed: ${error.message}`));
|
|
393
|
+
child.stderr?.setEncoding("utf8").on("data", (data) => {
|
|
394
|
+
this.stderr = (this.stderr + data).slice(-8192);
|
|
395
|
+
});
|
|
396
|
+
const frames = createInterface({ input: child.stdout });
|
|
397
|
+
this.ownedReaders.push(frames);
|
|
398
|
+
frames.on("line", (line) => {
|
|
399
|
+
try {
|
|
400
|
+
this.frame(JSON.parse(line));
|
|
401
|
+
} catch {
|
|
402
|
+
this.died("Python emitted invalid NDJSON protocol");
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
if (process.platform !== "win32") {
|
|
406
|
+
const requests = createInterface({ input: child.stdio[3] });
|
|
407
|
+
this.ownedReaders.push(requests);
|
|
408
|
+
const replies = child.stdio[4];
|
|
409
|
+
replies.on("error", () => {});
|
|
410
|
+
requests.on("line", (line) => {
|
|
411
|
+
let data;
|
|
412
|
+
try {
|
|
413
|
+
data = JSON.parse(line);
|
|
414
|
+
} catch {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.callback(data).then((reply) => {
|
|
418
|
+
if (!replies.destroyed && !replies.writableEnded) replies.write(JSON.stringify(reply) + "\n");
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const result = await this.perform(KERNEL_BOOTSTRAP + `\n__import__('better_snapshot').configure(${JSON.stringify(this.options.snapshotDirectory ?? "")}, ${this.options.snapshotSizeCapBytes ?? 268435456}, ${this.options.snapshotTimeoutMs})`, {
|
|
423
|
+
timeoutMs: this.options.startupTimeoutMs,
|
|
424
|
+
interruptGraceMs: 1e3,
|
|
425
|
+
interruptConfirmMs: this.options.interruptConfirmMs
|
|
426
|
+
});
|
|
427
|
+
if (result.outcome !== "completed" || result.status !== "ok") throw new Error(`OMP Python initialization failed: ${JSON.stringify(result)}`);
|
|
428
|
+
if (this.isDead) throw new Error("Python exited during initialization");
|
|
429
|
+
this.state = "running";
|
|
1308
430
|
} catch (error) {
|
|
1309
|
-
this.
|
|
1310
|
-
this.state = "idle";
|
|
431
|
+
await this.dispose();
|
|
1311
432
|
throw error;
|
|
1312
433
|
}
|
|
1313
|
-
const bootstrap = await this.executeCell(KERNEL_BOOTSTRAP, {
|
|
1314
|
-
timeoutMs: this.options.startupTimeoutMs,
|
|
1315
|
-
interruptGraceMs: 1e3,
|
|
1316
|
-
interruptConfirmMs: Math.min(this.options.interruptConfirmMs, 500)
|
|
1317
|
-
});
|
|
1318
|
-
if (bootstrap.outcome !== "completed" || bootstrap.status !== "ok") {
|
|
1319
|
-
const detail = bootstrap.outcome === "completed" && bootstrap.cellError ? `${bootstrap.cellError.ename}: ${bootstrap.cellError.evalue}` : "bootstrap cell failed";
|
|
1320
|
-
this.cleanupResources();
|
|
1321
|
-
this.state = "idle";
|
|
1322
|
-
throw new Error(`dashr kernel bootstrap failed: ${detail}`);
|
|
1323
|
-
}
|
|
1324
|
-
this.state = "running";
|
|
1325
|
-
}
|
|
1326
|
-
async waitForConnection(path$1) {
|
|
1327
|
-
const deadline = Date.now() + this.options.startupTimeoutMs;
|
|
1328
|
-
while (Date.now() < deadline) {
|
|
1329
|
-
if (this.state === "shutdown") throw new Error(`kernel exited before resolving ports. stderr:\n${this.stderrTail()}`);
|
|
1330
|
-
const info = readResolvedConnection(path$1);
|
|
1331
|
-
if (info) return info;
|
|
1332
|
-
await setTimeout$1(CONNECT_POLL_MS);
|
|
1333
|
-
}
|
|
1334
|
-
throw new Error(`kernel did not resolve connection ports within ${this.options.startupTimeoutMs}ms. stderr tail:\n${this.stderrTail()}`);
|
|
1335
434
|
}
|
|
1336
|
-
async
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
if (
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
435
|
+
async callback(data) {
|
|
436
|
+
const identity = record(data) ? {
|
|
437
|
+
id: data.id,
|
|
438
|
+
run: data.run
|
|
439
|
+
} : {};
|
|
440
|
+
const active = this.active;
|
|
441
|
+
if (!record(data) || !active || active.id !== data.run || active.pendingForce || !active.opts.hostRequestHandler) return {
|
|
442
|
+
...identity,
|
|
443
|
+
ok: false,
|
|
444
|
+
message: "host callback rejected: its cell is no longer active"
|
|
445
|
+
};
|
|
446
|
+
const revoked = {
|
|
447
|
+
...identity,
|
|
448
|
+
ok: false,
|
|
449
|
+
message: "host callback cancelled: its cell ended"
|
|
450
|
+
};
|
|
451
|
+
return new Promise((resolve$1) => {
|
|
452
|
+
let settled = false;
|
|
453
|
+
const finish = (reply) => {
|
|
454
|
+
if (settled) return;
|
|
455
|
+
settled = true;
|
|
456
|
+
active.rejectCallbacks.delete(reject);
|
|
457
|
+
resolve$1(reply);
|
|
458
|
+
};
|
|
459
|
+
const reject = () => finish(revoked);
|
|
460
|
+
active.rejectCallbacks.add(reject);
|
|
461
|
+
active.opts.hostRequestHandler(data).then((reply) => {
|
|
462
|
+
finish(this.active === active && !active.pendingForce ? {
|
|
463
|
+
...identity,
|
|
464
|
+
...reply
|
|
465
|
+
} : revoked);
|
|
466
|
+
}, (error) => finish({
|
|
467
|
+
...identity,
|
|
468
|
+
ok: false,
|
|
469
|
+
message: String(error)
|
|
470
|
+
}));
|
|
471
|
+
});
|
|
1346
472
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
473
|
+
frame(frame) {
|
|
474
|
+
if (!record(frame) || frame.id !== this.active?.id) return;
|
|
475
|
+
const active = this.active;
|
|
476
|
+
if (frame.type === "started") {
|
|
477
|
+
active.started = true;
|
|
478
|
+
if (active.pendingForce) this.signal("SIGINT");
|
|
479
|
+
} else if (frame.type === "stdout" || frame.type === "stderr") {
|
|
480
|
+
if (typeof frame.data === "string") active.streamText += frame.data;
|
|
481
|
+
} else if (frame.type === "display" || frame.type === "result") {
|
|
482
|
+
if (record(frame.bundle)) {
|
|
483
|
+
const text = frame.bundle["text/plain"] ?? frame.bundle["text/markdown"] ?? frame.bundle["text/html"];
|
|
484
|
+
if (typeof text === "string") active.streamText += `${text}\n`;
|
|
485
|
+
else if (frame.bundle["application/json"] !== void 0) active.streamText += `${JSON.stringify(frame.bundle["application/json"])}\n`;
|
|
486
|
+
else active.streamText += `[Python display: ${Object.keys(frame.bundle).join(", ")}]\n`;
|
|
487
|
+
}
|
|
488
|
+
} else if (frame.type === "error") active.error = {
|
|
489
|
+
ename: String(frame.ename),
|
|
490
|
+
evalue: String(frame.evalue),
|
|
491
|
+
traceback: Array.isArray(frame.traceback) ? frame.traceback.map(String) : []
|
|
492
|
+
};
|
|
493
|
+
else if (frame.type === "done") this.settle(active.pendingForce ? {
|
|
494
|
+
outcome: "forced",
|
|
495
|
+
failure: active.pendingForce,
|
|
496
|
+
streamText: active.streamText
|
|
497
|
+
} : {
|
|
498
|
+
outcome: "completed",
|
|
499
|
+
status: frame.status === "ok" ? "ok" : "error",
|
|
500
|
+
streamText: active.streamText,
|
|
501
|
+
...active.error ? { cellError: active.error } : {}
|
|
1359
502
|
});
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
503
|
+
}
|
|
504
|
+
executeCell(code, opts) {
|
|
505
|
+
const task = this.queue.then(() => {
|
|
506
|
+
if (!this.isRunning) return {
|
|
1363
507
|
outcome: "forced",
|
|
1364
508
|
failure: {
|
|
1365
509
|
kind: "worker-exit",
|
|
1366
|
-
message: "
|
|
510
|
+
message: "Python is not running"
|
|
1367
511
|
},
|
|
1368
512
|
streamText: ""
|
|
1369
513
|
};
|
|
1370
|
-
return
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
|
|
514
|
+
return this.perform(code, opts);
|
|
515
|
+
});
|
|
516
|
+
this.queue = task.catch(() => void 0);
|
|
517
|
+
return task;
|
|
1374
518
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
if (!transport) return Promise.resolve({
|
|
519
|
+
perform(code, opts) {
|
|
520
|
+
if (opts.signal?.aborted) return Promise.resolve({
|
|
1378
521
|
outcome: "forced",
|
|
1379
522
|
failure: {
|
|
1380
|
-
kind: "
|
|
1381
|
-
message:
|
|
523
|
+
kind: "abort",
|
|
524
|
+
message: String(opts.signal.reason)
|
|
1382
525
|
},
|
|
1383
526
|
streamText: ""
|
|
1384
527
|
});
|
|
1385
|
-
const msg = buildMessage("execute_request", {
|
|
1386
|
-
code,
|
|
1387
|
-
silent: false,
|
|
1388
|
-
store_history: false,
|
|
1389
|
-
user_expressions: {},
|
|
1390
|
-
allow_stdin: false,
|
|
1391
|
-
stop_on_error: true
|
|
1392
|
-
}, this.session, this.options.username);
|
|
1393
528
|
return new Promise((resolve$1) => {
|
|
1394
|
-
const
|
|
1395
|
-
|
|
529
|
+
const active = {
|
|
530
|
+
id: randomUUID(),
|
|
1396
531
|
opts,
|
|
1397
532
|
streamText: "",
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
533
|
+
started: false,
|
|
534
|
+
resolve: resolve$1,
|
|
535
|
+
rejectCallbacks: /* @__PURE__ */ new Set()
|
|
1401
536
|
};
|
|
1402
|
-
this.
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
if (this.activeExec === execution) this.activeExec = void 0;
|
|
1411
|
-
resolve$1(outcome);
|
|
1412
|
-
};
|
|
1413
|
-
const interruptAndForce = (failure) => {
|
|
1414
|
-
if (this.activeExec !== execution) return;
|
|
1415
|
-
if (execution.pendingForce === void 0) execution.pendingForce = failure;
|
|
1416
|
-
this.sendInterrupt(execution);
|
|
1417
|
-
execution.forceTimer = setTimeout(() => {
|
|
1418
|
-
settle({
|
|
1419
|
-
outcome: "forced",
|
|
1420
|
-
failure,
|
|
1421
|
-
streamText: execution.streamText
|
|
1422
|
-
});
|
|
1423
|
-
}, opts.interruptGraceMs);
|
|
1424
|
-
execution.forceTimer.unref?.();
|
|
1425
|
-
};
|
|
1426
|
-
const onAbort = () => {
|
|
1427
|
-
interruptAndForce({
|
|
537
|
+
this.active = active;
|
|
538
|
+
active.timeout = setTimeout(() => this.interrupt(active, {
|
|
539
|
+
kind: "timeout",
|
|
540
|
+
message: `Python cell exceeded ${opts.timeoutMs}ms`
|
|
541
|
+
}), opts.timeoutMs);
|
|
542
|
+
if (opts.signal) {
|
|
543
|
+
const signal = opts.signal;
|
|
544
|
+
const abort = () => this.interrupt(active, {
|
|
1428
545
|
kind: "abort",
|
|
1429
|
-
message: String(
|
|
1430
|
-
});
|
|
1431
|
-
};
|
|
1432
|
-
execution.timeoutTimer = setTimeout(() => {
|
|
1433
|
-
interruptAndForce({
|
|
1434
|
-
kind: "timeout",
|
|
1435
|
-
message: `cell exceeded ${opts.timeoutMs}ms wall budget`
|
|
1436
|
-
});
|
|
1437
|
-
}, opts.timeoutMs);
|
|
1438
|
-
execution.timeoutTimer.unref?.();
|
|
1439
|
-
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1440
|
-
execution.detachSignal = () => opts.signal?.removeEventListener("abort", onAbort);
|
|
1441
|
-
if (opts.signal?.aborted) onAbort();
|
|
1442
|
-
try {
|
|
1443
|
-
transport.send("shell", msg);
|
|
1444
|
-
} catch {
|
|
1445
|
-
settle({
|
|
1446
|
-
outcome: "forced",
|
|
1447
|
-
failure: {
|
|
1448
|
-
kind: "worker-exit",
|
|
1449
|
-
message: "shell channel send failed"
|
|
1450
|
-
},
|
|
1451
|
-
streamText: execution.streamText
|
|
546
|
+
message: String(signal.reason)
|
|
1452
547
|
});
|
|
548
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
549
|
+
active.detach = () => signal.removeEventListener("abort", abort);
|
|
1453
550
|
}
|
|
551
|
+
this.child?.stdin?.write(JSON.stringify({
|
|
552
|
+
id: active.id,
|
|
553
|
+
code,
|
|
554
|
+
...this.options.cwd ? { cwd: this.options.cwd } : {}
|
|
555
|
+
}) + "\n");
|
|
1454
556
|
});
|
|
1455
557
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
this.
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
outcome: "forced",
|
|
1481
|
-
failure,
|
|
1482
|
-
streamText: execution.streamText
|
|
1483
|
-
});
|
|
1484
|
-
}
|
|
1485
|
-
handleExecutionMessage(incoming) {
|
|
1486
|
-
const execution = this.activeExec;
|
|
1487
|
-
if (!execution || execution.settled) return;
|
|
1488
|
-
if (incoming.parent_header.msg_id !== execution.requestMsgId) return;
|
|
1489
|
-
const msgType = incoming.header.msg_type;
|
|
1490
|
-
if (msgType === "stream") {
|
|
1491
|
-
const content = incoming.content;
|
|
1492
|
-
if (typeof content.text === "string") execution.streamText += content.text;
|
|
1493
|
-
} else if (msgType === "error") {
|
|
1494
|
-
const content = incoming.content;
|
|
1495
|
-
execution.cellError = {
|
|
1496
|
-
ename: typeof content.ename === "string" ? content.ename : "Error",
|
|
1497
|
-
evalue: typeof content.evalue === "string" ? content.evalue : "",
|
|
1498
|
-
traceback: Array.isArray(content.traceback) ? content.traceback : []
|
|
1499
|
-
};
|
|
1500
|
-
execution.status = "error";
|
|
1501
|
-
} else if (msgType === "status") {
|
|
1502
|
-
if (incoming.content.execution_state === "idle") this.settleCompleted(execution);
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
settleCompleted(execution) {
|
|
1506
|
-
if (execution.settled) return;
|
|
1507
|
-
execution.settled = true;
|
|
1508
|
-
if (execution.forceTimer) clearTimeout(execution.forceTimer);
|
|
1509
|
-
if (execution.timeoutTimer) clearTimeout(execution.timeoutTimer);
|
|
1510
|
-
if (execution.alarmTimer) clearTimeout(execution.alarmTimer);
|
|
1511
|
-
execution.detachSignal?.();
|
|
1512
|
-
if (this.activeExec === execution) this.activeExec = void 0;
|
|
1513
|
-
if (execution.pendingForce !== void 0) {
|
|
1514
|
-
execution.resolve({
|
|
1515
|
-
outcome: "forced",
|
|
1516
|
-
failure: execution.pendingForce,
|
|
1517
|
-
streamText: execution.streamText
|
|
1518
|
-
});
|
|
1519
|
-
return;
|
|
1520
|
-
}
|
|
1521
|
-
execution.resolve({
|
|
1522
|
-
outcome: "completed",
|
|
1523
|
-
status: execution.status,
|
|
1524
|
-
streamText: execution.streamText,
|
|
1525
|
-
...execution.cellError ? { cellError: execution.cellError } : {}
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
|
-
handleCommMessage(incoming) {
|
|
1529
|
-
const content = incoming.content;
|
|
1530
|
-
const commId = content.comm_id;
|
|
1531
|
-
if (typeof commId !== "string") return;
|
|
1532
|
-
const msgType = incoming.header.msg_type;
|
|
1533
|
-
if (msgType === "comm_close") {
|
|
1534
|
-
this.commTargets.delete(commId);
|
|
1535
|
-
this.servedComms.delete(commId);
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
if (msgType === "comm_open") {
|
|
1539
|
-
const targetName = content.target_name;
|
|
1540
|
-
if (typeof targetName === "string") this.commTargets.set(commId, targetName);
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1543
|
-
if (msgType === "comm_msg" && this.commTargets.get(commId) === HOST_COMM_TARGET) this.serveHostRequest(commId, content.data);
|
|
1544
|
-
}
|
|
1545
|
-
serveHostRequest(commId, data) {
|
|
1546
|
-
if (this.servedComms.has(commId)) return;
|
|
1547
|
-
this.servedComms.add(commId);
|
|
1548
|
-
const handler = this.activeExec?.opts.hostRequestHandler;
|
|
1549
|
-
if (!handler) {
|
|
1550
|
-
this.replyComm(commId, {
|
|
1551
|
-
status: "error",
|
|
1552
|
-
error: "no host request handler is active"
|
|
1553
|
-
}).catch(() => void 0);
|
|
1554
|
-
return;
|
|
1555
|
-
}
|
|
1556
|
-
if (!isRecord(data)) {
|
|
1557
|
-
this.replyComm(commId, {
|
|
1558
|
-
status: "error",
|
|
1559
|
-
error: "host request payload must be an object"
|
|
1560
|
-
}).catch(() => void 0);
|
|
1561
|
-
return;
|
|
1562
|
-
}
|
|
1563
|
-
(async () => {
|
|
1564
|
-
try {
|
|
1565
|
-
const outcome = await handler(data);
|
|
1566
|
-
if (outcome.ok) await this.replyComm(commId, {
|
|
1567
|
-
status: "ok",
|
|
1568
|
-
result: outcome.result
|
|
1569
|
-
});
|
|
1570
|
-
else await this.replyComm(commId, {
|
|
1571
|
-
status: "error",
|
|
1572
|
-
error: outcome.message
|
|
1573
|
-
});
|
|
1574
|
-
} catch (error) {
|
|
1575
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1576
|
-
await this.replyComm(commId, {
|
|
1577
|
-
status: "error",
|
|
1578
|
-
error: message
|
|
1579
|
-
}).catch(() => void 0);
|
|
1580
|
-
}
|
|
1581
|
-
})().catch(() => void 0);
|
|
1582
|
-
}
|
|
1583
|
-
async replyComm(commId, data) {
|
|
1584
|
-
const transport = this.transport;
|
|
1585
|
-
if (!transport) throw new Error("kernel channels are not connected");
|
|
1586
|
-
transport.send("control", buildMessage("comm_msg", {
|
|
1587
|
-
comm_id: commId,
|
|
1588
|
-
data
|
|
1589
|
-
}, this.session, this.options.username));
|
|
1590
|
-
}
|
|
1591
|
-
/**
|
|
1592
|
-
* Phase one NOW, phase two on a timer: send the control-channel interrupt,
|
|
1593
|
-
* then — after the confirm window, and only if the execution has still not
|
|
1594
|
-
* settled — escalate to SIGALRM. The zmq interrupt only lands when the cell
|
|
1595
|
-
* yields to the kernel event loop; a busy sync cell ('while True: pass')
|
|
1596
|
-
* never does, which is why the escalation exists at all. Sending both
|
|
1597
|
-
* channels in the same tick (the M1 design) made SIGALRM the FIRST signal
|
|
1598
|
-
* an idle-or-booting kernel saw on timeout/abort: its handler raised
|
|
1599
|
-
* KeyboardInterrupt at the top level and the process exited cleanly — a
|
|
1600
|
-
* deterministic kill (10/10 same-tick aborts, 8/10 at +1-2ms, 40/40 during
|
|
1601
|
-
* cold boot; dev/m2a-report.md §5.1). Two-phase escalation plus the
|
|
1602
|
-
* kernel-side busy guard closes that window while keeping the hard-abort
|
|
1603
|
-
* contract: a busy loop still breaks, one confirm window later.
|
|
1604
|
-
*/
|
|
1605
|
-
async sendInterrupt(execution) {
|
|
1606
|
-
const transport = this.transport;
|
|
1607
|
-
if (!transport) return;
|
|
558
|
+
interrupt(active, failure) {
|
|
559
|
+
if (this.active !== active || active.pendingForce) return;
|
|
560
|
+
active.pendingForce = failure;
|
|
561
|
+
for (const revoke of [...active.rejectCallbacks]) revoke();
|
|
562
|
+
if (active.started) this.signal("SIGINT");
|
|
563
|
+
active.confirm = setTimeout(() => {
|
|
564
|
+
if (this.active === active && active.started && active.pendingForce) this.signal("SIGINT");
|
|
565
|
+
}, active.opts.interruptConfirmMs);
|
|
566
|
+
active.force = setTimeout(() => {
|
|
567
|
+
this.dispose();
|
|
568
|
+
}, active.opts.interruptGraceMs);
|
|
569
|
+
}
|
|
570
|
+
settle(outcome) {
|
|
571
|
+
const active = this.active;
|
|
572
|
+
if (!active) return;
|
|
573
|
+
this.active = void 0;
|
|
574
|
+
clearTimeout(active.timeout);
|
|
575
|
+
clearTimeout(active.force);
|
|
576
|
+
clearTimeout(active.confirm);
|
|
577
|
+
active.detach?.();
|
|
578
|
+
for (const revoke of [...active.rejectCallbacks]) revoke();
|
|
579
|
+
active.resolve(outcome);
|
|
580
|
+
}
|
|
581
|
+
signal(signal) {
|
|
1608
582
|
try {
|
|
1609
|
-
|
|
583
|
+
this.child?.kill(signal);
|
|
1610
584
|
} catch {}
|
|
1611
|
-
execution.alarmTimer = setTimeout(() => {
|
|
1612
|
-
if (execution.settled) return;
|
|
1613
|
-
if (this.kernel?.exitCode === null) try {
|
|
1614
|
-
this.kernel.kill("SIGALRM");
|
|
1615
|
-
} catch {}
|
|
1616
|
-
}, execution.opts.interruptConfirmMs);
|
|
1617
|
-
execution.alarmTimer.unref?.();
|
|
1618
585
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
* envelope (skipped-for-size vs written); a failed/skipped dump NEVER
|
|
1623
|
-
* replaces a previous good snapshot on disk.
|
|
1624
|
-
* @param payloadPath - dill payload destination.
|
|
1625
|
-
* @param manifestPath - JSON manifest destination (turn, names, python
|
|
1626
|
-
* version, venv path, skills, size).
|
|
1627
|
-
* @param spec - turn counter, skills list, and the size cap.
|
|
1628
|
-
*/
|
|
1629
|
-
async snapshotState(payloadPath, manifestPath, spec) {
|
|
1630
|
-
if (!this.isRunning) return {
|
|
1631
|
-
kind: "failed",
|
|
1632
|
-
reason: "kernel is not running"
|
|
1633
|
-
};
|
|
1634
|
-
const sentinel = `__dashr_snapshot_${randomBytes(9).toString("hex")}__`;
|
|
1635
|
-
const outcome = await this.executeCell(buildSnapshotCell(payloadPath, manifestPath, spec, sentinel), {
|
|
1636
|
-
timeoutMs: this.options.snapshotTimeoutMs,
|
|
1637
|
-
interruptGraceMs: 1e3,
|
|
1638
|
-
interruptConfirmMs: Math.min(this.options.interruptConfirmMs, 500)
|
|
1639
|
-
});
|
|
1640
|
-
if (outcome.outcome !== "completed" || outcome.status !== "ok") return {
|
|
1641
|
-
kind: "failed",
|
|
1642
|
-
reason: "snapshot cell failed"
|
|
1643
|
-
};
|
|
1644
|
-
return this.parseSnapshotEnvelope(outcome.streamText, sentinel);
|
|
1645
|
-
}
|
|
1646
|
-
/** Parse one snapshot cell's nonce envelope into its outcome. */
|
|
1647
|
-
parseSnapshotEnvelope(streamText, sentinel) {
|
|
1648
|
-
const capture = extractStream(streamText, sentinel);
|
|
1649
|
-
if (capture.envelope === void 0) return {
|
|
1650
|
-
kind: "failed",
|
|
1651
|
-
reason: "snapshot cell produced no envelope"
|
|
1652
|
-
};
|
|
1653
|
-
let envelope;
|
|
586
|
+
killGroup() {
|
|
587
|
+
const pid = this.child?.pid;
|
|
588
|
+
if (pid === void 0) return;
|
|
1654
589
|
try {
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
kind: "failed",
|
|
1659
|
-
reason: "snapshot envelope was not valid JSON"
|
|
1660
|
-
};
|
|
1661
|
-
}
|
|
1662
|
-
if (typeof envelope !== "object" || envelope === null) return {
|
|
1663
|
-
kind: "failed",
|
|
1664
|
-
reason: "snapshot envelope was not an object"
|
|
1665
|
-
};
|
|
1666
|
-
const record = envelope;
|
|
1667
|
-
if (record.ok === true) return {
|
|
1668
|
-
kind: "ok",
|
|
1669
|
-
sizeBytes: typeof record.sizeBytes === "number" ? record.sizeBytes : 0,
|
|
1670
|
-
names: typeof record.names === "number" ? record.names : 0
|
|
1671
|
-
};
|
|
1672
|
-
if (record.skipped === true) return {
|
|
1673
|
-
kind: "skipped",
|
|
1674
|
-
reason: typeof record.reason === "string" ? record.reason : "size cap",
|
|
1675
|
-
...typeof record.estimateBytes === "number" ? { estimateBytes: record.estimateBytes } : {},
|
|
1676
|
-
...typeof record.sizeBytes === "number" ? { sizeBytes: record.sizeBytes } : {},
|
|
1677
|
-
capBytes: typeof record.capBytes === "number" ? record.capBytes : 0
|
|
1678
|
-
};
|
|
1679
|
-
return {
|
|
1680
|
-
kind: "failed",
|
|
1681
|
-
reason: typeof record.reason === "string" ? record.reason : "snapshot failed"
|
|
1682
|
-
};
|
|
590
|
+
if (process.platform !== "win32") process.kill(-pid, "SIGKILL");
|
|
591
|
+
else this.child?.kill("SIGKILL");
|
|
592
|
+
} catch {}
|
|
1683
593
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
* @param payloadPath - dill payload source.
|
|
1690
|
-
* @param manifestPath - JSON manifest source.
|
|
1691
|
-
*/
|
|
1692
|
-
async restoreState(payloadPath, manifestPath) {
|
|
1693
|
-
if (!this.isRunning) return {
|
|
1694
|
-
kind: "degraded",
|
|
1695
|
-
reason: "kernel is not running"
|
|
594
|
+
died(message) {
|
|
595
|
+
this.state = "shutdown";
|
|
596
|
+
this.deathFailure ??= {
|
|
597
|
+
kind: "worker-exit",
|
|
598
|
+
message
|
|
1696
599
|
};
|
|
1697
|
-
|
|
1698
|
-
|
|
600
|
+
this.killGroup();
|
|
601
|
+
this.dispose();
|
|
602
|
+
}
|
|
603
|
+
async internal(code, sentinel) {
|
|
604
|
+
const result = await this.executeCell(code, {
|
|
1699
605
|
timeoutMs: this.options.snapshotTimeoutMs,
|
|
1700
606
|
interruptGraceMs: 1e3,
|
|
1701
|
-
interruptConfirmMs:
|
|
607
|
+
interruptConfirmMs: this.options.interruptConfirmMs
|
|
1702
608
|
});
|
|
1703
|
-
if (
|
|
1704
|
-
kind: "degraded",
|
|
1705
|
-
reason: "restore cell failed"
|
|
1706
|
-
};
|
|
1707
|
-
const capture = extractStream(outcome.streamText, sentinel);
|
|
1708
|
-
if (capture.envelope === void 0) return {
|
|
1709
|
-
kind: "degraded",
|
|
1710
|
-
reason: "restore cell produced no envelope"
|
|
1711
|
-
};
|
|
1712
|
-
let envelope;
|
|
609
|
+
if (result.outcome !== "completed" || result.status !== "ok") return void 0;
|
|
1713
610
|
try {
|
|
1714
|
-
envelope = JSON.parse(
|
|
611
|
+
const envelope = JSON.parse(extractStream(result.streamText, sentinel).envelope ?? "");
|
|
612
|
+
return record(envelope) ? envelope : void 0;
|
|
1715
613
|
} catch {
|
|
1716
|
-
return
|
|
1717
|
-
kind: "degraded",
|
|
1718
|
-
reason: "restore envelope was not valid JSON"
|
|
1719
|
-
};
|
|
614
|
+
return;
|
|
1720
615
|
}
|
|
1721
|
-
if (typeof envelope !== "object" || envelope === null) return {
|
|
1722
|
-
kind: "degraded",
|
|
1723
|
-
reason: "restore envelope was not an object"
|
|
1724
|
-
};
|
|
1725
|
-
const record = envelope;
|
|
1726
|
-
if (record.ok === true) return {
|
|
1727
|
-
kind: "restored",
|
|
1728
|
-
count: typeof record.restored === "number" ? record.restored : 0
|
|
1729
|
-
};
|
|
1730
|
-
return {
|
|
1731
|
-
kind: "degraded",
|
|
1732
|
-
reason: typeof record.reason === "string" ? record.reason : "snapshot not replayable"
|
|
1733
|
-
};
|
|
1734
616
|
}
|
|
1735
|
-
/**
|
|
1736
|
-
* Read one user-namespace variable by name (or list the namespace's user
|
|
1737
|
-
* names when `name` is `null`) via an internal cell. JSON-serializable
|
|
1738
|
-
* values cross as `kind: 'json'` text; anything else falls back to `repr`
|
|
1739
|
-
* text (`kind: 'repr'`) so a non-JSON variable is still readable. A missing
|
|
1740
|
-
* name reports `kind: 'missing'` rather than failing the cell.
|
|
1741
|
-
* @param name - the exact variable name, or `null` to list namespace names.
|
|
1742
|
-
*/
|
|
1743
617
|
async queryVar(name$1) {
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
};
|
|
1748
|
-
const sentinel = `__dashr_query_${randomBytes(9).toString("hex")}__`;
|
|
1749
|
-
const outcome = await this.executeCell(buildQueryVarCell(name$1, sentinel), {
|
|
1750
|
-
timeoutMs: this.options.snapshotTimeoutMs,
|
|
1751
|
-
interruptGraceMs: 1e3,
|
|
1752
|
-
interruptConfirmMs: Math.min(this.options.interruptConfirmMs, 500)
|
|
1753
|
-
});
|
|
1754
|
-
if (outcome.outcome !== "completed" || outcome.status !== "ok") return {
|
|
1755
|
-
kind: "failed",
|
|
1756
|
-
reason: "query cell failed"
|
|
1757
|
-
};
|
|
1758
|
-
const capture = extractStream(outcome.streamText, sentinel);
|
|
1759
|
-
if (capture.envelope === void 0) return {
|
|
1760
|
-
kind: "failed",
|
|
1761
|
-
reason: "query cell produced no envelope"
|
|
1762
|
-
};
|
|
1763
|
-
let envelope;
|
|
1764
|
-
try {
|
|
1765
|
-
envelope = JSON.parse(capture.envelope);
|
|
1766
|
-
} catch {
|
|
1767
|
-
return {
|
|
1768
|
-
kind: "failed",
|
|
1769
|
-
reason: "query envelope was not valid JSON"
|
|
1770
|
-
};
|
|
1771
|
-
}
|
|
1772
|
-
if (!isRecord(envelope)) return {
|
|
1773
|
-
kind: "failed",
|
|
1774
|
-
reason: "query envelope was not an object"
|
|
1775
|
-
};
|
|
1776
|
-
if (envelope.ok !== true) return {
|
|
1777
|
-
kind: "failed",
|
|
1778
|
-
reason: typeof envelope.reason === "string" ? envelope.reason : "query failed"
|
|
1779
|
-
};
|
|
1780
|
-
const kind = envelope.kind;
|
|
1781
|
-
if (kind === "names") return {
|
|
618
|
+
const sentinel = `__better_query_${randomBytes(12).toString("hex")}__`;
|
|
619
|
+
const reply = await this.internal(buildQueryVarCell(name$1, sentinel), sentinel);
|
|
620
|
+
if (reply?.kind === "names" && Array.isArray(reply.names)) return {
|
|
1782
621
|
kind: "names",
|
|
1783
|
-
names:
|
|
622
|
+
names: reply.names.filter((name$2) => typeof name$2 === "string")
|
|
1784
623
|
};
|
|
1785
|
-
if (kind === "missing") return { kind: "missing" };
|
|
1786
|
-
if (kind === "json" || kind === "repr")
|
|
1787
|
-
kind,
|
|
1788
|
-
text:
|
|
1789
|
-
} : {
|
|
1790
|
-
kind: "failed",
|
|
1791
|
-
reason: "query envelope carried no text"
|
|
624
|
+
if (reply?.kind === "missing") return { kind: "missing" };
|
|
625
|
+
if ((reply?.kind === "json" || reply?.kind === "repr") && typeof reply.text === "string") return {
|
|
626
|
+
kind: reply.kind,
|
|
627
|
+
text: reply.text
|
|
1792
628
|
};
|
|
1793
629
|
return {
|
|
1794
630
|
kind: "failed",
|
|
1795
|
-
reason:
|
|
631
|
+
reason: "Python variable query failed"
|
|
1796
632
|
};
|
|
1797
633
|
}
|
|
1798
|
-
/**
|
|
1799
|
-
* Assign one pre-serialized JSON value into the user namespace under
|
|
1800
|
-
* `name` via an internal cell. The host already validated the name and the
|
|
1801
|
-
* value; the cell only decodes and binds. Uses the same busy guard and
|
|
1802
|
-
* internal-cell budget as the snapshot cells.
|
|
1803
|
-
* @param name - the validated identifier to assign under.
|
|
1804
|
-
* @param valueJson - the lossless-JSON text to decode and bind.
|
|
1805
|
-
*/
|
|
1806
634
|
async setVar(name$1, valueJson) {
|
|
1807
|
-
|
|
635
|
+
const sentinel = `__better_set_${randomBytes(12).toString("hex")}__`;
|
|
636
|
+
return (await this.internal(buildSetVarCell(name$1, valueJson, sentinel), sentinel))?.ok === true ? { kind: "ok" } : {
|
|
1808
637
|
kind: "failed",
|
|
1809
|
-
reason: "
|
|
638
|
+
reason: "Python variable assignment failed"
|
|
1810
639
|
};
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
640
|
+
}
|
|
641
|
+
async snapshotState(payloadPath, manifestPath, spec) {
|
|
642
|
+
const sentinel = `__better_save_${randomBytes(12).toString("hex")}__`;
|
|
643
|
+
const reply = await this.internal(buildSnapshotCell(payloadPath, manifestPath, spec, sentinel), sentinel);
|
|
644
|
+
if (reply?.kind === "ok" && Array.isArray(reply.names) && typeof reply.sizeBytes === "number" && typeof reply.turn === "number" && record(reply.omitted)) return {
|
|
645
|
+
kind: "ok",
|
|
646
|
+
sizeBytes: reply.sizeBytes,
|
|
647
|
+
names: reply.names,
|
|
648
|
+
omitted: reply.omitted,
|
|
649
|
+
turn: reply.turn,
|
|
650
|
+
...typeof reply.warning === "string" ? { warning: reply.warning } : {}
|
|
1820
651
|
};
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
652
|
+
if (reply?.kind === "skipped") return {
|
|
653
|
+
kind: "skipped",
|
|
654
|
+
reason: String(reply.reason),
|
|
655
|
+
capBytes: spec.sizeCapBytes
|
|
1825
656
|
};
|
|
1826
|
-
|
|
1827
|
-
try {
|
|
1828
|
-
envelope = JSON.parse(capture.envelope);
|
|
1829
|
-
} catch {
|
|
1830
|
-
return {
|
|
1831
|
-
kind: "failed",
|
|
1832
|
-
reason: "set envelope was not valid JSON"
|
|
1833
|
-
};
|
|
1834
|
-
}
|
|
1835
|
-
if (!isRecord(envelope) || envelope.ok !== true) return {
|
|
657
|
+
return {
|
|
1836
658
|
kind: "failed",
|
|
1837
|
-
reason:
|
|
659
|
+
reason: typeof reply?.reason === "string" ? reply.reason : "checkpoint helper timed out, failed, or exited; commit status could not be confirmed"
|
|
1838
660
|
};
|
|
1839
|
-
return { kind: "ok" };
|
|
1840
661
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
this.
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
662
|
+
async restoreState(payloadPath, manifestPath, bindings = []) {
|
|
663
|
+
const sentinel = `__better_restore_${randomBytes(12).toString("hex")}__`;
|
|
664
|
+
const reply = await this.internal(buildRestoreCell(payloadPath, manifestPath, sentinel, bindings), sentinel);
|
|
665
|
+
if (reply?.kind === "restored" && typeof reply.count === "number" && typeof reply.turn === "number" && record(reply.omitted)) return {
|
|
666
|
+
kind: "restored",
|
|
667
|
+
count: reply.count,
|
|
668
|
+
turn: reply.turn,
|
|
669
|
+
omitted: reply.omitted
|
|
670
|
+
};
|
|
671
|
+
return {
|
|
672
|
+
kind: "degraded",
|
|
673
|
+
reason: typeof reply?.reason === "string" ? reply.reason : "checkpoint restore helper failed or timed out"
|
|
674
|
+
};
|
|
1848
675
|
}
|
|
1849
|
-
|
|
1850
|
-
this.
|
|
1851
|
-
this.
|
|
1852
|
-
this.connection = void 0;
|
|
1853
|
-
try {
|
|
1854
|
-
this.kernel?.kill("SIGKILL");
|
|
1855
|
-
} catch {}
|
|
1856
|
-
this.kernel = void 0;
|
|
1857
|
-
if (this.tempDir) try {
|
|
1858
|
-
rmSync(this.tempDir, {
|
|
1859
|
-
recursive: true,
|
|
1860
|
-
force: true
|
|
1861
|
-
});
|
|
1862
|
-
} catch {}
|
|
1863
|
-
this.tempDir = void 0;
|
|
676
|
+
dispose() {
|
|
677
|
+
this.disposePromise ??= this.doDispose();
|
|
678
|
+
return this.disposePromise;
|
|
1864
679
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
680
|
+
async doDispose() {
|
|
681
|
+
this.state = "shutdown";
|
|
682
|
+
const child = this.child;
|
|
683
|
+
if (!child) {
|
|
684
|
+
this.callbackServer?.closeAllConnections();
|
|
685
|
+
this.callbackServer?.close();
|
|
1869
686
|
return;
|
|
1870
687
|
}
|
|
1871
|
-
this.
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
688
|
+
if (!this.active && child.exitCode === null && child.signalCode === null) {
|
|
689
|
+
child.stdin?.end(JSON.stringify({ type: "exit" }) + "\n");
|
|
690
|
+
await this.waitClose(Math.min(200, this.options.disposeTimeoutMs));
|
|
691
|
+
}
|
|
692
|
+
this.killGroup();
|
|
693
|
+
await this.waitClose(this.options.disposeTimeoutMs);
|
|
694
|
+
if (this.active) this.settle({
|
|
695
|
+
outcome: "forced",
|
|
696
|
+
failure: this.active.pendingForce ?? this.deathFailure ?? {
|
|
697
|
+
kind: "worker-exit",
|
|
698
|
+
message: "Python disposed during cell execution"
|
|
699
|
+
},
|
|
700
|
+
streamText: this.active.streamText
|
|
701
|
+
});
|
|
702
|
+
for (const reader of this.ownedReaders) reader.close();
|
|
703
|
+
for (const stream of child.stdio) stream?.destroy();
|
|
704
|
+
this.callbackServer?.closeAllConnections();
|
|
705
|
+
this.callbackServer?.close();
|
|
706
|
+
this.child = void 0;
|
|
707
|
+
}
|
|
708
|
+
async waitClose(ms) {
|
|
709
|
+
if (!this.closePromise) return;
|
|
710
|
+
await new Promise((resolve$1) => {
|
|
711
|
+
const timer = setTimeout(resolve$1, ms);
|
|
712
|
+
this.closePromise.then(() => {
|
|
1890
713
|
clearTimeout(timer);
|
|
1891
714
|
resolve$1();
|
|
1892
|
-
};
|
|
1893
|
-
kernel.once("exit", onExit);
|
|
715
|
+
});
|
|
1894
716
|
});
|
|
1895
717
|
}
|
|
1896
718
|
};
|
|
@@ -1898,15 +720,8 @@ var IpyKernelBridge = class {
|
|
|
1898
720
|
//#endregion
|
|
1899
721
|
//#region src/runtime.ts
|
|
1900
722
|
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1901
|
-
/**
|
|
1902
|
-
|
|
1903
|
-
* bootstrap helpers plus the per-run `__dashr_completion__` and
|
|
1904
|
-
* `__dashr_injected__`), refused as binding globals on top of the seam's
|
|
1905
|
-
* shared set.
|
|
1906
|
-
*/
|
|
1907
|
-
const KERNEL_OWNED_NAME = /^_+dashr/i;
|
|
1908
|
-
/** Manifest format version; a snapshot written under a different version is not replayable. */
|
|
1909
|
-
const SNAPSHOT_FORMAT = 1;
|
|
723
|
+
/** Runner internals and legacy reserved shim prefix are never injectable variables. */
|
|
724
|
+
const KERNEL_OWNED_NAME = /^(?:_+dashr|__omp_)/i;
|
|
1910
725
|
function messageOf(error) {
|
|
1911
726
|
return error instanceof Error ? error.message : String(error);
|
|
1912
727
|
}
|
|
@@ -1953,11 +768,16 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
1953
768
|
/** One entry per session principal that has run code (lazy — never pre-seeded). */
|
|
1954
769
|
kernels = /* @__PURE__ */ new Map();
|
|
1955
770
|
disposed = false;
|
|
771
|
+
transactions = /* @__PURE__ */ new Map();
|
|
1956
772
|
/** Lazily-resolved (and, when managed, provisioned) kernel interpreter. */
|
|
1957
773
|
kernelEnvPromise;
|
|
1958
774
|
constructor(ctx, config) {
|
|
1959
775
|
super(ctx);
|
|
1960
|
-
|
|
776
|
+
const profile = ctx.get("profileContext");
|
|
777
|
+
this.config = {
|
|
778
|
+
...config,
|
|
779
|
+
snapshotDir: config.snapshotDir ?? (profile ? join(profile.home, "better-dsh", "python-repl", createHash("sha256").update(profile.dir).digest("hex")) : void 0)
|
|
780
|
+
};
|
|
1961
781
|
for (const key of [
|
|
1962
782
|
"startupTimeoutMs",
|
|
1963
783
|
"runTimeoutMs",
|
|
@@ -1997,9 +817,21 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
1997
817
|
* @param request - the program, its bindings, and the abort signal.
|
|
1998
818
|
* @returns the run's outcome per the seam contract.
|
|
1999
819
|
*/
|
|
2000
|
-
|
|
820
|
+
transaction(key, operation) {
|
|
821
|
+
const result = (this.transactions.get(key) ?? Promise.resolve()).catch(() => void 0).then(operation);
|
|
822
|
+
this.transactions.set(key, result);
|
|
823
|
+
result.finally(() => {
|
|
824
|
+
if (this.transactions.get(key) === result) this.transactions.delete(key);
|
|
825
|
+
}).catch(() => void 0);
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
run(request) {
|
|
829
|
+
return this.transaction(request.principal || AGENTLESS_KEY, () => this.runTransaction(request));
|
|
830
|
+
}
|
|
831
|
+
async runTransaction(request) {
|
|
2001
832
|
if (this.disposed) throw new Error("dashr-repl: run() after disposal");
|
|
2002
833
|
const bindings = this.validateBindings(request);
|
|
834
|
+
const bindingNames = request.bindings.flatMap((binding) => [binding.global, ...binding.errorClass ? [binding.errorClass.name] : []]);
|
|
2003
835
|
if (request.signal?.aborted) return {
|
|
2004
836
|
logs: [],
|
|
2005
837
|
error: {
|
|
@@ -2012,7 +844,7 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
2012
844
|
const dead = this.kernels.get(key);
|
|
2013
845
|
if (dead?.bridge.isDead) {
|
|
2014
846
|
this.kernels.delete(key);
|
|
2015
|
-
if (dead.ranCells) return await this.reviveAfterDeath(key, dead);
|
|
847
|
+
if (dead.ranCells) return await this.reviveAfterDeath(key, dead, false, bindingNames);
|
|
2016
848
|
}
|
|
2017
849
|
let entry;
|
|
2018
850
|
try {
|
|
@@ -2025,8 +857,8 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
2025
857
|
}
|
|
2026
858
|
if (!entry.restoreAttempted) {
|
|
2027
859
|
entry.restoreAttempted = true;
|
|
2028
|
-
const restore = await this.restoreEntry(entry);
|
|
2029
|
-
if (restore.kind === "restored") entry.pendingNotice = `namespace restored from the turn-${restore.turn} snapshot`;
|
|
860
|
+
const restore = await this.restoreEntry(entry, bindingNames);
|
|
861
|
+
if (restore.kind === "restored") entry.pendingNotice = `namespace restored from the turn-${restore.turn} snapshot${this.omissionNotice(restore.omitted)}`;
|
|
2030
862
|
else if (restore.kind === "degraded") entry.pendingNotice = `snapshot not replayable (${restore.reason}); started from an EMPTY namespace — re-create any state you need`;
|
|
2031
863
|
}
|
|
2032
864
|
const kernel = entry.bridge;
|
|
@@ -2049,17 +881,13 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
2049
881
|
entry.turn += 1;
|
|
2050
882
|
} else if (outcome.outcome === "forced" && outcome.failure.kind === "worker-exit" && kernel.isDead && !this.disposed) {
|
|
2051
883
|
if (this.kernels.get(key) === entry) this.kernels.delete(key);
|
|
2052
|
-
|
|
884
|
+
const revived = await this.reviveAfterDeath(key, entry, true, bindingNames);
|
|
885
|
+
return finalizeOutput(extractStream(outcome.streamText, sentinel).logs, revived.value, revived.error, this.config.maxOutputBytes);
|
|
2053
886
|
}
|
|
2054
887
|
let result = this.assembleResult(outcome, sentinel);
|
|
2055
888
|
if (outcome.outcome === "completed") {
|
|
2056
889
|
const snapshot = await this.snapshotTurnEnd(entry);
|
|
2057
|
-
const notices = [];
|
|
2058
|
-
if (snapshot.skipped && !entry.warnedSnapshotSkip) {
|
|
2059
|
-
entry.warnedSnapshotSkip = true;
|
|
2060
|
-
const detail = snapshot.estimateBytes !== void 0 ? `estimated ${snapshot.estimateBytes} bytes` : `${snapshot.sizeBytes ?? "unknown"} bytes serialized`;
|
|
2061
|
-
notices.push(`[dashr] namespace snapshot skipped: ${detail} exceeds snapshotSizeCapBytes (${this.config.snapshotSizeCapBytes}) — later turn-end snapshots are skipped while the namespace stays this large`);
|
|
2062
|
-
}
|
|
890
|
+
const notices = snapshot ? [snapshot] : [];
|
|
2063
891
|
if (entry.pendingNotice) {
|
|
2064
892
|
notices.push(`[dashr] ${entry.pendingNotice}`);
|
|
2065
893
|
entry.pendingNotice = void 0;
|
|
@@ -2079,7 +907,10 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
2079
907
|
* @param name - the variable name, or empty to list namespace names.
|
|
2080
908
|
* @param principal - the session key (absent → the shared default).
|
|
2081
909
|
*/
|
|
2082
|
-
|
|
910
|
+
queryVar(name$1, principal) {
|
|
911
|
+
return this.transaction(principal || AGENTLESS_KEY, () => this.queryTransaction(name$1, principal));
|
|
912
|
+
}
|
|
913
|
+
async queryTransaction(name$1, principal) {
|
|
2083
914
|
if (this.disposed) throw new Error("dashr-repl: queryVar() after disposal");
|
|
2084
915
|
const key = principal && principal.length > 0 ? principal : AGENTLESS_KEY;
|
|
2085
916
|
const bridge = this.kernels.get(key)?.bridge;
|
|
@@ -2115,12 +946,15 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
2115
946
|
* @param value - the lossless-JSON value to bind.
|
|
2116
947
|
* @param principal - the session key (absent → the shared default).
|
|
2117
948
|
*/
|
|
2118
|
-
|
|
949
|
+
setVar(name$1, value, principal) {
|
|
950
|
+
return this.transaction(principal || AGENTLESS_KEY, () => this.setTransaction(name$1, value, principal));
|
|
951
|
+
}
|
|
952
|
+
async setTransaction(name$1, value, principal) {
|
|
2119
953
|
if (this.disposed) throw new Error("dashr-repl: setVar() after disposal");
|
|
2120
954
|
const trimmed = name$1.trim();
|
|
2121
955
|
if (trimmed.length === 0) throw new Error("dashr-repl: setVar() requires a non-empty variable name");
|
|
2122
956
|
if (!IDENTIFIER.test(trimmed)) throw new Error(`dashr-repl: setVar() name ${JSON.stringify(trimmed)} is not a usable identifier`);
|
|
2123
|
-
if (KERNEL_OWNED_NAME.test(trimmed)) throw new Error(`dashr-repl: setVar() name ${JSON.stringify(trimmed)} is reserved by the kernel`);
|
|
957
|
+
if (KERNEL_OWNED_NAME.test(trimmed) || RESERVED_BINDING_GLOBALS.has(trimmed) || trimmed === "__doc__") throw new Error(`dashr-repl: setVar() name ${JSON.stringify(trimmed)} is reserved by the kernel`);
|
|
2124
958
|
const jsonValue = snapshotJsonValue(value);
|
|
2125
959
|
if (jsonValue === void 0) throw new Error("dashr-repl: setVar() value must be lossless JSON");
|
|
2126
960
|
const key = principal && principal.length > 0 ? principal : AGENTLESS_KEY;
|
|
@@ -2128,6 +962,9 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
2128
962
|
if (!bridge || !bridge.isRunning) throw new Error("dashr-repl: setVar() requires a live kernel for this session — run a cell first");
|
|
2129
963
|
const outcome = await bridge.setVar(trimmed, JSON.stringify(jsonValue));
|
|
2130
964
|
if (outcome.kind === "failed") throw new Error(`dashr-repl: setVar(${JSON.stringify(trimmed)}) failed: ${outcome.reason}`);
|
|
965
|
+
const entry = this.kernels.get(key);
|
|
966
|
+
const notice = await this.snapshotTurnEnd(entry);
|
|
967
|
+
if (notice) entry.pendingNotice = [entry.pendingNotice, notice].filter(Boolean).join("; ");
|
|
2131
968
|
}
|
|
2132
969
|
/** Map one cell outcome onto the seam's result shape under the output ledger. */
|
|
2133
970
|
assembleResult(outcome, sentinel) {
|
|
@@ -2193,8 +1030,8 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
2193
1030
|
ok: false,
|
|
2194
1031
|
message: "host request must be a binding.call with global and name"
|
|
2195
1032
|
};
|
|
2196
|
-
const record = bindings.get(data.global)?.functions;
|
|
2197
|
-
const fn = record && Object.hasOwn(record, data.name) ? record[data.name] : void 0;
|
|
1033
|
+
const record$1 = bindings.get(data.global)?.functions;
|
|
1034
|
+
const fn = record$1 && Object.hasOwn(record$1, data.name) ? record$1[data.name] : void 0;
|
|
2198
1035
|
if (typeof fn !== "function") return {
|
|
2199
1036
|
ok: false,
|
|
2200
1037
|
message: `unknown binding ${JSON.stringify(`${data.global}.${data.name}`)}`
|
|
@@ -2261,140 +1098,114 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
2261
1098
|
disposeTimeoutMs: this.config.disposeTimeoutMs,
|
|
2262
1099
|
interruptConfirmMs: this.config.interruptConfirmMs,
|
|
2263
1100
|
snapshotTimeoutMs: this.config.snapshotTimeoutMs,
|
|
2264
|
-
username: this.config.username
|
|
1101
|
+
username: this.config.username,
|
|
1102
|
+
snapshotDirectory: this.config.snapshotDir ? join(this.config.snapshotDir, keyDirectoryName(key)) : void 0,
|
|
1103
|
+
snapshotSizeCapBytes: this.config.snapshotSizeCapBytes
|
|
2265
1104
|
}),
|
|
2266
1105
|
ranCells: false,
|
|
2267
1106
|
turn: 0
|
|
2268
1107
|
};
|
|
2269
1108
|
this.kernels.set(key, entry);
|
|
2270
1109
|
}
|
|
1110
|
+
if (this.disposed) {
|
|
1111
|
+
await entry.bridge.dispose();
|
|
1112
|
+
throw new Error("runtime disposed during startup");
|
|
1113
|
+
}
|
|
2271
1114
|
await entry.bridge.start();
|
|
2272
1115
|
return entry;
|
|
2273
1116
|
}
|
|
2274
|
-
/**
|
|
2275
|
-
|
|
2276
|
-
* exists and its manifest is plausible. Called once per entry boot. The
|
|
2277
|
-
* kernel-side restore cell performs the authoritative environment checks
|
|
2278
|
-
* (python version, interpreter identity, skills); this method only gates on
|
|
2279
|
-
* the manifest the host can read cheaply, then records the turn so the
|
|
2280
|
-
* first-run notice and the revive chain can name it.
|
|
2281
|
-
*/
|
|
2282
|
-
async restoreEntry(entry) {
|
|
1117
|
+
/** Restore once before user admission; any degraded load retires that child. */
|
|
1118
|
+
async restoreEntry(entry, bindingNames) {
|
|
2283
1119
|
if (!this.config.snapshotDir) return { kind: "none" };
|
|
2284
1120
|
const dir = join(this.config.snapshotDir, keyDirectoryName(entry.key));
|
|
2285
|
-
const payloadPath = join(dir, "state.dill");
|
|
2286
1121
|
const manifestPath = join(dir, "manifest.json");
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
if (manifest.snapshotFormat !== SNAPSHOT_FORMAT) return {
|
|
2290
|
-
kind: "degraded",
|
|
2291
|
-
reason: `snapshot format ${String(manifest.snapshotFormat)} is not replayable`
|
|
2292
|
-
};
|
|
2293
|
-
if (manifest.skipped === true) return { kind: "none" };
|
|
2294
|
-
if (!existsSync(payloadPath)) return {
|
|
2295
|
-
kind: "degraded",
|
|
2296
|
-
reason: "snapshot payload is missing"
|
|
2297
|
-
};
|
|
2298
|
-
const restored = await entry.bridge.restoreState(payloadPath, manifestPath);
|
|
1122
|
+
if (!existsSync(manifestPath)) return { kind: "none" };
|
|
1123
|
+
const restored = await entry.bridge.restoreState(join(dir, "state.dill"), manifestPath, bindingNames);
|
|
2299
1124
|
if (restored.kind === "restored") {
|
|
2300
|
-
entry.turn =
|
|
2301
|
-
entry.lastSnapshotTurn =
|
|
1125
|
+
entry.turn = restored.turn;
|
|
1126
|
+
entry.lastSnapshotTurn = restored.turn;
|
|
2302
1127
|
entry.ranCells = true;
|
|
2303
1128
|
return {
|
|
2304
1129
|
kind: "restored",
|
|
2305
|
-
turn:
|
|
1130
|
+
turn: restored.turn,
|
|
1131
|
+
omitted: restored.omitted
|
|
2306
1132
|
};
|
|
2307
1133
|
}
|
|
1134
|
+
await entry.bridge.dispose();
|
|
1135
|
+
this.kernels.delete(entry.key);
|
|
1136
|
+
entry.bridge = (await this.ensureKernel(entry.key, entry.cwd)).bridge;
|
|
1137
|
+
this.kernels.set(entry.key, entry);
|
|
2308
1138
|
return {
|
|
2309
1139
|
kind: "degraded",
|
|
2310
1140
|
reason: restored.reason
|
|
2311
1141
|
};
|
|
2312
1142
|
}
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
try {
|
|
2317
|
-
return JSON.parse(readFileSync(join(this.config.snapshotDir, keyDirectoryName(key), "manifest.json"), "utf8"));
|
|
2318
|
-
} catch {
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
1143
|
+
omissionNotice(omitted) {
|
|
1144
|
+
const entries = Object.entries(omitted);
|
|
1145
|
+
return entries.length ? `; partial coverage, omitted ${entries.map(([name$1, reason]) => `${name$1} (${reason})`).join(", ")}` : "";
|
|
2321
1146
|
}
|
|
2322
|
-
/**
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
* return the death-observing run's explicit error naming the turn. When no
|
|
2326
|
-
* replayable snapshot exists the message falls back to M3-A's fresh-empty
|
|
2327
|
-
* contract. The run itself does NOT execute — an error that lies about a
|
|
2328
|
-
* NameError costs the model more than one dropped cell (blueprint §8.3).
|
|
2329
|
-
* @param key - the run principal.
|
|
2330
|
-
* @param dead - the entry that died (its `turn` is the last completed turn).
|
|
2331
|
-
*/
|
|
2332
|
-
async reviveAfterDeath(key, dead) {
|
|
1147
|
+
/** Restore a fresh child and report whether the observing call had started. */
|
|
1148
|
+
async reviveAfterDeath(key, dead, started, bindingNames) {
|
|
1149
|
+
const execution = started ? "This cell may have executed partially, including external side effects; inspect their state before choosing any retry. No user code was replayed." : "This observing call did not execute. No user code was replayed.";
|
|
2333
1150
|
let entry;
|
|
2334
1151
|
try {
|
|
2335
1152
|
entry = await this.ensureKernel(key, dead.cwd);
|
|
2336
1153
|
} catch (error) {
|
|
2337
1154
|
return finalizeOutput([], void 0, {
|
|
2338
1155
|
kind: "worker-exit",
|
|
2339
|
-
message: `kernel died and respawn failed (${messageOf(error)}); the next run will retry a fresh kernel`
|
|
1156
|
+
message: `kernel died and respawn failed (${messageOf(error)}); the next run will retry a fresh kernel. ${execution}`
|
|
2340
1157
|
}, this.config.maxOutputBytes);
|
|
2341
1158
|
}
|
|
2342
1159
|
entry.restoreAttempted = true;
|
|
2343
|
-
const restore = await this.restoreEntry(entry);
|
|
2344
|
-
|
|
2345
|
-
if (restore.kind === "restored") {
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
kind: "worker-exit",
|
|
2350
|
-
message: `the kernel for this session died; its namespace was restored from the turn-${restore.turn} snapshot — the last ${lost} ${rounds} of variable operations must be replayed. This run did not execute.`
|
|
2351
|
-
}, this.config.maxOutputBytes);
|
|
2352
|
-
}
|
|
1160
|
+
const restore = await this.restoreEntry(entry, bindingNames);
|
|
1161
|
+
entry.turn = Math.max(entry.turn, dead.turn);
|
|
1162
|
+
if (restore.kind === "restored") return finalizeOutput([], void 0, {
|
|
1163
|
+
kind: "worker-exit",
|
|
1164
|
+
message: `the kernel for this session died; its namespace was restored from the turn-${restore.turn} snapshot${this.omissionNotice(restore.omitted)}. Changes after that checkpoint may be lost. ${execution}`
|
|
1165
|
+
}, this.config.maxOutputBytes);
|
|
2353
1166
|
return finalizeOutput([], void 0, {
|
|
2354
1167
|
kind: "worker-exit",
|
|
2355
|
-
message: `the kernel for this session died and no
|
|
1168
|
+
message: `the kernel for this session died and no usable snapshot was available${restore.kind === "degraded" ? ` (${restore.reason})` : ""}; the next run uses an EMPTY namespace. ${execution}`
|
|
2356
1169
|
}, this.config.maxOutputBytes);
|
|
2357
1170
|
}
|
|
2358
|
-
/**
|
|
2359
|
-
* Write one key's turn-end snapshot, when a snapshot directory is
|
|
2360
|
-
* configured. A size-cap skip is reported (so the run can warn the model
|
|
2361
|
-
* once); other failures log without disturbing the run.
|
|
2362
|
-
*/
|
|
1171
|
+
/** Save completed mutations and expose partial/failed coverage within output limits. */
|
|
2363
1172
|
async snapshotTurnEnd(entry) {
|
|
2364
|
-
if (!this.config.snapshotDir
|
|
2365
|
-
const kernel = entry.bridge;
|
|
2366
|
-
if (!kernel.isRunning) return { skipped: false };
|
|
1173
|
+
if (!this.config.snapshotDir || !entry.bridge.isRunning || this.disposed) return;
|
|
2367
1174
|
const dir = join(this.config.snapshotDir, keyDirectoryName(entry.key));
|
|
2368
|
-
mkdirSync(dir, { recursive: true });
|
|
2369
1175
|
const spec = {
|
|
2370
1176
|
turn: entry.turn,
|
|
2371
1177
|
skills: [],
|
|
2372
1178
|
sizeCapBytes: this.config.snapshotSizeCapBytes
|
|
2373
1179
|
};
|
|
2374
|
-
|
|
1180
|
+
let outcome;
|
|
1181
|
+
try {
|
|
1182
|
+
mkdirSync(dir, {
|
|
1183
|
+
recursive: true,
|
|
1184
|
+
mode: 448
|
|
1185
|
+
});
|
|
1186
|
+
outcome = await entry.bridge.snapshotState(join(dir, "state.dill"), join(dir, "manifest.json"), spec);
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
outcome = {
|
|
1189
|
+
kind: "failed",
|
|
1190
|
+
reason: messageOf(error)
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
2375
1193
|
if (outcome.kind === "ok") {
|
|
2376
|
-
entry.lastSnapshotTurn =
|
|
2377
|
-
|
|
1194
|
+
entry.lastSnapshotTurn = outcome.turn;
|
|
1195
|
+
const omission = this.omissionNotice(outcome.omitted);
|
|
1196
|
+
const warning = outcome.warning ? `; ${outcome.warning}` : "";
|
|
1197
|
+
return omission || warning ? `[dashr] turn-${outcome.turn} checkpoint saved${omission}${warning}` : void 0;
|
|
2378
1198
|
}
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
...outcome.estimateBytes !== void 0 ? { estimateBytes: outcome.estimateBytes } : {},
|
|
2382
|
-
...outcome.sizeBytes !== void 0 ? { sizeBytes: outcome.sizeBytes } : {}
|
|
2383
|
-
};
|
|
2384
|
-
this.logger.warn(`turn-end snapshot for ${JSON.stringify(entry.key)} failed: ${outcome.reason}`);
|
|
2385
|
-
return { skipped: false };
|
|
1199
|
+
const latest = entry.lastSnapshotTurn === void 0 ? "no successful checkpoint available" : `latest successful checkpoint is turn-${entry.lastSnapshotTurn}`;
|
|
1200
|
+
return `[dashr] namespace snapshot ${outcome.kind === "skipped" ? "skipped" : "failed"}: ${outcome.reason}; ${latest}`;
|
|
2386
1201
|
}
|
|
2387
|
-
/**
|
|
2388
|
-
* Dispose to quiescence: snapshot every live key's namespace when a
|
|
2389
|
-
* snapshot directory is configured (each under its own per-key
|
|
2390
|
-
* subdirectory), then shut the subprocesses down. Registered as the
|
|
2391
|
-
* plugin's `ctx.effect` disposer.
|
|
2392
|
-
*/
|
|
1202
|
+
/** Cancel active work, drain admitted transactions, and reap every owned child. */
|
|
2393
1203
|
async teardown() {
|
|
2394
1204
|
this.disposed = true;
|
|
2395
|
-
|
|
1205
|
+
await Promise.all([...this.kernels.values()].map((entry) => entry.bridge.dispose()));
|
|
1206
|
+
await Promise.allSettled([...this.transactions.values()]);
|
|
1207
|
+
await Promise.all([...this.kernels.values()].map((entry) => entry.bridge.dispose()));
|
|
2396
1208
|
this.kernels.clear();
|
|
2397
|
-
await Promise.all(entries.map((entry) => this.teardownKernel(entry)));
|
|
2398
1209
|
}
|
|
2399
1210
|
/**
|
|
2400
1211
|
* Tear one session's kernel down (session end via the `agent/disposed`
|
|
@@ -2402,15 +1213,17 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
2402
1213
|
* which is what makes hearing every agent's disposal safe.
|
|
2403
1214
|
*/
|
|
2404
1215
|
async destroyKey(key) {
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
1216
|
+
await this.kernels.get(key)?.bridge.dispose();
|
|
1217
|
+
await this.transaction(key, async () => {
|
|
1218
|
+
const entry = this.kernels.get(key);
|
|
1219
|
+
this.kernels.delete(key);
|
|
1220
|
+
await entry?.bridge.dispose();
|
|
1221
|
+
});
|
|
2409
1222
|
}
|
|
2410
1223
|
/**
|
|
2411
1224
|
* Reset one session's kernel to a fresh, empty namespace: dispose the live
|
|
2412
1225
|
* subprocess WITHOUT a turn-end snapshot and clear its on-disk snapshot, so
|
|
2413
|
-
* the next ensureKernel spawns empty (restore finds
|
|
1226
|
+
* the next ensureKernel spawns empty (restore finds no checkpoint).
|
|
2414
1227
|
*/
|
|
2415
1228
|
async resetKey(key) {
|
|
2416
1229
|
const entry = this.kernels.get(key);
|
|
@@ -2423,24 +1236,6 @@ var DashrRuntime = class extends ReplRuntime {
|
|
|
2423
1236
|
force: true
|
|
2424
1237
|
});
|
|
2425
1238
|
}
|
|
2426
|
-
/** Snapshot (when configured) then dispose one key's kernel; failures log, never throw into a listener. */
|
|
2427
|
-
async teardownKernel(entry) {
|
|
2428
|
-
const kernel = entry.bridge;
|
|
2429
|
-
if (this.config.snapshotDir && kernel.isRunning) try {
|
|
2430
|
-
const dir = join(this.config.snapshotDir, keyDirectoryName(entry.key));
|
|
2431
|
-
mkdirSync(dir, { recursive: true });
|
|
2432
|
-
const spec = {
|
|
2433
|
-
turn: entry.turn,
|
|
2434
|
-
skills: [],
|
|
2435
|
-
sizeCapBytes: this.config.snapshotSizeCapBytes
|
|
2436
|
-
};
|
|
2437
|
-
const saved = await kernel.snapshotState(join(dir, "state.dill"), join(dir, "manifest.json"), spec);
|
|
2438
|
-
if (saved.kind !== "ok" && saved.kind !== "skipped") this.logger.warn(`kernel namespace snapshot for ${JSON.stringify(entry.key)} failed: ${saved.reason}`);
|
|
2439
|
-
} catch (error) {
|
|
2440
|
-
this.logger.warn(`kernel namespace snapshot for ${JSON.stringify(entry.key)} failed: ${messageOf(error)}`);
|
|
2441
|
-
}
|
|
2442
|
-
await kernel.dispose();
|
|
2443
|
-
}
|
|
2444
1239
|
};
|
|
2445
1240
|
|
|
2446
1241
|
//#endregion
|
|
@@ -3389,7 +2184,7 @@ function createRunCellTool(registry, options) {
|
|
|
3389
2184
|
},
|
|
3390
2185
|
reset: {
|
|
3391
2186
|
type: "boolean",
|
|
3392
|
-
description: "Optional: reset the persistent pad namespace to EMPTY before running — variables, imports, and definitions from earlier cells are discarded and the pad restarts fresh. Omit to keep the namespace."
|
|
2187
|
+
description: "Optional: reset the persistent pad namespace to EMPTY before running and delete its saved checkpoints — variables, imports, and definitions from earlier cells are discarded and the pad restarts fresh. Omit to keep the namespace."
|
|
3393
2188
|
}
|
|
3394
2189
|
},
|
|
3395
2190
|
output: {
|