@hy-sde-org/dsh-code-runtime-kernels 0.1.2-rc.1 → 0.1.5-rc.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.
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6kBtC,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.2-rc.1",
3
+ "version": "0.1.5-rc.1",
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-code-runtime-kernels.git"
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-code-runtime": "^0.1.2-rc.1",
49
- "@deepseek-ai/dsh-invariants": "^0.1.2-rc.1",
50
- "@deepseek-ai/dsh-session": "^0.1.2-rc.1",
51
- "@deepseek-ai/dsh-spill": "^0.1.2-rc.1",
52
- "@deepseek-ai/dsh-timeout": "^0.1.2-rc.1",
53
- "@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
54
- "@deepseek-ai/dsh-util-values": "^0.1.2-rc.1"
49
+ "@deepseek-ai/dsh-code-runtime": "^0.1.5-rc.1",
50
+ "@deepseek-ai/dsh-invariants": "^0.1.5-rc.1",
51
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.1",
52
+ "@deepseek-ai/dsh-spill": "^0.1.5-rc.1",
53
+ "@deepseek-ai/dsh-timeout": "^0.1.5-rc.1",
54
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.1",
55
+ "@deepseek-ai/dsh-util-values": "^0.1.5-rc.1"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@deepseek-ai/cordis": "^4.0.2",
58
- "@deepseek-ai/dsh-code-runtime": "^0.1.2-rc.1",
59
- "@deepseek-ai/dsh-invariants": "^0.1.2-rc.1",
60
- "@deepseek-ai/dsh-session": "^0.1.2-rc.1",
61
- "@deepseek-ai/dsh-spill": "^0.1.2-rc.1",
62
- "@deepseek-ai/dsh-system-prompt": "^0.1.2-rc.1",
63
- "@deepseek-ai/dsh-timeout": "^0.1.2-rc.1",
64
- "@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
59
+ "@deepseek-ai/dsh-code-runtime": "^0.1.5-rc.1",
60
+ "@deepseek-ai/dsh-invariants": "^0.1.5-rc.1",
61
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.1",
62
+ "@deepseek-ai/dsh-spill": "^0.1.5-rc.1",
63
+ "@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.1",
64
+ "@deepseek-ai/dsh-timeout": "^0.1.5-rc.1",
65
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.1",
65
66
  "@deepseek-ai/schemastery": "3.18.1",
66
67
  "@types/node": "^24.13.3",
67
68
  "typescript": "^5.9.3",
68
69
  "vitest": "^3.2.7",
69
- "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
70
- "@deepseek-ai/dsh-util-values": "^0.1.2-rc.1"
70
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
71
+ "@deepseek-ai/dsh-util-values": "^0.1.5-rc.1"
71
72
  },
72
73
  "keywords": [
73
74
  "deepseek",