@code-yeongyu/senpi-codemode 2026.10.9 → 2026.10.10-11
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/CHANGELOG.md +190 -0
- package/README.md +161 -11
- package/package.json +6 -5
- package/src/bridge/memory-protocol.ts +8 -1
- package/src/bridge/protocol.ts +9 -4
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/eval-environment-schemas.ts +52 -0
- package/src/bridges/eval-virtual-schemas.ts +11 -4
- package/src/bridges/reserved-dispatch.ts +13 -1
- package/src/config/executable-settings.json +5 -0
- package/src/config/project-trust.ts +51 -0
- package/src/config/settings-overrides.ts +2 -0
- package/src/environments/editable-check.ts +1 -1
- package/src/environments/js-environments.ts +208 -0
- package/src/environments/js-installer.ts +268 -0
- package/src/environments/js-revision-files.ts +91 -0
- package/src/environments/no-links.ts +132 -0
- package/src/environments/packages-install.ts +98 -0
- package/src/environments/project-resolves.ts +54 -0
- package/src/environments/py-environment.ts +7 -3
- package/src/environments/py-installer.ts +5 -1
- package/src/environments/python-environments.ts +1 -1
- package/src/environments/revision-store.ts +20 -4
- package/src/extension/bridge-tool-call.ts +5 -1
- package/src/extension/eval-notifier.ts +13 -2
- package/src/extension/kernel-registration.ts +7 -2
- package/src/extension/runtime-factory.ts +51 -13
- package/src/extension/runtime-info.ts +6 -2
- package/src/extension/session-cwd.ts +25 -9
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager.ts +20 -6
- package/src/index.ts +3 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/jl/runner.jl +11 -22
- package/src/kernels/js/cell-dispatch.ts +44 -0
- package/src/kernels/js/cell-ownership.js +268 -0
- package/src/kernels/js/cell-run-context.js +89 -0
- package/src/kernels/js/context-manager.ts +46 -51
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/group-signal-notice.js +117 -0
- package/src/kernels/js/host-entries.ts +62 -0
- package/src/kernels/js/inline-worker.ts +2 -0
- package/src/kernels/js/kernel-contract.ts +16 -1
- package/src/kernels/js/kernel-memory-bridge.ts +1 -1
- package/src/kernels/js/kernel-memory.ts +79 -0
- package/src/kernels/js/kernel-tools-registry.js +1 -0
- package/src/kernels/js/local-module-loader.ts +25 -6
- package/src/kernels/js/process-drain-scan.d.ts +8 -0
- package/src/kernels/js/process-drain-scan.js +28 -0
- package/src/kernels/js/process-entry.js +466 -0
- package/src/kernels/js/process-memory-host.ts +83 -0
- package/src/kernels/js/process-tree-host.ts +4 -8
- package/src/kernels/js/process-tree.d.ts +11 -0
- package/src/kernels/js/process-tree.js +55 -10
- package/src/kernels/js/process-worker.ts +298 -0
- package/src/kernels/js/rejection-reports.js +61 -0
- package/src/kernels/js/run-queue.ts +7 -0
- package/src/kernels/js/windows-tree-kill-host.ts +117 -0
- package/src/kernels/js/windows-tree-kill.d.ts +19 -0
- package/src/kernels/js/windows-tree-kill.js +100 -0
- package/src/kernels/js/worker-core.js +53 -8
- package/src/kernels/js/worker-cwd.js +46 -12
- package/src/kernels/js/worker-host.ts +9 -2
- package/src/kernels/js/worker-indirect-eval.js +37 -13
- package/src/kernels/js/worker-package-resolve.js +153 -0
- package/src/kernels/js/worker-require.d.ts +19 -0
- package/src/kernels/js/worker-require.js +52 -0
- package/src/kernels/js/worker-runtime.js +66 -13
- package/src/kernels/js/worker-shadow-guard.js +2 -6
- package/src/kernels/js/worker-shell-capture.js +9 -0
- package/src/kernels/js/worker-slot.ts +8 -0
- package/src/kernels/js/worker-startup.ts +49 -1
- package/src/kernels/js/worker-webview.js +15 -1
- package/src/kernels/py/host-cell.ts +8 -31
- package/src/kernels/py/prelude.py +69 -27
- package/src/kernels/py/process.ts +4 -0
- package/src/kernels/py/transport.ts +4 -0
- package/src/kernels/rb/runner.rb +9 -19
- package/src/kernels/sandbox/sandbox-cell.ts +141 -0
- package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +1 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +14 -1
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +4 -1
- package/src/kernels/sandbox/vendor/pi-codemode/types.ts +2 -0
- package/src/kernels/shared/bun-ffi.ts +24 -0
- package/src/kernels/shared/host-cell.ts +57 -0
- package/src/kernels/shared/kernel-memory-host.ts +30 -7
- package/src/kernels/shared/process-cpu.ts +7 -3
- package/src/kernels/shared/process-group-cpu.ts +6 -3
- package/src/kernels/shared/subprocess-kernel.ts +29 -3
- package/src/kernels/shared/subprocess-memory-globals.ts +101 -0
- package/src/kernels/shared/subprocess-process.ts +24 -2
- package/src/kernels/shared/subprocess-queue.ts +1 -0
- package/src/prompt/eval-prompt.ts +8 -1
- package/src/tool/AGENTS.md +1 -0
- package/src/tool/cell-handler.ts +12 -11
- package/src/tool/cell-reserved-dispatch.ts +1 -0
- package/src/tool/cell-runtime.ts +4 -2
- package/src/tool/detached-cell-contract.ts +3 -0
- package/src/tool/detached-cell-manager.ts +4 -8
- package/src/tool/detached-cell-notification.ts +11 -63
- package/src/tool/detached-cell-snapshot.ts +17 -0
- package/src/tool/detached-eval-result.ts +13 -7
- package/src/tool/detached-notification-queue.ts +1 -2
- package/src/tool/eval-request.ts +23 -1
- package/src/tool/eval-tool-options.ts +3 -0
- package/src/tool/eval-tool.ts +19 -5
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- package/src/tool/load-cell.ts +18 -3
- package/src/tool/magic-cell-host.ts +60 -2
- package/src/tool/magic-cells.ts +12 -2
- package/src/tool/managed-cell.ts +0 -4
- package/src/tool/render-agent.ts +110 -0
- package/src/tool/render-blocks.ts +252 -0
- package/src/tool/render-call.ts +130 -0
- package/src/tool/render-cell.ts +165 -0
- package/src/tool/render-detail.ts +152 -0
- package/src/tool/render-live.ts +288 -0
- package/src/tool/render-result.ts +202 -0
- package/src/tool/render-status.ts +166 -0
- package/src/tool/render-tail.ts +139 -0
- package/src/tool/render.ts +3 -1069
- package/src/tool/run-eval-cell.ts +53 -16
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +34 -3
|
@@ -61,6 +61,7 @@ RESERVED_HANDLE_STATUS_TOOL = "__handle_status__"
|
|
|
61
61
|
RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__"
|
|
62
62
|
RESERVED_HANDLE_SEND_TOOL = "__handle_send__"
|
|
63
63
|
RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__"
|
|
64
|
+
RESERVED_PACKAGES_INSTALL_TOOL = "__packages_install__"
|
|
64
65
|
TIMEOUT_PAUSE_OP = "timeout-pause"
|
|
65
66
|
TIMEOUT_RESUME_OP = "timeout-resume"
|
|
66
67
|
|
|
@@ -660,25 +661,6 @@ class ThreadRoutedStream(io.TextIOBase):
|
|
|
660
661
|
)
|
|
661
662
|
self.cell_target: io.StringIO | None = None
|
|
662
663
|
|
|
663
|
-
@contextlib.contextmanager
|
|
664
|
-
def capture(self, buffer: io.StringIO, *, cell: bool) -> Iterator[None]:
|
|
665
|
-
me = threading.get_ident()
|
|
666
|
-
previous = self._targets.get(me)
|
|
667
|
-
self._targets[me] = buffer
|
|
668
|
-
inherited = self._inherited.set(buffer)
|
|
669
|
-
if cell:
|
|
670
|
-
self.cell_target = buffer
|
|
671
|
-
try:
|
|
672
|
-
yield
|
|
673
|
-
finally:
|
|
674
|
-
self._inherited.reset(inherited)
|
|
675
|
-
if previous is None:
|
|
676
|
-
self._targets.pop(me, None)
|
|
677
|
-
else:
|
|
678
|
-
self._targets[me] = previous
|
|
679
|
-
if cell:
|
|
680
|
-
self.cell_target = None
|
|
681
|
-
|
|
682
664
|
def write(self, text: str) -> int:
|
|
683
665
|
target = self._targets.get(threading.get_ident()) or self._inherited.get() or self.cell_target
|
|
684
666
|
if target is None:
|
|
@@ -693,6 +675,38 @@ class ThreadRoutedStream(io.TextIOBase):
|
|
|
693
675
|
return None
|
|
694
676
|
|
|
695
677
|
|
|
678
|
+
@contextlib.contextmanager
|
|
679
|
+
def capture_streams(
|
|
680
|
+
streams: tuple[ThreadRoutedStream, ThreadRoutedStream],
|
|
681
|
+
buffers: tuple[io.StringIO, io.StringIO],
|
|
682
|
+
*,
|
|
683
|
+
cell: bool,
|
|
684
|
+
) -> Iterator[None]:
|
|
685
|
+
stdout, stderr = streams
|
|
686
|
+
out, err = buffers
|
|
687
|
+
me = threading.get_ident()
|
|
688
|
+
previous_out, previous_err = stdout._targets.get(me), stderr._targets.get(me)
|
|
689
|
+
stdout._targets[me], stderr._targets[me] = out, err
|
|
690
|
+
inherited_out, inherited_err = stdout._inherited.set(out), stderr._inherited.set(err)
|
|
691
|
+
if cell:
|
|
692
|
+
stdout.cell_target, stderr.cell_target = out, err
|
|
693
|
+
try:
|
|
694
|
+
yield
|
|
695
|
+
finally:
|
|
696
|
+
stderr._inherited.reset(inherited_err)
|
|
697
|
+
stdout._inherited.reset(inherited_out)
|
|
698
|
+
if previous_err is None:
|
|
699
|
+
stderr._targets.pop(me, None)
|
|
700
|
+
else:
|
|
701
|
+
stderr._targets[me] = previous_err
|
|
702
|
+
if previous_out is None:
|
|
703
|
+
stdout._targets.pop(me, None)
|
|
704
|
+
else:
|
|
705
|
+
stdout._targets[me] = previous_out
|
|
706
|
+
if cell:
|
|
707
|
+
stdout.cell_target, stderr.cell_target = None, None
|
|
708
|
+
|
|
709
|
+
|
|
696
710
|
@dataclass
|
|
697
711
|
class _Entry:
|
|
698
712
|
name: str
|
|
@@ -704,7 +718,8 @@ class _Entry:
|
|
|
704
718
|
|
|
705
719
|
|
|
706
720
|
class Registry:
|
|
707
|
-
def __init__(self, on_change: Callable[[list[str]], None]) -> None:
|
|
721
|
+
def __init__(self, on_change: Callable[[list[str]], None], *, disabled: bool = False) -> None:
|
|
722
|
+
self._disabled = disabled
|
|
708
723
|
self._lock = threading.Lock()
|
|
709
724
|
self._entries: dict[str, _Entry] = {}
|
|
710
725
|
# Revisions outlive undefine: a redefined name never reuses a revision an old descriptor carries.
|
|
@@ -713,6 +728,8 @@ class Registry:
|
|
|
713
728
|
self.generation = 1
|
|
714
729
|
|
|
715
730
|
def define(self, fn: Any, *, name: Any = None, description: Any = None, schema: Any = None) -> Any:
|
|
731
|
+
if self._disabled:
|
|
732
|
+
raise KernelToolError("tools_unavailable", "kernel tools are turned off for this project (kernelTools.enabled is false)")
|
|
716
733
|
inferred = infer_tool(fn, name=name, description=description, schema=schema)
|
|
717
734
|
key = tool_key(inferred["name"])
|
|
718
735
|
with self._lock:
|
|
@@ -901,8 +918,7 @@ class KernelToolRunner:
|
|
|
901
918
|
if invocation.cancelled.is_set():
|
|
902
919
|
raise Cancelled(CANCELLED_BEFORE_START)
|
|
903
920
|
CURRENT.set(invocation)
|
|
904
|
-
|
|
905
|
-
with stdout.capture(output, cell=False), stderr.capture(output, cell=False):
|
|
921
|
+
with capture_streams(self._streams, (output, output), cell=False):
|
|
906
922
|
value = self._call(invocation, entry.fn, args)
|
|
907
923
|
finally:
|
|
908
924
|
CURRENT.set(None)
|
|
@@ -983,7 +999,12 @@ async def _awaited(awaitable: Any) -> Any:
|
|
|
983
999
|
|
|
984
1000
|
KERNEL_TOOL_TOKEN = OwnerToken()
|
|
985
1001
|
KERNEL_TOOL_STREAMS = (ThreadRoutedStream(stderr=False), ThreadRoutedStream(stderr=True))
|
|
986
|
-
|
|
1002
|
+
# kernelTools.enabled is read once, while the prelude loads and before any cell runs, then removed from the
|
|
1003
|
+
# environment: rewriting os.environ cannot turn kernel tools back on, and cell subprocesses never see the switch.
|
|
1004
|
+
KERNEL_TOOL_REGISTRY = Registry(
|
|
1005
|
+
lambda names: emit({"type": "kernel-tools-defined", "names": names}),
|
|
1006
|
+
disabled=os.environ.pop("SENPI_CODEMODE_KERNEL_TOOLS", None) == "0",
|
|
1007
|
+
)
|
|
987
1008
|
KERNEL_TOOL_RUNNER = KernelToolRunner(emit, KERNEL_TOOL_TOKEN, KERNEL_TOOL_REGISTRY, KERNEL_TOOL_STREAMS)
|
|
988
1009
|
|
|
989
1010
|
|
|
@@ -1154,6 +1175,27 @@ def tool_schema(name: str | None = None) -> Any:
|
|
|
1154
1175
|
)
|
|
1155
1176
|
|
|
1156
1177
|
|
|
1178
|
+
class _Packages:
|
|
1179
|
+
"""packages.install(manager, requirements, *, timeout=600): the %pip installer as a call; returns its receipt."""
|
|
1180
|
+
|
|
1181
|
+
__slots__ = ()
|
|
1182
|
+
|
|
1183
|
+
def install(self, manager: str, requirements: Any, *, timeout: float | None = None) -> Any:
|
|
1184
|
+
args: dict[str, Any] = {"manager": manager, "requirements": requirements}
|
|
1185
|
+
if timeout is not None:
|
|
1186
|
+
args["timeout"] = timeout
|
|
1187
|
+
# A long install must not hit the 60 s socket cap; the host bounds it by the timeout (600 s by default).
|
|
1188
|
+
socket_timeout = (600 if timeout is None else float(timeout)) + _WAIT_SOCKET_GRACE_SECONDS
|
|
1189
|
+
return bridge_post(
|
|
1190
|
+
"/call",
|
|
1191
|
+
{"callId": f"py-{uuid.uuid4()}", "toolName": RESERVED_PACKAGES_INSTALL_TOOL, "args": args},
|
|
1192
|
+
socket_timeout=socket_timeout,
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
packages = _Packages()
|
|
1197
|
+
|
|
1198
|
+
|
|
1157
1199
|
def output(
|
|
1158
1200
|
*ids: str,
|
|
1159
1201
|
format: str = "raw",
|
|
@@ -1781,6 +1823,7 @@ USER_NS.update(
|
|
|
1781
1823
|
"workpool": workpool,
|
|
1782
1824
|
"output": output,
|
|
1783
1825
|
"tool_schema": tool_schema,
|
|
1826
|
+
"packages": packages,
|
|
1784
1827
|
"wait": wait,
|
|
1785
1828
|
"handle": handle,
|
|
1786
1829
|
"__senpi_magic": _magic,
|
|
@@ -2187,8 +2230,7 @@ def run_cell(
|
|
|
2187
2230
|
cell_scope = CURRENT_CELL_TOKEN.set(cell_token)
|
|
2188
2231
|
try:
|
|
2189
2232
|
KERNEL_TOOL_TOKEN.acquire()
|
|
2190
|
-
|
|
2191
|
-
with cell_stdout.capture(stdout, cell=True), cell_stderr.capture(stderr, cell=True):
|
|
2233
|
+
with capture_streams(KERNEL_TOOL_STREAMS, (stdout, stderr), cell=True):
|
|
2192
2234
|
apply_preludes(preludes)
|
|
2193
2235
|
leave_source_file = _enter_source_file(source_file)
|
|
2194
2236
|
body, expression = compile_cell(code, source_file or "<cell>")
|
|
@@ -2342,7 +2384,7 @@ def _start_parent_watch() -> None:
|
|
|
2342
2384
|
).start()
|
|
2343
2385
|
|
|
2344
2386
|
|
|
2345
|
-
def _read_control(commands: queue.
|
|
2387
|
+
def _read_control(commands: queue.SimpleQueue[tuple[str, Any]]) -> None:
|
|
2346
2388
|
# Kernel-tool frames are served here, so a callback can be admitted while the main thread is inside a cell.
|
|
2347
2389
|
for raw in sys.stdin:
|
|
2348
2390
|
try:
|
|
@@ -2361,7 +2403,7 @@ def main() -> None:
|
|
|
2361
2403
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
2362
2404
|
_start_parent_watch()
|
|
2363
2405
|
sys.stdout, sys.stderr = KERNEL_TOOL_STREAMS
|
|
2364
|
-
commands: queue.
|
|
2406
|
+
commands: queue.SimpleQueue[tuple[str, Any]] = queue.SimpleQueue()
|
|
2365
2407
|
threading.Thread(target=_read_control, args=(commands,), name="senpi-control-reader", daemon=True).start()
|
|
2366
2408
|
host_closed = False
|
|
2367
2409
|
while True:
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
2
4
|
import type { Readable, Writable } from "node:stream";
|
|
3
5
|
|
|
4
6
|
export interface KernelChild {
|
|
@@ -53,6 +55,8 @@ export function defaultSpawn(options: KernelSpawnOptions): KernelChild {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export function splitCommand(commandLine: string): { readonly command: string; readonly args: readonly string[] } {
|
|
58
|
+
// A configured interpreter path is one executable even when it contains spaces.
|
|
59
|
+
if (isAbsolute(commandLine) && existsSync(commandLine)) return { command: commandLine, args: [] };
|
|
56
60
|
const [command, ...args] = commandLine.split(" ").filter(Boolean);
|
|
57
61
|
if (!command) throw new Error("Python interpreter path is empty");
|
|
58
62
|
return { command, args };
|
|
@@ -244,6 +244,10 @@ export class PythonKernelTransport {
|
|
|
244
244
|
this.#child.stderr.on("data", onStderr);
|
|
245
245
|
this.#child.on("error", onError);
|
|
246
246
|
this.#child.on("exit", onExit);
|
|
247
|
+
// senpi#3016: a frame written while the interpreter is dying fails on the stdin stream (EPIPE), not on the child.
|
|
248
|
+
// #onError already ignores it once the child has exited or the transport is inactive, and the listener is never
|
|
249
|
+
// detached, so a late write failure is never unhandled.
|
|
250
|
+
this.#child.stdin.on("error", onError);
|
|
247
251
|
const { sessionId, connection, memory, kernelGeneration } = this.#options;
|
|
248
252
|
this.#write({
|
|
249
253
|
type: "init",
|
package/src/kernels/rb/runner.rb
CHANGED
|
@@ -9,6 +9,7 @@ $stdout.flush
|
|
|
9
9
|
$__senpi_binding = TOPLEVEL_BINDING
|
|
10
10
|
$__senpi_frame_mutex = Mutex.new
|
|
11
11
|
$__senpi_current_cell = nil
|
|
12
|
+
$__senpi_memory_cell = nil
|
|
12
13
|
$__senpi_capture_cell = nil
|
|
13
14
|
$__senpi_connection = nil
|
|
14
15
|
$__senpi_frame_io = STDOUT.dup
|
|
@@ -123,8 +124,6 @@ rescue JSON::GeneratorError
|
|
|
123
124
|
value.inspect
|
|
124
125
|
end
|
|
125
126
|
|
|
126
|
-
$__senpi_memory = nil
|
|
127
|
-
|
|
128
127
|
SENPI_SIZER_SAMPLE = 1_000
|
|
129
128
|
SENPI_SIZER_NODE_BUDGET = 5_000
|
|
130
129
|
SENPI_SIZER_MAX_DEPTH = 64
|
|
@@ -239,19 +238,6 @@ rescue StandardError
|
|
|
239
238
|
[]
|
|
240
239
|
end
|
|
241
240
|
|
|
242
|
-
# The host reads the interpreter's footprint and decides on the notice; the runner only names its
|
|
243
|
-
# largest globals. Nothing here spawns a process, so the user's `$?` is left alone.
|
|
244
|
-
def __senpi_memory_report
|
|
245
|
-
return nil if $__senpi_memory.nil?
|
|
246
|
-
named = __senpi_largest_globals(5)
|
|
247
|
-
# The host replaces liveBytes with the interpreter footprint it reads itself; only globals is used.
|
|
248
|
-
report = { "liveBytes" => 0, "measure" => "footprint" }
|
|
249
|
-
report["globals"] = named unless named.empty?
|
|
250
|
-
report
|
|
251
|
-
rescue StandardError
|
|
252
|
-
nil
|
|
253
|
-
end
|
|
254
|
-
|
|
255
241
|
SENPI_NON_DISPLAY_NODES = %i[
|
|
256
242
|
LASGN IASGN GASGN CVASGN DASGN OP_ASGN OP_CDECL CDECL MASGN CASGN
|
|
257
243
|
DEFN DEFS CLASS MODULE SCLASS ALIAS UNDEF
|
|
@@ -305,8 +291,6 @@ def __senpi_run_cell(message)
|
|
|
305
291
|
"durationMs" => ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round,
|
|
306
292
|
}
|
|
307
293
|
frame["valueRepr"] = __senpi_value_repr(value) if !value.nil? && __senpi_should_display_result?(source)
|
|
308
|
-
memory = __senpi_memory_report
|
|
309
|
-
frame["memory"] = memory unless memory.nil?
|
|
310
294
|
__senpi_emit(frame)
|
|
311
295
|
rescue Exception => error
|
|
312
296
|
__senpi_emit({
|
|
@@ -319,6 +303,7 @@ def __senpi_run_cell(message)
|
|
|
319
303
|
ensure
|
|
320
304
|
$__senpi_capture_cell = nil
|
|
321
305
|
$__senpi_current_cell = nil
|
|
306
|
+
$__senpi_memory_cell = cell_id
|
|
322
307
|
end
|
|
323
308
|
end
|
|
324
309
|
|
|
@@ -332,10 +317,15 @@ $__senpi_protocol_stdin.each_line do |line|
|
|
|
332
317
|
when "init"
|
|
333
318
|
__senpi_emit({ "type" => "status", "event" => { "op" => "kernel-startup", "stage" => "host-init" } })
|
|
334
319
|
$__senpi_connection = message["connection"]
|
|
335
|
-
|
|
336
|
-
__senpi_emit({ "type" => "ready" })
|
|
320
|
+
__senpi_emit({ "type" => "ready", "memoryGlobals" => true })
|
|
337
321
|
when "run"
|
|
322
|
+
$__senpi_memory_cell = nil
|
|
338
323
|
__senpi_run_cell(message)
|
|
324
|
+
when "memory-globals"
|
|
325
|
+
if message["cellId"] == $__senpi_memory_cell && $__senpi_current_cell.nil?
|
|
326
|
+
__senpi_emit({ "type" => "memory-globals-result", "cellId" => message["cellId"], "globals" => __senpi_largest_globals(5) })
|
|
327
|
+
$__senpi_memory_cell = nil
|
|
328
|
+
end
|
|
339
329
|
when "close"
|
|
340
330
|
__senpi_emit({ "type" => "closed" })
|
|
341
331
|
exit 0
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { ResolvedSandbox } from "../../config/feature-settings.ts";
|
|
2
|
+
import { marshalToolResult } from "../../tool/image.ts";
|
|
3
|
+
import type { EvalRuntimeInfo, ExecuteTool, HostCellExecutor } from "../../tool/types.ts";
|
|
4
|
+
import type { CodemodeSandbox } from "./vendor/pi-codemode/runtime/host.ts";
|
|
5
|
+
import type { CodemodeError, CodemodeOutputFrame, CodemodeTool } from "./vendor/pi-codemode/types.ts";
|
|
6
|
+
|
|
7
|
+
export interface SandboxCellOptions {
|
|
8
|
+
readonly sandbox: ResolvedSandbox;
|
|
9
|
+
readonly executeTool: ExecuteTool;
|
|
10
|
+
readonly toolNames: () => readonly string[];
|
|
11
|
+
readonly describeTool?: (name: string) => string | undefined;
|
|
12
|
+
/** Where the QuickJS wasm lives; by default the installed `quickjs-wasi` package's file. */
|
|
13
|
+
readonly wasmPath?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The runtime an isolated cell's result reports: the QuickJS build it runs on, never the persistent kernel's (senpi#2811).
|
|
18
|
+
* Read from the installed quickjs-wasi package the wasm loads from, only when an isolated cell runs; when that package
|
|
19
|
+
* cannot be resolved the cell itself fails with eval_isolate_unavailable, and the version says so.
|
|
20
|
+
*/
|
|
21
|
+
export function sandboxRuntimeInfo(): EvalRuntimeInfo {
|
|
22
|
+
let version = "unavailable";
|
|
23
|
+
try {
|
|
24
|
+
// getBuiltinModule, not a static import: node:module would otherwise load with the extension.
|
|
25
|
+
const manifest: unknown = process.getBuiltinModule("node:module").createRequire(import.meta.url)(
|
|
26
|
+
"quickjs-wasi/package.json",
|
|
27
|
+
);
|
|
28
|
+
if (
|
|
29
|
+
typeof manifest === "object" &&
|
|
30
|
+
manifest !== null &&
|
|
31
|
+
"version" in manifest &&
|
|
32
|
+
typeof manifest.version === "string"
|
|
33
|
+
)
|
|
34
|
+
version = manifest.version;
|
|
35
|
+
} catch {
|
|
36
|
+
// Reported as "unavailable": the same missing package makes the cell fail with eval_isolate_unavailable.
|
|
37
|
+
}
|
|
38
|
+
return { name: "quickjs", version, isolation: "sandbox" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// On the script's first line so reported line numbers still match the cell; globals, so a cell's own
|
|
42
|
+
// `const print` shadows them instead of failing as a redeclaration.
|
|
43
|
+
const OUTPUT_ALIASES =
|
|
44
|
+
'globalThis.print = (...values) => text(values.map((value) => (typeof value === "string" ? value : JSON.stringify(value))).join(" ")); globalThis.display = (value) => ((typeof value === "string" && value.startsWith("data:image/")) || (value !== null && typeof value === "object" && ("image_url" in value || value.type === "image")) ? image(value) : text(typeof value === "string" ? value : JSON.stringify(value))); globalThis.tool = tools; ';
|
|
45
|
+
|
|
46
|
+
const ERROR_CODES: Partial<Record<CodemodeError["kind"], string>> = {
|
|
47
|
+
timeout: "eval_isolate_timeout",
|
|
48
|
+
aborted: "eval_isolate_aborted",
|
|
49
|
+
sandbox: "eval_isolate_unavailable",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function codeFor(error: CodemodeError): string | undefined {
|
|
53
|
+
if (error.kind === "script" && error.reason === "memory") return "eval_isolate_memory_limit";
|
|
54
|
+
if (error.kind === "script" && error.name === "CodemodeStoreDisabledError") return "eval_isolate_no_state";
|
|
55
|
+
if (error.kind === "script" && error.reason === "unresolved") return "eval_isolate_unresolved_promise";
|
|
56
|
+
return ERROR_CODES[error.kind];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The persistent kernel never sees an isolated cell; its tools go through executeTool so permission hooks still apply. */
|
|
60
|
+
export function sandboxCellExecutor(code: string, options: SandboxCellOptions): HostCellExecutor {
|
|
61
|
+
return async ({ signal, emit }) => {
|
|
62
|
+
let sandbox: CodemodeSandbox;
|
|
63
|
+
try {
|
|
64
|
+
// Lazy: the vendored QuickJS runtime loads only when a session runs its first isolated cell. The wasm is loaded
|
|
65
|
+
// here, before any of the cell runs, so a missing runtime is reported as unavailable instead of failing inside
|
|
66
|
+
// the script.
|
|
67
|
+
const runtime = await import("./vendor/pi-codemode/runtime/host.ts");
|
|
68
|
+
const { loadQuickJSWasm } = await import("./vendor/pi-codemode/wasm.ts");
|
|
69
|
+
const wasm = await loadQuickJSWasm(options.wasmPath);
|
|
70
|
+
const items = new Map<number, string[]>();
|
|
71
|
+
const tools: CodemodeTool[] = options
|
|
72
|
+
.toolNames()
|
|
73
|
+
.filter((name) => name !== "eval")
|
|
74
|
+
.map((name) => ({
|
|
75
|
+
name,
|
|
76
|
+
description: options.describeTool?.(name) ?? "",
|
|
77
|
+
execute: async (args: unknown, context: { signal: AbortSignal }) =>
|
|
78
|
+
marshalToolResult(await options.executeTool(name, args, { signal: context.signal })),
|
|
79
|
+
}));
|
|
80
|
+
sandbox = new runtime.CodemodeSandbox({
|
|
81
|
+
wasm,
|
|
82
|
+
tools,
|
|
83
|
+
timeoutMs: options.sandbox.timeoutSeconds * 1_000,
|
|
84
|
+
memoryLimitBytes: options.sandbox.memoryMb * 1024 * 1024,
|
|
85
|
+
output: "stream",
|
|
86
|
+
builtins: { store: "reject" },
|
|
87
|
+
onOutputFrame: (frame: CodemodeOutputFrame) => {
|
|
88
|
+
const parts = items.get(frame.itemId) ?? [];
|
|
89
|
+
parts.push(frame.chunk);
|
|
90
|
+
if (!frame.final) {
|
|
91
|
+
items.set(frame.itemId, parts);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
items.delete(frame.itemId);
|
|
95
|
+
const payload = parts.join("");
|
|
96
|
+
if (frame.type === "image") {
|
|
97
|
+
emit({ type: "display", mimeType: frame.mimeType ?? "image/png", dataBase64: payload });
|
|
98
|
+
} else {
|
|
99
|
+
emit({ type: "text", stream: "stdout", data: payload.endsWith("\n") ? payload : `${payload}\n` });
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// The full error, paths included, goes to the host's own log so a maintainer can diagnose it; the cell gets
|
|
105
|
+
// only what failed and the error's code. This covers every setup step: the runtime import, the tool list,
|
|
106
|
+
// the wasm load and the sandbox itself.
|
|
107
|
+
console.error("[senpi-codemode] isolated cell setup failed:", error);
|
|
108
|
+
const code =
|
|
109
|
+
error instanceof Error && "code" in error && typeof error.code === "string" ? ` (${error.code})` : "";
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: {
|
|
113
|
+
name: "SandboxUnavailableError",
|
|
114
|
+
message: `eval_isolate_unavailable: the QuickJS runtime for isolated cells could not be loaded${code}; nothing in the cell ran`,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const result = await sandbox.execute(OUTPUT_ALIASES + code, { signal });
|
|
120
|
+
if (result.ok) {
|
|
121
|
+
return result.value === undefined
|
|
122
|
+
? { ok: true }
|
|
123
|
+
: {
|
|
124
|
+
ok: true,
|
|
125
|
+
valueRepr: typeof result.value === "string" ? result.value : JSON.stringify(result.value),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const errorCode = codeFor(result.error);
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
error: {
|
|
132
|
+
...(result.error.name === undefined ? {} : { name: result.error.name }),
|
|
133
|
+
message: errorCode === undefined ? result.error.message : `${errorCode}: ${result.error.message}`,
|
|
134
|
+
...(result.error.stack === undefined ? {} : { stack: result.error.stack }),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
} finally {
|
|
138
|
+
await sandbox.close();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -40,3 +40,4 @@ A sync must re-apply both kinds: dropping a marked line (for example `"output-fr
|
|
|
40
40
|
- A normal finish settles only after every frame is consumed. An abort or timeout settles at once and drops frames still queued. The timer and the abort signal stay armed while a finished run's last frames drain, so a late abort, timeout or `close()` abandons them too (the run settles with that error): a consumer that never returns cannot keep a run alive.
|
|
41
41
|
- `frameBytes` and `windowBytes` must be at least 4, so a frame can always hold a whole surrogate pair.
|
|
42
42
|
2. **Builtin store policy** (`builtins: { store: "collect" | "reject" }`; `types.ts`, `runtime/protocol.ts`, `runtime/worker.ts`, `runtime/host.ts`, `runtime/prelude-source.ts`). With `"reject"`, `load()` returns `undefined` and `store()` throws `CodemodeStoreDisabledError`. The names stay defined, so scripts written for upstream fail with a clear message instead of a ReferenceError.
|
|
43
|
+
3. **Runtime-set error reasons** (`types.ts` `CodemodeError.reason`, `runtime/prelude-source.ts`, `runtime/worker.ts`). The prelude captures the engine's own `InternalError` before user code runs and tags a failure `reason: "memory"` only for an instance of it whose message is exactly "out of memory"; its stalled-promise failure is tagged `reason: "unresolved"`; the worker tags an engine out-of-memory from `evalCode` the same way; and `text()` rethrows the engine's own error instead of replacing it with a `TypeError`. The host maps the isolate error codes from `reason` only, never from message text. Upstream has no `reason`, so its behaviour is unchanged.
|
|
@@ -115,7 +115,7 @@ class Execution {
|
|
|
115
115
|
private finished = false;
|
|
116
116
|
// senpi-change begin: output streaming
|
|
117
117
|
private readonly stream: StreamOptions | undefined;
|
|
118
|
-
private readonly credits: Int32Array | undefined;
|
|
118
|
+
private readonly credits: Int32Array<SharedArrayBuffer> | undefined;
|
|
119
119
|
private frameChain: Promise<void> = Promise.resolve();
|
|
120
120
|
private frames = 0;
|
|
121
121
|
private inFlightBytes = 0;
|
|
@@ -188,7 +188,7 @@ class Execution {
|
|
|
188
188
|
// senpi-change begin: output streaming and builtin policy
|
|
189
189
|
...(this.credits === undefined || this.stream === undefined
|
|
190
190
|
? {}
|
|
191
|
-
: { stream: { credits: this.credits.buffer
|
|
191
|
+
: { stream: { credits: this.credits.buffer, frameBytes: this.stream.frameBytes } }),
|
|
192
192
|
storePolicy: options.storePolicy,
|
|
193
193
|
// senpi-change end
|
|
194
194
|
};
|
|
@@ -46,6 +46,10 @@ export const PRELUDE_SOURCE: string = `(function (bridge, toolsJson, globalsJson
|
|
|
46
46
|
const options = optionsJson === undefined ? {} : JSON.parse(optionsJson);
|
|
47
47
|
const streamOutput = options.streamOutput === true;
|
|
48
48
|
const rejectStore = options.store === "reject";
|
|
49
|
+
// The engine's own InternalError, captured before user code runs: a failure is tagged by identity, never by its
|
|
50
|
+
// text, so a cell's own error that merely mentions memory keeps its own code. A script can still construct this
|
|
51
|
+
// constructor itself and so label its own failure as the memory limit; that mislabels only its own cell.
|
|
52
|
+
const EngineInternalError = typeof InternalError === "function" ? InternalError : undefined;
|
|
49
53
|
// senpi-change end
|
|
50
54
|
"use strict";
|
|
51
55
|
const stringify = JSON.stringify;
|
|
@@ -94,7 +98,13 @@ export const PRELUDE_SOURCE: string = `(function (bridge, toolsJson, globalsJson
|
|
|
94
98
|
|
|
95
99
|
function describeError(error) {
|
|
96
100
|
if (error instanceof ErrorCtor) {
|
|
97
|
-
|
|
101
|
+
// senpi-change begin: the engine's out-of-memory error is reported by identity, not by matching its message
|
|
102
|
+
const reason =
|
|
103
|
+
EngineInternalError !== undefined && error instanceof EngineInternalError && error.message === "out of memory"
|
|
104
|
+
? "memory"
|
|
105
|
+
: undefined;
|
|
106
|
+
return stringify({ name: error.name, message: error.message, stack: errorText(error), reason });
|
|
107
|
+
// senpi-change end
|
|
98
108
|
}
|
|
99
109
|
return stringify({ message: format(error) });
|
|
100
110
|
}
|
|
@@ -285,6 +295,8 @@ export const PRELUDE_SOURCE: string = `(function (bridge, toolsJson, globalsJson
|
|
|
285
295
|
try {
|
|
286
296
|
rendered = outputText(value);
|
|
287
297
|
} catch (error) {
|
|
298
|
+
// The engine's own out-of-memory error keeps its identity, so it is still reported as the memory limit.
|
|
299
|
+
if (EngineInternalError !== undefined && error instanceof EngineInternalError) throw error; // senpi-change
|
|
288
300
|
throw new TypeErrorCtor(error instanceof ErrorCtor ? error.message : String(error));
|
|
289
301
|
}
|
|
290
302
|
output("text", rendered);
|
|
@@ -423,6 +435,7 @@ export const PRELUDE_SOURCE: string = `(function (bridge, toolsJson, globalsJson
|
|
|
423
435
|
name: "Error",
|
|
424
436
|
message:
|
|
425
437
|
"The script is waiting on a promise that can never settle: no tool call is pending, and timers do not exist here.",
|
|
438
|
+
reason: "unresolved", // senpi-change: tagged by the runtime, so a script's own error text never matches it
|
|
426
439
|
}),
|
|
427
440
|
);
|
|
428
441
|
return true;
|
|
@@ -48,7 +48,10 @@ function discardOutput(memory: { readonly buffer: ArrayBufferLike }) {
|
|
|
48
48
|
function describeException(error: JSException): string {
|
|
49
49
|
const head = error.message ? `${error.name}: ${error.message}` : error.name;
|
|
50
50
|
const stack = error.stack?.trimEnd();
|
|
51
|
-
|
|
51
|
+
// senpi-change begin: evalCode fails before any script runs, so an InternalError here is the engine's own
|
|
52
|
+
const reason = error.name === "InternalError" && error.message === "out of memory" ? "memory" : undefined;
|
|
53
|
+
return JSON.stringify({ name: error.name, message: error.message, stack: stack ? `${head}\n${stack}` : head, reason });
|
|
54
|
+
// senpi-change end
|
|
52
55
|
}
|
|
53
56
|
// senpi-change begin: output streaming
|
|
54
57
|
/**
|
|
@@ -71,6 +71,8 @@ export interface CodemodeError {
|
|
|
71
71
|
name?: string;
|
|
72
72
|
message: string;
|
|
73
73
|
stack?: string;
|
|
74
|
+
/** senpi-change: set by the runtime itself (never from script text) for its own memory-limit and stalled failures. */
|
|
75
|
+
reason?: "memory" | "unresolved";
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
/** Keys the script changed with `store()`. Only successful executions report writes. */
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one read of `bun:ffi` behind a guard: `process.getBuiltinModule` returns the module on Bun and
|
|
3
|
+
* `undefined` on Node, so this module loads on either runtime, and only a result carrying the callable
|
|
4
|
+
* surface the CPU readers bind is treated as FFI - the same boundary `display-js.ts` uses for `Bun`.
|
|
5
|
+
*/
|
|
6
|
+
export type BunFfi = {
|
|
7
|
+
readonly dlopen: <S>(path: string, definitions: Record<string, unknown>) => { readonly symbols: S };
|
|
8
|
+
readonly FFIType: Record<string, number>;
|
|
9
|
+
readonly ptr: (buffer: ArrayBufferView) => number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** True when `value` carries the `bun:ffi` surface the kernel CPU readers use (`dlopen`, `FFIType`, `ptr`). */
|
|
13
|
+
export function isBunFfi(value: unknown): value is BunFfi {
|
|
14
|
+
if (typeof value !== "object" || value === null) return false;
|
|
15
|
+
if (!("dlopen" in value) || typeof value.dlopen !== "function") return false;
|
|
16
|
+
if (!("ptr" in value) || typeof value.ptr !== "function") return false;
|
|
17
|
+
return "FFIType" in value && typeof value.FFIType === "object" && value.FFIType !== null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The runtime's `bun:ffi` module, or `undefined` on Node and on any runtime whose builtin is not one. */
|
|
21
|
+
export function loadBunFfi(): BunFfi | undefined {
|
|
22
|
+
const ffi: unknown = process.getBuiltinModule("bun:ffi");
|
|
23
|
+
return isBunFfi(ffi) ? ffi : undefined;
|
|
24
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { HostCellExecutor } from "../../tool/types.ts";
|
|
3
|
+
|
|
4
|
+
type HostCellOutcome = Awaited<ReturnType<HostCellExecutor>>;
|
|
5
|
+
type HostResult = Extract<KernelToHostMessage, { type: "result" }>;
|
|
6
|
+
|
|
7
|
+
export interface HostCellRun {
|
|
8
|
+
readonly cellId: string;
|
|
9
|
+
readonly abort: AbortController;
|
|
10
|
+
readonly done: Promise<HostResult>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runs a host entry (a cell the host executes, such as an install) for one queue slot: output after the outcome or after
|
|
15
|
+
* an abort belongs to no cell, a synchronous throw stays in this cell, and `settle` receives the cell's result.
|
|
16
|
+
* `settleAfterAbort` says whether an aborted entry still settles here or whoever aborted it settles the cell.
|
|
17
|
+
*/
|
|
18
|
+
export function runHostCell(
|
|
19
|
+
cellId: string,
|
|
20
|
+
host: HostCellExecutor,
|
|
21
|
+
io: {
|
|
22
|
+
readonly emit: (message: KernelToHostMessage) => void;
|
|
23
|
+
readonly settle: (result: HostResult) => void;
|
|
24
|
+
readonly durationMs: () => number;
|
|
25
|
+
readonly settleAfterAbort: boolean;
|
|
26
|
+
},
|
|
27
|
+
): HostCellRun {
|
|
28
|
+
const abort = new AbortController();
|
|
29
|
+
let finished = false;
|
|
30
|
+
const emit = (message: KernelToHostMessage): void => {
|
|
31
|
+
if (!finished && !abort.signal.aborted) io.emit(message);
|
|
32
|
+
};
|
|
33
|
+
const done = new Promise<HostCellOutcome>((resolve) => resolve(host({ signal: abort.signal, emit }))).then(
|
|
34
|
+
(outcome): HostResult =>
|
|
35
|
+
outcome.ok
|
|
36
|
+
? {
|
|
37
|
+
type: "result",
|
|
38
|
+
cellId,
|
|
39
|
+
ok: true,
|
|
40
|
+
durationMs: io.durationMs(),
|
|
41
|
+
...(outcome.valueRepr === undefined ? {} : { valueRepr: outcome.valueRepr }),
|
|
42
|
+
}
|
|
43
|
+
: { type: "result", cellId, ok: false, error: outcome.error, durationMs: 0 },
|
|
44
|
+
(error: unknown): HostResult => ({
|
|
45
|
+
type: "result",
|
|
46
|
+
cellId,
|
|
47
|
+
ok: false,
|
|
48
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
49
|
+
durationMs: 0,
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
void done.then((result) => {
|
|
53
|
+
finished = true;
|
|
54
|
+
if (io.settleAfterAbort || !abort.signal.aborted) io.settle(result);
|
|
55
|
+
});
|
|
56
|
+
return { cellId, abort, done };
|
|
57
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { KernelMemoryThresholds } from "../../bridge/memory-protocol.ts";
|
|
1
|
+
import type { KernelMemoryReport, KernelMemoryThresholds } from "../../bridge/memory-protocol.ts";
|
|
2
2
|
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
3
3
|
import type { EvalLanguage } from "../../tool/types.ts";
|
|
4
4
|
import { KernelMemoryPolicy } from "./kernel-memory.ts";
|
|
@@ -11,7 +11,7 @@ export type FootprintReader = (pid: number) => { readonly bytes: number } | unde
|
|
|
11
11
|
export interface KernelMemoryHostOptions {
|
|
12
12
|
/**
|
|
13
13
|
* Host-measured kernel (rb, jl): the host reads the interpreter's footprint after each result and
|
|
14
|
-
* replaces the runner's live reading with it; the runner
|
|
14
|
+
* replaces the runner's live reading with it; the runner names its largest globals on demand, so the
|
|
15
15
|
* notice and the ceiling both apply, with every reading counting (no collection precedes it).
|
|
16
16
|
*/
|
|
17
17
|
readonly readFootprint?: FootprintReader;
|
|
@@ -36,12 +36,35 @@ export class KernelMemoryHost {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
annotate(result: ResultMessage, pid?: number): ResultMessage {
|
|
39
|
+
return this.annotateReport(result, this.readReport(result, pid));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
readReport(result: ResultMessage, pid?: number): KernelMemoryReport | undefined {
|
|
39
43
|
const runnerReport = result.memory;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
return this.#readFootprint === undefined
|
|
45
|
+
? runnerReport
|
|
46
|
+
: this.#footprintReport(this.#readFootprint, pid, runnerReport);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
needsGlobals(report: KernelMemoryReport): boolean {
|
|
50
|
+
const { noticeBytes, ceilingBytes } = this.#policy.thresholds;
|
|
51
|
+
// Collect at or above the host-read notice threshold or ceiling, regardless of notice hysteresis;
|
|
52
|
+
// never collect below both thresholds. Zero disables a threshold, not the lazy diagnostic gate.
|
|
53
|
+
return (
|
|
54
|
+
(noticeBytes > 0 && report.liveBytes >= noticeBytes) || (ceilingBytes > 0 && report.liveBytes >= ceilingBytes)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
annotateReport(result: ResultMessage, report: KernelMemoryReport | undefined): ResultMessage {
|
|
59
|
+
if (report === undefined) return result;
|
|
60
|
+
const memory = this.#policy.annotate(report);
|
|
61
|
+
// Safety accounting still observes every cell, but ordinary host-measured results carry no payload.
|
|
62
|
+
// A recycle announcement remains visible even when the replacement process is below threshold.
|
|
63
|
+
if (this.#readFootprint !== undefined && !this.needsGlobals(report) && memory.notice === undefined) {
|
|
64
|
+
const { memory: _memory, ...ordinary } = result;
|
|
65
|
+
return ordinary;
|
|
66
|
+
}
|
|
67
|
+
return { ...result, memory };
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
/**
|