@hy-sde-org/dsh-code-runtime-kernels 0.1.2-rc.1 → 0.1.6-alpha.2
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 +42 -3
- package/README.zh.md +39 -3
- package/dist/core/kernel.d.ts +31 -5
- package/dist/core/kernel.d.ts.map +1 -1
- package/dist/core/kernel.js +12 -1
- package/dist/core/kernel.js.map +1 -1
- package/dist/core/protocol.d.ts +22 -5
- package/dist/core/protocol.d.ts.map +1 -1
- package/dist/core/session.d.ts +17 -2
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +65 -3
- package/dist/core/session.js.map +1 -1
- package/dist/index.d.ts +47 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +117 -35
- package/dist/index.js.map +1 -1
- package/dist/nodejs/runner.js +162 -0
- package/dist/nodejs/runner.js.map +1 -1
- package/dist/python/runner.d.ts.map +1 -1
- package/dist/python/runner.js +365 -0
- package/dist/python/runner.js.map +1 -1
- package/package.json +19 -18
package/dist/python/runner.js
CHANGED
|
@@ -35,13 +35,19 @@ import ast
|
|
|
35
35
|
import asyncio
|
|
36
36
|
import builtins
|
|
37
37
|
import contextvars
|
|
38
|
+
import importlib
|
|
38
39
|
import io
|
|
39
40
|
import json
|
|
41
|
+
import marshal
|
|
40
42
|
import os
|
|
43
|
+
import pickle
|
|
41
44
|
import signal
|
|
42
45
|
import sys
|
|
43
46
|
import threading
|
|
47
|
+
import time
|
|
44
48
|
import traceback
|
|
49
|
+
import types
|
|
50
|
+
import zlib
|
|
45
51
|
from typing import Any
|
|
46
52
|
|
|
47
53
|
# ---------------------------------------------------------------------------
|
|
@@ -194,11 +200,70 @@ class _RunnerState:
|
|
|
194
200
|
self.capture_rid: str | None = None
|
|
195
201
|
# (run_id, seq) -> Future awaiting the host's reply to a binding call.
|
|
196
202
|
self.pending_calls: dict[tuple[str, int], asyncio.Future] = {}
|
|
203
|
+
# Namespace persistence (session snapshots): the spec of the CURRENT
|
|
204
|
+
# exec, whether this process already restored once, and how the last
|
|
205
|
+
# run settled (a snapshot is written only after a successful run).
|
|
206
|
+
self.snapshot_spec: dict | None = None
|
|
207
|
+
self.restored_once: bool = False
|
|
208
|
+
self.last_status: str = "ok"
|
|
197
209
|
|
|
198
210
|
|
|
199
211
|
_CURRENT_RID: contextvars.ContextVar[str | None] = contextvars.ContextVar("dsh_rid", default=None)
|
|
200
212
|
_STATE = _RunnerState()
|
|
201
213
|
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# Optional ipykernel/real-IPython execution mode (config pythonImpl: ipykernel)
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
#
|
|
218
|
+
# The default runner is self-contained stdlib; this mode swaps cell execution
|
|
219
|
+
# to a real IPython shell (magics, !cmd, display) while keeping the SAME
|
|
220
|
+
# protocol, namespaces, binding proxies, and snapshot machinery.
|
|
221
|
+
|
|
222
|
+
_IPYTHON_SHELL = None
|
|
223
|
+
_IPYTHON_SKIP = {"In", "Out", "get_ipython", "exit", "quit", "open", "_", "__", "___", "_oh", "_ih"}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _maybe_init_ipython() -> None:
|
|
227
|
+
"""Activate IPython semantics when the host configured pythonImpl: ipykernel.
|
|
228
|
+
|
|
229
|
+
Fails loud at boot (exit 2) when IPython is not importable so the host
|
|
230
|
+
reports a startup failure instead of silently running with stdlib.
|
|
231
|
+
"""
|
|
232
|
+
global _IPYTHON_SHELL
|
|
233
|
+
if os.environ.get("DSH_KERNEL_PYTHON_IMPL") != "ipykernel":
|
|
234
|
+
return
|
|
235
|
+
try:
|
|
236
|
+
from IPython.core.displayhook import DisplayHook
|
|
237
|
+
from IPython.core.interactiveshell import DisplayTrap, InteractiveShell
|
|
238
|
+
except ImportError as exc:
|
|
239
|
+
sys.stderr.write(
|
|
240
|
+
"dsh-code-runtime-kernels: pythonImpl 'ipykernel' needs IPython in the interpreter"
|
|
241
|
+
+ " that spawns kernels (pip install ipykernel): " + str(exc) + "\n")
|
|
242
|
+
sys.stderr.flush()
|
|
243
|
+
os._exit(2)
|
|
244
|
+
shell = InteractiveShell.instance()
|
|
245
|
+
shell.colors = "NoColor"
|
|
246
|
+
|
|
247
|
+
class _QuietDisplayHook(DisplayHook):
|
|
248
|
+
def write_output_prompt(self):
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
def write_format_data(self, format_dict, md_dict=None):
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
# IPython installs its own sys.displayhook + DisplayTrap at init; both must
|
|
255
|
+
# be replaced (and the trap re-bound) or every expression prints Out[n].
|
|
256
|
+
hook = _QuietDisplayHook(shell=shell, cache_size=0)
|
|
257
|
+
shell.displayhook = hook
|
|
258
|
+
shell.display_trap = DisplayTrap(hook=hook)
|
|
259
|
+
sys.displayhook = hook
|
|
260
|
+
ns = shell.user_ns
|
|
261
|
+
if not isinstance(ns.get("__name__"), str):
|
|
262
|
+
ns["__name__"] = "__main__"
|
|
263
|
+
_STATE.user_ns = ns
|
|
264
|
+
_IPYTHON_SHELL = shell
|
|
265
|
+
|
|
266
|
+
|
|
202
267
|
_call_seq = 0
|
|
203
268
|
|
|
204
269
|
|
|
@@ -380,6 +445,7 @@ def _finish_run(
|
|
|
380
445
|
invalid_output: bool = False,
|
|
381
446
|
message: str = "",
|
|
382
447
|
) -> None:
|
|
448
|
+
_STATE.last_status = "ok" if (status == "ok" and not cancelled) else "error"
|
|
383
449
|
_STATE.execution_count += 1
|
|
384
450
|
frame: dict = {
|
|
385
451
|
"type": "done",
|
|
@@ -410,7 +476,77 @@ async def _await_code(code_obj: Any, ns: dict) -> Any:
|
|
|
410
476
|
return eval(code_obj, ns)
|
|
411
477
|
|
|
412
478
|
|
|
479
|
+
async def _run_cell_ipython(code: str, run_id: str, namespaces: list[dict]) -> None:
|
|
480
|
+
"""IPython-mode cell: real shell semantics (magics, !cmd, display, TLA).
|
|
481
|
+
|
|
482
|
+
run_cell_async executes inside the runner's live asyncio loop (run_cell
|
|
483
|
+
would try run_until_complete on a loop that is already running), so
|
|
484
|
+
top-level await interleaves exactly like stdlib mode. It returns the last
|
|
485
|
+
expression as result.result and splits syntax/exec errors into
|
|
486
|
+
error_before_exec / error_in_exec; the completion value still goes through
|
|
487
|
+
the lossless-JSON gate so the return-value contract is identical to
|
|
488
|
+
stdlib mode.
|
|
489
|
+
"""
|
|
490
|
+
ns = _STATE.user_ns
|
|
491
|
+
restores = _materialize_namespaces(run_id, ns, namespaces)
|
|
492
|
+
_STATE.capture_rid = run_id
|
|
493
|
+
token = _CURRENT_RID.set(run_id)
|
|
494
|
+
try:
|
|
495
|
+
transformed_cell = None
|
|
496
|
+
preprocessing_exc = None
|
|
497
|
+
try:
|
|
498
|
+
transformed_cell = _IPYTHON_SHELL.transform_cell(code)
|
|
499
|
+
except Exception as exc: # noqa: BLE001 - any transform failure is the cell's parse error
|
|
500
|
+
preprocessing_exc = (type(exc), exc, exc.__traceback__)
|
|
501
|
+
result = await _IPYTHON_SHELL.run_cell_async(
|
|
502
|
+
code, store_history=False, transformed_cell=transformed_cell, preprocessing_exc_tuple=preprocessing_exc)
|
|
503
|
+
_flush_stream_proxies(run_id)
|
|
504
|
+
if not result.success:
|
|
505
|
+
exc = result.error_in_exec
|
|
506
|
+
if exc is None:
|
|
507
|
+
exc = result.error_before_exec
|
|
508
|
+
tb = traceback.format_exception(type(exc), exc, exc.__traceback__) if exc is not None else []
|
|
509
|
+
_emit({
|
|
510
|
+
"type": "error",
|
|
511
|
+
"id": run_id,
|
|
512
|
+
"ename": type(exc).__name__ if exc is not None else "Error",
|
|
513
|
+
"evalue": str(exc) if exc is not None else "cell failed",
|
|
514
|
+
"traceback": tb,
|
|
515
|
+
})
|
|
516
|
+
_finish_run(run_id, status="error", message=str(exc) if exc is not None else "cell failed")
|
|
517
|
+
return
|
|
518
|
+
value = result.result
|
|
519
|
+
if value is None:
|
|
520
|
+
_finish_run(run_id, status="ok")
|
|
521
|
+
return
|
|
522
|
+
serialized_ok, serialized = _lossless_json(value)
|
|
523
|
+
if not serialized_ok:
|
|
524
|
+
_finish_run(run_id, status="error", invalid_output=True)
|
|
525
|
+
return
|
|
526
|
+
_finish_run(run_id, status="ok", value=serialized, has_value=True)
|
|
527
|
+
except KeyboardInterrupt:
|
|
528
|
+
_flush_stream_proxies(run_id)
|
|
529
|
+
_STATE.cancel_requested = False
|
|
530
|
+
_finish_run(run_id, status="error", cancelled=True)
|
|
531
|
+
except asyncio.CancelledError:
|
|
532
|
+
_flush_stream_proxies(run_id)
|
|
533
|
+
_STATE.cancel_requested = False
|
|
534
|
+
_finish_run(run_id, status="error", cancelled=True)
|
|
535
|
+
raise
|
|
536
|
+
finally:
|
|
537
|
+
_CURRENT_RID.reset(token)
|
|
538
|
+
_STATE.capture_rid = None
|
|
539
|
+
for name, previous in restores:
|
|
540
|
+
if previous is _MISSING:
|
|
541
|
+
ns.pop(name, None)
|
|
542
|
+
else:
|
|
543
|
+
ns[name] = previous
|
|
544
|
+
|
|
545
|
+
|
|
413
546
|
async def _run_cell(code: str, run_id: str, namespaces: list[dict]) -> None:
|
|
547
|
+
if _IPYTHON_SHELL is not None:
|
|
548
|
+
await _run_cell_ipython(code, run_id, namespaces)
|
|
549
|
+
return
|
|
414
550
|
sys.stdout = _StreamProxy("stdout")
|
|
415
551
|
sys.stderr = _StreamProxy("stderr") # noqa: F841 - kept for symmetry
|
|
416
552
|
ns = _STATE.user_ns
|
|
@@ -477,6 +613,224 @@ async def _run_cell(code: str, run_id: str, namespaces: list[dict]) -> None:
|
|
|
477
613
|
ns[name] = previous
|
|
478
614
|
|
|
479
615
|
|
|
616
|
+
# ---------------------------------------------------------------------------
|
|
617
|
+
# Namespace persistence: session snapshots (stdlib pickle + zlib, atomic)
|
|
618
|
+
# ---------------------------------------------------------------------------
|
|
619
|
+
#
|
|
620
|
+
# The kernel owns the file. After every successfully settled run the user
|
|
621
|
+
# namespace is packed entry by entry and written to spec["path"] via
|
|
622
|
+
# temp-file-and-rename; a fresh process restores it once, on its first exec.
|
|
623
|
+
# Entries that cannot be represented (user-defined class instances, lambdas in
|
|
624
|
+
# containers, closures over non-picklables) are named, never silently dropped.
|
|
625
|
+
# Pickle is NOT a security boundary — a snapshot file grants code execution to
|
|
626
|
+
# whoever reads it, but the kernel already runs model code (bash-equivalent
|
|
627
|
+
# trust), so the file stays inside the user's own state directory.
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
_SNAPSHOT_VERSION = 1
|
|
631
|
+
_RESTORE_NOTICE_LIMIT = 12
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _is_snapshot_spec(value: Any) -> bool:
|
|
635
|
+
if not isinstance(value, dict):
|
|
636
|
+
return False
|
|
637
|
+
path = value.get("path")
|
|
638
|
+
max_bytes = value.get("maxBytes")
|
|
639
|
+
max_entry = value.get("maxEntryBytes")
|
|
640
|
+
return (
|
|
641
|
+
isinstance(path, str)
|
|
642
|
+
and bool(path)
|
|
643
|
+
and isinstance(max_bytes, int)
|
|
644
|
+
and max_bytes > 0
|
|
645
|
+
and isinstance(max_entry, int)
|
|
646
|
+
and max_entry > 0
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _pack_class(cls: type) -> dict:
|
|
651
|
+
base_payloads = []
|
|
652
|
+
for base in cls.__bases__:
|
|
653
|
+
base_payloads.append(pickle.dumps(base, protocol=pickle.HIGHEST_PROTOCOL))
|
|
654
|
+
members: dict[str, dict] = {}
|
|
655
|
+
skip = {"__dict__", "__weakref__", "__module__", "__qualname__", "__doc__", "__slots__"}
|
|
656
|
+
for key, value in list(cls.__dict__.items()):
|
|
657
|
+
if key in skip:
|
|
658
|
+
continue
|
|
659
|
+
if isinstance(value, staticmethod):
|
|
660
|
+
members[key] = {"kind": "staticmethod", "func": _pack_value(value.__func__)}
|
|
661
|
+
elif isinstance(value, classmethod):
|
|
662
|
+
members[key] = {"kind": "classmethod", "func": _pack_value(value.__func__)}
|
|
663
|
+
elif isinstance(value, property):
|
|
664
|
+
raise TypeError("property members are not snapshot-supported")
|
|
665
|
+
else:
|
|
666
|
+
members[key] = _pack_value(value)
|
|
667
|
+
return {"kind": "class", "name": cls.__name__, "bases": base_payloads, "members": members}
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _pack_value(value: Any) -> dict:
|
|
671
|
+
"""One namespace entry as a snapshot record. Raises when it cannot be saved."""
|
|
672
|
+
if isinstance(value, types.ModuleType):
|
|
673
|
+
return {"kind": "module", "name": value.__name__}
|
|
674
|
+
if isinstance(value, (types.BuiltinFunctionType, types.BuiltinMethodType)):
|
|
675
|
+
module_name = getattr(value, "__module__", None)
|
|
676
|
+
qualname = getattr(value, "__qualname__", getattr(value, "__name__", None))
|
|
677
|
+
if isinstance(module_name, str) and isinstance(qualname, str):
|
|
678
|
+
return {"kind": "builtin", "module": module_name, "qualname": qualname}
|
|
679
|
+
raise TypeError("builtin without importable identity cannot be snapshotted")
|
|
680
|
+
if isinstance(value, type):
|
|
681
|
+
return _pack_class(value)
|
|
682
|
+
if isinstance(value, types.FunctionType):
|
|
683
|
+
# Imported functions/classes pickle by reference; __main__-defined
|
|
684
|
+
# callables do not, so fall back to by-value (code + defaults + cells).
|
|
685
|
+
try:
|
|
686
|
+
return {"kind": "ref", "payload": pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL)}
|
|
687
|
+
except Exception:
|
|
688
|
+
pass
|
|
689
|
+
closure = value.__closure__ or ()
|
|
690
|
+
# Code objects are not picklable, so the bytecode travels as marshal
|
|
691
|
+
# bytes (same-interpreter restarts guarantee a compatible marshal
|
|
692
|
+
# format; a cross-version restore fails per-name and is reported).
|
|
693
|
+
return {
|
|
694
|
+
"kind": "function",
|
|
695
|
+
"name": value.__name__,
|
|
696
|
+
"qualname": getattr(value, "__qualname__", value.__name__),
|
|
697
|
+
"code": marshal.dumps(value.__code__),
|
|
698
|
+
"defaults": value.__defaults__,
|
|
699
|
+
"kwdefaults": value.__kwdefaults__,
|
|
700
|
+
"closure": [cell.cell_contents for cell in closure],
|
|
701
|
+
}
|
|
702
|
+
return {"kind": "data", "payload": pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL)}
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _rebuild_function(record: dict) -> Any:
|
|
706
|
+
code = marshal.loads(record["code"])
|
|
707
|
+
defaults = record.get("defaults")
|
|
708
|
+
closure_values = record.get("closure")
|
|
709
|
+
cells = None
|
|
710
|
+
if isinstance(closure_values, list):
|
|
711
|
+
cells = tuple(types.CellType(value) for value in closure_values)
|
|
712
|
+
fn = types.FunctionType(code, _STATE.user_ns, record.get("name") or "<lambda>", defaults, cells)
|
|
713
|
+
kwdefaults = record.get("kwdefaults")
|
|
714
|
+
if kwdefaults is not None:
|
|
715
|
+
fn.__kwdefaults__ = kwdefaults
|
|
716
|
+
qualname = record.get("qualname")
|
|
717
|
+
if isinstance(qualname, str):
|
|
718
|
+
fn.__qualname__ = qualname
|
|
719
|
+
fn.__module__ = "__main__"
|
|
720
|
+
return fn
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _rebuild_class(record: dict) -> Any:
|
|
724
|
+
bases = tuple(pickle.loads(payload) for payload in record["bases"])
|
|
725
|
+
members: dict[str, Any] = {}
|
|
726
|
+
for key, member in record["members"].items():
|
|
727
|
+
kind = member.get("kind")
|
|
728
|
+
if kind == "staticmethod":
|
|
729
|
+
members[key] = staticmethod(_unpack_value(member["func"]))
|
|
730
|
+
elif kind == "classmethod":
|
|
731
|
+
members[key] = classmethod(_unpack_value(member["func"]))
|
|
732
|
+
else:
|
|
733
|
+
members[key] = _unpack_value(member)
|
|
734
|
+
cls = type(record["name"], bases, members)
|
|
735
|
+
cls.__module__ = "__main__"
|
|
736
|
+
return cls
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _unpack_value(record: dict) -> Any:
|
|
740
|
+
kind = record.get("kind")
|
|
741
|
+
if kind == "module":
|
|
742
|
+
return importlib.import_module(record["name"])
|
|
743
|
+
if kind == "builtin":
|
|
744
|
+
target = importlib.import_module(record["module"])
|
|
745
|
+
for part in record["qualname"].split("."):
|
|
746
|
+
target = getattr(target, part)
|
|
747
|
+
return target
|
|
748
|
+
if kind in ("data", "ref"):
|
|
749
|
+
return pickle.loads(record["payload"])
|
|
750
|
+
if kind == "function":
|
|
751
|
+
return _rebuild_function(record)
|
|
752
|
+
if kind == "class":
|
|
753
|
+
return _rebuild_class(record)
|
|
754
|
+
raise ValueError(f"unknown snapshot record kind {kind!r}")
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _collect_entries(spec: dict) -> tuple[dict, list[str]]:
|
|
758
|
+
max_bytes = spec["maxBytes"]
|
|
759
|
+
max_entry = spec["maxEntryBytes"]
|
|
760
|
+
entries: dict[str, dict] = {}
|
|
761
|
+
skipped: list[str] = []
|
|
762
|
+
total = 0
|
|
763
|
+
for name, value in list(_STATE.user_ns.items()):
|
|
764
|
+
if name.startswith("__") or name in _IPYTHON_SKIP:
|
|
765
|
+
continue
|
|
766
|
+
try:
|
|
767
|
+
record = _pack_value(value)
|
|
768
|
+
size = len(pickle.dumps(record, protocol=pickle.HIGHEST_PROTOCOL))
|
|
769
|
+
except Exception:
|
|
770
|
+
skipped.append(name)
|
|
771
|
+
continue
|
|
772
|
+
if size > max_entry or total + size > max_bytes:
|
|
773
|
+
skipped.append(name)
|
|
774
|
+
continue
|
|
775
|
+
entries[name] = record
|
|
776
|
+
total += size
|
|
777
|
+
return entries, skipped
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _save_snapshot(spec: dict) -> None:
|
|
781
|
+
"""Write the current namespace atomically; a failure never fails the run."""
|
|
782
|
+
path = spec["path"]
|
|
783
|
+
tmp = ""
|
|
784
|
+
try:
|
|
785
|
+
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
786
|
+
entries, skipped = _collect_entries(spec)
|
|
787
|
+
doc = {"version": _SNAPSHOT_VERSION, "savedAt": time.time(), "entries": entries, "lost": skipped}
|
|
788
|
+
payload = zlib.compress(pickle.dumps(doc, protocol=pickle.HIGHEST_PROTOCOL))
|
|
789
|
+
tmp = path + ".tmp"
|
|
790
|
+
with open(tmp, "wb") as handle:
|
|
791
|
+
handle.write(payload)
|
|
792
|
+
os.replace(tmp, path)
|
|
793
|
+
except BaseException:
|
|
794
|
+
try:
|
|
795
|
+
if tmp:
|
|
796
|
+
os.unlink(tmp)
|
|
797
|
+
except OSError:
|
|
798
|
+
pass
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _restore_snapshot(spec: dict, rid: str) -> None:
|
|
802
|
+
"""Restore the last saved namespace once; report revived and lost names."""
|
|
803
|
+
path = spec["path"]
|
|
804
|
+
if not os.path.exists(path):
|
|
805
|
+
return
|
|
806
|
+
try:
|
|
807
|
+
with open(path, "rb") as handle:
|
|
808
|
+
doc = pickle.loads(zlib.decompress(handle.read()))
|
|
809
|
+
if not isinstance(doc, dict) or doc.get("version") != _SNAPSHOT_VERSION:
|
|
810
|
+
raise ValueError("unsupported snapshot version")
|
|
811
|
+
entries = doc.get("entries")
|
|
812
|
+
if not isinstance(entries, dict):
|
|
813
|
+
raise ValueError("snapshot entries missing")
|
|
814
|
+
except BaseException as exc: # noqa: BLE001 - a bad snapshot must not wedge the kernel
|
|
815
|
+
_emit({"type": "log", "id": rid, "text": f"[dsh-kernels] snapshot unreadable (restore skipped): {exc}"})
|
|
816
|
+
return
|
|
817
|
+
restored: list[str] = []
|
|
818
|
+
lost = [name for name in doc.get("lost", []) if isinstance(name, str)]
|
|
819
|
+
for name, record in entries.items():
|
|
820
|
+
try:
|
|
821
|
+
_STATE.user_ns[name] = _unpack_value(record)
|
|
822
|
+
restored.append(name)
|
|
823
|
+
except BaseException:
|
|
824
|
+
lost.append(name)
|
|
825
|
+
notice = f"[dsh-kernels] restored {len(restored)} names from snapshot"
|
|
826
|
+
if lost:
|
|
827
|
+
shown = ", ".join(lost[:_RESTORE_NOTICE_LIMIT])
|
|
828
|
+
if len(lost) > _RESTORE_NOTICE_LIMIT:
|
|
829
|
+
shown += ", ..."
|
|
830
|
+
notice += f" (could not restore: {shown})"
|
|
831
|
+
_emit({"type": "log", "id": rid, "text": notice})
|
|
832
|
+
|
|
833
|
+
|
|
480
834
|
def _install_idle_sigint() -> None:
|
|
481
835
|
"""SIGINT (host interrupt/escalation) interrupts the active run.
|
|
482
836
|
|
|
@@ -545,18 +899,29 @@ async def _handle_request_async(req: dict) -> None:
|
|
|
545
899
|
namespaces = req.get("namespaces")
|
|
546
900
|
if not isinstance(namespaces, list):
|
|
547
901
|
namespaces = []
|
|
902
|
+
# Namespace persistence: restore once per process (on the first exec that
|
|
903
|
+
# carries a spec), then remember the spec so the run's success can persist.
|
|
904
|
+
snapshot = req.get("snapshot")
|
|
905
|
+
_STATE.snapshot_spec = snapshot if _is_snapshot_spec(snapshot) else None
|
|
906
|
+
if _STATE.snapshot_spec is not None and not _STATE.restored_once:
|
|
907
|
+
_STATE.restored_once = True
|
|
908
|
+
_restore_snapshot(_STATE.snapshot_spec, rid)
|
|
548
909
|
_STATE.cancel_requested = False
|
|
549
910
|
try:
|
|
550
911
|
await _run_cell(code, rid, namespaces)
|
|
551
912
|
except asyncio.CancelledError:
|
|
552
913
|
_STATE.cancel_requested = False
|
|
553
914
|
_finish_run(rid, status="error", cancelled=True)
|
|
915
|
+
finally:
|
|
916
|
+
if _STATE.snapshot_spec is not None and _STATE.last_status == "ok":
|
|
917
|
+
_save_snapshot(_STATE.snapshot_spec)
|
|
554
918
|
|
|
555
919
|
|
|
556
920
|
async def _main_async() -> None:
|
|
557
921
|
sys.stdout = _StreamProxy("stdout")
|
|
558
922
|
sys.stderr = _StreamProxy("stderr")
|
|
559
923
|
_install_idle_sigint()
|
|
924
|
+
_maybe_init_ipython()
|
|
560
925
|
_start_capture_drain()
|
|
561
926
|
|
|
562
927
|
stdin = sys.__stdin__
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/python/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/python/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA07BtC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hy-sde-org/dsh-code-runtime-kernels",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6-alpha.2",
|
|
4
4
|
"description": "The run_kernel_code tool for DeepSeek Harness: persistent Python and JavaScript kernels (a long-lived python3/node subprocess per session, sharing a core host driver) — a standalone plugin with no upstream harness changes required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
|
-
"url": "git+https://github.com/hy-sde/dsh-
|
|
35
|
+
"url": "git+https://github.com/hy-sde/dsh-plugins.git",
|
|
36
|
+
"directory": "dsh-code-runtime-kernels/packages/code-runtime-kernels"
|
|
36
37
|
},
|
|
37
38
|
"license": "MIT",
|
|
38
39
|
"dsh": {
|
|
@@ -45,29 +46,29 @@
|
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
|
47
48
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/dsh-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
49
|
+
"@deepseek-ai/dsh-invariants": "^0.1.6-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-session": "^0.1.6-alpha.2",
|
|
51
|
+
"@deepseek-ai/dsh-spill": "^0.1.6-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-timeout": "^0.1.6-alpha.2",
|
|
53
|
+
"@deepseek-ai/dsh-tools": "^0.1.6-alpha.2",
|
|
54
|
+
"@deepseek-ai/dsh-util-values": "^0.1.6-alpha.2",
|
|
55
|
+
"@deepseek-ai/dsh-ptc-runtime": "^0.1.6-alpha.2"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
64
|
-
"@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-invariants": "^0.1.6-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-session": "^0.1.6-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-spill": "^0.1.6-alpha.2",
|
|
62
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.6-alpha.2",
|
|
63
|
+
"@deepseek-ai/dsh-timeout": "^0.1.6-alpha.2",
|
|
64
|
+
"@deepseek-ai/dsh-tools": "^0.1.6-alpha.2",
|
|
65
65
|
"@deepseek-ai/schemastery": "3.18.1",
|
|
66
66
|
"@types/node": "^24.13.3",
|
|
67
67
|
"typescript": "^5.9.3",
|
|
68
68
|
"vitest": "^3.2.7",
|
|
69
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
70
|
-
"@deepseek-ai/dsh-util-values": "^0.1.
|
|
69
|
+
"@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
|
|
70
|
+
"@deepseek-ai/dsh-util-values": "^0.1.6-alpha.2",
|
|
71
|
+
"@deepseek-ai/dsh-ptc-runtime": "^0.1.6-alpha.2"
|
|
71
72
|
},
|
|
72
73
|
"keywords": [
|
|
73
74
|
"deepseek",
|